fix: separate bot vs viewer WebSocket connections, add client identification

The dashboard's own WS connection was being counted as a bot subscriber,
causing "1 bot connected" with no bots actually present. Now WS clients
send a role ("bot" or "viewer") in the subscribe message. Only bots count
toward the subscriber total. Bot plugins also send a configurable client_id
so the dashboard shows which specific bots are connected.

Made-with: Cursor
This commit is contained in:
cottongin
2026-03-12 07:51:55 -04:00
parent 658c0d4a15
commit d6d5ac10e6
8 changed files with 184 additions and 35 deletions

View File

@@ -33,6 +33,11 @@
.toast.error { background: #e53935; }
details summary h3 { display: inline; cursor: pointer; }
.track-num { width: 40px; text-align: center; }
.client-tag {
display: inline-block; padding: 2px 8px; margin: 2px 4px;
border-radius: 4px; background: #2a2a2a; font-size: 0.8rem;
}
#client-detail { display: none; margin-top: 4px; }
</style>
</head>
<body>
@@ -42,6 +47,7 @@
<span class="status-dot disconnected" id="status-dot"></span>
<strong>NtR Playlist Dashboard</strong>
<small id="sub-count">(connecting...)</small>
<div id="client-detail"></div>
</div>
<a href="/logout" role="button" class="outline secondary">Logout</a>
</nav>
@@ -161,16 +167,28 @@
}
}
function updateStatus(count) {
function updateStatus(count, clients) {
subscriberCount = count;
const dot = document.getElementById("status-dot");
const sub = document.getElementById("sub-count");
const detail = document.getElementById("client-detail");
if (count > 0) {
dot.className = "status-dot connected";
sub.textContent = `(${count} bot${count > 1 ? "s" : ""} connected)`;
if (clients && clients.length > 0) {
detail.innerHTML = clients.map(c => {
const name = esc(c.client_id || "unknown");
const addr = esc(c.remote_addr || "?");
return `<span class="client-tag">${name} (${addr})</span>`;
}).join(" ");
detail.style.display = "block";
} else {
detail.style.display = "none";
}
} else {
dot.className = "status-dot disconnected";
sub.textContent = "(no bots connected)";
detail.style.display = "none";
}
document.querySelectorAll(".announce-btn").forEach(btn => {
if (count === 0) {
@@ -188,19 +206,19 @@
const proto = location.protocol === "https:" ? "wss:" : "ws:";
const ws = new WebSocket(`${proto}//${location.host}/ws/announce`);
ws.onopen = () => {
ws.send(JSON.stringify({type: "subscribe", token: WS_TOKEN}));
ws.send(JSON.stringify({type: "subscribe", token: WS_TOKEN, role: "viewer"}));
wsBackoff = 1000;
};
ws.onmessage = (e) => {
try {
const data = JSON.parse(e.data);
if (data.type === "status") {
updateStatus(data.subscribers);
updateStatus(data.subscribers, data.clients || []);
}
} catch {}
};
ws.onclose = () => {
updateStatus(0);
updateStatus(0, []);
document.getElementById("sub-count").textContent = "(reconnecting...)";
setTimeout(connectWS, wsBackoff);
wsBackoff = Math.min(wsBackoff * 2, 60000);