Merge pull request #1 from gnmyt/development

🏷️ Version 1.0.2
This commit is contained in:
Mathias Wagner
2022-05-09 23:32:44 +02:00
committed by GitHub
27 changed files with 2991 additions and 5221 deletions

View File

@@ -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
View 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

File diff suppressed because it is too large Load Diff

View File

@@ -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"
},

View File

@@ -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

View File

@@ -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";

View File

@@ -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>

View File

@@ -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;

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 (<></>)

View File

@@ -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);

View File

@@ -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>

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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
View 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
View File

@@ -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",

View File

@@ -1,6 +1,6 @@
{
"name": "myspeed",
"version": "1.0.0",
"version": "1.0.1",
"scripts": {
"client": "cd client && npm start",
"server": "nodemon server",

View 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));
}
}

View File

@@ -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
View 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

View 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;

View File

@@ -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"});
});

View File

@@ -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 = () => {