@@ -22,6 +22,44 @@ function logsCacheKey(apiBase: string): string {
2222 return `ocx.logs.list.v1:${ apiBase } ` ;
2323}
2424
25+ interface LogsCachePayload {
26+ logs : LogEntry [ ] ;
27+ timeZone ?: string ;
28+ }
29+
30+ function acceptClientTimeZone ( timeZone : string | undefined ) : string | undefined {
31+ const trimmed = timeZone ?. trim ( ) ;
32+ if ( ! trimmed ) return undefined ;
33+ try {
34+ new Intl . DateTimeFormat ( undefined , { timeZone : trimmed } ) . format ( 0 ) ;
35+ return trimmed ;
36+ } catch {
37+ return undefined ;
38+ }
39+ }
40+
41+ function readLogsCache ( apiBase : string ) : LogsCachePayload | null {
42+ const raw = readSessionListCache < LogEntry [ ] | LogsCachePayload > ( logsCacheKey ( apiBase ) ) ;
43+ if ( ! raw ) return null ;
44+ if ( Array . isArray ( raw ) ) return { logs : raw } ;
45+ if ( Array . isArray ( raw . logs ) ) {
46+ return { logs : raw . logs , timeZone : acceptClientTimeZone ( raw . timeZone ) } ;
47+ }
48+ return null ;
49+ }
50+
51+ function parseLogsApiResponse ( body : unknown ) : LogsCachePayload {
52+ if ( Array . isArray ( body ) ) return { logs : body } ;
53+ if ( body && typeof body === "object" ) {
54+ const record = body as { logs ?: unknown ; timeZone ?: unknown } ;
55+ return {
56+ logs : Array . isArray ( record . logs ) ? record . logs as LogEntry [ ] : [ ] ,
57+ timeZone : acceptClientTimeZone ( typeof record . timeZone === "string" ? record . timeZone : undefined ) ,
58+ } ;
59+ }
60+ return { logs : [ ] } ;
61+ }
62+
2563interface UsageBreakdown {
2664 inputTokens : number ;
2765 outputTokens : number ;
@@ -285,12 +323,22 @@ function statusColor(status: number): string {
285323 return "var(--amber)" ;
286324}
287325
288- function formatLogTimestamp ( ts : number , localeTag ?: string ) : string {
289- return new Date ( ts ) . toLocaleTimeString ( localeTag ) ;
326+ function formatLogTimestamp ( ts : number , localeTag ?: string , timeZone ?: string ) : string {
327+ const zone = acceptClientTimeZone ( timeZone ) ;
328+ try {
329+ return new Date ( ts ) . toLocaleTimeString ( localeTag , zone ? { timeZone : zone } : undefined ) ;
330+ } catch {
331+ return new Date ( ts ) . toLocaleTimeString ( localeTag ) ;
332+ }
290333}
291334
292- function formatLogDateTime ( ts : number , localeTag ?: string ) : string {
293- return new Date ( ts ) . toLocaleString ( localeTag ) ;
335+ function formatLogDateTime ( ts : number , localeTag ?: string , timeZone ?: string ) : string {
336+ const zone = acceptClientTimeZone ( timeZone ) ;
337+ try {
338+ return new Date ( ts ) . toLocaleString ( localeTag , zone ? { timeZone : zone } : undefined ) ;
339+ } catch {
340+ return new Date ( ts ) . toLocaleString ( localeTag ) ;
341+ }
294342}
295343
296344function modelTitle ( log : LogEntry ) : string {
@@ -336,7 +384,8 @@ function summarizeFilteredLogs(entries: LogEntry[]): {
336384
337385export default function Logs ( { apiBase } : { apiBase : string } ) {
338386 const { t, locale } = useI18n ( ) ;
339- const cachedLogs = readSessionListCache < LogEntry [ ] > ( logsCacheKey ( apiBase ) ) ;
387+ const cachedLogsPayload = readLogsCache ( apiBase ) ;
388+ const [ serverTimeZone , setServerTimeZone ] = useState < string | undefined > ( ( ) => cachedLogsPayload ?. timeZone ) ;
340389 const [ autoRefresh , setAutoRefresh ] = useState ( true ) ;
341390 const [ detail , setDetail ] = useState < LogEntry | null > ( null ) ;
342391 const [ surfaceFilter , setSurfaceFilter ] = useState < LogSurfaceFilter > ( "all" ) ;
@@ -366,9 +415,10 @@ export default function Logs({ apiBase }: { apiBase: string }) {
366415 const loadLogs = useCallback ( async ( signal : AbortSignal ) : Promise < LogEntry [ ] > => {
367416 const res = await fetch ( `${ apiBase } /api/logs` , { signal } ) ;
368417 if ( ! res . ok ) throw new Error ( `${ res . status } ${ res . statusText } ` . trim ( ) ) ;
369- const next = await res . json ( ) as LogEntry [ ] ;
370- writeSessionListCache ( logsCacheKey ( apiBase ) , next ) ;
371- return next ;
418+ const parsed = parseLogsApiResponse ( await res . json ( ) ) ;
419+ if ( parsed . timeZone ) setServerTimeZone ( parsed . timeZone ) ;
420+ writeSessionListCache ( logsCacheKey ( apiBase ) , parsed ) ;
421+ return parsed . logs ;
372422 } , [ apiBase ] ) ;
373423
374424 // The resource layer owns the request and the 2s poll. It keeps held rows through a quiet
@@ -385,7 +435,7 @@ export default function Logs({ apiBase }: { apiBase: string }) {
385435 } ,
386436 ) ;
387437 const logsState = logsResource . state ;
388- const logs = logsState . data ?? cachedLogs ?? [ ] ;
438+ const logs = logsState . data ?? cachedLogsPayload ?. logs ?? [ ] ;
389439 const fetchLogs = logsResource . refresh ;
390440
391441 // A single failed tick on a two-second poll is noise, but an outage that never recovers must not
@@ -628,7 +678,7 @@ export default function Logs({ apiBase }: { apiBase: string }) {
628678 data-index = { virtualRow . index }
629679 ref = { rowVirtualizer . measureElement }
630680 >
631- < td className = "muted mono" > { formatLogTimestamp ( log . timestamp , localeTag ) } </ td >
681+ < td className = "muted mono" > { formatLogTimestamp ( log . timestamp , localeTag , serverTimeZone ) } </ td >
632682 < td className = "num mono log-col-tokens" title = { tokensTitle ( log , t ) } >
633683 { ( ( ) => {
634684 const tokenTotal = displayContextTokenTotal ( log ) ;
@@ -715,6 +765,7 @@ export default function Logs({ apiBase }: { apiBase: string }) {
715765 detailInfo = { detailInfo }
716766 localeCode = { locale }
717767 localeTag = { localeTag }
768+ serverTimeZone = { serverTimeZone }
718769 t = { t }
719770 onClose = { ( ) => setDetail ( null ) }
720771 onFilterConversation = { id => {
@@ -740,12 +791,13 @@ function useModalDialog(open: boolean) {
740791}
741792
742793function LogDetailDialog ( {
743- detail, detailInfo, localeCode, localeTag, t, onClose, onFilterConversation,
794+ detail, detailInfo, localeCode, localeTag, serverTimeZone , t, onClose, onFilterConversation,
744795} : {
745796 detail : LogEntry ;
746797 detailInfo : ReturnType < typeof statusCodeInfo > | null ;
747798 localeCode : string ;
748799 localeTag ?: string ;
800+ serverTimeZone ?: string ;
749801 t : TFn ;
750802 onClose : ( ) => void ;
751803 onFilterConversation ?: ( conversationId : string ) => void ;
@@ -787,7 +839,7 @@ function LogDetailDialog({
787839 < section className = "log-detail-section" aria-labelledby = "log-detail-basic" >
788840 < h4 id = "log-detail-basic" className = "log-detail-section-title" > { t ( "logs.detail.section.basic" ) } </ h4 >
789841 < div className = "log-detail-grid" >
790- < span className = "muted" > { t ( "logs.col.time" ) } </ span > < span className = "mono" > { formatLogDateTime ( detail . timestamp , localeTag ) } </ span >
842+ < span className = "muted" > { t ( "logs.col.time" ) } </ span > < span className = "mono" > { formatLogDateTime ( detail . timestamp , localeTag , serverTimeZone ) } </ span >
791843 < span className = "muted" > { t ( "logs.col.request" ) } </ span >
792844 < span className = "log-detail-request-row" >
793845 < span className = "mono log-detail-break" > { detail . requestId ?? "\u2014" } </ span >
0 commit comments