Files
ip/index.html
T
2026-09-10 16:12:43 -04:00

269 lines
14 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>IPTV 运行状态</title>
<style>
:root { --bg:#eef3fb; --panel:#ffffff; --panel-2:#f6f9ff; --text:#1c2333; --muted:#667085; --line:#dfe7f5; --accent:#2d6cdf; --accent-2:#34a853; --warn:#f59e0b; --danger:#ef4444; --shadow:0 8px 18px rgba(21,37,63,0.08); }
* { box-sizing:border-box; }
body { margin:0; font:14px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background:var(--bg); color:var(--text); }
.container { max-width:1100px; margin:32px auto; padding:0 16px 40px; }
h1 { margin:0 0 8px; font-size:32px; }
.meta { color:var(--muted); margin-bottom:18px; }
.card { background:var(--panel); border:1px solid var(--line); border-radius:16px; box-shadow:var(--shadow); }
.progress-card { padding:22px 20px; margin-bottom:18px; }
.row { display:flex; align-items:center; justify-content:space-between; gap:12px; }
.badge { font-weight:700; color:var(--accent); background:#edf3ff; padding:6px 10px; border-radius:999px; }
.progress { height:14px; background:#edf2fb; border-radius:999px; overflow:hidden; margin:16px 0; border:1px solid var(--line); }
.progress-bar { width:0%; height:100%; background:linear-gradient(90deg, var(--accent), #6aa7ff); transition:width .35s ease; }
.mini-grid { display:grid; grid-template-columns:repeat(auto-fit, minmax(170px, 1fr)); gap:12px; }
.mini { background:var(--panel-2); border:1px solid var(--line); border-radius:12px; padding:12px 14px; }
.mini label { display:block; color:var(--muted); font-size:12px; margin-bottom:6px; }
.mini strong { font-size:20px; }
.grid { display:grid; grid-template-columns:repeat(auto-fit, minmax(180px, 1fr)); gap:12px; margin:18px 0; }
.stat { padding:16px 14px; }
.stat small { color:var(--muted); display:block; margin-bottom:6px; }
.stat .n { font-size:26px; font-weight:700; }
.section { margin-top:18px; }
.section h2 { margin:0 0 12px; font-size:20px; }
.stage-list, .probe-list, .run-list { display:grid; gap:10px; }
.stage-item, .probe-item, .run-item { background:var(--panel); border:1px solid var(--line); border-radius:12px; padding:12px 14px; }
.stage-head, .probe-head { display:flex; justify-content:space-between; align-items:center; gap:10px; }
.stage-name, .probe-name { font-weight:700; }
.muted { color:var(--muted); }
.tag { display:inline-block; padding:4px 8px; border-radius:999px; font-size:12px; font-weight:600; }
.tag.ok { background:#ebf9f0; color:#19763d; }
.tag.running { background:#edf3ff; color:var(--accent); }
.tag.wait { background:#f6f7fb; color:#59708d; }
.tag.err { background:#fff0f1; color:#c62828; }
.percent-line { margin-top:10px; height:8px; border-radius:999px; overflow:hidden; background:#edf2fb; }
.percent-fill { height:100%; border-radius:999px; background:linear-gradient(90deg, var(--accent-2), #7dd48a); }
.summary-grid { display:grid; grid-template-columns:repeat(auto-fit, minmax(180px, 1fr)); gap:12px; margin:18px 0; }
.summary-box { background:var(--panel); border:1px solid var(--line); border-radius:12px; padding:14px; }
.summary-box .label { color:var(--muted); font-size:12px; margin-bottom:6px; }
.summary-box .value { font-size:22px; font-weight:700; }
.error-box { border:1px solid #f3d7d9; background:#fff5f6; color:#8b1e2f; border-radius:12px; padding:12px 14px; margin-top:18px; }
.probe-grid { display:grid; grid-template-columns:repeat(auto-fit, minmax(220px, 1fr)); gap:12px; }
.run-item { display:grid; grid-template-columns: 110px 1fr 90px 120px; gap:12px; align-items:center; }
.run-item .right { text-align:right; }
table { width:100%; border-collapse:collapse; background:var(--panel); border:1px solid var(--line); border-radius:12px; overflow:hidden; }
th, td { padding:10px 12px; border-bottom:1px solid var(--line); text-align:left; }
th { background:#f7faff; }
a { color:var(--accent); text-decoration:none; }
@media (max-width: 720px) { .run-item { grid-template-columns:1fr; } .run-item .right { text-align:left; } }
</style>
</head>
<body>
<div class="container">
<h1>IPTV 运行状态</h1>
<div id="meta" class="meta">加载中...</div>
<div class="card progress-card">
<div class="row">
<h2 style="margin:0">总任务进度</h2>
<span id="progress-badge" class="badge">0%</span>
</div>
<div class="progress"><div id="progress-bar" class="progress-bar"></div></div>
<div class="mini-grid">
<div class="mini"><label>已完成阶段</label><strong id="stage-count">0/0</strong></div>
<div class="mini"><label>已耗时</label><strong id="elapsed-total">0s</strong></div>
<div class="mini"><label>当前阶段</label><strong id="current-stage">-</strong></div>
</div>
</div>
<div id="summary-grid" class="summary-grid"></div>
<div id="error-box" class="error-box" style="display:none"></div>
<div id="stream-cards" class="grid"></div>
<div class="section">
<h2>各阶段耗时</h2>
<div id="stage-list" class="stage-list"></div>
</div>
<div class="section">
<h2>GitHub / 码云探针</h2>
<div id="probe-list" class="probe-grid"></div>
</div>
<div class="section">
<h2>质量闭环</h2>
<div id="quality-box" class="card stage-item"></div>
</div>
<div class="section">
<h2>最近阶段记录</h2>
<div id="recent-runs" class="run-list"></div>
</div>
</div>
<script>
function fmtSeconds(v) {
const n = Number(v || 0);
if (n < 60) return n.toFixed(1) + 's';
const m = Math.floor(n / 60);
const s = Math.round(n % 60);
return m + 'm ' + s + 's';
}
function statusTag(state) {
if (state === 'ok') return '<span class="tag ok">完成</span>'
if (state === 'running') return '<span class="tag running">运行中</span>'
if (state === 'err') return '<span class="tag err">异常</span>'
if (state === 'failed') return '<span class="tag err">失败</span>'
return '<span class="tag wait">待处理</span>'
}
function renderSummary(job) {
const summary = job || {};
const health = summary.stage_health || {};
const items = [
{label: '任务状态', value: summary.state || 'unknown'},
{label: '当前阶段', value: summary.current_stage || '待定'},
{label: '已完成', value: `${health.ok || 0} / ${(health.ok || 0) + (health.running || 0) + (health.waiting || 0) + (health.err || 0)}`},
{label: '异常数', value: health.err || 0}
];
document.getElementById('summary-grid').innerHTML = items.map(item => `
<div class="summary-box">
<div class="label">${item.label}</div>
<div class="value">${item.value}</div>
</div>
`).join('');
const lastError = summary.last_error || '';
const errorBox = document.getElementById('error-box');
if (lastError && lastError !== '未发现异常') {
errorBox.style.display = 'block';
errorBox.textContent = '最近异常: ' + lastError;
} else {
errorBox.style.display = 'none';
errorBox.textContent = '';
}
}
function renderStageList(stages) {
const items = Object.entries(stages || {}).map(([name, stage]) => {
const label = stage.label || name;
const state = stage.state || 'wait';
const total = Number(stage.total || 0);
const current = Number(stage.current || 0);
const known = total > 0;
const pct = known ? Math.min(100, Math.round((current / total) * 100)) : (state === 'ok' ? 100 : 0);
const progressText = known ? `${current}/${total} · ${pct}%` : '实际计数未提供';
return `
<div class="stage-item">
<div class="stage-head">
<div class="stage-name">${label}</div>
${statusTag(state)}
</div>
<div class="muted">${state === 'ok' ? '耗时' : '已用'}: ${fmtSeconds(stage.elapsed_seconds || 0)} · ${progressText}</div>
<div class="percent-line"><div class="percent-fill" style="width:${pct}%"></div></div>
</div>
`;
});
return items.join('');
}
function renderProbe(data) {
const summary = data.probe_summary || {};
const items = [];
items.push({
name: '码云探针',
value: data.pending_probe ? (data.pending_probe.batch_id || '已提交') : '无待处理',
extra: data.pending_probe ? '过期: ' + (data.pending_probe.expires_at || '-') : '等待新批次',
state: data.pending_probe ? 'running' : 'wait',
meta: `累计 ${summary.gitee_total || 0} 批 · 已提交 ${summary.gitee_submitted || 0} · 已消费 ${summary.gitee_consumed || 0}`
});
items.push({
name: 'GitHub 探针',
value: data.pending_github ? (data.pending_github.batch_id || '已提交') : '无待处理',
extra: data.pending_github ? '过期: ' + (data.pending_github.expires_at || '-') : '等待新批次',
state: data.pending_github ? 'running' : 'wait',
meta: `累计 ${summary.github_total || 0} 批 · 已提交 ${summary.github_submitted || 0} · 已消费 ${summary.github_consumed || 0}`
});
const details = (data.probe_details || []).map(item => {
const candidate = item.candidate || {};
const result = item.result || {};
const resultText = result.count == null ? '暂无回传' : `${result.count} 条结果 · ${result.status || '未知'}`;
const progress = item.progress || {};
const progressText = progress.total ? `处理中 ${progress.processed}/${progress.total},剩余 ${progress.remaining},${progress.status}` : '暂无处理中进度';
return `<div class="muted" style="margin-top:6px">${item.label}: ${progressText}<br>候选 ${candidate.count || 0} 条 · ${resultText}<br>候选批次 ${candidate.batch_id || '-'} · 回传批次 ${result.batch_id || '-'}<br>结果更新时间 ${result.mtime || '暂无'} · 进度更新时间 ${progress.updated_at || '暂无'}</div>`;
}).join('');
return items.map(item => `
<div class="probe-item">
<div class="probe-head">
<div class="probe-name">${item.name}</div>
${statusTag(item.state)}
</div>
<div class="muted" style="margin-top:8px">${item.value}</div>
<div class="muted">${item.extra}</div>
<div class="muted" style="margin-top:6px; color:#2d6cdf; font-weight:600">${item.meta}</div>${details}
</div>
`).join('');
}
function renderRuns(rows) {
if (!rows || !rows.length) return '<div class="card stage-item">暂无最近阶段记录</div>'
return rows.map(r => `
<div class="run-item card">
<div>${(r.run_id || '-').slice(0, 12)}</div>
<div>${r.stage || '-'}</div>
<div>${r.status || '-'}</div>
<div class="right">${r.end_at || '-'}</div>
</div>
`).join('')
}
function loadData() {
fetch('status.json', {cache:'no-store'}).then(r => r.json()).then(d => {
const rt = d.runtime || {};
const job = d.job_summary || {};
const pct = Number(rt.progress_pct || 0);
document.getElementById('meta').textContent = `状态: ${rt.state || 'unknown'} · 更新于 ${d.generated_at || '-'}`;
document.getElementById('progress-badge').textContent = pct.toFixed(2) + '%';
document.getElementById('progress-bar').style.width = pct + '%';
document.getElementById('stage-count').textContent = `${rt.completed_stage_count || 0}/${rt.total_stage_count || 0}`;
document.getElementById('elapsed-total').textContent = fmtSeconds(rt.elapsed_seconds || 0);
document.getElementById('current-stage').textContent = rt.current_stage || '-';
renderSummary(job);
const streamCards = Object.entries(d.streams || {}).map(([k, v]) => `
<div class="card stat">
<small>${k}</small>
<div class="n">${v}</div>
</div>
`).join('') + `
<div class="card stat"><small>严格稳定</small><div class="n">${d.strict_streams || 0}</div></div>`;
streamCards += `<div class="card stat"><small>时间性观察</small><div class="n">${d.intermittent_streams || 0}</div></div>`;
document.getElementById('stream-cards').innerHTML = streamCards;
document.getElementById('stage-list').innerHTML = renderStageList(rt.stages || {});
document.getElementById('probe-list').innerHTML = renderProbe(d);
const quality = d.quality || {};
const qText = `GitHub 已深检 ${quality.github_total || 0} 条,可解码 ${quality.github_video_ok || 0} 条,风险事件 ${quality.github_risk_events || 0}。时间性观察线路 ${d.intermittent_streams || 0} 条,需跨时段复测后再进入严格列表。`;
document.getElementById('quality-box').innerHTML = `<div class="muted">${qText}</div>`;
document.getElementById('recent-runs').innerHTML = renderRuns(d.recent_runs || []);
}).catch(err => {
document.getElementById('meta').textContent = '状态读取失败: ' + err;
});
}
let refreshTimer = null;
function startRefresh() {
if (document.hidden || refreshTimer) return;
refreshTimer = setInterval(loadData, 10000);
}
function stopRefresh() {
if (refreshTimer) {
clearInterval(refreshTimer);
refreshTimer = null;
}
}
document.addEventListener('visibilitychange', () => document.hidden ? stopRefresh() : startRefresh());
window.addEventListener('pagehide', stopRefresh);
loadData();
startRefresh();
</script>
</body>
</html>