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:
commit
bc1be49a37
12 changed files with 2677 additions and 0 deletions
135
server/simulator.js
Normal file
135
server/simulator.js
Normal 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),
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue