fix: review round 3 — a new tab keeps its account's notices, a revoked tab account lands on the default

Deep-review round 3 over main..improvement/watch-background-audio.

Account boot
- The notification history and settings, and the hints toggle, are read
  when their modules load, which in a new tab (bookmark, typed URL) is
  before BootGate pins the account: they read as nobody's, and the tab's
  first notice replaced the account's stored history (up to 100 entries)
  with a one-item list. They now read again when the tab is pinned
  (onAccountPinned, run by setActiveAccount); a notice raised before the
  pin stays, after the history.
- A tab pinned to an account still in this browser's wallet but whose
  session ended (suspended, deleted, or revoked by "log out everywhere")
  got "signed out" from /api/me while the default account was still
  signed in, and signing in again as anyone else left it there. A refused
  per-tab account now leaves the wallet, and the probe answers as the
  default, so the tab reloads onto it.

Share page (/watch)
- The password unlock is limited per viewer (link + client IP), under a
  per-link ceiling that counts only wrong passwords. Keyed on the link
  alone, ten wrong guesses every five minutes locked out every viewer,
  the ones with the password included. client_ip moves to app.ratelimit
  so the public routes can key on it.
- Its failure messages and the load-failure screen are announced
  (role=alert, the password input described by its error). The sign-in,
  passphrase, password-change and setup form errors get role=alert too.

Comments that still described removed machinery are corrected: App's
collapse flags, SettingsPanel's Save bar, ConfigPanel, authed_format_sig.

Tests: the account's history and hints across the pin; a revoked and a
suspended pinned account; the per-viewer limit and the ceiling; the
limiter's exhausted(); the share page's announced failures; a share
link's feed filters persisted. Each mutation-checked RED for its own
reason.
This commit is contained in:
2026-09-15 02:25:00 +02:00
parent f569b19c49
commit e7f38396a2
26 changed files with 366 additions and 93 deletions
+17
View File
@@ -2,6 +2,7 @@
// the Notification Center shows (info events, actions awaiting interaction, and app
// errors). History is persisted to localStorage so it survives reloads; action
// callbacks are live-only and dropped on reload.
import { activeAccountId, onAccountPinned } from "./activeAccount";
import { LS, readAccount, readAccountMerged, writeAccount } from "./storage";
export type NotifLevel = "info" | "success" | "warning" | "error" | "fatal";
@@ -205,6 +206,22 @@ function emit() {
recompute();
// The history and settings above are read at import, which in a new tab is before BootGate pins its
// account — so they read as nobody's. Read them again for the account once it is pinned: otherwise the
// tab's first write would replace the account's stored history with its own empty one. A notice raised
// before the pin (a failed boot probe) belongs to no account, so it stays, after the history.
let loadedFor = activeAccountId();
onAccountPinned(() => {
const unpinned = loadedFor == null ? items : [];
loadedFor = activeAccountId();
config = loadSettings();
// Fresh ids for the history: the notices kept may already hold the low ones (a timer, a producer).
const history = load().map((n) => ({ ...n, id: counter++ }));
items = [...history, ...unpinned].slice(-MAX_HISTORY);
recompute();
listeners.forEach((l) => l());
});
export function subscribe(listener: () => void): () => void {
listeners.push(listener);
return () => {