Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce07404405 | ||
|
|
7228ecad52 | ||
|
|
017d93dcbd | ||
|
|
fee7d0e833 | ||
|
|
2b3264372b | ||
|
|
cdcb477b55 | ||
|
|
0b4e601719 | ||
|
|
e5f4021de9 | ||
|
|
e3e0fdaab5 | ||
|
|
63f393afcb | ||
|
|
0105fd49f5 | ||
|
|
1426fd96b1 | ||
|
|
b464d016b2 | ||
|
|
c241085806 | ||
|
|
05ce4dc8ec | ||
|
|
b84cab1aa4 | ||
|
|
1ec47647ab | ||
|
|
2f11f08b72 | ||
|
|
430a48402f | ||
|
|
4d5bb22de5 | ||
|
|
b24eaa12ee | ||
|
|
1e67ec2244 | ||
|
|
341773a4ff |
@@ -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
|
||||
@@ -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,
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# govoplan-ops
|
||||
|
||||
<!-- govoplan-repository-type:start -->
|
||||
**Repository type:** module (platform).
|
||||
<!-- govoplan-repository-type:end -->
|
||||
|
||||
GovOPlaN Ops provides the operator surface for deployment health, runtime
|
||||
profile visibility, worker split assumptions, and sizing guidance.
|
||||
|
||||
@@ -9,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
|
||||
@@ -17,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.
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@govoplan/ops-webui",
|
||||
"version": "0.1.7",
|
||||
"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.7",
|
||||
"@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
@@ -4,14 +4,14 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "govoplan-ops"
|
||||
version = "0.1.7"
|
||||
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.7",
|
||||
"govoplan-access>=0.1.7",
|
||||
"govoplan-core>=0.1.15",
|
||||
"govoplan-access>=0.1.15",
|
||||
]
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
__all__ = ["__version__"]
|
||||
|
||||
__version__ = "0.1.6"
|
||||
__version__ = "0.1.15"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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.7",
|
||||
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,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
@@ -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
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@govoplan/ops-webui",
|
||||
"version": "0.1.7",
|
||||
"version": "0.1.15",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
@@ -13,14 +13,14 @@
|
||||
}
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@govoplan/core-webui": "^0.1.7",
|
||||
"@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": {
|
||||
|
||||
@@ -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" }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user