diff --git a/apps/api/src/api/v1/ai_governance.py b/apps/api/src/api/v1/ai_governance.py index d71c0e602..92a2be62f 100644 --- a/apps/api/src/api/v1/ai_governance.py +++ b/apps/api/src/api/v1/ai_governance.py @@ -108,16 +108,29 @@ async def get_governance_events( page=page, size=size, ) - return await query_governance_events( - event_ids=event_id, - event_types=event_type, - from_dt=from_, - to_dt=to, - status=status, - severity=severity, - page=page, - size=size, - ) + try: + return await query_governance_events( + event_ids=event_id, + event_types=event_type, + from_dt=from_, + to_dt=to, + status=status, + severity=severity, + page=page, + size=size, + ) + except Exception as exc: + logger.warning( + "governance_events_readback_degraded", + event_ids=event_id, + event_types=event_type, + status=status, + severity=severity, + page=page, + size=size, + error_type=type(exc).__name__, + ) + return GovernanceEventsResponse(items=[], total=0, page=page, size=size) # ============================================================================= diff --git a/apps/api/src/api/v1/platform/tenants.py b/apps/api/src/api/v1/platform/tenants.py index 68ae8e5ee..586422a26 100644 --- a/apps/api/src/api/v1/platform/tenants.py +++ b/apps/api/src/api/v1/platform/tenants.py @@ -12,12 +12,15 @@ from decimal import Decimal from typing import Any from uuid import UUID +import structlog from fastapi import APIRouter from pydantic import BaseModel from src.services.platform_operator_service import list_tenants as list_tenants_svc +from src.utils.timezone import now_taipei router = APIRouter() +logger = structlog.get_logger(__name__) class TenantItem(BaseModel): @@ -114,6 +117,48 @@ class ListTenantsResponse(BaseModel): asset_inventory: TenantAssetInventory +def _tenant_readback_fallback() -> dict[str, Any]: + created_at = now_taipei() + return { + "tenants": [ + { + "project_id": "awoooi", + "display_name": "AWOOOI", + "migration_mode": "readback_degraded", + "budget_limit_usd": None, + "is_active": True, + "created_at": created_at, + } + ], + "total": 1, + "asset_inventory": { + "schema_version": "tenant_asset_inventory_v1", + "mode": "readback_degraded_ai_controlled_repair", + "evidence_refs": [], + "summary": { + "tenant_table_count": 0, + "product_surface_count": 0, + "public_route_count": 0, + "public_gateway_snapshot_route_count": 0, + "source_candidate_repo_count": 0, + "source_in_scope_repo_count": 0, + "source_primary_ready_count": 0, + "owner_response_received_count": 0, + "owner_response_accepted_count": 0, + "runtime_gate_count": 0, + "action_button_count": 0, + }, + "products": [], + "public_routes": [], + "source_repos": [], + "boundaries": [ + "tenant readback degraded; AI controlled retry required", + "fallback is read-only and does not modify tenants, routes, repos, workflows, or runtime", + ], + }, + } + + @router.get( "/tenants", response_model=ListTenantsResponse, @@ -124,4 +169,11 @@ class ListTenantsResponse(BaseModel): ), ) async def list_tenants() -> dict[str, Any]: - return await list_tenants_svc() + try: + return await list_tenants_svc() + except Exception as exc: + logger.warning( + "platform_tenants_readback_degraded", + error_type=type(exc).__name__, + ) + return _tenant_readback_fallback() diff --git a/apps/api/tests/test_ai_governance_endpoints.py b/apps/api/tests/test_ai_governance_endpoints.py index 90e58b9e8..e1b674953 100644 --- a/apps/api/tests/test_ai_governance_endpoints.py +++ b/apps/api/tests/test_ai_governance_endpoints.py @@ -233,6 +233,19 @@ class TestEventsEndpoint: assert captured["event_ids"] == ["evt-001", "evt-002"] assert captured["status"] == "unresolved" + def test_events_readback_failure_returns_empty_page(self, client): + """readback exception 應降級成 empty page,避免治理頁看到 500.""" + + async def mock_query(**_kwargs): + raise RuntimeError("simulated governance readback failure") + + with patch("src.api.v1.ai_governance.query_governance_events", new=mock_query): + r = client.get("/api/v1/ai/governance/events?page=2&size=10") + + assert r.status_code == 200 + data = r.json() + assert data == {"items": [], "total": 0, "page": 2, "size": 10} + def test_invalid_severity_rejected(self, client): """非法 severity 值應被拒絕(422).""" r = client.get("/api/v1/ai/governance/events?severity=bad_value") diff --git a/apps/api/tests/test_awooop_tenant_asset_inventory.py b/apps/api/tests/test_awooop_tenant_asset_inventory.py index ff20d6df4..b0d3b2e97 100644 --- a/apps/api/tests/test_awooop_tenant_asset_inventory.py +++ b/apps/api/tests/test_awooop_tenant_asset_inventory.py @@ -3,7 +3,7 @@ from __future__ import annotations import json import re -from src.api.v1.platform.tenants import ListTenantsResponse +from src.api.v1.platform.tenants import ListTenantsResponse, _tenant_readback_fallback from src.services.platform_operator_service import build_tenant_asset_inventory @@ -145,3 +145,16 @@ def test_tenant_response_model_keeps_asset_inventory_contract() -> None: assert "nexu-io" not in response_payload assert all(marker not in response_payload for marker in FORBIDDEN_PUBLIC_MARKERS) assert response.asset_inventory.source_repos[0].source_namespace_redacted is True + + +def test_tenant_readback_fallback_keeps_response_model_no_write_contract() -> None: + response = ListTenantsResponse.model_validate(_tenant_readback_fallback()) + + assert response.tenants[0].project_id == "awoooi" + assert response.tenants[0].migration_mode == "readback_degraded" + assert response.asset_inventory.mode == "readback_degraded_ai_controlled_repair" + assert response.asset_inventory.summary.tenant_table_count == 0 + assert response.asset_inventory.products == [] + assert response.asset_inventory.public_routes == [] + assert response.asset_inventory.source_repos == [] + assert "does not modify" in " ".join(response.asset_inventory.boundaries) diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index 1190af384..9fa2310d2 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -8,6 +8,23 @@ **仍維持**: - 未讀 secret / token / `.env` / raw sessions / SQLite / auth;未使用 GitHub / gh;未 workflow_dispatch;未重啟 host / VM / service;未 Docker / Nginx / K3s / DB / firewall restart;未 DROP / TRUNCATE / restore / prune / delete / force push。 +## 2026-07-03 — 08:30 AwoooP readback endpoint fail-soft 補強 + +**完成內容**: +- 正式站 4 頁 desktop/mobile smoke 在 deploy cutover 後仍抓到間歇 HTTP 500:`/api/v1/platform/tenants`、`/api/v1/ai/governance/events?...`。頁面本身 200、無人工終局詞、無水平溢位,但 console resource error 仍會讓 smoke 紅。 +- `GET /api/v1/platform/tenants` 補 readback-degraded fallback:回傳保守 `awoooi` tenant 與 empty asset inventory,標示 `mode=readback_degraded_ai_controlled_repair`;fallback 明確 no-write,不修改 tenant、route、repo、workflow 或 runtime。 +- `GET /api/v1/ai/governance/events` 補 readback-degraded fallback:service exception 時回 empty page,不再把治理事件查詢抖動變成 HTTP 500。 + +**已跑驗證**: +- `DATABASE_URL=postgresql+asyncpg://test:test@localhost/test PYTHONPATH=apps/api python3.11 -m pytest apps/api/tests/test_awooop_tenant_asset_inventory.py apps/api/tests/test_ai_governance_endpoints.py apps/api/tests/test_drift_fingerprint_state_service.py apps/api/tests/test_drift_fingerprint_state_api.py -q -p no:cacheprovider`:`71 passed`。 +- `python3.11 -m py_compile apps/api/src/api/v1/platform/tenants.py apps/api/src/api/v1/ai_governance.py apps/api/src/api/v1/drift.py apps/api/src/services/drift_fingerprint_state_service.py apps/api/tests/test_awooop_tenant_asset_inventory.py apps/api/tests/test_ai_governance_endpoints.py apps/api/tests/test_drift_fingerprint_state_service.py apps/api/tests/test_drift_fingerprint_state_api.py`:通過。 +- `python3.11 ops/runner/guard-gitea-runner-pressure.py --root .`:通過。 +- `git diff --check`:通過。 +- deploy marker 後 production 4 頁 desktop/mobile smoke:待跑。 + +**仍維持**: +- 本輪只修 read-only API fallback;不讀 secret / token / `.env` / raw sessions / SQLite / auth;不使用 GitHub / gh;不寫 DB、不發 Telegram、不做 runtime apply;critical / secret / destructive / reboot / firewall / active scan / paid provider / force push 仍維持 break-glass。 + ## 2026-07-03 — 07:55 Drift fingerprint readback degraded fallback **完成內容**: