Files

124 lines
8.4 KiB
HTML

{% extends "admin/base.html" %}
{% block admin_title %}作业中心{% endblock %}
{% block page_title %}作业中心{% endblock %}
{% block page_subtitle %}跟踪导入、全量构建、失败日志与执行耗时{% endblock %}
{% block admin_content %}
<div class="space-y-6">
<section class="grid grid-cols-2 gap-3 sm:grid-cols-5">
{% for key, label in [('total','全部'),('running','运行中'),('queued','排队'),('succeeded','成功'),('failed','失败')] %}
<a href="{{ url_for('admin.jobs', status=key if key != 'total' else None) }}"
class="rounded-xl border bg-white p-4 shadow-sm transition hover:border-yrtv-300 dark:bg-slate-900
{% if status == key or (key == 'total' and not status) %}border-yrtv-400 ring-2 ring-yrtv-100{% else %}border-slate-200 dark:border-slate-800{% endif %}">
<div class="text-xs font-bold uppercase text-slate-400">{{ label }}</div>
<div class="mt-1 text-2xl font-black text-slate-900 dark:text-white">{{ summary[key] }}</div>
</a>
{% endfor %}
</section>
<div class="grid grid-cols-1 gap-6 xl:grid-cols-[minmax(0,1fr)_420px]">
<section class="overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900">
<div class="flex flex-col gap-3 border-b border-slate-100 px-5 py-4 sm:flex-row sm:items-center sm:justify-between dark:border-slate-800">
<div>
<h2 class="font-black text-slate-900 dark:text-white">执行记录</h2>
<p class="text-xs text-slate-400">最多显示最近 50 条</p>
</div>
<div class="flex gap-2">
<a href="{{ url_for('admin.import_match') }}" class="rounded-lg bg-yrtv-600 px-3 py-2 text-xs font-bold text-white">新建导入</a>
<a href="{{ url_for('admin.jobs') }}" class="rounded-lg border border-slate-200 px-3 py-2 text-xs font-bold text-slate-600 dark:border-slate-700 dark:text-slate-300">清除筛选</a>
</div>
</div>
<div class="overflow-x-auto">
<table class="min-w-full">
<thead class="bg-slate-50 text-left text-[10px] font-black uppercase tracking-wider text-slate-400 dark:bg-slate-950/50">
<tr><th class="px-5 py-3">Job</th><th class="px-4 py-3">Match</th><th class="px-4 py-3">Stage</th><th class="px-4 py-3">Status</th><th class="px-4 py-3 text-right">Duration</th></tr>
</thead>
<tbody class="divide-y divide-slate-100 dark:divide-slate-800">
{% for job in jobs %}
<tr class="{% if selected_job and selected_job.id == job.id %}bg-yrtv-50 dark:bg-yrtv-950/20{% endif %} hover:bg-slate-50 dark:hover:bg-slate-800/50">
<td class="px-5 py-3">
<a href="{{ url_for('admin.jobs', job_id=job.id, status=status, type=job_type) }}" class="font-black text-yrtv-600">#{{ job.id }}</a>
<div class="text-xs text-slate-400">{{ job.job_type }}</div>
</td>
<td class="max-w-[180px] truncate px-4 py-3 font-mono text-xs text-slate-500">{{ job.match_id or '-' }}</td>
<td class="px-4 py-3 text-xs text-slate-500">{{ job.current_stage or '-' }} · {{ job.progress }}%</td>
<td class="px-4 py-3">
<span class="rounded-full px-2 py-1 text-[10px] font-black uppercase
{% if job.status == 'succeeded' %}bg-emerald-100 text-emerald-700
{% elif job.status == 'failed' %}bg-red-100 text-red-700
{% elif job.status == 'running' %}bg-amber-100 text-amber-700
{% else %}bg-slate-100 text-slate-600{% endif %}">{{ job.status }}</span>
</td>
<td class="px-4 py-3 text-right font-mono text-xs text-slate-500">{{ '%.2fs'|format(job.duration_seconds) if job.duration_seconds is not none else '-' }}</td>
</tr>
{% else %}
<tr><td colspan="5" class="px-5 py-12 text-center text-sm text-slate-400">当前筛选没有作业</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</section>
<aside class="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm dark:border-slate-800 dark:bg-slate-900">
{% if selected_job %}
<div class="flex items-start justify-between">
<div>
<div class="text-xs font-black uppercase tracking-wider text-slate-400">Job Detail</div>
<h2 class="mt-1 text-xl font-black text-slate-900 dark:text-white">#{{ selected_job.id }} {{ selected_job.job_type }}</h2>
</div>
<span id="job-status" class="rounded-full bg-slate-100 px-3 py-1 text-xs font-black uppercase text-slate-600">{{ selected_job.status }}</span>
</div>
<div class="mt-5">
<div class="flex justify-between text-xs text-slate-500"><span id="job-stage">{{ selected_job.current_stage or selected_job.status }}</span><span id="job-progress-label">{{ selected_job.progress }}%</span></div>
<div class="mt-2 h-2 overflow-hidden rounded-full bg-slate-100 dark:bg-slate-800"><div id="job-progress" class="h-full rounded-full bg-yrtv-500 transition-all" style="width:{{ selected_job.progress }}%"></div></div>
<p id="job-message" class="mt-3 text-sm text-slate-600 dark:text-slate-300">{{ selected_job.message }}</p>
</div>
<dl class="mt-5 grid grid-cols-2 gap-3 text-xs">
<div class="rounded-lg bg-slate-50 p-3 dark:bg-slate-800"><dt class="text-slate-400">Created</dt><dd class="mt-1 font-mono text-slate-700 dark:text-slate-200">{{ selected_job.created_at }}</dd></div>
<div class="rounded-lg bg-slate-50 p-3 dark:bg-slate-800"><dt class="text-slate-400">Duration</dt><dd class="mt-1 font-mono text-slate-700 dark:text-slate-200">{{ '%.3fs'|format(selected_job.duration_seconds) if selected_job.duration_seconds is not none else '-' }}</dd></div>
</dl>
<div class="mt-5 flex items-center justify-between">
<h3 class="text-xs font-black uppercase tracking-wider text-slate-400">Execution Log</h3>
<button onclick="copyJobLog()" class="text-xs font-bold text-yrtv-600">复制日志</button>
</div>
<pre id="job-log" class="mt-2 max-h-[420px] overflow-auto whitespace-pre-wrap rounded-xl bg-slate-950 p-4 text-[11px] leading-5 text-slate-300">{{ selected_job.log_text }}</pre>
{% else %}
<div class="flex min-h-[420px] flex-col items-center justify-center text-center">
<div class="flex h-14 w-14 items-center justify-center rounded-2xl bg-slate-100 text-2xl dark:bg-slate-800"></div>
<h2 class="mt-4 font-black text-slate-800 dark:text-white">选择一个作业</h2>
<p class="mt-2 max-w-xs text-sm text-slate-400">查看阶段进度、执行信息和完整构建日志。</p>
</div>
{% endif %}
</aside>
</div>
</div>
{% endblock %}
{% block scripts %}
{{ super() }}
{% if selected_job %}
<script>
const jobId = {{ selected_job.id }};
let jobPoll = null;
async function refreshJob() {
const response = await fetch(`/admin/api/jobs/${jobId}`);
if (!response.ok) return;
const job = await response.json();
document.getElementById('job-status').textContent = job.status;
document.getElementById('job-stage').textContent = job.current_stage || job.status;
document.getElementById('job-progress-label').textContent = `${job.progress}%`;
document.getElementById('job-progress').style.width = `${job.progress}%`;
document.getElementById('job-message').textContent = job.message || '';
document.getElementById('job-log').textContent = job.log_text || '';
if (['succeeded', 'failed'].includes(job.status) && jobPoll) clearInterval(jobPoll);
}
function copyJobLog() {
navigator.clipboard.writeText(document.getElementById('job-log').textContent);
}
jobPoll = setInterval(refreshJob, 1500);
refreshJob();
</script>
{% endif %}
{% endblock %}