# 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