fix: the diagnostics tab keeps its focus, its level and its consent

Deep-review round 1 of E22.

- After a 429 the stream sent one POST per new event while a full batch
  waited; it now stays quiet for 15 s. Past the queue cap the oldest
  events go, as the comment always said, a refused batch included.
- A batch refused after the level was lowered came back into the queue
  of a later consent in the same tab; each consent is its own session.
- A `me` fetch that left before a level or preference change could put the
  old value back for up to a minute. `updateMeCache` writes the cache and
  cancels that fetch; usePref goes through it too.
- The consent box's buttons and "revert now" took the focus with them to
  <body>; it returns to the level picker. Watched until the button is
  gone — in Chrome the saved level arrives in two renders, and a one-shot
  check spent itself on the first (jsdom never showed it).
- The countdown line is no longer a live region re-read every minute.
- "Saját napló" rows were keyed by index on a newest-first list, so every
  new event remounted every row and dropped the focus off a row button.
This commit is contained in:
2026-09-17 03:53:40 +02:00
parent 4bfba4aa86
commit f92cd9017f
8 changed files with 220 additions and 30 deletions
+56 -2
View File
@@ -130,12 +130,66 @@ describe("diagnostics level", () => {
d.log.debug("ui.click", "kept for later");
await vi.advanceTimersByTimeAsync(5_000);
answer = () => Promise.reject(new d.HttpError(409, "off"));
await vi.advanceTimersByTimeAsync(5_000);
expect(codes(sent[1])).toEqual(codes(sent[0])); // the same batch, retried after the 429
await vi.advanceTimersByTimeAsync(15_000);
expect(codes(sent[1])).toEqual(codes(sent[0])); // the same batch, retried after the back-off
expect(lapsed).toHaveBeenCalledOnce();
expect(d.enabled("DEBUG")).toBe(false);
});
it("after a 429 nothing is sent until the back-off is over, however full the queue", async () => {
const d = await fresh();
d.applyDiagnostics(raised("DEBUG"), () => {});
answer = () => Promise.reject(new d.HttpError(429, "slow down"));
for (let i = 0; i < 99; i++) d.log.debug("ui.click", `click ${i}`);
await vi.advanceTimersByTimeAsync(0);
expect(sent).toHaveLength(1); // the full batch went at once, and was refused
answer = () => Promise.resolve({ ok: true, stored: 0 });
for (let i = 0; i < 50; i++) d.log.debug("ui.click", `more ${i}`);
await vi.advanceTimersByTimeAsync(14_000);
expect(sent).toHaveLength(1); // one POST per new event is exactly the storm to avoid
await vi.advanceTimersByTimeAsync(1_000);
expect(codes(sent[1])).toEqual(codes(sent[0]));
});
it("past the queue cap the oldest events go, a refused batch included", async () => {
const d = await fresh();
d.applyDiagnostics(raised("DEBUG"), () => {});
answer = () => Promise.reject(new d.HttpError(429, "slow down"));
for (let i = 0; i < 99; i++) d.log.debug("ui.click", `click ${i}`);
await vi.advanceTimersByTimeAsync(0);
answer = () => Promise.resolve({ ok: true, stored: 0 });
for (let i = 99; i < 1_100; i++) d.log.debug("ui.click", `click ${i}`);
await vi.advanceTimersByTimeAsync(60_000);
const msgs = sent
.slice(1)
.flat()
.map((e) => (e as { msg: string }).msg);
expect(msgs).toHaveLength(1_000);
expect(msgs[0]).toBe("click 100");
expect(msgs[msgs.length - 1]).toBe("click 1099");
});
it("a batch refused after the level was lowered is not sent under a later consent", async () => {
const d = await fresh();
let refuse: (err: unknown) => void = () => {};
answer = () => new Promise((_, reject) => (refuse = reject));
d.applyDiagnostics(raised("DEBUG"), () => {});
d.log.debug("ui.click", "under the first consent");
await vi.advanceTimersByTimeAsync(5_000); // in flight
d.applyDiagnostics({ level: "INFO", until: null, retention_days: 7 }, () => {});
refuse(new d.HttpError(429, "slow down"));
await vi.advanceTimersByTimeAsync(0);
answer = () => Promise.resolve({ ok: true, stored: 0 });
d.applyDiagnostics(raised("TRACE"), () => {});
await vi.advanceTimersByTimeAsync(60_000);
const later = sent
.slice(1)
.flat()
.map((e) => (e as { msg: string }).msg);
expect(later).not.toContain("under the first consent");
expect(later).toContain("diagnostics level raised");
});
it("a closing tab sends what is still waiting with a keepalive request", async () => {
const d = await fresh();
d.applyDiagnostics(raised("DEBUG"), () => {});