Files
fusion/packages/dashboard/app/utils/relativeTimeAgo.ts
gsxdsm ca14a12b94 FN-6601: consolidate relative-time bucket formatting
Consolidate dashboard relative-time calculations behind a shared bucket helper while preserving each surface's display behavior.

- Add getRelativeTimeBucket to centralize parsing, boundary math, and bucket metadata.
- Route ActivityFeed, AgentLogViewer, MissionManager, and PlanningModeModal timestamp labels through the shared helper.
- Extend unit coverage for bucket boundaries plus surface-specific just-now and future timestamp behavior.

Files changed:
 packages/dashboard/app/components/ActivityFeed.tsx | 36 ++++++----
 .../dashboard/app/components/AgentLogViewer.tsx    | 36 ++++++----
 .../dashboard/app/components/MissionManager.tsx    | 32 +++++----
 .../dashboard/app/components/PlanningModeModal.tsx | 35 ++++++----
 .../app/components/__tests__/ActivityFeed.test.tsx |  8 +++
 .../components/__tests__/AgentLogViewer.test.tsx   |  8 +++
 .../app/utils/__tests__/relativeTimeAgo.test.ts    | 77 +++++++++++++++++++++-
 packages/dashboard/app/utils/relativeTimeAgo.ts    | 66 +++++++++++++++----
 8 files changed, 236 insertions(+), 62 deletions(-)

Fusion-Task-Id: FN-6601

Fusion-Task-Lineage: 3521afa0-f3ce-49a4-8861-efd4f64f9a69
2026-06-17 17:57:19 -07:00

66 lines
2.4 KiB
TypeScript

export type RelativeTimeBucket = "just-now" | "minutes" | "hours" | "days" | "weeks" | "older";
export interface RelativeTimeBucketResult {
bucket: RelativeTimeBucket;
count: number;
days: number;
date: Date;
}
/**
* FNXC:RelativeTime 2026-06-17-17:22:
* FN-6601 consolidates relative-time bucket math while preserving each surface's existing i18n keys, capitalization, and fallback policy.
* Callers map buckets to their local strings instead of sharing rendered copy.
*/
export function getRelativeTimeBucket(iso: string, now: number = Date.now()): RelativeTimeBucketResult | null {
if (!iso) return null;
const timestampMs = Date.parse(iso);
if (!Number.isFinite(timestampMs)) return null;
const diffMs = now - timestampMs;
if (diffMs < 0) return null;
const diffSeconds = Math.floor(diffMs / 1_000);
const diffMinutes = Math.floor(diffMs / 60_000);
const diffHours = Math.floor(diffMs / 3_600_000);
const diffDays = Math.floor(diffMs / 86_400_000);
const diffWeeks = Math.floor(diffDays / 7);
const date = new Date(timestampMs);
if (diffSeconds < 60) return { bucket: "just-now", count: 0, days: 0, date };
if (diffMinutes < 60) return { bucket: "minutes", count: diffMinutes, days: 0, date };
if (diffHours < 24) return { bucket: "hours", count: diffHours, days: 0, date };
if (diffDays < 7) return { bucket: "days", count: diffDays, days: diffDays, date };
if (diffDays < 28) return { bucket: "weeks", count: diffWeeks, days: diffDays, date };
return { bucket: "older", count: diffWeeks, days: diffDays, date };
}
/**
* FNXC:TaskChatTimestamps 2026-06-17-15:40:
* FN-6597 requires compact relative timestamps for task-chat agent groups and user messages without live polling.
* Invalid or missing timestamps must return an empty string so UI callers can omit the label instead of rendering NaN or Invalid Date.
*/
export function formatRelativeTimeAgo(iso: string, now: number = Date.now()): string {
const bucket = getRelativeTimeBucket(iso, now);
if (!bucket) {
const timestampMs = Date.parse(iso);
return iso && Number.isFinite(timestampMs) && now - timestampMs < 0 ? "just now" : "";
}
switch (bucket.bucket) {
case "just-now":
return "just now";
case "minutes":
return `${bucket.count}m ago`;
case "hours":
return `${bucket.count}h ago`;
case "days":
return `${bucket.count}d ago`;
case "weeks":
case "older":
return bucket.date.toLocaleDateString();
}
}