Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b09b653987 | ||
|
|
02e5d91d56 | ||
|
|
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
|
# 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
|
## Scope
|
||||||
|
|
||||||
This repository owns the GovOPlaN operations module seed: runtime health,
|
This repository owns the GovOPlaN operations module seed: runtime health,
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
# govoplan-ops
|
# govoplan-ops
|
||||||
|
|
||||||
|
<!-- govoplan-repository-type:start -->
|
||||||
|
**Repository type:** module (platform).
|
||||||
|
<!-- govoplan-repository-type:end -->
|
||||||
|
|
||||||
GovOPlaN Ops provides the operator surface for deployment health, runtime
|
GovOPlaN Ops provides the operator surface for deployment health, runtime
|
||||||
profile visibility, worker split assumptions, and sizing guidance.
|
profile visibility, worker split assumptions, and sizing guidance.
|
||||||
|
|
||||||
@@ -9,6 +13,19 @@ This repository owns:
|
|||||||
|
|
||||||
- backend module manifest `ops`
|
- backend module manifest `ops`
|
||||||
- operator-facing status APIs
|
- 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
|
- deployment profile and sizing assumption summaries
|
||||||
- WebUI route contribution `@govoplan/ops-webui`
|
- WebUI route contribution `@govoplan/ops-webui`
|
||||||
- future operational runbooks that describe the configured platform rather than
|
- 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
|
Core owns lifecycle management, module discovery, maintenance mode, package
|
||||||
installation safety, and shared WebUI shell behavior.
|
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
|
## Runbooks
|
||||||
|
|
||||||
- `docs/SCALABILITY_PROFILES.md` explains how to use the Ops page with the
|
- `docs/SCALABILITY_PROFILES.md` explains how to use the Ops page with the
|
||||||
canonical sizing matrix, readiness model, and profile-selection worksheet.
|
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.
|
campaigns, imports, exports, or workflow automation.
|
||||||
6. Record the current profile and open measurements before moving to a larger
|
6. Record the current profile and open measurements before moving to a larger
|
||||||
topology.
|
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
|
## Live Profile Signals
|
||||||
|
|
||||||
@@ -30,10 +36,40 @@ The Ops API reports:
|
|||||||
- HTTP/certificate deployment posture through the `deployment_security` check
|
- HTTP/certificate deployment posture through the `deployment_security` check
|
||||||
- readiness blockers
|
- readiness blockers
|
||||||
- reference deployment profiles and sizing assumptions
|
- 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
|
These values are intentionally diagnostic. They do not replace deployment
|
||||||
configuration management, backups, monitoring, or restore drills.
|
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
|
`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
|
profiles it warns when secure cookies or CORS origins still look local. In
|
||||||
production it becomes readiness-critical because TLS certificates, proxy
|
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
|
depends on PostgreSQL, Redis, Celery, module package lifecycle, or durable file
|
||||||
storage.
|
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
|
Promote from a single-process profile to a split-worker profile when queued
|
||||||
work becomes part of normal operation:
|
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
|
- API workers when `MASTER_KEY_B64`, `DATABASE_URL`, storage, queue, and module
|
||||||
configuration are shared
|
configuration are shared
|
||||||
- Background workers when queues and idempotency keys are used
|
- 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:
|
Stateful or singleton-sensitive:
|
||||||
|
|
||||||
- PostgreSQL
|
- 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
|
- Redis/queue state
|
||||||
- module installer daemon and package mutation operations
|
- module installer daemon and package mutation operations
|
||||||
- migration execution
|
- 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
|
- outgoing campaign append/send jobs unless claim tokens are enforced
|
||||||
|
|
||||||
## Readiness And Degraded Modes
|
## Readiness And Degraded Modes
|
||||||
|
|
||||||
| Component | Ready When | Degraded Mode |
|
| 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. |
|
| 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. |
|
| 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. |
|
| 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",
|
"name": "@govoplan/ops-webui",
|
||||||
"version": "0.1.7",
|
"version": "0.1.17",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "webui/src/index.ts",
|
"main": "webui/src/index.ts",
|
||||||
@@ -17,14 +17,14 @@
|
|||||||
"README.md"
|
"README.md"
|
||||||
],
|
],
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@govoplan/core-webui": "^0.1.7",
|
"@govoplan/core-webui": "^0.1.17",
|
||||||
"lucide-react": "^1.23.0",
|
"lucide-react": "^1.23.0",
|
||||||
"react": "^19.0.0",
|
"react": ">=19.2.7 <20",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": ">=19.2.7 <20",
|
||||||
"react-router-dom": "^7.1.1",
|
"react-router": ">=8.3.0 <9",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^5.2.0",
|
||||||
"typescript": "^5.7.2",
|
"typescript": "^5.7.2",
|
||||||
"vite": "^6.0.6"
|
"vite": "^7.3.6"
|
||||||
},
|
},
|
||||||
"peerDependenciesMeta": {
|
"peerDependenciesMeta": {
|
||||||
"@govoplan/core-webui": {
|
"@govoplan/core-webui": {
|
||||||
|
|||||||
+3
-3
@@ -4,14 +4,14 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "govoplan-ops"
|
name = "govoplan-ops"
|
||||||
version = "0.1.7"
|
version = "0.1.17"
|
||||||
description = "GovOPlaN operations module for health, deployment profile, and sizing visibility."
|
description = "GovOPlaN operations module for health, deployment profile, and sizing visibility."
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
authors = [{ name = "GovOPlaN" }]
|
authors = [{ name = "GovOPlaN" }]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"govoplan-core>=0.1.7",
|
"govoplan-core>=0.1.17",
|
||||||
"govoplan-access>=0.1.7",
|
"govoplan-access>=0.1.17",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.setuptools.packages.find]
|
[tool.setuptools.packages.find]
|
||||||
|
|||||||
@@ -2,4 +2,4 @@
|
|||||||
|
|
||||||
__all__ = ["__version__"]
|
__all__ = ["__version__"]
|
||||||
|
|
||||||
__version__ = "0.1.6"
|
__version__ = "0.1.17"
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,69 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from govoplan_core.core.access import CAPABILITY_AUTH_PERMISSION_EVALUATOR, CAPABILITY_AUTH_PRINCIPAL_RESOLVER
|
from govoplan_core.core.access import (
|
||||||
from govoplan_core.core.modules import FrontendModule, FrontendRoute, ModuleContext, ModuleManifest, NavItem, PermissionDefinition, RoleTemplate
|
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_SCOPE = "ops:operations:read"
|
||||||
OPS_READ_SCOPES = (OPS_READ_SCOPE, "system:settings:read", "admin:settings: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:
|
def _permission(scope: str, label: str, description: str) -> PermissionDefinition:
|
||||||
@@ -31,8 +90,11 @@ def _route_factory(context: ModuleContext):
|
|||||||
manifest = ModuleManifest(
|
manifest = ModuleManifest(
|
||||||
id="ops",
|
id="ops",
|
||||||
name="Ops",
|
name="Ops",
|
||||||
version="0.1.7",
|
version="0.1.17",
|
||||||
required_capabilities=(CAPABILITY_AUTH_PRINCIPAL_RESOLVER, CAPABILITY_AUTH_PERMISSION_EVALUATOR),
|
required_capabilities=(
|
||||||
|
CAPABILITY_AUTH_PRINCIPAL_RESOLVER,
|
||||||
|
CAPABILITY_AUTH_PERMISSION_EVALUATOR,
|
||||||
|
),
|
||||||
optional_dependencies=("audit", "docs", "notifications"),
|
optional_dependencies=("audit", "docs", "notifications"),
|
||||||
permissions=(
|
permissions=(
|
||||||
_permission(
|
_permission(
|
||||||
@@ -40,6 +102,11 @@ manifest = ModuleManifest(
|
|||||||
"View operations status",
|
"View operations status",
|
||||||
"Read runtime health, deployment profile, and sizing information.",
|
"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=(
|
role_templates=(
|
||||||
RoleTemplate(
|
RoleTemplate(
|
||||||
@@ -49,15 +116,212 @@ manifest = ModuleManifest(
|
|||||||
permissions=(OPS_READ_SCOPE,),
|
permissions=(OPS_READ_SCOPE,),
|
||||||
level="system",
|
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,
|
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(
|
frontend=FrontendModule(
|
||||||
module_id="ops",
|
module_id="ops",
|
||||||
package_name="@govoplan/ops-webui",
|
package_name="@govoplan/ops-webui",
|
||||||
routes=(FrontendRoute(path="/ops", component="OpsPage", required_any=OPS_READ_SCOPES, order=890),),
|
routes=(
|
||||||
nav_items=(NavItem(path="/ops", label="Ops", icon="activity", required_any=OPS_READ_SCOPES, order=890),),
|
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",
|
"name": "@govoplan/ops-webui",
|
||||||
"version": "0.1.7",
|
"version": "0.1.17",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
@@ -13,14 +13,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@govoplan/core-webui": "^0.1.7",
|
"@govoplan/core-webui": "^0.1.17",
|
||||||
"lucide-react": "^1.23.0",
|
"lucide-react": "^1.23.0",
|
||||||
"react": "^19.0.0",
|
"react": ">=19.2.7 <20",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": ">=19.2.7 <20",
|
||||||
"react-router-dom": "^7.1.1",
|
"react-router": ">=8.3.0 <9",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^5.2.0",
|
||||||
"typescript": "^5.7.2",
|
"typescript": "^5.7.2",
|
||||||
"vite": "^6.0.6"
|
"vite": "^7.3.6"
|
||||||
},
|
},
|
||||||
"peerDependenciesMeta": {
|
"peerDependenciesMeta": {
|
||||||
"@govoplan/core-webui": {
|
"@govoplan/core-webui": {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export type OpsCheck = {
|
|||||||
state: "ok" | "warning" | "error" | string;
|
state: "ok" | "warning" | "error" | string;
|
||||||
detail: string;
|
detail: string;
|
||||||
readiness_critical?: boolean;
|
readiness_critical?: boolean;
|
||||||
|
metrics?: Record<string, unknown>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type OpsDeploymentProfile = {
|
export type OpsDeploymentProfile = {
|
||||||
@@ -23,6 +24,129 @@ export type OpsSizingAssumption = {
|
|||||||
operator_note: string;
|
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 = {
|
export type OpsStatus = {
|
||||||
summary: {
|
summary: {
|
||||||
app_env: string;
|
app_env: string;
|
||||||
@@ -36,7 +160,32 @@ export type OpsStatus = {
|
|||||||
message?: string | null;
|
message?: string | null;
|
||||||
};
|
};
|
||||||
database_url: string;
|
database_url: string;
|
||||||
|
database_connection_peak?: number | null;
|
||||||
|
database_connection_available?: number | null;
|
||||||
file_storage_backend: string;
|
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: {
|
readiness: {
|
||||||
ready: boolean;
|
ready: boolean;
|
||||||
@@ -49,10 +198,61 @@ export type OpsStatus = {
|
|||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
checks: OpsCheck[];
|
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[];
|
deployment_profiles: OpsDeploymentProfile[];
|
||||||
sizing: OpsSizingAssumption[];
|
sizing: OpsSizingAssumption[];
|
||||||
|
runtime_cluster: OpsRuntimeCluster;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function fetchOpsStatus(settings: ApiSettings): Promise<OpsStatus> {
|
export function fetchOpsStatus(settings: ApiSettings): Promise<OpsStatus> {
|
||||||
return apiFetch(settings, "/api/v1/ops/status");
|
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 { useEffect, useState } from "react";
|
||||||
import {
|
import {
|
||||||
DismissibleAlert,
|
DismissibleAlert,
|
||||||
|
DocumentationHelpLink,
|
||||||
LoadingFrame,
|
LoadingFrame,
|
||||||
MetricCard,
|
MetricCard,
|
||||||
StatusBadge,
|
StatusBadge,
|
||||||
@@ -8,6 +9,7 @@ import {
|
|||||||
type ApiSettings
|
type ApiSettings
|
||||||
} from "@govoplan/core-webui";
|
} from "@govoplan/core-webui";
|
||||||
import { fetchOpsStatus, type OpsStatus } from "../../api/ops";
|
import { fetchOpsStatus, type OpsStatus } from "../../api/ops";
|
||||||
|
import { OPS_DOCUMENTATION } from "./interfacePatterns";
|
||||||
|
|
||||||
export default function OpsHealthWidget({ settings, refreshKey }: { settings: ApiSettings; refreshKey: number }) {
|
export default function OpsHealthWidget({ settings, refreshKey }: { settings: ApiSettings; refreshKey: number }) {
|
||||||
const [status, setStatus] = useState<OpsStatus | null>(null);
|
const [status, setStatus] = useState<OpsStatus | null>(null);
|
||||||
@@ -29,13 +31,28 @@ export default function OpsHealthWidget({ settings, refreshKey }: { settings: Ap
|
|||||||
const warningCount = checks.filter((item) => item.state === "warning").length;
|
const warningCount = checks.filter((item) => item.state === "warning").length;
|
||||||
const errorCount = checks.filter((item) => item.state === "error").length;
|
const errorCount = checks.filter((item) => item.state === "error").length;
|
||||||
const ready = status?.readiness.ready ?? false;
|
const ready = status?.readiness.ready ?? false;
|
||||||
|
const 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 (
|
return (
|
||||||
<LoadingFrame loading={loading} label="Loading operations status">
|
<LoadingFrame loading={loading} label="Loading operations status">
|
||||||
{error && <DismissibleAlert tone="warning" resetKey={error}>{error}</DismissibleAlert>}
|
{error && <DismissibleAlert tone="warning" resetKey={error}>{error}</DismissibleAlert>}
|
||||||
|
<div className="button-row compact-actions">
|
||||||
|
<DocumentationHelpLink reference={OPS_DOCUMENTATION} />
|
||||||
|
</div>
|
||||||
<div className="metric-grid inside dashboard-widget-metrics">
|
<div className="metric-grid inside dashboard-widget-metrics">
|
||||||
<MetricCard label="Readiness" value={ready ? "ready" : "blocked"} tone={ready ? "good" : "danger"} detail={status?.readiness.profile ?? "-"} />
|
<MetricCard label="Readiness" value={ready ? "ready" : "blocked"} tone={ready ? "good" : "danger"} detail={status?.readiness.profile ?? "-"} />
|
||||||
<MetricCard label="Workers" value={status?.summary.celery_enabled ? "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" />
|
<MetricCard label="Warnings" value={warningCount + errorCount} tone={errorCount ? "danger" : warningCount ? "warning" : "good"} detail="Current health checks" />
|
||||||
</div>
|
</div>
|
||||||
{status?.readiness.blockers.length ?
|
{status?.readiness.blockers.length ?
|
||||||
@@ -51,4 +68,3 @@ export default function OpsHealthWidget({ settings, refreshKey }: { settings: Ap
|
|||||||
}
|
}
|
||||||
</LoadingFrame>);
|
</LoadingFrame>);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,52 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { RefreshCw } from "lucide-react";
|
import { PauseCircle, PlayCircle, RefreshCw } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
|
ActionBlockerHint,
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
|
ConfirmDialog,
|
||||||
|
DataGrid,
|
||||||
DismissibleAlert,
|
DismissibleAlert,
|
||||||
|
DocumentationHelpLink,
|
||||||
LoadingFrame,
|
LoadingFrame,
|
||||||
MetricCard,
|
MetricCard,
|
||||||
|
PageScrollViewport,
|
||||||
PageTitle,
|
PageTitle,
|
||||||
StatusBadge,
|
StatusBadge,
|
||||||
|
TableActionGroup,
|
||||||
adminErrorMessage,
|
adminErrorMessage,
|
||||||
type ApiSettings } from
|
hasAnyScope,
|
||||||
|
i18nMessage,
|
||||||
|
type ApiSettings,
|
||||||
|
type AuthInfo,
|
||||||
|
type DataGridColumn } from
|
||||||
"@govoplan/core-webui";
|
"@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 [status, setStatus] = useState<OpsStatus | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [runningProbes, setRunningProbes] = useState(false);
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
|
const [drainTarget, setDrainTarget] = useState<OpsRuntimeNode | null>(null);
|
||||||
|
const [nodeActionId, setNodeActionId] = useState("");
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
setLoading(true);
|
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]);
|
useEffect(() => {void load();}, [settings.apiBaseUrl, settings.apiKey, settings.accessToken]);
|
||||||
|
|
||||||
const checks = status?.checks ?? [];
|
const checks = status?.checks ?? [];
|
||||||
const warningCount = checks.filter((item) => item.state === "warning").length;
|
const warningCount = checks.filter((item) => item.state === "warning").length;
|
||||||
const errorCount = checks.filter((item) => item.state === "error").length;
|
const errorCount = checks.filter((item) => item.state === "error").length;
|
||||||
const ready = status?.readiness.ready ?? false;
|
const ready = status?.readiness.ready ?? false;
|
||||||
|
const canRunChecks = hasAnyScope(auth, ["ops:operations:run", "system:settings:write"]);
|
||||||
|
const 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 (
|
return (
|
||||||
|
<PageScrollViewport>
|
||||||
<div className="content-pad workspace-data-page">
|
<div className="content-pad workspace-data-page">
|
||||||
<div className="page-heading split workspace-heading">
|
<div className="page-heading split workspace-heading">
|
||||||
<div>
|
<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>
|
<p>i18n:govoplan-ops.runtime_health_deployment_profile_worker_split_a.55340156</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="button-row compact-actions">
|
<div className="button-row compact-actions">
|
||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && <DismissibleAlert tone="danger" resetKey={error} floating>{error}</DismissibleAlert>}
|
{error && <DismissibleAlert tone="danger" resetKey={error} floating>{error}</DismissibleAlert>}
|
||||||
|
{status && !ready && (
|
||||||
|
<ActionBlockerHint
|
||||||
|
reason={{
|
||||||
|
summary: "i18n:govoplan-ops.readiness_blocked_summary",
|
||||||
|
details: i18nMessage("i18n:govoplan-ops.readiness_blocked_details", { value0: status.readiness.blockers.length }),
|
||||||
|
requiredAction: "i18n:govoplan-ops.readiness_blocked_action",
|
||||||
|
actor: "i18n:govoplan-ops.operations_operator",
|
||||||
|
target: "i18n:govoplan-ops.readiness_blocked_target"
|
||||||
|
}}
|
||||||
|
documentation={OPS_RECOVERY_DOCUMENTATION}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<LoadingFrame loading={loading} label="i18n:govoplan-ops.loading_operations_status.6890fe6e">
|
<LoadingFrame loading={loading} label="i18n:govoplan-ops.loading_operations_status.6890fe6e">
|
||||||
<div className="metric-grid">
|
<div className="metric-grid">
|
||||||
<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.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.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.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.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="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>
|
||||||
|
|
||||||
<div className="dashboard-grid">
|
<div className="dashboard-grid">
|
||||||
@@ -66,6 +175,24 @@ export default function OpsPage({ settings }: {settings: ApiSettings;}) {
|
|||||||
<CheckList checks={checks} />
|
<CheckList checks={checks} />
|
||||||
</Card>
|
</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">
|
<Card title="i18n:govoplan-ops.deployment_profiles.b0caa179">
|
||||||
<ProfileList profiles={status?.deployment_profiles ?? []} />
|
<ProfileList profiles={status?.deployment_profiles ?? []} />
|
||||||
</Card>
|
</Card>
|
||||||
@@ -75,10 +202,271 @@ export default function OpsPage({ settings }: {settings: ApiSettings;}) {
|
|||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</LoadingFrame>
|
</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[];}) {
|
function CheckList({ checks }: {checks: OpsCheck[];}) {
|
||||||
if (!checks.length) return <p className="muted">i18n:govoplan-ops.no_health_checks_reported.03c067c4</p>;
|
if (!checks.length) return <p className="muted">i18n:govoplan-ops.no_health_checks_reported.03c067c4</p>;
|
||||||
return (
|
return (
|
||||||
@@ -95,47 +483,27 @@ function CheckList({ checks }: {checks: OpsCheck[];}) {
|
|||||||
|
|
||||||
function ProfileList({ profiles }: {profiles: OpsDeploymentProfile[];}) {
|
function ProfileList({ profiles }: {profiles: OpsDeploymentProfile[];}) {
|
||||||
if (!profiles.length) return <p className="muted">i18n:govoplan-ops.no_deployment_profiles_reported.7c3af1db</p>;
|
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 (
|
return (
|
||||||
<div className="admin-table-wrap">
|
<DataGrid id="ops-deployment-profiles" rows={profiles} columns={columns} getRowKey={(profile) => profile.id} />);
|
||||||
<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>);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function SizingTable({ items }: {items: OpsSizingAssumption[];}) {
|
function SizingTable({ items }: {items: OpsSizingAssumption[];}) {
|
||||||
if (!items.length) return <p className="muted">i18n:govoplan-ops.no_sizing_assumptions_reported.17515959</p>;
|
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 (
|
return (
|
||||||
<div className="admin-table-wrap">
|
<DataGrid id="ops-sizing-assumptions" rows={items} columns={columns} getRowKey={(item) => item.area} />);
|
||||||
<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>);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,3 +513,60 @@ function stateTone(state: string): string {
|
|||||||
if (state === "error") return "error";
|
if (state === "error") return "error";
|
||||||
return "inactive";
|
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 = {
|
export const generatedTranslations: PlatformTranslations = {
|
||||||
"en": {
|
"en": {
|
||||||
|
"i18n:govoplan-ops.surface.navigation": "Operations navigation",
|
||||||
|
"i18n:govoplan-ops.surface.page": "Operations workspace",
|
||||||
|
"i18n:govoplan-ops.surface.summary": "Operations summary",
|
||||||
|
"i18n:govoplan-ops.surface.health": "Health checks",
|
||||||
|
"i18n:govoplan-ops.surface.runtime": "Runtime cluster",
|
||||||
|
"i18n:govoplan-ops.surface.recovery": "Recovery evidence",
|
||||||
|
"i18n:govoplan-ops.surface.governance": "Governance inventory",
|
||||||
|
"i18n:govoplan-ops.surface.deployment": "Deployment profiles",
|
||||||
|
"i18n:govoplan-ops.surface.sizing": "Sizing assumptions",
|
||||||
|
"i18n:govoplan-ops.surface.run_probes": "Run operational probes",
|
||||||
|
"i18n:govoplan-ops.surface.drain_node": "Drain runtime node",
|
||||||
|
"i18n:govoplan-ops.reason.loading": "Operations status is loading.",
|
||||||
|
"i18n:govoplan-ops.reason.running_probes": "Operational probes are already running.",
|
||||||
|
"i18n:govoplan-ops.reason.run_permission_required": "Operations-run permission is required.",
|
||||||
|
"i18n:govoplan-ops.reason.node_action_active": "A lifecycle action is already running for this node.",
|
||||||
|
"i18n:govoplan-ops.reason.stale_node": "A stale node cannot be drained because its current incarnation is not reporting.",
|
||||||
|
"i18n:govoplan-ops.reason.stopped_node": "A stopped node cannot accept lifecycle actions.",
|
||||||
|
"i18n:govoplan-ops.readiness_blocked_summary": "The runtime is not ready for normal traffic.",
|
||||||
|
"i18n:govoplan-ops.readiness_blocked_details": "{value0} readiness blocker(s) are active.",
|
||||||
|
"i18n:govoplan-ops.readiness_blocked_action": "Review the health checks, runtime cluster, and owning-module recovery evidence before restoring traffic.",
|
||||||
|
"i18n:govoplan-ops.operations_operator": "Operations operator",
|
||||||
|
"i18n:govoplan-ops.readiness_blocked_target": "Health checks, Runtime cluster, and Recovery evidence below",
|
||||||
|
"i18n:govoplan-ops.lifecycle_actions_for_value": "Lifecycle actions for {value0}",
|
||||||
|
"i18n:govoplan-ops.cancel_drain_for_value": "Cancel drain for {value0}",
|
||||||
|
"i18n:govoplan-ops.drain_value": "Drain {value0}",
|
||||||
|
"i18n:govoplan-ops.drain_runtime_node_message": "Stop routing new work to {value0}. In-flight work is allowed to finish.",
|
||||||
|
"i18n:govoplan-ops.this_node": "this node",
|
||||||
|
"i18n:govoplan-ops.drain_node": "Drain node",
|
||||||
"i18n:govoplan-ops.area.2745deba": "Area",
|
"i18n:govoplan-ops.area.2745deba": "Area",
|
||||||
"i18n:govoplan-ops.baseline.e6ab7982": "Baseline",
|
"i18n:govoplan-ops.baseline.e6ab7982": "Baseline",
|
||||||
|
"i18n:govoplan-ops.authority.8802e425": "Authority",
|
||||||
"i18n:govoplan-ops.celery_worker_setting.323d7737": "Celery worker setting",
|
"i18n:govoplan-ops.celery_worker_setting.323d7737": "Celery worker setting",
|
||||||
"i18n:govoplan-ops.components.9289473e": "Components",
|
"i18n:govoplan-ops.components.9289473e": "Components",
|
||||||
"i18n:govoplan-ops.current_health_checks.7830bccf": "Current health checks",
|
"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.deployment_profiles.b0caa179": "Deployment Profiles",
|
||||||
"i18n:govoplan-ops.enabled_in_the_runtime_registry.d2c6142d": "Enabled in the runtime registry",
|
"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.fit.dab564d8": "Fit",
|
||||||
"i18n:govoplan-ops.health_checks.201c869f": "Health Checks",
|
"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.loading_operations_status.6890fe6e": "Loading operations status...",
|
||||||
"i18n:govoplan-ops.modules.04e9462c": "Modules",
|
"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_database_url.51a2db0c": "No database URL",
|
||||||
"i18n:govoplan-ops.no_deployment_profiles_reported.7c3af1db": "No deployment profiles reported.",
|
"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_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.no_sizing_assumptions_reported.17515959": "No sizing assumptions reported.",
|
||||||
"i18n:govoplan-ops.operator_note.1dc58f7b": "Operator note",
|
"i18n:govoplan-ops.operator_note.1dc58f7b": "Operator note",
|
||||||
"i18n:govoplan-ops.ops.907a54c2": "Ops",
|
"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.profile.ff4fc027": "Profile",
|
||||||
"i18n:govoplan-ops.readiness.1db9d6fb": "Readiness",
|
"i18n:govoplan-ops.readiness.1db9d6fb": "Readiness",
|
||||||
"i18n:govoplan-ops.reload.cce71553": "Reload",
|
"i18n:govoplan-ops.reload.cce71553": "Reload",
|
||||||
"i18n:govoplan-ops.redis.5eaa1f2f": "Redis",
|
"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.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.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",
|
"i18n:govoplan-ops.scale_trigger.1c85e10e": "Scale trigger",
|
||||||
@@ -32,27 +71,66 @@ export const generatedTranslations: PlatformTranslations = {
|
|||||||
"i18n:govoplan-ops.workers.b6ef3acd": "Workers"
|
"i18n:govoplan-ops.workers.b6ef3acd": "Workers"
|
||||||
},
|
},
|
||||||
"de": {
|
"de": {
|
||||||
|
"i18n:govoplan-ops.surface.navigation": "Betriebsnavigation",
|
||||||
|
"i18n:govoplan-ops.surface.page": "Betriebsarbeitsbereich",
|
||||||
|
"i18n:govoplan-ops.surface.summary": "Betriebsübersicht",
|
||||||
|
"i18n:govoplan-ops.surface.health": "Systemprüfungen",
|
||||||
|
"i18n:govoplan-ops.surface.runtime": "Laufzeitcluster",
|
||||||
|
"i18n:govoplan-ops.surface.recovery": "Wiederherstellungsnachweise",
|
||||||
|
"i18n:govoplan-ops.surface.governance": "Governance-Inventar",
|
||||||
|
"i18n:govoplan-ops.surface.deployment": "Bereitstellungsprofile",
|
||||||
|
"i18n:govoplan-ops.surface.sizing": "Dimensionierungsannahmen",
|
||||||
|
"i18n:govoplan-ops.surface.run_probes": "Betriebsprüfungen ausführen",
|
||||||
|
"i18n:govoplan-ops.surface.drain_node": "Laufzeitknoten leeren",
|
||||||
|
"i18n:govoplan-ops.reason.loading": "Der Betriebsstatus wird geladen.",
|
||||||
|
"i18n:govoplan-ops.reason.running_probes": "Betriebsprüfungen werden bereits ausgeführt.",
|
||||||
|
"i18n:govoplan-ops.reason.run_permission_required": "Die Berechtigung zum Ausführen von Betriebsprüfungen ist erforderlich.",
|
||||||
|
"i18n:govoplan-ops.reason.node_action_active": "Für diesen Knoten wird bereits eine Lebenszyklusaktion ausgeführt.",
|
||||||
|
"i18n:govoplan-ops.reason.stale_node": "Ein veralteter Knoten kann nicht geleert werden, weil seine aktuelle Instanz keine Statusmeldungen sendet.",
|
||||||
|
"i18n:govoplan-ops.reason.stopped_node": "Ein gestoppter Knoten kann keine Lebenszyklusaktionen annehmen.",
|
||||||
|
"i18n:govoplan-ops.readiness_blocked_summary": "Die Laufzeitumgebung ist nicht für normalen Datenverkehr bereit.",
|
||||||
|
"i18n:govoplan-ops.readiness_blocked_details": "{value0} Bereitschaftsblocker sind aktiv.",
|
||||||
|
"i18n:govoplan-ops.readiness_blocked_action": "Prüfen Sie Systemprüfungen, Laufzeitcluster und Wiederherstellungsnachweise der zuständigen Module, bevor der Datenverkehr wieder freigegeben wird.",
|
||||||
|
"i18n:govoplan-ops.operations_operator": "Betriebsverantwortliche Person",
|
||||||
|
"i18n:govoplan-ops.readiness_blocked_target": "Systemprüfungen, Laufzeitcluster und Wiederherstellungsnachweise weiter unten",
|
||||||
|
"i18n:govoplan-ops.lifecycle_actions_for_value": "Lebenszyklusaktionen für {value0}",
|
||||||
|
"i18n:govoplan-ops.cancel_drain_for_value": "Leeren von {value0} abbrechen",
|
||||||
|
"i18n:govoplan-ops.drain_value": "{value0} leeren",
|
||||||
|
"i18n:govoplan-ops.drain_runtime_node_message": "Keine neue Arbeit mehr an {value0} weiterleiten. Laufende Arbeit darf abgeschlossen werden.",
|
||||||
|
"i18n:govoplan-ops.this_node": "diesen Knoten",
|
||||||
|
"i18n:govoplan-ops.drain_node": "Knoten leeren",
|
||||||
"i18n:govoplan-ops.area.2745deba": "Area",
|
"i18n:govoplan-ops.area.2745deba": "Area",
|
||||||
"i18n:govoplan-ops.baseline.e6ab7982": "Baseline",
|
"i18n:govoplan-ops.baseline.e6ab7982": "Baseline",
|
||||||
|
"i18n:govoplan-ops.authority.8802e425": "Berechtigungen",
|
||||||
"i18n:govoplan-ops.celery_worker_setting.323d7737": "Celery worker setting",
|
"i18n:govoplan-ops.celery_worker_setting.323d7737": "Celery worker setting",
|
||||||
"i18n:govoplan-ops.components.9289473e": "Components",
|
"i18n:govoplan-ops.components.9289473e": "Components",
|
||||||
"i18n:govoplan-ops.current_health_checks.7830bccf": "Current health checks",
|
"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.deployment_profiles.b0caa179": "Deployment Profiles",
|
||||||
"i18n:govoplan-ops.enabled_in_the_runtime_registry.d2c6142d": "Enabled in the runtime registry",
|
"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.fit.dab564d8": "Fit",
|
||||||
"i18n:govoplan-ops.health_checks.201c869f": "Health Checks",
|
"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.loading_operations_status.6890fe6e": "Loading operations status...",
|
||||||
"i18n:govoplan-ops.modules.04e9462c": "Module",
|
"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_database_url.51a2db0c": "No database URL",
|
||||||
"i18n:govoplan-ops.no_deployment_profiles_reported.7c3af1db": "No deployment profiles reported.",
|
"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_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.no_sizing_assumptions_reported.17515959": "No sizing assumptions reported.",
|
||||||
"i18n:govoplan-ops.operator_note.1dc58f7b": "Operator note",
|
"i18n:govoplan-ops.operator_note.1dc58f7b": "Operator note",
|
||||||
"i18n:govoplan-ops.ops.907a54c2": "Betrieb",
|
"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.profile.ff4fc027": "Profil",
|
||||||
"i18n:govoplan-ops.readiness.1db9d6fb": "Bereitschaft",
|
"i18n:govoplan-ops.readiness.1db9d6fb": "Bereitschaft",
|
||||||
"i18n:govoplan-ops.reload.cce71553": "Neu laden",
|
"i18n:govoplan-ops.reload.cce71553": "Neu laden",
|
||||||
"i18n:govoplan-ops.redis.5eaa1f2f": "Redis",
|
"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.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.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",
|
"i18n:govoplan-ops.scale_trigger.1c85e10e": "Scale trigger",
|
||||||
|
|||||||
+17
-2
@@ -14,6 +14,7 @@ const dashboardWidgets: DashboardWidgetsUiCapability = {
|
|||||||
widgets: [
|
widgets: [
|
||||||
{
|
{
|
||||||
id: "ops.health",
|
id: "ops.health",
|
||||||
|
surfaceId: "ops.widget.health",
|
||||||
title: "Operations health",
|
title: "Operations health",
|
||||||
description: "Readiness, worker mode, and current warning count.",
|
description: "Readiness, worker mode, and current warning count.",
|
||||||
moduleId: "ops",
|
moduleId: "ops",
|
||||||
@@ -34,9 +35,23 @@ export const opsModule: PlatformWebModule = {
|
|||||||
dependencies: ["access"],
|
dependencies: ["access"],
|
||||||
optionalDependencies: ["audit", "docs", "notifications"],
|
optionalDependencies: ["audit", "docs", "notifications"],
|
||||||
translations,
|
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: [
|
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: {
|
uiCapabilities: {
|
||||||
"dashboard.widgets": dashboardWidgets
|
"dashboard.widgets": dashboardWidgets
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user