Skip to content

Commit dcbe264

Browse files
MaxGhenisclaude
andauthored
Web UI: skip status re-render when nothing the UI shows changed (#41)
The backend pushes a status SSE event on every platform state change, and the client re-ran setConnectionState + renderEmptyState + chat-header chrome on each one. A flapping bridge — e.g. a signal-cli crash loop that tears down and reconnects every few seconds — turned that into a constant banner/header rebuild that visibly flickered the whole app (reported live: "flickering every few seconds, unusable", traced to signal-cli 0.14.1 NPE-ing on an unparseable envelope and the bridge flapping connected). applyAppStatus now renders only when a signature of the *visible* state changes. The signature keys secondary platforms off whether they're PAIRED (what the banner/empty-state actually use), not their live connected flag, so a Signal/WhatsApp connect↔disconnect flap with the Platforms dialog closed is a no-op. Live per-platform detail (connect progress, QR) is folded in only while that dialog is open, and freshness timestamps are excluded so message arrival never triggers a chrome rebuild. Genuine changes — Google connect/disconnect, pairing progress, a new QR — still render. Verified with a new e2e test: 6× Signal flap leaves the banner byte-identical while a Google disconnect still updates it. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 435d407 commit dcbe264

2 files changed

Lines changed: 61 additions & 0 deletions

File tree

e2e/ui.spec.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1089,3 +1089,34 @@ test('escape closes the platforms overlay without clearing search', async ({ pag
10891089
await expect(page.locator('#wa-overlay')).not.toHaveClass(/show/);
10901090
await expect(page.locator('#search-input')).toHaveValue('hike');
10911091
});
1092+
1093+
test('status flap from a flapping bridge does not re-render the main UI', async ({ page }) => {
1094+
await page.waitForFunction(() => window.__openMessageTestHooks?.applyAppStatus);
1095+
const r = await page.evaluate(() => {
1096+
const H = window.__openMessageTestHooks;
1097+
const banner = () => {
1098+
const b = document.getElementById('connection-banner');
1099+
const copy = document.getElementById('connection-banner-copy');
1100+
return JSON.stringify({ disp: b ? getComputedStyle(b).display : null, text: copy ? copy.textContent : null });
1101+
};
1102+
const base = {
1103+
connected: true,
1104+
google: { connected: true, paired: true, needs_pairing: false },
1105+
whatsapp: { connected: true, paired: true },
1106+
signal: { connected: true, paired: true },
1107+
backfill: { running: false },
1108+
};
1109+
const flap = JSON.parse(JSON.stringify(base)); flap.signal.connected = false; // bridge crash; paired stays
1110+
const gDown = JSON.parse(JSON.stringify(base)); gDown.google.connected = false; gDown.connected = false;
1111+
1112+
H.applyAppStatus(base);
1113+
const sig0 = H.lastStatusRenderSig(); const banner0 = banner();
1114+
for (let i = 0; i < 6; i++) H.applyAppStatus(i % 2 ? flap : base);
1115+
const flapStable = sig0 === H.lastStatusRenderSig() && banner0 === banner();
1116+
H.applyAppStatus(gDown);
1117+
const realChanged = sig0 !== H.lastStatusRenderSig() && banner0 !== banner();
1118+
return { flapStable, realChanged };
1119+
});
1120+
expect(r.flapStable).toBe(true); // Signal flapping must not churn the banner
1121+
expect(r.realChanged).toBe(true); // a genuine Google disconnect still renders
1122+
});

internal/web/static/index.html

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4582,6 +4582,12 @@ <h2 id="empty-state-title">No conversations yet</h2>
45824582
contacts_checked: 0,
45834583
errors: 0,
45844584
};
4585+
// Signature of the last status that actually drove a render. The backend
4586+
// pushes a status SSE event on every platform state change, and a flapping
4587+
// bridge (or the 10s fallback poll) can fire many identical ones; rendering
4588+
// each rebuilds the connection banner + header chrome and visibly flickers.
4589+
// applyAppStatus skips the render pass when this signature is unchanged.
4590+
let lastStatusRenderSig = null;
45854591
let whatsAppPollTimer = null;
45864592
const knownMessageIDs = new Set();
45874593
const knownMessageOrder = [];
@@ -8340,6 +8346,24 @@ <h2 id="empty-state-title">No conversations yet</h2>
83408346
whatsAppStatus = normalizeWhatsAppStatus(appStatus.whatsapp || {});
83418347
signalStatus = normalizeSignalStatus(appStatus.signal || {});
83428348
backfillStatus = normalizeBackfillStatus(appStatus.backfill || {});
8349+
// Only re-render when something the always-on UI actually shows changed.
8350+
// The banner and empty state key off whether secondary platforms are
8351+
// PAIRED, not their live connected flag — so a Signal/WhatsApp bridge
8352+
// flapping connected↔disconnected (e.g. a signal-cli crash loop) must not
8353+
// churn the main UI. Live per-platform detail (connect progress, QR) only
8354+
// matters while the Platforms dialog is open, so it's folded in only then.
8355+
// Freshness timestamps are excluded entirely (they tick on every message).
8356+
const overlayOpen = !!($waOverlay && $waOverlay.classList.contains('show'));
8357+
const hasSecondaryPaired = whatsAppStatus.paired || whatsAppStatus.connected
8358+
|| signalStatus.paired || signalStatus.connected;
8359+
const sig = JSON.stringify([
8360+
googleStatus, hasSecondaryPaired,
8361+
backfillStatus.running, allConversations.length,
8362+
browserReportsOnline(), activeConvoId, overlayOpen,
8363+
overlayOpen ? [whatsAppStatus, signalStatus] : null,
8364+
]);
8365+
if (sig === lastStatusRenderSig) return;
8366+
lastStatusRenderSig = sig;
83438367
setConnectionState();
83448368
renderWhatsAppButton();
83458369
renderEmptyState();
@@ -9773,6 +9797,12 @@ <h2 id="empty-state-title">No conversations yet</h2>
97739797
windowSize: THREAD_RENDER_WINDOW_SIZE,
97749798
};
97759799
},
9800+
applyAppStatus(status = {}) {
9801+
applyAppStatus(status || {});
9802+
},
9803+
lastStatusRenderSig() {
9804+
return lastStatusRenderSig;
9805+
},
97769806
};
97779807
}
97789808

0 commit comments

Comments
 (0)