Some checks failed
CD Pipeline / deploy (push) Failing after 59s
- 建立 Gitea Actions CD pipeline (.gitea/workflows/cd.yaml) - 部署模式: rsync Python 檔案至 188 → docker restart (volume mount) - Dockerfile/requirements 變動時自動重建 Docker image - 部署通知: Telegram (開始/成功/失敗) - 健康檢查: https://mo.wooo.work/health (最多 5 次重試) - 同步最新 CLAUDE.md / ADR-008 / memory (2026-04-19) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
145 lines
7.1 KiB
HTML
145 lines
7.1 KiB
HTML
<!DOCTYPE html>
|
|
{% macro slugify(text) -%}
|
|
{{ text|string|replace(' ', '_')|replace(':', '')|replace('!', '')|replace('?', '')|replace('/', '')|replace('&', '')|replace('(', '')|replace(')', '') }}
|
|
{%- endmacro %}
|
|
<html lang="zh-TW">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>MOMO 限時搶購監控</title>
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
<style>
|
|
body { background-color: #f8f9fa; }
|
|
.card { border: none; box-shadow: 0 2px 4px rgba(0,0,0,0.05); }
|
|
.badge-up { background-color: #dc3545; color: white; }
|
|
.badge-down { background-color: #198754; color: white; }
|
|
.badge-new { background-color: #0d6efd; color: white; }
|
|
.nav-pills .nav-link.active { background-color: #d63384; }
|
|
.nav-pills .nav-link { color: #666; }
|
|
.price-tag { font-weight: bold; color: #d63384; font-size: 1.1em; }
|
|
.status-badge { font-size: 0.8em; padding: 4px 8px; border-radius: 4px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<nav class="navbar navbar-dark bg-dark mb-4">
|
|
<div class="container">
|
|
<a class="navbar-brand" href="/">MOMO 監控系統</a>
|
|
<div class="navbar-nav">
|
|
<a class="nav-link" href="/">主看板</a>
|
|
<a class="nav-link active" href="/edm">限時搶購 (EDM)</a>
|
|
<a class="nav-link" href="/logs">系統日誌</a>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
|
|
<div class="container mb-5">
|
|
<div class="row mb-4">
|
|
<div class="col-md-8">
|
|
<h2>🔥 限時搶購監控儀表板</h2>
|
|
<p class="text-muted">
|
|
活動時間: {{ activity_time }} |
|
|
最後更新: {{ last_update }} |
|
|
商品總數: {{ total_edm_products }}
|
|
</p>
|
|
</div>
|
|
<div class="col-md-4 text-end">
|
|
<button class="btn btn-outline-primary" onclick="triggerEdmTask()">🔄 手動更新</button>
|
|
<button class="btn btn-outline-success" onclick="triggerNotification()">📢 發送通知</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 時段頁籤 -->
|
|
<ul class="nav nav-pills mb-3" id="pills-tab" role="tablist">
|
|
{% for slot, stats in slot_stats.items() %}
|
|
{% set slot_id = slugify(slot) %}
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link {% if slot == active_tab %}active{% endif %}"
|
|
id="pills-{{ slot_id }}-tab"
|
|
data-bs-toggle="pill"
|
|
data-bs-target="#pills-{{ slot_id }}"
|
|
type="button" role="tab"
|
|
aria-controls="pills-{{ slot_id }}"
|
|
aria-selected="{{ 'true' if slot == active_tab else 'false' }}">
|
|
{{ slot }}
|
|
<span class="badge bg-light text-dark rounded-pill ms-2">{{ stats.on_shelf }}</span>
|
|
</button>
|
|
</li>
|
|
{% endfor %}
|
|
</ul>
|
|
|
|
<div class="tab-content" id="pills-tabContent">
|
|
{% for slot, stats in slot_stats.items() %}
|
|
{% set items = grouped_items.get(slot, []) %}
|
|
{% set slot_id = slugify(slot) %}
|
|
<div class="tab-pane fade {% if slot == active_tab %}show active{% endif %}"
|
|
id="pills-{{ slot_id }}" role="tabpanel" aria-labelledby="pills-{{ slot_id }}-tab">
|
|
|
|
<!-- 該時段統計 -->
|
|
<div class="alert alert-light border mb-3">
|
|
<strong>📊 時段統計:</strong>
|
|
<span class="badge bg-primary me-2">新品: {{ stats['new'] }}</span>
|
|
<span class="badge bg-danger me-2">漲價: {{ stats['up'] }}</span>
|
|
<span class="badge bg-success me-2">降價: {{ stats['down'] }}</span>
|
|
<span class="badge bg-secondary" title="今日異動">下架: {{ stats.get('delisted_last_run', 0) }}</span>
|
|
</div>
|
|
|
|
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4">
|
|
{% for item in items %}
|
|
<div class="col">
|
|
<div class="card h-100">
|
|
<div class="card-body">
|
|
<h6 class="card-title text-truncate">
|
|
<a href="{{ item.url }}" target="_blank" class="text-decoration-none text-dark">
|
|
{{ item.name }}
|
|
</a>
|
|
</h6>
|
|
<div class="d-flex justify-content-between align-items-center mt-2">
|
|
<span class="price-tag">${{ item.price }}</span>
|
|
<div>
|
|
{% if item.status_change == 'NEW' %}
|
|
<span class="badge badge-new status-badge">NEW</span>
|
|
{% elif item.status_change == 'PRICE_DOWN' %}
|
|
<span class="badge badge-down status-badge">↘ 降價</span>
|
|
{% elif item.status_change == 'PRICE_UP' %}
|
|
<span class="badge badge-up status-badge">↗ 漲價</span>
|
|
{% elif item.status_change == 'DELISTED' %}
|
|
<span class="badge bg-secondary status-badge">下架</span>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
<div class="mt-2 text-muted small">
|
|
分類: <span style="color: {{ item.category_color }}">{{ item.main_category or '未分類' }}</span>
|
|
<br>
|
|
頻次: {{ item.frequency }} 次
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
|
<script>
|
|
function triggerEdmTask() {
|
|
if(confirm('確定要手動執行 EDM 爬蟲嗎?')) {
|
|
fetch('/api/run_edm_task', {method: 'POST'})
|
|
.then(r => r.json())
|
|
.then(data => alert(data.message))
|
|
.catch(e => alert('錯誤: ' + e));
|
|
}
|
|
}
|
|
function triggerNotification() {
|
|
if(confirm('確定要發送比價通知嗎?')) {
|
|
fetch('/api/trigger_edm_notification', {method: 'POST'})
|
|
.then(r => r.json())
|
|
.then(data => alert(data.message))
|
|
.catch(e => alert('錯誤: ' + e));
|
|
}
|
|
}
|
|
</script>
|
|
</body>
|
|
</html> |