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:
35
.env.example
35
.env.example
@@ -181,9 +181,41 @@ WORKER_RESTART_COOLDOWN=60
|
|||||||
# Watchdog loop check interval in seconds
|
# Watchdog loop check interval in seconds
|
||||||
WATCHDOG_CHECK_INTERVAL=5
|
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
|
# 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
|
RUNTIME_CONTRACT_ENFORCE=false
|
||||||
|
|
||||||
|
# Health endpoint memo cache TTL in seconds
|
||||||
|
HEALTH_MEMO_TTL_SECONDS=5
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# Runtime Resilience Diagnostics Thresholds
|
# Runtime Resilience Diagnostics Thresholds
|
||||||
# ============================================================
|
# ============================================================
|
||||||
@@ -205,6 +237,3 @@ RESILIENCE_RESTART_CHURN_THRESHOLD=3
|
|||||||
# Example: https://example.com,https://app.example.com
|
# Example: https://example.com,https://app.example.com
|
||||||
# Set to * for development (not recommended for production)
|
# Set to * for development (not recommended for production)
|
||||||
CORS_ALLOWED_ORIGINS=
|
CORS_ALLOWED_ORIGINS=
|
||||||
|
|
||||||
# Health endpoint memo cache TTL in seconds
|
|
||||||
HEALTH_MEMO_TTL_SECONDS=5
|
|
||||||
|
|||||||
57
.github/workflows/full-modernization-gates.yml
vendored
Normal file
57
.github/workflows/full-modernization-gates.yml
vendored
Normal 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
|
||||||
10
README.md
10
README.md
@@ -5,6 +5,7 @@
|
|||||||
> 專案主執行根目錄:`DashBoard_vite/`
|
> 專案主執行根目錄:`DashBoard_vite/`
|
||||||
> 目前已移除舊版 `DashBoard/` 代碼,僅保留新架構。
|
> 目前已移除舊版 `DashBoard/` 代碼,僅保留新架構。
|
||||||
> 2026-02-11:`portal-shell-route-view-integration` 已完成並封存,Portal Shell 全面採用 no-iframe 的 SPA route-view 架構。
|
> 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) | ✅ 已完成 |
|
| QC-GATE 即時狀態報表(Vue 3 + Vite) | ✅ 已完成 |
|
||||||
| 數據表查詢頁面 Vue 3 遷移 | ✅ 已完成 |
|
| 數據表查詢頁面 Vue 3 遷移 | ✅ 已完成 |
|
||||||
| WIP 三頁 Vue 3 遷移(Overview/Detail/Hold Detail) | ✅ 已完成 |
|
| WIP 三頁 Vue 3 遷移(Overview/Detail/Hold Detail) | ✅ 已完成 |
|
||||||
|
| 全站現代化治理 Phase 1(路由治理/品質門檻/資產就緒/手動驗收) | ✅ 已完成(已封存) |
|
||||||
|
| Deferred routes 現代化(`/tables`、`/excel-query`、`/query-tool`、`/mid-section-defect`) | ⏳ 進行中(follow-up 提案) |
|
||||||
| WIP Overview/Detail 篩選條件保留 | ✅ 已完成 |
|
| WIP Overview/Detail 篩選條件保留 | ✅ 已完成 |
|
||||||
| 設備雙頁 Vue 3 遷移(Status/History) | ✅ 已完成 |
|
| 設備雙頁 Vue 3 遷移(Status/History) | ✅ 已完成 |
|
||||||
| 設備快取 DataFrame TTL 一致性修復 | ✅ 已完成 |
|
| 設備快取 DataFrame TTL 一致性修復 | ✅ 已完成 |
|
||||||
@@ -55,6 +58,7 @@
|
|||||||
|
|
||||||
## 開發歷史(Vite 重構後)
|
## 開發歷史(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:完成 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:完成 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` 條件,矩陣與明細篩選一致。
|
- 2026-02-11:完成設備雙頁級聯篩選(`/resource`、`/resource-history`)— 新增 Group/Family/Machine 多層篩選聯動,前後端支援 `resource_ids` 條件,矩陣與明細篩選一致。
|
||||||
@@ -116,6 +120,7 @@
|
|||||||
- 頁面架構與抽屜分類:`docs/page_architecture_map.md`
|
- 頁面架構與抽屜分類:`docs/page_architecture_map.md`
|
||||||
- 前端計算前移與 parity 規則:`docs/frontend_compute_shift_plan.md`
|
- 前端計算前移與 parity 規則:`docs/frontend_compute_shift_plan.md`
|
||||||
- Portal Shell route-view 遷移基線與驗收:`docs/migration/portal-shell-route-view-integration/`
|
- Portal Shell route-view 遷移基線與驗收:`docs/migration/portal-shell-route-view-integration/`
|
||||||
|
- 全站現代化架構 Phase 1 治理文件:`docs/migration/full-modernization-architecture-blueprint/`
|
||||||
- Hold 歷史頁資料口徑說明:`docs/hold_history.md`
|
- Hold 歷史頁資料口徑說明:`docs/hold_history.md`
|
||||||
- Cutover gates / rollout / rollback:`docs/migration_gates_and_runbook.md`
|
- Cutover gates / rollout / rollback:`docs/migration_gates_and_runbook.md`
|
||||||
- 環境依賴缺口與對策:`docs/environment_gaps_and_mitigation.md`
|
- 環境依賴缺口與對策:`docs/environment_gaps_and_mitigation.md`
|
||||||
@@ -163,6 +168,11 @@
|
|||||||
- LDAP API URL 啟動驗證:僅允許 `https` + host allowlist。
|
- LDAP API URL 啟動驗證:僅允許 `https` + host allowlist。
|
||||||
- 全域 security headers:CSP/X-Frame-Options/X-Content-Type-Options/Referrer-Policy(production 含 HSTS)。
|
- 全域 security headers:CSP/X-Frame-Options/X-Content-Type-Options/Referrer-Policy(production 含 HSTS)。
|
||||||
|
|
||||||
|
8. 現階段仍保留部分相容路徑(非 bug,屬於分期策略)
|
||||||
|
- in-scope 報表路由已改為 canonical shell redirect(直接進入 `/portal-shell/...`)。
|
||||||
|
- `portal-shell` 為目前主路由容器,尚不可移除。
|
||||||
|
- deferred scope(`/tables`、`/excel-query`、`/query-tool`、`/mid-section-defect`)與其相容模板/入口會保留到 follow-up 提案完成。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 快速開始
|
## 快速開始
|
||||||
|
|||||||
21
data/modernization_feature_flags.json
Normal file
21
data/modernization_feature_flags.json
Normal 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}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -88,7 +88,7 @@
|
|||||||
{
|
{
|
||||||
"route": "/tmtt-defect",
|
"route": "/tmtt-defect",
|
||||||
"name": "TMTT印字腳型不良分析",
|
"name": "TMTT印字腳型不良分析",
|
||||||
"status": "released",
|
"status": "dev",
|
||||||
"drawer_id": "dev-tools",
|
"drawer_id": "dev-tools",
|
||||||
"order": 5
|
"order": 5
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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/`.
|
||||||
@@ -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"]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"change": "full-modernization-architecture-blueprint",
|
||||||
|
"records": [],
|
||||||
|
"rule": "next route cutover is blocked until current route has approved manual sign-off"
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
@@ -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`
|
||||||
@@ -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"]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"mode": "block",
|
||||||
|
"errors": [],
|
||||||
|
"warnings": [],
|
||||||
|
"info": [],
|
||||||
|
"passed": true
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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"]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"generated_at": "2026-02-12T02:26:36.887797+00:00",
|
||||||
"source": "data/page_status.json",
|
"source": "data/page_status.json",
|
||||||
"admin": [
|
"admin": [
|
||||||
{
|
{
|
||||||
@@ -16,7 +17,7 @@
|
|||||||
{
|
{
|
||||||
"route": "/hold-overview",
|
"route": "/hold-overview",
|
||||||
"name": "Hold 即時概況",
|
"name": "Hold 即時概況",
|
||||||
"status": "dev",
|
"status": "released",
|
||||||
"order": 2
|
"order": 2
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -42,7 +43,7 @@
|
|||||||
{
|
{
|
||||||
"route": "/hold-history",
|
"route": "/hold-history",
|
||||||
"name": "Hold 歷史績效",
|
"name": "Hold 歷史績效",
|
||||||
"status": "dev",
|
"status": "released",
|
||||||
"order": 3
|
"order": 3
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -106,7 +107,7 @@
|
|||||||
{
|
{
|
||||||
"route": "/tmtt-defect",
|
"route": "/tmtt-defect",
|
||||||
"name": "TMTT印字腳型不良分析",
|
"name": "TMTT印字腳型不良分析",
|
||||||
"status": "released",
|
"status": "dev",
|
||||||
"order": 5
|
"order": 5
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -131,6 +132,12 @@
|
|||||||
"status": "released",
|
"status": "released",
|
||||||
"order": 1
|
"order": 1
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"route": "/hold-overview",
|
||||||
|
"name": "Hold 即時概況",
|
||||||
|
"status": "released",
|
||||||
|
"order": 2
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"route": "/resource",
|
"route": "/resource",
|
||||||
"name": "設備即時概況",
|
"name": "設備即時概況",
|
||||||
@@ -151,6 +158,12 @@
|
|||||||
"order": 2,
|
"order": 2,
|
||||||
"admin_only": false,
|
"admin_only": false,
|
||||||
"pages": [
|
"pages": [
|
||||||
|
{
|
||||||
|
"route": "/hold-history",
|
||||||
|
"name": "Hold 歷史績效",
|
||||||
|
"status": "released",
|
||||||
|
"order": 3
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"route": "/resource-history",
|
"route": "/resource-history",
|
||||||
"name": "設備歷史績效",
|
"name": "設備歷史績效",
|
||||||
|
|||||||
@@ -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",
|
"source": "data/page_status.json",
|
||||||
"admin": [
|
"admin": [
|
||||||
{
|
{
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
{
|
{
|
||||||
"route": "/hold-overview",
|
"route": "/hold-overview",
|
||||||
"name": "Hold 即時概況",
|
"name": "Hold 即時概況",
|
||||||
"status": "dev",
|
"status": "released",
|
||||||
"order": 2
|
"order": 2
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -43,7 +43,7 @@
|
|||||||
{
|
{
|
||||||
"route": "/hold-history",
|
"route": "/hold-history",
|
||||||
"name": "Hold 歷史績效",
|
"name": "Hold 歷史績效",
|
||||||
"status": "dev",
|
"status": "released",
|
||||||
"order": 3
|
"order": 3
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -107,7 +107,7 @@
|
|||||||
{
|
{
|
||||||
"route": "/tmtt-defect",
|
"route": "/tmtt-defect",
|
||||||
"name": "TMTT印字腳型不良分析",
|
"name": "TMTT印字腳型不良分析",
|
||||||
"status": "released",
|
"status": "dev",
|
||||||
"order": 5
|
"order": 5
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -132,6 +132,12 @@
|
|||||||
"status": "released",
|
"status": "released",
|
||||||
"order": 1
|
"order": 1
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"route": "/hold-overview",
|
||||||
|
"name": "Hold 即時概況",
|
||||||
|
"status": "released",
|
||||||
|
"order": 2
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"route": "/resource",
|
"route": "/resource",
|
||||||
"name": "設備即時概況",
|
"name": "設備即時概況",
|
||||||
@@ -152,6 +158,12 @@
|
|||||||
"order": 2,
|
"order": 2,
|
||||||
"admin_only": false,
|
"admin_only": false,
|
||||||
"pages": [
|
"pages": [
|
||||||
|
{
|
||||||
|
"route": "/hold-history",
|
||||||
|
"name": "Hold 歷史績效",
|
||||||
|
"status": "released",
|
||||||
|
"order": 3
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"route": "/resource-history",
|
"route": "/resource-history",
|
||||||
"name": "設備歷史績效",
|
"name": "設備歷史績效",
|
||||||
|
|||||||
@@ -22,17 +22,13 @@ const props = defineProps({
|
|||||||
|
|
||||||
const emit = defineEmits(['change']);
|
const emit = defineEmits(['change']);
|
||||||
|
|
||||||
const holdTypeModel = computed({
|
const HOLD_TYPE_OPTIONS = Object.freeze([
|
||||||
get() {
|
{ value: 'quality', label: '品質異常' },
|
||||||
return props.holdType || 'quality';
|
{ value: 'non-quality', label: '非品質異常' },
|
||||||
},
|
{ value: 'all', label: '全部' },
|
||||||
set(nextValue) {
|
]);
|
||||||
emit('change', {
|
|
||||||
holdType: nextValue || 'quality',
|
const holdTypeModel = computed(() => props.holdType || 'quality');
|
||||||
reason: props.reason || '',
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const reasonModel = computed({
|
const reasonModel = computed({
|
||||||
get() {
|
get() {
|
||||||
@@ -59,29 +55,44 @@ const reasonOptions = computed(() => {
|
|||||||
});
|
});
|
||||||
return items;
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="filter-bar card">
|
<section class="filter-bar card hold-overview-filter-bar">
|
||||||
<div class="filter-group hold-type-group">
|
<div class="filter-group hold-type-group hold-overview-hold-type-group">
|
||||||
<span class="filter-label">Hold Type</span>
|
<span class="filter-label">Hold Type</span>
|
||||||
<div class="radio-group">
|
<div class="hold-type-segment" role="radiogroup" aria-label="Hold Type">
|
||||||
<label class="radio-option" :class="{ active: holdTypeModel === 'quality' }">
|
<button
|
||||||
<input v-model="holdTypeModel" type="radio" value="quality" :disabled="disabled" />
|
v-for="item in HOLD_TYPE_OPTIONS"
|
||||||
<span>品質異常</span>
|
:key="item.value"
|
||||||
</label>
|
type="button"
|
||||||
<label class="radio-option" :class="{ active: holdTypeModel === 'non-quality' }">
|
role="radio"
|
||||||
<input v-model="holdTypeModel" type="radio" value="non-quality" :disabled="disabled" />
|
class="hold-type-btn"
|
||||||
<span>非品質異常</span>
|
:class="{ active: holdTypeModel === item.value }"
|
||||||
</label>
|
:aria-checked="holdTypeModel === item.value"
|
||||||
<label class="radio-option" :class="{ active: holdTypeModel === 'all' }">
|
:disabled="disabled"
|
||||||
<input v-model="holdTypeModel" type="radio" value="all" :disabled="disabled" />
|
@click="selectHoldType(item.value)"
|
||||||
<span>全部</span>
|
>
|
||||||
</label>
|
{{ item.label }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
<label class="filter-label" for="hold-overview-reason">Reason</label>
|
||||||
<select
|
<select
|
||||||
id="hold-overview-reason"
|
id="hold-overview-reason"
|
||||||
|
|||||||
@@ -56,45 +56,74 @@
|
|||||||
padding: 16px 20px;
|
padding: 16px 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hold-overview-filter-bar {
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
.filter-group {
|
.filter-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hold-overview-hold-type-group {
|
||||||
|
flex: 1 1 520px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hold-overview-reason-group {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
.filter-label {
|
.filter-label {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.radio-group {
|
.hold-type-segment {
|
||||||
display: flex;
|
display: grid;
|
||||||
gap: 12px;
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
flex-wrap: wrap;
|
gap: 8px;
|
||||||
|
min-width: 420px;
|
||||||
|
padding: 4px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #f8fafc;
|
||||||
}
|
}
|
||||||
|
|
||||||
.radio-option {
|
.hold-type-btn {
|
||||||
display: inline-flex;
|
appearance: none;
|
||||||
align-items: center;
|
border: none;
|
||||||
gap: 6px;
|
background: transparent;
|
||||||
padding: 6px 10px;
|
color: #475569;
|
||||||
border: 1px solid var(--border);
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.25;
|
||||||
|
padding: 10px 14px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 13px;
|
transition: background-color 0.15s ease, color 0.15s ease, box-shadow 0.15s ease;
|
||||||
background: #fff;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.radio-option.active {
|
.hold-type-btn:hover:not(:disabled) {
|
||||||
border-color: var(--primary);
|
|
||||||
background: #eef2ff;
|
background: #eef2ff;
|
||||||
color: var(--primary-dark);
|
color: #3730a3;
|
||||||
font-weight: 600;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.radio-option input {
|
.hold-type-btn.active {
|
||||||
margin: 0;
|
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 {
|
.reason-select {
|
||||||
@@ -353,6 +382,11 @@
|
|||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hold-type-segment {
|
||||||
|
min-width: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.reason-select {
|
.reason-select {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|||||||
@@ -9,8 +9,12 @@
|
|||||||
padding: 22px 24px;
|
padding: 22px 24px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background: linear-gradient(135deg, var(--portal-brand-start) 0%, var(--portal-brand-end) 100%);
|
background: linear-gradient(
|
||||||
box-shadow: var(--portal-shadow-panel);
|
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 {
|
.job-query-header h1 {
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ export function buildDynamicNavigationState(
|
|||||||
diagnostics.missingContractRoutes.push(page.route);
|
diagnostics.missingContractRoutes.push(page.route);
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderMode = 'native';
|
const renderMode = contract?.renderMode || 'native';
|
||||||
dynamicRoutes.push({
|
dynamicRoutes.push({
|
||||||
routeName: `shell-page-${index++}`,
|
routeName: `shell-page-${index++}`,
|
||||||
shellPath,
|
shellPath,
|
||||||
@@ -112,6 +112,9 @@ export function buildDynamicNavigationState(
|
|||||||
owner: contract?.owner || '',
|
owner: contract?.owner || '',
|
||||||
renderMode,
|
renderMode,
|
||||||
routeId: contract?.routeId || '',
|
routeId: contract?.routeId || '',
|
||||||
|
visibilityPolicy: contract?.visibilityPolicy || 'released_or_admin',
|
||||||
|
scope: contract?.scope || 'unknown',
|
||||||
|
compatibilityPolicy: contract?.compatibilityPolicy || 'legacy_direct_entry_allowed',
|
||||||
});
|
});
|
||||||
registeredRoutes.add(page.route);
|
registeredRoutes.add(page.route);
|
||||||
allowedPaths.push(shellPath);
|
allowedPaths.push(shellPath);
|
||||||
@@ -141,8 +144,11 @@ export function buildDynamicNavigationState(
|
|||||||
pageName: contract?.title || route,
|
pageName: contract?.title || route,
|
||||||
drawerName: '',
|
drawerName: '',
|
||||||
owner: contract?.owner || '',
|
owner: contract?.owner || '',
|
||||||
renderMode: 'native',
|
renderMode: contract?.renderMode || 'native',
|
||||||
routeId: contract?.routeId || '',
|
routeId: contract?.routeId || '',
|
||||||
|
visibilityPolicy: contract?.visibilityPolicy || 'released_or_admin',
|
||||||
|
scope: contract?.scope || 'unknown',
|
||||||
|
compatibilityPolicy: contract?.compatibilityPolicy || 'legacy_direct_entry_allowed',
|
||||||
});
|
});
|
||||||
registeredRoutes.add(route);
|
registeredRoutes.add(route);
|
||||||
allowedPaths.push(shellPath);
|
allowedPaths.push(shellPath);
|
||||||
|
|||||||
@@ -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({
|
const ROUTE_CONTRACTS = Object.freeze({
|
||||||
'/wip-overview': {
|
'/wip-overview': buildContract({
|
||||||
|
route: '/wip-overview',
|
||||||
routeId: 'wip-overview',
|
routeId: 'wip-overview',
|
||||||
renderMode: 'native',
|
renderMode: 'native',
|
||||||
owner: 'frontend-mes-reporting',
|
owner: 'frontend-mes-reporting',
|
||||||
title: 'WIP 即時概況',
|
title: 'WIP 即時概況',
|
||||||
rollbackStrategy: 'fallback_to_legacy_route',
|
rollbackStrategy: 'fallback_to_legacy_route',
|
||||||
},
|
visibilityPolicy: 'released_or_admin',
|
||||||
'/wip-detail': {
|
scope: 'in-scope',
|
||||||
|
compatibilityPolicy: 'redirect_to_shell_when_spa_enabled',
|
||||||
|
}),
|
||||||
|
'/wip-detail': buildContract({
|
||||||
|
route: '/wip-detail',
|
||||||
routeId: 'wip-detail',
|
routeId: 'wip-detail',
|
||||||
renderMode: 'native',
|
renderMode: 'native',
|
||||||
owner: 'frontend-mes-reporting',
|
owner: 'frontend-mes-reporting',
|
||||||
title: 'WIP 詳細列表',
|
title: 'WIP 詳細列表',
|
||||||
rollbackStrategy: 'fallback_to_legacy_route',
|
rollbackStrategy: 'fallback_to_legacy_route',
|
||||||
},
|
visibilityPolicy: 'released_or_admin',
|
||||||
'/hold-overview': {
|
scope: 'in-scope',
|
||||||
|
compatibilityPolicy: 'redirect_to_shell_when_spa_enabled',
|
||||||
|
}),
|
||||||
|
'/hold-overview': buildContract({
|
||||||
|
route: '/hold-overview',
|
||||||
routeId: 'hold-overview',
|
routeId: 'hold-overview',
|
||||||
renderMode: 'native',
|
renderMode: 'native',
|
||||||
owner: 'frontend-mes-reporting',
|
owner: 'frontend-mes-reporting',
|
||||||
title: 'Hold 即時概況',
|
title: 'Hold 即時概況',
|
||||||
rollbackStrategy: 'fallback_to_legacy_route',
|
rollbackStrategy: 'fallback_to_legacy_route',
|
||||||
},
|
visibilityPolicy: 'released_or_admin',
|
||||||
'/hold-detail': {
|
scope: 'in-scope',
|
||||||
|
compatibilityPolicy: 'redirect_to_shell_when_spa_enabled',
|
||||||
|
}),
|
||||||
|
'/hold-detail': buildContract({
|
||||||
|
route: '/hold-detail',
|
||||||
routeId: 'hold-detail',
|
routeId: 'hold-detail',
|
||||||
renderMode: 'native',
|
renderMode: 'native',
|
||||||
owner: 'frontend-mes-reporting',
|
owner: 'frontend-mes-reporting',
|
||||||
title: 'Hold 詳細查詢',
|
title: 'Hold 詳細查詢',
|
||||||
rollbackStrategy: 'fallback_to_legacy_route',
|
rollbackStrategy: 'fallback_to_legacy_route',
|
||||||
},
|
visibilityPolicy: 'released_or_admin',
|
||||||
'/hold-history': {
|
scope: 'in-scope',
|
||||||
|
compatibilityPolicy: 'redirect_to_shell_when_spa_enabled',
|
||||||
|
}),
|
||||||
|
'/hold-history': buildContract({
|
||||||
|
route: '/hold-history',
|
||||||
routeId: 'hold-history',
|
routeId: 'hold-history',
|
||||||
renderMode: 'native',
|
renderMode: 'native',
|
||||||
owner: 'frontend-mes-reporting',
|
owner: 'frontend-mes-reporting',
|
||||||
title: 'Hold 歷史報表',
|
title: 'Hold 歷史報表',
|
||||||
rollbackStrategy: 'fallback_to_legacy_route',
|
rollbackStrategy: 'fallback_to_legacy_route',
|
||||||
},
|
visibilityPolicy: 'released_or_admin',
|
||||||
'/resource': {
|
scope: 'in-scope',
|
||||||
|
compatibilityPolicy: 'redirect_to_shell_when_spa_enabled',
|
||||||
|
}),
|
||||||
|
'/resource': buildContract({
|
||||||
|
route: '/resource',
|
||||||
routeId: 'resource',
|
routeId: 'resource',
|
||||||
renderMode: 'native',
|
renderMode: 'native',
|
||||||
owner: 'frontend-mes-reporting',
|
owner: 'frontend-mes-reporting',
|
||||||
title: '設備即時狀況',
|
title: '設備即時狀況',
|
||||||
rollbackStrategy: 'fallback_to_legacy_route',
|
rollbackStrategy: 'fallback_to_legacy_route',
|
||||||
},
|
visibilityPolicy: 'released_or_admin',
|
||||||
'/resource-history': {
|
scope: 'in-scope',
|
||||||
|
compatibilityPolicy: 'redirect_to_shell_when_spa_enabled',
|
||||||
|
}),
|
||||||
|
'/resource-history': buildContract({
|
||||||
|
route: '/resource-history',
|
||||||
routeId: 'resource-history',
|
routeId: 'resource-history',
|
||||||
renderMode: 'native',
|
renderMode: 'native',
|
||||||
owner: 'frontend-mes-reporting',
|
owner: 'frontend-mes-reporting',
|
||||||
title: '設備歷史績效',
|
title: '設備歷史績效',
|
||||||
rollbackStrategy: 'fallback_to_legacy_route',
|
rollbackStrategy: 'fallback_to_legacy_route',
|
||||||
},
|
visibilityPolicy: 'released_or_admin',
|
||||||
'/qc-gate': {
|
scope: 'in-scope',
|
||||||
|
compatibilityPolicy: 'redirect_to_shell_when_spa_enabled',
|
||||||
|
}),
|
||||||
|
'/qc-gate': buildContract({
|
||||||
|
route: '/qc-gate',
|
||||||
routeId: 'qc-gate',
|
routeId: 'qc-gate',
|
||||||
renderMode: 'native',
|
renderMode: 'native',
|
||||||
owner: 'frontend-mes-reporting',
|
owner: 'frontend-mes-reporting',
|
||||||
title: 'QC-GATE 狀態',
|
title: 'QC-GATE 狀態',
|
||||||
rollbackStrategy: 'fallback_to_legacy_route',
|
rollbackStrategy: 'fallback_to_legacy_route',
|
||||||
},
|
visibilityPolicy: 'released_or_admin',
|
||||||
'/job-query': {
|
scope: 'in-scope',
|
||||||
|
compatibilityPolicy: 'redirect_to_shell_when_spa_enabled',
|
||||||
|
}),
|
||||||
|
'/job-query': buildContract({
|
||||||
|
route: '/job-query',
|
||||||
routeId: 'job-query',
|
routeId: 'job-query',
|
||||||
renderMode: 'native',
|
renderMode: 'native',
|
||||||
owner: 'frontend-mes-reporting',
|
owner: 'frontend-mes-reporting',
|
||||||
title: '設備維修查詢',
|
title: '設備維修查詢',
|
||||||
rollbackStrategy: 'fallback_to_legacy_route',
|
rollbackStrategy: 'fallback_to_legacy_route',
|
||||||
},
|
visibilityPolicy: 'released_or_admin',
|
||||||
'/excel-query': {
|
scope: 'in-scope',
|
||||||
routeId: 'excel-query',
|
compatibilityPolicy: 'redirect_to_shell_when_spa_enabled',
|
||||||
renderMode: 'native',
|
}),
|
||||||
owner: 'frontend-mes-reporting',
|
'/tmtt-defect': buildContract({
|
||||||
title: 'Excel 查詢工具',
|
route: '/tmtt-defect',
|
||||||
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': {
|
|
||||||
routeId: 'tmtt-defect',
|
routeId: 'tmtt-defect',
|
||||||
renderMode: 'native',
|
renderMode: 'native',
|
||||||
owner: 'frontend-mes-reporting',
|
owner: 'frontend-mes-reporting',
|
||||||
title: 'TMTT Defect',
|
title: 'TMTT Defect',
|
||||||
rollbackStrategy: 'fallback_to_legacy_route',
|
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) {
|
export function normalizeRoutePath(route) {
|
||||||
const normalized = String(route || '').trim();
|
const normalized = String(route || '').trim();
|
||||||
if (!normalized || normalized === '/') {
|
if (!normalized || normalized === '/') {
|
||||||
@@ -100,3 +265,51 @@ export function getRouteContract(route) {
|
|||||||
export function getRouteContractMap() {
|
export function getRouteContractMap() {
|
||||||
return ROUTE_CONTRACTS;
|
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();
|
||||||
|
}
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ export function syncNavigationRoutes(
|
|||||||
pageName: entry.pageName,
|
pageName: entry.pageName,
|
||||||
drawerName: entry.drawerName,
|
drawerName: entry.drawerName,
|
||||||
owner: entry.owner,
|
owner: entry.owner,
|
||||||
|
renderMode: entry.renderMode,
|
||||||
},
|
},
|
||||||
meta: {
|
meta: {
|
||||||
title: entry.pageName,
|
title: entry.pageName,
|
||||||
@@ -65,6 +66,9 @@ export function syncNavigationRoutes(
|
|||||||
targetRoute: entry.targetRoute,
|
targetRoute: entry.targetRoute,
|
||||||
renderMode: entry.renderMode,
|
renderMode: entry.renderMode,
|
||||||
routeId: entry.routeId,
|
routeId: entry.routeId,
|
||||||
|
visibilityPolicy: entry.visibilityPolicy,
|
||||||
|
scope: entry.scope,
|
||||||
|
compatibilityPolicy: entry.compatibilityPolicy,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
dynamicRouteNames.push(entry.routeName);
|
dynamicRouteNames.push(entry.routeName);
|
||||||
|
|||||||
@@ -21,6 +21,10 @@ const props = defineProps({
|
|||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
|
renderMode: {
|
||||||
|
type: String,
|
||||||
|
default: 'native',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
@@ -78,6 +82,12 @@ async function loadNativeModule(route) {
|
|||||||
moduleError.value = '';
|
moduleError.value = '';
|
||||||
resolvedComponent.value = null;
|
resolvedComponent.value = null;
|
||||||
|
|
||||||
|
if (props.renderMode === 'external') {
|
||||||
|
moduleLoading.value = false;
|
||||||
|
openLegacyPage();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const loader = getNativeModuleLoader(route);
|
const loader = getNativeModuleLoader(route);
|
||||||
if (!loader) {
|
if (!loader) {
|
||||||
moduleLoading.value = false;
|
moduleLoading.value = false;
|
||||||
@@ -101,8 +111,8 @@ async function loadNativeModule(route) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.targetRoute,
|
() => [props.targetRoute, props.renderMode],
|
||||||
(route) => {
|
([route]) => {
|
||||||
void loadNativeModule(route);
|
void loadNativeModule(route);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,34 +1,28 @@
|
|||||||
:root {
|
.qc-gate-page,
|
||||||
|
.qc-gate-page * {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qc-gate-page {
|
||||||
--bg: #f5f7fa;
|
--bg: #f5f7fa;
|
||||||
--card-bg: #ffffff;
|
--card-bg: #ffffff;
|
||||||
--text: #1f2937;
|
--text: #1f2937;
|
||||||
--muted: #64748b;
|
--muted: #64748b;
|
||||||
--border: #dbe3ef;
|
--border: #dbe3ef;
|
||||||
--header-from: #667eea;
|
--header-from: var(--portal-brand-start, #6366f1);
|
||||||
--header-to: #764ba2;
|
--header-to: var(--portal-brand-end, #7c3aed);
|
||||||
--success: #22c55e;
|
--success: #22c55e;
|
||||||
--warning: #facc15;
|
--warning: #facc15;
|
||||||
--danger: #ef4444;
|
--danger: #ef4444;
|
||||||
--shadow: 0 2px 10px rgba(15, 23, 42, 0.08);
|
--shadow: 0 2px 10px rgba(15, 23, 42, 0.08);
|
||||||
--shadow-strong: 0 6px 24px rgba(102, 126, 234, 0.24);
|
--shadow-strong: 0 6px 24px rgba(102, 126, 234, 0.24);
|
||||||
}
|
margin: 0 auto;
|
||||||
|
|
||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
font-family: 'Microsoft JhengHei', Arial, sans-serif;
|
font-family: 'Microsoft JhengHei', Arial, sans-serif;
|
||||||
background: var(--bg);
|
background: var(--bg);
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
}
|
|
||||||
|
|
||||||
.qc-gate-page {
|
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
max-width: 1900px;
|
max-width: 1900px;
|
||||||
margin: 0 auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.qc-gate-header {
|
.qc-gate-header {
|
||||||
|
|||||||
@@ -9,8 +9,12 @@
|
|||||||
padding: 22px 24px;
|
padding: 22px 24px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background: linear-gradient(135deg, var(--portal-brand-start) 0%, var(--portal-brand-end) 100%);
|
background: linear-gradient(
|
||||||
box-shadow: var(--portal-shadow-panel);
|
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 {
|
.tmtt-header h1 {
|
||||||
@@ -113,7 +117,7 @@
|
|||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
border-left: 4px solid #64748b;
|
border-left: 4px solid #64748b;
|
||||||
background: #fff;
|
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 {
|
.tmtt-kpi-card.tone-danger {
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { readFileSync } from 'node:fs';
|
import { existsSync, readFileSync } from 'node:fs';
|
||||||
import { resolve } from 'node:path';
|
import { resolve } from 'node:path';
|
||||||
|
|
||||||
function readSource(relativePath) {
|
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', () => {
|
test('portal shell app renders health summary component and admin entry controls', () => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { readFileSync } from 'node:fs';
|
import { existsSync, readFileSync } from 'node:fs';
|
||||||
import { resolve } from 'node:path';
|
import { resolve } from 'node:path';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -10,7 +10,11 @@ import {
|
|||||||
} from '../src/portal-shell/healthSummary.js';
|
} from '../src/portal-shell/healthSummary.js';
|
||||||
|
|
||||||
function readSource(relativePath) {
|
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');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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('/wip-detail'), true);
|
||||||
assert.equal(state.allowedPaths.includes('/hold-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');
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { readFileSync } from 'node:fs';
|
import { existsSync, readFileSync } from 'node:fs';
|
||||||
import { resolve } from 'node:path';
|
import { resolve } from 'node:path';
|
||||||
|
|
||||||
function readSource(relativePath) {
|
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', () => {
|
test('shell route host sources do not contain iframe rendering paths', () => {
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { readFileSync } from 'node:fs';
|
import { existsSync, readFileSync } from 'node:fs';
|
||||||
import { resolve } from 'node:path';
|
import { resolve } from 'node:path';
|
||||||
|
|
||||||
function readSource(relativePath) {
|
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', () => {
|
test('table parity: Wave B native pages keep deterministic column and empty-state handling', () => {
|
||||||
|
|||||||
@@ -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}`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { readFileSync } from 'node:fs';
|
import { existsSync, readFileSync } from 'node:fs';
|
||||||
import { resolve } from 'node:path';
|
import { resolve } from 'node:path';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -11,7 +11,11 @@ import {
|
|||||||
} from '../src/portal-shell/sidebarState.js';
|
} from '../src/portal-shell/sidebarState.js';
|
||||||
|
|
||||||
function readSource(relativePath) {
|
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', () => {
|
test('buildSidebarUiState marks desktop collapse correctly', () => {
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { readFileSync } from 'node:fs';
|
import { existsSync, readFileSync } from 'node:fs';
|
||||||
import { resolve } from 'node:path';
|
import { resolve } from 'node:path';
|
||||||
|
|
||||||
function readSource(relativePath) {
|
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)', () => {
|
test('shell route view uses direct RouterView host (no transition blank-state)', () => {
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
created: 2026-02-11
|
||||||
@@ -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?
|
||||||
@@ -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.
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
created: 2026-02-12
|
||||||
@@ -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?
|
||||||
@@ -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.
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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`
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
created: 2026-02-12
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|
||||||
29
openspec/specs/frontend-quality-gate-modernization/spec.md
Normal file
29
openspec/specs/frontend-quality-gate-modernization/spec.md
Normal 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
|
||||||
|
|
||||||
@@ -2,22 +2,27 @@
|
|||||||
Define stable requirements for full-vite-page-modularization.
|
Define stable requirements for full-vite-page-modularization.
|
||||||
## Requirements
|
## Requirements
|
||||||
### Requirement: Major Pages SHALL be Managed by Vite Modules
|
### 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
|
#### Scenario: In-scope module governance completeness
|
||||||
- **WHEN** the portal experience is rendered
|
- **WHEN** modernization route coverage is validated for this phase
|
||||||
- **THEN** it MUST load its behavior from a Vite-built module asset when available
|
- **THEN** every in-scope route SHALL have deterministic module-governance metadata and ownership mapping
|
||||||
|
|
||||||
#### Scenario: Module fallback continuity
|
#### Scenario: Deferred route exclusion in this phase
|
||||||
- **WHEN** a required Vite asset is unavailable
|
- **WHEN** completeness validation executes for this phase
|
||||||
- **THEN** the system MUST keep affected page behavior functional through explicit fallback logic
|
- **THEN** deferred routes SHALL be excluded from mandatory pass criteria
|
||||||
|
|
||||||
### Requirement: Build Pipeline SHALL Produce Backend-Served Assets
|
### 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
|
#### Scenario: Build artifact readiness for in-scope routes
|
||||||
- **WHEN** frontend build is executed
|
- **WHEN** frontend build is executed for release
|
||||||
- **THEN** generated JS/CSS files SHALL be written to the configured backend static dist directory
|
- **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
|
### 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.
|
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
|
#### Scenario: Shared utility change propagates across both pages
|
||||||
- **WHEN** autocomplete mapping rules are updated in the shared core module
|
- **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
|
- **THEN** both WIP overview and WIP detail modules MUST consume the updated behavior without duplicated page-local logic edits
|
||||||
|
|
||||||
|
|||||||
@@ -17,3 +17,21 @@ Cache, throttling, and index-related numeric literals that control behavior MUST
|
|||||||
- **WHEN** operators need to tune cache/index thresholds
|
- **WHEN** operators need to tune cache/index thresholds
|
||||||
- **THEN** they MUST find values in named constants or environment variables rather than scattered inline literals
|
- **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
|
||||||
|
|
||||||
|
|||||||
58
openspec/specs/page-content-modernization-safety/spec.md
Normal file
58
openspec/specs/page-content-modernization-safety/spec.md
Normal 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
|
||||||
|
|
||||||
@@ -2,46 +2,31 @@
|
|||||||
Define stable requirements for spa-shell-navigation.
|
Define stable requirements for spa-shell-navigation.
|
||||||
## Requirements
|
## Requirements
|
||||||
### Requirement: Portal SHALL provide a SPA shell driven by Vue Router
|
### 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
|
#### Scenario: In-scope route renders through shell governance
|
||||||
- **WHEN** a user clicks a sidebar page entry whose migration mode is `native`
|
- **WHEN** a user navigates to an in-scope shell-governed route
|
||||||
- **THEN** the active route SHALL be updated through Vue Router
|
- **THEN** the route SHALL resolve through Vue Router with shell contract metadata
|
||||||
- **THEN** the main content area SHALL render the corresponding page module inside shell route-view without iframe usage
|
- **THEN** the shell SHALL render the corresponding module/target without iframe fallback
|
||||||
- **THEN** the content area SHALL fill the available viewport width minus the sidebar width (if sidebar is expanded)
|
|
||||||
|
|
||||||
#### Scenario: Shell layout fills full viewport
|
#### Scenario: Admin route appears as governed target
|
||||||
- **WHEN** the portal shell renders
|
- **WHEN** an admin user opens shell navigation
|
||||||
- **THEN** the shell SHALL span the full viewport width with no max-width constraint
|
- **THEN** `/admin/pages` and `/admin/performance` SHALL be exposed as governed navigation targets per access policy
|
||||||
- **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: Page-level max-width constraints are removed when embedded
|
#### Scenario: Deferred route is excluded from this phase route-governance requirement
|
||||||
- **WHEN** a page module registered in the shell route contracts renders inside `.shell-content`
|
- **WHEN** phase-level shell-governance compliance is evaluated
|
||||||
- **THEN** page-level max-width constraints SHALL be overridden to allow full-width rendering
|
- **THEN** `/tables`, `/excel-query`, `/query-tool`, and `/mid-section-defect` SHALL be treated as deferred and excluded from pass/fail criteria for this phase
|
||||||
- **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
|
|
||||||
|
|
||||||
### Requirement: Existing route contracts SHALL remain stable in SPA mode
|
### 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
|
#### Scenario: Canonical shell path behavior for in-scope routes
|
||||||
- **WHEN** a user opens an existing route directly (bookmark or refresh)
|
- **WHEN** a user opens an in-scope report route via canonical shell path
|
||||||
- **THEN** the route SHALL resolve to the same page functionality as before migration
|
- **THEN** route behavior and query semantics SHALL remain compatible with established baseline behavior
|
||||||
- **THEN** required query parameters SHALL continue to be interpreted with compatible semantics
|
|
||||||
|
|
||||||
#### Scenario: Query continuity across shell navigation
|
#### Scenario: Compatibility policy for direct route entry
|
||||||
- **WHEN** users navigate from shell list pages to detail pages and back
|
- **WHEN** a user opens an in-scope report route via direct non-canonical entry
|
||||||
- **THEN** query-state parameters required by list/detail workflows SHALL remain consistent with pre-migration behavior
|
- **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
|
### 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.
|
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 redirect to a safe fallback route
|
||||||
- **THEN** the shell SHALL NOT expose iframe-based fallback rendering
|
- **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
|
||||||
|
|
||||||
|
|||||||
40
openspec/specs/style-isolation-and-token-enforcement/spec.md
Normal file
40
openspec/specs/style-isolation-and-token-enforcement/spec.md
Normal 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
|
||||||
|
|
||||||
@@ -1,29 +1,28 @@
|
|||||||
## Purpose
|
## Purpose
|
||||||
Define stable requirements for tailwind-design-system.
|
Define stable requirements for tailwind-design-system.
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
### Requirement: Frontend styles SHALL be governed by Tailwind design tokens
|
### 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
|
#### Scenario: Shared token usage across in-scope modules
|
||||||
- **WHEN** two report modules render equivalent UI elements (e.g., card, filter chip, primary button)
|
- **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** 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
|
#### Scenario: Token governance review
|
||||||
- **WHEN** the portal shell renders
|
- **WHEN** an in-scope route introduces new shared UI styling
|
||||||
- **THEN** `--portal-shell-max-width` SHALL resolve to `none`
|
- **THEN** the styling SHALL map to shared tokens/layers or be recorded in an approved exception registry
|
||||||
- **THEN** `--portal-sidebar-width` SHALL resolve to `240px`
|
|
||||||
- **THEN** `.u-content-shell` SHALL apply `width: 100%` without max-width constraint
|
|
||||||
|
|
||||||
### Requirement: Tailwind migration SHALL support coexistence with legacy CSS
|
### 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
|
#### Scenario: In-scope global selector control
|
||||||
- **WHEN** a not-yet-migrated page is rendered
|
- **WHEN** in-scope route styles are reviewed
|
||||||
- **THEN** existing CSS behavior SHALL remain intact
|
- **THEN** new route-local styling SHALL NOT introduce page-global selectors (`:root`, `body`) for local presentation behavior
|
||||||
- **THEN** Tailwind introduction SHALL NOT cause blocking style regressions
|
|
||||||
|
#### 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
|
### 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.
|
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
|
- **WHEN** a new shared component is introduced in migration scope
|
||||||
- **THEN** its primary style contract SHALL be expressed through Tailwind utilities/components
|
- **THEN** its primary style contract SHALL be expressed through Tailwind utilities/components
|
||||||
- **THEN** page-local CSS additions SHALL be minimized and justified
|
- **THEN** page-local CSS additions SHALL be minimized and justified
|
||||||
|
|
||||||
|
|||||||
51
openspec/specs/unified-shell-route-coverage/spec.md
Normal file
51
openspec/specs/unified-shell-route-coverage/spec.md
Normal 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
|
||||||
|
|
||||||
@@ -1,135 +1,139 @@
|
|||||||
## Purpose
|
## Purpose
|
||||||
Define stable requirements for vue-vite-page-architecture.
|
Define stable requirements for vue-vite-page-architecture.
|
||||||
|
## Requirements
|
||||||
## 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.
|
||||||
|
|
||||||
### Requirement: Pure Vite pages SHALL be served as static HTML
|
#### Scenario: In-scope canonical shell entry
|
||||||
The system SHALL support serving Vite-built HTML pages directly via Flask without Jinja2 rendering.
|
- **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: Serve pure Vite page
|
|
||||||
- **WHEN** user navigates to a pure Vite page route (e.g., `/qc-gate`)
|
#### Scenario: Direct-entry compatibility policy for in-scope routes
|
||||||
- **THEN** Flask SHALL serve the pre-built HTML file from `static/dist/` via `send_from_directory`
|
- **WHEN** a user opens an in-scope route through direct non-canonical entry
|
||||||
- **THEN** the HTML SHALL NOT pass through Jinja2 template rendering
|
- **THEN** the system SHALL apply explicit compatibility behavior without breaking established query semantics
|
||||||
|
|
||||||
#### Scenario: Page works as top-level navigation target
|
#### Scenario: Admin targets in shell governance
|
||||||
- **WHEN** a pure Vite page is opened from portal direct navigation
|
- **WHEN** shell navigation is rendered for an authorized admin user
|
||||||
- **THEN** the page SHALL render correctly as a top-level route without iframe embedding dependency
|
- **THEN** `/admin/pages` and `/admin/performance` SHALL be reachable through governed admin navigation targets
|
||||||
- **THEN** page functionality SHALL NOT rely on portal-managed frame lifecycle
|
|
||||||
|
#### Scenario: Deferred routes excluded from this phase architecture criteria
|
||||||
### Requirement: Vite config SHALL support Vue SFC and HTML entry points
|
- **WHEN** this phase architecture compliance is evaluated
|
||||||
The Vite build configuration SHALL support Vue Single File Components alongside existing vanilla JS entries.
|
- **THEN** `/tables`, `/excel-query`, `/query-tool`, and `/mid-section-defect` SHALL be excluded and handled in a follow-up change
|
||||||
|
|
||||||
#### Scenario: Vue plugin coexistence
|
### Requirement: Vite config SHALL support Vue SFC and HTML entry points
|
||||||
- **WHEN** `vite build` is executed
|
The Vite build configuration SHALL support Vue Single File Components alongside existing vanilla JS entries.
|
||||||
- **THEN** Vue SFC (`.vue` files) SHALL be compiled by `@vitejs/plugin-vue`
|
|
||||||
- **THEN** existing vanilla JS entry points SHALL continue to build without modification
|
#### Scenario: Vue plugin coexistence
|
||||||
|
- **WHEN** `vite build` is executed
|
||||||
#### Scenario: HTML entry point
|
- **THEN** Vue SFC (`.vue` files) SHALL be compiled by `@vitejs/plugin-vue`
|
||||||
- **WHEN** a page uses an HTML file as its Vite entry point
|
- **THEN** existing vanilla JS entry points SHALL continue to build without modification
|
||||||
- **THEN** Vite SHALL process the HTML and its referenced JS/CSS into `static/dist/`
|
|
||||||
- **THEN** the output SHALL include `<page-name>.html`, `<page-name>.js`, and `<page-name>.css`
|
#### Scenario: HTML entry point
|
||||||
|
- **WHEN** a page uses an HTML file as its Vite entry point
|
||||||
#### Scenario: Chunk splitting
|
- **THEN** Vite SHALL process the HTML and its referenced JS/CSS into `static/dist/`
|
||||||
- **WHEN** Vite builds the project
|
- **THEN** the output SHALL include `<page-name>.html`, `<page-name>.js`, and `<page-name>.css`
|
||||||
- **THEN** Vue runtime SHALL be split into a `vendor-vue` chunk
|
|
||||||
- **THEN** ECharts modules (including TreemapChart, BarChart, LineChart) SHALL be split into the existing `vendor-echarts` chunk
|
#### Scenario: Chunk splitting
|
||||||
- **THEN** chunk splitting SHALL NOT affect existing page bundles
|
- **WHEN** Vite builds the project
|
||||||
|
- **THEN** Vue runtime SHALL be split into a `vendor-vue` chunk
|
||||||
#### Scenario: Migrated page entry replacement
|
- **THEN** ECharts modules (including TreemapChart, BarChart, LineChart) SHALL be split into the existing `vendor-echarts` chunk
|
||||||
- **WHEN** a vanilla JS page is migrated to Vue 3
|
- **THEN** chunk splitting SHALL NOT affect existing page bundles
|
||||||
- **THEN** its Vite entry SHALL change from JS file to HTML file (e.g., `src/wip-overview/main.js` → `src/wip-overview/index.html`)
|
|
||||||
- **THEN** the original JS entry SHALL be replaced, not kept alongside
|
#### Scenario: Migrated page entry replacement
|
||||||
|
- **WHEN** a vanilla JS page is migrated to Vue 3
|
||||||
#### Scenario: Hold Overview entry point
|
- **THEN** its Vite entry SHALL change from JS file to HTML file (e.g., `src/wip-overview/main.js` → `src/wip-overview/index.html`)
|
||||||
- **WHEN** the hold-overview page is added
|
- **THEN** the original JS entry SHALL be replaced, not kept alongside
|
||||||
- **THEN** `vite.config.js` input SHALL include `'hold-overview': resolve(__dirname, 'src/hold-overview/index.html')`
|
|
||||||
- **THEN** the build SHALL produce `hold-overview.html`, `hold-overview.js`, and `hold-overview.css` in `static/dist/`
|
#### Scenario: Hold Overview entry point
|
||||||
|
- **WHEN** the hold-overview page is added
|
||||||
#### Scenario: Hold History entry point
|
- **THEN** `vite.config.js` input SHALL include `'hold-overview': resolve(__dirname, 'src/hold-overview/index.html')`
|
||||||
- **WHEN** the hold-history page is added
|
- **THEN** the build SHALL produce `hold-overview.html`, `hold-overview.js`, and `hold-overview.css` in `static/dist/`
|
||||||
- **THEN** `vite.config.js` input SHALL include `'hold-history': resolve(__dirname, 'src/hold-history/index.html')`
|
|
||||||
- **THEN** the build SHALL produce `hold-history.html`, `hold-history.js`, and `hold-history.css` in `static/dist/`
|
#### Scenario: Hold History entry point
|
||||||
|
- **WHEN** the hold-history page is added
|
||||||
#### Scenario: Shared CSS import across migrated pages
|
- **THEN** `vite.config.js` input SHALL include `'hold-history': resolve(__dirname, 'src/hold-history/index.html')`
|
||||||
- **WHEN** multiple migrated pages import a shared CSS module (e.g., `wip-shared/styles.css`)
|
- **THEN** the build SHALL produce `hold-history.html`, `hold-history.js`, and `hold-history.css` in `static/dist/`
|
||||||
- **THEN** Vite SHALL bundle the shared CSS into each page's output CSS
|
|
||||||
- **THEN** shared CSS SHALL NOT create a separate shared chunk that requires additional HTTP requests
|
#### Scenario: Shared CSS import across migrated pages
|
||||||
|
- **WHEN** multiple migrated pages import a shared CSS module (e.g., `wip-shared/styles.css`)
|
||||||
#### Scenario: Shared composable import across module boundaries
|
- **THEN** Vite SHALL bundle the shared CSS into each page's output CSS
|
||||||
- **WHEN** a migrated page imports a composable from another shared module (e.g., `hold-history` imports `useAutoRefresh` from `wip-shared/`)
|
- **THEN** shared CSS SHALL NOT create a separate shared chunk that requires additional HTTP requests
|
||||||
- **THEN** the composable SHALL be bundled into the importing page's JS output
|
|
||||||
- **THEN** cross-module imports SHALL NOT create unexpected shared chunks
|
#### Scenario: Shared composable import across module boundaries
|
||||||
|
- **WHEN** a migrated page imports a composable from another shared module (e.g., `hold-history` imports `useAutoRefresh` from `wip-shared/`)
|
||||||
### Requirement: Pure Vite pages SHALL handle API calls without legacy MesApi
|
- **THEN** the composable SHALL be bundled into the importing page's JS output
|
||||||
Pure Vite pages SHALL use the existing `frontend/src/core/api.js` module for API communication without depending on the global `window.MesApi` object from `_base.html`.
|
- **THEN** cross-module imports SHALL NOT create unexpected shared chunks
|
||||||
|
|
||||||
#### Scenario: API GET request from pure Vite page
|
### Requirement: Pure Vite pages SHALL handle API calls without legacy MesApi
|
||||||
- **WHEN** a pure Vite page makes a GET API call
|
Pure Vite pages SHALL use the existing `frontend/src/core/api.js` module for API communication without depending on the global `window.MesApi` object from `_base.html`.
|
||||||
- **THEN** the call SHALL use the `apiGet` function from `core/api.js`
|
|
||||||
- **THEN** the call SHALL work without `window.MesApi` being present
|
#### Scenario: API GET request from pure Vite page
|
||||||
|
- **WHEN** a pure Vite page makes a GET API call
|
||||||
### Requirement: Pure Vite pages SHALL handle POST API calls without legacy MesApi
|
- **THEN** the call SHALL use the `apiGet` function from `core/api.js`
|
||||||
Pure Vite pages SHALL use the `apiPost` function from `core/api.js` for POST requests without depending on `window.MesApi`.
|
- **THEN** the call SHALL work without `window.MesApi` being present
|
||||||
|
|
||||||
#### Scenario: API POST request from pure Vite page
|
### Requirement: Pure Vite pages SHALL handle POST API calls without legacy MesApi
|
||||||
- **WHEN** a pure Vite page makes a POST API call
|
Pure Vite pages SHALL use the `apiPost` function from `core/api.js` for POST requests without depending on `window.MesApi`.
|
||||||
- **THEN** the call SHALL use the `apiPost` function from `core/api.js`
|
|
||||||
- **THEN** the call SHALL include `Content-Type: application/json` header
|
#### Scenario: API POST request from pure Vite page
|
||||||
- **THEN** the call SHALL work without `window.MesApi` being present
|
- **WHEN** a pure Vite page makes a POST API call
|
||||||
|
- **THEN** the call SHALL use the `apiPost` function from `core/api.js`
|
||||||
#### Scenario: CSRF token handling in POST requests
|
- **THEN** the call SHALL include `Content-Type: application/json` header
|
||||||
- **WHEN** a pure Vite page calls `apiPost`
|
- **THEN** the call SHALL work without `window.MesApi` being present
|
||||||
- **THEN** `apiPost` SHALL attempt to read CSRF token from `<meta name="csrf-token">`
|
|
||||||
- **THEN** if no meta tag exists, the request SHALL still proceed (non-admin APIs do not enforce CSRF)
|
#### Scenario: CSRF token handling in POST requests
|
||||||
|
- **WHEN** a pure Vite page calls `apiPost`
|
||||||
### Requirement: Pure Vite pages with server-side route validation SHALL use send_from_directory with pre-validation
|
- **THEN** `apiPost` SHALL attempt to read CSRF token from `<meta name="csrf-token">`
|
||||||
Pages that require server-side parameter validation before serving SHALL validate parameters in the Flask route and then serve the static HTML.
|
- **THEN** if no meta tag exists, the request SHALL still proceed (non-admin APIs do not enforce CSRF)
|
||||||
|
|
||||||
#### Scenario: Hold Detail reason validation
|
### Requirement: Pure Vite pages with server-side route validation SHALL use send_from_directory with pre-validation
|
||||||
- **WHEN** user navigates to `/hold-detail` without a `reason` parameter
|
Pages that require server-side parameter validation before serving SHALL validate parameters in the Flask route and then serve the static HTML.
|
||||||
- **THEN** Flask SHALL redirect to `/wip-overview`
|
|
||||||
- **WHEN** user navigates to `/hold-detail?reason={value}`
|
#### Scenario: Hold Detail reason validation
|
||||||
- **THEN** Flask SHALL serve the pre-built HTML file from `static/dist/` via `send_from_directory`
|
- **WHEN** user navigates to `/hold-detail` without a `reason` parameter
|
||||||
- **THEN** the HTML SHALL NOT pass through Jinja2 template rendering
|
- **THEN** Flask SHALL redirect to `/wip-overview`
|
||||||
|
- **WHEN** user navigates to `/hold-detail?reason={value}`
|
||||||
#### Scenario: Frontend fallback validation
|
- **THEN** Flask SHALL serve the pre-built HTML file from `static/dist/` via `send_from_directory`
|
||||||
- **WHEN** the pure Vite hold-detail page loads
|
- **THEN** the HTML SHALL NOT pass through Jinja2 template rendering
|
||||||
- **THEN** the page SHALL read `reason` from URL parameters
|
|
||||||
- **THEN** if `reason` is empty or missing, the page SHALL redirect to `/wip-overview`
|
#### Scenario: Frontend fallback validation
|
||||||
|
- **WHEN** the pure Vite hold-detail page loads
|
||||||
### Requirement: Mid-section defect page SHALL separate filter state from query state
|
- **THEN** the page SHALL read `reason` from URL parameters
|
||||||
The mid-section defect page SHALL maintain separate reactive state for UI input (`filters`) and committed query parameters (`committedFilters`).
|
- **THEN** if `reason` is empty or missing, the page SHALL redirect to `/wip-overview`
|
||||||
|
|
||||||
#### Scenario: User changes date without clicking query
|
### Requirement: Mid-section defect page SHALL separate filter state from query state
|
||||||
- **WHEN** user modifies the date range in the filter bar but does not click "查詢"
|
The mid-section defect page SHALL maintain separate reactive state for UI input (`filters`) and committed query parameters (`committedFilters`).
|
||||||
- **THEN** auto-refresh, pagination, and CSV export SHALL continue using the previously committed filter values
|
|
||||||
- **THEN** the new date range SHALL NOT affect any API calls until "查詢" is clicked
|
#### Scenario: User changes date without clicking query
|
||||||
|
- **WHEN** user modifies the date range in the filter bar but does not click "查詢"
|
||||||
#### Scenario: User clicks query button
|
- **THEN** auto-refresh, pagination, and CSV export SHALL continue using the previously committed filter values
|
||||||
- **WHEN** user clicks "查詢"
|
- **THEN** the new date range SHALL NOT affect any API calls until "查詢" is clicked
|
||||||
- **THEN** the current `filters` state SHALL be snapshotted into `committedFilters`
|
|
||||||
- **THEN** all subsequent API calls SHALL use the committed values
|
#### Scenario: User clicks query button
|
||||||
|
- **WHEN** user clicks "查詢"
|
||||||
#### Scenario: CSV export uses committed filters
|
- **THEN** the current `filters` state SHALL be snapshotted into `committedFilters`
|
||||||
- **WHEN** user clicks "匯出 CSV" after modifying filters without re-querying
|
- **THEN** all subsequent API calls SHALL use the committed values
|
||||||
- **THEN** the export SHALL use the committed filter values from the last query
|
|
||||||
- **THEN** the export SHALL NOT use the current UI filter values
|
#### Scenario: CSV export uses committed filters
|
||||||
|
- **WHEN** user clicks "匯出 CSV" after modifying filters without re-querying
|
||||||
### Requirement: Mid-section defect page SHALL cancel in-flight requests on new query
|
- **THEN** the export SHALL use the committed filter values from the last query
|
||||||
The mid-section defect page SHALL use `AbortController` to cancel in-flight API requests when a new query is initiated.
|
- **THEN** the export SHALL NOT use the current UI filter values
|
||||||
|
|
||||||
#### Scenario: New query cancels previous query
|
### Requirement: Mid-section defect page SHALL cancel in-flight requests on new query
|
||||||
- **WHEN** user clicks "查詢" while a previous query is still in-flight
|
The mid-section defect page SHALL use `AbortController` to cancel in-flight API requests when a new query is initiated.
|
||||||
- **THEN** the previous query's summary and detail requests SHALL be aborted
|
|
||||||
- **THEN** the AbortError SHALL be handled silently (no error banner shown)
|
#### Scenario: New query cancels previous query
|
||||||
|
- **WHEN** user clicks "查詢" while a previous query is still in-flight
|
||||||
#### Scenario: Page navigation cancels previous detail request
|
- **THEN** the previous query's summary and detail requests SHALL be aborted
|
||||||
- **WHEN** user clicks next page while a previous page request is still in-flight
|
- **THEN** the AbortError SHALL be handled silently (no error banner shown)
|
||||||
- **THEN** the previous page request SHALL be aborted
|
|
||||||
- **THEN** the new page request SHALL proceed independently
|
#### Scenario: Page navigation cancels previous detail request
|
||||||
|
- **WHEN** user clicks next page while a previous page request is still in-flight
|
||||||
#### Scenario: Query and pagination use independent abort keys
|
- **THEN** the previous page request SHALL be aborted
|
||||||
- **WHEN** a query is in-flight and user triggers pagination
|
- **THEN** the new page request SHALL proceed independently
|
||||||
- **THEN** the query SHALL NOT be cancelled by the pagination request
|
|
||||||
- **THEN** the pagination SHALL use a separate abort key from the query
|
#### Scenario: Query and pagination use independent abort keys
|
||||||
|
- **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
|
||||||
|
|
||||||
|
|||||||
460
scripts/check_full_modernization_gates.py
Executable file
460
scripts/check_full_modernization_gates.py
Executable 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())
|
||||||
@@ -211,10 +211,37 @@ with engine.connect() as conn:
|
|||||||
}
|
}
|
||||||
|
|
||||||
build_frontend_assets() {
|
build_frontend_assets() {
|
||||||
if [ "${FRONTEND_BUILD_ON_START:-true}" != "true" ]; then
|
local mode="${FRONTEND_BUILD_MODE:-}"
|
||||||
log_info "Skip frontend build (FRONTEND_BUILD_ON_START=${FRONTEND_BUILD_ON_START})"
|
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
|
return 0
|
||||||
fi
|
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
|
if [ ! -f "${ROOT}/frontend/package.json" ]; then
|
||||||
return 0
|
return 0
|
||||||
@@ -225,54 +252,61 @@ build_frontend_assets() {
|
|||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local required_entries=(
|
local needs_build=true
|
||||||
"portal.js"
|
if [ "${mode}" = "auto" ]; then
|
||||||
"wip-overview.js"
|
local required_entries=(
|
||||||
"wip-detail.js"
|
"portal.js"
|
||||||
"hold-detail.js"
|
"wip-overview.js"
|
||||||
"hold-overview.js"
|
"wip-detail.js"
|
||||||
"hold-history.js"
|
"hold-detail.js"
|
||||||
"resource-status.js"
|
"hold-overview.js"
|
||||||
"resource-history.js"
|
"hold-history.js"
|
||||||
"job-query.js"
|
"resource-status.js"
|
||||||
"excel-query.js"
|
"resource-history.js"
|
||||||
"tables.js"
|
"job-query.js"
|
||||||
"query-tool.js"
|
"excel-query.js"
|
||||||
"tmtt-defect.js"
|
"tables.js"
|
||||||
"qc-gate.js"
|
"query-tool.js"
|
||||||
"mid-section-defect.js"
|
"tmtt-defect.js"
|
||||||
)
|
"qc-gate.js"
|
||||||
local needs_build=false
|
"mid-section-defect.js"
|
||||||
local newest_entry=""
|
)
|
||||||
|
needs_build=false
|
||||||
|
local newest_entry=""
|
||||||
|
|
||||||
for entry in "${required_entries[@]}"; do
|
for entry in "${required_entries[@]}"; do
|
||||||
local entry_path="${ROOT}/src/mes_dashboard/static/dist/${entry}"
|
local entry_path="${ROOT}/src/mes_dashboard/static/dist/${entry}"
|
||||||
if [ ! -f "${entry_path}" ]; then
|
if [ ! -f "${entry_path}" ]; then
|
||||||
|
needs_build=true
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
if [ -z "${newest_entry}" ] || [ "${entry_path}" -nt "${newest_entry}" ]; then
|
||||||
|
newest_entry="${entry_path}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$needs_build" = false ] && find "${ROOT}/frontend/src" -type f -newer "${newest_entry}" | grep -q .; then
|
||||||
needs_build=true
|
needs_build=true
|
||||||
break
|
|
||||||
fi
|
fi
|
||||||
if [ -z "${newest_entry}" ] || [ "${entry_path}" -nt "${newest_entry}" ]; then
|
if [ "$needs_build" = false ] && ([ "${ROOT}/frontend/package.json" -nt "${newest_entry}" ] || [ "${ROOT}/frontend/vite.config.js" -nt "${newest_entry}" ]); then
|
||||||
newest_entry="${entry_path}"
|
needs_build=true
|
||||||
fi
|
fi
|
||||||
done
|
|
||||||
|
|
||||||
if [ "$needs_build" = false ] && find "${ROOT}/frontend/src" -type f -newer "${newest_entry}" | grep -q .; then
|
if [ "$needs_build" = false ]; then
|
||||||
needs_build=true
|
log_success "Frontend assets are up to date (FRONTEND_BUILD_MODE=auto)"
|
||||||
fi
|
return 0
|
||||||
if [ "$needs_build" = false ] && ([ "${ROOT}/frontend/package.json" -nt "${newest_entry}" ] || [ "${ROOT}/frontend/vite.config.js" -nt "${newest_entry}" ]); then
|
fi
|
||||||
needs_build=true
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$needs_build" = false ]; then
|
log_info "Building frontend assets with Vite (FRONTEND_BUILD_MODE=${mode})..."
|
||||||
log_success "Frontend assets are up to date"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
log_info "Building frontend assets with Vite..."
|
|
||||||
if npm --prefix "${ROOT}/frontend" run build >/dev/null 2>&1; then
|
if npm --prefix "${ROOT}/frontend" run build >/dev/null 2>&1; then
|
||||||
log_success "Frontend assets built"
|
log_success "Frontend assets built"
|
||||||
else
|
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
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -594,7 +628,7 @@ do_start() {
|
|||||||
PORT=$(echo "${GUNICORN_BIND:-0.0.0.0:8080}" | cut -d: -f2)
|
PORT=$(echo "${GUNICORN_BIND:-0.0.0.0:8080}" | cut -d: -f2)
|
||||||
export PYTHONPATH="${ROOT}/src:${PYTHONPATH:-}"
|
export PYTHONPATH="${ROOT}/src:${PYTHONPATH:-}"
|
||||||
cd "$ROOT"
|
cd "$ROOT"
|
||||||
build_frontend_assets
|
build_frontend_assets || return 1
|
||||||
|
|
||||||
# Log startup
|
# Log startup
|
||||||
echo "[$(timestamp)] Starting server" >> "$STARTUP_LOG"
|
echo "[$(timestamp)] Starting server" >> "$STARTUP_LOG"
|
||||||
|
|||||||
@@ -44,6 +44,14 @@ from mes_dashboard.services.realtime_equipment_cache import (
|
|||||||
init_realtime_equipment_cache,
|
init_realtime_equipment_cache,
|
||||||
stop_equipment_status_sync_worker,
|
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.redis_client import close_redis
|
||||||
from mes_dashboard.core.runtime_contract import build_runtime_contract_diagnostics
|
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()
|
_SHUTDOWN_LOCK = threading.Lock()
|
||||||
_ATEXIT_REGISTERED = False
|
_ATEXIT_REGISTERED = False
|
||||||
_SHELL_ROUTE_CONTRACT_LOCK = threading.Lock()
|
_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:
|
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
|
Environment variable takes precedence so operators can toggle behavior
|
||||||
without code changes during migration rehearsal/cutover.
|
without code changes during migration rehearsal/cutover.
|
||||||
"""
|
"""
|
||||||
raw = os.getenv("PORTAL_SPA_ENABLED")
|
return resolve_bool_flag(
|
||||||
if raw is None:
|
"PORTAL_SPA_ENABLED",
|
||||||
return bool(app.config.get("PORTAL_SPA_ENABLED", False))
|
config=app.config,
|
||||||
return raw.strip().lower() in {"1", "true", "yes", "on"}
|
default=bool(app.config.get("PORTAL_SPA_ENABLED", False)),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _can_view_page_for_user(route: str, *, is_admin: bool) -> bool:
|
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
|
return default
|
||||||
|
|
||||||
|
|
||||||
def _load_shell_route_contract_routes() -> set[str]:
|
def _load_shell_route_contract_map() -> dict[str, dict[str, object]]:
|
||||||
"""Load shell route contract routes used for navigation diagnostics."""
|
"""Load shell route contract map used for navigation diagnostics."""
|
||||||
global _SHELL_ROUTE_CONTRACT_ROUTES
|
global _SHELL_ROUTE_CONTRACT_MAP
|
||||||
with _SHELL_ROUTE_CONTRACT_LOCK:
|
with _SHELL_ROUTE_CONTRACT_LOCK:
|
||||||
if _SHELL_ROUTE_CONTRACT_ROUTES is not None:
|
if _SHELL_ROUTE_CONTRACT_MAP is not None:
|
||||||
return _SHELL_ROUTE_CONTRACT_ROUTES
|
return _SHELL_ROUTE_CONTRACT_MAP
|
||||||
|
|
||||||
contract_file = (
|
project_root = Path(__file__).resolve().parents[2]
|
||||||
Path(__file__).resolve().parents[2]
|
contract_candidates = [
|
||||||
|
project_root
|
||||||
|
/ "docs"
|
||||||
|
/ "migration"
|
||||||
|
/ "full-modernization-architecture-blueprint"
|
||||||
|
/ "route_contracts.json",
|
||||||
|
project_root
|
||||||
/ "docs"
|
/ "docs"
|
||||||
/ "migration"
|
/ "migration"
|
||||||
/ "portal-shell-route-view-integration"
|
/ "portal-shell-route-view-integration"
|
||||||
/ "route_migration_contract.json"
|
/ "route_migration_contract.json",
|
||||||
)
|
]
|
||||||
if not contract_file.exists():
|
|
||||||
_SHELL_ROUTE_CONTRACT_ROUTES = set()
|
|
||||||
return _SHELL_ROUTE_CONTRACT_ROUTES
|
|
||||||
|
|
||||||
try:
|
contract_map: dict[str, dict[str, object]] = {}
|
||||||
payload = json.loads(contract_file.read_text(encoding="utf-8"))
|
logger = logging.getLogger("mes_dashboard")
|
||||||
routes = payload.get("routes", [])
|
for index, contract_file in enumerate(contract_candidates):
|
||||||
_SHELL_ROUTE_CONTRACT_ROUTES = {
|
if not contract_file.exists():
|
||||||
str(item.get("route", "")).strip()
|
continue
|
||||||
for item in routes
|
try:
|
||||||
if isinstance(item, dict) and str(item.get("route", "")).strip().startswith("/")
|
payload = json.loads(contract_file.read_text(encoding="utf-8"))
|
||||||
}
|
routes = payload.get("routes", [])
|
||||||
except Exception as exc:
|
for item in routes:
|
||||||
logging.getLogger("mes_dashboard").warning(
|
if not isinstance(item, dict):
|
||||||
"Failed to load shell route contract for diagnostics: %s",
|
continue
|
||||||
exc,
|
route = str(item.get("route", "")).strip()
|
||||||
)
|
if route.startswith("/"):
|
||||||
_SHELL_ROUTE_CONTRACT_ROUTES = set()
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
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:
|
def _shutdown_runtime_resources() -> None:
|
||||||
@@ -251,10 +315,11 @@ def _register_shutdown_hooks(app: Flask) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def _is_runtime_contract_enforced(app: Flask) -> bool:
|
def _is_runtime_contract_enforced(app: Flask) -> bool:
|
||||||
raw = os.getenv("RUNTIME_CONTRACT_ENFORCE")
|
return resolve_bool_flag(
|
||||||
if raw is not None:
|
"RUNTIME_CONTRACT_ENFORCE",
|
||||||
return raw.strip().lower() in {"1", "true", "yes", "on"}
|
config=app.config,
|
||||||
return _is_production_env(app)
|
default=_is_production_env(app),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _validate_runtime_contract(app: Flask) -> None:
|
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 first
|
||||||
_configure_logging(app)
|
_configure_logging(app)
|
||||||
_validate_runtime_contract(app)
|
_validate_runtime_contract(app)
|
||||||
|
_validate_in_scope_asset_readiness(app)
|
||||||
security_headers = _build_security_headers(_is_production_env(app))
|
security_headers = _build_security_headers(_is_production_env(app))
|
||||||
|
|
||||||
# Route-level cache backend (L1 memory + optional L2 Redis)
|
# 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()
|
source = get_navigation_config()
|
||||||
drawers: list[dict] = []
|
drawers: list[dict] = []
|
||||||
shell_contract_routes = _load_shell_route_contract_routes()
|
shell_contract_routes = _load_shell_route_contract_routes()
|
||||||
|
deferred_routes = _load_shell_deferred_routes()
|
||||||
diagnostics: dict[str, object] = {
|
diagnostics: dict[str, object] = {
|
||||||
"filtered_drawers": 0,
|
"filtered_drawers": 0,
|
||||||
"filtered_pages": 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
|
diagnostics["filtered_pages"] = int(diagnostics["filtered_pages"]) + 1
|
||||||
continue
|
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)
|
mismatch_routes.add(route)
|
||||||
nav_logger.warning(
|
nav_logger.warning(
|
||||||
"Navigation route missing shell contract: drawer_id=%s route=%s",
|
"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')
|
@app.route('/wip-overview')
|
||||||
def wip_overview_page():
|
def wip_overview_page():
|
||||||
"""WIP Overview Dashboard served as pure Vite HTML output."""
|
"""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_dir = os.path.join(app.static_folder or "", "dist")
|
||||||
dist_html = os.path.join(dist_dir, "wip-overview.html")
|
dist_html = os.path.join(dist_dir, "wip-overview.html")
|
||||||
if os.path.exists(dist_html):
|
if os.path.exists(dist_html):
|
||||||
return send_from_directory(dist_dir, 'wip-overview.html')
|
return send_from_directory(dist_dir, 'wip-overview.html')
|
||||||
|
|
||||||
# Test/local fallback when frontend build artifacts are absent.
|
# 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\">"
|
"<!doctype html><html lang=\"zh-Hant\"><head><meta charset=\"UTF-8\">"
|
||||||
"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">"
|
"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">"
|
||||||
"<title>WIP Overview Dashboard</title>"
|
"<title>WIP Overview Dashboard</title>"
|
||||||
"<script type=\"module\" src=\"/static/dist/wip-overview.js\"></script>"
|
"<script type=\"module\" src=\"/static/dist/wip-overview.js\"></script>"
|
||||||
"</head><body><div id='app'></div></body></html>",
|
"</head><body><div id='app'></div></body></html>",
|
||||||
200,
|
200,
|
||||||
)
|
))
|
||||||
|
|
||||||
@app.route('/wip-detail')
|
@app.route('/wip-detail')
|
||||||
def wip_detail_page():
|
def wip_detail_page():
|
||||||
"""WIP Detail Dashboard served as pure Vite HTML output."""
|
"""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_dir = os.path.join(app.static_folder or "", "dist")
|
||||||
dist_html = os.path.join(dist_dir, "wip-detail.html")
|
dist_html = os.path.join(dist_dir, "wip-detail.html")
|
||||||
if os.path.exists(dist_html):
|
if os.path.exists(dist_html):
|
||||||
return send_from_directory(dist_dir, 'wip-detail.html')
|
return send_from_directory(dist_dir, 'wip-detail.html')
|
||||||
|
|
||||||
# Test/local fallback when frontend build artifacts are absent.
|
# 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\">"
|
"<!doctype html><html lang=\"zh-Hant\"><head><meta charset=\"UTF-8\">"
|
||||||
"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">"
|
"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">"
|
||||||
"<title>WIP Detail Dashboard</title>"
|
"<title>WIP Detail Dashboard</title>"
|
||||||
"<script type=\"module\" src=\"/static/dist/wip-detail.js\"></script>"
|
"<script type=\"module\" src=\"/static/dist/wip-detail.js\"></script>"
|
||||||
"</head><body><div id='app'></div></body></html>",
|
"</head><body><div id='app'></div></body></html>",
|
||||||
200,
|
200,
|
||||||
)
|
))
|
||||||
|
|
||||||
@app.route('/resource')
|
@app.route('/resource')
|
||||||
def resource_page():
|
def resource_page():
|
||||||
"""Resource status report page served as pure Vite HTML output."""
|
"""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_dir = os.path.join(app.static_folder or "", "dist")
|
||||||
dist_html = os.path.join(dist_dir, "resource-status.html")
|
dist_html = os.path.join(dist_dir, "resource-status.html")
|
||||||
if os.path.exists(dist_html):
|
if os.path.exists(dist_html):
|
||||||
return send_from_directory(dist_dir, 'resource-status.html')
|
return send_from_directory(dist_dir, 'resource-status.html')
|
||||||
|
|
||||||
# Test/local fallback when frontend build artifacts are absent.
|
# 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\">"
|
"<!doctype html><html lang=\"zh-Hant\"><head><meta charset=\"UTF-8\">"
|
||||||
"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">"
|
"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">"
|
||||||
"<title>設備即時概況</title>"
|
"<title>設備即時概況</title>"
|
||||||
"<script type=\"module\" src=\"/static/dist/resource-status.js\"></script>"
|
"<script type=\"module\" src=\"/static/dist/resource-status.js\"></script>"
|
||||||
"</head><body><div id='app'></div></body></html>",
|
"</head><body><div id='app'></div></body></html>",
|
||||||
200,
|
200,
|
||||||
)
|
))
|
||||||
|
|
||||||
@app.route('/excel-query')
|
@app.route('/excel-query')
|
||||||
def excel_query_page():
|
def excel_query_page():
|
||||||
@@ -691,31 +770,53 @@ def create_app(config_name: str | None = None) -> Flask:
|
|||||||
@app.route('/resource-history')
|
@app.route('/resource-history')
|
||||||
def resource_history_page():
|
def resource_history_page():
|
||||||
"""Resource history analysis page served as pure Vite HTML output."""
|
"""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_dir = os.path.join(app.static_folder or "", "dist")
|
||||||
dist_html = os.path.join(dist_dir, "resource-history.html")
|
dist_html = os.path.join(dist_dir, "resource-history.html")
|
||||||
if os.path.exists(dist_html):
|
if os.path.exists(dist_html):
|
||||||
return send_from_directory(dist_dir, 'resource-history.html')
|
return send_from_directory(dist_dir, 'resource-history.html')
|
||||||
|
|
||||||
# Test/local fallback when frontend build artifacts are absent.
|
# 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\">"
|
"<!doctype html><html lang=\"zh-Hant\"><head><meta charset=\"UTF-8\">"
|
||||||
"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">"
|
"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">"
|
||||||
"<title>設備歷史績效</title>"
|
"<title>設備歷史績效</title>"
|
||||||
"<script type=\"module\" src=\"/static/dist/resource-history.js\"></script>"
|
"<script type=\"module\" src=\"/static/dist/resource-history.js\"></script>"
|
||||||
"</head><body><div id='app'></div></body></html>",
|
"</head><body><div id='app'></div></body></html>",
|
||||||
200,
|
200,
|
||||||
)
|
))
|
||||||
|
|
||||||
@app.route('/tmtt-defect')
|
@app.route('/tmtt-defect')
|
||||||
def tmtt_defect_page():
|
def tmtt_defect_page():
|
||||||
"""TMTT printing & lead form defect analysis 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')
|
return render_template('tmtt_defect.html')
|
||||||
|
|
||||||
@app.route('/qc-gate')
|
@app.route('/qc-gate')
|
||||||
def qc_gate_page():
|
def qc_gate_page():
|
||||||
"""QC-GATE status report served as pure Vite HTML output."""
|
"""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_dir = os.path.join(app.static_folder or "", "dist")
|
||||||
return send_from_directory(dist_dir, 'qc-gate.html')
|
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')
|
@app.route('/mid-section-defect')
|
||||||
def mid_section_defect_page():
|
def mid_section_defect_page():
|
||||||
|
|||||||
49
src/mes_dashboard/core/feature_flags.py
Normal file
49
src/mes_dashboard/core/feature_flags.py
Normal 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
|
||||||
167
src/mes_dashboard/core/modernization_policy.py
Normal file
167
src/mes_dashboard/core/modernization_policy.py
Normal 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
|
||||||
@@ -8,16 +8,12 @@ import shutil
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Mapping
|
from typing import Any, Mapping
|
||||||
|
|
||||||
|
from mes_dashboard.core.feature_flags import resolve_bool_flag
|
||||||
|
|
||||||
CONTRACT_VERSION = "2026.02-p2"
|
CONTRACT_VERSION = "2026.02-p2"
|
||||||
DEFAULT_PROJECT_ROOT = Path(__file__).resolve().parents[3]
|
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:
|
def _resolve_path(value: str | None, fallback: Path, project_root: Path) -> Path:
|
||||||
if value is None or not str(value).strip():
|
if value is None or not str(value).strip():
|
||||||
return fallback.resolve()
|
return fallback.resolve()
|
||||||
@@ -68,7 +64,12 @@ def load_runtime_contract(
|
|||||||
"watchdog_pid_file": str(pid_file),
|
"watchdog_pid_file": str(pid_file),
|
||||||
"watchdog_state_file": str(state_file),
|
"watchdog_state_file": str(state_file),
|
||||||
"watchdog_check_interval": int(env.get("WATCHDOG_CHECK_INTERVAL", "5")),
|
"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
|
return contract
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ from typing import Optional, Tuple
|
|||||||
from flask import Blueprint, current_app, jsonify, request, send_from_directory
|
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.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 (
|
from mes_dashboard.services.hold_history_service import (
|
||||||
get_hold_history_duration,
|
get_hold_history_duration,
|
||||||
get_hold_history_list,
|
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')
|
@hold_history_bp.route('/hold-history')
|
||||||
def hold_history_page():
|
def hold_history_page():
|
||||||
"""Render Hold History page from static Vite output."""
|
"""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_dir = os.path.join(current_app.static_folder or '', 'dist')
|
||||||
dist_html = os.path.join(dist_dir, 'hold-history.html')
|
dist_html = os.path.join(dist_dir, 'hold-history.html')
|
||||||
if os.path.exists(dist_html):
|
if os.path.exists(dist_html):
|
||||||
return send_from_directory(dist_dir, 'hold-history.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">'
|
'<!doctype html><html lang="zh-Hant"><head><meta charset="UTF-8">'
|
||||||
'<meta name="viewport" content="width=device-width, initial-scale=1.0">'
|
'<meta name="viewport" content="width=device-width, initial-scale=1.0">'
|
||||||
'<title>Hold History</title>'
|
'<title>Hold History</title>'
|
||||||
'<script type="module" src="/static/dist/hold-history.js"></script>'
|
'<script type="module" src="/static/dist/hold-history.js"></script>'
|
||||||
'</head><body><div id="app"></div></body></html>',
|
'</head><body><div id="app"></div></body></html>',
|
||||||
200,
|
200,
|
||||||
)
|
))
|
||||||
|
|
||||||
|
|
||||||
@hold_history_bp.route('/api/hold-history/trend')
|
@hold_history_bp.route('/api/hold-history/trend')
|
||||||
|
|||||||
@@ -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.rate_limit import configured_rate_limit
|
||||||
from mes_dashboard.core.utils import parse_bool_query
|
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 (
|
from mes_dashboard.services.wip_service import (
|
||||||
get_hold_detail_lots,
|
get_hold_detail_lots,
|
||||||
get_hold_detail_summary,
|
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')
|
@hold_overview_bp.route('/hold-overview')
|
||||||
def hold_overview_page():
|
def hold_overview_page():
|
||||||
"""Render hold overview page from static Vite output."""
|
"""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_dir = os.path.join(current_app.static_folder or "", "dist")
|
||||||
dist_html = os.path.join(dist_dir, "hold-overview.html")
|
dist_html = os.path.join(dist_dir, "hold-overview.html")
|
||||||
if os.path.exists(dist_html):
|
if os.path.exists(dist_html):
|
||||||
return send_from_directory(dist_dir, 'hold-overview.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\">"
|
"<!doctype html><html lang=\"zh-Hant\"><head><meta charset=\"UTF-8\">"
|
||||||
"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">"
|
"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">"
|
||||||
"<title>Hold Overview</title>"
|
"<title>Hold Overview</title>"
|
||||||
"<script type=\"module\" src=\"/static/dist/hold-overview.js\"></script>"
|
"<script type=\"module\" src=\"/static/dist/hold-overview.js\"></script>"
|
||||||
"</head><body><div id='app'></div></body></html>",
|
"</head><body><div id='app'></div></body></html>",
|
||||||
200,
|
200,
|
||||||
)
|
))
|
||||||
|
|
||||||
|
|
||||||
@hold_overview_bp.route('/api/hold-overview/summary')
|
@hold_overview_bp.route('/api/hold-overview/summary')
|
||||||
|
|||||||
@@ -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.rate_limit import configured_rate_limit
|
||||||
from mes_dashboard.core.utils import parse_bool_query
|
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 (
|
from mes_dashboard.services.wip_service import (
|
||||||
get_hold_detail_summary,
|
get_hold_detail_summary,
|
||||||
get_hold_detail_distribution,
|
get_hold_detail_distribution,
|
||||||
@@ -43,11 +47,18 @@ def hold_detail_page():
|
|||||||
Returns:
|
Returns:
|
||||||
Rendered HTML template
|
Rendered HTML template
|
||||||
"""
|
"""
|
||||||
reason = request.args.get('reason', '').strip()
|
reason = request.args.get('reason', '').strip()
|
||||||
if not reason:
|
if not reason:
|
||||||
# Redirect to WIP Overview when reason is missing
|
# Redirect to overview route; in SPA mode this becomes canonical shell URL.
|
||||||
return redirect('/wip-overview')
|
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.
|
# Keep server-side validation, then serve static Vite output directly.
|
||||||
dist_dir = os.path.join(current_app.static_folder or "", "dist")
|
dist_dir = os.path.join(current_app.static_folder or "", "dist")
|
||||||
dist_html = os.path.join(dist_dir, "hold-detail.html")
|
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')
|
return send_from_directory(dist_dir, 'hold-detail.html')
|
||||||
|
|
||||||
safe_reason = html.escape(reason, quote=True)
|
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\">"
|
"<!doctype html><html lang=\"zh-Hant\"><head><meta charset=\"UTF-8\">"
|
||||||
"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">"
|
"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">"
|
||||||
f"<title>Hold Detail - {safe_reason}</title>"
|
f"<title>Hold Detail - {safe_reason}</title>"
|
||||||
@@ -63,7 +74,7 @@ def hold_detail_page():
|
|||||||
f"<meta name=\"hold-reason\" content=\"{safe_reason}\">"
|
f"<meta name=\"hold-reason\" content=\"{safe_reason}\">"
|
||||||
"</head><body><div id='app'></div></body></html>",
|
"</head><body><div id='app'></div></body></html>",
|
||||||
200,
|
200,
|
||||||
)
|
))
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|||||||
@@ -9,10 +9,11 @@ Contains Flask Blueprint for maintenance job query endpoints:
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from flask import Blueprint, jsonify, request, Response, render_template
|
from flask import Blueprint, jsonify, request, Response, render_template
|
||||||
|
|
||||||
from mes_dashboard.core.rate_limit import configured_rate_limit
|
from mes_dashboard.core.rate_limit import configured_rate_limit
|
||||||
from mes_dashboard.services.job_query_service import (
|
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_jobs_by_resources,
|
||||||
get_job_txn_history,
|
get_job_txn_history,
|
||||||
export_jobs_with_history,
|
export_jobs_with_history,
|
||||||
@@ -46,10 +47,13 @@ _JOB_EXPORT_RATE_LIMIT = configured_rate_limit(
|
|||||||
# Page Route
|
# Page Route
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
||||||
@job_query_bp.route('/job-query')
|
@job_query_bp.route('/job-query')
|
||||||
def job_query_page():
|
def job_query_page():
|
||||||
"""Render the job query page."""
|
"""Render the job query page."""
|
||||||
return render_template('job_query.html')
|
canonical_redirect = maybe_redirect_to_canonical_shell('/job-query')
|
||||||
|
if canonical_redirect is not None:
|
||||||
|
return canonical_redirect
|
||||||
|
return render_template('job_query.html')
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|||||||
@@ -22,15 +22,24 @@ os.environ.setdefault('WATCHDOG_STATE_FILE', os.path.join(_TMP_DIR, 'mes_dashboa
|
|||||||
|
|
||||||
import mes_dashboard.core.database as db
|
import mes_dashboard.core.database as db
|
||||||
from mes_dashboard.app import create_app
|
from mes_dashboard.app import create_app
|
||||||
|
from mes_dashboard.core.modernization_policy import clear_modernization_policy_cache
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def app():
|
def app():
|
||||||
"""Create application for testing."""
|
"""Create application for testing."""
|
||||||
db._ENGINE = None
|
db._ENGINE = None
|
||||||
app = create_app('testing')
|
app = create_app('testing')
|
||||||
app.config['TESTING'] = True
|
app.config['TESTING'] = True
|
||||||
return 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
|
@pytest.fixture
|
||||||
|
|||||||
@@ -130,14 +130,21 @@ class TestWipAndHoldPagesE2E:
|
|||||||
back_href = page.locator("a.btn-back").get_attribute("href") or ""
|
back_href = page.locator("a.btn-back").get_attribute("href") or ""
|
||||||
parsed = urlparse(back_href)
|
parsed = urlparse(back_href)
|
||||||
params = parse_qs(parsed.query)
|
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("type", [None])[0] == "PJA3460"
|
||||||
assert params.get("status", [None])[0] in {"queue", "QUEUE"}
|
assert params.get("status", [None])[0] in {"queue", "QUEUE"}
|
||||||
|
|
||||||
def test_hold_detail_without_reason_redirects_to_overview(self, page: Page, app_server: str):
|
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)
|
response = _get_with_retry(f"{app_server}/hold-detail", attempts=3, timeout=10.0)
|
||||||
assert response.status_code == 302
|
assert response.status_code == 302
|
||||||
assert response.headers.get("Location") == "/wip-overview"
|
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):
|
def test_hold_detail_calls_summary_distribution_and_lots(self, page: Page, app_server: str):
|
||||||
reason = _pick_hold_reason(app_server)
|
reason = _pick_hold_reason(app_server)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user