Files
archy/.planning/phases/01-federation-mesh-hardening/01-14-SUMMARY.md
T
archipelagoandClaude Opus 5 90884e6259
Demo images / Build & push demo images (push) Successful in 4m21s
feat(01-02): chat mutations mutate demo state instead of acking (FED-04)
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>
2026-08-01 06:37:00 -04:00

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
vue
composable
lightbox
loading-state
vitest
phase provides
02-ui-performance PERF-02 no-spinner-on-cached-revisit guarantee, useCachedResource/KeepAlive infrastructure, and the split-signal-cost root-cause findings for the eight named slow-open surfaces
usePaidItemViewer() composable — fetch/decode/route/loading/error state for a purchased item, replacing the inline window.open() path
A second MediaLightbox instance in Cloud.vue, fed synthetic FileBrowserItems for purchased images/video
Row-level 'Opening…' loading affordance and non-interactive-while-loading guard on Paid Files rows
Evidence-backed per-surface verdict for every surface phase 2's findings named as a slow-open regression
cloud
media-viewer
added patterns
Fetch-before-show: the composable fetches+decodes the purchased blob BEFORE opening the lightbox, so the lightbox's own fetchBlobUrl/streamUrl props resolve instantly from an internal map — the row's own 'opening' spinner covers the real (possibly-slow) network wait, not the lightbox's own (now-instant) internal loading flash.
One key, one owner: opening/inFlight/urlByPath are all keyed by the same paidItemKey(onion+content_id) formula the row's v-for :key already uses, so the row's spinner condition and the composable's dedup guard can never drift apart.
created modified
neode-ui/src/composables/usePaidItemViewer.ts
neode-ui/src/composables/__tests__/usePaidItemViewer.test.ts
neode-ui/src/views/Cloud.vue
Documents (and any other mime with no in-app viewer) keep the browser-tab fallback unchanged — this is a scope boundary from the plan, not an omission.
All eight surfaces phase 2 flagged as slow-opening (Discover, Server, Web5, Fleet, AppDetails, OpenWrtGateway, MarketplaceAppDetails, Wallet-send) are CACHED-REVISIT render-cost regressions, not missing-first-load-affordance gaps — every one already shows a loading affordance on its genuinely slow path (first load or the specific in-flight async op), so no loader was added anywhere in Task 2. Adding one to any of them would put a spinner on a cached revisit, a direct PERF-02 regression.
URL ownership: the lightbox revokes every URL it is handed, on unmount, exactly as it already does for My Files/Peer Files. The composable never schedules a competing revoke for a URL routed to the lightbox — only the browser-tab fallback path keeps its own revoke timer, unchanged from before.
Row-level loading + non-interactive guard for a fetch-then-route action, reusing PeerFiles.vue's existing 3x3 spinner + 'Opening...' treatment rather than inventing new visual language.
UIFIX-04
UIFIX-06
id description requirement verification human_judgment
D1 A purchased picture or video opens in the app's own MediaLightbox instead of a browser tab UIFIX-04
kind ref status
unit neode-ui/src/composables/__tests__/usePaidItemViewer.test.ts#routes an image mime to the lightbox, not window.open pass
kind ref status
unit neode-ui/src/composables/__tests__/usePaidItemViewer.test.ts#routes a video mime to the lightbox, not window.open pass
false
id description requirement verification human_judgment
D2 Audio still routes to the global bottom-bar player, never the lightbox; documents keep today's browser-tab fallback UIFIX-04
kind ref status
unit neode-ui/src/composables/__tests__/usePaidItemViewer.test.ts#routes an audio mime to the audio player, never the lightbox pass
kind ref status
unit neode-ui/src/composables/__tests__/usePaidItemViewer.test.ts#falls back to the browser tab for a mime with no in-app viewer pass
false
id description requirement verification human_judgment
D3 The Paid Files row shows a visible loading affordance for the whole duration of a purchased-item fetch, and clears it (including on failure); a second click on the same item in flight issues one RPC UIFIX-06
kind ref status
unit neode-ui/src/composables/__tests__/usePaidItemViewer.test.ts#sets opening for the whole duration of the fetch and clears it on success pass
kind ref status
unit neode-ui/src/composables/__tests__/usePaidItemViewer.test.ts#surfaces a rejected/timed-out fetch as an error, clears opening, and does not throw past the caller pass
kind ref status
unit neode-ui/src/composables/__tests__/usePaidItemViewer.test.ts#issues exactly one RPC when open() is called twice in quick succession for the same item pass
false
id description requirement verification human_judgment
D4 Every surface phase 2's findings named as slow-opening has a recorded, evidence-backed verdict; no loader was added to a cached-revisit path UIFIX-06
kind ref status
other Per-surface verdict table below, cross-referenced against .planning/phases/02-ui-performance/02-FINDINGS.md's Per-surface verdict / Accepted deviations sections and direct grep of each surface's own loading-state code pass
false
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: moves Cloud.vue's inline content.owned-get fetch/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-flight open() guard keyed on onion+content_id, and a resolveBlobUrl the 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 (reusing PeerFiles.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 MediaLightbox instance added below the existing My Files one, fed the composable's lightboxItems/lightboxIndex and resolveBlobUrl for both fetch-blob-url and stream-url.
  • The composable's catch sets a real, user-facing error message that Cloud.vue surfaces through its existing loadError/alert-error block — 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

  1. 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 run alone doesn't flag): 4a8925f0 (fix)
  2. 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 items
  • neode-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 dedup
  • neode-ui/src/views/Cloud.vue - viewPaidItem() now delegates to the composable; row loading/error UI; second MediaLightbox instance

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 build verification
  • Issue: viewer.lightboxItems.value[0].name — TS2532 "Object is possibly 'undefined'" on two array-index test assertions. npx vitest run alone doesn't type-check test files under this project's config, so this only surfaced during the build's own vue-tsc -b step.
  • Fix: Optional-chained both reads (viewer.lightboxItems.value[0]?.name).
  • Files modified: neode-ui/src/composables/__tests__/usePaidItemViewer.test.ts
  • Verification: npm run build exits 0 after the fix; npx vitest run for 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.ts confirmed byte-for-byte unmodified via git status --short and 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-*.js confirms 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