Files
ytplayer/plans/queue/015-availability-ui-and-settings-3b9397.md

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
014-p2p-presence-hub-ceced8
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:
                <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:
        <div class="set-group">
          <div class="set-group-title">Offline cache</div>
    
    and further down the handlers:
      $('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:
    .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:
    // 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:
        <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:
      $('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:

  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.