22 Commits
Author SHA1 Message Date
zemion ce07404405 Release v0.1.15
Module Package Release / publish-packages (push) Successful in 11s
2026-08-04 15:18:12 +02:00
zemion 7228ecad52 Make package publication retries hash-safe 2026-08-04 14:32:20 +02:00
zemion 017d93dcbd Harden module package publication 2026-08-04 14:02:41 +02:00
zemion fee7d0e833 Add protected package release workflow 2026-08-04 04:14:06 +02:00
zemion 2b3264372b Migrate Ops interface patterns 2026-08-03 15:18:32 +02:00
zemion cdcb477b55 Document module lifecycle recovery operations 2026-08-03 07:02:23 +02:00
zemion 0b4e601719 Document workflow recovery operations 2026-08-03 06:37:45 +02:00
zemion e5f4021de9 Document Dataflow recovery operations 2026-08-03 06:09:52 +02:00
zemion e3e0fdaab5 Document provider recovery reconciliation 2026-08-03 05:00:37 +02:00
zemion 63f393afcb Distinguish verified recovery rejections 2026-08-03 03:48:20 +02:00
zemion 0105fd49f5 Expose sanitized backup restore evidence 2026-08-03 02:02:36 +02:00
zemion 1426fd96b1 Complete operational monitoring summary 2026-08-03 00:21:06 +02:00
zemion b464d016b2 feat: expose multi-host runtime readiness 2026-08-02 05:29:47 +02:00
zemion c241085806 feat: expose institutional architecture and runtime state 2026-08-01 17:48:37 +02:00
zemion 05ce4dc8ec Add module probes and worker telemetry 2026-07-31 22:48:07 +02:00
zemion b84cab1aa4 feat: add governance operations dashboard 2026-07-31 04:21:34 +02:00
zemion 1ec47647ab Align Ops WebUI runtime dependencies 2026-07-31 02:48:56 +02:00
zemion 2f11f08b72 chore(webui): require patched React Router 2026-07-29 14:32:54 +02:00
zemion 430a48402f fix: make operations page scrollable 2026-07-28 22:50:11 +02:00
zemion 4d5bb22de5 Declare operations View surfaces 2026-07-28 21:04:54 +02:00
zemion b24eaa12ee refactor(webui): use central data grids 2026-07-21 12:06:44 +02:00
zemion 1e67ec2244 fix(release): align package version metadata 2026-07-21 12:06:39 +02:00
21 changed files with 3148 additions and 116 deletions
+270
View File
@@ -0,0 +1,270 @@
name: Module Package Release
on:
push:
tags:
- "v*"
workflow_dispatch:
inputs:
release_tag:
description: Existing protected version tag to publish
required: true
type: string
jobs:
publish-packages:
runs-on: ubuntu-latest
env:
GITEA_REPOSITORY: ${{ gitea.repository }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
with:
fetch-depth: 0
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065
with:
python-version: "3.12"
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: "22"
- name: Select and validate protected release tag
shell: bash
env:
REQUESTED_TAG: ${{ inputs.release_tag }}
TRIGGER_TAG: ${{ gitea.ref_name }}
run: |
set -euo pipefail
tag="${REQUESTED_TAG:-$TRIGGER_TAG}"
case "$tag" in
v[0-9]*.[0-9]*.[0-9]*) ;;
*) echo "Release tag must start with a SemVer-shaped vX.Y.Z value" >&2; exit 1 ;;
esac
git fetch --force origin "refs/tags/$tag:refs/tags/$tag" refs/heads/main:refs/remotes/origin/main
tag_commit="$(git rev-list -n 1 "$tag")"
git merge-base --is-ancestor "$tag_commit" refs/remotes/origin/main || {
echo "Release tag is not contained in main" >&2
exit 1
}
git checkout --detach "$tag"
printf 'RELEASE_TAG=%s\n' "$tag" >> "$GITEA_ENV"
printf 'SOURCE_DATE_EPOCH=%s\n' "$(git show -s --format=%ct HEAD)" >> "$GITEA_ENV"
- name: Validate package versions
run: |
python - <<'PY'
import json
from pathlib import Path
import os
import re
import tomllib
tag = os.environ["RELEASE_TAG"]
expected = tag.removeprefix("v")
project = tomllib.loads(Path("pyproject.toml").read_text(encoding="utf-8"))["project"]
if project.get("version") != expected:
raise SystemExit(f"pyproject version {project.get('version')!r} does not match {tag}")
if re.fullmatch(r"govoplan-[a-z0-9-]+", str(project.get("name", ""))) is None:
raise SystemExit("Python distribution name must use the govoplan-* namespace")
webui = Path("webui/package.json")
if webui.is_file():
package = json.loads(webui.read_text(encoding="utf-8"))
if package.get("version") != expected:
raise SystemExit(f"WebUI version {package.get('version')!r} does not match {tag}")
if re.fullmatch(r"@govoplan/[a-z0-9-]+-webui", str(package.get("name", ""))) is None:
raise SystemExit("WebUI package name must use the @govoplan/*-webui namespace")
release = Path("webui/package.release.json")
if release.is_file():
release_package = json.loads(release.read_text(encoding="utf-8"))
if (
release_package.get("name") != package.get("name")
or release_package.get("version") != expected
):
raise SystemExit("WebUI release package identity does not match package.json and the release tag")
PY
- name: Build immutable package artifacts
shell: bash
run: |
set -euo pipefail
python -m pip install --disable-pip-version-check build==1.5.0 twine==7.0.0
rm -rf dist .package-webui
python -m build --wheel --outdir dist
python -m twine check dist/*.whl
if [[ -f webui/package.json ]]; then
mkdir .package-webui
cp -a webui/. .package-webui/
rm -rf .package-webui/node_modules .package-webui/dist
if [[ -f .package-webui/package.release.json ]]; then
cp .package-webui/package.release.json .package-webui/package.json
fi
node <<'NODE'
const fs = require("node:fs");
const path = ".package-webui/package.json";
const packageJson = JSON.parse(fs.readFileSync(path, "utf8"));
const groups = ["dependencies", "optionalDependencies", "peerDependencies"];
for (const group of groups) {
for (const [name, specifier] of Object.entries(packageJson[group] || {})) {
if (!name.startsWith("@govoplan/")) continue;
if (typeof specifier !== "string") {
throw new Error(`${group}.${name} must use a string version`);
}
const packageSlug = name.slice("@govoplan/".length);
if (!packageSlug.endsWith("-webui")) {
throw new Error(`${group}.${name} is outside the WebUI package namespace`);
}
const repository = `govoplan-${packageSlug.slice(0, -"-webui".length)}`;
const escapedRepository = repository.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const gitTag = specifier.match(
new RegExp(
`^git\\+(?:ssh://git@|https://)git\\.add-ideas\\.de/(?:GovOPlaN|add-ideas)/${escapedRepository}\\.git#v([0-9]+\\.[0-9]+\\.[0-9]+)$`,
),
);
if (gitTag) {
packageJson[group][name] = gitTag[1];
continue;
}
if (specifier.startsWith("file:") || specifier.startsWith("git+")) {
throw new Error(
`${group}.${name} must resolve to an exact registry version for publication`,
);
}
}
}
delete packageJson.private;
fs.writeFileSync(path, `${JSON.stringify(packageJson, null, 2)}\n`);
NODE
npm pkg delete private --prefix .package-webui
(cd .package-webui && npm pack --ignore-scripts --pack-destination ../dist)
fi
python - <<'PY'
import hashlib
import json
from pathlib import Path
import os
import subprocess
artifacts = []
for path in sorted(Path("dist").iterdir()):
if path.suffix not in {".whl", ".tgz"}:
continue
digest = hashlib.sha256(path.read_bytes()).hexdigest()
artifacts.append({"filename": path.name, "sha256": digest, "size": path.stat().st_size})
payload = {
"schema_version": "1",
"repository": os.environ["GITEA_REPOSITORY"],
"tag": os.environ["RELEASE_TAG"],
"commit": subprocess.check_output(["git", "rev-parse", "HEAD"], text=True).strip(),
"artifacts": artifacts,
}
Path("dist/package-artifacts.json").write_text(
json.dumps(payload, indent=2, sort_keys=True) + "\n",
encoding="utf-8",
)
PY
- name: Retain package hash evidence
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32
with:
name: module-packages-${{ gitea.ref_name }}
path: dist/package-artifacts.json
- name: Check immutable registry state
shell: bash
env:
PACKAGE_TOKEN: ${{ secrets.GOVOPLAN_PACKAGE_TOKEN }}
run: |
set -euo pipefail
test -n "$PACKAGE_TOKEN"
python - <<'PY'
import hashlib
import json
import os
from pathlib import Path
import tomllib
from urllib.error import HTTPError
from urllib.parse import quote
from urllib.request import Request, urlopen
api_root = "https://git.add-ideas.de/api/v1/packages/GovOPlaN"
token = os.environ["PACKAGE_TOKEN"]
def should_publish(kind, name, version, path):
package_url = "/".join(
(api_root, kind, quote(name, safe=""), quote(version, safe=""), "files")
)
request = Request(
package_url,
headers={"Accept": "application/json", "Authorization": f"token {token}"},
)
try:
with urlopen(request, timeout=30) as response:
files = json.load(response)
except HTTPError as exc:
if exc.code == 404:
print(f"{kind} package {name}=={version} is not published yet")
return True
raise
if not isinstance(files, list) or len(files) != 1:
raise SystemExit(
f"immutable {kind} package {name}=={version} has an unexpected file set"
)
expected_sha256 = hashlib.sha256(path.read_bytes()).hexdigest()
if files[0].get("sha256") != expected_sha256:
raise SystemExit(
f"immutable {kind} package {name}=={version} already exists with a different SHA-256"
)
print(f"verified existing {kind} package {name}=={version} ({expected_sha256})")
return False
project = tomllib.loads(Path("pyproject.toml").read_text(encoding="utf-8"))["project"]
wheels = tuple(Path("dist").glob("*.whl"))
if len(wheels) != 1:
raise SystemExit("release build must contain exactly one wheel")
publish_pypi = should_publish(
"pypi", str(project["name"]), str(project["version"]), wheels[0]
)
tarballs = tuple(Path("dist").glob("*.tgz"))
if len(tarballs) > 1:
raise SystemExit("release build must contain at most one npm package")
publish_npm = False
if tarballs:
webui = json.loads(
Path(".package-webui/package.json").read_text(encoding="utf-8")
)
publish_npm = should_publish(
"npm", str(webui["name"]), str(webui["version"]), tarballs[0]
)
with Path(os.environ["GITEA_ENV"]).open("a", encoding="utf-8") as env_file:
env_file.write(f"PUBLISH_PYPI={int(publish_pypi)}\n")
env_file.write(f"PUBLISH_NPM={int(publish_npm)}\n")
PY
- name: Publish wheel and WebUI package
shell: bash
env:
PACKAGE_USERNAME: ${{ secrets.GOVOPLAN_PACKAGE_USERNAME }}
PACKAGE_TOKEN: ${{ secrets.GOVOPLAN_PACKAGE_TOKEN }}
run: |
set -euo pipefail
test -n "$PACKAGE_USERNAME"
test -n "$PACKAGE_TOKEN"
if [[ "$PUBLISH_PYPI" == 1 ]]; then
TWINE_USERNAME="$PACKAGE_USERNAME" TWINE_PASSWORD="$PACKAGE_TOKEN" \
python -m twine upload --non-interactive \
--repository-url https://git.add-ideas.de/api/packages/GovOPlaN/pypi \
dist/*.whl
else
echo "Exact wheel is already present; skipping immutable retry."
fi
shopt -s nullglob
webui_packages=(dist/*.tgz)
if (( ${#webui_packages[@]} )) && [[ "$PUBLISH_NPM" == 1 ]]; then
npmrc="$(mktemp)"
trap 'rm -f "$npmrc"' EXIT
chmod 600 "$npmrc"
printf '%s\n' \
'@govoplan:registry=https://git.add-ideas.de/api/packages/GovOPlaN/npm/' \
"//git.add-ideas.de/api/packages/GovOPlaN/npm/:_authToken=$PACKAGE_TOKEN" \
> "$npmrc"
NPM_CONFIG_USERCONFIG="$npmrc" npm publish "./${webui_packages[0]}" \
--ignore-scripts --access public \
--registry https://git.add-ideas.de/api/packages/GovOPlaN/npm/
elif (( ${#webui_packages[@]} )); then
echo "Exact WebUI package is already present; skipping immutable retry."
fi
+6
View File
@@ -1,5 +1,11 @@
# GovOPlaN Ops Codex Guide
## Documentation Contract
- Treat documentation as part of every behavior change. Update this module's manifest-driven `DocumentationTopic` contributions for affected user and administrator behavior.
- Keep feature content here; `govoplan-docs` projects it without importing Ops internals.
- Maintain a static user/admin baseline and run `/mnt/DATA/git/govoplan/tools/checks/check-manifest-shapes.py` after behavior or manifest changes.
## Scope
This repository owns the GovOPlaN operations module seed: runtime health,
+46
View File
@@ -13,6 +13,19 @@ This repository owns:
- backend module manifest `ops`
- operator-facing status APIs
- runtime-node registration, heartbeat, composition, stale-node, and expected
replica projection
- queue-specific worker-pool coverage, release/composition skew, and the
deployment-rendered PostgreSQL connection budget
- audited API and worker drain/cancel controls
- recovery-operation status and evidence-chain summaries
- sanitized, expiry-aware coordinated backup and isolated-restore status from
the deployment verifier, without backup artifact or key-custody references
- governance inventory for module-declared permissions, roles, capabilities,
policies, documentation, access-control hooks, search providers, and
migration ownership
- sanitized runtime health, freshness, conflict, and recovery state for
configured external-provider bindings
- deployment profile and sizing assumption summaries
- WebUI route contribution `@govoplan/ops-webui`
- future operational runbooks that describe the configured platform rather than
@@ -21,7 +34,40 @@ This repository owns:
Core owns lifecycle management, module discovery, maintenance mode, package
installation safety, and shared WebUI shell behavior.
Core exposes the registry contract but does not own an operations dashboard.
Ops projects the provider-neutral registry metadata and runtime checks into the
operator-facing governance surface.
Provider declarations describe supported behavior; module-owned runtime-state
providers describe the currently configured bindings. An optional provider
failure is isolated and reported as an attention state without suppressing the
rest of the governance inventory. Secrets, endpoints, and raw provider errors
are not part of this projection.
## Runbooks
- `docs/SCALABILITY_PROFILES.md` explains how to use the Ops page with the
canonical sizing matrix, readiness model, and profile-selection worksheet.
The Runtime cluster panel is backed by Core's shared PostgreSQL coordination
tables. A drain request is durable and is observed on the node heartbeat: API
readiness closes and workers stop taking new queue work. The Recovery panel
shows operations requiring forward recovery or manual intervention; it does not
claim that a production database backup exists.
For provider effects, Ops is the status and evidence-chain projection rather
than the reconciliation authority. Follow the owning module's bounded action:
for example, reconcile an unknown Mail SMTP command from provider evidence and
never retry the original effect merely because its caller state is incomplete.
For Dataflow, a database-only run is atomic. A published-output run is forward
recovery: inspect its recorded output digest and sink idempotency key, and do
not start another publication while its state is `outcome_unknown`.
For Workflow Engine, Ops exposes the action operation and evidence chain while
the Workflow handoff remains the reconciliation surface. Verify the provider,
then record **Effect confirmed** to continue without replay or **Effect absent**
to enable a deliberate retry. Instance, trigger-delivery, and timer leases show
which runtime currently owns transition authority across hosts.
For Core module lifecycle, the installer run record supplies the operation id.
Treat `recovery_required` and `outcome_unknown` as a deployment-wide stop: verify
the hashed package/database evidence, complete the declared rollback or forward
repair, and reconcile the operation before another install or live graph change.
The local installer lock is not a substitute for this database fence.
+22
View File
@@ -0,0 +1,22 @@
# Backup Evidence Status
The Ops health surface reports the sanitized result of deployment-side backup
verification. It never reads backup artifacts, provider URLs, credentials,
encryption-key references, signing keys, or orchestrator APIs.
The status can be:
- `verified`: a signed coordinated PostgreSQL, object, configuration, and key
custody recovery point and its isolated restore drill are current;
- `expired`: the retained receipt is no longer fresh enough to authorize a
release-changing migration;
- `absent`: no signed receipt has been adopted;
- `invalid`: the deployment verifier rejected, lost, or only partially received
the evidence set.
The panel shows the recovery point and drill identifiers, capture/expiry times,
component count, and measured RPO/RTO. These values help operators find the
private report in the approved evidence store; they are not themselves a
backup. Resolve an absent, expired, or invalid state through the deployment
runbook and `govoplan-deploy verify-backup --adopt`, then reconcile the runtime
environment. Do not upload provider reports or keys through the application.
+21
View File
@@ -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.
+51 -4
View File
@@ -15,6 +15,12 @@ live Ops page.
campaigns, imports, exports, or workflow automation.
6. Record the current profile and open measurements before moving to a larger
topology.
7. In a replicated profile, compare active non-stale API and worker counts with
configured expectations, and resolve composition skew before rollout.
8. Drain a node before replacement, then verify it is no longer ready/consuming
before terminating it.
9. Inspect recovery-required and outcome-unknown operations; follow the recorded
recovery mode rather than retrying or rolling back blindly.
## Live Profile Signals
@@ -30,10 +36,40 @@ The Ops API reports:
- HTTP/certificate deployment posture through the `deployment_security` check
- readiness blockers
- reference deployment profiles and sizing assumptions
- runtime node identity, role, software/module composition, queues, heartbeat,
stale state, and drain state
- configured versus active API and worker replica counts
- active worker-pool names, exact queue coverage, and missing queue owners
- release/module-composition skew and software-version skew across active nodes
- rendered PostgreSQL connection peak, declared server limit, and operator reserve
- recovery operation status, mode, checkpoint count, and last update
These values are intentionally diagnostic. They do not replace deployment
configuration management, backups, monitoring, or restore drills.
Drain controls are cooperative. API and worker processes observe the request on
their next heartbeat. API readiness then fails; workers cancel their configured
queue consumers but may still be finishing already claimed work. Confirm the
process state and queue evidence before forcefully terminating a node.
Recovery evidence is similarly conservative. A forward-recovery or
manual-intervention record means an operator must repair the current release or
restore a separately verified coordinated backup. Ops does not convert that
state into a safe rollback.
An `outcome_unknown` provider operation must be resolved in its owning module.
Mail SMTP and IMAP APPEND operations, for example, carry stable attempt IDs and
digest-only evidence; use Mail's command reconciliation with provider evidence.
Do not replay the original effect from Ops. Read-only Mail index/source scans
are fenced across nodes and may be repeated only after the prior fence closes.
For the `shared` profile, missing expected replicas, release/composition skew,
unserved queues, or an invalid PostgreSQL connection budget are readiness
errors. Stale historical records remain visible, but cannot downgrade one of
those errors to a warning. A database budget at or above 80 percent is reported
as a warning so operators retain room for measured bursts and administrative
connections.
`deployment_security` is inactive for local/test profiles. In staging or pilot
profiles it warns when secure cookies or CORS origins still look local. In
production it becomes readiness-critical because TLS certificates, proxy
@@ -46,6 +82,14 @@ Promote from local development to a production-like profile when a feature
depends on PostgreSQL, Redis, Celery, module package lifecycle, or durable file
storage.
The Operations page is the canonical monitoring surface. Its summary reports
worker and queue coverage, queue depth, active tasks, local filesystem capacity
when observable, backup/restore evidence, runtime-node skew, and recovery-ledger
operations split into active, failed, recovery-required, and outcome-unknown
states. S3 capacity remains provider-owned unless a configured module check
supplies bounded usage metrics; Ops must not enumerate an object store merely to
render a dashboard.
Promote from a single-process profile to a split-worker profile when queued
work becomes part of normal operation:
@@ -93,23 +137,26 @@ Stateless and horizontally replicable:
- API workers when `MASTER_KEY_B64`, `DATABASE_URL`, storage, queue, and module
configuration are shared
- Background workers when queues and idempotency keys are used
- Scheduler replicas only when leader election or an external lock exists
- Scheduler processes only through Core's renewable PostgreSQL lease and
fencing-token runner; deploy one desired scheduler replica
Stateful or singleton-sensitive:
- PostgreSQL
- local file storage when not replaced by object storage
- local file storage when not replaced by object storage (or a one-host shared
volume under the `host-shared` profile)
- Redis/queue state
- module installer daemon and package mutation operations
- migration execution
- scheduler without distributed locking
- migration execution, although competing jobs are serialized by a PostgreSQL
advisory lock
- outgoing campaign append/send jobs unless claim tokens are enforced
## Readiness And Degraded Modes
| Component | Ready When | Degraded Mode |
| --- | --- | --- |
| API | Database reachable, migrations current, enabled module registry builds, maintenance mode understood | Read-only/admin-only where routes allow it; otherwise fail closed. |
| API | Database reachable, migrations current, enabled module registry builds, maintenance mode understood, and the node is not draining | Read-only/admin-only where routes allow it; otherwise fail closed. |
| WebUI | Static assets match backend module metadata contract | Show unavailable modules/routes with reason; do not invent routes. |
| PostgreSQL | Accepts connections and migration head is current | Block writes and package changes if migration state is unknown. |
| Storage | Configured backend is reachable and writable for write flows | Read-only file views may continue if storage is read-only but reachable. |
+7 -7
View File
@@ -1,6 +1,6 @@
{
"name": "@govoplan/ops-webui",
"version": "0.1.8",
"version": "0.1.15",
"private": true,
"type": "module",
"main": "webui/src/index.ts",
@@ -17,14 +17,14 @@
"README.md"
],
"peerDependencies": {
"@govoplan/core-webui": "^0.1.8",
"@govoplan/core-webui": "^0.1.15",
"lucide-react": "^1.23.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router-dom": "^7.1.1",
"@vitejs/plugin-react": "^4.3.4",
"react": ">=19.2.7 <20",
"react-dom": ">=19.2.7 <20",
"react-router": ">=8.3.0 <9",
"@vitejs/plugin-react": "^5.2.0",
"typescript": "^5.7.2",
"vite": "^6.0.6"
"vite": "^7.3.6"
},
"peerDependenciesMeta": {
"@govoplan/core-webui": {
+3 -3
View File
@@ -4,14 +4,14 @@ build-backend = "setuptools.build_meta"
[project]
name = "govoplan-ops"
version = "0.1.8"
version = "0.1.15"
description = "GovOPlaN operations module for health, deployment profile, and sizing visibility."
readme = "README.md"
requires-python = ">=3.12"
authors = [{ name = "GovOPlaN" }]
dependencies = [
"govoplan-core>=0.1.8",
"govoplan-access>=0.1.8",
"govoplan-core>=0.1.15",
"govoplan-access>=0.1.15",
]
[tool.setuptools.packages.find]
+1 -1
View File
@@ -2,4 +2,4 @@
__all__ = ["__version__"]
__version__ = "0.1.6"
__version__ = "0.1.15"
File diff suppressed because it is too large Load Diff
+271 -7
View File
@@ -1,10 +1,69 @@
from __future__ import annotations
from govoplan_core.core.access import CAPABILITY_AUTH_PERMISSION_EVALUATOR, CAPABILITY_AUTH_PRINCIPAL_RESOLVER
from govoplan_core.core.modules import FrontendModule, FrontendRoute, ModuleContext, ModuleManifest, NavItem, PermissionDefinition, RoleTemplate
from govoplan_core.core.access import (
CAPABILITY_AUTH_PERMISSION_EVALUATOR,
CAPABILITY_AUTH_PRINCIPAL_RESOLVER,
)
from govoplan_core.core.modules import (
DocumentationCondition,
DocumentationTopic,
FrontendModule,
FrontendRoute,
ModuleContext,
ModuleManifest,
NavItem,
PermissionDefinition,
RoleTemplate,
)
from govoplan_core.core.provider_governance import (
ModuleArchitectureDeclaration,
ModuleArchitectureDocumentation,
ModuleMaturityEvidence,
)
from govoplan_core.core.views import ViewSurface
OPS_READ_SCOPE = "ops:operations:read"
OPS_READ_SCOPES = (OPS_READ_SCOPE, "system:settings:read", "admin:settings:read")
OPS_RUN_SCOPE = "ops:operations:run"
OPS_RUN_SCOPES = (OPS_RUN_SCOPE, "system:settings:write")
ARCHITECTURE = ModuleArchitectureDeclaration(
layer="runtime_meta",
kind="operations",
maturity="vertical_slice",
evidence=(
ModuleMaturityEvidence(
kind="test",
reference="tests/test_governance_inventory.py",
summary="Tests safe operational projection of module governance declarations.",
),
ModuleMaturityEvidence(
kind="documentation",
reference="docs/SCALABILITY_PROFILES.md",
summary="Documents operational topology and scaling posture.",
),
ModuleMaturityEvidence(
kind="documentation",
reference="docs/BACKUP_EVIDENCE_STATUS.md",
summary="Documents the sanitized signed backup and restore status projection.",
),
),
known_limits=(
"Provider health observations depend on module-owned operational probes and may be unavailable until configured.",
),
owned_concepts=("operations status projection", "bounded operational probes"),
non_owned_concepts=("domain repair", "external provider credentials"),
documentation=ModuleArchitectureDocumentation(
recovery=(
"docs/SCALABILITY_PROFILES.md",
"docs/BACKUP_EVIDENCE_STATUS.md",
),
operations=(
"docs/SCALABILITY_PROFILES.md",
"docs/BACKUP_EVIDENCE_STATUS.md",
),
),
)
def _permission(scope: str, label: str, description: str) -> PermissionDefinition:
@@ -31,8 +90,11 @@ def _route_factory(context: ModuleContext):
manifest = ModuleManifest(
id="ops",
name="Ops",
version="0.1.8",
required_capabilities=(CAPABILITY_AUTH_PRINCIPAL_RESOLVER, CAPABILITY_AUTH_PERMISSION_EVALUATOR),
version="0.1.15",
required_capabilities=(
CAPABILITY_AUTH_PRINCIPAL_RESOLVER,
CAPABILITY_AUTH_PERMISSION_EVALUATOR,
),
optional_dependencies=("audit", "docs", "notifications"),
permissions=(
_permission(
@@ -40,6 +102,11 @@ manifest = ModuleManifest(
"View operations status",
"Read runtime health, deployment profile, and sizing information.",
),
_permission(
OPS_RUN_SCOPE,
"Run operational checks",
"Run bounded module-owned persistence and integration probes.",
),
),
role_templates=(
RoleTemplate(
@@ -49,15 +116,212 @@ manifest = ModuleManifest(
permissions=(OPS_READ_SCOPE,),
level="system",
),
RoleTemplate(
slug="ops_operator",
name="Operations operator",
description="Read platform health and run bounded operational probes.",
permissions=(OPS_READ_SCOPE, OPS_RUN_SCOPE),
level="system",
),
),
documentation=(
DocumentationTopic(
id="ops.health-governance-and-sizing",
title="Inspect platform health and deployment posture",
summary="Ops combines module-owned health checks with deployment profile, governance inventory, worker assumptions, and sizing guidance.",
body="Read-only status distinguishes configured capabilities from healthy integrations. Authorized operators can run bounded probes; a probe must not perform unbounded business work or silently repair data. Use readiness and worker results when diagnosing a node, and use the deployment profile and sizing assumptions when planning horizontal capacity.",
documentation_types=("admin", "user"),
audience=("operator", "system_admin"),
related_modules=("audit", "docs", "notifications"),
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(
id="ops.runtime-coordination-and-recovery",
title="Drain runtime nodes and inspect recovery evidence",
summary="Ops projects shared runtime heartbeats, replica gaps, drain controls, and recovery states that require operator attention.",
body="Use the runtime table to identify stale or composition-skewed API and worker replicas. Drain before replacement so API readiness closes and workers stop taking new queue work; cancellation is available while the node is still draining. The recovery table reports durable Core recovery operations. A rejected operation is a verified provider rejection and needs no recovery; outcome-unknown and recovery-required operations still require reconciliation through the owning module. Core module-lifecycle entries block every later install or live graph change: use the installer run id to verify package, backup, migration, and health evidence before rollback or forward repair. Mail SMTP and IMAP APPEND entries use stable attempt identifiers and digest-only evidence: reconcile the Mail command from provider evidence, never by replaying the original effect from Ops. Dataflow database-only runs are atomic, while published-output runs use forward recovery: reconcile the recorded output digest and sink idempotency key before allowing another publication. Backup status separately projects only the sanitized deployment verification receipt: a verified status identifies a coordinated recovery point and isolated restore drill, while absent, expired, or invalid evidence blocks a release-changing migration.",
documentation_types=("admin", "user"),
audience=("operator", "system_admin"),
conditions=(
DocumentationCondition(
required_modules=("ops",),
any_scopes=OPS_READ_SCOPES,
),
),
related_modules=("audit", "notifications"),
metadata={
"kind": "workflow",
"route": "/ops",
"screen": "Runtime cluster and recovery evidence",
"steps": [
"Compare active non-stale nodes with the configured API and worker replica expectations.",
"Request drain and wait for the node to report draining before replacing it.",
"Inspect every recovery-required, outcome-unknown, or manual-intervention operation and follow its recorded recovery mode.",
"Confirm that backup evidence is verified and current before authorizing a release-changing migration.",
"Verify replacement composition, readiness, queue consumers, and recovery evidence before closing the operation.",
],
"limitations": [
"Drain is observed on the runtime heartbeat interval and does not forcibly terminate active work.",
"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.",
],
"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",
},
},
),
),
route_factory=_route_factory,
nav_items=(NavItem(path="/ops", label="Ops", icon="activity", required_any=OPS_READ_SCOPES, order=890),),
nav_items=(
NavItem(
path="/ops",
label="Ops",
icon="activity",
required_any=OPS_READ_SCOPES,
order=890,
),
),
frontend=FrontendModule(
module_id="ops",
package_name="@govoplan/ops-webui",
routes=(FrontendRoute(path="/ops", component="OpsPage", required_any=OPS_READ_SCOPES, order=890),),
nav_items=(NavItem(path="/ops", label="Ops", icon="activity", required_any=OPS_READ_SCOPES, order=890),),
routes=(
FrontendRoute(
path="/ops",
component="OpsPage",
required_any=OPS_READ_SCOPES,
order=890,
),
),
nav_items=(
NavItem(
path="/ops",
label="Ops",
icon="activity",
required_any=OPS_READ_SCOPES,
order=890,
),
),
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(
id="ops.widget.health",
module_id="ops",
kind="section",
label="Operations health widget",
order=100,
),
),
),
architecture=ARCHITECTURE,
)
+167
View File
@@ -0,0 +1,167 @@
from __future__ import annotations
import unittest
from govoplan_core.core.modules import (
CapabilityDocumentation,
DocumentationTopic,
MigrationSpec,
ModuleManifest,
PermissionDefinition,
RoleTemplate,
)
from govoplan_core.core.registry import PlatformRegistry
from govoplan_core.core.provider_governance import (
ExternalProviderDeclaration,
ModuleArchitectureDeclaration,
ModuleArchitectureDocumentation,
ModuleMaturityEvidence,
ProviderBehaviorDeclaration,
ProviderObjectDeclaration,
)
from govoplan_ops.backend.api.v1.routes import _governance_inventory
class GovernanceInventoryTests(unittest.TestCase):
def test_inventory_projects_manifest_governance_without_provider_internals(self) -> None:
registry = PlatformRegistry()
registry.register(ModuleManifest(
id="example",
name="Example",
version="1.2.3",
permissions=(
PermissionDefinition(
scope="example:item:read",
label="Read examples",
description="Read example records.",
category="Examples",
level="tenant",
module_id="example",
resource="item",
action="read",
),
),
role_templates=(
RoleTemplate(
slug="example_reader",
name="Example reader",
description="Reads examples.",
permissions=("example:item:read",),
),
),
capability_factories={
"example.lookup": lambda _context: object(),
"policy.example": lambda _context: object(),
},
capability_documentation={
"example.lookup": CapabilityDocumentation(
label="Example lookup",
summary="Resolves examples.",
),
},
documentation=(
DocumentationTopic(
id="example.reference",
title="Example reference",
summary="Documents the example module.",
),
),
migration_spec=MigrationSpec(module_id="example"),
architecture=ModuleArchitectureDeclaration(
layer="governance_accountability",
kind="governance",
maturity="vertical_slice",
evidence=(
ModuleMaturityEvidence(
kind="test",
reference="tests/test_governance_inventory.py",
summary="Exercises the governance projection.",
),
ModuleMaturityEvidence(
kind="documentation",
reference="example.reference",
summary="Documents the example provider.",
),
),
known_limits=("The example has no runtime implementation.",),
supported_authority_modes=("external_mirror",),
owned_concepts=("example projections",),
documentation=ModuleArchitectureDocumentation(
operations=("example.reference",),
),
),
external_providers=(
ExternalProviderDeclaration(
id="example.read",
module_id="example",
label="Example reader",
maturity="read",
operations=("read", "search"),
objects=(
ProviderObjectDeclaration(
object_type="example.record",
field_groups=("identity", "summary"),
authority_modes=("external_mirror",),
default_authority_mode="external_mirror",
),
),
behavior=ProviderBehaviorDeclaration(
freshness="Request-time read with a five-minute cache.",
health="Reported through example.health.",
max_read_items=100,
outage="Reads fail closed and retain the last inspected snapshot.",
classifications=("internal",),
purposes=("operations",),
retention="No provider-owned payload is retained.",
),
documentation_topic_ids=("example.reference",),
),
),
))
payload = _governance_inventory(
registry,
external_provider_states={
"example.read": {
"configured": True,
"active": True,
"health": "warning",
"freshness": "stale",
"recovery": "attention",
}
},
)
self.assertEqual(payload["summary"]["module_count"], 1)
self.assertEqual(payload["summary"]["permission_count"], 1)
self.assertEqual(payload["summary"]["role_template_count"], 1)
self.assertEqual(payload["summary"]["capability_count"], 2)
self.assertEqual(payload["summary"]["policy_count"], 1)
self.assertEqual(payload["summary"]["documented_module_count"], 1)
self.assertEqual(payload["summary"]["migration_module_count"], 1)
self.assertEqual(payload["summary"]["architecture_declared_module_count"], 1)
self.assertEqual(payload["summary"]["external_provider_count"], 1)
self.assertEqual(
payload["summary"]["configured_external_provider_count"],
1,
)
self.assertEqual(payload["summary"]["provider_attention_count"], 1)
self.assertEqual(payload["modules"][0]["architecture"]["maturity"], "vertical_slice")
self.assertEqual(payload["modules"][0]["external_providers"][0]["id"], "example.read")
self.assertEqual(
payload["modules"][0]["external_providers"][0]["runtime_state"][
"freshness"
],
"stale",
)
self.assertEqual(payload["modules"][0]["module_id"], "example")
self.assertNotIn("capability_factories", payload["modules"][0])
self.assertFalse(any(
callable(value)
for value in payload["modules"][0].values()
))
if __name__ == "__main__":
unittest.main()
@@ -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()
+213
View File
@@ -0,0 +1,213 @@
from __future__ import annotations
from dataclasses import dataclass
from datetime import UTC, datetime, timedelta
from pathlib import Path
from govoplan_core.core.operations import (
OperationalCheck,
OperationalCheckProviderRegistration,
)
from govoplan_ops.backend.api.v1 import routes
@dataclass
class _Manifest:
operational_check_providers: tuple[OperationalCheckProviderRegistration, ...]
class _Registry:
def __init__(self, *registrations: OperationalCheckProviderRegistration):
self._manifest = _Manifest(tuple(registrations))
def manifests(self):
return (self._manifest,)
def test_module_operational_checks_cache_and_force() -> None:
routes._module_check_cache.clear()
calls = 0
def provider() -> OperationalCheck:
nonlocal calls
calls += 1
return OperationalCheck("example.roundtrip", "Example", "ok", "Passed")
registration = OperationalCheckProviderRegistration(
module_id="example",
check_id="example.roundtrip",
provider=provider,
)
registry = _Registry(registration)
assert routes._module_operational_checks(registry, force=False)[0]["state"] == "ok"
assert routes._module_operational_checks(registry, force=False)[0]["state"] == "ok"
assert calls == 1
routes._module_operational_checks(registry, force=True)
assert calls == 2
def test_module_operational_check_failure_is_isolated() -> None:
routes._module_check_cache.clear()
def provider() -> OperationalCheck:
raise RuntimeError("secret detail")
result = routes._module_operational_checks(
_Registry(
OperationalCheckProviderRegistration(
module_id="example",
check_id="example.failed",
provider=provider,
)
),
force=True,
)[0]
assert result["state"] == "error"
assert "secret detail" not in result["detail"]
def test_shared_runtime_cluster_missing_replicas_blocks_readiness() -> None:
check = routes._runtime_cluster_check(
{
"available": True,
"state_profile": "shared",
"nodes": [
{"role": "api", "state": "active", "stale": False},
{"role": "worker", "state": "active", "stale": True},
],
"expected": {"api": 2, "worker": 1},
"active": {"api": 1, "worker": 0},
"recovery": {"requires_attention": 1},
}
)
assert check["state"] == "error"
assert check["readiness_critical"] is True
assert check["metrics"]["recovery_required"] == 1
def test_shared_runtime_cluster_skew_or_unserved_queue_blocks_readiness() -> None:
check = routes._runtime_cluster_check(
{
"available": True,
"state_profile": "shared",
"nodes": [],
"expected": {"api": 2, "worker": 2},
"active": {"api": 2, "worker": 2},
"composition": {"skewed": True},
"software_versions": {"skewed": False},
"queues": {"missing": ["calendar"]},
"recovery": {"requires_attention": 0},
}
)
assert check["state"] == "error"
assert check["readiness_critical"] is True
assert check["metrics"]["composition_skewed"] is True
assert check["metrics"]["missing_queues"] == 1
def test_database_capacity_check_enforces_shared_rendered_budget(
monkeypatch,
) -> None:
monkeypatch.setattr(routes.core_settings, "state_profile", "shared")
monkeypatch.setattr(routes.core_settings, "database_connection_limit", 100)
monkeypatch.setattr(routes.core_settings, "database_connection_reserve", 10)
monkeypatch.setattr(routes.core_settings, "database_connection_available", 90)
monkeypatch.setattr(routes.core_settings, "database_connection_peak", 70)
healthy = routes._database_capacity_check()
assert healthy["state"] == "ok"
assert healthy["metrics"]["peak"] == 70
monkeypatch.setattr(routes.core_settings, "database_connection_peak", 91)
overrun = routes._database_capacity_check()
assert overrun["state"] == "error"
assert overrun["readiness_critical"] is True
def test_recovery_metrics_separate_failure_unknown_and_active_work() -> None:
metrics = routes._recovery_metrics(
[
{"status": "running"},
{"status": "failed"},
{"status": "outcome_unknown"},
{"status": "recovery_required"},
{"status": "manual_intervention"},
]
)
assert metrics == {
"failed": 2,
"outcome_unknown": 1,
"recovery_required": 1,
"active": 1,
"requires_attention": 4,
}
def test_local_storage_capacity_reports_bounded_filesystem_metrics(
tmp_path: Path,
) -> None:
metrics = routes._local_storage_capacity(tmp_path)
assert metrics["backend"] == "local"
assert metrics["capacity_observable"] is True
assert metrics["capacity_total_bytes"] > 0
assert 0 <= metrics["capacity_used_percent"] <= 100
def test_deployer_backup_receipt_reports_verified_and_expired_state(
monkeypatch,
) -> None:
now = datetime.now(UTC)
values = {
"GOVOPLAN_BACKUP_EVIDENCE_STATE": "verified",
"GOVOPLAN_BACKUP_EVIDENCE_ID": "evidence-1",
"GOVOPLAN_BACKUP_RECOVERY_POINT_ID": "recovery-1",
"GOVOPLAN_BACKUP_RESTORE_DRILL_ID": "drill-1",
"GOVOPLAN_BACKUP_EVIDENCE_SHA256": "a" * 64,
"GOVOPLAN_BACKUP_RELEASE_MANIFEST_SHA256": "b" * 64,
"GOVOPLAN_BACKUP_CAPTURED_AT": (now - timedelta(hours=2)).isoformat(),
"GOVOPLAN_BACKUP_EXPIRES_AT": (now + timedelta(hours=2)).isoformat(),
"GOVOPLAN_BACKUP_RESTORE_STARTED_AT": (
now - timedelta(hours=1, minutes=5)
).isoformat(),
"GOVOPLAN_BACKUP_RESTORE_COMPLETED_AT": (now - timedelta(hours=1)).isoformat(),
"GOVOPLAN_BACKUP_VERIFIED_AT": (now - timedelta(minutes=30)).isoformat(),
"GOVOPLAN_BACKUP_MEASURED_RPO_SECONDS": "120",
"GOVOPLAN_BACKUP_MEASURED_RTO_SECONDS": "300",
"GOVOPLAN_BACKUP_COMPONENT_COUNT": "4",
}
for key, value in values.items():
monkeypatch.setenv(key, value)
verified = routes._backup_restore_check()
assert verified["state"] == "ok"
assert verified["metrics"]["recovery_point_id"] == "recovery-1"
assert verified["metrics"]["measured_rto_seconds"] == 300
monkeypatch.setenv(
"GOVOPLAN_BACKUP_EXPIRES_AT",
(now - timedelta(minutes=1)).isoformat(),
)
expired = routes._backup_restore_check()
assert expired["state"] == "warning"
assert expired["metrics"]["evidence_state"] == "expired"
def test_deployer_backup_receipt_fails_closed_without_exposing_private_evidence(
monkeypatch,
) -> None:
monkeypatch.setenv("GOVOPLAN_BACKUP_EVIDENCE_STATE", "invalid")
invalid = routes._backup_restore_check()
assert invalid["state"] == "warning"
assert invalid["metrics"] == {
"evidence_state": "invalid",
"restore_drill_ok": False,
}
assert "artifact" not in invalid["detail"].lower()
assert "key" not in invalid["detail"].lower()
+7 -7
View File
@@ -1,6 +1,6 @@
{
"name": "@govoplan/ops-webui",
"version": "0.1.8",
"version": "0.1.15",
"private": true,
"type": "module",
"main": "src/index.ts",
@@ -13,14 +13,14 @@
}
},
"peerDependencies": {
"@govoplan/core-webui": "^0.1.8",
"@govoplan/core-webui": "^0.1.15",
"lucide-react": "^1.23.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router-dom": "^7.1.1",
"@vitejs/plugin-react": "^4.3.4",
"react": ">=19.2.7 <20",
"react-dom": ">=19.2.7 <20",
"react-router": ">=8.3.0 <9",
"@vitejs/plugin-react": "^5.2.0",
"typescript": "^5.7.2",
"vite": "^6.0.6"
"vite": "^7.3.6"
},
"peerDependenciesMeta": {
"@govoplan/core-webui": {
+200
View File
@@ -6,6 +6,7 @@ export type OpsCheck = {
state: "ok" | "warning" | "error" | string;
detail: string;
readiness_critical?: boolean;
metrics?: Record<string, unknown>;
};
export type OpsDeploymentProfile = {
@@ -23,6 +24,129 @@ export type OpsSizingAssumption = {
operator_note: string;
};
export type OpsGovernanceModule = {
module_id: string;
name: string;
version: string;
permission_count: number;
role_template_count: number;
capability_count: number;
policy_count: number;
documentation_count: number;
documentation_provider_count: number;
access_control_count: number;
search_provider_count: number;
migration_managed: boolean;
architecture?: {
contract_version: string;
layer: string;
kind: string;
maturity: string;
known_limits: string[];
supported_authority_modes: string[];
owned_concepts: string[];
non_owned_concepts: string[];
evidence: Array<{ kind: string; reference: string; summary: string }>;
} | null;
external_provider_count: number;
external_providers: Array<{
id: string;
label: string;
maturity: string;
authority_modes: string[];
operations: string[];
behavior: { outage?: string | null };
runtime_state?: {
configured: boolean;
active: boolean;
authority_mode?: string | null;
authority_modes?: string[];
health: string;
freshness: string;
conflict: string;
recovery: string;
observed_at: string;
last_success_at?: string | null;
bindings?: Array<{
binding_ref: string;
authority_mode: string;
active: boolean;
health: string;
freshness: string;
conflict: string;
recovery: string;
}>;
} | null;
}>;
};
export type OpsRuntimeNode = {
node_id: string;
incarnation: string;
role: string;
software_version: string;
composition_hash: string;
queues: string[];
state: "active" | "draining" | "stopped" | string;
started_at: string;
last_heartbeat_at: string;
drain_requested_at?: string | null;
drain_reason?: string | null;
stopped_at?: string | null;
stale: boolean;
};
export type OpsRecoveryOperation = {
id: string;
module_id: string;
operation_type: string;
resource_type?: string | null;
resource_id?: string | null;
mode: string;
status: string;
checkpoint_count: number;
evidence_head_sha256?: string | null;
failure_summary?: string | null;
updated_at: string;
};
export type OpsRuntimeCluster = {
available: boolean;
detail: string;
installation_id?: string;
state_profile: string;
expected?: { api: number; worker: number };
active?: { api: number; worker: number };
composition?: {
expected_hash?: string | null;
active_hashes: string[];
unexpected_nodes: string[];
skewed: boolean;
};
software_versions?: {
active: string[];
skewed: boolean;
};
queues?: {
expected: string[];
active: string[];
missing: string[];
worker_pools: string[];
};
nodes: OpsRuntimeNode[];
recovery: {
operations: OpsRecoveryOperation[];
requires_attention: number;
metrics?: {
failed?: number;
outcome_unknown?: number;
recovery_required?: number;
active?: number;
requires_attention?: number;
};
};
};
export type OpsStatus = {
summary: {
app_env: string;
@@ -36,7 +160,32 @@ export type OpsStatus = {
message?: string | null;
};
database_url: string;
database_connection_peak?: number | null;
database_connection_available?: number | null;
file_storage_backend: string;
storage_metrics?: {
backend?: string;
capacity_observable?: boolean;
capacity_total_bytes?: number;
capacity_used_bytes?: number;
capacity_free_bytes?: number;
capacity_used_percent?: number;
};
backup_state?: string;
worker_metrics: {
workers?: number;
active_tasks?: number;
expected_queues?: string[];
active_queues?: string[];
missing_queues?: string[];
queue_depths?: Record<string, number>;
};
operational_probe_count: number;
runtime_node_count: number;
recovery_required_count: number;
failed_operation_count?: number;
outcome_unknown_count?: number;
active_operation_count?: number;
};
readiness: {
ready: boolean;
@@ -49,10 +198,61 @@ export type OpsStatus = {
}>;
};
checks: OpsCheck[];
governance: {
summary: {
module_count: number;
permission_count: number;
role_template_count: number;
capability_count: number;
policy_count: number;
documented_module_count: number;
access_control_count: number;
search_provider_count: number;
migration_module_count: number;
architecture_declared_module_count: number;
external_provider_count: number;
configured_external_provider_count: number;
provider_attention_count: number;
supported_module_count: number;
};
modules: OpsGovernanceModule[];
};
deployment_profiles: OpsDeploymentProfile[];
sizing: OpsSizingAssumption[];
runtime_cluster: OpsRuntimeCluster;
};
export function fetchOpsStatus(settings: ApiSettings): Promise<OpsStatus> {
return apiFetch(settings, "/api/v1/ops/status");
}
export function runOpsChecks(settings: ApiSettings): Promise<OpsStatus> {
return apiFetch(settings, "/api/v1/ops/checks/run", { method: "POST" });
}
export function drainRuntimeNode(
settings: ApiSettings,
nodeId: string,
reason = "operator request"
): Promise<{ node_id: string; state: string; drain_reason: string }> {
return apiFetch(
settings,
`/api/v1/ops/runtime/nodes/${encodeURIComponent(nodeId)}/drain`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ reason })
}
);
}
export function cancelRuntimeNodeDrain(
settings: ApiSettings,
nodeId: string
): Promise<{ node_id: string; state: string }> {
return apiFetch(
settings,
`/api/v1/ops/runtime/nodes/${encodeURIComponent(nodeId)}/drain`,
{ method: "DELETE" }
);
}
+18 -2
View File
@@ -1,6 +1,7 @@
import { useEffect, useState } from "react";
import {
DismissibleAlert,
DocumentationHelpLink,
LoadingFrame,
MetricCard,
StatusBadge,
@@ -8,6 +9,7 @@ import {
type ApiSettings
} from "@govoplan/core-webui";
import { fetchOpsStatus, type OpsStatus } from "../../api/ops";
import { OPS_DOCUMENTATION } from "./interfacePatterns";
export default function OpsHealthWidget({ settings, refreshKey }: { settings: ApiSettings; refreshKey: number }) {
const [status, setStatus] = useState<OpsStatus | null>(null);
@@ -29,13 +31,28 @@ export default function OpsHealthWidget({ settings, refreshKey }: { settings: Ap
const warningCount = checks.filter((item) => item.state === "warning").length;
const errorCount = checks.filter((item) => item.state === "error").length;
const ready = status?.readiness.ready ?? false;
const workerMetrics = status?.summary.worker_metrics;
const queueDepths = workerMetrics?.queue_depths ?? {};
const queuedTasks = Object.values(queueDepths).reduce((sum, value) => sum + value, 0);
const missingQueues = workerMetrics?.missing_queues ?? [];
const storageUsage = status?.summary.storage_metrics?.capacity_used_percent;
return (
<LoadingFrame loading={loading} label="Loading operations status">
{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">
<MetricCard label="Readiness" value={ready ? "ready" : "blocked"} tone={ready ? "good" : "danger"} detail={status?.readiness.profile ?? "-"} />
<MetricCard label="Workers" value={status?.summary.celery_enabled ? "split" : "off"} tone={status?.summary.celery_enabled ? "good" : "warning"} detail={status?.summary.celery_queues?.length ? status.summary.celery_queues.join(", ") : "single-process"} />
<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="Active tasks" value={workerMetrics?.active_tasks ?? 0} tone="info" detail={status?.summary.celery_enabled ? "Reported by live workers" : "Workers disabled"} />
<MetricCard label="Queued tasks" value={queuedTasks} tone={queuedTasks ? "warning" : "neutral"} detail={Object.keys(queueDepths).length ? `${Object.keys(queueDepths).length} measured queue(s)` : "Queue depth unavailable"} />
<MetricCard label="Storage" value={storageUsage === undefined ? status?.summary.file_storage_backend ?? "-" : `${storageUsage}%`} tone={storageUsage !== undefined && storageUsage >= 90 ? "danger" : storageUsage !== undefined && storageUsage >= 75 ? "warning" : "neutral"} detail="Managed Files backend capacity" />
<MetricCard label="Failed" value={status?.summary.failed_operation_count ?? 0} tone={status?.summary.failed_operation_count ? "danger" : "good"} detail="Recovery-ledger operations" />
<MetricCard label="Unknown" value={status?.summary.outcome_unknown_count ?? 0} tone={status?.summary.outcome_unknown_count ? "danger" : "good"} detail="Outcome-unknown operations" />
<MetricCard label="Backup" value={status?.summary.backup_state ?? "unknown"} tone={status?.summary.backup_state === "ok" ? "good" : "warning"} detail="Backup and restore evidence" />
<MetricCard label="Probes" value={status?.summary.operational_probe_count ?? 0} tone="neutral" detail="Module-owned operational checks" />
<MetricCard label="Warnings" value={warningCount + errorCount} tone={errorCount ? "danger" : warningCount ? "warning" : "good"} detail="Current health checks" />
</div>
{status?.readiness.blockers.length ?
@@ -51,4 +68,3 @@ export default function OpsHealthWidget({ settings, refreshKey }: { settings: Ap
}
</LoadingFrame>);
}
+466 -41
View File
@@ -1,22 +1,52 @@
import { useEffect, useState } from "react";
import { RefreshCw } from "lucide-react";
import { PauseCircle, PlayCircle, RefreshCw } from "lucide-react";
import {
ActionBlockerHint,
Button,
Card,
ConfirmDialog,
DataGrid,
DismissibleAlert,
DocumentationHelpLink,
LoadingFrame,
MetricCard,
PageScrollViewport,
PageTitle,
StatusBadge,
TableActionGroup,
adminErrorMessage,
type ApiSettings } from
hasAnyScope,
i18nMessage,
type ApiSettings,
type AuthInfo,
type DataGridColumn } from
"@govoplan/core-webui";
import { fetchOpsStatus, type OpsCheck, type OpsDeploymentProfile, type OpsSizingAssumption, type OpsStatus } from "../../api/ops";
import {
fetchOpsStatus,
cancelRuntimeNodeDrain,
drainRuntimeNode,
runOpsChecks,
type OpsCheck,
type OpsDeploymentProfile,
type OpsGovernanceModule,
type OpsRecoveryOperation,
type OpsRuntimeNode,
type OpsSizingAssumption,
type OpsStatus
} from "../../api/ops";
import {
OPS_DOCUMENTATION,
OPS_I18N,
OPS_RECOVERY_DOCUMENTATION,
} from "./interfacePatterns";
export default function OpsPage({ settings }: {settings: ApiSettings;}) {
export default function OpsPage({ settings, auth }: {settings: ApiSettings;auth: AuthInfo;}) {
const [status, setStatus] = useState<OpsStatus | null>(null);
const [loading, setLoading] = useState(true);
const [runningProbes, setRunningProbes] = useState(false);
const [error, setError] = useState("");
const [drainTarget, setDrainTarget] = useState<OpsRuntimeNode | null>(null);
const [nodeActionId, setNodeActionId] = useState("");
async function load() {
setLoading(true);
@@ -30,14 +60,68 @@ export default function OpsPage({ settings }: {settings: ApiSettings;}) {
}
}
async function runChecks() {
setRunningProbes(true);
setLoading(true);
setError("");
try {
setStatus(await runOpsChecks(settings));
} catch (err) {
setError(adminErrorMessage(err));
} finally {
setRunningProbes(false);
setLoading(false);
}
}
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 ?? [];
const warningCount = checks.filter((item) => item.state === "warning").length;
const errorCount = checks.filter((item) => item.state === "error").length;
const ready = status?.readiness.ready ?? false;
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 queuedTasks = Object.values(queueDepths).reduce((total, value) => total + value, 0);
const storageUsage = status?.summary.storage_metrics?.capacity_used_percent;
return (
<PageScrollViewport>
<div className="content-pad workspace-data-page">
<div className="page-heading split workspace-heading">
<div>
@@ -45,20 +129,45 @@ export default function OpsPage({ settings }: {settings: ApiSettings;}) {
<p>i18n:govoplan-ops.runtime_health_deployment_profile_worker_split_a.55340156</p>
</div>
<div className="button-row compact-actions">
<Button onClick={() => void load()} disabled={loading}><RefreshCw size={16} /> i18n:govoplan-ops.reload.cce71553</Button>
<DocumentationHelpLink reference={OPS_DOCUMENTATION} />
<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>
{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">
<div className="metric-grid">
<MetricCard label="i18n:govoplan-ops.profile.ff4fc027" value={status?.summary.active_profile ?? "-"} tone="info" detail={status?.summary.database_url ?? "i18n:govoplan-ops.no_database_url.51a2db0c"} />
<MetricCard label="i18n:govoplan-ops.readiness.1db9d6fb" value={ready ? "ready" : "not ready"} tone={ready ? "good" : "danger"} detail={status?.readiness.blockers.length ? `${status.readiness.blockers.length} blocker(s)` : "i18n:govoplan-ops.no_readiness_blockers.0df259bd"} />
<MetricCard label="i18n:govoplan-ops.modules.04e9462c" value={status?.summary.module_count ?? 0} tone="neutral" detail="i18n:govoplan-ops.enabled_in_the_runtime_registry.d2c6142d" />
<MetricCard label="i18n:govoplan-ops.workers.b6ef3acd" value={status?.summary.celery_enabled ? "split" : "off"} tone={status?.summary.celery_enabled ? "good" : "warning"} detail={status?.summary.celery_queues?.length ? status.summary.celery_queues.join(", ") : "i18n:govoplan-ops.celery_worker_setting.323d7737"} />
<MetricCard label="i18n:govoplan-ops.permissions.842c35eb" value={status?.governance.summary.permission_count ?? 0} tone="neutral" detail="i18n:govoplan-ops.declared_governance_permissions.d08d3bf1" />
<MetricCard label="i18n:govoplan-ops.policies.e7800f56" value={status?.governance.summary.policy_count ?? 0} tone="neutral" detail="i18n:govoplan-ops.registered_policy_capabilities.112a2b64" />
<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="Queued tasks" value={queuedTasks} tone={queuedTasks ? "warning" : "good"} detail={Object.keys(queueDepths).length ? `${Object.keys(queueDepths).length} measured queue(s)` : "Queue depth unavailable"} />
<MetricCard label="Storage" value={storageUsage === undefined ? status?.summary.file_storage_backend ?? "-" : `${storageUsage}%`} tone={storageUsage !== undefined && storageUsage >= 90 ? "danger" : storageUsage !== undefined && storageUsage >= 75 ? "warning" : "neutral"} detail={storageMetricDetail(status)} />
<MetricCard label="Backup evidence" value={status?.summary.backup_state ?? "unknown"} tone={status?.summary.backup_state === "ok" ? "good" : "warning"} detail="Latest coordinated backup and restore-drill evidence" />
<MetricCard label="Failed operations" value={status?.summary.failed_operation_count ?? 0} tone={status?.summary.failed_operation_count ? "danger" : "good"} detail="Terminal failures or manual intervention" />
<MetricCard label="Unknown outcomes" value={status?.summary.outcome_unknown_count ?? 0} tone={status?.summary.outcome_unknown_count ? "danger" : "good"} detail={`${status?.summary.active_operation_count ?? 0} active recovery-ledger operation(s)`} />
<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="Database capacity" value={databaseCapacityValue(status)} tone={databaseCapacityTone(status)} detail="Peak pooled connections / available connections" />
<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">
@@ -66,6 +175,24 @@ export default function OpsPage({ settings }: {settings: ApiSettings;}) {
<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>
<Card title="i18n:govoplan-ops.deployment_profiles.b0caa179">
<ProfileList profiles={status?.deployment_profiles ?? []} />
</Card>
@@ -75,10 +202,271 @@ export default function OpsPage({ settings }: {settings: ApiSettings;}) {
</Card>
</div>
</LoadingFrame>
</div>);
<ConfirmDialog
open={Boolean(drainTarget)}
title="i18n:govoplan-ops.surface.drain_node"
message={i18nMessage("i18n:govoplan-ops.drain_runtime_node_message", { value0: drainTarget?.node_id ?? "i18n:govoplan-ops.this_node" })}
confirmLabel="i18n:govoplan-ops.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) => {
const busy = busyNodeId === node.node_id;
const cancelling = node.state === "draining";
const disabledReason = !canManage
? OPS_I18N.runPermissionRequired
: busy
? OPS_I18N.nodeActionActive
: node.state === "stopped"
? OPS_I18N.stoppedNode
: !cancelling && node.stale
? OPS_I18N.staleNode
: undefined;
return (
<TableActionGroup
minimumSlots={1}
label={i18nMessage("i18n:govoplan-ops.lifecycle_actions_for_value", { value0: node.node_id })}
actions={[{
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)
}]}
/>
);
}
}
];
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>[] = [
{
id: "module",
header: "i18n:govoplan-ops.module.b8ff0289",
width: "minmax(200px, 1fr)",
minWidth: 180,
resizable: true,
sortable: true,
filterable: true,
value: (module) => `${module.name} ${module.module_id} ${module.version}`,
render: (module) => <div><strong>{module.name}</strong><span className="muted block">{module.module_id} · {module.version}</span></div>
},
{
id: "authority",
header: "i18n:govoplan-ops.authority.8802e425",
width: "minmax(190px, .8fr)",
minWidth: 170,
resizable: true,
value: (module) => `${module.permission_count} ${module.role_template_count}`,
render: (module) => `${module.permission_count} permissions · ${module.role_template_count} roles`
},
{
id: "contracts",
header: "i18n:govoplan-ops.contracts.57d80902",
width: "minmax(190px, .8fr)",
minWidth: 170,
resizable: true,
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",
width: "minmax(190px, .8fr)",
minWidth: 170,
resizable: true,
value: (module) => `${module.access_control_count} ${module.search_provider_count}`,
render: (module) => `${module.access_control_count} access · ${module.search_provider_count} search`
},
{
id: "evidence",
header: "i18n:govoplan-ops.evidence.7ea014de",
width: "minmax(190px, .8fr)",
minWidth: 170,
resizable: true,
value: (module) => `${module.documentation_count} ${module.documentation_provider_count} ${module.migration_managed}`,
render: (module) => (
<div>
{module.documentation_count + module.documentation_provider_count} docs
<span className="muted block">{module.migration_managed ? "migration managed" : "no module migrations"}</span>
</div>
)
}
];
return (
<DataGrid
id="ops-governance-inventory"
rows={modules}
columns={columns}
getRowKey={(module) => module.module_id}
emptyText="i18n:govoplan-ops.no_modules_reported.847f06d9"
/>
);
}
function CheckList({ checks }: {checks: OpsCheck[];}) {
if (!checks.length) return <p className="muted">i18n:govoplan-ops.no_health_checks_reported.03c067c4</p>;
return (
@@ -95,47 +483,27 @@ function CheckList({ checks }: {checks: OpsCheck[];}) {
function ProfileList({ profiles }: {profiles: OpsDeploymentProfile[];}) {
if (!profiles.length) return <p className="muted">i18n:govoplan-ops.no_deployment_profiles_reported.7c3af1db</p>;
const columns: DataGridColumn<OpsDeploymentProfile>[] = [
{ id: "profile", header: "i18n:govoplan-ops.profile.ff4fc027", width: "minmax(220px, 1fr)", minWidth: 180, resizable: true, sortable: true, filterable: true, value: (profile) => `${profile.name} ${profile.id}`, render: (profile) => <div><strong>{profile.name}</strong><span className="muted block">{profile.id}</span></div> },
{ id: "status", header: "i18n:govoplan-ops.status.bae7d5be", width: 140, sortable: true, filterable: true, value: (profile) => profile.current ? "current" : "reference", render: (profile) => <StatusBadge status={profile.current ? "success" : "inactive"} label={profile.current ? "current" : "reference"} /> },
{ id: "components", header: "i18n:govoplan-ops.components.9289473e", width: "minmax(220px, 1fr)", minWidth: 180, resizable: true, filterable: true, value: (profile) => profile.components.join(" "), render: (profile) => profile.components.join(", ") },
{ id: "fit", header: "i18n:govoplan-ops.fit.dab564d8", width: "minmax(180px, .8fr)", minWidth: 160, resizable: true, filterable: true, value: (profile) => profile.fit }
];
return (
<div className="admin-table-wrap">
<table className="admin-table">
<thead>
<tr><th>i18n:govoplan-ops.profile.ff4fc027</th><th>i18n:govoplan-ops.status.bae7d5be</th><th>i18n:govoplan-ops.components.9289473e</th><th>i18n:govoplan-ops.fit.dab564d8</th></tr>
</thead>
<tbody>
{profiles.map((profile) =>
<tr key={profile.id}>
<td><strong>{profile.name}</strong><span className="muted block">{profile.id}</span></td>
<td><StatusBadge status={profile.current ? "success" : "inactive"} label={profile.current ? "current" : "reference"} /></td>
<td>{profile.components.join(", ")}</td>
<td>{profile.fit}</td>
</tr>
)}
</tbody>
</table>
</div>);
<DataGrid id="ops-deployment-profiles" rows={profiles} columns={columns} getRowKey={(profile) => profile.id} />);
}
function SizingTable({ items }: {items: OpsSizingAssumption[];}) {
if (!items.length) return <p className="muted">i18n:govoplan-ops.no_sizing_assumptions_reported.17515959</p>;
const columns: DataGridColumn<OpsSizingAssumption>[] = [
{ id: "area", header: "i18n:govoplan-ops.area.2745deba", width: "minmax(180px, .7fr)", minWidth: 160, resizable: true, sortable: true, filterable: true, value: (item) => item.area, render: (item) => <strong>{item.area}</strong> },
{ id: "baseline", header: "i18n:govoplan-ops.baseline.e6ab7982", width: "minmax(220px, 1fr)", minWidth: 180, resizable: true, filterable: true, value: (item) => item.baseline },
{ id: "trigger", header: "i18n:govoplan-ops.scale_trigger.1c85e10e", width: "minmax(220px, 1fr)", minWidth: 180, resizable: true, filterable: true, value: (item) => item.scale_trigger },
{ id: "note", header: "i18n:govoplan-ops.operator_note.1dc58f7b", width: "minmax(240px, 1.2fr)", minWidth: 200, resizable: true, filterable: true, value: (item) => item.operator_note }
];
return (
<div className="admin-table-wrap">
<table className="admin-table">
<thead>
<tr><th>i18n:govoplan-ops.area.2745deba</th><th>i18n:govoplan-ops.baseline.e6ab7982</th><th>i18n:govoplan-ops.scale_trigger.1c85e10e</th><th>i18n:govoplan-ops.operator_note.1dc58f7b</th></tr>
</thead>
<tbody>
{items.map((item) =>
<tr key={item.area}>
<td><strong>{item.area}</strong></td>
<td>{item.baseline}</td>
<td>{item.scale_trigger}</td>
<td>{item.operator_note}</td>
</tr>
)}
</tbody>
</table>
</div>);
<DataGrid id="ops-sizing-assumptions" rows={items} columns={columns} getRowKey={(item) => item.area} />);
}
@@ -145,3 +513,60 @@ function stateTone(state: string): string {
if (state === "error") return "error";
return "inactive";
}
function recoveryTone(state: string): string {
if (["succeeded", "recovered"].includes(state)) return "success";
if (state === "rejected") return "warning";
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 databaseCapacityValue(status: OpsStatus | null): string {
const peak = status?.summary.database_connection_peak;
const available = status?.summary.database_connection_available;
return typeof peak === "number" && typeof available === "number"
? `${peak} / ${available}`
: "not declared";
}
function databaseCapacityTone(status: OpsStatus | null): "good" | "warning" | "danger" {
const peak = status?.summary.database_connection_peak;
const available = status?.summary.database_connection_available;
if (typeof peak !== "number" || typeof available !== "number") {
return status?.runtime_cluster.state_profile === "shared" ? "danger" : "warning";
}
if (peak > available) return "danger";
return peak / available >= 0.8 ? "warning" : "good";
}
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;
if (metrics.missing_queues?.length) return `Missing queues: ${metrics.missing_queues.join(", ")}`;
const queued = Object.values(metrics.queue_depths ?? {}).reduce((total, value) => total + value, 0);
return `${metrics.active_tasks ?? 0} active · ${queued} queued`;
}
function storageMetricDetail(status: OpsStatus | null): string {
const metrics = status?.summary.storage_metrics;
if (!metrics?.capacity_observable) {
return `${status?.summary.file_storage_backend ?? "Storage"} capacity is provider-managed or unavailable`;
}
return `${formatBytes(metrics.capacity_used_bytes ?? 0)} used of ${formatBytes(metrics.capacity_total_bytes ?? 0)}`;
}
function formatBytes(value: number): string {
if (!Number.isFinite(value) || value <= 0) return "0 B";
const units = ["B", "KB", "MB", "GB", "TB", "PB"];
const index = Math.min(Math.floor(Math.log(value) / Math.log(1024)), units.length - 1);
return `${(value / 1024 ** index).toFixed(index ? 1 : 0)} ${units[index]}`;
}
@@ -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;
+78
View File
@@ -2,27 +2,66 @@ import type { PlatformTranslations } from "@govoplan/core-webui";
export const generatedTranslations: PlatformTranslations = {
"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.baseline.e6ab7982": "Baseline",
"i18n:govoplan-ops.authority.8802e425": "Authority",
"i18n:govoplan-ops.celery_worker_setting.323d7737": "Celery worker setting",
"i18n:govoplan-ops.components.9289473e": "Components",
"i18n:govoplan-ops.current_health_checks.7830bccf": "Current health checks",
"i18n:govoplan-ops.contracts.57d80902": "Contracts",
"i18n:govoplan-ops.controls.0cdb80fb": "Controls",
"i18n:govoplan-ops.declared_governance_permissions.d08d3bf1": "Declared governance permissions",
"i18n:govoplan-ops.deployment_profiles.b0caa179": "Deployment Profiles",
"i18n:govoplan-ops.enabled_in_the_runtime_registry.d2c6142d": "Enabled in the runtime registry",
"i18n:govoplan-ops.evidence.7ea014de": "Evidence",
"i18n:govoplan-ops.fit.dab564d8": "Fit",
"i18n:govoplan-ops.health_checks.201c869f": "Health Checks",
"i18n:govoplan-ops.governance_inventory.835d8e57": "Governance Inventory",
"i18n:govoplan-ops.loading_operations_status.6890fe6e": "Loading operations status...",
"i18n:govoplan-ops.modules.04e9462c": "Modules",
"i18n:govoplan-ops.module.b8ff0289": "Module",
"i18n:govoplan-ops.no_database_url.51a2db0c": "No database URL",
"i18n:govoplan-ops.no_deployment_profiles_reported.7c3af1db": "No deployment profiles reported.",
"i18n:govoplan-ops.no_health_checks_reported.03c067c4": "No health checks reported.",
"i18n:govoplan-ops.no_modules_reported.847f06d9": "No modules reported.",
"i18n:govoplan-ops.no_sizing_assumptions_reported.17515959": "No sizing assumptions reported.",
"i18n:govoplan-ops.operator_note.1dc58f7b": "Operator note",
"i18n:govoplan-ops.ops.907a54c2": "Ops",
"i18n:govoplan-ops.permissions.842c35eb": "Permissions",
"i18n:govoplan-ops.policies.e7800f56": "Policies",
"i18n:govoplan-ops.profile.ff4fc027": "Profile",
"i18n:govoplan-ops.readiness.1db9d6fb": "Readiness",
"i18n:govoplan-ops.reload.cce71553": "Reload",
"i18n:govoplan-ops.redis.5eaa1f2f": "Redis",
"i18n:govoplan-ops.registered_policy_capabilities.112a2b64": "Registered policy capabilities",
"i18n:govoplan-ops.no_readiness_blockers.0df259bd": "No readiness blockers",
"i18n:govoplan-ops.runtime_health_deployment_profile_worker_split_a.55340156": "Runtime health, deployment profile, worker split, and sizing assumptions.",
"i18n:govoplan-ops.scale_trigger.1c85e10e": "Scale trigger",
@@ -32,27 +71,66 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-ops.workers.b6ef3acd": "Workers"
},
"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.baseline.e6ab7982": "Baseline",
"i18n:govoplan-ops.authority.8802e425": "Berechtigungen",
"i18n:govoplan-ops.celery_worker_setting.323d7737": "Celery worker setting",
"i18n:govoplan-ops.components.9289473e": "Components",
"i18n:govoplan-ops.current_health_checks.7830bccf": "Current health checks",
"i18n:govoplan-ops.contracts.57d80902": "Verträge",
"i18n:govoplan-ops.controls.0cdb80fb": "Kontrollen",
"i18n:govoplan-ops.declared_governance_permissions.d08d3bf1": "Deklarierte Governance-Berechtigungen",
"i18n:govoplan-ops.deployment_profiles.b0caa179": "Deployment Profiles",
"i18n:govoplan-ops.enabled_in_the_runtime_registry.d2c6142d": "Enabled in the runtime registry",
"i18n:govoplan-ops.evidence.7ea014de": "Nachweise",
"i18n:govoplan-ops.fit.dab564d8": "Fit",
"i18n:govoplan-ops.health_checks.201c869f": "Health Checks",
"i18n:govoplan-ops.governance_inventory.835d8e57": "Governance-Inventar",
"i18n:govoplan-ops.loading_operations_status.6890fe6e": "Loading operations status...",
"i18n:govoplan-ops.modules.04e9462c": "Module",
"i18n:govoplan-ops.module.b8ff0289": "Modul",
"i18n:govoplan-ops.no_database_url.51a2db0c": "No database URL",
"i18n:govoplan-ops.no_deployment_profiles_reported.7c3af1db": "No deployment profiles reported.",
"i18n:govoplan-ops.no_health_checks_reported.03c067c4": "No health checks reported.",
"i18n:govoplan-ops.no_modules_reported.847f06d9": "Keine Module gemeldet.",
"i18n:govoplan-ops.no_sizing_assumptions_reported.17515959": "No sizing assumptions reported.",
"i18n:govoplan-ops.operator_note.1dc58f7b": "Operator note",
"i18n:govoplan-ops.ops.907a54c2": "Betrieb",
"i18n:govoplan-ops.permissions.842c35eb": "Berechtigungen",
"i18n:govoplan-ops.policies.e7800f56": "Richtlinien",
"i18n:govoplan-ops.profile.ff4fc027": "Profil",
"i18n:govoplan-ops.readiness.1db9d6fb": "Bereitschaft",
"i18n:govoplan-ops.reload.cce71553": "Neu laden",
"i18n:govoplan-ops.redis.5eaa1f2f": "Redis",
"i18n:govoplan-ops.registered_policy_capabilities.112a2b64": "Registrierte Richtlinien-Fähigkeiten",
"i18n:govoplan-ops.no_readiness_blockers.0df259bd": "Keine Bereitschaftsblocker",
"i18n:govoplan-ops.runtime_health_deployment_profile_worker_split_a.55340156": "Runtime health, deployment profile, worker split, and sizing assumptions.",
"i18n:govoplan-ops.scale_trigger.1c85e10e": "Scale trigger",
+17 -2
View File
@@ -14,6 +14,7 @@ const dashboardWidgets: DashboardWidgetsUiCapability = {
widgets: [
{
id: "ops.health",
surfaceId: "ops.widget.health",
title: "Operations health",
description: "Readiness, worker mode, and current warning count.",
moduleId: "ops",
@@ -34,9 +35,23 @@ export const opsModule: PlatformWebModule = {
dependencies: ["access"],
optionalDependencies: ["audit", "docs", "notifications"],
translations,
navItems: [{ to: "/ops", label: "i18n:govoplan-ops.ops.907a54c2", iconName: "activity", anyOf: opsReadScopes, order: 890 }],
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 }
],
navItems: [{ to: "/ops", label: "i18n:govoplan-ops.ops.907a54c2", iconName: "activity", anyOf: opsReadScopes, order: 890, surfaceId: "ops.navigation" }],
routes: [
{ path: "/ops", anyOf: opsReadScopes, order: 890, render: ({ settings }) => createElement(OpsPage, { settings }) }],
{ path: "/ops", anyOf: opsReadScopes, order: 890, surfaceId: "ops.page", render: ({ settings, auth }) => createElement(OpsPage, { settings, auth }) }],
uiCapabilities: {
"dashboard.widgets": dashboardWidgets
}