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