feat(delivery): add closure workbench summary api
This commit is contained in:
@@ -322,6 +322,9 @@ from src.services.dependency_supply_chain_drift_monitor import (
|
||||
from src.services.dependency_upgrade_approval_package_template import (
|
||||
load_latest_dependency_upgrade_approval_package_template,
|
||||
)
|
||||
from src.services.delivery_closure_workbench import (
|
||||
load_delivery_closure_workbench,
|
||||
)
|
||||
from src.services.docker_build_surface_inventory import (
|
||||
load_latest_docker_build_surface_inventory,
|
||||
)
|
||||
@@ -896,6 +899,36 @@ async def get_awoooi_status_cleanup_dashboard() -> dict[str, Any]:
|
||||
) from exc
|
||||
|
||||
|
||||
@router.get(
|
||||
"/delivery-closure-workbench",
|
||||
response_model=dict[str, Any],
|
||||
summary="取得交付閉環工作台彙總",
|
||||
description=(
|
||||
"彙總 AWOOOI 狀態清理、GitHub 私有備援、Gitea / CI-CD、Runtime surface "
|
||||
"與 Backup / DR 的既有只讀快照,回傳前端工作台可直接使用的交付主線、"
|
||||
"完成度、阻擋數與下一步。此端點不呼叫 GitHub / Gitea / Wazuh / K8s live API、"
|
||||
"不建立 repo、不改 visibility、不同步 refs、不觸發 workflow、不執行備份或還原、"
|
||||
"不讀 secret、不做 runtime write。"
|
||||
),
|
||||
)
|
||||
async def get_delivery_closure_workbench() -> dict[str, Any]:
|
||||
"""回傳 AWOOOI 交付閉環工作台只讀彙總。"""
|
||||
try:
|
||||
payload = await asyncio.to_thread(load_delivery_closure_workbench)
|
||||
return redact_public_lan_topology(payload)
|
||||
except FileNotFoundError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=str(exc),
|
||||
) from exc
|
||||
except (json.JSONDecodeError, ValueError) as exc:
|
||||
logger.error("delivery_closure_workbench_invalid", error=str(exc))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="AWOOOI 交付閉環工作台彙總無效",
|
||||
) from exc
|
||||
|
||||
|
||||
@router.get(
|
||||
"/agent-12-agent-war-room",
|
||||
response_model=dict[str, Any],
|
||||
|
||||
321
apps/api/src/services/delivery_closure_workbench.py
Normal file
321
apps/api/src/services/delivery_closure_workbench.py
Normal file
@@ -0,0 +1,321 @@
|
||||
"""Delivery closure workbench summary.
|
||||
|
||||
Builds the product-facing delivery closure view from existing committed,
|
||||
read-only snapshots. The summary is intentionally compact so the UI does not
|
||||
need to fan out across five separate endpoints or duplicate blocker math.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from src.services.awoooi_status_cleanup_dashboard import (
|
||||
load_latest_awoooi_status_cleanup_dashboard,
|
||||
)
|
||||
from src.services.backup_dr_readiness_matrix import (
|
||||
load_latest_backup_dr_readiness_matrix,
|
||||
)
|
||||
from src.services.gitea_workflow_runner_health import (
|
||||
load_latest_gitea_workflow_runner_health,
|
||||
)
|
||||
from src.services.runtime_surface_inventory import (
|
||||
load_latest_runtime_surface_inventory,
|
||||
)
|
||||
|
||||
_SCHEMA_VERSION = "delivery_closure_workbench_v1"
|
||||
|
||||
|
||||
def load_delivery_closure_workbench() -> dict[str, Any]:
|
||||
"""Load existing delivery snapshots and return a compact workbench model."""
|
||||
status_cleanup = load_latest_awoooi_status_cleanup_dashboard()
|
||||
github = _load_github_private_backup_evidence_gate()
|
||||
gitea = load_latest_gitea_workflow_runner_health()
|
||||
runtime = load_latest_runtime_surface_inventory()
|
||||
backup = load_latest_backup_dr_readiness_matrix()
|
||||
return build_delivery_closure_workbench(
|
||||
status_cleanup=status_cleanup,
|
||||
github=github,
|
||||
gitea=gitea,
|
||||
runtime=runtime,
|
||||
backup=backup,
|
||||
)
|
||||
|
||||
|
||||
def build_delivery_closure_workbench(
|
||||
*,
|
||||
status_cleanup: dict[str, Any],
|
||||
github: dict[str, Any],
|
||||
gitea: dict[str, Any],
|
||||
runtime: dict[str, Any],
|
||||
backup: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
"""Build the delivery workbench response from already validated snapshots."""
|
||||
status_summary = _dict(status_cleanup.get("summary"))
|
||||
github_summary = _dict(github.get("summary"))
|
||||
gitea_status = _dict(gitea.get("program_status"))
|
||||
gitea_rollups = _dict(gitea.get("rollups"))
|
||||
runtime_status = _dict(runtime.get("program_status"))
|
||||
runtime_rollups = _dict(runtime.get("rollups"))
|
||||
backup_status = _dict(backup.get("program_status"))
|
||||
backup_rollups = _dict(backup.get("rollups"))
|
||||
|
||||
github_required = _int(github_summary.get("approval_required_target_count"))
|
||||
github_verified = _int(github_summary.get("private_backup_verified_count"))
|
||||
runtime_action_required = set(_strings(runtime_rollups.get("action_required_surface_ids")))
|
||||
runtime_secret_surfaces = set(_strings(runtime_rollups.get("secret_surface_ids")))
|
||||
|
||||
lanes = [
|
||||
{
|
||||
"id": "release",
|
||||
"source_id": "status_cleanup",
|
||||
"completion_percent": _percent(status_summary.get("overall_completion_percent")),
|
||||
"status": str(status_summary.get("dashboard_status") or "unknown"),
|
||||
"blocker_count": _int(status_summary.get("blocked_gate_count")),
|
||||
"metric": {
|
||||
"kind": "blocked_gate",
|
||||
"blocked": _int(status_summary.get("blocked_gate_count")),
|
||||
"total": _int(status_summary.get("gate_count")),
|
||||
},
|
||||
"href": "/governance?tab=automation-inventory",
|
||||
"next_action": _first_string(status_cleanup.get("next_actions")),
|
||||
},
|
||||
{
|
||||
"id": "github",
|
||||
"source_id": "github_private_backup",
|
||||
"completion_percent": _percent(
|
||||
(github_verified / github_required) * 100 if github_required else 0
|
||||
),
|
||||
"status": str(github.get("status") or "unknown"),
|
||||
"blocker_count": _int(github_summary.get("blocked_target_count")),
|
||||
"metric": {
|
||||
"kind": "private_backup_verified",
|
||||
"verified": github_verified,
|
||||
"total": github_required,
|
||||
},
|
||||
"href": "/governance?tab=automation-inventory",
|
||||
"next_action": str(github.get("next_action") or _first_target_action(github.get("targets"))),
|
||||
},
|
||||
{
|
||||
"id": "gitea",
|
||||
"source_id": "gitea_ci_cd",
|
||||
"completion_percent": _percent(gitea_status.get("overall_completion_percent")),
|
||||
"status": str(gitea_status.get("current_task_id") or "unknown"),
|
||||
"blocker_count": len(_strings(gitea_rollups.get("runner_contracts_requiring_action"))),
|
||||
"metric": {
|
||||
"kind": "workflow_count",
|
||||
"count": _int(gitea_rollups.get("total_workflows")),
|
||||
},
|
||||
"href": "/deployments",
|
||||
"next_action": _first_contract_action(gitea.get("runner_contracts")),
|
||||
},
|
||||
{
|
||||
"id": "runtime",
|
||||
"source_id": "runtime_surface",
|
||||
"completion_percent": _percent(runtime_status.get("overall_completion_percent")),
|
||||
"status": str(runtime_status.get("current_task_id") or "unknown"),
|
||||
"blocker_count": len(runtime_action_required | runtime_secret_surfaces),
|
||||
"metric": {
|
||||
"kind": "surface_count",
|
||||
"total": _int(runtime_rollups.get("total_surfaces")),
|
||||
},
|
||||
"href": "/governance?tab=automation-inventory",
|
||||
"next_action": _first_surface_action(runtime.get("runtime_surfaces")),
|
||||
},
|
||||
{
|
||||
"id": "backup",
|
||||
"source_id": "backup_dr",
|
||||
"completion_percent": _percent(backup_status.get("overall_completion_percent")),
|
||||
"status": str(backup_status.get("current_task_id") or "unknown"),
|
||||
"blocker_count": len(_strings(backup_rollups.get("blocked_row_ids"))),
|
||||
"metric": {
|
||||
"kind": "readiness_row_count",
|
||||
"rows": _int(backup_rollups.get("total_rows")),
|
||||
},
|
||||
"href": "/operations",
|
||||
"next_action": _first_backup_action(backup.get("readiness_rows")),
|
||||
},
|
||||
]
|
||||
|
||||
for lane in lanes:
|
||||
lane["tone"] = _tone(_int(lane["blocker_count"]), _int(lane["completion_percent"]))
|
||||
|
||||
source_statuses = [
|
||||
_source_status("status_cleanup", status_cleanup),
|
||||
_source_status("github_private_backup", github),
|
||||
_source_status("gitea_ci_cd", gitea),
|
||||
_source_status("runtime_surface", runtime),
|
||||
_source_status("backup_dr", backup),
|
||||
]
|
||||
generated_candidates = [source["generated_at"] for source in source_statuses if source["generated_at"]]
|
||||
loaded_source_count = sum(1 for source in source_statuses if source["loaded"])
|
||||
high_risk_blocker_count = sum(_int(lane["blocker_count"]) for lane in lanes)
|
||||
average_completion = _percent(
|
||||
sum(_int(lane["completion_percent"]) for lane in lanes) / max(len(lanes), 1)
|
||||
)
|
||||
next_focus = [
|
||||
{
|
||||
"lane_id": lane["id"],
|
||||
"blocker_count": lane["blocker_count"],
|
||||
"completion_percent": lane["completion_percent"],
|
||||
"next_action": lane["next_action"],
|
||||
}
|
||||
for lane in lanes
|
||||
if _int(lane["blocker_count"]) > 0 or _int(lane["completion_percent"]) < 80
|
||||
][:5]
|
||||
|
||||
return {
|
||||
"schema_version": _SCHEMA_VERSION,
|
||||
"generated_at": max(generated_candidates) if generated_candidates else "",
|
||||
"status": "blocked_delivery_actions_required" if high_risk_blocker_count else "ready",
|
||||
"summary": {
|
||||
"source_count": len(source_statuses),
|
||||
"loaded_source_count": loaded_source_count,
|
||||
"average_completion_percent": average_completion,
|
||||
"high_risk_blocker_count": high_risk_blocker_count,
|
||||
"runtime_execution_authorized": False,
|
||||
"remote_write_authorized": False,
|
||||
"repo_creation_authorized": False,
|
||||
"refs_sync_authorized": False,
|
||||
"workflow_trigger_authorized": False,
|
||||
"secret_values_collected": False,
|
||||
},
|
||||
"source_statuses": source_statuses,
|
||||
"lanes": lanes,
|
||||
"next_focus": next_focus,
|
||||
"operation_boundaries": {
|
||||
"read_only_api_allowed": True,
|
||||
"runtime_write_allowed": False,
|
||||
"remote_write_allowed": False,
|
||||
"repo_creation_allowed": False,
|
||||
"visibility_change_allowed": False,
|
||||
"refs_sync_allowed": False,
|
||||
"workflow_trigger_allowed": False,
|
||||
"secret_value_collection_allowed": False,
|
||||
"backup_restore_execution_allowed": False,
|
||||
"active_scan_allowed": False,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _source_status(source_id: str, payload: dict[str, Any]) -> dict[str, Any]:
|
||||
source_missing = payload.get("source_missing") is True
|
||||
return {
|
||||
"id": source_id,
|
||||
"loaded": not source_missing,
|
||||
"schema_version": str(payload.get("schema_version") or ""),
|
||||
"generated_at": str(payload.get("generated_at") or ""),
|
||||
"missing_reason": str(payload.get("missing_reason") or "") if source_missing else "",
|
||||
}
|
||||
|
||||
|
||||
def _load_github_private_backup_evidence_gate() -> dict[str, Any]:
|
||||
try:
|
||||
from src.services.github_target_private_backup_evidence_gate import (
|
||||
load_latest_github_target_private_backup_evidence_gate,
|
||||
)
|
||||
|
||||
return load_latest_github_target_private_backup_evidence_gate()
|
||||
except ModuleNotFoundError as exc:
|
||||
if exc.name != "src.services.github_target_private_backup_evidence_gate":
|
||||
raise
|
||||
return _missing_github_private_backup_source("service_module_missing_on_release_base")
|
||||
except FileNotFoundError:
|
||||
return _missing_github_private_backup_source("snapshot_missing_on_release_base")
|
||||
|
||||
|
||||
def _missing_github_private_backup_source(reason: str) -> dict[str, Any]:
|
||||
return {
|
||||
"schema_version": "missing_github_target_private_backup_evidence_gate_v1",
|
||||
"generated_at": "",
|
||||
"source_missing": True,
|
||||
"missing_reason": reason,
|
||||
"status": "blocked_github_private_backup_source_missing",
|
||||
"next_action": "補上 GitHub 私有備援 evidence gate source 後重新回讀,不可把缺席來源標成已驗證。",
|
||||
"summary": {
|
||||
"approval_required_target_count": 0,
|
||||
"private_backup_verified_count": 0,
|
||||
"blocked_target_count": 1,
|
||||
},
|
||||
"targets": [],
|
||||
}
|
||||
|
||||
|
||||
def _tone(blocker_count: int, percent: int) -> str:
|
||||
if blocker_count > 0:
|
||||
return "danger"
|
||||
if percent < 80:
|
||||
return "warn"
|
||||
return "ok"
|
||||
|
||||
|
||||
def _dict(value: Any) -> dict[str, Any]:
|
||||
return value if isinstance(value, dict) else {}
|
||||
|
||||
|
||||
def _int(value: Any) -> int:
|
||||
if isinstance(value, bool):
|
||||
return int(value)
|
||||
if isinstance(value, (int, float)):
|
||||
return int(value)
|
||||
return 0
|
||||
|
||||
|
||||
def _percent(value: Any) -> int:
|
||||
return max(0, min(100, round(float(value or 0))))
|
||||
|
||||
|
||||
def _strings(value: Any) -> list[str]:
|
||||
if not isinstance(value, list):
|
||||
return []
|
||||
return [str(item) for item in value if item is not None]
|
||||
|
||||
|
||||
def _first_string(value: Any) -> str:
|
||||
if isinstance(value, list) and value:
|
||||
return str(value[0])
|
||||
return ""
|
||||
|
||||
|
||||
def _first_target_action(value: Any) -> str:
|
||||
if not isinstance(value, list):
|
||||
return ""
|
||||
for row in value:
|
||||
if isinstance(row, dict) and row.get("approval_required") is True:
|
||||
return str(row.get("next_action") or "")
|
||||
return _first_row_action(value)
|
||||
|
||||
|
||||
def _first_contract_action(value: Any) -> str:
|
||||
if not isinstance(value, list):
|
||||
return ""
|
||||
for row in value:
|
||||
if isinstance(row, dict) and row.get("status") == "action_required":
|
||||
return str(row.get("next_action") or "")
|
||||
return _first_row_action(value)
|
||||
|
||||
|
||||
def _first_surface_action(value: Any) -> str:
|
||||
if not isinstance(value, list):
|
||||
return ""
|
||||
for row in value:
|
||||
if isinstance(row, dict) and row.get("status") != "manifest_mapped":
|
||||
return str(row.get("next_action") or "")
|
||||
return _first_row_action(value)
|
||||
|
||||
|
||||
def _first_backup_action(value: Any) -> str:
|
||||
if not isinstance(value, list):
|
||||
return ""
|
||||
for row in value:
|
||||
if isinstance(row, dict) and row.get("overall_readiness") in {"blocked", "action_required"}:
|
||||
return str(row.get("next_action") or "")
|
||||
return _first_row_action(value)
|
||||
|
||||
|
||||
def _first_row_action(value: Any) -> str:
|
||||
if not isinstance(value, list):
|
||||
return ""
|
||||
for row in value:
|
||||
if isinstance(row, dict) and row.get("next_action"):
|
||||
return str(row["next_action"])
|
||||
return ""
|
||||
57
apps/api/tests/test_delivery_closure_workbench_api.py
Normal file
57
apps/api/tests/test_delivery_closure_workbench_api.py
Normal file
@@ -0,0 +1,57 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from src.api.v1.agents import router
|
||||
|
||||
|
||||
def test_delivery_closure_workbench_endpoint_returns_product_summary():
|
||||
app = FastAPI()
|
||||
app.include_router(router, prefix="/api/v1")
|
||||
client = TestClient(app)
|
||||
|
||||
response = client.get("/api/v1/agents/delivery-closure-workbench")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["schema_version"] == "delivery_closure_workbench_v1"
|
||||
assert data["summary"]["source_count"] == 5
|
||||
assert 4 <= data["summary"]["loaded_source_count"] <= 5
|
||||
assert data["summary"]["runtime_execution_authorized"] is False
|
||||
assert data["summary"]["remote_write_authorized"] is False
|
||||
assert data["summary"]["repo_creation_authorized"] is False
|
||||
assert data["summary"]["refs_sync_authorized"] is False
|
||||
assert data["summary"]["workflow_trigger_authorized"] is False
|
||||
assert data["summary"]["secret_values_collected"] is False
|
||||
assert data["summary"]["average_completion_percent"] >= 0
|
||||
assert data["summary"]["high_risk_blocker_count"] > 0
|
||||
|
||||
lanes = {lane["id"]: lane for lane in data["lanes"]}
|
||||
sources = {source["id"]: source for source in data["source_statuses"]}
|
||||
assert sorted(lanes) == ["backup", "gitea", "github", "release", "runtime"]
|
||||
assert lanes["release"]["metric"]["kind"] == "blocked_gate"
|
||||
assert lanes["github"]["metric"]["kind"] == "private_backup_verified"
|
||||
assert lanes["gitea"]["metric"]["kind"] == "workflow_count"
|
||||
assert lanes["runtime"]["metric"]["kind"] == "surface_count"
|
||||
assert lanes["backup"]["metric"]["kind"] == "readiness_row_count"
|
||||
if sources["github_private_backup"]["loaded"] is False:
|
||||
assert lanes["github"]["blocker_count"] == 1
|
||||
assert lanes["github"]["status"] == "blocked_github_private_backup_source_missing"
|
||||
assert sources["github_private_backup"]["missing_reason"]
|
||||
assert all(0 <= lane["completion_percent"] <= 100 for lane in lanes.values())
|
||||
assert all(lane["tone"] in {"ok", "warn", "danger"} for lane in lanes.values())
|
||||
|
||||
boundaries = data["operation_boundaries"]
|
||||
assert boundaries["read_only_api_allowed"] is True
|
||||
assert boundaries["runtime_write_allowed"] is False
|
||||
assert boundaries["remote_write_allowed"] is False
|
||||
assert boundaries["repo_creation_allowed"] is False
|
||||
assert boundaries["visibility_change_allowed"] is False
|
||||
assert boundaries["refs_sync_allowed"] is False
|
||||
assert boundaries["workflow_trigger_allowed"] is False
|
||||
assert boundaries["secret_value_collection_allowed"] is False
|
||||
assert boundaries["backup_restore_execution_allowed"] is False
|
||||
assert boundaries["active_scan_allowed"] is False
|
||||
|
||||
assert "192.168.0." not in response.text
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
apiClient,
|
||||
type AwoooIStatusCleanupDashboardSnapshot,
|
||||
type BackupDrReadinessMatrixSnapshot,
|
||||
type DeliveryClosureWorkbenchSnapshot,
|
||||
type GiteaWorkflowRunnerHealthSnapshot,
|
||||
type GithubTargetPrivateBackupEvidenceGateSnapshot,
|
||||
type RuntimeSurfaceInventorySnapshot,
|
||||
@@ -44,11 +45,14 @@ interface DeliveryLane {
|
||||
status: string
|
||||
metric: string
|
||||
blockerCount: number
|
||||
nextAction: string
|
||||
href: string
|
||||
tone: DeliveryTone
|
||||
Icon: typeof Rocket
|
||||
}
|
||||
|
||||
const SOURCE_COUNT = 5
|
||||
|
||||
const EMPTY_DATA: DeliveryData = {
|
||||
statusCleanup: null,
|
||||
github: null,
|
||||
@@ -79,6 +83,18 @@ const resolveTone = (blockerCount: number, percent: number): DeliveryTone => {
|
||||
return 'ok'
|
||||
}
|
||||
|
||||
const firstActionRequiredTarget = (data: GithubTargetPrivateBackupEvidenceGateSnapshot | null) =>
|
||||
data?.targets.find(target => target.approval_required)?.next_action ?? ''
|
||||
|
||||
const firstActionRequiredRunner = (data: GiteaWorkflowRunnerHealthSnapshot | null) =>
|
||||
data?.runner_contracts.find(contract => contract.status === 'action_required')?.next_action ?? ''
|
||||
|
||||
const firstRuntimeAction = (data: RuntimeSurfaceInventorySnapshot | null) =>
|
||||
data?.runtime_surfaces.find(surface => surface.status !== 'manifest_mapped')?.next_action ?? ''
|
||||
|
||||
const firstBackupAction = (data: BackupDrReadinessMatrixSnapshot | null) =>
|
||||
data?.readiness_rows.find(row => row.overall_readiness === 'blocked' || row.overall_readiness === 'action_required')?.next_action ?? ''
|
||||
|
||||
function StatusPill({ tone, label }: { tone: DeliveryTone; label: string }) {
|
||||
return (
|
||||
<span
|
||||
@@ -212,6 +228,7 @@ function LaneCard({ lane, locale }: { lane: DeliveryLane; locale: string }) {
|
||||
|
||||
export default function DeliveryPage({ params }: { params: { locale: string } }) {
|
||||
const t = useTranslations('delivery')
|
||||
const [workbench, setWorkbench] = useState<DeliveryClosureWorkbenchSnapshot | null>(null)
|
||||
const [data, setData] = useState<DeliveryData>(EMPTY_DATA)
|
||||
const [errors, setErrors] = useState<string[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
@@ -220,6 +237,19 @@ export default function DeliveryPage({ params }: { params: { locale: string } })
|
||||
let cancelled = false
|
||||
const load = async () => {
|
||||
setLoading(true)
|
||||
setErrors([])
|
||||
try {
|
||||
const summary = await apiClient.getDeliveryClosureWorkbench()
|
||||
if (cancelled) return
|
||||
setWorkbench(summary)
|
||||
setData(EMPTY_DATA)
|
||||
setErrors([])
|
||||
setLoading(false)
|
||||
return
|
||||
} catch {
|
||||
// Summary endpoint may not exist until the API release lands; keep the page useful with legacy reads.
|
||||
}
|
||||
|
||||
const results = await Promise.allSettled([
|
||||
apiClient.getAwoooIStatusCleanupDashboard(),
|
||||
apiClient.getGithubTargetPrivateBackupEvidenceGate(),
|
||||
@@ -241,6 +271,7 @@ export default function DeliveryPage({ params }: { params: { locale: string } })
|
||||
.filter(({ result }) => result.status === 'rejected')
|
||||
.map(({ index }) => t(`sources.${index}.error`))
|
||||
|
||||
setWorkbench(null)
|
||||
setData(nextData)
|
||||
setErrors(nextErrors)
|
||||
setLoading(false)
|
||||
@@ -253,6 +284,42 @@ export default function DeliveryPage({ params }: { params: { locale: string } })
|
||||
}, [t])
|
||||
|
||||
const lanes = useMemo<DeliveryLane[]>(() => {
|
||||
if (workbench) {
|
||||
return workbench.lanes.map(lane => {
|
||||
const iconMap = {
|
||||
release: Rocket,
|
||||
github: GitBranch,
|
||||
gitea: PackageCheck,
|
||||
runtime: Server,
|
||||
backup: HardDrive,
|
||||
}
|
||||
const metric =
|
||||
lane.metric.kind === 'blocked_gate'
|
||||
? t('lanes.release.metric', { blocked: lane.metric.blocked })
|
||||
: lane.metric.kind === 'private_backup_verified'
|
||||
? t('lanes.github.metric', { verified: lane.metric.verified, total: lane.metric.total })
|
||||
: lane.metric.kind === 'workflow_count'
|
||||
? t('lanes.gitea.metric', { count: lane.metric.count })
|
||||
: lane.metric.kind === 'surface_count'
|
||||
? t('lanes.runtime.metric', { total: lane.metric.total })
|
||||
: t('lanes.backup.metric', { rows: lane.metric.rows })
|
||||
|
||||
return {
|
||||
id: lane.id,
|
||||
title: t(`lanes.${lane.id}.title`),
|
||||
description: t(`lanes.${lane.id}.description`),
|
||||
percent: clampPercent(lane.completion_percent),
|
||||
status: lane.status || t('states.noData'),
|
||||
metric,
|
||||
blockerCount: lane.blocker_count,
|
||||
nextAction: lane.next_action,
|
||||
href: lane.href,
|
||||
tone: lane.tone,
|
||||
Icon: iconMap[lane.id],
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const statusBlocked = Number(data.statusCleanup?.summary.blocked_gate_count ?? 0)
|
||||
const statusPercent = clampPercent(data.statusCleanup?.summary.overall_completion_percent)
|
||||
const githubRequired = data.github?.summary.approval_required_target_count ?? 0
|
||||
@@ -275,6 +342,7 @@ export default function DeliveryPage({ params }: { params: { locale: string } })
|
||||
status: data.statusCleanup?.summary.dashboard_status ?? t('states.noData'),
|
||||
metric: t('lanes.release.metric', { blocked: statusBlocked }),
|
||||
blockerCount: statusBlocked,
|
||||
nextAction: data.statusCleanup?.next_actions[0] ?? '',
|
||||
href: '/governance?tab=automation-inventory',
|
||||
tone: resolveTone(statusBlocked, statusPercent),
|
||||
Icon: Rocket,
|
||||
@@ -287,6 +355,7 @@ export default function DeliveryPage({ params }: { params: { locale: string } })
|
||||
status: data.github?.status ?? t('states.noData'),
|
||||
metric: t('lanes.github.metric', { verified: githubVerified, total: githubRequired }),
|
||||
blockerCount: githubBlocked,
|
||||
nextAction: firstActionRequiredTarget(data.github),
|
||||
href: '/governance?tab=automation-inventory',
|
||||
tone: resolveTone(githubBlocked, githubPercent),
|
||||
Icon: GitBranch,
|
||||
@@ -299,6 +368,7 @@ export default function DeliveryPage({ params }: { params: { locale: string } })
|
||||
status: data.gitea?.program_status.current_task_id ?? t('states.noData'),
|
||||
metric: t('lanes.gitea.metric', { count: data.gitea?.rollups.total_workflows ?? 0 }),
|
||||
blockerCount: giteaBlocked,
|
||||
nextAction: firstActionRequiredRunner(data.gitea),
|
||||
href: '/deployments',
|
||||
tone: resolveTone(giteaBlocked, giteaPercent),
|
||||
Icon: PackageCheck,
|
||||
@@ -311,6 +381,7 @@ export default function DeliveryPage({ params }: { params: { locale: string } })
|
||||
status: data.runtime?.program_status.current_task_id ?? t('states.noData'),
|
||||
metric: t('lanes.runtime.metric', { total: data.runtime?.rollups.total_surfaces ?? 0 }),
|
||||
blockerCount: runtimeBlocked,
|
||||
nextAction: firstRuntimeAction(data.runtime),
|
||||
href: '/governance?tab=automation-inventory',
|
||||
tone: resolveTone(runtimeBlocked, runtimePercent),
|
||||
Icon: Server,
|
||||
@@ -323,17 +394,19 @@ export default function DeliveryPage({ params }: { params: { locale: string } })
|
||||
status: data.backup?.program_status.current_task_id ?? t('states.noData'),
|
||||
metric: t('lanes.backup.metric', { rows: data.backup?.rollups.total_rows ?? 0 }),
|
||||
blockerCount: backupBlocked,
|
||||
nextAction: firstBackupAction(data.backup),
|
||||
href: '/operations',
|
||||
tone: resolveTone(backupBlocked, backupPercent),
|
||||
Icon: HardDrive,
|
||||
},
|
||||
]
|
||||
}, [data, t])
|
||||
}, [data, t, workbench])
|
||||
|
||||
const loadedCount = Object.values(data).filter(Boolean).length
|
||||
const highRiskBlockers = lanes.reduce((sum, lane) => sum + lane.blockerCount, 0)
|
||||
const averageCompletion = clampPercent(lanes.reduce((sum, lane) => sum + lane.percent, 0) / Math.max(lanes.length, 1))
|
||||
const pageTone: DeliveryTone = highRiskBlockers > 0 ? 'danger' : loadedCount === lanes.length ? 'ok' : 'warn'
|
||||
const sourceTotal = workbench?.summary.source_count ?? SOURCE_COUNT
|
||||
const loadedCount = workbench?.summary.loaded_source_count ?? Object.values(data).filter(Boolean).length
|
||||
const highRiskBlockers = workbench?.summary.high_risk_blocker_count ?? lanes.reduce((sum, lane) => sum + lane.blockerCount, 0)
|
||||
const averageCompletion = workbench?.summary.average_completion_percent ?? clampPercent(lanes.reduce((sum, lane) => sum + lane.percent, 0) / Math.max(lanes.length, 1))
|
||||
const pageTone: DeliveryTone = highRiskBlockers > 0 ? 'danger' : loadedCount === sourceTotal ? 'ok' : 'warn'
|
||||
|
||||
return (
|
||||
<AppLayout locale={params.locale}>
|
||||
@@ -378,7 +451,7 @@ export default function DeliveryPage({ params }: { params: { locale: string } })
|
||||
</section>
|
||||
|
||||
<section className="delivery-metrics">
|
||||
<MetricTile label={t('metrics.loaded')} value={`${loadedCount}/5`} detail={t('metrics.loadedDetail')} tone={loadedCount === 5 ? 'ok' : 'warn'} />
|
||||
<MetricTile label={t('metrics.loaded')} value={`${loadedCount}/${sourceTotal}`} detail={t('metrics.loadedDetail')} tone={loadedCount === sourceTotal ? 'ok' : 'warn'} />
|
||||
<MetricTile label={t('metrics.completion')} value={`${averageCompletion}%`} detail={t('metrics.completionDetail')} tone={averageCompletion >= 80 ? 'ok' : 'warn'} />
|
||||
<MetricTile label={t('metrics.blockers')} value={highRiskBlockers} detail={t('metrics.blockersDetail')} tone={highRiskBlockers > 0 ? 'danger' : 'ok'} />
|
||||
<MetricTile label={t('metrics.execution')} value="0" detail={t('metrics.executionDetail')} tone="ok" />
|
||||
@@ -423,7 +496,7 @@ export default function DeliveryPage({ params }: { params: { locale: string } })
|
||||
<lane.Icon size={17} aria-hidden="true" />
|
||||
<div>
|
||||
<strong>{lane.title}</strong>
|
||||
<span>{lane.metric}</span>
|
||||
<span>{lane.nextAction || lane.metric}</span>
|
||||
</div>
|
||||
<StatusPill tone={lane.tone} label={`${lane.percent}%`} />
|
||||
</div>
|
||||
|
||||
@@ -473,6 +473,16 @@ export const apiClient = {
|
||||
return handleResponse<AwoooIStatusCleanupDashboardSnapshot>(res)
|
||||
},
|
||||
|
||||
async getDeliveryClosureWorkbench() {
|
||||
const res = await fetch(`${API_BASE_URL}/agents/delivery-closure-workbench`)
|
||||
return handleResponse<DeliveryClosureWorkbenchSnapshot>(res)
|
||||
},
|
||||
|
||||
async getGithubTargetPrivateBackupEvidenceGate() {
|
||||
const res = await fetch(`${API_BASE_URL}/agents/github-target-private-backup-evidence-gate`)
|
||||
return handleResponse<GithubTargetPrivateBackupEvidenceGateSnapshot>(res)
|
||||
},
|
||||
|
||||
async getAiAgent12AgentWarRoom() {
|
||||
const res = await fetch(`${API_BASE_URL}/agents/agent-12-agent-war-room`)
|
||||
return handleResponse<AiAgent12AgentWarRoomSnapshot>(res)
|
||||
@@ -1752,6 +1762,64 @@ export interface AwoooIStatusCleanupDashboardSnapshot {
|
||||
ui_implementation_authorized: false
|
||||
}
|
||||
|
||||
export interface DeliveryClosureWorkbenchSnapshot {
|
||||
schema_version: 'delivery_closure_workbench_v1'
|
||||
generated_at: string
|
||||
status: 'blocked_delivery_actions_required' | 'ready' | string
|
||||
summary: {
|
||||
source_count: number
|
||||
loaded_source_count: number
|
||||
average_completion_percent: number
|
||||
high_risk_blocker_count: number
|
||||
runtime_execution_authorized: false
|
||||
remote_write_authorized: false
|
||||
repo_creation_authorized: false
|
||||
refs_sync_authorized: false
|
||||
workflow_trigger_authorized: false
|
||||
secret_values_collected: false
|
||||
}
|
||||
source_statuses: Array<{
|
||||
id: string
|
||||
loaded: boolean
|
||||
schema_version: string
|
||||
generated_at: string
|
||||
}>
|
||||
lanes: Array<{
|
||||
id: 'release' | 'github' | 'gitea' | 'runtime' | 'backup'
|
||||
source_id: string
|
||||
completion_percent: number
|
||||
status: string
|
||||
blocker_count: number
|
||||
metric:
|
||||
| { kind: 'blocked_gate'; blocked: number; total: number }
|
||||
| { kind: 'private_backup_verified'; verified: number; total: number }
|
||||
| { kind: 'workflow_count'; count: number }
|
||||
| { kind: 'surface_count'; total: number }
|
||||
| { kind: 'readiness_row_count'; rows: number }
|
||||
href: string
|
||||
next_action: string
|
||||
tone: 'ok' | 'warn' | 'danger'
|
||||
}>
|
||||
next_focus: Array<{
|
||||
lane_id: string
|
||||
blocker_count: number
|
||||
completion_percent: number
|
||||
next_action: string
|
||||
}>
|
||||
operation_boundaries: {
|
||||
read_only_api_allowed: true
|
||||
runtime_write_allowed: false
|
||||
remote_write_allowed: false
|
||||
repo_creation_allowed: false
|
||||
visibility_change_allowed: false
|
||||
refs_sync_allowed: false
|
||||
workflow_trigger_allowed: false
|
||||
secret_value_collection_allowed: false
|
||||
backup_restore_execution_allowed: false
|
||||
active_scan_allowed: false
|
||||
}
|
||||
}
|
||||
|
||||
export interface AiAgent12AgentWarRoomSnapshot {
|
||||
schema_version: 'ai_agent_12_agent_war_room_v1'
|
||||
generated_at: string
|
||||
@@ -13046,6 +13114,88 @@ export interface GiteaWorkflowRunnerHealthSnapshot {
|
||||
approval_boundaries: Record<string, false>
|
||||
}
|
||||
|
||||
export interface GithubTargetPrivateBackupEvidenceGateSnapshot {
|
||||
schema_version: 'github_target_private_backup_evidence_gate_v1'
|
||||
generated_at: string
|
||||
status:
|
||||
| 'blocked_public_visibility_and_safe_credential_evidence_required'
|
||||
| 'blocked_private_visibility_and_safe_credential_evidence_required'
|
||||
mode: 'read_only_private_backup_evidence_gate'
|
||||
source_reviews: Record<string, string>
|
||||
summary: {
|
||||
target_decision_count: number
|
||||
approval_required_target_count: number
|
||||
approval_package_item_count: number
|
||||
public_probe_visible_target_count: number
|
||||
not_found_or_private_target_count: number
|
||||
private_backup_verified_count: number
|
||||
private_visibility_evidence_missing_count: number
|
||||
safe_credential_required_count: number
|
||||
safe_credential_accepted_evidence_count: number
|
||||
owner_response_received_count: number
|
||||
owner_response_accepted_count: number
|
||||
execution_ready_count: number
|
||||
blocked_target_count: number
|
||||
external_scope_target_count: number
|
||||
forbidden_action_count: number
|
||||
repo_creation_authorized: false
|
||||
visibility_change_authorized: false
|
||||
refs_sync_authorized: false
|
||||
github_primary_switch_authorized: false
|
||||
workflow_modification_authorized: false
|
||||
workflow_trigger_authorized: false
|
||||
secret_value_collection_allowed: false
|
||||
private_clone_url_collection_allowed: false
|
||||
not_found_or_private_as_absent_allowed: false
|
||||
public_repo_allowed: false
|
||||
}
|
||||
targets: Array<{
|
||||
github_repo: string
|
||||
source_key: string
|
||||
approval_required: boolean
|
||||
probe_status: string
|
||||
target_state: string
|
||||
risk: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL' | string
|
||||
visibility_evidence_status:
|
||||
| 'external_scope_not_backup_target'
|
||||
| 'blocked_public_probe_visible_private_evidence_required'
|
||||
| 'blocked_private_or_absent_not_verified'
|
||||
| 'blocked_probe_status_unknown'
|
||||
private_backup_verified: false
|
||||
private_visibility_owner_evidence_ref: string | null
|
||||
safe_credential_evidence_status: string
|
||||
safe_credential_evidence_ref: string | null
|
||||
owner_response_accepted: false
|
||||
refs_sync_ready: false
|
||||
execution_ready: false
|
||||
blockers: string[]
|
||||
evidence_refs: string[]
|
||||
next_action: string
|
||||
forbidden_actions: string[]
|
||||
repo_creation_authorized: false
|
||||
visibility_change_authorized: false
|
||||
refs_sync_authorized: false
|
||||
github_primary_switch_authorized: false
|
||||
secret_values_collected: false
|
||||
}>
|
||||
acceptance_requirements: string[]
|
||||
rejection_rules: string[]
|
||||
operation_boundaries: {
|
||||
read_only_api_allowed: true
|
||||
github_api_write_allowed: false
|
||||
gitea_api_write_allowed: false
|
||||
repo_creation_allowed: false
|
||||
visibility_change_allowed: false
|
||||
refs_sync_allowed: false
|
||||
workflow_modification_allowed: false
|
||||
workflow_trigger_allowed: false
|
||||
github_primary_switch_allowed: false
|
||||
secret_value_collection_allowed: false
|
||||
private_clone_url_collection_allowed: false
|
||||
}
|
||||
authorization_flags: Record<string, false>
|
||||
}
|
||||
|
||||
export interface ObservabilityContractMatrixSnapshot {
|
||||
schema_version: 'observability_contract_matrix_v1'
|
||||
generated_at: string
|
||||
|
||||
Reference in New Issue
Block a user