mirror of
https://github.com/gethomepage/homepage.git
synced 2025-07-31 14:33:48 -04:00
Merge branch 'dev'
This commit is contained in:
commit
cbf2e1a509
2
.github/workflows/docker-publish.yml
vendored
2
.github/workflows/docker-publish.yml
vendored
@ -35,7 +35,6 @@ jobs:
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
run_install: false
|
||||
|
||||
- name: Setup Node.js
|
||||
@ -94,7 +93,6 @@ jobs:
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
run_install: false
|
||||
|
||||
- name: Setup Node.js
|
||||
|
32
.vscode/launch.json
vendored
32
.vscode/launch.json
vendored
@ -1,19 +1,31 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Next.js: debug full stack",
|
||||
"name": "Debug homepage",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/next",
|
||||
"serverReadyAction": {
|
||||
"pattern": "started server on .+, url: (https?://.+)",
|
||||
"uriFormat": "%s",
|
||||
"action": "debugWithChrome"
|
||||
"runtimeExecutable": "pnpm",
|
||||
"runtimeArgs": ["run", "dev"],
|
||||
"env": {
|
||||
"LOG_LEVEL": "debug"
|
||||
},
|
||||
"skipFiles": ["<node_internals>/**"],
|
||||
"console": "integratedTerminal",
|
||||
"serverReadyAction":{
|
||||
"pattern": ".*http://localhost:3000.*",
|
||||
"action": "startDebugging",
|
||||
"name": "Launch Chromium",
|
||||
"killOnServerStop": true,
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Launch Chromium",
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"url": "http://localhost:3000",
|
||||
"urlFilter": "http://localhost:3000",
|
||||
"webRoot": "${workspaceFolder}",
|
||||
"trace": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -44,9 +44,15 @@ fi
|
||||
|
||||
if [ -d /app/.next ]; then
|
||||
CURRENT_UID=$(stat -c %u /app/.next)
|
||||
if [ "$CURRENT_UID" -ne "$PUID" ]; then
|
||||
CURRENT_GID=$(stat -c %g /app/.next)
|
||||
|
||||
if [ "$PUID" -ne 0 ] && ([ "$CURRENT_UID" -ne "$PUID" ] || [ "$CURRENT_GID" -ne "$PGID" ]); then
|
||||
echo "Fixing ownership of /app/.next"
|
||||
chown -R "$PUID:$PGID" /app/.next || echo "Warning: Could not chown /app/.next"
|
||||
if ! chown -R "$PUID:$PGID" /app/.next 2>/dev/null; then
|
||||
echo "Warning: Could not chown /app/.next; continuing anyway"
|
||||
fi
|
||||
else
|
||||
echo "/app/.next already owned by correct UID/GID or running as root, skipping chown"
|
||||
fi
|
||||
fi
|
||||
|
||||
|
79
docs/configs/proxmox.md
Normal file
79
docs/configs/proxmox.md
Normal file
@ -0,0 +1,79 @@
|
||||
---
|
||||
title: Proxmox
|
||||
description: Proxmox Configuration
|
||||
---
|
||||
|
||||
The Proxmox connection is configured in the `proxmox.yaml` file. See [Create token](#create-token) section below for details on how to generate the required API token.
|
||||
|
||||
```yaml
|
||||
url: https://proxmox.host.or.ip:8006
|
||||
token: username@pam!Token ID
|
||||
secret: secret
|
||||
```
|
||||
|
||||
## Services
|
||||
|
||||
Once the Proxmox connection is configured, individual services can be configured to pull statistics of VMs or LXCs. Only CPU and Memory are currently supported.
|
||||
|
||||
### Configuration Options
|
||||
|
||||
- `proxmoxNode`: The name of the Proxmox node where your VM/LXC is running
|
||||
- `proxmoxVMID`: The ID of the Proxmox VM or LXC container
|
||||
- `proxmoxType`: (Optional) The type of Proxmox virtual machine. Defaults to `qemu` for VMs, but can be set to `lxc` for LXC containers
|
||||
|
||||
#### Examples
|
||||
|
||||
For a QEMU VM (default):
|
||||
|
||||
```yaml
|
||||
- HomeAssistant:
|
||||
icon: home-assistant.png
|
||||
href: http://homeassistant.local/
|
||||
description: Home automation
|
||||
proxmoxNode: pve
|
||||
proxmoxVMID: 101
|
||||
# proxmoxType: qemu # This is the default, so it can be omitted
|
||||
```
|
||||
|
||||
For an LXC container:
|
||||
|
||||
```yaml
|
||||
- Nginx:
|
||||
icon: nginx.png
|
||||
href: http://nginx.local/
|
||||
description: Web server
|
||||
proxmoxNode: pve
|
||||
proxmoxVMID: 200
|
||||
proxmoxType: lxc
|
||||
```
|
||||
|
||||
## Create token
|
||||
|
||||
You will need to generate an API Token for new or an existing user. Here is an example of how to do this for a new user.
|
||||
|
||||
1. Navigate to the Proxmox portal, click on Datacenter
|
||||
2. Expand Permissions, click on Groups
|
||||
3. Click the Create button
|
||||
4. Name the group something informative, like api-ro-users
|
||||
5. Click on the Permissions "folder"
|
||||
6. Click Add -> Group Permission
|
||||
- Path: /
|
||||
- Group: group from bullet 4 above
|
||||
- Role: PVEAuditor
|
||||
- Propagate: Checked
|
||||
7. Expand Permissions, click on Users
|
||||
8. Click the Add button
|
||||
- User name: something informative like `api`
|
||||
- Realm: Linux PAM standard authentication
|
||||
- Group: group from bullet 4 above
|
||||
9. Expand Permissions, click on API Tokens
|
||||
10. Click the Add button
|
||||
- User: user from bullet 8 above
|
||||
- Token ID: something informative like the application or purpose like `homepage`
|
||||
- Privilege Separation: Checked
|
||||
11. Go back to the "Permissions" menu
|
||||
12. Click Add -> API Token Permission
|
||||
- Path: /
|
||||
- API Token: select the Token ID created in Step 10
|
||||
- Role: PVE Auditor
|
||||
- Propagate: Checked
|
@ -22,6 +22,7 @@ widget:
|
||||
service_group: Media # group name where widget exists
|
||||
service_name: Sonarr # service name for that widget
|
||||
color: teal # optional - defaults to pre-defined color for the service (teal for sonarr)
|
||||
baseUrl: https://sonarr.domain.url # optional - adds links to sonarr/radarr pages
|
||||
params: # optional - additional params for the service
|
||||
unmonitored: true # optional - defaults to false, used with *arr stack
|
||||
- type: ical # Show calendar events from another service
|
||||
|
@ -17,6 +17,7 @@ widget:
|
||||
enableBlocks: true # optional, defaults to false
|
||||
enableNowPlaying: true # optional, defaults to true
|
||||
enableUser: true # optional, defaults to false
|
||||
enableMediaControl: false # optional, defaults to true
|
||||
showEpisodeNumber: true # optional, defaults to false
|
||||
expandOneStreamToTwoRows: false # optional, defaults to true
|
||||
```
|
||||
|
@ -5,11 +5,18 @@ description: Grafana Widget Configuration
|
||||
|
||||
Learn more about [Grafana](https://github.com/grafana/grafana).
|
||||
|
||||
| Grafana Version | Homepage Widget Version |
|
||||
| --------------- | ----------------------- |
|
||||
| <= v10.4 | 1 (default) |
|
||||
| > v10.4 | 2 |
|
||||
|
||||
Allowed fields: `["dashboards", "datasources", "totalalerts", "alertstriggered"]`.
|
||||
|
||||
```yaml
|
||||
widget:
|
||||
type: grafana
|
||||
version: 2 # optional, default is 1
|
||||
alerts: alertmanager # optional, default is grafana
|
||||
url: http://grafana.host.or.ip:port
|
||||
username: username
|
||||
password: password
|
||||
|
@ -17,6 +17,7 @@ widget:
|
||||
enableBlocks: true # optional, defaults to false
|
||||
enableNowPlaying: true # optional, defaults to true
|
||||
enableUser: true # optional, defaults to false
|
||||
enableMediaControl: false # optional, defaults to true
|
||||
showEpisodeNumber: true # optional, defaults to false
|
||||
expandOneStreamToTwoRows: false # optional, defaults to true
|
||||
```
|
||||
|
22
docs/widgets/services/komodo.md
Normal file
22
docs/widgets/services/komodo.md
Normal file
@ -0,0 +1,22 @@
|
||||
---
|
||||
title: Komodo
|
||||
description: Komodo Widget Configuration
|
||||
---
|
||||
|
||||
This widget shows either details about all containers or stacks (if `showStacks` is true) managed by [Komodo](https://komo.do/) or the number of running servers, containers and stacks when `showSummary` is enabled.
|
||||
|
||||
The api key and secret can be found in the Komodo settings.
|
||||
|
||||
Allowed fields (max 4): `["total", "running", "stopped", "unhealthy", "unknown"]`.
|
||||
Allowed fields with `showStacks` (max 4): `["total", "running", "down", "unhealthy", "unknown"]`.
|
||||
Allowed fields with `showSummary`: `["servers", "stacks", "containers"]`.
|
||||
|
||||
```yaml
|
||||
widget:
|
||||
type: komodo
|
||||
url: http://komodo.hostname.or.ip:port
|
||||
key: K-xxxxxx...
|
||||
secret: S-xxxxxx...
|
||||
showSummary: true # optional, default: false
|
||||
showStacks: true # optional, default: false
|
||||
```
|
@ -7,12 +7,16 @@ Learn more about [Portainer](https://github.com/portainer/portainer).
|
||||
|
||||
You'll need to make sure you have the correct environment set for the integration to work properly. From the Environments section inside of Portainer, click the one you'd like to connect to and observe the ID at the end of the URL (should be), something like `#!/endpoints/1`, here `1` is the value to set as the `env` value. In order to generate an API key, please follow the steps outlined here https://docs.portainer.io/api/access.
|
||||
|
||||
Allowed fields: `["running", "stopped", "total"]`.
|
||||
Allowed fields:
|
||||
|
||||
- For Docker mode (default): `["running", "stopped", "total"]`
|
||||
- For Kubernetes mode (`kubernetes: true`): `["applications", "services", "namespaces"]`
|
||||
|
||||
```yaml
|
||||
widget:
|
||||
type: portainer
|
||||
url: https://portainer.host.or.ip:9443
|
||||
env: 1
|
||||
kubernetes: true # optional, defaults to false
|
||||
key: ptr_accesskeyaccesskeyaccesskeyaccesskey
|
||||
```
|
||||
|
@ -7,34 +7,7 @@ Learn more about [Proxmox](https://www.proxmox.com/en/).
|
||||
|
||||
This widget shows the running and total counts of both QEMU VMs and LX Containers in the Proxmox cluster. It also shows the CPU and memory usage of the first node in the cluster.
|
||||
|
||||
You will need to generate an API Token for new or an existing user. Here is an example of how to do this for a new user.
|
||||
|
||||
1. Navigate to the Proxmox portal, click on Datacenter
|
||||
2. Expand Permissions, click on Groups
|
||||
3. Click the Create button
|
||||
4. Name the group something informative, like api-ro-users
|
||||
5. Click on the Permissions "folder"
|
||||
6. Click Add -> Group Permission
|
||||
- Path: /
|
||||
- Group: group from bullet 4 above
|
||||
- Role: PVEAuditor
|
||||
- Propagate: Checked
|
||||
7. Expand Permissions, click on Users
|
||||
8. Click the Add button
|
||||
- User name: something informative like `api`
|
||||
- Realm: Linux PAM standard authentication
|
||||
- Group: group from bullet 4 above
|
||||
9. Expand Permissions, click on API Tokens
|
||||
10. Click the Add button
|
||||
- User: user from bullet 8 above
|
||||
- Token ID: something informative like the application or purpose like `homepage`
|
||||
- Privilege Separation: Checked
|
||||
11. Go back to the "Permissions" menu
|
||||
12. Click Add -> API Token Permission
|
||||
- Path: /
|
||||
- API Token: select the Token ID created in Step 10
|
||||
- Role: PVE Auditor
|
||||
- Propagate: Checked
|
||||
See the [Proxmox configuration documentation](../../configs/proxmox.md#create-token) for details on creating API tokens.
|
||||
|
||||
Use `username@pam!Token ID` as the `username` (e.g `api@pam!homepage`) setting and `Secret` as the `password` setting.
|
||||
|
||||
|
17
docs/widgets/services/trilium.md
Normal file
17
docs/widgets/services/trilium.md
Normal file
@ -0,0 +1,17 @@
|
||||
---
|
||||
title: Trilium
|
||||
description: Trilium Widget Configuration
|
||||
---
|
||||
|
||||
Learn more about [Trilium](https://github.com/TriliumNext/Notes).
|
||||
|
||||
This widget is compatible with [TriliumNext](https://github.com/TriliumNext/Notes) versions >= [v0.94.0](https://github.com/TriliumNext/Notes/releases/tag/v0.94.0).
|
||||
|
||||
Find (or create) your ETAPI key under `Options > ETAPI > Create new ETAPI token`.
|
||||
|
||||
```yaml
|
||||
widget:
|
||||
type: trilium
|
||||
url: https://trilium.host.or.ip
|
||||
key: etapi_token
|
||||
```
|
21
package.json
21
package.json
@ -22,7 +22,7 @@
|
||||
"ical.js": "^2.1.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"json-rpc-2.0": "^1.7.0",
|
||||
"luxon": "^3.5.0",
|
||||
"luxon": "^3.6.1",
|
||||
"memory-cache": "^0.2.0",
|
||||
"minecraftstatuspinger": "^1.2.2",
|
||||
"next": "^15.3.1",
|
||||
@ -32,11 +32,11 @@
|
||||
"raw-body": "^3.0.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-i18next": "^11.18.6",
|
||||
"react-i18next": "^15.5.3",
|
||||
"react-icons": "^5.4.0",
|
||||
"recharts": "^2.15.3",
|
||||
"swr": "^2.3.3",
|
||||
"systeminformation": "^5.25.11",
|
||||
"systeminformation": "^5.27.7",
|
||||
"tough-cookie": "^5.1.2",
|
||||
"urbackup-server-api": "^0.8.9",
|
||||
"winston": "^3.17.0",
|
||||
@ -50,19 +50,26 @@
|
||||
"eslint-config-prettier": "^10.1.1",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||
"eslint-plugin-prettier": "^5.2.6",
|
||||
"eslint-plugin-prettier": "^5.5.1",
|
||||
"eslint-plugin-react": "^7.37.4",
|
||||
"eslint-plugin-react-hooks": "^5.1.0",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"postcss": "^8.5.3",
|
||||
"prettier": "^3.5.2",
|
||||
"prettier": "^3.6.2",
|
||||
"prettier-plugin-organize-imports": "^4.1.0",
|
||||
"tailwind-scrollbar": "^4.0.1",
|
||||
"tailwind-scrollbar": "^4.0.2",
|
||||
"tailwindcss": "^4.0.9",
|
||||
"typescript": "^5.7.3"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"osx-temperature-sensor": "^1.0.8"
|
||||
},
|
||||
"packageManager": "pnpm@10.8.1",
|
||||
"devEngines": {
|
||||
"packageManager": {
|
||||
"name": "pnpm",
|
||||
"version": "10.8.1"
|
||||
}
|
||||
},
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"sharp"
|
||||
|
132
pnpm-lock.yaml
generated
132
pnpm-lock.yaml
generated
@ -42,8 +42,8 @@ importers:
|
||||
specifier: ^1.7.0
|
||||
version: 1.7.0
|
||||
luxon:
|
||||
specifier: ^3.5.0
|
||||
version: 3.5.0
|
||||
specifier: ^3.6.1
|
||||
version: 3.6.1
|
||||
memory-cache:
|
||||
specifier: ^0.2.0
|
||||
version: 0.2.0
|
||||
@ -72,8 +72,8 @@ importers:
|
||||
specifier: ^18.3.1
|
||||
version: 18.3.1(react@18.3.1)
|
||||
react-i18next:
|
||||
specifier: ^11.18.6
|
||||
version: 11.18.6(i18next@24.2.3(typescript@5.7.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
specifier: ^15.5.3
|
||||
version: 15.5.3(i18next@24.2.3(typescript@5.7.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.3)
|
||||
react-icons:
|
||||
specifier: ^5.4.0
|
||||
version: 5.4.0(react@18.3.1)
|
||||
@ -84,8 +84,8 @@ importers:
|
||||
specifier: ^2.3.3
|
||||
version: 2.3.3(react@18.3.1)
|
||||
systeminformation:
|
||||
specifier: ^5.25.11
|
||||
version: 5.25.11
|
||||
specifier: ^5.27.7
|
||||
version: 5.27.7
|
||||
tough-cookie:
|
||||
specifier: ^5.1.2
|
||||
version: 5.1.2
|
||||
@ -98,10 +98,6 @@ importers:
|
||||
xml-js:
|
||||
specifier: ^1.6.11
|
||||
version: 1.6.11
|
||||
optionalDependencies:
|
||||
osx-temperature-sensor:
|
||||
specifier: ^1.0.8
|
||||
version: 1.0.8
|
||||
devDependencies:
|
||||
'@tailwindcss/forms':
|
||||
specifier: ^0.5.10
|
||||
@ -125,32 +121,36 @@ importers:
|
||||
specifier: ^6.10.2
|
||||
version: 6.10.2(eslint@9.25.1(jiti@2.4.2))
|
||||
eslint-plugin-prettier:
|
||||
specifier: ^5.2.6
|
||||
version: 5.2.6(eslint-config-prettier@10.1.1(eslint@9.25.1(jiti@2.4.2)))(eslint@9.25.1(jiti@2.4.2))(prettier@3.5.2)
|
||||
specifier: ^5.5.1
|
||||
version: 5.5.1(eslint-config-prettier@10.1.1(eslint@9.25.1(jiti@2.4.2)))(eslint@9.25.1(jiti@2.4.2))(prettier@3.6.2)
|
||||
eslint-plugin-react:
|
||||
specifier: ^7.37.4
|
||||
version: 7.37.4(eslint@9.25.1(jiti@2.4.2))
|
||||
eslint-plugin-react-hooks:
|
||||
specifier: ^5.1.0
|
||||
version: 5.1.0(eslint@9.25.1(jiti@2.4.2))
|
||||
specifier: ^5.2.0
|
||||
version: 5.2.0(eslint@9.25.1(jiti@2.4.2))
|
||||
postcss:
|
||||
specifier: ^8.5.3
|
||||
version: 8.5.3
|
||||
prettier:
|
||||
specifier: ^3.5.2
|
||||
version: 3.5.2
|
||||
specifier: ^3.6.2
|
||||
version: 3.6.2
|
||||
prettier-plugin-organize-imports:
|
||||
specifier: ^4.1.0
|
||||
version: 4.1.0(prettier@3.5.2)(typescript@5.7.3)
|
||||
version: 4.1.0(prettier@3.6.2)(typescript@5.7.3)
|
||||
tailwind-scrollbar:
|
||||
specifier: ^4.0.1
|
||||
version: 4.0.1(react@18.3.1)(tailwindcss@4.0.9)
|
||||
specifier: ^4.0.2
|
||||
version: 4.0.2(react@18.3.1)(tailwindcss@4.0.9)
|
||||
tailwindcss:
|
||||
specifier: ^4.0.9
|
||||
version: 4.0.9
|
||||
typescript:
|
||||
specifier: ^5.7.3
|
||||
version: 5.7.3
|
||||
optionalDependencies:
|
||||
osx-temperature-sensor:
|
||||
specifier: ^1.0.8
|
||||
version: 1.0.8
|
||||
|
||||
packages:
|
||||
|
||||
@ -166,8 +166,8 @@ packages:
|
||||
resolution: {integrity: sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/runtime@7.27.1':
|
||||
resolution: {integrity: sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==}
|
||||
'@babel/runtime@7.27.6':
|
||||
resolution: {integrity: sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@balena/dockerignore@1.0.2':
|
||||
@ -476,8 +476,8 @@ packages:
|
||||
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
'@pkgr/core@0.2.4':
|
||||
resolution: {integrity: sha512-ROFF39F6ZrnzSUEmQQZUar0Jt4xVoP9WnDRdWwF4NNcXs3xBTLgBUDoOwW141y1jP+S8nahIbdxbFC7IShw9Iw==}
|
||||
'@pkgr/core@0.2.7':
|
||||
resolution: {integrity: sha512-YLT9Zo3oNPJoBjBc4q8G2mjU4tqIbf5CEOORbUUr48dCD9q3umJ3IPlVqOqDakPfd2HuwccBaqlGhN4Gmr5OWg==}
|
||||
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
|
||||
|
||||
'@protobufjs/aspromise@1.1.2':
|
||||
@ -1364,8 +1364,8 @@ packages:
|
||||
peerDependencies:
|
||||
eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9
|
||||
|
||||
eslint-plugin-prettier@5.2.6:
|
||||
resolution: {integrity: sha512-mUcf7QG2Tjk7H055Jk0lGBjbgDnfrvqjhXh9t2xLMSCjZVcw9Rb1V6sVNXO0th3jgeO7zllWPTNRil3JW94TnQ==}
|
||||
eslint-plugin-prettier@5.5.1:
|
||||
resolution: {integrity: sha512-dobTkHT6XaEVOo8IO90Q4DOSxnm3Y151QxPJlM/vKC0bVy+d6cVWQZLlFiuZPP0wS6vZwSKeJgKkcS+KfMBlRw==}
|
||||
engines: {node: ^14.18.0 || >=16.0.0}
|
||||
peerDependencies:
|
||||
'@types/eslint': '>=8.0.0'
|
||||
@ -1378,8 +1378,8 @@ packages:
|
||||
eslint-config-prettier:
|
||||
optional: true
|
||||
|
||||
eslint-plugin-react-hooks@5.1.0:
|
||||
resolution: {integrity: sha512-mpJRtPgHN2tNAvZ35AMfqeB3Xqeo273QxrHJsbBEPWODRM4r0yB6jfoROqKEYrOn27UtRPpcpHc2UqyBSuUNTw==}
|
||||
eslint-plugin-react-hooks@5.2.0:
|
||||
resolution: {integrity: sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==}
|
||||
engines: {node: '>=10'}
|
||||
peerDependencies:
|
||||
eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0
|
||||
@ -1989,8 +1989,8 @@ packages:
|
||||
lru-cache@10.4.3:
|
||||
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
|
||||
|
||||
luxon@3.5.0:
|
||||
resolution: {integrity: sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==}
|
||||
luxon@3.6.1:
|
||||
resolution: {integrity: sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
math-intrinsics@1.1.0:
|
||||
@ -2262,8 +2262,8 @@ packages:
|
||||
vue-tsc:
|
||||
optional: true
|
||||
|
||||
prettier@3.5.2:
|
||||
resolution: {integrity: sha512-lc6npv5PH7hVqozBR7lkBNOGXV9vMwROAPlumdBkX0wTbbzPu/U1hk5yL8p2pt4Xoc+2mkT8t/sow2YrV/M5qg==}
|
||||
prettier@3.6.2:
|
||||
resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==}
|
||||
engines: {node: '>=14'}
|
||||
hasBin: true
|
||||
|
||||
@ -2322,6 +2322,22 @@ packages:
|
||||
react-native:
|
||||
optional: true
|
||||
|
||||
react-i18next@15.5.3:
|
||||
resolution: {integrity: sha512-ypYmOKOnjqPEJZO4m1BI0kS8kWqkBNsKYyhVUfij0gvjy9xJNoG/VcGkxq5dRlVwzmrmY1BQMAmpbbUBLwC4Kw==}
|
||||
peerDependencies:
|
||||
i18next: '>= 23.2.3'
|
||||
react: '>= 16.8.0'
|
||||
react-dom: '*'
|
||||
react-native: '*'
|
||||
typescript: ^5
|
||||
peerDependenciesMeta:
|
||||
react-dom:
|
||||
optional: true
|
||||
react-native:
|
||||
optional: true
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
react-icons@5.4.0:
|
||||
resolution: {integrity: sha512-7eltJxgVt7X64oHh6wSWNwwbKTCtMfK35hcjvJS0yxEAhPM8oUKdS3+kqaW1vicIltw+kR2unHaa12S9pPALoQ==}
|
||||
peerDependencies:
|
||||
@ -2632,18 +2648,18 @@ packages:
|
||||
peerDependencies:
|
||||
react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
|
||||
synckit@0.11.4:
|
||||
resolution: {integrity: sha512-Q/XQKRaJiLiFIBNN+mndW7S/RHxvwzuZS6ZwmRzUBqJBv/5QIKCEwkBC8GBf8EQJKYnaFs0wOZbKTXBPj8L9oQ==}
|
||||
synckit@0.11.8:
|
||||
resolution: {integrity: sha512-+XZ+r1XGIJGeQk3VvXhT6xx/VpbHsRzsTkGgF6E5RX9TTXD0118l87puaEBZ566FhqblC6U0d4XnubznJDm30A==}
|
||||
engines: {node: ^14.18.0 || >=16.0.0}
|
||||
|
||||
systeminformation@5.25.11:
|
||||
resolution: {integrity: sha512-jI01fn/t47rrLTQB0FTlMCC+5dYx8o0RRF+R4BPiUNsvg5OdY0s9DKMFmJGrx5SwMZQ4cag0Gl6v8oycso9b/g==}
|
||||
systeminformation@5.27.7:
|
||||
resolution: {integrity: sha512-saaqOoVEEFaux4v0K8Q7caiauRwjXC4XbD2eH60dxHXbpKxQ8kH9Rf7Jh+nryKpOUSEFxtCdBlSUx0/lO6rwRg==}
|
||||
engines: {node: '>=8.0.0'}
|
||||
os: [darwin, linux, win32, freebsd, openbsd, netbsd, sunos, android]
|
||||
hasBin: true
|
||||
|
||||
tailwind-scrollbar@4.0.1:
|
||||
resolution: {integrity: sha512-j2ZfUI7p8xmSQdlqaCxEb4Mha8ErvWjDVyu2Ke4IstWprQ/6TmIz1GSLE62vsTlXwnMLYhuvbFbIFzaJGOGtMg==}
|
||||
tailwind-scrollbar@4.0.2:
|
||||
resolution: {integrity: sha512-wAQiIxAPqk0MNTPptVe/xoyWi27y+NRGnTwvn4PQnbvB9kp8QUBiGl/wsfoVBHnQxTmhXJSNt9NHTmcz9EivFA==}
|
||||
engines: {node: '>=12.13.0'}
|
||||
peerDependencies:
|
||||
tailwindcss: 4.x
|
||||
@ -2897,7 +2913,7 @@ snapshots:
|
||||
dependencies:
|
||||
regenerator-runtime: 0.14.1
|
||||
|
||||
'@babel/runtime@7.27.1': {}
|
||||
'@babel/runtime@7.27.6': {}
|
||||
|
||||
'@balena/dockerignore@1.0.2': {}
|
||||
|
||||
@ -3181,7 +3197,7 @@ snapshots:
|
||||
'@pkgjs/parseargs@0.11.0':
|
||||
optional: true
|
||||
|
||||
'@pkgr/core@0.2.4': {}
|
||||
'@pkgr/core@0.2.7': {}
|
||||
|
||||
'@protobufjs/aspromise@1.1.2': {}
|
||||
|
||||
@ -3929,7 +3945,7 @@ snapshots:
|
||||
|
||||
dom-helpers@5.2.1:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.27.1
|
||||
'@babel/runtime': 7.27.6
|
||||
csstype: 3.1.3
|
||||
|
||||
dom-serializer@2.0.0:
|
||||
@ -4089,7 +4105,7 @@ snapshots:
|
||||
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.29.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.7.3))(eslint-import-resolver-typescript@3.10.0)(eslint@9.25.1(jiti@2.4.2))
|
||||
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.25.1(jiti@2.4.2))
|
||||
eslint-plugin-react: 7.37.4(eslint@9.25.1(jiti@2.4.2))
|
||||
eslint-plugin-react-hooks: 5.1.0(eslint@9.25.1(jiti@2.4.2))
|
||||
eslint-plugin-react-hooks: 5.2.0(eslint@9.25.1(jiti@2.4.2))
|
||||
optionalDependencies:
|
||||
typescript: 5.7.3
|
||||
transitivePeerDependencies:
|
||||
@ -4183,16 +4199,16 @@ snapshots:
|
||||
safe-regex-test: 1.1.0
|
||||
string.prototype.includes: 2.0.1
|
||||
|
||||
eslint-plugin-prettier@5.2.6(eslint-config-prettier@10.1.1(eslint@9.25.1(jiti@2.4.2)))(eslint@9.25.1(jiti@2.4.2))(prettier@3.5.2):
|
||||
eslint-plugin-prettier@5.5.1(eslint-config-prettier@10.1.1(eslint@9.25.1(jiti@2.4.2)))(eslint@9.25.1(jiti@2.4.2))(prettier@3.6.2):
|
||||
dependencies:
|
||||
eslint: 9.25.1(jiti@2.4.2)
|
||||
prettier: 3.5.2
|
||||
prettier: 3.6.2
|
||||
prettier-linter-helpers: 1.0.0
|
||||
synckit: 0.11.4
|
||||
synckit: 0.11.8
|
||||
optionalDependencies:
|
||||
eslint-config-prettier: 10.1.1(eslint@9.25.1(jiti@2.4.2))
|
||||
|
||||
eslint-plugin-react-hooks@5.1.0(eslint@9.25.1(jiti@2.4.2)):
|
||||
eslint-plugin-react-hooks@5.2.0(eslint@9.25.1(jiti@2.4.2)):
|
||||
dependencies:
|
||||
eslint: 9.25.1(jiti@2.4.2)
|
||||
|
||||
@ -4535,7 +4551,7 @@ snapshots:
|
||||
|
||||
i18next@21.10.0:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.27.0
|
||||
'@babel/runtime': 7.26.9
|
||||
|
||||
i18next@24.2.3(typescript@5.7.3):
|
||||
dependencies:
|
||||
@ -4842,7 +4858,7 @@ snapshots:
|
||||
|
||||
lru-cache@10.4.3: {}
|
||||
|
||||
luxon@3.5.0: {}
|
||||
luxon@3.6.1: {}
|
||||
|
||||
math-intrinsics@1.1.0: {}
|
||||
|
||||
@ -5089,12 +5105,12 @@ snapshots:
|
||||
dependencies:
|
||||
fast-diff: 1.3.0
|
||||
|
||||
prettier-plugin-organize-imports@4.1.0(prettier@3.5.2)(typescript@5.7.3):
|
||||
prettier-plugin-organize-imports@4.1.0(prettier@3.6.2)(typescript@5.7.3):
|
||||
dependencies:
|
||||
prettier: 3.5.2
|
||||
prettier: 3.6.2
|
||||
typescript: 5.7.3
|
||||
|
||||
prettier@3.5.2: {}
|
||||
prettier@3.6.2: {}
|
||||
|
||||
pretty-bytes@6.1.1: {}
|
||||
|
||||
@ -5160,14 +5176,15 @@ snapshots:
|
||||
optionalDependencies:
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
|
||||
react-i18next@11.18.6(i18next@24.2.3(typescript@5.7.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
react-i18next@15.5.3(i18next@24.2.3(typescript@5.7.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.3):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.26.9
|
||||
'@babel/runtime': 7.27.6
|
||||
html-parse-stringify: 3.0.1
|
||||
i18next: 24.2.3(typescript@5.7.3)
|
||||
react: 18.3.1
|
||||
optionalDependencies:
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
typescript: 5.7.3
|
||||
|
||||
react-icons@5.4.0(react@18.3.1):
|
||||
dependencies:
|
||||
@ -5187,7 +5204,7 @@ snapshots:
|
||||
|
||||
react-transition-group@4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.27.1
|
||||
'@babel/runtime': 7.27.6
|
||||
dom-helpers: 5.2.1
|
||||
loose-envify: 1.4.0
|
||||
prop-types: 15.8.1
|
||||
@ -5559,14 +5576,13 @@ snapshots:
|
||||
react: 18.3.1
|
||||
use-sync-external-store: 1.5.0(react@18.3.1)
|
||||
|
||||
synckit@0.11.4:
|
||||
synckit@0.11.8:
|
||||
dependencies:
|
||||
'@pkgr/core': 0.2.4
|
||||
tslib: 2.8.1
|
||||
'@pkgr/core': 0.2.7
|
||||
|
||||
systeminformation@5.25.11: {}
|
||||
systeminformation@5.27.7: {}
|
||||
|
||||
tailwind-scrollbar@4.0.1(react@18.3.1)(tailwindcss@4.0.9):
|
||||
tailwind-scrollbar@4.0.2(react@18.3.1)(tailwindcss@4.0.9):
|
||||
dependencies:
|
||||
prism-react-renderer: 2.4.1(react@18.3.1)
|
||||
tailwindcss: 4.0.9
|
||||
|
@ -359,6 +359,12 @@
|
||||
"services": "Services",
|
||||
"middleware": "Middleware"
|
||||
},
|
||||
"trilium": {
|
||||
"version": "Version",
|
||||
"notesCount": "Notes",
|
||||
"dbSize": "Database Size",
|
||||
"unknown": "Unknown"
|
||||
},
|
||||
"navidrome": {
|
||||
"nothing_streaming": "No Active Streams",
|
||||
"please_wait": "Please Wait"
|
||||
@ -1054,5 +1060,16 @@
|
||||
"checkmk": {
|
||||
"serviceErrors": "Service issues",
|
||||
"hostErrors": "Host issues"
|
||||
},
|
||||
"komodo": {
|
||||
"total": "Total",
|
||||
"running": "Running",
|
||||
"stopped": "Stopped",
|
||||
"down": "Down",
|
||||
"unhealthy": "Unhealthy",
|
||||
"unknown": "Unknown",
|
||||
"servers": "Servers",
|
||||
"stacks": "Stacks",
|
||||
"containers": "Containers"
|
||||
}
|
||||
}
|
||||
|
@ -4,9 +4,11 @@ import { useContext, useState } from "react";
|
||||
import { SettingsContext } from "utils/contexts/settings";
|
||||
import Docker from "widgets/docker/component";
|
||||
import Kubernetes from "widgets/kubernetes/component";
|
||||
import ProxmoxVM from "widgets/proxmoxvm/component";
|
||||
|
||||
import KubernetesStatus from "./kubernetes-status";
|
||||
import Ping from "./ping";
|
||||
import ProxmoxStatus from "./proxmox-status";
|
||||
import SiteMonitor from "./site-monitor";
|
||||
import Status from "./status";
|
||||
import Widget from "./widget";
|
||||
@ -121,6 +123,16 @@ export default function Item({ service, groupName, useEqualHeights }) {
|
||||
<span className="sr-only">View container stats</span>
|
||||
</button>
|
||||
)}
|
||||
{service.proxmoxNode && service.proxmoxVMID && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => (statsOpen ? closeStats() : setStatsOpen(true))}
|
||||
className="shrink-0 flex items-center justify-center cursor-pointer service-tag service-proxmoxstatus"
|
||||
>
|
||||
<ProxmoxStatus service={service} style={statusStyle} />
|
||||
<span className="sr-only">View Proxmox stats</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -152,6 +164,26 @@ export default function Item({ service, groupName, useEqualHeights }) {
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{service.proxmoxNode && service.proxmoxVMID && (
|
||||
<div
|
||||
className={classNames(
|
||||
showStats || (statsOpen && !statsClosing) ? "max-h-[110px] opacity-100" : " max-h-0 opacity-0",
|
||||
"w-full overflow-hidden transition-all duration-300 ease-in-out service-stats",
|
||||
)}
|
||||
>
|
||||
{(showStats || statsOpen) && (
|
||||
<ProxmoxVM
|
||||
service={{
|
||||
widget: {
|
||||
node: service.proxmoxNode,
|
||||
vmid: service.proxmoxVMID,
|
||||
type: service.proxmoxType,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{service.widgets.map((widget) => (
|
||||
<Widget widget={widget} service={service} key={widget.index} />
|
||||
|
65
src/components/services/proxmox-status.jsx
Normal file
65
src/components/services/proxmox-status.jsx
Normal file
@ -0,0 +1,65 @@
|
||||
import { useTranslation } from "next-i18next";
|
||||
import useSWR from "swr";
|
||||
|
||||
export default function ProxmoxStatus({ service, style }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const vmType = service.proxmoxType || "qemu";
|
||||
const apiUrl = `/api/proxmox/stats/${service.proxmoxNode}/${service.proxmoxVMID}?type=${vmType}`;
|
||||
|
||||
const { data, error } = useSWR(apiUrl);
|
||||
|
||||
let statusLabel = t("docker.unknown");
|
||||
let backgroundClass = "px-1.5 py-0.5 bg-theme-500/10 dark:bg-theme-900/50";
|
||||
let colorClass = "text-black/20 dark:text-white/40 ";
|
||||
|
||||
if (error) {
|
||||
statusLabel = t("docker.error");
|
||||
colorClass = "text-rose-500/80";
|
||||
} else if (data) {
|
||||
if (data.status === "running") {
|
||||
statusLabel = t("docker.running");
|
||||
colorClass = "text-emerald-500/80";
|
||||
}
|
||||
|
||||
if (data.status === "stopped") {
|
||||
statusLabel = t("docker.exited");
|
||||
colorClass = "text-orange-400/50 dark:text-orange-400/80";
|
||||
}
|
||||
|
||||
if (data.status === "paused") {
|
||||
statusLabel = "paused";
|
||||
colorClass = "text-blue-500/80";
|
||||
}
|
||||
|
||||
if (data.status === "offline") {
|
||||
statusLabel = "offline";
|
||||
colorClass = "text-orange-400/50 dark:text-orange-400/80";
|
||||
}
|
||||
|
||||
if (data.status === "not found") {
|
||||
statusLabel = t("docker.not_found");
|
||||
colorClass = "text-orange-400/50 dark:text-orange-400/80";
|
||||
}
|
||||
}
|
||||
|
||||
if (style === "dot") {
|
||||
colorClass = colorClass.replace(/text-/g, "bg-").replace(/\/\d\d/g, "");
|
||||
backgroundClass = "p-4 hover:bg-theme-500/10 dark:hover:bg-theme-900/20";
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`w-auto text-center overflow-hidden ${backgroundClass} rounded-b-[3px] proxmoxstatus proxmoxstatus-${statusLabel
|
||||
.toLowerCase()
|
||||
.replace(" ", "-")}`}
|
||||
title={statusLabel}
|
||||
>
|
||||
{style !== "dot" ? (
|
||||
<div className={`text-[8px] font-bold ${colorClass} uppercase`}>{statusLabel}</div>
|
||||
) : (
|
||||
<div className={`rounded-full h-3 w-3 ${colorClass}`} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -55,7 +55,7 @@ export default function ColorToggle() {
|
||||
leaveFrom="opacity-100 translate-y-0"
|
||||
leaveTo="opacity-0 translate-y-1"
|
||||
>
|
||||
<Popover.Panel className="absolute -top-[75px] left-0">
|
||||
<Popover.Panel className="absolute -top-[75px] left-0 z-10">
|
||||
<div className="rounded-md shadow-lg ring-1 ring-black ring-opacity-5 w-[85vw] sm:w-full">
|
||||
<div className="relative grid gap-2 p-2 grid-cols-11 bg-white/50 dark:bg-white/10 shadow-black/10 dark:shadow-black/20 rounded-md shadow-md">
|
||||
{colors.map((color) => (
|
||||
|
65
src/pages/api/proxmox/stats/[...service].js
Normal file
65
src/pages/api/proxmox/stats/[...service].js
Normal file
@ -0,0 +1,65 @@
|
||||
import { getProxmoxConfig } from "utils/config/proxmox";
|
||||
import createLogger from "utils/logger";
|
||||
import { httpProxy } from "utils/proxy/http";
|
||||
|
||||
const logger = createLogger("proxmoxStatsService");
|
||||
|
||||
export default async function handler(req, res) {
|
||||
const { service, type: vmType } = req.query;
|
||||
|
||||
const [node, vmid] = service;
|
||||
|
||||
if (!node) {
|
||||
return res.status(400).send({
|
||||
error: "Proxmox node parameter is required",
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const proxmoxConfig = getProxmoxConfig();
|
||||
|
||||
if (!proxmoxConfig) {
|
||||
return res.status(500).send({
|
||||
error: "Proxmox server configuration not found",
|
||||
});
|
||||
}
|
||||
|
||||
const baseUrl = `${proxmoxConfig.url}/api2/json`;
|
||||
const headers = {
|
||||
Authorization: `PVEAPIToken=${proxmoxConfig.token}=${proxmoxConfig.secret}`,
|
||||
};
|
||||
|
||||
const statusUrl = `${baseUrl}/nodes/${node}/${vmType}/${vmid}/status/current`;
|
||||
|
||||
const [status, , data] = await httpProxy(statusUrl, {
|
||||
method: "GET",
|
||||
headers,
|
||||
});
|
||||
|
||||
if (status !== 200) {
|
||||
logger.error("HTTP Error %d calling Proxmox API", status);
|
||||
return res.status(status).send({
|
||||
error: `Failed to fetch Proxmox ${vmType} status`,
|
||||
});
|
||||
}
|
||||
|
||||
let parsedData = JSON.parse(Buffer.from(data).toString());
|
||||
|
||||
if (!parsedData || !parsedData.data) {
|
||||
return res.status(500).send({
|
||||
error: "Invalid response from Proxmox API",
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).json({
|
||||
status: parsedData.data.status || "unknown",
|
||||
cpu: parsedData.data.cpu,
|
||||
mem: parsedData.data.mem,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Error fetching Proxmox status:", error);
|
||||
return res.status(500).send({
|
||||
error: "Failed to fetch Proxmox status",
|
||||
});
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import checkAndCopyConfig from "utils/config/config";
|
||||
|
||||
const configs = ["docker.yaml", "settings.yaml", "services.yaml", "bookmarks.yaml"];
|
||||
const configs = ["docker.yaml", "settings.yaml", "services.yaml", "bookmarks.yaml", "kubernetes.yaml", "proxmox.yaml"];
|
||||
|
||||
export default async function handler(req, res) {
|
||||
const errors = configs.map((config) => checkAndCopyConfig(config)).filter((status) => status !== true);
|
||||
|
@ -67,7 +67,7 @@ export default async function handler(req, res) {
|
||||
nodeMap[nodeMetric.metadata.name].memory.percent = (mem / nodeMap[nodeMetric.metadata.name].memory.total) * 100;
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Error getting metrics, ensure you have metrics-server installed: s", JSON.stringify(error));
|
||||
logger.error("Error getting metrics, ensure you have metrics-server installed:", JSON.stringify(error));
|
||||
return res.status(500).send({
|
||||
error: "Error getting metrics, check logs for more details",
|
||||
});
|
||||
|
4
src/skeleton/proxmox.yaml
Normal file
4
src/skeleton/proxmox.yaml
Normal file
@ -0,0 +1,4 @@
|
||||
---
|
||||
# url: https://proxmox.host.or.ip:8006
|
||||
# token: username@pam!Token ID
|
||||
# secret: secret
|
14
src/utils/config/proxmox.js
Normal file
14
src/utils/config/proxmox.js
Normal file
@ -0,0 +1,14 @@
|
||||
import { readFileSync } from "fs";
|
||||
import path from "path";
|
||||
|
||||
import yaml from "js-yaml";
|
||||
|
||||
import checkAndCopyConfig, { CONF_DIR, substituteEnvironmentVars } from "utils/config/config";
|
||||
|
||||
export function getProxmoxConfig() {
|
||||
checkAndCopyConfig("proxmox.yaml");
|
||||
const configFile = path.join(CONF_DIR, "proxmox.yaml");
|
||||
const rawConfigData = readFileSync(configFile, "utf8");
|
||||
const configData = substituteEnvironmentVars(rawConfigData);
|
||||
return yaml.load(configData);
|
||||
}
|
@ -295,6 +295,7 @@ export function cleanServiceGroups(groups) {
|
||||
// emby, jellyfin
|
||||
enableBlocks,
|
||||
enableNowPlaying,
|
||||
enableMediaControl,
|
||||
|
||||
// emby, jellyfin, tautulli
|
||||
enableUser,
|
||||
@ -337,6 +338,10 @@ export function cleanServiceGroups(groups) {
|
||||
// jellystat
|
||||
days,
|
||||
|
||||
// komodo
|
||||
showSummary,
|
||||
showStacks,
|
||||
|
||||
// kopia
|
||||
snapshotHost,
|
||||
snapshotPath,
|
||||
@ -362,6 +367,9 @@ export function cleanServiceGroups(groups) {
|
||||
// opnsense, pfsense
|
||||
wan,
|
||||
|
||||
// portainer
|
||||
kubernetes,
|
||||
|
||||
// prometheusmetric
|
||||
metrics,
|
||||
|
||||
@ -399,6 +407,9 @@ export function cleanServiceGroups(groups) {
|
||||
|
||||
// spoolman
|
||||
spoolIds,
|
||||
|
||||
// grafana
|
||||
alerts,
|
||||
} = widgetData;
|
||||
|
||||
let fieldsList = fields;
|
||||
@ -443,12 +454,19 @@ export function cleanServiceGroups(groups) {
|
||||
if (type === "unifi") {
|
||||
if (site) widget.site = site;
|
||||
}
|
||||
if (type === "portainer") {
|
||||
if (kubernetes) widget.kubernetes = !!JSON.parse(kubernetes);
|
||||
}
|
||||
if (type === "proxmox") {
|
||||
if (node) widget.node = node;
|
||||
}
|
||||
if (type === "proxmoxbackupserver") {
|
||||
if (datastore) widget.datastore = datastore;
|
||||
}
|
||||
if (type === "komodo") {
|
||||
if (showSummary !== undefined) widget.showSummary = !!JSON.parse(showSummary);
|
||||
if (showStacks !== undefined) widget.showStacks = !!JSON.parse(showStacks);
|
||||
}
|
||||
if (type === "kubernetes") {
|
||||
if (namespace) widget.namespace = namespace;
|
||||
if (app) widget.app = app;
|
||||
@ -471,6 +489,7 @@ export function cleanServiceGroups(groups) {
|
||||
if (wan) widget.wan = wan;
|
||||
}
|
||||
if (["emby", "jellyfin"].includes(type)) {
|
||||
if (enableMediaControl !== undefined) widget.enableMediaControl = !!JSON.parse(enableMediaControl);
|
||||
if (enableBlocks !== undefined) widget.enableBlocks = JSON.parse(enableBlocks);
|
||||
if (enableNowPlaying !== undefined) widget.enableNowPlaying = JSON.parse(enableNowPlaying);
|
||||
}
|
||||
@ -498,7 +517,18 @@ export function cleanServiceGroups(groups) {
|
||||
if (snapshotPath) widget.snapshotPath = snapshotPath;
|
||||
}
|
||||
if (
|
||||
["beszel", "glances", "immich", "komga", "mealie", "pfsense", "pihole", "speedtest", "wgeasy"].includes(type)
|
||||
[
|
||||
"beszel",
|
||||
"glances",
|
||||
"immich",
|
||||
"komga",
|
||||
"mealie",
|
||||
"pfsense",
|
||||
"pihole",
|
||||
"speedtest",
|
||||
"wgeasy",
|
||||
"grafana",
|
||||
].includes(type)
|
||||
) {
|
||||
if (version) widget.version = parseInt(version, 10);
|
||||
}
|
||||
@ -577,6 +607,9 @@ export function cleanServiceGroups(groups) {
|
||||
if (type === "jellystat") {
|
||||
if (days !== undefined) widget.days = parseInt(days, 10);
|
||||
}
|
||||
if (type === "grafana") {
|
||||
if (alerts) widget.alerts = alerts;
|
||||
}
|
||||
return widget;
|
||||
});
|
||||
return cleanedService;
|
||||
|
@ -106,6 +106,8 @@ export default async function credentialedProxyHandler(req, res, map) {
|
||||
} else {
|
||||
headers.Authorization = widget.password;
|
||||
}
|
||||
} else if (widget.type === "trilium") {
|
||||
headers.Authorization = widget.key;
|
||||
} else if (widget.type === "gitlab") {
|
||||
headers["PRIVATE-TOKEN"] = widget.key;
|
||||
} else if (widget.type === "speedtest") {
|
||||
|
@ -19,9 +19,11 @@ export default async function genericProxyHandler(req, res, map) {
|
||||
|
||||
if (widget) {
|
||||
// if there are more than one question marks, replace others to &
|
||||
const url = new URL(
|
||||
formatApiCall(widgets[widget.type].api, { endpoint, ...widget }).replace(/(?<=\?.*)\?/g, "&"),
|
||||
);
|
||||
let urlString = formatApiCall(widgets[widget.type].api, { endpoint, ...widget }).replace(/(?<=\?.*)\?/g, "&");
|
||||
if (widget.type === "customapi" && widget.url?.endsWith("/")) {
|
||||
urlString += "/"; // Ensure we dont lose the trailing slash for custom API calls
|
||||
}
|
||||
const url = new URL(urlString);
|
||||
|
||||
const headers = req.extraHeaders ?? widget.headers ?? widgets[widget.type].headers ?? {};
|
||||
|
||||
|
@ -8,13 +8,8 @@ export default function Event({ event, colorVariants, showDate = false, showTime
|
||||
const [hover, setHover] = useState(false);
|
||||
const { i18n } = useTranslation();
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex flex-row text-theme-700 dark:text-theme-200 items-center text-xs relative h-5 w-full rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1"
|
||||
onMouseEnter={() => setHover(!hover)}
|
||||
onMouseLeave={() => setHover(!hover)}
|
||||
key={`event-${event.title}-${event.date}-${event.additional}`}
|
||||
>
|
||||
const children = (
|
||||
<>
|
||||
{showDateColumn && (
|
||||
<span className="ml-2 w-12">
|
||||
<span>
|
||||
@ -36,6 +31,26 @@ export default function Event({ event, colorVariants, showDate = false, showTime
|
||||
<IoMdCheckmarkCircleOutline />
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
const className =
|
||||
"flex flex-row text-theme-700 dark:text-theme-200 items-center text-xs relative h-5 w-full rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1";
|
||||
const key = `event-${event.title}-${event.date}-${event.additional}`;
|
||||
return event.url ? (
|
||||
<a
|
||||
className={classNames(className, "hover:bg-theme-300/50 dark:hover:bg-theme-800/20")}
|
||||
onMouseEnter={() => setHover(!hover)}
|
||||
onMouseLeave={() => setHover(!hover)}
|
||||
key={key}
|
||||
href={event.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
) : (
|
||||
<div className={className} onMouseEnter={() => setHover(!hover)} onMouseLeave={() => setHover(!hover)} key={key}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -54,6 +54,7 @@ export default function Integration({ config, params, setEvents, hideErrors, tim
|
||||
ICAL.Time.now(), // handles events without a date
|
||||
location: event.getFirstPropertyValue("location"),
|
||||
status: event.getFirstPropertyValue("status"),
|
||||
url: event.getFirstPropertyValue("url"),
|
||||
};
|
||||
};
|
||||
|
||||
@ -133,6 +134,7 @@ export default function Integration({ config, params, setEvents, hideErrors, tim
|
||||
isCompleted: getIsCompleted(),
|
||||
additional: event.location,
|
||||
type: "ical",
|
||||
url: event.url,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
@ -22,6 +22,7 @@ export default function Integration({ config, params, setEvents, hideErrors = fa
|
||||
const cinemaTitle = `${event.title} - ${t("calendar.inCinemas")}`;
|
||||
const physicalTitle = `${event.title} - ${t("calendar.physicalRelease")}`;
|
||||
const digitalTitle = `${event.title} - ${t("calendar.digitalRelease")}`;
|
||||
const url = config?.baseUrl && event.titleSlug && `${config.baseUrl}/movie/${event.titleSlug}`;
|
||||
|
||||
if (event.inCinemas) {
|
||||
eventsToAdd[cinemaTitle] = {
|
||||
@ -30,6 +31,7 @@ export default function Integration({ config, params, setEvents, hideErrors = fa
|
||||
color: config?.color ?? "amber",
|
||||
isCompleted: event.hasFile,
|
||||
additional: "",
|
||||
url,
|
||||
};
|
||||
}
|
||||
|
||||
@ -40,6 +42,7 @@ export default function Integration({ config, params, setEvents, hideErrors = fa
|
||||
color: config?.color ?? "cyan",
|
||||
isCompleted: event.hasFile,
|
||||
additional: "",
|
||||
url,
|
||||
};
|
||||
}
|
||||
|
||||
@ -50,6 +53,7 @@ export default function Integration({ config, params, setEvents, hideErrors = fa
|
||||
color: config?.color ?? "emerald",
|
||||
isCompleted: event.hasFile,
|
||||
additional: "",
|
||||
url,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
@ -29,6 +29,7 @@ export default function Integration({ config, params, setEvents, hideErrors = fa
|
||||
color: config?.color ?? "teal",
|
||||
isCompleted: event.hasFile,
|
||||
additional: `S${event.seasonNumber} E${event.episodeNumber}`,
|
||||
url: config?.baseUrl && event.series.titleSlug && `${config.baseUrl}/series/${event.series.titleSlug}`,
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -16,7 +16,14 @@ export default async function calendarProxyHandler(req, res) {
|
||||
return res.status(403).json({ error: "No integration URL specified" });
|
||||
}
|
||||
|
||||
const [status, contentType, data] = await httpProxy(integration.url);
|
||||
const options = {};
|
||||
if (integration.url?.includes("outlook")) {
|
||||
// Outlook requires a user agent header
|
||||
options.headers = {
|
||||
"User-Agent": `gethomepage/${process.env.NEXT_PUBLIC_VERSION || "dev"}`,
|
||||
};
|
||||
}
|
||||
const [status, contentType, data] = await httpProxy(integration.url, options);
|
||||
|
||||
if (contentType) res.setHeader("Content-Type", contentType);
|
||||
|
||||
|
@ -63,6 +63,7 @@ const components = {
|
||||
jellystat: dynamic(() => import("./jellystat/component")),
|
||||
kavita: dynamic(() => import("./kavita/component")),
|
||||
komga: dynamic(() => import("./komga/component")),
|
||||
komodo: dynamic(() => import("./komodo/component")),
|
||||
kopia: dynamic(() => import("./kopia/component")),
|
||||
lidarr: dynamic(() => import("./lidarr/component")),
|
||||
linkwarden: dynamic(() => import("./linkwarden/component")),
|
||||
@ -132,6 +133,7 @@ const components = {
|
||||
tdarr: dynamic(() => import("./tdarr/component")),
|
||||
traefik: dynamic(() => import("./traefik/component")),
|
||||
transmission: dynamic(() => import("./transmission/component")),
|
||||
trilium: dynamic(() => import("./trilium/component")),
|
||||
tubearchivist: dynamic(() => import("./tubearchivist/component")),
|
||||
truenas: dynamic(() => import("./truenas/component")),
|
||||
unifi: dynamic(() => import("./unifi/component")),
|
||||
|
@ -45,7 +45,7 @@ function generateStreamTitle(session, enableUser, showEpisodeNumber) {
|
||||
return enableUser ? `${streamTitle} (${UserName})` : streamTitle;
|
||||
}
|
||||
|
||||
function SingleSessionEntry({ playCommand, session, enableUser, showEpisodeNumber }) {
|
||||
function SingleSessionEntry({ playCommand, session, enableUser, showEpisodeNumber, enableMediaControl }) {
|
||||
const {
|
||||
PlayState: { PositionTicks, IsPaused, IsMuted },
|
||||
} = session;
|
||||
@ -85,7 +85,7 @@ function SingleSessionEntry({ playCommand, session, enableUser, showEpisodeNumbe
|
||||
}}
|
||||
/>
|
||||
<div className="text-xs z-10 self-center ml-1">
|
||||
{IsPaused && (
|
||||
{enableMediaControl && IsPaused && (
|
||||
<BsFillPlayFill
|
||||
onClick={() => {
|
||||
playCommand(session, "Unpause");
|
||||
@ -93,7 +93,7 @@ function SingleSessionEntry({ playCommand, session, enableUser, showEpisodeNumbe
|
||||
className="inline-block w-4 h-4 cursor-pointer -mt-[1px] mr-1 opacity-80"
|
||||
/>
|
||||
)}
|
||||
{!IsPaused && (
|
||||
{enableMediaControl && !IsPaused && (
|
||||
<BsPauseFill
|
||||
onClick={() => {
|
||||
playCommand(session, "Pause");
|
||||
@ -114,7 +114,7 @@ function SingleSessionEntry({ playCommand, session, enableUser, showEpisodeNumbe
|
||||
);
|
||||
}
|
||||
|
||||
function SessionEntry({ playCommand, session, enableUser, showEpisodeNumber }) {
|
||||
function SessionEntry({ playCommand, session, enableUser, showEpisodeNumber, enableMediaControl }) {
|
||||
const {
|
||||
PlayState: { PositionTicks, IsPaused, IsMuted },
|
||||
} = session;
|
||||
@ -139,7 +139,7 @@ function SessionEntry({ playCommand, session, enableUser, showEpisodeNumber }) {
|
||||
}}
|
||||
/>
|
||||
<div className="text-xs z-10 self-center ml-1">
|
||||
{IsPaused && (
|
||||
{enableMediaControl && IsPaused && (
|
||||
<BsFillPlayFill
|
||||
onClick={() => {
|
||||
playCommand(session, "Unpause");
|
||||
@ -147,7 +147,7 @@ function SessionEntry({ playCommand, session, enableUser, showEpisodeNumber }) {
|
||||
className="inline-block w-4 h-4 cursor-pointer -mt-[1px] mr-1 opacity-80"
|
||||
/>
|
||||
)}
|
||||
{!IsPaused && (
|
||||
{enableMediaControl && !IsPaused && (
|
||||
<BsPauseFill
|
||||
onClick={() => {
|
||||
playCommand(session, "Pause");
|
||||
@ -238,6 +238,7 @@ export default function Component({ service }) {
|
||||
|
||||
const enableBlocks = service.widget?.enableBlocks;
|
||||
const enableNowPlaying = service.widget?.enableNowPlaying ?? true;
|
||||
const enableMediaControl = service.widget?.enableMediaControl !== false; // default is true
|
||||
const enableUser = !!service.widget?.enableUser; // default is false
|
||||
const expandOneStreamToTwoRows = service.widget?.expandOneStreamToTwoRows !== false; // default is true
|
||||
const showEpisodeNumber = !!service.widget?.showEpisodeNumber; // default is false
|
||||
@ -304,6 +305,7 @@ export default function Component({ service }) {
|
||||
session={session}
|
||||
enableUser={enableUser}
|
||||
showEpisodeNumber={showEpisodeNumber}
|
||||
enableMediaControl={enableMediaControl}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
@ -321,6 +323,7 @@ export default function Component({ service }) {
|
||||
session={session}
|
||||
enableUser={enableUser}
|
||||
showEpisodeNumber={showEpisodeNumber}
|
||||
enableMediaControl={enableMediaControl}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
@ -31,7 +31,8 @@ function CPU({ quicklookData, className = "" }) {
|
||||
|
||||
return (
|
||||
quicklookData &&
|
||||
quicklookData.cpu && (
|
||||
quicklookData.cpu !== undefined &&
|
||||
quicklookData.cpu !== null && (
|
||||
<div className="text-xs flex place-content-between">
|
||||
<div className={className}>{t("glances.cpu")}</div>
|
||||
<div className={className}>
|
||||
@ -109,10 +110,10 @@ export default function Component({ service }) {
|
||||
return (
|
||||
<Container chart={chart}>
|
||||
{chart && (
|
||||
<div className="bg-linear-to-br from-theme-500/30 via-theme-600/20 to-theme-700/10 absolute -top-10 -left-2 -right-2 -bottom-2 h-[calc(100%+3em)] w-[calc(100%+1em)]" />
|
||||
<div className="bg-linear-to-br from-theme-500/30 via-theme-600/20 to-theme-700/10 absolute -top-20 -left-2 -right-2 -bottom-2" />
|
||||
)}
|
||||
|
||||
<Block position={chart ? "-top-6 right-2" : "top-3 right-3"}>
|
||||
<Block position={chart ? "-top-6 right-2" : "top-3 right-2"}>
|
||||
{quicklookData && quicklookData.cpu_name && chart && (
|
||||
<div className="text-[0.6rem] opacity-50">{quicklookData.cpu_name}</div>
|
||||
)}
|
||||
@ -124,7 +125,7 @@ export default function Component({ service }) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="w-[4rem]">{!chart && <Swap quicklookData={quicklookData} className="opacity-25" />}</div>
|
||||
<div>{!chart && <Swap quicklookData={quicklookData} className="opacity-25 ml-2" />}</div>
|
||||
</Block>
|
||||
|
||||
{chart && (
|
||||
@ -136,18 +137,18 @@ export default function Component({ service }) {
|
||||
)}
|
||||
|
||||
{!chart && (
|
||||
<Block position="bottom-3 left-3 w-[4rem]">
|
||||
<CPU quicklookData={quicklookData} className="opacity-75" />
|
||||
<Block position="bottom-3 left-3">
|
||||
<CPU quicklookData={quicklookData} className="opacity-75 mr-2" />
|
||||
</Block>
|
||||
)}
|
||||
|
||||
<Block position="bottom-3 right-2 w-[4rem]">
|
||||
{chart && <CPU quicklookData={quicklookData} className="opacity-50" />}
|
||||
<Block position="bottom-3 right-2">
|
||||
{chart && <CPU quicklookData={quicklookData} className="opacity-50 ml-2" />}
|
||||
|
||||
{chart && <Mem quicklookData={quicklookData} className="opacity-50" />}
|
||||
{!chart && <Mem quicklookData={quicklookData} className="opacity-75" />}
|
||||
{chart && <Mem quicklookData={quicklookData} className="opacity-50 ml-2" />}
|
||||
{!chart && <Mem quicklookData={quicklookData} className="opacity-75 ml-2" />}
|
||||
|
||||
{chart && <Swap quicklookData={quicklookData} className="opacity-50" />}
|
||||
{chart && <Swap quicklookData={quicklookData} className="opacity-50 ml-2" />}
|
||||
</Block>
|
||||
</Container>
|
||||
);
|
||||
|
@ -23,7 +23,7 @@ export default function Component({ service }) {
|
||||
const { widget } = service;
|
||||
const { chart, refreshInterval = defaultInterval, version = 3 } = widget;
|
||||
|
||||
const memoryInfoKey = version === 3 ? 0 : "data";
|
||||
const memoryInfoKey = version === 3 ? 0 : "rss";
|
||||
|
||||
const { data, error } = useWidgetAPI(service.widget, `${version}/processlist`, {
|
||||
refreshInterval: Math.max(defaultInterval, refreshInterval),
|
||||
@ -69,7 +69,7 @@ export default function Component({ service }) {
|
||||
<div className="opacity-25 w-14 text-right">{item.cpu_percent.toFixed(1)}%</div>
|
||||
<div className="opacity-25 w-14 text-right">
|
||||
{t("common.bytes", {
|
||||
value: item.memory_info[memoryInfoKey] ?? item.memory_info.wset,
|
||||
value: item.memory_info[memoryInfoKey] ?? item.memory_info.data ?? item.memory_info.wset,
|
||||
maximumFractionDigits: 0,
|
||||
})}
|
||||
</div>
|
||||
|
@ -6,27 +6,51 @@ import useWidgetAPI from "utils/proxy/use-widget-api";
|
||||
|
||||
export default function Component({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { widget } = service;
|
||||
|
||||
const { version = 1, alerts = "grafana" } = widget;
|
||||
|
||||
const { data: statsData, error: statsError } = useWidgetAPI(widget, "stats");
|
||||
const { data: alertsData, error: alertsError } = useWidgetAPI(widget, "alerts");
|
||||
const { data: alertmanagerData, error: alertmanagerError } = useWidgetAPI(widget, "alertmanager");
|
||||
|
||||
let alertsInt = 0;
|
||||
|
||||
if (alertsError || !alertsData || alertsData.length === 0) {
|
||||
if (alertmanagerData) {
|
||||
alertsInt = alertmanagerData.length;
|
||||
}
|
||||
} else {
|
||||
alertsInt = alertsData.filter((a) => a.state === "alerting").length;
|
||||
let primaryAlertsEndpoint = "alerts";
|
||||
let secondaryAlertsEndpoint = "grafana";
|
||||
if (version === 2) {
|
||||
primaryAlertsEndpoint = alerts;
|
||||
secondaryAlertsEndpoint = "";
|
||||
}
|
||||
|
||||
if (statsError || (alertsError && alertmanagerError)) {
|
||||
const { data: primaryAlertsData, error: primaryAlertsError } = useWidgetAPI(widget, primaryAlertsEndpoint);
|
||||
const { data: secondaryAlertsData, error: secondaryAlertsError } = useWidgetAPI(widget, secondaryAlertsEndpoint);
|
||||
|
||||
let alertsInt = 0;
|
||||
let alertsError = null;
|
||||
if (version === 1) {
|
||||
if (primaryAlertsError || !primaryAlertsData || primaryAlertsData.length === 0) {
|
||||
if (secondaryAlertsData) {
|
||||
alertsInt = secondaryAlertsData.length;
|
||||
}
|
||||
} else {
|
||||
alertsInt = primaryAlertsData.filter((a) => a.state === "alerting").length;
|
||||
}
|
||||
|
||||
if (primaryAlertsError && secondaryAlertsError) {
|
||||
alertsError = primaryAlertsError ?? secondaryAlertsError;
|
||||
}
|
||||
} else if (version === 2) {
|
||||
if (primaryAlertsData) {
|
||||
alertsInt = primaryAlertsData.length;
|
||||
}
|
||||
|
||||
if (primaryAlertsError) {
|
||||
alertsError = primaryAlertsError;
|
||||
}
|
||||
}
|
||||
|
||||
if (statsError || alertsError) {
|
||||
return <Container service={service} error={statsError ?? alertsError} />;
|
||||
}
|
||||
|
||||
if (!statsData || (!alertsData && !alertmanagerData)) {
|
||||
if (!statsData) {
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="grafana.dashboards" />
|
||||
|
@ -9,6 +9,9 @@ const widget = {
|
||||
endpoint: "alerts",
|
||||
},
|
||||
alertmanager: {
|
||||
endpoint: "alertmanager/alertmanager/api/v2/alerts",
|
||||
},
|
||||
grafana: {
|
||||
endpoint: "alertmanager/grafana/api/v2/alerts",
|
||||
},
|
||||
stats: {
|
||||
|
84
src/widgets/komodo/component.jsx
Normal file
84
src/widgets/komodo/component.jsx
Normal file
@ -0,0 +1,84 @@
|
||||
import Block from "components/services/widget/block";
|
||||
import Container from "components/services/widget/container";
|
||||
import { useTranslation } from "next-i18next";
|
||||
|
||||
import useWidgetAPI from "utils/proxy/use-widget-api";
|
||||
|
||||
const MAX_ALLOWED_FIELDS = 4;
|
||||
|
||||
export default function Component({ service }) {
|
||||
const { t } = useTranslation();
|
||||
const { widget } = service;
|
||||
const containersEndpoint = !(!widget.showSummary && widget.showStacks) ? "containers" : "";
|
||||
const { data: containersData, error: containersError } = useWidgetAPI(widget, containersEndpoint);
|
||||
const stacksEndpoint = widget.showSummary || widget.showStacks ? "stacks" : "";
|
||||
const { data: stacksData, error: stacksError } = useWidgetAPI(widget, stacksEndpoint);
|
||||
const serversEndpoint = widget.showSummary ? "servers" : "";
|
||||
const { data: serversData, error: serversError } = useWidgetAPI(widget, serversEndpoint);
|
||||
|
||||
if (containersError || stacksError || serversError) {
|
||||
return <Container service={service} error={containersError ?? stacksError ?? serversError} />;
|
||||
}
|
||||
|
||||
if (!widget.fields || widget.fields.length === 0) {
|
||||
widget.fields = widget.showSummary
|
||||
? ["servers", "stacks", "containers"]
|
||||
: widget.showStacks
|
||||
? ["total", "running", "down", "unhealthy"]
|
||||
: ["total", "running", "stopped", "unhealthy"];
|
||||
} else if (widget.fields?.length > MAX_ALLOWED_FIELDS) {
|
||||
widget.fields = widget.fields.slice(0, MAX_ALLOWED_FIELDS);
|
||||
}
|
||||
|
||||
if (
|
||||
(!widget.showStacks && !containersData) ||
|
||||
(widget.showSummary && (!stacksData || !serversData)) ||
|
||||
(widget.showStacks && !stacksData)
|
||||
) {
|
||||
return widget.showSummary ? (
|
||||
<Container service={service}>
|
||||
<Block label="komodo.servers" />
|
||||
<Block label="komodo.stacks" />
|
||||
<Block label="komodo.containers" />
|
||||
</Container>
|
||||
) : widget.showStacks ? (
|
||||
<Container service={service}>
|
||||
<Block label="komodo.total" />
|
||||
<Block label="komodo.running" />
|
||||
<Block label="komodo.down" />
|
||||
<Block label="komodo.unhealthy" />
|
||||
</Container>
|
||||
) : (
|
||||
<Container service={service}>
|
||||
<Block label="komodo.total" />
|
||||
<Block label="komodo.running" />
|
||||
<Block label="komodo.stopped" />
|
||||
<Block label="komodo.unhealthy" />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
return widget.showSummary ? (
|
||||
<Container service={service}>
|
||||
<Block label="komodo.servers" value={`${serversData.healthy} / ${serversData.total}`} />
|
||||
<Block label="komodo.stacks" value={`${stacksData.running} / ${stacksData.total}`} />
|
||||
<Block label="komodo.containers" value={`${containersData.running} / ${containersData.total}`} />
|
||||
</Container>
|
||||
) : widget.showStacks ? (
|
||||
<Container service={service}>
|
||||
<Block label="komodo.total" value={t("common.number", { value: stacksData.total })} />
|
||||
<Block label="komodo.running" value={t("common.number", { value: stacksData.running })} />
|
||||
<Block label="komodo.down" value={t("common.number", { value: stacksData.stopped + stacksData.down })} />
|
||||
<Block label="komodo.unhealthy" value={t("common.number", { value: stacksData.unhealthy })} />
|
||||
<Block label="komodo.unknown" value={t("common.number", { value: stacksData.unknown })} />
|
||||
</Container>
|
||||
) : (
|
||||
<Container service={service}>
|
||||
<Block label="komodo.total" value={t("common.number", { value: containersData.total })} />
|
||||
<Block label="komodo.running" value={t("common.number", { value: containersData.running })} />
|
||||
<Block label="komodo.stopped" value={t("common.number", { value: containersData.stopped })} />
|
||||
<Block label="komodo.unhealthy" value={t("common.number", { value: containersData.unhealthy })} />
|
||||
<Block label="komodo.unknown" value={t("common.number", { value: containersData.unknown })} />
|
||||
</Container>
|
||||
);
|
||||
}
|
55
src/widgets/komodo/proxy.js
Normal file
55
src/widgets/komodo/proxy.js
Normal file
@ -0,0 +1,55 @@
|
||||
import getServiceWidget from "utils/config/service-helpers";
|
||||
import createLogger from "utils/logger";
|
||||
import { formatApiCall, sanitizeErrorURL } from "utils/proxy/api-helpers";
|
||||
import { httpProxy } from "utils/proxy/http";
|
||||
import validateWidgetData from "utils/proxy/validate-widget-data";
|
||||
import widgets from "widgets/widgets";
|
||||
|
||||
const logger = createLogger("komodoProxyHandler");
|
||||
|
||||
export default async function komodoProxyHandler(req, res) {
|
||||
const { group, service, endpoint, index } = req.query;
|
||||
|
||||
if (group && service) {
|
||||
const widget = await getServiceWidget(group, service, index);
|
||||
if (!widgets?.[widget.type]?.api) {
|
||||
return res.status(403).json({ error: "Service does not support API calls" });
|
||||
}
|
||||
|
||||
if (widget) {
|
||||
// api uses unified read endpoint
|
||||
const url = new URL(formatApiCall(widgets[widget.type].api, { endpoint: "read", ...widget })).toString();
|
||||
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
"X-API-Key": `${widget.key}`,
|
||||
"X-API-Secret": `${widget.secret}`,
|
||||
};
|
||||
const [status, contentType, data] = await httpProxy(url, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(widgets[widget.type].mappings?.[endpoint]?.body || {}),
|
||||
headers,
|
||||
});
|
||||
|
||||
let resultData = data;
|
||||
|
||||
if (status >= 400) {
|
||||
logger.error("HTTP Error %d calling %s", status, sanitizeErrorURL(url));
|
||||
}
|
||||
|
||||
if (status === 200) {
|
||||
if (!validateWidgetData(widget, endpoint, resultData)) {
|
||||
return res
|
||||
.status(500)
|
||||
.json({ error: { message: "Invalid data", url: sanitizeErrorURL(url), data: resultData } });
|
||||
}
|
||||
}
|
||||
|
||||
if (contentType) res.setHeader("Content-Type", contentType);
|
||||
return res.status(status).send(resultData);
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug("Invalid or missing proxy service type '%s' in group '%s'", service, group);
|
||||
return res.status(400).json({ error: "Invalid proxy service type" });
|
||||
}
|
32
src/widgets/komodo/widget.js
Normal file
32
src/widgets/komodo/widget.js
Normal file
@ -0,0 +1,32 @@
|
||||
import komodoProxyHandler from "./proxy";
|
||||
|
||||
const widget = {
|
||||
api: "{url}/{endpoint}",
|
||||
proxyHandler: komodoProxyHandler,
|
||||
|
||||
mappings: {
|
||||
containers: {
|
||||
endpoint: "containers", // api actually uses unified read endpoint
|
||||
body: {
|
||||
type: "GetDockerContainersSummary",
|
||||
params: {},
|
||||
},
|
||||
},
|
||||
stacks: {
|
||||
endpoint: "stacks", // api actually uses unified read endpoint
|
||||
body: {
|
||||
type: "GetStacksSummary",
|
||||
params: {},
|
||||
},
|
||||
},
|
||||
servers: {
|
||||
endpoint: "servers", // api actually uses unified read endpoint
|
||||
body: {
|
||||
type: "GetServersSummary",
|
||||
params: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default widget;
|
@ -6,15 +6,64 @@ import useWidgetAPI from "utils/proxy/use-widget-api";
|
||||
export default function Component({ service }) {
|
||||
const { widget } = service;
|
||||
|
||||
const { data: containersData, error: containersError } = useWidgetAPI(widget, "docker/containers/json", {
|
||||
all: 1,
|
||||
});
|
||||
if (!widget.fields) {
|
||||
widget.fields = widget.kubernetes ? ["applications", "services", "namespaces"] : ["running", "stopped", "total"];
|
||||
}
|
||||
|
||||
const { data: containersCount, error: containersError } = useWidgetAPI(
|
||||
widget,
|
||||
widget.kubernetes ? "" : "docker/containers",
|
||||
{
|
||||
all: 1,
|
||||
},
|
||||
);
|
||||
|
||||
const { data: applicationsCount, error: applicationsError } = useWidgetAPI(
|
||||
widget,
|
||||
widget.kubernetes ? "kubernetes/applications" : "",
|
||||
);
|
||||
|
||||
const { data: servicesCount, error: servicesError } = useWidgetAPI(
|
||||
widget,
|
||||
widget.kubernetes ? "kubernetes/services" : "",
|
||||
);
|
||||
|
||||
const { data: namespacesCount, error: namespacesError } = useWidgetAPI(
|
||||
widget,
|
||||
widget.kubernetes ? "kubernetes/namespaces" : "",
|
||||
);
|
||||
|
||||
if (widget.kubernetes) {
|
||||
// count can be an error object
|
||||
const error = applicationsError ?? servicesError ?? namespacesError ?? applicationsCount;
|
||||
if (error) {
|
||||
return <Container service={service} error={error} />;
|
||||
}
|
||||
|
||||
if (applicationsCount == undefined || servicesCount == undefined || namespacesCount == undefined) {
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="portainer.applications" />
|
||||
<Block label="portainer.services" />
|
||||
<Block label="portainer.namespaces" />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="portainer.applications" value={applicationsCount ?? 0} />
|
||||
<Block label="portainer.services" value={servicesCount ?? 0} />
|
||||
<Block label="portainer.namespaces" value={namespacesCount ?? 0} />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
if (containersError) {
|
||||
return <Container service={service} error={containersError} />;
|
||||
}
|
||||
|
||||
if (!containersData) {
|
||||
if (!containersCount) {
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="portainer.running" />
|
||||
@ -24,14 +73,14 @@ export default function Component({ service }) {
|
||||
);
|
||||
}
|
||||
|
||||
if (containersData.error || containersData.message) {
|
||||
if (containersCount.error || containersCount.message) {
|
||||
// containersData can be itself an error object e.g. if environment fails
|
||||
return <Container service={service} error={containersData?.error ?? containersData} />;
|
||||
return <Container service={service} error={containersCount?.error ?? containersCount} />;
|
||||
}
|
||||
|
||||
const running = containersData.filter((c) => c.State === "running").length;
|
||||
const stopped = containersData.filter((c) => c.State === "exited").length;
|
||||
const total = containersData.length;
|
||||
const running = containersCount.filter((c) => c.State === "running").length;
|
||||
const stopped = containersCount.filter((c) => c.State === "exited").length;
|
||||
const total = containersCount.length;
|
||||
|
||||
return (
|
||||
<Container service={service}>
|
||||
|
@ -1,14 +1,23 @@
|
||||
import credentialedProxyHandler from "utils/proxy/handlers/credentialed";
|
||||
|
||||
const widget = {
|
||||
api: "{url}/api/endpoints/{env}/{endpoint}",
|
||||
api: "{url}/api/{endpoint}",
|
||||
proxyHandler: credentialedProxyHandler,
|
||||
|
||||
mappings: {
|
||||
"docker/containers/json": {
|
||||
endpoint: "docker/containers/json",
|
||||
"docker/containers": {
|
||||
endpoint: "endpoints/{env}/docker/containers/json",
|
||||
params: ["all"],
|
||||
},
|
||||
"kubernetes/applications": {
|
||||
endpoint: "kubernetes/{env}/applications/count",
|
||||
},
|
||||
"kubernetes/services": {
|
||||
endpoint: "kubernetes/{env}/services/count",
|
||||
},
|
||||
"kubernetes/namespaces": {
|
||||
endpoint: "kubernetes/{env}/namespaces/count",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
32
src/widgets/proxmoxvm/component.jsx
Normal file
32
src/widgets/proxmoxvm/component.jsx
Normal file
@ -0,0 +1,32 @@
|
||||
import Block from "components/services/widget/block";
|
||||
import Container from "components/services/widget/container";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import useSWR from "swr";
|
||||
|
||||
export default function ProxmoxVM({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { widget } = service;
|
||||
|
||||
const { data, error } = useSWR(`/api/proxmox/stats/${widget.node}/${widget.vmid}?type=${widget.type || "qemu"}`);
|
||||
|
||||
if (error) {
|
||||
return <Container service={service} error={error} />;
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="resources.cpu" />
|
||||
<Block label="resources.mem" />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="resources.cpu" value={t("common.percent", { value: data.cpu })} />
|
||||
<Block label="resources.mem" value={t("common.bytes", { value: data.mem })} />
|
||||
</Container>
|
||||
);
|
||||
}
|
@ -45,6 +45,25 @@ export default function Component({ service }) {
|
||||
}
|
||||
|
||||
const leech = torrentData.length - completed;
|
||||
const statePriority = [
|
||||
"downloading",
|
||||
"forcedDL",
|
||||
"metaDL",
|
||||
"forcedMetaDL",
|
||||
"checkingDL",
|
||||
"stalledDL",
|
||||
"queuedDL",
|
||||
"pausedDL",
|
||||
];
|
||||
|
||||
leechTorrents.sort((firstTorrent, secondTorrent) => {
|
||||
const firstStateIndex = statePriority.indexOf(firstTorrent.state);
|
||||
const secondStateIndex = statePriority.indexOf(secondTorrent.state);
|
||||
if (firstStateIndex !== secondStateIndex) {
|
||||
return firstStateIndex - secondStateIndex;
|
||||
}
|
||||
return secondTorrent.progress - firstTorrent.progress;
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
|
38
src/widgets/trilium/component.jsx
Normal file
38
src/widgets/trilium/component.jsx
Normal file
@ -0,0 +1,38 @@
|
||||
import Block from "components/services/widget/block";
|
||||
import Container from "components/services/widget/container";
|
||||
import { useTranslation } from "next-i18next";
|
||||
|
||||
import useWidgetAPI from "utils/proxy/use-widget-api";
|
||||
|
||||
export default function Component({ service }) {
|
||||
const { t } = useTranslation();
|
||||
const { widget } = service;
|
||||
|
||||
const { data: metricsData, error: metricsError } = useWidgetAPI(widget, "metrics");
|
||||
|
||||
if (metricsError) {
|
||||
return <Container service={service} error={metricsError} />;
|
||||
}
|
||||
|
||||
if (!metricsData) {
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="trilium.version" />
|
||||
<Block label="trilium.notesCount" />
|
||||
<Block label="trilium.dbSize" />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
const version = metricsData.version?.app;
|
||||
const notesCount = metricsData.database?.activeNotes || 0;
|
||||
const databaseSizeBytes = metricsData.statistics?.databaseSizeBytes || 0;
|
||||
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="trilium.version" value={version ? `v${version}` : t("trilium.unknown")} />
|
||||
<Block label="trilium.notesCount" value={t("common.number", { value: notesCount })} />
|
||||
<Block label="trilium.dbSize" value={t("common.bytes", { value: databaseSizeBytes })} />
|
||||
</Container>
|
||||
);
|
||||
}
|
15
src/widgets/trilium/widget.js
Normal file
15
src/widgets/trilium/widget.js
Normal file
@ -0,0 +1,15 @@
|
||||
import credentialedProxyHandler from "utils/proxy/handlers/credentialed";
|
||||
|
||||
const widget = {
|
||||
api: "{url}/etapi/{endpoint}",
|
||||
proxyHandler: credentialedProxyHandler,
|
||||
|
||||
mappings: {
|
||||
metrics: {
|
||||
endpoint: "metrics?format=json",
|
||||
validate: ["version", "database"],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default widget;
|
@ -54,6 +54,7 @@ import jellystat from "./jellystat/widget";
|
||||
import karakeep from "./karakeep/widget";
|
||||
import kavita from "./kavita/widget";
|
||||
import komga from "./komga/widget";
|
||||
import komodo from "./komodo/widget";
|
||||
import kopia from "./kopia/widget";
|
||||
import lidarr from "./lidarr/widget";
|
||||
import linkwarden from "./linkwarden/widget";
|
||||
@ -123,6 +124,7 @@ import tdarr from "./tdarr/widget";
|
||||
import technitium from "./technitium/widget";
|
||||
import traefik from "./traefik/widget";
|
||||
import transmission from "./transmission/widget";
|
||||
import trilium from "./trilium/widget";
|
||||
import truenas from "./truenas/widget";
|
||||
import tubearchivist from "./tubearchivist/widget";
|
||||
import unifi from "./unifi/widget";
|
||||
@ -196,6 +198,7 @@ const widgets = {
|
||||
jellystat,
|
||||
kavita,
|
||||
komga,
|
||||
komodo,
|
||||
kopia,
|
||||
lidarr,
|
||||
linkwarden,
|
||||
@ -266,6 +269,7 @@ const widgets = {
|
||||
tdarr,
|
||||
traefik,
|
||||
transmission,
|
||||
trilium,
|
||||
tubearchivist,
|
||||
truenas,
|
||||
unifi,
|
||||
|
Loading…
x
Reference in New Issue
Block a user