feat(07.1-05): user-tz on admin sync pages

- duo, sentinelone, datto-rmm, veeam, itglue, mimecast: each gets
  useUserTimezone() in default export and threads tz through
  fmtDate/sub-component props.
- duo (1 callsite, closure inline), sentinelone (1, module-scope helper),
  datto-rmm (1 helper + StatusTab/HistoryTab props), veeam (1 helper +
  5 sub-components), itglue (1 helper + StatusTab/HistoryTab props),
  mimecast (1 helper + 6 sub-components incl. 2 dialogs with inline
  toLocaleString calls).
- Module-scope fmtDate(d) signatures converted to fmtDate(d, tz).

Migrates 13 of 81 audit leak callsites.
This commit is contained in:
lorentz 2026-05-07 08:28:42 -04:00
parent a709144685
commit 8c56cafe0b
6 changed files with 80 additions and 64 deletions

View file

@ -17,10 +17,11 @@ import {
ArrowLeft, Activity, History, Monitor, Loader2, RefreshCw,
ExternalLink, Server, Wifi, WifiOff, AlertTriangle, Bell, XCircle, CheckCircle2, Clock,
} from 'lucide-react';
import { useUserTimezone } from '@/lib/hooks/use-user-timezone';
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', year: 'numeric', hour: '2-digit', minute: '2-digit' });
return new Date(d).toLocaleString(undefined, { month: 'short', day: 'numeric', year: 'numeric', hour: '2-digit', minute: '2-digit', timeZone: tz });
}
function StatCard({ label, value, sub, icon: Icon, cls }: { label: string; value: string | number; sub?: string; icon?: React.ElementType; cls?: string }) {
@ -35,7 +36,7 @@ function StatCard({ label, value, sub, icon: Icon, cls }: { label: string; value
);
}
function StatusTab({ data, onSync, syncing }: { data: any; onSync: () => void; syncing: boolean }) {
function StatusTab({ data, onSync, syncing, tz }: { data: any; onSync: () => void; syncing: boolean; tz: string }) {
if (!data) return <div className="flex items-center justify-center py-12"><Loader2 className="w-5 h-5 animate-spin text-muted-foreground" /></div>;
const devs = data.devices ?? {};
@ -46,7 +47,7 @@ function StatusTab({ data, onSync, syncing }: { data: any; onSync: () => void; s
<div className="flex items-center justify-between rounded-lg border p-4 bg-muted/30">
<div className="space-y-0.5">
<p className="text-sm font-medium">{data.configured ? 'Connected' : 'Not configured'}</p>
<p className="text-xs text-muted-foreground">Last sync: {fmtDate(data.lastSync)}</p>
<p className="text-xs text-muted-foreground">Last sync: {fmtDate(data.lastSync, tz)}</p>
</div>
<div className="flex gap-2">
<a href="https://concord.rmm.datto.com" target="_blank" rel="noopener noreferrer">
@ -99,7 +100,7 @@ function StatusTab({ data, onSync, syncing }: { data: any; onSync: () => void; s
);
}
function HistoryTab({ refreshKey }: { refreshKey: number }) {
function HistoryTab({ refreshKey, tz }: { refreshKey: number; tz: string }) {
const [rows, setRows] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
@ -140,7 +141,7 @@ function HistoryTab({ refreshKey }: { refreshKey: number }) {
<StatusBadge tone={tone}>{row.status}</StatusBadge>
</TableCell>
<TableCell className="text-right num">{row.records_added ?? 0}</TableCell>
<TableCell className="text-muted-foreground num">{fmtDate(row.started_at)}</TableCell>
<TableCell className="text-muted-foreground num">{fmtDate(row.started_at, tz)}</TableCell>
<TableCell className="text-right text-muted-foreground num">
{dur != null ? `${dur}s` : '—'}
</TableCell>
@ -154,6 +155,7 @@ function HistoryTab({ refreshKey }: { refreshKey: number }) {
}
export default function DattoRmmPage() {
const tz = useUserTimezone();
const [status, setStatus] = useState<any>(null);
const [syncing, setSyncing] = useState(false);
const [refreshKey, setRefreshKey] = useState(0);
@ -222,10 +224,10 @@ export default function DattoRmmPage() {
</TabsTrigger>
</TabsList>
<TabsContent value="status" className="mt-6">
<StatusTab data={status} onSync={handleSync} syncing={syncing} />
<StatusTab data={status} onSync={handleSync} syncing={syncing} tz={tz} />
</TabsContent>
<TabsContent value="history" className="mt-6">
<HistoryTab refreshKey={refreshKey} />
<HistoryTab refreshKey={refreshKey} tz={tz} />
</TabsContent>
<TabsContent value="about" className="mt-6">
<div className="space-y-4 text-sm text-muted-foreground">

View file

@ -12,6 +12,7 @@ import {
TableRow,
} from '@/components/ui/table';
import { RefreshCw, ArrowLeft, Loader2, CheckCircle2, AlertTriangle, Shield, Users, Smartphone, ScrollText, Layers, AppWindow, ChevronDown, ChevronUp, ShieldOff, ShieldAlert, ShieldX } from 'lucide-react';
import { useUserTimezone } from '@/lib/hooks/use-user-timezone';
interface DuoStatus {
connected: boolean;
@ -55,6 +56,7 @@ interface DuoAccount {
}
export default function DuoSyncPage() {
const tz = useUserTimezone();
const [status, setStatus] = useState<DuoStatus | null>(null);
const [accounts, setAccounts] = useState<DuoAccount[]>([]);
const [loading, setLoading] = useState(true);
@ -114,7 +116,7 @@ export default function DuoSyncPage() {
const fmtDate = (d: string | null) => {
if (!d) return 'Never';
return new Date(d).toLocaleString();
return new Date(d).toLocaleString(undefined, { timeZone: tz });
};
if (loading) {

View file

@ -18,12 +18,13 @@ import {
ExternalLink, CheckCircle2, XCircle, Clock, AlertTriangle,
Building2, Monitor, Users, Key, FileText, Globe, Shield, Package,
} from 'lucide-react';
import { useUserTimezone } from '@/lib/hooks/use-user-timezone';
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', year: 'numeric',
hour: '2-digit', minute: '2-digit',
hour: '2-digit', minute: '2-digit', timeZone: tz,
});
}
@ -65,9 +66,9 @@ function SyncStatusBadge({ status }: { status: string }) {
}
function StatusTab({
syncData, onSync, syncing,
syncData, onSync, syncing, tz,
}: {
syncData: any; onSync: () => void; syncing: boolean;
syncData: any; onSync: () => void; syncing: boolean; tz: string;
}) {
if (!syncData) {
return (
@ -90,7 +91,7 @@ function StatusTab({
Connected to IT Glue
</p>
<p className="text-xs text-muted-foreground">
Last sync: {fmtDate(latest?.completed_at ?? null)}
Last sync: {fmtDate(latest?.completed_at ?? null, tz)}
{latest?.duration_ms && ` · ${fmtDuration(latest.duration_ms)}`}
</p>
</div>
@ -165,7 +166,7 @@ function StatusTab({
);
}
function HistoryTab({ history }: { history: any[] }) {
function HistoryTab({ history, tz }: { history: any[]; tz: string }) {
if (!history.length) {
return (
<div className="text-center py-12 text-muted-foreground text-sm">
@ -192,7 +193,7 @@ function HistoryTab({ history }: { history: any[] }) {
<TableCell><SyncStatusBadge status={row.status} /></TableCell>
<TableCell className="text-muted-foreground capitalize">{row.triggered_by ?? 'system'}</TableCell>
<TableCell className="text-right num">{(row.total_upserted ?? 0).toLocaleString()}</TableCell>
<TableCell className="text-muted-foreground num">{fmtDate(row.started_at)}</TableCell>
<TableCell className="text-muted-foreground num">{fmtDate(row.started_at, tz)}</TableCell>
<TableCell className="text-right text-muted-foreground num">{fmtDuration(row.duration_ms)}</TableCell>
</TableRow>
))}
@ -261,6 +262,7 @@ function AboutTab() {
}
export default function ITGluePage() {
const tz = useUserTimezone();
const [syncData, setSyncData] = useState<any>(null);
const [syncing, setSyncing] = useState(false);
@ -341,11 +343,11 @@ export default function ITGluePage() {
</TabsList>
<TabsContent value="status" className="mt-6">
<StatusTab syncData={syncData} onSync={handleSync} syncing={syncing} />
<StatusTab syncData={syncData} onSync={handleSync} syncing={syncing} tz={tz} />
</TabsContent>
<TabsContent value="history" className="mt-6">
<HistoryTab history={syncData?.history ?? []} />
<HistoryTab history={syncData?.history ?? []} tz={tz} />
</TabsContent>
<TabsContent value="about" className="mt-6">

View file

@ -22,11 +22,12 @@ import {
import { StatusBadge } from '@/components/ui/status-badge';
import { Checkbox } from '@/components/ui/checkbox';
import SyncScheduler from '@/components/admin/SyncScheduler';
import { useUserTimezone } from '@/lib/hooks/use-user-timezone';
function fmtDate(d: string | null | undefined) {
function fmtDate(d: string | null | undefined, tz: string) {
if (!d) return 'Never';
return new Date(d).toLocaleString(undefined, {
month: 'short', day: 'numeric', year: 'numeric', hour: '2-digit', minute: '2-digit',
month: 'short', day: 'numeric', year: 'numeric', hour: '2-digit', minute: '2-digit', timeZone: tz,
});
}
@ -70,7 +71,7 @@ function ThreatLevelBadge({ level }: { level: string }) {
}
// ── Status Tab ────────────────────────────────────────────────────────────────
function StatusTab({ data, onSync, syncing }: { data: any; onSync: (t: string) => void; syncing: boolean }) {
function StatusTab({ data, onSync, syncing, tz }: { data: any; onSync: (t: string) => void; syncing: boolean; tz: string }) {
if (!data) return (
<div className="flex items-center justify-center py-12">
<Loader2 className="w-5 h-5 animate-spin text-muted-foreground" />
@ -96,7 +97,7 @@ function StatusTab({ data, onSync, syncing }: { data: any; onSync: (t: string) =
)}
</div>
<p className="text-xs text-muted-foreground">
Last sync: {fmtDate(stats.lastSync)} · Oldest message: {fmtDate(stats.oldestMessage)}
Last sync: {fmtDate(stats.lastSync, tz)} · Oldest message: {fmtDate(stats.oldestMessage, tz)}
</p>
</div>
<div className="flex gap-2">
@ -139,7 +140,7 @@ function StatusTab({ data, onSync, syncing }: { data: any; onSync: (t: string) =
}
// ── Messages Tab ──────────────────────────────────────────────────────────────
function MessagesTab() {
function MessagesTab({ tz }: { tz: string }) {
const [rows, setRows] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [search, setSearch] = useState('');
@ -232,7 +233,7 @@ function MessagesTab() {
<TableCell className="text-xs truncate max-w-[200px]" title={r.subject}>{r.subject ?? '—'}</TableCell>
<TableCell className="text-xs capitalize text-muted-foreground">{r.direction ?? '—'}</TableCell>
<TableCell><MessageStatusBadge status={r.status} /></TableCell>
<TableCell className="text-xs text-muted-foreground whitespace-nowrap num">{fmtDate(r.sent_datetime)}</TableCell>
<TableCell className="text-xs text-muted-foreground whitespace-nowrap num">{fmtDate(r.sent_datetime, tz)}</TableCell>
</TableRow>
))}
</TableBody>
@ -244,7 +245,7 @@ function MessagesTab() {
}
// ── Threats Tab ───────────────────────────────────────────────────────────────
function ThreatsTab() {
function ThreatsTab({ tz }: { tz: string }) {
const [rows, setRows] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
@ -283,7 +284,7 @@ function ThreatsTab() {
<TableCell className="text-xs text-muted-foreground truncate max-w-[200px]" title={r.url ?? r.file_name ?? ''}>
{r.url ?? r.file_name ?? '—'}
</TableCell>
<TableCell className="text-xs text-muted-foreground whitespace-nowrap num">{fmtDate(r.event_datetime)}</TableCell>
<TableCell className="text-xs text-muted-foreground whitespace-nowrap num">{fmtDate(r.event_datetime, tz)}</TableCell>
</TableRow>
))}
</TableBody>
@ -404,7 +405,7 @@ function CloudUserTab() {
}
// ── History Tab ───────────────────────────────────────────────────────────────
function HistoryTab() {
function HistoryTab({ tz }: { tz: string }) {
const [rows, setRows] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
@ -453,7 +454,7 @@ function HistoryTab() {
</TableCell>
<TableCell className="num text-xs">{fmtNum(meta.messagesUpserted ?? r.records_added)}</TableCell>
<TableCell className="num text-xs">{fmtNum(meta.threatsUpserted)}</TableCell>
<TableCell className="text-xs text-muted-foreground num">{fmtDate(r.started_at)}</TableCell>
<TableCell className="text-xs text-muted-foreground num">{fmtDate(r.started_at, tz)}</TableCell>
<TableCell className="text-xs text-muted-foreground num">{durStr}</TableCell>
</TableRow>
);
@ -593,11 +594,12 @@ function analyzeMessage(m: any): Analysis {
};
}
function MessageAnalysisDialog({ message, onClose, onRelease, releasing }: {
function MessageAnalysisDialog({ message, onClose, onRelease, releasing, tz }: {
message: any;
onClose: () => void;
onRelease: (m: any) => void;
releasing: boolean;
tz: string;
}) {
if (!message) return null;
const analysis = analyzeMessage(message);
@ -648,7 +650,7 @@ function MessageAnalysisDialog({ message, onClose, onRelease, releasing }: {
</div>
<div className="grid grid-cols-[72px_1fr] gap-2 px-3 py-2">
<span className="text-muted-foreground text-xs pt-0.5">Received</span>
<span>{new Date(message.dateReceived).toLocaleString()}</span>
<span>{new Date(message.dateReceived).toLocaleString(undefined, { timeZone: tz })}</span>
</div>
<div className="grid grid-cols-[72px_1fr] gap-2 px-3 py-2">
<span className="text-muted-foreground text-xs pt-0.5">Policy</span>
@ -734,7 +736,7 @@ const TENANT_OPTIONS = [
{ id: '2', name: 'Seubert & Associates' },
];
function HeldMailTab() {
function HeldMailTab({ tz }: { tz: string }) {
const [data, setData] = useState<any>(null);
const [messages, setMessages] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
@ -921,7 +923,7 @@ function HeldMailTab() {
{filtered.map((m: any) => (
<TableRow key={m.id}>
<TableCell className="text-muted-foreground whitespace-nowrap text-xs num">
{new Date(m.dateReceived).toLocaleString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })}
{new Date(m.dateReceived).toLocaleString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', timeZone: tz })}
</TableCell>
<TableCell className="text-xs" style={{overflow:'hidden'}}>
<div className="truncate">{m.to}</div>
@ -982,6 +984,7 @@ function HeldMailTab() {
onClose={() => setAnalysisMessage(null)}
onRelease={release}
releasing={analysisMessage ? !!releasing[analysisMessage.id] : false}
tz={tz}
/>
</div>
);
@ -1112,11 +1115,12 @@ function analyzeDelivered(m: any): { headline: string; explanation: string; seve
};
}
function DeliveredAnalysisDialog({ message, onClose, onFindSimilar, allMessages }: {
function DeliveredAnalysisDialog({ message, onClose, onFindSimilar, allMessages, tz }: {
message: any;
onClose: () => void;
onFindSimilar?: (type: 'sender' | 'ip' | 'subject', value: string) => void;
allMessages?: any[];
tz: string;
}) {
const [remedStep, setRemedStep] = useState<'idle' | 'searching' | 'confirm' | 'removing' | 'done'>('idle');
const [remedMatches, setRemedMatches] = useState<any[]>([]);
@ -1236,7 +1240,7 @@ function DeliveredAnalysisDialog({ message, onClose, onFindSimilar, allMessages
</div>
<div className="grid grid-cols-[72px_1fr] gap-2 px-3 py-2">
<span className="text-muted-foreground text-xs pt-0.5">Received</span>
<span>{new Date(message.received).toLocaleString()}</span>
<span>{new Date(message.received).toLocaleString(undefined, { timeZone: tz })}</span>
</div>
<div className="grid grid-cols-[72px_1fr] gap-2 px-3 py-2">
<span className="text-muted-foreground text-xs pt-0.5">Status</span>
@ -1402,7 +1406,7 @@ function DeliveredAnalysisDialog({ message, onClose, onFindSimilar, allMessages
<div className="min-w-0">
<div className="text-xs truncate">{m.subject || '(no subject)'}</div>
<div className="text-xs text-muted-foreground">
{new Date(m.receivedDateTime).toLocaleString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })}
{new Date(m.receivedDateTime).toLocaleString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', timeZone: tz })}
{!m.isRead && <span className="ml-1 text-blue-500 font-medium">unread</span>}
</div>
</div>
@ -1457,7 +1461,7 @@ function DeliveredAnalysisDialog({ message, onClose, onFindSimilar, allMessages
);
}
function DeliveredMailTab() {
function DeliveredMailTab({ tz }: { tz: string }) {
const [tenantId, setTenantId] = useState('1');
const [to, setTo] = useState('');
const [from, setFrom] = useState('');
@ -1837,7 +1841,7 @@ function DeliveredMailTab() {
: ''
}>
<TableCell className="text-muted-foreground whitespace-nowrap text-xs num">
{new Date(m.received).toLocaleString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })}
{new Date(m.received).toLocaleString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', timeZone: tz })}
</TableCell>
<TableCell className="text-xs" style={{overflow:'hidden'}}>
<div className="truncate">{m.to}</div>
@ -1887,6 +1891,7 @@ function DeliveredMailTab() {
onClose={() => setAnalysisMessage(null)}
onFindSimilar={handleFindSimilar}
allMessages={messages}
tz={tz}
/>
</div>
);
@ -1894,6 +1899,7 @@ function DeliveredMailTab() {
// ── Page ──────────────────────────────────────────────────────────────────────
export default function MimecastSyncPage() {
const tz = useUserTimezone();
const [statusData, setStatusData] = useState<any>(null);
const [syncing, setSyncing] = useState(false);
const [lastResult, setLastResult] = useState<any>(null);
@ -1972,14 +1978,14 @@ export default function MimecastSyncPage() {
</TabsList>
<TabsContent value="status" className="mt-6">
<StatusTab data={statusData} onSync={handleSync} syncing={syncing} />
<StatusTab data={statusData} onSync={handleSync} syncing={syncing} tz={tz} />
</TabsContent>
<TabsContent value="held" className="mt-6"><HeldMailTab /></TabsContent>
<TabsContent value="delivered" className="mt-6"><DeliveredMailTab /></TabsContent>
<TabsContent value="messages" className="mt-6"><MessagesTab /></TabsContent>
<TabsContent value="threats" className="mt-6"><ThreatsTab /></TabsContent>
<TabsContent value="held" className="mt-6"><HeldMailTab tz={tz} /></TabsContent>
<TabsContent value="delivered" className="mt-6"><DeliveredMailTab tz={tz} /></TabsContent>
<TabsContent value="messages" className="mt-6"><MessagesTab tz={tz} /></TabsContent>
<TabsContent value="threats" className="mt-6"><ThreatsTab tz={tz} /></TabsContent>
<TabsContent value="cloudusers" className="mt-6"><CloudUserTab /></TabsContent>
<TabsContent value="history" className="mt-6"><HistoryTab /></TabsContent>
<TabsContent value="history" className="mt-6"><HistoryTab tz={tz} /></TabsContent>
<TabsContent value="schedules" className="mt-6"><SyncScheduler /></TabsContent>
</Tabs>
</div>

View file

@ -9,10 +9,11 @@ import {
ArrowLeft, RefreshCw, Play, CheckCircle2, XCircle, Clock,
Shield, Monitor, AlertTriangle, Activity,
} from 'lucide-react';
import { useUserTimezone } from '@/lib/hooks/use-user-timezone';
function fmtDate(d: string | null) {
function fmtDate(d: string | null, tz: string) {
if (!d) return '—';
return new Date(d).toLocaleString();
return new Date(d).toLocaleString(undefined, { timeZone: tz });
}
function fmtDuration(ms: number | null) {
if (!ms) return '—';
@ -21,6 +22,7 @@ function fmtDuration(ms: number | null) {
}
export default function SentinelOneSyncPage() {
const tz = useUserTimezone();
const [data, setData] = useState<any>(null);
const [loading, setLoading] = useState(true);
const [syncing, setSyncing] = useState(false);
@ -120,7 +122,7 @@ export default function SentinelOneSyncPage() {
<div>
<div className="font-medium capitalize">{lastSync.status}</div>
<div className="text-xs text-muted-foreground">
{fmtDate(lastSync.completed_at || lastSync.started_at)} · {fmtDuration(lastSync.duration_ms)} · {lastSync.total_upserted?.toLocaleString()} records
{fmtDate(lastSync.completed_at || lastSync.started_at, tz)} · {fmtDuration(lastSync.duration_ms)} · {lastSync.total_upserted?.toLocaleString()} records
</div>
</div>
</div>
@ -159,7 +161,7 @@ export default function SentinelOneSyncPage() {
{h.status === 'completed' ? <CheckCircle2 className="w-4 h-4 text-green-500" />
: h.status === 'running' ? <RefreshCw className="w-4 h-4 text-blue-500 animate-spin" />
: <XCircle className="w-4 h-4 text-red-500" />}
<span className="text-muted-foreground">{fmtDate(h.started_at)}</span>
<span className="text-muted-foreground">{fmtDate(h.started_at, tz)}</span>
</div>
<div className="flex items-center gap-3 text-muted-foreground">
<span>{h.total_upserted?.toLocaleString() ?? 0} records</span>

View file

@ -19,11 +19,12 @@ import {
} from '@/components/ui/table';
import { StatusBadge } from '@/components/ui/status-badge';
import SyncScheduler from '@/components/admin/SyncScheduler';
import { useUserTimezone } from '@/lib/hooks/use-user-timezone';
// ── Helpers ───────────────────────────────────────────────────────────────────
function fmtDate(d: string | null | undefined) {
function fmtDate(d: string | null | undefined, tz: string) {
if (!d) return 'Never';
return new Date(d).toLocaleString(undefined, { month: 'short', day: 'numeric', year: 'numeric', hour: '2-digit', minute: '2-digit' });
return new Date(d).toLocaleString(undefined, { month: 'short', day: 'numeric', year: 'numeric', hour: '2-digit', minute: '2-digit', timeZone: tz });
}
function fmtDur(ms: number) {
if (ms < 60000) return `${Math.round(ms / 1000)}s`;
@ -54,7 +55,7 @@ function SyncStatusBadge({ status }: { status: string }) {
}
// ── Status Tab ────────────────────────────────────────────────────────────────
function VeeamStatusTab({ data, onSync, syncing }: { data: any; onSync: (t: string) => void; syncing: boolean }) {
function VeeamStatusTab({ data, onSync, syncing, tz }: { data: any; onSync: (t: string) => void; syncing: boolean; tz: string }) {
if (!data) return (
<div className="flex items-center justify-center py-12">
<Loader2 className="w-5 h-5 animate-spin text-muted-foreground" />
@ -73,7 +74,7 @@ function VeeamStatusTab({ data, onSync, syncing }: { data: any; onSync: (t: stri
<div className="flex items-center justify-between rounded-lg border p-4 bg-muted/30">
<div className="space-y-0.5">
<p className="text-sm font-medium">{data.configured ? 'Connected to VSPC' : 'Not configured'}</p>
<p className="text-xs text-muted-foreground">Last sync: {fmtDate(data.lastSync)}</p>
<p className="text-xs text-muted-foreground">Last sync: {fmtDate(data.lastSync, tz)}</p>
</div>
<div className="flex gap-2">
<Button size="sm" variant="outline" onClick={() => onSync('incremental')} disabled={syncing || !data.configured}>
@ -129,7 +130,7 @@ const ENTITY_LABELS: Record<string, string> = {
alarms: 'Alarms',
};
function HistoryRow({ row }: { row: any }) {
function HistoryRow({ row, tz }: { row: any; tz: string }) {
const [expanded, setExpanded] = useState(false);
const dur = row.completed_at && row.started_at
? new Date(row.completed_at).getTime() - new Date(row.started_at).getTime()
@ -160,7 +161,7 @@ function HistoryRow({ row }: { row: any }) {
</TableCell>
<TableCell><SyncStatusBadge status={row.status} /></TableCell>
<TableCell className="num font-medium">{(row.records_added ?? 0).toLocaleString()}</TableCell>
<TableCell className="text-muted-foreground text-xs num">{fmtDate(row.started_at)}</TableCell>
<TableCell className="text-muted-foreground text-xs num">{fmtDate(row.started_at, tz)}</TableCell>
<TableCell className="text-muted-foreground num">{dur != null ? fmtDur(dur) : '—'}</TableCell>
<TableCell className="text-muted-foreground capitalize">{row.triggered_by ?? '—'}</TableCell>
</TableRow>
@ -192,7 +193,7 @@ function HistoryRow({ row }: { row: any }) {
);
}
function VeeamHistoryTab({ refreshKey }: { refreshKey: number }) {
function VeeamHistoryTab({ refreshKey, tz }: { refreshKey: number; tz: string }) {
const [rows, setRows] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
@ -222,7 +223,7 @@ function VeeamHistoryTab({ refreshKey }: { refreshKey: number }) {
</TableRow>
</TableHeader>
<TableBody>
{rows.map((row, i) => <HistoryRow key={i} row={row} />)}
{rows.map((row, i) => <HistoryRow key={i} row={row} tz={tz} />)}
</TableBody>
</Table>
</div>
@ -323,7 +324,7 @@ function AgentsTab({ refreshKey }: { refreshKey: number }) {
}
// ── Alarms Tab ────────────────────────────────────────────────────────────────
function AlarmsTab({ refreshKey }: { refreshKey: number }) {
function AlarmsTab({ refreshKey, tz }: { refreshKey: number; tz: string }) {
const [data, setData] = useState<any>(null);
const [loading, setLoading] = useState(true);
@ -386,7 +387,7 @@ function AlarmsTab({ refreshKey }: { refreshKey: number }) {
</StatusBadge>
</TableCell>
<TableCell className="num text-xs">{a.repeat_count ?? 0}</TableCell>
<TableCell className="text-xs text-muted-foreground num">{fmtDate(a.last_activation_time)}</TableCell>
<TableCell className="text-xs text-muted-foreground num">{fmtDate(a.last_activation_time, tz)}</TableCell>
<TableCell className="text-xs text-muted-foreground max-w-xs truncate" title={a.last_activation_message ?? ''}>
{a.last_activation_message?.trim() || '—'}
</TableCell>
@ -400,7 +401,7 @@ function AlarmsTab({ refreshKey }: { refreshKey: number }) {
}
// ── RPO Tab ───────────────────────────────────────────────────────────────────
function RpoTab({ refreshKey }: { refreshKey: number }) {
function RpoTab({ refreshKey, tz }: { refreshKey: number; tz: string }) {
const [data, setData] = useState<any>(null);
const [loading, setLoading] = useState(true);
const [running, setRunning] = useState(false);
@ -503,7 +504,7 @@ function RpoTab({ refreshKey }: { refreshKey: number }) {
<TableRow key={j.job_instance_uid}>
<TableCell className="font-medium text-xs">{j.job_name}</TableCell>
<TableCell className="text-xs text-muted-foreground">{j.org_name}</TableCell>
<TableCell className="text-xs text-muted-foreground num">{fmtDate(j.last_end_time)}</TableCell>
<TableCell className="text-xs text-muted-foreground num">{fmtDate(j.last_end_time, tz)}</TableCell>
<TableCell className="num text-xs font-semibold text-red-600">{display}</TableCell>
<TableCell className="text-xs text-muted-foreground max-w-xs truncate" title={j.failure_category ?? ''}>
{j.failure_category ?? '—'}
@ -545,7 +546,7 @@ function RpoTab({ refreshKey }: { refreshKey: number }) {
<TableRow key={j.job_instance_uid}>
<TableCell className="font-medium text-xs">{j.job_name}</TableCell>
<TableCell className="text-xs text-muted-foreground">{j.org_name}</TableCell>
<TableCell className="text-xs text-muted-foreground num">{fmtDate(j.last_end_time)}</TableCell>
<TableCell className="text-xs text-muted-foreground num">{fmtDate(j.last_end_time, tz)}</TableCell>
<TableCell className="num text-xs text-green-700">
{j.hours_since_backup !== null ? `${j.hours_since_backup}h` : '—'}
</TableCell>
@ -562,6 +563,7 @@ function RpoTab({ refreshKey }: { refreshKey: number }) {
// ── Page ──────────────────────────────────────────────────────────────────────
export default function VeeamSyncPage() {
const tz = useUserTimezone();
const [status, setStatus] = useState<any>(null);
const [syncing, setSyncing] = useState(false);
const [refreshKey, setRefreshKey] = useState(0);
@ -633,11 +635,11 @@ export default function VeeamSyncPage() {
<TabsTrigger value="schedules" className="gap-1.5"><Calendar className="h-4 w-4" />Schedules</TabsTrigger>
</TabsList>
<TabsContent value="status" className="mt-6"><VeeamStatusTab data={status} onSync={handleSync} syncing={syncing} /></TabsContent>
<TabsContent value="rpo" className="mt-6"><RpoTab refreshKey={refreshKey} /></TabsContent>
<TabsContent value="history" className="mt-6"><VeeamHistoryTab refreshKey={refreshKey} /></TabsContent>
<TabsContent value="status" className="mt-6"><VeeamStatusTab data={status} onSync={handleSync} syncing={syncing} tz={tz} /></TabsContent>
<TabsContent value="rpo" className="mt-6"><RpoTab refreshKey={refreshKey} tz={tz} /></TabsContent>
<TabsContent value="history" className="mt-6"><VeeamHistoryTab refreshKey={refreshKey} tz={tz} /></TabsContent>
<TabsContent value="agents" className="mt-6"><AgentsTab refreshKey={refreshKey} /></TabsContent>
<TabsContent value="alarms" className="mt-6"><AlarmsTab refreshKey={refreshKey} /></TabsContent>
<TabsContent value="alarms" className="mt-6"><AlarmsTab refreshKey={refreshKey} tz={tz} /></TabsContent>
<TabsContent value="schedules" className="mt-6"><SyncScheduler /></TabsContent>
</Tabs>
</div>