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:
1 parent
a42232e67a
commit
64ad10b3f2
6 files changed
+267
-12
No files matched your search
@@ -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
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }
|
||||
/*! tailwindcss v3.4.19 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Noto Sans SC,Noto Sans TC,Noto Sans JP,Noto Sans KR,PingFang SC,Microsoft YaHei,Hiragino Sans,Apple SD Gothic Neo,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}input:where(:not([type])),input:where([type=date]),input:where([type=datetime-local]),input:where([type=email]),input:where([type=month]),input:where([type=number]),input:where([type=password]),input:where([type=search]),input:where([type=tel]),input:where([type=text]),input:where([type=time]),input:where([type=url]),input:where([type=week]),select,select:where([multiple]),textarea{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;border-color:#6b7280;border-width:1px;border-radius:0;padding:.5rem .75rem;font-size:1rem;line-height:1.5rem;--tw-shadow:0 0 #0000}input:where(:not([type])):focus,input:where([type=date]):focus,input:where([type=datetime-local]):focus,input:where([type=email]):focus,input:where([type=month]):focus,input:where([type=number]):focus,input:where([type=password]):focus,input:where([type=search]):focus,input:where([type=tel]):focus,input:where([type=text]):focus,input:where([type=time]):focus,input:where([type=url]):focus,input:where([type=week]):focus,select:focus,select:where([multiple]):focus,textarea:focus{outline:2px solid transparent;outline-offset:2px;--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#2563eb;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);border-color:#2563eb}input::-moz-placeholder,textarea::-moz-placeholder{color:#6b7280;opacity:1}input::placeholder,textarea::placeholder{color:#6b7280;opacity:1}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-date-and-time-value{min-height:1.5em;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-meridiem-field,::-webkit-datetime-edit-millisecond-field,::-webkit-datetime-edit-minute-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-second-field,::-webkit-datetime-edit-year-field{padding-top:0;padding-bottom:0}select{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath Line truncated
|
||||
/*! tailwindcss v3.4.19 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Noto Sans SC,Noto Sans TC,Noto Sans JP,Noto Sans KR,PingFang SC,Microsoft YaHei,Hiragino Sans,Apple SD Gothic Neo,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}input:where(:not([type])),input:where([type=date]),input:where([type=datetime-local]),input:where([type=email]),input:where([type=month]),input:where([type=number]),input:where([type=password]),input:where([type=search]),input:where([type=tel]),input:where([type=text]),input:where([type=time]),input:where([type=url]),input:where([type=week]),select,select:where([multiple]),textarea{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;border-color:#6b7280;border-width:1px;border-radius:0;padding:.5rem .75rem;font-size:1rem;line-height:1.5rem;--tw-shadow:0 0 #0000}input:where(:not([type])):focus,input:where([type=date]):focus,input:where([type=datetime-local]):focus,input:where([type=email]):focus,input:where([type=month]):focus,input:where([type=number]):focus,input:where([type=password]):focus,input:where([type=search]):focus,input:where([type=tel]):focus,input:where([type=text]):focus,input:where([type=time]):focus,input:where([type=url]):focus,input:where([type=week]):focus,select:focus,select:where([multiple]):focus,textarea:focus{outline:2px solid transparent;outline-offset:2px;--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#2563eb;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);border-color:#2563eb}input::-moz-placeholder,textarea::-moz-placeholder{color:#6b7280;opacity:1}input::placeholder,textarea::placeholder{color:#6b7280;opacity:1}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-date-and-time-value{min-height:1.5em;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-meridiem-field,::-webkit-datetime-edit-millisecond-field,::-webkit-datetime-edit-minute-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-second-field,::-webkit-datetime-edit-year-field{padding-top:0;padding-bottom:0}select{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath Line truncated
|
||||
Reference in new issue
Block a user