New router app/api/doc_center.py: GET /api/cma/docs (list, ?with_content) and GET /api/cma/docs/{slug}; runtime disk read, role-gated (ceo/finance/business/it), no existing API semantics changed.
Content dir /srv/hermes/shared/cma-content (docs/*.md + categories.json). Placed outside the repo on purpose: the deploy tree equals the committed tree (deploy.sh checkout -f + clean -fdx), so in-repo content would be reset on every deploy and content edits would fail the deploy-tree dirty check.
CMAKnowledge.vue: 20 hardcoded items migrated to md, page now fetches at runtime and renders md with the existing marked dep (no new deps); table borders restored via :deep() since scoped styles never reach v-html content.
Tests: backend/tests/test_doc_center_20260924.py (20 cases). Full suite 1448 passed (baseline 1428 + 20).
backend: GET /api/cma/data-quality/rule-detail (COUNT(*) total, structured items, server-side paging, scope=manual); governance seven-rule counts use real totals instead of len(LIMIT-ed rows). frontend: clickable anomaly count -> MyDialog detail (no new el-dialog) + full CSV export.
The component shipped a hardcoded initial period (2026-06) that is present in the periods list, so the previous guard kept it and the page never defaulted to the newest period. Initialize empty so loadPeriods() applies periods[0] (backend returns DESC); /diagnosis treats an empty period as 'newest with data'.
Backend GET /growth-quality/periods returns {entity_id,periods[],coverage{}}; axios interceptor already unwraps response.data so the old r.data||r||[] put the whole object into el-select and Vue rendered JSON.
Also default to periods[0]: backend returns periods in DESC order, so old code picked the OLDEST period. Only frontend/src/views/GrowthQuality.vue changed.
my_dashboard picked the row with max calculated_at, so batch backfills made stale periods win (revenue/net-profit showed 2026-07, rebate rate 2026-04). Now prefer the latest monthly period, fall back to calculated_at. Frontend adds data-as-of label and stale tag; not deployed yet (push gated by another pending commit).
- MapNode.vue: hover trash icon per KR row emitting delete-kr (inline SVG, no emoji)
- StrategyLayer.vue / MapCanvas.vue: passthrough + MyDialog second confirm + okrApi.deleteKr + whole-map save
- MapsDialog: single-KR policy hint (keep at least 1 KR per objective)
B1: /api/cma/dashboard/predict hand-rolled threshold parsing called float() on the whole
expression -> ValueError on multi-clause thresholds like '<=36|>=124' -> 500. Now routes
through app.utils.threshold_parser.eval_threshold (single source of truth; red then yellow,
invalid/blank expr -> False, no crash).
B2: ReportCenter.vue restatement tab template called restateDiffClass while the script
defines restatementDiffClass (name mismatch since ec6af75) -> TypeError on render.
Verified: predict 200 on new code vs 500 on live code; /reports A/B browser run shows 0 vs 3
occurrences of the TypeError; pytest 1113 passed / 1 xfailed; vite build OK.
api/index.ts: add permissionApi.getModules/getConfig/saveConfig + alignmentApi.config/tree.
views: replace 13 raw fetch calls and 6 local axios.create instances (each hand-building the Authorization header, without the shared 401 redirect / timeout toast) with the shared instance from api/index.ts.
KPIAlignment: fixes a live bug - the header value was the string literal 'Bearer ' + token, so /alignment/tree always returned 401 (reproduced against the production bundle).
Unwrap semantics preserved: the shared response interceptor returns response.data, so call sites that used res.data were adjusted accordingly.
Verified: scan-frontend-tokenless RAW-TOKENLESS=0; vite build ok; headless A/B (new dist vs production bundle, 8 affected pages, real login) -> identical rendered rows and identical pre-existing error profile, /alignment 401->200.
Push not executed: main is ahead 4 with other tickets' gated commits (see cma-main-ahead3-push-gate-notice-20260915).
Root cause: main.py include_router had no global auth dependency; user routers
(products/cost/predict/...) relied on assert_entity_scope, which treats a
missing session token as the Bot service channel and lets the request through.
Result: 46 user endpoints returned data without any token on a public site.
Changes:
- auth_middleware.py: add require_auth_401 (missing Bearer -> 401, delegates to
require_auth otherwise); 401/403 semantics are explicit.
- router-level Depends(require_auth_401) on products, cost, predict,
notifications, permissions, tenant, alignment; endpoint-level on
/auth/roles and /admin/cache/clear.
- deps.py: document that get_entity_id/assert_entity_scope are NOT auth.
- frontend: add the missing Bearer header to 5 tokenless fetch/axios calls that
hit the newly protected endpoints (KPIAlignment x2, RolePermissions x2,
Dashboard x1).
- tests: new test_api_auth_surface_20260914.py (58 cases: no-token 401,
token 200, cross-entity 403, bot channel unchanged); adapt test_predict /
test_kpi_forecast to send session tokens like production.
- scripts/audit_api_auth_surface.py: reusable FastAPI dependency-graph audit
(423 endpoints; A bot / B gap / C public), exits 1 when a B-class gap exists.
- data_quality.py: remove GET /check-governance (no caller repo-wide; its
entity_id param had no session auth -> cross-tenant read; duplicate of
governance-check) -> the path now returns 404
- reports.py: /reports/dupont resolves the analysis target from the session
account (assert_entity_slug_scope / resolve_session_slug); explicit
cross-entity entity param -> 403, illegal value -> 400; the 2026-08-31
cross-entity exemption is revoked (docstring updated)
- frontend: drop the checkGovernance api definition; DupontAnalysis.vue
entity radio group -> read-only session entity tag, request without param
- tests: TestCheckGovernanceRetired (404 x2), TestDupontSessionScope (3),
TestDupont reworked to account-bound tokens (8 tests)
- docs/api-verification-split.md: drop the retired row, note 334/404 -> 333/403
pytest: 10 failed / 957 passed / 1 xfailed vs HEAD baseline 10 failed /
951 passed / 1 xfailed (identical failure set -> zero regression)
(cma-residual-cross-entity-cleanup-20260914)
- DataQuality / ProductMatrix / AlertList / ActionPlanLibrary /
views.GrowthQuality / PredictAccuracy: drop entity el-select
(including the "all entities" option), show read-only session
entity tag (not clickable), same style as the top bar tag
- drop page-level entity param from those requests; backend resolves
entity from the session token (assert_entity_scope / get_entity_id)
- utils/entity.ts: add sessionEntityName() (cma_entity_name else id map)
- P3: ReportCenter header emoji removed; components/GrowthQuality
detail drill-down el-dialog -> MyDialog
(cma-entity-selectors-converge-6pages-20260914)
- MainLayout/ReportCenter: drop switch-entity switcher, keep read-only entity tag
- KPIList/PredictDashboard/GrowthQuality: entity selectors locked to session entity
- api/index.ts: drop switchEntity/myEntities wrappers (endpoint kept for compat)
- sidebar brand now shows logged-in entity name
(cma-remove-inapp-entity-switcher-20260914)