Files
daily-paper/app/templates/admin_dashboard.html
T

233 lines
12 KiB
HTML
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.
{% extends "base.html" %}
{% block title %}管理仪表盘 — HF Daily Papers{% endblock %}
{% block content %}
<div class="admin-page">
{% set active = "dashboard" %}{% include "partials/admin_subnav.html" %}
<h1 class="page-heading">📊 系统状态</h1>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-value">{{ stats.total_papers }}</div>
<div class="stat-label">论文总数</div>
</div>
<div class="stat-card">
<div class="stat-value">{{ stats.today_papers }}</div>
<div class="stat-label">今日新增</div>
</div>
<div class="stat-card">
<div class="stat-value {% if stats.pending_count > 0 %}stat-warn{% endif %}">
{{ stats.pending_count + stats.none_count }}
</div>
<div class="stat-label">待总结</div>
</div>
<div class="stat-card">
<div class="stat-value {% if stats.failed_count > 0 %}stat-danger{% endif %}">
{{ stats.failed_count }}
</div>
<div class="stat-label">总结失败</div>
</div>
</div>
<div class="admin-quick-actions">
<button class="admin-action-btn" onclick="adminAction('crawl')">🔄 抓取今天</button>
<button class="admin-action-btn" onclick="adminAction('summarize')">📝 批量总结</button>
<button class="admin-action-btn" onclick="adminAction('cleanup')">🧹 清理临时文件</button>
<button class="admin-action-btn" onclick="refreshUpvotes()">👍 刷新投票</button>
</div>
<div class="admin-info-grid">
<div class="admin-info-card">
<h2 class="admin-info-title">🕐 调度器</h2>
<div class="admin-info-body">
<div class="info-row">
<span class="info-label">状态</span>
<span class="info-value">
{% if stats.scheduler_enabled %}
<span class="status-dot status-dot-on"></span> 运行中
{% else %}
<span class="status-dot status-dot-off"></span> 未启用
{% endif %}
</span>
</div>
<div class="info-row">
<span class="info-label">调度时间</span>
<span class="info-value">{{ stats.schedule_time }}{{ stats.timezone }}</span>
</div>
{% if stats.next_run %}
<div class="info-row">
<span class="info-label">下次执行</span>
<span class="info-value">{{ stats.next_run[:19] | replace('T', ' ') }}</span>
</div>
{% endif %}
<div class="info-row">
<span class="info-label">投票刷新</span>
<span class="info-value">每日自动刷新最近 {{ stats.upvote_refresh_days | default(7) }} 天</span>
</div>
{% if stats.active_locks %}
<div class="info-row">
<span class="info-label">活跃任务</span>
<span class="info-value">
{% for lock in stats.active_locks %}
<span class="task-badge task-{{ lock.task }}" title="{{ lock.status }} · #{{ lock.id }}">{{ lock.task }}</span>
<button class="admin-action-btn admin-action-btn-sm admin-action-btn-danger" title="强制释放锁 #{{ lock.id }}" onclick="releaseLock({{ lock.id }})">🔓</button>
{% endfor %}
</span>
</div>
{% endif %}
<div class="info-row">
<span class="info-label"></span>
<button class="admin-action-btn admin-action-btn-sm" onclick="triggerPipeline()">
▶ 立即执行流水线
</button>
</div>
</div>
<div class="scheduler-history">
<h3 class="section-subtitle">执行历史</h3>
{% if scheduler_history %}
<div class="admin-table-wrap">
<table class="admin-table admin-table-compact">
<thead>
<tr><th>时间</th><th>状态</th><th>发现</th><th>新增</th><th>错误</th></tr>
</thead>
<tbody>
{% for log in scheduler_history %}
<tr>
<td class="time-cell">{{ log.started_at.strftime('%Y-%m-%d %H:%M') if log.started_at else '-' }}</td>
<td><span class="status-badge status-{{ log.status }}">
{% if log.status == 'success' %}✓{% elif log.status == 'running' %}⟳{% elif log.status == 'failed' %}✗{% else %}{{ log.status }}{% endif %}
</span></td>
<td>{{ log.papers_found or 0 }}</td>
<td>{{ log.papers_new or 0 }}</td>
<td class="error-cell" title="{{ log.error or '' }}">
{{ (log.error[:50] + '...') if log.error and log.error|length > 50 else (log.error or '-') }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="hint">暂无调度器执行记录。</p>
{% endif %}
</div>
</div>
<div class="admin-info-card">
<h2 class="admin-info-title">💾 存储概况</h2>
<div class="admin-info-body">
<div class="info-row"><span class="info-label">数据库</span><span class="info-value">{{ stats.db_size }}</span></div>
<div class="info-row"><span class="info-label">论文文件</span><span class="info-value">{{ stats.papers_size }}</span></div>
<div class="info-row"><span class="info-label">临时文件</span><span class="info-value">{{ stats.tmp_size }}</span></div>
<div class="info-row">
<span class="info-label">搜索索引</span>
<span class="info-value">
<button class="admin-action-btn admin-action-btn-sm" onclick="rebuildIndexes('fts')">🔤 重建全文</button>
{% if stats.config_overview.chroma_enabled %}
<button class="admin-action-btn admin-action-btn-sm" onclick="rebuildIndexes('chroma')">🧠 重建语义</button>
{% endif %}
</span>
</div>
</div>
<div class="summary-dist">
<h3 class="section-subtitle">总结状态分布</h3>
<div class="summary-dist-bars">
{% set total = stats.total_papers or 1 %}
{% set labels = {"done": "已完成", "pending": "待总结", "running": "运行中", "processing": "处理中", "failed": "失败", "permanent_failure": "永久失败", "none": "未开始"} %}
{% for st, cnt in stats.status_counts.items() %}
{% if cnt > 0 %}
<div class="dist-row">
<span class="dist-label">{{ labels.get(st, st) }}</span>
<div class="dist-bar-wrap"><div class="dist-bar dist-bar-{{ st }}" style="width: {{ (cnt / total * 100)|round(1) }}%"></div></div>
<span class="dist-count">{{ cnt }}</span>
</div>
{% endif %}
{% endfor %}
</div>
</div>
</div>
<div class="admin-info-card">
<h2 class="admin-info-title">⚙️ 运行配置</h2>
<div class="admin-info-body">
<div class="info-row"><span class="info-label">总结后端</span><span class="info-value">{{ stats.config_overview.summary_backend }} · {{ stats.config_overview.summary_pdf_mode }} 模式</span></div>
<div class="info-row"><span class="info-label">并发/超时</span><span class="info-value">{{ stats.config_overview.summary_concurrency }} 并发 · {{ stats.config_overview.summary_timeout_seconds }}s · 重试 {{ stats.config_overview.summary_max_retries }}</span></div>
<div class="info-row"><span class="info-label">调度</span><span class="info-value">{{ '启用' if stats.config_overview.scheduler_enabled else '未启用' }} · {{ stats.config_overview.schedule_time }} · {{ stats.config_overview.app_workers }} worker</span></div>
<div class="info-row"><span class="info-label">语义搜索</span><span class="info-value">{{ '启用' if stats.config_overview.chroma_enabled else '未启用' }} · {{ stats.config_overview.embed_model }}</span></div>
<div class="info-row"><span class="info-label">抓取</span><span class="info-value">TOP {{ stats.config_overview.top_n }} · 投票刷新 {{ stats.config_overview.upvote_refresh_days }} 天</span></div>
<div class="info-row"><span class="info-label">布局模型</span><span class="info-value">{{ stats.config_overview.layout_model }}</span></div>
<div class="info-row"><span class="info-label">数据库</span><span class="info-value">{{ stats.config_overview.database_url }}</span></div>
<div class="info-row"><span class="info-label">嵌入密钥</span><span class="info-value">{{ '已配置' if stats.config_overview.api_key_configured else '未配置' }}</span></div>
</div>
</div>
</div>
<div class="admin-section">
<h2 class="admin-section-title">📋 最近活动</h2>
{% if stats.recent_logs %}
<div class="admin-table-wrap">
<table class="admin-table">
<thead>
<tr><th>任务</th><th>状态</th><th>日期</th><th>发现</th><th>新增</th><th>开始时间</th><th>完成时间</th><th>错误</th></tr>
</thead>
<tbody>
{% for log in stats.recent_logs %}
<tr>
<td><span class="task-badge task-{{ log.task }}">{{ log.task }}</span></td>
<td><span class="status-badge status-{{ log.status }}">
{# djlint:off #}
{% if log.status == 'success' %}✓ 成功{% elif log.status == 'running' %}⟳ 运行中{% elif log.status == 'failed' %}✗ 失败{% else %}{{ log.status }}{% endif %}
{# djlint:on #}
</span></td>
<td>{{ log.date or '-' }}</td>
<td>{{ log.papers_found or 0 }}</td>
<td>{{ log.papers_new or 0 }}</td>
<td class="time-cell">{{ log.started_at.strftime('%Y-%m-%d %H:%M') if log.started_at else '-' }}</td>
<td class="time-cell">{{ log.completed_at.strftime('%Y-%m-%d %H:%M') if log.completed_at else '-' }}</td>
<td class="error-cell" title="{{ log.error or '' }}">
{{ (log.error[:60] + '...') if log.error and log.error|length > 60 else (log.error or '-') }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="empty-state">
<p>暂无活动日志</p>
<p class="hint">通过快捷操作触发任务后,日志将出现在这里。</p>
</div>
{% endif %}
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
function triggerPipeline() {
fetch("/admin/trigger-pipeline", { method: "POST", headers: { "Content-Type": "application/json" } })
.then(r => { if (r.status===303||r.status===401) { window.location.href="/admin/login"; return; } return r.json(); })
.then(data => { if (data) showToast(data.error ? "❌ " + data.error.substring(0,200) : "✅ 流水线已触发"); })
.catch(err => showToast("❌ 请求失败"));
}
function refreshUpvotes() {
fetch("/admin/refresh-upvotes", { method: "POST", headers: { "Content-Type": "application/json" } })
.then(r => { if (r.status===303||r.status===401) { window.location.href="/admin/login"; return; } return r.json(); })
.then(data => { if (data) showToast(data.error ? "❌ " + data.error.substring(0,200) : `✅ 已刷新 ${data.updated || 0} 篇论文投票`); })
.catch(err => showToast("❌ 请求失败"));
}
function releaseLock(lockId) {
fetch("/admin/locks/"+lockId+"/release", { method: "POST", headers: { "Content-Type": "application/json" } })
.then(r => { if (r.status===303||r.status===401) { window.location.href="/admin/login"; return; } return r.json(); })
.then(data => { if (data) showToast(data.error ? "❌ " + data.error.substring(0,200) : "✅ 已释放锁,刷新中…", {callback:function(){location.reload();}}); })
.catch(err => showToast("❌ 请求失败"));
}
function rebuildIndexes(target) {
fetch("/admin/rebuild-indexes", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({target: target}) })
.then(r => { if (r.status===303||r.status===401) { window.location.href="/admin/login"; return; } return r.json(); })
.then(data => { if (data) showToast(data.error ? "❌ " + data.error.substring(0,200) : "✅ 重建任务已创建,可在任务页查看"); })
.catch(err => showToast("❌ 请求失败"));
}
</script>
{% endblock %}