Initial public ModelForge release
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
node_modules
|
||||
dist
|
||||
*.tsbuildinfo
|
||||
.vite
|
||||
@@ -0,0 +1,57 @@
|
||||
# Build the operator console, then serve the built assets. The previous image ran `vite dev` as
|
||||
# root with the whole source tree inside it, which is a development server rather than a release
|
||||
# artefact: it rebuilds on request, exposes the module graph and needs write access to its own
|
||||
# source. M16 replaces it with a static build served by an unprivileged nginx.
|
||||
FROM node:22-alpine@sha256:c610fcdfb1d5b4740dd70c284ed3cb16bb857e0f7166196e36a5501df7a3aa32 AS build
|
||||
WORKDIR /app
|
||||
RUN apk upgrade --no-cache
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci
|
||||
COPY . .
|
||||
|
||||
# Vite inlines its VITE_* variables at build time, so the API base URL is a build argument rather
|
||||
# than a runtime one. The default matches the standard local deployment.
|
||||
ARG VITE_API_BASE_URL=http://localhost:8000
|
||||
ENV VITE_API_BASE_URL=${VITE_API_BASE_URL}
|
||||
RUN npm run build
|
||||
|
||||
FROM nginxinc/nginx-unprivileged:1.29-alpine@sha256:0c79d56aee561a1d81c63f00eee5fb5fe29279560cdc55e91425133104c7fbe6
|
||||
USER root
|
||||
RUN apk upgrade --no-cache
|
||||
ARG VITE_API_BASE_URL=http://localhost:8000
|
||||
ARG MODELFORGE_VERSION=0.0.0
|
||||
ARG MODELFORGE_COMMIT=""
|
||||
ARG MODELFORGE_BUILT_AT=""
|
||||
ENV MODELFORGE_BUILD_COMMIT=${MODELFORGE_COMMIT}
|
||||
ENV MODELFORGE_BUILD_TIMESTAMP=${MODELFORGE_BUILT_AT}
|
||||
LABEL org.opencontainers.image.title="ITWorx ModelForge operator console"
|
||||
LABEL org.opencontainers.image.description="Operator console for the ITWorx ModelForge control plane"
|
||||
LABEL org.opencontainers.image.version="${MODELFORGE_VERSION}"
|
||||
LABEL org.opencontainers.image.revision="${MODELFORGE_COMMIT}"
|
||||
LABEL org.opencontainers.image.created="${MODELFORGE_BUILT_AT}"
|
||||
LABEL org.opencontainers.image.source="https://git.example.com/example/modelforge.git"
|
||||
LABEL org.opencontainers.image.vendor="ITWorx"
|
||||
LABEL org.opencontainers.image.licenses="AGPL-3.0-or-later"
|
||||
|
||||
# The runtime listens above 1024 and returns to the image's unprivileged nginx user after the
|
||||
# signed package upgrade and immutable file assembly. It works with a read-only root filesystem
|
||||
# plus a tmpfs for its caches.
|
||||
COPY --from=build /app/dist /usr/share/nginx/html
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
COPY security-headers.inc.template /etc/nginx/security-headers.inc.template
|
||||
|
||||
# The bundle's API origin is compiled in at build time, so the connect-src that protects it is
|
||||
# derived from the same argument rather than maintained separately and allowed to drift. Only the
|
||||
# origin is used; a path in connect-src is ignored by the browser anyway. The final grep makes a
|
||||
# failed substitution break the build instead of shipping a policy with a placeholder in it.
|
||||
RUN set -eu; \
|
||||
API_ORIGIN=$(printf '%s' "${VITE_API_BASE_URL}" | cut -d/ -f1-3); \
|
||||
sed "s|__API_ORIGIN__|${API_ORIGIN}|" /etc/nginx/security-headers.inc.template \
|
||||
> /etc/nginx/conf.d/security-headers.inc; \
|
||||
rm /etc/nginx/security-headers.inc.template; \
|
||||
grep -q "connect-src 'self' ${API_ORIGIN};" /etc/nginx/conf.d/security-headers.inc
|
||||
USER nginx
|
||||
|
||||
EXPOSE 3000
|
||||
HEALTHCHECK --interval=15s --timeout=3s --retries=5 \
|
||||
CMD wget -q -O /dev/null http://127.0.0.1:3000/ || exit 1
|
||||
@@ -0,0 +1,14 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="theme-color" content="#0a0b0d" />
|
||||
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='8' fill='%2315171c'/%3E%3Cpath d='M9 10h14v12H9z' fill='none' stroke='%23aeb6cb' stroke-width='2'/%3E%3Cpath d='M12 14h8M12 18h5' stroke='%236fd19a' stroke-width='2'/%3E%3C/svg%3E" />
|
||||
<title>ITWorx ModelForge</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,44 @@
|
||||
# Static serving for the ModelForge operator console.
|
||||
#
|
||||
# The console is a single-page application that talks to the control plane directly from the
|
||||
# browser, so this server only ever returns files from its own build output. It proxies nothing,
|
||||
# which keeps it out of the request path between an operator and an authenticated admin route.
|
||||
#
|
||||
# nginx only inherits `add_header` into a location that declares none of its own, so the security
|
||||
# headers are included in every location rather than set once on the server. Setting them at the
|
||||
# server level alone silently drops them from exactly the responses that matter most.
|
||||
server {
|
||||
listen 3000;
|
||||
server_name _;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
server_tokens off;
|
||||
|
||||
# The console holds an operator credential in memory. These headers cost nothing and remove
|
||||
# the easiest ways to get someone else's script or frame near it.
|
||||
include /etc/nginx/conf.d/security-headers.inc;
|
||||
|
||||
# Hashed assets are immutable; the entry document must never be cached, or an operator can be
|
||||
# left driving a console that no longer matches the control plane it is talking to.
|
||||
location /assets/ {
|
||||
include /etc/nginx/conf.d/security-headers.inc;
|
||||
add_header Cache-Control "public, max-age=31536000, immutable" always;
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
location = /index.html {
|
||||
include /etc/nginx/conf.d/security-headers.inc;
|
||||
add_header Cache-Control "no-store" always;
|
||||
}
|
||||
|
||||
location / {
|
||||
include /etc/nginx/conf.d/security-headers.inc;
|
||||
add_header Cache-Control "no-store" always;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# Nothing else is served: no directory listings, no dotfiles, no source maps by path guess.
|
||||
location ~ /\. {
|
||||
deny all;
|
||||
}
|
||||
}
|
||||
Generated
+2456
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "modelforge-web",
|
||||
"private": true,
|
||||
"version": "1.2.2",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --host 0.0.0.0",
|
||||
"build": "tsc -b && vite build",
|
||||
"test": "vitest run",
|
||||
"preview": "vite preview --host 0.0.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vitejs/plugin-react": "^6.1.0",
|
||||
"lucide-react": "^1.34.0",
|
||||
"react": "^19.2.8",
|
||||
"react-dom": "^19.2.8",
|
||||
"typescript": "^7.0.2",
|
||||
"vite": "^8.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/jest-dom": "^7.0.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@types/react": "^19.2.18",
|
||||
"@types/react-dom": "^19.2.5",
|
||||
"jsdom": "^30.0.1",
|
||||
"vitest": "^4.1.11"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
# Security headers for the ModelForge operator console.
|
||||
#
|
||||
# Included in every location because nginx does not inherit `add_header` into a location that
|
||||
# declares one of its own — setting them once on the server silently dropped them from the entry
|
||||
# document, which is how M16 found this.
|
||||
#
|
||||
# Generated at image build time from VITE_API_BASE_URL: the console's API origin is compiled into
|
||||
# the bundle, so the policy that protects the bundle is derived from the same value rather than
|
||||
# maintained separately and allowed to drift.
|
||||
add_header Content-Security-Policy "default-src 'none'; script-src 'self'; style-src 'self'; style-src-attr 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self' __API_ORIGIN__; base-uri 'none'; form-action 'self'; frame-ancestors 'none'; object-src 'none'" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-Frame-Options "DENY" always;
|
||||
add_header Referrer-Policy "no-referrer" always;
|
||||
add_header Cross-Origin-Opener-Policy "same-origin" always;
|
||||
add_header Cross-Origin-Resource-Policy "same-origin" always;
|
||||
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
|
||||
@@ -0,0 +1,559 @@
|
||||
import { act, cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import App from "./App";
|
||||
import { clearOperatorCredential } from "./lib/api";
|
||||
import type {
|
||||
AcceleratorState, HardwareState, ModelSummary, NodeDecommissionPreview,
|
||||
NodeDecommissionResult, NodeState, ObservedValue,
|
||||
} from "./types";
|
||||
|
||||
const known = <T,>(value: T): ObservedValue<T> => ({ value, availability: "known" });
|
||||
const unknown = <T,>(): ObservedValue<T> => ({
|
||||
value: null,
|
||||
availability: "unknown",
|
||||
reason: "not reported",
|
||||
});
|
||||
|
||||
function accelerator(overrides: Partial<AcceleratorState> = {}): AcceleratorState {
|
||||
return {
|
||||
id: "accelerator-1",
|
||||
node_id: "node-1",
|
||||
status: "active",
|
||||
device_index: 0,
|
||||
device_uuid: "GPU-12345678-1234-1234-1234-123456789abc",
|
||||
pci_bus_id: known("0000:01:00.0"),
|
||||
name: "NVIDIA Test GPU",
|
||||
vendor: "NVIDIA",
|
||||
architecture: known("ampere"),
|
||||
compute_capability_major: known(8),
|
||||
compute_capability_minor: known(6),
|
||||
total_vram_bytes: known(8 * 1024 ** 3),
|
||||
driver_version: known("581.80"),
|
||||
cuda_driver_version: known("13.0"),
|
||||
mig_mode_current: known(false),
|
||||
first_seen_at: "2026-08-24T00:00:00Z",
|
||||
last_seen_at: "2026-08-24T00:00:00Z",
|
||||
inventory_at: "2026-08-24T00:00:00Z",
|
||||
telemetry: {
|
||||
observed_at: "2026-08-24T00:00:00Z",
|
||||
used_vram_bytes: known(2 * 1024 ** 3),
|
||||
free_vram_bytes: known(6 * 1024 ** 3),
|
||||
gpu_utilization_percent: known(25),
|
||||
memory_utilization_percent: known(10),
|
||||
temperature_c: known(55),
|
||||
power_draw_w: known(80),
|
||||
power_limit_w: known(200),
|
||||
graphics_clock_mhz: known(1500),
|
||||
memory_clock_mhz: known(7000),
|
||||
fan_speed_percent: unknown(),
|
||||
performance_state: known("P2"),
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function node(accelerators: AcceleratorState[] = []): NodeState {
|
||||
return {
|
||||
id: "node-1",
|
||||
generation: 1,
|
||||
identity_key: "stable-node-key",
|
||||
identity_source: "persisted_uuid",
|
||||
hostname: "forge-node",
|
||||
display_name: "Forge node",
|
||||
status: "active",
|
||||
os_name: "Linux",
|
||||
os_version: known("test"),
|
||||
architecture: "x86_64",
|
||||
kernel_version: known("6.6"),
|
||||
cpu_model: known("Test CPU"),
|
||||
logical_cpu_count: known(16),
|
||||
physical_core_count: known(8),
|
||||
total_ram_bytes: known(32 * 1024 ** 3),
|
||||
available_ram_bytes: known(20 * 1024 ** 3),
|
||||
agent_version: "0.1.0",
|
||||
first_seen_at: "2026-08-24T00:00:00Z",
|
||||
last_seen_at: "2026-08-24T00:00:00Z",
|
||||
inventory_at: "2026-08-24T00:00:00Z",
|
||||
hardware_fingerprint: "a".repeat(64),
|
||||
enabled: true,
|
||||
liveness: "online",
|
||||
agent_health: "healthy",
|
||||
observation_source: "remote_agent",
|
||||
protocol_version: 1,
|
||||
supported_capabilities: ["hardware.inventory", "hardware.telemetry"],
|
||||
last_heartbeat_at: new Date().toISOString(),
|
||||
labels: { site: "test" },
|
||||
production_eligible: false,
|
||||
lab_eligible: true,
|
||||
benchmark_eligible: false,
|
||||
storage: [],
|
||||
accelerators,
|
||||
};
|
||||
}
|
||||
|
||||
function hardware(nodes: NodeState[]): HardwareState {
|
||||
return {
|
||||
overview: {
|
||||
status: nodes.length ? "active" : "pending",
|
||||
inventory_state: nodes.length ? "active" : "pending",
|
||||
node_count: nodes.length,
|
||||
accelerator_count: nodes.flatMap((item) => item.accelerators).length,
|
||||
last_inventory_at: nodes.length ? "2026-08-24T00:00:00Z" : null,
|
||||
},
|
||||
nodes,
|
||||
};
|
||||
}
|
||||
|
||||
function mockApi(state: HardwareState, unavailableCommandEvidence = false): void {
|
||||
vi.stubGlobal("fetch", vi.fn((input: RequestInfo | URL) => {
|
||||
const url = String(input);
|
||||
if (unavailableCommandEvidence && (url.endsWith("/api/v1/capability-deployments") || url.endsWith("/api/v1/scheduler"))) {
|
||||
return Promise.reject(new Error("evidence source unavailable"));
|
||||
}
|
||||
let payload: unknown = state;
|
||||
if (url.endsWith("/api/v1/system")) payload = { name: "ModelForge", version: "0.1.0", environment: "test", api_version: "v1", milestone: "M2", production_inference_available: false };
|
||||
if (url.includes("/api/v1/models?")) payload = { items: [], page: 1, page_size: 20, total: 0, pages: 0 };
|
||||
if (url.endsWith("/api/v1/projects")) payload = [];
|
||||
if (url.endsWith("/api/v1/project-integrations")) payload = [];
|
||||
if (url.endsWith("/api/v1/capability-deployments") || url.endsWith("/api/v1/scheduler")) payload = [];
|
||||
if (url.includes("/api/v1/admin/operations/overview")) payload = { status: "HEALTHY", observed_at: "2026-08-29T12:00:00Z", history_available: false, active_alerts: [], slo_evaluations: [], capacity: [], capability_health: [], project_health: [], recent_failures: [] };
|
||||
if (url.includes("/api/v1/admin/recovery/dashboard")) payload = { observed_at: "2026-08-29T12:00:00Z", point_in_time_support: "NOT_SUPPORTED", backup_states: {}, verified_backup_count: 0, stale_backup: false, backup_staleness_threshold_seconds: 93600, protected_asset_count: 0, unprotected_assets: [], readiness: [], coverage_ratio: 1, estimated_protected_bytes: 0, estimated_rehydratable_bytes: 0 };
|
||||
return Promise.resolve(new Response(JSON.stringify(payload), { status: 200 }));
|
||||
}));
|
||||
}
|
||||
|
||||
async function renderUnlocked(credential = "admin-key"): Promise<void> {
|
||||
render(<App />);
|
||||
const input = await screen.findByLabelText("Operator API key");
|
||||
fireEvent.change(input, { target: { value: credential } });
|
||||
fireEvent.click(screen.getByRole("button", { name: "Unlock operator session" }));
|
||||
await waitFor(() => expect(screen.queryByRole("heading", { name: "Operator session locked" })).not.toBeInTheDocument());
|
||||
}
|
||||
|
||||
function decommissionPreview(safe: boolean): NodeDecommissionPreview {
|
||||
return {
|
||||
node_id: "node-1",
|
||||
persisted_identity: "stable-node-key",
|
||||
hostname: "forge-node",
|
||||
display_name: "Forge node",
|
||||
current_state: { status: "active", liveness: "offline", enabled: true },
|
||||
node_generation: 1,
|
||||
dependency_digest: "d".repeat(64),
|
||||
safe,
|
||||
blockers: safe ? [] : [{
|
||||
code: "active_serving_job",
|
||||
message: "Queued or running serving jobs must finish first",
|
||||
record_type: "serving_job",
|
||||
count: 1,
|
||||
resource_ids: ["job-1"],
|
||||
}],
|
||||
cleanup: [{ record_type: "active_node_credentials", count: 1, action: "revoke" }],
|
||||
preserved: [{ record_type: "serving_jobs", count: 3, action: "preserve" }],
|
||||
dependent_records: [],
|
||||
};
|
||||
}
|
||||
|
||||
function mockDecommission(preview: NodeDecommissionPreview, requests: RequestInit[] = []): void {
|
||||
const result: NodeDecommissionResult = {
|
||||
operation_id: "operation-1",
|
||||
node_id: "node-1",
|
||||
persisted_identity: "stable-node-key",
|
||||
status: "completed",
|
||||
decommissioned_at: "2026-08-28T12:00:00Z",
|
||||
cleanup_summary: { active_node_credentials: 1 },
|
||||
previous_state: { status: "active" },
|
||||
credential_revocations: 1,
|
||||
idempotent_replay: false,
|
||||
};
|
||||
vi.stubGlobal("fetch", vi.fn((input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const url = String(input);
|
||||
let payload: unknown = hardware([node()]);
|
||||
if (url.endsWith("/api/v1/system")) payload = { name: "ModelForge", version: "1.0.0", environment: "test", api_version: "v1", milestone: "post-v1", production_inference_available: false };
|
||||
if (url.includes("/api/v1/models?")) payload = { items: [], page: 1, page_size: 20, total: 0, pages: 0 };
|
||||
if (url.endsWith("/api/v1/projects") || url.endsWith("/api/v1/project-integrations")) payload = [];
|
||||
if (url.endsWith("/decommission/preview")) {
|
||||
requests.push(init ?? {});
|
||||
payload = preview;
|
||||
} else if (url.endsWith("/decommission")) {
|
||||
requests.push(init ?? {});
|
||||
payload = result;
|
||||
}
|
||||
return Promise.resolve(new Response(JSON.stringify(payload), { status: 200 }));
|
||||
}));
|
||||
}
|
||||
|
||||
function registryModel(): ModelSummary {
|
||||
return {
|
||||
id: "11111111-1111-4111-8111-111111111111", key: "registry-qwen", display_name: "Registry Qwen",
|
||||
description: "Governed locally", source_type: "huggingface", upstream_provider: "example",
|
||||
upstream_source: "example/model", upstream_metadata: { repository_id: "example/model" },
|
||||
local_metadata: { owner: "modelops" }, interpretation_metadata: { intended_capabilities: ["assistant.general"], evidence_status: "unverified_seed_claim" },
|
||||
family: null, modalities: [], parameter_metadata: {}, license_metadata: { status: "unknown", spdx_id: null },
|
||||
lifecycle: "candidate", revision_count: 0, artifact_count: 0, verification_status: "unverified",
|
||||
deployment_status: "not_deployed", created_at: "2026-08-25T00:00:00Z", updated_at: "2026-08-25T00:00:00Z",
|
||||
};
|
||||
}
|
||||
|
||||
function mockRegistry(deleteConflict = false): void {
|
||||
const model = registryModel();
|
||||
vi.stubGlobal("fetch", vi.fn((input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const url = String(input);
|
||||
if (url.endsWith("/api/v1/system")) return Promise.resolve(new Response(JSON.stringify({ name: "ModelForge", version: "0.1.0", environment: "test", api_version: "v1", milestone: "M2", production_inference_available: false })));
|
||||
if (url.endsWith("/api/v1/projects")) return Promise.resolve(new Response("[]"));
|
||||
if (url.endsWith("/api/v1/project-integrations")) return Promise.resolve(new Response("[]"));
|
||||
if (url.endsWith("/api/v1/hardware")) return Promise.resolve(new Response(JSON.stringify(hardware([]))));
|
||||
if (url.includes("/api/v1/models?") && init?.method !== "DELETE") return Promise.resolve(new Response(JSON.stringify({ items: [model], page: 1, page_size: 8, total: 1, pages: 1 })));
|
||||
if (url.endsWith(`/api/v1/models/${model.id}/revisions?page=1&page_size=100`)) return Promise.resolve(new Response(JSON.stringify({ items: [], page: 1, page_size: 100, total: 0, pages: 0 })));
|
||||
if (url.endsWith(`/api/v1/models/${model.id}`) && init?.method === "DELETE" && deleteConflict) return Promise.resolve(new Response(JSON.stringify({ error: { message: "resource has dependencies", details: { dependencies: [{ resource_type: "model_revision", resource_id: "22222222-2222-4222-8222-222222222222", relation: "revision" }] } } }), { status: 409 }));
|
||||
if (url.endsWith(`/api/v1/models/${model.id}`)) return Promise.resolve(new Response(JSON.stringify(model)));
|
||||
return Promise.resolve(new Response(JSON.stringify({ items: [], page: 1, page_size: 100, total: 0, pages: 0 })));
|
||||
}));
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
clearOperatorCredential();
|
||||
cleanup();
|
||||
vi.unstubAllGlobals();
|
||||
window.location.hash = "";
|
||||
window.localStorage.clear();
|
||||
document.documentElement.removeAttribute("data-theme");
|
||||
});
|
||||
|
||||
describe("v1 console polish", () => {
|
||||
it("loads only public liveness and version before the operator unlock", async () => {
|
||||
const requests: string[] = [];
|
||||
vi.stubGlobal("fetch", vi.fn((input: RequestInfo | URL) => {
|
||||
const url = String(input); requests.push(url);
|
||||
if (url.endsWith("/api/v1/version")) return Promise.resolve(new Response(JSON.stringify({ name: "ModelForge", version: "1.2.1" })));
|
||||
return Promise.resolve(new Response(JSON.stringify({ status: "healthy", version: "1.2.1" })));
|
||||
}));
|
||||
|
||||
render(<App />);
|
||||
|
||||
expect(await screen.findByRole("heading", { name: "Operator session locked" })).toBeInTheDocument();
|
||||
expect(await screen.findByText("healthy")).toBeInTheDocument();
|
||||
expect(screen.getByText("1.2.1")).toBeInTheDocument();
|
||||
expect(requests).toHaveLength(2);
|
||||
expect(requests.every((url) => url.endsWith("/api/v1/version") || url.endsWith("/api/v1/health/live"))).toBe(true);
|
||||
});
|
||||
|
||||
it("clears the memory-only session when the operator locks the console", async () => {
|
||||
mockApi(hardware([]));
|
||||
await renderUnlocked("operator-secret");
|
||||
|
||||
fireEvent.click(await screen.findByRole("button", { name: "Lock console" }));
|
||||
|
||||
expect(await screen.findByRole("heading", { name: "Operator session locked" })).toBeInTheDocument();
|
||||
expect(window.localStorage.getItem("operator-api-key")).toBeNull();
|
||||
expect(window.sessionStorage.getItem("operator-api-key")).toBeNull();
|
||||
});
|
||||
|
||||
it("renders an evidence-backed command center without inventing unavailable assurance state", async () => {
|
||||
mockApi(hardware([node([accelerator()])]));
|
||||
await renderUnlocked();
|
||||
|
||||
expect(await screen.findByRole("heading", { name: "Dashboard" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("region", { name: "Platform posture" })).toBeInTheDocument();
|
||||
expect(screen.getByText("GPU scheduling envelope")).toBeInTheDocument();
|
||||
expect(screen.getByText("Alerts and recovery readiness")).toBeInTheDocument();
|
||||
expect(screen.queryByLabelText("Operator API key")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("keeps production posture unknown when deployment or scheduler evidence cannot load", async () => {
|
||||
mockApi(hardware([node([accelerator()])]), true);
|
||||
await renderUnlocked();
|
||||
|
||||
expect(await screen.findByText("Production posture is unknown")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Production control plane is ready")).not.toBeInTheDocument();
|
||||
expect(screen.getByText("Restore command-center evidence")).toBeInTheDocument();
|
||||
expect(screen.queryByText(/gateway has no production deployment/i)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("persists an explicit theme without making it load-bearing", async () => {
|
||||
mockApi(hardware([]));
|
||||
await renderUnlocked();
|
||||
|
||||
const themeButtons = await screen.findAllByRole("button", {
|
||||
name: "Theme: system. Switch to dark.",
|
||||
});
|
||||
fireEvent.click(themeButtons[0]);
|
||||
|
||||
expect(document.documentElement).toHaveAttribute("data-theme", "dark");
|
||||
expect(window.localStorage.getItem("modelforge.theme")).toBe("dark");
|
||||
});
|
||||
|
||||
it("groups navigation and gives the selected page its own header", async () => {
|
||||
window.location.hash = "#/nodes";
|
||||
mockApi(hardware([]));
|
||||
await renderUnlocked();
|
||||
|
||||
expect(await screen.findByRole("heading", { name: "Nodes" })).toBeInTheDocument();
|
||||
expect(screen.getByText(/Enrolled compute nodes, accelerators/)).toBeInTheDocument();
|
||||
expect(screen.getByRole("navigation", { name: "Command" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("navigation", { name: "Model supply" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("navigation", { name: "Serving" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("navigation", { name: "Infrastructure" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("navigation", { name: "Assurance" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("navigation", { name: "Projects" })).toBeInTheDocument();
|
||||
expect(document.querySelectorAll(".nav-group nav button")).toHaveLength(13);
|
||||
});
|
||||
|
||||
it("opens a keyboard-accessible quick jump and navigates to the first match", async () => {
|
||||
mockApi(hardware([]));
|
||||
await renderUnlocked();
|
||||
|
||||
fireEvent.keyDown(window, { key: "k", ctrlKey: true });
|
||||
const search = await screen.findByRole("combobox", { name: "Search workspaces" });
|
||||
await waitFor(() => expect(search).toHaveFocus());
|
||||
fireEvent.change(search, { target: { value: "backup" } });
|
||||
fireEvent.keyDown(search, { key: "Enter" });
|
||||
|
||||
expect(await screen.findByRole("heading", { name: "Recovery" })).toBeInTheDocument();
|
||||
expect(screen.queryByRole("dialog", { name: "Quick jump" })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("closes the mobile navigation drawer with Escape", async () => {
|
||||
mockApi(hardware([]));
|
||||
await renderUnlocked();
|
||||
|
||||
fireEvent.click(await screen.findByRole("button", { name: "Open navigation" }));
|
||||
expect(document.querySelector(".sidebar")).toHaveClass("open");
|
||||
expect(document.querySelector("main")).toHaveAttribute("inert");
|
||||
expect(document.querySelector(".mobile-bar")).toHaveAttribute("inert");
|
||||
expect(document.querySelector(".nav-scrim")).toHaveAttribute("aria-hidden", "true");
|
||||
expect(document.activeElement).toHaveAttribute("aria-label", "Close navigation");
|
||||
|
||||
fireEvent.keyDown(window, { key: "Escape" });
|
||||
expect(document.querySelector(".sidebar")).not.toHaveClass("open");
|
||||
expect(document.querySelector("main")).not.toHaveAttribute("inert");
|
||||
expect(screen.getByRole("button", { name: "Open navigation" })).toHaveFocus();
|
||||
});
|
||||
|
||||
it("closes the drawer on desktop resize and focuses the active route", async () => {
|
||||
let mediaChange: ((event: MediaQueryListEvent) => void) | undefined;
|
||||
vi.stubGlobal("matchMedia", vi.fn(() => ({
|
||||
matches: true,
|
||||
media: "(max-width: 860px)",
|
||||
onchange: null,
|
||||
addEventListener: (_type: string, listener: (event: MediaQueryListEvent) => void) => {
|
||||
mediaChange = listener;
|
||||
},
|
||||
removeEventListener: vi.fn(),
|
||||
addListener: vi.fn(),
|
||||
removeListener: vi.fn(),
|
||||
dispatchEvent: vi.fn(),
|
||||
})));
|
||||
mockApi(hardware([]));
|
||||
await renderUnlocked();
|
||||
|
||||
fireEvent.click(await screen.findByRole("button", { name: "Open navigation" }));
|
||||
expect(document.querySelector("main")).toHaveAttribute("inert");
|
||||
|
||||
act(() => mediaChange?.({ matches: false } as MediaQueryListEvent));
|
||||
|
||||
expect(document.querySelector(".sidebar")).not.toHaveClass("open");
|
||||
expect(document.querySelector("main")).not.toHaveAttribute("inert");
|
||||
expect(document.querySelector('.sidebar button[aria-current="page"]')).toHaveFocus();
|
||||
|
||||
act(() => mediaChange?.({ matches: false } as MediaQueryListEvent));
|
||||
fireEvent.click(screen.getByRole("button", { name: "Open navigation" }));
|
||||
fireEvent.keyDown(window, { key: "Escape" });
|
||||
expect(screen.getByRole("button", { name: "Open navigation" })).toHaveFocus();
|
||||
});
|
||||
});
|
||||
|
||||
describe("M1.5 compute node UI", () => {
|
||||
it("shows loading while authenticated control-plane requests are pending", async () => {
|
||||
vi.stubGlobal("fetch", vi.fn((input: RequestInfo | URL) => {
|
||||
const url = String(input);
|
||||
if (url.endsWith("/api/v1/system")) return Promise.resolve(new Response(JSON.stringify({ name: "ModelForge", version: "1.2.1", environment: "test", api_version: "v1", milestone: "M18", production_inference_available: false })));
|
||||
if (url.endsWith("/api/v1/version")) return Promise.resolve(new Response(JSON.stringify({ name: "ModelForge", version: "1.2.1" })));
|
||||
if (url.endsWith("/api/v1/health/live")) return Promise.resolve(new Response(JSON.stringify({ status: "healthy", version: "1.2.1" })));
|
||||
return new Promise<Response>(() => undefined);
|
||||
}));
|
||||
await renderUnlocked();
|
||||
expect(await screen.findByText("Loading control-plane state…")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders a valid no-GPU state", async () => {
|
||||
window.location.hash = "#/nodes";
|
||||
mockApi(hardware([node()]));
|
||||
await renderUnlocked();
|
||||
fireEvent.click(await screen.findByText("Forge node"));
|
||||
expect(await screen.findByText("No NVIDIA accelerator detected.")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders unknown metrics without inventing zero", async () => {
|
||||
window.location.hash = "#/nodes";
|
||||
const gpu = accelerator({
|
||||
telemetry: { ...accelerator().telemetry!, temperature_c: unknown(), power_draw_w: unknown() },
|
||||
});
|
||||
mockApi(hardware([node([gpu])]));
|
||||
await renderUnlocked();
|
||||
fireEvent.click(await screen.findByText("Forge node"));
|
||||
expect(await screen.findByText("NVIDIA Test GPU")).toBeInTheDocument();
|
||||
expect(screen.getAllByText("unknown").length).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
|
||||
it("renders measured GPU inventory and telemetry", async () => {
|
||||
window.location.hash = "#/nodes";
|
||||
mockApi(hardware([node([accelerator()])]));
|
||||
await renderUnlocked();
|
||||
fireEvent.click(await screen.findByText("Forge node"));
|
||||
expect(await screen.findByText("NVIDIA Test GPU")).toBeInTheDocument();
|
||||
expect(screen.getByText("25%")).toBeInTheDocument();
|
||||
expect(screen.getByText("55 °C")).toBeInTheDocument();
|
||||
expect(screen.getByText("80 W")).toBeInTheDocument();
|
||||
expect(screen.getByText("581.80")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("distinguishes stale and offline nodes", async () => {
|
||||
window.location.hash = "#/nodes";
|
||||
const stale = { ...node(), id: "node-stale", display_name: "Stale node", liveness: "stale" as const };
|
||||
const offline = { ...node(), id: "node-offline", display_name: "Offline node", liveness: "offline" as const };
|
||||
mockApi(hardware([stale, offline]));
|
||||
await renderUnlocked();
|
||||
expect(await screen.findByText("Stale node")).toBeInTheDocument();
|
||||
expect(screen.getByText("Offline node")).toBeInTheDocument();
|
||||
expect(screen.getByText("stale")).toBeInTheDocument();
|
||||
expect(screen.getByText("offline")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("opens secure enrollment without claiming a node is connected", async () => {
|
||||
window.location.hash = "#/nodes";
|
||||
mockApi(hardware([]));
|
||||
await renderUnlocked();
|
||||
fireEvent.click(await screen.findByText("Enroll node"));
|
||||
expect(screen.getByText("Create one-time enrollment")).toBeInTheDocument();
|
||||
expect(screen.queryByLabelText("Operator API key")).not.toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Lock console" })).toBeInTheDocument();
|
||||
expect(screen.getByText(/No compute nodes are registered/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("audited node decommission UI", () => {
|
||||
it("shows fail-closed blockers and offers no execute action", async () => {
|
||||
window.location.hash = "#/nodes";
|
||||
mockDecommission(decommissionPreview(false));
|
||||
await renderUnlocked();
|
||||
fireEvent.click(await screen.findByText("Forge node"));
|
||||
fireEvent.click(screen.getByRole("button", { name: "Preview decommission" }));
|
||||
expect(await screen.findByText("1 safety blocker")).toBeInTheDocument();
|
||||
expect(screen.getByText("Queued or running serving jobs must finish first")).toBeInTheDocument();
|
||||
expect(screen.getByText("3 · preserve")).toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Permanently decommission node" })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("requires explicit identity, reason, operator, and typed confirmation before execute", async () => {
|
||||
window.location.hash = "#/nodes";
|
||||
const requests: RequestInit[] = [];
|
||||
mockDecommission(decommissionPreview(true), requests);
|
||||
await renderUnlocked();
|
||||
fireEvent.click(await screen.findByText("Forge node"));
|
||||
fireEvent.change(screen.getByLabelText("Operator identity"), { target: { value: "Jens" } });
|
||||
fireEvent.change(screen.getByLabelText("Reason"), { target: { value: "Disposable node has left the lab permanently." } });
|
||||
fireEvent.click(screen.getByRole("button", { name: "Preview decommission" }));
|
||||
expect(await screen.findByText("Safe to decommission")).toBeInTheDocument();
|
||||
const execute = screen.getByRole("button", { name: "Permanently decommission node" });
|
||||
expect(execute).toBeDisabled();
|
||||
fireEvent.change(screen.getByLabelText(/Type stable-node-key/), { target: { value: "stable-node-key" } });
|
||||
expect(execute).toBeEnabled();
|
||||
fireEvent.click(execute);
|
||||
expect(await screen.findByText("Node decommissioned")).toBeInTheDocument();
|
||||
const executeRequest = requests.at(-1);
|
||||
expect(executeRequest?.method).toBe("POST");
|
||||
expect(executeRequest?.headers).toMatchObject({ "X-ModelForge-Admin-Token": "admin-key" });
|
||||
expect(JSON.parse(String(executeRequest?.body))).toMatchObject({
|
||||
expected_generation: 1,
|
||||
preview_digest: "d".repeat(64),
|
||||
operator: "Jens",
|
||||
reason: "Disposable node has left the lab permanently.",
|
||||
confirmation: "stable-node-key",
|
||||
});
|
||||
expect(screen.getByText(/cannot automatically enroll/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("M2 model registry UI", () => {
|
||||
it("separates upstream facts, local governance and local interpretation", async () => {
|
||||
window.location.hash = "#/models";
|
||||
mockRegistry();
|
||||
await renderUnlocked();
|
||||
fireEvent.click((await screen.findAllByText("Registry Qwen"))[0]);
|
||||
expect(await screen.findByText("UPSTREAM FACTS")).toBeInTheDocument();
|
||||
expect(screen.getByText("LOCAL GOVERNANCE")).toBeInTheDocument();
|
||||
expect(screen.getByText("LOCAL INTERPRETATION · NOT UPSTREAM FACT")).toBeInTheDocument();
|
||||
expect(screen.getAllByText("example/model").length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("shows dependency details when explicit deletion is blocked", async () => {
|
||||
window.location.hash = "#/models";
|
||||
mockRegistry(true);
|
||||
await renderUnlocked();
|
||||
fireEvent.click((await screen.findAllByText("Registry Qwen"))[0]);
|
||||
fireEvent.click(await screen.findByRole("button", { name: "Delete" }));
|
||||
expect(await screen.findByRole("alertdialog", { name: "Delete model?" })).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole("checkbox", { name: /I understand this is a permanent registry action/ }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "Delete model" }));
|
||||
expect(await screen.findByText("Deletion blocked")).toBeInTheDocument();
|
||||
expect(screen.getByText("model_revision")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("M18 closure · registry tabs and operator token forms", () => {
|
||||
it("pairs each registry detail tab with the panel it controls", async () => {
|
||||
window.location.hash = "#/models";
|
||||
mockRegistry();
|
||||
await renderUnlocked();
|
||||
fireEvent.click((await screen.findAllByText("Registry Qwen"))[0]);
|
||||
await screen.findByRole("tablist", { name: "Model detail sections" });
|
||||
const selected = screen.getByRole("tab", { selected: true });
|
||||
const panel = screen.getByRole("tabpanel");
|
||||
expect(selected.getAttribute("aria-controls")).toBe(panel.getAttribute("id"));
|
||||
expect(panel.getAttribute("aria-labelledby")).toBe(selected.getAttribute("id"));
|
||||
const ids = screen.getAllByRole("tab").map((tab) => tab.getAttribute("id"));
|
||||
expect(new Set(ids).size).toBe(ids.length);
|
||||
expect(ids.every(Boolean)).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps registry tab selection and the rendered panel in step under the keyboard", async () => {
|
||||
window.location.hash = "#/models";
|
||||
mockRegistry();
|
||||
await renderUnlocked();
|
||||
fireEvent.click((await screen.findAllByText("Registry Qwen"))[0]);
|
||||
const tabs = await screen.findAllByRole("tab");
|
||||
fireEvent.keyDown(tabs[0], { key: "ArrowRight" });
|
||||
const selected = screen.getByRole("tab", { selected: true });
|
||||
expect(selected).toHaveTextContent("upstream");
|
||||
expect(screen.getByRole("tabpanel").getAttribute("aria-labelledby")).toBe(selected.getAttribute("id"));
|
||||
});
|
||||
|
||||
for (const route of ["operations", "recovery", "lifecycle", "migrations"] as const) {
|
||||
it(`reuses one memory-only operator session for the ${route} workspace`, async () => {
|
||||
window.location.hash = `#/${route}`;
|
||||
const seen: RequestInit[] = [];
|
||||
vi.stubGlobal("fetch", vi.fn((input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const url = String(input);
|
||||
if (init) seen.push(init);
|
||||
if (url.endsWith("/api/v1/system")) return Promise.resolve(new Response(JSON.stringify({ name: "ModelForge", version: "0.1.0", environment: "test", api_version: "v1", milestone: "M18", production_inference_available: false })));
|
||||
if (url.endsWith("/api/v1/hardware")) return Promise.resolve(new Response(JSON.stringify(hardware([]))));
|
||||
if (url.includes("/recovery/dashboard")) return Promise.resolve(new Response(JSON.stringify({ observed_at: "2026-08-29T12:00:00Z", point_in_time_support: "NOT_SUPPORTED", backup_states: {}, verified_backup_count: 0, stale_backup: false, backup_staleness_threshold_seconds: 93600, protected_asset_count: 0, unprotected_assets: [], readiness: [], coverage_ratio: 1, estimated_protected_bytes: 0, estimated_rehydratable_bytes: 0 })));
|
||||
if (url.includes("/operations/overview")) return Promise.resolve(new Response(JSON.stringify({ status: "HEALTHY", observed_at: "2026-08-29T12:00:00Z", history_available: false, capability_health: [], project_health: [] })));
|
||||
return Promise.resolve(new Response("[]"));
|
||||
}));
|
||||
await renderUnlocked("operator-secret");
|
||||
|
||||
await waitFor(() => expect(seen.some((init) => JSON.stringify(init.headers ?? {}).includes("operator-secret"))).toBe(true));
|
||||
expect(screen.queryByLabelText("Operator API key")).not.toBeInTheDocument();
|
||||
|
||||
// The credential is request-scoped: nothing durable, and nothing serialized into the URL.
|
||||
expect(window.localStorage.getItem("operator-api-key")).toBeNull();
|
||||
expect(JSON.stringify(window.localStorage)).not.toContain("operator-secret");
|
||||
expect(JSON.stringify(window.sessionStorage)).not.toContain("operator-secret");
|
||||
expect(document.cookie).not.toContain("operator-secret");
|
||||
expect(window.location.search).toBe("");
|
||||
expect(window.location.hash).toBe(`#/${route}`);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,976 @@
|
||||
import { FormEvent, useCallback, useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
Boxes, Command, Cpu, Database, FlaskConical, Menu, Monitor, Moon, Plus, RefreshCw,
|
||||
ShieldCheck, Sun, X,
|
||||
} from "lucide-react";
|
||||
|
||||
import { CommandCenter } from "./components/CommandCenter";
|
||||
import { CommandPalette } from "./components/CommandPalette";
|
||||
import { ModelsRegistry } from "./components/ModelsRegistry";
|
||||
import { DiscoverWorkspace } from "./components/DiscoverWorkspace";
|
||||
import { RuntimeWorkspace } from "./components/RuntimeWorkspace";
|
||||
import { CapabilityWorkspace } from "./components/CapabilityWorkspace";
|
||||
import { EvaluationWorkspace } from "./components/EvaluationWorkspace";
|
||||
import { AdvisorWorkspace } from "./components/AdvisorWorkspace";
|
||||
import { LifecycleWorkspace } from "./components/LifecycleWorkspace";
|
||||
import { MigrationWorkspace } from "./components/MigrationWorkspace";
|
||||
import { OperationsWorkspace } from "./components/OperationsWorkspace";
|
||||
import { RecoveryWorkspace } from "./components/RecoveryWorkspace";
|
||||
import {
|
||||
api, ApiError, clearOperatorCredential, setOperatorCredential,
|
||||
onOperatorSessionInvalidated, operatorCredentialReference,
|
||||
type PublicHealth, type PublicRelease,
|
||||
} from "./lib/api";
|
||||
import { navigation, navigationGroups, type RouteKey } from "./navigation";
|
||||
import type {
|
||||
AcceleratorState, CapabilityDeployment, EnrollmentCreated, HardwareState, ModelSummary,
|
||||
NodeDecommissionPreview, NodeDecommissionRecordCount, NodeDecommissionResult, NodeState,
|
||||
ObservedValue, ProjectIntegration, ProjectSummary, SchedulerBudget, SystemMetadata,
|
||||
} from "./types";
|
||||
|
||||
type Theme = "dark" | "light" | "system";
|
||||
|
||||
const THEME_STORAGE_KEY = "modelforge.theme";
|
||||
const MOBILE_NAV_MEDIA = "(max-width: 860px)";
|
||||
|
||||
function readStoredTheme(): Theme {
|
||||
// Storage can throw outright in a private window or when site data is blocked, so a failure to
|
||||
// read a cosmetic preference must never stop the console from rendering.
|
||||
try {
|
||||
const stored = window.localStorage.getItem(THEME_STORAGE_KEY);
|
||||
if (stored === "dark" || stored === "light" || stored === "system") return stored;
|
||||
} catch { /* fall through to the default */ }
|
||||
return "system";
|
||||
}
|
||||
|
||||
function currentRoute(): string {
|
||||
const route = window.location.hash.replace("#/", "");
|
||||
if (route.startsWith("nodes/")) return route;
|
||||
return navigation.some((item) => item.key === route) ? route : "overview";
|
||||
}
|
||||
|
||||
function routeKey(route: string): RouteKey {
|
||||
return route.startsWith("nodes/") ? "nodes" : (route as RouteKey);
|
||||
}
|
||||
|
||||
function LoadingState() {
|
||||
return <div className="state-card"><span className="state-spinner" />Loading control-plane state…</div>;
|
||||
}
|
||||
|
||||
function EmptyState({ children }: { children: string }) {
|
||||
return <div className="state-card"><Database size={18} />{children}</div>;
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const [route, setRoute] = useState(currentRoute);
|
||||
const [models, setModels] = useState<ModelSummary[]>([]);
|
||||
const [projects, setProjects] = useState<ProjectSummary[]>([]);
|
||||
const [projectIntegrations, setProjectIntegrations] = useState<ProjectIntegration[]>([]);
|
||||
const [hardware, setHardware] = useState<HardwareState | null>(null);
|
||||
const [deployments, setDeployments] = useState<CapabilityDeployment[]>([]);
|
||||
const [budgets, setBudgets] = useState<SchedulerBudget[]>([]);
|
||||
const [deploymentsLoaded, setDeploymentsLoaded] = useState(false);
|
||||
const [budgetsLoaded, setBudgetsLoaded] = useState(false);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [system, setSystem] = useState<SystemMetadata | null>(null);
|
||||
const [lastUpdated, setLastUpdated] = useState<Date | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [pollError, setPollError] = useState<string | null>(null);
|
||||
const [operatorUnlocked, setOperatorUnlocked] = useState(false);
|
||||
const [operatorDraft, setOperatorDraft] = useState("");
|
||||
const [unlocking, setUnlocking] = useState(false);
|
||||
const [publicHealth, setPublicHealth] = useState<PublicHealth | null>(null);
|
||||
const [publicRelease, setPublicRelease] = useState<PublicRelease | null>(null);
|
||||
const [theme, setTheme] = useState<Theme>(readStoredTheme);
|
||||
const [navOpen, setNavOpen] = useState(false);
|
||||
const [paletteOpen, setPaletteOpen] = useState(false);
|
||||
const menuButtonRef = useRef<HTMLButtonElement>(null);
|
||||
const closeButtonRef = useRef<HTMLButtonElement>(null);
|
||||
const sidebarRef = useRef<HTMLElement>(null);
|
||||
const navWasOpen = useRef(false);
|
||||
const navOpenRef = useRef(false);
|
||||
const closeFocusTarget = useRef<"menu" | "active-route">("menu");
|
||||
|
||||
const loadControlPlane = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
await Promise.all([api.system(), api.models(), api.projects(), api.projectIntegrations(), api.hardware()])
|
||||
.then(async ([systemData, modelData, projectData, integrationData, hardwareData]) => {
|
||||
setSystem(systemData);
|
||||
setModels(modelData.items);
|
||||
setProjects(projectData);
|
||||
setProjectIntegrations(integrationData);
|
||||
setHardware(hardwareData);
|
||||
const [deploymentResult, budgetResult] = await Promise.allSettled([
|
||||
api.capabilityDeployments(), api.scheduler(),
|
||||
]);
|
||||
setDeploymentsLoaded(deploymentResult.status === "fulfilled" && Array.isArray(deploymentResult.value));
|
||||
setBudgetsLoaded(budgetResult.status === "fulfilled" && Array.isArray(budgetResult.value));
|
||||
if (deploymentResult.status === "fulfilled" && Array.isArray(deploymentResult.value)) setDeployments(deploymentResult.value);
|
||||
if (budgetResult.status === "fulfilled" && Array.isArray(budgetResult.value)) setBudgets(budgetResult.value);
|
||||
setLastUpdated(new Date());
|
||||
})
|
||||
.catch((cause: unknown) => setError(cause instanceof Error ? cause.message : "Unable to load ModelForge API"))
|
||||
.finally(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const onHashChange = () => setRoute(currentRoute());
|
||||
window.addEventListener("hashchange", onHashChange);
|
||||
Promise.allSettled([api.liveness(), api.version()]).then(([healthResult, releaseResult]) => {
|
||||
if (healthResult.status === "fulfilled") setPublicHealth(healthResult.value);
|
||||
if (releaseResult.status === "fulfilled") setPublicRelease(releaseResult.value);
|
||||
});
|
||||
return () => window.removeEventListener("hashchange", onHashChange);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!operatorUnlocked) return;
|
||||
void loadControlPlane();
|
||||
}, [loadControlPlane, operatorUnlocked]);
|
||||
|
||||
useEffect(() => onOperatorSessionInvalidated(() => {
|
||||
lockOperatorSession();
|
||||
setError("Operator session is no longer valid; unlock again to continue");
|
||||
}), []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!operatorUnlocked) return undefined;
|
||||
const interval = window.setInterval(() => {
|
||||
api.hardware()
|
||||
.then((state) => { setHardware(state); setPollError(null); setLastUpdated(new Date()); })
|
||||
.catch((cause: unknown) => setPollError(cause instanceof Error ? cause.message : "Hardware polling failed"));
|
||||
}, 5000);
|
||||
return () => window.clearInterval(interval);
|
||||
}, [operatorUnlocked]);
|
||||
|
||||
useEffect(() => {
|
||||
const root = document.documentElement;
|
||||
if (theme === "system") root.removeAttribute("data-theme");
|
||||
else root.setAttribute("data-theme", theme);
|
||||
try { window.localStorage.setItem(THEME_STORAGE_KEY, theme); } catch { /* preference is not load-bearing */ }
|
||||
}, [theme]);
|
||||
|
||||
// Escape closes the mobile drawer; a drawer you can open but not close is a trap.
|
||||
useEffect(() => {
|
||||
if (!navOpen) return undefined;
|
||||
const onKeyDown = (event: KeyboardEvent) => { if (event.key === "Escape") setNavOpen(false); };
|
||||
window.addEventListener("keydown", onKeyDown);
|
||||
return () => window.removeEventListener("keydown", onKeyDown);
|
||||
}, [navOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
const onShortcut = (event: KeyboardEvent) => {
|
||||
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "k") {
|
||||
if (document.querySelector('[aria-modal="true"]')) return;
|
||||
event.preventDefault();
|
||||
setPaletteOpen((current) => !current);
|
||||
}
|
||||
};
|
||||
window.addEventListener("keydown", onShortcut);
|
||||
return () => window.removeEventListener("keydown", onShortcut);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window.matchMedia !== "function") return undefined;
|
||||
const mobile = window.matchMedia(MOBILE_NAV_MEDIA);
|
||||
const closeAfterResize = (event: MediaQueryListEvent) => {
|
||||
if (!event.matches && navOpenRef.current) {
|
||||
closeFocusTarget.current = "active-route";
|
||||
setNavOpen(false);
|
||||
}
|
||||
};
|
||||
mobile.addEventListener("change", closeAfterResize);
|
||||
return () => mobile.removeEventListener("change", closeAfterResize);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
navOpenRef.current = navOpen;
|
||||
if (navOpen) {
|
||||
navWasOpen.current = true;
|
||||
closeButtonRef.current?.focus();
|
||||
} else if (navWasOpen.current) {
|
||||
navWasOpen.current = false;
|
||||
if (closeFocusTarget.current === "active-route") {
|
||||
sidebarRef.current?.querySelector<HTMLButtonElement>('button[aria-current="page"]')?.focus();
|
||||
} else {
|
||||
menuButtonRef.current?.focus();
|
||||
}
|
||||
closeFocusTarget.current = "menu";
|
||||
}
|
||||
}, [navOpen]);
|
||||
|
||||
const activeRoute = routeKey(route);
|
||||
const routeInfo = navigation.find((item) => item.key === activeRoute)!;
|
||||
const detailNode = route.startsWith("nodes/")
|
||||
? hardware?.nodes.find((node) => node.id === route.slice(6))
|
||||
: undefined;
|
||||
|
||||
const navigate = useCallback((key: string) => {
|
||||
window.location.hash = `#/${key}`;
|
||||
setRoute(key);
|
||||
setNavOpen(false);
|
||||
}, []);
|
||||
|
||||
async function refreshHardware() {
|
||||
if (refreshing) return;
|
||||
setRefreshing(true);
|
||||
setError(null);
|
||||
try { setHardware(await api.refreshHardware()); }
|
||||
catch (cause: unknown) { setError(cause instanceof Error ? cause.message : "Hardware refresh failed"); }
|
||||
finally { setRefreshing(false); }
|
||||
}
|
||||
|
||||
async function refreshDashboard() {
|
||||
if (refreshing) return;
|
||||
setRefreshing(true);
|
||||
setError(null);
|
||||
try {
|
||||
const [hardwareResult, deploymentResult, budgetResult] = await Promise.allSettled([
|
||||
api.hardware(), api.capabilityDeployments(), api.scheduler(),
|
||||
]);
|
||||
if (hardwareResult.status === "fulfilled") setHardware(hardwareResult.value);
|
||||
setDeploymentsLoaded(deploymentResult.status === "fulfilled" && Array.isArray(deploymentResult.value));
|
||||
setBudgetsLoaded(budgetResult.status === "fulfilled" && Array.isArray(budgetResult.value));
|
||||
if (deploymentResult.status === "fulfilled" && Array.isArray(deploymentResult.value)) setDeployments(deploymentResult.value);
|
||||
if (budgetResult.status === "fulfilled" && Array.isArray(budgetResult.value)) setBudgets(budgetResult.value);
|
||||
const failures = [hardwareResult, deploymentResult, budgetResult].filter((result) => result.status === "rejected");
|
||||
setPollError(failures.length ? `${failures.length} command-center evidence source${failures.length === 1 ? "" : "s"} unavailable` : null);
|
||||
setLastUpdated(new Date());
|
||||
} finally { setRefreshing(false); }
|
||||
}
|
||||
|
||||
function cycleTheme() {
|
||||
setTheme((current) => (current === "system" ? "dark" : current === "dark" ? "light" : "system"));
|
||||
}
|
||||
|
||||
async function unlockOperatorSession(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
if (!operatorDraft || unlocking) return;
|
||||
setUnlocking(true);
|
||||
setError(null);
|
||||
try {
|
||||
const verifiedSystem = await api.verifyOperatorCredential(operatorDraft);
|
||||
setOperatorCredential(operatorDraft);
|
||||
setOperatorDraft("");
|
||||
setSystem(verifiedSystem);
|
||||
setOperatorUnlocked(true);
|
||||
} catch (cause: unknown) {
|
||||
clearOperatorCredential();
|
||||
setError(cause instanceof Error ? cause.message : "Operator authentication failed");
|
||||
} finally {
|
||||
setUnlocking(false);
|
||||
}
|
||||
}
|
||||
|
||||
function lockOperatorSession() {
|
||||
clearOperatorCredential();
|
||||
setOperatorDraft("");
|
||||
setOperatorUnlocked(false);
|
||||
setModels([]);
|
||||
setProjects([]);
|
||||
setProjectIntegrations([]);
|
||||
setHardware(null);
|
||||
setDeployments([]);
|
||||
setBudgets([]);
|
||||
setDeploymentsLoaded(false);
|
||||
setBudgetsLoaded(false);
|
||||
setSystem(null);
|
||||
setError(null);
|
||||
setPollError(null);
|
||||
setLastUpdated(null);
|
||||
}
|
||||
|
||||
const title = detailNode?.display_name ?? routeInfo.label;
|
||||
const description = detailNode
|
||||
? `Observed inventory and telemetry reported by this node's agent.`
|
||||
: routeInfo.description;
|
||||
|
||||
if (!operatorUnlocked) {
|
||||
return <OperatorSessionLock
|
||||
credential={operatorDraft}
|
||||
unlocking={unlocking}
|
||||
error={error}
|
||||
health={publicHealth}
|
||||
release={publicRelease}
|
||||
theme={theme}
|
||||
onCredential={setOperatorDraft}
|
||||
onSubmit={unlockOperatorSession}
|
||||
onTheme={cycleTheme}
|
||||
/>;
|
||||
}
|
||||
|
||||
return <div className="shell">
|
||||
<a className="skip-link" href="#workspace-content" onClick={(event) => {
|
||||
event.preventDefault();
|
||||
document.getElementById("workspace-content")?.focus();
|
||||
}}>Skip to workspace</a>
|
||||
<div className="mobile-bar" inert={navOpen ? true : undefined}>
|
||||
<button
|
||||
ref={menuButtonRef}
|
||||
onClick={() => setNavOpen(true)}
|
||||
aria-label="Open navigation"
|
||||
aria-expanded={navOpen}
|
||||
aria-controls="primary-sidebar"
|
||||
>
|
||||
<Menu size={17} />
|
||||
</button>
|
||||
<strong>{title}</strong>
|
||||
<ThemeButton theme={theme} onClick={cycleTheme} />
|
||||
</div>
|
||||
|
||||
{navOpen ? <div className="nav-scrim" aria-hidden="true" onClick={() => setNavOpen(false)} /> : null}
|
||||
|
||||
<aside
|
||||
ref={sidebarRef}
|
||||
id="primary-sidebar"
|
||||
className={navOpen ? "sidebar open" : "sidebar"}
|
||||
role={navOpen ? "dialog" : undefined}
|
||||
aria-modal={navOpen ? true : undefined}
|
||||
aria-label={navOpen ? "Primary navigation" : undefined}
|
||||
>
|
||||
<div className="brand">
|
||||
<div className="brand-mark"><Boxes size={18} /></div>
|
||||
<div className="brand-copy">
|
||||
<strong>ModelForge</strong>
|
||||
<span>Operator console</span>
|
||||
</div>
|
||||
{navOpen ? (
|
||||
<button
|
||||
ref={closeButtonRef}
|
||||
className="theme-toggle"
|
||||
onClick={() => setNavOpen(false)}
|
||||
aria-label="Close navigation"
|
||||
>
|
||||
<X size={15} />
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="nav-groups">
|
||||
{navigationGroups.map((group) => (
|
||||
<div className="nav-group" key={group}>
|
||||
<span className="nav-group-label">{group}</span>
|
||||
<nav aria-label={group}>
|
||||
{navigation.filter((item) => item.group === group).map((item) => (
|
||||
<button
|
||||
key={item.key}
|
||||
className={activeRoute === item.key ? "active" : ""}
|
||||
onClick={() => navigate(item.key)}
|
||||
aria-current={activeRoute === item.key ? "page" : undefined}
|
||||
>
|
||||
<item.icon size={16} />
|
||||
<span className="nav-copy"><strong>{item.label}</strong><small>{item.shortDescription}</small></span>
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="sidebar-footer">
|
||||
<span className={`status-dot ${error || pollError ? "error" : ""}`} />
|
||||
{error || pollError ? "Control plane unavailable" : `Control plane ${system?.version ?? "…"}`}
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main id="workspace-content" tabIndex={-1} inert={navOpen || paletteOpen ? true : undefined}>
|
||||
<div className="workspace-toolbar" role="toolbar" aria-label="Workspace controls">
|
||||
<div className="breadcrumbs"><span>ModelForge</span><span aria-hidden="true">/</span><strong>{routeInfo.group}</strong><span aria-hidden="true">/</span><span>{title}</span></div>
|
||||
<div className="workspace-actions">
|
||||
<button className="quick-jump" onClick={() => setPaletteOpen(true)}><Command size={14} /><span>Quick jump</span><kbd>Ctrl K</kbd></button>
|
||||
<button className="session-lock" onClick={lockOperatorSession}><ShieldCheck size={14} /><span>Lock console</span></button>
|
||||
<ThemeButton theme={theme} onClick={cycleTheme} />
|
||||
</div>
|
||||
</div>
|
||||
<header className="page-header">
|
||||
<div>
|
||||
<span className="eyebrow">{routeInfo.group.toUpperCase()}</span>
|
||||
<h1>{title}</h1>
|
||||
<p>{description}</p>
|
||||
</div>
|
||||
<span className="foundation-badge"><span className={`status-dot ${error || pollError ? "error" : ""}`} />{system?.environment ?? "control plane"} · v{system?.version ?? "…"}</span>
|
||||
</header>
|
||||
|
||||
{error ? (
|
||||
<div className="error-banner" role="alert">
|
||||
<strong>Operation failed</strong>
|
||||
<span>{error}. No operational state is being inferred.</span>
|
||||
</div>
|
||||
) : null}
|
||||
{pollError ? (
|
||||
<div className="error-banner" role="status">
|
||||
<strong>Live node updates paused</strong>
|
||||
<span>{pollError}. Last-good state is retained.</span>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{loading ? <LoadingState /> : null}
|
||||
{!loading && activeRoute === "overview" ? (
|
||||
<CommandCenter
|
||||
system={system}
|
||||
models={models}
|
||||
projects={projects}
|
||||
integrations={projectIntegrations}
|
||||
hardware={hardware}
|
||||
deployments={deployments}
|
||||
budgets={budgets}
|
||||
deploymentsLoaded={deploymentsLoaded}
|
||||
budgetsLoaded={budgetsLoaded}
|
||||
pollError={pollError}
|
||||
lastUpdated={lastUpdated}
|
||||
refreshing={refreshing}
|
||||
onRefresh={refreshDashboard}
|
||||
onNavigate={navigate}
|
||||
/>
|
||||
) : null}
|
||||
{!loading && activeRoute === "models" ? <ModelsRegistry /> : null}
|
||||
{!loading && activeRoute === "discover" ? <DiscoverWorkspace /> : null}
|
||||
{!loading && activeRoute === "runtime" ? <RuntimeWorkspace /> : null}
|
||||
{!loading && activeRoute === "capabilities" ? <CapabilityWorkspace onError={setError} /> : null}
|
||||
{!loading && activeRoute === "evaluation" ? <EvaluationWorkspace /> : null}
|
||||
{!loading && activeRoute === "recommendations" ? <AdvisorWorkspace /> : null}
|
||||
{!loading && activeRoute === "lifecycle" ? <LifecycleWorkspace /> : null}
|
||||
{!loading && activeRoute === "migrations" ? <MigrationWorkspace /> : null}
|
||||
{!loading && activeRoute === "operations" ? <OperationsWorkspace /> : null}
|
||||
{!loading && activeRoute === "recovery" ? <RecoveryWorkspace /> : null}
|
||||
{!loading && activeRoute === "projects" ? <Projects projects={projects} integrations={projectIntegrations} /> : null}
|
||||
{!loading && activeRoute === "nodes" && !route.startsWith("nodes/") ? (
|
||||
<Nodes
|
||||
hardware={hardware}
|
||||
refreshing={refreshing}
|
||||
onRefresh={refreshHardware}
|
||||
onSelect={(id) => navigate(`nodes/${id}`)}
|
||||
onError={setError}
|
||||
/>
|
||||
) : null}
|
||||
{!loading && route.startsWith("nodes/") ? (
|
||||
detailNode ? (
|
||||
<NodeDetail
|
||||
node={detailNode}
|
||||
onBack={() => navigate("nodes")}
|
||||
onChanged={async () => setHardware(await api.hardware())}
|
||||
onError={setError}
|
||||
/>
|
||||
) : <EmptyState>Compute node not found.</EmptyState>
|
||||
) : null}
|
||||
{!loading && !routeInfo.implemented ? <FutureRoute title={routeInfo.label} /> : null}
|
||||
</main>
|
||||
<CommandPalette open={paletteOpen} navigation={navigation} onClose={() => setPaletteOpen(false)} onNavigate={navigate} />
|
||||
</div>;
|
||||
}
|
||||
|
||||
function OperatorSessionLock({
|
||||
credential, unlocking, error, health, release, theme,
|
||||
onCredential, onSubmit, onTheme,
|
||||
}: {
|
||||
credential: string;
|
||||
unlocking: boolean;
|
||||
error: string | null;
|
||||
health: PublicHealth | null;
|
||||
release: PublicRelease | null;
|
||||
theme: Theme;
|
||||
onCredential: (value: string) => void;
|
||||
onSubmit: (event: FormEvent<HTMLFormElement>) => void;
|
||||
onTheme: () => void;
|
||||
}) {
|
||||
return <main className="operator-lock-shell">
|
||||
<section className="operator-lock-card" aria-labelledby="operator-lock-title">
|
||||
<div className="operator-lock-heading">
|
||||
<div className="brand-mark"><Boxes size={20} /></div>
|
||||
<div><span className="eyebrow">MODELFORGE CONTROL PLANE</span><h1 id="operator-lock-title">Operator session locked</h1></div>
|
||||
<ThemeButton theme={theme} onClick={onTheme} />
|
||||
</div>
|
||||
<p>Unlock once to load governed models, projects, hardware and operational evidence. The credential stays only in this page's memory and is cleared when you lock or reload the console.</p>
|
||||
<div className="operator-public-status" aria-label="Public platform status">
|
||||
<div><span>API liveness</span><strong>{health?.status ?? "checking"}</strong></div>
|
||||
<div><span>Release</span><strong>{release?.version ?? "checking"}</strong></div>
|
||||
</div>
|
||||
<form onSubmit={onSubmit}>
|
||||
<label htmlFor="operator-session-key">Operator API key</label>
|
||||
<input
|
||||
id="operator-session-key"
|
||||
name="operator-api-key"
|
||||
type="password"
|
||||
autoComplete="off"
|
||||
value={credential}
|
||||
onChange={(event) => onCredential(event.target.value)}
|
||||
autoFocus
|
||||
/>
|
||||
<button type="submit" disabled={!credential || unlocking}>
|
||||
<ShieldCheck size={16} />{unlocking ? "Verifying…" : "Unlock operator session"}
|
||||
</button>
|
||||
</form>
|
||||
{error ? <div className="error-banner" role="alert"><strong>Unlock denied</strong><span>{error}</span></div> : null}
|
||||
<small>No credential is written to localStorage, sessionStorage, a URL, telemetry or logs.</small>
|
||||
</section>
|
||||
</main>;
|
||||
}
|
||||
|
||||
function ThemeButton({ theme, onClick }: { theme: Theme; onClick: () => void }) {
|
||||
const Icon = theme === "dark" ? Moon : theme === "light" ? Sun : Monitor;
|
||||
const next = theme === "system" ? "dark" : theme === "dark" ? "light" : "system";
|
||||
return (
|
||||
<button className="theme-toggle" onClick={onClick} title={`Theme: ${theme}. Switch to ${next}.`} aria-label={`Theme: ${theme}. Switch to ${next}.`}>
|
||||
<Icon size={15} />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function Nodes({ hardware, refreshing, onRefresh, onSelect, onError }: {
|
||||
hardware: HardwareState | null; refreshing: boolean; onRefresh: () => void;
|
||||
onSelect: (id: string) => void; onError: (message: string | null) => void;
|
||||
}) {
|
||||
const [enrolling, setEnrolling] = useState(false);
|
||||
return <div className="hardware-stack">
|
||||
<div className="hardware-toolbar">
|
||||
<span>{hardware?.overview.node_count ?? 0} registered nodes · {hardware?.overview.accelerator_count ?? 0} accelerators</span>
|
||||
<div className="toolbar-actions">
|
||||
<button onClick={() => setEnrolling((value) => !value)}><Plus size={15} />Enroll node</button>
|
||||
<button onClick={onRefresh} disabled={refreshing}>
|
||||
<RefreshCw size={15} className={refreshing ? "spin" : ""} />{refreshing ? "Refreshing…" : "Refresh local"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{enrolling ? <EnrollmentPanel onClose={() => setEnrolling(false)} onError={onError} /> : null}
|
||||
{!hardware?.nodes.length ? (
|
||||
<EmptyState>No compute nodes are registered. Create an enrollment token to connect one.</EmptyState>
|
||||
) : (
|
||||
<section className="node-grid">
|
||||
{hardware.nodes.map((node) => {
|
||||
const gpu = node.accelerators[0];
|
||||
return (
|
||||
<button className="node-card panel" key={node.id} onClick={() => onSelect(node.id)}>
|
||||
<div className="panel-header">
|
||||
<div>
|
||||
<span className="eyebrow">{node.observation_source.replace("_", " ")}</span>
|
||||
<h2>{node.display_name}</h2>
|
||||
</div>
|
||||
<Liveness state={node.liveness} />
|
||||
</div>
|
||||
<div className="property-grid compact">
|
||||
<Property label="Hostname" value={node.hostname} />
|
||||
<Property label="Role" value={node.role ?? "unassigned"} />
|
||||
<Property label="Agent" value={node.agent_version ?? "not reported"} />
|
||||
<Property label="Last heartbeat" value={formatAge(node.last_heartbeat_at)} />
|
||||
<Property label="Inventory age" value={formatSeconds(node.inventory_age_seconds)} />
|
||||
<Property label="CPU / RAM" value={`${formatObserved(node.cpu_model)} · ${formatObservedBytes(node.total_ram_bytes)}`} />
|
||||
</div>
|
||||
<div className="node-card-footer">
|
||||
<span>{gpu ? `${gpu.name} · ${formatObservedBytes(gpu.total_vram_bytes)}` : "No accelerator"}</span>
|
||||
<span>{eligibility(node).join(" · ") || "not scheduler eligible"}</span>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
)}
|
||||
</div>;
|
||||
}
|
||||
|
||||
function EnrollmentPanel({ onClose, onError }: { onClose: () => void; onError: (message: string | null) => void }) {
|
||||
const sessionCredential = operatorCredentialReference();
|
||||
const [adminToken, setAdminToken] = useState(sessionCredential);
|
||||
const [displayName, setDisplayName] = useState("");
|
||||
const [role, setRole] = useState("worker");
|
||||
const [labelsJson, setLabelsJson] = useState("{}");
|
||||
const [productionEligible, setProductionEligible] = useState(false);
|
||||
const [labEligible, setLabEligible] = useState(true);
|
||||
const [benchmarkEligible, setBenchmarkEligible] = useState(false);
|
||||
const [result, setResult] = useState<EnrollmentCreated | null>(null);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
async function submit(event: FormEvent) {
|
||||
event.preventDefault();
|
||||
setSubmitting(true);
|
||||
onError(null);
|
||||
try {
|
||||
const labels = JSON.parse(labelsJson) as Record<string, string | boolean>;
|
||||
if (!labels || Array.isArray(labels) || typeof labels !== "object") throw new Error("Labels must be a JSON object");
|
||||
if (Object.values(labels).some((value) => typeof value !== "string" && typeof value !== "boolean")) {
|
||||
throw new Error("Label values must be strings or booleans");
|
||||
}
|
||||
setResult(await api.createEnrollment({
|
||||
expires_in_seconds: 900,
|
||||
display_name: displayName || undefined,
|
||||
role: role || undefined,
|
||||
labels,
|
||||
production_eligible: productionEligible,
|
||||
lab_eligible: labEligible,
|
||||
benchmark_eligible: benchmarkEligible,
|
||||
}, adminToken));
|
||||
setAdminToken("");
|
||||
} catch (cause: unknown) {
|
||||
onError(cause instanceof Error ? cause.message : "Enrollment creation failed");
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (result) {
|
||||
return <article className="panel enrollment-panel">
|
||||
<span className="eyebrow">ONE-TIME SECRET</span>
|
||||
<h2>Enrollment token created</h2>
|
||||
<p>Copy this now. ModelForge stores only its hash and will never display it again.</p>
|
||||
<code className="secret-output">{result.enrollment_token}</code>
|
||||
<pre>{Object.entries(result.setup_environment).map(([key, value]) => `${key}=${value}`).join("\n")}</pre>
|
||||
<small>
|
||||
Expires {new Date(result.expires_at).toLocaleString()} and becomes invalid after first use.
|
||||
Waiting for the first authenticated report; the node list updates automatically.
|
||||
</small>
|
||||
<button onClick={onClose}>Done</button>
|
||||
</article>;
|
||||
}
|
||||
|
||||
return <form className="panel enrollment-panel" onSubmit={submit}>
|
||||
<span className="eyebrow">SECURE ONBOARDING</span>
|
||||
<h2>Create one-time enrollment</h2>
|
||||
<p>The operator credential stays in memory for this request and is never persisted by the UI.</p>
|
||||
{!sessionCredential ? <label>Operator API key
|
||||
<input type="password" required value={adminToken} onChange={(event) => setAdminToken(event.target.value)} autoComplete="off" />
|
||||
</label> : null}
|
||||
<label>Display name
|
||||
<input value={displayName} onChange={(event) => setDisplayName(event.target.value)} placeholder="Inference server" />
|
||||
</label>
|
||||
<label>Role
|
||||
<input value={role} onChange={(event) => setRole(event.target.value)} placeholder="worker" />
|
||||
</label>
|
||||
<label>Labels (JSON)
|
||||
<input value={labelsJson} onChange={(event) => setLabelsJson(event.target.value)} />
|
||||
</label>
|
||||
<div className="eligibility-options">
|
||||
<label><input type="checkbox" checked={productionEligible} onChange={(event) => setProductionEligible(event.target.checked)} />Production eligible</label>
|
||||
<label><input type="checkbox" checked={labEligible} onChange={(event) => setLabEligible(event.target.checked)} />Lab eligible</label>
|
||||
<label><input type="checkbox" checked={benchmarkEligible} onChange={(event) => setBenchmarkEligible(event.target.checked)} />Benchmark eligible</label>
|
||||
</div>
|
||||
<div className="form-actions">
|
||||
<button type="button" onClick={onClose}>Cancel</button>
|
||||
<button type="submit" disabled={submitting}>{submitting ? "Creating…" : "Create token"}</button>
|
||||
</div>
|
||||
</form>;
|
||||
}
|
||||
|
||||
function NodeDetail({ node, onBack, onChanged, onError }: {
|
||||
node: NodeState; onBack: () => void; onChanged: () => Promise<void>;
|
||||
onError: (message: string | null) => void;
|
||||
}) {
|
||||
return <div className="hardware-stack">
|
||||
<div className="hardware-toolbar">
|
||||
<button onClick={onBack}>← All compute nodes</button>
|
||||
<Liveness state={node.liveness} />
|
||||
</div>
|
||||
{node.last_connection_error ? (
|
||||
<div className="error-banner"><strong>Agent report warning</strong><span>{node.last_connection_error}</span></div>
|
||||
) : null}
|
||||
<article className="panel hardware-node">
|
||||
<div className="panel-header">
|
||||
<div><span className="eyebrow">NODE ID {node.id}</span><h2>{node.hostname}</h2></div>
|
||||
<span className={`status-chip ${node.status}`}>{node.status}</span>
|
||||
</div>
|
||||
<div className="property-grid">
|
||||
<Property label="Source" value={node.observation_source.replaceAll("_", " ")} />
|
||||
<Property label="Environment" value={node.environment ?? "unknown"} />
|
||||
<Property label="OS / architecture" value={`${node.os_name ?? "unknown"} / ${node.architecture ?? "unknown"}`} />
|
||||
<Property label="Fingerprint" value={node.hardware_fingerprint ? `${node.hardware_fingerprint.slice(0, 16)}…` : "unknown"} />
|
||||
<Property label="Agent health" value={node.agent_health} />
|
||||
<Property label="Agent / protocol" value={`${node.agent_version ?? "unknown"} / v${node.protocol_version ?? "?"}`} />
|
||||
<Property label="Heartbeat" value={formatAge(node.last_heartbeat_at)} />
|
||||
<Property label="Inventory age" value={formatSeconds(node.inventory_age_seconds)} />
|
||||
<Property label="Telemetry age" value={formatSeconds(node.telemetry_age_seconds)} />
|
||||
<Property label="Role" value={node.role ?? "unassigned"} />
|
||||
<Property label="CPU" value={formatObserved(node.cpu_model)} />
|
||||
<Property label="RAM" value={`${formatObservedBytes(node.available_ram_bytes)} free / ${formatObservedBytes(node.total_ram_bytes)}`} />
|
||||
</div>
|
||||
<div className="tags">
|
||||
{Object.entries(node.labels).map(([key, value]) => <code key={key}>{key}={String(value)}</code>)}
|
||||
{eligibility(node).map((item) => <code key={item}>{item}</code>)}
|
||||
</div>
|
||||
<div className="accelerator-grid">
|
||||
{node.accelerators.length
|
||||
? node.accelerators.map((gpu) => <GpuCard key={gpu.id} gpu={gpu} />)
|
||||
: <EmptyState>No NVIDIA accelerator detected.</EmptyState>}
|
||||
</div>
|
||||
<div className="storage-list">
|
||||
{node.storage.map((disk) => (
|
||||
<div key={disk.id}>
|
||||
<strong>{disk.purpose}</strong>
|
||||
<code>{disk.path}</code>
|
||||
<span>{formatObservedBytes(disk.free_bytes)} free / {formatObservedBytes(disk.total_bytes)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</article>
|
||||
<NodeDecommissionPanel node={node} onChanged={onChanged} onError={onError} />
|
||||
</div>;
|
||||
}
|
||||
|
||||
function NodeDecommissionPanel({ node, onChanged, onError }: {
|
||||
node: NodeState; onChanged: () => Promise<void>; onError: (message: string | null) => void;
|
||||
}) {
|
||||
const sessionCredential = operatorCredentialReference();
|
||||
const [adminToken, setAdminToken] = useState(sessionCredential);
|
||||
const [operator, setOperator] = useState("");
|
||||
const [reason, setReason] = useState("");
|
||||
const [confirmation, setConfirmation] = useState("");
|
||||
const [preview, setPreview] = useState<NodeDecommissionPreview | null>(null);
|
||||
const [result, setResult] = useState<NodeDecommissionResult | null>(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [localError, setLocalError] = useState<string | null>(null);
|
||||
const [idempotencyKey] = useState(() => `node-decommission:${node.id}:${Date.now()}`);
|
||||
|
||||
async function loadPreview() {
|
||||
setBusy(true);
|
||||
setLocalError(null);
|
||||
onError(null);
|
||||
try {
|
||||
setPreview(await api.previewNodeDecommission(node.id, adminToken));
|
||||
setResult(null);
|
||||
} catch (cause: unknown) {
|
||||
setLocalError(cause instanceof Error ? cause.message : "Decommission preview failed");
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function execute() {
|
||||
if (!preview?.safe) return;
|
||||
setBusy(true);
|
||||
setLocalError(null);
|
||||
onError(null);
|
||||
try {
|
||||
const completed = await api.decommissionNode(node.id, {
|
||||
expected_generation: preview.node_generation,
|
||||
preview_digest: preview.dependency_digest,
|
||||
idempotency_key: idempotencyKey,
|
||||
operator,
|
||||
reason,
|
||||
confirmation,
|
||||
}, adminToken);
|
||||
setResult(completed);
|
||||
setAdminToken("");
|
||||
await onChanged();
|
||||
} catch (cause: unknown) {
|
||||
const message = cause instanceof Error ? cause.message : "Node decommission failed";
|
||||
setLocalError(message);
|
||||
if (cause instanceof ApiError && cause.code === "decommission_preview_stale") setPreview(null);
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (node.decommissioned_at || result) {
|
||||
return <section className="panel danger-zone decommission-complete" aria-labelledby="decommission-title">
|
||||
<span className="eyebrow">TERMINAL NODE TOMBSTONE</span>
|
||||
<h2 id="decommission-title">Node decommissioned</h2>
|
||||
<p>Old credentials are revoked and this persisted identity cannot automatically enroll, publish inventory, or return online.</p>
|
||||
<div className="property-grid compact">
|
||||
<Property label="When" value={new Date(result?.decommissioned_at ?? node.decommissioned_at!).toLocaleString()} />
|
||||
<Property label="Operator" value={(node.decommissioned_by ?? operator) || "recorded in audit"} />
|
||||
<Property label="Operation" value={result?.operation_id ?? "retained in authoritative history"} />
|
||||
<Property label="Reason" value={(node.decommission_reason ?? reason) || "retained in audit"} />
|
||||
</div>
|
||||
</section>;
|
||||
}
|
||||
|
||||
return <section className="panel danger-zone" aria-labelledby="decommission-title">
|
||||
<span className="eyebrow">DANGER ZONE · AUDITED LIFECYCLE</span>
|
||||
<h2 id="decommission-title">Decommission this node</h2>
|
||||
<p>This is not a delete. Preview first; execution fails closed if work, residency, leases, approvals, migrations, enrollment, or production safety still depends on the node.</p>
|
||||
{localError ? <div className="error-banner" role="alert"><strong>Decommission refused</strong><span>{localError}</span></div> : null}
|
||||
<div className="decommission-form">
|
||||
{!sessionCredential ? <label>Operator API key<input type="password" autoComplete="off" value={adminToken} onChange={(event) => setAdminToken(event.target.value)} /></label> : null}
|
||||
<label>Operator identity<input value={operator} onChange={(event) => setOperator(event.target.value)} placeholder="name or change owner" /></label>
|
||||
<label className="wide">Reason<textarea value={reason} onChange={(event) => setReason(event.target.value)} placeholder="Why this node is permanently leaving service" /></label>
|
||||
<button onClick={loadPreview} disabled={busy || !adminToken}>{busy ? "Checking…" : "Preview decommission"}</button>
|
||||
</div>
|
||||
{preview ? <div className="decommission-preview" aria-live="polite">
|
||||
<div className={`decommission-verdict ${preview.safe ? "safe" : "blocked"}`}>
|
||||
<strong>{preview.safe ? "Safe to decommission" : `${preview.blockers.length} safety blocker${preview.blockers.length === 1 ? "" : "s"}`}</strong>
|
||||
<span>Generation {preview.node_generation} · dependency digest {preview.dependency_digest.slice(0, 12)}…</span>
|
||||
</div>
|
||||
{preview.blockers.length ? <div className="decommission-blockers">
|
||||
<h3>Blocking dependencies</h3>
|
||||
{preview.blockers.map((blocker) => <article key={`${blocker.code}-${blocker.record_type}`}>
|
||||
<strong>{blocker.code.replaceAll("_", " ")}</strong>
|
||||
<span>{blocker.message}</span>
|
||||
<code>{blocker.record_type} · {blocker.count}</code>
|
||||
</article>)}
|
||||
</div> : null}
|
||||
<div className="decommission-columns">
|
||||
<DecommissionCounts title="Current state affected" rows={preview.cleanup} />
|
||||
<DecommissionCounts title="Historical evidence retained" rows={preview.preserved} />
|
||||
</div>
|
||||
{preview.safe ? <div className="decommission-confirmation">
|
||||
<label>Type <code>{preview.persisted_identity}</code>, <code>{preview.hostname}</code>, or <code>{preview.display_name}</code> to confirm
|
||||
<input value={confirmation} onChange={(event) => setConfirmation(event.target.value)} autoComplete="off" />
|
||||
</label>
|
||||
<button
|
||||
className="danger-action"
|
||||
onClick={execute}
|
||||
disabled={busy || operator.trim().length === 0 || reason.trim().length < 10 || ![preview.persisted_identity, preview.hostname, preview.display_name].includes(confirmation)}
|
||||
>{busy ? "Decommissioning…" : "Permanently decommission node"}</button>
|
||||
</div> : null}
|
||||
</div> : null}
|
||||
</section>;
|
||||
}
|
||||
|
||||
function DecommissionCounts({ title, rows }: { title: string; rows: NodeDecommissionRecordCount[] }) {
|
||||
return <div><h3>{title}</h3><dl>{rows.map((row) => <div key={`${row.record_type}-${row.action}`}>
|
||||
<dt>{row.record_type.replaceAll("_", " ")}</dt><dd>{row.count} · {row.action.replaceAll("_", " ")}</dd>
|
||||
</div>)}</dl></div>;
|
||||
}
|
||||
|
||||
function Liveness({ state }: { state: NodeState["liveness"] }) {
|
||||
return <span className={`liveness ${state}`}><i />{state}</span>;
|
||||
}
|
||||
|
||||
function eligibility(node: NodeState): string[] {
|
||||
return [node.production_eligible && "production", node.lab_eligible && "lab", node.benchmark_eligible && "benchmark"]
|
||||
.filter((value): value is string => Boolean(value));
|
||||
}
|
||||
|
||||
function formatAge(value?: string | null): string {
|
||||
if (!value) return "never";
|
||||
const seconds = Math.max(0, Math.round((Date.now() - new Date(value).getTime()) / 1000));
|
||||
return `${formatSeconds(seconds)} ago`;
|
||||
}
|
||||
|
||||
function formatSeconds(value?: number | null): string {
|
||||
if (value == null) return "unknown";
|
||||
if (value < 60) return `${value}s`;
|
||||
if (value < 3600) return `${Math.floor(value / 60)}m`;
|
||||
return `${Math.floor(value / 3600)}h`;
|
||||
}
|
||||
|
||||
function formatObserved<T>(observation?: ObservedValue<T> | null): string {
|
||||
if (!observation || observation.availability !== "known") {
|
||||
return observation?.availability?.replace("_", " ") ?? "unknown";
|
||||
}
|
||||
return String(observation.value);
|
||||
}
|
||||
|
||||
function formatObservedBytes(observation?: ObservedValue<number> | null): string {
|
||||
return observation?.availability === "known" && observation.value != null
|
||||
? `${(observation.value / 1024 ** 3).toFixed(1)} GB`
|
||||
: formatObserved(observation);
|
||||
}
|
||||
|
||||
function Meter({ value }: { value?: ObservedValue<number> | null }) {
|
||||
const percent = value?.availability === "known" && value.value != null
|
||||
? Math.max(0, Math.min(100, value.value))
|
||||
: null;
|
||||
return (
|
||||
<div className="meter" aria-label={percent == null ? formatObserved(value) : `${percent}%`}>
|
||||
<span style={{ width: percent == null ? "0%" : `${percent}%` }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Property({ label, value }: { label: string; value: string }) {
|
||||
return <div className="property"><span>{label}</span><strong>{value}</strong></div>;
|
||||
}
|
||||
|
||||
function GpuCard({ gpu }: { gpu: AcceleratorState }) {
|
||||
const compute = gpu.compute_capability_major.availability === "known" && gpu.compute_capability_minor.availability === "known"
|
||||
? `${gpu.compute_capability_major.value}.${gpu.compute_capability_minor.value}`
|
||||
: "unknown";
|
||||
return <div className="gpu-card">
|
||||
<div className="gpu-title">
|
||||
<div>
|
||||
<span className="eyebrow">NVIDIA ACCELERATOR</span>
|
||||
<h3>{gpu.name}</h3>
|
||||
<code>{gpu.device_uuid.length > 22 ? `${gpu.device_uuid.slice(0, 18)}…` : gpu.device_uuid}</code>
|
||||
</div>
|
||||
<span className={`status-chip ${gpu.status}`}>{gpu.status}</span>
|
||||
</div>
|
||||
<div className="gpu-meter-row">
|
||||
<span>GPU utilization</span>
|
||||
<strong>
|
||||
{gpu.telemetry
|
||||
? formatObserved(gpu.telemetry.gpu_utilization_percent) + (gpu.telemetry.gpu_utilization_percent.availability === "known" ? "%" : "")
|
||||
: "unknown"}
|
||||
</strong>
|
||||
</div>
|
||||
<Meter value={gpu.telemetry?.gpu_utilization_percent} />
|
||||
<div className="property-grid compact">
|
||||
<Property label="VRAM" value={`${formatObservedBytes(gpu.telemetry?.used_vram_bytes)} / ${formatObservedBytes(gpu.total_vram_bytes)}`} />
|
||||
<Property label="Temperature" value={gpu.telemetry?.temperature_c.availability === "known" ? `${gpu.telemetry.temperature_c.value} °C` : formatObserved(gpu.telemetry?.temperature_c)} />
|
||||
<Property label="Power" value={gpu.telemetry?.power_draw_w.availability === "known" ? `${gpu.telemetry.power_draw_w.value} W` : formatObserved(gpu.telemetry?.power_draw_w)} />
|
||||
<Property label="Driver" value={formatObserved(gpu.driver_version)} />
|
||||
<Property label="Compute" value={compute} />
|
||||
<Property label="PCI" value={formatObserved(gpu.pci_bus_id)} />
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
function ModelRows({ models }: { models: ModelSummary[] }) {
|
||||
if (!models.length) return <EmptyState>No candidate metadata has been registered.</EmptyState>;
|
||||
return <div className="model-list">
|
||||
{models.map((model) => {
|
||||
const capabilities = Array.isArray(model.interpretation_metadata.intended_capabilities)
|
||||
? model.interpretation_metadata.intended_capabilities.map(String)
|
||||
: [];
|
||||
return <div className="model-row" key={model.id}>
|
||||
<div className="model-icon"><Cpu size={17} /></div>
|
||||
<div className="model-copy">
|
||||
<strong>{model.display_name}</strong>
|
||||
<span>{String(model.interpretation_metadata.proposed_role ?? "No local role interpretation")}</span>
|
||||
<div className="tags">{capabilities.map((capability) => <code key={capability}>{capability}</code>)}</div>
|
||||
</div>
|
||||
<div className="model-meta">
|
||||
<span className="pill candidate">{model.verification_status}</span>
|
||||
<small>{model.deployment_status.replace("_", " ")}</small>
|
||||
</div>
|
||||
</div>;
|
||||
})}
|
||||
</div>;
|
||||
}
|
||||
|
||||
function Projects({ projects, integrations }: { projects: ProjectSummary[]; integrations: ProjectIntegration[] }) {
|
||||
if (!projects.length) return <EmptyState>No project manifests are registered.</EmptyState>;
|
||||
const activeIntegrations = integrations.filter((item) => item.state.toLowerCase() === "active").length;
|
||||
const requestVolume = integrations.reduce((sum, item) => sum + item.usage.request_volume, 0);
|
||||
return <div className="projects-workspace">
|
||||
<section className="projects-summary" aria-label="Project integration summary">
|
||||
<div><span>Registered projects</span><strong>{projects.length}</strong><small>Manifest-backed consumers</small></div>
|
||||
<div><span>Capability bindings</span><strong>{projects.reduce((sum, item) => sum + item.bindings.length, 0)}</strong><small>No concrete model paths</small></div>
|
||||
<div><span>Active integrations</span><strong>{activeIntegrations}/{integrations.length}</strong><small>{requestVolume.toLocaleString()} observed requests</small></div>
|
||||
</section>
|
||||
<section className="projects-list">
|
||||
{projects.map((project) => {
|
||||
const active = integrations.filter((item) => item.project_key === project.id);
|
||||
return <article className="panel project-workspace-card" key={project.id}>
|
||||
<header className="project-title-row"><div><span className="eyebrow">PROJECT · {project.id}</span><h2>{project.name}</h2><p>{project.description}</p></div><span className="project-count">{project.bindings.length} bindings · {active.length} integrations</span></header>
|
||||
<div className="project-columns">
|
||||
<section className="project-bindings" aria-label={`${project.name} capability bindings`}>
|
||||
<div className="section-label"><span>Declared capability contracts</span><small>Manifest source of truth</small></div>
|
||||
{project.bindings.map((binding) => <div className="project-binding-row" key={binding.capability}>
|
||||
<code>{binding.capability}@{binding.contract_version}</code>
|
||||
<span>{binding.binding.channel} · {binding.binding.priority}{binding.binding.optional ? " · optional" : ""}</span>
|
||||
</div>)}
|
||||
</section>
|
||||
<section className="project-integrations" aria-label={`${project.name} operational integrations`}>
|
||||
<div className="section-label"><span>Operational integrations</span><small>Credential identity, usage and project fit</small></div>
|
||||
{active.length ? active.map((item) => <article className="integration-row" key={item.client_id}>
|
||||
<div className="integration-title"><div><code>{item.capability}</code><small>{item.client_name} · {item.environment}</small></div><span className={`registry-status ${item.state.toLowerCase()}`}>{item.state}</span></div>
|
||||
<p>{item.purpose}</p>
|
||||
<div className="integration-metrics"><span><small>Requests</small><strong>{item.usage.request_volume.toLocaleString()}</strong></span><span><small>Success</small><strong>{item.usage.successful_requests.toLocaleString()}</strong></span><span><small>Errors</small><strong>{item.usage.error_count.toLocaleString()}</strong></span><span><small>p95</small><strong>{item.usage.latency_p95_ms == null ? "—" : `${item.usage.latency_p95_ms.toFixed(1)} ms`}</strong></span></div>
|
||||
<div className="project-fit-row"><span>Engineering <strong>{item.project_fit?.engineering_integration ?? "INCOMPLETE"}</strong></span><span>Fit <strong>{item.project_fit?.recommendation ?? "REQUIRES_MORE_EVIDENCE"}</strong></span><span>Production <strong>{item.project_fit?.production_validation ?? "REQUIRED"}</strong></span></div>
|
||||
</article>) : <div className="inline-empty"><ShieldCheck size={18} /><div><strong>Planning bindings only</strong><p>No scoped consumer identity has used ModelForge yet.</p></div></div>}
|
||||
</section>
|
||||
</div>
|
||||
</article>;
|
||||
})}
|
||||
</section>
|
||||
</div>;
|
||||
}
|
||||
|
||||
function FutureRoute({ title }: { title: string }) {
|
||||
return <div className="future-state">
|
||||
<FlaskConical size={28} />
|
||||
<span className="eyebrow">NOT AVAILABLE IN THIS RELEASE</span>
|
||||
<h2>{title} has no console workspace in this release</h2>
|
||||
<p>The route is reserved in the information architecture. ModelForge will not fabricate data or expose premature controls.</p>
|
||||
</div>;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { AdvisorWorkspace } from "./AdvisorWorkspace";
|
||||
|
||||
afterEach(() => { cleanup(); vi.unstubAllGlobals(); });
|
||||
|
||||
describe("M7 Advisor workspace", () => {
|
||||
it("explains a deterministic hard-blocked recommendation and no production action", async () => {
|
||||
vi.stubGlobal("fetch", vi.fn(() => Promise.resolve(new Response(JSON.stringify([{
|
||||
id: "recommendation-1", comparison_id: "comparison-1", project_id: "project-1",
|
||||
capability_contract_id: "contract-1", policy_id: "policy-1",
|
||||
candidate_key: "qwen-retrieval", current_embedding_space: "legacy-observed",
|
||||
candidate_embedding_space: "space-qwen", verdict: "KEEP_CURRENT", confidence: "HIGH",
|
||||
evidence_level: "A", quality_deltas: { recall_at_10: .09 }, latency_deltas: { p95_ms: 152 },
|
||||
resource_deltas: { resident_vram_bytes: 1500000000 },
|
||||
migration_impact: { class: "requires_reindex", chunks: 597 },
|
||||
security_state: { supply_chain_status: "verified" },
|
||||
key_improvements: ["recall_at_10 +0.090909"], blockers: ["critical_regression"],
|
||||
policy_snapshot: { critical_regression_hard_block: true },
|
||||
evidence_fingerprint: "a".repeat(64), status: "active",
|
||||
generated_at: "2026-08-25T15:00:00Z",
|
||||
}])))));
|
||||
render(<AdvisorWorkspace />);
|
||||
expect(await screen.findByText("KEEP CURRENT")).toBeInTheDocument();
|
||||
expect(screen.getByText("critical regression")).toBeInTheDocument();
|
||||
expect(screen.getByText(/Production action: none/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/HIGH CONFIDENCE/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { AlertTriangle, CheckCircle2, Database, Lightbulb } from "lucide-react";
|
||||
|
||||
import { api } from "../lib/api";
|
||||
import type { AdvisorRecommendation } from "../types";
|
||||
|
||||
export function AdvisorWorkspace() {
|
||||
const [items, setItems] = useState<AdvisorRecommendation[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
api.recommendations()
|
||||
.then(setItems)
|
||||
.catch((cause: unknown) => setError(
|
||||
cause instanceof Error ? cause.message : "Advisor evidence could not be loaded",
|
||||
))
|
||||
.finally(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
if (loading) return <div className="state-card"><span className="state-spinner" />Loading Advisor evidence…</div>;
|
||||
return <div className="evaluation-stack">
|
||||
<article className="panel evaluation-boundary"><Lightbulb size={24} /><div>
|
||||
<span className="eyebrow">M7 · DETERMINISTIC ADVISOR</span><h2>Project recommendations</h2>
|
||||
<p>Local project evidence outranks vendor claims. Hard gates can block aggregate improvements; no recommendation promotes automatically.</p>
|
||||
</div></article>
|
||||
{error ? <div className="error-banner"><strong>Advisor unavailable</strong><span>{error}</span></div> : null}
|
||||
{!items.length ? <div className="state-card"><Database size={18} />No evidence-backed recommendation is recorded.</div> :
|
||||
<section className="advisor-grid">{items.map((item) => <RecommendationCard key={item.id} item={item} />)}</section>}
|
||||
</div>;
|
||||
}
|
||||
|
||||
function RecommendationCard({ item }: { item: AdvisorRecommendation }) {
|
||||
const eligible = item.verdict === "PROMOTION_ELIGIBLE";
|
||||
return <article className="panel advisor-card">
|
||||
<div className="evaluation-title">{eligible ? <CheckCircle2 size={20} /> : <AlertTriangle size={20} />}
|
||||
<div><span className="eyebrow">EVIDENCE {item.evidence_level} · {item.confidence} CONFIDENCE</span>
|
||||
<h3>{item.candidate_key}</h3></div><span className={`status-chip ${item.verdict.toLowerCase()}`}>{item.verdict.replaceAll("_", " ")}</span>
|
||||
</div>
|
||||
<dl className="evaluation-facts"><div><dt>Current space</dt><dd>{item.current_embedding_space}</dd></div>
|
||||
<div><dt>Candidate space</dt><dd>{item.candidate_embedding_space ?? "unknown"}</dd></div>
|
||||
<div><dt>Status</dt><dd>{item.status}</dd></div><div><dt>Generated</dt><dd>{new Date(item.generated_at).toLocaleString()}</dd></div></dl>
|
||||
<div className="advisor-evidence"><section><strong>Key improvements</strong>
|
||||
{item.key_improvements.length ? <ul>{item.key_improvements.map((value) => <li key={value}>{value}</li>)}</ul> : <p>No qualifying metric improvement.</p>}
|
||||
</section><section><strong>Hard blockers</strong>
|
||||
{item.blockers.length ? <ul>{item.blockers.map((value) => <li key={value}>{value.replaceAll("_", " ")}</li>)}</ul> : <p>None.</p>}
|
||||
</section></div>
|
||||
<details><summary>Evidence and migration impact</summary><pre>{JSON.stringify({
|
||||
quality_deltas: item.quality_deltas,
|
||||
latency_deltas: item.latency_deltas,
|
||||
resource_deltas: item.resource_deltas,
|
||||
migration_impact: item.migration_impact,
|
||||
security_state: item.security_state,
|
||||
policy: item.policy_snapshot,
|
||||
}, null, 2)}</pre></details>
|
||||
<p><AlertTriangle size={15} /> Production action: none.</p>
|
||||
</article>;
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { CapabilityWorkspace } from "./CapabilityWorkspace";
|
||||
import type { CapabilityDeployment, SchedulerBudget } from "../types";
|
||||
|
||||
const deployment: CapabilityDeployment = {
|
||||
id: "deployment-1", capability: "rag.embedding", contract_version: 1,
|
||||
deployment_candidate_id: "candidate-1", production_approval_id: "approval-1",
|
||||
artifact_set_id: "artifact-set-1", runtime_profile_id: "profile-1",
|
||||
compute_node_id: "node-1", accelerator_id: "gpu-1",
|
||||
embedding_space: { id: "space-1", identity_digest: "a".repeat(64), dimension: 1024, normalized: true, migration_class: "requires_reindex", identity_facts: {}, created_at: "2026-08-25T12:00:00Z" },
|
||||
resource_envelope: { id: "envelope-1", runtime_probe_id: "probe-1", accelerator_kind: "NVIDIA RTX 4080 SUPER", accelerator_uuid: "GPU-test", environment_fingerprint: "b".repeat(64), concurrency: 1, batch_size: 8, max_sequence_length: 8192, baseline_vram_bytes: 1_100_000_000, resident_vram_bytes: 1_500_000_000, peak_vram_bytes: 2_700_000_000, required_vram_bytes: 1_600_000_000, cold_load_time_ms: 3690, inference_latency_ms: 1203, stale: false },
|
||||
residency: { id: "residency-1", state: "warm", worker_instance_id: "worker-1", load_count: 1, active_requests: 0, measured_resident_vram_bytes: 1_500_000_000, external_baseline_vram_bytes: 636_000_000, health: { functional: true }, resident_since: "2026-08-25T12:00:00Z", last_used_at: "2026-08-25T12:01:00Z" },
|
||||
channel: "stable", status: "stable", production: true, health_status: "healthy",
|
||||
routing_weight: 100, fallback_policy: {}, residency_policy: "keep_warm",
|
||||
keep_warm_seconds: 900, max_concurrency: 1, max_queue_depth: 16,
|
||||
config_fingerprint: "c".repeat(64), provenance: { runtime_probe_id: "probe-1", compatibility_assessment_id: "assessment-1" },
|
||||
rollback_policy: { mode: "drain_to_unavailable" }, promoted_at: "2026-08-25T12:00:00Z", created_at: "2026-08-25T12:00:00Z",
|
||||
};
|
||||
|
||||
const budget: SchedulerBudget = {
|
||||
compute_node_id: "node-1", node_name: "GPU Node", accelerator_id: "gpu-1",
|
||||
accelerator_uuid: "GPU-test", accelerator_name: "NVIDIA RTX 4080 SUPER",
|
||||
total_vram_bytes: 17_171_480_576, observed_used_vram_bytes: 2_200_000_000,
|
||||
external_vram_bytes: 700_000_000, resident_vram_bytes: 1_500_000_000,
|
||||
leased_vram_bytes: 0, safety_reserve_bytes: 1_073_741_824,
|
||||
schedulable_free_vram_bytes: 13_897_738_752, pressure: false, observed_at: "2026-08-25T12:00:00Z",
|
||||
pressure_state: "NORMAL", attribution_confidence: "KNOWN", invariant_delta_bytes: 0,
|
||||
policy_revision: "m10-v1",
|
||||
};
|
||||
|
||||
function mockServing(options: { deployments?: CapabilityDeployment[]; invokeError?: boolean; calls?: string[] } = {}): void {
|
||||
vi.stubGlobal("fetch", vi.fn((input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const url = String(input);
|
||||
let payload: unknown = [];
|
||||
let status = 200;
|
||||
if (url.endsWith("/api/v1/capability-deployments")) payload = options.deployments ?? [deployment];
|
||||
if (url.endsWith("/api/v1/capability-estate")) payload = [
|
||||
{ capability: "rag.embedding", version: 1, category: "RAG", purpose: "Local retrieval", declared_stability: "stable", operational_state: options.deployments?.length === 0 ? "not_deployed" : "stable", current_deployment_id: "deployment-1", model: "Nomic", revision: "a".repeat(40), runtime: "sentence_transformers", node: "node-1", resource_class: "MEDIUM", measured_required_vram_bytes: 1_600_000_000, consumers: ["examplerag"], privacy_class: "confidential", evaluation_type: "retrieval", evaluation_state: "completed" },
|
||||
{ capability: "document.ocr", version: 1, category: "DOCUMENT", purpose: "Local OCR", declared_stability: "experimental", operational_state: "not_deployed", resource_class: "MEDIUM", consumers: ["examplerag"], privacy_class: "restricted", evaluation_type: "ocr", evaluation_state: "not_evaluated" },
|
||||
{ capability: "vision.embedding", version: 1, category: "VISION", purpose: "Visual retrieval", declared_stability: "experimental", operational_state: "not_deployed", resource_class: "HEAVY", consumers: ["examplevision"], privacy_class: "confidential", evaluation_type: "visual-retrieval", evaluation_state: "not_evaluated" },
|
||||
{ capability: "speech.transcription", version: 1, category: "AUDIO", purpose: "Local transcription", declared_stability: "experimental", operational_state: "not_deployed", resource_class: "HEAVY", consumers: [], privacy_class: "restricted", evaluation_type: "asr", evaluation_state: "not_evaluated" },
|
||||
];
|
||||
if (url.endsWith("/api/v1/scheduler")) payload = [budget];
|
||||
if (url.endsWith("/api/v1/deployment-candidates")) payload = [];
|
||||
if (url.includes("/api/v1/gateway/requests")) payload = [{ request_id: "request-1", capability: "rag.embedding@1", client: "acceptance", status: "completed", cold: false, queue_time_ms: 1, total_latency_ms: 20, created_at: "2026-08-25T12:00:00Z" }];
|
||||
if (url.endsWith("/api/v1/admin/production-approvals")) payload = [];
|
||||
if (url.includes("/api/v1/admin/scheduler/placements")) payload = [];
|
||||
if (url.endsWith("/api/v1/admin/scheduler/policy")) payload = { revision: "m10-v1", active: true, configuration: { lab_paused: false }, created_at: "2026-08-25T12:00:00Z" };
|
||||
if (url.includes("/unload")) { options.calls?.push(url); payload = { unloaded: true }; }
|
||||
if (url.endsWith("/api/v1/admin/service-clients") && init?.method !== "POST") payload = [];
|
||||
if (url.endsWith("/api/v1/admin/service-clients") && init?.method === "POST") payload = { id: "client-1", name: "modelforge-m5-acceptance", status: "active", allowed_capabilities: ["rag.embedding@1"], requests_per_minute: 60, max_concurrent_requests: 1, credential_prefix: "mf_test", created_at: "2026-08-25T12:00:00Z", credential: "mf_test_one_time_secret" };
|
||||
if (url.endsWith("/api/v1/capabilities/rag.embedding@1/invoke")) {
|
||||
if (options.invokeError) { status = 429; payload = { error: { message: "rate limit exceeded", details: { failure_code: "RATE_LIMITED" } } }; }
|
||||
else payload = { capability: "rag.embedding@1", dimension: 1024, normalized: true, embedding_space_id: "space-1", data: [Array(1024).fill(0.03125)], request_id: "request-2", execution: { cold: false, node: "GPU Node", residency: "warm", load_count: 1, timings: { gateway_ms: 1, queue_ms: 0, load_ms: 0, inference_ms: 12, total_ms: 14 } }, usage: { input_count: 1, input_tokens: 4 } };
|
||||
}
|
||||
return Promise.resolve(new Response(JSON.stringify(payload), { status }));
|
||||
}));
|
||||
}
|
||||
|
||||
afterEach(() => { cleanup(); vi.unstubAllGlobals(); });
|
||||
|
||||
describe("M5 capability operations UI", () => {
|
||||
it("shows the healthy stable capability and warm residency", async () => {
|
||||
mockServing(); render(<CapabilityWorkspace onError={vi.fn()} />);
|
||||
expect(await screen.findByText("rag.embedding@1")).toBeInTheDocument();
|
||||
expect(screen.getByText("healthy")).toBeInTheDocument();
|
||||
expect(screen.getAllByText("warm").length).toBeGreaterThan(0);
|
||||
expect(screen.getByText("requires_reindex")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows unavailable honestly when promotion has not happened", async () => {
|
||||
mockServing({ deployments: [] }); render(<CapabilityWorkspace onError={vi.fn()} />);
|
||||
expect(await screen.findByText("unavailable")).toBeInTheDocument();
|
||||
expect(screen.getByText(/No stable deployment exists/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders real scheduler budget categories including external usage", async () => {
|
||||
mockServing(); render(<CapabilityWorkspace onError={vi.fn()} />);
|
||||
fireEvent.click(await screen.findByRole("tab", { name: "scheduler" }));
|
||||
expect(screen.getByText(/GPU SCHEDULING COCKPIT · GPU Node/)).toBeInTheDocument();
|
||||
expect(screen.getByText("External / unmanaged")).toBeInTheDocument();
|
||||
expect(screen.getByText("Safety reserve")).toBeInTheDocument();
|
||||
expect(screen.getByText("Schedulable free")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows a newly created credential exactly in the one-time result", async () => {
|
||||
mockServing(); render(<CapabilityWorkspace onError={vi.fn()} />);
|
||||
fireEvent.click(await screen.findByRole("tab", { name: "clients" }));
|
||||
fireEvent.change(screen.getByLabelText("Operator API key"), { target: { value: "admin-test" } });
|
||||
fireEvent.click(screen.getByRole("button", { name: "Unlock operational view" }));
|
||||
fireEvent.click(await screen.findByRole("button", { name: /Create one-time credential/ }));
|
||||
expect(await screen.findByText("mf_test_one_time_secret")).toBeInTheDocument();
|
||||
expect(screen.getByText(/Only its hash is stored/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("invokes by capability without asking for a concrete model name", async () => {
|
||||
mockServing(); render(<CapabilityWorkspace onError={vi.fn()} />);
|
||||
fireEvent.click(await screen.findByRole("tab", { name: "playground" }));
|
||||
expect(screen.queryByLabelText(/model name/i)).not.toBeInTheDocument();
|
||||
expect(screen.getByRole("option", { name: "document.ocr@1" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("option", { name: "vision.embedding@1" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("option", { name: "speech.transcription@1" })).toBeInTheDocument();
|
||||
fireEvent.change(screen.getByLabelText("Service credential"), { target: { value: "client-secret" } });
|
||||
fireEvent.click(screen.getByRole("button", { name: "Invoke capability" }));
|
||||
expect(await screen.findByText("1 × 1024 finite values")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("surfaces typed gateway failures", async () => {
|
||||
mockServing({ invokeError: true }); render(<CapabilityWorkspace onError={vi.fn()} />);
|
||||
fireEvent.click(await screen.findByRole("tab", { name: "playground" }));
|
||||
fireEvent.change(screen.getByLabelText("Service credential"), { target: { value: "client-secret" } });
|
||||
fireEvent.click(screen.getByRole("button", { name: "Invoke capability" }));
|
||||
expect(await screen.findByText("rate limit exceeded (RATE_LIMITED)")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("M18 closure · capability tabs and guarded residency unload", () => {
|
||||
async function openScheduler(calls: string[]) {
|
||||
mockServing({ calls });
|
||||
render(<CapabilityWorkspace onError={vi.fn()} />);
|
||||
fireEvent.click(await screen.findByRole("tab", { name: "scheduler" }));
|
||||
fireEvent.change(screen.getByLabelText("Operator API key"), { target: { value: "admin-test" } });
|
||||
fireEvent.click(screen.getByRole("button", { name: "Unlock operational view" }));
|
||||
return await screen.findByRole("button", { name: "Unload idle" });
|
||||
}
|
||||
|
||||
it("pairs each capability tab with the panel it controls", async () => {
|
||||
mockServing(); render(<CapabilityWorkspace onError={vi.fn()} />);
|
||||
await screen.findByRole("tablist", { name: "Capability operations" });
|
||||
const selected = screen.getByRole("tab", { selected: true });
|
||||
const panel = screen.getByRole("tabpanel");
|
||||
expect(selected.getAttribute("aria-controls")).toBe(panel.getAttribute("id"));
|
||||
expect(panel.getAttribute("aria-labelledby")).toBe(selected.getAttribute("id"));
|
||||
const ids = screen.getAllByRole("tab").map((tab) => tab.getAttribute("id"));
|
||||
expect(new Set(ids).size).toBe(ids.length);
|
||||
});
|
||||
|
||||
it("guards residency unload with the console safety dialog instead of a native confirm", async () => {
|
||||
const confirmSpy = vi.spyOn(window, "confirm");
|
||||
const calls: string[] = [];
|
||||
fireEvent.click(await openScheduler(calls));
|
||||
const dialog = await screen.findByRole("alertdialog", { name: "Unload idle residency?" });
|
||||
expect(dialog).toBeInTheDocument();
|
||||
// The native dialog is gone for good: it cannot carry this impact evidence.
|
||||
expect(confirmSpy).not.toHaveBeenCalled();
|
||||
expect(screen.getByText("rag.embedding@1")).toBeInTheDocument();
|
||||
expect(screen.getByText("Resident VRAM released")).toBeInTheDocument();
|
||||
expect(calls).toHaveLength(0);
|
||||
confirmSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("performs no mutation when the guarded unload is cancelled", async () => {
|
||||
const calls: string[] = [];
|
||||
fireEvent.click(await openScheduler(calls));
|
||||
await screen.findByRole("alertdialog", { name: "Unload idle residency?" });
|
||||
fireEvent.click(screen.getByRole("button", { name: "Cancel" }));
|
||||
await waitFor(() => expect(screen.queryByRole("alertdialog")).not.toBeInTheDocument());
|
||||
expect(calls).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("requires acknowledgement and then unloads exactly once", async () => {
|
||||
const calls: string[] = [];
|
||||
fireEvent.click(await openScheduler(calls));
|
||||
await screen.findByRole("alertdialog", { name: "Unload idle residency?" });
|
||||
const confirm = screen.getByRole("button", { name: "Unload residency" });
|
||||
expect(confirm).toBeDisabled();
|
||||
fireEvent.click(screen.getByRole("checkbox", { name: /cold-load cost/ }));
|
||||
expect(confirm).toBeEnabled();
|
||||
fireEvent.click(confirm);
|
||||
fireEvent.click(confirm);
|
||||
expect(await screen.findByRole("status")).toHaveTextContent(/was unloaded/);
|
||||
// Exactly one mutation, even though confirm was clicked twice.
|
||||
expect(calls).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("dismisses the guarded unload with Escape", async () => {
|
||||
const calls: string[] = [];
|
||||
fireEvent.click(await openScheduler(calls));
|
||||
await screen.findByRole("alertdialog", { name: "Unload idle residency?" });
|
||||
fireEvent.keyDown(window, { key: "Escape" });
|
||||
await waitFor(() => expect(screen.queryByRole("alertdialog")).not.toBeInTheDocument());
|
||||
expect(calls).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,320 @@
|
||||
import { FormEvent, useCallback, useEffect, useState } from "react";
|
||||
import type { ReactNode } from "react";
|
||||
import { Activity, CircleGauge, Clock3, KeyRound, Play, RefreshCw, ShieldCheck } from "lucide-react";
|
||||
|
||||
import { api, ApiError, operatorCredentialReference } from "../lib/api";
|
||||
import { SafetyDialog } from "./SafetyDialog";
|
||||
import { TabList, TabPanel } from "./TabList";
|
||||
import type { TabDescriptor } from "./TabList";
|
||||
import type {
|
||||
CapabilityDeployment, CapabilityEstate, DeploymentCandidate, GatewayInvokeResponse,
|
||||
GatewayRequest, OCRInvokeResponse, PlacementPlan, ProductionApproval, SchedulerBudget, SchedulerPolicy, ServiceClient,
|
||||
ServiceClientCreated, SpeechTranscriptionResponse, VisionEmbeddingResponse,
|
||||
} from "../types";
|
||||
|
||||
type View = "overview" | "scheduler" | "requests" | "clients" | "promotion" | "playground";
|
||||
const capabilityViews: TabDescriptor<View>[] = [
|
||||
{ key: "overview", label: "overview" }, { key: "scheduler", label: "scheduler" },
|
||||
{ key: "requests", label: "requests" }, { key: "clients", label: "clients" },
|
||||
{ key: "promotion", label: "promotion" }, { key: "playground", label: "playground" },
|
||||
];
|
||||
|
||||
export function CapabilityWorkspace({ onError }: { onError: (message: string | null) => void }) {
|
||||
const sessionReference = operatorCredentialReference();
|
||||
const sessionActive = Boolean(sessionReference);
|
||||
const [view, setView] = useState<View>("overview");
|
||||
const [deployments, setDeployments] = useState<CapabilityDeployment[]>([]);
|
||||
const [estate, setEstate] = useState<CapabilityEstate[]>([]);
|
||||
const [budgets, setBudgets] = useState<SchedulerBudget[]>([]);
|
||||
const [candidates, setCandidates] = useState<DeploymentCandidate[]>([]);
|
||||
const [requests, setRequests] = useState<GatewayRequest[]>([]);
|
||||
const [clients, setClients] = useState<ServiceClient[]>([]);
|
||||
const [approvals, setApprovals] = useState<ProductionApproval[]>([]);
|
||||
const [plans, setPlans] = useState<PlacementPlan[]>([]);
|
||||
const [policy, setPolicy] = useState<SchedulerPolicy | null>(null);
|
||||
const [adminToken, setAdminToken] = useState(sessionReference);
|
||||
const [adminUnlocked, setAdminUnlocked] = useState(sessionActive);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const loadPublic = useCallback(async () => {
|
||||
const [nextDeployments, nextBudgets, nextCandidates, nextEstate] = await Promise.all([
|
||||
api.capabilityDeployments(), api.scheduler(), api.deploymentCandidates(), api.capabilityEstate(),
|
||||
]);
|
||||
setDeployments(nextDeployments);
|
||||
setBudgets(nextBudgets);
|
||||
setCandidates(nextCandidates);
|
||||
setEstate(nextEstate);
|
||||
}, []);
|
||||
|
||||
const loadAdmin = useCallback(async (token: string) => {
|
||||
if (!token && !sessionActive) return;
|
||||
const [nextRequests, nextClients, nextApprovals, nextPlans, nextPolicy] = await Promise.all([
|
||||
api.gatewayRequests(token), api.serviceClients(token), api.productionApprovals(token),
|
||||
api.placementHistory(token), api.schedulerPolicy(token),
|
||||
]);
|
||||
setRequests(nextRequests);
|
||||
setClients(nextClients);
|
||||
setApprovals(nextApprovals);
|
||||
setPlans(nextPlans);
|
||||
setPolicy(nextPolicy);
|
||||
}, [sessionActive]);
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([loadPublic(), sessionActive ? loadAdmin(sessionReference) : Promise.resolve()])
|
||||
.catch((cause: unknown) => onError(errorMessage(cause)))
|
||||
.finally(() => setLoading(false));
|
||||
}, [loadAdmin, loadPublic, onError, sessionActive, sessionReference]);
|
||||
|
||||
useEffect(() => {
|
||||
const interval = window.setInterval(() => {
|
||||
loadPublic().catch((cause: unknown) => onError(errorMessage(cause)));
|
||||
if (adminUnlocked) loadAdmin(adminToken).catch((cause: unknown) => onError(errorMessage(cause)));
|
||||
}, 5000);
|
||||
return () => window.clearInterval(interval);
|
||||
}, [adminToken, adminUnlocked, loadAdmin, loadPublic, onError]);
|
||||
|
||||
const deployment = deployments.find((item) =>
|
||||
item.capability === "rag.embedding" && item.contract_version === 1 && item.production && item.status === "stable",
|
||||
) ?? deployments.find((item) => item.capability === "rag.embedding" && item.contract_version === 1);
|
||||
const refresh = async () => {
|
||||
onError(null);
|
||||
try { await Promise.all([loadPublic(), loadAdmin(adminToken)]); }
|
||||
catch (cause: unknown) { onError(errorMessage(cause)); }
|
||||
};
|
||||
const authenticateAdmin = async (event: FormEvent) => {
|
||||
event.preventDefault(); onError(null);
|
||||
try { await loadAdmin(adminToken); setAdminUnlocked(true); }
|
||||
catch (cause: unknown) { onError(errorMessage(cause)); }
|
||||
};
|
||||
if (loading) return <div className="state-card"><span className="state-spinner" />Loading capability serving state…</div>;
|
||||
return <div className="capability-stack">
|
||||
<section className="capability-hero panel">
|
||||
<div><span className="eyebrow">MULTI-CAPABILITY ESTATE</span><h2>{estate.length} governed contracts</h2><p>Capability-first routing across RAG, document, vision and audio · no client-visible model names</p></div>
|
||||
<div className="capability-health"><span className={`status-chip ${deployment && ["healthy", "ready_on_demand"].includes(deployment.health_status) ? "active" : "degraded"}`}>{deployment?.health_status ?? "unavailable"}</span><strong>{deployment?.status ?? "not promoted"}</strong></div>
|
||||
</section>
|
||||
<div className="capability-toolbar">
|
||||
<TabList idPrefix="capability" label="Capability operations" className="capability-tabs" tabs={capabilityViews} active={view} onSelect={setView} />
|
||||
<button className="icon-action" aria-label="Refresh capability state" onClick={refresh}><RefreshCw size={15} /></button>
|
||||
</div>
|
||||
<TabPanel idPrefix="capability" active={view} className="capability-tabpanel">
|
||||
{view === "overview" ? <CapabilityOverview deployment={deployment} requests={requests} estate={estate} /> : null}
|
||||
{view === "scheduler" ? <><SchedulerView budgets={budgets} deployments={deployments} plans={plans} policy={policy} token={adminToken} unlocked={adminUnlocked} onChanged={refresh} onError={onError} /><AdminBoundary unlocked={adminUnlocked} token={adminToken} onToken={setAdminToken} onSubmit={authenticateAdmin}><></></AdminBoundary></> : null}
|
||||
{view === "requests" ? <AdminBoundary unlocked={adminUnlocked} token={adminToken} onToken={setAdminToken} onSubmit={authenticateAdmin}><RequestHistory requests={requests} /></AdminBoundary> : null}
|
||||
{view === "clients" ? <AdminBoundary unlocked={adminUnlocked} token={adminToken} onToken={setAdminToken} onSubmit={authenticateAdmin}><Clients clients={clients} token={adminToken} onChanged={() => loadAdmin(adminToken)} onError={onError} /></AdminBoundary> : null}
|
||||
{view === "promotion" ? <AdminBoundary unlocked={adminUnlocked} token={adminToken} onToken={setAdminToken} onSubmit={authenticateAdmin}><Promotion deployment={deployment} candidates={candidates} approvals={approvals} token={adminToken} onChanged={refresh} onError={onError} /></AdminBoundary> : null}
|
||||
{view === "playground" ? <Playground /> : null}
|
||||
</TabPanel>
|
||||
</div>;
|
||||
}
|
||||
|
||||
function CapabilityOverview({ deployment, requests, estate }: { deployment?: CapabilityDeployment; requests: GatewayRequest[]; estate: CapabilityEstate[] }) {
|
||||
if (!estate.length) return <div className="state-card">No capability contracts are registered.</div>;
|
||||
const successful = requests.filter((item) => item.status === "completed");
|
||||
const warmHits = successful.filter((item) => item.cold === false).length;
|
||||
return <>
|
||||
<section className="project-grid">{estate.map((item) => <article className="panel project-card" key={`${item.capability}@${item.version}`}><span className="eyebrow">{item.category} · {item.resource_class}</span><h2>{item.capability}@{item.version}</h2><p>{item.purpose}</p><div className="binding-list"><div><span>State</span><strong>{item.operational_state}</strong></div><div><span>Evaluation</span><strong>{item.evaluation_state}</strong></div><div><span>Privacy</span><strong>{item.privacy_class}</strong></div><div><span>Consumers</span><strong>{item.consumers.join(", ") || "none declared"}</strong></div></div></article>)}</section>
|
||||
{!deployment ? <div className="state-card">No stable deployment exists. New modality capabilities remain LAB-only.</div> : <>
|
||||
<section className="metrics-grid capability-metrics">
|
||||
<CapabilityMetric label="Channel" value={deployment.channel} hint={deployment.production ? "Production approved" : "Not production"} />
|
||||
<CapabilityMetric label="Residency" value={deployment.residency.state} hint={`${deployment.residency.load_count} measured loads`} />
|
||||
<CapabilityMetric label="GPU envelope" value={formatBytes(deployment.resource_envelope.required_vram_bytes)} hint="Measured, not estimated" />
|
||||
<CapabilityMetric label="Queue" value={String(Math.max(0, requests.filter((item) => item.status === "queued").length))} hint={`Bounded at ${deployment.max_queue_depth}`} />
|
||||
<CapabilityMetric label="Requests" value={String(requests.length)} hint="Bounded visible history" />
|
||||
<CapabilityMetric label="Warm hit rate" value={successful.length ? `${Math.round((warmHits / successful.length) * 100)}%` : "No data"} hint="Only completed visible requests" />
|
||||
</section>
|
||||
<section className="grid-two">
|
||||
<article className="panel evidence-list"><div className="panel-header"><div><span className="eyebrow">CONTRACT</span><h2>Backend-authoritative boundary</h2></div><ShieldCheck size={19} /></div><Evidence label="Input modality" value="text"/><Evidence label="Output modality" value="vector"/><Evidence label="Dimension / normalized" value={`${deployment.embedding_space.dimension} / ${deployment.embedding_space.normalized}`}/><Evidence label="Migration" value={deployment.embedding_space.migration_class}/><Evidence label="Retention" value="No request content persisted"/></article>
|
||||
<article className="panel evidence-list"><div className="panel-header"><div><span className="eyebrow">RESIDENCY</span><h2>Load-on-demand state</h2></div><Activity size={19} /></div><Evidence label="Policy" value={`${deployment.residency_policy} · ${deployment.keep_warm_seconds}s`}/><Evidence label="State" value={deployment.residency.state}/><Evidence label="Active requests" value={String(deployment.residency.active_requests)}/><Evidence label="Resident since" value={formatTime(deployment.residency.resident_since)}/><Evidence label="Embedding space" value={deployment.embedding_space.id}/></article>
|
||||
</section>
|
||||
</>}</>;
|
||||
}
|
||||
|
||||
function SchedulerView({ budgets, deployments, plans, policy, token, unlocked, onChanged, onError }: { budgets: SchedulerBudget[]; deployments: CapabilityDeployment[]; plans: PlacementPlan[]; policy: SchedulerPolicy | null; token: string; unlocked: boolean; onChanged: () => Promise<void>; onError: (message: string | null) => void }) {
|
||||
const [latest, setLatest] = useState<Record<string, PlacementPlan>>({});
|
||||
const [pendingUnload, setPendingUnload] = useState<CapabilityDeployment | null>(null);
|
||||
const [unloadAcknowledged, setUnloadAcknowledged] = useState(false);
|
||||
const [unloadPending, setUnloadPending] = useState(false);
|
||||
const [unloadError, setUnloadError] = useState<string | null>(null);
|
||||
const [unloadOutcome, setUnloadOutcome] = useState<string | null>(null);
|
||||
if (!budgets.length) return <div className="state-card">No accelerator telemetry is available for scheduling.</div>;
|
||||
const simulate = async (deploymentId: string) => {
|
||||
try { const plan = await api.dryRunPlacement(deploymentId, "interactive", token); setLatest((current) => ({ ...current, [deploymentId]: plan })); }
|
||||
catch (cause: unknown) { onError(errorMessage(cause)); }
|
||||
};
|
||||
const previewUnload = (deployment: CapabilityDeployment) => {
|
||||
setUnloadAcknowledged(false); setUnloadError(null); setUnloadOutcome(null); setPendingUnload(deployment);
|
||||
};
|
||||
const confirmUnload = async (deployment: CapabilityDeployment) => {
|
||||
// Guarded against a replayed handler so one acknowledgement can never evict residency twice.
|
||||
if (unloadPending) return;
|
||||
setUnloadPending(true); setUnloadError(null);
|
||||
try {
|
||||
await api.unloadDeployment(deployment.id, token);
|
||||
setPendingUnload(null); setUnloadAcknowledged(false);
|
||||
setUnloadOutcome(`Residency for ${deployment.capability}@${deployment.contract_version} was unloaded.`);
|
||||
await onChanged();
|
||||
} catch (cause: unknown) { setUnloadError(errorMessage(cause)); }
|
||||
finally { setUnloadPending(false); }
|
||||
};
|
||||
const changePolicy = async (deployment: CapabilityDeployment, value: string) => {
|
||||
try { await api.updateResidencyPolicy(deployment.id, value, token); await onChanged(); }
|
||||
catch (cause: unknown) { onError(errorMessage(cause)); }
|
||||
};
|
||||
const pauseLab = async () => {
|
||||
try { await api.updateSchedulerPolicy(!Boolean(policy?.configuration.lab_paused), token); await onChanged(); }
|
||||
catch (cause: unknown) { onError(errorMessage(cause)); }
|
||||
};
|
||||
return <div className="capability-stack"><section className="scheduler-grid">{budgets.map((budget) => {
|
||||
const segments = [
|
||||
["External / unmanaged", budget.external_vram_bytes], ["Safety reserve", budget.safety_reserve_bytes],
|
||||
["ModelForge resident", budget.resident_vram_bytes], ["Active leases", budget.leased_vram_bytes],
|
||||
["Schedulable free", budget.schedulable_free_vram_bytes],
|
||||
] as const;
|
||||
return <article className="panel scheduler-card" key={budget.accelerator_id}><div className="panel-header"><div><span className="eyebrow">GPU SCHEDULING COCKPIT · {budget.node_name}</span><h2>{budget.accelerator_name}</h2></div><span className={`status-chip ${budget.pressure_state === "NORMAL" ? "active" : "degraded"}`}>{budget.pressure_state}</span></div><div className="budget-total"><strong>{formatBytes(budget.total_vram_bytes)}</strong><span>Total VRAM · attribution {budget.attribution_confidence}</span></div><div className="budget-bar" aria-label="GPU capacity distribution">{segments.map(([label, bytes], index) => <span key={label} className={`budget-segment segment-${index}`} style={{ width: `${Math.min(100, (bytes / budget.total_vram_bytes) * 100)}%` }} title={`${label}: ${formatBytes(bytes)}`} />)}</div><div className="budget-legend">{segments.map(([label, bytes]) => <Evidence key={label} label={label} value={formatBytes(bytes)} />)}</div><small>Observed {formatBytes(budget.observed_used_vram_bytes)} · invariant delta {formatBytes(budget.invariant_delta_bytes)} · policy {budget.policy_revision} · telemetry {formatTime(budget.observed_at)}. External usage is observation-only and has no controls.</small></article>;
|
||||
})}</section>{unlocked ? <><article className="panel evidence-list"><div className="panel-header"><div><span className="eyebrow">RESIDENCY MAP · WHAT CAN RUN NOW?</span><h2>Managed capability placements</h2></div><button onClick={pauseLab}>{Boolean(policy?.configuration.lab_paused) ? "Resume LAB" : "Pause LAB"}</button></div>{deployments.map((item) => { const plan = latest[item.id]; return <div className="client-row" key={item.id}><div><strong>{item.capability}@{item.contract_version} · {item.residency.state.toUpperCase()}</strong><span>{item.production ? "production" : "lab"} · {formatBytes(item.residency.measured_resident_vram_bytes)} · {item.residency.active_requests} active · cold load {formatMs(item.resource_envelope.cold_load_time_ms)}</span>{plan ? <span>{plan.verdict} · {plan.reason_codes.join(", ")} · after {formatBytes(plan.headroom_after_bytes)}{plan.evictions.length ? ` · evict ${plan.evictions.map((entry) => entry.capability).join(", ")}` : ""}</span> : null}</div><div className="promotion-actions"><button onClick={() => simulate(item.id)}>Dry run</button><select aria-label={`Residency policy for ${item.capability}`} value={item.residency_policy} onChange={(event) => changePolicy(item, event.target.value)}><option value="always_warm">ALWAYS_WARM</option><option value="keep_warm">KEEP_WARM</option><option value="load_on_demand">LOAD_ON_DEMAND</option><option value="lab_only">LAB_ONLY</option></select><button className="danger" disabled={item.residency.active_requests > 0 || item.residency.state === "cold"} onClick={() => previewUnload(item)}>Unload idle</button></div></div>; })}</article><article className="panel evidence-list"><div className="panel-header"><div><span className="eyebrow">PLACEMENT HISTORY</span><h2>Recent bounded decisions</h2></div><CircleGauge size={19}/></div>{plans.slice(0, 20).map((plan) => <Evidence key={plan.id ?? plan.decision_fingerprint} label={`${plan.capability} · ${plan.verdict}`} value={`${plan.reason_codes.join(", ")} · ${formatBytes(plan.headroom_before_bytes)} → ${formatBytes(plan.headroom_after_bytes)} · ${formatTime(plan.created_at)}`}/>)}</article></> : null}
|
||||
{unloadOutcome ? <p className="field-success" role="status">{unloadOutcome}</p> : null}
|
||||
{pendingUnload ? <SafetyDialog
|
||||
idPrefix="residency-unload"
|
||||
eyebrow="DESTRUCTIVE ACTION PREVIEW"
|
||||
title="Unload idle residency?"
|
||||
subject={`${pendingUnload.capability}@${pendingUnload.contract_version}`}
|
||||
description="ModelForge will evict this deployment from accelerator memory. Serving is not withdrawn: the next request reloads the model and pays the cold-load cost. Residency is unloaded only while no request is active."
|
||||
facts={[
|
||||
{ label: "Channel", value: pendingUnload.production ? "production" : "lab" },
|
||||
{ label: "Residency state", value: pendingUnload.residency.state },
|
||||
{ label: "Active requests", value: String(pendingUnload.residency.active_requests) },
|
||||
{ label: "Resident VRAM released", value: formatBytes(pendingUnload.residency.measured_resident_vram_bytes) },
|
||||
{ label: "Next cold load", value: formatMs(pendingUnload.resource_envelope.cold_load_time_ms) },
|
||||
]}
|
||||
acknowledgement="I understand the next request for this capability will pay the cold-load cost."
|
||||
confirmLabel="Unload residency"
|
||||
acknowledged={unloadAcknowledged}
|
||||
onAcknowledgedChange={setUnloadAcknowledged}
|
||||
pending={unloadPending}
|
||||
errorTitle="Unload blocked"
|
||||
error={unloadError}
|
||||
closeLabel="Close residency unload preview"
|
||||
onCancel={() => setPendingUnload(null)}
|
||||
onConfirm={() => void confirmUnload(pendingUnload)}
|
||||
/> : null}</div>;
|
||||
}
|
||||
|
||||
function RequestHistory({ requests }: { requests: GatewayRequest[] }) {
|
||||
if (!requests.length) return <div className="state-card">No gateway request metadata exists yet. Input text is never displayed or persisted.</div>;
|
||||
return <article className="panel table-panel"><div className="panel-header"><div><span className="eyebrow">PRIVACY-PRESERVING HISTORY</span><h2>Gateway requests</h2></div><Clock3 size={19}/></div><div className="operation-table"><div className="table-head"><span>Request</span><span>Client</span><span>State</span><span>Path</span><span>Queue</span><span>Total</span><span>Time</span></div>{requests.map((request) => <div className="table-row" key={request.request_id}><code>{request.request_id.slice(0, 8)}…</code><span>{request.client ?? "rejected identity"}</span><span className={`registry-status ${request.status}`}>{request.failure_code ?? request.status}</span><span>{request.cold == null ? "—" : request.cold ? "cold" : "warm"}</span><span>{formatMs(request.queue_time_ms)}</span><span>{formatMs(request.total_latency_ms)}</span><span>{formatTime(request.created_at)}</span></div>)}</div></article>;
|
||||
}
|
||||
|
||||
function Clients({ clients, token, onChanged, onError }: { clients: ServiceClient[]; token: string; onChanged: () => Promise<void>; onError: (message: string | null) => void }) {
|
||||
const [name, setName] = useState("modelforge-m5-acceptance");
|
||||
const [rpm, setRpm] = useState(60);
|
||||
const [created, setCreated] = useState<ServiceClientCreated | null>(null);
|
||||
const submit = async (event: FormEvent) => {
|
||||
event.preventDefault(); onError(null);
|
||||
try { setCreated(await api.createServiceClient({ name, allowed_capabilities: ["rag.embedding@1"], requests_per_minute: rpm, max_concurrent_requests: 1 }, token)); await onChanged(); }
|
||||
catch (cause: unknown) { onError(errorMessage(cause)); }
|
||||
};
|
||||
const revoke = async (clientId: string) => {
|
||||
onError(null);
|
||||
try { await api.revokeServiceCredential(clientId, token); await onChanged(); }
|
||||
catch (cause: unknown) { onError(errorMessage(cause)); }
|
||||
};
|
||||
return <section className="grid-two"><form className="panel client-form" onSubmit={submit}><span className="eyebrow">SCOPED IDENTITY</span><h2>Create service client</h2><label>Name<input required pattern="[a-z][a-z0-9-]*" value={name} onChange={(event) => setName(event.target.value)} /></label><label>Requests per minute<input type="number" min="1" max="10000" value={rpm} onChange={(event) => setRpm(Number(event.target.value))} /></label><p>Scope is fixed to <code>rag.embedding@1</code>; concurrency starts conservatively at one.</p><button type="submit"><KeyRound size={15}/>Create one-time credential</button>{created ? <div className="one-time-secret"><strong>Copy this credential now</strong><code>{created.credential}</code><small>Only its hash is stored. It cannot be retrieved again.</small></div> : null}</form><article className="panel evidence-list"><div className="panel-header"><div><span className="eyebrow">CLIENTS</span><h2>Authorized consumers</h2></div><KeyRound size={19}/></div>{clients.length ? clients.map((client) => <div className="client-row" key={client.id}><div><strong>{client.name}</strong><span>{client.allowed_capabilities.join(", ")} · {client.requests_per_minute}/min · last used {formatTime(client.last_used_at)}</span></div><button className="danger" disabled={client.status !== "active"} onClick={() => revoke(client.id)}>{client.status === "active" ? "Revoke" : "Revoked"}</button></div>) : <p>No service clients created.</p>}</article></section>;
|
||||
}
|
||||
|
||||
function Promotion({ deployment, candidates, approvals, token, onChanged, onError }: { deployment?: CapabilityDeployment; candidates: DeploymentCandidate[]; approvals: ProductionApproval[]; token: string; onChanged: () => Promise<void>; onError: (message: string | null) => void }) {
|
||||
const candidate = candidates[0];
|
||||
const approval = approvals.find((item) => item.deployment_candidate_id === candidate?.id);
|
||||
const reviewItems = [
|
||||
["exact_revision_reviewed", "Exact upstream revision and resolved commit reviewed"],
|
||||
["artifact_hashes_reviewed", "Local artifact SHA-256 digests reviewed"],
|
||||
["safetensors_only", "Artifact set contains safetensors only"],
|
||||
["remote_code_required", "No remote repository code is required"],
|
||||
["pickle_present", "No pickle payload is present"],
|
||||
["scanner_evidence_reviewed", "Supply-chain scanner evidence reviewed"],
|
||||
["provenance_complete", "Artifact and derivation provenance is complete"],
|
||||
["runtime_offline_reviewed", "Offline runtime behavior reviewed"],
|
||||
["dependency_provenance_reviewed", "Runtime dependency provenance reviewed"],
|
||||
["license_reviewed", "License evidence and production use reviewed"],
|
||||
] as const;
|
||||
const [reviewed, setReviewed] = useState<Record<string, boolean>>({});
|
||||
const [licenseIdentifier, setLicenseIdentifier] = useState("");
|
||||
const reviewComplete = reviewItems.every(([key]) => reviewed[key]) && licenseIdentifier.trim().length > 0;
|
||||
const evidence = deployment ? [
|
||||
["Artifact integrity", "verified"], ["Supply chain", "production approved"],
|
||||
["Runtime probe", String(deployment.provenance.runtime_probe_id ?? "bound")],
|
||||
["Compatibility", String(deployment.provenance.compatibility_assessment_id ?? "bound")],
|
||||
["Resource envelope", `${formatBytes(deployment.resource_envelope.required_vram_bytes)} · ${deployment.resource_envelope.stale ? "stale" : "current"}`],
|
||||
["Node eligibility", "production eligible"], ["Capability contract", "rag.embedding@1"],
|
||||
["Migration class", deployment.embedding_space.migration_class], ["Production approval", deployment.production_approval_id],
|
||||
] : [];
|
||||
const approve = async () => {
|
||||
if (!candidate) return;
|
||||
onError(null);
|
||||
try {
|
||||
await api.approveProduction(candidate.id, { approved_by: "operator-ui", reason: "Exact production evidence explicitly reviewed and accepted by operator.", supply_chain_review: { exact_revision_reviewed: true, artifact_hashes_reviewed: true, safetensors_only: true, remote_code_required: false, pickle_present: false, scanner_evidence_reviewed: true, provenance_complete: true, runtime_offline_reviewed: true, dependency_provenance_reviewed: true, license_reviewed: true, license_identifier: licenseIdentifier.trim() }, deployment_config: { capability: "rag.embedding@1", max_concurrency: 1 } }, token);
|
||||
await onChanged();
|
||||
} catch (cause: unknown) { onError(errorMessage(cause)); }
|
||||
};
|
||||
const promote = async () => {
|
||||
if (!candidate || !approval) return;
|
||||
onError(null);
|
||||
try { await api.promoteCandidate(candidate.id, { production_approval_id: approval.id, residency_policy: "keep_warm", keep_warm_seconds: 900, max_concurrency: 1, max_queue_depth: 16, routing_weight: 100 }, token); await onChanged(); }
|
||||
catch (cause: unknown) { onError(errorMessage(cause)); }
|
||||
};
|
||||
return <article className="panel evidence-list"><div className="panel-header"><div><span className="eyebrow">AUDITED PRODUCTION GATE</span><h2>Promotion evidence</h2></div><ShieldCheck size={20}/></div>{deployment ? evidence.map(([label, value]) => <Evidence key={label} label={label} value={value}/>) : <><p>No stable deployment exists. Promotion remains two explicit, audited actions.</p><Evidence label="LAB_READY candidate" value={candidate?.id ?? "none"}/><Evidence label="Production approval" value={approval?.status ?? "required"}/>{candidate && !approval ? <details className="promotion-review" open><summary>Review the evidence being attested</summary><p>ModelForge records only claims the operator explicitly confirms below. Verify each claim in Registry, Runtime and Evaluation before approval.</p><div className="promotion-checklist">{reviewItems.map(([key, label]) => <label key={key}><input type="checkbox" checked={Boolean(reviewed[key])} onChange={(event) => setReviewed((current) => ({ ...current, [key]: event.target.checked }))}/><span>{label}</span></label>)}</div><label className="license-review">Reviewed license identifier<input value={licenseIdentifier} onChange={(event) => setLicenseIdentifier(event.target.value)} placeholder="For example: Apache-2.0" /></label></details> : null}<div className="promotion-actions"><button onClick={approve} disabled={!candidate || Boolean(approval) || !reviewComplete}>1. Record reviewed approval</button><button onClick={promote} disabled={!candidate || !approval}>2. Promote exact binding</button></div></>}</article>;
|
||||
}
|
||||
|
||||
type ModalityResult = GatewayInvokeResponse | OCRInvokeResponse | VisionEmbeddingResponse | SpeechTranscriptionResponse;
|
||||
|
||||
async function fileBase64(file: File): Promise<string> {
|
||||
const bytes = new Uint8Array(await file.arrayBuffer());
|
||||
let binary = "";
|
||||
for (const byte of bytes) binary += String.fromCharCode(byte);
|
||||
return btoa(binary);
|
||||
}
|
||||
|
||||
function Playground() {
|
||||
const [capability, setCapability] = useState<"rag.embedding" | "document.ocr" | "vision.embedding" | "speech.transcription">("rag.embedding");
|
||||
const [credential, setCredential] = useState("");
|
||||
const [input, setInput] = useState("ModelForge capability gateway test");
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const [result, setResult] = useState<ModalityResult | null>(null);
|
||||
const [failure, setFailure] = useState<string | null>(null);
|
||||
const [running, setRunning] = useState(false);
|
||||
const invoke = async (event: FormEvent) => {
|
||||
event.preventDefault(); setFailure(null); setRunning(true);
|
||||
try {
|
||||
if (capability === "rag.embedding") setResult(await api.invokeEmbedding(input, credential));
|
||||
else {
|
||||
if (!file) throw new Error("Choose a local fixture first");
|
||||
const encoded = await fileBase64(file);
|
||||
if (capability === "document.ocr") {
|
||||
if (file.type !== "image/png" && file.type !== "image/jpeg") throw new Error("OCR accepts PNG or JPEG");
|
||||
setResult(await api.invokeOcr(encoded, file.type, credential));
|
||||
} else if (capability === "vision.embedding") {
|
||||
if (file.type !== "image/png" && file.type !== "image/jpeg") throw new Error("Vision accepts PNG or JPEG");
|
||||
setResult(await api.invokeVisionEmbedding(encoded, file.type, credential));
|
||||
} else {
|
||||
if (file.type !== "audio/wav" && !file.name.toLowerCase().endsWith(".wav")) throw new Error("Transcription accepts WAV");
|
||||
setResult(await api.invokeTranscription(encoded, credential));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (cause: unknown) { setFailure(errorMessage(cause)); }
|
||||
finally { setRunning(false); }
|
||||
};
|
||||
const output = result ? ("data" in result ? `${result.data.length} × ${result.dimension} finite values` : "text" in result ? result.text || "valid empty text" : "typed output") : "";
|
||||
return <form className="panel playground" onSubmit={invoke}><div className="panel-header"><div><span className="eyebrow">REAL GATEWAY PATH</span><h2>Capability playground</h2></div><Play size={20}/></div><p>The client selects a capability contract. No model name, path, or runtime engine is accepted here.</p><label>Capability<select value={capability} onChange={(event) => { setCapability(event.target.value as typeof capability); setResult(null); }}><option value="rag.embedding">rag.embedding@1</option><option value="document.ocr">document.ocr@1</option><option value="vision.embedding">vision.embedding@1</option><option value="speech.transcription">speech.transcription@1</option></select></label><label>Service credential<input required type="password" autoComplete="off" value={credential} onChange={(event) => setCredential(event.target.value)} /></label>{capability === "rag.embedding" ? <label>Input text<textarea required maxLength={8192} value={input} onChange={(event) => setInput(event.target.value)} /></label> : <label>{capability === "speech.transcription" ? "WAV audio" : "PNG or JPEG image"}<input required type="file" accept={capability === "speech.transcription" ? ".wav,audio/wav" : "image/png,image/jpeg"} onChange={(event) => setFile(event.target.files?.[0] ?? null)} /></label>}<button disabled={running}>{running ? "Invoking…" : "Invoke capability"}</button>{failure ? <div className="error-banner"><strong>Typed gateway failure</strong><span>{failure}</span></div> : null}{result ? <div className="playground-result"><span className={`registry-status ${result.execution.cold ? "candidate" : "ready"}`}>{result.execution.cold ? "cold" : "warm"}</span><Evidence label="Request ID" value={result.request_id}/><Evidence label="Output" value={output}/>{"embedding_space_id" in result ? <Evidence label="Embedding space" value={result.embedding_space_id}/> : null}<Evidence label="Total / inference" value={`${formatMs(result.execution.timings.total_ms)} / ${formatMs(result.execution.timings.inference_ms)}`}/></div> : null}</form>;
|
||||
}
|
||||
|
||||
function AdminBoundary({ unlocked, token, onToken, onSubmit, children }: { unlocked: boolean; token: string; onToken: (value: string) => void; onSubmit: (event: FormEvent) => Promise<void>; children: ReactNode }) {
|
||||
if (unlocked) return <>{children}</>;
|
||||
return <form className="panel admin-boundary" onSubmit={onSubmit}><ShieldCheck size={22}/><div><h2>Operator authentication required</h2><p>Administrative data and actions require the operator credential. It remains in browser memory only.</p></div><label htmlFor="capability-operator-key">Operator API key</label><input id="capability-operator-key" name="operator-api-key" required type="password" autoComplete="off" value={token} onChange={(event) => onToken(event.target.value)} /><button type="submit">Unlock operational view</button></form>;
|
||||
}
|
||||
|
||||
function CapabilityMetric({ label, value, hint }: { label: string; value: string; hint: string }) { return <article className="metric-card"><span>{label}</span><strong>{value}</strong><small>{hint}</small></article>; }
|
||||
function Evidence({ label, value }: { label: string; value: string }) { return <div className="evidence-row"><span>{label}</span><strong title={value}>{value}</strong></div>; }
|
||||
function formatBytes(value: number): string { return `${(value / 1024 ** 3).toFixed(2)} GiB`; }
|
||||
function formatMs(value?: number | null): string { return value == null ? "—" : `${value.toFixed(1)} ms`; }
|
||||
function formatTime(value?: string | null): string { return value ? new Date(value).toLocaleString() : "never"; }
|
||||
function errorMessage(cause: unknown): string { return cause instanceof ApiError ? `${cause.message}${typeof cause.details.failure_code === "string" ? ` (${cause.details.failure_code})` : ""}` : cause instanceof Error ? cause.message : "Capability operation failed"; }
|
||||
@@ -0,0 +1,279 @@
|
||||
import { FormEvent, useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
AlertTriangle, ArrowRight, Boxes, CheckCircle2, Cpu, DatabaseBackup,
|
||||
Gauge, KeyRound, Network, RefreshCw, Server, ShieldCheck, Siren,
|
||||
} from "lucide-react";
|
||||
|
||||
import { api, operatorCredentialReference } from "../lib/api";
|
||||
import type {
|
||||
CapabilityDeployment, HardwareState, ModelSummary, OperationsOverview,
|
||||
ProjectIntegration, ProjectSummary, RecoveryDashboard, SchedulerBudget, SystemMetadata,
|
||||
} from "../types";
|
||||
|
||||
type Tone = "healthy" | "warning" | "critical" | "neutral";
|
||||
|
||||
function formatBytes(value: number): string {
|
||||
if (!Number.isFinite(value)) return "unknown";
|
||||
const units = ["B", "KB", "MB", "GB", "TB"];
|
||||
const sign = value < 0 ? "−" : "";
|
||||
let amount = Math.abs(value);
|
||||
let index = 0;
|
||||
while (amount >= 1024 && index < units.length - 1) { amount /= 1024; index += 1; }
|
||||
return `${sign}${amount >= 10 || index === 0 ? amount.toFixed(0) : amount.toFixed(1)} ${units[index]}`;
|
||||
}
|
||||
|
||||
function age(value?: string | null): string {
|
||||
if (!value) return "not observed";
|
||||
const parsed = Date.parse(value);
|
||||
if (!Number.isFinite(parsed)) return "invalid timestamp";
|
||||
const seconds = Math.max(0, Math.floor((Date.now() - parsed) / 1000));
|
||||
if (seconds < 60) return `${seconds}s ago`;
|
||||
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
|
||||
if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`;
|
||||
return `${Math.floor(seconds / 86400)}d ago`;
|
||||
}
|
||||
|
||||
function StatusMark({ tone, children }: { tone: Tone; children: React.ReactNode }) {
|
||||
return <span className={`command-status ${tone}`}><span aria-hidden="true" />{children}</span>;
|
||||
}
|
||||
|
||||
function Kpi({ icon: Icon, label, value, detail, tone = "neutral", onClick }: {
|
||||
icon: typeof Gauge; label: string; value: string; detail: string; tone?: Tone; onClick?: () => void;
|
||||
}) {
|
||||
const content = <>
|
||||
<span className={`kpi-icon ${tone}`}><Icon size={17} /></span>
|
||||
<span className="kpi-copy"><small>{label}</small><strong>{value}</strong><span>{detail}</span></span>
|
||||
{onClick ? <ArrowRight size={15} aria-hidden="true" /> : null}
|
||||
</>;
|
||||
return onClick
|
||||
? <button className="command-kpi actionable" onClick={onClick}>{content}</button>
|
||||
: <article className="command-kpi">{content}</article>;
|
||||
}
|
||||
|
||||
export function CommandCenter({
|
||||
system, models, projects, integrations, hardware, deployments, budgets, deploymentsLoaded, budgetsLoaded,
|
||||
pollError, lastUpdated, refreshing, onRefresh, onNavigate,
|
||||
}: {
|
||||
system: SystemMetadata | null;
|
||||
models: ModelSummary[];
|
||||
projects: ProjectSummary[];
|
||||
integrations: ProjectIntegration[];
|
||||
hardware: HardwareState | null;
|
||||
deployments: CapabilityDeployment[];
|
||||
budgets: SchedulerBudget[];
|
||||
deploymentsLoaded: boolean;
|
||||
budgetsLoaded: boolean;
|
||||
pollError: string | null;
|
||||
lastUpdated: Date | null;
|
||||
refreshing: boolean;
|
||||
onRefresh: () => void;
|
||||
onNavigate: (key: string) => void;
|
||||
}) {
|
||||
const sessionReference = operatorCredentialReference();
|
||||
const sessionActive = Boolean(sessionReference);
|
||||
const [operatorToken, setOperatorToken] = useState(sessionReference);
|
||||
const [assuranceLoading, setAssuranceLoading] = useState(false);
|
||||
const [assuranceError, setAssuranceError] = useState<string | null>(null);
|
||||
const [operations, setOperations] = useState<OperationsOverview | null>(null);
|
||||
const [recovery, setRecovery] = useState<RecoveryDashboard | null>(null);
|
||||
|
||||
const nodes = hardware?.nodes ?? [];
|
||||
const gpu_node = nodes.find((node) => node.role === "primary-inference" || node.production_eligible) ?? nodes[0];
|
||||
const onlineNodes = nodes.filter((node) => node.liveness === "online");
|
||||
const productionDeployments = deployments.filter((item) =>
|
||||
item.production && item.status.toLowerCase() === "stable",
|
||||
);
|
||||
const healthyDeployments = productionDeployments.filter((item) =>
|
||||
!["failed", "unavailable", "degraded"].includes(item.health_status.toLowerCase()),
|
||||
);
|
||||
const totalVram = budgets.reduce((sum, item) => sum + item.total_vram_bytes, 0);
|
||||
const headroom = budgets.reduce((sum, item) => sum + item.schedulable_free_vram_bytes, 0);
|
||||
const used = budgets.reduce((sum, item) => sum + item.observed_used_vram_bytes, 0);
|
||||
const reserve = budgets.reduce((sum, item) => sum + item.safety_reserve_bytes, 0);
|
||||
const pressure = budgets.some((item) => item.pressure || ["HIGH", "CRITICAL"].includes(item.pressure_state));
|
||||
const evidenceComplete = deploymentsLoaded && budgetsLoaded;
|
||||
const platformTone: Tone = pollError || !gpu_node || gpu_node.liveness !== "online"
|
||||
? "critical"
|
||||
: !evidenceComplete
|
||||
? "neutral"
|
||||
: pressure || healthyDeployments.length < productionDeployments.length
|
||||
? "warning"
|
||||
: "healthy";
|
||||
const activeAlerts = operations?.active_alerts.filter((item) => ["FIRING", "PENDING"].includes(item.state)) ?? [];
|
||||
|
||||
const actions = useMemo(() => {
|
||||
const result: Array<{ title: string; detail: string; route: string; tone: Tone }> = [];
|
||||
if (!gpu_node || gpu_node.liveness !== "online") result.push({
|
||||
title: "Restore production compute visibility",
|
||||
detail: gpu_node ? `${gpu_node.display_name} is ${gpu_node.liveness}; inspect its last heartbeat and agent evidence.` : "No production-eligible compute node is registered.",
|
||||
route: "nodes", tone: "critical",
|
||||
});
|
||||
if (!deploymentsLoaded || !budgetsLoaded) result.push({
|
||||
title: "Restore command-center evidence",
|
||||
detail: `${!deploymentsLoaded ? "Deployment state" : "Scheduler capacity"} could not be loaded; production readiness remains unknown.`,
|
||||
route: "operations", tone: "warning",
|
||||
});
|
||||
if (budgetsLoaded && pressure) result.push({
|
||||
title: "Review scheduler pressure",
|
||||
detail: `${formatBytes(headroom)} remains schedulable after reservations. Review admission before promoting workloads.`,
|
||||
route: "capabilities", tone: "warning",
|
||||
});
|
||||
if (deploymentsLoaded && !productionDeployments.length) result.push({
|
||||
title: "No production capability deployment",
|
||||
detail: "The gateway has no production deployment to route to. Review promotion evidence and routing state.",
|
||||
route: "capabilities", tone: "warning",
|
||||
});
|
||||
const unverified = models.filter((item) => item.verification_status !== "verified").length;
|
||||
if (unverified) result.push({
|
||||
title: `Resolve provenance for ${unverified} model${unverified === 1 ? "" : "s"}`,
|
||||
detail: "Unverified candidates remain isolated from production until artifact and supply-chain evidence is complete.",
|
||||
route: "models", tone: "neutral",
|
||||
});
|
||||
if (recovery?.stale_backup || recovery?.unprotected_assets.length) result.push({
|
||||
title: recovery.stale_backup ? "Refresh stale recovery evidence" : "Close recovery coverage gaps",
|
||||
detail: recovery.stale_backup
|
||||
? `The latest verified backup is ${age(recovery.latest_verified_backup_at)}.`
|
||||
: `${recovery.unprotected_assets.length} assets are not protected.`,
|
||||
route: "recovery", tone: "warning",
|
||||
});
|
||||
if (!operations && result.length < 3) result.push({
|
||||
title: "Load protected assurance evidence",
|
||||
detail: "Active alerts and recovery posture are loading inside the authenticated operator session.",
|
||||
route: "operations", tone: "neutral",
|
||||
});
|
||||
return result.slice(0, 4);
|
||||
}, [budgetsLoaded, deploymentsLoaded, headroom, models, operations, pressure, productionDeployments.length, recovery, gpu_node]);
|
||||
|
||||
const activity = useMemo(() => [
|
||||
...deployments.map((item) => ({
|
||||
at: item.promoted_at ?? item.created_at,
|
||||
title: `${item.capability}@${item.contract_version}`,
|
||||
detail: item.production ? `Production deployment · ${item.health_status}` : `${item.channel} deployment · ${item.status}`,
|
||||
route: "capabilities",
|
||||
})),
|
||||
...models.map((item) => ({
|
||||
at: item.updated_at,
|
||||
title: item.display_name,
|
||||
detail: `Registry ${item.lifecycle} · ${item.verification_status}`,
|
||||
route: "models",
|
||||
})),
|
||||
].filter((item) => item.at).sort((left, right) => Date.parse(right.at) - Date.parse(left.at)).slice(0, 6), [deployments, models]);
|
||||
|
||||
async function loadAssuranceData(token: string) {
|
||||
if ((!token && !sessionActive) || assuranceLoading) return;
|
||||
setAssuranceLoading(true);
|
||||
setAssuranceError(null);
|
||||
try {
|
||||
const [operationsState, recoveryState] = await Promise.all([
|
||||
api.operationsOverview(token), api.recoveryDashboard(token),
|
||||
]);
|
||||
setOperations(operationsState);
|
||||
setRecovery(recoveryState);
|
||||
} catch (cause) {
|
||||
setAssuranceError(cause instanceof Error ? cause.message : "Unable to load protected assurance evidence");
|
||||
} finally { setAssuranceLoading(false); }
|
||||
}
|
||||
|
||||
async function loadAssurance(event: FormEvent) {
|
||||
event.preventDefault();
|
||||
await loadAssuranceData(operatorToken);
|
||||
setOperatorToken("");
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (sessionActive) void loadAssuranceData(sessionReference);
|
||||
// The page-level credential remains fixed until locking unmounts this workspace.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [sessionActive, sessionReference]);
|
||||
|
||||
return <div className="command-center">
|
||||
<section className={`health-band ${platformTone}`} aria-label="Platform posture">
|
||||
<div className="health-identity">
|
||||
{platformTone === "healthy" ? <CheckCircle2 size={22} /> : <AlertTriangle size={22} />}
|
||||
<div>
|
||||
<span className="eyebrow">CURRENT PLATFORM POSTURE</span>
|
||||
<h2>{platformTone === "healthy" ? "Production control plane is ready" : platformTone === "warning" ? "Platform needs operator attention" : platformTone === "neutral" ? "Production posture is unknown" : "Production posture is degraded"}</h2>
|
||||
<p>{system?.environment ?? "unknown environment"} · {onlineNodes.length}/{nodes.length} nodes online · {productionDeployments.length} production capability deployments</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="health-actions">
|
||||
<StatusMark tone={platformTone}>{pollError ? "Live updates paused" : `Observed ${lastUpdated ? age(lastUpdated.toISOString()) : "now"}`}</StatusMark>
|
||||
<button className="secondary-action" onClick={onRefresh} disabled={refreshing}>
|
||||
<RefreshCw size={15} className={refreshing ? "spin" : ""} />{refreshing ? "Refreshing…" : "Refresh state"}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="command-kpis" aria-label="Platform summary">
|
||||
<Kpi icon={Server} label="Production state" value={!deploymentsLoaded ? "Unknown" : system?.production_inference_available ? "Available" : "Not declared"}
|
||||
detail={deploymentsLoaded ? `${productionDeployments.length} routed · ${healthyDeployments.length} healthy` : "Deployment evidence unavailable"} tone={deploymentsLoaded && system?.production_inference_available ? "healthy" : "warning"} onClick={() => onNavigate("capabilities")} />
|
||||
<Kpi icon={Cpu} label="GPU Node compute" value={gpu_node?.display_name ?? "Not enrolled"}
|
||||
detail={gpu_node ? `${gpu_node.liveness} · ${gpu_node.accelerators.length} GPU` : "No primary compute evidence"} tone={gpu_node?.liveness === "online" ? "healthy" : "critical"} onClick={() => onNavigate("nodes")} />
|
||||
<Kpi icon={Gauge} label="Schedulable headroom" value={!budgetsLoaded ? "Unknown" : budgets.length ? formatBytes(headroom) : "Not measured"}
|
||||
detail={!budgetsLoaded ? "Scheduler evidence unavailable" : budgets.length ? `${formatBytes(used)} used · ${formatBytes(reserve)} reserved` : "Scheduler has no capacity envelope"} tone={budgetsLoaded && pressure ? "warning" : budgetsLoaded && budgets.length ? "healthy" : "neutral"} onClick={() => onNavigate("capabilities")} />
|
||||
<Kpi icon={Network} label="Capability health" value={deploymentsLoaded ? `${healthyDeployments.length}/${productionDeployments.length || 0} healthy` : "Unknown"}
|
||||
detail={deploymentsLoaded ? `${deployments.length} total deployments` : "Deployment evidence unavailable"} tone={deploymentsLoaded && healthyDeployments.length === productionDeployments.length && productionDeployments.length ? "healthy" : "warning"} onClick={() => onNavigate("capabilities")} />
|
||||
</section>
|
||||
|
||||
<section className="command-grid">
|
||||
<article className="panel capacity-panel">
|
||||
<div className="panel-header command-panel-header">
|
||||
<div><span className="eyebrow">GPU_NODE CAPACITY</span><h2>GPU scheduling envelope</h2></div>
|
||||
<StatusMark tone={pressure ? "warning" : budgets.length ? "healthy" : "neutral"}>{budgets.length ? budgets.map((item) => item.pressure_state).join(" · ") : "No budget"}</StatusMark>
|
||||
</div>
|
||||
{budgets.length ? budgets.map((budget) => {
|
||||
const usedPercent = budget.total_vram_bytes > 0 ? Math.min(100, budget.observed_used_vram_bytes / budget.total_vram_bytes * 100) : 0;
|
||||
const reservePercent = budget.total_vram_bytes > 0 ? Math.min(100 - usedPercent, budget.safety_reserve_bytes / budget.total_vram_bytes * 100) : 0;
|
||||
return <div className="capacity-row" key={budget.accelerator_id}>
|
||||
<div><strong>{budget.node_name} · {budget.accelerator_name}</strong><span>{formatBytes(budget.schedulable_free_vram_bytes)} schedulable</span></div>
|
||||
<div className="capacity-track" role="img" aria-label={`${Math.round(usedPercent)} percent VRAM used, ${Math.round(reservePercent)} percent reserved`}>
|
||||
<span className="capacity-used" style={{ width: `${usedPercent}%` }} />
|
||||
<span className="capacity-reserve" style={{ width: `${reservePercent}%` }} />
|
||||
</div>
|
||||
<div className="capacity-legend"><span><i className="used" />{formatBytes(budget.observed_used_vram_bytes)} used</span><span><i className="reserve" />{formatBytes(budget.safety_reserve_bytes)} reserve</span><span><i />{formatBytes(budget.schedulable_free_vram_bytes)} headroom</span></div>
|
||||
</div>;
|
||||
}) : <div className="inline-empty"><Gauge size={20} /><div><strong>No scheduler envelope recorded</strong><p>Capacity stays unknown until a compute node reports an attributable VRAM budget.</p></div></div>}
|
||||
</article>
|
||||
|
||||
<article className="panel attention-panel">
|
||||
<div className="panel-header command-panel-header"><div><span className="eyebrow">OPERATOR ATTENTION</span><h2>Recommended next actions</h2></div><Siren size={18} /></div>
|
||||
<div className="attention-list">
|
||||
{actions.length ? actions.map((action) => <button key={action.title} onClick={() => onNavigate(action.route)}>
|
||||
<span className={`attention-marker ${action.tone}`}><AlertTriangle size={15} /></span>
|
||||
<span><strong>{action.title}</strong><small>{action.detail}</small></span><ArrowRight size={15} />
|
||||
</button>) : <div className="inline-empty"><CheckCircle2 size={20} /><div><strong>No immediate action identified</strong><p>Current public and protected evidence has no unresolved recommendation.</p></div></div>}
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section className="command-grid lower">
|
||||
<article className="panel assurance-panel">
|
||||
<div className="panel-header command-panel-header"><div><span className="eyebrow">ASSURANCE</span><h2>Alerts and recovery readiness</h2></div><ShieldCheck size={18} /></div>
|
||||
{operations && recovery ? <div className="assurance-metrics">
|
||||
<button onClick={() => onNavigate("operations")}><Siren size={18} /><span><strong>{activeAlerts.length}</strong><small>active alerts</small></span><StatusMark tone={activeAlerts.length ? "critical" : "healthy"}>{operations.status}</StatusMark></button>
|
||||
<button onClick={() => onNavigate("recovery")}><DatabaseBackup size={18} /><span><strong>{Math.round(recovery.coverage_ratio * 100)}%</strong><small>recovery coverage</small></span><StatusMark tone={recovery.stale_backup || recovery.unprotected_assets.length ? "warning" : "healthy"}>{recovery.stale_backup ? "Stale" : "Current"}</StatusMark></button>
|
||||
<div className="assurance-foot">Latest verified backup: {age(recovery.latest_verified_backup_at)} · restore rehearsal: {age(recovery.last_restore_rehearsal_at)}</div>
|
||||
</div> : sessionActive ? <div className="state-card"><span className="state-spinner" />Loading protected assurance evidence…{assuranceError ? ` ${assuranceError}` : ""}</div> : <form className="assurance-gate" onSubmit={loadAssurance}>
|
||||
<div className="assurance-gate-copy"><KeyRound size={18} /><div><strong>Protected operator evidence</strong><p>Alert and recovery data is admin-scoped. The token is used for this request and is never persisted.</p></div></div>
|
||||
<label htmlFor="command-operator-key"><span>Operator API key</span></label><input id="command-operator-key" name="operator-api-key" type="password" value={operatorToken} onChange={(event) => setOperatorToken(event.target.value)} autoComplete="off" />
|
||||
<button type="submit" disabled={!operatorToken || assuranceLoading}>{assuranceLoading ? "Loading…" : "Load assurance"}</button>
|
||||
{assuranceError ? <p className="field-error" role="alert">{assuranceError}</p> : null}
|
||||
</form>}
|
||||
</article>
|
||||
|
||||
<article className="panel activity-panel">
|
||||
<div className="panel-header command-panel-header"><div><span className="eyebrow">RECENT CONTROL-PLANE ACTIVITY</span><h2>Evidence timeline</h2></div><Boxes size={18} /></div>
|
||||
<div className="activity-timeline">
|
||||
{activity.length ? activity.map((item, index) => <button onClick={() => onNavigate(item.route)} key={`${item.route}-${item.at}-${index}`}>
|
||||
<span className="timeline-node" aria-hidden="true" /><span><strong>{item.title}</strong><small>{item.detail}</small></span><time dateTime={item.at}>{age(item.at)}</time>
|
||||
</button>) : <div className="inline-empty"><Boxes size={20} /><div><strong>No recorded activity yet</strong><p>Registry and deployment events will appear here when the API reports them.</p></div></div>}
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section className="platform-contract" aria-label="Control-plane boundary">
|
||||
<ShieldCheck size={17} /><span><strong>Capability-first by construction.</strong> {projects.length} projects, {projects.reduce((sum, project) => sum + project.bindings.length, 0)} bindings and {integrations.length} observed integrations remain isolated from concrete model paths.</span>
|
||||
<button onClick={() => onNavigate("projects")}>Review projects <ArrowRight size={14} /></button>
|
||||
</section>
|
||||
</div>;
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import { useEffect, useId, useMemo, useRef, useState } from "react";
|
||||
import { ArrowRight, Command, Search, X } from "lucide-react";
|
||||
|
||||
import type { NavItem } from "../navigation";
|
||||
|
||||
export function CommandPalette({ open, navigation, onClose, onNavigate }: {
|
||||
open: boolean;
|
||||
navigation: NavItem[];
|
||||
onClose: () => void;
|
||||
onNavigate: (key: string) => void;
|
||||
}) {
|
||||
const [query, setQuery] = useState("");
|
||||
const [activeIndex, setActiveIndex] = useState(0);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const dialogRef = useRef<HTMLDivElement>(null);
|
||||
const restoreFocusRef = useRef<HTMLElement | null>(null);
|
||||
const listboxId = useId();
|
||||
const results = useMemo(() => {
|
||||
const needle = query.trim().toLowerCase();
|
||||
if (!needle) return navigation;
|
||||
return navigation.filter((item) =>
|
||||
[item.label, item.group, item.shortDescription, ...item.keywords].join(" ").toLowerCase().includes(needle),
|
||||
);
|
||||
}, [navigation, query]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return undefined;
|
||||
restoreFocusRef.current = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
||||
setQuery("");
|
||||
setActiveIndex(0);
|
||||
window.requestAnimationFrame(() => inputRef.current?.focus());
|
||||
return () => restoreFocusRef.current?.focus();
|
||||
}, [open]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return undefined;
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === "Escape") onClose();
|
||||
if (event.key === "Tab") {
|
||||
const focusable = dialogRef.current?.querySelectorAll<HTMLElement>(
|
||||
'button:not([disabled]), input:not([disabled]), [tabindex]:not([tabindex="-1"])',
|
||||
);
|
||||
if (!focusable?.length) return;
|
||||
const first = focusable[0];
|
||||
const last = focusable[focusable.length - 1];
|
||||
if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus(); }
|
||||
else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus(); }
|
||||
}
|
||||
};
|
||||
window.addEventListener("keydown", onKeyDown);
|
||||
return () => window.removeEventListener("keydown", onKeyDown);
|
||||
}, [onClose, open]);
|
||||
|
||||
useEffect(() => { setActiveIndex(0); }, [query]);
|
||||
|
||||
if (!open) return null;
|
||||
const select = (key: string) => { onNavigate(key); onClose(); };
|
||||
return (
|
||||
<div className="command-scrim" role="presentation" onMouseDown={(event) => {
|
||||
if (event.target === event.currentTarget) onClose();
|
||||
}}>
|
||||
<div className="command-dialog" role="dialog" aria-modal="true" aria-label="Quick jump" ref={dialogRef}>
|
||||
<div className="command-input-row">
|
||||
<Search size={18} aria-hidden="true" />
|
||||
<input
|
||||
ref={inputRef}
|
||||
value={query}
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "ArrowDown") { event.preventDefault(); setActiveIndex((value) => Math.min(value + 1, results.length - 1)); }
|
||||
if (event.key === "ArrowUp") { event.preventDefault(); setActiveIndex((value) => Math.max(value - 1, 0)); }
|
||||
if (event.key === "Home") { event.preventDefault(); setActiveIndex(0); }
|
||||
if (event.key === "End") { event.preventDefault(); setActiveIndex(Math.max(0, results.length - 1)); }
|
||||
if (event.key === "Enter" && results[activeIndex]) select(results[activeIndex].key);
|
||||
}}
|
||||
placeholder="Jump to a workspace or action…"
|
||||
aria-label="Search workspaces"
|
||||
role="combobox"
|
||||
aria-autocomplete="list"
|
||||
aria-expanded="true"
|
||||
aria-controls={listboxId}
|
||||
aria-activedescendant={results[activeIndex] ? `${listboxId}-${results[activeIndex].key}` : undefined}
|
||||
/>
|
||||
<button onClick={onClose} aria-label="Close quick jump"><X size={16} /></button>
|
||||
</div>
|
||||
<div id={listboxId} className="command-results" role="listbox" aria-label="Workspaces">
|
||||
{results.length ? results.map((item, index) => (
|
||||
<button id={`${listboxId}-${item.key}`} key={item.key} role="option" aria-selected={activeIndex === index} onMouseEnter={() => setActiveIndex(index)} onClick={() => select(item.key)}>
|
||||
<span className="command-result-icon"><item.icon size={17} /></span>
|
||||
<span><strong>{item.label}</strong><small>{item.group} · {item.shortDescription}</small></span>
|
||||
<ArrowRight size={15} aria-hidden="true" />
|
||||
</button>
|
||||
)) : <div className="command-empty">No workspace matches “{query}”.</div>}
|
||||
</div>
|
||||
<div className="command-hint"><span><Command size={13} /> K to open</span><span>Enter to jump · Esc to close</span></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { DiscoverWorkspace } from "./DiscoverWorkspace";
|
||||
|
||||
const model = {
|
||||
id: "11111111-1111-4111-8111-111111111111", key: "qwen", display_name: "Qwen Embedding",
|
||||
description: null, source_type: "huggingface", upstream_provider: "Qwen",
|
||||
upstream_source: "Qwen/Qwen3-Embedding-0.6B", upstream_metadata: {}, local_metadata: {},
|
||||
interpretation_metadata: {}, family: null, modalities: [], parameter_metadata: {},
|
||||
license_metadata: { status: "unknown" }, lifecycle: "candidate", revision_count: 0,
|
||||
artifact_count: 0, verification_status: "unverified", deployment_status: "not_deployed",
|
||||
created_at: "2026-08-25T00:00:00Z", updated_at: "2026-08-25T00:00:00Z",
|
||||
};
|
||||
const snapshot = {
|
||||
id: "snapshot-1", model_id: model.id, provider: "huggingface", repository_id: model.upstream_source,
|
||||
requested_revision: "main", resolved_commit_sha: "a".repeat(40), access_state: "public",
|
||||
metadata_snapshot: { pipeline_tag: "feature-extraction" }, card_metadata: { license: "apache-2.0" },
|
||||
security_metadata: { evidence_only: true }, source_updated_at: null,
|
||||
observed_at: "2026-08-25T00:00:00Z", stale_after: "2026-08-25T01:00:00Z", stale: false,
|
||||
files: [{ id: "file-1", path: "model.safetensors", size_bytes: 12, blob_id: "blob", upstream_sha256: "b".repeat(64), file_format: "safetensors", role: "weights", risk_flags: [], metadata_snapshot: {} }],
|
||||
};
|
||||
const artifactSet = {
|
||||
id: "set-1", revision_id: "revision-1", snapshot_id: snapshot.id,
|
||||
variant_key: "safetensors-default", label: "Safetensors Default",
|
||||
selection_reason: "Safetensors files were preferred; duplicate pickle weights were excluded.",
|
||||
selected_paths: ["model.safetensors"], total_size_bytes: 12, file_count: 1,
|
||||
availability: "remote", status: "remote", completeness: "planned", security_status: "unverified",
|
||||
license_status: "captured_unreviewed", immutable_at: snapshot.observed_at,
|
||||
created_at: snapshot.observed_at, updated_at: snapshot.observed_at,
|
||||
};
|
||||
|
||||
function mockApi(jobs: unknown[] = []): void {
|
||||
vi.stubGlobal("fetch", vi.fn((input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const url = String(input);
|
||||
let payload: unknown = [];
|
||||
if (url.includes("/api/v1/models?")) payload = { items: [model], page: 1, page_size: 100, total: 1, pages: 1 };
|
||||
else if (url.endsWith("/api/v1/storage-roots")) payload = [{ id: "root-1", compute_node_id: "node-1", name: "GPU Node cache", purpose: "model_artifacts", path: "/host", agent_path: "/data/artifacts/model-registry", status: "ready", writable: true, capacity_bytes: 1000, free_bytes: 800, reserve_bytes: 10, reserve_percent: 10 }];
|
||||
else if (url.endsWith("/api/v1/artifact-jobs")) payload = jobs;
|
||||
else if (url.endsWith(`/api/v1/models/${model.id}/refresh-upstream`)) payload = snapshot;
|
||||
else if (url.includes(`/api/v1/models/${model.id}/revisions`)) payload = { items: [{ id: "revision-1", model_id: model.id, upstream_revision: "main", resolved_commit_sha: "a".repeat(40), metadata_snapshot: {}, discovered_at: snapshot.observed_at, immutable_at: snapshot.observed_at, created_at: snapshot.observed_at, updated_at: snapshot.observed_at }], page: 1, page_size: 100, total: 1, pages: 1 };
|
||||
else if (url.endsWith("/api/v1/revisions/revision-1/artifact-sets")) payload = [artifactSet];
|
||||
else if (url.endsWith("/api/v1/discovery/search")) payload = [{ repository_id: model.upstream_source, resolved_commit_sha: "a".repeat(40), access_state: "public", pipeline_tag: "feature-extraction", library_name: "transformers", tags: [], downloads: 100, likes: 5, matched_model_id: model.id, upstream_facts: {}, local_interpretation: { approval: "not_evaluated" } }];
|
||||
else if (url.endsWith("/api/v1/download-plans") && init?.method === "POST") payload = { id: "plan-1", artifact_set_id: artifactSet.id, compute_node_id: "node-1", storage_root_id: "root-1", repository_id: model.upstream_source, resolved_commit_sha: "a".repeat(40), total_size_bytes: 12, file_count: 1, status: "planned", idempotency_key: "hash", preflight: { trust_remote_code: false }, immutable_payload: {}, planned_at: snapshot.observed_at, expires_at: snapshot.stale_after, immutable_at: snapshot.observed_at, stale: false, files: [{ ordinal: 0, path: "model.safetensors", size_bytes: 12, upstream_sha256: "b".repeat(64), file_format: "safetensors", role: "weights", risk_flags: [] }] };
|
||||
else if (url.endsWith("/api/v1/download-plans/plan-1/approve")) payload = { id: "plan-1", artifact_set_id: artifactSet.id, compute_node_id: "node-1", storage_root_id: "root-1", repository_id: model.upstream_source, resolved_commit_sha: "a".repeat(40), total_size_bytes: 12, file_count: 1, status: "ready", idempotency_key: "hash", preflight: { trust_remote_code: false }, immutable_payload: {}, planned_at: snapshot.observed_at, expires_at: snapshot.stale_after, immutable_at: snapshot.observed_at, stale: false, files: [{ ordinal: 0, path: "model.safetensors", size_bytes: 12, upstream_sha256: "b".repeat(64), file_format: "safetensors", role: "weights", risk_flags: [] }] };
|
||||
return Promise.resolve(new Response(JSON.stringify(payload), { status: 200 }));
|
||||
}));
|
||||
}
|
||||
|
||||
afterEach(() => { cleanup(); vi.unstubAllGlobals(); });
|
||||
|
||||
describe("M3 discovery and acquisition UI", () => {
|
||||
it("keeps discovery, integrity and approval visibly separate", async () => {
|
||||
mockApi();
|
||||
render(<DiscoverWorkspace />);
|
||||
expect(screen.getByText("Discovery and local integrity are not approval")).toBeInTheDocument();
|
||||
expect(await screen.findByText("Qwen Embedding · Qwen/Qwen3-Embedding-0.6B")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows exact revision and upstream scanner evidence semantics", async () => {
|
||||
mockApi();
|
||||
render(<DiscoverWorkspace />);
|
||||
fireEvent.click(await screen.findByText("Refresh from upstream"));
|
||||
expect(await screen.findByText("a".repeat(40))).toBeInTheDocument();
|
||||
expect(screen.getByText(/Scanner metadata: evidence only/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/Safetensors files were preferred/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("labels discovery results as registered upstream facts", async () => {
|
||||
mockApi();
|
||||
render(<DiscoverWorkspace />);
|
||||
fireEvent.click(await screen.findByText("Search"));
|
||||
expect(await screen.findByText(model.upstream_source)).toBeInTheDocument();
|
||||
expect(screen.getByText("registered")).toBeInTheDocument();
|
||||
expect(screen.getByText("Use candidate")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("reviews an immutable file plan before execution", async () => {
|
||||
mockApi();
|
||||
render(<DiscoverWorkspace />);
|
||||
fireEvent.click(await screen.findByText("Refresh from upstream"));
|
||||
fireEvent.click(await screen.findByText("Create immutable plan"));
|
||||
expect(await screen.findByText("model.safetensors")).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByText("Approve immutable plan"));
|
||||
expect(await screen.findByText("Execute on target node")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows real failed-job state and an explicit quarantine retry", async () => {
|
||||
mockApi([{
|
||||
id: "job-1", plan_id: "plan-1", compute_node_id: "node-1", storage_root_id: "root-1",
|
||||
status: "failed", attempt_count: 3, progress_bytes: 6, total_bytes: 12,
|
||||
current_file: "model.safetensors", cancel_requested: false,
|
||||
quarantine_relative_path: ".quarantine/job-1", promoted_relative_path: null,
|
||||
error_code: "download_transport_error", error_message: "temporary network failure",
|
||||
result: {}, started_at: snapshot.observed_at, completed_at: snapshot.observed_at,
|
||||
created_at: snapshot.observed_at, updated_at: snapshot.observed_at,
|
||||
}]);
|
||||
render(<DiscoverWorkspace />);
|
||||
expect(await screen.findByText("50% · 0.0 KiB / 0.0 KiB")).toBeInTheDocument();
|
||||
expect(screen.getByText("temporary network failure")).toBeInTheDocument();
|
||||
expect(screen.getByText("Retry from quarantine")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,280 @@
|
||||
import { FormEvent, useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { Download, FileSearch, RefreshCw, Search, ShieldAlert } from "lucide-react";
|
||||
|
||||
import { api } from "../lib/api";
|
||||
import type {
|
||||
ArtifactJob,
|
||||
ArtifactSet,
|
||||
DiscoveryCandidate,
|
||||
DownloadPlan,
|
||||
ModelSummary,
|
||||
StorageRoot,
|
||||
UpstreamSnapshot,
|
||||
} from "../types";
|
||||
|
||||
function formatBytes(value?: number | null): string {
|
||||
if (value == null) return "unknown";
|
||||
if (value < 1024 ** 2) return `${(value / 1024).toFixed(1)} KiB`;
|
||||
if (value < 1024 ** 3) return `${(value / 1024 ** 2).toFixed(1)} MiB`;
|
||||
return `${(value / 1024 ** 3).toFixed(2)} GiB`;
|
||||
}
|
||||
|
||||
function Status({ value }: { value: string }) {
|
||||
return <span className={`registry-status ${value}`}>{value.replaceAll("_", " ")}</span>;
|
||||
}
|
||||
|
||||
export function DiscoverWorkspace() {
|
||||
const [models, setModels] = useState<ModelSummary[]>([]);
|
||||
const [roots, setRoots] = useState<StorageRoot[]>([]);
|
||||
const [jobs, setJobs] = useState<ArtifactJob[]>([]);
|
||||
const [query, setQuery] = useState("Qwen embedding");
|
||||
const [pipelineTag, setPipelineTag] = useState("");
|
||||
const [sort, setSort] = useState<"downloads" | "likes" | "last_modified">("downloads");
|
||||
const [results, setResults] = useState<DiscoveryCandidate[]>([]);
|
||||
const [snapshot, setSnapshot] = useState<UpstreamSnapshot | null>(null);
|
||||
const [sets, setSets] = useState<ArtifactSet[]>([]);
|
||||
const [selectedModel, setSelectedModel] = useState<string>("");
|
||||
const [selectedSet, setSelectedSet] = useState<string>("");
|
||||
const [selectedRoot, setSelectedRoot] = useState<string>("");
|
||||
const [plan, setPlan] = useState<DownloadPlan | null>(null);
|
||||
const [busy, setBusy] = useState<string | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const loadState = useCallback(async () => {
|
||||
const [modelPage, storageRoots, artifactJobs] = await Promise.all([
|
||||
api.models("page=1&page_size=100&source_type=huggingface"),
|
||||
api.storageRoots(),
|
||||
api.artifactJobs(),
|
||||
]);
|
||||
setModels(modelPage.items);
|
||||
setRoots(storageRoots);
|
||||
setJobs(artifactJobs);
|
||||
setSelectedModel((value) => value || modelPage.items[0]?.id || "");
|
||||
setSelectedRoot((value) => value || storageRoots.find((root) => root.status === "ready")?.id || "");
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
void loadState().catch((cause: unknown) =>
|
||||
setError(cause instanceof Error ? cause.message : "Acquisition state failed"),
|
||||
);
|
||||
}, [loadState]);
|
||||
|
||||
useEffect(() => {
|
||||
const interval = window.setInterval(() => {
|
||||
void api.artifactJobs().then(setJobs).catch(() => undefined);
|
||||
}, 3000);
|
||||
return () => window.clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
const root = roots.find((item) => item.id === selectedRoot);
|
||||
const artifactSet = sets.find((item) => item.id === selectedSet);
|
||||
const activeJobs = useMemo(
|
||||
() => jobs.filter((job) => !["completed", "failed", "cancelled"].includes(job.status)),
|
||||
[jobs],
|
||||
);
|
||||
|
||||
async function search(event: FormEvent) {
|
||||
event.preventDefault();
|
||||
setBusy("search");
|
||||
setError(null);
|
||||
try {
|
||||
setResults(await api.discoverySearch({
|
||||
query,
|
||||
limit: 20,
|
||||
sort,
|
||||
pipeline_tag: pipelineTag || null,
|
||||
}));
|
||||
} catch (cause) {
|
||||
setError(cause instanceof Error ? cause.message : "Hugging Face search failed");
|
||||
} finally {
|
||||
setBusy(null);
|
||||
}
|
||||
}
|
||||
|
||||
async function registerCandidate(candidate: DiscoveryCandidate) {
|
||||
setBusy(`register-${candidate.repository_id}`);
|
||||
setError(null);
|
||||
try {
|
||||
const parts = candidate.repository_id.split("/");
|
||||
const key = candidate.repository_id
|
||||
.toLowerCase()
|
||||
.replaceAll("/", "--")
|
||||
.replace(/[^a-z0-9._-]+/g, "-")
|
||||
.slice(0, 128);
|
||||
const created = await api.createModel({
|
||||
key,
|
||||
display_name: parts.at(-1) ?? candidate.repository_id,
|
||||
source_type: "huggingface",
|
||||
upstream_provider: parts[0] ?? "unknown",
|
||||
upstream_source: candidate.repository_id,
|
||||
upstream_metadata: candidate.upstream_facts,
|
||||
local_metadata: {},
|
||||
interpretation_metadata: {
|
||||
discovery_state: "discovered",
|
||||
approval: "not_evaluated",
|
||||
},
|
||||
modalities: [],
|
||||
parameter_metadata: {},
|
||||
license_metadata: { status: "unknown" },
|
||||
lifecycle: "candidate",
|
||||
});
|
||||
await loadState();
|
||||
setSelectedModel(created.id);
|
||||
setResults((items) => items.map((item) => item.repository_id === candidate.repository_id
|
||||
? { ...item, matched_model_id: created.id }
|
||||
: item));
|
||||
} catch (cause) {
|
||||
setError(cause instanceof Error ? cause.message : "Candidate registration failed");
|
||||
} finally {
|
||||
setBusy(null);
|
||||
}
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
if (!selectedModel) return;
|
||||
setBusy("refresh");
|
||||
setError(null);
|
||||
setPlan(null);
|
||||
try {
|
||||
const upstream = await api.refreshUpstream(selectedModel);
|
||||
const revisions = await api.revisions(selectedModel);
|
||||
const exact = revisions.items.find(
|
||||
(revision) => revision.resolved_commit_sha === upstream.resolved_commit_sha,
|
||||
);
|
||||
const artifactSets = exact ? await api.artifactSets(exact.id) : [];
|
||||
setSnapshot(upstream);
|
||||
setSets(artifactSets);
|
||||
setSelectedSet(artifactSets[0]?.id || "");
|
||||
} catch (cause) {
|
||||
setError(cause instanceof Error ? cause.message : "Upstream refresh failed");
|
||||
} finally {
|
||||
setBusy(null);
|
||||
}
|
||||
}
|
||||
|
||||
async function createPlan() {
|
||||
if (!artifactSet || !root) return;
|
||||
setBusy("plan");
|
||||
setError(null);
|
||||
try {
|
||||
setPlan(
|
||||
await api.createDownloadPlan({
|
||||
artifact_set_id: artifactSet.id,
|
||||
compute_node_id: root.compute_node_id,
|
||||
storage_root_id: root.id,
|
||||
expires_in_seconds: 3600,
|
||||
}),
|
||||
);
|
||||
} catch (cause) {
|
||||
setError(cause instanceof Error ? cause.message : "Download plan failed");
|
||||
} finally {
|
||||
setBusy(null);
|
||||
}
|
||||
}
|
||||
|
||||
async function executePlan() {
|
||||
if (!plan) return;
|
||||
setBusy("execute");
|
||||
setError(null);
|
||||
try {
|
||||
await api.executeDownloadPlan(plan.id);
|
||||
setJobs(await api.artifactJobs());
|
||||
} catch (cause) {
|
||||
setError(cause instanceof Error ? cause.message : "Artifact job failed to queue");
|
||||
} finally {
|
||||
setBusy(null);
|
||||
}
|
||||
}
|
||||
|
||||
async function approvePlan() {
|
||||
if (!plan) return;
|
||||
setBusy("approve");
|
||||
setError(null);
|
||||
try {
|
||||
setPlan(await api.approveDownloadPlan(plan.id));
|
||||
} catch (cause) {
|
||||
setError(cause instanceof Error ? cause.message : "Download plan approval failed");
|
||||
} finally {
|
||||
setBusy(null);
|
||||
}
|
||||
}
|
||||
|
||||
async function retryJob(id: string) {
|
||||
setBusy(`retry-${id}`);
|
||||
setError(null);
|
||||
try {
|
||||
await api.retryArtifactJob(id);
|
||||
setJobs(await api.artifactJobs());
|
||||
} catch (cause) {
|
||||
setError(cause instanceof Error ? cause.message : "Artifact retry failed");
|
||||
} finally {
|
||||
setBusy(null);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="discovery-stack">
|
||||
<article className="panel acquisition-boundary">
|
||||
<ShieldAlert size={20} />
|
||||
<div>
|
||||
<strong>Discovery and local integrity are not approval</strong>
|
||||
<p>
|
||||
Metadata comes from Hugging Face, files are pinned to an exact commit, and the node
|
||||
verifies bytes in quarantine. Security/license approval and all runtime actions remain blocked.
|
||||
</p>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
{error ? <div className="error-banner"><strong>Acquisition blocked</strong><span>{error}</span></div> : null}
|
||||
|
||||
<section className="discovery-grid">
|
||||
<article className="panel discovery-search">
|
||||
<span className="eyebrow">OFFICIAL HUGGING FACE API</span>
|
||||
<h2>Search upstream</h2>
|
||||
<form onSubmit={search}>
|
||||
<input aria-label="Search Hugging Face" value={query} onChange={(event) => setQuery(event.target.value)} />
|
||||
<input aria-label="Pipeline filter" value={pipelineTag} onChange={(event) => setPipelineTag(event.target.value)} placeholder="pipeline (optional)" />
|
||||
<select aria-label="Sort discovery" value={sort} onChange={(event) => setSort(event.target.value as typeof sort)}><option value="downloads">Downloads</option><option value="likes">Likes</option><option value="last_modified">Recently updated</option></select>
|
||||
<button disabled={busy === "search"}><Search size={15} />{busy === "search" ? "Searching…" : "Search"}</button>
|
||||
</form>
|
||||
<div className="discovery-results">
|
||||
{results.map((item) => (
|
||||
<article key={item.repository_id}>
|
||||
<div><strong>{item.repository_id}</strong><small>{item.pipeline_tag ?? "no pipeline tag"} · {item.library_name ?? "unknown library"}</small><small>{item.tags.slice(0, 4).join(" · ") || "no reported tags"}</small></div>
|
||||
<div><Status value={item.matched_model_id ? "registered" : "upstream_only"} /><Status value={item.access_state} /><small>{item.downloads?.toLocaleString() ?? "?"} downloads · {item.likes?.toLocaleString() ?? "?"} likes</small>{item.matched_model_id ? <button onClick={() => setSelectedModel(String(item.matched_model_id))}>Use candidate</button> : <button onClick={() => void registerCandidate(item)} disabled={busy === `register-${item.repository_id}`}>{busy === `register-${item.repository_id}` ? "Registering…" : "Add candidate"}</button>}</div>
|
||||
</article>
|
||||
))}
|
||||
{!results.length ? <p>Search results are upstream signals only; no candidate is approved or downloaded.</p> : null}
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article className="panel discovery-refresh">
|
||||
<span className="eyebrow">RECONCILE EXISTING CANDIDATE</span>
|
||||
<h2>Resolve metadata and revision</h2>
|
||||
<label>Registry candidate<select value={selectedModel} onChange={(event) => setSelectedModel(event.target.value)}>{models.map((model) => <option key={model.id} value={model.id}>{model.display_name} · {model.upstream_source}</option>)}</select></label>
|
||||
<button onClick={() => void refresh()} disabled={!selectedModel || busy === "refresh"}><RefreshCw size={15} className={busy === "refresh" ? "spin" : ""} />{busy === "refresh" ? "Refreshing…" : "Refresh from upstream"}</button>
|
||||
{snapshot ? <div className="snapshot-summary"><Status value={snapshot.stale ? "stale" : snapshot.access_state} /><code>{snapshot.resolved_commit_sha}</code><span>{snapshot.files.length} upstream files · observed {new Date(snapshot.observed_at).toLocaleString()}</span><small>Scanner metadata: evidence only · license: captured, unreviewed</small></div> : <p>No metadata snapshot selected.</p>}
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section className="panel download-planner">
|
||||
<div className="panel-header"><div><span className="eyebrow">IMMUTABLE DOWNLOAD PLAN</span><h2>Plan node-local acquisition</h2></div><FileSearch size={20} /></div>
|
||||
<div className="planner-controls">
|
||||
<label>Artifact set<select value={selectedSet} onChange={(event) => { setSelectedSet(event.target.value); setPlan(null); }}><option value="">Refresh a candidate first</option>{sets.map((item) => <option key={item.id} value={item.id}>{item.label} · {formatBytes(item.total_size_bytes)} · {item.file_count} files</option>)}</select></label>
|
||||
<label>Approved storage root<select value={selectedRoot} onChange={(event) => { setSelectedRoot(event.target.value); setPlan(null); }}>{roots.map((item) => <option key={item.id} value={item.id}>{item.name} · {item.status} · {formatBytes(item.free_bytes)} free</option>)}</select></label>
|
||||
<button onClick={() => void createPlan()} disabled={!artifactSet || !root || busy === "plan"}>{busy === "plan" ? "Checking capacity…" : "Create immutable plan"}</button>
|
||||
</div>
|
||||
{artifactSet ? <div className="artifact-set-explain"><strong>{artifactSet.label}</strong><p>{artifactSet.selection_reason}</p><span>Completeness: {artifactSet.completeness} · Security: {artifactSet.security_status} · License: {artifactSet.license_status}</span></div> : null}
|
||||
{plan ? <div className="plan-review"><div><Status value={plan.stale ? "stale" : plan.status} /><strong>{plan.file_count} files · {formatBytes(plan.total_size_bytes)}</strong><code>{plan.resolved_commit_sha}</code></div><ul>{plan.files.map((file) => <li key={file.path}><span>{file.path}</span><small>{file.file_format} · {formatBytes(file.size_bytes)}{file.upstream_sha256 ? " · upstream SHA-256" : " · local SHA-256 after download"}</small></li>)}</ul>{plan.status === "planned" ? <button onClick={() => void approvePlan()} disabled={plan.stale || busy === "approve"}>{busy === "approve" ? "Approving…" : "Approve immutable plan"}</button> : <button onClick={() => void executePlan()} disabled={plan.status !== "ready" || plan.stale || busy === "execute"}><Download size={15} />{busy === "execute" ? "Queueing…" : "Execute on target node"}</button>}</div> : null}
|
||||
</section>
|
||||
|
||||
<section className="panel artifact-jobs">
|
||||
<div className="panel-header"><div><span className="eyebrow">OUTBOUND NODE JOBS</span><h2>Acquisition progress</h2></div><span>{activeJobs.length} active</span></div>
|
||||
{jobs.length ? jobs.map((job) => {
|
||||
const percent = job.total_bytes ? Math.min(100, Math.round(job.progress_bytes / job.total_bytes * 100)) : 0;
|
||||
return <article key={job.id}><div><Status value={job.status} /><strong>{percent}% · {formatBytes(job.progress_bytes)} / {formatBytes(job.total_bytes)}</strong><small>{job.error_message ?? job.current_file ?? job.promoted_relative_path ?? "Awaiting node claim"}</small></div><div className="job-progress"><span style={{ width: `${percent}%` }} /></div>{job.status === "failed" ? <button onClick={() => void retryJob(job.id)} disabled={busy === `retry-${job.id}`}>{busy === `retry-${job.id}` ? "Retrying…" : "Retry from quarantine"}</button> : !["completed", "cancelled"].includes(job.status) ? <button onClick={() => void api.cancelArtifactJob(job.id).then(loadState)}>Cancel</button> : null}</article>;
|
||||
}) : <p>No artifact jobs have been queued.</p>}
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { EvaluationWorkspace } from "./EvaluationWorkspace";
|
||||
|
||||
function mockEvaluation(): void {
|
||||
vi.stubGlobal("fetch", vi.fn((input: RequestInfo | URL) => {
|
||||
const url = String(input);
|
||||
let payload: unknown = [];
|
||||
if (url.endsWith("/api/v1/evaluation-suites")) payload = [{
|
||||
id: "suite-1", project_id: "project-1", key: "examplerag-retrieval",
|
||||
name: "ExampleRAG Retrieval Evaluation", description: "Reviewed production corpus cases",
|
||||
latest_revision_id: "revision-1", latest_revision: "v1", case_count: 30,
|
||||
critical_case_count: 3, created_at: "2026-08-25T12:00:00Z",
|
||||
}];
|
||||
if (url.endsWith("/api/v1/evaluation-runs")) payload = [
|
||||
{ id: "shadow-run", project_id: "project-1", suite_revision_id: "revision-1",
|
||||
target_kind: "shadow", target_index_ref: "rag_dense_modelforge-rag_m6",
|
||||
embedding_space_ref: "space-shadow", status: "completed", corpus_revision: "corpus",
|
||||
retrieval_config_digest: "a".repeat(64), environment_fingerprint: {},
|
||||
environment_digest: "b".repeat(64), expected_cases: 30, completed_cases: 30,
|
||||
error_count: 0, aggregate_metrics: { recall_at_5: .9, recall_at_10: .97, mrr: .81, ndcg_at_10: .84 },
|
||||
created_at: "2026-08-25T12:00:00Z" },
|
||||
{ id: "current-run", project_id: "project-1", suite_revision_id: "revision-1",
|
||||
target_kind: "current", target_index_ref: "rag_dense_nomic-embed-text_v1",
|
||||
embedding_space_ref: "legacy-observed", status: "completed", corpus_revision: "corpus",
|
||||
retrieval_config_digest: "a".repeat(64), environment_fingerprint: {},
|
||||
environment_digest: "c".repeat(64), expected_cases: 30, completed_cases: 30,
|
||||
error_count: 0, aggregate_metrics: { recall_at_5: .85, recall_at_10: .93, mrr: .78, ndcg_at_10: .8 },
|
||||
created_at: "2026-08-25T11:00:00Z" },
|
||||
];
|
||||
if (url.endsWith("/api/v1/embedding-migrations")) payload = [{
|
||||
id: "migration-1", project_id: "project-1", source_embedding_space: "legacy-observed",
|
||||
target_embedding_space_id: "space-shadow", source_index_ref: "rag_dense_nomic-embed-text_v1",
|
||||
target_index_ref: "rag_dense_modelforge-rag_m6", corpus_revision: "corpus",
|
||||
status: "ready_for_evaluation", total_chunks: 597, completed_chunks: 597,
|
||||
failed_chunks: 0, retried_chunks: 2, batch_size: 32, concurrency: 1,
|
||||
priority: "background", preflight_evidence: {}, progress_evidence: {},
|
||||
validation_evidence: { passed: true }, operational_metrics: {},
|
||||
evaluation_eligibility: true, cancel_requested: false,
|
||||
created_at: "2026-08-25T10:00:00Z", updated_at: "2026-08-25T10:05:00Z",
|
||||
}];
|
||||
if (url.endsWith("/api/v1/evaluation-comparisons")) payload = [{
|
||||
id: "comparison-1", project_id: "project-1", baseline_run_id: "current-run",
|
||||
candidate_run_id: "shadow-run", comparability: "comparable", comparability_evidence: {},
|
||||
metric_deltas: { recall_at_10: .04 }, improved_cases: 8, unchanged_cases: 20,
|
||||
regressed_cases: 2, critical_regressions: 0, case_comparisons: [],
|
||||
promotion_eligibility: "eligible", eligibility_evidence: { promotion_performed: false },
|
||||
created_at: "2026-08-25T13:00:00Z",
|
||||
}];
|
||||
if (url.endsWith("/api/v1/model-comparisons")) payload = [{
|
||||
id: "matrix-1", project_id: "project-1", capability_contract_id: "contract-1",
|
||||
suite_revision_id: "revision-1", current_run_id: "current-run",
|
||||
title: "ExampleRAG embedding candidates", comparability: "comparable",
|
||||
evidence_fingerprint: "d".repeat(64), created_at: "2026-08-25T14:00:00Z",
|
||||
candidates: [{ candidate_key: "qwen-retrieval", label: "Qwen retrieval-aware",
|
||||
status: "evaluated", evaluation_run_id: "shadow-run", embedding_space: "space-shadow",
|
||||
artifact_size_bytes: 1200, quality_metrics: { recall_at_5: .9, recall_at_10: .97, mrr: .81, ndcg_at_10: .84 },
|
||||
metric_deltas: { recall_at_10: .04 }, critical_regressions: 1,
|
||||
case_outcomes: { improved: 8, unchanged: 20, regressed: 2 }, comparability: "comparable",
|
||||
latency_ms: { baseline_p50: 95, baseline_p95: 160, p50: 255, p95: 312 },
|
||||
resource_evidence: { resident_vram_bytes: 1500000000 }, migration_impact: {},
|
||||
security_state: {}, provenance: { evidence_level: "A" }, blockers: [] }],
|
||||
}];
|
||||
if (url.endsWith("/api/v1/retrieval-candidate-pools")) payload = [{
|
||||
id: "pool-1", project_id: "project-1", suite_revision_id: "revision-1",
|
||||
evaluation_case_id: "case-1", source_embedding_space: "space-shadow",
|
||||
source_index_ref: "rag_dense_shadow", corpus_revision: "corpus",
|
||||
retrieval_config_digest: "e".repeat(64), candidate_count: 40,
|
||||
ordered_candidates: [], fingerprint: "f".repeat(64),
|
||||
created_at: "2026-08-25T14:00:00Z", immutable_at: "2026-08-25T14:00:00Z",
|
||||
}];
|
||||
if (url.endsWith("/api/v1/retrieval-pipeline-identities")) payload = [{
|
||||
id: "pipeline-1", project_id: "project-1", embedding_space_ref: "space-shadow",
|
||||
sparse_config_digest: "a".repeat(64), fusion_config_digest: "b".repeat(64),
|
||||
reranker_deployment_id: "reranker-1", reranker_config_digest: "c".repeat(64),
|
||||
candidate_k: 40, output_k: 10, identity_digest: "d".repeat(64), configuration: {},
|
||||
migration_class: "behavioral", created_at: "2026-08-25T14:00:00Z",
|
||||
immutable_at: "2026-08-25T14:00:00Z",
|
||||
}];
|
||||
if (url.endsWith("/api/v1/reranking-runs")) payload = [{
|
||||
id: "rerank-run-1", project_id: "project-1", suite_revision_id: "revision-1",
|
||||
pipeline_identity_id: "pipeline-1", control_pipeline_identity_id: "control-1",
|
||||
reranker_deployment_id: "reranker-1", status: "completed", corpus_revision: "corpus",
|
||||
candidate_pool_set_fingerprint: "f".repeat(64), environment_fingerprint: {},
|
||||
environment_digest: "a".repeat(64), expected_cases: 33, completed_cases: 33,
|
||||
error_count: 0, aggregate_metrics: { mrr: .5, ndcg_at_10: .6 },
|
||||
latency_metrics: { rerank_p95_ms: 719, total_p95_ms: 812 },
|
||||
created_at: "2026-08-25T14:00:00Z",
|
||||
}];
|
||||
if (url.endsWith("/api/v1/discovery-assessments")) payload = [{
|
||||
id: "discovery-1", upstream_snapshot_id: "snapshot-1", candidate_key: "bge-m3",
|
||||
repository_id: "BAAI/bge-m3", resolved_commit_sha: "a".repeat(40),
|
||||
artifact_evidence: { bytes: 2317574698 }, security_state: { status: "risk_detected" },
|
||||
license_state: { status: "captured_unreviewed" }, gpu_fit: { fits: "not_assessed" },
|
||||
status: "discovery_security_blocked", rationale: "Unsafe serialization blocks execution.",
|
||||
evidence_fingerprint: "b".repeat(64), created_at: "2026-08-25T14:00:00Z",
|
||||
immutable_at: "2026-08-25T14:00:00Z",
|
||||
}];
|
||||
if (url.endsWith("/evaluation-runs/shadow-run/cases")) payload = [{
|
||||
id: "result-1", run_id: "shadow-run", case_id: "case-1", case_key: "critical-query",
|
||||
critical: true, ranked_results: [{ chunk_id: "chunk-1", score: .03 }],
|
||||
relevant_results: ["chunk-1"], first_relevant_rank: 1,
|
||||
metrics: { recall_at_5: 1, recall_at_10: 1, mrr: 1, ndcg_at_10: 1 },
|
||||
latency_ms: 12.5,
|
||||
}];
|
||||
return Promise.resolve(new Response(JSON.stringify(payload)));
|
||||
}));
|
||||
}
|
||||
|
||||
afterEach(() => { cleanup(); vi.unstubAllGlobals(); });
|
||||
|
||||
describe("M6 evaluation workspace", () => {
|
||||
it("shows real current and shadow runs without claiming cutover", async () => {
|
||||
mockEvaluation(); render(<EvaluationWorkspace />);
|
||||
expect(await screen.findByText("SHADOW")).toBeInTheDocument();
|
||||
expect(screen.getByText("CURRENT")).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole("button", { name: "comparisons" }));
|
||||
expect(screen.getByText("eligible")).toBeInTheDocument();
|
||||
expect(screen.getByText(/Promotion performed: no/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders migration progress, failures, retries and background QoS", async () => {
|
||||
mockEvaluation(); render(<EvaluationWorkspace />);
|
||||
await screen.findByText("SHADOW");
|
||||
fireEvent.click(screen.getByRole("button", { name: "migrations" }));
|
||||
expect(screen.getByText("597 / 597 chunks · 0 failed · 2 retried")).toBeInTheDocument();
|
||||
expect(screen.getByText(/Priority background/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("supports bounded critical-case drilldown", async () => {
|
||||
mockEvaluation(); render(<EvaluationWorkspace />);
|
||||
fireEvent.click((await screen.findAllByRole("button", { name: "Inspect case evidence" }))[0]);
|
||||
expect(await screen.findByText("critical-query")).toBeInTheDocument();
|
||||
expect(screen.getByText("critical")).toBeInTheDocument();
|
||||
expect(screen.getByText(/First relevant rank: 1/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders a multi-candidate evidence matrix without a weighted score", async () => {
|
||||
mockEvaluation(); render(<EvaluationWorkspace />);
|
||||
await screen.findByText("SHADOW");
|
||||
fireEvent.click(screen.getByRole("button", { name: "model comparisons" }));
|
||||
expect(screen.getByRole("table", { name: "Embedding candidate comparison" })).toBeInTheDocument();
|
||||
expect(screen.getByText("Qwen retrieval-aware")).toBeInTheDocument();
|
||||
expect(screen.getByText("1.40 GiB")).toBeInTheDocument();
|
||||
expect(screen.getByText(/No weighted score or automatic cutover/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("separates frozen pools, pipeline runs and blocked discovery evidence", async () => {
|
||||
mockEvaluation(); render(<EvaluationWorkspace />);
|
||||
await screen.findByText("SHADOW");
|
||||
fireEvent.click(screen.getByRole("button", { name: "candidate pools" }));
|
||||
expect(screen.getByText("Frozen top-40")).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole("button", { name: "reranking" }));
|
||||
expect(screen.getByText("Fixed-pool reranking")).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole("button", { name: "discovery" }));
|
||||
expect(screen.getByText("bge-m3")).toBeInTheDocument();
|
||||
expect(screen.getByText("discovery security blocked")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,206 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { AlertTriangle, CheckCircle2, Database, GitCompare, Layers3 } from "lucide-react";
|
||||
|
||||
import { api } from "../lib/api";
|
||||
import type {
|
||||
DiscoveryAssessment, EmbeddingMigration, EvaluationCaseResult, EvaluationComparison, EvaluationRun,
|
||||
EvaluationSuite, ModelComparison, RerankingRun, RetrievalCandidatePool, RetrievalPipelineIdentity,
|
||||
} from "../types";
|
||||
|
||||
type Tab = "suites" | "datasets" | "runs" | "comparisons" | "model comparisons" | "candidate pools" | "pipelines" | "reranking" | "discovery" | "cases" | "migrations";
|
||||
const tabs: Tab[] = ["suites", "datasets", "runs", "comparisons", "model comparisons", "candidate pools", "pipelines", "reranking", "discovery", "cases", "migrations"];
|
||||
|
||||
export function EvaluationWorkspace() {
|
||||
const [tab, setTab] = useState<Tab>("runs");
|
||||
const [suites, setSuites] = useState<EvaluationSuite[]>([]);
|
||||
const [runs, setRuns] = useState<EvaluationRun[]>([]);
|
||||
const [migrations, setMigrations] = useState<EmbeddingMigration[]>([]);
|
||||
const [comparisons, setComparisons] = useState<EvaluationComparison[]>([]);
|
||||
const [modelComparisons, setModelComparisons] = useState<ModelComparison[]>([]);
|
||||
const [candidatePools, setCandidatePools] = useState<RetrievalCandidatePool[]>([]);
|
||||
const [pipelines, setPipelines] = useState<RetrievalPipelineIdentity[]>([]);
|
||||
const [rerankingRuns, setRerankingRuns] = useState<RerankingRun[]>([]);
|
||||
const [discovery, setDiscovery] = useState<DiscoveryAssessment[]>([]);
|
||||
const [selectedRun, setSelectedRun] = useState<string | null>(null);
|
||||
const [cases, setCases] = useState<EvaluationCaseResult[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([api.evaluationSuites(), api.evaluationRuns(), api.embeddingMigrations(), api.evaluationComparisons(), api.modelComparisons(), api.retrievalCandidatePools(), api.retrievalPipelineIdentities(), api.rerankingRuns(), api.discoveryAssessments()])
|
||||
.then(([suiteData, runData, migrationData, comparisonData, modelComparisonData, poolData, pipelineData, rerankingData, discoveryData]) => {
|
||||
setSuites(suiteData); setRuns(runData); setMigrations(migrationData); setComparisons(comparisonData);
|
||||
setModelComparisons(modelComparisonData); setCandidatePools(poolData); setPipelines(pipelineData);
|
||||
setRerankingRuns(rerankingData); setDiscovery(discoveryData);
|
||||
setSelectedRun(runData[0]?.id ?? null);
|
||||
})
|
||||
.catch((cause: unknown) => setError(
|
||||
cause instanceof Error ? cause.message : "Evaluation evidence could not be loaded",
|
||||
))
|
||||
.finally(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedRun || tab !== "cases") return;
|
||||
api.evaluationCases(selectedRun).then(setCases).catch((cause: unknown) => setError(
|
||||
cause instanceof Error ? cause.message : "Case evidence could not be loaded",
|
||||
));
|
||||
}, [selectedRun, tab]);
|
||||
|
||||
if (loading) return <div className="state-card"><span className="state-spinner" />Loading evaluation evidence…</div>;
|
||||
return <div className="evaluation-stack">
|
||||
<article className="panel evaluation-boundary">
|
||||
<Layers3 size={24} /><div><span className="eyebrow">M6–M8 · PROJECT-SPECIFIC EVIDENCE</span>
|
||||
<h2>Retrieval evaluation workspace</h2><p>Embedding, fixed-pool reranking and discovery evidence stay distinct. Eligibility is evidence, never an automatic cutover.</p></div>
|
||||
</article>
|
||||
{error ? <div className="error-banner"><strong>Evidence unavailable</strong><span>{error}</span></div> : null}
|
||||
<nav className="evaluation-tabs" aria-label="Evaluation workspace">
|
||||
{tabs.map((item) => <button key={item} className={tab === item ? "active" : ""} onClick={() => setTab(item)}>{item}</button>)}
|
||||
</nav>
|
||||
{tab === "suites" || tab === "datasets" ? <SuiteList suites={suites} datasetMode={tab === "datasets"} /> : null}
|
||||
{tab === "runs" ? <RunList runs={runs} onCases={(id) => { setSelectedRun(id); setTab("cases"); }} /> : null}
|
||||
{tab === "comparisons" ? <ComparisonSummary runs={runs} comparison={comparisons[0]} /> : null}
|
||||
{tab === "model comparisons" ? <ModelComparisonMatrix runs={runs} comparison={modelComparisons[0]} /> : null}
|
||||
{tab === "candidate pools" ? <CandidatePoolList pools={candidatePools} /> : null}
|
||||
{tab === "pipelines" ? <PipelineList pipelines={pipelines} /> : null}
|
||||
{tab === "reranking" ? <RerankingRunList runs={rerankingRuns} /> : null}
|
||||
{tab === "discovery" ? <DiscoveryList assessments={discovery} /> : null}
|
||||
{tab === "cases" ? <CaseList runs={runs} selectedRun={selectedRun} onSelect={setSelectedRun} cases={cases} /> : null}
|
||||
{tab === "migrations" ? <MigrationList migrations={migrations} /> : null}
|
||||
</div>;
|
||||
}
|
||||
|
||||
function CandidatePoolList({ pools }: { pools: RetrievalCandidatePool[] }) {
|
||||
if (!pools.length) return <Empty label="No immutable retrieval candidate pools are registered." />;
|
||||
return <section className="evaluation-grid">{pools.map((pool) => <article className="panel" key={pool.id}>
|
||||
<div className="evaluation-title"><strong>Frozen top-{pool.candidate_count}</strong><span className="status-chip completed">immutable</span></div>
|
||||
<p>{pool.source_index_ref}</p><dl className="evaluation-facts"><div><dt>Embedding space</dt><dd>{short(pool.source_embedding_space)}</dd></div><div><dt>Retrieval config</dt><dd>{short(pool.retrieval_config_digest)}</dd></div></dl>
|
||||
<code>{pool.fingerprint}</code>
|
||||
</article>)}</section>;
|
||||
}
|
||||
|
||||
function PipelineList({ pipelines }: { pipelines: RetrievalPipelineIdentity[] }) {
|
||||
if (!pipelines.length) return <Empty label="No immutable retrieval pipeline identities are registered." />;
|
||||
return <section className="evaluation-grid">{pipelines.map((pipeline) => <article className="panel" key={pipeline.id}>
|
||||
<div className="evaluation-title"><strong>{pipeline.reranker_deployment_id ? "Reranked pipeline" : "Control pipeline"}</strong><span className="status-chip completed">{pipeline.migration_class}</span></div>
|
||||
<p>Candidate k {pipeline.candidate_k} → output k {pipeline.output_k}</p><dl className="evaluation-facts"><div><dt>Embedding space</dt><dd>{short(pipeline.embedding_space_ref)}</dd></div><div><dt>Reranker</dt><dd>{pipeline.reranker_deployment_id ? short(pipeline.reranker_deployment_id) : "none"}</dd></div></dl>
|
||||
<code>{pipeline.identity_digest}</code>
|
||||
</article>)}</section>;
|
||||
}
|
||||
|
||||
function RerankingRunList({ runs }: { runs: RerankingRun[] }) {
|
||||
if (!runs.length) return <Empty label="No completed fixed-pool reranking runs are registered." />;
|
||||
return <section className="evaluation-grid">{runs.map((run) => <article className="panel run-card" key={run.id}>
|
||||
<div className="evaluation-title"><span className={`status-chip ${run.status}`}>{run.status}</span><strong>Fixed-pool reranking</strong></div>
|
||||
<MetricStrip metrics={run.aggregate_metrics} /><p>{run.completed_cases} / {run.expected_cases} cases · {run.error_count} errors</p>
|
||||
<p>Rerank p95 {formatMs(run.latency_metrics.rerank_p95_ms)} · total p95 {formatMs(run.latency_metrics.total_p95_ms)}</p>
|
||||
<code>{run.candidate_pool_set_fingerprint}</code>
|
||||
</article>)}</section>;
|
||||
}
|
||||
|
||||
function DiscoveryList({ assessments }: { assessments: DiscoveryAssessment[] }) {
|
||||
if (!assessments.length) return <Empty label="No bounded discovery assessments are registered." />;
|
||||
return <section className="evaluation-grid">{assessments.map((item) => <article className="panel" key={item.id}>
|
||||
<div className="evaluation-title"><strong>{item.candidate_key}</strong><span className={`status-chip ${item.status}`}>{item.status.replaceAll("_", " ")}</span></div>
|
||||
<p>{item.repository_id}@{item.resolved_commit_sha.slice(0, 12)}</p><p>{item.rationale}</p>
|
||||
<dl className="evaluation-facts"><div><dt>Artifact</dt><dd>{formatOptionalBytes(item.artifact_evidence.bytes)}</dd></div><div><dt>Security</dt><dd>{stringValue(item.security_state.status)}</dd></div><div><dt>GPU fit</dt><dd>{stringValue(item.gpu_fit.fits)}</dd></div><div><dt>License</dt><dd>{stringValue(item.license_state.status)}</dd></div></dl>
|
||||
<code>{item.evidence_fingerprint}</code>
|
||||
</article>)}</section>;
|
||||
}
|
||||
|
||||
function ModelComparisonMatrix({ runs, comparison }: { runs: EvaluationRun[]; comparison?: ModelComparison }) {
|
||||
if (!comparison) return <Empty label="No first-class candidate comparison has been recorded." />;
|
||||
const baseline = runs.find((run) => run.id === comparison.current_run_id);
|
||||
if (!baseline) return <Empty label="The comparison baseline run is unavailable." />;
|
||||
const baselineLatency = comparison.candidates.find((item) => item.status === "evaluated")?.latency_ms ?? {};
|
||||
const rows = [{
|
||||
key: "current", label: "nomic baseline", status: "current", metrics: baseline.aggregate_metrics,
|
||||
critical: 0, p50: baselineLatency.baseline_p50, p95: baselineLatency.baseline_p95,
|
||||
vram: undefined,
|
||||
}, ...comparison.candidates.map((item) => ({
|
||||
key: item.candidate_key, label: item.label, status: item.status,
|
||||
metrics: item.quality_metrics ?? {}, critical: item.critical_regressions,
|
||||
p50: item.latency_ms.p50, p95: item.latency_ms.p95,
|
||||
vram: numericValue(item.resource_evidence.resident_vram_bytes),
|
||||
}))];
|
||||
return <article className="panel comparison-card"><div className="evaluation-title"><GitCompare size={20} /><div><h3>{comparison.title}</h3><p>Raw project evidence; unknown remains unknown. No weighted score or automatic cutover.</p></div></div>
|
||||
<div className="model-comparison-table" role="table" aria-label="Embedding candidate comparison">
|
||||
<div role="row"><strong>Candidate</strong><strong>R@5</strong><strong>R@10</strong><strong>MRR</strong><strong>nDCG@10</strong><strong>Critical</strong><strong>p50</strong><strong>p95</strong><strong>Resident VRAM</strong><strong>Status</strong></div>
|
||||
{rows.map((row) => <div role="row" key={row.key}><strong>{row.label}</strong>
|
||||
<span>{formatMetric(row.metrics.recall_at_5)}</span><span>{formatMetric(row.metrics.recall_at_10)}</span>
|
||||
<span>{formatMetric(row.metrics.mrr)}</span><span>{formatMetric(row.metrics.ndcg_at_10)}</span>
|
||||
<span>{row.critical ?? "—"}</span><span>{formatMs(row.p50)}</span><span>{formatMs(row.p95)}</span>
|
||||
<span>{row.vram == null ? "—" : formatBytes(row.vram)}</span><span className={`status-chip ${row.status}`}>{row.status}</span>
|
||||
</div>)}
|
||||
</div><p><AlertTriangle size={15} /> Comparability: {comparison.comparability}. Candidate blockers stay visible in Advisor evidence.</p>
|
||||
</article>;
|
||||
}
|
||||
|
||||
function SuiteList({ suites, datasetMode }: { suites: EvaluationSuite[]; datasetMode: boolean }) {
|
||||
if (!suites.length) return <Empty label="No immutable evaluation suite revisions are registered." />;
|
||||
return <section className="evaluation-grid">{suites.map((suite) => <article className="panel" key={suite.id}>
|
||||
<span className="eyebrow">{datasetMode ? "DATASET REVISION" : "SUITE REVISION"}</span>
|
||||
<h3>{suite.name}</h3><p>{suite.description}</p><dl className="evaluation-facts">
|
||||
<div><dt>Revision</dt><dd>{suite.latest_revision}</dd></div>
|
||||
<div><dt>Cases</dt><dd>{suite.case_count}</dd></div>
|
||||
<div><dt>Critical</dt><dd>{suite.critical_case_count}</dd></div>
|
||||
</dl><code>{suite.latest_revision_id}</code>
|
||||
</article>)}</section>;
|
||||
}
|
||||
|
||||
function RunList({ runs, onCases }: { runs: EvaluationRun[]; onCases: (id: string) => void }) {
|
||||
if (!runs.length) return <Empty label="No baseline or shadow evaluation run has been recorded." />;
|
||||
return <section className="evaluation-grid">{runs.map((run) => <article className="panel run-card" key={run.id}>
|
||||
<div className="evaluation-title"><span className={`status-chip ${run.status}`}>{run.status}</span><strong>{run.target_kind.toUpperCase()}</strong></div>
|
||||
<code>{run.target_index_ref}</code><MetricStrip metrics={run.aggregate_metrics} />
|
||||
<p>{run.completed_cases} / {run.expected_cases} cases · {run.error_count} errors</p>
|
||||
<button onClick={() => onCases(run.id)}>Inspect case evidence</button>
|
||||
</article>)}</section>;
|
||||
}
|
||||
|
||||
function ComparisonSummary({ runs, comparison }: { runs: EvaluationRun[]; comparison?: EvaluationComparison }) {
|
||||
const current = runs.find((run) => run.target_kind === "current" && run.status.startsWith("completed"));
|
||||
const shadow = runs.find((run) => run.target_kind === "shadow" && run.status.startsWith("completed"));
|
||||
if (!current || !shadow || !comparison) return <Empty label="A persisted comparable CURRENT and SHADOW comparison is required." />;
|
||||
const metrics = Object.keys(current.aggregate_metrics);
|
||||
return <article className="panel comparison-card"><div className="evaluation-title"><GitCompare size={20} /><h3>Current versus shadow</h3></div>
|
||||
<div className="comparison-table"><div><strong>Metric</strong><strong>Current</strong><strong>Shadow</strong><strong>Delta</strong></div>
|
||||
{metrics.map((metric) => { const delta = shadow.aggregate_metrics[metric] - current.aggregate_metrics[metric]; return <div key={metric}>
|
||||
<span>{metric.replaceAll("_", " ")}</span><span>{formatMetric(current.aggregate_metrics[metric])}</span>
|
||||
<span>{formatMetric(shadow.aggregate_metrics[metric])}</span><span>{delta >= 0 ? "+" : ""}{formatMetric(delta)}</span>
|
||||
</div>; })}</div><div className="comparison-verdict"><span className={`status-chip ${comparison.promotion_eligibility}`}>{comparison.promotion_eligibility.replaceAll("_", " ")}</span><span>{comparison.improved_cases} improved · {comparison.regressed_cases} regressed · {comparison.unchanged_cases} unchanged · {comparison.critical_regressions} critical regressions</span></div>
|
||||
<p><AlertTriangle size={15} /> Promotion performed: no. Comparability: {comparison.comparability}.</p>
|
||||
</article>;
|
||||
}
|
||||
|
||||
function CaseList({ runs, selectedRun, onSelect, cases }: { runs: EvaluationRun[]; selectedRun: string | null; onSelect: (id: string) => void; cases: EvaluationCaseResult[] }) {
|
||||
return <div className="case-workspace"><label>Evaluation run<select value={selectedRun ?? ""} onChange={(event) => onSelect(event.target.value)}>
|
||||
{runs.map((run) => <option key={run.id} value={run.id}>{run.target_kind} · {run.id.slice(0, 8)}</option>)}</select></label>
|
||||
{!cases.length ? <Empty label="No bounded per-case evidence is available for this run." /> : <section className="case-list">{cases.map((item) => <article className="panel" key={item.id}>
|
||||
<div className="evaluation-title"><strong>{item.case_key}</strong>{item.critical ? <span className="critical-chip">critical</span> : null}</div>
|
||||
<p>First relevant rank: {item.first_relevant_rank ?? "miss"} · {item.latency_ms.toFixed(1)} ms</p>
|
||||
<MetricStrip metrics={item.metrics} /><details><summary>{item.ranked_results.length} ranked IDs</summary>
|
||||
<ol>{item.ranked_results.map((result) => <li key={result.chunk_id}><code>{result.chunk_id}</code><span>{result.score.toFixed(6)}</span></li>)}</ol></details>
|
||||
</article>)}</section>}
|
||||
</div>;
|
||||
}
|
||||
|
||||
function MigrationList({ migrations }: { migrations: EmbeddingMigration[] }) {
|
||||
if (!migrations.length) return <Empty label="No isolated embedding migration has been registered." />;
|
||||
return <section className="evaluation-grid">{migrations.map((item) => { const percent = item.total_chunks ? item.completed_chunks / item.total_chunks * 100 : 0; return <article className="panel migration-card" key={item.id}>
|
||||
<div className="evaluation-title"><strong>{item.status.replaceAll("_", " ")}</strong>{item.evaluation_eligibility ? <CheckCircle2 size={18} /> : <AlertTriangle size={18} />}</div>
|
||||
<code>{item.target_index_ref}</code><div className="migration-progress" aria-label={`${percent.toFixed(1)}% complete`}><span style={{ width: `${percent}%` }} /></div>
|
||||
<p>{item.completed_chunks} / {item.total_chunks} chunks · {item.failed_chunks} failed · {item.retried_chunks} retried</p>
|
||||
<small>Priority {item.priority} · batch {item.batch_size} · concurrency {item.concurrency}</small>
|
||||
</article>; })}</section>;
|
||||
}
|
||||
|
||||
function MetricStrip({ metrics }: { metrics: Record<string, number> }) { return <div className="metric-strip">{Object.entries(metrics).map(([key, value]) => <span key={key}><small>{key.replaceAll("_", " ")}</small><strong>{formatMetric(value)}</strong></span>)}</div>; }
|
||||
function Empty({ label }: { label: string }) { return <div className="state-card"><Database size={18} />{label}</div>; }
|
||||
function formatMetric(value: number | undefined): string { return value == null ? "—" : value.toFixed(4); }
|
||||
function formatMs(value: number | undefined): string { return value == null ? "—" : `${value.toFixed(1)} ms`; }
|
||||
function numericValue(value: unknown): number | undefined { return typeof value === "number" ? value : undefined; }
|
||||
function formatBytes(value: number): string { return `${(value / 1024 ** 3).toFixed(2)} GiB`; }
|
||||
function formatOptionalBytes(value: unknown): string { return typeof value === "number" ? formatBytes(value) : "—"; }
|
||||
function stringValue(value: unknown): string { return value == null ? "unknown" : String(value).replaceAll("_", " "); }
|
||||
function short(value: string): string { return value.length > 18 ? `${value.slice(0, 12)}…` : value; }
|
||||
@@ -0,0 +1,49 @@
|
||||
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { LifecycleWorkspace } from "./LifecycleWorkspace";
|
||||
|
||||
function mockLifecycle(): void {
|
||||
vi.stubGlobal("fetch", vi.fn((input: RequestInfo | URL) => {
|
||||
const url = String(input); let payload: unknown = [];
|
||||
if (url.includes("approval-policies")) payload = [{ id: "policy-1", key: "project-production", revision: 1, scope: "PROJECT_PRODUCTION", requirements: { production_validation: "SATISFIED" }, fingerprint: "a".repeat(64), active: true, created_by: "system", created_at: "2026-08-26T10:00:00Z" }];
|
||||
if (url.includes("approval-requests")) payload = [{ id: "approval-1", policy_revision_id: "policy-1", policy_key: "project-production", policy_revision: 1, version: 1, target_type: "PROJECT_BINDING", target_ref: "examplevision-vision", environment: "PRODUCTION", requested_transition: "STABLE", evidence_snapshot: { declared: { artifact_set_id: "artifact-1", model_revision_id: "revision-1", security: "APPROVED", license: "APPROVED", runtime_probe_id: "probe-1", evaluation: "EVALUATED", engineering_integration: "PASS", platform_readiness: "PROMOTION_ELIGIBLE", project_fit: "REQUIRES_MORE_EVIDENCE", scheduler_readiness: "READY", rollback_target_ref: "vision-lab-stable", production_validation: "DEFERRED_EXTERNAL_VALIDATION" }, resolved: {} }, evidence_fingerprint: "b".repeat(64), status: "BLOCKED", blockers: ["REQUIRED_EXTERNAL_VALIDATION_NOT_SATISFIED"], warnings: [], requested_by: "operator", reason: "M12 blocker rehearsal", created_at: "2026-08-26T10:01:00Z" }];
|
||||
if (url.includes("subjects")) payload = [{ id: "subject-1", target_type: "LAB_REHEARSAL", target_ref: "vision-safe-lab", environment: "LAB", state: "LAB_READY", version: 1, created_at: "2026-08-26T10:00:00Z", updated_at: "2026-08-26T10:00:00Z" }];
|
||||
if (url.includes("promotion-plans")) payload = [{ id: "plan-1", approval_request_id: "approval-2", subject_id: "subject-1", current_state: "LAB_READY", desired_state: "LAB_STABLE", migration_class: "behavioral", rollback_target_ref: "vision-lab-previous", project_consumers: [], affected_identities: {}, impact_analysis: { projects: [], runtime_deployments: ["deployment-1"] }, canary_strategy: { mode: "REQUEST" }, drain_strategy: {}, health_gates: {}, automatic_abort_conditions: ["HEALTH_FAILURE"], plan_fingerprint: "c".repeat(64), status: "APPROVED", version: 2, created_by: "requester", approved_by: "approver", immutable_at: "2026-08-26T10:02:00Z", created_at: "2026-08-26T10:01:00Z" }];
|
||||
if (url.includes("operations")) payload = [{ id: "operation-1", promotion_plan_id: "plan-1", version: 2, stage: "VERIFYING", idempotency_key: "m12-test", expected_subject_version: 1, requester: "requester", approver: "approver", executor: "executor", failure_details: {}, started_at: "2026-08-26T10:03:00Z", canary: { id: "canary-1", mode: "REQUEST", traffic_percent: 0, target_request_count: 3, request_count: 2, error_count: 0, latency_p95_ms: 92, status: "RUNNING", thresholds: { max_error_rate: 0, max_latency_p95_ms: 1000 } } }];
|
||||
if (url.includes("retention-policies")) payload = [{ id: "retention-1", key: "standard-rollback", revision: 1, minimum_rollback_days: 30, requirements: { previous_stable: "rollback_window" }, fingerprint: "d".repeat(64), active: true, created_by: "system", created_at: "2026-08-26T10:00:00Z" }];
|
||||
if (url.includes("cleanup-plans")) payload = [{ id: "cleanup-1", target_type: "ARTIFACT_SET", target_ref: "artifact-1", action: "ARCHIVE_METADATA", dependencies: [{ type: "CAPABILITY_DEPLOYMENT", ref: "deployment-1", blocking: true }], dependency_digest: "e".repeat(64), reclaimable_bytes: 1_500_000_000, retention_state: "ROLLBACK_RETAINED", blockers: ["CAPABILITY_DEPLOYMENT:deployment-1"], status: "BLOCKED", created_by: "operator", created_at: "2026-08-26T10:04:00Z" }];
|
||||
if (url.includes("events")) payload = [{ id: "event-1", event_type: "APPROVAL_REQUESTED", object_type: "PROJECT_BINDING", object_ref: "examplevision-vision", actor: "operator", actor_role: "REQUESTER", evidence_ids: [], reason: "M12 blocker rehearsal", change_id: "approval-1", details: {}, occurred_at: "2026-08-26T10:01:00Z" }];
|
||||
return Promise.resolve(new Response(JSON.stringify(payload), { status: 200 }));
|
||||
}));
|
||||
}
|
||||
|
||||
afterEach(() => { cleanup(); vi.unstubAllGlobals(); });
|
||||
|
||||
describe("M12 lifecycle operator workspace", () => {
|
||||
it("shows exact deferred-validation evidence and a human blocker", async () => {
|
||||
mockLifecycle(); render(<LifecycleWorkspace />);
|
||||
fireEvent.change(screen.getByLabelText("Operator API key"), { target: { value: "operator-test" } });
|
||||
fireEvent.click(screen.getByRole("button", { name: "Load lifecycle" }));
|
||||
expect(await screen.findByText("examplevision-vision")).toBeInTheDocument();
|
||||
expect(screen.getByText("REQUIRED_EXTERNAL_VALIDATION_NOT_SATISFIED")).toBeInTheDocument();
|
||||
expect(screen.getByText(/external production validation is deferred/i)).toBeInTheDocument();
|
||||
expect(screen.getByText("DEFERRED_EXTERNAL_VALIDATION")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders canary thresholds, rollback controls, retention and dependency blockers", async () => {
|
||||
mockLifecycle(); render(<LifecycleWorkspace />);
|
||||
fireEvent.change(screen.getByLabelText("Operator API key"), { target: { value: "operator-test" } });
|
||||
fireEvent.click(screen.getByRole("button", { name: "Load lifecycle" }));
|
||||
await screen.findByText("examplevision-vision");
|
||||
fireEvent.click(screen.getByRole("tab", { name: "Canaries" }));
|
||||
expect(screen.getByText("2 / 3")).toBeInTheDocument(); expect(screen.getByText("92 ms")).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole("tab", { name: "Rollbacks" }));
|
||||
expect(screen.getByRole("button", { name: "Review rollback" })).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole("tab", { name: "Retention" }));
|
||||
expect(screen.getByText("30 days")).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole("tab", { name: "Cleanup" }));
|
||||
expect(screen.getByText("Cannot delete: CAPABILITY_DEPLOYMENT")).toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Review explicit execution" })).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,141 @@
|
||||
import { FormEvent, useEffect, useMemo, useState } from "react";
|
||||
import { Archive, CheckCircle2, Clock3, GitPullRequest, History, LockKeyhole, RefreshCw, RotateCcw, ShieldAlert, Trash2 } from "lucide-react";
|
||||
|
||||
import { api, operatorCredentialReference } from "../lib/api";
|
||||
import { TabList, TabPanel } from "./TabList";
|
||||
import type { CleanupPlan, LifecycleApproval, LifecycleEvent, LifecycleOperation, LifecyclePolicy, LifecycleSubject, PromotionPlan, RetentionPolicy } from "../types";
|
||||
|
||||
type Tab = "approvals" | "plans" | "canaries" | "rollbacks" | "deprecations" | "retention" | "cleanup" | "history";
|
||||
const tabs: Array<{ key: Tab; label: string }> = [
|
||||
{ key: "approvals", label: "Approvals" }, { key: "plans", label: "Promotion Plans" },
|
||||
{ key: "canaries", label: "Canaries" }, { key: "rollbacks", label: "Rollbacks" },
|
||||
{ key: "deprecations", label: "Deprecations" }, { key: "retention", label: "Retention" },
|
||||
{ key: "cleanup", label: "Cleanup" }, { key: "history", label: "History" },
|
||||
];
|
||||
|
||||
interface LifecycleData {
|
||||
policies: LifecyclePolicy[]; subjects: LifecycleSubject[]; approvals: LifecycleApproval[];
|
||||
plans: PromotionPlan[]; operations: LifecycleOperation[]; retention: RetentionPolicy[];
|
||||
cleanup: CleanupPlan[]; events: LifecycleEvent[];
|
||||
}
|
||||
const emptyData: LifecycleData = { policies: [], subjects: [], approvals: [], plans: [], operations: [], retention: [], cleanup: [], events: [] };
|
||||
|
||||
function explainBlocker(code: string): string {
|
||||
const messages: Record<string, string> = {
|
||||
REQUIRED_EXTERNAL_VALIDATION_NOT_SATISFIED: "Owner-photo or equivalent external production validation is deferred.",
|
||||
PROJECT_PRODUCTION_VALIDATION_NOT_SATISFIED: "Required project production validation has not been satisfied.",
|
||||
PROJECT_PRODUCTION_FIT_NOT_ELIGIBLE: "Project-specific evidence does not support production use.",
|
||||
MISSING_PRODUCTION_EXECUTION_APPROVAL: "No exact production-approved deployment evidence exists.",
|
||||
MISSING_EVALUATION_RUNS: "No completed production evaluation run is attached.",
|
||||
SECURITY_NOT_APPROVED: "Supply-chain security evidence is not production-approved.",
|
||||
LICENSE_NOT_APPROVED: "License evidence is not production-approved.",
|
||||
CRITICAL_PROJECT_REGRESSION: "A critical project regression blocks aggregate-score override.",
|
||||
SCHEDULER_NOT_READY: "Current scheduler evidence cannot safely place the candidate.",
|
||||
};
|
||||
return messages[code] ?? code.replaceAll("_", " ").toLowerCase();
|
||||
}
|
||||
function short(value?: string | null): string { return value ? `${value.slice(0, 12)}${value.length > 12 ? "…" : ""}` : "—"; }
|
||||
function date(value?: string | null): string { return value ? new Date(value).toLocaleString() : "—"; }
|
||||
function bytes(value: number): string { return value < 1024 ** 2 ? `${(value / 1024).toFixed(1)} KiB` : `${(value / 1024 ** 3).toFixed(2)} GiB`; }
|
||||
|
||||
export function LifecycleWorkspace() {
|
||||
const sessionCredential = operatorCredentialReference();
|
||||
const [tab, setTab] = useState<Tab>("approvals");
|
||||
const [token, setToken] = useState(sessionCredential ?? ""); const [data, setData] = useState<LifecycleData>(emptyData);
|
||||
const [loading, setLoading] = useState(false); const [error, setError] = useState<string | null>(null);
|
||||
const [notice, setNotice] = useState<string | null>(null);
|
||||
|
||||
async function refresh() {
|
||||
if (!token) return; setLoading(true); setError(null);
|
||||
try {
|
||||
const [policies, subjects, approvals, plans, operations, retention, cleanup, events] = await Promise.all([
|
||||
api.lifecyclePolicies(token), api.lifecycleSubjects(token), api.lifecycleApprovals(token),
|
||||
api.lifecyclePlans(token), api.lifecycleOperations(token), api.lifecycleRetentionPolicies(token),
|
||||
api.lifecycleCleanupPlans(token), api.lifecycleEvents(token),
|
||||
]);
|
||||
setData({ policies, subjects, approvals, plans, operations, retention, cleanup, events });
|
||||
} catch (cause) { setError(cause instanceof Error ? cause.message : "Lifecycle state unavailable"); }
|
||||
finally { setLoading(false); }
|
||||
}
|
||||
async function mutate(action: () => Promise<unknown>, success: string) {
|
||||
setError(null); setNotice(null);
|
||||
try { await action(); setNotice(success); await refresh(); }
|
||||
catch (cause) { setError(cause instanceof Error ? cause.message : "Lifecycle operation failed"); }
|
||||
}
|
||||
|
||||
useEffect(() => { if (sessionCredential) void refresh(); }, [sessionCredential]);
|
||||
|
||||
const canaries = data.operations.filter((item) => item.canary);
|
||||
return <div className="lifecycle-stack">
|
||||
{!sessionCredential ? <form className="panel lifecycle-boundary" onSubmit={(event) => { event.preventDefault(); void refresh(); }}><LockKeyhole size={25} /><div><span className="eyebrow">M12 · PRODUCTION CHANGE CONTROL</span><h2>Evidence before authority, plan before execution</h2><p>The credential stays in memory. Requester, approver and executor remain distinct in every journaled operation.</p></div><label htmlFor="lifecycle-operator-key">Operator API key</label><input id="lifecycle-operator-key" name="operator-api-key" type="password" value={token} onChange={(event) => setToken(event.target.value)} autoComplete="off" /><button type="submit" disabled={!token || loading}><RefreshCw size={15} className={loading ? "spin" : ""} />{loading ? "Loading…" : "Load lifecycle"}</button></form> : null}
|
||||
{error ? <div className="error-banner"><strong>Lifecycle action denied</strong><span>{error}</span></div> : null}
|
||||
{notice ? <div className="lifecycle-notice"><CheckCircle2 size={16} />{notice}</div> : null}
|
||||
<TabList idPrefix="lifecycle" label="Lifecycle views" className="lifecycle-tabs" tabs={tabs} active={tab} onSelect={setTab} />
|
||||
<TabPanel idPrefix="lifecycle" active={tab}>
|
||||
{tab === "approvals" ? <Approvals policies={data.policies} approvals={data.approvals} token={token} mutate={mutate} /> : null}
|
||||
{tab === "plans" ? <Plans plans={data.plans} subjects={data.subjects} token={token} mutate={mutate} /> : null}
|
||||
{tab === "canaries" ? <Canaries operations={canaries} token={token} mutate={mutate} /> : null}
|
||||
{tab === "rollbacks" ? <Rollbacks operations={data.operations} token={token} mutate={mutate} /> : null}
|
||||
{tab === "deprecations" ? <Deprecations subjects={data.subjects} plans={data.plans} /> : null}
|
||||
{tab === "retention" ? <Retention policies={data.retention} plans={data.plans} /> : null}
|
||||
{tab === "cleanup" ? <Cleanup plans={data.cleanup} token={token} mutate={mutate} /> : null}
|
||||
{tab === "history" ? <Timeline events={data.events} /> : null}
|
||||
</TabPanel>
|
||||
</div>;
|
||||
}
|
||||
|
||||
function Approvals({ policies, approvals, token, mutate }: { policies: LifecyclePolicy[]; approvals: LifecycleApproval[]; token: string; mutate: (action: () => Promise<unknown>, success: string) => Promise<void> }) {
|
||||
const [show, setShow] = useState(false); const [policy, setPolicy] = useState("project-production");
|
||||
const [targetType, setTargetType] = useState("CAPABILITY"); const [targetRef, setTargetRef] = useState("vision.embedding@1");
|
||||
const [evidence, setEvidence] = useState('{"production_validation":"DEFERRED_EXTERNAL_VALIDATION","project_fit":"REQUIRES_MORE_EVIDENCE"}');
|
||||
async function submit(event: FormEvent) { event.preventDefault(); const bundle = JSON.parse(evidence) as object; await mutate(() => api.createLifecycleApproval({ policy_key: policy, target_type: targetType, target_ref: targetRef, environment: policy === "lab-promotion" ? "LAB" : "PRODUCTION", requested_transition: policy === "lab-promotion" ? "LAB_STABLE" : "STABLE", evidence: bundle, requested_by: "operator-ui", reason: "Explicit lifecycle request created from reviewed evidence in the operator workspace" }, token), "Approval request journaled; no deployment was changed."); }
|
||||
return <>
|
||||
<div className="lifecycle-toolbar"><span>{approvals.length} evidence-bound requests · {policies.filter((item) => item.active).length} active policy revisions</span><button onClick={() => setShow((value) => !value)}><GitPullRequest size={15} />New approval request</button></div>
|
||||
{show ? <form className="panel lifecycle-form" onSubmit={submit}><h3>Request exact transition approval</h3><label>Policy<select value={policy} onChange={(event) => setPolicy(event.target.value)}>{policies.filter((item) => item.active).map((item) => <option key={item.id} value={item.key}>{item.key} · revision {item.revision}</option>)}</select></label><label>Target type<select value={targetType} onChange={(event) => setTargetType(event.target.value)}><option>CAPABILITY</option><option>CAPABILITY_DEPLOYMENT</option><option>PROJECT_BINDING</option><option>LAB_REHEARSAL</option></select></label><label>Target reference<input value={targetRef} onChange={(event) => setTargetRef(event.target.value)} /></label><label className="wide">Exact evidence bundle (IDs and typed states)<textarea value={evidence} onChange={(event) => setEvidence(event.target.value)} /></label><button disabled={!token}>Create request only</button></form> : null}
|
||||
<div className="lifecycle-grid">{approvals.map((item) => <ApprovalCard key={item.id} item={item} token={token} mutate={mutate} />)}</div>
|
||||
</>;
|
||||
}
|
||||
|
||||
function ApprovalCard({ item, token, mutate }: { item: LifecycleApproval; token: string; mutate: (action: () => Promise<unknown>, success: string) => Promise<void> }) {
|
||||
const declared = item.evidence_snapshot.declared;
|
||||
const decide = (action: "approve" | "reject" | "revoke") => mutate(() => api.decideLifecycleApproval(item.id, action, { actor: "operator-ui", reason: `${action} decision after review of exact evidence snapshot` }, token), `Approval ${action} decision recorded.`);
|
||||
return <article className={`panel lifecycle-card ${item.status.toLowerCase()}`}><div className="lifecycle-title"><div><span className="eyebrow">{item.policy_key} · REVISION {item.policy_revision}</span><h3>{item.target_ref}</h3><code>{item.target_type} · {item.requested_transition}</code></div><span className={`status-chip ${item.status.toLowerCase()}`}>{item.status}</span></div>
|
||||
<div className="evidence-matrix"><Fact label="ArtifactSet" value={String(declared.artifact_set_id ?? "missing")} /><Fact label="ModelRevision" value={String(declared.model_revision_id ?? "missing")} /><Fact label="Security / license" value={`${String(declared.security ?? "unknown")} / ${String(declared.license ?? "unknown")}`} /><Fact label="Runtime probe" value={String(declared.runtime_probe_id ?? "missing")} /><Fact label="Evaluation" value={String(declared.evaluation ?? "not evaluated")} /><Fact label="Engineering" value={String(declared.engineering_integration ?? "unknown")} /><Fact label="Platform" value={String(declared.platform_readiness ?? "unknown")} /><Fact label="Project fit" value={String(declared.project_fit ?? "unknown")} /><Fact label="Production validation" value={String(declared.production_validation ?? "required")} /><Fact label="Scheduler" value={String(declared.scheduler_readiness ?? "unknown")} /><Fact label="Rollback target" value={String(declared.rollback_target_ref ?? "missing")} /></div>
|
||||
{item.blockers.length ? <div className="blocker-list"><ShieldAlert size={17} /><div><strong>Production promotion blocked</strong>{item.blockers.map((blocker) => <p key={blocker}><code>{blocker}</code><span>{explainBlocker(blocker)}</span></p>)}</div></div> : <div className="eligible-note"><CheckCircle2 size={15} />No policy blockers; an explicit approver decision is still required.</div>}
|
||||
<details><summary>Immutable evidence fingerprint and snapshot</summary><code>{item.evidence_fingerprint}</code><pre>{JSON.stringify(item.evidence_snapshot, null, 2)}</pre></details>
|
||||
<div className="lifecycle-actions">{item.status === "PENDING" ? <><button onClick={() => decide("approve")}>Approve evidence</button><button className="danger" onClick={() => decide("reject")}>Reject</button></> : null}{item.status === "APPROVED" ? <button className="danger" onClick={() => decide("revoke")}>Revoke approval</button> : null}</div>
|
||||
</article>;
|
||||
}
|
||||
|
||||
function Plans({ plans, subjects, token, mutate }: { plans: PromotionPlan[]; subjects: LifecycleSubject[]; token: string; mutate: (action: () => Promise<unknown>, success: string) => Promise<void> }) {
|
||||
const [body, setBody] = useState('{"approval_request_id":"","subject_id":"","desired_state":"LAB_STABLE","migration_class":"behavioral","rollback_target_ref":"lab-previous-stable","project_consumers":[],"affected_identities":{},"canary_strategy":{"mode":"REQUEST","traffic_percent":0,"target_request_count":3,"max_error_rate":0,"max_latency_p95_ms":2000},"health_gates":{"worker":"healthy","scheduler":"ready"},"automatic_abort_conditions":["HEALTH_FAILURE","ERROR_THRESHOLD","LATENCY_THRESHOLD","CAPACITY_FAILURE"],"created_by":"operator-ui"}');
|
||||
const create = (event: FormEvent) => { event.preventDefault(); return mutate(() => api.createLifecyclePlan(JSON.parse(body), token), "Promotion plan created without execution."); };
|
||||
return <><form className="panel lifecycle-json-form" onSubmit={create}><div><span className="eyebrow">PLAN, NOT EXECUTION</span><h3>Impact and rollback preview</h3><p>Use exact approval and subject IDs. An approved plan becomes immutable; changes require a new plan.</p></div><textarea aria-label="Promotion plan JSON" value={body} onChange={(event) => setBody(event.target.value)} /><button disabled={!token}>Create draft plan</button></form><div className="lifecycle-grid">{plans.map((plan) => { const subject = subjects.find((item) => item.id === plan.subject_id); return <article className="panel lifecycle-card" key={plan.id}><div className="lifecycle-title"><div><span className="eyebrow">{plan.migration_class}</span><h3>{subject?.target_ref ?? short(plan.subject_id)}</h3><code>{plan.current_state} → {plan.desired_state}</code></div><span className={`status-chip ${plan.status.toLowerCase()}`}>{plan.status}</span></div><div className="promotion-preview"><Fact label="Exact candidate" value={plan.candidate_deployment_id ?? "logical transition"} /><Fact label="Rollback target" value={plan.rollback_target_ref} /><Fact label="Consumers" value={plan.project_consumers.join(", ") || "none"} /><Fact label="Retention" value="Previous state retained by active policy" /></div><details open><summary>Blast radius</summary><pre>{JSON.stringify(plan.impact_analysis, null, 2)}</pre></details><div className="lifecycle-actions">{plan.status === "DRAFT" ? <button onClick={() => mutate(() => api.approveLifecyclePlan(plan.id, { actor: "operator-ui", reason: "Impact, canary, abort and rollback details reviewed" }, token), "Plan approved and frozen.")}>Approve and freeze</button> : null}{plan.status === "APPROVED" ? <button onClick={() => mutate(() => api.executeLifecyclePlan(plan.id, { executor: "operator-ui", idempotency_key: `ui-${plan.id}`, expected_subject_version: subject?.version ?? 1 }, token), "Lifecycle execution journal started.")}>Execute approved plan</button> : null}</div></article>; })}</div></>;
|
||||
}
|
||||
|
||||
function Canaries({ operations, token, mutate }: { operations: LifecycleOperation[]; token: string; mutate: (action: () => Promise<unknown>, success: string) => Promise<void> }) {
|
||||
return <div className="lifecycle-grid">{operations.map((item) => <article className="panel lifecycle-card" key={item.id}><div className="lifecycle-title"><div><span className="eyebrow">{item.canary?.mode} CANARY</span><h3>{item.canary?.status}</h3><code>{short(item.id)}</code></div><span className={`status-chip ${(item.canary?.status ?? "").toLowerCase()}`}>{item.stage}</span></div><div className="canary-metrics"><Fact label="Requests" value={`${item.canary?.request_count ?? 0} / ${item.canary?.target_request_count ?? 0}`} /><Fact label="Errors" value={String(item.canary?.error_count ?? 0)} /><Fact label="p95" value={`${item.canary?.latency_p95_ms ?? 0} ms`} /><Fact label="Abort trigger" value={item.canary?.abort_trigger ?? "none"} /></div><details><summary>Thresholds and capability scope</summary><pre>{JSON.stringify(item.canary?.thresholds, null, 2)}</pre></details>{item.canary?.status === "RUNNING" ? <div className="lifecycle-actions"><button onClick={() => mutate(() => api.observeLifecycleCanary(item.id, { request_count: 1, error_count: 0, latency_p95_ms: 100, capability_health: "HEALTHY", scheduler_ready: true, critical_project_failures: 0, external_pressure: false, worker_healthy: true }, token), "Bounded canary observation recorded.")}>Record healthy request</button><button className="danger" onClick={() => mutate(() => api.observeLifecycleCanary(item.id, { request_count: 1, error_count: 1, latency_p95_ms: 100, capability_health: "UNHEALTHY", scheduler_ready: true, critical_project_failures: 0, external_pressure: false, worker_healthy: false }, token), "Failure observation recorded; auto-abort applies.")}>Inject LAB health failure</button></div> : null}</article>)}</div>;
|
||||
}
|
||||
|
||||
function Rollbacks({ operations, token, mutate }: { operations: LifecycleOperation[]; token: string; mutate: (action: () => Promise<unknown>, success: string) => Promise<void> }) {
|
||||
const [confirmed, setConfirmed] = useState<string | null>(null);
|
||||
return <div className="lifecycle-grid">{operations.map((item) => <article className="panel lifecycle-card" key={item.id}><div className="lifecycle-title"><div><span className="eyebrow">IMMUTABLE SNAPSHOT</span><h3>{item.stage}</h3><code>{item.id}</code></div><RotateCcw size={20} /></div><Fact label="Requester / approver / executor" value={`${item.requester} / ${item.approver} / ${item.executor}`} /><Fact label="Measured rollback" value={item.rollback_duration_ms == null ? "not performed" : `${item.rollback_duration_ms.toFixed(3)} ms`} />{confirmed === item.id ? <div className="confirm-action"><ShieldAlert size={16} /><span>Restore the exact prior snapshot?</span><button onClick={() => mutate(() => api.rollbackLifecycleOperation(item.id, { actor: "operator-ui", reason: "Explicit rollback after operator review of the immutable snapshot" }, token), "Rollback executed and measured.")}>Confirm rollback</button><button onClick={() => setConfirmed(null)}>Cancel</button></div> : <button className="standalone-action" onClick={() => setConfirmed(item.id)} disabled={item.stage === "ROLLED_BACK"}>Review rollback</button>}</article>)}</div>;
|
||||
}
|
||||
|
||||
function Deprecations({ subjects, plans }: { subjects: LifecycleSubject[]; plans: PromotionPlan[] }) {
|
||||
const records = subjects.filter((item) => ["DRAINING", "DEPRECATED", "ARCHIVED"].includes(item.state) || plans.some((plan) => plan.subject_id === item.id && ["DRAINING", "DEPRECATED", "ARCHIVED"].includes(plan.desired_state)));
|
||||
return <div className="lifecycle-grid">{records.map((item) => <article className="panel lifecycle-card" key={item.id}><Archive size={20} /><span className="eyebrow">{item.environment}</span><h3>{item.target_ref}</h3><Fact label="Lifecycle state" value={item.state} /><Fact label="Superseded by" value={item.superseded_by_ref ?? "not declared"} /><p>Deprecation stops ordinary selection; archive retains identity, hashes, evidence and history. Physical removal remains a separate cleanup action.</p></article>)}</div>;
|
||||
}
|
||||
|
||||
function Retention({ policies, plans }: { policies: RetentionPolicy[]; plans: PromotionPlan[] }) {
|
||||
return <><div className="lifecycle-grid">{policies.map((item) => <article className="panel lifecycle-card" key={item.id}><Clock3 size={20} /><span className="eyebrow">ACTIVE POLICY REVISION {item.revision}</span><h3>{item.key}</h3><Fact label="Minimum rollback retention" value={`${item.minimum_rollback_days} days`} /><code>{item.fingerprint}</code><pre>{JSON.stringify(item.requirements, null, 2)}</pre></article>)}</div><article className="panel"><span className="eyebrow">ROLLBACK TARGETS</span><div className="retention-list">{plans.filter((item) => ["COMMITTED", "ROLLED_BACK"].includes(item.status)).map((item) => <div key={item.id}><code>{item.rollback_target_ref}</code><span>retained after {item.status.toLowerCase()} plan {short(item.id)}</span></div>)}</div></article></>;
|
||||
}
|
||||
|
||||
function Cleanup({ plans, token, mutate }: { plans: CleanupPlan[]; token: string; mutate: (action: () => Promise<unknown>, success: string) => Promise<void> }) {
|
||||
const [target, setTarget] = useState(""); const [type, setType] = useState("ARTIFACT_SET"); const [confirm, setConfirm] = useState<string | null>(null);
|
||||
const totals = useMemo(() => ({ now: plans.filter((item) => item.status === "READY").reduce((sum, item) => sum + item.reclaimable_bytes, 0), blocked: plans.filter((item) => item.status === "BLOCKED").reduce((sum, item) => sum + item.reclaimable_bytes, 0) }), [plans]);
|
||||
return <><div className="cleanup-summary"><Fact label="Reclaimable now" value={bytes(totals.now)} /><Fact label="Blocked / retained" value={bytes(totals.blocked)} /><Fact label="Execution default" value="dry-run only" /></div><form className="panel cleanup-form" onSubmit={(event) => { event.preventDefault(); void mutate(() => api.createLifecycleCleanupPlan({ target_type: type, target_ref: target, action: type === "ARTIFACT_SET" ? "ARCHIVE_METADATA" : "RECORD_LOCATION_REMOVAL", created_by: "operator-ui" }, token), "Dependency and retention dry-run created."); }}><Trash2 size={20} /><label>Object type<select value={type} onChange={(event) => setType(event.target.value)}><option>ARTIFACT_SET</option><option>ARTIFACT_LOCATION</option></select></label><label>Exact object ID<input value={target} onChange={(event) => setTarget(event.target.value)} /></label><button disabled={!target}>Run dependency dry-run</button></form><div className="lifecycle-grid">{plans.map((item) => <article className="panel lifecycle-card" key={item.id}><div className="lifecycle-title"><div><span className="eyebrow">{item.action}</span><h3>{short(item.target_ref)}</h3><code>{item.retention_state}</code></div><span className={`status-chip ${item.status.toLowerCase()}`}>{item.status}</span></div><Fact label="Reclaimable" value={bytes(item.reclaimable_bytes)} />{item.dependencies.map((dependency) => <div className={`dependency-row ${dependency.blocking ? "blocking" : ""}`} key={`${dependency.type}-${dependency.ref}`}><span>{dependency.blocking ? "Cannot delete" : "Observed"}: {dependency.type}</span><code>{short(dependency.ref)}</code></div>)}{confirm === item.id ? <div className="confirm-action"><ShieldAlert size={16} /><span>Dependencies will be rechecked. Confirm the typed storage action completed.</span><button onClick={() => mutate(() => api.executeLifecycleCleanupPlan(item.id, { executor: "operator-ui", confirm: true, physical_removal_confirmed: item.action === "RECORD_LOCATION_REMOVAL" }, token), "Cleanup record committed; provenance retained.")}>Confirm execution</button><button onClick={() => setConfirm(null)}>Cancel</button></div> : item.status === "READY" ? <button className="standalone-action" onClick={() => setConfirm(item.id)}>Review explicit execution</button> : null}</article>)}</div></>;
|
||||
}
|
||||
|
||||
function Timeline({ events }: { events: LifecycleEvent[] }) { return <article className="panel lifecycle-timeline"><span className="eyebrow">APPEND-ONLY CHANGE HISTORY</span>{events.map((item) => <div key={item.id}><History size={15} /><time>{date(item.occurred_at)}</time><div><strong>{item.event_type}</strong><span>{item.object_type} · {item.object_ref}</span><p>{item.reason}</p><small>{item.actor_role}: {item.actor} · {item.from_state ?? "∅"} → {item.to_state ?? "∅"} · change {short(item.change_id)}</small></div></div>)}</article>; }
|
||||
function Fact({ label, value }: { label: string; value: string }) { return <div className="lifecycle-fact"><span>{label}</span><strong title={value}>{value}</strong></div>; }
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
interface Props {
|
||||
label: string;
|
||||
value: ReactNode;
|
||||
hint?: string;
|
||||
}
|
||||
|
||||
export function MetricCard({ label, value, hint }: Props) {
|
||||
return (
|
||||
<article className="metric-card">
|
||||
<span className="eyebrow">{label}</span>
|
||||
<strong>{value}</strong>
|
||||
{hint ? <span className="muted">{hint}</span> : null}
|
||||
</article>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { MigrationWorkspace } from "./MigrationWorkspace";
|
||||
|
||||
function mockMigration(): void {
|
||||
vi.stubGlobal("fetch", vi.fn((input: RequestInfo | URL) => {
|
||||
const url = String(input); let payload: unknown = [];
|
||||
if (url.endsWith("/plans")) payload = [{ id: "plan-1", project_id: "project-1", project_binding_id: "binding-1", capability_contract_id: "contract-1", migration_class: "REQUIRES_REINDEX", environment: "LAB", state: "BACKFILL_PAUSED", version: 7, generation: 1, adapter: { key: "examplerag.qdrant-reindex", version: "1", fingerprint: "a".repeat(64), operations: [] }, source_identity: {}, target_identity: {}, source_data_target: "rag_source_v1", target_shadow_target: "rag_shadow_v2", source_space_ref: "space-v1", target_space_id: "space-2", corpus_revision: "corpus-1", migration_policy_revision: "m13-1", validation_policy_revision_id: "policy-1", lifecycle_approval_id: "approval-1", rollback_target_ref: "rag_source_v1", total_expected_items: 597, completed_items: 256, failed_items: 0, retryable_items: 0, permanent_failed_items: 0, batch_size: 32, priority: "BACKGROUND", rollback_retention_days: 30, plan_fingerprint: "b".repeat(64), approval_fingerprint: "c".repeat(64), failure_details: {}, last_cursor: "256", cancel_requested: false, immutable_at: "2026-08-26T10:00:00Z", created_at: "2026-08-26T10:00:00Z", updated_at: "2026-08-26T10:01:00Z" }];
|
||||
if (url.includes("/validation")) payload = [{ id: "validation-1", migration_plan_id: "plan-1", generation: 1, expected_count: 597, actual_count: 597, missing_count: 0, duplicate_count: 0, malformed_count: 0, non_finite_count: 0, wrong_dimension_count: 0, content_hash_mismatch_count: 0, wrong_space_count: 0, comparable: true, critical_regressions: 0, passed: true, technical_cutover_eligible: true, project_promotion_eligible: false, blockers: ["EXTERNAL_VALIDATION_NOT_SATISFIED"], target_fingerprint: "d".repeat(64), snapshot_fingerprint: "e".repeat(64), evidence: {}, created_at: "2026-08-26T10:02:00Z" }];
|
||||
if (url.includes("/cutovers")) payload = [{ id: "cutover-1", migration_plan_id: "plan-1", stage: "ROLLED_BACK", generation: 1, source_before: "rag_source_v1", target_after: "rag_shadow_v2", external_state_fingerprint: "f".repeat(64), health_evidence: {}, failure_details: {}, switch_duration_ms: 3.2, rollback_duration_ms: 2.1, started_at: "2026-08-26T10:03:00Z", finished_at: "2026-08-26T10:04:00Z" }];
|
||||
if (url.includes("/events")) payload = [{ id: "event-1", migration_plan_id: "plan-1", event_type: "BACKFILL_PAUSED", before_state: "BACKFILLING", after_state: "BACKFILL_PAUSED", actor: "operator", reason: "interruption rehearsal", policy_revision: "m13-1", evidence_refs: [], source_identity: {}, target_identity: {}, generation: 1, change_id: "change-1", details: {}, occurred_at: "2026-08-26T10:01:00Z" }];
|
||||
return Promise.resolve(new Response(JSON.stringify(payload), { status: 200 }));
|
||||
}));
|
||||
}
|
||||
|
||||
afterEach(() => { cleanup(); vi.unstubAllGlobals(); });
|
||||
|
||||
describe("M13 migration operator workspace", () => {
|
||||
it("separates technical cutover from project-promotion eligibility", async () => {
|
||||
mockMigration(); render(<MigrationWorkspace />);
|
||||
fireEvent.change(screen.getByLabelText("Operator API key"), { target: { value: "operator-test" } });
|
||||
fireEvent.click(screen.getByRole("button", { name: "Load migrations" }));
|
||||
await screen.findByText("256 / 597");
|
||||
fireEvent.click(screen.getByRole("tab", { name: "validation" }));
|
||||
expect(screen.getByText("technical pass")).toBeInTheDocument();
|
||||
expect(screen.getByText("external validation not satisfied")).toBeInTheDocument();
|
||||
expect(screen.getByText("blocked")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows resumability, exact cutover identities, rollback timing and audit history", async () => {
|
||||
mockMigration(); render(<MigrationWorkspace />);
|
||||
fireEvent.change(screen.getByLabelText("Operator API key"), { target: { value: "operator-test" } });
|
||||
fireEvent.click(screen.getByRole("button", { name: "Load migrations" }));
|
||||
await screen.findByText("256 / 597"); fireEvent.click(screen.getByRole("tab", { name: "plans" }));
|
||||
expect(screen.getByRole("button", { name: "Start/resume" })).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole("tab", { name: "cutover" }));
|
||||
expect(screen.getByText("3.2 ms")).toBeInTheDocument(); expect(screen.getByText("2.1 ms")).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole("tab", { name: "history" }));
|
||||
expect(screen.getByText("interruption rehearsal")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,84 @@
|
||||
import { FormEvent, useEffect, useMemo, useState } from "react";
|
||||
import { AlertTriangle, CheckCircle2, DatabaseZap, Pause, Play, RefreshCw, ShieldAlert, XCircle } from "lucide-react";
|
||||
|
||||
import { api, operatorCredentialReference } from "../lib/api";
|
||||
import { TabList, TabPanel } from "./TabList";
|
||||
import type { TabDescriptor } from "./TabList";
|
||||
import type { MigrationCutoverOperation, MigrationEvent, MigrationPlan, MigrationValidationSnapshot } from "../types";
|
||||
|
||||
type Tab = "overview" | "plans" | "validation" | "cutover" | "history";
|
||||
const migrationTabs: TabDescriptor<Tab>[] = [
|
||||
{ key: "overview", label: "overview" }, { key: "plans", label: "plans" },
|
||||
{ key: "validation", label: "validation" }, { key: "cutover", label: "cutover" },
|
||||
{ key: "history", label: "history" },
|
||||
];
|
||||
const terminal = new Set(["CUTOVER_COMMITTED", "ROLLED_BACK", "FAILED", "CANCELLED", "MANUAL_INTERVENTION_REQUIRED"]);
|
||||
const short = (value: string) => `${value.slice(0, 12)}${value.length > 12 ? "…" : ""}`;
|
||||
|
||||
export function MigrationWorkspace() {
|
||||
const sessionCredential = operatorCredentialReference();
|
||||
const [token, setToken] = useState(sessionCredential ?? "");
|
||||
const [tab, setTab] = useState<Tab>("overview");
|
||||
const [plans, setPlans] = useState<MigrationPlan[]>([]);
|
||||
const [validations, setValidations] = useState<MigrationValidationSnapshot[]>([]);
|
||||
const [cutovers, setCutovers] = useState<MigrationCutoverOperation[]>([]);
|
||||
const [events, setEvents] = useState<MigrationEvent[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [notice, setNotice] = useState<string | null>(null);
|
||||
const [draft, setDraft] = useState("");
|
||||
|
||||
async function refresh() {
|
||||
if (!token) return;
|
||||
setLoading(true); setError(null);
|
||||
try {
|
||||
const [nextPlans, nextCutovers, nextEvents] = await Promise.all([
|
||||
api.migrationPlans(token), api.migrationCutovers(token), api.migrationEvents(token),
|
||||
]);
|
||||
const snapshots = (await Promise.all(nextPlans.map((plan) => api.migrationValidations(plan.id, token)))).flat();
|
||||
setPlans(nextPlans); setCutovers(nextCutovers); setEvents(nextEvents); setValidations(snapshots);
|
||||
} catch (cause) { setError(cause instanceof Error ? cause.message : "Migration state unavailable"); }
|
||||
finally { setLoading(false); }
|
||||
}
|
||||
async function mutate(action: () => Promise<unknown>, message: string) {
|
||||
setError(null); setNotice(null);
|
||||
try { await action(); setNotice(message); await refresh(); }
|
||||
catch (cause) { setError(cause instanceof Error ? cause.message : "Migration action denied"); }
|
||||
}
|
||||
useEffect(() => { if (sessionCredential) void refresh(); }, [sessionCredential]);
|
||||
async function create(event: FormEvent) {
|
||||
event.preventDefault();
|
||||
await mutate(() => api.createMigrationPlan(JSON.parse(draft) as object, token), "Immutable migration plan recorded; no data-plane work has started.");
|
||||
}
|
||||
const active = useMemo(() => plans.filter((item) => !terminal.has(item.state)), [plans]);
|
||||
const completed = plans.reduce((sum, item) => sum + item.completed_items, 0);
|
||||
const expected = plans.reduce((sum, item) => sum + item.total_expected_items, 0);
|
||||
const latestValidation = new Map(validations.map((item) => [item.migration_plan_id, item]));
|
||||
const action = (plan: MigrationPlan, kind: "start" | "pause" | "cancel") => {
|
||||
const body = { expected_version: plan.version, actor: "operator-ui", reason: `Explicit ${kind} action from M13 operator workspace` };
|
||||
if (kind === "start") return api.startMigrationBackfill(plan.id, body, token);
|
||||
if (kind === "pause") return api.pauseMigrationBackfill(plan.id, body, token);
|
||||
return api.cancelMigration(plan.id, body, token);
|
||||
};
|
||||
|
||||
return <div className="migration-stack">
|
||||
{!sessionCredential ? <form className="panel migration-boundary" onSubmit={(event) => { event.preventDefault(); void refresh(); }}><DatabaseZap size={26} /><div><span className="eyebrow">M13 · STATEFUL MIGRATION CONTROL</span><h2>Shadow first, measured evidence, atomic switch</h2><p>ModelForge stores immutable intent and checkpoints. Registered adapters own bounded data-plane operations; arbitrary code and direct production mutation are not accepted.</p></div><label htmlFor="migrations-operator-key">Operator API key</label><input id="migrations-operator-key" name="operator-api-key" type="password" value={token} onChange={(event) => setToken(event.target.value)} autoComplete="off" /><button type="submit" disabled={!token || loading}><RefreshCw size={15} className={loading ? "spin" : ""} />{loading ? "Loading…" : "Load migrations"}</button></form> : null}
|
||||
{error ? <div className="error-banner"><strong>Migration action denied</strong><span>{error}</span></div> : null}
|
||||
{notice ? <div className="lifecycle-notice"><CheckCircle2 size={16} />{notice}</div> : null}
|
||||
<TabList idPrefix="migrations" label="Migration views" className="lifecycle-tabs" tabs={migrationTabs} active={tab} onSelect={setTab} />
|
||||
<TabPanel idPrefix="migrations" active={tab}>
|
||||
{tab === "overview" ? <><section className="migration-metrics"><Fact label="Active migrations" value={String(active.length)} /><Fact label="Durably completed items" value={`${completed} / ${expected}`} /><Fact label="Cutovers requiring attention" value={String(cutovers.filter((item) => !["COMMITTED", "ROLLED_BACK"].includes(item.stage)).length)} /><Fact label="Project-promotion eligible" value={String(validations.filter((item) => item.project_promotion_eligible).length)} /></section><article className="panel migration-warning"><ShieldAlert size={20} /><div><strong>LAB is not project production</strong><p>An isolated technical cutover can pass while project fit or external validation remains blocked. The two verdicts are displayed independently.</p></div></article></> : null}
|
||||
{tab === "plans" ? <><form className="panel migration-plan-form" onSubmit={create}><div><span className="eyebrow">IMMUTABLE PLAN</span><h3>Record reviewed adapter intent</h3><p>Paste the typed API contract with exact project, capability, source, target, approval and policy identities.</p></div><textarea aria-label="Migration plan JSON" value={draft} onChange={(event) => setDraft(event.target.value)} placeholder='{"migration_class":"REQUIRES_REINDEX", ...}' /><button disabled={!token || !draft}>Create plan only</button></form><div className="migration-list">{plans.map((plan) => <PlanCard key={plan.id} plan={plan} validation={latestValidation.get(plan.id)} mutate={mutate} action={action} />)}</div></> : null}
|
||||
{tab === "validation" ? <div className="migration-list">{validations.map((item) => <article className="panel migration-card" key={item.id}><div className="migration-title"><div><span className="eyebrow">VALIDATION SNAPSHOT</span><h3>{short(item.migration_plan_id)}</h3></div><span className={`status-chip ${item.passed ? "completed" : "failed"}`}>{item.passed ? "technical pass" : "blocked"}</span></div><div className="migration-eligibility"><Fact label="Technical cutover" value={item.technical_cutover_eligible ? "eligible" : "blocked"} /><Fact label="Project promotion" value={item.project_promotion_eligible ? "eligible" : "blocked"} /><Fact label="Completeness" value={`${item.actual_count}/${item.expected_count}`} /><Fact label="Critical regressions" value={String(item.critical_regressions)} /></div>{item.blockers.map((blocker) => <div className="migration-blocker" key={blocker}><AlertTriangle size={14} />{blocker.replaceAll("_", " ").toLowerCase()}</div>)}<code>{item.snapshot_fingerprint}</code></article>)}</div> : null}
|
||||
{tab === "cutover" ? <div className="migration-list">{cutovers.map((item) => <article className="panel migration-card" key={item.id}><div className="migration-title"><div><span className="eyebrow">ATOMIC CUTOVER JOURNAL</span><h3>{short(item.id)}</h3></div><span className={`status-chip ${item.stage.toLowerCase()}`}>{item.stage}</span></div><Fact label="Exact source" value={item.source_before} /><Fact label="Exact target" value={item.target_after} /><Fact label="Switch duration" value={item.switch_duration_ms == null ? "not reported" : `${item.switch_duration_ms.toFixed(1)} ms`} /><Fact label="Rollback duration" value={item.rollback_duration_ms == null ? "not exercised" : `${item.rollback_duration_ms.toFixed(1)} ms`} />{item.failure_code ? <div className="migration-blocker"><XCircle size={14} />{item.failure_code}</div> : null}</article>)}</div> : null}
|
||||
{tab === "history" ? <div className="migration-timeline">{events.map((item) => <article key={item.id}><i /><div><span>{new Date(item.occurred_at).toLocaleString()} · {item.actor}</span><strong>{item.event_type.replaceAll("_", " ")}</strong><p>{item.reason}</p><code>{item.before_state ?? "new"} → {item.after_state ?? item.before_state}</code></div></article>)}</div> : null}
|
||||
</TabPanel>
|
||||
</div>;
|
||||
}
|
||||
|
||||
function PlanCard({ plan, validation, mutate, action }: { plan: MigrationPlan; validation?: MigrationValidationSnapshot; mutate: (action: () => Promise<unknown>, message: string) => Promise<void>; action: (plan: MigrationPlan, kind: "start" | "pause" | "cancel") => Promise<unknown> }) {
|
||||
const percent = Math.round((plan.completed_items / plan.total_expected_items) * 100);
|
||||
return <article className="panel migration-card"><div className="migration-title"><div><span className="eyebrow">{plan.environment} · {plan.migration_class}</span><h3>{plan.source_data_target} → {plan.target_shadow_target}</h3></div><span className={`status-chip ${plan.state.toLowerCase()}`}>{plan.state}</span></div><div className="migration-progress"><span style={{ width: `${percent}%` }} /></div><div className="migration-eligibility"><Fact label="Progress" value={`${plan.completed_items}/${plan.total_expected_items} (${percent}%)`} /><Fact label="Checkpoint" value={plan.last_cursor ?? "not started"} /><Fact label="Technical cutover" value={validation?.technical_cutover_eligible ? "eligible" : "not proven"} /><Fact label="Project promotion" value={validation?.project_promotion_eligible ? "eligible" : "not eligible"} /></div><div className="migration-identities"><code>adapter {plan.adapter.key}@{plan.adapter.version}</code><code>plan {plan.plan_fingerprint}</code><code>approval {plan.approval_fingerprint}</code></div><div className="migration-actions">{["READY", "BACKFILL_PAUSED"].includes(plan.state) ? <button onClick={() => mutate(() => action(plan, "start"), "Background backfill explicitly started or resumed.")}><Play size={14} />Start/resume</button> : null}{plan.state === "BACKFILLING" ? <button onClick={() => mutate(() => action(plan, "pause"), "Backfill pause durably recorded.")}><Pause size={14} />Pause</button> : null}{!terminal.has(plan.state) ? <button onClick={() => mutate(() => action(plan, "cancel"), "Migration cancelled without deleting retained evidence.")}><XCircle size={14} />Cancel</button> : null}</div></article>;
|
||||
}
|
||||
|
||||
function Fact({ label, value }: { label: string; value: string }) { return <div className="migration-fact"><span>{label}</span><strong>{value}</strong></div>; }
|
||||
@@ -0,0 +1,244 @@
|
||||
import { FormEvent, useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { Archive, Boxes, ChevronLeft, ChevronRight, FileCheck2, GitCommit, Plus, RefreshCw, Trash2 } from "lucide-react";
|
||||
|
||||
import { ApiError, api } from "../lib/api";
|
||||
import { SafetyDialog } from "./SafetyDialog";
|
||||
import { TabList, TabPanel } from "./TabList";
|
||||
import type { TabDescriptor } from "./TabList";
|
||||
import type { DerivedArtifact, ModelArtifact, ModelInstallationRationale, ModelRevision, ModelSummary, Page, UpstreamSnapshot } from "../types";
|
||||
|
||||
type Tab = "overview" | "upstream" | "revisions" | "artifacts" | "provenance";
|
||||
const registryTabs: TabDescriptor<Tab>[] = [
|
||||
{ key: "overview", label: "overview" }, { key: "upstream", label: "upstream" },
|
||||
{ key: "revisions", label: "revisions" }, { key: "artifacts", label: "artifacts" },
|
||||
{ key: "provenance", label: "provenance" },
|
||||
];
|
||||
type Dependency = { resource_type: string; resource_id: string; relation: string };
|
||||
|
||||
function formatBytes(value: number): string {
|
||||
if (value < 1024) return `${value} B`;
|
||||
if (value < 1024 ** 2) return `${(value / 1024).toFixed(1)} KiB`;
|
||||
if (value < 1024 ** 3) return `${(value / 1024 ** 2).toFixed(1)} MiB`;
|
||||
return `${(value / 1024 ** 3).toFixed(1)} GiB`;
|
||||
}
|
||||
|
||||
function shortHash(hash: string): string { return `${hash.slice(0, 12)}…`; }
|
||||
function json(value: unknown): string { return JSON.stringify(value, null, 2); }
|
||||
|
||||
function Status({ value }: { value: string }) {
|
||||
return <span className={`registry-status ${value}`}>{value.replaceAll("_", " ")}</span>;
|
||||
}
|
||||
|
||||
function ConflictPanel({ dependencies, onClose }: { dependencies: Dependency[]; onClose: () => void }) {
|
||||
return <div className="dependency-conflict" role="alert"><div><strong>Deletion blocked</strong><p>This record still anchors provenance or operational dependencies.</p></div><ul>{dependencies.map((item) => <li key={`${item.resource_type}-${item.resource_id}`}><code>{item.resource_type}</code><span>{item.relation}</span><small>{item.resource_id}</small></li>)}</ul><button onClick={onClose}>Dismiss</button></div>;
|
||||
}
|
||||
|
||||
export function ModelsRegistry() {
|
||||
const [page, setPage] = useState(1);
|
||||
const [searchDraft, setSearchDraft] = useState("");
|
||||
const [search, setSearch] = useState("");
|
||||
const [lifecycle, setLifecycle] = useState("");
|
||||
const [sourceType, setSourceType] = useState("");
|
||||
const [models, setModels] = useState<Page<ModelSummary> | null>(null);
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
const [detail, setDetail] = useState<ModelSummary | null>(null);
|
||||
const [revisions, setRevisions] = useState<ModelRevision[]>([]);
|
||||
const [artifacts, setArtifacts] = useState<ModelArtifact[]>([]);
|
||||
const [derived, setDerived] = useState<DerivedArtifact[]>([]);
|
||||
const [rationale, setRationale] = useState<ModelInstallationRationale | null>(null);
|
||||
const [tab, setTab] = useState<Tab>("overview");
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [detailLoading, setDetailLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [dependencies, setDependencies] = useState<Dependency[]>([]);
|
||||
const [showCreate, setShowCreate] = useState(false);
|
||||
const [pendingDelete, setPendingDelete] = useState<{ kind: "model" | "revision" | "artifact"; id: string; label: string } | null>(null);
|
||||
const [deleteConfirmed, setDeleteConfirmed] = useState(false);
|
||||
const [deleteError, setDeleteError] = useState<string | null>(null);
|
||||
const [deletePending, setDeletePending] = useState(false);
|
||||
|
||||
const loadModels = useCallback(async () => {
|
||||
setLoading(true); setError(null);
|
||||
const params = new URLSearchParams({ page: String(page), page_size: "8" });
|
||||
if (search) params.set("search", search);
|
||||
if (lifecycle) params.set("lifecycle", lifecycle);
|
||||
if (sourceType) params.set("source_type", sourceType);
|
||||
try { setModels(await api.models(params.toString())); }
|
||||
catch (cause) { setError(cause instanceof Error ? cause.message : "Registry list failed"); }
|
||||
finally { setLoading(false); }
|
||||
}, [lifecycle, page, search, sourceType]);
|
||||
|
||||
const loadDetail = useCallback(async (id: string) => {
|
||||
setDetailLoading(true); setError(null);
|
||||
try {
|
||||
const [model, revisionPage, rationales] = await Promise.all([api.model(id), api.revisions(id), api.installationRationale()]);
|
||||
const artifactPages = await Promise.all(revisionPage.items.map((item) => api.artifacts(item.id)));
|
||||
const derivedPages = await Promise.all(revisionPage.items.map((item) => api.derivedArtifacts(item.id)));
|
||||
setDetail(model); setRevisions(revisionPage.items);
|
||||
setArtifacts(artifactPages.flatMap((item) => item.items));
|
||||
setDerived(derivedPages.flatMap((item) => item.items));
|
||||
setRationale(rationales.find((item) => item.model_id === id) ?? null);
|
||||
} catch (cause) { setError(cause instanceof Error ? cause.message : "Registry detail failed"); }
|
||||
finally { setDetailLoading(false); }
|
||||
}, []);
|
||||
|
||||
useEffect(() => { void loadModels(); }, [loadModels]);
|
||||
useEffect(() => { if (selectedId) void loadDetail(selectedId); else { setDetail(null); setRevisions([]); setArtifacts([]); setDerived([]); setRationale(null); } }, [loadDetail, selectedId]);
|
||||
const revisionById = useMemo(() => new Map(revisions.map((item) => [item.id, item])), [revisions]);
|
||||
|
||||
function exposeError(cause: unknown) {
|
||||
if (cause instanceof ApiError && cause.status === 409) {
|
||||
const values = cause.details.dependencies;
|
||||
setDependencies(Array.isArray(values) ? values as Dependency[] : []);
|
||||
} else setError(cause instanceof Error ? cause.message : "Registry operation failed");
|
||||
}
|
||||
|
||||
async function refreshAll() { await loadModels(); if (selectedId) await loadDetail(selectedId); }
|
||||
async function remove(kind: "model" | "revision" | "artifact", id: string) {
|
||||
// The dialog disables its own confirm while a request is in flight; this guard is the second
|
||||
// line, so a replayed handler can never issue a duplicate destructive call.
|
||||
if (deletePending) return;
|
||||
setError(null); setDependencies([]); setDeletePending(true);
|
||||
try {
|
||||
if (kind === "model") await api.deleteModel(id);
|
||||
else if (kind === "revision") await api.deleteRevision(id);
|
||||
else await api.deleteArtifact(id);
|
||||
if (kind === "model") setSelectedId(null);
|
||||
setPendingDelete(null); setDeleteConfirmed(false);
|
||||
await refreshAll();
|
||||
} catch (cause) {
|
||||
if (cause instanceof ApiError && cause.status === 409) {
|
||||
const values = cause.details.dependencies;
|
||||
setDependencies(Array.isArray(values) ? values as Dependency[] : []);
|
||||
}
|
||||
setDeleteError(cause instanceof Error ? cause.message : "Guarded deletion failed");
|
||||
} finally { setDeletePending(false); }
|
||||
}
|
||||
|
||||
function previewDelete(kind: "model" | "revision" | "artifact", id: string, label: string) {
|
||||
setDeleteConfirmed(false);
|
||||
setDeleteError(null);
|
||||
setDependencies([]);
|
||||
setPendingDelete({ kind, id, label });
|
||||
}
|
||||
return <><div className="registry-layout">
|
||||
<section className="registry-list panel">
|
||||
<div className="panel-header"><div><span className="eyebrow">POSTGRESQL SOURCE OF TRUTH</span><h2>Model registry</h2></div><button className="icon-action" onClick={() => setShowCreate((value) => !value)} aria-label="Create model"><Plus size={17} /></button></div>
|
||||
<form className="registry-filters" onSubmit={(event) => { event.preventDefault(); setPage(1); setSearch(searchDraft); }}>
|
||||
<input aria-label="Search models" value={searchDraft} onChange={(event) => setSearchDraft(event.target.value)} placeholder="Search name, key or source" />
|
||||
<select aria-label="Lifecycle filter" value={lifecycle} onChange={(event) => { setPage(1); setLifecycle(event.target.value); }}><option value="">All lifecycles</option><option value="candidate">Candidate</option><option value="deprecated">Deprecated</option><option value="archived">Archived</option></select>
|
||||
<select aria-label="Source type filter" value={sourceType} onChange={(event) => { setPage(1); setSourceType(event.target.value); }}><option value="">All sources</option><option value="huggingface">Hugging Face</option><option value="local">Local</option><option value="custom">Custom</option></select>
|
||||
<button type="submit">Search</button>
|
||||
</form>
|
||||
{showCreate ? <CreateModelForm onCreated={(model) => { setShowCreate(false); setSelectedId(model.id); void loadModels(); }} onError={exposeError} /> : null}
|
||||
{loading ? <div className="registry-loading"><RefreshCw className="spin" size={15} />Loading registry…</div> : null}
|
||||
{!loading && !models?.items.length ? <div className="registry-empty"><Boxes size={18} />No models match these filters.</div> : null}
|
||||
<div className="registry-models">{models?.items.map((model) => <button key={model.id} className={selectedId === model.id ? "selected" : ""} onClick={() => setSelectedId(model.id)}><div><strong>{model.display_name}</strong><span>{model.upstream_source}</span></div><div><Status value={model.lifecycle} /><small>{model.revision_count} rev · {model.artifact_count} files</small></div></button>)}</div>
|
||||
<div className="registry-pagination"><button disabled={page <= 1} onClick={() => setPage((value) => value - 1)} aria-label="Previous page"><ChevronLeft size={15} /></button><span>Page {models?.page ?? page} of {Math.max(models?.pages ?? 1, 1)} · {models?.total ?? 0} models</span><button disabled={!models || page >= models.pages} onClick={() => setPage((value) => value + 1)} aria-label="Next page"><ChevronRight size={15} /></button></div>
|
||||
</section>
|
||||
<section className="registry-detail">
|
||||
{error ? <div className="error-banner"><strong>Registry operation failed</strong><span>{error}</span></div> : null}
|
||||
{dependencies.length && !pendingDelete ? <ConflictPanel dependencies={dependencies} onClose={() => setDependencies([])} /> : null}
|
||||
{!selectedId ? <div className="registry-placeholder panel"><Boxes size={28} /><h2>Select a model</h2><p>Inspect separated upstream facts, local governance metadata, exact revisions, artifact states and lineage.</p></div> : null}
|
||||
{selectedId && detailLoading ? <div className="registry-placeholder panel"><RefreshCw className="spin" size={20} />Loading complete provenance…</div> : null}
|
||||
{detail && !detailLoading ? <>
|
||||
<article className="panel registry-hero"><div><span className="eyebrow">{detail.key} · {detail.source_type}</span><h2>{detail.display_name}</h2><p>{detail.description ?? "No local description."}</p></div><div className="registry-actions"><Status value={detail.verification_status} />{detail.lifecycle === "deprecated" ? <button onClick={async () => { try { await api.archiveModel(detail.id); await refreshAll(); } catch (cause) { exposeError(cause); } }}><Archive size={14} />Archive</button> : <button onClick={async () => { try { await api.deprecateModel(detail.id); await refreshAll(); } catch (cause) { exposeError(cause); } }}><Archive size={14} />Deprecate</button>}<button className="danger" onClick={() => previewDelete("model", detail.id, detail.display_name)}><Trash2 size={14} />Delete</button></div></article>
|
||||
<TabList idPrefix="registry" label="Model detail sections" className="registry-tabs" tabs={registryTabs} active={tab} onSelect={setTab} />
|
||||
<TabPanel idPrefix="registry" active={tab} className="registry-tabpanel">
|
||||
{tab === "overview" ? <><InstallationRationale rationale={rationale} /><OverviewTab model={detail} onSaved={refreshAll} onError={exposeError} /></> : null}
|
||||
{tab === "upstream" ? <UpstreamTab model={detail} onRefresh={refreshAll} /> : null}
|
||||
{tab === "revisions" ? <RevisionsTab model={detail} revisions={revisions} onRefresh={refreshAll} onDelete={(id) => previewDelete("revision", id, revisions.find((item) => item.id === id)?.upstream_revision ?? id)} onError={exposeError} /> : null}
|
||||
{tab === "artifacts" ? <ArtifactsTab model={detail} revisions={revisions} artifacts={artifacts} onRefresh={refreshAll} onDelete={(id) => previewDelete("artifact", id, artifacts.find((item) => item.id === id)?.filename ?? id)} onError={exposeError} /> : null}
|
||||
{tab === "provenance" ? <ProvenanceTab revisions={revisionById} artifacts={artifacts} derived={derived} /> : null}
|
||||
</TabPanel>
|
||||
</> : null}
|
||||
</section>
|
||||
</div>{pendingDelete ? <SafetyDialog
|
||||
idPrefix="delete"
|
||||
eyebrow="DESTRUCTIVE ACTION PREVIEW"
|
||||
title={`Delete ${pendingDelete.kind}?`}
|
||||
subject={pendingDelete.label}
|
||||
description="ModelForge will run dependency checks before deletion and fail closed when this record anchors revisions, artifacts, deployments, evaluations or project usage."
|
||||
facts={pendingDelete.kind === "model" && rationale ? [
|
||||
{ label: "Installed data", value: rationale.installed ? formatBytes(rationale.installed_bytes) : "metadata only" },
|
||||
{ label: "Known dependencies", value: String(rationale.dependencies.length) },
|
||||
{ label: "Preflight verdict", value: rationale.can_delete ? "eligible for backend validation" : "expected to be blocked" },
|
||||
] : undefined}
|
||||
acknowledgement="I understand this is a permanent registry action and want ModelForge to run the guarded deletion."
|
||||
confirmLabel={`Delete ${pendingDelete.kind}`}
|
||||
confirmIcon={<Trash2 size={14} />}
|
||||
acknowledged={deleteConfirmed}
|
||||
onAcknowledgedChange={setDeleteConfirmed}
|
||||
pending={deletePending}
|
||||
errorTitle="Deletion blocked"
|
||||
error={deleteError}
|
||||
errorExtra={dependencies.length ? <><small>{dependencies.length} active dependenc{dependencies.length === 1 ? "y" : "ies"} must be resolved first.</small><ul>{dependencies.map((item) => <li key={`${item.resource_type}-${item.resource_id}`}><code>{item.resource_type}</code> · {item.relation}</li>)}</ul></> : null}
|
||||
closeLabel="Close deletion preview"
|
||||
onCancel={() => setPendingDelete(null)}
|
||||
onConfirm={() => void remove(pendingDelete.kind, pendingDelete.id)}
|
||||
/> : null}</>;
|
||||
}
|
||||
|
||||
function InstallationRationale({ rationale }: { rationale: ModelInstallationRationale | null }) {
|
||||
if (!rationale) return <article className="panel metadata-section"><span className="eyebrow">WHY IS THIS INSTALLED?</span><p>No installed artifact or operational dependency is recorded.</p></article>;
|
||||
return <article className="panel metadata-section"><span className="eyebrow">WHY IS THIS INSTALLED?</span><h3>{rationale.installed ? `${formatBytes(rationale.installed_bytes)} on managed storage` : "Registry metadata only"}</h3><dl><div><dt>Safe to delete</dt><dd>{rationale.can_delete ? "yes" : "no"}</dd></div><div><dt>Dependencies</dt><dd>{rationale.dependencies.length}</dd></div></dl>{rationale.deletion_blockers.length ? <ul>{rationale.deletion_blockers.map((blocker) => <li key={blocker}>{blocker}</li>)}</ul> : <p>No deployment, project, evaluation, or provenance blocker is active.</p>}{rationale.dependencies.map((dependency) => <div key={dependency.deployment_id} className="registry-record"><div><strong>{dependency.capability}@{dependency.version} · {dependency.channel}</strong><small>{dependency.project_consumers.length ? `Consumers: ${dependency.project_consumers.join(", ")}` : "No declared consumers"} · {dependency.last_used_at ? `last used ${new Date(dependency.last_used_at).toLocaleString()}` : "never invoked"}</small><small>{dependency.evaluation_run_ids.length} evaluation evidence record(s)</small></div></div>)}</article>;
|
||||
}
|
||||
|
||||
function UpstreamTab({ model, onRefresh }: { model: ModelSummary; onRefresh: () => Promise<void> }) {
|
||||
const [snapshot, setSnapshot] = useState<UpstreamSnapshot | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try { setSnapshot(await api.upstream(model.id)); setError(null); }
|
||||
catch { setSnapshot(null); }
|
||||
finally { setLoading(false); }
|
||||
}, [model.id]);
|
||||
|
||||
useEffect(() => { void load(); }, [load]);
|
||||
|
||||
async function refresh() {
|
||||
setLoading(true); setError(null);
|
||||
try { setSnapshot(await api.refreshUpstream(model.id)); await onRefresh(); }
|
||||
catch (cause) { setError(cause instanceof Error ? cause.message : "Upstream refresh failed"); }
|
||||
finally { setLoading(false); }
|
||||
}
|
||||
|
||||
return <div className="registry-stack">
|
||||
<article className="panel provenance-note"><strong>Upstream evidence boundary</strong><p>Facts below are captured through the official Hugging Face API. Local interpretation, integrity verification, security approval and deployment remain separate states.</p><button onClick={() => void refresh()} disabled={loading}><RefreshCw size={14} className={loading ? "spin" : ""} />{snapshot ? "Refresh snapshot" : "Resolve exact revision"}</button></article>
|
||||
{error ? <div className="error-banner"><strong>Upstream refresh blocked</strong><span>{error}</span></div> : null}
|
||||
{loading ? <div className="registry-loading panel"><RefreshCw className="spin" size={15} />Loading upstream evidence…</div> : null}
|
||||
{!loading && !snapshot ? <div className="registry-empty panel">No upstream snapshot exists. Refresh to resolve an immutable commit and complete file inventory.</div> : null}
|
||||
{snapshot ? <><article className="panel metadata-section"><span className="eyebrow">EXACT UPSTREAM SNAPSHOT</span><h3>{snapshot.repository_id}</h3><dl><div><dt>Access</dt><dd>{snapshot.access_state}</dd></div><div><dt>Freshness</dt><dd>{snapshot.stale ? "stale" : "fresh"}</dd></div><div><dt>Files</dt><dd>{snapshot.files.length}</dd></div></dl><code>{snapshot.resolved_commit_sha}</code><pre>{json({ upstream_facts: snapshot.metadata_snapshot, model_card: snapshot.card_metadata, upstream_scanner_evidence: snapshot.security_metadata })}</pre></article><div className="panel upstream-files">{snapshot.files.map((file) => <div key={file.id}><div><strong>{file.path}</strong><small>{file.role} · {file.file_format} · {file.size_bytes == null ? "unknown size" : formatBytes(file.size_bytes)}</small></div><div>{file.risk_flags.map((risk) => <Status key={risk} value={risk} />)}{file.upstream_sha256 ? <code>{shortHash(file.upstream_sha256)}</code> : <small>no upstream SHA-256</small>}</div></div>)}</div></> : null}
|
||||
</div>;
|
||||
}
|
||||
|
||||
function CreateModelForm({ onCreated, onError }: { onCreated: (model: ModelSummary) => void; onError: (cause: unknown) => void }) {
|
||||
const [key, setKey] = useState(""); const [name, setName] = useState(""); const [source, setSource] = useState("");
|
||||
async function submit(event: FormEvent) { event.preventDefault(); try { const provider = source.includes("/") ? source.split("/", 1)[0] : "local"; onCreated(await api.createModel({ key, display_name: name, source_type: source.includes("/") ? "huggingface" : "local", upstream_provider: provider, upstream_source: source, upstream_metadata: { verification: "unverified" }, local_metadata: {}, interpretation_metadata: {}, modalities: [], parameter_metadata: {}, license_metadata: { status: "unknown", spdx_id: null }, lifecycle: "candidate" })); } catch (cause) { onError(cause); } }
|
||||
return <form className="registry-inline-form" onSubmit={submit}><strong>Create registry model</strong><label>Key<input required pattern="[a-z0-9][a-z0-9._-]*" value={key} onChange={(event) => setKey(event.target.value)} /></label><label>Display name<input required value={name} onChange={(event) => setName(event.target.value)} /></label><label>Upstream source<input required value={source} onChange={(event) => setSource(event.target.value)} placeholder="namespace/repository" /></label><button type="submit">Create unverified candidate</button></form>;
|
||||
}
|
||||
|
||||
function OverviewTab({ model, onSaved, onError }: { model: ModelSummary; onSaved: () => Promise<void>; onError: (cause: unknown) => void }) {
|
||||
const [name, setName] = useState(model.display_name); const [description, setDescription] = useState(model.description ?? ""); const [local, setLocal] = useState(json(model.local_metadata));
|
||||
async function save(event: FormEvent) { event.preventDefault(); try { await api.updateModel(model.id, { display_name: name, description: description || null, local_metadata: JSON.parse(local) }); await onSaved(); } catch (cause) { onError(cause); } }
|
||||
return <div className="registry-sections"><article className="panel metadata-section"><span className="eyebrow">UPSTREAM FACTS</span><h3>{model.upstream_source}</h3><dl><div><dt>Provider</dt><dd>{model.upstream_provider}</dd></div><div><dt>Source type</dt><dd>{model.source_type}</dd></div><div><dt>License</dt><dd>{String(model.license_metadata.status ?? "unknown")}</dd></div></dl><pre>{json(model.upstream_metadata)}</pre></article><article className="panel metadata-section"><span className="eyebrow">LOCAL GOVERNANCE</span><form onSubmit={save}><label>Display name<input value={name} onChange={(event) => setName(event.target.value)} /></label><label>Description<textarea value={description} onChange={(event) => setDescription(event.target.value)} /></label><label>Local metadata JSON<textarea value={local} onChange={(event) => setLocal(event.target.value)} /></label><button type="submit">Save local metadata</button></form></article><article className="panel metadata-section"><span className="eyebrow">LOCAL INTERPRETATION · NOT UPSTREAM FACT</span><pre>{json(model.interpretation_metadata)}</pre></article></div>;
|
||||
}
|
||||
|
||||
function RevisionsTab({ model, revisions, onRefresh, onDelete, onError }: { model: ModelSummary; revisions: ModelRevision[]; onRefresh: () => Promise<void>; onDelete: (id: string) => void; onError: (cause: unknown) => void }) {
|
||||
const [revision, setRevision] = useState("main"); const [sha, setSha] = useState("");
|
||||
async function submit(event: FormEvent) { event.preventDefault(); try { await api.createRevision(model.id, { upstream_revision: revision, resolved_commit_sha: sha, metadata_snapshot: { registered_by: "local-operator" } }); setSha(""); await onRefresh(); } catch (cause) { onError(cause); } }
|
||||
return <div className="registry-stack"><form className="panel registry-create-row" onSubmit={submit}><div><span className="eyebrow">IMMUTABLE REVISION</span><strong>Register exact commit</strong></div><label>Branch or tag<input value={revision} onChange={(event) => setRevision(event.target.value)} /></label><label>Resolved SHA<input required minLength={40} maxLength={64} pattern="[0-9a-f]{40,64}" value={sha} onChange={(event) => setSha(event.target.value)} placeholder="40–64 lowercase hex" /></label><button type="submit"><Plus size={14} />Register</button></form>{revisions.length ? revisions.map((item) => <article className="panel registry-record" key={item.id}><GitCommit size={18} /><div><strong>{item.upstream_revision}</strong><code title={item.resolved_commit_sha}>{item.resolved_commit_sha}</code><small>Immutable since {new Date(item.immutable_at).toLocaleString()}</small></div><button className="danger" onClick={() => onDelete(item.id)} aria-label={`Delete revision ${item.upstream_revision}`}><Trash2 size={14} /></button></article>) : <div className="registry-empty panel">No exact revisions registered.</div>}</div>;
|
||||
}
|
||||
|
||||
function ArtifactsTab({ model, revisions, artifacts, onRefresh, onDelete, onError }: { model: ModelSummary; revisions: ModelRevision[]; artifacts: ModelArtifact[]; onRefresh: () => Promise<void>; onDelete: (id: string) => void; onError: (cause: unknown) => void }) {
|
||||
const [revisionId, setRevisionId] = useState(revisions[0]?.id ?? ""); const [filename, setFilename] = useState(""); const [digest, setDigest] = useState(""); const [size, setSize] = useState("0");
|
||||
const revisionMap = new Map(revisions.map((item) => [item.id, item]));
|
||||
async function submit(event: FormEvent) { event.preventDefault(); try { await api.createArtifact(revisionId, { filename, artifact_type: "weights", serialization_format: "safetensors", sha256: digest, size_bytes: Number(size), status: "remote", security_status: "unverified", license_status: "unknown", locations: [] }); setFilename(""); setDigest(""); await onRefresh(); } catch (cause) { onError(cause); } }
|
||||
return <div className="registry-stack">{revisions.length ? <form className="panel registry-create-row artifact-form" onSubmit={submit}><div><span className="eyebrow">CONTENT IDENTITY</span><strong>Register artifact metadata</strong></div><label>Revision<select value={revisionId} onChange={(event) => setRevisionId(event.target.value)}>{revisions.map((item) => <option key={item.id} value={item.id}>{item.upstream_revision} · {shortHash(item.resolved_commit_sha)}</option>)}</select></label><label>Filename<input required value={filename} onChange={(event) => setFilename(event.target.value)} /></label><label>SHA-256<input required pattern="[0-9a-f]{64}" value={digest} onChange={(event) => setDigest(event.target.value)} /></label><label>Bytes<input required min="0" type="number" value={size} onChange={(event) => setSize(event.target.value)} /></label><button type="submit"><Plus size={14} />Register</button></form> : null}{artifacts.length ? artifacts.map((item) => { const revision = revisionMap.get(item.revision_id); return <article className="panel registry-record artifact-record" key={item.id}><FileCheck2 size={18} /><div><strong>{item.filename}</strong><code title={item.sha256}>{shortHash(item.sha256)}</code><small>{item.serialization_format} · {formatBytes(item.size_bytes)} · {item.locations.length} location(s)</small><details><summary>Artifact acquisition details</summary><dl><div><dt>Upstream</dt><dd>{model.upstream_source}</dd></div><div><dt>Exact revision</dt><dd><code>{revision?.resolved_commit_sha ?? "unknown"}</code></dd></div><div><dt>SHA-256</dt><dd><code>{item.sha256}</code></dd></div><div><dt>Security</dt><dd>{item.security_status}</dd></div><div><dt>License</dt><dd>{item.license_status}</dd></div><div><dt>Verification</dt><dd>{item.verified_at ? new Date(item.verified_at).toLocaleString() : "not verified"}</dd></div><div><dt>Download job</dt><dd><code>{String(item.verification_details.job_id ?? "not reported")}</code></dd></div></dl>{item.locations.map((location) => <div className="location-row" key={location.id}><code>{location.relative_path}</code><span>root {location.storage_root_id}</span><Status value={location.status} /></div>)}</details></div><Status value={item.quarantined ? "quarantined" : item.status} /><button className="danger" onClick={() => onDelete(item.id)} aria-label={`Delete artifact ${item.filename}`}><Trash2 size={14} /></button></article>; }) : <div className="registry-empty panel">No artifacts registered. Remote metadata can be registered without downloading files.</div>}</div>;
|
||||
}
|
||||
|
||||
function ProvenanceTab({ revisions, artifacts, derived }: { revisions: Map<string, ModelRevision>; artifacts: ModelArtifact[]; derived: DerivedArtifact[] }) {
|
||||
return <div className="registry-stack"><article className="panel provenance-note"><strong>Static inspection boundary</strong><p>Verification streams bytes into SHA-256 and compares size. Model payloads and executable repository code are never loaded here.</p></article>{artifacts.map((item) => <article className="panel provenance-card" key={item.id}><div><span className="eyebrow">SOURCE ARTIFACT · {revisions.get(item.revision_id)?.upstream_revision ?? "unknown revision"}</span><h3>{item.filename}</h3><code>{item.sha256}</code></div><div><Status value={item.status} /><span>Security: {item.security_status}</span><span>License: {item.license_status}</span></div>{item.locations.map((location) => <div className="location-row" key={location.id}><code>{location.relative_path}</code><span>root {location.storage_root_id}</span><Status value={location.status} /></div>)}</article>)}{derived.map((item) => <article className="panel provenance-card derived" key={item.id}><div><span className="eyebrow">DERIVED · {item.transformation_type}</span><h3>{item.filename}</h3><code>{item.sha256}</code></div><dl><div><dt>Tool</dt><dd>{item.tool}@{item.tool_version}</dd></div><div><dt>Sources</dt><dd>{item.sources.length}</dd></div></dl><div className="lineage-list">{item.sources.map((source) => <div key={source.artifact_id}><span>#{source.ordinal + 1}</span><code>{source.sha256}</code></div>)}</div><details><summary>Configuration and environment</summary><pre>{json({ configuration: item.configuration, environment: item.environment_snapshot })}</pre></details></article>)}</div>;
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { OperationsWorkspace } from "./OperationsWorkspace";
|
||||
|
||||
const alert = {
|
||||
id: "alert-1", rule_id: "rule-1", fingerprint: "a".repeat(64), alert_type: "NODE_OFFLINE",
|
||||
severity: "CRITICAL", state: "FIRING", source: "node_agent", subject_type: "node",
|
||||
subject_ref: "node-1", summary: "GPU Node is offline", details: {},
|
||||
first_seen_at: "2026-08-27T08:00:00Z", last_seen_at: "2026-08-27T08:01:00Z",
|
||||
firing_at: "2026-08-27T08:01:00Z", occurrence_count: 2,
|
||||
};
|
||||
const resolvedAlert = { ...alert, id: "alert-2", fingerprint: "c".repeat(64), alert_type: "STORAGE_LOW", state: "RESOLVED", summary: "Storage headroom recovered", resolved_at: "2026-08-27T08:02:00Z" };
|
||||
const evaluation = {
|
||||
id: "evaluation-1", policy_id: "policy-1", policy_key: "rag.embedding.production.success",
|
||||
policy_revision: 1, sli_key: "rag.embedding.success", environment: "PRODUCTION", objective: .99,
|
||||
observed_value: null, sample_count: 2, good_count: 2, bad_count: 0, state: "INSUFFICIENT_DATA",
|
||||
window_start: "2026-08-26T08:00:00Z", window_end: "2026-08-27T08:00:00Z",
|
||||
observed_at: "2026-08-27T08:00:00Z", allowed_bad: .02, consumed_bad: 0,
|
||||
remaining_bad: .02, evidence: { source: "gateway_requests" },
|
||||
};
|
||||
const capacity = {
|
||||
id: "capacity-1", observed_at: "2026-08-27T08:00:00Z", received_at: "2026-08-27T08:02:00Z",
|
||||
node_id: "node-1", node_name: "GPU Node", accelerator_id: "gpu-1", gpu_total_bytes: 17179869184,
|
||||
gpu_observed_bytes: 10737418240, gpu_external_bytes: 8589934592, gpu_managed_resident_bytes: 2147483648,
|
||||
gpu_leased_bytes: 0, gpu_reserve_bytes: 1073741824, gpu_schedulable_bytes: 5368709120,
|
||||
pressure_state: "NORMAL", system_ram_total_bytes: 68719476736, system_ram_available_bytes: 34359738368,
|
||||
storage_total_bytes: 2199023255552, storage_free_bytes: 1099511627776, availability: "STALE", freshness_seconds: 120,
|
||||
};
|
||||
|
||||
function mockOperations(): void {
|
||||
vi.stubGlobal("fetch", vi.fn((input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const url = String(input); let payload: unknown = [];
|
||||
if (url.endsWith("/overview")) payload = { status: "DEGRADED", observed_at: "2026-08-27T08:02:00Z", active_alerts: [alert], slo_evaluations: [evaluation], capacity: [capacity], capability_health: [{ capability: "rag.embedding", environment: "PRODUCTION", health: "healthy", node_id: "node-1" }], project_health: [{ project: "examplerag", capability: "rag.embedding", environment: "LAB", state: "ACTIVE" }], recent_failures: [], history_available: true };
|
||||
else if (url.includes("/slo-evaluations")) payload = [evaluation];
|
||||
else if (url.includes("/alerts")) payload = init?.method === "POST" && url.includes("acknowledge") ? { ...alert, state: "ACKNOWLEDGED" } : [alert, resolvedAlert];
|
||||
else if (url.includes("/capacity")) payload = [capacity];
|
||||
else if (url.endsWith("/incidents")) payload = [{ id: "incident-1", fingerprint: "b".repeat(64), title: "Operational degradation on node node-1", state: "OPEN", severity: "CRITICAL", root_subject_type: "node", root_subject_ref: "node-1", correlation: "LIKELY_ROOT", first_seen_at: "2026-08-27T08:01:00Z", last_seen_at: "2026-08-27T08:01:00Z" }];
|
||||
return Promise.resolve(new Response(JSON.stringify(payload), { status: 200 }));
|
||||
}));
|
||||
}
|
||||
|
||||
async function load(): Promise<void> {
|
||||
fireEvent.change(screen.getByLabelText("Operator API key"), { target: { value: "operator-test" } });
|
||||
fireEvent.click(screen.getByRole("button", { name: "Load operations" }));
|
||||
await screen.findByText("DEGRADED");
|
||||
}
|
||||
|
||||
afterEach(() => { cleanup(); vi.unstubAllGlobals(); });
|
||||
|
||||
describe("M14 operations workspace", () => {
|
||||
it("separates current degradation from historical evidence confidence", async () => {
|
||||
mockOperations(); render(<OperationsWorkspace />); await load();
|
||||
expect(screen.getByText("CRITICAL ACTIVE")).toBeInTheDocument();
|
||||
expect(screen.getByText("Historical samples available")).toBeInTheDocument();
|
||||
expect(screen.getAllByText("rag.embedding")).toHaveLength(2);
|
||||
expect(screen.getByText(/"project": "examplerag"/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders insufficient SLO data and production error budget without inventing health", async () => {
|
||||
mockOperations(); render(<OperationsWorkspace />); await load();
|
||||
fireEvent.click(screen.getByRole("tab", { name: "SLOs" }));
|
||||
expect(screen.getByText("INSUFFICIENT DATA")).toBeInTheDocument();
|
||||
expect(screen.getByText("2 (0 bad)")).toBeInTheDocument();
|
||||
expect(screen.getByText("0.02 remaining")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows alert lifecycle, stale capacity decomposition and incident causality", async () => {
|
||||
mockOperations(); render(<OperationsWorkspace />); await load();
|
||||
fireEvent.click(screen.getByRole("tab", { name: "Alerts" }));
|
||||
expect(screen.getByText("GPU Node is offline")).toBeInTheDocument();
|
||||
expect(screen.getByText("Storage headroom recovered")).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Acknowledge" })).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole("tab", { name: "GPU" }));
|
||||
expect(screen.getByText("External usage")).toBeInTheDocument();
|
||||
expect(screen.getByText("Schedulable")).toBeInTheDocument();
|
||||
expect(screen.getByText("STALE")).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole("tab", { name: "Capacity" }));
|
||||
expect(screen.getByText(/INSUFFICIENT_DATA · at least three persisted samples/)).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole("tab", { name: "Incidents" }));
|
||||
expect(screen.getByText("LIKELY_ROOT")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,109 @@
|
||||
import { Fragment, useEffect, useMemo, useState } from "react";
|
||||
import { Activity, AlertTriangle, CheckCircle2, Clock3, Gauge, History, LockKeyhole, RefreshCw, ServerCog } from "lucide-react";
|
||||
|
||||
import { api, operatorCredentialReference } from "../lib/api";
|
||||
import { TabList, TabPanel } from "./TabList";
|
||||
import type { CapacitySnapshot, OperationalAlert, OperationalIncident, OperationsOverview, SLOEvaluation } from "../types";
|
||||
|
||||
type Tab = "overview" | "slos" | "alerts" | "gpu" | "capacity" | "incidents" | "history";
|
||||
const tabs: Array<{ key: Tab; label: string }> = [
|
||||
{ key: "overview", label: "Overview" }, { key: "slos", label: "SLOs" },
|
||||
{ key: "alerts", label: "Alerts" }, { key: "gpu", label: "GPU" },
|
||||
{ key: "capacity", label: "Capacity" }, { key: "incidents", label: "Incidents" },
|
||||
{ key: "history", label: "History" },
|
||||
];
|
||||
interface OperationsData { overview: OperationsOverview | null; evaluations: SLOEvaluation[]; alerts: OperationalAlert[]; capacity: CapacitySnapshot[]; incidents: OperationalIncident[] }
|
||||
const empty: OperationsData = { overview: null, evaluations: [], alerts: [], capacity: [], incidents: [] };
|
||||
|
||||
function date(value?: string | null): string { return value ? new Date(value).toLocaleString() : "—"; }
|
||||
function bytes(value?: number | null): string { return value == null ? "unknown" : `${(value / 1024 ** 3).toFixed(2)} GiB`; }
|
||||
function ratio(value?: number | null): string { return value == null ? "insufficient data" : `${(value * 100).toFixed(2)}%`; }
|
||||
function stateClass(value: string): string { return value.toLowerCase().replaceAll("_", "-"); }
|
||||
|
||||
export function OperationsWorkspace() {
|
||||
const sessionCredential = operatorCredentialReference();
|
||||
const [tab, setTab] = useState<Tab>("overview");
|
||||
const [token, setToken] = useState(sessionCredential); const [data, setData] = useState<OperationsData>(empty);
|
||||
const [loading, setLoading] = useState(false); const [error, setError] = useState<string | null>(null);
|
||||
const [notice, setNotice] = useState<string | null>(null);
|
||||
|
||||
async function refresh() {
|
||||
if (!token) return; setLoading(true); setError(null);
|
||||
try {
|
||||
const [overview, evaluations, alerts, capacity, incidents] = await Promise.all([
|
||||
api.operationsOverview(token), api.sloEvaluations(token), api.operationalAlerts(token),
|
||||
api.capacitySnapshots(token), api.operationalIncidents(token),
|
||||
]);
|
||||
setData({ overview, evaluations, alerts, capacity, incidents });
|
||||
} catch (cause) { setError(cause instanceof Error ? cause.message : "Operational state unavailable"); }
|
||||
finally { setLoading(false); }
|
||||
}
|
||||
async function run(action: () => Promise<unknown>, message: string) {
|
||||
setError(null); setNotice(null);
|
||||
try { await action(); setNotice(message); await refresh(); }
|
||||
catch (cause) { setError(cause instanceof Error ? cause.message : "Operational action failed"); }
|
||||
}
|
||||
useEffect(() => { if (sessionCredential) void refresh(); }, [sessionCredential]);
|
||||
return <div className="operations-stack">
|
||||
{!sessionCredential ? <form className="panel operations-boundary" onSubmit={(event) => { event.preventDefault(); void refresh(); }}><LockKeyhole size={25} /><div><span className="eyebrow">M14 · OPERATIONAL TRUTH</span><h2>SLOs, alerts and bounded capacity evidence</h2><p>Current state and historical confidence stay separate. The credential remains in memory and observability cannot gate serving.</p></div><label htmlFor="operations-operator-key">Operator API key</label><input id="operations-operator-key" name="operator-api-key" type="password" value={token} onChange={(event) => setToken(event.target.value)} autoComplete="off" /><button type="submit" disabled={!token || loading}><RefreshCw size={15} className={loading ? "spin" : ""} />{loading ? "Loading…" : "Load operations"}</button></form> : null}
|
||||
{error ? <div className="error-banner"><strong>Operational view unavailable</strong><span>{error}. No healthy state is inferred.</span></div> : null}
|
||||
{notice ? <div className="lifecycle-notice"><CheckCircle2 size={16} />{notice}</div> : null}
|
||||
<TabList idPrefix="operations" label="Operations views" className="lifecycle-tabs operations-tabs" tabs={tabs} active={tab} onSelect={setTab} />
|
||||
<TabPanel idPrefix="operations" active={tab}>
|
||||
{tab === "overview" ? <Overview data={data} /> : null}
|
||||
{tab === "slos" ? <SLOs evaluations={data.evaluations} token={token} run={run} /> : null}
|
||||
{tab === "alerts" ? <Alerts alerts={data.alerts} token={token} run={run} /> : null}
|
||||
{tab === "gpu" ? <GpuCapacity capacity={data.capacity} /> : null}
|
||||
{tab === "capacity" ? <Capacity capacity={data.capacity} token={token} run={run} /> : null}
|
||||
{tab === "incidents" ? <Incidents incidents={data.incidents} /> : null}
|
||||
{tab === "history" ? <HistoryView alerts={data.alerts} evaluations={data.evaluations} /> : null}
|
||||
</TabPanel>
|
||||
</div>;
|
||||
}
|
||||
|
||||
function Overview({ data }: { data: OperationsData }) {
|
||||
const current = data.overview;
|
||||
const critical = data.alerts.filter((item) => item.severity === "CRITICAL" && ["PENDING", "FIRING", "ACKNOWLEDGED"].includes(item.state)).length;
|
||||
const insufficient = data.evaluations.filter((item) => item.state === "INSUFFICIENT_DATA" || item.state === "STALE").length;
|
||||
return <><section className="operations-summary">
|
||||
<article className="panel"><Gauge /><span className="eyebrow">CURRENT HEALTH</span><h3>{current?.status ?? "UNKNOWN"}</h3><p>Observed {date(current?.observed_at)}</p></article>
|
||||
<article className="panel"><AlertTriangle /><span className="eyebrow">CRITICAL ACTIVE</span><h3>{critical}</h3><p>{data.alerts.length} total journaled alerts</p></article>
|
||||
<article className="panel"><Clock3 /><span className="eyebrow">EVIDENCE GAPS</span><h3>{insufficient}</h3><p>{current?.history_available ? "Historical samples available" : "No historical confidence yet"}</p></article>
|
||||
</section><section className="operations-grid"><HealthCards title="Capability health" values={current?.capability_health ?? []} emptyText="No capability health evidence is available." /><HealthCards title="Critical project health" values={current?.project_health ?? []} emptyText="No critical project health evidence is available." /></section></>;
|
||||
}
|
||||
|
||||
function SLOs({ evaluations, token, run }: { evaluations: SLOEvaluation[]; token: string; run: (action: () => Promise<unknown>, message: string) => Promise<void> }) {
|
||||
return <section><div className="operations-section-title"><div><span className="eyebrow">VERSIONED POLICIES</span><h2>Service-level objectives and error budgets</h2></div><button disabled={!token} onClick={() => run(() => api.evaluateSlos(token), "A new immutable SLO evaluation snapshot was recorded.")}><Activity size={15} />Evaluate now</button></div><div className="operations-grid">{evaluations.length ? evaluations.map((item) => <article className="panel operation-card" key={item.id}><div><span className="eyebrow">{item.environment} · REV {item.policy_revision}</span><span className={`status-chip ${stateClass(item.state)}`}>{item.state.replaceAll("_", " ")}</span></div><h3>{item.policy_key}</h3><p>{item.sli_key}</p><dl><dt>Observed</dt><dd>{item.threshold_ms ? `${item.observed_value?.toFixed(1) ?? "—"} ms` : ratio(item.observed_value)}</dd><dt>Population</dt><dd>{item.sample_count} ({item.bad_count} bad)</dd><dt>Error budget</dt><dd>{item.allowed_bad == null ? "LAB · no production budget" : `${item.remaining_bad?.toFixed(2)} remaining`}</dd><dt>Burn short / long</dt><dd>{item.short_burn_rate?.toFixed(2) ?? "—"} / {item.long_burn_rate?.toFixed(2) ?? "—"}</dd></dl></article>) : <Empty text="No SLO evaluations exist. Run an evaluation; low populations remain explicitly insufficient." />}</div></section>;
|
||||
}
|
||||
|
||||
function Alerts({ alerts, token, run }: { alerts: OperationalAlert[]; token: string; run: (action: () => Promise<unknown>, message: string) => Promise<void> }) {
|
||||
return <section><div className="operations-section-title"><div><span className="eyebrow">DEDUPLICATED LIFECYCLE</span><h2>Pending, firing, acknowledged, resolved and suppressed</h2></div><button disabled={!token} onClick={() => run(() => api.evaluateOperationalAlerts(token), "Alert rules evaluated against current authoritative state.")}><RefreshCw size={15} />Evaluate alerts</button></div><div className="operations-grid">{alerts.length ? alerts.map((item) => <article className="panel operation-card" key={item.id}><div><span className="eyebrow">{item.severity} · {item.source}</span><span className={`status-chip ${stateClass(item.state)}`}>{item.state}</span></div><h3>{item.alert_type.replaceAll("_", " ")}</h3><p>{item.summary}</p><dl><dt>Subject</dt><dd>{item.subject_type} · {item.subject_ref}</dd><dt>First / last</dt><dd>{date(item.first_seen_at)} / {date(item.last_seen_at)}</dd><dt>Occurrences</dt><dd>{item.occurrence_count}</dd></dl>{["PENDING", "FIRING"].includes(item.state) ? <button disabled={!token} onClick={() => run(() => api.acknowledgeOperationalAlert(item.id, "Acknowledged after operator review in Operations UI", token), "Alert acknowledgement was audited.")}>Acknowledge</button> : null}</article>) : <Empty text="No operational alerts have been journaled." />}</div></section>;
|
||||
}
|
||||
|
||||
function Capacity({ capacity, token, run }: { capacity: CapacitySnapshot[]; token: string; run: (action: () => Promise<unknown>, message: string) => Promise<void> }) {
|
||||
return <section><div className="operations-section-title"><div><span className="eyebrow">BOUNDED SNAPSHOTS</span><h2>Capacity history and confidence</h2></div><button disabled={!token} onClick={() => run(() => api.collectCapacity(token), "A capacity snapshot was collected from existing telemetry truth.")}><ServerCog size={15} />Collect snapshot</button></div><CapacityTrends capacity={capacity} /><NodeCapacity capacity={capacity} /></section>;
|
||||
}
|
||||
function NodeCapacity({ capacity }: { capacity: CapacitySnapshot[] }) { const latest = latestPerNode(capacity); return <div className="operations-grid">{latest.length ? latest.map((item) => <article className="panel operation-card" key={item.id}><div><span className="eyebrow">NODE · {item.availability}</span><span className={`status-chip ${stateClass(item.availability)}`}>{item.freshness_seconds.toFixed(1)}s old</span></div><h3>{item.node_name}</h3><dl><dt>System RAM available</dt><dd>{bytes(item.system_ram_available_bytes)}</dd><dt>Storage free</dt><dd>{bytes(item.storage_free_bytes)}</dd><dt>Observed</dt><dd>{date(item.observed_at)}</dd></dl></article>) : <Empty text="No capacity snapshots exist; current hardware state is not presented as historical confidence." />}</div>; }
|
||||
function GpuCapacity({ capacity }: { capacity: CapacitySnapshot[] }) { const rows = latestPerNode(capacity).filter((item) => item.accelerator_id); return <div className="operations-grid">{rows.length ? rows.map((item) => <article className="panel operation-card" key={item.id}><div><span className="eyebrow">GPU · {item.pressure_state}</span><span className={`status-chip ${stateClass(item.availability)}`}>{item.availability}</span></div><h3>{item.node_name}</h3><dl><dt>Observed usage</dt><dd>{bytes(item.gpu_observed_bytes)}</dd><dt>Managed resident</dt><dd>{bytes(item.gpu_managed_resident_bytes)}</dd><dt>External usage</dt><dd>{bytes(item.gpu_external_bytes)}</dd><dt>Leased / reserve</dt><dd>{bytes(item.gpu_leased_bytes)} / {bytes(item.gpu_reserve_bytes)}</dd><dt>Schedulable</dt><dd>{bytes(item.gpu_schedulable_bytes)}</dd></dl></article>) : <Empty text="No accelerator capacity evidence is available." />}</div>; }
|
||||
function latestPerNode(rows: CapacitySnapshot[]): CapacitySnapshot[] { const seen = new Set<string>(); return [...rows].sort((a, b) => b.observed_at.localeCompare(a.observed_at)).filter((item) => { const key = `${item.node_id}:${item.accelerator_id ?? "host"}`; if (seen.has(key)) return false; seen.add(key); return true; }); }
|
||||
function CapacityTrends({ capacity }: { capacity: CapacitySnapshot[] }) {
|
||||
const groups = useMemo(() => { const result = new Map<string, CapacitySnapshot[]>(); for (const item of capacity) { const values = result.get(item.node_id) ?? []; values.push(item); result.set(item.node_id, values); } return [...result.values()].map((values) => values.sort((a, b) => a.observed_at.localeCompare(b.observed_at))); }, [capacity]);
|
||||
if (!groups.length || groups.every((items) => items.length < 3)) return <div className="state-card"><Clock3 size={18} />INSUFFICIENT_DATA · at least three persisted samples are required for a trend chart.</div>;
|
||||
return <div className="operations-grid capacity-trends">{groups.map((items) => items.length < 3 ? <article className="panel" key={items[0].node_id}><span className="eyebrow">{items[0].node_name}</span><p>INSUFFICIENT_DATA · {items.length} persisted sample{items.length === 1 ? "" : "s"}.</p></article> : <TrendChart key={items[0].node_id} items={items} />)}</div>;
|
||||
}
|
||||
function TrendChart({ items }: { items: CapacitySnapshot[] }) {
|
||||
const values = items.map((item) => item.gpu_schedulable_bytes ?? 0); const max = Math.max(...values, 1); const width = 320; const height = 90;
|
||||
const points = values.map((value, index) => `${(index / Math.max(1, values.length - 1)) * width},${height - (value / max) * height}`).join(" ");
|
||||
return <article className="panel operation-card"><div><span className="eyebrow">PERSISTED GPU HEADROOM</span><span className="status-chip">{items.length} samples</span></div><h3>{items[0].node_name}</h3><svg className="capacity-chart" viewBox={`0 0 ${width} ${height}`} role="img" aria-label={`${items[0].node_name} schedulable GPU capacity trend`}><polyline points={points} /></svg><small>{date(items[0].observed_at)} → {date(items.at(-1)?.observed_at)}</small></article>;
|
||||
}
|
||||
|
||||
function Incidents({ incidents }: { incidents: OperationalIncident[] }) { return <div className="operations-grid">{incidents.length ? incidents.map((item) => <article className="panel operation-card" key={item.id}><div><span className="eyebrow">{item.correlation}</span><span className={`status-chip ${stateClass(item.state)}`}>{item.state}</span></div><h3>{item.title}</h3><p>{item.root_subject_type} · {item.root_subject_ref}</p><small>{date(item.first_seen_at)} → {date(item.resolved_at)}</small></article>) : <Empty text="No correlated incidents exist." />}</div>; }
|
||||
function HistoryView({ alerts, evaluations }: { alerts: OperationalAlert[]; evaluations: SLOEvaluation[] }) { const entries = [...alerts.map((item) => ({ at: item.last_seen_at, title: `${item.alert_type} · ${item.state}`, detail: item.summary })), ...evaluations.map((item) => ({ at: item.observed_at, title: `${item.policy_key} · ${item.state}`, detail: `${item.sample_count} valid samples` }))].sort((a, b) => b.at.localeCompare(a.at)); return <div className="operations-timeline">{entries.length ? entries.map((item, index) => <article className="panel" key={`${item.at}-${index}`}><History size={16} /><div><strong>{item.title}</strong><p>{item.detail}</p><small>{date(item.at)}</small></div></article>) : <Empty text="No operational history exists yet." />}</div>; }
|
||||
function HealthCards({ title, values, emptyText }: { title: string; values: Record<string, unknown>[]; emptyText: string }) {
|
||||
return <section><span className="eyebrow">{title}</span>{values.length ? values.map((item, index) => {
|
||||
const subject = String(item.capability ?? item.project ?? item.key ?? "Operational subject");
|
||||
const facts = Object.entries(item).filter(([key]) => !["capability", "project", "key"].includes(key)).slice(0, 6);
|
||||
return <article className="panel operation-card" key={`${subject}-${index}`}><h3>{subject}</h3><dl>{facts.map(([key, value]) => <Fragment key={key}><dt>{key.replaceAll("_", " ")}</dt><dd>{value && typeof value === "object" ? JSON.stringify(value) : String(value ?? "unknown")}</dd></Fragment>)}</dl><details><summary>Raw evidence</summary><pre>{JSON.stringify(item, null, 2)}</pre></details></article>;
|
||||
}) : <Empty text={emptyText} />}</section>;
|
||||
}
|
||||
function Empty({ text }: { text: string }) { return <div className="state-card"><Clock3 size={18} />{text}</div>; }
|
||||
@@ -0,0 +1,245 @@
|
||||
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { RecoveryWorkspace } from "./RecoveryWorkspace";
|
||||
|
||||
interface Options {
|
||||
verified?: boolean;
|
||||
staleBackup?: boolean;
|
||||
failedBackup?: boolean;
|
||||
unprotected?: boolean;
|
||||
sufficientCapacity?: boolean;
|
||||
}
|
||||
|
||||
function backupSet(options: Options) {
|
||||
const failed = options.failedBackup ?? false;
|
||||
return {
|
||||
id: "backup-1", backup_id: "m15-rehearsal-a",
|
||||
state: failed ? "FAILED" : options.verified ? "VERIFIED" : "CREATED",
|
||||
policy_key: "control-plane.database", policy_revision: 1, modelforge_version: "0.1.0",
|
||||
modelforge_commit: "f".repeat(40), schema_revision: "20260827_0021",
|
||||
environment_fingerprint: {}, database_identity: { major_version: 17, server_version: "17.11" },
|
||||
destination_root: "/data/backups", manifest_relative_path: "m15-rehearsal-a/manifest.json",
|
||||
manifest_sha256: "a".repeat(64), included_asset_classes: ["AUTHORITATIVE"],
|
||||
excluded_asset_classes: ["EPHEMERAL"], payload_bytes: 4_294_967_296, encrypted: true,
|
||||
encryption_algorithm: "AES-256-GCM", encryption_key_id: "modelforge-backup-key-1",
|
||||
verification_details: failed
|
||||
? { verification: { checks: { manifest_hash: "MATCH" } } }
|
||||
: { verification: { checks: { manifest_hash: "MATCH", manifest_completeness: "COMPLETE", payload_hashes: "MATCH", archive_structure: "READABLE:412" } } },
|
||||
verified_at: options.verified ? "2026-08-27T09:00:00Z" : null,
|
||||
failure_code: failed ? "HASH_MISMATCH" : null,
|
||||
failure_reason: failed ? "database.dump payload hash does not match the manifest" : null,
|
||||
milestone: "m15-baseline", legal_hold: false,
|
||||
restore_eligible: options.verified === true && !failed,
|
||||
reason: "M15 disaster-recovery rehearsal", created_by: "operator",
|
||||
started_at: "2026-08-27T08:58:00Z", completed_at: "2026-08-27T08:59:00Z",
|
||||
expires_at: "2026-09-26T08:59:00Z", created_at: "2026-08-27T08:58:00Z",
|
||||
entries: [{ id: "entry-1", logical_asset_type: "control_plane_database", object_name: "database.dump.enc", relative_path: "m15-rehearsal-a/database.dump.enc", size_bytes: 4_294_967_296, sha256: "b".repeat(64), source_generation: "pg_dump:20260827_0021", schema_version: "20260827_0021", dependency_refs: {} }],
|
||||
};
|
||||
}
|
||||
|
||||
function mockRecovery(options: Options = {}): void {
|
||||
const resolved: Options = { ...options, verified: options.verified ?? true };
|
||||
const verified = resolved.verified === true;
|
||||
vi.stubGlobal("fetch", vi.fn((input: RequestInfo | URL) => {
|
||||
const url = String(input); let payload: unknown = [];
|
||||
if (url.includes("/recovery/dashboard")) payload = {
|
||||
observed_at: "2026-08-27T10:00:00Z", point_in_time_support: "NOT_SUPPORTED",
|
||||
latest_verified_backup_id: verified ? "m15-rehearsal-a" : null,
|
||||
latest_verified_backup_at: verified ? "2026-08-27T09:00:00Z" : null,
|
||||
latest_verified_backup_age_seconds: verified ? 3600 : null,
|
||||
latest_verified_schema_revision: verified ? "20260827_0021" : null,
|
||||
backup_states: verified ? { VERIFIED: 1 } : {}, verified_backup_count: verified ? 1 : 0,
|
||||
stale_backup: resolved.staleBackup ?? false, backup_staleness_threshold_seconds: 93600,
|
||||
last_restore_rehearsal_at: verified ? "2026-08-27T09:10:00Z" : null,
|
||||
last_restore_rehearsal_state: verified ? "READY" : null,
|
||||
observed_restore_seconds: verified ? 78.42 : null,
|
||||
observed_rpo_seconds: verified ? 612.5 : null,
|
||||
protected_asset_count: verified ? 14 : 13,
|
||||
unprotected_assets: resolved.unprotected ? ["postgres.modelforge"] : [],
|
||||
readiness: [
|
||||
{ asset_key: "postgres.modelforge", asset_name: "ModelForge PostgreSQL database", asset_class: "AUTHORITATIVE", readiness: resolved.unprotected ? "UNPROTECTED" : "PROTECTED", policy_key: "control-plane.database", rpo_seconds: 86400, detail: resolved.unprotected ? "no verified backup exists yet" : "protected by verified backup m15-rehearsal-a" },
|
||||
{ asset_key: "artifacts.huggingface", asset_name: "Hugging Face model artifacts", asset_class: "REBUILDABLE", readiness: "REHYDRATABLE", policy_key: "artifacts.rehydratable", rpo_seconds: null, detail: "exact-revision redownload into quarantine, verify, then promote" },
|
||||
{ asset_key: "credentials.project", asset_name: "Project gateway credentials", asset_class: "SECRET", readiness: "ROTATION_REQUIRED", policy_key: "secrets.credentials", rpo_seconds: null, detail: "hashes restore as authoritative state; plaintext is unrecoverable" },
|
||||
{ asset_key: "external.examplerag", asset_name: "ExampleRAG Qdrant collections and aliases", asset_class: "EXTERNAL", readiness: "EXTERNAL_DEPENDENCY", policy_key: "external.projects", rpo_seconds: null, detail: "owned by ExampleRAG; ModelForge recovery never writes to it" },
|
||||
],
|
||||
coverage_ratio: resolved.unprotected ? 0.9286 : 1, estimated_protected_bytes: 4_294_967_296,
|
||||
estimated_rehydratable_bytes: 548_000_000, destination_capacity_bytes: 1_099_511_627_776,
|
||||
destination_free_bytes: 549_755_813_888,
|
||||
};
|
||||
else if (url.includes("/recovery/capacity")) payload = {
|
||||
bytes_to_copy: 4_294_967_296, bytes_manifest_only: 548_000_000,
|
||||
estimated_protected_bytes: 4_294_967_296, available_bytes: 549_755_813_888,
|
||||
capacity_bytes: 1_099_511_627_776, sufficient: resolved.sufficientCapacity ?? true,
|
||||
detail: (resolved.sufficientCapacity ?? true) ? "549755813888 bytes available for an estimated 13958643712 byte requirement" : "only 1024 bytes remain for an estimated 13958643712 byte requirement",
|
||||
};
|
||||
else if (url.includes("/recovery/policies")) payload = [
|
||||
{ id: "policy-1", key: "control-plane.database", name: "Control-plane PostgreSQL", revision: 1, asset_class: "AUTHORITATIVE", backup_method: "POSTGRES_LOGICAL_CUSTOM", retention_days: 30, minimum_verified_backups: 2, rpo_seconds: 86400, rto_target_seconds: 3600, restore_verification: "FULL_RESTORE", encryption_required: true, external_dependency: false, rehydration_allowed: false, secret_class: null, rationale: "Provenance, lifecycle, migration, approval and audit truth exist nowhere else.", active: true, fingerprint: "c".repeat(64), created_by: "modelforge", created_at: "2026-08-27T08:00:00Z" },
|
||||
{ id: "policy-2", key: "runtime.ephemeral", name: "Queues, leases and runtime residency", revision: 1, asset_class: "EPHEMERAL", backup_method: "NOT_BACKED_UP", retention_days: 1, minimum_verified_backups: 1, rpo_seconds: null, rto_target_seconds: 300, restore_verification: "NOT_APPLICABLE", encryption_required: false, external_dependency: false, rehydration_allowed: false, secret_class: null, rationale: "Redis queues, GPU leases, residency and NVML telemetry describe the present, not history.", active: true, fingerprint: "d".repeat(64), created_by: "modelforge", created_at: "2026-08-27T08:00:00Z" },
|
||||
];
|
||||
else if (url.includes("/recovery/assets")) payload = [
|
||||
{ id: "asset-1", key: "postgres.modelforge", name: "ModelForge PostgreSQL database", asset_class: "AUTHORITATIVE", owner: "modelforge-control-plane", location: "postgres:5432/modelforge", backup_method: "POSTGRES_LOGICAL_CUSTOM", restore_method: "pg_restore of a verified custom-format dump into a fresh database", rebuild_method: null, rpo_seconds: 86400, readiness: "PROTECTED", dependencies: ["backup.destination"], notes: "Provenance, lifecycle, migration, approvals, audit and observability history.", policy_key: "control-plane.database", updated_at: "2026-08-27T08:00:00Z" },
|
||||
{ id: "asset-2", key: "external.examplerag", name: "ExampleRAG Qdrant collections and aliases", asset_class: "EXTERNAL", owner: "examplerag", location: "ExampleRAG-owned Qdrant instance", backup_method: "NOT_BACKED_UP", restore_method: "owned by ExampleRAG; ModelForge recovery never writes to it", rebuild_method: "ExampleRAG reindex using a ModelForge capability", rpo_seconds: null, readiness: "EXTERNAL_DEPENDENCY", dependencies: [], notes: "A ModelForge restore must leave the production alias untouched.", policy_key: "external.projects", updated_at: "2026-08-27T08:00:00Z" },
|
||||
];
|
||||
else if (url.includes("/recovery/backups")) payload = [backupSet(resolved)];
|
||||
else if (url.includes("/recovery/restore-plans")) payload = [{
|
||||
id: "plan-1", backup_set_id: "backup-1", backup_id: "m15-rehearsal-a", mode: "VALIDATION",
|
||||
state: "PREFLIGHT_PASSED", target_environment: "ISOLATED", target_label: "m15-isolated-restore",
|
||||
database_destination_redacted: "postgresql+psycopg://modelforge:***@postgres:5432/mf_restore",
|
||||
artifact_strategy: "MANIFEST_ONLY", secret_strategy: "RESTORE_HASHES", node_strategy: "NONE",
|
||||
expected_modelforge_version: null,
|
||||
preflight: { status: "PASS", checks: [{ check: "backup_verified", status: "PASS", detail: "backup state is VERIFIED" }, { check: "destination_isolation", status: "PASS", detail: "postgresql+psycopg://modelforge:***@postgres:5432/mf_restore" }], failure_codes: [] },
|
||||
validation_requirements: {}, fingerprint: "e".repeat(64),
|
||||
reason: "isolated M15 validation restore rehearsal", created_by: "operator",
|
||||
created_at: "2026-08-27T09:05:00Z",
|
||||
}];
|
||||
else if (url.includes("/recovery/restore-operations")) payload = [{
|
||||
id: "restore-1", plan_id: "plan-1", backup_set_id: "backup-1", backup_id: "m15-rehearsal-a",
|
||||
state: "READY", mode: "VALIDATION", attempt: 1, idempotency_key: "f".repeat(64),
|
||||
preflight_result: { status: "PASS" },
|
||||
phase_durations: { PREFLIGHT: 1.21, RESTORING_DATABASE: 61.4, VALIDATING: 12.8 },
|
||||
source_fingerprint: { digest: "1".repeat(64) }, restored_fingerprint: { digest: "1".repeat(64) },
|
||||
fingerprint_diff: { identical: true, identical_table_count: 112, differences: [] },
|
||||
validation_result: { table_count: 113, fingerprint_identical: true, differing_groups: [], observed_rpo_seconds: 612.5 },
|
||||
rpo_seconds: 612.5, rto_seconds: 78.42, failure_code: null, failure_reason: null,
|
||||
started_at: "2026-08-27T09:10:00Z", updated_at: "2026-08-27T09:11:20Z", ready_at: "2026-08-27T09:11:20Z",
|
||||
}];
|
||||
else if (url.includes("/recovery/artifact-recoveries")) payload = [{
|
||||
id: "artifact-1", restore_operation_id: null, artifact_set_id: "set-1",
|
||||
model_revision_id: "revision-1", recovery_class: "REHYDRATABLE", state: "RECOVERED",
|
||||
upstream_repository: "nomic-ai/nomic-embed-text-v1.5", upstream_commit_sha: "a".repeat(40),
|
||||
target_storage_root_id: "root-1", expected_files: [{ filename: "model.safetensors" }],
|
||||
verified_files: [{ filename: "model.safetensors" }], bytes_total: 548_000_000,
|
||||
bytes_recovered: 548_000_000, download_plan_id: null, artifact_job_id: null,
|
||||
lineage: { lineage_preserved: true }, duration_seconds: 96.3, failure_code: null,
|
||||
failure_reason: null, started_at: "2026-08-27T09:20:00Z", completed_at: "2026-08-27T09:21:36Z",
|
||||
}];
|
||||
return Promise.resolve(new Response(JSON.stringify(payload), { status: 200 }));
|
||||
}));
|
||||
}
|
||||
|
||||
async function load(options: Options = {}): Promise<void> {
|
||||
mockRecovery(options);
|
||||
render(<RecoveryWorkspace />);
|
||||
fireEvent.change(screen.getByLabelText("Operator API key"), { target: { value: "operator-test" } });
|
||||
fireEvent.click(screen.getByRole("button", { name: "Load recovery" }));
|
||||
await screen.findByRole("tab", { name: "Backup sets" });
|
||||
}
|
||||
|
||||
afterEach(() => { cleanup(); vi.unstubAllGlobals(); });
|
||||
|
||||
describe("M15 recovery operator workspace", () => {
|
||||
it("shows the latest verified backup, its age, schema and measured RTO/RPO", async () => {
|
||||
await load();
|
||||
expect(await screen.findByText("m15-rehearsal-a")).toBeInTheDocument();
|
||||
expect(screen.getByText("20260827_0021")).toBeInTheDocument();
|
||||
expect(screen.getByText("78.42 s")).toBeInTheDocument();
|
||||
expect(screen.getByText(/612\.50 s/)).toBeInTheDocument();
|
||||
expect(screen.getByText("Point-in-time recovery: NOT_SUPPORTED")).toBeInTheDocument();
|
||||
expect(screen.getByText("Within the recovery policy window")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("never invents recovery figures when nothing has been backed up", async () => {
|
||||
await load({ verified: false, staleBackup: true, unprotected: true });
|
||||
expect(await screen.findByText("NONE")).toBeInTheDocument();
|
||||
expect(screen.getByText("Authoritative state is currently unprotected.")).toBeInTheDocument();
|
||||
expect(screen.getAllByText("not measured").length).toBeGreaterThan(0);
|
||||
expect(screen.getByText(/Unprotected: postgres.modelforge/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/Stale against a 26\.0 h policy window/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("separates protected, rehydratable, rotation-required and external readiness", async () => {
|
||||
await load();
|
||||
await screen.findByText("ModelForge PostgreSQL database");
|
||||
expect(screen.getByText("PROTECTED")).toBeInTheDocument();
|
||||
expect(screen.getByText("REHYDRATABLE")).toBeInTheDocument();
|
||||
expect(screen.getByText("ROTATION REQUIRED")).toBeInTheDocument();
|
||||
expect(screen.getByText("EXTERNAL DEPENDENCY")).toBeInTheDocument();
|
||||
expect(screen.getByText(/owned by ExampleRAG; ModelForge recovery never writes to it/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows a verified backup as restore eligible with its manifest and encryption identity", async () => {
|
||||
await load();
|
||||
fireEvent.click(screen.getByRole("tab", { name: "Backup sets" }));
|
||||
expect(await screen.findByText("VERIFIED")).toBeInTheDocument();
|
||||
expect(screen.getByText(/Restore eligible/)).toBeInTheDocument();
|
||||
expect(screen.getByText("AES-256-GCM · modelforge-backup-key-1")).toBeInTheDocument();
|
||||
expect(screen.getByText("aaaaaaaaaaaa…")).toBeInTheDocument();
|
||||
expect(screen.getByText("4.00 GiB")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows a failed backup with its failure code and refuses to call it eligible", async () => {
|
||||
await load({ verified: false, failedBackup: true });
|
||||
fireEvent.click(screen.getByRole("tab", { name: "Backup sets" }));
|
||||
expect(await screen.findByText("FAILED")).toBeInTheDocument();
|
||||
expect(screen.getByText(/Not restore eligible/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/HASH_MISMATCH: database.dump payload hash does not match the manifest/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("warns instead of proceeding when the backup destination lacks capacity", async () => {
|
||||
await load({ sufficientCapacity: false });
|
||||
fireEvent.click(screen.getByRole("tab", { name: "Backup sets" }));
|
||||
expect(await screen.findByText("Insufficient destination capacity")).toBeInTheDocument();
|
||||
expect(screen.getAllByText(/only 1024 bytes remain/).length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("lists the individual verification checks that promoted a backup", async () => {
|
||||
await load();
|
||||
fireEvent.click(screen.getByRole("tab", { name: "Verification" }));
|
||||
expect(await screen.findByText("manifest hash")).toBeInTheDocument();
|
||||
expect(screen.getByText("manifest completeness")).toBeInTheDocument();
|
||||
expect(screen.getByText("READABLE:412")).toBeInTheDocument();
|
||||
expect(screen.getByText("1 manifest object")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows a restore plan with a redacted destination and its preflight checks", async () => {
|
||||
await load();
|
||||
fireEvent.click(screen.getByRole("tab", { name: "Restore plans" }));
|
||||
expect(await screen.findByText("m15-isolated-restore")).toBeInTheDocument();
|
||||
expect(screen.getAllByText(/postgresql\+psycopg:\/\/modelforge:\*\*\*@postgres:5432\/mf_restore/).length).toBe(2);
|
||||
expect(screen.getByText("PREFLIGHT PASSED")).toBeInTheDocument();
|
||||
expect(screen.getByText("destination isolation")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("reports restore phase timing and the semantic fingerprint comparison", async () => {
|
||||
await load();
|
||||
fireEvent.click(screen.getByRole("tab", { name: "Restores" }));
|
||||
expect(await screen.findByText("READY")).toBeInTheDocument();
|
||||
expect(screen.getByText("identical to the backup point")).toBeInTheDocument();
|
||||
expect(screen.getByText("61.40 s")).toBeInTheDocument();
|
||||
expect(screen.getByText("113")).toBeInTheDocument();
|
||||
expect(screen.getByText("none")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows artifact recovery bound to an exact upstream revision", async () => {
|
||||
await load();
|
||||
fireEvent.click(screen.getByRole("tab", { name: "Artifact recovery" }));
|
||||
expect(await screen.findByText("nomic-ai/nomic-embed-text-v1.5")).toBeInTheDocument();
|
||||
expect(screen.getByText(/Exact revision aaaaaaaaaaaa…/)).toBeInTheDocument();
|
||||
expect(screen.getByText("RECOVERED")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows versioned policies including the deliberately unbacked ephemeral class", async () => {
|
||||
await load();
|
||||
fireEvent.click(screen.getByRole("tab", { name: "Policies" }));
|
||||
expect(await screen.findByText("Queues, leases and runtime residency")).toBeInTheDocument();
|
||||
expect(screen.getByText("NOT_BACKED_UP")).toBeInTheDocument();
|
||||
expect(screen.getByText("not applicable")).toBeInTheDocument();
|
||||
expect(screen.getByText("30 days · keep 2 verified")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows the classified asset inventory with owners and external boundaries", async () => {
|
||||
await load();
|
||||
fireEvent.click(screen.getByRole("tab", { name: "Assets" }));
|
||||
expect(await screen.findByText("examplerag")).toBeInTheDocument();
|
||||
expect(screen.getByText("backup.destination")).toBeInTheDocument();
|
||||
expect(screen.getByText("not rebuildable")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("keeps the operator credential out of the rendered document", async () => {
|
||||
await load();
|
||||
const input = screen.getByLabelText("Operator API key") as HTMLInputElement;
|
||||
expect(input.type).toBe("password");
|
||||
expect(document.body.textContent).not.toContain("operator-test");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,147 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Archive, CheckCircle2, Clock3, DatabaseBackup, FileCheck2, HardDriveDownload, History, LockKeyhole, RefreshCw, ShieldAlert } from "lucide-react";
|
||||
|
||||
import { api, operatorCredentialReference } from "../lib/api";
|
||||
import { TabList, TabPanel } from "./TabList";
|
||||
import type { ArtifactRecovery, BackupCapacityEstimate, BackupSet, RecoveryAsset, RecoveryDashboard, RecoveryPolicy, RestoreOperation, RestorePlan } from "../types";
|
||||
|
||||
type Tab = "readiness" | "backups" | "verification" | "plans" | "restores" | "artifacts" | "policies" | "assets" | "history";
|
||||
const tabs: Array<{ key: Tab; label: string }> = [
|
||||
{ key: "readiness", label: "Recovery readiness" }, { key: "backups", label: "Backup sets" },
|
||||
{ key: "verification", label: "Verification" }, { key: "plans", label: "Restore plans" },
|
||||
{ key: "restores", label: "Restores" }, { key: "artifacts", label: "Artifact recovery" },
|
||||
{ key: "policies", label: "Policies" }, { key: "assets", label: "Assets" },
|
||||
{ key: "history", label: "History" },
|
||||
];
|
||||
|
||||
interface RecoveryData {
|
||||
dashboard: RecoveryDashboard | null; policies: RecoveryPolicy[]; assets: RecoveryAsset[];
|
||||
backups: BackupSet[]; plans: RestorePlan[]; restores: RestoreOperation[];
|
||||
artifacts: ArtifactRecovery[]; capacity: BackupCapacityEstimate | null;
|
||||
}
|
||||
const empty: RecoveryData = { dashboard: null, policies: [], assets: [], backups: [], plans: [], restores: [], artifacts: [], capacity: null };
|
||||
|
||||
function date(value?: string | null): string { return value ? new Date(value).toLocaleString() : "—"; }
|
||||
function bytes(value?: number | null): string { return value == null ? "unknown" : `${(value / 1024 ** 3).toFixed(2)} GiB`; }
|
||||
function seconds(value?: number | null): string { return value == null ? "not measured" : `${value.toFixed(2)} s`; }
|
||||
function duration(value?: number | null): string {
|
||||
if (value == null) return "not measured";
|
||||
if (value < 90) return `${value.toFixed(1)} s`;
|
||||
return `${(value / 3600).toFixed(1)} h`;
|
||||
}
|
||||
function stateClass(value: string): string { return value.toLowerCase().replaceAll("_", "-"); }
|
||||
function digest(value?: string | null): string { return value ? `${value.slice(0, 12)}…` : "—"; }
|
||||
|
||||
export function RecoveryWorkspace() {
|
||||
const sessionCredential = operatorCredentialReference();
|
||||
const [tab, setTab] = useState<Tab>("readiness");
|
||||
const [token, setToken] = useState(sessionCredential); const [data, setData] = useState<RecoveryData>(empty);
|
||||
const [loading, setLoading] = useState(false); const [error, setError] = useState<string | null>(null);
|
||||
const [notice, setNotice] = useState<string | null>(null);
|
||||
|
||||
async function refresh() {
|
||||
if (!token) return; setLoading(true); setError(null);
|
||||
try {
|
||||
const [dashboard, policies, assets, backups, plans, restores, artifacts, capacity] = await Promise.all([
|
||||
api.recoveryDashboard(token), api.recoveryPolicies(token), api.recoveryAssets(token),
|
||||
api.backupSets(token), api.restorePlans(token), api.restoreOperations(token),
|
||||
api.artifactRecoveries(token), api.recoveryCapacity(token),
|
||||
]);
|
||||
setData({ dashboard, policies, assets, backups, plans, restores, artifacts, capacity });
|
||||
} catch (cause) { setError(cause instanceof Error ? cause.message : "Recovery state unavailable"); }
|
||||
finally { setLoading(false); }
|
||||
}
|
||||
async function run(action: () => Promise<unknown>, message: string) {
|
||||
setError(null); setNotice(null);
|
||||
try { await action(); setNotice(message); await refresh(); }
|
||||
catch (cause) { setError(cause instanceof Error ? cause.message : "Recovery action failed"); }
|
||||
}
|
||||
useEffect(() => { if (sessionCredential) void refresh(); }, [sessionCredential]);
|
||||
|
||||
return <div className="operations-stack">
|
||||
{!sessionCredential ? <form className="panel operations-boundary" onSubmit={(event) => { event.preventDefault(); void refresh(); }}><LockKeyhole size={25} /><div><span className="eyebrow">M15 · RECOVERY TRUTH</span><h2>Verified backups, restore plans and measured recovery</h2><p>Every figure here is measured. A backup counts only once its manifest and payload hashes verify, and a restore is READY only after its restored state has been compared with the state that was backed up.</p></div><label htmlFor="recovery-operator-key">Operator API key</label><input id="recovery-operator-key" name="operator-api-key" type="password" value={token} onChange={(event) => setToken(event.target.value)} autoComplete="off" /><button type="submit" disabled={!token || loading}><RefreshCw size={15} className={loading ? "spin" : ""} />{loading ? "Loading…" : "Load recovery"}</button></form> : null}
|
||||
{error ? <div className="error-banner"><strong>Recovery view unavailable</strong><span>{error}. No protected state is inferred.</span></div> : null}
|
||||
{notice ? <div className="lifecycle-notice"><CheckCircle2 size={16} />{notice}</div> : null}
|
||||
<TabList idPrefix="recovery" label="Recovery views" className="lifecycle-tabs operations-tabs" tabs={tabs} active={tab} onSelect={setTab} />
|
||||
<TabPanel idPrefix="recovery" active={tab}>
|
||||
{tab === "readiness" ? <Readiness data={data} token={token} run={run} /> : null}
|
||||
{tab === "backups" ? <Backups backups={data.backups} capacity={data.capacity} token={token} run={run} /> : null}
|
||||
{tab === "verification" ? <Verification backups={data.backups} /> : null}
|
||||
{tab === "plans" ? <Plans plans={data.plans} token={token} run={run} /> : null}
|
||||
{tab === "restores" ? <Restores restores={data.restores} /> : null}
|
||||
{tab === "artifacts" ? <Artifacts artifacts={data.artifacts} /> : null}
|
||||
{tab === "policies" ? <Policies policies={data.policies} /> : null}
|
||||
{tab === "assets" ? <Assets assets={data.assets} /> : null}
|
||||
{tab === "history" ? <HistoryView backups={data.backups} restores={data.restores} /> : null}
|
||||
</TabPanel>
|
||||
</div>;
|
||||
}
|
||||
|
||||
function Readiness({ data, token, run }: { data: RecoveryData; token: string; run: (action: () => Promise<unknown>, message: string) => Promise<void> }) {
|
||||
const current = data.dashboard;
|
||||
if (!current) return <Empty text="No recovery state has been loaded. Supply an operator credential to read measured recovery truth." />;
|
||||
return <><section className="operations-summary">
|
||||
<article className="panel"><DatabaseBackup /><span className="eyebrow">LATEST VERIFIED BACKUP</span><h3>{current.latest_verified_backup_id ?? "NONE"}</h3><p>{current.latest_verified_backup_id ? `Verified ${date(current.latest_verified_backup_at)}` : "Authoritative state is currently unprotected."}</p></article>
|
||||
<article className="panel"><Clock3 /><span className="eyebrow">BACKUP AGE</span><h3>{current.latest_verified_backup_age_seconds == null ? "—" : duration(current.latest_verified_backup_age_seconds)}</h3><p>{current.stale_backup ? `Stale against a ${(current.backup_staleness_threshold_seconds / 3600).toFixed(1)} h policy window` : "Within the recovery policy window"}</p></article>
|
||||
<article className="panel"><FileCheck2 /><span className="eyebrow">SCHEMA REVISION</span><h3>{current.latest_verified_schema_revision ?? "—"}</h3><p>Point-in-time recovery: {current.point_in_time_support}</p></article>
|
||||
<article className="panel"><HardDriveDownload /><span className="eyebrow">OBSERVED RTO / RPO</span><h3>{seconds(current.observed_restore_seconds)}</h3><p>Data loss {seconds(current.observed_rpo_seconds)} · last rehearsal {date(current.last_restore_rehearsal_at)} ({current.last_restore_rehearsal_state ?? "none"})</p></article>
|
||||
</section>
|
||||
<section className="operations-section-title"><div><span className="eyebrow">COVERAGE</span><h2>{(current.coverage_ratio * 100).toFixed(0)}% of classified assets are protected, rehydratable or externally owned</h2><p>{current.unprotected_assets.length ? `Unprotected: ${current.unprotected_assets.join(", ")}` : "No classified asset is unprotected."} Protected {bytes(current.estimated_protected_bytes)} copied · {bytes(current.estimated_rehydratable_bytes)} rehydratable by manifest.</p></div><button disabled={!token} onClick={() => run(() => api.applyRecoveryRetention(token), "Retention ran; the last verified backup is always preserved.")}><Archive size={15} />Apply retention</button></section>
|
||||
<div className="operations-grid">{current.readiness.map((item) => <article className="panel operation-card" key={item.asset_key}><div><span className="eyebrow">{item.asset_class}</span><span className={`status-chip ${stateClass(item.readiness)}`}>{item.readiness.replaceAll("_", " ")}</span></div><h3>{item.asset_name}</h3><p>{item.detail}</p><dl><dt>Policy</dt><dd>{item.policy_key}</dd><dt>RPO target</dt><dd>{item.rpo_seconds == null ? "not applicable" : duration(item.rpo_seconds)}</dd></dl></article>)}</div></>;
|
||||
}
|
||||
|
||||
function Backups({ backups, capacity, token, run }: { backups: BackupSet[]; capacity: BackupCapacityEstimate | null; token: string; run: (action: () => Promise<unknown>, message: string) => Promise<void> }) {
|
||||
const [backupId, setBackupId] = useState("");
|
||||
return <section>
|
||||
<div className="operations-section-title"><div><span className="eyebrow">IMMUTABLE SNAPSHOTS</span><h2>Backup sets and destination capacity</h2>{capacity ? <p>{capacity.detail}. {bytes(capacity.bytes_to_copy)} to copy, {bytes(capacity.bytes_manifest_only)} protected by manifest only.</p> : <p>Capacity has not been read yet.</p>}</div>
|
||||
<label>New backup identity<input value={backupId} onChange={(event) => setBackupId(event.target.value)} placeholder="m15-nightly-0001" /></label>
|
||||
<button disabled={!token || backupId.length < 7} onClick={() => run(() => api.createBackupSet({ backup_id: backupId, reason: "Operator-initiated backup from the Recovery workspace" }, token), "A backup set was created; verify it before it becomes restore eligible.")}><DatabaseBackup size={15} />Create backup</button></div>
|
||||
{capacity && !capacity.sufficient ? <div className="error-banner"><strong>Insufficient destination capacity</strong><span>{capacity.detail}. The backup will be refused rather than filling the disk.</span></div> : null}
|
||||
<div className="operations-grid">{backups.length ? backups.map((item) => <article className="panel operation-card" key={item.id}><div><span className="eyebrow">{item.policy_key} · REV {item.policy_revision}</span><span className={`status-chip ${stateClass(item.state)}`}>{item.state}</span></div><h3>{item.backup_id}</h3><p>{item.restore_eligible ? "Restore eligible" : "Not restore eligible"}{item.milestone ? ` · milestone ${item.milestone}` : ""}{item.legal_hold ? " · legal hold" : ""}</p><dl><dt>Schema</dt><dd>{item.schema_revision ?? "unknown"}</dd><dt>Payload</dt><dd>{bytes(item.payload_bytes)}</dd><dt>Encryption</dt><dd>{item.encrypted ? `${item.encryption_algorithm} · ${item.encryption_key_id}` : "not encrypted"}</dd><dt>Manifest</dt><dd>{digest(item.manifest_sha256)}</dd><dt>Verified</dt><dd>{date(item.verified_at)}</dd><dt>Expires</dt><dd>{date(item.expires_at)}</dd></dl>{item.failure_code ? <p className="failure">{item.failure_code}: {item.failure_reason}</p> : null}{item.state === "CREATED" || item.state === "FAILED" ? <button disabled={!token} onClick={() => run(() => api.verifyBackupSet(item.id, token), `Backup ${item.backup_id} was re-verified against its manifest.`)}>Verify</button> : null}</article>) : <Empty text="No backup sets exist. Authoritative control-plane state is unprotected until a verified backup exists." />}</div>
|
||||
</section>;
|
||||
}
|
||||
|
||||
function Verification({ backups }: { backups: BackupSet[] }) {
|
||||
return <div className="operations-grid">{backups.length ? backups.map((item) => {
|
||||
const verification = (item.verification_details?.verification ?? {}) as Record<string, unknown>;
|
||||
const checks = (verification.checks ?? {}) as Record<string, string>;
|
||||
return <article className="panel operation-card" key={item.id}><div><span className="eyebrow">{item.encrypted ? "ENCRYPTED" : "PLAINTEXT"}</span><span className={`status-chip ${stateClass(item.state)}`}>{item.state}</span></div><h3>{item.backup_id}</h3>{Object.keys(checks).length ? <dl>{Object.entries(checks).map(([name, value]) => <div key={name}><dt>{name.replaceAll("_", " ")}</dt><dd>{value}</dd></div>)}</dl> : <p>This backup has not been verified yet.</p>}{item.failure_code ? <p className="failure">{item.failure_code}: {item.failure_reason}</p> : null}<small>{item.entries.length} manifest object{item.entries.length === 1 ? "" : "s"}</small></article>;
|
||||
}) : <Empty text="No verification evidence exists yet." />}</div>;
|
||||
}
|
||||
|
||||
function Plans({ plans, token, run }: { plans: RestorePlan[]; token: string; run: (action: () => Promise<unknown>, message: string) => Promise<void> }) {
|
||||
return <div className="operations-grid">{plans.length ? plans.map((item) => {
|
||||
const preflight = item.preflight as { status?: string; checks?: Array<{ check: string; status: string; detail: string }>; failure_codes?: string[] };
|
||||
return <article className="panel operation-card" key={item.id}><div><span className="eyebrow">{item.mode} · {item.target_environment}</span><span className={`status-chip ${stateClass(item.state)}`}>{item.state.replaceAll("_", " ")}</span></div><h3>{item.target_label}</h3><p>{item.backup_id} → {item.database_destination_redacted}</p><dl><dt>Artifacts</dt><dd>{item.artifact_strategy}</dd><dt>Secrets</dt><dd>{item.secret_strategy}</dd><dt>Nodes</dt><dd>{item.node_strategy}</dd></dl>{preflight.checks?.length ? <dl>{preflight.checks.map((check) => <div key={check.check}><dt>{check.check.replaceAll("_", " ")}</dt><dd>{check.status} · {check.detail}</dd></div>)}</dl> : <p>No preflight has run for this plan.</p>}{preflight.failure_codes?.length ? <p className="failure">{preflight.failure_codes.join(", ")}</p> : null}<button disabled={!token} onClick={() => run(() => api.restorePlanPreflight(item.id, token), "Restore preflight completed; a restore may only start once it passes.")}>Run preflight</button></article>;
|
||||
}) : <Empty text="No restore plans exist. A restore may never target the database this control plane runs on." />}</div>;
|
||||
}
|
||||
|
||||
function Restores({ restores }: { restores: RestoreOperation[] }) {
|
||||
return <div className="operations-grid">{restores.length ? restores.map((item) => {
|
||||
const validation = item.validation_result as { table_count?: number; fingerprint_identical?: boolean; differing_groups?: string[]; observed_rpo_seconds?: number | null };
|
||||
const diff = item.fingerprint_diff as { identical?: boolean; identical_table_count?: number; differences?: unknown[] };
|
||||
return <article className="panel operation-card" key={item.id}><div><span className="eyebrow">{item.mode} · ATTEMPT {item.attempt}</span><span className={`status-chip ${stateClass(item.state)}`}>{item.state.replaceAll("_", " ")}</span></div><h3>{item.backup_id}</h3><dl><dt>Measured RTO</dt><dd>{seconds(item.rto_seconds)}</dd><dt>Measured RPO</dt><dd>{seconds(item.rpo_seconds)}</dd><dt>Restored tables</dt><dd>{validation.table_count ?? "—"}</dd><dt>Semantic fingerprint</dt><dd>{diff.identical ? "identical to the backup point" : `${diff.identical_table_count ?? 0} identical · ${(diff.differences ?? []).length} differing`}</dd><dt>Differing subjects</dt><dd>{validation.differing_groups?.length ? validation.differing_groups.join(", ") : "none"}</dd></dl>{Object.keys(item.phase_durations).length ? <dl>{Object.entries(item.phase_durations).map(([phase, value]) => <div key={phase}><dt>{phase.replaceAll("_", " ").toLowerCase()}</dt><dd>{value.toFixed(2)} s</dd></div>)}</dl> : null}{item.failure_code ? <p className="failure">{item.failure_code}: {item.failure_reason}</p> : null}<small>{date(item.started_at)} → {date(item.ready_at)}</small></article>;
|
||||
}) : <Empty text="No restore operations have been journaled." />}</div>;
|
||||
}
|
||||
|
||||
function Artifacts({ artifacts }: { artifacts: ArtifactRecovery[] }) {
|
||||
return <div className="operations-grid">{artifacts.length ? artifacts.map((item) => <article className="panel operation-card" key={item.id}><div><span className="eyebrow">{item.recovery_class}</span><span className={`status-chip ${stateClass(item.state)}`}>{item.state}</span></div><h3>{item.upstream_repository ?? "local artifact set"}</h3><p>{item.upstream_commit_sha ? `Exact revision ${item.upstream_commit_sha.slice(0, 12)}…` : "No exact upstream revision; this artifact cannot be rehydrated."}</p><dl><dt>Expected files</dt><dd>{item.expected_files.length}</dd><dt>Verified files</dt><dd>{item.verified_files.length}</dd><dt>Bytes</dt><dd>{bytes(item.bytes_recovered)} / {bytes(item.bytes_total)}</dd><dt>Duration</dt><dd>{seconds(item.duration_seconds)}</dd></dl>{item.failure_code ? <p className="failure">{item.failure_code}: {item.failure_reason}</p> : null}</article>) : <Empty text="No artifact recovery operations exist." />}</div>;
|
||||
}
|
||||
|
||||
function Policies({ policies }: { policies: RecoveryPolicy[] }) {
|
||||
return <div className="operations-grid">{policies.length ? policies.map((item) => <article className="panel operation-card" key={item.id}><div><span className="eyebrow">{item.asset_class} · REV {item.revision}</span><span className={`status-chip ${item.active ? "verified" : "archived"}`}>{item.active ? "ACTIVE" : "SUPERSEDED"}</span></div><h3>{item.name}</h3><p>{item.rationale}</p><dl><dt>Backup method</dt><dd>{item.backup_method}</dd><dt>RPO target</dt><dd>{item.rpo_seconds == null ? "not applicable" : duration(item.rpo_seconds)}</dd><dt>RTO target</dt><dd>{item.rto_target_seconds == null ? "not stated" : duration(item.rto_target_seconds)}</dd><dt>Retention</dt><dd>{item.retention_days} days · keep {item.minimum_verified_backups} verified</dd><dt>Verification</dt><dd>{item.restore_verification}</dd><dt>Encryption</dt><dd>{item.encryption_required ? "required" : "not required"}</dd></dl></article>) : <Empty text="No recovery policies have been seeded." />}</div>;
|
||||
}
|
||||
|
||||
function Assets({ assets }: { assets: RecoveryAsset[] }) {
|
||||
return <div className="operations-grid">{assets.length ? assets.map((item) => <article className="panel operation-card" key={item.id}><div><span className="eyebrow">{item.asset_class}</span><span className={`status-chip ${stateClass(item.readiness)}`}>{item.readiness.replaceAll("_", " ")}</span></div><h3>{item.name}</h3><p>{item.notes}</p><dl><dt>Owner</dt><dd>{item.owner}</dd><dt>Location</dt><dd>{item.location}</dd><dt>Backup</dt><dd>{item.backup_method}</dd><dt>Restore</dt><dd>{item.restore_method}</dd><dt>Rebuild</dt><dd>{item.rebuild_method ?? "not rebuildable"}</dd><dt>Depends on</dt><dd>{item.dependencies.length ? item.dependencies.join(", ") : "nothing"}</dd></dl></article>) : <Empty text="The recovery asset inventory is empty." />}</div>;
|
||||
}
|
||||
|
||||
function HistoryView({ backups, restores }: { backups: BackupSet[]; restores: RestoreOperation[] }) {
|
||||
const entries = [
|
||||
...backups.map((item) => ({ at: item.completed_at ?? item.created_at, title: `${item.backup_id} · ${item.state}`, detail: item.failure_reason ?? item.reason })),
|
||||
...restores.map((item) => ({ at: item.started_at, title: `restore ${item.backup_id} · ${item.state}`, detail: item.failure_reason ?? `attempt ${item.attempt}` })),
|
||||
].sort((a, b) => b.at.localeCompare(a.at));
|
||||
return <div className="operations-timeline">{entries.length ? entries.map((item, index) => <article className="panel" key={`${item.at}-${index}`}><History size={16} /><div><strong>{item.title}</strong><p>{item.detail}</p><small>{date(item.at)}</small></div></article>) : <Empty text="No recovery history exists yet." />}</div>;
|
||||
}
|
||||
|
||||
function Empty({ text }: { text: string }) { return <div className="state-card"><ShieldAlert size={18} />{text}</div>; }
|
||||
@@ -0,0 +1,115 @@
|
||||
import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { RuntimeWorkspace } from "./RuntimeWorkspace";
|
||||
|
||||
const profile = {
|
||||
id: "profile-1", name: "Qwen embedding BF16", runtime_environment_id: "environment-1",
|
||||
artifact_set_id: "artifact-set-1", adapter: "sentence_transformers", runtime_version: "4.1.0",
|
||||
image_digest: `sha256:${"b".repeat(64)}`, version: 1, dtype: "bfloat16", quantization: null,
|
||||
modality: "embedding", max_sequence_length: 128, batch_size: 1, concurrency: 1,
|
||||
device_policy: "cuda_required", gpu_memory_policy: {}, launch_parameters: {}, environment_variables: {},
|
||||
trust_remote_code: false, network_egress: false, fingerprint: "f".repeat(64),
|
||||
health_contract: { process: "required", runtime: "required", model: "functional_embedding_required" },
|
||||
immutable_at: "2026-08-25T00:00:00Z", created_at: "2026-08-25T00:00:00Z",
|
||||
};
|
||||
const environment = {
|
||||
id: "environment-1", name: "Sentence Transformers 4.1", adapter: "sentence_transformers",
|
||||
runtime_version: "4.1.0", image_repository: "modelforge-runtime-worker", image_digest: profile.image_digest,
|
||||
python_version: "3.11", cuda_runtime_version: "12.8", package_versions: {}, supported_model_types: ["qwen3"],
|
||||
supported_formats: ["safetensors"], supported_modalities: ["embedding"], network_policy: "offline_control_plane_only",
|
||||
fingerprint: "e".repeat(64), immutable_at: "2026-08-25T00:00:00Z", created_at: "2026-08-25T00:00:00Z",
|
||||
};
|
||||
const assessment = {
|
||||
id: "assessment-1", artifact_set_id: "artifact-set-1", runtime_profile_id: "profile-1", compute_node_id: "node-1",
|
||||
adapter: "sentence_transformers", runtime_version: "4.1.0", status: "compatible",
|
||||
static_result: {}, evidence: {}, blockers: [], warnings: [], required_approvals: ["lab_execution"],
|
||||
hardware_facts: {}, artifact_facts: { integrity_status: "verified" }, environment_fingerprint: "a".repeat(64),
|
||||
stale: false, stale_reason: null, created_at: "2026-08-25T00:00:00Z",
|
||||
};
|
||||
const approval = {
|
||||
id: "approval-1", artifact_set_id: "artifact-set-1", scope: "lab_execution", status: "approved",
|
||||
evidence_fingerprint: "c".repeat(64), reason: "Reviewed exact provenance", approved_by: "operator",
|
||||
approved_at: "2026-08-25T00:00:00Z", expires_at: null, revoked_at: null, stale: false,
|
||||
};
|
||||
const hardware = {
|
||||
overview: { status: "active", inventory_state: "active", node_count: 1, accelerator_count: 1 },
|
||||
nodes: [{ id: "node-1", display_name: "GPU Node", lab_eligible: true, liveness: "online" }],
|
||||
};
|
||||
|
||||
type RuntimeFixtures = { assessments?: unknown[]; approvals?: unknown[]; probes?: unknown[]; candidates?: unknown[] };
|
||||
function mockRuntime(fixtures: RuntimeFixtures = {}) {
|
||||
const calls: Array<{ url: string; init?: RequestInit }> = [];
|
||||
vi.stubGlobal("fetch", vi.fn((input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const url = String(input); calls.push({ url, init });
|
||||
let payload: unknown = [];
|
||||
if (url.endsWith("/runtime-environments")) payload = [environment];
|
||||
else if (url.endsWith("/runtime-profiles")) payload = [profile];
|
||||
else if (url.endsWith("/compatibility-assessments")) payload = fixtures.assessments ?? [assessment];
|
||||
else if (url.endsWith("/execution-approvals")) payload = fixtures.approvals ?? [approval];
|
||||
else if (url.endsWith("/runtime-probes") && init?.method === "POST") payload = { status: "queued" };
|
||||
else if (url.endsWith("/runtime-probes")) payload = fixtures.probes ?? [];
|
||||
else if (url.endsWith("/deployment-candidates")) payload = fixtures.candidates ?? [];
|
||||
else if (url.endsWith("/hardware")) payload = hardware;
|
||||
return Promise.resolve(new Response(JSON.stringify(payload), { status: 200 }));
|
||||
}));
|
||||
return calls;
|
||||
}
|
||||
|
||||
afterEach(() => { cleanup(); vi.unstubAllGlobals(); });
|
||||
|
||||
describe("M4 runtime plane UI", () => {
|
||||
it("shows immutable runtime profile and separate security gates", async () => {
|
||||
mockRuntime(); render(<RuntimeWorkspace />);
|
||||
expect(await screen.findByText("sentence_transformers@4.1.0")).toBeInTheDocument();
|
||||
expect(screen.getByText("remote code off · network off")).toBeInTheDocument();
|
||||
expect(screen.getByText("LAB APPROVED")).toBeInTheDocument();
|
||||
expect(screen.getAllByText("NOT APPROVED").length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("renders understandable compatibility blockers", async () => {
|
||||
mockRuntime({ assessments: [{ ...assessment, status: "blocked", blockers: ["NO_GGUF_ARTIFACT_VARIANT"] }] });
|
||||
render(<RuntimeWorkspace />);
|
||||
expect(await screen.findAllByText("NO GGUF ARTIFACT VARIANT")).toHaveLength(2);
|
||||
expect(screen.getByText("Probe blocked")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not present stale compatibility as reusable", async () => {
|
||||
mockRuntime({ assessments: [{ ...assessment, stale: true, stale_reason: "driver changed" }] });
|
||||
render(<RuntimeWorkspace />);
|
||||
expect(await screen.findAllByText("STALE")).toHaveLength(2);
|
||||
expect(screen.getByText("driver changed")).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: /Start controlled probe/ })).toBeDisabled();
|
||||
});
|
||||
|
||||
it("starts only the typed fixed-input probe flow", async () => {
|
||||
const calls = mockRuntime(); render(<RuntimeWorkspace />);
|
||||
fireEvent.click(await screen.findByRole("button", { name: /Start controlled probe/ }));
|
||||
await waitFor(() => expect(calls.some((item) => item.url.endsWith("/runtime-probes") && item.init?.method === "POST")).toBe(true));
|
||||
const request = calls.find((item) => item.url.endsWith("/runtime-probes") && item.init?.method === "POST")!;
|
||||
expect(JSON.parse(String(request.init?.body)).input_text).toBe("ModelForge runtime compatibility probe");
|
||||
});
|
||||
|
||||
it("shows measured VRAM, latency and finite-vector shape without quality claims", async () => {
|
||||
mockRuntime({ probes: [{ ...assessment, id: "probe-1", runtime_profile_id: "profile-1", status: "completed", phase: "completed", attempt_count: 1, cancel_requested: false, load_result: { load_time_ms: 1250 }, health_result: {}, inference_result: { latency_ms: 18.5, output_shape: [1, 1024] }, unload_result: {}, measured_resources: { baseline_vram_bytes: 1024 ** 3, peak_vram_bytes: 3 * 1024 ** 3, reclaimed_vram_bytes: 2 * 1024 ** 3 }, runtime_facts: {}, failure_code: null, environment_fingerprint: "d".repeat(64) }] });
|
||||
render(<RuntimeWorkspace />);
|
||||
expect(await screen.findByText("18.5 ms")).toBeInTheDocument();
|
||||
expect(screen.getByText("[1,1024]")).toBeInTheDocument();
|
||||
expect(screen.getByText("3.00 GiB")).toBeInTheDocument();
|
||||
expect(screen.queryByText(/quality/i)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows failures and preserves unknown measurements", async () => {
|
||||
mockRuntime({ probes: [{ ...assessment, id: "probe-1", runtime_profile_id: "profile-1", status: "failed", phase: "failed", attempt_count: 1, cancel_requested: false, load_result: {}, health_result: {}, inference_result: {}, unload_result: {}, measured_resources: {}, runtime_facts: {}, failure_code: "GPU_OOM", environment_fingerprint: "d".repeat(64) }] });
|
||||
render(<RuntimeWorkspace />);
|
||||
expect(await screen.findByText("GPU_OOM")).toBeInTheDocument();
|
||||
expect(screen.getAllByText("unknown").length).toBeGreaterThanOrEqual(3);
|
||||
});
|
||||
|
||||
it("renders LAB_READY candidate while production remains false", async () => {
|
||||
mockRuntime({ candidates: [{ id: "candidate-1", runtime_profile_id: "profile-1", status: "lab_ready", production: false }] });
|
||||
render(<RuntimeWorkspace />);
|
||||
expect(await screen.findByText("LAB_READY")).toBeInTheDocument();
|
||||
expect(screen.getByText("PRODUCTION: FALSE")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,97 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { Activity, AlertTriangle, CheckCircle2, RefreshCw, ShieldCheck, Square } from "lucide-react";
|
||||
|
||||
import { api } from "../lib/api";
|
||||
import type { CompatibilityAssessment, DeploymentCandidate, ExecutionApproval, HardwareState, RuntimeEnvironment, RuntimeProbe, RuntimeProfile } from "../types";
|
||||
|
||||
type RuntimeState = {
|
||||
environments: RuntimeEnvironment[]; profiles: RuntimeProfile[];
|
||||
assessments: CompatibilityAssessment[]; approvals: ExecutionApproval[];
|
||||
probes: RuntimeProbe[]; candidates: DeploymentCandidate[]; hardware: HardwareState;
|
||||
};
|
||||
|
||||
const emptyHardware: HardwareState = { overview: { status: "unknown", inventory_state: "unknown", node_count: 0, accelerator_count: 0 }, nodes: [] };
|
||||
const terminal = new Set(["completed", "failed", "cancelled"]);
|
||||
|
||||
function statusLabel(value: string): string { return value.replaceAll("_", " ").toUpperCase(); }
|
||||
function bytes(value: unknown): string { return typeof value === "number" ? `${(value / 1024 ** 3).toFixed(2)} GiB` : "unknown"; }
|
||||
function milliseconds(value: unknown): string { return typeof value === "number" ? `${value.toFixed(1)} ms` : "unknown"; }
|
||||
function nested(source: Record<string, unknown>, key: string): unknown { return source[key]; }
|
||||
|
||||
export function RuntimeWorkspace() {
|
||||
const [state, setState] = useState<RuntimeState>({ environments: [], profiles: [], assessments: [], approvals: [], probes: [], candidates: [], hardware: emptyHardware });
|
||||
const [selectedProfileId, setSelectedProfileId] = useState("");
|
||||
const [selectedNodeId, setSelectedNodeId] = useState("");
|
||||
const [operator, setOperator] = useState("local-operator");
|
||||
const [reason, setReason] = useState("Reviewed verified artifact provenance for isolated M4 lab execution");
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [working, setWorking] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const load = useCallback(async (quiet = false) => {
|
||||
if (!quiet) setLoading(true);
|
||||
try {
|
||||
const [environments, profiles, assessments, approvals, probes, candidates, hardware] = await Promise.all([
|
||||
api.runtimeEnvironments(), api.runtimeProfiles(), api.compatibilityAssessments(),
|
||||
api.executionApprovals(), api.runtimeProbes(), api.deploymentCandidates(), api.hardware(),
|
||||
]);
|
||||
setState({ environments, profiles, assessments, approvals, probes, candidates, hardware });
|
||||
setSelectedProfileId((current) => current || profiles[0]?.id || "");
|
||||
setSelectedNodeId((current) => current || hardware.nodes.find((node) => node.lab_eligible)?.id || "");
|
||||
setError(null);
|
||||
} catch (cause) { setError(cause instanceof Error ? cause.message : "Runtime plane unavailable"); }
|
||||
finally { if (!quiet) setLoading(false); }
|
||||
}, []);
|
||||
|
||||
useEffect(() => { void load(); }, [load]);
|
||||
const activeProbe = state.probes.find((probe) => !terminal.has(probe.status));
|
||||
useEffect(() => {
|
||||
if (!activeProbe) return;
|
||||
const interval = window.setInterval(() => void load(true), 2000);
|
||||
return () => window.clearInterval(interval);
|
||||
}, [activeProbe, load]);
|
||||
|
||||
const selectedProfile = state.profiles.find((item) => item.id === selectedProfileId);
|
||||
const selectedEnvironment = state.environments.find((item) => item.id === selectedProfile?.runtime_environment_id);
|
||||
const currentAssessment = useMemo(() => state.assessments.find((item) => item.runtime_profile_id === selectedProfileId && item.compute_node_id === selectedNodeId), [selectedNodeId, selectedProfileId, state.assessments]);
|
||||
const currentApproval = state.approvals.find((item) => item.artifact_set_id === selectedProfile?.artifact_set_id && !item.stale && item.status === "approved");
|
||||
const selectedProbe = state.probes.find((item) => item.runtime_profile_id === selectedProfileId);
|
||||
const currentCandidate = state.candidates.find((item) => item.runtime_profile_id === selectedProfileId);
|
||||
|
||||
async function operate(action: () => Promise<unknown>) {
|
||||
setWorking(true); setError(null);
|
||||
try { await action(); await load(true); }
|
||||
catch (cause) { setError(cause instanceof Error ? cause.message : "Runtime operation failed"); }
|
||||
finally { setWorking(false); }
|
||||
}
|
||||
|
||||
if (loading) return <div className="state-card"><span className="state-spinner" />Loading runtime evidence…</div>;
|
||||
return <div className="runtime-stack">
|
||||
{error ? <div className="error-banner"><strong>Runtime operation blocked</strong><span>{error}</span></div> : null}
|
||||
<article className="panel runtime-boundary"><ShieldCheck size={20} /><div><strong>Technical execution boundary</strong><p>Verified bytes, static compatibility and explicit LAB approval are separate gates. A successful probe may create LAB_READY evidence, never production activation.</p></div><span className="registry-status blocked">PRODUCTION NOT APPROVED</span></article>
|
||||
<section className="runtime-grid">
|
||||
<article className="panel runtime-controls"><span className="eyebrow">REVIEW EXACT COMBINATION</span><h2>Runtime profile</h2>
|
||||
<label>Profile<select aria-label="Runtime profile" value={selectedProfileId} onChange={(event) => setSelectedProfileId(event.target.value)}><option value="">Select a profile</option>{state.profiles.map((item) => <option key={item.id} value={item.id}>{item.name} · v{item.version}</option>)}</select></label>
|
||||
<label>Target node<select aria-label="Target node" value={selectedNodeId} onChange={(event) => setSelectedNodeId(event.target.value)}><option value="">Select a node</option>{state.hardware.nodes.map((item) => <option key={item.id} value={item.id}>{item.display_name} · {item.liveness}</option>)}</select></label>
|
||||
{selectedProfile ? <dl className="runtime-facts"><div><dt>Adapter</dt><dd>{selectedProfile.adapter}@{selectedProfile.runtime_version}</dd></div><div><dt>ArtifactSet</dt><dd><code>{selectedProfile.artifact_set_id}</code></dd></div><div><dt>Mode</dt><dd>{selectedProfile.dtype} · batch {selectedProfile.batch_size} · seq {selectedProfile.max_sequence_length}</dd></div><div><dt>Isolation</dt><dd>remote code off · network off</dd></div><div><dt>Image</dt><dd><code>{selectedProfile.image_digest}</code></dd></div><div><dt>Environment</dt><dd>{selectedEnvironment?.python_version ?? "unknown"} · CUDA {selectedEnvironment?.cuda_runtime_version ?? "unknown"}</dd></div></dl> : <p className="empty">No immutable runtime profiles registered.</p>}
|
||||
<button disabled={working || !selectedProfile || !selectedNodeId} onClick={() => selectedProfile && operate(() => api.assessCompatibility(selectedProfile.artifact_set_id, selectedProfile.id, selectedNodeId))}><RefreshCw size={14} />Run static assessment</button>
|
||||
</article>
|
||||
<article className="panel runtime-controls"><span className="eyebrow">EXECUTION GATES</span><h2>Lab approval & probe</h2>
|
||||
<div className="gate-list"><div><span>Integrity</span><strong>{String(currentAssessment?.artifact_facts.integrity_status ?? "unknown").toUpperCase()}</strong></div><div><span>Static compatibility</span><strong>{currentAssessment ? statusLabel(currentAssessment.stale ? "stale" : currentAssessment.status) : "NOT ASSESSED"}</strong></div><div><span>Execution</span><strong>{currentApproval ? "LAB APPROVED" : "NOT APPROVED"}</strong></div><div><span>Production</span><strong>NOT APPROVED</strong></div></div>
|
||||
{currentAssessment?.blockers.length ? <div className="runtime-blockers"><AlertTriangle size={16} /><div><strong>Probe blocked</strong>{currentAssessment.blockers.map((item) => <span key={item}>{statusLabel(item)}</span>)}</div></div> : null}
|
||||
{currentAssessment?.stale ? <div className="runtime-blockers"><AlertTriangle size={16} /><div><strong>Compatibility evidence is stale</strong><span>{currentAssessment.stale_reason ?? "Evidence changed"}</span></div></div> : null}
|
||||
{!currentApproval && selectedProfile ? <><label>Approved by<input aria-label="Approved by" value={operator} onChange={(event) => setOperator(event.target.value)} /></label><label>Review reason<textarea aria-label="Approval reason" value={reason} onChange={(event) => setReason(event.target.value)} /></label><button disabled={working || reason.length < 8} onClick={() => operate(() => api.approveLabExecution(selectedProfile.artifact_set_id, operator, reason))}><ShieldCheck size={14} />Approve exact ArtifactSet for LAB</button></> : null}
|
||||
<button disabled={working || !currentAssessment || currentAssessment.stale || currentAssessment.blockers.length > 0 || !currentApproval} onClick={() => currentAssessment && currentApproval && operate(() => api.startRuntimeProbe(currentAssessment.id, currentApproval.id))}><Activity size={14} />Start controlled probe</button>
|
||||
</article>
|
||||
</section>
|
||||
<article className="panel"><div className="panel-header"><div><span className="eyebrow">STATIC COMPATIBILITY MATRIX</span><h2>ArtifactSet / runtime / node evidence</h2></div><span>{state.assessments.length} assessment(s)</span></div>
|
||||
<div className="runtime-matrix">{state.assessments.map((item) => <button key={item.id} onClick={() => { setSelectedProfileId(item.runtime_profile_id); setSelectedNodeId(item.compute_node_id); }}><div><strong>{item.adapter}</strong><span>{state.hardware.nodes.find((node) => node.id === item.compute_node_id)?.display_name ?? item.compute_node_id}</span></div><span className={`registry-status ${item.stale ? "blocked" : item.status}`}>{item.stale ? "STALE" : statusLabel(item.status)}</span><small>{item.blockers.length ? item.blockers.map(statusLabel).join(" · ") : "No static blockers"}</small></button>)}</div>
|
||||
</article>
|
||||
{selectedProbe ? <article className="panel probe-detail"><div className="panel-header"><div><span className="eyebrow">RUNTIME PROBE · ATTEMPT {selectedProbe.attempt_count}</span><h2>{statusLabel(selectedProbe.phase ?? selectedProbe.status)}</h2></div><span className={`registry-status ${selectedProbe.status === "completed" ? "verified" : selectedProbe.status}`}>{statusLabel(selectedProbe.status)}</span></div>
|
||||
<div className="probe-timeline">{["preparing", "loading", "healthchecking", "ready", "unloading", "completed"].map((phase) => <span key={phase} className={phase === selectedProbe.phase || selectedProbe.status === "completed" ? "active" : ""}>{statusLabel(phase)}</span>)}</div>
|
||||
<dl className="runtime-facts"><div><dt>Load time</dt><dd>{milliseconds(nested(selectedProbe.load_result, "load_time_ms"))}</dd></div><div><dt>Inference latency</dt><dd>{milliseconds(nested(selectedProbe.inference_result, "latency_ms"))}</dd></div><div><dt>Output shape</dt><dd>{JSON.stringify(nested(selectedProbe.inference_result, "shape") ?? nested(selectedProbe.inference_result, "output_shape") ?? "unknown")}</dd></div><div><dt>Baseline VRAM</dt><dd>{bytes(nested(selectedProbe.measured_resources, "baseline_vram_bytes"))}</dd></div><div><dt>Peak VRAM</dt><dd>{bytes(nested(selectedProbe.measured_resources, "peak_vram_bytes"))}</dd></div><div><dt>Reclaimed VRAM</dt><dd>{bytes(nested(selectedProbe.measured_resources, "reclaimed_vram_bytes"))}</dd></div><div><dt>Environment</dt><dd><code>{selectedProbe.environment_fingerprint}</code></dd></div><div><dt>Failure</dt><dd>{selectedProbe.failure_code ?? "none"}</dd></div></dl>
|
||||
{!terminal.has(selectedProbe.status) ? <button className="danger" onClick={() => operate(() => api.cancelRuntimeProbe(selectedProbe.id))}><Square size={13} />Cancel and reclaim</button> : null}
|
||||
</article> : null}
|
||||
{currentCandidate ? <article className="panel candidate-card"><CheckCircle2 size={22} /><div><span className="eyebrow">DEPLOYMENT CANDIDATE</span><h2>LAB_READY</h2><p>Created only from successful local compatibility evidence and a completed probe. Capability routing and production activation remain disabled.</p><code>{currentCandidate.id}</code></div><span className="registry-status blocked">PRODUCTION: FALSE</span></article> : null}
|
||||
</div>;
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import type { ReactNode } from "react";
|
||||
import { AlertTriangle, X } from "lucide-react";
|
||||
|
||||
/**
|
||||
* The console's single guarded-destructive-action surface. Every destructive operator action uses
|
||||
* this dialog so identity, impact and acknowledgement are presented the same way everywhere, and so
|
||||
* focus containment, Escape dismissal and focus restoration are implemented once rather than per
|
||||
* call site. Native `window.confirm` is deliberately not used: it cannot carry impact evidence, it
|
||||
* ignores the design system, and it cannot honour reduced-motion or forced-colours.
|
||||
*/
|
||||
export type SafetyFact = { label: string; value: string };
|
||||
|
||||
export function SafetyDialog({
|
||||
idPrefix, eyebrow, title, subject, description, facts, acknowledgement, confirmLabel, confirmIcon,
|
||||
acknowledged, onAcknowledgedChange, pending = false, errorTitle, error, errorExtra, extra,
|
||||
closeLabel, onCancel, onConfirm,
|
||||
}: {
|
||||
idPrefix: string;
|
||||
eyebrow: string;
|
||||
title: string;
|
||||
subject: string;
|
||||
description: string;
|
||||
facts?: SafetyFact[];
|
||||
acknowledgement: string;
|
||||
confirmLabel: string;
|
||||
confirmIcon?: ReactNode;
|
||||
acknowledged: boolean;
|
||||
onAcknowledgedChange: (value: boolean) => void;
|
||||
pending?: boolean;
|
||||
errorTitle?: string;
|
||||
error?: string | null;
|
||||
errorExtra?: ReactNode;
|
||||
extra?: ReactNode;
|
||||
closeLabel: string;
|
||||
onCancel: () => void;
|
||||
onConfirm: () => void;
|
||||
}) {
|
||||
const dialogRef = useRef<HTMLElement>(null);
|
||||
const closeRef = useRef<HTMLButtonElement>(null);
|
||||
const invokerRef = useRef<HTMLElement | null>(null);
|
||||
const titleId = `${idPrefix}-title`;
|
||||
const impactId = `${idPrefix}-impact`;
|
||||
|
||||
useEffect(() => {
|
||||
invokerRef.current = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
||||
window.requestAnimationFrame(() => closeRef.current?.focus());
|
||||
const onKeyDown = (event: globalThis.KeyboardEvent) => {
|
||||
if (event.key === "Escape") onCancel();
|
||||
if (event.key === "Tab") {
|
||||
const focusable = dialogRef.current?.querySelectorAll<HTMLElement>('button:not([disabled]), input:not([disabled])');
|
||||
if (!focusable?.length) return;
|
||||
const first = focusable[0]; const last = focusable[focusable.length - 1];
|
||||
if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus(); }
|
||||
else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus(); }
|
||||
}
|
||||
};
|
||||
window.addEventListener("keydown", onKeyDown);
|
||||
return () => {
|
||||
window.removeEventListener("keydown", onKeyDown);
|
||||
// Focus returns to whatever opened the dialog, so a keyboard operator is never dropped at the
|
||||
// top of the document after cancelling or completing a guarded action.
|
||||
window.requestAnimationFrame(() => invokerRef.current?.focus());
|
||||
};
|
||||
// The dialog is mounted only while a guarded action is pending, so this runs once per opening.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return <div className="safety-scrim" role="presentation" onMouseDown={(event) => { if (event.target === event.currentTarget && !pending) onCancel(); }}>
|
||||
<section ref={dialogRef} className="safety-dialog" role="alertdialog" aria-modal="true" aria-labelledby={titleId} aria-describedby={impactId}>
|
||||
<div className="safety-title">
|
||||
<span><AlertTriangle size={18} /></span>
|
||||
<div><span className="eyebrow">{eyebrow}</span><h2 id={titleId}>{title}</h2></div>
|
||||
<button ref={closeRef} onClick={onCancel} disabled={pending} aria-label={closeLabel}><X size={16} /></button>
|
||||
</div>
|
||||
<div id={impactId} className="safety-impact">
|
||||
<strong>{subject}</strong>
|
||||
<p>{description}</p>
|
||||
{facts?.length ? <dl>{facts.map((fact) => <div key={fact.label}><dt>{fact.label}</dt><dd>{fact.value}</dd></div>)}</dl> : null}
|
||||
{extra}
|
||||
</div>
|
||||
<label className="safety-confirm">
|
||||
<input type="checkbox" checked={acknowledged} disabled={pending} onChange={(event) => onAcknowledgedChange(event.target.checked)} />
|
||||
<span>{acknowledgement}</span>
|
||||
</label>
|
||||
{error ? <div className="error-banner" role="alert"><strong>{errorTitle ?? "Action blocked"}</strong><span>{error}</span>{errorExtra}</div> : null}
|
||||
<div className="safety-actions">
|
||||
<button onClick={onCancel} disabled={pending}>Cancel</button>
|
||||
<button className="danger" disabled={!acknowledged || pending} onClick={onConfirm}>{confirmIcon}{pending ? "Working…" : confirmLabel}</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>;
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { useState } from "react";
|
||||
|
||||
import { TabList, TabPanel } from "./TabList";
|
||||
import type { TabDescriptor } from "./TabList";
|
||||
|
||||
type Key = "alpha" | "beta" | "gamma";
|
||||
const tabs: TabDescriptor<Key>[] = [
|
||||
{ key: "alpha", label: "alpha" }, { key: "beta", label: "beta" }, { key: "gamma", label: "gamma" },
|
||||
];
|
||||
|
||||
function Harness() {
|
||||
const [active, setActive] = useState<Key>("alpha");
|
||||
return <>
|
||||
<TabList idPrefix="probe" label="Probe views" className="probe-tabs" tabs={tabs} active={active} onSelect={setActive} />
|
||||
<TabPanel idPrefix="probe" active={active}>content for {active}</TabPanel>
|
||||
</>;
|
||||
}
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
describe("shared ARIA tabs pattern", () => {
|
||||
it("pairs every tab with the panel it controls in both directions", () => {
|
||||
render(<Harness />);
|
||||
const list = screen.getByRole("tablist", { name: "Probe views" });
|
||||
expect(list).toBeInTheDocument();
|
||||
const selected = screen.getByRole("tab", { selected: true });
|
||||
const panel = screen.getByRole("tabpanel");
|
||||
// The tab names the panel, and the panel names the tab back. A screen reader needs both halves.
|
||||
expect(selected.getAttribute("aria-controls")).toBe(panel.getAttribute("id"));
|
||||
expect(panel.getAttribute("aria-labelledby")).toBe(selected.getAttribute("id"));
|
||||
});
|
||||
|
||||
it("gives every tab a unique id and a resolvable panel reference", () => {
|
||||
render(<Harness />);
|
||||
const ids = screen.getAllByRole("tab").map((tab) => tab.getAttribute("id"));
|
||||
expect(new Set(ids).size).toBe(ids.length);
|
||||
expect(ids.every(Boolean)).toBe(true);
|
||||
// Only the selected panel is rendered, so exactly one aria-controls target resolves at a time.
|
||||
expect(document.querySelectorAll('[role="tabpanel"]').length).toBe(1);
|
||||
});
|
||||
|
||||
it("keeps roving tabindex on the selected tab only", () => {
|
||||
render(<Harness />);
|
||||
const [alpha, beta, gamma] = screen.getAllByRole("tab");
|
||||
expect(alpha.getAttribute("tabindex")).toBe("0");
|
||||
expect(beta.getAttribute("tabindex")).toBe("-1");
|
||||
expect(gamma.getAttribute("tabindex")).toBe("-1");
|
||||
});
|
||||
|
||||
it("moves selection with ArrowRight, ArrowLeft, Home and End including wrap", () => {
|
||||
render(<Harness />);
|
||||
const tabAt = (index: number) => screen.getAllByRole("tab")[index];
|
||||
|
||||
fireEvent.keyDown(tabAt(0), { key: "ArrowRight" });
|
||||
expect(screen.getByRole("tab", { selected: true })).toHaveTextContent("beta");
|
||||
|
||||
fireEvent.keyDown(tabAt(1), { key: "End" });
|
||||
expect(screen.getByRole("tab", { selected: true })).toHaveTextContent("gamma");
|
||||
|
||||
fireEvent.keyDown(tabAt(2), { key: "Home" });
|
||||
expect(screen.getByRole("tab", { selected: true })).toHaveTextContent("alpha");
|
||||
|
||||
// Wrap backwards from the first tab to the last.
|
||||
fireEvent.keyDown(tabAt(0), { key: "ArrowLeft" });
|
||||
expect(screen.getByRole("tab", { selected: true })).toHaveTextContent("gamma");
|
||||
});
|
||||
|
||||
it("keeps the rendered panel in step with the selected tab", () => {
|
||||
render(<Harness />);
|
||||
fireEvent.click(screen.getByRole("tab", { name: "beta" }));
|
||||
const panel = screen.getByRole("tabpanel");
|
||||
expect(panel).toHaveTextContent("content for beta");
|
||||
expect(panel.getAttribute("aria-labelledby")).toBe(screen.getByRole("tab", { selected: true }).getAttribute("id"));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
import type { KeyboardEvent, ReactNode } from "react";
|
||||
|
||||
/**
|
||||
* The console's single tab implementation. Every workspace that presents tabbed detail uses it, so
|
||||
* the full ARIA tabs pattern — labelled tablist, tab/panel id pairing in both directions, selected
|
||||
* state, roving tabindex and Left/Right/Home/End keyboard movement — is implemented once instead of
|
||||
* being re-derived, and partially, per workspace.
|
||||
*/
|
||||
export type TabDescriptor<T extends string> = { key: T; label: string };
|
||||
|
||||
export const tabId = (idPrefix: string, key: string) => `${idPrefix}-tab-${key}`;
|
||||
export const tabPanelId = (idPrefix: string, key: string) => `${idPrefix}-panel-${key}`;
|
||||
|
||||
export function TabList<T extends string>({ idPrefix, label, className, tabs, active, onSelect }: {
|
||||
idPrefix: string;
|
||||
label: string;
|
||||
className: string;
|
||||
tabs: ReadonlyArray<TabDescriptor<T>>;
|
||||
active: T;
|
||||
onSelect: (key: T) => void;
|
||||
}) {
|
||||
function onKeyDown(event: KeyboardEvent<HTMLButtonElement>, index: number) {
|
||||
if (!["ArrowLeft", "ArrowRight", "Home", "End"].includes(event.key)) return;
|
||||
event.preventDefault();
|
||||
const next = event.key === "Home" ? 0
|
||||
: event.key === "End" ? tabs.length - 1
|
||||
: (index + (event.key === "ArrowRight" ? 1 : -1) + tabs.length) % tabs.length;
|
||||
const target = event.currentTarget.parentElement?.querySelectorAll<HTMLButtonElement>('[role="tab"]')[next];
|
||||
onSelect(tabs[next].key);
|
||||
// Selection follows focus, so the panel and the focused tab never disagree.
|
||||
window.requestAnimationFrame(() => target?.focus());
|
||||
}
|
||||
|
||||
return <div className={className} role="tablist" aria-label={label}>
|
||||
{tabs.map((item, index) => <button
|
||||
key={item.key}
|
||||
role="tab"
|
||||
id={tabId(idPrefix, item.key)}
|
||||
aria-controls={tabPanelId(idPrefix, item.key)}
|
||||
aria-selected={active === item.key}
|
||||
tabIndex={active === item.key ? 0 : -1}
|
||||
className={active === item.key ? "active" : ""}
|
||||
onKeyDown={(event) => onKeyDown(event, index)}
|
||||
onClick={() => onSelect(item.key)}
|
||||
>{item.label}</button>)}
|
||||
</div>;
|
||||
}
|
||||
|
||||
export function TabPanel({ idPrefix, active, className, children }: {
|
||||
idPrefix: string;
|
||||
active: string;
|
||||
className?: string;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return <div
|
||||
role="tabpanel"
|
||||
id={tabPanelId(idPrefix, active)}
|
||||
aria-labelledby={tabId(idPrefix, active)}
|
||||
tabIndex={0}
|
||||
className={className}
|
||||
>{children}</div>;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
/**
|
||||
* The console's API origin is compiled in by Vite, not resolved at runtime. v1.2.0 shipped a
|
||||
* release image whose bundle pointed at http://localhost:8000 because the release build never
|
||||
* passed VITE_API_BASE_URL — every unit test still passed, because jsdom never performs a real
|
||||
* cross-origin fetch and nothing asserted where the requests were addressed.
|
||||
*
|
||||
* These tests assert the address itself, so a build configured for one origin cannot silently
|
||||
* issue requests to another.
|
||||
*/
|
||||
describe("console API origin", () => {
|
||||
afterEach(() => { vi.unstubAllGlobals(); vi.resetModules(); });
|
||||
|
||||
async function captureRequestUrl(): Promise<string> {
|
||||
const seen: string[] = [];
|
||||
vi.stubGlobal("fetch", vi.fn((input: RequestInfo | URL) => {
|
||||
seen.push(String(input));
|
||||
return Promise.resolve(new Response("[]", { status: 200 }));
|
||||
}));
|
||||
const { api } = await import("./api");
|
||||
await api.hardware();
|
||||
return seen[0];
|
||||
}
|
||||
|
||||
it("addresses requests at the configured base URL", async () => {
|
||||
const url = await captureRequestUrl();
|
||||
const configured = import.meta.env.VITE_API_BASE_URL ?? "http://localhost:8000";
|
||||
expect(url).toBe(`${configured}/api/v1/hardware`);
|
||||
});
|
||||
|
||||
it("keeps the API path contract stable regardless of origin", async () => {
|
||||
const url = await captureRequestUrl();
|
||||
// Whatever the origin, the path the CSP and the backend router agree on must not drift.
|
||||
expect(new URL(url).pathname).toBe("/api/v1/hardware");
|
||||
});
|
||||
|
||||
it("never addresses a hardcoded origin that ignores the build configuration", async () => {
|
||||
const url = await captureRequestUrl();
|
||||
const configured = import.meta.env.VITE_API_BASE_URL ?? "http://localhost:8000";
|
||||
// If the configured origin is not localhost, no request may still go there. That is exactly
|
||||
// the v1.2.0 production failure, expressed as an assertion.
|
||||
if (!configured.includes("localhost")) {
|
||||
expect(url).not.toContain("localhost");
|
||||
}
|
||||
expect(url.startsWith(configured)).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,135 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import {
|
||||
ApiError, api, clearOperatorCredential, hasOperatorCredential,
|
||||
onOperatorSessionInvalidated, setOperatorCredential,
|
||||
} from "./api";
|
||||
|
||||
afterEach(() => { clearOperatorCredential(); vi.unstubAllGlobals(); });
|
||||
|
||||
function respondWith(status: number, body: unknown, statusText = "Service Unavailable"): void {
|
||||
vi.stubGlobal("fetch", vi.fn(() => Promise.resolve(
|
||||
new Response(body === undefined ? "" : JSON.stringify(body), { status, statusText }),
|
||||
)));
|
||||
}
|
||||
|
||||
describe("error reporting", () => {
|
||||
// Read requests previously threw away the response body and reported only the HTTP status line,
|
||||
// which meant the one identifier TROUBLESHOOTING.md tells an operator to search the logs for was
|
||||
// missing from exactly the case they would be troubleshooting.
|
||||
it("surfaces the platform's message and correlation id on a failed read", async () => {
|
||||
respondWith(503, {
|
||||
error: {
|
||||
code: "OBSERVABILITY_DEGRADED",
|
||||
message: "monitoring persistence is unavailable",
|
||||
correlation_id: "e2f4c1a0",
|
||||
},
|
||||
});
|
||||
|
||||
const error = await api.hardware().catch((cause: unknown) => cause);
|
||||
|
||||
expect(error).toBeInstanceOf(ApiError);
|
||||
const apiError = error as ApiError;
|
||||
expect(apiError.message).toContain("monitoring persistence is unavailable");
|
||||
expect(apiError.message).toContain("e2f4c1a0");
|
||||
expect(apiError.code).toBe("OBSERVABILITY_DEGRADED");
|
||||
expect(apiError.correlationId).toBe("e2f4c1a0");
|
||||
expect(apiError.status).toBe(503);
|
||||
});
|
||||
|
||||
it("falls back to the status line when the body carries no envelope", async () => {
|
||||
respondWith(502, undefined, "Bad Gateway");
|
||||
|
||||
const error = await api.hardware().catch((cause: unknown) => cause) as ApiError;
|
||||
|
||||
expect(error).toBeInstanceOf(ApiError);
|
||||
expect(error.message).toBe("502 Bad Gateway");
|
||||
expect(error.correlationId).toBeUndefined();
|
||||
});
|
||||
|
||||
it("keeps structured details so a caller can explain why a delete was refused", async () => {
|
||||
respondWith(409, {
|
||||
error: {
|
||||
message: "resource has dependencies",
|
||||
details: { dependencies: [{ resource_type: "model_revision", relation: "revision" }] },
|
||||
},
|
||||
}, "Conflict");
|
||||
|
||||
const error = await api.deleteModel("a-model").catch((cause: unknown) => cause) as ApiError;
|
||||
|
||||
expect(error.status).toBe(409);
|
||||
expect(error.details).toHaveProperty("dependencies");
|
||||
});
|
||||
});
|
||||
|
||||
describe("empty responses", () => {
|
||||
// A 204 has no body and a 200 may have an empty one. `response.json()` rejects on both, which
|
||||
// turned a successful delete into a thrown SyntaxError.
|
||||
it("resolves a 204 without attempting to parse a body", async () => {
|
||||
vi.stubGlobal("fetch", vi.fn(() => Promise.resolve(new Response(null, { status: 204 }))));
|
||||
await expect(api.deleteModel("a-model")).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("resolves a 200 that carries an empty body", async () => {
|
||||
vi.stubGlobal("fetch", vi.fn(() => Promise.resolve(new Response("", { status: 200 }))));
|
||||
await expect(api.deleteRevision("a-revision")).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("rejects an empty successful read at the API boundary", async () => {
|
||||
vi.stubGlobal("fetch", vi.fn(() => Promise.resolve(new Response("", { status: 200 }))));
|
||||
|
||||
const error = await api.hardware().catch((cause: unknown) => cause) as ApiError;
|
||||
|
||||
expect(error).toBeInstanceOf(ApiError);
|
||||
expect(error.code).toBe("EMPTY_RESPONSE");
|
||||
expect(error.message).toBe("200 response body is empty");
|
||||
});
|
||||
});
|
||||
|
||||
describe("memory-only operator session", () => {
|
||||
it("adds the operator header only to control-plane requests", async () => {
|
||||
const requests: Array<{ url: string; headers: Headers }> = [];
|
||||
vi.stubGlobal("fetch", vi.fn((input: RequestInfo | URL, init?: RequestInit) => {
|
||||
requests.push({ url: String(input), headers: new Headers(init?.headers) });
|
||||
return Promise.resolve(new Response(JSON.stringify({}), { status: 200 }));
|
||||
}));
|
||||
setOperatorCredential("only-in-memory");
|
||||
|
||||
await api.hardware();
|
||||
await api.version();
|
||||
await api.liveness();
|
||||
await api.invokeEmbedding("hello", "capability-secret");
|
||||
|
||||
expect(requests[0].headers.get("X-ModelForge-Admin-Token")).toBe("only-in-memory");
|
||||
expect(requests[1].headers.has("X-ModelForge-Admin-Token")).toBe(false);
|
||||
expect(requests[2].headers.has("X-ModelForge-Admin-Token")).toBe(false);
|
||||
expect(requests[3].headers.has("X-ModelForge-Admin-Token")).toBe(false);
|
||||
expect(requests[3].headers.get("Authorization")).toBe("Bearer capability-secret");
|
||||
});
|
||||
|
||||
it.each([401, 503])("clears and reports a revoked session on %s", async (status) => {
|
||||
const invalidated = vi.fn();
|
||||
const unsubscribe = onOperatorSessionInvalidated(invalidated);
|
||||
setOperatorCredential("revoked-in-memory");
|
||||
respondWith(status, { error: { message: "session invalid" } });
|
||||
|
||||
await api.hardware().catch(() => undefined);
|
||||
|
||||
expect(hasOperatorCredential()).toBe(false);
|
||||
expect(invalidated).toHaveBeenCalledOnce();
|
||||
unsubscribe();
|
||||
});
|
||||
|
||||
it("keeps the session on a role-level 403 refusal", async () => {
|
||||
const invalidated = vi.fn();
|
||||
const unsubscribe = onOperatorSessionInvalidated(invalidated);
|
||||
setOperatorCredential("valid-but-insufficient-role");
|
||||
respondWith(403, { error: { message: "role denied" } }, "Forbidden");
|
||||
|
||||
await api.deleteModel("admin-only").catch(() => undefined);
|
||||
|
||||
expect(hasOperatorCredential()).toBe(true);
|
||||
expect(invalidated).not.toHaveBeenCalled();
|
||||
unsubscribe();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,336 @@
|
||||
import type { AdvisorRecommendation, ArtifactJob, ArtifactSet, CapabilityDeployment, CapabilityEstate, CompatibilityAssessment, DeploymentCandidate, DerivedArtifact, DiscoveryAssessment, DiscoveryCandidate, DownloadPlan, EmbeddingMigration, EnrollmentCreated, EnrollmentRequest, EvaluationCaseResult, EvaluationComparison, EvaluationRun, EvaluationSuite, ExecutionApproval, GatewayInvokeResponse, GatewayRequest, HardwareState, ModelArtifact, ModelComparison, ModelInstallationRationale, ModelRevision, ModelSummary, NodeDecommissionPreview, NodeDecommissionResult, OCRInvokeResponse, Page, PlacementPlan, ProductionApproval, ProjectIntegration, ProjectSummary, RerankingRun, RetrievalCandidatePool, RetrievalPipelineIdentity, RuntimeEnvironment, RuntimeProbe, RuntimeProfile, SchedulerBudget, SchedulerPolicy, ServiceClient, ServiceClientCreated, SpeechTranscriptionResponse, StorageRoot, SystemMetadata, UpstreamSnapshot, VisionEmbeddingResponse } from "../types";
|
||||
import type { CleanupPlan, LifecycleApproval, LifecycleEvent, LifecycleOperation, LifecyclePolicy, LifecycleSubject, PromotionPlan, RetentionPolicy } from "../types";
|
||||
import type { MigrationCutoverOperation, MigrationEvent, MigrationPlan, MigrationValidationSnapshot } from "../types";
|
||||
import type { CapacitySnapshot, OperationalAlert, OperationalIncident, OperationsOverview, SLOEvaluation } from "../types";
|
||||
import type { ArtifactRecovery, BackupCapacityEstimate, BackupSet, RecoveryAsset, RecoveryDashboard, RecoveryPolicy, RestoreOperation, RestoreOperationEvent, RestorePlan } from "../types";
|
||||
|
||||
const API_BASE = import.meta.env.VITE_API_BASE_URL ?? "http://localhost:8000";
|
||||
const OPERATOR_HEADER = "X-ModelForge-Admin-Token";
|
||||
const IN_MEMORY_SESSION_REFERENCE = "modelforge:in-memory-operator-session";
|
||||
|
||||
let inMemoryOperatorCredential: string | null = null;
|
||||
let sessionInvalidatedListener: (() => void) | null = null;
|
||||
|
||||
/** Set only after a successful protected request. This value is never persisted or put in a URL. */
|
||||
export function setOperatorCredential(credential: string): void {
|
||||
inMemoryOperatorCredential = credential;
|
||||
}
|
||||
|
||||
export function clearOperatorCredential(): void {
|
||||
inMemoryOperatorCredential = null;
|
||||
}
|
||||
|
||||
export function hasOperatorCredential(): boolean {
|
||||
return inMemoryOperatorCredential !== null;
|
||||
}
|
||||
|
||||
/** Opaque UI capability reference; never the credential itself. */
|
||||
export function operatorCredentialReference(): string {
|
||||
return inMemoryOperatorCredential ? IN_MEMORY_SESSION_REFERENCE : "";
|
||||
}
|
||||
|
||||
export function onOperatorSessionInvalidated(listener: () => void): () => void {
|
||||
sessionInvalidatedListener = listener;
|
||||
return () => { if (sessionInvalidatedListener === listener) sessionInvalidatedListener = null; };
|
||||
}
|
||||
|
||||
function isPublicCredentialPath(path: string): boolean {
|
||||
const pathname = path.split("?", 1)[0];
|
||||
return pathname === "/"
|
||||
|| pathname === "/api/v1/health/live"
|
||||
|| pathname === "/api/v1/health/ready"
|
||||
|| pathname === "/api/v1/version";
|
||||
}
|
||||
|
||||
function isMachineCredentialPath(path: string): boolean {
|
||||
const pathname = path.split("?", 1)[0];
|
||||
if (pathname === "/api/v1/agent/enroll" || pathname.startsWith("/api/v1/agent/")) return true;
|
||||
if (pathname === "/v1/embeddings") return true;
|
||||
return /^\/api\/v1\/(?:capabilities\/[^/]+|capability-experiments\/[^/]+)\/invoke$/.test(pathname);
|
||||
}
|
||||
|
||||
function headerRecord(headers?: HeadersInit): Record<string, string> {
|
||||
if (!headers) return {};
|
||||
if (headers instanceof Headers) return Object.fromEntries(headers.entries());
|
||||
if (Array.isArray(headers)) return Object.fromEntries(headers);
|
||||
return { ...headers };
|
||||
}
|
||||
|
||||
function requestHeaders(path: string, headers?: HeadersInit): Record<string, string> {
|
||||
const result = headerRecord(headers);
|
||||
const hasExplicitOperatorHeader = Object.keys(result).some(
|
||||
(name) => name.toLowerCase() === OPERATOR_HEADER.toLowerCase(),
|
||||
);
|
||||
if (
|
||||
!isPublicCredentialPath(path)
|
||||
&& !isMachineCredentialPath(path)
|
||||
&& inMemoryOperatorCredential
|
||||
&& !hasExplicitOperatorHeader
|
||||
) {
|
||||
result[OPERATOR_HEADER] = inMemoryOperatorCredential;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function handleSessionInvalidation(path: string, status: number): void {
|
||||
if (
|
||||
inMemoryOperatorCredential
|
||||
&& !isPublicCredentialPath(path)
|
||||
&& !isMachineCredentialPath(path)
|
||||
&& (status === 401 || status === 503)
|
||||
) {
|
||||
clearOperatorCredential();
|
||||
sessionInvalidatedListener?.();
|
||||
}
|
||||
}
|
||||
|
||||
export interface PublicHealth {
|
||||
status: string;
|
||||
version: string;
|
||||
}
|
||||
|
||||
export interface PublicRelease {
|
||||
name: string;
|
||||
version: string;
|
||||
channel?: string;
|
||||
build?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export class ApiError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public status: number,
|
||||
public details: Record<string, unknown> = {},
|
||||
public code?: string,
|
||||
public correlationId?: string,
|
||||
) { super(message); }
|
||||
}
|
||||
|
||||
/** The shape every ModelForge error response uses: `{error: {code, message, correlation_id, details}}`. */
|
||||
type ErrorEnvelope = {
|
||||
detail?: string;
|
||||
error?: { code?: string; message?: string; correlation_id?: string; details?: Record<string, unknown> };
|
||||
};
|
||||
|
||||
/**
|
||||
* One error path for every request helper.
|
||||
*
|
||||
* Read requests used to throw `new Error("503 Service Unavailable")` and discard the body, so the
|
||||
* console showed an HTTP status where the platform had sent a reason. That mattered more than it
|
||||
* looks: TROUBLESHOOTING.md tells the operator to search the logs for the `correlation_id` carried
|
||||
* on every error response, and on a failed GET the console was the one place that id never
|
||||
* appeared. The status line is now the fallback, not the message.
|
||||
*/
|
||||
async function failure(response: Response): Promise<ApiError> {
|
||||
const payload = await response.json().catch(() => null) as ErrorEnvelope | null;
|
||||
const message = payload?.error?.message ?? payload?.detail ?? `${response.status} ${response.statusText}`;
|
||||
const correlationId = payload?.error?.correlation_id;
|
||||
return new ApiError(
|
||||
correlationId ? `${message} (correlation ${correlationId})` : message,
|
||||
response.status,
|
||||
payload?.error?.details ?? {},
|
||||
payload?.error?.code,
|
||||
correlationId,
|
||||
);
|
||||
}
|
||||
|
||||
/** 204 carries no body, and neither does a 200 with an empty one; `.json()` would throw on both. */
|
||||
async function decode<T>(response: Response, allowEmpty = false): Promise<T> {
|
||||
if (response.status === 204 || response.status === 205) return undefined as T;
|
||||
const body = await response.text();
|
||||
if (!body) {
|
||||
if (allowEmpty) return undefined as T;
|
||||
throw new ApiError(
|
||||
`${response.status} response body is empty`,
|
||||
response.status,
|
||||
{},
|
||||
"EMPTY_RESPONSE",
|
||||
);
|
||||
}
|
||||
return JSON.parse(body) as T;
|
||||
}
|
||||
|
||||
async function getJson<T>(path: string): Promise<T> {
|
||||
const response = await fetch(`${API_BASE}${path}`, { headers: requestHeaders(path) });
|
||||
handleSessionInvalidation(path, response.status);
|
||||
if (!response.ok) throw await failure(response);
|
||||
return decode<T>(response, false);
|
||||
}
|
||||
|
||||
async function requestJson<T>(path: string, init: RequestInit, allowEmpty = false): Promise<T> {
|
||||
const response = await fetch(`${API_BASE}${path}`, {
|
||||
...init,
|
||||
headers: requestHeaders(path, { "Content-Type": "application/json", ...headerRecord(init.headers) }),
|
||||
});
|
||||
handleSessionInvalidation(path, response.status);
|
||||
if (!response.ok) throw await failure(response);
|
||||
return decode<T>(response, allowEmpty);
|
||||
}
|
||||
|
||||
async function postJson<T>(path: string): Promise<T> {
|
||||
const response = await fetch(`${API_BASE}${path}`, { method: "POST", headers: requestHeaders(path) });
|
||||
handleSessionInvalidation(path, response.status);
|
||||
if (!response.ok) throw await failure(response);
|
||||
return decode<T>(response, false);
|
||||
}
|
||||
|
||||
async function postAdminJson<T>(path: string, body: unknown, adminToken: string): Promise<T> {
|
||||
return requestJson<T>(path, {
|
||||
method: "POST",
|
||||
headers: adminToken && adminToken !== IN_MEMORY_SESSION_REFERENCE
|
||||
? { [OPERATOR_HEADER]: adminToken }
|
||||
: {},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
}
|
||||
|
||||
async function adminRequestJson<T>(path: string, adminToken: string, init: RequestInit = {}): Promise<T> {
|
||||
return requestJson<T>(path, {
|
||||
...init,
|
||||
headers: {
|
||||
...(adminToken && adminToken !== IN_MEMORY_SESSION_REFERENCE
|
||||
? { [OPERATOR_HEADER]: adminToken }
|
||||
: {}),
|
||||
...headerRecord(init.headers),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export const api = {
|
||||
liveness: () => getJson<PublicHealth>("/api/v1/health/live"),
|
||||
version: () => getJson<PublicRelease>("/api/v1/version"),
|
||||
verifyOperatorCredential: (credential: string) => adminRequestJson<SystemMetadata>(
|
||||
"/api/v1/system",
|
||||
credential,
|
||||
),
|
||||
system: () => getJson<SystemMetadata>("/api/v1/system"),
|
||||
models: (query = "page=1&page_size=20") => getJson<Page<ModelSummary>>(`/api/v1/models?${query}`),
|
||||
model: (id: string) => getJson<ModelSummary>(`/api/v1/models/${id}`),
|
||||
createModel: (body: unknown) => requestJson<ModelSummary>("/api/v1/models", { method: "POST", body: JSON.stringify(body) }),
|
||||
updateModel: (id: string, body: unknown) => requestJson<ModelSummary>(`/api/v1/models/${id}`, { method: "PATCH", body: JSON.stringify(body) }),
|
||||
deprecateModel: (id: string) => requestJson<ModelSummary>(`/api/v1/models/${id}/deprecate`, { method: "POST" }),
|
||||
archiveModel: (id: string) => requestJson<ModelSummary>(`/api/v1/models/${id}/archive`, { method: "POST" }),
|
||||
deleteModel: (id: string) => requestJson<void>(`/api/v1/models/${id}`, { method: "DELETE" }, true),
|
||||
revisions: (modelId: string) => getJson<Page<ModelRevision>>(`/api/v1/models/${modelId}/revisions?page=1&page_size=100`),
|
||||
createRevision: (modelId: string, body: unknown) => requestJson<ModelRevision>(`/api/v1/models/${modelId}/revisions`, { method: "POST", body: JSON.stringify(body) }),
|
||||
deleteRevision: (id: string) => requestJson<void>(`/api/v1/revisions/${id}`, { method: "DELETE" }, true),
|
||||
artifacts: (revisionId: string) => getJson<Page<ModelArtifact>>(`/api/v1/revisions/${revisionId}/artifacts?page=1&page_size=100`),
|
||||
createArtifact: (revisionId: string, body: unknown) => requestJson<ModelArtifact>(`/api/v1/revisions/${revisionId}/artifacts`, { method: "POST", body: JSON.stringify(body) }),
|
||||
deleteArtifact: (id: string) => requestJson<void>(`/api/v1/artifacts/${id}`, { method: "DELETE" }, true),
|
||||
derivedArtifacts: (revisionId: string) => getJson<Page<DerivedArtifact>>(`/api/v1/revisions/${revisionId}/derived-artifacts?page=1&page_size=100`),
|
||||
discoverySearch: (body: unknown) => requestJson<DiscoveryCandidate[]>("/api/v1/discovery/search", { method: "POST", body: JSON.stringify(body) }),
|
||||
refreshUpstream: (modelId: string, revision = "main") => requestJson<UpstreamSnapshot>(`/api/v1/models/${modelId}/refresh-upstream`, { method: "POST", body: JSON.stringify({ revision }) }),
|
||||
upstream: (modelId: string) => getJson<UpstreamSnapshot>(`/api/v1/models/${modelId}/upstream`),
|
||||
artifactSets: (revisionId: string) => getJson<ArtifactSet[]>(`/api/v1/revisions/${revisionId}/artifact-sets`),
|
||||
storageRoots: () => getJson<StorageRoot[]>("/api/v1/storage-roots"),
|
||||
createDownloadPlan: (body: unknown) => requestJson<DownloadPlan>("/api/v1/download-plans", { method: "POST", body: JSON.stringify(body) }),
|
||||
approveDownloadPlan: (id: string) => requestJson<DownloadPlan>(`/api/v1/download-plans/${id}/approve`, { method: "POST" }),
|
||||
executeDownloadPlan: (id: string) => requestJson<ArtifactJob>(`/api/v1/download-plans/${id}/execute`, { method: "POST" }),
|
||||
artifactJobs: () => getJson<ArtifactJob[]>("/api/v1/artifact-jobs"),
|
||||
cancelArtifactJob: (id: string) => requestJson<ArtifactJob>(`/api/v1/artifact-jobs/${id}/cancel`, { method: "POST" }),
|
||||
retryArtifactJob: (id: string) => requestJson<ArtifactJob>(`/api/v1/artifact-jobs/${id}/retry`, { method: "POST" }),
|
||||
projects: () => getJson<ProjectSummary[]>("/api/v1/projects"),
|
||||
projectIntegrations: () => getJson<ProjectIntegration[]>("/api/v1/project-integrations"),
|
||||
hardware: () => getJson<HardwareState>("/api/v1/hardware"),
|
||||
refreshHardware: () => postJson<HardwareState>("/api/v1/hardware/refresh"),
|
||||
createEnrollment: (request: EnrollmentRequest, adminToken: string) =>
|
||||
postAdminJson<EnrollmentCreated>("/api/v1/admin/node-enrollments", request, adminToken),
|
||||
previewNodeDecommission: (nodeId: string, adminToken: string) =>
|
||||
adminRequestJson<NodeDecommissionPreview>(`/api/v1/admin/hardware/nodes/${nodeId}/decommission/preview`, adminToken, { method: "POST" }),
|
||||
decommissionNode: (nodeId: string, body: unknown, adminToken: string) =>
|
||||
adminRequestJson<NodeDecommissionResult>(`/api/v1/admin/hardware/nodes/${nodeId}/decommission`, adminToken, { method: "POST", body: JSON.stringify(body) }),
|
||||
runtimeEnvironments: () => getJson<RuntimeEnvironment[]>("/api/v1/runtime-environments"),
|
||||
runtimeProfiles: () => getJson<RuntimeProfile[]>("/api/v1/runtime-profiles"),
|
||||
compatibilityAssessments: () => getJson<CompatibilityAssessment[]>("/api/v1/compatibility-assessments"),
|
||||
assessCompatibility: (artifactSetId: string, runtimeProfileId: string, computeNodeId: string) =>
|
||||
requestJson<CompatibilityAssessment>(`/api/v1/artifact-sets/${artifactSetId}/compatibility-assessments`, { method: "POST", body: JSON.stringify({ runtime_profile_id: runtimeProfileId, compute_node_id: computeNodeId }) }),
|
||||
executionApprovals: () => getJson<ExecutionApproval[]>("/api/v1/execution-approvals"),
|
||||
approveLabExecution: (artifactSetId: string, approvedBy: string, reason: string) =>
|
||||
requestJson<ExecutionApproval>(`/api/v1/artifact-sets/${artifactSetId}/execution-approvals`, { method: "POST", body: JSON.stringify({ scope: "lab_execution", approved_by: approvedBy, reason }) }),
|
||||
runtimeProbes: () => getJson<RuntimeProbe[]>("/api/v1/runtime-probes"),
|
||||
startRuntimeProbe: (compatibilityAssessmentId: string, executionApprovalId: string) =>
|
||||
requestJson<RuntimeProbe>("/api/v1/runtime-probes", { method: "POST", body: JSON.stringify({ compatibility_assessment_id: compatibilityAssessmentId, execution_approval_id: executionApprovalId, input_text: "ModelForge runtime compatibility probe" }) }),
|
||||
cancelRuntimeProbe: (probeId: string) => requestJson<RuntimeProbe>(`/api/v1/runtime-probes/${probeId}/cancel`, { method: "POST" }),
|
||||
deploymentCandidates: () => getJson<DeploymentCandidate[]>("/api/v1/deployment-candidates"),
|
||||
capabilityDeployments: () => getJson<CapabilityDeployment[]>("/api/v1/capability-deployments"),
|
||||
capabilityEstate: () => getJson<CapabilityEstate[]>("/api/v1/capability-estate"),
|
||||
installationRationale: () => getJson<ModelInstallationRationale[]>("/api/v1/models/installation-rationale"),
|
||||
scheduler: () => getJson<SchedulerBudget[]>("/api/v1/scheduler"),
|
||||
schedulerPolicy: (adminToken: string) => adminRequestJson<SchedulerPolicy>("/api/v1/admin/scheduler/policy", adminToken),
|
||||
updateSchedulerPolicy: (labPaused: boolean, adminToken: string) => adminRequestJson<SchedulerPolicy>("/api/v1/admin/scheduler/policy", adminToken, { method: "PUT", body: JSON.stringify({ lab_paused: labPaused }) }),
|
||||
placementHistory: (adminToken: string) => adminRequestJson<PlacementPlan[]>("/api/v1/admin/scheduler/placements?limit=100", adminToken),
|
||||
dryRunPlacement: (deploymentId: string, priority: string, adminToken: string) => adminRequestJson<PlacementPlan>(`/api/v1/admin/scheduler/placements/${deploymentId}/dry-run`, adminToken, { method: "POST", body: JSON.stringify({ priority }) }),
|
||||
updateResidencyPolicy: (deploymentId: string, residencyPolicy: string, adminToken: string) => adminRequestJson<CapabilityDeployment>(`/api/v1/admin/capability-deployments/${deploymentId}/residency-policy`, adminToken, { method: "PUT", body: JSON.stringify({ residency_policy: residencyPolicy, keep_warm_seconds: 900 }) }),
|
||||
gatewayRequests: (adminToken: string) => adminRequestJson<GatewayRequest[]>("/api/v1/gateway/requests?limit=100", adminToken),
|
||||
serviceClients: (adminToken: string) => adminRequestJson<ServiceClient[]>("/api/v1/admin/service-clients", adminToken),
|
||||
productionApprovals: (adminToken: string) => adminRequestJson<ProductionApproval[]>("/api/v1/admin/production-approvals", adminToken),
|
||||
createServiceClient: (body: unknown, adminToken: string) => adminRequestJson<ServiceClientCreated>("/api/v1/admin/service-clients", adminToken, { method: "POST", body: JSON.stringify(body) }),
|
||||
revokeServiceCredential: (clientId: string, adminToken: string) => adminRequestJson<ServiceClient>(`/api/v1/admin/service-clients/${clientId}/credential`, adminToken, { method: "DELETE" }),
|
||||
approveProduction: (candidateId: string, body: unknown, adminToken: string) => adminRequestJson<ProductionApproval>(`/api/v1/admin/deployment-candidates/${candidateId}/production-approvals`, adminToken, { method: "POST", body: JSON.stringify(body) }),
|
||||
promoteCandidate: (candidateId: string, body: unknown, adminToken: string) => adminRequestJson<CapabilityDeployment>(`/api/v1/admin/deployment-candidates/${candidateId}/promote`, adminToken, { method: "POST", body: JSON.stringify(body) }),
|
||||
unloadDeployment: (deploymentId: string, adminToken: string) => adminRequestJson<CapabilityDeployment>(`/api/v1/admin/capability-deployments/${deploymentId}/unload`, adminToken, { method: "POST" }),
|
||||
invokeEmbedding: (input: string, credential: string) => requestJson<GatewayInvokeResponse>("/api/v1/capabilities/rag.embedding@1/invoke", { method: "POST", headers: { Authorization: `Bearer ${credential}` }, body: JSON.stringify({ input }) }),
|
||||
invokeOcr: (contentBase64: string, mediaType: "image/png" | "image/jpeg", credential: string) => requestJson<OCRInvokeResponse>("/api/v1/capabilities/document.ocr@1/invoke", { method: "POST", headers: { Authorization: `Bearer ${credential}` }, body: JSON.stringify({ content_base64: contentBase64, media_type: mediaType, language_hint: "auto" }) }),
|
||||
invokeVisionEmbedding: (imageBase64: string, mediaType: "image/png" | "image/jpeg", credential: string) => requestJson<VisionEmbeddingResponse>("/api/v1/capabilities/vision.embedding@1/invoke", { method: "POST", headers: { Authorization: `Bearer ${credential}` }, body: JSON.stringify({ items: [{ image_base64: imageBase64, media_type: mediaType }] }) }),
|
||||
invokeTranscription: (audioBase64: string, credential: string) => requestJson<SpeechTranscriptionResponse>("/api/v1/capabilities/speech.transcription@1/invoke", { method: "POST", headers: { Authorization: `Bearer ${credential}` }, body: JSON.stringify({ audio_base64: audioBase64, media_type: "audio/wav", language: "auto" }) }),
|
||||
evaluationSuites: () => getJson<EvaluationSuite[]>("/api/v1/evaluation-suites"),
|
||||
evaluationRuns: () => getJson<EvaluationRun[]>("/api/v1/evaluation-runs"),
|
||||
evaluationCases: (runId: string) => getJson<EvaluationCaseResult[]>(`/api/v1/evaluation-runs/${runId}/cases`),
|
||||
evaluationComparison: (comparisonId: string) => getJson<EvaluationComparison>(`/api/v1/evaluation-comparisons/${comparisonId}`),
|
||||
evaluationComparisons: () => getJson<EvaluationComparison[]>("/api/v1/evaluation-comparisons"),
|
||||
retrievalCandidatePools: () => getJson<RetrievalCandidatePool[]>("/api/v1/retrieval-candidate-pools"),
|
||||
retrievalPipelineIdentities: () => getJson<RetrievalPipelineIdentity[]>("/api/v1/retrieval-pipeline-identities"),
|
||||
rerankingRuns: () => getJson<RerankingRun[]>("/api/v1/reranking-runs"),
|
||||
discoveryAssessments: () => getJson<DiscoveryAssessment[]>("/api/v1/discovery-assessments"),
|
||||
modelComparisons: () => getJson<ModelComparison[]>("/api/v1/model-comparisons"),
|
||||
recommendations: () => getJson<AdvisorRecommendation[]>("/api/v1/recommendations"),
|
||||
embeddingMigrations: () => getJson<EmbeddingMigration[]>("/api/v1/embedding-migrations"),
|
||||
lifecyclePolicies: (adminToken: string) => adminRequestJson<LifecyclePolicy[]>("/api/v1/admin/lifecycle/approval-policies", adminToken),
|
||||
lifecycleSubjects: (adminToken: string) => adminRequestJson<LifecycleSubject[]>("/api/v1/admin/lifecycle/subjects", adminToken),
|
||||
lifecycleApprovals: (adminToken: string) => adminRequestJson<LifecycleApproval[]>("/api/v1/admin/lifecycle/approval-requests", adminToken),
|
||||
createLifecycleApproval: (body: unknown, adminToken: string) => adminRequestJson<LifecycleApproval>("/api/v1/admin/lifecycle/approval-requests", adminToken, { method: "POST", body: JSON.stringify(body) }),
|
||||
decideLifecycleApproval: (id: string, action: "approve" | "reject" | "revoke", body: unknown, adminToken: string) => adminRequestJson<LifecycleApproval>(`/api/v1/admin/lifecycle/approval-requests/${id}/${action}`, adminToken, { method: "POST", body: JSON.stringify(body) }),
|
||||
lifecyclePlans: (adminToken: string) => adminRequestJson<PromotionPlan[]>("/api/v1/admin/lifecycle/promotion-plans", adminToken),
|
||||
createLifecyclePlan: (body: unknown, adminToken: string) => adminRequestJson<PromotionPlan>("/api/v1/admin/lifecycle/promotion-plans", adminToken, { method: "POST", body: JSON.stringify(body) }),
|
||||
approveLifecyclePlan: (id: string, body: unknown, adminToken: string) => adminRequestJson<PromotionPlan>(`/api/v1/admin/lifecycle/promotion-plans/${id}/approve`, adminToken, { method: "POST", body: JSON.stringify(body) }),
|
||||
executeLifecyclePlan: (id: string, body: unknown, adminToken: string) => adminRequestJson<LifecycleOperation>(`/api/v1/admin/lifecycle/promotion-plans/${id}/execute`, adminToken, { method: "POST", body: JSON.stringify(body) }),
|
||||
lifecycleOperations: (adminToken: string) => adminRequestJson<LifecycleOperation[]>("/api/v1/admin/lifecycle/operations", adminToken),
|
||||
observeLifecycleCanary: (id: string, body: unknown, adminToken: string) => adminRequestJson<LifecycleOperation>(`/api/v1/admin/lifecycle/operations/${id}/canary`, adminToken, { method: "POST", body: JSON.stringify(body) }),
|
||||
commitLifecycleOperation: (id: string, body: unknown, adminToken: string) => adminRequestJson<LifecycleOperation>(`/api/v1/admin/lifecycle/operations/${id}/commit`, adminToken, { method: "POST", body: JSON.stringify(body) }),
|
||||
rollbackLifecycleOperation: (id: string, body: unknown, adminToken: string) => adminRequestJson<LifecycleOperation>(`/api/v1/admin/lifecycle/operations/${id}/rollback`, adminToken, { method: "POST", body: JSON.stringify(body) }),
|
||||
lifecycleRetentionPolicies: (adminToken: string) => adminRequestJson<RetentionPolicy[]>("/api/v1/admin/lifecycle/retention-policies", adminToken),
|
||||
lifecycleCleanupPlans: (adminToken: string) => adminRequestJson<CleanupPlan[]>("/api/v1/admin/lifecycle/cleanup-plans", adminToken),
|
||||
createLifecycleCleanupPlan: (body: unknown, adminToken: string) => adminRequestJson<CleanupPlan>("/api/v1/admin/lifecycle/cleanup-plans", adminToken, { method: "POST", body: JSON.stringify(body) }),
|
||||
executeLifecycleCleanupPlan: (id: string, body: unknown, adminToken: string) => adminRequestJson<CleanupPlan>(`/api/v1/admin/lifecycle/cleanup-plans/${id}/execute`, adminToken, { method: "POST", body: JSON.stringify(body) }),
|
||||
lifecycleEvents: (adminToken: string) => adminRequestJson<LifecycleEvent[]>("/api/v1/admin/lifecycle/events?limit=500", adminToken),
|
||||
migrationPlans: (adminToken: string) => adminRequestJson<MigrationPlan[]>("/api/v1/admin/migrations/plans", adminToken),
|
||||
createMigrationPlan: (body: unknown, adminToken: string) => adminRequestJson<MigrationPlan>("/api/v1/admin/migrations/plans", adminToken, { method: "POST", body: JSON.stringify(body) }),
|
||||
migrationValidations: (planId: string, adminToken: string) => adminRequestJson<MigrationValidationSnapshot[]>(`/api/v1/admin/migrations/plans/${planId}/validation`, adminToken),
|
||||
migrationCutovers: (adminToken: string) => adminRequestJson<MigrationCutoverOperation[]>("/api/v1/admin/migrations/cutovers", adminToken),
|
||||
migrationEvents: (adminToken: string) => adminRequestJson<MigrationEvent[]>("/api/v1/admin/migrations/events?limit=500", adminToken),
|
||||
startMigrationBackfill: (planId: string, body: unknown, adminToken: string) => adminRequestJson<MigrationPlan>(`/api/v1/admin/migrations/plans/${planId}/backfill/start`, adminToken, { method: "POST", body: JSON.stringify(body) }),
|
||||
pauseMigrationBackfill: (planId: string, body: unknown, adminToken: string) => adminRequestJson<MigrationPlan>(`/api/v1/admin/migrations/plans/${planId}/backfill/pause`, adminToken, { method: "POST", body: JSON.stringify(body) }),
|
||||
cancelMigration: (planId: string, body: unknown, adminToken: string) => adminRequestJson<MigrationPlan>(`/api/v1/admin/migrations/plans/${planId}/cancel`, adminToken, { method: "POST", body: JSON.stringify(body) }),
|
||||
operationsOverview: (adminToken: string) => adminRequestJson<OperationsOverview>("/api/v1/admin/operations/overview", adminToken),
|
||||
sloEvaluations: (adminToken: string) => adminRequestJson<SLOEvaluation[]>("/api/v1/admin/operations/slo-evaluations?limit=200", adminToken),
|
||||
evaluateSlos: (adminToken: string) => adminRequestJson<SLOEvaluation[]>("/api/v1/admin/operations/slo-evaluations/run", adminToken, { method: "POST" }),
|
||||
operationalAlerts: (adminToken: string) => adminRequestJson<OperationalAlert[]>("/api/v1/admin/operations/alerts?limit=500", adminToken),
|
||||
evaluateOperationalAlerts: (adminToken: string) => adminRequestJson<OperationalAlert[]>("/api/v1/admin/operations/alerts/evaluate", adminToken, { method: "POST" }),
|
||||
acknowledgeOperationalAlert: (id: string, reason: string, adminToken: string) => adminRequestJson<OperationalAlert>(`/api/v1/admin/operations/alerts/${id}/acknowledge`, adminToken, { method: "POST", body: JSON.stringify({ actor: "operator-ui", reason }) }),
|
||||
capacitySnapshots: (adminToken: string) => adminRequestJson<CapacitySnapshot[]>("/api/v1/admin/operations/capacity?limit=500", adminToken),
|
||||
collectCapacity: (adminToken: string) => adminRequestJson<CapacitySnapshot[]>("/api/v1/admin/operations/capacity/collect", adminToken, { method: "POST" }),
|
||||
operationalIncidents: (adminToken: string) => adminRequestJson<OperationalIncident[]>("/api/v1/admin/operations/incidents", adminToken),
|
||||
recoveryDashboard: (adminToken: string) => adminRequestJson<RecoveryDashboard>("/api/v1/admin/recovery/dashboard", adminToken),
|
||||
recoveryPolicies: (adminToken: string) => adminRequestJson<RecoveryPolicy[]>("/api/v1/admin/recovery/policies", adminToken),
|
||||
recoveryAssets: (adminToken: string) => adminRequestJson<RecoveryAsset[]>("/api/v1/admin/recovery/assets", adminToken),
|
||||
recoveryCapacity: (adminToken: string) => adminRequestJson<BackupCapacityEstimate>("/api/v1/admin/recovery/capacity", adminToken),
|
||||
backupSets: (adminToken: string) => adminRequestJson<BackupSet[]>("/api/v1/admin/recovery/backups?limit=200", adminToken),
|
||||
createBackupSet: (body: unknown, adminToken: string) => adminRequestJson<BackupSet>("/api/v1/admin/recovery/backups", adminToken, { method: "POST", body: JSON.stringify(body) }),
|
||||
verifyBackupSet: (id: string, adminToken: string) => adminRequestJson<BackupSet>(`/api/v1/admin/recovery/backups/${id}/verify`, adminToken, { method: "POST" }),
|
||||
restorePlans: (adminToken: string) => adminRequestJson<RestorePlan[]>("/api/v1/admin/recovery/restore-plans?limit=200", adminToken),
|
||||
restorePlanPreflight: (id: string, adminToken: string) => adminRequestJson<RestorePlan>(`/api/v1/admin/recovery/restore-plans/${id}/preflight`, adminToken, { method: "POST" }),
|
||||
restoreOperations: (adminToken: string) => adminRequestJson<RestoreOperation[]>("/api/v1/admin/recovery/restore-operations?limit=200", adminToken),
|
||||
restoreOperationEvents: (id: string, adminToken: string) => adminRequestJson<RestoreOperationEvent[]>(`/api/v1/admin/recovery/restore-operations/${id}/events`, adminToken),
|
||||
artifactRecoveries: (adminToken: string) => adminRequestJson<ArtifactRecovery[]>("/api/v1/admin/recovery/artifact-recoveries?limit=200", adminToken),
|
||||
applyRecoveryRetention: (adminToken: string) => adminRequestJson<Record<string, number>>("/api/v1/admin/recovery/retention/run", adminToken, { method: "POST" }),
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
|
||||
import App from "./App";
|
||||
import "./styles.css";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
);
|
||||
@@ -0,0 +1,82 @@
|
||||
import {
|
||||
Activity, Boxes, Cpu, DatabaseBackup, Gauge, GitBranch, GitCompare,
|
||||
Lightbulb, Network, Search, ShieldCheck, Siren,
|
||||
} from "lucide-react";
|
||||
|
||||
export type RouteKey =
|
||||
| "overview" | "models" | "projects" | "nodes" | "discover" | "runtime" | "capabilities"
|
||||
| "evaluation" | "recommendations" | "lifecycle" | "migrations" | "operations" | "recovery";
|
||||
|
||||
export type NavGroup = "Command" | "Model supply" | "Serving" | "Infrastructure" | "Assurance" | "Projects";
|
||||
|
||||
export type NavItem = {
|
||||
key: RouteKey;
|
||||
group: NavGroup;
|
||||
label: string;
|
||||
shortDescription: string;
|
||||
description: string;
|
||||
keywords: string[];
|
||||
icon: typeof Gauge;
|
||||
implemented: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Canonical console information architecture. Routes keep their stable hashes while the
|
||||
* presentation follows the operator's mental model: supply, serve, operate, assure, integrate.
|
||||
*/
|
||||
export const navigation: NavItem[] = [
|
||||
{ key: "overview", group: "Command", label: "Dashboard", icon: Gauge, implemented: true,
|
||||
shortDescription: "Platform command center",
|
||||
description: "Platform health, production posture, capacity pressure and the next actions that need an operator.",
|
||||
keywords: ["overview", "health", "gpu_node", "alerts", "capacity"] },
|
||||
{ key: "discover", group: "Model supply", label: "Discover", icon: Search, implemented: true,
|
||||
shortDescription: "Find and assess upstream models",
|
||||
description: "Search upstream sources and plan an acquisition. Nothing is downloaded without an explicit plan.",
|
||||
keywords: ["search", "hugging face", "acquisition", "download"] },
|
||||
{ key: "models", group: "Model supply", label: "Registry", icon: Boxes, implemented: true,
|
||||
shortDescription: "Governed models and artifacts",
|
||||
description: "Upstream facts, immutable revisions, local artifacts and governance evidence in one traceable registry.",
|
||||
keywords: ["models", "artifacts", "storage", "provenance", "revisions"] },
|
||||
{ key: "capabilities", group: "Serving", label: "Capabilities", icon: Network, implemented: true,
|
||||
shortDescription: "Contracts, deployments and routing",
|
||||
description: "Capability contracts, deployments, scheduler admission and clients. Consumers never bind to model paths.",
|
||||
keywords: ["deployments", "gateway", "contracts", "scheduler", "clients"] },
|
||||
{ key: "runtime", group: "Serving", label: "Runtime", icon: Activity, implemented: true,
|
||||
shortDescription: "Compatibility and execution profiles",
|
||||
description: "Runtime profiles and compatibility probes: what can execute safely on which compute node.",
|
||||
keywords: ["profiles", "compatibility", "probes", "workers"] },
|
||||
{ key: "nodes", group: "Infrastructure", label: "Nodes", icon: Cpu, implemented: true,
|
||||
shortDescription: "Compute, GPU and live telemetry",
|
||||
description: "Enrolled compute nodes, accelerators, schedulable roles and observed liveness.",
|
||||
keywords: ["compute", "gpu", "gpu_node", "vram", "telemetry"] },
|
||||
{ key: "operations", group: "Infrastructure", label: "Operations", icon: Siren, implemented: true,
|
||||
shortDescription: "SLOs, alerts and capacity history",
|
||||
description: "SLOs, alerts, incidents and capacity history. Observability informs operators without becoming a safety dependency.",
|
||||
keywords: ["alerts", "incidents", "slo", "gpu", "capacity", "history", "audit"] },
|
||||
{ key: "evaluation", group: "Assurance", label: "Evaluation", icon: GitCompare, implemented: true,
|
||||
shortDescription: "Benchmark evidence and comparisons",
|
||||
description: "Evaluation suites, runs and comparisons that turn candidates into reproducible evidence.",
|
||||
keywords: ["benchmarks", "evidence", "comparison", "quality"] },
|
||||
{ key: "recommendations", group: "Assurance", label: "Advisor", icon: Lightbulb, implemented: true,
|
||||
shortDescription: "Evidence-backed recommendations",
|
||||
description: "Recommendations derived from recorded evidence, with rationale and uncertainty attached.",
|
||||
keywords: ["recommendations", "fit", "evidence", "advice"] },
|
||||
{ key: "lifecycle", group: "Assurance", label: "Lifecycle", icon: ShieldCheck, implemented: true,
|
||||
shortDescription: "Approval, promotion and rollback",
|
||||
description: "Approval, promotion, canary and rollback with explicit evidence and operator guardrails.",
|
||||
keywords: ["approval", "promotion", "canary", "rollback", "cleanup"] },
|
||||
{ key: "migrations", group: "Assurance", label: "Migrations", icon: GitCompare, implemented: true,
|
||||
shortDescription: "Safe embedding-space transitions",
|
||||
description: "Embedding-space migrations, backfills and alias cutovers, including work deliberately waiting for an operator.",
|
||||
keywords: ["reindex", "embedding", "backfill", "cutover"] },
|
||||
{ key: "recovery", group: "Assurance", label: "Recovery", icon: DatabaseBackup, implemented: true,
|
||||
shortDescription: "Backups, restore and readiness",
|
||||
description: "Backup coverage, restore rehearsals and rehydration with a measured recovery point.",
|
||||
keywords: ["backup", "restore", "rpo", "rto", "disaster recovery"] },
|
||||
{ key: "projects", group: "Projects", label: "Projects", icon: GitBranch, implemented: true,
|
||||
shortDescription: "Bindings, credentials and fit",
|
||||
description: "Application bindings, client credentials, observed usage and integration readiness by capability.",
|
||||
keywords: ["bindings", "credentials", "integration", "consumers", "clients"] },
|
||||
];
|
||||
|
||||
export const navigationGroups = [...new Set(navigation.map((item) => item.group))];
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
@@ -0,0 +1,645 @@
|
||||
export type Availability = "known" | "unknown" | "unsupported" | "unavailable" | "temporarily_failed";
|
||||
export type HardwareStatus = "unknown" | "active" | "missing" | "unavailable" | "degraded" | "pending" | "decommissioned";
|
||||
export type NodeLiveness = "online" | "stale" | "offline" | "disabled" | "decommissioned";
|
||||
export interface ObservedValue<T> { value: T | null; availability: Availability; reason?: string | null; }
|
||||
|
||||
export interface AcceleratorTelemetry {
|
||||
observed_at: string;
|
||||
used_vram_bytes: ObservedValue<number>; free_vram_bytes: ObservedValue<number>;
|
||||
gpu_utilization_percent: ObservedValue<number>; memory_utilization_percent: ObservedValue<number>;
|
||||
temperature_c: ObservedValue<number>; power_draw_w: ObservedValue<number>; power_limit_w: ObservedValue<number>;
|
||||
graphics_clock_mhz: ObservedValue<number>; memory_clock_mhz: ObservedValue<number>;
|
||||
fan_speed_percent: ObservedValue<number>; performance_state: ObservedValue<string>;
|
||||
}
|
||||
export interface AcceleratorState {
|
||||
id: string; node_id: string; status: HardwareStatus; status_reason?: string | null;
|
||||
device_index: number; device_uuid: string; pci_bus_id: ObservedValue<string>; name: string; vendor: string;
|
||||
architecture: ObservedValue<string>; compute_capability_major: ObservedValue<number>;
|
||||
compute_capability_minor: ObservedValue<number>; total_vram_bytes: ObservedValue<number>;
|
||||
driver_version: ObservedValue<string>; cuda_driver_version: ObservedValue<string>;
|
||||
mig_mode_current: ObservedValue<boolean>; first_seen_at: string; last_seen_at?: string | null;
|
||||
inventory_at?: string | null; telemetry?: AcceleratorTelemetry | null;
|
||||
}
|
||||
export interface StorageState { id: string; purpose: string; path: string; total_bytes: ObservedValue<number>; used_bytes: ObservedValue<number>; free_bytes: ObservedValue<number>; observed_at: string; }
|
||||
export interface NodeState {
|
||||
id: string; identity_key: string; identity_source: string; hostname: string; display_name: string;
|
||||
status: HardwareStatus; status_reason?: string | null; os_name?: string | null; os_version: ObservedValue<string>;
|
||||
architecture?: string | null; kernel_version: ObservedValue<string>; cpu_model: ObservedValue<string>;
|
||||
logical_cpu_count: ObservedValue<number>; physical_core_count: ObservedValue<number>;
|
||||
total_ram_bytes: ObservedValue<number>; available_ram_bytes: ObservedValue<number>; agent_version?: string | null;
|
||||
first_seen_at: string; last_seen_at?: string | null; inventory_at?: string | null; hardware_fingerprint?: string | null;
|
||||
enabled: boolean; liveness: NodeLiveness; agent_health: "healthy" | "incompatible" | "revoked" | "unknown";
|
||||
observation_source: "local_control_plane" | "remote_agent"; protocol_version?: number | null;
|
||||
supported_capabilities: string[]; agent_started_at?: string | null; last_heartbeat_at?: string | null;
|
||||
last_inventory_received_at?: string | null; last_telemetry_received_at?: string | null;
|
||||
inventory_age_seconds?: number | null; telemetry_age_seconds?: number | null;
|
||||
last_connection_error?: string | null;
|
||||
role?: string | null; labels: Record<string, string | boolean>; production_eligible: boolean;
|
||||
lab_eligible: boolean; benchmark_eligible: boolean; environment?: string | null;
|
||||
generation: number; decommissioned_at?: string | null; decommission_reason?: string | null;
|
||||
decommissioned_by?: string | null;
|
||||
storage: StorageState[]; accelerators: AcceleratorState[];
|
||||
}
|
||||
export interface HardwareState { overview: { status: HardwareStatus; inventory_state: HardwareStatus; node_count: number; accelerator_count: number; last_inventory_at?: string | null; reason?: string | null; }; nodes: NodeState[]; }
|
||||
|
||||
export interface EnrollmentRequest {
|
||||
expires_in_seconds: number; display_name?: string; role?: string; labels: Record<string, string | boolean>;
|
||||
production_eligible: boolean; lab_eligible: boolean; benchmark_eligible: boolean;
|
||||
}
|
||||
export interface EnrollmentCreated {
|
||||
id: string; enrollment_token: string; expires_at: string; setup_environment: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface NodeDecommissionBlocker {
|
||||
code: string; message: string; record_type: string; count: number; resource_ids: string[];
|
||||
}
|
||||
export interface NodeDecommissionRecordCount { record_type: string; count: number; action: string; }
|
||||
export interface NodeDecommissionPreview {
|
||||
node_id: string; persisted_identity: string; hostname: string; display_name: string;
|
||||
current_state: JsonObject; node_generation: number; dependency_digest: string; safe: boolean;
|
||||
blockers: NodeDecommissionBlocker[]; cleanup: NodeDecommissionRecordCount[];
|
||||
preserved: NodeDecommissionRecordCount[]; dependent_records: NodeDecommissionRecordCount[];
|
||||
}
|
||||
export interface NodeDecommissionResult {
|
||||
operation_id: string; node_id: string; persisted_identity: string; status: string;
|
||||
decommissioned_at: string; cleanup_summary: Record<string, number>; previous_state: JsonObject;
|
||||
credential_revocations: number; idempotent_replay: boolean;
|
||||
}
|
||||
|
||||
export type JsonObject = Record<string, unknown>;
|
||||
export type ModelLifecycle = "discovered" | "candidate" | "downloading" | "quarantined" | "verified" | "testing" | "approved" | "standby" | "active" | "deprecated" | "archived" | "rejected" | "incompatible" | "security_blocked" | "license_blocked";
|
||||
export type ArtifactStatus = "remote" | "local" | "verifying" | "verified" | "missing" | "corrupt" | "quarantined" | "archived" | "unreachable";
|
||||
export interface Page<T> { items: T[]; page: number; page_size: number; total: number; pages: number; }
|
||||
export interface ModelSummary {
|
||||
id: string; key: string; display_name: string; description?: string | null;
|
||||
source_type: "huggingface" | "local" | "custom"; upstream_provider: string; upstream_source: string;
|
||||
upstream_metadata: JsonObject; local_metadata: JsonObject; interpretation_metadata: JsonObject;
|
||||
family?: string | null; modalities: string[]; parameter_metadata: JsonObject; license_metadata: JsonObject;
|
||||
lifecycle: ModelLifecycle; deprecated_at?: string | null; revision_count: number; artifact_count: number;
|
||||
verification_status: "unknown" | "unverified" | "quarantined" | "verified" | "blocked";
|
||||
deployment_status: "not_deployed"; created_at: string; updated_at: string;
|
||||
}
|
||||
export interface ModelRevision { id: string; model_id: string; upstream_revision: string; resolved_commit_sha: string; metadata_snapshot: JsonObject; discovered_at: string; approved_at?: string | null; immutable_at: string; deprecated_at?: string | null; archived_at?: string | null; created_at: string; updated_at: string; }
|
||||
export interface ArtifactLocation { id: string; artifact_id?: string | null; derived_artifact_id?: string | null; storage_root_id: string; relative_path: string; status: ArtifactStatus; size_bytes?: number | null; observed_sha256?: string | null; last_checked_at?: string | null; created_at: string; updated_at: string; }
|
||||
export interface ModelArtifact { id: string; revision_id: string; filename: string; artifact_type: string; serialization_format: string; sha256: string; size_bytes: number; status: ArtifactStatus; security_status: string; license_status: string; quarantined: boolean; verification_details: JsonObject; verified_at?: string | null; immutable_at?: string | null; deprecated_at?: string | null; archived_at?: string | null; created_at: string; updated_at: string; locations: ArtifactLocation[]; }
|
||||
export interface DerivedSource { artifact_id: string; sha256: string; ordinal: number; }
|
||||
export interface DerivedArtifact { id: string; revision_id: string; filename: string; artifact_type: string; sha256: string; size_bytes: number; transformation_type: string; tool: string; tool_version: string; configuration: JsonObject; environment_snapshot: JsonObject; status: ArtifactStatus; immutable_at?: string | null; created_at: string; updated_at: string; sources: DerivedSource[]; locations: ArtifactLocation[]; }
|
||||
export interface ProjectBinding { capability: string; contract_version: number; binding: { channel: string; priority: string; optional: boolean; migration_support: string; benchmark_suites: string[]; }; }
|
||||
export interface ProjectSummary { id: string; name: string; description: string; bindings: ProjectBinding[]; notes: string[]; }
|
||||
export interface SystemMetadata { name: string; version: string; environment: string; api_version: string; milestone: string; production_inference_available: boolean; }
|
||||
|
||||
export interface DiscoveryCandidate {
|
||||
repository_id: string; resolved_commit_sha?: string | null; access_state: string;
|
||||
pipeline_tag?: string | null; library_name?: string | null; tags: string[];
|
||||
downloads?: number | null; likes?: number | null; last_modified?: string | null;
|
||||
matched_model_id?: string | null; upstream_facts: JsonObject; local_interpretation: JsonObject;
|
||||
}
|
||||
export interface UpstreamFile {
|
||||
id: string; path: string; size_bytes?: number | null; blob_id?: string | null;
|
||||
upstream_sha256?: string | null; file_format: string; role: string; risk_flags: string[];
|
||||
metadata_snapshot: JsonObject;
|
||||
}
|
||||
export interface UpstreamSnapshot {
|
||||
id: string; model_id?: string | null; provider: string; repository_id: string;
|
||||
requested_revision: string; resolved_commit_sha: string; access_state: string;
|
||||
metadata_snapshot: JsonObject; card_metadata: JsonObject; security_metadata: JsonObject;
|
||||
source_updated_at?: string | null; observed_at: string; stale_after: string; stale: boolean;
|
||||
files: UpstreamFile[];
|
||||
}
|
||||
export interface ArtifactSet {
|
||||
id: string; revision_id: string; snapshot_id: string; variant_key: string; label: string;
|
||||
selection_reason: string; selected_paths: string[]; total_size_bytes: number; file_count: number;
|
||||
availability: string; status: string; completeness: string; security_status: string;
|
||||
license_status: string; immutable_at: string; created_at: string; updated_at: string;
|
||||
}
|
||||
export interface StorageRoot {
|
||||
id: string; compute_node_id: string; name: string; purpose: string; path: string;
|
||||
agent_path?: string | null; status: string; writable: boolean; capacity_bytes?: number | null;
|
||||
free_bytes?: number | null; reserve_bytes: number; reserve_percent: number;
|
||||
}
|
||||
export interface DownloadPlanFile { ordinal: number; path: string; size_bytes: number; upstream_sha256?: string | null; file_format: string; role: string; risk_flags: string[]; }
|
||||
export interface DownloadPlan {
|
||||
id: string; artifact_set_id: string; compute_node_id: string; storage_root_id: string;
|
||||
repository_id: string; resolved_commit_sha: string; total_size_bytes: number; file_count: number;
|
||||
status: string; idempotency_key: string; preflight: JsonObject; immutable_payload: JsonObject;
|
||||
planned_at: string; expires_at: string; immutable_at: string; stale: boolean; files: DownloadPlanFile[];
|
||||
}
|
||||
export interface ArtifactJob {
|
||||
id: string; plan_id: string; compute_node_id: string; storage_root_id: string; status: string;
|
||||
attempt_count: number; progress_bytes: number; total_bytes: number; current_file?: string | null;
|
||||
cancel_requested: boolean; quarantine_relative_path?: string | null; promoted_relative_path?: string | null;
|
||||
error_code?: string | null; error_message?: string | null; result: JsonObject;
|
||||
started_at?: string | null; completed_at?: string | null; created_at: string; updated_at: string;
|
||||
}
|
||||
|
||||
export type CompatibilityStatus = "compatible" | "incompatible" | "unknown" | "requires_probe" | "blocked";
|
||||
export type RuntimeProbeStatus = "queued" | "preparing" | "loading" | "healthchecking" | "ready" | "unloading" | "completed" | "failed" | "cancelled";
|
||||
export interface RuntimeEnvironment {
|
||||
id: string; name: string; adapter: string; runtime_version: string; image_repository: string;
|
||||
image_digest: string; python_version: string; cuda_runtime_version?: string | null;
|
||||
package_versions: Record<string, string>; supported_model_types: string[];
|
||||
supported_formats: string[]; supported_modalities: string[]; network_policy: string;
|
||||
fingerprint: string; immutable_at: string; created_at: string;
|
||||
}
|
||||
export interface RuntimeProfile {
|
||||
id: string; name: string; runtime_environment_id: string; artifact_set_id: string;
|
||||
adapter: string; runtime_version: string; image_digest: string; version: number;
|
||||
dtype: string; quantization?: string | null; modality: string; max_sequence_length: number;
|
||||
batch_size: number; concurrency: number; device_policy: string;
|
||||
gpu_memory_policy: JsonObject; launch_parameters: JsonObject;
|
||||
environment_variables: Record<string, string>; trust_remote_code: false; network_egress: false;
|
||||
fingerprint: string; health_contract: JsonObject; immutable_at: string; created_at: string;
|
||||
}
|
||||
export interface CompatibilityAssessment {
|
||||
id: string; artifact_set_id: string; runtime_profile_id: string; compute_node_id: string;
|
||||
adapter: string; runtime_version: string; status: CompatibilityStatus;
|
||||
static_result: JsonObject; evidence: JsonObject; blockers: string[]; warnings: string[];
|
||||
required_approvals: string[]; hardware_facts: JsonObject; artifact_facts: JsonObject;
|
||||
environment_fingerprint: string; stale: boolean; stale_reason?: string | null; created_at: string;
|
||||
}
|
||||
export interface ExecutionApproval {
|
||||
id: string; artifact_set_id: string; scope: string; status: string;
|
||||
evidence_fingerprint: string; reason: string; approved_by: string; approved_at: string;
|
||||
expires_at?: string | null; revoked_at?: string | null; stale: boolean;
|
||||
}
|
||||
export interface RuntimeProbe {
|
||||
id: string; artifact_set_id: string; runtime_profile_id: string; compute_node_id: string;
|
||||
compatibility_assessment_id: string; execution_approval_id: string; status: RuntimeProbeStatus;
|
||||
phase?: string | null; attempt_count: number; cancel_requested: boolean;
|
||||
load_result: JsonObject; health_result: JsonObject; inference_result: JsonObject;
|
||||
unload_result: JsonObject; measured_resources: JsonObject; runtime_facts: JsonObject;
|
||||
environment_fingerprint: string; failure_code?: string | null; failure_message?: string | null;
|
||||
started_at?: string | null; finished_at?: string | null; created_at: string; updated_at: string;
|
||||
}
|
||||
export interface DeploymentCandidate {
|
||||
id: string; artifact_set_id: string; runtime_profile_id: string; compute_node_id: string;
|
||||
compatibility_assessment_id: string; runtime_probe_id: string; channel: "lab";
|
||||
status: "lab_ready"; production: false; health_contract: JsonObject;
|
||||
measured_resources: JsonObject; created_at: string;
|
||||
}
|
||||
|
||||
export interface EmbeddingSpace {
|
||||
id: string; identity_digest: string; dimension: number; normalized: boolean;
|
||||
migration_class: "requires_reindex"; identity_facts: JsonObject; created_at: string;
|
||||
}
|
||||
export interface CapabilityResourceEnvelope {
|
||||
id: string; runtime_probe_id: string; accelerator_kind: string; accelerator_uuid: string;
|
||||
environment_fingerprint: string; concurrency: number; batch_size: number;
|
||||
max_sequence_length: number; baseline_vram_bytes: number; resident_vram_bytes: number;
|
||||
peak_vram_bytes: number; required_vram_bytes: number; cold_load_time_ms: number;
|
||||
inference_latency_ms: number; stale: boolean; stale_reason?: string | null;
|
||||
}
|
||||
export interface ResidencyAllocation {
|
||||
id: string; state: string; worker_instance_id?: string | null; load_count: number;
|
||||
active_requests: number; measured_resident_vram_bytes: number;
|
||||
external_baseline_vram_bytes: number; health: JsonObject;
|
||||
resident_since?: string | null; last_used_at?: string | null;
|
||||
failure_code?: string | null; failure_message?: string | null;
|
||||
}
|
||||
export interface CapabilityDeployment {
|
||||
id: string; capability: string; contract_version: number; deployment_candidate_id: string;
|
||||
production_approval_id: string; artifact_set_id: string; runtime_profile_id: string;
|
||||
compute_node_id: string; accelerator_id: string; embedding_space: EmbeddingSpace;
|
||||
resource_envelope: CapabilityResourceEnvelope; residency: ResidencyAllocation;
|
||||
channel: string; status: string; production: boolean; health_status: string;
|
||||
routing_weight: number; fallback_policy: JsonObject; residency_policy: string;
|
||||
keep_warm_seconds: number; max_concurrency: number; max_queue_depth: number;
|
||||
config_fingerprint: string; provenance: JsonObject; rollback_policy: JsonObject;
|
||||
promoted_at?: string | null; created_at: string;
|
||||
}
|
||||
export type CapabilityCategory = "TEXT" | "RAG" | "DOCUMENT" | "VISION" | "AUDIO" | "GENERATION" | "ASSISTANTS";
|
||||
export interface CapabilityEstate {
|
||||
capability: string; version: number; category: CapabilityCategory; purpose: string;
|
||||
declared_stability: "stable" | "experimental" | "blocked" | "planned";
|
||||
operational_state: string; current_deployment_id?: string | null; model?: string | null;
|
||||
revision?: string | null; runtime?: string | null; node?: string | null;
|
||||
resource_class: "LIGHT" | "MEDIUM" | "HEAVY" | "EXCLUSIVE_GPU";
|
||||
measured_required_vram_bytes?: number | null; consumers: string[]; privacy_class: string;
|
||||
evaluation_type: string; evaluation_state: string;
|
||||
}
|
||||
export interface InstallationDependency {
|
||||
capability: string; version: number; deployment_id: string; channel: string;
|
||||
production: boolean; project_consumers: string[]; active_project_consumers: string[];
|
||||
project_fit_evidence_ids: string[]; evaluation_run_ids: string[];
|
||||
last_used_at?: string | null;
|
||||
}
|
||||
export interface ModelInstallationRationale {
|
||||
model_id: string; display_name: string; upstream_source: string; installed: boolean;
|
||||
installed_bytes: number; dependencies: InstallationDependency[]; can_delete: boolean;
|
||||
deletion_blockers: string[];
|
||||
}
|
||||
export interface SchedulerBudget {
|
||||
compute_node_id: string; node_name: string; accelerator_id: string;
|
||||
accelerator_uuid: string; accelerator_name: string; total_vram_bytes: number;
|
||||
observed_used_vram_bytes: number; external_vram_bytes: number;
|
||||
resident_vram_bytes: number; leased_vram_bytes: number; safety_reserve_bytes: number;
|
||||
schedulable_free_vram_bytes: number; pressure: boolean;
|
||||
pressure_state: "NORMAL" | "ELEVATED" | "HIGH" | "CRITICAL";
|
||||
attribution_confidence: "KNOWN" | "ESTIMATED" | "UNKNOWN";
|
||||
invariant_delta_bytes: number; policy_revision: string; observed_at?: string | null;
|
||||
}
|
||||
export interface PlacementPlan {
|
||||
id?: string | null; deployment_id: string; capability: string; node_id: string;
|
||||
accelerator_id: string; policy_revision: string;
|
||||
verdict: "ADMIT" | "ADMIT_AFTER_EVICTION" | "QUEUE" | "REJECT_CAPACITY" | "REJECT_HEALTH" | "REJECT_POLICY";
|
||||
reason_codes: string[]; required_vram_bytes: number; headroom_before_bytes: number;
|
||||
headroom_after_bytes: number; expected_cold_load_ms: number;
|
||||
evictions: Array<{ deployment_id: string; capability: string; expected_reclaimed_bytes: number; reason: string }>;
|
||||
decision_fingerprint: string; dry_run: boolean; created_at: string;
|
||||
}
|
||||
export interface SchedulerPolicy {
|
||||
revision: string; active: boolean; configuration: JsonObject; created_at: string;
|
||||
}
|
||||
export interface GatewayRequest {
|
||||
request_id: string; capability: string; client?: string | null; status: string;
|
||||
cold?: boolean | null; queue_time_ms?: number | null; load_time_ms?: number | null;
|
||||
inference_time_ms?: number | null; total_latency_ms?: number | null;
|
||||
failure_code?: string | null; created_at: string;
|
||||
}
|
||||
export interface ServiceClient {
|
||||
id: string; name: string; status: string; allowed_capabilities: string[];
|
||||
requests_per_minute: number; max_concurrent_requests: number;
|
||||
workload_priority: "production" | "interactive" | "background" | "benchmark";
|
||||
project_key?: string | null; project_binding_id?: string | null;
|
||||
integration_environment?: string | null; purpose?: string | null;
|
||||
credential_prefix?: string | null; credential_created_at?: string | null;
|
||||
credential_expires_at?: string | null; credential_revoked_at?: string | null;
|
||||
last_used_at?: string | null; created_at: string;
|
||||
}
|
||||
export interface ServiceClientCreated extends ServiceClient { credential: string; }
|
||||
export interface ProjectFitEvidence {
|
||||
id: string; project_key: string; capability: string; environment: string;
|
||||
recommendation: "PROMOTION_ELIGIBLE" | "KEEP_LAB" | "BLOCKED" | "REQUIRES_MORE_EVIDENCE";
|
||||
evidence_class: "OWNER_PHOTO" | "PUBLIC_PHYSICAL_CAPTURE" | "CATALOG_REFERENCE";
|
||||
engineering_integration: "PASS" | "INCOMPLETE" | "BLOCKED";
|
||||
production_validation: "DEFERRED_EXTERNAL_VALIDATION" | "REQUIRED" | "SATISFIED";
|
||||
production_action: "NONE";
|
||||
case_count: number; metric_values: JsonObject; critical_errors: number;
|
||||
blockers: string[]; evidence_digest: string; evidence: JsonObject;
|
||||
capability_deployment_id?: string | null; created_at: string;
|
||||
}
|
||||
export interface ProjectIntegration {
|
||||
project_key: string; project_name: string; capability: string; environment: string;
|
||||
state: string; purpose: string; client_id: string; client_name: string;
|
||||
deployment_id?: string | null; resource_impact_bytes?: number | null;
|
||||
usage: { request_volume: number; successful_requests: number; error_count: number;
|
||||
last_used_at?: string | null; latency_p50_ms?: number | null; latency_p95_ms?: number | null };
|
||||
project_fit?: ProjectFitEvidence | null;
|
||||
}
|
||||
export interface GatewayInvokeResponse {
|
||||
capability: "rag.embedding@1"; dimension: 1024; normalized: true;
|
||||
embedding_space_id: string; data: number[][]; request_id: string;
|
||||
execution: { cold: boolean; node: string; residency: string; load_count: number; timings: {
|
||||
gateway_ms: number; queue_ms: number; load_ms: number; inference_ms: number; total_ms: number;
|
||||
} }; usage: { input_count: number; input_tokens: number };
|
||||
}
|
||||
export interface GatewayExecutionSummary {
|
||||
cold: boolean; node: string; residency: string; load_count: number;
|
||||
timings: { total_ms: number; inference_ms: number; load_ms: number; gateway_ms: number };
|
||||
}
|
||||
export interface OCRInvokeResponse {
|
||||
capability: "document.ocr@1"; text: string; pages: Array<{ page: 1; width: number; height: number }>;
|
||||
confidence?: number | null; request_id: string; execution: GatewayExecutionSummary;
|
||||
}
|
||||
export interface VisionEmbeddingResponse {
|
||||
capability: "vision.embedding@1"; dimension: number; normalized: true;
|
||||
embedding_space_id: string; data: number[][]; request_id: string;
|
||||
execution: GatewayExecutionSummary;
|
||||
}
|
||||
export interface SpeechTranscriptionResponse {
|
||||
capability: "speech.transcription@1"; text: string; language: string;
|
||||
duration_seconds: number; segments: Array<{ start_seconds: number; end_seconds: number; text: string }>;
|
||||
request_id: string; execution: GatewayExecutionSummary;
|
||||
}
|
||||
export interface ProductionApproval {
|
||||
id: string; deployment_candidate_id: string; capability_contract_id: string;
|
||||
artifact_set_id: string; runtime_profile_id: string; compute_node_id: string;
|
||||
deployment_config: JsonObject; supply_chain_evidence: JsonObject;
|
||||
evidence_fingerprint: string; status: string; approved_by: string; reason: string;
|
||||
approved_at: string; revoked_at?: string | null; stale: boolean;
|
||||
}
|
||||
|
||||
export interface LifecyclePolicy {
|
||||
id: string; key: string; revision: number; scope: string; requirements: JsonObject;
|
||||
fingerprint: string; active: boolean; created_by: string; created_at: string;
|
||||
}
|
||||
export interface LifecycleSubject {
|
||||
id: string; target_type: string; target_ref: string; environment: string; state: string;
|
||||
version: number; superseded_by_ref?: string | null; created_at: string; updated_at: string;
|
||||
}
|
||||
export interface LifecycleApproval {
|
||||
id: string; policy_revision_id: string; policy_key: string; policy_revision: number;
|
||||
version: number; subject_id?: string | null; target_type: string; target_ref: string;
|
||||
environment: string; requested_transition: string; evidence_snapshot: {
|
||||
declared: JsonObject; resolved: JsonObject;
|
||||
}; evidence_fingerprint: string; status: string; blockers: string[]; warnings: string[];
|
||||
requested_by: string; approved_by?: string | null; reason: string;
|
||||
expires_at?: string | null; stale_at?: string | null; decided_at?: string | null; created_at: string;
|
||||
}
|
||||
export interface PromotionPlan {
|
||||
id: string; approval_request_id: string; subject_id: string; current_state: string;
|
||||
desired_state: string; migration_class: string; candidate_deployment_id?: string | null;
|
||||
rollback_target_ref: string; project_consumers: string[]; affected_identities: JsonObject;
|
||||
impact_analysis: JsonObject; migration_id?: string | null; canary_strategy: JsonObject;
|
||||
drain_strategy: JsonObject; health_gates: JsonObject; automatic_abort_conditions: string[];
|
||||
plan_fingerprint: string; status: string; version: number; created_by: string;
|
||||
approved_by?: string | null; immutable_at?: string | null; created_at: string;
|
||||
}
|
||||
export interface LifecycleOperation {
|
||||
id: string; promotion_plan_id: string; version: number; stage: string; idempotency_key: string;
|
||||
expected_subject_version: number; requester: string; approver: string; executor: string;
|
||||
failure_code?: string | null; failure_details: JsonObject; rollback_duration_ms?: number | null;
|
||||
started_at: string; finished_at?: string | null; canary?: {
|
||||
id: string; mode: string; traffic_percent: number; target_request_count: number;
|
||||
request_count: number; error_count: number; latency_p95_ms?: number | null;
|
||||
status: string; abort_trigger?: string | null; thresholds: JsonObject;
|
||||
} | null;
|
||||
}
|
||||
export interface RetentionPolicy {
|
||||
id: string; key: string; revision: number; minimum_rollback_days: number;
|
||||
requirements: JsonObject; fingerprint: string; active: boolean; created_by: string; created_at: string;
|
||||
}
|
||||
export interface CleanupPlan {
|
||||
id: string; target_type: string; target_ref: string; action: string;
|
||||
dependencies: Array<{ type: string; ref: string; blocking: boolean; state?: string }>;
|
||||
dependency_digest: string; reclaimable_bytes: number; retention_state: string;
|
||||
blockers: string[]; status: string; created_by: string; created_at: string;
|
||||
executed_at?: string | null;
|
||||
}
|
||||
export interface LifecycleEvent {
|
||||
id: string; event_type: string; object_type: string; object_ref: string;
|
||||
from_state?: string | null; to_state?: string | null; actor: string; actor_role: string;
|
||||
policy_revision_id?: string | null; evidence_ids: string[]; reason: string;
|
||||
change_id: string; details: JsonObject; occurred_at: string;
|
||||
}
|
||||
|
||||
export interface EvaluationSuite {
|
||||
id: string; project_id: string; key: string; name: string; description: string;
|
||||
latest_revision_id: string; latest_revision: string; case_count: number;
|
||||
critical_case_count: number; created_at: string;
|
||||
}
|
||||
export interface EvaluationRun {
|
||||
id: string; project_id: string; suite_revision_id: string; target_kind: "current" | "shadow";
|
||||
target_index_ref: string; embedding_space_ref: string; capability_deployment_id?: string | null;
|
||||
status: string; corpus_revision: string; retrieval_config_digest: string;
|
||||
environment_fingerprint: JsonObject; environment_digest: string; expected_cases: number;
|
||||
completed_cases: number; error_count: number; aggregate_metrics: Record<string, number>;
|
||||
started_at?: string | null; completed_at?: string | null; created_at: string;
|
||||
}
|
||||
export interface EvaluationCaseResult {
|
||||
id: string; run_id: string; case_id: string; case_key: string; critical: boolean;
|
||||
ranked_results: Array<{ chunk_id: string; document_id?: string | null; score: number }>;
|
||||
relevant_results: string[]; first_relevant_rank?: number | null;
|
||||
metrics: Record<string, number>; latency_ms: number; error_code?: string | null;
|
||||
}
|
||||
export interface EvaluationComparison {
|
||||
id: string; project_id: string; baseline_run_id: string; candidate_run_id: string;
|
||||
comparability: string; comparability_evidence: JsonObject; metric_deltas: Record<string, number>;
|
||||
improved_cases: number; unchanged_cases: number; regressed_cases: number;
|
||||
critical_regressions: number; case_comparisons: Array<{ case_id: string; case_key: string;
|
||||
critical: boolean; classification: string; first_relevant_rank_before?: number | null;
|
||||
first_relevant_rank_after?: number | null }>;
|
||||
promotion_eligibility: string; eligibility_evidence: JsonObject; created_at: string;
|
||||
}
|
||||
export interface RetrievalCandidatePool {
|
||||
id: string; project_id: string; suite_revision_id: string; evaluation_case_id: string;
|
||||
source_embedding_space: string; source_index_ref: string; corpus_revision: string;
|
||||
retrieval_config_digest: string; candidate_count: number;
|
||||
ordered_candidates: Array<{ id: string; document_id?: string | null; score: number; content_sha256: string }>;
|
||||
fingerprint: string; created_at: string; immutable_at: string;
|
||||
}
|
||||
export interface RetrievalPipelineIdentity {
|
||||
id: string; project_id: string; embedding_space_ref: string; sparse_config_digest: string;
|
||||
fusion_config_digest: string; reranker_deployment_id?: string | null;
|
||||
reranker_config_digest?: string | null; candidate_k: number; output_k: number;
|
||||
identity_digest: string; configuration: JsonObject; migration_class: string;
|
||||
created_at: string; immutable_at: string;
|
||||
}
|
||||
export interface RerankingRun {
|
||||
id: string; project_id: string; suite_revision_id: string; pipeline_identity_id: string;
|
||||
control_pipeline_identity_id: string; reranker_deployment_id?: string | null; status: string;
|
||||
corpus_revision: string; candidate_pool_set_fingerprint: string;
|
||||
environment_fingerprint: JsonObject; environment_digest: string; expected_cases: number;
|
||||
completed_cases: number; error_count: number; aggregate_metrics: Record<string, number>;
|
||||
latency_metrics: Record<string, number>; started_at?: string | null;
|
||||
completed_at?: string | null; created_at: string;
|
||||
}
|
||||
export interface DiscoveryAssessment {
|
||||
id: string; model_id?: string | null; upstream_snapshot_id: string; candidate_key: string;
|
||||
repository_id: string; resolved_commit_sha: string; artifact_evidence: JsonObject;
|
||||
security_state: JsonObject; license_state: JsonObject; gpu_fit: JsonObject; status: string;
|
||||
rationale: string; evidence_fingerprint: string; created_at: string; immutable_at: string;
|
||||
}
|
||||
export interface ModelComparisonCandidate {
|
||||
candidate_key: string; label: string; status: "evaluated" | "blocked" | "unknown";
|
||||
evaluation_run_id?: string | null; candidate_deployment_id?: string | null;
|
||||
embedding_space?: string | null; artifact_size_bytes?: number | null;
|
||||
quality_metrics?: Record<string, number> | null; metric_deltas: Record<string, number>;
|
||||
critical_regressions?: number | null; case_outcomes?: { improved: number; unchanged: number; regressed: number } | null;
|
||||
comparability?: string; latency_ms: Record<string, number>; resource_evidence: JsonObject;
|
||||
migration_impact: JsonObject; security_state: JsonObject; provenance: JsonObject; blockers: string[];
|
||||
}
|
||||
export interface ModelComparison {
|
||||
id: string; project_id: string; capability_contract_id: string; suite_revision_id: string;
|
||||
current_run_id: string; title: string; candidates: ModelComparisonCandidate[];
|
||||
comparability: string; evidence_fingerprint: string; created_at: string;
|
||||
}
|
||||
export interface AdvisorRecommendation {
|
||||
id: string; comparison_id: string; project_id: string; capability_contract_id: string;
|
||||
policy_id: string; candidate_key: string; current_deployment_id?: string | null;
|
||||
candidate_deployment_id?: string | null; current_embedding_space: string;
|
||||
candidate_embedding_space?: string | null; verdict: "KEEP_CURRENT" | "KEEP_CURRENT_EMBEDDING_ADD_RERANKER_CANDIDATE" | "PROMOTION_ELIGIBLE" | "PROMOTION_NOT_RECOMMENDED" | "REQUIRES_MORE_EVIDENCE";
|
||||
confidence: "HIGH" | "MEDIUM" | "LOW"; evidence_level: string;
|
||||
quality_deltas: Record<string, number>; latency_deltas: JsonObject; resource_deltas: JsonObject;
|
||||
migration_impact: JsonObject; security_state: JsonObject; key_improvements: string[];
|
||||
blockers: string[]; policy_snapshot: JsonObject; evidence_fingerprint: string;
|
||||
status: string; generated_at: string; dismissed_at?: string | null;
|
||||
dismissed_by?: string | null; dismissal_reason?: string | null;
|
||||
}
|
||||
export interface EmbeddingMigration {
|
||||
id: string; project_id: string; source_embedding_space: string;
|
||||
target_embedding_space_id: string; source_index_ref: string; target_index_ref: string;
|
||||
corpus_revision: string; status: string; total_chunks: number; completed_chunks: number;
|
||||
failed_chunks: number; retried_chunks: number; batch_size: number; concurrency: number;
|
||||
priority: "background"; preflight_evidence: JsonObject; progress_evidence: JsonObject;
|
||||
validation_evidence: JsonObject; operational_metrics: JsonObject;
|
||||
evaluation_eligibility: boolean; cancel_requested: boolean; failure_code?: string | null;
|
||||
failure_message?: string | null; started_at?: string | null; finished_at?: string | null;
|
||||
created_at: string; updated_at: string;
|
||||
}
|
||||
|
||||
export interface MigrationPlan {
|
||||
id: string; project_id: string; project_binding_id: string; capability_contract_id: string;
|
||||
migration_class: "TRANSPARENT" | "BEHAVIORAL" | "REQUIRES_REINDEX" | "SCHEMA_BREAKING";
|
||||
environment: "LAB" | "PRODUCTION"; state: string; version: number; generation: number;
|
||||
adapter: { key: string; version: string; fingerprint: string; operations: string[] };
|
||||
source_identity: JsonObject; target_identity: JsonObject; source_data_target: string;
|
||||
target_shadow_target: string; source_space_ref: string; target_space_id: string;
|
||||
corpus_revision: string; migration_policy_revision: string; validation_policy_revision_id: string;
|
||||
lifecycle_approval_id: string; promotion_plan_id?: string | null; rollback_target_ref: string;
|
||||
total_expected_items: number; completed_items: number; failed_items: number;
|
||||
retryable_items: number; permanent_failed_items: number; batch_size: number;
|
||||
priority: "BACKGROUND"; rollback_retention_days: number; plan_fingerprint: string;
|
||||
approval_fingerprint: string; failure_code?: string | null; failure_details: JsonObject;
|
||||
last_cursor?: string | null; cancel_requested: boolean; started_at?: string | null;
|
||||
completed_at?: string | null; immutable_at: string; created_at: string; updated_at: string;
|
||||
}
|
||||
export interface MigrationValidationSnapshot {
|
||||
id: string; migration_plan_id: string; generation: number; expected_count: number;
|
||||
actual_count: number; missing_count: number; duplicate_count: number; malformed_count: number;
|
||||
non_finite_count: number; wrong_dimension_count: number; content_hash_mismatch_count: number;
|
||||
wrong_space_count: number; comparable: boolean; critical_regressions: number; passed: boolean;
|
||||
technical_cutover_eligible: boolean; project_promotion_eligible: boolean; blockers: string[];
|
||||
target_fingerprint: string; snapshot_fingerprint: string; evidence: JsonObject; created_at: string;
|
||||
}
|
||||
export interface MigrationCutoverOperation {
|
||||
id: string; migration_plan_id: string; stage: string; generation: number; source_before: string;
|
||||
target_after: string; external_state_fingerprint: string; health_evidence: JsonObject;
|
||||
failure_code?: string | null; failure_details: JsonObject; switch_duration_ms?: number | null;
|
||||
rollback_duration_ms?: number | null; started_at: string; finished_at?: string | null;
|
||||
}
|
||||
export interface MigrationEvent {
|
||||
id: string; migration_plan_id: string; operation_id?: string | null; event_type: string;
|
||||
before_state?: string | null; after_state?: string | null; actor: string; reason: string;
|
||||
policy_revision: string; evidence_refs: string[]; source_identity: JsonObject;
|
||||
target_identity: JsonObject; generation: number; change_id: string; details: JsonObject;
|
||||
occurred_at: string;
|
||||
}
|
||||
|
||||
export type SLOState = "HEALTHY" | "AT_RISK" | "BREACHED" | "INSUFFICIENT_DATA" | "STALE" | "DISABLED";
|
||||
export type OperationalAlertState = "PENDING" | "FIRING" | "ACKNOWLEDGED" | "RESOLVED" | "SUPPRESSED";
|
||||
|
||||
export interface SLOEvaluation {
|
||||
id: string; policy_id: string; policy_key: string; policy_revision: number; sli_key: string;
|
||||
environment: string; objective: number; observed_value?: number | null;
|
||||
threshold_ms?: number | null; sample_count: number; good_count: number; bad_count: number;
|
||||
state: SLOState; window_start: string; window_end: string; observed_at: string;
|
||||
freshness_seconds?: number | null; allowed_bad?: number | null; consumed_bad?: number | null;
|
||||
remaining_bad?: number | null; short_burn_rate?: number | null; long_burn_rate?: number | null;
|
||||
evidence: JsonObject;
|
||||
}
|
||||
export interface OperationalAlert {
|
||||
id: string; rule_id: string; fingerprint: string; alert_type: string; severity: string;
|
||||
state: OperationalAlertState; source: string; subject_type: string; subject_ref: string;
|
||||
summary: string; details: JsonObject; first_seen_at: string; last_seen_at: string;
|
||||
firing_at?: string | null; acknowledged_at?: string | null; acknowledged_by?: string | null;
|
||||
resolved_at?: string | null; suppressed_until?: string | null; occurrence_count: number;
|
||||
}
|
||||
export interface CapacitySnapshot {
|
||||
id: string; observed_at: string; received_at: string; node_id: string; node_name: string;
|
||||
accelerator_id?: string | null; gpu_total_bytes?: number | null; gpu_observed_bytes?: number | null;
|
||||
gpu_external_bytes?: number | null; gpu_managed_resident_bytes?: number | null;
|
||||
gpu_leased_bytes?: number | null; gpu_reserve_bytes?: number | null;
|
||||
gpu_schedulable_bytes?: number | null; pressure_state: string;
|
||||
system_ram_total_bytes?: number | null; system_ram_available_bytes?: number | null;
|
||||
storage_total_bytes?: number | null; storage_free_bytes?: number | null;
|
||||
availability: string; freshness_seconds: number;
|
||||
}
|
||||
export interface OperationalIncident {
|
||||
id: string; fingerprint: string; title: string; state: string; severity: string;
|
||||
root_subject_type: string; root_subject_ref: string;
|
||||
correlation: "RELATED" | "LIKELY_ROOT" | "DOWNSTREAM" | "UNKNOWN";
|
||||
first_seen_at: string; last_seen_at: string; resolved_at?: string | null;
|
||||
}
|
||||
export interface OperationsOverview {
|
||||
status: "HEALTHY" | "DEGRADED" | "OBSERVABILITY_DEGRADED"; observed_at: string;
|
||||
active_alerts: OperationalAlert[]; slo_evaluations: SLOEvaluation[];
|
||||
capacity: CapacitySnapshot[]; capability_health: JsonObject[]; project_health: JsonObject[];
|
||||
recent_failures: JsonObject[]; history_available: boolean;
|
||||
}
|
||||
|
||||
|
||||
export type RecoveryAssetClass = "AUTHORITATIVE" | "REBUILDABLE" | "EPHEMERAL" | "EXTERNAL" | "SECRET";
|
||||
export type BackupState = "PLANNED" | "CREATING" | "CREATED" | "VERIFYING" | "VERIFIED" | "FAILED" | "EXPIRED" | "DELETED";
|
||||
export type RestoreState =
|
||||
| "PLANNED" | "PREFLIGHT" | "RESTORING_DATABASE" | "RESTORING_CONFIGURATION"
|
||||
| "REHYDRATING_ARTIFACTS" | "RECONCILING" | "VALIDATING" | "READY" | "FAILED"
|
||||
| "MANUAL_INTERVENTION_REQUIRED";
|
||||
export type RecoveryReadinessState =
|
||||
| "PROTECTED" | "REHYDRATABLE" | "ROTATION_REQUIRED" | "EXTERNAL_DEPENDENCY" | "UNPROTECTED";
|
||||
|
||||
export interface RecoveryPolicy {
|
||||
id: string; key: string; name: string; revision: number; asset_class: RecoveryAssetClass;
|
||||
backup_method: string; retention_days: number; minimum_verified_backups: number;
|
||||
rpo_seconds?: number | null; rto_target_seconds?: number | null; restore_verification: string;
|
||||
encryption_required: boolean; external_dependency: boolean; rehydration_allowed: boolean;
|
||||
secret_class?: string | null; rationale: string; active: boolean; fingerprint: string;
|
||||
created_by: string; created_at: string;
|
||||
}
|
||||
export interface RecoveryAsset {
|
||||
id: string; key: string; name: string; asset_class: RecoveryAssetClass; owner: string;
|
||||
location: string; backup_method: string; restore_method: string; rebuild_method?: string | null;
|
||||
rpo_seconds?: number | null; readiness: RecoveryReadinessState; dependencies: string[];
|
||||
notes: string; policy_key: string; updated_at: string;
|
||||
}
|
||||
export interface BackupManifestEntry {
|
||||
id: string; logical_asset_type: string; object_name: string; relative_path: string;
|
||||
size_bytes: number; sha256: string; source_generation: string; schema_version?: string | null;
|
||||
dependency_refs: JsonObject;
|
||||
}
|
||||
export interface BackupSet {
|
||||
id: string; backup_id: string; state: BackupState; policy_key: string; policy_revision: number;
|
||||
modelforge_version: string; modelforge_commit?: string | null; schema_revision?: string | null;
|
||||
environment_fingerprint: JsonObject; database_identity: JsonObject; destination_root: string;
|
||||
manifest_relative_path?: string | null; manifest_sha256?: string | null;
|
||||
included_asset_classes: string[]; excluded_asset_classes: string[]; payload_bytes: number;
|
||||
encrypted: boolean; encryption_algorithm?: string | null; encryption_key_id?: string | null;
|
||||
verification_details: JsonObject; verified_at?: string | null; failure_code?: string | null;
|
||||
failure_reason?: string | null; milestone?: string | null; legal_hold: boolean;
|
||||
restore_eligible: boolean; reason: string; created_by: string; started_at?: string | null;
|
||||
completed_at?: string | null; expires_at?: string | null; created_at: string;
|
||||
entries: BackupManifestEntry[];
|
||||
}
|
||||
export interface RestorePlan {
|
||||
id: string; backup_set_id: string; backup_id: string; mode: string; state: string;
|
||||
target_environment: string; target_label: string; database_destination_redacted: string;
|
||||
artifact_strategy: string; secret_strategy: string; node_strategy: string;
|
||||
expected_modelforge_version?: string | null; preflight: JsonObject;
|
||||
validation_requirements: JsonObject; fingerprint: string; reason: string; created_by: string;
|
||||
created_at: string;
|
||||
}
|
||||
export interface RestoreOperation {
|
||||
id: string; plan_id: string; backup_set_id: string; backup_id: string; state: RestoreState;
|
||||
mode: string; attempt: number; idempotency_key: string; preflight_result: JsonObject;
|
||||
phase_durations: Record<string, number>; source_fingerprint: JsonObject;
|
||||
restored_fingerprint: JsonObject; fingerprint_diff: JsonObject; validation_result: JsonObject;
|
||||
rpo_seconds?: number | null; rto_seconds?: number | null; failure_code?: string | null;
|
||||
failure_reason?: string | null; started_at: string; updated_at: string; ready_at?: string | null;
|
||||
}
|
||||
export interface RestoreOperationEvent {
|
||||
id: string; restore_operation_id: string; from_state?: string | null; to_state: string;
|
||||
phase: string; actor: string; reason: string; evidence: JsonObject; occurred_at: string;
|
||||
}
|
||||
export interface ArtifactRecovery {
|
||||
id: string; restore_operation_id?: string | null; artifact_set_id: string;
|
||||
model_revision_id: string; recovery_class: string; state: string;
|
||||
upstream_repository?: string | null; upstream_commit_sha?: string | null;
|
||||
target_storage_root_id: string; expected_files: JsonObject[]; verified_files: JsonObject[];
|
||||
bytes_total: number; bytes_recovered: number; download_plan_id?: string | null;
|
||||
artifact_job_id?: string | null; lineage: JsonObject; duration_seconds?: number | null;
|
||||
failure_code?: string | null; failure_reason?: string | null; started_at: string;
|
||||
completed_at?: string | null;
|
||||
}
|
||||
export interface RecoveryReadinessEntry {
|
||||
asset_key: string; asset_name: string; asset_class: RecoveryAssetClass;
|
||||
readiness: RecoveryReadinessState; policy_key: string; rpo_seconds?: number | null;
|
||||
detail: string;
|
||||
}
|
||||
export interface RecoveryDashboard {
|
||||
observed_at: string; point_in_time_support: "SUPPORTED" | "NOT_SUPPORTED";
|
||||
latest_verified_backup_id?: string | null; latest_verified_backup_at?: string | null;
|
||||
latest_verified_backup_age_seconds?: number | null;
|
||||
latest_verified_schema_revision?: string | null; backup_states: Record<string, number>;
|
||||
verified_backup_count: number; stale_backup: boolean;
|
||||
backup_staleness_threshold_seconds: number; last_restore_rehearsal_at?: string | null;
|
||||
last_restore_rehearsal_state?: string | null; observed_restore_seconds?: number | null;
|
||||
observed_rpo_seconds?: number | null; protected_asset_count: number;
|
||||
unprotected_assets: string[]; readiness: RecoveryReadinessEntry[]; coverage_ratio: number;
|
||||
estimated_protected_bytes: number; estimated_rehydratable_bytes: number;
|
||||
destination_capacity_bytes?: number | null; destination_free_bytes?: number | null;
|
||||
}
|
||||
export interface BackupCapacityEstimate {
|
||||
bytes_to_copy: number; bytes_manifest_only: number; estimated_protected_bytes: number;
|
||||
available_bytes?: number | null; capacity_bytes?: number | null; sufficient: boolean;
|
||||
detail: string;
|
||||
}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": []
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { defineConfig } from "vitest/config";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import { loadEnv } from "vite";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
export default defineConfig(({ mode }) => {
|
||||
const env = loadEnv(mode, fileURLToPath(new URL(".", import.meta.url)), "VITE_");
|
||||
return {
|
||||
plugins: [react()],
|
||||
server: {
|
||||
port: 3000,
|
||||
// Optional same-origin development bridge for browser acceptance against a real remote API.
|
||||
// Production builds never use it and no target is inferred when the variable is absent.
|
||||
proxy: env.VITE_API_PROXY_TARGET ? {
|
||||
"/api": { target: env.VITE_API_PROXY_TARGET, changeOrigin: false },
|
||||
} : undefined,
|
||||
},
|
||||
test: {
|
||||
environment: "jsdom",
|
||||
setupFiles: "./src/test/setup.ts",
|
||||
// One reusable worker keeps the release gate deterministic on operator
|
||||
// hosts that also run several long-lived local control-plane processes.
|
||||
pool: "threads",
|
||||
maxWorkers: 1,
|
||||
isolate: false,
|
||||
},
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user