feat(modernization): full architecture blueprint with hardening follow-up

Implement phased modernization infrastructure for transitioning from
multi-page legacy routing to SPA portal-shell architecture, plus
post-delivery hardening fixes for policy loading, fallback consistency,
and governance drift detection.

Key changes:
- Add route contract enrichment with scope/visibility/compatibility policies
- Canonical 302 redirects from legacy direct-entry to /portal-shell/ routes
- Asset readiness enforcement and runtime fallback retirement for in-scope routes
- Shared feature-flag helpers (env > config > default) replacing duplicated _to_bool
- Defensive copy for lru_cached policy payloads preventing mutation corruption
- Unified retired-fallback response helper across app and blueprint routes
- Frontend/backend route-contract cross-validation in governance gates
- Shell CSS token fallback values for routes rendered outside shell scope
- Local-safe .env.example defaults with production recommendation comments
- Legacy contract fallback warning logging and single-hop redirect optimization

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
egg
2026-02-12 11:26:02 +08:00
parent 2c8d80afe6
commit 7cb0985b12
113 changed files with 4577 additions and 582 deletions

View File

@@ -181,9 +181,41 @@ WORKER_RESTART_COOLDOWN=60
# Watchdog loop check interval in seconds
WATCHDOG_CHECK_INTERVAL=5
# ============================================================
# Portal Shell / Full Modernization Flags
# ============================================================
# Frontend build mode: auto | always | never
# - auto: build only when dist is missing/stale
# - always: force build on every start/restart
# - never: skip build entirely
FRONTEND_BUILD_MODE=always
# Fail startup when frontend build fails (recommended in production)
FRONTEND_BUILD_FAIL_ON_ERROR=true
# Shell-first SPA navigation
# Local default: false (avoid coupling first boot to shell-first cutover behavior)
# Production recommendation: set true after shell cutover verification is complete
PORTAL_SPA_ENABLED=false
# Fail-fast readiness gate for in-scope dist assets
# Local default: false (allow startup when dist artifacts are intentionally absent)
# Production recommendation: set true to enforce release readiness at boot
MODERNIZATION_ENFORCE_ASSET_READINESS=false
# Retire in-scope runtime fallback behavior
# Local default: false (retain compatibility fallback while developing)
# Production recommendation: set true only after all in-scope dist assets are verified
MODERNIZATION_RETIRE_IN_SCOPE_RUNTIME_FALLBACK=false
# Runtime contract strict validation toggle
# Local default: false (avoid strict conda/runtime checks on onboarding machines)
# Production recommendation: set true to fail fast on contract drift
RUNTIME_CONTRACT_ENFORCE=false
# Health endpoint memo cache TTL in seconds
HEALTH_MEMO_TTL_SECONDS=5
# ============================================================
# Runtime Resilience Diagnostics Thresholds
# ============================================================
@@ -205,6 +237,3 @@ RESILIENCE_RESTART_CHURN_THRESHOLD=3
# Example: https://example.com,https://app.example.com
# Set to * for development (not recommended for production)
CORS_ALLOWED_ORIGINS=
# Health endpoint memo cache TTL in seconds
HEALTH_MEMO_TTL_SECONDS=5

View File

@@ -0,0 +1,57 @@
name: full-modernization-gates
on:
pull_request:
paths:
- "frontend/src/portal-shell/**"
- "frontend/src/qc-gate/**"
- "frontend/tests/portal-shell-*.test.js"
- "src/mes_dashboard/**"
- "tests/test_portal_shell_routes.py"
- "tests/test_full_modernization_gates.py"
- "tests/test_asset_readiness_policy.py"
- "scripts/check_full_modernization_gates.py"
- "docs/migration/full-modernization-architecture-blueprint/**"
- "openspec/changes/full-modernization-architecture-blueprint/**"
- ".github/workflows/full-modernization-gates.yml"
push:
branches: [ main ]
paths:
- "frontend/src/portal-shell/**"
- "frontend/src/qc-gate/**"
- "frontend/tests/portal-shell-*.test.js"
- "src/mes_dashboard/**"
- "tests/test_portal_shell_routes.py"
- "tests/test_full_modernization_gates.py"
- "tests/test_asset_readiness_policy.py"
- "scripts/check_full_modernization_gates.py"
- "docs/migration/full-modernization-architecture-blueprint/**"
- "openspec/changes/full-modernization-architecture-blueprint/**"
- ".github/workflows/full-modernization-gates.yml"
jobs:
frontend-route-governance:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
- name: Run shell governance tests
run: node --test frontend/tests/portal-shell-navigation.test.js frontend/tests/portal-shell-route-contract-governance.test.js
backend-modernization-gates:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.13"
- name: Install test dependencies
run: |
python -m pip install --upgrade pip
python -m pip install -e . pytest
- name: Run modernization governance gate script
run: python scripts/check_full_modernization_gates.py --mode block
- name: Run backend canonical/fallback gate tests
run: python -m pytest tests/test_portal_shell_routes.py tests/test_full_modernization_gates.py tests/test_asset_readiness_policy.py -q

View File

@@ -5,6 +5,7 @@
> 專案主執行根目錄:`DashBoard_vite/`
> 目前已移除舊版 `DashBoard/` 代碼,僅保留新架構。
> 2026-02-11`portal-shell-route-view-integration` 已完成並封存Portal Shell 全面採用 no-iframe 的 SPA route-view 架構。
> 2026-02-12`full-modernization-architecture-blueprint` 已完成並封存Phase 1目前僅剩 `deferred-route-modernization-follow-up` 進行中(`/tables`、`/excel-query`、`/query-tool`、`/mid-section-defect`)。
---
@@ -46,6 +47,8 @@
| QC-GATE 即時狀態報表Vue 3 + Vite | ✅ 已完成 |
| 數據表查詢頁面 Vue 3 遷移 | ✅ 已完成 |
| WIP 三頁 Vue 3 遷移Overview/Detail/Hold Detail | ✅ 已完成 |
| 全站現代化治理 Phase 1路由治理/品質門檻/資產就緒/手動驗收) | ✅ 已完成(已封存) |
| Deferred routes 現代化(`/tables``/excel-query``/query-tool``/mid-section-defect` | ⏳ 進行中follow-up 提案) |
| WIP Overview/Detail 篩選條件保留 | ✅ 已完成 |
| 設備雙頁 Vue 3 遷移Status/History | ✅ 已完成 |
| 設備快取 DataFrame TTL 一致性修復 | ✅ 已完成 |
@@ -55,6 +58,7 @@
## 開發歷史Vite 重構後)
- 2026-02-12完成並封存 `full-modernization-architecture-blueprint`Phase 1— in-scope 報表路由全面採 canonical shell entry`/portal-shell/...`)、補齊 admin 路由治理(`/admin/pages``/admin/performance`)、建立資產就緒 fail-fast 政策與前端品質門檻治理;並建立 deferred follow-up 提案承接 `/tables``/excel-query``/query-tool``/mid-section-defect`
- 2026-02-11完成 Portal Shell route-view 全遷移(`portal-shell-route-view-integration`)— 全站移除 iframe 內容嵌入、導入 Vue Router 動態註冊與 fallback guard、補齊 Wave A/Wave B page parity 測試、健康檢查 summary/detail UX 與 `/health/frontend-shell` 契約,並完成 pre/post parity 與 smoke 證據彙整。
- 2026-02-11完成 table query API `table_name` 白名單驗證(`/api/query_table``/api/get_table_columns`)— 拒絕未註冊資料表,補上整合測試,避免 SQL injection 入口。
- 2026-02-11完成設備雙頁級聯篩選`/resource``/resource-history`)— 新增 Group/Family/Machine 多層篩選聯動,前後端支援 `resource_ids` 條件,矩陣與明細篩選一致。
@@ -116,6 +120,7 @@
- 頁面架構與抽屜分類:`docs/page_architecture_map.md`
- 前端計算前移與 parity 規則:`docs/frontend_compute_shift_plan.md`
- Portal Shell route-view 遷移基線與驗收:`docs/migration/portal-shell-route-view-integration/`
- 全站現代化架構 Phase 1 治理文件:`docs/migration/full-modernization-architecture-blueprint/`
- Hold 歷史頁資料口徑說明:`docs/hold_history.md`
- Cutover gates / rollout / rollback`docs/migration_gates_and_runbook.md`
- 環境依賴缺口與對策:`docs/environment_gaps_and_mitigation.md`
@@ -163,6 +168,11 @@
- LDAP API URL 啟動驗證:僅允許 `https` + host allowlist。
- 全域 security headersCSP/X-Frame-Options/X-Content-Type-Options/Referrer-Policyproduction 含 HSTS
8. 現階段仍保留部分相容路徑(非 bug屬於分期策略
- in-scope 報表路由已改為 canonical shell redirect直接進入 `/portal-shell/...`)。
- `portal-shell` 為目前主路由容器,尚不可移除。
- deferred scope`/tables``/excel-query``/query-tool``/mid-section-defect`)與其相容模板/入口會保留到 follow-up 提案完成。
---
## 快速開始

View File

@@ -0,0 +1,21 @@
{
"change": "full-modernization-architecture-blueprint",
"route_flags": {
"/wip-overview": {"content_cutover_enabled": true},
"/wip-detail": {"content_cutover_enabled": true},
"/hold-overview": {"content_cutover_enabled": true},
"/hold-detail": {"content_cutover_enabled": true},
"/hold-history": {"content_cutover_enabled": true},
"/resource": {"content_cutover_enabled": true},
"/resource-history": {"content_cutover_enabled": true},
"/qc-gate": {"content_cutover_enabled": true},
"/job-query": {"content_cutover_enabled": true},
"/tmtt-defect": {"content_cutover_enabled": true},
"/admin/pages": {"content_cutover_enabled": true},
"/admin/performance": {"content_cutover_enabled": true},
"/tables": {"content_cutover_enabled": false},
"/excel-query": {"content_cutover_enabled": false},
"/query-tool": {"content_cutover_enabled": false},
"/mid-section-defect": {"content_cutover_enabled": false}
}
}

View File

@@ -88,7 +88,7 @@
{
"route": "/tmtt-defect",
"name": "TMTT印字腳型不良分析",
"status": "released",
"status": "dev",
"drawer_id": "dev-tools",
"order": 5
},

View File

@@ -0,0 +1,27 @@
# Full Modernization Architecture Blueprint Artifacts
This directory stores execution artifacts for `full-modernization-architecture-blueprint`.
## Core Governance
- `route_scope_matrix.json`: frozen in-scope/deferred route contract matrix.
- `governance_milestones.md`: completion and deprecation milestones.
- `exception_registry.json`: approved temporary exceptions with owner and milestone.
- Policy artifact runtime cache model:
- `src/mes_dashboard/core/modernization_policy.py` caches `route_scope_matrix.json` and
`asset_readiness_manifest.json` in-process with `lru_cache`.
- Runtime behavior is restart-refresh by default: JSON edits take effect after worker restart.
- Controlled refresh is available through `clear_modernization_policy_cache()` for tests or
explicit maintenance hooks; no automatic file watcher/hot reload is active in production.
## Content Modernization Safety
- `page_content_manual_acceptance_checklist.md`: mandatory manual sign-off checklist.
- `known_bug_baseline.json`: route-level known bug baseline and replay blocking policy.
## Rollout Operations
- `rollout_runbook.md`: phase steps and hold points.
- `rollback_controls.md`: rollback and false-positive gate handling.
- `observability_checkpoints.md`: route/gate/rollback observability contract.
- `deferred_route_handoff.md`: explicit handoff package to deferred-route follow-up change.

View File

@@ -0,0 +1,22 @@
{
"change": "full-modernization-architecture-blueprint",
"in_scope_required_assets": {
"/portal-shell": ["portal-shell.html", "portal-shell.js", "portal-shell.css", "tailwind.css"],
"/wip-overview": ["wip-overview.html", "wip-overview.js"],
"/wip-detail": ["wip-detail.html", "wip-detail.js"],
"/hold-overview": ["hold-overview.html", "hold-overview.js"],
"/hold-detail": ["hold-detail.html", "hold-detail.js"],
"/hold-history": ["hold-history.html", "hold-history.js"],
"/resource": ["resource-status.html", "resource-status.js"],
"/resource-history": ["resource-history.html", "resource-history.js"],
"/qc-gate": ["qc-gate.html", "qc-gate.js"],
"/job-query": ["job-query.js"],
"/tmtt-defect": ["tmtt-defect.js"]
},
"deferred_routes": [
"/tables",
"/excel-query",
"/query-tool",
"/mid-section-defect"
]
}

View File

@@ -0,0 +1,12 @@
{
"change": "full-modernization-architecture-blueprint",
"records": [],
"schema": {
"route": "string",
"known_bug_id": "string",
"replay_date": "YYYY-MM-DD",
"result": "pass|fail",
"notes": "string",
"signoff_owner": "string"
}
}

View File

@@ -0,0 +1,24 @@
{
"change": "full-modernization-architecture-blueprint",
"canonical_policy": {
"applies_when": "PORTAL_SPA_ENABLED=true",
"report_routes": "redirect direct route entry to /portal-shell/<route>",
"admin_routes": "shell target redirects to backend /admin/* while backend keeps auth authority"
},
"direct_entry_compatibility": {
"query_semantics_must_be_preserved": true,
"redirect_status_code": 302
},
"in_scope_report_routes": [
"/wip-overview",
"/wip-detail",
"/hold-overview",
"/hold-detail",
"/hold-history",
"/resource",
"/resource-history",
"/qc-gate",
"/job-query",
"/tmtt-defect"
]
}

View File

@@ -0,0 +1,40 @@
# Deferred Route Handoff (Phase 1 -> Follow-up)
## Source Change
- `openspec/changes/full-modernization-architecture-blueprint/`
## Deferred Routes (Not in Phase 1 Blocking Scope)
- `/tables`
- `/excel-query`
- `/query-tool`
- `/mid-section-defect`
## Follow-up Change
- `openspec/changes/deferred-route-modernization-follow-up/`
## Handoff Content
1. Scope boundary contract:
- Source: `docs/migration/full-modernization-architecture-blueprint/route_scope_matrix.json`
2. Required acceptance model to carry forward:
- Parity fixtures/checks:
- `docs/migration/full-modernization-architecture-blueprint/parity_golden_fixtures.json`
- `docs/migration/full-modernization-architecture-blueprint/interaction_parity_checks.json`
- Manual acceptance + bug replay:
- `docs/migration/full-modernization-architecture-blueprint/page_content_manual_acceptance_checklist.md`
- `docs/migration/full-modernization-architecture-blueprint/known_bug_baseline.json`
- `docs/migration/full-modernization-architecture-blueprint/bug_revalidation_records.json`
3. Governance policy to carry forward:
- `docs/migration/full-modernization-architecture-blueprint/quality_gate_policy.json`
- `docs/migration/full-modernization-architecture-blueprint/governance_milestones.md`
- `docs/migration/full-modernization-architecture-blueprint/asset_readiness_manifest.json`
## Transfer Rule
- Deferred routes remain excluded from phase-1 blocking criteria.
- Follow-up change MUST promote these routes to in-scope and apply equivalent parity/manual-acceptance/bug-revalidation gates before legacy retirement.

View File

@@ -0,0 +1,42 @@
{
"version": 1,
"change": "full-modernization-architecture-blueprint",
"fields": [
"id",
"type",
"scope",
"owner",
"introduced_by",
"reason",
"mitigation",
"status",
"milestone",
"tracking_issue"
],
"entries": [
{
"id": "style-admin-pages-inline-css",
"type": "style",
"scope": "/admin/pages",
"owner": "frontend-platform-admin",
"introduced_by": "legacy-template",
"reason": "admin pages remain backend-rendered in this phase",
"mitigation": "enforce shell contract governance while retaining backend auth authority",
"status": "approved-temporary",
"milestone": "2026-03-19",
"tracking_issue": "deferred-route-modernization-follow-up/admin-template-modernization"
},
{
"id": "style-admin-performance-inline-css",
"type": "style",
"scope": "/admin/performance",
"owner": "frontend-platform-admin",
"introduced_by": "legacy-template",
"reason": "admin performance remains backend-rendered in this phase",
"mitigation": "governed navigation + route-level fallback controls",
"status": "approved-temporary",
"milestone": "2026-03-19",
"tracking_issue": "deferred-route-modernization-follow-up/admin-template-modernization"
}
]
}

View File

@@ -0,0 +1,30 @@
# Full Modernization Governance Milestones
## Phase Completion Criteria
A phase is complete only when all criteria below are true:
1. Route governance: 100% of in-scope routes in `route_scope_matrix.json` have valid shell contract metadata and ownership.
2. Style governance: in-scope route-local styles do not introduce page-global selectors (`:root`, `body`) unless recorded in exception registry.
3. Quality governance: functional parity, visual checkpoints, accessibility checks, and performance budgets pass at configured gate severity.
4. Content safety governance: page-content parity evidence + manual acceptance sign-off exist for each migrated in-scope route.
5. Bug carry-over governance: known-bug replay checks for migrated scope do not reproduce legacy defects.
## Legacy Deprecation Milestones
- 2026-02-20: route contract CI completeness gate enabled in `warn` mode.
- 2026-02-27: route contract CI completeness gate promoted to `block` mode.
- 2026-03-05: in-scope asset readiness gate promoted to `block` mode.
- 2026-03-12: runtime fallback posture retired for in-scope routes in production policy.
- 2026-03-19: unresolved style exceptions past milestone fail modernization review.
## Deferred Route Linkage
Deferred routes are not pass/fail criteria in this phase and are handed over to a follow-up change:
- `/tables`
- `/excel-query`
- `/query-tool`
- `/mid-section-defect`
Follow-up change handoff is recorded in `openspec/changes/deferred-route-modernization-follow-up/`.

View File

@@ -0,0 +1,17 @@
{
"change": "full-modernization-architecture-blueprint",
"required_flows": {
"/wip-overview": ["filter_apply", "filter_reset", "status_card_drill"],
"/wip-detail": ["list_detail_continuity", "pagination", "filter_reset"],
"/hold-overview": ["reason_toggle", "matrix_selection", "lot_scope_sync"],
"/hold-detail": ["reason_required_redirect", "distribution_toggle", "lot_scope_sync"],
"/hold-history": ["date_range_query", "pareto_selection", "duration_bucket_selection"],
"/resource": ["status_filter", "workcenter_family_filter", "tooltip_open_close"],
"/resource-history": ["query_submit", "export_csv", "chart_detail_sync"],
"/qc-gate": ["chart_bucket_selection", "table_filter_sync", "filter_clear"],
"/job-query": ["resource_select", "query_submit", "export_csv"],
"/tmtt-defect": ["date_range_query", "chart_filter_link", "detail_sort"],
"/admin/pages": ["drawer_crud", "page_status_update", "admin_visibility"],
"/admin/performance": ["admin_auth_access", "performance_view_load"]
}
}

View File

@@ -0,0 +1,58 @@
{
"change": "full-modernization-architecture-blueprint",
"scope": "in-scope-routes-only",
"routes": {
"/wip-overview": {
"baseline_status": "initialized",
"known_bugs": []
},
"/wip-detail": {
"baseline_status": "initialized",
"known_bugs": []
},
"/hold-overview": {
"baseline_status": "initialized",
"known_bugs": []
},
"/hold-detail": {
"baseline_status": "initialized",
"known_bugs": []
},
"/hold-history": {
"baseline_status": "initialized",
"known_bugs": []
},
"/resource": {
"baseline_status": "initialized",
"known_bugs": []
},
"/resource-history": {
"baseline_status": "initialized",
"known_bugs": []
},
"/qc-gate": {
"baseline_status": "initialized",
"known_bugs": []
},
"/job-query": {
"baseline_status": "initialized",
"known_bugs": []
},
"/tmtt-defect": {
"baseline_status": "initialized",
"known_bugs": []
},
"/admin/pages": {
"baseline_status": "initialized",
"known_bugs": []
},
"/admin/performance": {
"baseline_status": "initialized",
"known_bugs": []
}
},
"revalidation_rule": {
"reproduced_legacy_bug_blocks_signoff": true,
"reproduced_legacy_bug_blocks_legacy_retirement": true
}
}

View File

@@ -0,0 +1,5 @@
{
"change": "full-modernization-architecture-blueprint",
"records": [],
"rule": "next route cutover is blocked until current route has approved manual sign-off"
}

View File

@@ -0,0 +1,32 @@
# Modernization Observability Checkpoints
## Route Governance Signals
1. `navigation_contract_mismatch_total`
- Source: `/api/portal/navigation` diagnostics.
- Alert condition: non-zero for in-scope routes.
2. `route_contract_missing_metadata_total`
- Source: route governance CI script.
- Alert condition: >0 in block mode.
## Quality Gate Signals
1. `quality_gate_failed_total{gate_id}`
- Source: quality gate report.
- Alert condition: any mandatory gate failed.
2. `manual_acceptance_pending_routes`
- Source: manual acceptance records.
- Alert condition: cutover attempted with pending sign-off.
## Fallback and Rollback Signals
1. `in_scope_runtime_fallback_served_total`
- Should remain zero after fallback retirement milestone.
2. `content_cutover_flag_rollbacks_total`
- Track frequency and route impact.
3. `legacy_bug_replay_failures_total`
- Any non-zero indicates carry-over risk and blocks sign-off.

View File

@@ -0,0 +1,28 @@
# Page Content Manual Acceptance Checklist
## Rule
- Route cutover proceeds one route at a time.
- Next route cutover is blocked until current route is manually signed off.
- Legacy code retirement is blocked until parity checks and manual sign-off are both complete.
## Mandatory Checks Per Route
1. Filter semantics match baseline (apply, reset, URL/query continuity).
2. Chart interactions match baseline (drill/selection/clear behavior).
3. Empty/loading/error/success states are correct and non-overlapping.
4. Table/chart linked interactions remain deterministic.
5. Accessibility: keyboard flow, focus visibility, `aria-*` semantics, reduced-motion behavior.
6. Known-bug replay checks completed (see `known_bug_baseline.json`).
7. No reproduced legacy bug in migrated scope.
## Sign-Off Template
- Route:
- Owner:
- Reviewer:
- Date:
- Parity evidence links:
- Known-bug replay result:
- Blocking defects:
- Decision: `approved` | `rework-required`

View File

@@ -0,0 +1,17 @@
{
"change": "full-modernization-architecture-blueprint",
"fixtures": {
"/wip-overview": ["tests/fixtures/frontend_compute_parity.json"],
"/wip-detail": ["docs/migration/portal-shell-route-view-integration/wave-b-parity-evidence.json"],
"/hold-overview": ["docs/migration/portal-shell-route-view-integration/baseline_interaction_evidence.json"],
"/hold-detail": ["docs/migration/portal-shell-route-view-integration/baseline_interaction_evidence.json"],
"/hold-history": ["docs/migration/portal-shell-route-view-integration/baseline_interaction_evidence.json"],
"/resource": ["docs/migration/portal-shell-route-view-integration/baseline_api_payload_contracts.json"],
"/resource-history": ["docs/migration/portal-shell-route-view-integration/baseline_api_payload_contracts.json"],
"/qc-gate": ["docs/migration/portal-shell-route-view-integration/visual-regression-snapshots.json"],
"/job-query": ["docs/migration/portal-shell-route-view-integration/wave-b-parity-evidence.json"],
"/tmtt-defect": ["docs/migration/portal-shell-route-view-integration/wave-b-parity-evidence.json"],
"/admin/pages": ["tests/test_portal_shell_routes.py"],
"/admin/performance": ["tests/test_performance_integration.py"]
}
}

View File

@@ -0,0 +1,13 @@
{
"change": "full-modernization-architecture-blueprint",
"targets": {
"shell_navigation_api_avg_ms": 150,
"shell_navigation_api_p95_ms": 350,
"portal_shell_entry_avg_ms": 200,
"portal_shell_entry_p95_ms": 450
},
"source_baselines": [
"docs/migration/portal-no-iframe/performance_baseline_spa.json",
"docs/migration/portal-no-iframe/performance_baseline_legacy.json"
]
}

View File

@@ -0,0 +1,21 @@
{
"change": "full-modernization-architecture-blueprint",
"severity_mode": {
"current": "warn",
"promotion_target": "block",
"promotion_milestone": "2026-02-27"
},
"gates": [
{"id": "Q1", "name": "functional-parity", "required": true},
{"id": "Q2", "name": "visual-regression", "required": true},
{"id": "Q3", "name": "accessibility-keyboard-aria-motion", "required": true},
{"id": "Q4", "name": "performance-budget", "required": true},
{"id": "Q5", "name": "manual-acceptance-and-bug-revalidation", "required": true}
],
"deferred_routes_excluded": [
"/tables",
"/excel-query",
"/query-tool",
"/mid-section-defect"
]
}

View File

@@ -0,0 +1,7 @@
{
"mode": "block",
"errors": [],
"warnings": [],
"info": [],
"passed": true
}

View File

@@ -0,0 +1,26 @@
# Full Modernization Rollback Controls
## Route-Level Reversion Controls
- `PORTAL_SPA_ENABLED=false`:
Disable shell-first navigation runtime globally.
- Route-scoped contract fallback:
Mark route contract with fallback strategy and redeploy shell assets.
- Content cutover feature flag:
Disable route-level modernized content path while keeping shell runtime up.
## False-Positive Gate Handling
1. Capture failing gate output and route impact.
2. Confirm whether failure is test flake or product defect.
3. If false-positive and production risk is high:
- Temporarily switch gate severity from `block` to `warn`.
- Record waiver with owner, reason, expiry.
4. Restore `block` mode after corrective action.
## Required Rollback Evidence
- Incident timestamp and impacted routes.
- Gate IDs that triggered rollback.
- Route contract state before/after rollback.
- Manual acceptance and known-bug replay references.

View File

@@ -0,0 +1,33 @@
# Full Modernization Rollout Runbook
## Phase Sequence
1. Governance freeze
- Confirm `route_scope_matrix.json` has no pending route-scope changes.
- Confirm exception registry entries include owner + milestone.
2. Route governance enforcement
- Run route contract completeness checks in warn mode.
- Fix all in-scope metadata gaps.
- Promote route governance checks to block mode.
3. Style/content hardening
- Apply style isolation checks for in-scope routes.
- Execute parity checks and manual acceptance route-by-route.
- Run known-bug replay checks per route.
4. Asset/gate enforcement
- Validate in-scope asset readiness.
- Run quality gate suite (functional, visual, accessibility, performance).
- Promote gate severity from warn to block according to policy.
## Hold Points
- Hold-1: Any in-scope route missing contract metadata.
- Hold-2: Any unresolved style exception past milestone.
- Hold-3: Any parity failure or known-bug replay failure.
- Hold-4: Any mandatory quality gate failure in block mode.
## Promotion Rule
Promotion is allowed only when all hold points are clear and no deferred-route checks were incorrectly included as blockers.

View File

@@ -0,0 +1,77 @@
{
"change": "full-modernization-architecture-blueprint",
"in_scope_routes": {
"/wip-overview": {
"filter_input_semantics": ["workorder", "lotid", "package", "type", "status"],
"query_payload_contract": ["workorder", "lotid", "package", "type", "status"],
"chart_data_shape": ["status_cards", "matrix", "pareto"],
"state_contract": ["loading", "empty", "error", "success"]
},
"/wip-detail": {
"filter_input_semantics": ["workcenter", "workorder", "lotid", "package", "type", "status", "page"],
"query_payload_contract": ["workcenter", "workorder", "lotid", "package", "type", "status", "page"],
"chart_data_shape": ["summary_cards", "lot_table"],
"state_contract": ["loading", "empty", "error", "success"]
},
"/hold-overview": {
"filter_input_semantics": ["hold_type", "reason", "workcenter", "package", "age_range", "page"],
"query_payload_contract": ["hold_type", "reason", "workcenter", "package", "age_range", "page"],
"chart_data_shape": ["summary_cards", "matrix", "treemap", "lot_table"],
"state_contract": ["loading", "empty", "error", "success"]
},
"/hold-detail": {
"filter_input_semantics": ["reason", "workcenter", "package", "age_range", "page"],
"query_payload_contract": ["reason", "workcenter", "package", "age_range", "page"],
"chart_data_shape": ["summary_cards", "distribution", "lot_table"],
"state_contract": ["loading", "empty", "error", "success"]
},
"/hold-history": {
"filter_input_semantics": ["start_date", "end_date", "hold_type", "record_type", "reason", "duration_range", "page"],
"query_payload_contract": ["start_date", "end_date", "hold_type", "record_type", "reason", "duration_range", "page"],
"chart_data_shape": ["trend", "reason_pareto", "duration", "detail_table"],
"state_contract": ["loading", "empty", "error", "success"]
},
"/resource": {
"filter_input_semantics": ["workcenter_groups", "families", "resource_ids", "statuses"],
"query_payload_contract": ["workcenter_groups", "families", "resource_ids", "statuses"],
"chart_data_shape": ["summary_cards", "status_matrix", "equipment_grid"],
"state_contract": ["loading", "empty", "error", "success"]
},
"/resource-history": {
"filter_input_semantics": ["start_date", "end_date", "granularity", "workcenter_groups", "families", "resource_ids", "is_production", "is_key", "is_monitor"],
"query_payload_contract": ["start_date", "end_date", "granularity", "workcenter_groups", "families", "resource_ids", "is_production", "is_key", "is_monitor"],
"chart_data_shape": ["kpi", "trend", "heatmap", "workcenter_comparison", "detail"],
"state_contract": ["loading", "empty", "error", "success"]
},
"/qc-gate": {
"filter_input_semantics": ["chart_bucket_selection", "table_sort"],
"query_payload_contract": ["summary", "table", "pareto"],
"chart_data_shape": ["station_stack_chart", "linked_lot_table"],
"state_contract": ["loading", "empty", "error", "success"]
},
"/job-query": {
"filter_input_semantics": ["resource_ids", "start_date", "end_date"],
"query_payload_contract": ["resource_ids", "start_date", "end_date"],
"chart_data_shape": ["job_table", "txn_table"],
"state_contract": ["loading", "empty", "error", "success"]
},
"/tmtt-defect": {
"filter_input_semantics": ["start_date", "end_date", "sort", "page"],
"query_payload_contract": ["start_date", "end_date"],
"chart_data_shape": ["kpi", "pareto", "trend", "detail_table"],
"state_contract": ["loading", "empty", "error", "success"]
},
"/admin/pages": {
"filter_input_semantics": ["drawer_crud", "page_status_edit", "order_updates"],
"query_payload_contract": ["drawers", "pages", "status"],
"chart_data_shape": ["n/a"],
"state_contract": ["loading", "empty", "error", "success"]
},
"/admin/performance": {
"filter_input_semantics": ["date_range", "severity", "search"],
"query_payload_contract": ["performance_summary", "log_stream"],
"chart_data_shape": ["timeline", "status_summary"],
"state_contract": ["loading", "empty", "error", "success"]
}
}
}

View File

@@ -0,0 +1,166 @@
{
"change": "full-modernization-architecture-blueprint",
"generated_at": "2026-02-12T00:00:00Z",
"routes": [
{
"route": "/wip-overview",
"route_id": "wip-overview",
"scope": "in-scope",
"render_mode": "native",
"owner": "frontend-mes-reporting",
"visibility_policy": "released_or_admin",
"canonical_shell_path": "/portal-shell/wip-overview",
"rollback_strategy": "fallback_to_legacy_route"
},
{
"route": "/wip-detail",
"route_id": "wip-detail",
"scope": "in-scope",
"render_mode": "native",
"owner": "frontend-mes-reporting",
"visibility_policy": "released_or_admin",
"canonical_shell_path": "/portal-shell/wip-detail",
"rollback_strategy": "fallback_to_legacy_route"
},
{
"route": "/hold-overview",
"route_id": "hold-overview",
"scope": "in-scope",
"render_mode": "native",
"owner": "frontend-mes-reporting",
"visibility_policy": "released_or_admin",
"canonical_shell_path": "/portal-shell/hold-overview",
"rollback_strategy": "fallback_to_legacy_route"
},
{
"route": "/hold-detail",
"route_id": "hold-detail",
"scope": "in-scope",
"render_mode": "native",
"owner": "frontend-mes-reporting",
"visibility_policy": "released_or_admin",
"canonical_shell_path": "/portal-shell/hold-detail",
"rollback_strategy": "fallback_to_legacy_route"
},
{
"route": "/hold-history",
"route_id": "hold-history",
"scope": "in-scope",
"render_mode": "native",
"owner": "frontend-mes-reporting",
"visibility_policy": "released_or_admin",
"canonical_shell_path": "/portal-shell/hold-history",
"rollback_strategy": "fallback_to_legacy_route"
},
{
"route": "/resource",
"route_id": "resource",
"scope": "in-scope",
"render_mode": "native",
"owner": "frontend-mes-reporting",
"visibility_policy": "released_or_admin",
"canonical_shell_path": "/portal-shell/resource",
"rollback_strategy": "fallback_to_legacy_route"
},
{
"route": "/resource-history",
"route_id": "resource-history",
"scope": "in-scope",
"render_mode": "native",
"owner": "frontend-mes-reporting",
"visibility_policy": "released_or_admin",
"canonical_shell_path": "/portal-shell/resource-history",
"rollback_strategy": "fallback_to_legacy_route"
},
{
"route": "/qc-gate",
"route_id": "qc-gate",
"scope": "in-scope",
"render_mode": "native",
"owner": "frontend-mes-reporting",
"visibility_policy": "released_or_admin",
"canonical_shell_path": "/portal-shell/qc-gate",
"rollback_strategy": "fallback_to_legacy_route"
},
{
"route": "/job-query",
"route_id": "job-query",
"scope": "in-scope",
"render_mode": "native",
"owner": "frontend-mes-reporting",
"visibility_policy": "released_or_admin",
"canonical_shell_path": "/portal-shell/job-query",
"rollback_strategy": "fallback_to_legacy_route"
},
{
"route": "/tmtt-defect",
"route_id": "tmtt-defect",
"scope": "in-scope",
"render_mode": "native",
"owner": "frontend-mes-reporting",
"visibility_policy": "released_or_admin",
"canonical_shell_path": "/portal-shell/tmtt-defect",
"rollback_strategy": "fallback_to_legacy_route"
},
{
"route": "/admin/pages",
"route_id": "admin-pages",
"scope": "in-scope",
"render_mode": "external",
"owner": "frontend-platform-admin",
"visibility_policy": "admin_only",
"canonical_shell_path": "/portal-shell/admin/pages",
"rollback_strategy": "external_route_reversion"
},
{
"route": "/admin/performance",
"route_id": "admin-performance",
"scope": "in-scope",
"render_mode": "external",
"owner": "frontend-platform-admin",
"visibility_policy": "admin_only",
"canonical_shell_path": "/portal-shell/admin/performance",
"rollback_strategy": "external_route_reversion"
},
{
"route": "/tables",
"route_id": "tables",
"scope": "deferred",
"render_mode": "native",
"owner": "frontend-mes-reporting",
"visibility_policy": "released_or_admin",
"canonical_shell_path": "/portal-shell/tables",
"rollback_strategy": "fallback_to_legacy_route"
},
{
"route": "/excel-query",
"route_id": "excel-query",
"scope": "deferred",
"render_mode": "native",
"owner": "frontend-mes-reporting",
"visibility_policy": "released_or_admin",
"canonical_shell_path": "/portal-shell/excel-query",
"rollback_strategy": "fallback_to_legacy_route"
},
{
"route": "/query-tool",
"route_id": "query-tool",
"scope": "deferred",
"render_mode": "native",
"owner": "frontend-mes-reporting",
"visibility_policy": "released_or_admin",
"canonical_shell_path": "/portal-shell/query-tool",
"rollback_strategy": "fallback_to_legacy_route"
},
{
"route": "/mid-section-defect",
"route_id": "mid-section-defect",
"scope": "deferred",
"render_mode": "native",
"owner": "frontend-mes-reporting",
"visibility_policy": "released_or_admin",
"canonical_shell_path": "/portal-shell/mid-section-defect",
"rollback_strategy": "fallback_to_legacy_route"
}
]
}

View File

@@ -0,0 +1,113 @@
{
"change": "full-modernization-architecture-blueprint",
"generated_at": "2026-02-12T00:00:00Z",
"phase": "phase-1-shell-and-content-modernization",
"policy": {
"scope_is_frozen": true,
"out_of_scope_tasks_must_be_rejected": true
},
"in_scope": [
{
"route": "/wip-overview",
"category": "report",
"canonical_shell_path": "/portal-shell/wip-overview",
"owner": "frontend-mes-reporting",
"visibility_policy": "released_or_admin"
},
{
"route": "/wip-detail",
"category": "report",
"canonical_shell_path": "/portal-shell/wip-detail",
"owner": "frontend-mes-reporting",
"visibility_policy": "released_or_admin"
},
{
"route": "/hold-overview",
"category": "report",
"canonical_shell_path": "/portal-shell/hold-overview",
"owner": "frontend-mes-reporting",
"visibility_policy": "released_or_admin"
},
{
"route": "/hold-detail",
"category": "report",
"canonical_shell_path": "/portal-shell/hold-detail",
"owner": "frontend-mes-reporting",
"visibility_policy": "released_or_admin"
},
{
"route": "/hold-history",
"category": "report",
"canonical_shell_path": "/portal-shell/hold-history",
"owner": "frontend-mes-reporting",
"visibility_policy": "released_or_admin"
},
{
"route": "/resource",
"category": "report",
"canonical_shell_path": "/portal-shell/resource",
"owner": "frontend-mes-reporting",
"visibility_policy": "released_or_admin"
},
{
"route": "/resource-history",
"category": "report",
"canonical_shell_path": "/portal-shell/resource-history",
"owner": "frontend-mes-reporting",
"visibility_policy": "released_or_admin"
},
{
"route": "/qc-gate",
"category": "report",
"canonical_shell_path": "/portal-shell/qc-gate",
"owner": "frontend-mes-reporting",
"visibility_policy": "released_or_admin"
},
{
"route": "/job-query",
"category": "report",
"canonical_shell_path": "/portal-shell/job-query",
"owner": "frontend-mes-reporting",
"visibility_policy": "released_or_admin"
},
{
"route": "/tmtt-defect",
"category": "report",
"canonical_shell_path": "/portal-shell/tmtt-defect",
"owner": "frontend-mes-reporting",
"visibility_policy": "released_or_admin"
},
{
"route": "/admin/pages",
"category": "admin",
"canonical_shell_path": "/portal-shell/admin/pages",
"owner": "frontend-platform-admin",
"visibility_policy": "admin_only"
},
{
"route": "/admin/performance",
"category": "admin",
"canonical_shell_path": "/portal-shell/admin/performance",
"owner": "frontend-platform-admin",
"visibility_policy": "admin_only"
}
],
"deferred": [
{
"route": "/tables",
"reason": "deferred-to-follow-up-change"
},
{
"route": "/excel-query",
"reason": "deferred-to-follow-up-change"
},
{
"route": "/query-tool",
"reason": "deferred-to-follow-up-change"
},
{
"route": "/mid-section-defect",
"reason": "deferred-to-follow-up-change"
}
]
}

View File

@@ -0,0 +1,30 @@
{
"change": "full-modernization-architecture-blueprint",
"generated_at": "2026-02-12T00:00:00Z",
"in_scope_routes": {
"/wip-overview": {"global_selectors": [], "status": "clean"},
"/wip-detail": {"global_selectors": [], "status": "clean"},
"/hold-overview": {"global_selectors": [], "status": "clean"},
"/hold-detail": {"global_selectors": [], "status": "clean"},
"/hold-history": {"global_selectors": [], "status": "clean"},
"/resource": {"global_selectors": [], "status": "clean"},
"/resource-history": {"global_selectors": [], "status": "clean"},
"/qc-gate": {"global_selectors": [], "status": "clean-after-refactor"},
"/job-query": {"global_selectors": [], "status": "clean"},
"/tmtt-defect": {"global_selectors": [], "status": "clean"},
"/admin/pages": {
"global_selectors": ["body"],
"status": "exception-registered",
"exception_id": "style-admin-pages-inline-css"
},
"/admin/performance": {
"global_selectors": ["body"],
"status": "exception-registered",
"exception_id": "style-admin-performance-inline-css"
}
},
"shared_layers": {
"frontend/src/styles/tailwind.css": [":root", "body"],
"frontend/src/portal-shell/style.css": [":root", "body"]
}
}

View File

@@ -1,4 +1,5 @@
{
"generated_at": "2026-02-12T02:26:36.887797+00:00",
"source": "data/page_status.json",
"admin": [
{
@@ -16,7 +17,7 @@
{
"route": "/hold-overview",
"name": "Hold 即時概況",
"status": "dev",
"status": "released",
"order": 2
},
{
@@ -42,7 +43,7 @@
{
"route": "/hold-history",
"name": "Hold 歷史績效",
"status": "dev",
"status": "released",
"order": 3
},
{
@@ -106,7 +107,7 @@
{
"route": "/tmtt-defect",
"name": "TMTT印字腳型不良分析",
"status": "released",
"status": "dev",
"order": 5
},
{
@@ -131,6 +132,12 @@
"status": "released",
"order": 1
},
{
"route": "/hold-overview",
"name": "Hold 即時概況",
"status": "released",
"order": 2
},
{
"route": "/resource",
"name": "設備即時概況",
@@ -151,6 +158,12 @@
"order": 2,
"admin_only": false,
"pages": [
{
"route": "/hold-history",
"name": "Hold 歷史績效",
"status": "released",
"order": 3
},
{
"route": "/resource-history",
"name": "設備歷史績效",

View File

@@ -1,5 +1,5 @@
{
"generated_at": "2026-02-11T07:44:03+00:00",
"generated_at": "2026-02-12T02:26:36.887797+00:00",
"source": "data/page_status.json",
"admin": [
{
@@ -17,7 +17,7 @@
{
"route": "/hold-overview",
"name": "Hold 即時概況",
"status": "dev",
"status": "released",
"order": 2
},
{
@@ -43,7 +43,7 @@
{
"route": "/hold-history",
"name": "Hold 歷史績效",
"status": "dev",
"status": "released",
"order": 3
},
{
@@ -107,7 +107,7 @@
{
"route": "/tmtt-defect",
"name": "TMTT印字腳型不良分析",
"status": "released",
"status": "dev",
"order": 5
},
{
@@ -132,6 +132,12 @@
"status": "released",
"order": 1
},
{
"route": "/hold-overview",
"name": "Hold 即時概況",
"status": "released",
"order": 2
},
{
"route": "/resource",
"name": "設備即時概況",
@@ -152,6 +158,12 @@
"order": 2,
"admin_only": false,
"pages": [
{
"route": "/hold-history",
"name": "Hold 歷史績效",
"status": "released",
"order": 3
},
{
"route": "/resource-history",
"name": "設備歷史績效",

View File

@@ -22,17 +22,13 @@ const props = defineProps({
const emit = defineEmits(['change']);
const holdTypeModel = computed({
get() {
return props.holdType || 'quality';
},
set(nextValue) {
emit('change', {
holdType: nextValue || 'quality',
reason: props.reason || '',
});
},
});
const HOLD_TYPE_OPTIONS = Object.freeze([
{ value: 'quality', label: '品質異常' },
{ value: 'non-quality', label: '非品質異常' },
{ value: 'all', label: '全部' },
]);
const holdTypeModel = computed(() => props.holdType || 'quality');
const reasonModel = computed({
get() {
@@ -59,29 +55,44 @@ const reasonOptions = computed(() => {
});
return items;
});
function selectHoldType(nextValue) {
if (props.disabled) {
return;
}
const normalized = nextValue || 'quality';
if (normalized === holdTypeModel.value) {
return;
}
emit('change', {
holdType: normalized,
reason: props.reason || '',
});
}
</script>
<template>
<section class="filter-bar card">
<div class="filter-group hold-type-group">
<section class="filter-bar card hold-overview-filter-bar">
<div class="filter-group hold-type-group hold-overview-hold-type-group">
<span class="filter-label">Hold Type</span>
<div class="radio-group">
<label class="radio-option" :class="{ active: holdTypeModel === 'quality' }">
<input v-model="holdTypeModel" type="radio" value="quality" :disabled="disabled" />
<span>品質異常</span>
</label>
<label class="radio-option" :class="{ active: holdTypeModel === 'non-quality' }">
<input v-model="holdTypeModel" type="radio" value="non-quality" :disabled="disabled" />
<span>非品質異常</span>
</label>
<label class="radio-option" :class="{ active: holdTypeModel === 'all' }">
<input v-model="holdTypeModel" type="radio" value="all" :disabled="disabled" />
<span>全部</span>
</label>
<div class="hold-type-segment" role="radiogroup" aria-label="Hold Type">
<button
v-for="item in HOLD_TYPE_OPTIONS"
:key="item.value"
type="button"
role="radio"
class="hold-type-btn"
:class="{ active: holdTypeModel === item.value }"
:aria-checked="holdTypeModel === item.value"
:disabled="disabled"
@click="selectHoldType(item.value)"
>
{{ item.label }}
</button>
</div>
</div>
<div class="filter-group reason-group">
<div class="filter-group reason-group hold-overview-reason-group">
<label class="filter-label" for="hold-overview-reason">Reason</label>
<select
id="hold-overview-reason"

View File

@@ -56,45 +56,74 @@
padding: 16px 20px;
}
.hold-overview-filter-bar {
justify-content: space-between;
}
.filter-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.hold-overview-hold-type-group {
flex: 1 1 520px;
}
.hold-overview-reason-group {
flex: 0 0 auto;
}
.filter-label {
font-size: 12px;
font-weight: 600;
color: var(--muted);
}
.radio-group {
display: flex;
gap: 12px;
flex-wrap: wrap;
.hold-type-segment {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 8px;
min-width: 420px;
padding: 4px;
border: 1px solid var(--border);
border-radius: 12px;
background: #f8fafc;
}
.radio-option {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 10px;
border: 1px solid var(--border);
.hold-type-btn {
appearance: none;
border: none;
background: transparent;
color: #475569;
font-size: 14px;
font-weight: 600;
line-height: 1.25;
padding: 10px 14px;
border-radius: 8px;
cursor: pointer;
font-size: 13px;
background: #fff;
transition: background-color 0.15s ease, color 0.15s ease, box-shadow 0.15s ease;
}
.radio-option.active {
border-color: var(--primary);
.hold-type-btn:hover:not(:disabled) {
background: #eef2ff;
color: var(--primary-dark);
font-weight: 600;
color: #3730a3;
}
.radio-option input {
margin: 0;
.hold-type-btn.active {
background: #dbeafe;
color: #1e3a8a;
box-shadow: inset 0 0 0 1px #93c5fd;
}
.hold-type-btn:focus-visible {
outline: none;
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.25);
}
.hold-type-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.reason-select {
@@ -353,6 +382,11 @@
align-items: stretch;
}
.hold-type-segment {
min-width: 0;
width: 100%;
}
.reason-select {
width: 100%;
min-width: 0;

View File

@@ -9,8 +9,12 @@
padding: 22px 24px;
margin-bottom: 16px;
color: #fff;
background: linear-gradient(135deg, var(--portal-brand-start) 0%, var(--portal-brand-end) 100%);
box-shadow: var(--portal-shadow-panel);
background: linear-gradient(
135deg,
var(--portal-brand-start, #6366f1) 0%,
var(--portal-brand-end, #7c3aed) 100%
);
box-shadow: var(--portal-shadow-panel, 0 8px 24px rgba(79, 70, 229, 0.18));
}
.job-query-header h1 {

View File

@@ -102,7 +102,7 @@ export function buildDynamicNavigationState(
diagnostics.missingContractRoutes.push(page.route);
}
const renderMode = 'native';
const renderMode = contract?.renderMode || 'native';
dynamicRoutes.push({
routeName: `shell-page-${index++}`,
shellPath,
@@ -112,6 +112,9 @@ export function buildDynamicNavigationState(
owner: contract?.owner || '',
renderMode,
routeId: contract?.routeId || '',
visibilityPolicy: contract?.visibilityPolicy || 'released_or_admin',
scope: contract?.scope || 'unknown',
compatibilityPolicy: contract?.compatibilityPolicy || 'legacy_direct_entry_allowed',
});
registeredRoutes.add(page.route);
allowedPaths.push(shellPath);
@@ -141,8 +144,11 @@ export function buildDynamicNavigationState(
pageName: contract?.title || route,
drawerName: '',
owner: contract?.owner || '',
renderMode: 'native',
renderMode: contract?.renderMode || 'native',
routeId: contract?.routeId || '',
visibilityPolicy: contract?.visibilityPolicy || 'released_or_admin',
scope: contract?.scope || 'unknown',
compatibilityPolicy: contract?.compatibilityPolicy || 'legacy_direct_entry_allowed',
});
registeredRoutes.add(route);
allowedPaths.push(shellPath);

View File

@@ -1,90 +1,255 @@
const IN_SCOPE_REPORT_ROUTES = Object.freeze([
'/wip-overview',
'/wip-detail',
'/hold-overview',
'/hold-detail',
'/hold-history',
'/resource',
'/resource-history',
'/qc-gate',
'/job-query',
'/tmtt-defect',
]);
const IN_SCOPE_ADMIN_ROUTES = Object.freeze([
'/admin/pages',
'/admin/performance',
]);
const DEFERRED_ROUTES = Object.freeze([
'/tables',
'/excel-query',
'/query-tool',
'/mid-section-defect',
]);
const ALL_KNOWN_ROUTES = Object.freeze([
...IN_SCOPE_REPORT_ROUTES,
...IN_SCOPE_ADMIN_ROUTES,
...DEFERRED_ROUTES,
]);
function buildContract({
route,
routeId,
title,
owner,
renderMode,
rollbackStrategy,
visibilityPolicy,
scope,
compatibilityPolicy,
}) {
return Object.freeze({
routeId,
route,
title,
owner,
renderMode,
rollbackStrategy,
visibilityPolicy,
scope,
compatibilityPolicy,
canonicalShellPath: `/portal-shell${route}`,
});
}
const ROUTE_CONTRACTS = Object.freeze({
'/wip-overview': {
'/wip-overview': buildContract({
route: '/wip-overview',
routeId: 'wip-overview',
renderMode: 'native',
owner: 'frontend-mes-reporting',
title: 'WIP 即時概況',
rollbackStrategy: 'fallback_to_legacy_route',
},
'/wip-detail': {
visibilityPolicy: 'released_or_admin',
scope: 'in-scope',
compatibilityPolicy: 'redirect_to_shell_when_spa_enabled',
}),
'/wip-detail': buildContract({
route: '/wip-detail',
routeId: 'wip-detail',
renderMode: 'native',
owner: 'frontend-mes-reporting',
title: 'WIP 詳細列表',
rollbackStrategy: 'fallback_to_legacy_route',
},
'/hold-overview': {
visibilityPolicy: 'released_or_admin',
scope: 'in-scope',
compatibilityPolicy: 'redirect_to_shell_when_spa_enabled',
}),
'/hold-overview': buildContract({
route: '/hold-overview',
routeId: 'hold-overview',
renderMode: 'native',
owner: 'frontend-mes-reporting',
title: 'Hold 即時概況',
rollbackStrategy: 'fallback_to_legacy_route',
},
'/hold-detail': {
visibilityPolicy: 'released_or_admin',
scope: 'in-scope',
compatibilityPolicy: 'redirect_to_shell_when_spa_enabled',
}),
'/hold-detail': buildContract({
route: '/hold-detail',
routeId: 'hold-detail',
renderMode: 'native',
owner: 'frontend-mes-reporting',
title: 'Hold 詳細查詢',
rollbackStrategy: 'fallback_to_legacy_route',
},
'/hold-history': {
visibilityPolicy: 'released_or_admin',
scope: 'in-scope',
compatibilityPolicy: 'redirect_to_shell_when_spa_enabled',
}),
'/hold-history': buildContract({
route: '/hold-history',
routeId: 'hold-history',
renderMode: 'native',
owner: 'frontend-mes-reporting',
title: 'Hold 歷史報表',
rollbackStrategy: 'fallback_to_legacy_route',
},
'/resource': {
visibilityPolicy: 'released_or_admin',
scope: 'in-scope',
compatibilityPolicy: 'redirect_to_shell_when_spa_enabled',
}),
'/resource': buildContract({
route: '/resource',
routeId: 'resource',
renderMode: 'native',
owner: 'frontend-mes-reporting',
title: '設備即時狀況',
rollbackStrategy: 'fallback_to_legacy_route',
},
'/resource-history': {
visibilityPolicy: 'released_or_admin',
scope: 'in-scope',
compatibilityPolicy: 'redirect_to_shell_when_spa_enabled',
}),
'/resource-history': buildContract({
route: '/resource-history',
routeId: 'resource-history',
renderMode: 'native',
owner: 'frontend-mes-reporting',
title: '設備歷史績效',
rollbackStrategy: 'fallback_to_legacy_route',
},
'/qc-gate': {
visibilityPolicy: 'released_or_admin',
scope: 'in-scope',
compatibilityPolicy: 'redirect_to_shell_when_spa_enabled',
}),
'/qc-gate': buildContract({
route: '/qc-gate',
routeId: 'qc-gate',
renderMode: 'native',
owner: 'frontend-mes-reporting',
title: 'QC-GATE 狀態',
rollbackStrategy: 'fallback_to_legacy_route',
},
'/job-query': {
visibilityPolicy: 'released_or_admin',
scope: 'in-scope',
compatibilityPolicy: 'redirect_to_shell_when_spa_enabled',
}),
'/job-query': buildContract({
route: '/job-query',
routeId: 'job-query',
renderMode: 'native',
owner: 'frontend-mes-reporting',
title: '設備維修查詢',
rollbackStrategy: 'fallback_to_legacy_route',
},
'/excel-query': {
routeId: 'excel-query',
renderMode: 'native',
owner: 'frontend-mes-reporting',
title: 'Excel 查詢工具',
rollbackStrategy: 'fallback_to_legacy_route',
},
'/query-tool': {
routeId: 'query-tool',
renderMode: 'native',
owner: 'frontend-mes-reporting',
title: 'Query Tool',
rollbackStrategy: 'fallback_to_legacy_route',
},
'/tmtt-defect': {
visibilityPolicy: 'released_or_admin',
scope: 'in-scope',
compatibilityPolicy: 'redirect_to_shell_when_spa_enabled',
}),
'/tmtt-defect': buildContract({
route: '/tmtt-defect',
routeId: 'tmtt-defect',
renderMode: 'native',
owner: 'frontend-mes-reporting',
title: 'TMTT Defect',
rollbackStrategy: 'fallback_to_legacy_route',
},
visibilityPolicy: 'released_or_admin',
scope: 'in-scope',
compatibilityPolicy: 'redirect_to_shell_when_spa_enabled',
}),
'/admin/pages': buildContract({
route: '/admin/pages',
routeId: 'admin-pages',
renderMode: 'external',
owner: 'frontend-platform-admin',
title: '頁面管理',
rollbackStrategy: 'external_route_reversion',
visibilityPolicy: 'admin_only',
scope: 'in-scope',
compatibilityPolicy: 'external_target_redirect',
}),
'/admin/performance': buildContract({
route: '/admin/performance',
routeId: 'admin-performance',
renderMode: 'external',
owner: 'frontend-platform-admin',
title: '效能監控',
rollbackStrategy: 'external_route_reversion',
visibilityPolicy: 'admin_only',
scope: 'in-scope',
compatibilityPolicy: 'external_target_redirect',
}),
'/tables': buildContract({
route: '/tables',
routeId: 'tables',
renderMode: 'native',
owner: 'frontend-mes-reporting',
title: '表格總覽',
rollbackStrategy: 'fallback_to_legacy_route',
visibilityPolicy: 'released_or_admin',
scope: 'deferred',
compatibilityPolicy: 'legacy_direct_entry_allowed',
}),
'/excel-query': buildContract({
route: '/excel-query',
routeId: 'excel-query',
renderMode: 'native',
owner: 'frontend-mes-reporting',
title: 'Excel 查詢工具',
rollbackStrategy: 'fallback_to_legacy_route',
visibilityPolicy: 'released_or_admin',
scope: 'deferred',
compatibilityPolicy: 'legacy_direct_entry_allowed',
}),
'/query-tool': buildContract({
route: '/query-tool',
routeId: 'query-tool',
renderMode: 'native',
owner: 'frontend-mes-reporting',
title: 'Query Tool',
rollbackStrategy: 'fallback_to_legacy_route',
visibilityPolicy: 'released_or_admin',
scope: 'deferred',
compatibilityPolicy: 'legacy_direct_entry_allowed',
}),
'/mid-section-defect': buildContract({
route: '/mid-section-defect',
routeId: 'mid-section-defect',
renderMode: 'native',
owner: 'frontend-mes-reporting',
title: '中段製程不良追溯',
rollbackStrategy: 'fallback_to_legacy_route',
visibilityPolicy: 'released_or_admin',
scope: 'deferred',
compatibilityPolicy: 'legacy_direct_entry_allowed',
}),
});
const REQUIRED_FIELDS = Object.freeze([
'routeId',
'route',
'title',
'owner',
'renderMode',
'rollbackStrategy',
'visibilityPolicy',
'scope',
'compatibilityPolicy',
'canonicalShellPath',
]);
const VALID_RENDER_MODES = new Set(['native', 'external']);
const VALID_VISIBILITY_POLICIES = new Set(['released_or_admin', 'admin_only']);
const VALID_SCOPES = new Set(['in-scope', 'deferred']);
export function normalizeRoutePath(route) {
const normalized = String(route || '').trim();
if (!normalized || normalized === '/') {
@@ -100,3 +265,51 @@ export function getRouteContract(route) {
export function getRouteContractMap() {
return ROUTE_CONTRACTS;
}
export function getInScopeRoutes() {
return [...IN_SCOPE_REPORT_ROUTES, ...IN_SCOPE_ADMIN_ROUTES];
}
export function getDeferredRoutes() {
return [...DEFERRED_ROUTES];
}
export function getKnownRoutes() {
return [...ALL_KNOWN_ROUTES];
}
export function validateRouteContractMap({ inScopeOnly = false } = {}) {
const entries = Object.entries(ROUTE_CONTRACTS).filter(([, contract]) => {
if (!inScopeOnly) {
return true;
}
return contract.scope === 'in-scope';
});
const errors = [];
entries.forEach(([path, contract]) => {
REQUIRED_FIELDS.forEach((field) => {
if (!String(contract?.[field] ?? '').trim()) {
errors.push(`${path}: missing required field ${field}`);
}
});
if (contract.route !== path) {
errors.push(`${path}: route field does not match key`);
}
if (!VALID_RENDER_MODES.has(contract.renderMode)) {
errors.push(`${path}: invalid renderMode ${contract.renderMode}`);
}
if (!VALID_VISIBILITY_POLICIES.has(contract.visibilityPolicy)) {
errors.push(`${path}: invalid visibilityPolicy ${contract.visibilityPolicy}`);
}
if (!VALID_SCOPES.has(contract.scope)) {
errors.push(`${path}: invalid scope ${contract.scope}`);
}
if (contract.canonicalShellPath !== `/portal-shell${path}`) {
errors.push(`${path}: canonicalShellPath mismatch`);
}
});
return [...new Set(errors)].sort();
}

View File

@@ -58,6 +58,7 @@ export function syncNavigationRoutes(
pageName: entry.pageName,
drawerName: entry.drawerName,
owner: entry.owner,
renderMode: entry.renderMode,
},
meta: {
title: entry.pageName,
@@ -65,6 +66,9 @@ export function syncNavigationRoutes(
targetRoute: entry.targetRoute,
renderMode: entry.renderMode,
routeId: entry.routeId,
visibilityPolicy: entry.visibilityPolicy,
scope: entry.scope,
compatibilityPolicy: entry.compatibilityPolicy,
},
});
dynamicRouteNames.push(entry.routeName);

View File

@@ -21,6 +21,10 @@ const props = defineProps({
type: String,
default: '',
},
renderMode: {
type: String,
default: 'native',
},
});
const route = useRoute();
@@ -78,6 +82,12 @@ async function loadNativeModule(route) {
moduleError.value = '';
resolvedComponent.value = null;
if (props.renderMode === 'external') {
moduleLoading.value = false;
openLegacyPage();
return;
}
const loader = getNativeModuleLoader(route);
if (!loader) {
moduleLoading.value = false;
@@ -101,8 +111,8 @@ async function loadNativeModule(route) {
}
watch(
() => props.targetRoute,
(route) => {
() => [props.targetRoute, props.renderMode],
([route]) => {
void loadNativeModule(route);
},
);

View File

@@ -1,34 +1,28 @@
:root {
.qc-gate-page,
.qc-gate-page * {
box-sizing: border-box;
}
.qc-gate-page {
--bg: #f5f7fa;
--card-bg: #ffffff;
--text: #1f2937;
--muted: #64748b;
--border: #dbe3ef;
--header-from: #667eea;
--header-to: #764ba2;
--header-from: var(--portal-brand-start, #6366f1);
--header-to: var(--portal-brand-end, #7c3aed);
--success: #22c55e;
--warning: #facc15;
--danger: #ef4444;
--shadow: 0 2px 10px rgba(15, 23, 42, 0.08);
--shadow-strong: 0 6px 24px rgba(102, 126, 234, 0.24);
}
* {
box-sizing: border-box;
}
body {
margin: 0;
margin: 0 auto;
font-family: 'Microsoft JhengHei', Arial, sans-serif;
background: var(--bg);
color: var(--text);
}
.qc-gate-page {
min-height: 100vh;
padding: 20px;
max-width: 1900px;
margin: 0 auto;
}
.qc-gate-header {

View File

@@ -9,8 +9,12 @@
padding: 22px 24px;
margin-bottom: 16px;
color: #fff;
background: linear-gradient(135deg, var(--portal-brand-start) 0%, var(--portal-brand-end) 100%);
box-shadow: var(--portal-shadow-panel);
background: linear-gradient(
135deg,
var(--portal-brand-start, #6366f1) 0%,
var(--portal-brand-end, #7c3aed) 100%
);
box-shadow: var(--portal-shadow-panel, 0 8px 24px rgba(79, 70, 229, 0.18));
}
.tmtt-header h1 {
@@ -113,7 +117,7 @@
border-radius: 10px;
border-left: 4px solid #64748b;
background: #fff;
box-shadow: var(--portal-shadow-soft);
box-shadow: var(--portal-shadow-soft, 0 2px 8px rgba(15, 23, 42, 0.08));
}
.tmtt-kpi-card.tone-danger {

View File

@@ -1,10 +1,14 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { existsSync, readFileSync } from 'node:fs';
import { resolve } from 'node:path';
function readSource(relativePath) {
return readFileSync(resolve(process.cwd(), relativePath), 'utf8');
const directPath = resolve(process.cwd(), relativePath);
if (existsSync(directPath)) {
return readFileSync(directPath, 'utf8');
}
return readFileSync(resolve(process.cwd(), 'frontend', relativePath), 'utf8');
}
test('portal shell app renders health summary component and admin entry controls', () => {

View File

@@ -1,6 +1,6 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { existsSync, readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import {
@@ -10,7 +10,11 @@ import {
} from '../src/portal-shell/healthSummary.js';
function readSource(relativePath) {
return readFileSync(resolve(process.cwd(), relativePath), 'utf8');
const directPath = resolve(process.cwd(), relativePath);
if (existsSync(directPath)) {
return readFileSync(directPath, 'utf8');
}
return readFileSync(resolve(process.cwd(), 'frontend', relativePath), 'utf8');
}

View File

@@ -105,3 +105,26 @@ test('buildDynamicNavigationState can include standalone drilldown routes withou
assert.equal(state.allowedPaths.includes('/wip-detail'), true);
assert.equal(state.allowedPaths.includes('/hold-detail'), true);
});
test('buildDynamicNavigationState keeps admin routes as governed external targets', () => {
const state = buildDynamicNavigationState(
[
{
id: 'dev-tools',
name: 'Dev',
order: 1,
admin_only: true,
pages: [{ route: '/admin/pages', name: 'Admin Pages', status: 'dev', order: 1 }],
},
],
{ isAdmin: true },
);
assert.equal(state.diagnostics.missingContractRoutes.length, 0);
assert.equal(state.dynamicRoutes.length, 1);
assert.equal(state.dynamicRoutes[0].targetRoute, '/admin/pages');
assert.equal(state.dynamicRoutes[0].renderMode, 'external');
assert.equal(state.dynamicRoutes[0].visibilityPolicy, 'admin_only');
assert.equal(state.dynamicRoutes[0].scope, 'in-scope');
});

View File

@@ -1,10 +1,14 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { existsSync, readFileSync } from 'node:fs';
import { resolve } from 'node:path';
function readSource(relativePath) {
return readFileSync(resolve(process.cwd(), relativePath), 'utf8');
const directPath = resolve(process.cwd(), relativePath);
if (existsSync(directPath)) {
return readFileSync(directPath, 'utf8');
}
return readFileSync(resolve(process.cwd(), 'frontend', relativePath), 'utf8');
}
test('shell route host sources do not contain iframe rendering paths', () => {

View File

@@ -1,10 +1,14 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { existsSync, readFileSync } from 'node:fs';
import { resolve } from 'node:path';
function readSource(relativePath) {
return readFileSync(resolve(process.cwd(), relativePath), 'utf8');
const directPath = resolve(process.cwd(), relativePath);
if (existsSync(directPath)) {
return readFileSync(directPath, 'utf8');
}
return readFileSync(resolve(process.cwd(), 'frontend', relativePath), 'utf8');
}
test('table parity: Wave B native pages keep deterministic column and empty-state handling', () => {

View File

@@ -0,0 +1,83 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import {
getDeferredRoutes,
getInScopeRoutes,
getRouteContract,
getKnownRoutes,
validateRouteContractMap,
} from '../src/portal-shell/routeContracts.js';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const backendContractPath = path.resolve(
__dirname,
'../../docs/migration/full-modernization-architecture-blueprint/route_contracts.json',
);
test('in-scope route contracts satisfy governance metadata requirements', () => {
const errors = validateRouteContractMap({ inScopeOnly: true });
assert.deepEqual(errors, []);
});
test('admin shell targets are governed and rendered as external targets', () => {
const pagesContract = getRouteContract('/admin/pages');
const perfContract = getRouteContract('/admin/performance');
assert.equal(pagesContract.scope, 'in-scope');
assert.equal(perfContract.scope, 'in-scope');
assert.equal(pagesContract.visibilityPolicy, 'admin_only');
assert.equal(perfContract.visibilityPolicy, 'admin_only');
assert.equal(pagesContract.renderMode, 'external');
assert.equal(perfContract.renderMode, 'external');
});
test('deferred routes stay discoverable but are separable from in-scope gates', () => {
const inScope = new Set(getInScopeRoutes());
const deferred = getDeferredRoutes();
deferred.forEach((route) => {
assert.equal(inScope.has(route), false, `deferred route leaked into in-scope: ${route}`);
const contract = getRouteContract(route);
assert.equal(contract.scope, 'deferred');
});
});
test('known route inventory covers in-scope + deferred surfaces', () => {
const known = new Set(getKnownRoutes());
[...getInScopeRoutes(), ...getDeferredRoutes()].forEach((route) => {
assert.equal(known.has(route), true, `missing known route: ${route}`);
});
});
test('frontend route inventory stays aligned with backend route contracts', () => {
const backendPayload = JSON.parse(fs.readFileSync(backendContractPath, 'utf-8'));
const backendRoutes = new Map(
(backendPayload.routes || []).map((route) => [route.route, route.scope]),
);
const frontendRoutes = getKnownRoutes();
assert.deepEqual(
[...new Set(frontendRoutes)].sort(),
[...backendRoutes.keys()].sort(),
'route set drift between frontend routeContracts.js and backend route_contracts.json',
);
frontendRoutes.forEach((route) => {
const frontendScope = getRouteContract(route).scope;
const backendScope = backendRoutes.get(route);
assert.equal(
frontendScope,
backendScope,
`scope mismatch for ${route}: frontend=${frontendScope}, backend=${backendScope}`,
);
});
});

View File

@@ -1,6 +1,6 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { existsSync, readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import {
@@ -11,7 +11,11 @@ import {
} from '../src/portal-shell/sidebarState.js';
function readSource(relativePath) {
return readFileSync(resolve(process.cwd(), relativePath), 'utf8');
const directPath = resolve(process.cwd(), relativePath);
if (existsSync(directPath)) {
return readFileSync(directPath, 'utf8');
}
return readFileSync(resolve(process.cwd(), 'frontend', relativePath), 'utf8');
}
test('buildSidebarUiState marks desktop collapse correctly', () => {

View File

@@ -1,10 +1,14 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { existsSync, readFileSync } from 'node:fs';
import { resolve } from 'node:path';
function readSource(relativePath) {
return readFileSync(resolve(process.cwd(), relativePath), 'utf8');
const directPath = resolve(process.cwd(), relativePath);
if (existsSync(directPath)) {
return readFileSync(directPath, 'utf8');
}
return readFileSync(resolve(process.cwd(), 'frontend', relativePath), 'utf8');
}
test('shell route view uses direct RouterView host (no transition blank-state)', () => {

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-02-11

View File

@@ -0,0 +1,146 @@
## Context
The project has completed the no-iframe shell migration and fluid shell Phase 1, but frontend modernization remains fragmented across routing governance, styling ownership, and quality enforcement.
Current state:
- Shell contract currently governs a subset of report routes, and route governance is implemented through `frontend/src/portal-shell/routeContracts.js` + `nativeModuleRegistry.js`.
- Multiple page families still ship page-global CSS patterns (`:root`, `body`, page-level max-width wrappers), creating style ownership ambiguity and cross-page leakage risk.
- Existing specs still preserve fallback-era assumptions in several areas (runtime fallback continuity and coexistence-oriented style policy).
Scope decisions for this change:
- In scope: admin surfaces `/admin/pages`, `/admin/performance`.
- Out of scope (follow-up change): `/tables`, `/excel-query`, `/query-tool`, `/mid-section-defect`.
Stakeholders:
- Frontend platform owners (shell/routing/design system)
- Report page module owners
- Backend maintainers for route serving and release gates
- QA/operations for release readiness and rollback governance
## Goals / Non-Goals
**Goals:**
- Define a shell-first canonical frontend architecture for in-scope routes with explicit route governance.
- Converge style architecture from permissive coexistence to enforceable isolation + token semantics.
- Modernize in-scope page content (charts, filters, interactions) with contract-first parity validation to prevent architecture drift during migration.
- Replace runtime fallback dependency for in-scope modules with build/deploy readiness gates.
- Add modernization-grade quality gates (behavioral, visual, accessibility, performance).
- Provide phased migration and rollback-safe program governance.
**Non-Goals:**
- Implementing code changes in this artifact phase.
- Rewriting excluded routes (`/tables`, `/excel-query`, `/query-tool`, `/mid-section-defect`) in this change.
- Replacing backend business APIs or changing report data semantics.
- Introducing mandatory framework rewrites for all pages in one release window.
## Decisions
### D1. Route Scope Matrix with Explicit Inclusion/Exclusion
**Decision:** Define an explicit route matrix for modernization enforcement.
- Included: in-scope shell-governed report routes + `/admin/pages`, `/admin/performance`.
- Excluded for follow-up: `/tables`, `/excel-query`, `/query-tool`, `/mid-section-defect`.
**Rationale:** Prevent uncontrolled scope creep while still addressing governance gaps for admin functionality.
**Alternatives considered:**
- Modernize all routes at once: rejected due to high blast radius and schedule risk.
- Only modernize report routes, defer admin pages: rejected because admin navigation governance remains inconsistent.
### D2. Canonical Shell Routing Policy for In-Scope Report Routes
**Decision:** In-scope report routes MUST have a canonical shell entry path and contract metadata. Direct route entry remains available only through explicit compatibility policy.
**Rationale:** Removes ambiguity between direct route behavior and shell-governed behavior; improves observability and testability.
**Alternatives considered:**
- Keep dual-mode indefinitely (direct vs shell): rejected due to long-term drift risk.
### D3. Admin Surfaces Become First-Class Contracted Navigation Targets
**Decision:** `/admin/pages` and `/admin/performance` are governed as explicit shell navigation targets with visibility and access policy, while retaining server authority for auth/session.
**Rationale:** Admin actions are part of platform governance and must be consistently modeled in route and visibility contracts.
### D4. Style Convergence Policy: Isolation + Token Enforcement
**Decision:** Move from "Tailwind and legacy CSS coexistence" to "controlled convergence":
- In-scope modules MUST avoid page-global selectors (`:root`, `body`) for page-local concerns.
- Shared semantics MUST be token-driven through `frontend/src/styles/tailwind.css` and shared UI layers.
- Legacy CSS usage in in-scope routes requires explicit exception policy.
**Rationale:** Reduces style collisions and maintenance overhead from fragmented style ownership.
### D5. Asset Readiness Over Runtime Fallback for In-Scope Modules
**Decision:** In-scope modules require build/deploy readiness guarantees; runtime fallback is no longer primary resilience strategy for these routes.
**Rationale:** Runtime fallback hides release failures and delays detection. Fail-fast at build/release is safer for correctness and easier to operate.
### D6. Modernization Quality Gates as Release Contract
**Decision:** Define mandatory route-level acceptance gates:
- Functional behavior parity
- Visual regression checks for critical states
- Accessibility checks (keyboard semantics, reduced-motion compatibility, landmark/label quality)
- Performance budgets (bundle and runtime thresholds)
**Rationale:** Architecture convergence without quality gates causes unstable rollouts and regressions.
### D7. Contract-First Page-Content Migration for Charts and Filters
**Decision:** In-scope chart/filter/page-content migration MUST follow a contract-first flow with reversible rollout:
- Freeze per-route content contracts before refactor (filter input semantics, query payload structure, chart data shape, interaction events, empty/error states).
- Implement a parity harness using golden fixtures and critical-state comparisons before switching default rendering.
- Cut over with route-scoped feature flags and immediate rollback controls (no irreversible flip in one step).
- Progress route-by-route only after manual acceptance sign-off is completed for the current route.
- Remove legacy content implementation only after parity checks and manual acceptance sign-off are completed.
**Rationale:** Prior migration failures were caused by implementation-first rewrites without strict parity and rollback controls, leading to layout/interaction drift and hard-to-debug regressions.
### D8. Mandatory "BUG Revalidation During Migration" Gate
**Decision:** Each route modernization MUST include explicit BUG revalidation before sign-off:
- Create a route-level known-bug baseline (within migrated scope: chart, filter, and page interaction behavior) before implementation.
- During manual acceptance, replay known-bug checks on the modernized route.
- If a known legacy bug is reproduced in the modernized implementation, route sign-off MUST fail and cutover/legacy-retirement MUST be blocked until fixed.
**Rationale:** Parity-only migration can accidentally preserve old defects. The modernization objective is not only structural migration, but also preventing legacy defect carry-over into the new architecture.
## Risks / Trade-offs
- **[Scope ambiguity]** Route inclusion can drift during execution. → Mitigation: publish a frozen in-scope/out-of-scope matrix in specs and tasks.
- **[Admin integration complexity]** Admin routes have different auth/session behavior than report modules. → Mitigation: keep backend auth authority unchanged; only modernize navigation contract layer in this change.
- **[Temporary dual standards]** Excluded routes still use legacy conventions. → Mitigation: explicit follow-up change linkage and governance deadline.
- **[Release friction increase]** Fail-fast asset readiness can block releases more often initially. → Mitigation: phased enforcement with warning mode then blocking mode.
- **[Style migration churn]** Token/isolation enforcement may require broad CSS refactor in in-scope pages. → Mitigation: staged rollout by route family and exception registry.
- **[Content migration instability]** Chart/filter rewrites can regress data semantics or interaction flows. → Mitigation: contract freeze + golden fixture parity + page-by-page manual acceptance + feature-flagged cutover with rollback.
- **[Legacy bug carry-over]** Modernized routes can pass parity yet still replicate known legacy defects. → Mitigation: mandatory per-route bug baseline and replay checks as blocking sign-off criteria.
## Migration Plan
1. Publish and freeze modernization scope matrix (included/excluded routes).
2. Define delta specs for route governance, style enforcement, quality gates, and asset readiness retirement policy.
3. Derive implementation tasks in phased waves (governance first, then style convergence, then gate enforcement).
4. Publish page-content contracts and golden fixtures for in-scope routes before chart/filter cutover.
5. Record route-level known-bug baselines for migrated scope and attach them to acceptance checklists.
6. Execute page-by-page manual acceptance sign-off (including bug replay checks) and only then move to the next route.
7. Enable non-blocking observability checks first (dry-run mode), then switch to blocking gates.
8. Roll out per route family with rollback criteria and runbook updates.
9. Open follow-up modernization change for excluded routes.
## Rollback Strategy
- Maintain reversible config switches for new quality gates during initial adoption.
- If blocking gates cause production-impacting false positives, revert gate mode to warning while preserving telemetry.
- Keep route-level rollback path documented for each in-scope family.
- Keep per-route manual acceptance records so rollback decisions can reference concrete pass/fail evidence.
- Keep per-route bug baseline and bug-replay results so cutover and rollback decisions can prove legacy bug non-carry-over.
## Open Questions
- Should admin pages remain backend-rendered targets with shell-managed links only, or move to shell-native view composition in a later phase?
- Which visual regression toolchain should be standardized (existing snapshot evidence extension vs dedicated UI visual diff pipeline)?
- What are initial enforceable performance thresholds for route bundles and shell startup latency?
- Should parity harness for charts use DOM-level snapshots only, or also include canonicalized data-level assertions as a blocking gate?

View File

@@ -0,0 +1,46 @@
## Why
The project has completed the critical iframe-to-Vue migration and Phase 1 shell fluidization, but the frontend architecture is still only partially modernized. Several routes remain outside shell contract governance, style ownership is still fragmented across page-local global CSS (`:root`, `body`, page-level `max-width`), and fallback-era patterns remain in docs and runtime expectations.
A full modernization blueprint is needed now to converge the system into a single, predictable frontend architecture before more feature work increases divergence and maintenance cost.
## What Changes
- Establish a shell-first architecture baseline where in-scope routes are contract-governed and module-registered with deterministic route visibility and ownership metadata.
- **BREAKING**: Retire legacy runtime fallback expectations for primary report routes in favor of build-readiness and deploy gating (fail-fast in CI/release instead of runtime degraded UX).
- **BREAKING**: Adopt canonical shell routing policy for report pages (legacy direct-entry routes become explicit compatibility redirects with sunset policy).
- Standardize style architecture: remove page-level global CSS side effects, enforce scoped styling boundaries, and converge to token-first Tailwind design primitives.
- Define contract-first page-content modernization for in-scope routes (charts, filters, and page interactions) with parity checkpoints before cutover.
- Use page-by-page manual acceptance sign-off as the required readiness gate for content migration progression.
- Enforce mandatory "BUG revalidation during migration" for each route so known legacy bugs in migrated scope SHALL NOT be carried into the new architecture.
- Introduce modernization quality gates covering interaction behavior, visual regressions, accessibility, and performance budgets.
- Define phased execution and rollback-safe governance for “full modernization” as a program (not a single code patch), including explicit completion criteria and deprecation milestones.
- Include admin surfaces in this change scope: `/admin/pages` and `/admin/performance` SHALL be first-class shell-governed routes with explicit contract metadata and visibility policy.
- Defer the following routes to a subsequent change (out of scope for this change): `/tables`, `/excel-query`, `/query-tool`, `/mid-section-defect`.
## Capabilities
### New Capabilities
- `frontend-platform-modernization-governance`: Defines target frontend architecture, phased milestones, modernization guardrails, and deprecation policy.
- `unified-shell-route-coverage`: Ensures in-scope routes (including admin pages/performance) are covered by shell route contracts, loader registry, and navigation visibility rules with CI blocking on gaps.
- `style-isolation-and-token-enforcement`: Enforces CSS scope boundaries and token-first style semantics; eliminates page-global style leakage patterns.
- `page-content-modernization-safety`: Governs chart/filter/page-content migration with contract baselines, parity verification, legacy bug carry-over prevention, and reversible rollout controls.
- `frontend-quality-gate-modernization`: Adds behavioral, visual, accessibility, and performance acceptance gates for route-level changes.
- `asset-readiness-and-fallback-retirement`: Defines build/deploy guarantees and controlled retirement of runtime fallback-era behavior.
### Modified Capabilities
- `spa-shell-navigation`: Extend from current native route-view baseline to full route coverage, canonical shell routing, and contract completeness enforcement.
- `tailwind-design-system`: Shift from coexistence-with-legacy to convergence-and-enforcement with explicit legacy CSS deprecation rules.
- `full-vite-page-modularization`: Tighten from “fallback continuity” to “asset readiness governance” with stronger release-time guarantees.
- `vue-vite-page-architecture`: Update route serving expectations to shell-first canonical model with explicit compatibility redirects and sunset criteria.
## Impact
- **Frontend shell and routing**: `frontend/src/portal-shell/` (`App.vue`, `router.js`, `routeContracts.js`, `nativeModuleRegistry.js`, navigation metadata handling).
- **Frontend page style system**: `frontend/src/*/style.css`, `frontend/src/*/styles.css`, `frontend/src/styles/tailwind.css`, shared UI/style modules.
- **Frontend page-content modules**: chart, filter, and page interaction modules under `frontend/src/**` used by in-scope routes, including compatibility adapters where needed.
- **Backend route serving and compatibility**: Flask route handlers that serve page entries and legacy direct-entry paths, plus deploy-time asset-readiness checks.
- **Quality system**: `frontend/tests/`, Python integration tests, and potential new CI checks for visual/a11y/performance budgets.
- **Documentation and operations**: migration/runbook docs, architecture maps, and release governance docs aligned to modernization milestones.
- **Dependencies/tooling**: likely introduction of additional frontend QA tooling in dev workflow (subject to design artifact decisions).
- **Scope boundary note**: this change explicitly excludes `/tables`, `/excel-query`, `/query-tool`, `/mid-section-defect`; those routes are planned for a follow-up modernization change.

View File

@@ -0,0 +1,25 @@
## ADDED Requirements
### Requirement: In-scope frontend assets SHALL be release-ready before deployment
In-scope routes SHALL rely on build/deploy readiness guarantees instead of runtime fallback behavior as the primary resilience mechanism.
#### Scenario: Build-readiness enforcement
- **WHEN** release artifacts are prepared for deployment
- **THEN** in-scope route assets SHALL be validated for presence and loadability
- **THEN** missing required in-scope assets SHALL fail the release gate
### Requirement: Runtime fallback retirement SHALL follow a governed phase policy
Runtime fallback behavior for in-scope modernization routes SHALL be retired under explicit governance milestones.
#### Scenario: Fallback retirement in phase scope
- **WHEN** a route is marked in-scope for fallback retirement
- **THEN** runtime fallback behavior for that route SHALL be removed or disabled by policy
- **THEN** reliability for that route SHALL be guaranteed by release-time readiness gates
### Requirement: Deferred routes SHALL keep existing fallback posture in this phase
Routes deferred from this modernization phase SHALL retain their existing fallback posture until handled by a follow-up change.
#### Scenario: Deferred fallback continuity
- **WHEN** `/tables`, `/excel-query`, `/query-tool`, or `/mid-section-defect` is evaluated in this phase
- **THEN** fallback retirement SHALL NOT be required for phase completion
- **THEN** fallback retirement decisions for those routes SHALL be addressed in a follow-up modernization change

View File

@@ -0,0 +1,26 @@
## ADDED Requirements
### Requirement: Frontend modernization scope SHALL be explicitly governed
The modernization program SHALL define an explicit in-scope and out-of-scope route matrix for each phase, and SHALL treat that matrix as a release-governed contract artifact.
#### Scenario: Scope matrix publication
- **WHEN** a modernization phase is created
- **THEN** the phase SHALL publish an explicit in-scope route list and out-of-scope route list
- **THEN** the matrix SHALL include `/admin/pages` and `/admin/performance` in scope for this phase
- **THEN** the matrix SHALL mark `/tables`, `/excel-query`, `/query-tool`, and `/mid-section-defect` as deferred routes for a follow-up phase
#### Scenario: Scope drift prevention
- **WHEN** implementation tasks are derived from the phase specs
- **THEN** tasks targeting routes outside the in-scope matrix SHALL be rejected for this phase
### Requirement: Modernization phases SHALL define completion and deprecation milestones
Each modernization phase SHALL define measurable completion criteria and deprecation milestones for legacy-era patterns.
#### Scenario: Phase completion criteria
- **WHEN** a phase reaches release review
- **THEN** it SHALL provide objective completion criteria for route governance, style governance, and quality gates
- **THEN** it SHALL identify any remaining deferred routes and their next-phase linkage
#### Scenario: Legacy deprecation milestones
- **WHEN** legacy fallback or legacy style exceptions remain in phase scope
- **THEN** the phase SHALL define a dated milestone or release gate to remove those exceptions

View File

@@ -0,0 +1,25 @@
## ADDED Requirements
### Requirement: Modernization releases SHALL pass multi-dimensional frontend quality gates
In-scope modernization releases SHALL pass functional, visual, accessibility, and performance gates before promotion.
#### Scenario: Gate bundle at release candidate
- **WHEN** a release candidate includes in-scope modernization changes
- **THEN** it SHALL execute functional behavior parity checks for affected routes
- **THEN** it SHALL execute critical-state visual regression checks for affected routes
- **THEN** it SHALL execute accessibility checks for keyboard and reduced-motion behavior
- **THEN** it SHALL execute performance budget checks for defined shell/route thresholds
### Requirement: Gate failures SHALL block release promotion
Blocking quality gates SHALL prevent release promotion for in-scope modernization changes.
#### Scenario: Blocking gate failure
- **WHEN** any mandatory modernization quality gate fails
- **THEN** release promotion SHALL be blocked until the failure is resolved or explicitly waived per governance policy
### Requirement: Deferred routes SHALL be excluded from this phase gate baseline
The route baseline for this modernization phase SHALL exclude deferred routes.
#### Scenario: Deferred route baseline exclusion
- **WHEN** gate baseline is computed for this phase
- **THEN** `/tables`, `/excel-query`, `/query-tool`, and `/mid-section-defect` SHALL be excluded from mandatory modernization gate coverage

View File

@@ -0,0 +1,24 @@
## MODIFIED Requirements
### Requirement: Major Pages SHALL be Managed by Vite Modules
The system SHALL provide Vite-managed module entries for all in-scope modernization routes under shell-first governance, including admin surfaces `/admin/pages` and `/admin/performance` as governed targets. Deferred routes (`/tables`, `/excel-query`, `/query-tool`, `/mid-section-defect`) are excluded from this phase's required module-governance completeness.
#### Scenario: In-scope module governance completeness
- **WHEN** modernization route coverage is validated for this phase
- **THEN** every in-scope route SHALL have deterministic module-governance metadata and ownership mapping
#### Scenario: Deferred route exclusion in this phase
- **WHEN** completeness validation executes for this phase
- **THEN** deferred routes SHALL be excluded from mandatory pass criteria
### Requirement: Build Pipeline SHALL Produce Backend-Served Assets
Vite build output for in-scope modernization routes MUST be emitted into backend static paths and validated at release time. Missing required in-scope assets SHALL fail release gates instead of relying on runtime fallback behavior.
#### Scenario: Build artifact readiness for in-scope routes
- **WHEN** frontend build is executed for release
- **THEN** required in-scope route artifacts SHALL be present in configured backend static dist paths
- **THEN** missing required artifacts SHALL fail readiness checks
#### Scenario: Deferred route fallback posture unchanged in this phase
- **WHEN** deferred routes are evaluated in this phase
- **THEN** existing fallback posture SHALL not block this phase's completion

View File

@@ -0,0 +1,54 @@
## ADDED Requirements
### Requirement: In-scope page-content modernization SHALL be contract-first
Before chart/filter/page interaction refactors are cut over, each in-scope route SHALL define a contract baseline that captures data and interaction semantics.
#### Scenario: Route contract baseline defined
- **WHEN** an in-scope route is selected for chart/filter modernization
- **THEN** the route SHALL define filter input semantics, query payload expectations, and chart data-shape contracts
- **THEN** the route SHALL define critical state expectations for loading, empty, error, and success interactions
### Requirement: Cutover SHALL require parity evidence against baseline behavior
In-scope chart/filter modernization cutover SHALL require parity evidence against baseline fixtures and critical interaction flows.
#### Scenario: Parity gate before default switch
- **WHEN** a route is proposed for defaulting to a modernized chart/filter implementation
- **THEN** golden fixture parity checks SHALL pass for defined critical states
- **THEN** interaction parity checks SHALL pass for filter apply/reset and chart selection/drill behaviors
### Requirement: Route-level content cutover SHALL be reversible
Modernized chart/filter content rollouts SHALL use reversible controls that allow immediate rollback without reverting unrelated shell architecture work.
#### Scenario: Controlled rollout and rollback
- **WHEN** a modernized route is enabled for users
- **THEN** the route SHALL be controlled by route-scoped feature flag or equivalent switch
- **THEN** rollback procedure SHALL be documented and executable within one release cycle
### Requirement: Page-content modernization progression SHALL require manual route acceptance
In-scope chart/filter/page-content migration SHALL progress one route at a time with explicit manual acceptance records.
#### Scenario: Route-by-route manual acceptance gate
- **WHEN** an in-scope route completes modernization implementation and parity checks
- **THEN** that route SHALL be manually accepted using a defined checklist covering filter flows, chart interactions, empty/error behavior, and visual correctness
- **THEN** the next route SHALL NOT begin cutover until manual acceptance for the current route is signed off
### Requirement: Known legacy bugs in migrated scope SHALL NOT be carried into modernized routes
Modernized route acceptance SHALL include explicit revalidation of known legacy defects in migrated scope, and reproduced defects SHALL block sign-off.
#### Scenario: Route-level legacy bug baseline and replay
- **WHEN** an in-scope route enters chart/filter/page-content modernization
- **THEN** a route-level known-bug baseline (within migrated scope) SHALL be recorded before implementation
- **THEN** manual acceptance SHALL replay those known-bug checks on the modernized route
#### Scenario: Legacy bug carry-over is blocked
- **WHEN** manual acceptance finds that a known legacy bug is still reproducible in the modernized route
- **THEN** route sign-off SHALL fail
- **THEN** route cutover completion and legacy code retirement SHALL be blocked until the bug is fixed
### Requirement: Legacy content path retirement SHALL require parity and manual acceptance
Legacy chart/filter implementations SHALL be removed only after parity checks and manual acceptance criteria are satisfied.
#### Scenario: Legacy removal approval
- **WHEN** legacy chart/filter code is planned for removal on an in-scope route
- **THEN** the route SHALL provide parity pass evidence and manual acceptance sign-off records
- **THEN** unresolved parity failures or manual acceptance defects SHALL block legacy removal

View File

@@ -0,0 +1,28 @@
## MODIFIED Requirements
### Requirement: Portal SHALL provide a SPA shell driven by Vue Router
The portal frontend SHALL use a single SPA shell entry and Vue Router to render in-scope page modules without iframe embedding. In-scope routes for this phase SHALL include the governed report routes and admin surfaces `/admin/pages` and `/admin/performance`, while deferred routes (`/tables`, `/excel-query`, `/query-tool`, `/mid-section-defect`) are explicitly excluded from this phase contract.
#### Scenario: In-scope route renders through shell governance
- **WHEN** a user navigates to an in-scope shell-governed route
- **THEN** the route SHALL resolve through Vue Router with shell contract metadata
- **THEN** the shell SHALL render the corresponding module/target without iframe fallback
#### Scenario: Admin route appears as governed target
- **WHEN** an admin user opens shell navigation
- **THEN** `/admin/pages` and `/admin/performance` SHALL be exposed as governed navigation targets per access policy
#### Scenario: Deferred route is excluded from this phase route-governance requirement
- **WHEN** phase-level shell-governance compliance is evaluated
- **THEN** `/tables`, `/excel-query`, `/query-tool`, and `/mid-section-defect` SHALL be treated as deferred and excluded from pass/fail criteria for this phase
### Requirement: Existing route contracts SHALL remain stable in SPA mode
Migration to the shell-first SPA model SHALL preserve route/query compatibility for in-scope routes while introducing canonical shell routing policy and explicit compatibility handling.
#### Scenario: Canonical shell path behavior for in-scope routes
- **WHEN** a user opens an in-scope report route via canonical shell path
- **THEN** route behavior and query semantics SHALL remain compatible with established baseline behavior
#### Scenario: Compatibility policy for direct route entry
- **WHEN** a user opens an in-scope report route via direct non-canonical entry
- **THEN** the system SHALL apply explicit compatibility policy (preserve behavior or compatibility redirect) without breaking route semantics

View File

@@ -0,0 +1,25 @@
## ADDED Requirements
### Requirement: In-scope pages SHALL enforce style isolation boundaries
In-scope modernization pages SHALL avoid page-global selectors for page-local concerns and SHALL keep style concerns scoped to route-level containers or shared design-system layers.
#### Scenario: Global selector control
- **WHEN** style governance checks analyze in-scope page styles
- **THEN** page-local style changes SHALL NOT introduce new `:root` or `body` rules for route-local presentation concerns
- **THEN** shared cross-route concerns SHALL be authored in designated shared style layers
### Requirement: In-scope shared semantics SHALL be token-first
Shared UI semantics in in-scope routes SHALL be implemented with token-backed Tailwind/shared-style primitives before page-local overrides are allowed.
#### Scenario: Token-first UI pattern adoption
- **WHEN** an in-scope route introduces or updates shared UI semantics (layout shell, card, filter, action, status)
- **THEN** the route SHALL consume token-backed shared primitives
- **THEN** page-local hard-coded visual values SHALL require explicit exception justification
### Requirement: Legacy style exceptions SHALL be tracked and sunset
Legacy CSS exceptions for in-scope routes SHALL be tracked with ownership and removal milestones.
#### Scenario: Exception registry requirement
- **WHEN** an in-scope route cannot yet remove legacy style behavior
- **THEN** the route SHALL be registered with an exception owner and planned removal milestone
- **THEN** unresolved exceptions past milestone SHALL fail modernization governance review

View File

@@ -0,0 +1,24 @@
## MODIFIED Requirements
### Requirement: Frontend styles SHALL be governed by Tailwind design tokens
The frontend SHALL enforce a token-governed style system for in-scope routes. Shared visual semantics SHALL be expressed through token-backed Tailwind/shared layers, and ad-hoc page-local hard-coded values for shared semantics SHALL require explicit exception governance.
#### Scenario: Shared token usage across in-scope modules
- **WHEN** two in-scope modules render equivalent UI semantics (e.g., card, filter chip, primary action, status indicator)
- **THEN** they SHALL use the same token-backed style semantics
- **THEN** visual output SHALL remain consistent across those modules
#### Scenario: Token governance review
- **WHEN** an in-scope route introduces new shared UI styling
- **THEN** the styling SHALL map to shared tokens/layers or be recorded in an approved exception registry
### Requirement: Tailwind migration SHALL support coexistence with legacy CSS
Tailwind migration SHALL support controlled coexistence only as a transition state for this phase. In-scope routes SHALL move toward isolation-first style ownership and SHALL NOT introduce new page-global CSS side effects for route-local concerns.
#### Scenario: In-scope global selector control
- **WHEN** in-scope route styles are reviewed
- **THEN** new route-local styling SHALL NOT introduce page-global selectors (`:root`, `body`) for local presentation behavior
#### Scenario: Deferred route coexistence allowance
- **WHEN** deferred routes (`/tables`, `/excel-query`, `/query-tool`, `/mid-section-defect`) are evaluated during this phase
- **THEN** existing coexistence posture SHALL be allowed and handled by a follow-up modernization change

View File

@@ -0,0 +1,29 @@
## ADDED Requirements
### Requirement: In-scope routes SHALL be shell-contract governed
All in-scope modernization routes SHALL be represented in shell route contracts, loader registration policy, and navigation visibility governance.
#### Scenario: In-scope coverage validation
- **WHEN** shell route contract validation is executed
- **THEN** every in-scope route SHALL have route metadata, ownership metadata, and visibility policy metadata
- **THEN** missing in-scope route contracts SHALL fail validation
#### Scenario: Admin route inclusion
- **WHEN** shell navigation is built for admin users
- **THEN** `/admin/pages` and `/admin/performance` SHALL be represented as governed navigation targets according to visibility/access policy
### Requirement: Out-of-scope routes SHALL not block this phase
Routes explicitly marked as out-of-scope for this modernization phase SHALL be excluded from required shell-coverage gates in this phase.
#### Scenario: Deferred route exclusion
- **WHEN** modernization gates execute for this phase
- **THEN** `/tables`, `/excel-query`, `/query-tool`, and `/mid-section-defect` SHALL be treated as deferred routes
- **THEN** deferred route absence from new shell-governance gates SHALL NOT fail this phase
### Requirement: Route coverage governance SHALL be CI-enforced
Route coverage and contract completeness checks for in-scope routes SHALL run as CI gates.
#### Scenario: CI gate failure on in-scope gap
- **WHEN** CI detects an in-scope route without required contract metadata
- **THEN** the modernization gate SHALL fail
- **THEN** release promotion SHALL be blocked until resolved

View File

@@ -0,0 +1,20 @@
## MODIFIED Requirements
### Requirement: Pure Vite pages SHALL be served as static HTML
The system SHALL serve in-scope pure Vite pages through backend static HTML delivery under a shell-first canonical routing policy. Direct-entry compatibility for in-scope routes SHALL be explicit and governed. Admin targets `/admin/pages` and `/admin/performance` SHALL be represented as governed shell navigation targets, while maintaining backend auth/session authority.
#### Scenario: In-scope canonical shell entry
- **WHEN** a user navigates to an in-scope canonical shell route
- **THEN** the shell SHALL render the target route via governed route contracts and static asset delivery
#### Scenario: Direct-entry compatibility policy for in-scope routes
- **WHEN** a user opens an in-scope route through direct non-canonical entry
- **THEN** the system SHALL apply explicit compatibility behavior without breaking established query semantics
#### Scenario: Admin targets in shell governance
- **WHEN** shell navigation is rendered for an authorized admin user
- **THEN** `/admin/pages` and `/admin/performance` SHALL be reachable through governed admin navigation targets
#### Scenario: Deferred routes excluded from this phase architecture criteria
- **WHEN** this phase architecture compliance is evaluated
- **THEN** `/tables`, `/excel-query`, `/query-tool`, and `/mid-section-defect` SHALL be excluded and handled in a follow-up change

View File

@@ -0,0 +1,78 @@
## 1. Program Governance Baseline
- [x] 1.1 Publish a frozen in-scope/out-of-scope route matrix for this phase (include `/admin/pages`, `/admin/performance`; exclude `/tables`, `/excel-query`, `/query-tool`, `/mid-section-defect`)
- [x] 1.2 Add modernization governance documentation for phase completion criteria and legacy deprecation milestones
- [x] 1.3 Create an exception registry format for temporary route/style exceptions (owner + milestone)
## 2. Shell Route Contract Expansion (In Scope)
- [x] 2.1 Extend shell route contract metadata to cover all in-scope routes for this phase
- [x] 2.2 Add governed navigation target definitions for `/admin/pages` and `/admin/performance`
- [x] 2.3 Ensure route visibility/access policy metadata is validated for in-scope routes
- [x] 2.4 Add CI contract checks that fail on missing in-scope route metadata
## 3. Canonical Routing and Compatibility Policy
- [x] 3.1 Define canonical shell entry behavior for in-scope report routes
- [x] 3.2 Implement explicit compatibility policy for direct non-canonical route entry
- [x] 3.3 Verify query-semantics compatibility for in-scope routes under canonical/compatibility paths
## 4. Admin Surface Modernization Integration
- [x] 4.1 Integrate `/admin/pages` and `/admin/performance` into shell-governed navigation flow
- [x] 4.2 Preserve backend auth/session authority while modernizing shell navigation governance
- [x] 4.3 Add admin visibility/access behavior tests for shell-governed admin targets
## 5. Style Isolation and Token Enforcement
- [x] 5.1 Inventory in-scope route styles for page-global selector usage (`:root`, `body`) and classify required refactors
- [x] 5.2 Refactor in-scope route-local styles to scoped/container-based ownership
- [x] 5.3 Move shared visual semantics to token-backed Tailwind/shared layers
- [x] 5.4 Add style-governance lint/check rules for in-scope routes and exception handling
## 6. Page-Content Modernization Safety (Charts/Filters/Interactions)
- [x] 6.1 Define route-level content contracts for in-scope pages (filter input semantics, query payload structure, chart data shape, state transitions)
- [x] 6.2 Build golden fixtures and parity assertions for chart/filter critical states before cutover
- [x] 6.3 Add interaction parity checks for critical flows (filter apply/reset, chart drill/selection, empty/error states)
- [x] 6.4 Add route-scoped feature flags and immediate rollback controls for content cutover
- [x] 6.5 Define a per-route manual acceptance checklist for chart/filter/page-content migration
- [x] 6.6 Require manual acceptance sign-off for each route before moving to the next route
- [x] 6.7 Require parity pass + manual acceptance sign-off before legacy content path retirement
- [x] 6.8 Create route-level known-bug baselines for migrated scope before implementation begins
- [x] 6.9 Add mandatory "BUG revalidation during migration" checklist items to manual acceptance for each route
- [x] 6.10 Block route sign-off and legacy retirement if known legacy bugs are reproduced in the modernized route
## 7. Asset Readiness and Fallback Retirement (In Scope)
- [x] 7.1 Define required in-scope asset readiness checks for build/release pipeline
- [x] 7.2 Enforce fail-fast release behavior when required in-scope assets are missing
- [x] 7.3 Retire runtime fallback posture for in-scope routes per governance milestones
- [x] 7.4 Keep deferred route fallback posture unchanged in this phase and document follow-up linkage
## 8. Frontend Quality Gate Modernization
- [x] 8.1 Define mandatory functional parity checks for in-scope modernization routes
- [x] 8.2 Define visual regression checkpoints for critical states in in-scope routes
- [x] 8.3 Define accessibility checks (keyboard flows, aria semantics, reduced-motion behavior)
- [x] 8.4 Define performance budgets and measurement points for shell/route behavior
- [x] 8.5 Configure gate severity policy (warn mode rollout -> blocking mode promotion)
## 9. Test and CI Wiring
- [x] 9.1 Extend frontend test suite for new route governance/admin scenarios
- [x] 9.2 Extend backend/integration tests for canonical routing and compatibility behavior
- [x] 9.3 Add CI jobs for route-governance completeness, quality-gate execution, and asset readiness checks
- [x] 9.4 Ensure deferred routes are excluded from this phase blocking criteria
## 10. Migration and Rollback Runbook
- [x] 10.1 Update runbook with phased rollout steps and hold points for modernization gates
- [x] 10.2 Document rollback controls for gate false positives and route-level reversion
- [x] 10.3 Add operational observability checkpoints for route governance and gate outcomes
## 11. Follow-up Change Preparation (Deferred Routes)
- [x] 11.1 Create a linked follow-up modernization change for `/tables`, `/excel-query`, `/query-tool`, `/mid-section-defect`
- [x] 11.2 Transfer deferred-route requirements and acceptance criteria into the follow-up change
- [x] 11.3 Record explicit handoff from this phase to the deferred-route phase

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-02-12

View File

@@ -0,0 +1,108 @@
## Context
Phase 1 modernization was completed and archived, but post-delivery review identified several hardening gaps across policy loading, fallback behavior consistency, feature-flag ergonomics, and route-governance drift detection. The gaps are cross-cutting: backend route hosts, frontend contract metadata, CI governance checks, and operator onboarding defaults.
Current risks include:
- shared mutable cached policy payloads via `lru_cache` return values,
- inconsistent retired-fallback 503 response surfaces between app routes and blueprint routes,
- local bootstrap failures caused by strict `.env.example` defaults,
- duplicated boolean feature-flag parsing with slightly different precedence logic,
- missing frontend/backend route-inventory cross-validation.
## Goals / Non-Goals
**Goals:**
- Make modernization policy loading deterministic, mutation-safe, and explicitly documented.
- Standardize retired-fallback error response behavior across all in-scope route hosts.
- Keep `.env.example` local-safe while documenting production hardening expectations.
- Centralize feature-flag resolution semantics in shared helpers.
- Enforce route-contract parity across backend artifacts and frontend shell contracts.
- Improve operational observability for legacy contract fallback loading.
**Non-Goals:**
- No new route migrations in this change.
- No redesign of shell navigation UX.
- No deferred-route modernization implementation (`/tables`, `/excel-query`, `/query-tool`, `/mid-section-defect`).
- No runtime hot-reload framework for all policy artifacts.
## Decisions
### Decision 1: Introduce shared feature-flag helpers for bool parsing and env/config precedence
- Choice: Add shared helpers (for example `parse_bool`, `resolve_bool_flag`) used by app/policy/runtime modules.
- Rationale: eliminates duplicated parsing and precedence drift.
- Alternative considered: keep local `_to_bool` and alignment-by-convention.
- Why not alternative: high regression risk from future divergence and incomplete audits.
### Decision 2: Protect cached policy payloads from cross-caller mutation
- Choice: keep internal cache, but return defensive copies to callers; document refresh semantics and expose explicit cache-clear helper for tests/controlled refresh points.
- Rationale: avoids shared-reference corruption without changing call sites.
- Alternative considered: return `MappingProxyType`.
- Why not alternative: nested list/dict payloads still remain mutable unless deeply frozen.
### Decision 3: Unify retired-fallback response generation
- Choice: move to a shared fallback-retirement response helper callable from both app-level and blueprint-level routes.
- Rationale: consistent status/template/body contract and easier testing.
- Alternative considered: leave blueprint-specific inline HTML responses.
- Why not alternative: inconsistent user/operator behavior and duplicated logic.
### Decision 4: Rebalance `.env.example` for safe local onboarding
- Choice: set strict modernization toggles to local-safe defaults and annotate production-recommended values inline.
- Rationale: avoid false-negative startup failures in local/test environments while preserving explicit production guidance.
- Alternative considered: keep strict defaults and require all local users to override manually.
- Why not alternative: unnecessary onboarding friction and frequent bootstrap failures.
### Decision 5: Add governance parity checks across frontend and backend route contracts
- Choice: extend governance checks/tests to compare backend route contract artifacts with frontend route inventory/scope metadata.
- Rationale: catches silent drift before release.
- Alternative considered: rely only on backend JSON consistency.
- Why not alternative: frontend contract drift can still break runtime behavior silently.
### Decision 6: Emit explicit warning when legacy contract source is used
- Choice: log warning when loader falls back from primary contract artifact to legacy path.
- Rationale: improves observability during migration tail.
- Alternative considered: silent fallback.
- Why not alternative: hard to detect stale-source dependency in production.
### Decision 7: Reduce unnecessary redirect hops in `/hold-detail` missing-reason flow
- Choice: when SPA shell mode is enabled, redirect directly to canonical shell overview path.
- Rationale: reduces redirect chain complexity and improves deterministic route tracing.
- Alternative considered: keep current two-hop behavior.
- Why not alternative: no benefit, adds trace/debug noise.
### Decision 8: Add token fallbacks for shell-dependent route styles
- Choice: where route-local CSS consumes shell variables, include fallback values in `var(--token, fallback)` form.
- Rationale: prevents degraded rendering when route is rendered outside shell variable scope.
- Alternative considered: assume shell-only render path.
- Why not alternative: fallback/compatibility entry paths still exist in this phase.
## Risks / Trade-offs
- [Risk] Shared helper refactor may alter existing truthy/falsey behavior in edge env values.
- Mitigation: add unit tests covering canonical and malformed env values before replacing call sites.
- [Risk] Contract parity gate can fail current CI if artifacts are already drifted.
- Mitigation: land parity test with synchronized artifacts in same change.
- [Risk] Defensive-copy strategy adds minor per-call overhead.
- Mitigation: policy payloads are small and low-frequency; prioritize correctness over micro-optimization.
- [Risk] `.env.example` default changes may be interpreted as weaker production stance.
- Mitigation: add explicit production recommendation comments next to each local-safe default.
## Migration Plan
1. Add shared feature-flag helpers and migrate existing bool parsing call sites.
2. Refactor modernization policy cache-return behavior to mutation-safe contract and document refresh semantics.
3. Introduce shared retired-fallback response helper and migrate hold-overview/hold-history/hold-detail route handlers.
4. Update `.env.example` defaults and production guidance comments.
5. Extend governance script/tests for frontend/backend route-contract parity.
6. Add warning log on legacy contract-source fallback.
7. Update `/hold-detail` missing-reason redirect to single-hop canonical target under SPA mode.
8. Add fallback values for QC-GATE shell-derived CSS variables.
9. Run targeted unit/integration/e2e + governance checks.
Rollback strategy:
- Changes are config/code-level and can be reverted by standard git rollback.
- If parity gate causes unexpected release blocking, gate can temporarily run in warning mode while drift is fixed in same release window.
## Open Questions
- Should policy cache refresh be strictly restart-based, or do we want an operator-triggered cache-clear hook in production later?
- Do we want a single centralized governance artifact as source-of-truth long-term, with generated frontend/backend contract outputs?

View File

@@ -0,0 +1,34 @@
## Why
Recent code review on the Phase 1 modernization delivery found several high-risk consistency gaps: mutable cached policy payloads, inconsistent fallback-retirement error behavior, aggressive `.env.example` defaults, and route-governance drift risks between backend JSON and frontend contract definitions. These issues can cause environment-dependent startup failures, silent policy drift, and hard-to-debug runtime behavior.
## What Changes
- Harden modernization policy loaders to prevent shared mutable cache corruption and make refresh semantics explicit/documented.
- Unify in-scope fallback-retirement response behavior across app-level routes and blueprint routes.
- Make `.env.example` safe for local onboarding while still documenting production-recommended hardening values.
- Consolidate feature-flag boolean resolution (`env > config > default`) into a shared helper and remove duplicated `_to_bool` implementations.
- Add route-contract cross-validation between backend contract artifacts and frontend `routeContracts.js` inventory.
- Add explicit warning telemetry when legacy shell-contract artifact fallback is used.
- Document and test intentional canonical-redirect scope asymmetry (report routes vs admin external targets).
- Reduce avoidable redirect chain length for `/hold-detail` missing-reason flow in SPA mode.
- Add shell-token fallback values for QC-GATE page CSS variables to prevent rendering degradation outside shell context.
## Capabilities
### New Capabilities
- None.
### Modified Capabilities
- `asset-readiness-and-fallback-retirement`: unify fallback-retirement failure surfaces and safe local defaults for readiness-related flags.
- `frontend-platform-modernization-governance`: add governance requirements for contract-source consistency and legacy artifact fallback observability.
- `spa-shell-navigation`: document and enforce canonical redirect scope rules, including missing-reason redirect behavior.
- `unified-shell-route-coverage`: require frontend/backend route-contract set consistency checks.
- `style-isolation-and-token-enforcement`: require token fallback behavior for route-local styles that may render outside shell variable scope.
- `maintainability-type-and-constant-hygiene`: require shared feature-flag and boolean parsing helpers for policy/runtime modules.
## Impact
- Affected backend modules: `src/mes_dashboard/core/modernization_policy.py`, `src/mes_dashboard/core/runtime_contract.py`, `src/mes_dashboard/app.py`, hold-related routes.
- Affected frontend modules: `frontend/src/portal-shell/routeContracts.js`, `frontend/src/qc-gate/style.css`.
- Affected governance and tests: modernization gate script/tests, route-contract consistency tests, redirect/fallback behavior tests, `.env.example` documentation.

View File

@@ -0,0 +1,12 @@
## ADDED Requirements
### Requirement: Fallback-retirement failure response SHALL be consistent across route hosts
When in-scope runtime fallback retirement is enabled and route assets are unavailable, app-level and blueprint-level route handlers SHALL return a consistent retired-fallback response surface.
#### Scenario: App-level in-scope route enters retired fallback state
- **WHEN** an in-scope app-level route cannot serve required dist assets and fallback retirement is enabled
- **THEN** the route SHALL return the standardized retired-fallback response contract
#### Scenario: Blueprint-level in-scope route enters retired fallback state
- **WHEN** an in-scope blueprint-level route cannot serve required dist assets and fallback retirement is enabled
- **THEN** the route SHALL return the same standardized retired-fallback response contract used by app-level routes

View File

@@ -0,0 +1,19 @@
## ADDED Requirements
### Requirement: Operator-facing environment defaults SHALL be onboarding-safe
`.env.example` SHALL prioritize local onboarding safety while clearly documenting production hardening recommendations for modernization controls.
#### Scenario: Local bootstrap from `.env.example`
- **WHEN** a developer initializes `.env` from `.env.example` in a local non-production environment
- **THEN** startup-critical modernization flags SHALL default to onboarding-safe values that do not fail boot solely because dist readiness gates are strict by default
#### Scenario: Production recommendation visibility
- **WHEN** operators review `.env.example` for deployment configuration
- **THEN** production-recommended values for shell-first and modernization-hardening flags SHALL be explicitly documented in adjacent comments
### Requirement: Policy cache refresh model SHALL be explicit in governance docs
Governance-owned policy artifacts that are loaded with in-process caching SHALL document runtime refresh behavior and operator expectations.
#### Scenario: Cached policy artifact behavior documentation
- **WHEN** maintainers read modernization governance artifacts
- **THEN** they SHALL find explicit guidance on whether policy JSON updates require process restart, cache clear, or automatic reload

View File

@@ -0,0 +1,19 @@
## ADDED Requirements
### Requirement: Feature-flag resolution SHALL use shared helper semantics
Environment/config/default feature-flag resolution logic SHALL be implemented through shared helper utilities instead of duplicated per-module parsing.
#### Scenario: Feature-flag evaluation in app and policy modules
- **WHEN** modules resolve boolean feature flags from environment variables and Flask config
- **THEN** they SHALL use a shared helper that enforces consistent precedence and truthy/falsey parsing behavior
### Requirement: Cached policy payloads SHALL protect against shared mutable-state corruption
Policy loader functions that cache JSON payloads in-process SHALL prevent downstream callers from mutating the shared cached object reference.
#### Scenario: Cached policy payload consumed by multiple callers
- **WHEN** multiple callers read cached policy payloads during process lifetime
- **THEN** one caller's accidental mutation SHALL NOT alter another caller's observed policy state through shared reference side effects
#### Scenario: Policy cache behavior documentation
- **WHEN** maintainers inspect cached policy loader code
- **THEN** they SHALL find explicit comments describing refresh/invalidation behavior expectations

View File

@@ -0,0 +1,20 @@
## ADDED Requirements
### Requirement: Canonical redirect scope boundaries SHALL be explicit and intentional
Canonical shell direct-entry redirects SHALL apply only to governed in-scope report routes and SHALL explicitly exclude admin external targets with documented rationale.
#### Scenario: In-scope report route direct entry
- **WHEN** SPA shell mode is enabled and a user enters an in-scope report route directly
- **THEN** the system SHALL redirect to the canonical `/portal-shell/...` route while preserving query semantics
#### Scenario: Admin external target direct entry
- **WHEN** SPA shell mode is enabled and a user enters `/admin/pages` or `/admin/performance` directly
- **THEN** the system SHALL NOT apply report-route canonical redirect policy
- **THEN** the exclusion rationale SHALL be documented in code-level comments or governance docs
### Requirement: Missing-required-parameter redirects SHALL avoid avoidable multi-hop chains
Routes with server-side required query parameters SHALL minimize redirect hops under SPA shell mode.
#### Scenario: Hold detail missing reason in SPA shell mode
- **WHEN** a user opens `/hold-detail` without `reason` while SPA shell mode is enabled
- **THEN** the route SHALL resolve via a single-hop redirect to the canonical overview shell path

View File

@@ -0,0 +1,12 @@
## ADDED Requirements
### Requirement: Route-local token usage SHALL include fallback values outside shell scope
Route-level styles that reference shell-provided token variables SHALL define fallback values to preserve rendering correctness when rendered outside shell variable scope.
#### Scenario: Route rendered outside portal shell variable scope
- **WHEN** a route-local stylesheet references shell token variables and the page is rendered without shell-level CSS variables
- **THEN** visual-critical properties (for example header gradients) SHALL still resolve through explicit fallback token values
#### Scenario: Style governance check for unresolved shell variables
- **WHEN** style-governance validation inspects in-scope route styles
- **THEN** unresolved shell-variable references without fallback SHALL be flagged as governance failures or approved exceptions

View File

@@ -0,0 +1,19 @@
## ADDED Requirements
### Requirement: Frontend and backend route-contract inventories SHALL be cross-validated
Route-governance checks SHALL verify that frontend shell route contracts and backend route contract artifacts describe the same governed route set and scope classes.
#### Scenario: Cross-source contract parity gate
- **WHEN** modernization governance checks run in CI
- **THEN** mismatches between backend route contract JSON and frontend `routeContracts.js` route inventory SHALL fail the gate
#### Scenario: Scope classification drift detection
- **WHEN** a route has inconsistent scope classification between frontend and backend contract sources
- **THEN** governance checks SHALL report the specific route and conflicting scope values
### Requirement: Legacy contract-source fallback SHALL emit operational warning
When contract loading falls back from the primary modernization contract artifact to a legacy artifact path, the service SHALL emit explicit warning telemetry.
#### Scenario: Legacy contract fallback path selected
- **WHEN** the primary contract artifact is unavailable and a legacy contract file is loaded
- **THEN** the system SHALL log a warning that includes the selected legacy source path

View File

@@ -0,0 +1,50 @@
## 1. Shared Flag and Policy Helper Hardening
- [x] 1.1 Introduce shared boolean parsing and feature-flag resolution helpers (`env > config > default`) in a common core utility module.
- [x] 1.2 Replace duplicated `_to_bool` / inline bool parsing in `app.py`, `modernization_policy.py`, and `runtime_contract.py` with shared helpers.
- [x] 1.3 Refactor modernization policy cached JSON loaders to prevent shared mutable-state corruption (defensive return strategy + clear cache helper for controlled refresh/testing).
- [x] 1.4 Add inline documentation comments describing policy cache refresh behavior and operator expectations.
## 2. Fallback and Redirect Behavior Consistency
- [x] 2.1 Implement a shared retired-fallback response helper usable by both app-level and blueprint-level route handlers.
- [x] 2.2 Migrate hold-related blueprint routes (`hold-overview`, `hold-history`, `hold-detail`) to the shared retired-fallback response contract.
- [x] 2.3 Update `/hold-detail` missing-`reason` logic to single-hop redirect to canonical shell overview when SPA shell mode is enabled.
- [x] 2.4 Document canonical redirect scope boundaries (report routes only, admin external targets excluded) in policy code comments.
## 3. Environment and Governance Documentation Safety
- [x] 3.1 Update `.env.example` modernization/runtime hardening flags to onboarding-safe defaults for local environments.
- [x] 3.2 Add explicit production-recommended values and rationale comments next to each adjusted flag in `.env.example`.
- [x] 3.3 Update modernization governance docs to clarify policy artifact cache refresh/invalidation model.
## 4. Route Contract Drift Detection and Observability
- [x] 4.1 Extend modernization governance checks to cross-validate backend route contract artifacts against frontend `routeContracts.js` route inventory and scope classifications.
- [x] 4.2 Add/extend tests that fail on frontend-backend route set drift and scope mismatch.
- [x] 4.3 Emit explicit warning logs when shell contract loading falls back to a legacy contract artifact path.
- [x] 4.4 Add test coverage verifying legacy contract fallback warning behavior.
## 5. Style Token Fallback Resilience
- [x] 5.1 Update QC-GATE route-local CSS variables that depend on shell tokens to include fallback values.
- [x] 5.2 Add style-governance check or test assertion that shell-derived route styles include fallback values unless explicitly exempted.
## 6. Test Coverage and Regression Validation
- [x] 6.1 Add redirect compatibility tests for non-ASCII query parameters through canonical redirect flows.
- [x] 6.2 Add explicit test for `/hold-detail` missing-`reason` redirect chain behavior under SPA enabled mode.
- [x] 6.3 Narrow broad `os.path.exists` patches in route/template tests to targeted path-specific behavior where feasible.
- [x] 6.4 Run relevant unit/integration/frontend/e2e governance test suites and record pass criteria for this hardening change.
## Validation Record
- `pytest -q tests/test_feature_flags.py tests/test_modernization_policy_hardening.py tests/test_asset_readiness_policy.py tests/test_hold_routes.py tests/test_portal_shell_routes.py tests/test_full_modernization_gates.py tests/test_template_integration.py``84 passed`
- `pytest -q tests/test_hold_overview_routes.py tests/test_hold_history_routes.py``34 passed`
- `pytest -q tests/test_wip_routes.py``27 passed`
- `pytest -q tests/test_runtime_contract.py tests/test_runtime_hardening.py -k "runtime_contract or runtime"``10 passed`
- `pytest -q tests/test_wip_hold_pages_integration.py``3 passed`
- `npm --prefix frontend test``64 passed`
- `python scripts/check_full_modernization_gates.py --mode block``[OK] modernization gates passed`
- `E2E_BASE_URL=http://127.0.0.1:8091 pytest -q tests/e2e/test_wip_hold_pages_e2e.py -k "hold_detail_without_reason_redirects_to_overview" --run-e2e``1 passed`
- `STRESS_TEST_URL=http://127.0.0.1:8091 STRESS_CONCURRENT_USERS=2 STRESS_REQUESTS_PER_USER=3 pytest -q tests/stress/test_api_load.py -k "hold_detail_lots_concurrent_load"``1 passed`

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-02-12

View File

@@ -0,0 +1,60 @@
## Context
This follow-up change consumes the explicit handoff from `full-modernization-architecture-blueprint` phase 1. Deferred routes were intentionally excluded to control blast radius, but they now represent the remaining architecture gap.
Current deferred-route risks:
- Shell contract incompleteness and mixed navigation behavior.
- Legacy runtime fallback dependency.
- Content modernization parity not yet formalized.
- Potential legacy bug carry-over if migration is done implementation-first.
## Goals / Non-Goals
**Goals**
- Complete shell-governed route coverage for deferred routes.
- Adopt canonical shell routing and explicit compatibility policy.
- Execute contract-first content modernization with parity and rollback controls.
- Enforce mandatory manual acceptance and BUG revalidation before sign-off.
**Non-Goals**
- Reworking already in-scope phase-1 routes again.
- Changing backend business data semantics beyond compatibility safeguards.
- Bundling unrelated admin/report features into this follow-up.
## Decisions
### D1. Deferred routes are promoted to in-scope as a single governed wave
- `/tables`, `/excel-query`, `/query-tool`, `/mid-section-defect` are all in-scope for this change.
### D2. Canonical policy matches phase 1
- Canonical shell path is `/portal-shell/<route>`.
- Direct entry behavior remains explicit compatibility (redirect or compatibility render) with preserved query semantics.
### D3. Content migration remains route-by-route, not big-bang
- Only one deferred route can be in cutover state at a time.
- Next route is blocked until current route has parity pass + manual sign-off + bug replay pass.
### D4. Legacy bug carry-over prevention is a hard gate
- Known bug baseline must be recorded before implementation.
- Reproduced known bugs on modernized path block sign-off and legacy retirement.
## Risks / Trade-offs
- Deferred routes may have heavier legacy coupling than phase-1 routes.
- Route-by-route cutover increases total elapsed time but reduces rollback blast radius.
- Asset-readiness enforcement can block releases earlier; rollout plan must phase warn->block.
## Migration Plan
1. Freeze deferred-route scope matrix for this follow-up change.
2. Extend shell route contracts and metadata coverage to full completeness.
3. Define route content contracts + golden fixtures + interaction parity checks.
4. Execute per-route migration with feature-flagged cutover and manual acceptance.
5. Retire deferred-route runtime fallback posture after acceptance and readiness gates pass.
6. Update runbook/rollback docs and close handoff linkage.
## Rollback Strategy
- Keep route-scoped cutover flags for immediate per-route rollback.
- Allow temporary gate downgrade (`block` -> `warn`) only with explicit waiver and expiry.
- Preserve route-level evidence (parity, manual sign-off, bug replay) to support rollback decisions.

View File

@@ -0,0 +1,33 @@
## Why
`full-modernization-architecture-blueprint` intentionally deferred these routes:
- `/tables`
- `/excel-query`
- `/query-tool`
- `/mid-section-defect`
Those routes still run on legacy posture (direct-entry-first + fallback continuity + mixed style ownership). A dedicated follow-up change is required to finish modernization without reopening scope in phase 1.
## What Changes
- Promote all deferred routes to first-class in-scope shell-governed targets.
- Apply canonical shell routing policy and explicit direct-entry compatibility behavior for each deferred route.
- Modernize deferred route page-content flow (filters/charts/interactions) with contract-first parity gates.
- Apply the same mandatory manual acceptance + BUG revalidation blocking policy used in phase 1.
- Move deferred routes from fallback-era runtime posture to asset-readiness + governed retirement posture.
## Capabilities
### Modified Capabilities
- `unified-shell-route-coverage`: deferred routes become in-scope and CI-blocking for route contract completeness.
- `spa-shell-navigation`: deferred routes adopt canonical shell entry policy and governed compatibility behavior.
- `page-content-modernization-safety`: deferred routes require contract baselines, parity evidence, manual sign-off, and known-bug revalidation.
- `asset-readiness-and-fallback-retirement`: deferred routes adopt release-time asset checks and governed fallback retirement milestones.
## Impact
- Frontend route modules for `/tables`, `/excel-query`, `/query-tool`, `/mid-section-defect`.
- Shell contract and navigation governance in `frontend/src/portal-shell/**`.
- Backend route handlers serving deferred routes and compatibility behavior.
- Quality gate artifacts, runbook updates, and rollout/rollback policy for deferred-route cutover.

View File

@@ -0,0 +1,18 @@
## MODIFIED Requirements
### Requirement: Deferred-route assets SHALL be release-ready before promotion
Deferred routes SHALL adopt release-time asset-readiness checks and SHALL fail promotion when required assets are missing.
#### Scenario: Deferred-route readiness validation
- **WHEN** release artifacts are prepared for follow-up phase promotion
- **THEN** required assets for `/tables`, `/excel-query`, `/query-tool`, and `/mid-section-defect` SHALL be validated
- **THEN** missing required assets SHALL fail release gating
### Requirement: Deferred-route runtime fallback SHALL be retired by governed policy
Deferred routes SHALL not remain on runtime fallback posture after follow-up modernization completion criteria are met.
#### Scenario: Deferred-route fallback retirement
- **WHEN** a deferred route passes readiness + parity + manual acceptance gates
- **THEN** runtime fallback posture for that route SHALL be retired according to milestone policy
- **THEN** rollback control SHALL remain available via explicit route-level governance switch

View File

@@ -0,0 +1,26 @@
## MODIFIED Requirements
### Requirement: Deferred-route content modernization SHALL remain contract-first
Before deferred routes are cut over to modernized implementations, each route SHALL define filter/query/data-state contracts and critical interaction expectations.
#### Scenario: Deferred-route contract baseline defined
- **WHEN** `/tables`, `/excel-query`, `/query-tool`, or `/mid-section-defect` enters modernization
- **THEN** a route-level baseline SHALL capture filter input semantics, query payload shape, and critical state expectations
### Requirement: Deferred-route cutover SHALL require parity + manual acceptance
Deferred routes SHALL NOT complete cutover without parity evidence and explicit manual sign-off.
#### Scenario: Parity and sign-off before route progression
- **WHEN** a deferred route reports implementation complete
- **THEN** golden fixture parity checks and interaction parity checks SHALL pass
- **THEN** manual acceptance checklist sign-off SHALL be recorded
- **THEN** next deferred route cutover SHALL be blocked until sign-off is complete
### Requirement: Legacy bug carry-over SHALL be blocked for deferred routes
Known legacy bugs in deferred-route migrated scope SHALL be replayed during acceptance and SHALL block sign-off if reproduced.
#### Scenario: Deferred-route bug replay gate
- **WHEN** deferred-route manual acceptance executes
- **THEN** known-bug replay checks SHALL run
- **THEN** reproduced known bugs SHALL fail route sign-off and block legacy retirement

View File

@@ -0,0 +1,14 @@
## MODIFIED Requirements
### Requirement: Deferred routes SHALL become shell-contract governed in this follow-up phase
All routes deferred by phase 1 (`/tables`, `/excel-query`, `/query-tool`, `/mid-section-defect`) SHALL be represented as in-scope shell contracts with complete ownership and visibility metadata.
#### Scenario: Deferred route contract promotion
- **WHEN** follow-up route coverage validation is executed
- **THEN** each deferred route SHALL have route metadata, owner metadata, and visibility/access policy metadata
- **THEN** missing metadata SHALL fail route governance validation
#### Scenario: CI gate blocks deferred route contract gaps
- **WHEN** CI evaluates route-governance completeness for this follow-up change
- **THEN** any deferred route missing required contract fields SHALL block promotion

View File

@@ -0,0 +1,38 @@
## 1. Scope and Governance Freeze
- [ ] 1.1 Publish frozen in-scope matrix for `/tables`, `/excel-query`, `/query-tool`, `/mid-section-defect`
- [ ] 1.2 Define completion criteria, deprecation milestones, and exception registry updates for deferred-route phase
- [ ] 1.3 Record explicit upstream linkage to `full-modernization-architecture-blueprint` handoff artifacts
## 2. Shell Route Contract Completion
- [ ] 2.1 Promote deferred routes to in-scope in shell route contracts with complete metadata
- [ ] 2.2 Implement governed navigation targets and visibility policy validation for all deferred routes
- [ ] 2.3 Add CI-blocking checks for missing deferred-route contract metadata in this phase
## 3. Canonical Routing and Compatibility
- [ ] 3.1 Define canonical shell entry behavior for each deferred route
- [ ] 3.2 Implement explicit compatibility policy for direct non-canonical entry with query continuity
- [ ] 3.3 Add integration tests for canonical redirect and compatibility semantics
## 4. Page-Content Modernization Safety
- [ ] 4.1 Define per-route content contracts (filter semantics, payload, chart/table shape, state transitions)
- [ ] 4.2 Build golden fixtures and interaction parity checks for each deferred route
- [ ] 4.3 Add route-scoped feature flags and rollback controls for deferred-route cutover
- [ ] 4.4 Define and enforce per-route manual acceptance checklist and sign-off records
- [ ] 4.5 Record known-bug baselines before implementation and require bug replay during acceptance
- [ ] 4.6 Block sign-off and legacy retirement when known bugs reproduce on modernized routes
## 5. Asset Readiness and Fallback Retirement
- [ ] 5.1 Extend asset-readiness manifest/checks to deferred routes
- [ ] 5.2 Enforce fail-fast release behavior when deferred-route assets are missing
- [ ] 5.3 Retire deferred-route runtime fallback posture per governance milestones
## 6. Quality Gates, CI, and Rollout
- [ ] 6.1 Extend functional/visual/accessibility/performance gates to deferred routes
- [ ] 6.2 Wire CI jobs for route governance, quality gates, and readiness checks for deferred scope
- [ ] 6.3 Update rollout runbook, rollback controls, and observability checkpoints for deferred-route cutover

View File

@@ -0,0 +1,40 @@
# asset-readiness-and-fallback-retirement Specification
## Purpose
TBD - created by archiving change full-modernization-architecture-blueprint. Update Purpose after archive.
## Requirements
### Requirement: In-scope frontend assets SHALL be release-ready before deployment
In-scope routes SHALL rely on build/deploy readiness guarantees instead of runtime fallback behavior as the primary resilience mechanism.
#### Scenario: Build-readiness enforcement
- **WHEN** release artifacts are prepared for deployment
- **THEN** in-scope route assets SHALL be validated for presence and loadability
- **THEN** missing required in-scope assets SHALL fail the release gate
### Requirement: Runtime fallback retirement SHALL follow a governed phase policy
Runtime fallback behavior for in-scope modernization routes SHALL be retired under explicit governance milestones.
#### Scenario: Fallback retirement in phase scope
- **WHEN** a route is marked in-scope for fallback retirement
- **THEN** runtime fallback behavior for that route SHALL be removed or disabled by policy
- **THEN** reliability for that route SHALL be guaranteed by release-time readiness gates
### Requirement: Deferred routes SHALL keep existing fallback posture in this phase
Routes deferred from this modernization phase SHALL retain their existing fallback posture until handled by a follow-up change.
#### Scenario: Deferred fallback continuity
- **WHEN** `/tables`, `/excel-query`, `/query-tool`, or `/mid-section-defect` is evaluated in this phase
- **THEN** fallback retirement SHALL NOT be required for phase completion
- **THEN** fallback retirement decisions for those routes SHALL be addressed in a follow-up modernization change
### Requirement: Fallback-retirement failure response SHALL be consistent across route hosts
When in-scope runtime fallback retirement is enabled and route assets are unavailable, app-level and blueprint-level route handlers SHALL return a consistent retired-fallback response surface.
#### Scenario: App-level in-scope route enters retired fallback state
- **WHEN** an in-scope app-level route cannot serve required dist assets and fallback retirement is enabled
- **THEN** the route SHALL return the standardized retired-fallback response contract
#### Scenario: Blueprint-level in-scope route enters retired fallback state
- **WHEN** an in-scope blueprint-level route cannot serve required dist assets and fallback retirement is enabled
- **THEN** the route SHALL return the same standardized retired-fallback response contract used by app-level routes

View File

@@ -0,0 +1,48 @@
# frontend-platform-modernization-governance Specification
## Purpose
TBD - created by archiving change full-modernization-architecture-blueprint. Update Purpose after archive.
## Requirements
### Requirement: Frontend modernization scope SHALL be explicitly governed
The modernization program SHALL define an explicit in-scope and out-of-scope route matrix for each phase, and SHALL treat that matrix as a release-governed contract artifact.
#### Scenario: Scope matrix publication
- **WHEN** a modernization phase is created
- **THEN** the phase SHALL publish an explicit in-scope route list and out-of-scope route list
- **THEN** the matrix SHALL include `/admin/pages` and `/admin/performance` in scope for this phase
- **THEN** the matrix SHALL mark `/tables`, `/excel-query`, `/query-tool`, and `/mid-section-defect` as deferred routes for a follow-up phase
#### Scenario: Scope drift prevention
- **WHEN** implementation tasks are derived from the phase specs
- **THEN** tasks targeting routes outside the in-scope matrix SHALL be rejected for this phase
### Requirement: Modernization phases SHALL define completion and deprecation milestones
Each modernization phase SHALL define measurable completion criteria and deprecation milestones for legacy-era patterns.
#### Scenario: Phase completion criteria
- **WHEN** a phase reaches release review
- **THEN** it SHALL provide objective completion criteria for route governance, style governance, and quality gates
- **THEN** it SHALL identify any remaining deferred routes and their next-phase linkage
#### Scenario: Legacy deprecation milestones
- **WHEN** legacy fallback or legacy style exceptions remain in phase scope
- **THEN** the phase SHALL define a dated milestone or release gate to remove those exceptions
### Requirement: Operator-facing environment defaults SHALL be onboarding-safe
`.env.example` SHALL prioritize local onboarding safety while clearly documenting production hardening recommendations for modernization controls.
#### Scenario: Local bootstrap from `.env.example`
- **WHEN** a developer initializes `.env` from `.env.example` in a local non-production environment
- **THEN** startup-critical modernization flags SHALL default to onboarding-safe values that do not fail boot solely because dist readiness gates are strict by default
#### Scenario: Production recommendation visibility
- **WHEN** operators review `.env.example` for deployment configuration
- **THEN** production-recommended values for shell-first and modernization-hardening flags SHALL be explicitly documented in adjacent comments
### Requirement: Policy cache refresh model SHALL be explicit in governance docs
Governance-owned policy artifacts that are loaded with in-process caching SHALL document runtime refresh behavior and operator expectations.
#### Scenario: Cached policy artifact behavior documentation
- **WHEN** maintainers read modernization governance artifacts
- **THEN** they SHALL find explicit guidance on whether policy JSON updates require process restart, cache clear, or automatic reload

View File

@@ -0,0 +1,29 @@
# frontend-quality-gate-modernization Specification
## Purpose
TBD - created by archiving change full-modernization-architecture-blueprint. Update Purpose after archive.
## Requirements
### Requirement: Modernization releases SHALL pass multi-dimensional frontend quality gates
In-scope modernization releases SHALL pass functional, visual, accessibility, and performance gates before promotion.
#### Scenario: Gate bundle at release candidate
- **WHEN** a release candidate includes in-scope modernization changes
- **THEN** it SHALL execute functional behavior parity checks for affected routes
- **THEN** it SHALL execute critical-state visual regression checks for affected routes
- **THEN** it SHALL execute accessibility checks for keyboard and reduced-motion behavior
- **THEN** it SHALL execute performance budget checks for defined shell/route thresholds
### Requirement: Gate failures SHALL block release promotion
Blocking quality gates SHALL prevent release promotion for in-scope modernization changes.
#### Scenario: Blocking gate failure
- **WHEN** any mandatory modernization quality gate fails
- **THEN** release promotion SHALL be blocked until the failure is resolved or explicitly waived per governance policy
### Requirement: Deferred routes SHALL be excluded from this phase gate baseline
The route baseline for this modernization phase SHALL exclude deferred routes.
#### Scenario: Deferred route baseline exclusion
- **WHEN** gate baseline is computed for this phase
- **THEN** `/tables`, `/excel-query`, `/query-tool`, and `/mid-section-defect` SHALL be excluded from mandatory modernization gate coverage

View File

@@ -2,22 +2,27 @@
Define stable requirements for full-vite-page-modularization.
## Requirements
### Requirement: Major Pages SHALL be Managed by Vite Modules
The system SHALL provide Vite-managed module entries for major portal pages under a phased SPA-shell migration while keeping direct route access compatible.
The system SHALL provide Vite-managed module entries for all in-scope modernization routes under shell-first governance, including admin surfaces `/admin/pages` and `/admin/performance` as governed targets. Deferred routes (`/tables`, `/excel-query`, `/query-tool`, `/mid-section-defect`) are excluded from this phase's required module-governance completeness.
#### Scenario: Portal shell module loading
- **WHEN** the portal experience is rendered
- **THEN** it MUST load its behavior from a Vite-built module asset when available
#### Scenario: In-scope module governance completeness
- **WHEN** modernization route coverage is validated for this phase
- **THEN** every in-scope route SHALL have deterministic module-governance metadata and ownership mapping
#### Scenario: Module fallback continuity
- **WHEN** a required Vite asset is unavailable
- **THEN** the system MUST keep affected page behavior functional through explicit fallback logic
#### Scenario: Deferred route exclusion in this phase
- **WHEN** completeness validation executes for this phase
- **THEN** deferred routes SHALL be excluded from mandatory pass criteria
### Requirement: Build Pipeline SHALL Produce Backend-Served Assets
Vite build output MUST be emitted into backend static paths and served by Flask/Gunicorn on the same origin.
Vite build output for in-scope modernization routes MUST be emitted into backend static paths and validated at release time. Missing required in-scope assets SHALL fail release gates instead of relying on runtime fallback behavior.
#### Scenario: Build artifact placement
- **WHEN** frontend build is executed
- **THEN** generated JS/CSS files SHALL be written to the configured backend static dist directory
#### Scenario: Build artifact readiness for in-scope routes
- **WHEN** frontend build is executed for release
- **THEN** required in-scope route artifacts SHALL be present in configured backend static dist paths
- **THEN** missing required artifacts SHALL fail readiness checks
#### Scenario: Deferred route fallback posture unchanged in this phase
- **WHEN** deferred routes are evaluated in this phase
- **THEN** existing fallback posture SHALL not block this phase's completion
### Requirement: Vite Page Modules SHALL Reuse Shared Chart and Query Building Blocks
Page entry modules MUST consume shared chart/query/drawer utilities for common behaviors.
@@ -83,3 +88,4 @@ WIP overview and WIP detail Vite entry modules SHALL use shared frontend core ut
#### Scenario: Shared utility change propagates across both pages
- **WHEN** autocomplete mapping rules are updated in the shared core module
- **THEN** both WIP overview and WIP detail modules MUST consume the updated behavior without duplicated page-local logic edits

View File

@@ -17,3 +17,21 @@ Cache, throttling, and index-related numeric literals that control behavior MUST
- **WHEN** operators need to tune cache/index thresholds
- **THEN** they MUST find values in named constants or environment variables rather than scattered inline literals
### Requirement: Feature-flag resolution SHALL use shared helper semantics
Environment/config/default feature-flag resolution logic SHALL be implemented through shared helper utilities instead of duplicated per-module parsing.
#### Scenario: Feature-flag evaluation in app and policy modules
- **WHEN** modules resolve boolean feature flags from environment variables and Flask config
- **THEN** they SHALL use a shared helper that enforces consistent precedence and truthy/falsey parsing behavior
### Requirement: Cached policy payloads SHALL protect against shared mutable-state corruption
Policy loader functions that cache JSON payloads in-process SHALL prevent downstream callers from mutating the shared cached object reference.
#### Scenario: Cached policy payload consumed by multiple callers
- **WHEN** multiple callers read cached policy payloads during process lifetime
- **THEN** one caller's accidental mutation SHALL NOT alter another caller's observed policy state through shared reference side effects
#### Scenario: Policy cache behavior documentation
- **WHEN** maintainers inspect cached policy loader code
- **THEN** they SHALL find explicit comments describing refresh/invalidation behavior expectations

View File

@@ -0,0 +1,58 @@
# page-content-modernization-safety Specification
## Purpose
TBD - created by archiving change full-modernization-architecture-blueprint. Update Purpose after archive.
## Requirements
### Requirement: In-scope page-content modernization SHALL be contract-first
Before chart/filter/page interaction refactors are cut over, each in-scope route SHALL define a contract baseline that captures data and interaction semantics.
#### Scenario: Route contract baseline defined
- **WHEN** an in-scope route is selected for chart/filter modernization
- **THEN** the route SHALL define filter input semantics, query payload expectations, and chart data-shape contracts
- **THEN** the route SHALL define critical state expectations for loading, empty, error, and success interactions
### Requirement: Cutover SHALL require parity evidence against baseline behavior
In-scope chart/filter modernization cutover SHALL require parity evidence against baseline fixtures and critical interaction flows.
#### Scenario: Parity gate before default switch
- **WHEN** a route is proposed for defaulting to a modernized chart/filter implementation
- **THEN** golden fixture parity checks SHALL pass for defined critical states
- **THEN** interaction parity checks SHALL pass for filter apply/reset and chart selection/drill behaviors
### Requirement: Route-level content cutover SHALL be reversible
Modernized chart/filter content rollouts SHALL use reversible controls that allow immediate rollback without reverting unrelated shell architecture work.
#### Scenario: Controlled rollout and rollback
- **WHEN** a modernized route is enabled for users
- **THEN** the route SHALL be controlled by route-scoped feature flag or equivalent switch
- **THEN** rollback procedure SHALL be documented and executable within one release cycle
### Requirement: Page-content modernization progression SHALL require manual route acceptance
In-scope chart/filter/page-content migration SHALL progress one route at a time with explicit manual acceptance records.
#### Scenario: Route-by-route manual acceptance gate
- **WHEN** an in-scope route completes modernization implementation and parity checks
- **THEN** that route SHALL be manually accepted using a defined checklist covering filter flows, chart interactions, empty/error behavior, and visual correctness
- **THEN** the next route SHALL NOT begin cutover until manual acceptance for the current route is signed off
### Requirement: Known legacy bugs in migrated scope SHALL NOT be carried into modernized routes
Modernized route acceptance SHALL include explicit revalidation of known legacy defects in migrated scope, and reproduced defects SHALL block sign-off.
#### Scenario: Route-level legacy bug baseline and replay
- **WHEN** an in-scope route enters chart/filter/page-content modernization
- **THEN** a route-level known-bug baseline (within migrated scope) SHALL be recorded before implementation
- **THEN** manual acceptance SHALL replay those known-bug checks on the modernized route
#### Scenario: Legacy bug carry-over is blocked
- **WHEN** manual acceptance finds that a known legacy bug is still reproducible in the modernized route
- **THEN** route sign-off SHALL fail
- **THEN** route cutover completion and legacy code retirement SHALL be blocked until the bug is fixed
### Requirement: Legacy content path retirement SHALL require parity and manual acceptance
Legacy chart/filter implementations SHALL be removed only after parity checks and manual acceptance criteria are satisfied.
#### Scenario: Legacy removal approval
- **WHEN** legacy chart/filter code is planned for removal on an in-scope route
- **THEN** the route SHALL provide parity pass evidence and manual acceptance sign-off records
- **THEN** unresolved parity failures or manual acceptance defects SHALL block legacy removal

View File

@@ -2,46 +2,31 @@
Define stable requirements for spa-shell-navigation.
## Requirements
### Requirement: Portal SHALL provide a SPA shell driven by Vue Router
The portal frontend SHALL use a single SPA shell entry and Vue Router to render page modules without iframe embedding, and SHALL route each page through native route-view integration. The shell layout SHALL use a full-viewport fluid layout with flexbox, removing all max-width constraints and block-centered styling. The main content area (`.shell-content`) SHALL fill available space as a flex child, and the sidebar SHALL be a collapsible flex child that pushes content when expanded on desktop. The content area class SHALL be `.shell-content` (not `.content`) to avoid CSS collision with page-level `.content` classes.
The portal frontend SHALL use a single SPA shell entry and Vue Router to render in-scope page modules without iframe embedding. In-scope routes for this phase SHALL include the governed report routes and admin surfaces `/admin/pages` and `/admin/performance`, while deferred routes (`/tables`, `/excel-query`, `/query-tool`, `/mid-section-defect`) are explicitly excluded from this phase contract.
#### Scenario: Drawer navigation renders integrated route view
- **WHEN** a user clicks a sidebar page entry whose migration mode is `native`
- **THEN** the active route SHALL be updated through Vue Router
- **THEN** the main content area SHALL render the corresponding page module inside shell route-view without iframe usage
- **THEN** the content area SHALL fill the available viewport width minus the sidebar width (if sidebar is expanded)
#### Scenario: In-scope route renders through shell governance
- **WHEN** a user navigates to an in-scope shell-governed route
- **THEN** the route SHALL resolve through Vue Router with shell contract metadata
- **THEN** the shell SHALL render the corresponding module/target without iframe fallback
#### Scenario: Shell layout fills full viewport
- **WHEN** the portal shell renders
- **THEN** the shell SHALL span the full viewport width with no max-width constraint
- **THEN** the header SHALL span edge-to-edge with no border-radius
- **THEN** the sidebar and content area SHALL have no outer borders or border-radius
#### Scenario: Admin route appears as governed target
- **WHEN** an admin user opens shell navigation
- **THEN** `/admin/pages` and `/admin/performance` SHALL be exposed as governed navigation targets per access policy
#### Scenario: Page-level max-width constraints are removed when embedded
- **WHEN** a page module registered in the shell route contracts renders inside `.shell-content`
- **THEN** page-level max-width constraints SHALL be overridden to allow full-width rendering
- **THEN** page-level duplicate padding SHALL be removed to avoid double spacing
- **THEN** standalone page rendering (outside the shell) SHALL remain unaffected
#### Scenario: Shell content class avoids collision with page-level classes
- **WHEN** a page module that uses its own `.content` class renders inside the shell
- **THEN** the shell's content wrapper (`.shell-content`) SHALL NOT interfere with the page's `.content` styling
#### Scenario: Wrapper route remains available during migration
- **WHEN** a user clicks a sidebar page entry whose migration mode is `wrapper`
- **THEN** Vue Router SHALL render the wrapper host in shell content area
- **THEN** the wrapper SHALL preserve page reachability until native rewrite is completed
#### Scenario: Deferred route is excluded from this phase route-governance requirement
- **WHEN** phase-level shell-governance compliance is evaluated
- **THEN** `/tables`, `/excel-query`, `/query-tool`, and `/mid-section-defect` SHALL be treated as deferred and excluded from pass/fail criteria for this phase
### Requirement: Existing route contracts SHALL remain stable in SPA mode
Migration to SPA shell SHALL preserve existing route paths, deep-link behavior, and query semantics during both native and wrapper phases.
Migration to the shell-first SPA model SHALL preserve route/query compatibility for in-scope routes while introducing canonical shell routing policy and explicit compatibility handling.
#### Scenario: Direct route entry remains functional
- **WHEN** a user opens an existing route directly (bookmark or refresh)
- **THEN** the route SHALL resolve to the same page functionality as before migration
- **THEN** required query parameters SHALL continue to be interpreted with compatible semantics
#### Scenario: Canonical shell path behavior for in-scope routes
- **WHEN** a user opens an in-scope report route via canonical shell path
- **THEN** route behavior and query semantics SHALL remain compatible with established baseline behavior
#### Scenario: Query continuity across shell navigation
- **WHEN** users navigate from shell list pages to detail pages and back
- **THEN** query-state parameters required by list/detail workflows SHALL remain consistent with pre-migration behavior
#### Scenario: Compatibility policy for direct route entry
- **WHEN** a user opens an in-scope report route via direct non-canonical entry
- **THEN** the system SHALL apply explicit compatibility policy (preserve behavior or compatibility redirect) without breaking route semantics
### Requirement: SPA shell navigation SHALL enforce page visibility rules
SPA navigation SHALL respect backend-defined drawer and page visibility outcomes, including admin entry visibility and route fallback for hidden routes.
@@ -60,3 +45,22 @@ SPA navigation SHALL respect backend-defined drawer and page visibility outcomes
- **THEN** the shell SHALL redirect to a safe fallback route
- **THEN** the shell SHALL NOT expose iframe-based fallback rendering
### Requirement: Canonical redirect scope boundaries SHALL be explicit and intentional
Canonical shell direct-entry redirects SHALL apply only to governed in-scope report routes and SHALL explicitly exclude admin external targets with documented rationale.
#### Scenario: In-scope report route direct entry
- **WHEN** SPA shell mode is enabled and a user enters an in-scope report route directly
- **THEN** the system SHALL redirect to the canonical `/portal-shell/...` route while preserving query semantics
#### Scenario: Admin external target direct entry
- **WHEN** SPA shell mode is enabled and a user enters `/admin/pages` or `/admin/performance` directly
- **THEN** the system SHALL NOT apply report-route canonical redirect policy
- **THEN** the exclusion rationale SHALL be documented in code-level comments or governance docs
### Requirement: Missing-required-parameter redirects SHALL avoid avoidable multi-hop chains
Routes with server-side required query parameters SHALL minimize redirect hops under SPA shell mode.
#### Scenario: Hold detail missing reason in SPA shell mode
- **WHEN** a user opens `/hold-detail` without `reason` while SPA shell mode is enabled
- **THEN** the route SHALL resolve via a single-hop redirect to the canonical overview shell path

View File

@@ -0,0 +1,40 @@
# style-isolation-and-token-enforcement Specification
## Purpose
TBD - created by archiving change full-modernization-architecture-blueprint. Update Purpose after archive.
## Requirements
### Requirement: In-scope pages SHALL enforce style isolation boundaries
In-scope modernization pages SHALL avoid page-global selectors for page-local concerns and SHALL keep style concerns scoped to route-level containers or shared design-system layers.
#### Scenario: Global selector control
- **WHEN** style governance checks analyze in-scope page styles
- **THEN** page-local style changes SHALL NOT introduce new `:root` or `body` rules for route-local presentation concerns
- **THEN** shared cross-route concerns SHALL be authored in designated shared style layers
### Requirement: In-scope shared semantics SHALL be token-first
Shared UI semantics in in-scope routes SHALL be implemented with token-backed Tailwind/shared-style primitives before page-local overrides are allowed.
#### Scenario: Token-first UI pattern adoption
- **WHEN** an in-scope route introduces or updates shared UI semantics (layout shell, card, filter, action, status)
- **THEN** the route SHALL consume token-backed shared primitives
- **THEN** page-local hard-coded visual values SHALL require explicit exception justification
### Requirement: Legacy style exceptions SHALL be tracked and sunset
Legacy CSS exceptions for in-scope routes SHALL be tracked with ownership and removal milestones.
#### Scenario: Exception registry requirement
- **WHEN** an in-scope route cannot yet remove legacy style behavior
- **THEN** the route SHALL be registered with an exception owner and planned removal milestone
- **THEN** unresolved exceptions past milestone SHALL fail modernization governance review
### Requirement: Route-local token usage SHALL include fallback values outside shell scope
Route-level styles that reference shell-provided token variables SHALL define fallback values to preserve rendering correctness when rendered outside shell variable scope.
#### Scenario: Route rendered outside portal shell variable scope
- **WHEN** a route-local stylesheet references shell token variables and the page is rendered without shell-level CSS variables
- **THEN** visual-critical properties (for example header gradients) SHALL still resolve through explicit fallback token values
#### Scenario: Style governance check for unresolved shell variables
- **WHEN** style-governance validation inspects in-scope route styles
- **THEN** unresolved shell-variable references without fallback SHALL be flagged as governance failures or approved exceptions

View File

@@ -1,29 +1,28 @@
## Purpose
Define stable requirements for tailwind-design-system.
## Requirements
### Requirement: Frontend styles SHALL be governed by Tailwind design tokens
The frontend SHALL define a Tailwind-based design token system for color, spacing, typography, radius, and elevation to ensure consistent styling across modules. The `--portal-shell-max-width` CSS variable SHALL be set to `none` to support the fluid layout. A `--portal-sidebar-width` variable SHALL be added for sidebar width reference. The `.u-content-shell` utility class SHALL use `width: 100%` instead of `max-width` constraint.
The frontend SHALL enforce a token-governed style system for in-scope routes. Shared visual semantics SHALL be expressed through token-backed Tailwind/shared layers, and ad-hoc page-local hard-coded values for shared semantics SHALL require explicit exception governance.
#### Scenario: Shared token usage across modules
- **WHEN** two report modules render equivalent UI elements (e.g., card, filter chip, primary button)
#### Scenario: Shared token usage across in-scope modules
- **WHEN** two in-scope modules render equivalent UI semantics (e.g., card, filter chip, primary action, status indicator)
- **THEN** they SHALL use the same token-backed style semantics
- **THEN** visual output SHALL remain consistent across modules
- **THEN** visual output SHALL remain consistent across those modules
#### Scenario: Fluid layout tokens
- **WHEN** the portal shell renders
- **THEN** `--portal-shell-max-width` SHALL resolve to `none`
- **THEN** `--portal-sidebar-width` SHALL resolve to `240px`
- **THEN** `.u-content-shell` SHALL apply `width: 100%` without max-width constraint
#### Scenario: Token governance review
- **WHEN** an in-scope route introduces new shared UI styling
- **THEN** the styling SHALL map to shared tokens/layers or be recorded in an approved exception registry
### Requirement: Tailwind migration SHALL support coexistence with legacy CSS
The migration SHALL allow Tailwind and existing page CSS to coexist during phased rollout without breaking existing pages.
Tailwind migration SHALL support controlled coexistence only as a transition state for this phase. In-scope routes SHALL move toward isolation-first style ownership and SHALL NOT introduce new page-global CSS side effects for route-local concerns.
#### Scenario: Legacy page remains functional during coexistence
- **WHEN** a not-yet-migrated page is rendered
- **THEN** existing CSS behavior SHALL remain intact
- **THEN** Tailwind introduction SHALL NOT cause blocking style regressions
#### Scenario: In-scope global selector control
- **WHEN** in-scope route styles are reviewed
- **THEN** new route-local styling SHALL NOT introduce page-global selectors (`:root`, `body`) for local presentation behavior
#### Scenario: Deferred route coexistence allowance
- **WHEN** deferred routes (`/tables`, `/excel-query`, `/query-tool`, `/mid-section-defect`) are evaluated during this phase
- **THEN** existing coexistence posture SHALL be allowed and handled by a follow-up modernization change
### Requirement: New shared UI components SHALL prefer Tailwind-first styling
Newly introduced shared components SHALL be implemented with Tailwind-first conventions to avoid expanding duplicated page-local CSS.
@@ -32,3 +31,4 @@ Newly introduced shared components SHALL be implemented with Tailwind-first conv
- **WHEN** a new shared component is introduced in migration scope
- **THEN** its primary style contract SHALL be expressed through Tailwind utilities/components
- **THEN** page-local CSS additions SHALL be minimized and justified

View File

@@ -0,0 +1,51 @@
# unified-shell-route-coverage Specification
## Purpose
TBD - created by archiving change full-modernization-architecture-blueprint. Update Purpose after archive.
## Requirements
### Requirement: In-scope routes SHALL be shell-contract governed
All in-scope modernization routes SHALL be represented in shell route contracts, loader registration policy, and navigation visibility governance.
#### Scenario: In-scope coverage validation
- **WHEN** shell route contract validation is executed
- **THEN** every in-scope route SHALL have route metadata, ownership metadata, and visibility policy metadata
- **THEN** missing in-scope route contracts SHALL fail validation
#### Scenario: Admin route inclusion
- **WHEN** shell navigation is built for admin users
- **THEN** `/admin/pages` and `/admin/performance` SHALL be represented as governed navigation targets according to visibility/access policy
### Requirement: Out-of-scope routes SHALL not block this phase
Routes explicitly marked as out-of-scope for this modernization phase SHALL be excluded from required shell-coverage gates in this phase.
#### Scenario: Deferred route exclusion
- **WHEN** modernization gates execute for this phase
- **THEN** `/tables`, `/excel-query`, `/query-tool`, and `/mid-section-defect` SHALL be treated as deferred routes
- **THEN** deferred route absence from new shell-governance gates SHALL NOT fail this phase
### Requirement: Route coverage governance SHALL be CI-enforced
Route coverage and contract completeness checks for in-scope routes SHALL run as CI gates.
#### Scenario: CI gate failure on in-scope gap
- **WHEN** CI detects an in-scope route without required contract metadata
- **THEN** the modernization gate SHALL fail
- **THEN** release promotion SHALL be blocked until resolved
### Requirement: Frontend and backend route-contract inventories SHALL be cross-validated
Route-governance checks SHALL verify that frontend shell route contracts and backend route contract artifacts describe the same governed route set and scope classes.
#### Scenario: Cross-source contract parity gate
- **WHEN** modernization governance checks run in CI
- **THEN** mismatches between backend route contract JSON and frontend `routeContracts.js` route inventory SHALL fail the gate
#### Scenario: Scope classification drift detection
- **WHEN** a route has inconsistent scope classification between frontend and backend contract sources
- **THEN** governance checks SHALL report the specific route and conflicting scope values
### Requirement: Legacy contract-source fallback SHALL emit operational warning
When contract loading falls back from the primary modernization contract artifact to a legacy artifact path, the service SHALL emit explicit warning telemetry.
#### Scenario: Legacy contract fallback path selected
- **WHEN** the primary contract artifact is unavailable and a legacy contract file is loaded
- **THEN** the system SHALL log a warning that includes the selected legacy source path

View File

@@ -1,21 +1,24 @@
## Purpose
Define stable requirements for vue-vite-page-architecture.
## Requirements
### Requirement: Pure Vite pages SHALL be served as static HTML
The system SHALL support serving Vite-built HTML pages directly via Flask without Jinja2 rendering.
The system SHALL serve in-scope pure Vite pages through backend static HTML delivery under a shell-first canonical routing policy. Direct-entry compatibility for in-scope routes SHALL be explicit and governed. Admin targets `/admin/pages` and `/admin/performance` SHALL be represented as governed shell navigation targets, while maintaining backend auth/session authority.
#### Scenario: Serve pure Vite page
- **WHEN** user navigates to a pure Vite page route (e.g., `/qc-gate`)
- **THEN** Flask SHALL serve the pre-built HTML file from `static/dist/` via `send_from_directory`
- **THEN** the HTML SHALL NOT pass through Jinja2 template rendering
#### Scenario: In-scope canonical shell entry
- **WHEN** a user navigates to an in-scope canonical shell route
- **THEN** the shell SHALL render the target route via governed route contracts and static asset delivery
#### Scenario: Page works as top-level navigation target
- **WHEN** a pure Vite page is opened from portal direct navigation
- **THEN** the page SHALL render correctly as a top-level route without iframe embedding dependency
- **THEN** page functionality SHALL NOT rely on portal-managed frame lifecycle
#### Scenario: Direct-entry compatibility policy for in-scope routes
- **WHEN** a user opens an in-scope route through direct non-canonical entry
- **THEN** the system SHALL apply explicit compatibility behavior without breaking established query semantics
#### Scenario: Admin targets in shell governance
- **WHEN** shell navigation is rendered for an authorized admin user
- **THEN** `/admin/pages` and `/admin/performance` SHALL be reachable through governed admin navigation targets
#### Scenario: Deferred routes excluded from this phase architecture criteria
- **WHEN** this phase architecture compliance is evaluated
- **THEN** `/tables`, `/excel-query`, `/query-tool`, and `/mid-section-defect` SHALL be excluded and handled in a follow-up change
### Requirement: Vite config SHALL support Vue SFC and HTML entry points
The Vite build configuration SHALL support Vue Single File Components alongside existing vanilla JS entries.
@@ -133,3 +136,4 @@ The mid-section defect page SHALL use `AbortController` to cancel in-flight API
- **WHEN** a query is in-flight and user triggers pagination
- **THEN** the query SHALL NOT be cancelled by the pagination request
- **THEN** the pagination SHALL use a separate abort key from the query

View File

@@ -0,0 +1,460 @@
#!/usr/bin/env python3
"""Run governance/quality/readiness checks for full modernization change."""
from __future__ import annotations
import argparse
import json
import re
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any
ROOT = Path(__file__).resolve().parents[1]
DOCS_DIR = ROOT / "docs" / "migration" / "full-modernization-architecture-blueprint"
SCOPE_MATRIX_FILE = DOCS_DIR / "route_scope_matrix.json"
ROUTE_CONTRACT_FILE = DOCS_DIR / "route_contracts.json"
EXCEPTION_REGISTRY_FILE = DOCS_DIR / "exception_registry.json"
QUALITY_POLICY_FILE = DOCS_DIR / "quality_gate_policy.json"
ASSET_MANIFEST_FILE = DOCS_DIR / "asset_readiness_manifest.json"
KNOWN_BUG_BASELINE_FILE = DOCS_DIR / "known_bug_baseline.json"
MANUAL_ACCEPTANCE_FILE = DOCS_DIR / "manual_acceptance_records.json"
BUG_REVALIDATION_FILE = DOCS_DIR / "bug_revalidation_records.json"
STYLE_INVENTORY_FILE = DOCS_DIR / "style_inventory.json"
OUTPUT_REPORT_FILE = DOCS_DIR / "quality_gate_report.json"
FRONTEND_ROUTE_CONTRACT_FILE = ROOT / "frontend" / "src" / "portal-shell" / "routeContracts.js"
GLOBAL_SELECTOR_PATTERN = re.compile(r"(^|\\s)(:root|body)\\b", re.MULTILINE)
FRONTEND_ROUTE_ENTRY_PATTERN = re.compile(
r"""['"](?P<key>/[^'"]+)['"]\s*:\s*buildContract\(\s*{(?P<body>.*?)}\s*\)""",
re.DOTALL,
)
FRONTEND_ROUTE_FIELD_PATTERN = re.compile(r"""route\s*:\s*['"](?P<route>/[^'"]+)['"]""")
FRONTEND_SCOPE_FIELD_PATTERN = re.compile(r"""scope\s*:\s*['"](?P<scope>[^'"]+)['"]""")
SHELL_TOKEN_VAR_PATTERN = re.compile(r"""var\(\s*(--portal-[\w-]+)(?P<fallback>\s*,[^)]*)?\)""")
@dataclass
class CheckReport:
mode: str
errors: list[str] = field(default_factory=list)
warnings: list[str] = field(default_factory=list)
info: list[str] = field(default_factory=list)
def fail(self, message: str) -> None:
self.errors.append(message)
def warn(self, message: str) -> None:
self.warnings.append(message)
def note(self, message: str) -> None:
self.info.append(message)
def to_dict(self) -> dict[str, Any]:
return {
"mode": self.mode,
"errors": self.errors,
"warnings": self.warnings,
"info": self.info,
"passed": not self.errors,
}
def _load_json(path: Path, *, default: dict[str, Any] | None = None) -> dict[str, Any]:
if not path.exists():
return dict(default or {})
return json.loads(path.read_text(encoding="utf-8"))
def _display_path(path: Path) -> str:
try:
return str(path.relative_to(ROOT))
except ValueError:
return str(path)
def _route_css_targets() -> dict[str, list[Path]]:
return {
"/wip-overview": [ROOT / "frontend/src/wip-overview/style.css"],
"/wip-detail": [ROOT / "frontend/src/wip-detail/style.css"],
"/hold-overview": [ROOT / "frontend/src/hold-overview/style.css"],
"/hold-detail": [ROOT / "frontend/src/hold-detail/style.css"],
"/hold-history": [ROOT / "frontend/src/hold-history/style.css"],
"/resource": [ROOT / "frontend/src/resource-status/style.css"],
"/resource-history": [ROOT / "frontend/src/resource-history/style.css"],
"/qc-gate": [ROOT / "frontend/src/qc-gate/style.css"],
"/job-query": [ROOT / "frontend/src/job-query/style.css"],
"/tmtt-defect": [ROOT / "frontend/src/tmtt-defect/style.css"],
"/admin/pages": [ROOT / "src/mes_dashboard/templates/admin/pages.html"],
"/admin/performance": [ROOT / "src/mes_dashboard/templates/admin/performance.html"],
}
def _find_global_selectors(path: Path) -> list[str]:
if not path.exists():
return []
text = path.read_text(encoding="utf-8", errors="ignore")
selectors = []
for match in GLOBAL_SELECTOR_PATTERN.finditer(text):
selectors.append(match.group(2))
return sorted(set(selectors))
def _find_shell_tokens_without_fallback(path: Path) -> list[str]:
if not path.exists() or path.suffix.lower() != ".css":
return []
text = path.read_text(encoding="utf-8", errors="ignore")
missing: list[str] = []
for match in SHELL_TOKEN_VAR_PATTERN.finditer(text):
token = match.group(1)
fallback = match.group("fallback")
if fallback is None:
missing.append(token)
return sorted(set(missing))
def _check_scope_matrix(scope_matrix: dict[str, Any], report: CheckReport) -> tuple[set[str], set[str]]:
in_scope = {
str(item.get("route", "")).strip()
for item in scope_matrix.get("in_scope", [])
if str(item.get("route", "")).strip().startswith("/")
}
deferred = {
str(item.get("route", "")).strip()
for item in scope_matrix.get("deferred", [])
if str(item.get("route", "")).strip().startswith("/")
}
if not in_scope:
report.fail("scope matrix has no in-scope routes")
if "/admin/pages" not in in_scope or "/admin/performance" not in in_scope:
report.fail("scope matrix must include /admin/pages and /admin/performance")
required_deferred = {"/tables", "/excel-query", "/query-tool", "/mid-section-defect"}
if deferred != required_deferred:
report.fail("scope matrix deferred routes mismatch expected policy")
return in_scope, deferred
def _check_route_contracts(
route_contracts: dict[str, Any],
in_scope: set[str],
report: CheckReport,
) -> dict[str, dict[str, Any]]:
required_fields = {
"route",
"route_id",
"scope",
"render_mode",
"owner",
"visibility_policy",
"canonical_shell_path",
"rollback_strategy",
}
routes = route_contracts.get("routes", [])
if not isinstance(routes, list):
report.fail("route contract file routes must be a list")
return {}
route_map: dict[str, dict[str, Any]] = {}
for entry in routes:
if not isinstance(entry, dict):
report.fail("route contract entry must be object")
continue
route = str(entry.get("route", "")).strip()
if not route.startswith("/"):
report.fail(f"invalid route contract route: {route!r}")
continue
route_map[route] = entry
missing = sorted(field for field in required_fields if not str(entry.get(field, "")).strip())
if missing:
report.fail(f"{route} missing required contract fields: {', '.join(missing)}")
missing_routes = sorted(in_scope - set(route_map.keys()))
if missing_routes:
report.fail("in-scope routes missing contracts: " + ", ".join(missing_routes))
for route in sorted(in_scope):
entry = route_map.get(route)
if not entry:
continue
if str(entry.get("scope", "")).strip() != "in-scope":
report.fail(f"{route} must be scope=in-scope")
if route.startswith("/admin/") and str(entry.get("visibility_policy", "")).strip() != "admin_only":
report.fail(f"{route} must be admin_only visibility")
return route_map
def _load_frontend_route_contract_inventory(
path: Path,
report: CheckReport,
) -> dict[str, str]:
if not path.exists():
report.fail(f"frontend route contract file missing: {_display_path(path)}")
return {}
text = path.read_text(encoding="utf-8")
route_scopes: dict[str, str] = {}
for match in FRONTEND_ROUTE_ENTRY_PATTERN.finditer(text):
route_key = match.group("key")
body = match.group("body")
route_match = FRONTEND_ROUTE_FIELD_PATTERN.search(body)
scope_match = FRONTEND_SCOPE_FIELD_PATTERN.search(body)
if route_match is None:
report.fail(f"{route_key} missing route field in frontend route contract")
continue
if scope_match is None:
report.fail(f"{route_key} missing scope field in frontend route contract")
continue
route_value = route_match.group("route").strip()
scope = scope_match.group("scope").strip()
if route_value != route_key:
report.fail(
f"{route_key} frontend contract key/route mismatch "
f"(route field: {route_value})"
)
continue
route_scopes[route_key] = scope
if not route_scopes:
report.fail("frontend route contract inventory parse returned no routes")
return route_scopes
def _check_frontend_backend_route_contract_parity(
backend_route_map: dict[str, dict[str, Any]],
frontend_route_scope_map: dict[str, str],
report: CheckReport,
) -> None:
backend_routes = set(backend_route_map.keys())
frontend_routes = set(frontend_route_scope_map.keys())
backend_only = sorted(backend_routes - frontend_routes)
if backend_only:
report.fail(
"backend route contracts missing from frontend routeContracts.js: "
+ ", ".join(backend_only)
)
frontend_only = sorted(frontend_routes - backend_routes)
if frontend_only:
report.fail(
"frontend routeContracts.js routes missing from backend route contracts: "
+ ", ".join(frontend_only)
)
for route in sorted(backend_routes & frontend_routes):
backend_scope = str(backend_route_map[route].get("scope", "")).strip()
frontend_scope = str(frontend_route_scope_map[route]).strip()
if backend_scope != frontend_scope:
report.fail(
f"route scope mismatch for {route}: "
f"backend={backend_scope!r}, frontend={frontend_scope!r}"
)
def _check_quality_policy(
quality_policy: dict[str, Any],
deferred: set[str],
report: CheckReport,
) -> str:
configured_mode = str(quality_policy.get("severity_mode", {}).get("current", "warn")).strip().lower()
if configured_mode not in {"warn", "block"}:
report.fail("quality gate severity_mode.current must be warn or block")
configured_mode = "warn"
excluded = {
str(route).strip()
for route in quality_policy.get("deferred_routes_excluded", [])
if str(route).strip().startswith("/")
}
if excluded != deferred:
report.fail("quality gate deferred exclusion list must match scope matrix deferred list")
return configured_mode
def _check_exception_registry(
exception_registry: dict[str, Any],
report: CheckReport,
) -> dict[str, dict[str, Any]]:
entries = exception_registry.get("entries", [])
if not isinstance(entries, list):
report.fail("exception registry entries must be a list")
return {}
lookup: dict[str, dict[str, Any]] = {}
for entry in entries:
if not isinstance(entry, dict):
report.fail("exception entry must be object")
continue
entry_id = str(entry.get("id", "")).strip()
scope = str(entry.get("scope", "")).strip()
owner = str(entry.get("owner", "")).strip()
milestone = str(entry.get("milestone", "")).strip()
if not entry_id:
report.fail("exception entry missing id")
continue
if not scope.startswith("/"):
report.fail(f"{entry_id} missing valid scope route")
if not owner:
report.fail(f"{entry_id} missing owner")
if not milestone:
report.fail(f"{entry_id} missing milestone")
lookup[scope] = entry
return lookup
def _check_style_governance(
in_scope: set[str],
exception_by_scope: dict[str, dict[str, Any]],
report: CheckReport,
) -> None:
route_targets = _route_css_targets()
for route in sorted(in_scope):
for path in route_targets.get(route, []):
selectors = _find_global_selectors(path)
if selectors:
if route in exception_by_scope:
report.warn(
f"{route} uses global selectors {selectors} in {_display_path(path)} "
"with approved exception"
)
else:
report.fail(
f"{route} uses global selectors {selectors} in {_display_path(path)} "
"without exception"
)
missing_shell_fallbacks = _find_shell_tokens_without_fallback(path)
if not missing_shell_fallbacks:
continue
if route in exception_by_scope:
report.warn(
f"{route} uses shell tokens without fallback {missing_shell_fallbacks} "
f"in {_display_path(path)} with approved exception"
)
continue
report.fail(
f"{route} uses shell tokens without fallback {missing_shell_fallbacks} "
f"in {_display_path(path)}"
)
def _check_asset_readiness(
asset_manifest: dict[str, Any],
deferred: set[str],
report: CheckReport,
) -> None:
required = asset_manifest.get("in_scope_required_assets", {})
if not isinstance(required, dict) or not required:
report.fail("asset readiness manifest missing in_scope_required_assets")
return
declared_deferred = {
str(route).strip()
for route in asset_manifest.get("deferred_routes", [])
if str(route).strip().startswith("/")
}
if declared_deferred != deferred:
report.fail("asset readiness deferred route list must match scope matrix")
dist_dir = ROOT / "src/mes_dashboard/static/dist"
for route, assets in sorted(required.items()):
if not isinstance(assets, list) or not assets:
report.fail(f"asset manifest route {route} must define non-empty asset list")
continue
for filename in assets:
if not isinstance(filename, str) or not filename.strip():
report.fail(f"asset manifest route {route} contains invalid filename")
continue
asset_path = dist_dir / filename
if not asset_path.exists():
report.warn(f"missing dist asset for {route}: {filename}")
def _check_content_safety(
known_bug_baseline: dict[str, Any],
manual_acceptance: dict[str, Any],
bug_revalidation: dict[str, Any],
in_scope: set[str],
report: CheckReport,
) -> None:
baseline_routes = set((known_bug_baseline.get("routes") or {}).keys())
missing_baselines = sorted(in_scope - baseline_routes)
if missing_baselines:
report.fail("known bug baseline missing routes: " + ", ".join(missing_baselines))
records = manual_acceptance.get("records", [])
if not isinstance(records, list):
report.fail("manual acceptance records must be a list")
replay_records = bug_revalidation.get("records", [])
if not isinstance(replay_records, list):
report.fail("bug revalidation records must be a list")
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--mode",
choices=("warn", "block"),
default=None,
help="Gate severity mode override (default: use quality_gate_policy.json)",
)
parser.add_argument(
"--report",
default=str(OUTPUT_REPORT_FILE),
help="Output report JSON path",
)
return parser.parse_args()
def main() -> int:
args = parse_args()
scope_matrix = _load_json(SCOPE_MATRIX_FILE)
route_contracts = _load_json(ROUTE_CONTRACT_FILE)
exception_registry = _load_json(EXCEPTION_REGISTRY_FILE)
quality_policy = _load_json(QUALITY_POLICY_FILE)
asset_manifest = _load_json(ASSET_MANIFEST_FILE)
known_bug_baseline = _load_json(KNOWN_BUG_BASELINE_FILE)
manual_acceptance = _load_json(MANUAL_ACCEPTANCE_FILE, default={"records": []})
bug_revalidation = _load_json(BUG_REVALIDATION_FILE, default={"records": []})
_ = _load_json(STYLE_INVENTORY_FILE, default={})
report = CheckReport(mode=args.mode or "warn")
in_scope, deferred = _check_scope_matrix(scope_matrix, report)
backend_route_map = _check_route_contracts(route_contracts, in_scope, report)
frontend_route_scope_map = _load_frontend_route_contract_inventory(FRONTEND_ROUTE_CONTRACT_FILE, report)
_check_frontend_backend_route_contract_parity(backend_route_map, frontend_route_scope_map, report)
configured_mode = _check_quality_policy(quality_policy, deferred, report)
report.mode = args.mode or configured_mode
exception_by_scope = _check_exception_registry(exception_registry, report)
_check_style_governance(in_scope, exception_by_scope, report)
_check_asset_readiness(asset_manifest, deferred, report)
_check_content_safety(known_bug_baseline, manual_acceptance, bug_revalidation, in_scope, report)
output_path = Path(args.report)
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(json.dumps(report.to_dict(), ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
if report.mode == "block" and report.errors:
print(f"[BLOCK] modernization gates failed with {len(report.errors)} error(s)")
for error in report.errors:
print(f"- {error}")
return 1
if report.errors:
print(f"[WARN] modernization gates found {len(report.errors)} error(s) but mode is warn")
for error in report.errors:
print(f"- {error}")
else:
print("[OK] modernization gates passed")
if report.warnings:
print(f"[WARN] additional warnings: {len(report.warnings)}")
for warning in report.warnings:
print(f"- {warning}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -211,10 +211,37 @@ with engine.connect() as conn:
}
build_frontend_assets() {
if [ "${FRONTEND_BUILD_ON_START:-true}" != "true" ]; then
log_info "Skip frontend build (FRONTEND_BUILD_ON_START=${FRONTEND_BUILD_ON_START})"
local mode="${FRONTEND_BUILD_MODE:-}"
local fail_on_error="${FRONTEND_BUILD_FAIL_ON_ERROR:-}"
# Backward compatibility:
# - FRONTEND_BUILD_MODE takes precedence when set.
# - Otherwise, retain FRONTEND_BUILD_ON_START behavior.
if [ -z "${mode}" ]; then
if [ "${FRONTEND_BUILD_ON_START:-true}" = "true" ]; then
mode="auto"
else
mode="never"
fi
fi
mode="$(echo "${mode}" | tr '[:upper:]' '[:lower:]')"
if [ -z "${fail_on_error}" ]; then
if [ "$(echo "${FLASK_ENV:-development}" | tr '[:upper:]' '[:lower:]')" = "production" ]; then
fail_on_error="true"
else
fail_on_error="false"
fi
fi
if [ "${mode}" = "never" ]; then
log_info "Skip frontend build (FRONTEND_BUILD_MODE=${mode})"
return 0
fi
if [ "${mode}" != "auto" ] && [ "${mode}" != "always" ]; then
log_warn "Invalid FRONTEND_BUILD_MODE='${mode}', fallback to auto"
mode="auto"
fi
if [ ! -f "${ROOT}/frontend/package.json" ]; then
return 0
@@ -225,6 +252,8 @@ build_frontend_assets() {
return 0
fi
local needs_build=true
if [ "${mode}" = "auto" ]; then
local required_entries=(
"portal.js"
"wip-overview.js"
@@ -242,7 +271,7 @@ build_frontend_assets() {
"qc-gate.js"
"mid-section-defect.js"
)
local needs_build=false
needs_build=false
local newest_entry=""
for entry in "${required_entries[@]}"; do
@@ -264,15 +293,20 @@ build_frontend_assets() {
fi
if [ "$needs_build" = false ]; then
log_success "Frontend assets are up to date"
log_success "Frontend assets are up to date (FRONTEND_BUILD_MODE=auto)"
return 0
fi
fi
log_info "Building frontend assets with Vite..."
log_info "Building frontend assets with Vite (FRONTEND_BUILD_MODE=${mode})..."
if npm --prefix "${ROOT}/frontend" run build >/dev/null 2>&1; then
log_success "Frontend assets built"
else
log_warn "Frontend build failed; continuing with fallback inline scripts"
if is_enabled "${fail_on_error}"; then
log_error "Frontend build failed; aborting startup (FRONTEND_BUILD_FAIL_ON_ERROR=${fail_on_error})"
return 1
fi
log_warn "Frontend build failed; continuing startup (FRONTEND_BUILD_FAIL_ON_ERROR=${fail_on_error})"
fi
}
@@ -594,7 +628,7 @@ do_start() {
PORT=$(echo "${GUNICORN_BIND:-0.0.0.0:8080}" | cut -d: -f2)
export PYTHONPATH="${ROOT}/src:${PYTHONPATH:-}"
cd "$ROOT"
build_frontend_assets
build_frontend_assets || return 1
# Log startup
echo "[$(timestamp)] Starting server" >> "$STARTUP_LOG"

View File

@@ -44,6 +44,14 @@ from mes_dashboard.services.realtime_equipment_cache import (
init_realtime_equipment_cache,
stop_equipment_status_sync_worker,
)
from mes_dashboard.core.modernization_policy import (
get_deferred_routes as get_deferred_routes_from_scope_matrix,
get_missing_in_scope_assets,
is_asset_readiness_enforced,
missing_in_scope_asset_response,
maybe_redirect_to_canonical_shell,
)
from mes_dashboard.core.feature_flags import resolve_bool_flag
from mes_dashboard.core.redis_client import close_redis
from mes_dashboard.core.runtime_contract import build_runtime_contract_diagnostics
@@ -51,7 +59,7 @@ from mes_dashboard.core.runtime_contract import build_runtime_contract_diagnosti
_SHUTDOWN_LOCK = threading.Lock()
_ATEXIT_REGISTERED = False
_SHELL_ROUTE_CONTRACT_LOCK = threading.Lock()
_SHELL_ROUTE_CONTRACT_ROUTES: set[str] | None = None
_SHELL_ROUTE_CONTRACT_MAP: dict[str, dict[str, object]] | None = None
def _configure_logging(app: Flask) -> None:
@@ -149,10 +157,11 @@ def _resolve_portal_spa_enabled(app: Flask) -> bool:
Environment variable takes precedence so operators can toggle behavior
without code changes during migration rehearsal/cutover.
"""
raw = os.getenv("PORTAL_SPA_ENABLED")
if raw is None:
return bool(app.config.get("PORTAL_SPA_ENABLED", False))
return raw.strip().lower() in {"1", "true", "yes", "on"}
return resolve_bool_flag(
"PORTAL_SPA_ENABLED",
config=app.config,
default=bool(app.config.get("PORTAL_SPA_ENABLED", False)),
)
def _can_view_page_for_user(route: str, *, is_admin: bool) -> bool:
@@ -172,40 +181,95 @@ def _safe_order(value: object, default: int = 9999) -> int:
return default
def _load_shell_route_contract_routes() -> set[str]:
"""Load shell route contract routes used for navigation diagnostics."""
global _SHELL_ROUTE_CONTRACT_ROUTES
def _load_shell_route_contract_map() -> dict[str, dict[str, object]]:
"""Load shell route contract map used for navigation diagnostics."""
global _SHELL_ROUTE_CONTRACT_MAP
with _SHELL_ROUTE_CONTRACT_LOCK:
if _SHELL_ROUTE_CONTRACT_ROUTES is not None:
return _SHELL_ROUTE_CONTRACT_ROUTES
if _SHELL_ROUTE_CONTRACT_MAP is not None:
return _SHELL_ROUTE_CONTRACT_MAP
contract_file = (
Path(__file__).resolve().parents[2]
project_root = Path(__file__).resolve().parents[2]
contract_candidates = [
project_root
/ "docs"
/ "migration"
/ "full-modernization-architecture-blueprint"
/ "route_contracts.json",
project_root
/ "docs"
/ "migration"
/ "portal-shell-route-view-integration"
/ "route_migration_contract.json"
)
if not contract_file.exists():
_SHELL_ROUTE_CONTRACT_ROUTES = set()
return _SHELL_ROUTE_CONTRACT_ROUTES
/ "route_migration_contract.json",
]
contract_map: dict[str, dict[str, object]] = {}
logger = logging.getLogger("mes_dashboard")
for index, contract_file in enumerate(contract_candidates):
if not contract_file.exists():
continue
try:
payload = json.loads(contract_file.read_text(encoding="utf-8"))
routes = payload.get("routes", [])
_SHELL_ROUTE_CONTRACT_ROUTES = {
str(item.get("route", "")).strip()
for item in routes
if isinstance(item, dict) and str(item.get("route", "")).strip().startswith("/")
}
except Exception as exc:
logging.getLogger("mes_dashboard").warning(
"Failed to load shell route contract for diagnostics: %s",
for item in routes:
if not isinstance(item, dict):
continue
route = str(item.get("route", "")).strip()
if route.startswith("/"):
contract_map[route] = dict(item)
if contract_map:
if index > 0:
logger.warning(
"Using legacy contract file fallback for shell route contracts: %s",
contract_file,
)
break
except Exception as exc: # pragma: no cover - defensive logging
logger.warning(
"Failed to load shell route contract from %s: %s",
contract_file,
exc,
)
_SHELL_ROUTE_CONTRACT_ROUTES = set()
return _SHELL_ROUTE_CONTRACT_ROUTES
_SHELL_ROUTE_CONTRACT_MAP = contract_map
return _SHELL_ROUTE_CONTRACT_MAP
def _load_shell_route_contract_routes() -> set[str]:
return set(_load_shell_route_contract_map().keys())
def _load_shell_deferred_routes() -> set[str]:
contract_map = _load_shell_route_contract_map()
deferred_from_contract = {
route
for route, metadata in contract_map.items()
if str(metadata.get("scope", "")).strip() == "deferred"
}
deferred_from_scope = set(get_deferred_routes_from_scope_matrix())
return deferred_from_contract | deferred_from_scope
def _validate_in_scope_asset_readiness(app: Flask) -> None:
"""Validate in-scope dist assets and enforce fail-fast policy when configured."""
dist_dir = Path(app.static_folder or "") / "dist"
missing_assets = get_missing_in_scope_assets(dist_dir)
diagnostics = {
"dist_dir": str(dist_dir),
"missing_in_scope_assets": missing_assets,
"enforced": False,
}
app.extensions["asset_readiness"] = diagnostics
if not missing_assets:
return
with app.app_context():
enforced = is_asset_readiness_enforced()
diagnostics["enforced"] = enforced
message = "In-scope asset readiness check failed: " + ", ".join(missing_assets)
if enforced:
raise RuntimeError(message)
logging.getLogger("mes_dashboard").warning(message)
def _shutdown_runtime_resources() -> None:
@@ -251,10 +315,11 @@ def _register_shutdown_hooks(app: Flask) -> None:
def _is_runtime_contract_enforced(app: Flask) -> bool:
raw = os.getenv("RUNTIME_CONTRACT_ENFORCE")
if raw is not None:
return raw.strip().lower() in {"1", "true", "yes", "on"}
return _is_production_env(app)
return resolve_bool_flag(
"RUNTIME_CONTRACT_ENFORCE",
config=app.config,
default=_is_production_env(app),
)
def _validate_runtime_contract(app: Flask) -> None:
@@ -299,6 +364,7 @@ def create_app(config_name: str | None = None) -> Flask:
# Configure logging first
_configure_logging(app)
_validate_runtime_contract(app)
_validate_in_scope_asset_readiness(app)
security_headers = _build_security_headers(_is_production_env(app))
# Route-level cache backend (L1 memory + optional L2 Redis)
@@ -486,6 +552,7 @@ def create_app(config_name: str | None = None) -> Flask:
source = get_navigation_config()
drawers: list[dict] = []
shell_contract_routes = _load_shell_route_contract_routes()
deferred_routes = _load_shell_deferred_routes()
diagnostics: dict[str, object] = {
"filtered_drawers": 0,
"filtered_pages": 0,
@@ -546,7 +613,7 @@ def create_app(config_name: str | None = None) -> Flask:
diagnostics["filtered_pages"] = int(diagnostics["filtered_pages"]) + 1
continue
if shell_contract_routes and route not in shell_contract_routes:
if shell_contract_routes and route not in shell_contract_routes and route not in deferred_routes:
mismatch_routes.add(route)
nav_logger.warning(
"Navigation route missing shell contract: drawer_id=%s route=%s",
@@ -632,56 +699,68 @@ def create_app(config_name: str | None = None) -> Flask:
@app.route('/wip-overview')
def wip_overview_page():
"""WIP Overview Dashboard served as pure Vite HTML output."""
canonical_redirect = maybe_redirect_to_canonical_shell('/wip-overview')
if canonical_redirect is not None:
return canonical_redirect
dist_dir = os.path.join(app.static_folder or "", "dist")
dist_html = os.path.join(dist_dir, "wip-overview.html")
if os.path.exists(dist_html):
return send_from_directory(dist_dir, 'wip-overview.html')
# Test/local fallback when frontend build artifacts are absent.
return (
return missing_in_scope_asset_response('/wip-overview', (
"<!doctype html><html lang=\"zh-Hant\"><head><meta charset=\"UTF-8\">"
"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">"
"<title>WIP Overview Dashboard</title>"
"<script type=\"module\" src=\"/static/dist/wip-overview.js\"></script>"
"</head><body><div id='app'></div></body></html>",
200,
)
))
@app.route('/wip-detail')
def wip_detail_page():
"""WIP Detail Dashboard served as pure Vite HTML output."""
canonical_redirect = maybe_redirect_to_canonical_shell('/wip-detail')
if canonical_redirect is not None:
return canonical_redirect
dist_dir = os.path.join(app.static_folder or "", "dist")
dist_html = os.path.join(dist_dir, "wip-detail.html")
if os.path.exists(dist_html):
return send_from_directory(dist_dir, 'wip-detail.html')
# Test/local fallback when frontend build artifacts are absent.
return (
return missing_in_scope_asset_response('/wip-detail', (
"<!doctype html><html lang=\"zh-Hant\"><head><meta charset=\"UTF-8\">"
"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">"
"<title>WIP Detail Dashboard</title>"
"<script type=\"module\" src=\"/static/dist/wip-detail.js\"></script>"
"</head><body><div id='app'></div></body></html>",
200,
)
))
@app.route('/resource')
def resource_page():
"""Resource status report page served as pure Vite HTML output."""
canonical_redirect = maybe_redirect_to_canonical_shell('/resource')
if canonical_redirect is not None:
return canonical_redirect
dist_dir = os.path.join(app.static_folder or "", "dist")
dist_html = os.path.join(dist_dir, "resource-status.html")
if os.path.exists(dist_html):
return send_from_directory(dist_dir, 'resource-status.html')
# Test/local fallback when frontend build artifacts are absent.
return (
return missing_in_scope_asset_response('/resource', (
"<!doctype html><html lang=\"zh-Hant\"><head><meta charset=\"UTF-8\">"
"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">"
"<title>設備即時概況</title>"
"<script type=\"module\" src=\"/static/dist/resource-status.js\"></script>"
"</head><body><div id='app'></div></body></html>",
200,
)
))
@app.route('/excel-query')
def excel_query_page():
@@ -691,32 +770,54 @@ def create_app(config_name: str | None = None) -> Flask:
@app.route('/resource-history')
def resource_history_page():
"""Resource history analysis page served as pure Vite HTML output."""
canonical_redirect = maybe_redirect_to_canonical_shell('/resource-history')
if canonical_redirect is not None:
return canonical_redirect
dist_dir = os.path.join(app.static_folder or "", "dist")
dist_html = os.path.join(dist_dir, "resource-history.html")
if os.path.exists(dist_html):
return send_from_directory(dist_dir, 'resource-history.html')
# Test/local fallback when frontend build artifacts are absent.
return (
return missing_in_scope_asset_response('/resource-history', (
"<!doctype html><html lang=\"zh-Hant\"><head><meta charset=\"UTF-8\">"
"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">"
"<title>設備歷史績效</title>"
"<script type=\"module\" src=\"/static/dist/resource-history.js\"></script>"
"</head><body><div id='app'></div></body></html>",
200,
)
))
@app.route('/tmtt-defect')
def tmtt_defect_page():
"""TMTT printing & lead form defect analysis page."""
canonical_redirect = maybe_redirect_to_canonical_shell('/tmtt-defect')
if canonical_redirect is not None:
return canonical_redirect
return render_template('tmtt_defect.html')
@app.route('/qc-gate')
def qc_gate_page():
"""QC-GATE status report served as pure Vite HTML output."""
canonical_redirect = maybe_redirect_to_canonical_shell('/qc-gate')
if canonical_redirect is not None:
return canonical_redirect
dist_dir = os.path.join(app.static_folder or "", "dist")
dist_html = os.path.join(dist_dir, "qc-gate.html")
if os.path.exists(dist_html):
return send_from_directory(dist_dir, 'qc-gate.html')
return missing_in_scope_asset_response('/qc-gate', (
"<!doctype html><html lang=\"zh-Hant\"><head><meta charset=\"UTF-8\">"
"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">"
"<title>QC-GATE 狀態</title>"
"<script type=\"module\" src=\"/static/dist/qc-gate.js\"></script>"
"</head><body><div id='app'></div></body></html>",
200,
))
@app.route('/mid-section-defect')
def mid_section_defect_page():
"""Mid-section defect traceability analysis page (pure Vite)."""

View File

@@ -0,0 +1,49 @@
# -*- coding: utf-8 -*-
"""Shared helpers for boolean parsing and feature-flag resolution."""
from __future__ import annotations
import os
from typing import Any, Mapping
_TRUE_VALUES = {"1", "true", "yes", "on"}
_FALSE_VALUES = {"0", "false", "no", "off"}
def parse_bool(value: Any, default: bool = False) -> bool:
"""Parse bool-like values with explicit true/false token support."""
if value is None:
return default
if isinstance(value, bool):
return value
text = str(value).strip().lower()
if not text:
return default
if text in _TRUE_VALUES:
return True
if text in _FALSE_VALUES:
return False
return default
def resolve_bool_flag(
env_key: str,
*,
config: Mapping[str, Any] | None = None,
config_key: str | None = None,
default: bool = False,
environ: Mapping[str, str] | None = None,
) -> bool:
"""Resolve bool flag using precedence: environment > config > default."""
env = environ or os.environ
env_value = env.get(env_key)
if env_value is not None:
return parse_bool(env_value, default=default)
cfg = config or {}
key = config_key or env_key
if key in cfg:
return parse_bool(cfg.get(key), default=default)
return default

View File

@@ -0,0 +1,167 @@
# -*- coding: utf-8 -*-
"""Modernization policy helpers shared by routes and release gates."""
from __future__ import annotations
import copy
import json
from functools import lru_cache
from pathlib import Path
from typing import Any
from flask import current_app, redirect, render_template, request
from mes_dashboard.core.feature_flags import resolve_bool_flag
PROJECT_ROOT = Path(__file__).resolve().parents[3]
DOCS_DIR = PROJECT_ROOT / "docs" / "migration" / "full-modernization-architecture-blueprint"
SCOPE_MATRIX_FILE = DOCS_DIR / "route_scope_matrix.json"
ASSET_MANIFEST_FILE = DOCS_DIR / "asset_readiness_manifest.json"
def _read_json(path: Path, fallback: dict[str, Any]) -> dict[str, Any]:
if not path.exists():
return dict(fallback)
try:
return json.loads(path.read_text(encoding="utf-8"))
except (json.JSONDecodeError, OSError):
return dict(fallback)
@lru_cache(maxsize=1)
def _load_scope_matrix_cached() -> dict[str, Any]:
return _read_json(SCOPE_MATRIX_FILE, fallback={"in_scope": [], "deferred": []})
@lru_cache(maxsize=1)
def _load_asset_manifest_cached() -> dict[str, Any]:
return _read_json(ASSET_MANIFEST_FILE, fallback={"in_scope_required_assets": {}, "deferred_routes": []})
def clear_modernization_policy_cache() -> None:
"""Clear in-process policy caches for tests/controlled refresh flows.
Runtime expects these policy artifacts to be refreshed by worker restart.
Keep this helper for explicit test setup or operational maintenance hooks.
"""
_load_scope_matrix_cached.cache_clear()
_load_asset_manifest_cached.cache_clear()
def load_scope_matrix() -> dict[str, Any]:
# Defensive copy prevents callers from mutating the shared cached payload.
return copy.deepcopy(_load_scope_matrix_cached())
def load_asset_manifest() -> dict[str, Any]:
# Defensive copy prevents callers from mutating the shared cached payload.
return copy.deepcopy(_load_asset_manifest_cached())
def get_in_scope_routes() -> list[str]:
matrix = load_scope_matrix()
routes = []
for item in matrix.get("in_scope", []):
route = str(item.get("route", "")).strip()
if route.startswith("/"):
routes.append(route)
return routes
def get_in_scope_report_routes() -> list[str]:
matrix = load_scope_matrix()
routes = []
for item in matrix.get("in_scope", []):
route = str(item.get("route", "")).strip()
category = str(item.get("category", "")).strip().lower()
if route.startswith("/") and category == "report":
routes.append(route)
return routes
def get_deferred_routes() -> list[str]:
matrix = load_scope_matrix()
routes = []
for item in matrix.get("deferred", []):
route = str(item.get("route", "")).strip()
if route.startswith("/"):
routes.append(route)
return routes
def is_in_scope_route(route: str) -> bool:
return route in set(get_in_scope_routes())
def is_in_scope_report_route(route: str) -> bool:
return route in set(get_in_scope_report_routes())
def is_deferred_route(route: str) -> bool:
return route in set(get_deferred_routes())
def canonical_shell_path(route: str) -> str:
normalized = str(route or "").strip()
if not normalized.startswith("/"):
normalized = f"/{normalized}"
return f"/portal-shell{normalized}"
def should_apply_canonical_redirect() -> bool:
# Canonical direct-entry compatibility policy only applies in shell-first mode.
return bool(current_app.config.get("PORTAL_SPA_ENABLED", False))
def maybe_redirect_to_canonical_shell(route: str):
# Intentional scope boundary: canonical redirects are for in-scope report routes only.
# Admin routes are rendered as shell external targets; forcing canonical redirects for
# those routes can create redirect loops between shell and legacy entry points.
if not should_apply_canonical_redirect():
return None
if not is_in_scope_report_route(route):
return None
target = canonical_shell_path(route)
query_string = request.query_string.decode("utf-8")
if query_string:
target = f"{target}?{query_string}"
return redirect(target, code=302)
def is_runtime_fallback_retired_for_route(route: str) -> bool:
retired = resolve_bool_flag(
"MODERNIZATION_RETIRE_IN_SCOPE_RUNTIME_FALLBACK",
config=current_app.config,
default=bool(current_app.config.get("MODERNIZATION_RETIRE_IN_SCOPE_RUNTIME_FALLBACK", False)),
)
return retired and is_in_scope_route(route)
def is_asset_readiness_enforced() -> bool:
return resolve_bool_flag(
"MODERNIZATION_ENFORCE_ASSET_READINESS",
config=current_app.config,
default=bool(current_app.config.get("MODERNIZATION_ENFORCE_ASSET_READINESS", False)),
)
def get_missing_in_scope_assets(dist_dir: Path) -> list[str]:
manifest = load_asset_manifest()
required_assets = manifest.get("in_scope_required_assets", {})
missing: list[str] = []
for route, assets in required_assets.items():
for filename in assets:
target = dist_dir / filename
if not target.exists():
missing.append(f"{route}:{filename}")
return sorted(set(missing))
def missing_in_scope_asset_response(route: str, fallback_response: Any):
"""Apply retired-fallback policy with shared response contract."""
if is_runtime_fallback_retired_for_route(route):
return render_template("500.html"), 503
return fallback_response

View File

@@ -8,16 +8,12 @@ import shutil
from pathlib import Path
from typing import Any, Mapping
from mes_dashboard.core.feature_flags import resolve_bool_flag
CONTRACT_VERSION = "2026.02-p2"
DEFAULT_PROJECT_ROOT = Path(__file__).resolve().parents[3]
def _to_bool(value: str | None, default: bool) -> bool:
if value is None:
return default
return value.strip().lower() in {"1", "true", "yes", "on"}
def _resolve_path(value: str | None, fallback: Path, project_root: Path) -> Path:
if value is None or not str(value).strip():
return fallback.resolve()
@@ -68,7 +64,12 @@ def load_runtime_contract(
"watchdog_pid_file": str(pid_file),
"watchdog_state_file": str(state_file),
"watchdog_check_interval": int(env.get("WATCHDOG_CHECK_INTERVAL", "5")),
"validation_enforced": _to_bool(env.get("RUNTIME_CONTRACT_ENFORCE"), False),
"validation_enforced": resolve_bool_flag(
"RUNTIME_CONTRACT_ENFORCE",
config={},
default=False,
environ=env,
),
}
return contract

View File

@@ -10,6 +10,10 @@ from typing import Optional, Tuple
from flask import Blueprint, current_app, jsonify, request, send_from_directory
from mes_dashboard.core.rate_limit import configured_rate_limit
from mes_dashboard.core.modernization_policy import (
missing_in_scope_asset_response,
maybe_redirect_to_canonical_shell,
)
from mes_dashboard.services.hold_history_service import (
get_hold_history_duration,
get_hold_history_list,
@@ -88,19 +92,23 @@ def _parse_record_type(default: str = 'new') -> tuple[Optional[str], Optional[tu
@hold_history_bp.route('/hold-history')
def hold_history_page():
"""Render Hold History page from static Vite output."""
canonical_redirect = maybe_redirect_to_canonical_shell('/hold-history')
if canonical_redirect is not None:
return canonical_redirect
dist_dir = os.path.join(current_app.static_folder or '', 'dist')
dist_html = os.path.join(dist_dir, 'hold-history.html')
if os.path.exists(dist_html):
return send_from_directory(dist_dir, 'hold-history.html')
return (
return missing_in_scope_asset_response('/hold-history', (
'<!doctype html><html lang="zh-Hant"><head><meta charset="UTF-8">'
'<meta name="viewport" content="width=device-width, initial-scale=1.0">'
'<title>Hold History</title>'
'<script type="module" src="/static/dist/hold-history.js"></script>'
'</head><body><div id="app"></div></body></html>',
200,
)
))
@hold_history_bp.route('/api/hold-history/trend')

View File

@@ -8,6 +8,10 @@ from flask import Blueprint, current_app, jsonify, request, send_from_directory
from mes_dashboard.core.rate_limit import configured_rate_limit
from mes_dashboard.core.utils import parse_bool_query
from mes_dashboard.core.modernization_policy import (
missing_in_scope_asset_response,
maybe_redirect_to_canonical_shell,
)
from mes_dashboard.services.wip_service import (
get_hold_detail_lots,
get_hold_detail_summary,
@@ -53,19 +57,23 @@ def _parse_hold_type(default: str = 'quality') -> tuple[Optional[str], Optional[
@hold_overview_bp.route('/hold-overview')
def hold_overview_page():
"""Render hold overview page from static Vite output."""
canonical_redirect = maybe_redirect_to_canonical_shell('/hold-overview')
if canonical_redirect is not None:
return canonical_redirect
dist_dir = os.path.join(current_app.static_folder or "", "dist")
dist_html = os.path.join(dist_dir, "hold-overview.html")
if os.path.exists(dist_html):
return send_from_directory(dist_dir, 'hold-overview.html')
return (
return missing_in_scope_asset_response('/hold-overview', (
"<!doctype html><html lang=\"zh-Hant\"><head><meta charset=\"UTF-8\">"
"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">"
"<title>Hold Overview</title>"
"<script type=\"module\" src=\"/static/dist/hold-overview.js\"></script>"
"</head><body><div id='app'></div></body></html>",
200,
)
))
@hold_overview_bp.route('/api/hold-overview/summary')

View File

@@ -11,6 +11,10 @@ from flask import Blueprint, current_app, jsonify, redirect, request, send_from_
from mes_dashboard.core.rate_limit import configured_rate_limit
from mes_dashboard.core.utils import parse_bool_query
from mes_dashboard.core.modernization_policy import (
missing_in_scope_asset_response,
maybe_redirect_to_canonical_shell,
)
from mes_dashboard.services.wip_service import (
get_hold_detail_summary,
get_hold_detail_distribution,
@@ -45,9 +49,16 @@ def hold_detail_page():
"""
reason = request.args.get('reason', '').strip()
if not reason:
# Redirect to WIP Overview when reason is missing
# Redirect to overview route; in SPA mode this becomes canonical shell URL.
overview_redirect = maybe_redirect_to_canonical_shell('/wip-overview')
if overview_redirect is not None:
return overview_redirect
return redirect('/wip-overview')
canonical_redirect = maybe_redirect_to_canonical_shell('/hold-detail')
if canonical_redirect is not None:
return canonical_redirect
# Keep server-side validation, then serve static Vite output directly.
dist_dir = os.path.join(current_app.static_folder or "", "dist")
dist_html = os.path.join(dist_dir, "hold-detail.html")
@@ -55,7 +66,7 @@ def hold_detail_page():
return send_from_directory(dist_dir, 'hold-detail.html')
safe_reason = html.escape(reason, quote=True)
return (
return missing_in_scope_asset_response('/hold-detail', (
"<!doctype html><html lang=\"zh-Hant\"><head><meta charset=\"UTF-8\">"
"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">"
f"<title>Hold Detail - {safe_reason}</title>"
@@ -63,7 +74,7 @@ def hold_detail_page():
f"<meta name=\"hold-reason\" content=\"{safe_reason}\">"
"</head><body><div id='app'></div></body></html>",
200,
)
))
# ============================================================

View File

@@ -12,6 +12,7 @@ import logging
from flask import Blueprint, jsonify, request, Response, render_template
from mes_dashboard.core.rate_limit import configured_rate_limit
from mes_dashboard.core.modernization_policy import maybe_redirect_to_canonical_shell
from mes_dashboard.services.job_query_service import (
get_jobs_by_resources,
get_job_txn_history,
@@ -49,6 +50,9 @@ _JOB_EXPORT_RATE_LIMIT = configured_rate_limit(
@job_query_bp.route('/job-query')
def job_query_page():
"""Render the job query page."""
canonical_redirect = maybe_redirect_to_canonical_shell('/job-query')
if canonical_redirect is not None:
return canonical_redirect
return render_template('job_query.html')

View File

@@ -22,6 +22,7 @@ os.environ.setdefault('WATCHDOG_STATE_FILE', os.path.join(_TMP_DIR, 'mes_dashboa
import mes_dashboard.core.database as db
from mes_dashboard.app import create_app
from mes_dashboard.core.modernization_policy import clear_modernization_policy_cache
@pytest.fixture
@@ -33,6 +34,14 @@ def app():
return app
@pytest.fixture(autouse=True)
def _reset_modernization_policy_cache():
"""Keep policy-cache state isolated across tests."""
clear_modernization_policy_cache()
yield
clear_modernization_policy_cache()
@pytest.fixture
def client(app):
"""Create test client."""

View File

@@ -130,13 +130,20 @@ class TestWipAndHoldPagesE2E:
back_href = page.locator("a.btn-back").get_attribute("href") or ""
parsed = urlparse(back_href)
params = parse_qs(parsed.query)
assert parsed.path == "/wip-overview"
assert parsed.path in {"/wip-overview", "/portal-shell/wip-overview"}
assert params.get("type", [None])[0] == "PJA3460"
assert params.get("status", [None])[0] in {"queue", "QUEUE"}
def test_hold_detail_without_reason_redirects_to_overview(self, page: Page, app_server: str):
nav_resp = _get_with_retry(f"{app_server}/api/portal/navigation", attempts=3, timeout=10.0)
nav_payload = nav_resp.json() if nav_resp.ok else {}
spa_enabled = bool(nav_payload.get("portal_spa_enabled"))
response = _get_with_retry(f"{app_server}/hold-detail", attempts=3, timeout=10.0)
assert response.status_code == 302
if spa_enabled:
assert response.headers.get("Location") == "/portal-shell/wip-overview"
else:
assert response.headers.get("Location") == "/wip-overview"
def test_hold_detail_calls_summary_distribution_and_lots(self, page: Page, app_server: str):

Some files were not shown because too many files have changed in this diff Show More