shopfloor dashboard: show site time, and align the state badge
Some checks failed
CI / backend (push) Failing after 8s
CI / naming (push) Successful in 2s
CI / frontend (push) Successful in 9s
CI / migrations-mysql (push) Failing after 7s

Every time on the board was wrong, from two faults stacked.

The shopfloor feed serialised starttime/endtime with a bare isoformat(). Those
columns are stored NAIVE but hold UTC, so an untagged string is read by the
browser as LOCAL and every card shifted by the tz offset. The model's to_dict
already learned this - its _utc_iso helper documents the exact symptom, a 14:34
notification showing 18:34 - but the feed had not, so the feed now uses it too.

The dashboard then formatted with toLocaleString, i.e. the VIEWER's zone. A
board hangs on a wall in the plant: it has to read plant time whatever the
machine driving it is set to, and a kiosk with a wrong system timezone would
otherwise show wrong times to the floor with nothing to reveal it. It now
loads site_timezone and formats through formatInZone, the wall clock included -
a header disagreeing with the cards beneath it is worse than either being
wrong alone.

startsWhen was worse still: it decided TODAY/TOMORROW from browser-local
calendar days, so the wording itself could differ between the board and a
remote admin looking at the same card. That arithmetic now runs on the site's
calendar day.

Separately, the type chip carried a margin-bottom while the state chip beside
it did not. .chip-row centres each item's MARGIN box, so that margin lifted the
type chip about 4px and left "Starts Thu, Aug 13 8:00 PM" looking low. The row
already provides the spacing, so the chip's own margin is gone.
This commit is contained in:
cproudlock
2026-08-10 08:34:31 -04:00
parent efe34034e3
commit 4d3985ce22
2 changed files with 53 additions and 20 deletions

View File

@@ -237,7 +237,8 @@
<script setup>
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
import { notificationsApi, businessUnitsApi, dashboardDefaultsApi } from '@/api'
import { notificationsApi, businessUnitsApi, dashboardDefaultsApi, settingsApi } from '@/api'
import { formatInZone, zonedInputFromUtc, DEFAULT_TZ } from '@/utils/datetime'
import { getFacilityName, getSiteLogo, getServicenowUrls } from '@/utils/siteSettings'
import { withBase } from '@/utils/basePath'
@@ -386,17 +387,30 @@ const upcomingNotifications = computed(() =>
)
// Clock
// Site timezone drives every time on the board. Defaults until the setting
// loads, so a slow settings call shows plausible time rather than blank.
const siteTimezone = ref(DEFAULT_TZ)
const currentTime = ref('')
function updateClock() {
const now = new Date()
currentTime.value = now.toLocaleTimeString('en-US', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
// The wall clock too: a board showing a different time to the cards beneath
// it is worse than either being wrong on its own.
currentTime.value = formatInZone(new Date(), siteTimezone.value, {
year: undefined, month: undefined, day: undefined,
hour: '2-digit', minute: '2-digit', second: '2-digit'
})
}
onMounted(async () => {
// Before the first clock tick, so the board never renders viewer-zone time.
try {
const response = await settingsApi.get('site_timezone')
const value = response?.data?.data?.value
if (value) siteTimezone.value = value
} catch (e) {
// Keep the default zone; the board must still come up.
}
updateClock()
setInterval(updateClock, 1000)
@@ -528,15 +542,24 @@ function stateKind(item) {
// timestamp alone makes a reader do the arithmetic.
function startsWhen(dateStr) {
if (!dateStr) return 'SOON'
const start = new Date(dateStr)
const time = start.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' })
const startday = new Date(start.getFullYear(), start.getMonth(), start.getDate())
const now = new Date()
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate())
const days = Math.round((startday - today) / 86400000)
const time = formatInZone(dateStr, siteTimezone.value, {
year: undefined, month: undefined, day: undefined,
hour: 'numeric', minute: '2-digit'
})
// TODAY/TOMORROW has to be decided on the SITE's calendar day. Doing the
// arithmetic on browser-local days makes the wording itself wrong for a
// viewer in another zone - a card can read TOMORROW to a remote admin and
// TODAY on the board, from the same data.
const startday = zonedInputFromUtc(dateStr, siteTimezone.value).slice(0, 10)
const todayday = zonedInputFromUtc(new Date(), siteTimezone.value).slice(0, 10)
const days = Math.round(
(new Date(`${startday}T00:00:00`) - new Date(`${todayday}T00:00:00`)) / 86400000)
if (days <= 0) return `TODAY ${time}`
if (days === 1) return `TOMORROW ${time}`
const when = start.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' })
const when = formatInZone(dateStr, siteTimezone.value, {
weekday: 'short', month: 'short', day: 'numeric',
hour: undefined, minute: undefined
})
return `${when} ${time}`
}
@@ -604,17 +627,20 @@ function textOn(backgroundcolor) {
return (0.2126 * r + 0.7152 * g + 0.0722 * b) > 150 ? '#231b00' : '#fff'
}
// SITE zone, not the viewer's. A board hangs on a wall in the plant: it must
// read plant time whatever the machine driving it is set to, and a kiosk with a
// wrong system timezone would otherwise silently show wrong times to the floor.
function formatTime(dateStr) {
if (!dateStr) return ''
return new Date(dateStr).toLocaleTimeString('en-US', {
hour: '2-digit',
minute: '2-digit'
return formatInZone(dateStr, siteTimezone.value, {
year: undefined, month: undefined, day: undefined,
hour: '2-digit', minute: '2-digit'
})
}
function formatDateTime(dateStr) {
if (!dateStr) return ''
return new Date(dateStr).toLocaleString('en-US', {
return formatInZone(dateStr, siteTimezone.value, {
weekday: 'short',
month: 'short',
day: 'numeric',
@@ -935,7 +961,9 @@ function handlePhotoError(e) {
font-weight: 700;
letter-spacing: 1px;
text-transform: uppercase;
margin-bottom: 8px;
/* No margin-bottom. .chip-row centres each item's MARGIN box, so a bottom
margin here lifted this chip ~4px and left the state chip beside it
looking low. The row already provides the spacing underneath. */
}
/* Type colour as a bar down the left edge, the way the legacy board marked a