diff --git a/client/src/common/components/Dropdown/DropdownComponent.jsx b/client/src/common/components/Dropdown/DropdownComponent.jsx
index e1812b19..94bb8780 100644
--- a/client/src/common/components/Dropdown/DropdownComponent.jsx
+++ b/client/src/common/components/Dropdown/DropdownComponent.jsx
@@ -3,7 +3,7 @@ import "./styles.sass";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {
faArrowDown,
- faArrowUp, faClock, faClose, faFileExport,
+ faArrowUp, faCalendarDays, faClock, faClose, faFileExport,
faGear, faInfo,
faKey,
faPause,
@@ -14,6 +14,7 @@ import {
import {ConfigContext} from "@/common/contexts/Config";
import {StatusContext} from "@/common/contexts/Status";
import {DialogContext} from "@/common/contexts/Dialog";
+import {SpeedtestContext} from "@/common/contexts/Speedtests";
let icon;
@@ -33,6 +34,7 @@ function DropdownComponent() {
const [config, reloadConfig] = useContext(ConfigContext);
const [status, updateStatus] = useContext(StatusContext);
+ const updateTests = useContext(SpeedtestContext)[1];
const [setDialog] = useContext(DialogContext);
let headers = localStorage.getItem("password") ? {password: localStorage.getItem("password")} : {};
@@ -236,6 +238,26 @@ function DropdownComponent() {
});
}
+ const updateTime = async () => {
+ toggleDropdown();
+ setDialog({
+ title: "Zeige Tests der letzten ...",
+ select: true,
+ selectOptions: {
+ 1: "24 Stunden (Standard)",
+ 2: "2 Tage (Insgesamt)",
+ 3: "7 Tage (Durchschnitt)",
+ 4: "30 Tage (Durchschnitt)"
+ },
+ value: localStorage.getItem("testTime") || 1,
+ onSuccess: value => {
+ localStorage.setItem("testTime", value);
+ updateTests();
+ showFeedback(undefined, false);
+ }
+ });
+ }
+
return (
@@ -272,6 +294,10 @@ function DropdownComponent() {
Häufigkeit einstellen
+
+
+
Zeitraum festlegen
+
Tests exportieren
diff --git a/client/src/common/contexts/Speedtests/SpeedtestContext.jsx b/client/src/common/contexts/Speedtests/SpeedtestContext.jsx
index 1064a795..16a5c36e 100644
--- a/client/src/common/contexts/Speedtests/SpeedtestContext.jsx
+++ b/client/src/common/contexts/Speedtests/SpeedtestContext.jsx
@@ -6,9 +6,24 @@ export const SpeedtestProvider = (props) => {
const [speedtests, setSpeedtests] = useState({});
+ const generatePath = (level) => {
+ switch (level) {
+ case 1:
+ return "?hours=24";
+ case 2:
+ return "?hours=48";
+ case 3:
+ return "/averages?days=7";
+ case 4:
+ return "/averages?days=30";
+ }
+ }
+
const updateTests = () => {
let passwordHeaders = localStorage.getItem("password") ? {password: localStorage.getItem("password")} : {}
- fetch("/api/speedtests", {headers: passwordHeaders})
+ let testTime = localStorage.getItem("testTime") || 1;
+
+ fetch("/api/speedtests" + generatePath(parseInt(testTime)), {headers: passwordHeaders})
.then(res => res.json())
.then(tests => setSpeedtests(tests))
}
diff --git a/client/src/pages/Home/components/LatestTest/LatestTestComponent.jsx b/client/src/pages/Home/components/LatestTest/LatestTestComponent.jsx
index 503af61e..963766a3 100644
--- a/client/src/pages/Home/components/LatestTest/LatestTestComponent.jsx
+++ b/client/src/pages/Home/components/LatestTest/LatestTestComponent.jsx
@@ -21,7 +21,7 @@ function LatestTestComponent() {
useEffect(() => {
if (latest) setLatestTestTime(generateRelativeTime(latest.created));
- const interval = setInterval(() => setLatestTestTime(generateRelativeTime(latest.created || 0)), 1000);
+ const interval = setInterval(() => setLatestTestTime(generateRelativeTime(latest ? latest.created : 0)), 1000);
return () => clearInterval(interval);
}, [latest]);
diff --git a/client/src/pages/Home/components/Speedtest/SpeedtestComponent.jsx b/client/src/pages/Home/components/Speedtest/SpeedtestComponent.jsx
index a1ec770a..ad408a6e 100644
--- a/client/src/pages/Home/components/Speedtest/SpeedtestComponent.jsx
+++ b/client/src/pages/Home/components/Speedtest/SpeedtestComponent.jsx
@@ -22,6 +22,12 @@ const errors = {
"Could not retrieve or read configuration": "Die Konfigurationsdatei konnte nicht geladen werden",
}
+const tooltips = {
+ custom: "Benutzerdefiniert",
+ average: "Durchschnitt",
+ auto: "Automatisiert",
+}
+
function SpeedtestComponent(props) {
const [setDialog] = useContext(DialogContext);
@@ -31,40 +37,69 @@ function SpeedtestComponent(props) {
let errorMessage = "Unbekannter Fehler: " + props.error;
+ let timeString;
+ if (props.type === "average") {
+ timeString = String(props.time.getDate()).padStart(2, '0') + "." + String(props.time.getMonth() + 1).padStart(2, '0');
+ } else {
+ timeString = String(props.time.getHours()).padStart(2, '0') + ":" + String(props.time.getMinutes()).padStart(2, '0');
+ }
+
if (props.error) {
for (let errorsKey in errors)
if (props.error.includes(errorsKey)) errorMessage = errors[errorsKey];
}
+ const showErrorDialog = () => {
+ setDialog({
+ title: "Test fehlgeschlagen",
+ description: errorMessage + ". Bitte überprüfe weitestgehend, ob das öfters passiert.",
+ buttonText: "Okay",
+ unsetButton: true,
+ unsetButtonText: "Test löschen",
+ onClear: () => fetch("/api/speedtests/" + props.id, {headers: passwordHeaders, method: "DELETE"})
+ .then(updateTests)
+ });
+ }
+
+ const showInfoDialog = () => {
+ if (props.type === "average") {
+ setDialog({
+ title: "Durchschnittsgeschwindigkeit",
+ buttonText: "Okay",
+ description: <>{props.amount} Tests haben ergeben, dass am {timeString} eine durchschnittliche Downloadgeschwindigkeit von {props.down} Mbit/s und eine Upload-geschwindigkeit von {props.up} Mbit/s bestand. Die Tests dauerten im Durchschnitt {props.duration} Sekunden.>
+ });
+ } else {
+ setDialog({
+ title: "Testergebnis",
+ description: <>Dieser Test erreichte eine maximale Downloadgeschwindigkeit von {props.down} Mbit/s
+ und eine maximale Uploadgeschwindigkeit von {props.up} Mbit/s.
+ Er wurde {props.type === "custom"
+ ? "von dir" : "automatisch"} angelegt und hat {props.duration} Sekunden gedauert.>,
+ buttonText: "Okay",
+ unsetButton: true,
+ unsetButtonText: "Test löschen",
+ onClear: () => fetch("/api/speedtests/" + props.id, {headers: passwordHeaders, method: "DELETE"})
+ .then(updateTests)
+ });
+ }
+ }
+
return (
setDialog({
- title: "Test fehlgeschlagen",
- description: errorMessage + ". Bitte überprüfe weitestgehend, ob das öfters passiert.",
- buttonText: "Okay",
- unsetButton: true,
- unsetButtonText: "Test löschen",
- onClear: () => fetch("/api/speedtests/"+props.id, {headers: passwordHeaders, method: "DELETE"})
- .then(updateTests)
- }) : () => setDialog({
- title: "Testergebnis",
- description: <>Dieser Test erreichte eine maximale Downloadgeschwindigkeit von {props.down} Mbit/s
- und eine maximale Uploadgeschwindigkeit von {props.up} Mbit/s. Er wurde {props.type === "custom"
- ? "von dir" : "automatisch"} angelegt und hat {props.duration} Sekunden gedauert.>,
- buttonText: "Okay",
- unsetButton: true,
- unsetButtonText: "Test löschen",
- onClear: () => fetch("/api/speedtests/"+props.id, {headers: passwordHeaders, method: "DELETE"})
- .then(updateTests)
- })} />
- {props.type === "custom" ? "Benutzerdefiniert" :"Automatisiert"}
+ onClick={props.error ? showErrorDialog : showInfoDialog}/>
+ {tooltips[props.type]}
-
-
Um {props.time}
+
{(props.type === "average" ? "Am " : "Um ") + timeString}
>)
+ if (Object.entries(config).length === 0) return (<>>);
return (
{speedtests.map ? speedtests.map(test => {
let date = new Date(Date.parse(test.created));
- let timeString = String(date.getHours()).padStart(2, '0') + ":" + String(date.getMinutes()).padStart(2, '0');
- return
}) : ""}
diff --git a/server/controller/speedtests.js b/server/controller/speedtests.js
index 29ed46f5..1c54c173 100644
--- a/server/controller/speedtests.js
+++ b/server/controller/speedtests.js
@@ -15,16 +15,63 @@ module.exports.get = async (id) => {
}
// Lists all speedtests from the database
-module.exports.list = async () => {
- let dbEntries = await tests.findAll({order: [["created", "DESC"]]});
- let all = [];
+module.exports.list = async (hours = 24) => {
+ let dbEntries = (await tests.findAll({order: [["created", "DESC"]]}))
+ .filter((entry) => new Date(entry.created) > new Date().getTime() - hours * 3600000);
+ for (let dbEntry of dbEntries)
+ if (dbEntry.error === null) delete dbEntry.error
+
+ return dbEntries;
+}
+
+// Lists all speedtests from the database grouped by days
+module.exports.listByDays = async (days) => {
+ let dbEntries = (await tests.findAll({order: [["created", "DESC"]]})).filter((entry) => entry.error === null)
+ .filter((entry) => new Date(entry.created) > new Date().getTime() - days * 24 * 3600000);
+
+ let averages = {};
dbEntries.forEach((entry) => {
- if (entry.error === null) delete entry.error
- all.push(entry);
+ const day = new Date(entry.created).toLocaleDateString();
+ if (!averages[day]) averages[day] = [];
+ averages[day].push(entry);
});
- return all;
+ return averages;
+}
+
+// Calculates the average speedtests and lists them
+module.exports.listAverage = async (days) => {
+ const averages = await this.listByDays(days);
+ let result = [];
+
+ if (Object.keys(averages).length !== 0)
+ result.push(averages[Object.keys(averages)[0]][0]);
+
+ for (let day in averages) {
+ let avgNumbers = {ping: 0, down: 0, up: 0, time: 0};
+ let currentDay = averages[day];
+
+ currentDay.forEach((current) => {
+ avgNumbers.ping += current.ping;
+ avgNumbers.down += current.download;
+ avgNumbers.up += current.upload;
+ avgNumbers.time += current.time;
+ });
+
+ const created = new Date(currentDay[0].created);
+ result.push({
+ ping: Math.round(avgNumbers["ping"] / currentDay.length),
+ download: parseFloat((avgNumbers["down"] / currentDay.length).toFixed(2)),
+ upload: parseFloat((avgNumbers["up"] / currentDay.length).toFixed(2)),
+ time: Math.round(avgNumbers["time"] / currentDay.length),
+ type: "average",
+ amount: currentDay.length,
+ created: created.getFullYear() + "-" + (created.getMonth() + 1) + "-" + created.getDate()
+ });
+ }
+
+ return result;
}
// Gets the latest speedtest from the database
@@ -41,12 +88,12 @@ module.exports.delete = async (id) => {
return true;
}
-// Removes speedtests older than 24 hours
+// Removes speedtests older than 30 days
module.exports.removeOld = async () => {
await tests.destroy({
where: {
created: {
- [Op.lte]: Sequelize.literal(`datetime('now', '-1 day')`)
+ [Op.lte]: Sequelize.literal(`datetime('now', '-30 days')`)
}
}
});
diff --git a/server/routes/speedtests.js b/server/routes/speedtests.js
index 85691bad..e5285bf2 100644
--- a/server/routes/speedtests.js
+++ b/server/routes/speedtests.js
@@ -4,7 +4,12 @@ const pauseController = require('../controller/pause');
// List all speedtests
app.get("/", async (req, res) => {
- res.json(await tests.list());
+ res.json(await tests.list(req.query.hours || 24));
+});
+
+// List all speedtests by average
+app.get("/averages", async (req, res) => {
+ res.json(await tests.listAverage(req.query.days || 7));
});
// Runs a speedtest
diff --git a/server/tasks/speedtest.js b/server/tasks/speedtest.js
index 8263cdc4..414fb0de 100644
--- a/server/tasks/speedtest.js
+++ b/server/tasks/speedtest.js
@@ -50,7 +50,7 @@ module.exports.create = async (type = "auto", retried = false) => {
let upload = roundSpeed(test.upload.bytes, test.upload.elapsed);
let time = Math.round((test.download.elapsed + test.upload.elapsed) / 1000);
let testResult = await tests.create(ping, download, upload, time, type);
- console.log(`Test #${testResult} was executed successfully. 🏓 ${ping} ⬇ ${download}️ ⬆ ${upload}️`);
+ console.log(`Test #${testResult} was executed successfully in ${time}s. 🏓 ${ping} ⬇ ${download}️ ⬆ ${upload}️`);
createRecommendations().then(() => "");
} catch (e) {
if (!retried) return this.create(type, true);