From a4b91393a6990144cc25489f580fcbfa6cb62426 Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Wed, 27 May 2026 11:22:04 -0400 Subject: [PATCH] chore: remove completed TIMEZONE_PLAN.md The timezone feature has been fully implemented across the codebase (database queries, API handlers, profile form, reader settings). This planning document is no longer needed. --- TIMEZONE_PLAN.md | 379 ----------------------------------------------- 1 file changed, 379 deletions(-) delete mode 100644 TIMEZONE_PLAN.md diff --git a/TIMEZONE_PLAN.md b/TIMEZONE_PLAN.md deleted file mode 100644 index d2137d4..0000000 --- a/TIMEZONE_PLAN.md +++ /dev/null @@ -1,379 +0,0 @@ -# Timezone Implementation Plan - -## Overview - -Add per-user timezone support with system-wide fallback (set via docker-compose), defaulting to UTC. The database already stores all timestamps as UTC via `TIMESTAMPTZ` columns, so this is primarily a display-layer feature. - -**Display format:** MM-DD-YYYY HH:MM (US convention, no timezone abbreviation shown) - -**Timezone selection:** Manual dropdown only (no browser auto-detect) - ---- - -## Phase 1: Database Schema - -**File:** `database/schema/schema.sql` - -1. Add `timezone` column directly to the `users` table definition (line ~36): - -```sql -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' -); -``` - -> 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; -``` - -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); -``` - -1. Regenerate sqlc code: - -```bash -cd internal/database && sqlc generate -``` - ---- - -## Phase 2: Database Queries - -**File:** `internal/database/queries/queries.sql` - -Add new queries: - -```sql --- 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'; -``` - -> Note: `UpdateSystemTimezone` is omitted because the existing `UpdateSystemSetting` query handles it by passing `'default_timezone'` as the key parameter. - -Regenerate after adding queries: - -```bash -cd internal/database && sqlc generate -``` - ---- - -## Phase 3: Template Utilities - -**File:** `templates/utils.go` - -Add timezone-aware time formatting helpers: - -```go -package templates - -import ( - "time" - - "github.com/jackc/pgx/v5/pgtype" -) - -// FormatInTimezone formats a time.Time in the specified timezone as MM-DD-YYYY HH:MM -func FormatInTimezone(t time.Time, timezone string) string { - if t.IsZero() { - return "" - } - - loc, err := time.LoadLocation(timezone) - if err != nil { - loc = time.UTC - } - - return t.In(loc).Format("01-02-2006 03:04 PM") -} - -// FormatTimestamptzInTimezone formats a pgtype.Timestamptz in the specified timezone -func FormatTimestamptzInTimezone(t pgtype.Timestamptz, timezone string) string { - if !t.Valid { - return "" - } - return FormatInTimezone(t.Time, timezone) -} -``` - ---- - -## Phase 4: User Context Update - -**File:** `templates/types.go` - -Add `Timezone` field to the `User` struct: - -```go -type User struct { - ID string - Email string - Username string - Role string - Theme string - FirstName string - LastName string - CreatedAt time.Time - Token string - Timezone string -} -``` - -**File:** `internal/router/helpers.go` - -Update `getTemplateUserWithTheme()` to include timezone: - -```go -userTimezone := "UTC" -if userDB.Timezone.Valid { - userTimezone = userDB.Timezone.String -} - -return templates.User{ - // ... existing fields ... - Timezone: userTimezone, -} -``` - ---- - -## Phase 5: Handlers - -**File:** `internal/handlers/auth.go` - -Update `UpdateProfileRequest` struct: - -```go -type UpdateProfileRequest struct { - Username string `json:"username,omitempty" validate:"omitempty,min=3,max=50"` - Email string `json:"email,omitempty" validate:"omitempty,email"` - 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"` -} -``` - -Add timezone update logic in `UpdateProfile()`: - -```go -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(ctx, database.UpdateUserTimezoneParams{ - ID: pgtype.UUID{Bytes: userUUID, Valid: true}, - Timezone: pgtype.Text{String: req.Timezone, Valid: true}, - }) - if err != nil { - return err - } -} -``` - -**File:** `internal/handlers/system_settings.go` - -Add timezone settings handler: - -```go -type UpdateTimezoneSettingsRequest struct { - DefaultTimezone string `json:"default_timezone" validate:"required"` -} - -func (h *SystemSettingsHandler) UpdateTimezoneSettings(c *echo.Context) error { - var req UpdateTimezoneSettingsRequest - if err := c.Bind(&req); err != nil { - return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid request"}) - } - if _, err := time.LoadLocation(req.DefaultTimezone); err != nil { - return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid timezone"}) - } - err := h.db.UpdateSystemSetting(c.Request().Context(), database.UpdateSystemSettingParams{ - SettingKey: "default_timezone", - SettingValue: req.DefaultTimezone, - }) - if err != nil { - return err - } - return c.JSON(http.StatusOK, map[string]string{"message": "Timezone updated"}) -} -``` - ---- - -## Phase 6: User Profile UI - -**File:** `templates/profile_form.templ` - -Add timezone dropdown after the theme field: - -```templ -
- - -
-``` - -Include timezone in the HTMX form submission payload. - ---- - -## Phase 7: Admin Settings UI - -**File:** `templates/admin_settings.templ` - -Add system default timezone setting: - -```templ -
-

System Defaults

- - -
-``` - ---- - -## Phase 8: Template Time Display Updates - -### 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` | - -### Change pattern - -```templ - -{ book.ReadingProgress.LastReadAt.Time.Format("01-02-2006 03:04 PM") } - - -{ templates.FormatTimestamptzInTimezone(book.ReadingProgress.LastReadAt, user.Timezone) } -``` - -For `time.Time` fields: - -```templ - -{ device.LastSync.Format("01-02-2006 03:04 PM") } - - -{ templates.FormatInTimezone(device.LastSync, user.Timezone) } -``` - ---- - -## Phase 9: Docker Configuration - -**File:** `docker-compose.yml` - -```yaml -services: - server: - environment: - - TZ=UTC -``` - -**File:** `.env.example` - -``` -# System default timezone (fallback if not set in DB) -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 (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 | - ---- - -## Testing Checklist - -- [ ] Create user, set timezone to Eastern, verify times display in MM-DD-YYYY HH:MM format -- [ ] Create user, set timezone to Pacific, verify different offset -- [ ] Test system default timezone fallback for users with no timezone set -- [ ] Verify invalid timezone values are rejected by the API -- [ ] Verify existing users (no timezone set) fall back to system default -- [ ] Verify all templates show consistent MM-DD-YYYY HH:MM format -- [ ] Run `make test-integration` to verify no regressions - ---- - -## Deployment Steps - -1. Update `database/schema/schema.sql` with new column and settings -2. Regenerate sqlc: `cd internal/database && sqlc generate` -3. Apply schema changes (restart database container with `make rebuild-force-db`) -4. Deploy backend code changes -5. Verify with existing data