chore: reinitialize project with vite architecture

This commit is contained in:
beabigegg
2026-02-08 08:30:48 +08:00
commit b56e80381b
264 changed files with 75752 additions and 0 deletions

View File

@@ -0,0 +1,60 @@
SELECT L.LOTID AS ""Run Card Lot ID"",
L.Workorder AS ""Work Order ID"",
L.Qty AS ""Lot Qty(pcs)"",
L.Qty2 AS ""Lot Qty(Wafer pcs)"",
L.Status AS ""Run Card Status"",
L.HOLDREASONNAME AS ""Hold Reason"",
L.CurrentHoldCount AS ""Hold Count"",
L.Owner AS ""Work Order Owner"",
L.StartDate AS ""Run Card Start Date"",
L.UTS,
L.Product AS ""Product P/N"",
L.Productlinename AS ""Package"",
L.Package_LEF as ""Package(LF)"",
L.PJ_FUNCTION AS ""Product Function"",
L.Pj_Type AS ""Product Type"",
L.BOP,
L.FirstName AS ""Wafer Lot ID"",
L.WAFERNAME AS ""Wafer P/N"",
L.WaferLot ""Wafer Lot ID(Prefix)"",
L.SpecName AS ""Spec"",
L.SPECSEQUENCE AS ""Spec Sequence"",
L.SPECSEQUENCE || '_' || L.SpecName AS ""Spec(Order)"",
L.Workcentername AS ""Work Center"",
L.WorkCenterSequence AS ""Work Center Sequence"",
L.WorkCenter_Group AS ""Work Center(Group)"",
L.WorkCenter_Short AS ""Work Center(Short)"",
L.WorkCenterSequence_Group AS ""Work Center Sequence(Group)"",
L.WorkCenterSequence_Group || '_' || L.WorkCenter_Group AS ""Work Center Group(Order)"",
L.AgeByDays AS ""Age By Days"",
L.Equipments AS ""Equipment ID"",
L.EquipmentCount AS ""Equipment Count"",
L.Workflowname AS ""Work Flow Name"",
L.Datecode AS ""Product Date Code"",
L.LEADFRAMENAME AS ""LF Material Part"",
L.LEADFRAMEOPTION AS ""LF Option ID"",
L.COMNAME AS ""Compound Material Part"",
L.LOCATIONNAME AS ""Run Card Location"",
L.Eventname AS ""NCR ID"",
L.Occurrencedate AS ""NCR-issued Time"",
L.ReleaseTime AS ""Release Time"",
L.ReleaseEmp AS ""Release Employee"",
L.ReleaseReason AS ""Release Comment"",
L.COMMENT_HOLD AS ""Hold Comment"",
L.CONTAINERCOMMENTS AS ""Comment"",
L.COMMENT_DATE AS ""Run Card Comment"",
L.COMMENT_EMP AS ""Run Card Comment Employee"",
L.COMMENT_FUTURE AS ""Future Hold Comment"",
L.HOLDEMP AS ""Hold Employee"",
L.DEPTNAME AS ""Hold Employee Dept"",
L.PJ_PRODUCEREGION AS ""Produce Region"",
L.Prioritycodename AS ""Work Order Priority"",
L.TMTT_R AS ""TMTT Remaining"",
L.wafer_factor AS ""Die Consumption Qty"",
Case When (L.EquipmentCount>0) Then 'RUN'
When (L.CurrentHoldCount>0) Then 'HOLD'
ELSE 'QUENE' End AS ""WIP Status"",
Case When (L.EquipmentCount>0) Then 1
When (L.CurrentHoldCount>0) Then 3
ELSE 2 End AS ""WIP Status Sequence"",
sys_date AS ""Data Update Date""

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,36 @@
# Oracle 可使用 TABLE/VIEW 清單DWH
**產生時間**: 2026-01-29 13:34:22
**使用者**: (詳見 .env 中的 DB_USER)
**Schema**: DWH
## 摘要
- 可使用物件總數: 19
- TABLE: 16
- VIEW: 3
- 來源 (去重後物件數): DIRECT 19, PUBLIC 0, ROLE 0, SYSTEM 0
## 物件清單
| 物件 | 類型 | 權限 | 授權來源 |
|------|------|------|----------|
| `DWH.DW_MES_CONTAINER` | TABLE | SELECT | DIRECT |
| `DWH.DW_MES_EQUIPMENTSTATUS_WIP_V` | VIEW | SELECT | DIRECT |
| `DWH.DW_MES_HM_LOTMOVEOUT` | TABLE | SELECT | DIRECT |
| `DWH.DW_MES_HOLDRELEASEHISTORY` | TABLE | SELECT | DIRECT |
| `DWH.DW_MES_JOB` | TABLE | SELECT | DIRECT |
| `DWH.DW_MES_JOBTXNHISTORY` | TABLE | SELECT | DIRECT |
| `DWH.DW_MES_LOTMATERIALSHISTORY` | TABLE | SELECT | DIRECT |
| `DWH.DW_MES_LOTREJECTHISTORY` | TABLE | SELECT | DIRECT |
| `DWH.DW_MES_LOTWIPDATAHISTORY` | TABLE | SELECT | DIRECT |
| `DWH.DW_MES_LOTWIPHISTORY` | TABLE | SELECT | DIRECT |
| `DWH.DW_MES_LOT_V` | VIEW | SELECT | DIRECT |
| `DWH.DW_MES_MAINTENANCE` | TABLE | SELECT | DIRECT |
| `DWH.DW_MES_PARTREQUESTORDER` | TABLE | SELECT | DIRECT |
| `DWH.DW_MES_PJ_COMBINEDASSYLOTS` | TABLE | SELECT | DIRECT |
| `DWH.DW_MES_RESOURCE` | TABLE | SELECT | DIRECT |
| `DWH.DW_MES_RESOURCESTATUS` | TABLE | SELECT | DIRECT |
| `DWH.DW_MES_RESOURCESTATUS_SHIFT` | TABLE | SELECT | DIRECT |
| `DWH.DW_MES_SPEC_WORKCENTER_V` | VIEW | SELECT | DIRECT |
| `DWH.DW_MES_WIP` | TABLE | SELECT | DIRECT |

View File

@@ -0,0 +1,936 @@
# MES Dashboard - Architecture Findings
本文件記錄專案開發過程中確立的架構設計、全局規範與資料處理規則。
---
## 1. 資料庫連線管理
### 連線池統一使用
所有資料庫操作必須透過 `mes_dashboard.core.database` 模組:
```python
from mes_dashboard.core.database import read_sql_df, get_engine
# 讀取資料 (推薦方式)
df = read_sql_df(sql, params)
# 取得 engine若需要直接操作
engine = get_engine()
```
### 連線池配置 (位置: `core/database.py`)
| 參數 | 開發環境 | 生產環境 | 說明 |
|------|---------|---------|------|
| pool_size | 2 | 10 | 基礎連線數 |
| max_overflow | 3 | 20 | 額外連線數 |
| pool_timeout | 30 | 30 | 等待超時 (秒) |
| pool_recycle | 1800 | 1800 | 回收週期 (30分鐘) |
| pool_pre_ping | True | True | 使用前驗證連線 |
### Keep-Alive 機制
- 背景執行緒每 5 分鐘執行 `SELECT 1 FROM DUAL`
- 防止 NAT/防火牆斷開閒置連線
- 啟動: `start_keepalive()`,停止: `stop_keepalive()`
### 注意事項
- **禁止**在各 service 中自行建立連線
- **禁止**直接使用 `oracledb.connect()`
- 連線池由 `database.py` 統一管理,避免連線洩漏
- 測試環境需在 setUp 中重置:`db._ENGINE = None`
---
## 2. SQL 集中管理
### 目錄結構
所有 SQL 查詢放在 `src/mes_dashboard/sql/` 目錄:
```
sql/
├── loader.py # SQL 檔案載入器 (LRU 快取)
├── builder.py # 參數化查詢構建器
├── filters.py # 通用篩選條件
├── dashboard/ # 儀表板 SQL
│ ├── kpi.sql
│ ├── heatmap.sql
│ └── workcenter_cards.sql
├── wip/ # WIP SQL
│ ├── summary.sql
│ └── detail.sql
├── resource/ # 設備 SQL
│ ├── by_status.sql
│ └── detail.sql
├── resource_history/ # 歷史 SQL
└── job_query/ # 維修工單 SQL
```
### SQLLoader 使用方式
```python
from mes_dashboard.sql.loader import SQLLoader
# 載入 SQL 檔案 (自動 LRU 快取,最多 100 個)
sql = SQLLoader.load("wip/summary")
# 結構性參數替換 (用於 SQL 片段)
sql = SQLLoader.load_with_params("dashboard/kpi",
LATEST_STATUS_SUBQUERY="...",
WHERE_CLAUSE="...")
# 清除快取
SQLLoader.clear_cache()
```
### QueryBuilder 使用方式
```python
from mes_dashboard.sql.builder import QueryBuilder
builder = QueryBuilder()
# 添加條件 (自動參數化,防 SQL 注入)
builder.add_param_condition("STATUS", "PRD")
builder.add_in_condition("STATUS", ["PRD", "SBY"])
builder.add_not_in_condition("HOLD_REASON", exclude_list)
builder.add_like_condition("LOTID", user_input, position="both")
builder.add_or_like_conditions(["COL1", "COL2"], [val1, val2])
builder.add_is_null("COLUMN")
builder.add_is_not_null("COLUMN")
builder.add_condition("FIXED_CONDITION = 1") # 固定條件
# 構建 WHERE 子句
where_clause, params = builder.build_where_only()
# 替換佔位符並執行
sql = sql.replace("{{ WHERE_CLAUSE }}", where_clause)
df = read_sql_df(sql, params)
```
### 佔位符規範
| 類型 | 語法 | 用途 | 安全性 |
|------|------|------|--------|
| 結構性 | `{{ PLACEHOLDER }}` | 靜態 SQL 片段 | 僅限預定義值 |
| 參數 | `:param_name` | 動態用戶輸入 | Oracle bind variables |
### Oracle IN 子句限制
Oracle IN 子句上限 1000 個值,需分批處理:
```python
BATCH_SIZE = 1000
# 參考 job_query_service.py 的 _build_resource_filter()
```
---
## 3. 快取機制
### 多層快取架構
```
請求 → 進程級快取 (30 秒 TTL)
→ Redis 快取 (可配置 TTL)
→ Oracle 資料庫
```
### 全局快取 API
使用 `mes_dashboard.core.cache` 模組:
```python
from mes_dashboard.core.cache import cache_get, cache_set, make_cache_key
# 建立快取 key支援 filters dict
cache_key = make_cache_key("resource_history_summary", filters={
'start_date': start_date,
'workcenter_groups': sorted(groups) if groups else None,
})
# 讀取/寫入快取
result = cache_get(cache_key)
if result is None:
result = query_data()
cache_set(cache_key, result, ttl=CACHE_TTL_TREND)
```
### 快取 TTL 常數
定義於 `mes_dashboard.config.constants`
```python
CACHE_TTL_DEFAULT = 60 # 1 分鐘
CACHE_TTL_FILTER_OPTIONS = 600 # 10 分鐘
CACHE_TTL_PIVOT_COLUMNS = 300 # 5 分鐘
CACHE_TTL_KPI = 60 # 1 分鐘
CACHE_TTL_TREND = 300 # 5 分鐘
```
### Redis 快取配置
環境變數:
```
REDIS_ENABLED=true
REDIS_URL=redis://localhost:6379/0
REDIS_KEY_PREFIX=mes_wip
```
### 專用快取服務
| 服務 | 位置 | 用途 |
|------|------|------|
| WIP 快取更新器 | `core/cache_updater.py` | 背景線程自動更新 WIP 數據 |
| 資源快取 | `services/resource_cache.py` | DW_MES_RESOURCE 表快取 (4 小時同步) |
| 設備狀態快取 | `services/realtime_equipment_cache.py` | 設備實時狀態 (5 分鐘同步) |
| Filter 快取 | `services/filter_cache.py` | 篩選選項快取 |
---
## 4. Filter Cache篩選選項快取
### 位置
`mes_dashboard.services.filter_cache`
### 用途
快取全站共用的篩選選項,避免重複查詢資料庫:
```python
from mes_dashboard.services.filter_cache import (
get_workcenter_groups, # 取得 workcenter group 列表
get_workcenter_mapping, # 取得 workcentername → group 對應
get_workcenters_for_groups, # 根據 group 取得 workcentername 列表
get_resource_families, # 取得 resource family 列表
)
```
### Workcenter 對應關係
```
WORKCENTERNAME (資料庫) → WORKCENTER_GROUP (顯示)
焊接_DB_1 → 焊接_DB
焊接_DB_2 → 焊接_DB
成型_1 → 成型
```
### 資料來源
- Workcenter Groups: `DW_PJ_LOT_V` (WORKCENTER_GROUP, WORKCENTERSEQUENCE_GROUP)
- Resource Families: `DW_MES_RESOURCE` (RESOURCEFAMILYNAME)
---
## 5. 熔斷器 (Circuit Breaker)
### 位置
`mes_dashboard.core.circuit_breaker`
### 狀態機制
```
CLOSED (正常)
↓ 失敗達到閾值
OPEN (故障,拒絕請求)
↓ 等待 recovery_timeout
HALF_OPEN (測試恢復)
↓ 成功 → CLOSED / 失敗 → OPEN
```
### 配置 (環境變數)
```
CIRCUIT_BREAKER_ENABLED=true
CIRCUIT_BREAKER_FAILURE_THRESHOLD=5 # 最少失敗次數
CIRCUIT_BREAKER_FAILURE_RATE=0.5 # 失敗率閾值 (0.0-1.0)
CIRCUIT_BREAKER_RECOVERY_TIMEOUT=30 # OPEN 狀態等待秒數
CIRCUIT_BREAKER_WINDOW_SIZE=10 # 滑動窗口大小
```
### 使用方式
熔斷器已整合在 `read_sql_df()` 中,自動:
- 檢查是否允許請求
- 記錄成功/失敗
- 狀態轉移
### 狀態查詢
```python
from mes_dashboard.core.circuit_breaker import get_database_circuit_breaker
cb = get_database_circuit_breaker()
status = cb.get_status()
# status.state, status.failure_count, status.success_count, status.failure_rate
```
---
## 6. 統一 API 響應格式
### 位置
`mes_dashboard.core.response`
### 響應格式
```python
# 成功響應
{
"success": True,
"data": {...},
"meta": {"timestamp": "2024-02-04T10:30:45.123456"}
}
# 錯誤響應
{
"success": False,
"error": {
"code": "DB_CONNECTION_FAILED",
"message": "資料庫連線失敗,請稍後再試",
"details": "ORA-12541" # 僅開發模式
},
"meta": {"timestamp": "..."}
}
```
### 錯誤代碼
| 代碼 | HTTP | 說明 |
|------|------|------|
| DB_CONNECTION_FAILED | 503 | 資料庫連線失敗 |
| DB_QUERY_TIMEOUT | 504 | 查詢逾時 |
| DB_QUERY_ERROR | 500 | 查詢執行錯誤 |
| SERVICE_UNAVAILABLE | 503 | 服務不可用 |
| CIRCUIT_BREAKER_OPEN | 503 | 熔斷器開啟 |
| VALIDATION_ERROR | 400 | 驗證失敗 |
| UNAUTHORIZED | 401 | 未授權 |
| FORBIDDEN | 403 | 禁止訪問 |
| NOT_FOUND | 404 | 不存在 |
| TOO_MANY_REQUESTS | 429 | 過多請求 |
| INTERNAL_ERROR | 500 | 內部錯誤 |
### 便利函數
```python
from mes_dashboard.core.response import (
success_response,
validation_error, # 400
unauthorized_error, # 401
forbidden_error, # 403
not_found_error, # 404
db_connection_error, # 503
internal_error, # 500
)
```
---
## 7. 認證與授權機制
### 認證服務
位置: `mes_dashboard.services.auth_service`
#### LDAP 認證 (生產環境)
```python
from mes_dashboard.services.auth_service import authenticate
user = authenticate(username, password)
# 返回: {username, displayName, mail, department}
```
#### 本地認證 (開發環境)
```
LOCAL_AUTH_ENABLED=true
LOCAL_AUTH_USERNAME=admin
LOCAL_AUTH_PASSWORD=password
```
### Session 管理
```python
# 登入後存入 session
session["admin"] = {
"username": user.get("username"),
"displayName": user.get("displayName"),
"mail": user.get("mail"),
"department": user.get("department"),
"login_time": datetime.now().isoformat()
}
# Session 配置
SESSION_COOKIE_SECURE = True # HTTPS only (生產)
SESSION_COOKIE_HTTPONLY = True # 防止 JS 訪問
SESSION_COOKIE_SAMESITE = 'Lax' # CSRF 防護
PERMANENT_SESSION_LIFETIME = 28800 # 8 小時
```
### 權限檢查
位置: `mes_dashboard.core.permissions`
```python
from mes_dashboard.core.permissions import is_admin_logged_in, admin_required
# 檢查登入狀態
if is_admin_logged_in():
...
# 裝飾器保護路由
@admin_required
def admin_only_view():
...
```
### 登入速率限制
- 單 IP 每 5 分鐘最多 5 次嘗試
- 位置: `routes/auth_routes.py`
---
## 8. 頁面狀態管理
### 位置
- 服務: `mes_dashboard.services.page_registry`
- 數據: `data/page_status.json`
### 狀態定義
| 狀態 | 說明 |
|------|------|
| `released` | 所有用戶可訪問 |
| `dev` | 僅管理員可訪問 |
| `None` | 未註冊,由 Flask 路由控制 |
### 數據格式
```json
{
"pages": [
{"route": "/wip-overview", "name": "WIP 即時概況", "status": "released"},
{"route": "/tables", "name": "表格總覽", "status": "dev"}
],
"api_public": true
}
```
### API
```python
from mes_dashboard.services.page_registry import (
get_page_status, # 取得頁面狀態
set_page_status, # 設定頁面狀態
is_api_public, # API 是否公開
get_all_pages, # 取得所有頁面
)
```
### 權限檢查 (自動)
`app.py``@app.before_request` 中自動執行:
- dev 頁面 + 非管理員 → 403
---
## 9. 日誌系統
### 雙層日誌架構
| 層級 | 目標 | 用途 |
|------|------|------|
| 控制台 (stderr) | Gunicorn 捕獲 | 即時監控 |
| SQLite | 管理員儀表板 | 歷史查詢 |
### 配置 (環境變數)
```
LOG_STORE_ENABLED=true
LOG_SQLITE_PATH=logs/admin_logs.sqlite
LOG_SQLITE_RETENTION_DAYS=7
LOG_SQLITE_MAX_ROWS=100000
```
### 日誌記錄規範
```python
import logging
logger = logging.getLogger('mes_dashboard')
logger.debug("詳細調試資訊")
logger.info("一般操作記錄")
logger.warning("警告但可繼續")
logger.error("錯誤需要關注", exc_info=True) # 包含堆棧
```
### SQLite 日誌查詢
位置: `mes_dashboard.core.log_store`
```python
from mes_dashboard.core.log_store import get_log_store
store = get_log_store()
logs = store.query_logs(
level="ERROR",
limit=100,
offset=0,
search="keyword"
)
```
---
## 10. 健康檢查
### 端點
| 端點 | 認證 | 說明 |
|------|------|------|
| `/health` | 無需 | 基本健康檢查 |
| `/health/deep` | 需管理員 | 詳細指標 |
### 基本檢查項目
- 資料庫連線 (`SELECT 1 FROM DUAL`)
- Redis 連線 (`PING`)
- 各快取狀態
### 詳細檢查項目 (deep)
- 資料庫延遲 (毫秒)
- 連線池狀態 (size, checked_out, overflow)
- 快取新鮮度
- 熔斷器狀態
- 查詢性能指標 (P50/P95/P99)
### 狀態判定
- `200 OK` (healthy/degraded): DB 正常
- `503 Unavailable` (unhealthy): DB 故障
---
## 11. API 路由結構 (Blueprint)
### Blueprint 列表
| Blueprint | URL 前綴 | 檔案 |
|-----------|---------|------|
| wip | `/api/wip` | `wip_routes.py` |
| resource | `/api/resource` | `resource_routes.py` |
| dashboard | `/api/dashboard` | `dashboard_routes.py` |
| excel_query | `/api/excel-query` | `excel_query_routes.py` |
| hold | `/api/hold` | `hold_routes.py` |
| resource_history | `/api/resource-history` | `resource_history_routes.py` |
| job_query | `/api/job-query` | `job_query_routes.py` |
| admin | `/admin` | `admin_routes.py` |
| auth | `/admin` | `auth_routes.py` |
| health | `/` | `health_routes.py` |
### 路由註冊
位置: `routes/__init__.py``register_routes(app)`
---
## 12. 前端全局組件
### Toast 通知
定義於 `static/js/toast.js`,透過 `_base.html` 載入:
```javascript
// 正確用法
Toast.info('訊息');
Toast.success('成功');
Toast.warning('警告');
Toast.error('錯誤', { retry: () => loadData() });
const id = Toast.loading('載入中...');
Toast.update(id, { message: '完成!' });
Toast.dismiss(id);
// 錯誤用法(不存在)
MESToast.warning('...'); // ❌ 錯誤
```
### 自動消失時間
- info: 3000ms
- success: 2000ms
- warning: 5000ms
- error: 永久(需手動關閉)
- loading: 永久
### MesApiHTTP 請求)
定義於 `static/js/mes-api.js`
```javascript
// GET 請求
const data = await MesApi.get('/api/wip/summary', {
params: { page: 1 },
timeout: 60000,
retries: 5,
signal: abortController.signal,
silent: true // 禁用 toast 通知
});
// POST 請求
const data = await MesApi.post('/api/query_table', {
table_name: 'TABLE_A',
filters: {...}
});
```
### MesApi 特性
- 自動重試 (3 次,指數退避: 1s, 2s, 4s)
- 自動 Toast 通知
- 請求 ID 追蹤
- AbortSignal 支援
- 4xx 不重試5xx 重試
---
## 13. 資料表預篩選規則
### 設備類型篩選
定義於 `mes_dashboard.config.constants.EQUIPMENT_TYPE_FILTER`
```sql
((OBJECTCATEGORY = 'ASSEMBLY' AND OBJECTTYPE = 'ASSEMBLY')
OR (OBJECTCATEGORY = 'WAFERSORT' AND OBJECTTYPE = 'WAFERSORT'))
```
### 排除條件
```python
# 排除的地點
EXCLUDED_LOCATIONS = [
'ATEC', 'F區', 'F區焊接站', '報廢', '實驗室',
'山東', '成型站_F區', '焊接F區', '無錫', '熒茂'
]
# 排除的資產狀態
EXCLUDED_ASSET_STATUSES = ['Disapproved']
```
### CommonFilters 使用
位置: `mes_dashboard.sql.filters`
```python
from mes_dashboard.sql.filters import CommonFilters
# 添加標準篩選
CommonFilters.add_location_exclusion(builder, 'r')
CommonFilters.add_asset_status_exclusion(builder, 'r')
CommonFilters.add_wip_base_filters(builder, filters)
CommonFilters.add_equipment_filter(builder, filters)
```
---
## 14. 資料庫欄位對應
### DW_MES_RESOURCE
| 常見錯誤 | 正確欄位名 |
|---------|-----------|
| ASSETSTATUS | PJ_ASSETSSTATUS雙 S|
| LOCATION | LOCATIONNAME |
| ISPRODUCTION | PJ_ISPRODUCTION |
| ISKEY | PJ_ISKEY |
| ISMONITOR | PJ_ISMONITOR |
### DW_MES_RESOURCESTATUS_SHIFT
| 欄位 | 說明 |
|-----|------|
| HISTORYID | 對應 DW_MES_RESOURCE.RESOURCEID |
| TXNDATE | 交易日期 |
| OLDSTATUSNAME | E10 狀態 (PRD, SBY, UDT, SDT, EGT, NST) |
| HOURS | 該狀態時數 |
### DW_PJ_LOT_V
| 欄位 | 說明 |
|-----|------|
| WORKCENTERNAME | 站點名稱(細分)|
| WORKCENTER_GROUP | 站點群組(顯示用)|
| WORKCENTERSEQUENCE_GROUP | 群組排序 |
---
## 15. E10 狀態定義
| 狀態 | 說明 | 計入 OU% |
|-----|------|---------|
| PRD | Production生產| 是(分子)|
| SBY | Standby待機| 是(分母)|
| UDT | Unscheduled Downtime非計畫停機| 是(分母)|
| SDT | Scheduled Downtime計畫停機| 是(分母)|
| EGT | Engineering Time工程時間| 是(分母)|
| NST | Non-Scheduled Time非排程時間| 否 |
### OU% 計算公式
```
OU% = PRD / (PRD + SBY + UDT + SDT + EGT) × 100
```
### 狀態顯示名稱
```python
STATUS_DISPLAY_NAMES = {
'PRD': '生產中',
'SBY': '待機',
'UDT': '非計畫停機',
'SDT': '計畫停機',
'EGT': '工程時間',
'NST': '未排單',
}
```
---
## 16. 配置管理
### 環境變數 (.env)
#### 資料庫
```
DB_HOST=<your_database_host>
DB_PORT=1521
DB_SERVICE=<your_service_name>
DB_USER=<your_username>
DB_PASSWORD=<your_password>
DB_POOL_SIZE=5
DB_MAX_OVERFLOW=10
```
> 實際值請參考 `.env` 或 `.env.example`
#### Flask
```
FLASK_ENV=production
FLASK_DEBUG=0
SECRET_KEY=your_secret_key
SESSION_LIFETIME=28800
```
#### 認證
```
LDAP_API_URL=<your_ldap_api_url>
ADMIN_EMAILS=<admin_email_list>
LOCAL_AUTH_ENABLED=false
```
#### Gunicorn
```
GUNICORN_BIND=0.0.0.0:8080
GUNICORN_WORKERS=4
GUNICORN_THREADS=8
```
#### 快取
```
REDIS_ENABLED=true
REDIS_URL=redis://localhost:6379/0
CACHE_CHECK_INTERVAL=600
RESOURCE_CACHE_ENABLED=true
RESOURCE_SYNC_INTERVAL=14400
```
#### 熔斷器
```
CIRCUIT_BREAKER_ENABLED=true
CIRCUIT_BREAKER_FAILURE_THRESHOLD=5
CIRCUIT_BREAKER_FAILURE_RATE=0.5
CIRCUIT_BREAKER_RECOVERY_TIMEOUT=30
```
#### 日誌
```
LOG_STORE_ENABLED=true
LOG_SQLITE_PATH=logs/admin_logs.sqlite
LOG_SQLITE_RETENTION_DAYS=7
```
### 環境配置類
位置: `mes_dashboard.config.settings`
```python
class DevelopmentConfig(Config):
DEBUG = True
DB_POOL_SIZE = 2
class ProductionConfig(Config):
DEBUG = False
DB_POOL_SIZE = 10
class TestingConfig(Config):
TESTING = True
DB_POOL_SIZE = 1
```
---
## 17. 平行查詢
### ThreadPoolExecutor
對於多個獨立查詢,使用平行執行提升效能:
```python
from concurrent.futures import ThreadPoolExecutor, as_completed
with ThreadPoolExecutor(max_workers=4) as executor:
futures = {
executor.submit(read_sql_df, kpi_sql): 'kpi',
executor.submit(read_sql_df, trend_sql): 'trend',
executor.submit(read_sql_df, heatmap_sql): 'heatmap',
}
for future in as_completed(futures):
query_name = futures[future]
results[query_name] = future.result()
```
### 注意事項
- Mock 測試時不能使用 `side_effect` 列表(順序不可預測)
- 應使用函式判斷 SQL 內容來回傳對應的 mock 資料
---
## 18. Oracle SQL 優化
### CTE MATERIALIZE Hint
防止 Oracle 優化器將 CTE inline 多次執行:
```sql
WITH shift_data AS (
SELECT /*+ MATERIALIZE */ HISTORYID, TXNDATE, OLDSTATUSNAME, HOURS
FROM DW_MES_RESOURCESTATUS_SHIFT
WHERE TXNDATE >= TO_DATE('2024-01-01', 'YYYY-MM-DD')
AND TXNDATE < TO_DATE('2024-01-07', 'YYYY-MM-DD') + 1
)
SELECT ...
```
### 日期範圍查詢
```sql
-- 包含 end_date 當天
WHERE TXNDATE >= TO_DATE(:start_date, 'YYYY-MM-DD')
AND TXNDATE < TO_DATE(:end_date, 'YYYY-MM-DD') + 1
```
### 慢查詢警告
- 閾值: 1 秒 (警告)5 秒 (`SLOW_QUERY_THRESHOLD`)
- 自動記錄到日誌
---
## 19. 前端資料限制
### 明細資料上限
為避免瀏覽器記憶體問題,明細查詢有筆數限制:
```python
MAX_DETAIL_RECORDS = 5000
if total > MAX_DETAIL_RECORDS:
df = df.head(MAX_DETAIL_RECORDS)
truncated = True
```
前端顯示警告:
```javascript
if (result.truncated) {
Toast.warning(`資料超過 ${result.max_records} 筆,請使用篩選條件縮小範圍。`);
}
```
---
## 20. JavaScript 注意事項
### Array.reverse() 原地修改
```javascript
// 錯誤 - 原地修改陣列
const arr = [1, 2, 3];
arr.reverse(); // arr 被修改為 [3, 2, 1]
// 正確 - 建立新陣列
const reversed = arr.slice().reverse(); // arr 不變
// 或
const reversed = [...arr].reverse();
```
---
## 21. 測試規範
### 測試檔案結構
```
tests/
├── conftest.py # pytest fixtures
├── test_*_service.py # 單元測試service layer
├── test_*_routes.py # 整合測試API endpoints
├── e2e/
│ └── test_*_e2e.py # 端對端測試(完整流程)
└── stress/
└── test_*.py # 壓力測試
```
### 測試前重置
```python
def setUp(self):
db._ENGINE = None # 重置連線池
self.app = create_app('testing')
```
### 執行測試
```bash
# 單一模組
pytest tests/test_resource_history_service.py -v
# 全部相關測試
pytest tests/test_resource_history_*.py tests/e2e/test_resource_history_e2e.py -v
# 覆蓋率報告
pytest tests/ --cov=mes_dashboard
```
---
## 22. 錯誤處理模式
### 三層錯誤處理
```python
# 1. 路由層 - 驗證錯誤
@bp.route('/api/query')
def query():
if not request.json.get('table_name'):
return validation_error("table_name 為必填")
# 2. 服務層 - 業務錯誤 (優雅降級)
def get_wip_summary(filters):
try:
df = query_wip(filters)
if df.empty:
return None
return process_data(df)
except Exception as exc:
logger.error(f"WIP query failed: {exc}")
return None
# 3. 核心層 - 基礎設施錯誤
def read_sql_df(sql, params):
if not circuit_breaker.allow_request():
raise RuntimeError("Circuit breaker open")
```
### 全局錯誤處理
位置: `app.py``_register_error_handlers()`
- 401 → `unauthorized_error()`
- 403 → `forbidden_error()`
- 404 → JSON (API) 或 HTML (頁面)
- 500 → `internal_error()`
- Exception → 通用處理
---
## 參考檔案索引
| 功能 | 檔案位置 |
|------|---------|
| SQL 載入 | `src/mes_dashboard/sql/loader.py` |
| 查詢構建 | `src/mes_dashboard/sql/builder.py` |
| 通用篩選 | `src/mes_dashboard/sql/filters.py` |
| 資料庫操作 | `src/mes_dashboard/core/database.py` |
| 快取 | `src/mes_dashboard/core/cache.py` |
| 熔斷器 | `src/mes_dashboard/core/circuit_breaker.py` |
| API 響應 | `src/mes_dashboard/core/response.py` |
| 權限檢查 | `src/mes_dashboard/core/permissions.py` |
| 日誌存儲 | `src/mes_dashboard/core/log_store.py` |
| 配置類 | `src/mes_dashboard/config/settings.py` |
| 常量定義 | `src/mes_dashboard/config/constants.py` |
| 認證服務 | `src/mes_dashboard/services/auth_service.py` |
| 頁面狀態 | `src/mes_dashboard/services/page_registry.py` |
| Filter 快取 | `src/mes_dashboard/services/filter_cache.py` |
| 資源快取 | `src/mes_dashboard/services/resource_cache.py` |
| API 客戶端 | `src/mes_dashboard/static/js/mes-api.js` |
| Toast 系統 | `src/mes_dashboard/static/js/toast.js` |

View File

@@ -0,0 +1,34 @@
# Environment-dependent Gaps and Mitigation
## Oracle-dependent checks
### Gap
- Service/integration paths that execute Oracle SQL require live DB credentials and network reachability.
- Local CI-like runs may not have Oracle connectivity.
- In this environment, `tests/test_cache_integration.py` has Oracle-dependent fallback failures when cache fixtures are insufficient.
### Mitigation
- Keep unit tests isolated with mocks for SQL entry points.
- Reserve Oracle-connected tests for gated environments.
- Use `testing` config for app factory tests where possible.
## Redis-dependent checks
### Gap
- Redis availability differs across environments.
- Health/caching behavior differs between `L1+L2` and `L1-only degraded` modes.
### Mitigation
- Expose route-cache telemetry in `/health` and `/health/deep`.
- Keep degraded mode visible and non-fatal where DB remains healthy.
- Validate both modes in unit tests (`tests/test_cache.py`, `tests/test_health_routes.py`).
## Frontend build availability
### Gap
- Node/npm may be absent on constrained runtime nodes.
### Mitigation
- Keep inline script fallback in templates when dist assets are missing.
- Build artifacts in deployment pipeline where Node is available.
- Startup script logs fallback mode explicitly on build failure.

View File

@@ -0,0 +1,42 @@
# Frontend Compute Shift Plan
## Targeted Calculations
## Resource History (migrated to frontend helpers)
- `ou_pct`
- `availability_pct`
- status percentages:
- `prd_pct`
- `sby_pct`
- `udt_pct`
- `sdt_pct`
- `egt_pct`
- `nst_pct`
These are now computed by `frontend/src/core/compute.js` via:
- `buildResourceKpiFromHours`
- `calcOuPct`
- `calcAvailabilityPct`
- `calcStatusPct`
## Parity Rules
1. Rounding rule
- one decimal place, identical to backend (`round(..., 1)`)
2. Formula rule
- OU%: `PRD / (PRD + SBY + UDT + SDT + EGT)`
- Availability%: `(PRD + SBY + EGT) / (PRD + SBY + EGT + SDT + UDT + NST)`
- Status%: `status_hours / total_hours`
3. Zero denominator rule
- all percentages return `0`
4. Data compatibility rule
- backend keeps existing fields to preserve API compatibility
- frontend recomputes display values from hours for deterministic parity
## Validation
- Python backend formula baseline: `mes_dashboard.services.resource_history_service`
- Frontend parity check: `tests/test_frontend_compute_parity.py`

View File

@@ -0,0 +1,113 @@
# Migration Gates and Runbook
## Gate Checklist (Cutover Readiness)
A release is cutover-ready only when all gates pass:
1. Frontend build gate
- `npm --prefix frontend run build` succeeds
- expected artifacts exist in `src/mes_dashboard/static/dist/`
2. Root execution gate
- startup and deploy scripts run from repository root only
- no runtime dependency on any legacy subtree path
3. Functional parity gate
- resource-history frontend compute parity checks pass
- job-query/resource-history export headers match shared field contracts
4. Cache observability gate
- `/health` returns route cache telemetry and degraded flags
- `/health/deep` returns route cache telemetry for diagnostics
- `/health` includes `database_pool.runtime/state`, `degraded_reason`
- resource/wip derived index telemetry is visible (`resource_cache.derived_index`, `cache.derived_search_index`)
5. Runtime resilience gate
- pool exhaustion path returns `503` + `DB_POOL_EXHAUSTED` and `Retry-After`
- circuit-open path returns `503` + `CIRCUIT_BREAKER_OPEN` and fail-fast semantics
- frontend client does not aggressively retry on degraded pool exhaustion responses
6. Conda-systemd contract gate
- `deploy/mes-dashboard.service` and `deploy/mes-dashboard-watchdog.service` both run in the same conda runtime contract
- `WATCHDOG_RESTART_FLAG`, `WATCHDOG_PID_FILE`, `WATCHDOG_STATE_FILE` paths are consistent across app/admin/watchdog
- single-port bind (`GUNICORN_BIND`) remains stable during restart workflow
7. Regression gate
- focused unit/integration test subset passes (see validation evidence)
8. Documentation alignment gate
- `README.md` (and project-required mirror docs such as `README.mdj`) reflect current runtime architecture contract
- resilience diagnostics fields (thresholds/churn/recommendation) are documented for operators
- frontend shared-core governance updates are reflected in architecture notes
## Rollout Procedure
1. Prepare environment
- Activate conda env (`mes-dashboard`)
- install Python deps: `pip install -r requirements.txt`
- install frontend deps: `npm --prefix frontend install`
2. Build frontend artifacts
- `npm --prefix frontend run build`
3. Run migration gate tests
- execute focused pytest set covering templates/cache/contracts/health
4. Deploy with single-port mode
- start app with root `scripts/start_server.sh`
- verify portal and module pages render on same origin/port
5. Conda + systemd rehearsal (recommended before production cutover)
- `sudo cp deploy/mes-dashboard.service /etc/systemd/system/`
- `sudo cp deploy/mes-dashboard-watchdog.service /etc/systemd/system/`
- `sudo mkdir -p /etc/mes-dashboard && sudo cp .env /etc/mes-dashboard/mes-dashboard.env`
- `sudo systemctl daemon-reload`
- `sudo systemctl enable --now mes-dashboard mes-dashboard-watchdog`
- call `/admin/api/worker/status` and verify runtime contract paths exist
6. Post-deploy checks
- call `/health` and `/health/deep`
- confirm route cache mode, degraded flags, and pool/runtime diagnostics align with environment (Redis on/off)
- trigger one controlled worker restart from admin API and verify single-port continuity
- verify README architecture section matches deployed runtime contract
## Rollback Procedure
1. Trigger rollback criteria
- any critical gate failure after deployment (page unusable, export mismatch, health degradation beyond acceptable limits)
2. Operational rollback steps
- stop service: `scripts/start_server.sh stop`
- restore previously known-good build artifacts (or prior release package)
- restart service: `scripts/start_server.sh start`
- if using systemd: `sudo systemctl restart mes-dashboard mes-dashboard-watchdog`
3. Validation after rollback
- verify `/health` status is at least expected baseline
- re-run focused smoke tests for portal + key pages
- confirm CSV export downloads and headers
- verify degraded reason is cleared or matches expected dependency outage only
## Rollback Rehearsal Checklist
1. Simulate failure condition (e.g. invalid dist artifact deployment)
2. Execute stop/restore/start sequence
3. Verify health and page smoke checks
4. Capture timings and any manual intervention points
5. Update this runbook if any step was unclear or missing
## Alert Thresholds (Operational Contract)
Use these initial thresholds for alerting/escalation:
1. Sustained degraded state
- `degraded_reason` non-empty for >= 5 minutes
2. Worker restart churn
- >= 3 watchdog-triggered restarts within 10 minutes
3. Pool saturation pressure
- `database_pool.state.saturation >= 0.90` for >= 3 consecutive health probes
4. Frontend/API retry pressure
- significant increase of client retries for `DB_POOL_EXHAUSTED` or `CIRCUIT_BREAKER_OPEN` responses over baseline

View File

@@ -0,0 +1,60 @@
# Migration Validation Evidence
Date: 2026-02-07
## Build
Command:
- `npm --prefix frontend run build`
Result:
- PASS
- Generated page bundles:
- `portal.js`
- `resource-status.js`
- `resource-history.js`
- `job-query.js`
- `excel-query.js`
- `tables.js`
## Root Startup Smoke
Command:
- `PYTHONPATH=src python -c \"from mes_dashboard.app import create_app; app=create_app('testing'); print('routes', len(list(app.url_map.iter_rules())))\"`
Result:
- PASS
- `routes 71`
- Redis/Oracle warnings observed in this local environment; app factory and route registration still completed.
## Focused Test Gate (root project)
Command:
- `python -m pytest -q tests/test_app_factory.py tests/test_template_integration.py tests/test_cache.py tests/test_health_routes.py tests/test_field_contracts.py tests/test_frontend_compute_parity.py tests/test_job_query_service.py tests/test_resource_history_service.py`
Result:
- PASS
- `107 passed`
## Extended Regression Spot-check
Command:
- `python -m pytest -q tests/test_job_query_routes.py tests/test_resource_history_routes.py tests/test_cache_integration.py`
Result:
- PARTIAL
- `45 passed, 2 failed`
- Failed tests:
- `tests/test_cache_integration.py::TestWipApiWithCache::test_wip_matrix_uses_cache`
- `tests/test_cache_integration.py::TestWipApiWithCache::test_packages_uses_cache`
Failure profile:
- cache-fallback path hit Oracle in local environment and returned ORA connectivity/thick-mode errors.
- categorized as environment-dependent (see `docs/environment_gaps_and_mitigation.md`).
## Health/Telemetry Coverage
Validated by tests:
- `/health` includes `route_cache` telemetry and degraded warnings
- `/health/deep` includes route-cache telemetry block
- cache telemetry includes L1/L2 mode, hit/miss counters, degraded state

View File

@@ -0,0 +1,44 @@
# Page Architecture Map
## Portal Navigation Model
Portal (`/`) uses drawer-based navigation and keeps existing operational flow:
- 報表類
- `/wip-overview`
- `/resource`
- `/resource-history`
- 查詢類
- `/tables`
- `/excel-query`
- `/job-query`
- 開發工具
- `/admin/pages`
- `/admin/performance`
## Independent Pages
These pages are independent views (iframe tabs in portal) and can be loaded directly:
- `/wip-overview`
- `/resource`
- `/resource-history`
- `/tables`
- `/excel-query`
- `/job-query`
## Drill-down Pages
These pages are drill-down/detail pages, linked from parent views:
- `/wip-detail` (from WIP flows)
- `/hold-detail` (from hold-related flows)
## Vite Entry Mapping
- `portal` -> `frontend/src/portal/main.js`
- `resource-status` -> `frontend/src/resource-status/main.js`
- `resource-history` -> `frontend/src/resource-history/main.js`
- `job-query` -> `frontend/src/job-query/main.js`
- `excel-query` -> `frontend/src/excel-query/main.js`
- `tables` -> `frontend/src/tables/main.js`
All pages keep inline fallback scripts in templates when module assets are unavailable.

View File

@@ -0,0 +1,56 @@
# Root Cutover Inventory
## Scope
- Workspace root: `/Users/egg/Projects/DashBoard_vite`
- Legacy subtree `DashBoard/`: removed on 2026-02-08
- Objective: ensure runtime/test/deploy flows depend only on root architecture.
## 1. Runtime / Test / Deploy Path Audit
### Legacy path references
- Historical mentions may exist in archived OpenSpec artifacts for traceability.
- Active runtime/test/deploy code MUST NOT reference removed legacy subtree paths.
### Result
- Legacy code directory is removed.
- No active runtime code in `src/`, `scripts/`, or `tests/` requires legacy subtree paths.
- Remaining mentions are documentation-only migration history.
## 2. Root-only Execution Hardening
### Updated
- `scripts/start_server.sh`
- Frontend build readiness now checks all required root dist entries:
- `portal.js`
- `resource-status.js`
- `resource-history.js`
- `job-query.js`
- `excel-query.js`
- `tables.js`
### Verified behavior target
- Startup/build logic remains anchored to root paths:
- `frontend/`
- `src/mes_dashboard/static/dist/`
- `src/`
## 3. Root-only Smoke Checks (single-port)
### Build smoke
- `npm --prefix frontend run build`
### 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.
### HTTP smoke (Flask test client)
- Verify page renders and module asset tags resolve/fallback:
- `/`
- `/resource`
- `/resource-history`
- `/job-query`
- `/excel-query`
- `/tables`
### Test smoke
- `python -m pytest -q tests/test_app_factory.py tests/test_template_integration.py tests/test_cache.py`

View File

@@ -0,0 +1,37 @@
# Root Refactor Validation Notes
Date: 2026-02-07
## Focused Validation (Root Project)
- Frontend build:
- `npm --prefix frontend run build`
- Python focused tests:
- `python -m pytest -q tests/test_app_factory.py tests/test_cache.py tests/test_job_query_service.py` ✅ (46 passed)
- Root portal asset integration check:
- GET `/` from Flask test client includes `/static/dist/portal.js` and `/static/dist/portal.css`
## Environment-Dependent Gaps
The following are known non-functional gaps in local validation due to missing external runtime dependencies:
1. Oracle-dependent integration tests
- Some routes/services start background workers that attempt Oracle queries at app init.
- In local environment without valid Oracle connectivity, logs contain `DPY-3001` and related query failures.
2. Redis-dependent runtime checks
- Redis is not reachable in local environment (`localhost:6379` connection refused).
- Cache fallback paths continue to run, but Redis health-dependent behavior is not fully exercised.
3. Dev-page permission tests
- Certain template tests expecting `/tables` or `/excel-query` content may fail when page status is `dev` for non-admin sessions.
## Recommended Next Validation Stage
- Run full test suite in an environment with:
- reachable Oracle test endpoint
- reachable Redis endpoint
- page status fixtures aligned with expected test roles
- Add CI matrix split:
- unit/fallback tests (no Oracle/Redis required)
- integration tests (Oracle/Redis required)