docs(260729-hj1): pre-dispatch plan for peer-files media batch

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
archipelago 2026-07-29 12:49:31 -04:00
parent d8903ad5f3
commit dcef3cb1bd

View File

@ -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"
---
<objective>
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.
</objective>
<context>
## 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 <paths>`), 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.
</context>
<tasks>
<task type="auto">
<name>Task 1: Demo dataset — Wavlake tracks, real photos, dedupe, purchases, real paid bytes, streaming route</name>
<files>neode-ui/mock-backend.js, demo/content/music/ (2 new mp3s), demo/peer-media/ (artwork + replaced photo-*.jpg)</files>
<action>
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', <filename>), 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/).
</action>
<verify>
<automated>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.)</automated>
</verify>
<done>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.</done>
</task>
<task type="auto">
<name>Task 2: Shared viewer routing — free-image lightbox fix, click-to-open, unified post-payment open</name>
<files>neode-ui/src/views/PeerFiles.vue</files>
<action>
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).
</action>
<verify>
<automated>cd neode-ui && npx vitest run src/views/__tests__/PeerFilesRefresh.test.ts src/composables/__tests__/useAudioPlayer.test.ts</automated>
</verify>
<done>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.</done>
</task>
<task type="auto">
<name>Task 3: Full test suite, production build, bundle verification, demo smoke</name>
<files>(no new files — verification only; fixes belong to the task that broke them)</files>
<action>
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.
</action>
<verify>
<automated>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/</automated>
</verify>
<done>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.</done>
</task>
</tasks>
<threat_model>
## 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 |
</threat_model>
<verification>
- `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/.
</verification>
<success_criteria>
- 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.
</success_criteria>
<output>
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).
</output>