├── .dockerignore ├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .github ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.yaml │ └── feature_request.yaml ├── SECURITY.md ├── dependabot.yml ├── images │ ├── banner.png │ └── banner_muted.png ├── pull_request_template.md ├── renovate.json └── workflows │ ├── deploy.yaml │ └── test.yaml ├── .gitignore ├── .husky └── commit-msg ├── .prettierignore ├── .vscode ├── extensions.json └── settings.json ├── .yarn └── releases │ └── yarn-3.8.7.cjs ├── .yarnrc.yml ├── Dockerfile ├── Dockerfile.nvidia ├── LICENSE.md ├── README.md ├── apps ├── cli │ ├── .eslintrc.json │ ├── project.json │ ├── src │ │ └── main.ts │ ├── tsconfig.app.json │ ├── tsconfig.json │ └── tsconfig.spec.json ├── docs │ ├── babel.config.js │ ├── deploy.ts │ ├── dist │ │ └── apps │ │ │ └── cli │ │ │ └── package.json │ ├── docs │ │ ├── configuration │ │ │ ├── _category_.json │ │ │ ├── basic.mdx │ │ │ ├── cpu.mdx │ │ │ ├── gpu.mdx │ │ │ ├── network.mdx │ │ │ ├── ram.mdx │ │ │ ├── server.mdx │ │ │ └── storage.mdx │ │ ├── help.mdx │ │ ├── installation │ │ │ ├── _category_.json │ │ │ ├── docker-compose.mdx │ │ │ ├── docker.mdx │ │ │ ├── from-source.mdx │ │ │ ├── helm.mdx │ │ │ └── index.md │ │ └── integration │ │ │ ├── _category_.json │ │ │ ├── api-preview.jsx │ │ │ ├── api.mdx │ │ │ ├── index.mdx │ │ │ ├── widget-preview.jsx │ │ │ └── widgets.mdx │ ├── docusaurus.config.js │ ├── project.json │ ├── sidebars.js │ ├── src │ │ ├── css │ │ │ └── custom.css │ │ └── pages │ │ │ ├── index.module.scss │ │ │ └── index.tsx │ ├── static │ │ ├── .nojekyll │ │ ├── CNAME │ │ └── img │ │ │ ├── cpu_view_normal.png │ │ │ ├── cpu_view_temps.png │ │ │ ├── cpu_view_temps_fahrenheit.png │ │ │ ├── favicon.ico │ │ │ ├── heimdall_integration.png │ │ │ ├── homarr_integration.png │ │ │ ├── logo512.png │ │ │ ├── network_bits_per_second.png │ │ │ ├── network_bytes_per_second.png │ │ │ ├── screenshot_darkmode.png │ │ │ ├── screenshot_darkmode_alternate_order.png │ │ │ ├── screenshot_darkmode_with_perc.png │ │ │ ├── screenshot_lightmode.png │ │ │ ├── server_view_normal.png │ │ │ ├── server_view_with_host.png │ │ │ ├── storage_view_multi.png │ │ │ ├── storage_view_single.png │ │ │ ├── version_bottom_right.png │ │ │ └── version_server_hover.png │ └── tsconfig.json ├── server │ ├── .eslintrc.json │ ├── __TESTS__ │ │ ├── dynamic-info.test.ts │ │ ├── static-info.test.ts │ │ └── test-cases.ts │ ├── jest.config.ts │ ├── project.json │ ├── src │ │ ├── config.ts │ │ ├── data │ │ │ ├── cpu.ts │ │ │ ├── gpu.ts │ │ │ ├── network.ts │ │ │ ├── os.ts │ │ │ ├── ram.ts │ │ │ └── storage │ │ │ │ ├── dynamic.ts │ │ │ │ ├── index.ts │ │ │ │ └── static.ts │ │ ├── dynamic-info.ts │ │ ├── environments │ │ │ ├── environment.prod.ts │ │ │ └── environment.ts │ │ ├── index.ts │ │ ├── setup.ts │ │ ├── static-info.ts │ │ └── utils.ts │ ├── tsconfig.app.json │ ├── tsconfig.json │ ├── tsconfig.spec.json │ └── webpack.config.js └── view │ ├── .eslintrc.json │ ├── index.html │ ├── project.json │ ├── public │ ├── Inter.ttf │ ├── favicon.ico │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt │ ├── src │ ├── App.tsx │ ├── components │ │ ├── chart-components.tsx │ │ ├── chart-container.tsx │ │ ├── glass-pane.tsx │ │ ├── hardware-info-container.tsx │ │ ├── info-table.tsx │ │ ├── main-widget-container.tsx │ │ ├── single-widget-chart.tsx │ │ ├── text.tsx │ │ └── widget-switch.tsx │ ├── environment.ts │ ├── index.css │ ├── index.tsx │ ├── react-app-env.d.ts │ ├── reportWebVitals.ts │ ├── services │ │ ├── mobile.tsx │ │ ├── page-data.ts │ │ ├── query-params.ts │ │ └── settings.ts │ ├── theme │ │ └── theme.ts │ ├── utils │ │ ├── array-utils.ts │ │ ├── calculations.ts │ │ ├── format.ts │ │ └── types.ts │ └── widgets │ │ ├── cpu.tsx │ │ ├── error.tsx │ │ ├── gpu.tsx │ │ ├── network.tsx │ │ ├── ram.tsx │ │ ├── server.tsx │ │ └── storage.tsx │ ├── tsconfig.app.json │ ├── tsconfig.json │ ├── tsconfig.spec.json │ └── vite.config.ts ├── babel.config.json ├── docker-compose.yml ├── jest.config.ts ├── jest.preset.js ├── libs └── common │ ├── .eslintrc.json │ ├── project.json │ ├── src │ ├── index.ts │ ├── types.ts │ └── utils.ts │ ├── tsconfig.json │ └── tsconfig.lib.json ├── nx.json ├── package.json ├── scripts └── strip_package_json.js ├── speedtest_result ├── tsconfig.base.json └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | .yarn/* 4 | !.yarn/cache 5 | !.yarn/patches 6 | !.yarn/plugins 7 | !.yarn/releases 8 | !.yarn/sdks 9 | !.yarn/versions 10 | 11 | # production 12 | /dist 13 | 14 | # misc 15 | **.DS_Store 16 | .github 17 | .vscode 18 | **/*.md 19 | 20 | # docker 21 | .git 22 | .dockerignore 23 | Dockerfile 24 | Dockerfile.dev 25 | Dockerfile.nvidia 26 | docker-compose.yml -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": ["**/*"], 4 | "plugins": ["@nx"], 5 | "overrides": [ 6 | { 7 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 8 | "rules": { 9 | "@nx/enforce-module-boundaries": [ 10 | "error", 11 | { 12 | "enforceBuildableLibDependency": true, 13 | "allow": [], 14 | "depConstraints": [ 15 | { 16 | "sourceTag": "*", 17 | "onlyDependOnLibsWithTags": ["*"] 18 | } 19 | ] 20 | } 21 | ] 22 | } 23 | }, 24 | { 25 | "files": ["*.ts", "*.tsx"], 26 | "extends": ["plugin:@nx/typescript"], 27 | "rules": { 28 | "@typescript-eslint/ban-ts-comment": "off", 29 | "@typescript-eslint/no-non-null-assertion": "warn", 30 | "@typescript-eslint/no-explicit-any": "warn" 31 | } 32 | }, 33 | { 34 | "files": ["*.js", "*.jsx"], 35 | "extends": ["plugin:@nx/javascript"], 36 | "rules": {} 37 | } 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | The simplest way of contributing is to create 4 | [a new issue](https://github.com/MauriceNino/dashdot/issues) using the 5 | corresponding templates for feature-requests and bug-reports. 6 | 7 | If you are able to, you can also create a 8 | [pull request](https://github.com/MauriceNino/dashdot/pulls) to add the wanted 9 | features or fix the found bug yourself. Any contribution is highly appreciated! 10 | 11 | ## Setup 12 | 13 | To start working on this project, run the following series of commands: 14 | 15 | ```bash 16 | git clone https://github.com/MauriceNino/dashdot &&\ 17 | cd dashdot &&\ 18 | yarn &&\ 19 | yarn build 20 | ``` 21 | 22 | After that, you might need to restart Visual Studio Code, because otherwise there 23 | can be some errors with Typescript. 24 | 25 | When you are done with all that, you can start a dev server using `docker-compose` 26 | with: 27 | 28 | ```bash 29 | yarn dev 30 | ``` 31 | 32 | ## Git 33 | 34 | Development is done on the `dev` branch, so please use that as the base branch in your work. 35 | 36 | This project uses semantic commit messages - if you are unsure on how to create a semantic commit message, 37 | you can check out the [Semantic Commit Message Standards](https://www.conventionalcommits.org/en/v1.0.0/), 38 | or run the interactive commit message creator, which will create the commit message for you: 39 | 40 | ```bash 41 | yarn commit 42 | ``` 43 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [MauriceNino] 2 | ko_fi: MauriceNino 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yaml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: File a bug report 3 | title: '[Bug] ' 4 | labels: ['type: bug'] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: Thanks for taking the time to fill out this bug report! 9 | - type: textarea 10 | id: describe-the-bug 11 | attributes: 12 | label: Description of the bug 13 | description: Please describe in full detail, what happened and what you expected to happen instead. Feel free to add pictures if applicable. 14 | validations: 15 | required: true 16 | - type: textarea 17 | id: ways-to-reproduce 18 | attributes: 19 | label: How to reproduce 20 | description: If you remember, please add a description of what you did to reproduce the bug. 21 | - type: textarea 22 | id: logs 23 | attributes: 24 | label: Relevant log output 25 | description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. 26 | render: shell 27 | - type: textarea 28 | id: cli-info 29 | attributes: 30 | label: Info output of dashdot cli 31 | description: If you are running docker, please execute `docker exec CONTAINER yarn cli info` and paste the output. If you are running from source, just execute `yarn cli info` and paste the output. This will be automatically formatted into code, so no need for backticks. 32 | render: shell 33 | validations: 34 | required: true 35 | - type: dropdown 36 | id: browsers 37 | attributes: 38 | label: What browsers are you seeing the problem on? 39 | multiple: true 40 | options: 41 | - Firefox 42 | - Chrome 43 | - Safari 44 | - Microsoft Edge 45 | - type: dropdown 46 | id: deployment 47 | attributes: 48 | label: Where is your instance running? 49 | multiple: true 50 | options: 51 | - Linux Server 52 | - Windows Server 53 | - Desktop PC (Windows) 54 | - Desktop PC (Linux) 55 | - Desktop PC (Mac OS) 56 | - Raspberry Pi 57 | - Other (Please specify below) 58 | - type: textarea 59 | id: extra 60 | attributes: 61 | label: Additional context 62 | description: If there is anything else you want to note? 63 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yaml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: File a feature request 3 | title: '[Feature] ' 4 | labels: ['type: feature'] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: Thanks for taking the time to fill out this feature request! 9 | - type: textarea 10 | id: describe-the-feature 11 | attributes: 12 | label: Description of the feature 13 | description: Please describe in full detail, what you would like as a feature in dash.. Feel free to add any pictures or details to make it as clear as possible. 14 | validations: 15 | required: true 16 | - type: textarea 17 | id: extra 18 | attributes: 19 | label: Additional context 20 | description: If there is anything else you want to note? 21 | -------------------------------------------------------------------------------- /.github/SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Only the latest version is supported. 6 | 7 | | Version | Supported | 8 | | ------- | ------------------ | 9 | | latest | :white_check_mark: | 10 | 11 | ## Reporting a Vulnerability 12 | 13 | If you encounter a security vulnerability, please file a bug report using the correct template. 14 | I will make sure to remove the vulnerability as fast as possible and release a new version. 15 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: 'npm' 5 | directory: '/' 6 | schedule: 7 | interval: 'daily' 8 | target-branch: 'dev' 9 | open-pull-requests-limit: 0 10 | -------------------------------------------------------------------------------- /.github/images/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MauriceNino/dashdot/483b75a0384abd5aba5eee5ad90afd373aab45ec/.github/images/banner.png -------------------------------------------------------------------------------- /.github/images/banner_muted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MauriceNino/dashdot/483b75a0384abd5aba5eee5ad90afd373aab45ec/.github/images/banner_muted.png -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | **Description** 2 | 3 | 4 | 5 | **Related Issue(s)** 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["config:base", ":disableDependencyDashboard"], 4 | "baseBranches": ["dev"], 5 | "commitMessage": "{{{commitMessagePrefix}}} {{{commitMessageAction}}} {{{commitMessageTopic}}} {{{commitMessageExtra}}} {{{commitMessageSuffix}}}", 6 | "commitMessagePrefix": "chore(deps):", 7 | "commitMessageAction": "upgrade", 8 | "commitMessageTopic": "{{depName}}", 9 | "packageRules": [ 10 | { 11 | "matchUpdateTypes": ["patch", "pin", "digest"], 12 | "automerge": true, 13 | "automergeType": "pr", 14 | "automergeStrategy": "squash" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: ['dev', 'feature/**', 'renovate/**'] 6 | pull_request: 7 | workflow_dispatch: 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | # SETUP 15 | - uses: actions/checkout@v4 16 | - uses: actions/setup-node@v4 17 | with: 18 | node-version: '20.x' 19 | cache: 'yarn' 20 | 21 | # TEST 22 | - run: yarn --immutable 23 | - run: yarn test --skipNxCache 24 | - run: yarn build --skipNxCache --verbose 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | 8 | # dependencies 9 | node_modules 10 | .yarn/* 11 | !.yarn/patches 12 | !.yarn/plugins 13 | !.yarn/releases 14 | !.yarn/sdks 15 | !.yarn/versions 16 | 17 | # IDEs and editors 18 | /.idea 19 | .project 20 | .classpath 21 | .c9/ 22 | *.launch 23 | .settings/ 24 | *.sublime-workspace 25 | 26 | # IDE - VSCode 27 | .vscode/* 28 | !.vscode/settings.json 29 | !.vscode/tasks.json 30 | !.vscode/launch.json 31 | !.vscode/extensions.json 32 | 33 | # misc 34 | /.sass-cache 35 | /connect.lock 36 | /coverage 37 | /libpeerconnection.log 38 | npm-debug.log 39 | yarn-error.log 40 | testem.log 41 | /typings 42 | 43 | # System Files 44 | .DS_Store 45 | Thumbs.db 46 | 47 | # Generated Docusaurus files 48 | .docusaurus/ 49 | .cache-loader/ 50 | .nx/cache -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn commitlint --edit "${1}" 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | /.yarn 4 | 5 | # production 6 | /dist 7 | /coverage 8 | .docusaurus/ 9 | 10 | # misc 11 | **.DS_Store 12 | .prettierignore 13 | tmp 14 | 15 | # Auto generated 16 | /.github/CHANGELOG.md 17 | /.nx/cache -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "nrwl.angular-console", 4 | "esbenp.prettier-vscode", 5 | "firsttris.vscode-jest-runner", 6 | "dbaeumer.vscode-eslint" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "search.exclude": { 3 | "**/.yarn": true, 4 | "**/.pnp.*": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | httpTimeout: 600000 2 | nodeLinker: node-modules 3 | defaultSemverRangePrefix: '' 4 | yarnPath: .yarn/releases/yarn-3.8.7.cjs 5 | supportedArchitectures: 6 | os: 7 | - 'darwin' 8 | - 'linux' 9 | - 'win32' 10 | cpu: 11 | - 'x64' 12 | - 'arm' 13 | - 'arm64' 14 | - 'ia32' 15 | libc: 16 | - 'glibc' 17 | - 'musl' 18 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # BASE # 2 | FROM node:20-alpine AS base 3 | 4 | WORKDIR /app 5 | ARG TARGETPLATFORM 6 | ENV DASHDOT_IMAGE=base 7 | ENV DASHDOT_RUNNING_IN_DOCKER=true 8 | 9 | RUN \ 10 | /bin/echo ">> installing dependencies" &&\ 11 | apk update &&\ 12 | apk --no-cache add \ 13 | lsblk \ 14 | mdadm \ 15 | dmidecode \ 16 | coreutils \ 17 | util-linux \ 18 | lm-sensors \ 19 | speedtest-cli &&\ 20 | if [ "$TARGETPLATFORM" = "linux/amd64" ] || [ "$(uname -m)" = "x86_64" ]; \ 21 | then \ 22 | /bin/echo ">> installing dependencies (amd64)" &&\ 23 | wget -qO- https://install.speedtest.net/app/cli/ookla-speedtest-1.1.1-linux-x86_64.tgz \ 24 | | tar xmoz -C /usr/bin speedtest; \ 25 | elif [ "$TARGETPLATFORM" = "linux/arm64" ] || [ "$(uname -m)" = "aarch64" ]; \ 26 | then \ 27 | /bin/echo ">> installing dependencies (arm64)" &&\ 28 | wget -qO- https://install.speedtest.net/app/cli/ookla-speedtest-1.1.1-linux-aarch64.tgz \ 29 | | tar xmoz -C /usr/bin speedtest &&\ 30 | apk --no-cache add raspberrypi; \ 31 | elif [ "$TARGETPLATFORM" = "linux/arm/v7" ]; \ 32 | then \ 33 | /bin/echo ">> installing dependencies (arm/v7)" &&\ 34 | wget -qO- https://install.speedtest.net/app/cli/ookla-speedtest-1.1.1-linux-armhf.tgz \ 35 | | tar xmoz -C /usr/bin speedtest &&\ 36 | apk --no-cache add raspberrypi; \ 37 | else /bin/echo "Unsupported platform"; exit 1; \ 38 | fi 39 | 40 | # DEV # 41 | FROM base AS dev 42 | 43 | EXPOSE 3001 44 | EXPOSE 3000 45 | 46 | RUN \ 47 | /bin/echo -e ">> installing dependencies (dev)" &&\ 48 | apk --no-cache add \ 49 | git &&\ 50 | git config --global --add safe.directory /app 51 | 52 | # BUILD # 53 | FROM base as build 54 | 55 | ARG BUILDHASH 56 | ARG VERSION 57 | 58 | ENV NX_DAEMON=false 59 | 60 | RUN \ 61 | /bin/echo -e ">> installing dependencies (build)" &&\ 62 | apk --no-cache add \ 63 | git \ 64 | make \ 65 | clang \ 66 | build-base &&\ 67 | git config --global --add safe.directory /app &&\ 68 | /bin/echo -e "{\"version\":\"$VERSION\",\"buildhash\":\"$BUILDHASH\"}" > /app/version.json 69 | 70 | COPY . ./ 71 | 72 | RUN \ 73 | yarn --immutable &&\ 74 | yarn build:prod &&\ 75 | node scripts/strip_package_json.js 76 | 77 | # PROD # 78 | FROM base as prod 79 | 80 | EXPOSE 3001 81 | 82 | COPY --from=build /app/version.json . 83 | COPY --from=build /app/.yarn/releases .yarn/releases 84 | COPY --from=build /app/.yarnrc.yml .yarnrc.yml 85 | COPY --from=build /app/dist/apps/server dist/apps/server 86 | COPY --from=build /app/dist/apps/cli dist/apps/cli 87 | COPY --from=build /app/dist/apps/view dist/apps/view 88 | COPY --from=build /app/dist/package.json package.json 89 | 90 | RUN yarn 91 | 92 | CMD ["node", "."] -------------------------------------------------------------------------------- /Dockerfile.nvidia: -------------------------------------------------------------------------------- 1 | # BASE # 2 | FROM nvidia/cuda:12.2.0-base-ubuntu20.04 AS base 3 | 4 | WORKDIR /app 5 | ARG TARGETPLATFORM 6 | ENV DASHDOT_IMAGE=nvidia 7 | ENV DASHDOT_RUNNING_IN_DOCKER=true 8 | ENV NVIDIA_VISIBLE_DEVICES=all 9 | ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility" 10 | 11 | RUN \ 12 | /bin/echo ">> installing dependencies" &&\ 13 | apt-get update &&\ 14 | apt-get install -y \ 15 | curl \ 16 | wget \ 17 | mdadm \ 18 | dmidecode \ 19 | util-linux \ 20 | pciutils \ 21 | lm-sensors \ 22 | speedtest-cli &&\ 23 | curl -fsSL https://deb.nodesource.com/setup_20.x | bash - &&\ 24 | apt-get install -y nodejs &&\ 25 | corepack enable &&\ 26 | if [ "$TARGETPLATFORM" = "linux/amd64" ] || [ "$(uname -m)" = "x86_64" ]; \ 27 | then \ 28 | /bin/echo ">> installing dependencies (amd64)" &&\ 29 | wget -qO- https://install.speedtest.net/app/cli/ookla-speedtest-1.1.1-linux-x86_64.tgz \ 30 | | tar xmoz -C /usr/bin speedtest; \ 31 | elif [ "$TARGETPLATFORM" = "linux/arm64" ] || [ "$(uname -m)" = "aarch64" ]; \ 32 | then \ 33 | /bin/echo ">> installing dependencies (arm64)" &&\ 34 | wget -qO- https://install.speedtest.net/app/cli/ookla-speedtest-1.1.1-linux-aarch64.tgz \ 35 | | tar xmoz -C /usr/bin speedtest; \ 36 | elif [ "$TARGETPLATFORM" = "linux/arm/v7" ]; \ 37 | then \ 38 | /bin/echo ">> installing dependencies (arm/v7)" &&\ 39 | wget -qO- https://install.speedtest.net/app/cli/ookla-speedtest-1.1.1-linux-armhf.tgz \ 40 | | tar xmoz -C /usr/bin speedtest; \ 41 | else /bin/echo "Unsupported platform"; exit 1; \ 42 | fi &&\ 43 | /bin/echo -e ">> clean-up" &&\ 44 | apt-get clean && \ 45 | rm -rf /tmp/* /var/tmp/* 46 | 47 | # DEV # 48 | FROM base AS dev 49 | 50 | EXPOSE 3001 51 | EXPOSE 3000 52 | 53 | RUN \ 54 | /bin/echo -e ">> installing dependencies (dev)" &&\ 55 | apt-get install -y \ 56 | git &&\ 57 | git config --global --add safe.directory /app 58 | 59 | # BUILD # 60 | FROM base as build 61 | 62 | ARG BUILDHASH 63 | ARG VERSION 64 | 65 | RUN \ 66 | /bin/echo -e ">> installing dependencies (build)" &&\ 67 | apt-get install -y \ 68 | git \ 69 | make \ 70 | clang \ 71 | build-essential &&\ 72 | git config --global --add safe.directory /app &&\ 73 | /bin/echo -e "{\"version\":\"$VERSION\",\"buildhash\":\"$BUILDHASH\"}" > /app/version.json 74 | 75 | COPY . ./ 76 | 77 | RUN \ 78 | yarn --immutable &&\ 79 | yarn build:prod &&\ 80 | node scripts/strip_package_json.js 81 | 82 | # PROD # 83 | FROM base as prod 84 | 85 | EXPOSE 3001 86 | 87 | COPY --from=build /app/version.json . 88 | COPY --from=build /app/.yarn/releases .yarn/releases 89 | COPY --from=build /app/.yarnrc.yml .yarnrc.yml 90 | COPY --from=build /app/dist/apps/server dist/apps/server 91 | COPY --from=build /app/dist/apps/cli dist/apps/cli 92 | COPY --from=build /app/dist/apps/view dist/apps/view 93 | COPY --from=build /app/dist/package.json package.json 94 | 95 | RUN yarn 96 | 97 | CMD ["node", "."] -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Maurice el-Banna 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |

3 | dash. - a modern server dashboard 4 |

5 | 6 |

7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |

15 |

16 | Feel free to join the Discord and star the repo if you like the project! 17 |

18 | 19 |
20 | 21 |

22 | dash. (or dashdot) is a modern server dashboard, 23 | running on the latest tech, designed with glassmorphism in mind. 24 | It is intended to be used for smaller VPS and private servers. 25 |

26 |
27 |

28 | Live Demo 29 | | 30 | Docker Image 31 |

32 | 33 | # 34 | 35 | 36 | Consider sponsoring the development of this project 43 | 44 | 45 | 46 | 47 | **dash.** is a open-source project, so any contribution is highly appreciated. 48 | If you are interested in further developing this project, have a look at the 49 | [Contributing.md](./.github/CONTRIBUTING.md). 50 | 51 | In case you want to financially support this project, you can visit my 52 | [GitHub Sponsors](https://github.com/sponsors/MauriceNino), or my [Ko-Fi](https://ko-fi.com/mauricenino). 53 | 54 | ## Preview 55 | 56 | 57 | 58 | | Dark-Mode | Light-Mode | 59 | | -------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | 60 | | Screenshot of the dark-mode | Screenshot of the light-mode | 61 | 62 | 63 | 64 | ## Documentation 65 | 66 | - [Installation Options](https://getdashdot.com/docs/installation) 67 | - [Configuration Options](https://getdashdot.com/docs/configuration) 68 | - [Contributing](./.github/CONTRIBUTING.md) 69 | - [Changelog](./.github/CHANGELOG.md) 70 | 71 | ## Quick Install (Docker) 72 | 73 | Images are hosted on [DockerHub](https://hub.docker.com/r/mauricenino/dashdot), 74 | and are available for both AMD64 and ARM devices. 75 | 76 | ```bash 77 | docker container run -it \ 78 | -p 80:3001 \ 79 | -v /:/mnt/host:ro \ 80 | --privileged \ 81 | mauricenino/dashdot 82 | ``` 83 | 84 | To get more information on why which flag is needed, or if you want to use other 85 | install options instead (`docker-compose`, or from source), have a look at the 86 | [installation options](https://getdashdot.com/docs/installation). 87 | 88 | To read more about configuration options, you can visit the [configuration options](https://getdashdot.com/docs/configuration). 89 | 90 | ## Developer Notice 91 | 92 | > Note: Due to the consistently growing size of the `.git` folder, which was 93 | > caused by a combination of [yarn offline mirror](https://yarnpkg.com/features/caching#offline-mirror) 94 | > and [dependabot](https://docs.github.com/en/code-security/getting-started/dependabot-quickstart-guide) 95 | > changes, I had to rewrite the entire history and remove the `.yarn/cache` folder. 96 | > You can read more about this problem [here](https://github.com/yarnpkg/berry/issues/180) 97 | > 98 | > This resulted in a loss of all forks, which were created before the 18th of 99 | > March 2025. If you are one of the affected forks, I am sorry for the inconvenience. 100 | > Please consider re-forking the repository. 101 | -------------------------------------------------------------------------------- /apps/cli/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": {} 8 | }, 9 | { 10 | "files": ["*.ts", "*.tsx"], 11 | "rules": {} 12 | }, 13 | { 14 | "files": ["*.js", "*.jsx"], 15 | "rules": {} 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /apps/cli/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cli", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "sourceRoot": "apps/cli/src", 5 | "projectType": "application", 6 | "targets": { 7 | "build": { 8 | "executor": "@nx/esbuild:esbuild", 9 | "outputs": ["{options.outputPath}"], 10 | "defaultConfiguration": "production", 11 | "options": { 12 | "platform": "node", 13 | "outputPath": "dist/apps/cli", 14 | "format": ["cjs"], 15 | "bundle": true, 16 | "thirdParty": true, 17 | "main": "apps/cli/src/main.ts", 18 | "tsConfig": "apps/cli/tsconfig.app.json", 19 | "assets": [], 20 | "generatePackageJson": true, 21 | "esbuildOptions": { 22 | "sourcemap": true, 23 | "outExtension": { 24 | ".js": ".js" 25 | } 26 | } 27 | }, 28 | "configurations": { 29 | "development": {}, 30 | "production": { 31 | "esbuildOptions": { 32 | "sourcemap": false, 33 | "outExtension": { 34 | ".js": ".js" 35 | } 36 | } 37 | } 38 | } 39 | }, 40 | "serve": { 41 | "executor": "@nx/js:node", 42 | "defaultConfiguration": "development", 43 | "options": { 44 | "buildTarget": "cli:build" 45 | }, 46 | "configurations": { 47 | "development": { 48 | "buildTarget": "cli:build:development" 49 | }, 50 | "production": { 51 | "buildTarget": "cli:build:production" 52 | } 53 | } 54 | }, 55 | "test": { 56 | "executor": "@nx/eslint:lint", 57 | "outputs": ["{options.outputFile}"] 58 | } 59 | }, 60 | "tags": [] 61 | } 62 | -------------------------------------------------------------------------------- /apps/cli/src/main.ts: -------------------------------------------------------------------------------- 1 | import { exec } from 'child_process'; 2 | import dedent from 'dedent'; 3 | import { existsSync } from 'fs'; 4 | import * as si from 'systeminformation'; 5 | import { inspect, promisify } from 'util'; 6 | import yargs from 'yargs'; 7 | import { hideBin } from 'yargs/helpers'; 8 | 9 | const execp = promisify(exec); 10 | const execpnoerr = async (cmd: string) => { 11 | return execp(cmd) 12 | .then(({ stdout }) => stdout.trim()) 13 | .catch(() => ''); 14 | }; 15 | 16 | const inspectObj = (obj: unknown): string => { 17 | return inspect(obj, { 18 | showHidden: false, 19 | depth: null, 20 | colors: true, 21 | }); 22 | }; 23 | 24 | yargs(hideBin(process.argv)) 25 | .scriptName('yarn cli') 26 | .usage('yarn cli command [options...]') 27 | .command( 28 | 'info', 29 | 'show general information about your installation', 30 | yargs => yargs, 31 | async () => { 32 | const isDocker = existsSync('/.dockerenv'); 33 | const isPodman = existsSync('/run/.containerenv'); 34 | const yarnVersion = await execpnoerr('yarn --version'); 35 | const nodeVersion = await execpnoerr('node --version'); 36 | const buildInfoJson = await execpnoerr('cat version.json'); 37 | const gitHash = await execpnoerr('git log -1 --format="%H"'); 38 | const platform = await execpnoerr('uname -a'); 39 | 40 | const runningInDocker = await execpnoerr( 41 | 'echo $DASHDOT_RUNNING_IN_DOCKER' 42 | ); 43 | const image = await execpnoerr('echo $DASHDOT_IMAGE'); 44 | const buildInfo = JSON.parse(buildInfoJson || '{}'); 45 | const version = buildInfo.version ?? 'unknown'; 46 | const buildhash = buildInfo.buildhash ?? gitHash; 47 | 48 | console.log( 49 | dedent` 50 | INFO 51 | ========= 52 | Yarn: ${yarnVersion} 53 | Node: ${nodeVersion} 54 | Dash: ${version} 55 | 56 | Cwd: ${process.cwd()} 57 | Hash: ${buildhash} 58 | Platform: ${platform} 59 | Docker image: ${image} 60 | In Docker: ${isDocker} 61 | In Docker (env): ${runningInDocker} 62 | In Podman: ${isPodman}` 63 | ); 64 | } 65 | ) 66 | .command( 67 | 'raw-data', 68 | 'show the raw data that is collected in the backend for specific parts', 69 | yargs => 70 | yargs 71 | .option('os', { 72 | boolean: true, 73 | describe: 'show raw os info', 74 | }) 75 | .option('cpu', { 76 | boolean: true, 77 | describe: 'show raw cpu info', 78 | }) 79 | .option('ram', { 80 | boolean: true, 81 | describe: 'show raw ram info', 82 | }) 83 | .option('storage', { 84 | boolean: true, 85 | describe: 'show raw storage info', 86 | }) 87 | .option('network', { 88 | boolean: true, 89 | describe: 'show raw network info', 90 | }) 91 | .option('gpu', { 92 | boolean: true, 93 | describe: 'show raw gpu info', 94 | }) 95 | .option('custom', { 96 | string: true, 97 | describe: 98 | 'show custom raw info (provide systeminformation function name)', 99 | }), 100 | async args => { 101 | console.log( 102 | dedent` 103 | If you were asked to paste the output of this command, please post only the following: 104 | 105 | - On GitHub: Everything between (and excluding) the lines 106 | - On Discord: Everything between (and including) the \`\`\` 107 | 108 | ${'-'.repeat(40)} 109 | 110 |
111 | Output: 112 | 113 | \`\`\`js 114 | ` 115 | ); 116 | 117 | if (args.os) { 118 | console.log('const osInfo = ', inspectObj(await si.osInfo())); 119 | } 120 | if (args.cpu) { 121 | console.log('const cpuInfo = ', inspectObj(await si.cpu())); 122 | console.log('const cpuLoad = ', inspectObj(await si.currentLoad())); 123 | console.log('const cpuTemp = ', inspectObj(await si.cpuTemperature())); 124 | } 125 | if (args.ram) { 126 | console.log('const memInfo = ', inspectObj(await si.mem())); 127 | console.log('const memLayout = ', inspectObj(await si.memLayout())); 128 | } 129 | if (args.storage) { 130 | console.log('const disks = ', inspectObj(await si.diskLayout())); 131 | console.log('const sizes = ', inspectObj(await si.fsSize())); 132 | console.log('const blocks = ', inspectObj(await si.blockDevices())); 133 | } 134 | if (args.network) { 135 | console.log( 136 | 'const networkInfo = ', 137 | inspectObj(await si.networkInterfaces()) 138 | ); 139 | console.log( 140 | 'const networkStats = ', 141 | inspectObj(await si.networkStats()) 142 | ); 143 | } 144 | if (args.gpu) { 145 | console.log('const gpuInfo = ', inspectObj(await si.graphics())); 146 | } 147 | if (args.custom) { 148 | console.log( 149 | `const custom_${args.custom}:`, 150 | inspectObj(await si[args.custom]()) 151 | ); 152 | } 153 | 154 | console.log( 155 | dedent` 156 | \`\`\` 157 | 158 |
159 | 160 | ${'-'.repeat(40)} 161 | ` 162 | ); 163 | } 164 | ) 165 | .demandCommand(1, 1, 'You need to specify a single command') 166 | .strict() 167 | .parse(); 168 | -------------------------------------------------------------------------------- /apps/cli/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["node"] 7 | }, 8 | "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"], 9 | "include": ["src/**/*.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /apps/cli/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.app.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | } 12 | ], 13 | "compilerOptions": { 14 | "esModuleInterop": true 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /apps/cli/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": [ 9 | "jest.config.ts", 10 | "src/**/*.test.ts", 11 | "src/**/*.spec.ts", 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /apps/docs/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')], 3 | }; 4 | -------------------------------------------------------------------------------- /apps/docs/deploy.ts: -------------------------------------------------------------------------------- 1 | const { deploy } = require('@docusaurus/core/lib/commands/deploy'); 2 | 3 | deploy(__dirname, { 4 | skipBuild: true, 5 | outDir: '../../dist/apps/docs', 6 | }); 7 | -------------------------------------------------------------------------------- /apps/docs/dist/apps/cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cli", 3 | "version": "0.0.1", 4 | "main": "./main.js", 5 | "type": "commonjs" 6 | } 7 | -------------------------------------------------------------------------------- /apps/docs/docs/configuration/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Configuration", 3 | "position": 2 4 | } 5 | -------------------------------------------------------------------------------- /apps/docs/docs/configuration/basic.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | tags: 4 | - Configuration 5 | - Styles 6 | title: Basic 7 | --- 8 | 9 | {/* markdownlint-disable */} 10 | 11 | # Basic Configuration 12 | 13 | {/* markdownlint-enable */} 14 | 15 | ## `DASHDOT_WIDGET_LIST` 16 | 17 | Selects which widgets to show on the dashboard. 18 | 19 | The available options are: `os`, `cpu`, `storage`, `ram`, `network`, `gpu`. 20 | 21 | - type: `string (comma separated list)` 22 | - default: `os,cpu,storage,ram,network` 23 | 24 | | `os,cpu,storage,ram,network` | `ram,cpu,storage,network` | 25 | | ------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | 26 | | Dashboard in normal order | Dashboard in alternate order | 27 | 28 | ## `DASHDOT_PORT` 29 | 30 | The port where the express backend is running (the backend serves the frontend, so it is the same port for both). 31 | 32 | - type: `number` 33 | - default: `3001` 34 | 35 | ## `DASHDOT_PAGE_TITLE` 36 | 37 | If you want to show a custom string in the browser page title. 38 | 39 | - type: `string` 40 | - default: `dash.` 41 | 42 | ## `DASHDOT_DISABLE_INTEGRATIONS` 43 | 44 | Disables support for integrations. This does two things: disable CORS and disable open API endpoints. 45 | 46 | - type: `boolean` 47 | - default: `false` 48 | 49 | ## `DASHDOT_SHOW_DASH_VERSION` 50 | 51 | If you want to show the version number of your dash. instance on the bottom right of the page, or alternatively on the GitHub icon (hover). 52 | 53 | The available options are: `bottom_right`, `icon_hover` and `none`. 54 | 55 | - type: `string` 56 | - default: `icon_hover` 57 | 58 | | `bottom_right` | `icon_hover` | 59 | | --------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | 60 | | Dash. version bottom right | Dash. version on GitHub icon hover | 61 | 62 | ## `DASHDOT_USE_IMPERIAL` 63 | 64 | Shows any units in the imperial system, instead of the default metric. 65 | 66 | - type: `boolean` 67 | - default: `false` 68 | 69 | | `false` | `true` | 70 | | --------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | 71 | | CPU widget with temps in celsius | CPU widget with temps in fahrenheit | 72 | 73 | ## `DASHDOT_ALWAYS_SHOW_PERCENTAGES` 74 | 75 | To always show the current percentage of each graph in the top-left corner. Without enabling this, 76 | they will only be shown on lower resolution devices (mobile phones). 77 | 78 | - type: `boolean` 79 | - default: `false` 80 | 81 | | `false` | `true` | 82 | | ------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------ | 83 | | Dashboard iwith no percentages | Dashboard with percentages | 84 | -------------------------------------------------------------------------------- /apps/docs/docs/configuration/cpu.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 2 3 | tags: 4 | - Configuration 5 | - Styles 6 | title: CPU 7 | --- 8 | 9 | {/* markdownlint-disable */} 10 | 11 | # CPU Widget 12 | 13 | {/* markdownlint-enable */} 14 | 15 | ## Options 16 | 17 | ### `DASHDOT_CPU_LABEL_LIST` 18 | 19 | Selects which data is shown in the CPU widget. 20 | 21 | The available options are: `brand`, `model`, `cores`, `threads`, `frequency`. 22 | 23 | - type: `string (comma separated list)` 24 | - default: `brand,model,cores,threads,frequency` 25 | 26 | ### `DASHDOT_ENABLE_CPU_TEMPS` 27 | 28 | If you want to show the CPU temperature in the graph. 29 | This will probably not work on a VPS, so you need to try it on your own if this works. 30 | For home servers it might work just fine. 31 | 32 | - type: `boolean` 33 | - default: `false` 34 | 35 | | `false` | `true` | 36 | | ------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | 37 | | CPU widget normal | CPU widget with temps | 38 | 39 | :::note 40 | 41 | If you want to show the data using imperial units, you need to set the 42 | [`DASHDOT_USE_IMPERIAL`](./basic#dashdot_use_imperial) option to `true`. 43 | 44 | ::: 45 | 46 | ### `DASHDOT_CPU_TEMPS_MODE` 47 | 48 | To choose between average and max CPU temperature (accross all cores). Needs [`DASHDOT_ENABLE_CPU_TEMPS`](./cpu#dashdot_enable_cpu_temps) enabled to work. 49 | 50 | The available options are: `avg`, `max`. 51 | 52 | - type: `string` 53 | - default: `avg` 54 | 55 | ### `DASHDOT_CPU_CORES_TOGGLE_MODE` 56 | 57 | Switches the Processor core view depending on the selected option. The `toggle` 58 | option allows you to switch the view from the dashboard, other options hide the 59 | toggle from the dashboard. 60 | 61 | The available options are: `toggle`, `multi-core`, `average`. 62 | 63 | - type: `string` 64 | - default: `toggle` 65 | 66 | ## Styles 67 | 68 | ### `DASHDOT_CPU_WIDGET_GROW` 69 | 70 | To adjust the relative size of the Processor widget. 71 | 72 | - type: `number` 73 | - default: `4` 74 | 75 | ### `DASHDOT_CPU_WIDGET_MIN_WIDTH` 76 | 77 | To adjust the minimum width of the Processor widget (in px). 78 | 79 | - type: `number` 80 | - default: `500` 81 | 82 | ### `DASHDOT_CPU_SHOWN_DATAPOINTS` 83 | 84 | The amount of datapoints in the Processor graph. 85 | 86 | - type: `number` 87 | - default: `20` 88 | 89 | ### `DASHDOT_CPU_POLL_INTERVAL` 90 | 91 | Read the Processor load every x milliseconds. 92 | 93 | - type: `number` 94 | - default: `1000` 95 | 96 | ## Overrides 97 | 98 | You can use overrides to manually set statically gathered data. This is useful 99 | if you want to use dashdot on a VPS or a system where the data cannot be 100 | gathered automatically. 101 | 102 | ### `DASHDOT_OVERRIDE_CPU_BRAND` 103 | 104 | - type: `string` 105 | - default: `unset` 106 | 107 | ### `DASHDOT_OVERRIDE_CPU_MODEL` 108 | 109 | - type: `string` 110 | - default: `unset` 111 | 112 | ### `DASHDOT_OVERRIDE_CPU_CORES` 113 | 114 | - type: `number` 115 | - default: `unset` 116 | 117 | ### `DASHDOT_OVERRIDE_CPU_THREADS` 118 | 119 | - type: `number` 120 | - default: `unset` 121 | 122 | ### `DASHDOT_OVERRIDE_CPU_FREQUENCY` 123 | 124 | Number needs to be passed in GHz (e.g. `2.8`) 125 | 126 | - type: `number` 127 | - default: `unset` 128 | -------------------------------------------------------------------------------- /apps/docs/docs/configuration/gpu.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 6 3 | tags: 4 | - Configuration 5 | - Styles 6 | title: GPU 7 | --- 8 | 9 | {/* markdownlint-disable */} 10 | 11 | # GPU Widget 12 | 13 | :::note 14 | 15 | - To use the GPU widget, make sure to include it in the [`DASHDOT_WIDGET_LIST`](./basic#dashdot_widget_list). 16 | - Also, if you are running the docker image, make sure to use the `nvidia` tag (see [Installation with Docker](../installation/docker#gpu-support)). 17 | 18 | ::: 19 | 20 | {/* markdownlint-enable */} 21 | 22 | ## Options 23 | 24 | ### `DASHDOT_GPU_LABEL_LIST` 25 | 26 | Selects which data is shown in the gpu widget. 27 | 28 | The available options are: `brand`, `model`, `memory`. 29 | 30 | - type: `string (comma separated list)` 31 | - default: `brand, model, memory` 32 | 33 | ## Styles 34 | 35 | ### `DASHDOT_GPU_WIDGET_GROW` 36 | 37 | To adjust the relative size of the GPU widget. 38 | 39 | - type: `number` 40 | - default: `6` 41 | 42 | ### `DASHDOT_GPU_WIDGET_MIN_WIDTH` 43 | 44 | To adjust the minimum width of the GPU widget (in px). 45 | 46 | - type: `number` 47 | - default: `700` 48 | 49 | ### `DASHDOT_GPU_SHOWN_DATAPOINTS` 50 | 51 | The amount of datapoints in the GPU graph. 52 | 53 | - type: `number` 54 | - default: `20` 55 | 56 | ### `DASHDOT_GPU_POLL_INTERVAL` 57 | 58 | Read the GPU load every x milliseconds. 59 | 60 | - type: `number` 61 | - default: `1000` 62 | 63 | ## Overrides 64 | 65 | You can use overrides to manually set statically gathered data. This is useful 66 | if you want to use dashdot on a VPS or a system where the data cannot be 67 | gathered automatically. 68 | 69 | ### `DASHDOT_OVERRIDE_GPU_BRANDS` 70 | 71 | Pass a comma-separated list of brands of your GPUs. You can skip correct GPUs, by passing empty values for it (e.g. `Intel,,Nvidia` would result in `Intel` for GPU 1 and `Nvidia` for GPU 3). 72 | 73 | - type: `string` 74 | - default: `unset` 75 | 76 | ### `DASHDOT_OVERRIDE_GPU_MODELS` 77 | 78 | Pass a comma-separated list of models of your GPUs. You can skip correct GPUs, by passing empty values for it (e.g. `CometLake-H GT2,,GeForce GTX 1650 Ti` would result in `CometLake-H GT2` for GPU 1 and `GeForce GTX 1650 Ti` for GPU 3). 79 | 80 | - type: `string` 81 | - default: `unset` 82 | 83 | ### `DASHDOT_OVERRIDE_GPU_MEMORIES` 84 | 85 | Pass a comma-separated list of memory-sizes of your GPUs. You can skip correct GPUs, by passing empty values for it (e.g. `4096,,256` would result in `4 GiB` for GPU 1 and `256 MiB` for GPU 3). 86 | 87 | - type: `string` 88 | - default: `unset` 89 | -------------------------------------------------------------------------------- /apps/docs/docs/configuration/network.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 5 3 | tags: 4 | - Configuration 5 | - Styles 6 | title: Network 7 | --- 8 | 9 | {/* markdownlint-disable */} 10 | 11 | # Network Widget 12 | 13 | {/* markdownlint-enable */} 14 | 15 | ## Options 16 | 17 | ### `DASHDOT_NETWORK_LABEL_LIST` 18 | 19 | Selects which data is shown in the network widget. 20 | 21 | The available options are: `type`, `speed_up`, `speed_down`, `interface_speed`, `public_ip`. 22 | 23 | - type: `string (comma separated list)` 24 | - default: `type,speed_up,speed_down,interface_speed` 25 | 26 | ### `DASHDOT_ACCEPT_OOKLA_EULA` 27 | 28 | Use the newer and more accurate `speedtest` tool from Ookla, instead of the old `speedtest-cli` for your speedtests. 29 | When passing this flag, you accept Ooklas: [EULA](https://www.speedtest.net/about/eula), [TERMS](https://www.speedtest.net/about/terms) and [PRIVACY](https://www.speedtest.net/about/privacy). 30 | 31 | - type: `boolean` 32 | - default: `false` 33 | 34 | ### `DASHDOT_USE_NETWORK_INTERFACE` 35 | 36 | If dash. detects the wrong gateway as your default interface, you can provide a name here that is used instead. 37 | 38 | - type: `string` 39 | - default: `unset` 40 | 41 | ### `DASHDOT_SPEED_TEST_FROM_PATH` 42 | 43 | You can provide a local file-path from where dash. should read its speed-test results. 44 | This is also useful, if you want to disable speed-tests, as you can just pass a maximum value for your network graphs 45 | and then disable the `Speed (Up)` and `Speed (Down)` labels in your network widget. 46 | 47 | The file that is being read, should have the following format (you will need to remove the comments): 48 | 49 | ```json 50 | { 51 | "unit": "bit", 52 | "speedDown": 150000000, 53 | "speedUp": 50000000, 54 | "publicIp": "123.123.123.123" 55 | } 56 | ``` 57 | 58 | The values can be passed in bit (e.g. `100000000` for 100 Mb/s, because it is `100 * 1000 * 1000`) 59 | or in bytes (e.g. `100000000` for 800 Mb/s, because it is `8 * 100 * 1000 * 1000`). 60 | 61 | :::note 62 | 63 | If you are running dash. using Docker, you will have to prepend your file path with `/mnt/host` 64 | 65 | ::: 66 | 67 | - type: `string` 68 | - default: `unset` 69 | 70 | ### `DASHDOT_NETWORK_SPEED_AS_BYTES` 71 | 72 | Shows the upload and download speed in bytes (e.g. Megabytes per second) instead of bits (e.g. Megabit per second). 73 | 74 | - type: `boolean` 75 | - default: `false` 76 | 77 | | `false` | `true` | 78 | | ------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------- | 79 | | Network widget with speed in bits per second | Network widget with speed in bytes per second | 80 | 81 | ## Styles 82 | 83 | ### `DASHDOT_SPEED_TEST_INTERVAL` 84 | 85 | At which interval the network speed-test should be rerun (in minutes). 86 | 87 | - type: `number` 88 | - default: `240` (every 4 hours) 89 | 90 | ### `DASHDOT_SPEED_TEST_INTERVAL_CRON` 91 | 92 | At which interval the network speed-test should be rerun, passed as a cron string. 93 | This setting overrides `DASHDOT_SPEED_TEST_INTERVAL` if set. 94 | 95 | Example: `0 0 * * *` (every day at midnight) 96 | 97 | - type: `string` 98 | - default: `unset` 99 | 100 | ### `DASHDOT_NETWORK_WIDGET_GROW` 101 | 102 | To adjust the relative size of the Network widget. 103 | 104 | - type: `number` 105 | - default: `6` 106 | 107 | ### `DASHDOT_NETWORK_WIDGET_MIN_WIDTH` 108 | 109 | To adjust the minimum width of the Network widget (in px). 110 | 111 | - type: `number` 112 | - default: `500` 113 | 114 | ### `DASHDOT_NETWORK_SHOWN_DATAPOINTS` 115 | 116 | The amount of datapoints in each of the Network graphs. 117 | 118 | - type: `number` 119 | - default: `20` 120 | 121 | ### `DASHDOT_NETWORK_POLL_INTERVAL` 122 | 123 | Read the Network load every x milliseconds. 124 | 125 | - type: `number` 126 | - default: `1000` 127 | 128 | ## Overrides 129 | 130 | You can use overrides to manually set statically gathered data. This is useful 131 | if you want to use dashdot on a VPS or a system where the data cannot be 132 | gathered automatically. 133 | 134 | ### `DASHDOT_OVERRIDE_NETWORK_TYPE` 135 | 136 | - type: `string` 137 | - default: `unset` 138 | 139 | ### `DASHDOT_OVERRIDE_NETWORK_SPEED_UP` 140 | 141 | Number needs to be passed in bit (e.g. `100000000` for 100 Mb/s, because it is `100 * 1000 * 1000`) 142 | 143 | - type: `number` 144 | - default: `unset` 145 | 146 | ### `DASHDOT_OVERRIDE_NETWORK_SPEED_DOWN` 147 | 148 | Number needs to be passed in bit (e.g. `100000000` for 100 Mb/s, because it is `100 * 1000 * 1000`) 149 | 150 | - type: `number` 151 | - default: `unset` 152 | 153 | ### `DASHDOT_OVERRIDE_NETWORK_INTERFACE_SPEED` 154 | 155 | Number needs to be passed in Megabit (e.g. `10000` for 10 GB/s, because it is `10 * 1000`) 156 | 157 | - type: `number` 158 | - default: `unset` 159 | 160 | ### `DASHDOT_OVERRIDE_NETWORK_PUBLIC_IP` 161 | 162 | - type: `string` 163 | - default: `unset` 164 | -------------------------------------------------------------------------------- /apps/docs/docs/configuration/ram.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 4 3 | tags: 4 | - Configuration 5 | - Styles 6 | title: RAM 7 | --- 8 | 9 | {/* markdownlint-disable */} 10 | 11 | # RAM Widget 12 | 13 | {/* markdownlint-enable */} 14 | 15 | ## Options 16 | 17 | ### `DASHDOT_RAM_LABEL_LIST` 18 | 19 | Selects which data is shown in the RAM widget. 20 | 21 | The available options are: `brand`, `size`, `type`, `frequency`. 22 | 23 | - type: `string (comma separated list)` 24 | - default: `brand,size,type,frequency` 25 | 26 | ## Styles 27 | 28 | ### `DASHDOT_RAM_WIDGET_GROW` 29 | 30 | To adjust the relative size of the Memory widget. 31 | 32 | - type: `number` 33 | - default: `4` 34 | 35 | ### `DASHDOT_RAM_WIDGET_MIN_WIDTH` 36 | 37 | To adjust the minimum width of the Memory widget (in px). 38 | 39 | - type: `number` 40 | - default: `500` 41 | 42 | ### `DASHDOT_RAM_SHOWN_DATAPOINTS` 43 | 44 | The amount of datapoints in the Memory graph. 45 | 46 | - type: `number` 47 | - default: `20` 48 | 49 | ### `DASHDOT_RAM_POLL_INTERVAL` 50 | 51 | Read the Memory load every x milliseconds. 52 | 53 | - type: `number` 54 | - default: `1000` 55 | 56 | ## Overrides 57 | 58 | You can use overrides to manually set statically gathered data. This is useful 59 | if you want to use dashdot on a VPS or a system where the data cannot be 60 | gathered automatically. 61 | 62 | ### `DASHDOT_OVERRIDE_RAM_BRAND` 63 | 64 | - type: `string` 65 | - default: `unset` 66 | 67 | ### `DASHDOT_OVERRIDE_RAM_SIZE` 68 | 69 | Number needs to be passed in bytes (e.g. `34359738368` for 32 GB, because it is `32 * 1024 * 1024 * 1024`) 70 | 71 | - type: `number` 72 | - default: `unset` 73 | 74 | ### `DASHDOT_OVERRIDE_RAM_TYPE` 75 | 76 | - type: `string` 77 | - default: `unset` 78 | 79 | ### `DASHDOT_OVERRIDE_RAM_FREQUENCY` 80 | 81 | - type: `number` 82 | - default: `unset` 83 | -------------------------------------------------------------------------------- /apps/docs/docs/configuration/server.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | tags: 4 | - Configuration 5 | - Styles 6 | title: Server 7 | --- 8 | 9 | {/* markdownlint-disable */} 10 | 11 | # Server Widget 12 | 13 | {/* markdownlint-enable */} 14 | 15 | ## Options 16 | 17 | ### `DASHDOT_OS_LABEL_LIST` 18 | 19 | Selects which data is shown in the server widget. 20 | 21 | The available options are: `os`, `arch`, `up_since`, `dash_version`. 22 | 23 | - type: `string (comma separated list)` 24 | - default: `os,arch,up_since` 25 | 26 | ### `DASHDOT_SHOW_HOST` 27 | 28 | If you want to show the host part in the server widget (e.g. `dash.` -> `dash.mauz.dev`). 29 | 30 | - type: `boolean` 31 | - default: `false` 32 | 33 | | `false` | `true` | 34 | | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | 35 | | Server widget normal | Server widget with host | 36 | 37 | ### `DASHDOT_CUSTOM_HOST` 38 | 39 | If you want to show a custom host in the server widget (needs [`DASHDOT_SHOW_HOST`](#dashdot_show_host) enabled to work). 40 | 41 | - type: `string` 42 | - default: `unset` 43 | 44 | ## Styles 45 | 46 | ### `DASHDOT_OS_WIDGET_GROW` 47 | 48 | To adjust the relative size of the OS widget. 49 | 50 | - type: `number` 51 | - default: `2.5` 52 | 53 | ### `DASHDOT_OS_WIDGET_MIN_WIDTH` 54 | 55 | To adjust the minimum width of the OS widget (in px). 56 | 57 | - type: `number` 58 | - default: `300` 59 | 60 | ## Overrides 61 | 62 | ### `DASHDOT_OVERRIDE_OS` 63 | 64 | - type: `string` 65 | - default: `unset` 66 | 67 | ### `DASHDOT_OVERRIDE_ARCH` 68 | 69 | - type: `string` 70 | - default: `unset` 71 | -------------------------------------------------------------------------------- /apps/docs/docs/configuration/storage.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 3 3 | tags: 4 | - Configuration 5 | - Styles 6 | title: Storage 7 | --- 8 | 9 | {/* markdownlint-disable */} 10 | 11 | # Storage Widget 12 | 13 | {/* markdownlint-enable */} 14 | 15 | ## Options 16 | 17 | ### `DASHDOT_STORAGE_LABEL_LIST` 18 | 19 | Selects which data is shown in the storage widget. 20 | 21 | The available options are: `brand`, `size`, `type`. 22 | 23 | - type: `string (comma separated list)` 24 | - default: `brand,size,type` 25 | 26 | ### `DASHDOT_FS_DEVICE_FILTER` 27 | 28 | To hide specific drives, you can pass the device names as a string list using this parameter. 29 | If you don't know the device names of your drives, have a look at the log of dash. and look for the `Static Server Info -> storage` output. 30 | 31 | ```js 32 | storage: { 33 | layout: [ 34 | { 35 | // highlight-next-line 36 | device: 'nvme0n1', 37 | brand: 'Samsung', 38 | size: 500107862016, 39 | type: 'NVMe', 40 | raidGroup: '', 41 | }, 42 | { 43 | // highlight-next-line 44 | device: 'sda', 45 | brand: 'DELL', 46 | size: 4000225165312, 47 | type: 'HD', 48 | raidGroup: '', 49 | }, 50 | ]; 51 | } 52 | ``` 53 | 54 | - type: `string (comma separated list)` 55 | - default: `unset` 56 | 57 | ### `DASHDOT_FS_TYPE_FILTER` 58 | 59 | If dash. detects network drives as internal drives, you can provide a list of ignored FS types here. 60 | Please also create a [bug ticket](https://github.com/MauriceNino/dashdot/issues/new/choose) on the 61 | repository though, so that we can fix this problem for everyone. 62 | 63 | - type: `string (comma separated list)` 64 | - default: `cifs,9p,fuse.rclone,fuse.mergerfs,nfs4,iso9660,fuse.shfs,autofs` 65 | 66 | ### `DASHDOT_FS_VIRTUAL_MOUNTS` 67 | 68 | If you want to show a virtual mount in the storage split view, that is ignored in the filter above. 69 | Can be used for example to show `fuse.mergerfs` mounts, which is basically a grouping of other mounts. 70 | 71 | You need to pass the names of the filesystems you want to use. To find out what the name is, execute `df` 72 | and look at the first column. 73 | 74 | Note that this will only be shown in the storage split view. The normal pie chart will not be affected 75 | and there will also be no disk shown in the list of the storage widget. 76 | 77 | - type: `string (comma separated list)` 78 | - default: `unset` 79 | 80 | ## Styles 81 | 82 | ### `DASHDOT_STORAGE_WIDGET_ITEMS_PER_PAGE` 83 | 84 | To adjust the number of items per page in the Storage view, when multiple disks are 85 | present. 86 | 87 | - type: `number` 88 | - default: `3` 89 | 90 | ### `DASHDOT_STORAGE_WIDGET_GROW` 91 | 92 | To adjust the relative size of the Storage widget. 93 | 94 | - type: `number` 95 | - default: `3.5` 96 | 97 | ### `DASHDOT_STORAGE_WIDGET_MIN_WIDTH` 98 | 99 | To adjust the minimum width of the Storage widget (in px). 100 | 101 | - type: `number` 102 | - default: `500` 103 | 104 | ### `DASHDOT_STORAGE_POLL_INTERVAL` 105 | 106 | Read the Storage load every x milliseconds. 107 | 108 | - type: `number` 109 | - default: `60000` 110 | 111 | ## Overrides 112 | 113 | You can use overrides to manually set statically gathered data. This is useful 114 | if you want to use dashdot on a VPS or a system where the data cannot be 115 | gathered automatically. 116 | 117 | ### `DASHDOT_OVERRIDE_STORAGE_BRANDS` 118 | 119 | Pass a list of key-value pairs where the key is the device name of your drive (have a look in the log in `Static Server Info` > `storage` to gather the device name) and the value is the new brand of the device. An example value could be `sda=Samsung,sdb=Western Digital`. 120 | 121 | - type: `string` 122 | - default: `unset` 123 | 124 | ### `DASHDOT_OVERRIDE_STORAGE_SIZES` 125 | 126 | Pass a list of key-value pairs where the key is the device name of your drive (have a look in the log in `Static Server Info` > `storage` to gather the device name) and the value is the new size of the device (In RAIDS you only need to apply this to a single drive in the RAID). An example value could be `sda=56127367,sdb=6172637222`. 127 | Number needs to be passed in bytes (e.g. `34359738368` for 32 GB, because it is `32 * 1024 * 1024 * 1024`). 128 | 129 | - type: `string` 130 | - default: `unset` 131 | 132 | ### `DASHDOT_OVERRIDE_STORAGE_TYPES` 133 | 134 | Pass a list of key-value pairs where the key is the device name of your drive (have a look in the log in `Static Server Info` > `storage` to gather the device name) and the value is the new type of the device. An example value could be `sda=SSD,sdb=HDD`. 135 | 136 | - type: `string` 137 | - default: `unset` 138 | -------------------------------------------------------------------------------- /apps/docs/docs/help.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 4 3 | tags: 4 | - Help 5 | --- 6 | 7 | # Help 8 | 9 | ## Q&A 10 | 11 | ### How can I make the percentage labels in the top left corner stay? 12 | 13 | In earlier versions, the top-left percentage labels were on by default, but due to UX decisions, they are now 14 | only enabled on mobile devices. If you want to bring back the old behavior, there is the flag 15 | [`DASHDOT_ALWAYS_SHOW_PERCENTAGES`](./configuration/basic#dashdot_always_show_percentages) for that. 16 | 17 | ### The network information can not be read correctly - what should I do? 18 | 19 | First of all, if you are running docker, make sure that you are passing the `-v /:/mnt/host:ro` 20 | bind mount. If you have done so, and it still does not work, please do the following: 21 | 22 | > Check your logs for a message like `Using network interface "xxxxx"`. 23 | 24 | **Is this the correct network interface?** If not, please find out your default interface, and pass the name 25 | manually, using the [`DASHDOT_USE_NETWORK_INTERFACE`](./configuration/network#dashdot_use_network_interface) flag. 26 | 27 | If it **is** the correct network interface, please open a GitHub issue with the relevant log outputs and information. 28 | 29 | **Is there no message like this?** If so, please check your log for any errors and open a new issue on GitHub with 30 | that information. 31 | 32 | ### Can you use dash. without mounting the whole host drive? 33 | 34 | Yes, you can - the mount is only needed to make it easier for the user, but you can 35 | manually mount the relevant parts into the container as well. 36 | 37 | For this you need to use the following volume mounts: 38 | 39 | - `/etc/os-release:/mnt/host/etc/os-release:ro` for reading the OS version of the host 40 | - `/proc/1/ns/net:/mnt/host/proc/1/ns/net:ro` for reading the Network usage of the host 41 | - alternatively, you can just bind the container to the host network using `--net=host`, 42 | but this is not recommended, because it messes with Dockers internal networking 43 | - `/mnt:/mnt/host/mnt:ro` and `/media:/mnt/host/media:ro` for reading the usage stats of all drives 44 | - keep in mind that this covers only the most basic mount paths of linux. if your system uses other mount paths, 45 | you will need to manually add them to the list of volumes, following the pattern `/xxx:/mnt/host/xxx:ro`. To 46 | check where all your mounts are on your system, you can use the command `df` and run it in a shell. 47 | 48 | ## Answer not found? 49 | 50 | If you think there is no answer for your question and it is actually a bug, or a missing feature, 51 | please create a [new Issue on GitHub](https://github.com/MauriceNino/dashdot/issues). 52 | 53 | If you need further help, please [join our Discord](https://discord.gg/3teHFBNQ9W) - we are happy to answer any questions. 54 | 55 |
56 | 64 | -------------------------------------------------------------------------------- /apps/docs/docs/installation/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Installation", 3 | "position": 1 4 | } 5 | -------------------------------------------------------------------------------- /apps/docs/docs/installation/docker-compose.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 2 3 | description: How to install and configure dash. using docker-compose 4 | tags: 5 | - Installation 6 | - Configuration 7 | - Docker 8 | - Docker-Compose 9 | --- 10 | 11 | import CodeBlock from '@theme/CodeBlock'; 12 | 13 | # Docker-Compose 14 | 15 | ```yaml 16 | version: '3.5' 17 | 18 | services: 19 | dash: 20 | image: mauricenino/dashdot:latest 21 | restart: unless-stopped 22 | privileged: true 23 | ports: 24 | - '80:3001' 25 | volumes: 26 | - /:/mnt/host:ro 27 | ``` 28 | 29 | ## Configuration 30 | 31 | Config options can optionally set, by using the [environment](https://docs.docker.com/compose/compose-file/#environment) field. 32 | 33 | ```yaml 34 | services: 35 | dash: 36 | environment: 37 | DASHDOT_ENABLE_CPU_TEMPS: 'true' 38 | # ... 39 | ``` 40 | 41 | ## GPU Support 42 | 43 | GPU support is available with another image tag and a slightly different config. 44 | 45 | ```yaml 46 | version: '3.5' 47 | 48 | services: 49 | dash: 50 | image: mauricenino/dashdot:nvidia 51 | restart: unless-stopped 52 | privileged: true 53 | deploy: 54 | resources: 55 | reservations: 56 | devices: 57 | - capabilities: 58 | - gpu 59 | ports: 60 | - '80:3001' 61 | volumes: 62 | - /:/mnt/host:ro 63 | environment: 64 | DASHDOT_WIDGET_LIST: 'os,cpu,storage,ram,network,gpu' 65 | ``` 66 | -------------------------------------------------------------------------------- /apps/docs/docs/installation/docker.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | description: How to install and configure dash. using docker 4 | tags: 5 | - Installation 6 | - Configuration 7 | - Docker 8 | --- 9 | 10 | # Docker 11 | 12 | ```bash 13 | docker container run -it \ 14 | -p 80:3001 \ 15 | -v /:/mnt/host:ro \ 16 | --privileged \ 17 | mauricenino/dashdot 18 | ``` 19 | 20 | ## Configuration 21 | 22 | Config options can optionally passed using the 23 | [`--env`](https://docs.docker.com/engine/reference/commandline/run/#set-environment-variables--e---env---env-file) 24 | flag. 25 | 26 | ```bash 27 | docker container run -it \ 28 | --env DASHDOT_ENABLE_CPU_TEMPS="true" \ 29 | # ... 30 | ``` 31 | 32 | ## GPU Support 33 | 34 | GPU support is available with another image tag and a slightly different command. 35 | 36 | ```bash 37 | docker container run -it \ 38 | -p 80:3001 \ 39 | -v /:/mnt/host:ro \ 40 | --privileged \ 41 | --gpus all \ 42 | --env DASHDOT_WIDGET_LIST="os,cpu,storage,ram,network,gpu" \ 43 | mauricenino/dashdot:nvidia 44 | ``` 45 | -------------------------------------------------------------------------------- /apps/docs/docs/installation/from-source.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 3 3 | description: How to install and configure dash. from source 4 | tags: 5 | - Installation 6 | - Configuration 7 | - Source 8 | --- 9 | 10 | # From Source 11 | 12 | ## Prerequisites 13 | 14 | To run this repository from source, you need to have the following installed: 15 | 16 | - [node.js](https://nodejs.org/) (recommended version 20.x) 17 | - [yarn](https://yarnpkg.com/) 18 | - [git](https://git-scm.com/) 19 | - [speedtest](https://www.speedtest.net/apps/cli) (recommended) 20 | - or alternatively: [speedtest-cli](https://github.com/sivel/speedtest-cli) 21 | - or alternatively: [use local file](../configuration/network#dashdot_speed_test_from_path) 22 | 23 | ## Setup 24 | 25 | After that, download and build the project (might take a few minutes) 26 | 27 | import TabItem from '@theme/TabItem'; 28 | import Tabs from '@theme/Tabs'; 29 | 30 | 31 | 32 | 33 | ```bash 34 | git clone https://github.com/MauriceNino/dashdot \ 35 | && cd dashdot \ 36 | && yarn \ 37 | && yarn build:prod 38 | ``` 39 | 40 | 41 | 42 | 43 | ```bash 44 | git clone https://github.com/MauriceNino/dashdot ^ 45 | && cd dashdot ^ 46 | && yarn ^ 47 | && yarn build:prod 48 | ``` 49 | 50 | 51 | 52 | 53 | When done, you can run the dashboard by executing: 54 | 55 | 56 | 57 | 58 | ```bash 59 | sudo -E yarn start 60 | ``` 61 | 62 | 63 | 64 | 65 | For the most accurate results, it is recommended to start the Cmd as an Admin. 66 | 67 | ```bash 68 | yarn start 69 | ``` 70 | 71 | 72 | 73 | 74 | :::info 75 | 76 | In case you get a speedtest related error, you might have to accept the license 77 | before being able to start your dashboard. 78 | 79 | For this, run the following in your terminal: 80 | 81 | ```bash 82 | speedtest --accept-license 83 | ``` 84 | 85 | ::: 86 | 87 | ## Configuration 88 | 89 | Config options can optionally passed using environment variables. 90 | 91 | 92 | 93 | 94 | ```bash 95 | export DASHDOT_PORT="8080" \ 96 | && sudo -E yarn start 97 | ``` 98 | 99 | 100 | 101 | 102 | ```bash 103 | set DASHDOT_PORT="8080" ^ 104 | && yarn start 105 | ``` 106 | 107 | 108 | 109 | 110 | ## Update 111 | 112 | You can update your Dash. instance, by stopping your instance, then 113 | running the following commands and in the end, restart your instance. 114 | 115 | 116 | 117 | 118 | ```bash 119 | git pull --rebase \ 120 | && yarn \ 121 | && yarn build:prod 122 | ``` 123 | 124 | 125 | 126 | 127 | ```bash 128 | git pull --rebase ^ 129 | && yarn ^ 130 | && yarn build:prod 131 | ``` 132 | 133 | 134 | 135 | -------------------------------------------------------------------------------- /apps/docs/docs/installation/helm.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 4 3 | description: How to install dashdot using helm chart 4 | tags: 5 | - Installation 6 | - Configuration 7 | - Helm 8 | --- 9 | 10 | import Tabs from '@theme/Tabs'; 11 | import TabItem from '@theme/TabItem'; 12 | 13 | # Helm chart 14 | 15 | ## Installation using Helm 16 | 17 | Helm is a package manager for Kubernetes, easing deployment of applications like Dashdot. The Helm chart for Dashdot simplifies its deployment on any supported platform by Kubernetes, enabling efficient scaling and management. 18 | 19 | ### Prerequisites 20 | 21 | - [Helm](https://helm.sh/docs/intro/install/) 22 | 23 | ### Installation 24 | 25 | To install Dashdot using Helm chart, please follow this steps. 26 | 27 | 28 | 29 | 30 | helm install dashdot oci://ghcr.io/oben01/charts/dashdot 31 | 32 | 33 | 34 | 35 | helm repo add oben01 https://oben01.github.io/charts/ 36 | helm repo update 37 | helm install dashdot oben01/dashdot 38 | 39 | 40 | 41 | 42 | #### Example using override values: 43 | 44 | ```bash 45 | helm install dashdot oben01/dashdot --namespace dashdot --create-namespace -f override.yaml 46 | ``` 47 | 48 | ```yml title="override.yaml" 49 | ingress: 50 | enabled: true 51 | className: 'traefik' 52 | annotations: 53 | # Add any additional annotations as needed 54 | hosts: 55 | - host: dashdot.homelab.dev 56 | paths: 57 | - path: / 58 | pathType: ImplementationSpecific 59 | tls: 60 | - hosts: 61 | - 'dashdot.homelab.dev' 62 | - 'www.dashdot.homelab.dev' 63 | secretName: homelab-dev-tls 64 | ``` 65 | 66 | All available values are listed on the [artifacthub](https://artifacthub.io/packages/helm/oben01/dashdot?modal=values). If you find any issue please open an issue on [github](https://github.com/oben01/charts/issues/new?assignees=oben01&labels=bug&template=bug_report.yaml&name=dashdot) 67 | 68 | ## Uninstalling the Chart 69 | 70 | To uninstall the `Dashdot` deployment 71 | 72 | ```bash 73 | helm uninstall dashdot 74 | ``` 75 | 76 | The command removes all the Kubernetes components associated with the chart including persistent volumes and deletes the release. 77 | -------------------------------------------------------------------------------- /apps/docs/docs/installation/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | tags: 3 | - Installation 4 | - Docker 5 | --- 6 | 7 | # Installation 8 | 9 | ## Quick Setup (Linux) 10 | 11 | Images are hosted on [DockerHub](https://hub.docker.com/r/mauricenino/dashdot) 12 | and are available for Linux devices, running on both AMD64 and ARM architecture. 13 | 14 | ```bash 15 | docker container run -it \ 16 | -p 80:3001 \ 17 | -v /:/mnt/host:ro \ 18 | --privileged \ 19 | mauricenino/dashdot 20 | ``` 21 | 22 | :::info 23 | 24 | - The `--privileged` flag is needed to correctly determine the memory and storage info. 25 | 26 | - The volume mounts on `/:/mnt/host:ro` is needed to read the usage stats of all drives, 27 | read the network usage and read the os version of the host. If you don't like to use this 28 | mount, feel free to check out the [help page](./help#can-you-use-dash-without-mounting-the-whole-host-drive) to find a guide on how to set it up manually. 29 | 30 | ::: 31 | 32 | :::warning 33 | 34 | The speed testing feature can consume significant amounts of bandwidth, which can pose 35 | problems if your usage is metered (say, by a VPS provider). 36 | 37 | Setting the environment variable [`DASHDOT_SPEED_TEST_INTERVAL`](./configuration/network#dashdot_speed_test_interval) 38 | to a higher number can mitigate this concern. 39 | 40 | ::: 41 | 42 | ## Windows / MacOS 43 | 44 | If you want to install Dash. on Windows or MacOS, you will have to use the [from source](./installation/from-source) 45 | installation, as Docker Desktop is technologically impossible to support. 46 | 47 | ## Installation Options 48 | 49 | ```mdx-code-block 50 | import DocCardList from '@theme/DocCardList'; 51 | import {useCurrentSidebarCategory} from '@docusaurus/theme-common'; 52 | 53 | 54 | ``` 55 | -------------------------------------------------------------------------------- /apps/docs/docs/integration/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Integrations", 3 | "position": 3 4 | } 5 | -------------------------------------------------------------------------------- /apps/docs/docs/integration/api-preview.jsx: -------------------------------------------------------------------------------- 1 | import { useColorMode } from '@docusaurus/theme-common'; 2 | import { Input, MantineProvider, Select } from '@mantine/core'; 3 | import CodeBlock from '@theme/CodeBlock'; 4 | import React, { useEffect, useMemo, useRef, useState } from 'react'; 5 | import { debounce } from 'throttle-debounce'; 6 | 7 | const getDataFromUrl = url => { 8 | const [data, setData] = useState({ 9 | error: null, 10 | data: null, 11 | loading: false, 12 | }); 13 | 14 | const requestCallback = useRef(); 15 | const doRequest = useMemo( 16 | () => debounce(400, () => requestCallback.current()), 17 | [] 18 | ); 19 | 20 | useEffect(() => { 21 | requestCallback.current = async () => { 22 | setData({ 23 | loading: true, 24 | }); 25 | 26 | try { 27 | const fetched = await fetch(url); 28 | if (!fetched.ok) { 29 | throw Error('Request failed'); 30 | } 31 | 32 | const json = await fetched.json(); 33 | 34 | setData({ 35 | error: null, 36 | data: json, 37 | loading: false, 38 | }); 39 | } catch (e) { 40 | setData({ 41 | error: e, 42 | loading: false, 43 | }); 44 | } 45 | }; 46 | 47 | doRequest(); 48 | }, [url]); 49 | 50 | return data; 51 | }; 52 | 53 | export const ApiPreview = () => { 54 | const { colorMode } = useColorMode(); 55 | 56 | const [protocol, setProtocol] = useState('https'); 57 | const [url, setUrl] = useState('dash.mauz.dev'); 58 | 59 | const baseUrl = `${protocol}://${url}`; 60 | const info = getDataFromUrl(baseUrl + '/info'); 61 | const config = getDataFromUrl(baseUrl + '/config'); 62 | const cpuLoad = getDataFromUrl(baseUrl + '/load/cpu'); 63 | const storageLoad = getDataFromUrl(baseUrl + '/load/storage'); 64 | const ramLoad = getDataFromUrl(baseUrl + '/load/ram'); 65 | const networkLoad = getDataFromUrl(baseUrl + '/load/network'); 66 | const gpuLoad = getDataFromUrl(baseUrl + '/load/gpu'); 67 | 68 | const formatOutput = data => { 69 | return !data.loading 70 | ? !data.error 71 | ? JSON.stringify(data.data, null, 2) 72 | : data.error.message ?? 'Error' 73 | : 'Loading ...'; 74 | }; 75 | 76 | return ( 77 | 82 |
89 |
98 | 99 |
105 | setUrl(e.target.value)} /> 115 |
116 |
117 |
118 | 119 |

Info

120 | {`${baseUrl}/info`} 121 | 122 | {formatOutput(info)} 123 | 124 |

Config

125 | {`${baseUrl}/config`} 126 | 127 | 128 | {formatOutput(config)} 129 | 130 | 131 |

CPU Load

132 | {`${baseUrl}/load/cpu`} 135 | 136 | 137 | {formatOutput(cpuLoad)} 138 | 139 | 140 |

Storage Load

141 | {`${baseUrl}/load/storage`} 144 | 145 | 146 | {formatOutput(storageLoad)} 147 | 148 | 149 |

RAM Load

150 | {`${baseUrl}/load/ram`} 153 | 154 | 155 | {formatOutput(ramLoad)} 156 | 157 | 158 |

Network Load

159 | {`${baseUrl}/load/network`} 162 | 163 | 164 | {formatOutput(networkLoad)} 165 | 166 | 167 |

GPU Load

168 | {`${baseUrl}/load/gpu`} 171 | 172 | 173 | {formatOutput(gpuLoad)} 174 | 175 |
176 |
177 | ); 178 | }; 179 | -------------------------------------------------------------------------------- /apps/docs/docs/integration/api.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 2 3 | description: Query information about the dash. installation 4 | tags: 5 | - Integration 6 | --- 7 | 8 | # API Endpoints 9 | 10 | ## Basic 11 | 12 | There are two endpoints which can be used to retrieve data about the running dash. instance: 13 | 14 | ### `/info` 15 | 16 | Gives information about the static data of the running instance. 17 | Mostly needed to read the speed-test result at `network.speedDown` and `network.speedUp`. 18 | 19 | - Example: `https:///info` 20 | 21 | ### `/config` 22 | 23 | Gives information about the configurations of the runnning instance. 24 | Mostly needed to read the enabled widgets at `config.widget_list`. 25 | 26 | - Example: `https:///config` 27 | 28 | ### `/load` 29 | 30 | Has multiple sub-routes, each returning the current load of the given type. 31 | 32 | - `/load/cpu` 33 | - `/load/storage` 34 | - `/load/ram` 35 | - `/load/network` 36 | - `/load/gpu` 37 | 38 |
39 | 40 | - Example: `https:///load/cpu` 41 | 42 | ## URL Preview 43 | 44 | import { ApiPreview } from './api-preview'; 45 | 46 | 47 | -------------------------------------------------------------------------------- /apps/docs/docs/integration/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | tags: 3 | - Integration 4 | --- 5 | 6 | # Integrations 7 | 8 | ## Homarr 9 | 10 | ### Preview 11 | 12 | Homarr screenshot with dash. integration 17 | 18 | ### Setup 19 | 20 | Website: [https://homarr.dev](https://homarr.dev) 21 | 22 | 1. Install and setup Homarr [according to the docs](https://homarr.dev/docs/getting-started/) 23 | 2. Enter the edit mode 24 | 3. Add the Dash. widget from the header menu 25 | 4. Drag your widget to your desired location on the dashboard 26 | 5. Click the three dots and edit the `Dash. URL` field to your Dash. instance. 27 | 6. Click save and exit the edit mode 28 | 29 | ## Heimdall 30 | 31 | ### Preview 32 | 33 | Heimdall screenshot with dash. integration 38 | 39 | ### Setup 40 | 41 | Website: [https://heimdall.site](https://heimdall.site) 42 | 43 | 1. Install and setup Heimdall [according to the docs](https://github.com/linuxserver/Heimdall/discussions/778) 44 | 2. Add a new service from the application list 45 | 3. Click on "Application Type" and select `Dashdot` 46 | 4. Optionally: Change the title to "Dash." 47 | 48 | ## Custom 49 | 50 | import { useCurrentSidebarCategory } from '@docusaurus/theme-common'; 51 | import DocCardList from '@theme/DocCardList'; 52 | 53 | 54 | -------------------------------------------------------------------------------- /apps/docs/docs/integration/widgets.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | description: Show dash. graphs on your page 4 | tags: 5 | - Integration 6 | --- 7 | 8 | # Widgets 9 | 10 | ## Manual 11 | 12 | You can implement your running dash. instance on your websites and personal dashboards, via iframes that 13 | can be configured using query parameters. 14 | 15 | ```html 16 | 22 | ``` 23 | 24 | ## Parameters 25 | 26 | ### `graph` 27 | 28 | Select the specific graph that you want to show. 29 | 30 | - type: `string` (`cpu`, `storage`, `ram`, `network`, `gpu`) 31 | - required: `true` 32 | 33 | ### `multiView` 34 | 35 | For the `cpu` and `storage` widgets, there are multi-device views. 36 | These can be shown, by passing this parameter. 37 | 38 | - type: `boolean` 39 | 40 | ### `filter` 41 | 42 | For views with multiple graphs, filter the view to just a single graph. 43 | 44 | - type (when `graph=network`): `string` (`up`, `down`) 45 | - type (when `graph=gpu`): `string` (`load`, `memory`) 46 | 47 | ### `showPercentage` 48 | 49 | Show a label with the current percentage of the graph. 50 | 51 | - type: `boolean` 52 | 53 | ### `theme` 54 | 55 | To override the theme of the graph. If this is not passed, the default theme of the users browser is used. 56 | 57 | - type: `string` (`dark`, `light`) 58 | 59 | ### `color` 60 | 61 | To override the color of the graph, passed as a hex string. If this is not passed, the default color of the widget is used. 62 | The color needs to be passed _without_ the trailing `#`. 63 | 64 | - type: `string` 65 | 66 | ### `surface` 67 | 68 | To override the background color, passed as a hex string. If this is not passed, the default color of the theme is used. 69 | The color needs to be passed _without_ the trailing `#`. 70 | 71 | - type: `string` 72 | 73 | ### `innerRadius` 74 | 75 | This changes the `border-radius` on the graphs. This is mainly needed to add `border-radius` on views with multiple graphs, 76 | like the network graph. If you want to add a bigger `border-radius` to the whole container, just add it to the iframe's styles. 77 | 78 | - type: `string` 79 | 80 | ### `gap` 81 | 82 | This changes the `gap` between the graphs. This is mainly needed on views with multiple graphs, like the network graph. 83 | 84 | - type: `string` 85 | 86 | ### `textSize` 87 | 88 | This changes the `font-size` of the texts (e.g. percentage) in your graph. 89 | 90 | - type: `string` 91 | 92 | ### `textOffset` 93 | 94 | This changes the `margin` of the texts (e.g. percentage) in your graph. 95 | 96 | - type: `string` 97 | 98 | ## Widget Creator 99 | 100 | import { WidgetPreview } from './widget-preview'; 101 | 102 | 103 | -------------------------------------------------------------------------------- /apps/docs/docusaurus.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | const { themes } = require('prism-react-renderer'); 4 | const lightCodeTheme = themes.github; 5 | const darkCodeTheme = themes.dracula; 6 | 7 | /** @type {import('@docusaurus/types').Config} */ 8 | const config = { 9 | title: 'dash.', 10 | tagline: 'a modern server dashboard', 11 | url: 'https://getdashdot.com', 12 | baseUrl: '/', 13 | trailingSlash: false, 14 | onBrokenLinks: 'throw', 15 | onBrokenMarkdownLinks: 'warn', 16 | favicon: 'img/favicon.ico', 17 | organizationName: 'MauriceNino', 18 | projectName: 'dashdot', 19 | 20 | presets: [ 21 | [ 22 | 'classic', 23 | /** @type {import('@docusaurus/preset-classic').Options} */ 24 | ({ 25 | docs: { 26 | sidebarPath: require.resolve('./sidebars.js'), 27 | editUrl: 'https://github.com/MauriceNino/dashdot/edit/main/apps/docs', 28 | }, 29 | theme: { 30 | customCss: require.resolve('./src/css/custom.css'), 31 | }, 32 | }), 33 | ], 34 | ], 35 | 36 | themeConfig: 37 | /** @type {import('@docusaurus/preset-classic').ThemeConfig} */ 38 | ({ 39 | navbar: { 40 | title: 'dash.', 41 | logo: { 42 | alt: 'Logo', 43 | src: 'img/logo512.png', 44 | }, 45 | items: [ 46 | { 47 | type: 'doc', 48 | docId: 'installation/index', 49 | position: 'left', 50 | label: 'Documentation', 51 | }, 52 | { 53 | href: 'https://dash.mauz.dev', 54 | label: 'Demo', 55 | position: 'left', 56 | }, 57 | { 58 | href: 'https://ko-fi.com/mauricenino', 59 | label: 'Donate on Ko-Fi', 60 | position: 'right', 61 | }, 62 | { 63 | href: 'https://github.com/MauriceNino/dashdot', 64 | label: 'GitHub', 65 | position: 'right', 66 | }, 67 | ], 68 | }, 69 | footer: { 70 | style: 'dark', 71 | links: [ 72 | { 73 | title: 'Help', 74 | items: [ 75 | { 76 | label: 'Installation', 77 | to: 'docs/installation', 78 | }, 79 | { 80 | label: 'Configuration', 81 | to: 'docs/configuration/basic', 82 | }, 83 | { 84 | label: 'Changelog', 85 | href: 'https://github.com/MauriceNino/dashdot/blob/main/.github/CHANGELOG.md', 86 | }, 87 | ], 88 | }, 89 | { 90 | title: 'Community', 91 | items: [ 92 | { 93 | label: 'Discord', 94 | href: 'https://discord.gg/3teHFBNQ9W', 95 | }, 96 | { 97 | label: 'GitHub', 98 | href: 'https://github.com/MauriceNino/dashdot', 99 | }, 100 | ], 101 | }, 102 | ], 103 | }, 104 | prism: { 105 | theme: lightCodeTheme, 106 | darkTheme: darkCodeTheme, 107 | additionalLanguages: ['bash', 'json', 'yaml'], 108 | }, 109 | colorMode: { 110 | defaultMode: 'dark', 111 | respectPrefersColorScheme: false, 112 | }, 113 | zoom: { 114 | selector: 'img', 115 | config: { 116 | background: { 117 | light: 'rgb(255, 255, 255)', 118 | dark: 'rgb(50, 50, 50)', 119 | }, 120 | }, 121 | }, 122 | }), 123 | 124 | themes: [ 125 | [ 126 | require.resolve('@easyops-cn/docusaurus-search-local'), 127 | { 128 | hashed: true, 129 | indexBlog: false, 130 | }, 131 | ], 132 | ], 133 | 134 | plugins: [ 135 | 'docusaurus-plugin-sass', 136 | require.resolve('docusaurus-plugin-image-zoom'), 137 | ], 138 | }; 139 | 140 | module.exports = config; 141 | -------------------------------------------------------------------------------- /apps/docs/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "projectType": "application", 5 | "sourceRoot": "apps/docs/src", 6 | "targets": { 7 | "build": { 8 | "executor": "@nx-plus/docusaurus:browser", 9 | "options": { 10 | "outputPath": "dist/apps/docs" 11 | } 12 | }, 13 | "serve": { 14 | "executor": "@nx-plus/docusaurus:dev-server", 15 | "options": { 16 | "port": 3002, 17 | "host": "0.0.0.0" 18 | } 19 | }, 20 | "deploy": { 21 | "executor": "nx:run-commands", 22 | "options": { 23 | "cwd": "apps/docs", 24 | "command": "ts-node deploy.ts" 25 | } 26 | } 27 | }, 28 | "tags": [] 29 | } 30 | -------------------------------------------------------------------------------- /apps/docs/sidebars.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Creating a sidebar enables you to: 3 | - create an ordered group of docs 4 | - render a sidebar for each doc of that group 5 | - provide next/previous navigation 6 | 7 | The sidebars can be generated from the filesystem, or explicitly defined here. 8 | 9 | Create as many sidebars as you want. 10 | */ 11 | 12 | // @ts-check 13 | 14 | /** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */ 15 | const sidebars = { 16 | // By default, Docusaurus generates a sidebar from the docs folder structure 17 | tutorialSidebar: [{ type: 'autogenerated', dirName: '.' }], 18 | 19 | // But you can create a sidebar manually 20 | /* 21 | tutorialSidebar: [ 22 | { 23 | type: 'category', 24 | label: 'Tutorial', 25 | items: ['hello'], 26 | }, 27 | ], 28 | */ 29 | }; 30 | 31 | module.exports = sidebars; 32 | -------------------------------------------------------------------------------- /apps/docs/src/css/custom.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --ifm-color-primary: #9770c9; 3 | --ifm-color-secondary: #d5a3e9; 4 | --ifm-background-color: #f5f5ff; 5 | } 6 | 7 | .docusaurus-highlight-code-line { 8 | background-color: rgba(0, 0, 0, 0.1); 9 | display: block; 10 | margin: 0 calc(-1 * var(--ifm-pre-padding)); 11 | padding: 0 var(--ifm-pre-padding); 12 | } 13 | 14 | html[data-theme='dark'] .docusaurus-highlight-code-line { 15 | background-color: rgba(0, 0, 0, 0.3); 16 | } 17 | 18 | .medium-zoom-overlay, 19 | .medium-zoom-image { 20 | z-index: 9999; 21 | } 22 | -------------------------------------------------------------------------------- /apps/docs/src/pages/index.module.scss: -------------------------------------------------------------------------------- 1 | .banner { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | justify-content: center; 6 | padding: 4rem 0; 7 | background-color: var(--ifm-color-primary); 8 | } 9 | 10 | .bannerCode { 11 | width: 500px; 12 | max-width: calc(100% - 20px); 13 | } 14 | 15 | .header { 16 | text-align: center; 17 | 18 | > h1 { 19 | font-size: 3rem; 20 | } 21 | > p { 22 | font-size: 1.5rem; 23 | font-style: italic; 24 | } 25 | } 26 | 27 | .buttons { 28 | display: flex; 29 | gap: 20px; 30 | justify-content: center; 31 | margin-top: 40px; 32 | 33 | &:first-child { 34 | background-color: var(--ifm-color-primary); 35 | } 36 | } 37 | 38 | .infoContainer { 39 | display: flex; 40 | flex-direction: column; 41 | align-items: center; 42 | gap: 100px; 43 | padding: 4rem 0; 44 | } 45 | 46 | .infoSection { 47 | position: relative; 48 | display: flex; 49 | flex-direction: column; 50 | align-items: center; 51 | width: calc(100% - 20px); 52 | max-width: 600px; 53 | 54 | img { 55 | width: 100%; 56 | border-radius: 15px; 57 | } 58 | ul { 59 | text-align: center; 60 | list-style: inside; 61 | 62 | li { 63 | margin-left: -30px; 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /apps/docs/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import Link from '@docusaurus/Link'; 2 | import { useColorMode } from '@docusaurus/theme-common'; 3 | import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; 4 | import CodeBlock from '@theme/CodeBlock'; 5 | import Layout from '@theme/Layout'; 6 | import dedent from 'dedent'; 7 | import React from 'react'; 8 | import styles from './index.module.scss'; 9 | 10 | const HomepageHeader = () => { 11 | const { siteConfig } = useDocusaurusContext(); 12 | 13 | return ( 14 |
15 |
16 |

{siteConfig.title}

17 |

{siteConfig.tagline}

18 |
19 | 20 | 21 | {dedent.withOptions({ escapeSpecialCharacters: false })` 22 | docker container run -it \ 23 | -p 80:3001 \ 24 | -v /:/mnt/host:ro \ 25 | --privileged \ 26 | mauricenino/dashdot 27 | `} 28 | 29 | 30 |
31 | 35 | Installation 36 | 37 | 41 | Configuration 42 | 43 |
44 |
45 | ); 46 | }; 47 | 48 | const HomePageInfo = () => { 49 | const { colorMode } = useColorMode(); 50 | 51 | return ( 52 |
53 |
54 |

dash. is beautiful

55 | {colorMode === 'dark' ? ( 56 | Dark-Mode 57 | ) : ( 58 | Light-Mode 59 | )} 60 |
61 | 62 |
63 |

dash. is feature-rich

64 | 65 |
    66 |
  • Dark/Light-Mode
  • 67 |
  • Customizable Widgets
  • 68 |
  • Beautiful Animations and Styles
  • 69 |
  • Support for multiple architectures
  • 70 |
  • A lot of personalization options
  • 71 |
72 |
73 |
74 | ); 75 | }; 76 | 77 | export default function Home(): JSX.Element { 78 | return ( 79 | 83 | 84 | 85 | 86 | ); 87 | } 88 | -------------------------------------------------------------------------------- /apps/docs/static/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MauriceNino/dashdot/483b75a0384abd5aba5eee5ad90afd373aab45ec/apps/docs/static/.nojekyll -------------------------------------------------------------------------------- /apps/docs/static/CNAME: -------------------------------------------------------------------------------- 1 | getdashdot.com -------------------------------------------------------------------------------- /apps/docs/static/img/cpu_view_normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MauriceNino/dashdot/483b75a0384abd5aba5eee5ad90afd373aab45ec/apps/docs/static/img/cpu_view_normal.png -------------------------------------------------------------------------------- /apps/docs/static/img/cpu_view_temps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MauriceNino/dashdot/483b75a0384abd5aba5eee5ad90afd373aab45ec/apps/docs/static/img/cpu_view_temps.png -------------------------------------------------------------------------------- /apps/docs/static/img/cpu_view_temps_fahrenheit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MauriceNino/dashdot/483b75a0384abd5aba5eee5ad90afd373aab45ec/apps/docs/static/img/cpu_view_temps_fahrenheit.png -------------------------------------------------------------------------------- /apps/docs/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MauriceNino/dashdot/483b75a0384abd5aba5eee5ad90afd373aab45ec/apps/docs/static/img/favicon.ico -------------------------------------------------------------------------------- /apps/docs/static/img/heimdall_integration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MauriceNino/dashdot/483b75a0384abd5aba5eee5ad90afd373aab45ec/apps/docs/static/img/heimdall_integration.png -------------------------------------------------------------------------------- /apps/docs/static/img/homarr_integration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MauriceNino/dashdot/483b75a0384abd5aba5eee5ad90afd373aab45ec/apps/docs/static/img/homarr_integration.png -------------------------------------------------------------------------------- /apps/docs/static/img/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MauriceNino/dashdot/483b75a0384abd5aba5eee5ad90afd373aab45ec/apps/docs/static/img/logo512.png -------------------------------------------------------------------------------- /apps/docs/static/img/network_bits_per_second.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MauriceNino/dashdot/483b75a0384abd5aba5eee5ad90afd373aab45ec/apps/docs/static/img/network_bits_per_second.png -------------------------------------------------------------------------------- /apps/docs/static/img/network_bytes_per_second.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MauriceNino/dashdot/483b75a0384abd5aba5eee5ad90afd373aab45ec/apps/docs/static/img/network_bytes_per_second.png -------------------------------------------------------------------------------- /apps/docs/static/img/screenshot_darkmode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MauriceNino/dashdot/483b75a0384abd5aba5eee5ad90afd373aab45ec/apps/docs/static/img/screenshot_darkmode.png -------------------------------------------------------------------------------- /apps/docs/static/img/screenshot_darkmode_alternate_order.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MauriceNino/dashdot/483b75a0384abd5aba5eee5ad90afd373aab45ec/apps/docs/static/img/screenshot_darkmode_alternate_order.png -------------------------------------------------------------------------------- /apps/docs/static/img/screenshot_darkmode_with_perc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MauriceNino/dashdot/483b75a0384abd5aba5eee5ad90afd373aab45ec/apps/docs/static/img/screenshot_darkmode_with_perc.png -------------------------------------------------------------------------------- /apps/docs/static/img/screenshot_lightmode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MauriceNino/dashdot/483b75a0384abd5aba5eee5ad90afd373aab45ec/apps/docs/static/img/screenshot_lightmode.png -------------------------------------------------------------------------------- /apps/docs/static/img/server_view_normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MauriceNino/dashdot/483b75a0384abd5aba5eee5ad90afd373aab45ec/apps/docs/static/img/server_view_normal.png -------------------------------------------------------------------------------- /apps/docs/static/img/server_view_with_host.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MauriceNino/dashdot/483b75a0384abd5aba5eee5ad90afd373aab45ec/apps/docs/static/img/server_view_with_host.png -------------------------------------------------------------------------------- /apps/docs/static/img/storage_view_multi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MauriceNino/dashdot/483b75a0384abd5aba5eee5ad90afd373aab45ec/apps/docs/static/img/storage_view_multi.png -------------------------------------------------------------------------------- /apps/docs/static/img/storage_view_single.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MauriceNino/dashdot/483b75a0384abd5aba5eee5ad90afd373aab45ec/apps/docs/static/img/storage_view_single.png -------------------------------------------------------------------------------- /apps/docs/static/img/version_bottom_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MauriceNino/dashdot/483b75a0384abd5aba5eee5ad90afd373aab45ec/apps/docs/static/img/version_bottom_right.png -------------------------------------------------------------------------------- /apps/docs/static/img/version_server_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MauriceNino/dashdot/483b75a0384abd5aba5eee5ad90afd373aab45ec/apps/docs/static/img/version_server_hover.png -------------------------------------------------------------------------------- /apps/docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "esModuleInterop": true, 6 | "jsx": "react", 7 | "lib": ["DOM"], 8 | "noEmit": true, 9 | "noImplicitAny": false, 10 | "types": [ 11 | "node", 12 | "@docusaurus/module-type-aliases", 13 | "@docusaurus/theme-classic" 14 | ], 15 | "baseUrl": ".", 16 | "paths": { 17 | "@site/*": ["./*"] 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /apps/server/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": {} 8 | }, 9 | { 10 | "files": ["*.ts", "*.tsx"], 11 | "rules": {} 12 | }, 13 | { 14 | "files": ["*.js", "*.jsx"], 15 | "rules": {} 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /apps/server/__TESTS__/dynamic-info.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { CONFIG } from '../src/config'; 3 | import { DynamicStorageMapper } from '../src/data/storage/dynamic'; 4 | import { 5 | TEST_CASE_14, 6 | TEST_CASE_15, 7 | TEST_CASE_16, 8 | TEST_CASE_17, 9 | TEST_CASE_18, 10 | TEST_CASE_19, 11 | TEST_CASE_20, 12 | TEST_CASE_21, 13 | TEST_CASE_22, 14 | TEST_CASE_23, 15 | TEST_CASE_24, 16 | TEST_CASE_25, 17 | TEST_CASE_26, 18 | TestCase, 19 | } from './test-cases'; 20 | 21 | const toStorageInp = (inp: TestCase) => 22 | [inp.layout, inp.blocks, inp.sizes] as const; 23 | 24 | describe('Dynamic Info', () => { 25 | beforeEach(() => { 26 | CONFIG.running_in_docker = true; 27 | CONFIG.fs_device_filter = []; 28 | }); 29 | 30 | describe('Storage', () => { 31 | it('Test Case 14', () => { 32 | const output = new DynamicStorageMapper( 33 | false, 34 | ...toStorageInp(TEST_CASE_14) 35 | ).getMappedLayout(); 36 | expect(output).to.deep.equal(TEST_CASE_14.output); 37 | }); 38 | it('Test Case 15', () => { 39 | CONFIG.running_in_docker = false; 40 | const output = new DynamicStorageMapper( 41 | true, 42 | ...toStorageInp(TEST_CASE_15) 43 | ).getMappedLayout(); 44 | expect(output).to.deep.equal(TEST_CASE_15.output); 45 | }); 46 | it('Test Case 16', () => { 47 | CONFIG.running_in_docker = false; 48 | const output = new DynamicStorageMapper( 49 | true, 50 | ...toStorageInp(TEST_CASE_16) 51 | ).getMappedLayout(); 52 | expect(output).to.deep.equal(TEST_CASE_16.output); 53 | }); 54 | it('Test Case 17', () => { 55 | const output = new DynamicStorageMapper( 56 | false, 57 | ...toStorageInp(TEST_CASE_17) 58 | ).getMappedLayout(); 59 | expect(output).to.deep.equal(TEST_CASE_17.output); 60 | }); 61 | it('Test Case 18', () => { 62 | const output = new DynamicStorageMapper( 63 | false, 64 | ...toStorageInp(TEST_CASE_18) 65 | ).getMappedLayout(); 66 | expect(output).to.deep.equal(TEST_CASE_18.output); 67 | }); 68 | it('Test Case 19', () => { 69 | CONFIG.fs_device_filter = ['sda']; 70 | const output = new DynamicStorageMapper( 71 | false, 72 | ...toStorageInp(TEST_CASE_19) 73 | ).getMappedLayout(); 74 | expect(output).to.deep.equal(TEST_CASE_19.output); 75 | }); 76 | it('Test Case 20', () => { 77 | const output = new DynamicStorageMapper( 78 | false, 79 | ...toStorageInp(TEST_CASE_20) 80 | ).getMappedLayout(); 81 | expect(output).to.deep.equal(TEST_CASE_20.output); 82 | }); 83 | it('Test Case 21', () => { 84 | const output = new DynamicStorageMapper( 85 | false, 86 | ...toStorageInp(TEST_CASE_21) 87 | ).getMappedLayout(); 88 | expect(output).to.deep.equal(TEST_CASE_21.output); 89 | }); 90 | it('Test Case 22', () => { 91 | const output = new DynamicStorageMapper( 92 | false, 93 | ...toStorageInp(TEST_CASE_22) 94 | ).getMappedLayout(); 95 | expect(output).to.deep.equal(TEST_CASE_22.output); 96 | }); 97 | it('Test Case 23', () => { 98 | const output = new DynamicStorageMapper( 99 | false, 100 | ...toStorageInp(TEST_CASE_23) 101 | ).getMappedLayout(); 102 | expect(output).to.deep.equal(TEST_CASE_23.output); 103 | }); 104 | it('Test Case 24', () => { 105 | const output = new DynamicStorageMapper( 106 | false, 107 | ...toStorageInp(TEST_CASE_24) 108 | ).getMappedLayout(); 109 | expect(output).to.deep.equal(TEST_CASE_24.output); 110 | }); 111 | it('Test Case 25', () => { 112 | const output = new DynamicStorageMapper( 113 | false, 114 | ...toStorageInp(TEST_CASE_25) 115 | ).getMappedLayout(); 116 | expect(output).to.deep.equal(TEST_CASE_25.output); 117 | }); 118 | it('Test Case 26', () => { 119 | const output = new DynamicStorageMapper( 120 | false, 121 | ...toStorageInp(TEST_CASE_26) 122 | ).getMappedLayout(); 123 | expect(output).to.deep.equal(TEST_CASE_26.output); 124 | }); 125 | }); 126 | }); 127 | -------------------------------------------------------------------------------- /apps/server/__TESTS__/static-info.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { CONFIG } from '../src/config'; 3 | import { mapToStorageLayout } from '../src/data/storage/static'; 4 | import { 5 | TEST_CASE_14, 6 | TEST_CASE_15, 7 | TEST_CASE_16, 8 | TEST_CASE_17, 9 | TEST_CASE_18, 10 | TEST_CASE_19, 11 | TEST_CASE_20, 12 | TEST_CASE_21, 13 | TEST_CASE_22, 14 | TEST_CASE_23, 15 | TEST_CASE_24, 16 | TEST_CASE_25, 17 | TEST_CASE_26, 18 | TestCase, 19 | } from './test-cases'; 20 | 21 | const toStorageInp = (inp: TestCase) => 22 | [inp.disks, inp.blocks, inp.sizes] as const; 23 | 24 | describe('Static Info', () => { 25 | beforeEach(() => { 26 | CONFIG.running_in_docker = true; 27 | CONFIG.fs_device_filter = []; 28 | }); 29 | 30 | describe('Storage', () => { 31 | it('Test Case 14', () => { 32 | const output = mapToStorageLayout(false, ...toStorageInp(TEST_CASE_14)); 33 | expect(output).to.deep.equal(TEST_CASE_14.layout); 34 | }); 35 | it('Test Case 15', () => { 36 | CONFIG.running_in_docker = false; 37 | const output = mapToStorageLayout(true, ...toStorageInp(TEST_CASE_15)); 38 | expect(output).to.deep.equal(TEST_CASE_15.layout); 39 | }); 40 | it('Test Case 16', () => { 41 | CONFIG.running_in_docker = false; 42 | const output = mapToStorageLayout(true, ...toStorageInp(TEST_CASE_16)); 43 | expect(output).to.deep.equal(TEST_CASE_16.layout); 44 | }); 45 | it('Test Case 17', () => { 46 | const output = mapToStorageLayout(false, ...toStorageInp(TEST_CASE_17)); 47 | expect(output).to.deep.equal(TEST_CASE_17.layout); 48 | }); 49 | it('Test Case 18', () => { 50 | const output = mapToStorageLayout(false, ...toStorageInp(TEST_CASE_18)); 51 | expect(output).to.deep.equal(TEST_CASE_18.layout); 52 | }); 53 | it('Test Case 19', () => { 54 | CONFIG.fs_device_filter = ['sda']; 55 | const output = mapToStorageLayout(false, ...toStorageInp(TEST_CASE_19)); 56 | expect(output).to.deep.equal(TEST_CASE_19.layout); 57 | }); 58 | it('Test Case 20', () => { 59 | const output = mapToStorageLayout(false, ...toStorageInp(TEST_CASE_20)); 60 | expect(output).to.deep.equal(TEST_CASE_20.layout); 61 | }); 62 | it('Test Case 21', () => { 63 | const output = mapToStorageLayout(false, ...toStorageInp(TEST_CASE_21)); 64 | expect(output).to.deep.equal(TEST_CASE_21.layout); 65 | }); 66 | it('Test Case 22', () => { 67 | CONFIG.fs_device_filter = ['sda', 'sdb', 'sdd']; 68 | const output = mapToStorageLayout(false, ...toStorageInp(TEST_CASE_22)); 69 | expect(output).to.deep.equal(TEST_CASE_22.layout); 70 | }); 71 | it('Test Case 23', () => { 72 | const output = mapToStorageLayout(false, ...toStorageInp(TEST_CASE_23)); 73 | expect(output).to.deep.equal(TEST_CASE_23.layout); 74 | }); 75 | it('Test Case 24', () => { 76 | const output = mapToStorageLayout(false, ...toStorageInp(TEST_CASE_24)); 77 | expect(output).to.deep.equal(TEST_CASE_24.layout); 78 | }); 79 | it('Test Case 25', () => { 80 | const output = mapToStorageLayout(false, ...toStorageInp(TEST_CASE_25)); 81 | expect(output).to.deep.equal(TEST_CASE_25.layout); 82 | }); 83 | it('Test Case 26', () => { 84 | const output = mapToStorageLayout(false, ...toStorageInp(TEST_CASE_26)); 85 | expect(output).to.deep.equal(TEST_CASE_26.layout); 86 | }); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /apps/server/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: 'server', 4 | preset: '../../jest.preset.js', 5 | testEnvironment: 'node', 6 | transform: { 7 | '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }], 8 | }, 9 | moduleFileExtensions: ['ts', 'js', 'html'], 10 | coverageDirectory: '../../coverage/apps/server', 11 | }; 12 | -------------------------------------------------------------------------------- /apps/server/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "sourceRoot": "apps/server/src", 5 | "projectType": "application", 6 | "targets": { 7 | "build": { 8 | "executor": "@nx/webpack:webpack", 9 | "outputs": ["{options.outputPath}"], 10 | "defaultConfiguration": "production", 11 | "options": { 12 | "target": "node", 13 | "compiler": "tsc", 14 | "outputPath": "dist/apps/server", 15 | "main": "apps/server/src/index.ts", 16 | "tsConfig": "apps/server/tsconfig.app.json", 17 | "assets": [], 18 | "webpackConfig": "apps/server/webpack.config.js" 19 | }, 20 | "configurations": { 21 | "development": {}, 22 | "production": { 23 | "externalDependencies": "none", 24 | "optimization": true, 25 | "fileReplacements": [ 26 | { 27 | "replace": "apps/server/src/environments/environment.ts", 28 | "with": "apps/server/src/environments/environment.prod.ts" 29 | } 30 | ] 31 | } 32 | } 33 | }, 34 | "serve": { 35 | "executor": "@nx/js:node", 36 | "defaultConfiguration": "development", 37 | "options": { 38 | "buildTarget": "server:build", 39 | "host": "0.0.0.0" 40 | }, 41 | "configurations": { 42 | "development": { 43 | "buildTarget": "server:build:development" 44 | }, 45 | "production": { 46 | "buildTarget": "server:build:production" 47 | } 48 | } 49 | }, 50 | "lint": { 51 | "executor": "@nx/eslint:lint", 52 | "outputs": ["{options.outputFile}"] 53 | }, 54 | "test": { 55 | "executor": "@nx/jest:jest", 56 | "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], 57 | "options": { 58 | "jestConfig": "apps/server/jest.config.ts" 59 | }, 60 | "dependsOn": [ 61 | { 62 | "projects": "self", 63 | "target": "lint", 64 | "params": "ignore" 65 | } 66 | ] 67 | } 68 | }, 69 | "tags": [] 70 | } 71 | -------------------------------------------------------------------------------- /apps/server/src/config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import { Config } from '@dash/common'; 3 | 4 | const numNull = (val: string | undefined): number | undefined => { 5 | if (val === undefined || val === '') { 6 | return undefined; 7 | } 8 | return +val; 9 | }; 10 | 11 | const penv = (key: string): string | undefined => process.env[`DASHDOT_${key}`]; 12 | const lst = (item: string): string[] => (item === '' ? [] : item.split(',')); 13 | const numlst = (item: string): number[] => lst(item).map(numNull); 14 | const kv = ( 15 | inp: string[], 16 | toNum: T 17 | ): Record => 18 | Object.fromEntries( 19 | inp.map(p => { 20 | const [key, value] = p.split('='); 21 | if (toNum) { 22 | return [key, +value]; 23 | } else { 24 | return [key, value]; 25 | } 26 | }) 27 | ); 28 | 29 | export const CONFIG: Config = { 30 | port: numNull(penv('PORT')) ?? 3001, 31 | running_in_docker: penv('RUNNING_IN_DOCKER') === 'true', 32 | accept_ookla_eula: penv('ACCEPT_OOKLA_EULA') === 'true', 33 | use_network_interface: penv('USE_NETWORK_INTERFACE'), 34 | speed_test_from_path: penv('SPEED_TEST_FROM_PATH'), 35 | fs_device_filter: lst(penv('FS_DEVICE_FILTER') ?? ''), 36 | fs_type_filter: lst( 37 | penv('FS_TYPE_FILTER') ?? 38 | 'cifs,9p,fuse.rclone,fuse.mergerfs,nfs4,iso9660,fuse.shfs,autofs' 39 | ), 40 | fs_virtual_mounts: lst(penv('FS_VIRTUAL_MOUNTS') ?? ''), 41 | disable_integrations: penv('DISABLE_INTEGRATIONS') === 'true', 42 | 43 | show_dash_version: (penv('SHOW_DASH_VERSION') as any) ?? 'icon_hover', 44 | show_host: penv('SHOW_HOST') === 'true', 45 | custom_host: penv('CUSTOM_HOST'), 46 | page_title: penv('PAGE_TITLE') ?? 'dash.', 47 | use_imperial: penv('USE_IMPERIAL') === 'true', 48 | network_speed_as_bytes: penv('NETWORK_SPEED_AS_BYTES') === 'true', 49 | always_show_percentages: penv('ALWAYS_SHOW_PERCENTAGES') === 'true', 50 | enable_cpu_temps: penv('ENABLE_CPU_TEMPS') === 'true', 51 | cpu_temps_mode: (penv('CPU_TEMPS_MODE') as any) ?? 'avg', 52 | 53 | widget_list: lst( 54 | penv('WIDGET_LIST') ?? 'os,cpu,storage,ram,network' 55 | ) as any[], 56 | os_label_list: lst(penv('OS_LABEL_LIST') ?? 'os,arch,up_since') as any[], 57 | cpu_label_list: lst( 58 | penv('CPU_LABEL_LIST') ?? 'brand,model,cores,threads,frequency' 59 | ) as any[], 60 | storage_label_list: lst( 61 | penv('STORAGE_LABEL_LIST') ?? 'brand,type,size,raid' 62 | ) as any[], 63 | ram_label_list: lst( 64 | penv('RAM_LABEL_LIST') ?? 'brand,type,size,frequency' 65 | ) as any[], 66 | network_label_list: lst( 67 | penv('NETWORK_LABEL_LIST') ?? 'type,speed_up,speed_down,interface_speed' 68 | ) as any[], 69 | gpu_label_list: lst(penv('GPU_LABEL_LIST') ?? 'brand,model,memory') as any[], 70 | 71 | os_widget_grow: numNull(penv('OS_WIDGET_GROW')) ?? 2.5, 72 | os_widget_min_width: numNull(penv('OS_WIDGET_MIN_WIDTH')) ?? 300, 73 | 74 | cpu_widget_grow: numNull(penv('CPU_WIDGET_GROW')) ?? 4, 75 | cpu_widget_min_width: numNull(penv('CPU_WIDGET_MIN_WIDTH')) ?? 500, 76 | cpu_shown_datapoints: numNull(penv('CPU_SHOWN_DATAPOINTS')) ?? 20, 77 | cpu_poll_interval: numNull(penv('CPU_POLL_INTERVAL')) ?? 1000, 78 | cpu_cores_toggle_mode: (penv('CPU_CORES_TOGGLE_MODE') as any) ?? 'toggle', 79 | 80 | storage_widget_items_per_page: 81 | numNull(penv('STORAGE_WIDGET_ITEMS_PER_PAGE')) ?? 3, 82 | storage_widget_grow: numNull(penv('STORAGE_WIDGET_GROW')) ?? 3.5, 83 | storage_widget_min_width: numNull(penv('STORAGE_WIDGET_MIN_WIDTH')) ?? 500, 84 | storage_poll_interval: numNull(penv('STORAGE_POLL_INTERVAL')) ?? 60000, 85 | 86 | ram_widget_grow: numNull(penv('RAM_WIDGET_GROW')) ?? 4, 87 | ram_widget_min_width: numNull(penv('RAM_WIDGET_MIN_WIDTH')) ?? 500, 88 | ram_shown_datapoints: numNull(penv('RAM_SHOWN_DATAPOINTS')) ?? 20, 89 | ram_poll_interval: numNull(penv('RAM_POLL_INTERVAL')) ?? 1000, 90 | 91 | speed_test_interval: numNull(penv('SPEED_TEST_INTERVAL')) ?? 60 * 4, 92 | speed_test_interval_cron: penv('SPEED_TEST_INTERVAL_CRON'), 93 | network_widget_grow: numNull(penv('NETWORK_WIDGET_GROW')) ?? 6, 94 | network_widget_min_width: numNull(penv('NETWORK_WIDGET_MIN_WIDTH')) ?? 500, 95 | network_shown_datapoints: numNull(penv('NETWORK_SHOWN_DATAPOINTS')) ?? 20, 96 | network_poll_interval: numNull(penv('NETWORK_POLL_INTERVAL')) ?? 1000, 97 | 98 | gpu_widget_grow: numNull(penv('GPU_WIDGET_GROW')) ?? 6, 99 | gpu_widget_min_width: numNull(penv('GPU_WIDGET_MIN_WIDTH')) ?? 700, 100 | gpu_shown_datapoints: numNull(penv('GPU_SHOWN_DATAPOINTS')) ?? 20, 101 | gpu_poll_interval: numNull(penv('GPU_POLL_INTERVAL')) ?? 1000, 102 | 103 | override: { 104 | os: penv('OVERRIDE_OS'), 105 | arch: penv('OVERRIDE_ARCH'), 106 | cpu_brand: penv('OVERRIDE_CPU_BRAND'), 107 | cpu_model: penv('OVERRIDE_CPU_MODEL'), 108 | cpu_cores: numNull(penv('OVERRIDE_CPU_CORES')), 109 | cpu_threads: numNull(penv('OVERRIDE_CPU_THREADS')), 110 | cpu_frequency: numNull(penv('OVERRIDE_CPU_FREQUENCY')), 111 | ram_brand: penv('OVERRIDE_RAM_BRAND'), 112 | ram_size: numNull(penv('OVERRIDE_RAM_SIZE')), 113 | ram_type: penv('OVERRIDE_RAM_TYPE'), 114 | ram_frequency: numNull(penv('OVERRIDE_RAM_FREQUENCY')), 115 | network_type: penv('OVERRIDE_NETWORK_TYPE'), 116 | network_speed_up: numNull(penv('OVERRIDE_NETWORK_SPEED_UP')), 117 | network_speed_down: numNull(penv('OVERRIDE_NETWORK_SPEED_DOWN')), 118 | network_interface_speed: numNull(penv('OVERRIDE_NETWORK_INTERFACE_SPEED')), 119 | network_public_ip: penv('OVERRIDE_NETWORK_PUBLIC_IP'), 120 | storage_brands: kv(lst(penv('OVERRIDE_STORAGE_BRANDS') ?? ''), false), 121 | storage_sizes: kv(lst(penv('OVERRIDE_STORAGE_SIZES') ?? ''), true), 122 | storage_types: kv(lst(penv('OVERRIDE_STORAGE_TYPES') ?? ''), false), 123 | gpu_brands: lst(penv('OVERRIDE_GPU_BRANDS') ?? ''), 124 | gpu_models: lst(penv('OVERRIDE_GPU_MODELS') ?? ''), 125 | gpu_memories: numlst(penv('OVERRIDE_GPU_MEMORIES') ?? ''), 126 | }, 127 | }; 128 | -------------------------------------------------------------------------------- /apps/server/src/data/cpu.ts: -------------------------------------------------------------------------------- 1 | import { CpuInfo, CpuLoad } from '@dash/common'; 2 | import * as si from 'systeminformation'; 3 | import { CONFIG } from '../config'; 4 | import { getStaticServerInfo } from '../static-info'; 5 | 6 | const normalizeCpuModel = (cpuModel: string) => { 7 | return cpuModel 8 | .replace(/Processor/g, '') 9 | .replace(/[A-Za-z0-9]*-Core/g, '') 10 | .trim(); 11 | }; 12 | 13 | export default { 14 | dynamic: async (): Promise => { 15 | const { cpu } = await getStaticServerInfo(); 16 | const cpuLoad = (await si.currentLoad()).cpus; 17 | 18 | let temps: si.Systeminformation.CpuTemperatureData['cores'] = []; 19 | let mainTemp = 0; 20 | if (CONFIG.enable_cpu_temps) { 21 | const cpuTemp = await si.cpuTemperature(); 22 | const threadsPerCore = (cpu.threads - cpu.ecores) / cpu.pcores; 23 | temps = cpuTemp.cores.flatMap((temp, i) => 24 | i < cpu.pcores ? Array(threadsPerCore).fill(temp) : temp 25 | ); 26 | mainTemp = cpuTemp.main; // AVG temp of all cores, in case no per-core data is found 27 | } 28 | 29 | return cpuLoad.map(({ load }, i) => ({ 30 | load, 31 | temp: temps[i] ?? mainTemp, 32 | core: i, 33 | })); 34 | }, 35 | static: async (): Promise => { 36 | const cpuInfo = await si.cpu(); 37 | 38 | return { 39 | brand: cpuInfo.manufacturer, 40 | model: normalizeCpuModel(cpuInfo.brand), 41 | cores: cpuInfo.physicalCores, 42 | ecores: cpuInfo.efficiencyCores, 43 | pcores: cpuInfo.performanceCores, 44 | threads: cpuInfo.cores, 45 | frequency: cpuInfo.speedMax, 46 | }; 47 | }, 48 | }; 49 | -------------------------------------------------------------------------------- /apps/server/src/data/gpu.ts: -------------------------------------------------------------------------------- 1 | import { GpuInfo, GpuLoad } from '@dash/common'; 2 | import * as si from 'systeminformation'; 3 | 4 | const normalizeGpuBrand = (brand: string) => { 5 | return brand ? brand.replace(/(corporation)/gi, '').trim() : undefined; 6 | }; 7 | 8 | const normalizeGpuName = (name: string) => { 9 | return name ? name.replace(/(nvidia|amd|intel)/gi, '').trim() : undefined; 10 | }; 11 | 12 | const normalizeGpuModel = (model: string) => { 13 | return model ? model.replace(/\[.*\]/gi, '').trim() : undefined; 14 | }; 15 | 16 | const isValidController = (controller: si.Systeminformation.GraphicsControllerData) => { 17 | const blacklist = ['monitor']; 18 | const model = controller.model.toLowerCase(); 19 | return blacklist.every( w => ! model.includes(w) ); 20 | }; 21 | 22 | export default { 23 | dynamic: async (): Promise => { 24 | const gpuInfo = await si.graphics(); 25 | 26 | return { 27 | layout: gpuInfo.controllers.filter(isValidController).map(controller => ({ 28 | load: controller.utilizationGpu ?? 0, 29 | memory: controller.utilizationMemory ?? 0, 30 | })), 31 | }; 32 | }, 33 | static: async (): Promise => { 34 | const gpuInfo = await si.graphics(); 35 | 36 | return { 37 | layout: gpuInfo.controllers.filter(isValidController).map(controller => ({ 38 | brand: normalizeGpuBrand(controller.vendor), 39 | model: 40 | normalizeGpuName(controller.name) ?? 41 | normalizeGpuModel(controller.model), 42 | memory: controller.memoryTotal ?? controller.vram ?? 0, 43 | })), 44 | }; 45 | }, 46 | }; 47 | -------------------------------------------------------------------------------- /apps/server/src/data/os.ts: -------------------------------------------------------------------------------- 1 | import { OsInfo } from '@dash/common'; 2 | import { exec } from 'child_process'; 3 | import * as si from 'systeminformation'; 4 | import { promisify } from 'util'; 5 | 6 | const execp = promisify(exec); 7 | 8 | export default { 9 | static: async (): Promise => { 10 | const osInfo = await si.osInfo(); 11 | 12 | const buildInfo = JSON.parse( 13 | (await execp('cat version.json') 14 | .then(({ stdout }) => stdout) 15 | .catch(() => undefined)) ?? '{}' 16 | ); 17 | const gitHash = await execp('git log -1 --format="%H"') 18 | .then(({ stdout }) => stdout.trim()) 19 | .catch(() => undefined); 20 | const dash_version = buildInfo.version ?? 'unknown'; 21 | const dash_buildhash = buildInfo.buildhash ?? gitHash; 22 | 23 | return { 24 | arch: osInfo.arch, 25 | distro: osInfo.distro, 26 | kernel: osInfo.kernel, 27 | platform: osInfo.platform, 28 | release: 29 | osInfo.release === 'unknown' 30 | ? osInfo.build || 'unknown' 31 | : osInfo.release, 32 | uptime: 0, 33 | dash_version, 34 | dash_buildhash, 35 | }; 36 | }, 37 | }; 38 | -------------------------------------------------------------------------------- /apps/server/src/data/ram.ts: -------------------------------------------------------------------------------- 1 | import { RamInfo, RamLoad } from '@dash/common'; 2 | import * as si from 'systeminformation'; 3 | 4 | export default { 5 | dynamic: async (): Promise => { 6 | const memInfo = (await si.mem()).active; 7 | 8 | return memInfo; 9 | }, 10 | static: async (): Promise => { 11 | const [memInfo, memLayout] = await Promise.all([si.mem(), si.memLayout()]); 12 | 13 | return { 14 | size: memInfo.total, 15 | layout: memLayout 16 | .filter(({ size }) => size !== 0) 17 | .map(({ manufacturer, type, clockSpeed }) => ({ 18 | brand: manufacturer, 19 | type: type, 20 | frequency: clockSpeed ?? undefined, 21 | })), 22 | }; 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /apps/server/src/data/storage/dynamic.ts: -------------------------------------------------------------------------------- 1 | import { StorageInfo, StorageLoad, sumUp } from '@dash/common'; 2 | import * as si from 'systeminformation'; 3 | import { CONFIG } from '../../config'; 4 | import { getStaticServerInfo } from '../../static-info'; 5 | import { PLATFORM_IS_WINDOWS, fromHost } from '../../utils'; 6 | 7 | type Block = si.Systeminformation.BlockDevicesData; 8 | type Size = si.Systeminformation.FsSizeData; 9 | 10 | export class DynamicStorageMapper { 11 | private validSizes: Size[]; 12 | 13 | constructor( 14 | private hostWin32: boolean, 15 | private layout: StorageInfo, 16 | private blocks: Block[], 17 | private sizes: Size[] 18 | ) { 19 | this.validSizes = this.getValidSizes(); 20 | } 21 | 22 | // Setup local values 23 | private getValidSizes() { 24 | return this.sizes.filter( 25 | ({ mount, type }) => 26 | (this.hostWin32 || mount.startsWith(fromHost('/'))) && 27 | !CONFIG.fs_type_filter.includes(type) 28 | ); 29 | } 30 | 31 | // Helpers 32 | private getBlocksForDisks(disks: StorageInfo[number]['disks']) { 33 | return this.blocks.filter(({ name, device }) => 34 | disks.some(d => 35 | this.hostWin32 ? d.device === device : name.startsWith(d.device) 36 | ) 37 | ); 38 | } 39 | private getBlocksForRaid(raidLabel: string, raidName: string) { 40 | return this.blocks.filter( 41 | ({ label, name }) => 42 | label.startsWith(raidLabel) || name.startsWith(raidName) 43 | ); 44 | } 45 | private getBlocksForXfs(parts: Block[]) { 46 | return this.blocks.filter( 47 | ({ uuid, type, fsType }) => 48 | type === 'md' && 49 | fsType === 'xfs' && 50 | parts.some(part => part.uuid === uuid) 51 | ); 52 | } 53 | 54 | private isRootMount(mount: string) { 55 | return ( 56 | !this.hostWin32 && 57 | (mount === fromHost('/') || mount.startsWith(fromHost('/boot'))) 58 | ); 59 | } 60 | 61 | // Get size of the mounts of the partitions/disks of device 62 | private getSizeForBlocks( 63 | deviceBlocks: Block[], 64 | diskSize: number, 65 | isHost: boolean 66 | ) { 67 | const sizes = this.validSizes.filter(size => 68 | deviceBlocks.some(block => { 69 | const matchedByMount = 70 | size.mount && 71 | (block.mount === size.mount || 72 | size.mount.endsWith(`dev-disk-by-uuid-${block.uuid}`)); 73 | const matchedByDevice = 74 | block.device && size.fs.startsWith(block.device); 75 | const matchedByHost = isHost && this.isRootMount(size.mount); 76 | 77 | return matchedByMount || matchedByDevice || matchedByHost; 78 | }) 79 | ); 80 | 81 | if (sizes.length === 0) { 82 | return -1; 83 | } 84 | 85 | const calculatedSize = sumUp(sizes, 'used'); 86 | const isLvm = deviceBlocks.some(({ fsType }) => fsType === 'LVM2_member'); 87 | 88 | if (isLvm) { 89 | return calculatedSize; 90 | } 91 | 92 | const totalAvailable = sumUp(sizes, 'size'); 93 | const preAllocated = Math.max(0, diskSize - totalAvailable); 94 | 95 | return calculatedSize + preAllocated; 96 | } 97 | 98 | public getMappedLayout() { 99 | return this.layout.map(({ size, disks, virtual, raidLabel, raidName }) => { 100 | if (virtual) { 101 | const size = this.sizes.find(s => s.fs === disks[0].device); 102 | return size?.used ?? 0; 103 | } 104 | 105 | const deviceParts = this.getBlocksForDisks(disks); 106 | const deviceBlocks = deviceParts 107 | .concat(this.getBlocksForRaid(raidLabel, raidName)) 108 | .concat(this.getBlocksForXfs(deviceParts)); 109 | 110 | const isHost = deviceBlocks.some(({ mount }) => this.isRootMount(mount)); 111 | 112 | return this.getSizeForBlocks(deviceBlocks, size, isHost); 113 | }); 114 | } 115 | } 116 | 117 | export default async (): Promise => { 118 | const svInfo = getStaticServerInfo(); 119 | const [sizes, blocks] = await Promise.all([si.fsSize(), si.blockDevices()]); 120 | 121 | return new DynamicStorageMapper( 122 | PLATFORM_IS_WINDOWS, 123 | svInfo.storage, 124 | blocks, 125 | sizes 126 | ).getMappedLayout(); 127 | }; 128 | -------------------------------------------------------------------------------- /apps/server/src/data/storage/index.ts: -------------------------------------------------------------------------------- 1 | import getDynamic from './dynamic'; 2 | import getStatic from './static'; 3 | 4 | export default { 5 | dynamic: getDynamic, 6 | static: getStatic, 7 | }; 8 | -------------------------------------------------------------------------------- /apps/server/src/data/storage/static.ts: -------------------------------------------------------------------------------- 1 | import { RaidType, StorageInfo, sumUp } from '@dash/common'; 2 | import * as si from 'systeminformation'; 3 | import { CONFIG } from '../../config'; 4 | import { PLATFORM_IS_WINDOWS } from '../../utils'; 5 | 6 | type Block = si.Systeminformation.BlockDevicesData; 7 | type Disk = si.Systeminformation.DiskLayoutData; 8 | type Size = si.Systeminformation.FsSizeData; 9 | 10 | const getDiskBlocks = (blocks: Block[]) => 11 | blocks.filter( 12 | block => 13 | block.type === 'disk' && 14 | block.size > 0 && 15 | !/^zram\d+$/.test(block.name) && 16 | !CONFIG.fs_device_filter.includes(block.name) && 17 | !CONFIG.fs_type_filter.includes(block.fsType) 18 | ); 19 | 20 | const getDiskParts = (blocks: Block[], diskBlock: Block, hostWin32: boolean) => 21 | blocks.filter( 22 | ({ name, device, type }) => 23 | type === 'part' && 24 | (hostWin32 25 | ? diskBlock.device === device 26 | : name.startsWith(diskBlock.name)) 27 | ); 28 | 29 | const getNativeDisk = ( 30 | disks: Disk[], 31 | block: Block 32 | ): { 33 | vendor: string; 34 | size: number; 35 | type: string; 36 | interfaceType?: string; 37 | } => 38 | disks.find( 39 | d => 40 | d.device === block.device || (block.model != '' && d.name === block.model) 41 | ) ?? { 42 | vendor: block.name, 43 | size: block.size, 44 | type: block.physical, 45 | }; 46 | 47 | const getVirtualMountsLayout = (sizes: Size[]) => 48 | CONFIG.fs_virtual_mounts 49 | .map(mount => { 50 | const size = sizes.find(s => s.fs === mount); 51 | 52 | return size 53 | ? { 54 | size: size.size, 55 | virtual: true, 56 | disks: [ 57 | { 58 | device: size.fs, 59 | brand: size.fs, 60 | type: 'VIRTUAL', 61 | }, 62 | ], 63 | } 64 | : undefined; 65 | }) 66 | .filter(d => d != null); 67 | 68 | const getRaidInfo = (blocks: Block[], diskBlock: Block, hostWin32: boolean) => { 69 | const allRaidBlocks = blocks.filter(block => block.type.startsWith('raid')); 70 | const partBlocks = [diskBlock, ...getDiskParts(blocks, diskBlock, hostWin32)]; 71 | const raidBlocks = allRaidBlocks.reduce( 72 | (acc, curr) => { 73 | if (partBlocks.some(({ group }) => curr.name === group)) { 74 | acc.current.push(curr); 75 | } else { 76 | acc.other.push(curr); 77 | } 78 | return acc; 79 | }, 80 | { current: [] as Block[], other: [] as Block[] } 81 | ); 82 | 83 | if (raidBlocks.current.length === 0) return; 84 | 85 | const firstRaidBlock = raidBlocks.current[0]; 86 | const splitLabel = firstRaidBlock.label.split(':')[0]; 87 | const label = firstRaidBlock.label.includes(':') 88 | ? raidBlocks.other.some(({ label }) => label.startsWith(`${splitLabel}:`)) 89 | ? firstRaidBlock.label 90 | : splitLabel 91 | : firstRaidBlock.label; 92 | 93 | return { 94 | label, 95 | type: raidBlocks.current[0].type === 'raid0' ? RaidType.ZERO : RaidType.ONE, 96 | name: raidBlocks.current[0].name, 97 | size: sumUp(raidBlocks.current, 'size'), 98 | }; 99 | }; 100 | 101 | const getDiskType = (type: string, interfaceType?: string) => { 102 | return type === 'SSD' && interfaceType === 'NVMe' ? 'NVMe' : type; 103 | }; 104 | 105 | export const mapToStorageLayout = ( 106 | hostWin32: boolean, 107 | disks: Disk[], 108 | blocks: Block[], 109 | sizes: Size[] 110 | ): StorageInfo => { 111 | const diskBlocks = getDiskBlocks(blocks); 112 | 113 | const mapDiskBlock = (acc: StorageInfo, diskBlock: Block) => { 114 | const device = diskBlock.name; 115 | const nativeDisk = getNativeDisk(disks, diskBlock); 116 | const raidInfo = getRaidInfo(blocks, diskBlock, hostWin32); 117 | 118 | const disk: StorageInfo[number]['disks'][number] = { 119 | device: hostWin32 ? diskBlock.device : device, 120 | brand: nativeDisk.vendor, 121 | type: getDiskType(nativeDisk.type, nativeDisk.interfaceType), 122 | }; 123 | 124 | if (raidInfo != null) { 125 | const existingRaid = acc.find(r => r.raidName === raidInfo.name); 126 | if (existingRaid) { 127 | existingRaid.disks.push(disk); 128 | } else { 129 | acc.push({ 130 | raidLabel: raidInfo.label, 131 | raidName: raidInfo.name, 132 | raidType: raidInfo.type, 133 | size: raidInfo.size, 134 | disks: [disk], 135 | }); 136 | } 137 | } else { 138 | acc.push({ 139 | size: nativeDisk.size, 140 | disks: [disk], 141 | }); 142 | } 143 | 144 | return acc; 145 | }; 146 | 147 | const blockLayout = ( 148 | hostWin32 149 | ? diskBlocks.reduce((acc, curr) => { 150 | if (curr.device && !acc.some(b => b.device === curr.device)) { 151 | acc.push(curr); 152 | } 153 | return acc; 154 | }, [] as Block[]) 155 | : diskBlocks 156 | ).reduce(mapDiskBlock, [] as StorageInfo); 157 | 158 | const sizesLayout = getVirtualMountsLayout(sizes); 159 | 160 | return blockLayout.concat(sizesLayout); 161 | }; 162 | 163 | export default async (): Promise => { 164 | const [disks, blocks, sizes] = await Promise.all([ 165 | si.diskLayout(), 166 | si.blockDevices(), 167 | si.fsSize(), 168 | ]); 169 | 170 | return mapToStorageLayout(PLATFORM_IS_WINDOWS, disks, blocks, sizes); 171 | }; 172 | -------------------------------------------------------------------------------- /apps/server/src/dynamic-info.ts: -------------------------------------------------------------------------------- 1 | import cron from 'node-cron'; 2 | import { interval, mergeMap, Observable, ReplaySubject, Subject } from 'rxjs'; 3 | import { inspect } from 'util'; 4 | import { CONFIG } from './config'; 5 | import getCpuInfo from './data/cpu'; 6 | import getGpuInfo from './data/gpu'; 7 | import getNetworkInfo from './data/network'; 8 | import getRamInfo from './data/ram'; 9 | import getStorageInfo from './data/storage'; 10 | import { loadInfo } from './static-info'; 11 | 12 | const createBufferedInterval = ( 13 | name: string, 14 | enabled: boolean, 15 | bufferSize: number, 16 | intervalMs: number, 17 | factory: () => Promise 18 | ): Observable => { 19 | const buffer = new ReplaySubject(bufferSize); 20 | 21 | if (enabled) { 22 | // Instantly load first value 23 | factory() 24 | .then(value => { 25 | console.log( 26 | `First measurement [${name}]:`, 27 | inspect(value, { 28 | showHidden: false, 29 | depth: null, 30 | colors: true, 31 | }) 32 | ); 33 | 34 | buffer.next(value); 35 | }) 36 | .catch(err => buffer.error(err)); 37 | 38 | // Load values every intervalMs 39 | interval(intervalMs).pipe(mergeMap(factory)).subscribe(buffer); 40 | 41 | return buffer.asObservable(); 42 | } 43 | 44 | return new Observable(); 45 | }; 46 | 47 | export const getDynamicServerInfo = () => { 48 | const cpuObs = createBufferedInterval( 49 | 'CPU', 50 | CONFIG.widget_list.includes('cpu'), 51 | CONFIG.cpu_shown_datapoints, 52 | CONFIG.cpu_poll_interval, 53 | getCpuInfo.dynamic 54 | ); 55 | 56 | const ramObs = createBufferedInterval( 57 | 'RAM', 58 | CONFIG.widget_list.includes('ram'), 59 | CONFIG.ram_shown_datapoints, 60 | CONFIG.ram_poll_interval, 61 | getRamInfo.dynamic 62 | ); 63 | 64 | const storageObs = createBufferedInterval( 65 | 'Storage', 66 | CONFIG.widget_list.includes('storage'), 67 | 1, 68 | CONFIG.storage_poll_interval, 69 | getStorageInfo.dynamic 70 | ); 71 | 72 | const networkObs = createBufferedInterval( 73 | 'Network', 74 | CONFIG.widget_list.includes('network'), 75 | CONFIG.network_shown_datapoints, 76 | CONFIG.network_poll_interval, 77 | getNetworkInfo.dynamic 78 | ); 79 | 80 | const gpuObs = createBufferedInterval( 81 | 'GPU', 82 | CONFIG.widget_list.includes('gpu'), 83 | CONFIG.gpu_shown_datapoints, 84 | CONFIG.gpu_poll_interval, 85 | getGpuInfo.dynamic 86 | ); 87 | 88 | let speedTestObs = new Observable(); 89 | 90 | if (CONFIG.widget_list.includes('network')) { 91 | if (CONFIG.speed_test_interval_cron) { 92 | const subject = new Subject(); 93 | 94 | cron.schedule(CONFIG.speed_test_interval_cron, async () => { 95 | subject.next(await loadInfo('network', getNetworkInfo.speedTest, true)); 96 | }); 97 | 98 | speedTestObs = subject.asObservable(); 99 | } else { 100 | speedTestObs = interval(CONFIG.speed_test_interval * 60 * 1000).pipe( 101 | mergeMap( 102 | async () => await loadInfo('network', getNetworkInfo.speedTest, true) 103 | ) 104 | ); 105 | } 106 | } 107 | 108 | return { 109 | cpu: cpuObs, 110 | ram: ramObs, 111 | storage: storageObs, 112 | network: networkObs, 113 | gpu: gpuObs, 114 | speedTest: speedTestObs, 115 | }; 116 | }; 117 | -------------------------------------------------------------------------------- /apps/server/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | }; 4 | -------------------------------------------------------------------------------- /apps/server/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: false, 3 | }; 4 | -------------------------------------------------------------------------------- /apps/server/src/setup.ts: -------------------------------------------------------------------------------- 1 | import { exec as exaca } from 'child_process'; 2 | import * as fs from 'fs'; 3 | import * as si from 'systeminformation'; 4 | import { promisify } from 'util'; 5 | import { CONFIG } from './config'; 6 | import { PLATFORM_IS_WINDOWS } from './utils'; 7 | 8 | const exec = promisify(exaca); 9 | 10 | const NET_PATH = CONFIG.running_in_docker 11 | ? '/mnt/host/sys/class/net/' 12 | : '/sys/class/net/'; 13 | const NET_PATH_INTERNAL = '/internal_mnt/host_sys/class/net/'; 14 | 15 | const NS_NET = '/mnt/host/proc/1/ns/net'; 16 | export let NET_INTERFACE_PATH = undefined; 17 | 18 | const getDefaultIface = async (): Promise => { 19 | if (CONFIG.use_network_interface != null) { 20 | return CONFIG.use_network_interface; 21 | } 22 | 23 | try { 24 | let ifaceStr: string; 25 | if (CONFIG.running_in_docker) { 26 | const { stdout } = await exec( 27 | `nsenter --net=${NS_NET} route | grep default | awk '{print $8}'` 28 | ); 29 | ifaceStr = stdout; 30 | } else { 31 | const { stdout } = await exec(`route | grep default | awk '{print $8}'`); 32 | ifaceStr = stdout; 33 | } 34 | 35 | const ifaces = ifaceStr.split('\n').filter(i => i !== ''); 36 | const iface = ifaces[0].trim(); 37 | 38 | if (ifaces.length > 1) { 39 | console.warn( 40 | `Multiple default network interfaces found [${ifaces.join( 41 | ', ' 42 | )}], using "${iface}"` 43 | ); 44 | } 45 | return iface; 46 | } catch (e) { 47 | console.error('Could not get default iface path'); 48 | return undefined; 49 | } 50 | }; 51 | 52 | const setupIfacePath = async (defaultIface: string) => { 53 | if (fs.existsSync(`${NET_PATH}${defaultIface}`)) { 54 | NET_INTERFACE_PATH = `${NET_PATH}${defaultIface}`; 55 | console.log(`Using default network interface "${defaultIface}"`); 56 | } else if (CONFIG.running_in_docker) { 57 | const mountpoint = `${NET_PATH_INTERNAL}${defaultIface}`; 58 | await exec(`mkdir -p /internal_mnt/host_sys/`); 59 | await exec( 60 | `mountpoint -q /internal_mnt/host_sys || nsenter --net=${NS_NET} mount -t sysfs nodevice /internal_mnt/host_sys` 61 | ); 62 | 63 | if (fs.existsSync(mountpoint)) { 64 | NET_INTERFACE_PATH = mountpoint; 65 | console.log( 66 | `Using internally mounted network interface "${defaultIface}"` 67 | ); 68 | } else { 69 | console.warn( 70 | `Network interface "${defaultIface}" not successfully mounted` 71 | ); 72 | } 73 | } else { 74 | console.warn(`No path for iface "${defaultIface}" found`); 75 | } 76 | }; 77 | 78 | export const setupNetworking = async () => { 79 | const iface = await getDefaultIface(); 80 | if (iface) { 81 | await setupIfacePath(iface); 82 | } 83 | 84 | if (!NET_INTERFACE_PATH) { 85 | console.log('Using default network interface with no modifications'); 86 | } 87 | }; 88 | 89 | const LOCAL_OS_PATHS = ['/etc/os-release', '/usr/lib/os-release']; 90 | const MNT_OS_PATH = '/mnt/host/etc/os-release'; 91 | 92 | export const setupOsVersion = async () => { 93 | try { 94 | if (CONFIG.running_in_docker && fs.existsSync(MNT_OS_PATH)) { 95 | for (const LOCAL_OS_PATH of LOCAL_OS_PATHS) { 96 | if (fs.existsSync(LOCAL_OS_PATH)) { 97 | await exec(`ln -sf ${MNT_OS_PATH} ${LOCAL_OS_PATH}`); 98 | } 99 | } 100 | 101 | console.log(`Using host os version from "${MNT_OS_PATH}"`); 102 | } else { 103 | console.log(`Using host os version from ${LOCAL_OS_PATHS.map(p => `"${p}"`).join(' and ')}`); 104 | } 105 | } catch (e) { 106 | console.warn(e); 107 | } 108 | }; 109 | 110 | export const setupHostSpecific = async () => { 111 | if (PLATFORM_IS_WINDOWS) { 112 | console.log('Acquiring Windows Persistent Powershell'); 113 | si.powerShellStart(); 114 | } 115 | }; 116 | 117 | export const tearDownHostSpecific = () => { 118 | if (PLATFORM_IS_WINDOWS) { 119 | console.log('Releasing Windows Persistent Powershell'); 120 | si.powerShellRelease(); 121 | } 122 | }; 123 | -------------------------------------------------------------------------------- /apps/server/src/static-info.ts: -------------------------------------------------------------------------------- 1 | import { HardwareInfo, ServerInfo } from '@dash/common'; 2 | import { BehaviorSubject, Observable, map } from 'rxjs'; 3 | import * as si from 'systeminformation'; 4 | import { inspect } from 'util'; 5 | import { CONFIG } from './config'; 6 | import getCpuInfo from './data/cpu'; 7 | import getGpuInfo from './data/gpu'; 8 | import getNetworkInfo from './data/network'; 9 | import getOsInfo from './data/os'; 10 | import getRamInfo from './data/ram'; 11 | import getStorageInfo from './data/storage'; 12 | 13 | const STATIC_INFO = new BehaviorSubject({ 14 | os: { 15 | arch: '', 16 | distro: '', 17 | kernel: '', 18 | platform: '', 19 | release: '', 20 | uptime: 0, 21 | dash_buildhash: '', 22 | dash_version: '', 23 | }, 24 | cpu: { 25 | brand: '', 26 | model: '', 27 | cores: 0, 28 | ecores: 0, 29 | pcores: 0, 30 | threads: 0, 31 | frequency: 0, 32 | }, 33 | ram: { 34 | size: 0, 35 | layout: [], 36 | }, 37 | storage: [], 38 | network: { 39 | interfaceSpeed: 0, 40 | speedDown: 0, 41 | speedUp: 0, 42 | lastSpeedTest: 0, 43 | type: '', 44 | publicIp: '', 45 | }, 46 | gpu: { 47 | layout: [], 48 | }, 49 | }); 50 | 51 | const promIf = (condition: boolean, func: () => Promise): Promise => { 52 | return condition ? func() : Promise.resolve(null); 53 | }; 54 | 55 | export const loadInfo = < 56 | T extends 'os' | 'cpu' | 'storage' | 'ram' | 'network' | 'gpu', 57 | B extends boolean 58 | >( 59 | info: T, 60 | loader: () => Promise< 61 | B extends true ? Partial : HardwareInfo[T] 62 | >, 63 | append: B 64 | ) => { 65 | return promIf(CONFIG.widget_list.includes(info), async () => { 66 | STATIC_INFO.next({ 67 | ...STATIC_INFO.getValue(), 68 | [info]: append 69 | ? { 70 | ...STATIC_INFO.getValue()[info], 71 | ...(await loader()), 72 | } 73 | : await loader(), 74 | }); 75 | }); 76 | }; 77 | 78 | export const loadStaticServerInfo = async (): Promise => { 79 | await loadInfo('os', getOsInfo.static, false); 80 | await loadInfo('cpu', getCpuInfo.static, false); 81 | await loadInfo('ram', getRamInfo.static, false); 82 | await loadInfo('storage', getStorageInfo.static, false); 83 | await loadInfo('network', getNetworkInfo.static, true); 84 | await loadInfo('gpu', getGpuInfo.static, false); 85 | 86 | console.log( 87 | 'Static Server Info:', 88 | inspect(getStaticServerInfo(), { 89 | showHidden: false, 90 | depth: null, 91 | colors: true, 92 | }) 93 | ); 94 | }; 95 | 96 | export const getStaticServerInfo = (): ServerInfo => { 97 | return { 98 | ...STATIC_INFO.getValue(), 99 | os: { 100 | ...STATIC_INFO.getValue().os, 101 | uptime: +si.time().uptime, 102 | }, 103 | config: CONFIG, 104 | }; 105 | }; 106 | 107 | export const getStaticServerInfoObs = (): Observable => { 108 | return STATIC_INFO.pipe( 109 | map(info => ({ 110 | ...info, 111 | os: { 112 | ...info.os, 113 | uptime: +si.time().uptime, 114 | }, 115 | config: CONFIG, 116 | })) 117 | ); 118 | }; 119 | -------------------------------------------------------------------------------- /apps/server/src/utils.ts: -------------------------------------------------------------------------------- 1 | import os from 'os'; 2 | import { join } from 'path'; 3 | import { CONFIG } from './config'; 4 | 5 | export const fromHost = (path: string): string => { 6 | const pathInDocker = path === '/' ? '/mnt/host' : join('/mnt/host', path); 7 | return CONFIG.running_in_docker ? pathInDocker : path; 8 | }; 9 | 10 | export const PLATFORM_IS_WINDOWS = os.platform() === 'win32'; 11 | -------------------------------------------------------------------------------- /apps/server/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["node", "express"] 7 | }, 8 | "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"], 9 | "include": ["src/**/*.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /apps/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.app.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | } 12 | ], 13 | "compilerOptions": { 14 | "esModuleInterop": true 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /apps/server/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": [ 9 | "jest.config.ts", 10 | "src/**/*.test.ts", 11 | "src/**/*.spec.ts", 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /apps/server/webpack.config.js: -------------------------------------------------------------------------------- 1 | const { composePlugins, withNx } = require('@nx/webpack'); 2 | 3 | // Nx plugins for webpack. 4 | module.exports = composePlugins(withNx(), config => { 5 | // Update the webpack config as needed here. 6 | // e.g. `config.plugins.push(new MyPlugin())` 7 | return config; 8 | }); 9 | -------------------------------------------------------------------------------- /apps/view/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["plugin:@nx/react", "../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": {} 8 | }, 9 | { 10 | "files": ["*.ts", "*.tsx"], 11 | "rules": {} 12 | }, 13 | { 14 | "files": ["*.js", "*.jsx"], 15 | "rules": {} 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /apps/view/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | dash. 15 | 16 | 17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /apps/view/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "view", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "sourceRoot": "apps/view/src", 5 | "projectType": "application", 6 | "targets": { 7 | "build": { 8 | "executor": "@nx/vite:build", 9 | "outputs": ["{options.outputPath}"], 10 | "defaultConfiguration": "production", 11 | "options": { 12 | "outputPath": "dist/apps/view", 13 | "base": "/", 14 | "sourcemap": true 15 | }, 16 | "configurations": { 17 | "development": { 18 | "mode": "development" 19 | }, 20 | "production": { 21 | "mode": "production" 22 | } 23 | } 24 | }, 25 | "serve": { 26 | "executor": "@nx/vite:dev-server", 27 | "defaultConfiguration": "development", 28 | "options": { 29 | "buildTarget": "view:build", 30 | "port": 3000, 31 | "host": "0.0.0.0" 32 | }, 33 | "configurations": { 34 | "development": { 35 | "buildTarget": "view:build:development", 36 | "hmr": true 37 | }, 38 | "production": { 39 | "buildTarget": "view:build:production", 40 | "hmr": false 41 | } 42 | } 43 | }, 44 | "preview": { 45 | "executor": "@nx/vite:preview-server", 46 | "defaultConfiguration": "development", 47 | "options": { 48 | "buildTarget": "view:build" 49 | }, 50 | "configurations": { 51 | "development": { 52 | "buildTarget": "view:build:development" 53 | }, 54 | "production": { 55 | "buildTarget": "view:build:production" 56 | } 57 | } 58 | }, 59 | "test": { 60 | "executor": "@nx/vite:test", 61 | "outputs": ["{workspaceRoot}/coverage/apps/view"], 62 | "options": { 63 | "passWithNoTests": true, 64 | "reportsDirectory": "../../coverage/apps/view" 65 | }, 66 | "dependsOn": [ 67 | { 68 | "projects": "self", 69 | "target": "lint", 70 | "params": "ignore" 71 | } 72 | ] 73 | }, 74 | "lint": { 75 | "executor": "@nx/eslint:lint", 76 | "outputs": ["{options.outputFile}"] 77 | }, 78 | "serve-static": { 79 | "executor": "@nx/web:file-server", 80 | "options": { 81 | "buildTarget": "view:build" 82 | } 83 | } 84 | }, 85 | "tags": [] 86 | } 87 | -------------------------------------------------------------------------------- /apps/view/public/Inter.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MauriceNino/dashdot/483b75a0384abd5aba5eee5ad90afd373aab45ec/apps/view/public/Inter.ttf -------------------------------------------------------------------------------- /apps/view/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MauriceNino/dashdot/483b75a0384abd5aba5eee5ad90afd373aab45ec/apps/view/public/favicon.ico -------------------------------------------------------------------------------- /apps/view/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MauriceNino/dashdot/483b75a0384abd5aba5eee5ad90afd373aab45ec/apps/view/public/logo192.png -------------------------------------------------------------------------------- /apps/view/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MauriceNino/dashdot/483b75a0384abd5aba5eee5ad90afd373aab45ec/apps/view/public/logo512.png -------------------------------------------------------------------------------- /apps/view/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dash.", 3 | "icons": [ 4 | { 5 | "src": "/favicon.ico", 6 | "sizes": "256x256", 7 | "type": "image/x-icon" 8 | }, 9 | { 10 | "src": "/logo192.png", 11 | "type": "image/png", 12 | "sizes": "192x192" 13 | }, 14 | { 15 | "src": "/logo512.png", 16 | "type": "image/png", 17 | "sizes": "512x512" 18 | } 19 | ], 20 | "start_url": "/", 21 | "display": "standalone", 22 | "theme_color": "#775a9e", 23 | "background_color": "#4f4f4f" 24 | } 25 | -------------------------------------------------------------------------------- /apps/view/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /apps/view/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { ConfigProvider } from 'antd'; 2 | import { FC, useMemo } from 'react'; 3 | import { 4 | DefaultTheme, 5 | ThemeProvider, 6 | createGlobalStyle, 7 | } from 'styled-components'; 8 | import { useColorScheme } from 'use-color-scheme'; 9 | import { MainWidgetContainer } from './components/main-widget-container'; 10 | import { SingleWidgetChart } from './components/single-widget-chart'; 11 | import { MobileContextProvider } from './services/mobile'; 12 | import { useQuery } from './services/query-params'; 13 | import { useSetting } from './services/settings'; 14 | import { darkTheme, lightTheme } from './theme/theme'; 15 | 16 | const getLightGradient = (theme: DefaultTheme) => ` 17 | radial-gradient( 18 | circle at 10% 10%, 19 | ${theme.colors.secondary}66 10%, 20 | transparent 10.2% 21 | ), 22 | radial-gradient(circle at 10% 10%, #ffffff 10%, transparent 10.2%), 23 | radial-gradient( 24 | circle at 90% 85%, 25 | ${theme.colors.primary}66 20%, 26 | transparent 20.2% 27 | ), 28 | radial-gradient(circle at 90% 85%, white 20%, transparent 20.2%), 29 | linear-gradient( 30 | 200deg, 31 | ${theme.colors.primary} 0%, 32 | ${theme.colors.secondary} 60% 33 | )`; 34 | 35 | const getDarkGradient = (theme: DefaultTheme) => ` 36 | radial-gradient( 37 | circle at 10% 10%, 38 | ${theme.colors.primary} 10%, 39 | transparent 10.5% 40 | ), 41 | radial-gradient( 42 | circle at 110% 90%, 43 | ${theme.colors.secondary} 30%, 44 | transparent 30.5% 45 | ), 46 | linear-gradient( 47 | 290deg, 48 | ${theme.colors.primary} 0%, 49 | ${theme.colors.secondary} 40% 50 | )`; 51 | 52 | const GlobalStyle = createGlobalStyle<{ noBg: boolean }>` 53 | body { 54 | background-color: ${({ theme, noBg }) => 55 | noBg ? 'transparent' : theme.colors.background}; 56 | height: 100vh; 57 | width: 100vw; 58 | } 59 | 60 | #root { 61 | width: 100%; 62 | height: 100%; 63 | 64 | background: ${({ theme, noBg }) => 65 | noBg 66 | ? 'transparent' 67 | : theme.dark 68 | ? getDarkGradient(theme) 69 | : getLightGradient(theme)}; 70 | 71 | transition: background 0.5s ease; 72 | background-attachment: fixed; 73 | } 74 | 75 | .ant-switch { 76 | background-color: rgba(0, 0, 0, 0.25); 77 | background-image: unset; 78 | } 79 | 80 | .ant-btn { 81 | background: ${({ theme }) => theme.colors.background}; 82 | border: none; 83 | } 84 | `; 85 | 86 | const overrideColor = ( 87 | colors: (typeof darkTheme)['colors'], 88 | query: ReturnType 89 | ) => { 90 | if (query.singleWidget) { 91 | if (query.overrideThemeColor) { 92 | colors.cpuPrimary = `#${query.overrideThemeColor}`; 93 | colors.storagePrimary = `#${query.overrideThemeColor}`; 94 | colors.ramPrimary = `#${query.overrideThemeColor}`; 95 | colors.networkPrimary = `#${query.overrideThemeColor}`; 96 | colors.gpuPrimary = `#${query.overrideThemeColor}`; 97 | colors.primary = `#${query.overrideThemeColor}`; 98 | } 99 | 100 | if (query.overrideThemeSurface) { 101 | colors.surface = `#${query.overrideThemeSurface}`; 102 | } 103 | } 104 | }; 105 | 106 | export const App: FC = () => { 107 | const { scheme } = useColorScheme(); 108 | const [darkMode] = useSetting('darkMode', scheme === 'dark'); 109 | const query = useQuery(); 110 | 111 | const theme = useMemo(() => { 112 | const baseTheme = darkMode ? darkTheme : lightTheme; 113 | 114 | if (query.singleWidget) { 115 | const queryTheme = query.overrideTheme 116 | ? query.overrideTheme === 'dark' 117 | ? darkTheme 118 | : lightTheme 119 | : baseTheme; 120 | overrideColor(queryTheme.colors, query); 121 | 122 | return queryTheme; 123 | } 124 | 125 | return baseTheme; 126 | }, [darkMode, query]); 127 | 128 | return ( 129 | 130 | 138 | 139 | {query.singleWidget ? : } 140 | 141 | 142 | 143 | 144 | ); 145 | }; 146 | -------------------------------------------------------------------------------- /apps/view/src/components/chart-container.tsx: -------------------------------------------------------------------------------- 1 | import { Transient } from '@dash/common'; 2 | import { motion } from 'framer-motion'; 3 | import { forwardRef, ReactElement } from 'react'; 4 | import { SwapSpinner } from 'react-spinners-kit'; 5 | import ReactVirtualizedAutoSizer from 'react-virtualized-auto-sizer'; 6 | import styled, { useTheme } from 'styled-components'; 7 | import { useIsMobile } from '../services/mobile'; 8 | 9 | type ContainerProps = Transient<{ 10 | edges: [boolean, boolean, boolean, boolean]; 11 | loading: boolean; 12 | }>; 13 | const Container = styled.div` 14 | position: relative; 15 | display: flex; 16 | 17 | min-width: 0; 18 | background-color: ${({ theme }) => theme.colors.surface}; 19 | border-radius: ${({ $edges: [top, right, bottom, left] }) => 20 | `${top ? '25px' : '10px'} ${right ? '25px' : '10px'} ${ 21 | bottom ? '25px' : '10px' 22 | } ${left ? '25px' : '10px'}`}; 23 | z-index: auto; 24 | 25 | transition: background-color 0.3s ease; 26 | align-items: center; 27 | justify-content: center; 28 | 29 | > div { 30 | overflow: hidden; 31 | ${({ $edges: [top, right, bottom, left], $loading }) => 32 | !$loading && 33 | ` 34 | position: absolute; 35 | border-radius: ${`${top ? '25px' : '10px'} ${right ? '25px' : '10px'} ${ 36 | bottom ? '25px' : '10px' 37 | } ${left ? '25px' : '10px'}`}; 38 | `} 39 | } 40 | 41 | &:before { 42 | content: ''; 43 | position: absolute; 44 | width: 100%; 45 | height: 100%; 46 | left: 0; 47 | top: 0; 48 | border-radius: inherit; 49 | box-shadow: -13px -13px 35px 0px rgba(0, 0, 0, 0.15); 50 | z-index: -1; 51 | } 52 | `; 53 | 54 | type StatTextProps = Transient<{ 55 | float: 'left' | 'right'; 56 | offset?: string; 57 | size?: string; 58 | }>; 59 | const StatText = styled.p` 60 | position: absolute; 61 | top: 0; 62 | ${({ $float }) => ($float === 'left' ? 'left: 0' : 'right: 0')}; 63 | ${({ $float, $offset }) => 64 | $float === 'left' 65 | ? `margin-left: ${$offset ?? 'min(13%, 30px)'}` 66 | : `margin-right: ${$offset ?? 'min(13%, 30px)'}`}; 67 | margin-top: ${({ $offset }) => $offset ?? 'min(13%, 30px)'}; 68 | font-size: ${({ $size }) => $size ?? 'unset'}; 69 | z-index: 2; 70 | color: ${({ theme }) => theme.colors.text}AA; 71 | `; 72 | 73 | type ChartContainerProps = { 74 | contentLoaded: boolean; 75 | edges?: [boolean, boolean, boolean, boolean]; 76 | textLeft?: string; 77 | textRight?: string; 78 | textOffset?: string; 79 | textSize?: string; 80 | renderChart: (size: { width: number; height: number }) => ReactElement; 81 | }; 82 | 83 | export const ChartContainer = motion( 84 | forwardRef((props, ref) => { 85 | const theme = useTheme(); 86 | 87 | return ( 88 | 93 | {props.contentLoaded ? ( 94 | <> 95 | 100 | {props.textLeft} 101 | 102 | 107 | {props.textRight} 108 | 109 | 116 | {size => 117 | props.renderChart({ 118 | width: size.width ?? 0, 119 | height: size.height ?? 0, 120 | }) 121 | } 122 | 123 | 124 | ) : ( 125 | 130 | )} 131 | 132 | ); 133 | }) 134 | ); 135 | 136 | type MultiChartContainerProps = { 137 | columns?: number; 138 | gap?: number; 139 | children: ReactElement | ReactElement[]; 140 | }; 141 | 142 | export const SMultiChartContainer = styled.div< 143 | Omit & { mobile: boolean } 144 | >` 145 | position: relative; 146 | flex: 1 1 auto; 147 | min-width: 0; 148 | ${({ mobile }) => mobile && `height: 270px;`} 149 | 150 | > div { 151 | display: grid; 152 | grid-template-columns: repeat(${({ columns }) => columns ?? 1}, 1fr); 153 | grid-auto-flow: row; 154 | gap: ${({ gap }) => gap ?? 20}px; 155 | justify-items: stretch; 156 | align-items: stretch; 157 | 158 | height: ${({ mobile }) => (mobile ? '100%' : '110%')}; 159 | width: 100%; 160 | 161 | position: ${({ mobile }) => (mobile ? 'relative' : 'absolute')}; 162 | bottom: ${({ mobile }) => (mobile ? '0' : '-10px')}; 163 | right: -10px; 164 | 165 | z-index: 1; 166 | } 167 | `; 168 | 169 | export const MultiChartContainer = motion( 170 | forwardRef( 171 | ({ children, ...props }, ref) => { 172 | const isMobile = useIsMobile(); 173 | 174 | return ( 175 | 176 |
{children}
177 |
178 | ); 179 | } 180 | ) 181 | ); 182 | -------------------------------------------------------------------------------- /apps/view/src/components/glass-pane.tsx: -------------------------------------------------------------------------------- 1 | import { Transient } from '@dash/common'; 2 | import { motion } from 'framer-motion'; 3 | import { forwardRef } from 'react'; 4 | import styled from 'styled-components'; 5 | import { useIsMobile } from '../services/mobile'; 6 | 7 | type SCProps = Transient< 8 | Pick & { mobile: boolean } 9 | >; 10 | 11 | const Container = styled.div` 12 | min-width: ${({ $minWidth, $mobile }) => ($mobile ? 0 : $minWidth)}px; 13 | width: ${({ $mobile }) => ($mobile ? '100%' : 'unset')}; 14 | min-height: 360px; 15 | max-height: ${({ $mobile }) => ($mobile ? 'unset' : '500px')}; 16 | 17 | flex-grow: ${({ $grow }) => $grow ?? 1}; 18 | flex-basis: ${({ $mobile }) => ($mobile ? 'unset' : '0')}; 19 | transition: opacity 0.3 ease-in-out; 20 | 21 | display: flex; 22 | backdrop-filter: blur(20px); 23 | background-color: ${({ theme }) => theme.colors.surface}44; 24 | border-radius: 25px; 25 | border: 1px solid ${({ theme }) => theme.colors.text}33; 26 | `; 27 | 28 | type GlassPaneProps = { 29 | grow?: number; 30 | minWidth?: number; 31 | enableTilt?: boolean; 32 | children?: React.ReactNode; 33 | }; 34 | 35 | export const GlassPane = motion( 36 | forwardRef((props, ref) => { 37 | const isMobile = useIsMobile(); 38 | 39 | return ( 40 | 46 | {props.children} 47 | 48 | ); 49 | }) 50 | ); 51 | -------------------------------------------------------------------------------- /apps/view/src/components/hardware-info-container.tsx: -------------------------------------------------------------------------------- 1 | import { Transient } from '@dash/common'; 2 | import { IconProp } from '@fortawesome/fontawesome-svg-core'; 3 | import { faArrowLeft, faArrowRight } from '@fortawesome/free-solid-svg-icons'; 4 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 5 | import { AnimatePresence, motion } from 'framer-motion'; 6 | import { FC, ReactNode, useState } from 'react'; 7 | import styled from 'styled-components'; 8 | import { useIsMobile } from '../services/mobile'; 9 | import { InfoTable, InfoTableProps } from './info-table'; 10 | import { ThemedText } from './text'; 11 | 12 | const Container = styled.div` 13 | position: relative; 14 | display: flex; 15 | flex-direction: column; 16 | width: 100%; 17 | `; 18 | 19 | const ContentContainer = styled.div>` 20 | display: flex; 21 | flex-direction: ${({ $mobile }) => ($mobile ? 'column-reverse' : 'row')}; 22 | flex: 1 1 auto; 23 | ${({ $mobile }) => $mobile && 'padding: 20px 0'}; 24 | `; 25 | 26 | const InfoContainer = styled.div` 27 | display: flex; 28 | flex-direction: column; 29 | 30 | width: 300px; 31 | min-width: 300px; 32 | flex-grow: 0 !important; 33 | `; 34 | 35 | const InfoIcon = styled.div>` 36 | display: flex; 37 | align-items: center; 38 | justify-content: center; 39 | width: 100px; 40 | height: 100px; 41 | min-height: 100px; 42 | 43 | position: relative; 44 | top: -10px; 45 | left: 20px; 46 | 47 | background-color: ${props => props.$color}; 48 | border-radius: 10px; 49 | box-shadow: 13px 13px 35px 0px rgba(0, 0, 0, 0.15); 50 | 51 | transition: background-color 0.5s ease; 52 | 53 | svg { 54 | color: ${props => props.theme.colors.text}; 55 | } 56 | `; 57 | 58 | const InfoHeadingContainer = styled.div` 59 | display: flex; 60 | flex-direction: row; 61 | justify-content: space-between; 62 | margin: 30px 30px 0 30px; 63 | `; 64 | 65 | const InfoHeading = styled(ThemedText)` 66 | font-size: 1.5rem; 67 | font-weight: bold; 68 | `; 69 | 70 | const Controls = styled.div` 71 | display: flex; 72 | flex-direction: row; 73 | gap: 15px; 74 | 75 | svg { 76 | cursor: pointer; 77 | color: ${props => props.theme.colors.text}; 78 | } 79 | `; 80 | 81 | const IconContainer = styled(motion.div)``; 82 | 83 | type HardwareInfoProps = { 84 | color: string; 85 | heading: string; 86 | icon: IconProp; 87 | extraContent?: JSX.Element; 88 | children?: ReactNode; 89 | infosPerPage: number; 90 | onPageChange?: (page: number) => void; 91 | } & InfoTableProps; 92 | 93 | export const HardwareInfoContainer: FC = props => { 94 | const isMobile = useIsMobile(); 95 | const [page, setPage] = useState(0); 96 | 97 | const changePage = (page: number) => { 98 | setPage(page); 99 | props.onPageChange?.(page); 100 | }; 101 | 102 | return ( 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | {props.heading} 112 | 113 | 114 | 115 | {page > 0 && ( 116 | 117 | changePage(page - 1)} 120 | /> 121 | 122 | )} 123 | {(page + 1) * props.infosPerPage < props.infos.length && ( 124 | 125 | changePage(page + 1)} 128 | /> 129 | 130 | )} 131 | 132 | 133 | 134 | 135 | 140 | 141 | 142 | {props.extraContent} 143 | 144 | {props.children} 145 | 146 | 147 | ); 148 | }; 149 | -------------------------------------------------------------------------------- /apps/view/src/components/info-table.tsx: -------------------------------------------------------------------------------- 1 | import { Tooltip } from 'antd'; 2 | import { motion, Variants } from 'framer-motion'; 3 | import { FC } from 'react'; 4 | import styled from 'styled-components'; 5 | import { InfoTableArr } from '../utils/format'; 6 | import { ThemedText } from './text'; 7 | 8 | const itemVariants: Variants = { 9 | initial: { 10 | opacity: 0, 11 | scale: 0.9, 12 | }, 13 | animate: { 14 | opacity: 1, 15 | scale: 1, 16 | }, 17 | }; 18 | 19 | export const InfoTextContainer = styled.div<{ noPadding?: boolean }>` 20 | display: table; 21 | padding: 30px 30px 15px 30px; 22 | color: ${props => props.theme.colors.text}; 23 | `; 24 | 25 | const InfoTextRow = styled(motion.div)` 26 | display: table-row; 27 | `; 28 | 29 | const InfoTextLabel = styled(ThemedText)` 30 | display: table-cell; 31 | width: auto; 32 | font-size: 0.8rem; 33 | padding-bottom: 3px; 34 | padding-right: 15px; 35 | line-height: 1.5; 36 | white-space: pre; 37 | `; 38 | 39 | const InfoTextValue = styled(ThemedText)` 40 | line-height: 1.5; 41 | white-space: pre-wrap; 42 | display: table-cell; 43 | font-size: 1rem; 44 | font-weight: bold; 45 | padding-bottom: 3px; 46 | `; 47 | 48 | export type InfoTableProps = { 49 | infos: InfoTableArr; 50 | className?: string; 51 | }; 52 | 53 | export const InfoTable: FC< 54 | InfoTableProps & { 55 | page: number; 56 | itemsPerPage: number; 57 | } 58 | > = props => { 59 | const itemsStart = props.page * props.itemsPerPage; 60 | const items = props.infos.slice( 61 | itemsStart, 62 | Math.min(itemsStart + props.itemsPerPage, props.infos.length) 63 | ); 64 | 65 | return ( 66 | 67 | {items.map((info, i) => ( 68 | 75 | {info.label} 76 | 77 | {info.value == null || info.value.trim().length === 0 ? ( 78 | '/' 79 | ) : info.tooltip ? ( 80 | {info.value} 81 | ) : ( 82 | info.value 83 | )} 84 | 85 | 86 | ))} 87 | 88 | ); 89 | }; 90 | -------------------------------------------------------------------------------- /apps/view/src/components/single-widget-chart.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentProps, ComponentType, FC } from 'react'; 2 | import styled from 'styled-components'; 3 | import { usePageData } from '../services/page-data'; 4 | import { useQuery } from '../services/query-params'; 5 | import { CpuChart } from '../widgets/cpu'; 6 | import { ErrorWidget } from '../widgets/error'; 7 | import { GpuChart } from '../widgets/gpu'; 8 | import { NetworkChart } from '../widgets/network'; 9 | import { RamChart } from '../widgets/ram'; 10 | import { StorageChart } from '../widgets/storage'; 11 | 12 | const Container = styled.div<{ radius?: string; gap?: string }>` 13 | height: 100vh; 14 | width: 100vw; 15 | overflow: hidden; 16 | 17 | > div { 18 | height: 100vh; 19 | width: 100vw; 20 | > div { 21 | height: 100vh; 22 | width: 100vw; 23 | position: relative; 24 | right: unset; 25 | bottom: unset; 26 | gap: ${({ gap }) => gap ?? '12px'}; 27 | 28 | > div { 29 | border-radius: ${({ radius }) => radius ?? '10px'}; 30 | 31 | > div { 32 | border-radius: ${({ radius }) => radius ?? '10px'}; 33 | } 34 | 35 | &::before { 36 | box-shadow: unset; 37 | } 38 | } 39 | } 40 | } 41 | `; 42 | 43 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 44 | type Graph> = { 45 | Component: T; 46 | props: Omit, 'showPercentages'>; 47 | }; 48 | 49 | type GraphMap = { 50 | cpu: Graph; 51 | storage: Graph; 52 | ram: Graph; 53 | network: Graph; 54 | gpu: Graph; 55 | }; 56 | 57 | export const SingleWidgetChart: FC = () => { 58 | const { 59 | pageLoaded, 60 | error, 61 | serverInfo, 62 | config, 63 | cpuLoad, 64 | storageLoad, 65 | ramLoad, 66 | networkLoad, 67 | gpuLoad, 68 | } = usePageData(); 69 | const query = useQuery(); 70 | 71 | if (error) { 72 | return ; 73 | } 74 | 75 | if (!pageLoaded || !config || !serverInfo || !query.singleWidget) return null; 76 | 77 | const configs: GraphMap = { 78 | cpu: { 79 | Component: CpuChart, 80 | props: { 81 | load: cpuLoad, 82 | config: config, 83 | multiView: query.multiView ?? false, 84 | }, 85 | }, 86 | storage: { 87 | Component: StorageChart, 88 | props: { 89 | load: storageLoad, 90 | data: serverInfo.storage, 91 | config: config, 92 | multiView: query.multiView ?? false, 93 | }, 94 | }, 95 | ram: { 96 | Component: RamChart, 97 | props: { 98 | load: ramLoad, 99 | data: serverInfo?.ram, 100 | }, 101 | }, 102 | network: { 103 | Component: NetworkChart, 104 | props: { 105 | load: networkLoad, 106 | data: serverInfo.network, 107 | config: config, 108 | filter: query.filter, 109 | }, 110 | }, 111 | gpu: { 112 | Component: GpuChart, 113 | props: { 114 | load: gpuLoad, 115 | index: 0, 116 | filter: query.filter, 117 | }, 118 | }, 119 | }; 120 | 121 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 122 | const compConfig = configs[query.graph] as Graph; 123 | 124 | if (!compConfig) return null; 125 | 126 | return ( 127 | 128 | 134 | 135 | ); 136 | }; 137 | -------------------------------------------------------------------------------- /apps/view/src/components/text.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const ThemedText = styled.p` 4 | color: ${props => props.theme.colors.text}; 5 | display: inline-block; 6 | `; 7 | -------------------------------------------------------------------------------- /apps/view/src/components/widget-switch.tsx: -------------------------------------------------------------------------------- 1 | import { Switch } from 'antd'; 2 | import styled from 'styled-components'; 3 | import { ThemedText } from './text'; 4 | 5 | const SwitchContainer = styled.div` 6 | position: absolute; 7 | right: 25px; 8 | top: 25px; 9 | z-index: 2; 10 | 11 | display: flex; 12 | flex-direction: row; 13 | align-items: center; 14 | gap: 15px; 15 | `; 16 | 17 | type WidgetSwitchProps = { 18 | label?: string; 19 | checked: boolean; 20 | onChange: (checked: boolean) => void; 21 | }; 22 | 23 | export const WidgetSwitch = ({ 24 | label, 25 | checked, 26 | onChange, 27 | }: WidgetSwitchProps) => ( 28 | 29 | {label} 30 | 31 | 32 | ); 33 | -------------------------------------------------------------------------------- /apps/view/src/environment.ts: -------------------------------------------------------------------------------- 1 | export const environment = 2 | import.meta.env.MODE === 'production' 3 | ? { 4 | production: true, 5 | backendUrl: window.location.origin, 6 | } 7 | : { 8 | production: false, 9 | backendUrl: 'http://localhost:3001', 10 | }; 11 | -------------------------------------------------------------------------------- /apps/view/src/index.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Inter'; 3 | font-style: normal; 4 | font-display: swap; 5 | src: url(/Inter.ttf) format('woff2'); 6 | } 7 | 8 | body { 9 | margin: 0; 10 | font-family: 'Inter', sans-serif; 11 | -webkit-font-smoothing: antialiased; 12 | -moz-osx-font-smoothing: grayscale; 13 | } 14 | 15 | html, 16 | body, 17 | div, 18 | span, 19 | applet, 20 | object, 21 | iframe, 22 | h1, 23 | h2, 24 | h3, 25 | h4, 26 | h5, 27 | h6, 28 | p, 29 | blockquote, 30 | pre, 31 | a, 32 | abbr, 33 | acronym, 34 | address, 35 | big, 36 | cite, 37 | code, 38 | del, 39 | dfn, 40 | em, 41 | img, 42 | ins, 43 | kbd, 44 | q, 45 | s, 46 | samp, 47 | small, 48 | strike, 49 | strong, 50 | sub, 51 | sup, 52 | tt, 53 | var, 54 | b, 55 | u, 56 | i, 57 | center, 58 | dl, 59 | dt, 60 | dd, 61 | ol, 62 | ul, 63 | li, 64 | fieldset, 65 | form, 66 | label, 67 | legend, 68 | table, 69 | caption, 70 | tbody, 71 | tfoot, 72 | thead, 73 | tr, 74 | th, 75 | td, 76 | article, 77 | aside, 78 | canvas, 79 | details, 80 | embed, 81 | figure, 82 | figcaption, 83 | footer, 84 | header, 85 | hgroup, 86 | menu, 87 | nav, 88 | output, 89 | ruby, 90 | section, 91 | summary, 92 | time, 93 | mark, 94 | audio, 95 | video { 96 | margin: 0; 97 | padding: 0; 98 | border: 0; 99 | font-size: 100%; 100 | vertical-align: baseline; 101 | box-sizing: border-box; 102 | } 103 | /* HTML5 display-role reset for older browsers */ 104 | article, 105 | aside, 106 | details, 107 | figcaption, 108 | figure, 109 | footer, 110 | header, 111 | hgroup, 112 | menu, 113 | nav, 114 | section { 115 | display: block; 116 | } 117 | body { 118 | line-height: 1; 119 | } 120 | ol, 121 | ul { 122 | list-style: none; 123 | } 124 | blockquote, 125 | q { 126 | quotes: none; 127 | } 128 | blockquote:before, 129 | blockquote:after, 130 | q:before, 131 | q:after { 132 | content: ''; 133 | content: none; 134 | } 135 | table { 136 | border-collapse: collapse; 137 | border-spacing: 0; 138 | } 139 | -------------------------------------------------------------------------------- /apps/view/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react'; 2 | import * as ReactDOM from 'react-dom/client'; 3 | import { App } from './App'; 4 | import './index.css'; 5 | 6 | const root = ReactDOM.createRoot( 7 | document.getElementById('root') as HTMLElement 8 | ); 9 | root.render( 10 | 11 | 12 | 13 | ); 14 | -------------------------------------------------------------------------------- /apps/view/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /apps/view/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 | -------------------------------------------------------------------------------- /apps/view/src/services/mobile.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | createContext, 3 | ReactNode, 4 | useContext, 5 | useEffect, 6 | useState, 7 | } from 'react'; 8 | 9 | const isMobileFunc = (): boolean => { 10 | if (typeof window !== 'undefined') { 11 | return window.innerWidth < 750; 12 | } 13 | return true; 14 | }; 15 | 16 | export const MobileContext = createContext(isMobileFunc()); 17 | 18 | export const MobileContextProvider: React.FC<{ children: ReactNode }> = ({ 19 | children, 20 | }) => { 21 | const [isMobile, setIsMobile] = useState(isMobileFunc()); 22 | 23 | useEffect(() => { 24 | const handleResize = () => { 25 | setIsMobile(isMobileFunc()); 26 | }; 27 | 28 | window.addEventListener('resize', handleResize); 29 | 30 | return () => { 31 | window.removeEventListener('resize', handleResize); 32 | }; 33 | }, []); 34 | 35 | return ( 36 | {children} 37 | ); 38 | }; 39 | 40 | export const useIsMobile = () => { 41 | const isMob = useContext(MobileContext); 42 | return isMob; 43 | }; 44 | -------------------------------------------------------------------------------- /apps/view/src/services/page-data.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CpuLoad, 3 | GpuLoad, 4 | NetworkLoad, 5 | RamLoad, 6 | ServerInfo, 7 | StorageLoad, 8 | } from '@dash/common'; 9 | import { useEffect, useState } from 'react'; 10 | import { Socket, io } from 'socket.io-client'; 11 | import { environment } from '../environment'; 12 | 13 | export const usePageData = () => { 14 | const [pageLoaded, setPageLoaded] = useState(false); 15 | const [socketError, setSocketError] = useState(false); 16 | const [serverInfo, setServerInfo] = useState(); 17 | const [cpuLoad, setCpuLoad] = useState([]); 18 | const [ramLoad, setRamLoad] = useState([]); 19 | const [networkLoad, setNetworkLoad] = useState([]); 20 | const [gpuLoad, setGpuLoad] = useState([]); 21 | const [storageLoad, setStorageLoad] = useState(); 22 | 23 | const config = serverInfo?.config; 24 | 25 | useEffect(() => { 26 | const socket = io(environment.backendUrl); 27 | 28 | socket.on('static-info', data => { 29 | setServerInfo(data); 30 | }); 31 | 32 | socket.on('connect', () => { 33 | setTimeout(() => setPageLoaded(true), 50); 34 | setSocketError(false); 35 | }); 36 | socket.on('connect_error', () => { 37 | setSocketError(true); 38 | }); 39 | 40 | return () => { 41 | socket.close(); 42 | }; 43 | }, []); 44 | 45 | useEffect(() => { 46 | let socket: Socket | undefined; 47 | if (config) { 48 | socket = io(environment.backendUrl); 49 | 50 | socket.on('cpu-load', data => { 51 | setCpuLoad(oldData => { 52 | if (oldData.length >= (config.cpu_shown_datapoints ?? 0)) { 53 | return [...oldData.slice(1), data]; 54 | } else { 55 | return [...oldData, data]; 56 | } 57 | }); 58 | }); 59 | 60 | socket.on('ram-load', data => { 61 | setRamLoad(oldData => { 62 | if (oldData.length >= (config.ram_shown_datapoints ?? 0)) { 63 | return [...oldData.slice(1), data]; 64 | } else { 65 | return [...oldData, data]; 66 | } 67 | }); 68 | }); 69 | 70 | socket.on('network-load', data => { 71 | setNetworkLoad(oldData => { 72 | if (oldData.length >= (config.network_shown_datapoints ?? 0)) { 73 | return [...oldData.slice(1), data]; 74 | } else { 75 | return [...oldData, data]; 76 | } 77 | }); 78 | }); 79 | 80 | socket.on('gpu-load', data => { 81 | setGpuLoad(oldData => { 82 | if (oldData.length >= (config.gpu_shown_datapoints ?? 0)) { 83 | return [...oldData.slice(1), data]; 84 | } else { 85 | return [...oldData, data]; 86 | } 87 | }); 88 | }); 89 | 90 | socket.on('storage-load', data => { 91 | setStorageLoad(data); 92 | }); 93 | } 94 | 95 | return () => { 96 | socket?.close(); 97 | }; 98 | }, [config]); 99 | 100 | const errors = [ 101 | { 102 | condition: !config, 103 | text: 'Invalid or incomplete static data loaded!', 104 | }, 105 | { 106 | condition: socketError, 107 | text: 'Unable to connect to backend!', 108 | }, 109 | ]; 110 | const error = errors.find(e => e.condition && pageLoaded === true); 111 | 112 | return { 113 | pageLoaded, 114 | error, 115 | serverInfo, 116 | config, 117 | cpuLoad, 118 | ramLoad, 119 | networkLoad, 120 | gpuLoad, 121 | storageLoad, 122 | }; 123 | }; 124 | -------------------------------------------------------------------------------- /apps/view/src/services/query-params.ts: -------------------------------------------------------------------------------- 1 | import qs from 'qs'; 2 | import { useMemo } from 'react'; 3 | 4 | export type FullscreenGraphTypes = 5 | | 'cpu' 6 | | 'storage' 7 | | 'ram' 8 | | 'network' 9 | | 'gpu'; 10 | 11 | type QueryResult = 12 | | { 13 | singleWidget: false; 14 | } 15 | | { 16 | singleWidget: true; 17 | graph: FullscreenGraphTypes; 18 | multiView: boolean; 19 | showPercentage: boolean; 20 | textOffset: string; 21 | textSize: string; 22 | overrideTheme?: 'light' | 'dark'; 23 | overrideThemeColor?: string; 24 | overrideThemeSurface?: string; 25 | radius?: string; 26 | gap?: string; 27 | filter?: string; 28 | }; 29 | 30 | const sizeRegex = /^\d+\D+$/; 31 | const extractSizeValue = (input?: string) => 32 | input ? (sizeRegex.test(input) ? input : input + 'px') : undefined; 33 | 34 | export const useQuery = (): QueryResult => { 35 | const query = useMemo( 36 | () => qs.parse(window.location.search, { ignoreQueryPrefix: true }), 37 | [] 38 | ); 39 | 40 | if (query['graph'] != null) { 41 | return { 42 | singleWidget: true, 43 | graph: query['graph'] as FullscreenGraphTypes, 44 | multiView: query['multiView'] === 'true', 45 | showPercentage: query['showPercentage'] === 'true', 46 | textOffset: extractSizeValue(query['textOffset'] as string) ?? '24px', 47 | textSize: extractSizeValue(query['textSize'] as string) ?? '16px', 48 | overrideTheme: query['theme'] as 'light' | 'dark', 49 | overrideThemeColor: query['color'] as string, 50 | overrideThemeSurface: query['surface'] as string, 51 | radius: extractSizeValue(query['innerRadius'] as string), 52 | gap: extractSizeValue(query['gap'] as string), 53 | filter: ((query['filter'] as string) ?? "").toLowerCase(), 54 | }; 55 | } 56 | 57 | return { 58 | singleWidget: false, 59 | }; 60 | }; 61 | -------------------------------------------------------------------------------- /apps/view/src/services/settings.ts: -------------------------------------------------------------------------------- 1 | import { Dispatch, useEffect, useState } from 'react'; 2 | import store from 'store'; 3 | 4 | type Settings = { 5 | darkMode: boolean; 6 | multiCore: boolean; 7 | splitStorage: boolean; 8 | }; 9 | 10 | export const setStoreSetting = ( 11 | property: T, 12 | value: Settings[T] 13 | ) => { 14 | store.set(property, value); 15 | }; 16 | 17 | export const getStoreSetting = ( 18 | property: T 19 | ) => { 20 | return store.get(property) as Settings[T]; 21 | }; 22 | 23 | const ALL_DISPATCHES: { 24 | [T in keyof Settings]: Dispatch[]; 25 | } = { 26 | darkMode: [], 27 | multiCore: [], 28 | splitStorage: [], 29 | }; 30 | 31 | export const useSetting = ( 32 | key: T, 33 | initialValue?: Settings[T] 34 | ): [Settings[T], Dispatch] => { 35 | const [localValue, setLocalValue] = useState( 36 | () => getStoreSetting(key) ?? initialValue 37 | ); 38 | 39 | const setSetting = (newValue: Settings[T]) => { 40 | ALL_DISPATCHES[key].forEach(dispatch => dispatch(newValue)); 41 | setStoreSetting(key, newValue); 42 | }; 43 | 44 | useEffect(() => { 45 | ALL_DISPATCHES[key].push(setLocalValue); 46 | 47 | if (initialValue !== undefined && getStoreSetting(key) === undefined) { 48 | setSetting(initialValue); 49 | } 50 | // eslint-disable-next-line react-hooks/exhaustive-deps 51 | }, []); 52 | 53 | return [localValue, setSetting]; 54 | }; 55 | -------------------------------------------------------------------------------- /apps/view/src/theme/theme.ts: -------------------------------------------------------------------------------- 1 | type Theme = { 2 | dark: boolean; 3 | colors: { 4 | primary: string; 5 | secondary: string; 6 | error: string; 7 | 8 | surface: string; 9 | background: string; 10 | 11 | text: string; 12 | 13 | cpuPrimary: string; 14 | ramPrimary: string; 15 | storagePrimary: string; 16 | networkPrimary: string; 17 | gpuPrimary: string; 18 | }; 19 | }; 20 | 21 | const lightTheme: Theme = { 22 | dark: false, 23 | colors: { 24 | primary: '#5ee2ff', 25 | secondary: '#f5a4e3', 26 | error: '#f44336', 27 | 28 | surface: '#fafafa', 29 | background: '#d9d9d9', 30 | 31 | text: '#404040', 32 | 33 | cpuPrimary: '#52d7ff', 34 | ramPrimary: '#ff526f', 35 | storagePrimary: '#49e37a', 36 | networkPrimary: '#ffd745', 37 | gpuPrimary: '#c97bff', 38 | }, 39 | }; 40 | 41 | const darkTheme: Theme = { 42 | dark: true, 43 | colors: { 44 | primary: '#775a9e', 45 | secondary: '#434047', 46 | error: lightTheme.colors.error, 47 | 48 | surface: '#212121', 49 | background: '#4f4f4f', 50 | 51 | text: '#fafafa', 52 | 53 | cpuPrimary: '#2259b3', 54 | ramPrimary: '#c43148', 55 | storagePrimary: '#22b352', 56 | networkPrimary: '#e9c235', 57 | gpuPrimary: '#a135eb', 58 | }, 59 | }; 60 | 61 | export { lightTheme, darkTheme }; 62 | 63 | declare module 'styled-components' { 64 | // eslint-disable-next-line @typescript-eslint/no-empty-interface 65 | export interface DefaultTheme extends Theme {} 66 | } 67 | -------------------------------------------------------------------------------- /apps/view/src/utils/array-utils.ts: -------------------------------------------------------------------------------- 1 | export const removeDuplicates = (array?: T[]) => { 2 | return ( 3 | array?.filter((item, index) => array.indexOf(item) === index) ?? [] 4 | ).filter( 5 | item => item != null && (typeof item !== 'string' || item.trim() !== '') 6 | ); 7 | }; 8 | -------------------------------------------------------------------------------- /apps/view/src/utils/calculations.ts: -------------------------------------------------------------------------------- 1 | export const toCommas = (num: number, commas = 1): number => { 2 | return Math.round(num * Math.pow(10, commas)) / Math.pow(10, commas); 3 | }; 4 | 5 | export const bpsPrettyPrint = (bits: number, asBytes = false) => { 6 | return bits >= 1000 * 1000 * 1000 7 | ? asBytes 8 | ? `${(bits / 1000 / 1000 / 1000 / 8).toFixed(1)} GB/s` 9 | : `${(bits / 1000 / 1000 / 1000).toFixed(1)} Gb/s` 10 | : bits >= 1000 * 1000 11 | ? asBytes 12 | ? `${(bits / 1000 / 1000 / 8).toFixed(1)} MB/s` 13 | : `${(bits / 1000 / 1000).toFixed(1)} Mb/s` 14 | : bits >= 1000 15 | ? asBytes 16 | ? `${(bits / 1000 / 8).toFixed(1)} KB/s` 17 | : `${(bits / 1000).toFixed(1)} Kb/s` 18 | : asBytes 19 | ? `${(bits / 8).toFixed(1)} B/s` 20 | : `${bits.toFixed(1)} b/s`; 21 | }; 22 | 23 | export const bytePrettyPrint = (byte: number): string => { 24 | return byte > 1024 * 1024 * 1024 * 1024 * 10 25 | ? `${(byte / 1024 / 1024 / 1024 / 1024).toFixed(1)} TiB` 26 | : byte > 1024 * 1024 * 1024 27 | ? `${(byte / 1024 / 1024 / 1024).toFixed(1)} GiB` 28 | : byte > 1024 * 1024 29 | ? `${(byte / 1024 / 1024).toFixed(1)} MiB` 30 | : byte > 1024 31 | ? `${(byte / 1024).toFixed(1)} KiB` 32 | : `${byte.toFixed(1)} B`; 33 | }; 34 | 35 | export const celsiusToFahrenheit = (celsius: number): number => 36 | (celsius * 9) / 5 + 32; 37 | -------------------------------------------------------------------------------- /apps/view/src/utils/format.ts: -------------------------------------------------------------------------------- 1 | export type InfoTableArr = { 2 | label: string; 3 | value?: string; 4 | tooltip?: string; 5 | }[]; 6 | 7 | export const toInfoTable = >( 8 | order: T, 9 | mappings: Record< 10 | T[number], 11 | { label: string; value?: string | number; tooltip?: string } 12 | > 13 | ): InfoTableArr => 14 | order.map(key => { 15 | const { label, tooltip, value } = mappings[key as T[number]]; 16 | 17 | return { 18 | label, 19 | tooltip, 20 | value: value 21 | ? typeof value === 'number' 22 | ? value.toString() 23 | : value 24 | : undefined, 25 | }; 26 | }); 27 | -------------------------------------------------------------------------------- /apps/view/src/utils/types.ts: -------------------------------------------------------------------------------- 1 | export type ChartVal = { 2 | x: number; 3 | y: number; 4 | }; 5 | -------------------------------------------------------------------------------- /apps/view/src/widgets/error.tsx: -------------------------------------------------------------------------------- 1 | import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons'; 2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 3 | import { FC } from 'react'; 4 | import styled, { useTheme } from 'styled-components'; 5 | import { ThemedText } from '../components/text'; 6 | 7 | const Content = styled.div` 8 | flex: 1; 9 | display: flex; 10 | flex-direction: column; 11 | justify-content: center; 12 | align-items: center; 13 | gap: 50px; 14 | padding: 30px; 15 | `; 16 | 17 | type ErrorWidgetProps = { 18 | errorText: string; 19 | }; 20 | export const ErrorWidget: FC = ({ errorText }) => { 21 | const theme = useTheme(); 22 | 23 | return ( 24 | 25 | 30 | {errorText} 31 | 32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /apps/view/src/widgets/gpu.tsx: -------------------------------------------------------------------------------- 1 | import { Config, GpuInfo, GpuLoad } from '@dash/common'; 2 | import { faDesktop } from '@fortawesome/free-solid-svg-icons'; 3 | import { FC, useMemo, useState } from 'react'; 4 | import { YAxis } from 'recharts'; 5 | import { useTheme } from 'styled-components'; 6 | import { DefaultAreaChart } from '../components/chart-components'; 7 | import { 8 | ChartContainer, 9 | MultiChartContainer, 10 | } from '../components/chart-container'; 11 | import { HardwareInfoContainer } from '../components/hardware-info-container'; 12 | import { useIsMobile } from '../services/mobile'; 13 | import { bytePrettyPrint } from '../utils/calculations'; 14 | import { toInfoTable } from '../utils/format'; 15 | import { ChartVal } from '../utils/types'; 16 | 17 | type GpuChartProps = { 18 | load: GpuLoad[]; 19 | index: number; 20 | showPercentages: boolean; 21 | textOffset?: string; 22 | textSize?: string; 23 | filter?: string; 24 | }; 25 | 26 | export const GpuChart: FC = ({ 27 | load, 28 | index, 29 | showPercentages, 30 | textOffset, 31 | textSize, 32 | filter, 33 | }) => { 34 | const theme = useTheme(); 35 | 36 | const chartDataLoad = load.map((load, i) => ({ 37 | x: i, 38 | y: load.layout[index].load, 39 | })) as ChartVal[]; 40 | const chartDataMemory = load.map((load, i) => ({ 41 | x: i, 42 | y: load.layout[index].memory, 43 | })) as ChartVal[]; 44 | 45 | const chartLoad = ( 46 | 1} 48 | textLeft={ 49 | showPercentages 50 | ? `%: ${(chartDataLoad.at(-1)?.y as number)?.toFixed(1)} (Load)` 51 | : undefined 52 | } 53 | textOffset={textOffset} 54 | textSize={textSize} 55 | renderChart={size => ( 56 | `${val.payload?.[0]?.value?.toFixed(1)} %`} 62 | > 63 | 64 | 65 | )} 66 | > 67 | ); 68 | 69 | const chartMemory = ( 70 | 1} 72 | textLeft={`%: ${(chartDataMemory.at(-1)?.y as number)?.toFixed( 73 | 1 74 | )} (Memory)`} 75 | textOffset={textOffset} 76 | textSize={textSize} 77 | renderChart={size => ( 78 | `${val.payload?.[0]?.value?.toFixed(1)} %`} 84 | > 85 | 86 | 87 | )} 88 | > 89 | ); 90 | 91 | if (filter === 'load') 92 | return {chartLoad}; 93 | else if (filter === 'memory') 94 | return {chartMemory}; 95 | else 96 | return ( 97 | 98 | {chartLoad} 99 | {chartMemory} 100 | 101 | ); 102 | }; 103 | 104 | type GpuWidgetProps = { 105 | load: GpuLoad[]; 106 | data: GpuInfo; 107 | config: Config; 108 | }; 109 | 110 | export const GpuWidget: FC = ({ load, data, config }) => { 111 | const theme = useTheme(); 112 | const isMobile = useIsMobile(); 113 | const [page, setPage] = useState(0); 114 | const override = config.override; 115 | 116 | const infos = useMemo( 117 | () => 118 | data.layout.flatMap((gpu, i) => { 119 | const brand = override.gpu_brands[i] ?? gpu.brand; 120 | const model = override.gpu_models[i] ?? gpu.model; 121 | const memory = override.gpu_memories[i] ?? gpu.memory; 122 | 123 | return toInfoTable(config.gpu_label_list, { 124 | brand: { label: 'Brand', value: brand }, 125 | model: { label: 'Model', value: model }, 126 | memory: { 127 | label: 'Memory', 128 | value: memory ? bytePrettyPrint(memory * 1024 * 1024) : memory, 129 | }, 130 | }); 131 | }), 132 | [ 133 | config.gpu_label_list, 134 | data.layout, 135 | override.gpu_brands, 136 | override.gpu_memories, 137 | override.gpu_models, 138 | ] 139 | ); 140 | 141 | return ( 142 | 150 | 155 | 156 | ); 157 | }; 158 | -------------------------------------------------------------------------------- /apps/view/src/widgets/ram.tsx: -------------------------------------------------------------------------------- 1 | import { Config, RamInfo, RamLoad } from '@dash/common'; 2 | import { faMemory } from '@fortawesome/free-solid-svg-icons'; 3 | import { FC } from 'react'; 4 | import { Tooltip, YAxis } from 'recharts'; 5 | import { useTheme } from 'styled-components'; 6 | import { DefaultAreaChart } from '../components/chart-components'; 7 | import { 8 | ChartContainer, 9 | MultiChartContainer, 10 | } from '../components/chart-container'; 11 | import { HardwareInfoContainer } from '../components/hardware-info-container'; 12 | import { ThemedText } from '../components/text'; 13 | import { useIsMobile } from '../services/mobile'; 14 | import { removeDuplicates } from '../utils/array-utils'; 15 | import { bytePrettyPrint } from '../utils/calculations'; 16 | import { toInfoTable } from '../utils/format'; 17 | import { ChartVal } from '../utils/types'; 18 | 19 | export type RamChartProps = { 20 | load: RamLoad[]; 21 | data: RamInfo; 22 | showPercentages: boolean; 23 | textOffset?: string; 24 | textSize?: string; 25 | }; 26 | 27 | export const RamChart: FC = ({ 28 | load, 29 | data, 30 | showPercentages, 31 | textOffset, 32 | textSize, 33 | }) => { 34 | const theme = useTheme(); 35 | 36 | const chartData = load.map((load, i) => ({ 37 | x: i, 38 | y: (load / (data.size ?? 1)) * 100, 39 | })) as ChartVal[]; 40 | 41 | return ( 42 | 43 | 1} 45 | textLeft={ 46 | showPercentages 47 | ? `%: ${(chartData.at(-1)?.y as number)?.toFixed( 48 | 1 49 | )} (${bytePrettyPrint(load.at(-1) ?? 0)})` 50 | : undefined 51 | } 52 | textOffset={textOffset} 53 | textSize={textSize} 54 | renderChart={size => ( 55 | `${val.payload?.[0]?.value?.toFixed(1)} %`} 61 | > 62 | 63 | ( 65 | 66 | {(x.payload?.[0]?.value as number)?.toFixed(1)} % 67 | 68 | )} 69 | /> 70 | 71 | )} 72 | > 73 | 74 | ); 75 | }; 76 | 77 | type RamWidgetProps = { 78 | load: RamLoad[]; 79 | data: RamInfo; 80 | config: Config; 81 | }; 82 | 83 | export const RamWidget: FC = ({ load, data, config }) => { 84 | const theme = useTheme(); 85 | const isMobile = useIsMobile(); 86 | const override = config.override; 87 | 88 | const brands = removeDuplicates( 89 | override.ram_brand ? [override.ram_brand] : data.layout?.map(l => l.brand) 90 | ); 91 | const size = override.ram_size ?? data.size; 92 | const types = removeDuplicates( 93 | override.ram_type ? [override.ram_type] : data.layout?.map(l => l.type) 94 | ); 95 | const frequencies = removeDuplicates( 96 | override.ram_frequency 97 | ? [override.ram_frequency] 98 | : data.layout?.map(l => l.frequency).filter(c => c && c !== 0) 99 | ).map(s => `${s} MHz`); 100 | 101 | return ( 102 | 1 ? 'Brands' : 'Brand', 108 | value: brands.join(', '), 109 | }, 110 | size: { label: 'Size', value: size ? `${bytePrettyPrint(size)}` : '' }, 111 | type: { 112 | label: types.length > 1 ? 'Types' : 'Type', 113 | value: types.join(', '), 114 | }, 115 | frequency: { 116 | label: frequencies.length > 1 ? 'Frequencies' : 'Frequency', 117 | value: frequencies.join(', '), 118 | }, 119 | })} 120 | infosPerPage={7} 121 | icon={faMemory} 122 | > 123 | 128 | 129 | ); 130 | }; 131 | -------------------------------------------------------------------------------- /apps/view/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "types": [ 6 | "node", 7 | "vite/client", 8 | "@nx/react/typings/cssmodule.d.ts", 9 | "@nx/react/typings/image.d.ts" 10 | ] 11 | }, 12 | "files": [ 13 | "../../node_modules/@nx/react/typings/cssmodule.d.ts", 14 | "../../node_modules/@nx/react/typings/image.d.ts" 15 | ], 16 | "exclude": [ 17 | "src/**/*.spec.ts", 18 | "src/**/*.test.ts", 19 | "src/**/*.spec.tsx", 20 | "src/**/*.test.tsx", 21 | "src/**/*.spec.js", 22 | "src/**/*.test.js", 23 | "src/**/*.spec.jsx", 24 | "src/**/*.test.jsx" 25 | ], 26 | "include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"] 27 | } 28 | -------------------------------------------------------------------------------- /apps/view/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react-jsx", 4 | "allowJs": false, 5 | "esModuleInterop": false, 6 | "allowSyntheticDefaultImports": true, 7 | "strict": true, 8 | "types": ["vite/client", "vitest"] 9 | }, 10 | "files": [], 11 | "include": [], 12 | "references": [ 13 | { 14 | "path": "./tsconfig.app.json" 15 | }, 16 | { 17 | "path": "./tsconfig.spec.json" 18 | } 19 | ], 20 | "extends": "../../tsconfig.base.json" 21 | } 22 | -------------------------------------------------------------------------------- /apps/view/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "types": [ 6 | "vitest/globals", 7 | "vitest/importMeta", 8 | "vite/client", 9 | "node", 10 | "@nx/react/typings/cssmodule.d.ts", 11 | "@nx/react/typings/image.d.ts" 12 | ] 13 | }, 14 | "include": [ 15 | "vite.config.ts", 16 | "src/**/*.test.ts", 17 | "src/**/*.spec.ts", 18 | "src/**/*.test.tsx", 19 | "src/**/*.spec.tsx", 20 | "src/**/*.test.js", 21 | "src/**/*.spec.js", 22 | "src/**/*.test.jsx", 23 | "src/**/*.spec.jsx", 24 | "src/**/*.d.ts" 25 | ], 26 | "files": [ 27 | "../../node_modules/@nx/react/typings/cssmodule.d.ts", 28 | "../../node_modules/@nx/react/typings/image.d.ts" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /apps/view/vite.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; 3 | import react from '@vitejs/plugin-react'; 4 | import { defineConfig } from 'vite'; 5 | 6 | export default defineConfig({ 7 | root: __dirname, 8 | cacheDir: '../../node_modules/.vite/view', 9 | 10 | server: { 11 | port: 3000, 12 | host: '0.0.0.0', 13 | }, 14 | 15 | preview: { 16 | port: 3000, 17 | host: '0.0.0.0', 18 | }, 19 | 20 | plugins: [react(), nxViteTsPaths()], 21 | 22 | //@ts-ignore 23 | test: { 24 | reporters: ['default'], 25 | coverage: { 26 | reportsDirectory: '../../coverage/apps/view', 27 | provider: 'v8', 28 | }, 29 | globals: true, 30 | cache: { 31 | dir: '../../node_modules/.vitest', 32 | }, 33 | environment: 'jsdom', 34 | include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], 35 | }, 36 | 37 | build: { 38 | outDir: '../../dist/apps/view', 39 | reportCompressedSize: true, 40 | commonjsOptions: { transformMixedEsModules: true }, 41 | target: 'es2015', 42 | }, 43 | }); 44 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "babelrcRoots": ["*"] 3 | } 4 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | # !! Attention !! 2 | # This is a docker-compose file used ONLY for the dev environment and can not be used for production. 3 | # If you need instructions on how to run dashdot, please have a look at the README.md file in the root of the project. 4 | services: 5 | dash: 6 | build: 7 | context: . 8 | dockerfile: Dockerfile 9 | target: dev 10 | tty: true 11 | command: yarn run serve 12 | restart: unless-stopped 13 | privileged: true 14 | environment: 15 | - DASHDOT_ENABLE_CPU_TEMPS=true 16 | - DASHDOT_SPEED_TEST_FROM_PATH=/app/speedtest_result 17 | - DASHDOT_SHOW_DASH_VERSION=none 18 | ports: 19 | - 3000:3000 # view 20 | - 3001:3001 # server 21 | - 3002:3002 # docs 22 | volumes: 23 | - ./:/app 24 | - /:/mnt/host:ro 25 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import { getJestProjects } from '@nx/jest'; 2 | 3 | export default { 4 | projects: getJestProjects(), 5 | }; 6 | -------------------------------------------------------------------------------- /jest.preset.js: -------------------------------------------------------------------------------- 1 | const nxPreset = require('@nx/jest/preset').default; 2 | 3 | module.exports = { ...nxPreset }; 4 | -------------------------------------------------------------------------------- /libs/common/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": {} 8 | }, 9 | { 10 | "files": ["*.ts", "*.tsx"], 11 | "rules": {} 12 | }, 13 | { 14 | "files": ["*.js", "*.jsx"], 15 | "rules": {} 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /libs/common/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "common", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "sourceRoot": "libs/common/src", 5 | "projectType": "library", 6 | "targets": { 7 | "build": { 8 | "executor": "@nx/js:tsc", 9 | "outputs": ["{options.outputPath}"], 10 | "options": { 11 | "outputPath": "dist/libs/common", 12 | "main": "libs/common/src/index.ts", 13 | "tsConfig": "libs/common/tsconfig.lib.json", 14 | "assets": ["libs/common/*.md"] 15 | } 16 | }, 17 | "lint": { 18 | "executor": "@nx/eslint:lint", 19 | "outputs": ["{options.outputFile}"] 20 | } 21 | }, 22 | "tags": [] 23 | } 24 | -------------------------------------------------------------------------------- /libs/common/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types'; 2 | export * from './utils'; 3 | -------------------------------------------------------------------------------- /libs/common/src/types.ts: -------------------------------------------------------------------------------- 1 | export type CpuInfo = { 2 | brand: string; 3 | model: string; 4 | cores: number; 5 | ecores: number; 6 | pcores: number; 7 | threads: number; 8 | frequency: number; 9 | }; 10 | export type CpuLoad = { 11 | core: number; 12 | load: number; 13 | temp: number; 14 | }[]; 15 | 16 | export type RamInfo = { 17 | size: number; 18 | layout: { 19 | brand?: string; 20 | type?: string; 21 | frequency?: number; 22 | }[]; 23 | }; 24 | export type RamLoad = number; 25 | 26 | export enum RaidType { 27 | ZERO = 0, 28 | ONE = 1, 29 | } 30 | export type StorageInfo = { 31 | raidType?: RaidType; 32 | raidLabel?: string; 33 | raidName?: string; 34 | size: number; 35 | virtual?: boolean; 36 | 37 | disks: { 38 | device: string; 39 | brand: string; 40 | type: string; 41 | }[]; 42 | }[]; 43 | export type StorageLoad = number[]; 44 | 45 | export type NetworkInfo = { 46 | interfaceSpeed: number; 47 | speedDown: number; 48 | speedUp: number; 49 | lastSpeedTest: number; 50 | type: string; 51 | publicIp: string; 52 | }; 53 | export type NetworkLoad = { 54 | up: number; 55 | down: number; 56 | }; 57 | 58 | export type GpuInfo = { 59 | layout: { 60 | brand: string; 61 | model: string; 62 | memory: number; 63 | }[]; 64 | }; 65 | export type GpuLoad = { 66 | layout: { 67 | load: number; 68 | memory: number; 69 | }[]; 70 | }; 71 | 72 | export type OsInfo = { 73 | platform: string; 74 | distro: string; 75 | release: string; 76 | kernel: string; 77 | arch: string; 78 | uptime: number; 79 | dash_version: string; 80 | dash_buildhash: string; 81 | }; 82 | 83 | export type HardwareInfo = { 84 | os: OsInfo; 85 | cpu: CpuInfo; 86 | ram: RamInfo; 87 | storage: StorageInfo; 88 | network: NetworkInfo; 89 | gpu: GpuInfo; 90 | }; 91 | 92 | export type Config = { 93 | // General 94 | port: number; 95 | running_in_docker: boolean; 96 | use_network_interface?: string; 97 | speed_test_from_path?: string; 98 | accept_ookla_eula: boolean; 99 | fs_device_filter: string[]; 100 | fs_type_filter: string[]; 101 | fs_virtual_mounts: string[]; 102 | disable_integrations: boolean; 103 | 104 | show_dash_version?: 'icon_hover' | 'bottom_right'; 105 | show_host: boolean; 106 | custom_host?: string; 107 | page_title: string; 108 | use_imperial: boolean; 109 | network_speed_as_bytes: boolean; 110 | always_show_percentages: boolean; 111 | enable_cpu_temps: boolean; 112 | cpu_temps_mode: 'max' | 'avg'; 113 | 114 | // Widgets, Labels 115 | widget_list: ('os' | 'cpu' | 'storage' | 'ram' | 'network' | 'gpu')[]; 116 | os_label_list: ('os' | 'arch' | 'up_since' | 'dash_version')[]; 117 | cpu_label_list: ('brand' | 'model' | 'cores' | 'threads' | 'frequency')[]; 118 | storage_label_list: ('brand' | 'size' | 'type' | 'raid')[]; 119 | ram_label_list: ('brand' | 'size' | 'type' | 'frequency')[]; 120 | network_label_list: ( 121 | | 'type' 122 | | 'speed_up' 123 | | 'speed_down' 124 | | 'interface_speed' 125 | | 'public_ip' 126 | )[]; 127 | gpu_label_list: ('brand' | 'model' | 'memory')[]; 128 | 129 | // OS Widget 130 | os_widget_grow: number; 131 | os_widget_min_width: number; 132 | 133 | // CPU Widget 134 | cpu_widget_grow: number; 135 | cpu_widget_min_width: number; 136 | cpu_shown_datapoints: number; 137 | cpu_poll_interval: number; 138 | cpu_cores_toggle_mode: 'toggle' | 'multi-core' | 'average'; 139 | 140 | // Storage Widget 141 | storage_widget_items_per_page: number; 142 | storage_widget_grow: number; 143 | storage_widget_min_width: number; 144 | storage_poll_interval: number; 145 | 146 | // RAM Widget 147 | ram_widget_grow: number; 148 | ram_widget_min_width: number; 149 | ram_shown_datapoints: number; 150 | ram_poll_interval: number; 151 | 152 | // Network Widget 153 | speed_test_interval: number; 154 | speed_test_interval_cron?: string; 155 | network_widget_grow: number; 156 | network_widget_min_width: number; 157 | network_shown_datapoints: number; 158 | network_poll_interval: number; 159 | 160 | // GPU Widget 161 | gpu_widget_grow: number; 162 | gpu_widget_min_width: number; 163 | gpu_shown_datapoints: number; 164 | gpu_poll_interval: number; 165 | 166 | // Overrides 167 | override: { 168 | os?: string; 169 | arch?: string; 170 | cpu_brand?: string; 171 | cpu_model?: string; 172 | cpu_cores?: number; 173 | cpu_threads?: number; 174 | cpu_frequency?: number; 175 | ram_brand?: string; 176 | ram_size?: number; 177 | ram_type?: string; 178 | ram_frequency?: number; 179 | network_type?: string; 180 | network_speed_up?: number; 181 | network_speed_down?: number; 182 | network_interface_speed?: number; 183 | network_public_ip?: string; 184 | storage_brands: Record; 185 | storage_types: Record; 186 | storage_sizes: Record; 187 | gpu_brands: string[]; 188 | gpu_models: string[]; 189 | gpu_memories: number[]; 190 | }; 191 | }; 192 | 193 | export type ServerInfo = HardwareInfo & { 194 | config: Config; 195 | }; 196 | 197 | export type Transient = { 198 | [K in keyof T & string as `$${K}`]: T[K]; 199 | }; 200 | -------------------------------------------------------------------------------- /libs/common/src/utils.ts: -------------------------------------------------------------------------------- 1 | export const clamp = (num: number, min: number, max: number) => { 2 | return num > max ? max : num < min ? min : num; 3 | }; 4 | 5 | export const sumUp = (arr: T, key: keyof T[number]) => { 6 | return arr.reduce((acc, obj) => acc + obj[key], 0); 7 | }; 8 | 9 | export const findLastIndex = ( 10 | array: Array, 11 | predicate: (value: T, index: number, obj: T[]) => boolean 12 | ): number => { 13 | let l = array.length; 14 | while (l--) { 15 | if (predicate(array[l], l, array)) return l; 16 | } 17 | return -1; 18 | }; 19 | 20 | export const isFilledLine = (s: string): boolean => /^\s*$/.test(s) === false; 21 | 22 | export const capFirst = (str: string) => { 23 | return str[0].toUpperCase() + str.slice(1); 24 | }; 25 | -------------------------------------------------------------------------------- /libs/common/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "forceConsistentCasingInFileNames": true, 6 | "strict": true, 7 | "noImplicitOverride": true, 8 | "noPropertyAccessFromIndexSignature": true, 9 | "noImplicitReturns": true, 10 | "noFallthroughCasesInSwitch": true 11 | }, 12 | "files": [], 13 | "include": [], 14 | "references": [ 15 | { 16 | "path": "./tsconfig.lib.json" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /libs/common/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "declaration": true, 6 | "types": ["node"] 7 | }, 8 | "include": ["src/**/*.ts"], 9 | "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /nx.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/nx/schemas/nx-schema.json", 3 | "targetDefaults": { 4 | "build": { 5 | "dependsOn": ["^build"], 6 | "inputs": ["production", "^production"], 7 | "cache": true 8 | }, 9 | "lint": { 10 | "inputs": [ 11 | "default", 12 | "{workspaceRoot}/.eslintrc.json", 13 | "{workspaceRoot}/.eslintignore" 14 | ], 15 | "cache": true 16 | }, 17 | "@nx/jest:jest": { 18 | "inputs": ["default", "^production", "{workspaceRoot}/jest.preset.js"], 19 | "cache": true, 20 | "options": { 21 | "passWithNoTests": true 22 | }, 23 | "configurations": { 24 | "ci": { 25 | "ci": true, 26 | "codeCoverage": true 27 | } 28 | } 29 | }, 30 | "@nx/vite:test": { 31 | "inputs": ["default", "^production", "{workspaceRoot}/jest.preset.js"], 32 | "cache": true 33 | } 34 | }, 35 | "namedInputs": { 36 | "default": ["{projectRoot}/**/*", "sharedGlobals"], 37 | "production": [ 38 | "default", 39 | "!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)", 40 | "!{projectRoot}/tsconfig.spec.json", 41 | "!{projectRoot}/.eslintrc.json", 42 | "!{projectRoot}/jest.config.[jt]s", 43 | "!{projectRoot}/src/test-setup.[jt]s" 44 | ], 45 | "sharedGlobals": [] 46 | }, 47 | "workspaceLayout": { 48 | "appsDir": "apps", 49 | "libsDir": "libs" 50 | }, 51 | "generators": { 52 | "@nx/react": { 53 | "application": { 54 | "style": "styled-components", 55 | "linter": "eslint", 56 | "bundler": "vite", 57 | "babel": true 58 | }, 59 | "component": { 60 | "style": "styled-components" 61 | }, 62 | "library": { 63 | "style": "styled-components", 64 | "linter": "eslint" 65 | } 66 | } 67 | }, 68 | "nxCloudAccessToken": "YWQ3NGUyYmUtYzdkZS00OTUyLTkxYTYtYWI0M2I3YTVmOGJmfHJlYWQtd3JpdGU=" 69 | } 70 | -------------------------------------------------------------------------------- /scripts/strip_package_json.js: -------------------------------------------------------------------------------- 1 | const packageJson = require('../package.json') 2 | const fs = require('fs') 3 | 4 | const newPackageJson = { 5 | name: packageJson.name, 6 | version: packageJson.version, 7 | description: packageJson.description, 8 | packageManager: packageJson.packageManager, 9 | main: packageJson.main, 10 | scripts: { 11 | cli: 'node dist/apps/cli/main.js', 12 | } 13 | } 14 | 15 | fs.writeFileSync('dist/package.json', JSON.stringify(newPackageJson, null, 2)) -------------------------------------------------------------------------------- /speedtest_result: -------------------------------------------------------------------------------- 1 | { 2 | "unit": "bit", 3 | "speedDown": 150000000, 4 | "speedUp": 50000000 5 | } 6 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "rootDir": ".", 5 | "sourceMap": true, 6 | "declaration": false, 7 | "moduleResolution": "node", 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "importHelpers": true, 11 | "target": "es2015", 12 | "module": "esnext", 13 | "lib": ["es2020", "dom"], 14 | "skipLibCheck": true, 15 | "skipDefaultLibCheck": true, 16 | "baseUrl": ".", 17 | "paths": { 18 | "@dash/common": ["libs/common/src/index.ts"] 19 | } 20 | }, 21 | "exclude": ["node_modules", "tmp"] 22 | } 23 | --------------------------------------------------------------------------------