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:
2026-08-10 08:03:02 -04:00
parent 537330e7e0
commit 598d70f735
6 changed files with 559 additions and 68 deletions
+19
View File
@@ -303,3 +303,22 @@ func formatDateForInput(d pgtype.Date) string {
}
return d.Time.Format("2006-01-02")
}
// GroupTunableSettings splits a flat, group-sorted entry list into labeled
// sub-section groups, separated into "live" (applies immediately) and
// "restart required" buckets. Entries keep their original order so groups stay
// coherent.
func GroupTunableSettings(entries []SettingEntry) (live, restart []SettingGroup) {
var liveGroups, restartGroups []SettingGroup
for _, e := range entries {
target := &liveGroups
if e.RequiresRestart {
target = &restartGroups
}
if len(*target) == 0 || (*target)[len(*target)-1].Name != e.Group {
*target = append(*target, SettingGroup{Name: e.Group})
}
(*target)[len(*target)-1].Entries = append((*target)[len(*target)-1].Entries, e)
}
return liveGroups, restartGroups
}