From 84cd00368da44c6730e6562a66fa04f79855dcb3 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 30 Sep 2026 08:02:52 +0000 Subject: [PATCH] Add a P2P panel to the admin page --- frontend/admin.html | 44 +++++++++++++++++++ plans/INDEX.md | 2 +- .../019-admin-p2p-panel-4dc623.md | 6 +++ server/p2p-db.js | 11 +++++ server/server.js | 18 ++++++++ 5 files changed, 80 insertions(+), 1 deletion(-) rename plans/{active => done}/019-admin-p2p-panel-4dc623.md (87%) diff --git a/frontend/admin.html b/frontend/admin.html index 551eece..7266f92 100644 --- a/frontend/admin.html +++ b/frontend/admin.html @@ -187,6 +187,20 @@
+
+
+

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.

+
+
+
+

Recent edits

@@ -238,6 +252,35 @@ $('logoutBtn').classList.add('hidden'); } + 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(); + }); + async function boot() { const me = await api('/api/admin/me'); if (!me.enabled) { $('disabledBox').classList.remove('hidden'); return; } @@ -249,6 +292,7 @@ loadTokens(); loadRevs(); loadUploads(); + loadP2p(); const want = videoIdFrom(new URLSearchParams(location.search).get('v') || ''); if (want) leOpen(want); } diff --git a/plans/INDEX.md b/plans/INDEX.md index ee7b412..c82cabc 100644 --- a/plans/INDEX.md +++ b/plans/INDEX.md @@ -26,4 +26,4 @@ green, app boots with no JS errors, P2P on by default, offline boot works). | 016 | 016-intake-and-server-verification-cfe031 | Let a device hand a file to the server for hashing and validation | done | Let a device hand a file to the server for hashing and validation | needs ffmpeg for tests | | 017 | 017-peer-transfer-1faaa7 | Download a verified file from another device over WebRTC | done | Download a verified file from another device over WebRTC | STUN only | | 018 | 018-server-rehydrate-from-peer-4fb8bd | Restore an evicted server copy from an online holder | done | Restore an evicted server copy from an online holder | | -| 019 | 019-admin-p2p-panel-4dc623 | Add a P2P panel to the admin page | in-progress | | | +| 019 | 019-admin-p2p-panel-4dc623 | Add a P2P panel to the admin page | done | Add a P2P panel to the admin page | | diff --git a/plans/active/019-admin-p2p-panel-4dc623.md b/plans/done/019-admin-p2p-panel-4dc623.md similarity index 87% rename from plans/active/019-admin-p2p-panel-4dc623.md rename to plans/done/019-admin-p2p-panel-4dc623.md index 7e6a37a..a41395e 100644 --- a/plans/active/019-admin-p2p-panel-4dc623.md +++ b/plans/done/019-admin-p2p-panel-4dc623.md @@ -167,3 +167,9 @@ Output ONLY the following, no other prose: 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. diff --git a/server/p2p-db.js b/server/p2p-db.js index 7a7784f..d329f2a 100644 --- a/server/p2p-db.js +++ b/server/p2p-db.js @@ -268,3 +268,14 @@ export async function findMediaByCid(cid) { }); return rowsOf(r)[0] || null; } + +// 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 })); +} diff --git a/server/server.js b/server/server.js index 9128c5e..a7bc571 100644 --- a/server/server.js +++ b/server/server.js @@ -2008,6 +2008,24 @@ const p2pRehydrate = createRehydrator({ p2pDb, hub: p2pHub, enabled: () => P2P.enabled, hasServerCopy: async (id) => !!(await media.getReady(id).catch(() => null)), }); +// 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 }); +}); // ============================================================================ // GET /sw.js — serve the service worker with BUILD_TAG injected