From 49763582603cea3937c201fe49423cb053399e5c Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Fri, 6 Mar 2026 13:35:36 -0500 Subject: [PATCH] Replace fake relative time strings with ISO 8601 timestamps in API The API was returning human-readable strings like "2h ago" and "in 4 hours" instead of real timestamps. The frontend now formats these for display using timeAgo() and a new timeUntil() helper. Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/arc-web/app/lib/time.ts | 14 +++++ apps/arc-web/app/routes/insights.tsx | 3 +- .../app/routes/verification-detail.tsx | 3 +- apps/arc-web/app/routes/workflows.tsx | 5 +- crates/arc-api/src/demo/mod.rs | 58 ++++++++++--------- docs/api-reference/arc-api.yaml | 24 ++++---- .../src/models/history-entry.ts | 2 +- .../src/models/recent-control-result.ts | 2 +- .../src/models/workflow-last-run.ts | 4 +- .../src/models/workflow-schedule.ts | 2 +- 10 files changed, 70 insertions(+), 47 deletions(-) diff --git a/apps/arc-web/app/lib/time.ts b/apps/arc-web/app/lib/time.ts index cddcd4b16..631b18c71 100644 --- a/apps/arc-web/app/lib/time.ts +++ b/apps/arc-web/app/lib/time.ts @@ -12,6 +12,20 @@ export function timeAgo(iso: string): string { return `${days}d ago`; } +/** + * Format an ISO 8601 timestamp as a relative future time string (e.g. "in 2h", "in 3d"). + */ +export function timeUntil(iso: string): string { + const seconds = Math.floor((new Date(iso).getTime() - Date.now()) / 1000); + if (seconds < 60) return "in <1m"; + const minutes = Math.floor(seconds / 60); + if (minutes < 60) return `in ${minutes}m`; + const hours = Math.floor(minutes / 60); + if (hours < 24) return `in ${hours}h`; + const days = Math.floor(hours / 24); + return `in ${days}d`; +} + /** * Return a human-readable date label for grouping (e.g. "Today", "Yesterday", "Previous 7 days"). */ diff --git a/apps/arc-web/app/routes/insights.tsx b/apps/arc-web/app/routes/insights.tsx index d9dfbf0f5..ab05b3274 100644 --- a/apps/arc-web/app/routes/insights.tsx +++ b/apps/arc-web/app/routes/insights.tsx @@ -1,6 +1,7 @@ import { Link, Outlet, useNavigate } from "react-router"; import { PlusIcon } from "@heroicons/react/24/outline"; import { apiJson } from "../api-client"; +import { timeAgo } from "../lib/time"; import type { PaginatedSavedQueryList, PaginatedHistoryEntryList } from "@qltysh/arc-api-client"; import type { Route } from "./+types/insights"; @@ -108,7 +109,7 @@ export default function InsightsLayout({ loaderData }: Route.ComponentProps) { {entry.sql} - {entry.timestamp} · {entry.rowsReturned} rows + {timeAgo(entry.timestamp)} · {entry.rowsReturned} rows ))} diff --git a/apps/arc-web/app/routes/verification-detail.tsx b/apps/arc-web/app/routes/verification-detail.tsx index 2a40cdded..0c74f28e4 100644 --- a/apps/arc-web/app/routes/verification-detail.tsx +++ b/apps/arc-web/app/routes/verification-detail.tsx @@ -56,6 +56,7 @@ import type { VerificationStatus, } from "../data/verifications"; import { apiJson } from "../api-client"; +import { timeAgo } from "../lib/time"; import type { VerificationDetailResponse } from "@qltysh/arc-api-client"; import type { Route } from "./+types/verification-detail"; @@ -317,7 +318,7 @@ export default function VerificationDetail({ loaderData }: Route.ComponentProps) {run.workflow} - {run.timestamp} + {timeAgo(run.timestamp)} ))} diff --git a/apps/arc-web/app/routes/workflows.tsx b/apps/arc-web/app/routes/workflows.tsx index cd48a0ddb..e9a121b8c 100644 --- a/apps/arc-web/app/routes/workflows.tsx +++ b/apps/arc-web/app/routes/workflows.tsx @@ -14,6 +14,7 @@ import { } from "@heroicons/react/24/outline"; import { Link } from "react-router"; import { apiJson } from "../api-client"; +import { timeAgo, timeUntil } from "../lib/time"; import type { PaginatedWorkflowList } from "@qltysh/arc-api-client"; import type { Route } from "./+types/workflows"; @@ -110,10 +111,10 @@ export async function loader({ request }: Route.LoaderArgs) { name: w.name, slug: w.slug, filename: w.filename, - lastRun: w.last_run?.label ?? "never", + lastRun: w.last_run?.ran_at ? timeAgo(w.last_run.ran_at) : "never", color: slugColorMap[w.slug] ?? "var(--color-teal-500)", schedule: w.schedule?.expression, - nextRun: w.schedule?.next_run, + nextRun: w.schedule?.next_run ? timeUntil(w.schedule.next_run) : undefined, })); return { workflows }; } diff --git a/crates/arc-api/src/demo/mod.rs b/crates/arc-api/src/demo/mod.rs index b48d558cb..0c2786125 100644 --- a/crates/arc-api/src/demo/mod.rs +++ b/crates/arc-api/src/demo/mod.rs @@ -1104,6 +1104,7 @@ mod usage { } mod workflows { + use super::ts; use arc_types::*; pub fn list_items() -> Vec { @@ -1112,28 +1113,28 @@ mod workflows { name: "Fix Build".into(), slug: "fix_build".into(), filename: "fix_build.dot".into(), - last_run: Some(WorkflowLastRun { label: "2 hours ago".into() }), + last_run: Some(WorkflowLastRun { ran_at: ts("2025-09-15T12:00:00Z") }), schedule: None, }, WorkflowListItem { name: "Implement Feature".into(), slug: "implement".into(), filename: "implement.dot".into(), - last_run: Some(WorkflowLastRun { label: "4 days ago".into() }), + last_run: Some(WorkflowLastRun { ran_at: ts("2025-09-11T10:00:00Z") }), schedule: None, }, WorkflowListItem { name: "Sync Drift".into(), slug: "sync_drift".into(), filename: "sync_drift.dot".into(), - last_run: Some(WorkflowLastRun { label: "1 day ago".into() }), + last_run: Some(WorkflowLastRun { ran_at: ts("2025-09-14T14:00:00Z") }), schedule: None, }, WorkflowListItem { name: "Expand Product".into(), slug: "expand".into(), filename: "expand.dot".into(), - last_run: Some(WorkflowLastRun { label: "2 weeks ago".into() }), + last_run: Some(WorkflowLastRun { ran_at: ts("2025-09-01T08:00:00Z") }), schedule: None, }, ] @@ -1373,6 +1374,7 @@ disk = 10 } mod verifications { + use super::ts; use arc_types::*; // ── Category definitions (name, question, controls) ───────────────── @@ -1417,35 +1419,35 @@ mod verifications { run_title: "PR #312 \u{2014} Add OAuth2 PKCE flow", workflow: "code_review", result: VerificationStatus::Pass, - timestamp: "2h ago", + timestamp: "2025-09-15T12:00:00Z", }, RecentResultDef { run_id: "run-046", run_title: "PR #311 \u{2014} Update rate limiter config", workflow: "code_review", result: VerificationStatus::Pass, - timestamp: "5h ago", + timestamp: "2025-09-15T09:00:00Z", }, RecentResultDef { run_id: "run-044", run_title: "PR #309 \u{2014} Migrate to pnpm", workflow: "code_review", result: VerificationStatus::Pass, - timestamp: "1d ago", + timestamp: "2025-09-14T14:00:00Z", }, RecentResultDef { run_id: "run-042", run_title: "PR #307 \u{2014} Fix session timeout", workflow: "fix_build", result: VerificationStatus::Pass, - timestamp: "2d ago", + timestamp: "2025-09-13T14:00:00Z", }, RecentResultDef { run_id: "run-040", run_title: "PR #305 \u{2014} Add webhook retries", workflow: "code_review", result: VerificationStatus::Pass, - timestamp: "3d ago", + timestamp: "2025-09-12T14:00:00Z", }, ]; @@ -1455,35 +1457,35 @@ mod verifications { run_title: "PR #312 \u{2014} Add OAuth2 PKCE flow", workflow: "code_review", result: VerificationStatus::Pass, - timestamp: "2h ago", + timestamp: "2025-09-15T12:00:00Z", }, RecentResultDef { run_id: "run-046", run_title: "PR #311 \u{2014} Update rate limiter config", workflow: "code_review", result: VerificationStatus::Pass, - timestamp: "5h ago", + timestamp: "2025-09-15T09:00:00Z", }, RecentResultDef { run_id: "run-044", run_title: "PR #309 \u{2014} Migrate to pnpm", workflow: "code_review", result: VerificationStatus::Fail, - timestamp: "1d ago", + timestamp: "2025-09-14T14:00:00Z", }, RecentResultDef { run_id: "run-042", run_title: "PR #307 \u{2014} Fix session timeout", workflow: "fix_build", result: VerificationStatus::Pass, - timestamp: "2d ago", + timestamp: "2025-09-13T14:00:00Z", }, RecentResultDef { run_id: "run-040", run_title: "PR #305 \u{2014} Add webhook retries", workflow: "code_review", result: VerificationStatus::Pass, - timestamp: "3d ago", + timestamp: "2025-09-12T14:00:00Z", }, ]; @@ -1493,35 +1495,35 @@ mod verifications { run_title: "PR #312 \u{2014} Add OAuth2 PKCE flow", workflow: "code_review", result: VerificationStatus::Pass, - timestamp: "2h ago", + timestamp: "2025-09-15T12:00:00Z", }, RecentResultDef { run_id: "run-046", run_title: "PR #311 \u{2014} Update rate limiter config", workflow: "code_review", result: VerificationStatus::Fail, - timestamp: "5h ago", + timestamp: "2025-09-15T09:00:00Z", }, RecentResultDef { run_id: "run-044", run_title: "PR #309 \u{2014} Migrate to pnpm", workflow: "code_review", result: VerificationStatus::Pass, - timestamp: "1d ago", + timestamp: "2025-09-14T14:00:00Z", }, RecentResultDef { run_id: "run-042", run_title: "PR #307 \u{2014} Fix session timeout", workflow: "fix_build", result: VerificationStatus::Pass, - timestamp: "2d ago", + timestamp: "2025-09-13T14:00:00Z", }, RecentResultDef { run_id: "run-040", run_title: "PR #305 \u{2014} Add webhook retries", workflow: "code_review", result: VerificationStatus::Fail, - timestamp: "3d ago", + timestamp: "2025-09-12T14:00:00Z", }, ]; @@ -1531,35 +1533,35 @@ mod verifications { run_title: "PR #312 \u{2014} Add OAuth2 PKCE flow", workflow: "code_review", result: VerificationStatus::Fail, - timestamp: "2h ago", + timestamp: "2025-09-15T12:00:00Z", }, RecentResultDef { run_id: "run-046", run_title: "PR #311 \u{2014} Update rate limiter config", workflow: "code_review", result: VerificationStatus::Pass, - timestamp: "5h ago", + timestamp: "2025-09-15T09:00:00Z", }, RecentResultDef { run_id: "run-044", run_title: "PR #309 \u{2014} Migrate to pnpm", workflow: "code_review", result: VerificationStatus::Fail, - timestamp: "1d ago", + timestamp: "2025-09-14T14:00:00Z", }, RecentResultDef { run_id: "run-042", run_title: "PR #307 \u{2014} Fix session timeout", workflow: "fix_build", result: VerificationStatus::Fail, - timestamp: "2d ago", + timestamp: "2025-09-13T14:00:00Z", }, RecentResultDef { run_id: "run-040", run_title: "PR #305 \u{2014} Add webhook retries", workflow: "code_review", result: VerificationStatus::Pass, - timestamp: "3d ago", + timestamp: "2025-09-12T14:00:00Z", }, ]; @@ -2027,7 +2029,7 @@ mod verifications { run: RunReference { id: r.run_id.into(), title: r.run_title.into() }, workflow: WorkflowReference { slug: r.workflow.into() }, result: r.result, - timestamp: r.timestamp.into(), + timestamp: ts(r.timestamp), }) .collect() } @@ -2420,14 +2422,14 @@ mod insights { HistoryEntry { id: "h1".into(), sql: "SELECT workflow_name, COUNT(*) FROM runs GROUP BY 1".into(), - timestamp: "2 min ago".into(), + timestamp: ts("2025-09-15T13:58:00Z"), elapsed: 0.342, row_count: 6, }, HistoryEntry { id: "h2".into(), sql: "SELECT * FROM runs WHERE status = 'failed' LIMIT 100".into(), - timestamp: "8 min ago".into(), + timestamp: ts("2025-09-15T13:52:00Z"), elapsed: 0.127, row_count: 23, }, @@ -2435,7 +2437,7 @@ mod insights { id: "h3".into(), sql: "SELECT date_trunc('day', created_at) as d, COUNT(*) FROM runs GROUP BY 1" .into(), - timestamp: "15 min ago".into(), + timestamp: ts("2025-09-15T13:45:00Z"), elapsed: 0.531, row_count: 30, }, diff --git a/docs/api-reference/arc-api.yaml b/docs/api-reference/arc-api.yaml index f25d8e31c..ae8a62556 100644 --- a/docs/api-reference/arc-api.yaml +++ b/docs/api-reference/arc-api.yaml @@ -1796,19 +1796,21 @@ components: example: "0 */6 * * *" next_run: type: string - description: Human-readable relative timestamp of the next run. - example: in 4 hours + format: date-time + description: ISO 8601 timestamp of the next scheduled run. + example: "2025-09-15T18:00:00Z" WorkflowLastRun: description: Information about a workflow's most recent run. type: object required: - - label + - ran_at properties: - label: + ran_at: type: string - description: Human-readable relative timestamp. - example: 2 hours ago + format: date-time + description: ISO 8601 timestamp of the most recent run. + example: "2025-09-15T12:00:00Z" UsageStageRef: description: Reference to a usage stage. @@ -2536,8 +2538,9 @@ components: $ref: "#/components/schemas/VerificationStatus" timestamp: type: string - description: Human-readable relative timestamp of the evaluation. - example: 2h ago + format: date-time + description: ISO 8601 timestamp of the evaluation. + example: "2025-09-15T12:00:00Z" SiblingControl: description: Summary of a sibling verification control in the same category. @@ -2998,8 +3001,9 @@ components: example: "SELECT workflow_name, COUNT(*) FROM runs GROUP BY 1" timestamp: type: string - description: Human-readable relative timestamp of execution. - example: 2 min ago + format: date-time + description: ISO 8601 timestamp of execution. + example: "2025-09-15T14:00:00Z" elapsed: type: number description: Query execution time in seconds. diff --git a/packages/arc-api-client/src/models/history-entry.ts b/packages/arc-api-client/src/models/history-entry.ts index a002be31f..b6277f1f4 100644 --- a/packages/arc-api-client/src/models/history-entry.ts +++ b/packages/arc-api-client/src/models/history-entry.ts @@ -27,7 +27,7 @@ export interface HistoryEntry { */ 'sql': string; /** - * Human-readable relative timestamp of execution. + * ISO 8601 timestamp of execution. */ 'timestamp': string; /** diff --git a/packages/arc-api-client/src/models/recent-control-result.ts b/packages/arc-api-client/src/models/recent-control-result.ts index e1d3966c8..cc2ad2e63 100644 --- a/packages/arc-api-client/src/models/recent-control-result.ts +++ b/packages/arc-api-client/src/models/recent-control-result.ts @@ -31,7 +31,7 @@ export interface RecentControlResult { 'workflow': WorkflowReference; 'result': VerificationStatus; /** - * Human-readable relative timestamp of the evaluation. + * ISO 8601 timestamp of the evaluation. */ 'timestamp': string; } diff --git a/packages/arc-api-client/src/models/workflow-last-run.ts b/packages/arc-api-client/src/models/workflow-last-run.ts index 0f40c1912..0cf0887ba 100644 --- a/packages/arc-api-client/src/models/workflow-last-run.ts +++ b/packages/arc-api-client/src/models/workflow-last-run.ts @@ -19,8 +19,8 @@ */ export interface WorkflowLastRun { /** - * Human-readable relative timestamp. + * ISO 8601 timestamp of the most recent run. */ - 'label': string; + 'ran_at': string; } diff --git a/packages/arc-api-client/src/models/workflow-schedule.ts b/packages/arc-api-client/src/models/workflow-schedule.ts index 2df677374..cd9266038 100644 --- a/packages/arc-api-client/src/models/workflow-schedule.ts +++ b/packages/arc-api-client/src/models/workflow-schedule.ts @@ -23,7 +23,7 @@ export interface WorkflowSchedule { */ 'expression': string; /** - * Human-readable relative timestamp of the next run. + * ISO 8601 timestamp of the next scheduled run. */ 'next_run'?: string; }