fabro/apps/fabro-web/app/lib/format.ts
Bryan Helmkamp ac0492a617
feat: track last_event_at on runs and show it in the run header
Add a `last_event_at` timestamp to RunProjection (set in apply_event so
every event ticks the field) and surface it through RunSummary and the
RunListItem board response. Backed by an OpenAPI extension so both the
Rust and TypeScript clients pick up the new optional field.

In the web UI, the run-detail header gains a "Last activity Xm ago"
badge next to the elapsed-time chip, driven by a 30-second ticker so the
relative time stays current between event refreshes.

The fabro-server tests.rs hunk is incidental rustfmt drift surfaced by
running `cargo fmt --all` over the workspace; including it keeps CI's
fmt-check green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 10:15:04 -07:00

63 lines
2.2 KiB
TypeScript

/**
* Format a number of seconds into a human-readable duration string.
* Examples: "23s", "7m", "2h 15m", "3d"
*/
export function formatElapsedSecs(secs: number): string {
if (secs < 60) return `${Math.round(secs)}s`;
const minutes = Math.floor(secs / 60);
if (minutes < 60) {
const remainSecs = Math.round(secs % 60);
return remainSecs > 0 ? `${minutes}m ${remainSecs}s` : `${minutes}m`;
}
const hours = Math.floor(minutes / 60);
if (hours < 24) {
const remainMin = minutes % 60;
return remainMin > 0 ? `${hours}h ${remainMin}m` : `${hours}h`;
}
const days = Math.floor(hours / 24);
const remainHrs = hours % 24;
return remainHrs > 0 ? `${days}d ${remainHrs}h` : `${days}d`;
}
/**
* Format a byte count for display (e.g., "1.23 MB", "247.32 KB", "742 B").
*/
export function formatBytes(bytes: number): string {
if (bytes >= 1e9) return `${(bytes / 1e9).toFixed(2)} GB`;
if (bytes >= 1e6) return `${(bytes / 1e6).toFixed(2)} MB`;
if (bytes >= 1e3) return `${(bytes / 1e3).toFixed(2)} KB`;
return `${bytes} B`;
}
/**
* Short relative time string (e.g. "just now", "15s ago", "4m ago", "2h ago", "3d ago").
* Future timestamps clamp to "just now".
*/
export function formatRelativeTime(iso: string, now: number = Date.now()): string {
const then = Date.parse(iso);
if (Number.isNaN(then)) return "";
const secs = Math.max(0, Math.floor((now - then) / 1000));
if (secs < 5) return "just now";
if (secs < 60) return `${secs}s ago`;
const minutes = Math.floor(secs / 60);
if (minutes < 60) return `${minutes}m ago`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}h ago`;
const days = Math.floor(hours / 24);
return `${days}d ago`;
}
/**
* Format seconds into a duration string for display (e.g., "1m 12s", "23s").
*/
export function formatDurationSecs(secs: number): string {
if (secs < 60) return `${Math.round(secs)}s`;
const minutes = Math.floor(secs / 60);
const remainSecs = Math.round(secs % 60);
if (minutes < 60) {
return remainSecs > 0 ? `${minutes}m ${remainSecs}s` : `${minutes}m`;
}
const hours = Math.floor(minutes / 60);
const remainMin = minutes % 60;
return remainMin > 0 ? `${hours}h ${remainMin}m` : `${hours}h`;
}