plan: start 015-availability-ui-and-settings-3b9397
This commit is contained in:
184
plans/active/015-availability-ui-and-settings-3b9397.md
Normal file
184
plans/active/015-availability-ui-and-settings-3b9397.md
Normal 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.
|
||||
Reference in New Issue
Block a user