Compare commits

..

7 commits
dev ... v1.12.0

Author SHA1 Message Date
shamoon
02989a4366
Bump version to 1.12.0
Some checks failed
Docker CI / Linting Checks (push) Has been cancelled
Docs / Linting Checks (push) Has been cancelled
Tests / vitest (1) (push) Has been cancelled
Tests / vitest (2) (push) Has been cancelled
Tests / vitest (3) (push) Has been cancelled
Tests / vitest (4) (push) Has been cancelled
Docker CI / Docker Build & Push (push) Has been cancelled
Docs / Test Build Docs (push) Has been cancelled
Docs / Build & Deploy Docs (push) Has been cancelled
2026-03-27 15:18:07 -07:00
shamoon
bc6acf7fd1
Merge branch 'dev' 2026-03-27 15:17:33 -07:00
shamoon
a4e29bc7a7
1.11.0
Some checks failed
Docker CI / Linting Checks (push) Has been cancelled
Docs / Linting Checks (push) Has been cancelled
Tests / vitest (1) (push) Has been cancelled
Tests / vitest (2) (push) Has been cancelled
Tests / vitest (3) (push) Has been cancelled
Tests / vitest (4) (push) Has been cancelled
Docker CI / Docker Build & Push (push) Has been cancelled
Docs / Test Build Docs (push) Has been cancelled
Docs / Build & Deploy Docs (push) Has been cancelled
2026-03-14 08:58:53 -07:00
shamoon
a7982bda06
Merge branch 'dev' 2026-03-14 08:58:38 -07:00
shamoon
6b3bff1f1d
Fix typo in shortcuts documentation 2026-03-07 16:13:08 -08:00
shamoon
e3ca0adf11
Documentation: add 'unit' option for temperature in glances config 2026-02-20 22:12:12 -08:00
Kristiyan Nikolov
d62404f164
Documentation: Fix doc heading for PWA/App icons (#6290) 2026-02-05 11:36:19 -08:00
87 changed files with 229 additions and 1160 deletions

View file

@ -17,9 +17,9 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 uses: actions/checkout@v6
- name: crowdin action - name: crowdin action
uses: crowdin/github-action@7ca9c452bfe9197d3bb7fa83a4d7e2b0c9ae835d # v2 uses: crowdin/github-action@v2
with: with:
upload_translations: false upload_translations: false
download_translations: true download_translations: true

View file

@ -17,12 +17,44 @@ env:
IMAGE_NAME: ${{ github.repository }} IMAGE_NAME: ${{ github.repository }}
jobs: jobs:
pre-commit:
name: Linting Checks
runs-on: ubuntu-22.04
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Install python
uses: actions/setup-python@v6
with:
python-version: 3.x
- name: Check files
uses: pre-commit/action@v3.0.1
- name: Install pnpm
uses: pnpm/action-setup@v5
with:
version: 10
run_install: false
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 24
cache: 'pnpm'
- name: Install dependencies
run: pnpm install
- name: Lint frontend
run: pnpm run lint
build: build:
name: Docker Build & Push name: Docker Build & Push
if: github.repository == 'gethomepage/homepage' if: github.repository == 'gethomepage/homepage'
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
needs: [ pre-commit ]
permissions: permissions:
contents: read contents: read
packages: write packages: write
@ -30,11 +62,11 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 uses: actions/checkout@v6
- name: Extract Docker metadata - name: Extract Docker metadata
id: meta id: meta
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6 uses: docker/metadata-action@v6
with: with:
images: | images: |
${{ env.IMAGE_NAME }} ${{ env.IMAGE_NAME }}
@ -52,7 +84,7 @@ jobs:
latest=auto latest=auto
- name: Next.js build cache - name: Next.js build cache
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5 uses: actions/cache@v5
with: with:
path: .next/cache path: .next/cache
key: nextjs-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}-${{ hashFiles('**/*.js', '**/*.jsx') }} key: nextjs-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}-${{ hashFiles('**/*.js', '**/*.jsx') }}
@ -60,13 +92,13 @@ jobs:
nextjs-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }} nextjs-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@a8198c4bff370c8506180b035930dea56dbd5288 # v5 uses: pnpm/action-setup@v5
with: with:
version: 10 version: 10
run_install: false run_install: false
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 uses: actions/setup-node@v6
with: with:
node-version: 24 node-version: 24
cache: 'pnpm' cache: 'pnpm'
@ -83,7 +115,7 @@ jobs:
- name: Log into registry ${{ env.REGISTRY }} - name: Log into registry ${{ env.REGISTRY }}
if: github.event_name != 'pull_request' if: github.event_name != 'pull_request'
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4 uses: docker/login-action@v4
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}
@ -91,20 +123,20 @@ jobs:
- name: Login to Docker Hub - name: Login to Docker Hub
if: github.event_name != 'pull_request' if: github.event_name != 'pull_request'
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4 uses: docker/login-action@v4
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Setup QEMU - name: Setup QEMU
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 uses: docker/setup-qemu-action@v4.0.0
- name: Setup Docker buildx - name: Setup Docker buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4 uses: docker/setup-buildx-action@v4
- name: Build and push Docker image - name: Build and push Docker image
id: build-and-push id: build-and-push
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7 uses: docker/build-push-action@v7
with: with:
context: . context: .
push: ${{ github.event_name != 'pull_request' }} push: ${{ github.event_name != 'pull_request' }}

View file

@ -14,18 +14,32 @@ permissions:
id-token: write id-token: write
jobs: jobs:
pre-commit:
name: Linting Checks
runs-on: ubuntu-22.04
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Install python
uses: actions/setup-python@v6
with:
python-version: 3.x
- name: Check files
uses: pre-commit/action@v3.0.1
test: test:
name: Test Build Docs name: Test Build Docs
if: github.repository == 'gethomepage/homepage' && github.event_name == 'pull_request' if: github.repository == 'gethomepage/homepage' && github.event_name == 'pull_request'
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs:
- pre-commit
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - uses: actions/checkout@v6
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 - uses: actions/setup-python@v6
with: with:
python-version-file: ".python-version" python-version-file: ".python-version"
- name: Install uv - name: Install uv
uses: astral-sh/setup-uv@94527f2e458b27549849d47d273a16bec83a01e9 # v7 uses: astral-sh/setup-uv@v7
- run: sudo apt-get install pngquant - run: sudo apt-get install pngquant
- name: Test Docs Build - name: Test Docs Build
run: uv run --frozen zensical build --clean run: uv run --frozen zensical build --clean
@ -36,19 +50,21 @@ jobs:
environment: environment:
name: github-pages name: github-pages
url: ${{ steps.deployment.outputs.page_url }} url: ${{ steps.deployment.outputs.page_url }}
needs:
- pre-commit
steps: steps:
- uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b # v5 - uses: actions/configure-pages@v5
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - uses: actions/checkout@v6
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 - uses: actions/setup-python@v6
with: with:
python-version-file: ".python-version" python-version-file: ".python-version"
- name: Install uv - name: Install uv
uses: astral-sh/setup-uv@94527f2e458b27549849d47d273a16bec83a01e9 # v7 uses: astral-sh/setup-uv@v7
- run: sudo apt-get install pngquant - run: sudo apt-get install pngquant
- name: Build Docs - name: Build Docs
run: uv run --frozen zensical build --clean run: uv run --frozen zensical build --clean
- uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4 - uses: actions/upload-pages-artifact@v4
with: with:
path: site path: site
- uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4 - uses: actions/deploy-pages@v4
id: deployment id: deployment

View file

@ -1,41 +0,0 @@
name: Lint
on:
pull_request:
push:
workflow_dispatch:
merge_group:
jobs:
lint:
name: Linting Checks
runs-on: ubuntu-22.04
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Install python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with:
python-version: 3.x
- name: Check files
uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1
- name: Install pnpm
uses: pnpm/action-setup@a8198c4bff370c8506180b035930dea56dbd5288 # v5
with:
version: 10
run_install: false
- name: Setup Node.js
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
with:
node-version: 24
cache: 'pnpm'
- name: Install dependencies
run: pnpm install
- name: Lint frontend
run: pnpm run lint

View file

@ -13,6 +13,6 @@ jobs:
anti-slop: anti-slop:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: peakoss/anti-slop@a5a4b2440c9de6f65b64f0718a0136a1fdb04f6f # v0 - uses: peakoss/anti-slop@v0
with: with:
max-failures: 4 max-failures: 4

View file

@ -15,4 +15,4 @@ jobs:
action: action:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: dessant/reaction-comments@e86d247c12bd5c043eec379a1a4453f20cadf913 # v4 - uses: dessant/reaction-comments@v4

View file

@ -26,14 +26,14 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- if: github.event_name == 'workflow_dispatch' && github.event.inputs.version != '' - if: github.event_name == 'workflow_dispatch' && github.event.inputs.version != ''
uses: release-drafter/release-drafter@a6acf82562eee06318b77ab8cb0b11ed81c677a7 # v7 uses: release-drafter/release-drafter@v7
with: with:
config-name: release-drafter.yml config-name: release-drafter.yml
version: ${{ github.event.inputs.version }} version: ${{ github.event.inputs.version }}
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- if: github.event_name != 'workflow_dispatch' || github.event.inputs.version == '' - if: github.event_name != 'workflow_dispatch' || github.event.inputs.version == ''
uses: release-drafter/release-drafter@a6acf82562eee06318b77ab8cb0b11ed81c677a7 # v7 uses: release-drafter/release-drafter@v7
with: with:
config-name: release-drafter.yml config-name: release-drafter.yml
env: env:

View file

@ -18,7 +18,7 @@ jobs:
name: 'Stale' name: 'Stale'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10 - uses: actions/stale@v10
with: with:
days-before-stale: 7 days-before-stale: 7
days-before-close: 14 days-before-close: 14
@ -32,7 +32,7 @@ jobs:
name: 'Lock Old Threads' name: 'Lock Old Threads'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: dessant/lock-threads@7266a7ce5c1df01b1c6db85bf8cd86c737dadbe7 # v6 - uses: dessant/lock-threads@v6
with: with:
issue-inactive-days: '30' issue-inactive-days: '30'
pr-inactive-days: '30' pr-inactive-days: '30'
@ -57,7 +57,7 @@ jobs:
name: 'Close Answered Discussions' name: 'Close Answered Discussions'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - uses: actions/github-script@v8
with: with:
script: | script: |
function sleep(ms) { function sleep(ms) {
@ -113,7 +113,7 @@ jobs:
name: 'Close Outdated Discussions' name: 'Close Outdated Discussions'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - uses: actions/github-script@v8
with: with:
script: | script: |
function sleep(ms) { function sleep(ms) {
@ -204,7 +204,7 @@ jobs:
name: 'Close Unsupported Feature Requests' name: 'Close Unsupported Feature Requests'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - uses: actions/github-script@v8
with: with:
script: | script: |
function sleep(ms) { function sleep(ms) {

View file

@ -13,13 +13,13 @@ jobs:
matrix: matrix:
shard: [1, 2, 3, 4] shard: [1, 2, 3, 4]
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - uses: actions/checkout@v6
- uses: pnpm/action-setup@a8198c4bff370c8506180b035930dea56dbd5288 # v5 - uses: pnpm/action-setup@v5
with: with:
version: 9 version: 9
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 - uses: actions/setup-node@v6
with: with:
node-version: 20 node-version: 20
cache: pnpm cache: pnpm
@ -28,7 +28,7 @@ jobs:
# Run Vitest directly so `--shard` is parsed as an option # Run Vitest directly so `--shard` is parsed as an option
- run: pnpm -s exec vitest run --coverage --shard ${{ matrix.shard }}/4 --pool forks - run: pnpm -s exec vitest run --coverage --shard ${{ matrix.shard }}/4 --pool forks
- name: Upload coverage reports to Codecov - name: Upload coverage reports to Codecov
uses: codecov/codecov-action@75cd11691c0faa626561e295848008c8a7dddffe # v5 uses: codecov/codecov-action@v5
with: with:
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}
files: ./coverage/lcov.info files: ./coverage/lcov.info

View file

@ -12,7 +12,6 @@ ARG CI
ARG BUILDTIME ARG BUILDTIME
ARG VERSION ARG VERSION
ARG REVISION ARG REVISION
ARG NEXT_PUBLIC_BASE_PATH
ENV CI=$CI ENV CI=$CI
# Install and build only outside CI # Install and build only outside CI
@ -23,7 +22,6 @@ RUN if [ "$CI" != "true" ]; then \
NEXT_PUBLIC_BUILDTIME=$BUILDTIME \ NEXT_PUBLIC_BUILDTIME=$BUILDTIME \
NEXT_PUBLIC_VERSION=$VERSION \ NEXT_PUBLIC_VERSION=$VERSION \
NEXT_PUBLIC_REVISION=$REVISION \ NEXT_PUBLIC_REVISION=$REVISION \
NEXT_PUBLIC_BASE_PATH=$NEXT_PUBLIC_BASE_PATH \
pnpm run build; \ pnpm run build; \
else \ else \
echo "✅ Using prebuilt app from CI context"; \ echo "✅ Using prebuilt app from CI context"; \

View file

@ -1,14 +0,0 @@
---
- Repositories:
- DeckForge:
- abbr: DF
href: https://bitbucket.org/zlalani/ppt-tool
- GMAL Scope Builder:
- abbr: GSB
href: https://bitbucket.org/zlalani/gmal-scope-builder
- Homepage:
- abbr: HP
href: https://bitbucket.org/zlalani/homepage
- OliVAS:
- abbr: OL
href: https://bitbucket.org/zlalani/olivas

View file

@ -1,3 +0,0 @@
---
local:
socket: /var/run/docker.sock

View file

@ -1,96 +0,0 @@
---
- AI Tools:
- DeckForge:
icon: mdi-presentation
href: https://optical-dev.oliver.solutions/ppt-tool
description: AI presentation generator
container: ppt-tool-web-1
server: local
siteMonitor: https://optical-dev.oliver.solutions/ppt-tool
showStats: true
widget:
type: deploy
service: ppt-tool
label: DeckForge
- GMAL Scope Builder:
icon: mdi-briefcase-outline
href: https://optical-dev.oliver.solutions/gsb
description: AI ratecard & team scoping
container: gmal-scope-builder-backend-1
server: local
siteMonitor: https://optical-dev.oliver.solutions/gsb/
showStats: true
widget:
type: deploy
service: gmal-scope-builder
label: Scope Builder
- Semblance:
icon: mdi-account-group-outline
href: https://optical-dev.oliver.solutions/semblance
description: Synthetic personas & focus groups
container: semblance-backend-1
server: local
siteMonitor: https://optical-dev.oliver.solutions/semblance/
showStats: true
widget:
type: deploy
service: semblance
label: Semblance
- CC Dashboard:
icon: mdi-view-dashboard-outline
href: https://optical-dev.oliver.solutions/cc-dashboard
description: API key & project management
container: cc-dashboard-app-1
server: local
siteMonitor: https://optical-dev.oliver.solutions/cc-dashboard/
showStats: true
widget:
type: deploy
service: cc-dashboard
label: CC Dashboard
- OliVAS:
icon: mdi-robot-outline
href: https://optical-dev.oliver.solutions
description: OliVAS backend API
container: olivas-backend-1
server: local
siteMonitor: https://optical-dev.oliver.solutions/api/health
showStats: true
widget:
type: deploy
service: olivas
label: OliVAS
- Infrastructure:
- Homepage:
icon: mdi-home-outline
href: https://optical-dev.oliver.solutions/homepage
description: This dashboard
container: homepage-app-1
server: local
siteMonitor: https://optical-dev.oliver.solutions/homepage
showStats: true
widget:
type: deploy
service: homepage
label: Homepage
- Deploy API:
icon: mdi-rocket-launch-outline
href: https://optical-dev.oliver.solutions/deploy-api/docs
description: One-click deploy service
siteMonitor: https://optical-dev.oliver.solutions/deploy-api/services
- PostgreSQL × 4:
icon: mdi-database-outline
description: "ppt-tool · olivas · cc-dashboard · gmal"
href: https://optical-dev.oliver.solutions
- Redis + MongoDB:
icon: mdi-server-outline
description: ppt-tool (Redis) · semblance (Mongo)
href: https://optical-dev.oliver.solutions

View file

@ -1,33 +0,0 @@
---
title: Optical Dev
language: en
theme: dark
color: zinc
# Full-width layout, equal card heights, hide noise
fullWidth: true
useEqualHeights: true
headerStyle: clean
cardBlur: md
statusStyle: dot
hideVersion: true
disableUpdateCheck: true
target: _blank
# Layout sections
layout:
Widgets:
style: row
columns: 2
AI Tools:
style: row
columns: 5
useEqualHeights: true
Infrastructure:
style: row
columns: 4
useEqualHeights: true
Repositories:
style: row
columns: 4
iconsOnly: false

View file

@ -1,15 +0,0 @@
---
- resources:
cpu: true
memory: true
disk: /
label: optical-dev server
expanded: true
cputemp: false
- datetime:
text_size: xl
format:
timeStyle: short
dateStyle: short
hourCycle: h23

View file

@ -1,30 +0,0 @@
#!/bin/bash
set -e
DEPLOY_DIR="/opt/homepage"
REPO="git@bitbucket.org:zlalani/homepage.git"
echo "=== Homepage Deploy ==="
if [ ! -d "$DEPLOY_DIR/.git" ]; then
echo "Cloning repository..."
git clone "$REPO" "$DEPLOY_DIR"
cd "$DEPLOY_DIR"
else
echo "Pulling latest changes..."
cd "$DEPLOY_DIR"
git pull origin dev
fi
mkdir -p config
echo "Building and starting container..."
docker compose build --no-cache
docker compose up -d
echo "Waiting for healthcheck..."
sleep 15
docker compose ps
echo ""
echo "Done. Homepage available at https://optical-dev.oliver.solutions/homepage"

View file

@ -1,21 +0,0 @@
services:
app:
build:
context: .
args:
NEXT_PUBLIC_BASE_PATH: /homepage
ports:
- "127.0.0.1:3001:3000"
volumes:
- ./config:/app/config
- /var/run/docker.sock:/var/run/docker.sock:ro
environment:
HOSTNAME: "::"
HOMEPAGE_ALLOWED_HOSTS: "optical-dev.oliver.solutions"
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://127.0.0.1:3000/api/healthcheck || exit 1"]
interval: 10s
timeout: 3s
start_period: 30s
retries: 3

View file

@ -129,7 +129,7 @@ A progressive web app is an app that can be installed on a device and provide us
More information on PWAs can be found in [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps). More information on PWAs can be found in [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps).
## App icons ### App icons
You can set custom icons for installable apps. More information about how you can set them can be found in the [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Manifest/Reference/icons). You can set custom icons for installable apps. More information about how you can set them can be found in the [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Manifest/Reference/icons).
@ -150,7 +150,7 @@ For icon `src` you can pass either full URL or a local path relative to the `/ap
### Shortcuts ### Shortcuts
Shortcuts can e used to specify links to tabs, to be preselected when the homepage is opened as an app. Shortcuts can be used to specify links to tabs, to be preselected when the homepage is opened as an app.
More information about how you can set them can be found in the [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Manifest/Reference/shortcuts). More information about how you can set them can be found in the [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Manifest/Reference/shortcuts).
```yaml ```yaml

View file

@ -16,6 +16,7 @@ The Glances widget allows you to monitor the resources (CPU, memory, storage, te
cpu: true # optional, enabled by default, disable by setting to false cpu: true # optional, enabled by default, disable by setting to false
mem: true # optional, enabled by default, disable by setting to false mem: true # optional, enabled by default, disable by setting to false
cputemp: true # disabled by default cputemp: true # disabled by default
unit: imperial # optional for temp, default is metric
uptime: true # disabled by default uptime: true # disabled by default
disk: / # disabled by default, use mount point of disk(s) in glances. Can also be a list (see below) disk: / # disabled by default, use mount point of disk(s) in glances. Can also be a list (see below)
diskUnits: bytes # optional, bytes (default) or bbytes. Only applies to disk diskUnits: bytes # optional, bytes (default) or bbytes. Only applies to disk
@ -31,5 +32,3 @@ disk:
- /boot - /boot
... ...
``` ```
_Added in v0.4.18, updated in v0.6.11, v0.6.21_

View file

@ -1,24 +0,0 @@
---
title: UniFi Drive
description: UniFi Drive Widget Configuration
---
Learn more about [UniFi Drive](https://ui.com/integrations/network-storage).
## Configuration
Displays storage statistics from your UniFi Network Attached Storage (UNAS) device. Requires a local UniFi account with at least read privileges.
Allowed fields: `["total", "used", "available", "status"]`
```yaml
widget:
type: unifi_drive
url: https://unifi.host.or.ip
username: your_username
password: your_password
```
!!! hint
If you enter incorrect credentials and receive an "API Error", you may need to recreate the container or restart the service to clear the cache.

View file

@ -171,7 +171,6 @@ nav:
- widgets/services/truenas.md - widgets/services/truenas.md
- widgets/services/tubearchivist.md - widgets/services/tubearchivist.md
- widgets/services/unifi-controller.md - widgets/services/unifi-controller.md
- widgets/services/unifi-drive.md
- widgets/services/unmanic.md - widgets/services/unmanic.md
- widgets/services/unraid.md - widgets/services/unraid.md
- widgets/services/uptime-kuma.md - widgets/services/uptime-kuma.md

View file

@ -4,7 +4,6 @@ const { i18n } = require("./next-i18next.config");
const nextConfig = { const nextConfig = {
reactStrictMode: true, reactStrictMode: true,
output: "standalone", output: "standalone",
basePath: process.env.NEXT_PUBLIC_BASE_PATH || "",
images: { images: {
remotePatterns: [ remotePatterns: [
{ {

View file

@ -1,6 +1,6 @@
{ {
"name": "homepage", "name": "homepage",
"version": "1.10.1", "version": "1.12.0",
"private": true, "private": true,
"scripts": { "scripts": {
"preinstall": "npx only-allow pnpm", "preinstall": "npx only-allow pnpm",

16
pnpm-lock.yaml generated
View file

@ -1593,11 +1593,11 @@ packages:
bl@4.1.0: bl@4.1.0:
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
brace-expansion@1.1.13: brace-expansion@1.1.12:
resolution: {integrity: sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==} resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==}
brace-expansion@2.0.3: brace-expansion@2.0.2:
resolution: {integrity: sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==} resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==}
braces@3.0.3: braces@3.0.3:
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
@ -5162,12 +5162,12 @@ snapshots:
inherits: 2.0.4 inherits: 2.0.4
readable-stream: 3.6.2 readable-stream: 3.6.2
brace-expansion@1.1.13: brace-expansion@1.1.12:
dependencies: dependencies:
balanced-match: 1.0.2 balanced-match: 1.0.2
concat-map: 0.0.1 concat-map: 0.0.1
brace-expansion@2.0.3: brace-expansion@2.0.2:
dependencies: dependencies:
balanced-match: 1.0.2 balanced-match: 1.0.2
@ -6598,11 +6598,11 @@ snapshots:
minimatch@3.1.2: minimatch@3.1.2:
dependencies: dependencies:
brace-expansion: 1.1.13 brace-expansion: 1.1.12
minimatch@9.0.5: minimatch@9.0.5:
dependencies: dependencies:
brace-expansion: 2.0.3 brace-expansion: 2.0.2
minimist@1.2.8: {} minimist@1.2.8: {}

View file

@ -66,11 +66,6 @@
"wait": "Wag asseblief", "wait": "Wag asseblief",
"empty_data": "Substelsel status onbekend" "empty_data": "Substelsel status onbekend"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "RX", "rx": "RX",
"tx": "TX", "tx": "TX",

View file

@ -66,11 +66,6 @@
"wait": "Please wait", "wait": "Please wait",
"empty_data": "حالة النظام الفرعي غير معروفة" "empty_data": "حالة النظام الفرعي غير معروفة"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "استقبال", "rx": "استقبال",
"tx": "ارسال", "tx": "ارسال",

View file

@ -66,11 +66,6 @@
"wait": "Моля изчакайте", "wait": "Моля изчакайте",
"empty_data": "Неизвестен статус на подсистема" "empty_data": "Неизвестен статус на подсистема"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "ПЧ", "rx": "ПЧ",
"tx": "ИЗ", "tx": "ИЗ",

View file

@ -66,11 +66,6 @@
"wait": "Si us plau espera", "wait": "Si us plau espera",
"empty_data": "Estat del subsistema desconegut" "empty_data": "Estat del subsistema desconegut"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "Rebut", "rx": "Rebut",
"tx": "Transmès", "tx": "Transmès",

View file

@ -39,7 +39,7 @@
"placeholder": "Hledat…" "placeholder": "Hledat…"
}, },
"resources": { "resources": {
"cpu": "Využití procesoru", "cpu": "Zatížení procesoru",
"mem": "Využití paměti", "mem": "Využití paměti",
"total": "Celkem", "total": "Celkem",
"free": "Volné", "free": "Volné",
@ -66,16 +66,11 @@
"wait": "Čekejte prosím", "wait": "Čekejte prosím",
"empty_data": "Stav podsystému neznámý" "empty_data": "Stav podsystému neznámý"
}, },
"unifi_drive": {
"healthy": "Zdravý",
"degraded": "Degradováno",
"no_data": "Nejsou k dispozici žádná data úložiště"
},
"docker": { "docker": {
"rx": "RX", "rx": "RX",
"tx": "TX", "tx": "TX",
"mem": "Využití paměti", "mem": "Využití paměti",
"cpu": "Využití procesoru", "cpu": "Zatížení procesoru",
"running": "Běží", "running": "Běží",
"offline": "Offline", "offline": "Offline",
"error": "Chyba", "error": "Chyba",
@ -237,7 +232,7 @@
"seed": "Seedované" "seed": "Seedované"
}, },
"qnap": { "qnap": {
"cpuUsage": "Využití procesoru", "cpuUsage": "Zatížení procesoru",
"memUsage": "Využití paměti", "memUsage": "Využití paměti",
"systemTempC": "Teplota systému", "systemTempC": "Teplota systému",
"poolUsage": "Využití fondu", "poolUsage": "Využití fondu",
@ -450,12 +445,12 @@
}, },
"proxmox": { "proxmox": {
"mem": "Využití paměti", "mem": "Využití paměti",
"cpu": "Využití procesoru", "cpu": "Zatížení procesoru",
"lxc": "LXC", "lxc": "LXC",
"vms": "Virtuální Stroje" "vms": "Virtuální Stroje"
}, },
"glances": { "glances": {
"cpu": "Využití procesoru", "cpu": "Zatížení procesoru",
"load": "Zatížení", "load": "Zatížení",
"wait": "Čekejte prosím", "wait": "Čekejte prosím",
"temp": "TEPLOTA", "temp": "TEPLOTA",
@ -640,7 +635,7 @@
"no_devices": "Žádná přijatá data zařízení" "no_devices": "Žádná přijatá data zařízení"
}, },
"mikrotik": { "mikrotik": {
"cpuLoad": "Využití procesoru", "cpuLoad": "Zatížení procesoru",
"memoryUsed": "Využití paměti", "memoryUsed": "Využití paměti",
"uptime": "Doba provozu", "uptime": "Doba provozu",
"numberOfLeases": "Pronájmy" "numberOfLeases": "Pronájmy"
@ -691,7 +686,7 @@
"proxmoxbackupserver": { "proxmoxbackupserver": {
"datastore_usage": "Datové úložiště", "datastore_usage": "Datové úložiště",
"failed_tasks_24h": "Neúspěšné úlohy 24h", "failed_tasks_24h": "Neúspěšné úlohy 24h",
"cpu_usage": "Využití procesoru", "cpu_usage": "Zatížení procesoru",
"memory_usage": "Využití paměti" "memory_usage": "Využití paměti"
}, },
"immich": { "immich": {
@ -755,7 +750,7 @@
"alertstriggered": "Spuštěné výstrahy" "alertstriggered": "Spuštěné výstrahy"
}, },
"nextcloud": { "nextcloud": {
"cpuload": "Využití procesoru", "cpuload": "Zatížení procesoru",
"memoryusage": "Využití paměti", "memoryusage": "Využití paměti",
"freespace": "Volný prostor", "freespace": "Volný prostor",
"activeusers": "Aktivní uživatelé", "activeusers": "Aktivní uživatelé",
@ -878,7 +873,7 @@
}, },
"openwrt": { "openwrt": {
"uptime": "Doba provozu", "uptime": "Doba provozu",
"cpuLoad": "Prům. využití procesoru (5m)", "cpuLoad": "Prům. zatížení procesoru (5m)",
"up": "Běží", "up": "Běží",
"down": "Výpadek", "down": "Výpadek",
"bytesTx": "Přeneseno", "bytesTx": "Přeneseno",
@ -1042,7 +1037,7 @@
"pending": "Čekající", "pending": "Čekající",
"status": "Stav", "status": "Stav",
"updated": "Aktualizováno", "updated": "Aktualizováno",
"cpu": "Využití procesoru", "cpu": "Zatížení procesoru",
"memory": "Využití paměti", "memory": "Využití paměti",
"disk": "Disk", "disk": "Disk",
"network": "Síť" "network": "Síť"
@ -1138,7 +1133,7 @@
"NO_DATA_DISKS": "Žádné datové disky", "NO_DATA_DISKS": "Žádné datové disky",
"notifications": "Upozornění", "notifications": "Upozornění",
"status": "Stav", "status": "Stav",
"cpu": "Využití procesoru", "cpu": "Zatížení procesoru",
"memoryUsed": "Využití paměti", "memoryUsed": "Využití paměti",
"memoryAvailable": "Volná paměť", "memoryAvailable": "Volná paměť",
"arrayUsed": "Využito pole", "arrayUsed": "Využito pole",
@ -1169,7 +1164,7 @@
"dockhand": { "dockhand": {
"running": "Běží", "running": "Běží",
"stopped": "Zastaveno", "stopped": "Zastaveno",
"cpu": "Využití procesoru", "cpu": "Zatížení procesoru",
"memory": "Využití paměti", "memory": "Využití paměti",
"images": "Obrazy", "images": "Obrazy",
"volumes": "Úložiště", "volumes": "Úložiště",

View file

@ -66,11 +66,6 @@
"wait": "Please wait", "wait": "Please wait",
"empty_data": "Subsystem status ukendt" "empty_data": "Subsystem status ukendt"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "RX", "rx": "RX",
"tx": "TX", "tx": "TX",

View file

@ -66,11 +66,6 @@
"wait": "Bitte warten", "wait": "Bitte warten",
"empty_data": "Subsystem-Status unbekannt" "empty_data": "Subsystem-Status unbekannt"
}, },
"unifi_drive": {
"healthy": "Gesund",
"degraded": "Beeinträchtigt",
"no_data": "Keine Speicherdaten verfügbar"
},
"docker": { "docker": {
"rx": "RX", "rx": "RX",
"tx": "TX", "tx": "TX",
@ -120,7 +115,7 @@
"movies": "Filme", "movies": "Filme",
"series": "Serien", "series": "Serien",
"episodes": "Episoden", "episodes": "Episoden",
"songs": "Titel" "songs": "Songs"
}, },
"esphome": { "esphome": {
"offline": "Offline", "offline": "Offline",
@ -190,10 +185,10 @@
"plex_connection_error": "Prüfe Plex-Verbindung" "plex_connection_error": "Prüfe Plex-Verbindung"
}, },
"tracearr": { "tracearr": {
"no_active": "Keine aktiven Streams", "no_active": "No Active Streams",
"streams": "Streams", "streams": "Streams",
"transcodes": "Transkodieren", "transcodes": "Transcodes",
"directplay": "Direkte Wiedergabe", "directplay": "Direct Play",
"bitrate": "Bitrate" "bitrate": "Bitrate"
}, },
"omada": { "omada": {
@ -295,12 +290,12 @@
"available": "Verfügbar" "available": "Verfügbar"
}, },
"seerr": { "seerr": {
"pending": "Ausstehend", "pending": "Pending",
"approved": "Bestätigt", "approved": "Approved",
"available": "Verfügbar", "available": "Available",
"completed": "Abgeschlossen", "completed": "Completed",
"processing": "Wird verarbeitet", "processing": "Processing",
"issues": "Offene Probleme" "issues": "Open Issues"
}, },
"netalertx": { "netalertx": {
"total": "Total", "total": "Total",
@ -620,7 +615,7 @@
}, },
"pangolin": { "pangolin": {
"orgs": "Orgs", "orgs": "Orgs",
"sites": "Seiten", "sites": "Sites",
"resources": "Ressourcen", "resources": "Ressourcen",
"targets": "Ziele", "targets": "Ziele",
"traffic": "Traffic", "traffic": "Traffic",
@ -724,7 +719,7 @@
"volumeAvailable": "Verfügbar" "volumeAvailable": "Verfügbar"
}, },
"dispatcharr": { "dispatcharr": {
"channels": "Kanäle", "channels": "Channels",
"streams": "Streams" "streams": "Streams"
}, },
"mylar": { "mylar": {
@ -816,10 +811,10 @@
"series": "Serien" "series": "Serien"
}, },
"booklore": { "booklore": {
"libraries": "Bibliotheken", "libraries": "Libraries",
"books": "Bücher", "books": "Bücher",
"reading": "Am Lesen", "reading": "Reading",
"finished": "Fertig" "finished": "Finished"
}, },
"jdownloader": { "jdownloader": {
"downloadCount": "Warteschlange", "downloadCount": "Warteschlange",
@ -1160,11 +1155,11 @@
"artists": "Künstler" "artists": "Künstler"
}, },
"arcane": { "arcane": {
"containers": "Container", "containers": "Containers",
"images": "Images", "images": "Images",
"image_updates": "Image-Updates", "image_updates": "Image Updates",
"images_unused": "Ungenutzt", "images_unused": "Unused",
"environment_required": "Umgebungs-ID erforderlich" "environment_required": "Environment ID Required"
}, },
"dockhand": { "dockhand": {
"running": "Wird ausgeführt", "running": "Wird ausgeführt",
@ -1181,9 +1176,9 @@
"environment_not_found": "Umgebung nicht gefunden" "environment_not_found": "Umgebung nicht gefunden"
}, },
"sparkyfitness": { "sparkyfitness": {
"eaten": "", "eaten": "Eaten",
"burned": "Verbrannt", "burned": "Burned",
"remaining": "Verbleibend", "remaining": "Remaining",
"steps": "Schritte" "steps": "Steps"
} }
} }

View file

@ -66,11 +66,6 @@
"wait": "Please wait", "wait": "Please wait",
"empty_data": "Άγνωστη κατάσταση υποσυστήματος" "empty_data": "Άγνωστη κατάσταση υποσυστήματος"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "RX", "rx": "RX",
"tx": "TX", "tx": "TX",

View file

@ -66,11 +66,6 @@
"wait": "Please wait", "wait": "Please wait",
"empty_data": "Subsystem status unknown" "empty_data": "Subsystem status unknown"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "RX", "rx": "RX",
"tx": "TX", "tx": "TX",

View file

@ -66,11 +66,6 @@
"wait": "Please wait", "wait": "Please wait",
"empty_data": "Subsistemostatuso nekonata" "empty_data": "Subsistemostatuso nekonata"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "RX", "rx": "RX",
"tx": "TX", "tx": "TX",

View file

@ -66,11 +66,6 @@
"wait": "Espere, por favor", "wait": "Espere, por favor",
"empty_data": "Se desconoce el estado del subsistema" "empty_data": "Se desconoce el estado del subsistema"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "Recibido", "rx": "Recibido",
"tx": "Transmitido", "tx": "Transmitido",

View file

@ -66,11 +66,6 @@
"wait": "Please wait", "wait": "Please wait",
"empty_data": "Subsystem status unknown" "empty_data": "Subsystem status unknown"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "RX", "rx": "RX",
"tx": "TX", "tx": "TX",

View file

@ -66,11 +66,6 @@
"wait": "Please wait", "wait": "Please wait",
"empty_data": "Subsystem status unknown" "empty_data": "Subsystem status unknown"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "RX", "rx": "RX",
"tx": "TX", "tx": "TX",

View file

@ -66,11 +66,6 @@
"wait": "Veuillez patienter", "wait": "Veuillez patienter",
"empty_data": "Statut du sous-système inconnu" "empty_data": "Statut du sous-système inconnu"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "Rx", "rx": "Rx",
"tx": "Tx", "tx": "Tx",

View file

@ -66,11 +66,6 @@
"wait": "נא להמתין", "wait": "נא להמתין",
"empty_data": "מצב תת-מערכת לא ידוע" "empty_data": "מצב תת-מערכת לא ידוע"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "RX", "rx": "RX",
"tx": "TX", "tx": "TX",

View file

@ -66,11 +66,6 @@
"wait": "Please wait", "wait": "Please wait",
"empty_data": "Subsystem status unknown" "empty_data": "Subsystem status unknown"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "RX", "rx": "RX",
"tx": "TX", "tx": "TX",

View file

@ -66,11 +66,6 @@
"wait": "Pričekaj", "wait": "Pričekaj",
"empty_data": "Stanje podsustava nepoznato" "empty_data": "Stanje podsustava nepoznato"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "RX", "rx": "RX",
"tx": "TX", "tx": "TX",

View file

@ -66,11 +66,6 @@
"wait": "Kérjük várjon", "wait": "Kérjük várjon",
"empty_data": "Az alrendszer állapota ismeretlen" "empty_data": "Az alrendszer állapota ismeretlen"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "RX", "rx": "RX",
"tx": "TX", "tx": "TX",

View file

@ -66,11 +66,6 @@
"wait": "Please wait", "wait": "Please wait",
"empty_data": "Status subsistem tdk diketahui" "empty_data": "Status subsistem tdk diketahui"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "RX", "rx": "RX",
"tx": "TX", "tx": "TX",

View file

@ -66,11 +66,6 @@
"wait": "Please wait", "wait": "Please wait",
"empty_data": "Stato del sottosistema sconosciuto" "empty_data": "Stato del sottosistema sconosciuto"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "RX", "rx": "RX",
"tx": "TX", "tx": "TX",

View file

@ -66,11 +66,6 @@
"wait": "お待ちください", "wait": "お待ちください",
"empty_data": "サブシステムの状態は不明" "empty_data": "サブシステムの状態は不明"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "受信済み", "rx": "受信済み",
"tx": "送信済み", "tx": "送信済み",

View file

@ -66,11 +66,6 @@
"wait": "잠시만 기다려주세요", "wait": "잠시만 기다려주세요",
"empty_data": "서브시스템 상태 알 수 없음" "empty_data": "서브시스템 상태 알 수 없음"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "수신", "rx": "수신",
"tx": "송신", "tx": "송신",
@ -113,14 +108,14 @@
"songs": "음악" "songs": "음악"
}, },
"jellyfin": { "jellyfin": {
"playing": "재생 중", "playing": "Playing",
"transcoding": "트랜스코딩 중", "transcoding": "Transcoding",
"bitrate": "비트레이트", "bitrate": "Bitrate",
"no_active": "활성 스트림 없음", "no_active": "No Active Streams",
"movies": "영상", "movies": "Movies",
"series": "시리즈", "series": "Series",
"episodes": "에피소드", "episodes": "Episodes",
"songs": "음악" "songs": "Songs"
}, },
"esphome": { "esphome": {
"offline": "오프라인", "offline": "오프라인",
@ -190,11 +185,11 @@
"plex_connection_error": "Plex 연결 확인" "plex_connection_error": "Plex 연결 확인"
}, },
"tracearr": { "tracearr": {
"no_active": "활성 스트림 없음", "no_active": "No Active Streams",
"streams": "스트림", "streams": "Streams",
"transcodes": "트랜스코드", "transcodes": "Transcodes",
"directplay": "다이렉트 플레이", "directplay": "Direct Play",
"bitrate": "비트레이트" "bitrate": "Bitrate"
}, },
"omada": { "omada": {
"connectedAp": "연결된 AP", "connectedAp": "연결된 AP",
@ -295,12 +290,12 @@
"available": "이용 가능" "available": "이용 가능"
}, },
"seerr": { "seerr": {
"pending": "대기 중", "pending": "Pending",
"approved": "승인됨", "approved": "Approved",
"available": "사용 가능", "available": "Available",
"completed": "완료됨", "completed": "Completed",
"processing": "처리 중", "processing": "Processing",
"issues": "열린 이슈" "issues": "Open Issues"
}, },
"netalertx": { "netalertx": {
"total": "전체", "total": "전체",
@ -551,7 +546,7 @@
"up": "업", "up": "업",
"pending": "대기 중", "pending": "대기 중",
"down": "다운", "down": "다운",
"ok": "확인" "ok": "Ok"
}, },
"healthchecks": { "healthchecks": {
"new": "신규", "new": "신규",
@ -623,9 +618,9 @@
"sites": "Sites", "sites": "Sites",
"resources": "Resources", "resources": "Resources",
"targets": "Targets", "targets": "Targets",
"traffic": "트래픽", "traffic": "Traffic",
"in": "수신", "in": "In",
"out": "송신" "out": "Out"
}, },
"peanut": { "peanut": {
"battery_charge": "배터리 충전", "battery_charge": "배터리 충전",
@ -724,8 +719,8 @@
"volumeAvailable": "사용 가능" "volumeAvailable": "사용 가능"
}, },
"dispatcharr": { "dispatcharr": {
"channels": "채널", "channels": "Channels",
"streams": "스트림" "streams": "Streams"
}, },
"mylar": { "mylar": {
"series": "시리즈", "series": "시리즈",
@ -792,7 +787,7 @@
"gross_percent_today": "오늘", "gross_percent_today": "오늘",
"gross_percent_1y": "1년", "gross_percent_1y": "1년",
"gross_percent_max": "전체 기간", "gross_percent_max": "전체 기간",
"net_worth": "순자산" "net_worth": "Net Worth"
}, },
"audiobookshelf": { "audiobookshelf": {
"podcasts": "팟캐스트", "podcasts": "팟캐스트",
@ -816,10 +811,10 @@
"series": "시리즈" "series": "시리즈"
}, },
"booklore": { "booklore": {
"libraries": "라이브러리", "libraries": "Libraries",
"books": "", "books": "Books",
"reading": "읽는 중", "reading": "Reading",
"finished": "완료" "finished": "Finished"
}, },
"jdownloader": { "jdownloader": {
"downloadCount": "대기열", "downloadCount": "대기열",
@ -1155,30 +1150,30 @@
"bytes_added_30": "추가된 용량" "bytes_added_30": "추가된 용량"
}, },
"yourspotify": { "yourspotify": {
"songs": "음악", "songs": "Songs",
"time": "시간", "time": "Time",
"artists": "아티스트" "artists": "Artists"
}, },
"arcane": { "arcane": {
"containers": "컨테이너", "containers": "Containers",
"images": "이미지", "images": "Images",
"image_updates": "이미지 업데이트", "image_updates": "Image Updates",
"images_unused": "미사용", "images_unused": "Unused",
"environment_required": "환경 ID 필요" "environment_required": "Environment ID Required"
}, },
"dockhand": { "dockhand": {
"running": "실행 중", "running": "Running",
"stopped": "정지됨", "stopped": "Stopped",
"cpu": "CPU", "cpu": "CPU",
"memory": "메모리", "memory": "Memory",
"images": "이미지", "images": "Images",
"volumes": "볼륨", "volumes": "Volumes",
"events_today": "오늘의 이벤트", "events_today": "Events Today",
"pending_updates": "대기 중인 업데이트", "pending_updates": "Pending Updates",
"stacks": "스택", "stacks": "Stacks",
"paused": "일시정지됨", "paused": "Paused",
"total": "전체", "total": "Total",
"environment_not_found": "환경 없음" "environment_not_found": "Environment Not Found"
}, },
"sparkyfitness": { "sparkyfitness": {
"eaten": "Eaten", "eaten": "Eaten",

View file

@ -66,11 +66,6 @@
"wait": "Please wait", "wait": "Please wait",
"empty_data": "Subsystem status unknown" "empty_data": "Subsystem status unknown"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "RX", "rx": "RX",
"tx": "TX", "tx": "TX",

View file

@ -66,11 +66,6 @@
"wait": "Please wait", "wait": "Please wait",
"empty_data": "Status subsistem tak diketahui" "empty_data": "Status subsistem tak diketahui"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "RX", "rx": "RX",
"tx": "TX", "tx": "TX",

View file

@ -66,11 +66,6 @@
"wait": "Even geduld", "wait": "Even geduld",
"empty_data": "Subsysteem status onbekend" "empty_data": "Subsysteem status onbekend"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "RX", "rx": "RX",
"tx": "TX", "tx": "TX",

View file

@ -66,11 +66,6 @@
"wait": "Please wait", "wait": "Please wait",
"empty_data": "Ukjent undersystemstatus" "empty_data": "Ukjent undersystemstatus"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "RX", "rx": "RX",
"tx": "TX", "tx": "TX",

View file

@ -66,11 +66,6 @@
"wait": "Proszę czekać", "wait": "Proszę czekać",
"empty_data": "Status podsystemu nieznany" "empty_data": "Status podsystemu nieznany"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "Rx", "rx": "Rx",
"tx": "Tx", "tx": "Tx",

View file

@ -66,11 +66,6 @@
"wait": "Please wait", "wait": "Please wait",
"empty_data": "Status de Subsistema Desconhecido" "empty_data": "Status de Subsistema Desconhecido"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "Rx", "rx": "Rx",
"tx": "Tx", "tx": "Tx",

View file

@ -14,7 +14,7 @@
"date": "{{value, date}}", "date": "{{value, date}}",
"relativeDate": "{{value, relativeDate}}", "relativeDate": "{{value, relativeDate}}",
"duration": "{{value, duration}}", "duration": "{{value, duration}}",
"months": "mo", "months": "M",
"days": "d", "days": "d",
"hours": "h", "hours": "h",
"minutes": "m", "minutes": "m",
@ -66,14 +66,9 @@
"wait": "Por favor, aguarde", "wait": "Por favor, aguarde",
"empty_data": "Status do Subsistema desconhecido" "empty_data": "Status do Subsistema desconhecido"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "RX", "rx": "Rx",
"tx": "TX", "tx": "Tx",
"mem": "MEM", "mem": "MEM",
"cpu": "CPU", "cpu": "CPU",
"running": "Executando", "running": "Executando",
@ -106,21 +101,21 @@
"playing": "A reproduzir", "playing": "A reproduzir",
"transcoding": "Transcodificação", "transcoding": "Transcodificação",
"bitrate": "Taxa de bits", "bitrate": "Taxa de bits",
"no_active": "Sem Transmissões Ativas", "no_active": "Sem Streams Ativos",
"movies": "Filmes", "movies": "Filmes",
"series": "Séries", "series": "Séries",
"episodes": "Episódios", "episodes": "Episódios",
"songs": "Canções" "songs": "Canções"
}, },
"jellyfin": { "jellyfin": {
"playing": "Jogando", "playing": "Playing",
"transcoding": "Transcoding", "transcoding": "Transcoding",
"bitrate": "Bitrate", "bitrate": "Bitrate",
"no_active": "No Active Streams", "no_active": "No Active Streams",
"movies": "Filmes", "movies": "Movies",
"series": "Séries", "series": "Series",
"episodes": "Episódios", "episodes": "Episodes",
"songs": "Músicas" "songs": "Songs"
}, },
"esphome": { "esphome": {
"offline": "Offline", "offline": "Offline",
@ -295,12 +290,12 @@
"available": "Disponível" "available": "Disponível"
}, },
"seerr": { "seerr": {
"pending": "Pendente", "pending": "Pending",
"approved": "Aprovado", "approved": "Approved",
"available": "Disponível", "available": "Available",
"completed": "Concluído", "completed": "Completed",
"processing": "Processando", "processing": "Processing",
"issues": "Erros pendentes" "issues": "Open Issues"
}, },
"netalertx": { "netalertx": {
"total": "Total", "total": "Total",
@ -621,7 +616,7 @@
"pangolin": { "pangolin": {
"orgs": "Orgs", "orgs": "Orgs",
"sites": "Sites", "sites": "Sites",
"resources": "Recursos", "resources": "Resources",
"targets": "Targets", "targets": "Targets",
"traffic": "Traffic", "traffic": "Traffic",
"in": "In", "in": "In",
@ -724,8 +719,8 @@
"volumeAvailable": "Disponível" "volumeAvailable": "Disponível"
}, },
"dispatcharr": { "dispatcharr": {
"channels": "Canais", "channels": "Channels",
"streams": "Transmissões" "streams": "Streams"
}, },
"mylar": { "mylar": {
"series": "Séries", "series": "Séries",
@ -816,10 +811,10 @@
"series": "Séries" "series": "Séries"
}, },
"booklore": { "booklore": {
"libraries": "Bibliotecas", "libraries": "Libraries",
"books": "Livros", "books": "Books",
"reading": "Lendo", "reading": "Reading",
"finished": "Finalizado" "finished": "Finished"
}, },
"jdownloader": { "jdownloader": {
"downloadCount": "Fila de espera", "downloadCount": "Fila de espera",
@ -1160,23 +1155,23 @@
"artists": "Artistas" "artists": "Artistas"
}, },
"arcane": { "arcane": {
"containers": "Recipientes", "containers": "Containers",
"images": "Imagens", "images": "Images",
"image_updates": "Atualizações de Imagem", "image_updates": "Image Updates",
"images_unused": "Não utilizado", "images_unused": "Unused",
"environment_required": "Environment ID Required" "environment_required": "Environment ID Required"
}, },
"dockhand": { "dockhand": {
"running": "Executando", "running": "Running",
"stopped": "Stopped", "stopped": "Stopped",
"cpu": "CPU", "cpu": "CPU",
"memory": "Memória", "memory": "Memory",
"images": "Imagens", "images": "Images",
"volumes": "Quantidades", "volumes": "Volumes",
"events_today": "Eventos hoje", "events_today": "Events Today",
"pending_updates": "Atualizações pendentes", "pending_updates": "Pending Updates",
"stacks": "Pilhas", "stacks": "Stacks",
"paused": "Pausado", "paused": "Paused",
"total": "Total", "total": "Total",
"environment_not_found": "Environment Not Found" "environment_not_found": "Environment Not Found"
}, },

View file

@ -66,11 +66,6 @@
"wait": "Please wait", "wait": "Please wait",
"empty_data": "Starea subsistemului este necunoscut" "empty_data": "Starea subsistemului este necunoscut"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "RX", "rx": "RX",
"tx": "TX", "tx": "TX",

View file

@ -66,11 +66,6 @@
"wait": "Пожалуйста, подождите", "wait": "Пожалуйста, подождите",
"empty_data": "Статус подсистемы неизвестен" "empty_data": "Статус подсистемы неизвестен"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "RX", "rx": "RX",
"tx": "TX", "tx": "TX",

View file

@ -66,11 +66,6 @@
"wait": "Čakajte, prosím", "wait": "Čakajte, prosím",
"empty_data": "Stav podsystému neznámy" "empty_data": "Stav podsystému neznámy"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "Prijaté", "rx": "Prijaté",
"tx": "Odoslané", "tx": "Odoslané",

View file

@ -66,11 +66,6 @@
"wait": "Please wait", "wait": "Please wait",
"empty_data": "Neznani status podsistema" "empty_data": "Neznani status podsistema"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "RX", "rx": "RX",
"tx": "TX", "tx": "TX",

View file

@ -66,11 +66,6 @@
"wait": "Молим сачекајте", "wait": "Молим сачекајте",
"empty_data": "Статус подсистема непознат" "empty_data": "Статус подсистема непознат"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "RX", "rx": "RX",
"tx": "TX", "tx": "TX",

View file

@ -66,11 +66,6 @@
"wait": "Please wait", "wait": "Please wait",
"empty_data": "Subsystem status unknown" "empty_data": "Subsystem status unknown"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "RX", "rx": "RX",
"tx": "TX", "tx": "TX",

View file

@ -66,11 +66,6 @@
"wait": "Please wait", "wait": "Please wait",
"empty_data": "Subsystem status unknown" "empty_data": "Subsystem status unknown"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "RX", "rx": "RX",
"tx": "TX", "tx": "TX",

View file

@ -66,11 +66,6 @@
"wait": "Please wait", "wait": "Please wait",
"empty_data": "Subsystem status unknown" "empty_data": "Subsystem status unknown"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "RX", "rx": "RX",
"tx": "TX", "tx": "TX",

View file

@ -66,11 +66,6 @@
"wait": "Lütfen bekleyin", "wait": "Lütfen bekleyin",
"empty_data": "Alt sistem durumu bilinmiyor" "empty_data": "Alt sistem durumu bilinmiyor"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "Gelen Veri", "rx": "Gelen Veri",
"tx": "Giden Veri", "tx": "Giden Veri",

View file

@ -66,11 +66,6 @@
"wait": "Будь ласка, зачекайте", "wait": "Будь ласка, зачекайте",
"empty_data": "Статус підсистеми невідомий" "empty_data": "Статус підсистеми невідомий"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "RX", "rx": "RX",
"tx": "TX", "tx": "TX",

View file

@ -66,11 +66,6 @@
"wait": "Vui lòng chờ", "wait": "Vui lòng chờ",
"empty_data": "Trạng thái hệ thống phụ không xác định" "empty_data": "Trạng thái hệ thống phụ không xác định"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "RX", "rx": "RX",
"tx": "TX", "tx": "TX",

View file

@ -66,11 +66,6 @@
"wait": "Please wait", "wait": "Please wait",
"empty_data": "子系統狀態未知" "empty_data": "子系統狀態未知"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "接收", "rx": "接收",
"tx": "發送", "tx": "發送",

View file

@ -66,11 +66,6 @@
"wait": "请稍候", "wait": "请稍候",
"empty_data": "子系统状态未知" "empty_data": "子系统状态未知"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "接收", "rx": "接收",
"tx": "发送", "tx": "发送",

View file

@ -66,11 +66,6 @@
"wait": "Please wait", "wait": "Please wait",
"empty_data": "子系統狀態未知" "empty_data": "子系統狀態未知"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "接收", "rx": "接收",
"tx": "傳送", "tx": "傳送",

View file

@ -2,7 +2,7 @@ import { MdRefresh } from "react-icons/md";
export default function Revalidate() { export default function Revalidate() {
const revalidate = () => { const revalidate = () => {
fetch(`${process.env.NEXT_PUBLIC_BASE_PATH ?? ""}/api/revalidate`).then((res) => { fetch("/api/revalidate").then((res) => {
if (res.ok) { if (res.ok) {
window.location.reload(); window.location.reload();
} }

View file

@ -1,88 +0,0 @@
import { useState } from "react";
import useSWR from "swr";
const STATUS_COLORS = {
idle: "text-theme-500 dark:text-theme-400",
running: "text-blue-500 dark:text-blue-400",
success: "text-emerald-500 dark:text-emerald-400",
failed: "text-red-500 dark:text-red-400",
};
const STATUS_LABELS = {
idle: "Never deployed",
running: "Deploying...",
success: "Deployed",
failed: "Failed",
};
function formatTime(iso) {
if (!iso) return null;
const d = new Date(iso);
return d.toLocaleString("en-GB", { dateStyle: "short", timeStyle: "short", hourCycle: "h23" });
}
export default function Deploy({ options }) {
const { service, label, apiBase = "/deploy-api" } = options ?? {};
const bp = process.env.NEXT_PUBLIC_BASE_PATH ?? "";
// SWR middleware already prepends bp, so statusUrl must NOT include it
const statusUrl = service ? `${apiBase}/status/${service}` : null;
const { data, mutate } = useSWR(statusUrl, {
refreshInterval: (d) => (d?.status === "running" ? 2000 : 10000),
});
const [triggering, setTriggering] = useState(false);
const status = data?.status ?? "idle";
const lastRun = data?.last_run ? formatTime(data.last_run) : null;
const isRunning = status === "running";
const handleDeploy = async () => {
if (isRunning || triggering) return;
setTriggering(true);
try {
await fetch(`${bp}${apiBase}/deploy/${service}`, { method: "POST" });
await mutate();
} finally {
setTriggering(false);
}
};
if (!service) {
return (
<div className="flex flex-col items-center text-theme-500 text-xs p-2">
<span>No service configured</span>
</div>
);
}
return (
<div className="flex flex-col items-center justify-center p-2 gap-1 w-full">
<div className={`text-xs font-semibold ${STATUS_COLORS[status] ?? STATUS_COLORS.idle}`}>
{isRunning ? (
<span className="animate-pulse">{STATUS_LABELS.running}</span>
) : (
STATUS_LABELS[status] ?? status
)}
</div>
{lastRun && (
<div className="text-theme-500 dark:text-theme-400 text-xs opacity-75">{lastRun}</div>
)}
<button
type="button"
onClick={handleDeploy}
disabled={isRunning || triggering}
className={[
"mt-1 px-3 py-1 rounded text-xs font-medium transition-colors",
isRunning || triggering
? "bg-theme-300 dark:bg-theme-600 text-theme-500 dark:text-theme-400 cursor-not-allowed"
: "bg-theme-500 hover:bg-theme-600 dark:bg-theme-600 dark:hover:bg-theme-500 text-white cursor-pointer",
].join(" ")}
>
{isRunning ? "Running..." : `Deploy ${label ?? service}`}
</button>
</div>
);
}

View file

@ -106,7 +106,7 @@ export default function Search({ options }) {
query.trim().length > 0 && query.trim().length > 0 &&
query.trim() !== searchSuggestions[0] query.trim() !== searchSuggestions[0]
) { ) {
fetch(`${process.env.NEXT_PUBLIC_BASE_PATH ?? ""}/api/search/searchSuggestion?query=${encodeURIComponent(query)}&providerName=${selectedProvider.name}`, { fetch(`/api/search/searchSuggestion?query=${encodeURIComponent(query)}&providerName=${selectedProvider.name}`, {
signal: abortController.signal, signal: abortController.signal,
}) })
.then(async (searchSuggestionResult) => { .then(async (searchSuggestionResult) => {

View file

@ -15,7 +15,6 @@ const widgetMappings = {
longhorn: dynamic(() => import("components/widgets/longhorn/longhorn")), longhorn: dynamic(() => import("components/widgets/longhorn/longhorn")),
kubernetes: dynamic(() => import("components/widgets/kubernetes/kubernetes")), kubernetes: dynamic(() => import("components/widgets/kubernetes/kubernetes")),
stocks: dynamic(() => import("components/widgets/stocks/stocks")), stocks: dynamic(() => import("components/widgets/stocks/stocks")),
deploy: dynamic(() => import("components/widgets/deploy/deploy"), { ssr: false }),
}; };
export default function Widget({ widget, style }) { export default function Widget({ widget, style }) {

View file

@ -67,18 +67,11 @@ const tailwindSafelist = [
"2xl:h-0 2xl:h-1 2xl:h-2 2xl:h-3 2xl:h-4 2xl:h-5 2xl:h-6 2xl:h-7 2xl:h-8 2xl:h-9 2xl:h-10 2xl:h-11 2xl:h-12 2xl:h-13 2xl:h-14 2xl:h-15 2xl:h-16 2xl:h-17 2xl:h-18 2xl:h-19 2xl:h-20 2xl:h-21 2xl:h-22 2xl:h-23 2xl:h-24 2xl:h-25 2xl:h-26 2xl:h-27 2xl:h-28 2xl:h-29 2xl:h-30 2xl:h-31 2xl:h-32 2xl:h-33 2xl:h-34 2xl:h-35 2xl:h-36 2xl:h-37 2xl:h-38 2xl:h-39 2xl:h-40 2xl:h-41 2xl:h-42 2xl:h-43 2xl:h-44 2xl:h-45 2xl:h-46 2xl:h-47 2xl:h-48 2xl:h-49 2xl:h-50 2xl:h-51 2xl:h-52 2xl:h-53 2xl:h-54 2xl:h-55 2xl:h-56 2xl:h-57 2xl:h-58 2xl:h-59 2xl:h-60 2xl:h-61 2xl:h-62 2xl:h-63 2xl:h-64 2xl:h-65 2xl:h-66 2xl:h-67 2xl:h-68 2xl:h-69 2xl:h-70 2xl:h-71 2xl:h-72 2xl:h-73 2xl:h-74 2xl:h-75 2xl:h-76 2xl:h-77 2xl:h-78 2xl:h-79 2xl:h-80 2xl:h-81 2xl:h-82 2xl:h-83 2xl:h-84 2xl:h-85 2xl:h-86 2xl:h-87 2xl:h-88 2xl:h-89 2xl:h-90 2xl:h-91 2xl:h-92 2xl:h-93 2xl:h-94 2xl:h-95 2xl:h-96", "2xl:h-0 2xl:h-1 2xl:h-2 2xl:h-3 2xl:h-4 2xl:h-5 2xl:h-6 2xl:h-7 2xl:h-8 2xl:h-9 2xl:h-10 2xl:h-11 2xl:h-12 2xl:h-13 2xl:h-14 2xl:h-15 2xl:h-16 2xl:h-17 2xl:h-18 2xl:h-19 2xl:h-20 2xl:h-21 2xl:h-22 2xl:h-23 2xl:h-24 2xl:h-25 2xl:h-26 2xl:h-27 2xl:h-28 2xl:h-29 2xl:h-30 2xl:h-31 2xl:h-32 2xl:h-33 2xl:h-34 2xl:h-35 2xl:h-36 2xl:h-37 2xl:h-38 2xl:h-39 2xl:h-40 2xl:h-41 2xl:h-42 2xl:h-43 2xl:h-44 2xl:h-45 2xl:h-46 2xl:h-47 2xl:h-48 2xl:h-49 2xl:h-50 2xl:h-51 2xl:h-52 2xl:h-53 2xl:h-54 2xl:h-55 2xl:h-56 2xl:h-57 2xl:h-58 2xl:h-59 2xl:h-60 2xl:h-61 2xl:h-62 2xl:h-63 2xl:h-64 2xl:h-65 2xl:h-66 2xl:h-67 2xl:h-68 2xl:h-69 2xl:h-70 2xl:h-71 2xl:h-72 2xl:h-73 2xl:h-74 2xl:h-75 2xl:h-76 2xl:h-77 2xl:h-78 2xl:h-79 2xl:h-80 2xl:h-81 2xl:h-82 2xl:h-83 2xl:h-84 2xl:h-85 2xl:h-86 2xl:h-87 2xl:h-88 2xl:h-89 2xl:h-90 2xl:h-91 2xl:h-92 2xl:h-93 2xl:h-94 2xl:h-95 2xl:h-96",
]; ];
const basePathMiddleware = (useSWRNext) => (key, fetcher, config) => {
const bp = process.env.NEXT_PUBLIC_BASE_PATH ?? "";
const adjustedKey = bp && typeof key === "string" ? `${bp}${key}` : key;
return useSWRNext(adjustedKey, fetcher, config);
};
function MyApp({ Component, pageProps }) { function MyApp({ Component, pageProps }) {
return ( return (
<SWRConfig <SWRConfig
value={{ value={{
fetcher: (resource, init) => fetch(resource, init).then((res) => res.json()), fetcher: (resource, init) => fetch(resource, init).then((res) => res.json()),
use: [basePathMiddleware],
}} }}
> >
<Head> <Head>

View file

@ -6,8 +6,8 @@ export default function Document() {
<Head> <Head>
<meta name="mobile-web-app-capable" content="yes" /> <meta name="mobile-web-app-capable" content="yes" />
<link rel="manifest" href="/site.webmanifest?v=4" crossOrigin="use-credentials" /> <link rel="manifest" href="/site.webmanifest?v=4" crossOrigin="use-credentials" />
<link rel="preload" href={`${process.env.NEXT_PUBLIC_BASE_PATH ?? ""}/api/config/custom.css`} as="style" /> <link rel="preload" href="/api/config/custom.css" as="style" />
<link rel="stylesheet" href={`${process.env.NEXT_PUBLIC_BASE_PATH ?? ""}/api/config/custom.css`} /> {/* eslint-disable-line @next/next/no-css-tags */} <link rel="stylesheet" href="/api/config/custom.css" /> {/* eslint-disable-line @next/next/no-css-tags */}
</Head> </Head>
<body> <body>
<Main /> <Main />

View file

@ -63,15 +63,14 @@ export async function getStaticProps() {
const widgets = await widgetsResponse(); const widgets = await widgetsResponse();
const language = normalizeLanguage(settings.language); const language = normalizeLanguage(settings.language);
const bp = process.env.NEXT_PUBLIC_BASE_PATH ?? "";
return { return {
props: { props: {
initialSettings: settings, initialSettings: settings,
fallback: { fallback: {
[`${bp}/api/services`]: services, "/api/services": services,
[`${bp}/api/bookmarks`]: bookmarks, "/api/bookmarks": bookmarks,
[`${bp}/api/widgets`]: widgets, "/api/widgets": widgets,
[`${bp}/api/hash`]: false, "/api/hash": false,
}, },
...(await serverSideTranslations(language)), ...(await serverSideTranslations(language)),
}, },
@ -84,10 +83,10 @@ export async function getStaticProps() {
props: { props: {
initialSettings: {}, initialSettings: {},
fallback: { fallback: {
[`${process.env.NEXT_PUBLIC_BASE_PATH ?? ""}/api/services`]: [], "/api/services": [],
[`${process.env.NEXT_PUBLIC_BASE_PATH ?? ""}/api/bookmarks`]: [], "/api/bookmarks": [],
[`${process.env.NEXT_PUBLIC_BASE_PATH ?? ""}/api/widgets`]: [], "/api/widgets": [],
[`${process.env.NEXT_PUBLIC_BASE_PATH ?? ""}/api/hash`]: false, "/api/hash": false,
}, },
...(await serverSideTranslations("en")), ...(await serverSideTranslations("en")),
}, },
@ -121,7 +120,7 @@ function Index({ initialSettings, fallback }) {
setStale(true); setStale(true);
localStorage.setItem("hash", hashData.hash); localStorage.setItem("hash", hashData.hash);
fetch(`${process.env.NEXT_PUBLIC_BASE_PATH ?? ""}/api/revalidate`).then((res) => { fetch("/api/revalidate").then((res) => {
if (res.ok) { if (res.ok) {
window.location.reload(); window.location.reload();
} }
@ -435,7 +434,7 @@ function Home({ initialSettings }) {
<meta name="color-scheme" content="dark light"></meta> <meta name="color-scheme" content="dark light"></meta>
</Head> </Head>
<Script src={`${process.env.NEXT_PUBLIC_BASE_PATH ?? ""}/api/config/custom.js`} /> <Script src="/api/config/custom.js" />
<div <div
className={classNames( className={classNames(

View file

@ -435,11 +435,6 @@ export function cleanServiceGroups(groups) {
// grafana // grafana
alerts, alerts,
// deploy
service: deployService,
label: deployLabel,
apiBase: deployApiBase,
} = widgetData; } = widgetData;
let fieldsList = fields; let fieldsList = fields;
@ -690,11 +685,6 @@ export function cleanServiceGroups(groups) {
if (type === "grafana") { if (type === "grafana") {
if (alerts) widget.alerts = alerts; if (alerts) widget.alerts = alerts;
} }
if (type === "deploy") {
if (deployService) widget.service = deployService;
if (deployLabel) widget.label = deployLabel;
if (deployApiBase) widget.apiBase = deployApiBase;
}
if (type === "unraid") { if (type === "unraid") {
if (pool1) widget.pool1 = pool1; if (pool1) widget.pool1 = pool1;
if (pool2) widget.pool2 = pool2; if (pool2) widget.pool2 = pool2;

View file

@ -29,7 +29,7 @@ export function formatProxyUrl(widget, endpoint, queryParams) {
if (queryParams) { if (queryParams) {
params.append("query", JSON.stringify(queryParams)); params.append("query", JSON.stringify(queryParams));
} }
return `${process.env.NEXT_PUBLIC_BASE_PATH ?? ""}/api/services/proxy?${params.toString()}`; return `/api/services/proxy?${params.toString()}`;
} }
export function asJson(data) { export function asJson(data) {

View file

@ -26,7 +26,6 @@ const components = {
iframe: dynamic(() => import("./iframe/component")), iframe: dynamic(() => import("./iframe/component")),
customapi: dynamic(() => import("./customapi/component")), customapi: dynamic(() => import("./customapi/component")),
deluge: dynamic(() => import("./deluge/component")), deluge: dynamic(() => import("./deluge/component")),
deploy: dynamic(() => import("./deploy/component")),
develancacheui: dynamic(() => import("./develancacheui/component")), develancacheui: dynamic(() => import("./develancacheui/component")),
diskstation: dynamic(() => import("./diskstation/component")), diskstation: dynamic(() => import("./diskstation/component")),
dispatcharr: dynamic(() => import("./dispatcharr/component")), dispatcharr: dynamic(() => import("./dispatcharr/component")),
@ -148,7 +147,6 @@ const components = {
tubearchivist: dynamic(() => import("./tubearchivist/component")), tubearchivist: dynamic(() => import("./tubearchivist/component")),
truenas: dynamic(() => import("./truenas/component")), truenas: dynamic(() => import("./truenas/component")),
unifi: dynamic(() => import("./unifi/component")), unifi: dynamic(() => import("./unifi/component")),
unifi_drive: dynamic(() => import("./unifi_drive/component")),
unmanic: dynamic(() => import("./unmanic/component")), unmanic: dynamic(() => import("./unmanic/component")),
unraid: dynamic(() => import("./unraid/component")), unraid: dynamic(() => import("./unraid/component")),
uptimekuma: dynamic(() => import("./uptimekuma/component")), uptimekuma: dynamic(() => import("./uptimekuma/component")),

View file

@ -1,78 +0,0 @@
import { useState } from "react";
import useSWR from "swr";
const STATUS_COLORS = {
idle: "text-theme-500 dark:text-theme-400",
running: "text-blue-500 dark:text-blue-400",
success: "text-emerald-500 dark:text-emerald-400",
failed: "text-red-500 dark:text-red-400",
};
const STATUS_LABELS = {
idle: "Never deployed",
running: "Deploying...",
success: "Deployed",
failed: "Failed",
};
function formatTime(iso) {
if (!iso) return null;
const d = new Date(iso);
return d.toLocaleString("en-GB", { dateStyle: "short", timeStyle: "short", hourCycle: "h23" });
}
export default function DeployComponent({ service }) {
const { service: svcName, label, apiBase = "/deploy-api" } = service?.widget ?? {};
const bp = process.env.NEXT_PUBLIC_BASE_PATH ?? "";
// SWR middleware already prepends bp, so statusUrl must NOT include it
const statusUrl = svcName ? `${apiBase}/status/${svcName}` : null;
const { data, mutate } = useSWR(statusUrl, {
refreshInterval: (d) => (d?.status === "running" ? 2000 : 10000),
});
const [triggering, setTriggering] = useState(false);
const status = data?.status ?? "idle";
const lastRun = data?.last_run ? formatTime(data.last_run) : null;
const isRunning = status === "running";
const handleDeploy = async () => {
if (isRunning || triggering) return;
setTriggering(true);
try {
await fetch(`${bp}${apiBase}/deploy/${svcName}`, { method: "POST" });
await mutate();
} finally {
setTriggering(false);
}
};
if (!svcName) return null;
return (
<div className="flex flex-row items-center justify-between px-2 py-1 gap-2 w-full">
<div className="flex flex-col">
<span className={`text-xs font-semibold ${STATUS_COLORS[status] ?? STATUS_COLORS.idle}`}>
{isRunning ? <span className="animate-pulse">{STATUS_LABELS.running}</span> : (STATUS_LABELS[status] ?? status)}
</span>
{lastRun && (
<span className="text-theme-500 dark:text-theme-400 text-xs opacity-60">{lastRun}</span>
)}
</div>
<button
type="button"
onClick={handleDeploy}
disabled={isRunning || triggering}
className={[
"px-3 py-1 rounded text-xs font-medium transition-colors shrink-0",
isRunning || triggering
? "bg-theme-300 dark:bg-theme-600 text-theme-500 dark:text-theme-400 cursor-not-allowed"
: "bg-theme-500 hover:bg-theme-600 dark:bg-theme-600 dark:hover:bg-theme-500 text-white cursor-pointer",
].join(" ")}
>
{isRunning ? "Running..." : `Deploy ${label ?? svcName}`}
</button>
</div>
);
}

View file

@ -12,7 +12,7 @@ async function login(widget) {
const loginParams = { const loginParams = {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: "{}", body: null,
}; };
if (widget.username && widget.password) { if (widget.username && widget.password) {

View file

@ -45,7 +45,7 @@ describe("widgets/flood/proxy", () => {
expect(httpProxy).toHaveBeenCalledTimes(3); expect(httpProxy).toHaveBeenCalledTimes(3);
expect(httpProxy.mock.calls[0][0].toString()).toBe("http://flood/api/stats"); expect(httpProxy.mock.calls[0][0].toString()).toBe("http://flood/api/stats");
expect(httpProxy.mock.calls[1][0]).toBe("http://flood/api/auth/authenticate"); expect(httpProxy.mock.calls[1][0]).toBe("http://flood/api/auth/authenticate");
expect(httpProxy.mock.calls[1][1].body).toBe("{}"); expect(httpProxy.mock.calls[1][1].body).toBeNull();
expect(httpProxy.mock.calls[2][0].toString()).toBe("http://flood/api/stats"); expect(httpProxy.mock.calls[2][0].toString()).toBe("http://flood/api/stats");
expect(res.statusCode).toBe(200); expect(res.statusCode).toBe(200);
expect(res.body).toEqual(Buffer.from("data")); expect(res.body).toEqual(Buffer.from("data"));

View file

@ -1,58 +0,0 @@
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: storageData, error: storageError } = useWidgetAPI(widget, "storage");
if (storageError) {
return <Container service={service} error={storageError} />;
}
if (!storageData) {
return (
<Container service={service}>
<Block field="unifi_drive.total" label="resources.total" />
<Block field="unifi_drive.used" label="resources.used" />
<Block field="unifi_drive.available" label="resources.free" />
<Block field="unifi_drive.status" label="widget.status" />
</Container>
);
}
const { data: storage } = storageData;
if (!storage) {
return (
<Container service={service}>
<Block value={t("unifi_drive.no_data")} />
</Container>
);
}
const { totalQuota, usage, status } = storage;
const totalBytes = totalQuota ?? 0;
const usedBytes = (usage?.system || 0) + (usage?.myDrives || 0) + (usage?.sharedDrives || 0);
const availableBytes = Math.max(0, totalBytes - usedBytes);
let statusValue = status;
if (status === "healthy") statusValue = t("unifi_drive.healthy");
else if (status === "degraded") statusValue = t("unifi_drive.degraded");
return (
<Container service={service}>
<Block field="unifi_drive.total" label="resources.total" value={t("common.bytes", { value: totalBytes })} />
<Block field="unifi_drive.used" label="resources.used" value={t("common.bytes", { value: usedBytes })} />
<Block
field="unifi_drive.available"
label="resources.free"
value={t("common.bytes", { value: availableBytes })}
/>
<Block field="unifi_drive.status" label="widget.status" value={statusValue} />
</Container>
);
}

View file

@ -1,92 +0,0 @@
// @vitest-environment jsdom
import { screen } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { renderWithProviders } from "test-utils/render-with-providers";
import { expectBlockValue } from "test-utils/widget-assertions";
const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
import Component from "./component";
describe("widgets/unifi_drive/component", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("renders placeholders while loading", () => {
useWidgetAPI.mockReturnValue({ data: undefined, error: undefined });
const service = { widget: { type: "unifi_drive" } };
const { container } = renderWithProviders(<Component service={service} />, { settings: { hideErrors: false } });
expect(container.querySelectorAll(".service-block")).toHaveLength(4);
expect(screen.getByText("resources.total")).toBeInTheDocument();
expect(screen.getByText("resources.used")).toBeInTheDocument();
expect(screen.getByText("resources.free")).toBeInTheDocument();
expect(screen.getByText("widget.status")).toBeInTheDocument();
});
it("renders error when API fails", () => {
useWidgetAPI.mockReturnValue({ data: undefined, error: new Error("fail") });
const service = { widget: { type: "unifi_drive" } };
renderWithProviders(<Component service={service} />, { settings: { hideErrors: false } });
expect(screen.getAllByText("widget.api_error", { exact: false }).length).toBeGreaterThan(0);
});
it("renders no_data when storage data is missing", () => {
useWidgetAPI.mockReturnValue({ data: { data: null }, error: undefined });
const service = { widget: { type: "unifi_drive" } };
renderWithProviders(<Component service={service} />, { settings: { hideErrors: false } });
expect(screen.getByText("unifi_drive.no_data")).toBeInTheDocument();
});
it("renders storage statistics when data is loaded", () => {
useWidgetAPI.mockReturnValue({
data: {
data: {
totalQuota: 1000000000000,
usage: { system: 100000000000, myDrives: 200000000000, sharedDrives: 50000000000 },
status: "healthy",
},
},
error: undefined,
});
const service = { widget: { type: "unifi_drive" } };
const { container } = renderWithProviders(<Component service={service} />, { settings: { hideErrors: false } });
expect(container.querySelectorAll(".service-block")).toHaveLength(4);
expectBlockValue(container, "resources.total", 1000000000000);
expectBlockValue(container, "resources.used", 350000000000);
expectBlockValue(container, "resources.free", 650000000000);
expectBlockValue(container, "widget.status", "unifi_drive.healthy");
});
it("renders degraded status", () => {
useWidgetAPI.mockReturnValue({
data: {
data: {
totalQuota: 100,
usage: { system: 10, myDrives: 20, sharedDrives: 5 },
status: "degraded",
},
},
error: undefined,
});
const service = { widget: { type: "unifi_drive" } };
const { container } = renderWithProviders(<Component service={service} />, { settings: { hideErrors: false } });
expect(container.querySelectorAll(".service-block")).toHaveLength(4);
expectBlockValue(container, "widget.status", "unifi_drive.degraded");
expectBlockValue(container, "resources.free", 65);
});
});

View file

@ -1,36 +0,0 @@
import getServiceWidget from "utils/config/service-helpers";
import createUnifiProxyHandler from "utils/proxy/handlers/unifi";
import { httpProxy } from "utils/proxy/http";
const drivePrefix = "/proxy/drive";
async function getWidget(req, logger) {
const { group, service, index } = req.query;
if (!group || !service) return null;
const widget = await getServiceWidget(group, service, index);
if (!widget) {
logger.debug("Invalid or missing widget for service '%s' in group '%s'", service, group);
return null;
}
return widget;
}
async function resolveRequestContext({ cachedPrefix, widget }) {
if (cachedPrefix !== null) {
return { prefix: cachedPrefix };
}
const [, , , responseHeaders] = await httpProxy(widget.url);
return {
prefix: drivePrefix,
csrfToken: responseHeaders?.["x-csrf-token"],
};
}
export default createUnifiProxyHandler({
proxyName: "unifiDriveProxyHandler",
resolveWidget: getWidget,
resolveRequestContext,
});

View file

@ -1,82 +0,0 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import createMockRes from "test-utils/create-mock-res";
const { httpProxy, getServiceWidget, cache, logger } = vi.hoisted(() => {
const store = new Map();
return {
httpProxy: vi.fn(),
getServiceWidget: vi.fn(),
cache: {
get: vi.fn((k) => (store.has(k) ? store.get(k) : null)),
put: vi.fn((k, v) => store.set(k, v)),
del: vi.fn((k) => store.delete(k)),
_reset: () => store.clear(),
},
logger: { debug: vi.fn(), error: vi.fn() },
};
});
vi.mock("memory-cache", () => ({ default: cache, ...cache }));
vi.mock("utils/logger", () => ({ default: () => logger }));
vi.mock("utils/config/service-helpers", () => ({ default: getServiceWidget }));
vi.mock("utils/proxy/http", () => ({ httpProxy }));
vi.mock("widgets/widgets", () => ({
default: { unifi_drive: { api: "{url}{prefix}/api/{endpoint}" } },
}));
import unifiDriveProxyHandler from "./proxy";
const widgetConfig = { type: "unifi_drive", url: "http://unifi", username: "u", password: "p" };
describe("widgets/unifi_drive/proxy", () => {
beforeEach(() => {
vi.clearAllMocks();
cache._reset();
});
it("returns 400 when widget config is missing", async () => {
getServiceWidget.mockResolvedValue(null);
const res = createMockRes();
await unifiDriveProxyHandler(
{ query: { group: "g", service: "s", endpoint: "v1/systems/storage?type=detail" } },
res,
);
expect(res.statusCode).toBe(400);
});
it("returns 403 when widget type has no API config", async () => {
getServiceWidget.mockResolvedValue({ ...widgetConfig, type: "unknown" });
const res = createMockRes();
await unifiDriveProxyHandler({ query: { group: "g", service: "s", endpoint: "storage" } }, res);
expect(res.statusCode).toBe(403);
});
it("uses /proxy/drive prefix and returns data on success", async () => {
getServiceWidget.mockResolvedValue({ ...widgetConfig });
httpProxy
.mockResolvedValueOnce([200, "text/html", Buffer.from(""), {}])
.mockResolvedValueOnce([200, "application/json", Buffer.from('{"data":{}}'), {}]);
const res = createMockRes();
await unifiDriveProxyHandler({ query: { group: "g", service: "s", endpoint: "storage" } }, res);
expect(httpProxy.mock.calls[0][0]).toBe("http://unifi");
expect(httpProxy.mock.calls[1][0].toString()).toContain("/proxy/drive/api/");
expect(cache.put).toHaveBeenCalledWith("unifiDriveProxyHandler__prefix.s", "/proxy/drive");
expect(res.statusCode).toBe(200);
});
it("skips prefix detection when cached", async () => {
getServiceWidget.mockResolvedValue({ ...widgetConfig });
cache.put("unifiDriveProxyHandler__prefix.s", "/proxy/drive");
httpProxy.mockResolvedValueOnce([200, "application/json", Buffer.from('{"data":{}}'), {}]);
const res = createMockRes();
await unifiDriveProxyHandler({ query: { group: "g", service: "s", endpoint: "storage" } }, res);
expect(httpProxy).toHaveBeenCalledTimes(1);
expect(httpProxy.mock.calls[0][0].toString()).toContain("/proxy/drive/api/");
expect(res.statusCode).toBe(200);
});
});

View file

@ -1,14 +0,0 @@
import unifiDriveProxyHandler from "./proxy";
const widget = {
api: "{url}{prefix}/api/{endpoint}",
proxyHandler: unifiDriveProxyHandler,
mappings: {
storage: {
endpoint: "v1/systems/storage?type=detail",
},
},
};
export default widget;

View file

@ -1,11 +0,0 @@
import { describe, it } from "vitest";
import { expectWidgetConfigShape } from "test-utils/widget-config";
import widget from "./widget";
describe("unifi_drive widget config", () => {
it("exports a valid widget config", () => {
expectWidgetConfigShape(widget);
});
});

View file

@ -137,7 +137,6 @@ import trilium from "./trilium/widget";
import truenas from "./truenas/widget"; import truenas from "./truenas/widget";
import tubearchivist from "./tubearchivist/widget"; import tubearchivist from "./tubearchivist/widget";
import unifi from "./unifi/widget"; import unifi from "./unifi/widget";
import unifi_drive from "./unifi_drive/widget";
import unmanic from "./unmanic/widget"; import unmanic from "./unmanic/widget";
import unraid from "./unraid/widget"; import unraid from "./unraid/widget";
import uptimekuma from "./uptimekuma/widget"; import uptimekuma from "./uptimekuma/widget";
@ -297,7 +296,6 @@ const widgets = {
truenas, truenas,
unifi, unifi,
unifi_console: unifi, unifi_console: unifi,
unifi_drive,
unmanic, unmanic,
unraid, unraid,
uptimekuma, uptimekuma,