feat(07.1-05): user-tz on analyzer pages

- itglue/applications, applications/[id], configurations,
  configurations/[id], sites/[companyId], queue, ticket/[ticketNumber],
  tickets, reports, reports/[id]: useUserTimezone() in default export;
  thread tz into every inline toLocale*String call.
- analyzer/tickets/page.tsx converts module-scope formatRelative(iso)
  helper to formatRelative(iso, tz); updates 1 callsite.

Migrates 16 of 81 audit leak callsites.
This commit is contained in:
lorentz 2026-05-07 08:34:58 -04:00
parent 23b179f2a7
commit 96edfb4444
10 changed files with 38 additions and 18 deletions

View file

@ -23,6 +23,7 @@ import {
CheckCircle2, CheckCircle2,
Undo2, Undo2,
} from 'lucide-react'; } from 'lucide-react';
import { useUserTimezone } from '@/lib/hooks/use-user-timezone';
import { useSession } from '@/lib/auth-client'; import { useSession } from '@/lib/auth-client';
interface FieldRow { interface FieldRow {
@ -157,6 +158,7 @@ export default function ApplicationAuditPage({
}: { }: {
params: Promise<{ id: string }>; params: Promise<{ id: string }>;
}) { }) {
const tz = useUserTimezone();
const { id } = use(params); const { id } = use(params);
const { data: session } = useSession(); const { data: session } = useSession();
const role = (session?.user as { role?: string } | undefined)?.role ?? 'user'; const role = (session?.user as { role?: string } | undefined)?.role ?? 'user';
@ -408,7 +410,7 @@ export default function ApplicationAuditPage({
<CardTitle className="text-base"> <CardTitle className="text-base">
Audit findings Audit findings
<span className="ml-2 text-xs text-muted-foreground font-normal"> <span className="ml-2 text-xs text-muted-foreground font-normal">
{new Date(audit.generated_at).toLocaleString()} {new Date(audit.generated_at).toLocaleString(undefined, { timeZone: tz })}
{' · '} {' · '}
{audit.provider === 'openrouter' ? 'DeepSeek' : 'Claude'} {audit.provider === 'openrouter' ? 'DeepSeek' : 'Claude'}
{audit.estimated_cost_usd !== null {audit.estimated_cost_usd !== null
@ -717,7 +719,7 @@ export default function ApplicationAuditPage({
</Badge> </Badge>
</p> </p>
<p className="text-xs text-muted-foreground mt-1"> <p className="text-xs text-muted-foreground mt-1">
{new Date(w.performed_at).toLocaleString()} {new Date(w.performed_at).toLocaleString(undefined, { timeZone: tz })}
</p> </p>
<p className="text-xs mt-1 break-words"> <p className="text-xs mt-1 break-words">
<span className="text-muted-foreground">Before: </span> <span className="text-muted-foreground">Before: </span>
@ -776,7 +778,7 @@ export default function ApplicationAuditPage({
className="py-2 flex items-center justify-between text-sm" className="py-2 flex items-center justify-between text-sm"
> >
<span className="text-muted-foreground"> <span className="text-muted-foreground">
{new Date(h.generated_at).toLocaleString()} {new Date(h.generated_at).toLocaleString(undefined, { timeZone: tz })}
{' · '} {' · '}
{h.provider === 'openrouter' ? 'DeepSeek' : 'Claude'} {h.provider === 'openrouter' ? 'DeepSeek' : 'Claude'}
</span> </span>

View file

@ -7,6 +7,7 @@ import { Badge } from '@/components/ui/badge';
import { Skeleton } from '@/components/ui/skeleton'; import { Skeleton } from '@/components/ui/skeleton';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { useUserTimezone } from '@/lib/hooks/use-user-timezone';
interface ApplicationRow { interface ApplicationRow {
id: string; id: string;
@ -32,6 +33,7 @@ function scoreBadgeVariant(
} }
export default function ApplicationsListPage() { export default function ApplicationsListPage() {
const tz = useUserTimezone();
const [rows, setRows] = useState<ApplicationRow[] | null>(null); const [rows, setRows] = useState<ApplicationRow[] | null>(null);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [filter, setFilter] = useState(''); const [filter, setFilter] = useState('');
@ -126,7 +128,7 @@ export default function ApplicationsListPage() {
{' · '} {' · '}
last audited{' '} last audited{' '}
{r.latestAudit.generatedAt {r.latestAudit.generatedAt
? new Date(r.latestAudit.generatedAt).toLocaleDateString() ? new Date(r.latestAudit.generatedAt).toLocaleDateString(undefined, { timeZone: tz })
: 'unknown'} : 'unknown'}
{' '} {' '}
( (

View file

@ -25,6 +25,7 @@ import {
Server, Server,
} from 'lucide-react'; } from 'lucide-react';
import { useSession } from '@/lib/auth-client'; import { useSession } from '@/lib/auth-client';
import { useUserTimezone } from '@/lib/hooks/use-user-timezone';
interface FieldRow { interface FieldRow {
id: string; id: string;
@ -136,6 +137,7 @@ export default function ConfigurationAuditPage({
params: Promise<{ id: string }>; params: Promise<{ id: string }>;
}) { }) {
const { id } = use(params); const { id } = use(params);
const tz = useUserTimezone();
const { data: session } = useSession(); const { data: session } = useSession();
const role = (session?.user as { role?: string } | undefined)?.role ?? 'user'; const role = (session?.user as { role?: string } | undefined)?.role ?? 'user';
const canWrite = role === 'admin' || role === 'super-admin'; const canWrite = role === 'admin' || role === 'super-admin';
@ -390,7 +392,7 @@ export default function ConfigurationAuditPage({
<CardTitle className="text-base"> <CardTitle className="text-base">
Audit findings Audit findings
<span className="ml-2 text-xs text-muted-foreground font-normal"> <span className="ml-2 text-xs text-muted-foreground font-normal">
{new Date(audit.generated_at).toLocaleString()} {new Date(audit.generated_at).toLocaleString(undefined, { timeZone: tz })}
{' · '} {' · '}
{audit.provider === 'openrouter' ? 'DeepSeek' : 'Claude'} {audit.provider === 'openrouter' ? 'DeepSeek' : 'Claude'}
{audit.estimated_cost_usd !== null {audit.estimated_cost_usd !== null
@ -685,7 +687,7 @@ export default function ConfigurationAuditPage({
</Badge> </Badge>
</p> </p>
<p className="text-xs text-muted-foreground mt-1"> <p className="text-xs text-muted-foreground mt-1">
{new Date(w.performed_at).toLocaleString()} {new Date(w.performed_at).toLocaleString(undefined, { timeZone: tz })}
</p> </p>
<p className="text-xs mt-1 break-words"> <p className="text-xs mt-1 break-words">
<span className="text-muted-foreground">Before: </span> <span className="text-muted-foreground">Before: </span>
@ -738,7 +740,7 @@ export default function ConfigurationAuditPage({
{history.map((h) => ( {history.map((h) => (
<li key={h.id} className="py-2 flex items-center justify-between text-sm"> <li key={h.id} className="py-2 flex items-center justify-between text-sm">
<span className="text-muted-foreground"> <span className="text-muted-foreground">
{new Date(h.generated_at).toLocaleString()} {new Date(h.generated_at).toLocaleString(undefined, { timeZone: tz })}
{' · '} {' · '}
{h.provider === 'openrouter' ? 'DeepSeek' : 'Claude'} {h.provider === 'openrouter' ? 'DeepSeek' : 'Claude'}
</span> </span>

View file

@ -7,6 +7,7 @@ import { Badge } from '@/components/ui/badge';
import { Skeleton } from '@/components/ui/skeleton'; import { Skeleton } from '@/components/ui/skeleton';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { useUserTimezone } from '@/lib/hooks/use-user-timezone';
interface ConfigurationRow { interface ConfigurationRow {
id: string; id: string;
@ -34,6 +35,7 @@ function scoreBadgeVariant(
} }
export default function ConfigurationsListPage() { export default function ConfigurationsListPage() {
const tz = useUserTimezone();
const [rows, setRows] = useState<ConfigurationRow[] | null>(null); const [rows, setRows] = useState<ConfigurationRow[] | null>(null);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [filter, setFilter] = useState(''); const [filter, setFilter] = useState('');
@ -127,7 +129,7 @@ export default function ConfigurationsListPage() {
<> <>
{' · '}last audited{' '} {' · '}last audited{' '}
{r.latestAudit.generatedAt {r.latestAudit.generatedAt
? new Date(r.latestAudit.generatedAt).toLocaleDateString() ? new Date(r.latestAudit.generatedAt).toLocaleDateString(undefined, { timeZone: tz })
: 'unknown'} : 'unknown'}
{' '} {' '}
( (

View file

@ -7,6 +7,7 @@ import { Skeleton } from '@/components/ui/skeleton';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { RmmScriptPicker } from '@/components/rmm/rmm-script-picker'; import { RmmScriptPicker } from '@/components/rmm/rmm-script-picker';
import { Server } from 'lucide-react'; import { Server } from 'lucide-react';
import { useUserTimezone } from '@/lib/hooks/use-user-timezone';
interface SiteInfo { interface SiteInfo {
companyId: string; companyId: string;
@ -45,6 +46,7 @@ export default function SiteDiscoveryPage({
params: Promise<{ companyId: string }>; params: Promise<{ companyId: string }>;
}) { }) {
const { companyId } = use(params); const { companyId } = use(params);
const tz = useUserTimezone();
const [info, setInfo] = useState<SiteInfo | null>(null); const [info, setInfo] = useState<SiteInfo | null>(null);
const [executions, setExecutions] = useState<ExecRow[] | null>(null); const [executions, setExecutions] = useState<ExecRow[] | null>(null);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@ -209,7 +211,7 @@ export default function SiteDiscoveryPage({
</p> </p>
<p className="text-xs text-muted-foreground mt-0.5"> <p className="text-xs text-muted-foreground mt-0.5">
{e.targetHostname ?? '—'} ·{' '} {e.targetHostname ?? '—'} ·{' '}
{new Date(e.queuedAt).toLocaleString()} {new Date(e.queuedAt).toLocaleString(undefined, { timeZone: tz })}
</p> </p>
</div> </div>
</div> </div>

View file

@ -8,8 +8,10 @@ import { Skeleton } from '@/components/ui/skeleton';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { AlertTriangle } from 'lucide-react'; import { AlertTriangle } from 'lucide-react';
import type { PersistedAnalysis } from '@/lib/types/analyzer'; import type { PersistedAnalysis } from '@/lib/types/analyzer';
import { useUserTimezone } from '@/lib/hooks/use-user-timezone';
export default function AnalyzerQueuePage() { export default function AnalyzerQueuePage() {
const tz = useUserTimezone();
const [analyses, setAnalyses] = useState<PersistedAnalysis[] | null>(null); const [analyses, setAnalyses] = useState<PersistedAnalysis[] | null>(null);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@ -100,7 +102,7 @@ export default function AnalyzerQueuePage() {
</ul> </ul>
)} )}
<p className="text-xs text-muted-foreground mt-1"> <p className="text-xs text-muted-foreground mt-1">
{new Date(a.triggeredAt).toLocaleString()} {new Date(a.triggeredAt).toLocaleString(undefined, { timeZone: tz })}
</p> </p>
</div> </div>
<div className="flex flex-col items-end gap-1 shrink-0"> <div className="flex flex-col items-end gap-1 shrink-0">

View file

@ -13,6 +13,7 @@ import {
XCircle, XCircle,
} from 'lucide-react'; } from 'lucide-react';
import { AnalysisMarkdown } from '@/components/analyzer/analysis-markdown'; import { AnalysisMarkdown } from '@/components/analyzer/analysis-markdown';
import { useUserTimezone } from '@/lib/hooks/use-user-timezone';
type Report = { type Report = {
id: string; id: string;
@ -124,6 +125,7 @@ export default function AggregateReportPage({
params: Promise<{ id: string }>; params: Promise<{ id: string }>;
}) { }) {
const { id } = use(params); const { id } = use(params);
const tz = useUserTimezone();
const [report, setReport] = useState<Report | null>(null); const [report, setReport] = useState<Report | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@ -183,7 +185,7 @@ export default function AggregateReportPage({
{report.reportTitle ?? `Aggregate report · ${report.ticketCount} tickets`} {report.reportTitle ?? `Aggregate report · ${report.ticketCount} tickets`}
</CardTitle> </CardTitle>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
{new Date(report.generatedAt).toLocaleString()} {new Date(report.generatedAt).toLocaleString(undefined, { timeZone: tz })}
{report.modelUsed && ` · ${report.modelUsed}`} {report.modelUsed && ` · ${report.modelUsed}`}
{report.estimatedCostUsd !== null && ( {report.estimatedCostUsd !== null && (
<> · ${report.estimatedCostUsd.toFixed(4)}</> <> · ${report.estimatedCostUsd.toFixed(4)}</>
@ -196,9 +198,9 @@ export default function AggregateReportPage({
{report.dateRangeActual?.earliest && report.dateRangeActual?.latest && ( {report.dateRangeActual?.earliest && report.dateRangeActual?.latest && (
<> <>
{' · '} {' · '}
{new Date(report.dateRangeActual.earliest).toLocaleDateString()} {new Date(report.dateRangeActual.earliest).toLocaleDateString(undefined, { timeZone: tz })}
{' '} {' '}
{new Date(report.dateRangeActual.latest).toLocaleDateString()} {new Date(report.dateRangeActual.latest).toLocaleDateString(undefined, { timeZone: tz })}
</> </>
)} )}
</p> </p>

View file

@ -14,6 +14,7 @@ import {
TableRow, TableRow,
} from '@/components/ui/table'; } from '@/components/ui/table';
import { CheckCircle2, XCircle, Loader2 } from 'lucide-react'; import { CheckCircle2, XCircle, Loader2 } from 'lucide-react';
import { useUserTimezone } from '@/lib/hooks/use-user-timezone';
interface ReportSummary { interface ReportSummary {
id: string; id: string;
@ -27,6 +28,7 @@ interface ReportSummary {
} }
export default function ReportsListPage() { export default function ReportsListPage() {
const tz = useUserTimezone();
const [reports, setReports] = useState<ReportSummary[]>([]); const [reports, setReports] = useState<ReportSummary[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@ -133,7 +135,7 @@ export default function ReportsListPage() {
: `$${r.estimatedCostUsd.toFixed(4)}`} : `$${r.estimatedCostUsd.toFixed(4)}`}
</TableCell> </TableCell>
<TableCell className="text-xs text-muted-foreground"> <TableCell className="text-xs text-muted-foreground">
{new Date(r.generatedAt).toLocaleString()} {new Date(r.generatedAt).toLocaleString(undefined, { timeZone: tz })}
</TableCell> </TableCell>
<TableCell className="text-right"> <TableCell className="text-right">
<Button variant="outline" size="sm" asChild> <Button variant="outline" size="sm" asChild>

View file

@ -15,6 +15,7 @@ import {
import { PageHeader } from '@/components/navigation/page-header'; import { PageHeader } from '@/components/navigation/page-header';
import { Sparkles, Zap } from 'lucide-react'; import { Sparkles, Zap } from 'lucide-react';
import type { PersistedAnalysis } from '@/lib/types/analyzer'; import type { PersistedAnalysis } from '@/lib/types/analyzer';
import { useUserTimezone } from '@/lib/hooks/use-user-timezone';
export default function TicketAnalyzerPage({ export default function TicketAnalyzerPage({
params, params,
@ -22,6 +23,7 @@ export default function TicketAnalyzerPage({
params: Promise<{ ticketNumber: string }>; params: Promise<{ ticketNumber: string }>;
}) { }) {
const { ticketNumber } = use(params); const { ticketNumber } = use(params);
const tz = useUserTimezone();
const [analyses, setAnalyses] = useState<PersistedAnalysis[] | null>(null); const [analyses, setAnalyses] = useState<PersistedAnalysis[] | null>(null);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [provider, setProvider] = useState<AnalyzerProvider>('anthropic'); const [provider, setProvider] = useState<AnalyzerProvider>('anthropic');
@ -140,7 +142,7 @@ export default function TicketAnalyzerPage({
)} )}
</Link> </Link>
<p className="text-xs text-muted-foreground mt-1"> <p className="text-xs text-muted-foreground mt-1">
{new Date(a.triggeredAt).toLocaleString()} {new Date(a.triggeredAt).toLocaleString(undefined, { timeZone: tz })}
{' · '} {' · '}
{tierLabel} {tierLabel}
{' · '}${a.estimatedCostUsd.toFixed(4)} {' · '}${a.estimatedCostUsd.toFixed(4)}

View file

@ -39,6 +39,7 @@ import {
Clock, Clock,
} from 'lucide-react'; } from 'lucide-react';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { useUserTimezone } from '@/lib/hooks/use-user-timezone';
type Period = type Period =
| 'today' | 'today'
@ -98,7 +99,7 @@ interface FilterOptions {
const PAGE_SIZE = 50; const PAGE_SIZE = 50;
const SELECTION_LS_KEY = 'analyzer:ticket-selection:v1'; const SELECTION_LS_KEY = 'analyzer:ticket-selection:v1';
function formatRelative(iso: string | null): string { function formatRelative(iso: string | null, tz: string): string {
if (!iso) return '—'; if (!iso) return '—';
const d = new Date(iso); const d = new Date(iso);
const diffMs = Date.now() - d.getTime(); const diffMs = Date.now() - d.getTime();
@ -109,7 +110,7 @@ function formatRelative(iso: string | null): string {
if (diffHr < 24) return `${diffHr}h ago`; if (diffHr < 24) return `${diffHr}h ago`;
const diffDays = Math.round(diffHr / 24); const diffDays = Math.round(diffHr / 24);
if (diffDays < 7) return `${diffDays}d ago`; if (diffDays < 7) return `${diffDays}d ago`;
return d.toLocaleDateString(); return d.toLocaleDateString(undefined, { timeZone: tz });
} }
function formatAge(days: number | null): string { function formatAge(days: number | null): string {
@ -142,6 +143,7 @@ function writeSelection(ids: string[]) {
} }
export default function AnalyzerBrowseTicketsPage() { export default function AnalyzerBrowseTicketsPage() {
const tz = useUserTimezone();
const router = useRouter(); const router = useRouter();
const [period, setPeriod] = useState<Period>('last_30d'); const [period, setPeriod] = useState<Period>('last_30d');
@ -896,7 +898,7 @@ export default function AnalyzerBrowseTicketsPage() {
className="text-xs text-muted-foreground" className="text-xs text-muted-foreground"
title={t.lastActivityAtAutotask ?? ''} title={t.lastActivityAtAutotask ?? ''}
> >
{formatRelative(t.lastActivityAtAutotask)} {formatRelative(t.lastActivityAtAutotask, tz)}
</TableCell> </TableCell>
<TableCell className="text-xs truncate max-w-[120px]"> <TableCell className="text-xs truncate max-w-[120px]">
{t.assignedResourceName ?? ( {t.assignedResourceName ?? (