請輸入 LOT ID、流水號或工單後,點擊「查詢」
+diff --git a/.env.example b/.env.example index 60c0bd3..92a6a7c 100644 --- a/.env.example +++ b/.env.example @@ -103,6 +103,17 @@ RESOURCE_CACHE_ENABLED=true # The cache will check for updates at this interval using MAX(LASTCHANGEDATE) RESOURCE_SYNC_INTERVAL=14400 +# Resource index version check interval in seconds (default: 5) +RESOURCE_INDEX_VERSION_CHECK_INTERVAL=5 + +# Realtime equipment status cache toggle and sync interval +REALTIME_EQUIPMENT_CACHE_ENABLED=true +EQUIPMENT_STATUS_SYNC_INTERVAL=300 + +# Filter cache SQL view overrides +FILTER_CACHE_WIP_VIEW=DWH.DW_MES_LOT_V +FILTER_CACHE_SPEC_WORKCENTER_VIEW=DWH.DW_MES_SPEC_WORKCENTER_V + # ============================================================ # Circuit Breaker Configuration # ============================================================ @@ -128,6 +139,9 @@ CIRCUIT_BREAKER_WINDOW_SIZE=10 # Note: Real-time Oracle views may take 2-5s per query, set threshold accordingly SLOW_QUERY_THRESHOLD=5.0 +# In-memory query metrics sliding window size +METRICS_WINDOW_SIZE=1000 + # ============================================================ # SQLite Log Store Configuration # ============================================================ @@ -164,6 +178,12 @@ WATCHDOG_RESTART_HISTORY_MAX=50 # Cooldown period between restart requests in seconds (default: 60) WORKER_RESTART_COOLDOWN=60 +# Watchdog loop check interval in seconds +WATCHDOG_CHECK_INTERVAL=5 + +# Runtime contract strict validation toggle +RUNTIME_CONTRACT_ENFORCE=false + # ============================================================ # Runtime Resilience Diagnostics Thresholds # ============================================================ @@ -185,3 +205,6 @@ RESILIENCE_RESTART_CHURN_THRESHOLD=3 # Example: https://example.com,https://app.example.com # Set to * for development (not recommended for production) CORS_ALLOWED_ORIGINS= + +# Health endpoint memo cache TTL in seconds +HEALTH_MEMO_TTL_SECONDS=5 diff --git a/data/page_status.json b/data/page_status.json index 427a548..f6d87a6 100644 --- a/data/page_status.json +++ b/data/page_status.json @@ -44,6 +44,16 @@ "route": "/job-query", "name": "設備維修查詢", "status": "released" + }, + { + "route": "/query-tool", + "name": "批次追蹤工具", + "status": "released" + }, + { + "route": "/tmtt-defect", + "name": "TMTT印字腳型不良分析", + "status": "dev" } ], "api_public": true, @@ -53,4 +63,4 @@ "object_count": 19, "source": "tools/query_table_schema.py" } -} \ No newline at end of file +} diff --git a/docs/env_sync_report.md b/docs/env_sync_report.md new file mode 100644 index 0000000..acb9285 --- /dev/null +++ b/docs/env_sync_report.md @@ -0,0 +1,26 @@ +# Environment Sync Report + +- Source: `/home/egg/Project/DashBoard/.env` +- Target: `/home/egg/Project/DashBoard_vite/.env` +- Example Baseline: `.env.example` + +- Keys in source env: 39 +- Keys in vite .env.example: 54 +- Missing keys auto-added to target: 15 + +## Auto-Added Keys +- `DB_POOL_TIMEOUT` +- `DB_POOL_RECYCLE` +- `DB_TCP_CONNECT_TIMEOUT` +- `DB_CONNECT_RETRY_COUNT` +- `DB_CONNECT_RETRY_DELAY` +- `DB_CALL_TIMEOUT_MS` +- `WIP_CACHE_TTL_SECONDS` +- `WATCHDOG_RUNTIME_DIR` +- `WATCHDOG_PID_FILE` +- `WATCHDOG_RESTART_HISTORY_MAX` +- `RESILIENCE_DEGRADED_ALERT_SECONDS` +- `RESILIENCE_POOL_SATURATION_WARNING` +- `RESILIENCE_POOL_SATURATION_CRITICAL` +- `RESILIENCE_RESTART_CHURN_WINDOW_SECONDS` +- `RESILIENCE_RESTART_CHURN_THRESHOLD` diff --git a/docs/env_usage_gap_report.md b/docs/env_usage_gap_report.md new file mode 100644 index 0000000..85239c6 --- /dev/null +++ b/docs/env_usage_gap_report.md @@ -0,0 +1,9 @@ +# Environment Usage Gap Report + +- Parsed env keys used in code/tests: 37 +- Keys present in `.env`: 63 +- Missing keys: 2 + +## Missing Keys +- `CONDA_DEFAULT_ENV` +- `PYTEST_CURRENT_TEST` diff --git a/docs/migration_validation_evidence.md b/docs/migration_validation_evidence.md index 9bd104b..b62525b 100644 --- a/docs/migration_validation_evidence.md +++ b/docs/migration_validation_evidence.md @@ -26,6 +26,7 @@ Result: - PASS - `routes 71` - Redis/Oracle warnings observed in this local environment; app factory and route registration still completed. +- Note: current tree includes additional routes (query-tool / tmtt-defect / hardening paths), so fresh smoke now reports `routes 83`. ## Focused Test Gate (root project) diff --git a/docs/root_cutover_inventory.md b/docs/root_cutover_inventory.md index 8b50439..882f7af 100644 --- a/docs/root_cutover_inventory.md +++ b/docs/root_cutover_inventory.md @@ -41,7 +41,7 @@ ### App import smoke - `PYTHONPATH=src python -c "from mes_dashboard.app import create_app; app=create_app('testing'); print(app.url_map)"` -- Verified route initialization count (`routes 71`) in root-only execution context. +- Verified route initialization count (`routes 83`) in root-only execution context. ### HTTP smoke (Flask test client) - Verify page renders and module asset tags resolve/fallback: diff --git a/frontend/src/portal/main.js b/frontend/src/portal/main.js index 0852c7e..fd4be83 100644 --- a/frontend/src/portal/main.js +++ b/frontend/src/portal/main.js @@ -177,9 +177,8 @@ import './portal.css'; window.openTool = openTool; window.toggleHealthPopup = toggleHealthPopup; - if (healthStatus) { - healthStatus.addEventListener('click', toggleHealthPopup); - } + // Click handler is wired via inline onclick in template for fallback compatibility. + // Avoid duplicate binding here, otherwise a single click toggles twice. document.addEventListener('click', (e) => { if (!e.target.closest('#healthStatus') && !e.target.closest('#healthPopup') && healthPopup) { healthPopup.classList.remove('show'); diff --git a/frontend/src/query-tool/main.js b/frontend/src/query-tool/main.js new file mode 100644 index 0000000..4a04027 --- /dev/null +++ b/frontend/src/query-tool/main.js @@ -0,0 +1,3139 @@ +import { ensureMesApiAvailable } from '../core/api.js'; + +ensureMesApiAvailable(); + +/** + * Query Tool JavaScript + * + * Handles batch tracing and equipment period query functionality. + */ + +// ============================================================ +// State Management +// ============================================================ + +const QueryToolState = { + // LOT query + queryType: 'lot_id', + resolvedLots: [], + selectedLotIndex: 0, + lotHistories: {}, // container_id -> history data + lotAssociations: {}, // container_id -> { materials, rejects, holds, jobs } + + // Timeline + timelineSelectedLots: new Set(), // Set of indices for timeline display + currentLotIndex: 0, // For association highlight + + // Workcenter group filter + workcenterGroups: [], // All available groups [{name, sequence}] + selectedWorkcenterGroups: new Set(), // Selected group names for filtering + + // Equipment query + allEquipments: [], + selectedEquipments: new Set(), + equipmentResults: null, +}; + +// Expose for debugging +window.QueryToolState = QueryToolState; + +// ============================================================ +// State Cleanup (Memory Management) +// ============================================================ + +/** + * Clear all query state to free memory before new query or page unload. + * This prevents browser memory issues with large datasets. + */ +function clearQueryState() { + // Clear LOT query state + QueryToolState.resolvedLots = []; + QueryToolState.selectedLotIndex = 0; + QueryToolState.lotHistories = {}; + QueryToolState.lotAssociations = {}; + QueryToolState.timelineSelectedLots = new Set(); + QueryToolState.currentLotIndex = 0; + + // Clear workcenter group selection (keep workcenterGroups as it's reused) + QueryToolState.selectedWorkcenterGroups = new Set(); + + // Hide selection bar (contains LOT selector and workcenter filter) + const selectionBar = document.getElementById('selectionBar'); + if (selectionBar) selectionBar.style.display = 'none'; + + // Clear equipment query state + QueryToolState.equipmentResults = null; + // Note: Keep allEquipments and selectedEquipments as they are reused + + // Clear global timeline data (can be large) + if (window._timelineData) { + window._timelineData.lotsData = []; + window._timelineData.stationColors = {}; + window._timelineData.allStations = []; + window._timelineData.selectedStations = new Set(); + window._timelineData = null; + } + + // Close any open popups + closeTimelinePopup(); + + // Clear DOM content + const lotResultsContent = document.getElementById('lotResultsContent'); + if (lotResultsContent) { + lotResultsContent.innerHTML = ''; + lotResultsContent.style.display = 'none'; + } + + // Reset empty state visibility + const lotEmptyState = document.getElementById('lotEmptyState'); + if (lotEmptyState) { + lotEmptyState.style.display = 'block'; + } + + // Hide LOT info bar + const lotInfoBar = document.getElementById('lotInfoBar'); + if (lotInfoBar) lotInfoBar.style.display = 'none'; + + console.log('[QueryTool] State cleared for memory management'); +} + +// Clear state before page unload to help garbage collection +window.addEventListener('beforeunload', () => { + clearQueryState(); +}); + +// Expose for manual cleanup if needed +window.clearQueryState = clearQueryState; + +// ============================================================ +// Initialization +// ============================================================ + +document.addEventListener('DOMContentLoaded', () => { + loadEquipments(); + loadWorkcenterGroups(); // Load workcenter groups for filtering + setLast30Days(); + + // Close dropdowns when clicking outside + document.addEventListener('click', (e) => { + // Equipment dropdown + const eqDropdown = document.getElementById('equipmentDropdown'); + const eqSelector = document.querySelector('.equipment-selector'); + if (eqSelector && !eqSelector.contains(e.target)) { + eqDropdown.classList.remove('show'); + } + + // LOT selector dropdown + const lotDropdown = document.getElementById('lotSelectorDropdown'); + const lotSelector = document.getElementById('lotSelectorContainer'); + if (lotSelector && !lotSelector.contains(e.target)) { + lotDropdown.classList.remove('show'); + } + + // Workcenter group dropdown + const wcDropdown = document.getElementById('wcGroupDropdown'); + const wcSelector = document.getElementById('workcenterGroupSelectorContainer'); + if (wcSelector && !wcSelector.contains(e.target)) { + if (wcDropdown) wcDropdown.classList.remove('show'); + } + }); + + // Handle Enter key in search input + const searchInput = document.getElementById('lotInputField'); + if (searchInput) { + searchInput.addEventListener('keypress', (e) => { + if (e.key === 'Enter') { + executeLotQuery(); + } + }); + } +}); + +// ============================================================ +// Query Mode Switching (Batch vs Equipment) +// ============================================================ + +function switchQueryMode(mode) { + // Update tabs + document.querySelectorAll('.query-mode-tab').forEach(tab => { + tab.classList.toggle('active', tab.dataset.mode === mode); + }); + + // Show/hide filter bars + document.getElementById('batchFilterBar').style.display = mode === 'batch' ? 'flex' : 'none'; + document.getElementById('equipmentFilterBar').style.display = mode === 'equipment' ? 'flex' : 'none'; + + // Show/hide results panels + document.getElementById('batchResultsPanel').style.display = mode === 'batch' ? 'block' : 'none'; + document.getElementById('equipmentResultsPanel').style.display = mode === 'equipment' ? 'block' : 'none'; + + // Hide LOT info bar when switching to equipment mode + if (mode === 'equipment') { + document.getElementById('lotInfoBar').style.display = 'none'; + } +} + +// ============================================================ +// Query Type Selection +// ============================================================ + +function setQueryType(type) { + QueryToolState.queryType = type; + + // Update select element if called programmatically + const select = document.getElementById('queryTypeSelect'); + if (select && select.value !== type) { + select.value = type; + } + + // Update input placeholder based on type + const placeholders = { + 'lot_id': '輸入 LOT ID(多筆以逗號分隔)', + 'serial_number': '輸入流水號(多筆以逗號分隔)', + 'work_order': '輸入 GA工單(多筆以逗號分隔)', + }; + + const inputField = document.getElementById('lotInputField'); + if (inputField) { + inputField.placeholder = placeholders[type] || placeholders['lot_id']; + } +} + +// ============================================================ +// LOT Query Functions +// ============================================================ + +function parseInputValues(text) { + // Parse input: split by newlines and commas, trim whitespace, filter empty + return text + .split(/[\n,]/) + .map(s => s.trim()) + .filter(s => s.length > 0); +} + +async function executeLotQuery() { + const input = document.getElementById('lotInputField').value; + const values = parseInputValues(input); + + if (values.length === 0) { + Toast.error('請輸入查詢條件'); + return; + } + + // Validate limits + const limits = { 'lot_id': 50, 'serial_number': 50, 'work_order': 10 }; + const limit = limits[QueryToolState.queryType]; + if (values.length > limit) { + Toast.error(`輸入數量超過上限 (${limit} 筆)`); + return; + } + + // Clear previous query state to free memory + clearQueryState(); + + // Show loading + document.getElementById('lotEmptyState').style.display = 'none'; + document.getElementById('lotResultsContent').style.display = 'block'; + document.getElementById('lotResultsContent').innerHTML = ` +
查無符合的批次資料
+ ${resolveResult.not_found && resolveResult.not_found.length > 0 + ? `未找到: ${resolveResult.not_found.join(', ')}
` + : ''} +請選擇要查看的批次
| LOT ID | +站點 | +設備 | +規格 | +產品類型 | +BOP | +Wafer Lot | +上機時間 | +下機時間 | +入數 | +出數 | +操作 | +
|---|---|---|---|---|---|---|---|---|---|---|---|
| ${step.LOT_ID} | +${step.WORKCENTERNAME || ''} | +${step.EQUIPMENTNAME || ''} | +${truncateText(step.SPECNAME, 15)} | +${step.PJ_TYPE || '-'} | +${step.PJ_BOP || '-'} | +${step.WAFER_LOT_ID || '-'} | +${formatDateTime(step.TRACKINTIMESTAMP)} | +${formatDateTime(step.TRACKOUTTIMESTAMP)} | +${step.TRACKINQTY || ''} | +${step.TRACKOUTQTY || ''} | ++ + | +
無${getAssocLabel(type)}資料
| ${colLabels[col] || col} | `; + }); + html += `
|---|
| ${value !== null && value !== undefined ? value : ''} | `; + }); + html += `
無拆併批紀錄
| 操作 | +來源批次 | +目標批次 | +數量 | +時間 | +
|---|---|---|---|---|
| ${record.operation_type_display} | +${record.source_lot || '-'} | +${record.target_lot || '-'} | +${record.target_qty || '-'} | +${formatDateTime(record.txn_date)} | +
無生產歷程資料
| # | +站點 | +設備 | +規格 | +產品類型 | +BOP | +Wafer Lot | +上機時間 | +下機時間 | +入數 | +出數 | +操作 | +
|---|---|---|---|---|---|---|---|---|---|---|---|
| ${idx + 1} | +${step.WORKCENTERNAME || ''} | +${step.EQUIPMENTNAME || ''} | +${truncateText(step.SPECNAME, 12)} | +${step.PJ_TYPE || '-'} | +${step.PJ_BOP || '-'} | +${step.WAFER_LOT_ID || '-'} | +${formatDateTime(step.TRACKINTIMESTAMP)} | +${formatDateTime(step.TRACKOUTTIMESTAMP)} | +${step.TRACKINQTY || ''} | +${step.TRACKOUTQTY || ''} | ++ + | +
無${getAssocLabel(type)}資料
| ${colLabels[col] || col} | `; + }); + html += `
|---|
| ${value !== null && value !== undefined ? value : ''} | `; + }); + html += `
| 操作 | +來源批次 | +目標批次 | +數量 | +時間 | +
|---|---|---|---|---|
| ${record.operation_type_display} | +${record.source_lot || '-'} | +${record.target_lot || '-'} | +${record.target_qty || '-'} | +${formatDateTime(record.txn_date)} | +
無生產歷程資料可顯示
無前後批資料
| 相對位置 | +LOT ID | +產品類型 | +BOP | +Wafer Lot | +工單 | +批次號 | +上機時間 | +下機時間 | +上機數 | +下機數 | +
|---|---|---|---|---|---|---|---|---|---|---|
| ${posLabel} | +${lot.CONTAINERNAME || '-'} | +${lot.PJ_TYPE || '-'} | +${lot.PJ_BOP || '-'} | +${lot.WAFER_LOT_ID || '-'} | +${lot.PJ_WORKORDER || '-'} | +${lot.FINISHEDRUNCARD || ''} | +${formatDateTime(lot.TRACKINTIMESTAMP)} | +${formatDateTime(lot.TRACKOUTTIMESTAMP)} | +${lot.TRACKINQTY || ''} | +${lot.TRACKOUTQTY || ''} | +
無資料
| ${def.labels[col] || col} | `; + }); + html += `
|---|
| ${value !== null && value !== undefined ? value : ''} | `; + }); + html += `
查無符合的批次資料
+ ${resolveResult.not_found && resolveResult.not_found.length > 0 + ? `未找到: ${resolveResult.not_found.join(', ')}
` + : ''} +請選擇要查看的批次
| LOT ID | +站點 | +設備 | +規格 | +產品類型 | +BOP | +Wafer Lot | +上機時間 | +下機時間 | +入數 | +出數 | +操作 | +
|---|---|---|---|---|---|---|---|---|---|---|---|
| ${step.LOT_ID} | +${step.WORKCENTERNAME || ''} | +${step.EQUIPMENTNAME || ''} | +${truncateText(step.SPECNAME, 15)} | +${step.PJ_TYPE || '-'} | +${step.PJ_BOP || '-'} | +${step.WAFER_LOT_ID || '-'} | +${formatDateTime(step.TRACKINTIMESTAMP)} | +${formatDateTime(step.TRACKOUTTIMESTAMP)} | +${step.TRACKINQTY || ''} | +${step.TRACKOUTQTY || ''} | ++ + | +
無${getAssocLabel(type)}資料
| ${colLabels[col] || col} | `; + }); + html += `
|---|
| ${value !== null && value !== undefined ? value : ''} | `; + }); + html += `
無拆併批紀錄
| 操作 | +來源批次 | +目標批次 | +數量 | +時間 | +
|---|---|---|---|---|
| ${record.operation_type_display} | +${record.source_lot || '-'} | +${record.target_lot || '-'} | +${record.target_qty || '-'} | +${formatDateTime(record.txn_date)} | +
無生產歷程資料
| # | +站點 | +設備 | +規格 | +產品類型 | +BOP | +Wafer Lot | +上機時間 | +下機時間 | +入數 | +出數 | +操作 | +
|---|---|---|---|---|---|---|---|---|---|---|---|
| ${idx + 1} | +${step.WORKCENTERNAME || ''} | +${step.EQUIPMENTNAME || ''} | +${truncateText(step.SPECNAME, 12)} | +${step.PJ_TYPE || '-'} | +${step.PJ_BOP || '-'} | +${step.WAFER_LOT_ID || '-'} | +${formatDateTime(step.TRACKINTIMESTAMP)} | +${formatDateTime(step.TRACKOUTTIMESTAMP)} | +${step.TRACKINQTY || ''} | +${step.TRACKOUTQTY || ''} | ++ + | +
無${getAssocLabel(type)}資料
| ${colLabels[col] || col} | `; + }); + html += `
|---|
| ${value !== null && value !== undefined ? value : ''} | `; + }); + html += `
| 操作 | +來源批次 | +目標批次 | +數量 | +時間 | +
|---|---|---|---|---|
| ${record.operation_type_display} | +${record.source_lot || '-'} | +${record.target_lot || '-'} | +${record.target_qty || '-'} | +${formatDateTime(record.txn_date)} | +
無生產歷程資料可顯示
無前後批資料
| 相對位置 | +LOT ID | +產品類型 | +BOP | +Wafer Lot | +工單 | +批次號 | +上機時間 | +下機時間 | +上機數 | +下機數 | +
|---|---|---|---|---|---|---|---|---|---|---|
| ${posLabel} | +${lot.CONTAINERNAME || '-'} | +${lot.PJ_TYPE || '-'} | +${lot.PJ_BOP || '-'} | +${lot.WAFER_LOT_ID || '-'} | +${lot.PJ_WORKORDER || '-'} | +${lot.FINISHEDRUNCARD || ''} | +${formatDateTime(lot.TRACKINTIMESTAMP)} | +${formatDateTime(lot.TRACKOUTTIMESTAMP)} | +${lot.TRACKINQTY || ''} | +${lot.TRACKOUTQTY || ''} | +
無資料
| ${def.labels[col] || col} | `; + }); + html += `
|---|
| ${value !== null && value !== undefined ? value : ''} | `; + }); + html += `
批次生產歷程追蹤、前後批比對、設備時段綜合查詢
+請輸入 LOT ID、流水號或工單後,點擊「查詢」
+請選擇日期範圍後點擊「查詢」
+