Ctrl K
ComunidadGuías…

internal/server/timezone.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// 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",
}