internal/server/timezone.go
Rama main · réplica de lectura del repositorio original.
// SPDX-License-Identifier: Apache-2.0
package server
import (
"database/sql"
"log"
"sync/atomic"
"time"
"github.com/X4Applegate/caddyui/internal/models"
)
// settingTimezone is the settings-table key for the IANA timezone name the
// admin picked in Settings → Timezone. Empty string means "no DB override —
// fall back to the TZ env var (Go's time.Local), then UTC".
const settingTimezone = "timezone"
// activeLoc holds the timezone CaddyUI currently renders DB-stored timestamps
// in. Set once at startup from loadActiveLocation(), then updated whenever an
// admin saves a new timezone in Settings. Reads are hot (every page render,
// every template fmtDate call), so we use atomic.Pointer to stay lock-free.
var activeLoc atomic.Pointer[time.Location]
// loadActiveLocation resolves the timezone to use for rendering, in priority
// order:
// 1. DB value from the settings table (admin picked it in /settings)
// 2. TZ env var — Go's time.Local is already populated from this at startup
// 3. UTC — final fallback if nothing else is configured
//
// Called once from Server.New so the very first request renders in the right
// zone. Returns the resolved location so callers can log which zone won.
func loadActiveLocation(db *sql.DB) *time.Location {
// DB wins over env. Empty string means "no override" — fall through.
if name, _ := models.GetSetting(db, settingTimezone); name != "" {
if loc, err := time.LoadLocation(name); err == nil {
activeLoc.Store(loc)
return loc
}
// Bad zone in DB — log and fall through so we don't crash on boot
// just because someone edited the DB by hand.
log.Printf("timezone: invalid DB value %q, falling back to TZ env/UTC", name)
}
// No DB override — Go's time.Local already reflects the TZ env var
// (auto-read by the stdlib at init). If TZ was unset it's UTC.
activeLoc.Store(time.Local)
return time.Local
}
// setActiveLocation validates name as an IANA zone and swaps the active
// location atomically. Empty name resets to time.Local (TZ env var). Returns
// the error from time.LoadLocation so postSettings can surface it to the
// admin instead of silently saving garbage.
func setActiveLocation(name string) error {
if name == "" {
activeLoc.Store(time.Local)
return nil
}
loc, err := time.LoadLocation(name)
if err != nil {
return err
}
activeLoc.Store(loc)
return nil
}
// activeLocation returns the timezone every template fmtDate/fmtDateTime call
// converts through. Safe to call from any goroutine. Falls back to UTC if
// somehow never initialised (shouldn't happen — New calls loadActiveLocation).
func activeLocation() *time.Location {
if loc := activeLoc.Load(); loc != nil {
return loc
}
return time.UTC
}
// commonTimezones is the short list rendered in the Settings → Timezone
// dropdown. Covers most CaddyUI users without forcing them to type an IANA
// name. Users who need a zone outside this list can pick "Other…" and type
// one; time.LoadLocation accepts anything tzdata knows about.
//
// Order: UTC first (safe default), then Americas → Europe → Asia → Oceania
// roughly west-to-east within each region so the list reads like a map.
var commonTimezones = []string{
"UTC",
"America/Los_Angeles",
"America/Denver",
"America/Chicago",
"America/New_York",
"America/Sao_Paulo",
"Europe/London",
"Europe/Paris",
"Europe/Berlin",
"Europe/Moscow",
"Africa/Johannesburg",
"Asia/Dubai",
"Asia/Kolkata",
"Asia/Singapore",
"Asia/Shanghai",
"Asia/Tokyo",
"Australia/Sydney",
"Pacific/Auckland",
}