Migrate Ops interface patterns
This commit is contained in:
@@ -0,0 +1,21 @@
|
|||||||
|
# Ops interface pattern migration
|
||||||
|
|
||||||
|
Ops uses the platform monitoring and governed-operation patterns. It projects sanitized state owned by Core and modules; it does not become the repair authority for domain effects.
|
||||||
|
|
||||||
|
## Surfaces
|
||||||
|
|
||||||
|
- `ops.page` is the route-level operations workspace and `ops.navigation` is its navigation entry.
|
||||||
|
- `ops.page.summary` presents readiness, capacity, queue, storage, backup, recovery, provider, and governance metrics.
|
||||||
|
- `ops.page.health` owns bounded module probes; `ops.action.run-probes` is available only to an operations runner.
|
||||||
|
- `ops.page.runtime` owns runtime heartbeats and the confirmed `ops.action.drain-node` lifecycle action.
|
||||||
|
- `ops.page.recovery` presents sanitized durable recovery evidence without replay controls.
|
||||||
|
- `ops.page.governance`, `ops.page.deployment`, and `ops.page.sizing` expose declared architecture, provider, topology, and capacity assumptions.
|
||||||
|
- `ops.widget.health` is a read-only Dashboard projection of the same status endpoint.
|
||||||
|
|
||||||
|
Backend and WebUI manifests publish the same identifiers and parent hierarchy for Views and configured-system Docs.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
Running probes performs only bounded, declared operational checks and refreshes the status projection. Draining a node stops new work from being routed to the selected runtime incarnation while in-flight work completes; cancellation is possible while the node remains draining. The shared confirmation dialog names the target and consequence. Stale, stopped, unauthorized, loading, and already-running states remain visible with an explicit reason.
|
||||||
|
|
||||||
|
Ops does not reconcile outcome-unknown work, restore backups, expose secrets, or invoke private sibling-module code. Operators follow the owning module's recovery contract and use Ops only for bounded observation and runtime coordination.
|
||||||
@@ -133,7 +133,22 @@ manifest = ModuleManifest(
|
|||||||
documentation_types=("admin", "user"),
|
documentation_types=("admin", "user"),
|
||||||
audience=("operator", "system_admin"),
|
audience=("operator", "system_admin"),
|
||||||
related_modules=("audit", "docs", "notifications"),
|
related_modules=("audit", "docs", "notifications"),
|
||||||
metadata={"kind": "reference"},
|
metadata={
|
||||||
|
"kind": "reference",
|
||||||
|
"help_contexts": [
|
||||||
|
"ops.page",
|
||||||
|
"ops.page.summary",
|
||||||
|
"ops.page.health",
|
||||||
|
"ops.page.governance",
|
||||||
|
"ops.page.deployment",
|
||||||
|
"ops.page.sizing",
|
||||||
|
"ops.widget.health",
|
||||||
|
"ops.state.read-only",
|
||||||
|
],
|
||||||
|
"consequence_classes": {
|
||||||
|
"run_probes": "run bounded module-owned health probes and refresh the sanitized operational projection",
|
||||||
|
},
|
||||||
|
},
|
||||||
),
|
),
|
||||||
DocumentationTopic(
|
DocumentationTopic(
|
||||||
id="ops.runtime-coordination-and-recovery",
|
id="ops.runtime-coordination-and-recovery",
|
||||||
@@ -165,6 +180,18 @@ manifest = ModuleManifest(
|
|||||||
"Ops does not create or restore backups and never receives private artifact or key-custody references.",
|
"Ops does not create or restore backups and never receives private artifact or key-custody references.",
|
||||||
"A verified receipt proves the recorded drill; it does not make an unsafe post-migration code rollback reversible.",
|
"A verified receipt proves the recorded drill; it does not make an unsafe post-migration code rollback reversible.",
|
||||||
],
|
],
|
||||||
|
"help_contexts": [
|
||||||
|
"ops.page.runtime",
|
||||||
|
"ops.page.recovery",
|
||||||
|
"ops.action.drain-node",
|
||||||
|
"ops.state.readiness-blocked",
|
||||||
|
"ops.state.stale-node",
|
||||||
|
],
|
||||||
|
"consequence_classes": {
|
||||||
|
"drain_node": "stop routing new work to the selected runtime incarnation while in-flight work completes",
|
||||||
|
"cancel_node_drain": "return a still-draining runtime node to active scheduling",
|
||||||
|
"inspect_recovery": "read sanitized durable recovery state without replaying or repairing the owning effect",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -199,6 +226,92 @@ manifest = ModuleManifest(
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
view_surfaces=(
|
view_surfaces=(
|
||||||
|
ViewSurface(
|
||||||
|
id="ops.navigation",
|
||||||
|
module_id="ops",
|
||||||
|
kind="navigation",
|
||||||
|
label="Operations navigation",
|
||||||
|
order=10,
|
||||||
|
),
|
||||||
|
ViewSurface(
|
||||||
|
id="ops.page",
|
||||||
|
module_id="ops",
|
||||||
|
kind="route",
|
||||||
|
label="Operations workspace",
|
||||||
|
order=20,
|
||||||
|
),
|
||||||
|
ViewSurface(
|
||||||
|
id="ops.page.summary",
|
||||||
|
module_id="ops",
|
||||||
|
kind="section",
|
||||||
|
label="Operations summary",
|
||||||
|
parent_id="ops.page",
|
||||||
|
order=10,
|
||||||
|
),
|
||||||
|
ViewSurface(
|
||||||
|
id="ops.page.health",
|
||||||
|
module_id="ops",
|
||||||
|
kind="section",
|
||||||
|
label="Health checks",
|
||||||
|
parent_id="ops.page",
|
||||||
|
order=20,
|
||||||
|
),
|
||||||
|
ViewSurface(
|
||||||
|
id="ops.page.runtime",
|
||||||
|
module_id="ops",
|
||||||
|
kind="section",
|
||||||
|
label="Runtime cluster",
|
||||||
|
parent_id="ops.page",
|
||||||
|
order=30,
|
||||||
|
),
|
||||||
|
ViewSurface(
|
||||||
|
id="ops.page.recovery",
|
||||||
|
module_id="ops",
|
||||||
|
kind="section",
|
||||||
|
label="Recovery evidence",
|
||||||
|
parent_id="ops.page",
|
||||||
|
order=40,
|
||||||
|
),
|
||||||
|
ViewSurface(
|
||||||
|
id="ops.page.governance",
|
||||||
|
module_id="ops",
|
||||||
|
kind="section",
|
||||||
|
label="Governance inventory",
|
||||||
|
parent_id="ops.page",
|
||||||
|
order=50,
|
||||||
|
),
|
||||||
|
ViewSurface(
|
||||||
|
id="ops.page.deployment",
|
||||||
|
module_id="ops",
|
||||||
|
kind="section",
|
||||||
|
label="Deployment profiles",
|
||||||
|
parent_id="ops.page",
|
||||||
|
order=60,
|
||||||
|
),
|
||||||
|
ViewSurface(
|
||||||
|
id="ops.page.sizing",
|
||||||
|
module_id="ops",
|
||||||
|
kind="section",
|
||||||
|
label="Sizing assumptions",
|
||||||
|
parent_id="ops.page",
|
||||||
|
order=70,
|
||||||
|
),
|
||||||
|
ViewSurface(
|
||||||
|
id="ops.action.run-probes",
|
||||||
|
module_id="ops",
|
||||||
|
kind="action",
|
||||||
|
label="Run operational probes",
|
||||||
|
parent_id="ops.page.health",
|
||||||
|
order=80,
|
||||||
|
),
|
||||||
|
ViewSurface(
|
||||||
|
id="ops.action.drain-node",
|
||||||
|
module_id="ops",
|
||||||
|
kind="action",
|
||||||
|
label="Drain runtime node",
|
||||||
|
parent_id="ops.page.runtime",
|
||||||
|
order=90,
|
||||||
|
),
|
||||||
ViewSurface(
|
ViewSurface(
|
||||||
id="ops.widget.health",
|
id="ops.widget.health",
|
||||||
module_id="ops",
|
module_id="ops",
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from govoplan_ops.backend.manifest import get_manifest
|
||||||
|
|
||||||
|
|
||||||
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
|
||||||
|
|
||||||
|
class OpsInterfaceDocumentationContractTests(unittest.TestCase):
|
||||||
|
def test_backend_surfaces_and_hierarchy_remain_declared(self) -> None:
|
||||||
|
frontend = get_manifest().frontend
|
||||||
|
self.assertIsNotNone(frontend)
|
||||||
|
surfaces = {item.id: item for item in frontend.view_surfaces} # type: ignore[union-attr]
|
||||||
|
expected = {
|
||||||
|
"ops.navigation",
|
||||||
|
"ops.page",
|
||||||
|
"ops.page.summary",
|
||||||
|
"ops.page.health",
|
||||||
|
"ops.page.runtime",
|
||||||
|
"ops.page.recovery",
|
||||||
|
"ops.page.governance",
|
||||||
|
"ops.page.deployment",
|
||||||
|
"ops.page.sizing",
|
||||||
|
"ops.action.run-probes",
|
||||||
|
"ops.action.drain-node",
|
||||||
|
"ops.widget.health",
|
||||||
|
}
|
||||||
|
self.assertEqual(expected, set(surfaces))
|
||||||
|
for surface_id in (
|
||||||
|
"ops.page.summary",
|
||||||
|
"ops.page.health",
|
||||||
|
"ops.page.runtime",
|
||||||
|
"ops.page.recovery",
|
||||||
|
"ops.page.governance",
|
||||||
|
"ops.page.deployment",
|
||||||
|
"ops.page.sizing",
|
||||||
|
):
|
||||||
|
self.assertEqual("ops.page", surfaces[surface_id].parent_id)
|
||||||
|
self.assertEqual("ops.page.health", surfaces["ops.action.run-probes"].parent_id)
|
||||||
|
self.assertEqual("ops.page.runtime", surfaces["ops.action.drain-node"].parent_id)
|
||||||
|
|
||||||
|
def test_help_and_consequence_metadata_remain_published(self) -> None:
|
||||||
|
topics = {topic.id: topic for topic in get_manifest().documentation}
|
||||||
|
status = topics["ops.health-governance-and-sizing"]
|
||||||
|
recovery = topics["ops.runtime-coordination-and-recovery"]
|
||||||
|
|
||||||
|
self.assertIn("ops.page.health", status.metadata["help_contexts"])
|
||||||
|
self.assertIn("run_probes", status.metadata["consequence_classes"])
|
||||||
|
self.assertIn("ops.action.drain-node", recovery.metadata["help_contexts"])
|
||||||
|
self.assertIn("drain_node", recovery.metadata["consequence_classes"])
|
||||||
|
self.assertIn("cancel_node_drain", recovery.metadata["consequence_classes"])
|
||||||
|
self.assertIn("inspect_recovery", recovery.metadata["consequence_classes"])
|
||||||
|
|
||||||
|
def test_webui_uses_shared_operational_patterns(self) -> None:
|
||||||
|
page = (REPO_ROOT / "webui/src/features/ops/OpsPage.tsx").read_text(encoding="utf-8")
|
||||||
|
widget = (REPO_ROOT / "webui/src/features/ops/OpsHealthWidget.tsx").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
for component in (
|
||||||
|
"ActionBlockerHint",
|
||||||
|
"ConfirmDialog",
|
||||||
|
"DataGrid",
|
||||||
|
"DocumentationHelpLink",
|
||||||
|
"LoadingFrame",
|
||||||
|
"MetricCard",
|
||||||
|
"TableActionGroup",
|
||||||
|
):
|
||||||
|
self.assertIn(component, page)
|
||||||
|
self.assertIn("DocumentationHelpLink", widget)
|
||||||
|
self.assertIn("LoadingFrame", widget)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import {
|
import {
|
||||||
DismissibleAlert,
|
DismissibleAlert,
|
||||||
|
DocumentationHelpLink,
|
||||||
LoadingFrame,
|
LoadingFrame,
|
||||||
MetricCard,
|
MetricCard,
|
||||||
StatusBadge,
|
StatusBadge,
|
||||||
@@ -8,6 +9,7 @@ import {
|
|||||||
type ApiSettings
|
type ApiSettings
|
||||||
} from "@govoplan/core-webui";
|
} from "@govoplan/core-webui";
|
||||||
import { fetchOpsStatus, type OpsStatus } from "../../api/ops";
|
import { fetchOpsStatus, type OpsStatus } from "../../api/ops";
|
||||||
|
import { OPS_DOCUMENTATION } from "./interfacePatterns";
|
||||||
|
|
||||||
export default function OpsHealthWidget({ settings, refreshKey }: { settings: ApiSettings; refreshKey: number }) {
|
export default function OpsHealthWidget({ settings, refreshKey }: { settings: ApiSettings; refreshKey: number }) {
|
||||||
const [status, setStatus] = useState<OpsStatus | null>(null);
|
const [status, setStatus] = useState<OpsStatus | null>(null);
|
||||||
@@ -38,6 +40,9 @@ export default function OpsHealthWidget({ settings, refreshKey }: { settings: Ap
|
|||||||
return (
|
return (
|
||||||
<LoadingFrame loading={loading} label="Loading operations status">
|
<LoadingFrame loading={loading} label="Loading operations status">
|
||||||
{error && <DismissibleAlert tone="warning" resetKey={error}>{error}</DismissibleAlert>}
|
{error && <DismissibleAlert tone="warning" resetKey={error}>{error}</DismissibleAlert>}
|
||||||
|
<div className="button-row compact-actions">
|
||||||
|
<DocumentationHelpLink reference={OPS_DOCUMENTATION} />
|
||||||
|
</div>
|
||||||
<div className="metric-grid inside dashboard-widget-metrics">
|
<div className="metric-grid inside dashboard-widget-metrics">
|
||||||
<MetricCard label="Readiness" value={ready ? "ready" : "blocked"} tone={ready ? "good" : "danger"} detail={status?.readiness.profile ?? "-"} />
|
<MetricCard label="Readiness" value={ready ? "ready" : "blocked"} tone={ready ? "good" : "danger"} detail={status?.readiness.profile ?? "-"} />
|
||||||
<MetricCard label="Workers" value={status?.summary.celery_enabled ? workerMetrics?.workers ?? 0 : "off"} tone={status?.summary.celery_enabled && !missingQueues.length ? "good" : "warning"} detail={missingQueues.length ? `${missingQueues.length} queue(s) without a consumer` : status?.summary.celery_enabled ? "All configured queues covered" : "Single-process mode"} />
|
<MetricCard label="Workers" value={status?.summary.celery_enabled ? workerMetrics?.workers ?? 0 : "off"} tone={status?.summary.celery_enabled && !missingQueues.length ? "good" : "warning"} detail={missingQueues.length ? `${missingQueues.length} queue(s) without a consumer` : status?.summary.celery_enabled ? "All configured queues covered" : "Single-process mode"} />
|
||||||
|
|||||||
@@ -1,18 +1,22 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { PauseCircle, PlayCircle, RefreshCw } from "lucide-react";
|
import { PauseCircle, PlayCircle, RefreshCw } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
|
ActionBlockerHint,
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
ConfirmDialog,
|
ConfirmDialog,
|
||||||
DataGrid,
|
DataGrid,
|
||||||
DismissibleAlert,
|
DismissibleAlert,
|
||||||
|
DocumentationHelpLink,
|
||||||
LoadingFrame,
|
LoadingFrame,
|
||||||
MetricCard,
|
MetricCard,
|
||||||
PageScrollViewport,
|
PageScrollViewport,
|
||||||
PageTitle,
|
PageTitle,
|
||||||
StatusBadge,
|
StatusBadge,
|
||||||
|
TableActionGroup,
|
||||||
adminErrorMessage,
|
adminErrorMessage,
|
||||||
hasAnyScope,
|
hasAnyScope,
|
||||||
|
i18nMessage,
|
||||||
type ApiSettings,
|
type ApiSettings,
|
||||||
type AuthInfo,
|
type AuthInfo,
|
||||||
type DataGridColumn } from
|
type DataGridColumn } from
|
||||||
@@ -30,10 +34,16 @@ import {
|
|||||||
type OpsSizingAssumption,
|
type OpsSizingAssumption,
|
||||||
type OpsStatus
|
type OpsStatus
|
||||||
} from "../../api/ops";
|
} from "../../api/ops";
|
||||||
|
import {
|
||||||
|
OPS_DOCUMENTATION,
|
||||||
|
OPS_I18N,
|
||||||
|
OPS_RECOVERY_DOCUMENTATION,
|
||||||
|
} from "./interfacePatterns";
|
||||||
|
|
||||||
export default function OpsPage({ settings, auth }: {settings: ApiSettings;auth: AuthInfo;}) {
|
export default function OpsPage({ settings, auth }: {settings: ApiSettings;auth: AuthInfo;}) {
|
||||||
const [status, setStatus] = useState<OpsStatus | null>(null);
|
const [status, setStatus] = useState<OpsStatus | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [runningProbes, setRunningProbes] = useState(false);
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
const [drainTarget, setDrainTarget] = useState<OpsRuntimeNode | null>(null);
|
const [drainTarget, setDrainTarget] = useState<OpsRuntimeNode | null>(null);
|
||||||
const [nodeActionId, setNodeActionId] = useState("");
|
const [nodeActionId, setNodeActionId] = useState("");
|
||||||
@@ -51,6 +61,7 @@ export default function OpsPage({ settings, auth }: {settings: ApiSettings;auth:
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function runChecks() {
|
async function runChecks() {
|
||||||
|
setRunningProbes(true);
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError("");
|
setError("");
|
||||||
try {
|
try {
|
||||||
@@ -58,6 +69,7 @@ export default function OpsPage({ settings, auth }: {settings: ApiSettings;auth:
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(adminErrorMessage(err));
|
setError(adminErrorMessage(err));
|
||||||
} finally {
|
} finally {
|
||||||
|
setRunningProbes(false);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -97,6 +109,13 @@ export default function OpsPage({ settings, auth }: {settings: ApiSettings;auth:
|
|||||||
const errorCount = checks.filter((item) => item.state === "error").length;
|
const errorCount = checks.filter((item) => item.state === "error").length;
|
||||||
const ready = status?.readiness.ready ?? false;
|
const ready = status?.readiness.ready ?? false;
|
||||||
const canRunChecks = hasAnyScope(auth, ["ops:operations:run", "system:settings:write"]);
|
const canRunChecks = hasAnyScope(auth, ["ops:operations:run", "system:settings:write"]);
|
||||||
|
const runProbesDisabledReason = runningProbes
|
||||||
|
? OPS_I18N.runningProbes
|
||||||
|
: loading
|
||||||
|
? OPS_I18N.loading
|
||||||
|
: !canRunChecks
|
||||||
|
? OPS_I18N.runPermissionRequired
|
||||||
|
: undefined;
|
||||||
const queueDepths = status?.summary.worker_metrics.queue_depths ?? {};
|
const queueDepths = status?.summary.worker_metrics.queue_depths ?? {};
|
||||||
const queuedTasks = Object.values(queueDepths).reduce((total, value) => total + value, 0);
|
const queuedTasks = Object.values(queueDepths).reduce((total, value) => total + value, 0);
|
||||||
const storageUsage = status?.summary.storage_metrics?.capacity_used_percent;
|
const storageUsage = status?.summary.storage_metrics?.capacity_used_percent;
|
||||||
@@ -110,12 +129,25 @@ export default function OpsPage({ settings, auth }: {settings: ApiSettings;auth:
|
|||||||
<p>i18n:govoplan-ops.runtime_health_deployment_profile_worker_split_a.55340156</p>
|
<p>i18n:govoplan-ops.runtime_health_deployment_profile_worker_split_a.55340156</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="button-row compact-actions">
|
<div className="button-row compact-actions">
|
||||||
{canRunChecks ? <Button variant="primary" onClick={() => void runChecks()} disabled={loading}>Run probes</Button> : null}
|
<DocumentationHelpLink reference={OPS_DOCUMENTATION} />
|
||||||
<Button onClick={() => void load()} disabled={loading}><RefreshCw size={16} /> i18n:govoplan-ops.reload.cce71553</Button>
|
<Button variant="primary" onClick={() => void runChecks()} disabled={Boolean(runProbesDisabledReason)} disabledReason={runProbesDisabledReason}>i18n:govoplan-ops.surface.run_probes</Button>
|
||||||
|
<Button onClick={() => void load()} disabled={loading} disabledReason={loading ? OPS_I18N.loading : undefined}><RefreshCw size={16} /> i18n:govoplan-ops.reload.cce71553</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && <DismissibleAlert tone="danger" resetKey={error} floating>{error}</DismissibleAlert>}
|
{error && <DismissibleAlert tone="danger" resetKey={error} floating>{error}</DismissibleAlert>}
|
||||||
|
{status && !ready && (
|
||||||
|
<ActionBlockerHint
|
||||||
|
reason={{
|
||||||
|
summary: "i18n:govoplan-ops.readiness_blocked_summary",
|
||||||
|
details: i18nMessage("i18n:govoplan-ops.readiness_blocked_details", { value0: status.readiness.blockers.length }),
|
||||||
|
requiredAction: "i18n:govoplan-ops.readiness_blocked_action",
|
||||||
|
actor: "i18n:govoplan-ops.operations_operator",
|
||||||
|
target: "i18n:govoplan-ops.readiness_blocked_target"
|
||||||
|
}}
|
||||||
|
documentation={OPS_RECOVERY_DOCUMENTATION}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<LoadingFrame loading={loading} label="i18n:govoplan-ops.loading_operations_status.6890fe6e">
|
<LoadingFrame loading={loading} label="i18n:govoplan-ops.loading_operations_status.6890fe6e">
|
||||||
<div className="metric-grid">
|
<div className="metric-grid">
|
||||||
@@ -172,9 +204,9 @@ export default function OpsPage({ settings, auth }: {settings: ApiSettings;auth:
|
|||||||
</LoadingFrame>
|
</LoadingFrame>
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
open={Boolean(drainTarget)}
|
open={Boolean(drainTarget)}
|
||||||
title="Drain runtime node"
|
title="i18n:govoplan-ops.surface.drain_node"
|
||||||
message={`Stop routing new work to ${drainTarget?.node_id ?? "this node"}. In-flight work is allowed to finish.`}
|
message={i18nMessage("i18n:govoplan-ops.drain_runtime_node_message", { value0: drainTarget?.node_id ?? "i18n:govoplan-ops.this_node" })}
|
||||||
confirmLabel="Drain node"
|
confirmLabel="i18n:govoplan-ops.drain_node"
|
||||||
busy={Boolean(nodeActionId)}
|
busy={Boolean(nodeActionId)}
|
||||||
onCancel={() => setDrainTarget(null)}
|
onCancel={() => setDrainTarget(null)}
|
||||||
onConfirm={() => void confirmDrain()}
|
onConfirm={() => void confirmDrain()}
|
||||||
@@ -244,24 +276,32 @@ function RuntimeNodeTable({
|
|||||||
width: 56,
|
width: 56,
|
||||||
value: (node) => node.state,
|
value: (node) => node.state,
|
||||||
render: (node) => {
|
render: (node) => {
|
||||||
if (!canManage || node.state === "stopped") return null;
|
|
||||||
const busy = busyNodeId === node.node_id;
|
const busy = busyNodeId === node.node_id;
|
||||||
return node.state === "draining" ? (
|
const cancelling = node.state === "draining";
|
||||||
<Button
|
const disabledReason = !canManage
|
||||||
variant="ghost"
|
? OPS_I18N.runPermissionRequired
|
||||||
title="Cancel drain"
|
: busy
|
||||||
aria-label={`Cancel drain for ${node.node_id}`}
|
? OPS_I18N.nodeActionActive
|
||||||
disabled={busy}
|
: node.state === "stopped"
|
||||||
onClick={() => onCancelDrain(node)}
|
? OPS_I18N.stoppedNode
|
||||||
><PlayCircle size={16} /></Button>
|
: !cancelling && node.stale
|
||||||
) : (
|
? OPS_I18N.staleNode
|
||||||
<Button
|
: undefined;
|
||||||
variant="ghost"
|
return (
|
||||||
title="Drain node"
|
<TableActionGroup
|
||||||
aria-label={`Drain ${node.node_id}`}
|
minimumSlots={1}
|
||||||
disabled={busy || node.stale}
|
label={i18nMessage("i18n:govoplan-ops.lifecycle_actions_for_value", { value0: node.node_id })}
|
||||||
onClick={() => onDrain(node)}
|
actions={[{
|
||||||
><PauseCircle size={16} /></Button>
|
id: cancelling ? "cancel-drain" : "drain",
|
||||||
|
label: cancelling
|
||||||
|
? i18nMessage("i18n:govoplan-ops.cancel_drain_for_value", { value0: node.node_id })
|
||||||
|
: i18nMessage("i18n:govoplan-ops.drain_value", { value0: node.node_id }),
|
||||||
|
icon: cancelling ? <PlayCircle size={16} /> : <PauseCircle size={16} />,
|
||||||
|
disabled: Boolean(disabledReason),
|
||||||
|
disabledReason,
|
||||||
|
onClick: () => cancelling ? onCancelDrain(node) : onDrain(node)
|
||||||
|
}]}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import type { DocumentationHelpReference } from "@govoplan/core-webui";
|
||||||
|
|
||||||
|
export const OPS_DOCUMENTATION = {
|
||||||
|
topicId: "ops.health-governance-and-sizing",
|
||||||
|
documentationType: "admin"
|
||||||
|
} satisfies DocumentationHelpReference;
|
||||||
|
|
||||||
|
export const OPS_RECOVERY_DOCUMENTATION = {
|
||||||
|
topicId: "ops.runtime-coordination-and-recovery",
|
||||||
|
documentationType: "admin"
|
||||||
|
} satisfies DocumentationHelpReference;
|
||||||
|
|
||||||
|
export const OPS_I18N = {
|
||||||
|
loading: "i18n:govoplan-ops.reason.loading",
|
||||||
|
runningProbes: "i18n:govoplan-ops.reason.running_probes",
|
||||||
|
runPermissionRequired: "i18n:govoplan-ops.reason.run_permission_required",
|
||||||
|
nodeActionActive: "i18n:govoplan-ops.reason.node_action_active",
|
||||||
|
staleNode: "i18n:govoplan-ops.reason.stale_node",
|
||||||
|
stoppedNode: "i18n:govoplan-ops.reason.stopped_node"
|
||||||
|
} as const;
|
||||||
@@ -2,6 +2,34 @@ import type { PlatformTranslations } from "@govoplan/core-webui";
|
|||||||
|
|
||||||
export const generatedTranslations: PlatformTranslations = {
|
export const generatedTranslations: PlatformTranslations = {
|
||||||
"en": {
|
"en": {
|
||||||
|
"i18n:govoplan-ops.surface.navigation": "Operations navigation",
|
||||||
|
"i18n:govoplan-ops.surface.page": "Operations workspace",
|
||||||
|
"i18n:govoplan-ops.surface.summary": "Operations summary",
|
||||||
|
"i18n:govoplan-ops.surface.health": "Health checks",
|
||||||
|
"i18n:govoplan-ops.surface.runtime": "Runtime cluster",
|
||||||
|
"i18n:govoplan-ops.surface.recovery": "Recovery evidence",
|
||||||
|
"i18n:govoplan-ops.surface.governance": "Governance inventory",
|
||||||
|
"i18n:govoplan-ops.surface.deployment": "Deployment profiles",
|
||||||
|
"i18n:govoplan-ops.surface.sizing": "Sizing assumptions",
|
||||||
|
"i18n:govoplan-ops.surface.run_probes": "Run operational probes",
|
||||||
|
"i18n:govoplan-ops.surface.drain_node": "Drain runtime node",
|
||||||
|
"i18n:govoplan-ops.reason.loading": "Operations status is loading.",
|
||||||
|
"i18n:govoplan-ops.reason.running_probes": "Operational probes are already running.",
|
||||||
|
"i18n:govoplan-ops.reason.run_permission_required": "Operations-run permission is required.",
|
||||||
|
"i18n:govoplan-ops.reason.node_action_active": "A lifecycle action is already running for this node.",
|
||||||
|
"i18n:govoplan-ops.reason.stale_node": "A stale node cannot be drained because its current incarnation is not reporting.",
|
||||||
|
"i18n:govoplan-ops.reason.stopped_node": "A stopped node cannot accept lifecycle actions.",
|
||||||
|
"i18n:govoplan-ops.readiness_blocked_summary": "The runtime is not ready for normal traffic.",
|
||||||
|
"i18n:govoplan-ops.readiness_blocked_details": "{value0} readiness blocker(s) are active.",
|
||||||
|
"i18n:govoplan-ops.readiness_blocked_action": "Review the health checks, runtime cluster, and owning-module recovery evidence before restoring traffic.",
|
||||||
|
"i18n:govoplan-ops.operations_operator": "Operations operator",
|
||||||
|
"i18n:govoplan-ops.readiness_blocked_target": "Health checks, Runtime cluster, and Recovery evidence below",
|
||||||
|
"i18n:govoplan-ops.lifecycle_actions_for_value": "Lifecycle actions for {value0}",
|
||||||
|
"i18n:govoplan-ops.cancel_drain_for_value": "Cancel drain for {value0}",
|
||||||
|
"i18n:govoplan-ops.drain_value": "Drain {value0}",
|
||||||
|
"i18n:govoplan-ops.drain_runtime_node_message": "Stop routing new work to {value0}. In-flight work is allowed to finish.",
|
||||||
|
"i18n:govoplan-ops.this_node": "this node",
|
||||||
|
"i18n:govoplan-ops.drain_node": "Drain node",
|
||||||
"i18n:govoplan-ops.area.2745deba": "Area",
|
"i18n:govoplan-ops.area.2745deba": "Area",
|
||||||
"i18n:govoplan-ops.baseline.e6ab7982": "Baseline",
|
"i18n:govoplan-ops.baseline.e6ab7982": "Baseline",
|
||||||
"i18n:govoplan-ops.authority.8802e425": "Authority",
|
"i18n:govoplan-ops.authority.8802e425": "Authority",
|
||||||
@@ -43,6 +71,34 @@ export const generatedTranslations: PlatformTranslations = {
|
|||||||
"i18n:govoplan-ops.workers.b6ef3acd": "Workers"
|
"i18n:govoplan-ops.workers.b6ef3acd": "Workers"
|
||||||
},
|
},
|
||||||
"de": {
|
"de": {
|
||||||
|
"i18n:govoplan-ops.surface.navigation": "Betriebsnavigation",
|
||||||
|
"i18n:govoplan-ops.surface.page": "Betriebsarbeitsbereich",
|
||||||
|
"i18n:govoplan-ops.surface.summary": "Betriebsübersicht",
|
||||||
|
"i18n:govoplan-ops.surface.health": "Systemprüfungen",
|
||||||
|
"i18n:govoplan-ops.surface.runtime": "Laufzeitcluster",
|
||||||
|
"i18n:govoplan-ops.surface.recovery": "Wiederherstellungsnachweise",
|
||||||
|
"i18n:govoplan-ops.surface.governance": "Governance-Inventar",
|
||||||
|
"i18n:govoplan-ops.surface.deployment": "Bereitstellungsprofile",
|
||||||
|
"i18n:govoplan-ops.surface.sizing": "Dimensionierungsannahmen",
|
||||||
|
"i18n:govoplan-ops.surface.run_probes": "Betriebsprüfungen ausführen",
|
||||||
|
"i18n:govoplan-ops.surface.drain_node": "Laufzeitknoten leeren",
|
||||||
|
"i18n:govoplan-ops.reason.loading": "Der Betriebsstatus wird geladen.",
|
||||||
|
"i18n:govoplan-ops.reason.running_probes": "Betriebsprüfungen werden bereits ausgeführt.",
|
||||||
|
"i18n:govoplan-ops.reason.run_permission_required": "Die Berechtigung zum Ausführen von Betriebsprüfungen ist erforderlich.",
|
||||||
|
"i18n:govoplan-ops.reason.node_action_active": "Für diesen Knoten wird bereits eine Lebenszyklusaktion ausgeführt.",
|
||||||
|
"i18n:govoplan-ops.reason.stale_node": "Ein veralteter Knoten kann nicht geleert werden, weil seine aktuelle Instanz keine Statusmeldungen sendet.",
|
||||||
|
"i18n:govoplan-ops.reason.stopped_node": "Ein gestoppter Knoten kann keine Lebenszyklusaktionen annehmen.",
|
||||||
|
"i18n:govoplan-ops.readiness_blocked_summary": "Die Laufzeitumgebung ist nicht für normalen Datenverkehr bereit.",
|
||||||
|
"i18n:govoplan-ops.readiness_blocked_details": "{value0} Bereitschaftsblocker sind aktiv.",
|
||||||
|
"i18n:govoplan-ops.readiness_blocked_action": "Prüfen Sie Systemprüfungen, Laufzeitcluster und Wiederherstellungsnachweise der zuständigen Module, bevor der Datenverkehr wieder freigegeben wird.",
|
||||||
|
"i18n:govoplan-ops.operations_operator": "Betriebsverantwortliche Person",
|
||||||
|
"i18n:govoplan-ops.readiness_blocked_target": "Systemprüfungen, Laufzeitcluster und Wiederherstellungsnachweise weiter unten",
|
||||||
|
"i18n:govoplan-ops.lifecycle_actions_for_value": "Lebenszyklusaktionen für {value0}",
|
||||||
|
"i18n:govoplan-ops.cancel_drain_for_value": "Leeren von {value0} abbrechen",
|
||||||
|
"i18n:govoplan-ops.drain_value": "{value0} leeren",
|
||||||
|
"i18n:govoplan-ops.drain_runtime_node_message": "Keine neue Arbeit mehr an {value0} weiterleiten. Laufende Arbeit darf abgeschlossen werden.",
|
||||||
|
"i18n:govoplan-ops.this_node": "diesen Knoten",
|
||||||
|
"i18n:govoplan-ops.drain_node": "Knoten leeren",
|
||||||
"i18n:govoplan-ops.area.2745deba": "Area",
|
"i18n:govoplan-ops.area.2745deba": "Area",
|
||||||
"i18n:govoplan-ops.baseline.e6ab7982": "Baseline",
|
"i18n:govoplan-ops.baseline.e6ab7982": "Baseline",
|
||||||
"i18n:govoplan-ops.authority.8802e425": "Berechtigungen",
|
"i18n:govoplan-ops.authority.8802e425": "Berechtigungen",
|
||||||
|
|||||||
+13
-2
@@ -36,11 +36,22 @@ export const opsModule: PlatformWebModule = {
|
|||||||
optionalDependencies: ["audit", "docs", "notifications"],
|
optionalDependencies: ["audit", "docs", "notifications"],
|
||||||
translations,
|
translations,
|
||||||
viewSurfaces: [
|
viewSurfaces: [
|
||||||
|
{ id: "ops.navigation", moduleId: "ops", kind: "navigation", label: "i18n:govoplan-ops.surface.navigation", order: 10 },
|
||||||
|
{ id: "ops.page", moduleId: "ops", kind: "route", label: "i18n:govoplan-ops.surface.page", order: 20 },
|
||||||
|
{ id: "ops.page.summary", moduleId: "ops", kind: "section", label: "i18n:govoplan-ops.surface.summary", parentId: "ops.page", order: 10 },
|
||||||
|
{ id: "ops.page.health", moduleId: "ops", kind: "section", label: "i18n:govoplan-ops.surface.health", parentId: "ops.page", order: 20 },
|
||||||
|
{ id: "ops.page.runtime", moduleId: "ops", kind: "section", label: "i18n:govoplan-ops.surface.runtime", parentId: "ops.page", order: 30 },
|
||||||
|
{ id: "ops.page.recovery", moduleId: "ops", kind: "section", label: "i18n:govoplan-ops.surface.recovery", parentId: "ops.page", order: 40 },
|
||||||
|
{ id: "ops.page.governance", moduleId: "ops", kind: "section", label: "i18n:govoplan-ops.surface.governance", parentId: "ops.page", order: 50 },
|
||||||
|
{ id: "ops.page.deployment", moduleId: "ops", kind: "section", label: "i18n:govoplan-ops.surface.deployment", parentId: "ops.page", order: 60 },
|
||||||
|
{ id: "ops.page.sizing", moduleId: "ops", kind: "section", label: "i18n:govoplan-ops.surface.sizing", parentId: "ops.page", order: 70 },
|
||||||
|
{ id: "ops.action.run-probes", moduleId: "ops", kind: "action", label: "i18n:govoplan-ops.surface.run_probes", parentId: "ops.page.health", order: 80 },
|
||||||
|
{ id: "ops.action.drain-node", moduleId: "ops", kind: "action", label: "i18n:govoplan-ops.surface.drain_node", parentId: "ops.page.runtime", order: 90 },
|
||||||
{ id: "ops.widget.health", moduleId: "ops", kind: "section", label: "Operations health widget", order: 100 }
|
{ id: "ops.widget.health", moduleId: "ops", kind: "section", label: "Operations health widget", order: 100 }
|
||||||
],
|
],
|
||||||
navItems: [{ to: "/ops", label: "i18n:govoplan-ops.ops.907a54c2", iconName: "activity", anyOf: opsReadScopes, order: 890 }],
|
navItems: [{ to: "/ops", label: "i18n:govoplan-ops.ops.907a54c2", iconName: "activity", anyOf: opsReadScopes, order: 890, surfaceId: "ops.navigation" }],
|
||||||
routes: [
|
routes: [
|
||||||
{ path: "/ops", anyOf: opsReadScopes, order: 890, render: ({ settings, auth }) => createElement(OpsPage, { settings, auth }) }],
|
{ path: "/ops", anyOf: opsReadScopes, order: 890, surfaceId: "ops.page", render: ({ settings, auth }) => createElement(OpsPage, { settings, auth }) }],
|
||||||
uiCapabilities: {
|
uiCapabilities: {
|
||||||
"dashboard.widgets": dashboardWidgets
|
"dashboard.widgets": dashboardWidgets
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user