# 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
<div class="form-group">
    <label for="timezone">Timezone</label>
    <select name="timezone" id="timezone" class="form-select">
        <option value="UTC" selected?={ user.Timezone == "UTC" }>UTC (Coordinated Universal Time)</option>
        <option value="America/New_York" selected?={ user.Timezone == "America/New_York" }>Eastern Time</option>
        <option value="America/Chicago" selected?={ user.Timezone == "America/Chicago" }>Central Time</option>
        <option value="America/Denver" selected?={ user.Timezone == "America/Denver" }>Mountain Time</option>
        <option value="America/Los_Angeles" selected?={ user.Timezone == "America/Los_Angeles" }>Pacific Time</option>
        <option value="America/Phoenix" selected?={ user.Timezone == "America/Phoenix" }>Mountain Time (no DST)</option>
        <option value="America/Anchorage" selected?={ user.Timezone == "America/Anchorage" }>Alaska Time</option>
        <option value="Pacific/Honolulu" selected?={ user.Timezone == "Pacific/Honolulu" }>Hawaii Time</option>
    </select>
</div>
```

Include timezone in the HTMX form submission payload.

---

## Phase 7: Admin Settings UI

**File:** `templates/admin_settings.templ`

Add system default timezone setting:

```templ
<div class="setting-group">
    <h3>System Defaults</h3>
    <label for="default_timezone">Default Timezone</label>
    <select name="default_timezone" id="default_timezone">
        <option value="UTC">UTC (Coordinated Universal Time)</option>
        <option value="America/New_York">Eastern Time</option>
        <option value="America/Chicago">Central Time</option>
        <option value="America/Denver">Mountain Time</option>
        <option value="America/Los_Angeles">Pacific Time</option>
        <option value="America/Phoenix">Mountain Time (no DST)</option>
        <option value="America/Anchorage">Alaska Time</option>
        <option value="Pacific/Honolulu">Hawaii Time</option>
    </select>
</div>
```

---

## 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
<!-- Before -->
{ book.ReadingProgress.LastReadAt.Time.Format("01-02-2006 03:04 PM") }

<!-- After -->
{ templates.FormatTimestamptzInTimezone(book.ReadingProgress.LastReadAt, user.Timezone) }
```

For `time.Time` fields:

```templ
<!-- Before -->
{ device.LastSync.Format("01-02-2006 03:04 PM") }

<!-- After -->
{ 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
