feat(speedtest): full speedtest-tracker-style automation (2.9.0)

Switch worker to the Ookla CLI (jitter, packet loss, server, ISP,
shareable result URL, bytes). Migration 028 enriches speedtest_results
+ adds a generic app_settings store. New /speedtest page: KPIs,
throughput + latency charts, window stats, configurable schedule
(reschedulable cron) & low-speed alert threshold, history table.
SV card gains ping/jitter + a link through to the page.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
root
2026-06-09 22:55:04 +10:00
parent 600057582e
commit 359ae21d59
14 changed files with 405 additions and 29 deletions

View File

@@ -6,18 +6,42 @@ const pexec = promisify(execFile);
export const NAME = 'speedtest';
// Default runner uses speedtest-cli --json (bits/s → Mbps). Swap binary/flags
// here if the box has the Ookla `speedtest -f json` CLI instead.
async function defaultRunner() {
const { stdout } = await pexec('speedtest-cli', ['--json'], { timeout: 120000 });
// Ookla CLI gives the full metric set (jitter, packet loss, server, ISP,
// shareable result URL). Override the binary via SPEEDTEST_BIN if needed.
const OOKLA_BIN = process.env.SPEEDTEST_BIN || 'ookla-speedtest';
async function ooklaRunner() {
const { stdout } = await pexec(OOKLA_BIN,
['-f', 'json', '--accept-license', '--accept-gdpr'], { timeout: 120000 });
const j = JSON.parse(stdout);
return { down_mbps: j.download / 1e6, up_mbps: j.upload / 1e6, ping_ms: j.ping };
const mbps = bw => (Number(bw) || 0) * 8 / 1e6; // Ookla bandwidth is bytes/s
return {
down_mbps: mbps(j.download?.bandwidth),
up_mbps: mbps(j.upload?.bandwidth),
ping_ms: j.ping?.latency ?? null,
jitter_ms: j.ping?.jitter ?? null,
packet_loss: j.packetLoss ?? null,
server_name: j.server ? [j.server.name, j.server.location].filter(Boolean).join(' · ') : null,
server_id: j.server?.id != null ? String(j.server.id) : null,
isp: j.isp ?? null,
result_url: j.result?.url ?? null,
down_bytes: j.download?.bytes ?? null,
up_bytes: j.upload?.bytes ?? null,
ok: true
};
}
let runner = defaultRunner;
let runner = ooklaRunner;
export function _setRunner(fn) { runner = fn; }
export async function handler(_job) {
const r = await runner();
await repo.record(r);
log.info(r, 'speedtest recorded');
try {
const r = await runner();
const saved = await repo.record(r);
log.info({ down: r.down_mbps, up: r.up_mbps, ping: r.ping_ms }, 'speedtest recorded');
return saved;
} catch (e) {
await repo.record({ ok: false, error: String(e?.message || e).slice(0, 300) });
log.error({ err: e }, 'speedtest failed');
throw e;
}
}