Reactions, replies, read-receipts, edits, deletes, forwards and channel sends
shared one bare `{ ok: true, sent: true }` case, so none of them rendered on
the demo — the UI derives reaction chips and reply quotes from the message
store, and there was nothing in it to derive from.
Each now mirrors its daemon counterpart. Reactions/replies/receipts push typed
messages carrying the { sender_pubkey, sender_seq } target key Mesh.vue's
reactionIndex and replyTargetPreview read. Edits rewrite the text and set
edited_at; deletes tombstone IN PLACE (plaintext, typed_payload.deleted,
message_type 'delete') because that is what mesh/mod.rs apply_local_delete
does — it does not remove the row.
Edits and deletes go through a per-session overrides overlay keyed by
sender_seq, because mesh.messages rebuilds its seed array on every read, so
in-place mutation would only ever work for messages sent this session.
mesh.refresh and mesh.reboot-radio stay acknowledgements on purpose — the
daemon's handlers have no message-store effect either — with a comment saying
so, so a later reader does not "fix" them into divergence.
Also completes the phase bookkeeping for 01-02/03/11/12/13/14/15 and lands the
orphaned 01-12/01-14 SUMMARYs.
Verified: parity harness 17/17 live assertions; full frontend suite 102 files
/ 822 tests green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
16 KiB
phase, plan, subsystem, tags, requires, provides, affects, tech-stack, key-files, key-decisions, patterns-established, requirements-completed, coverage, duration, completed, status
| phase | plan | subsystem | tags | requires | provides | affects | tech-stack | key-files | key-decisions | patterns-established | requirements-completed | coverage | duration | completed | status | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 01-federation-mesh-hardening | 14 | ui |
|
|
|
|
|
|
|
|
|
|
30min | 2026-07-31 | complete |
Phase 01 Plan 14: Paid Files lightbox + loading-state audit Summary
Purchased pictures/videos now open in the app's own MediaLightbox with a visible "Opening…" row spinner and real error surfacing, replacing window.open() + a swallowed catch; the eight other surfaces phase 2 flagged as slow are confirmed cached-revisit render-cost regressions, not missing loaders, so none of them were touched.
Performance
- Duration: ~30 min
- Started: 2026-07-31T22:00Z (approx, from first read)
- Completed: 2026-07-31T22:30Z
- Tasks: 2 (Task 1 tracer + Task 2 audit)
- Files modified: 3
Accomplishments
- New
usePaidItemViewer()composable: movesCloud.vue's inlinecontent.owned-getfetch/decode chain into a reusable, unit-tested unit with per-mime routing (image/video → lightbox, audio → bottom-bar player, everything else → today's browser-tab fallback), a single-flightopen()guard keyed ononion+content_id, and aresolveBlobUrlthe lightbox calls to read the already-fetched URL. Cloud.vue's Paid Files row:window.open()is gone entirely (grep -c 'window.open'= 0); the row now shows a spinner + "Opening…" label (reusingPeerFiles.vue's existing treatment verbatim) for the fetch's duration and becomes non-interactive so a second click can't queue a second fetch.- A second
MediaLightboxinstance added below the existing My Files one, fed the composable'slightboxItems/lightboxIndexandresolveBlobUrlfor bothfetch-blob-urlandstream-url. - The composable's
catchsets a real, user-facingerrormessage that Cloud.vue surfaces through its existingloadError/alert-errorblock — no more silent failure. - Task 2 audit: every one of the eight surfaces phase 2's findings named (Discover, Server, Web5, Fleet, AppDetails, OpenWrtGateway, MarketplaceAppDetails, Wallet-send) confirmed as a cached-revisit client-side render-cost regression with an existing first-load/first-fetch affordance already in place — no new loader added anywhere.
Task Commits
- Task 1: End-to-end — a purchased picture opens in the lightbox, with the wait visible -
bc9a210c(fix)- Follow-up type-check fix (vue-tsc caught two possibly-undefined array reads
npx vitest runalone doesn't flag):4a8925f0(fix)
- Follow-up type-check fix (vue-tsc caught two possibly-undefined array reads
- Task 2: Settle the slow-open inventory — verdict per surface, loader only where genuinely missing - no code changes; audit-only, documented below (the pinning test required by this task's acceptance criteria was already written as part of Task 1's test file, see "opening is not derived from a background-refresh flag" test)
Plan metadata: (this commit)
Files Created/Modified
neode-ui/src/composables/usePaidItemViewer.ts- fetch/decode/route/loading/error composable for purchased itemsneode-ui/src/composables/__tests__/usePaidItemViewer.test.ts- 9 tests: per-mime routing (image/video/audio/no-viewer), extension-carrying synthetic item, loading-state, the not-derived-from-refresh pin, error surfacing, double-click dedupneode-ui/src/views/Cloud.vue-viewPaidItem()now delegates to the composable; row loading/error UI; secondMediaLightboxinstance
Per-Surface Slow-Open Verdict (Task 2)
Audited against .planning/phases/02-ui-performance/02-FINDINGS.md's "Per-surface verdict" and "Accepted deviations" sections (the authoritative measured evidence) plus a direct grep of each surface's own loading-state code, per the plan's own instruction to re-verify rather than assume a grep hit covers the first uncached open.
| Surface | File | Existing affordance (first/genuinely-slow open) | Genuinely slow first open? | Action taken |
|---|---|---|---|---|
| Discover | neode-ui/src/views/Discover.vue |
catalogResource.entry.loadState === 'loading' drives a loading message (loadingCommunity, line 303) |
No — 02-FINDINGS: 0 RPC confound-free reading, regression is Apps-tab-KeepAlive-transit render/reactivation cost on revisit, not the first uncached open | None — first-load already covered; a revisit regression is out of scope for a loader (would violate PERF-02) |
| Server | neode-ui/src/views/Server.vue |
Multiple animate-spin loading indicators per-section (e.g. line 91, 250) plus keyed useCachedResource per load group (02-06) |
No — 02-FINDINGS/02-09: component instance confirmed to genuinely survive the round-trip (vm.$.uid proof); regression is 7 cached resources' reactivation cost on revisit, RPC count improved (8→2) |
None — same reasoning; revisit-only cost |
| Web5 | neode-ui/src/views/web5/Web5.vue |
loadingDidDoc text indicator (line 44) plus each sub-card's own useCachedResource |
No — 02-FINDINGS: strongest zero-overlap evidence of pure client-side reactivation cost on a confirmed-surviving instance, 0 RPC throughout, all three measurement runs | None — same reasoning; revisit-only cost |
| Fleet | neode-ui/src/views/Fleet.vue |
animate-spin loading state (lines 64, 93) |
No — 02-FINDINGS: instance survives (confirmed), 0 RPC either time, the most severe of the split-signal regressions (330ms→2631ms) but purely a revisit/reactivation cost | None — same reasoning; revisit-only cost |
| AppDetails | neode-ui/src/views/AppDetails.vue |
credentialsLoading computed from credentialsResource.loadState.value === 'loading', passed to its credentials section (line 44/206) |
No — 02-FINDINGS: RPC count improved (2→1) via keyed useCachedResource (02-03); the keyed-lookup/re-render cost now exceeds what the eliminated fetch saved, on revisit |
None — same reasoning; revisit-only cost |
| OpenWrtGateway | neode-ui/src/views/server/OpenWrtGateway.vue |
Explicit "Loading skeleton" block (line 522) with a documented first-load-only condition (line 102-104 comment) | No — 02-FINDINGS: same split-signal class as AppDetails; RPC evidence confounded by Server-tab transit but revisitMs alone shows the regression is on the cached path, and the skeleton is already gated to first-load-only |
None — same reasoning; revisit-only cost, and the surface deliberately avoids showing a skeleton on revisit already (matches PERF-02) |
| MarketplaceAppDetails | neode-ui/src/views/MarketplaceAppDetails.vue |
v-if="loading" full-card skeleton with animate-spin (line 7-8) |
No — 02-FINDINGS: RPC evidence confounded by Home-tab transit; only package.versions is a trustworthy call and it improved; regression (if any residual) reads as revisit render cost, and the raw remounted: false reading is flagged as likely the same selector-ambiguity artifact 02-09 proved, not new instance caching |
None — first-load skeleton already present and correctly gated |
Wallet / send flow (SendBitcoinModal.vue) |
neode-ui/src/components/SendBitcoinModal.vue |
feeEstimateLoading ref renders '…' in place of the fee figure while the fee-estimate call is in flight (lines 388-419) |
No — 02-FINDINGS: cleared as environmental noise in the re-measurement (median dropped below both prior runs); the separate, still-open anomaly (revisit consistently slower than first-visit, 0 RPC) is because BaseModal's v-if always fully remounts the modal — not a missing loader, and out of this plan's files_modified (SendBitcoinModal.vue isn't in this plan's scope; carried forward in 02-FINDINGS' Outstanding section) |
None — the modal's own async op (fee estimate) already has a first-load affordance; the remount-cost anomaly is a pre-existing, separately-tracked issue this plan doesn't own |
No affordance was added to any cached-revisit path in this plan. PERF-02's no-spinner-on-revisit guarantee is preserved everywhere — the only genuinely slow, uncached, first-open gap that existed (Cloud.vue's viewPaidItem() / content.owned-get, 60s timeout, zero indicator) is the one fixed in Task 1; every other named surface's slow-open regression is client-side render/reactivation cost on an already-cached revisit, which a loader cannot fix and must not paper over.
Decisions Made
- Documents (no in-app viewer) deliberately keep the browser-tab fallback — a scope boundary named explicitly in the plan, not a gap.
- No new loader was added anywhere in Task 2 — see the per-surface verdict table above; every named surface's regression is revisit-only render cost, confirmed by phase 2's own measured evidence (
02-FINDINGS.md), not a missing first-load affordance.
Deviations from Plan
Auto-fixed Issues
1. [Rule 3 - Blocking] Fixed two TS strict-null-check errors caught by npm run build's vue-tsc -b pass
- Found during: Task 1, post-commit
npm run buildverification - Issue:
viewer.lightboxItems.value[0].name— TS2532 "Object is possibly 'undefined'" on two array-index test assertions.npx vitest runalone doesn't type-check test files under this project's config, so this only surfaced during the build's ownvue-tsc -bstep. - Fix: Optional-chained both reads (
viewer.lightboxItems.value[0]?.name). - Files modified:
neode-ui/src/composables/__tests__/usePaidItemViewer.test.ts - Verification:
npm run buildexits 0 after the fix;npx vitest runfor the file still 9/9 green. - Committed in:
4a8925f0
Total deviations: 1 auto-fixed (blocking, build type-check) Impact on plan: Test-only, no behavior change. No scope creep.
Issues Encountered
None beyond the above.
Verification
cd neode-ui && npx vitest run src/composables/__tests__/usePaidItemViewer.test.ts— 9/9 passed.cd neode-ui && npx vitest run— 99 test files / 812 tests, all green (keepAliveTabs.test.tsconfirmed byte-for-byte unmodified viagit status --shortand still passing within that run).cd neode-ui && npx vue-tsc --noEmit— clean.cd neode-ui && npm run build— succeeds;grep -rq 'Opening…' ../web/dist/neode-ui/assets/Cloud-*.jsconfirms the new string reached the bundle.grep -c 'window.open' neode-ui/src/views/Cloud.vue= 0;grep -c 'MediaLightbox' neode-ui/src/views/Cloud.vue= 4 (import + 3 usages incl. the doc comment referencing it);grep -c 'usePaidItemViewer' neode-ui/src/views/Cloud.vue= 3.git diff -- neode-ui/src/views/Cloud.vue | grep -c "^-.*key: 'cloud\."= 0 — no cached-resource key touched.
User Setup Required
None - no external service configuration required.
Next Phase Readiness
- UIFIX-04 and UIFIX-06 are both closed for their named gap (Paid Files). No blockers for future phase-1 plans.
- Carried-forward, unrelated-to-this-plan items already tracked in
02-FINDINGS.md's Outstanding section (Discover/Server/Web5/Fleet/AppDetails/OpenWrtGateway split-signal render-cost regressions, Wallet-send's remount anomaly) remain open there — this plan's audit reconfirmed them but did not fix them, per the plan's own explicit instruction that a cached-revisit render-cost regression is not a UIFIX-06 loader gap.
Phase: 01-federation-mesh-hardening Completed: 2026-07-31