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).
32 lines
899 B
Go
32 lines
899 B
Go
package utils
|
|
|
|
import (
|
|
"time"
|
|
)
|
|
|
|
// UserLocation resolves an IANA timezone name to a *time.Location, falling
|
|
// back to UTC for empty or invalid values. The single resolution point for
|
|
// server-side display conversion: the server owns timezone rendering, so
|
|
// clients receive display-ready strings (or tz-aware instants) and never
|
|
// convert themselves.
|
|
func UserLocation(timezone string) *time.Location {
|
|
if timezone == "" {
|
|
return time.UTC
|
|
}
|
|
loc, err := time.LoadLocation(timezone)
|
|
if err != nil || loc == nil {
|
|
return time.UTC
|
|
}
|
|
return loc
|
|
}
|
|
|
|
// FormatTimeInTimezone renders t in the given IANA timezone as
|
|
// MM-DD-YYYY HH:MM AM/PM (the established display format across the
|
|
// templates). Zero times render empty.
|
|
func FormatTimeInTimezone(t time.Time, timezone string) string {
|
|
if t.IsZero() {
|
|
return ""
|
|
}
|
|
return t.In(UserLocation(timezone)).Format("01-02-2006 03:04 PM")
|
|
}
|