Surface account storage preparation and background history progress through the existing live connection status channel. Clearly distinguish unavailable accounts from accounts that remain fully usable during maintenance.
Show processed totals, completion percentage, estimated remaining time, and retry states in both the global notice and account overview. Preserve existing connection update filtering to avoid unnecessary refreshes.
Adversarial review caught: HttpPostAdapter.describeStatus() embeds
HH:MM:SS in 'detail' (e.g. "上次推送 14:53:01"). Every webhook delivery
rotates that string every second under any active push — and the diff
loop's raw JSON.stringify compared everything, so it published on every
500ms tick under any non-idle webhook. Net effect: SSE pushed a full
connections frame at 2 Hz, exactly the polling the loop was meant to
replace, just on the push channel.
Add an optional pickComparable projector. server.ts caller passes one
that returns { uin, nickname, adapters[{ name, kind, status }] } — the
LEVEL only, not the live-rendered detail string. The SSE handler still
ships the FULL snapshot (including detail) to the WebUI; only the diff
key is pruned.
2 new tests pin the invariant: 10 detail-only ticks → ZERO publishes; a
real status-level transition publishes exactly once. Plus a throwing-
projector isolation test. 7/7 diff-loop tests green.
Phase 4b. OneBot adapters expose no event-emitter for connection-state edges
(listening / connected / client-count / last-delivery), so the SSE handler
can't subscribe — without this loop a connection change would only reach the
WebUI on the 30s REST reconcile fallback.
startConnectionDiffLoop polls getConnectionStatuses() at 500ms, compares
JSON-serialised snapshots, and publishes 'connections' through the StateBus
when (and only when) it actually moves. The first SUCCESSFUL observation is
treated as a baseline (no publish): the SSE handler's sendAllInitial has
already shipped the initial snapshot to every connected client on connect.
Snapshot exceptions are isolated — the loop retries on the next tick without
clobbering the baseline.
Started once per initWebUI lifetime; lives until process exit (consistent
with the other heartbeat-style intervals like the SSE 15s keepalives).
431/431 core suite green (5 new tests cover stable=silent, change=publish,
first-observation=baseline, dispose stops, snapshot-throw isolation).