@opengeni/react 1.0.1 → 2.3.0-canary.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 +33 -10
- package/dist/artifacts-spreadsheet.js +1 -1
- package/dist/artifacts.js +1 -1
- package/dist/browser-viewer-GXUICIAG.js +14 -0
- package/dist/{chunk-R3AZOAB5.js → chunk-4MDLEHXL.js} +9 -115
- package/dist/chunk-4MDLEHXL.js.map +1 -0
- package/dist/chunk-5YK2ZDIG.js +116 -0
- package/dist/chunk-5YK2ZDIG.js.map +1 -0
- package/dist/chunk-H5YGYNXS.js +329 -0
- package/dist/chunk-H5YGYNXS.js.map +1 -0
- package/dist/{chunk-7WCLAEFV.js → chunk-ICA6R6TW.js} +2 -2
- package/dist/{chunk-7WCLAEFV.js.map → chunk-ICA6R6TW.js.map} +1 -1
- package/dist/{chunk-IGC6A2ZI.js → chunk-JOAVA2TW.js} +589 -217
- package/dist/chunk-JOAVA2TW.js.map +1 -0
- package/dist/{chunk-Y7OEDUJ5.js → chunk-LS4POUTY.js} +1057 -426
- package/dist/chunk-LS4POUTY.js.map +1 -0
- package/dist/{chunk-5UQPNLIO.js → chunk-LTCHJ4MS.js} +9 -9
- package/dist/chunk-LTCHJ4MS.js.map +1 -0
- package/dist/{chunk-C5ZPSSS5.js → chunk-NEXLLTP4.js} +2 -1
- package/dist/{chunk-C5ZPSSS5.js.map → chunk-NEXLLTP4.js.map} +1 -1
- package/dist/chunk-QGBJ6GHG.js +2300 -0
- package/dist/chunk-QGBJ6GHG.js.map +1 -0
- package/dist/{chunk-6KLX2RH5.js → chunk-RSXJN73N.js} +365 -21
- package/dist/chunk-RSXJN73N.js.map +1 -0
- package/dist/{chunk-B6WPZSTT.js → chunk-SE4W4WQ3.js} +78 -16
- package/dist/chunk-SE4W4WQ3.js.map +1 -0
- package/dist/{chunk-MKPIAGVL.js → chunk-W6NFJXSA.js} +8 -19
- package/dist/chunk-W6NFJXSA.js.map +1 -0
- package/dist/{chunk-XBZWOM2K.js → chunk-X4A66EWZ.js} +8 -3
- package/dist/chunk-X4A66EWZ.js.map +1 -0
- package/dist/chunk-X4JAFD5R.js +3907 -0
- package/dist/chunk-X4JAFD5R.js.map +1 -0
- package/dist/{chunk-HA46CYQK.js → chunk-XXWH2JLC.js} +792 -206
- package/dist/chunk-XXWH2JLC.js.map +1 -0
- package/dist/client.d.ts +2 -2
- package/dist/components/browser-viewer.d.ts +7 -1
- package/dist/components/computer-viewer.d.ts +4 -1
- package/dist/components/enrollment-consent.d.ts +6 -3
- package/dist/components/file-browser.d.ts +7 -1
- package/dist/components/human-input-form.d.ts +1 -1
- package/dist/components/human-input-surface.d.ts +1 -1
- package/dist/components/machine-card.d.ts +4 -1
- package/dist/components/machines/machine-detail.d.ts +4 -1
- package/dist/components/machines/machine-operation-policy.d.ts +11 -0
- package/dist/components/machines-dashboard.d.ts +3 -1
- package/dist/components/markdown.d.ts +20 -1
- package/dist/components/message-timeline.d.ts +1 -1
- package/dist/components/model-policy-picker.d.ts +2 -0
- package/dist/components/pierre-file.d.ts +1 -1
- package/dist/components/sandbox-files.d.ts +8 -2
- package/dist/components/sandbox-workspace.d.ts +33 -3
- package/dist/components/session-chrome.d.ts +15 -3
- package/dist/components/workspace-dock.d.ts +6 -2
- package/dist/composer.js +5 -5
- package/dist/computer-viewer-ZOP4Q7A7.js +16 -0
- package/dist/computer-viewer-ZOP4Q7A7.js.map +1 -0
- package/dist/hooks/channel-cache.d.ts +23 -0
- package/dist/hooks/shared-machines-list.d.ts +14 -0
- package/dist/hooks/use-attached-browsers.d.ts +2 -1
- package/dist/hooks/use-channels.d.ts +1 -0
- package/dist/hooks/use-composer.d.ts +37 -9
- package/dist/hooks/use-machine-chip.d.ts +3 -3
- package/dist/hooks/use-machines.d.ts +19 -1
- package/dist/hooks/use-sandbox-files.d.ts +1 -0
- package/dist/hooks/use-session-background-commands.d.ts +14 -0
- package/dist/hooks/use-turn-queue.d.ts +12 -0
- package/dist/hooks/use-workspace-sessions.d.ts +1 -0
- package/dist/index.d.ts +7 -5
- package/dist/index.js +1096 -623
- package/dist/index.js.map +1 -1
- package/dist/interaction.js +29 -181
- package/dist/interaction.js.map +1 -1
- package/dist/lib/interaction-errors.d.ts +3 -0
- package/dist/machines.d.ts +4 -2
- package/dist/machines.js +10 -2
- package/dist/model-policy.d.ts +1 -1
- package/dist/model-policy.js +1 -1
- package/dist/platform-activity-row-BJHUHXAT.js +169 -0
- package/dist/platform-activity-row-BJHUHXAT.js.map +1 -0
- package/dist/{queue-surface-implementation-2KVJTYXH.js → queue-surface-implementation-BSKUWHRI.js} +76 -16
- package/dist/queue-surface-implementation-BSKUWHRI.js.map +1 -0
- package/dist/realtime/realtime-control.d.ts +6 -3
- package/dist/realtime.js +36 -14
- package/dist/realtime.js.map +1 -1
- package/dist/session-ui.d.ts +1 -1
- package/dist/session-ui.js +6 -2
- package/dist/session.d.ts +2 -2
- package/dist/session.js +4 -4
- package/dist/timeline/index.d.ts +1 -1
- package/dist/timeline/platform-activity-row.d.ts +15 -0
- package/dist/timeline/screenshot-lightbox.d.ts +1 -1
- package/dist/timeline/shared.d.ts +4 -2
- package/dist/timeline/types.d.ts +50 -5
- package/dist/types/machines.d.ts +2 -2
- package/package.json +3 -3
- package/src/client.ts +12 -1
- package/src/components/artifacts/editable-spreadsheet.tsx +1 -1
- package/src/components/browser-viewer.tsx +361 -58
- package/src/components/chat-composer.tsx +1 -1
- package/src/components/composer.tsx +6 -6
- package/src/components/computer-viewer.tsx +368 -115
- package/src/components/enrollment-consent.tsx +64 -6
- package/src/components/file-browser.tsx +117 -2
- package/src/components/human-input-form.tsx +103 -59
- package/src/components/human-input-surface.tsx +1 -1
- package/src/components/machine-card.tsx +111 -0
- package/src/components/machine-input-display.ts +15 -0
- package/src/components/machines/machine-detail.tsx +47 -0
- package/src/components/machines/machine-operation-policy.tsx +179 -0
- package/src/components/machines-dashboard.tsx +6 -0
- package/src/components/markdown.tsx +174 -20
- package/src/components/message-timeline.tsx +215 -18
- package/src/components/model-policy-picker.tsx +9 -17
- package/src/components/pierre-file.tsx +1 -1
- package/src/components/queue-surface-implementation.tsx +82 -16
- package/src/components/sandbox-files.tsx +135 -19
- package/src/components/sandbox-workspace.tsx +204 -92
- package/src/components/session-chrome.tsx +415 -208
- package/src/components/session-status.tsx +1 -1
- package/src/components/user-message-body.tsx +168 -48
- package/src/components/workspace-dock.tsx +141 -175
- package/src/hooks/channel-cache.ts +104 -0
- package/src/hooks/shared-machines-list.ts +239 -0
- package/src/hooks/use-attached-browsers.ts +5 -1
- package/src/hooks/use-browser-frame-stream.ts +6 -1
- package/src/hooks/use-browser-session.ts +6 -4
- package/src/hooks/use-browser-sessions.ts +7 -1
- package/src/hooks/use-channels.ts +32 -10
- package/src/hooks/use-composer.ts +760 -291
- package/src/hooks/use-computer-frame-stream.ts +14 -9
- package/src/hooks/use-computer-session.ts +16 -14
- package/src/hooks/use-computer-sessions.ts +8 -1
- package/src/hooks/use-last-started-turn-policy.ts +1 -1
- package/src/hooks/use-machine-chip.ts +23 -6
- package/src/hooks/use-machines.ts +104 -6
- package/src/hooks/use-sandbox-files.ts +195 -140
- package/src/hooks/use-sandbox-git.ts +58 -29
- package/src/hooks/use-session-background-commands.ts +56 -0
- package/src/hooks/use-session-capabilities.ts +18 -0
- package/src/hooks/use-turn-queue.ts +111 -12
- package/src/hooks/use-workspace-sessions.ts +5 -1
- package/src/index.ts +26 -4
- package/src/lib/interaction-errors.ts +18 -0
- package/src/machines.ts +9 -1
- package/src/model-policy.ts +10 -2
- package/src/realtime/realtime-control.tsx +53 -17
- package/src/session-context.ts +1 -0
- package/src/session-ui.ts +6 -1
- package/src/session.ts +9 -1
- package/src/timeline/activity-rail.tsx +20 -75
- package/src/timeline/index.ts +4 -0
- package/src/timeline/platform-activity-row.tsx +201 -0
- package/src/timeline/projection.ts +529 -25
- package/src/timeline/screenshot-lightbox.tsx +48 -14
- package/src/timeline/shared.tsx +8 -2
- package/src/timeline/tool-renderers.tsx +104 -6
- package/src/timeline/turn-summary.tsx +6 -1
- package/src/timeline/types.ts +77 -2
- package/src/types/machines.ts +10 -0
- package/styles/compiled.css +115 -104
- package/dist/chunk-5UQPNLIO.js.map +0 -1
- package/dist/chunk-6KLX2RH5.js.map +0 -1
- package/dist/chunk-B6WPZSTT.js.map +0 -1
- package/dist/chunk-HA46CYQK.js.map +0 -1
- package/dist/chunk-IGC6A2ZI.js.map +0 -1
- package/dist/chunk-LLOLUHVG.js +0 -5937
- package/dist/chunk-LLOLUHVG.js.map +0 -1
- package/dist/chunk-MKPIAGVL.js.map +0 -1
- package/dist/chunk-R3AZOAB5.js.map +0 -1
- package/dist/chunk-XBZWOM2K.js.map +0 -1
- package/dist/chunk-Y7OEDUJ5.js.map +0 -1
- package/dist/interactive-workbench-viewers-TOHBXJEE.js +0 -15
- package/dist/queue-surface-implementation-2KVJTYXH.js.map +0 -1
- /package/dist/{interactive-workbench-viewers-TOHBXJEE.js.map → browser-viewer-GXUICIAG.js.map} +0 -0
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
|
+
AlertTriangleIcon,
|
|
2
3
|
ArrowRightIcon,
|
|
3
4
|
CpuIcon,
|
|
5
|
+
CircleCheckIcon,
|
|
6
|
+
DownloadIcon,
|
|
4
7
|
LaptopIcon,
|
|
5
8
|
MonitorIcon,
|
|
6
9
|
MonitorOffIcon,
|
|
10
|
+
LoaderCircleIcon,
|
|
7
11
|
ServerIcon,
|
|
8
12
|
UsersIcon,
|
|
9
13
|
} from "lucide-react";
|
|
@@ -20,6 +24,9 @@ export type MachineCardProps = {
|
|
|
20
24
|
onAttach?: ((machine: MachineView) => void) | undefined;
|
|
21
25
|
/** Whether an attach/swap is currently in flight (disables the affordance). */
|
|
22
26
|
attaching?: boolean | undefined;
|
|
27
|
+
/** Start/retry the exact signed Connected Machine agent update. */
|
|
28
|
+
onUpdateAgent?: ((machine: MachineView) => void) | undefined;
|
|
29
|
+
updatingAgent?: boolean | undefined;
|
|
23
30
|
/** Short recent history (e.g. 15m) — drives the card's CPU trend + preview. */
|
|
24
31
|
series?: MetricSample[] | undefined;
|
|
25
32
|
/** Open the full per-machine telemetry detail. Makes the whole card actionable. */
|
|
@@ -45,6 +52,8 @@ export function MachineCard({
|
|
|
45
52
|
machine,
|
|
46
53
|
onAttach,
|
|
47
54
|
attaching,
|
|
55
|
+
onUpdateAgent,
|
|
56
|
+
updatingAgent,
|
|
48
57
|
series,
|
|
49
58
|
onOpenDetail,
|
|
50
59
|
now,
|
|
@@ -59,6 +68,43 @@ export function MachineCard({
|
|
|
59
68
|
: null;
|
|
60
69
|
const cpuPts = (series ?? []).map((s) => ({ t: new Date(s.sampledAt).getTime(), v: s.cpuPct }));
|
|
61
70
|
const openable = Boolean(onOpenDetail);
|
|
71
|
+
const runtime = machine.runtime;
|
|
72
|
+
const updateRunning = runtime?.versionState === "updating";
|
|
73
|
+
const dispatchConfirmationStalled =
|
|
74
|
+
runtime?.update?.status === "requested" &&
|
|
75
|
+
clockNow - new Date(runtime.update.updatedAt).getTime() >= 30_000;
|
|
76
|
+
const canUpdate =
|
|
77
|
+
Boolean(machine.enrollmentId && onUpdateAgent) &&
|
|
78
|
+
!offline &&
|
|
79
|
+
(runtime?.versionState === "outdated" ||
|
|
80
|
+
runtime?.versionState === "update_failed" ||
|
|
81
|
+
dispatchConfirmationStalled);
|
|
82
|
+
const updateLabel = (() => {
|
|
83
|
+
const update = runtime?.update;
|
|
84
|
+
if (!update) return null;
|
|
85
|
+
switch (update.status) {
|
|
86
|
+
case "requested":
|
|
87
|
+
return dispatchConfirmationStalled ? "Agent confirmation delayed" : "Confirming with agent";
|
|
88
|
+
case "accepted":
|
|
89
|
+
return "Update accepted";
|
|
90
|
+
case "waiting_for_idle":
|
|
91
|
+
return "Waiting for current work";
|
|
92
|
+
case "downloading":
|
|
93
|
+
case "verifying":
|
|
94
|
+
case "applying":
|
|
95
|
+
return `${update.status}…`;
|
|
96
|
+
case "restarting":
|
|
97
|
+
return "Restarting agent";
|
|
98
|
+
case "succeeded":
|
|
99
|
+
return `Updated · v${update.targetVersion}`;
|
|
100
|
+
case "failed": {
|
|
101
|
+
const detail = update.errorCode?.replaceAll("_", " ");
|
|
102
|
+
return update.rolledBack
|
|
103
|
+
? `Update rolled back safely${detail ? ` · ${detail}` : ""}`
|
|
104
|
+
: `Update failed${detail ? ` · ${detail}` : ""}`;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
})();
|
|
62
108
|
|
|
63
109
|
return (
|
|
64
110
|
<div
|
|
@@ -108,6 +154,10 @@ export function MachineCard({
|
|
|
108
154
|
) : null}
|
|
109
155
|
</div>
|
|
110
156
|
<div className="mt-0.5 flex flex-wrap items-center gap-x-2 gap-y-0.5 text-og-xs text-og-fg-subtle">
|
|
157
|
+
<span data-machine-scope={machine.scope} className="capitalize">
|
|
158
|
+
{machine.scope === "user" ? "personal" : machine.scope}
|
|
159
|
+
</span>
|
|
160
|
+
<span aria-hidden>·</span>
|
|
111
161
|
<span className="capitalize">
|
|
112
162
|
{machine.isSessionGroup ? "session sandbox" : machine.kind}
|
|
113
163
|
</span>
|
|
@@ -146,6 +196,67 @@ export function MachineCard({
|
|
|
146
196
|
</p>
|
|
147
197
|
) : null}
|
|
148
198
|
|
|
199
|
+
{machine.connectionAuthority.duplicateRunnerDeniedCount > 0 ? (
|
|
200
|
+
<p
|
|
201
|
+
data-runner-conflict
|
|
202
|
+
className="flex items-center gap-1.5 rounded-og-md border border-og-status-failed/30 bg-og-status-failed/5 px-2.5 py-1.5 text-og-xs text-og-status-failed"
|
|
203
|
+
>
|
|
204
|
+
<AlertTriangleIcon className="size-3.5 shrink-0" aria-hidden />
|
|
205
|
+
Competing agent process blocked. Open diagnostics for details.
|
|
206
|
+
</p>
|
|
207
|
+
) : null}
|
|
208
|
+
|
|
209
|
+
{runtime ? (
|
|
210
|
+
<div
|
|
211
|
+
data-agent-runtime
|
|
212
|
+
className="flex items-center justify-between gap-3 rounded-og-md border border-og-border/70 bg-og-surface-2/35 px-2.5 py-2 text-og-xs"
|
|
213
|
+
>
|
|
214
|
+
<div className="min-w-0">
|
|
215
|
+
<div className="flex items-center gap-1.5 text-og-fg-muted">
|
|
216
|
+
{runtime.versionState === "current" ? (
|
|
217
|
+
<CircleCheckIcon className="size-3.5 text-og-status-idle" aria-hidden />
|
|
218
|
+
) : updateRunning ? (
|
|
219
|
+
<LoaderCircleIcon className="size-3.5 animate-og-spin text-og-accent" aria-hidden />
|
|
220
|
+
) : (
|
|
221
|
+
<DownloadIcon className="size-3.5" aria-hidden />
|
|
222
|
+
)}
|
|
223
|
+
<span className="truncate">
|
|
224
|
+
Agent{" "}
|
|
225
|
+
{runtime.installedVersion ? `v${runtime.installedVersion}` : "version unknown"}
|
|
226
|
+
</span>
|
|
227
|
+
</div>
|
|
228
|
+
<p className="mt-0.5 truncate text-og-fg-subtle">
|
|
229
|
+
{updateLabel ??
|
|
230
|
+
(runtime.versionState === "current"
|
|
231
|
+
? `Current · ${runtime.updateChannel ?? "channel unknown"}`
|
|
232
|
+
: runtime.desiredVersion
|
|
233
|
+
? `Promoted v${runtime.desiredVersion}`
|
|
234
|
+
: "No promoted version configured")}
|
|
235
|
+
</p>
|
|
236
|
+
</div>
|
|
237
|
+
{canUpdate ? (
|
|
238
|
+
<button
|
|
239
|
+
type="button"
|
|
240
|
+
data-update-agent
|
|
241
|
+
disabled={updatingAgent}
|
|
242
|
+
onClick={(event) => {
|
|
243
|
+
event.stopPropagation();
|
|
244
|
+
onUpdateAgent?.(machine);
|
|
245
|
+
}}
|
|
246
|
+
className="shrink-0 rounded-og-sm border border-og-border px-2 py-1 font-medium text-og-fg-muted transition-colors hover:border-og-border-strong hover:text-og-fg disabled:cursor-not-allowed disabled:opacity-50 pointer-coarse:min-h-11"
|
|
247
|
+
>
|
|
248
|
+
{updatingAgent
|
|
249
|
+
? "Starting…"
|
|
250
|
+
: dispatchConfirmationStalled
|
|
251
|
+
? "Retry delivery"
|
|
252
|
+
: runtime.versionState === "update_failed"
|
|
253
|
+
? "Retry"
|
|
254
|
+
: "Update"}
|
|
255
|
+
</button>
|
|
256
|
+
) : null}
|
|
257
|
+
</div>
|
|
258
|
+
) : null}
|
|
259
|
+
|
|
149
260
|
<MachineMetrics metrics={machine.metrics} />
|
|
150
261
|
|
|
151
262
|
{/* CPU trend — previews the history and invites opening the detail. */}
|
|
@@ -7,6 +7,11 @@ export const MACHINE_INPUT_META: Record<MachineInputMember["kind"], string> = {
|
|
|
7
7
|
agent_steer_instruction: "Agent direction",
|
|
8
8
|
child_terminal_result: "Agent finished",
|
|
9
9
|
media_generation_result: "Video update",
|
|
10
|
+
child_requires_action: "Agent needs input",
|
|
11
|
+
child_requires_action_resolved: "Agent unblocked",
|
|
12
|
+
child_paused: "Agent paused",
|
|
13
|
+
child_waiting_capacity: "Agent waiting for capacity",
|
|
14
|
+
child_progress: "Agent progress",
|
|
10
15
|
};
|
|
11
16
|
|
|
12
17
|
/**
|
|
@@ -35,6 +40,16 @@ export function machineInputBatchLabel(members: readonly MachineInputMember[]):
|
|
|
35
40
|
return n === 1 ? "Agent direction" : `${n} agent directions`;
|
|
36
41
|
case "media_generation_result":
|
|
37
42
|
return n === 1 ? "Video ready" : `${n} video updates`;
|
|
43
|
+
case "child_requires_action":
|
|
44
|
+
return n === 1 ? "Agent needs input" : `${n} agents need input`;
|
|
45
|
+
case "child_requires_action_resolved":
|
|
46
|
+
return n === 1 ? "Agent unblocked" : `${n} agents unblocked`;
|
|
47
|
+
case "child_paused":
|
|
48
|
+
return n === 1 ? "Agent paused" : `${n} agents paused`;
|
|
49
|
+
case "child_waiting_capacity":
|
|
50
|
+
return n === 1 ? "Agent waiting for capacity" : `${n} agents waiting for capacity`;
|
|
51
|
+
case "child_progress":
|
|
52
|
+
return n === 1 ? "Agent progress" : `${n} agent progress notes`;
|
|
38
53
|
}
|
|
39
54
|
}
|
|
40
55
|
const parts = [...counts.entries()].map(([kind, count]) => {
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
// `og-*` token system — no new visual language, just the missing depth.
|
|
8
8
|
// ----------------------------------------------------------------------------
|
|
9
9
|
import {
|
|
10
|
+
AlertTriangleIcon,
|
|
10
11
|
ArrowLeftIcon,
|
|
11
12
|
CpuIcon,
|
|
12
13
|
LaptopIcon,
|
|
@@ -21,6 +22,8 @@ import { deriveHealth, HEALTH_TOKEN, healthPulses } from "./health";
|
|
|
21
22
|
import { MetricHistoryChart } from "./metric-history-chart";
|
|
22
23
|
import { MetricSparkline } from "./metric-sparkline";
|
|
23
24
|
import { METRICS, METRIC_WINDOWS, pointsFor, WINDOW_LABEL, type MetricWindow } from "./series";
|
|
25
|
+
import { MachineOperationPolicyEditor } from "./machine-operation-policy";
|
|
26
|
+
import type { UpdateMachineOperationPolicyRequest } from "@opengeni/sdk";
|
|
24
27
|
|
|
25
28
|
export type MachineDetailProps = {
|
|
26
29
|
machine: MachineView;
|
|
@@ -32,6 +35,10 @@ export type MachineDetailProps = {
|
|
|
32
35
|
onBack?: (() => void) | undefined;
|
|
33
36
|
/** Open the workspace-admin removal confirmation for a self-hosted machine. */
|
|
34
37
|
onRemove?: ((machine: MachineView) => void) | undefined;
|
|
38
|
+
onUpdateOperationPolicy?:
|
|
39
|
+
| ((machine: MachineView, request: UpdateMachineOperationPolicyRequest) => Promise<unknown>)
|
|
40
|
+
| undefined;
|
|
41
|
+
updatingOperationPolicy?: boolean | undefined;
|
|
35
42
|
now?: number | undefined;
|
|
36
43
|
className?: string | undefined;
|
|
37
44
|
};
|
|
@@ -50,6 +57,8 @@ export function MachineDetail({
|
|
|
50
57
|
loadingSeries,
|
|
51
58
|
onBack,
|
|
52
59
|
onRemove,
|
|
60
|
+
onUpdateOperationPolicy,
|
|
61
|
+
updatingOperationPolicy,
|
|
53
62
|
now = Date.now(),
|
|
54
63
|
className,
|
|
55
64
|
}: MachineDetailProps) {
|
|
@@ -57,6 +66,10 @@ export function MachineDetail({
|
|
|
57
66
|
const tone = HEALTH_TOKEN[health.level];
|
|
58
67
|
const latest = machine.metrics;
|
|
59
68
|
const sampledAgo = latest ? formatRelativeTime(latest.sampledAt, new Date(now)) : null;
|
|
69
|
+
const authority = machine.connectionAuthority;
|
|
70
|
+
const duplicateDeniedAgo = authority.duplicateRunnerDeniedAt
|
|
71
|
+
? formatRelativeTime(authority.duplicateRunnerDeniedAt, new Date(now))
|
|
72
|
+
: null;
|
|
60
73
|
|
|
61
74
|
// Only render metrics that have a value (GPU is dropped on headless boxes).
|
|
62
75
|
const visible = METRICS.filter((m) => !(m.key === "gpu" && latest?.gpuUtilPct == null));
|
|
@@ -206,8 +219,42 @@ export function MachineDetail({
|
|
|
206
219
|
})}
|
|
207
220
|
</div>
|
|
208
221
|
)}
|
|
222
|
+
|
|
223
|
+
{authority.state !== "not_applicable" ? (
|
|
224
|
+
<div
|
|
225
|
+
data-connection-authority={authority.state}
|
|
226
|
+
className="flex flex-wrap items-center justify-between gap-2 rounded-og-md border border-og-border bg-og-surface-1/75 px-3 py-2 text-og-xs"
|
|
227
|
+
>
|
|
228
|
+
<span className="text-og-fg-muted">
|
|
229
|
+
Runner {authority.state.replace("_", " ")} · generation {authority.generation}
|
|
230
|
+
{authority.supersededCount > 0
|
|
231
|
+
? ` · ${authority.supersededCount} earlier ${authority.supersededCount === 1 ? "runner" : "runners"} fenced`
|
|
232
|
+
: ""}
|
|
233
|
+
</span>
|
|
234
|
+
{authority.duplicateRunnerDeniedCount > 0 ? (
|
|
235
|
+
<span className="inline-flex items-center gap-1.5 text-og-status-failed">
|
|
236
|
+
<AlertTriangleIcon className="size-3.5" aria-hidden />
|
|
237
|
+
Blocked {authority.duplicateRunnerDeniedCount} competing runner
|
|
238
|
+
{authority.duplicateRunnerDeniedCount === 1 ? "" : "s"}
|
|
239
|
+
{duplicateDeniedAgo
|
|
240
|
+
? ` · ${duplicateDeniedAgo === "now" ? "now" : `${duplicateDeniedAgo} ago`}`
|
|
241
|
+
: ""}
|
|
242
|
+
</span>
|
|
243
|
+
) : null}
|
|
244
|
+
</div>
|
|
245
|
+
) : null}
|
|
209
246
|
</div>
|
|
210
247
|
|
|
248
|
+
{onUpdateOperationPolicy && machine.enrollmentId && machine.operationPolicy ? (
|
|
249
|
+
<MachineOperationPolicyEditor
|
|
250
|
+
policy={machine.operationPolicy}
|
|
251
|
+
memorySupported={machine.runtime?.capabilities.operationResourcePolicy === true}
|
|
252
|
+
cpuSupported={machine.runtime?.capabilities.operationCpuQuota === true}
|
|
253
|
+
saving={updatingOperationPolicy}
|
|
254
|
+
onSave={(request) => onUpdateOperationPolicy(machine, request)}
|
|
255
|
+
/>
|
|
256
|
+
) : null}
|
|
257
|
+
|
|
211
258
|
{/* ── history charts ──────────────────────────────────────────────── */}
|
|
212
259
|
<div className="grid grid-cols-1 gap-3 lg:grid-cols-2">
|
|
213
260
|
{visible.map((m) => {
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { AlertTriangleIcon, Loader2Icon } from "lucide-react";
|
|
2
|
+
import { useEffect, useState, type FormEvent } from "react";
|
|
3
|
+
import type { MachineOperationPolicy, UpdateMachineOperationPolicyRequest } from "@opengeni/sdk";
|
|
4
|
+
|
|
5
|
+
export type MachineOperationPolicyEditorProps = {
|
|
6
|
+
policy: MachineOperationPolicy;
|
|
7
|
+
memorySupported: boolean;
|
|
8
|
+
cpuSupported: boolean;
|
|
9
|
+
saving?: boolean | undefined;
|
|
10
|
+
onSave: (request: UpdateMachineOperationPolicyRequest) => Promise<unknown> | unknown;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
function fieldValue(value: number | null): string {
|
|
14
|
+
return value === null ? "" : String(value);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function parseByteCount(label: string, value: string): number | null {
|
|
18
|
+
const trimmed = value.trim();
|
|
19
|
+
if (!trimmed) return null;
|
|
20
|
+
if (!/^\d+$/.test(trimmed)) {
|
|
21
|
+
throw new Error(`${label} must be a whole number of bytes or blank`);
|
|
22
|
+
}
|
|
23
|
+
const parsed = Number(trimmed);
|
|
24
|
+
if (!Number.isSafeInteger(parsed) || parsed <= 0) {
|
|
25
|
+
throw new Error(`${label} must be a positive safe-integer byte count or blank`);
|
|
26
|
+
}
|
|
27
|
+
return parsed;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function parseCpuMillicores(value: string): number | null {
|
|
31
|
+
const trimmed = value.trim();
|
|
32
|
+
if (!trimmed) return null;
|
|
33
|
+
if (!/^\d+$/.test(trimmed)) {
|
|
34
|
+
throw new Error("CPU maximum must be a whole number of millicores or blank");
|
|
35
|
+
}
|
|
36
|
+
const parsed = Number(trimmed);
|
|
37
|
+
if (!Number.isSafeInteger(parsed) || parsed <= 0 || parsed > 0xffff_ffff) {
|
|
38
|
+
throw new Error("CPU maximum must be between 1 and 4294967295 millicores or blank");
|
|
39
|
+
}
|
|
40
|
+
return parsed;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Exact editor for the optional per-enrollment command resource policy. Blank
|
|
44
|
+
* values remain genuinely unrestricted; CPU is a period-independent ratio. */
|
|
45
|
+
export function MachineOperationPolicyEditor({
|
|
46
|
+
policy,
|
|
47
|
+
memorySupported,
|
|
48
|
+
cpuSupported,
|
|
49
|
+
saving = false,
|
|
50
|
+
onSave,
|
|
51
|
+
}: MachineOperationPolicyEditorProps) {
|
|
52
|
+
const [memoryMaxBytes, setMemoryMaxBytes] = useState(fieldValue(policy.memoryMaxBytes));
|
|
53
|
+
const [memoryHighBytes, setMemoryHighBytes] = useState(fieldValue(policy.memoryHighBytes));
|
|
54
|
+
const [cpuMaxMillicores, setCpuMaxMillicores] = useState(fieldValue(policy.cpuMaxMillicores));
|
|
55
|
+
const [validationError, setValidationError] = useState<string | null>(null);
|
|
56
|
+
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
setMemoryMaxBytes(fieldValue(policy.memoryMaxBytes));
|
|
59
|
+
setMemoryHighBytes(fieldValue(policy.memoryHighBytes));
|
|
60
|
+
setCpuMaxMillicores(fieldValue(policy.cpuMaxMillicores));
|
|
61
|
+
setValidationError(null);
|
|
62
|
+
}, [policy.memoryMaxBytes, policy.memoryHighBytes, policy.cpuMaxMillicores, policy.revision]);
|
|
63
|
+
|
|
64
|
+
const submit = async (event: FormEvent<HTMLFormElement>) => {
|
|
65
|
+
event.preventDefault();
|
|
66
|
+
try {
|
|
67
|
+
const max = parseByteCount("Hard ceiling", memoryMaxBytes);
|
|
68
|
+
const high = parseByteCount("Reclaim threshold", memoryHighBytes);
|
|
69
|
+
if (max !== null && high !== null && high > max) {
|
|
70
|
+
throw new Error("Reclaim threshold cannot exceed the hard ceiling");
|
|
71
|
+
}
|
|
72
|
+
const cpuMax = parseCpuMillicores(cpuMaxMillicores);
|
|
73
|
+
setValidationError(null);
|
|
74
|
+
await onSave({
|
|
75
|
+
memoryMaxBytes: max,
|
|
76
|
+
memoryHighBytes: high,
|
|
77
|
+
cpuMaxMillicores: cpuMax,
|
|
78
|
+
expectedRevision: policy.revision,
|
|
79
|
+
});
|
|
80
|
+
} catch (error) {
|
|
81
|
+
setValidationError(error instanceof Error ? error.message : String(error));
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const configured =
|
|
86
|
+
policy.memoryMaxBytes !== null ||
|
|
87
|
+
policy.memoryHighBytes !== null ||
|
|
88
|
+
policy.cpuMaxMillicores !== null;
|
|
89
|
+
const memoryConfigured = policy.memoryMaxBytes !== null || policy.memoryHighBytes !== null;
|
|
90
|
+
const unsupportedMemory = memoryConfigured && !memorySupported;
|
|
91
|
+
const unsupportedCpu = policy.cpuMaxMillicores !== null && (!memorySupported || !cpuSupported);
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<section
|
|
95
|
+
data-machine-operation-policy
|
|
96
|
+
className="rounded-og-lg border border-og-border bg-og-surface-1 p-4 shadow-og-sm"
|
|
97
|
+
>
|
|
98
|
+
<div className="flex flex-col gap-1">
|
|
99
|
+
<h2 className="text-og-sm font-semibold text-og-fg">Per-command resource policy</h2>
|
|
100
|
+
<p className="text-og-xs leading-relaxed text-og-fg-muted">
|
|
101
|
+
Optional limits applied separately to each newly admitted exec and Git command. Running
|
|
102
|
+
commands keep their admitted policy. Blank means unrestricted. The runner or host may
|
|
103
|
+
apply a stricter explicit policy; browser, computer-use, and terminal capabilities remain
|
|
104
|
+
unchanged.
|
|
105
|
+
</p>
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
{configured && (unsupportedMemory || unsupportedCpu) ? (
|
|
109
|
+
<div className="mt-3 flex gap-2 rounded-og-md border border-og-status-failed/40 bg-og-status-failed/5 p-3 text-og-xs text-og-status-failed">
|
|
110
|
+
<AlertTriangleIcon className="mt-0.5 size-3.5 shrink-0" aria-hidden />
|
|
111
|
+
<span>
|
|
112
|
+
{unsupportedCpu
|
|
113
|
+
? "This runner cannot enforce the configured CPU limit."
|
|
114
|
+
: "This runner cannot enforce the configured memory limit."}{" "}
|
|
115
|
+
Command execution fails closed until the current runner, OS, and service configuration
|
|
116
|
+
can enforce it, or the unsupported limit is cleared. You can still save limits now for
|
|
117
|
+
later enforcement.
|
|
118
|
+
</span>
|
|
119
|
+
</div>
|
|
120
|
+
) : null}
|
|
121
|
+
|
|
122
|
+
<form className="mt-4 grid gap-3 sm:grid-cols-2" onSubmit={submit}>
|
|
123
|
+
<label className="flex flex-col gap-1.5 text-og-xs text-og-fg-muted">
|
|
124
|
+
Hard ceiling (bytes)
|
|
125
|
+
<input
|
|
126
|
+
inputMode="numeric"
|
|
127
|
+
pattern="[0-9]*"
|
|
128
|
+
value={memoryMaxBytes}
|
|
129
|
+
onChange={(event) => setMemoryMaxBytes(event.target.value)}
|
|
130
|
+
placeholder="Unlimited"
|
|
131
|
+
disabled={saving}
|
|
132
|
+
className="min-h-9 rounded-og-sm border border-og-border bg-og-bg px-2.5 font-og-mono text-og-sm text-og-fg outline-hidden focus-visible:ring-2 focus-visible:ring-og-accent/50 disabled:opacity-60"
|
|
133
|
+
/>
|
|
134
|
+
</label>
|
|
135
|
+
<label className="flex flex-col gap-1.5 text-og-xs text-og-fg-muted">
|
|
136
|
+
Reclaim threshold (bytes)
|
|
137
|
+
<input
|
|
138
|
+
inputMode="numeric"
|
|
139
|
+
pattern="[0-9]*"
|
|
140
|
+
value={memoryHighBytes}
|
|
141
|
+
onChange={(event) => setMemoryHighBytes(event.target.value)}
|
|
142
|
+
placeholder="Unlimited"
|
|
143
|
+
disabled={saving}
|
|
144
|
+
className="min-h-9 rounded-og-sm border border-og-border bg-og-bg px-2.5 font-og-mono text-og-sm text-og-fg outline-hidden focus-visible:ring-2 focus-visible:ring-og-accent/50 disabled:opacity-60"
|
|
145
|
+
/>
|
|
146
|
+
</label>
|
|
147
|
+
<label className="flex flex-col gap-1.5 text-og-xs text-og-fg-muted">
|
|
148
|
+
CPU maximum (millicores)
|
|
149
|
+
<input
|
|
150
|
+
inputMode="numeric"
|
|
151
|
+
pattern="[0-9]*"
|
|
152
|
+
value={cpuMaxMillicores}
|
|
153
|
+
onChange={(event) => setCpuMaxMillicores(event.target.value)}
|
|
154
|
+
placeholder="Unlimited"
|
|
155
|
+
disabled={saving}
|
|
156
|
+
className="min-h-9 rounded-og-sm border border-og-border bg-og-bg px-2.5 font-og-mono text-og-sm text-og-fg outline-hidden focus-visible:ring-2 focus-visible:ring-og-accent/50 disabled:opacity-60"
|
|
157
|
+
/>
|
|
158
|
+
<span>1000 millicores = one CPU core. Blank is unlimited.</span>
|
|
159
|
+
</label>
|
|
160
|
+
<div className="flex min-h-9 items-center justify-between gap-3 sm:col-span-2">
|
|
161
|
+
<span
|
|
162
|
+
role={validationError ? "alert" : undefined}
|
|
163
|
+
className="text-og-xs text-og-status-failed"
|
|
164
|
+
>
|
|
165
|
+
{validationError}
|
|
166
|
+
</span>
|
|
167
|
+
<button
|
|
168
|
+
type="submit"
|
|
169
|
+
disabled={saving}
|
|
170
|
+
className="inline-flex min-h-9 items-center gap-1.5 rounded-og-sm bg-og-accent px-3 py-1.5 text-og-xs font-medium text-og-accent-fg transition-colors hover:bg-og-accent-strong disabled:cursor-not-allowed disabled:opacity-60"
|
|
171
|
+
>
|
|
172
|
+
{saving ? <Loader2Icon className="size-3.5 animate-spin" aria-hidden /> : null}
|
|
173
|
+
Save policy
|
|
174
|
+
</button>
|
|
175
|
+
</div>
|
|
176
|
+
</form>
|
|
177
|
+
</section>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
@@ -14,6 +14,8 @@ export type MachinesDashboardProps = {
|
|
|
14
14
|
onAttach?: ((machine: MachineView) => void) | undefined;
|
|
15
15
|
/** The sandbox id currently being attached/swapped to (disables that card). */
|
|
16
16
|
attachingSandboxId?: string | null | undefined;
|
|
17
|
+
onUpdateAgent?: ((machine: MachineView) => void) | undefined;
|
|
18
|
+
updatingEnrollmentId?: string | null | undefined;
|
|
17
19
|
/** Short recent history per machine (keyed by sandboxId) — drives card sparklines. */
|
|
18
20
|
seriesByMachine?: Record<string, MetricSample[]> | undefined;
|
|
19
21
|
/** Open the per-machine telemetry detail (makes each card actionable). */
|
|
@@ -116,6 +118,8 @@ export function MachinesDashboard({
|
|
|
116
118
|
error,
|
|
117
119
|
onAttach,
|
|
118
120
|
attachingSandboxId,
|
|
121
|
+
onUpdateAgent,
|
|
122
|
+
updatingEnrollmentId,
|
|
119
123
|
seriesByMachine,
|
|
120
124
|
onOpenDetail,
|
|
121
125
|
now,
|
|
@@ -172,6 +176,8 @@ export function MachinesDashboard({
|
|
|
172
176
|
}}
|
|
173
177
|
onAttach={onAttach}
|
|
174
178
|
attaching={attachingSandboxId === machine.sandboxId}
|
|
179
|
+
onUpdateAgent={onUpdateAgent}
|
|
180
|
+
updatingAgent={updatingEnrollmentId === machine.enrollmentId}
|
|
175
181
|
series={seriesByMachine?.[machine.sandboxId]}
|
|
176
182
|
onOpenDetail={onOpenDetail}
|
|
177
183
|
now={now}
|