9.1 KiB
9.1 KiB
id, title, created, depends_on, est_files
| id | title | created | depends_on | est_files | |
|---|---|---|---|---|---|
| 015-availability-ui-and-settings-3b9397 | Show peer availability with stale markers and add Sharing settings | 2026-09-29 |
|
6 |
015 — Availability line + Sharing settings
Objective
Implements the UI half of flow 5 and the settings of docs/p2p-architecture.md.
After this plan:
- Under the now-playing title a small line shows peer availability, e.g.
📡 On 3 devices · 1 online now · last checked 2d ago. Hovering lists each holder (peer · online/offline · checked Xd ago (stale) · spot-checked). When EVERY holder is older than the server'sstaleDays, the line says(not checked recently)and is styled dimmer — holders are never hidden for age. No holders → no line. - Settings gets a "Sharing (peer-to-peer)" group: Share my saved videos and Get videos from other devices, both ON by default, plus a status row (registered / online peer id, verified/saved counts).
Pre-tested: frontend/p2p-core.test.js 5/5.
Context the executor must NOT rediscover
plans/patches/015-p2p-core-new.diffcreatesfrontend/p2p-core.js(window.P2PCore:ago,formatAvailability(payload, now),pickPeers(payload, cid)) and its node test.frontend/index.html:223-227:<div id="nowPlayingMeta" class="now-meta hidden"> <div class="np-text"> <div class="np-title" id="npTitle"></div> <div class="np-channel" id="npChannel"></div> </div>frontend/app.jsfunction updateNowPlayingActions()(~line 2389) beginsif (!current || !current.meta) return; const id = current.meta.id;and ends with the save-buttonif/elsechain followed by}.frontend/app.jsasync function renderSettings()(~line 7321). Its template contains:and further down the handlers:<div class="set-group"> <div class="set-group-title">Offline cache</div>$('setAutoPreload').addEventListener('change', (e) => { data.settings.autoPreload = e.target.checked; persist(); if (e.target.checked) data.playlists.forEach(preloadPlaylist); });frontend/styles.css:737—.np-actions { display: flex; gap: 8px; flex-shrink: 0; }; the dim text colour token isvar(--text-dim).window.P2PClient(plans 013/014):device(),isConnected(),peer(),sync().window.DeviceDB.listFiles()(plan 012).
Steps
- From the repo root:
git apply plans/patches/015-p2p-core-new.diff(STOP on failure). frontend/index.html— after<div class="np-channel" id="npChannel"></div>add:<div class="np-avail hidden" id="npAvail" aria-live="polite"></div>and add<script src="p2p-core.js"></script>directly after thep2p-client.jsscript line.frontend/sw.jsSHELL— add'/p2p-core.js',directly after'/p2p-client.js',.frontend/styles.css— directly after the.np-actions { … }line add:.np-avail { font-size: 12px; color: var(--text-dim); margin-top: 4px; cursor: default; } .np-avail.stale { opacity: 0.6; font-style: italic; }frontend/app.js— directly ABOVEfunction updateNowPlayingActions() {add:// P2P availability under the title (docs/p2p-architecture.md flow 5). // Cached 60 s per video so the frequent updateNowPlayingActions() calls // don't hammer the server. const availCache = new Map(); // videoId -> { at, payload } async function renderAvailability(id) { const el = $('npAvail'); if (!el) return; const off = data.settings.p2pShare === false && data.settings.p2pReceive === false; if (!WEB || !window.P2PCore || off || navigator.onLine === false || String(id).startsWith('edit_')) { el.classList.add('hidden'); return; } let hit = availCache.get(id); if (!hit || Date.now() - hit.at > 60_000) { hit = { at: Date.now(), payload: null }; availCache.set(id, hit); try { const r = await fetch(`/api/p2p/holders?v=${encodeURIComponent(id)}`); hit.payload = r.ok ? await r.json() : null; } catch { /* offline / disabled — no line */ } if (availCache.size > 200) availCache.delete(availCache.keys().next().value); } if (!current || !current.meta || current.meta.id !== id) return; // moved on meanwhile const f = window.P2PCore.formatAvailability(hit.payload, Date.now()); if (!f) { el.classList.add('hidden'); return; } el.textContent = f.text; el.title = f.title; el.classList.toggle('stale', f.stale); el.classList.remove('hidden'); }frontend/app.jsupdateNowPlayingActions— directly afterconst id = current.meta.id;addrenderAvailability(id);frontend/app.jsrenderSettingstemplate — directly BEFORE the<div class="set-group">whose title isOffline cache, insert:<div class="set-group"> <div class="set-group-title">Sharing (peer-to-peer)</div> <label class="set-row"> <span> Share my saved videos <small>Other devices can download videos you saved when YouTube and the server can't provide them. Files are verified by their hash. On by default.</small> </span> <input id="setP2pShare" type="checkbox" ${data.settings.p2pShare !== false ? 'checked' : ''} /> </label> <label class="set-row"> <span> Get videos from other devices <small>When a video is gone from YouTube and the server, download a verified copy from a device that has it. On by default.</small> </span> <input id="setP2pReceive" type="checkbox" ${data.settings.p2pReceive !== false ? 'checked' : ''} /> </label> <div class="set-row"> <span>This device<small id="p2pDeviceInfo">…</small></span> <span id="p2pHoldingCount" class="set-stat">…</span> </div> </div>frontend/app.jsrenderSettings— directly after the$('setAutoPreload').addEventListener(…);block add:$('setP2pShare').addEventListener('change', (e) => { data.settings.p2pShare = e.target.checked; persist(); if (window.P2PClient) window.P2PClient.sync(); // share off → withdraws every holding }); $('setP2pReceive').addEventListener('change', (e) => { data.settings.p2pReceive = e.target.checked; persist(); if (window.P2PClient) window.P2PClient.sync(); }); (async () => { const info = $('p2pDeviceInfo'); const cnt = $('p2pHoldingCount'); if (!info || !cnt) return; const P = window.P2PClient; const d = P && P.device(); info.textContent = !d ? 'Not registered yet' : P.isConnected() ? `Online · peer ${P.peer()}` : 'Registered · not connected'; const files = window.DeviceDB ? await window.DeviceDB.listFiles() : []; cnt.textContent = `${files.filter((f) => f.state === 'verified').length} verified / ${files.length} saved`; })();
Out of scope / do NOT touch
- No download button yet (plan 017 adds "Get from a device"). Do not change
p2p-client.js. - Do not show availability for edited cuts (
edit_…).
Verification
cd /home/user/ytplayer && node --check frontend/app.js frontend/p2p-core.js && echo FRONT_OK
node --test frontend/*.test.js 2>&1 | grep -E "^# (pass|fail)"
grep -c "p2p-core.js" frontend/index.html frontend/sw.js
grep -c "setP2pShare\|setP2pReceive\|renderAvailability(id)" frontend/app.js
Expected: FRONT_OK; # fail 0; 1 and 1; app.js grep ≥ 5.
Manual (optional, needs a running server with P2P data): open the app, play a video that has holders,
see the 📡 line; Settings shows the Sharing group with both boxes ticked.
Report format (executor: follow exactly)
Output ONLY the following, no other prose:
git diff(unified) of all changes.- Raw output of the Verification commands.
Findings:— max 10 lines.
Do not commit. Do not push. Do not touch files outside the Steps.
Execution log
- Executor: in-session Agent (haiku). Attempts: 1. Fix rounds: 0.
- Orchestrator re-ran Verification:
p2p-core.jsand its test byte-identical to the tested versions; all frontend files passnode --check; 61 frontend tests pass; app.js references 6; index.html and sw.js each referencep2p-core.jsonce. Real Chromium load against the patched server: no JS errors, both Sharing checkboxes checked by default, device row showsOnline · peer .... OnlyP2PTransferis absent (plan 017). - Executor Findings (verbatim): All 8 steps of plan 015 applied successfully. git apply of the p2p-core.js patch succeeded with no conflicts. All syntax checks pass, unit tests pass (61/61), and all grep verification counts match or exceed expectations. The p2p-core.js and p2p-core.test.js files were created by the git patch as expected (visible in git status as untracked but not modified by this executor). P2P Sharing settings UI group inserted before Offline cache group. Availability line HTML, CSS, JavaScript, and service worker cache entries all in place.