notifications: correct timezone handling + configurable site timezone

Notification start/end times displayed and stored wrong by the tz offset
(a 2:34 PM entry showed 6:34 PM). Two stacked bugs: to_dict emitted stored
UTC as naive ISO (no offset) so the browser read it as local, and the form
filled the datetime-local input from toISOString() (UTC).

Fix and generalize to a configurable site timezone (multi-site):
- New setting site_timezone (default America/New_York), public, editable in
  Settings > Site > Localization (common-zone dropdown).
- Backend tags datetimes UTC (_utc_iso); parse normalizes to naive UTC
  (_parse_utc); daily-reset expiry uses the site zone (_next_site_time);
  calendar allDay events key off the site-local day (_site_date).
- Shared frontend util datetime.js (Intl-based, DST-safe) converts between a
  UTC instant and a site-zone wall clock. Notification form, list, and
  calendar all render/enter in the site zone.
This commit is contained in:
cproudlock
2026-07-30 15:08:59 -04:00
parent 3ad26ba010
commit ea6fae91c3
9 changed files with 920 additions and 40 deletions

View File

@@ -74,6 +74,7 @@ const LABELS = {
dualpath_single_machine: 'Dualpath as Single Machine',
employee_directory_mode: 'Employee Directory Mode',
usb_directory_mode: 'USB Directory Mode',
site_timezone: 'Site Timezone',
setup_complete: 'Setup Complete'
}
function prettyLabel(key) {
@@ -83,7 +84,12 @@ function prettyLabel(key) {
// Enumerated settings render as dropdowns to prevent typos.
const OPTIONS = {
employee_directory_mode: ['selfhosted', 'external'],
usb_directory_mode: ['selfhosted', 'external']
usb_directory_mode: ['selfhosted', 'external'],
site_timezone: [
'America/New_York', 'America/Chicago', 'America/Denver', 'America/Phoenix',
'America/Los_Angeles', 'America/Anchorage', 'Pacific/Honolulu',
'America/Indiana/Indianapolis'
]
}
function isTrue(setting) {
@@ -97,6 +103,7 @@ const GROUPS = [
{ title: 'Behavior', keys: ['dualpath_single_machine'] },
{ title: 'Naming & Patterns', keys: ['pc_access_domain', 'printer_hostname_template', 'contact_email_domain', 'employeeid_pattern'] },
{ title: 'Data Sources', keys: ['employee_directory_mode', 'usb_directory_mode'] },
{ title: 'Localization', keys: ['site_timezone'] },
{ title: 'System', keys: ['setup_complete'] }
]
@@ -108,7 +115,8 @@ const HELP = {
employeeid_pattern: 'Regular expression that a scanned/typed employee ID must match to be recognized. Default: ^\\d{9}$ (9 digits). An invalid regex is ignored and the default is used.',
printer_hostname_template: 'Template for generating printer hostnames from an IP. Use {ip} where the dash-separated IP goes. Example: Printer-{ip}.printer.geaerospace.net',
contact_email_domain: 'Email domain appended to a support contact SSO to build email (sso@domain) and Microsoft Teams chat links. Example: geaerospace.com. Leave blank to hide the contact action buttons.',
dualpath_single_machine: 'Treat a Dualpath pair (a dual-bay machine with one controller) as a single machine in lists, counts, and the floor map. Both bay records are always kept; detail pages stay per-bay with a sibling banner. Enter true or false. Default: true.'
dualpath_single_machine: 'Treat a Dualpath pair (a dual-bay machine with one controller) as a single machine in lists, counts, and the floor map. Both bay records are always kept; detail pages stay per-bay with a sibling banner. Enter true or false. Default: true.',
site_timezone: 'IANA timezone for this site. Notification start/end times are shown and entered in this zone, and daily-reset notification expiry is computed here. Default: America/New_York.'
}
function fieldHelp(key) {
return HELP[key] || ''