stabilize ids and harden release pipeline

This commit is contained in:
2026-11-12 20:42:50 +07:00
parent fd802a8870
commit e9f29dc398
28 changed files with 1199 additions and 281 deletions
+90 -1
View File
@@ -4,7 +4,8 @@ Date: 2026-11-12 · Revision audited: `8fa79aef` (master) · Scope: entire works
> **Remediation status (2026-11-12, later the same day):** all ten P0 items from §8 have been
> implemented and validated (workspace clippy clean, `cargo fmt --check` clean, 35 tests passing).
> See §9 for the per-fix log.
> See §9 for the per-fix log. The P1 list (committed as `fd802a88` base) has also been processed —
> see §10.
## 1. Scope and Method
@@ -905,3 +906,91 @@ Behavior changes to be aware of when testing:
- nip60: reconcile now performs one NUT-08 mint round-trip when unseen relay proofs appear.
- Reactions render per NIP-25 replace semantics (one reaction per author per target).
- Messages keep their line breaks; only URL-adjacent whitespace is tidied.
## 10. P1 Remediation Log (2026-11-12)
All eleven P1 items from §8 fixed on top of the P0 commit (`fd802a88`). Validation after the
full batch: `cargo clippy --workspace --all-targets` zero warnings, `cargo fmt --all --check`
clean, `cargo test --workspace` 38 passed / 0 failed. Diff: 25 files, +1072/−247.
1. **Dock panel dedup made real** (`workspace/src/dock/mod.rs`, `lib.rs`, `skin.rs`):
`PanelKey` — `Named(panel_name)` for singletons, `Room(room_id)` for chat panels — recovered
from docked panels via `PanelHandle::of` + downcasts; `add_panel` now focuses the existing
tab (reopening a collapsed dock first) instead of stacking a duplicate; group panels dedupe
through `Workspace.group_panels: HashMap<GroupKey, PanelId>` (group identity is not readable
off a `GroupPanel`), with stale ids pruned via `area.panel(id)` validation. The dead
`find_panel`/`move_panel`/`InsertTarget` machinery is deleted, along with `skin.rs`'s
write-once `toggle_button_visible`. Known residual: the panel opened by the new-group dialog
(`dialogs/new_group.rs`, still plain `add_panel_to`) isn't registered — P2.
2. **"Ignore" screening action** (`sidebar/rows.rs`, `dock/mod.rs`): closes the request room's
tab via `dock::close_room_panel` (find by room key → `DockArea::remove_panel`) instead of
dispatching `ClosePanel` into a focus path that can never reach its handler.
3. **Updater observation** (`sidebar/mod.rs`, `sidebar/header.rs`, `auto_update/src/lib.rs`):
`AutoUpdater` now exposes `UpdateDisplay` (`up_to_date`, `staged`, `status` text) via
`display()`; the Sidebar mirrors it into a small `Entity<Option<UpdateDisplay>>`, updating it
only when a rendered fact actually changes, and the header renders from that snapshot (the
restart click fetches the global entity at click time). Observing the whole updater and
notifying the entire cached sidebar was rejected as too blunt: download-progress ticks would
have invalidated the panel on every byte.
4. **Wallet invoice flow** (`nip60_ui/wallet.rs`, `nip60/src/lib.rs`): pending mint quotes render
in the panel (amount, mint, bolt11 with middle-ellipsis, copy via the house `Clipboard`
component, check/claim wired to the existing `Job::ClaimQuote` path, per-quote dismiss via the
new `dismiss_mint_quote`); claim results refresh through the existing `WalletEvent::Updated`;
an unpaid check emits "Invoice is not paid yet" (gated on `ClaimQuote` — flagged behavior
change). Receive-via-lightning is functional end-to-end.
5. **Person fetch blacklisting** (`person/src/lib.rs`): `in_flight` is now shared
(`Arc<RwLock<HashSet<_>>>`) with the batching task; all three duplicated `get_metadata` error
arms factored into `flush_batch`, which releases the whole batch for retry on failure and
logs. A 2 s timeout now costs a retry, not a session-long placeholder.
6. **Chat trash surfaced** (`chat/src/lib.rs`): the signal consumer counts *freshly* trashed
messages (BTreeSet insert dedupes reprocess attempts) and emits one batched
`ChatEvent::Error` ("N messages could not be decrypted"), which the workspace already renders
as an error notification.
7. **chat_ui list fixes** (`chat_ui/src/lib.rs`): room connect/load failures now push persistent
error notifications (`notify_failure`, `.autohide(false)`) instead of silently empty rooms;
message inserts splice at the real row (`pos + 1`, welcome row accounted for — append case is
byte-identical to the old behavior); bulk loads are O(n log n) sort-merge with a single splice
and one index rebuild, preserving the old first-arrival-wins timestamp dedup exactly.
8. **Dialog fixes** (`workspace/src/dialogs/{profile,new_chat,screening}.rs`, `panels/backup.rs`):
profile validates avatar/website URLs before publishing (inline error banner; empty still
means "keep saved value"); backup surfaces keyring/decode failures in-panel and its bech32
`unwrap`s are gone; new_chat re-filters on `InputEvent::Change` (and profile's avatar preview
tracks typing); screening no longer closes all stacked dialogs on release, NIP-05 verification
is tri-state (pending/verified/mismatched/failed — network failure no longer asserts "does not
match"), and report submission shows failures.
9. **Parser recursion** (`common/src/parser.rs`): `NostrParserIter::next` is a loop; the
pending-match fields are gone (provably passthrough); 50,000 consecutive malformed `nostr:`
URIs now cost loop iterations, not stack frames (regression tests added) — wasm tab-crash
vector closed.
10. **Stable room ids** (`common/src/event.rs`, `nip29`): `DefaultHasher` replaced by SHA-256
(`common::stable_id` 128-bit hex, `stable_id_u64` for u64-keyed consumers); `Event` and
`UnsignedEvent` `uniq_id` impls factored into one `pubkey_set_uniq_id` helper; nip29's
`GroupKey::uniq_id` and `browse_subscription` unified (verified session-local). **Room id
values change** (new hash domain) — existing cached groupings regroup per session as rumors
re-arrive; release notes should mention it. Follow-up if 128-bit ids are wanted: chat's id
fields are u64-typed; migrating to `String` ids is mechanical.
11. **nip4e lifecycle** (`nip4e/src/lib.rs`): approval has an `approving` re-entry guard (the
device secret can no longer be double-sent), toast lifecycle is correct on both outcomes
(success clears only the matching request; failure rebuilds the toast with a fresh enabled
button); rotation aborts before overwrite when load/archive fails (surfaces via
`DeviceEvent::error`); the relay listener logs dropped events and its own death instead of
stopping silently.
12. **Proxy hardening round 2** (`browser-signer-proxy/src/lib.rs`): signed events are checked
for correspondence (`event.compute_id()` vs returned id — proves field equality, covers
stale ids), closing the event-substitution hole; the response log carries id + ok/error only,
never values; request lines/headers are capped (16 KiB → 400), `Content-Length` capped
(1 MiB → 413), invalid UTF-8 → 400, and connections are dropped after 30 s (slowloris
bound).
### P1 behavior changes to be aware of when testing
- Opening an already-open room/group/panel focuses the existing tab; the "Ignore" action closes
the request room's tab.
- Wallet panel shows a "Pending invoices" section; unpaid checks report "Invoice is not paid
yet"; quotes are dismissable.
- Room groupings regenerate under a new id scheme — first launch after this change rebuilds
rooms from cached rumors (history is kept; grouping self-heals).
- Profile saves with an invalid URL now stop with an inline error instead of silently dropping
the field; NIP-05 shows "could not be verified" on network failure instead of "does not
match"; undecryptable messages and room load/connect failures produce visible notifications.
- First message-send/receive after the room-id change republishes `r` tags under the new ids.