diff --git a/routes/admin_observability_routes.py b/routes/admin_observability_routes.py index 0ec70f8..97a2bbc 100644 --- a/routes/admin_observability_routes.py +++ b/routes/admin_observability_routes.py @@ -261,6 +261,146 @@ def quality_trend_dashboard(): ) +# ───────────────────────────────────────────────────────────────────────────── +# /admin/budget — Phase 29 預算管理 + 手動 throttle +# ───────────────────────────────────────────────────────────────────────────── + +@admin_observability_bp.route('/budget') +def budget_dashboard(): + """ai_call_budgets 編輯 + 當月 spent 即時對比""" + from datetime import datetime as _dt + today = _dt.now() + month_start = _dt(today.year, today.month, 1) + + session = get_session() + try: + budgets = session.execute( + sa_text(""" + SELECT id, period, provider, budget_usd, alert_pct, updated_at + FROM ai_call_budgets + ORDER BY period, provider NULLS FIRST + """), + ).fetchall() + + spent_rows = session.execute( + sa_text(""" + SELECT provider, COALESCE(SUM(cost_usd), 0) AS spent + FROM ai_calls + WHERE called_at >= :ms + GROUP BY provider + """), + {'ms': month_start}, + ).fetchall() + spent_map = {r[0]: float(r[1] or 0) for r in spent_rows} + + # throttle 狀態 + throttle_state = {} + try: + from services.cost_throttle_service import get_throttle_state + throttle_state = get_throttle_state() + except Exception: + pass + + rows = [] + for b in budgets: + provider = b[2] # 可能 None(全供應商總額) + spent = spent_map.get(provider, 0.0) if provider else sum(spent_map.values()) + budget_usd = float(b[3] or 0) + ratio = (spent / budget_usd) if budget_usd > 0 else 0 + rows.append({ + 'id': b[0], 'period': b[1], 'provider': provider or '(all)', + 'budget_usd': budget_usd, 'alert_pct': int(b[4] or 80), + 'spent': spent, 'ratio': ratio, + 'throttled': throttle_state.get(provider, {}).get('throttled', False) if provider else False, + 'updated_at': b[5].strftime('%Y-%m-%d %H:%M') if b[5] else '-', + }) + + return render_template('admin/budget.html', rows=rows, error=None) + except Exception as e: + return render_template('admin/budget.html', rows=[], + error=f'查詢失敗: {type(e).__name__}: {str(e)[:200]}') + finally: + session.close() + + +@admin_observability_bp.route('/budget/update/', methods=['POST']) +def budget_update(budget_id: int): + """更新 budget_usd / alert_pct""" + try: + new_budget = float(request.json.get('budget_usd')) + new_alert = int(request.json.get('alert_pct', 80)) + if new_budget <= 0 or not (1 <= new_alert <= 100): + return jsonify({'ok': False, 'error': 'invalid range'}), 400 + + session = get_session() + try: + session.execute( + sa_text(""" + UPDATE ai_call_budgets + SET budget_usd = :b, alert_pct = :a, updated_at = NOW() + WHERE id = :id + """), + {'b': new_budget, 'a': new_alert, 'id': budget_id}, + ) + session.commit() + return jsonify({'ok': True}) + finally: + session.close() + except Exception as e: + return jsonify({'ok': False, 'error': str(e)[:200]}), 500 + + +# ───────────────────────────────────────────────────────────────────────────── +# /admin/ppt_audit_history — Phase 29 PPT 視覺審核歷史 +# ───────────────────────────────────────────────────────────────────────────── + +@admin_observability_bp.route('/ppt_audit_history') +def ppt_audit_history(): + """掃 reports/ 目錄列近 7 日 .pptx 檔 + 即時跑 audit(如已啟用)""" + import os + reports_dir = 'reports' + files = [] + error = None + + try: + if not os.path.isdir(reports_dir): + error = f'{reports_dir} 目錄不存在' + else: + cutoff = __import__('time').time() - 7 * 86400 + for f in os.listdir(reports_dir): + if not f.lower().endswith('.pptx'): + continue + full = os.path.join(reports_dir, f) + try: + mtime = os.path.getmtime(full) + if mtime >= cutoff: + files.append({ + 'name': f, + 'size_kb': round(os.path.getsize(full) / 1024, 1), + 'mtime': __import__('datetime').datetime.fromtimestamp(mtime).strftime('%Y-%m-%d %H:%M'), + 'mtime_ts': mtime, + }) + except OSError: + continue + files.sort(key=lambda x: x['mtime_ts'], reverse=True) + except Exception as e: + error = f'{type(e).__name__}: {str(e)[:200]}' + + # PPT vision 啟用狀態 + try: + from services.ppt_vision_service import is_ppt_vision_enabled + vision_enabled = is_ppt_vision_enabled() + except Exception: + vision_enabled = False + + return render_template( + 'admin/ppt_audit_history.html', + files=files, + vision_enabled=vision_enabled, + error=error, + ) + + # ───────────────────────────────────────────────────────────────────────────── # /admin/host_health — 三主機 + MCP 健康度 # ───────────────────────────────────────────────────────────────────────────── diff --git a/templates/admin/budget.html b/templates/admin/budget.html new file mode 100644 index 0000000..26590fa --- /dev/null +++ b/templates/admin/budget.html @@ -0,0 +1,110 @@ +{% extends "base.html" %} + +{% block title %}Budget Manager{% endblock %} + +{% block content %} +
+

💰 Budget Manager + ai_call_budgets × 當月 spent 即時對比 +

+ + {% if error %}
⚠️ {{ error }}
{% endif %} + +

+ 依 ADR-028 預算 + Phase 20 cost_throttle:每小時 cron 檢查當月 spent, + 線性外推月底成本超 110% → 自動 throttle(claude→gemini fallback)。 + 手動編輯 budget 後立即生效(不需 restart)。 +

+ + + + + + + + + + + + + {% for r in rows %} + = 0.8 %}class="table-warning"{% endif %}> + + + + + + + + + + + {% else %} + + {% endfor %} + +
PeriodProviderSpent (USD)Budget (USD)Alert %Ratio狀態Last Update動作
{{ r.period }}{{ r.provider }}${{ "%.2f"|format(r.spent) }} + + + + + + {{ "%.0f"|format(r.ratio * 100) }}% + + + {% if r.throttled %} + ⚠️ THROTTLED + {% elif r.ratio >= 0.8 %} + ⚠ 接近上限 + {% else %} + ✅ 正常 + {% endif %} + {{ r.updated_at }} + +
無預算資料(先跑 migrations/025)
+ +

+ 🤖 Operation Ollama-First v5.0 / Phase 29 — Budget Manager + | AI Calls + | Host Health + | Promotion Review +

+
+ + +{% endblock %} diff --git a/templates/admin/ppt_audit_history.html b/templates/admin/ppt_audit_history.html new file mode 100644 index 0000000..af0ffaa --- /dev/null +++ b/templates/admin/ppt_audit_history.html @@ -0,0 +1,58 @@ +{% extends "base.html" %} + +{% block title %}PPT Audit History{% endblock %} + +{% block content %} +
+

🔍 PPT 視覺審核歷史 + reports/ 過去 7 日 .pptx +

+ + {% if error %}
⚠️ {{ error }}
{% endif %} + +
+ PPT_VISION_ENABLED: + {% if vision_enabled %} + ✅ 已啟用 — daily 22:00 cron 自動跑 minicpm-v 視覺檢查,有 issues 推 Telegram + {% else %} + ⏸ 未啟用 — 設 PPT_VISION_ENABLED=true + 188 安裝 LibreOffice 即生效 + {% endif %} +
+ + + + + + + + + + {% for f in files %} + + + + + + + {% else %} + + {% endfor %} + +
檔名大小 (KB)修改時間動作
{{ f.name }}{{ f.size_kb }}{{ f.mtime }} + audit cron 22:00 自動跑 +
過去 7 日無 PPT 生成
+ +

+ 審核結果:有 issues 才推 Telegram(避免靜默無問題洗版)。 + 手動觸發單檔審核需 SSH 188 跑: + python3 -c "from services.ppt_vision_service import ppt_vision_service; print(ppt_vision_service.check_ppt_file('reports/xxx.pptx'))" +

+ +

+ 🤖 Operation Ollama-First v5.0 / Phase 29 — PPT Audit History + | AI Calls + | Host Health + | Budget +

+
+{% endblock %}