├── .github ├── stale.yml └── workflows │ ├── azure-static-web-apps-develop.yml │ └── azure-static-web-apps-production.yml ├── .gitignore ├── .gitmodules ├── .vscode ├── launch.json └── settings.json ├── Dockerfile ├── LICENSE ├── README.md ├── assets └── favicon.psb ├── balena.yml ├── docker-compose.yml ├── documentation ├── dashgood.png ├── dashoffline.png ├── dashslow.png ├── rpi.jpg └── rpi.md ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json ├── robots.txt └── routes.json ├── scripts ├── dockerloadenv.sh ├── dockerpublish.bat └── dockerpublish.sh ├── src ├── components │ ├── common │ │ ├── icon.css │ │ └── icon.tsx │ ├── dashboard │ │ ├── AdditionalInfoRow.if.ts │ │ ├── AdditionalInfoRow.style.ts │ │ ├── AdditionalInfoRow.tsx │ │ ├── Container.if.ts │ │ ├── Container.style.ts │ │ ├── Container.tsx │ │ ├── StatusRow.if.ts │ │ ├── StatusRow.style.ts │ │ ├── StatusRow.tsx │ │ └── index.tsx │ ├── loading │ │ ├── Container.if.ts │ │ ├── Container.style.ts │ │ ├── Container.tsx │ │ └── index.tsx │ └── offline │ │ ├── Container.if.ts │ │ ├── Container.style.ts │ │ ├── Container.tsx │ │ └── index.tsx ├── config │ ├── index.di.ts │ ├── index.ts │ ├── logging.ts │ ├── ping.ts │ ├── speedtest.ts │ ├── threshold.ts │ └── view.ts ├── controllers │ ├── dashboard.map.ts │ ├── dashboard.tsx │ ├── dashboard.util.ts │ ├── loading.map.ts │ ├── loading.tsx │ ├── offline.map.ts │ └── offline.tsx ├── index.css ├── index.tsx ├── interfaces │ ├── config.ts │ ├── di.ts │ ├── env.ts │ └── log.ts ├── ioc.ts ├── react-app-env.d.ts ├── reportWebVitals.ts ├── router │ ├── dashboard.tsx │ ├── history.tsx │ ├── index.tsx │ ├── interface.ts │ ├── loading.tsx │ ├── offline.tsx │ └── util.ts ├── services │ ├── index.ts │ ├── interface.ts │ ├── manager.ts │ ├── networkspeed │ │ ├── index.ts │ │ ├── interface.ts │ │ ├── model.ts │ │ ├── service.ts │ │ ├── speedRunner.ts │ │ └── util.ts │ └── onlinestatus │ │ ├── index.ts │ │ ├── interface.ts │ │ ├── model.ts │ │ ├── reachable.ts │ │ ├── service.ts │ │ └── util.ts ├── setupTests.ts ├── state │ ├── index.ts │ ├── interface.ts │ ├── networkspeed │ │ ├── dispatcher.ts │ │ ├── index.ts │ │ ├── interface.ts │ │ ├── reducer.ts │ │ └── state.ts │ ├── onlinestatus │ │ ├── dispatcher.ts │ │ ├── index.ts │ │ ├── interface.ts │ │ ├── reducer.ts │ │ └── state.ts │ └── store.ts └── util │ ├── env.di.ts │ ├── env.ts │ ├── log.di.ts │ └── log.ts └── tsconfig.json /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 30 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - pinned 8 | - security 9 | # Label to use when marking an issue as stale 10 | staleLabel: wontfix 11 | # Comment to post when marking an issue as stale. Set to `false` to disable 12 | markComment: > 13 | This issue has been automatically marked as stale because it has not had 14 | recent activity. It will be closed if no further activity occurs. Thank you 15 | for your contributions. 16 | # Comment to post when closing a stale issue. Set to `false` to disable 17 | closeComment: false 18 | -------------------------------------------------------------------------------- /.github/workflows/azure-static-web-apps-develop.yml: -------------------------------------------------------------------------------- 1 | name: Azure Static Web Apps CI/CD 2 | 3 | on: 4 | push: 5 | branches: 6 | - develop 7 | pull_request: 8 | types: [opened, synchronize, reopened, closed] 9 | branches: 10 | - develop 11 | 12 | jobs: 13 | build_and_deploy_job: 14 | if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed') 15 | runs-on: ubuntu-latest 16 | name: Build and Deploy Job 17 | ### Needed for Oryx to build. See: https://github.com/microsoft/Oryx/issues/605 18 | env: 19 | CI: false 20 | steps: 21 | - uses: actions/checkout@v2 22 | with: 23 | submodules: true 24 | - name: Build And Deploy 25 | id: builddeploy 26 | uses: Azure/static-web-apps-deploy@v0.0.1-preview 27 | with: 28 | azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_ICY_SEA_01BBDCE03 }} 29 | repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for Github integrations (i.e. PR comments) 30 | action: "upload" 31 | ###### Repository/Build Configurations - These values can be configured to match your app requirements. ###### 32 | # For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig 33 | app_location: "/" # App source code path 34 | api_location: "api" # Api source code path - optional 35 | output_location: "" # Built app content directory - optional 36 | ###### End of Repository/Build Configurations ###### 37 | 38 | close_pull_request_job: 39 | if: github.event_name == 'pull_request' && github.event.action == 'closed' 40 | runs-on: ubuntu-latest 41 | name: Close Pull Request Job 42 | steps: 43 | - name: Close Pull Request 44 | id: closepullrequest 45 | uses: Azure/static-web-apps-deploy@v0.0.1-preview 46 | with: 47 | azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_ICY_SEA_01BBDCE03 }} 48 | action: "close" 49 | -------------------------------------------------------------------------------- /.github/workflows/azure-static-web-apps-production.yml: -------------------------------------------------------------------------------- 1 | name: Azure Static Web Apps CI/CD 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | types: [opened, synchronize, reopened, closed] 9 | branches: 10 | - master 11 | 12 | jobs: 13 | build_and_deploy_job: 14 | if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed') 15 | runs-on: ubuntu-latest 16 | name: Build and Deploy Job 17 | ### Needed for Oryx to build. See: https://github.com/microsoft/Oryx/issues/605 18 | env: 19 | CI: false 20 | steps: 21 | - uses: actions/checkout@v2 22 | with: 23 | submodules: true 24 | - name: Build And Deploy 25 | id: builddeploy 26 | uses: Azure/static-web-apps-deploy@v0.0.1-preview 27 | with: 28 | azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_MANGO_GRASS_0EE861903 }} 29 | repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for Github integrations (i.e. PR comments) 30 | action: "upload" 31 | ###### Repository/Build Configurations - These values can be configured to match you app requirements. ###### 32 | # For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig 33 | app_build_command: "npm cache clean --force; npm run build" 34 | app_location: "/" # App source code path 35 | api_location: "api" # Api source code path - optional 36 | output_location: "build" # Built app content directory - optional 37 | ###### End of Repository/Build Configurations ###### 38 | 39 | close_pull_request_job: 40 | if: github.event_name == 'pull_request' && github.event.action == 'closed' 41 | runs-on: ubuntu-latest 42 | name: Close Pull Request Job 43 | steps: 44 | - name: Close Pull Request 45 | id: closepullrequest 46 | uses: Azure/static-web-apps-deploy@v0.0.1-preview 47 | with: 48 | azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_MANGO_GRASS_0EE861903 }} 49 | action: "close" 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | node_modules 3 | .DS_Store 4 | .eslintcache 5 | debug.log 6 | build 7 | 8 | public/speedtest.js 9 | public/speedtest_worker.js 10 | 11 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "git_modules/speedtest"] 2 | path = git_modules/speedtest 3 | url = https://github.com/Ryandev/librespeed-speedtest.git 4 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Chrome", 9 | "type": "chrome", 10 | "request": "launch", 11 | "url": "http://localhost:3000", 12 | "webRoot": "${workspaceRoot}/src", 13 | "runtimeArgs": ["--disable-web-security", "-allow-cross-origin-auth-prompt", "--allow-insecure-localhost"] 14 | }, 15 | { 16 | "name": "Debug Tests", 17 | "type": "node", 18 | "request": "launch", 19 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/react-scripts", 20 | "args": ["test", "--runInBand", "--no-cache", "--watchAll=false"], 21 | "cwd": "${workspaceRoot}", 22 | "protocol": "inspector", 23 | "console": "integratedTerminal", 24 | "internalConsoleOptions": "neverOpen", 25 | "env": { "CI": "true" }, 26 | "disableOptimisticBPs": true 27 | }, 28 | { 29 | "name": "Jest Debug opened file", 30 | "type": "node", 31 | "request": "launch", 32 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/react-scripts", 33 | "args": ["test", "${fileBasenameNoExtension}", "--runInBand", "--no-cache", "--watchAll"], 34 | "cwd": "${workspaceRoot}", 35 | "protocol": "inspector", 36 | "console": "integratedTerminal", 37 | "internalConsoleOptions": "neverOpen", 38 | "env": [ 39 | "NODE_TLS_REJECT_UNAUTHORIZED=0" 40 | ] 41 | }, 42 | { 43 | "name": "Launch via NPM", 44 | "request": "launch", 45 | "runtimeArgs": [ 46 | "run-script", 47 | "debug" 48 | ], 49 | "runtimeExecutable": "npm", 50 | "skipFiles": [ 51 | "/**" 52 | ], 53 | "type": "pwa-node" 54 | }, 55 | { 56 | "type": "pwa-chrome", 57 | "request": "launch", 58 | "name": "Launch Chrome against localhost", 59 | "url": "http://localhost:8080", 60 | "webRoot": "${workspaceFolder}" 61 | }, 62 | ] 63 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "**/.classpath": true, 4 | "**/.project": true, 5 | "**/.settings": true, 6 | "**/.factorypath": true, 7 | "**/.git": true, 8 | "**/.DS_Store": true, 9 | "**/node_modules": true, 10 | "node_modules": true 11 | }, 12 | "jestrunner.jestCommand": "npm run test --", 13 | "testEnvironment": "node", 14 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Stage.0 Build environment 2 | FROM node:11.6.0-alpine as build 3 | 4 | WORKDIR . 5 | 6 | ENV PATH /node_modules/.bin:/usr/bin:/bin:$PATH 7 | 8 | COPY . ./ 9 | COPY package.json ./ 10 | COPY package-lock.json ./ 11 | COPY scripts/dockerloadenv.sh /dockerloadenv.sh 12 | 13 | 14 | # Install deps 15 | RUN npm ci --silent 16 | 17 | # build website 18 | RUN npm run build 19 | 20 | 21 | # Stage.1 Run 22 | FROM nginx:1.19.10-alpine as run 23 | 24 | # Copy production environment 25 | COPY --from=build /build /usr/share/nginx/html 26 | 27 | # Install jq, needed for dockerloadenv.sh 28 | RUN apk update \ 29 | && apk add jq \ 30 | && rm -rf /var/cache/apk/* 31 | 32 | EXPOSE 80 33 | 34 | COPY scripts/dockerloadenv.sh /dockerloadenv.sh 35 | # Expose docker env to website index.html & then launch nginx 36 | ENTRYPOINT ["sh", "/dockerloadenv.sh"] 37 | CMD ["nginx", "-g", "daemon off;"] 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020-present Ryan Powell 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NetStatus 2 | NetStatus is designed as an always-on dashboard WebUI to track internet connectivity 3 | It will periodically recheck its connection & provide a live view of status, online or off. speed up/down & latency 4 | 5 | Live: [http://netstatus.ryanpowell.dev](http://netstatus.ryanpowell.dev) 6 | 7 | This project was intended for use with Raspberry Pi + 3.5" LCD display, however it supports multiple devices, aspect-ratios 8 | ![Raspberry PI screenshot](https://raw.githubusercontent.com/Ryandev/NetStatus/master/documentation/rpi.jpg "Raspberry PI screenshot") 9 | Device setup instructions for Raspberry PI, (flasing image, package installs, autostart setup) [RPI](https://github.com/Ryandev/NetStatus/blob/master/documentation/rpi.md) 10 | 11 | ## Features 12 | - Offline notifications 13 | - Periodic netspeed speed checks (latency, jitter, upload/download speed) 14 | - Configurable 15 | - Easy to run (Docker) 16 | - Optimized for small screens, however designed to work on any screensize or orientation, 17 | 18 | ## Screenshots 19 | 20 | ##### Everything ok 21 | ![Dashboard status ok](https://raw.githubusercontent.com/Ryandev/NetStatus/master/documentation/dashgood.png "Dashboard status ok") 22 | 23 | ##### No network connection found/lost 24 | ![Dashboard offline](https://raw.githubusercontent.com/Ryandev/NetStatus/master/documentation/dashoffline.png "Offline") 25 | 26 | ##### Latency/Upload error status & download warning status 27 | ![Dashboard status slow](https://raw.githubusercontent.com/Ryandev/NetStatus/master/documentation/dashslow.png "Dashboard status slow") 28 | 29 | > Bottom row: left is the time elapsed since the last speed test. Right is your public IP address 30 | > 31 | > When the WiFi icon is showing, a new speed-test is underway, display will be updated once all results are in 32 | 33 | ## Run 34 | Options: 35 | 1. Load url [https://netstatus.ryanpowell.dev](https://netstatus.ryanpowell.dev) in Browser 36 | 2. Build project & serve static files (Download project, run `npm run build` & serve contents from ./build) 37 | 3. Deploy locally with Docker below & open http://localhost:80 38 | 4. [Deploy with balena](https://dashboard.balena-cloud.com/deploy?repoUrl=https://github.com/Ryandev/NetStatus) (more details below) 39 | 40 | ### Docker Deployment 41 | Run below (supports amd64, arm64 & arv7, aka PC, Pi4, Pi3) 42 | ```sudo docker run --name netspeed -d --restart=always -p 80:80 ryandev/netspeed``` 43 | 44 | 45 | #### Docker Configurables 46 | | Name | Description | Environment name | Value units | Default value | 47 | |-------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------|-------------|-------------------------| 48 | | Frequency of ping checks | How frequently to fetch a favicon to check if the network is there. This is needed as `navigator.isOnline` implmentation varies across browsers | REACT_APP_PINGINTERVAL | Seconds | 15 | 49 | | Which websites to check connectivity with | Used with the above parameter, a random website from this list is pulled and the favicon is fetched from. To override this value please set as a JSON array | REACT_APP_PINGWEBSITES | N/A | See config/ping.ts | 50 | | Speed test interval | How frequently to check network speed (latency, jitter, upload & download speed) | REACT_APP_TESTINTERVAL | Seconds | 300 | 51 | | Speed test servers | List of speed test servers to use to test speed against. Configuration is passed to Librespeed, for more info see the config or Librespeed/speedtest website | REACT_APP_SERVERCONFIGURATIONS | N/A | See config/speedtest.ts | 52 | | Upload warning threshold | Threshold at which the color of the upload status will be shown as a warning. Example: if after a speed test the upload speed is less than `REACT_APP_UPLOADWARN` then display as warning | REACT_APP_UPLOADWARN | Mbit/s | 4 | 53 | | Upload error threshold | Same as above except for displaying as error status | REACT_APP_UPLOADERROR | Mbit/s | 1 | 54 | | Download warning theshold | | REACT_APP_DOWNLOADWARN | Mbit/s | 8 | 55 | | Download error threshold | | REACT_APP_DOWNLOADERROR | Mbit/s | 1 | 56 | | Latency warning threshold | | REACT_APP_LATENCYWARN | ms | 40 | 57 | | Latency error threshold | | REACT_APP_LATENCYERROR | ms | 100 | 58 | | Jitter warning threshold | | REACT_APP_JITTERWARN | ms | 50 | 59 | | Jitter error threshold | | REACT_APP_JITTERERROR | ms | 100 | 60 | All configurables can be found under src/config/*.ts 61 | 62 | #### Docker Deployment Example 63 | Set speed test interval to 10mins, ping checks every 1min, & latency warn threshold to 20ms 64 | ``` 65 | sudo docker run --name netspeed -d --restart=always -p 80:80 --env REACT_APP_TESTINTERVAL=600 --env REACT_APP_PINGINTERVAL=60 --env REACT_APP_LATENCYWARN=20 ryandev/netspeed:arm64 66 | ``` 67 | 68 | #### Balena Deployment Example 69 | You can use balenaCloud to deploy this project to Raspberry Pis and other single board computers in just a few clicks, avoiding the need to manually configure any software packages. 70 | 71 | [![](https://balena.io/deploy.svg)](https://dashboard.balena-cloud.com/deploy?repoUrl=https://github.com/Ryandev/NetStatus) 72 | 73 | **or, manually:** 74 | 75 | * Install the [balena CLI tools](https://github.com/balena-io/balena-cli/blob/master/INSTALL.md) 76 | * Login with `balena login` 77 | * Download this project and from the project directory run `balena push ` where `` is the name you gave your balenaCloud application in the first step 78 | 79 | 80 | ### Attributions 81 | - [Librespeed - SpeedTest](github.com/librespeed/speedtest) 82 | - [FontAwesome - Iconography](fontawesome.com) 83 | - [Bootstrap - HTML Layout](getbootstrap.com) 84 | - [Inconsolata, Google fonts - Typography](https://fonts.google.com/specimen/Inconsolata?query=consol&preview.text=NetSpeed&preview.text_type=custom) 85 | 86 | 87 | License 88 | ---- 89 | 90 | MIT 91 | 92 | -------------------------------------------------------------------------------- /assets/favicon.psb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ryandev/NetStatus/6821a175c163809a55a9881f6024214d714838bb/assets/favicon.psb -------------------------------------------------------------------------------- /balena.yml: -------------------------------------------------------------------------------- 1 | name: "NetStatus" 2 | description: "Internet speed & offline status monitor. Upload, download, ping, latency dashboard display" 3 | type: "sw.application" 4 | assets: 5 | repository: 6 | type: "blob.asset" 7 | data: 8 | url: "https://github.com/Ryandev/NetStatus" 9 | logo: 10 | type: "blob.asset" 11 | data: 12 | url: "https://raw.githubusercontent.com/Ryandev/NetStatus/master/public/logo512.png" 13 | data: 14 | defaultDeviceType: "raspberrypi3" 15 | supportedDeviceTypes: 16 | - "raspberrypi4-64" 17 | - "fincm3" 18 | - "raspberrypi3" 19 | - "raspberrypi3-64" 20 | - "intel-nuc" 21 | - "genericx86-64-ext" 22 | - "raspberrypi400-64" 23 | - "orange-pi-zero" 24 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | netspeed: 4 | image: ryandev/netspeed 5 | ports: 6 | - "80" 7 | restart: always 8 | browser: 9 | image: balenablocks/browser 10 | network_mode: host 11 | privileged: true 12 | environment: 13 | - 'KIOSK=1' 14 | fbcp: 15 | image: balenablocks/fbcp 16 | privileged: true -------------------------------------------------------------------------------- /documentation/dashgood.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ryandev/NetStatus/6821a175c163809a55a9881f6024214d714838bb/documentation/dashgood.png -------------------------------------------------------------------------------- /documentation/dashoffline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ryandev/NetStatus/6821a175c163809a55a9881f6024214d714838bb/documentation/dashoffline.png -------------------------------------------------------------------------------- /documentation/dashslow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ryandev/NetStatus/6821a175c163809a55a9881f6024214d714838bb/documentation/dashslow.png -------------------------------------------------------------------------------- /documentation/rpi.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ryandev/NetStatus/6821a175c163809a55a9881f6024214d714838bb/documentation/rpi.jpg -------------------------------------------------------------------------------- /documentation/rpi.md: -------------------------------------------------------------------------------- 1 | # NetStatus - Raspberry Pi setup instructions 2 | Note this is currently for the Raspberry Pi 4 (arm64) only 3 | 4 | This project was intended for use with Raspberry Pi 4 + 480x320px 3.5" LCD 5 | 6 | ![Raspberry PI screenshot](https://raw.githubusercontent.com/Ryandev/NetStatus/master/documentation/rpi.jpg "Raspberry PI screenshot") 7 | 8 | ### 1. Prerequisites 9 | Ubuntu 20.04 LTS fresh install on a 16GiB or greater SD card 10 | 11 | ### 2. Setup System 12 | Login via ssh & run the following 13 | 14 | (update) 15 | ``` 16 | sudo apt-get -y upgrade; sudo apt-get -u update; 17 | ``` 18 | 19 | (`docker.io` for Docker, `unattended-upgrades` for auto-updates, `net-tools` for ifconfig, `xorg` for xwindows & `chromium-browser` will be our browser ) 20 | ``` 21 | sudo apt -y install docker.io unattended-upgrades net-tools xorg chromium-browser 22 | ``` 23 | 24 | Install 3.5" screen support for ubuntu (triggers reboot) 25 | reference: http://www.lcdwiki.com/3.5inch_RPi_Display 26 | ``` 27 | git clone https://github.com/lcdwiki/LCD-show-ubuntu.git 28 | cd LCD-show-ubuntu/ 29 | cd .. 30 | chmod 755 LCD-show-ubuntu/ 31 | cd LCD-show-ubuntu/ 32 | sudo ./LCD35-show 33 | ``` 34 | 35 | 36 | ### 3. Crontab 37 | With a terminal window open run crontab -e & add the following lines 38 | 1. (Setup updates @ 1am) 39 | 2. (Setup daily restart @ 5am) 40 | ``` 41 | 0 1 * * * sudo apt-get clean && sudo apt --fix-broken install && sudo unattended-upgrades 42 | 0 5 * * * /sbin/shutdown -r +5 43 | ``` 44 | 45 | 46 | ##### 4. Setup Gnome ubuntu for unattended use 47 | (Run the following in a terminal window) 48 | ``` 49 | echo 'Disabling screensaver' && 50 | gsettings set org.gnome.desktop.screensaver lock-enabled false 51 | 52 | echo 'Disabling auto-lock' && 53 | gsettings set org.gnome.desktop.session idle-delay 0 54 | 55 | echo 'Disable suspend' && 56 | gsettings set org.gnome.settings-daemon.plugins.power sleep-inactive-ac-type "nothing" && 57 | gsettings set org.gnome.settings-daemon.plugins.power sleep-inactive-ac-timeout 0 58 | 59 | echo 'Disabling sleep' && 60 | sudo systemctl mask sleep.target suspend.target hibernate.target hybrid-sleep.target 61 | 62 | echo 'Installing unclutter to hide cursor' && 63 | sudo apt install -y unclutter 64 | 65 | echo 'Installing xdotool to enable triggering fullscreen on browser' && 66 | sudo apt install xdotool 67 | 68 | echo 'Clearing gnome background' && 69 | gsettings set org.gnome.desktop.background picture-uri none && 70 | gsettings set org.gnome.desktop.background color-shading-type 'solid' && 71 | gsettings set org.gnome.desktop.background primary-color '#000000' 72 | ``` 73 | 74 | ### 5. Docker autostart 75 | ``` 76 | echo 'Setting up Docker to auto start on boot/login' && 77 | sudo systemctl enable docker 78 | ``` 79 | 80 | ### 6. Setup browser on startup gnome 81 | ``` 82 | echo 'Creating browser.sh auto start on boot script'; 83 | mkdir ~/scripts 84 | mkdir ~/.config/autostart 85 | echo ' 86 | [Desktop Entry] 87 | Type=Application 88 | Name=Browser 89 | Exec=~/scripts/browser.sh 90 | X-GNOME-Autostart-enabled=true 91 | ' > ~/.config/autostart/.desktop 92 | ``` 93 | 94 | Save the following to ~/scripts/browser.sh 95 | ``` 96 | URL="http://localhost" 97 | 98 | # Stop the Raspberry Pi’s display power management system from kicking in and blanking out the screen 99 | xset s noblank 100 | xset s off 101 | xset -dpms 102 | 103 | # Hide cursor 104 | unclutter -idle 0.5 -root & 105 | 106 | sed -i 's/"exited_cleanly":false/"exited_cleanly":true/' ~/.config/chromium/Default/Preferences 107 | sed -i 's/"exit_type":"Crashed"/"exit_type":"Normal"/' ~/.config/chromium/Default/Preferences 108 | /usr/bin/chromium-browser --check-for-update-interval=31536000 --no-first-run --start-fullscreen --start-maximized --disable-notifications --noerrdialogs --disable-infobars --load-extension=~/.config/chromium/extensions/crashautoreload.crx --window-size=480,320 --force-device-scale-factor=1 --app=$URL | at now + 10s 109 | ``` 110 | then run `chmod +x ~/scripts/browser.sh` 111 | 112 | ### 7. Setup browser to auto-restart on crash 113 | ``` 114 | echo 'Download & install oh-no-you-didnt https://chrome.google.com/webstore/detail/oh-no-you-didnt/acdablfhjbhkjbcifldncdkmlophfgda/related?hl=en' && 115 | echo 'This will reload on "Aw, Snap!" messages automatically' && 116 | mkdir -p ~/.config/chromium/extensions && 117 | curl -H 'Host: clients2.google.com' -H 'sec-fetch-site: none' -H 'sec-fetch-mode: navigate' -H 'sec-fetch-dest: empty' -H 'user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36' -H 'accept-language: en-US,en;q=0.9' --compressed 'https://clients2.google.com/service/update2/crx?response=redirect&prodversion=45&acceptformat=crx2,crx3&x=id%3Dacdablfhjbhkjbcifldncdkmlophfgda%26uc' -L -o ~/.config/chromium/extensions/crashautoreload.crx 118 | ``` 119 | 120 | 121 | ### 8. Setup docker to run Netspeed 122 | ``` 123 | sudo docker run --name netspeed -d --restart=always -p 80:80 ryandev/netspeed 124 | ``` 125 | 126 | Finally Reboot :) -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "netspeedts", 3 | "version": "1.0.0", 4 | "private": true, 5 | "dependencies": { 6 | "@fortawesome/fontawesome-svg-core": "^1.2.32", 7 | "@fortawesome/free-solid-svg-icons": "^5.15.1", 8 | "@fortawesome/react-fontawesome": "^0.1.13", 9 | "@testing-library/jest-dom": "^5.11.9", 10 | "@testing-library/react": "^11.2.3", 11 | "@testing-library/user-event": "^12.6.2", 12 | "@types/jest": "^26.0.20", 13 | "@types/node": "^12.19.15", 14 | "@types/react": "^16.14.2", 15 | "@types/react-dom": "^16.9.10", 16 | "inversify": "^5.0.5", 17 | "react": "^17.0.1", 18 | "react-dom": "^17.0.1", 19 | "react-helmet": "^6.1.0", 20 | "react-redux": "^7.2.2", 21 | "react-router": "^5.2.0", 22 | "react-router-dom": "^5.2.0", 23 | "react-scripts": "4.0.1", 24 | "redux": "^4.0.5", 25 | "reflect-metadata": "^0.1.13", 26 | "typescript": "^4.1.3", 27 | "web-vitals": "^0.2.4" 28 | }, 29 | "scripts": { 30 | "setup": "cp git_modules/speedtest/speedtest.js public/speedtest.js; cp git_modules/speedtest/speedtest_worker.js public/speedtest_worker.js;", 31 | "start": "npm run setup; react-scripts start", 32 | "build": "npm run setup; react-scripts build", 33 | "test": "react-scripts test", 34 | "eject": "react-scripts eject" 35 | }, 36 | "eslintConfig": { 37 | "rules": { 38 | "no-unused-vars": [ 39 | "warn", 40 | { 41 | "vars": "all", 42 | "args": "after-used", 43 | "ignoreRestSiblings": false 44 | } 45 | ], 46 | "no-undef": [ 47 | "warn" 48 | ] 49 | }, 50 | "extends": [ 51 | "react-app", 52 | "react-app/jest", 53 | "eslint:recommended" 54 | ] 55 | }, 56 | "browserslist": { 57 | "production": [ 58 | ">0.2%", 59 | "not dead", 60 | "not op_mini all" 61 | ], 62 | "development": [ 63 | "last 1 chrome version", 64 | "last 1 firefox version", 65 | "last 1 safari version" 66 | ] 67 | }, 68 | "devDependencies": { 69 | "@types/history": "^4.7.8", 70 | "@types/react-helmet": "^6.1.0", 71 | "@types/react-redux": "^7.1.12", 72 | "@types/react-router-dom": "^5.1.6" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ryandev/NetStatus/6821a175c163809a55a9881f6024214d714838bb/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Net Status 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 |
35 | 36 | 37 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ryandev/NetStatus/6821a175c163809a55a9881f6024214d714838bb/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ryandev/NetStatus/6821a175c163809a55a9881f6024214d714838bb/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "NetSpeed", 3 | "name": "Network Speed Status", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#9ae94e", 24 | "background_color": "#000000" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /public/routes.json: -------------------------------------------------------------------------------- 1 | { 2 | "routes": [ 3 | { 4 | "route": "/*", 5 | "serve": "/index.html", 6 | "statusCode": 200 7 | } 8 | ] 9 | } -------------------------------------------------------------------------------- /scripts/dockerloadenv.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -euo pipefail 3 | 4 | # Capture all environment variables starting with REACT_APP_ and make JSON string from them 5 | ENV_JSON="$(jq --compact-output --null-input 'env | with_entries(select(.key | startswith("REACT_APP_")))')" 6 | 7 | # Escape sed replacement's special characters: \, &, /. 8 | # No need to escape newlines, because --compact-output already removed them. 9 | # Inside of JSON strings newlines are already escaped. 10 | ENV_JSON_ESCAPED="$(printf "%s" "${ENV_JSON}" | sed -e 's/[\&/]/\\&/g')" 11 | 12 | HTML_PATH=/usr/share/nginx/html/index.html 13 | HTML_PATH_TMP=/tmp/index.html 14 | 15 | # Find the existing placeholder script tag & swap for our values 16 | SCRIPT_EXISTING='window.env={}' 17 | SCRIPT_NEW="window.env=${ENV_JSON_ESCAPED}" 18 | 19 | sed "s/${SCRIPT_EXISTING}/${SCRIPT_NEW}/g" ${HTML_PATH} > ${HTML_PATH_TMP} 20 | 21 | mv ${HTML_PATH_TMP} ${HTML_PATH} 22 | 23 | exec "$@" 24 | -------------------------------------------------------------------------------- /scripts/dockerpublish.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | ECHO "Starting docker build & publish script" 3 | 4 | :: Get script dir 5 | pushd %~dp0 6 | set SCRIPTDIR=%CD% 7 | popd 8 | 9 | :: Args 10 | SET IMGNAME="ryandev/netspeed" 11 | SET TAGVER="latest" 12 | SET DOCKERDIR=%SCRIPTDIR%\.. 13 | 14 | :: Run 15 | @REM Requires experimental flag of Docker daemon enabled for buildx to work 16 | ECHO "Building docker image" 17 | docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t %IMGNAME%:%TAGVER% --push %DOCKERDIR% 18 | @REM If this returns with an error, follow the instruction 'docker buildx create --use' & rerun 19 | IF %ERRORLEVEL% NEQ 0 EXIT 1 20 | 21 | echo "Published new build for arch arm7/arm64 & amd64" -------------------------------------------------------------------------------- /scripts/dockerpublish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" 3 | 4 | echo "Starting docker build & publish script" 5 | 6 | #Args 7 | IMGNAME="ryandev/netspeed" 8 | TAGVER="latest" 9 | DOCKERDIR=$SCRIPTDIR/.. 10 | 11 | #Build 12 | #Requires experimental flag of Docker daemon enabled for buildx to work 13 | echo "Building docker image" 14 | 15 | docker buildx build --output=type=image --platform linux/amd64,linux/arm64,linux/arm/v7 -t $IMGNAME:$TAGVER --push $DOCKERDIR 16 | EXITSTATUS=$? 17 | 18 | if [[ $EXITSTATUS -ne 0 ]]; then 19 | echo "Published new build for arch arm7/arm64 & amd64" 20 | fi 21 | 22 | exit $EXITSTATUS 23 | -------------------------------------------------------------------------------- /src/components/common/icon.css: -------------------------------------------------------------------------------- 1 | 2 | @-webkit-keyframes animateRotate /* Safari and Chrome */ { 3 | from { 4 | -webkit-transform: rotate(0deg); 5 | -o-transform: rotate(0deg); 6 | transform: rotate(0deg); 7 | } 8 | to { 9 | -webkit-transform: rotate(360deg); 10 | -o-transform: rotate(360deg); 11 | transform: rotate(360deg); 12 | } 13 | } 14 | 15 | @keyframes animateRotate { 16 | from { 17 | -ms-transform: rotate(0deg); 18 | -moz-transform: rotate(0deg); 19 | -webkit-transform: rotate(0deg); 20 | -o-transform: rotate(0deg); 21 | transform: rotate(0deg); 22 | } 23 | to { 24 | -ms-transform: rotate(360deg); 25 | -moz-transform: rotate(360deg); 26 | -webkit-transform: rotate(360deg); 27 | -o-transform: rotate(360deg); 28 | transform: rotate(360deg); 29 | } 30 | } 31 | 32 | .animateRotate { 33 | -webkit-animation: animateRotate 1.0s linear infinite; 34 | -moz-animation: animateRotate 1.0s linear infinite; 35 | -ms-animation: animateRotate 1.0s linear infinite; 36 | -o-animation: animateRotate 1.0s linear infinite; 37 | animation: animateRotate 1.0s linear infinite; 38 | } 39 | -------------------------------------------------------------------------------- /src/components/common/icon.tsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react'; 3 | import { IconName, library } from '@fortawesome/fontawesome-svg-core' 4 | import { faCheckCircle, faPlug, faQuestionCircle, faExclamationTriangle, faWifi, faSpinner, faMapMarkerAlt, faClock } from '@fortawesome/free-solid-svg-icons' 5 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 6 | import './icon.css'; 7 | 8 | var _hasInitializedFontAwesome = false; 9 | 10 | const _initFontAwesome = () => { 11 | if ( _hasInitializedFontAwesome ) { return; } 12 | _hasInitializedFontAwesome = true; 13 | 14 | library.add(faCheckCircle, faPlug, faQuestionCircle, faExclamationTriangle, faWifi, faMapMarkerAlt, faClock); 15 | } 16 | 17 | function ComponentForIconName(faIconName: IconName, className="", styling={}): JSX.Element { 18 | _initFontAwesome(); 19 | return (); 20 | } 21 | 22 | const TickCircle = (styling={}) => ComponentForIconName(faCheckCircle.iconName, '', styling); 23 | const QuestionMarkCircle = (styling={}) => ComponentForIconName(faQuestionCircle.iconName, '', styling); 24 | const ExclamationTriangle = (styling={}) => ComponentForIconName(faExclamationTriangle.iconName, '', styling); 25 | const Plug = (styling={}) => ComponentForIconName(faPlug.iconName, '', styling); 26 | const WiFi = (styling={}) => ComponentForIconName(faWifi.iconName, '', styling); 27 | const Spinner = (styling={}) => ComponentForIconName(faSpinner.iconName, 'animateRotate', styling); 28 | const MapMarker = (styling={}) => ComponentForIconName(faMapMarkerAlt.iconName, '', styling); 29 | const Clock = (styling={}) => ComponentForIconName(faClock.iconName, '', styling); 30 | 31 | const exports = { 32 | TickCircle, 33 | QuestionMarkCircle, 34 | ExclamationTriangle, 35 | Plug, 36 | WiFi, 37 | Spinner, 38 | MapMarker, 39 | Clock, 40 | }; 41 | 42 | export default exports; 43 | -------------------------------------------------------------------------------- /src/components/dashboard/AdditionalInfoRow.if.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface IAdditionalInfoRowProps { 3 | showIconLeft: boolean; 4 | iconLeft: "wifi" | "spinner" | "clock"; 5 | iconRight: "map-marker"; 6 | textLeft: string; 7 | textRight: string; 8 | } 9 | -------------------------------------------------------------------------------- /src/components/dashboard/AdditionalInfoRow.style.ts: -------------------------------------------------------------------------------- 1 | 2 | import React from "react"; 3 | 4 | 5 | const styleContainer: React.CSSProperties = { 6 | width: '100%', 7 | marginLeft: '0px', 8 | marginRight: '0px', 9 | marginTop: '-2vmin', 10 | borderRadius: '0px', 11 | padding: '0px', 12 | height: '14vmin', 13 | fontSize: '9vmin', 14 | color: '#fff', 15 | overflow: 'hidden', 16 | paddingLeft: '5vw', 17 | paddingRight: '3vw', 18 | letterSpacing: '-1px', 19 | }; 20 | 21 | const styleColLeft: React.CSSProperties = { 22 | width: '36%', 23 | height: '100%', 24 | overflow: 'hidden', 25 | textOverflow: 'hidden', 26 | whiteSpace: 'nowrap', 27 | display: 'flex', 28 | alignItems: 'center', 29 | justifyContent: 'flex-start', 30 | }; 31 | 32 | const styleColRight: React.CSSProperties = { 33 | width: '64%', 34 | height: '100%', 35 | overflow: 'hidden', 36 | textOverflow: 'hidden', 37 | whiteSpace: 'nowrap', 38 | textAlign: 'right', 39 | display: 'flex', 40 | alignItems: 'center', 41 | justifyContent: 'flex-end', 42 | }; 43 | 44 | const styleSpanLeft: React.CSSProperties = { 45 | marginLeft: '2vmin', 46 | } 47 | 48 | const styleSpanRight: React.CSSProperties = { 49 | marginRight: '2vmin', 50 | } 51 | 52 | const style = { 53 | container: styleContainer, 54 | colLeft: styleColLeft, 55 | colRight: styleColRight, 56 | spanLeft: styleSpanLeft, 57 | spanRight: styleSpanRight, 58 | } 59 | 60 | export default style; 61 | -------------------------------------------------------------------------------- /src/components/dashboard/AdditionalInfoRow.tsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react'; 3 | import IOC from '../../ioc' 4 | import { ILogger } from '../../interfaces/log'; 5 | import Icon from '../common/icon'; 6 | import styling from './AdditionalInfoRow.style'; 7 | import { IAdditionalInfoRowProps } from './AdditionalInfoRow.if'; 8 | 9 | 10 | const iconForStatus = (statusVal: "wifi"|"spinner"|"clock"|"map-marker", log: ILogger = IOC().logger()): JSX.Element => { 11 | let elem = Icon.ExclamationTriangle(); 12 | 13 | switch (statusVal) { 14 | case "wifi": 15 | elem = Icon.WiFi(); 16 | break; 17 | 18 | case "spinner": 19 | elem = Icon.Spinner(); 20 | break; 21 | 22 | case "clock": 23 | elem = Icon.Clock(); 24 | break; 25 | 26 | case "map-marker": 27 | elem = Icon.MapMarker(); 28 | break; 29 | 30 | default: 31 | log.error('Unrecognised status: ' + statusVal); 32 | } 33 | 34 | return elem; 35 | } 36 | 37 | function AdditionalInfoRow(props: IAdditionalInfoRowProps): JSX.Element { 38 | const iconLeft = (props.showIconLeft ? iconForStatus(props.iconLeft) : ''); 39 | const iconRight = iconForStatus(props.iconRight); 40 | 41 | return ( 42 |
43 |
44 | {iconLeft} 45 | {props.textLeft} 46 |
47 |
48 | {props.textRight} 49 | {iconRight} 50 |
51 |
52 | ); 53 | } 54 | 55 | export default AdditionalInfoRow; 56 | -------------------------------------------------------------------------------- /src/components/dashboard/Container.if.ts: -------------------------------------------------------------------------------- 1 | 2 | export enum DashboardStatusValue { 3 | Unknown = 0, 4 | Bad = 1, 5 | Warning = 2, 6 | Good = 3, 7 | } 8 | 9 | export interface IDashboardStatusItem { 10 | name: string; 11 | value: string; 12 | status : DashboardStatusValue; 13 | } 14 | 15 | export interface IDashboardProps { 16 | statusItems: IDashboardStatusItem[]; 17 | showInfoIcon: boolean; 18 | infoIcon: "wifi" | "spinner" | "clock"; 19 | infoTextLeft: string; 20 | infoTextRight: string; 21 | infoIconRight: "map-marker"; 22 | refreshPageInterval: number; 23 | } 24 | -------------------------------------------------------------------------------- /src/components/dashboard/Container.style.ts: -------------------------------------------------------------------------------- 1 | 2 | const style = { 3 | container: { 4 | primary: { 5 | backgroundColor: '#000', 6 | paddingLeft: '0px', 7 | paddingRight: '0px', 8 | paddingTop: '6vh', 9 | paddingBottom: '0vh', 10 | marginLeft: '0', 11 | marginRight: '0', 12 | width: '100%', 13 | height: '100vmin', 14 | maxWidth: '100%', 15 | } 16 | } 17 | }; 18 | 19 | export default style; 20 | -------------------------------------------------------------------------------- /src/components/dashboard/Container.tsx: -------------------------------------------------------------------------------- 1 | 2 | import React from "react"; 3 | import StatusRow from "./StatusRow"; 4 | import AdditionalInfoRow from "./AdditionalInfoRow"; 5 | import styling from "./Container.style"; 6 | import { IDashboardStatusItem, IDashboardProps, DashboardStatusValue } from "./Container.if"; 7 | import { Status } from "./StatusRow.if"; 8 | import { IAdditionalInfoRowProps } from "./AdditionalInfoRow.if"; 9 | 10 | 11 | function Container(props: IDashboardProps): JSX.Element { 12 | const mapStatusItemToStatusRow = (item: IDashboardStatusItem): JSX.Element => { 13 | const statusMap: Record = { 14 | [DashboardStatusValue.Good]: Status.Good, 15 | [DashboardStatusValue.Warning]: Status.Warning, 16 | [DashboardStatusValue.Bad]: Status.Bad, 17 | [DashboardStatusValue.Unknown]: Status.Bad, 18 | }; 19 | 20 | return 26 | } 27 | 28 | const statusRows = props 29 | .statusItems 30 | .map((data) => mapStatusItemToStatusRow(data)); 31 | 32 | const infoProps: IAdditionalInfoRowProps = { 33 | showIconLeft: props.showInfoIcon, 34 | iconLeft: props.infoIcon, 35 | iconRight: props.infoIconRight, 36 | textLeft: props.infoTextLeft, 37 | textRight: props.infoTextRight, 38 | }; 39 | 40 | return ( 41 |
45 | {statusRows} 46 | 47 |
48 | ); 49 | } 50 | 51 | export default Container; 52 | -------------------------------------------------------------------------------- /src/components/dashboard/StatusRow.if.ts: -------------------------------------------------------------------------------- 1 | 2 | export enum Status { 3 | Good = "Good", 4 | Warning = "Warning", 5 | Bad = "Bad", 6 | } 7 | 8 | export interface IStatusProps { 9 | name: string; 10 | value: string; 11 | status: Status; 12 | } 13 | -------------------------------------------------------------------------------- /src/components/dashboard/StatusRow.style.ts: -------------------------------------------------------------------------------- 1 | 2 | import React from "react"; 3 | import IOC from '../../ioc' 4 | import { ILogger } from "../../interfaces/log"; 5 | import { Status, IStatusProps } from './StatusRow.if'; 6 | 7 | 8 | type Element = 'Container' | 'InnerContainer' | 'SpanName' | 'SpanValue' | 'StatusIcon'; 9 | 10 | const DEFAULTS: Record> = { 11 | CONTAINER: { 12 | DEFAULT: { 13 | width: '100%', 14 | height: '15vh', 15 | marginTop: '0px', 16 | marginBottom: '5vh', 17 | marginLeft: '0px', 18 | marginRight: '0px', 19 | borderRadius: '0px', 20 | overflow: 'hidden', 21 | textOverflow: 'hidden', 22 | whiteSpace: 'nowrap', 23 | padding: '0%', 24 | }, 25 | STATUS_GOOD: { 26 | backgroundColor: '#9ae94e', 27 | borderColor: '#9ae94e', 28 | color: '#000', 29 | }, 30 | STATUS_WARNING: { 31 | backgroundColor: '#f56e07', 32 | borderColor: '#f56e07', 33 | color: '#fff', 34 | }, 35 | STATUS_BAD: { 36 | backgroundColor: '#e63010', 37 | borderColor: '#e63010', 38 | color: '#fff', 39 | } 40 | }, 41 | INNERCONTAINER: { 42 | DEFAULT: { 43 | display: 'flex', 44 | alignItems: 'center', 45 | width: '100%', 46 | height: '100%', 47 | padding: '0px', 48 | paddingLeft: '5%', 49 | } 50 | }, 51 | STATUSICON: { 52 | DEFAULT: { 53 | width: '9vmin', 54 | height: '9vmin', 55 | } 56 | }, 57 | SPANNAME: { 58 | DEFAULT: { 59 | display: 'inline-block', 60 | fontSize: '10vmin', 61 | lineHeight: '10vmin', 62 | fontFamily: 'Inconsolata', 63 | width: '45vw', 64 | paddingLeft: '2%', 65 | paddingRight: '2%', 66 | wordWrap: 'break-word', 67 | wordBreak: 'break-all', 68 | overflowWrap: 'anywhere', 69 | whiteSpace: 'normal', 70 | } 71 | }, 72 | SPANVALUE: { 73 | DEFAULT: { 74 | display: 'inline-block', 75 | position: 'relative', 76 | width: '40vw', 77 | fontSize: '10vmin', 78 | lineHeight: '10vmin', 79 | fontFamily: 'Inconsolata', 80 | paddingLeft: '2%', 81 | wordWrap: 'break-word', 82 | wordBreak: 'break-all', 83 | overflowWrap: 'anywhere', 84 | whiteSpace: 'normal', 85 | } 86 | }, 87 | }; 88 | 89 | const styling = (element: Element, props: IStatusProps, log: ILogger = IOC().logger()): React.CSSProperties => { 90 | var elemStyle = {}; 91 | 92 | switch (element) { 93 | case "Container": 94 | Object.assign(elemStyle, 95 | DEFAULTS.CONTAINER.DEFAULT, 96 | props.status === Status.Good && DEFAULTS.CONTAINER.STATUS_GOOD, 97 | props.status === Status.Warning && DEFAULTS.CONTAINER.STATUS_WARNING, 98 | props.status === Status.Bad && DEFAULTS.CONTAINER.STATUS_BAD); 99 | break; 100 | 101 | case "StatusIcon": 102 | Object.assign(elemStyle, DEFAULTS.STATUSICON.DEFAULT); 103 | break; 104 | 105 | case "InnerContainer": 106 | Object.assign(elemStyle, DEFAULTS.INNERCONTAINER.DEFAULT); 107 | break; 108 | 109 | case "SpanName": 110 | Object.assign(elemStyle, DEFAULTS.SPANNAME.DEFAULT); 111 | break; 112 | 113 | case "SpanValue": 114 | Object.assign(elemStyle, DEFAULTS.SPANVALUE.DEFAULT); 115 | break; 116 | 117 | default: 118 | log.error("Unknown property: " + element); 119 | break; 120 | } 121 | 122 | return elemStyle; 123 | } 124 | 125 | export default styling; 126 | -------------------------------------------------------------------------------- /src/components/dashboard/StatusRow.tsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react'; 3 | import IOC from '../../ioc' 4 | import { ILogger } from '../../interfaces/log'; 5 | import Icon from '../common/icon'; 6 | import styling from './StatusRow.style'; 7 | import { Status, IStatusProps } from './StatusRow.if'; 8 | 9 | 10 | const iconForStatus = (statusVal: Status, style={}, log: ILogger = IOC().logger()): JSX.Element => { 11 | let elem = Icon.ExclamationTriangle(style); 12 | 13 | switch (statusVal) { 14 | case Status.Good: 15 | elem = Icon.TickCircle(style); 16 | break; 17 | 18 | case Status.Warning: 19 | elem = Icon.QuestionMarkCircle(style); 20 | break; 21 | 22 | case Status.Bad: 23 | elem = Icon.ExclamationTriangle(style); 24 | break; 25 | 26 | default: 27 | log.error('Unrecognised status: ' + statusVal); 28 | } 29 | 30 | return elem; 31 | } 32 | 33 | function StatusRow(props: IStatusProps) { 34 | return ( 35 |
39 |
40 | {iconForStatus(props.status, styling('StatusIcon', props))} 41 | {props.name} 42 | {props.value} 43 |
44 |
45 | ); 46 | } 47 | 48 | export default StatusRow; 49 | -------------------------------------------------------------------------------- /src/components/dashboard/index.tsx: -------------------------------------------------------------------------------- 1 | 2 | import Container from './Container'; 3 | 4 | export default Container; 5 | -------------------------------------------------------------------------------- /src/components/loading/Container.if.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface ILoadingProps { 3 | title: string; 4 | subtitle: string; 5 | iconName: "exclamation"|"spinner"; 6 | } 7 | -------------------------------------------------------------------------------- /src/components/loading/Container.style.ts: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react'; 3 | import { ILoadingProps } from './Container.if'; 4 | 5 | 6 | type Element = "Container" | "Col" | "P" | "Row" | "Icon" | "SpanTitle" | "SpanSubTitle"; 7 | 8 | const DEFAULT_STYLE: Record = { 9 | CONTAINER: { 10 | backgroundColor: '#000', 11 | paddingLeft: '0px', 12 | paddingRight: '0px', 13 | paddingTop: '10vh', 14 | paddingBottom: '10vh', 15 | width: '100%', 16 | height: '100%' 17 | }, 18 | COL: { 19 | width: '100%' 20 | }, 21 | P: { 22 | width: '100%', 23 | fontFamily: 'Inconsolata' 24 | }, 25 | ROW: { 26 | textAlign: 'center', 27 | color: '#fff', 28 | height: '30%', 29 | width: '100%', 30 | marginLeft: 0, 31 | marginRight: 0, 32 | }, 33 | ICON: { 34 | fontSize: '18vmin', 35 | textAlign: 'center', 36 | color: '#fff' 37 | }, 38 | SPANTITLE: { 39 | fontSize: '12vmin', 40 | lineHeight: '10vmin', 41 | }, 42 | SPANSUBTITLE: { 43 | fontSize: '8vmin', 44 | lineHeight: '6vmin', 45 | }, 46 | }; 47 | 48 | function stylingForElement(element: Element, props: ILoadingProps): React.CSSProperties { 49 | let styling: React.CSSProperties = {}; 50 | 51 | switch (element) { 52 | case 'Container': 53 | Object.assign(styling, DEFAULT_STYLE.CONTAINER); 54 | break; 55 | 56 | case 'Col': 57 | Object.assign(styling, DEFAULT_STYLE.COL); 58 | break; 59 | 60 | case 'P': 61 | Object.assign(styling, DEFAULT_STYLE.P); 62 | break; 63 | 64 | case 'Row': 65 | Object.assign(styling, DEFAULT_STYLE.ROW); 66 | break; 67 | 68 | case 'Icon': 69 | Object.assign(styling, DEFAULT_STYLE.ICON); 70 | break; 71 | 72 | case 'SpanTitle': 73 | Object.assign(styling, DEFAULT_STYLE.SPANTITLE); 74 | break; 75 | 76 | case 'SpanSubTitle': 77 | Object.assign(styling, DEFAULT_STYLE.SPANSUBTITLE); 78 | break; 79 | } 80 | 81 | return styling; 82 | } 83 | 84 | export default stylingForElement; 85 | -------------------------------------------------------------------------------- /src/components/loading/Container.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { ILogger } from '../../interfaces/log'; 3 | import IOC from '../../ioc' 4 | import Icon from '../common/icon'; 5 | import styling from './Container.style'; 6 | import { ILoadingProps } from './Container.if'; 7 | 8 | 9 | const iconForStatus = (statusVal: "exclamation"|"spinner", style: React.CSSProperties = {}, log: ILogger = IOC().logger()): JSX.Element => { 10 | let elem = Icon.ExclamationTriangle(); 11 | 12 | switch (statusVal) { 13 | case "exclamation": 14 | elem = Icon.ExclamationTriangle(style); 15 | break; 16 | 17 | case "spinner": 18 | elem = Icon.Spinner(style); 19 | break; 20 | 21 | default: 22 | log.error('Unrecognised status: ' + statusVal); 23 | } 24 | 25 | return elem; 26 | } 27 | 28 | function Container(props: ILoadingProps) { 29 | return ( 30 |
34 |
35 |
36 |

37 | {props.title} 38 |

39 |
40 |
41 | 42 |
43 |
44 |

45 | {props.subtitle} 46 |

47 |
48 |
49 | 50 |
51 |
52 | {iconForStatus(props.iconName, styling('Icon', props))} 53 |
54 |
55 |
56 | ); 57 | } 58 | 59 | export default Container; 60 | -------------------------------------------------------------------------------- /src/components/loading/index.tsx: -------------------------------------------------------------------------------- 1 | 2 | import Container from './Container'; 3 | 4 | export default Container; 5 | -------------------------------------------------------------------------------- /src/components/offline/Container.if.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface IOfflineProps { 3 | title: string; 4 | subtitle: string; 5 | iconName: "plug"|"spinner"; 6 | } 7 | -------------------------------------------------------------------------------- /src/components/offline/Container.style.ts: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react'; 3 | import { IOfflineProps } from './Container.if'; 4 | 5 | 6 | type Element = "Container" | "Col" | "P" | "Row" | "Icon"; 7 | 8 | const DEFAULT_STYLE: Record = { 9 | CONTAINER: { 10 | backgroundColor: '#000', 11 | paddingLeft: '0px', 12 | paddingRight: '0px', 13 | paddingTop: '18vh', 14 | paddingBottom: '18vh', 15 | width: '100%', 16 | height: '100%' 17 | }, 18 | COL: { 19 | height: '100%', 20 | width: '100%' 21 | }, 22 | P: { 23 | height: '100%', 24 | width: '100%', 25 | margin: 0, 26 | fontFamily: 'Inconsolata', 27 | }, 28 | ROW: { 29 | fontSize: '12vmin', 30 | textAlign: 'center', 31 | color: '#fff', 32 | height: '20vmin', 33 | width: '100%', 34 | marginLeft: 0, 35 | marginRight: 0, 36 | }, 37 | ICON: { 38 | fontSize: '25vmin', 39 | textAlign: 'center', 40 | color: '#fff' 41 | } 42 | }; 43 | 44 | function stylingForElement(element: Element, props: IOfflineProps): React.CSSProperties { 45 | let styling: React.CSSProperties = {}; 46 | 47 | switch (element) { 48 | case 'Container': 49 | Object.assign(styling, DEFAULT_STYLE.CONTAINER); 50 | break; 51 | 52 | case 'Col': 53 | Object.assign(styling, DEFAULT_STYLE.COL); 54 | break; 55 | 56 | case 'P': 57 | Object.assign(styling, DEFAULT_STYLE.P); 58 | break; 59 | 60 | case 'Row': 61 | Object.assign(styling, DEFAULT_STYLE.ROW); 62 | break; 63 | 64 | case 'Icon': 65 | Object.assign(styling, DEFAULT_STYLE.ICON); 66 | break; 67 | } 68 | 69 | return styling; 70 | } 71 | 72 | export default stylingForElement; 73 | -------------------------------------------------------------------------------- /src/components/offline/Container.tsx: -------------------------------------------------------------------------------- 1 | 2 | import IOC from '../../ioc' 3 | import { ILogger } from '../../interfaces/log'; 4 | import Icon from '../common/icon'; 5 | import { IOfflineProps } from './Container.if'; 6 | import styling from './Container.style'; 7 | 8 | 9 | const iconForStatus = (statusVal: "plug"|"spinner", style: React.CSSProperties = {}, log: ILogger = IOC().logger()): JSX.Element => { 10 | let elem = Icon.ExclamationTriangle(); 11 | 12 | switch (statusVal) { 13 | case "plug": 14 | elem = Icon.Plug(style); 15 | break; 16 | 17 | case "spinner": 18 | elem = Icon.Spinner(style); 19 | break; 20 | 21 | default: 22 | log.error('Unrecognised status: ' + statusVal); 23 | } 24 | 25 | return elem; 26 | } 27 | 28 | function Container(props: IOfflineProps) { 29 | return ( 30 |
34 |
35 |
36 |

37 | {props.title} 38 |

39 |
40 |
41 | 42 |
43 |
44 | {iconForStatus(props.iconName,styling('Icon', props))} 45 |
46 |
47 | 48 |
49 |
50 |

51 | {props.subtitle} 52 |

53 |
54 |
55 |
56 | ); 57 | } 58 | 59 | 60 | export default Container; 61 | -------------------------------------------------------------------------------- /src/components/offline/index.tsx: -------------------------------------------------------------------------------- 1 | 2 | import Container from './Container'; 3 | 4 | function Root(props: any) { 5 | return ( 6 | 7 | ); 8 | } 9 | 10 | export default Root; 11 | -------------------------------------------------------------------------------- /src/config/index.di.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Container } from "inversify"; 3 | import { IInjectable } from "../interfaces/di"; 4 | import { IConfig } from "../interfaces/config"; 5 | import { IEnv } from "../interfaces/env"; 6 | import config from './index'; 7 | 8 | 9 | const type = Symbol.for('IConfig'); 10 | 11 | const exports: IInjectable = { 12 | addBinding: (container) => { 13 | const env = container.get(Symbol.for('IEnv')); 14 | container 15 | .bind(type) 16 | .toConstantValue(config(env)); 17 | }, 18 | resolve: (container: Container) => { 19 | return container.get(type); 20 | }, 21 | type, 22 | } 23 | 24 | export default exports; 25 | -------------------------------------------------------------------------------- /src/config/index.ts: -------------------------------------------------------------------------------- 1 | 2 | import IOC from '../ioc'; 3 | import { IConfig } from '../interfaces/config'; 4 | import { IEnv } from '../interfaces/env'; 5 | import logging from './logging'; 6 | import speedtest from './speedtest'; 7 | import ping from './ping'; 8 | import threshold from './threshold'; 9 | import view from './view'; 10 | 11 | 12 | const exports = (env: IEnv = IOC().env()): IConfig => { 13 | return { 14 | logging: logging(env), 15 | speedtest: speedtest(env), 16 | threshold: threshold(env), 17 | view: view(env), 18 | ping: ping(env), 19 | } 20 | }; 21 | 22 | export default exports -------------------------------------------------------------------------------- /src/config/logging.ts: -------------------------------------------------------------------------------- 1 | 2 | import { IEnv } from '../interfaces/env'; 3 | import IOC from '../ioc'; 4 | 5 | 6 | const exports = (env: IEnv = IOC().env()) => { 7 | return { 8 | levels: { 9 | verbose: env.getBool('REACT_APP_ENABLELOGGINGVERBOSE', false), 10 | info: env.getBool('REACT_APP_ENABLELOGGINGINFO', false), 11 | warn: env.getBool('REACT_APP_ENABLELOGGINGWARN', true), 12 | error: env.getBool('REACT_APP_ENABLELOGGINGERROR', true), 13 | fatal: env.getBool('REACT_APP_ENABLELOGGINGFATAL', true), 14 | }, 15 | } 16 | }; 17 | 18 | export default exports 19 | -------------------------------------------------------------------------------- /src/config/ping.ts: -------------------------------------------------------------------------------- 1 | 2 | import { IEnv } from '../interfaces/env'; 3 | import IOC from '../ioc'; 4 | 5 | 6 | const DEFAULTS = { 7 | TESTINTERVAL: 15, 8 | WEBSITES: ["https://www.google.com", "https://www.youtube.com", "https://www.twitter.com", "https://www.facebook.com", "https://www.ebay.com", "https://www.walmart.com", "https://www.bing.com"] 9 | }; 10 | 11 | const exports = (env: IEnv = IOC().env()) => { 12 | return { 13 | testInterval : env.getInt('REACT_APP_PINGINTERVAL', DEFAULTS.TESTINTERVAL), 14 | servers : env.getArray('REACT_APP_PINGWEBSITES', DEFAULTS.WEBSITES), 15 | } 16 | }; 17 | 18 | export default exports 19 | -------------------------------------------------------------------------------- /src/config/speedtest.ts: -------------------------------------------------------------------------------- 1 | 2 | import { IEnv } from '../interfaces/env'; 3 | import IOC from '../ioc'; 4 | 5 | 6 | const DEFAULTS_TESTINTERVAL = 300; 7 | const DEFAULTS_SERVERCONFIGURATIONS = [{"name":"Amsterdam, Netherlands (Clouvider)","server":"//ams.speedtest.clouvider.net/backend","id":51,"dlURL":"garbage.php","ulURL":"empty.php","pingURL":"empty.php","getIpURL":"getIP.php","sponsorName":"Clouvider","sponsorURL":"https://www.clouvider.co.uk/"},{"name":"Atlanta, United States (Clouvider)","server":"//atl.speedtest.clouvider.net/backend","id":53,"dlURL":"garbage.php","ulURL":"empty.php","pingURL":"empty.php","getIpURL":"getIP.php","sponsorName":"Clouvider","sponsorURL":"https://www.clouvider.co.uk/"},{"name":"Austria, Klagenfurt (A1 Telekom)","server":"https://cloudfare.at/","id":56,"dlURL":"backend/garbage.php","ulURL":"backend/empty.php","pingURL":"backend/empty.php","getIpURL":"backend/getIP.php","sponsorName":"Lavis","sponsorURL":"https://cloudfare.at/"},{"name":"Bari, Italy (GARR)","server":"https://st-be-ba1.infra.garr.it","id":33,"dlURL":"garbage.php","ulURL":"empty.php","pingURL":"empty.php","getIpURL":"getIP.php","sponsorName":"Consortium GARR","sponsorURL":"//garr.it"},{"name":"Bologna, Italy (GARR)","server":"https://st-be-bo1.infra.garr.it","id":34,"dlURL":"garbage.php","ulURL":"empty.php","pingURL":"empty.php","getIpURL":"getIP.php","sponsorName":"Consortium GARR","sponsorURL":"//garr.it"},{"name":"Frankfurt, Germany (Clouvider)","server":"//fra.speedtest.clouvider.net/backend","id":50,"dlURL":"garbage.php","ulURL":"empty.php","pingURL":"empty.php","getIpURL":"getIP.php","sponsorName":"Clouvider","sponsorURL":"https://www.clouvider.co.uk/"},{"name":"Frankfurt, Germany (DigitalOcean)","server":"//speed2.lukas-heinrich.com/","id":47,"dlURL":"garbage.php","ulURL":"empty.php","pingURL":"empty.php","getIpURL":"getIP.php","sponsorName":"luki9100","sponsorURL":"https://lukas-heinrich.com/"},{"name":"Helsinki, Finland (1) (Hetzner)","server":"//fi1.backend.librespeed.org/","id":20,"dlURL":"garbage.php","ulURL":"empty.php","pingURL":"empty.php","getIpURL":"getIP.php","sponsorName":"Snopyta","sponsorURL":"https://snopyta.org"},{"name":"Helsinki, Finland (2) (Hetzner)","server":"https://fi2.backend.librespeed.org:8443/","id":21,"dlURL":"garbage.php","ulURL":"empty.php","pingURL":"empty.php","getIpURL":"getIP.php","sponsorName":"Snopyta","sponsorURL":"https://snopyta.org"},{"name":"Helsinki, Finland (3) (Hetzner)","server":"//fi.openspeed.org/","id":22,"dlURL":"garbage.php","ulURL":"empty.php","pingURL":"empty.php","getIpURL":"getIP.php","sponsorName":"B2B Risk","sponsorURL":null},{"name":"Helsinki, Finland (5) (Hetzner)","server":"//fast.kabi.tk/","id":24,"dlURL":"garbage.php","ulURL":"empty.php","pingURL":"empty.php","getIpURL":"getIP.php","sponsorName":"KABI.tk","sponsorURL":"//kabi.tk"},{"name":"Indianapolis, United States ","server":"https://speedtest.chet.space/","id":55,"dlURL":"backend/garbage.php","ulURL":"backend/empty.php","pingURL":"backend/empty.php","getIpURL":"backend/getIP.php","sponsorName":"Erda-Networks.com","sponsorURL":"http://blog.chet.space"},{"name":"Las Vegas, United States (BuyVM)","server":"https://lv1.backend.librespeed.nixnet.services/","id":37,"dlURL":"garbage.php","ulURL":"empty.php","pingURL":"empty.php","getIpURL":"getIP.php","sponsorName":"NixNet","sponsorURL":"https://nixnet.services"},{"name":"London, England (Clouvider)","server":"//lon.speedtest.clouvider.net/backend","id":49,"dlURL":"garbage.php","ulURL":"empty.php","pingURL":"empty.php","getIpURL":"getIP.php","sponsorName":"Clouvider","sponsorURL":"https://www.clouvider.co.uk/"},{"name":"Los Angeles, United States (Clouvider)","server":"//la.speedtest.clouvider.net/backend","id":54,"dlURL":"garbage.php","ulURL":"empty.php","pingURL":"empty.php","getIpURL":"getIP.php","sponsorName":"Clouvider","sponsorURL":"https://www.clouvider.co.uk/"},{"name":"New York, United States (BuyVM)","server":"https://ny1.backend.librespeed.nixnet.services/","id":38,"dlURL":"garbage.php","ulURL":"empty.php","pingURL":"empty.php","getIpURL":"getIP.php","sponsorName":"NixNet","sponsorURL":"https://nixnet.services"},{"name":"New York, United States (Clouvider)","server":"//nyc.speedtest.clouvider.net/backend","id":52,"dlURL":"garbage.php","ulURL":"empty.php","pingURL":"empty.php","getIpURL":"getIP.php","sponsorName":"Clouvider","sponsorURL":"https://www.clouvider.co.uk/"},{"name":"Nottingham, England (LayerIP)","server":"https://uk1.backend.librespeed.org","id":43,"dlURL":"garbage.php","ulURL":"empty.php","pingURL":"empty.php","getIpURL":"getIP.php","sponsorName":"fosshost.org","sponsorURL":"https://fosshost.org"},{"name":"Nuremberg, Germany (1) (Hetzner)","server":"//de1.backend.librespeed.org","id":28,"dlURL":"garbage.php","ulURL":"empty.php","pingURL":"empty.php","getIpURL":"getIP.php","sponsorName":"Snopyta","sponsorURL":"https://snopyta.org"},{"name":"Nuremberg, Germany (2) (Netcup)","server":"//de2.backend.librespeed.org","id":29,"dlURL":"garbage.php","ulURL":"empty.php","pingURL":"empty.php","getIpURL":"getIP.php","sponsorName":"NixNet","sponsorURL":"https://nixnet.services"},{"name":"Nuremberg, Germany (3) (Hetzner)","server":"//de3.backend.librespeed.org","id":30,"dlURL":"garbage.php","ulURL":"empty.php","pingURL":"empty.php","getIpURL":"getIP.php","sponsorName":null,"sponsorURL":null},{"name":"Nuremberg, Germany (4) (Hetzner)","server":"//de5.backend.librespeed.org","id":31,"dlURL":"garbage.php","ulURL":"empty.php","pingURL":"empty.php","getIpURL":"getIP.php","sponsorName":null,"sponsorURL":null},{"name":"Nuremberg, Germany (5) (Hetzner)","server":"https://speedtest.ciapa.tech/","id":32,"dlURL":"garbage.php","ulURL":"empty.php","pingURL":"empty.php","getIpURL":"getIP.php","sponsorName":null,"sponsorURL":null},{"name":"Nuremberg, Germany (6) (Hetzner)","server":"//librespeed.lukas-heinrich.com/","id":46,"dlURL":"garbage.php","ulURL":"empty.php","pingURL":"empty.php","getIpURL":"getIP.php","sponsorName":"luki9100","sponsorURL":"https://lukas-heinrich.com/"},{"name":"Paris, France (Hurricane Electric)","server":"//librespeed.louifox.house/","id":25,"dlURL":"backend/garbage.php","ulURL":"backend/empty.php","pingURL":"backend/empty.php","getIpURL":"backend/getIP.php","sponsorName":"LouiFox.House","sponsorURL":"https://louifox.house"},{"name":"Paris, France (online.net)","server":"//fr1.backend.librespeed.org","id":26,"dlURL":"garbage.php","ulURL":"empty.php","pingURL":"empty.php","getIpURL":"getIP.php","sponsorName":"lelux.fi","sponsorURL":"https://lelux.fi"},{"name":"Roma, Italy (GARR)","server":"https://st-be-rm2.infra.garr.it","id":35,"dlURL":"garbage.php","ulURL":"empty.php","pingURL":"empty.php","getIpURL":"getIP.php","sponsorName":"Consortium GARR","sponsorURL":"//garr.it"},{"name":"Roost, Luxembourg (BuyVM)","server":"https://lux1.backend.librespeed.nixnet.services/","id":39,"dlURL":"garbage.php","ulURL":"empty.php","pingURL":"empty.php","getIpURL":"getIP.php","sponsorName":"NixNet","sponsorURL":"https://nixnet.services"},{"name":"Singapore, Singapore (OVH)","server":"https://sgp.check.lewd.wtf/","id":48,"dlURL":"garbage.php","ulURL":"empty.php","pingURL":"empty.php","getIpURL":"getIP.php","sponsorName":null,"sponsorURL":null},{"name":"Strasbourg, France (Host Europe)","server":"https://sxb.bandspeed.de/","id":44,"dlURL":"backend/garbage.php","ulURL":"backend/empty.php","pingURL":"backend/empty.php","getIpURL":"backend/getIP.php","sponsorName":"adminForge","sponsorURL":"https://adminforge.de/"},{"name":"Sydney, Australia (Linode)","server":"//librespeed.fossdaily.xyz","id":36,"dlURL":"garbage.php","ulURL":"empty.php","pingURL":"empty.php","getIpURL":"getIP.php","sponsorName":"Foss Daily","sponsorURL":"https://fossdaily.xyz/"}] 8 | 9 | const exports = (env: IEnv = IOC().env()) => { 10 | return { 11 | testInterval : env.getInt('REACT_APP_TESTINTERVAL', DEFAULTS_TESTINTERVAL), 12 | serverConfigurations: env.getArray('REACT_APP_SERVERCONFIGURATONS', DEFAULTS_SERVERCONFIGURATIONS), 13 | } 14 | }; 15 | 16 | export default exports 17 | -------------------------------------------------------------------------------- /src/config/threshold.ts: -------------------------------------------------------------------------------- 1 | 2 | import { IEnv } from '../interfaces/env'; 3 | import IOC from '../ioc'; 4 | 5 | 6 | const DEFAULTS = { 7 | UPLOAD: { 8 | WARNING: 4, 9 | ERROR: 1 10 | }, 11 | DOWNLOAD: { 12 | WARNING: 8, 13 | ERROR: 1 14 | }, 15 | LATENCY: { 16 | WARNING: 40, 17 | ERROR: 100 18 | }, 19 | JITTER: { 20 | WARNING: 50, 21 | ERROR: 100 22 | }, 23 | }; 24 | 25 | const exports = (env: IEnv = IOC().env()) => { 26 | const val = { 27 | upload: { 28 | warning: env.getInt('REACT_APP_UPLOADWARN', DEFAULTS.UPLOAD.WARNING), 29 | error: env.getInt('REACT_APP_UPLOADERROR', DEFAULTS.UPLOAD.ERROR) 30 | }, 31 | download: { 32 | warning: env.getInt('REACT_APP_DOWNLOADWARN', DEFAULTS.DOWNLOAD.WARNING), 33 | error: env.getInt('REACT_APP_DOWNLOADERROR', DEFAULTS.DOWNLOAD.ERROR) 34 | }, 35 | latency: { 36 | warning: env.getInt('REACT_APP_LATENCYWARN', DEFAULTS.LATENCY.WARNING), 37 | error: env.getInt('REACT_APP_LATENCYERROR', DEFAULTS.LATENCY.ERROR) 38 | }, 39 | jitter: { 40 | warning: env.getInt('REACT_APP_JITTERWARN', DEFAULTS.JITTER.WARNING), 41 | error: env.getInt('REACT_APP_JITTERERROR', DEFAULTS.JITTER.ERROR) 42 | }, 43 | }; 44 | return val; 45 | }; 46 | 47 | export default exports; 48 | -------------------------------------------------------------------------------- /src/config/view.ts: -------------------------------------------------------------------------------- 1 | 2 | import { IEnv } from '../interfaces/env'; 3 | import IOC from '../ioc'; 4 | 5 | 6 | const DEFAULTS_REFRESHPAGEINTERVAL = 60; 7 | 8 | const exports = (env: IEnv = IOC().env()) => { 9 | return { 10 | refreshPageInterval : env.getInt('REACT_VIEW_TESTINTERVAL', DEFAULTS_REFRESHPAGEINTERVAL), 11 | } 12 | }; 13 | 14 | export default exports 15 | -------------------------------------------------------------------------------- /src/controllers/dashboard.map.ts: -------------------------------------------------------------------------------- 1 | 2 | import { IConfig } from "../interfaces/config"; 3 | import IOC from '../ioc'; 4 | import { IGlobalState } from "../state/interface"; 5 | import { DashboardStatusValue, IDashboardProps, IDashboardStatusItem } from "../components/dashboard/Container.if"; 6 | import util from './dashboard.util'; 7 | 8 | 9 | const mapStateToProps = (state: IGlobalState, config: IConfig = IOC().config()): IDashboardProps => { 10 | const latencyProps: IDashboardStatusItem = { 11 | name : "LATENCY", 12 | value : Math.round(state?.SpeedTest?.latency ?? -1) + "ms", 13 | status : (state?.SpeedTest?.latency ?? config.threshold.latency.error) >= config.threshold.latency.error 14 | ? DashboardStatusValue.Bad 15 | : state?.SpeedTest?.latency > config.threshold.latency.warning 16 | ? DashboardStatusValue.Warning 17 | : DashboardStatusValue.Good 18 | }; 19 | 20 | const jitterProps: IDashboardStatusItem = { 21 | name : "JITTER", 22 | value : Math.round(state?.SpeedTest?.jitter ?? -1) + "ms", 23 | status : (state?.SpeedTest?.jitter ?? config.threshold.jitter.error) >= config.threshold.jitter.error 24 | ? DashboardStatusValue.Bad 25 | : state?.SpeedTest?.jitter > config.threshold.jitter.warning 26 | ? DashboardStatusValue.Warning 27 | : DashboardStatusValue.Good 28 | }; 29 | 30 | const ulProps: IDashboardStatusItem = { 31 | name : "UPLOAD", 32 | value : Math.round(state?.SpeedTest?.uploadSpeed ?? -1) + "Mbps", 33 | status : state?.SpeedTest?.uploadSpeed <= config.threshold.upload.error 34 | ? DashboardStatusValue.Bad 35 | : state?.SpeedTest?.uploadSpeed < config.threshold.upload.warning 36 | ? DashboardStatusValue.Warning 37 | : DashboardStatusValue.Good 38 | }; 39 | 40 | const dlProps: IDashboardStatusItem = { 41 | name : "DOWNLOAD", 42 | value : Math.round(state?.SpeedTest?.downloadSpeed ?? -1) + "Mbps", 43 | status : state?.SpeedTest?.downloadSpeed < config.threshold.download.error 44 | ? DashboardStatusValue.Bad 45 | : state?.SpeedTest?.downloadSpeed < config.threshold.download.warning 46 | ? DashboardStatusValue.Warning 47 | : DashboardStatusValue.Good 48 | }; 49 | 50 | const isTestRunning = state?.SpeedTest?.isTestRunning ?? false; 51 | const dateOfLastTest = state?.SpeedTest?.dateOfLastTest ?? null; 52 | const clientIp = state?.SpeedTest?.clientIp ?? state?.SpeedTest?.ispInfo ?? 'missing'; 53 | const infoTextLeft = isTestRunning 54 | ? '' 55 | : util.describeDifferenceBetweenDates(dateOfLastTest, new Date()); 56 | 57 | return { 58 | statusItems: [latencyProps, jitterProps, dlProps, ulProps], 59 | showInfoIcon: true, 60 | infoIcon: isTestRunning ? "wifi" : "clock", 61 | infoTextLeft: infoTextLeft, 62 | infoTextRight: clientIp, 63 | infoIconRight: "map-marker", 64 | refreshPageInterval: Math.max(1, config.view.refreshPageInterval) 65 | 66 | }; 67 | }; 68 | 69 | export default mapStateToProps; 70 | -------------------------------------------------------------------------------- /src/controllers/dashboard.tsx: -------------------------------------------------------------------------------- 1 | 2 | import React, { useEffect } from 'react'; 3 | import {connect} from "react-redux"; 4 | import View from '../components/dashboard'; 5 | import { IDashboardProps } from "../components/dashboard/Container.if"; 6 | import mapping from './dashboard.map'; 7 | 8 | 9 | function Controller(props: IDashboardProps): JSX.Element { 10 | /* redraw page, to trigger seconds since last update timer */ 11 | const [, updateState] = React.useState<{}>(); 12 | const forceUpdate = React.useCallback(() => updateState({}), []); 13 | 14 | useEffect(() => { 15 | const timer = setInterval(() => { 16 | forceUpdate(); 17 | }, 18 | 1000 * props.refreshPageInterval); 19 | clearInterval(timer); 20 | }, [props, forceUpdate]) 21 | 22 | return ( 23 | 24 | ) 25 | } 26 | 27 | export default connect(mapping)(Controller); 28 | -------------------------------------------------------------------------------- /src/controllers/dashboard.util.ts: -------------------------------------------------------------------------------- 1 | 2 | const secondsToMinutes = (tVal: number) => (tVal / 60); 3 | const secondsToHours = (tVal: number) => (tVal / 60 / 60); 4 | const secondsToDays = (tVal: number) => (tVal / 60 / 60 / 24); 5 | const minutesToSeconds = (tVal: number) => (tVal * 60); 6 | const hoursToSeconds = (tVal: number) => (tVal * 60 * 60); 7 | const daysToSeconds = (tVal: number) => (tVal * 60 * 60 * 24); 8 | 9 | const describeDifferenceBetweenDates = function (startDate: Date, endDate: Date): string { 10 | if (!startDate) { 11 | return ''; 12 | } 13 | if (!endDate) { 14 | return ''; 15 | } 16 | 17 | const deltaSeconds = Math.round(Math.abs(startDate.getTime() - endDate.getTime()) / 1000) 18 | 19 | switch (true) { 20 | case(deltaSeconds < 1): 21 | return 'now'; 22 | 23 | case(deltaSeconds < 60): 24 | return deltaSeconds + 's'; 25 | 26 | case(deltaSeconds < minutesToSeconds(60)): 27 | { 28 | const count = Math.round(secondsToMinutes(deltaSeconds)) 29 | return count + 'min' + (count > 1 30 | ? 's' 31 | : ''); 32 | } 33 | 34 | case(deltaSeconds < hoursToSeconds(24)): 35 | { 36 | const count = Math.round(secondsToHours(deltaSeconds)) 37 | return count + 'hr' + (count > 1 38 | ? 's' 39 | : ''); 40 | } 41 | 42 | default: 43 | { 44 | const count = Math.round(secondsToDays(deltaSeconds)) 45 | return count + 'day' + (count > 1 46 | ? 's' 47 | : ''); 48 | } 49 | } 50 | } 51 | 52 | const exports = { 53 | secondsToMinutes, 54 | secondsToHours, 55 | secondsToDays, 56 | minutesToSeconds, 57 | hoursToSeconds, 58 | daysToSeconds, 59 | describeDifferenceBetweenDates, 60 | } 61 | 62 | export default exports; 63 | -------------------------------------------------------------------------------- /src/controllers/loading.map.ts: -------------------------------------------------------------------------------- 1 | 2 | import { ILoadingProps } from "../components/loading/Container.if"; 3 | import { IGlobalState } from "../state/interface"; 4 | 5 | 6 | const mapStateToProps = (state: IGlobalState, props: ILoadingProps): ILoadingProps => { 7 | return { 8 | title: 'FETCHING NETWORK SPEED', 9 | subtitle: "(delay upto 1min)", 10 | iconName: "spinner", 11 | }; 12 | } 13 | 14 | export default mapStateToProps; 15 | -------------------------------------------------------------------------------- /src/controllers/loading.tsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react'; 3 | import {connect} from "react-redux"; 4 | import View from '../components/loading'; 5 | import { ILoadingProps } from "../components/loading/Container.if"; 6 | import mappings from './loading.map'; 7 | 8 | 9 | function Controller(props: ILoadingProps): JSX.Element { 10 | return ( 11 | 12 | ) 13 | } 14 | 15 | export default connect(mappings)(Controller); 16 | -------------------------------------------------------------------------------- /src/controllers/offline.map.ts: -------------------------------------------------------------------------------- 1 | 2 | import { IOfflineProps } from "../components/offline/Container.if"; 3 | import { IGlobalState } from "../state/interface"; 4 | 5 | const dateDeltaDays = (dateLow: Date, dateHigh: Date) => { 6 | const deltaMS = Math.abs(dateHigh.getTime() - dateLow.getTime()); 7 | const deltaS = deltaMS / 1000; 8 | const deltaMin = deltaS / 60; 9 | const deltaHr = deltaMin / 60; 10 | const deltaDay = deltaHr / 24; 11 | return deltaDay; 12 | } 13 | 14 | const mapStateToProps = (state: IGlobalState): IOfflineProps => { 15 | const dateNow = new Date(); 16 | const dateOffline = state.OnlineStatus.dateWasLastOnline; 17 | 18 | let formatParams: Intl.DateTimeFormatOptions = { hour: 'numeric', minute: 'numeric' } 19 | 20 | /* show the date if the time elapsed offline is > 24hrs */ 21 | if ( dateDeltaDays(dateOffline, dateNow) >= 1 ) { 22 | Object.assign(formatParams, { year: 'numeric', month: 'numeric', day: 'numeric' }); 23 | } 24 | 25 | const timeFormat = new Intl.DateTimeFormat('default', formatParams); 26 | 27 | return { 28 | title: "Connection Lost!", 29 | subtitle: `Last online at ${timeFormat.format(dateOffline)}`, 30 | iconName: "plug", 31 | }; 32 | } 33 | 34 | export default mapStateToProps; 35 | -------------------------------------------------------------------------------- /src/controllers/offline.tsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react'; 3 | import {connect} from "react-redux"; 4 | import View from '../components/offline'; 5 | import { IOfflineProps } from "../components/offline/Container.if"; 6 | import mapping from './offline.map'; 7 | 8 | 9 | function Controller(props: IOfflineProps): JSX.Element { 10 | return ( 11 | 12 | ) 13 | } 14 | 15 | export default connect(mapping)(Controller); 16 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | 2 | * { padding: 0; margin: 0; } 3 | 4 | html, body { 5 | margin: 0; 6 | font-family: Inconsolata, "Segoe UI", "Roboto", "Oxygen", 7 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 8 | sans-serif; 9 | 10 | -webkit-font-smoothing: antialiased; 11 | -moz-osx-font-smoothing: grayscale; 12 | font-smooth: always; 13 | 14 | /* Prevent user selecting any text on screen */ 15 | -webkit-touch-callout: none; 16 | -webkit-tap-highlight-color: rgba(0,0,0,0); 17 | -webkit-tap-highlight-color: transparent; 18 | -webkit-touch-callout: none; 19 | -webkit-user-select: none; 20 | -khtml-user-select: none; 21 | -moz-user-select: none; 22 | -ms-user-select: none; 23 | user-select: none; 24 | 25 | background-color: '#000'; 26 | width: '100%'; 27 | height: '100%'; 28 | overflow: 'hidden'; 29 | } 30 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import {Provider} from "react-redux"; 4 | import './index.css'; 5 | import reportWebVitals from './reportWebVitals'; 6 | import Router from './router'; 7 | import serviceInit from './services'; 8 | import { IServiceStatus } from './services/interface'; 9 | import state from './state'; 10 | import IOC from './ioc'; 11 | 12 | 13 | const services = serviceInit(); 14 | const providerStore = state.store() 15 | 16 | const startServices = () => services.allServices.map((service) => service.start()); 17 | const updateServices = () => services.allServices.map((service) => service.forceUpdate()); 18 | 19 | window.addEventListener('load', () => { 20 | Promise.all(startServices()) 21 | .then(()=>IOC().logger().info("Services started")) 22 | 23 | const rootElem = document.getElementById('root'); 24 | if ( rootElem ) { 25 | rootElem.onclick = function() { 26 | Promise.all(updateServices()) 27 | .then(()=>IOC().logger().info("Services statuses force-updated")) 28 | } 29 | } 30 | }); 31 | 32 | services.onlineStatus.subscribeForUpdates("OnlineStatus-CB", (service) => { 33 | const isOnline = service.state.isOnline; 34 | const action = state.onlinestatus.dispatcher.isOnline(providerStore, isOnline); 35 | providerStore.dispatch(action); 36 | }) 37 | 38 | services.networkSpeed.subscribeForUpdates("NetSpeed-CB", (service) => { 39 | switch ( service.status ) { 40 | case IServiceStatus.Busy: 41 | { 42 | const action = state.networkspeed.dispatcher.speedTestStarted(providerStore); 43 | providerStore.dispatch(action); 44 | } 45 | break; 46 | 47 | case IServiceStatus.Idle: 48 | { 49 | const actionEnded = state.networkspeed.dispatcher.speedTestEnded(providerStore); 50 | providerStore.dispatch(actionEnded); 51 | const actionUpdate = state.networkspeed.dispatcher.updateResults(providerStore, service.state); 52 | providerStore.dispatch(actionUpdate); 53 | } 54 | break; 55 | } 56 | }) 57 | 58 | providerStore.subscribe(() => { 59 | const history = Router.history(); 60 | if ( !history ) { return; } 61 | 62 | const state = providerStore.getState(); 63 | 64 | const isShowingDashboard = Router.routes.dashboard.isShowing(history); 65 | const isOnline = state.OnlineStatus.isOnline; 66 | const isFetchingNetworkSpeed = !( services.networkSpeed.status === IServiceStatus.Idle ); /* it may be uninit'ed at this point, so don't assume == Busy */ 67 | const hasFinishedInitialLoading = (state?.SpeedTest?.dateOfLastTest ?? 0) > 0; 68 | 69 | const showOffline = (!isFetchingNetworkSpeed || isShowingDashboard) && !isOnline; 70 | const showDashboard = isOnline && hasFinishedInitialLoading; 71 | 72 | if ( showOffline ) { 73 | Router.routes.offline.navigate(history); 74 | } else if ( showDashboard ) { 75 | Router.routes.dashboard.navigate(history) 76 | } 77 | }) 78 | 79 | ReactDOM.render( 80 | 81 | 82 | 83 | 84 | , 85 | document.getElementById('root')); 86 | 87 | if (!process.env.NODE_ENV || process.env.NODE_ENV === 'development') { 88 | reportWebVitals(console.log); 89 | } 90 | -------------------------------------------------------------------------------- /src/interfaces/config.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface ILog { 3 | levels: { 4 | verbose: boolean; 5 | info: boolean; 6 | warn: boolean; 7 | error: boolean; 8 | fatal: boolean; 9 | } 10 | } 11 | 12 | export interface IPing { 13 | testInterval: number; 14 | servers: string[]; 15 | } 16 | 17 | export interface IServerConfiguration { 18 | name: string; 19 | server: string; 20 | id: number; 21 | dlURL: string; 22 | ulURL: string; 23 | pingURL: string; 24 | getIpURL: string; 25 | sponsorName: string|null; 26 | sponsorURL: string|null; 27 | } 28 | 29 | export interface ISpeedTest { 30 | testInterval: number; 31 | serverConfigurations: Array; 32 | } 33 | 34 | export interface IThreshold { 35 | upload: { 36 | warning: number; 37 | error: number; 38 | }, 39 | download: { 40 | warning: number; 41 | error: number; 42 | }, 43 | latency: { 44 | warning: number; 45 | error: number; 46 | }, 47 | jitter: { 48 | warning: number; 49 | error: number; 50 | }, 51 | } 52 | 53 | export interface IView { 54 | refreshPageInterval: number; 55 | } 56 | 57 | export interface IConfig { 58 | logging: ILog, 59 | ping: IPing, 60 | speedtest: ISpeedTest, 61 | threshold: IThreshold, 62 | view: IView, 63 | } 64 | -------------------------------------------------------------------------------- /src/interfaces/di.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Container } from "inversify"; 3 | 4 | 5 | export interface IInjectable { 6 | addBinding: (container: Container) => void; 7 | resolve: (container: Container) => T 8 | type: Symbol; 9 | } 10 | -------------------------------------------------------------------------------- /src/interfaces/env.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface IEnv { 3 | getInt : (envName: string, defaultValue?: number) => number; 4 | getBool : (envName: string, defaultValue?: boolean) => boolean; 5 | getObject : (envName: string, defaultValue?: object) => object; 6 | getArray : (envName: string, defaultValue?: Array) => Array; 7 | } 8 | -------------------------------------------------------------------------------- /src/interfaces/log.ts: -------------------------------------------------------------------------------- 1 | 2 | export enum LOGSEVERITY { 3 | FATAL, 4 | ERROR, 5 | WARN, 6 | INFO, 7 | VERBOSE, 8 | } 9 | 10 | export interface ILogger { 11 | fatal: (msg: string) => void; 12 | error: (msg: string) => void; 13 | warn: (msg: string) => void; 14 | info: (msg: string) => void; 15 | verbose: (msg: string) => void; 16 | } 17 | -------------------------------------------------------------------------------- /src/ioc.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Container } from "inversify"; 3 | 4 | import { IEnv } from "./interfaces/env"; 5 | import { ILogger } from "./interfaces/log"; 6 | import { IConfig } from "./interfaces/config"; 7 | 8 | import LogDi from './util/log.di'; 9 | import EnvDI from './util/env.di'; 10 | import ConfigDI from './config/index.di'; 11 | 12 | 13 | const symbol = Symbol.for('IOC'); 14 | 15 | export interface IIOC { 16 | logger: () => ILogger; 17 | env: () => IEnv; 18 | config: () => IConfig; 19 | } 20 | 21 | const newContainer = (): IIOC => { 22 | 23 | const container = new Container(); 24 | 25 | EnvDI.addBinding(container); 26 | ConfigDI.addBinding(container); 27 | LogDi.addBinding(container); 28 | 29 | const mapped: IIOC = { 30 | env: () => EnvDI.resolve(container), 31 | config: () => ConfigDI.resolve(container), 32 | logger: () => LogDi.resolve(container), 33 | }; 34 | 35 | (global as any)[symbol] = mapped; 36 | 37 | return mapped; 38 | } 39 | 40 | const getIOC = (): IIOC|null => { 41 | return (global as any)[symbol]; 42 | } 43 | 44 | const get = (): IIOC => { 45 | let ioc: IIOC|null = getIOC(); 46 | if ( !ioc ) { 47 | ioc = newContainer(); 48 | } 49 | return ioc; 50 | }; 51 | 52 | export default get 53 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /src/router/dashboard.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { RouteComponentProps } from 'react-router-dom'; 3 | import { History } from 'history'; 4 | import { IRoute } from './interface'; 5 | import Controller from '../controllers/dashboard'; 6 | import util from './util'; 7 | 8 | 9 | const route: IRoute = { 10 | path: "/dashboard", 11 | isShowing: (history: History) => util.historyMatchesLocation(history, route.path), 12 | component: (props: any) => , 13 | navigate: (history: History, props?: RouteComponentProps<{}>) => { 14 | if ( route.isShowing(history) ) { return; } 15 | history.push(route.path) 16 | } 17 | } 18 | 19 | export default route; -------------------------------------------------------------------------------- /src/router/history.tsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react'; 3 | import { withRouter } from 'react-router'; 4 | import { History } from 'history'; 5 | 6 | 7 | let globalHistory: History|null = null; 8 | 9 | class HistoryFetch extends React.Component { 10 | constructor(props: any) { 11 | super(props); 12 | globalHistory = props.history; 13 | } 14 | 15 | render = () => null; 16 | } 17 | 18 | export const ReactRouterSaveHistory = withRouter(HistoryFetch); 19 | 20 | const exports = { 21 | ReactRouterSaveHistory, 22 | getHistory: (): History|null => globalHistory, 23 | }; 24 | 25 | export default exports; 26 | -------------------------------------------------------------------------------- /src/router/index.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { Switch, Route, Redirect, BrowserRouter } from "react-router-dom"; 3 | import BrowserHistory from './history'; 4 | import { IRoute } from './interface'; 5 | import loading from './loading'; 6 | import offline from './offline'; 7 | import dashboard from './dashboard'; 8 | 9 | 10 | const routes = { 11 | loading, 12 | dashboard, 13 | offline, 14 | } 15 | 16 | function Render(): JSX.Element { 17 | const routesComponents = Object.values(routes).map( 18 | ((route: IRoute) => { 19 | return 25 | }) 26 | ) 27 | 28 | return ( 29 | 30 | 31 | 32 | {routesComponents} 33 | 34 | 35 | 36 | ) 37 | } 38 | 39 | const exports = { 40 | Render, 41 | routes, 42 | history: () => BrowserHistory.getHistory(), 43 | }; 44 | 45 | export default exports; 46 | -------------------------------------------------------------------------------- /src/router/interface.ts: -------------------------------------------------------------------------------- 1 | 2 | import { RouteComponentProps } from 'react-router-dom'; 3 | import { History } from 'history'; 4 | 5 | 6 | export interface IRouteComponentProps { 7 | history: any, 8 | location: any, 9 | match: any, 10 | } 11 | 12 | export interface IRoute { 13 | path: string; 14 | isShowing: (history: History) => boolean; 15 | component: (props: any) => JSX.Element; 16 | navigate: (history: History, props?: RouteComponentProps<{}>) => void; 17 | } 18 | -------------------------------------------------------------------------------- /src/router/loading.tsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react'; 3 | import { RouteComponentProps } from 'react-router-dom'; 4 | import { History } from 'history'; 5 | import { IRoute } from './interface'; 6 | import Controller from '../controllers/loading'; 7 | import util from './util'; 8 | 9 | 10 | const route: IRoute = { 11 | path: "/loading", 12 | isShowing: (history: History) => util.historyMatchesLocation(history, route.path), 13 | component: (props: any) => { 14 | return ( 15 | 16 | ) 17 | } , 18 | navigate: (history: History, props?: RouteComponentProps<{}>) => { 19 | if ( route.isShowing(history) ) { return; } 20 | history.push(route.path) 21 | } 22 | } 23 | 24 | export default route; 25 | -------------------------------------------------------------------------------- /src/router/offline.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { RouteComponentProps } from 'react-router-dom'; 3 | import { History } from 'history'; 4 | import { IRoute } from './interface'; 5 | import Controller from '../controllers/offline'; 6 | import util from './util'; 7 | 8 | 9 | const route: IRoute = { 10 | path: "/offline", 11 | isShowing: (history: History) => util.historyMatchesLocation(history, route.path), 12 | component: (props: any) => , 13 | navigate: (history: History, props?: RouteComponentProps<{}>) => { 14 | if ( route.isShowing(history) ) { return; } 15 | history.push(route.path) 16 | } 17 | } 18 | 19 | export default route; 20 | -------------------------------------------------------------------------------- /src/router/util.ts: -------------------------------------------------------------------------------- 1 | 2 | import { History } from 'history'; 3 | 4 | 5 | function historyMatchesLocation(history: History, path: string): boolean { 6 | let location = history.location.pathname; 7 | 8 | if ( location === path ) { return true; } 9 | 10 | if ( location[0] === '/' ) { 11 | location = history.location.pathname.substring(1); /* remove leading / */ 12 | } 13 | 14 | const matched = path === location; 15 | 16 | return matched; 17 | } 18 | 19 | const exports = { 20 | historyMatchesLocation, 21 | } 22 | 23 | export default exports; -------------------------------------------------------------------------------- /src/services/index.ts: -------------------------------------------------------------------------------- 1 | 2 | import manager from './manager'; 3 | 4 | export default manager; 5 | -------------------------------------------------------------------------------- /src/services/interface.ts: -------------------------------------------------------------------------------- 1 | 2 | import { INetworkSpeedState } from "./networkspeed/interface"; 3 | import { IOnlineStatusState } from "./onlinestatus/interface"; 4 | 5 | 6 | export interface IServiceManager { 7 | allServices: IService[]; 8 | onlineStatus: IService; 9 | networkSpeed: IService; 10 | } 11 | 12 | export interface IServiceState {} 13 | 14 | export enum IServiceStatus { 15 | Unitialized, 16 | Initialized, 17 | Idle, 18 | Busy, 19 | Stopped, 20 | Error, 21 | } 22 | 23 | export interface IService { 24 | name: string; 25 | status: IServiceStatus; 26 | state: T; 27 | subscribeForUpdates: (subscriberKey: string, callback: (service: IService) => void) => void; 28 | unsubscribeFromUpdates: (subscriberKey: string) => void; 29 | unsubscribeAll: () => void; 30 | start: () => Promise; 31 | stop: () => Promise; 32 | forceUpdate: () => Promise; 33 | } 34 | -------------------------------------------------------------------------------- /src/services/manager.ts: -------------------------------------------------------------------------------- 1 | 2 | import IOC from '../ioc'; 3 | import { IServiceManager } from "./interface"; 4 | import onlinestatus from './onlinestatus'; 5 | import speedtest from './networkspeed'; 6 | import { IOnlineStatusConfig } from "./onlinestatus/interface"; 7 | import { INetworkSpeedConfig } from "./networkspeed/interface"; 8 | 9 | 10 | function manager( 11 | onlineStatusConfig: IOnlineStatusConfig = IOC().config().ping, 12 | networkSpeedConfig: INetworkSpeedConfig = IOC().config().speedtest): IServiceManager { 13 | const onlineStatus = onlinestatus(onlineStatusConfig); 14 | const networkSpeed = speedtest(networkSpeedConfig); 15 | 16 | return { 17 | allServices: [onlineStatus, networkSpeed], 18 | onlineStatus, 19 | networkSpeed, 20 | } 21 | } 22 | 23 | export default manager; 24 | -------------------------------------------------------------------------------- /src/services/networkspeed/index.ts: -------------------------------------------------------------------------------- 1 | 2 | import service from './service'; 3 | 4 | export default service; 5 | -------------------------------------------------------------------------------- /src/services/networkspeed/interface.ts: -------------------------------------------------------------------------------- 1 | 2 | import { IService } from "../interface"; 3 | 4 | 5 | export interface INetworkSpeedService extends IService { 6 | callbackUpdates: Record) => void>; 7 | timer: any; 8 | worker: ISpeedRunner; 9 | } 10 | 11 | export interface ISpeedRunner { 12 | isReady: () => boolean; 13 | ready: () => Promise; 14 | run: () => Promise; 15 | } 16 | 17 | export interface INetworkSpeedState { 18 | clientIp: string; 19 | ispInfo: string; 20 | downloadSpeed: number; 21 | uploadSpeed: number; 22 | latency: number; 23 | jitter: number; 24 | isTestRunning: boolean; 25 | dateOfLastTest?: Date; 26 | 27 | updateWithData: (data: object) => void; 28 | apply: (data: Partial) => INetworkSpeedState; 29 | } 30 | 31 | export enum SpeedTestState { 32 | Idle, 33 | Starting, 34 | Running, 35 | Ended, 36 | } 37 | 38 | export interface IServerConfiguration { 39 | name: string; 40 | server: string; 41 | id: number; 42 | dlURL: string; 43 | ulURL: string; 44 | pingURL: string; 45 | getIpURL: string; 46 | sponsorName: string|null; 47 | sponsorURL: string|null; 48 | } 49 | 50 | export interface INetworkSpeedConfig { 51 | testInterval: number; 52 | serverConfigurations: Array; 53 | } 54 | -------------------------------------------------------------------------------- /src/services/networkspeed/model.ts: -------------------------------------------------------------------------------- 1 | 2 | import { INetworkSpeedState } from './interface'; 3 | import IOC from '../../ioc' 4 | import { ILogger } from '../../interfaces/log'; 5 | 6 | 7 | const searchValue = (objSearch: object, valueSearch: string[], defaultValue: any): any => { 8 | for (var i = 0; i < valueSearch.length; i++) { 9 | const key = valueSearch[i]; 10 | const val = (objSearch as any)[key]; 11 | if (val !== undefined) { 12 | return val; 13 | } 14 | } 15 | return defaultValue; 16 | } 17 | 18 | function _updateWithData( 19 | speedTest: INetworkSpeedState, 20 | data: object, 21 | log: ILogger = IOC().logger()) { 22 | 23 | function clientIp(objSearch: object, defaultValue: any = '') { 24 | var val = searchValue(objSearch, [ 25 | 'clientIp' 26 | ], defaultValue); 27 | return val !== undefined 28 | ? val 29 | : defaultValue; 30 | } 31 | 32 | function ispInfo(objSearch: object, defaultValue: any = '') { 33 | var val = searchValue(objSearch, [ 34 | 'ispInfo' 35 | ], defaultValue); 36 | return val !== undefined 37 | ? val 38 | : defaultValue; 39 | } 40 | 41 | function dlValue(objSearch: object, defaultValue = -1) { 42 | var val = parseFloat(searchValue(objSearch, [ 43 | 'dlStatus', 'downloadSpeed' 44 | ], defaultValue)); 45 | return isNaN(val) 46 | ? defaultValue 47 | : val; 48 | } 49 | 50 | function ulValue(objSearch: object, defaultValue = -1) { 51 | var val = parseFloat(searchValue(objSearch, [ 52 | 'ulStatus', 'uploadSpeed' 53 | ], defaultValue)); 54 | return isNaN(val) 55 | ? defaultValue 56 | : val; 57 | } 58 | 59 | function latencyValue(objSearch: object, defaultValue = -1) { 60 | var val = parseFloat(searchValue(objSearch, [ 61 | 'ping', 'pingStatus', 'latency', 'latencyStatus' 62 | ], defaultValue)); 63 | return isNaN(val) 64 | ? defaultValue 65 | : val; 66 | } 67 | 68 | function jitterValue(objSearch: object, defaultValue = -1) { 69 | var val = parseFloat(searchValue(objSearch, [ 70 | 'jitter', 'jitterStatus' 71 | ], defaultValue)); 72 | return isNaN(val) 73 | ? defaultValue 74 | : val; 75 | } 76 | 77 | function dateOfLastTest(objSearch: object, defaultValue = null) { 78 | var val = searchValue(objSearch, [ 79 | 'dateOfLastTest', 'timeAtLastRun', 'timeOfLastRun' 80 | ], defaultValue); 81 | return val 82 | ? val 83 | : defaultValue; 84 | } 85 | 86 | function isTestRunning(objSearch: object, defaultValue = false) { 87 | if ( !objSearch ) { return defaultValue; } 88 | 89 | if ( (objSearch as any)['isTestRunning'] ?? false ) { 90 | return Boolean((objSearch as any).isTestRunning); 91 | } 92 | 93 | return defaultValue; 94 | } 95 | 96 | if (clientIp(data, null)) { 97 | speedTest.clientIp = clientIp(data); 98 | } 99 | 100 | if (ispInfo(data, null)) { 101 | speedTest.ispInfo = ispInfo(data); 102 | } 103 | 104 | if (dlValue(data, -1) > -1) { 105 | speedTest.downloadSpeed = Math.max(speedTest.downloadSpeed, dlValue(data)); 106 | } 107 | 108 | if (ulValue(data, -1) > -1) { 109 | speedTest.uploadSpeed = Math.max(speedTest.uploadSpeed, ulValue(data)); 110 | } 111 | 112 | if (latencyValue(data, -1) > -1) { 113 | speedTest.latency = Math.max(speedTest.latency, latencyValue(data)); 114 | } 115 | 116 | if (jitterValue(data, -1) > -1) { 117 | speedTest.jitter = Math.max(speedTest.jitter, jitterValue(data)); 118 | } 119 | 120 | if (dateOfLastTest(data, null)) { 121 | speedTest.dateOfLastTest = dateOfLastTest(data); 122 | } 123 | 124 | if (isTestRunning(data)) { 125 | speedTest.isTestRunning = isTestRunning(data); 126 | } 127 | 128 | log.verbose('Updated SpeedTest to: ' + JSON.stringify(speedTest)); 129 | } 130 | 131 | function SpeedTest( 132 | clientIp: string = 'unknown', 133 | ispInfo: string = 'unknown', 134 | downloadSpeed: number = 0, 135 | uploadSpeed: number = 0, 136 | latency: number = 0, 137 | jitter: number = 0, 138 | isTestRunning: boolean = false, 139 | dateOfLastTest?: Date 140 | ): INetworkSpeedState { 141 | 142 | const speedTest: INetworkSpeedState = { 143 | clientIp, 144 | ispInfo, 145 | downloadSpeed, 146 | uploadSpeed, 147 | latency, 148 | jitter, 149 | isTestRunning, 150 | 151 | updateWithData: function(this: INetworkSpeedState, data: object, log: ILogger = IOC().logger()) { 152 | _updateWithData(this, data, log); 153 | }, 154 | apply: function (data: Partial) { 155 | const copy = SpeedTest( 156 | data?.clientIp ?? '', 157 | data?.ispInfo ?? '', 158 | data?.downloadSpeed ?? 0, 159 | data?.uploadSpeed ?? 0, 160 | data?.latency ?? 0, 161 | data?.jitter ?? 0, 162 | data?.isTestRunning ?? false, 163 | data?.dateOfLastTest, 164 | ); 165 | return copy; 166 | } 167 | } 168 | 169 | if ( dateOfLastTest ) { 170 | speedTest.dateOfLastTest = dateOfLastTest; 171 | } 172 | 173 | return speedTest 174 | } 175 | 176 | export default SpeedTest; 177 | -------------------------------------------------------------------------------- /src/services/networkspeed/service.ts: -------------------------------------------------------------------------------- 1 | 2 | import { IService, IServiceStatus } from "../interface"; 3 | import { INetworkSpeedState, INetworkSpeedService, INetworkSpeedConfig } from "./interface"; 4 | import model from './model'; 5 | import util from './util'; 6 | import newRunner from './speedRunner'; 7 | 8 | 9 | function createService(config: INetworkSpeedConfig): IService { 10 | const runner = newRunner(config.serverConfigurations); 11 | 12 | const service: INetworkSpeedService = { 13 | status: IServiceStatus.Initialized, 14 | name: 'NetSpeed', 15 | state: model(), 16 | subscribeForUpdates: function(this: INetworkSpeedService, subscriberKey: string, callback: (service: IService) => void) { 17 | this.callbackUpdates[subscriberKey] = callback; 18 | }, 19 | unsubscribeFromUpdates: function(this: INetworkSpeedService, subscriberKey: string) { 20 | delete this.callbackUpdates[subscriberKey]; 21 | }, 22 | unsubscribeAll: function(this: INetworkSpeedService) { 23 | this.callbackUpdates = {}; 24 | }, 25 | start: function(this: INetworkSpeedService) { 26 | if ( util.isRunning(this) ) { return Promise.resolve(); } 27 | this.status = IServiceStatus.Idle; 28 | 29 | runSpeedTest(); 30 | 31 | window.addEventListener('online', runSpeedTest); 32 | 33 | return Promise.resolve(); 34 | }, 35 | stop: function(this: INetworkSpeedService) { 36 | if ( !util.isRunning(this) ) { return Promise.resolve(); } 37 | this.status = IServiceStatus.Stopped; 38 | 39 | clearTimeout(this.timer); 40 | this.timer = null; 41 | 42 | window.removeEventListener('online', runSpeedTest); 43 | 44 | return Promise.resolve(); 45 | }, 46 | forceUpdate: function(this: INetworkSpeedService) { 47 | runSpeedTest(); 48 | return Promise.resolve(); 49 | }, 50 | 51 | callbackUpdates: {}, 52 | timer: null, 53 | worker: runner, 54 | }; 55 | 56 | const runSpeedTest = () => { 57 | service.worker.ready() 58 | .then(function(){ 59 | util.startSpeedTest(service, config); 60 | }) 61 | }; 62 | 63 | return service; 64 | } 65 | 66 | export default createService; 67 | -------------------------------------------------------------------------------- /src/services/networkspeed/speedRunner.ts: -------------------------------------------------------------------------------- 1 | 2 | import IOC from '../../ioc' 3 | import { IServerConfiguration } from '../../interfaces/config'; 4 | import { ILogger } from '../../interfaces/log'; 5 | import model from './model'; 6 | import { INetworkSpeedState, ISpeedRunner } from './interface'; 7 | 8 | 9 | interface IRunnerData { 10 | testState: number, 11 | dlStatus: string, 12 | ulStatus: string, 13 | pingStatus: string, 14 | clientIp: string, 15 | ispInfo: any, 16 | jitterStatus: string, 17 | dlProgress: number, 18 | ulProgress: number, 19 | pingProgress: number, 20 | testId: null, 21 | } 22 | 23 | interface ISpeedRunnerPrivate extends ISpeedRunner { 24 | runner: any; 25 | flagIsReady: boolean; 26 | } 27 | 28 | function makeRunner(testServers: IServerConfiguration[], log: ILogger = IOC().logger()): ISpeedRunner { 29 | const runner = new (window as any).Speedtest(); 30 | runner.setParameter("telemetry_level", "none"); 31 | runner.addTestPoints(testServers); 32 | 33 | const returnRunner: ISpeedRunnerPrivate = { 34 | runner, 35 | flagIsReady: false, 36 | isReady: function(this: ISpeedRunnerPrivate) { 37 | return this.flagIsReady; 38 | }, 39 | ready: function(this: ISpeedRunnerPrivate) { 40 | if ( this.flagIsReady ) { return Promise.resolve() } 41 | 42 | const self = this; 43 | 44 | return new Promise(function(resolve, reject) { 45 | self.runner.selectServer(function (server: IServerConfiguration) { 46 | if (server) { 47 | log.info('Finished selecting server ' + JSON.stringify(server) + ', starting speed test'); 48 | self.flagIsReady = true; 49 | resolve(); 50 | } else { 51 | log.error('No test runner server found'); 52 | reject('Failed to find server to start update'); 53 | } 54 | }); 55 | }) 56 | }, 57 | run: function(this: ISpeedRunnerPrivate) { 58 | if ( !this.flagIsReady ) { return Promise.reject("Cannot run speed test before running ready()") } 59 | 60 | const runner = this.runner; 61 | runner.onupdate = null; 62 | runner.onend = null; 63 | 64 | return new Promise(function(resolve, reject) { 65 | let speedTest = model(); 66 | 67 | runner.onupdate = function(data: IRunnerData) { 68 | log.verbose('Test runner update ' + JSON.stringify(data)); 69 | let ispInfo = data.ispInfo; 70 | if ( typeof ispInfo !== 'string' ) { 71 | const ispInfoObj = ispInfo as any; 72 | ispInfo = ispInfoObj?.hostname ?? 'unknown'; 73 | } 74 | const updateData: Partial = { 75 | ispInfo: ispInfo, 76 | clientIp: data.clientIp, 77 | downloadSpeed: parseFloat(data.dlStatus), 78 | uploadSpeed: parseFloat(data.ulStatus), 79 | latency: parseFloat(data.pingStatus), 80 | jitter: parseFloat(data.jitterStatus), 81 | isTestRunning: true, 82 | } 83 | speedTest = speedTest.apply(updateData); 84 | }; 85 | 86 | runner.onend = function (wasAborted: boolean) { 87 | log.verbose('Test runner completed'); 88 | resolve(speedTest); 89 | }; 90 | 91 | runner.start(); 92 | }) 93 | }, 94 | } 95 | 96 | return returnRunner; 97 | } 98 | 99 | export default makeRunner; 100 | -------------------------------------------------------------------------------- /src/services/networkspeed/util.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | import { IServiceStatus } from "../interface"; 4 | import { INetworkSpeedService, INetworkSpeedConfig } from "./interface"; 5 | import IOC from '../../ioc'; 6 | 7 | 8 | function isRunning(service: INetworkSpeedService): boolean { 9 | return service.status === IServiceStatus.Idle || service.status === IServiceStatus.Busy; 10 | } 11 | 12 | function startSpeedTest(service: INetworkSpeedService, config: INetworkSpeedConfig, log=IOC().logger()) { 13 | clearTimeout(service.timer); 14 | service.timer = null; 15 | 16 | service.status = IServiceStatus.Busy; 17 | notifyCallbacks(service); 18 | 19 | service.worker.run() 20 | .then((result) => { 21 | if ( !isRunning(service) ) { return; } 22 | service.state = service.state.apply(result); 23 | service.status = IServiceStatus.Idle; 24 | notifyCallbacks(service); 25 | service.timer = setTimeout(()=>startSpeedTest(service, config), 26 | Math.max(config.testInterval, 60) * 1000); 27 | }) 28 | .catch((error: Error) => { 29 | log.error('Speed test run failed: ' + JSON.stringify(error)); 30 | if ( !isRunning(service) ) { return; } 31 | service.status = IServiceStatus.Error; 32 | service.timer = setTimeout(()=>startSpeedTest(service, config), 33 | Math.max(config.testInterval, 60) * 1000); 34 | }) 35 | } 36 | 37 | const notifyCallbacks = (service: INetworkSpeedService) => { 38 | Object.values(service.callbackUpdates).forEach((callback) => callback(service)); 39 | } 40 | 41 | const exports = { 42 | isRunning, 43 | startSpeedTest, 44 | notifyCallbacks, 45 | }; 46 | 47 | export default exports; 48 | -------------------------------------------------------------------------------- /src/services/onlinestatus/index.ts: -------------------------------------------------------------------------------- 1 | 2 | import service from './service' 3 | 4 | export default service; 5 | -------------------------------------------------------------------------------- /src/services/onlinestatus/interface.ts: -------------------------------------------------------------------------------- 1 | 2 | import { IService } from "../interface"; 3 | 4 | 5 | export interface IOnlineStatusService extends IService { 6 | callbackUpdates: Record) => void>; 7 | timer: any; 8 | } 9 | 10 | export interface IOnlineStatusState { 11 | isOnline: boolean; 12 | dateWasLastOnline?: Date; 13 | updateWithData: (isOnline: boolean) => void; 14 | apply: (data: Partial) => IOnlineStatusState; 15 | } 16 | 17 | export interface IOnlineStatusConfig { 18 | testInterval: number; 19 | servers: string[]; 20 | } 21 | -------------------------------------------------------------------------------- /src/services/onlinestatus/model.ts: -------------------------------------------------------------------------------- 1 | 2 | import { IOnlineStatusState } from './interface'; 3 | 4 | 5 | function OnlineStatus(isOnline: boolean = false) : IOnlineStatusState { 6 | function _updateWithData(onlineStatus: IOnlineStatusState, newStatus: boolean) { 7 | const wasOnline = onlineStatus.isOnline; 8 | onlineStatus.isOnline = newStatus; 9 | if (!onlineStatus.isOnline && wasOnline) { 10 | onlineStatus.dateWasLastOnline = new Date(); 11 | } 12 | } 13 | 14 | return { 15 | isOnline, 16 | updateWithData: function(this: IOnlineStatusState, isOnline: boolean) { 17 | _updateWithData(this, isOnline); 18 | }, 19 | apply: function (data: Partial) { 20 | const copy = OnlineStatus( 21 | data?.isOnline ?? false, 22 | ); 23 | return copy; 24 | }, 25 | } 26 | } 27 | 28 | export default OnlineStatus; 29 | -------------------------------------------------------------------------------- /src/services/onlinestatus/reachable.ts: -------------------------------------------------------------------------------- 1 | 2 | enum PINGSTATUS { 3 | INIT, 4 | RUNNING, 5 | PASSED, 6 | FAILED_ERROR, 7 | FAILED_TIMEOUT, 8 | } 9 | 10 | interface IReachable { 11 | status: PINGSTATUS, 12 | address: string, 13 | isRunning: ()=>boolean; 14 | isOnline: ()=>boolean; 15 | isOffline: ()=>boolean; 16 | start: ()=>void; 17 | } 18 | 19 | interface IImageChecker extends IReachable { 20 | callback?: (result: IReachable) => void; 21 | image: any; 22 | timerHandler: any; 23 | } 24 | 25 | const ImageChecker = function(address: string, callback: (result: IReachable)=>void, timeout=5000): IImageChecker { 26 | function _updateResult(imageChecker: IImageChecker, newStatus: PINGSTATUS, error?: Error) { 27 | if ( !imageChecker.isRunning() ) { return; } 28 | if ( !imageChecker.callback ) { return; } 29 | clearInterval(imageChecker.timerHandler); 30 | imageChecker.status = newStatus; 31 | if ( imageChecker.callback ) { 32 | imageChecker.callback(imageChecker); 33 | delete imageChecker.callback; 34 | } 35 | } 36 | 37 | return { 38 | status: PINGSTATUS.INIT, 39 | address, 40 | isRunning: function(this: IImageChecker) { return this.status === PINGSTATUS.RUNNING; }, 41 | isOnline: function(this: IImageChecker) { return this.status === PINGSTATUS.PASSED; }, 42 | isOffline: function(this: IImageChecker) { return this.status === PINGSTATUS.FAILED_TIMEOUT; }, 43 | start: function(this: IImageChecker) { 44 | const _that = this; 45 | 46 | this.image = new Image(); 47 | this.image.onload = function () { 48 | _updateResult(_that, PINGSTATUS.PASSED); 49 | }; 50 | this.image.onerror = function (error: Error) { 51 | _updateResult(_that, PINGSTATUS.FAILED_ERROR, error); 52 | }; 53 | this.image.onabort = function() { 54 | _updateResult(_that, PINGSTATUS.FAILED_ERROR, Error("User aborted request")); 55 | } 56 | 57 | this.status = PINGSTATUS.RUNNING; 58 | this.timerHandler = setTimeout(function () { 59 | _updateResult(_that, PINGSTATUS.FAILED_TIMEOUT); 60 | }, timeout); 61 | 62 | /* start the load process */ 63 | this.image.src = address; 64 | }, 65 | callback, 66 | image: (new Image()), 67 | timerHandler: 0, 68 | }; 69 | }; 70 | 71 | const canReach = function(website: string, callback: (result: IReachable)=>void): IReachable { 72 | /* append time=$EPOCH, to prevent browser caching result */ 73 | const url = website + '/favicon.ico?time=' + (new Date().getTime()/1000); 74 | const checker = ImageChecker(url, callback); 75 | checker.start(); 76 | return checker; 77 | }; 78 | 79 | const exports = { 80 | canReach, 81 | status : PINGSTATUS, 82 | } 83 | 84 | export default exports; 85 | -------------------------------------------------------------------------------- /src/services/onlinestatus/service.ts: -------------------------------------------------------------------------------- 1 | 2 | import { ILogger } from '../../interfaces/log'; 3 | import IOC from '../../ioc' 4 | import { IService, IServiceStatus } from '../interface'; 5 | import { IOnlineStatusConfig, IOnlineStatusService, IOnlineStatusState } from './interface' 6 | import util from './util'; 7 | import model from './model'; 8 | 9 | 10 | function createService(config: IOnlineStatusConfig, log: ILogger = IOC().logger()): IOnlineStatusService { 11 | const service: IOnlineStatusService = { 12 | status: IServiceStatus.Initialized, 13 | name: 'OnlineStatus', 14 | state: model(), 15 | subscribeForUpdates: function(this: IOnlineStatusService, subscriberKey: string, callback: (service: IService) => void) { 16 | this.callbackUpdates[subscriberKey] = callback; 17 | }, 18 | unsubscribeFromUpdates: function(this: IOnlineStatusService, subscriberKey: string) { 19 | delete this.callbackUpdates[subscriberKey]; 20 | }, 21 | unsubscribeAll: function(this: IOnlineStatusService) { 22 | this.callbackUpdates = {}; 23 | }, 24 | start: function(this: IOnlineStatusService) { 25 | if ( util.isRunning(this) ) { return Promise.resolve(); } 26 | this.status = IServiceStatus.Idle; 27 | 28 | const pingCheckCallback = function(this: IOnlineStatusService, isOnline: boolean) { 29 | this.state.isOnline = isOnline; 30 | util.notifyCallbacks(service); 31 | 32 | setTimeout(()=>util.startNetPingCheck(config, pingCheckCallback), 33 | Math.max(config.testInterval, 5) * 1000) 34 | }.bind(this); 35 | 36 | util.startNetPingCheck(config, pingCheckCallback); 37 | 38 | window.addEventListener('online', updateNavigatorOnline); 39 | window.addEventListener('offline', updateNavigatorOnline); 40 | 41 | return Promise.resolve(); 42 | }, 43 | stop: function(this: IOnlineStatusService) { 44 | if ( !util.isRunning(this) ) { return Promise.resolve(); } 45 | this.status = IServiceStatus.Stopped; 46 | 47 | clearTimeout(this.timer); 48 | this.timer = null; 49 | 50 | window.removeEventListener('online', updateNavigatorOnline); 51 | window.removeEventListener('offline', updateNavigatorOnline); 52 | 53 | return Promise.resolve(); 54 | }, 55 | forceUpdate: function(this: IOnlineStatusService) { 56 | updateNavigatorOnline(); 57 | return Promise.resolve(); 58 | }, 59 | 60 | callbackUpdates: {}, 61 | timer: null, 62 | }; 63 | 64 | const updateNavigatorOnline = () => { 65 | service.state.isOnline = util.isNavigatorOnline(); 66 | log.info('online status changed to: ' + (service.state.isOnline ? 'online' : 'offline')); 67 | util.notifyCallbacks(service); 68 | } 69 | 70 | return service; 71 | } 72 | 73 | export default createService; 74 | -------------------------------------------------------------------------------- /src/services/onlinestatus/util.ts: -------------------------------------------------------------------------------- 1 | 2 | import IOC from '../../ioc' 3 | import { IConfig } from '../../interfaces/config'; 4 | import { ILogger } from '../../interfaces/log'; 5 | import { IServiceStatus } from '../interface'; 6 | import { IOnlineStatusConfig, IOnlineStatusService } from './interface'; 7 | import reachable from './reachable'; 8 | 9 | 10 | const isNavigatorOnline = () => navigator.onLine; 11 | 12 | function isRunning(service: IOnlineStatusService): boolean { 13 | return service.status === IServiceStatus.Idle || service.status === IServiceStatus.Busy; 14 | } 15 | 16 | function retryOperation(operation: () => Promise, delay: number, retries: number): Promise { 17 | const wait = (ms:number) => new Promise(r => setTimeout(r, ms)); 18 | 19 | return new Promise((resolve, reject) => { 20 | return operation() 21 | .then(resolve) 22 | .catch((reason) => { 23 | if (retries <= 0) { reject(reason); } 24 | 25 | return wait(delay) 26 | .then((): Promise => { 27 | return retryOperation(operation, delay, retries-1); 28 | }) 29 | .then(resolve) 30 | .catch(reject); 31 | }); 32 | }); 33 | } 34 | 35 | function startNetPingCheck( 36 | config: IOnlineStatusConfig, 37 | callback: (isOnline: boolean) => void, 38 | log: ILogger = IOC().logger(), 39 | constants: IConfig = IOC().config()) { 40 | if ( !navigator.onLine ) { 41 | log.warn('offline, cannot run speedtest request'); 42 | callback(false); 43 | return; 44 | } 45 | 46 | const getWebsiteURL = (): string => { 47 | const fetchIdx = Math.round(Math.random() * (constants.ping.servers.length-1)); 48 | const website = constants.ping.servers[fetchIdx]; 49 | return website; 50 | } 51 | 52 | function canReachAsync(website: string): Promise { 53 | log.verbose('Checking can access site' + website); 54 | return new Promise(function(resolve, reject) { 55 | reachable.canReach(getWebsiteURL(), function(result){ 56 | const isOnline = result.isOnline(); 57 | if ( !isOnline ) { 58 | log.error(`Failed to reach website: ${website}, result:${JSON.stringify(result)}`) 59 | } 60 | resolve(isOnline); 61 | }); 62 | }) 63 | } 64 | 65 | const url = getWebsiteURL(); 66 | 67 | retryOperation(()=>canReachAsync(url), 1000, 3) 68 | .then(function(isOnline: boolean) { 69 | if ( isOnline ) { 70 | log.info('Successfully reached website: ' + url); 71 | } else { 72 | log.error('Failed to reach website: ' + url); 73 | } 74 | callback(isOnline); 75 | }) 76 | .catch(function(error: Error) { 77 | log.error(`Failed to reach website:${url} error: + JSON.stringify(error)`); 78 | callback(false); 79 | }) 80 | } 81 | 82 | const notifyCallbacks = (service: IOnlineStatusService) => { 83 | Object.values(service.callbackUpdates).forEach((callback) => callback(service)); 84 | } 85 | 86 | const exports = { 87 | isRunning, 88 | isNavigatorOnline, 89 | startNetPingCheck, 90 | notifyCallbacks, 91 | } 92 | 93 | export default exports; 94 | -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /src/state/index.ts: -------------------------------------------------------------------------------- 1 | 2 | import networkspeed from './networkspeed'; 3 | import onlinestatus from './onlinestatus'; 4 | import store from './store'; 5 | 6 | 7 | const exports = { 8 | networkspeed, 9 | onlinestatus, 10 | store, 11 | }; 12 | 13 | export default exports; 14 | -------------------------------------------------------------------------------- /src/state/interface.ts: -------------------------------------------------------------------------------- 1 | 2 | import { INetworkSpeedState } from "./networkspeed/interface"; 3 | import { IOnlineStatusState } from "./onlinestatus/interface"; 4 | 5 | 6 | export interface IGlobalState { 7 | OnlineStatus: IOnlineStatusState; 8 | SpeedTest: INetworkSpeedState; 9 | } 10 | -------------------------------------------------------------------------------- /src/state/networkspeed/dispatcher.ts: -------------------------------------------------------------------------------- 1 | 2 | import { INetworkSpeedState, StateModifier } from "./interface"; 3 | 4 | 5 | function updateResults(store: any, state: Partial<{ 6 | latency: number; 7 | jitter: number; 8 | downloadSpeed: number; 9 | uploadSpeed: number; 10 | ispInfo: string; 11 | clientIp: string; 12 | }>): StateModifier { 13 | return { 14 | type: 'NetworkSpeed-UpdateResult', 15 | apply: (model: INetworkSpeedState) => { 16 | let newModel = {...model}; 17 | newModel.jitter = state?.jitter ?? newModel.jitter; 18 | newModel.latency = state?.latency ?? newModel.latency; 19 | newModel.downloadSpeed = state?.downloadSpeed ?? newModel.downloadSpeed; 20 | newModel.uploadSpeed = state?.uploadSpeed ?? newModel.uploadSpeed; 21 | if ( ( state?.ispInfo?.length ?? 0 ) > 0 ) { 22 | newModel.ispInfo = state?.ispInfo ?? ''; 23 | } 24 | if ( ( state?.clientIp?.length ?? 0 ) > 0 ) { 25 | newModel.clientIp = state?.clientIp ?? ''; 26 | } 27 | return newModel; 28 | } 29 | } 30 | } 31 | 32 | function speedTestStarted(store: any): StateModifier { 33 | return { 34 | type: 'NetworkSpeed-TestStart', 35 | apply: (model: INetworkSpeedState) => { 36 | let newModel = {...model}; 37 | newModel.isTestRunning = true; 38 | return newModel; 39 | } 40 | } 41 | } 42 | 43 | function speedTestEnded(store: any): StateModifier { 44 | return { 45 | type: 'NetworkSpeed-TestEnd', 46 | apply: (model: INetworkSpeedState) => { 47 | let newModel = {...model}; 48 | newModel.isTestRunning = false; 49 | newModel.dateOfLastTest = new Date(); 50 | return newModel; 51 | } 52 | } 53 | } 54 | 55 | const exports = { 56 | speedTestStarted, 57 | speedTestEnded, 58 | updateResults, 59 | } 60 | 61 | export default exports; 62 | -------------------------------------------------------------------------------- /src/state/networkspeed/index.ts: -------------------------------------------------------------------------------- 1 | 2 | import state from './state'; 3 | import dispatcher from './dispatcher'; 4 | 5 | 6 | const exports = { 7 | state, 8 | dispatcher, 9 | }; 10 | 11 | export default exports; 12 | -------------------------------------------------------------------------------- /src/state/networkspeed/interface.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface INetworkSpeedState { 3 | jitter: number; 4 | latency: number; 5 | uploadSpeed: number; 6 | downloadSpeed: number; 7 | ispInfo: string; 8 | clientIp: string; 9 | isTestRunning: boolean; 10 | dateOfLastTest: Date; 11 | } 12 | 13 | export interface StateModifier { 14 | type: string; 15 | apply(modelIn: T): T; 16 | } 17 | -------------------------------------------------------------------------------- /src/state/networkspeed/reducer.ts: -------------------------------------------------------------------------------- 1 | 2 | import { INetworkSpeedState, StateModifier } from './interface'; 3 | import newState from './state'; 4 | 5 | 6 | function reducer(state = newState(), action: StateModifier) { 7 | if (state === undefined) { 8 | state = newState(); 9 | } 10 | 11 | if (action === undefined) { 12 | return state; 13 | } 14 | 15 | let stateReturn = state; 16 | 17 | if ( 'apply' in action ) { 18 | stateReturn = action.apply(state); 19 | } 20 | 21 | return stateReturn; 22 | } 23 | 24 | export default reducer; 25 | -------------------------------------------------------------------------------- /src/state/networkspeed/state.ts: -------------------------------------------------------------------------------- 1 | 2 | import { INetworkSpeedState } from "./interface" 3 | 4 | 5 | function State(): INetworkSpeedState { 6 | const state = { 7 | jitter: 0, 8 | latency: 0, 9 | uploadSpeed: 0, 10 | downloadSpeed: 0, 11 | isTestRunning: false, 12 | dateOfLastTest: new Date(0), 13 | ispInfo: 'unknown', 14 | clientIp: 'unknown', 15 | } 16 | 17 | return state; 18 | } 19 | 20 | export default State 21 | -------------------------------------------------------------------------------- /src/state/onlinestatus/dispatcher.ts: -------------------------------------------------------------------------------- 1 | import { IOnlineStatusState, StateModifier } from "./interface"; 2 | 3 | 4 | function isOnline(store: any, isOnline: boolean): StateModifier { 5 | return { 6 | type: 'OnlineStatus-Change', 7 | apply: (model: IOnlineStatusState) => { 8 | let newModel = {...model}; 9 | 10 | newModel.isOnline = isOnline; 11 | 12 | if ( isOnline ) { 13 | newModel.dateWasLastOnline = new Date(); 14 | } 15 | 16 | return newModel; 17 | } 18 | } 19 | } 20 | 21 | const exports = { 22 | isOnline, 23 | } 24 | 25 | export default exports; 26 | -------------------------------------------------------------------------------- /src/state/onlinestatus/index.ts: -------------------------------------------------------------------------------- 1 | 2 | import state from './state'; 3 | import dispatcher from './dispatcher'; 4 | 5 | 6 | const exports = { 7 | state, 8 | dispatcher, 9 | }; 10 | 11 | export default exports; 12 | -------------------------------------------------------------------------------- /src/state/onlinestatus/interface.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface IOnlineStatusState { 3 | isOnline: boolean; 4 | dateWasLastOnline: Date; 5 | } 6 | 7 | export interface StateModifier { 8 | type: string; 9 | apply(modelIn: T): T; 10 | } 11 | -------------------------------------------------------------------------------- /src/state/onlinestatus/reducer.ts: -------------------------------------------------------------------------------- 1 | 2 | import { IOnlineStatusState, StateModifier } from './interface'; 3 | import newState from './state'; 4 | 5 | 6 | function reducer(state = newState(), action: StateModifier) { 7 | if (state === undefined) { 8 | state = newState(); 9 | } 10 | 11 | if (action === undefined) { 12 | return state; 13 | } 14 | 15 | let stateReturn = state; 16 | 17 | if ( 'apply' in action ) { 18 | stateReturn = action.apply(state); 19 | } 20 | 21 | return stateReturn; 22 | } 23 | 24 | export default reducer; 25 | -------------------------------------------------------------------------------- /src/state/onlinestatus/state.ts: -------------------------------------------------------------------------------- 1 | 2 | import { IOnlineStatusState } from "./interface" 3 | 4 | 5 | function State(): IOnlineStatusState { 6 | const state = { 7 | isOnline: true, 8 | dateWasLastOnline: new Date(0), 9 | } 10 | 11 | return state; 12 | } 13 | 14 | export default State 15 | -------------------------------------------------------------------------------- /src/state/store.ts: -------------------------------------------------------------------------------- 1 | 2 | import {createStore, combineReducers, StoreEnhancer } from "redux"; 3 | import OnlineStatus from './onlinestatus/reducer'; 4 | import SpeedTest from './networkspeed/reducer'; 5 | 6 | 7 | function store(initialiState = {}, reducers={}, enhancers?: StoreEnhancer) { 8 | const rootReducers = combineReducers({OnlineStatus, SpeedTest, ...reducers}); 9 | 10 | const store = createStore(rootReducers, initialiState, enhancers); 11 | 12 | return store; 13 | } 14 | 15 | export default store; 16 | -------------------------------------------------------------------------------- /src/util/env.di.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Container } from "inversify"; 3 | import { IInjectable } from "../interfaces/di"; 4 | import { IEnv } from "../interfaces/env"; 5 | import env from './env'; 6 | 7 | 8 | const type = Symbol.for('IEnv'); 9 | 10 | const exports: IInjectable = { 11 | addBinding: (container) => { 12 | container 13 | .bind(type) 14 | .toConstantValue(env); 15 | }, 16 | resolve: (container: Container) => { 17 | return container.get(type); 18 | }, 19 | type, 20 | 21 | } 22 | 23 | export default exports; 24 | -------------------------------------------------------------------------------- /src/util/env.ts: -------------------------------------------------------------------------------- 1 | 2 | const getProcessVal = (envName: string): any|null => { 3 | const envVal = process.env[envName]; 4 | return envVal; 5 | }; 6 | 7 | const getWindowVal = (envName: string): any|null => { 8 | const envVal = (window as any)?.env?.[envName]; 9 | return envVal; 10 | }; 11 | 12 | const getInt = (envName: string, defaultValue=-1): number => { 13 | const envVal = 14 | getProcessVal(envName) ?? 15 | getWindowVal(envName); 16 | let retVal = defaultValue 17 | 18 | if ( envVal !== undefined ) { 19 | retVal = parseInt(envVal); 20 | } 21 | 22 | return retVal; 23 | } 24 | 25 | const getBool = (envName: string, defaultValue=false): boolean => { 26 | const envVal = 27 | getProcessVal(envName) ?? 28 | getWindowVal(envName) 29 | let retVal = defaultValue; 30 | 31 | if ( envVal !== undefined ) { 32 | retVal = Boolean(envVal); 33 | } 34 | 35 | return retVal; 36 | } 37 | 38 | const getObject = (envName: string, defaultValue={}): object => { 39 | const envVal = 40 | getProcessVal(envName) ?? 41 | getWindowVal(envName); 42 | let retVal = defaultValue; 43 | 44 | if ( envVal !== undefined ) { 45 | retVal = JSON.parse(envVal); 46 | } 47 | 48 | return retVal; 49 | } 50 | 51 | const getArray = (envName: string, defaultValue: Array = []): Array => { 52 | const envVal = 53 | getProcessVal(envName) ?? 54 | getWindowVal(envName); 55 | let retVal = defaultValue; 56 | 57 | if ( ( envVal !== undefined) && 58 | ( typeof envVal === 'object' ) ) { 59 | retVal = JSON.parse(envVal); 60 | } 61 | 62 | return retVal; 63 | } 64 | 65 | const exports = { 66 | getInt, 67 | getBool, 68 | getObject, 69 | getArray, 70 | }; 71 | 72 | export default exports; 73 | -------------------------------------------------------------------------------- /src/util/log.di.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Container } from "inversify"; 3 | import { IInjectable } from "../interfaces/di"; 4 | import { IConfig } from "../interfaces/config"; 5 | import { ILogger, LOGSEVERITY } from "../interfaces/log"; 6 | import createLogger from './log'; 7 | 8 | 9 | const type = Symbol.for('ILogger'); 10 | 11 | const exports: IInjectable = { 12 | addBinding: (container) => { 13 | const config = container.get(Symbol.for('IConfig')); 14 | let levels = []; 15 | 16 | if ( config.logging.levels.verbose ) { 17 | levels.push(LOGSEVERITY.VERBOSE); 18 | } 19 | 20 | if ( config.logging.levels.info ) { 21 | levels.push(LOGSEVERITY.INFO); 22 | } 23 | 24 | if ( config.logging.levels.warn ) { 25 | levels.push(LOGSEVERITY.WARN); 26 | } 27 | 28 | if ( config.logging.levels.error ) { 29 | levels.push(LOGSEVERITY.ERROR); 30 | } 31 | 32 | if ( config.logging.levels.fatal ) { 33 | levels.push(LOGSEVERITY.FATAL); 34 | } 35 | 36 | const settings = { 37 | levels, 38 | } 39 | const logger = createLogger(settings); 40 | container 41 | .bind(type) 42 | .toConstantValue(logger); 43 | }, 44 | resolve: (container: Container) => { 45 | return container.get(type); 46 | }, 47 | type, 48 | } 49 | 50 | export default exports; 51 | -------------------------------------------------------------------------------- /src/util/log.ts: -------------------------------------------------------------------------------- 1 | 2 | import { ILogger, LOGSEVERITY } from '../interfaces/log'; 3 | 4 | export interface ILogConfig { 5 | levels: LOGSEVERITY[]; 6 | } 7 | 8 | function log(config: ILogConfig, severityLevel: LOGSEVERITY, msg: string) { 9 | if ( severityLevel == null ) { return; } 10 | if ( msg == null ) { return; } 11 | if ( !config.levels.includes(severityLevel) ) { return; } 12 | 13 | switch (severityLevel) { 14 | case LOGSEVERITY.FATAL: 15 | console.error(msg); 16 | break; 17 | 18 | case LOGSEVERITY.ERROR: 19 | console.error(msg); 20 | break; 21 | 22 | case LOGSEVERITY.WARN: 23 | console.warn(msg); 24 | break; 25 | 26 | case LOGSEVERITY.INFO: 27 | console.log(msg); 28 | break; 29 | 30 | case LOGSEVERITY.VERBOSE: 31 | console.log(msg); 32 | break; 33 | 34 | default: 35 | console.error('Unexpected severity level: ' + severityLevel); 36 | break; 37 | } 38 | } 39 | 40 | function Logger(config: ILogConfig): ILogger { 41 | return { 42 | fatal: function(msg: string) { 43 | log(config, LOGSEVERITY.FATAL, msg); 44 | }, 45 | error: function(msg: string) { 46 | log(config, LOGSEVERITY.ERROR, msg); 47 | }, 48 | warn: function(msg: string) { 49 | log(config, LOGSEVERITY.WARN, msg); 50 | }, 51 | info: function(msg: string) { 52 | log(config, LOGSEVERITY.INFO, msg); 53 | }, 54 | verbose: function(msg: string) { 55 | log(config, LOGSEVERITY.VERBOSE, msg); 56 | } 57 | } 58 | } 59 | 60 | export default Logger; 61 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx", 22 | "experimentalDecorators": true, 23 | "emitDecoratorMetadata": true, 24 | }, 25 | "include": [ 26 | "src" 27 | ] 28 | } 29 | --------------------------------------------------------------------------------