mirror of
https://github.com/gnmyt/myspeed.git
synced 2026-02-10 15:49:42 -06:00
2
.github/workflows/create_release.yml
vendored
2
.github/workflows/create_release.yml
vendored
@@ -29,7 +29,7 @@ jobs:
|
||||
run: sudo apt-get install zip
|
||||
|
||||
- name: Zip all files
|
||||
run: zip -r MySpeed-${{ steps.get_version.outputs.version }}.zip build node_modules server package.json package-lock.json
|
||||
run: zip -r MySpeed-${{ steps.get_version.outputs.version }}.zip build server package.json package-lock.json
|
||||
|
||||
- uses: "marvinpinto/action-automatic-releases@latest"
|
||||
with:
|
||||
|
||||
26
Dockerfile
Executable file
26
Dockerfile
Executable file
@@ -0,0 +1,26 @@
|
||||
FROM node:16-alpine
|
||||
RUN apk add g++ make cmake python3 --no-cache
|
||||
|
||||
ENV NODE_ENV=production
|
||||
|
||||
WORKDIR /myspeed
|
||||
|
||||
COPY --chown=node:node ./client ./client
|
||||
COPY --chown=node:node ./server ./server
|
||||
COPY --chown=node:node ./package.json ./package.json
|
||||
|
||||
RUN npm install
|
||||
RUN cd client && npm install
|
||||
RUN npm run build
|
||||
RUN rm -rf /myspeed/client
|
||||
RUN mkdir -p /myspeed/data
|
||||
|
||||
RUN chown -R node:node /myspeed
|
||||
|
||||
USER node
|
||||
|
||||
VOLUME ["/myspeed/data"]
|
||||
|
||||
EXPOSE 5216
|
||||
|
||||
CMD ["node", "server"]
|
||||
7687
client/package-lock.json
generated
7687
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "client",
|
||||
"version": "0.0.1",
|
||||
"version": "0.0.2",
|
||||
"proxy": "http://localhost:5216/",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome": "^1.1.8",
|
||||
@@ -13,7 +13,7 @@
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0",
|
||||
"react-scripts": "5.0.0",
|
||||
"react-scripts": "^5.0.1",
|
||||
"sass": "^1.50.0",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
|
||||
@@ -23,9 +23,15 @@ body, html
|
||||
color: #F1F1F1
|
||||
font-size: 26pt
|
||||
|
||||
.help-icon
|
||||
cursor: help
|
||||
|
||||
.icon-red
|
||||
color: #C64545
|
||||
|
||||
.icon-error
|
||||
color: #900c0c
|
||||
|
||||
.icon-green
|
||||
color: #45C65A
|
||||
|
||||
|
||||
@@ -4,14 +4,14 @@ export function generateRelativeTime(created) {
|
||||
|
||||
const diff = (currentDate - date) / 1000;
|
||||
|
||||
if (diff === 0) {
|
||||
if (diff < 5) {
|
||||
return "Gerade eben"
|
||||
} else if (diff < 60) {
|
||||
return diff === 1 ? "Einer Sekunde" : `${Math.floor(diff)} Sekunden`
|
||||
return diff === 1 ? "1 Sekunde" : `${Math.floor(diff)} Sekunden`
|
||||
} else if (diff < 3600) {
|
||||
return Math.floor(diff / 60) === 1 ? "Einer Minute" : `${Math.floor(diff / 60)} Minuten`
|
||||
return Math.floor(diff / 60) === 1 ? "1 Minute" : `${Math.floor(diff / 60)} Minuten`
|
||||
} else if (diff < 86400) {
|
||||
return Math.floor(diff / 3600) === 1 ? "Einer Stunde" : `${Math.floor(diff / 3600)} Stunden`
|
||||
return Math.floor(diff / 3600) === 1 ? "1 Stunde" : `${Math.floor(diff / 3600)} Stunden`
|
||||
}
|
||||
|
||||
return "Einer langen Zeit"
|
||||
@@ -20,6 +20,8 @@ export function generateRelativeTime(created) {
|
||||
export function getIconBySpeed(current, optional, higherIsBetter) {
|
||||
let speed = Math.floor((current / optional) * 100);
|
||||
|
||||
if (current === 0) return "error";
|
||||
|
||||
if (higherIsBetter) {
|
||||
if (speed >= 75) return "green";
|
||||
if (speed >= 30) return "orange";
|
||||
|
||||
@@ -3,20 +3,25 @@ import "../style/Dropdown.sass";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {
|
||||
faArrowDown,
|
||||
faArrowUp,
|
||||
faGaugeHigh,
|
||||
faArrowUp, faClose,
|
||||
faGaugeHigh, faGear, faInfo,
|
||||
faKey,
|
||||
faPingPongPaddleBall,
|
||||
faServer
|
||||
faServer, faWandMagicSparkles
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import {DialogContext} from "../context/DialogContext";
|
||||
|
||||
export const toggleDropdown = () => {
|
||||
let icon;
|
||||
|
||||
export const toggleDropdown = (setIcon) => {
|
||||
if (setIcon) icon = setIcon;
|
||||
let classList = document.getElementsByClassName("dropdown")[0].classList;
|
||||
if (classList.contains("dropdown-invisible")) {
|
||||
classList.remove("dropdown-invisible");
|
||||
icon(faClose);
|
||||
} else {
|
||||
classList.add("dropdown-invisible");
|
||||
icon(faGear);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,11 +35,12 @@ function DropdownComponent() {
|
||||
toggleDropdown();
|
||||
fetch("/api/config/ping", {headers: headers}).then(res => res.json())
|
||||
.then(ping => setDialog({
|
||||
title: "Optimalen Ping setzen",
|
||||
title: "Optimalen Ping setzen (ms)",
|
||||
placeholder: "Ping",
|
||||
value: ping.value,
|
||||
onSuccess: value => {
|
||||
fetch("/api/config/ping", {headers: headers, method: "PATCH", body: JSON.stringify({value: value})});
|
||||
fetch("/api/config/ping", {headers: headers, method: "PATCH", body: JSON.stringify({value: value})})
|
||||
.then(() => showFeedback());
|
||||
}
|
||||
}));
|
||||
}
|
||||
@@ -43,11 +49,12 @@ function DropdownComponent() {
|
||||
toggleDropdown();
|
||||
fetch("/api/config/download", {headers: headers}).then(res => res.json())
|
||||
.then(down => setDialog({
|
||||
title: "Optimalen Down-Speed setzen",
|
||||
title: "Optimalen Down-Speed setzen (Mbit/s)",
|
||||
placeholder: "Down-Speed",
|
||||
value: down.value,
|
||||
onSuccess: value => {
|
||||
fetch("/api/config/download", {headers: headers, method: "PATCH", body: JSON.stringify({value: value})});
|
||||
fetch("/api/config/download", {headers: headers, method: "PATCH", body: JSON.stringify({value: value})})
|
||||
.then(() => showFeedback());
|
||||
}
|
||||
}));
|
||||
}
|
||||
@@ -56,11 +63,12 @@ function DropdownComponent() {
|
||||
toggleDropdown();
|
||||
fetch("/api/config/upload", {headers: headers}).then(res => res.json())
|
||||
.then(up => setDialog({
|
||||
title: "Optimalen Up-Speed setzen",
|
||||
title: "Optimalen Up-Speed setzen (Mbit/s)",
|
||||
placeholder: "Up-Speed",
|
||||
value: up.value,
|
||||
onSuccess: value => {
|
||||
fetch("/api/config/upload", {headers: headers, method: "PATCH", body: JSON.stringify({value: value})});
|
||||
fetch("/api/config/upload", {headers: headers, method: "PATCH", body: JSON.stringify({value: value})})
|
||||
.then(() => showFeedback());
|
||||
}
|
||||
}));
|
||||
}
|
||||
@@ -70,8 +78,17 @@ function DropdownComponent() {
|
||||
setDialog({
|
||||
title: "Neues Passwort festlegen",
|
||||
placeholder: "Neues Passwort",
|
||||
password: true,
|
||||
unsetButton: true,
|
||||
unsetButtonText: "Sperre aufheben",
|
||||
onClear: () => {
|
||||
fetch("/api/config/password", {headers: headers, method: "PATCH", body: JSON.stringify({value: "none"})})
|
||||
.then(() => showFeedback(<>Die Passwortsperre wurde aufgehoben.</>));
|
||||
localStorage.removeItem("password");
|
||||
},
|
||||
onSuccess: value => {
|
||||
fetch("/api/config/password", {headers: headers, method: "PATCH", body: JSON.stringify({value: value})});
|
||||
fetch("/api/config/password", {headers: headers, method: "PATCH", body: JSON.stringify({value: value})})
|
||||
.then(() => showFeedback());
|
||||
localStorage.setItem("password", value);
|
||||
}
|
||||
})
|
||||
@@ -85,7 +102,8 @@ function DropdownComponent() {
|
||||
placeholder: "Server-ID",
|
||||
value: ping.value,
|
||||
onSuccess: value => {
|
||||
fetch("/api/config/serverId", {headers: headers, method: "PATCH", body: JSON.stringify({value: value})});
|
||||
fetch("/api/config/serverId", {headers: headers, method: "PATCH", body: JSON.stringify({value: value})})
|
||||
.then(() => showFeedback());
|
||||
}
|
||||
}));
|
||||
}
|
||||
@@ -95,6 +113,45 @@ function DropdownComponent() {
|
||||
setDialog({speedtest: true, promise: fetch("/api/speedtests/run", {headers: headers, method: "POST"})});
|
||||
}
|
||||
|
||||
const showCredits = () => {
|
||||
toggleDropdown();
|
||||
setDialog({title: "MySpeed", description: <><a href="https://github.com/gnmyt/myspeed" target="_blank">MySpeed</a> wird von GNMYT bereitgestellt
|
||||
und verwendet die <a href="https://www.speedtest.net/apps/cli" target="_blank">Speedtest-CLI</a> von Ookla.</>, buttonText: "Schließen"});
|
||||
}
|
||||
|
||||
const showFeedback = (customText) => {
|
||||
setDialog({title: "MySpeed", description: customText || <>Deine Änderungen wurden übernommen.</>, buttonText: "Okay",
|
||||
onSuccess: () => window.location.reload(), onClose: () => window.location.reload()});
|
||||
}
|
||||
|
||||
const recommendedSettings = async () => {
|
||||
toggleDropdown();
|
||||
fetch("/api/recommendations", {headers: headers}).then(res => res.json())
|
||||
.then(values => values.message !== undefined ? setDialog({
|
||||
title: "Automatische Empfehlungen",
|
||||
description: <>Du musst mindestens 10 Tests machen, damit ein Durchschnitt ermittelt werden kann. Ob die
|
||||
Tests manuell oder automatisch durchgeführt wurden ist egal.
|
||||
</>,
|
||||
buttonText: "Okay"
|
||||
}) : setDialog({
|
||||
title: "Automatische Empfehlungen setzen?",
|
||||
description: <>Anhand der letzten 10 Testergebnisse wurde festgestellt, dass der optimale Ping bei <span
|
||||
className="dialog-value">
|
||||
{values.ping} ms</span>, der Download bei <span
|
||||
className="dialog-value">{values.download} Mbit/s </span>
|
||||
und der Upload bei <span className="dialog-value">{values.upload} Mbit/s</span> liegt. <br/>
|
||||
Orientiere dich am besten an deinem Internetvertrag und übernehme es nur, wenn es mit dem
|
||||
übereinstimmt.</>,
|
||||
buttonText: "Ja, übernehmen",
|
||||
onSuccess: async () => {
|
||||
await fetch("/api/config/ping", {headers: headers, method: "PATCH", body: JSON.stringify({value: values.ping})});
|
||||
await fetch("/api/config/download", {headers: headers, method: "PATCH", body: JSON.stringify({value: values.download})});
|
||||
await fetch("/api/config/upload", {headers: headers, method: "PATCH", body: JSON.stringify({value: values.upload})});
|
||||
showFeedback();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="dropdown dropdown-invisible">
|
||||
<div id="dropdown" className="dropdown-content">
|
||||
@@ -104,13 +161,17 @@ function DropdownComponent() {
|
||||
<FontAwesomeIcon icon={faPingPongPaddleBall}/>
|
||||
<h3>Optimaler Ping</h3>
|
||||
</div>
|
||||
<div className="dropdown-item" onClick={updateUpload}>
|
||||
<FontAwesomeIcon icon={faArrowUp}/>
|
||||
<h3>Optimaler Up-Speed</h3>
|
||||
</div>
|
||||
<div className="dropdown-item" onClick={updateDownload}>
|
||||
<FontAwesomeIcon icon={faArrowDown}/>
|
||||
<h3>Optimaler Down-Speed</h3>
|
||||
</div>
|
||||
<div className="dropdown-item" onClick={updateUpload}>
|
||||
<FontAwesomeIcon icon={faArrowUp}/>
|
||||
<h3>Optimaler Up-Speed</h3>
|
||||
<div className="dropdown-item" onClick={recommendedSettings}>
|
||||
<FontAwesomeIcon icon={faWandMagicSparkles}/>
|
||||
<h3>Optimale Werte</h3>
|
||||
</div>
|
||||
<div className="center">
|
||||
<hr className="dropdown-hr"/>
|
||||
@@ -127,6 +188,10 @@ function DropdownComponent() {
|
||||
<FontAwesomeIcon icon={faGaugeHigh}/>
|
||||
<h3>Speedtest starten</h3>
|
||||
</div>
|
||||
<div className="dropdown-item" onClick={showCredits}>
|
||||
<FontAwesomeIcon icon={faInfo}/>
|
||||
<h3>Info</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,23 +1,27 @@
|
||||
import "../style/Header.sass";
|
||||
import {Component} from "react";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {faGear} from "@fortawesome/free-solid-svg-icons";
|
||||
import DropdownComponent, {toggleDropdown} from "./DropdownComponent";
|
||||
import {useState} from "react";
|
||||
|
||||
class HeaderComponent extends Component {
|
||||
function HeaderComponent() {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<header>
|
||||
<div className="header-main">
|
||||
<h2>Netzwerkanalyse</h2>
|
||||
<FontAwesomeIcon icon={faGear} className="settings" onClick={toggleDropdown}/>
|
||||
</div>
|
||||
<DropdownComponent/>
|
||||
</header>
|
||||
)
|
||||
const [icon, setIcon] = useState(faGear);
|
||||
|
||||
function switchDropdown() {
|
||||
toggleDropdown(setIcon);
|
||||
}
|
||||
|
||||
return (
|
||||
<header>
|
||||
<div className="header-main">
|
||||
<h2>Netzwerkanalyse</h2>
|
||||
<FontAwesomeIcon icon={icon} className="settings" onClick={switchDropdown}/>
|
||||
</div>
|
||||
<DropdownComponent/>
|
||||
</header>
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
export default HeaderComponent;
|
||||
@@ -4,19 +4,35 @@ import "../style/LatestTest.sass";
|
||||
import {generateRelativeTime, getIconBySpeed} from "../HelperFunctions";
|
||||
import {useContext, useEffect, useState} from "react";
|
||||
import {ConfigContext} from "../context/ConfigContext";
|
||||
import {DialogContext} from "../context/DialogContext";
|
||||
|
||||
function LatestTestComponent() {
|
||||
const [latest, setLatest] = useState({});
|
||||
const [latestTestTime, setLatestTestTime] = useState("");
|
||||
const [setDialog] = useContext(DialogContext);
|
||||
const config = useContext(ConfigContext);
|
||||
|
||||
useEffect(() => {
|
||||
function updateTest() {
|
||||
let passwordHeaders = localStorage.getItem("password") ? {password: localStorage.getItem("password")} : {}
|
||||
fetch("/api/speedtests/latest", {headers: passwordHeaders})
|
||||
.then(res => res.json())
|
||||
.then(latest => setLatest(latest));
|
||||
.then(latest => {
|
||||
setLatest(latest);
|
||||
setLatestTestTime(generateRelativeTime(latest.created));
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => updateTest(), 15000);
|
||||
updateTest();
|
||||
return () => clearInterval(interval);
|
||||
}, [setLatest]);
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => setLatestTestTime(generateRelativeTime(latest.created)), 1000);
|
||||
return () => clearInterval(interval);
|
||||
}, [setLatestTestTime, latest]);
|
||||
|
||||
if (Object.entries(config).length === 0) return (<></>)
|
||||
|
||||
return (
|
||||
@@ -24,47 +40,58 @@ function LatestTestComponent() {
|
||||
{/* Ping */}
|
||||
<div className="inner-container">
|
||||
<div className="container-header">
|
||||
<FontAwesomeIcon icon={faPingPongPaddleBall}
|
||||
className={"container-icon icon-" + getIconBySpeed(latest.ping, config.ping, false)}/>
|
||||
<FontAwesomeIcon onClick={() => setDialog({title: "Ping", description: "Der Ping zeigt dir, wie schnell der jeweilige Anbieter antwortet. " +
|
||||
"Umso kürzer die Zeit, desto besser.", buttonText: "Okay"})}
|
||||
icon={faPingPongPaddleBall} className={"container-icon help-icon icon-" + getIconBySpeed(latest.ping, config.ping, false)}/>
|
||||
<h2 className="container-text">Ping<span className="container-subtext">ms</span></h2>
|
||||
</div>
|
||||
<div className="container-main">
|
||||
<h2>{latest.ping}</h2>
|
||||
<h2>{latest.ping === 0 ? "Test" : latest.ping}</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Download */}
|
||||
<div className="inner-container">
|
||||
<div className="container-header">
|
||||
<FontAwesomeIcon icon={faArrowDown}
|
||||
className={"container-icon icon-" + getIconBySpeed(latest.download, config.download, true)}/>
|
||||
<h2 className="container-text">Download<span className="container-subtext">Mbps</span></h2>
|
||||
<FontAwesomeIcon onClick={() => setDialog({title: "Download-Geschwindigkeit", description: "Die Downloadgeschwindigkeit wirkt sich " +
|
||||
"auf dein Surferlebnis aus. Umso mehr du bekommst, desto besser und stabiler ist das Internet. " +
|
||||
"Achte hierbei auch auf den Internetvertrag und prüfe, ob die Bedingungen erfüllt werden. " +
|
||||
"Du kannst nur so viel bekommen, wie dein Anbieter auch verspricht.", buttonText: "Okay"})}
|
||||
icon={faArrowDown} className={"container-icon help-icon icon-" + getIconBySpeed(latest.download, config.download, true)}/>
|
||||
<h2 className="container-text">Download<span className="container-subtext">Mbit/s</span></h2>
|
||||
</div>
|
||||
<div className="container-main">
|
||||
<h2>{latest.download}</h2>
|
||||
<h2>{latest.download === 0 ? "schlug" : latest.download}</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Upload */}
|
||||
<div className="inner-container">
|
||||
<div className="container-header">
|
||||
<FontAwesomeIcon icon={faArrowUp}
|
||||
className={"container-icon icon-" + getIconBySpeed(latest.upload, config.upload, true)}/>
|
||||
<h2 className="container-text">Upload<span className="container-subtext">Mbps</span></h2>
|
||||
<FontAwesomeIcon onClick={() => setDialog({title: "Upload-Geschwindigkeit", description: "Die Uploadgeschwindigkeit wirkt sich " +
|
||||
"auf dein Surferlebnis aus. Umso mehr du bekommst, desto besser und stabiler ist das Internet. " +
|
||||
"Achte hierbei auch auf den Internetvertrag und prüfe, ob die Bedingungen erfüllt werden. " +
|
||||
"Du kannst nur so viel bekommen, wie dein Anbieter auch verspricht.", buttonText: "Okay"})}
|
||||
icon={faArrowUp} className={"container-icon help-icon icon-" + getIconBySpeed(latest.upload, config.upload, true)}/>
|
||||
<h2 className="container-text">Upload<span className="container-subtext">Mbit/s</span></h2>
|
||||
</div>
|
||||
<div className="container-main">
|
||||
<h2>{latest.upload}</h2>
|
||||
<h2>{latest.upload === 0 ? "fehl!" : latest.upload}</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Latest update */}
|
||||
<div className="inner-container">
|
||||
<div className="container-header">
|
||||
<FontAwesomeIcon icon={faClockRotateLeft} className="container-icon icon-blue"/>
|
||||
<h2 className="container-text">Letztes Update<span className="container-subtext">vor</span></h2>
|
||||
<FontAwesomeIcon onClick={() => setDialog({title: "Letzter Test", description: "Dies ist die Zeit, die dir zeigt, wann der letzte Test " +
|
||||
"ausgeführt wurde. In diesem Fall wurde der letzte Test am " + new Date(latest.created).toLocaleDateString("de-DE") + " um " +
|
||||
new Date(latest.created).toLocaleTimeString("de-DE", {hour: "2-digit", minute: "2-digit"}) + " ausgeführt.",
|
||||
buttonText: "Okay"})}
|
||||
icon={faClockRotateLeft} className="container-icon icon-blue help-icon"/>
|
||||
<h2 className="container-text">Letzter Test<span className="container-subtext">vor</span></h2>
|
||||
</div>
|
||||
<div className="container-main">
|
||||
<h2>{generateRelativeTime(latest.created)}</h2>
|
||||
<h2>{latestTestTime}</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import {Component} from "react";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {faArrowDown, faArrowUp, faClockRotateLeft, faPingPongPaddleBall} from "@fortawesome/free-solid-svg-icons";
|
||||
import {
|
||||
faArrowDown,
|
||||
faArrowUp,
|
||||
faClockRotateLeft,
|
||||
faClose,
|
||||
faPingPongPaddleBall
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import "../style/Speedtest.sass";
|
||||
|
||||
class Speedtest extends Component {
|
||||
@@ -14,16 +20,16 @@ class Speedtest extends Component {
|
||||
<h2 className="date-text">Um {this.props.time}</h2>
|
||||
</div>
|
||||
<div className="speedtest-row">
|
||||
<FontAwesomeIcon icon={faPingPongPaddleBall} className={"speedtest-icon icon-" + this.props.pingLevel}/>
|
||||
<h2 className="speedtest-text">{this.props.ping}</h2>
|
||||
<FontAwesomeIcon icon={this.props.ping !== 0 ? faPingPongPaddleBall : faClose} className={"speedtest-icon icon-" + this.props.pingLevel}/>
|
||||
<h2 className="speedtest-text">{this.props.ping === 0 ? "" : this.props.ping}</h2>
|
||||
</div>
|
||||
<div className="speedtest-row">
|
||||
<FontAwesomeIcon icon={faArrowDown} className={"speedtest-icon icon-" + this.props.downLevel} />
|
||||
<h2 className="speedtest-text">{this.props.down}</h2>
|
||||
<FontAwesomeIcon icon={this.props.down !== 0 ? faArrowDown : faClose} className={"speedtest-icon icon-" + this.props.downLevel} />
|
||||
<h2 className="speedtest-text">{this.props.down === 0 ? "" : this.props.down}</h2>
|
||||
</div>
|
||||
<div className="speedtest-row">
|
||||
<FontAwesomeIcon icon={faArrowUp} className={"speedtest-icon icon-" + this.props.upLevel}/>
|
||||
<h2 className="speedtest-text">{this.props.up}</h2>
|
||||
<FontAwesomeIcon icon={this.props.up !== 0 ? faArrowUp : faClose} className={"speedtest-icon icon-" + this.props.upLevel}/>
|
||||
<h2 className="speedtest-text">{this.props.up === 0 ? "" : this.props.up}</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,11 +7,17 @@ function TestArea() {
|
||||
const config = useContext(ConfigContext);
|
||||
const [tests, setTests] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
function updateTests() {
|
||||
let passwordHeaders = localStorage.getItem("password") ? {password: localStorage.getItem("password")} : {}
|
||||
fetch("/api/speedtests", {headers: passwordHeaders})
|
||||
.then(res => res.json())
|
||||
.then(tests => setTests(tests));
|
||||
.then(tests => setTests(tests))
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => updateTests(), 15000);
|
||||
updateTests();
|
||||
return () => clearInterval(interval);
|
||||
}, [setTests]);
|
||||
|
||||
if (Object.entries(config).length === 0) return (<></>)
|
||||
|
||||
@@ -21,6 +21,9 @@ export const ConfigProvider = (props) => {
|
||||
.catch(() => setDialog({
|
||||
title: "Passwort erforderlich",
|
||||
placeholder: "Dein Passwort",
|
||||
description: localStorage.getItem("password") ? <span className="icon-red">Das von dir eingegebene Passwort ist falsch</span> : "",
|
||||
password: true,
|
||||
buttonText: "Fertig",
|
||||
onClose: () => window.location.reload(),
|
||||
onSuccess: (value) => {
|
||||
localStorage.setItem("password", value);
|
||||
|
||||
@@ -29,8 +29,17 @@ const Dialog = ({dialog, setDialog}) => {
|
||||
if (dialog.onSuccess) dialog.onSuccess(value);
|
||||
}
|
||||
|
||||
function clear() {
|
||||
setDialog();
|
||||
if (dialog.onClear) dialog.onClear();
|
||||
}
|
||||
|
||||
if (dialog.speedtest) {
|
||||
dialog.promise.then(() => window.location.reload());
|
||||
dialog.promise.then(res => {
|
||||
if (res.status === 409) {
|
||||
setDialog({title: "Fehlgeschlagen", description: "Es läuft bereits ein Speedtest. Bitte gedulde dich ein wenig, bis dieser fertig ist.", buttonText: "Okay"});
|
||||
} else window.location.reload();
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="dialog-area">
|
||||
@@ -49,11 +58,13 @@ const Dialog = ({dialog, setDialog}) => {
|
||||
<FontAwesomeIcon icon={faClose} className="dialog-text dialog-icon" onClick={closeDialog}/>
|
||||
</div>
|
||||
<div className="dialog-main">
|
||||
<input className="dialog-input" type="text" placeholder={dialog.placeholder} value={value}
|
||||
onChange={updateValue}/>
|
||||
{dialog.description ? <h3 className="dialog-description">{dialog.description}</h3>: ""}
|
||||
{dialog.placeholder ? <input className="dialog-input" type={dialog.password ? "password" : "text"} placeholder={dialog.placeholder} value={value}
|
||||
onChange={updateValue}/> : ""}
|
||||
</div>
|
||||
<div className="dialog-buttons">
|
||||
<button className="dialog-btn" onClick={submit}>Aktualisieren</button>
|
||||
{dialog.unsetButton ? <button className="dialog-btn dialog-secondary" onClick={clear}>{dialog.unsetButtonText || "Entfernen"}</button> : ""}
|
||||
<button className="dialog-btn" onClick={submit}>{dialog.buttonText || "Aktualisieren"}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
right: 0
|
||||
width: 100%
|
||||
height: 100%
|
||||
background-color: rgb(0,0,0)
|
||||
background-color: rgba(0,0,0,0.6)
|
||||
background-color: rgb(0, 0, 0)
|
||||
background-color: rgba(0, 0, 0, 0.6)
|
||||
display: flex
|
||||
align-items: center
|
||||
z-index: 10
|
||||
@@ -15,7 +15,6 @@
|
||||
|
||||
.dialog
|
||||
width: 480px
|
||||
height: 180px
|
||||
padding: 15px
|
||||
background-color: #27282B
|
||||
border-radius: 15px
|
||||
@@ -37,12 +36,13 @@
|
||||
|
||||
.dialog-main
|
||||
display: flex
|
||||
height: 65%
|
||||
justify-content: center
|
||||
align-items: center
|
||||
flex-direction: column
|
||||
|
||||
.dialog-buttons
|
||||
display: flex
|
||||
margin-top: 5px
|
||||
justify-content: right
|
||||
|
||||
.dialog-text
|
||||
@@ -50,14 +50,27 @@
|
||||
color: #C9C9C9
|
||||
margin: 0
|
||||
|
||||
.dialog-description
|
||||
font-size: 16pt
|
||||
margin: 10px 2px 2px
|
||||
color: #cccccc
|
||||
|
||||
.dialog-description a
|
||||
color: #45C65A
|
||||
|
||||
.dialog-value
|
||||
color: #45C65A
|
||||
|
||||
.dialog-icon
|
||||
cursor: pointer
|
||||
|
||||
.dialog-input
|
||||
width: 200px
|
||||
font-size: 20pt
|
||||
font-size: 18pt
|
||||
padding: 15px
|
||||
font-weight: 700
|
||||
margin-top: 15px
|
||||
margin-bottom: 15px
|
||||
background-color: #2F3136
|
||||
color: #C9C9C9
|
||||
border: none
|
||||
@@ -73,9 +86,11 @@
|
||||
color: #27282B
|
||||
background-color: #45C65A
|
||||
cursor: pointer
|
||||
margin-left: 5px
|
||||
margin-right: 5px
|
||||
|
||||
|
||||
|
||||
.dialog-secondary
|
||||
background-color: #C64545
|
||||
|
||||
.lds-ellipsis
|
||||
display: inline-block
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
display: inline-block
|
||||
position: absolute
|
||||
width: 320px
|
||||
height: 270px
|
||||
height: 335px
|
||||
overflow: auto
|
||||
border-radius: 10px
|
||||
box-shadow: 0 8px 16px 0 rgba(0,0,0,0.2)
|
||||
|
||||
@@ -15,4 +15,14 @@
|
||||
margin-right: 10%
|
||||
|
||||
.settings
|
||||
cursor: pointer
|
||||
cursor: pointer
|
||||
transition: all 50ms ease-in-out
|
||||
width: 30px
|
||||
height: 30px
|
||||
|
||||
.settings:hover
|
||||
transform: scale(1.1)
|
||||
|
||||
@media (max-width: 320px)
|
||||
.header-main h2
|
||||
margin-left: 0
|
||||
@@ -24,15 +24,17 @@
|
||||
.container-subtext
|
||||
color: #B0B0B0
|
||||
font-size: 16pt
|
||||
margin-left: 5px
|
||||
|
||||
.container-main
|
||||
text-align: center
|
||||
color: #C8C8C8
|
||||
h2
|
||||
font-size: 28pt
|
||||
font-weight: 900
|
||||
margin: 1rem
|
||||
|
||||
@media (max-width: 1370px)
|
||||
@media (max-width: 1351px)
|
||||
.analyse-area
|
||||
flex-direction: column
|
||||
width: 60rem
|
||||
@@ -54,6 +56,10 @@
|
||||
padding-left: 1rem
|
||||
padding-bottom: 1rem
|
||||
padding-top: 1rem
|
||||
width: 15rem
|
||||
width: 18rem
|
||||
.inner-container
|
||||
margin: 0
|
||||
margin: 0
|
||||
|
||||
@media (max-width: 320px)
|
||||
.analyse-area
|
||||
width: 16rem
|
||||
@@ -28,7 +28,7 @@
|
||||
font-weight: 900
|
||||
color: #C8C8C8
|
||||
|
||||
@media (max-width: 1370px)
|
||||
@media (max-width: 1351px)
|
||||
.speedtest
|
||||
width: 60rem
|
||||
|
||||
@@ -51,4 +51,8 @@
|
||||
|
||||
@media (max-width: 475px)
|
||||
.speedtest
|
||||
width: 15rem
|
||||
width: 18rem
|
||||
|
||||
@media (max-width: 320px)
|
||||
.speedtest
|
||||
width: 16rem
|
||||
85
install.sh
Executable file
85
install.sh
Executable file
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
INSTALLATION_PATH="/opt/myspeed"
|
||||
RELEASE_URL=$(curl -s https://api.github.com/repos/gnmyt/myspeed/releases/latest | grep browser_download_url | cut -d '"' -f 4)
|
||||
|
||||
log () {
|
||||
printf "\033[0;34m$1\e[0m\n"
|
||||
}
|
||||
|
||||
# Root check
|
||||
if [ $EUID -ne 0 ]; then
|
||||
echo "Du musst dieses Skript als root ausführen"
|
||||
exit
|
||||
fi
|
||||
|
||||
# Check if installed
|
||||
if [ -d $INSTALLATION_PATH ]; then
|
||||
log "Eine MySpeed-Instanz unter $INSTALLATION_PATH wurde bereits installiert. Die Installation wird abgebrochen."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
|
||||
# Update all packages
|
||||
apt-get update -y
|
||||
|
||||
# Check for wget
|
||||
if ! command -v wget &> /dev/null
|
||||
then
|
||||
log "Das Paket \"wget\" wurde nicht gefunden, wird aber benötigt. Es wird nun installiert..."
|
||||
sleep 2
|
||||
apt-get install wget -y
|
||||
fi
|
||||
|
||||
# Check for unzip
|
||||
if ! command -v unzip &> /dev/null
|
||||
then
|
||||
log "Das Paket \"unzip\" wurde nicht gefunden, wird aber benötigt. Es wird nun installiert..."
|
||||
sleep 2
|
||||
apt-get install unzip -y
|
||||
fi
|
||||
|
||||
# Check for curl
|
||||
if ! command -v curl &> /dev/null
|
||||
then
|
||||
log "Das Paket \"curl\" wurde nicht gefunden, wird aber benötigt. Es wird nun installiert..."
|
||||
sleep 2
|
||||
apt-get install curl -y
|
||||
fi
|
||||
|
||||
# Check for node
|
||||
if ! command -v node &> /dev/null
|
||||
then
|
||||
log "Das Paket \"nodejs\" wurde nicht gefunden, wird aber benötigt. Es wird nun installiert..."
|
||||
sleep 2
|
||||
curl -sSL https://deb.nodesource.com/setup_16.x | bash
|
||||
apt-get install nodejs -y
|
||||
fi
|
||||
|
||||
log "Alle notwendigen Pakete sind installiert. Starte installation von MySpeed..."
|
||||
sleep 2
|
||||
|
||||
if [ ! -d $INSTALLATION_PATH ]
|
||||
then
|
||||
log "MySpeed wird unter $INSTALLATION_PATH installiert. Der Ordner wird nun erstellt."
|
||||
sleep 2
|
||||
mkdir $INSTALLATION_PATH
|
||||
fi
|
||||
|
||||
cd $INSTALLATION_PATH
|
||||
|
||||
log "Lade notwendige Daten herunter..."
|
||||
sleep 2
|
||||
wget "$RELEASE_URL"
|
||||
|
||||
log "Entpacke notwendige Daten..."
|
||||
sleep 2
|
||||
unzip MySpeed*.zip
|
||||
rm MySpeed-*.zip
|
||||
|
||||
log "Lade die notwendigen Abhängigkeiten herunter..."
|
||||
sleep 2
|
||||
npm install
|
||||
|
||||
clear
|
||||
log "Erfolg! MySpeed wurde unter $INSTALLATION_PATH installiert."
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "myspeed",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "myspeed",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1",
|
||||
"dependencies": {
|
||||
"bcrypt": "^5.0.1",
|
||||
"better-sqlite3": "^7.5.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "myspeed",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1",
|
||||
"scripts": {
|
||||
"client": "cd client && npm start",
|
||||
"server": "nodemon server",
|
||||
|
||||
17
server/controller/recommendations.js
Normal file
17
server/controller/recommendations.js
Normal file
@@ -0,0 +1,17 @@
|
||||
const db = require('../index').database;
|
||||
|
||||
// Gets the current recommendations
|
||||
module.exports.get = () => {
|
||||
return db.prepare("SELECT * FROM recommendations").get();
|
||||
}
|
||||
|
||||
// Sets new recommendations
|
||||
module.exports.set = (ping, download, upload) => {
|
||||
if (this.get() === undefined) {
|
||||
return db.prepare("INSERT INTO recommendations (ping, download, upload) VALUES (?, ?, ?)")
|
||||
.run(Math.round(ping), download.toFixed(2), upload.toFixed(2));
|
||||
} else {
|
||||
return db.prepare("UPDATE recommendations SET ping = ?, download = ?, upload = ?")
|
||||
.run(Math.round(ping), download.toFixed(2), upload.toFixed(2));
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,11 @@ module.exports.create = () => {
|
||||
" download double," +
|
||||
" upload double," +
|
||||
" created DATETIME DEFAULT CURRENT_TIMESTAMP);");
|
||||
db.exec("create table if not exists recommendations(" +
|
||||
" id integer primary key autoincrement," +
|
||||
" ping integer(5000)," +
|
||||
" download double," +
|
||||
" upload double);");
|
||||
}
|
||||
|
||||
module.exports.insert = () => {
|
||||
|
||||
13
server/index.js
Normal file → Executable file
13
server/index.js
Normal file → Executable file
@@ -1,12 +1,22 @@
|
||||
const express = require('express');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
const app = express();
|
||||
const port = process.env.port || 5216;
|
||||
|
||||
if (!fs.existsSync("data")) {
|
||||
try {
|
||||
fs.mkdirSync("data", {recursive: true});
|
||||
} catch (e) {
|
||||
console.error("Could not create the data folder. Please check the permission");
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
let db;
|
||||
try {
|
||||
db = require('better-sqlite3')('storage.db');
|
||||
db = require('better-sqlite3')('data/storage.db');
|
||||
console.log("Successfully connected to the database file");
|
||||
} catch (e) {
|
||||
console.error("Could not open the database file. Maybe it is damaged?");
|
||||
@@ -30,6 +40,7 @@ app.use("/api/*", require('./middlewares/password'));
|
||||
app.use("/api/config", require('./routes/config'));
|
||||
app.use("/api/speedtests", require('./routes/speedtests'));
|
||||
app.use("/api/info", require('./routes/system'));
|
||||
app.use("/api/recommendations", require('./routes/recommendations'));
|
||||
app.use("/api*", (req, res) => res.status(404).json({message: "Route not found"}));
|
||||
|
||||
// Enable production
|
||||
|
||||
12
server/routes/recommendations.js
Normal file
12
server/routes/recommendations.js
Normal file
@@ -0,0 +1,12 @@
|
||||
const app = require('express').Router();
|
||||
const recommendations = require('../controller/recommendations');
|
||||
|
||||
// Gets all config entries
|
||||
app.get("/", (req, res) => {
|
||||
let currentRecommendations = recommendations.get();
|
||||
if (currentRecommendations === undefined) return res.status(501).json({message: "There are no recommendations yet"});
|
||||
|
||||
return res.json(currentRecommendations);
|
||||
});
|
||||
|
||||
module.exports = app;
|
||||
@@ -8,7 +8,8 @@ app.get("/", (req, res) => {
|
||||
|
||||
// Runs a speedtest
|
||||
app.post("/run", async (req, res) => {
|
||||
await require("../tasks/speedtest").create();
|
||||
let speedtest = await require("../tasks/speedtest").create();
|
||||
if (speedtest !== undefined) return res.status(409).json({message: "An speedtest is already running"});
|
||||
res.json({message: "Speedtest successfully created"});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,12 +1,30 @@
|
||||
const speedTest = require('speedtest-net');
|
||||
const tests = require('../controller/speedtests');
|
||||
const config = require('../controller/config');
|
||||
const recommendations = require("../controller/recommendations");
|
||||
|
||||
let isRunning = false;
|
||||
|
||||
function roundSpeed(bytes, elapsed) {
|
||||
return Math.round((bytes * 8 / elapsed) / 10) / 100;
|
||||
}
|
||||
|
||||
async function createRecommendations() {
|
||||
let list = tests.list();
|
||||
if (list.length >= 10) {
|
||||
let avgNumbers = {ping: 0, down: 0, up: 0};
|
||||
for (let i = 0; i < 10; i++) {
|
||||
avgNumbers["ping"] += list[i].ping;
|
||||
avgNumbers["down"] += list[i].download;
|
||||
avgNumbers["up"] += list[i].upload;
|
||||
}
|
||||
|
||||
recommendations.set(avgNumbers["ping"] / 10, avgNumbers["down"] / 10, avgNumbers["up"] / 10);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.run = async () => {
|
||||
isRunning = true;
|
||||
let serverId = config.get("serverId").value;
|
||||
if (serverId === "none")
|
||||
serverId = undefined;
|
||||
@@ -20,12 +38,21 @@ module.exports.run = async () => {
|
||||
}
|
||||
|
||||
module.exports.create = async () => {
|
||||
let test = await this.run();
|
||||
let ping = Math.round(test.ping.latency);
|
||||
let download = roundSpeed(test.download.bytes, test.download.elapsed);
|
||||
let upload = roundSpeed(test.upload.bytes, test.upload.elapsed);
|
||||
let testResult = tests.create(ping, download, upload);
|
||||
console.log(`Test #${testResult} was executed successfully. 🏓 ${ping} ⬇ ${download}️ ⬆ ${upload}️`);
|
||||
if (isRunning) return 500;
|
||||
|
||||
try {
|
||||
let test = await this.run();
|
||||
let ping = Math.round(test.ping.latency);
|
||||
let download = roundSpeed(test.download.bytes, test.download.elapsed);
|
||||
let upload = roundSpeed(test.upload.bytes, test.upload.elapsed);
|
||||
let testResult = tests.create(ping, download, upload);
|
||||
console.log(`Test #${testResult} was executed successfully. 🏓 ${ping} ⬇ ${download}️ ⬆ ${upload}️`);
|
||||
createRecommendations().then(() => "");
|
||||
} catch (e) {
|
||||
let testResult = tests.create(0, 0, 0);
|
||||
console.log(`Test #${testResult} was not executed successfully. Please try reconnecting to the internet or restarting the software.`);
|
||||
}
|
||||
isRunning = false;
|
||||
}
|
||||
|
||||
module.exports.removeOld = () => {
|
||||
|
||||
Reference in New Issue
Block a user