feat: expose institutional architecture and runtime state

This commit is contained in:
2026-08-01 17:48:37 +02:00
parent 05ce4dc8ec
commit c241085806
10 changed files with 906 additions and 10 deletions
+254 -1
View File
@@ -1,8 +1,9 @@
import { useEffect, useState } from "react";
import { RefreshCw } from "lucide-react";
import { PauseCircle, PlayCircle, RefreshCw } from "lucide-react";
import {
Button,
Card,
ConfirmDialog,
DataGrid,
DismissibleAlert,
LoadingFrame,
@@ -18,10 +19,14 @@ import {
"@govoplan/core-webui";
import {
fetchOpsStatus,
cancelRuntimeNodeDrain,
drainRuntimeNode,
runOpsChecks,
type OpsCheck,
type OpsDeploymentProfile,
type OpsGovernanceModule,
type OpsRecoveryOperation,
type OpsRuntimeNode,
type OpsSizingAssumption,
type OpsStatus
} from "../../api/ops";
@@ -30,6 +35,8 @@ export default function OpsPage({ settings, auth }: {settings: ApiSettings;auth:
const [status, setStatus] = useState<OpsStatus | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
const [drainTarget, setDrainTarget] = useState<OpsRuntimeNode | null>(null);
const [nodeActionId, setNodeActionId] = useState("");
async function load() {
setLoading(true);
@@ -55,6 +62,34 @@ export default function OpsPage({ settings, auth }: {settings: ApiSettings;auth:
}
}
async function confirmDrain() {
if (!drainTarget) return;
setNodeActionId(drainTarget.node_id);
setError("");
try {
await drainRuntimeNode(settings, drainTarget.node_id);
setDrainTarget(null);
setStatus(await fetchOpsStatus(settings));
} catch (err) {
setError(adminErrorMessage(err));
} finally {
setNodeActionId("");
}
}
async function cancelDrain(node: OpsRuntimeNode) {
setNodeActionId(node.node_id);
setError("");
try {
await cancelRuntimeNodeDrain(settings, node.node_id);
setStatus(await fetchOpsStatus(settings));
} catch (err) {
setError(adminErrorMessage(err));
} finally {
setNodeActionId("");
}
}
useEffect(() => {void load();}, [settings.apiBaseUrl, settings.apiKey, settings.accessToken]);
const checks = status?.checks ?? [];
@@ -89,6 +124,9 @@ export default function OpsPage({ settings, auth }: {settings: ApiSettings;auth:
<MetricCard label="i18n:govoplan-ops.workers.b6ef3acd" value={status?.summary.celery_enabled ? status.summary.worker_metrics.workers ?? 0 : "off"} tone={status?.summary.celery_enabled && !(status.summary.worker_metrics.missing_queues?.length) ? "good" : "warning"} detail={workerMetricDetail(status)} />
<MetricCard label="i18n:govoplan-ops.redis.5eaa1f2f" value={status?.summary.redis_url ? "configured" : "-"} tone={status?.summary.celery_enabled ? "info" : "neutral"} detail={status?.summary.redis_url ?? "-"} />
<MetricCard label="i18n:govoplan-ops.warnings.1430f976" value={warningCount + errorCount} tone={errorCount ? "danger" : warningCount ? "warning" : "good"} detail="i18n:govoplan-ops.current_health_checks.7830bccf" />
<MetricCard label="Runtime nodes" value={status?.summary.runtime_node_count ?? 0} tone={status?.runtime_cluster.available ? "good" : "danger"} detail={runtimeNodeMetricDetail(status)} />
<MetricCard label="Recovery" value={status?.summary.recovery_required_count ?? 0} tone={status?.summary.recovery_required_count ? "danger" : "good"} detail="Operations requiring recovery attention" />
<MetricCard label="Provider bindings" value={status?.governance.summary.configured_external_provider_count ?? 0} tone={status?.governance.summary.provider_attention_count ? "warning" : "good"} detail={`${status?.governance.summary.provider_attention_count ?? 0} requiring attention`} />
</div>
<div className="dashboard-grid">
@@ -96,6 +134,20 @@ export default function OpsPage({ settings, auth }: {settings: ApiSettings;auth:
<CheckList checks={checks} />
</Card>
<Card title="Runtime cluster">
<RuntimeNodeTable
nodes={status?.runtime_cluster.nodes ?? []}
canManage={canRunChecks}
busyNodeId={nodeActionId}
onDrain={setDrainTarget}
onCancelDrain={(node) => void cancelDrain(node)}
/>
</Card>
<Card title="Recovery evidence">
<RecoveryTable operations={status?.runtime_cluster.recovery.operations ?? []} />
</Card>
<Card title="i18n:govoplan-ops.governance_inventory.835d8e57">
<GovernanceTable modules={status?.governance.modules ?? []} />
</Card>
@@ -109,11 +161,159 @@ export default function OpsPage({ settings, auth }: {settings: ApiSettings;auth:
</Card>
</div>
</LoadingFrame>
<ConfirmDialog
open={Boolean(drainTarget)}
title="Drain runtime node"
message={`Stop routing new work to ${drainTarget?.node_id ?? "this node"}. In-flight work is allowed to finish.`}
confirmLabel="Drain node"
busy={Boolean(nodeActionId)}
onCancel={() => setDrainTarget(null)}
onConfirm={() => void confirmDrain()}
/>
</div>
</PageScrollViewport>);
}
function RuntimeNodeTable({
nodes,
canManage,
busyNodeId,
onDrain,
onCancelDrain
}: {
nodes: OpsRuntimeNode[];
canManage: boolean;
busyNodeId: string;
onDrain: (node: OpsRuntimeNode) => void;
onCancelDrain: (node: OpsRuntimeNode) => void;
}) {
const columns: DataGridColumn<OpsRuntimeNode>[] = [
{
id: "node",
header: "Node",
width: "minmax(220px, 1fr)",
minWidth: 200,
resizable: true,
sortable: true,
filterable: true,
value: (node) => `${node.node_id} ${node.role}`,
render: (node) => <div><strong>{node.node_id}</strong><span className="muted block">{node.role} · {node.software_version}</span></div>
},
{
id: "state",
header: "State",
width: 150,
sortable: true,
filterable: true,
value: (node) => node.stale ? "stale" : node.state,
render: (node) => <StatusBadge status={node.stale ? "error" : stateTone(node.state)} label={node.stale ? "stale" : node.state} />
},
{
id: "heartbeat",
header: "Last heartbeat",
width: "minmax(210px, .8fr)",
minWidth: 190,
resizable: true,
sortable: true,
value: (node) => node.last_heartbeat_at,
render: (node) => new Date(node.last_heartbeat_at).toLocaleString()
},
{
id: "queues",
header: "Queues",
width: "minmax(180px, .7fr)",
minWidth: 160,
resizable: true,
filterable: true,
value: (node) => node.queues.join(" "),
render: (node) => node.queues.join(", ") || "-"
},
{
id: "actions",
header: "",
width: 56,
value: (node) => node.state,
render: (node) => {
if (!canManage || node.state === "stopped") return null;
const busy = busyNodeId === node.node_id;
return node.state === "draining" ? (
<Button
variant="ghost"
title="Cancel drain"
aria-label={`Cancel drain for ${node.node_id}`}
disabled={busy}
onClick={() => onCancelDrain(node)}
><PlayCircle size={16} /></Button>
) : (
<Button
variant="ghost"
title="Drain node"
aria-label={`Drain ${node.node_id}`}
disabled={busy || node.stale}
onClick={() => onDrain(node)}
><PauseCircle size={16} /></Button>
);
}
}
];
return <DataGrid id="ops-runtime-nodes" rows={nodes} columns={columns} getRowKey={(node) => `${node.node_id}:${node.incarnation}`} emptyText="No runtime nodes reported." />;
}
function RecoveryTable({ operations }: { operations: OpsRecoveryOperation[] }) {
const columns: DataGridColumn<OpsRecoveryOperation>[] = [
{
id: "operation",
header: "Operation",
width: "minmax(240px, 1fr)",
minWidth: 220,
resizable: true,
sortable: true,
filterable: true,
value: (operation) => `${operation.module_id} ${operation.operation_type}`,
render: (operation) => <div><strong>{operation.operation_type}</strong><span className="muted block">{operation.module_id} · {operation.mode}</span></div>
},
{
id: "resource",
header: "Resource",
width: "minmax(180px, .8fr)",
minWidth: 160,
resizable: true,
filterable: true,
value: (operation) => `${operation.resource_type ?? ""} ${operation.resource_id ?? ""}`,
render: (operation) => operation.resource_type ? `${operation.resource_type}: ${operation.resource_id ?? "-"}` : "-"
},
{
id: "status",
header: "Status",
width: 170,
sortable: true,
filterable: true,
value: (operation) => operation.status,
render: (operation) => <StatusBadge status={recoveryTone(operation.status)} label={operation.status.replaceAll("_", " ")} />
},
{
id: "evidence",
header: "Evidence",
width: 150,
sortable: true,
value: (operation) => operation.checkpoint_count,
render: (operation) => `${operation.checkpoint_count} checkpoint(s)`
},
{
id: "updated",
header: "Updated",
width: "minmax(210px, .8fr)",
minWidth: 190,
resizable: true,
sortable: true,
value: (operation) => operation.updated_at,
render: (operation) => new Date(operation.updated_at).toLocaleString()
}
];
return <DataGrid id="ops-recovery-operations" rows={operations} columns={columns} getRowKey={(operation) => operation.id} emptyText="No recovery operations recorded." />;
}
function GovernanceTable({ modules }: { modules: OpsGovernanceModule[] }) {
const columns: DataGridColumn<OpsGovernanceModule>[] = [
{
@@ -145,6 +345,44 @@ function GovernanceTable({ modules }: { modules: OpsGovernanceModule[] }) {
value: (module) => `${module.capability_count} ${module.policy_count}`,
render: (module) => `${module.capability_count} capabilities · ${module.policy_count} policies`
},
{
id: "architecture",
header: "Architecture",
width: "minmax(210px, .9fr)",
minWidth: 190,
resizable: true,
sortable: true,
filterable: true,
value: (module) => module.architecture
? `${module.architecture.layer} ${module.architecture.kind} ${module.architecture.maturity}`
: "undeclared",
render: (module) => module.architecture ? (
<div>
<strong>{module.architecture.maturity}</strong>
<span className="muted block">{module.architecture.layer} · {module.architecture.kind}</span>
{module.architecture.known_limits.length ? <span className="muted block">{module.architecture.known_limits.length} known limit(s)</span> : null}
</div>
) : <span className="muted">staged declaration pending</span>
},
{
id: "providers",
header: "External providers",
width: "minmax(230px, 1fr)",
minWidth: 210,
resizable: true,
filterable: true,
value: (module) => `${module.external_provider_count} ${module.external_providers.map((provider) => `${provider.id} ${provider.maturity} ${provider.authority_modes.join(" ")} ${provider.runtime_state?.health ?? "unobserved"} ${provider.runtime_state?.freshness ?? ""}`).join(" ")}`,
render: (module) => module.external_provider_count ? (
<div>
<strong>{module.external_provider_count} declared</strong>
{module.external_providers.map((provider) => (
<span className="muted block" key={provider.id}>
{provider.label}: {provider.maturity} · {provider.runtime_state ? `${provider.runtime_state.health}/${provider.runtime_state.freshness} · ${provider.runtime_state.bindings?.length ?? 0} binding(s)` : "unobserved"}
</span>
))}
</div>
) : <span className="muted">none declared</span>
},
{
id: "controls",
header: "i18n:govoplan-ops.controls.0cdb80fb",
@@ -227,6 +465,21 @@ function stateTone(state: string): string {
return "inactive";
}
function recoveryTone(state: string): string {
if (["succeeded", "recovered"].includes(state)) return "success";
if (["failed", "recovery_required"].includes(state)) return "error";
if (["running", "recovering", "prepared"].includes(state)) return "warning";
return "inactive";
}
function runtimeNodeMetricDetail(status: OpsStatus | null): string {
const cluster = status?.runtime_cluster;
if (!cluster?.available) return cluster?.detail ?? "Runtime directory unavailable";
const active = cluster.active ?? { api: 0, worker: 0 };
const expected = cluster.expected ?? { api: 0, worker: 0 };
return `${active.api}/${expected.api} API · ${active.worker}/${expected.worker} workers`;
}
function workerMetricDetail(status: OpsStatus | null): string {
if (!status?.summary.celery_enabled) return "i18n:govoplan-ops.celery_worker_setting.323d7737";
const metrics = status.summary.worker_metrics;