feat: add admin dashboard, pipeline service, lightbox, and update dependencies
This commit is contained in:
+89
-319
@@ -1,68 +1,43 @@
|
||||
{% extends "base.html" %} {% block title %}管理日志 — HF Daily Papers{% endblock
|
||||
%} {% block content %}
|
||||
<div class="admin-logs-page">
|
||||
{% extends "base.html" %}
|
||||
{% block title %}管理日志 — HF Daily Papers{% endblock %}
|
||||
{% block content %}
|
||||
<div class="admin-page">
|
||||
{% set active = "logs" %}{% include "partials/admin_subnav.html" %}
|
||||
|
||||
<h1 class="page-heading">📋 管理日志</h1>
|
||||
|
||||
<!-- Tab 切换 -->
|
||||
<div class="admin-tabs">
|
||||
<button class="admin-tab active" data-tab="crawl-logs">抓取日志</button>
|
||||
<button class="admin-tab" data-tab="delete-jobs">删除记录</button>
|
||||
<button class="admin-tab" data-tab="summary-status">总结状态</button>
|
||||
</div>
|
||||
|
||||
<!-- 抓取日志 Tab -->
|
||||
<!-- 抓取日志 -->
|
||||
<div class="admin-tab-content active" id="crawl-logs">
|
||||
{% if crawl_logs %}
|
||||
<div class="admin-table-wrap">
|
||||
<table class="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>任务</th>
|
||||
<th>状态</th>
|
||||
<th>日期</th>
|
||||
<th>发现</th>
|
||||
<th>新增</th>
|
||||
<th>开始时间</th>
|
||||
<th>完成时间</th>
|
||||
<th>错误</th>
|
||||
</tr>
|
||||
<tr><th>ID</th><th>任务</th><th>状态</th><th>日期</th><th>发现</th><th>新增</th><th>开始时间</th><th>完成时间</th><th>错误</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for log in crawl_logs %}
|
||||
<tr>
|
||||
<td>{{ log.id }}</td>
|
||||
<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><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('%m-%d %H:%M') if log.started_at else
|
||||
'-' }}
|
||||
</td>
|
||||
<td class="time-cell">
|
||||
{{ log.completed_at.strftime('%m-%d %H:%M') if log.completed_at
|
||||
else '-' }}
|
||||
</td>
|
||||
<td class="time-cell">{{ log.started_at.strftime('%m-%d %H:%M') if log.started_at else '-' }}</td>
|
||||
<td class="time-cell">{{ log.completed_at.strftime('%m-%d %H:%M') if log.completed_at else '-' }}</td>
|
||||
<td class="error-cell" title="{{ log.error or '' }}">
|
||||
{{ log.error[:80] + '...' if log.error and log.error|length > 80
|
||||
else (log.error or '-') }}
|
||||
{{ log.error[:80] + '...' if log.error and log.error|length > 80 else (log.error or '-') }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
@@ -77,23 +52,13 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- 删除记录 Tab -->
|
||||
<!-- 删除记录 -->
|
||||
<div class="admin-tab-content" id="delete-jobs">
|
||||
{% if delete_jobs %}
|
||||
<div class="admin-table-wrap">
|
||||
<table class="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>起始日期</th>
|
||||
<th>结束日期</th>
|
||||
<th>包含笔记</th>
|
||||
<th>论文数</th>
|
||||
<th>状态</th>
|
||||
<th>开始时间</th>
|
||||
<th>完成时间</th>
|
||||
<th>错误</th>
|
||||
</tr>
|
||||
<tr><th>ID</th><th>起始日期</th><th>结束日期</th><th>包含笔记</th><th>论文数</th><th>状态</th><th>开始时间</th><th>完成时间</th><th>错误</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for job in delete_jobs %}
|
||||
@@ -103,32 +68,15 @@
|
||||
<td>{{ job.date_end }}</td>
|
||||
<td>{{ '是' if job.include_notes else '否' }}</td>
|
||||
<td>{{ job.paper_count or 0 }}</td>
|
||||
<td>
|
||||
<span class="status-badge status-{{ job.status }}">
|
||||
{# djlint:off #}
|
||||
{% if job.status == 'success' %}
|
||||
✓ 成功
|
||||
{% elif job.status == 'running' %}
|
||||
⟳ 运行中
|
||||
{% elif job.status == 'failed' %}
|
||||
✗ 失败
|
||||
{% else %}
|
||||
{{ job.status }}
|
||||
{% endif %}
|
||||
{# djlint:on #}
|
||||
</span>
|
||||
</td>
|
||||
<td class="time-cell">
|
||||
{{ job.started_at.strftime('%m-%d %H:%M') if job.started_at else
|
||||
'-' }}
|
||||
</td>
|
||||
<td class="time-cell">
|
||||
{{ job.completed_at.strftime('%m-%d %H:%M') if job.completed_at
|
||||
else '-' }}
|
||||
</td>
|
||||
<td><span class="status-badge status-{{ job.status }}">
|
||||
{# djlint:off #}
|
||||
{% if job.status == 'success' %}✓ 成功{% elif job.status == 'running' %}⟳ 运行中{% elif job.status == 'failed' %}✗ 失败{% else %}{{ job.status }}{% endif %}
|
||||
{# djlint:on #}
|
||||
</span></td>
|
||||
<td class="time-cell">{{ job.started_at.strftime('%m-%d %H:%M') if job.started_at else '-' }}</td>
|
||||
<td class="time-cell">{{ job.completed_at.strftime('%m-%d %H:%M') if job.completed_at else '-' }}</td>
|
||||
<td class="error-cell" title="{{ job.error or '' }}">
|
||||
{{ job.error[:80] + '...' if job.error and job.error|length > 80
|
||||
else (job.error or '-') }}
|
||||
{{ job.error[:80] + '...' if job.error and job.error|length > 80 else (job.error or '-') }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
@@ -143,259 +91,81 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- 总结状态 -->
|
||||
<div class="admin-tab-content" id="summary-status">
|
||||
<div class="summary-filters">
|
||||
<span class="summary-filter-label">筛选:</span>
|
||||
<button class="filter-chip active" data-status="all">全部</button>
|
||||
<button class="filter-chip" data-status="none">未开始</button>
|
||||
<button class="filter-chip" data-status="pending">待总结</button>
|
||||
<button class="filter-chip" data-status="processing">运行中</button>
|
||||
<button class="filter-chip" data-status="failed">失败</button>
|
||||
<button class="filter-chip" data-status="permanent_failure">永久失败</button>
|
||||
<button class="filter-chip" data-status="done">已完成</button>
|
||||
</div>
|
||||
<div class="summary-stats-row">
|
||||
<span class="summary-stat">全部 <strong>{{ summary_total or 0 }}</strong></span>
|
||||
<span class="summary-stat summary-stat-pending">待总结 <strong>{{ summary_pending or 0 }}</strong></span>
|
||||
<span class="summary-stat summary-stat-failed">失败 <strong>{{ summary_failed or 0 }}</strong></span>
|
||||
<span class="summary-stat summary-stat-done">已完成 <strong>{{ summary_done or 0 }}</strong></span>
|
||||
</div>
|
||||
<div id="summary-list"
|
||||
hx-get="/admin/summary-status"
|
||||
hx-trigger="load"
|
||||
hx-target="#summary-list"
|
||||
hx-swap="innerHTML">
|
||||
<div class="empty-state"><p>加载中...</p></div>
|
||||
</div>
|
||||
<div class="summary-batch-actions">
|
||||
<button class="admin-action-btn" onclick="retryAllFailed()">🔄 重试所有失败</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 管理操作区 -->
|
||||
<div class="admin-actions">
|
||||
<h2 class="admin-actions-title">管理操作</h2>
|
||||
<div class="admin-action-buttons">
|
||||
<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="adminAction('crawl')">🔄 抓取今天</button>
|
||||
<button class="admin-action-btn" onclick="adminAction('summarize')">📝 批量总结</button>
|
||||
<button class="admin-action-btn" onclick="adminAction('cleanup')">🧹 清理临时文件</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
<style>
|
||||
/* ── Admin Logs ────────────────────────────────────────────────── */
|
||||
.admin-logs-page {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.admin-tabs {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
border-bottom: 2px solid var(--border);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.admin-tab {
|
||||
padding: 10px 24px;
|
||||
border: none;
|
||||
background: none;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
color: var(--ink-light);
|
||||
cursor: pointer;
|
||||
border-bottom: 2px solid transparent;
|
||||
margin-bottom: -2px;
|
||||
transition:
|
||||
color 0.2s,
|
||||
border-color 0.2s;
|
||||
font-family: var(--font-sans);
|
||||
}
|
||||
|
||||
.admin-tab:hover {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.admin-tab.active {
|
||||
color: var(--accent);
|
||||
border-bottom-color: var(--accent);
|
||||
}
|
||||
|
||||
.admin-tab-content {
|
||||
display: none;
|
||||
}
|
||||
.admin-tab-content.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* ── Table ─────────────────────────────────────────────────────── */
|
||||
.admin-table-wrap {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.admin-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.85rem;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
.admin-table th {
|
||||
padding: 10px 12px;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
color: var(--ink-light);
|
||||
background: var(--bg);
|
||||
border-bottom: 1px solid var(--border);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.admin-table td {
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
color: var(--ink);
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.admin-table tbody tr:hover {
|
||||
background: var(--bg);
|
||||
}
|
||||
.admin-table tbody tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.time-cell {
|
||||
white-space: nowrap;
|
||||
color: var(--ink-light);
|
||||
}
|
||||
.error-cell {
|
||||
max-width: 200px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: #c62828;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
/* ── Badges ────────────────────────────────────────────────────── */
|
||||
.task-badge,
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 3px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.task-crawl {
|
||||
background: #e3f2fd;
|
||||
color: #1565c0;
|
||||
}
|
||||
.task-summarize {
|
||||
background: #f3e5f5;
|
||||
color: #7b1fa2;
|
||||
}
|
||||
.task-cleanup {
|
||||
background: #e8f5e9;
|
||||
color: #2e7d32;
|
||||
}
|
||||
.task-delete {
|
||||
background: #fce4ec;
|
||||
color: #c62828;
|
||||
}
|
||||
.task-scheduler {
|
||||
background: #fff3e0;
|
||||
color: #e65100;
|
||||
}
|
||||
|
||||
.status-success {
|
||||
background: #e8f5e9;
|
||||
color: #388e3c;
|
||||
}
|
||||
.status-running {
|
||||
background: #e3f2fd;
|
||||
color: #1976d2;
|
||||
}
|
||||
.status-failed {
|
||||
background: #fce4ec;
|
||||
color: #c62828;
|
||||
}
|
||||
|
||||
/* ── Admin Actions ─────────────────────────────────────────────── */
|
||||
.admin-actions {
|
||||
margin-top: 32px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.admin-actions-title {
|
||||
font-family: var(--font-body);
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12px;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.admin-action-buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.admin-action-btn {
|
||||
padding: 8px 18px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
color: var(--ink);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
font-family: var(--font-sans);
|
||||
}
|
||||
|
||||
.admin-action-btn:hover {
|
||||
border-color: var(--accent);
|
||||
color: var(--accent);
|
||||
box-shadow: 0 2px 8px var(--shadow);
|
||||
}
|
||||
|
||||
/* ── Responsive ────────────────────────────────────────────────── */
|
||||
@media (max-width: 640px) {
|
||||
.admin-table {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
.admin-table th,
|
||||
.admin-table td {
|
||||
padding: 6px 8px;
|
||||
}
|
||||
.admin-action-buttons {
|
||||
flex-direction: column;
|
||||
}
|
||||
.admin-action-btn {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %} {% block scripts %}
|
||||
{% block scripts %}
|
||||
<script>
|
||||
function adminAction(action) {
|
||||
const url = "/admin/" + action;
|
||||
fetch(url, {
|
||||
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) {
|
||||
alert(JSON.stringify(data, null, 2));
|
||||
location.reload();
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
alert("请求失败: " + err.message);
|
||||
});
|
||||
function retrySummary(arxivId, btn) {
|
||||
btn.disabled=true; btn.textContent="处理中...";
|
||||
fetch("/admin/summarize/"+arxivId,{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):"✅ 已提交重试");setTimeout(()=>htmx.trigger("#summary-list","reloadSummary"),1000);}})
|
||||
.catch(err=>showToast("❌ 请求失败"))
|
||||
.finally(()=>{btn.disabled=false;btn.textContent="重试";});
|
||||
}
|
||||
function retryAllFailed() {
|
||||
if(!confirm("确定重试所有失败的总结任务?"))return;
|
||||
fetch("/admin/summary-retry-failed",{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.message||"已提交"));setTimeout(()=>htmx.trigger("#summary-list","reloadSummary"),1500);}})
|
||||
.catch(err=>showToast("❌ 请求失败"));
|
||||
}
|
||||
|
||||
// Tab 切换
|
||||
document.querySelectorAll(".admin-tab").forEach((tab) => {
|
||||
tab.addEventListener("click", () => {
|
||||
document
|
||||
.querySelectorAll(".admin-tab")
|
||||
.forEach((t) => t.classList.remove("active"));
|
||||
document
|
||||
.querySelectorAll(".admin-tab-content")
|
||||
.forEach((c) => c.classList.remove("active"));
|
||||
document.querySelectorAll(".admin-tab").forEach(tab=>{
|
||||
tab.addEventListener("click",()=>{
|
||||
document.querySelectorAll(".admin-tab").forEach(t=>t.classList.remove("active"));
|
||||
document.querySelectorAll(".admin-tab-content").forEach(c=>c.classList.remove("active"));
|
||||
tab.classList.add("active");
|
||||
document.getElementById(tab.dataset.tab).classList.add("active");
|
||||
});
|
||||
});
|
||||
// 总结状态筛选
|
||||
document.querySelectorAll(".summary-filters .filter-chip").forEach(chip=>{
|
||||
chip.addEventListener("click",()=>{
|
||||
document.querySelectorAll(".summary-filters .filter-chip").forEach(c=>c.classList.remove("active"));
|
||||
chip.classList.add("active");
|
||||
htmx.ajax("GET","/admin/summary-status?status="+chip.dataset.status,"#summary-list");
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user