Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
489d44d18d | ||
|
|
ac6b211827 |
@@ -49,6 +49,18 @@ The Ops API reports:
|
||||
- rendered PostgreSQL connection peak, declared server limit, and operator reserve
|
||||
- recovery operation status, mode, checkpoint count, and last update
|
||||
|
||||
The separately authorized
|
||||
`GET /api/v1/ops/infrastructure/dependencies` endpoint collects current,
|
||||
module-owned infrastructure dependencies through the Core provider contract.
|
||||
It is intended for the host deployer's destructive-change preflight, not for a
|
||||
general data export. Results contain stable configuration references, lifecycle
|
||||
states, scopes, numeric counts and required preparation actions; they never
|
||||
contain credentials, tenant identifiers, file keys or secret-bearing endpoint
|
||||
values. Ops itself contributes the current database, Redis coordination,
|
||||
ingress, and load-balancing runtime bindings; feature modules contribute their
|
||||
own persisted configuration and data. One unavailable or invalid provider makes the result incomplete, so a
|
||||
caller must fail closed rather than treating missing provider data as zero.
|
||||
|
||||
These values are intentionally diagnostic. They do not replace deployment
|
||||
configuration management, backups, monitoring, or restore drills.
|
||||
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@govoplan/ops-webui",
|
||||
"version": "0.1.20",
|
||||
"version": "0.1.22",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "webui/src/index.ts",
|
||||
@@ -17,7 +17,7 @@
|
||||
"README.md"
|
||||
],
|
||||
"peerDependencies": {
|
||||
"@govoplan/core-webui": "^0.1.18",
|
||||
"@govoplan/core-webui": "^0.1.45",
|
||||
"lucide-react": "^1.23.0",
|
||||
"react": ">=19.2.7 <20",
|
||||
"react-dom": ">=19.2.7 <20",
|
||||
|
||||
+2
-2
@@ -4,13 +4,13 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "govoplan-ops"
|
||||
version = "0.1.20"
|
||||
version = "0.1.22"
|
||||
description = "GovOPlaN operations module for health, deployment profile, and sizing visibility."
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
authors = [{ name = "GovOPlaN" }]
|
||||
dependencies = [
|
||||
"govoplan-core>=0.1.18",
|
||||
"govoplan-core>=0.1.45",
|
||||
"govoplan-access>=0.1.18",
|
||||
]
|
||||
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
__all__ = ["__version__"]
|
||||
|
||||
__version__ = "0.1.20"
|
||||
__version__ = "0.1.22"
|
||||
|
||||
@@ -47,7 +47,10 @@ from govoplan_core.db.session import get_database
|
||||
from govoplan_core.settings import settings as core_settings
|
||||
|
||||
from govoplan_ops.backend.manifest import OPS_READ_SCOPES, OPS_RUN_SCOPES
|
||||
from govoplan_ops.backend.infrastructure import deployment_capability_status
|
||||
from govoplan_ops.backend.infrastructure import (
|
||||
deployment_capability_status,
|
||||
infrastructure_dependency_inventory,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/ops", tags=["ops"])
|
||||
_module_check_cache: dict[str, tuple[float, dict[str, Any]]] = {}
|
||||
@@ -93,6 +96,20 @@ def run_ops_checks(
|
||||
return _ops_status_payload(request, force_module_checks=True)
|
||||
|
||||
|
||||
@router.get("/infrastructure/dependencies")
|
||||
def infrastructure_dependencies(
|
||||
request: Request,
|
||||
principal: ApiPrincipal = Depends(require_any_scope(*OPS_READ_SCOPES)),
|
||||
) -> dict[str, object]:
|
||||
"""Return a fresh, non-secret inventory for host deployment preflight."""
|
||||
|
||||
del principal
|
||||
return infrastructure_dependency_inventory(
|
||||
_registry(request),
|
||||
installation_id=core_settings.installation_id,
|
||||
).to_dict()
|
||||
|
||||
|
||||
@router.get("/runtime/nodes")
|
||||
def runtime_nodes(
|
||||
request: Request,
|
||||
|
||||
@@ -3,8 +3,96 @@ from __future__ import annotations
|
||||
from pathlib import Path
|
||||
|
||||
from govoplan_core.core.infrastructure_capabilities import (
|
||||
INFRASTRUCTURE_DEPENDENCY_PROVIDER_CAPABILITY_PREFIX,
|
||||
InfrastructureDependency,
|
||||
InfrastructureDependencyInventory,
|
||||
InfrastructureDependencyProvider,
|
||||
collect_infrastructure_dependency_inventory,
|
||||
deployment_capability_status as _deployment_capability_status,
|
||||
load_infrastructure_capability_receipt,
|
||||
)
|
||||
from govoplan_core.settings import settings as core_settings
|
||||
|
||||
|
||||
OPS_INFRASTRUCTURE_DEPENDENCY_CAPABILITY = (
|
||||
f"{INFRASTRUCTURE_DEPENDENCY_PROVIDER_CAPABILITY_PREFIX}ops"
|
||||
)
|
||||
|
||||
|
||||
class OpsInfrastructureDependencyProvider(InfrastructureDependencyProvider):
|
||||
"""Report Core runtime bindings that optional feature providers do not own."""
|
||||
|
||||
module_id = "ops"
|
||||
capability_ids = (
|
||||
"database.postgresql",
|
||||
"coordination.redis",
|
||||
"network.ingress",
|
||||
"runtime.load_balancing",
|
||||
)
|
||||
|
||||
def infrastructure_dependencies(self) -> tuple[InfrastructureDependency, ...]:
|
||||
dependencies = [
|
||||
InfrastructureDependency(
|
||||
capability_id="database.postgresql",
|
||||
module_id=self.module_id,
|
||||
dependency_type="application_state_binding",
|
||||
dependency_ref="runtime:database.postgresql",
|
||||
state="runtime_binding",
|
||||
scope="system",
|
||||
summary=(
|
||||
"GovOPlaN persists application and control-plane state in the active PostgreSQL database."
|
||||
),
|
||||
metrics={},
|
||||
required_action=(
|
||||
"Create and restore-verify a coordinated database backup before rebinding PostgreSQL."
|
||||
),
|
||||
),
|
||||
InfrastructureDependency(
|
||||
capability_id="network.ingress",
|
||||
module_id=self.module_id,
|
||||
dependency_type="public_runtime_binding",
|
||||
dependency_ref="runtime:network.ingress",
|
||||
state="runtime_binding",
|
||||
scope="system",
|
||||
summary="Ops and application APIs are reached through the active ingress boundary.",
|
||||
metrics={},
|
||||
required_action=(
|
||||
"Verify trusted proxy headers, TLS, health, and public reachability on the replacement ingress."
|
||||
),
|
||||
),
|
||||
InfrastructureDependency(
|
||||
capability_id="runtime.load_balancing",
|
||||
module_id=self.module_id,
|
||||
dependency_type="runtime_routing_binding",
|
||||
dependency_ref="runtime:load-balancing",
|
||||
state="runtime_binding",
|
||||
scope="system",
|
||||
summary="API and WebUI replicas are reached through the runtime load balancer.",
|
||||
metrics={},
|
||||
required_action=(
|
||||
"Verify health-aware API and WebUI routing before replacing the load balancer."
|
||||
),
|
||||
),
|
||||
]
|
||||
if core_settings.redis_url.strip():
|
||||
dependencies.append(
|
||||
InfrastructureDependency(
|
||||
capability_id="coordination.redis",
|
||||
module_id=self.module_id,
|
||||
dependency_type="coordination_runtime_binding",
|
||||
dependency_ref="runtime:coordination.redis",
|
||||
state="runtime_binding",
|
||||
scope="system",
|
||||
summary=(
|
||||
"The active runtime uses Redis for queues or shared coordination."
|
||||
),
|
||||
metrics={"celery_enabled": int(core_settings.celery_enabled)},
|
||||
required_action=(
|
||||
"Drain queued and in-flight work, verify idempotent recovery, and provision replacement coordination before rebinding or disabling Redis."
|
||||
),
|
||||
)
|
||||
)
|
||||
return tuple(dependencies)
|
||||
|
||||
|
||||
def deployment_capability_status(
|
||||
@@ -15,4 +103,31 @@ def deployment_capability_status(
|
||||
return _deployment_capability_status(path)
|
||||
|
||||
|
||||
__all__ = ["deployment_capability_status"]
|
||||
def infrastructure_dependency_inventory(
|
||||
registry: object,
|
||||
*,
|
||||
installation_id: str,
|
||||
) -> InfrastructureDependencyInventory:
|
||||
"""Collect module-owned persisted dependencies for an authorized host plan."""
|
||||
|
||||
receipt = load_infrastructure_capability_receipt()
|
||||
if receipt is None:
|
||||
raise ValueError(
|
||||
"Infrastructure dependency inventory requires a mounted deployment receipt."
|
||||
)
|
||||
if receipt.installation_id != installation_id:
|
||||
raise ValueError(
|
||||
"Mounted deployment receipt belongs to a different installation."
|
||||
)
|
||||
return collect_infrastructure_dependency_inventory(
|
||||
registry,
|
||||
installation_id=installation_id,
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"deployment_capability_status",
|
||||
"infrastructure_dependency_inventory",
|
||||
"OPS_INFRASTRUCTURE_DEPENDENCY_CAPABILITY",
|
||||
"OpsInfrastructureDependencyProvider",
|
||||
]
|
||||
|
||||
@@ -2,6 +2,10 @@ from __future__ import annotations
|
||||
|
||||
from govoplan_core.core.modules import with_documentation_structured_translations
|
||||
from govoplan_ops.backend.german_structured_documentation import GERMAN_STRUCTURED_TRANSLATIONS
|
||||
from govoplan_ops.backend.infrastructure import (
|
||||
OPS_INFRASTRUCTURE_DEPENDENCY_CAPABILITY,
|
||||
OpsInfrastructureDependencyProvider,
|
||||
)
|
||||
|
||||
from govoplan_core.core.access import (
|
||||
CAPABILITY_AUTH_PERMISSION_EVALUATOR,
|
||||
@@ -9,6 +13,7 @@ from govoplan_core.core.access import (
|
||||
)
|
||||
from govoplan_core.core.modules import (
|
||||
DocumentationCondition,
|
||||
DocumentationLink,
|
||||
DocumentationTopic,
|
||||
FrontendModule,
|
||||
FrontendRoute,
|
||||
@@ -95,7 +100,7 @@ def _route_factory(context: ModuleContext):
|
||||
manifest = ModuleManifest(
|
||||
id="ops",
|
||||
name="Ops",
|
||||
version="0.1.20",
|
||||
version="0.1.22",
|
||||
required_capabilities=(
|
||||
CAPABILITY_AUTH_PRINCIPAL_RESOLVER,
|
||||
CAPABILITY_AUTH_PERMISSION_EVALUATOR,
|
||||
@@ -137,15 +142,32 @@ manifest = ModuleManifest(
|
||||
cache_seconds=15,
|
||||
),
|
||||
),
|
||||
capability_factories={
|
||||
OPS_INFRASTRUCTURE_DEPENDENCY_CAPABILITY: (
|
||||
lambda context: OpsInfrastructureDependencyProvider()
|
||||
),
|
||||
},
|
||||
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. Worker and queue providers use a Core runtime-status contract, so Ops never imports a provider backend. The surface distinguishes intentionally disabled, unconfigured, starting, healthy with unsupported queue depth, measured idle, busy, degraded, stale, and unreachable states. It shows enabled/configured state, backend, workers, heartbeat age and stale threshold, queue depth, active/reserved work, and failures only when each value is actually reported; unavailable values are never rendered as zero or healthy. Local development treats intentionally disabled workers as expected, while production profiles require an enabled, configured, reachable provider before queue-backed work is accepted. Polling is bounded to one request, pauses while the page is hidden, and refreshes on return. When the deployment mounts a signed or locally generated non-secret infrastructure capability receipt, Ops shows whether PostgreSQL, Redis, SMTP, file storage, load balancing, and ingress are configured, externally supplied, available but unconfigured, or unavailable. Secret values never cross this boundary; only stable environment or credential-envelope references may be disclosed. Pending post-install tasks remain visible with a stable resume key. 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.",
|
||||
body="Read-only status distinguishes configured capabilities from healthy integrations. Worker and queue providers use a Core runtime-status contract, so Ops never imports a provider backend. The surface distinguishes intentionally disabled, unconfigured, starting, healthy with unsupported queue depth, measured idle, busy, degraded, stale, and unreachable states. It shows enabled/configured state, backend, workers, heartbeat age and stale threshold, queue depth, active/reserved work, and failures only when each value is actually reported; unavailable values are never rendered as zero or healthy. Local development treats intentionally disabled workers as expected, while production profiles require an enabled, configured, reachable provider before queue-backed work is accepted. Polling is bounded to one request, pauses while the page is hidden, and refreshes on return. When the deployment mounts a signed or locally generated non-secret infrastructure capability receipt, Ops shows whether PostgreSQL, Redis, SMTP, file storage, load balancing, and ingress are configured, externally supplied, available but unconfigured, or unavailable. Secret values never cross this boundary; only stable environment or credential-envelope references may be disclosed. Pending post-install tasks remain visible with a stable resume key. The separately authorized infrastructure-dependency endpoint asks enabled module providers for their current persisted configuration and data dependencies. Each provider returns only stable references, states, scopes, numeric metrics, and required migration actions; one failed provider marks the entire fresh inventory incomplete so the host deployer blocks a capability change. 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"),
|
||||
links=(
|
||||
DocumentationLink(
|
||||
label="Infrastructure dependency inventory API",
|
||||
href="/api/v1/ops/infrastructure/dependencies",
|
||||
kind="api",
|
||||
),
|
||||
DocumentationLink(
|
||||
label="Operations profile handbook",
|
||||
href="govoplan-ops/docs/SCALABILITY_PROFILES.md",
|
||||
kind="repository",
|
||||
),
|
||||
),
|
||||
translations={
|
||||
"de": {
|
||||
"title": "Plattformzustand und Bereitstellungsprofil prüfen",
|
||||
@@ -159,7 +181,9 @@ manifest = ModuleManifest(
|
||||
"Die Abfrage bleibt auf eine Anfrage je Intervall begrenzt, pausiert bei ausgeblendeter Seite und wird bei der Rückkehr fortgesetzt. "
|
||||
"Ist ein signierter oder lokal erzeugter Infrastrukturbeleg ohne Geheimwerte eingebunden, zeigt Ops für PostgreSQL, Redis, SMTP, Dateispeicher, Lastverteilung und Ingress, ob die Fähigkeit konfiguriert, extern bereitgestellt, verfügbar aber nicht konfiguriert oder nicht verfügbar ist. "
|
||||
"Geheimwerte überschreiten diese Grenze nie; offengelegt werden dürfen nur stabile Umgebungs- oder Credential-Envelope-Referenzen. "
|
||||
"Ausstehende Aufgaben nach einer Installation bleiben mit einem stabilen Fortsetzungsschlüssel sichtbar. Autorisierte Betriebsverantwortliche dürfen begrenzte Prüfungen ausführen; eine Prüfung darf weder unbegrenzte Facharbeit auslösen noch Daten stillschweigend reparieren. "
|
||||
"Ausstehende Aufgaben nach einer Installation bleiben mit einem stabilen Fortsetzungsschlüssel sichtbar. Der separat autorisierte Infrastruktur-Abhängigkeitsendpunkt fragt aktivierte Modulprovider nach ihren aktuellen gespeicherten Konfigurations- und Datenabhängigkeiten. "
|
||||
"Jeder Provider liefert nur stabile Referenzen, Zustände, Geltungsbereiche, numerische Kennzahlen und erforderliche Migrationsmaßnahmen; schlägt ein Provider fehl, ist das gesamte frische Inventar unvollständig und der Host-Deployer blockiert die Fähigkeitsänderung. "
|
||||
"Autorisierte Betriebsverantwortliche dürfen begrenzte Prüfungen ausführen; eine Prüfung darf weder unbegrenzte Facharbeit auslösen noch Daten stillschweigend reparieren. "
|
||||
"Nutzen Sie Bereitschafts- und Worker-Ergebnisse zur Diagnose eines Knotens sowie Bereitstellungsprofil und Dimensionierungsannahmen zur Planung horizontaler Kapazität."
|
||||
),
|
||||
}
|
||||
@@ -185,7 +209,7 @@ manifest = ModuleManifest(
|
||||
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.",
|
||||
summary="Ops projects shared runtime heartbeats, replica gaps, drain controls, and recovery states that require operator attention. Node controls remain pinned at the right edge of horizontally scrolled runtime tables.",
|
||||
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. Files blob writes, hard purge, reference-checked garbage collection, and conditional S3 connector writes record Core recovery evidence. For a Files connector outcome, inspect the provider request/content markers and revision before allowing another write to the fenced path; for blob GC, recheck FileVersion references and exact object absence. Development SQLite can show only handled-rollback reconstruction for caller-transaction blob uploads; after a hard SQLite process loss, run the owning Files integrity scan because an orphan may have no Ops ledger row. 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"),
|
||||
@@ -199,7 +223,7 @@ manifest = ModuleManifest(
|
||||
translations={
|
||||
"de": {
|
||||
"title": "Laufzeitknoten leeren und Wiederherstellungsnachweise prüfen",
|
||||
"summary": "Ops projiziert gemeinsame Laufzeit-Heartbeats, Replikatlücken, Leerungssteuerung und Wiederherstellungszustände, die betriebliche Aufmerksamkeit erfordern.",
|
||||
"summary": "Ops projiziert gemeinsame Laufzeit-Heartbeats, Replikatlücken, Leerungssteuerung und Wiederherstellungszustände, die betriebliche Aufmerksamkeit erfordern. Die Knotensteuerung bleibt am rechten Rand horizontal gescrollter Laufzeittabellen angeheftet.",
|
||||
"body": (
|
||||
"Verwenden Sie die Laufzeittabelle, um veraltete API- und Worker-Replikate oder Replikate mit abweichender Modulzusammensetzung zu erkennen. "
|
||||
"Leeren Sie einen Knoten vor dem Austausch, damit seine API-Bereitschaft geschlossen wird und Worker keine neue Warteschlangenarbeit annehmen; solange der Knoten noch geleert wird, kann der Vorgang abgebrochen werden. "
|
||||
|
||||
@@ -3,12 +3,73 @@ from __future__ import annotations
|
||||
import json
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
from types import SimpleNamespace
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from govoplan_ops.backend.infrastructure import deployment_capability_status
|
||||
from govoplan_core.core.infrastructure_capabilities import InfrastructureDependency
|
||||
from govoplan_ops.backend.infrastructure import (
|
||||
OpsInfrastructureDependencyProvider,
|
||||
deployment_capability_status,
|
||||
infrastructure_dependency_inventory,
|
||||
)
|
||||
|
||||
|
||||
class _Provider:
|
||||
module_id = "mail"
|
||||
capability_ids = ("mail.smtp",)
|
||||
|
||||
def infrastructure_dependencies(self) -> tuple[InfrastructureDependency, ...]:
|
||||
return (
|
||||
InfrastructureDependency(
|
||||
capability_id="mail.smtp",
|
||||
module_id="mail",
|
||||
dependency_type="smtp_endpoint",
|
||||
dependency_ref="mail:server-1",
|
||||
state="active",
|
||||
scope="system",
|
||||
summary="Persisted SMTP endpoint.",
|
||||
metrics={"credential_binding_count": 1},
|
||||
required_action="Rebind it before changing SMTP.",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class _Registry:
|
||||
def capability_names(self) -> tuple[str, ...]:
|
||||
return ("infrastructure.dependency_inventory.mail",)
|
||||
|
||||
def capability(self, name: str) -> object | None:
|
||||
return _Provider() if name.endswith(".mail") else None
|
||||
|
||||
|
||||
class InfrastructureCapabilityTests(unittest.TestCase):
|
||||
def test_collects_authorized_module_dependency_inventory(self) -> None:
|
||||
with patch(
|
||||
"govoplan_ops.backend.infrastructure.load_infrastructure_capability_receipt",
|
||||
return_value=SimpleNamespace(installation_id="govoplan-test"),
|
||||
):
|
||||
result = infrastructure_dependency_inventory(
|
||||
_Registry(),
|
||||
installation_id="govoplan-test",
|
||||
).to_dict()
|
||||
|
||||
self.assertTrue(result["complete"])
|
||||
self.assertEqual("mail.smtp", result["dependencies"][0]["capability_id"])
|
||||
self.assertEqual(1, result["dependencies"][0]["metrics"]["credential_binding_count"])
|
||||
|
||||
def test_ops_provider_reports_runtime_bindings_without_endpoint_secrets(self) -> None:
|
||||
dependencies = OpsInfrastructureDependencyProvider().infrastructure_dependencies()
|
||||
by_capability = {item.capability_id: item for item in dependencies}
|
||||
|
||||
self.assertIn("database.postgresql", by_capability)
|
||||
self.assertIn("network.ingress", by_capability)
|
||||
self.assertEqual(
|
||||
"runtime:database.postgresql",
|
||||
by_capability["database.postgresql"].dependency_ref,
|
||||
)
|
||||
self.assertNotIn("postgresql://", json.dumps([item.to_dict() for item in dependencies]))
|
||||
|
||||
def test_reads_bounded_non_secret_capability_receipt(self) -> None:
|
||||
with tempfile.TemporaryDirectory(prefix="govoplan-ops-capabilities-") as root:
|
||||
path = Path(root) / "capabilities.json"
|
||||
|
||||
+3
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@govoplan/ops-webui",
|
||||
"version": "0.1.20",
|
||||
"version": "0.1.22",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
@@ -13,10 +13,11 @@
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"test:action-layout": "node scripts/test-action-layout.mjs",
|
||||
"test:runtime-status": "rm -rf .runtime-status-test-build && mkdir -p .runtime-status-test-build && printf '{\"type\":\"commonjs\"}\\n' > .runtime-status-test-build/package.json && ../../govoplan-core/webui/node_modules/.bin/tsc -p tsconfig.runtime-status-tests.json && node .runtime-status-test-build/tests/runtime-status.test.js"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@govoplan/core-webui": "^0.1.18",
|
||||
"@govoplan/core-webui": "^0.1.45",
|
||||
"lucide-react": "^1.23.0",
|
||||
"react": ">=19.2.7 <20",
|
||||
"react-dom": ">=19.2.7 <20",
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
const page = readFileSync(new URL("../src/features/ops/OpsPage.tsx", import.meta.url), "utf8");
|
||||
assert.match(page, /id: "actions",\s*header: "",\s*width: 72,\s*sticky: "end",\s*resizable: false,\s*align: "right"/);
|
||||
assert.match(page, /<TableActionGroup\s*minimumSlots=\{1\}/);
|
||||
console.log("Runtime node controls use the shared pinned action layout.");
|
||||
@@ -399,7 +399,10 @@ function RuntimeNodeTable({
|
||||
{
|
||||
id: "actions",
|
||||
header: "",
|
||||
width: 56,
|
||||
width: 72,
|
||||
sticky: "end",
|
||||
resizable: false,
|
||||
align: "right",
|
||||
value: (node) => node.state,
|
||||
render: (node) => {
|
||||
const busy = busyNodeId === node.node_id;
|
||||
|
||||
Reference in New Issue
Block a user