demo/peer-media/photo-*.jpg (all 10 replaced with real Wikimedia Commons photos)
14d1a453 feat(demo): Wavlake tracks, real photos, deduped peer catalog, working paid flow
f52c5407 fix(peer-files): free-image lightbox, click-to-open routing, in-app open after every payment rail
duration
tasks
completed
64m
3
2026-07-29
Quick Task 260729-hj1: Peer-Files Media Batch (Wavlake, Paid Track, Real Photos) Summary
Two real Wavlake tracks (paid buy->autoplay flow now delivers real mp3 bytes), seeded purchase history, deduped peer catalog, ten real Commons photographs, and a shared-frontend fix so every peer file click opens the right viewer (free images finally open in the lightbox).
Wavlake tracks (A1-A3): metadata fetched live from catalog.wavlake.com/v1/tracks/{id} (title/artist/album/duration confirmed against plan recon), mp3 bytes + 1400x1400 artwork downloaded from the CloudFront CDN and committed. song-webfive ("WEBFIVEFOURTHREETWOONE" by Zazawowow, 21 sats, PAID) and song-michael-saylor (free). Both verified as real MPEG audio with file(1).
Real paid bytes (A3): the shared content.owned-get / download-peer-paid / -invoice / -onchain handler now returns real disk bytes with the correct mime_type when the entry has them (text placeholder only for the fictional no-disk films/books). Ecash rail-deduction logic preserved verbatim.
Purchase history (A4): per-session sessionOwnedContent() store lazily seeds 3 dated purchases (song-builders 100 sats — audio with real bytes; film-the-signal 2100; book-cypherpunk-essays 210), computed against the session's own federation onions/catalog slices. Every purchase path appends, so Owned badges survive the post-purchase loadOwned() refresh. song-builders was converted from free to paid (100 sats) so the seeded history contains a playable paid audio item without pre-owning the showcase Wavlake track.
Dedupe (A5):peerCatalogFor is now a deterministic one-peer-per-item assignment (index % 12 against the session's node order) plus a 3-item POPULAR list (song-webfive, film-block-height, photo-aurora-fjord) that appears on exactly one extra peer each. Verified: 29/29 ids present, exactly 3 duplicated ids, both Wavlake tracks on trusted peers.
Session-stable onions:demoFederationNodes() was regenerated (fresh random onions) on every RPC call; it is now memoised per session (sessionFederationNodes()) so catalogs, owned records and the Federation view agree.
Real photos (A6): all ten photo-*.jpg picsum placeholders replaced with real Wikimedia Commons photographs (aurora over Lofoten fjord, Lago di Limides/Dolomites, Dotonbori Osaka neon, Erg Chebbi dunes, Black Forest mist, Cliffs of Moher, Iceland winter road, Stowe VT autumn, St Ives harbour, Grindelwald ridge hiker). Each is >=1920px wide, visually inspected, license-credited in its description; size_bytes updated to real sizes; filenames kept so PEER_MEDIA refs are unchanged.
Streaming route: new GET /api/peer-content/:onion/:content_id serving entry.disk via res.sendFile (Range/206 works — probed). Whitelist lookup only (T-hj1-03), paid entries return 403 so full paid bytes are unreachable without purchase. Demo nginx already proxies /api/ -> :5959; added the missing /api proxy to the vite dev config so dev works too.
Paid audio preview:content.preview-peer serves a ~10% leading slice of the real mp3 for audio entries (the Preview button plays it), images unchanged.
Task 2 — Shared viewer routing (commit f52c5407, both builds)
A8 fix + A7 routing: card click goes through openItem(): owned -> cached viewer/bottom bar; paid playable -> 10% preview; paid non-playable -> pay modal (image never fetched pre-purchase, T-hj1-04); free image -> existing Teleport-to-body viewer as a lightbox with viewerUrl pointed at the stream URL (no base64 round-trip); free audio/video -> playMedia (bottom bar / video modal).
Viewer footer: "Owned · unlocked" (green) only for owned items; "Free · shared by peer" (neutral) otherwise — also the bundle-grep sentinel. Save button streams free files (streamDownload) instead of calling content.owned-get.
Unified post-payment:payWithLightning, pollInvoice and pollOnchain now share openPurchased() with the ecash flow — mark owned, refresh owned list, audio autoplays in the bottom bar, image/video open in the viewer. triggerDownload remains for the explicit Save path.
Blob-URL hygiene:releaseViewerUrl() only revokes blob: URLs (free items use plain URLs).
Preview-thumbnail watcher deliberately NOT extended to audio (real nodes return audio bytes there). Cloud.vue/CloudFolder.vue verified to already open free images via MediaLightbox — untouched.
Regression test added: free image click opens the lightbox.
Task 3 — Verification
Full unit suite: 697/697 passed (baseline in plan said 195; suite has since grown — all green).
npm run build (vue-tsc + vite): success; grep -rl "Free · shared by peer" web/dist/neode-ui/assets/ hits PeerFiles-DsotBwvS.js (build did not no-op).
Mock RPC smoke (20 checks, all pass): browse-peer aggregation contains both Wavlake titles; paid purchase deducts 21 sats ecash and returns audio/mpeg >1MB; purchase appended to owned-list; /api/peer-content answers 206 to a Range probe, 403 for paid items, 404 for unknown/traversal ids; paid audio preview is audio bytes; image previews still jpeg.
Git: two focused code commits, nothing staged under indeedhub/, only other agents' pre-existing untracked files remain.
Deviations from Plan
1. [Rule 3 - Blocking] Session-memoised federation nodes. The plan assumed onions were "random per session"; they were actually random per RPC call, which would have made seeded owned-content onions never match what the frontend sees. Added sessionFederationNodes() memoisation (per-visitor via the existing session store). Commit 14d1a453.
2. [Rule 2 - Missing critical] /api dev proxy in vite.config.ts. The demo nginx proxies /api/ to the mock, but the vite dev server did not — the new route (and the existing /api/blob, /api/app-catalog) would 404 on :8100 dev. Added a /api proxy entry (dev-server-only config; real nodes use nginx). Commit 14d1a453.
3. [Minor scope choice] song-builders converted free -> paid (100 sats). The plan required a seeded audio purchase backed by real disk bytes, but the only paid audio item is the showcase Wavlake track, which must NOT be pre-owned (it would kill the A3 buy demo). Making one existing song paid provides a legitimately purchasable audio item for the seeded history and an Owned-badge example in the gallery.
Search (A1/A2): Cloud -> search "Zazawowow" (or "WEBFIVE" / "Michael") — both tracks appear as peer-file results.
Paid buy -> autoplay (A3): open the peer holding "Zazawowow - WEBFIVEFOURTHREETWOONE.mp3" (21 sats), Buy -> ecash -> Pay: the bottom GlobalAudioPlayer bar appears and the track audibly plays; card flips to green "Owned".
Preview before buying (A3): the paid track's Preview button plays ~25s of real audio, not silence/artwork.
Paid Files tab (A4): Cloud -> Paid Files shows 3+ seeded purchases with sats + dates; clicking "Builders, not talkers (Remastered).mp3" plays it in the bottom bar; the fresh Wavlake purchase from step 2 is now listed too.
Dedupe (A5): browsing several peers, each file appears on ~1 peer; only 3 files (WEBFIVE track, Block Height film, aurora photo) appear on two.
Real photos (A6): photo cards show real photographs (aurora, Dolomites lake, Osaka neon, dunes, mist, cliffs, Iceland road, Vermont autumn, St Ives, Grindelwald) — no picsum grey placeholders.
Free image lightbox (A8): clicking any photo card opens the full-screen viewer with backdrop; footer reads "Free · shared by peer"; Save downloads it.
Free audio/video (A7): clicking "Zazawowow - Michael Michael Saylor.mp3" (free) plays in the bottom bar; a free video (if on the browsed peer) opens the video modal.
Paid gating (A7): a blurred paid image click opens the pay modal, never the image; paying via the Lightning QR path also opens the content in-app (no orphan browser download).
Self-Check: PASSED
demo/content/music/Zazawowow - WEBFIVEFOURTHREETWOONE.mp3 — FOUND (MPEG audio)
demo/content/music/Zazawowow - Michael Michael Saylor.mp3 — FOUND (MPEG audio)
demo/peer-media/artwork-webfive.jpg, artwork-michael-saylor.jpg — FOUND (JPEG)
All 10 demo/peer-media/photo-*.jpg — FOUND (JPEG, >=1920px wide)