diff --git a/src/components/StrategyTimeline.tsx b/src/components/StrategyTimeline.tsx index 25ad963d..d0d01ddf 100644 --- a/src/components/StrategyTimeline.tsx +++ b/src/components/StrategyTimeline.tsx @@ -31,6 +31,14 @@ type StrategyTimelineProps = { compact?: boolean; }; +function parseTimestamp(value?: string | null) { + if (!value) return null; + const hasTimezone = /Z|[+-]\d{2}:?\d{2}$/.test(value); + const normalized = hasTimezone ? value : `${value}Z`; + const parsed = new Date(normalized); + return Number.isNaN(parsed.getTime()) ? null : parsed; +} + function normalizeLog(log: unknown): StrategyEvent { if (typeof log === "string") { try { @@ -49,25 +57,28 @@ function normalizeLog(log: unknown): StrategyEvent { return { message: "Unknown log entry" }; } -function formatTimestamp(value?: string) { +function formatTimestamp(value?: string | null) { if (!value) return "Unknown time"; - const hasTimezone = /Z|[+-]\d{2}:?\d{2}$/.test(value); - const normalized = hasTimezone ? value : `${value}Z`; - const parsed = new Date(normalized); - if (Number.isNaN(parsed.getTime())) return value; + const parsed = parseTimestamp(value); + if (!parsed) return value; return parsed.toLocaleString(); } function formatCompactTimestamp(value?: string | null) { - if (!value) return null; - const parsed = new Date(value); - if (Number.isNaN(parsed.getTime())) return null; - return parsed.toLocaleTimeString([], { + const parsed = parseTimestamp(value); + if (!parsed) return null; + return parsed.toLocaleString([], { + month: "short", + day: "numeric", hour: "numeric", minute: "2-digit", }); } +function getTimestampMs(value?: string | null) { + return parseTimestamp(value)?.getTime() ?? 0; +} + function getRunId(entry: StrategyEvent) { return entry.run_id ?? "unknown"; } @@ -178,9 +189,7 @@ function CompactStrategySummary() {
{message} - {updatedAt ? ( - Updated {updatedAt} - ) : null} + {updatedAt ? Updated {updatedAt} : null}
); @@ -228,25 +237,34 @@ function VerboseStrategyTimeline() { }, []); const groupedRuns = useMemo(() => { - const indexed = logs.map((entry, index) => ({ entry, index })); - indexed.sort((a, b) => { - const runCompare = getRunId(b.entry).localeCompare(getRunId(a.entry)); - if (runCompare !== 0) return runCompare; - const seqCompare = getSeq(a.entry) - getSeq(b.entry); + const grouped = new Map(); + for (const entry of logs) { + const runId = getRunId(entry); + const events = grouped.get(runId) ?? []; + events.push(entry); + grouped.set(runId, events); + } + + const groups = Array.from(grouped.entries()).map(([runId, events]) => ({ + runId, + events: [...events].sort((a, b) => { + const seqCompare = getSeq(a) - getSeq(b); + if (seqCompare !== 0) return seqCompare; + return getTimestampMs(a.ts ?? a.timestamp) - getTimestampMs(b.ts ?? b.timestamp); + }), + })); + + groups.sort((a, b) => { + const aLast = a.events[a.events.length - 1]; + const bLast = b.events[b.events.length - 1]; + const seqCompare = getSeq(bLast) - getSeq(aLast); if (seqCompare !== 0) return seqCompare; - return a.index - b.index; + const timeCompare = + getTimestampMs(bLast.ts ?? bLast.timestamp) - getTimestampMs(aLast.ts ?? aLast.timestamp); + if (timeCompare !== 0) return timeCompare; + return b.runId.localeCompare(a.runId); }); - const groups: StrategyRun[] = []; - for (const { entry } of indexed) { - const runId = getRunId(entry); - const last = groups[groups.length - 1]; - if (!last || last.runId !== runId) { - groups.push({ runId, events: [entry] }); - } else { - last.events.push(entry); - } - } return groups; }, [logs]); @@ -316,7 +334,7 @@ function VerboseStrategyTimeline() {
- {formatTimestamp(timestamp)} • {entry.event ?? "UNKNOWN"} + {formatTimestamp(timestamp)} - {entry.event ?? "UNKNOWN"}
{message && message !== entry.event ? (
{message}