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.
+
+
+