plan: start 015-availability-ui-and-settings-3b9397

This commit is contained in:
Claude
2026-09-30 07:42:35 +00:00
parent 6fa930ad62
commit 780624347f
2 changed files with 1 additions and 1 deletions

View File

@@ -0,0 +1,184 @@
---
id: 015-availability-ui-and-settings-3b9397
title: Show peer availability with stale markers and add Sharing settings
created: 2026-09-29
depends_on: [014-p2p-presence-hub-ceced8]
est_files: 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's `staleDays`, 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.diff` creates `frontend/p2p-core.js` (`window.P2PCore`:
`ago`, `formatAvailability(payload, now)`, `pickPeers(payload, cid)`) and its node test.
- `frontend/index.html:223-227`:
```html
<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.js` `function updateNowPlayingActions()` (~line 2389) begins
`if (!current || !current.meta) return; const id = current.meta.id;` and ends with the
save-button `if/else` chain followed by `}`.
- `frontend/app.js` `async function renderSettings()` (~line 7321). Its template contains:
```html
<div class="set-group">
<div class="set-group-title">Offline cache</div>
```
and further down the handlers:
```js
$('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 is `var(--text-dim)`.
- `window.P2PClient` (plans 013/014): `device()`, `isConnected()`, `peer()`, `sync()`.
`window.DeviceDB.listFiles()` (plan 012).
## Steps
1. From the repo root: `git apply plans/patches/015-p2p-core-new.diff` (STOP on failure).
2. `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 the `p2p-client.js` script line.
3. `frontend/sw.js` `SHELL` — add `'/p2p-core.js',` directly after `'/p2p-client.js',`.
4. `frontend/styles.css` — directly after the `.np-actions { … }` line add:
```css
.np-avail { font-size: 12px; color: var(--text-dim); margin-top: 4px; cursor: default; }
.np-avail.stale { opacity: 0.6; font-style: italic; }
```
5. `frontend/app.js` — directly ABOVE `function updateNowPlayingActions() {` add:
```js
// 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');
}
```
6. `frontend/app.js` `updateNowPlayingActions` — directly after `const id = current.meta.id;` add
`renderAvailability(id);`
7. `frontend/app.js` `renderSettings` template — directly BEFORE the
`<div class="set-group">` whose title is `Offline cache`, insert:
```html
<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>
```
8. `frontend/app.js` `renderSettings` — directly after the `$('setAutoPreload').addEventListener(…);`
block add:
```js
$('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
```bash
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:
1. `git diff` (unified) of all changes.
2. Raw output of the Verification commands.
3. `Findings:` — max 10 lines.
Do not commit. Do not push. Do not touch files outside the Steps.