Remove CTM and inbound prediction

This commit is contained in:
2026-07-31 18:23:24 +08:00
commit e4b4fa5cdb
91 changed files with 15128 additions and 0 deletions
+396
View File
@@ -0,0 +1,396 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Stream Manager</title>
<style>
:root {
--bg: #0b1220;
--panel: #111a2e;
--border: rgba(255,255,255,0.08);
--text: rgba(255,255,255,0.92);
--muted: rgba(255,255,255,0.65);
--good: #22c55e;
--warn: #f59e0b;
--bad: #ef4444;
--link: #60a5fa;
--mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
}
body {
margin: 0;
font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial;
background: var(--bg);
color: var(--text);
}
header {
padding: 18px 20px;
border-bottom: 1px solid var(--border);
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
header h1 {
margin: 0;
font-size: 16px;
font-weight: 600;
}
header .meta {
font-size: 12px;
color: var(--muted);
display: flex;
gap: 14px;
align-items: center;
flex-wrap: wrap;
justify-content: flex-end;
}
main { padding: 16px 20px 28px; }
.toolbar {
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
margin-bottom: 12px;
}
.toolbar input {
background: var(--panel);
border: 1px solid var(--border);
color: var(--text);
padding: 8px 10px;
border-radius: 8px;
width: 320px;
max-width: 100%;
outline: none;
}
.toolbar button, .toolbar a {
background: var(--panel);
border: 1px solid var(--border);
color: var(--text);
padding: 8px 10px;
border-radius: 8px;
cursor: pointer;
text-decoration: none;
font-size: 12px;
}
.toolbar button:hover, .toolbar a:hover { border-color: rgba(96,165,250,0.6); }
table {
width: 100%;
border-collapse: collapse;
background: var(--panel);
border: 1px solid var(--border);
border-radius: 10px;
overflow: hidden;
}
th, td {
padding: 10px 12px;
border-bottom: 1px solid rgba(255,255,255,0.06);
font-size: 12px;
vertical-align: top;
}
th {
text-align: left;
color: var(--muted);
font-weight: 600;
background: rgba(255,255,255,0.02);
position: sticky;
top: 0;
z-index: 1;
}
tr:hover td { background: rgba(96,165,250,0.06); }
.mono { font-family: var(--mono); }
a.link { color: var(--link); text-decoration: none; }
a.link:hover { text-decoration: underline; }
.pill {
display: inline-flex;
align-items: center;
padding: 2px 8px;
border: 1px solid var(--border);
border-radius: 999px;
font-size: 11px;
color: var(--muted);
white-space: nowrap;
}
.btn-danger { border-color: rgba(239,68,68,0.4); color: rgba(239,68,68,0.95); }
.btn-danger:hover { border-color: rgba(239,68,68,0.9); }
.modal {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.65);
display: none;
align-items: center;
justify-content: center;
padding: 20px;
z-index: 10;
}
.modal.open { display: flex; }
.modal-card {
width: 100%;
max-width: 520px;
background: var(--panel);
border: 1px solid var(--border);
border-radius: 12px;
overflow: hidden;
}
.modal-header {
padding: 12px 14px;
border-bottom: 1px solid var(--border);
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-title { font-weight: 600; font-size: 14px; }
.modal-body { padding: 14px; display: flex; flex-direction: column; gap: 10px; }
.modal-body label { font-size: 12px; color: var(--muted); }
.modal-body input {
background: rgba(0,0,0,0.25);
border: 1px solid var(--border);
color: var(--text);
padding: 10px 10px;
border-radius: 8px;
outline: none;
font-family: var(--mono);
}
.modal-footer {
padding: 12px 14px;
border-top: 1px solid var(--border);
display: flex;
justify-content: flex-end;
gap: 10px;
}
</style>
</head>
<body>
<header>
<h1>Stream Manager</h1>
<div class="meta">
<span id="meta-base"></span>
<span id="meta-updated"></span>
</div>
</header>
<main>
<div class="toolbar">
<input id="filter" placeholder="筛选:站名 / 站号 / 流名" />
<button id="btn-add">新增站点</button>
<button id="btn-refresh">刷新</button>
<a href="/" target="_blank" rel="noreferrer">控制台</a>
</div>
<table>
<thead>
<tr>
<th style="width: 150px;">流名</th>
<th style="width: 260px;">车站</th>
<th style="width: 140px;">站号</th>
<th>播放地址(m3u8)</th>
<th style="width: 180px;">操作</th>
</tr>
</thead>
<tbody id="tbody"></tbody>
</table>
</main>
<div class="modal" id="modal">
<div class="modal-card">
<div class="modal-header">
<div class="modal-title" id="modal-title">站点</div>
<button id="modal-close">关闭</button>
</div>
<div class="modal-body">
<div style="display:flex; flex-direction:column; gap:6px;">
<label>站名(唯一键)</label>
<input id="input-name" placeholder="例如:HC-02 站台 / Tokyo Station" />
</div>
<div style="display:flex; flex-direction:column; gap:6px;">
<label>站号(会影响流名)</label>
<input id="input-code" placeholder="例如:HC-02_2" />
</div>
<div style="display:flex; flex-direction:column; gap:6px;">
<label>重命名(可选,仅编辑时生效)</label>
<input id="input-newname" placeholder="不填则不改站名" />
</div>
</div>
<div class="modal-footer">
<button id="btn-cancel">取消</button>
<button id="btn-save">保存</button>
</div>
</div>
</div>
<script>
const tbody = document.getElementById('tbody');
const filterInput = document.getElementById('filter');
const metaBase = document.getElementById('meta-base');
const metaUpdated = document.getElementById('meta-updated');
const modal = document.getElementById('modal');
const modalTitle = document.getElementById('modal-title');
const inputName = document.getElementById('input-name');
const inputCode = document.getElementById('input-code');
const inputNewName = document.getElementById('input-newname');
let mode = 'create';
let editKey = '';
let lastData = null;
function escapeHtml(s) {
return String(s ?? '')
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#39;');
}
function openModal(nextMode, key, row) {
mode = nextMode;
editKey = key || '';
if (mode === 'create') {
modalTitle.textContent = '新增站点';
inputName.disabled = false;
inputName.value = '';
inputCode.value = '';
inputNewName.value = '';
} else {
modalTitle.textContent = '编辑站点';
inputName.disabled = true;
inputName.value = row?.stationName || '';
inputCode.value = row?.stationCode || '';
inputNewName.value = '';
}
modal.classList.add('open');
}
function closeModal() {
modal.classList.remove('open');
}
function render(data) {
lastData = data;
metaBase.textContent = `Base: ${data.publicBase || '-'}`;
metaUpdated.textContent = `Updated: ${new Date().toLocaleTimeString()}`;
const q = filterInput.value.trim().toLowerCase();
const rows = (data.streams || []).filter(s => {
if (!q) return true;
return String(s.streamName || '').toLowerCase().includes(q)
|| String(s.stationName || '').toLowerCase().includes(q)
|| String(s.stationCode || '').toLowerCase().includes(q)
|| String(s.hlsUrl || '').toLowerCase().includes(q);
});
tbody.innerHTML = rows.map(s => {
const hasStation = !!s.stationName;
const actions = hasStation
? `
<button onclick="editStation('${escapeHtml(s.stationName)}')">编辑</button>
<button class="btn-danger" onclick="deleteStation('${escapeHtml(s.stationName)}')">删除站点</button>
`
: `
<button class="btn-danger" onclick="deleteStream('${escapeHtml(s.streamName)}')">清理流</button>
`;
return `
<tr>
<td class="mono">${escapeHtml(s.streamName || '-')}</td>
<td>${escapeHtml(s.stationName || '-')}</td>
<td class="mono">${escapeHtml(s.stationCode || '-')}</td>
<td class="mono">
<a class="link" href="${escapeHtml(s.hlsUrl || '#')}" target="_blank" rel="noreferrer">${escapeHtml(s.hlsUrl || '-')}</a>
</td>
<td style="white-space:nowrap">${actions}</td>
</tr>
`;
}).join('');
}
async function refresh() {
const res = await fetch('/api/streams', { cache: 'no-store' });
const data = await res.json();
render(data);
}
async function editStation(stationName) {
const row = (lastData?.streams || []).find(s => s.stationName === stationName);
openModal('edit', stationName, row);
}
async function deleteStation(stationName) {
if (!confirm(`删除站点:${stationName} ?`)) return;
await fetch(`/api/stations/${encodeURIComponent(stationName)}`, { method: 'DELETE' });
await refresh();
}
async function deleteStream(streamName) {
if (!confirm(`清理流状态:${streamName} ?`)) return;
await fetch(`/api/streams/${encodeURIComponent(streamName)}`, { method: 'DELETE' });
await refresh();
}
async function saveModal() {
const name = inputName.value.trim();
const code = inputCode.value.trim();
const newName = inputNewName.value.trim();
if (mode === 'create') {
if (!name) { alert('站名不能为空'); return; }
const res = await fetch('/api/stations', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, code })
});
const data = await res.json();
if (!data.success) { alert(data.message || '保存失败'); return; }
} else {
const res = await fetch(`/api/stations/${encodeURIComponent(editKey)}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code, newName: newName || undefined })
});
const data = await res.json();
if (!data.success) { alert(data.message || '保存失败'); return; }
}
closeModal();
await refresh();
}
document.getElementById('btn-add').addEventListener('click', () => openModal('create'));
document.getElementById('btn-refresh').addEventListener('click', refresh);
filterInput.addEventListener('input', () => { if (lastData) render(lastData); });
document.getElementById('modal-close').addEventListener('click', closeModal);
document.getElementById('btn-cancel').addEventListener('click', closeModal);
document.getElementById('btn-save').addEventListener('click', saveModal);
modal.addEventListener('click', (e) => { if (e.target === modal) closeModal(); });
refresh();
setInterval(refresh, 3000);
</script>
</body>
</html>