Initial commit: NAUT torrent web UI with stubbed server

Advanced torrent client web UI aimed at power users, with a
zero-dependency Node stub server (built-in http + SSE) serving live
mock data.

- Dense sortable/multi-select torrent grid with live updates
- Detail panel: General/Trackers/Peers/Content/Pieces (resizable)
- Sidebar filters: status, categories, tags, trackers
- Create/delete categories and tags (sidebar + right-click)
- Add via magnet or client-side-parsed .torrent upload
- RSS, integrated search, and read-only engine views
- Hand-drawn SVG icon set, dark theme, keyboard shortcuts

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ookami125 2026-06-16 21:15:31 -04:00
commit bc1be49a37
12 changed files with 2677 additions and 0 deletions

135
server/simulator.js Normal file
View file

@ -0,0 +1,135 @@
// Mutates the in-memory fleet over time so the UI shows live activity:
// speeds fluctuate, downloads progress, pieces fill in, ETA/ratio drift,
// session totals accumulate, and the occasional torrent completes.
import { db, constants } from './data.js';
const { KiB, MiB } = constants;
function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); }
function jitter(v, frac) { return v * (1 + (Math.random() * 2 - 1) * frac); }
const DL_STATES = new Set(['downloading', 'forcedDL', 'metaDL']);
const UP_STATES = new Set(['uploading', 'forcedUP']);
export function tick() {
const now = Date.now();
for (const t of db.torrents) {
const isDL = DL_STATES.has(t.state);
const isUP = UP_STATES.has(t.state) || isDL;
// --- speeds ---
if (isDL) {
const ceiling = t.downLimit > 0 ? t.downLimit : 18 * MiB;
t.dlspeed = clamp(Math.round(jitter(t.dlspeed || 500 * KiB, 0.4)), 20 * KiB, ceiling);
// occasionally stall
if (Math.random() < 0.03) { t.state = 'stalledDL'; t.dlspeed = 0; }
} else if (t.state === 'stalledDL') {
t.dlspeed = 0;
if (Math.random() < 0.08) { t.state = 'downloading'; t.dlspeed = 800 * KiB; }
} else {
t.dlspeed = 0;
}
if (isUP) {
const ceiling = t.upLimit > 0 ? t.upLimit : 6 * MiB;
t.upspeed = clamp(Math.round(jitter(t.upspeed || 100 * KiB, 0.5)), 0, ceiling);
} else {
t.upspeed = 0;
}
// --- progress ---
if (isDL && t.progress < 1 && t.dlspeed > 0) {
const gained = t.dlspeed; // ~1s of bytes
t.downloaded += gained;
t.downloadedSession += gained;
t.progress = clamp(t.progress + gained / t.size, 0, 1);
// fill some pieces proportionally
const targetDone = Math.floor(t.pieceCount * t.progress);
let done = 0;
for (const p of t.pieces) if (p === 2) done++;
let toFill = targetDone - done;
for (let i = 0; i < t.pieces.length && toFill > 0; i++) {
if (t.pieces[i] !== 2) {
if (t.pieces[i] === 1 || Math.random() < 0.5) { t.pieces[i] = 2; toFill--; }
else t.pieces[i] = 1;
}
}
if (t.state === 'metaDL' && t.progress > 0.01) t.state = 'downloading';
// completion
if (t.progress >= 1) {
t.progress = 1;
t.state = 'uploading';
t.completionOn = now;
t.dlspeed = 0;
for (let i = 0; i < t.pieces.length; i++) t.pieces[i] = 2;
}
}
// --- uploaded / ratio ---
if (t.upspeed > 0) {
t.uploaded += t.upspeed;
t.uploadedSession += t.upspeed;
}
t.ratio = t.downloaded > 0 ? t.uploaded / t.downloaded : (t.uploaded > 0 ? 9999 : 0);
// --- eta ---
t.eta = isDL && t.dlspeed > 0
? Math.floor((t.size - t.downloaded) / t.dlspeed)
: 8640000;
// --- swarm drift ---
if (!t.state.startsWith('paused')) {
t.seeds = clamp(t.seeds + (Math.random() < 0.5 ? -1 : 1) * (Math.random() < 0.3 ? 1 : 0), 0, t.seedsTotal);
t.peers = clamp(t.peers + (Math.random() < 0.5 ? -1 : 1) * (Math.random() < 0.3 ? 1 : 0), 0, t.peersTotal);
t.lastActivity = now;
t.timeActive += 1;
}
// --- peer speeds drift ---
for (const p of t.peersList) {
if (p.dlspeed) p.dlspeed = clamp(Math.round(jitter(p.dlspeed, 0.5)), 0, 4 * MiB);
if (p.upspeed) p.upspeed = clamp(Math.round(jitter(p.upspeed, 0.5)), 0, 800 * KiB);
if (p.progress < 1) p.progress = clamp(p.progress + Math.random() * 0.01, 0, 1);
}
}
}
// Global session/server stats derived from the fleet.
export function globalStats() {
let dl = 0, up = 0, dlSession = 0, upSession = 0, totalDown = 0, totalUp = 0;
let active = 0;
for (const t of db.torrents) {
dl += t.dlspeed;
up += t.upspeed;
dlSession += t.downloadedSession;
upSession += t.uploadedSession;
totalDown += t.downloaded;
totalUp += t.uploaded;
if (t.dlspeed > 0 || t.upspeed > 0) active++;
}
const p = db.preferences;
return {
dl_info_speed: dl,
up_info_speed: up,
dl_info_data: dlSession,
up_info_data: upSession,
dl_rate_limit: p.alt_speed_enabled ? p.alt_dl_limit : p.dl_limit,
up_rate_limit: p.alt_speed_enabled ? p.alt_up_limit : p.up_limit,
alt_speed_enabled: p.alt_speed_enabled,
global_ratio: totalDown > 0 ? totalUp / totalDown : 0,
dht_nodes: 312 + Math.floor(Math.random() * 40),
connection_status: 'connected',
listen_port: p.listen_port,
free_space: 812 * 1024 * 1024 * 1024 + Math.floor(Math.random() * 1e9),
active_torrents: active,
total_torrents: db.torrents.length,
queued_io_jobs: Math.floor(Math.random() * 4),
read_cache_hits: (88 + Math.random() * 8).toFixed(1),
total_buffer_size: 16 * MiB,
average_time_queue: Math.floor(Math.random() * 12),
};
}