diff --git a/.env.example b/.env.example index cfa212d..cdb581b 100644 --- a/.env.example +++ b/.env.example @@ -19,4 +19,6 @@ DBPASS=your-secure-database-password-here # Conversion Tool: Switch from kepubify to ebook-convert # BOOKHOARD_CONVERSION_TOOL=/usr/bin/ebook-convert # Conversion Cache TTL: Override default 24h -# BOOKHOARD_CONVERSION_CACHE_TTL=48h \ No newline at end of file +# BOOKHOARD_CONVERSION_CACHE_TTL=48h +# System timezone (fallback for server-side time operations, defaults to UTC) +# TZ=America/New_York \ No newline at end of file diff --git a/TIMEZONE_PLAN.md b/TIMEZONE_PLAN.md index fcf85b7..d2137d4 100644 --- a/TIMEZONE_PLAN.md +++ b/TIMEZONE_PLAN.md @@ -14,27 +14,44 @@ Add per-user timezone support with system-wide fallback (set via docker-compose) **File:** `database/schema/schema.sql` -1. Add `timezone` column to `users` table: +1. Add `timezone` column directly to the `users` table definition (line ~36): ```sql -ALTER TABLE users ADD COLUMN IF NOT EXISTS timezone VARCHAR(50) DEFAULT 'UTC'; +CREATE TABLE IF NOT EXISTS users ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + email VARCHAR(255) UNIQUE NOT NULL, + username VARCHAR(255) UNIQUE NOT NULL, + password_hash VARCHAR(255) NOT NULL, + first_name VARCHAR(255), + last_name VARCHAR(255), + role VARCHAR(20) NOT NULL DEFAULT 'user' CHECK (role IN ('admin', 'user')), + theme VARCHAR(50) DEFAULT 'tokyo-night', + max_devices INTEGER DEFAULT 10, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + timezone VARCHAR(50) DEFAULT 'UTC' +); ``` -2. Add system-wide default timezone to `system_settings`: +> Note: The `timezone` column is already present at line 36 in the current schema. No change needed for this step. + +1. Add `default_timezone` to the `system_settings` INSERT block (line ~49-52): ```sql INSERT INTO system_settings (setting_key, setting_value, description) VALUES +('scan_poll_interval_seconds', '60', 'How often to scan all libraries in minutes'), +('auto_scan_enabled', 'true', 'Whether auto-scanning is enabled system-wide'), ('default_timezone', 'UTC', 'System default timezone') ON CONFLICT (setting_key) DO NOTHING; ``` -3. Add index: +1. Add index in the indexes section (after line ~460, with other user indexes): ```sql CREATE INDEX IF NOT EXISTS idx_users_timezone ON users(timezone); ``` -4. Regenerate sqlc code: +1. Regenerate sqlc code: ```bash cd internal/database && sqlc generate @@ -54,11 +71,10 @@ UPDATE users SET timezone = $2, updated_at = NOW() WHERE id = $1; -- name: GetSystemTimezone :one SELECT setting_value FROM system_settings WHERE setting_key = 'default_timezone'; - --- name: UpdateSystemTimezone :exec -UPDATE system_settings SET setting_value = $2, updated_at = NOW() WHERE setting_key = 'default_timezone'; ``` +> Note: `UpdateSystemTimezone` is omitted because the existing `UpdateSystemSetting` query handles it by passing `'default_timezone'` as the key parameter. + Regenerate after adding queries: ```bash @@ -93,7 +109,7 @@ func FormatInTimezone(t time.Time, timezone string) string { loc = time.UTC } - return t.In(loc).Format("01-02-2006 15:04") + return t.In(loc).Format("01-02-2006 03:04 PM") } // FormatTimestamptzInTimezone formats a pgtype.Timestamptz in the specified timezone @@ -199,7 +215,7 @@ func (h *SystemSettingsHandler) UpdateTimezoneSettings(c *echo.Context) error { if _, err := time.LoadLocation(req.DefaultTimezone); err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid timezone"}) } - err := h.db.UpdateSystemTimezone(c.Request().Context(), database.UpdateSystemTimezoneParams{ + err := h.db.UpdateSystemSetting(c.Request().Context(), database.UpdateSystemSettingParams{ SettingKey: "default_timezone", SettingValue: req.DefaultTimezone, }) @@ -267,20 +283,20 @@ Add system default timezone setting: ### Files to update -| Template | Line(s) | Field(s) | -|----------|---------|----------| -| `templates/book_detail.templ` | ~253, ~282 | `LastReadAt`, `DatePublished` | -| `templates/book_detail_modals.templ` | ~68, ~113 | `Timestamp`, `LastReadAt` | -| `templates/devices.templ` | ~83, ~91, ~174 | `LastSync`, `LastSeen`, `ExpiresAt` | -| `templates/conflicts.templ` | ~114 | `CreatedAt` | -| `templates/admin_users.templ` | ~89 | `CreatedAt` | -| `templates/queue.templ` | ~138 | `CreatedAt` | +| Template | Line(s) | Field(s) | +| ------------------------------------ | -------------- | ----------------------------------- | +| `templates/book_detail.templ` | ~253, ~282 | `LastReadAt`, `DatePublished` | +| `templates/book_detail_modals.templ` | ~68, ~113 | `Timestamp`, `LastReadAt` | +| `templates/devices.templ` | ~83, ~91, ~174 | `LastSync`, `LastSeen`, `ExpiresAt` | +| `templates/conflicts.templ` | ~114 | `CreatedAt` | +| `templates/admin_users.templ` | ~89 | `CreatedAt` | +| `templates/queue.templ` | ~138 | `CreatedAt` | ### Change pattern ```templ -{ book.ReadingProgress.LastReadAt.Time.Format("2006-01-02 15:04") } +{ book.ReadingProgress.LastReadAt.Time.Format("01-02-2006 03:04 PM") } { templates.FormatTimestamptzInTimezone(book.ReadingProgress.LastReadAt, user.Timezone) } @@ -290,7 +306,7 @@ For `time.Time` fields: ```templ -{ device.LastSync.Format("2006-01-02 15:04") } +{ device.LastSync.Format("01-02-2006 03:04 PM") } { templates.FormatInTimezone(device.LastSync, user.Timezone) } @@ -320,25 +336,25 @@ TZ=UTC ## Files Modified Summary -| File | Change | -|------|--------| -| `database/schema/schema.sql` | Add timezone column to users, system_setting row | -| `internal/database/queries/queries.sql` | Add UpdateUserTimezone, GetSystemTimezone, UpdateSystemTimezone | -| `templates/utils.go` | Add FormatInTimezone, FormatTimestamptzInTimezone | -| `templates/types.go` | Add Timezone field to User struct | -| `internal/router/helpers.go` | Pass timezone to template User | -| `internal/handlers/auth.go` | Handle timezone updates in UpdateProfile | -| `internal/handlers/system_settings.go` | Add timezone settings handler | -| `templates/profile_form.templ` | Add timezone dropdown | -| `templates/admin_settings.templ` | Add default timezone setting | -| `templates/book_detail.templ` | Update time displays | -| `templates/book_detail_modals.templ` | Update time displays | -| `templates/devices.templ` | Update time displays | -| `templates/conflicts.templ` | Update time displays | -| `templates/admin_users.templ` | Update time displays | -| `templates/queue.templ` | Update time displays | -| `docker-compose.yml` | Add TZ env var | -| `.env.example` | Add TZ example | +| File | Change | +| --------------------------------------- | ------------------------------------------------------------------------------- | +| `database/schema/schema.sql` | Add timezone column to users, system_setting row | +| `internal/database/queries/queries.sql` | Add UpdateUserTimezone, GetSystemTimezone (reuses existing UpdateSystemSetting) | +| `templates/utils.go` | Add FormatInTimezone, FormatTimestamptzInTimezone | +| `templates/types.go` | Add Timezone field to User struct | +| `internal/router/helpers.go` | Pass timezone to template User | +| `internal/handlers/auth.go` | Handle timezone updates in UpdateProfile | +| `internal/handlers/system_settings.go` | Add timezone settings handler | +| `templates/profile_form.templ` | Add timezone dropdown | +| `templates/admin_settings.templ` | Add default timezone setting | +| `templates/book_detail.templ` | Update time displays | +| `templates/book_detail_modals.templ` | Update time displays | +| `templates/devices.templ` | Update time displays | +| `templates/conflicts.templ` | Update time displays | +| `templates/admin_users.templ` | Update time displays | +| `templates/queue.templ` | Update time displays | +| `docker-compose.yml` | Add TZ env var | +| `.env.example` | Add TZ example | --- diff --git a/database/schema/schema.sql b/database/schema/schema.sql index 44a5178..2dd96dd 100644 --- a/database/schema/schema.sql +++ b/database/schema/schema.sql @@ -32,6 +32,7 @@ CREATE TABLE IF NOT EXISTS users ( theme VARCHAR(50) DEFAULT 'tokyo-night', max_devices INTEGER DEFAULT 10, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + timezone VARCHAR(50) DEFAULT 'UTC', updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); @@ -47,7 +48,8 @@ CREATE TABLE IF NOT EXISTS system_settings ( -- Insert default system settings INSERT INTO system_settings (setting_key, setting_value, description) VALUES ('scan_poll_interval_seconds', '60', 'How often to scan all libraries in minutes'), -('auto_scan_enabled', 'true', 'Whether auto-scanning is enabled system-wide') +('auto_scan_enabled', 'true', 'Whether auto-scanning is enabled system-wide'), +('default_timezone', 'UTC', 'System default timezone') ON CONFLICT (setting_key) DO NOTHING; -- Create refresh_tokens table @@ -457,6 +459,7 @@ CREATE TABLE IF NOT EXISTS reading_history ( -- Create indexes for better query performance CREATE INDEX IF NOT EXISTS idx_users_email ON users(email); CREATE INDEX IF NOT EXISTS idx_users_username ON users(username); +CREATE INDEX IF NOT EXISTS idx_users_timezone ON users(timezone); CREATE INDEX IF NOT EXISTS idx_library_types_name ON library_types(name); CREATE INDEX IF NOT EXISTS idx_refresh_tokens_token ON refresh_tokens(token); CREATE INDEX IF NOT EXISTS idx_refresh_tokens_user_id ON refresh_tokens(user_id); diff --git a/docker-compose.yml b/docker-compose.yml index 8608f7f..f140295 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -57,6 +57,9 @@ services: BOOKHOARD_CONVERSION_CACHE_DIR: ${BOOKHOARD_CONVERSION_CACHE_DIR:-/app/cache/kepub} BOOKHOARD_CONVERSION_TOOL: ${BOOKHOARD_CONVERSION_TOOL:-/usr/bin/kepubify} BOOKHOARD_CONVERSION_CACHE_TTL: ${BOOKHOARD_CONVERSION_CACHE_TTL:-24h} + + # System timezone (fallback for server-side time operations) + TZ: ${TZ:-UTC} ports: - "8765:8765" depends_on: diff --git a/internal/database/models.go b/internal/database/models.go index b20110a..3fb239b 100644 --- a/internal/database/models.go +++ b/internal/database/models.go @@ -507,5 +507,6 @@ type Users struct { Theme pgtype.Text `db:"theme" json:"theme"` MaxDevices pgtype.Int4 `db:"max_devices" json:"max_devices"` CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"` + Timezone pgtype.Text `db:"timezone" json:"timezone"` UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"` } diff --git a/internal/database/querier.go b/internal/database/querier.go index e902dd1..0b41efd 100644 --- a/internal/database/querier.go +++ b/internal/database/querier.go @@ -247,6 +247,7 @@ type Querier interface { GetSystemConfig(ctx context.Context, key string) (SystemConfig, error) // System Settings queries GetSystemSetting(ctx context.Context, settingKey string) (string, error) + GetSystemTimezone(ctx context.Context) (string, error) // Get universal progress for a book GetUniversalProgress(ctx context.Context, arg GetUniversalProgressParams) (GetUniversalProgressRow, error) // Get unlinked book by ContentId @@ -373,6 +374,7 @@ type Querier interface { UpdateUserProfile(ctx context.Context, arg UpdateUserProfileParams) error UpdateUserRole(ctx context.Context, arg UpdateUserRoleParams) (UpdateUserRoleRow, error) UpdateUserTheme(ctx context.Context, arg UpdateUserThemeParams) error + UpdateUserTimezone(ctx context.Context, arg UpdateUserTimezoneParams) error UpdateUsername(ctx context.Context, arg UpdateUsernameParams) error UpsertDashboardPreferences(ctx context.Context, arg UpsertDashboardPreferencesParams) (UserDashboardPreferences, error) UpsertPanelData(ctx context.Context, arg UpsertPanelDataParams) (PanelData, error) diff --git a/internal/database/queries.sql.go b/internal/database/queries.sql.go index 5274924..dd201e5 100644 --- a/internal/database/queries.sql.go +++ b/internal/database/queries.sql.go @@ -5408,6 +5408,17 @@ func (q *Queries) GetSystemSetting(ctx context.Context, settingKey string) (stri return setting_value, err } +const GetSystemTimezone = `-- name: GetSystemTimezone :one +SELECT setting_value FROM system_settings WHERE setting_key = 'default_timezone' +` + +func (q *Queries) GetSystemTimezone(ctx context.Context) (string, error) { + row := q.db.QueryRow(ctx, GetSystemTimezone) + var setting_value string + err := row.Scan(&setting_value) + return setting_value, err +} + const GetUniversalProgress = `-- name: GetUniversalProgress :one SELECT rp.id, @@ -5651,6 +5662,7 @@ SELECT u.max_devices, u.created_at, u.updated_at, + u.timezone, (SELECT COUNT(*) FROM devices WHERE user_id = u.id) as device_count FROM users u WHERE u.id = $1 @@ -5667,6 +5679,7 @@ type GetUserRow struct { MaxDevices pgtype.Int4 `db:"max_devices" json:"max_devices"` CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"` UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"` + Timezone pgtype.Text `db:"timezone" json:"timezone"` DeviceCount int64 `db:"device_count" json:"device_count"` } @@ -5684,6 +5697,7 @@ func (q *Queries) GetUser(ctx context.Context, id pgtype.UUID) (GetUserRow, erro &i.MaxDevices, &i.CreatedAt, &i.UpdatedAt, + &i.Timezone, &i.DeviceCount, ) return i, err @@ -10256,7 +10270,7 @@ func (q *Queries) UpdateUniversalProgress(ctx context.Context, arg UpdateUnivers const UpdateUserMaxDevices = `-- name: UpdateUserMaxDevices :one UPDATE users SET max_devices = $2, updated_at = NOW() WHERE id = $1 -RETURNING id, email, username, password_hash, first_name, last_name, role, theme, max_devices, created_at, updated_at +RETURNING id, email, username, password_hash, first_name, last_name, role, theme, max_devices, created_at, timezone, updated_at ` type UpdateUserMaxDevicesParams struct { @@ -10278,6 +10292,7 @@ func (q *Queries) UpdateUserMaxDevices(ctx context.Context, arg UpdateUserMaxDev &i.Theme, &i.MaxDevices, &i.CreatedAt, + &i.Timezone, &i.UpdatedAt, ) return i, err @@ -10341,6 +10356,20 @@ func (q *Queries) UpdateUserTheme(ctx context.Context, arg UpdateUserThemeParams return err } +const UpdateUserTimezone = `-- name: UpdateUserTimezone :exec +UPDATE users SET timezone = $2, updated_at = NOW() WHERE id = $1 +` + +type UpdateUserTimezoneParams struct { + ID pgtype.UUID `db:"id" json:"id"` + Timezone pgtype.Text `db:"timezone" json:"timezone"` +} + +func (q *Queries) UpdateUserTimezone(ctx context.Context, arg UpdateUserTimezoneParams) error { + _, err := q.db.Exec(ctx, UpdateUserTimezone, arg.ID, arg.Timezone) + return err +} + const UpdateUsername = `-- name: UpdateUsername :exec UPDATE users SET username = $2, updated_at = NOW() WHERE id = $1 ` diff --git a/internal/database/queries/queries.sql b/internal/database/queries/queries.sql index f1b8131..7033466 100644 --- a/internal/database/queries/queries.sql +++ b/internal/database/queries/queries.sql @@ -27,6 +27,7 @@ SELECT u.max_devices, u.created_at, u.updated_at, + u.timezone, (SELECT COUNT(*) FROM devices WHERE user_id = u.id) as device_count FROM users u WHERE u.id = $1; @@ -300,6 +301,12 @@ RETURNING *; UPDATE users SET role = $2, updated_at = NOW() WHERE id = $1 RETURNING id, email, username, role; +-- name: UpdateUserTimezone :exec +UPDATE users SET timezone = $2, updated_at = NOW() WHERE id = $1; + +-- name: GetSystemTimezone :one +SELECT setting_value FROM system_settings WHERE setting_key = 'default_timezone'; + -- name: CountUserDevices :one SELECT COUNT(*) FROM devices WHERE user_id = $1; diff --git a/internal/handlers/analytics.go b/internal/handlers/analytics.go index 9df4a66..1b0644e 100644 --- a/internal/handlers/analytics.go +++ b/internal/handlers/analytics.go @@ -75,18 +75,18 @@ func (h *AnalyticsHandler) GetReadingStats(c *echo.Context) error { endDate := c.QueryParam("end_date") if startDate == "" { - startDate = time.Now().AddDate(0, -1, 0).Format("2006-01-02") + startDate = time.Now().AddDate(0, -1, 0).Format("01-02-2006") } if endDate == "" { - endDate = time.Now().Format("2006-01-02") + endDate = time.Now().Format("01-02-2006") } - startTime, err := time.Parse("2006-01-02", startDate) + startTime, err := time.Parse("01-02-2006", startDate) if err != nil { return echo.NewHTTPError(http.StatusBadRequest, "invalid start_date format") } - endTime, err := time.Parse("2006-01-02", endDate) + endTime, err := time.Parse("01-02-2006", endDate) if err != nil { return echo.NewHTTPError(http.StatusBadRequest, "invalid end_date format") } @@ -130,7 +130,7 @@ func (h *AnalyticsHandler) calculateReadingStats(history []database.GetUserReadi longestSession = int(minutes) } - dateKey := entry.CreatedAt.Time.Format("2006-01-02") + dateKey := entry.CreatedAt.Time.Format("01-02-2006") if dailyMap[dateKey] == nil { dailyMap[dateKey] = &DailyReading{ Date: dateKey, @@ -141,7 +141,7 @@ func (h *AnalyticsHandler) calculateReadingStats(history []database.GetUserReadi if entry.PagesRead.Valid { totalPages += int(entry.PagesRead.Int32) - dateKey := entry.CreatedAt.Time.Format("2006-01-02") + dateKey := entry.CreatedAt.Time.Format("01-02-2006") if dailyMap[dateKey] != nil { dailyMap[dateKey].Pages += int(entry.PagesRead.Int32) } @@ -222,7 +222,7 @@ func (h *AnalyticsHandler) GetDeviceUsage(c *echo.Context) error { lastSync := "" if u.LastSync != nil { if t, ok := u.LastSync.(time.Time); ok { - lastSync = t.Format("2006-01-02 15:04:05") + lastSync = t.Format("01-02-2006 03:04:05 PM") } } @@ -274,7 +274,7 @@ func (h *AnalyticsHandler) GetPopularBooks(c *echo.Context) error { lastRead := "" if book.LastRead != nil { if t, ok := book.LastRead.(time.Time); ok { - lastRead = t.Format("2006-01-02 15:04:05") + lastRead = t.Format("01-02-2006 03:04:05 PM") } } diff --git a/internal/handlers/auth.go b/internal/handlers/auth.go index b9806b7..242f251 100644 --- a/internal/handlers/auth.go +++ b/internal/handlers/auth.go @@ -87,6 +87,7 @@ type UpdateProfileRequest struct { FirstName string `json:"first_name,omitempty" validate:"omitempty,max=100"` LastName string `json:"last_name,omitempty" validate:"omitempty,max=100"` Theme string `json:"theme,omitempty" validate:"omitempty"` + Timezone string `json:"timezone,omitempty" validate:"omitempty"` } type AdminUpdateUserRequest struct { @@ -95,6 +96,7 @@ type AdminUpdateUserRequest struct { FirstName string `json:"first_name,omitempty" validate:"omitempty,max=100"` LastName string `json:"last_name,omitempty" validate:"omitempty,max=100"` Theme string `json:"theme,omitempty" validate:"omitempty"` + Timezone string `json:"timezone,omitempty" validate:"omitempty"` Role string `json:"role,omitempty" validate:"omitempty,oneof=user admin"` } @@ -564,6 +566,22 @@ func (h *AuthHandler) UpdateProfile(c *echo.Context) error { } } + // Update timezone + if req.Timezone != "" { + if _, err := time.LoadLocation(req.Timezone); err != nil { + return c.JSON(http.StatusBadRequest, map[string]string{ + "error": "Invalid timezone", + }) + } + err := h.db.UpdateUserTimezone(c.Request().Context(), database.UpdateUserTimezoneParams{ + ID: targetUserUUID, + Timezone: pgtype.Text{String: req.Timezone, Valid: true}, + }) + if err != nil { + return err + } + } + // Update email (if provided) if req.Email != "" { existingUser, err := h.db.GetUserByEmail(c.Request().Context(), req.Email) diff --git a/internal/handlers/progress.go b/internal/handlers/progress.go index f10946e..0af5ea9 100644 --- a/internal/handlers/progress.go +++ b/internal/handlers/progress.go @@ -306,7 +306,7 @@ func (h *Handler) GetAllProgress(c *echo.Context) error { lastUpdated := "" if progress.LastReadAt.Valid { - lastUpdated = progress.LastReadAt.Time.Format("2006-01-02 15:04") + lastUpdated = progress.LastReadAt.Time.Format("01-02-2006 03:04 PM") } progressList = append(progressList, ProgressWithMedia{ diff --git a/internal/handlers/sidecar.go b/internal/handlers/sidecar.go index 92c3b6a..9e469ce 100644 --- a/internal/handlers/sidecar.go +++ b/internal/handlers/sidecar.go @@ -388,6 +388,23 @@ func (h *SidecarHandler) UpdateSystemConfiguration(c *echo.Context) error { // Update each config value for key, value := range req { + if key == "default_timezone" { + if _, err := time.LoadLocation(value); err != nil { + return c.JSON(http.StatusBadRequest, map[string]string{ + "error": "invalid timezone", + }) + } + err := h.db.UpdateSystemSetting(ctx, database.UpdateSystemSettingParams{ + SettingKey: "default_timezone", + SettingValue: value, + }) + if err != nil { + return c.JSON(http.StatusInternalServerError, map[string]string{ + "error": "failed to update default timezone", + }) + } + continue + } _, err := h.db.SetSystemConfig(ctx, database.SetSystemConfigParams{ Key: key, Value: value, @@ -408,6 +425,12 @@ func (h *SidecarHandler) UpdateSystemConfiguration(c *echo.Context) error { return c.HTML(http.StatusInternalServerError, `
Default timezone for users who haven't set their own.
+Device Sync: %s/api/sync
Default timezone for users who haven't set their own.
+The public URL of your Bookhoard instance (e.g., https://books.example.com). Used for device sync, OPDS, and API endpoints.
OPDS: ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\" placeholder=\"https://books.example.com\" class=\"w-full px-4 py-2 rounded-lg border\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\" required>
The public URL of your Bookhoard instance (e.g., https://books.example.com). Used for device sync, OPDS, and API endpoints.
Default timezone for users who haven't set their own.
OPDS: ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } 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: 58, Col: 60} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 96, Col: 60} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "/opds
API: ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "/opds
API: ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } 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: 59, Col: 59} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 97, Col: 59} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "/api
Device Sync: ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "/api
Device Sync: ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } 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: 60, Col: 67} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 98, Col: 67} } _, 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, 10, "/api/sync