Files
DashBoard/src/mes_dashboard/routes/resource_routes.py
beabigegg f823d8cefd feat: 新增設備即時狀態快取與合併查詢 API
- 新增 realtime_equipment_cache 模組,從 DW_MES_EQUIPMENTSTATUS_WIP_V 同步設備即時狀態
- 新增 resource_service 合併三層快取(resource-cache、realtime-equipment、workcenter-mapping)
- 新增 /api/resource/status/* API 端點提供設備狀態查詢
- 更新 health_routes 顯示 realtime equipment cache 狀態
- 更新 portal.html 顯示設備即時快取資訊
- 重構 resource_status.html 前端頁面
- 新增相關 OpenSpec 規格文件與測試

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 07:51:30 +08:00

305 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- coding: utf-8 -*-
"""Resource (Equipment) API routes for MES Dashboard.
Contains Flask Blueprint for resource/equipment-related API endpoints.
"""
from flask import Blueprint, jsonify, request
from mes_dashboard.core.database import get_db_connection
from mes_dashboard.core.cache import cache_get, cache_set, make_cache_key
from mes_dashboard.core.utils import get_days_back
from mes_dashboard.services.resource_service import (
query_resource_status_summary,
query_resource_by_status,
query_resource_by_workcenter,
query_resource_detail,
query_resource_workcenter_status_matrix,
query_resource_filter_options,
get_merged_resource_status,
get_resource_status_summary,
get_workcenter_status_matrix,
)
from mes_dashboard.services.filter_cache import get_workcenter_groups
from mes_dashboard.config.constants import STATUS_CATEGORIES
# Create Blueprint
resource_bp = Blueprint('resource', __name__, url_prefix='/api/resource')
@resource_bp.route('/summary')
def api_resource_summary():
"""API: Resource status summary."""
days_back = request.args.get('days_back', 30, type=int)
cache_key = make_cache_key("resource_summary", days_back)
summary = cache_get(cache_key)
if summary is None:
summary = query_resource_status_summary(days_back)
if summary:
cache_set(cache_key, summary)
if summary:
return jsonify({'success': True, 'data': summary})
return jsonify({'success': False, 'error': '查詢失敗'}), 500
@resource_bp.route('/by_status')
def api_resource_by_status():
"""API: Resource count by status."""
days_back = request.args.get('days_back', 30, type=int)
cache_key = make_cache_key("resource_by_status", days_back)
data = cache_get(cache_key)
if data is None:
df = query_resource_by_status(days_back)
if df is not None:
data = df.to_dict(orient='records')
cache_set(cache_key, data)
else:
data = None
if data is not None:
return jsonify({'success': True, 'data': data})
return jsonify({'success': False, 'error': '查詢失敗'}), 500
@resource_bp.route('/by_workcenter')
def api_resource_by_workcenter():
"""API: Resource count by workcenter."""
days_back = request.args.get('days_back', 30, type=int)
cache_key = make_cache_key("resource_by_workcenter", days_back)
data = cache_get(cache_key)
if data is None:
df = query_resource_by_workcenter(days_back)
if df is not None:
data = df.to_dict(orient='records')
cache_set(cache_key, data)
else:
data = None
if data is not None:
return jsonify({'success': True, 'data': data})
return jsonify({'success': False, 'error': '查詢失敗'}), 500
@resource_bp.route('/workcenter_status_matrix')
def api_resource_workcenter_status_matrix():
"""API: Resource count matrix by workcenter and status category."""
days_back = request.args.get('days_back', 30, type=int)
cache_key = make_cache_key("resource_workcenter_matrix", days_back)
data = cache_get(cache_key)
if data is None:
df = query_resource_workcenter_status_matrix(days_back)
if df is not None:
data = df.to_dict(orient='records')
cache_set(cache_key, data)
else:
data = None
if data is not None:
return jsonify({'success': True, 'data': data})
return jsonify({'success': False, 'error': '查詢失敗'}), 500
@resource_bp.route('/detail', methods=['POST'])
def api_resource_detail():
"""API: Resource detail with filters."""
data = request.get_json() or {}
filters = data.get('filters')
limit = data.get('limit', 500)
offset = data.get('offset', 0)
days_back = get_days_back(filters)
df = query_resource_detail(filters, limit, offset, days_back)
if df is not None:
records = df.to_dict(orient='records')
return jsonify({'success': True, 'data': records, 'count': len(records), 'offset': offset})
return jsonify({'success': False, 'error': '查詢失敗'}), 500
@resource_bp.route('/filter_options')
def api_resource_filter_options():
"""API: Get filter options."""
days_back = request.args.get('days_back', 30, type=int)
cache_key = make_cache_key("resource_filter_options", days_back)
options = cache_get(cache_key)
if options is None:
options = query_resource_filter_options(days_back)
if options:
cache_set(cache_key, options)
if options:
return jsonify({'success': True, 'data': options})
return jsonify({'success': False, 'error': '查詢失敗'}), 500
@resource_bp.route('/status_values')
def api_resource_status_values():
"""API: Get all distinct status values with counts (for verification)."""
connection = get_db_connection()
if not connection:
return jsonify({'success': False, 'error': '數據庫連接失敗'}), 500
try:
sql = """
SELECT DISTINCT NEWSTATUSNAME, COUNT(*) as CNT
FROM DWH.DW_MES_RESOURCESTATUS
WHERE NEWSTATUSNAME IS NOT NULL
AND LASTSTATUSCHANGEDATE >= SYSDATE - 30
GROUP BY NEWSTATUSNAME
ORDER BY CNT DESC
"""
cursor = connection.cursor()
cursor.execute(sql)
rows = cursor.fetchall()
cursor.close()
connection.close()
data = [{'status': row[0], 'count': row[1]} for row in rows]
return jsonify({'success': True, 'data': data})
except Exception as exc:
if connection:
connection.close()
return jsonify({'success': False, 'error': str(exc)}), 500
# ============================================================
# Realtime Equipment Status APIs (New)
# ============================================================
@resource_bp.route('/status')
def api_resource_status():
"""API: Get merged resource status from realtime cache.
Query params:
workcenter_groups: Comma-separated group names (e.g., '焊接,成型')
is_production: '1' or 'true' to filter production equipment
is_key: '1' or 'true' to filter key equipment
is_monitor: '1' or 'true' to filter monitor equipment
status_categories: Comma-separated categories (e.g., 'PRODUCTIVE,DOWN')
"""
# Parse filters
wc_groups_param = request.args.get('workcenter_groups')
workcenter_groups = wc_groups_param.split(',') if wc_groups_param else None
is_production = None
is_prod_param = request.args.get('is_production')
if is_prod_param:
is_production = is_prod_param.lower() in ('1', 'true', 'yes')
is_key = None
is_key_param = request.args.get('is_key')
if is_key_param:
is_key = is_key_param.lower() in ('1', 'true', 'yes')
is_monitor = None
is_monitor_param = request.args.get('is_monitor')
if is_monitor_param:
is_monitor = is_monitor_param.lower() in ('1', 'true', 'yes')
status_cats_param = request.args.get('status_categories')
status_categories = status_cats_param.split(',') if status_cats_param else None
try:
data = get_merged_resource_status(
workcenter_groups=workcenter_groups,
is_production=is_production,
is_key=is_key,
is_monitor=is_monitor,
status_categories=status_categories,
)
return jsonify({
'success': True,
'data': data,
'count': len(data),
})
except Exception as exc:
return jsonify({'success': False, 'error': str(exc)}), 500
@resource_bp.route('/status/options')
def api_resource_status_options():
"""API: Get filter options for realtime status queries.
Returns workcenter_groups, status_categories, and other filter options.
"""
try:
# Get workcenter groups from cache
wc_groups = get_workcenter_groups() or []
return jsonify({
'success': True,
'data': {
'workcenter_groups': [g['name'] for g in wc_groups],
'status_categories': STATUS_CATEGORIES,
}
})
except Exception as exc:
return jsonify({'success': False, 'error': str(exc)}), 500
@resource_bp.route('/status/summary')
def api_resource_status_summary():
"""API: Get resource status summary statistics.
Query params: same as /status
"""
# Parse filters (same as /status)
wc_groups_param = request.args.get('workcenter_groups')
workcenter_groups = wc_groups_param.split(',') if wc_groups_param else None
is_production = None
is_prod_param = request.args.get('is_production')
if is_prod_param:
is_production = is_prod_param.lower() in ('1', 'true', 'yes')
is_key = None
is_key_param = request.args.get('is_key')
if is_key_param:
is_key = is_key_param.lower() in ('1', 'true', 'yes')
is_monitor = None
is_monitor_param = request.args.get('is_monitor')
if is_monitor_param:
is_monitor = is_monitor_param.lower() in ('1', 'true', 'yes')
try:
data = get_resource_status_summary(
workcenter_groups=workcenter_groups,
is_production=is_production,
is_key=is_key,
is_monitor=is_monitor,
)
return jsonify({'success': True, 'data': data})
except Exception as exc:
return jsonify({'success': False, 'error': str(exc)}), 500
@resource_bp.route('/status/matrix')
def api_resource_status_matrix():
"""API: Get workcenter × status matrix.
Query params:
is_production: Filter by production equipment
is_key: Filter by key equipment
is_monitor: Filter by monitor equipment
"""
is_production = None
is_prod_param = request.args.get('is_production')
if is_prod_param:
is_production = is_prod_param.lower() in ('1', 'true', 'yes')
is_key = None
is_key_param = request.args.get('is_key')
if is_key_param:
is_key = is_key_param.lower() in ('1', 'true', 'yes')
is_monitor = None
is_monitor_param = request.args.get('is_monitor')
if is_monitor_param:
is_monitor = is_monitor_param.lower() in ('1', 'true', 'yes')
try:
data = get_workcenter_status_matrix(
is_production=is_production,
is_key=is_key,
is_monitor=is_monitor,
)
return jsonify({'success': True, 'data': data})
except Exception as exc:
return jsonify({'success': False, 'error': str(exc)}), 500