diff --git a/.env.example b/.env.example index 38c31bc..94f7cac 100644 --- a/.env.example +++ b/.env.example @@ -31,6 +31,7 @@ DB_CALL_TIMEOUT_MS=55000 # Must stay below worker timeout # Flask Configuration # ============================================================ # Environment mode: development | production | testing +# If omitted, runtime defaults to production (fail-safe) FLASK_ENV=development # Debug mode: 0 for production, 1 for development @@ -43,6 +44,24 @@ SECRET_KEY=your-secret-key-change-in-production # Session timeout in seconds (default: 28800 = 8 hours) SESSION_LIFETIME=28800 +# JSON request payload upper bound in bytes (default: 262144 = 256KB) +MAX_JSON_BODY_BYTES=262144 + +# Route input-budget guardrails +QUERY_TOOL_MAX_CONTAINER_IDS=200 +RESOURCE_DETAIL_DEFAULT_LIMIT=500 +RESOURCE_DETAIL_MAX_LIMIT=500 + +# Trust boundary for forwarded headers (safe default: false) +# Direct-exposure deployment (no reverse proxy): keep this false +TRUST_PROXY_HEADERS=false +# Required when TRUST_PROXY_HEADERS=true. Supports comma-separated IP/CIDR entries. +# Example: TRUSTED_PROXY_IPS=127.0.0.1,10.0.0.0/24 +TRUSTED_PROXY_IPS= + +# CSP opt-in compatibility flag (default false = safer) +CSP_ALLOW_UNSAFE_EVAL=false + # ============================================================ # Authentication Configuration # ============================================================ diff --git a/.github/workflows/released-pages-hardening-gates.yml b/.github/workflows/released-pages-hardening-gates.yml new file mode 100644 index 0000000..2108050 --- /dev/null +++ b/.github/workflows/released-pages-hardening-gates.yml @@ -0,0 +1,61 @@ +name: released-pages-hardening-gates + +on: + pull_request: + paths: + - "src/mes_dashboard/**" + - "frontend/src/job-query/**" + - "tests/test_query_tool_routes.py" + - "tests/test_job_query_routes.py" + - "tests/test_resource_routes.py" + - "tests/test_rate_limit_identity.py" + - "tests/test_page_registry.py" + - "tests/test_redis_client.py" + - "tests/test_runtime_hardening.py" + - "tests/test_hold_routes.py" + - "tests/test_wip_routes.py" + - "tests/test_job_query_frontend_safety.py" + - ".github/workflows/released-pages-hardening-gates.yml" + push: + branches: [ main ] + paths: + - "src/mes_dashboard/**" + - "frontend/src/job-query/**" + - "tests/test_query_tool_routes.py" + - "tests/test_job_query_routes.py" + - "tests/test_resource_routes.py" + - "tests/test_rate_limit_identity.py" + - "tests/test_page_registry.py" + - "tests/test_redis_client.py" + - "tests/test_runtime_hardening.py" + - "tests/test_hold_routes.py" + - "tests/test_wip_routes.py" + - "tests/test_job_query_frontend_safety.py" + - ".github/workflows/released-pages-hardening-gates.yml" + +jobs: + released-pages-hardening: + 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 released-pages hardening regression suite + run: | + python -m pytest -q \ + tests/test_query_tool_routes.py \ + tests/test_job_query_routes.py \ + tests/test_resource_routes.py \ + tests/test_rate_limit_identity.py \ + tests/test_page_registry.py \ + tests/test_redis_client.py \ + tests/test_runtime_hardening.py \ + tests/test_hold_routes.py \ + tests/test_wip_routes.py \ + tests/test_job_query_frontend_safety.py \ + -k "not TestJobQueryPage and not TestHoldDetailPageRoute and not TestPageRoutes" diff --git a/README.md b/README.md index 5a03745..d9fe245 100644 --- a/README.md +++ b/README.md @@ -277,6 +277,19 @@ DB_PASSWORD=your_password # Flask 設定 FLASK_ENV=production # production | development SECRET_KEY=your-secret-key # 生產環境請更換 +MAX_JSON_BODY_BYTES=262144 # JSON 請求大小上限(bytes) + +# 輸入預算保護(Released 高成本 API) +QUERY_TOOL_MAX_CONTAINER_IDS=200 +RESOURCE_DETAIL_DEFAULT_LIMIT=500 +RESOURCE_DETAIL_MAX_LIMIT=500 + +# 反向代理信任邊界(無反向代理時務必維持 false) +TRUST_PROXY_HEADERS=false +TRUSTED_PROXY_IPS=127.0.0.1 + +# CSP 相容開關(預設 false;僅在必要時啟用) +CSP_ALLOW_UNSAFE_EVAL=false # Gunicorn 設定 GUNICORN_BIND=0.0.0.0:8080 # 服務監聽位址 diff --git a/docs/migration/full-modernization-architecture-blueprint/asset_readiness_manifest.json b/docs/migration/full-modernization-architecture-blueprint/asset_readiness_manifest.json new file mode 100644 index 0000000..9f2c9ab --- /dev/null +++ b/docs/migration/full-modernization-architecture-blueprint/asset_readiness_manifest.json @@ -0,0 +1,22 @@ +{ + "in_scope_required_assets": { + "/wip-overview": ["wip-overview.js"], + "/wip-detail": ["wip-detail.js"], + "/hold-overview": ["hold-overview.js"], + "/hold-detail": ["hold-detail.js"], + "/hold-history": ["hold-history.js"], + "/reject-history": ["reject-history.js"], + "/resource": ["resource-status.js"], + "/resource-history": ["resource-history.js"], + "/qc-gate": ["qc-gate.js"], + "/job-query": ["job-query.js"], + "/tmtt-defect": ["tmtt-defect.js"], + "/admin/pages": ["admin-pages.js"], + "/admin/performance": ["admin-performance.js"], + "/tables": ["tables.js"], + "/excel-query": ["excel-query.js"], + "/query-tool": ["query-tool.js"], + "/mid-section-defect": ["mid-section-defect.js"] + }, + "deferred_routes": [] +} diff --git a/docs/migration/full-modernization-architecture-blueprint/bug_revalidation_records.json b/docs/migration/full-modernization-architecture-blueprint/bug_revalidation_records.json new file mode 100644 index 0000000..aa38702 --- /dev/null +++ b/docs/migration/full-modernization-architecture-blueprint/bug_revalidation_records.json @@ -0,0 +1,3 @@ +{ + "records": [] +} diff --git a/docs/migration/full-modernization-architecture-blueprint/exception_registry.json b/docs/migration/full-modernization-architecture-blueprint/exception_registry.json new file mode 100644 index 0000000..47ce49a --- /dev/null +++ b/docs/migration/full-modernization-architecture-blueprint/exception_registry.json @@ -0,0 +1,123 @@ +{ + "entries": [ + { + "id": "style-legacy-wip-overview", + "scope": "/wip-overview", + "owner": "frontend-mes-reporting", + "milestone": "full-modernization-phase2", + "reason": "Legacy styles pending full token and scope migration" + }, + { + "id": "style-legacy-wip-detail", + "scope": "/wip-detail", + "owner": "frontend-mes-reporting", + "milestone": "full-modernization-phase2", + "reason": "Legacy styles pending full token and scope migration" + }, + { + "id": "style-legacy-hold-overview", + "scope": "/hold-overview", + "owner": "frontend-mes-reporting", + "milestone": "full-modernization-phase2", + "reason": "Legacy styles pending full token and scope migration" + }, + { + "id": "style-legacy-hold-detail", + "scope": "/hold-detail", + "owner": "frontend-mes-reporting", + "milestone": "full-modernization-phase2", + "reason": "Legacy styles pending full token and scope migration" + }, + { + "id": "style-legacy-hold-history", + "scope": "/hold-history", + "owner": "frontend-mes-reporting", + "milestone": "full-modernization-phase2", + "reason": "Legacy styles pending full token and scope migration" + }, + { + "id": "style-legacy-reject-history", + "scope": "/reject-history", + "owner": "frontend-mes-reporting", + "milestone": "full-modernization-phase2", + "reason": "Legacy styles pending full token and scope migration" + }, + { + "id": "style-legacy-resource", + "scope": "/resource", + "owner": "frontend-mes-reporting", + "milestone": "full-modernization-phase2", + "reason": "Legacy styles pending full token and scope migration" + }, + { + "id": "style-legacy-resource-history", + "scope": "/resource-history", + "owner": "frontend-mes-reporting", + "milestone": "full-modernization-phase2", + "reason": "Legacy styles pending full token and scope migration" + }, + { + "id": "style-legacy-qc-gate", + "scope": "/qc-gate", + "owner": "frontend-mes-reporting", + "milestone": "full-modernization-phase2", + "reason": "Legacy styles pending full token and scope migration" + }, + { + "id": "style-legacy-job-query", + "scope": "/job-query", + "owner": "frontend-mes-reporting", + "milestone": "full-modernization-phase2", + "reason": "Legacy styles pending full token and scope migration" + }, + { + "id": "style-legacy-tmtt-defect", + "scope": "/tmtt-defect", + "owner": "frontend-mes-reporting", + "milestone": "full-modernization-phase2", + "reason": "Legacy styles pending full token and scope migration" + }, + { + "id": "style-legacy-admin-pages", + "scope": "/admin/pages", + "owner": "frontend-platform-admin", + "milestone": "full-modernization-phase2", + "reason": "Legacy styles pending full token and scope migration" + }, + { + "id": "style-legacy-admin-performance", + "scope": "/admin/performance", + "owner": "frontend-platform-admin", + "milestone": "full-modernization-phase2", + "reason": "Legacy styles pending full token and scope migration" + }, + { + "id": "style-legacy-tables", + "scope": "/tables", + "owner": "frontend-mes-reporting", + "milestone": "full-modernization-phase2", + "reason": "Legacy styles pending full token and scope migration" + }, + { + "id": "style-legacy-excel-query", + "scope": "/excel-query", + "owner": "frontend-mes-reporting", + "milestone": "full-modernization-phase2", + "reason": "Legacy styles pending full token and scope migration" + }, + { + "id": "style-legacy-query-tool", + "scope": "/query-tool", + "owner": "frontend-mes-reporting", + "milestone": "full-modernization-phase2", + "reason": "Legacy styles pending full token and scope migration" + }, + { + "id": "style-legacy-mid-section-defect", + "scope": "/mid-section-defect", + "owner": "frontend-mes-reporting", + "milestone": "full-modernization-phase2", + "reason": "Legacy styles pending full token and scope migration" + } + ] +} diff --git a/docs/migration/full-modernization-architecture-blueprint/known_bug_baseline.json b/docs/migration/full-modernization-architecture-blueprint/known_bug_baseline.json new file mode 100644 index 0000000..ab9ae30 --- /dev/null +++ b/docs/migration/full-modernization-architecture-blueprint/known_bug_baseline.json @@ -0,0 +1,21 @@ +{ + "routes": { + "/wip-overview": { "known_issues": [] }, + "/wip-detail": { "known_issues": [] }, + "/hold-overview": { "known_issues": [] }, + "/hold-detail": { "known_issues": [] }, + "/hold-history": { "known_issues": [] }, + "/reject-history": { "known_issues": [] }, + "/resource": { "known_issues": [] }, + "/resource-history": { "known_issues": [] }, + "/qc-gate": { "known_issues": [] }, + "/job-query": { "known_issues": [] }, + "/tmtt-defect": { "known_issues": [] }, + "/tables": { "known_issues": [] }, + "/excel-query": { "known_issues": [] }, + "/query-tool": { "known_issues": [] }, + "/mid-section-defect": { "known_issues": [] }, + "/admin/pages": { "known_issues": [] }, + "/admin/performance": { "known_issues": [] } + } +} diff --git a/docs/migration/full-modernization-architecture-blueprint/manual_acceptance_records.json b/docs/migration/full-modernization-architecture-blueprint/manual_acceptance_records.json new file mode 100644 index 0000000..aa38702 --- /dev/null +++ b/docs/migration/full-modernization-architecture-blueprint/manual_acceptance_records.json @@ -0,0 +1,3 @@ +{ + "records": [] +} diff --git a/docs/migration/full-modernization-architecture-blueprint/quality_gate_policy.json b/docs/migration/full-modernization-architecture-blueprint/quality_gate_policy.json new file mode 100644 index 0000000..2e2db19 --- /dev/null +++ b/docs/migration/full-modernization-architecture-blueprint/quality_gate_policy.json @@ -0,0 +1,6 @@ +{ + "severity_mode": { + "current": "block" + }, + "deferred_routes_excluded": [] +} diff --git a/docs/migration/full-modernization-architecture-blueprint/route_contracts.json b/docs/migration/full-modernization-architecture-blueprint/route_contracts.json new file mode 100644 index 0000000..9bd63cf --- /dev/null +++ b/docs/migration/full-modernization-architecture-blueprint/route_contracts.json @@ -0,0 +1,208 @@ +{ + "routes": [ + { + "route": "/wip-overview", + "route_id": "wip-overview", + "title": "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", + "compatibility_policy": "redirect_to_shell_when_spa_enabled" + }, + { + "route": "/wip-detail", + "route_id": "wip-detail", + "title": "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", + "compatibility_policy": "redirect_to_shell_when_spa_enabled" + }, + { + "route": "/hold-overview", + "route_id": "hold-overview", + "title": "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", + "compatibility_policy": "redirect_to_shell_when_spa_enabled" + }, + { + "route": "/hold-detail", + "route_id": "hold-detail", + "title": "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", + "compatibility_policy": "redirect_to_shell_when_spa_enabled" + }, + { + "route": "/hold-history", + "route_id": "hold-history", + "title": "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", + "compatibility_policy": "redirect_to_shell_when_spa_enabled" + }, + { + "route": "/reject-history", + "route_id": "reject-history", + "title": "Reject History", + "scope": "in-scope", + "render_mode": "native", + "owner": "frontend-mes-reporting", + "visibility_policy": "released_or_admin", + "canonical_shell_path": "/portal-shell/reject-history", + "rollback_strategy": "fallback_to_legacy_route", + "compatibility_policy": "redirect_to_shell_when_spa_enabled" + }, + { + "route": "/resource", + "route_id": "resource", + "title": "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", + "compatibility_policy": "redirect_to_shell_when_spa_enabled" + }, + { + "route": "/resource-history", + "route_id": "resource-history", + "title": "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", + "compatibility_policy": "redirect_to_shell_when_spa_enabled" + }, + { + "route": "/qc-gate", + "route_id": "qc-gate", + "title": "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", + "compatibility_policy": "redirect_to_shell_when_spa_enabled" + }, + { + "route": "/job-query", + "route_id": "job-query", + "title": "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", + "compatibility_policy": "redirect_to_shell_when_spa_enabled" + }, + { + "route": "/tmtt-defect", + "route_id": "tmtt-defect", + "title": "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", + "compatibility_policy": "redirect_to_shell_when_spa_enabled" + }, + { + "route": "/admin/pages", + "route_id": "admin-pages", + "title": "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", + "compatibility_policy": "external_target_redirect" + }, + { + "route": "/admin/performance", + "route_id": "admin-performance", + "title": "Admin Performance", + "scope": "in-scope", + "render_mode": "native", + "owner": "frontend-platform-admin", + "visibility_policy": "admin_only", + "canonical_shell_path": "/portal-shell/admin/performance", + "rollback_strategy": "fallback_to_legacy_route", + "compatibility_policy": "redirect_to_shell_when_spa_enabled" + }, + { + "route": "/tables", + "route_id": "tables", + "title": "Tables", + "scope": "in-scope", + "render_mode": "native", + "owner": "frontend-mes-reporting", + "visibility_policy": "released_or_admin", + "canonical_shell_path": "/portal-shell/tables", + "rollback_strategy": "fallback_to_legacy_route", + "compatibility_policy": "redirect_to_shell_when_spa_enabled" + }, + { + "route": "/excel-query", + "route_id": "excel-query", + "title": "Excel Query", + "scope": "in-scope", + "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", + "compatibility_policy": "redirect_to_shell_when_spa_enabled" + }, + { + "route": "/query-tool", + "route_id": "query-tool", + "title": "Query Tool", + "scope": "in-scope", + "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", + "compatibility_policy": "redirect_to_shell_when_spa_enabled" + }, + { + "route": "/mid-section-defect", + "route_id": "mid-section-defect", + "title": "Mid Section Defect", + "scope": "in-scope", + "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", + "compatibility_policy": "redirect_to_shell_when_spa_enabled" + } + ] +} diff --git a/docs/migration/full-modernization-architecture-blueprint/route_scope_matrix.json b/docs/migration/full-modernization-architecture-blueprint/route_scope_matrix.json new file mode 100644 index 0000000..bcb00b2 --- /dev/null +++ b/docs/migration/full-modernization-architecture-blueprint/route_scope_matrix.json @@ -0,0 +1,22 @@ +{ + "in_scope": [ + { "route": "/wip-overview", "category": "report" }, + { "route": "/wip-detail", "category": "report" }, + { "route": "/hold-overview", "category": "report" }, + { "route": "/hold-detail", "category": "report" }, + { "route": "/hold-history", "category": "report" }, + { "route": "/reject-history", "category": "report" }, + { "route": "/resource", "category": "report" }, + { "route": "/resource-history", "category": "report" }, + { "route": "/qc-gate", "category": "report" }, + { "route": "/job-query", "category": "report" }, + { "route": "/tmtt-defect", "category": "report" }, + { "route": "/tables", "category": "report" }, + { "route": "/excel-query", "category": "report" }, + { "route": "/query-tool", "category": "report" }, + { "route": "/mid-section-defect", "category": "report" }, + { "route": "/admin/pages", "category": "admin" }, + { "route": "/admin/performance", "category": "admin" } + ], + "deferred": [] +} diff --git a/docs/migration/full-modernization-architecture-blueprint/style_inventory.json b/docs/migration/full-modernization-architecture-blueprint/style_inventory.json new file mode 100644 index 0000000..f3d18ec --- /dev/null +++ b/docs/migration/full-modernization-architecture-blueprint/style_inventory.json @@ -0,0 +1,4 @@ +{ + "routes": {}, + "notes": "Baseline placeholder inventory" +} diff --git a/docs/migration/portal-no-iframe/baseline_api_payload_contracts.json b/docs/migration/portal-no-iframe/baseline_api_payload_contracts.json new file mode 100644 index 0000000..42a4774 --- /dev/null +++ b/docs/migration/portal-no-iframe/baseline_api_payload_contracts.json @@ -0,0 +1,46 @@ +{ + "source": "current frontend API consumption contracts", + "apis": { + "/api/wip/overview/summary": { + "required_keys": [ + "dataUpdateDate", + "runLots", + "queueLots", + "holdLots" + ], + "notes": "summary header and cards depend on these fields" + }, + "/api/wip/overview/matrix": { + "required_keys": [ + "workcenters", + "packages", + "matrix", + "workcenter_totals" + ], + "notes": "matrix table rendering contract" + }, + "/api/wip/hold-detail/summary": { + "required_keys": [ + "workcenterCount", + "packageCount", + "lotCount" + ], + "notes": "hold detail summary cards contract" + }, + "/api/resource/history/summary": { + "required_keys": [ + "kpi", + "trend", + "heatmap", + "workcenter_comparison" + ], + "notes": "resource history chart summary contract" + }, + "/api/resource/history/detail": { + "required_keys": [ + "data" + ], + "notes": "detail table contract (plus truncated/max_records metadata when present)" + } + } +} diff --git a/docs/migration/portal-no-iframe/baseline_drawer_contract_validation.json b/docs/migration/portal-no-iframe/baseline_drawer_contract_validation.json new file mode 100644 index 0000000..177e28b --- /dev/null +++ b/docs/migration/portal-no-iframe/baseline_drawer_contract_validation.json @@ -0,0 +1,4 @@ +{ + "source": "data/page_status.json", + "errors": [] +} diff --git a/docs/migration/portal-no-iframe/baseline_drawer_visibility.json b/docs/migration/portal-no-iframe/baseline_drawer_visibility.json new file mode 100644 index 0000000..cf834dc --- /dev/null +++ b/docs/migration/portal-no-iframe/baseline_drawer_visibility.json @@ -0,0 +1,201 @@ +{ + "source": "data/page_status.json", + "admin": [ + { + "id": "reports", + "name": "即時報表", + "order": 1, + "admin_only": false, + "pages": [ + { + "route": "/wip-overview", + "name": "WIP 即時概況", + "status": "released", + "order": 1 + }, + { + "route": "/hold-overview", + "name": "Hold 即時概況", + "status": "released", + "order": 2 + }, + { + "route": "/resource", + "name": "設備即時概況", + "status": "released", + "order": 4 + }, + { + "route": "/qc-gate", + "name": "QC-GATE 狀態", + "status": "released", + "order": 6 + } + ] + }, + { + "id": "drawer-2", + "name": "歷史報表", + "order": 2, + "admin_only": false, + "pages": [ + { + "route": "/hold-history", + "name": "Hold 歷史績效", + "status": "released", + "order": 3 + }, + { + "route": "/reject-history", + "name": "報廢歷史查詢", + "status": "dev", + "order": 4 + }, + { + "route": "/resource-history", + "name": "設備歷史績效", + "status": "released", + "order": 5 + } + ] + }, + { + "id": "drawer", + "name": "查詢工具", + "order": 3, + "admin_only": false, + "pages": [ + { + "route": "/job-query", + "name": "設備維修查詢", + "status": "released", + "order": 1 + }, + { + "route": "/query-tool", + "name": "批次追蹤工具", + "status": "released", + "order": 2 + } + ] + }, + { + "id": "dev-tools", + "name": "開發工具", + "order": 4, + "admin_only": true, + "pages": [ + { + "route": "/tables", + "name": "表格總覽", + "status": "dev", + "order": 1 + }, + { + "route": "/admin/pages", + "name": "頁面管理", + "status": "released", + "order": 1 + }, + { + "route": "/excel-query", + "name": "Excel 批次查詢", + "status": "dev", + "order": 2 + }, + { + "route": "/admin/performance", + "name": "效能監控", + "status": "dev", + "order": 2 + }, + { + "route": "/tmtt-defect", + "name": "TMTT印字腳型不良分析", + "status": "dev", + "order": 5 + }, + { + "route": "/mid-section-defect", + "name": "中段製程不良追溯", + "status": "dev", + "order": 6 + } + ] + } + ], + "non_admin": [ + { + "id": "reports", + "name": "即時報表", + "order": 1, + "admin_only": false, + "pages": [ + { + "route": "/wip-overview", + "name": "WIP 即時概況", + "status": "released", + "order": 1 + }, + { + "route": "/hold-overview", + "name": "Hold 即時概況", + "status": "released", + "order": 2 + }, + { + "route": "/resource", + "name": "設備即時概況", + "status": "released", + "order": 4 + }, + { + "route": "/qc-gate", + "name": "QC-GATE 狀態", + "status": "released", + "order": 6 + } + ] + }, + { + "id": "drawer-2", + "name": "歷史報表", + "order": 2, + "admin_only": false, + "pages": [ + { + "route": "/hold-history", + "name": "Hold 歷史績效", + "status": "released", + "order": 3 + }, + { + "route": "/resource-history", + "name": "設備歷史績效", + "status": "released", + "order": 5 + } + ] + }, + { + "id": "drawer", + "name": "查詢工具", + "order": 3, + "admin_only": false, + "pages": [ + { + "route": "/job-query", + "name": "設備維修查詢", + "status": "released", + "order": 1 + }, + { + "route": "/query-tool", + "name": "批次追蹤工具", + "status": "released", + "order": 2 + } + ] + } + ] +} diff --git a/docs/migration/portal-no-iframe/baseline_route_query_contracts.json b/docs/migration/portal-no-iframe/baseline_route_query_contracts.json new file mode 100644 index 0000000..220b0ec --- /dev/null +++ b/docs/migration/portal-no-iframe/baseline_route_query_contracts.json @@ -0,0 +1,46 @@ +{ + "source": "frontend route parsing and current parity matrix", + "routes": { + "/wip-overview": { + "query_keys": [ + "workorder", + "lotid", + "package", + "type", + "status" + ], + "notes": "filters + status URL state must remain compatible" + }, + "/wip-detail": { + "query_keys": [ + "workcenter", + "workorder", + "lotid", + "package", + "type", + "status" + ], + "notes": "workcenter deep-link and back-link query continuity" + }, + "/hold-detail": { + "query_keys": [ + "reason" + ], + "notes": "reason required for normal access flow" + }, + "/resource-history": { + "query_keys": [ + "start_date", + "end_date", + "granularity", + "workcenter_groups", + "families", + "resource_ids", + "is_production", + "is_key", + "is_monitor" + ], + "notes": "query/export params must remain compatible" + } + } +} diff --git a/frontend/src/job-query/main.js b/frontend/src/job-query/main.js index 869fab8..0b94584 100644 --- a/frontend/src/job-query/main.js +++ b/frontend/src/job-query/main.js @@ -11,10 +11,27 @@ window.__FIELD_CONTRACTS__['job_query:txn_table'] = getPageContract('job_query', const jobTableFields = getPageContract('job_query', 'jobs_table'); const txnTableFields = getPageContract('job_query', 'txn_table'); +function toDataToken(value) { + return encodeURIComponent(safeText(value)); +} + +function fromDataToken(value) { + if (!value) { + return ''; + } + try { + return decodeURIComponent(value); + } catch (_error) { + return value; + } +} + function renderJobCell(job, apiKey) { if (apiKey === 'JOBSTATUS') { const value = safeText(job[apiKey]); - return `${value}`; + const classToken = safeText(value).replace(/[^A-Za-z0-9_-]/g, '_'); + const escaped = escapeHtml(value); + return `${escaped}`; } if (apiKey === 'CREATEDATE' || apiKey === 'COMPLETEDATE') { return formatDate(job[apiKey]); @@ -25,7 +42,9 @@ function renderJobCell(job, apiKey) { function renderTxnCell(txn, apiKey) { if (apiKey === 'FROMJOBSTATUS' || apiKey === 'JOBSTATUS') { const value = safeText(txn[apiKey], '-'); - return `${escapeHtml(value)}`; + const classToken = safeText(value).replace(/[^A-Za-z0-9_-]/g, '_'); + const escaped = escapeHtml(value); + return `${escaped}`; } if (apiKey === 'TXNDATE') { return formatDate(txn[apiKey]); @@ -48,6 +67,16 @@ function renderTxnCell(txn, apiKey) { loadEquipments(); setLast90Days(); + const equipmentList = document.getElementById('equipmentList'); + if (equipmentList) { + equipmentList.addEventListener('click', handleEquipmentListClick); + } + + const resultSection = document.getElementById('resultSection'); + if (resultSection) { + resultSection.addEventListener('click', handleResultSectionClick); + } + // Close dropdown when clicking outside document.addEventListener('click', (e) => { const dropdown = document.getElementById('equipmentDropdown'); @@ -94,20 +123,22 @@ function renderTxnCell(txn, apiKey) { const allSelected = selectedInGroup === groupIds.length; const someSelected = selectedInGroup > 0 && !allSelected; const escapedName = escapeHtml(workcenterName); - html += `