diff --git a/docs/DOCKER.md b/docs/DOCKER.md index 55e17f1f..59af9410 100644 --- a/docs/DOCKER.md +++ b/docs/DOCKER.md @@ -26,6 +26,10 @@ LOCATOR=FN20 TZ=America/New_York ``` +> **`TZ` is optional** — if omitted, each visitor's browser timezone is used for +> the local-time display. Setting it is still recommended so that server-side +> timestamps (logs, cache TTLs, etc.) match your local time. + Restart to apply: ```bash diff --git a/server/routes/config-routes.js b/server/routes/config-routes.js index e04eb7ab..ec9f88e3 100644 --- a/server/routes/config-routes.js +++ b/server/routes/config-routes.js @@ -173,8 +173,23 @@ module.exports = function (app, ctx) { // Whether config is incomplete (show setup wizard) configIncomplete: CONFIG.callsign === 'N0CALL' || !CONFIG.gridSquare, - // Server timezone (from TZ env var or system) - timezone: process.env.TZ || Intl.DateTimeFormat().resolvedOptions().timeZone || '', + // Server timezone (from TZ env var or system), validated. + // On minimal Linux containers without TZ set, Intl can return "Etc/Unknown" + // which browsers reject with RangeError. Validate before sending. + timezone: (() => { + const tz = process.env.TZ || Intl.DateTimeFormat().resolvedOptions().timeZone || ''; + if (!tz) return ''; + try { + new Intl.DateTimeFormat(undefined, { timeZone: tz }); + return tz; + } catch (e) { + console.warn( + '[config] Invalid resolved timezone "%s" — falling back to empty (client will use browser TZ). Set TZ env var to silence.', + tz, + ); + return ''; + } + })(), // Feature availability features: { diff --git a/src/hooks/app/useTimeState.js b/src/hooks/app/useTimeState.js index 5b34e822..b6ff7af2 100644 --- a/src/hooks/app/useTimeState.js +++ b/src/hooks/app/useTimeState.js @@ -61,11 +61,24 @@ export default function useTimeState(configLocation, dxLocation, timezone) { const utcTime = currentTime.toISOString().substr(11, 8); const utcDate = currentTime.toISOString().substr(0, 10); + // Validate the timezone once per changed value, not on every render. + // new Intl.DateTimeFormat throws a RangeError for invalid values such as + // "Etc/Unknown" (returned by Node on minimal Linux containers with no TZ set). + const safeTimezone = useMemo(() => { + if (!timezone) return ''; + try { + new Intl.DateTimeFormat(undefined, { timeZone: timezone }); + return timezone; + } catch { + return ''; + } + }, [timezone]); + const localTimeOpts = { hour12: use12Hour }; const localDateOpts = { weekday: 'short', month: 'short', day: 'numeric' }; - if (timezone) { - localTimeOpts.timeZone = timezone; - localDateOpts.timeZone = timezone; + if (safeTimezone) { + localTimeOpts.timeZone = safeTimezone; + localDateOpts.timeZone = safeTimezone; } const localTime = currentTime.toLocaleTimeString('en-US', localTimeOpts); const localDate = currentTime.toLocaleDateString('en-US', localDateOpts);