Compare commits

...

64 Commits

Author SHA1 Message Date
shamoon
a1c1c3343f Cleanup gitlab test
[ci skip]
2025-03-30 20:22:49 -07:00
shamoon
1c6820428d Remove error mocking 2025-03-30 20:07:00 -07:00
shamoon
fa8ebc56ef Disable coverage for now 2025-03-30 20:03:58 -07:00
shamoon
b04ed9d628 Just save this
[ci skip]
2025-03-30 18:44:51 -07:00
shamoon
954ab54493 Speed up CI: Skip unnecessary build steps, optimize caching etc (#5098) 2025-03-30 08:52:23 -07:00
Chris
30cb893354 Fix: remove unneeded import from Hoarder widget.js (#5097)
Some checks failed
Crowdin Action / Crowdin Sync (push) Has been cancelled
Docker / Linting Checks (push) Has been cancelled
Docker / Docker Build & Push (push) Has been cancelled
Repository Maintenance / Stale (push) Has been cancelled
Repository Maintenance / Lock Old Threads (push) Has been cancelled
Repository Maintenance / Close Answered Discussions (push) Has been cancelled
Repository Maintenance / Close Outdated Discussions (push) Has been cancelled
Repository Maintenance / Close Unsupported Feature Requests (push) Has been cancelled
2025-03-29 19:49:53 -07:00
shamoon
4567427b9c Enhancement: add shvl fallback for custom api dynamic list (#5091)
Some checks are pending
Docker / Linting Checks (push) Waiting to run
Docker / Docker Build & Push (push) Blocked by required conditions
2025-03-28 16:32:56 -07:00
Matheus Vellone
ee7e8fab61 Fix: support shvl on customapi dynamic list target (#5081)
Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
2025-03-28 16:29:23 -07:00
shamoon
313835c0e6 Bump version to 1.1.1
Some checks failed
Docker / Linting Checks (push) Has been cancelled
Docs / Linting Checks (push) Has been cancelled
Docker / Docker Build & Push (push) Has been cancelled
Docs / Test Build (push) Has been cancelled
Docs / Build & Deploy (push) Has been cancelled
2025-03-27 20:20:02 -07:00
shamoon
999dade861 Fix: add fallback for shvl syntax (#5080)
Some checks are pending
Docker / Linting Checks (push) Waiting to run
Docker / Docker Build & Push (push) Blocked by required conditions
2025-03-27 20:16:50 -07:00
shamoon
2cabe77b55 Fix: add fallback for shvl syntax 2025-03-27 20:15:39 -07:00
shamoon
0a50d5a6aa Merge branch 'main' into dev 2025-03-27 13:27:26 -07:00
shamoon
e19dcc2729 Only deploy docs from main branch 2025-03-27 13:27:20 -07:00
shamoon
cadf1433af Bump version to 1.1.0 2025-03-27 13:22:02 -07:00
shamoon
6bae078a8d Merge branch 'dev' 2025-03-27 13:21:45 -07:00
github-actions[bot]
77bbdc6a04 New Crowdin translations by GitHub Action (#5010)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2025-03-27 13:21:33 -07:00
shamoon
1f8fd1c69d Add install pnpm to source instructions
Some checks failed
Docs / Linting Checks (push) Has been cancelled
Docs / Test Build (push) Has been cancelled
Docs / Build & Deploy (push) Has been cancelled
2025-03-23 07:11:20 -07:00
dependabot[bot]
5d557844cc Chore(deps): Bump next from 15.1.7 to 15.2.3 (#5055)
Some checks failed
Docker / Linting Checks (push) Has been cancelled
Docker / Docker Build & Push (push) Has been cancelled
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-22 07:49:16 -07:00
chiragkrishna
a4b07b91fe Enhancement: Add Repositories field to Gitea (#5053)
Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
2025-03-22 07:45:27 -07:00
Chris
d853bbfe44 Feature: Slskd Service Widget (#5045)
Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
2025-03-20 21:46:54 -07:00
IhatemyISP
7fd5e6ccb1 Tweak: change moonraker widget standby output (#4060)
Some checks failed
Docker / Linting Checks (push) Has been cancelled
Docker / Docker Build & Push (push) Has been cancelled
Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
2025-03-19 16:26:49 -07:00
shamoon
aed602ad70 Tweak: try to reduce rootless startup time (#5037)
Some checks are pending
Docker / Linting Checks (push) Waiting to run
Docker / Docker Build & Push (push) Blocked by required conditions
2025-03-18 19:29:43 -07:00
shamoon
fa28a11658 Fix: include new backdrop-blur-xs option (#5030)
Some checks are pending
Docker / Linting Checks (push) Waiting to run
Docker / Docker Build & Push (push) Blocked by required conditions
2025-03-17 23:30:07 -07:00
shamoon
a3b693e2b6 Support maxBookmarkGroupColumns too
Some checks are pending
Docker / Linting Checks (push) Waiting to run
Docker / Docker Build & Push (push) Blocked by required conditions
2025-03-17 13:03:04 -07:00
shamoon
708c4e64d1 Tweak this 2025-03-17 12:47:34 -07:00
shamoon
11a6b5d0b8 Enhancement: support maximum group cols up 8 (#5022) 2025-03-17 12:32:09 -07:00
shamoon
ce0102eb6f Enhancement: support full width container (#5021) 2025-03-17 12:25:10 -07:00
shamoon
42f1ed3c47 Update customapi.md
Some checks are pending
Docker / Linting Checks (push) Waiting to run
Docker / Docker Build & Push (push) Blocked by required conditions
2025-03-17 11:49:35 -07:00
shamoon
dca23e8547 Enhancement: support shvl syntax for customapi fields (#5020) 2025-03-17 11:47:55 -07:00
morliont
8d20f22932 Enhancement: support dynamic list rendering in custom api widget (#5012)
Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
2025-03-17 11:30:01 -07:00
shamoon
071b5275bc Documentation: fix install page buttons
Some checks failed
Docs / Linting Checks (push) Has been cancelled
Docs / Test Build (push) Has been cancelled
Docs / Build & Deploy (push) Has been cancelled
2025-03-17 11:19:27 -07:00
shamoon
de9c015f7f Fix: fix minecraft players after move to minecraftstatuspinger (#5017) 2025-03-17 07:24:12 -07:00
shamoon
b4dc53c7c0 Feature: allow disable ipv6 in proxy, refactor cacheFetch to use proxy (#5011)
Some checks are pending
Docker / Linting Checks (push) Waiting to run
Docker / Docker Build & Push (push) Blocked by required conditions
2025-03-16 20:09:34 -07:00
shamoon
934ad3a6f1 Fix: remove medusa widget trailing slash (#5007)
Some checks are pending
Docker / Linting Checks (push) Waiting to run
Docker / Docker Build & Push (push) Blocked by required conditions
2025-03-16 15:33:38 -07:00
shamoon
6fbe080b28 Bump version to 1.0.4
Some checks failed
Docs / Linting Checks (push) Waiting to run
Docs / Test Build (push) Blocked by required conditions
Docs / Build & Deploy (push) Blocked by required conditions
Docker / Linting Checks (push) Has been cancelled
Docker / Docker Build & Push (push) Has been cancelled
2025-03-16 14:13:12 -07:00
shamoon
4047f8d6e8 Merge branch 'dev' 2025-03-16 14:12:45 -07:00
github-actions[bot]
59cd5564f8 New Crowdin translations by GitHub Action (#4985)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2025-03-16 14:12:30 -07:00
shamoon
5f19e4af91 Fix: fix custom quicklaunch search without search widget (#5003) 2025-03-16 13:12:20 -07:00
shamoon
5ea46881ee Fix: re-add tailwind css safelist (#4999)
Some checks are pending
Docker / Linting Checks (push) Waiting to run
Docker / Docker Build & Push (push) Blocked by required conditions
2025-03-16 08:32:27 -07:00
shamoon
9545757bb9 Fix: fix bookmark heights again (#4997) 2025-03-16 07:33:30 -07:00
shamoon
95507aab54 Change: re-enable k8s ingress by default (#4988)
Some checks are pending
Docker / Linting Checks (push) Waiting to run
Docker / Docker Build & Push (push) Blocked by required conditions
2025-03-16 02:35:43 -07:00
shamoon
564dfb7ce3 Add k3d ingress setting and HOMEPAGE_ALLOWED_HOSTS 2025-03-15 21:05:11 -07:00
shamoon
7b72442114 Update index.md
Some checks are pending
Docs / Linting Checks (push) Waiting to run
Docs / Test Build (push) Blocked by required conditions
Docs / Build & Deploy (push) Blocked by required conditions
2025-03-15 16:29:02 -07:00
shamoon
65370a7668 Fix: fix Kubernetes stats in v1.0.0 (#4984)
Some checks are pending
Docker / Linting Checks (push) Waiting to run
Docker / Docker Build & Push (push) Blocked by required conditions
Co-authored-by: djeinstine <2105133+djeinstine@users.noreply.github.com>
2025-03-15 16:23:29 -07:00
shamoon
6e9339b14c Chore: update minecraftstatuspinger to 1.2.2 (#4982) 2025-03-15 15:30:21 -07:00
shamoon
241c786710 Merge branch 'main' into dev
Some checks are pending
Docker / Linting Checks (push) Waiting to run
Docker / Docker Build & Push (push) Blocked by required conditions
2025-03-15 10:18:34 -07:00
shamoon
801ce479d8 Add links to docs
Some checks failed
Docs / Linting Checks (push) Waiting to run
Docs / Test Build (push) Blocked by required conditions
Docs / Build & Deploy (push) Blocked by required conditions
Docker / Linting Checks (push) Has been cancelled
Docker / Docker Build & Push (push) Has been cancelled
2025-03-15 10:18:26 -07:00
shamoon
16730ffec2 Merge branch 'main' into dev 2025-03-15 09:46:36 -07:00
shamoon
b35dd80e8c Improve docs 2025-03-15 09:46:23 -07:00
shamoon
82b159bf14 Improve docs 2025-03-15 09:30:10 -07:00
shamoon
6292a0709c Bump version to 1.0.3 2025-03-15 07:43:43 -07:00
shamoon
42af93bef3 Merge branch 'dev' 2025-03-15 07:43:30 -07:00
Xavier Alexander
964991781c Documentation: add HOMEPAGE_ALLOWED_HOSTS to k8s docs (#4969) 2025-03-15 07:40:26 -07:00
shamoon
607a14083e Fix: jellyfin handle empty episode numbers (#4970) 2025-03-15 07:38:35 -07:00
shamoon
133a0a6539 Fix this 2025-03-15 07:31:15 -07:00
shamoon
0d0f465e16 Update index.md 2025-03-15 07:16:42 -07:00
shamoon
16c1b2da9b Enhancement: allow disabling host header checking (#4967) 2025-03-15 07:14:41 -07:00
shamoon
4761a56b3d Remove reference to helm chart 2025-03-15 07:13:23 -07:00
shamoon
9d40b67d49 Change: prefer IPv4 in docker image (#4954)
Some checks are pending
Docker / Linting Checks (push) Waiting to run
Docker / Docker Build & Push (push) Blocked by required conditions
2025-03-14 22:51:07 -07:00
shamoon
97f4bcbdb0 Documentation: note disable ipv6
Some checks are pending
Docker / Linting Checks (push) Waiting to run
Docker / Docker Build & Push (push) Blocked by required conditions
Docs / Linting Checks (push) Waiting to run
Docs / Test Build (push) Blocked by required conditions
Docs / Build & Deploy (push) Blocked by required conditions
2025-03-14 22:35:47 -07:00
brikim
ea1375e575 Fix: correct units for speedtest tracker API v2 (#4950)
Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
2025-03-14 22:02:13 -07:00
shamoon
733a3140d1 Documentation: note komga api key 2025-03-14 21:16:20 -07:00
shamoon
b5ac617597 Fix: fix kavita API body with key (#4948) 2025-03-14 21:09:46 -07:00
shamoon
9b06212a92 Fix: wrapping in bookmarks (#4945) 2025-03-14 18:29:27 -07:00
103 changed files with 4243 additions and 676 deletions

View File

@@ -16,11 +16,11 @@
**/compose* **/compose*
**/Dockerfile* **/Dockerfile*
**/node_modules **/node_modules
!.next/standalone/node_modules
**/npm-debug.log **/npm-debug.log
**/obj **/obj
**/secrets.dev.yaml **/secrets.dev.yaml
**/values.dev.yaml **/values.dev.yaml
**/.next
README.md README.md
config/ config/
k3d/ k3d/

View File

@@ -1,9 +1,4 @@
name: Docker name: Docker CI
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
on: on:
schedule: schedule:
@@ -13,7 +8,6 @@ on:
- main - main
- feature/** - feature/**
- dev - dev
# Publish semver tags as releases.
tags: [ 'v*.*.*' ] tags: [ 'v*.*.*' ]
paths-ignore: paths-ignore:
- 'docs/**' - 'docs/**'
@@ -26,89 +20,56 @@ on:
merge_group: merge_group:
env: env:
# github.repository as <account>/<repo>
IMAGE_NAME: ${{ github.repository }} IMAGE_NAME: ${{ github.repository }}
jobs: jobs:
pre-commit: pre-commit:
name: Linting Checks name: Linting Checks
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- - name: Checkout repository
name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
-
name: Install python - name: Install python
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
python-version: 3.x python-version: 3.x
-
name: Check files - name: Check files
uses: pre-commit/action@v3.0.1 uses: pre-commit/action@v3.0.1
-
name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
with: with:
version: 10 version: 10
run_install: false run_install: false
-
name: Install Node.js - name: Setup Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: 20 node-version: 20
cache: 'pnpm' cache: 'pnpm'
-
name: Install dependencies - name: Install dependencies
run: pnpm install run: pnpm install
-
name: Lint frontend - name: Lint frontend
run: pnpm run lint 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: self-hosted runs-on: self-hosted
needs: needs: [ pre-commit ]
- pre-commit
permissions: permissions:
contents: read contents: read
packages: write packages: write
# This is used to complete the identity challenge
# with sigstore/fulcio when running outside of PRs.
id-token: write id-token: write
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
# Login to Docker Registry
# https://github.com/docker/login-action
- name: Log into registry ${{ env.REGISTRY }}
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to Docker Hub
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
# Setup QEMU
# https://github.com/marketplace/actions/docker-setup-buildx#with-qemu
- name: Setup QEMU
uses: docker/setup-qemu-action@v3.6.0
# Workaround: https://github.com/docker/build-push-action/issues/461
- name: Setup Docker buildx
uses: docker/setup-buildx-action@v3
# Extract metadata (tags, labels) for Docker
# https://github.com/docker/metadata-action
- name: Extract Docker metadata - name: Extract Docker metadata
id: meta id: meta
uses: docker/metadata-action@v5 uses: docker/metadata-action@v5
@@ -119,8 +80,57 @@ jobs:
flavor: | flavor: |
latest=auto latest=auto
# Build and push Docker image with Buildx (don't push on PR) - name: Next.js build cache
# https://github.com/docker/build-push-action uses: actions/cache@v4
with:
path: .next/cache
key: nextjs-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}-${{ hashFiles('**/*.js', '**/*.jsx') }}
restore-keys: |
nextjs-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
run_install: false
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Install dependencies
run: pnpm install
- name: Build app
run: |
NEXT_PUBLIC_BUILDTIME="${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.created'] }}" \
NEXT_PUBLIC_VERSION="${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }}" \
NEXT_PUBLIC_REVISION="${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.revision'] }}" \
pnpm run build
- name: Log into registry ${{ env.REGISTRY }}
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to Docker Hub
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Setup QEMU
uses: docker/setup-qemu-action@v3.6.0
- name: Setup Docker buildx
uses: docker/setup-buildx-action@v3
- 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@v6 uses: docker/build-push-action@v6
@@ -130,18 +140,15 @@ jobs:
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
build-args: | build-args: |
CI=true
BUILDTIME=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.created'] }} BUILDTIME=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.created'] }}
VERSION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }} VERSION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }}
REVISION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.revision'] }} REVISION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.revision'] }}
# https://github.com/docker/setup-qemu-action#about
# platforms: linux/amd64,linux/arm64,linux/riscv64,linux/ppc64le,linux/s390x,linux/386,linux/mips64le,linux/mips64,linux/arm/v7,linux/arm/v6
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
cache-from: type=local,src=/tmp/.buildx-cache cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
# Temp fix # https://github.com/docker/build-push-action/issues/252 / https://github.com/moby/buildkit/issues/1896
# https://github.com/docker/build-push-action/issues/252
# https://github.com/moby/buildkit/issues/1896
- name: Move cache - name: Move cache
run: | run: |
rm -rf /tmp/.buildx-cache rm -rf /tmp/.buildx-cache

View File

@@ -55,7 +55,7 @@ jobs:
run: MKINSIDERS=false mkdocs build run: MKINSIDERS=false mkdocs build
deploy: deploy:
name: Build & Deploy name: Build & Deploy
if: github.repository == 'gethomepage/homepage' && github.event_name != 'pull_request' if: github.repository == 'gethomepage/homepage' && github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: needs:
- pre-commit - pre-commit

View File

@@ -1,67 +1,63 @@
# Install dependencies only when needed # =========================
FROM docker.io/node:22-alpine AS deps # Builder Stage
# =========================
WORKDIR /app FROM node:22-slim AS builder
COPY --link package.json pnpm-lock.yaml* ./
SHELL ["/bin/ash", "-xeo", "pipefail", "-c"]
RUN apk add --no-cache libc6-compat \
&& apk add --no-cache --virtual .gyp python3 make g++ \
&& npm install -g pnpm
RUN --mount=type=cache,id=pnpm-store,target=/root/.local/share/pnpm/store pnpm fetch | grep -v "cross-device link not permitted\|Falling back to copying packages from store"
RUN --mount=type=cache,id=pnpm-store,target=/root/.local/share/pnpm/store pnpm install -r --offline
# Rebuild the source code only when needed
FROM docker.io/node:22-alpine AS builder
WORKDIR /app WORKDIR /app
# Setup
RUN mkdir config RUN mkdir config
COPY . .
ARG CI
ARG BUILDTIME ARG BUILDTIME
ARG VERSION ARG VERSION
ARG REVISION ARG REVISION
ENV CI=$CI
COPY --link --from=deps /app/node_modules ./node_modules/ # Install and build only outside CI
COPY . . RUN if [ "$CI" != "true" ]; then \
corepack enable && corepack prepare pnpm@latest --activate && \
pnpm install --frozen-lockfile --prefer-offline && \
NEXT_TELEMETRY_DISABLED=1 \
NEXT_PUBLIC_BUILDTIME=$BUILDTIME \
NEXT_PUBLIC_VERSION=$VERSION \
NEXT_PUBLIC_REVISION=$REVISION \
pnpm run build; \
else \
echo "✅ Using prebuilt app from CI context"; \
fi
SHELL ["/bin/ash", "-xeo", "pipefail", "-c"] # =========================
RUN npm install -g pnpm \ # Runtime Stage
&& pnpm run telemetry \ # =========================
&& NEXT_PUBLIC_BUILDTIME=$BUILDTIME NEXT_PUBLIC_VERSION=$VERSION NEXT_PUBLIC_REVISION=$REVISION pnpm run build FROM node:22-alpine AS runner
LABEL org.opencontainers.image.title="Homepage"
# Production image, copy all the files and run next LABEL org.opencontainers.image.description="A self-hosted services landing page, with docker and service integrations."
FROM docker.io/node:22-alpine AS runner
LABEL org.opencontainers.image.title "Homepage"
LABEL org.opencontainers.image.description "A self-hosted services landing page, with docker and service integrations."
LABEL org.opencontainers.image.url="https://github.com/gethomepage/homepage" LABEL org.opencontainers.image.url="https://github.com/gethomepage/homepage"
LABEL org.opencontainers.image.documentation='https://github.com/gethomepage/homepage/wiki' LABEL org.opencontainers.image.documentation='https://github.com/gethomepage/homepage/wiki'
LABEL org.opencontainers.image.source='https://github.com/gethomepage/homepage' LABEL org.opencontainers.image.source='https://github.com/gethomepage/homepage'
LABEL org.opencontainers.image.licenses='Apache-2.0' LABEL org.opencontainers.image.licenses='Apache-2.0'
ENV NODE_ENV=production # Setup
WORKDIR /app WORKDIR /app
# Copy files from context (this allows the files to copy before the builder stage is done). # Copy some files from context
COPY --link --chown=1000:1000 package.json next.config.js ./
COPY --link --chown=1000:1000 /public ./public/ COPY --link --chown=1000:1000 /public ./public/
# Copy files from builder
COPY --link --from=builder --chown=1000:1000 /app/.next/standalone ./
COPY --link --from=builder --chown=1000:1000 /app/.next/static/ ./.next/static/
COPY --link --chmod=755 docker-entrypoint.sh /usr/local/bin/ COPY --link --chmod=755 docker-entrypoint.sh /usr/local/bin/
# Copy only necessary files from the build stage
COPY --link --from=builder --chown=1000:1000 /app/.next/standalone/ ./
COPY --link --from=builder --chown=1000:1000 /app/.next/static/ ./.next/static
RUN apk add --no-cache su-exec RUN apk add --no-cache su-exec
ENV HOSTNAME=:: ENV NODE_ENV=production
ENV HOSTNAME=0.0.0.0
ENV PORT=3000 ENV PORT=3000
EXPOSE $PORT EXPOSE $PORT
HEALTHCHECK --interval=10s --timeout=3s --start-period=20s \ HEALTHCHECK --interval=10s --timeout=3s --start-period=20s \
CMD wget --no-verbose --tries=1 --spider --no-check-certificate http://localhost:$PORT/api/healthcheck || exit 1 CMD wget --no-verbose --tries=1 --spider http://127.0.0.1:$PORT/api/healthcheck || exit 1
ENTRYPOINT ["docker-entrypoint.sh"] ENTRYPOINT ["docker-entrypoint.sh"]
CMD ["node", "server.js"] CMD ["node", "server.js"]

View File

@@ -80,7 +80,7 @@ services:
image: ghcr.io/gethomepage/homepage:latest image: ghcr.io/gethomepage/homepage:latest
container_name: homepage container_name: homepage
environment: environment:
HOMEPAGE_ALLOWED_HOSTS: gethomepage.dev # required, may need port HOMEPAGE_ALLOWED_HOSTS: gethomepage.dev # required, may need port. See gethomepage.dev/installation/#homepage_allowed_hosts
PUID: 1000 # optional, your user id PUID: 1000 # optional, your user id
PGID: 1000 # optional, your group id PGID: 1000 # optional, your group id
ports: ports:

View File

@@ -15,7 +15,7 @@ export HOMEPAGE_BUILDTIME=$(date +%s)
# Set privileges for /app but only if pid 1 user is root and we are dropping privileges. # Set privileges for /app but only if pid 1 user is root and we are dropping privileges.
# If container is run as an unprivileged user, it means owner already handled ownership setup on their own. # If container is run as an unprivileged user, it means owner already handled ownership setup on their own.
# Running chown in that case (as non-root) will cause error # Running chown in that case (as non-root) will cause error
[ "$(id -u)" == "0" ] && [ "${PUID}" != "0" ] && chown -R ${PUID}:${PGID} /app [ "$(id -u)" == "0" ] && [ "${PUID}" != "0" ] && chown -R ${PUID}:${PGID} /app/config /app/public
# Drop privileges (when asked to) if root, otherwise run as current user # Drop privileges (when asked to) if root, otherwise run as current user
if [ "$(id -u)" == "0" ] && [ "${PUID}" != "0" ]; then if [ "$(id -u)" == "0" ] && [ "${PUID}" != "0" ]; then

View File

@@ -1,3 +0,0 @@
.md-typeset[data-page-id="landing"] .md-header-anchor {
display: none;
}

View File

@@ -25,13 +25,13 @@ To configure Kubernetes gateway-api, ingress or ingressRoute service discovery,
Example settings: Example settings:
```yaml ```yaml
ingress: true # enable ingress only ingress: true # default, enable ingress only
``` ```
or or
```yaml ```yaml
ingress: true # enable ingress ingress: true # default, enable ingress
traefik: true # enable traefik ingressRoute traefik: true # enable traefik ingressRoute
gateway: true # enable gateway-api gateway: true # enable gateway-api
``` ```

View File

@@ -78,7 +78,7 @@ background:
You can apply a blur filter to the service & bookmark cards. Note this option is incompatible with the background blur, saturate and brightness filters. You can apply a blur filter to the service & bookmark cards. Note this option is incompatible with the background blur, saturate and brightness filters.
```yaml ```yaml
cardBlur: sm # sm, "", md, etc... see https://tailwindcss.com/docs/backdrop-blur cardBlur: xs # xs, md, etc... see https://tailwindcss.com/docs/backdrop-blur
``` ```
## Favicon ## Favicon
@@ -254,15 +254,29 @@ layout:
columns: 4 columns: 4
``` ```
### Five Columns ### Full Width
You can add a fifth column to services (when `style: columns` which is default) by adding: You can make homepage take up the entire window width by adding:
```yaml ```yaml
fiveColumns: true fullWidth: true
``` ```
By default homepage will max out at 4 columns for services with `columns` style ### Maximum Group Columns
You can set the maximum number of columns of groups on larger screen sizes (note this is only for groups with the default `style: columns`, not groups with `stle: row`) by adding:
```yaml
maxGroupColumns: 8 # default is 4 for services, 6 for bookmarks, max 8
```
By default homepage will max out at 4 columns for services and 6 for bookmarks, thus the minimum for this setting is _5_. Of course, if you're setting this to higher numbers, you may want to consider enabling the [fullWidth](#full-width) option as well.
If you want to set the maximum columns for bookmark groups separately, you can do so by adding:
```yaml
maxBookmarkGroupColumns: 6 # default is 6, max 8
```
### Collapsible sections ### Collapsible sections

View File

@@ -16,7 +16,7 @@ services:
- /path/to/config:/app/config # Make sure your local config directory exists - /path/to/config:/app/config # Make sure your local config directory exists
- /var/run/docker.sock:/var/run/docker.sock # (optional) For docker integrations - /var/run/docker.sock:/var/run/docker.sock # (optional) For docker integrations
environment: environment:
HOMEPAGE_ALLOWED_HOSTS: gethomepage.dev # required, may need port HOMEPAGE_ALLOWED_HOSTS: gethomepage.dev # required, may need port. See gethomepage.dev/installation/#homepage_allowed_hosts
``` ```
### Running as non-root ### Running as non-root
@@ -38,7 +38,7 @@ services:
- /path/to/config:/app/config # Make sure your local config directory exists - /path/to/config:/app/config # Make sure your local config directory exists
- /var/run/docker.sock:/var/run/docker.sock # (optional) For docker integrations, see alternative methods - /var/run/docker.sock:/var/run/docker.sock # (optional) For docker integrations, see alternative methods
environment: environment:
HOMEPAGE_ALLOWED_HOSTS: gethomepage.dev # required, may need port HOMEPAGE_ALLOWED_HOSTS: gethomepage.dev # required, may need port. See gethomepage.dev/installation/#homepage_allowed_hosts
PUID: $PUID PUID: $PUID
PGID: $PGID PGID: $PGID
``` ```

View File

@@ -13,20 +13,28 @@ You have a few options for deploying homepage, depending on your needs. We offer
<br> <br>
<div class="grid cards" style="margin: 0 auto;" markdown> <div class="grid cards" style="margin: 0 auto;" markdown>
:simple-docker: [&nbsp; Install on Docker :octicons-arrow-right-24:](docker.md) [:simple-docker: &nbsp; Install on Docker :octicons-arrow-right-24:](docker.md)
{ .card } { .card }
:simple-kubernetes: [&nbsp; Install on Kubernetes :octicons-arrow-right-24:](k8s.md) [:simple-kubernetes: &nbsp; Install on Kubernetes :octicons-arrow-right-24:](k8s.md)
{ .card } { .card }
:simple-unraid: [&nbsp; Install on UNRAID :octicons-arrow-right-24:](unraid.md) [:simple-unraid: &nbsp; Install on UNRAID :octicons-arrow-right-24:](unraid.md)
{ .card } { .card }
:simple-nextdotjs: [&nbsp; Building from source :octicons-arrow-right-24:](source.md) [:simple-nextdotjs: &nbsp; Building from source :octicons-arrow-right-24:](source.md)
{ .card } { .card }
</div> </div>
### `HOMEPAGE_ALLOWED_HOSTS` ### `HOMEPAGE_ALLOWED_HOSTS`
As of v1.0 there is one required environment variable when deploying via a public URL, <code>HOMEPAGE_ALLOWED_HOSTS</code>. This is a comma separated (no spaces) list of allowed hosts (sometimes with the port) that can access your homepage. See the [docker](docker.md) and [source](source.md) installation pages for examples. As of v1.0 there is one required environment variable to access homepage via a URL other than `localhost`, <code>HOMEPAGE_ALLOWED_HOSTS</code>. The setting helps prevent certain kinds of attacks when retrieving data from the homepage API proxy.
The value is a comma-separated (no spaces) list of allowed hosts (sometimes with the port) that can host your homepage install. See the [docker](docker.md), [kubernetes](k8s.md) and [source](source.md) installation pages for more information about where / how to set the variable.
`localhost:3000` and `127.0.0.1:3000` are always included, but you can add a domain or IP address to this list to allow that host such as `HOMEPAGE_ALLOWED_HOSTS=gethomepage.dev,192.168.1.2:1234`, etc.
If you are seeing errors about host validation, check the homepage logs and ensure that the host exactly as output in the logs is in the `HOMEPAGE_ALLOWED_HOSTS` list.
This can be disabled by setting `HOMEPAGE_ALLOWED_HOSTS` to `*` but this is not recommended.

View File

@@ -3,85 +3,6 @@ title: Kubernetes Installation
description: Install on Kubernetes description: Install on Kubernetes
--- ---
## Install with Helm
There is an [unofficial helm chart](https://github.com/jameswynn/helm-charts/tree/main/charts/homepage) that creates all the necessary manifests, including the service account and RBAC entities necessary for service discovery.
```sh
helm repo add jameswynn https://jameswynn.github.io/helm-charts
helm install homepage jameswynn/homepage -f values.yaml
```
The helm chart allows for all the configurations to be inlined directly in your `values.yaml`:
```yaml
config:
bookmarks:
- Developer:
- Github:
- abbr: GH
href: https://github.com/
services:
- My First Group:
- My First Service:
href: http://localhost/
description: Homepage is awesome
- My Second Group:
- My Second Service:
href: http://localhost/
description: Homepage is the best
- My Third Group:
- My Third Service:
href: http://localhost/
description: Homepage is 😎
widgets:
# show the kubernetes widget, with the cluster summary and individual nodes
- kubernetes:
cluster:
show: true
cpu: true
memory: true
showLabel: true
label: "cluster"
nodes:
show: true
cpu: true
memory: true
showLabel: true
- search:
provider: duckduckgo
target: _blank
kubernetes:
mode: cluster
settings:
# The service account is necessary to allow discovery of other services
serviceAccount:
create: true
name: homepage
# This enables the service account to access the necessary resources
enableRbac: true
ingress:
main:
enabled: true
annotations:
# Example annotations to add Homepage to your Homepage!
gethomepage.dev/enabled: "true"
gethomepage.dev/name: "Homepage"
gethomepage.dev/description: "Dynamically Detected Homepage"
gethomepage.dev/group: "Dynamic"
gethomepage.dev/icon: "homepage.png"
hosts:
- host: homepage.example.com
paths:
- path: /
pathType: Prefix
```
## Install with Kubernetes Manifests ## Install with Kubernetes Manifests
If you don't want to use the unofficial Helm chart, you can also create your own Kubernetes manifest(s) and apply them with `kubectl apply -f filename.yaml`. If you don't want to use the unofficial Helm chart, you can also create your own Kubernetes manifest(s) and apply them with `kubectl apply -f filename.yaml`.
@@ -302,6 +223,9 @@ spec:
- name: homepage - name: homepage
image: "ghcr.io/gethomepage/homepage:latest" image: "ghcr.io/gethomepage/homepage:latest"
imagePullPolicy: Always imagePullPolicy: Always
env:
- name: HOMEPAGE_ALLOWED_HOSTS
value: gethomepage.dev # required, may need port. See gethomepage.dev/installation/#homepage_allowed_hosts
ports: ports:
- name: http - name: http
containerPort: 3000 containerPort: 3000

View File

@@ -9,7 +9,13 @@ First, clone the repository:
git clone https://github.com/gethomepage/homepage.git git clone https://github.com/gethomepage/homepage.git
``` ```
Then install dependencies and build the production bundle (I'm using pnpm here, you can use npm or yarn if you like): If `pnpm` is not installed, install it:
```bash
npm install -g pnpm
```
Then install dependencies and build the production bundle:
```bash ```bash
pnpm install pnpm install
@@ -25,3 +31,5 @@ HOMEPAGE_ALLOWED_HOSTS=gethomepage.dev:1234 pnpm start
``` ```
When updating homepage versions you will need to re-build the static files i.e. repeat the process above. When updating homepage versions you will need to re-build the static files i.e. repeat the process above.
See [HOMEPAGE_ALLOWED_HOSTS](index.md#homepage_allowed_hosts) for more information on this environment variable.

View File

@@ -286,3 +286,13 @@ body[data-md-color-scheme="default"] {
.md-tabs__link { .md-tabs__link {
transform: translateZ(0); transform: translateZ(0);
} }
.grid.cards .card {
padding: 0;
}
.grid.cards .card a {
display: block;
padding: 0.8rem;
text-decoration: none;
}

View File

@@ -12,6 +12,7 @@ hide:
- Check config/logs/homepage.log, on docker simply e.g. `docker logs homepage`. This may provide some insight into the reason for an error. - Check config/logs/homepage.log, on docker simply e.g. `docker logs homepage`. This may provide some insight into the reason for an error.
- Check the browser error console, this can also sometimes provide useful information. - Check the browser error console, this can also sometimes provide useful information.
- Consider setting the `ENV` variable `LOG_LEVEL` to `debug`. - Consider setting the `ENV` variable `LOG_LEVEL` to `debug`.
- If certain widgets are failing when connecting to public APIs, consider [disabling IPv6](#disabling-ipv6).
## Service Widget Errors ## Service Widget Errors
@@ -66,3 +67,17 @@ All service widgets work essentially the same, that is, homepage makes a proxied
## Missing custom icons ## Missing custom icons
If, after correctly adding and mapping your custom icons via the [Icons](../configs/services.md#icons) instructions, you are still unable to see your icons please try recreating your container. If, after correctly adding and mapping your custom icons via the [Icons](../configs/services.md#icons) instructions, you are still unable to see your icons please try recreating your container.
## Disabling IPv6
If you are having issues with certain widgets that are unable to reach public APIs (e.g. weather), in certain setups you may need to disable IPv6. You can set the environment variable `HOMEPAGE_PROXY_DISABLE_IPV6` to `true` to disable IPv6 for the homepage proxy.
Alternatively, you can use the `sysctls` option in your docker-compose file to disable IPv6 for the homepage container completely:
```yaml
services:
homepage:
...
sysctls:
- net.ipv6.conf.all.disable_ipv6=1
```

View File

@@ -19,27 +19,22 @@ widget:
requestBody: # optional, can be string or object, see below requestBody: # optional, can be string or object, see below
display: # optional, default to block, see below display: # optional, default to block, see below
mappings: mappings:
- field: key # needs to be YAML string or object - field: key
label: Field 1 label: Field 1
format: text # optional - defaults to text format: text # optional - defaults to text
- field: # needs to be YAML string or object - field: path.to.key2
path:
to: key2
format: number # optional - defaults to text format: number # optional - defaults to text
label: Field 2 label: Field 2
- field: # needs to be YAML string or object - field: path.to.another.key3
path:
to:
another: key3
label: Field 3 label: Field 3
format: percent # optional - defaults to text format: percent # optional - defaults to text
- field: key # needs to be YAML string or object - field: key
label: Field 4 label: Field 4
format: date # optional - defaults to text format: date # optional - defaults to text
locale: nl # optional locale: nl # optional
dateStyle: long # optional - defaults to "long". Allowed values: `["full", "long", "medium", "short"]`. dateStyle: long # optional - defaults to "long". Allowed values: `["full", "long", "medium", "short"]`.
timeStyle: medium # optional - Allowed values: `["full", "long", "medium", "short"]`. timeStyle: medium # optional - Allowed values: `["full", "long", "medium", "short"]`.
- field: key # needs to be YAML string or object - field: key
label: Field 5 label: Field 5
format: relativeDate # optional - defaults to text format: relativeDate # optional - defaults to text
locale: nl # optional locale: nl # optional
@@ -49,9 +44,7 @@ widget:
label: Field 6 label: Field 6
format: text format: text
additionalField: # optional additionalField: # optional
field: field: hourly.time.key
hourly:
time: other key
color: theme # optional - defaults to "". Allowed values: `["theme", "adaptive", "black", "white"]`. color: theme # optional - defaults to "". Allowed values: `["theme", "adaptive", "black", "white"]`.
format: date # optional format: date # optional
- field: key - field: key
@@ -103,9 +96,16 @@ mappings:
label: Name label: Name
- field: status # Alive - field: status # Alive
label: Status label: Status
- field: - field: origin.name # Earth (C-137)
origin: name # Earth (C-137)
label: Origin label: Origin
- field: locations.1.name # Citadel of Ricks
label: Location
```
Note that older versions of the widget accepted fields as a yaml object, which is still supported. E.g.:
```yaml
mappings:
- field: - field:
locations: locations:
1: name # Citadel of Ricks 1: name # Citadel of Ricks
@@ -138,7 +138,15 @@ You can manipulate data with the following tools `remap`, `scale`, `prefix` and
prefix: "$" prefix: "$"
``` ```
## List View ## Display Options
The widget supports different display modes that can be set using the `display` property.
### Block View (Default)
The default display mode is `block`, which shows fields in a block format.
### List View
You can change the default block view to a list view by setting the `display` option to `list`. You can change the default block view to a list view by setting the `display` option to `list`.
@@ -162,13 +170,53 @@ The list view can optionally display an additional field next to the primary fie
- any: true # will map all other values - any: true # will map all other values
to: Unknown to: Unknown
additionalField: additionalField:
field: field: hourly.time.key
hourly:
time: key
color: theme color: theme
format: date format: date
``` ```
### Dynamic List View
To display a list of items from an array in the API response, set the `display` property to `dynamic-list` and configure the `mappings` object with the following properties:
```yaml
widget:
type: customapi
url: https://example.com/api/servers
display: dynamic-list
mappings:
items: data # optional, the path to the array in the API response. Omit this option if the array is at the root level
name: id # required, field in each item to use as the item name (left side)
label: ip_address # required, field in each item to use as the item label (right side)
limit: 5 # optional, limit the number of items to display
target: https://example.com/server/{id} # optional, makes items clickable with template support
```
This configuration would work with an API that returns a response like:
```json
{
"data": [
{ "id": "server1", "name": "Server 1", "ip_address": "192.168.0.1" },
{ "id": "server2", "name": "Server 2", "ip_address": "192.168.0.2" }
]
}
```
The widget would display a list with two items:
- "Server 1" on the left and "192.168.0.1" on the right, clickable to "https://example.com/server/server1"
- "Server 2" on the left and "192.168.0.2" on the right, clickable to "https://example.com/server/server2"
For nested fields in the items, you can use dot notation:
```yaml
mappings:
items: data.results.servers
name: details.id
label: details.name
```
## Custom Headers ## Custom Headers
Pass custom headers using the `headers` option, for example: Pass custom headers using the `headers` option, for example:

View File

@@ -7,7 +7,7 @@ Learn more about [Gitea](https://gitea.com).
API token requires `notifications`, `repository` and `issue` permissions. See the [gitea documentation](https://docs.gitea.com/development/api-usage#generating-and-listing-api-tokens) for details on generating tokens. API token requires `notifications`, `repository` and `issue` permissions. See the [gitea documentation](https://docs.gitea.com/development/api-usage#generating-and-listing-api-tokens) for details on generating tokens.
Allowed fields: `["notifications", "issues", "pulls"]`. Allowed fields: `["repositories", "notifications", "issues", "pulls"]`.
```yaml ```yaml
widget: widget:

View File

@@ -117,6 +117,7 @@ You can also find a list of all available service widgets in the sidebar navigat
- [ruTorrent](rutorrent.md) - [ruTorrent](rutorrent.md)
- [SABnzbd](sabnzbd.md) - [SABnzbd](sabnzbd.md)
- [Scrutiny](scrutiny.md) - [Scrutiny](scrutiny.md)
- [Slskd](slskd.md)
- [Sonarr](sonarr.md) - [Sonarr](sonarr.md)
- [Speedtest Tracker](speedtest-tracker.md) - [Speedtest Tracker](speedtest-tracker.md)
- [Stash](stash.md) - [Stash](stash.md)

View File

@@ -20,4 +20,5 @@ widget:
url: http://komga.host.or.ip:port url: http://komga.host.or.ip:port
username: username username: username
password: password password: password
key: komgaapikey # optional
``` ```

View File

@@ -0,0 +1,25 @@
---
title: Slskd
description: Slskd Widget Configuration
---
Learn more about [Slskd](https://github.com/slskd/slskd).
Generate an API key for slskd with `openssl rand -base64 48`.
Add it to your `path/to/config/slskd.yml` in `web > authentication > api_keys`:
```yaml
homepage_widget:
key: <generated key>
role: readonly
cidr: <homepage subnet>
```
Allowed fields: `["slskStatus", "updateStatus", "downloads", "uploads", "sharedFiles"]` (maximum of 4).
```yaml
widget:
type: slskd
url: http[s]://slskd.host.or.ip[:5030]
key: generatedapikey
```

21
jest.config.js Normal file
View File

@@ -0,0 +1,21 @@
const nextJest = require("next/jest");
const createJestConfig = nextJest({ dir: "./" });
const customJestConfig = {
testEnvironment: "jsdom",
setupFilesAfterEnv: ["<rootDir>/jest.setup.js"],
moduleDirectories: ["node_modules"],
moduleNameMapper: {
"^components/(.*)$": "<rootDir>/src/components/$1",
"^utils/(.*)$": "<rootDir>/src/utils/$1",
"^widgets/(.*)$": "<rootDir>/src/widgets/$1",
},
transformIgnorePatterns: ["/node_modules/"],
testPathIgnorePatterns: ["<rootDir>/node_modules/", "config/"],
// collectCoverage: true,
// coverageDirectory: "coverage",
clearMocks: true,
};
module.exports = createJestConfig(customJestConfig);

45
jest.i18n.js Normal file
View File

@@ -0,0 +1,45 @@
import i18n from "i18next";
import { initReactI18next, setI18n } from "react-i18next";
import rawTranslationEN from "./public/locales/en/common.json";
import i18nConfig from "./next-i18next.config.js";
const translationEN = {
common: rawTranslationEN,
};
// Set up formatter service
i18n.services = i18n.services || {};
i18n.services.formatter = {
add: (name, fn) => {
i18n.formatters = i18n.formatters || {};
i18n.formatters[name] = fn;
},
format: (value, format, lng, options) => {
return i18n.formatters?.[format]?.(value, lng, options) ?? value;
},
};
// Register custom formatters
i18nConfig.use[0].init(i18n);
// Initialize i18n
i18n.use(initReactI18next).init({
lng: "en",
fallbackLng: "en",
debug: false,
initImmediate: false, // make sure init is synchronous in tests
interpolation: {
escapeValue: false,
format: (value, format, lng, options) => i18n.services.formatter.format(value, format, lng, options),
},
resources: {
en: translationEN,
},
ns: ["common"],
defaultNS: "common",
returnNull: false,
});
setI18n(i18n);
export default i18n;

20
jest.setup.js Normal file
View File

@@ -0,0 +1,20 @@
import "@testing-library/jest-dom";
import "./jest.i18n.js";
jest.mock("utils/proxy/use-widget-api");
jest.mock("next-i18next", () => {
const reactI18next = require("react-i18next");
const i18n = require("./jest.i18n").default;
return {
useTranslation: () => reactI18next.useTranslation("", { i18n }),
Trans: reactI18next.Trans,
I18nextProvider: reactI18next.I18nextProvider,
appWithTranslation: (Component) => (props) => (
<reactI18next.I18nextProvider i18n={i18n}>
<Component {...props} />
</reactI18next.I18nextProvider>
),
};
});

View File

@@ -46,6 +46,10 @@ config:
docker: docker:
settings: settings:
env:
- name: HOMEPAGE_ALLOWED_HOSTS
value: "homepage.k3d.localhost:8080"
serviceAccount: serviceAccount:
create: true create: true
name: homepage name: homepage

View File

@@ -1,6 +1,6 @@
{ {
"name": "homepage", "name": "homepage",
"version": "1.0.2", "version": "1.1.1",
"private": true, "private": true,
"scripts": { "scripts": {
"preinstall": "npx only-allow pnpm", "preinstall": "npx only-allow pnpm",
@@ -8,7 +8,8 @@
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "next lint", "lint": "next lint",
"telemetry": "next telemetry disable" "telemetry": "next telemetry disable",
"test": "jest"
}, },
"dependencies": { "dependencies": {
"@headlessui/react": "^1.7.19", "@headlessui/react": "^1.7.19",
@@ -24,8 +25,8 @@
"json-rpc-2.0": "^1.7.0", "json-rpc-2.0": "^1.7.0",
"luxon": "^3.5.0", "luxon": "^3.5.0",
"memory-cache": "^0.2.0", "memory-cache": "^0.2.0",
"minecraftstatuspinger": "^1.2.1", "minecraftstatuspinger": "^1.2.2",
"next": "^15.1.7", "next": "^15.2.3",
"next-i18next": "^12.1.0", "next-i18next": "^12.1.0",
"ping": "^0.4.4", "ping": "^0.4.4",
"pretty-bytes": "^6.1.1", "pretty-bytes": "^6.1.1",
@@ -46,6 +47,10 @@
"devDependencies": { "devDependencies": {
"@tailwindcss/forms": "^0.5.10", "@tailwindcss/forms": "^0.5.10",
"@tailwindcss/postcss": "^4.0.9", "@tailwindcss/postcss": "^4.0.9",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.2.0",
"babel-jest": "^29.7.0",
"eslint": "^9.21.0", "eslint": "^9.21.0",
"eslint-config-next": "^15.1.7", "eslint-config-next": "^15.1.7",
"eslint-config-prettier": "^10.0.2", "eslint-config-prettier": "^10.0.2",
@@ -54,10 +59,14 @@
"eslint-plugin-prettier": "^5.2.3", "eslint-plugin-prettier": "^5.2.3",
"eslint-plugin-react": "^7.37.4", "eslint-plugin-react": "^7.37.4",
"eslint-plugin-react-hooks": "^5.1.0", "eslint-plugin-react-hooks": "^5.1.0",
"identity-obj-proxy": "^3.0.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"postcss": "^8.5.2", "postcss": "^8.5.2",
"prettier": "^3.5.2", "prettier": "^3.5.2",
"tailwind-scrollbar": "^4.0.1", "tailwind-scrollbar": "^4.0.1",
"tailwindcss": "^4.0.9", "tailwindcss": "^4.0.9",
"ts-node": "^10.9.2",
"typescript": "^5.7.3" "typescript": "^5.7.3"
}, },
"optionalDependencies": { "optionalDependencies": {

2733
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -885,7 +885,8 @@
"gitea": { "gitea": {
"notifications": "Kennisgewings", "notifications": "Kennisgewings",
"issues": "Kwessies", "issues": "Kwessies",
"pulls": "Trek Versoeke" "pulls": "Trek Versoeke",
"repositories": "Bewaarplekke"
}, },
"stash": { "stash": {
"scenes": "Tonele", "scenes": "Tonele",
@@ -1024,11 +1025,22 @@
"timeleft": "Oorblywende Tyd" "timeleft": "Oorblywende Tyd"
}, },
"hoarder": { "hoarder": {
"bookmarks": "Bookmarks", "bookmarks": "Boekmerke",
"favorites": "Favorites", "favorites": "Gunstelinge",
"archived": "Archived", "archived": "Geargiveer",
"highlights": "Highlights", "highlights": "Hoogtepunte",
"lists": "Lists", "lists": "Lyste",
"tags": "Merkers" "tags": "Merkers"
},
"slskd": {
"slskStatus": "Netwerk",
"connected": "Gekoppel",
"disconnected": "Ontkoppel",
"updateStatus": "Opdateer",
"update_yes": "Beskikbaar",
"update_no": "Op Datum",
"downloads": "Aflaaie",
"uploads": "Oplaaie",
"sharedFiles": "Lêers"
} }
} }

View File

@@ -885,7 +885,8 @@
"gitea": { "gitea": {
"notifications": "الإشعارات", "notifications": "الإشعارات",
"issues": "المُشكِلات", "issues": "المُشكِلات",
"pulls": "طلبات السحب" "pulls": "طلبات السحب",
"repositories": "Repositories"
}, },
"stash": { "stash": {
"scenes": "المشاهد", "scenes": "المشاهد",
@@ -1030,5 +1031,16 @@
"highlights": "Highlights", "highlights": "Highlights",
"lists": "Lists", "lists": "Lists",
"tags": "التصنيفات" "tags": "التصنيفات"
},
"slskd": {
"slskStatus": "الشبكة",
"connected": "متصل",
"disconnected": "غير متصل",
"updateStatus": "Update",
"update_yes": "متاح",
"update_no": "حتى الآن",
"downloads": "Downloads",
"uploads": "Uploads",
"sharedFiles": "ملفات"
} }
} }

View File

@@ -885,7 +885,8 @@
"gitea": { "gitea": {
"notifications": "Известия", "notifications": "Известия",
"issues": "Издания", "issues": "Издания",
"pulls": "Заявки за сливане" "pulls": "Заявки за сливане",
"repositories": "Repositories"
}, },
"stash": { "stash": {
"scenes": "Сцени", "scenes": "Сцени",
@@ -1030,5 +1031,16 @@
"highlights": "Highlights", "highlights": "Highlights",
"lists": "Lists", "lists": "Lists",
"tags": "Тагове" "tags": "Тагове"
},
"slskd": {
"slskStatus": "Мрежа",
"connected": "Свързан",
"disconnected": "Не е свързан",
"updateStatus": "Update",
"update_yes": "Наличен",
"update_no": "Актуално",
"downloads": "Downloads",
"uploads": "Uploads",
"sharedFiles": "Файлове"
} }
} }

View File

@@ -885,7 +885,8 @@
"gitea": { "gitea": {
"notifications": "Notificacions", "notifications": "Notificacions",
"issues": "Problemes", "issues": "Problemes",
"pulls": "Sol·licitud de Canvis" "pulls": "Sol·licitud de Canvis",
"repositories": "Repositories"
}, },
"stash": { "stash": {
"scenes": "Escenes", "scenes": "Escenes",
@@ -1030,5 +1031,16 @@
"highlights": "Highlights", "highlights": "Highlights",
"lists": "Lists", "lists": "Lists",
"tags": "Etiquetes" "tags": "Etiquetes"
},
"slskd": {
"slskStatus": "Xarxa",
"connected": "Connectat",
"disconnected": "Desconnectat",
"updateStatus": "Update",
"update_yes": "Disponible",
"update_no": "Actualitzat",
"downloads": "Downloads",
"uploads": "Uploads",
"sharedFiles": "Fitxers"
} }
} }

View File

@@ -885,7 +885,8 @@
"gitea": { "gitea": {
"notifications": "Notifications", "notifications": "Notifications",
"issues": "Problémy", "issues": "Problémy",
"pulls": "Pull Requests" "pulls": "Pull Requests",
"repositories": "Repositories"
}, },
"stash": { "stash": {
"scenes": "Scenes", "scenes": "Scenes",
@@ -1030,5 +1031,16 @@
"highlights": "Highlights", "highlights": "Highlights",
"lists": "Lists", "lists": "Lists",
"tags": "Tags" "tags": "Tags"
},
"slskd": {
"slskStatus": "Network",
"connected": "",
"disconnected": "Odpojeno",
"updateStatus": "Update",
"update_yes": "Dostupné",
"update_no": "Žádné",
"downloads": "Downloads",
"uploads": "Uploads",
"sharedFiles": "Soubory"
} }
} }

View File

@@ -885,7 +885,8 @@
"gitea": { "gitea": {
"notifications": "Notifications", "notifications": "Notifications",
"issues": "Problemer", "issues": "Problemer",
"pulls": "Pull Requests" "pulls": "Pull Requests",
"repositories": "Repositories"
}, },
"stash": { "stash": {
"scenes": "Scenes", "scenes": "Scenes",
@@ -1030,5 +1031,16 @@
"highlights": "Highlights", "highlights": "Highlights",
"lists": "Lists", "lists": "Lists",
"tags": "Tags" "tags": "Tags"
},
"slskd": {
"slskStatus": "Network",
"connected": "Connected",
"disconnected": "Disconnected",
"updateStatus": "Update",
"update_yes": "Tilgængelig",
"update_no": "Opdateret",
"downloads": "Downloads",
"uploads": "Uploads",
"sharedFiles": "Filer"
} }
} }

View File

@@ -447,7 +447,7 @@
"write": "Schreiben", "write": "Schreiben",
"gpu": "GPU", "gpu": "GPU",
"mem": "RAM", "mem": "RAM",
"swap": "Swap" "swap": "Auslagerung"
}, },
"quicklaunch": { "quicklaunch": {
"bookmark": "Lesezeichen", "bookmark": "Lesezeichen",
@@ -885,7 +885,8 @@
"gitea": { "gitea": {
"notifications": "Benachrichtigungen", "notifications": "Benachrichtigungen",
"issues": "Probleme", "issues": "Probleme",
"pulls": "Pull-Requests" "pulls": "Pull-Requests",
"repositories": "Repositories"
}, },
"stash": { "stash": {
"scenes": "Szenen", "scenes": "Szenen",
@@ -1024,11 +1025,22 @@
"timeleft": "Verbleibende Zeit" "timeleft": "Verbleibende Zeit"
}, },
"hoarder": { "hoarder": {
"bookmarks": "Bookmarks", "bookmarks": "Lesezeichen",
"favorites": "Favorites", "favorites": "Favoriten",
"archived": "Archived", "archived": "Archiviert",
"highlights": "Highlights", "highlights": "Highlights",
"lists": "Lists", "lists": "Listen",
"tags": "Schlagwörter" "tags": "Schlagwörter"
},
"slskd": {
"slskStatus": "Netzwerk",
"connected": "Verbunden",
"disconnected": "Getrennt",
"updateStatus": "Update",
"update_yes": "Verfügbar",
"update_no": "Aktuell",
"downloads": "Downloads",
"uploads": "Uploads",
"sharedFiles": "Dateien"
} }
} }

View File

@@ -885,7 +885,8 @@
"gitea": { "gitea": {
"notifications": "Notifications", "notifications": "Notifications",
"issues": "Issues", "issues": "Issues",
"pulls": "Pull Requests" "pulls": "Pull Requests",
"repositories": "Repositories"
}, },
"stash": { "stash": {
"scenes": "Scenes", "scenes": "Scenes",
@@ -1030,5 +1031,16 @@
"highlights": "Highlights", "highlights": "Highlights",
"lists": "Lists", "lists": "Lists",
"tags": "Ετικέτες" "tags": "Ετικέτες"
},
"slskd": {
"slskStatus": "Δίκτυο",
"connected": "Συνδέθηκε",
"disconnected": "Αποσυνδέθηκε",
"updateStatus": "Update",
"update_yes": "Διαθέσιμο",
"update_no": "Ενημερωμένο",
"downloads": "Downloads",
"uploads": "Uploads",
"sharedFiles": "Αρχεία"
} }
} }

View File

@@ -883,9 +883,10 @@
"species": "Species" "species": "Species"
}, },
"gitea": { "gitea": {
"notifications": "Notifications", "notifications": "Notifications",
"issues": "Issues", "issues": "Issues",
"pulls": "Pull Requests" "pulls": "Pull Requests",
"repositories": "Repositories"
}, },
"stash": { "stash": {
"scenes": "Scenes", "scenes": "Scenes",
@@ -1030,5 +1031,16 @@
"highlights": "Highlights", "highlights": "Highlights",
"lists": "Lists", "lists": "Lists",
"tags": "Tags" "tags": "Tags"
},
"slskd": {
"slskStatus": "Network",
"connected": "Connected",
"disconnected": "Disconnected",
"updateStatus": "Update",
"update_yes": "Available",
"update_no": "Up to Date",
"downloads": "Downloads",
"uploads": "Uploads",
"sharedFiles": "Files"
} }
} }

View File

@@ -885,7 +885,8 @@
"gitea": { "gitea": {
"notifications": "Notifications", "notifications": "Notifications",
"issues": "Issues", "issues": "Issues",
"pulls": "Pull Requests" "pulls": "Pull Requests",
"repositories": "Repositories"
}, },
"stash": { "stash": {
"scenes": "Scenes", "scenes": "Scenes",
@@ -1030,5 +1031,16 @@
"highlights": "Highlights", "highlights": "Highlights",
"lists": "Lists", "lists": "Lists",
"tags": "Tags" "tags": "Tags"
},
"slskd": {
"slskStatus": "Network",
"connected": "Connected",
"disconnected": "Disconnected",
"updateStatus": "Update",
"update_yes": "Havebla",
"update_no": "Up to Date",
"downloads": "Downloads",
"uploads": "Uploads",
"sharedFiles": "Files"
} }
} }

View File

@@ -885,7 +885,8 @@
"gitea": { "gitea": {
"notifications": "Notificaciones", "notifications": "Notificaciones",
"issues": "Números", "issues": "Números",
"pulls": "Solicitudes de cambios" "pulls": "Solicitudes de cambios",
"repositories": "Repositories"
}, },
"stash": { "stash": {
"scenes": "Escenas", "scenes": "Escenas",
@@ -1030,5 +1031,16 @@
"highlights": "Highlights", "highlights": "Highlights",
"lists": "Lists", "lists": "Lists",
"tags": "Etiquetas" "tags": "Etiquetas"
},
"slskd": {
"slskStatus": "Red",
"connected": "Conectado",
"disconnected": "Desconectado",
"updateStatus": "Update",
"update_yes": "Disponible",
"update_no": "Actualizado",
"downloads": "Downloads",
"uploads": "Uploads",
"sharedFiles": "Archivos"
} }
} }

View File

@@ -85,16 +85,16 @@
"ping": { "ping": {
"error": "Error", "error": "Error",
"ping": "Ping", "ping": "Ping",
"down": "Down", "down": "Behera",
"up": "Up", "up": "Gora",
"not_available": "Not Available" "not_available": "Not Available"
}, },
"siteMonitor": { "siteMonitor": {
"http_status": "HTTP status", "http_status": "HTTP status",
"error": "Error", "error": "Error",
"response": "Response", "response": "Erantzuna",
"down": "Down", "down": "Behera",
"up": "Up", "up": "Gora",
"not_available": "Not Available" "not_available": "Not Available"
}, },
"emby": { "emby": {
@@ -102,8 +102,8 @@
"transcoding": "Transcoding", "transcoding": "Transcoding",
"bitrate": "Bit-tasa", "bitrate": "Bit-tasa",
"no_active": "No Active Streams", "no_active": "No Active Streams",
"movies": "Movies", "movies": "Filmak",
"series": "Series", "series": "Serieak",
"episodes": "Episodes", "episodes": "Episodes",
"songs": "Abestiak" "songs": "Abestiak"
}, },
@@ -115,39 +115,39 @@
"unknown": "Ezezaguna" "unknown": "Ezezaguna"
}, },
"evcc": { "evcc": {
"pv_power": "Production", "pv_power": "Produkzioak",
"battery_soc": "Battery", "battery_soc": "Bateria",
"grid_power": "Grid", "grid_power": "Sarea",
"home_power": "Consumption", "home_power": "Kontsumoa",
"charge_power": "Charger", "charge_power": "Kargagailua",
"kilowatt": "kW" "kilowatt": "kW"
}, },
"flood": { "flood": {
"download": "Download", "download": "Jeitsierak",
"upload": "Upload", "upload": "Kargatu",
"leech": "Leech", "leech": "Leech",
"seed": "Seed" "seed": "Seed"
}, },
"freshrss": { "freshrss": {
"subscriptions": "Subscriptions", "subscriptions": "Harpidetzak",
"unread": "Unread" "unread": "Irakurri gabe"
}, },
"fritzbox": { "fritzbox": {
"connectionStatus": "Status", "connectionStatus": "Status",
"connectionStatusUnconfigured": "Unconfigured", "connectionStatusUnconfigured": "Unconfigured",
"connectionStatusConnecting": "Connecting", "connectionStatusConnecting": "Konektatzen",
"connectionStatusAuthenticating": "Authenticating", "connectionStatusAuthenticating": "Authenticating",
"connectionStatusPendingDisconnect": "Pending Disconnect", "connectionStatusPendingDisconnect": "Pending Disconnect",
"connectionStatusDisconnecting": "Disconnecting", "connectionStatusDisconnecting": "Disconnecting",
"connectionStatusDisconnected": "Disconnected", "connectionStatusDisconnected": "Deskonektatuta",
"connectionStatusConnected": "Connected", "connectionStatusConnected": "Konektatuta",
"uptime": "Uptime", "uptime": "Uptime",
"maxDown": "Max. Down", "maxDown": "Max. Down",
"maxUp": "Max. Up", "maxUp": "Max. Up",
"down": "Down", "down": "Behera",
"up": "Up", "up": "Gora",
"received": "Received", "received": "Received",
"sent": "Sent", "sent": "Bidalita",
"externalIPAddress": "Ext. IP", "externalIPAddress": "Ext. IP",
"externalIPv6Address": "Ext. IPv6", "externalIPv6Address": "Ext. IPv6",
"externalIPv6Prefix": "Ext. IPv6-Prefix" "externalIPv6Prefix": "Ext. IPv6-Prefix"
@@ -189,7 +189,7 @@
"plex": { "plex": {
"streams": "Active Streams", "streams": "Active Streams",
"albums": "Albums", "albums": "Albums",
"movies": "Movies", "movies": "Filmak",
"tv": "TV Shows" "tv": "TV Shows"
}, },
"sabnzbd": { "sabnzbd": {
@@ -199,18 +199,18 @@
}, },
"rutorrent": { "rutorrent": {
"active": "Active", "active": "Active",
"upload": "Upload", "upload": "Kargatu",
"download": "Download" "download": "Jeitsierak"
}, },
"transmission": { "transmission": {
"download": "Download", "download": "Jeitsierak",
"upload": "Upload", "upload": "Kargatu",
"leech": "Leech", "leech": "Leech",
"seed": "Seed" "seed": "Seed"
}, },
"qbittorrent": { "qbittorrent": {
"download": "Download", "download": "Jeitsierak",
"upload": "Upload", "upload": "Kargatu",
"leech": "Leech", "leech": "Leech",
"seed": "Seed" "seed": "Seed"
}, },
@@ -223,8 +223,8 @@
"invalid": "Invalid" "invalid": "Invalid"
}, },
"deluge": { "deluge": {
"download": "Download", "download": "Jeitsierak",
"upload": "Upload", "upload": "Kargatu",
"leech": "Leech", "leech": "Leech",
"seed": "Seed" "seed": "Seed"
}, },
@@ -233,15 +233,15 @@
"cachemissbytes": "Cache Miss Bytes" "cachemissbytes": "Cache Miss Bytes"
}, },
"downloadstation": { "downloadstation": {
"download": "Download", "download": "Jeitsierak",
"upload": "Upload", "upload": "Kargatu",
"leech": "Leech", "leech": "Leech",
"seed": "Seed" "seed": "Seed"
}, },
"sonarr": { "sonarr": {
"wanted": "Wanted", "wanted": "Wanted",
"queued": "Queued", "queued": "Queued",
"series": "Series", "series": "Serieak",
"queue": "Queue", "queue": "Queue",
"unknown": "Ezezaguna" "unknown": "Ezezaguna"
}, },
@@ -249,7 +249,7 @@
"wanted": "Wanted", "wanted": "Wanted",
"missing": "Missing", "missing": "Missing",
"queued": "Queued", "queued": "Queued",
"movies": "Movies", "movies": "Filmak",
"queue": "Queue", "queue": "Queue",
"unknown": "Ezezaguna" "unknown": "Ezezaguna"
}, },
@@ -285,7 +285,7 @@
}, },
"netalertx": { "netalertx": {
"total": "Guztira", "total": "Guztira",
"connected": "Connected", "connected": "Konektatuta",
"new_devices": "New Devices", "new_devices": "New Devices",
"down_alerts": "Down Alerts" "down_alerts": "Down Alerts"
}, },
@@ -302,8 +302,8 @@
"latency": "Latency" "latency": "Latency"
}, },
"speedtest": { "speedtest": {
"upload": "Upload", "upload": "Kargatu",
"download": "Download", "download": "Jeitsierak",
"ping": "Ping" "ping": "Ping"
}, },
"portainer": { "portainer": {
@@ -315,7 +315,7 @@
"download": "Downloaded", "download": "Downloaded",
"nondownload": "Non-Downloaded", "nondownload": "Non-Downloaded",
"read": "Read", "read": "Read",
"unread": "Unread", "unread": "Irakurri gabe",
"downloadedread": "Downloaded & Read", "downloadedread": "Downloaded & Read",
"downloadedunread": "Downloaded & Unread", "downloadedunread": "Downloaded & Unread",
"nondownloadedread": "Non-Downloaded & Read", "nondownloadedread": "Non-Downloaded & Read",
@@ -405,7 +405,7 @@
"medusa": { "medusa": {
"wanted": "Wanted", "wanted": "Wanted",
"queued": "Queued", "queued": "Queued",
"series": "Series" "series": "Serieak"
}, },
"minecraft": { "minecraft": {
"players": "Jokalariak", "players": "Jokalariak",
@@ -416,7 +416,7 @@
}, },
"miniflux": { "miniflux": {
"read": "Read", "read": "Read",
"unread": "Unread" "unread": "Irakurri gabe"
}, },
"authentik": { "authentik": {
"users": "Users", "users": "Users",
@@ -523,15 +523,15 @@
"up_to_date": "Up to Date", "up_to_date": "Up to Date",
"child_bridges": "Child Bridges", "child_bridges": "Child Bridges",
"child_bridges_status": "{{ok}}/{{total}}", "child_bridges_status": "{{ok}}/{{total}}",
"up": "Up", "up": "Gora",
"pending": "Pending", "pending": "Pending",
"down": "Down" "down": "Behera"
}, },
"healthchecks": { "healthchecks": {
"new": "New", "new": "New",
"up": "Up", "up": "Gora",
"grace": "In Grace Period", "grace": "In Grace Period",
"down": "Down", "down": "Behera",
"paused": "Paused", "paused": "Paused",
"status": "Status", "status": "Status",
"last_ping": "Last Ping", "last_ping": "Last Ping",
@@ -646,8 +646,8 @@
"load": "Load Avg", "load": "Load Avg",
"memory": "Mem Usage", "memory": "Mem Usage",
"wanStatus": "WAN Status", "wanStatus": "WAN Status",
"up": "Up", "up": "Gora",
"down": "Down", "down": "Behera",
"temp": "Temp", "temp": "Temp",
"disk": "Disk Usage", "disk": "Disk Usage",
"wanIP": "WAN IP" "wanIP": "WAN IP"
@@ -660,7 +660,7 @@
}, },
"immich": { "immich": {
"users": "Users", "users": "Users",
"photos": "Photos", "photos": "Argazkiak",
"videos": "Videos", "videos": "Videos",
"storage": "Storage" "storage": "Storage"
}, },
@@ -672,14 +672,14 @@
"m": "m" "m": "m"
}, },
"atsumeru": { "atsumeru": {
"series": "Series", "series": "Serieak",
"archives": "Archives", "archives": "Archives",
"chapters": "Chapters", "chapters": "Chapters",
"categories": "Categories" "categories": "Categories"
}, },
"komga": { "komga": {
"libraries": "Libraries", "libraries": "Libraries",
"series": "Series", "series": "Serieak",
"books": "Books" "books": "Books"
}, },
"diskstation": { "diskstation": {
@@ -688,13 +688,13 @@
"volumeAvailable": "Available" "volumeAvailable": "Available"
}, },
"mylar": { "mylar": {
"series": "Series", "series": "Serieak",
"issues": "Issues", "issues": "Arazoak",
"wanted": "Wanted" "wanted": "Wanted"
}, },
"photoprism": { "photoprism": {
"albums": "Albums", "albums": "Albums",
"photos": "Photos", "photos": "Argazkiak",
"videos": "Videos", "videos": "Videos",
"people": "People" "people": "People"
}, },
@@ -772,7 +772,7 @@
"books": "Books", "books": "Books",
"authors": "Authors", "authors": "Authors",
"categories": "Categories", "categories": "Categories",
"series": "Series" "series": "Serieak"
}, },
"jdownloader": { "jdownloader": {
"downloadCount": "Queue", "downloadCount": "Queue",
@@ -781,7 +781,7 @@
"downloadSpeed": "Speed" "downloadSpeed": "Speed"
}, },
"kavita": { "kavita": {
"seriesCount": "Series", "seriesCount": "Serieak",
"totalFiles": "Files" "totalFiles": "Files"
}, },
"azuredevops": { "azuredevops": {
@@ -801,8 +801,8 @@
"status": "Status", "status": "Status",
"online": "Online", "online": "Online",
"offline": "Offline", "offline": "Offline",
"name": "Name", "name": "Izena",
"map": "Map", "map": "Mapa",
"currentPlayers": "Current players", "currentPlayers": "Current players",
"players": "Jokalariak", "players": "Jokalariak",
"maxPlayers": "Max players", "maxPlayers": "Max players",
@@ -819,10 +819,10 @@
"recipes": "Recipes", "recipes": "Recipes",
"users": "Users", "users": "Users",
"categories": "Categories", "categories": "Categories",
"tags": "Tags" "tags": "Etiketak"
}, },
"openmediavault": { "openmediavault": {
"downloading": "Downloading", "downloading": "Deskargatzen",
"total": "Guztira", "total": "Guztira",
"running": "Running", "running": "Running",
"stopped": "Stopped", "stopped": "Stopped",
@@ -832,8 +832,8 @@
"openwrt": { "openwrt": {
"uptime": "Uptime", "uptime": "Uptime",
"cpuLoad": "CPU Load Avg (5m)", "cpuLoad": "CPU Load Avg (5m)",
"up": "Up", "up": "Gora",
"down": "Down", "down": "Behera",
"bytesTx": "Transmitted", "bytesTx": "Transmitted",
"bytesRx": "Received" "bytesRx": "Received"
}, },
@@ -846,9 +846,9 @@
"sitesDown": "Sites Down", "sitesDown": "Sites Down",
"paused": "Paused", "paused": "Paused",
"notyetchecked": "Not Yet Checked", "notyetchecked": "Not Yet Checked",
"up": "Up", "up": "Gora",
"seemsdown": "Seems Down", "seemsdown": "Seems Down",
"down": "Down", "down": "Behera",
"unknown": "Ezezaguna" "unknown": "Ezezaguna"
}, },
"calendar": { "calendar": {
@@ -856,7 +856,7 @@
"physicalRelease": "Physical release", "physicalRelease": "Physical release",
"digitalRelease": "Digital release", "digitalRelease": "Digital release",
"noEventsToday": "No events for today!", "noEventsToday": "No events for today!",
"noEventsFound": "No events found" "noEventsFound": "Ez da gertaerarik aurkitu."
}, },
"romm": { "romm": {
"platforms": "Platforms", "platforms": "Platforms",
@@ -868,7 +868,7 @@
}, },
"mailcow": { "mailcow": {
"domains": "Domains", "domains": "Domains",
"mailboxes": "Mailboxes", "mailboxes": "Gutunontziak",
"mails": "Mails", "mails": "Mails",
"storage": "Storage" "storage": "Storage"
}, },
@@ -877,15 +877,16 @@
"criticals": "Criticals" "criticals": "Criticals"
}, },
"plantit": { "plantit": {
"events": "Events", "events": "Ekitaldiak",
"plants": "Plants", "plants": "Landareak",
"photos": "Photos", "photos": "Argazkiak",
"species": "Species" "species": "Species"
}, },
"gitea": { "gitea": {
"notifications": "Notifications", "notifications": "Jakinarazpenak",
"issues": "Issues", "issues": "Arazoak",
"pulls": "Pull Requests" "pulls": "Pull Requests",
"repositories": "Repositories"
}, },
"stash": { "stash": {
"scenes": "Scenes", "scenes": "Scenes",
@@ -894,34 +895,34 @@
"playDuration": "Time Watched", "playDuration": "Time Watched",
"sceneSize": "Scenes Size", "sceneSize": "Scenes Size",
"sceneDuration": "Scenes Duration", "sceneDuration": "Scenes Duration",
"images": "Images", "images": "Irudia",
"imageSize": "Images Size", "imageSize": "Irudiaren tamaina",
"galleries": "Galleries", "galleries": "Galleries",
"performers": "Performers", "performers": "Performers",
"studios": "Studios", "studios": "Studios",
"movies": "Movies", "movies": "Filmak",
"tags": "Tags", "tags": "Etiketak",
"oCount": "O Count" "oCount": "O Count"
}, },
"tandoor": { "tandoor": {
"users": "Users", "users": "Users",
"recipes": "Recipes", "recipes": "Recipes",
"keywords": "Keywords" "keywords": "Hitz gakoak"
}, },
"homebox": { "homebox": {
"items": "Items", "items": "Elementuak",
"totalWithWarranty": "With Warranty", "totalWithWarranty": "With Warranty",
"locations": "Locations", "locations": "Locations",
"labels": "Labels", "labels": "Etiketak",
"users": "Users", "users": "Users",
"totalValue": "Total Value" "totalValue": "Guztira"
}, },
"crowdsec": { "crowdsec": {
"alerts": "Alerts", "alerts": "Alerts",
"bans": "Bans" "bans": "Bans"
}, },
"wgeasy": { "wgeasy": {
"connected": "Connected", "connected": "Konektatuta",
"enabled": "Enabled", "enabled": "Enabled",
"disabled": "Disabled", "disabled": "Disabled",
"total": "Guztira" "total": "Guztira"
@@ -934,8 +935,8 @@
}, },
"myspeed": { "myspeed": {
"ping": "Ping", "ping": "Ping",
"download": "Download", "download": "Jeitsierak",
"upload": "Upload" "upload": "Kargatu"
}, },
"stocks": { "stocks": {
"stocks": "Stocks", "stocks": "Stocks",
@@ -951,23 +952,23 @@
}, },
"linkwarden": { "linkwarden": {
"links": "Links", "links": "Links",
"collections": "Collections", "collections": "Bildumak",
"tags": "Tags" "tags": "Etiketak"
}, },
"zabbix": { "zabbix": {
"unclassified": "Not classified", "unclassified": "Not classified",
"information": "Informazioa", "information": "Informazioa",
"warning": "Warning", "warning": "Abisua",
"average": "Average", "average": "Batez besteko",
"high": "High", "high": "Altua",
"disaster": "Disaster" "disaster": "Disaster"
}, },
"lubelogger": { "lubelogger": {
"vehicle": "Vehicle", "vehicle": "Vehicle",
"vehicles": "Vehicles", "vehicles": "Ibilgailuak",
"serviceRecords": "Service Records", "serviceRecords": "Service Records",
"reminders": "Reminders", "reminders": "Oroigarriak",
"nextReminder": "Next Reminder", "nextReminder": "Hurrengo abisua",
"none": "None" "none": "None"
}, },
"vikunja": { "vikunja": {
@@ -977,7 +978,7 @@
"tasksInProgress": "Tasks In Progress" "tasksInProgress": "Tasks In Progress"
}, },
"headscale": { "headscale": {
"name": "Name", "name": "Izena",
"address": "Address", "address": "Address",
"last_seen": "Last Seen", "last_seen": "Last Seen",
"status": "Status", "status": "Status",
@@ -985,10 +986,10 @@
"offline": "Offline" "offline": "Offline"
}, },
"beszel": { "beszel": {
"name": "Name", "name": "Izena",
"systems": "Systems", "systems": "Systems",
"up": "Up", "up": "Gora",
"down": "Down", "down": "Behera",
"paused": "Paused", "paused": "Paused",
"pending": "Pending", "pending": "Pending",
"status": "Status", "status": "Status",
@@ -999,23 +1000,23 @@
"network": "NET" "network": "NET"
}, },
"argocd": { "argocd": {
"apps": "Apps", "apps": "Aplikazioak",
"synced": "Synced", "synced": "Sinkronizatuta",
"outOfSync": "Out Of Sync", "outOfSync": "Out Of Sync",
"healthy": "Osasuntsu", "healthy": "Osasuntsu",
"degraded": "Degraded", "degraded": "Degraded",
"progressing": "Progressing", "progressing": "Progressing",
"missing": "Missing", "missing": "Missing",
"suspended": "Suspended" "suspended": "Etenda"
}, },
"spoolman": { "spoolman": {
"loading": "Loading" "loading": "Loading"
}, },
"gitlab": { "gitlab": {
"groups": "Groups", "groups": "Taldeak",
"issues": "Issues", "issues": "Arazoak",
"merges": "Merge Requests", "merges": "Merge Requests",
"projects": "Projects" "projects": "Proiektuak"
}, },
"apcups": { "apcups": {
"status": "Status", "status": "Status",
@@ -1024,11 +1025,22 @@
"timeleft": "Time Left" "timeleft": "Time Left"
}, },
"hoarder": { "hoarder": {
"bookmarks": "Bookmarks", "bookmarks": "Laster-markak",
"favorites": "Favorites", "favorites": "Gogokoak",
"archived": "Archived", "archived": "Artxibatuta",
"highlights": "Highlights", "highlights": "Highlights",
"lists": "Lists", "lists": "Zerrendak",
"tags": "Tags" "tags": "Etiketak"
},
"slskd": {
"slskStatus": "Network",
"connected": "Konektatuta",
"disconnected": "Deskonektatuta",
"updateStatus": "Update",
"update_yes": "Available",
"update_no": "Up to Date",
"downloads": "Downloads",
"uploads": "Uploads",
"sharedFiles": "Files"
} }
} }

View File

@@ -885,7 +885,8 @@
"gitea": { "gitea": {
"notifications": "Notifications", "notifications": "Notifications",
"issues": "Issues", "issues": "Issues",
"pulls": "Pull Requests" "pulls": "Pull Requests",
"repositories": "Repositories"
}, },
"stash": { "stash": {
"scenes": "Scenes", "scenes": "Scenes",
@@ -1030,5 +1031,16 @@
"highlights": "Highlights", "highlights": "Highlights",
"lists": "Lists", "lists": "Lists",
"tags": "Tags" "tags": "Tags"
},
"slskd": {
"slskStatus": "Network",
"connected": "Connected",
"disconnected": "Disconnected",
"updateStatus": "Update",
"update_yes": "Saatavilla",
"update_no": "Up to Date",
"downloads": "Downloads",
"uploads": "Uploads",
"sharedFiles": "Files"
} }
} }

View File

@@ -149,8 +149,8 @@
"received": "Reçu", "received": "Reçu",
"sent": "Envoyé", "sent": "Envoyé",
"externalIPAddress": "IP externe", "externalIPAddress": "IP externe",
"externalIPv6Address": "Ext. IPv6", "externalIPv6Address": "IPv6 externe",
"externalIPv6Prefix": "Ext. IPv6-Prefix" "externalIPv6Prefix": "Préfixe IPv6 externe"
}, },
"caddy": { "caddy": {
"upstreams": "En amont", "upstreams": "En amont",
@@ -178,7 +178,7 @@
"connectedAp": "AP connectés", "connectedAp": "AP connectés",
"activeUser": "Périphériques actifs", "activeUser": "Périphériques actifs",
"alerts": "Alertes", "alerts": "Alertes",
"connectedGateways": "Connected gateways", "connectedGateways": "Passerelles connectées",
"connectedSwitches": "Switchs connectés" "connectedSwitches": "Switchs connectés"
}, },
"nzbget": { "nzbget": {
@@ -885,7 +885,8 @@
"gitea": { "gitea": {
"notifications": "Notifications", "notifications": "Notifications",
"issues": "Anomalies", "issues": "Anomalies",
"pulls": "Demandes de tirage" "pulls": "Demandes de tirage",
"repositories": "Repositories"
}, },
"stash": { "stash": {
"scenes": "Scènes", "scenes": "Scènes",
@@ -1024,11 +1025,22 @@
"timeleft": "Temps restant" "timeleft": "Temps restant"
}, },
"hoarder": { "hoarder": {
"bookmarks": "Bookmarks", "bookmarks": "Marque-pages",
"favorites": "Favorites", "favorites": "Favoris",
"archived": "Archived", "archived": "Archivé",
"highlights": "Highlights", "highlights": "Highlights",
"lists": "Lists", "lists": "Listes",
"tags": "Étiquettes" "tags": "Étiquettes"
},
"slskd": {
"slskStatus": "Réseau",
"connected": "Connecté",
"disconnected": "Déconnecté",
"updateStatus": "Mise à jour",
"update_yes": "Disponible",
"update_no": "À jour",
"downloads": "Téléchargements",
"uploads": "Téléversements",
"sharedFiles": "Fichiers"
} }
} }

View File

@@ -885,7 +885,8 @@
"gitea": { "gitea": {
"notifications": "Notifications", "notifications": "Notifications",
"issues": "Issues", "issues": "Issues",
"pulls": "Pull Requests" "pulls": "Pull Requests",
"repositories": "Repositories"
}, },
"stash": { "stash": {
"scenes": "Scenes", "scenes": "Scenes",
@@ -1030,5 +1031,16 @@
"highlights": "Highlights", "highlights": "Highlights",
"lists": "Lists", "lists": "Lists",
"tags": "Tags" "tags": "Tags"
},
"slskd": {
"slskStatus": "Network",
"connected": "Connected",
"disconnected": "Disconnected",
"updateStatus": "Update",
"update_yes": "זמין",
"update_no": "Up to Date",
"downloads": "Downloads",
"uploads": "Uploads",
"sharedFiles": "Files"
} }
} }

View File

@@ -885,7 +885,8 @@
"gitea": { "gitea": {
"notifications": "Notifications", "notifications": "Notifications",
"issues": "Issues", "issues": "Issues",
"pulls": "Pull Requests" "pulls": "Pull Requests",
"repositories": "Repositories"
}, },
"stash": { "stash": {
"scenes": "Scenes", "scenes": "Scenes",
@@ -1030,5 +1031,16 @@
"highlights": "Highlights", "highlights": "Highlights",
"lists": "Lists", "lists": "Lists",
"tags": "Tags" "tags": "Tags"
},
"slskd": {
"slskStatus": "Network",
"connected": "Connected",
"disconnected": "Disconnected",
"updateStatus": "Update",
"update_yes": "Available",
"update_no": "Up to Date",
"downloads": "Downloads",
"uploads": "Uploads",
"sharedFiles": "Files"
} }
} }

View File

@@ -885,7 +885,8 @@
"gitea": { "gitea": {
"notifications": "Obavijesti", "notifications": "Obavijesti",
"issues": "Problemi", "issues": "Problemi",
"pulls": "Zahtjevi za povlačenje" "pulls": "Zahtjevi za povlačenje",
"repositories": "Repositories"
}, },
"stash": { "stash": {
"scenes": "Scene", "scenes": "Scene",
@@ -1030,5 +1031,16 @@
"highlights": "Highlights", "highlights": "Highlights",
"lists": "Lists", "lists": "Lists",
"tags": "Oznake" "tags": "Oznake"
},
"slskd": {
"slskStatus": "Mreža",
"connected": "Povezano",
"disconnected": "Odspojeno",
"updateStatus": "Update",
"update_yes": "Dostupno",
"update_no": "Aktualno",
"downloads": "Downloads",
"uploads": "Uploads",
"sharedFiles": "Datoteke"
} }
} }

View File

@@ -885,7 +885,8 @@
"gitea": { "gitea": {
"notifications": "Üzenetek", "notifications": "Üzenetek",
"issues": "Problémák", "issues": "Problémák",
"pulls": "Pull request-ek" "pulls": "Pull request-ek",
"repositories": "Repositories"
}, },
"stash": { "stash": {
"scenes": "Jelenetek", "scenes": "Jelenetek",
@@ -1030,5 +1031,16 @@
"highlights": "Highlights", "highlights": "Highlights",
"lists": "Lists", "lists": "Lists",
"tags": "Címkék" "tags": "Címkék"
},
"slskd": {
"slskStatus": "Hálózat",
"connected": "Csatlakozva",
"disconnected": "Kapcsolat bontva",
"updateStatus": "Update",
"update_yes": "Elérhető",
"update_no": "Naprakész",
"downloads": "Downloads",
"uploads": "Uploads",
"sharedFiles": "Fájlok"
} }
} }

View File

@@ -342,7 +342,7 @@
"totalNxDomain": "Domain NX", "totalNxDomain": "Domain NX",
"totalRefused": "Ditolak", "totalRefused": "Ditolak",
"totalAuthoritative": "Authoritative", "totalAuthoritative": "Authoritative",
"totalRecursive": "Recursive", "totalRecursive": "Rekursif",
"totalCached": "Cached", "totalCached": "Cached",
"totalBlocked": "Terblokir", "totalBlocked": "Terblokir",
"totalDropped": "Dropped", "totalDropped": "Dropped",
@@ -705,8 +705,8 @@
"time": "Waktu" "time": "Waktu"
}, },
"firefly": { "firefly": {
"networth": "Net Worth", "networth": "Kekayaan Bersih",
"budget": "Budget" "budget": "Anggaran"
}, },
"grafana": { "grafana": {
"dashboards": "Dasbor", "dashboards": "Dasbor",
@@ -885,7 +885,8 @@
"gitea": { "gitea": {
"notifications": "Notifikasi", "notifications": "Notifikasi",
"issues": "Isu", "issues": "Isu",
"pulls": "Pull Requests" "pulls": "Pull Requests",
"repositories": "Repositories"
}, },
"stash": { "stash": {
"scenes": "Adegan", "scenes": "Adegan",
@@ -965,7 +966,7 @@
"lubelogger": { "lubelogger": {
"vehicle": "Kendaraan", "vehicle": "Kendaraan",
"vehicles": "Kendaraan", "vehicles": "Kendaraan",
"serviceRecords": "Service Records", "serviceRecords": "Catatan Servis",
"reminders": "Pengingat", "reminders": "Pengingat",
"nextReminder": "Pengingat Berikutnya", "nextReminder": "Pengingat Berikutnya",
"none": "Tidak ada" "none": "Tidak ada"
@@ -1024,11 +1025,22 @@
"timeleft": "Sisa Waktu" "timeleft": "Sisa Waktu"
}, },
"hoarder": { "hoarder": {
"bookmarks": "Bookmarks", "bookmarks": "Markah",
"favorites": "Favorites", "favorites": "Favorit",
"archived": "Archived", "archived": "Diarsipkan",
"highlights": "Highlights", "highlights": "Sorotan",
"lists": "Lists", "lists": "Daftar",
"tags": "Tag" "tags": "Tag"
},
"slskd": {
"slskStatus": "Jaringan",
"connected": "Tersambung",
"disconnected": "Terputus",
"updateStatus": "Update",
"update_yes": "Tersedia",
"update_no": "Terbaru",
"downloads": "Downloads",
"uploads": "Uploads",
"sharedFiles": "File"
} }
} }

View File

@@ -885,7 +885,8 @@
"gitea": { "gitea": {
"notifications": "Notifiche", "notifications": "Notifiche",
"issues": "Problemi", "issues": "Problemi",
"pulls": "Richieste di Pull" "pulls": "Richieste di Pull",
"repositories": "Repositories"
}, },
"stash": { "stash": {
"scenes": "Scene", "scenes": "Scene",
@@ -1030,5 +1031,16 @@
"highlights": "Highlights", "highlights": "Highlights",
"lists": "Lists", "lists": "Lists",
"tags": "Tag" "tags": "Tag"
},
"slskd": {
"slskStatus": "Rete",
"connected": "Connesso",
"disconnected": "Disconnesso",
"updateStatus": "Update",
"update_yes": "Disponibili",
"update_no": "Aggiornato",
"downloads": "Downloads",
"uploads": "Uploads",
"sharedFiles": "File"
} }
} }

View File

@@ -885,7 +885,8 @@
"gitea": { "gitea": {
"notifications": "通知", "notifications": "通知",
"issues": "課題", "issues": "課題",
"pulls": "プルリクエスト" "pulls": "プルリクエスト",
"repositories": "Repositories"
}, },
"stash": { "stash": {
"scenes": "シーン", "scenes": "シーン",
@@ -1030,5 +1031,16 @@
"highlights": "Highlights", "highlights": "Highlights",
"lists": "Lists", "lists": "Lists",
"tags": "タグ" "tags": "タグ"
},
"slskd": {
"slskStatus": "ネットワーク",
"connected": "接続済",
"disconnected": "切断されました",
"updateStatus": "Update",
"update_yes": "利用可",
"update_no": "最新",
"downloads": "Downloads",
"uploads": "Uploads",
"sharedFiles": "ファイル"
} }
} }

View File

@@ -885,7 +885,8 @@
"gitea": { "gitea": {
"notifications": "알림", "notifications": "알림",
"issues": "Issues", "issues": "Issues",
"pulls": "Pull Requests" "pulls": "Pull Requests",
"repositories": "Repositories"
}, },
"stash": { "stash": {
"scenes": "장면", "scenes": "장면",
@@ -1030,5 +1031,16 @@
"highlights": "Highlights", "highlights": "Highlights",
"lists": "Lists", "lists": "Lists",
"tags": "태그" "tags": "태그"
},
"slskd": {
"slskStatus": "네트워크",
"connected": "연결됨",
"disconnected": "연결 끊김",
"updateStatus": "Update",
"update_yes": "이용 가능",
"update_no": "최신 상태",
"downloads": "Downloads",
"uploads": "Uploads",
"sharedFiles": "파일"
} }
} }

View File

@@ -885,7 +885,8 @@
"gitea": { "gitea": {
"notifications": "Notifications", "notifications": "Notifications",
"issues": "Issues", "issues": "Issues",
"pulls": "Pull Requests" "pulls": "Pull Requests",
"repositories": "Repositories"
}, },
"stash": { "stash": {
"scenes": "Scenes", "scenes": "Scenes",
@@ -1030,5 +1031,16 @@
"highlights": "Highlights", "highlights": "Highlights",
"lists": "Lists", "lists": "Lists",
"tags": "Tags" "tags": "Tags"
},
"slskd": {
"slskStatus": "Network",
"connected": "Connected",
"disconnected": "Disconnected",
"updateStatus": "Update",
"update_yes": "Available",
"update_no": "Up to Date",
"downloads": "Downloads",
"uploads": "Uploads",
"sharedFiles": "Files"
} }
} }

View File

@@ -885,7 +885,8 @@
"gitea": { "gitea": {
"notifications": "Notifications", "notifications": "Notifications",
"issues": "Issues", "issues": "Issues",
"pulls": "Pull Requests" "pulls": "Pull Requests",
"repositories": "Repositories"
}, },
"stash": { "stash": {
"scenes": "Adegan", "scenes": "Adegan",
@@ -1030,5 +1031,16 @@
"highlights": "Highlights", "highlights": "Highlights",
"lists": "Lists", "lists": "Lists",
"tags": "Tanda nama" "tags": "Tanda nama"
},
"slskd": {
"slskStatus": "Rangkaian",
"connected": "Connected",
"disconnected": "Sambungan Terputus",
"updateStatus": "Update",
"update_yes": "Sudah Ada",
"update_no": "Terkemaskini",
"downloads": "Downloads",
"uploads": "Uploads",
"sharedFiles": "Files"
} }
} }

View File

@@ -885,7 +885,8 @@
"gitea": { "gitea": {
"notifications": "Notificaties", "notifications": "Notificaties",
"issues": "Problemen", "issues": "Problemen",
"pulls": "Pull Requests" "pulls": "Pull Requests",
"repositories": "Repositories"
}, },
"stash": { "stash": {
"scenes": "Scènes", "scenes": "Scènes",
@@ -1030,5 +1031,16 @@
"highlights": "Highlights", "highlights": "Highlights",
"lists": "Lists", "lists": "Lists",
"tags": "Label" "tags": "Label"
},
"slskd": {
"slskStatus": "Netwerk",
"connected": "Verbonden",
"disconnected": "Verbinding verbroken",
"updateStatus": "Update",
"update_yes": "Beschikbaar",
"update_no": "Bijgewerkt",
"downloads": "Downloads",
"uploads": "Uploads",
"sharedFiles": "Bestanden"
} }
} }

View File

@@ -885,7 +885,8 @@
"gitea": { "gitea": {
"notifications": "Varslinger", "notifications": "Varslinger",
"issues": "Issues", "issues": "Issues",
"pulls": "Forespørsel" "pulls": "Forespørsel",
"repositories": "Repositories"
}, },
"stash": { "stash": {
"scenes": "Scener", "scenes": "Scener",
@@ -1030,5 +1031,16 @@
"highlights": "Highlights", "highlights": "Highlights",
"lists": "Lists", "lists": "Lists",
"tags": "Stikkord" "tags": "Stikkord"
},
"slskd": {
"slskStatus": "Nettverk",
"connected": "Tilkoblet",
"disconnected": "Frakoblet",
"updateStatus": "Update",
"update_yes": "Tilgjengelig",
"update_no": "Oppdatert",
"downloads": "Downloads",
"uploads": "Uploads",
"sharedFiles": "Files"
} }
} }

View File

@@ -885,7 +885,8 @@
"gitea": { "gitea": {
"notifications": "Powiadomienia", "notifications": "Powiadomienia",
"issues": "Zgłoszenia", "issues": "Zgłoszenia",
"pulls": "Żądania Pull" "pulls": "Żądania Pull",
"repositories": "Repositories"
}, },
"stash": { "stash": {
"scenes": "Sceny", "scenes": "Sceny",
@@ -1030,5 +1031,16 @@
"highlights": "Highlights", "highlights": "Highlights",
"lists": "Lists", "lists": "Lists",
"tags": "Tagi" "tags": "Tagi"
},
"slskd": {
"slskStatus": "Sieć",
"connected": "Połączono",
"disconnected": "Rozłączono",
"updateStatus": "Update",
"update_yes": "Dostępne",
"update_no": "Aktualny",
"downloads": "Downloads",
"uploads": "Uploads",
"sharedFiles": "Pliki"
} }
} }

View File

@@ -885,7 +885,8 @@
"gitea": { "gitea": {
"notifications": "Notificações", "notifications": "Notificações",
"issues": "Problemas", "issues": "Problemas",
"pulls": "Solicitar pull" "pulls": "Solicitar pull",
"repositories": "Repositories"
}, },
"stash": { "stash": {
"scenes": "Cenas", "scenes": "Cenas",
@@ -1030,5 +1031,16 @@
"highlights": "Highlights", "highlights": "Highlights",
"lists": "Lists", "lists": "Lists",
"tags": "Etiquetas" "tags": "Etiquetas"
},
"slskd": {
"slskStatus": "Rede",
"connected": "Conectado",
"disconnected": "Desconectado",
"updateStatus": "Update",
"update_yes": "Disponível",
"update_no": "Atualizado",
"downloads": "Downloads",
"uploads": "Uploads",
"sharedFiles": "Ficheiros"
} }
} }

View File

@@ -47,7 +47,7 @@
"load": "Carga", "load": "Carga",
"temp": "TEMP", "temp": "TEMP",
"max": "Máximo", "max": "Máximo",
"uptime": "CIMA" "uptime": "ATIVO"
}, },
"unifi": { "unifi": {
"users": "Usuários", "users": "Usuários",
@@ -61,7 +61,7 @@
"wlan_devices": "Dispositivos WLAN", "wlan_devices": "Dispositivos WLAN",
"lan_users": "Usuários de LAN", "lan_users": "Usuários de LAN",
"wlan_users": "Usuários de WLAN", "wlan_users": "Usuários de WLAN",
"up": "CIMA", "up": "ATIVO",
"down": "Desligado", "down": "Desligado",
"wait": "Por favor, aguarde", "wait": "Por favor, aguarde",
"empty_data": "Status do Subsistema desconhecido" "empty_data": "Status do Subsistema desconhecido"
@@ -149,8 +149,8 @@
"received": "Recebido", "received": "Recebido",
"sent": "Enviado", "sent": "Enviado",
"externalIPAddress": "IP Externo", "externalIPAddress": "IP Externo",
"externalIPv6Address": "Ext. IPv6", "externalIPv6Address": "IPv6 Externo",
"externalIPv6Prefix": "Ext. IPv6-Prefix" "externalIPv6Prefix": "Prefixo IPv6 Externo"
}, },
"caddy": { "caddy": {
"upstreams": "Streams de Envio", "upstreams": "Streams de Envio",
@@ -178,7 +178,7 @@
"connectedAp": "APs Ligados", "connectedAp": "APs Ligados",
"activeUser": "Dispositivos ativos", "activeUser": "Dispositivos ativos",
"alerts": "Alertas", "alerts": "Alertas",
"connectedGateways": "Connected gateways", "connectedGateways": "Gateways conectados",
"connectedSwitches": "Switches conectados" "connectedSwitches": "Switches conectados"
}, },
"nzbget": { "nzbget": {
@@ -229,8 +229,8 @@
"seed": "Semente" "seed": "Semente"
}, },
"develancacheui": { "develancacheui": {
"cachehitbytes": "Cache Hit Bytes", "cachehitbytes": "Bytes de Acerto de Cache",
"cachemissbytes": "Cache Miss Bytes" "cachemissbytes": "Bytes de Falha de Cache"
}, },
"downloadstation": { "downloadstation": {
"download": "Descarregar", "download": "Descarregar",
@@ -313,13 +313,13 @@
}, },
"suwayomi": { "suwayomi": {
"download": "Baixado", "download": "Baixado",
"nondownload": "Non-Downloaded", "nondownload": "Não Baixado",
"read": "Lido", "read": "Lido",
"unread": "Não lida", "unread": "Não lida",
"downloadedread": "Downloaded & Read", "downloadedread": "Baixado e Lido",
"downloadedunread": "Downloaded & Unread", "downloadedunread": "Baixado e Não Lido",
"nondownloadedread": "Non-Downloaded & Read", "nondownloadedread": "Não Baixado e Lido",
"nondownloadedunread": "Non-Downloaded & Unread" "nondownloadedunread": "Não Baixado e Não Lido"
}, },
"tailscale": { "tailscale": {
"address": "Endereço", "address": "Endereço",
@@ -337,15 +337,15 @@
}, },
"technitium": { "technitium": {
"totalQueries": "Consultas", "totalQueries": "Consultas",
"totalNoError": "Success", "totalNoError": "Sucesso",
"totalServerFailure": "Failures", "totalServerFailure": "Falhas",
"totalNxDomain": "NX Domains", "totalNxDomain": "Domínios NX",
"totalRefused": "Refused", "totalRefused": "Recusado",
"totalAuthoritative": "Authoritative", "totalAuthoritative": "Autoritativo",
"totalRecursive": "Recursive", "totalRecursive": "Recursivo",
"totalCached": "Cached", "totalCached": "Em cache",
"totalBlocked": "Bloqueado", "totalBlocked": "Bloqueado",
"totalDropped": "Dropped", "totalDropped": "Perdidos",
"totalClients": "Clientes" "totalClients": "Clientes"
}, },
"tdarr": { "tdarr": {
@@ -436,7 +436,7 @@
"temp": "TEMP", "temp": "TEMP",
"_temp": "Temperatura", "_temp": "Temperatura",
"warn": "Aviso", "warn": "Aviso",
"uptime": "CIMA", "uptime": "ATIVO",
"total": "Total", "total": "Total",
"free": "Livre", "free": "Livre",
"used": "Utilizado", "used": "Utilizado",
@@ -705,8 +705,8 @@
"time": "Hora" "time": "Hora"
}, },
"firefly": { "firefly": {
"networth": "Net Worth", "networth": "Valor Líquido",
"budget": "Budget" "budget": "Orçamento"
}, },
"grafana": { "grafana": {
"dashboards": "Painéis", "dashboards": "Painéis",
@@ -860,16 +860,16 @@
}, },
"romm": { "romm": {
"platforms": "Plataformas", "platforms": "Plataformas",
"totalRoms": "Games", "totalRoms": "Jogos",
"saves": "Saves", "saves": "Saves",
"states": "States", "states": "Estados",
"screenshots": "Screenshots", "screenshots": "Capturas de Tela",
"totalfilesize": "Total Size" "totalfilesize": "Tamanho total"
}, },
"mailcow": { "mailcow": {
"domains": "Domínios", "domains": "Domínios",
"mailboxes": "Mailboxes", "mailboxes": "Caixas de e-mail",
"mails": "Mails", "mails": "Mensagens",
"storage": "Armazenamento" "storage": "Armazenamento"
}, },
"netdata": { "netdata": {
@@ -885,7 +885,8 @@
"gitea": { "gitea": {
"notifications": "Notificações", "notifications": "Notificações",
"issues": "Problemas", "issues": "Problemas",
"pulls": "Solicitações de Envio" "pulls": "Solicitações de Envio",
"repositories": "Repositories"
}, },
"stash": { "stash": {
"scenes": "Cenas", "scenes": "Cenas",
@@ -951,30 +952,30 @@
}, },
"linkwarden": { "linkwarden": {
"links": "Links", "links": "Links",
"collections": "Collections", "collections": "Coleções",
"tags": "Marcadores" "tags": "Marcadores"
}, },
"zabbix": { "zabbix": {
"unclassified": "Not classified", "unclassified": "Não classificado",
"information": "Informação", "information": "Informação",
"warning": "Warning", "warning": "Aviso",
"average": "Average", "average": "Médio",
"high": "High", "high": "Alto",
"disaster": "Disaster" "disaster": "Desastre"
}, },
"lubelogger": { "lubelogger": {
"vehicle": "Vehicle", "vehicle": "Veículo",
"vehicles": "Vehicles", "vehicles": "Veículos",
"serviceRecords": "Service Records", "serviceRecords": "Registros de Serviço",
"reminders": "Reminders", "reminders": "Lembretes",
"nextReminder": "Next Reminder", "nextReminder": "Próximo Lembrete",
"none": "None" "none": "Nenhum"
}, },
"vikunja": { "vikunja": {
"projects": "Active Projects", "projects": "Projetos Ativos",
"tasks7d": "Tasks Due This Week", "tasks7d": "Tarefas que vencem nesta semana",
"tasksOverdue": "Overdue Tasks", "tasksOverdue": "Tarefas Atrasadas",
"tasksInProgress": "Tasks In Progress" "tasksInProgress": "Tarefas em Andamento"
}, },
"headscale": { "headscale": {
"name": "Nome", "name": "Nome",
@@ -986,7 +987,7 @@
}, },
"beszel": { "beszel": {
"name": "Nome", "name": "Nome",
"systems": "Systems", "systems": "Sistemas",
"up": "Ativo", "up": "Ativo",
"down": "Inativo", "down": "Inativo",
"paused": "Pausado", "paused": "Pausado",
@@ -995,27 +996,27 @@
"updated": "Atualizado", "updated": "Atualizado",
"cpu": "CPU", "cpu": "CPU",
"memory": "MEM", "memory": "MEM",
"disk": "Disk", "disk": "Disco",
"network": "NET" "network": "Rede"
}, },
"argocd": { "argocd": {
"apps": "Apps", "apps": "Aplicativos",
"synced": "Synced", "synced": "Sincronizado",
"outOfSync": "Out Of Sync", "outOfSync": "Fora de sincronia",
"healthy": "Saudável", "healthy": "Saudável",
"degraded": "Degraded", "degraded": "Degradado",
"progressing": "Progressing", "progressing": "Progredindo",
"missing": "Faltando", "missing": "Faltando",
"suspended": "Suspended" "suspended": "Suspenso"
}, },
"spoolman": { "spoolman": {
"loading": "Carregando" "loading": "Carregando"
}, },
"gitlab": { "gitlab": {
"groups": "Groups", "groups": "Grupos",
"issues": "Problemas", "issues": "Problemas",
"merges": "Merge Requests", "merges": "Solicitações de mesclagem",
"projects": "Projects" "projects": "Projetos"
}, },
"apcups": { "apcups": {
"status": "Status", "status": "Status",
@@ -1024,11 +1025,22 @@
"timeleft": "Tempo restante" "timeleft": "Tempo restante"
}, },
"hoarder": { "hoarder": {
"bookmarks": "Bookmarks", "bookmarks": "Favoritos",
"favorites": "Favorites", "favorites": "Favoritos",
"archived": "Archived", "archived": "Arquivados",
"highlights": "Highlights", "highlights": "Destaques",
"lists": "Lists", "lists": "Listas",
"tags": "Marcadores" "tags": "Marcadores"
},
"slskd": {
"slskStatus": "Rede",
"connected": "Conectado",
"disconnected": "Desconectado",
"updateStatus": "Update",
"update_yes": "Disponível",
"update_no": "Atualizado",
"downloads": "Transferências",
"uploads": "Envios",
"sharedFiles": "Arquivos"
} }
} }

View File

@@ -885,7 +885,8 @@
"gitea": { "gitea": {
"notifications": "Notifications", "notifications": "Notifications",
"issues": "Issues", "issues": "Issues",
"pulls": "Pull Requests" "pulls": "Pull Requests",
"repositories": "Repositories"
}, },
"stash": { "stash": {
"scenes": "Scenes", "scenes": "Scenes",
@@ -1030,5 +1031,16 @@
"highlights": "Highlights", "highlights": "Highlights",
"lists": "Lists", "lists": "Lists",
"tags": "Tags" "tags": "Tags"
},
"slskd": {
"slskStatus": "Network",
"connected": "Connected",
"disconnected": "Disconnected",
"updateStatus": "Update",
"update_yes": "Disponibile",
"update_no": "Up to Date",
"downloads": "Downloads",
"uploads": "Uploads",
"sharedFiles": "Files"
} }
} }

View File

@@ -149,8 +149,8 @@
"received": "Получено", "received": "Получено",
"sent": "Отправлено", "sent": "Отправлено",
"externalIPAddress": "Внеш. IP", "externalIPAddress": "Внеш. IP",
"externalIPv6Address": "Ext. IPv6", "externalIPv6Address": "Внешний IPv6",
"externalIPv6Prefix": "Ext. IPv6-Prefix" "externalIPv6Prefix": "Внешний IPv6 префикс"
}, },
"caddy": { "caddy": {
"upstreams": "Входящие каналы", "upstreams": "Входящие каналы",
@@ -178,7 +178,7 @@
"connectedAp": "Подключенные точки доступа", "connectedAp": "Подключенные точки доступа",
"activeUser": "Активные устройства", "activeUser": "Активные устройства",
"alerts": "Предупреждения", "alerts": "Предупреждения",
"connectedGateways": "Connected gateways", "connectedGateways": "Подключенные шлюзы",
"connectedSwitches": "Подключенные коммутаторы" "connectedSwitches": "Подключенные коммутаторы"
}, },
"nzbget": { "nzbget": {
@@ -705,8 +705,8 @@
"time": "Время" "time": "Время"
}, },
"firefly": { "firefly": {
"networth": "Net Worth", "networth": "Общая средства",
"budget": "Budget" "budget": "Бюджет"
}, },
"grafana": { "grafana": {
"dashboards": "Панели", "dashboards": "Панели",
@@ -861,7 +861,7 @@
"romm": { "romm": {
"platforms": "Платформы", "platforms": "Платформы",
"totalRoms": "Игры", "totalRoms": "Игры",
"saves": "Сейвы", "saves": "Сохранения",
"states": "Состояния", "states": "Состояния",
"screenshots": "Скриншоты", "screenshots": "Скриншоты",
"totalfilesize": "Общий объем" "totalfilesize": "Общий объем"
@@ -885,7 +885,8 @@
"gitea": { "gitea": {
"notifications": "Уведомления", "notifications": "Уведомления",
"issues": "Вопросы", "issues": "Вопросы",
"pulls": "Запросы на слияние (Pull Request)" "pulls": "Запросы на слияние (Pull Request)",
"repositories": "Repositories"
}, },
"stash": { "stash": {
"scenes": "Сцены", "scenes": "Сцены",
@@ -927,7 +928,7 @@
"total": "Всего" "total": "Всего"
}, },
"swagdashboard": { "swagdashboard": {
"proxied": "Прокси", "proxied": "Проксировано",
"auth": "С Авторизацией", "auth": "С Авторизацией",
"outdated": "Устаревшие", "outdated": "Устаревшие",
"banned": "Заблокированные" "banned": "Заблокированные"
@@ -958,17 +959,17 @@
"unclassified": "Не классифицировано", "unclassified": "Не классифицировано",
"information": "Информация", "information": "Информация",
"warning": "Предупреждение", "warning": "Предупреждение",
"average": "Средняя", "average": "Среднее",
"high": "Высокая", "high": "Высокая",
"disaster": "Чрезвычайная" "disaster": "Чрезвычайное"
}, },
"lubelogger": { "lubelogger": {
"vehicle": "Автомобиль", "vehicle": "Транспорт",
"vehicles": "Автомобили", "vehicles": "Транспорты",
"serviceRecords": "Сервисные работы", "serviceRecords": "Сервисные записи",
"reminders": "Напоминания", "reminders": "Напоминания",
"nextReminder": "Следующее напоминание", "nextReminder": "Следующее напоминание",
"none": "Нет" "none": "Отсутствует"
}, },
"vikunja": { "vikunja": {
"projects": "Активные Проекты", "projects": "Активные Проекты",
@@ -1024,11 +1025,22 @@
"timeleft": "Осталось" "timeleft": "Осталось"
}, },
"hoarder": { "hoarder": {
"bookmarks": "Bookmarks", "bookmarks": "Закладки",
"favorites": "Favorites", "favorites": "Избранное",
"archived": "Archived", "archived": "Архивированное",
"highlights": "Highlights", "highlights": "События",
"lists": "Lists", "lists": "Список",
"tags": "Теги" "tags": "Теги"
},
"slskd": {
"slskStatus": "Сеть",
"connected": "Подключено",
"disconnected": "Отключено",
"updateStatus": "Обновление",
"update_yes": "Доступно",
"update_no": "Последняя версия",
"downloads": "Скачивания",
"uploads": "Загрузки",
"sharedFiles": "Файлов"
} }
} }

View File

@@ -885,7 +885,8 @@
"gitea": { "gitea": {
"notifications": "Oznámenia", "notifications": "Oznámenia",
"issues": "Problémy", "issues": "Problémy",
"pulls": "Pull requesty" "pulls": "Pull requesty",
"repositories": "Repositories"
}, },
"stash": { "stash": {
"scenes": "Scény", "scenes": "Scény",
@@ -1030,5 +1031,16 @@
"highlights": "Highlights", "highlights": "Highlights",
"lists": "Lists", "lists": "Lists",
"tags": "Štítky" "tags": "Štítky"
},
"slskd": {
"slskStatus": "Sieť",
"connected": "Pripojené",
"disconnected": "Odpojené",
"updateStatus": "Update",
"update_yes": "Dostupné",
"update_no": "Aktuálny",
"downloads": "Downloads",
"uploads": "Uploads",
"sharedFiles": "Súborov"
} }
} }

View File

@@ -885,7 +885,8 @@
"gitea": { "gitea": {
"notifications": "Obvestila", "notifications": "Obvestila",
"issues": "Težave", "issues": "Težave",
"pulls": "Zahteve za prenos" "pulls": "Zahteve za prenos",
"repositories": "Repositories"
}, },
"stash": { "stash": {
"scenes": "Scene", "scenes": "Scene",
@@ -1030,5 +1031,16 @@
"highlights": "Highlights", "highlights": "Highlights",
"lists": "Lists", "lists": "Lists",
"tags": "Značke" "tags": "Značke"
},
"slskd": {
"slskStatus": "Omrežje",
"connected": "Povezan",
"disconnected": "Prekinjeno",
"updateStatus": "Update",
"update_yes": "Na voljo",
"update_no": "Posodobljeno",
"downloads": "Downloads",
"uploads": "Uploads",
"sharedFiles": "Datotek"
} }
} }

View File

@@ -885,7 +885,8 @@
"gitea": { "gitea": {
"notifications": "Notifications", "notifications": "Notifications",
"issues": "Issues", "issues": "Issues",
"pulls": "Pull Requests" "pulls": "Pull Requests",
"repositories": "Repositories"
}, },
"stash": { "stash": {
"scenes": "Scenes", "scenes": "Scenes",
@@ -1030,5 +1031,16 @@
"highlights": "Highlights", "highlights": "Highlights",
"lists": "Lists", "lists": "Lists",
"tags": "Tags" "tags": "Tags"
},
"slskd": {
"slskStatus": "Network",
"connected": "Connected",
"disconnected": "Disconnected",
"updateStatus": "Update",
"update_yes": "Available",
"update_no": "Up to Date",
"downloads": "Downloads",
"uploads": "Uploads",
"sharedFiles": "Files"
} }
} }

View File

@@ -885,7 +885,8 @@
"gitea": { "gitea": {
"notifications": "Notifications", "notifications": "Notifications",
"issues": "Issues", "issues": "Issues",
"pulls": "Pull Requests" "pulls": "Pull Requests",
"repositories": "Repositories"
}, },
"stash": { "stash": {
"scenes": "Scenes", "scenes": "Scenes",
@@ -1030,5 +1031,16 @@
"highlights": "Highlights", "highlights": "Highlights",
"lists": "Lists", "lists": "Lists",
"tags": "Tags" "tags": "Tags"
},
"slskd": {
"slskStatus": "Network",
"connected": "Connected",
"disconnected": "Disconnected",
"updateStatus": "Update",
"update_yes": "Tillgänglig",
"update_no": "Up to Date",
"downloads": "Downloads",
"uploads": "Uploads",
"sharedFiles": "Files"
} }
} }

View File

@@ -885,7 +885,8 @@
"gitea": { "gitea": {
"notifications": "Notifications", "notifications": "Notifications",
"issues": "Issues", "issues": "Issues",
"pulls": "Pull Requests" "pulls": "Pull Requests",
"repositories": "Repositories"
}, },
"stash": { "stash": {
"scenes": "Scenes", "scenes": "Scenes",
@@ -1030,5 +1031,16 @@
"highlights": "Highlights", "highlights": "Highlights",
"lists": "Lists", "lists": "Lists",
"tags": "Tags" "tags": "Tags"
},
"slskd": {
"slskStatus": "Network",
"connected": "Connected",
"disconnected": "Disconnected",
"updateStatus": "Update",
"update_yes": "అందుబాటులో వున్నవి",
"update_no": "తాజాగా",
"downloads": "Downloads",
"uploads": "Uploads",
"sharedFiles": "Files"
} }
} }

View File

@@ -885,7 +885,8 @@
"gitea": { "gitea": {
"notifications": "Notifications", "notifications": "Notifications",
"issues": "Issues", "issues": "Issues",
"pulls": "Pull Requests" "pulls": "Pull Requests",
"repositories": "Repositories"
}, },
"stash": { "stash": {
"scenes": "Scenes", "scenes": "Scenes",
@@ -1030,5 +1031,16 @@
"highlights": "Highlights", "highlights": "Highlights",
"lists": "Lists", "lists": "Lists",
"tags": "Tags" "tags": "Tags"
},
"slskd": {
"slskStatus": "Network",
"connected": "Connected",
"disconnected": "Disconnected",
"updateStatus": "Update",
"update_yes": "Available",
"update_no": "Up to Date",
"downloads": "Downloads",
"uploads": "Uploads",
"sharedFiles": "Files"
} }
} }

View File

@@ -885,7 +885,8 @@
"gitea": { "gitea": {
"notifications": "Bildirimler", "notifications": "Bildirimler",
"issues": "Sorunlar", "issues": "Sorunlar",
"pulls": "Değişiklik İstekleri" "pulls": "Değişiklik İstekleri",
"repositories": "Repositories"
}, },
"stash": { "stash": {
"scenes": "Sahneler", "scenes": "Sahneler",
@@ -1030,5 +1031,16 @@
"highlights": "Highlights", "highlights": "Highlights",
"lists": "Lists", "lists": "Lists",
"tags": "Etiketler" "tags": "Etiketler"
},
"slskd": {
"slskStatus": "Ağ",
"connected": "Bağlandı",
"disconnected": "Bağlantı kesildi",
"updateStatus": "Update",
"update_yes": "Kullanılabilir",
"update_no": "Güncel",
"downloads": "Downloads",
"uploads": "Uploads",
"sharedFiles": "Dosyalar"
} }
} }

View File

@@ -885,7 +885,8 @@
"gitea": { "gitea": {
"notifications": "Сповіщення", "notifications": "Сповіщення",
"issues": "Питання", "issues": "Питання",
"pulls": "Pull-запити" "pulls": "Pull-запити",
"repositories": "Repositories"
}, },
"stash": { "stash": {
"scenes": "Сцени", "scenes": "Сцени",
@@ -1030,5 +1031,16 @@
"highlights": "Highlights", "highlights": "Highlights",
"lists": "Lists", "lists": "Lists",
"tags": "Теги" "tags": "Теги"
},
"slskd": {
"slskStatus": "Мережа",
"connected": "З'єднано",
"disconnected": "Відключено",
"updateStatus": "Update",
"update_yes": "Доступно",
"update_no": "Актуально",
"downloads": "Downloads",
"uploads": "Uploads",
"sharedFiles": "Файли"
} }
} }

View File

@@ -885,7 +885,8 @@
"gitea": { "gitea": {
"notifications": "Notifications", "notifications": "Notifications",
"issues": "Issues", "issues": "Issues",
"pulls": "Pull Requests" "pulls": "Pull Requests",
"repositories": "Repositories"
}, },
"stash": { "stash": {
"scenes": "Scenes", "scenes": "Scenes",
@@ -1030,5 +1031,16 @@
"highlights": "Highlights", "highlights": "Highlights",
"lists": "Lists", "lists": "Lists",
"tags": "Tags" "tags": "Tags"
},
"slskd": {
"slskStatus": "Network",
"connected": "Connected",
"disconnected": "Disconnected",
"updateStatus": "Update",
"update_yes": "Available",
"update_no": "Up to Date",
"downloads": "Downloads",
"uploads": "Uploads",
"sharedFiles": "Files"
} }
} }

View File

@@ -885,7 +885,8 @@
"gitea": { "gitea": {
"notifications": "信息", "notifications": "信息",
"issues": "出版", "issues": "出版",
"pulls": "提取請求" "pulls": "提取請求",
"repositories": "Repositories"
}, },
"stash": { "stash": {
"scenes": "場景", "scenes": "場景",
@@ -1030,5 +1031,16 @@
"highlights": "Highlights", "highlights": "Highlights",
"lists": "Lists", "lists": "Lists",
"tags": "標籤" "tags": "標籤"
},
"slskd": {
"slskStatus": "網絡",
"connected": "Connected",
"disconnected": "連接已中斷",
"updateStatus": "Update",
"update_yes": "可用",
"update_no": "已更新至最新",
"downloads": "Downloads",
"uploads": "Uploads",
"sharedFiles": "檔案"
} }
} }

View File

@@ -885,7 +885,8 @@
"gitea": { "gitea": {
"notifications": "通知", "notifications": "通知",
"issues": "问题", "issues": "问题",
"pulls": "PR" "pulls": "PR",
"repositories": "Repositories"
}, },
"stash": { "stash": {
"scenes": "场景", "scenes": "场景",
@@ -1030,5 +1031,16 @@
"highlights": "Highlights", "highlights": "Highlights",
"lists": "Lists", "lists": "Lists",
"tags": "Tags" "tags": "Tags"
},
"slskd": {
"slskStatus": "网络",
"connected": "已连接",
"disconnected": "未连接",
"updateStatus": "Update",
"update_yes": "可用",
"update_no": "Up to Date",
"downloads": "Downloads",
"uploads": "Uploads",
"sharedFiles": "Files"
} }
} }

View File

@@ -885,7 +885,8 @@
"gitea": { "gitea": {
"notifications": "信息", "notifications": "信息",
"issues": "出版", "issues": "出版",
"pulls": "提取請求" "pulls": "提取請求",
"repositories": "Repositories"
}, },
"stash": { "stash": {
"scenes": "場景", "scenes": "場景",
@@ -1030,5 +1031,16 @@
"highlights": "Highlights", "highlights": "Highlights",
"lists": "Lists", "lists": "Lists",
"tags": "標籤" "tags": "標籤"
},
"slskd": {
"slskStatus": "網絡",
"connected": "Connected",
"disconnected": "連接已中斷",
"updateStatus": "Update",
"update_yes": "可觀看",
"update_no": "已更新至最新",
"downloads": "Downloads",
"uploads": "Uploads",
"sharedFiles": "檔案"
} }
} }

View File

@@ -12,6 +12,7 @@ export default function BookmarksGroup({
disableCollapse, disableCollapse,
groupsInitiallyCollapsed, groupsInitiallyCollapsed,
bookmarksStyle, bookmarksStyle,
maxGroupColumns,
}) { }) {
const panel = useRef(); const panel = useRef();
@@ -25,6 +26,9 @@ export default function BookmarksGroup({
className={classNames( className={classNames(
"bookmark-group flex-1 overflow-hidden", "bookmark-group flex-1 overflow-hidden",
layout?.style === "row" ? "basis-full" : "basis-full md:basis-1/4 lg:basis-1/5 xl:basis-1/6", layout?.style === "row" ? "basis-full" : "basis-full md:basis-1/4 lg:basis-1/5 xl:basis-1/6",
layout?.style !== "row" && maxGroupColumns && parseInt(maxGroupColumns, 10) > 6
? `3xl:basis-1/${maxGroupColumns}`
: "",
layout?.header === false ? "px-1" : "p-1 pb-0", layout?.header === false ? "px-1" : "p-1 pb-0",
)} )}
> >

View File

@@ -22,7 +22,7 @@ export default function Item({ bookmark, iconOnly = false }) {
className={classNames( className={classNames(
settings.cardBlur !== undefined && `backdrop-blur${settings.cardBlur.length ? "-" : ""}${settings.cardBlur}`, settings.cardBlur !== undefined && `backdrop-blur${settings.cardBlur.length ? "-" : ""}${settings.cardBlur}`,
"text-left cursor-pointer transition-all rounded-md font-medium text-theme-700 dark:text-theme-200 dark:hover:text-theme-300 shadow-md shadow-theme-900/10 dark:shadow-theme-900/20 bg-theme-100/20 hover:bg-theme-300/20 dark:bg-white/5 dark:hover:bg-white/10", "text-left cursor-pointer transition-all rounded-md font-medium text-theme-700 dark:text-theme-200 dark:hover:text-theme-300 shadow-md shadow-theme-900/10 dark:shadow-theme-900/20 bg-theme-100/20 hover:bg-theme-300/20 dark:bg-white/5 dark:hover:bg-white/10",
iconOnly ? "h-[60px] w-[60px] grid" : "block w-full h-8 mb-3", iconOnly ? "h-[60px] w-[60px] grid" : "block w-full mb-3",
)} )}
> >
{iconOnly ? ( {iconOnly ? (

View File

@@ -10,7 +10,7 @@ import { columnMap } from "../../utils/layout/columns";
export default function ServicesGroup({ export default function ServicesGroup({
group, group,
layout, layout,
fiveColumns, maxGroupColumns,
disableCollapse, disableCollapse,
useEqualHeights, useEqualHeights,
groupsInitiallyCollapsed, groupsInitiallyCollapsed,
@@ -31,7 +31,7 @@ export default function ServicesGroup({
className={classNames( className={classNames(
"services-group flex-1", "services-group flex-1",
layout?.style === "row" ? "basis-full" : "basis-full md:basis-1/2 lg:basis-1/3 xl:basis-1/4", layout?.style === "row" ? "basis-full" : "basis-full md:basis-1/2 lg:basis-1/3 xl:basis-1/4",
layout?.style !== "row" && fiveColumns ? "3xl:basis-1/5" : "", layout?.style !== "row" && maxGroupColumns ? `3xl:basis-1/${maxGroupColumns}` : "",
groupPadding, groupPadding,
isSubgroup ? "subgroup" : "", isSubgroup ? "subgroup" : "",
)} )}
@@ -97,7 +97,7 @@ export default function ServicesGroup({
key={subgroup.name} key={subgroup.name}
group={subgroup} group={subgroup}
layout={layout?.[subgroup.name]} layout={layout?.[subgroup.name]}
fiveColumns={fiveColumns} maxGroupColumns={maxGroupColumns}
disableCollapse={disableCollapse} disableCollapse={disableCollapse}
useEqualHeights={useEqualHeights} useEqualHeights={useEqualHeights}
groupsInitiallyCollapsed={groupsInitiallyCollapsed} groupsInitiallyCollapsed={groupsInitiallyCollapsed}

View File

@@ -4,11 +4,12 @@ export function middleware(req) {
// Check the Host header, if HOMEPAGE_ALLOWED_HOSTS is set // Check the Host header, if HOMEPAGE_ALLOWED_HOSTS is set
const host = req.headers.get("host"); const host = req.headers.get("host");
const port = process.env.PORT || 3000; const port = process.env.PORT || 3000;
let allowedHosts = [`localhost:${port}`]; let allowedHosts = [`localhost:${port}`, `127.0.0.1:${port}`];
const allowAll = process.env.HOMEPAGE_ALLOWED_HOSTS === "*";
if (process.env.HOMEPAGE_ALLOWED_HOSTS) { if (process.env.HOMEPAGE_ALLOWED_HOSTS) {
allowedHosts = allowedHosts.concat(process.env.HOMEPAGE_ALLOWED_HOSTS.split(",")); allowedHosts = allowedHosts.concat(process.env.HOMEPAGE_ALLOWED_HOSTS.split(","));
} }
if (!host || !allowedHosts.includes(host)) { if (!allowAll && (!host || !allowedHosts.includes(host))) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error( console.error(
`Host validation failed for: ${host}. Hint: Set the HOMEPAGE_ALLOWED_HOSTS environment variable to allow requests from this host / port.`, `Host validation failed for: ${host}. Hint: Set the HOMEPAGE_ALLOWED_HOSTS environment variable to allow requests from this host / port.`,

View File

@@ -12,6 +12,61 @@ import { TabProvider } from "utils/contexts/tab";
import nextI18nextConfig from "../../next-i18next.config"; import nextI18nextConfig from "../../next-i18next.config";
// eslint-disable-next-line no-unused-vars
const tailwindSafelist = [
// TODO: remove pending https://github.com/tailwindlabs/tailwindcss/pull/17147
"backdrop-blur",
"backdrop-blur-xs",
"backdrop-blur-sm",
"backdrop-blur-md",
"backdrop-blur-xl",
"backdrop-saturate-0",
"backdrop-saturate-50",
"backdrop-saturate-100",
"backdrop-saturate-150",
"backdrop-saturate-200",
"backdrop-brightness-0",
"backdrop-brightness-50",
"backdrop-brightness-75",
"backdrop-brightness-90",
"backdrop-brightness-95",
"backdrop-brightness-100",
"backdrop-brightness-105",
"backdrop-brightness-110",
"backdrop-brightness-125",
"backdrop-brightness-150",
"backdrop-brightness-200",
"grid-cols-1",
"md:grid-cols-1",
"md:grid-cols-2",
"lg:grid-cols-1",
"lg:grid-cols-2",
"lg:grid-cols-3",
"lg:grid-cols-4",
"lg:grid-cols-5",
"lg:grid-cols-6",
"lg:grid-cols-7",
"lg:grid-cols-8",
// for status
"bg-white",
"bg-black",
"dark:bg-white",
"bg-orange-400",
"dark:bg-orange-400",
// maxGroupColumns
"3xl:basis-1/5",
"3xl:basis-1/6",
"3xl:basis-1/7",
"3xl:basis-1/8",
// yep
"h-0 h-1 h-2 h-3 h-4 h-5 h-6 h-7 h-8 h-9 h-10 h-11 h-12 h-13 h-14 h-15 h-16 h-17 h-18 h-19 h-20 h-21 h-22 h-23 h-24 h-25 h-26 h-27 h-28 h-29 h-30 h-31 h-32 h-33 h-34 h-35 h-36 h-37 h-38 h-39 h-40 h-41 h-42 h-43 h-44 h-45 h-46 h-47 h-48 h-49 h-50 h-51 h-52 h-53 h-54 h-55 h-56 h-57 h-58 h-59 h-60 h-61 h-62 h-63 h-64 h-65 h-66 h-67 h-68 h-69 h-70 h-71 h-72 h-73 h-74 h-75 h-76 h-77 h-78 h-79 h-80 h-81 h-82 h-83 h-84 h-85 h-86 h-87 h-88 h-89 h-90 h-91 h-92 h-93 h-94 h-95 h-96",
"sm:h-0 sm:h-1 sm:h-2 sm:h-3 sm:h-4 sm:h-5 sm:h-6 sm:h-7 sm:h-8 sm:h-9 sm:h-10 sm:h-11 sm:h-12 sm:h-13 sm:h-14 sm:h-15 sm:h-16 sm:h-17 sm:h-18 sm:h-19 sm:h-20 sm:h-21 sm:h-22 sm:h-23 sm:h-24 sm:h-25 sm:h-26 sm:h-27 sm:h-28 sm:h-29 sm:h-30 sm:h-31 sm:h-32 sm:h-33 sm:h-34 sm:h-35 sm:h-36 sm:h-37 sm:h-38 sm:h-39 sm:h-40 sm:h-41 sm:h-42 sm:h-43 sm:h-44 sm:h-45 sm:h-46 sm:h-47 sm:h-48 sm:h-49 sm:h-50 sm:h-51 sm:h-52 sm:h-53 sm:h-54 sm:h-55 sm:h-56 sm:h-57 sm:h-58 sm:h-59 sm:h-60 sm:h-61 sm:h-62 sm:h-63 sm:h-64 sm:h-65 sm:h-66 sm:h-67 sm:h-68 sm:h-69 sm:h-70 sm:h-71 sm:h-72 sm:h-73 sm:h-74 sm:h-75 sm:h-76 sm:h-77 sm:h-78 sm:h-79 sm:h-80 sm:h-81 sm:h-82 sm:h-83 sm:h-84 sm:h-85 sm:h-86 sm:h-87 sm:h-88 sm:h-89 sm:h-90 sm:h-91 sm:h-92 sm:h-93 sm:h-94 sm:h-95 sm:h-96",
"md:h-0 md:h-1 md:h-2 md:h-3 md:h-4 md:h-5 md:h-6 md:h-7 md:h-8 md:h-9 md:h-10 md:h-11 md:h-12 md:h-13 md:h-14 md:h-15 md:h-16 md:h-17 md:h-18 md:h-19 md:h-20 md:h-21 md:h-22 md:h-23 md:h-24 md:h-25 md:h-26 md:h-27 md:h-28 md:h-29 md:h-30 md:h-31 md:h-32 md:h-33 md:h-34 md:h-35 md:h-36 md:h-37 md:h-38 md:h-39 md:h-40 md:h-41 md:h-42 md:h-43 md:h-44 md:h-45 md:h-46 md:h-47 md:h-48 md:h-49 md:h-50 md:h-51 md:h-52 md:h-53 md:h-54 md:h-55 md:h-56 md:h-57 md:h-58 md:h-59 md:h-60 md:h-61 md:h-62 md:h-63 md:h-64 md:h-65 md:h-66 md:h-67 md:h-68 md:h-69 md:h-70 md:h-71 md:h-72 md:h-73 md:h-74 md:h-75 md:h-76 md:h-77 md:h-78 md:h-79 md:h-80 md:h-81 md:h-82 md:h-83 md:h-84 md:h-85 md:h-86 md:h-87 md:h-88 md:h-89 md:h-90 md:h-91 md:h-92 md:h-93 md:h-94 md:h-95 md:h-96",
"lg:h-0 lg:h-1 lg:h-2 lg:h-3 lg:h-4 lg:h-5 lg:h-6 lg:h-7 lg:h-8 lg:h-9 lg:h-10 lg:h-11 lg:h-12 lg:h-13 lg:h-14 lg:h-15 lg:h-16 lg:h-17 lg:h-18 lg:h-19 lg:h-20 lg:h-21 lg:h-22 lg:h-23 lg:h-24 lg:h-25 lg:h-26 lg:h-27 lg:h-28 lg:h-29 lg:h-30 lg:h-31 lg:h-32 lg:h-33 lg:h-34 lg:h-35 lg:h-36 lg:h-37 lg:h-38 lg:h-39 lg:h-40 lg:h-41 lg:h-42 lg:h-43 lg:h-44 lg:h-45 lg:h-46 lg:h-47 lg:h-48 lg:h-49 lg:h-50 lg:h-51 lg:h-52 lg:h-53 lg:h-54 lg:h-55 lg:h-56 lg:h-57 lg:h-58 lg:h-59 lg:h-60 lg:h-61 lg:h-62 lg:h-63 lg:h-64 lg:h-65 lg:h-66 lg:h-67 lg:h-68 lg:h-69 lg:h-70 lg:h-71 lg:h-72 lg:h-73 lg:h-74 lg:h-75 lg:h-76 lg:h-77 lg:h-78 lg:h-79 lg:h-80 lg:h-81 lg:h-82 lg:h-83 lg:h-84 lg:h-85 lg:h-86 lg:h-87 lg:h-88 lg:h-89 lg:h-90 lg:h-91 lg:h-92 lg:h-93 lg:h-94 lg:h-95 lg:h-96",
"xl:h-0 xl:h-1 xl:h-2 xl:h-3 xl:h-4 xl:h-5 xl:h-6 xl:h-7 xl:h-8 xl:h-9 xl:h-10 xl:h-11 xl:h-12 xl:h-13 xl:h-14 xl:h-15 xl:h-16 xl:h-17 xl:h-18 xl:h-19 xl:h-20 xl:h-21 xl:h-22 xl:h-23 xl:h-24 xl:h-25 xl:h-26 xl:h-27 xl:h-28 xl:h-29 xl:h-30 xl:h-31 xl:h-32 xl:h-33 xl:h-34 xl:h-35 xl:h-36 xl:h-37 xl:h-38 xl:h-39 xl:h-40 xl:h-41 xl:h-42 xl:h-43 xl:h-44 xl:h-45 xl:h-46 xl:h-47 xl:h-48 xl:h-49 xl:h-50 xl:h-51 xl:h-52 xl:h-53 xl:h-54 xl:h-55 xl:h-56 xl:h-57 xl:h-58 xl:h-59 xl:h-60 xl:h-61 xl:h-62 xl:h-63 xl:h-64 xl:h-65 xl:h-66 xl:h-67 xl:h-68 xl:h-69 xl:h-70 xl:h-71 xl:h-72 xl:h-73 xl:h-74 xl:h-75 xl:h-76 xl:h-77 xl:h-78 xl:h-79 xl:h-80 xl:h-81 xl:h-82 xl:h-83 xl:h-84 xl:h-85 xl:h-86 xl:h-87 xl:h-88 xl:h-89 xl:h-90 xl:h-91 xl:h-92 xl:h-93 xl:h-94 xl:h-95 xl: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",
];
function MyApp({ Component, pageProps }) { function MyApp({ Component, pageProps }) {
return ( return (
<SWRConfig <SWRConfig

View File

@@ -71,8 +71,8 @@ export default async function handler(req, res) {
let depMem = 0; let depMem = 0;
let depCpu = 0; let depCpu = 0;
const podMetrics = await metricsApi const podMetrics = await metricsApi
.getPodMetrics(namespace, pod.metadata.name) .getPodMetrics(namespace, pod.items)
.then((response) => response) .then((response) => response.items)
.catch((err) => { .catch((err) => {
// 404 generally means that the metrics have not been populated yet // 404 generally means that the metrics have not been populated yet
if (err.statusCode !== 404) { if (err.statusCode !== 404) {
@@ -81,9 +81,11 @@ export default async function handler(req, res) {
return null; return null;
}); });
if (podMetrics) { if (podMetrics) {
podMetrics.containers.forEach((container) => { podMetrics.forEach((metrics) => {
depMem += parseMemory(container.usage.memory); metrics.containers.forEach((container) => {
depCpu += parseCpu(container.usage.cpu); depMem += parseMemory(container.usage.memory);
depCpu += parseCpu(container.usage.cpu);
});
}); });
} }
return { return {

View File

@@ -1,4 +1,4 @@
import cachedFetch from "utils/proxy/cached-fetch"; import { cachedRequest } from "utils/proxy/http";
import createLogger from "utils/logger"; import createLogger from "utils/logger";
const logger = createLogger("releases"); const logger = createLogger("releases");
@@ -6,7 +6,7 @@ const logger = createLogger("releases");
export default async function handler(req, res) { export default async function handler(req, res) {
const releasesURL = "https://api.github.com/repos/gethomepage/homepage/releases"; const releasesURL = "https://api.github.com/repos/gethomepage/homepage/releases";
try { try {
return res.send(await cachedFetch(releasesURL, 5)); return res.send(await cachedRequest(releasesURL, 5));
} catch (e) { } catch (e) {
logger.error(`Error checking GitHub releases: ${e}`); logger.error(`Error checking GitHub releases: ${e}`);
return res.send([]); return res.send([]);

View File

@@ -1,6 +1,7 @@
import { searchProviders } from "components/widgets/search/search"; import { searchProviders } from "components/widgets/search/search";
import cachedFetch from "utils/proxy/cached-fetch"; import { getSettings } from "utils/config/config";
import { cachedRequest } from "utils/proxy/http";
import { widgetsFromConfig } from "utils/config/widget-helpers"; import { widgetsFromConfig } from "utils/config/widget-helpers";
export default async function handler(req, res) { export default async function handler(req, res) {
@@ -12,13 +13,21 @@ export default async function handler(req, res) {
const widgets = await widgetsFromConfig(); const widgets = await widgetsFromConfig();
const searchWidget = widgets.find((w) => w.type === "search"); const searchWidget = widgets.find((w) => w.type === "search");
provider.url = searchWidget.options.url; if (searchWidget) {
provider.suggestionUrl = searchWidget.options.suggestionUrl; provider.url = searchWidget.options.url;
provider.suggestionUrl = searchWidget.options.suggestionUrl;
} else {
const settings = getSettings();
if (settings.quicklaunch && settings.quicklaunch.provider === "custom") {
provider.url = settings.quicklaunch.url;
provider.suggestionUrl = settings.quicklaunch.suggestionUrl;
}
}
} }
if (!provider.suggestionUrl) { if (!provider.suggestionUrl) {
return res.json([query, []]); // Responde with the same array format but with no suggestions. return res.json([query, []]); // Responde with the same array format but with no suggestions.
} }
return res.send(await cachedFetch(`${provider.suggestionUrl}${encodeURIComponent(query)}`, 5, "Mozilla/5.0")); return res.send(await cachedRequest(`${provider.suggestionUrl}${encodeURIComponent(query)}`, 5, "Mozilla/5.0"));
} }

View File

@@ -1,9 +1,9 @@
import cachedFetch from "utils/proxy/cached-fetch"; import { cachedRequest } from "utils/proxy/http";
export default async function handler(req, res) { export default async function handler(req, res) {
const { latitude, longitude, units, cache, timezone } = req.query; const { latitude, longitude, units, cache, timezone } = req.query;
const degrees = units === "metric" ? "celsius" : "fahrenheit"; const degrees = units === "metric" ? "celsius" : "fahrenheit";
const timezeone = timezone ?? "auto"; const timezeone = timezone ?? "auto";
const apiUrl = `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}&daily=sunrise,sunset&current_weather=true&temperature_unit=${degrees}&timezone=${timezeone}`; const apiUrl = `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}&daily=sunrise,sunset&current_weather=true&temperature_unit=${degrees}&timezone=${timezeone}`;
return res.send(await cachedFetch(apiUrl, cache)); return res.send(await cachedRequest(apiUrl, cache));
} }

View File

@@ -1,4 +1,4 @@
import cachedFetch from "utils/proxy/cached-fetch"; import { cachedRequest } from "utils/proxy/http";
import { getSettings } from "utils/config/config"; import { getSettings } from "utils/config/config";
import { getPrivateWidgetOptions } from "utils/config/widget-helpers"; import { getPrivateWidgetOptions } from "utils/config/widget-helpers";
@@ -26,5 +26,5 @@ export default async function handler(req, res) {
const apiUrl = `https://api.openweathermap.org/data/2.5/weather?lat=${latitude}&lon=${longitude}&appid=${apiKey}&units=${units}&lang=${lang}`; const apiUrl = `https://api.openweathermap.org/data/2.5/weather?lat=${latitude}&lon=${longitude}&appid=${apiKey}&units=${units}&lang=${lang}`;
return res.send(await cachedFetch(apiUrl, cache)); return res.send(await cachedRequest(apiUrl, cache));
} }

View File

@@ -1,4 +1,4 @@
import cachedFetch from "utils/proxy/cached-fetch"; import { cachedRequest } from "utils/proxy/http";
import { getSettings } from "utils/config/config"; import { getSettings } from "utils/config/config";
import createLogger from "utils/logger"; import createLogger from "utils/logger";
@@ -60,7 +60,7 @@ export default async function handler(req, res) {
const apiUrl = `https://finnhub.io/api/v1/quote?symbol=${ticker}&token=${apiKey}`; const apiUrl = `https://finnhub.io/api/v1/quote?symbol=${ticker}&token=${apiKey}`;
// Finnhub free accounts allow up to 60 calls/minute // Finnhub free accounts allow up to 60 calls/minute
// https://finnhub.io/pricing // https://finnhub.io/pricing
const { c, dp } = await cachedFetch(apiUrl, cache || 1); const { c, dp } = await cachedRequest(apiUrl, cache || 1);
logger.debug("Finnhub API response for %s: %o", ticker, { c, dp }); logger.debug("Finnhub API response for %s: %o", ticker, { c, dp });
// API sometimes returns 200, but values returned are `null` // API sometimes returns 200, but values returned are `null`

View File

@@ -1,4 +1,4 @@
import cachedFetch from "utils/proxy/cached-fetch"; import { cachedRequest } from "utils/proxy/http";
import { getSettings } from "utils/config/config"; import { getSettings } from "utils/config/config";
import { getPrivateWidgetOptions } from "utils/config/widget-helpers"; import { getPrivateWidgetOptions } from "utils/config/widget-helpers";
@@ -26,5 +26,5 @@ export default async function handler(req, res) {
const apiUrl = `http://api.weatherapi.com/v1/current.json?q=${latitude},${longitude}&key=${apiKey}&lang=${lang}`; const apiUrl = `http://api.weatherapi.com/v1/current.json?q=${latitude},${longitude}&key=${apiKey}&lang=${lang}`;
return res.send(await cachedFetch(apiUrl, cache)); return res.send(await cachedRequest(apiUrl, cache));
} }

View File

@@ -323,7 +323,7 @@ function Home({ initialSettings }) {
key={group.name} key={group.name}
group={group} group={group}
layout={settings.layout?.[group.name]} layout={settings.layout?.[group.name]}
fiveColumns={settings.fiveColumns} maxGroupColumns={settings.fiveColumns ? 5 : settings.maxGroupColumns}
disableCollapse={settings.disableCollapse} disableCollapse={settings.disableCollapse}
useEqualHeights={settings.useEqualHeights} useEqualHeights={settings.useEqualHeights}
groupsInitiallyCollapsed={settings.groupsInitiallyCollapsed} groupsInitiallyCollapsed={settings.groupsInitiallyCollapsed}
@@ -334,6 +334,7 @@ function Home({ initialSettings }) {
bookmarks={group} bookmarks={group}
layout={settings.layout?.[group.name]} layout={settings.layout?.[group.name]}
disableCollapse={settings.disableCollapse} disableCollapse={settings.disableCollapse}
maxGroupColumns={settings.maxBookmarkGroupColumns ?? settings.maxGroupColumns}
groupsInitiallyCollapsed={settings.groupsInitiallyCollapsed} groupsInitiallyCollapsed={settings.groupsInitiallyCollapsed}
/> />
), ),
@@ -347,7 +348,7 @@ function Home({ initialSettings }) {
key={group.name} key={group.name}
group={group} group={group}
layout={settings.layout?.[group.name]} layout={settings.layout?.[group.name]}
fiveColumns={settings.fiveColumns} maxGroupColumns={settings.fiveColumns ? 5 : settings.maxGroupColumns}
disableCollapse={settings.disableCollapse} disableCollapse={settings.disableCollapse}
groupsInitiallyCollapsed={settings.groupsInitiallyCollapsed} groupsInitiallyCollapsed={settings.groupsInitiallyCollapsed}
/> />
@@ -362,6 +363,7 @@ function Home({ initialSettings }) {
bookmarks={group} bookmarks={group}
layout={settings.layout?.[group.name]} layout={settings.layout?.[group.name]}
disableCollapse={settings.disableCollapse} disableCollapse={settings.disableCollapse}
maxGroupColumns={settings.maxBookmarkGroupColumns ?? settings.maxGroupColumns}
groupsInitiallyCollapsed={settings.groupsInitiallyCollapsed} groupsInitiallyCollapsed={settings.groupsInitiallyCollapsed}
bookmarksStyle={settings.bookmarksStyle} bookmarksStyle={settings.bookmarksStyle}
/> />
@@ -377,6 +379,8 @@ function Home({ initialSettings }) {
bookmarks, bookmarks,
settings.layout, settings.layout,
settings.fiveColumns, settings.fiveColumns,
settings.maxGroupColumns,
settings.maxBookmarkGroupColumns,
settings.disableCollapse, settings.disableCollapse,
settings.useEqualHeights, settings.useEqualHeights,
settings.cardBlur, settings.cardBlur,
@@ -417,7 +421,12 @@ function Home({ initialSettings }) {
<Script src="/api/config/custom.js" /> <Script src="/api/config/custom.js" />
<div className="relative container m-auto flex flex-col justify-start z-10 h-full"> <div
className={classNames(
settings.fullWidth ? "" : "container",
"relative m-auto flex flex-col justify-start z-10 h-full",
)}
>
<QuickLaunch <QuickLaunch
servicesAndBookmarks={servicesAndBookmarks} servicesAndBookmarks={servicesAndBookmarks}
searchString={searchString} searchString={searchString}

View File

@@ -56,21 +56,22 @@ export async function cleanWidgetGroups(widgets) {
export async function getPrivateWidgetOptions(type, widgetIndex) { export async function getPrivateWidgetOptions(type, widgetIndex) {
const widgets = await widgetsFromConfig(); const widgets = await widgetsFromConfig();
const privateOptions = widgets.map((widget) => { const privateOptions =
const { index, url, username, password, key, apiKey } = widget.options; widgets.map((widget) => {
const { index, url, username, password, key, apiKey } = widget.options;
return { return {
type: widget.type, type: widget.type,
options: { options: {
index, index,
url, url,
username, username,
password, password,
key, key,
apiKey, apiKey,
}, },
}; };
}); }) || {};
return type !== undefined && widgetIndex !== undefined return type !== undefined && widgetIndex !== undefined
? privateOptions.find((o) => o.type === type && o.options.index === parseInt(widgetIndex, 10))?.options ? privateOptions.find((o) => o.type === type && o.options.index === parseInt(widgetIndex, 10))?.options

View File

@@ -8,7 +8,7 @@ const kc = getKubeConfig();
export default async function listIngress() { export default async function listIngress() {
const networking = kc.makeApiClient(NetworkingV1Api); const networking = kc.makeApiClient(NetworkingV1Api);
const { ingress } = getKubernetes(); const { ingress = true } = getKubernetes();
let ingressList = []; let ingressList = [];
if (ingress) { if (ingress) {

View File

@@ -0,0 +1,13 @@
let mockResponses = {};
export function __setEndpointMockData(endpoint, response) {
mockResponses[endpoint] = response;
}
export function __clearMocks() {
mockResponses = {};
}
export default function useWidgetAPI(widget, endpoint) {
return mockResponses[endpoint] ?? { data: undefined, error: undefined };
}

View File

@@ -1,25 +0,0 @@
import cache from "memory-cache";
const defaultDuration = 5;
export default async function cachedFetch(url, duration, ua) {
const cached = cache.get(url);
// eslint-disable-next-line no-param-reassign
duration = duration || defaultDuration;
if (cached) {
return cached;
}
// wrapping text in JSON.parse to handle utf-8 issues
const options = {};
if (ua) {
options.headers = {
"User-Agent": ua,
};
}
const data = await fetch(url, options).then((res) => res.json());
cache.put(url, data, duration * 1000 * 60);
return data;
}

View File

@@ -3,6 +3,7 @@
import { createUnzip, constants as zlibConstants } from "node:zlib"; import { createUnzip, constants as zlibConstants } from "node:zlib";
import { http, https } from "follow-redirects"; import { http, https } from "follow-redirects";
import cache from "memory-cache";
import { addCookieToJar, setCookieHeader } from "./cookie-jar"; import { addCookieToJar, setCookieHeader } from "./cookie-jar";
import { sanitizeErrorURL } from "./api-helpers"; import { sanitizeErrorURL } from "./api-helpers";
@@ -81,20 +82,46 @@ export function httpRequest(url, params) {
return handleRequest(http, url, params); return handleRequest(http, url, params);
} }
export async function cachedRequest(url, duration = 5, ua = "homepage") {
const cached = cache.get(url);
if (cached) {
return cached;
}
const options = {
headers: {
"User-Agent": ua,
Accept: "application/json",
},
};
let [, , data] = await httpProxy(url, options);
if (Buffer.isBuffer(data)) {
try {
data = JSON.parse(Buffer.from(data).toString());
} catch (e) {
logger.debug("Error parsing cachedRequest data for %s: %s %s", url, Buffer.from(data).toString(), e);
data = Buffer.from(data).toString();
}
}
cache.put(url, data, duration * 1000 * 60);
return data;
}
export async function httpProxy(url, params = {}) { export async function httpProxy(url, params = {}) {
const constructedUrl = new URL(url); const constructedUrl = new URL(url);
const disableIpv6 = process.env.HOMEPAGE_PROXY_DISABLE_IPV6 === "true";
const agentOptions = disableIpv6 ? { family: 4, autoSelectFamily: false } : {};
let request = null; let request = null;
if (constructedUrl.protocol === "https:") { if (constructedUrl.protocol === "https:") {
request = httpsRequest(constructedUrl, { request = httpsRequest(constructedUrl, {
agent: new https.Agent({ agent: new https.Agent({ ...agentOptions, rejectUnauthorized: false }),
rejectUnauthorized: false,
}),
...params, ...params,
}); });
} else { } else {
request = httpRequest(constructedUrl, { request = httpRequest(constructedUrl, {
agent: new http.Agent(), agent: new http.Agent(agentOptions),
...params, ...params,
}); });
} }

View File

@@ -113,6 +113,7 @@ const components = {
rutorrent: dynamic(() => import("./rutorrent/component")), rutorrent: dynamic(() => import("./rutorrent/component")),
sabnzbd: dynamic(() => import("./sabnzbd/component")), sabnzbd: dynamic(() => import("./sabnzbd/component")),
scrutiny: dynamic(() => import("./scrutiny/component")), scrutiny: dynamic(() => import("./scrutiny/component")),
slskd: dynamic(() => import("./slskd/component")),
sonarr: dynamic(() => import("./sonarr/component")), sonarr: dynamic(() => import("./sonarr/component")),
speedtest: dynamic(() => import("./speedtest/component")), speedtest: dynamic(() => import("./speedtest/component")),
spoolman: dynamic(() => import("./spoolman/component")), spoolman: dynamic(() => import("./spoolman/component")),

View File

@@ -1,8 +1,10 @@
import { useTranslation } from "next-i18next"; import { useTranslation } from "next-i18next";
import Container from "components/services/widget/container"; import Container from "components/services/widget/container";
import Block from "components/services/widget/block"; import Block from "components/services/widget/block";
import classNames from "classnames";
import useWidgetAPI from "utils/proxy/use-widget-api"; import useWidgetAPI from "utils/proxy/use-widget-api";
import * as shvl from "utils/config/shvl";
function getValue(field, data) { function getValue(field, data) {
let value = data; let value = data;
@@ -14,6 +16,11 @@ function getValue(field, data) {
return value; return value;
} }
// shvl is easier, everything else is kept for backwards compatibility.
if (typeof field === "string") {
return shvl.get(data, field, null) ?? data[field] ?? null;
}
while (typeof lastField === "object") { while (typeof lastField === "object") {
key = Object.keys(lastField)[0] ?? null; key = Object.keys(lastField)[0] ?? null;
@@ -165,6 +172,16 @@ export default function Component({ service }) {
if (!customData) { if (!customData) {
switch (display) { switch (display) {
case "dynamic-list":
return (
<Container service={service}>
<div className="flex flex-col w-full">
<div className="bg-theme-200/50 dark:bg-theme-900/20 rounded-sm m-1 flex-1 flex flex-row items-center justify-between p-1 text-xs animate-pulse">
<div className="font-thin pl-2">Loading...</div>
</div>
</div>
</Container>
);
case "list": case "list":
return ( return (
<Container service={service}> <Container service={service}>
@@ -196,6 +213,76 @@ export default function Component({ service }) {
} }
switch (display) { switch (display) {
case "dynamic-list":
let listItems = customData;
if (mappings.items) listItems = shvl.get(customData, mappings.items, null);
let error;
if (!listItems || !Array.isArray(listItems)) {
error = { message: "Unable to find items" };
}
const name = mappings.name;
const label = mappings.label;
if (!name || !label) {
error = { message: "Name and label properties are required" };
}
if (error) {
return <Container service={service} error={error}></Container>;
}
const target = mappings.target;
if (mappings.limit && parseInt(mappings.limit, 10) > 0) {
listItems.splice(mappings.limit);
}
return (
<Container service={service}>
<div className="flex flex-col w-full">
{listItems.length === 0 ? (
<div className="bg-theme-200/50 dark:bg-theme-900/20 rounded-sm m-1 flex-1 flex flex-row items-center justify-between p-1 text-xs">
<div className="font-thin pl-2">No items found</div>
</div>
) : (
listItems.map((item, index) => {
const itemName = shvl.get(item, name, item[name]) ?? "";
const itemLabel = shvl.get(item, label, item[label]) ?? "";
const itemUrl = target
? [...target.matchAll(/\{(.*?)\}/g)]
.map((match) => match[1])
.reduce((url, targetTemplate) => {
const value = shvl.get(item, targetTemplate, item[targetTemplate]) ?? "";
return url.replaceAll(`{${targetTemplate}}`, value);
}, target)
: null;
const className =
"bg-theme-200/50 dark:bg-theme-900/20 rounded-sm m-1 flex-1 flex flex-row items-center justify-between p-1 text-xs";
return itemUrl ? (
<a
key={`${itemName}-${index}`}
className={classNames(className, "hover:bg-theme-300/50 dark:hover:bg-theme-800/20")}
href={itemUrl}
target="_blank"
rel="noopener noreferrer"
>
<div className="font-thin pl-2">{itemName}</div>
<div className="flex flex-row text-right">
<div className="font-bold mr-2">{itemLabel}</div>
</div>
</a>
) : (
<div key={`${itemName}-${index}`} className={className}>
<div className="font-thin pl-2">{itemName}</div>
<div className="flex flex-row text-right">
<div className="font-bold mr-2">{itemLabel}</div>
</div>
</div>
);
})
)}
</div>
</Container>
);
case "list": case "list":
return ( return (
<Container service={service}> <Container service={service}>

View File

@@ -35,8 +35,8 @@ function generateStreamTitle(session, enableUser, showEpisodeNumber) {
let streamTitle = ""; let streamTitle = "";
if (Type === "Episode" && showEpisodeNumber) { if (Type === "Episode" && showEpisodeNumber) {
const seasonStr = `S${ParentIndexNumber.toString().padStart(2, "0")}`; const seasonStr = ParentIndexNumber ? `S${ParentIndexNumber.toString().padStart(2, "0")}` : "";
const episodeStr = `E${IndexNumber.toString().padStart(2, "0")}`; const episodeStr = IndexNumber ? `E${IndexNumber.toString().padStart(2, "0")}` : "";
streamTitle = `${SeriesName}: ${seasonStr} · ${episodeStr} - ${Name}`; streamTitle = `${SeriesName}: ${seasonStr} · ${episodeStr} - ${Name}`;
} else { } else {
streamTitle = `${Name}${SeriesName ? ` - ${SeriesName}` : ""}`; streamTitle = `${Name}${SeriesName ? ` - ${SeriesName}` : ""}`;

View File

@@ -8,17 +8,21 @@ export default function Component({ service }) {
const { data: giteaNotifications, error: giteaNotificationsError } = useWidgetAPI(widget, "notifications"); const { data: giteaNotifications, error: giteaNotificationsError } = useWidgetAPI(widget, "notifications");
const { data: giteaIssues, error: giteaIssuesError } = useWidgetAPI(widget, "issues"); const { data: giteaIssues, error: giteaIssuesError } = useWidgetAPI(widget, "issues");
const { data: giteaRepositories, error: giteaRepositoriesError } = useWidgetAPI(widget, "repositories");
if (giteaNotificationsError || giteaIssuesError) { if (giteaNotificationsError || giteaIssuesError || giteaRepositoriesError) {
return <Container service={service} error={giteaNotificationsError ?? giteaIssuesError} />; return (
<Container service={service} error={giteaNotificationsError ?? giteaIssuesError ?? giteaRepositoriesError} />
);
} }
if (!giteaNotifications || !giteaIssues) { if (!giteaNotifications || !giteaIssues || !giteaRepositories) {
return ( return (
<Container service={service}> <Container service={service}>
<Block label="gitea.notifications" /> <Block label="gitea.notifications" />
<Block label="gitea.issues" /> <Block label="gitea.issues" />
<Block label="gitea.pulls" /> <Block label="gitea.pulls" />
<Block label="gitea.repositories" />
</Container> </Container>
); );
} }
@@ -28,6 +32,7 @@ export default function Component({ service }) {
<Block label="gitea.notifications" value={giteaNotifications.length} /> <Block label="gitea.notifications" value={giteaNotifications.length} />
<Block label="gitea.issues" value={giteaIssues.issues.length} /> <Block label="gitea.issues" value={giteaIssues.issues.length} />
<Block label="gitea.pulls" value={giteaIssues.pulls.length} /> <Block label="gitea.pulls" value={giteaIssues.pulls.length} />
<Block label="gitea.repositories" value={giteaRepositories.data.length} />
</Container> </Container>
); );
} }

View File

@@ -16,6 +16,9 @@ const widget = {
issues: asJson(data).filter((issue) => !issue.pull_request), issues: asJson(data).filter((issue) => !issue.pull_request),
}), }),
}, },
repositories: {
endpoint: "repos/search",
},
}, },
}; };

View File

@@ -0,0 +1,60 @@
import { render, screen } from "@testing-library/react";
import Component from "widgets/gitlab/component";
import { SettingsContext } from "utils/contexts/settings";
import React from "react";
import { __setEndpointMockData, __clearMocks } from "utils/proxy/use-widget-api";
const mockSettings = {
hideErrors: false,
};
const service = {
name: "Gitlab",
widget: {
type: "gitlab",
key: "gitlabapikey",
url: "https://127.0.0.1",
},
};
describe("Gitlab Service Widget", () => {
beforeEach(() => {
__clearMocks();
});
it("renders while data is pending", () => {
render(
<SettingsContext.Provider value={{ settings: mockSettings }}>
<Component service={service} />
</SettingsContext.Provider>,
);
expect(screen.getByText("Groups")).toBeInTheDocument();
expect(screen.getByText("Issues")).toBeInTheDocument();
expect(screen.getByText("Merge Requests")).toBeInTheDocument();
expect(screen.getByText("Projects")).toBeInTheDocument();
});
it("renders with sample data", () => {
__setEndpointMockData("counts", {
data: {
groups_count: 3,
issues_count: 7,
merge_requests_count: 2,
projects_count: 5,
},
});
render(
<SettingsContext.Provider value={{ settings: {} }}>
<Component service={service} />
</SettingsContext.Provider>,
);
expect(screen.getByText("3")).toBeInTheDocument();
expect(screen.getByText("7")).toBeInTheDocument();
expect(screen.getByText("2")).toBeInTheDocument();
expect(screen.getByText("5")).toBeInTheDocument();
});
});

View File

@@ -1,5 +1,4 @@
import credentialedProxyHandler from "utils/proxy/handlers/credentialed"; import credentialedProxyHandler from "utils/proxy/handlers/credentialed";
import { asJson } from "utils/proxy/api-helpers";
const widget = { const widget = {
api: `{url}/api/v1/{endpoint}`, api: `{url}/api/v1/{endpoint}`,

View File

@@ -14,7 +14,11 @@ async function login(widget, service) {
const endpoint = "Account/login"; const endpoint = "Account/login";
const api = widgets?.[widget.type]?.api; const api = widgets?.[widget.type]?.api;
const loginUrl = new URL(formatApiCall(api, { endpoint, ...widget })); const loginUrl = new URL(formatApiCall(api, { endpoint, ...widget }));
const loginBody = {}; const loginBody = {
username: "",
password: "",
apiKey: "",
};
if (widget.username && widget.password) { if (widget.username && widget.password) {
loginBody.username = widget.username; loginBody.username = widget.username;
loginBody.password = widget.password; loginBody.password = widget.password;

View File

@@ -1,7 +1,7 @@
import genericProxyHandler from "utils/proxy/handlers/generic"; import genericProxyHandler from "utils/proxy/handlers/generic";
const widget = { const widget = {
api: "{url}/api/v1/{key}/{endpoint}/", api: "{url}/api/v1/{key}/{endpoint}",
proxyHandler: genericProxyHandler, proxyHandler: genericProxyHandler,
mappings: { mappings: {

View File

@@ -18,7 +18,7 @@ export default async function minecraftProxyHandler(req, res) {
res.status(200).send({ res.status(200).send({
version: pingResponse.status.version.name, version: pingResponse.status.version.name,
online: true, online: true,
players: pingResponse.status.players.online, players: pingResponse.status.players,
}); });
} catch (e) { } catch (e) {
if (e) logger.error(e); if (e) logger.error(e);

View File

@@ -36,14 +36,16 @@ export default function Component({ service }) {
const printStatsInfo = printStats.result.status.print_stats.info ?? {}; const printStatsInfo = printStats.result.status.print_stats.info ?? {};
const { current_layer: currentLayer = "-", total_layer: totalLayer = "-" } = printStatsInfo; const { current_layer: currentLayer = "-", total_layer: totalLayer = "-" } = printStatsInfo;
const layers = printStats.result.status.print_stats.state === "standby" ? "- / -" : `${currentLayer} / ${totalLayer}`;
const progress =
printStats.result.status.print_stats.state === "standby"
? "-"
: t("common.percent", { value: displayStatus.result.status.display_status.progress * 100 });
return ( return (
<Container service={service}> <Container service={service}>
<Block label="moonraker.layers" value={`${currentLayer} / ${totalLayer}`} /> <Block label="moonraker.layers" value={layers} />
<Block <Block label="moonraker.print_progress" value={progress} />
label="moonraker.print_progress"
value={t("common.percent", { value: displayStatus.result.status.display_status.progress * 100 })}
/>
<Block label="moonraker.print_status" value={printStats.result.status.print_stats.state} /> <Block label="moonraker.print_status" value={printStats.result.status.print_stats.state} />
</Container> </Container>
); );

View File

@@ -0,0 +1,55 @@
import { useTranslation } from "next-i18next";
import Container from "components/services/widget/container";
import Block from "components/services/widget/block";
import useWidgetAPI from "utils/proxy/use-widget-api";
const slskdDefaultFields = ["slskStatus", "downloads", "uploads", "sharedFiles"];
const MAX_ALLOWED_FIELDS = 4;
export default function Component({ service }) {
const { t } = useTranslation();
const { widget } = service;
const { data: appData, error: appError } = useWidgetAPI(widget, "application");
const { data: downData, error: downError } = useWidgetAPI(widget, "downloads");
const { data: upData, error: upError } = useWidgetAPI(widget, "uploads");
if (appError || downError || upError) {
return <Container service={service} error={appError ?? downError ?? upError} />;
}
if (!widget.fields || widget.fields.length === 0) {
widget.fields = slskdDefaultFields;
} else if (widget.fields?.length > MAX_ALLOWED_FIELDS) {
widget.fields = widget.fields.slice(0, MAX_ALLOWED_FIELDS);
}
if (!appData || !downData || !upData) {
return (
<Container service={service}>
<Block label="slskd.slskStatus" />
<Block label="slskd.updateStatus" />
<Block label="slskd.downloads" />
<Block label="slskd.uploads" />
<Block label="slskd.sharedFiles" />
</Container>
);
}
return (
<Container service={service}>
<Block
label="slskd.slskStatus"
value={appData.server?.isConnected ? t("slskd.connected") : t("slskd.disconnected")}
/>
<Block
label="slskd.updateStatus"
value={appData.version?.isUpdateAvailable ? t("slskd.update_yes") : t("slskd.update_no")}
/>
<Block label="slskd.downloads" value={t("common.number", { value: downData.length ?? 0 })} />
<Block label="slskd.uploads" value={t("common.number", { value: upData.length ?? 0 })} />
<Block label="slskd.sharedFiles" value={t("common.number", { value: appData.shares?.files ?? 0 })} />
</Container>
);
}

View File

@@ -0,0 +1,21 @@
import credentialedProxyHandler from "utils/proxy/handlers/credentialed";
import { asJson } from "utils/proxy/api-helpers";
const widget = {
api: `{url}/api/v0/{endpoint}`,
proxyHandler: credentialedProxyHandler,
mappings: {
application: {
endpoint: "application",
},
downloads: {
endpoint: "transfers/downloads",
},
uploads: {
endpoint: "transfers/uploads",
},
},
};
export default widget;

Some files were not shown because too many files have changed in this diff Show More