From dcef3cb1bd0e7be2bdbaa429929d8a030c99ae94 Mon Sep 17 00:00:00 2001 From: archipelago Date: Wed, 29 Jul 2026 12:49:31 -0400 Subject: [PATCH] docs(260729-hj1): pre-dispatch plan for peer-files media batch Co-Authored-By: Claude Fable 5 --- .../260729-hj1-PLAN.md | 376 ++++++++++++++++++ 1 file changed, 376 insertions(+) create mode 100644 .planning/quick/260729-hj1-peer-files-media-batch-wavlake-paid-trac/260729-hj1-PLAN.md diff --git a/.planning/quick/260729-hj1-peer-files-media-batch-wavlake-paid-trac/260729-hj1-PLAN.md b/.planning/quick/260729-hj1-peer-files-media-batch-wavlake-paid-trac/260729-hj1-PLAN.md new file mode 100644 index 00000000..5bc52285 --- /dev/null +++ b/.planning/quick/260729-hj1-peer-files-media-batch-wavlake-paid-trac/260729-hj1-PLAN.md @@ -0,0 +1,376 @@ +--- +phase: quick-260729-hj1 +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - neode-ui/mock-backend.js + - neode-ui/src/views/PeerFiles.vue + - demo/content/music/ (2 new Wavlake mp3s) + - demo/peer-media/ (Wavlake artwork + replaced photo-*.jpg files) +autonomous: true +requirements: [A1, A2, A3, A4, A5, A6, A7, A8] +user_setup: [] + +must_haves: + truths: + - "Searching the Cloud files search for the Wavlake track titles returns them as peer-file results from at least one demo peer (A1, A2)" + - "The first Wavlake track is PAID; buying it via the demo ecash flow immediately autoplays it in the bottom GlobalAudioPlayer bar (A3)" + - "Cloud -> Paid Files tab shows pre-seeded purchase-history entries (sats paid + date); clicking an audio purchase plays it in the bottom bar (A4)" + - "The aggregated Peer Files tab shows each library item on ~1 peer, with only 2-3 items deliberately duplicated across peers (A5)" + - "Photo peer files show real photographs as card previews and in the full-screen viewer, not picsum placeholders (A6)" + - "In the PeerFiles gallery, clicking a FREE image opens the full-screen image viewer (A8); free audio -> bottom player; free video -> video modal; paid unowned items stay gated behind the pay modal (A7)" + - "Real-node build unaffected: all existing unit tests stay green, npm run build succeeds, and the frontend changes degrade gracefully when the real backend omits demo-only fields" + artifacts: + - "demo/content/music/: two downloaded Wavlake mp3s (real bytes, committed)" + - "demo/peer-media/: Wavlake artwork jpg(s) + photo-*.jpg replaced with real photographs" + - "neode-ui/mock-backend.js: Wavlake PEER_LIBRARY entries, deduped peerCatalogFor, seeded+persistent owned-content state, real-bytes paid/owned downloads, /api/peer-content streaming route" + - "neode-ui/src/views/PeerFiles.vue: click-to-open viewer routing incl. free-image lightbox fix, unified post-payment in-app open/autoplay" + key_links: + - "content.download-peer-paid must return real bytes + correct mime_type -> confirmEcashPay's audio branch -> audioPlayer.play (this is the A3 autoplay chain; today the mock returns text/plain which breaks it)" + - "new /api/peer-content/:onion/:id mock route -> free-audio streaming AND the free-image viewer src (frontend already builds this URL at PeerFiles.vue:1011/1470; it 404s in the demo today)" + - "seeded content.owned-list entries must reference onions of the SAME session's demoFederationNodes() output and items actually present in that peer's peerCatalogFor slice, or Owned badges will not line up" + - "mock content.owned-list must include session purchases, otherwise loadOwned() (called after every purchase at PeerFiles.vue:1314) wipes the just-bought Owned state" +--- + + +Peer-files media batch for the public demo plus two shared-behavior fixes. + +Demo/mock only (A1-A6): add two real Wavlake tracks (metadata + bytes fetched at +execution time), make the first one PAID with a working pay -> autoplay-in-bottom-bar +flow, seed visible purchase history, dedupe the peer catalog to ~2-3 intentional +duplicates, and replace picsum placeholder photos with real photographs. + +Both builds (A7, A8): clicking a peer file opens the appropriate viewer (photos -> +full-screen image viewer, audio -> bottom music player, video -> video modal), and +fix the confirmed bug that FREE images are a click no-op in the PeerFiles gallery +(PeerFiles.vue line 91 ternary falls through to `undefined` for non-playable free items). + +Purpose: the peer-files demo is a flagship "buy content over the mesh" showcase; today +paid purchases unlock a text placeholder, free images don't open, free streaming 404s, +and the catalog is visibly duplicate-heavy. +Output: updated mock-backend.js + committed demo media assets + PeerFiles.vue behavior +fixes, tests green, built bundle verified. + + + + +## Verified recon (do not re-derive; spot-check only) + +**Wavlake API (probed 2026-07-29, works from this network):** +- Track 1 (user link `wavlake.com/track/3504d80b-...`): + `https://catalog.wavlake.com/v1/tracks/3504d80b-b4bf-4196-923b-7ed8b60caec9` + returns `data.title = "WEBFIVEFOURTHREETWOONE"`, `data.artist = "Zazawowow"`, + `data.albumTitle = "WEBFIVE"`, `data.duration = 249`, + `data.artworkUrl = https://d12wklypp119aj.cloudfront.net/image/ed6c75e5-e469-4f52-b073-a18b237dadae.jpg`, + and a stream URL whose direct CDN form is + `https://d12wklypp119aj.cloudfront.net/track/3504d80b-b4bf-4196-923b-7ed8b60caec9.mp3` + (HTTP 200, `content-type: audio/mpeg`, `content-length: 6041903`, accept-ranges, + NO signature/expiry params — stable). +- Album (user link `wavlake.com/album/9be3cdce-...`): + `https://catalog.wavlake.com/v1/albums/9be3cdce-015e-4e7e-8ffe-c017945465c4` + returns title "Michael Michael Saylor" (single) by Zazawowow with one track id + `ba80e385-62a5-4309-ac1e-bb0493b8539f`. Fetch that id from the tracks endpoint for + its metadata; its mp3 follows the same CDN pattern + (`https://d12wklypp119aj.cloudfront.net/track/ba80e385-62a5-4309-ac1e-bb0493b8539f.mp3`). +- Hotlink-vs-download criterion (decided): DOWNLOAD the mp3s + artwork into the repo. + The CDN URLs are stable, but the mock serves content as base64 bytes / disk files + (`content.download-peer`, `content.preview-peer` read `entry.disk` with + `fsSync.readFileSync`), so committed local files are the architecturally consistent + choice and remove any CORS/expiry/offline risk. This matches the existing pattern — + `demo/content/music/*.mp3` and `demo/peer-media/*.jpg` are already committed real files. + External wavlake/CDN URLs would be allowed under the demo IP-leak rule (only + 146.59.87.168 must never appear), but are simply not needed. + +**Mock backend (`neode-ui/mock-backend.js`, port 5959, RPC at `/rpc/v1`):** +- `PEER_LIBRARY` at line ~1256: films/series/books paid (no disk bytes), music/photos/ + docs free with `disk:` pointing at committed files under `demo/content/` and + `demo/peer-media/` (helpers `PEER_MEDIA`/`PEER_CONTENT` at lines 1254-1255). +- `peerCatalogFor(onion)` line ~1321: hash-based slice; the + `((seed ^ (i * 2654435761)) >>> 0) % 12 < 3` clause puts each item on ~25% of the + 12 peers (`demoFederationNodes()` line ~1339, onions are RANDOM per session) — + this is the heavy-duplication source (A5). +- `content.preview-peer` (line ~2358): serves `entry.preview` or the image's `disk`. +- `content.download-peer` (line ~2372): serves `entry.disk` bytes, else text placeholder. +- `content.owned-list` (line ~2402): returns `{ items: [] }` — no purchase history, and + it CLOBBERS just-bought state (see key_links). +- `content.owned-get` / `content.download-peer-paid` / `-invoice` / `-onchain` + (lines ~2405-2425): all return a TEXT PLACEHOLDER (`mime_type: text/plain`) — this is + why paid autoplay (A3) cannot work today. Ecash rail deduction logic here must be kept. +- There is NO `/api/peer-content/:onion/:id` route — the frontend's free-item stream + URL (PeerFiles.vue lines 1011 and 1470) 404s in the demo. + +**Frontend (`neode-ui/src/views/PeerFiles.vue`, 1616 lines):** +- Card click (line 91): `isOwned ? viewOwned : (isPlayable ? playMedia : undefined)` — + free images fall to `undefined` = the A8 bug. `isPlayable` (line 963) is video/audio only. +- `viewOwned` (line 702): audio -> `audioPlayer.play` (bottom bar), image/video -> the + Teleport-to-body "Owned-content viewer" modal (template line 310, footer hardcodes + "Owned · unlocked" at line 354). +- `confirmEcashPay` (line 1277): on success marks owned, audio -> `audioPlayer.play` + (A3 autoplay ALREADY implemented here — only the mock's text/plain response breaks it), + then `void loadOwned()` (line 1314). +- `payWithLightning` (line 1364), `pollInvoice` (line 1417), `pollOnchain` (line 1197): + on success these call `triggerDownload` (browser download) instead of the in-app + open/autoplay path — inconsistent with confirmEcashPay. +- `playMedia` (line 1460): free audio -> `audioPlayer.play(streamUrl)`, free video -> + video modal via the same `/api/peer-content/` stream URL; paid uses + `content.preview-peer` bytes ("10% preview"). NOTE: for a PAID AUDIO item the + pre-purchase Preview button plays whatever `content.preview-peer` returns — so for the + paid Wavlake track the mock MUST return audio bytes there, not artwork. +- Preview thumbnails (watcher line 872): fetched ONLY for image/video mimes; audio cards + show a waveform icon. The real backend's `content.preview-peer` + (core/archipelago/src/api/rpc/content.rs:1113) proxies the seller's + `/content/{id}/preview` — for audio that is audio bytes, so do NOT extend the + thumbnail watcher to audio (it would fetch audio blobs as "thumbnails" on real nodes). +- `Cloud.vue`: Paid Files tab (line ~155) lists `content.owned-list` items + (`PaidItem { onion, content_id, filename, mime_type, size_bytes, paid_sats, + purchased_at }`) and `viewPaidItem` (line ~470) plays audio in the bottom bar — this + is the A4 purchase-history surface, already built; it only needs seeded data. + Cloud search (`runSearch` line ~880) filters the aggregated `peerFiles` by filename — + Wavlake items are searchable as soon as they appear in any peer's catalog slice. +- `GlobalAudioPlayer` is mounted in `App.vue` (line 48); `useAudioPlayer` is a global + singleton — nothing to change there. +- Tests: `src/views/__tests__/PeerFilesRefresh.test.ts` mounts PeerFiles.vue — keep green. + +**Project rules that bind this work:** +- Modals/lightboxes must Teleport to body with full-screen backdrop (the existing + owned-content viewer already complies — reuse it). +- Do not add new UI entry points (no new tabs/cards/nav); only change behavior of + existing elements. +- NEVER stage/commit anything under `indeedhub/` (git submodule). Stage explicitly by + path (`git add `), never `git add -A`. +- Never expose 146.59.87.168 in demo-served content. +- Commit each task when it works, message trailer `Co-Authored-By: Claude ...`. + Executor commits code only; docs/summary are committed by the orchestrator. + + + + + + Task 1: Demo dataset — Wavlake tracks, real photos, dedupe, purchases, real paid bytes, streaming route + neode-ui/mock-backend.js, demo/content/music/ (2 new mp3s), demo/peer-media/ (artwork + replaced photo-*.jpg) + + All changes in this task are demo/mock-only (mock-backend.js is not part of the + real-node build). + + 1. Fetch Wavlake assets at execution time (A1, A2). Use curl against the verified + endpoints in context: resolve track 3504d80b-b4bf-4196-923b-7ed8b60caec9 and the + album 9be3cdce-015e-4e7e-8ffe-c017945465c4's single track + ba80e385-62a5-4309-ac1e-bb0493b8539f via catalog.wavlake.com/v1/tracks/{id}. + Parse real title/artist/albumTitle/duration/artworkUrl from the JSON (jq or node -e) + — do NOT hardcode metadata from this plan except the ids; the API is the source of + truth. Download each track's CDN mp3 into demo/content/music/ (filename derived from + real title, e.g. "Zazawowow - WEBFIVEFOURTHREETWOONE.mp3") and each artworkUrl jpg + into demo/peer-media/. Sanity-check downloads with file(1): mp3s must be MPEG audio + (~6MB expected for track 1), artwork must be JPEG. Abort and report if the API shape + changed. + + 2. Add both tracks to PEER_LIBRARY (A1-A3). mime_type audio/mpeg, real size_bytes + (actual file size), disk: PEER_CONTENT('music', ), description carrying + real artist/album/duration metadata (this makes artist searchable too). First track + (WEBFIVEFOURTHREETWOONE): access paid with a price the demo ecash balance easily + covers (e.g. 21 sats). Second track: access 'free'. For the PAID track, give it a + preview behavior consistent with playMedia: content.preview-peer for audio entries + must return AUDIO bytes (read entry.disk; serving the full mp3 is acceptable for the + demo, or the first ~10% byte-slice to match the "10% preview" badge — mp3 frames + tolerate truncation), NOT the artwork jpg — see the recon note on playMedia's paid + path. Keep image entries' preview behavior unchanged. + + 3. Dedupe peerCatalogFor (A5). Replace the probabilistic + `((seed ^ (i * 2654435761)) >>> 0) % 12 < 3` inclusion with a deterministic + assignment: each PEER_LIBRARY item lives on exactly one peer (e.g. index-based + slot), plus a small explicit POPULAR list of 2-3 item ids that additionally appear + on 1-2 more peers (a little duplication is realistic). Keep the function signature + and per-session determinism (same onion -> same slice within a session). Ensure both + Wavlake tracks land on at least one trusted peer's slice so Cloud search finds them. + + 4. Real photos (A6). Replace all ten demo/peer-media/photo-*.jpg picsum placeholders + with real photographs downloaded at execution time from a stable free-license source + (Wikimedia Commons Special:FilePath URLs with a width parameter, e.g. ?width=1200, + are reliable and hotlink-free once committed). Pick images that match each entry's + existing description (aurora over fjord, mountain lake, neon city rain, desert + dunes, forest mist, ocean cliff, northern road, autumn valley, harbor dawn, alpine + ridge) or update the descriptions (including the "sourced via picsum.photos" + credit text and the stale PEER_LIBRARY header comment) to match reality. Keep the + photo-*.jpg filenames so PEER_MEDIA references don't change; update each entry's + size_bytes to the new actual file size. Verify each file with file(1) is a real + JPEG of reasonable resolution (>= ~1000px wide). + + 5. Owned/purchase state (A3, A4). Introduce a mock owned-content store (e.g. + mockState.ownedContent array of { onion, content_id, filename, mime_type, + size_bytes, paid_sats, purchased_at }). Lazily seed it on first access with 2-4 + plausible past purchases dated days-to-weeks ago, referencing onions from the + CURRENT session's demoFederationNodes() output and content ids actually present in + that peer's peerCatalogFor slice (onions are random per session — compute, don't + hardcode). At least one seeded purchase must be an audio item backed by real disk + bytes so the Paid Files tab click plays music. content.owned-list returns this + store (all fields of Cloud.vue's PaidItem interface). Every successful purchase + path (content.download-peer-paid, and the -invoice/-onchain and onchain/invoice + status flows' download calls) appends an entry with the real price and + purchased_at=now, so the Owned badge survives the post-purchase loadOwned() + refresh and purchases show up in the Paid Files tab. + + 6. Real bytes for paid/owned downloads (A3). Rework the shared + content.owned-get / content.download-peer-paid / -invoice / -onchain case: look up + the PEER_LIBRARY entry by content_id; when entry.disk exists return the real file + bytes with mime_type entry.mime_type; keep the text placeholder only as fallback + for entries without disk bytes (the fictional 2GB films). Preserve the existing + ecash rail-deduction logic exactly. This makes buying the Wavlake track deliver a + real mp3 with mime_type audio/mpeg, which is what confirmEcashPay's existing audio + branch needs to autoplay in the bottom bar. + + 7. Streaming route. Add an Express GET route /api/peer-content/:onion/:content_id + to mock-backend.js that resolves the PEER_LIBRARY entry and serves entry.disk via + res.sendFile (Express handles Range/206 automatically — the frontend probes with + Range: bytes=0-0), 404 JSON { error } otherwise. This unbreaks the demo's existing + free-audio/video streaming and is required by Task 2's free-image viewer. Verify + the demo/dev proxy actually forwards /api/* to the mock (check vite.config.ts / + vite.preview.config.mts proxy config and the demo docker nginx config if present); + if /api is not proxied in dev, register the route on whatever path prefix reaches + the mock and keep the frontend URL unchanged (the frontend path is fixed — it must + work on real nodes too, where nginx proxies /api to the daemon). + + Commit this task on its own (stage mock-backend.js + the demo/ media files + explicitly by path; git add demo/content/music demo/peer-media is fine, never + git add -A; nothing under indeedhub/). + + + cd neode-ui && node --check mock-backend.js && node mock-backend.js & sleep 2; then RPC-probe localhost:5959: (a) content.browse-peer for each federation onion — every PEER_LIBRARY id appears on >=1 peer, total duplicate ids across all peers <= 3, both Wavlake ids present; (b) content.owned-list returns >=2 seeded items with paid_sats and purchased_at; (c) content.download-peer-paid for the paid Wavlake id returns mime_type audio/mpeg with data length > 1MB base64; (d) curl -H "Range: bytes=0-0" /api/peer-content/{onion}/{free-wavlake-id} returns 206; (e) file demo/peer-media/photo-*.jpg all report JPEG; then kill the mock. (If /rpc/v1 requires a session, log in first with the mock demo password password123.) + + Both Wavlake tracks in the catalog with real committed bytes + real API metadata; first is paid and delivers real audio/mpeg bytes on purchase; owned-list seeded and purchase-persistent; catalog duplication reduced to <=3 intentional items; photos are real JPEGs; /api/peer-content serves Range requests. Committed. + + + + Task 2: Shared viewer routing — free-image lightbox fix, click-to-open, unified post-payment open + neode-ui/src/views/PeerFiles.vue + + These changes ship in BOTH the real-node build and the demo — no IS_DEMO gating, + and every path must degrade gracefully against the real backend. + + 1. Fix the A8 bug + A7 click routing at the card click handler (line 91). Replace + the ternary with a single openItem(item) dispatcher: owned -> viewOwned (existing); + paid-unowned playable -> playMedia (existing 10% preview, keep); paid-unowned + NON-playable (images, blurred) -> openPayModal(item) so the click gets the + appropriate "viewer" for locked content while preserving the paid gating (the + full image is never fetched or revealed pre-purchase — only the blurred thumbnail); + FREE image -> open the existing Teleport-to-body owned-content viewer modal + (template line 310) as a lightbox: set viewerItem/viewerMime and point viewerUrl + at the free stream URL /api/peer-content/{onion}/{id} directly (an img src streams + it; no base64 round-trip; works on real nodes via the Range proxy and in the demo + via Task 1's new route). Free audio/video already route through playMedia — keep. + Guard closeViewer's URL.revokeObjectURL so it only revokes blob: URLs. + + 2. Generalize the viewer footer (line 354): the hardcoded "Owned · unlocked" green + caption must only show for owned items; for free items show a neutral caption + (exact string "Free · shared by peer" — also used as the bundle-grep sentinel), and + make the footer Save button use the free download path for free items (streamDownload) + instead of content.owned-get. + + 3. Unify post-payment success handling (A3-adjacent, both builds). payWithLightning + (line 1364), pollInvoice (line 1417) and pollOnchain (line 1197) currently + triggerDownload on success; align them with confirmEcashPay (line 1277): mark the + item owned in ownedKeys, refresh loadOwned, and open in-app — audio -> + audioPlayer.play (bottom-bar autoplay), image/video -> the viewer modal — using the + mime_type from the download response with item.mime_type as fallback. Extract the + shared logic (e.g. an openPurchased(item, data, mime) helper) rather than + duplicating it four times. Keep triggerDownload available via the viewer's Save + button. Do not touch the payment/polling logic itself. + + 4. Do NOT extend the preview-thumbnail watcher (line 872) to audio — on real nodes + content.preview-peer returns audio bytes for audio items (see recon), which must + not be used as an img src. Audio cards keep the waveform icon. + + 5. Sanity-check the other previews-grid views for the same A8 class of bug: + CloudFolder.vue / Cloud.vue My Files use FileCard @preview -> handlePreview -> + MediaLightbox and should already open free images; verify by reading the handler + chain (no change expected — do not modify them if they work). + + 6. Keep src/views/__tests__/PeerFilesRefresh.test.ts green; if the click-dispatch + refactor is cheaply testable, extend that test file with a case asserting a free + image click sets viewerUrl (do not build new test infrastructure). + + Commit this task separately (stage neode-ui/src paths explicitly). + + + cd neode-ui && npx vitest run src/views/__tests__/PeerFilesRefresh.test.ts src/composables/__tests__/useAudioPlayer.test.ts + + Clicking a free image in PeerFiles opens the full-screen viewer (Teleport-to-body, backdrop preserved); free audio -> bottom bar; free video -> video modal; paid unowned image click opens the pay modal and never reveals the image; all four payment-success paths open purchased content in-app with audio autoplaying in the bottom bar. Targeted tests green. Committed. + + + + Task 3: Full test suite, production build, bundle verification, demo smoke + (no new files — verification only; fixes belong to the task that broke them) + + 1. Run the full unit suite: cd neode-ui && npx vitest run — all tests (currently + 195) must pass. Fix any regression in the file that caused it, amending or adding + a fixup commit to the responsible task's change. + 2. Production build per CLAUDE.md: cd neode-ui && npm run build (vue-tsc must pass; + output lands in web/dist/neode-ui). Grep the built bundle for the Task 2 sentinel + string to prove the build did not silently no-op: + grep -rl "Free · shared by peer" ../web/dist/neode-ui/assets/ must match at least + one js file. + 3. End-to-end demo smoke against the mock: start node mock-backend.js plus the dev + frontend (or vite preview against the built dist) and exercise via curl/RPC: search + corpus contains the Wavlake titles (browse-peer aggregation), paid purchase of the + Wavlake track deducts ecash and returns audio/mpeg, owned-list grows by the + purchase, /api/peer-content serves the free track with 206. Confirm no occurrence + of 146.59.87.168 in mock-backend.js additions or demo-served data: + grep -rn "146.59.87.168" neode-ui/mock-backend.js demo/ must be empty. + 4. Confirm git hygiene: git status shows nothing staged under indeedhub/; all work + is committed across the task commits (code only — the summary doc is the + orchestrator's commit). Leave deploy/push to the orchestrator. + 5. Write into the task summary a post-deploy live checklist for + http://146.59.87.168:2100 (orchestrator deploys): search finds the Wavlake tracks; + buy the paid track with ecash -> bottom bar autoplays; Paid Files tab shows seeded + purchases; peer catalog shows <=3 duplicated files; photos are real and open in the + lightbox on click; free song click plays in bottom bar. + + + cd neode-ui && npx vitest run && npm run build && grep -rl "Free · shared by peer" ../web/dist/neode-ui/assets/ | head -1 && ! grep -rn "146.59.87.168" mock-backend.js ../demo/ + + Full suite green, production build succeeds, bundle grep proves the new UI string shipped, demo smoke passes, no IP leak, clean git state with per-task commits. + + + + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| executor -> catalog.wavlake.com / CloudFront / Wikimedia | External bytes fetched at execution time get committed into the repo and served by the demo | +| demo visitor -> mock backend | Untrusted public visitors hit the new /api/peer-content route | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan | +|-----------|----------|-----------|----------|-------------|-----------------| +| T-hj1-01 | Tampering | Downloaded mp3/jpg assets | medium | mitigate | Verify every download with file(1) magic-type + plausible size before committing; fetch only from the verified catalog.wavlake.com/CloudFront/Wikimedia URLs over https | +| T-hj1-02 | Information Disclosure | Demo-served data | high | mitigate | Task 3 gate: grep for 146.59.87.168 across mock-backend.js and demo/ must be empty | +| T-hj1-03 | Tampering (path traversal) | /api/peer-content route | medium | mitigate | Route resolves content_id strictly against PEER_LIBRARY entries (whitelist lookup, never a filesystem path built from request input) | +| T-hj1-04 | Elevation | Paid-content gating in PeerFiles.vue | medium | mitigate | Paid-unowned image click opens the pay modal only; the free-image viewer path is reachable solely when access !== paid; no pre-purchase full-content fetch is added | + + + +- `cd neode-ui && npx vitest run` — full suite green (195 tests baseline). +- `cd neode-ui && npm run build` — vue-tsc + vite build succeed; bundle grep for "Free · shared by peer" hits. +- Mock RPC smoke: Wavlake items searchable via browse-peer aggregation; paid purchase returns audio/mpeg real bytes and appends to owned-list; duplicate ids across all 12 peers <= 3; /api/peer-content answers 206 to a Range probe; owned-list seeded with dated purchases. +- `git log --oneline` shows one focused commit per task; nothing staged under indeedhub/. + + + +- A1/A2: both Wavlake tracks (real API metadata, real committed bytes) searchable in the peer-files search. +- A3: first Wavlake track is paid; demo ecash purchase autoplays it in the bottom GlobalAudioPlayer bar. +- A4: Cloud -> Paid Files shows seeded purchase history; audio purchases play on click; new purchases persist in the list. +- A5: at most 2-3 deliberately duplicated files across the aggregated peer catalog. +- A6: all photo peer files are real photographs matching their descriptions. +- A7/A8 (both builds): free image click opens the full-screen viewer, free audio -> bottom player, free video -> video modal, paid gating preserved; all payment paths open purchased media in-app. +- Non-demo build safe: tests green, build clean, frontend degrades gracefully without demo-only mock data. + + + +On completion the executor reports per-task commit hashes and the post-deploy live +verification checklist for http://146.59.87.168:2100 (deployment is the orchestrator's +job; docs/summary committed by the orchestrator). +