fix: deduplicate online user count by username

Same user with multiple tabs/connections (different peer IDs) was
counted multiple times. Now deduplicates by username, keeping the
most recently seen entry per user for badge count and panel display.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-03-30 23:27:35 -07:00
parent 55771eb26e
commit 7fec0cb699
1 changed files with 24 additions and 9 deletions

View File

@ -485,6 +485,19 @@ export class RStackCollabOverlay extends HTMLElement {
}
}
/** Deduplicate peers by username, keeping the most recently seen entry per user. */
#uniquePeers(): PeerState[] {
const byName = new Map<string, PeerState>();
for (const peer of this.#peers.values()) {
const key = peer.username.toLowerCase();
const existing = byName.get(key);
if (!existing || peer.lastSeen > existing.lastSeen) {
byName.set(key, peer);
}
}
return Array.from(byName.values());
}
// ── Color assignment ──
#colorForPeer(peerId: string): string {
@ -536,7 +549,8 @@ export class RStackCollabOverlay extends HTMLElement {
const countEl = this.#shadow.getElementById('panel-count');
if (!list) return;
const onlineCount = this.#peers.size + 1;
const uniquePeers = this.#uniquePeers();
const onlineCount = uniquePeers.length + 1;
if (countEl) countEl.textContent = this.#connState === 'connected' ? `${onlineCount} online` : '\u2014';
const fragments: string[] = [];
@ -566,9 +580,9 @@ export class RStackCollabOverlay extends HTMLElement {
</div>
`);
// Remote peer rows
// Remote peer rows (deduplicated by username)
const isCanvas = this.#moduleId === 'rspace';
for (const [pid, peer] of this.#peers) {
for (const peer of uniquePeers) {
const ctxParts: string[] = [];
if (peer.module) ctxParts.push(peer.module);
if (peer.context) ctxParts.push(peer.context);
@ -580,15 +594,15 @@ export class RStackCollabOverlay extends HTMLElement {
<span class="name">${this.#escHtml(peer.username)}</span>
${ctxStr ? `<span class="peer-context">${this.#escHtml(ctxStr)}</span>` : ''}
</div>
${isCanvas ? `<button class="actions-btn" data-pid="${this.#escHtml(pid)}">&gt;</button>` : ''}
${isCanvas ? `<button class="actions-btn" data-pid="${this.#escHtml(peer.peerId)}">&gt;</button>` : ''}
</div>
`);
// Expanded actions for canvas
if (isCanvas && this.#openActionsId === pid) {
if (isCanvas && this.#openActionsId === peer.peerId) {
fragments.push(`
<div class="people-actions">
<button data-action="navigate" data-pid="${this.#escHtml(pid)}">Navigate to</button>
<button data-action="ping" data-pid="${this.#escHtml(pid)}">Ping to join you</button>
<button data-action="navigate" data-pid="${this.#escHtml(peer.peerId)}">Navigate to</button>
<button data-action="ping" data-pid="${this.#escHtml(peer.peerId)}">Ping to join you</button>
</div>
`);
}
@ -707,9 +721,10 @@ export class RStackCollabOverlay extends HTMLElement {
return;
}
const count = this.#peers.size + 1; // +1 for self
const uniquePeers = this.#uniquePeers();
const count = uniquePeers.length + 1; // +1 for self
const dots = Array.from(this.#peers.values())
const dots = uniquePeers
.slice(0, 5) // show max 5 dots
.map(p => `<span class="dot" style="background:${p.color}"></span>`)
.join('');