Files
daily-paper/app/templates/admin_logs.html
T
Rain-Bus 2cfd1a8a9f feat: add admin crawl, cleanup, delete, logs endpoints with scheduler and tests
- Add POST /admin/crawl with TaskLock-based reentrancy guard
- Add POST /admin/cleanup (tmp files older than 24h) with CrawlLog
- Add POST /admin/delete with date range and 'DELETE' confirm token
- Add GET /admin/logs (paginated CrawlLog + DataDeleteJob viewer)
- Add app/services/cleaner.py (cleanup_tmp, delete_papers_by_date_range)
- Add app/services/scheduler.py (APScheduler daily crawl/cleanup jobs)
- Wire scheduler startup/shutdown hooks in app/main.py
- Add admin nav link in base.html and APP_HOST security warning
- Add apscheduler>=3.10 dependency
- Add tests/test_admin_phase4.py covering the new endpoints
2026-06-05 23:07:45 +08:00

300 lines
9.5 KiB
HTML

{% extends "base.html" %}
{% block title %}管理日志 — HF Daily Papers{% endblock %}
{% block content %}
<div class="admin-logs-page">
<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>
</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>
</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 }}">
{% if log.status == 'success' %}✓ 成功
{% elif log.status == 'running' %}⟳ 运行中
{% elif log.status == 'failed' %}✗ 失败
{% else %}{{ log.status }}{% endif %}
</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="error-cell" title="{{ log.error or '' }}">{{ log.error[:80] + '...' if log.error and log.error|length > 80 else (log.error or '-') }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="empty-state">
<p>暂无抓取日志</p>
<p class="hint">通过管理接口触发抓取或总结后,日志将出现在这里。</p>
</div>
{% 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>
</thead>
<tbody>
{% for job in delete_jobs %}
<tr>
<td>{{ job.id }}</td>
<td>{{ job.date_start }}</td>
<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 }}">
{% if job.status == 'success' %}✓ 成功
{% elif job.status == 'running' %}⟳ 运行中
{% elif job.status == 'failed' %}✗ 失败
{% else %}{{ job.status }}{% endif %}
</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 '-') }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="empty-state">
<p>暂无删除记录</p>
<p class="hint">通过管理接口删除论文后,记录将出现在这里。</p>
</div>
{% endif %}
</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>
</div>
</div>
</div>
<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 %}
<script>
function adminAction(action) {
const token = prompt('请输入 Admin Token:');
if (!token) return;
const url = '/admin/' + action;
fetch(url, {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + token,
'Content-Type': 'application/json',
},
})
.then(r => r.json())
.then(data => {
alert(JSON.stringify(data, null, 2));
location.reload();
})
.catch(err => {
alert('请求失败: ' + err.message);
});
}
// 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'));
tab.classList.add('active');
document.getElementById(tab.dataset.tab).classList.add('active');
});
});
</script>
{% endblock %}