2cfd1a8a9f
- 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
300 lines
9.5 KiB
HTML
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 %}
|