Files
JKTV-online/web/templates/admin/dashboard.html
T

148 lines
9.6 KiB
HTML

{% extends "admin/base.html" %}
{% block admin_title %}运营总览{% endblock %}
{% block page_title %}运营总览{% endblock %}
{% block page_subtitle %}数据规模、流水线状态与最近比赛一屏掌握{% endblock %}
{% block admin_content %}
{% set health_color = 'emerald' if integrity.overall_status == 'pass' else ('amber' if integrity.overall_status == 'warn' else 'red') %}
<div class="space-y-7">
<section class="grid grid-cols-2 gap-3 xl:grid-cols-5">
{% for label, value, note in [
('比赛总量', metrics.matches, 'L2 matches'),
('Active Roster', metrics.roster_players, roster_versions|string + ' roster version'),
('赛后报告', metrics.reports, 'L3 reports'),
('周期奖项', metrics.awards, 'Awards generated'),
('备份占用', metrics.backup_size, integrity.counts.backup_sets|string + ' snapshots')
] %}
<div class="rounded-2xl border border-slate-200 bg-white p-4 shadow-sm dark:border-slate-800 dark:bg-slate-900">
<div class="text-xs font-bold uppercase tracking-wider text-slate-400">{{ label }}</div>
<div class="mt-2 text-3xl font-black text-slate-900 dark:text-white">{{ value }}</div>
<div class="mt-1 truncate text-[11px] text-slate-400">{{ note }}</div>
</div>
{% endfor %}
</section>
<div class="grid grid-cols-1 gap-6 xl:grid-cols-3">
<section class="overflow-hidden rounded-2xl bg-gradient-to-br from-slate-950 via-slate-900 to-yrtv-950 p-6 text-white shadow-xl xl:col-span-2">
<div class="flex flex-col gap-6 sm:flex-row sm:items-start sm:justify-between">
<div>
<div class="text-xs font-black uppercase tracking-[0.22em] text-yrtv-300">Data Pipeline</div>
<h2 class="mt-2 text-2xl font-black">完整数据流水线</h2>
<p class="mt-2 max-w-xl text-sm leading-6 text-slate-300">运行前自动备份 L1/L2/L3,串行构建并验证 {{ integrity.checks|length }} 项数据质量规则。失败时自动恢复。</p>
</div>
<div class="rounded-2xl bg-white/10 px-4 py-3 text-right">
<div class="text-xs uppercase text-slate-400">Running</div>
<div class="text-2xl font-black {% if job_summary.running %}text-amber-300{% else %}text-emerald-300{% endif %}">{{ job_summary.running }}</div>
</div>
</div>
<div class="mt-6 flex flex-wrap gap-3">
<a href="{{ url_for('admin.import_match') }}" class="rounded-xl bg-yrtv-500 px-5 py-3 text-sm font-black text-white hover:bg-yrtv-400">导入新比赛</a>
<button id="pipeline-button" onclick="triggerPipeline()" class="rounded-xl bg-white px-5 py-3 text-sm font-black text-slate-900 hover:bg-slate-100 disabled:cursor-not-allowed disabled:opacity-50" {% if job_summary.running %}disabled{% endif %}>运行完整流水线</button>
<a href="{{ url_for('admin.jobs') }}" class="rounded-xl border border-white/20 px-5 py-3 text-sm font-bold text-white hover:bg-white/10">查看所有作业</a>
</div>
<p id="pipeline-message" class="mt-3 text-xs text-slate-400"></p>
</section>
<a href="{{ url_for('admin.data_integrity') }}" class="group rounded-2xl border border-{{ health_color }}-200 bg-{{ health_color }}-50 p-6 shadow-sm transition hover:-translate-y-0.5 hover:shadow-md dark:border-{{ health_color }}-900/50 dark:bg-{{ health_color }}-950/20">
<div class="flex items-center justify-between">
<div class="text-xs font-black uppercase tracking-widest text-{{ health_color }}-600">Data Health</div>
<span class="rounded-full bg-{{ health_color }}-100 px-3 py-1 text-xs font-black uppercase text-{{ health_color }}-700">{{ integrity.overall_status }}</span>
</div>
<div class="mt-5 flex items-end gap-3">
<div class="text-5xl font-black text-{{ health_color }}-700">{{ integrity.totals.pass }}</div>
<div class="pb-1 text-sm text-{{ health_color }}-700">项检查通过</div>
</div>
<div class="mt-4 grid grid-cols-2 gap-2 text-xs">
<div class="rounded-lg bg-white/70 p-2 text-slate-600">警告 <b>{{ integrity.totals.warn }}</b></div>
<div class="rounded-lg bg-white/70 p-2 text-slate-600">失败 <b>{{ integrity.totals.fail }}</b></div>
</div>
<div class="mt-4 text-xs font-bold text-{{ health_color }}-700 group-hover:underline">查看完整报告 →</div>
</a>
</div>
<div class="grid grid-cols-1 gap-6 xl:grid-cols-3">
<section class="overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900 xl:col-span-2">
<div class="flex items-center justify-between border-b border-slate-100 px-5 py-4 dark:border-slate-800">
<div>
<h2 class="font-black text-slate-900 dark:text-white">最近作业</h2>
<p class="text-xs text-slate-400">成功 {{ job_summary.succeeded }} · 失败 {{ job_summary.failed }}</p>
</div>
<a href="{{ url_for('admin.jobs') }}" class="text-xs font-bold text-yrtv-600 hover:underline">作业中心</a>
</div>
<div class="divide-y divide-slate-100 dark:divide-slate-800">
{% for job in jobs %}
<a href="{{ url_for('admin.jobs', job_id=job.id) }}" class="flex items-center gap-4 px-5 py-3.5 hover:bg-slate-50 dark:hover:bg-slate-800/50">
<span class="h-2.5 w-2.5 rounded-full {% if job.status == 'succeeded' %}bg-emerald-500{% elif job.status == 'failed' %}bg-red-500{% elif job.status == 'running' %}bg-amber-400 animate-pulse{% else %}bg-slate-300{% endif %}"></span>
<div class="min-w-0 flex-1">
<div class="truncate text-sm font-bold text-slate-800 dark:text-slate-200">#{{ job.id }} {{ job.job_type }}</div>
<div class="truncate text-xs text-slate-400">{{ job.message or job.current_stage or '等待执行' }}</div>
</div>
<div class="text-right text-xs text-slate-400">
<div class="font-bold uppercase">{{ job.status }}</div>
<div>{{ '%.2fs'|format(job.duration_seconds) if job.duration_seconds is not none else job.progress|string + '%' }}</div>
</div>
</a>
{% else %}
<div class="px-5 py-10 text-center text-sm text-slate-400">暂无流水线作业</div>
{% endfor %}
</div>
</section>
<section class="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm dark:border-slate-800 dark:bg-slate-900">
<div class="text-xs font-black uppercase tracking-widest text-slate-400">Latest Match</div>
{% if latest_match %}
<div class="mt-4 flex items-center justify-between">
<span class="rounded-lg px-2.5 py-1 text-xs font-black {% if latest_match.is_win %}bg-emerald-100 text-emerald-700{% else %}bg-red-100 text-red-600{% endif %}">{{ 'WIN' if latest_match.is_win else 'LOSS' }}</span>
<span class="text-xs text-slate-400">{{ latest_match.date_text }}</span>
</div>
<div class="mt-4 text-2xl font-black text-slate-900 dark:text-white">{{ latest_match.map_name }}</div>
<div class="mt-1 text-sm text-slate-500">Team Rating {{ '%.2f'|format(latest_match.team_avg_rating) }}</div>
<p class="mt-4 line-clamp-3 text-xs leading-5 text-slate-500">{{ latest_match.summary_text }}</p>
<a href="{{ url_for('matches.detail', match_id=latest_match.match_id) }}" class="mt-4 block text-xs font-black text-yrtv-600 hover:underline">打开赛后报告 →</a>
{% else %}
<div class="mt-8 text-sm text-slate-400">暂无比赛数据</div>
{% endif %}
</section>
</div>
<section class="grid grid-cols-2 gap-3 sm:grid-cols-4">
{% for endpoint, title, note in [
('admin.sql_runner', '查询数据', '只读 SQL Console'),
('admin.system', '系统信息', '数据库与备份'),
('teams.performance', '战队履历', 'Roster & Lineup'),
('awards.index', '荣誉中心', '周期最佳')
] %}
<a href="{{ url_for(endpoint) }}" class="rounded-xl border border-slate-200 bg-white p-4 transition hover:border-yrtv-300 hover:shadow-sm dark:border-slate-800 dark:bg-slate-900">
<div class="font-black text-slate-900 dark:text-white">{{ title }}</div>
<div class="mt-1 text-xs text-slate-400">{{ note }}</div>
</a>
{% endfor %}
</section>
</div>
{% endblock %}
{% block scripts %}
{{ super() }}
<script>
async function triggerPipeline() {
const button = document.getElementById('pipeline-button');
const message = document.getElementById('pipeline-message');
button.disabled = true;
message.textContent = '正在创建后台流水线...';
try {
const response = await fetch("{{ url_for('admin.trigger_etl') }}", {
method: 'POST',
headers: {'X-CSRF-Token': "{{ csrf_token() }}"}
});
const data = await response.json();
if (!response.ok || !data.success) throw new Error(data.error || '启动失败');
window.location.href = "{{ url_for('admin.jobs') }}?job_id=" + data.job_id;
} catch (error) {
message.textContent = error.message;
button.disabled = false;
}
}
</script>
{% endblock %}