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:
@@ -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) {
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
@@ -107,61 +107,10 @@ templ AdminSettings(user User, systemConfig map[string]string, scanSettings Scan
|
||||
<p><span class="font-medium uppercase tracking-wide text-xs" style="color: var(--text-secondary)">Device Sync:</span> { systemConfig["base_url"] }/api/sync</p>
|
||||
</div>
|
||||
</div>
|
||||
@ScanSettingsSection(scanSettings)
|
||||
<div class="mt-6 card p-6">
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
@Icon("info", "h-5 w-5 shrink-0")
|
||||
<h3 class="text-lg font-semibold" style="color: var(--text-primary)">System Information</h3>
|
||||
</div>
|
||||
<p class="text-sm mb-4" style="color: var(--text-secondary)">These values are hardcoded and require a code change to modify.</p>
|
||||
<div class="divide-y" style="--tw-divide-opacity:1;">
|
||||
<div class="flex justify-between items-center py-3" style="border-color: color-mix(in srgb, var(--text-primary) 7%, transparent);">
|
||||
<span class="text-sm font-medium" style="color: var(--text-primary)">Session Duration</span>
|
||||
<span class="text-sm" style="color: var(--text-secondary)">7 days</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center py-3" style="border-color: color-mix(in srgb, var(--text-primary) 7%, transparent);">
|
||||
<span class="text-sm font-medium" style="color: var(--text-primary)">Password Requirements</span>
|
||||
<span class="text-sm" style="color: var(--text-secondary)">8+ chars, upper/lower/number/special</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center py-3" style="border-color: color-mix(in srgb, var(--text-primary) 7%, transparent);">
|
||||
<span class="text-sm font-medium" style="color: var(--text-primary)">Login Lockout</span>
|
||||
<span class="text-sm" style="color: var(--text-secondary)">5 attempts / 15 min</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center py-3" style="border-color: color-mix(in srgb, var(--text-primary) 7%, transparent);">
|
||||
<span class="text-sm font-medium" style="color: var(--text-primary)">Auth Rate Limit</span>
|
||||
<span class="text-sm" style="color: var(--text-secondary)">10 requests/min</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center py-3" style="border-color: color-mix(in srgb, var(--text-primary) 7%, transparent);">
|
||||
<span class="text-sm font-medium" style="color: var(--text-primary)">Device Rate Limits</span>
|
||||
<span class="text-sm" style="color: var(--text-secondary)">Sync 60/min, Progress 120/min, Metadata 30/min</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center py-3" style="border-color: color-mix(in srgb, var(--text-primary) 7%, transparent);">
|
||||
<span class="text-sm font-medium" style="color: var(--text-primary)">Sync Queue</span>
|
||||
<span class="text-sm" style="color: var(--text-secondary)">Every 5s, batch of 50</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center py-3" style="border-color: color-mix(in srgb, var(--text-primary) 7%, transparent);">
|
||||
<span class="text-sm font-medium" style="color: var(--text-primary)">Annotation Tombstone TTL</span>
|
||||
<span class="text-sm" style="color: var(--text-secondary)">30 days</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center py-3" style="border-color: color-mix(in srgb, var(--text-primary) 7%, transparent);">
|
||||
<span class="text-sm font-medium" style="color: var(--text-primary)">Worker Pool</span>
|
||||
<span class="text-sm" style="color: var(--text-secondary)">3 workers, queue cap 100</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center py-3" style="border-color: color-mix(in srgb, var(--text-primary) 7%, transparent);">
|
||||
<span class="text-sm font-medium" style="color: var(--text-primary)">OPDS Page Size</span>
|
||||
<span class="text-sm" style="color: var(--text-secondary)">50 per page (max 200)</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center py-3" style="border-color: color-mix(in srgb, var(--text-primary) 7%, transparent);">
|
||||
<span class="text-sm font-medium" style="color: var(--text-primary)">Conversion Cache TTL</span>
|
||||
<span class="text-sm" style="color: var(--text-secondary)">24 hours</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center py-3">
|
||||
<span class="text-sm font-medium" style="color: var(--text-primary)">CORS Origins</span>
|
||||
<span class="text-sm" style="color: var(--text-secondary)">*</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ScanSettingsSection(scanSettings)
|
||||
@TunableSettingsSection(liveGroups, false)
|
||||
@TunableSettingsSection(restartGroups, true)
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -219,3 +168,89 @@ templ ScanSettingsSection(scanSettings ScanSettingsData) {
|
||||
</form>
|
||||
</div>
|
||||
}
|
||||
|
||||
// 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) {
|
||||
<div class="mt-6 card p-6">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
if restartRequired {
|
||||
@Icon("alert", "h-5 w-5 shrink-0")
|
||||
<h3 class="text-lg font-semibold" style="color: var(--text-primary)">Tunable Settings — Restart Required</h3>
|
||||
} else {
|
||||
@Icon("settings", "h-5 w-5 shrink-0")
|
||||
<h3 class="text-lg font-semibold" style="color: var(--text-primary)">Tunable Settings — Live</h3>
|
||||
}
|
||||
</div>
|
||||
if restartRequired {
|
||||
<p class="text-sm mb-4" style="color: var(--status-warning)">Changes are saved immediately but only take effect after the server restarts.</p>
|
||||
} else {
|
||||
<p class="text-sm mb-4" style="color: var(--text-secondary)">Changes apply immediately — no restart needed.</p>
|
||||
}
|
||||
for _, g := range groups {
|
||||
<div class="mt-5 first:mt-0">
|
||||
<h4 class="text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary)">{ g.Name }</h4>
|
||||
<div>
|
||||
for _, e := range g.Entries {
|
||||
@TunableSettingRow(e)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
// TunableSettingRow renders a single editable setting as an inline HTMX form.
|
||||
templ TunableSettingRow(e SettingEntry) {
|
||||
<div class="flex flex-col sm:flex-row sm:items-center gap-3 py-3" style="border-top: 1px solid color-mix(in srgb, var(--text-primary) 7%, transparent);">
|
||||
<div class="flex-1 min-w-0">
|
||||
<label class="block text-sm font-medium" style="color: var(--text-primary)">{ e.Description }</label>
|
||||
if !e.IsDefault {
|
||||
<p class="text-xs mt-0.5" style="color: var(--text-secondary)">{ e.Key } — modified from default</p>
|
||||
} else {
|
||||
<p class="text-xs mt-0.5" style="color: var(--text-secondary)">{ e.Key }</p>
|
||||
}
|
||||
</div>
|
||||
<form
|
||||
class="flex items-center gap-2 shrink-0"
|
||||
hx-put="/admin/settings/tunable"
|
||||
hx-target={ "#status-" + e.Key }
|
||||
hx-swap="innerHTML"
|
||||
hx-disinherit="*"
|
||||
>
|
||||
<input type="hidden" name="key" value={ e.Key }/>
|
||||
if e.Type == "bool" {
|
||||
<select name="value" class="input py-1.5 text-sm w-28">
|
||||
<option value="true" selected?={ e.Value == "true" }>Yes</option>
|
||||
<option value="false" selected?={ e.Value != "true" }>No</option>
|
||||
</select>
|
||||
} else if e.Type == "int" {
|
||||
<input
|
||||
type="number"
|
||||
name="value"
|
||||
value={ e.Value }
|
||||
if e.Min != "" {
|
||||
min={ e.Min }
|
||||
}
|
||||
if e.Max != "" {
|
||||
max={ e.Max }
|
||||
}
|
||||
class="input py-1.5 text-sm w-32"
|
||||
/>
|
||||
} else {
|
||||
<input
|
||||
type="text"
|
||||
name="value"
|
||||
value={ e.Value }
|
||||
class="input py-1.5 text-sm w-40"
|
||||
/>
|
||||
}
|
||||
<button type="submit" class="btn btn-secondary px-3 py-1.5 text-sm">
|
||||
@Icon("save", "h-3.5 w-3.5")
|
||||
Save
|
||||
</button>
|
||||
</form>
|
||||
<span id={ "status-" + e.Key } class="text-xs w-24 text-right" style="color: var(--text-secondary)"></span>
|
||||
</div>
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user