diff --git a/frontend/src/components/Common/ConnectionNotice.tsx b/frontend/src/components/Common/ConnectionNotice.tsx
index f4675f7..03954d2 100644
--- a/frontend/src/components/Common/ConnectionNotice.tsx
+++ b/frontend/src/components/Common/ConnectionNotice.tsx
@@ -1,9 +1,9 @@
import { useEffect, useSyncExternalStore } from "react"
-import { ago } from "@/components/Health/queries"
import { connectionStore } from "@/lib/connectionStore"
import { dismiss, notify } from "@/lib/notificationStore"
import { isPortal } from "@/lib/portal"
+import { ago } from "@/lib/utils"
/** One card, replaced in place, rather than one per poll that confirms it. */
const ID = "connection-offline"
diff --git a/frontend/src/components/Flow/EdgeInspector.tsx b/frontend/src/components/Flow/EdgeInspector.tsx
index 02d1a60..6c2ec0f 100644
--- a/frontend/src/components/Flow/EdgeInspector.tsx
+++ b/frontend/src/components/Flow/EdgeInspector.tsx
@@ -6,6 +6,7 @@ import { Marquee } from "@/components/Common/Marquee"
import { Button } from "@/components/ui/button"
import { Popover, PopoverAnchor, PopoverContent } from "@/components/ui/popover"
import useCustomToast from "@/hooks/useCustomToast"
+import { ago } from "@/lib/utils"
import { displayName } from "./deriveEdges"
import { useLiveValue } from "./liveStore"
import { MessageSparkline } from "./MessageSparkline"
@@ -13,11 +14,7 @@ import { ValuePreview } from "./ValuePreview"
export function relativeTime(ts: number | null | undefined): string {
if (!ts) return "not seen yet"
- const seconds = Math.max(0, Math.round(Date.now() / 1000 - ts))
- if (seconds < 5) return "just now"
- if (seconds < 60) return `${seconds}s ago`
- if (seconds < 3600) return `${Math.round(seconds / 60)}m ago`
- return `${Math.round(seconds / 3600)}h ago`
+ return Date.now() / 1000 - ts < 5 ? "just now" : ago(ts)
}
/**
diff --git a/frontend/src/components/Health/HealthActivity.tsx b/frontend/src/components/Health/HealthActivity.tsx
index 2b4c0be..6386111 100644
--- a/frontend/src/components/Health/HealthActivity.tsx
+++ b/frontend/src/components/Health/HealthActivity.tsx
@@ -8,9 +8,8 @@ import { UplotChart } from "@/components/Common/UplotChart"
import { useEngineEventMinute } from "@/components/Flow/liveStore"
import { PANEL_SECTION } from "@/components/Flow/SidePanel"
import { Button } from "@/components/ui/button"
-import { cn, dur, si } from "@/lib/utils"
+import { ago, cn, dur, si } from "@/lib/utils"
import {
- ago,
auditQueryOptions,
CARD,
clock,
@@ -358,7 +357,7 @@ export function HealthActivity({ range }: { range: Range }) {
{dur(run.duration_ms)}
-
+
{ago(run.started_at)}
diff --git a/frontend/src/components/Health/HealthOverview.tsx b/frontend/src/components/Health/HealthOverview.tsx
index 61e2b06..81c5571 100644
--- a/frontend/src/components/Health/HealthOverview.tsx
+++ b/frontend/src/components/Health/HealthOverview.tsx
@@ -5,9 +5,8 @@ import { type Range, RangePicker } from "@/components/Common/RangePicker"
import { PANEL_SECTION } from "@/components/Flow/SidePanel"
import { runOverviewQueryOptions } from "@/components/Runs/queries"
import { Badge } from "@/components/ui/badge"
-import { cn, dur, si } from "@/lib/utils"
+import { ago, cn, dur, si } from "@/lib/utils"
import {
- ago,
CARD,
failuresQueryOptions,
flowRollupsQueryOptions,
diff --git a/frontend/src/components/Health/queries.ts b/frontend/src/components/Health/queries.ts
index fecc76b..81c269b 100644
--- a/frontend/src/components/Health/queries.ts
+++ b/frontend/src/components/Health/queries.ts
@@ -122,17 +122,6 @@ export const deadLetterQueryOptions = () => ({
refetchInterval: REFRESH,
})
-/** How long ago something happened, in the coarsest unit that still says it. */
-export function ago(ts: string | number | null | undefined): string {
- if (!ts) return "—"
- const stamp = typeof ts === "number" ? ts * 1000 : Date.parse(ts)
- const seconds = Math.max(0, (Date.now() - stamp) / 1000)
- if (seconds < 90) return `${Math.round(seconds)}s ago`
- if (seconds < 5400) return `${Math.round(seconds / 60)}m ago`
- if (seconds < 172800) return `${Math.round(seconds / 3600)}h ago`
- return `${Math.round(seconds / 86400)}d ago`
-}
-
/**
* The minute a timestamp belongs to, as unix seconds.
*
diff --git a/frontend/src/components/Runs/RunDetail.tsx b/frontend/src/components/Runs/RunDetail.tsx
index 6a8b5c4..a26b8b0 100644
--- a/frontend/src/components/Runs/RunDetail.tsx
+++ b/frontend/src/components/Runs/RunDetail.tsx
@@ -5,7 +5,6 @@ import { useState } from "react"
import type { ArtifactRow, RunNodeRow } from "@/client"
import { ValuePreview } from "@/components/Flow/ValuePreview"
-import { ago } from "@/components/Health/queries"
import { Button } from "@/components/ui/button"
import { Skeleton } from "@/components/ui/skeleton"
import {
@@ -17,7 +16,7 @@ import {
TableRow,
} from "@/components/ui/table"
import useCustomToast from "@/hooks/useCustomToast"
-import { cn, dur, si } from "@/lib/utils"
+import { ago, cn, dur, si } from "@/lib/utils"
import { MetricPicker, RunMetricChart, useMetricNames } from "./MetricChart"
import { OpenInDashboard } from "./OpenInDashboard"
import {
diff --git a/frontend/src/components/Runs/RunsScreen.tsx b/frontend/src/components/Runs/RunsScreen.tsx
index cdd4c95..18037c6 100644
--- a/frontend/src/components/Runs/RunsScreen.tsx
+++ b/frontend/src/components/Runs/RunsScreen.tsx
@@ -39,7 +39,7 @@ import {
TableHeader,
TableRow,
} from "@/components/ui/table"
-import { cn, dur } from "@/lib/utils"
+import { ago, cn, dur } from "@/lib/utils"
import {
AxisPicker,
MetricPicker,
@@ -425,12 +425,6 @@ function ParamValue({ value }: { value: unknown }) {
)
}
-/** How long ago a moment was, in `dur`'s units so a row's two times agree. */
-function since(ts: unknown): string {
- if (!ts) return "—"
- return `${dur(Date.now() - Date.parse(String(ts)))} ago`
-}
-
function RunsTable({
runs,
search,
@@ -611,7 +605,7 @@ function RunsTable({
`${dur(run.duration_ms)} ago`
) : (
<>
- {since(run.created_at)}
+ {ago(String(run.created_at ?? ""))}
|
{run.duration_ms ? dur(run.duration_ms) : "—"}
>
diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts
index 3969708..5608c7d 100644
--- a/frontend/src/lib/utils.ts
+++ b/frontend/src/lib/utils.ts
@@ -81,3 +81,13 @@ export function dur(ms: number, digits = 3): string {
]
return `${Number((rounded / factor).toPrecision(digits))} ${unit}`
}
+
+/**
+ * How long ago a moment was, in `dur`'s notation: "4.52 min ago". A number is
+ * unix seconds, a string an ISO timestamp.
+ */
+export function ago(ts: string | number | null | undefined): string {
+ if (!ts) return "—"
+ const stamp = typeof ts === "number" ? ts * 1000 : Date.parse(ts)
+ return `${dur(Math.max(0, Date.now() - stamp))} ago`
+}
diff --git a/frontend/src/routes/_layout/workers.tsx b/frontend/src/routes/_layout/workers.tsx
index 7abb438..c5a73d1 100644
--- a/frontend/src/routes/_layout/workers.tsx
+++ b/frontend/src/routes/_layout/workers.tsx
@@ -23,6 +23,7 @@ import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import useAuth from "@/hooks/useAuth"
import useCustomToast from "@/hooks/useCustomToast"
+import { ago } from "@/lib/utils"
import { handleError } from "@/utils"
export const Route = createFileRoute("/_layout/workers")({
@@ -40,13 +41,6 @@ const emptyFlavor = { name: "", cpus: 1, gpus: 0, ram: 2048, description: "" }
/** Fresh enough to watch a node take a machine, slow enough to be free. */
const REFETCH_MS = 5000
-function ago(seconds: number): string {
- const since = Date.now() / 1000 - seconds
- if (since < 60) return `${Math.max(0, Math.round(since))}s ago`
- if (since < 3600) return `${Math.round(since / 60)}m ago`
- return `${Math.round(since / 3600)}h ago`
-}
-
type Level = { total: number; free: number }
/** "cpu 2/18" — what is in use, out of what there is. */
@@ -203,7 +197,7 @@ function Attached() {
{worker.protocol}
- {worker.behind ? "refused" : "seen"} {ago(worker.last_seen ?? 0)}
+ {worker.behind ? "refused" : "seen"} {ago(worker.last_seen)}
diff --git a/frontend/src/utils.ts b/frontend/src/utils.ts
index a7eb1eb..eb5f66c 100644
--- a/frontend/src/utils.ts
+++ b/frontend/src/utils.ts
@@ -1,7 +1,7 @@
import { AxiosError } from "axios"
import type { ApiError } from "./client"
-import { ago } from "./components/Health/queries"
import { offlineDetail } from "./lib/connectionStore"
+import { ago } from "./lib/utils"
function extractErrorMessage(err: ApiError): string {
// An unreachable instance is not a rejected action: say plainly that