fix(tz): server-side timezone conversion for all user-facing time displays

The audit showed only 5 of ~11 time-display surfaces converted to the
user's timezone; everything else rendered pgx-decoded UTC. The server
owns conversion — clients (web/app/KOReader) receive display-ready
strings or machine-readable instants and never convert.

- Progress page: LastUpdated formatted in the requesting user's tz
  (was raw UTC).
- Analytics: date-range defaults, day buckets, most-active-day, device
  last-sync, and popular-book last-read all computed in the user's tz —
  evening sessions no longer land on the next UTC day. Also fixed the
  end-date boundary: the query bounds with <= and the end bound was
  midnight, so the end date's own sessions (including 'today') never
  appeared.
- Queue page: new created_at_display field (admin tz) rendered by the
  template; created_at stays RFC3339 for API consumers.
- Admin hash-conflicts SSR bridge: detected date rendered in the
  admin's tz.
- OPDS: .UTC() before the Z-suffixed feed timestamps (honesty fix —
  correct today only because the session zone happens to be UTC).
- KOReader annotation datetimes deliberately untouched (device owns its
  display).
This commit is contained in:
John O'Keefe committed 2026-10-13 13:59:49 -04:00
1 parent 740213f0a4
commit 1601548e6c
8 files changed
+75 -24

No files matched your search

+12 -3
View File
@@ -2,6 +2,7 @@ package handlers
import (
"bookhoard/internal/database"
"bookhoard/internal/utils"
"context"
"net/http"
"strconv"
@@ -47,7 +48,11 @@ type QueueItemResponse struct {
Status string `json:"status"`
ErrorMessage *string `json:"error_message,omitempty"`
CreatedAt string `json:"created_at"`
ProcessedAt *string `json:"processed_at,omitempty"`
// CreatedAtDisplay is the server-rendered wall-clock string in the
// requesting admin's timezone (templates render this; created_at stays
// machine-readable RFC3339 for API consumers).
CreatedAtDisplay string `json:"created_at_display,omitempty"`
ProcessedAt *string `json:"processed_at,omitempty"`
}
func (h *QueueHandler) GetDeviceQueueStats(c *echo.Context) error {
@@ -136,7 +141,9 @@ func (h *QueueHandler) ListDeviceQueueItems(c *echo.Context) error {
Status: item.Status.String,
ErrorMessage: textPtrToString(item.ErrorMessage),
CreatedAt: item.CreatedAt.Time.Format("2006-01-02T15:04:05Z07:00"),
ProcessedAt: timestamptzPtrToString(item.ProcessedAt),
CreatedAtDisplay: utils.FormatTimeInTimezone(
item.CreatedAt.Time, user.Timezone.String),
ProcessedAt: timestamptzPtrToString(item.ProcessedAt),
})
}
@@ -189,7 +196,9 @@ func (h *QueueHandler) GetQueueData(c *echo.Context) ([]QueueItemResponse, error
Status: item.Status.String,
ErrorMessage: textPtrToString(item.ErrorMessage),
CreatedAt: item.CreatedAt.Time.Format("2006-01-02T15:04:05Z07:00"),
ProcessedAt: timestamptzPtrToString(item.ProcessedAt),
CreatedAtDisplay: utils.FormatTimeInTimezone(
item.CreatedAt.Time, user.Timezone.String),
ProcessedAt: timestamptzPtrToString(item.ProcessedAt),
})
}