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

@@ -88,9 +88,10 @@
<script setup>
import { ref, onMounted } from 'vue'
import { notificationsApi } from '@/api'
import { notificationsApi, settingsApi } from '@/api'
import PaginationBar from '@/components/PaginationBar.vue'
import { useListQuery } from '@/composables/listQuery'
import { formatInZone, DEFAULT_TZ } from '@/utils/datetime'
const notifications = ref([])
const types = ref([])
@@ -101,10 +102,16 @@ const currentFilter = ref('')
const perPage = ref(20)
const total = ref(0)
const totalPages = ref(1)
const siteTimezone = ref(DEFAULT_TZ)
let searchTimeout = null
onMounted(async () => {
try {
const tzRes = await settingsApi.get('site_timezone')
const tzValue = tzRes?.data?.data?.value
if (tzValue) siteTimezone.value = tzValue
} catch (e) { /* keep default tz */ }
await loadTypes()
await loadNotifications()
})
@@ -170,6 +177,9 @@ function changePerPage(newPerPage) {
function formatDate(dateStr) {
if (!dateStr) return ''
return new Date(dateStr).toLocaleDateString()
// startdate/enddate are UTC; show the site-zone calendar date.
return formatInZone(dateStr, siteTimezone.value, {
year: 'numeric', month: 'numeric', day: 'numeric', hour: undefined, minute: undefined
})
}
</script>