├── .github
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ ├── bug_report_de.yml
│ ├── bug_report_en.yml
│ ├── feature_request_de.yml
│ └── feature_request_en.yml
├── PULL_REQUEST_TEMPLATE.md
├── dependabot.yml
└── workflows
│ ├── create_release.yml
│ ├── deploy_docker_dev.yml
│ ├── merge-dependabot.yml
│ └── upload_docs.yml
├── .gitignore
├── Dockerfile
├── LICENSE
├── README.de.md
├── README.md
├── client
├── .gitignore
├── index.html
├── jsconfig.json
├── package-lock.json
├── package.json
├── public
│ ├── assets
│ │ ├── img
│ │ │ ├── favicon.ico
│ │ │ ├── logo.png
│ │ │ └── logo192.png
│ │ └── locales
│ │ │ ├── bg.json
│ │ │ ├── de.json
│ │ │ ├── en.json
│ │ │ ├── es.json
│ │ │ ├── fr.json
│ │ │ ├── it.json
│ │ │ ├── nl.json
│ │ │ ├── pt.json
│ │ │ ├── ru.json
│ │ │ ├── tr.json
│ │ │ └── zh.json
│ ├── manifest.json
│ └── robots.txt
├── src
│ ├── App.jsx
│ ├── common
│ │ ├── assets
│ │ │ ├── icons
│ │ │ │ └── pushover.js
│ │ │ └── languages
│ │ │ │ ├── bg.webp
│ │ │ │ ├── br.webp
│ │ │ │ ├── de.webp
│ │ │ │ ├── en.webp
│ │ │ │ ├── es.webp
│ │ │ │ ├── fr.webp
│ │ │ │ ├── it.webp
│ │ │ │ ├── nl.webp
│ │ │ │ ├── ru.webp
│ │ │ │ ├── tr.webp
│ │ │ │ └── zh.webp
│ │ ├── components
│ │ │ ├── Dropdown
│ │ │ │ ├── DropdownComponent.jsx
│ │ │ │ ├── index.js
│ │ │ │ ├── styles.sass
│ │ │ │ └── utils
│ │ │ │ │ ├── infos.jsx
│ │ │ │ │ ├── options.js
│ │ │ │ │ └── utils.jsx
│ │ │ ├── Header
│ │ │ │ ├── HeaderComponent.jsx
│ │ │ │ ├── components
│ │ │ │ │ └── Pagination
│ │ │ │ │ │ ├── Pagination.jsx
│ │ │ │ │ │ ├── index.js
│ │ │ │ │ │ └── styles.sass
│ │ │ │ ├── index.js
│ │ │ │ ├── styles.sass
│ │ │ │ └── utils
│ │ │ │ │ └── infos.jsx
│ │ │ ├── IntegrationDialog
│ │ │ │ ├── IntegrationDialog.jsx
│ │ │ │ ├── components
│ │ │ │ │ ├── AvailableIntegrations
│ │ │ │ │ │ ├── AvailableIntegrations.jsx
│ │ │ │ │ │ ├── index.js
│ │ │ │ │ │ └── styles.sass
│ │ │ │ │ ├── IntegrationAddButton
│ │ │ │ │ │ ├── IntegrationAddButton.jsx
│ │ │ │ │ │ ├── index.js
│ │ │ │ │ │ └── styles.sass
│ │ │ │ │ ├── IntegrationItem
│ │ │ │ │ │ ├── IntegrationItem.jsx
│ │ │ │ │ │ ├── components
│ │ │ │ │ │ │ └── IntegrationItemHeader
│ │ │ │ │ │ │ │ ├── IntegrationItemHeader.jsx
│ │ │ │ │ │ │ │ ├── index.js
│ │ │ │ │ │ │ │ └── styles.sass
│ │ │ │ │ │ ├── index.js
│ │ │ │ │ │ └── styles.sass
│ │ │ │ │ └── NoIntegrationsTab
│ │ │ │ │ │ ├── NoIntegrationsTab.jsx
│ │ │ │ │ │ ├── index.js
│ │ │ │ │ │ └── styles.sass
│ │ │ │ ├── index.js
│ │ │ │ └── styles.sass
│ │ │ ├── LanguageDialog
│ │ │ │ ├── LanguageDialog.jsx
│ │ │ │ ├── index.js
│ │ │ │ └── styles.sass
│ │ │ ├── LoadingDialog
│ │ │ │ ├── LoadingDialog.jsx
│ │ │ │ ├── index.js
│ │ │ │ └── styles.sass
│ │ │ ├── ProviderDialog
│ │ │ │ ├── ProviderDialog.jsx
│ │ │ │ ├── assets
│ │ │ │ │ └── img
│ │ │ │ │ │ ├── cloudflare.webp
│ │ │ │ │ │ ├── libre.webp
│ │ │ │ │ │ └── ookla.webp
│ │ │ │ ├── index.js
│ │ │ │ └── styles.sass
│ │ │ ├── StorageDialog
│ │ │ │ ├── StorageDialog.jsx
│ │ │ │ ├── index.js
│ │ │ │ ├── styles.sass
│ │ │ │ └── tabs
│ │ │ │ │ ├── Configuration.jsx
│ │ │ │ │ └── Speedtests.jsx
│ │ │ └── WelcomeDialog
│ │ │ │ ├── WelcomeDialog.jsx
│ │ │ │ ├── banner.webp
│ │ │ │ ├── index.js
│ │ │ │ ├── steps
│ │ │ │ ├── DataHelper
│ │ │ │ │ ├── DataHelper.jsx
│ │ │ │ │ ├── index.js
│ │ │ │ │ └── styles.sass
│ │ │ │ ├── Greetings
│ │ │ │ │ ├── Greetings.jsx
│ │ │ │ │ ├── index.js
│ │ │ │ │ └── styles.sass
│ │ │ │ ├── OoklaLicense
│ │ │ │ │ ├── OoklaLicense.jsx
│ │ │ │ │ ├── index.js
│ │ │ │ │ └── styles.sass
│ │ │ │ └── ProviderChooser
│ │ │ │ │ ├── ProviderChooser.jsx
│ │ │ │ │ ├── index.js
│ │ │ │ │ └── styles.sass
│ │ │ │ └── styles.sass
│ │ ├── contexts
│ │ │ ├── Config
│ │ │ │ ├── ConfigContext.jsx
│ │ │ │ ├── dialog.jsx
│ │ │ │ └── index.js
│ │ │ ├── Dialog
│ │ │ │ ├── DialogContext.jsx
│ │ │ │ ├── index.js
│ │ │ │ └── styles.sass
│ │ │ ├── InputDialog
│ │ │ │ ├── InputDialog.jsx
│ │ │ │ ├── index.js
│ │ │ │ └── styles.sass
│ │ │ ├── Node
│ │ │ │ ├── NodeContext.jsx
│ │ │ │ └── index.js
│ │ │ ├── Speedtests
│ │ │ │ ├── SpeedtestContext.jsx
│ │ │ │ └── index.js
│ │ │ ├── Status
│ │ │ │ ├── StatusContext.jsx
│ │ │ │ └── index.js
│ │ │ └── ToastNotification
│ │ │ │ ├── ToastNotificationContext.jsx
│ │ │ │ ├── index.js
│ │ │ │ └── styles.sass
│ │ ├── styles
│ │ │ ├── _colors.sass
│ │ │ ├── default.sass
│ │ │ └── spinner.sass
│ │ └── utils
│ │ │ ├── RequestUtil.js
│ │ │ └── TestUtil.js
│ ├── i18n.js
│ ├── index.jsx
│ └── pages
│ │ ├── Error
│ │ ├── Error.jsx
│ │ ├── index.js
│ │ └── styles.sass
│ │ ├── Home
│ │ ├── Home.jsx
│ │ ├── components
│ │ │ ├── LatestTest
│ │ │ │ ├── LatestTestComponent.jsx
│ │ │ │ ├── index.js
│ │ │ │ ├── styles.sass
│ │ │ │ ├── utils.js
│ │ │ │ └── utils
│ │ │ │ │ └── dialogs.jsx
│ │ │ ├── Speedtest
│ │ │ │ ├── SpeedtestComponent.jsx
│ │ │ │ ├── index.js
│ │ │ │ ├── styles.sass
│ │ │ │ └── utils
│ │ │ │ │ ├── errors.js
│ │ │ │ │ └── infos.jsx
│ │ │ └── TestArea
│ │ │ │ ├── TestAreaComponent.jsx
│ │ │ │ ├── index.js
│ │ │ │ └── styles.sass
│ │ └── index.js
│ │ ├── Loading
│ │ ├── Loading.jsx
│ │ ├── index.js
│ │ └── styles.sass
│ │ ├── Nodes
│ │ ├── Nodes.jsx
│ │ ├── components
│ │ │ ├── CreateNodeDialog
│ │ │ │ ├── CreateNodeDialog.jsx
│ │ │ │ ├── index.js
│ │ │ │ └── styles.sass
│ │ │ ├── NodeContainer
│ │ │ │ ├── NodeContainer.jsx
│ │ │ │ ├── index.js
│ │ │ │ └── styles.sass
│ │ │ └── NodeHeader
│ │ │ │ ├── NodeHeader.jsx
│ │ │ │ ├── index.js
│ │ │ │ └── styles.sass
│ │ ├── index.js
│ │ └── styles.sass
│ │ └── Statistics
│ │ ├── Statistics.jsx
│ │ ├── charts
│ │ ├── AverageChart
│ │ │ ├── AverageChart.jsx
│ │ │ ├── index.js
│ │ │ └── styles.sass
│ │ ├── DurationChart.jsx
│ │ ├── FailedChart.jsx
│ │ ├── LatestTestChart
│ │ │ ├── LatestTestChart.jsx
│ │ │ ├── index.js
│ │ │ └── styles.sass
│ │ ├── ManualChart.jsx
│ │ ├── OverviewChart
│ │ │ ├── OverviewChart.jsx
│ │ │ ├── index.js
│ │ │ └── styles.sass
│ │ ├── PingChart.jsx
│ │ └── SpeedChart.jsx
│ │ ├── components
│ │ └── StatisticContainer
│ │ │ ├── StatisticContainer.jsx
│ │ │ ├── index.js
│ │ │ └── styles.sass
│ │ ├── index.js
│ │ └── styles.sass
└── vite.config.js
├── crowdin.yml
├── docs
├── CNAME
├── assets
│ └── images
│ │ ├── de
│ │ ├── integrations.png
│ │ ├── interface.png
│ │ ├── latest.png
│ │ ├── settings.png
│ │ ├── tests.png
│ │ └── view.png
│ │ ├── en
│ │ ├── integrations.png
│ │ ├── interface.png
│ │ ├── latest.png
│ │ ├── settings.png
│ │ ├── tests.png
│ │ └── view.png
│ │ ├── latest_test.png
│ │ └── logo.png
├── faq.de.md
├── faq.en.md
├── guides
│ ├── reverse-proxy.de.md
│ └── reverse-proxy.en.md
├── index.de.md
├── index.en.md
├── instructions
│ ├── main.de.md
│ ├── main.en.md
│ ├── settings.de.md
│ └── settings.en.md
├── setup
│ ├── linux.de.md
│ ├── linux.en.md
│ ├── windows.de.md
│ └── windows.en.md
├── troubleshooting.de.md
└── troubleshooting.en.md
├── mkdocs.yml
├── package-lock.json
├── package.json
├── scripts
├── chooser.sh
├── docker-install.sh
├── install.sh
└── uninstall.sh
├── server
├── config
│ ├── binaries.js
│ └── database.js
├── controller
│ ├── config.js
│ ├── integrations.js
│ ├── node.js
│ ├── opengraph.js
│ ├── pause.js
│ ├── recommendations.js
│ ├── servers.js
│ └── speedtests.js
├── index.js
├── integrations
│ ├── discord.js
│ ├── gotify.js
│ ├── healthChecks.js
│ ├── pushover.js
│ ├── telegram.js
│ └── webhook.js
├── middlewares
│ ├── error.js
│ ├── password.js
│ └── passwordWrapper.js
├── models
│ ├── Config.js
│ ├── IntegrationData.js
│ ├── Node.js
│ ├── Recommendations.js
│ └── Speedtests.js
├── routes
│ ├── config.js
│ ├── integrations.js
│ ├── nodes.js
│ ├── opengraph.js
│ ├── prometheus.js
│ ├── recommendations.js
│ ├── speedtests.js
│ ├── storage.js
│ └── system.js
├── tasks
│ ├── cloudflare.js
│ ├── integrations.js
│ ├── speedtest.js
│ └── timer.js
├── templates
│ └── env.html
└── util
│ ├── createFolders.js
│ ├── errorHandler.js
│ ├── helpers.js
│ ├── loadCli.js
│ ├── loadInterfaces.js
│ ├── loadServers.js
│ ├── providers
│ ├── loadLibre.js
│ ├── loadOokla.js
│ └── parseData.js
│ └── speedtest.js
└── web
├── .eslintrc.cjs
├── .gitignore
├── index.html
├── jsconfig.json
├── package-lock.json
├── package.json
├── public
├── assets
│ ├── fonts
│ │ ├── inter-v12-latin-300.ttf
│ │ ├── inter-v12-latin-300.woff2
│ │ ├── inter-v12-latin-500.ttf
│ │ ├── inter-v12-latin-500.woff2
│ │ ├── inter-v12-latin-700.ttf
│ │ ├── inter-v12-latin-700.woff2
│ │ ├── inter-v12-latin-900.ttf
│ │ ├── inter-v12-latin-900.woff2
│ │ ├── inter-v12-latin-regular.ttf
│ │ └── inter-v12-latin-regular.woff2
│ └── img
│ │ ├── favicon.ico
│ │ ├── logo.png
│ │ └── logo192.png
└── robots.txt
├── src
├── common
│ ├── assets
│ │ ├── background.png
│ │ ├── feature
│ │ │ ├── cron.png
│ │ │ ├── integrations.png
│ │ │ ├── language.png
│ │ │ └── views.png
│ │ ├── interface.png
│ │ ├── logo192.png
│ │ ├── logo_docs.png
│ │ ├── not_found.svg
│ │ ├── sc1.png
│ │ └── sc2.png
│ ├── components
│ │ ├── Button
│ │ │ ├── Button.jsx
│ │ │ ├── index.js
│ │ │ └── styles.sass
│ │ └── Navigation
│ │ │ ├── Navigation.jsx
│ │ │ ├── index.js
│ │ │ └── styles.sass
│ ├── layouts
│ │ └── Root
│ │ │ ├── Root.jsx
│ │ │ ├── index.js
│ │ │ └── styles.sass
│ └── styles
│ │ ├── _colors.sass
│ │ ├── default.sass
│ │ └── fonts.sass
├── main.jsx
└── pages
│ ├── Home
│ ├── Home.jsx
│ ├── components
│ │ ├── FeatureGrid
│ │ │ ├── FeatureGrid.jsx
│ │ │ ├── index.js
│ │ │ └── styles.sass
│ │ ├── Features
│ │ │ ├── Features.jsx
│ │ │ ├── index.js
│ │ │ └── styles.sass
│ │ ├── Footer
│ │ │ ├── Footer.jsx
│ │ │ ├── index.js
│ │ │ └── styles.sass
│ │ └── GetStarted
│ │ │ ├── GetStarted.jsx
│ │ │ ├── index.js
│ │ │ └── styles.sass
│ ├── index.js
│ └── styles.sass
│ ├── Imprint
│ ├── Imprint.jsx
│ ├── index.js
│ └── styles.sass
│ ├── Install
│ ├── Install.jsx
│ ├── index.js
│ └── styles.sass
│ ├── NotFound
│ ├── NotFound.jsx
│ ├── index.js
│ └── styles.sass
│ ├── Privacy
│ ├── Privacy.jsx
│ ├── index.js
│ └── styles.sass
│ ├── TutorialSubmission
│ ├── TutorialSubmission.jsx
│ ├── index.js
│ └── styles.sass
│ └── Tutorials
│ ├── Tutorials.jsx
│ ├── index.js
│ ├── sources
│ ├── blog_posts.jsx
│ ├── channels
│ │ ├── addrom.webp
│ │ ├── belginux.webp
│ │ ├── bigbeartechworld.png
│ │ ├── dbtech.webp
│ │ ├── gigazine.png
│ │ ├── linuxiac.webp
│ │ ├── mariushosting.png
│ │ ├── pavl21.png
│ │ ├── retromiketech.png
│ │ └── ubunlog.png
│ ├── thumbs
│ │ ├── 20240128.webp
│ │ ├── 7108075382452079878.webp
│ │ ├── 7roj87Fytz0.webp
│ │ ├── Iic14oUCCVo.webp
│ │ ├── MFbeWdKesTE.webp
│ │ ├── SM3RJRktwIk.webp
│ │ ├── ZIIa6yF-Tvo.webp
│ │ ├── addrom-myspeed.webp
│ │ ├── belginux-myspeed.webp
│ │ ├── linuxiac-myspeed.webp
│ │ ├── marius-myspeed.webp
│ │ ├── tBJmhgn3ZOM.webp
│ │ └── ubunlog-myspeed.webp
│ └── videos.jsx
│ └── styles.sass
└── vite.config.js
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: gnmyt
2 | ko_fi: gnmyt
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report_de.yml:
--------------------------------------------------------------------------------
1 | name: 🐛 Fehler melden
2 | description: Melde einen Fehler oder Bug in MySpeed (🇩🇪)
3 | title: "[Fehler] "
4 | labels: ["bug"]
5 | body:
6 | - type: checkboxes
7 | attributes:
8 | label: Allgemeines
9 | description: Bitte bestätige, dass die Folgenden aussagen zutreffen
10 | options:
11 | - label: Ich habe auf die neuste Version von MySpeed aktualisiert.
12 | required: true
13 | - label: Mein Bug wurde noch nicht gemeldet
14 | required: true
15 | - type: textarea
16 | attributes:
17 | label: Der Fehler
18 | description: Beschreibe den Bug/Fehler genau ins Detail. Füge falls vorhanden auch Screenshots hinzu und gib an, wie man den Fehler reproduzieren kann
19 | placeholder: Es erscheint ein Fehler bei ...
20 | validations:
21 | required: true
22 | - type: dropdown
23 | id: browsers
24 | attributes:
25 | label: Auf welchem Gerät rufst du die Seite auf?
26 | multiple: true
27 | options:
28 | - Im Browser
29 | - Auf dem Handy
30 | - Auf einem Tablet
31 | validations:
32 | required: true
33 | - type: dropdown
34 | id: server
35 | attributes:
36 | label: Auf welchem Betriebssystem läuft deine MySpeed-Instanz?
37 | multiple: true
38 | options:
39 | - Linux
40 | - Windows
41 | - macOS
42 | validations:
43 | required: true
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report_en.yml:
--------------------------------------------------------------------------------
1 | name: 🐛 Report a Bug
2 | description: Report a bug or issue in MySpeed (🇬🇧)
3 | title: "[Bug] "
4 | labels: ["bug"]
5 | body:
6 | - type: checkboxes
7 | attributes:
8 | label: General
9 | description: Please confirm the following statements
10 | options:
11 | - label: I have updated to the latest version of MySpeed.
12 | required: true
13 | - label: My bug has not been reported yet.
14 | required: true
15 | - type: textarea
16 | attributes:
17 | label: The Bug
18 | description: Describe the bug/issue in detail. If possible, include screenshots and provide steps to reproduce the error.
19 | placeholder: An error occurs when ...
20 | validations:
21 | required: true
22 | - type: dropdown
23 | id: browsers
24 | attributes:
25 | label: What device are you using to access the page?
26 | multiple: true
27 | options:
28 | - In the browser
29 | - On mobile
30 | - On a tablet
31 | validations:
32 | required: true
33 | - type: dropdown
34 | id: server
35 | attributes:
36 | label: Which operating system is your MySpeed instance running on?
37 | multiple: true
38 | options:
39 | - Linux
40 | - Windows
41 | - macOS
42 | validations:
43 | required: true
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request_de.yml:
--------------------------------------------------------------------------------
1 | name: 💡 Idee vorschlagen
2 | description: Hast du eine Idee? Hier bist du richtig (🇩🇪)
3 | title: "[Feature] "
4 | labels: ["enhancement"]
5 | body:
6 | - type: checkboxes
7 | attributes:
8 | label: Allgemeines
9 | description: Bitte bestätige, dass die Folgenden aussagen zutreffen
10 | options:
11 | - label: Mein Feature existiert noch nicht in der neusten Version von MySpeed.
12 | required: true
13 | - label: Ich habe überprüft, dass mein Feature noch von niemanden vorgeschlagen wurde.
14 | required: true
15 | - type: textarea
16 | attributes:
17 | label: Deine Idee
18 | description: Was können wir hinzufügen? Beschreibe deine Idee so genau wie möglich
19 | placeholder: Meine Idee wäre es, ...
20 | validations:
21 | required: true
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request_en.yml:
--------------------------------------------------------------------------------
1 | name: 💡 Suggest idea
2 | description: Do you have an idea? You're in the right place (🇬🇧)
3 | title: "[Feature] "
4 | labels: ["enhancement"]
5 | body:
6 | - type: checkboxes
7 | attributes:
8 | label: General
9 | description: Please confirm the following statements
10 | options:
11 | - label: My feature does not exist in the latest version of MySpeed.
12 | required: true
13 | - label: I have checked that my feature has not been suggested by anyone else.
14 | required: true
15 | - type: textarea
16 | attributes:
17 | label: Your Idea
18 | description: What can we add? Describe your idea as precisely as possible.
19 | placeholder: My idea would be to ...
20 | validations:
21 | required: true
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ## 📋 Description
2 |
3 | Please include a summary of the changes and the related issue. Explain the problem that you are solving and provide the
4 | necessary context.
5 |
6 | ## 🚀 Changes made to ...
7 |
8 | - [ ] 🔧 Server
9 | - [ ] 🖥️ Client
10 | - [ ] 📚 Documentation
11 | - [ ] 🔄 Other: ___
12 |
13 | ## ✅ Checklist
14 |
15 | - [ ] My code follows the style guidelines of this project
16 | - [ ] I have performed a self-review of my own code
17 | - [ ] I have looked for similar pull requests in the repository and found none
18 |
19 | ## 🔗 Related Issues
20 |
21 | Fixes #(issue)
22 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: 'npm'
4 | directory: '/'
5 | schedule:
6 | interval: 'daily'
7 | - package-ecosystem: 'npm'
8 | directory: '/client'
9 | schedule:
10 | interval: 'daily'
--------------------------------------------------------------------------------
/.github/workflows/create_release.yml:
--------------------------------------------------------------------------------
1 | name: Upload the latest release
2 |
3 | on:
4 | push:
5 | tags: [ "v*" ]
6 |
7 | jobs:
8 | create:
9 | name: "Creates the newest release by version"
10 | runs-on: "ubuntu-latest"
11 |
12 | steps:
13 | - name: Checkout project
14 | uses: actions/checkout@v2.3.4
15 |
16 | - name: Setup node
17 | uses: actions/setup-node@v3
18 | with:
19 | node-version: '16'
20 |
21 | - name: Set up QEMU
22 | uses: docker/setup-qemu-action@master
23 | with:
24 | platforms: all
25 |
26 | - name: Set up Docker Build
27 | uses: docker/setup-buildx-action@v2
28 |
29 | - name: Login to DockerHub
30 | uses: docker/login-action@v2
31 | with:
32 | username: ${{ secrets.DOCKERHUB_USERNAME }}
33 | password: ${{ secrets.DOCKERHUB_TOKEN }}
34 |
35 | - run: cd client && npm install --force
36 | - run: npm run build && mv client/build .
37 |
38 | - name: Get version
39 | id: get_version
40 | run: echo "::set-output name=version::$(jq .version package.json --raw-output)"
41 |
42 | - name: Install zip
43 | run: sudo apt-get install zip
44 |
45 | - name: Zip all files
46 | run: zip -r MySpeed-${{ steps.get_version.outputs.version }}.zip build server package.json package-lock.json
47 |
48 | - uses: "marvinpinto/action-automatic-releases@latest"
49 | with:
50 | repo_token: "${{ secrets.GITHUB_TOKEN }}"
51 | prerelease: false
52 | title: Release ${{ steps.get_version.outputs.version }}
53 | files: |
54 | ./MySpeed-*.zip
55 |
56 | - name: Build and push
57 | uses: docker/build-push-action@v3
58 | with:
59 | push: true
60 | platforms: linux/amd64,linux/arm64,linux/arm/v7
61 | tags: |
62 | germannewsmaker/myspeed:latest
63 | germannewsmaker/myspeed:${{ steps.get_version.outputs.version }}
--------------------------------------------------------------------------------
/.github/workflows/deploy_docker_dev.yml:
--------------------------------------------------------------------------------
1 | name: Deploy Development Release to DockerHub
2 |
3 | on:
4 | push:
5 | branches: [ "development" ]
6 |
7 | jobs:
8 | docker:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - name: Checkout
12 | uses: actions/checkout@v3
13 |
14 | - name: Set up QEMU
15 | uses: docker/setup-qemu-action@master
16 | with:
17 | platforms: all
18 |
19 | - name: Set up Docker Build
20 | uses: docker/setup-buildx-action@v2
21 |
22 | - name: Login to DockerHub
23 | uses: docker/login-action@v2
24 | with:
25 | username: ${{ secrets.DOCKERHUB_USERNAME }}
26 | password: ${{ secrets.DOCKERHUB_TOKEN }}
27 |
28 | - name: Build and push
29 | uses: docker/build-push-action@v3
30 | with:
31 | push: true
32 | platforms: linux/amd64,linux/arm64,linux/arm/v7
33 | tags: germannewsmaker/myspeed:development
--------------------------------------------------------------------------------
/.github/workflows/merge-dependabot.yml:
--------------------------------------------------------------------------------
1 | name: Dependabot auto-approve
2 | on: pull_request
3 |
4 | permissions:
5 | contents: write
6 | pull-requests: write
7 |
8 | jobs:
9 | dependabot:
10 | runs-on: ubuntu-latest
11 | if: github.event.pull_request.user.login == 'dependabot[bot]' && github.repository == 'gnmyt/myspeed'
12 | steps:
13 | - name: Dependabot metadata
14 | id: metadata
15 | uses: dependabot/fetch-metadata@v2
16 | with:
17 | github-token: "${{ secrets.GITHUB_TOKEN }}"
18 | - name: Approve a PR
19 | run: gh pr merge --auto --merge "$PR_URL"
20 | env:
21 | PR_URL: ${{github.event.pull_request.html_url}}
22 | GH_TOKEN: ${{secrets.GITHUB_TOKEN}}
--------------------------------------------------------------------------------
/.github/workflows/upload_docs.yml:
--------------------------------------------------------------------------------
1 | name: Deploy documentation to GitHub
2 | on:
3 | push:
4 | branches: [ "development" ]
5 | paths: [ "docs/**", "mkdocs.yml" ]
6 |
7 | jobs:
8 | deploy:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v3
12 |
13 | - uses: actions/setup-python@v4
14 | with:
15 | python-version: 3.9
16 |
17 | - name: Install dependencies
18 | run: pip install mkdocs-material mkdocs-static-i18n
19 |
20 | - name: Deploy documentation
21 | run: mkdocs gh-deploy --force
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # code editors
2 | /.idea
3 |
4 | # dependencies
5 | /node_modules
6 | /client/node_modules
7 | /.pnp
8 | .pnp.js
9 |
10 |
11 | # production
12 | /build
13 | /data
14 | /bin
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:22-alpine AS build
2 | RUN apk add --no-cache g++ make cmake python3 py3-setuptools
3 |
4 | WORKDIR /myspeed
5 |
6 | COPY ./client ./client
7 | COPY ./server ./server
8 | COPY ./package.json ./package.json
9 |
10 | RUN yarn install
11 | RUN cd client && yarn install --force
12 | RUN npm run build
13 | RUN mv /myspeed/client/build /myspeed
14 |
15 | FROM node:22-alpine
16 |
17 | RUN apk add --no-cache tzdata
18 |
19 | ENV NODE_ENV=production
20 | ENV TZ=Etc/UTC
21 |
22 | WORKDIR /myspeed
23 |
24 | COPY --from=build /myspeed/build /myspeed/build
25 | COPY --from=build /myspeed/server /myspeed/server
26 | COPY --from=build /myspeed/node_modules /myspeed/node_modules
27 | COPY --from=build /myspeed/package.json /myspeed/package.json
28 |
29 | VOLUME ["/myspeed/data"]
30 |
31 | EXPOSE 5216
32 |
33 | CMD ["node", "server"]
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Mathias Wagner
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/client/.gitignore:
--------------------------------------------------------------------------------
1 | # dependencies
2 | /node_modules
3 | /.pnp
4 | .pnp.js
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env.local
15 | .env.development.local
16 | .env.test.local
17 | .env.production.local
18 |
19 | npm-debug.log*
20 | yarn-debug.log*
21 | yarn-error.log*
22 |
--------------------------------------------------------------------------------
/client/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | MySpeed
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/client/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "paths": {
5 | "@/*": ["./src/*"]
6 | }
7 | }
8 | }
--------------------------------------------------------------------------------
/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "client",
3 | "version": "1.0.9",
4 | "scripts": {
5 | "dev": "vite",
6 | "build": "vite build",
7 | "preview": "vite preview"
8 | },
9 | "dependencies": {
10 | "@fontsource/inter": "^5.2.5",
11 | "@fortawesome/fontawesome-svg-core": "^6.7.2",
12 | "@fortawesome/free-brands-svg-icons": "^6.7.2",
13 | "@fortawesome/free-solid-svg-icons": "^6.7.2",
14 | "@fortawesome/react-fontawesome": "^0.2.2",
15 | "@vitejs/plugin-react": "^4.5.0",
16 | "chart.js": "^4.4.9",
17 | "cron-parser": "^5.2.0",
18 | "i18next": "^25.2.1",
19 | "i18next-browser-languagedetector": "^8.1.0",
20 | "i18next-http-backend": "^3.0.2",
21 | "react": "^19.1.0",
22 | "react-chartjs-2": "^5.3.0",
23 | "react-dom": "^19.1.0",
24 | "react-i18next": "^15.5.2",
25 | "react-router-dom": "^7.6.1",
26 | "sass": "^1.89.1",
27 | "uuid": "^11.1.0",
28 | "vite": "^6.3.5",
29 | "vite-plugin-pwa": "^1.0.0"
30 | },
31 | "devDependencies": {
32 | "@types/react": "^19.1.6",
33 | "@types/react-dom": "^19.1.5"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/client/public/assets/img/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnmyt/MySpeed/5b41668ee5ecb3bde8898e32e5dacba0e523ef03/client/public/assets/img/favicon.ico
--------------------------------------------------------------------------------
/client/public/assets/img/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnmyt/MySpeed/5b41668ee5ecb3bde8898e32e5dacba0e523ef03/client/public/assets/img/logo.png
--------------------------------------------------------------------------------
/client/public/assets/img/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnmyt/MySpeed/5b41668ee5ecb3bde8898e32e5dacba0e523ef03/client/public/assets/img/logo192.png
--------------------------------------------------------------------------------
/client/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "MySpeed",
3 | "name": "MySpeed - Speedtests",
4 | "description": "A speed test analysis software that shows your internet speed for up to 30 days",
5 | "icons": [
6 | {
7 | "src": "/assets/img/favicon.ico",
8 | "sizes": "64x64 32x32 24x24 16x16",
9 | "type": "image/x-icon"
10 | },
11 | {
12 | "src": "/assets/img/logo192.png",
13 | "type": "image/png",
14 | "sizes": "192x192",
15 | "purpose": "maskable"
16 | },
17 | {
18 | "src": "/assets/img/logo.png",
19 | "type": "image/png",
20 | "sizes": "512x512"
21 | }
22 | ],
23 | "start_url": ".",
24 | "display": "standalone",
25 | "theme_color": "#232835",
26 | "background_color": "#232835"
27 | }
28 |
--------------------------------------------------------------------------------
/client/public/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Disallow:
--------------------------------------------------------------------------------
/client/src/common/assets/languages/bg.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnmyt/MySpeed/5b41668ee5ecb3bde8898e32e5dacba0e523ef03/client/src/common/assets/languages/bg.webp
--------------------------------------------------------------------------------
/client/src/common/assets/languages/br.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnmyt/MySpeed/5b41668ee5ecb3bde8898e32e5dacba0e523ef03/client/src/common/assets/languages/br.webp
--------------------------------------------------------------------------------
/client/src/common/assets/languages/de.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnmyt/MySpeed/5b41668ee5ecb3bde8898e32e5dacba0e523ef03/client/src/common/assets/languages/de.webp
--------------------------------------------------------------------------------
/client/src/common/assets/languages/en.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnmyt/MySpeed/5b41668ee5ecb3bde8898e32e5dacba0e523ef03/client/src/common/assets/languages/en.webp
--------------------------------------------------------------------------------
/client/src/common/assets/languages/es.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnmyt/MySpeed/5b41668ee5ecb3bde8898e32e5dacba0e523ef03/client/src/common/assets/languages/es.webp
--------------------------------------------------------------------------------
/client/src/common/assets/languages/fr.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnmyt/MySpeed/5b41668ee5ecb3bde8898e32e5dacba0e523ef03/client/src/common/assets/languages/fr.webp
--------------------------------------------------------------------------------
/client/src/common/assets/languages/it.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnmyt/MySpeed/5b41668ee5ecb3bde8898e32e5dacba0e523ef03/client/src/common/assets/languages/it.webp
--------------------------------------------------------------------------------
/client/src/common/assets/languages/nl.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnmyt/MySpeed/5b41668ee5ecb3bde8898e32e5dacba0e523ef03/client/src/common/assets/languages/nl.webp
--------------------------------------------------------------------------------
/client/src/common/assets/languages/ru.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnmyt/MySpeed/5b41668ee5ecb3bde8898e32e5dacba0e523ef03/client/src/common/assets/languages/ru.webp
--------------------------------------------------------------------------------
/client/src/common/assets/languages/tr.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnmyt/MySpeed/5b41668ee5ecb3bde8898e32e5dacba0e523ef03/client/src/common/assets/languages/tr.webp
--------------------------------------------------------------------------------
/client/src/common/assets/languages/zh.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnmyt/MySpeed/5b41668ee5ecb3bde8898e32e5dacba0e523ef03/client/src/common/assets/languages/zh.webp
--------------------------------------------------------------------------------
/client/src/common/components/Dropdown/index.js:
--------------------------------------------------------------------------------
1 | export {default} from './DropdownComponent';
--------------------------------------------------------------------------------
/client/src/common/components/Dropdown/styles.sass:
--------------------------------------------------------------------------------
1 | @import "@/common/styles/colors"
2 |
3 | .dropdown
4 | visibility: visible
5 | opacity: 1
6 | transition: opacity 0.1s linear
7 | user-select: none
8 |
9 | .dropdown-content
10 | float: right
11 | margin-right: 10%
12 | display: inline-block
13 | position: absolute
14 | width: auto
15 | overflow: auto
16 | border-radius: 10px
17 | box-shadow: 0 8px 16px 0 $dark-gray
18 | border: 1px solid $light-gray
19 | right: 0
20 | z-index: 1
21 | padding: 15px
22 | background-color: $dark-gray
23 |
24 | .dropdown-invisible
25 | visibility: hidden
26 | opacity: 0
27 | transition: visibility 0s 0.1s, opacity 0.1s linear
28 |
29 | .dropdown-content h2
30 | color: $subtext
31 | margin: 0
32 | font-size: 16pt
33 |
34 | .dropdown-hr
35 | border: 1px solid $light-gray
36 | margin: 5px
37 | width: 30px
38 |
39 | .dropdown-entries
40 | margin-top: 10px
41 | display: flex
42 | flex-direction: column
43 |
44 | .dropdown-item
45 | margin: 5px 10px
46 | display: flex
47 | align-items: center
48 | cursor: pointer
49 | color: $white
50 |
51 | .dropdown-item *
52 | margin: 0
53 | font-size: 16pt
54 | font-weight: 500
55 |
56 | .dropdown-item:hover
57 | color: $green
58 |
59 | .dropdown-item svg
60 | width: 25px
61 | height: 25px
62 |
63 | .dropdown-item h3
64 | margin-left: 15px
65 |
66 | .center
67 | display: flex
68 | justify-content: center
69 |
70 | @media (max-width: 390px)
71 | .dropdown-content
72 | margin-right: 0
--------------------------------------------------------------------------------
/client/src/common/components/Dropdown/utils/infos.jsx:
--------------------------------------------------------------------------------
1 | import {DONATION_URL, PROJECT_URL, WEB_URL} from "@/index";
2 | import {Trans} from "react-i18next";
3 |
4 |
5 | export const creditsInfo = () => ,
6 | Github: , Donate: }}>info.credits
7 |
8 | export const recommendationsInfo = (ping, down, up) => }}
9 | values={{ping, down, up}}>info.recommendations_info
--------------------------------------------------------------------------------
/client/src/common/components/Dropdown/utils/options.js:
--------------------------------------------------------------------------------
1 | import {t} from "i18next";
2 |
3 | export const timeOptions = () => ({
4 | 1: t("options.time.24hours"),
5 | 2: t("options.time.2days"),
6 | 3: t("options.time.7days"),
7 | 4: t("options.time.30days")
8 | });
9 |
10 | export const levelOptions = () => ({
11 | "none": t("options.level.no_access"),
12 | "read": t("options.level.read_access")
13 | });
14 |
15 | export const selectOptions = () => ({
16 | "* * * * *": t("options.cron.continuous"),
17 | "0,30 * * * *": t("options.cron.frequent"),
18 | "0 * * * *": t("options.cron.default"),
19 | "0 0,3,6,9,12,15,18,21 * * *": t("options.cron.rare"),
20 | "0 0,6,12,18 * * *": t("options.cron.really_rare")
21 | });
--------------------------------------------------------------------------------
/client/src/common/components/Dropdown/utils/utils.jsx:
--------------------------------------------------------------------------------
1 | import {parseExpression} from "cron-parser";
2 | import {t} from "i18next";
3 |
4 | // Parses cron as a locale string
5 | export const parseCron = (cron) => {
6 | try {
7 | return parseExpression(cron).next().toDate().toLocaleString()
8 | } catch (e) {
9 | return {t("dropdown.invalid")};
10 | }
11 | }
12 |
13 | // Fixes the cron provided by the user
14 | export const stringifyCron = (cron) => {
15 | try {
16 | return parseExpression(cron).stringify();
17 | } catch (e) {
18 | return null;
19 | }
20 | }
--------------------------------------------------------------------------------
/client/src/common/components/Header/components/Pagination/index.js:
--------------------------------------------------------------------------------
1 | export {Pagination as default} from "./Pagination";
--------------------------------------------------------------------------------
/client/src/common/components/Header/components/Pagination/styles.sass:
--------------------------------------------------------------------------------
1 | @import "@/common/styles/colors"
2 |
3 | .pagination
4 | display: flex
5 | justify-content: center
6 | background-color: $dark-gray
7 | border: 2px solid $light-gray
8 | padding: 0.7rem 0.8rem
9 | gap: 1rem
10 | border-radius: 1rem
11 | position: relative
12 | overflow: hidden
13 | box-sizing: border-box
14 | user-select: none
15 |
16 | .pagination-item
17 | display: flex
18 | color: $subtext
19 | justify-content: center
20 | align-items: center
21 | cursor: pointer
22 | padding: 0.5rem 1.5rem
23 | gap: 1rem
24 | border-radius: 0.5rem
25 | position: relative
26 | z-index: 1
27 |
28 | &:hover
29 | color: $white
30 |
31 | p
32 | margin: 0
33 | font-weight: 500
34 | font-size: 16pt
35 | svg
36 | font-size: 16pt
37 |
38 |
39 |
40 | .page-active
41 | color: $white
42 |
43 | .pagination-active-background
44 | position: absolute
45 | top: 50%
46 | left: var(--active-left, 0)
47 | width: var(--active-width, 0)
48 | height: calc(100% - 1rem)
49 | background-color: $light-gray
50 | border-radius: 0.8rem
51 | transition: left 0.3s ease, width 0.3s ease
52 | transform: translateY(-50%)
53 | z-index: 0
54 |
55 |
56 | @media (max-width: 968px)
57 | .pagination-item
58 | gap: 0.5rem
59 |
60 | p
61 | display: none
62 |
63 |
64 | @media (max-width: 768px)
65 | .pagination
66 | position: fixed
67 | bottom: 1rem
68 | left: 50%
69 | transform: translateX(-50%)
70 | z-index: 100
71 | box-shadow: 0 0 10px 0 $light-gray
--------------------------------------------------------------------------------
/client/src/common/components/Header/index.js:
--------------------------------------------------------------------------------
1 | export {default} from './HeaderComponent';
--------------------------------------------------------------------------------
/client/src/common/components/Header/utils/infos.jsx:
--------------------------------------------------------------------------------
1 | import {Trans} from "react-i18next";
2 | import {PROJECT_URL, PROJECT_WIKI} from "@/index";
3 |
4 | export const updateInfo = (version) => ,
5 | DLLink: }}>info.update
--------------------------------------------------------------------------------
/client/src/common/components/IntegrationDialog/components/AvailableIntegrations/AvailableIntegrations.jsx:
--------------------------------------------------------------------------------
1 | import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
2 | import React from "react";
3 | import {t} from "i18next";
4 | import "./styles.sass";
5 |
6 | export const AvailableIntegrations = ({integrations, currentTab, setCurrentTab}) => (
7 |
8 | {Object.keys(integrations).map((key) =>
setCurrentTab(key)}>
11 |
12 |
13 |
{t(`integrations.${key}.title`)}
14 |
)}
15 |
16 | )
--------------------------------------------------------------------------------
/client/src/common/components/IntegrationDialog/components/AvailableIntegrations/index.js:
--------------------------------------------------------------------------------
1 | export {AvailableIntegrations as default} from './AvailableIntegrations';
--------------------------------------------------------------------------------
/client/src/common/components/IntegrationDialog/components/AvailableIntegrations/styles.sass:
--------------------------------------------------------------------------------
1 | @import "@/common/styles/colors"
2 |
3 | .available-integrations
4 | display: flex
5 | flex-direction: column
6 | gap: 0.5rem
7 | user-select: none
8 | overflow-x: hidden
9 | overflow-y: scroll
10 |
11 | .integration-tab
12 | display: flex
13 | align-items: center
14 | gap: 0.5rem
15 | color: $subtext
16 | padding: 0.6rem 0.7rem
17 | border: 2px solid transparent
18 | border-radius: 1rem
19 | cursor: pointer
20 |
21 | svg
22 | width: 1.3rem
23 | height: 1.3rem
24 | p
25 | margin: 0
26 | font-size: 14pt
27 |
28 | .integration-active
29 | background-color: $light-gray
30 | color: $white
31 |
32 | @media (max-width: 781px)
33 | .available-integrations
34 | flex-direction: row
35 | overflow-x: scroll
36 | overflow-y: hidden
37 | gap: 0.5rem
--------------------------------------------------------------------------------
/client/src/common/components/IntegrationDialog/components/IntegrationAddButton/IntegrationAddButton.jsx:
--------------------------------------------------------------------------------
1 | import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
2 | import {faAdd} from "@fortawesome/free-solid-svg-icons";
3 | import {t} from "i18next";
4 | import React from "react";
5 | import "./styles.sass";
6 |
7 | export const IntegrationAddButton = ({onClick}) => (
8 |
9 |
10 |
11 | {t("integrations.create")}
12 |
13 |
14 | )
--------------------------------------------------------------------------------
/client/src/common/components/IntegrationDialog/components/IntegrationAddButton/index.js:
--------------------------------------------------------------------------------
1 | export {IntegrationAddButton as default} from "./IntegrationAddButton";
--------------------------------------------------------------------------------
/client/src/common/components/IntegrationDialog/components/IntegrationAddButton/styles.sass:
--------------------------------------------------------------------------------
1 | @import "@/common/styles/colors"
2 |
3 | .add-container
4 | width: 100%
5 | display: flex
6 | justify-content: flex-end
7 |
8 | .add-integration
9 | color: $subtext
10 | cursor: pointer
11 | border-radius: 0.8rem
12 | display: flex
13 | align-items: center
14 | justify-content: center
15 | gap: 0.5rem
16 | padding: 0.7rem 0.8rem
17 | user-select: none
18 |
19 | p
20 | margin: 0
21 |
22 | &:hover
23 | background-color: $light-gray
--------------------------------------------------------------------------------
/client/src/common/components/IntegrationDialog/components/IntegrationItem/components/IntegrationItemHeader/index.js:
--------------------------------------------------------------------------------
1 | export {IntegrationItemHeader as default} from "./IntegrationItemHeader";
--------------------------------------------------------------------------------
/client/src/common/components/IntegrationDialog/components/IntegrationItem/components/IntegrationItemHeader/styles.sass:
--------------------------------------------------------------------------------
1 | @import "@/common/styles/colors"
2 |
3 | .integration-item-header
4 | display: flex
5 | justify-content: space-between
6 |
7 | .integration-item-left
8 | display: flex
9 | align-items: center
10 | gap: 0.5rem
11 |
12 | svg
13 | width: 1.8rem
14 | height: 1.8rem
15 |
16 | .integration-title-container h3
17 | margin: 0
18 | font-size: 17px
19 | text-overflow: ellipsis
20 | overflow: hidden
21 | white-space: nowrap
22 | max-width: 11rem
23 |
24 | .integration-item-activity
25 | display: flex
26 | align-items: center
27 | gap: 0.3rem
28 | .integration-item-activity-circle
29 | width: 0.5rem
30 | height: 0.5rem
31 | border-radius: 5rem
32 | p
33 | margin: 0
34 | font-size: 11pt
35 | font-weight: 500
36 |
37 | .circle-error
38 | background-color: $red
39 |
40 | .circle-inactive
41 | background-color: $dark-gray
42 |
43 | .circle-active
44 | background-color: $green
45 |
46 | .integration-item-right
47 | display: flex
48 | align-items: center
49 | gap: 0.8rem
50 |
51 | .integration-item-right svg
52 | width: 1.5rem
53 | height: 3rem
54 | cursor: pointer
55 | font-weight: 1000
56 |
57 | .integration-green:hover
58 | color: $green
59 |
60 | .integration-red:hover
61 | color: $red
--------------------------------------------------------------------------------
/client/src/common/components/IntegrationDialog/components/IntegrationItem/index.js:
--------------------------------------------------------------------------------
1 | export {IntegrationItem as default} from "./IntegrationItem";
--------------------------------------------------------------------------------
/client/src/common/components/IntegrationDialog/components/IntegrationItem/styles.sass:
--------------------------------------------------------------------------------
1 | @import "@/common/styles/colors"
2 |
3 | .integration-item
4 | border: 2px solid $light-gray
5 | border-radius: 0.8rem
6 | padding: 0.5rem 1.5rem
7 | color: $subtext
8 | transition: border-color 0.3s
9 |
10 | .green-border
11 | border: 2px solid $green
12 |
13 | .error-border
14 | border: 2px solid $red
15 |
16 | .integration-body
17 | margin-left: 0.5rem
18 | margin-right: 0.5rem
19 |
20 | .integration-field
21 | display: flex
22 | align-items: center
23 | gap: 0.5rem
24 | margin-top: 0.5rem
25 | justify-content: space-between
26 |
27 | p
28 | margin: 0
29 |
30 | .error-item
31 | color: $red
32 |
33 | .integration-field-input
34 | background-color: $darker-gray
35 | border: 1px solid $light-gray
36 | border-radius: 0.5rem
37 | padding: 0.1rem 0.8rem
38 | color: $subtext
39 | font-size: 12pt
40 | height: 2rem
41 | width: 50%
42 |
43 | .item-error-border
44 | border: 1px solid $red
45 |
46 | .text-area
47 | font-family: "Inter", sans-serif
48 | height: 3.5rem
49 | resize: none
50 |
51 | input[type="checkbox"]
52 | appearance: none
53 | border: 1px solid $light-gray
54 | width: 2rem
55 | height: 2rem
56 | border-radius: 0.5rem
57 | outline: none
58 | transition: border-color 0.3s
59 | background-color: $darker-gray
60 | cursor: pointer
61 |
62 | &:checked
63 | content: "\2713"
64 | color: $green
65 | display: flex
66 | justify-content: center
67 | align-items: center
68 | font-weight: 700
69 | font-size: 18pt
70 |
71 | &::before
72 | color: $green
73 |
74 | &:checked::before
75 | content: "\2713"
--------------------------------------------------------------------------------
/client/src/common/components/IntegrationDialog/components/NoIntegrationsTab/NoIntegrationsTab.jsx:
--------------------------------------------------------------------------------
1 | import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
2 | import {Trans} from "react-i18next";
3 | import React from "react";
4 | import "./styles.sass";
5 |
6 | export const NoIntegrationsTab = ({onClick, integration}) => (
7 |
8 |
9 | }}>
11 | integrations.none_active
12 |
13 | )
--------------------------------------------------------------------------------
/client/src/common/components/IntegrationDialog/components/NoIntegrationsTab/index.js:
--------------------------------------------------------------------------------
1 | export {NoIntegrationsTab as default} from "./NoIntegrationsTab";
--------------------------------------------------------------------------------
/client/src/common/components/IntegrationDialog/components/NoIntegrationsTab/styles.sass:
--------------------------------------------------------------------------------
1 | @import "@/common/styles/colors"
2 |
3 | .no-integrations
4 | display: flex
5 | justify-content: center
6 | align-items: center
7 | flex-direction: column
8 | gap: 1rem
9 | height: 100%
10 | text-align: center
11 |
12 | svg
13 | width: 3rem
14 | height: 3rem
15 | color: $subtext
16 |
17 | .integration-add
18 | color: $green
19 | cursor: pointer
--------------------------------------------------------------------------------
/client/src/common/components/IntegrationDialog/index.js:
--------------------------------------------------------------------------------
1 | export * from './IntegrationDialog';
--------------------------------------------------------------------------------
/client/src/common/components/IntegrationDialog/styles.sass:
--------------------------------------------------------------------------------
1 | @import "@/common/styles/colors"
2 |
3 | .integration-dialog
4 | display: flex
5 | margin-left: 0.5rem
6 | margin-right: 0.5rem
7 | gap: 1rem
8 | width: 45rem
9 | margin-top: 1rem
10 | height: 20rem
11 |
12 | .integrations-tab
13 | display: flex
14 | flex-direction: column
15 | gap: 0.5rem
16 | overflow-y: scroll
17 | overflow-x: clip
18 | width: 70%
19 |
20 | .pr-integration-container
21 | display: flex
22 | gap: 0.5rem
23 | align-items: center
24 | justify-content: center
25 | padding: 0.5rem
26 | border-radius: 0.5rem
27 | border: 2px solid $red
28 | color: $red
29 | font-size: 1.25rem
30 |
31 | p
32 | margin: 0
33 | font-weight: 500
34 |
35 | @media (max-width: 781px)
36 | .integration-dialog
37 | width: 90vw
38 | margin-left: 0
39 | height: 100%
40 | margin-right: 0
41 | margin-top: 0.5rem
42 | flex-direction: column
43 |
44 | .integrations-tab
45 | max-height: 15rem
46 | width: 100%
--------------------------------------------------------------------------------
/client/src/common/components/LanguageDialog/index.js:
--------------------------------------------------------------------------------
1 | export {LanguageDialog as default} from "./LanguageDialog";
--------------------------------------------------------------------------------
/client/src/common/components/LanguageDialog/styles.sass:
--------------------------------------------------------------------------------
1 | @import "@/common/styles/colors"
2 |
3 | .language-chooser-dialog
4 | margin-top: 1rem
5 | margin-bottom: 1rem
6 | display: grid
7 | grid-template-columns: repeat(2, 1fr)
8 | grid-gap: 1rem
9 | height: 20rem
10 | overflow-y: scroll
11 |
12 | .language-chooser-item
13 | display: flex
14 | width: 10rem
15 | height: 2.5rem
16 | align-items: center
17 | justify-content: center
18 | padding: 0.5rem
19 | gap: 0.5rem
20 | cursor: pointer
21 | transition: background-color 0.3s
22 | border-radius: 0.5rem
23 | border: 1px solid $light-gray
24 |
25 | &:hover
26 | background-color: $darker-gray
27 |
28 | img
29 | width: 2rem
30 | margin-right: 0.5rem
31 | border-radius: 0.2rem
32 |
33 | p
34 | text-align: center
35 | font-size: 1.2rem
36 | color: $subtext
37 |
38 | .language-selected
39 | background-color: $light-gray
40 | color: $white
41 |
42 | &:hover
43 | background-color: $light-gray
44 |
45 | @media screen and (max-height: 425px)
46 | .language-chooser-dialog
47 | height: 15rem
48 |
49 | @media screen and (max-height: 375px)
50 | .language-chooser-dialog
51 | height: 5rem
52 |
53 | @media screen and (max-width: 425px)
54 | .language-chooser-dialog
55 | grid-template-columns: 1fr
56 |
57 | .language-chooser-item
58 | margin-left: 1.5rem
59 | margin-right: 1.5rem
--------------------------------------------------------------------------------
/client/src/common/components/LoadingDialog/LoadingDialog.jsx:
--------------------------------------------------------------------------------
1 | import {DialogProvider} from "@/common/contexts/Dialog";
2 | import "./styles.sass";
3 |
4 | export const LoadingDialog = (props) => {
5 |
6 | return (
7 | <>
8 | {props.isOpen &&
9 |
12 | }
13 | >
14 | )
15 | }
--------------------------------------------------------------------------------
/client/src/common/components/LoadingDialog/index.js:
--------------------------------------------------------------------------------
1 | export * from './LoadingDialog';
--------------------------------------------------------------------------------
/client/src/common/components/LoadingDialog/styles.sass:
--------------------------------------------------------------------------------
1 | @import "@/common/styles/colors"
2 |
3 | .dialog-loading
4 | display: flex
5 | justify-content: center
6 | align-content: center
7 | align-items: center
8 | width: 70px
9 | height: 70px
--------------------------------------------------------------------------------
/client/src/common/components/ProviderDialog/assets/img/cloudflare.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnmyt/MySpeed/5b41668ee5ecb3bde8898e32e5dacba0e523ef03/client/src/common/components/ProviderDialog/assets/img/cloudflare.webp
--------------------------------------------------------------------------------
/client/src/common/components/ProviderDialog/assets/img/libre.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnmyt/MySpeed/5b41668ee5ecb3bde8898e32e5dacba0e523ef03/client/src/common/components/ProviderDialog/assets/img/libre.webp
--------------------------------------------------------------------------------
/client/src/common/components/ProviderDialog/assets/img/ookla.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnmyt/MySpeed/5b41668ee5ecb3bde8898e32e5dacba0e523ef03/client/src/common/components/ProviderDialog/assets/img/ookla.webp
--------------------------------------------------------------------------------
/client/src/common/components/ProviderDialog/index.js:
--------------------------------------------------------------------------------
1 | export {ProviderDialog as default} from "./ProviderDialog";
--------------------------------------------------------------------------------
/client/src/common/components/ProviderDialog/styles.sass:
--------------------------------------------------------------------------------
1 | @import "@/common/styles/colors"
2 |
3 | .provider-dialog-content
4 | display: flex
5 | margin: 1rem 0.5rem
6 | user-select: none
7 | flex-direction: column
8 |
9 | .provider-header
10 | display: flex
11 | gap: 1rem
12 |
13 | .provider-item
14 | display: flex
15 | align-items: center
16 | padding: 0.3rem 0.5rem
17 | gap: 0.5rem
18 | border-radius: 0.8rem
19 | border: 2px solid $light-gray
20 | color: $subtext
21 | cursor: pointer
22 |
23 | img
24 | width: 2.5rem
25 | height: 2.5rem
26 |
27 | h3
28 | margin: 0
29 |
30 | &:hover
31 | background-color: $darker-gray
32 |
33 | .provider-item-active
34 | background-color: $light-gray
35 |
36 | &:hover
37 | background-color: $light-gray
38 |
39 | .provider-content
40 | display: flex
41 | flex-direction: column
42 | margin-top: 1rem
43 |
44 |
45 | .provider-setting
46 | display: flex
47 | gap: 1rem
48 | align-items: center
49 | justify-content: space-between
50 |
51 | .provider-input
52 | width: 20rem
53 | box-sizing: border-box
54 | margin-top: 0.5rem
55 | margin-bottom: 0.5rem
56 | font-size: 1.3rem
57 |
58 | h3
59 | color: $subtext
60 |
61 | .cloudflare-provider-info
62 | color: $subtext
63 | text-align: center
64 |
65 | .provider-dialog-footer
66 | display: flex
67 | align-items: center
68 | justify-content: space-between
69 |
70 | .provider-license-box
71 | display: flex
72 | align-items: center
73 | gap: 0.5rem
74 |
75 | input
76 | border: 2px solid $light-gray
77 |
78 | .cb-error
79 | border-color: $red
80 |
81 | label
82 | color: $subtext
83 | max-width: 16rem
84 | flex: 1
85 |
86 |
87 | @media screen and (max-width: 610px)
88 | .provider-dialog-content
89 | .provider-header
90 | flex-direction: column
91 |
92 | @media screen and (max-width: 520px)
93 | .provider-dialog-content
94 | .provider-setting .provider-input
95 | width: 60%
--------------------------------------------------------------------------------
/client/src/common/components/StorageDialog/index.js:
--------------------------------------------------------------------------------
1 | export {StorageDialog as default} from "./StorageDialog";
--------------------------------------------------------------------------------
/client/src/common/components/StorageDialog/styles.sass:
--------------------------------------------------------------------------------
1 | @import "@/common/styles/colors"
2 |
3 | .storage-dialog
4 | display: flex
5 | margin-left: 0.5rem
6 | margin-right: 0.5rem
7 | gap: 1rem
8 | width: 45rem
9 | margin-top: 1rem
10 | height: 14rem
11 | user-select: none
12 |
13 | .storage-options
14 | display: flex
15 | flex-direction: column
16 | justify-content: space-between
17 |
18 | .storage-top
19 | display: flex
20 | flex-direction: column
21 | gap: 0.5rem
22 | user-select: none
23 | overflow-x: hidden
24 | overflow-y: scroll
25 |
26 | .storage-tab
27 | display: flex
28 | gap: 0.5rem
29 | align-items: center
30 | padding: 0.6rem 0.7rem
31 | color: $subtext
32 | border: 2px solid transparent
33 | border-radius: 1rem
34 | cursor: pointer
35 |
36 | svg
37 | width: 1.3rem
38 | height: 1.3rem
39 | p
40 | margin: 0
41 | font-size: 14pt
42 |
43 |
44 | .reset-cursor
45 | cursor: default
46 |
47 | .storage-item-active
48 | background-color: $light-gray
49 | color: $white
50 |
51 | .storage-manager
52 | width: 70%
53 | display: flex
54 | flex-direction: column
55 | gap: 1rem
56 |
57 | .storage-manager .storage-row
58 | display: flex
59 | justify-content: space-between
60 | align-items: center
61 |
62 | h3
63 | margin: 0
64 | color: $subtext
65 |
66 | .dialog-btn
67 | padding: 0.4rem 1rem
68 |
69 |
70 | @media (max-width: 781px)
71 | .storage-dialog
72 | width: 90vw
73 | margin-left: 0
74 | height: 100%
75 | margin-top: 0.5rem
76 | margin-right: 0
77 | flex-direction: column
78 |
79 | .storage-bottom
80 | display: none
81 |
82 | .storage-manager
83 | max-height: 15rem
84 | width: 100%
85 |
86 | .storage-top
87 | flex-direction: row
88 | justify-content: center
89 | overflow-x: scroll
90 | overflow-y: hidden
91 | gap: 0.5rem
--------------------------------------------------------------------------------
/client/src/common/components/WelcomeDialog/banner.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnmyt/MySpeed/5b41668ee5ecb3bde8898e32e5dacba0e523ef03/client/src/common/components/WelcomeDialog/banner.webp
--------------------------------------------------------------------------------
/client/src/common/components/WelcomeDialog/index.js:
--------------------------------------------------------------------------------
1 | export {WelcomeDialog as default} from "./WelcomeDialog";
--------------------------------------------------------------------------------
/client/src/common/components/WelcomeDialog/steps/DataHelper/index.js:
--------------------------------------------------------------------------------
1 | export {DataHelper as default} from "./DataHelper";
--------------------------------------------------------------------------------
/client/src/common/components/WelcomeDialog/steps/DataHelper/styles.sass:
--------------------------------------------------------------------------------
1 | @import "@/common/styles/colors"
2 |
3 | .data-helper
4 |
5 | h2
6 | margin: 0 0 0.5rem
7 | color: $subtext
8 |
9 | p
10 | margin: 0
11 | color: $subtext
12 |
13 |
14 | .speeds
15 | display: flex
16 | justify-content: center
17 | gap: 2rem
18 | margin-top: 1rem
19 |
20 | .speed
21 | display: flex
22 | flex-direction: column
23 | align-items: center
24 |
25 | input
26 | box-sizing: border-box
27 | width: 100%
28 |
29 | .speed-header
30 | display: flex
31 | align-items: center
32 | margin-bottom: 0.5rem
33 |
34 | svg
35 | font-size: 28pt
36 | margin-right: 0.5rem
37 | color: $green
38 |
39 | .speed-text h2
40 | margin: 0
41 | color: $subtext
42 |
43 | .speed-text p
44 | margin: 0
45 | color: $subtext
46 |
47 |
48 | @media screen and (max-width: 600px)
49 | .data-helper .speeds
50 | flex-direction: column
51 | gap: 1rem
52 | margin-top: 0.5rem
53 |
54 | .speed
55 | width: 100%
56 | flex-direction: row
57 | justify-content: space-between
58 |
59 | input
60 | width: 50%
--------------------------------------------------------------------------------
/client/src/common/components/WelcomeDialog/steps/Greetings/Greetings.jsx:
--------------------------------------------------------------------------------
1 | import Banner from "@/common/components/WelcomeDialog/banner.webp";
2 | import "./styles.sass";
3 | import {t} from "i18next";
4 |
5 | export const Greetings = () => {
6 | return (
7 |
8 |

9 |
{t("welcome.title")}
10 |
{t("welcome.subtext")}
11 |
12 | );
13 | }
--------------------------------------------------------------------------------
/client/src/common/components/WelcomeDialog/steps/Greetings/index.js:
--------------------------------------------------------------------------------
1 | export {Greetings as default} from "./Greetings";
--------------------------------------------------------------------------------
/client/src/common/components/WelcomeDialog/steps/Greetings/styles.sass:
--------------------------------------------------------------------------------
1 | @import "@/common/styles/colors"
2 |
3 | .welcome-greetings
4 | display: flex
5 | margin: 1rem 0
6 | flex-direction: column
7 | align-items: center
8 | height: 100%
9 | justify-content: center
10 | text-align: center
11 | gap: 1rem
12 |
13 | img
14 | height: 5rem
15 |
16 | h2
17 | margin: 0
18 | font-size: 24pt
19 | color: $white
20 |
21 | p
22 | margin: 0
23 | padding-left: 2rem
24 | padding-right: 2rem
25 | font-size: 14pt
26 | color: $subtext
27 |
--------------------------------------------------------------------------------
/client/src/common/components/WelcomeDialog/steps/OoklaLicense/OoklaLicense.jsx:
--------------------------------------------------------------------------------
1 | import "./styles.sass";
2 | import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
3 | import {faFileLines} from "@fortawesome/free-solid-svg-icons";
4 | import {t} from "i18next";
5 |
6 | export const documents = [
7 | {url: "https://www.speedtest.net/about/terms", title: "Ookla ToS"},
8 | {url: "https://www.speedtest.net/about/eula", title: "Ookla EULA"},
9 | {url: "https://www.speedtest.net/about/privacy", title: "Ookla GDPR"}
10 | ]
11 |
12 | export const OoklaLicense = () => {
13 | return (
14 |
15 |
{t("welcome.accept_title")}
16 |
17 | {t("welcome.accept_subtext")}
18 |
19 |
28 |
29 | );
30 | }
--------------------------------------------------------------------------------
/client/src/common/components/WelcomeDialog/steps/OoklaLicense/index.js:
--------------------------------------------------------------------------------
1 | export {OoklaLicense as default} from "./OoklaLicense";
--------------------------------------------------------------------------------
/client/src/common/components/WelcomeDialog/steps/OoklaLicense/styles.sass:
--------------------------------------------------------------------------------
1 | @import "@/common/styles/colors"
2 |
3 | .ookla-license
4 | h2
5 | margin: 0 0 0.5rem
6 | color: $subtext
7 |
8 | p
9 | margin: 0
10 | color: $subtext
11 |
12 | .documents
13 | display: flex
14 | flex-direction: column
15 | margin-top: 1rem
16 |
17 | .document
18 | display: flex
19 | align-items: center
20 | margin: 0.3rem 0
21 | color: $green
22 | text-decoration: none
23 |
24 | svg
25 | margin-right: 0.5rem
26 | font-size: 1.5rem
27 |
28 | p
29 | color: $green
--------------------------------------------------------------------------------
/client/src/common/components/WelcomeDialog/steps/ProviderChooser/ProviderChooser.jsx:
--------------------------------------------------------------------------------
1 | import "./styles.sass";
2 |
3 | import {providers} from "@/common/components/ProviderDialog/ProviderDialog";
4 | import {t} from "i18next";
5 |
6 | export const ProviderChooser = ({provider, setProvider}) => {
7 | return (
8 |
9 |
{t("welcome.provider_title")}
10 |
{t("welcome.provider_subtext")}
11 |
12 | {providers.map((current) => (
13 |
setProvider(current.id)} key={current.id}>
15 |

16 |
{current.name}
17 |
18 | ))}
19 |
20 |
21 | );
22 | }
--------------------------------------------------------------------------------
/client/src/common/components/WelcomeDialog/steps/ProviderChooser/index.js:
--------------------------------------------------------------------------------
1 | export {ProviderChooser as default} from "./ProviderChooser";
--------------------------------------------------------------------------------
/client/src/common/components/WelcomeDialog/steps/ProviderChooser/styles.sass:
--------------------------------------------------------------------------------
1 | @import "@/common/styles/colors"
2 |
3 | .provider-chooser
4 |
5 | h2
6 | margin: 0 0 0.5rem
7 | color: $subtext
8 |
9 | p
10 | margin: 0
11 | color: $subtext
12 |
13 | .provider-list
14 | margin-top: 1rem
15 | display: flex
16 | gap: 1rem
17 | flex-wrap: wrap
18 |
19 | .provider-item
20 | display: flex
21 | align-items: center
22 | padding: 0.2rem 1rem
23 | gap: 0.5rem
24 | border-radius: 0.8rem
25 | border: 2px solid $light-gray
26 | color: $subtext
27 | cursor: pointer
28 |
29 | img
30 | width: 3rem
31 | height: 3rem
32 |
33 | h2
34 | margin: 0
35 |
36 | &:hover
37 | background-color: $darker-gray
38 |
39 | .provider-item-active
40 | background-color: $light-gray
41 |
42 | &:hover
43 | background-color: $light-gray
--------------------------------------------------------------------------------
/client/src/common/components/WelcomeDialog/styles.sass:
--------------------------------------------------------------------------------
1 | @import "@/common/styles/colors"
2 |
3 | .welcome-banner
4 | width: 30rem
5 | display: flex
6 | flex-direction: column
7 | justify-content: space-between
8 | user-select: none
9 |
10 | .welcome-inner
11 | height: 100%
12 |
13 | .welcome-actions
14 | display: flex
15 | justify-content: space-between
16 | align-items: center
17 | margin-top: 1rem
18 |
19 | h3
20 | margin: 0
21 | font-size: 14pt
22 | color: $subtext
23 |
24 | .dialog-btn
25 | padding: 0.4rem 1.3rem
26 | border-radius: 0.6rem
27 |
28 | .slide-in
29 | animation: slide-in 0.5s forwards
30 |
31 | @keyframes slide-in
32 | from
33 | opacity: 0
34 | transform: translateX(10%) rotate(10deg) scale(0.5)
35 | to
36 | opacity: 1
37 | transform: translateX(0)
38 |
39 |
40 | @media screen and (max-width: 600px)
41 | .welcome-banner
42 | width: 100%
43 |
44 | .slide-in
45 | animation: slide-in 0.5s forwards
--------------------------------------------------------------------------------
/client/src/common/contexts/Config/dialog.jsx:
--------------------------------------------------------------------------------
1 | import {t} from "i18next";
2 |
3 | export const passwordRequiredDialog = () => ({
4 | title: t("dialog.password.title"),
5 | placeholder: t("dialog.password.placeholder"),
6 | description: localStorage.getItem("password") ? {t("dialog.password.wrong")} : "",
7 | type: "password",
8 | buttonText: t("dialog.login"),
9 | disableCloseButton: true,
10 | onSuccess: (value) => {
11 | localStorage.setItem("password", value);
12 | window.location.reload();
13 | }
14 | });
15 |
16 | export const apiErrorDialog = () => ({
17 | title: t("dialog.api.title"),
18 | description: {t("dialog.api.description")},
19 | buttonText: t("dialog.retry"),
20 | disableCloseButton: true,
21 | onSuccess: () => window.location.reload()
22 | });
--------------------------------------------------------------------------------
/client/src/common/contexts/Config/index.js:
--------------------------------------------------------------------------------
1 | export * from './ConfigContext';
--------------------------------------------------------------------------------
/client/src/common/contexts/Dialog/DialogContext.jsx:
--------------------------------------------------------------------------------
1 | import React, {createContext, useEffect, useRef} from "react";
2 | import "./styles.sass";
3 |
4 | export const DialogContext = createContext({});
5 |
6 | export const DialogProvider = (props) => {
7 | const areaRef = useRef();
8 | const ref = useRef();
9 |
10 | const close = (force = false) => {
11 | if (props.disableClosing && !force) return;
12 | areaRef.current?.classList.add("dialog-area-hidden");
13 | ref.current?.classList.add("dialog-hidden");
14 | }
15 |
16 | const onClose = (e) => {
17 | if (e.animationName === "fadeOut") {
18 | hideTooltips(false);
19 | props?.close();
20 | }
21 | }
22 |
23 | const handleKeyDown = (e) => {
24 | if (e.code === "Enter" && props.submit) props.submit();
25 | }
26 |
27 | const hideTooltips = (state) => Array.from(document.getElementsByClassName("tooltip")).forEach(element => {
28 | if (state && !element.classList.contains("tooltip-invisible"))
29 | element.classList.add("tooltip-invisible");
30 | if (!state && element.classList.contains("tooltip-invisible"))
31 | element.classList.remove("tooltip-invisible");
32 | });
33 |
34 | useEffect(() => {
35 | const handleClick = (event) => {
36 | if (!ref.current?.contains(event.target)) close();
37 | }
38 |
39 | document.addEventListener("mousedown", handleClick);
40 |
41 | return () => document.removeEventListener("mousedown", handleClick);
42 | }, [ref]);
43 |
44 | return (
45 |
46 |
47 |
hideTooltips(true)}>
49 | {props.children}
50 |
51 |
52 |
53 | )
54 | }
--------------------------------------------------------------------------------
/client/src/common/contexts/Dialog/index.js:
--------------------------------------------------------------------------------
1 | export * from './DialogContext';
--------------------------------------------------------------------------------
/client/src/common/contexts/Dialog/styles.sass:
--------------------------------------------------------------------------------
1 | @import "@/common/styles/colors"
2 |
3 | .dialog-area
4 | position: fixed
5 | top: 0
6 | bottom: 0
7 | left: 0
8 | right: 0
9 | width: 100%
10 | height: 100%
11 | background-color: rgba(0, 0, 0, 0.6)
12 | display: flex
13 | align-items: center
14 | z-index: 10
15 | justify-content: center
16 | backdrop-filter: blur(2px)
17 | transition: all 0.2s
18 | animation: opacity 0.3s
19 |
20 | .dialog-area-hidden
21 | opacity: 0
22 | animation: opacity 0.3s reverse
23 |
24 | .dialog
25 | padding: 15px
26 | background-color: $dark-gray
27 | border: 1px solid $light-gray
28 | border-radius: 15px
29 | transition: all 0.2s
30 | animation: fadeIn 0.3s
31 |
32 | .dialog-hidden
33 | visibility: hidden
34 | opacity: 0
35 | animation: fadeOut 0.3s
36 |
37 | .dialog-header
38 | display: flex
39 | align-items: center
40 | justify-content: space-between
41 | user-select: none
42 |
43 | .dialog a
44 | color: $green
45 | cursor: pointer
46 |
47 | .dialog-main
48 | display: flex
49 | justify-content: center
50 | align-items: center
51 | flex-direction: column
52 |
53 | .dialog-buttons
54 | display: flex
55 | margin-top: 5px
56 | justify-content: right
57 |
58 | .dialog-text
59 | font-size: 16pt
60 | color: $subtext
61 | margin: 0
62 |
63 | .dialog-description
64 | font-size: 16pt
65 | margin: 10px 2px 2px
66 | color: $subtext
67 |
68 | .dialog-description a
69 | color: $green
70 | text-decoration: underline
71 |
72 | .dialog-value
73 | color: $green
74 |
75 | .dialog-icon
76 | cursor: pointer
77 |
78 | .dialog-icon:hover
79 | color: $red
80 |
81 | .dialog-btn
82 | font-size: 16pt
83 | padding: 8px 15px
84 | border: none
85 | font-weight: 700
86 | border-radius: 5px
87 | color: $dark-gray
88 | background-color: $green
89 | cursor: pointer
90 | margin-left: 5px
91 | margin-right: 5px
92 | transition: all 0.2s
93 |
94 | .dialog-btn:hover
95 | filter: brightness(0.8)
96 |
97 | .dialog-secondary
98 | background-color: $red
99 |
100 | .dialog-secondary:hover
101 | background-color: $red
--------------------------------------------------------------------------------
/client/src/common/contexts/InputDialog/index.js:
--------------------------------------------------------------------------------
1 | export * from './InputDialog';
--------------------------------------------------------------------------------
/client/src/common/contexts/InputDialog/styles.sass:
--------------------------------------------------------------------------------
1 | @import "@/common/styles/colors"
2 |
3 | .input-dialog
4 | width: 480px
5 |
6 | .dialog-input
7 | font-size: 18pt
8 | padding: 15px
9 | font-weight: 700
10 | margin-top: 15px
11 | margin-bottom: 15px
12 | background-color: $darker-gray
13 | color: $subtext
14 | border: 1px solid $light-gray
15 | border-radius: 15px
16 | text-align: center
17 | box-sizing: border-box
18 | outline: none
19 |
20 | .dialog-input:focus
21 | border: 1px solid $green
22 |
23 | .input-error
24 | border: 1px solid $red
--------------------------------------------------------------------------------
/client/src/common/contexts/Node/NodeContext.jsx:
--------------------------------------------------------------------------------
1 | import React, {useState, createContext, useEffect, useContext} from "react";
2 | import {baseRequest} from "@/common/utils/RequestUtil";
3 | import {ConfigContext} from "@/common/contexts/Config";
4 |
5 | export const NodeContext = createContext({});
6 |
7 | export const NodeProvider = (props) => {
8 |
9 | const [config] = useContext(ConfigContext);
10 | const [nodes, setNodes] = useState([]);
11 | const [currentNode, setCurrentNode] = useState(parseInt(localStorage.getItem("currentNode")) || 0);
12 |
13 | const updateNodes = async () => baseRequest("/nodes").then(async nodes => {
14 | if (nodes.ok) setNodes(await nodes.json());
15 | });
16 |
17 | useEffect(() => {
18 | if (Object.keys(config).length === 0) return;
19 | if (!config.viewMode) updateNodes();
20 | }, [config]);
21 |
22 | const updateCurrentNode = (node) => {
23 | localStorage.setItem("currentNode", node);
24 | setCurrentNode(parseInt(node));
25 | }
26 |
27 | const findNode = (nodeId) => nodes?.find(node => node.id === nodeId);
28 |
29 | return (
30 |
31 | {props.children}
32 |
33 | )
34 | }
--------------------------------------------------------------------------------
/client/src/common/contexts/Node/index.js:
--------------------------------------------------------------------------------
1 | export * from "./NodeContext";
--------------------------------------------------------------------------------
/client/src/common/contexts/Speedtests/SpeedtestContext.jsx:
--------------------------------------------------------------------------------
1 | import React, {useState, createContext, useEffect} from "react";
2 | import {jsonRequest} from "@/common/utils/RequestUtil";
3 |
4 | export const SpeedtestContext = createContext({});
5 |
6 | export const SpeedtestProvider = (props) => {
7 |
8 | const [speedtests, setSpeedtests] = useState({});
9 |
10 | const generatePath = (level = 1) => {
11 | switch (level) {
12 | case 1:
13 | return "?hours=24";
14 | case 2:
15 | return "?hours=48";
16 | case 3:
17 | return "/averages?days=7";
18 | case 4:
19 | return "/averages?days=30";
20 | }
21 | }
22 |
23 | const updateTests = () => jsonRequest("/speedtests" + generatePath(parseInt(localStorage.getItem("testTime") || 1)))
24 | .then(tests => setSpeedtests(tests));
25 |
26 |
27 | useEffect(() => {
28 | updateTests();
29 | const interval = setInterval(() => updateTests(), 15000);
30 | return () => clearInterval(interval);
31 | }, []);
32 |
33 | return (
34 |
35 | {props.children}
36 |
37 | )
38 | }
--------------------------------------------------------------------------------
/client/src/common/contexts/Speedtests/index.js:
--------------------------------------------------------------------------------
1 | export * from './SpeedtestContext';
--------------------------------------------------------------------------------
/client/src/common/contexts/Status/StatusContext.jsx:
--------------------------------------------------------------------------------
1 | import React, {useState, createContext, useEffect} from "react";
2 | import {jsonRequest} from "@/common/utils/RequestUtil";
3 |
4 | export const StatusContext = createContext({});
5 |
6 | export const StatusProvider = (props) => {
7 |
8 | const [status, setStatus] = useState({paused: false, running: false});
9 |
10 | const updateStatus = () => jsonRequest("/speedtests/status").then(status => setStatus(status));
11 |
12 | useEffect(() => {
13 | updateStatus();
14 | const interval = setInterval(() => updateStatus(), 5000);
15 | return () => clearInterval(interval);
16 | }, []);
17 |
18 | return (
19 |
20 | {props.children}
21 |
22 | )
23 | }
--------------------------------------------------------------------------------
/client/src/common/contexts/Status/index.js:
--------------------------------------------------------------------------------
1 | export * from './StatusContext';
--------------------------------------------------------------------------------
/client/src/common/contexts/ToastNotification/ToastNotificationContext.jsx:
--------------------------------------------------------------------------------
1 | import React, {createContext, useRef, useState} from "react";
2 | import {faExclamationTriangle} from "@fortawesome/free-solid-svg-icons";
3 | import "./styles.sass";
4 | import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
5 |
6 | export const ToastNotificationContext = createContext({});
7 |
8 | export const ToastNotificationProvider = (props) => {
9 |
10 | const notificationRef = useRef();
11 | const [timeOutId, setTimeOutId] = useState(null);
12 | const [toastNotification, setToastNotification] = useState(null);
13 |
14 | const updateToast = (text, color = "red", icon = faExclamationTriangle) => {
15 | setToastNotification({text, color, icon});
16 |
17 | if (timeOutId) {
18 | clearTimeout(timeOutId);
19 | setTimeOutId(null);
20 | }
21 | setTimeOutId(setTimeout(close, 5000));
22 | }
23 |
24 | const close = () => {
25 | notificationRef.current.classList.add("toast-hidden");
26 | }
27 |
28 | const onAnimationEnd = (event) => {
29 | if (event.animationName === "moveOut")
30 | setToastNotification(null);
31 | }
32 |
33 | return (
34 |
35 |
37 | {toastNotification &&
38 |
39 |
{toastNotification.text}
40 | }
41 |
42 |
43 | {props.children}
44 |
45 | );
46 | }
--------------------------------------------------------------------------------
/client/src/common/contexts/ToastNotification/index.js:
--------------------------------------------------------------------------------
1 | export * from "./ToastNotificationContext";
--------------------------------------------------------------------------------
/client/src/common/contexts/ToastNotification/styles.sass:
--------------------------------------------------------------------------------
1 | @import "@/common/styles/colors"
2 |
3 | .toast-notification
4 | position: fixed
5 | bottom: 2rem
6 | right: 1rem
7 | z-index: 5
8 | background-color: $darker-gray
9 | box-shadow: 0 0 1rem $darker-gray
10 | border: 2px solid $light-gray
11 | border-radius: 0.5rem
12 | animation: 0.5s moveIn
13 | cursor: pointer
14 |
15 | .toast-hidden
16 | visibility: hidden
17 | transition: all 0s 0.5s
18 | animation: 0.5s moveOut
19 |
20 | .toast-green
21 | transition: all 0.3s
22 |
23 | & .toast-content svg
24 | color: $green
25 |
26 | .toast-green:hover
27 | border-color: $green
28 | filter: brightness(0.9)
29 |
30 | .toast-red
31 | & .toast-content svg
32 | color: $red
33 |
34 | .toast-red:hover
35 | border-color: $red
36 | filter: brightness(0.9)
37 |
38 | .toast-content
39 | display: flex
40 | align-items: center
41 | padding: 1rem 1rem
42 | color: $white
43 | font-size: 14px
44 | font-weight: 500
45 |
46 | .toast-content svg
47 | margin-right: 1rem
48 | width: 2rem
49 | height: 2rem
50 |
51 | .toast-content h2
52 | margin: 0
53 | font-size: 1.4rem
54 |
55 |
56 | @keyframes moveIn
57 | 0%
58 | transform: translateX(100%)
59 | 60%
60 | transform: translateX(-10%)
61 | 100%
62 | transform: translateX(0)
63 |
64 | @keyframes moveOut
65 | 0%
66 | transform: translateX(0)
67 | 60%
68 | transform: translateX(-10%)
69 | 100%
70 | transform: translateX(100%)
71 |
72 |
73 | @media screen and (max-width: 425px)
74 | .toast-notification
75 | bottom: 1rem
76 | right: 1rem
77 | left: 1rem
78 |
79 | @keyframes moveIn
80 | 0%
81 | transform: translateY(100%)
82 | 60%
83 | transform: translateY(-10%)
84 | 100%
85 | transform: translateY(0)
86 |
87 | @keyframes moveOut
88 | 0%
89 | transform: translateY(0)
90 | 60%
91 | transform: translateY(-10%)
92 | 100%
93 | transform: translateY(100%)
--------------------------------------------------------------------------------
/client/src/common/styles/_colors.sass:
--------------------------------------------------------------------------------
1 | $background: #232835
2 |
3 | $light-gray: #353A47
4 | $dark-gray: #1d2128
5 | $darker-gray: #20252F
6 |
7 | $white: #F1F1F1
8 | $subtext: #C8C8C8
9 |
10 | $blue: #456AC6
11 | $orange: #E58A00
12 | $green: #45C65A
13 | $red: #C64545
--------------------------------------------------------------------------------
/client/src/common/styles/default.sass:
--------------------------------------------------------------------------------
1 | @import "colors"
2 |
3 | body, html
4 | margin: 0
5 | overflow-x: hidden
6 | background-color: $background
7 | font-family: "Inter", sans-serif
8 | font-weight: 700
9 |
10 | ::-webkit-scrollbar
11 | width: 13px
12 |
13 | ::-webkit-scrollbar-thumb
14 | background: $darker-gray
15 | border-radius: 10px
16 |
17 | ::-webkit-scrollbar-thumb:hover
18 | background: $dark-gray
19 |
20 | .speedtest-icon
21 | font-size: 26pt
22 | color: $white
23 | margin-right: 10px
24 |
25 | .container-icon
26 | width: 35px
27 | height: 35px
28 | color: $white
29 | font-size: 26pt
30 |
31 | .help-icon
32 | cursor: help
33 |
34 | .icon-red
35 | color: $red
36 |
37 | .icon-error
38 | color: $red
39 | filter: brightness(0.8)
40 |
41 | .icon-green
42 | color: $green
43 |
44 | .icon-orange
45 | color: $orange
46 |
47 | .icon-blue
48 | color: $blue
49 |
50 | main
51 | display: flex
52 | flex-direction: column
53 | align-items: center
54 | margin-left: 1rem
55 | margin-right: 1rem
56 |
57 | @keyframes opacity
58 | 0%
59 | opacity: 0
60 | 100%
61 | opacity: 1
62 |
63 | @keyframes fadeIn
64 | 0%
65 | opacity: 0
66 | transform: scale(0.4)
67 | filter: blur(5px)
68 | 100%
69 | opacity: 1
70 |
71 | @keyframes fadeOut
72 | 0%
73 | opacity: 1
74 | 100%
75 | opacity: 0
76 | transform: scale(0.4)
77 | filter: blur(5px)
78 |
79 | @media (max-width: 730px)
80 | ::-webkit-scrollbar
81 | width: 5px
--------------------------------------------------------------------------------
/client/src/common/styles/spinner.sass:
--------------------------------------------------------------------------------
1 | @import "colors"
2 |
3 | .lds-ellipsis
4 | display: inline-block
5 | position: relative
6 | width: 80px
7 | height: 80px
8 |
9 | .lds-ellipsis div
10 | position: absolute
11 | top: 33px
12 | width: 13px
13 | height: 13px
14 | border-radius: 50%
15 | background: $white
16 | animation-timing-function: cubic-bezier(0, 1, 1, 0)
17 |
18 | .lds-ellipsis div:nth-child(1)
19 | left: 8px
20 | animation: lds-ellipsis1 0.6s infinite
21 |
22 | .lds-ellipsis div:nth-child(2)
23 | left: 8px
24 | animation: lds-ellipsis2 0.6s infinite
25 |
26 | .lds-ellipsis div:nth-child(3)
27 | left: 32px
28 | animation: lds-ellipsis2 0.6s infinite
29 |
30 | .lds-ellipsis div:nth-child(4)
31 | left: 56px
32 | animation: lds-ellipsis3 0.6s infinite
33 |
34 | @keyframes lds-ellipsis1
35 | 0%
36 | transform: scale(0)
37 | 100%
38 | transform: scale(1)
39 |
40 | @keyframes lds-ellipsis3
41 | 0%
42 | transform: scale(1)
43 | 100%
44 | transform: scale(0)
45 |
46 |
47 | @keyframes lds-ellipsis2
48 | 0%
49 | transform: translate(0, 0)
50 | 100%
51 | transform: translate(24px, 0)
--------------------------------------------------------------------------------
/client/src/common/utils/TestUtil.js:
--------------------------------------------------------------------------------
1 | export function getIconBySpeed(current, optional, higherIsBetter) {
2 | let speed = Math.floor((current / optional) * 100);
3 |
4 | if (current === -1) return "error";
5 |
6 | if (higherIsBetter) {
7 | if (speed >= 75) return "green";
8 | if (speed >= 30) return "orange";
9 | return "red";
10 | } else {
11 | if (speed >= 180) return "red";
12 | if (speed >= 130) return "orange";
13 | return "green";
14 | }
15 | }
--------------------------------------------------------------------------------
/client/src/i18n.js:
--------------------------------------------------------------------------------
1 | import i18n from "i18next";
2 | import {initReactI18next} from "react-i18next";
3 | import LanguageDetector from 'i18next-browser-languagedetector';
4 | import HttpApi from 'i18next-http-backend';
5 | import EnglishFlag from "@/common/assets/languages/en.webp";
6 | import GermanFlag from "@/common/assets/languages/de.webp";
7 | import BulgarianFlag from "@/common/assets/languages/bg.webp";
8 | import ChineseFlag from "@/common/assets/languages/zh.webp";
9 | import DutchFlag from "@/common/assets/languages/nl.webp";
10 | import FranceFlag from "@/common/assets/languages/fr.webp";
11 | import ItalianFlag from "@/common/assets/languages/it.webp";
12 | import PortugueseBrazilFlag from "@/common/assets/languages/br.webp";
13 | import RussianFlag from "@/common/assets/languages/ru.webp";
14 | import SpanishFlag from "@/common/assets/languages/es.webp";
15 | import TurkishFlag from "@/common/assets/languages/tr.webp";
16 |
17 | if (localStorage.getItem('language') === null)
18 | localStorage.setItem('language', navigator.language.split('-')[0]);
19 |
20 | export const languages = [
21 | {name: 'English', code: 'en', flag: EnglishFlag},
22 | {name: 'Deutsch', code: 'de', flag: GermanFlag},
23 | {name: 'Български', code: 'bg', flag: BulgarianFlag},
24 | {name: '中文', code: 'zh', flag: ChineseFlag},
25 | {name: 'Nederlands', code: 'nl', flag: DutchFlag},
26 | {name: 'Français', code: 'fr', flag: FranceFlag},
27 | {name: 'Italiano', code: 'it', flag: ItalianFlag},
28 | {name: 'Português do Brasil', code: 'pt', flag: PortugueseBrazilFlag},
29 | {name: 'Русский', code: 'ru', flag: RussianFlag},
30 | {name: 'Español', code: 'es', flag: SpanishFlag},
31 | {name: 'Türkçe', code: 'tr', flag: TurkishFlag}
32 | ]
33 |
34 | i18n.use(initReactI18next).use(LanguageDetector).use(HttpApi).init({
35 | supportedLngs: languages.map(lang => lang.code),
36 | fallbackLng: 'en',
37 | backend: {
38 | loadPath: '/assets/locales/{{lng}}.json'
39 | },
40 | detection: {
41 | order: ['localStorage'],
42 | lookupLocalStorage: 'language'
43 | }
44 | });
45 |
46 | export default i18n;
--------------------------------------------------------------------------------
/client/src/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from "react-dom/client";
3 | import App from './App';
4 |
5 | export const PROJECT_URL = "https://github.com/gnmyt/myspeed";
6 | export const WEB_URL = "https://myspeed.dev";
7 | export const PROJECT_WIKI = "https://docs.myspeed.dev";
8 | export const DONATION_URL = "https://ko-fi.com/gnmyt";
9 |
10 | const root = ReactDOM.createRoot(document.getElementById('root'));
11 |
12 | root.render(
13 |
14 |
15 |
16 | );
17 |
--------------------------------------------------------------------------------
/client/src/pages/Error/Error.jsx:
--------------------------------------------------------------------------------
1 | import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
2 | import {faExclamationTriangle} from "@fortawesome/free-solid-svg-icons";
3 | import {useEffect, useState} from "react";
4 | import "./styles.sass";
5 |
6 | export const Error = (props) => {
7 | const [reloadTimer, setReloadTimer] = useState(5);
8 |
9 | useEffect(() => {
10 | if (props.disableReload) return;
11 | const interval = setInterval(() => {
12 | if (reloadTimer > 0) {
13 | setReloadTimer(reloadTimer - 1)
14 | } else {
15 | window.location = window.location.href;
16 | }
17 | }, 1000);
18 |
19 | return () => clearInterval(interval);
20 | }, [reloadTimer]);
21 |
22 | return (
23 |
24 |
25 |
{props.text}
26 | {!props.disableReload && Reloading {reloadTimer !== 0 ? <>in {reloadTimer} seconds> :
27 | now}...
}
28 |
29 | )
30 | }
--------------------------------------------------------------------------------
/client/src/pages/Error/index.js:
--------------------------------------------------------------------------------
1 | export {Error as default} from "./Error";
--------------------------------------------------------------------------------
/client/src/pages/Error/styles.sass:
--------------------------------------------------------------------------------
1 | @import "@/common/styles/colors"
2 |
3 | .error-page
4 | min-height: 100vh
5 | display: flex
6 | flex-direction: column
7 | justify-content: center
8 | align-items: center
9 | color: $white
10 | text-align: center
11 | margin: 0
12 |
13 | .no-reload
14 | width: unset
15 | height: unset
16 | position: center
17 | display: unset
18 |
19 | .error-page svg
20 | color: $red
21 |
22 | .error-page h1
23 | padding-left: 1rem
24 | padding-right: 1rem
25 |
26 | .error-page h2
27 | margin: 0
28 | padding-left: 0.5rem
29 | padding-right: 0.5rem
30 |
31 | .error-page span
32 | color: $green
--------------------------------------------------------------------------------
/client/src/pages/Home/Home.jsx:
--------------------------------------------------------------------------------
1 | import LatestTestComponent from "./components/LatestTest";
2 | import TestAreaComponent from "./components/TestArea";
3 |
4 | const Home = () => (
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | )
13 |
14 |
15 | export default Home;
--------------------------------------------------------------------------------
/client/src/pages/Home/components/LatestTest/index.js:
--------------------------------------------------------------------------------
1 | export {default} from './LatestTestComponent';
--------------------------------------------------------------------------------
/client/src/pages/Home/components/LatestTest/styles.sass:
--------------------------------------------------------------------------------
1 | @import "@/common/styles/colors"
2 |
3 | .analyse-area
4 | margin-top: 2rem
5 | display: flex
6 | padding: 2.5rem 2rem
7 | border: transparent 1px solid
8 | background-color: $dark-gray
9 | border-radius: 15px
10 | width: 100%
11 | transition: all 0.5s
12 | align-items: center
13 | justify-content: center
14 | margin-bottom: 2rem
15 | box-sizing: border-box
16 | user-select: none
17 |
18 | .pulse
19 | border: $green 2px solid
20 | animation: pulse 2s infinite
21 |
22 | @keyframes pulse
23 | 0%
24 | box-shadow: 0 0 0 0 #14AB37FF
25 | 70%
26 | box-shadow: 0 0 0 10px #CCA92C00
27 | 100%
28 | box-shadow: 0 0 0 0 #CCA92C00
29 |
30 | .inner-container
31 | margin-left: 2rem
32 | margin-right: 2rem
33 | transition: all 0.5s
34 | animation: fadeIn 0.5s
35 |
36 | .container-header
37 | display: flex
38 | align-items: center
39 |
40 | .tests-paused
41 | border: $orange 2px solid
42 |
43 | .container-text
44 | margin: 0 0 0 25px
45 | color: $white
46 | font-weight: 700
47 | font-size: 24pt
48 | white-space: nowrap
49 |
50 | .container-subtext
51 | color: $subtext
52 | font-size: 16pt
53 | margin-left: 10px
54 | font-weight: 500
55 |
56 | .container-main
57 | text-align: center
58 | color: $subtext
59 |
60 | h2
61 | font-size: 28pt
62 | font-weight: 700
63 | margin: 1rem
64 |
65 | @media (max-width: 1351px)
66 | .inner-container
67 | margin-left: 1rem
68 | margin-right: 1rem
69 |
70 | @media (max-width: 1200px)
71 | .analyse-area
72 | flex-wrap: wrap
73 | .mobile-break
74 | width: 100%
75 | .analyse-area
76 | justify-content: space-evenly
77 |
78 | @media (max-width: 730px)
79 | .analyse-area
80 | flex-direction: column
81 |
82 | @media (max-width: 475px)
83 | .analyse-area
84 | padding-left: 1rem
85 | padding-bottom: 1rem
86 | padding-top: 1rem
87 | .inner-container
88 | margin: 0
--------------------------------------------------------------------------------
/client/src/pages/Home/components/LatestTest/utils.js:
--------------------------------------------------------------------------------
1 | import {t} from "i18next";
2 |
3 | export function generateRelativeTime(created) {
4 | let currentDate = new Date().getTime();
5 | let date = new Date(Date.parse(created)).getTime();
6 |
7 | const diff = (currentDate - date) / 1000;
8 |
9 | if (diff < 5) {
10 | return t("time.now");
11 | } else if (diff < 60) {
12 | return t("time.seconds", {replace: {seconds: Math.floor(diff)}});
13 | } else if (diff < 3600) {
14 | return Math.floor(diff / 60) === 1 ? t("time.minute") : t("time.minutes", {replace: {minutes: Math.floor(diff / 60)}});
15 | } else if (diff < 86400) {
16 | return Math.floor(diff / 3600) === 1 ? t("time.hour") : t("time.hours", {replace: {hours: Math.floor(diff / 3600)}});
17 | }
18 |
19 | return "N/A"
20 | }
--------------------------------------------------------------------------------
/client/src/pages/Home/components/LatestTest/utils/dialogs.jsx:
--------------------------------------------------------------------------------
1 | import {t} from "i18next";
2 | import {Trans} from "react-i18next";
3 |
4 | export const downloadInfo = () => ({title: t("info.down.title"), description: t("info.down.description"), buttonText: t("dialog.okay")});
5 |
6 | export const pingInfo = () => ({title: t("info.ping.title"), description: t("info.ping.description"), buttonText: t("dialog.okay")});
7 |
8 | export const uploadInfo = () => ({title: t("info.up.title"), description: t("info.up.description"), buttonText: t("dialog.okay")});
9 |
10 | export const latestTestInfo = (latest) => ({
11 | title: t("info.latest.title"),
12 | description: latest.created ? }} values={{date: new Date(latest.created).toLocaleDateString(),
13 | time: new Date(latest.created).toLocaleTimeString(undefined, {hour: "2-digit", minute: "2-digit"})}}>
14 | info.latest.description : t("test.no_latest"),
15 | buttonText: t("dialog.okay")
16 | });
--------------------------------------------------------------------------------
/client/src/pages/Home/components/Speedtest/index.js:
--------------------------------------------------------------------------------
1 | export {default} from './SpeedtestComponent';
--------------------------------------------------------------------------------
/client/src/pages/Home/components/Speedtest/styles.sass:
--------------------------------------------------------------------------------
1 | @import "@/common/styles/colors"
2 |
3 | .speedtest
4 | margin-bottom: 2rem
5 | display: flex
6 | padding: 1.5rem 2rem
7 | border: $light-gray 2px solid
8 | background-color: $dark-gray
9 | border-radius: 15px
10 | justify-content: space-between
11 | width: 100%
12 | flex-wrap: wrap
13 | transition: all 0.5s
14 | animation: fadeIn 0.5s
15 | cursor: pointer
16 | user-select: none
17 | box-sizing: border-box
18 |
19 | .speedtest:hover
20 | border: $green 2px solid
21 |
22 | .speedtest-hidden
23 | visibility: hidden
24 | opacity: 0
25 | animation: fadeOut 0.3s
26 |
27 | .speedtest-row
28 | display: flex
29 | align-items: center
30 |
31 | .date
32 | display: flex
33 | align-items: center
34 |
35 | .date-text
36 | margin: 0 0 0 1rem
37 | font-size: 24pt
38 | font-weight: 700
39 | color: $subtext
40 |
41 | .speedtest-text
42 | margin: 0
43 | font-size: 28pt
44 | font-weight: 700
45 | color: $subtext
46 |
47 | .tooltip-element
48 | position: relative
49 |
50 | .tooltip-element .tooltip
51 | user-select: none
52 | pointer-events: none
53 | font-size: 16pt
54 | opacity: 0
55 | bottom: 120%
56 | left: 50%
57 | margin-left: -60px
58 | background-color: $dark-gray
59 | color: $subtext
60 | text-align: center
61 | border-radius: 6px
62 | padding: 5px 10px
63 | height: fit-content
64 |
65 | z-index: 90
66 | position: absolute
67 | transition: opacity 0.2s
68 |
69 | .tooltip-bottom .tooltip
70 | bottom: 0
71 | top: 120%
72 |
73 | .tooltip-element .tooltip-invisible
74 | visibility: hidden
75 |
76 | .tooltip-element:hover .tooltip
77 | opacity: 1
78 |
79 | @media (max-width: 900px)
80 | .tooltip-element .tooltip
81 | font-size: 14pt
82 |
83 | @media (max-width: 730px)
84 | .tooltip-element .tooltip
85 | font-size: 11pt
86 |
87 | @media (max-width: 605px)
88 | .speedtest
89 | flex-direction: column
90 | .date-text
91 | font-size: 32pt
92 | .speedtest-text
93 | font-size: 32pt
94 |
95 | @media (max-width: 475px)
96 | .tooltip-element .tooltip
97 | font-size: 12pt
98 | padding: 5px 4px
--------------------------------------------------------------------------------
/client/src/pages/Home/components/Speedtest/utils/errors.js:
--------------------------------------------------------------------------------
1 | import {t} from "i18next";
2 |
3 | export const errors = () => ({
4 | "Network unreachable": t("errors.network_unreachable"),
5 | "Timeout occurred in connect": t("errors.took_too_long"),
6 | "permission denied": t("errors.no_permission"),
7 | "Resource temporarily unavailable": t("errors.resource_unavailable"),
8 | "No route to host": t("errors.no_route"),
9 | "Connection refused": t("errors.connection_refused"),
10 | "timed out": t("errors.timed_out"),
11 | "Could not retrieve or read configuration": t("errors.config"),
12 | });
--------------------------------------------------------------------------------
/client/src/pages/Home/components/Speedtest/utils/infos.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {Trans} from "react-i18next";
3 | import {t} from "i18next";
4 |
5 | const RESULT_URL = "https://www.speedtest.net/result/c/";
6 |
7 | export const averageResultDialog = (timeString, props) => }}
8 | values={{amount: props.amount, date: timeString, down: props.down,
9 | up: props.up, duration: props.duration}}>test.average.description
10 |
11 | export const resultDialog = (props) => ,
12 | Link: (props.resultId ? : ) }}
13 | values={{down: props.down, up: props.up,
14 | type: t("test.result." + (props.type === "custom" ? "from_you" : "automatic")),
15 | duration: props.duration}}>test.result.description
--------------------------------------------------------------------------------
/client/src/pages/Home/components/TestArea/index.js:
--------------------------------------------------------------------------------
1 | export {default} from './TestAreaComponent';
--------------------------------------------------------------------------------
/client/src/pages/Home/components/TestArea/styles.sass:
--------------------------------------------------------------------------------
1 | @import "@/common/styles/colors"
2 |
3 | .error-text
4 | margin: 0
5 | font-size: 26pt
6 | font-weight: 700
7 | color: $subtext
8 |
9 | @media (max-width: 605px)
10 | .error-text
11 | text-align: center
12 | width: 28rem
13 |
14 | @media (max-width: 475px)
15 | .error-text
16 | width: 20rem
--------------------------------------------------------------------------------
/client/src/pages/Home/index.js:
--------------------------------------------------------------------------------
1 | export {default} from './Home';
--------------------------------------------------------------------------------
/client/src/pages/Loading/Loading.jsx:
--------------------------------------------------------------------------------
1 | import "./styles.sass";
2 |
3 | export const Loading = () => (
4 |
12 | );
--------------------------------------------------------------------------------
/client/src/pages/Loading/index.js:
--------------------------------------------------------------------------------
1 | export {Loading as default} from "./Loading";
--------------------------------------------------------------------------------
/client/src/pages/Loading/styles.sass:
--------------------------------------------------------------------------------
1 | .loading
2 | width: 100vw
3 | height: 100vh
4 | position: absolute
5 | display: flex
6 | justify-content: center
7 | align-items: center
--------------------------------------------------------------------------------
/client/src/pages/Nodes/Nodes.jsx:
--------------------------------------------------------------------------------
1 | import "./styles.sass";
2 | import NodeHeader from "@/pages/Nodes/components/NodeHeader";
3 | import NodeContainer from "@/pages/Nodes/components/NodeContainer";
4 | import {useContext, useEffect, useState} from "react";
5 | import {NodeContext} from "@/common/contexts/Node";
6 | import {t} from "i18next";
7 | import CreateNodeDialog from "@/pages/Nodes/components/CreateNodeDialog";
8 | import {ConfigContext} from "@/common/contexts/Config";
9 | import {InputDialogContext} from "@/common/contexts/InputDialog";
10 | import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
11 | import {faPlus} from "@fortawesome/free-solid-svg-icons";
12 |
13 | export const Nodes = () => {
14 | const [config] = useContext(ConfigContext);
15 | const [nodes, updateNodes] = useContext(NodeContext);
16 | const [setDialog] = useContext(InputDialogContext);
17 | const [createDialogOpen, setCreateDialogOpen] = useState(false);
18 |
19 | const openPreviewInfoDialog = () => {
20 | setDialog({title: t("preview.title"), description: t("nodes.preview_active"), buttonText: t("dialog.close")});
21 | }
22 |
23 | useEffect(() => {
24 | updateNodes();
25 | }, []);
26 |
27 | return (
28 |
29 | {createDialogOpen &&
setCreateDialogOpen(false)}/>}
30 |
31 |
32 |
33 |
34 | {nodes.map(node => )}
35 |
36 | config.previewMode
37 | ? openPreviewInfoDialog() : setCreateDialogOpen(true)}>
38 |
39 | {t("nodes.add")}
40 |
41 |
42 |
43 | )
44 | }
--------------------------------------------------------------------------------
/client/src/pages/Nodes/components/CreateNodeDialog/index.js:
--------------------------------------------------------------------------------
1 | export {CreateNodeDialog as default} from './CreateNodeDialog';
--------------------------------------------------------------------------------
/client/src/pages/Nodes/components/CreateNodeDialog/styles.sass:
--------------------------------------------------------------------------------
1 | @import "@/common/styles/colors"
2 |
3 | .server-input
4 | font-size: 18pt
5 | padding: 15px
6 | font-weight: 700
7 | margin-top: 15px
8 | background-color: $darker-gray
9 | border: 1px solid $light-gray
10 | color: $subtext
11 | border-radius: 15px
12 |
13 | .server-dialog
14 | transition: all 0.3s ease-in-out
15 | padding-left: 1rem
16 | padding-right: 1rem
17 | padding-top: 1rem
18 | display: flex
19 | flex-direction: column
20 |
21 | .server-group
22 | display: flex
23 | flex-direction: column
24 | align-items: flex-start
25 | margin: 0 0 15px 0
26 |
27 | .server-group h2
28 | margin: 0
29 | color: $green
30 | font-weight: 700
31 | display: flex
32 | align-items: center
33 | gap: 0.5rem
34 |
35 | .server-error
36 | .server-input
37 | border: 1px solid $red
38 | color: $red
39 |
40 | @media screen and (max-width: 425px)
41 | .server-dialog
42 | padding-left: 0.5rem
43 | padding-right: 0.5rem
44 | .server-group h2
45 | font-size: 16pt
46 | .server-input
47 | font-size: 16pt
--------------------------------------------------------------------------------
/client/src/pages/Nodes/components/NodeContainer/index.js:
--------------------------------------------------------------------------------
1 | export {NodeContainer as default} from "./NodeContainer";
--------------------------------------------------------------------------------
/client/src/pages/Nodes/components/NodeContainer/styles.sass:
--------------------------------------------------------------------------------
1 | @import "@/common/styles/colors"
2 |
3 | .node-item
4 | border: 2px solid $light-gray
5 | background-color: $dark-gray
6 | border-radius: 15px
7 | display: flex
8 | width: 50rem
9 | cursor: pointer
10 | padding: 1rem 1.5rem
11 | justify-content: space-between
12 | align-items: center
13 | user-select: none
14 |
15 | .hover-green:hover
16 | border: 2px solid $green
17 |
18 | .hover-orange:hover
19 | border: 2px solid $orange
20 |
21 | .hover-red:hover
22 | border: 2px solid $red
23 |
24 | .node-info-area
25 | display: flex
26 | align-items: center
27 | gap: 1rem
28 |
29 | .node-info-area svg
30 | font-size: 30pt
31 |
32 | .node-info-area h1
33 | margin: 0
34 | font-size: 16pt
35 |
36 | .node-info-area p
37 | margin: 0
38 | font-size: 11pt
39 | color: $subtext
40 |
41 | .node-info
42 | display: flex
43 | flex-direction: column
44 | align-items: flex-start
45 |
46 | .node-info *
47 | max-width: 11rem
48 | text-overflow: ellipsis
49 | overflow: hidden
50 | white-space: nowrap
51 |
52 | .speed-area
53 | display: flex
54 | gap: 1rem
55 |
56 | .icon-text
57 | display: flex
58 | gap: 0.5rem
59 | align-items: center
60 |
61 | .icon-text h2
62 | margin: 0
63 | color: $subtext
64 |
65 | .speed-item
66 | display: flex
67 | gap: 0.5rem
68 |
69 | .speed-item svg
70 | font-size: 24pt
71 |
72 | .speed-item h1
73 | margin: 0
74 | font-size: 20pt
75 | color: $subtext
76 |
77 | .speed-icon
78 | font-size: 28pt
79 |
80 | @media screen and (max-width: 862px)
81 | .node-item
82 | width: calc(20vw + 8rem)
83 | flex-direction: column
84 | gap: 1rem
85 |
86 | .speed-area
87 | flex-direction: column
88 | gap: 0.5rem
89 | .icon-text h2
90 | font-size: 14pt
--------------------------------------------------------------------------------
/client/src/pages/Nodes/components/NodeHeader/NodeHeader.jsx:
--------------------------------------------------------------------------------
1 | import "./styles.sass";
2 |
3 | export const NodeHeader = () => {
4 | return (
5 |
6 |

7 |
MySpeed
8 |
9 | )
10 | }
--------------------------------------------------------------------------------
/client/src/pages/Nodes/components/NodeHeader/index.js:
--------------------------------------------------------------------------------
1 | export {NodeHeader as default} from "./NodeHeader";
--------------------------------------------------------------------------------
/client/src/pages/Nodes/components/NodeHeader/styles.sass:
--------------------------------------------------------------------------------
1 | .node-header
2 | display: flex
3 | align-items: center
4 | gap: 1rem
5 |
6 | .node-header img
7 | width: 5.3rem
8 | height: 5.3rem
9 | border-radius: 50%
10 |
11 | .node-header h1
12 | font-size: 38pt
13 | font-weight: 700
14 | margin: 0
--------------------------------------------------------------------------------
/client/src/pages/Nodes/index.js:
--------------------------------------------------------------------------------
1 | export {Nodes as default} from './Nodes';
--------------------------------------------------------------------------------
/client/src/pages/Nodes/styles.sass:
--------------------------------------------------------------------------------
1 | @import "@/common/styles/colors"
2 |
3 | .node-page
4 | min-height: 100vh
5 | display: flex
6 | flex-direction: column
7 | justify-content: center
8 | align-items: center
9 | color: $white
10 | text-align: center
11 | margin: 0
12 | gap: 0.7rem
13 |
14 | .node-page hr
15 | margin: 0
16 |
17 | .node-area
18 | display: flex
19 | flex-direction: column
20 | gap: 1.5rem
21 | margin-top: 1rem
22 |
23 | .node-add
24 | width: 53rem
25 | display: flex
26 | justify-content: center
27 | align-items: center
28 | border: 2px solid $light-gray
29 | background-color: $dark-gray
30 | border-radius: 15px
31 | cursor: pointer
32 | user-select: none
33 | gap: 0.5rem
34 |
35 | .node-add h1
36 | font-size: 22pt
37 | font-weight: 700
38 | color: $subtext
39 | margin-right: 1rem
40 |
41 | .node-add svg
42 | font-size: 22pt
43 | margin-left: 1rem
44 |
45 | .node-add:hover
46 | border: 2px solid $green
47 |
48 | h1
49 | color: $white
50 |
51 | .node-disabled
52 | width: 53rem
53 | border: 2px dashed $light-gray
54 | border-radius: 15px
55 | cursor: not-allowed
56 | user-select: none
57 |
58 | .node-disabled:hover
59 | border: 2px dashed $light-gray
60 |
61 | h1
62 | color: $subtext
63 |
64 | @media screen and (max-width: 862px)
65 | .node-add
66 | width: calc(20vw + 11rem)
67 | border-radius: 15px
68 | cursor: pointer
69 |
70 | .node-add h1
71 | font-size: 22pt
--------------------------------------------------------------------------------
/client/src/pages/Statistics/charts/AverageChart/AverageChart.jsx:
--------------------------------------------------------------------------------
1 | import StatisticContainer from "@/pages/Statistics/components/StatisticContainer";
2 | import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
3 | import {faGauge, faMinusCircle, faPlusCircle} from "@fortawesome/free-solid-svg-icons";
4 | import {t} from "i18next";
5 | import "./styles.sass";
6 |
7 | export const AverageChart = (props) => {
8 |
9 | return (
10 |
11 |
12 |
13 |
14 |
{t("statistics.values.min")}
15 |
{props.data.min} {t("latest.speed_unit")}
16 |
17 |
18 |
19 |
20 |
21 |
{t("statistics.values.max")}
22 |
{props.data.max} {t("latest.speed_unit")}
23 |
24 |
25 |
26 |
27 |
28 |
{t("statistics.values.avg")}
29 |
{props.data.avg} {t("latest.speed_unit")}
30 |
31 |
32 |
33 |
34 |
35 | );
36 |
37 | }
--------------------------------------------------------------------------------
/client/src/pages/Statistics/charts/AverageChart/index.js:
--------------------------------------------------------------------------------
1 | export {AverageChart as default} from './AverageChart';
--------------------------------------------------------------------------------
/client/src/pages/Statistics/charts/AverageChart/styles.sass:
--------------------------------------------------------------------------------
1 | @import "@/common/styles/colors"
2 |
3 | .value-container
4 | display: flex
5 | flex-direction: column
6 | gap: 2rem
7 | justify-content: center
8 | width: 100%
9 |
10 | .value-item
11 | display: flex
12 | justify-content: space-between
13 |
14 | .value-item svg
15 | width: 2.5rem
16 | height: 2.5rem
17 | color: $green
18 |
19 | .value-item .value-info h2
20 | font-size: 16pt
21 | color: $white
22 | margin: 0
23 |
24 | .value-item .value-info p
25 | color: $green
26 | margin: 0
27 | font-weight: 600
--------------------------------------------------------------------------------
/client/src/pages/Statistics/charts/DurationChart.jsx:
--------------------------------------------------------------------------------
1 | import StatisticContainer from "@/pages/Statistics/components/StatisticContainer";
2 | import {Bar} from "react-chartjs-2";
3 | import {useEffect, useState} from "react";
4 | import {t} from "i18next";
5 |
6 | const chartOptions = {
7 | plugins: {
8 | legend: {
9 | display: false
10 | }
11 | },
12 | scales: {
13 | x: {
14 | ticks: {
15 | color: "#B0B0B0",
16 | }
17 | },
18 | y: {
19 | ticks: {
20 | color: "#B0B0B0",
21 | },
22 | beginAtZero: true,
23 | }
24 | }
25 | }
26 |
27 | const DurationChart = (props) => {
28 |
29 | const chartData = {
30 | labels: [],
31 | datasets: [{
32 | label: t("statistics.duration.label"),
33 | data: [],
34 | backgroundColor: ['#45C65A', '#456AC6', '#C64545', '#C6C645', '#C645C6'],
35 | borderWidth: 0,
36 | }],
37 | };
38 |
39 | const [data, setData] = useState({});
40 |
41 | useEffect(() => {
42 | if (!props.time) return;
43 |
44 | const frequencies = {};
45 | const tempData = {...chartData};
46 | props.time.forEach(second => {
47 | frequencies[second] ? frequencies[second]++ : frequencies[second] = 1;
48 | });
49 |
50 | for (let second in frequencies) {
51 | tempData.labels.push(t("time.seconds", {replace: {seconds: second.toString()}}));
52 | tempData.datasets[0].data.push(frequencies[second]);
53 | }
54 |
55 | setData(tempData);
56 | }, [props.time]);
57 |
58 | if (Object.keys(data).length === 0) return <>>;
59 |
60 | return (
61 |
62 |
63 |
64 | );
65 |
66 | }
67 |
68 | export default DurationChart;
--------------------------------------------------------------------------------
/client/src/pages/Statistics/charts/FailedChart.jsx:
--------------------------------------------------------------------------------
1 | import {Doughnut} from "react-chartjs-2";
2 | import StatisticContainer from "@/pages/Statistics/components/StatisticContainer";
3 | import {t} from "i18next";
4 |
5 | const chartOptions = {
6 | plugins: {
7 | legend: {
8 | display: false
9 | }
10 | },
11 | responsive: true,
12 | cutout: 80
13 | };
14 |
15 | const FailedChart = (props) => {
16 |
17 | const chartData = {
18 | labels: [t("statistics.failed.success"), t("statistics.failed.failed")],
19 | datasets: [{
20 | label: t("statistics.failed.label"),
21 | data: [props.tests.total - props.tests.failed, props.tests.failed],
22 | backgroundColor: ['#456AC6', '#C64545'],
23 | borderWidth: 0
24 | }]
25 | };
26 |
27 | return (
28 |
29 |
30 |
31 | );
32 | }
33 |
34 |
35 | export default FailedChart;
--------------------------------------------------------------------------------
/client/src/pages/Statistics/charts/LatestTestChart/index.js:
--------------------------------------------------------------------------------
1 | export {LatestTestChart as default} from "./LatestTestChart";
--------------------------------------------------------------------------------
/client/src/pages/Statistics/charts/LatestTestChart/styles.sass:
--------------------------------------------------------------------------------
1 | @import "@/common/styles/colors"
2 |
3 | .info-container
4 | display: flex
5 | flex-direction: column
6 | gap: 2rem
7 | justify-content: center
8 | width: 100%
9 |
10 | .test-container
11 | display: flex
12 | justify-content: space-between
13 |
14 | .test-container svg
15 | width: 3rem
16 | height: 3rem
17 |
18 | .test-container .test-info h2
19 | color: $white
20 | margin: 0
21 |
22 | .test-container .test-info p
23 | margin: 0
24 | font-weight: 600
--------------------------------------------------------------------------------
/client/src/pages/Statistics/charts/ManualChart.jsx:
--------------------------------------------------------------------------------
1 | import {Doughnut} from "react-chartjs-2";
2 | import StatisticContainer from "@/pages/Statistics/components/StatisticContainer";
3 | import {t} from "i18next";
4 |
5 | const chartOptions = {
6 | plugins: {
7 | legend: {
8 | display: false
9 | }
10 | },
11 | cutout: 80
12 | };
13 |
14 | const ManuelChart = (props) => {
15 |
16 | const chartData = {
17 | labels: [t("statistics.manual.yes"), t("statistics.manual.no")],
18 | datasets: [{
19 | label: t("statistics.failed.label"),
20 | data: [props.tests.custom, props.tests.total - props.tests.custom],
21 | backgroundColor: ['#456AC6', '#45C65A'],
22 | borderWidth: 0
23 | }]
24 | };
25 |
26 | return (
27 |
28 |
29 |
30 | );
31 | }
32 |
33 |
34 | export default ManuelChart;
--------------------------------------------------------------------------------
/client/src/pages/Statistics/charts/OverviewChart/OverviewChart.jsx:
--------------------------------------------------------------------------------
1 | import StatisticContainer from "@/pages/Statistics/components/StatisticContainer";
2 | import {t} from "i18next";
3 | import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
4 | import {faCircleExclamation, faGaugeHigh, faStopwatch} from "@fortawesome/free-solid-svg-icons";
5 | import "./styles.sass";
6 |
7 | export const OverviewChart = (props) => {
8 |
9 | const title = t("test.overview.title", {replace: {amount: t("test.overview." + (localStorage.getItem("testTime") || 1))}});
10 |
11 | const items = [
12 | {
13 | icon: faGaugeHigh,
14 | title: "statistics.overview.total_title",
15 | description: "statistics.overview.total_description",
16 | value: props.tests.total
17 | },
18 | {
19 | icon: faCircleExclamation,
20 | title: "statistics.overview.failed_title",
21 | description: "statistics.overview.failed_description",
22 | value: props.tests.failed
23 | },
24 | {
25 | icon: faStopwatch,
26 | title: "statistics.overview.average_title",
27 | description: "statistics.overview.average_description",
28 | value: props.time.avg + "s"
29 | }
30 | ];
31 |
32 | return (
33 |
34 |
35 | {items.map((item, index) => (
36 |
37 |
38 |
39 |
40 |
{t(item.title)}
41 |
{t(item.description)}
42 |
43 |
44 |
{item.value}
45 |
46 | ))}
47 |
48 |
49 | );
50 |
51 | }
--------------------------------------------------------------------------------
/client/src/pages/Statistics/charts/OverviewChart/index.js:
--------------------------------------------------------------------------------
1 | export {OverviewChart as default} from "./OverviewChart";
--------------------------------------------------------------------------------
/client/src/pages/Statistics/charts/OverviewChart/styles.sass:
--------------------------------------------------------------------------------
1 | @import "@/common/styles/colors"
2 |
3 | .overview-items
4 | display: flex
5 | justify-content: center
6 | flex-direction: column
7 | width: 100%
8 | overflow-y: clip
9 |
10 | .overview-item
11 | display: flex
12 | justify-content: space-between
13 |
14 | .overview-item .info-area
15 | display: flex
16 | justify-content: center
17 | align-content: center
18 | align-items: center
19 | gap: 1rem
20 |
21 | & svg
22 | width: 2.5rem
23 | height: 2.5rem
24 | color: $white
25 |
26 | .overview-item .info-area .text-area
27 | & h2
28 | margin: 0
29 | color: $white
30 | & p
31 | margin: 0
32 | font-weight: 500
33 | color: $subtext
34 |
35 | .overview-item h2
36 | color: $green
37 |
38 | @media screen and (max-width: 1500px)
39 | .overview-item
40 | flex-wrap: wrap
41 | flex-direction: row
42 |
43 | @media screen and (max-width: 1400px)
44 | .info-area svg
45 | display: none
46 | .overview-item .info-area h2
47 | display: inline-block
48 | width: 15rem
49 | white-space: nowrap
50 | overflow: hidden !important
51 | text-overflow: ellipsis
52 | .overview-item .info-area .text-area p
53 | display: none
54 |
55 | @media screen and (max-width: 1000px)
56 | .overview-item .info-area h2
57 | width: 10rem
--------------------------------------------------------------------------------
/client/src/pages/Statistics/charts/PingChart.jsx:
--------------------------------------------------------------------------------
1 | import {Line} from "react-chartjs-2";
2 | import StatisticContainer from "@/pages/Statistics/components/StatisticContainer";
3 | import {t} from "i18next";
4 |
5 | const chartOptions = {
6 | plugins: {
7 | tooltip: {
8 | callbacks: {
9 | label: (item) => item.dataset.label + ": " + item.formattedValue + " " + t("latest.ping_unit")
10 | }
11 | },
12 | legend: {
13 | display: false
14 | }
15 | },
16 | scales: {x: {reverse: true}},
17 | responsive: true
18 | };
19 |
20 | const SpeedChart = (props) => {
21 | const testTime = localStorage.getItem("testTime") || 1;
22 | const chartData = {
23 | labels: testTime < 3 ? props.labels.map((label) => new Date(label).toLocaleTimeString([],
24 | {hour: "2-digit", minute: "2-digit"})) : props.labels.slice(1).map((label) =>
25 | new Date(label).toLocaleDateString()),
26 | datasets: [
27 | {
28 | label: t("latest.ping"),
29 | data: testTime < 3 ? props.data.ping : props.data.ping.slice(1),
30 | borderColor: '#45C65A',
31 | },
32 | ],
33 | };
34 |
35 | return (
36 |
37 |
38 |
39 | )
40 |
41 | }
42 | export default SpeedChart;
--------------------------------------------------------------------------------
/client/src/pages/Statistics/charts/SpeedChart.jsx:
--------------------------------------------------------------------------------
1 | import {Line} from "react-chartjs-2";
2 | import StatisticContainer from "@/pages/Statistics/components/StatisticContainer";
3 | import {t} from "i18next";
4 |
5 | const chartOptions = {
6 | plugins: {
7 | tooltip: {
8 | callbacks: {
9 | label: (item) => item.dataset.label + ": " + item.formattedValue + " " + t("latest.speed_unit")
10 | }
11 | },
12 | legend: {
13 | position: "bottom"
14 | }
15 | },
16 | scales: {x: {reverse: true}}
17 | };
18 |
19 | const SpeedChart = (props) => {
20 | const testTime = localStorage.getItem("testTime") || 1;
21 | const chartData = {
22 | labels: testTime < 3 ? props.labels.map((label) => new Date(label).toLocaleTimeString([],
23 | {hour: "2-digit", minute: "2-digit"})) : props.labels.slice(1).map((label) =>
24 | new Date(label).toLocaleDateString()),
25 | datasets: [
26 | {
27 | label: t("latest.down"),
28 | data: testTime < 3 ? props.data.download : props.data.download.slice(1),
29 | borderColor: '#45C65A'
30 | },
31 | {
32 | label: t("latest.up"),
33 | data: testTime < 3 ? props.data.upload : props.data.upload.slice(1),
34 | borderColor: '#456AC6'
35 | },
36 | ],
37 | };
38 |
39 | return (
40 |
41 |
42 |
43 | )
44 |
45 | }
46 | export default SpeedChart;
--------------------------------------------------------------------------------
/client/src/pages/Statistics/components/StatisticContainer/StatisticContainer.jsx:
--------------------------------------------------------------------------------
1 | import "./styles.sass";
2 |
3 | export const StatisticContainer = (props) => {
4 |
5 | return (
6 |
7 |
8 | {props.title}
9 |
10 |
11 | {props.children}
12 |
13 |
14 | );
15 | }
--------------------------------------------------------------------------------
/client/src/pages/Statistics/components/StatisticContainer/index.js:
--------------------------------------------------------------------------------
1 | export {StatisticContainer as default} from './StatisticContainer';
--------------------------------------------------------------------------------
/client/src/pages/Statistics/components/StatisticContainer/styles.sass:
--------------------------------------------------------------------------------
1 | @import "@/common/styles/colors"
2 |
3 | .stats-container
4 | display: flex
5 | flex-direction: column
6 | min-width: 16rem
7 | border: 2px solid $light-gray
8 | background-color: $dark-gray
9 | border-radius: 1rem
10 | cursor: pointer
11 | transition: all 0.2s ease-in-out
12 | animation: 0.3s fadeIn
13 | flex: 1 0 15%
14 |
15 | .stats-container:hover
16 | border: 2px solid $green
17 | transform: scale(1.05)
18 |
19 | .stats-header
20 | color: $white
21 | font-size: 16pt
22 | border-radius: 1rem 1rem 0 0
23 | padding: 0.75rem 0.5rem 0.5rem 1rem
24 |
25 | .stats-content
26 | display: flex
27 | padding-bottom: 1rem
28 | padding-left: 1.5rem
29 | height: 14rem
30 | padding-right: 1.5rem
31 |
32 | .container-center
33 | justify-content: center
34 | align-items: center
35 |
36 | .container-small
37 | flex: 1 0 5%
38 |
39 | .container-normal
40 | flex: 1 0 30%
41 |
42 | .container-large
43 | flex: 1 0 35%
--------------------------------------------------------------------------------
/client/src/pages/Statistics/index.js:
--------------------------------------------------------------------------------
1 | export {Statistics as default} from "./Statistics";
--------------------------------------------------------------------------------
/client/src/pages/Statistics/styles.sass:
--------------------------------------------------------------------------------
1 | @import "@/common/styles/colors"
2 |
3 | .statistic-area
4 | margin-left: 20rem
5 | margin-right: 20rem
6 | padding-top: 1.5rem
7 | margin-bottom: 2rem
8 | display: flex
9 | gap: 2rem
10 | justify-content: space-between
11 | flex-wrap: wrap
12 | transition: 1s all ease-in-out
13 |
14 | @media screen and (max-width: 1472px)
15 | .statistic-area
16 | margin-left: 10rem
17 | margin-right: 10rem
18 |
19 | @media screen and (max-width: 425px)
20 | .statistic-area
21 | margin-left: 3rem
22 | margin-right: 3rem
23 |
24 | @media screen and (max-width: 375px)
25 | .statistic-area
26 | margin-left: 1rem
27 | margin-right: 1rem
--------------------------------------------------------------------------------
/client/vite.config.js:
--------------------------------------------------------------------------------
1 | import {defineConfig} from "vite";
2 | import react from "@vitejs/plugin-react";
3 | import {VitePWA} from "vite-plugin-pwa";
4 | import * as path from "path";
5 |
6 | export default defineConfig({
7 | plugins: [
8 | VitePWA({injectRegister: "auto", manifest: false}),
9 | react()
10 | ],
11 | build: {
12 | outDir: "build",
13 | chunkSizeWarningLimit: 1600,
14 | rollupOptions: {
15 | output: {
16 | manualChunks(id) {
17 | if (id.includes('node_modules'))
18 | return id.includes('@fortawesome') ? 'icons' : 'vendor';
19 | }
20 | }
21 | }
22 | },
23 | resolve: {
24 | alias: {
25 | "@": path.resolve(__dirname, "./src"),
26 | },
27 | },
28 | server: {
29 | proxy: {
30 | "/api": "http://localhost:5216/"
31 | }
32 | }
33 | });
--------------------------------------------------------------------------------
/crowdin.yml:
--------------------------------------------------------------------------------
1 | files:
2 | - source: /client/public/assets/locales/en.json
3 | translation: /client/public/assets/locales/%two_letters_code%.json
4 |
--------------------------------------------------------------------------------
/docs/CNAME:
--------------------------------------------------------------------------------
1 | docs.myspeed.dev
--------------------------------------------------------------------------------
/docs/assets/images/de/integrations.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnmyt/MySpeed/5b41668ee5ecb3bde8898e32e5dacba0e523ef03/docs/assets/images/de/integrations.png
--------------------------------------------------------------------------------
/docs/assets/images/de/interface.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnmyt/MySpeed/5b41668ee5ecb3bde8898e32e5dacba0e523ef03/docs/assets/images/de/interface.png
--------------------------------------------------------------------------------
/docs/assets/images/de/latest.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnmyt/MySpeed/5b41668ee5ecb3bde8898e32e5dacba0e523ef03/docs/assets/images/de/latest.png
--------------------------------------------------------------------------------
/docs/assets/images/de/settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnmyt/MySpeed/5b41668ee5ecb3bde8898e32e5dacba0e523ef03/docs/assets/images/de/settings.png
--------------------------------------------------------------------------------
/docs/assets/images/de/tests.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnmyt/MySpeed/5b41668ee5ecb3bde8898e32e5dacba0e523ef03/docs/assets/images/de/tests.png
--------------------------------------------------------------------------------
/docs/assets/images/de/view.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnmyt/MySpeed/5b41668ee5ecb3bde8898e32e5dacba0e523ef03/docs/assets/images/de/view.png
--------------------------------------------------------------------------------
/docs/assets/images/en/integrations.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnmyt/MySpeed/5b41668ee5ecb3bde8898e32e5dacba0e523ef03/docs/assets/images/en/integrations.png
--------------------------------------------------------------------------------
/docs/assets/images/en/interface.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnmyt/MySpeed/5b41668ee5ecb3bde8898e32e5dacba0e523ef03/docs/assets/images/en/interface.png
--------------------------------------------------------------------------------
/docs/assets/images/en/latest.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnmyt/MySpeed/5b41668ee5ecb3bde8898e32e5dacba0e523ef03/docs/assets/images/en/latest.png
--------------------------------------------------------------------------------
/docs/assets/images/en/settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnmyt/MySpeed/5b41668ee5ecb3bde8898e32e5dacba0e523ef03/docs/assets/images/en/settings.png
--------------------------------------------------------------------------------
/docs/assets/images/en/tests.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnmyt/MySpeed/5b41668ee5ecb3bde8898e32e5dacba0e523ef03/docs/assets/images/en/tests.png
--------------------------------------------------------------------------------
/docs/assets/images/en/view.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnmyt/MySpeed/5b41668ee5ecb3bde8898e32e5dacba0e523ef03/docs/assets/images/en/view.png
--------------------------------------------------------------------------------
/docs/assets/images/latest_test.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnmyt/MySpeed/5b41668ee5ecb3bde8898e32e5dacba0e523ef03/docs/assets/images/latest_test.png
--------------------------------------------------------------------------------
/docs/assets/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnmyt/MySpeed/5b41668ee5ecb3bde8898e32e5dacba0e523ef03/docs/assets/images/logo.png
--------------------------------------------------------------------------------
/docs/index.en.md:
--------------------------------------------------------------------------------
1 | # MySpeed Documentation
2 |
3 | This documentation explains how to install, configure and use MySpeed on a server of your choice.
4 |
5 | !!! note ""
6 | Did you find another suggestion or bug? Then open an [Issue](https://github.com/gnmyt/myspeed/issues) or write me a
7 | message on [Discord (@GNM)](https://discord.com/users/386242172632170496)
8 |
9 | ## What is it?
10 |
11 | MySpeed is a speed test analysis software that stores the speed of your internet for up to 30 days. This can also be
12 | useful if you want to know when your network might have drops or if you want to check if your internet matches the
13 | booked values from your contract.
14 |
15 | It is recommended to install the software at home. Of course, it can also be installed on a server in a data center, if
16 | you wish ;)
17 |
18 | ## How does it work?
19 |
20 | MySpeed automatically creates tests every hour, which you can view in a dashboard. If a test is older than 30 days, it
21 | will be deleted to keep the dashboard clearer.
22 |
23 | ## What do I need?
24 |
25 | In summary, just a device that is capable of running **24/7** and is supported by **NodeJS**. This includes Windows,
26 | macOS or Linux, among others.
27 |
28 | Even though it is possible to install the system on a normal computer, it is recommended to install it on a device that
29 | consumes little power. Perfect for this would be for example a so-called [Raspberry Pi](https://www.raspberrypi.com/) or
30 | other [Mini-PCs](https://www.amazon.de/s?k=mini+pc).
31 |
32 | ## :heart: And a big thanks to ...
33 |
34 | - [Sierra Devoplers](https://sierra-dev.de/myspeed) for the page created there
35 | - [Ookla](https://www.ookla.com/) for the test data and the great test network
36 | - [All translators](https://crowdin.com/project/myspeed) for providing it in different languages
37 | - [All contributors](https://github.com/gnmyt/myspeed/graphs/contributors) for their help and work into the project
--------------------------------------------------------------------------------
/docs/instructions/main.de.md:
--------------------------------------------------------------------------------
1 | # Die Oberfläche
2 |
3 | Die Oberfläche von MySpeed ist vom Grund auf so konzipiert, dass sie einfach zu bedienen ist. Um mögliche Verwirrungen
4 | jedoch zu vermeiden, wird alles hier noch einmal erklärt.
5 |
6 | ## Die Oberfläche
7 |
8 | {: align="left" }
9 |
10 | Die Oberfläche von MySpeed ist in **3** Teile unterteilt.
11 |
12 | Der **erste** Teil ist der sogenannte Header und enthält einen direkten Zugriff auf die Einstellungen und das Starten
13 | eines Speedtests. Auf die Einstellungen wird [hier](../settings) genauer eingegangen.
14 |
15 | Im **zweiten** Teil wird immer der aktuellste Speedtest angezeigt.
16 |
17 | Der **dritte** Teil zeigt alle erstellten Speedtests an.
18 |
19 |
20 |
21 | ## Letzter Test
22 | {align="left"}
23 |
24 | Dieses Teil zeigt immer den aktuellsten, also zuletzt erstellten Speedtest an. Klickt man auf ein Icon, öffnet sich
25 | das jeweilige Hilfe-Menü mit mehr Informationen
26 |
27 |
28 |
29 | ## Übersicht aller Tests
30 |
31 | {align="left"}
32 |
33 | Hier lassen sich alle Tests anzeigen, die erstellt wurden. Es lässt sich jetzt auch auf die Uhr links klicken, um
34 | einen Dialog mit weiteren Informationen zu öffnen. Dieser Dialog zeigt nun weitere Informationen über den Test und
35 | bietet sogar die Möglichkeit, ihn direkt zu löschen (falls nötig).
36 |
--------------------------------------------------------------------------------
/docs/instructions/main.en.md:
--------------------------------------------------------------------------------
1 | # The interface
2 |
3 | MySpeed's interface is designed from the ground up to be easy to use. However, to avoid possible confusion,
4 | everything is explained here.
5 |
6 | ## The user interface
7 |
8 | {: align="left" }
9 |
10 | The interface of MySpeed is divided into **3** parts.
11 |
12 | The **first** part is the so-called header and contains direct access to the settings and the start of a speed test.
13 | The settings are described in more detail [here](../settings).
14 |
15 | The **second** part always shows the most recent speedtest.
16 |
17 | The **third** part shows all created speedtests.
18 |
19 |
20 |
21 | ## Last test
22 | {align="left"}
23 |
24 | This part always shows the most recent, i.e. last created speedtest. If you click on an icon, the respective help menu will open
25 | with more information
26 |
27 |
28 |
29 | ## Overview of all tests
30 |
31 | {align="left"}
32 |
33 | Here you can view all tests that have been created. Now you can also click on the clock on the left to open a dialog
34 | with more information. This dialog now shows more information about the test and even offers the possibility to delete
35 | it directly (if necessary).
--------------------------------------------------------------------------------
/docs/troubleshooting.de.md:
--------------------------------------------------------------------------------
1 | # Fehlerbehebung
2 |
3 | In diesem Guide erfährst du, wie du bekannte Fehler mit diesem Dienst behebst.
4 |
5 | ??? failure "Could not open the database file. Maybe it is damaged?"
6 | Bei diesem Fehler kann es mehrere Lösungen geben. Arbeite einfach alle Möglichkeiten durch und dein Problem sollte gelöst sein. :)
7 |
8 | 1. Berechtigungen setzen
9 | Um die Berechtigungen zu setzen, gib den Befehl `chmod 700 /opt/myspeed` ein. (Ersetze /opt/myspeed mit deinem Installationsort)
10 |
11 | 2. Build Essentials Installieren
12 | Es ist aktuell möglich, dass der Fehler auftritt, weil das Paket `build-essential` nicht installiert ist. Installiere es ganz einfach mit dem Befehl `sudo apt-get install build-essential` nach.
13 |
14 | 3. Führe eine Neuinstallation der Module aus
15 | Gib zuerst den Befehl `rm -R node_modules` im Installationsordner ein, um die Module zu löschen und installiere sie dann mit `npm install` nach.
16 |
17 | ??? failure "Diese MySpeed-Instanz befindet sich aktuell im Entwicklungsmodus"
18 | Das ist nicht wirklich ein Fehler, lediglich eine Sicherung um das Tool nur in Produktionsumgebungen verwenden zu können.
19 | Setze dazu die Umgebungsvariable `NODE_ENV` auf den Wert `production`.
20 | Unter Linux erreichst du das mit `export NODE_ENV=production` und unter Windows in der Powershell mit `$env:NODE_ENV="production"`
21 | Lies dir auch mal den [Guide zur 24/7 Installation](../setup/linux) durch,
22 | wenn du planst, MySpeed im Hintergrund laufen zu lassen und beim Systemstart automatisch hochzufahren.
--------------------------------------------------------------------------------
/docs/troubleshooting.en.md:
--------------------------------------------------------------------------------
1 | # Troubleshooting
2 |
3 | In this guide you will learn how to fix known errors with this service.
4 |
5 | ??? failure "Could not open the database file. Maybe it is damaged?"
6 | There can be several solutions to this error. Just work through all possibilities and your problem should be solved. :)
7 |
8 | 1. Set the required permissions
9 | To set the permissions, enter the command `chmod 700 /opt/myspeed`. (Replace /opt/myspeed with your installation location).
10 |
11 | 2. Install the build essentials
12 | It is currently possible that the error occurs because the `build-essential` package is not installed. Simply reinstall it with the command `sudo apt-get install build-essential`.
13 |
14 | 3. Perform a new installation of the modules
15 | First enter the command `rm -R node_modules` in the installation folder to delete the modules and then reinstall them with `npm install`.
16 |
17 | ??? failure "This MySpeed instance is currently in development mode"
18 | This is not really an error, just a backup to use the tool only in production environments.
19 | Set the environment variable `NODE_ENV` to the value `production`.
20 | On Linux you can do this with `export NODE_ENV=production` and on Windows in the Powershell with `$env:NODE_ENV="production"`.
21 | Also read the [guide for 24/7 installation](../setup/linux), if you plan to run MySpeed in the background and start it automatically
22 | at system startup.
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "myspeed",
3 | "version": "1.0.9",
4 | "scripts": {
5 | "client": "cd client && npm run dev",
6 | "server": "nodemon server",
7 | "build": "cd client && npm run build",
8 | "dev": "concurrently --kill-others-on-fail \"npm run server\" \"npm run client\""
9 | },
10 | "dependencies": {
11 | "@resvg/resvg-js": "^2.6.2",
12 | "axios": "^1.9.0",
13 | "bcrypt": "^6.0.0",
14 | "cron-validator": "^1.3.1",
15 | "decompress": "^4.2.1",
16 | "decompress-targz": "^4.1.1",
17 | "decompress-unzip": "^4.0.1",
18 | "express": "^5.1.0",
19 | "mysql2": "^3.14.1",
20 | "node-schedule": "^2.1.1",
21 | "prom-client": "^15.1.3",
22 | "satori": "^0.13.2",
23 | "satori-html": "^0.3.2",
24 | "sequelize": "^6.37.7",
25 | "sqlite3": "^5.1.7",
26 | "tmp": "^0.2.3"
27 | },
28 | "devDependencies": {
29 | "concurrently": "^9.1.2",
30 | "nodemon": "^3.1.10"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/scripts/chooser.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | GREEN='\033[0;32m'
4 | BLUE='\033[1;34m'
5 | YELLOW='\033[1;33m'
6 | RED='\033[1;31m'
7 | NORMAL='\033[0;39m'
8 | PURPLE='\033[0;35m'
9 |
10 | if [ $EUID -ne 0 ]; then
11 | echo -e "$RED-$NORMAL-$RED-$NORMAL-$RED-$NORMAL-$RED-$NORMAL-$RED-$NORMAL-$RED-$NORMAL-$RED-$NORMAL-$RED-$NORMAL-$RED-$NORMAL-"
12 | echo -e "$RED✗ ABORTED"
13 | echo -e "$NORMAL The installation is currently running via a user without root privileges. However, this is required. Please log in with a Root Account to continue."
14 | echo -e "$RED-$NORMAL-$RED-$NORMAL-$RED-$NORMAL-$RED-$NORMAL-$RED-$NORMAL-$RED-$NORMAL-$RED-$NORMAL-$RED-$NORMAL-$RED-$NORMAL-"
15 | exit
16 | fi
17 |
18 | echo -e "${GREEN}---------${BLUE} MySpeed Installation ${GREEN}---------${NORMAL}"
19 | echo -e "${BLUE}Welcome to MySpeed Installation Script!${NORMAL}"
20 | echo -e "${YELLOW}Do you want to install MySpeed with Docker or the normal installation script?${NORMAL}"
21 | echo -e "${YELLOW}[1]${NORMAL} Docker (Recommended)"
22 | echo -e "${YELLOW}[2]${NORMAL} Normal Install Script"
23 |
24 | read -p "Enter your choice (1/2): " choice
25 |
26 | case $choice in
27 | 1)
28 | echo -e "${BLUE}Running Docker installation script...${NORMAL}"
29 | bash <(curl -sSL https://raw.githubusercontent.com/gnmyt/myspeed/development/scripts/docker-install.sh)
30 | ;;
31 | 2)
32 | echo -e "${BLUE}Running normal installation script...${NORMAL}"
33 | bash <(curl -sSL https://raw.githubusercontent.com/gnmyt/myspeed/development/scripts/install.sh)
34 | ;;
35 | *)
36 | echo -e "${RED}Invalid choice. Exiting.${NORMAL}"
37 | exit 1
38 | ;;
39 | esac
--------------------------------------------------------------------------------
/scripts/docker-install.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | GREEN='\033[0;32m'
4 | BLUE='\033[1;34m'
5 | YELLOW='\033[1;33m'
6 | RED='\033[1;31m'
7 | NORMAL='\033[0;39m'
8 |
9 | if [ $EUID -ne 0 ]; then
10 | echo -e "$RED-$NORMAL-$RED-$NORMAL-$RED-$NORMAL-$RED-$NORMAL-$RED-$NORMAL-$RED-$NORMAL-$RED-$NORMAL-$RED-$NORMAL-$RED-$NORMAL-"
11 | echo -e "$RED✗ ABORTED"
12 | echo -e "$NORMAL The installation is currently running via a user without root privileges. However, this is required. Please log in with a Root Account to continue."
13 | echo -e "$RED-$NORMAL-$RED-$NORMAL-$RED-$NORMAL-$RED-$NORMAL-$RED-$NORMAL-$RED-$NORMAL-$RED-$NORMAL-$RED-$NORMAL-$RED-$NORMAL-"
14 | exit
15 | fi
16 |
17 | if ! command -v docker &> /dev/null; then
18 | echo -e "${YELLOW}Docker is not installed. Installing Docker...${NORMAL}"
19 | curl -fsSL https://get.docker.com -o get-docker.sh
20 | sh get-docker.sh
21 | rm get-docker.sh
22 | fi
23 |
24 | INSTALLATION_PATH="/opt/myspeed-dockerized"
25 | mkdir -p "$INSTALLATION_PATH"
26 |
27 | echo -e "${BLUE}Creating docker-compose.yml file...${NORMAL}"
28 | cat << EOF > "$INSTALLATION_PATH/docker-compose.yml"
29 | version: '3'
30 | services:
31 | myspeed:
32 | image: germannewsmaker/myspeed
33 | ports:
34 | - "5216:5216"
35 | volumes:
36 | - myspeed:/myspeed/data
37 | restart: unless-stopped
38 | container_name: MySpeed
39 | volumes:
40 | myspeed:
41 | EOF
42 |
43 | echo -e "${GREEN}Starting MySpeed Docker container...${NORMAL}"
44 | cd "$INSTALLATION_PATH" && docker compose up -d
45 |
46 | if [ $? -eq 0 ]; then
47 | echo -e "${GREEN}MySpeed Docker container started successfully.${NORMAL}"
48 | else
49 | echo -e "${RED}Error: Failed to start MySpeed Docker container.${NORMAL}"
50 | exit 1
51 | fi
--------------------------------------------------------------------------------
/server/config/binaries.js:
--------------------------------------------------------------------------------
1 | module.exports.ooklaVersion = "1.2.0";
2 | module.exports.ooklaList = [
3 | // MacOS
4 | {os: 'darwin', arch: 'x64', suffix: 'macosx-x86_64.tgz'},
5 |
6 | // Windows
7 | {os: 'win32', arch: 'x64', suffix: 'win64.zip'},
8 |
9 | // Linux
10 | {os: 'linux', arch: 'ia32', suffix: 'linux-i386.tgz'},
11 | {os: 'linux', arch: 'x64', suffix: 'linux-x86_64.tgz'},
12 | {os: 'linux', arch: 'arm', suffix: 'linux-armhf.tgz'},
13 | {os: 'linux', arch: 'arm64', suffix: 'linux-aarch64.tgz'},
14 |
15 | // FreeBSD
16 | {os: 'freebsd', arch: 'x64', suffix: 'freebsd12-x86_64.pkg'}
17 | ];
18 |
19 | module.exports.libreVersion = "1.0.10";
20 | module.exports.libreList = [
21 | // MacOS
22 | {os: 'darwin', arch: 'x64', suffix: 'darwin_amd64.tar.gz'},
23 | {os: 'darwin', arch: 'arm64', suffix: 'darwin_arm64.tar.gz'},
24 |
25 | // Windows
26 | {os: 'win32', arch: 'x64', suffix: 'windows_amd64.zip'},
27 | {os: 'win32', arch: 'ia32', suffix: 'windows_386.zip'},
28 | {os: 'win32', arch: 'arm64', suffix: 'windows_arm64.zip'},
29 |
30 | // Linux
31 | {os: 'linux', arch: 'x64', suffix: 'linux_amd64.tar.gz'},
32 | {os: 'linux', arch: 'ia32', suffix: 'linux_386.tar.gz'},
33 | {os: 'linux', arch: 'arm', suffix: 'linux_armv7.tar.gz'},
34 | {os: 'linux', arch: 'arm64', suffix: 'linux_arm64.tar.gz'},
35 |
36 | // FreeBSD
37 | {os: 'freebsd', arch: 'x64', suffix: 'freebsd_amd64.tar.gz'},
38 | {os: 'freebsd', arch: 'ia32', suffix: 'freebsd_386.tar.gz'},
39 | {os: 'freebsd', arch: 'arm', suffix: 'freebsd_armv7.tar.gz'},
40 | {os: 'freebsd', arch: 'arm64', suffix: 'freebsd_arm64.tar.gz'}
41 | ]
--------------------------------------------------------------------------------
/server/config/database.js:
--------------------------------------------------------------------------------
1 | const {Sequelize} = require('sequelize');
2 |
3 | const STORAGE_PATH = `data/storage${process.env.PREVIEW_MODE === "true" ? "_preview" : ""}.db`;
4 |
5 | Sequelize.DATE.prototype._stringify = () => {
6 | return new Date().toISOString();
7 | }
8 |
9 | if (process.env.DB_TYPE === "mysql") {
10 |
11 | if (!process.env.DB_NAME || !process.env.DB_PASS || !process.env.DB_USER)
12 | throw new Error("Missing database environment variables");
13 |
14 | module.exports = new Sequelize(process.env.DB_NAME, process.env.DB_USER, process.env.DB_PASS, {
15 | host: process.env.DB_HOST || "localhost",
16 | dialect: 'mysql',
17 | logging: false,
18 | query: {raw: true}
19 | });
20 | } else if (!process.env.DB_TYPE || process.env.DB_TYPE === "sqlite") {
21 | module.exports = new Sequelize({dialect: 'sqlite', storage: STORAGE_PATH, logging: false, query: {raw: true}});
22 | } else {
23 | throw new Error("Invalid database type");
24 | }
--------------------------------------------------------------------------------
/server/controller/node.js:
--------------------------------------------------------------------------------
1 | const axios = require('axios');
2 | const nodes = require('../models/Node');
3 |
4 | module.exports.listAll = async () => await nodes.findAll()
5 | .then((result) => result.map((node) => ({...node, password: node.password !== null})));
6 |
7 | module.exports.create = async (name, url, password) => await nodes.create({name: name, url: url, password: password});
8 |
9 | module.exports.delete = async (nodeId) => await nodes.destroy({where: {id: nodeId}});
10 |
11 | module.exports.getOne = async (nodeId) => await nodes.findOne({where: {id: nodeId}});
12 |
13 | module.exports.updateName = async (nodeId, name) => await nodes.update({name: name}, {where: {id: nodeId}});
14 |
15 | module.exports.updatePassword = async (nodeId, password) => await nodes.update({password: password}, {where: {id: nodeId}});
16 |
17 | module.exports.checkStatus = async (url, password) => {
18 | if (password === "none") password = undefined;
19 | const api = await axios.get(url + "/api/config", {headers: {password: password}}).catch(() => {
20 | return "INVALID_URL";
21 | });
22 |
23 | if (api === "INVALID_URL" || api.status !== 200) return "INVALID_URL";
24 |
25 | if (!api.data.ping) return "INVALID_URL";
26 |
27 | if (api.data.viewMode) return "PASSWORD_REQUIRED";
28 |
29 | return "NODE_VALID";
30 | }
31 |
32 | module.exports.proxyRequest = async (url, req, res) => {
33 | const response = await axios(url, {
34 | method: req.method,
35 | headers: req.headers,
36 | data: req.method === "GET" ? undefined : JSON.stringify(req.body),
37 | signal: req.signal,
38 | validateStatus: (status) => status >= 200 && status < 400
39 | }).catch(() => "INVALID_URL");
40 |
41 | if (response === "INVALID_URL")
42 | return res.status(500).json({message: "Internal server error"});
43 |
44 | if (response.headers["content-disposition"])
45 | res.setHeader("content-disposition", response.headers["content-disposition"]);
46 |
47 | res.status(response.status).json(response.data);
48 | }
--------------------------------------------------------------------------------
/server/controller/pause.js:
--------------------------------------------------------------------------------
1 | let currentState = false;
2 | let updateTimer;
3 |
4 | module.exports.updateState = (newState) => {
5 | this.currentState = newState;
6 | }
7 |
8 | module.exports.resumeIn = (hours) => {
9 | if (/[^0-9]/.test(hours)) return false;
10 |
11 |
12 | if (updateTimer !== null)
13 | clearTimeout(updateTimer);
14 |
15 | this.updateState(true);
16 | updateTimer = setTimeout(() => this.updateState(false), hours * 3600000); // time in hours
17 |
18 | return true;
19 | }
20 |
21 | module.exports.currentState = currentState;
--------------------------------------------------------------------------------
/server/controller/recommendations.js:
--------------------------------------------------------------------------------
1 | const recommendations = require('../models/Recommendations');
2 | const {triggerEvent} = require("./integrations");
3 |
4 | module.exports.getCurrent = async () => {
5 | return await recommendations.findOne();
6 | }
7 |
8 | module.exports.update = async (ping, download, upload) => {
9 | const configuration = {ping: Math.round(ping), download: parseFloat(download.toFixed(2)),
10 | upload: parseFloat(upload.toFixed(2))};
11 |
12 | await recommendations.destroy({truncate: true});
13 |
14 | triggerEvent("recommendationsUpdated", configuration).then(() => {});
15 |
16 | return recommendations.create(configuration);
17 | }
--------------------------------------------------------------------------------
/server/controller/servers.js:
--------------------------------------------------------------------------------
1 | const fs = require("fs");
2 | let ooklaServers;
3 | let libreServers;
4 |
5 | module.exports.getLibreServers = () => {
6 | if (libreServers) return libreServers;
7 |
8 | if (fs.existsSync("./data/servers/librespeed.json")) {
9 | libreServers = fs.readFileSync("./data/servers/librespeed.json");
10 | libreServers = JSON.parse(libreServers);
11 |
12 | return libreServers;
13 | }
14 |
15 | return [];
16 | }
17 |
18 | module.exports.getOoklaServers = () => {
19 | if (ooklaServers) return ooklaServers;
20 |
21 | if (fs.existsSync("./data/servers/ookla.json")) {
22 | ooklaServers = fs.readFileSync("./data/servers/ookla.json");
23 | ooklaServers = JSON.parse(ooklaServers);
24 |
25 | return ooklaServers;
26 | }
27 |
28 | return [];
29 | }
30 |
31 | module.exports.getByMode = (mode) => {
32 | if (mode === "ookla") return this.getOoklaServers();
33 | if (mode === "libre") return this.getLibreServers();
34 | }
--------------------------------------------------------------------------------
/server/integrations/discord.js:
--------------------------------------------------------------------------------
1 | const axios = require("axios");
2 | const {replaceVariables} = require("../util/helpers");
3 |
4 | const defaults = {
5 | finished: ":sparkles: **A speedtest is finished**\n > :ping_pong: `Ping`: %ping% ms\n > :arrow_up: `Upload`: %upload% Mbps\n > :arrow_down: `Download`: %download% Mbps",
6 | failed: ":x: **A speedtest has failed**\n > `Reason`: %error%"
7 | }
8 |
9 | const postWebhook = async (url, username, color, message, triggerActivity) => {
10 | axios.post(url, {
11 | content: null, username,
12 | embeds: [{description: message, color, footer: {text: "MySpeed"}, timestamp: new Date().toISOString()}],
13 | })
14 | .then(() => triggerActivity())
15 | .catch(() => triggerActivity(true));
16 | }
17 |
18 | module.exports = (registerEvent) => {
19 | registerEvent('testFinished', async (integration, data, activity) => {
20 | if (integration.data.send_finished)
21 | await postWebhook(integration.data.url, integration.data.display_name || "MySpeed", 4572762,
22 | replaceVariables(integration.data.finished_message || defaults.finished, data), activity)
23 | });
24 |
25 | registerEvent('testFailed', async (integration, error, activity) => {
26 | if (integration.data.send_failed)
27 | await postWebhook(integration.data.url, integration.data.display_name || "MySpeed", 12993861,
28 | replaceVariables(integration.data.failed_message || defaults.failed, {error}), activity)
29 | });
30 |
31 | return {
32 | icon: "fa-brands fa-discord",
33 | fields: [
34 | {name: "url", type: "text", required: true, regex: /https:\/\/.*discord.com\/api\/webhooks\/\d+\/.+/},
35 | {name: "display_name", type: "text", required: false},
36 | {name: "send_finished", type: "boolean", required: false},
37 | {name: "finished_message", type: "textarea", required: false},
38 | {name: "send_failed", type: "boolean", required: false},
39 | {name: "error_message", type: "textarea", required: false}
40 | ]
41 | };
42 | }
--------------------------------------------------------------------------------
/server/integrations/gotify.js:
--------------------------------------------------------------------------------
1 | const axios = require("axios");
2 | const {replaceVariables} = require("../util/helpers");
3 |
4 | const defaults = {
5 | finished: "A speedtest is finished:\nPing: %ping% ms\nUpload: %upload% Mbps\nDownload: %download% Mbps",
6 | failed: "A speedtest has failed. Reason: %error%"
7 | }
8 |
9 | const postWebhook = async (url, key, triggerActivity, message, priority) => {
10 | axios.post(`${url}/message`, {message, priority: parseInt(priority)},
11 | {headers: {"Authorization": "Bearer " + key}})
12 | .then(() => triggerActivity())
13 | .catch(() => triggerActivity(true));
14 | }
15 |
16 | module.exports = (registerEvent) => {
17 | registerEvent('testFinished', async (integration, data, activity) => {
18 | if (integration.data.send_finished)
19 | await postWebhook(integration.data.url, integration.data.key, activity,
20 | replaceVariables(integration.data.finished_message || defaults.finished, data),
21 | integration.data.priority);
22 | });
23 |
24 | registerEvent('testFailed', async (integration, error, activity) => {
25 | if (integration.data.send_failed)
26 | await postWebhook(integration.data.url, integration.data.key, activity,
27 | replaceVariables(integration.data.failed_message || defaults.failed, {error}), 8);
28 | });
29 |
30 | return {
31 | icon: "fa-solid fa-bell",
32 | fields: [
33 | {name: "url", type: "text", required: true, regex: /https?:\/\/.+/},
34 | {name: "key", type: "text", required: true, regex: /^.{15}$/},
35 | {name: "priority", type: "text", required: true, regex: /^[0-9]$/},
36 | {name: "send_finished", type: "boolean", required: false},
37 | {name: "finished_message", type: "textarea", required: false},
38 | {name: "send_failed", type: "boolean", required: false},
39 | {name: "error_message", type: "textarea", required: false}
40 | ]
41 | };
42 | }
--------------------------------------------------------------------------------
/server/integrations/healthChecks.js:
--------------------------------------------------------------------------------
1 | const axios = require("axios");
2 |
3 | const sendPing = async (url, path, error, triggerActivity) => {
4 | if (url == null) return;
5 | if (path) url += "/" + path;
6 |
7 | await axios.post(url, error, {headers: {"user-agent": "MySpeed/HealthAgent"}})
8 | .then(() => triggerActivity())
9 | .catch(() => triggerActivity(true));
10 | }
11 |
12 | module.exports = (registerEvent) => {
13 | registerEvent('minutePassed', async (integration, data, triggerActivity) => {
14 | if (integration.data.url) await sendPing(integration.data.url, undefined, undefined, triggerActivity);
15 | });
16 |
17 | registerEvent('testFailed', async (integration, error, triggerActivity) => {
18 | if (integration.data.url) await sendPing(integration.data.url, "fail", error, triggerActivity);
19 | });
20 |
21 | registerEvent('testStarted', async (integration, data, triggerActivity) => {
22 | if (integration.data.url) await sendPing(integration.data.url, "start", data, triggerActivity);
23 | });
24 |
25 | registerEvent('testFinished', async (integration, data, triggerActivity) => {
26 | if (integration.data.url) await sendPing(integration.data.url, undefined, undefined, triggerActivity);
27 | });
28 |
29 | return {
30 | icon: "fa-solid fa-heart-pulse",
31 | fields: [
32 | {name: "url", type: "text", required: true, regex: /https?:\/\/.+/},
33 | ]
34 | };
35 | }
--------------------------------------------------------------------------------
/server/integrations/pushover.js:
--------------------------------------------------------------------------------
1 | const axios = require("axios");
2 | const {replaceVariables} = require("../util/helpers");
3 |
4 | const BASE_URL = "https://api.pushover.net/1";
5 |
6 | const defaults = {
7 | finished: "A speedtest is finished:\nPing: %ping% ms\nUpload: %upload% Mbps\nDownload: %download% Mbps",
8 | failed: "A speedtest has failed. Reason: %error%"
9 | }
10 |
11 | module.exports = (registerEvent) => {
12 | registerEvent('testFinished', async (integration, data, triggerActivity) => {
13 | if (!integration.data.send_finished) return;
14 |
15 | const message = replaceVariables(integration.data.finished_message || defaults.finished, data);
16 |
17 | axios.post(`${BASE_URL}/messages.json`, {
18 | token: integration.data.token,
19 | user: integration.data.user_key, message
20 | }).then(() => triggerActivity())
21 | .catch(() => triggerActivity(true));
22 | });
23 |
24 | registerEvent('testFailed', async (integration, error, triggerActivity) => {
25 | if (!integration.data.send_failed) return;
26 |
27 | const message = replaceVariables(integration.data.error_message || defaults.failed, {error});
28 |
29 | axios.post(`${BASE_URL}/messages.json`, {
30 | token: integration.data.token,
31 | user: integration.data.user_key, message
32 | }).then(() => triggerActivity())
33 | .catch(() => triggerActivity(true));
34 | });
35 |
36 | return {
37 | icon: "fa-solid fa-pushover",
38 | fields: [
39 | {name: "token", type: "text", required: true, regex: /^[a-z0-9]{30}$/},
40 | {name: "user_key", type: "text", required: true, regex: /^[a-z0-9]{30}$/},
41 | {name: "send_finished", type: "boolean", required: false},
42 | {name: "finished_message", type: "textarea", required: false},
43 | {name: "send_failed", type: "boolean", required: false},
44 | {name: "error_message", type: "textarea", required: false}
45 | ]
46 | };
47 | }
--------------------------------------------------------------------------------
/server/integrations/telegram.js:
--------------------------------------------------------------------------------
1 | const axios = require("axios");
2 | const {replaceVariables} = require("../util/helpers");
3 |
4 | const defaults = {
5 | finished: "✨ *A speedtest is finished*\n🏓 `Ping`: %ping% ms\n🔼 `Upload`: %upload% Mbps\n🔽 `Download`: %download% Mbps",
6 | failed: "❌ *A speedtest has failed*\n`Reason`: %error%"
7 | }
8 |
9 | const postWebhook = async (token, chatId, message, triggerActivity) => {
10 | axios.post(`https://api.telegram.org/bot${token}/sendMessage`, {
11 | text: message, chat_id: chatId, parse_mode: "markdown"
12 | })
13 | .then(() => triggerActivity())
14 | .catch(() => triggerActivity(true));
15 | }
16 |
17 | module.exports = (registerEvent) => {
18 | registerEvent('testFinished', async (integration, data, activity) => {
19 | if (integration.data.send_finished)
20 | await postWebhook(integration.data.token, integration.data.chat_id,
21 | replaceVariables(integration.data.finished_message || defaults.finished, data), activity)
22 | });
23 |
24 | registerEvent('testFailed', async (integration, error, activity) => {
25 | if (integration.data.send_failed)
26 | await postWebhook(integration.data.token, integration.data.chat_id,
27 | replaceVariables(integration.data.failed_message || defaults.failed, {error}), activity)
28 | });
29 |
30 | return {
31 | icon: "fa-brands fa-telegram",
32 | fields: [
33 | {name: "token", type: "text", required: true, regex: /(\d+):[a-zA-Z0-9_-]+/},
34 | {name: "chat_id", type: "text", required: true, regex: /\d+/},
35 | {name: "send_finished", type: "boolean", required: false},
36 | {name: "finished_message", type: "textarea", required: false},
37 | {name: "send_failed", type: "boolean", required: false},
38 | {name: "error_message", type: "textarea", required: false}
39 | ]
40 | };
41 | }
--------------------------------------------------------------------------------
/server/middlewares/error.js:
--------------------------------------------------------------------------------
1 | module.exports = (err, req, res, next) => {
2 | if (!(err instanceof SyntaxError)) return next();
3 |
4 | res.status(400).json({message: "You need to provide a valid JSON body"});
5 | }
--------------------------------------------------------------------------------
/server/middlewares/password.js:
--------------------------------------------------------------------------------
1 | const config = require('../controller/config');
2 | const bcrypt = require('bcrypt');
3 |
4 | module.exports = (allowViewAccess) => async (req, res, next) => {
5 | if (process.env.PREVIEW_MODE === "true") return next();
6 |
7 | let passwordHash = await config.getValue("password");
8 | let passwordLevel = await config.getValue("passwordLevel");
9 |
10 | if (passwordHash === "none") {
11 | req.viewMode = false;
12 | return next();
13 | }
14 |
15 | if (req.headers.password && bcrypt.compareSync(req.headers.password, passwordHash)) {
16 | req.viewMode = false;
17 | return next();
18 | }
19 |
20 | if (passwordLevel === "read" && allowViewAccess) {
21 | req.viewMode = true;
22 | return next();
23 | }
24 |
25 | return res.status(401).json({message: "Please provide the correct password in the header"});
26 | }
--------------------------------------------------------------------------------
/server/middlewares/passwordWrapper.js:
--------------------------------------------------------------------------------
1 | const passwordMiddleware = require('./password');
2 |
3 | const passwordWrapper = (allowViewAccess, customResponseHandler) => async (req, res, next) => {
4 | // Intercept the response send method
5 | const originalSend = res.send.bind(res);
6 |
7 | res.send = function (body) {
8 | // Check if the status code is 401 and a custom response handler is provided
9 | if (res.statusCode === 401 && typeof customResponseHandler === 'function') {
10 | // The password middleware has returned a 401 status code, call the custom response handler
11 | return customResponseHandler(req, res);
12 | }
13 | // Call the original send method for other statuses
14 | return originalSend(body);
15 | };
16 |
17 | try {
18 | // Execute the original password middleware
19 | await passwordMiddleware(allowViewAccess)(req, res, next);
20 | } catch (err) {
21 | next(err);
22 | }
23 | };
24 |
25 | module.exports = passwordWrapper;
26 |
--------------------------------------------------------------------------------
/server/models/Config.js:
--------------------------------------------------------------------------------
1 | const Sequelize = require('sequelize');
2 | const db = require("../config/database");
3 |
4 | module.exports = db.define("config", {
5 | key: {
6 | type: Sequelize.STRING,
7 | primaryKey: true,
8 | allowNull: false
9 | },
10 | value: {
11 | type: Sequelize.STRING,
12 | allowNull: false
13 | }
14 | }, {freezeTableName: true, createdAt: false, updatedAt: false});
--------------------------------------------------------------------------------
/server/models/IntegrationData.js:
--------------------------------------------------------------------------------
1 | const Sequelize = require('sequelize');
2 | const db = require("../config/database");
3 |
4 | module.exports = db.define("integration_data", {
5 | id: {
6 | type: Sequelize.STRING,
7 | required: true,
8 | primaryKey: true,
9 | defaultValue: () => Math.random().toString(36).substring(2, 15)
10 | },
11 | displayName: {
12 | type: Sequelize.STRING,
13 | defaultValue: "Untitled"
14 | },
15 | name: {
16 | type: Sequelize.STRING,
17 | required: true,
18 | },
19 | data: {
20 | type: Sequelize.JSON,
21 | defaultValue: {},
22 | },
23 | lastActivity: {
24 | type: Sequelize.DATE,
25 | required: false
26 | },
27 | activityFailed: {
28 | type: Sequelize.BOOLEAN,
29 | defaultValue: false
30 | }
31 | }, {freezeTableName: true, createdAt: false, updatedAt: false});
--------------------------------------------------------------------------------
/server/models/Node.js:
--------------------------------------------------------------------------------
1 | const Sequelize = require('sequelize');
2 | const db = require("../config/database");
3 |
4 | module.exports = db.define("nodes", {
5 | name: {
6 | type: Sequelize.STRING,
7 | defaultValue: "MySpeed Server"
8 | },
9 | url: {
10 | type: Sequelize.STRING,
11 | allowNull: false
12 | },
13 | password: {
14 | type: Sequelize.STRING,
15 | allowNull: true
16 | }
17 | }, {createdAt: false, updatedAt: false});
--------------------------------------------------------------------------------
/server/models/Recommendations.js:
--------------------------------------------------------------------------------
1 | const Sequelize = require('sequelize');
2 | const db = require("../config/database");
3 |
4 | module.exports = db.define("recommendations", {
5 | ping: {
6 | type: Sequelize.INTEGER,
7 | allowNull: false
8 | },
9 | download: {
10 | type: Sequelize.DOUBLE,
11 | allowNull: false
12 | },
13 | upload: {
14 | type: Sequelize.DOUBLE,
15 | allowNull: false
16 | }
17 | }, {createdAt: false, updatedAt: false});
--------------------------------------------------------------------------------
/server/models/Speedtests.js:
--------------------------------------------------------------------------------
1 | const Sequelize = require('sequelize');
2 | const db = require("../config/database");
3 |
4 | module.exports = db.define("speedtests", {
5 | id: {
6 | type: Sequelize.INTEGER,
7 | primaryKey: true,
8 | autoIncrement: true,
9 | },
10 | serverId: {
11 | type: Sequelize.INTEGER,
12 | defaultValue: 0
13 | },
14 | ping: {
15 | type: Sequelize.INTEGER,
16 | allowNull: false
17 | },
18 | download: {
19 | type: Sequelize.DOUBLE,
20 | allowNull: false
21 | },
22 | upload: {
23 | type: Sequelize.DOUBLE,
24 | allowNull: false
25 | },
26 | error: {
27 | type: Sequelize.STRING,
28 | allowNull: true
29 | },
30 | type: {
31 | type: Sequelize.STRING,
32 | defaultValue: "auto"
33 | },
34 | resultId: {
35 | type: Sequelize.STRING,
36 | allowNull: true
37 | },
38 | time: {
39 | type: Sequelize.INTEGER,
40 | defaultValue: 0
41 | },
42 | created: {
43 | type: process.env.DB_TYPE === "mysql" ? Sequelize.STRING : Sequelize.TIME,
44 | defaultValue: Sequelize.NOW
45 | }
46 | }, {createdAt: false, updatedAt: false});
--------------------------------------------------------------------------------
/server/routes/config.js:
--------------------------------------------------------------------------------
1 | const app = require('express').Router();
2 | const config = require('../controller/config');
3 | const timer = require('../tasks/timer');
4 | const password = require('../middlewares/password');
5 |
6 | app.get("/", password(true), async (req, res) => {
7 | let configValues = {};
8 | (await config.listAll()).forEach(row => {
9 | if (row.key !== "password" && !(req.viewMode && ["ooklaId", "libreId", "cron", "passwordLevel"].includes(row.key)))
10 | configValues[row.key] = row.value;
11 | });
12 | configValues['viewMode'] = req.viewMode;
13 | configValues['previewMode'] = process.env.PREVIEW_MODE === "true";
14 |
15 | if (process.env.PREVIEW_MODE === "true")
16 | configValues['previewMessage'] = String(process.env.PREVIEW_MESSAGE || "The owner of this instance has not provided a message");
17 |
18 | if (Object.keys(configValues).length === 0) return res.status(404).json({message: "Hmm. There are no config values. Weird..."});
19 | res.json(configValues);
20 | });
21 |
22 | app.patch("/:key", password(false), async (req, res) => {
23 | const value = await config.validateInput(req.params.key, req.body?.value);
24 | if (Object.keys(value).length !== 1) return res.status(400).json({message: value});
25 |
26 | if (!await config.updateValue(req.params.key, value.value))
27 | return res.status(500).json({message: `Error updating the key '${req.params.key}'`});
28 |
29 | if (req.params.key === "cron") {
30 | timer.stopTimer();
31 | timer.startTimer(req.body.value.toString());
32 | }
33 |
34 | res.json({message: `The key '${req.params.key}' has been successfully updated`});
35 | });
36 |
37 | module.exports = app;
--------------------------------------------------------------------------------
/server/routes/opengraph.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const app = express.Router();
3 | const passwordWrapper = require('../middlewares/passwordWrapper');
4 | const generateOpenGraphImage = require("../controller/opengraph");
5 |
6 | app.get("/image", passwordWrapper(true, (req, res) => {
7 | // If there is a password set and the user does not want others to view their test data, return the project banner
8 | res.redirect('https://repository-images.githubusercontent.com/478222232/b5331514-aa27-4a56-af3e-c4b25446438d');
9 | }), async (req, res) => {
10 |
11 | try {
12 | const png = await generateOpenGraphImage(req);
13 |
14 | if (!png) {
15 | return res.status(500).json({ message: "Error fetching test data" });
16 | }
17 |
18 | res.setHeader("Content-Type", "image/png").status(200).send(png);
19 | } catch (error) {
20 | res.status(500).json({ message: error.message });
21 | }
22 | });
23 |
24 | module.exports = app;
25 |
--------------------------------------------------------------------------------
/server/routes/recommendations.js:
--------------------------------------------------------------------------------
1 | const app = require('express').Router();
2 | const recommendations = require('../controller/recommendations');
3 | const password = require('../middlewares/password');
4 |
5 | app.get("/", password(false), async (req, res) => {
6 | let currentRecommendations = await recommendations.getCurrent();
7 | if (currentRecommendations === null) return res.status(501).json({message: "There are no recommendations yet"});
8 |
9 | return res.json(currentRecommendations);
10 | });
11 |
12 | module.exports = app;
--------------------------------------------------------------------------------
/server/routes/system.js:
--------------------------------------------------------------------------------
1 | const app = require('express').Router();
2 | const version = require('../../package.json').version;
3 | const remote_url = "https://api.github.com/repos/gnmyt/myspeed/releases/latest";
4 | const axios = require('axios');
5 | const password = require('../middlewares/password');
6 | const serverController = require('../controller/servers');
7 | const interfaces = require('../util/loadInterfaces');
8 |
9 | app.get("/version", password(false), async (req, res) => {
10 | if (process.env.PREVIEW_MODE === "true") return res.json({local: version, remote: "0"});
11 |
12 | try {
13 | res.json({local: version, remote: ((await axios.get(remote_url)).data.tag_name).replace("v", "")});
14 | } catch (e) {
15 | res.json({local: version, remote: "0"});
16 | }
17 | });
18 |
19 | app.get("/server/:provider", password(false), (req, res) => {
20 | if (!["ookla", "libre"].includes(req.params.provider))
21 | return res.status(400).json({message: "Invalid provider"});
22 |
23 | res.json(serverController.getByMode(req.params.provider));
24 | });
25 |
26 | app.get("/interfaces", password(false), async (req, res) => {
27 | res.json(interfaces.interfaces);
28 | });
29 |
30 | module.exports = app;
--------------------------------------------------------------------------------
/server/tasks/integrations.js:
--------------------------------------------------------------------------------
1 | const schedule = require('node-schedule');
2 | const {triggerEvent} = require("../controller/integrations");
3 |
4 | let currentState = "ping";
5 | let job;
6 |
7 | module.exports.setState = (state = "ping") => {
8 | currentState = state;
9 | }
10 |
11 | module.exports.sendPing = async (type, message) => {
12 | await triggerEvent("minutePassed", {type, message});
13 | }
14 |
15 | module.exports.sendCurrent = async () => {
16 | if (currentState === "ping") await this.sendPing();
17 | }
18 |
19 | module.exports.sendError = async (error = "Unknown error") => {
20 | await triggerEvent("testFailed", error);
21 | }
22 |
23 | module.exports.sendRunning = async () => {
24 | await triggerEvent("testStarted");
25 | }
26 |
27 | module.exports.sendFinished = async (data) => {
28 | await triggerEvent("testFinished", data);
29 | }
30 |
31 | module.exports.startTimer = () => {
32 | job = schedule.scheduleJob('* * * * *', () => this.sendCurrent());
33 | }
34 |
35 | module.exports.stopTimer = () => {
36 | if (job !== undefined) {
37 | job.cancel();
38 | job = undefined;
39 | }
40 | }
--------------------------------------------------------------------------------
/server/tasks/timer.js:
--------------------------------------------------------------------------------
1 | const pauseController = require('../controller/pause');
2 | const schedule = require('node-schedule');
3 | const {isValidCron} = require("cron-validator");
4 |
5 | let job;
6 |
7 | module.exports.startTimer = (cron) => {
8 | if (!isValidCron(cron)) return;
9 | job = schedule.scheduleJob(cron, () => this.runTask());
10 | }
11 |
12 | module.exports.runTask = async () => {
13 | if (pauseController.currentState) {
14 | console.warn("Speedtests currently paused. Trying again later...");
15 | return;
16 | }
17 |
18 | await require('./speedtest').create("auto");
19 | }
20 |
21 | module.exports.stopTimer = () => {
22 | if (job !== undefined) {
23 | job.cancel();
24 | job = undefined;
25 | }
26 | }
27 |
28 | module.exports.job = job;
--------------------------------------------------------------------------------
/server/util/createFolders.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 |
3 | const neededFolder = ["data", "bin", "data/logs", "data/servers"];
4 |
5 | neededFolder.forEach(folder => {
6 | if (!fs.existsSync(folder)) {
7 | try {
8 | fs.mkdirSync(folder, {recursive: true});
9 | } catch (e) {
10 | console.error("Could not create the data folder. Please check the permission");
11 | process.exit(0);
12 | }
13 | }
14 | });
--------------------------------------------------------------------------------
/server/util/errorHandler.js:
--------------------------------------------------------------------------------
1 | const fs = require("fs");
2 | const filePath = process.cwd() + "/data/logs/error.log";
3 |
4 | module.exports = (error) => {
5 | const date = new Date().toLocaleString();
6 | const lineStarter = fs.existsSync(filePath) ? "\n\n" : "# Found a bug? Report it here: https://github.com/gnmyt/myspeed/issues\n\n";
7 |
8 | console.error("An error occurred: " + error.message);
9 |
10 | fs.writeFile(filePath, lineStarter + "## " + date + "\n" + error, {flag: 'a+'}, err => {
11 | if (err) console.error("Could not save error log file.", error);
12 |
13 | process.exit(1);
14 | });
15 | }
--------------------------------------------------------------------------------
/server/util/helpers.js:
--------------------------------------------------------------------------------
1 | module.exports.replaceVariables = (message, variables) => {
2 | for (const variable in variables)
3 | message = message.replace(`%${variable}%`, variables[variable]);
4 | return message;
5 | }
6 |
7 | module.exports.mapFixed = (entries, type) => ({
8 | min: Math.min(...entries.map((entry) => entry[type])),
9 | max: Math.max(...entries.map((entry) => entry[type])),
10 | avg: parseFloat((entries.reduce((a, b) => a + b[type], 0) / entries.length).toFixed(2))
11 | });
12 |
13 | module.exports.mapRounded = (entries, type) => ({
14 | min: Math.min(...entries.map((entry) => entry[type])),
15 | max: Math.max(...entries.map((entry) => entry[type])),
16 | avg: Math.round(entries.reduce((a, b) => a + b[type], 0) / entries.length)
17 | });
18 |
19 | module.exports.calculateTestAverages = (tests) => {
20 | let avgNumbers = {ping: 0, down: 0, up: 0, time: 0};
21 |
22 | tests.forEach((current) => {
23 | avgNumbers.ping += current.ping;
24 | avgNumbers.down += current.download;
25 | avgNumbers.up += current.upload;
26 | avgNumbers.time += current.time;
27 | });
28 |
29 | Object.keys(avgNumbers).forEach((key) => {
30 | avgNumbers[key] = avgNumbers[key] / tests.length;
31 | });
32 |
33 | return avgNumbers;
34 | }
--------------------------------------------------------------------------------
/server/util/loadCli.js:
--------------------------------------------------------------------------------
1 | const libreProvider = require('./providers/loadLibre');
2 | const ooklaProvider = require('./providers/loadOokla');
3 |
4 | module.exports.load = async () => {
5 | await libreProvider.load();
6 | await ooklaProvider.load();
7 | }
--------------------------------------------------------------------------------
/server/util/loadServers.js:
--------------------------------------------------------------------------------
1 | const axios = require('axios');
2 | const fs = require('fs');
3 |
4 | // Load servers from ookla
5 | if (!fs.existsSync("data/servers/ookla.json")) {
6 | let servers = {};
7 | try {
8 | axios.get("https://www.speedtest.net/api/js/servers?limit=20")
9 | .then(res => res.data)
10 | .then(data => {
11 | data?.forEach(row => {
12 | servers[row.id] = row.name + " (" + row.distance + "km)";
13 | });
14 |
15 | try {
16 | fs.writeFileSync("data/servers/ookla.json", JSON.stringify(servers, null, 4));
17 | } catch (e) {
18 | console.error("Could not save servers file");
19 | }
20 | });
21 | } catch (e) {
22 | console.error("Could not get servers");
23 | }
24 | }
25 |
26 | // Load servers from librespeed
27 | if (!fs.existsSync("data/servers/librespeed.json")) {
28 | let servers = {};
29 | try {
30 | axios.get("https://librespeed.org/backend-servers/servers.php")
31 | .then(res => res.data)
32 | .then(data => {
33 | data?.forEach(row => {
34 | servers[row.id] = row.name;
35 | });
36 | try {
37 | fs.writeFileSync("data/servers/librespeed.json", JSON.stringify(servers, null, 4));
38 | } catch (e) {
39 | console.error("Could not save servers file");
40 | }
41 | });
42 | } catch (e) {
43 | console.error("Could not get servers");
44 | }
45 | }
--------------------------------------------------------------------------------
/server/util/providers/loadLibre.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const {get} = require('https');
3 | const decompress = require("decompress");
4 | const {file} = require("tmp");
5 | const decompressTarGz = require('decompress-targz');
6 | const decompressUnzip = require('decompress-unzip');
7 | const binaries = require('../../config/binaries');
8 |
9 | const binaryRegex = /librespeed-cli(.exe)?$/;
10 | const binaryDirectory = __dirname + "/../../../bin/";
11 | const binaryPath = `${binaryDirectory}/librespeed-cli` + (process.platform === "win32" ? ".exe" : "");
12 |
13 | const downloadPath = `https://github.com/librespeed/speedtest-cli/releases/download/v${binaries.libreVersion}/librespeed-cli_${binaries.libreVersion}_`;
14 |
15 | module.exports.fileExists = async () => fs.existsSync(binaryPath);
16 |
17 | module.exports.downloadFile = async () => {
18 | const binary = binaries.libreList.find(b => b.os === process.platform && b.arch === process.arch);
19 |
20 | if (!binary)
21 | throw new Error(`Your platform (${process.platform}-${process.arch}) is not supported by the LibreSpeed CLI`);
22 |
23 | await new Promise((resolve) => {
24 | file({postfix: binary.suffix}, async (err, path) => {
25 | const location = await new Promise((resolve) => get(downloadPath + binary.suffix, (res) => {
26 | resolve(res.headers.location);
27 | }));
28 |
29 | get(location, async resp => {
30 | resp.pipe(fs.createWriteStream(path)).on('finish', async () => {
31 | await decompress(path, binaryDirectory, {
32 | plugins: [decompressTarGz(), decompressUnzip()],
33 | filter: file => binaryRegex.test(file.path),
34 | map: file => {
35 | file.path = "librespeed-cli" + (process.platform === "win32" ? ".exe" : "");
36 | return file;
37 | }
38 | });
39 | resolve();
40 | });
41 | });
42 | });
43 | });
44 | }
45 |
46 | module.exports.load = async () => {
47 | if (!await this.fileExists())
48 | await this.downloadFile();
49 | }
--------------------------------------------------------------------------------
/server/util/providers/loadOokla.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const {get} = require('https');
3 | const decompress = require("decompress");
4 | const {file} = require("tmp");
5 | const decompressTarGz = require('decompress-targz');
6 | const decompressUnzip = require('decompress-unzip');
7 | const binaries = require('../../config/binaries');
8 |
9 | const binaryRegex = /speedtest(.exe)?$/;
10 | const binaryDirectory = __dirname + "/../../../bin/";
11 | const binaryPath = `${binaryDirectory}/ookla` + (process.platform === "win32" ? ".exe" : "");
12 |
13 | const downloadPath = `https://install.speedtest.net/app/cli/ookla-speedtest-${binaries.ooklaVersion}-`;
14 |
15 | module.exports.fileExists = async () => fs.existsSync(binaryPath);
16 |
17 | module.exports.downloadFile = async () => {
18 | const binary = binaries.ooklaList.find(b => b.os === process.platform && b.arch === process.arch);
19 |
20 | if (!binary)
21 | throw new Error(`Your platform (${process.platform}-${process.arch}) is not supported by the Speedtest CLI`);
22 |
23 | await new Promise((resolve) => {
24 | file({postfix: binary.suffix}, async (err, path) => {
25 | get(downloadPath + binary.suffix, async resp => {
26 | resp.pipe(fs.createWriteStream(path)).on('finish', async () => {
27 | await decompress(path, binaryDirectory, {
28 | plugins: [decompressTarGz(), decompressUnzip()],
29 | filter: file => binaryRegex.test(file.path),
30 | map: file => {
31 | file.path = "speedtest" + (process.platform === "win32" ? ".exe" : "");
32 | return file;
33 | }
34 | });
35 | resolve();
36 | });
37 | });
38 | });
39 | });
40 | }
41 |
42 | module.exports.load = async () => {
43 | if (!await this.fileExists())
44 | await this.downloadFile();
45 | }
--------------------------------------------------------------------------------
/server/util/providers/parseData.js:
--------------------------------------------------------------------------------
1 | const roundSpeed = (bandwidth) => {
2 | return Math.round(bandwidth / 1250) / 100;
3 | }
4 |
5 | module.exports.parseOokla = (test) => {
6 | let ping = Math.round(test.ping.latency);
7 | let download = roundSpeed(test.download.bandwidth);
8 | let upload = roundSpeed(test.upload.bandwidth);
9 | let time = Math.round((test.download.elapsed + test.upload.elapsed) / 1000);
10 |
11 | return {ping, download, upload, time, resultId: test.result?.id};
12 | }
13 |
14 | module.exports.parseLibre = (test) => ({...test, ping: Math.round(test.ping), time: Math.round(test.elapsed / 1000),
15 | resultId: null});
16 |
17 | module.exports.parseCloudflare = (test) => ({...test, time: Math.round(test.elapsed), resultId: null});
18 |
19 | module.exports.parseData = (provider, data) => {
20 | switch (provider) {
21 | case "ookla":
22 | return this.parseOokla(data);
23 | case "libre":
24 | return this.parseLibre(data);
25 | case "cloudflare":
26 | return this.parseCloudflare(data);
27 | default:
28 | throw {message: "Invalid provider"};
29 | }
30 | }
--------------------------------------------------------------------------------
/web/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: { browser: true, es2020: true },
4 | extends: [
5 | 'eslint:recommended',
6 | 'plugin:react/recommended',
7 | 'plugin:react/jsx-runtime',
8 | 'plugin:react-hooks/recommended',
9 | ],
10 | ignorePatterns: ['dist', '.eslintrc.cjs'],
11 | parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
12 | settings: { react: { version: '18.2' } },
13 | plugins: ['react-refresh'],
14 | rules: {
15 | 'react/jsx-no-target-blank': 'off',
16 | 'react-refresh/only-export-components': [
17 | 'warn',
18 | { allowConstantExport: true },
19 | ],
20 | },
21 | }
22 |
--------------------------------------------------------------------------------
/web/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/web/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | MySpeed - Automated Speedtests
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/web/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "paths": {
5 | "@/*": ["./src/*"]
6 | }
7 | }
8 | }
--------------------------------------------------------------------------------
/web/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "website",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "vite build",
9 | "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
10 | "preview": "vite preview"
11 | },
12 | "dependencies": {
13 | "@fortawesome/fontawesome-svg-core": "^6.5.1",
14 | "@fortawesome/free-brands-svg-icons": "^6.5.1",
15 | "@fortawesome/free-solid-svg-icons": "^6.5.1",
16 | "@fortawesome/react-fontawesome": "^0.2.0",
17 | "react": "^18.2.0",
18 | "react-dom": "^18.2.0",
19 | "react-router-dom": "^7.5.2",
20 | "sass": "^1.71.1"
21 | },
22 | "devDependencies": {
23 | "@types/react": "^18.2.56",
24 | "@types/react-dom": "^18.2.19",
25 | "@vitejs/plugin-react": "^4.2.1",
26 | "eslint": "^8.56.0",
27 | "eslint-plugin-react": "^7.33.2",
28 | "eslint-plugin-react-hooks": "^4.6.0",
29 | "eslint-plugin-react-refresh": "^0.4.5",
30 | "vite": "^6.2.7"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/web/public/assets/fonts/inter-v12-latin-300.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnmyt/MySpeed/5b41668ee5ecb3bde8898e32e5dacba0e523ef03/web/public/assets/fonts/inter-v12-latin-300.ttf
--------------------------------------------------------------------------------
/web/public/assets/fonts/inter-v12-latin-300.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnmyt/MySpeed/5b41668ee5ecb3bde8898e32e5dacba0e523ef03/web/public/assets/fonts/inter-v12-latin-300.woff2
--------------------------------------------------------------------------------
/web/public/assets/fonts/inter-v12-latin-500.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnmyt/MySpeed/5b41668ee5ecb3bde8898e32e5dacba0e523ef03/web/public/assets/fonts/inter-v12-latin-500.ttf
--------------------------------------------------------------------------------
/web/public/assets/fonts/inter-v12-latin-500.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnmyt/MySpeed/5b41668ee5ecb3bde8898e32e5dacba0e523ef03/web/public/assets/fonts/inter-v12-latin-500.woff2
--------------------------------------------------------------------------------
/web/public/assets/fonts/inter-v12-latin-700.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnmyt/MySpeed/5b41668ee5ecb3bde8898e32e5dacba0e523ef03/web/public/assets/fonts/inter-v12-latin-700.ttf
--------------------------------------------------------------------------------
/web/public/assets/fonts/inter-v12-latin-700.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnmyt/MySpeed/5b41668ee5ecb3bde8898e32e5dacba0e523ef03/web/public/assets/fonts/inter-v12-latin-700.woff2
--------------------------------------------------------------------------------
/web/public/assets/fonts/inter-v12-latin-900.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnmyt/MySpeed/5b41668ee5ecb3bde8898e32e5dacba0e523ef03/web/public/assets/fonts/inter-v12-latin-900.ttf
--------------------------------------------------------------------------------
/web/public/assets/fonts/inter-v12-latin-900.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnmyt/MySpeed/5b41668ee5ecb3bde8898e32e5dacba0e523ef03/web/public/assets/fonts/inter-v12-latin-900.woff2
--------------------------------------------------------------------------------
/web/public/assets/fonts/inter-v12-latin-regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnmyt/MySpeed/5b41668ee5ecb3bde8898e32e5dacba0e523ef03/web/public/assets/fonts/inter-v12-latin-regular.ttf
--------------------------------------------------------------------------------
/web/public/assets/fonts/inter-v12-latin-regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnmyt/MySpeed/5b41668ee5ecb3bde8898e32e5dacba0e523ef03/web/public/assets/fonts/inter-v12-latin-regular.woff2
--------------------------------------------------------------------------------
/web/public/assets/img/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnmyt/MySpeed/5b41668ee5ecb3bde8898e32e5dacba0e523ef03/web/public/assets/img/favicon.ico
--------------------------------------------------------------------------------
/web/public/assets/img/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnmyt/MySpeed/5b41668ee5ecb3bde8898e32e5dacba0e523ef03/web/public/assets/img/logo.png
--------------------------------------------------------------------------------
/web/public/assets/img/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnmyt/MySpeed/5b41668ee5ecb3bde8898e32e5dacba0e523ef03/web/public/assets/img/logo192.png
--------------------------------------------------------------------------------
/web/public/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Disallow:
--------------------------------------------------------------------------------
/web/src/common/assets/background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnmyt/MySpeed/5b41668ee5ecb3bde8898e32e5dacba0e523ef03/web/src/common/assets/background.png
--------------------------------------------------------------------------------
/web/src/common/assets/feature/cron.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnmyt/MySpeed/5b41668ee5ecb3bde8898e32e5dacba0e523ef03/web/src/common/assets/feature/cron.png
--------------------------------------------------------------------------------
/web/src/common/assets/feature/integrations.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnmyt/MySpeed/5b41668ee5ecb3bde8898e32e5dacba0e523ef03/web/src/common/assets/feature/integrations.png
--------------------------------------------------------------------------------
/web/src/common/assets/feature/language.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnmyt/MySpeed/5b41668ee5ecb3bde8898e32e5dacba0e523ef03/web/src/common/assets/feature/language.png
--------------------------------------------------------------------------------
/web/src/common/assets/feature/views.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnmyt/MySpeed/5b41668ee5ecb3bde8898e32e5dacba0e523ef03/web/src/common/assets/feature/views.png
--------------------------------------------------------------------------------
/web/src/common/assets/interface.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnmyt/MySpeed/5b41668ee5ecb3bde8898e32e5dacba0e523ef03/web/src/common/assets/interface.png
--------------------------------------------------------------------------------
/web/src/common/assets/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnmyt/MySpeed/5b41668ee5ecb3bde8898e32e5dacba0e523ef03/web/src/common/assets/logo192.png
--------------------------------------------------------------------------------
/web/src/common/assets/logo_docs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnmyt/MySpeed/5b41668ee5ecb3bde8898e32e5dacba0e523ef03/web/src/common/assets/logo_docs.png
--------------------------------------------------------------------------------
/web/src/common/assets/sc1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnmyt/MySpeed/5b41668ee5ecb3bde8898e32e5dacba0e523ef03/web/src/common/assets/sc1.png
--------------------------------------------------------------------------------
/web/src/common/assets/sc2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnmyt/MySpeed/5b41668ee5ecb3bde8898e32e5dacba0e523ef03/web/src/common/assets/sc2.png
--------------------------------------------------------------------------------
/web/src/common/components/Button/Button.jsx:
--------------------------------------------------------------------------------
1 | import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
2 | import "./styles.sass";
3 |
4 | export const Button = ({color, text, icon, onClick, disabled}) => {
5 | return (
6 |
10 | );
11 | }
--------------------------------------------------------------------------------
/web/src/common/components/Button/index.js:
--------------------------------------------------------------------------------
1 | export {Button as default} from "./Button.jsx";
--------------------------------------------------------------------------------
/web/src/common/components/Button/styles.sass:
--------------------------------------------------------------------------------
1 | @import "@/common/styles/colors"
2 |
3 | .btn
4 | padding: 0.5rem 1rem
5 | border-radius: 0.8rem
6 | border: 1px solid transparent
7 | display: flex
8 | align-items: center
9 | transition: all 0.2s ease-in-out
10 | cursor: pointer
11 |
12 | .btn:hover
13 | transform: translateY(-2px)
14 | filter: brightness(1.2)
15 |
16 | .btn:active
17 | transform: translateY(0) scale(0.98)
18 |
19 | .btn svg
20 | margin-right: 10px
21 | font-size: 18pt
22 |
23 | .btn h1
24 | margin: 0
25 | font-weight: 900
26 |
27 | .btn-color-red
28 | background-color: rgba($red, 0.1)
29 | color: $red
30 | border: 1px solid $red
31 |
32 | .btn-color-green
33 | background-color: rgba($primary, 0.1)
34 | color: $primary
35 | border: 1px solid $primary
36 |
37 | .btn-color-blue
38 | background-color: rgba($blue, 0.1)
39 | color: $blue
40 | border: 1px solid $blue
--------------------------------------------------------------------------------
/web/src/common/components/Navigation/index.js:
--------------------------------------------------------------------------------
1 | export {Navigation as default} from "./Navigation.jsx";
--------------------------------------------------------------------------------
/web/src/common/layouts/Root/Root.jsx:
--------------------------------------------------------------------------------
1 | import {Outlet} from "react-router-dom";
2 | import Navigation from "@/common/components/Navigation";
3 | import "./styles.sass";
4 | import {useEffect} from "react";
5 |
6 | export const Root = () => {
7 |
8 | useEffect(() => {
9 | const handleKeydown = (event) => {
10 | if (event.ctrlKey && (event.key === "+" || event.key === "-")) event.preventDefault();
11 | }
12 |
13 | const handleWheel = (event) => {
14 | if (event.ctrlKey) event.preventDefault();
15 | }
16 |
17 | document.addEventListener("keydown", handleKeydown);
18 | document.addEventListener("wheel", handleWheel, {passive: false});
19 |
20 | return () => {
21 | document.removeEventListener("keydown", handleKeydown);
22 | document.removeEventListener("wheel", handleWheel);
23 | }
24 | }, []);
25 |
26 | return (
27 | <>
28 |
29 |
30 |
31 |
32 | >
33 | );
34 | }
--------------------------------------------------------------------------------
/web/src/common/layouts/Root/index.js:
--------------------------------------------------------------------------------
1 | export {Root as default} from "./Root.jsx";
--------------------------------------------------------------------------------
/web/src/common/layouts/Root/styles.sass:
--------------------------------------------------------------------------------
1 | main
2 | padding: 1rem
3 | height: calc(100vh - 7rem)
4 |
5 | @media screen and (max-width: 1000px)
6 | main
7 | padding: 0.2rem
--------------------------------------------------------------------------------
/web/src/common/styles/_colors.sass:
--------------------------------------------------------------------------------
1 | $background: #131A20
2 | $background-lighter: #1E222F
3 | $text: #E5E5E5
4 | $primary: #45C65A
5 | $blue: #456AC6
6 | $red: #EC5555
--------------------------------------------------------------------------------
/web/src/common/styles/default.sass:
--------------------------------------------------------------------------------
1 | @import "colors"
2 |
3 | body, html
4 | margin: 0
5 | background-color: $background
6 | color: $text
7 | font-family: "Inter", sans-serif
8 | font-weight: 700
9 |
10 | ::-webkit-scrollbar
11 | width: 13px
12 |
13 | ::-webkit-scrollbar-thumb
14 | filter: brightness(0.7)
15 | border-radius: 10px
16 |
17 | ::-webkit-scrollbar-thumb:hover
18 | filter: brightness(0.8)
--------------------------------------------------------------------------------
/web/src/common/styles/fonts.sass:
--------------------------------------------------------------------------------
1 | @font-face
2 | font-display: swap
3 | font-family: 'Inter'
4 | font-style: normal
5 | font-weight: 300
6 | src: url('/assets/fonts/inter-v12-latin-300.woff2') format("woff2"), url('/assets/fonts/inter-v12-latin-300.ttf') format("truetype")
7 |
8 | @font-face
9 | font-display: swap
10 | font-family: 'Inter'
11 | font-style: normal
12 | font-weight: 400
13 | src: url('/assets/fonts/inter-v12-latin-regular.woff2') format("woff2"), url('/assets/fonts/inter-v12-latin-regular.ttf') format("truetype")
14 |
15 | @font-face
16 | font-display: swap
17 | font-family: 'Inter'
18 | font-style: normal
19 | font-weight: 500
20 | src: url('/assets/fonts/inter-v12-latin-500.woff2') format("woff2"), url('/assets/fonts/inter-v12-latin-500.ttf') format("truetype")
21 |
22 | @font-face
23 | font-display: swap
24 | font-family: 'Inter'
25 | font-style: normal
26 | font-weight: 700
27 | src: url('/assets/fonts/inter-v12-latin-700.woff2') format("woff2"), url('/assets/fonts/inter-v12-latin-700.ttf') format("truetype")
28 |
29 | @font-face
30 | font-display: swap
31 | font-family: 'Inter'
32 | font-style: normal
33 | font-weight: 900
34 | src: url('/assets/fonts/inter-v12-latin-900.woff2') format("woff2"), url('/assets/fonts/inter-v12-latin-900.ttf') format("truetype")
--------------------------------------------------------------------------------
/web/src/main.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom/client";
3 | import "@/common/styles/fonts.sass";
4 | import "@/common/styles/default.sass";
5 | import {createBrowserRouter, RouterProvider} from "react-router-dom";
6 | import Root from "@/common/layouts/Root";
7 | import NotFound from "@/pages/NotFound";
8 | import Home from "@/pages/Home";
9 | import Imprint from "@/pages/Imprint";
10 | import Privacy from "@/pages/Privacy";
11 | import Install from "@/pages/Install";
12 | import Tutorials from "@/pages/Tutorials";
13 | import TutorialSubmission from "@/pages/TutorialSubmission";
14 |
15 | const router = createBrowserRouter([
16 | {
17 | path: "/",
18 | element: ,
19 | errorElement: ,
20 | children: [
21 | {path: "/", element: },
22 | {path: "/install", element: },
23 | {path: "/tutorials", element: },
24 | {path: "/tutorials/submit", element: },
25 | {path: "/imprint", element: },
26 | {path: "/privacy", element: },
27 | ]
28 | }
29 | ]);
30 |
31 | export const DOCUMENTATION_BASE = "https://docs.myspeed.dev";
32 |
33 | ReactDOM.createRoot(document.getElementById("root")).render(
34 |
35 |
36 | ,
37 | );
--------------------------------------------------------------------------------
/web/src/pages/Home/Home.jsx:
--------------------------------------------------------------------------------
1 | import InterfaceImage from "@/common/assets/interface.png";
2 | import Logo from "@/common/assets/logo192.png";
3 | import "./styles.sass";
4 | import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
5 | import {faArrowDown, faArrowUp} from "@fortawesome/free-solid-svg-icons";
6 | import Features from "@/pages/Home/components/Features";
7 | import FeatureGrid from "@/pages/Home/components/FeatureGrid";
8 | import GetStarted from "@/pages/Home/components/GetStarted";
9 | import Footer from "@/pages/Home/components/Footer";
10 |
11 |
12 | const COLORS = ["#45C65A", "#E58A00", "#EC5555"];
13 |
14 | const chooseRandomColor = () => COLORS[Math.floor(Math.random() * COLORS.length)];
15 |
16 | export const Home = () => {
17 | return (
18 |
19 |
20 |
21 |

22 |
23 |
MySpeed
24 | Speedtest automation made simple
25 |
26 |
27 |
28 | {Array(5).fill(0).map((_, index) => (
29 |
30 |
31 |
32 |
33 | ))}
34 |
35 |
36 |

37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | )
48 | }
--------------------------------------------------------------------------------
/web/src/pages/Home/components/FeatureGrid/FeatureGrid.jsx:
--------------------------------------------------------------------------------
1 | import "./styles.sass";
2 |
3 | import LanguageFeature from "@/common/assets/feature/language.png";
4 | import ViewsFeature from "@/common/assets/feature/views.png";
5 | import IntegrationsFeature from "@/common/assets/feature/integrations.png";
6 | import CronFeature from "@/common/assets/feature/cron.png";
7 |
8 | export const TRANSLATIONS_LINK = "https://crowdin.com/project/myspeed";
9 |
10 | export const FeatureGrid = () => {
11 | return (
12 |
13 |
14 |
15 |

16 |
Multilingual
17 |
MySpeed supports English, German and more languages.
18 |
19 |
20 |

21 |
Multiple Views
22 |
Choose between different views to display your data.
23 |
24 |
25 |
26 |
27 |

28 |
Extensible
29 |
You can integrate MySpeed in WhatsApp, Discord, HealthChecks or your own Webhook
30 |
31 |
32 |

33 |
Choose your time
34 |
Whether you want to check every 5 minutes or every 5 hours, MySpeed has you covered.
35 |
36 |
37 |
38 | )
39 | }
--------------------------------------------------------------------------------
/web/src/pages/Home/components/FeatureGrid/index.js:
--------------------------------------------------------------------------------
1 | export {FeatureGrid as default} from "./FeatureGrid.jsx";
--------------------------------------------------------------------------------
/web/src/pages/Home/components/FeatureGrid/styles.sass:
--------------------------------------------------------------------------------
1 | @import "@/common/styles/colors"
2 |
3 | .feature-grid-section
4 | margin-top: 5rem
5 |
6 | .feature-grid-row
7 | display: flex
8 | align-items: center
9 | justify-content: center
10 |
11 | .feature-box
12 | display: flex
13 | flex-direction: column
14 | align-items: center
15 | width: 35rem
16 |
17 | img
18 | width: 20rem
19 |
20 | h1
21 | margin-top: 2rem
22 | font-size: 2rem
23 |
24 | p
25 | margin-top: 1rem
26 | font-size: 1.3rem
27 | width: 25rem
28 | text-align: center
29 |
30 | a
31 | color: $primary
32 |
33 | @media screen and (max-width: 800px)
34 | .feature-grid-row
35 | flex-direction: column
36 | gap: 2rem
--------------------------------------------------------------------------------
/web/src/pages/Home/components/Features/Features.jsx:
--------------------------------------------------------------------------------
1 | import "./styles.sass";
2 | import Screenshot1 from "@/common/assets/sc1.png";
3 | import Screenshot2 from "@/common/assets/sc2.png";
4 |
5 |
6 | export const Features = () => {
7 | return (
8 |
9 |
10 |

11 |
12 |
What is MySpeed?
13 |
MySpeed is a Software that helps you keeping track of your network speed.
It
14 | automatically creates speedtests based on your schedule and displays them in a list.
15 |
16 |
17 |
18 |
19 |

20 |
21 |
Detailed statistics
22 |
MySpeed provides you with detailed statistics about your network speed.
This
23 | includes data about your download and upload speed, as well as your ping.
24 |
25 |
26 |
27 |
28 | )
29 | }
--------------------------------------------------------------------------------
/web/src/pages/Home/components/Features/index.js:
--------------------------------------------------------------------------------
1 | export {Features as default} from "./Features.jsx";
--------------------------------------------------------------------------------
/web/src/pages/Home/components/Features/styles.sass:
--------------------------------------------------------------------------------
1 | @import "@/common/styles/colors"
2 |
3 | .feature-area
4 | margin-top: 10vh
5 | padding-left: 5rem
6 | padding-right: 5rem
7 | display: flex
8 | flex-direction: column
9 | gap: 3rem
10 |
11 | .feature
12 | display: flex
13 | gap: 2rem
14 | justify-content: space-around
15 | align-items: center
16 |
17 | img
18 | width: 40rem
19 | border-radius: 1rem
20 | transition: 0.3s
21 |
22 | img:hover
23 | transform: scale(1.02) translateY(-0.5rem)
24 | box-shadow: 0 0 1rem 0.5rem $background-lighter
25 |
26 | h1
27 | font-size: 3rem
28 | margin-bottom: 1rem
29 |
30 | p
31 | font-size: 1.6rem
32 | margin-bottom: 2rem
33 | max-width: 40rem
34 |
35 | .feature-reverse
36 | flex-direction: row-reverse
37 |
38 | @media screen and (max-width: 1200px)
39 | .feature-area .feature img
40 | width: 30rem
41 | .feature-area .feature h1
42 | font-size: 2.5rem
43 | .feature-area .feature p
44 | font-size: 1.3rem
45 | max-width: 30rem
46 |
47 | @media screen and (max-width: 1000px)
48 | .feature-area .feature img
49 | width: 25rem
50 | .feature-area .feature h1
51 | font-size: 2rem
52 | .feature-area .feature p
53 | font-size: 1rem
54 | max-width: 30rem
55 |
56 | @media screen and (max-width: 700px)
57 | .feature-area .feature
58 | width: 100%
59 | .feature-area .feature img
60 | width: 70vw
61 | .feature-area .feature
62 | flex-direction: column
63 | .feature-area .feature p
64 | margin-bottom: 1rem
65 | max-width: 100%
--------------------------------------------------------------------------------
/web/src/pages/Home/components/Footer/Footer.jsx:
--------------------------------------------------------------------------------
1 | import {Link} from "react-router-dom";
2 | import "./styles.sass";
3 |
4 | export const Footer = () => {
5 | return (
6 |
7 |
© {new Date().getFullYear()} Mathias Wagner
8 |
9 |
10 | Imprint
11 | Privacy Policy (GER)
12 |
13 |
14 | )
15 | }
--------------------------------------------------------------------------------
/web/src/pages/Home/components/Footer/index.js:
--------------------------------------------------------------------------------
1 | export {Footer as default} from "./Footer.jsx";
--------------------------------------------------------------------------------
/web/src/pages/Home/components/Footer/styles.sass:
--------------------------------------------------------------------------------
1 | @import "@/common/styles/colors"
2 |
3 | .footer
4 | background-color: $background-lighter
5 | margin: 0 5rem 0 5rem
6 | padding: 0 5rem
7 | display: flex
8 | align-items: center
9 | justify-content: space-between
10 |
11 | .legal-nav
12 | display: flex
13 | gap: 1rem
14 |
15 | a
16 | color: $text
17 | text-decoration: none
18 |
19 | a:hover
20 | text-decoration: underline
21 |
22 |
23 | @media screen and (max-width: 710px)
24 | .footer
25 | margin: 0 0 0 1rem
26 | padding: 0 2rem
27 | flex-direction: column
--------------------------------------------------------------------------------
/web/src/pages/Home/components/GetStarted/GetStarted.jsx:
--------------------------------------------------------------------------------
1 | import "./styles.sass";
2 |
3 | import DocsLogo from "@/common/assets/logo_docs.png";
4 | import Button from "@/common/components/Button/index.js";
5 | import {faPlay} from "@fortawesome/free-solid-svg-icons";
6 | import {Link, useNavigate} from "react-router-dom";
7 | import {DOCUMENTATION_BASE} from "@/main.jsx";
8 |
9 | export const GetStarted = () => {
10 | const navigate = useNavigate();
11 | return (
12 |
13 |
14 |
Convinced?
15 | Great! Press the button below to get started.
16 |
19 |
20 |

21 |
22 | );
23 | }
--------------------------------------------------------------------------------
/web/src/pages/Home/components/GetStarted/index.js:
--------------------------------------------------------------------------------
1 | export {GetStarted as default} from "./GetStarted.jsx";
--------------------------------------------------------------------------------
/web/src/pages/Home/components/GetStarted/styles.sass:
--------------------------------------------------------------------------------
1 | @import "@/common/styles/colors"
2 |
3 | .get-started
4 | display: flex
5 | align-items: center
6 | justify-content: space-between
7 | background-color: $background-lighter
8 | border-top-left-radius: 5rem
9 | border-top-right-radius: 5rem
10 | padding: 2rem 5rem 1rem 5rem
11 | margin: 2rem 5rem 0 5rem
12 |
13 |
14 | h1
15 | font-size: 2.4rem
16 | margin: 0
17 |
18 | h2
19 | font-size: 1.5rem
20 | margin: 1rem 0
21 |
22 | .btn
23 | h1
24 | font-size: 1.3rem
25 |
26 | img
27 | width: 5rem
28 |
29 | @media screen and (max-width: 710px)
30 | .get-started
31 | padding: 2rem 2rem 1rem 2rem
32 | margin: 2rem 0 0 1rem
33 |
34 | h1
35 | font-size: 2rem
36 |
37 | h2
38 | font-size: 1.3rem
39 |
40 | h1
41 | font-size: 1.1rem
42 |
43 | img
44 | display: none
--------------------------------------------------------------------------------
/web/src/pages/Home/index.js:
--------------------------------------------------------------------------------
1 | export {Home as default} from "./Home.jsx";
--------------------------------------------------------------------------------
/web/src/pages/Imprint/Imprint.jsx:
--------------------------------------------------------------------------------
1 | import "./styles.sass";
2 |
3 | export const Imprint = () => {
4 | return (
5 |
6 |
Imprint
7 |
Information in accordance with § 5 TMG
8 |
9 |
Mathias Wagner
c/o IP-Management #14358
Ludwig-Erhard-Str. 18
20459 Hamburg
10 |
11 |
Contact
12 |
E-Mail: mathias@gnmyt.dev
13 |
14 | )
15 | }
--------------------------------------------------------------------------------
/web/src/pages/Imprint/index.js:
--------------------------------------------------------------------------------
1 | export {Imprint as default} from "./Imprint.jsx";
--------------------------------------------------------------------------------
/web/src/pages/Imprint/styles.sass:
--------------------------------------------------------------------------------
1 | @import "@/common/styles/colors"
2 |
3 | .imprint-page
4 | padding: 5rem
5 |
6 | h1
7 | font-size: 3rem
8 | margin: 0 0 2rem
9 | color: $primary
10 |
11 | h2
12 | font-size: 1.6rem
13 | margin-bottom: 1rem
14 | color: $primary
15 | filter: brightness(0.9)
--------------------------------------------------------------------------------
/web/src/pages/Install/index.js:
--------------------------------------------------------------------------------
1 | export {Install as default} from "./Install.jsx";
--------------------------------------------------------------------------------
/web/src/pages/NotFound/NotFound.jsx:
--------------------------------------------------------------------------------
1 | import {useEffect} from "react";
2 | import "./styles.sass";
3 | import NotFoundImage from "@/common/assets/not_found.svg";
4 | import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
5 | import {faQuestionCircle} from "@fortawesome/free-solid-svg-icons";
6 |
7 | export const NotFound = () => {
8 |
9 | useEffect(() => {
10 | const timeout = setTimeout(() => {
11 | window.location.href = "/";
12 | }, 2000);
13 |
14 | return () => clearTimeout(timeout);
15 | }, []);
16 |
17 | return (
18 |
19 |
20 |
21 |
22 |
Page not found
23 |
24 |
You will be redirected to the home page in a few seconds.
25 |
26 |

27 |
28 | );
29 | }
--------------------------------------------------------------------------------
/web/src/pages/NotFound/index.js:
--------------------------------------------------------------------------------
1 | export {NotFound as default} from "./NotFound.jsx";
--------------------------------------------------------------------------------
/web/src/pages/NotFound/styles.sass:
--------------------------------------------------------------------------------
1 | .page-404
2 | display: flex
3 | height: 100vh
4 | justify-content: space-around
5 | align-items: center
6 | flex-wrap: wrap
7 |
8 | .logo-container
9 | display: flex
10 | align-items: center
11 |
12 | h1
13 | margin: 1rem
14 |
15 | img
16 | width: 30rem
17 |
18 | @media screen and (max-width: 768px)
19 | .page-404
20 | padding: 2rem
21 | gap: 2rem
22 |
23 | .page-404 img
24 | width: 60vw
--------------------------------------------------------------------------------
/web/src/pages/Privacy/index.js:
--------------------------------------------------------------------------------
1 | export {Privacy as default} from "./Privacy.jsx";
--------------------------------------------------------------------------------
/web/src/pages/Privacy/styles.sass:
--------------------------------------------------------------------------------
1 | @import "@/common/styles/colors"
2 |
3 | .privacy-page
4 | padding: 5rem
5 |
6 | h1
7 | font-size: 3rem
8 | margin: 0 0 2rem
9 | color: $primary
10 |
11 | h2
12 | font-size: 1.6rem
13 | margin-bottom: 1rem
14 | color: $primary
15 | filter: brightness(0.9)
16 |
17 | h3
18 | font-size: 1.3rem
19 | margin-bottom: 1rem
20 | color: $primary
21 | filter: brightness(0.8)
22 |
23 | h4
24 | font-size: 1.1rem
25 | margin-bottom: 1rem
26 |
27 | a
28 | color: $primary
29 | text-decoration: underline
30 | transition: color 0.2s
31 |
32 | &:hover
33 | color: darken($primary, 10%)
34 |
35 | @media screen and (max-width: 1000px)
36 | .privacy-page
37 | padding: 5rem 1rem
38 |
39 | h1
40 | font-size: 2rem
41 | h2
42 | font-size: 1.4rem
43 | h3
44 | font-size: 1.1rem
45 | h4
46 | font-size: 1rem
--------------------------------------------------------------------------------
/web/src/pages/TutorialSubmission/index.js:
--------------------------------------------------------------------------------
1 | export {TutorialSubmission as default} from "./TutorialSubmission.jsx";
--------------------------------------------------------------------------------
/web/src/pages/Tutorials/index.js:
--------------------------------------------------------------------------------
1 | export {Tutorials as default} from "./Tutorials.jsx";
--------------------------------------------------------------------------------
/web/src/pages/Tutorials/sources/channels/addrom.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnmyt/MySpeed/5b41668ee5ecb3bde8898e32e5dacba0e523ef03/web/src/pages/Tutorials/sources/channels/addrom.webp
--------------------------------------------------------------------------------
/web/src/pages/Tutorials/sources/channels/belginux.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnmyt/MySpeed/5b41668ee5ecb3bde8898e32e5dacba0e523ef03/web/src/pages/Tutorials/sources/channels/belginux.webp
--------------------------------------------------------------------------------
/web/src/pages/Tutorials/sources/channels/bigbeartechworld.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnmyt/MySpeed/5b41668ee5ecb3bde8898e32e5dacba0e523ef03/web/src/pages/Tutorials/sources/channels/bigbeartechworld.png
--------------------------------------------------------------------------------
/web/src/pages/Tutorials/sources/channels/dbtech.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnmyt/MySpeed/5b41668ee5ecb3bde8898e32e5dacba0e523ef03/web/src/pages/Tutorials/sources/channels/dbtech.webp
--------------------------------------------------------------------------------
/web/src/pages/Tutorials/sources/channels/gigazine.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnmyt/MySpeed/5b41668ee5ecb3bde8898e32e5dacba0e523ef03/web/src/pages/Tutorials/sources/channels/gigazine.png
--------------------------------------------------------------------------------
/web/src/pages/Tutorials/sources/channels/linuxiac.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnmyt/MySpeed/5b41668ee5ecb3bde8898e32e5dacba0e523ef03/web/src/pages/Tutorials/sources/channels/linuxiac.webp
--------------------------------------------------------------------------------
/web/src/pages/Tutorials/sources/channels/mariushosting.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnmyt/MySpeed/5b41668ee5ecb3bde8898e32e5dacba0e523ef03/web/src/pages/Tutorials/sources/channels/mariushosting.png
--------------------------------------------------------------------------------
/web/src/pages/Tutorials/sources/channels/pavl21.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnmyt/MySpeed/5b41668ee5ecb3bde8898e32e5dacba0e523ef03/web/src/pages/Tutorials/sources/channels/pavl21.png
--------------------------------------------------------------------------------
/web/src/pages/Tutorials/sources/channels/retromiketech.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnmyt/MySpeed/5b41668ee5ecb3bde8898e32e5dacba0e523ef03/web/src/pages/Tutorials/sources/channels/retromiketech.png
--------------------------------------------------------------------------------
/web/src/pages/Tutorials/sources/channels/ubunlog.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnmyt/MySpeed/5b41668ee5ecb3bde8898e32e5dacba0e523ef03/web/src/pages/Tutorials/sources/channels/ubunlog.png
--------------------------------------------------------------------------------
/web/src/pages/Tutorials/sources/thumbs/20240128.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnmyt/MySpeed/5b41668ee5ecb3bde8898e32e5dacba0e523ef03/web/src/pages/Tutorials/sources/thumbs/20240128.webp
--------------------------------------------------------------------------------
/web/src/pages/Tutorials/sources/thumbs/7108075382452079878.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnmyt/MySpeed/5b41668ee5ecb3bde8898e32e5dacba0e523ef03/web/src/pages/Tutorials/sources/thumbs/7108075382452079878.webp
--------------------------------------------------------------------------------
/web/src/pages/Tutorials/sources/thumbs/7roj87Fytz0.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnmyt/MySpeed/5b41668ee5ecb3bde8898e32e5dacba0e523ef03/web/src/pages/Tutorials/sources/thumbs/7roj87Fytz0.webp
--------------------------------------------------------------------------------
/web/src/pages/Tutorials/sources/thumbs/Iic14oUCCVo.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnmyt/MySpeed/5b41668ee5ecb3bde8898e32e5dacba0e523ef03/web/src/pages/Tutorials/sources/thumbs/Iic14oUCCVo.webp
--------------------------------------------------------------------------------
/web/src/pages/Tutorials/sources/thumbs/MFbeWdKesTE.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnmyt/MySpeed/5b41668ee5ecb3bde8898e32e5dacba0e523ef03/web/src/pages/Tutorials/sources/thumbs/MFbeWdKesTE.webp
--------------------------------------------------------------------------------
/web/src/pages/Tutorials/sources/thumbs/SM3RJRktwIk.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnmyt/MySpeed/5b41668ee5ecb3bde8898e32e5dacba0e523ef03/web/src/pages/Tutorials/sources/thumbs/SM3RJRktwIk.webp
--------------------------------------------------------------------------------
/web/src/pages/Tutorials/sources/thumbs/ZIIa6yF-Tvo.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnmyt/MySpeed/5b41668ee5ecb3bde8898e32e5dacba0e523ef03/web/src/pages/Tutorials/sources/thumbs/ZIIa6yF-Tvo.webp
--------------------------------------------------------------------------------
/web/src/pages/Tutorials/sources/thumbs/addrom-myspeed.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnmyt/MySpeed/5b41668ee5ecb3bde8898e32e5dacba0e523ef03/web/src/pages/Tutorials/sources/thumbs/addrom-myspeed.webp
--------------------------------------------------------------------------------
/web/src/pages/Tutorials/sources/thumbs/belginux-myspeed.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnmyt/MySpeed/5b41668ee5ecb3bde8898e32e5dacba0e523ef03/web/src/pages/Tutorials/sources/thumbs/belginux-myspeed.webp
--------------------------------------------------------------------------------
/web/src/pages/Tutorials/sources/thumbs/linuxiac-myspeed.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnmyt/MySpeed/5b41668ee5ecb3bde8898e32e5dacba0e523ef03/web/src/pages/Tutorials/sources/thumbs/linuxiac-myspeed.webp
--------------------------------------------------------------------------------
/web/src/pages/Tutorials/sources/thumbs/marius-myspeed.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnmyt/MySpeed/5b41668ee5ecb3bde8898e32e5dacba0e523ef03/web/src/pages/Tutorials/sources/thumbs/marius-myspeed.webp
--------------------------------------------------------------------------------
/web/src/pages/Tutorials/sources/thumbs/tBJmhgn3ZOM.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnmyt/MySpeed/5b41668ee5ecb3bde8898e32e5dacba0e523ef03/web/src/pages/Tutorials/sources/thumbs/tBJmhgn3ZOM.webp
--------------------------------------------------------------------------------
/web/src/pages/Tutorials/sources/thumbs/ubunlog-myspeed.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnmyt/MySpeed/5b41668ee5ecb3bde8898e32e5dacba0e523ef03/web/src/pages/Tutorials/sources/thumbs/ubunlog-myspeed.webp
--------------------------------------------------------------------------------
/web/vite.config.js:
--------------------------------------------------------------------------------
1 | import {defineConfig} from "vite"
2 | import react from "@vitejs/plugin-react";
3 | import path from "path";
4 |
5 | export default defineConfig({
6 | plugins: [react()],
7 | resolve: {
8 | alias: {
9 | "@": path.resolve(__dirname, "./src"),
10 | },
11 | }
12 | });
--------------------------------------------------------------------------------