diff --git a/internal/router/frontend.go b/internal/router/frontend.go
index 70f8154..d8741d6 100644
--- a/internal/router/frontend.go
+++ b/internal/router/frontend.go
@@ -1024,8 +1024,38 @@ func registerFrontendRoutes(cfg *Config) {
}
}
+ // Load tunable settings entries from the registry. Exclude keys that
+ // already have their own dedicated UI cards (timezone dropdown, scan
+ // settings) so they aren't listed twice.
+ dedicatedUI := map[string]bool{
+ "default_timezone": true,
+ "scan_poll_interval_seconds": true,
+ "auto_scan_enabled": true,
+ }
+ var tunableSettings []templates.SettingEntry
+ if cfg.Settings != nil {
+ for _, e := range cfg.Settings.All() {
+ if dedicatedUI[e.Key] {
+ continue
+ }
+ tunableSettings = append(tunableSettings, templates.SettingEntry{
+ Key: e.Key,
+ Value: e.Value,
+ Type: e.Type,
+ Min: e.Min,
+ Max: e.Max,
+ RequiresRestart: e.RequiresRestart,
+ Category: e.Category,
+ Group: e.Group,
+ Description: e.Description,
+ IsDefault: e.IsDefault,
+ })
+ }
+ }
+ liveGroups, restartGroups := templates.GroupTunableSettings(tunableSettings)
+
var buf bytes.Buffer
- err = templates.AdminSettings(user, systemConfig, scanSettings, "").Render(ctx, &buf)
+ err = templates.AdminSettings(user, systemConfig, scanSettings, liveGroups, restartGroups, "").Render(ctx, &buf)
if err != nil {
return err
}
diff --git a/templates/admin_settings.templ b/templates/admin_settings.templ
index 3888b30..2c5240b 100644
--- a/templates/admin_settings.templ
+++ b/templates/admin_settings.templ
@@ -2,7 +2,7 @@ package templates
import "fmt"
-templ AdminSettings(user User, systemConfig map[string]string, scanSettings ScanSettingsData, errorMessage string) {
+templ AdminSettings(user User, systemConfig map[string]string, scanSettings ScanSettingsData, liveGroups []SettingGroup, restartGroups []SettingGroup, errorMessage string) {
@@ -107,61 +107,10 @@ templ AdminSettings(user User, systemConfig map[string]string, scanSettings Scan
Device Sync: { systemConfig["base_url"] }/api/sync
- @ScanSettingsSection(scanSettings)
-
-
- @Icon("info", "h-5 w-5 shrink-0")
-
System Information
-
-
These values are hardcoded and require a code change to modify.
-
-
- Session Duration
- 7 days
-
-
- Password Requirements
- 8+ chars, upper/lower/number/special
-
-
- Login Lockout
- 5 attempts / 15 min
-
-
- Auth Rate Limit
- 10 requests/min
-
-
- Device Rate Limits
- Sync 60/min, Progress 120/min, Metadata 30/min
-
-
- Sync Queue
- Every 5s, batch of 50
-
-
- Annotation Tombstone TTL
- 30 days
-
-
- Worker Pool
- 3 workers, queue cap 100
-
-
- OPDS Page Size
- 50 per page (max 200)
-
-
- Conversion Cache TTL
- 24 hours
-
-
- CORS Origins
- *
-
-
-
-
+ @ScanSettingsSection(scanSettings)
+ @TunableSettingsSection(liveGroups, false)
+ @TunableSettingsSection(restartGroups, true)
+
@@ -219,3 +168,89 @@ templ ScanSettingsSection(scanSettings ScanSettingsData) {
}
+
+// TunableSettingsSection renders the editable tunables for a given bucket
+// (live vs restart-required). Within the card, settings are clustered into
+// labeled sub-sections by Group (e.g. "Password Quality", "Device Rate Limits").
+templ TunableSettingsSection(groups []SettingGroup, restartRequired bool) {
+
+
+ if restartRequired {
+ @Icon("alert", "h-5 w-5 shrink-0")
+
Tunable Settings — Restart Required
+ } else {
+ @Icon("settings", "h-5 w-5 shrink-0")
+ Tunable Settings — Live
+ }
+
+ if restartRequired {
+
Changes are saved immediately but only take effect after the server restarts.
+ } else {
+
Changes apply immediately — no restart needed.
+ }
+ for _, g := range groups {
+
+
{ g.Name }
+
+ for _, e := range g.Entries {
+ @TunableSettingRow(e)
+ }
+
+
+ }
+
+}
+
+// TunableSettingRow renders a single editable setting as an inline HTMX form.
+templ TunableSettingRow(e SettingEntry) {
+
+
+
+ if !e.IsDefault {
+
{ e.Key } — modified from default
+ } else {
+
{ e.Key }
+ }
+
+
+
+
+}
diff --git a/templates/admin_settings_templ.go b/templates/admin_settings_templ.go
index d3e4667..f9ff465 100644
--- a/templates/admin_settings_templ.go
+++ b/templates/admin_settings_templ.go
@@ -10,7 +10,7 @@ import templruntime "github.com/a-h/templ/runtime"
import "fmt"
-func AdminSettings(user User, systemConfig map[string]string, scanSettings ScanSettingsData, errorMessage string) templ.Component {
+func AdminSettings(user User, systemConfig map[string]string, scanSettings ScanSettingsData, liveGroups []SettingGroup, restartGroups []SettingGroup, errorMessage string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
@@ -410,15 +410,15 @@ func AdminSettings(user User, systemConfig map[string]string, scanSettings ScanS
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "")
+ templ_7745c5c3_Err = TunableSettingsSection(liveGroups, false).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = Icon("info", "h-5 w-5 shrink-0").Render(ctx, templ_7745c5c3_Buffer)
+ templ_7745c5c3_Err = TunableSettingsSection(restartGroups, true).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "
System Information
These values are hardcoded and require a code change to modify.
Session Duration 7 days
Password Requirements 8+ chars, upper/lower/number/special
Login Lockout 5 attempts / 15 min
Auth Rate Limit 10 requests/min
Device Rate Limits Sync 60/min, Progress 120/min, Metadata 30/min
Sync Queue Every 5s, batch of 50
Annotation Tombstone TTL 30 days
Worker Pool 3 workers, queue cap 100
OPDS Page Size 50 per page (max 200)
Conversion Cache TTL 24 hours
CORS Origins *