Files
traefik-llmgateway/webui/src/lib/last-seen.ts
T
lukaszraczylo ee49467e64 feat(webui): redesign the admin dashboard around admin tasks
A sidebar replaces the flat tabs with seven pages, and one filter bar
sets the time range, comparison period and scope for all of them.

- Home: spend against budget, forecast, request rate, error rate,
  open breakers, top users and models, recent events and warnings.
- Spend: spend over time with comparison, budget burn-down, an
  attribution drill-down from group to user to model, pricing health
  and cost avoided by free models.
- Consumers: a directory with last seen and limit bars, an access
  matrix, and a detail page per user.
- Models: the catalog with billing prices, fleet latency and error
  rates, and provider health.
- Reliability: error, timeout, failover, 429 and 402 trends, events,
  and latency per provider.
- Config: the redacted config and a change helper that emits YAML or
  users.json snippets to paste into git.
- MCP & Agents: callers per target.

Page state lives in the URL hash, and old tab links map to the new
pages. The replaced views are removed. admin_assets_gen.go is
regenerated.

Claude-Session: https://claude.ai/code/session_01V5jYJNxpDFdJkyyExpADrv
2026-09-23 16:41:01 +01:00

40 lines
2.3 KiB
TypeScript

// Consumers page (redesign-plan.md section 3.4): pure relative-time
// formatting for AdminUsageEntryView.lastSeen — a fleet-wide, throttled
// (60s/replica/scope) unix-seconds timestamp (types/api.ts's own doc
// comment). Kept out of usage-columns.ts/ConsumerDirectory.vue the same way
// every other pure display helper in this panel (lib/format.ts, lib/
// usage-bars.ts, lib/forecast.ts) is — vitest's node-environment config
// exercises it directly, no component mount or fake timers required (every
// function here takes `now` as an explicit argument, never reads Date.now()
// itself, matching lib/forecast.ts's own determinism convention).
const SECONDS_PER_MINUTE = 60
const SECONDS_PER_HOUR = 3600
const SECONDS_PER_DAY = 86400
/** MONTH_THRESHOLD_DAYS is where formatLastSeen switches from "Nd ago" to "Nmo ago" — 2 months' worth of days, so a scope last seen 6 weeks ago still reads as a legible day count rather than rounding down to "1mo ago" (a needlessly coarse figure for something that recent). */
const MONTH_THRESHOLD_DAYS = 60
const DAYS_PER_MONTH_APPROX = 30
/**
* formatLastSeen renders a scope's last-seen unix-seconds timestamp as a
* short relative-time label: "just now" under a minute, "Nm ago" under an
* hour, "Nh ago" under a day, "Nd ago" under MONTH_THRESHOLD_DAYS, "Nmo
* ago" beyond it. `undefined` or `0` means "never seen, or admin.stats.
* lastSeen is off" (types/api.ts's own doc comment: "never render 0 as an
* epoch date") — rendered as the literal "never", never "0s ago" or a
* fabricated 1970 date. A timestamp in the future (clock skew between this
* replica and the reader's own clock) clamps to "just now" rather than a
* nonsensical negative duration.
*/
export function formatLastSeen(seconds: number | undefined, now: Date = new Date()): string {
if (!seconds) return 'never'
const nowSeconds = Math.floor(now.getTime() / 1000)
const delta = Math.max(0, nowSeconds - seconds)
if (delta < SECONDS_PER_MINUTE) return 'just now'
if (delta < SECONDS_PER_HOUR) return `${Math.floor(delta / SECONDS_PER_MINUTE)}m ago`
if (delta < SECONDS_PER_DAY) return `${Math.floor(delta / SECONDS_PER_HOUR)}h ago`
const days = Math.floor(delta / SECONDS_PER_DAY)
if (days < MONTH_THRESHOLD_DAYS) return `${days}d ago`
return `${Math.floor(days / DAYS_PER_MONTH_APPROX)}mo ago`
}