@opengeni/react 0.13.0 → 0.15.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.
- package/README.md +19 -13
- package/dist/chunk-TOJR776I.js +2280 -0
- package/dist/chunk-TOJR776I.js.map +1 -0
- package/dist/index.d.ts +314 -255
- package/dist/index.js +5862 -4404
- package/dist/index.js.map +1 -1
- package/dist/{machines-CnlMb7E-.d.ts → machines-BpdwuQcD.d.ts} +130 -10
- package/dist/machines.d.ts +1 -1
- package/dist/machines.js +23 -1
- package/package.json +5 -2
- package/src/client.ts +10 -1
- package/src/components/chat-composer.tsx +309 -57
- package/src/components/machine-card.tsx +81 -15
- package/src/components/machine-health-pill.tsx +68 -0
- package/src/components/machine-metrics.tsx +10 -24
- package/src/components/machines/health.ts +146 -0
- package/src/components/machines/machine-detail.tsx +220 -0
- package/src/components/machines/metric-history-chart.tsx +298 -0
- package/src/components/machines/metric-sparkline.tsx +76 -0
- package/src/components/machines/series.ts +113 -0
- package/src/components/machines-dashboard.tsx +13 -1
- package/src/components/queue-surface.tsx +578 -0
- package/src/components/sandbox-files.tsx +94 -9
- package/src/components/sandbox-workspace.tsx +186 -52
- package/src/components/session-status.tsx +0 -6
- package/src/components/workbench-changes.tsx +64 -20
- package/src/components/workspace-dock.tsx +146 -55
- package/src/hooks/use-composer.ts +369 -39
- package/src/hooks/use-session-control.ts +6 -7
- package/src/hooks/use-session-events.ts +3 -2
- package/src/hooks/use-session-lineage.ts +15 -6
- package/src/hooks/use-session.ts +10 -2
- package/src/hooks/use-turn-queue.ts +175 -47
- package/src/index.ts +13 -7
- package/src/machines.ts +16 -0
- package/src/provider.tsx +192 -5
- package/src/timeline/parsers.ts +43 -6
- package/src/timeline/projection.ts +24 -2
- package/styles/index.css +22 -0
- package/dist/chunk-NFYVQWIB.js +0 -1377
- package/dist/chunk-NFYVQWIB.js.map +0 -1
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// The card's primary status: the fused HEALTH verdict (dot + label), which
|
|
2
|
+
// subsumes plain connection state (offline/reconnecting fall out of it) and adds
|
|
3
|
+
// resource-pressure + staleness. Capability limitations (consent / no display /
|
|
4
|
+
// enrolling) are orthogonal to reachability, so they ride alongside as their own
|
|
5
|
+
// quiet chip — reusing the existing badge meta so the two surfaces never drift.
|
|
6
|
+
import { cn } from "../lib/cn";
|
|
7
|
+
import type { MachineState, MetricSample } from "../types/machines";
|
|
8
|
+
import { deriveHealth, HEALTH_TOKEN, healthPulses } from "./machines/health";
|
|
9
|
+
import { MACHINE_STATE_BADGE_META } from "./machine-status-pill";
|
|
10
|
+
|
|
11
|
+
export type MachineHealthPillProps = {
|
|
12
|
+
state: MachineState;
|
|
13
|
+
metrics: MetricSample | null;
|
|
14
|
+
now?: number | undefined;
|
|
15
|
+
size?: "sm" | "md" | undefined;
|
|
16
|
+
className?: string | undefined;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export function MachineHealthPill({
|
|
20
|
+
state,
|
|
21
|
+
metrics,
|
|
22
|
+
now,
|
|
23
|
+
size = "sm",
|
|
24
|
+
className,
|
|
25
|
+
}: MachineHealthPillProps) {
|
|
26
|
+
const health = deriveHealth(state, metrics, now ?? Date.now());
|
|
27
|
+
const tone = HEALTH_TOKEN[health.level];
|
|
28
|
+
const badge = MACHINE_STATE_BADGE_META[state];
|
|
29
|
+
const pad = size === "md" ? "px-2.5 py-1 text-og-sm" : "px-2 py-0.5 text-og-xs";
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div className={cn("flex shrink-0 items-center gap-1.5", className)}>
|
|
33
|
+
{badge ? (
|
|
34
|
+
<span
|
|
35
|
+
className={cn(
|
|
36
|
+
"rounded-full border px-2 py-0.5 text-og-xs font-medium",
|
|
37
|
+
badge.badgeClassName,
|
|
38
|
+
)}
|
|
39
|
+
>
|
|
40
|
+
{badge.label}
|
|
41
|
+
</span>
|
|
42
|
+
) : null}
|
|
43
|
+
<span
|
|
44
|
+
className={cn(
|
|
45
|
+
"inline-flex items-center gap-1.5 rounded-full border font-medium",
|
|
46
|
+
pad,
|
|
47
|
+
tone.soft,
|
|
48
|
+
tone.text,
|
|
49
|
+
)}
|
|
50
|
+
data-health={health.level}
|
|
51
|
+
title={health.reason}
|
|
52
|
+
>
|
|
53
|
+
<span className="relative flex size-2">
|
|
54
|
+
{healthPulses(health.level) && (
|
|
55
|
+
<span
|
|
56
|
+
className={cn(
|
|
57
|
+
"absolute inline-flex size-full animate-og-pulse rounded-full opacity-60",
|
|
58
|
+
tone.dot,
|
|
59
|
+
)}
|
|
60
|
+
/>
|
|
61
|
+
)}
|
|
62
|
+
<span className={cn("relative inline-flex size-2 rounded-full", tone.dot)} />
|
|
63
|
+
</span>
|
|
64
|
+
{health.label}
|
|
65
|
+
</span>
|
|
66
|
+
</div>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
@@ -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-
|
|
36
|
-
: "bg-og-status-
|
|
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.
|
|
74
|
-
*
|
|
75
|
-
*
|
|
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=
|
|
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
|
+
}
|