diff --git a/deno.json b/deno.json index 8b224c32..4ba9ca7c 100644 --- a/deno.json +++ b/deno.json @@ -14,6 +14,7 @@ "compile:macos-arm": "deno compile --allow-all --no-check --include build --include server/integrations --include server/templates --target aarch64-apple-darwin --output myspeed-macos-arm server/index.js" }, "imports": { + "cron-parser": "npm:cron-parser@^5.5.0", "express": "npm:express@^5.2.1", "sequelize": "npm:sequelize@^6.37.7", "sqlite3": "npm:sqlite3@^5.1.7", diff --git a/server/controller/config.js b/server/controller/config.js index eca6f9f2..8f4a03cf 100644 --- a/server/controller/config.js +++ b/server/controller/config.js @@ -21,6 +21,7 @@ const configDefaults = { download: "100", upload: "50", cron: "0 * * * *", + scheduleOffset: "true", provider: "none", ooklaId: "none", libreId: "none", @@ -117,6 +118,9 @@ export const validateInput = async (key, value) => { if (key === "cron" && !cron.isValidCron(value.toString())) return "Not a valid cron expression"; + if (key === "scheduleOffset" && !["true", "false"].includes(value)) + return "You need to provide either true or false"; + if (key === "interface" && !Object.keys(interfaces.interfaces).includes(value)) return "The provided interface does not exist"; diff --git a/server/routes/config.js b/server/routes/config.js index 3eaae85a..2ff30b57 100644 --- a/server/routes/config.js +++ b/server/routes/config.js @@ -8,7 +8,7 @@ const app = express.Router(); app.get("/", password(true), async (req, res) => { let configValues = {}; (await config.listAll()).forEach(row => { - if (row.key !== "password" && !(req.viewMode && ["ooklaId", "libreId", "libreUrl", "cron", "passwordLevel"].includes(row.key))) + if (row.key !== "password" && !(req.viewMode && ["ooklaId", "libreId", "libreUrl", "cron", "scheduleOffset", "passwordLevel"].includes(row.key))) configValues[row.key] = row.value; }); configValues['viewMode'] = req.viewMode; diff --git a/server/tasks/timer.js b/server/tasks/timer.js index 981c8ee5..faa74d22 100644 --- a/server/tasks/timer.js +++ b/server/tasks/timer.js @@ -1,12 +1,44 @@ import * as pauseController from '../controller/pause.js'; +import * as config from '../controller/config.js'; import schedule from 'node-schedule'; import { isValidCron } from "cron-validator"; +import { CronExpressionParser } from "cron-parser"; import { create as createSpeedtest } from './speedtest.js'; let job; +let currentCron; + +const calculateMaxDelay = (cron) => { + try { + const parser = CronExpressionParser.parse(cron); + const next1 = parser.next().getTime(); + const next2 = parser.next().getTime(); + const intervalMs = next2 - next1; + const intervalMinutes = intervalMs / 60000; + + if (intervalMinutes <= 1) { + return 30 * 1000; // 30 seconds + } else if (intervalMinutes <= 30) { + return 2 * 60 * 1000; // 2 minutes + } else if (intervalMinutes <= 60) { + return 3 * 60 * 1000; // 3 minutes + } else { + return 5 * 60 * 1000; // 5 minutes + } + } catch { + return 2 * 60 * 1000; // Default to 2 minutes if parsing fails + } +}; + +const getRandomDelay = (cron) => { + const minDelay = 30 * 1000; + const maxDelay = calculateMaxDelay(cron); + return Math.floor(Math.random() * (maxDelay - minDelay + 1)) + minDelay; +}; export const startTimer = (cron) => { if (!isValidCron(cron)) return; + currentCron = cron; job = schedule.scheduleJob(cron, () => runTask()); }; @@ -16,6 +48,19 @@ export const runTask = async () => { return; } + const scheduleOffset = await config.getValue("scheduleOffset"); + + if (scheduleOffset === "true" && currentCron) { + const delay = getRandomDelay(currentCron); + console.log(`Schedule offset enabled. Delaying speedtest by ${Math.round(delay / 1000)} seconds...`); + await new Promise(resolve => setTimeout(resolve, delay)); + + if (pauseController.currentState) { + console.warn("Speedtests paused during delay. Skipping this test..."); + return; + } + } + await createSpeedtest("auto"); };