openvibers/openvibe.network Commits

Admin: a Lineage tab, the operator view of unresolved references (roadmap D20, remaining item 2). It lists what Live's canonical channel/owner resolver answered unresolved (conflict, source_unavailable, not_found, display_name_only), by reference and reason, with detail, last caller, count, and first/last seen, filterable by reason. It reads Live's GET /api/admin/lineage/unresolved (4479b66) through the existing admin-only /api/admin/streamer proxy.

OP OpenVibers committed 26 Sept 2026 · 1117f253f6 · 1 file, +38 −0


Changes

ddiff --git a/public/admin.html b/public/admin.html
iindex 6a05e229..93d98a8b 100644
---- a/public/admin.html
++++ b/public/admin.html
@@@ -197,6 +197,7 @@ body{min-height:100%;color:var(--text);font-family:'Inter',-apple-system,BlinkMa
<button class="admin-tab-btn active" onclick="showTab('dashboard',this)"><i class="fa-solid fa-chart-line"></i> Dashboard</button>
<button class="admin-tab-btn" onclick="showTab('users',this)"><i class="fa-solid fa-users"></i> Users</button>
<button class="admin-tab-btn" onclick="showTab('streams',this)"><i class="fa-solid fa-broadcast-tower"></i> Streams</button>
++ <button class="admin-tab-btn" onclick="showTab('lineage',this)"><i class="fa-solid fa-diagram-project"></i> Lineage</button>
<button class="admin-tab-btn" onclick="showTab('bans',this)"><i class="fa-solid fa-ban"></i> Bans</button>
<button class="admin-tab-btn" onclick="showTab('chat-logs',this)"><i class="fa-solid fa-message"></i> Chat Logs</button>
<button class="admin-tab-btn" onclick="showTab('moderators',this)"><i class="fa-solid fa-shield-halved"></i> Moderators</button>
@@@ -252,6 +253,13 @@ body{min-height:100%;color:var(--text);font-family:'Inter',-apple-system,BlinkMa
<div id="streams-content"><div class="loading">Loading...</div></div>
</div>
++ <!-- Lineage: references the canonical resolver could not resolve (Live, roadmap D20) -->
++ <div class="section" id="sec-lineage">
++ <p class="muted">References Live's canonical channel/owner resolver answered <em>unresolved</em>, from other services and Live's own checks, counted by inputs and reason over the last 30 days. A display name never resolves anything.</p>
++ <div class="sub-tabs" id="lineage-reasons"></div>
++ <div id="lineage-content"><div class="loading">Loading...</div></div>
++ </div>
++
<!-- Bans -->
<div class="section" id="sec-bans">
<div id="bans-content"><div class="loading">Loading...</div></div>
@@@ -853,6 +861,7 @@ const tabLoaders = {
dashboard: loadDashboard,
users: loadUsers,
streams: loadStreams,
++ lineage: () => loadLineage(),
bans: loadBans,
'chat-logs': () => {}, // manual search
moderators: loadModerators,
@@@ -1285,6 +1294,35 @@ async function loadStreams() {
} catch (e) { c.innerHTML = `<p class="muted">Error: ${esc(e.message)}</p>`; }
}
++// Unresolved lineage references (Live GET /api/admin/lineage/unresolved through the /api/admin/streamer proxy).
++async function loadLineage(reason = '') {
++ const c = document.getElementById('lineage-content');
++ const tabs = document.getElementById('lineage-reasons');
++ c.innerHTML = '<div class="loading">Loading...</div>';
++ try {
++ const data = await api(`/api/admin/streamer/lineage/unresolved${reason ? `?reason=${encodeURIComponent(reason)}` : ''}`);
++ const by = data.by_reason || {};
++ tabs.innerHTML = [['', 'All']].concat(Object.keys(by).sort().map(r => [r, `${r.replace(/_/g, ' ')} (${by[r].refs})`]))
++ .map(([r, label]) => `<button class="${r === reason ? 'active' : ''}" onclick="loadLineage('${esc(r)}')">${esc(label)}</button>`).join('');
++ const rows = data.unresolved || [];
++ if (!rows.length) { c.innerHTML = '<p class="muted">Nothing unresolved in the last 30 days.</p>'; return; }
++ c.innerHTML = `
++ <div style="overflow-x:auto"><table class="admin-table">
++ <thead><tr><th>Reference</th><th>Reason</th><th>Detail</th><th>Last caller</th><th>Times</th><th>First seen</th><th>Last seen</th></tr></thead>
++ <tbody>${rows.map(u => `
++ <tr>
++ <td><code>${esc(u.ref)}</code></td>
++ <td><span class="badge badge-warning">${esc(u.reason)}</span></td>
++ <td>${esc(u.detail || '')}</td>
++ <td>${esc(u.last_caller || '-')}</td>
++ <td>${Number(u.count) || 0}</td>
++ <td>${u.first_at ? timeAgo(u.first_at) : '-'}</td>
++ <td>${u.last_at ? timeAgo(u.last_at) : '-'}</td>
++ </tr>`).join('')}</tbody>
++ </table></div>`;
++ } catch (e) { c.innerHTML = `<p class="muted">Error: ${esc(e.message)}</p>`; }
++}
++
async function endStream(id) {
if (!confirm('Force end this stream?')) return;
try {