Add a P2P panel to the admin page
This commit is contained in:
175
plans/done/019-admin-p2p-panel-4dc623.md
Normal file
175
plans/done/019-admin-p2p-panel-4dc623.md
Normal file
@@ -0,0 +1,175 @@
|
||||
---
|
||||
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 `<section>…</section>` inside `<div id="app">`; the "Recent edits" section
|
||||
(~line 190) starts with
|
||||
```html
|
||||
<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.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" `<section>` insert:
|
||||
```html
|
||||
<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>
|
||||
|
||||
```
|
||||
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 = '<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();
|
||||
});
|
||||
```
|
||||
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.
|
||||
Reference in New Issue
Block a user