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

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

Execution log

  • Executor: in-session Agent (haiku). Attempts: 1. Fix rounds: 0.
  • Orchestrator re-ran Verification: p2p-core.js and its test byte-identical to the tested versions; all frontend files pass node --check; 61 frontend tests pass; app.js references 6; index.html and sw.js each reference p2p-core.js once. Real Chromium load against the patched server: no JS errors, both Sharing checkboxes checked by default, device row shows Online · peer .... Only P2PTransfer is 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.