diff --git a/client/index.html b/client/index.html index 04e6b0c5..9f9c6b6b 100644 --- a/client/index.html +++ b/client/index.html @@ -1,18 +1,15 @@ - + - + - + - - - - + MySpeed diff --git a/client/package-lock.json b/client/package-lock.json index d0f7e222..fea35462 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1,12 +1,12 @@ { "name": "client", - "version": "1.0.7", + "version": "1.0.8", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "client", - "version": "1.0.7", + "version": "1.0.8", "dependencies": { "@fortawesome/fontawesome-svg-core": "^6.4.0", "@fortawesome/free-brands-svg-icons": "^6.4.0", diff --git a/client/package.json b/client/package.json index 4273fcdd..76ad3792 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "client", - "version": "1.0.7", + "version": "1.0.8", "scripts": { "dev": "vite", "build": "vite build", diff --git a/client/public/assets/fonts/inter-v12-latin-300.ttf b/client/public/assets/fonts/inter-v12-latin-300.ttf new file mode 100644 index 00000000..5be620cf Binary files /dev/null and b/client/public/assets/fonts/inter-v12-latin-300.ttf differ diff --git a/client/public/assets/fonts/inter-v12-latin-300.woff2 b/client/public/assets/fonts/inter-v12-latin-300.woff2 new file mode 100644 index 00000000..e085aa82 Binary files /dev/null and b/client/public/assets/fonts/inter-v12-latin-300.woff2 differ diff --git a/client/public/assets/fonts/inter-v12-latin-500.ttf b/client/public/assets/fonts/inter-v12-latin-500.ttf new file mode 100644 index 00000000..a5e8c6e4 Binary files /dev/null and b/client/public/assets/fonts/inter-v12-latin-500.ttf differ diff --git a/client/public/assets/fonts/inter-v12-latin-500.woff2 b/client/public/assets/fonts/inter-v12-latin-500.woff2 new file mode 100644 index 00000000..6fc94ad0 Binary files /dev/null and b/client/public/assets/fonts/inter-v12-latin-500.woff2 differ diff --git a/client/public/assets/fonts/inter-v12-latin-700.ttf b/client/public/assets/fonts/inter-v12-latin-700.ttf new file mode 100644 index 00000000..d39cfb0a Binary files /dev/null and b/client/public/assets/fonts/inter-v12-latin-700.ttf differ diff --git a/client/public/assets/fonts/inter-v12-latin-700.woff2 b/client/public/assets/fonts/inter-v12-latin-700.woff2 new file mode 100644 index 00000000..8fcc4321 Binary files /dev/null and b/client/public/assets/fonts/inter-v12-latin-700.woff2 differ diff --git a/client/public/assets/fonts/inter-v12-latin-900.ttf b/client/public/assets/fonts/inter-v12-latin-900.ttf new file mode 100644 index 00000000..c885c815 Binary files /dev/null and b/client/public/assets/fonts/inter-v12-latin-900.ttf differ diff --git a/client/public/assets/fonts/inter-v12-latin-900.woff2 b/client/public/assets/fonts/inter-v12-latin-900.woff2 new file mode 100644 index 00000000..b1c208a1 Binary files /dev/null and b/client/public/assets/fonts/inter-v12-latin-900.woff2 differ diff --git a/client/public/assets/fonts/inter-v12-latin-regular.ttf b/client/public/assets/fonts/inter-v12-latin-regular.ttf new file mode 100644 index 00000000..179ee0cf Binary files /dev/null and b/client/public/assets/fonts/inter-v12-latin-regular.ttf differ diff --git a/client/public/assets/fonts/inter-v12-latin-regular.woff2 b/client/public/assets/fonts/inter-v12-latin-regular.woff2 new file mode 100644 index 00000000..c659f5e4 Binary files /dev/null and b/client/public/assets/fonts/inter-v12-latin-regular.woff2 differ diff --git a/client/public/favicon.ico b/client/public/assets/img/favicon.ico similarity index 100% rename from client/public/favicon.ico rename to client/public/assets/img/favicon.ico diff --git a/client/public/logo.png b/client/public/assets/img/logo.png similarity index 100% rename from client/public/logo.png rename to client/public/assets/img/logo.png diff --git a/client/public/logo192.png b/client/public/assets/img/logo192.png similarity index 100% rename from client/public/logo192.png rename to client/public/assets/img/logo192.png diff --git a/client/public/locales/de.json b/client/public/assets/locales/de.json similarity index 95% rename from client/public/locales/de.json rename to client/public/assets/locales/de.json index 4c305eaa..c3952462 100644 --- a/client/public/locales/de.json +++ b/client/public/assets/locales/de.json @@ -281,7 +281,20 @@ "display_name": "Anzeigename", "display_name_placeholder": "MySpeed Benachrichtigung", "error_message": "Fehlernachricht", - "error_message_placeholder": "Fehler: %upload%", + "error_message_placeholder": "Fehler: %error%", + "send_failed": "Fehlernachrichten senden", + "send_finished": "Abgeschlossene senden", + "finished_message": "Abgeschlossen-Nachricht", + "finished_message_placeholder": "%ping% ms, %download% Mbit/s, %upload% Mbit/s" + } + }, + "gotify": { + "title": "Gotify", + "fields": { + "url": "Server-URL", + "key": "Anwendungs-Token", + "error_message": "Fehlernachricht", + "error_message_placeholder": "Fehler: %error%", "send_failed": "Fehlernachrichten senden", "send_finished": "Abgeschlossene senden", "finished_message": "Abgeschlossen-Nachricht", @@ -301,7 +314,7 @@ "token": "Bot Token", "chat_id": "Chat ID", "error_message": "Fehlernachricht", - "error_message_placeholder": "Fehler: %upload%", + "error_message_placeholder": "Fehler: %error%", "send_failed": "Fehlernachrichten senden", "send_finished": "Abgeschlossene senden", "finished_message": "Abgeschlossen-Nachricht", diff --git a/client/public/locales/en.json b/client/public/assets/locales/en.json similarity index 95% rename from client/public/locales/en.json rename to client/public/assets/locales/en.json index 6e0e2ab0..95588180 100644 --- a/client/public/locales/en.json +++ b/client/public/assets/locales/en.json @@ -281,7 +281,20 @@ "display_name": "Display name", "display_name_placeholder": "MySpeed Notification", "error_message": "Error message", - "error_message_placeholder": "Error: %upload%", + "error_message_placeholder": "Error: %error%", + "send_failed": "Send error messages", + "send_finished": "Send finished messages", + "finished_message": "Finished message", + "finished_message_placeholder": "%ping% ms, %download% Mbps, %upload% Mbps" + } + }, + "gotify": { + "title": "Gotify", + "fields": { + "url": "Server-URL", + "key": "App token", + "error_message": "Error message", + "error_message_placeholder": "Error: %error%", "send_failed": "Send error messages", "send_finished": "Send finished messages", "finished_message": "Finished message", @@ -301,7 +314,7 @@ "token": "Bot Token", "chat_id": "Chat ID", "error_message": "Error message", - "error_message_placeholder": "Error: %upload%", + "error_message_placeholder": "Error: %error%", "send_failed": "Send error messages", "send_finished": "Send finished messages", "finished_message": "Finished message", diff --git a/client/public/manifest.json b/client/public/manifest.json index 648573ae..5d90dd31 100644 --- a/client/public/manifest.json +++ b/client/public/manifest.json @@ -4,18 +4,18 @@ "description": "A speed test analysis software that shows your internet speed for up to 30 days", "icons": [ { - "src": "favicon.ico", + "src": "/assets/img/favicon.ico", "sizes": "64x64 32x32 24x24 16x16", "type": "image/x-icon" }, { - "src": "logo192.png", + "src": "/assets/img/logo192.png", "type": "image/png", "sizes": "192x192", "purpose": "maskable" }, { - "src": "logo.png", + "src": "/assets/img/logo.png", "type": "image/png", "sizes": "512x512" } diff --git a/client/src/App.jsx b/client/src/App.jsx index 433c74b1..d11b489b 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -1,4 +1,6 @@ -import './App.sass'; +import "@/common/styles/default.sass"; +import "@/common/styles/fonts.sass"; +import "@/common/styles/spinner.sass"; import Home from "./pages/Home"; import HeaderComponent from "./common/components/Header"; import {SpeedtestProvider} from "./common/contexts/Speedtests"; @@ -8,7 +10,6 @@ import {InputDialogProvider} from "@/common/contexts/InputDialog/InputDialog"; import {useContext, useState} from "react"; import i18n from './i18n'; import Loading from "@/pages/Loading"; -import "@/common/styles/spinner.sass"; import Error from "@/pages/Error"; import {ViewContext, ViewProvider} from "@/common/contexts/View"; import Statistics from "@/pages/Statistics"; diff --git a/client/src/common/components/IntegrationDialog/IntegrationDialog.jsx b/client/src/common/components/IntegrationDialog/IntegrationDialog.jsx index cff2ecb2..8571b09d 100644 --- a/client/src/common/components/IntegrationDialog/IntegrationDialog.jsx +++ b/client/src/common/components/IntegrationDialog/IntegrationDialog.jsx @@ -53,18 +53,16 @@ export const IntegrationDialog = (props) => { useEffect(() => { jsonRequest("/integrations") - .then(data => setIntegrationData(data)) - .catch(err => console.log(err)); + .then(data => setIntegrationData(data)); jsonRequest("/integrations/active") - .then(data => setActiveData(data.map(item => ({...item, uuid: uuid()})))) - .catch(err => console.log(err)); + .then(data => setActiveData(data.map(item => ({...item, uuid: uuid()})))); }, []); return ( <> - - {!integrationData || !activeData &&
} + + {(!integrationData || !activeData) &&
} {integrationData && activeData && } diff --git a/client/src/common/components/IntegrationDialog/components/AvailableIntegrations/styles.sass b/client/src/common/components/IntegrationDialog/components/AvailableIntegrations/styles.sass index e565f7ab..f72e2cb0 100644 --- a/client/src/common/components/IntegrationDialog/components/AvailableIntegrations/styles.sass +++ b/client/src/common/components/IntegrationDialog/components/AvailableIntegrations/styles.sass @@ -5,6 +5,8 @@ flex-direction: column gap: 0.5rem user-select: none + overflow-x: hidden + overflow-y: scroll .integration-tab display: flex diff --git a/client/src/common/components/IntegrationDialog/components/IntegrationItem/components/IntegrationItemHeader/IntegrationItemHeader.jsx b/client/src/common/components/IntegrationDialog/components/IntegrationItem/components/IntegrationItemHeader/IntegrationItemHeader.jsx index f73a8fc3..a2827e44 100644 --- a/client/src/common/components/IntegrationDialog/components/IntegrationItem/components/IntegrationItemHeader/IntegrationItemHeader.jsx +++ b/client/src/common/components/IntegrationDialog/components/IntegrationItem/components/IntegrationItemHeader/IntegrationItemHeader.jsx @@ -10,9 +10,21 @@ import { faTrashArrowUp } from "@fortawesome/free-solid-svg-icons"; import "./styles.sass"; +import {useEffect, useState} from "react"; export const IntegrationItemHeader = ({integration, displayName, unsavedChanges, changesConfirmed, saveIntegration, - deleteConfirmed, deleteIntegration, open, setOpen, data}) => ( + deleteConfirmed, deleteIntegration, open, setOpen, data}) => { + const [lastActivity, setLastActivity] = useState(generateRelativeTime(data.lastActivity)); + + useEffect(() => { + const interval = setInterval(() => { + if (data.lastActivity !== null) setLastActivity(generateRelativeTime(data.lastActivity)); + }, 1000); + + return () => clearInterval(interval); + }); + + return (
@@ -23,7 +35,7 @@ export const IntegrationItemHeader = ({integration, displayName, unsavedChanges, : (data.lastActivity === null || !data.lastActivity ? "inactive" : "active"))}/>

{data.activityFailed ? t("failed") : (data.lastActivity === null || !data.lastActivity - ? t("integrations.activity.never_executed") : t("integrations.activity.last_run") + generateRelativeTime(data.lastActivity))}

+ ? t("integrations.activity.never_executed") : t("integrations.activity.last_run") + lastActivity)}

@@ -41,4 +53,4 @@ export const IntegrationItemHeader = ({integration, displayName, unsavedChanges, className="integration-green"/>
-) \ No newline at end of file +)} \ No newline at end of file diff --git a/client/src/common/components/IntegrationDialog/styles.sass b/client/src/common/components/IntegrationDialog/styles.sass index 77857c9f..d34a5cdd 100644 --- a/client/src/common/components/IntegrationDialog/styles.sass +++ b/client/src/common/components/IntegrationDialog/styles.sass @@ -15,7 +15,7 @@ gap: 0.5rem overflow-y: scroll overflow-x: clip - width: 100% + width: 70% @media (max-width: 781px) .integration-dialog @@ -27,4 +27,5 @@ flex-direction: column .integrations-tab - max-height: 15rem \ No newline at end of file + max-height: 15rem + width: 100% \ No newline at end of file diff --git a/client/src/common/components/LoadingDialog/LoadingDialog.jsx b/client/src/common/components/LoadingDialog/LoadingDialog.jsx index 474b5916..cbd25b49 100644 --- a/client/src/common/components/LoadingDialog/LoadingDialog.jsx +++ b/client/src/common/components/LoadingDialog/LoadingDialog.jsx @@ -5,12 +5,10 @@ export const LoadingDialog = (props) => { return ( <> - {props.isOpen && -
+ {props.isOpen &&
-
} ) diff --git a/client/src/App.sass b/client/src/common/styles/default.sass similarity index 97% rename from client/src/App.sass rename to client/src/common/styles/default.sass index 47773d37..20a9f023 100644 --- a/client/src/App.sass +++ b/client/src/common/styles/default.sass @@ -1,4 +1,4 @@ -@import "./common/styles/colors" +@import "colors" body, html margin: 0 diff --git a/client/src/common/styles/fonts.sass b/client/src/common/styles/fonts.sass new file mode 100644 index 00000000..7d25bc4d --- /dev/null +++ b/client/src/common/styles/fonts.sass @@ -0,0 +1,34 @@ +@font-face + font-display: swap + font-family: 'Inter' + font-style: normal + font-weight: 300 + src: url('/assets/fonts/inter-v12-latin-300.woff2') format("woff2"), url('/assets/fonts/inter-v12-latin-300.ttf') format("truetype") + +@font-face + font-display: swap + font-family: 'Inter' + font-style: normal + font-weight: 400 + src: url('/assets/fonts/inter-v12-latin-regular.woff2') format("woff2"), url('/assets/fonts/inter-v12-latin-regular.ttf') format("truetype") + +@font-face + font-display: swap + font-family: 'Inter' + font-style: normal + font-weight: 500 + src: url('/assets/fonts/inter-v12-latin-500.woff2') format("woff2"), url('/assets/fonts/inter-v12-latin-500.ttf') format("truetype") + +@font-face + font-display: swap + font-family: 'Inter' + font-style: normal + font-weight: 700 + src: url('/assets/fonts/inter-v12-latin-700.woff2') format("woff2"), url('/assets/fonts/inter-v12-latin-700.ttf') format("truetype") + +@font-face + font-display: swap + font-family: 'Inter' + font-style: normal + font-weight: 900 + src: url('/assets/fonts/inter-v12-latin-900.woff2') format("woff2"), url('/assets/fonts/inter-v12-latin-900.ttf') format("truetype") \ No newline at end of file diff --git a/client/src/i18n.js b/client/src/i18n.js index 2f222ab0..726cec3e 100644 --- a/client/src/i18n.js +++ b/client/src/i18n.js @@ -10,7 +10,7 @@ i18n.use(initReactI18next).use(LanguageDetector).use(HttpApi).init({ supportedLngs: ['en', 'de'], fallbackLng: 'en', backend: { - loadPath: '/locales/{{lng}}.json' + loadPath: '/assets/locales/{{lng}}.json' }, detection: { order: ['localStorage'], diff --git a/client/src/pages/Home/components/TestArea/TestAreaComponent.jsx b/client/src/pages/Home/components/TestArea/TestAreaComponent.jsx index 15a85afb..76bca9dc 100644 --- a/client/src/pages/Home/components/TestArea/TestAreaComponent.jsx +++ b/client/src/pages/Home/components/TestArea/TestAreaComponent.jsx @@ -17,7 +17,9 @@ function TestArea() { return (
{speedtests.map ? speedtests.map(test => { - let date = new Date(Date.parse(test.created)); + const dateArray = test.created.split("-").map((value) => parseInt(value)); + const date = test.type === "average" ? new Date(dateArray[0], dateArray[1] - 1, dateArray[2]) + : new Date(Date.parse(test.created)); let item = localStorage.getItem("testTime"); if ((item === "3" || item === "4") && test.type !== "average") return; diff --git a/client/src/pages/Nodes/components/NodeHeader/NodeHeader.jsx b/client/src/pages/Nodes/components/NodeHeader/NodeHeader.jsx index a277ec50..f50c817d 100644 --- a/client/src/pages/Nodes/components/NodeHeader/NodeHeader.jsx +++ b/client/src/pages/Nodes/components/NodeHeader/NodeHeader.jsx @@ -3,7 +3,7 @@ import "./styles.sass"; export const NodeHeader = () => { return (
- Logo + Logo

MySpeed

) diff --git a/client/vite.config.js b/client/vite.config.js index 114582f7..81ae74d1 100644 --- a/client/vite.config.js +++ b/client/vite.config.js @@ -1,18 +1,27 @@ -import react from '@vitejs/plugin-react' -import path from "path"; +import react from "@vitejs/plugin-react"; import {VitePWA} from "vite-plugin-pwa"; +import path from "path"; export default { plugins: [ - VitePWA({ injectRegister: 'auto' }), + VitePWA({injectRegister: "auto", manifest: false}), react() ], build: { - outDir: 'build' + outDir: "build", + chunkSizeWarningLimit: 1600, + rollupOptions: { + output: { + manualChunks(id) { + if (id.includes('node_modules')) + return id.includes('@fortawesome') ? 'icons' : 'vendor'; + } + } + } }, resolve: { alias: { - '@': path.resolve(__dirname, './src'), + "@": path.resolve(__dirname, "./src"), }, }, server: { diff --git a/package-lock.json b/package-lock.json index 0bac4e56..829dffb4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "myspeed", - "version": "1.0.7", + "version": "1.0.8", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "myspeed", - "version": "1.0.7", + "version": "1.0.8", "dependencies": { "axios": "^1.4.0", "bcrypt": "^5.1.0", diff --git a/package.json b/package.json index c31f7fe6..839d4b0e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "myspeed", - "version": "1.0.7", + "version": "1.0.8", "scripts": { "client": "cd client && npm run dev", "server": "nodemon server", @@ -17,8 +17,8 @@ "express": "^4.18.2", "node-schedule": "^2.1.1", "sequelize": "^6.31.1", - "tmp": "^0.2.1", - "sqlite3": "^5.1.6" + "sqlite3": "^5.1.6", + "tmp": "^0.2.1" }, "devDependencies": { "concurrently": "^8.0.1", diff --git a/server/controller/config.js b/server/controller/config.js index 59c6e994..4f7b264e 100644 --- a/server/controller/config.js +++ b/server/controller/config.js @@ -9,8 +9,7 @@ const configDefaults = { "serverId": "none", "password": "none", "passwordLevel": "none", - "acceptOoklaLicense": "false", - "healthChecksUrl": "https://hc-ping.com/" + "acceptOoklaLicense": "false" } module.exports.insertDefaults = async () => { diff --git a/server/integrations/gotify.js b/server/integrations/gotify.js new file mode 100644 index 00000000..7afb7429 --- /dev/null +++ b/server/integrations/gotify.js @@ -0,0 +1,39 @@ +const axios = require("axios"); +const {replaceVariables} = require("../util/variables"); + +const defaults = { + finished: "A speedtest is finished:\nPing: %ping% ms\nUpload: %upload% Mbps\nDownload: %download% Mbps", + failed: "A speedtest has failed. Reason: %error%" +} + +const postWebhook = async (url, key, triggerActivity, message, priority) => { + axios.post(`${url}/message`, {message, priority}, {headers: {"Authorization": "Bearer " + key}}) + .then(() => triggerActivity()) + .catch(() => triggerActivity(true)); +} + +module.exports = (registerEvent) => { + registerEvent('testFinished', async (integration, data, activity) => { + if (integration.data.send_finished) + await postWebhook(integration.data.url, integration.data.key, activity, + replaceVariables(integration.data.finished_message || defaults.finished, data), 3); + }); + + registerEvent('testFailed', async (integration, error, activity) => { + if (integration.data.send_failed) + await postWebhook(integration.data.url, integration.data.key, activity, + replaceVariables(integration.data.failed_message || defaults.failed, {error}), 8); + }); + + return { + icon: "fa-solid fa-bell", + fields: [ + {name: "url", type: "text", required: true, regex: /https?:\/\/.+/}, + {name: "key", type: "text", required: true, regex: /^.{15}$/}, + {name: "send_finished", type: "boolean", required: false}, + {name: "finished_message", type: "textarea", required: false}, + {name: "send_failed", type: "boolean", required: false}, + {name: "error_message", type: "textarea", required: false} + ] + }; +} \ No newline at end of file diff --git a/server/integrations/webhook.js b/server/integrations/webhook.js index 4f612c46..c81f449c 100644 --- a/server/integrations/webhook.js +++ b/server/integrations/webhook.js @@ -34,7 +34,7 @@ module.exports = (registerEvent) => { return { icon: "fa-solid fa-globe", fields: [ - {name: "url", type: "text", required: true, regex: /https:\/\/.+/}, + {name: "url", type: "text", required: true, regex: /https?:\/\/.+/}, {name: "send_started", type: "boolean", required: false}, {name: "send_finished", type: "boolean", required: false}, {name: "send_alive", type: "boolean", required: false}, diff --git a/server/routes/config.js b/server/routes/config.js index 4395e9b6..6deaf398 100644 --- a/server/routes/config.js +++ b/server/routes/config.js @@ -8,7 +8,7 @@ const password = require('../middlewares/password'); app.get("/", password(true), async (req, res) => { let configValues = {}; (await config.list()).forEach(row => { - if (row.key !== "password" && !(req.viewMode && ["healthChecksUrl", "serverId", "cron", "passwordLevel"].includes(row.key))) + if (row.key !== "password" && !(req.viewMode && ["serverId", "cron", "passwordLevel"].includes(row.key))) configValues[row.key] = row.value; }); configValues['viewMode'] = req.viewMode; diff --git a/server/routes/integrations.js b/server/routes/integrations.js index 701f28e4..ec9e4a23 100644 --- a/server/routes/integrations.js +++ b/server/routes/integrations.js @@ -3,14 +3,9 @@ const integrations = require('../controller/integrations'); const password = require('../middlewares/password'); const {validateInput} = require("../controller/integrations"); -app.get("/", password(false), (req, res) => { - return res.json(integrations.getIntegrations()); -}); +app.get("/", password(false), (req, res) => res.json(integrations.getIntegrations())); -app.get("/active", password(false), async (req, res) => { - const active = await integrations.getActive(); - return res.json(active); -}); +app.get("/active", password(false), async (req, res) => res.json(await integrations.getActive())); app.put("/:integrationName", password(false), async (req, res) => { const integration = integrations.getIntegration(req.params.integrationName); diff --git a/server/tasks/integrations.js b/server/tasks/integrations.js index 8208e9bf..6d6e594f 100644 --- a/server/tasks/integrations.js +++ b/server/tasks/integrations.js @@ -9,7 +9,7 @@ module.exports.setState = (state = "ping") => { currentState = state; } -// Send a ping to the provided healthcheck server +// Send a ping to the event system module.exports.sendPing = async (type, message) => { await triggerEvent("minutePassed", {type, message}); } @@ -19,16 +19,17 @@ module.exports.sendCurrent = async () => { if (currentState === "ping") await this.sendPing(); } -// Sends an error to the healthcheck server +// Sends an 'error' ping to the event system module.exports.sendError = async (error = "Unknown error") => { await triggerEvent("testFailed", error); } -// Sends a 'running' ping to the healthcheck server +// Sends a 'running' ping to the event system module.exports.sendRunning = async () => { await triggerEvent("testStarted"); } +// Sends a 'finished' ping to the event system module.exports.sendFinished = async (data) => { await triggerEvent("testFinished", data); }