Queue executable speed and peer-to-peer sharing plans with tested patches and harnesses
This commit is contained in:
169
plans/queue/019-admin-p2p-panel-4dc623.md
Normal file
169
plans/queue/019-admin-p2p-panel-4dc623.md
Normal file
@@ -0,0 +1,169 @@
|
||||
---
|
||||
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.
|
||||
Reference in New Issue
Block a user