# 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: 永久

### MesApi（HTTP 請求）
定義於 `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` |
