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

188 lines
8.9 KiB
HTML

{% 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>
<!-- 抓取日志 -->
<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 }}">
{# 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[: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>
<!-- 删除记录 -->
<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 }}">
{# 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('%Y-%m-%d %H:%M') if job.started_at else '-' }}</td>
<td class="time-cell">{{ job.completed_at.strftime('%Y-%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-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>
{% if failure_breakdown %}
<div class="summary-dist" style="margin-top:12px;">
<h3 class="section-subtitle">失败原因分布({{ summary_failed or 0 }} 篇)</h3>
<div class="summary-dist-bars">
{% set fb_total = (failure_breakdown | map(attribute='count') | sum) or 1 %}
{% set error_labels = {"pdf_download_failed":"PDF下载失败","timeout":"超时","process_error":"进程错误","json_not_found":"JSON缺失","json_invalid":"JSON无效","field_missing":"字段缺失","schema_error":"结构错误","unknown":"未分类"} %}
{% for item in failure_breakdown %}
<div class="dist-row">
<span class="dist-label">{{ error_labels.get(item.error_type, item.error_type) }}</span>
<div class="dist-bar-wrap"><div class="dist-bar dist-bar-failed" style="width:{{ (item.count / fb_total * 100)|round(1) }}%"></div></div>
<span class="dist-count">{{ item.count }}</span>
</div>
{% endfor %}
</div>
</div>
{% endif %}
<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>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
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"));
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 %}