From 8f955a0ff9d7b7086bbaa58aaa41aba2123e2cf0 Mon Sep 17 00:00:00 2001 From: lorentz Date: Thu, 7 May 2026 08:43:27 -0400 Subject: [PATCH] feat(07.1-05): user-tz on shared client components MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DetailModal: thread tz through resolveLabel(...) module helper + default export's 3 inline date/time calls. - IntegrationStatusTabs: thread tz through fmtDate helper + VeeamTab sub-component prop. - SyncScheduler: thread tz into closure-scoped formatDate helper. - audit-log-table, user-table, user-sessions, active-sessions: inline toLocale calls in component body. - analysis-view: useUserTimezone in AnalysisView; thread tz into 4 toLocaleString calls. - resolution-trend, volume-trend (recharts): module-scope fmtDate(iso) → fmtDate(iso, tz); useUserTimezone in named export; thread tz into axis tickFormatter + tooltip labelFormatter. - ticket-detail-modal: thread tz into formatDate arrow inside TicketDetailModal. - TimelineView: useUserTimezone; thread tz into 4 toLocale*String calls (hour/day/month/event-time formatters). - ScoreCard: useUserTimezone in AggregateScoreCard; thread tz into the date-range latest call. - addigy-tab: useUserTimezone in AddigyTab; thread tz into 2 inline calls. - activity-sparkline: module-scope fmtHour(iso) → fmtHour(iso, tz); useUserTimezone in ActivitySparkline; update 3 callsites in title/aria. - compliance-detail-table: thread tz from ComplianceDetailTable into ContractCoverageModal sub-component (2 inline date calls). - company-backup-detail: module-scope formatDate(d) → formatDate(d, tz); useUserTimezone in CompanyBackupDetail; update 3 callsites. Migrates 31 of 81 audit leak callsites. --- components/admin/DetailModal.tsx | 20 ++++++++++--------- components/admin/IntegrationStatusTabs.tsx | 12 ++++++----- components/admin/SyncScheduler.tsx | 4 +++- components/admin/audit/audit-log-table.tsx | 4 +++- components/admin/users/user-sessions.tsx | 6 ++++-- components/admin/users/user-table.tsx | 4 +++- components/analytics/ScoreCard.tsx | 4 +++- components/analytics/TimelineView.tsx | 17 ++++++++++------ components/analyzer/analysis-view.tsx | 10 ++++++---- components/backup/company-backup-detail.tsx | 12 ++++++----- components/backup/compliance-detail-table.tsx | 9 +++++++-- components/configuration-items/addigy-tab.tsx | 6 ++++-- components/dashboard/resolution-trend.tsx | 10 ++++++---- components/dashboard/volume-trend.tsx | 10 ++++++---- components/quotes/ticket-detail-modal.tsx | 4 +++- components/settings/active-sessions.tsx | 4 +++- components/status/activity-sparkline.tsx | 11 ++++++---- 17 files changed, 94 insertions(+), 53 deletions(-) diff --git a/components/admin/DetailModal.tsx b/components/admin/DetailModal.tsx index ef6df4b..ec3a476 100644 --- a/components/admin/DetailModal.tsx +++ b/components/admin/DetailModal.tsx @@ -22,6 +22,7 @@ import { paletteClass, } from '@/lib/status-registry'; import { useState, useEffect } from 'react'; +import { useUserTimezone } from '@/lib/hooks/use-user-timezone'; // ── Live lookup types (fetched from DB) ─────────────────────────────────────── @@ -132,7 +133,7 @@ const COMPANY_GROUPS: FieldGroup[] = [ // ── Helpers ──────────────────────────────────────────────────────────────────── -function resolveLabel(key: string, value: any, type: FieldType | undefined, lookups: Lookups): { display: React.ReactNode; isEmpty: boolean } { +function resolveLabel(key: string, value: any, type: FieldType | undefined, lookups: Lookups, tz: string): { display: React.ReactNode; isEmpty: boolean } { if (value === null || value === undefined || value === '') { return { display: , isEmpty: true }; } @@ -157,7 +158,7 @@ function resolveLabel(key: string, value: any, type: FieldType | undefined, look display: ( - {d.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' })} + {d.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric', timeZone: tz })} ), isEmpty: false, @@ -246,7 +247,7 @@ function resolveLabel(key: string, value: any, type: FieldType | undefined, look } if (typeof value === 'string' && value.match(/^\d{4}-\d{2}-\d{2}/)) { - return resolveLabel(key, value, 'date', lookups); + return resolveLabel(key, value, 'date', lookups, tz); } return { display: {String(value)}, isEmpty: false }; @@ -271,6 +272,7 @@ interface DetailModalProps { } export default function DetailModal({ open, onOpenChange, title, data, fields }: DetailModalProps) { + const tz = useUserTimezone(); const [copiedField, setCopiedField] = useState(null); const [lookups, setLookups] = useState(EMPTY_LOOKUPS); const [lookupsLoading, setLookupsLoading] = useState(false); @@ -427,7 +429,7 @@ export default function DetailModal({ open, onOpenChange, title, data, fields }:
{visibleFields.map((field) => { const value = data[field.key]; - const { display, isEmpty } = resolveLabel(field.key, value, field.type, lookups); + const { display, isEmpty } = resolveLabel(field.key, value, field.type, lookups, tz); if (isEmpty) return null; return (
@@ -457,7 +459,7 @@ export default function DetailModal({ open, onOpenChange, title, data, fields }:
{fields.map((field, idx) => { const value = data[field.key]; - const { display, isEmpty } = resolveLabel(field.key, value, field.type, lookups); + const { display, isEmpty } = resolveLabel(field.key, value, field.type, lookups, tz); const stringValue = value !== null && value !== undefined ? String(value) : ''; return (
@@ -504,7 +506,7 @@ export default function DetailModal({ open, onOpenChange, title, data, fields }:
{visibleFields.map((field, idx) => { const value = data[field.key]; - const { display, isEmpty } = resolveLabel(field.key, value, field.type, lookups); + const { display, isEmpty } = resolveLabel(field.key, value, field.type, lookups, tz); const stringValue = value !== null && value !== undefined ? String(value) : ''; return (
@@ -607,7 +609,7 @@ export default function DetailModal({ open, onOpenChange, title, data, fields }: {entry.entry_date && ( - {new Date(entry.entry_date).toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })} + {new Date(entry.entry_date).toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric', timeZone: tz })} )}
@@ -653,9 +655,9 @@ export default function DetailModal({ open, onOpenChange, title, data, fields }: {note.create_date_time && ( - {new Date(note.create_date_time).toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' })} + {new Date(note.create_date_time).toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric', timeZone: tz })} {' '} - {new Date(note.create_date_time).toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' })} + {new Date(note.create_date_time).toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit', timeZone: tz })} )}
diff --git a/components/admin/IntegrationStatusTabs.tsx b/components/admin/IntegrationStatusTabs.tsx index 48bd0de..1f0f6c3 100644 --- a/components/admin/IntegrationStatusTabs.tsx +++ b/components/admin/IntegrationStatusTabs.tsx @@ -8,6 +8,7 @@ import { CheckCircle2, XCircle, AlertTriangle, RefreshCw, Loader2, Server, HardDrive, Cpu, Clock, } from 'lucide-react'; +import { useUserTimezone } from '@/lib/hooks/use-user-timezone'; function StatusDot({ ok, warn }: { ok: boolean; warn?: boolean }) { if (!ok) return ; @@ -31,12 +32,12 @@ function StatCard({ label, value, sub, icon: Icon, cls }: { ); } -function fmtDate(d: string | null) { +function fmtDate(d: string | null, tz: string) { if (!d) return 'Never'; - return new Date(d).toLocaleString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); + return new Date(d).toLocaleString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', timeZone: tz }); } -function VeeamTab({ data, onSync, syncing }: { data: any; onSync: () => void; syncing: boolean }) { +function VeeamTab({ data, onSync, syncing, tz }: { data: any; onSync: () => void; syncing: boolean; tz: string }) { if (!data) return
; const aj = data.agentJobs ?? {}; const bj = data.backupJobs ?? {}; @@ -50,7 +51,7 @@ function VeeamTab({ data, onSync, syncing }: { data: any; onSync: () => void; sy 0 || totalWarning > 0} />

{data.configured ? 'Connected to VSPC' : 'Not configured'}

-

Last sync: {fmtDate(data.lastSync)}

+

Last sync: {fmtDate(data.lastSync, tz)}

- {new Date(session.created_at).toLocaleString()} + {new Date(session.created_at).toLocaleString(undefined, { timeZone: tz })} - {new Date(session.expires_at).toLocaleString()} + {new Date(session.expires_at).toLocaleString(undefined, { timeZone: tz })}
diff --git a/components/analytics/TimelineView.tsx b/components/analytics/TimelineView.tsx index b5d05f5..1c5b13d 100644 --- a/components/analytics/TimelineView.tsx +++ b/components/analytics/TimelineView.tsx @@ -9,6 +9,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@ import { Calendar, Clock, Users, ChevronDown, ChevronRight, Activity, AlertCircle, CheckCircle } from 'lucide-react'; import { TimelineEvent, TimelineView as TimelineViewType } from '@/lib/types/analytics'; import { cn } from '@/lib/utils'; +import { useUserTimezone } from '@/lib/hooks/use-user-timezone'; interface TimelineViewProps { events: TimelineEvent[]; @@ -18,13 +19,14 @@ interface TimelineViewProps { className?: string; } -export function TimelineView({ - events, - timeRange, - onTimeRangeChange, +export function TimelineView({ + events, + timeRange, + onTimeRangeChange, loading = false, - className + className }: TimelineViewProps) { + const tz = useUserTimezone(); const [expandedSections, setExpandedSections] = useState>(new Set()); const [selectedEvent, setSelectedEvent] = useState(null); @@ -135,12 +137,14 @@ export function TimelineView({ day: 'numeric', hour: 'numeric', hour12: true, + timeZone: tz, }); case 'day': return new Date(groupKey).toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', + timeZone: tz, }); case 'week': return groupKey; @@ -148,6 +152,7 @@ export function TimelineView({ return new Date(groupKey + '-01').toLocaleDateString('en-US', { month: 'long', year: 'numeric', + timeZone: tz, }); default: return groupKey; @@ -300,7 +305,7 @@ export function TimelineView({
- {new Date(event.timestamp).toLocaleTimeString()} + {new Date(event.timestamp).toLocaleTimeString(undefined, { timeZone: tz })} {event.duration && ( diff --git a/components/analyzer/analysis-view.tsx b/components/analyzer/analysis-view.tsx index e89234a..e0fa915 100644 --- a/components/analyzer/analysis-view.tsx +++ b/components/analyzer/analysis-view.tsx @@ -22,6 +22,7 @@ import { ShareModal } from './share-modal'; import { AnalyzeButton } from './analyze-button'; import { AnalysisMarkdown } from './analysis-markdown'; import type { PersistedAnalysis, Visibility } from '@/lib/types/analyzer'; +import { useUserTimezone } from '@/lib/hooks/use-user-timezone'; interface AnalysisViewProps { analysis: PersistedAnalysis; @@ -70,6 +71,7 @@ function ModelBadges({ a }: { a: PersistedAnalysis }) { } export function AnalysisView({ analysis: a }: AnalysisViewProps) { + const tz = useUserTimezone(); const [expandedEvent, setExpandedEvent] = useState(null); const [nextStepOpen, setNextStepOpen] = useState(false); @@ -112,7 +114,7 @@ export function AnalysisView({ analysis: a }: AnalysisViewProps) { )}
- AI Analysis · {new Date(a.triggeredAt).toLocaleString()} + AI Analysis · {new Date(a.triggeredAt).toLocaleString(undefined, { timeZone: tz })}

{a.totalInputTokens.toLocaleString()} in /{' '} @@ -219,7 +221,7 @@ export function AnalysisView({ analysis: a }: AnalysisViewProps) { {VISIBILITY_MARKER[event.visibility]} - {new Date(event.timestamp).toLocaleString()} + {new Date(event.timestamp).toLocaleString(undefined, { timeZone: tz })} {event.actor} @@ -307,7 +309,7 @@ export function AnalysisView({ analysis: a }: AnalysisViewProps) { onClick={() => jumpToEvent(ts)} className="underline mr-2 font-mono" > - {new Date(ts).toLocaleString()} + {new Date(ts).toLocaleString(undefined, { timeZone: tz })} ))}

@@ -398,7 +400,7 @@ export function AnalysisView({ analysis: a }: AnalysisViewProps) {

{a.timeline[expandedEvent].actor} ·{' '} - {new Date(a.timeline[expandedEvent].timestamp).toLocaleString()} + {new Date(a.timeline[expandedEvent].timestamp).toLocaleString(undefined, { timeZone: tz })}

{a.timeline[expandedEvent].action}

diff --git a/components/backup/company-backup-detail.tsx b/components/backup/company-backup-detail.tsx index c8af9fa..40605d5 100644 --- a/components/backup/company-backup-detail.tsx +++ b/components/backup/company-backup-detail.tsx @@ -12,6 +12,7 @@ import { TableHeader, TableRow, } from '@/components/ui/table'; +import { useUserTimezone } from '@/lib/hooks/use-user-timezone'; interface CompanyBackupDetailProps { companyId: number | null; @@ -43,12 +44,13 @@ function formatBytes(bytes: number | null): string { return `${val.toFixed(1)} ${units[i]}`; } -function formatDate(dateStr: string | null): string { +function formatDate(dateStr: string | null, tz: string): string { if (!dateStr) return 'Never'; - return new Date(dateStr).toLocaleString(); + return new Date(dateStr).toLocaleString(undefined, { timeZone: tz }); } export function CompanyBackupDetail({ companyId, companyName }: CompanyBackupDetailProps) { + const tz = useUserTimezone(); const [workloads, setWorkloads] = useState([]); const [jobs, setJobs] = useState<{ serverJobs: any[]; agentJobs: any[] }>({ serverJobs: [], agentJobs: [] }); const [compliance, setCompliance] = useState([]); @@ -110,7 +112,7 @@ export function CompanyBackupDetail({ companyId, companyName }: CompanyBackupDet {w.name} {w.restore_points ?? '-'} - {formatDate(w.latest_restore_point_date)} + {formatDate(w.latest_restore_point_date, tz)} {formatBytes(w.used_source_size)} ))} @@ -147,7 +149,7 @@ export function CompanyBackupDetail({ companyId, companyName }: CompanyBackupDet {j.name} {j.type || 'Server'} - {formatDate(j.last_run)} + {formatDate(j.last_run, tz)} {j.last_duration ? `${Math.round(j.last_duration / 60)}m` : '-'} ))} @@ -156,7 +158,7 @@ export function CompanyBackupDetail({ companyId, companyName }: CompanyBackupDet {j.name} {j.backup_mode || 'Agent'} - {formatDate(j.last_run)} + {formatDate(j.last_run, tz)} {j.last_duration ? `${Math.round(j.last_duration / 60)}m` : '-'} ))} diff --git a/components/backup/compliance-detail-table.tsx b/components/backup/compliance-detail-table.tsx index 44cd745..e5ac524 100644 --- a/components/backup/compliance-detail-table.tsx +++ b/components/backup/compliance-detail-table.tsx @@ -19,6 +19,7 @@ import { TableRow, } from '@/components/ui/table'; import { Search, CheckCircle2, XCircle, Loader2, ExternalLink, Package } from 'lucide-react'; +import { useUserTimezone } from '@/lib/hooks/use-user-timezone'; interface ComplianceMismatch { id: number; @@ -87,11 +88,13 @@ function ContractCoverageModal({ companyName, open, onClose, + tz, }: { contractId: number | null; companyName: string | null; open: boolean; onClose: () => void; + tz: string; }) { const [data, setData] = useState<{ contract: ContractDetail; services: ContractService[] } | null>(null); const [loading, setLoading] = useState(false); @@ -167,10 +170,10 @@ function ContractCoverageModal({
{contract.start_date && ( - Start: {new Date(contract.start_date).toLocaleDateString()} + Start: {new Date(contract.start_date).toLocaleDateString(undefined, { timeZone: tz })} )} {contract.end_date && ( - End: {new Date(contract.end_date).toLocaleDateString()} + End: {new Date(contract.end_date).toLocaleDateString(undefined, { timeZone: tz })} )} @@ -238,6 +241,7 @@ function ServiceTable({ services, highlight }: { services: ContractService[]; hi } export function ComplianceDetailTable({ mismatches }: ComplianceDetailTableProps) { + const tz = useUserTimezone(); const [search, setSearch] = useState(''); const [typeFilter, setTypeFilter] = useState('all'); const [modalContractId, setModalContractId] = useState(null); @@ -370,6 +374,7 @@ export function ComplianceDetailTable({ mismatches }: ComplianceDetailTableProps companyName={modalCompanyName} open={modalOpen} onClose={() => setModalOpen(false)} + tz={tz} />
); diff --git a/components/configuration-items/addigy-tab.tsx b/components/configuration-items/addigy-tab.tsx index bb17e55..53cf254 100644 --- a/components/configuration-items/addigy-tab.tsx +++ b/components/configuration-items/addigy-tab.tsx @@ -15,12 +15,14 @@ import { AlertCircle } from 'lucide-react'; import { AddigyDevice } from '@/lib/types/addigy'; +import { useUserTimezone } from '@/lib/hooks/use-user-timezone'; interface AddigyTabProps { device?: AddigyDevice; } export function AddigyTab({ device }: AddigyTabProps) { + const tz = useUserTimezone(); if (!device) { return ( @@ -104,7 +106,7 @@ export function AddigyTab({ device }: AddigyTabProps) {

- {new Date(device['Last Check In']).toLocaleString()} + {new Date(device['Last Check In']).toLocaleString(undefined, { timeZone: tz })}

)} @@ -324,7 +326,7 @@ export function AddigyTab({ device }: AddigyTabProps) {

- Expires: {new Date(device['Warranty Expiration Date']).toLocaleDateString()} + Expires: {new Date(device['Warranty Expiration Date']).toLocaleDateString(undefined, { timeZone: tz })} {device['Warranty Days Left'] !== undefined && ( ({device['Warranty Days Left']} days left) )} diff --git a/components/dashboard/resolution-trend.tsx b/components/dashboard/resolution-trend.tsx index 5dc7876..a231573 100644 --- a/components/dashboard/resolution-trend.tsx +++ b/components/dashboard/resolution-trend.tsx @@ -12,6 +12,7 @@ import { XAxis, YAxis, } from 'recharts'; +import { useUserTimezone } from '@/lib/hooks/use-user-timezone'; interface ResolutionPoint { date: string; @@ -23,18 +24,19 @@ interface ResolutionTrendProps { height?: number; } -function fmtDate(iso: string) { - return new Date(iso).toLocaleDateString(undefined, { month: 'short', day: 'numeric' }); +function fmtDate(iso: string, tz: string) { + return new Date(iso).toLocaleDateString(undefined, { month: 'short', day: 'numeric', timeZone: tz }); } export function ResolutionTrend({ data, height = 180 }: ResolutionTrendProps) { + const tz = useUserTimezone(); return ( fmtDate(iso, tz)} interval="preserveStartEnd" minTickGap={48} tick={{ fontSize: 11, fill: 'var(--muted-foreground)' }} @@ -55,7 +57,7 @@ export function ResolutionTrend({ data, height = 180 }: ResolutionTrendProps) { borderRadius: 6, fontSize: 12, }} - labelFormatter={(value) => fmtDate(String(value))} + labelFormatter={(value) => fmtDate(String(value), tz)} formatter={(value) => value == null ? ['—', 'avg'] : [`${Number(value).toFixed(1)} h`, 'avg'] } diff --git a/components/dashboard/volume-trend.tsx b/components/dashboard/volume-trend.tsx index 5b87a74..51c5b05 100644 --- a/components/dashboard/volume-trend.tsx +++ b/components/dashboard/volume-trend.tsx @@ -13,6 +13,7 @@ import { XAxis, YAxis, } from 'recharts'; +import { useUserTimezone } from '@/lib/hooks/use-user-timezone'; interface VolumePoint { date: string; @@ -24,11 +25,12 @@ interface VolumeTrendProps { height?: number; } -function fmtDate(iso: string) { - return new Date(iso).toLocaleDateString(undefined, { month: 'short', day: 'numeric' }); +function fmtDate(iso: string, tz: string) { + return new Date(iso).toLocaleDateString(undefined, { month: 'short', day: 'numeric', timeZone: tz }); } export function VolumeTrend({ data, height = 180 }: VolumeTrendProps) { + const tz = useUserTimezone(); return ( @@ -40,7 +42,7 @@ export function VolumeTrend({ data, height = 180 }: VolumeTrendProps) { fmtDate(iso, tz)} interval="preserveStartEnd" minTickGap={48} tick={{ fontSize: 11, fill: 'var(--muted-foreground)' }} @@ -61,7 +63,7 @@ export function VolumeTrend({ data, height = 180 }: VolumeTrendProps) { borderRadius: 6, fontSize: 12, }} - labelFormatter={(value) => fmtDate(String(value))} + labelFormatter={(value) => fmtDate(String(value), tz)} formatter={(value) => [value ?? 0, 'opened']} /> (null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); @@ -230,7 +232,7 @@ export function TicketDetailModal({ ticketNumber, open, onOpenChange }: TicketDe if (!dateString) return 'N/A'; return new Date(dateString).toLocaleString('en-US', { month: 'short', day: 'numeric', year: 'numeric', - hour: '2-digit', minute: '2-digit', + hour: '2-digit', minute: '2-digit', timeZone: tz, }); }; diff --git a/components/settings/active-sessions.tsx b/components/settings/active-sessions.tsx index 5b567fb..28bdb12 100644 --- a/components/settings/active-sessions.tsx +++ b/components/settings/active-sessions.tsx @@ -5,6 +5,7 @@ import { Loader2, Monitor, Smartphone, Trash2, Globe } from "lucide-react"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; +import { useUserTimezone } from "@/lib/hooks/use-user-timezone"; interface Session { id: string; @@ -32,6 +33,7 @@ function parseUserAgent(ua: string): { device: string; browser: string } { } export function ActiveSessions({ userId }: ActiveSessionsProps) { + const tz = useUserTimezone(); const [sessions, setSessions] = useState([]); const [isLoading, setIsLoading] = useState(true); const [revokingId, setRevokingId] = useState(null); @@ -115,7 +117,7 @@ export function ActiveSessions({ userId }: ActiveSessionsProps) { {session.ip_address || "Unknown IP"} - {new Date(session.created_at).toLocaleDateString()} + {new Date(session.created_at).toLocaleDateString(undefined, { timeZone: tz })}

diff --git a/components/status/activity-sparkline.tsx b/components/status/activity-sparkline.tsx index 1265540..02544bf 100644 --- a/components/status/activity-sparkline.tsx +++ b/components/status/activity-sparkline.tsx @@ -13,6 +13,7 @@ 'use client'; import { cn } from '@/lib/utils'; +import { useUserTimezone } from '@/lib/hooks/use-user-timezone'; interface ActivityBucket { hour: string; @@ -26,10 +27,11 @@ interface ActivitySparklineProps { height?: number; } -function fmtHour(iso: string): string { +function fmtHour(iso: string, tz: string): string { return new Date(iso).toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit', + timeZone: tz, }); } @@ -38,6 +40,7 @@ export function ActivitySparkline({ className, height = 32, }: ActivitySparklineProps) { + const tz = useUserTimezone(); if (data.length === 0) { return null; } @@ -57,10 +60,10 @@ export function ActivitySparkline({ key={bucket.hour} title={ empty - ? `${fmtHour(bucket.hour)} · idle` - : `${fmtHour(bucket.hour)} · ${bucket.success} ok · ${bucket.failure} fail` + ? `${fmtHour(bucket.hour, tz)} · idle` + : `${fmtHour(bucket.hour, tz)} · ${bucket.success} ok · ${bucket.failure} fail` } - aria-label={`${fmtHour(bucket.hour)}: ${bucket.success} ok, ${bucket.failure} fail`} + aria-label={`${fmtHour(bucket.hour, tz)}: ${bucket.success} ok, ${bucket.failure} fail`} className="relative flex-1 min-w-[1px] flex flex-col-reverse rounded-[1px] overflow-hidden" style={{ height: `${empty ? 12 : Math.max(totalPct, 8)}%` }} data-bucket-index={i}