feat(admin): add scan settings + system information to settings page
- Scanning section: auto-scan toggle (on/off) and poll interval input with HTMX form that updates system_settings table - System Information section: surfaces all hardcoded constants (session duration, password policy, rate limits, worker pool, sync queue, tombstone TTL, OPDS page size, CORS, etc.) - New ScanSettingsData type, ScanSettingsSection partial template - New HTMX endpoint: PUT /admin/settings/scan - registerAdminSettingsRoutes for scan settings HTMX CRUD - Fix bookhoord→bookhoard import typo in admin_library.go
This commit is contained in:
@@ -10,6 +10,7 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@@ -378,3 +379,39 @@ func getAdminStats(ctx context.Context, cfg *Config) templates.AdminStats {
|
||||
|
||||
return stats
|
||||
}
|
||||
|
||||
func registerAdminSettingsRoutes(cfg *Config, frontendProtected *echo.Group) {
|
||||
g := frontendProtected.Group("", handlers.AdminMiddleware)
|
||||
|
||||
g.PUT("/admin/settings/scan", func(c *echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
|
||||
autoScan := c.FormValue("auto_scan_enabled") == "true"
|
||||
intervalStr := c.FormValue("scan_poll_interval_seconds")
|
||||
interval, err := strconv.Atoi(intervalStr)
|
||||
if err != nil || interval < 1 || interval > 3600 {
|
||||
return c.HTML(http.StatusBadRequest, `<div class="text-sm" style="color: var(--status-danger);">Interval must be between 1 and 3600 seconds</div>`)
|
||||
}
|
||||
|
||||
autoScanStr := "false"
|
||||
if autoScan {
|
||||
autoScanStr = "true"
|
||||
}
|
||||
_ = cfg.Queries.UpdateSystemSetting(ctx, database.UpdateSystemSettingParams{
|
||||
SettingKey: "auto_scan_enabled",
|
||||
SettingValue: autoScanStr,
|
||||
})
|
||||
_ = cfg.Queries.UpdateSystemSetting(ctx, database.UpdateSystemSettingParams{
|
||||
SettingKey: "scan_poll_interval_seconds",
|
||||
SettingValue: strconv.Itoa(interval),
|
||||
})
|
||||
|
||||
scanSettings := templates.ScanSettingsData{
|
||||
AutoScanEnabled: autoScan,
|
||||
ScanPollIntervalSeconds: interval,
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
_ = templates.ScanSettingsSection(scanSettings).Render(ctx, &buf)
|
||||
return c.HTML(http.StatusOK, buf.String())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -995,21 +995,37 @@ func registerFrontendRoutes(cfg *Config) {
|
||||
return renderErrorPage(c, "Error loading user", "user_load_error")
|
||||
}
|
||||
|
||||
ctx := c.Request().Context()
|
||||
|
||||
// Fetch current system configuration - just base_url
|
||||
baseURL := cfg.getBaseURL(c.Request().Context())
|
||||
baseURL := cfg.getBaseURL(ctx)
|
||||
|
||||
systemConfig := map[string]string{
|
||||
"base_url": baseURL,
|
||||
"default_timezone": "UTC",
|
||||
}
|
||||
|
||||
defaultTimezone, err := cfg.Queries.GetSystemTimezone(c.Request().Context())
|
||||
defaultTimezone, err := cfg.Queries.GetSystemTimezone(ctx)
|
||||
if err == nil && defaultTimezone != "" {
|
||||
systemConfig["default_timezone"] = defaultTimezone
|
||||
}
|
||||
|
||||
// Fetch scan settings
|
||||
scanSettings := templates.ScanSettingsData{
|
||||
AutoScanEnabled: true,
|
||||
ScanPollIntervalSeconds: 60,
|
||||
}
|
||||
if val, err := cfg.Queries.GetSystemSetting(ctx, "auto_scan_enabled"); err == nil {
|
||||
scanSettings.AutoScanEnabled = val == "true"
|
||||
}
|
||||
if val, err := cfg.Queries.GetSystemSetting(ctx, "scan_poll_interval_seconds"); err == nil {
|
||||
if n, err := strconv.Atoi(val); err == nil {
|
||||
scanSettings.ScanPollIntervalSeconds = n
|
||||
}
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = templates.AdminSettings(user, systemConfig, "").Render(c.Request().Context(), &buf)
|
||||
err = templates.AdminSettings(user, systemConfig, scanSettings, "").Render(ctx, &buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1053,6 +1069,8 @@ func registerFrontendRoutes(cfg *Config) {
|
||||
|
||||
// Admin library HTMX endpoints
|
||||
registerAdminLibraryRoutes(cfg, frontendProtected)
|
||||
// Admin settings HTMX endpoints
|
||||
registerAdminSettingsRoutes(cfg, frontendProtected)
|
||||
|
||||
// ============================================================================
|
||||
// LEGACY API ROUTES (for backward compatibility)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package templates
|
||||
|
||||
templ AdminSettings(user User, systemConfig map[string]string, errorMessage string) {
|
||||
import "fmt"
|
||||
|
||||
templ AdminSettings(user User, systemConfig map[string]string, scanSettings ScanSettingsData, errorMessage string) {
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
@@ -105,8 +107,115 @@ templ AdminSettings(user User, systemConfig map[string]string, errorMessage stri
|
||||
<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>
|
||||
</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>
|
||||
}
|
||||
|
||||
@@ -8,7 +8,9 @@ package templates
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
func AdminSettings(user User, systemConfig map[string]string, errorMessage string) templ.Component {
|
||||
import "fmt"
|
||||
|
||||
func AdminSettings(user User, systemConfig map[string]string, scanSettings ScanSettingsData, 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 {
|
||||
@@ -65,7 +67,7 @@ func AdminSettings(user User, systemConfig map[string]string, errorMessage strin
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(errorMessage)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 28, Col: 28}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 30, Col: 28}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -91,7 +93,7 @@ func AdminSettings(user User, systemConfig map[string]string, errorMessage strin
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(systemConfig["base_url"])
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 42, Col: 42}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 44, Col: 42}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -368,7 +370,7 @@ func AdminSettings(user User, systemConfig map[string]string, errorMessage strin
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(systemConfig["base_url"])
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 103, Col: 145}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 105, Col: 145}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -381,7 +383,7 @@ func AdminSettings(user User, systemConfig map[string]string, errorMessage strin
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(systemConfig["base_url"])
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 104, Col: 144}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 106, Col: 144}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -394,13 +396,97 @@ func AdminSettings(user User, systemConfig map[string]string, errorMessage strin
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(systemConfig["base_url"])
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 105, Col: 152}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 107, Col: 152}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "/api/sync</p></div></div></div></main></body></html>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "/api/sync</p></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = ScanSettingsSection(scanSettings).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "<div class=\"mt-6 card p-6\"><div class=\"flex items-center gap-2 mb-4\">")
|
||||
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)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "<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></main></body></html>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func ScanSettingsSection(scanSettings ScanSettingsData) 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 {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var7 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var7 == nil {
|
||||
templ_7745c5c3_Var7 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "<div id=\"scan-settings-section\" class=\"mt-6 card p-6\"><div class=\"flex items-center gap-2 mb-6\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("refresh", "h-5 w-5 shrink-0").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "<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\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if scanSettings.AutoScanEnabled {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, " checked")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, " 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=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("%d", scanSettings.ScanPollIntervalSeconds))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 204, Col: 69}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var8)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "\" 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\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("save", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "Save Scan Settings</button></div></div></form></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
||||
@@ -66,6 +66,11 @@ type AdminStats struct {
|
||||
DeviceCount int
|
||||
}
|
||||
|
||||
type ScanSettingsData struct {
|
||||
AutoScanEnabled bool
|
||||
ScanPollIntervalSeconds int
|
||||
}
|
||||
|
||||
type SeriesCardData struct {
|
||||
Name string
|
||||
BookCount int64
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user