@opengeni/react 0.13.0 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/README.md +129 -23
  2. package/dist/chunk-6BXBGJ3B.js +1956 -0
  3. package/dist/chunk-6BXBGJ3B.js.map +1 -0
  4. package/dist/chunk-6UT5OC3P.js +2133 -0
  5. package/dist/chunk-6UT5OC3P.js.map +1 -0
  6. package/dist/chunk-EBTT3MYN.js +92 -0
  7. package/dist/chunk-EBTT3MYN.js.map +1 -0
  8. package/dist/chunk-HHFA4AFX.js +425 -0
  9. package/dist/chunk-HHFA4AFX.js.map +1 -0
  10. package/dist/chunk-LAZ2A743.js +2393 -0
  11. package/dist/chunk-LAZ2A743.js.map +1 -0
  12. package/dist/chunk-MRSECXRP.js +563 -0
  13. package/dist/chunk-MRSECXRP.js.map +1 -0
  14. package/dist/chunk-TK7G6XLT.js +18 -0
  15. package/dist/chunk-TK7G6XLT.js.map +1 -0
  16. package/dist/composer-DvUaa5ki.d.ts +585 -0
  17. package/dist/composer.d.ts +6 -0
  18. package/dist/composer.js +56 -0
  19. package/dist/composer.js.map +1 -0
  20. package/dist/index.d.ts +642 -1080
  21. package/dist/index.js +7672 -8005
  22. package/dist/index.js.map +1 -1
  23. package/dist/machines.d.ts +412 -3
  24. package/dist/machines.js +25 -1
  25. package/dist/noto-sans-arabic-loader-IA2T4X2A.js +21 -0
  26. package/dist/noto-sans-arabic-loader-IA2T4X2A.js.map +1 -0
  27. package/dist/noto-sans-jp-loader.generated-HFGLYBR3.js +292 -0
  28. package/dist/noto-sans-jp-loader.generated-HFGLYBR3.js.map +1 -0
  29. package/dist/queue-surface-implementation-NQMV2ML3.js +890 -0
  30. package/dist/queue-surface-implementation-NQMV2ML3.js.map +1 -0
  31. package/dist/session-Gd4iVPYw.d.ts +425 -0
  32. package/dist/session-context-DIFFlXKc.d.ts +52 -0
  33. package/dist/session.d.ts +4 -0
  34. package/dist/session.js +50 -0
  35. package/dist/session.js.map +1 -0
  36. package/dist/use-composer-BCfm4mzX.d.ts +112 -0
  37. package/package.json +23 -4
  38. package/src/approvals.ts +5 -3
  39. package/src/client.ts +50 -1
  40. package/src/components/approval-surface.tsx +198 -0
  41. package/src/components/chat-composer.tsx +104 -751
  42. package/src/components/code-editor.tsx +104 -26
  43. package/src/components/command-palette.tsx +8 -2
  44. package/src/components/composer-transcription-control.tsx +211 -0
  45. package/src/components/composer.tsx +1536 -0
  46. package/src/components/desktop-viewer.tsx +3 -3
  47. package/src/components/enrollment-consent.tsx +2 -2
  48. package/src/components/file-browser.tsx +367 -29
  49. package/src/components/human-input-form.tsx +417 -0
  50. package/src/components/machine-card.tsx +82 -16
  51. package/src/components/machine-health-pill.tsx +68 -0
  52. package/src/components/machine-metrics.tsx +10 -24
  53. package/src/components/machines/health.ts +146 -0
  54. package/src/components/machines/machine-detail.tsx +220 -0
  55. package/src/components/machines/metric-history-chart.tsx +298 -0
  56. package/src/components/machines/metric-sparkline.tsx +76 -0
  57. package/src/components/machines/series.ts +113 -0
  58. package/src/components/machines-dashboard.tsx +16 -4
  59. package/src/components/message-timeline.tsx +15 -7
  60. package/src/components/model-picker.tsx +12 -3
  61. package/src/components/pierre-diff.tsx +32 -6
  62. package/src/components/queue-surface-implementation.tsx +1041 -0
  63. package/src/components/queue-surface-state.tsx +94 -0
  64. package/src/components/queue-surface.tsx +60 -0
  65. package/src/components/sandbox-files.tsx +230 -21
  66. package/src/components/sandbox-terminal.tsx +8 -2
  67. package/src/components/sandbox-workspace.tsx +489 -135
  68. package/src/components/session-status.tsx +0 -6
  69. package/src/components/workbench-changes.tsx +145 -50
  70. package/src/components/workspace-dock.tsx +329 -68
  71. package/src/composer.ts +60 -0
  72. package/src/hooks/internal.ts +115 -34
  73. package/src/hooks/use-composer.ts +635 -74
  74. package/src/hooks/use-human-input.ts +131 -0
  75. package/src/hooks/use-machine-chip.ts +2 -2
  76. package/src/hooks/use-machines.ts +27 -11
  77. package/src/hooks/use-sandbox-files.ts +310 -62
  78. package/src/hooks/use-sandbox-git.ts +154 -40
  79. package/src/hooks/use-sandbox-terminal.ts +28 -6
  80. package/src/hooks/use-session-capabilities.ts +38 -9
  81. package/src/hooks/use-session-control.ts +43 -15
  82. package/src/hooks/use-session-events.ts +523 -48
  83. package/src/hooks/use-session-lineage.ts +15 -6
  84. package/src/hooks/use-session.ts +10 -2
  85. package/src/hooks/use-slash-commands.ts +38 -38
  86. package/src/hooks/use-transcription.ts +757 -0
  87. package/src/hooks/use-turn-queue.ts +273 -77
  88. package/src/hooks/use-windowed-sections.ts +2 -2
  89. package/src/hooks/use-workspace-capture.ts +146 -40
  90. package/src/hooks/use-workspace-edit.ts +50 -14
  91. package/src/hooks/use-workspace-sessions.ts +3 -0
  92. package/src/human-input.ts +72 -0
  93. package/src/index.ts +88 -12
  94. package/src/lib/noto-sans-arabic-loader.ts +21 -0
  95. package/src/lib/noto-sans-jp-loader.generated.ts +295 -0
  96. package/src/lib/use-portal-token-style.ts +51 -0
  97. package/src/lib/use-unicode-fonts.ts +57 -0
  98. package/src/machines.ts +16 -0
  99. package/src/provider.tsx +163 -41
  100. package/src/session-context.ts +135 -0
  101. package/src/session.ts +79 -0
  102. package/src/timeline/parsers.ts +78 -6
  103. package/src/timeline/projection.ts +36 -6
  104. package/src/timeline/tool-renderers.tsx +41 -0
  105. package/src/timeline/turn-summary.tsx +2 -2
  106. package/src/timeline/types.ts +16 -8
  107. package/styles/index.css +78 -0
  108. package/styles/index.d.ts +2 -0
  109. package/styles/tokens.css +11 -6
  110. package/styles/tokens.d.ts +2 -0
  111. package/dist/chunk-NFYVQWIB.js +0 -1377
  112. package/dist/chunk-NFYVQWIB.js.map +0 -1
  113. package/dist/machines-CnlMb7E-.d.ts +0 -329
@@ -28,12 +28,14 @@ function Meter({
28
28
  fillPct: number;
29
29
  tone: "ok" | "warn" | "hot";
30
30
  }) {
31
+ // Traffic-light ramp, aligned with the fused health verdict: calm resources
32
+ // read green (idle), pressure ambers (running), near-the-wall reds (failed).
31
33
  const fillClass =
32
34
  tone === "hot"
33
35
  ? "bg-og-status-failed"
34
36
  : tone === "warn"
35
- ? "bg-og-status-waiting"
36
- : "bg-og-status-running";
37
+ ? "bg-og-status-running"
38
+ : "bg-og-status-idle";
37
39
  return (
38
40
  <div className="flex min-w-0 flex-col gap-1" data-metric={label.toLowerCase()}>
39
41
  <div className="flex items-baseline justify-between gap-2">
@@ -59,20 +61,12 @@ function toneFor(p: number): "ok" | "warn" | "hot" {
59
61
  return "ok";
60
62
  }
61
63
 
62
- /** The token text color for a tone (matches the `Meter` fill ramp). */
63
- function toneTextClass(tone: "ok" | "warn" | "hot"): string {
64
- return tone === "hot"
65
- ? "text-og-status-failed"
66
- : tone === "warn"
67
- ? "text-og-status-waiting"
68
- : "text-og-status-running";
69
- }
70
-
71
64
  /**
72
65
  * Load average as three labeled mini-stats (1m / 5m / 15m) — an honest triple of
73
- * numbers, NOT a fake gauge. The whole triple is tinted by `load1`'s tone (load
74
- * isn't a 0..100 ratio, so we tone it but never draw a bar). Run queue rides
75
- * alongside as a quiet chip when there's pending work.
66
+ * numbers, NOT a fake gauge. Load is not a 0..100 ratio and we don't know the
67
+ * core count, so a raw load of 2 or 8 is not inherently "bad" we render it
68
+ * NEUTRAL rather than falsely alarming. The real saturation signal is the run
69
+ * queue, which rides alongside as a quiet chip when there's pending work.
76
70
  */
77
71
  function StatTriple({
78
72
  load1,
@@ -85,9 +79,6 @@ function StatTriple({
85
79
  load15: number;
86
80
  runQueue: number;
87
81
  }) {
88
- // Load isn't a percent — tone by load1 against a nominal single-core ceiling
89
- // (≥0.9/core ≈ saturated). Used only to tint the labels/values, never a bar.
90
- const tone = toneFor(load1 * 100);
91
82
  const stats: Array<{ label: string; value: number }> = [
92
83
  { label: "1m", value: load1 },
93
84
  { label: "5m", value: load5 },
@@ -102,15 +93,10 @@ function StatTriple({
102
93
  <div className="flex items-baseline gap-3">
103
94
  {stats.map((s) => (
104
95
  <div key={s.label} className="flex items-baseline gap-1">
105
- <span
106
- className={cn(
107
- "text-[10px] font-medium uppercase tracking-wide",
108
- toneTextClass(tone),
109
- )}
110
- >
96
+ <span className="text-[10px] font-medium uppercase tracking-wide text-og-fg-subtle">
111
97
  {s.label}
112
98
  </span>
113
- <span className={cn("font-og-mono text-[12px] tabular-nums", toneTextClass(tone))}>
99
+ <span className="font-og-mono text-[12px] tabular-nums text-og-fg">
114
100
  {s.value.toFixed(2)}
115
101
  </span>
116
102
  </div>
@@ -0,0 +1,146 @@
1
+ // ----------------------------------------------------------------------------
2
+ // Machine health — the single derived signal.
3
+ //
4
+ // The dashboard already shows connection state (a pill) and per-resource meters
5
+ // (independently tinted). What it lacks is ONE verdict that fuses the three
6
+ // things that actually make a machine "in trouble":
7
+ //
8
+ // 1. reachability — is the control plane still hearing from it?
9
+ // 2. resource load — is a resource (mem/disk/cpu) near the wall?
10
+ // 3. freshness — is the latest sample recent, or has telemetry gone quiet?
11
+ //
12
+ // This module is a PURE function from a machine's state + latest sample + clock
13
+ // to a `HealthVerdict`. No React, no tokens — the UI maps `level` to a color.
14
+ // Keeping it pure means it is trivially testable and reused by cards, the detail
15
+ // hero, and any future fleet-summary count.
16
+ // ----------------------------------------------------------------------------
17
+ import type { MachineState, MetricSample } from "../../types/machines";
18
+
19
+ export type HealthLevel = "healthy" | "degraded" | "critical" | "offline" | "unknown";
20
+
21
+ export type HealthVerdict = {
22
+ level: HealthLevel;
23
+ /** Title-case label for the pill ("Healthy", "Under load", "Critical", …). */
24
+ label: string;
25
+ /** One short human clause naming the dominant cause ("Memory 94%", "No display"…). */
26
+ reason: string;
27
+ /** The latest sample is older than we'd expect from a live agent (~5s cadence). */
28
+ stale: boolean;
29
+ };
30
+
31
+ // Resource pressure thresholds. `warn` mirrors the existing meter ramp (>=70
32
+ // tints), `crit` is the "near the wall" line. Disk is stricter than CPU/mem
33
+ // because a full disk is a hard failure, whereas transient CPU/mem spikes are
34
+ // normal on a working machine.
35
+ const PRESSURE = {
36
+ cpu: { warn: 90, crit: 98 },
37
+ mem: { warn: 85, crit: 95 },
38
+ disk: { warn: 90, crit: 96 },
39
+ } as const;
40
+
41
+ // A live agent samples ~every 5s and the series downsamples to ~1/min. We treat
42
+ // a latest sample older than 45s as "telemetry has gone quiet" — enough slack to
43
+ // not flap on a single missed beat, tight enough to notice a wedged sampler.
44
+ const STALE_AFTER_MS = 45_000;
45
+
46
+ function pct(used: number, total: number): number {
47
+ if (!Number.isFinite(used) || !Number.isFinite(total) || total <= 0) return 0;
48
+ return (used / total) * 100;
49
+ }
50
+
51
+ /** Fold a machine's reachability + latest sample + wall-clock into one verdict. */
52
+ export function deriveHealth(
53
+ state: MachineState,
54
+ metrics: MetricSample | null,
55
+ now: number = Date.now(),
56
+ ): HealthVerdict {
57
+ // Reachability dominates: an offline machine has no meaningful resource story.
58
+ if (state === "offline") {
59
+ return { level: "offline", label: "Offline", reason: "Not reachable", stale: true };
60
+ }
61
+ if (state === "enrolling") {
62
+ return { level: "unknown", label: "Enrolling", reason: "Completing enrollment", stale: false };
63
+ }
64
+
65
+ const stale = metrics ? now - new Date(metrics.sampledAt).getTime() > STALE_AFTER_MS : true;
66
+
67
+ // Reconnecting, or online-but-quiet: reachable yet we can't trust the numbers.
68
+ if (state === "reconnecting") {
69
+ return { level: "degraded", label: "Reconnecting", reason: "Re-establishing link", stale };
70
+ }
71
+ if (!metrics) {
72
+ return { level: "unknown", label: "Online", reason: "Awaiting first sample", stale };
73
+ }
74
+ if (stale) {
75
+ return { level: "degraded", label: "Telemetry stale", reason: "No recent sample", stale: true };
76
+ }
77
+
78
+ // Reachable + fresh: the verdict is now the worst resource pressure. Evaluate
79
+ // each resource, keep the most severe, and name it in the reason.
80
+ const memPct = pct(metrics.memUsedBytes, metrics.memTotalBytes);
81
+ const diskPct = pct(metrics.diskUsedBytes, metrics.diskTotalBytes);
82
+ const cpu = metrics.cpuPct;
83
+
84
+ const pressures: Array<{ level: HealthLevel; reason: string; sev: number }> = [
85
+ resolve("Disk", diskPct, PRESSURE.disk),
86
+ resolve("Memory", memPct, PRESSURE.mem),
87
+ resolve("CPU", cpu, PRESSURE.cpu),
88
+ ];
89
+ const worst = pressures.reduce((a, b) => (b.sev > a.sev ? b : a));
90
+
91
+ if (worst.sev === 2) return { level: "critical", label: "Critical", reason: worst.reason, stale };
92
+ if (worst.sev === 1)
93
+ return { level: "degraded", label: "Under load", reason: worst.reason, stale };
94
+ return { level: "healthy", label: "Healthy", reason: "All systems nominal", stale };
95
+ }
96
+
97
+ function resolve(
98
+ name: string,
99
+ value: number,
100
+ t: { warn: number; crit: number },
101
+ ): { level: HealthLevel; reason: string; sev: number } {
102
+ const rounded = Math.round(value);
103
+ if (value >= t.crit) return { level: "critical", reason: `${name} ${rounded}%`, sev: 2 };
104
+ if (value >= t.warn) return { level: "degraded", reason: `${name} ${rounded}%`, sev: 1 };
105
+ return { level: "healthy", reason: `${name} ${rounded}%`, sev: 0 };
106
+ }
107
+
108
+ // --- UI mapping helpers (color/token names live here so every surface agrees) --
109
+
110
+ /**
111
+ * The token color ramp for a health level. Green/amber/red/grey is the
112
+ * universally-legible traffic model; we reserve it for the HEALTH verdict
113
+ * specifically (per-resource meters keep their own ramp in `machine-metrics`).
114
+ */
115
+ export const HEALTH_TOKEN: Record<HealthLevel, { text: string; dot: string; soft: string }> = {
116
+ healthy: {
117
+ text: "text-og-status-idle",
118
+ dot: "bg-og-status-idle",
119
+ soft: "bg-og-status-idle/10 border-og-status-idle/25",
120
+ },
121
+ degraded: {
122
+ text: "text-og-status-running",
123
+ dot: "bg-og-status-running",
124
+ soft: "bg-og-status-running/10 border-og-status-running/25",
125
+ },
126
+ critical: {
127
+ text: "text-og-status-failed",
128
+ dot: "bg-og-status-failed",
129
+ soft: "bg-og-status-failed/10 border-og-status-failed/30",
130
+ },
131
+ offline: {
132
+ text: "text-og-fg-subtle",
133
+ dot: "bg-og-fg-subtle",
134
+ soft: "bg-og-surface-2 border-og-border",
135
+ },
136
+ unknown: {
137
+ text: "text-og-fg-muted",
138
+ dot: "bg-og-fg-muted",
139
+ soft: "bg-og-surface-2 border-og-border",
140
+ },
141
+ };
142
+
143
+ /** Live/transient levels breathe; settled levels hold still. */
144
+ export function healthPulses(level: HealthLevel): boolean {
145
+ return level === "degraded" || level === "critical";
146
+ }
@@ -0,0 +1,220 @@
1
+ // ----------------------------------------------------------------------------
2
+ // MachineDetail — the marquee per-machine telemetry view.
3
+ //
4
+ // Opens from a machine card. Answers, at a glance and then in depth: is this
5
+ // machine healthy right now (one fused verdict), what are its live numbers, and
6
+ // how have they moved over the chosen window. Built entirely on the existing
7
+ // `og-*` token system — no new visual language, just the missing depth.
8
+ // ----------------------------------------------------------------------------
9
+ import { ArrowLeftIcon, CpuIcon, LaptopIcon, RadioIcon, ServerIcon } from "lucide-react";
10
+ import { cn } from "../../lib/cn";
11
+ import { formatRelativeTime } from "../../lib/format";
12
+ import type { MachineView, MetricSample } from "../../types/machines";
13
+ import { deriveHealth, HEALTH_TOKEN, healthPulses } from "./health";
14
+ import { MetricHistoryChart } from "./metric-history-chart";
15
+ import { MetricSparkline } from "./metric-sparkline";
16
+ import { METRICS, METRIC_WINDOWS, pointsFor, WINDOW_LABEL, type MetricWindow } from "./series";
17
+
18
+ export type MachineDetailProps = {
19
+ machine: MachineView;
20
+ /** Downsampled history for the active window (from `fetchSeries`). */
21
+ series: MetricSample[];
22
+ window: MetricWindow;
23
+ onWindowChange: (w: MetricWindow) => void;
24
+ loadingSeries?: boolean | undefined;
25
+ onBack?: (() => void) | undefined;
26
+ now?: number | undefined;
27
+ className?: string | undefined;
28
+ };
29
+
30
+ function KindIcon({ kind, session }: { kind: string; session: boolean }) {
31
+ if (session) return <ServerIcon className="size-4 text-og-fg-subtle" aria-hidden />;
32
+ if (kind === "modal") return <CpuIcon className="size-4 text-og-fg-subtle" aria-hidden />;
33
+ return <LaptopIcon className="size-4 text-og-fg-subtle" aria-hidden />;
34
+ }
35
+
36
+ export function MachineDetail({
37
+ machine,
38
+ series,
39
+ window,
40
+ onWindowChange,
41
+ loadingSeries,
42
+ onBack,
43
+ now = Date.now(),
44
+ className,
45
+ }: MachineDetailProps) {
46
+ const health = deriveHealth(machine.state, machine.metrics, now);
47
+ const tone = HEALTH_TOKEN[health.level];
48
+ const latest = machine.metrics;
49
+ const sampledAgo = latest ? formatRelativeTime(latest.sampledAt, new Date(now)) : null;
50
+
51
+ // Only render metrics that have a value (GPU is dropped on headless boxes).
52
+ const visible = METRICS.filter((m) => !(m.key === "gpu" && latest?.gpuUtilPct == null));
53
+ // The tile grid tracks the metric count exactly so it never leaves a dead cell.
54
+ const tileCols =
55
+ visible.length >= 5
56
+ ? "grid-cols-2 sm:grid-cols-3 lg:grid-cols-5"
57
+ : "grid-cols-2 sm:grid-cols-4";
58
+
59
+ return (
60
+ <div className={cn("og-root mx-auto flex w-full max-w-5xl flex-col gap-5", className)}>
61
+ {/* ── top bar ─────────────────────────────────────────────────────── */}
62
+ <div className="flex items-center justify-between gap-3">
63
+ <div className="flex min-w-0 items-center gap-2.5">
64
+ {onBack && (
65
+ <button
66
+ type="button"
67
+ onClick={onBack}
68
+ className="flex size-7 shrink-0 items-center justify-center rounded-og-sm border border-og-border bg-og-surface-1 text-og-fg-muted transition-colors hover:bg-og-surface-2 hover:text-og-fg"
69
+ aria-label="Back to machines"
70
+ >
71
+ <ArrowLeftIcon className="size-3.5" aria-hidden />
72
+ </button>
73
+ )}
74
+ <KindIcon kind={machine.kind} session={machine.isSessionGroup} />
75
+ <h1 className="truncate text-og-md font-semibold text-og-fg">{machine.name}</h1>
76
+ <span className="hidden shrink-0 rounded-og-sm border border-og-border bg-og-surface-1 px-1.5 py-0.5 font-og-mono text-og-xs text-og-fg-subtle sm:inline">
77
+ {machine.os} · {machine.arch}
78
+ </span>
79
+ </div>
80
+
81
+ {/* range selector */}
82
+ <div className="flex shrink-0 items-center gap-0.5 rounded-og-md border border-og-border bg-og-surface-1 p-0.5">
83
+ {METRIC_WINDOWS.map((w) => (
84
+ <button
85
+ key={w}
86
+ type="button"
87
+ onClick={() => onWindowChange(w)}
88
+ data-active={w === window}
89
+ className={cn(
90
+ "rounded-og-sm px-2 py-1 font-og-mono text-og-xs transition-colors",
91
+ w === window
92
+ ? "bg-og-surface-3 text-og-fg shadow-og-sm"
93
+ : "text-og-fg-subtle hover:text-og-fg-muted",
94
+ )}
95
+ >
96
+ {w}
97
+ </button>
98
+ ))}
99
+ </div>
100
+ </div>
101
+
102
+ {/* ── health hero ─────────────────────────────────────────────────── */}
103
+ <div
104
+ className={cn(
105
+ "relative flex flex-col gap-4 overflow-hidden rounded-og-lg border p-5 shadow-og-sm",
106
+ tone.soft,
107
+ )}
108
+ >
109
+ <span aria-hidden className={cn("absolute inset-y-0 left-0 w-1", tone.dot)} />
110
+ <div className="flex flex-wrap items-center justify-between gap-3 pl-1">
111
+ <div className="flex items-center gap-3">
112
+ <span className="relative flex size-3">
113
+ {healthPulses(health.level) && (
114
+ <span
115
+ className={cn(
116
+ "absolute inline-flex size-full animate-og-pulse rounded-full opacity-60",
117
+ tone.dot,
118
+ )}
119
+ />
120
+ )}
121
+ <span className={cn("relative inline-flex size-3 rounded-full", tone.dot)} />
122
+ </span>
123
+ <div className="flex flex-col">
124
+ <span className={cn("text-og-md font-semibold", tone.text)}>{health.label}</span>
125
+ <span className="text-og-sm text-og-fg-muted">{health.reason}</span>
126
+ </div>
127
+ </div>
128
+ <div className="flex items-start gap-5 pr-1">
129
+ {sampledAgo && (
130
+ <div className="flex flex-col items-end gap-0.5">
131
+ <span className="text-[10px] font-medium uppercase tracking-wide text-og-fg-subtle">
132
+ Sampled
133
+ </span>
134
+ <span className="flex items-center gap-1 font-og-mono text-og-sm tabular-nums text-og-fg">
135
+ <RadioIcon
136
+ className={cn("size-3", health.stale ? "text-og-fg-subtle" : tone.text)}
137
+ aria-hidden
138
+ />
139
+ {sampledAgo === "now" ? "live" : `${sampledAgo} ago`}
140
+ </span>
141
+ </div>
142
+ )}
143
+ {machine.lastSeenAt && (
144
+ <div className="hidden flex-col items-end gap-0.5 sm:flex">
145
+ <span className="text-[10px] font-medium uppercase tracking-wide text-og-fg-subtle">
146
+ Last seen
147
+ </span>
148
+ <span className="font-og-mono text-og-sm tabular-nums text-og-fg-muted">
149
+ {formatRelativeTime(machine.lastSeenAt, new Date(now))}
150
+ </span>
151
+ </div>
152
+ )}
153
+ </div>
154
+ </div>
155
+
156
+ {/* current-stat tiles */}
157
+ {latest && (
158
+ <div
159
+ className={cn(
160
+ "grid gap-px overflow-hidden rounded-og-md border border-og-border bg-og-border",
161
+ tileCols,
162
+ )}
163
+ >
164
+ {visible.map((m) => {
165
+ const pts = pointsFor(m, series);
166
+ return (
167
+ <div key={m.key} className="flex flex-col gap-1.5 bg-og-surface-1 p-3">
168
+ <span className="text-[10px] font-medium uppercase tracking-wide text-og-fg-subtle">
169
+ {m.title}
170
+ </span>
171
+ <span className="font-og-mono text-og-md font-medium tabular-nums text-og-fg">
172
+ {m.current(latest)}
173
+ </span>
174
+ <MetricSparkline points={pts} color={m.color} yMax={m.yMax} height={22} />
175
+ {m.sub && (
176
+ <span className="truncate font-og-mono text-[10px] text-og-fg-subtle">
177
+ {m.sub(latest)}
178
+ </span>
179
+ )}
180
+ </div>
181
+ );
182
+ })}
183
+ </div>
184
+ )}
185
+ </div>
186
+
187
+ {/* ── history charts ──────────────────────────────────────────────── */}
188
+ <div className="grid grid-cols-1 gap-3 lg:grid-cols-2">
189
+ {visible.map((m) => {
190
+ const pts = pointsFor(m, series);
191
+ return (
192
+ <div
193
+ key={m.key}
194
+ className="flex flex-col gap-3 rounded-og-lg border border-og-border bg-og-surface-1 p-4 shadow-og-sm"
195
+ >
196
+ <div className="flex items-baseline justify-between">
197
+ <span className="text-og-sm font-medium text-og-fg">{m.title}</span>
198
+ {latest && (
199
+ <span className="font-og-mono text-og-sm tabular-nums text-og-fg-muted">
200
+ {m.current(latest)}
201
+ </span>
202
+ )}
203
+ </div>
204
+ <MetricHistoryChart
205
+ points={pts}
206
+ yMax={m.yMax}
207
+ unit={m.unit}
208
+ color={m.color}
209
+ thresholds={m.thresholds}
210
+ rangeLabel={WINDOW_LABEL[window]}
211
+ height={128}
212
+ className={cn(loadingSeries && "opacity-50 transition-opacity")}
213
+ />
214
+ </div>
215
+ );
216
+ })}
217
+ </div>
218
+ </div>
219
+ );
220
+ }