9.7 KiB
9.7 KiB
id, title, created, depends_on, est_files
| id | title | created | depends_on | est_files | |
|---|---|---|---|---|---|
| 019-admin-p2p-panel-4dc623 | Add a P2P panel to the admin page | 2026-09-29 |
|
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
verifiedcids).
Context the executor must NOT rediscover
- Admin auth middleware:
notes.requireAdminOrToken(returned byregisterNoteRoutes,server/server.js~line 1823). Used likeapp.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.jshasrowsOf,p2pStats(),revokeContent(cid).frontend/admin.html:- sections are
<section>…</section>inside<div id="app">; the "Recent edits" section (~line 190) starts with<section> <div class="row" style="margin-bottom:10px"> <h2 style="margin:0">Recent edits</h2> - helpers inside the script IIFE:
$(id),esc(s),api(path, opts)(JSON in/out,opts.bodyis JSON-encoded), classestbl,scroll,muted,row,spacer. async function boot()callsloadTokens(); loadRevs(); loadUploads();.
- sections are
Steps
server/p2p-db.js— append:// 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 })); }server/server.js— directly after theconst p2pRehydrate = createRehydrator({…});statement add:// 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 }); });frontend/admin.html— directly BEFORE the "Recent edits"<section>insert:<section> <div class="row" style="margin-bottom:10px"> <h2 style="margin:0">Peer-to-peer</h2> <span class="spacer"></span> <button id="p2pRefreshBtn">↻ Refresh</button> </div> <p class="muted">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 (<code>P2P_MALWARE_SCAN=1</code> to enable). Holders are never expired — ones not re-checked for the stale period are only marked stale.</p> <div id="p2pSummary" class="msg"></div> <div class="scroll"><table class="tbl" id="p2pTable"></table></div> </section>frontend/admin.htmlscript — directly ABOVEasync function boot() {add: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 = '<tr><th>Video</th><th>cid</th><th>Origin</th><th>Scan</th><th>Size</th><th>Verified</th><th>Holders</th><th></th></tr>' + j.recent.map((r) => { let title = ''; try { title = JSON.parse(r.meta || '{}').title || ''; } catch { /* no meta */ } return `<tr${r.status === 'revoked' ? ' class="muted"' : ''}><td>${esc(title || r.video_id)}<br><small>${esc(r.video_id)}</small></td>` + `<td><code>${esc(r.cid.slice(0, 12))}</code></td><td>${esc(r.origin)}</td><td>${esc(r.scan)}</td><td>${mb(r.size)}</td>` + `<td>${esc(new Date(Number(r.verified_at)).toLocaleString())}</td><td>${r.holders}</td>` + `<td>${r.status === 'verified' ? `<button data-revoke="${esc(r.cid)}">Revoke</button>` : 'revoked'}</td></tr>`; }).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(); });frontend/admin.htmlboot()—loadUploads();appears 3 times in the file; use ONLY the one insideasync function boot(), i.e. this exact pair of lines:and insertloadUploads(); const want = videoIdFrom(new URLSearchParams(location.search).get('v') || '');loadP2p();between them.
Out of scope / do NOT touch
- No editing of config from the page (env vars stay the source of truth).
admin.htmlis never cached by the SW — nosw.jschange.
Verification
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:
git diff(unified) of all changes.- Raw output of the Verification commands.
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;
loadP2pappears 4 times and is called betweenloadUploads();andvideoIdFrom(...)insideboot(); all 15 server test files 0 fail earlier in the run andSERVER_OK. Real Chromium test on/admin: login, panel rendersP2P ON · malware scan off · stale after 7 d ..., one seeded file listed with a Revoke button, clicking Revoke changed the summary from1 verified (0 revoked)to0 verified (1 revoked), no JS errors. Unauthenticated/api/admin/p2preturns 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.