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:
17
lib/db/repos/app_settings.js
Normal file
17
lib/db/repos/app_settings.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import { pool } from '../pool.js';
|
||||
|
||||
// Generic owner-scoped key→jsonb settings store. Used by the speedtest schedule
|
||||
// and (later) the theming panel. Keep values small + JSON-serialisable.
|
||||
export async function get(key, fallback = null) {
|
||||
const { rows } = await pool.query(`SELECT value FROM app_settings WHERE key = $1`, [key]);
|
||||
return rows[0] ? rows[0].value : fallback;
|
||||
}
|
||||
|
||||
export async function set(key, value) {
|
||||
const { rows } = await pool.query(
|
||||
`INSERT INTO app_settings (key, value, updated_at) VALUES ($1, $2::jsonb, now())
|
||||
ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = now()
|
||||
RETURNING value`,
|
||||
[key, JSON.stringify(value)]);
|
||||
return rows[0].value;
|
||||
}
|
||||
@@ -1,12 +1,51 @@
|
||||
import { pool } from '../pool.js';
|
||||
export async function record({ down_mbps, up_mbps, ping_ms = null }) {
|
||||
|
||||
export async function record(r = {}) {
|
||||
const { rows } = await pool.query(
|
||||
`INSERT INTO speedtest_results (down_mbps, up_mbps, ping_ms) VALUES ($1,$2,$3) RETURNING *`,
|
||||
[down_mbps, up_mbps, ping_ms]);
|
||||
`INSERT INTO speedtest_results
|
||||
(down_mbps, up_mbps, ping_ms, jitter_ms, packet_loss, server_name, server_id,
|
||||
isp, result_url, down_bytes, up_bytes, ok, error)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13) RETURNING *`,
|
||||
[r.down_mbps ?? null, r.up_mbps ?? null, r.ping_ms ?? null, r.jitter_ms ?? null,
|
||||
r.packet_loss ?? null, r.server_name ?? null, r.server_id ?? null, r.isp ?? null,
|
||||
r.result_url ?? null, r.down_bytes ?? null, r.up_bytes ?? null, r.ok ?? true, r.error ?? null]);
|
||||
return rows[0];
|
||||
}
|
||||
|
||||
export async function history(limit = 30) {
|
||||
const { rows } = await pool.query(
|
||||
`SELECT * FROM speedtest_results ORDER BY ran_at DESC LIMIT $1`, [limit]);
|
||||
return rows;
|
||||
}
|
||||
|
||||
// Rows within the last N hours (ascending for charting), capped.
|
||||
export async function range(hours = 168, limit = 1000) {
|
||||
const { rows } = await pool.query(
|
||||
`SELECT * FROM (
|
||||
SELECT * FROM speedtest_results
|
||||
WHERE ran_at >= now() - ($1 || ' hours')::interval
|
||||
ORDER BY ran_at DESC LIMIT $2
|
||||
) t ORDER BY ran_at ASC`, [hours, limit]);
|
||||
return rows;
|
||||
}
|
||||
|
||||
export async function latest() {
|
||||
const { rows } = await pool.query(
|
||||
`SELECT * FROM speedtest_results WHERE ok ORDER BY ran_at DESC LIMIT 1`);
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
export async function stats(hours = 24) {
|
||||
const { rows } = await pool.query(
|
||||
`SELECT count(*) FILTER (WHERE ok) AS n,
|
||||
count(*) FILTER (WHERE NOT ok) AS failures,
|
||||
avg(down_mbps) FILTER (WHERE ok) AS avg_down,
|
||||
min(down_mbps) FILTER (WHERE ok) AS min_down,
|
||||
max(down_mbps) FILTER (WHERE ok) AS max_down,
|
||||
avg(up_mbps) FILTER (WHERE ok) AS avg_up,
|
||||
avg(ping_ms) FILTER (WHERE ok) AS avg_ping,
|
||||
max(ping_ms) FILTER (WHERE ok) AS max_ping
|
||||
FROM speedtest_results
|
||||
WHERE ran_at >= now() - ($1 || ' hours')::interval`, [hours]);
|
||||
return rows[0];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user