150 lines
9.2 KiB
HTML
150 lines
9.2 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}任务监控 — HF Daily Papers{% endblock %}
|
|
|
|
{% set type_label = {"crawl_daily":"抓取","pipeline_daily":"流水线","summarize_batch":"批量总结","summarize_one":"单篇总结","refresh_upvotes":"刷新投票","delete_range":"删除","cleanup_tmp":"清理","reindex_fts":"重建全文","reindex_chroma":"重建语义","recrawl_one":"重抓","recrawl_batch":"批量重抓"} %}
|
|
{% set type_badge = {"crawl_daily":"task-crawl","pipeline_daily":"task-crawl","recrawl_one":"task-crawl","recrawl_batch":"task-crawl","refresh_upvotes":"task-crawl","summarize_batch":"task-summarize","summarize_one":"task-summarize","cleanup_tmp":"task-cleanup","delete_range":"task-delete","reindex_fts":"task-reindex","reindex_chroma":"task-reindex"} %}
|
|
{% set status_label = {"queued":"排队","running":"运行中","success":"成功","failed":"失败","stale":"已过期","cancelled":"已取消"} %}
|
|
{% set status_badge = {"queued":"status-queued","running":"status-running","success":"status-success","failed":"status-failed","stale":"status-stale","cancelled":"status-stale"} %}
|
|
|
|
{% macro fmt_duration(s) -%}
|
|
{%- if s is none %}-
|
|
{%- elif s < 60 %}{{ "%.0f"|format(s) }}s
|
|
{%- elif s < 3600 %}{{ (s // 60)|int }}m {{ (s % 60)|round|int }}s
|
|
{%- else %}{{ (s // 3600)|int }}h {{ ((s % 3600) // 60)|int }}m
|
|
{%- endif -%}
|
|
{%- endmacro %}
|
|
|
|
{% block content %}
|
|
<div class="admin-page">
|
|
{% set active = "jobs" %}{% include "partials/admin_subnav.html" %}
|
|
|
|
<h1 class="page-heading">🧰 任务监控</h1>
|
|
|
|
{% set _total = (status_counts.values() | sum) if status_counts else 0 %}
|
|
<div class="summary-stats-row">
|
|
<span class="summary-stat">总计 <strong>{{ _total }}</strong></span>
|
|
<span class="summary-stat summary-stat-pending">排队 <strong>{{ status_counts.get('queued', 0) }}</strong></span>
|
|
<span class="summary-stat">运行中 <strong>{{ status_counts.get('running', 0) }}</strong></span>
|
|
<span class="summary-stat summary-stat-done">成功 <strong>{{ status_counts.get('success', 0) }}</strong></span>
|
|
<span class="summary-stat summary-stat-failed">失败 <strong>{{ status_counts.get('failed', 0) + status_counts.get('stale', 0) }}</strong></span>
|
|
</div>
|
|
|
|
{% set statuses = [("all","全部"),("queued","排队"),("running","运行中"),("success","成功"),("failed","失败"),("stale","已过期")] %}
|
|
<div class="summary-filters">
|
|
<span class="summary-filter-label">状态:</span>
|
|
{% for key, label in statuses %}
|
|
<a class="filter-chip {{ 'active' if current_status == key else '' }}"
|
|
href="?status={{ key }}{% if current_type != 'all' %}&type={{ current_type }}{% endif %}">{{ label }}
|
|
({% if key == 'all' %}{{ _total }}{% else %}{{ status_counts.get(key, 0) }}{% endif %})</a>
|
|
{% endfor %}
|
|
</div>
|
|
|
|
{% set types = [("crawl_daily","抓取"),("pipeline_daily","流水线"),("summarize_batch","批量总结"),("summarize_one","单篇总结"),("refresh_upvotes","刷新投票"),("recrawl_one","重抓"),("recrawl_batch","批量重抓"),("delete_range","删除"),("cleanup_tmp","清理"),("reindex_fts","重建全文"),("reindex_chroma","重建语义")] %}
|
|
<form method="get" class="summary-filters">
|
|
<span class="summary-filter-label">类型:</span>
|
|
<input type="hidden" name="status" value="{{ current_status }}" />
|
|
<select name="type" class="paper-filter-input" onchange="this.form.submit()">
|
|
<option value="all" {{ 'selected' if current_type == 'all' }}>全部类型</option>
|
|
{% for key, label in types %}
|
|
<option value="{{ key }}" {{ 'selected' if current_type == key }}>{{ label }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</form>
|
|
|
|
{% if jobs %}
|
|
<div class="admin-table-wrap">
|
|
<table class="admin-table admin-table-compact">
|
|
<thead>
|
|
<tr><th>ID</th><th>类型</th><th>状态</th><th>触发者</th><th>创建时间</th><th>耗时</th><th>操作</th></tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for job in jobs %}
|
|
<tr>
|
|
<td>{{ job.id }}</td>
|
|
<td><span class="task-badge {{ type_badge.get(job.type, 'task-crawl') }}">{{ type_label.get(job.type, job.type) }}</span></td>
|
|
<td><span class="status-badge {{ status_badge.get(job.status, 'status-running') }}">{{ status_label.get(job.status, job.status) }}</span></td>
|
|
<td>{{ job.owner or '-' }}</td>
|
|
<td class="time-cell">{{ job.created_at.strftime('%Y-%m-%d %H:%M:%S') if job.created_at else '-' }}</td>
|
|
<td class="time-cell">{{ fmt_duration(job.duration_seconds) }}</td>
|
|
<td class="action-cell"><button class="action-btn-sm" title="详情" onclick="showJobDetail({{ job.id }})">📋</button></td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
{% set total_pages = ((total + per_page - 1) // per_page) if total else 1 %}
|
|
{% if total_pages > 1 %}
|
|
<div class="pagination">
|
|
{% if page > 1 %}<a class="page-btn" href="{{ pagination_url(page - 1) }}">← 上一页</a>{% endif %}
|
|
<span class="page-info">第 {{ page }} / {{ total_pages }} 页(共 {{ total }} 个)</span>
|
|
{% if page < total_pages %}<a class="page-btn" href="{{ pagination_url(page + 1) }}">下一页 →</a>{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
{% else %}
|
|
<div class="empty-state">
|
|
<p>暂无任务记录</p>
|
|
<p class="hint">触发抓取、总结等操作后,任务会出现在这里。可在「详情」中查看阶段事件。</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- 任务详情 modal -->
|
|
<div class="confirm-overlay" id="job-detail-overlay" style="display:none;">
|
|
<div class="confirm-dialog" style="max-width:660px;max-height:85vh;overflow:auto;">
|
|
<h3 class="admin-info-title" id="job-detail-title">任务详情</h3>
|
|
<div id="job-detail-body"><p class="hint">加载中...</p></div>
|
|
<div class="confirm-actions">
|
|
<button class="confirm-btn confirm-btn-cancel" onclick="closeJobDetail()">关闭</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script>
|
|
const TYPE_LABEL = {"crawl_daily":"抓取","pipeline_daily":"流水线","summarize_batch":"批量总结","summarize_one":"单篇总结","refresh_upvotes":"刷新投票","delete_range":"删除","cleanup_tmp":"清理","reindex_fts":"重建全文","reindex_chroma":"重建语义","recrawl_one":"重抓","recrawl_batch":"批量重抓"};
|
|
const STATUS_LABEL = {"queued":"排队","running":"运行中","success":"成功","failed":"失败","stale":"已过期","cancelled":"已取消"};
|
|
|
|
function fmtTime(s){ return s ? s.replace('T',' ').slice(0,19) : '-'; }
|
|
function esc(s){ return String(s==null?'':s).replace(/[&<>"]/g, c=>({'&':'&','<':'<','>':'>','"':'"'}[c])); }
|
|
function eventBadge(s){ return {'success':'status-success','failed':'status-failed','started':'status-running','info':'status-queued'}[s] || 'status-queued'; }
|
|
function jobStatusBadge(s){ return {'success':'success','failed':'failed','running':'running','stale':'stale','cancelled':'stale','queued':'queued'}[s] || 'running'; }
|
|
function infoRow(label, val){ return '<div class="info-row"><span class="info-label">'+label+'</span><span class="info-value">'+val+'</span></div>'; }
|
|
|
|
function showJobDetail(id){
|
|
document.getElementById('job-detail-overlay').style.display='flex';
|
|
document.getElementById('job-detail-body').innerHTML='<p class="hint">加载中...</p>';
|
|
fetch('/admin/jobs/'+id)
|
|
.then(r=>{if(r.status===303||r.status===401){window.location.href='/admin/login';return;}return r.json();})
|
|
.then(d=>{ if(d) renderJobDetail(d); })
|
|
.catch(()=>{document.getElementById('job-detail-body').innerHTML='<p class="hint">加载失败</p>';});
|
|
}
|
|
function renderJobDetail(d){
|
|
let h='<div class="admin-info-body">';
|
|
h+=infoRow('ID', d.id);
|
|
h+=infoRow('类型', esc(TYPE_LABEL[d.type]||d.type));
|
|
h+=infoRow('状态', '<span class="status-badge status-'+jobStatusBadge(d.status)+'">'+esc(STATUS_LABEL[d.status]||d.status)+'</span>');
|
|
h+=infoRow('触发者', esc(d.owner||'-'));
|
|
h+=infoRow('创建', fmtTime(d.created_at));
|
|
h+=infoRow('开始', fmtTime(d.started_at));
|
|
h+=infoRow('完成', fmtTime(d.completed_at));
|
|
if(d.payload && Object.keys(d.payload).length) h+=infoRow('参数', '<code style="word-break:break-all;">'+esc(JSON.stringify(d.payload))+'</code>');
|
|
if(d.result) h+=infoRow('结果', '<code style="word-break:break-all;">'+esc(JSON.stringify(d.result))+'</code>');
|
|
if(d.error) h+=infoRow('错误', '<span class="error-cell" style="max-width:480px;">'+esc(d.error)+'</span>');
|
|
h+='</div>';
|
|
if(d.events && d.events.length){
|
|
h+='<h3 class="section-subtitle" style="margin-top:18px;">事件时间线</h3>';
|
|
h+='<div class="admin-table-wrap" style="max-height:220px;overflow:auto;"><table class="admin-table admin-table-compact"><thead><tr><th>阶段</th><th>状态</th><th>时间</th><th>消息</th></tr></thead><tbody>';
|
|
d.events.forEach(e=>{
|
|
h+='<tr><td>'+esc(e.stage)+'</td><td><span class="status-badge '+eventBadge(e.status)+'">'+esc(e.status)+'</span></td><td class="time-cell">'+fmtTime(e.created_at)+'</td><td class="error-cell" style="max-width:240px;">'+esc(e.message||'')+'</td></tr>';
|
|
});
|
|
h+='</tbody></table></div>';
|
|
}
|
|
document.getElementById('job-detail-body').innerHTML=h;
|
|
}
|
|
function closeJobDetail(){ document.getElementById('job-detail-overlay').style.display='none'; }
|
|
document.addEventListener('keydown',e=>{if(e.key==='Escape')closeJobDetail();});
|
|
</script>
|
|
{% endblock %}
|