2.0.0 Alpha: Data Refinery

This commit is contained in:
2026-08-08 21:31:56 +08:00
parent fa75081d4d
commit 562775e5db
48 changed files with 4172 additions and 661 deletions
+12 -9
View File
@@ -12,9 +12,8 @@
<div class="border border-gray-200 dark:border-gray-700 rounded-lg p-4">
<h3 class="text-lg font-bold text-gray-900 dark:text-white mb-4">数据管线 (ETL)</h3>
<div class="space-y-2">
<button onclick="triggerEtl('L1A.py')" class="w-full bg-blue-600 text-white py-2 px-4 rounded hover:bg-blue-700">Trigger L1A (Ingest)</button>
<button onclick="triggerEtl('L2_Builder.py')" class="w-full bg-blue-600 text-white py-2 px-4 rounded hover:bg-blue-700">Trigger L2 Builder</button>
<button onclick="triggerEtl('L3_Builder.py')" class="w-full bg-blue-600 text-white py-2 px-4 rounded hover:bg-blue-700">Trigger L3 Builder</button>
<a href="{{ url_for('admin.import_match') }}" class="block w-full text-center bg-yrtv-600 text-white py-2 px-4 rounded hover:bg-yrtv-500">上传并导入比赛</a>
<button onclick="triggerEtl()" class="w-full bg-blue-600 text-white py-2 px-4 rounded hover:bg-blue-700">运行完整 L1 → L2 → L3</button>
</div>
<div id="etlResult" class="mt-4 text-sm text-gray-600 dark:text-gray-400"></div>
</div>
@@ -23,6 +22,7 @@
<div class="border border-gray-200 dark:border-gray-700 rounded-lg p-4">
<h3 class="text-lg font-bold text-gray-900 dark:text-white mb-4">工具箱</h3>
<div class="space-y-2">
<a href="{{ url_for('admin.data_integrity') }}" class="block w-full text-center bg-emerald-600 text-white py-2 px-4 rounded hover:bg-emerald-700">数据完整性中心</a>
<a href="{{ url_for('admin.sql_runner') }}" class="block w-full text-center bg-gray-600 text-white py-2 px-4 rounded hover:bg-gray-700">SQL Runner</a>
<a href="{{ url_for('wiki.index') }}" class="block w-full text-center bg-gray-600 text-white py-2 px-4 rounded hover:bg-gray-700">Manage Wiki</a>
</div>
@@ -31,20 +31,23 @@
</div>
<script>
function triggerEtl(scriptName) {
function triggerEtl() {
const resultDiv = document.getElementById('etlResult');
resultDiv.innerText = "Triggering " + scriptName + "...";
resultDiv.innerText = "正在创建后台流水线...";
fetch("{{ url_for('admin.trigger_etl') }}", {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: 'script=' + scriptName
})
.then(response => response.text())
.then(text => {
resultDiv.innerText = text;
.then(response => response.json())
.then(data => {
if (data.success) {
window.location.href = "{{ url_for('admin.import_match') }}?job_id=" + data.job_id;
} else {
resultDiv.innerText = data.error || "启动失败";
}
})
.catch(err => {
resultDiv.innerText = "Error: " + err;
+84
View File
@@ -0,0 +1,84 @@
{% extends "base.html" %}
{% block title %}数据完整性 - YRTV{% endblock %}
{% block content %}
{% set status_styles = {
'pass': 'bg-emerald-100 text-emerald-800 dark:bg-emerald-900/40 dark:text-emerald-300',
'warn': 'bg-amber-100 text-amber-800 dark:bg-amber-900/40 dark:text-amber-300',
'fail': 'bg-red-100 text-red-800 dark:bg-red-900/40 dark:text-red-300'
} %}
<div class="space-y-6 px-4 sm:px-0">
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div>
<div class="flex items-center gap-3">
<h1 class="text-2xl font-bold text-slate-900 dark:text-white">数据完整性中心</h1>
<span class="rounded-full px-3 py-1 text-xs font-semibold uppercase {{ status_styles[report.overall_status] }}">
{{ report.overall_status }}
</span>
</div>
<p class="mt-1 text-sm text-slate-500 dark:text-slate-400">
校验时间:{{ report.generated_at }}
</p>
</div>
<div class="flex gap-2">
<a href="{{ url_for('admin.data_integrity', format='json') }}"
class="rounded-lg border border-slate-300 px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50 dark:border-slate-600 dark:text-slate-200 dark:hover:bg-slate-800">
JSON
</a>
<a href="{{ url_for('admin.data_integrity') }}"
class="rounded-lg bg-yrtv-600 px-4 py-2 text-sm font-medium text-white hover:bg-yrtv-500">
重新校验
</a>
</div>
</div>
<div class="grid grid-cols-3 gap-3">
{% for status, label in [('pass', '通过'), ('warn', '警告'), ('fail', '失败')] %}
<div class="rounded-xl bg-white p-4 shadow dark:bg-slate-800">
<div class="text-sm text-slate-500 dark:text-slate-400">{{ label }}</div>
<div class="mt-1 text-3xl font-bold {% if status == 'pass' %}text-emerald-600{% elif status == 'warn' %}text-amber-600{% else %}text-red-600{% endif %}">
{{ report.totals[status] }}
</div>
</div>
{% endfor %}
</div>
<div class="rounded-xl bg-white p-5 shadow dark:bg-slate-800">
<h2 class="mb-4 text-lg font-semibold text-slate-900 dark:text-white">数据规模</h2>
<div class="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-5">
{% for key, value in report.counts.items() %}
<div class="rounded-lg bg-slate-50 p-3 dark:bg-slate-900/60">
<div class="truncate text-xs uppercase tracking-wide text-slate-500">{{ key|replace('_', ' ') }}</div>
<div class="mt-1 text-xl font-semibold text-slate-900 dark:text-white">{{ "{:,}".format(value) }}</div>
</div>
{% endfor %}
</div>
</div>
<div class="overflow-hidden rounded-xl bg-white shadow dark:bg-slate-800">
<div class="border-b border-slate-200 px-5 py-4 dark:border-slate-700">
<h2 class="text-lg font-semibold text-slate-900 dark:text-white">校验项目</h2>
</div>
<div class="divide-y divide-slate-100 dark:divide-slate-700">
{% for check in report.checks %}
<div class="flex flex-col gap-2 px-5 py-4 sm:flex-row sm:items-center sm:justify-between">
<div>
<div class="font-medium text-slate-900 dark:text-white">{{ check.name }}</div>
<div class="mt-1 text-sm text-slate-500 dark:text-slate-400">{{ check.detail }}</div>
</div>
<span class="self-start rounded-full px-3 py-1 text-xs font-semibold uppercase sm:self-center {{ status_styles[check.status] }}">
{{ check.status }}
</span>
</div>
{% endfor %}
</div>
</div>
<div>
<a href="{{ url_for('admin.dashboard') }}" class="text-sm font-medium text-yrtv-600 hover:text-yrtv-500">
返回管理后台
</a>
</div>
</div>
{% endblock %}
+153
View File
@@ -0,0 +1,153 @@
{% extends "base.html" %}
{% block title %}比赛导入 - YRTV{% endblock %}
{% block content %}
<div class="space-y-6 px-4 sm:px-0">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<div class="space-y-2">
{% for category, message in messages %}
<div class="rounded-lg px-4 py-3 text-sm {% if category == 'success' %}bg-emerald-100 text-emerald-800{% elif category == 'warning' %}bg-amber-100 text-amber-800{% else %}bg-red-100 text-red-800{% endif %}">
{{ message }}
</div>
{% endfor %}
</div>
{% endif %}
{% endwith %}
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold text-slate-900 dark:text-white">比赛数据导入</h1>
<p class="mt-1 text-sm text-slate-500 dark:text-slate-400">
上传完整的 iframe_network.json,系统会自动识别比赛 ID,并执行 L1 → L2 → L3。
</p>
</div>
<a href="{{ url_for('admin.dashboard') }}" class="text-sm font-medium text-yrtv-600 hover:text-yrtv-500">
返回管理后台
</a>
</div>
<div class="grid grid-cols-1 gap-6 lg:grid-cols-3">
<div class="rounded-xl bg-white p-6 shadow dark:bg-slate-800">
<h2 class="text-lg font-semibold text-slate-900 dark:text-white">上传抓包</h2>
<form method="POST" enctype="multipart/form-data" class="mt-5 space-y-4">
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300">
iframe_network.json
</label>
<input type="file" name="capture" accept=".json,application/json" required
class="mt-2 block w-full text-sm text-slate-600 file:mr-4 file:rounded-lg file:border-0 file:bg-yrtv-50 file:px-4 file:py-2 file:font-medium file:text-yrtv-700 hover:file:bg-yrtv-100 dark:text-slate-300">
</div>
<label class="flex items-start gap-2 text-sm text-slate-600 dark:text-slate-300">
<input type="checkbox" name="replace" value="1" class="mt-1 rounded border-slate-300 text-yrtv-600">
<span>允许替换已存在但内容不同的比赛。流水线失败时会自动恢复数据库。</span>
</label>
<button type="submit"
class="w-full rounded-lg bg-yrtv-600 px-4 py-2.5 font-medium text-white hover:bg-yrtv-500">
校验并开始导入
</button>
</form>
<div class="mt-5 rounded-lg bg-amber-50 p-3 text-xs text-amber-800 dark:bg-amber-900/30 dark:text-amber-200">
直接重复上传会被拒绝。每次流水线运行前都会备份 L1、L2、L3,并执行后置完整性检查。
</div>
</div>
<div class="rounded-xl bg-white p-6 shadow dark:bg-slate-800 lg:col-span-2">
<div class="flex items-center justify-between">
<h2 class="text-lg font-semibold text-slate-900 dark:text-white">作业状态</h2>
{% if selected_job %}
<span id="job-status" class="rounded-full bg-slate-100 px-3 py-1 text-xs font-semibold uppercase text-slate-700 dark:bg-slate-700 dark:text-slate-200">
{{ selected_job.status }}
</span>
{% endif %}
</div>
{% if selected_job %}
<div class="mt-5">
<div class="flex justify-between text-sm text-slate-600 dark:text-slate-300">
<span id="job-stage">{{ selected_job.current_stage or 'queued' }}</span>
<span id="job-progress-label">{{ selected_job.progress }}%</span>
</div>
<div class="mt-2 h-2 overflow-hidden rounded-full bg-slate-200 dark:bg-slate-700">
<div id="job-progress" class="h-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 or '等待执行' }}
</p>
<pre id="job-log" class="mt-4 max-h-96 overflow-auto whitespace-pre-wrap rounded-lg bg-slate-950 p-4 text-xs text-slate-200">{{ selected_job.log_text }}</pre>
</div>
{% else %}
<div class="mt-8 rounded-lg border border-dashed border-slate-300 p-8 text-center text-sm text-slate-500 dark:border-slate-600">
上传比赛或从下方选择历史作业。
</div>
{% endif %}
</div>
</div>
<div class="overflow-hidden rounded-xl bg-white shadow dark:bg-slate-800">
<div class="border-b border-slate-200 px-5 py-4 dark:border-slate-700">
<h2 class="font-semibold text-slate-900 dark:text-white">最近作业</h2>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-slate-200 dark:divide-slate-700">
<thead class="bg-slate-50 dark:bg-slate-900/50">
<tr>
<th class="px-5 py-3 text-left text-xs font-semibold uppercase text-slate-500">ID</th>
<th class="px-5 py-3 text-left text-xs font-semibold uppercase text-slate-500">类型</th>
<th class="px-5 py-3 text-left text-xs font-semibold uppercase text-slate-500">比赛</th>
<th class="px-5 py-3 text-left text-xs font-semibold uppercase text-slate-500">状态</th>
<th class="px-5 py-3 text-left text-xs font-semibold uppercase text-slate-500">阶段</th>
<th class="px-5 py-3 text-left text-xs font-semibold uppercase text-slate-500">耗时</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100 dark:divide-slate-700">
{% for job in jobs %}
<tr class="hover:bg-slate-50 dark:hover:bg-slate-700/40">
<td class="px-5 py-3 text-sm">
<a href="{{ url_for('admin.import_match', job_id=job.id) }}" class="font-medium text-yrtv-600">#{{ job.id }}</a>
</td>
<td class="px-5 py-3 text-sm text-slate-600 dark:text-slate-300">{{ job.job_type }}</td>
<td class="px-5 py-3 text-sm font-mono text-slate-600 dark:text-slate-300">{{ job.match_id or '-' }}</td>
<td class="px-5 py-3 text-sm text-slate-600 dark:text-slate-300">{{ job.status }}</td>
<td class="px-5 py-3 text-sm text-slate-600 dark:text-slate-300">{{ job.current_stage or '-' }}</td>
<td class="px-5 py-3 text-sm text-slate-600 dark:text-slate-300">{{ '%.2fs'|format(job.duration_seconds) if job.duration_seconds is not none else '-' }}</td>
</tr>
{% else %}
<tr><td colspan="6" class="px-5 py-8 text-center text-sm text-slate-500">暂无作业</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
{% if selected_job %}
<script>
const jobId = {{ selected_job.id }};
let pollTimer = 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 || '';
const log = document.getElementById('job-log');
log.textContent = job.log_text || '';
log.scrollTop = log.scrollHeight;
if (job.status === 'succeeded' || job.status === 'failed') {
clearInterval(pollTimer);
}
}
pollTimer = setInterval(refreshJob, 1500);
refreshJob();
</script>
{% endif %}
{% endblock %}