Bring the tighten-ui brand treatment into new-ui:
- Replace the emoji book (📚) in the sidebar header with the book-open
icon rendered in the theme accent color, matching the tighten-ui
header brand (templates/header.templ).
- Add web/static/favicon.svg (book-open glyph, tokyo-night accent
#7aa2f7 stroke) and reference it from the <head> of all 27 page
templates, so the favicon is present on login/setup/error pages too.
- Regenerate templ output for all affected templates.
258 lines
12 KiB
Templ
258 lines
12 KiB
Templ
package templates
|
||
|
||
import "fmt"
|
||
|
||
templ AdminSettings(user User, systemConfig map[string]string, scanSettings ScanSettingsData, liveGroups []SettingGroup, restartGroups []SettingGroup, errorMessage string) {
|
||
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8"/>
|
||
<title>System Settings - Bookhoard</title>
|
||
<script src="/static/htmx.min.js"></script>
|
||
<link href="/static/style.css" rel="stylesheet"/>
|
||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg"/>
|
||
</head>
|
||
<body class="theme-{ user.Theme }">
|
||
@Header(user, "/admin/settings")
|
||
<main class="p-8">
|
||
<div class="max-w-4xl">
|
||
<div class="mb-8">
|
||
<div class="flex items-center gap-3 mb-1">
|
||
<span class="grid place-items-center h-10 w-10 rounded-xl" style="background-color: var(--accent-muted); color: var(--accent);">
|
||
@Icon("gear", "h-5 w-5")
|
||
</span>
|
||
<h1 class="text-2xl font-bold tracking-tight" style="color: var(--text-primary)">System Settings</h1>
|
||
</div>
|
||
<p class="text-sm" style="color: var(--text-secondary)">Configure your Bookhoard instance</p>
|
||
</div>
|
||
if errorMessage != "" {
|
||
<div class="mb-6 p-4 rounded-xl border flex items-start gap-3" style="background-color: color-mix(in srgb, var(--status-danger) 12%, var(--bg-secondary)); border-color: var(--status-danger); color: var(--status-danger);">
|
||
@Icon("alert", "h-5 w-5 shrink-0 mt-0.5")
|
||
<span>{ errorMessage }</span>
|
||
</div>
|
||
}
|
||
<form id="settings-form" hx-put="/api/system/config" hx-target="#settings-form" hx-swap="outerHTML">
|
||
<div class="card p-6">
|
||
<div class="flex items-center gap-2 mb-6">
|
||
@Icon("globe", "h-5 w-5 shrink-0")
|
||
<h3 class="text-lg font-semibold" style="color: var(--text-primary)">Base URL</h3>
|
||
</div>
|
||
<div>
|
||
<label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Base URL</label>
|
||
<input
|
||
type="url"
|
||
name="base_url"
|
||
value={ systemConfig["base_url"] }
|
||
placeholder="https://books.example.com"
|
||
class="input"
|
||
required
|
||
/>
|
||
<p class="text-sm mt-2" style="color: var(--text-secondary)">The public URL of your Bookhoard instance (e.g., https://books.example.com). Used for device sync, OPDS, and API endpoints.</p>
|
||
</div>
|
||
<div class="mt-6 flex justify-end">
|
||
<button type="submit" class="btn btn-primary">
|
||
@Icon("save", "h-4 w-4")
|
||
Save Settings
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="mt-6 card p-6">
|
||
<div class="flex items-center gap-2 mb-6">
|
||
@Icon("clock", "h-5 w-5 shrink-0")
|
||
<h3 class="text-lg font-semibold" style="color: var(--text-primary)">System Defaults</h3>
|
||
</div>
|
||
<div>
|
||
<label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Default Timezone</label>
|
||
<select
|
||
name="default_timezone"
|
||
id="default_timezone"
|
||
class="input"
|
||
>
|
||
<option value="UTC" selected?={ systemConfig["default_timezone"] == "UTC" }>UTC (UTC+0)</option>
|
||
<option value="Pacific/Honolulu" selected?={ systemConfig["default_timezone"] == "Pacific/Honolulu" }>Hawaii (UTC-10)</option>
|
||
<option value="America/Anchorage" selected?={ systemConfig["default_timezone"] == "America/Anchorage" }>Alaska (UTC-9/-8)</option>
|
||
<option value="America/Los_Angeles" selected?={ systemConfig["default_timezone"] == "America/Los_Angeles" }>Pacific (UTC-8/-7)</option>
|
||
<option value="America/Denver" selected?={ systemConfig["default_timezone"] == "America/Denver" }>Mountain (UTC-7/-6)</option>
|
||
<option value="America/Phoenix" selected?={ systemConfig["default_timezone"] == "America/Phoenix" }>Mountain - no DST (UTC-7)</option>
|
||
<option value="America/Chicago" selected?={ systemConfig["default_timezone"] == "America/Chicago" }>Central (UTC-6/-5)</option>
|
||
<option value="America/New_York" selected?={ systemConfig["default_timezone"] == "America/New_York" }>Eastern (UTC-5/-4)</option>
|
||
<option value="America/Sao_Paulo" selected?={ systemConfig["default_timezone"] == "America/Sao_Paulo" }>Brasilia (UTC-3/-2)</option>
|
||
<option value="Europe/London" selected?={ systemConfig["default_timezone"] == "Europe/London" }>British (UTC+0/+1)</option>
|
||
<option value="Europe/Paris" selected?={ systemConfig["default_timezone"] == "Europe/Paris" }>Central European (UTC+1/+2)</option>
|
||
<option value="Europe/Helsinki" selected?={ systemConfig["default_timezone"] == "Europe/Helsinki" }>Eastern European (UTC+2/+3)</option>
|
||
<option value="Europe/Moscow" selected?={ systemConfig["default_timezone"] == "Europe/Moscow" }>Moscow (UTC+3)</option>
|
||
<option value="Asia/Tehran" selected?={ systemConfig["default_timezone"] == "Asia/Tehran" }>Iran (UTC+3:30)</option>
|
||
<option value="Asia/Dubai" selected?={ systemConfig["default_timezone"] == "Asia/Dubai" }>Gulf (UTC+4)</option>
|
||
<option value="Asia/Karachi" selected?={ systemConfig["default_timezone"] == "Asia/Karachi" }>Pakistan (UTC+5)</option>
|
||
<option value="Asia/Kolkata" selected?={ systemConfig["default_timezone"] == "Asia/Kolkata" }>India (UTC+5:30)</option>
|
||
<option value="Asia/Dhaka" selected?={ systemConfig["default_timezone"] == "Asia/Dhaka" }>Bangladesh (UTC+6)</option>
|
||
<option value="Asia/Bangkok" selected?={ systemConfig["default_timezone"] == "Asia/Bangkok" }>Indochina (UTC+7)</option>
|
||
<option value="Asia/Shanghai" selected?={ systemConfig["default_timezone"] == "Asia/Shanghai" }>China (UTC+8)</option>
|
||
<option value="Asia/Tokyo" selected?={ systemConfig["default_timezone"] == "Asia/Tokyo" }>Japan/Korea (UTC+9)</option>
|
||
<option value="Australia/Darwin" selected?={ systemConfig["default_timezone"] == "Australia/Darwin" }>Australian Central (UTC+9:30)</option>
|
||
<option value="Australia/Sydney" selected?={ systemConfig["default_timezone"] == "Australia/Sydney" }>Australian Eastern (UTC+10/+11)</option>
|
||
<option value="Pacific/Auckland" selected?={ systemConfig["default_timezone"] == "Pacific/Auckland" }>New Zealand (UTC+12/+13)</option>
|
||
</select>
|
||
<p class="text-sm mt-2" style="color: var(--text-secondary)">Default timezone for users who haven't set their own.</p>
|
||
</div>
|
||
</div>
|
||
</form>
|
||
<div class="mt-6 card p-6">
|
||
<div class="flex items-center gap-2 mb-4">
|
||
@Icon("external", "h-5 w-5 shrink-0")
|
||
<h3 class="text-lg font-semibold" style="color: var(--text-primary)">URL Paths</h3>
|
||
</div>
|
||
<div class="space-y-2 text-sm" style="color: var(--text-secondary);">
|
||
<p><span class="font-medium uppercase tracking-wide text-xs" style="color: var(--text-secondary)">OPDS:</span> { systemConfig["base_url"] }/opds</p>
|
||
<p><span class="font-medium uppercase tracking-wide text-xs" style="color: var(--text-secondary)">API:</span> { systemConfig["base_url"] }/api</p>
|
||
<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)
|
||
@TunableSettingsSection(liveGroups, false)
|
||
@TunableSettingsSection(restartGroups, true)
|
||
</div>
|
||
</main>
|
||
</body>
|
||
</html>
|
||
}
|
||
|
||
templ ScanSettingsSection(scanSettings ScanSettingsData) {
|
||
<div id="scan-settings-section" class="mt-6 card p-6">
|
||
<div class="flex items-center gap-2 mb-6">
|
||
@Icon("refresh", "h-5 w-5 shrink-0")
|
||
<h3 class="text-lg font-semibold" style="color: var(--text-primary)">Scanning</h3>
|
||
</div>
|
||
<form
|
||
hx-put="/admin/settings/scan"
|
||
hx-target="#scan-settings-section"
|
||
hx-swap="outerHTML"
|
||
>
|
||
<div class="space-y-4">
|
||
<div class="flex items-center justify-between gap-4">
|
||
<div>
|
||
<label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary)">Auto-Scan</label>
|
||
<p class="text-sm" style="color: var(--text-secondary)">Watch libraries for file changes on startup</p>
|
||
</div>
|
||
<label class="relative inline-flex items-center cursor-pointer shrink-0">
|
||
<input
|
||
type="checkbox"
|
||
name="auto_scan_enabled"
|
||
value="true"
|
||
checked?={ scanSettings.AutoScanEnabled }
|
||
class="sr-only peer"
|
||
/>
|
||
<div class="w-11 h-6 rounded-full peer peer-checked:bg-brand transition-colors" style="background-color: color-mix(in srgb, var(--text-primary) 15%, transparent);"></div>
|
||
<div class="absolute left-0.5 top-0.5 bg-white rounded-full w-5 h-5 transition-transform peer-checked:translate-x-5"></div>
|
||
</label>
|
||
</div>
|
||
<div>
|
||
<label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Scan Interval (seconds)</label>
|
||
<input
|
||
type="number"
|
||
name="scan_poll_interval_seconds"
|
||
value={ fmt.Sprintf("%d", scanSettings.ScanPollIntervalSeconds) }
|
||
min="1"
|
||
max="3600"
|
||
class="input"
|
||
required
|
||
/>
|
||
<p class="text-sm mt-2" style="color: var(--text-secondary)">How often to poll libraries for changes (1–3600 seconds). Default: 60.</p>
|
||
</div>
|
||
<div class="flex justify-end">
|
||
<button type="submit" class="btn btn-primary">
|
||
@Icon("save", "h-4 w-4")
|
||
Save Scan Settings
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</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>
|
||
}
|