feat(admin): editable tunable settings UI with grouped sub-sections
Replace the read-only "System Information" card (which listed hardcoded values) with editable HTMX forms, organized so the live vs restart distinction and related settings are visually clear. admin_settings.templ: - AdminSettings signature now takes liveGroups and restartGroups ([]SettingGroup) instead of a flat entry list. - Remove the static System Information list. Render two cards: "Live" (green, applies immediately) and "Restart Required" (warning header, saved but only takes effect after restart). - Within each card, TunableSettingsSection clusters entries into labeled sub-sections by Group (e.g. "Password Quality", "Device Rate Limits", "Login Lockout", "Worker Pool") with uppercase tracked sub-headers. - TunableSettingRow renders an inline HTMX form per setting: a Yes/No select for bools, a number input with min/max for ints, text otherwise, posting to /admin/settings/tunable. Rows show "modified from default" when the value differs from the compiled default. types.go: - Add SettingEntry (template-local mirror of database.SettingEntry, keeps templates from importing database) and SettingGroup. utils.go: - Add GroupTunableSettings: splits a flat, group-sorted entry list into live and restart []SettingGroup buckets preserving source order. utils_test.go covers the multi-group + empty cases. frontend.go: - The /admin/settings page handler now loads entries from the registry, drops the three keys that have dedicated UI cards (default_timezone dropdown, scan_poll_interval_seconds, auto_scan_enabled) so they are not listed twice, groups the rest, and passes liveGroups/restartGroups into the template.
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
package templates
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestGroupTunableSettings(t *testing.T) {
|
||||
entries := []SettingEntry{
|
||||
{Key: "session_duration_seconds", Group: "Session", RequiresRestart: false},
|
||||
{Key: "password_min_length", Group: "Password Quality", RequiresRestart: false},
|
||||
{Key: "password_require_upper", Group: "Password Quality", RequiresRestart: false},
|
||||
{Key: "opds_default_page_size", Group: "OPDS Catalog", RequiresRestart: false},
|
||||
{Key: "auth_rate_limit_per_min", Group: "Auth Rate Limiting", RequiresRestart: true},
|
||||
{Key: "login_max_attempts", Group: "Login Lockout", RequiresRestart: true},
|
||||
{Key: "login_lockout_minutes", Group: "Login Lockout", RequiresRestart: true},
|
||||
}
|
||||
|
||||
live, restart := GroupTunableSettings(entries)
|
||||
|
||||
if len(live) != 3 {
|
||||
t.Fatalf("expected 3 live groups, got %d", len(live))
|
||||
}
|
||||
if live[0].Name != "Session" || len(live[0].Entries) != 1 {
|
||||
t.Errorf("live[0] = %+v", live[0])
|
||||
}
|
||||
if live[1].Name != "Password Quality" || len(live[1].Entries) != 2 {
|
||||
t.Errorf("live[1] = %+v", live[1])
|
||||
}
|
||||
if live[2].Name != "OPDS Catalog" || len(live[2].Entries) != 1 {
|
||||
t.Errorf("live[2] = %+v", live[2])
|
||||
}
|
||||
|
||||
if len(restart) != 2 {
|
||||
t.Fatalf("expected 2 restart groups, got %d", len(restart))
|
||||
}
|
||||
if restart[0].Name != "Auth Rate Limiting" || len(restart[0].Entries) != 1 {
|
||||
t.Errorf("restart[0] = %+v", restart[0])
|
||||
}
|
||||
if restart[1].Name != "Login Lockout" || len(restart[1].Entries) != 2 {
|
||||
t.Errorf("restart[1] = %+v", restart[1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestGroupTunableSettingsEmpty(t *testing.T) {
|
||||
live, restart := GroupTunableSettings(nil)
|
||||
if len(live) != 0 || len(restart) != 0 {
|
||||
t.Errorf("expected empty groups, got live=%d restart=%d", len(live), len(restart))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user