├── .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 |
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 |
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 | | | |
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 | | | |
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 | | | |
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 | | | |
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 | | | |
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 | | | |
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 | | | |
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 | | | |
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 | setProtocol(e)}
109 | data={[
110 | { value: 'https', label: 'https://' },
111 | { value: 'http', label: 'http://' },
112 | ]}
113 | />
114 | 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 |
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 |
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 |
57 | ) : (
58 |
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 |
--------------------------------------------------------------------------------