8.0 KiB
8.0 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.