update 2026-09-10 11:03
This commit is contained in:
1 parent
1cf88c5d14
commit
65ab3a9ae2
6 files changed
+380
-33
No files matched your search
+257
-1
@@ -1 +1,257 @@
|
||||
<!doctype html><meta charset=utf-8><meta name=viewport content='width=device-width,initial-scale=1'><title>IPTV 运行状态</title><style>body{max-width:920px;margin:32px auto;padding:0 16px;font:15px system-ui;color:#182230;background:#f5f7fb}h1{margin-bottom:4px}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(145px,1fr));gap:12px}.card{padding:14px;border-radius:10px;background:#fff;box-shadow:0 1px 3px #0001}.n{font-size:26px;font-weight:700}table{width:100%;border-collapse:collapse;background:#fff;margin-top:16px}td,th{padding:9px;border-bottom:1px solid #eee;text-align:left}a{color:#1463cf}small{color:#667}</style><h1>IPTV 运行状态</h1><small id=time>加载中</small><h2>线路</h2><div class=grid id=cards></div><h2>分类</h2><table id=cats><tr><th>分类</th><th>频道</th><th>线路</th></tr></table><h2>质量闭环</h2><div class=card id=quality></div><h2>探针状态</h2><div class=card id=probe></div><h2>最近阶段</h2><table id=runs><tr><th>运行</th><th>阶段</th><th>状态</th><th>完成时间</th></tr></table><p><a href='https://gitea.zzjp.help/zjp/ip/raw/branch/main/all.m3u'>通用 M3U</a> · <a href='https://gitea.zzjp.help/zjp/ip/raw/branch/main/tvbox.json'>TVBox</a> · <a href='https://gitea.zzjp.help/zjp/ip/raw/branch/main/epg.xml'>EPG XMLTV</a> · <a href='https://gitea.zzjp.help/zjp/ip/raw/branch/main/README.md'>使用说明</a></p><script>const labels={cctv:'央视',satellite:'卫视',local:'地方台',hk_mo_tw:'港澳台',webcast:'网络直播'};fetch('status.json',{cache:'no-store'}).then(x=>x.json()).then(d=>{time.textContent='更新于 '+d.generated_at;cards.innerHTML=Object.entries(d.streams).map(([k,v])=>`<div class=card><small>${k}</small><div class=n>${v}</div></div>`).join('')+`<div class=card><small>严格稳定</small><div class=n>${d.strict_streams}</div></div>`;cats.innerHTML+=Object.entries(d.categories).map(([k,v])=>`<tr><td>${labels[k]||k}</td><td>${v.channels}</td><td>${v.streams}</td></tr>`).join('');quality.textContent=d.quality?`GitHub 已深检 ${d.quality.github_total} 条,可解码 ${d.quality.github_video_ok} 条,风险事件 ${d.quality.github_risk_events} 条;VPS 视频深检已关闭`:'暂无深检质量数据';probe.textContent=(d.pending_probe?`Gitee 等待 ${d.pending_probe.batch_id},截止 ${d.pending_probe.expires_at}`:'Gitee 无等待批次')+(d.pending_github?`;GitHub 等待 ${d.pending_github.batch_id},截止 ${d.pending_github.expires_at}`:';GitHub 无等待批次');runs.innerHTML+=d.recent_runs.map(x=>`<tr><td>${x.run_id}</td><td>${x.stage}</td><td>${x.status}</td><td>${x.end_at||''}</td></tr>`).join('')}).catch(()=>time.textContent='状态数据读取失败')</script>
|
||||
<!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 pct = total > 0 ? Math.min(100, Math.round((current / total) * 100)) : (state === 'ok' ? 100 : 0);
|
||||
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)} · ${current}/${total || 1}</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}`
|
||||
});
|
||||
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>
|
||||
</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>`;
|
||||
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}。`;
|
||||
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>
|
||||
Reference in new issue
Block a user