--- id: 019-admin-p2p-panel-4dc623 title: Add a P2P panel to the admin page created: 2026-09-29 depends_on: [018-server-rehydrate-from-peer-4fb8bd] est_files: 3 --- # 019 — Admin P2P panel ## Objective Give the admin one place to see and steer peer-to-peer sharing (`docs/p2p-architecture.md`). After this plan `/admin` has a **Peer-to-peer** section showing: - config: P2P on/off, malware scan on/off (with a note that it is OFF by default and hashing + validation always run), stale-after days, retention thresholds; - counts: verified content, revoked, devices, active holder rows, cids with ≥1 holder, devices online now, open/active intake uploads; - the 30 most recently verified files (video, cid prefix, origin, scan, size, verified at, holder count) with a **Revoke** button (revoked cids are never offered to or accepted from devices again — plan 013 only accepts `verified` cids). ## Context the executor must NOT rediscover - Admin auth middleware: `notes.requireAdminOrToken` (returned by `registerNoteRoutes`, `server/server.js` ~line 1823). Used like `app.get('/api/admin/media', requireAdminOrToken, async (c) => …)`. - In server.js (plans 008–018): `P2P` (config), `p2pDb` (module), `p2pHub.onlineCount()`, `intake.openTickets()`, `intake.active()`. - `server/p2p-db.js` has `rowsOf`, `p2pStats()`, `revokeContent(cid)`. - `frontend/admin.html`: - sections are `
…
` inside `
`; the "Recent edits" section (~line 190) starts with ```html

Recent edits

``` - helpers inside the script IIFE: `$(id)`, `esc(s)`, `api(path, opts)` (JSON in/out, `opts.body` is JSON-encoded), classes `tbl`, `scroll`, `muted`, `row`, `spacer`. - `async function boot()` calls `loadTokens(); loadRevs(); loadUploads();`. ## Steps 1. `server/p2p-db.js` — append: ```js // Admin panel (plan 019): newest verified/revoked files with their holder counts. export async function recentContent(limit = 30) { const r = await db.execute({ sql: `SELECT c.cid, c.video_id, c.size, c.height, c.vcodec, c.origin, c.status, c.scan, c.verified_at, c.meta, (SELECT COUNT(*) FROM p2p_holders h WHERE h.cid = c.cid AND h.status = 'active') AS holders FROM p2p_content c ORDER BY c.verified_at DESC LIMIT ?`, args: [limit], }); return rowsOf(r).map((x) => ({ ...x, holders: Number(x.holders) || 0 })); } ``` 2. `server/server.js` — directly after the `const p2pRehydrate = createRehydrator({…});` statement add: ```js // Admin: P2P overview + revoke (frontend/admin.html → Peer-to-peer). app.get('/api/admin/p2p', notes.requireAdminOrToken, async (c) => c.json({ ok: true, config: { enabled: P2P.enabled, malwareScan: P2P.malwareScan, staleDays: P2P.staleDays, keepMinViews: P2P.keepMinViews, keepDays: P2P.keepDays, keepRecentDays: P2P.keepRecentDays, }, stats: { ...(await p2pDb.p2pStats()), online: p2pHub.onlineCount(), intakeOpen: intake.openTickets(), intakeActive: intake.active() }, recent: await p2pDb.recentContent(30), }, 200, { 'Cache-Control': 'no-store' })); app.post('/api/admin/p2p/revoke', notes.requireAdminOrToken, async (c) => { const body = await c.req.json().catch(() => ({})); const cid = String(body.cid || '').toLowerCase(); if (!/^[0-9a-f]{64}$/.test(cid)) return c.json({ ok: false, error: 'bad cid' }, 400); await p2pDb.revokeContent(cid); console.warn(`[p2p] admin revoked ${cid.slice(0, 12)}`); return c.json({ ok: true }); }); ``` 3. `frontend/admin.html` — directly BEFORE the "Recent edits" `
` insert: ```html

Peer-to-peer

Devices share verified copies with each other (docs/p2p-architecture.md). A file's hash is only added after the server itself hashed and validated it. The malware scan is off by default (P2P_MALWARE_SCAN=1 to enable). Holders are never expired — ones not re-checked for the stale period are only marked stale.

``` 4. `frontend/admin.html` script — directly ABOVE `async function boot() {` add: ```js async function loadP2p() { const j = await api('/api/admin/p2p'); if (!j.ok) { $('p2pSummary').textContent = j.error || 'P2P unavailable'; return; } const c = j.config, s = j.stats; $('p2pSummary').textContent = `P2P ${c.enabled ? 'ON' : 'OFF'} · malware scan ${c.malwareScan ? 'ON' : 'off'} · stale after ${c.staleDays} d · ` + `keep ≥${c.keepMinViews} views/${c.keepDays} d or played in ${c.keepRecentDays} d — ` + `${s.content} verified (${s.revoked} revoked) · ${s.devices} devices, ${s.online} online · ` + `${s.holders} holdings over ${s.heldCids} files · intake ${s.intakeActive} running / ${s.intakeOpen} open`; const mb = (b) => (Number(b) / 1048576).toFixed(1) + ' MB'; $('p2pTable').innerHTML = 'VideocidOriginScanSizeVerifiedHolders' + j.recent.map((r) => { let title = ''; try { title = JSON.parse(r.meta || '{}').title || ''; } catch { /* no meta */ } return `${esc(title || r.video_id)}
${esc(r.video_id)}` + `${esc(r.cid.slice(0, 12))}${esc(r.origin)}${esc(r.scan)}${mb(r.size)}` + `${esc(new Date(Number(r.verified_at)).toLocaleString())}${r.holders}` + `${r.status === 'verified' ? `` : 'revoked'}`; }).join(''); } $('p2pRefreshBtn').addEventListener('click', loadP2p); $('p2pTable').addEventListener('click', async (e) => { const b = e.target.closest('[data-revoke]'); if (!b || !confirm('Revoke this file? Devices will stop sharing it and the server will never accept it again.')) return; const j = await api('/api/admin/p2p/revoke', { method: 'POST', body: { cid: b.dataset.revoke } }); if (!j.ok) alert(j.error || 'failed'); loadP2p(); }); ``` 5. `frontend/admin.html` `boot()` — `loadUploads();` appears 3 times in the file; use ONLY the one inside `async function boot()`, i.e. this exact pair of lines: ```js loadUploads(); const want = videoIdFrom(new URLSearchParams(location.search).get('v') || ''); ``` and insert ` loadP2p();` between them. ## Out of scope / do NOT touch - No editing of config from the page (env vars stay the source of truth). - `admin.html` is never cached by the SW — no `sw.js` change. ## Verification ```bash cd /home/user/ytplayer/server && bun install >/dev/null 2>&1 && [ -e public ] || ln -s ../frontend public bun build server.js --target=bun --outdir=/tmp/ytp-check >/dev/null && echo SERVER_OK bun run test 2>&1 | grep -E "^ *[0-9]+ (pass|fail)" DBDIR=$(mktemp -d); DB_PATH=$DBDIR/t.db MEDIA_DIR=$DBDIR/media ADMIN_PASSWORD=test-pass PORT=3995 bun server.js >/tmp/ytp019.log 2>&1 & SRV=$!; sleep 4 curl -s -c /tmp/ytp019.jar -H 'Content-Type: application/json' -d '{"password":"test-pass"}' http://localhost:3995/api/admin/login echo curl -s -b /tmp/ytp019.jar http://localhost:3995/api/admin/p2p | head -c 400; echo curl -s -o /dev/null -w '%{http_code}\n' http://localhost:3995/api/admin/p2p curl -s -b /tmp/ytp019.jar -H 'Content-Type: application/json' -d '{"cid":"nope"}' http://localhost:3995/api/admin/p2p/revoke echo kill $SRV; true grep -c "loadP2p" ../frontend/admin.html ``` Expected: `SERVER_OK`; every test file `0 fail`; login `{"ok":true…}`; the p2p call returns `{"ok":true,"config":{"enabled":true,"malwareScan":false,"staleDays":7,…},"stats":{"content":0,…},"recent":[]}`; without the cookie `401`; bad cid `{"ok":false,"error":"bad cid"}`; grep ≥ `3`. ## 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: only the 3 planned files changed; `loadP2p` appears 4 times and is called between `loadUploads();` and `videoIdFrom(...)` inside `boot()`; all 15 server test files 0 fail earlier in the run and `SERVER_OK`. Real Chromium test on `/admin`: login, panel renders `P2P ON · malware scan off · stale after 7 d ...`, one seeded file listed with a Revoke button, clicking Revoke changed the summary from `1 verified (0 revoked)` to `0 verified (1 revoked)`, no JS errors. Unauthenticated `/api/admin/p2p` returns 401 (executor run). No leftover processes. - Executor Findings (verbatim): All 5 steps executed verbatim as specified in the plan. Build succeeded, all tests pass (92 total: 0 fail). Server endpoints respond correctly with proper authentication gating (401 unauthenticated), config/stats, and CID validation. P2P panel HTML section added before Recent edits. loadP2p() function added to script with event listeners and revoke logic. loadP2p() call inserted in boot() between loadUploads() and videoIdFrom(). Four occurrences of "loadP2p" in admin.html as expected.