mirror of
https://github.com/gethomepage/homepage.git
synced 2026-09-12 07:05:56 -07:00
Compare commits
64 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 541b2fe9f1 | |||
| 574ba2c7cb | |||
| f78cab7557 | |||
| 2cd77eae1f | |||
| 6ab83b0b5f | |||
| 9aa5a79c41 | |||
| 1c2cb76702 | |||
| 7de1faa769 | |||
| 1024a382ee | |||
| 438788240b | |||
| df34927198 | |||
| ddc5adc91c | |||
| 22f1317ad3 | |||
| 7c36d09217 | |||
| 378f2d5d6d | |||
| d907ca1d0e | |||
| 81accd8dbe | |||
| 070e1b9cca | |||
| 6324c5ee63 | |||
| 9b475f48d8 | |||
| 35b72b65de | |||
| f6dca0c5ad | |||
| e6ad43a76c | |||
| c46b3e6cff | |||
| e913243149 | |||
| 99e16c6da0 | |||
| c0b6eb3501 | |||
| a17763ca9f | |||
| bd39fb48b9 | |||
| f2dc80e39e | |||
| 7d48b7ea78 | |||
| 4c361422ca | |||
| 7c26cf6d97 | |||
| 5873f8e7d5 | |||
| 6099837373 | |||
| f0bd255adb | |||
| 21bf7ed8e2 | |||
| 728c6fbe18 | |||
| 6a96289717 | |||
| fa47a2d86c | |||
| cc22d04429 | |||
| a414f376b9 | |||
| bdd77342d8 | |||
| 63d2172ef7 | |||
| 85e4e439bc | |||
| 36aa241689 | |||
| 0933ba015a | |||
| c57117daae | |||
| 93763b7c10 | |||
| 6c551ace34 | |||
| 7886742d56 | |||
| a253861061 | |||
| edfb28ab5d | |||
| 3ab6b040b2 | |||
| 9cabb46bb7 | |||
| d7cc2d3f19 | |||
| b5084ba8b3 | |||
| dca0c9ab81 | |||
| d4fd14b724 | |||
| 600c9eee78 | |||
| 198c86fb49 | |||
| f711d290a7 | |||
| 342afa2215 | |||
| 27704f962e |
@@ -10,6 +10,7 @@ version-resolver:
|
|||||||
- 'breaking-change'
|
- 'breaking-change'
|
||||||
minor:
|
minor:
|
||||||
labels:
|
labels:
|
||||||
|
- 'minor'
|
||||||
- 'enhancement'
|
- 'enhancement'
|
||||||
- 'feature'
|
- 'feature'
|
||||||
patch:
|
patch:
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ on:
|
|||||||
]
|
]
|
||||||
branches: [ dev ]
|
branches: [ dev ]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
synchronize-with-crowdin:
|
synchronize-with-crowdin:
|
||||||
name: Crowdin Sync
|
name: Crowdin Sync
|
||||||
@@ -19,7 +23,7 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
- name: crowdin action
|
- name: crowdin action
|
||||||
uses: crowdin/github-action@c7af9bc98b01694653031fef2a0dc6c7888ce9bc # v2
|
uses: crowdin/github-action@e4a6c1338b4063c77d46a81875265f9e8bd76f95 # v3.0.0
|
||||||
with:
|
with:
|
||||||
upload_translations: false
|
upload_translations: false
|
||||||
download_translations: true
|
download_translations: true
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ on:
|
|||||||
env:
|
env:
|
||||||
IMAGE_NAME: ${{ github.repository }}
|
IMAGE_NAME: ${{ github.repository }}
|
||||||
|
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
||||||
|
|
||||||
@@ -27,7 +29,6 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
packages: write
|
packages: write
|
||||||
id-token: write
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
@@ -101,7 +102,7 @@ jobs:
|
|||||||
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
|
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
|
||||||
|
|
||||||
- name: Setup Docker buildx
|
- name: Setup Docker buildx
|
||||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4
|
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4
|
||||||
|
|
||||||
- name: Build and push Docker image
|
- name: Build and push Docker image
|
||||||
id: build-and-push
|
id: build-and-push
|
||||||
|
|||||||
@@ -10,8 +10,6 @@ on:
|
|||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
pages: write
|
|
||||||
id-token: write
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
||||||
@@ -25,7 +23,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
python-version-file: ".python-version"
|
python-version-file: ".python-version"
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
||||||
- run: sudo apt-get install pngquant
|
- run: sudo apt-get install pngquant
|
||||||
- name: Test Docs Build
|
- name: Test Docs Build
|
||||||
run: uv run --frozen zensical build --clean
|
run: uv run --frozen zensical build --clean
|
||||||
@@ -33,6 +31,10 @@ jobs:
|
|||||||
name: Build & Deploy Docs
|
name: Build & Deploy Docs
|
||||||
if: github.repository == 'gethomepage/homepage' && github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
|
if: github.repository == 'gethomepage/homepage' && github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pages: write
|
||||||
|
id-token: write
|
||||||
environment:
|
environment:
|
||||||
name: github-pages
|
name: github-pages
|
||||||
url: ${{ steps.deployment.outputs.page_url }}
|
url: ${{ steps.deployment.outputs.page_url }}
|
||||||
@@ -43,7 +45,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
python-version-file: ".python-version"
|
python-version-file: ".python-version"
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
||||||
- run: sudo apt-get install pngquant
|
- run: sudo apt-get install pngquant
|
||||||
- name: Build Docs
|
- name: Build Docs
|
||||||
run: uv run --frozen zensical build --clean
|
run: uv run --frozen zensical build --clean
|
||||||
|
|||||||
@@ -6,15 +6,15 @@ on:
|
|||||||
discussion:
|
discussion:
|
||||||
types: [labeled]
|
types: [labeled]
|
||||||
|
|
||||||
permissions:
|
permissions: {}
|
||||||
discussions: write
|
|
||||||
issues: write
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
close-needs-discussion:
|
close-needs-discussion:
|
||||||
name: Issues Need Discussion
|
name: Issues Need Discussion
|
||||||
if: github.event_name == 'issues' && github.event.label.name == 'needs-discussion'
|
if: github.event_name == 'issues' && github.event.label.name == 'needs-discussion'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||||
with:
|
with:
|
||||||
@@ -49,6 +49,8 @@ jobs:
|
|||||||
name: Discussions Need Information
|
name: Discussions Need Information
|
||||||
if: github.event_name == 'discussion' && github.event.label.name == 'needs-information'
|
if: github.event_name == 'discussion' && github.event.label.name == 'needs-information'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
discussions: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
merge_group:
|
merge_group:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
name: Linting Checks
|
name: Linting Checks
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ name: PR Quality
|
|||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
issues: read
|
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ on:
|
|||||||
types: [labeled]
|
types: [labeled]
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
issues: write
|
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|||||||
@@ -5,10 +5,7 @@ on:
|
|||||||
- cron: '0 3 * * *'
|
- cron: '0 3 * * *'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions: {}
|
||||||
issues: write
|
|
||||||
pull-requests: write
|
|
||||||
discussions: write
|
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: lock
|
group: lock
|
||||||
@@ -17,6 +14,9 @@ jobs:
|
|||||||
stale:
|
stale:
|
||||||
name: 'Stale'
|
name: 'Stale'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@1e223db275d687790206a7acac4d1a11bd6fe629 # v10
|
- uses: actions/stale@1e223db275d687790206a7acac4d1a11bd6fe629 # v10
|
||||||
with:
|
with:
|
||||||
@@ -31,6 +31,10 @@ jobs:
|
|||||||
lock-threads:
|
lock-threads:
|
||||||
name: 'Lock Old Threads'
|
name: 'Lock Old Threads'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
discussions: write
|
||||||
|
issues: write
|
||||||
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- uses: dessant/lock-threads@89ae32b08ed1a541efecbab17912962a5e38981c # v6.0.2
|
- uses: dessant/lock-threads@89ae32b08ed1a541efecbab17912962a5e38981c # v6.0.2
|
||||||
with:
|
with:
|
||||||
@@ -56,6 +60,8 @@ jobs:
|
|||||||
close-answered-discussions:
|
close-answered-discussions:
|
||||||
name: 'Close Answered Discussions'
|
name: 'Close Answered Discussions'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
discussions: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||||
with:
|
with:
|
||||||
@@ -112,6 +118,8 @@ jobs:
|
|||||||
close-outdated-discussions:
|
close-outdated-discussions:
|
||||||
name: 'Close Outdated Discussions'
|
name: 'Close Outdated Discussions'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
discussions: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||||
with:
|
with:
|
||||||
@@ -203,6 +211,8 @@ jobs:
|
|||||||
close-unsupported-feature-requests:
|
close-unsupported-feature-requests:
|
||||||
name: 'Close Unsupported Feature Requests'
|
name: 'Close Unsupported Feature Requests'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
discussions: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ on:
|
|||||||
push:
|
push:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
vitest:
|
vitest:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ As of version v0.6.30 homepage supports adding your own custom css & javascript.
|
|||||||
|
|
||||||
To add custom css simply edit the `custom.css` file under your config directory, similarly for javascript you would edit `custom.js`. You can then target elements in homepage with various classes / ids to customize things to your liking.
|
To add custom css simply edit the `custom.css` file under your config directory, similarly for javascript you would edit `custom.js`. You can then target elements in homepage with various classes / ids to customize things to your liking.
|
||||||
|
|
||||||
|
!!! warning
|
||||||
|
|
||||||
|
When Homepage authentication is enabled, `custom.css` remains publicly accessible so it can style the sign-in page. Do not include secrets or sensitive internal URLs in this file. `custom.js` remains protected and is only loaded after authentication.
|
||||||
|
|
||||||
You can also set a specific `id` for a service or bookmark to target with your custom css or javascript, e.g.
|
You can also set a specific `id` for a service or bookmark to target with your custom css or javascript, e.g.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
|
|||||||
@@ -49,6 +49,23 @@ environment:
|
|||||||
HOMEPAGE_MCP_TOKEN: "generate-with-openssl-rand-base64-32"
|
HOMEPAGE_MCP_TOKEN: "generate-with-openssl-rand-base64-32"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Connecting Claude Desktop
|
||||||
|
|
||||||
|
Claude Desktop can use [`mcp-remote`](https://www.npmjs.com/package/mcp-remote) to communicate with the homepage MCP endpoint in `claude_desktop_config.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"homepage": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["-y", "mcp-remote", "http://localhost:3000/api/mcp", "--header", "X-Homepage-MCP-Token: your-token"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The **Add custom connector** option will not work: remote connectors authenticate with OAuth and cannot supply `HOMEPAGE_MCP_TOKEN`.
|
||||||
|
|
||||||
## Read-only by default
|
## Read-only by default
|
||||||
|
|
||||||
The MCP endpoint exposes tools for reading and validating supported Homepage config files. File writes are disabled unless you opt in with:
|
The MCP endpoint exposes tools for reading and validating supported Homepage config files. File writes are disabled unless you opt in with:
|
||||||
|
|||||||
@@ -69,11 +69,16 @@ For OIDC login (overrides password login):
|
|||||||
- `HOMEPAGE_OIDC_CLIENT_ID`
|
- `HOMEPAGE_OIDC_CLIENT_ID`
|
||||||
- `HOMEPAGE_OIDC_CLIENT_SECRET`
|
- `HOMEPAGE_OIDC_CLIENT_SECRET`
|
||||||
- Optional: `HOMEPAGE_OIDC_NAME` (display name), `HOMEPAGE_OIDC_SCOPE` (defaults to `openid email profile`)
|
- Optional: `HOMEPAGE_OIDC_NAME` (display name), `HOMEPAGE_OIDC_SCOPE` (defaults to `openid email profile`)
|
||||||
|
- Optional: `HOMEPAGE_OIDC_AUTO_LOGIN=true` to skip the login page and send unauthenticated visitors straight to the provider
|
||||||
|
|
||||||
|
!!! tip
|
||||||
|
|
||||||
|
With auto-login enabled, visit `/auth/signin?autologin=0` to reach the login page without being redirected, e.g. in case something goes wrong!
|
||||||
|
|
||||||
!!! warning
|
!!! warning
|
||||||
|
|
||||||
Homepage grants access to any identity that the configured OIDC provider authorizes for this client. Configure client assignments, groups, or access policies at the identity provider. Homepage does not apply additional claim-based authorization.
|
Homepage grants access to any identity that the configured OIDC provider authorizes for this client. Configure client assignments, groups, or access policies at the identity provider. Homepage does not apply additional claim-based authorization.
|
||||||
|
|
||||||
All app pages and `/api` routes except `/api/healthcheck` will require a signed-in session. Static assets remain public.
|
All app pages and `/api` routes except `/api/healthcheck` and `/api/config/custom.css` will require a signed-in session. Static assets remain public.
|
||||||
|
|
||||||
Configure your OIDC provider with the a callback URI like `https://homepage.example.com/api/auth/callback/homepage-oidc`.
|
Configure your OIDC provider with the a callback URI like `https://homepage.example.com/api/auth/callback/homepage-oidc`.
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ To ensure cohesiveness of various widgets, the following should be used as a gui
|
|||||||
- Note that we reserve the right to decline widgets for projects that are very young (eg < ~1y) or those with a small reach (eg low GitHub stars). Again, this is in an effort to keep overall widget maintenance under control.
|
- Note that we reserve the right to decline widgets for projects that are very young (eg < ~1y) or those with a small reach (eg low GitHub stars). Again, this is in an effort to keep overall widget maintenance under control.
|
||||||
- Widgets should be only one row of blocks
|
- Widgets should be only one row of blocks
|
||||||
- Widgets should be no more than 4 blocks wide and generally conform to the styling / design choices of other widgets
|
- Widgets should be no more than 4 blocks wide and generally conform to the styling / design choices of other widgets
|
||||||
|
- Use `withWidgetFields(service, defaultFields)` from `utils/widget-fields` to apply default fields and cap configured fields without mutating the service. The helper limits widgets to 4 fields.
|
||||||
- Minimize the number of API calls
|
- Minimize the number of API calls
|
||||||
- Avoid the use of custom proxy unless absolutely necessary
|
- Avoid the use of custom proxy unless absolutely necessary
|
||||||
- Widgets should be 'read-only', as in they should not make write changes using the relevant tool's API. Homepage widgets are designed to surface information, not to be a (usually worse) replacement for the tool itself.
|
- Widgets should be 'read-only', as in they should not make write changes using the relevant tool's API. Homepage widgets are designed to surface information, not to be a (usually worse) replacement for the tool itself.
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ description: Authentik Widget Configuration
|
|||||||
|
|
||||||
Learn more about [Authentik](https://github.com/goauthentik/authentik).
|
Learn more about [Authentik](https://github.com/goauthentik/authentik).
|
||||||
|
|
||||||
This widget reads the number of active users in the system, as well as logins for the last 24 hours.
|
This widget reads the number of active users in the system, as well as logins for the last 24 hours, and authorizations when using widget version 2.
|
||||||
|
|
||||||
You will need to generate an API token for an existing user under `Admin Portal` > `Directory` > `Tokens & App passwords`.
|
You will need to generate an API token for an existing user under `Admin Portal` > `Directory` > `Tokens & App passwords`.
|
||||||
Make sure to set Intent to "API Token".
|
Make sure to set Intent to "API Token".
|
||||||
@@ -15,7 +15,7 @@ The account you made the API token for also needs the following **Assigned globa
|
|||||||
- authentik Core -> Can view User (Model: User)
|
- authentik Core -> Can view User (Model: User)
|
||||||
- authentik Events -> Can view Event (Model: Event)
|
- authentik Events -> Can view Event (Model: Event)
|
||||||
|
|
||||||
Allowed fields: `["users", "loginsLast24H", "failedLoginsLast24H"]`.
|
Allowed fields: `["users", "loginsLast24H", "failedLoginsLast24H", "authorizationsLast24H"]` (`authorizationsLast24H` works with v2 only).
|
||||||
|
|
||||||
| Authentik Version | Homepage Widget Version |
|
| Authentik Version | Homepage Widget Version |
|
||||||
| ----------------- | ----------------------- |
|
| ----------------- | ----------------------- |
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ See the [crowdsec docs](https://docs.crowdsec.net/docs/local_api/intro/#machines
|
|||||||
in most instances you can use the default credentials (`/etc/crowdsec/local_api_credentials.yaml`).
|
in most instances you can use the default credentials (`/etc/crowdsec/local_api_credentials.yaml`).
|
||||||
|
|
||||||
!!! note
|
!!! note
|
||||||
Without the `limit24h` option, the widget will fetch all alerts which is limited to 100 by the API to avoid performance issues.
|
|
||||||
|
`alerts` counts alerts raised by your own engine in the last 24 hours. Alerts originating from the
|
||||||
|
CrowdSec community blocklist are excluded.
|
||||||
|
|
||||||
Allowed fields: `["alerts", "bans"]`.
|
Allowed fields: `["alerts", "bans"]`.
|
||||||
|
|
||||||
@@ -19,5 +21,4 @@ widget:
|
|||||||
url: http://crowdsechostorip:port
|
url: http://crowdsechostorip:port
|
||||||
username: localhost # machine_id in crowdsec
|
username: localhost # machine_id in crowdsec
|
||||||
password: password
|
password: password
|
||||||
limit24h: true # optional, limits alerts to last 24h. Default: false
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ Allowed fields: `["movies", "series", "episodes", "songs", "albums"]`.
|
|||||||
|
|
||||||
| Jellyfin Version | Homepage Widget Version |
|
| Jellyfin Version | Homepage Widget Version |
|
||||||
| ---------------- | ----------------------- |
|
| ---------------- | ----------------------- |
|
||||||
| < 10.12 | 1 (default) |
|
| < 12 (10.12) | 1 (default) |
|
||||||
| >= 10.12 | 2 |
|
| >= 12 (10.12) | 2 |
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
widget:
|
widget:
|
||||||
|
|||||||
@@ -15,5 +15,5 @@ widget:
|
|||||||
url: https://proxmoxbackupserver.host:port
|
url: https://proxmoxbackupserver.host:port
|
||||||
username: api_token_id
|
username: api_token_id
|
||||||
password: api_token_secret
|
password: api_token_secret
|
||||||
datastore: datastore_name #optional; if ommitted, will display a combination of all datastores used / total
|
datastore: datastore_name #optional; if omitted, will display a combination of all datastores used / total
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -12,5 +12,6 @@ widget:
|
|||||||
type: pulse
|
type: pulse
|
||||||
url: http://pulse.host.or.ip:7655
|
url: http://pulse.host.or.ip:7655
|
||||||
key: your-api-token # `monitoring:read` scope is required
|
key: your-api-token # `monitoring:read` scope is required
|
||||||
|
version: 2 # required for Pulse v6, defaults to 1
|
||||||
fields: ["nodes", "vms", "lxcs"] # optional
|
fields: ["nodes", "vms", "lxcs"] # optional
|
||||||
```
|
```
|
||||||
|
|||||||
+5
-20
@@ -1,27 +1,12 @@
|
|||||||
import path from "node:path";
|
import nextCoreWebVitals from "eslint-config-next/core-web-vitals";
|
||||||
import { fileURLToPath } from "node:url";
|
import prettierConfig from "eslint-config-prettier/flat";
|
||||||
|
|
||||||
import { fixupConfigRules } from "@eslint/compat";
|
|
||||||
import { FlatCompat } from "@eslint/eslintrc";
|
|
||||||
import js from "@eslint/js";
|
|
||||||
import prettier from "eslint-plugin-prettier";
|
|
||||||
import { defineConfig, globalIgnores } from "eslint/config";
|
import { defineConfig, globalIgnores } from "eslint/config";
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
|
||||||
const __dirname = path.dirname(__filename);
|
|
||||||
const compat = new FlatCompat({
|
|
||||||
baseDirectory: __dirname,
|
|
||||||
recommendedConfig: js.configs.recommended,
|
|
||||||
allConfig: js.configs.all,
|
|
||||||
});
|
|
||||||
|
|
||||||
export default defineConfig([
|
export default defineConfig([
|
||||||
|
...nextCoreWebVitals,
|
||||||
|
prettierConfig,
|
||||||
{
|
{
|
||||||
extends: fixupConfigRules(compat.extends("next/core-web-vitals", "prettier", "plugin:react-hooks/recommended")),
|
files: ["**/*.{js,mjs,cjs,jsx}"],
|
||||||
|
|
||||||
plugins: {
|
|
||||||
prettier,
|
|
||||||
},
|
|
||||||
|
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
ecmaVersion: 6,
|
ecmaVersion: 6,
|
||||||
|
|||||||
+9
-17
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "homepage",
|
"name": "homepage",
|
||||||
"version": "2.0.0",
|
"version": "2.3.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"preinstall": "npx only-allow pnpm",
|
"preinstall": "npx only-allow pnpm",
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@headlessui/react": "^2.2.10",
|
"@headlessui/react": "^2.2.10",
|
||||||
"@kubernetes/client-node": "^1.4.0",
|
"@kubernetes/client-node": "^2.0.0",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
"compare-versions": "^6.1.1",
|
"compare-versions": "^6.1.1",
|
||||||
"dockerode": "^5.0.0",
|
"dockerode": "^5.0.0",
|
||||||
@@ -23,12 +23,12 @@
|
|||||||
"gamedig": "^5.3.3",
|
"gamedig": "^5.3.3",
|
||||||
"i18next": "^26.3.6",
|
"i18next": "^26.3.6",
|
||||||
"ical.js": "^2.2.1",
|
"ical.js": "^2.2.1",
|
||||||
"js-yaml": "^4.3.1",
|
"js-yaml": "^5.4.0",
|
||||||
"json-rpc-2.0": "^1.7.1",
|
"json-rpc-2.0": "^1.7.1",
|
||||||
"luxon": "^3.7.2",
|
"luxon": "^3.7.2",
|
||||||
"memory-cache": "^0.2.0",
|
"memory-cache": "^0.2.0",
|
||||||
"minecraftstatuspinger": "^1.2.2",
|
"minecraftstatuspinger": "^1.2.2",
|
||||||
"next": "^16.2.12",
|
"next": "^16.3.3",
|
||||||
"next-auth": "^4.24.15",
|
"next-auth": "^4.24.15",
|
||||||
"next-i18next": "^16.0.8",
|
"next-i18next": "^16.0.8",
|
||||||
"ping": "^1.0.0",
|
"ping": "^1.0.0",
|
||||||
@@ -36,10 +36,10 @@
|
|||||||
"raw-body": "^4.0.0",
|
"raw-body": "^4.0.0",
|
||||||
"react": "^19.2.8",
|
"react": "^19.2.8",
|
||||||
"react-dom": "^19.2.8",
|
"react-dom": "^19.2.8",
|
||||||
"react-i18next": "^17.0.11",
|
"react-i18next": "^17.0.12",
|
||||||
"react-icons": "^5.6.0",
|
"react-icons": "^5.6.0",
|
||||||
"recharts": "^3.1.2",
|
"recharts": "^3.1.2",
|
||||||
"swr": "^2.4.2",
|
"swr": "^2.5.1",
|
||||||
"systeminformation": "^5.33.1",
|
"systeminformation": "^5.33.1",
|
||||||
"tough-cookie": "^6.0.2",
|
"tough-cookie": "^6.0.2",
|
||||||
"urbackup-server-api": "^0.92.2",
|
"urbackup-server-api": "^0.92.2",
|
||||||
@@ -48,29 +48,21 @@
|
|||||||
"xml-js": "^1.6.11"
|
"xml-js": "^1.6.11"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/compat": "^2.1.0",
|
|
||||||
"@eslint/eslintrc": "^3.3.3",
|
|
||||||
"@eslint/js": "^9.39.4",
|
|
||||||
"@tailwindcss/forms": "^0.5.11",
|
"@tailwindcss/forms": "^0.5.11",
|
||||||
"@tailwindcss/postcss": "^4.3.3",
|
"@tailwindcss/postcss": "^4.3.3",
|
||||||
"@testing-library/jest-dom": "^7.0.0",
|
"@testing-library/jest-dom": "^7.0.1",
|
||||||
"@testing-library/react": "^16.3.0",
|
"@testing-library/react": "^16.3.0",
|
||||||
"@vitest/coverage-v8": "^4.1.10",
|
"@vitest/coverage-v8": "^4.1.10",
|
||||||
"eslint": "^9.25.1",
|
"eslint": "^9.25.1",
|
||||||
"eslint-config-next": "^15.5.11",
|
"eslint-config-next": "^16.3.3",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"eslint-plugin-import": "^2.32.0",
|
|
||||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
|
||||||
"eslint-plugin-prettier": "^5.5.6",
|
|
||||||
"eslint-plugin-react": "^7.37.4",
|
|
||||||
"eslint-plugin-react-hooks": "^5.2.0",
|
|
||||||
"jsdom": "^30.0.1",
|
"jsdom": "^30.0.1",
|
||||||
"postcss": "^8.5.26",
|
"postcss": "^8.5.26",
|
||||||
"prettier": "^3.8.4",
|
"prettier": "^3.8.4",
|
||||||
"prettier-plugin-organize-imports": "^4.3.0",
|
"prettier-plugin-organize-imports": "^4.3.0",
|
||||||
"tailwind-scrollbar": "^4.0.2",
|
"tailwind-scrollbar": "^4.0.2",
|
||||||
"tailwindcss": "^4.3.3",
|
"tailwindcss": "^4.3.3",
|
||||||
"typescript": "^5.7.3",
|
"typescript": "^6.0.3",
|
||||||
"vitest": "^4.1.0"
|
"vitest": "^4.1.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
|
|||||||
Generated
+593
-463
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10"><circle cx="5" cy="5" r="4"/></svg>
|
||||||
|
After Width: | Height: | Size: 96 B |
@@ -21,7 +21,7 @@
|
|||||||
"seconds": "s"
|
"seconds": "s"
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"signout": "Sign out"
|
"signout": "Teken uit"
|
||||||
},
|
},
|
||||||
"widget": {
|
"widget": {
|
||||||
"missing_type": "Ontbrekende legstuk-tipe: {{type}}",
|
"missing_type": "Ontbrekende legstuk-tipe: {{type}}",
|
||||||
@@ -39,7 +39,9 @@
|
|||||||
"wait": "Wag asseblief"
|
"wait": "Wag asseblief"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"placeholder": "Soek…"
|
"placeholder": "Soek…",
|
||||||
|
"provider": "Soekenjin: {{name}}",
|
||||||
|
"select_provider": "Kies soekenjin"
|
||||||
},
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
"cpu": "SVE",
|
"cpu": "SVE",
|
||||||
@@ -97,7 +99,12 @@
|
|||||||
"unhealthy": "Ongesond",
|
"unhealthy": "Ongesond",
|
||||||
"not_found": "Nie Gevind Nie",
|
"not_found": "Nie Gevind Nie",
|
||||||
"exited": "Verlaat",
|
"exited": "Verlaat",
|
||||||
"partial": "Gedeeltelik"
|
"partial": "Gedeeltelik",
|
||||||
|
"created": "Created",
|
||||||
|
"paused": "Paused",
|
||||||
|
"restarting": "Restarting",
|
||||||
|
"removing": "Removing",
|
||||||
|
"dead": "Dead"
|
||||||
},
|
},
|
||||||
"ping": {
|
"ping": {
|
||||||
"error": "Fout",
|
"error": "Fout",
|
||||||
@@ -471,7 +478,8 @@
|
|||||||
"authentik": {
|
"authentik": {
|
||||||
"users": "Gebruikers",
|
"users": "Gebruikers",
|
||||||
"loginsLast24H": "Aantekenings (24h)",
|
"loginsLast24H": "Aantekenings (24h)",
|
||||||
"failedLoginsLast24H": "Mislukte Aantekenings (24h)"
|
"failedLoginsLast24H": "Mislukte Aantekenings (24h)",
|
||||||
|
"authorizationsLast24H": "Magtigings (24h)"
|
||||||
},
|
},
|
||||||
"proxmox": {
|
"proxmox": {
|
||||||
"mem": "GEH",
|
"mem": "GEH",
|
||||||
|
|||||||
@@ -39,7 +39,9 @@
|
|||||||
"wait": "الرجاء الإنتظار"
|
"wait": "الرجاء الإنتظار"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"placeholder": "بحث …"
|
"placeholder": "بحث …",
|
||||||
|
"provider": "Search engine: {{name}}",
|
||||||
|
"select_provider": "Select search engine"
|
||||||
},
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
"cpu": "المعالج",
|
"cpu": "المعالج",
|
||||||
@@ -97,7 +99,12 @@
|
|||||||
"unhealthy": "غير سليم",
|
"unhealthy": "غير سليم",
|
||||||
"not_found": "غير موجود",
|
"not_found": "غير موجود",
|
||||||
"exited": "خرجت",
|
"exited": "خرجت",
|
||||||
"partial": "جزئي"
|
"partial": "جزئي",
|
||||||
|
"created": "Created",
|
||||||
|
"paused": "Paused",
|
||||||
|
"restarting": "Restarting",
|
||||||
|
"removing": "Removing",
|
||||||
|
"dead": "Dead"
|
||||||
},
|
},
|
||||||
"ping": {
|
"ping": {
|
||||||
"error": "خطأ",
|
"error": "خطأ",
|
||||||
@@ -471,7 +478,8 @@
|
|||||||
"authentik": {
|
"authentik": {
|
||||||
"users": "المستخدمون",
|
"users": "المستخدمون",
|
||||||
"loginsLast24H": "تسجيلات الدخول (٢٤س)",
|
"loginsLast24H": "تسجيلات الدخول (٢٤س)",
|
||||||
"failedLoginsLast24H": "فشل تسجيلات الدخول (٢٤س)"
|
"failedLoginsLast24H": "فشل تسجيلات الدخول (٢٤س)",
|
||||||
|
"authorizationsLast24H": "Auths (24h)"
|
||||||
},
|
},
|
||||||
"proxmox": {
|
"proxmox": {
|
||||||
"mem": "الذاكرة",
|
"mem": "الذاكرة",
|
||||||
|
|||||||
@@ -39,7 +39,9 @@
|
|||||||
"wait": "Моля изчакайте"
|
"wait": "Моля изчакайте"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"placeholder": "Търсене…"
|
"placeholder": "Търсене…",
|
||||||
|
"provider": "Search engine: {{name}}",
|
||||||
|
"select_provider": "Select search engine"
|
||||||
},
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
"cpu": "Процесор",
|
"cpu": "Процесор",
|
||||||
@@ -97,7 +99,12 @@
|
|||||||
"unhealthy": "Нездравословен",
|
"unhealthy": "Нездравословен",
|
||||||
"not_found": "Не е открит",
|
"not_found": "Не е открит",
|
||||||
"exited": "Напуснал",
|
"exited": "Напуснал",
|
||||||
"partial": "Частично"
|
"partial": "Частично",
|
||||||
|
"created": "Created",
|
||||||
|
"paused": "Paused",
|
||||||
|
"restarting": "Restarting",
|
||||||
|
"removing": "Removing",
|
||||||
|
"dead": "Dead"
|
||||||
},
|
},
|
||||||
"ping": {
|
"ping": {
|
||||||
"error": "Грешка",
|
"error": "Грешка",
|
||||||
@@ -471,7 +478,8 @@
|
|||||||
"authentik": {
|
"authentik": {
|
||||||
"users": "Потребители",
|
"users": "Потребители",
|
||||||
"loginsLast24H": "Logins (24h)",
|
"loginsLast24H": "Logins (24h)",
|
||||||
"failedLoginsLast24H": "Failed Logins (24h)"
|
"failedLoginsLast24H": "Failed Logins (24h)",
|
||||||
|
"authorizationsLast24H": "Auths (24h)"
|
||||||
},
|
},
|
||||||
"proxmox": {
|
"proxmox": {
|
||||||
"mem": "MEM",
|
"mem": "MEM",
|
||||||
|
|||||||
+261
-253
@@ -21,7 +21,7 @@
|
|||||||
"seconds": "s"
|
"seconds": "s"
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"signout": "Sign out"
|
"signout": "Tanca la sessió"
|
||||||
},
|
},
|
||||||
"widget": {
|
"widget": {
|
||||||
"missing_type": "Falta el tipus de widget: {{type}}",
|
"missing_type": "Falta el tipus de widget: {{type}}",
|
||||||
@@ -39,7 +39,9 @@
|
|||||||
"wait": "Si us plau, espereu"
|
"wait": "Si us plau, espereu"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"placeholder": "Cerca…"
|
"placeholder": "Cerca…",
|
||||||
|
"provider": "Motor de cerca: {{name}}",
|
||||||
|
"select_provider": "Escull motor de cerca"
|
||||||
},
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
"cpu": "CPU",
|
"cpu": "CPU",
|
||||||
@@ -70,17 +72,17 @@
|
|||||||
"empty_data": "Estat del subsistema desconegut"
|
"empty_data": "Estat del subsistema desconegut"
|
||||||
},
|
},
|
||||||
"unifi_drive": {
|
"unifi_drive": {
|
||||||
"healthy": "Healthy",
|
"healthy": "Saludable",
|
||||||
"degraded": "Degraded",
|
"degraded": "Degradat",
|
||||||
"no_data": "No storage data available"
|
"no_data": "Sense dades d'emmagatzematge disponible"
|
||||||
},
|
},
|
||||||
"duplicati": {
|
"duplicati": {
|
||||||
"jobs": "Jobs",
|
"jobs": "Tasques",
|
||||||
"stored": "Stored",
|
"stored": "Desat",
|
||||||
"lastBackup": "Last Run",
|
"lastBackup": "Darrera execució",
|
||||||
"nextRun": "Next Run",
|
"nextRun": "Propera execució",
|
||||||
"running": "Running",
|
"running": "En execució",
|
||||||
"warnings": "Warnings",
|
"warnings": "Avisos",
|
||||||
"errors": "Errors"
|
"errors": "Errors"
|
||||||
},
|
},
|
||||||
"docker": {
|
"docker": {
|
||||||
@@ -97,7 +99,12 @@
|
|||||||
"unhealthy": "No saludable",
|
"unhealthy": "No saludable",
|
||||||
"not_found": "No trobat",
|
"not_found": "No trobat",
|
||||||
"exited": "Tancat",
|
"exited": "Tancat",
|
||||||
"partial": "Parcial"
|
"partial": "Parcial",
|
||||||
|
"created": "Creat",
|
||||||
|
"paused": "Pausat",
|
||||||
|
"restarting": "Reiniciant",
|
||||||
|
"removing": "Eliminant",
|
||||||
|
"dead": "Mort"
|
||||||
},
|
},
|
||||||
"ping": {
|
"ping": {
|
||||||
"error": "Error",
|
"error": "Error",
|
||||||
@@ -133,7 +140,7 @@
|
|||||||
"series": "Sèries",
|
"series": "Sèries",
|
||||||
"episodes": "Episodis",
|
"episodes": "Episodis",
|
||||||
"songs": "Cançons",
|
"songs": "Cançons",
|
||||||
"albums": "Albums"
|
"albums": "Àlbums"
|
||||||
},
|
},
|
||||||
"esphome": {
|
"esphome": {
|
||||||
"offline": "Desconnectat",
|
"offline": "Desconnectat",
|
||||||
@@ -244,10 +251,10 @@
|
|||||||
"seed": "Sembrat"
|
"seed": "Sembrat"
|
||||||
},
|
},
|
||||||
"qbittorrent": {
|
"qbittorrent": {
|
||||||
"download": "Download",
|
"download": "Baixada",
|
||||||
"upload": "Upload",
|
"upload": "Pujada",
|
||||||
"leech": "Leech",
|
"leech": "Sangonera",
|
||||||
"seed": "Seed"
|
"seed": "Sembrat"
|
||||||
},
|
},
|
||||||
"qnap": {
|
"qnap": {
|
||||||
"cpuUsage": "Ús de CPU",
|
"cpuUsage": "Ús de CPU",
|
||||||
@@ -258,44 +265,44 @@
|
|||||||
"invalid": "No vàlid"
|
"invalid": "No vàlid"
|
||||||
},
|
},
|
||||||
"deluge": {
|
"deluge": {
|
||||||
"download": "Download",
|
"download": "Baixada",
|
||||||
"upload": "Upload",
|
"upload": "Pujada",
|
||||||
"leech": "Leech",
|
"leech": "Sangonera",
|
||||||
"seed": "Seed"
|
"seed": "Sembrat"
|
||||||
},
|
},
|
||||||
"develancacheui": {
|
"develancacheui": {
|
||||||
"cachehitbytes": "Bytes trobats a la memòria cau",
|
"cachehitbytes": "Bytes trobats a la memòria cau",
|
||||||
"cachemissbytes": "Bytes no trobats a la memòria cau"
|
"cachemissbytes": "Bytes no trobats a la memòria cau"
|
||||||
},
|
},
|
||||||
"downloadstation": {
|
"downloadstation": {
|
||||||
"download": "Download",
|
"download": "Baixada",
|
||||||
"upload": "Upload",
|
"upload": "Pujada",
|
||||||
"leech": "Leech",
|
"leech": "Sangonera",
|
||||||
"seed": "Seed"
|
"seed": "Sembrat"
|
||||||
},
|
},
|
||||||
"sonarr": {
|
"sonarr": {
|
||||||
"wanted": "Volgut",
|
"wanted": "Volgut",
|
||||||
"queued": "En cua",
|
"queued": "En cua",
|
||||||
"series": "Series",
|
"series": "Sèries",
|
||||||
"queue": "Queue",
|
"queue": "Cua",
|
||||||
"unknown": "Unknown"
|
"unknown": "Desconegut"
|
||||||
},
|
},
|
||||||
"radarr": {
|
"radarr": {
|
||||||
"wanted": "Wanted",
|
"wanted": "Volguts",
|
||||||
"missing": "Falten",
|
"missing": "Falten",
|
||||||
"queued": "Queued",
|
"queued": "Encuat",
|
||||||
"movies": "Movies",
|
"movies": "Pel·lícules",
|
||||||
"queue": "Queue",
|
"queue": "Cua",
|
||||||
"unknown": "Unknown"
|
"unknown": "Desconegut"
|
||||||
},
|
},
|
||||||
"lidarr": {
|
"lidarr": {
|
||||||
"wanted": "Wanted",
|
"wanted": "Volguts",
|
||||||
"queued": "Queued",
|
"queued": "Encuat",
|
||||||
"artists": "Artistes"
|
"artists": "Artistes"
|
||||||
},
|
},
|
||||||
"readarr": {
|
"readarr": {
|
||||||
"wanted": "Wanted",
|
"wanted": "Volguts",
|
||||||
"queued": "Queued",
|
"queued": "Encuat",
|
||||||
"books": "Llibres"
|
"books": "Llibres"
|
||||||
},
|
},
|
||||||
"bazarr": {
|
"bazarr": {
|
||||||
@@ -308,16 +315,16 @@
|
|||||||
"available": "Disponible"
|
"available": "Disponible"
|
||||||
},
|
},
|
||||||
"seerr": {
|
"seerr": {
|
||||||
"pending": "Pending",
|
"pending": "Pendent",
|
||||||
"approved": "Approved",
|
"approved": "Aprovat",
|
||||||
"available": "Available",
|
"available": "Disponible",
|
||||||
"completed": "Completed",
|
"completed": "Completat",
|
||||||
"processing": "Processing",
|
"processing": "Processant",
|
||||||
"issues": "Open Issues"
|
"issues": "Temes oberts"
|
||||||
},
|
},
|
||||||
"netalertx": {
|
"netalertx": {
|
||||||
"total": "Total",
|
"total": "Total",
|
||||||
"connected": "Connected",
|
"connected": "Connectat",
|
||||||
"new_devices": "Nous dispositius",
|
"new_devices": "Nous dispositius",
|
||||||
"down_alerts": "Alertes de caigudes"
|
"down_alerts": "Alertes de caigudes"
|
||||||
},
|
},
|
||||||
@@ -328,18 +335,18 @@
|
|||||||
"gravity": "Gravetat"
|
"gravity": "Gravetat"
|
||||||
},
|
},
|
||||||
"adguard": {
|
"adguard": {
|
||||||
"queries": "Queries",
|
"queries": "Consultes",
|
||||||
"blocked": "Blocked",
|
"blocked": "Bloquejat",
|
||||||
"filtered": "Filtrat",
|
"filtered": "Filtrat",
|
||||||
"latency": "Latència"
|
"latency": "Latència"
|
||||||
},
|
},
|
||||||
"speedtest": {
|
"speedtest": {
|
||||||
"upload": "Upload",
|
"upload": "Pujada",
|
||||||
"download": "Download",
|
"download": "Baixada",
|
||||||
"ping": "Ping"
|
"ping": "Latència"
|
||||||
},
|
},
|
||||||
"portainer": {
|
"portainer": {
|
||||||
"running": "Running",
|
"running": "En execució",
|
||||||
"stopped": "Aturat",
|
"stopped": "Aturat",
|
||||||
"total": "Total"
|
"total": "Total"
|
||||||
},
|
},
|
||||||
@@ -357,16 +364,16 @@
|
|||||||
"address": "Adreça",
|
"address": "Adreça",
|
||||||
"expires": "Caduca",
|
"expires": "Caduca",
|
||||||
"never": "Mai",
|
"never": "Mai",
|
||||||
"user": "User",
|
"user": "Usuari",
|
||||||
"hostname": "Hostname",
|
"hostname": "Nom d'hoste",
|
||||||
"name": "Name",
|
"name": "Nom",
|
||||||
"client_version": "Client Version",
|
"client_version": "Versió de client",
|
||||||
"os": "OS",
|
"os": "OS",
|
||||||
"created": "Created",
|
"created": "Creat",
|
||||||
"authorized": "Authorized",
|
"authorized": "Autoritzat",
|
||||||
"is_external": "Is External",
|
"is_external": "És extern",
|
||||||
"update_available": "Update Available",
|
"update_available": "Actualització disponible",
|
||||||
"tags": "Tags",
|
"tags": "Etiquetes",
|
||||||
"last_seen": "Vist per darrer cop",
|
"last_seen": "Vist per darrer cop",
|
||||||
"now": "Ara",
|
"now": "Ara",
|
||||||
"years": "{{number}}a",
|
"years": "{{number}}a",
|
||||||
@@ -376,7 +383,7 @@
|
|||||||
"minutes": "{{number}}m",
|
"minutes": "{{number}}m",
|
||||||
"seconds": "{{number}}s",
|
"seconds": "{{number}}s",
|
||||||
"ago": "Fa {{value}}",
|
"ago": "Fa {{value}}",
|
||||||
"true": "Yes",
|
"true": "Sí",
|
||||||
"false": "No"
|
"false": "No"
|
||||||
},
|
},
|
||||||
"technitium": {
|
"technitium": {
|
||||||
@@ -471,7 +478,8 @@
|
|||||||
"authentik": {
|
"authentik": {
|
||||||
"users": "Usuaris",
|
"users": "Usuaris",
|
||||||
"loginsLast24H": "Inicis de sessió (24h)",
|
"loginsLast24H": "Inicis de sessió (24h)",
|
||||||
"failedLoginsLast24H": "Errors d'inici de sessió (24h)"
|
"failedLoginsLast24H": "Errors d'inici de sessió (24h)",
|
||||||
|
"authorizationsLast24H": "Autentificacions (últ. 24h)"
|
||||||
},
|
},
|
||||||
"proxmox": {
|
"proxmox": {
|
||||||
"mem": "MEM",
|
"mem": "MEM",
|
||||||
@@ -539,37 +547,37 @@
|
|||||||
"61-day": "Pluja lleugera",
|
"61-day": "Pluja lleugera",
|
||||||
"61-night": "Pluja lleugera",
|
"61-night": "Pluja lleugera",
|
||||||
"63-day": "Pluja",
|
"63-day": "Pluja",
|
||||||
"63-night": "Rain",
|
"63-night": "Pluja",
|
||||||
"65-day": "Pluja intensa",
|
"65-day": "Pluja intensa",
|
||||||
"65-night": "Heavy Rain",
|
"65-night": "Pluja forta",
|
||||||
"66-day": "Pluja gelada",
|
"66-day": "Pluja gelada",
|
||||||
"66-night": "Freezing Rain",
|
"66-night": "Pluja gelada",
|
||||||
"67-day": "Freezing Rain",
|
"67-day": "Pluja gelada",
|
||||||
"67-night": "Freezing Rain",
|
"67-night": "Pluja gelada",
|
||||||
"71-day": "Neu lleugera",
|
"71-day": "Neu lleugera",
|
||||||
"71-night": "Light Snow",
|
"71-night": "Nevada lleugera",
|
||||||
"73-day": "Neu",
|
"73-day": "Neu",
|
||||||
"73-night": "Snow",
|
"73-night": "Nevada",
|
||||||
"75-day": "Neu intensa",
|
"75-day": "Neu intensa",
|
||||||
"75-night": "Heavy Snow",
|
"75-night": "Nevada forta",
|
||||||
"77-day": "Neu lleugera",
|
"77-day": "Neu lleugera",
|
||||||
"77-night": "Snow Grains",
|
"77-night": "Flocs de neu",
|
||||||
"80-day": "Plovisqueig",
|
"80-day": "Plovisqueig",
|
||||||
"80-night": "Light Showers",
|
"80-night": "Plugims suaus",
|
||||||
"81-day": "Xàfecs",
|
"81-day": "Xàfecs",
|
||||||
"81-night": "Showers",
|
"81-night": "Plugims",
|
||||||
"82-day": "Xàfecs intensos",
|
"82-day": "Xàfecs intensos",
|
||||||
"82-night": "Heavy Showers",
|
"82-night": "Plugims forts",
|
||||||
"85-day": "Xàfecs de neu",
|
"85-day": "Xàfecs de neu",
|
||||||
"85-night": "Snow Showers",
|
"85-night": "Plugims amb neu",
|
||||||
"86-day": "Snow Showers",
|
"86-day": "Plugims amb neu",
|
||||||
"86-night": "Snow Showers",
|
"86-night": "Plugims amb neu",
|
||||||
"95-day": "Tempesta",
|
"95-day": "Tempesta",
|
||||||
"95-night": "Thunderstorm",
|
"95-night": "Tormenta",
|
||||||
"96-day": "Tempesta amb calamarsa",
|
"96-day": "Tempesta amb calamarsa",
|
||||||
"96-night": "Thunderstorm With Hail",
|
"96-night": "Tormenta amb calamarsa",
|
||||||
"99-day": "Thunderstorm With Hail",
|
"99-day": "Tormenta amb calamarsa",
|
||||||
"99-night": "Thunderstorm With Hail"
|
"99-night": "Tormenta amb calamarsa"
|
||||||
},
|
},
|
||||||
"homebridge": {
|
"homebridge": {
|
||||||
"available_update": "Sistema",
|
"available_update": "Sistema",
|
||||||
@@ -578,18 +586,18 @@
|
|||||||
"up_to_date": "Actualitzat",
|
"up_to_date": "Actualitzat",
|
||||||
"child_bridges": "Ponts fills",
|
"child_bridges": "Ponts fills",
|
||||||
"child_bridges_status": "{{ok}}/{{total}}",
|
"child_bridges_status": "{{ok}}/{{total}}",
|
||||||
"up": "Up",
|
"up": "En línia",
|
||||||
"pending": "Pending",
|
"pending": "Pendent",
|
||||||
"down": "Down",
|
"down": "Inactiu",
|
||||||
"ok": "Ok"
|
"ok": "Ok"
|
||||||
},
|
},
|
||||||
"healthchecks": {
|
"healthchecks": {
|
||||||
"new": "Nou",
|
"new": "Nou",
|
||||||
"up": "Up",
|
"up": "Actiu",
|
||||||
"grace": "En Període de gràcia",
|
"grace": "En Període de gràcia",
|
||||||
"down": "Down",
|
"down": "Inactiu",
|
||||||
"paused": "En pausa",
|
"paused": "En pausa",
|
||||||
"status": "Status",
|
"status": "Estat",
|
||||||
"last_ping": "Últim ping",
|
"last_ping": "Últim ping",
|
||||||
"never": "Sense pings"
|
"never": "Sense pings"
|
||||||
},
|
},
|
||||||
@@ -599,74 +607,74 @@
|
|||||||
"containers_failed": "Error"
|
"containers_failed": "Error"
|
||||||
},
|
},
|
||||||
"autobrr": {
|
"autobrr": {
|
||||||
"approvedPushes": "Approved",
|
"approvedPushes": "Aprovat",
|
||||||
"rejectedPushes": "Rebutjat",
|
"rejectedPushes": "Rebutjat",
|
||||||
"filters": "Filtres",
|
"filters": "Filtres",
|
||||||
"indexers": "Indexers"
|
"indexers": "Indexadors"
|
||||||
},
|
},
|
||||||
"tubearchivist": {
|
"tubearchivist": {
|
||||||
"downloads": "Queue",
|
"downloads": "Cua",
|
||||||
"videos": "Vídeos",
|
"videos": "Vídeos",
|
||||||
"channels": "Canals",
|
"channels": "Canals",
|
||||||
"playlists": "Llistes de reproducció"
|
"playlists": "Llistes de reproducció"
|
||||||
},
|
},
|
||||||
"truenas": {
|
"truenas": {
|
||||||
"load": "Càrrega del sistema",
|
"load": "Càrrega del sistema",
|
||||||
"uptime": "Uptime",
|
"uptime": "Temps actiu",
|
||||||
"alerts": "Alerts"
|
"alerts": "Alertes"
|
||||||
},
|
},
|
||||||
"pyload": {
|
"pyload": {
|
||||||
"speed": "Velocitat",
|
"speed": "Velocitat",
|
||||||
"active": "Active",
|
"active": "Actiu",
|
||||||
"queue": "Queue",
|
"queue": "Cua",
|
||||||
"total": "Total"
|
"total": "Total"
|
||||||
},
|
},
|
||||||
"gluetun": {
|
"gluetun": {
|
||||||
"public_ip": "IP Pública",
|
"public_ip": "IP Pública",
|
||||||
"region": "Regió",
|
"region": "Regió",
|
||||||
"country": "País",
|
"country": "País",
|
||||||
"port_forwarded": "Port Forwarded"
|
"port_forwarded": "Ports redirigits"
|
||||||
},
|
},
|
||||||
"hdhomerun": {
|
"hdhomerun": {
|
||||||
"channels": "Channels",
|
"channels": "Canals",
|
||||||
"hd": "HD",
|
"hd": "HD",
|
||||||
"tunerCount": "Sintonitzadors",
|
"tunerCount": "Sintonitzadors",
|
||||||
"channelNumber": "Canal",
|
"channelNumber": "Canal",
|
||||||
"channelNetwork": "Xarxa",
|
"channelNetwork": "Xarxa",
|
||||||
"signalStrength": "Intensitat",
|
"signalStrength": "Intensitat",
|
||||||
"signalQuality": "Qualitat",
|
"signalQuality": "Qualitat",
|
||||||
"symbolQuality": "Quality",
|
"symbolQuality": "Qualitat",
|
||||||
"networkRate": "Bitrate",
|
"networkRate": "Taxa de bits",
|
||||||
"clientIP": "Client"
|
"clientIP": "Client"
|
||||||
},
|
},
|
||||||
"scrutiny": {
|
"scrutiny": {
|
||||||
"passed": "Aprovat",
|
"passed": "Aprovat",
|
||||||
"failed": "Failed",
|
"failed": "Error",
|
||||||
"unknown": "Unknown"
|
"unknown": "Desconegut"
|
||||||
},
|
},
|
||||||
"paperlessngx": {
|
"paperlessngx": {
|
||||||
"inbox": "Safata d'entrada",
|
"inbox": "Safata d'entrada",
|
||||||
"total": "Total"
|
"total": "Total"
|
||||||
},
|
},
|
||||||
"pangolin": {
|
"pangolin": {
|
||||||
"orgs": "Orgs",
|
"orgs": "Organitzacions",
|
||||||
"sites": "Sites",
|
"sites": "Llocs",
|
||||||
"resources": "Resources",
|
"resources": "Recursos",
|
||||||
"targets": "Targets",
|
"targets": "Objectius",
|
||||||
"traffic": "Traffic",
|
"traffic": "Tràfic",
|
||||||
"in": "In",
|
"in": "Dins",
|
||||||
"out": "Out"
|
"out": "Fora"
|
||||||
},
|
},
|
||||||
"peanut": {
|
"peanut": {
|
||||||
"battery_charge": "Càrrega de la bateria",
|
"battery_charge": "Càrrega de la bateria",
|
||||||
"ups_load": "Càrrega del SAI",
|
"ups_load": "Càrrega del SAI",
|
||||||
"ups_status": "Estat del SAI",
|
"ups_status": "Estat del SAI",
|
||||||
"online": "Online",
|
"online": "En línia",
|
||||||
"on_battery": "En Bateria",
|
"on_battery": "En Bateria",
|
||||||
"low_battery": "Bateria Baixa"
|
"low_battery": "Bateria Baixa"
|
||||||
},
|
},
|
||||||
"nextdns": {
|
"nextdns": {
|
||||||
"wait": "Please Wait",
|
"wait": "Espereu si us plau",
|
||||||
"no_devices": "No s'han rebut dades del Dispositiu"
|
"no_devices": "No s'han rebut dades del Dispositiu"
|
||||||
},
|
},
|
||||||
"mikrotik": {
|
"mikrotik": {
|
||||||
@@ -854,37 +862,37 @@
|
|||||||
"jdownloader": {
|
"jdownloader": {
|
||||||
"downloadCount": "Cua",
|
"downloadCount": "Cua",
|
||||||
"downloadBytesRemaining": "Restant",
|
"downloadBytesRemaining": "Restant",
|
||||||
"downloadTotalBytes": "Size",
|
"downloadTotalBytes": "Mida",
|
||||||
"downloadSpeed": "Speed"
|
"downloadSpeed": "Velocitat"
|
||||||
},
|
},
|
||||||
"kavita": {
|
"kavita": {
|
||||||
"seriesCount": "Series",
|
"seriesCount": "Sèries",
|
||||||
"totalFiles": "Files"
|
"totalFiles": "Fitxers"
|
||||||
},
|
},
|
||||||
"azuredevops": {
|
"azuredevops": {
|
||||||
"result": "Resultat",
|
"result": "Resultat",
|
||||||
"status": "Status",
|
"status": "Estat",
|
||||||
"buildId": "Id de compilació",
|
"buildId": "Id de compilació",
|
||||||
"succeeded": "Amb èxit",
|
"succeeded": "Amb èxit",
|
||||||
"notStarted": "No Iniciat",
|
"notStarted": "No Iniciat",
|
||||||
"failed": "Failed",
|
"failed": "Error",
|
||||||
"canceled": "Cancel·lat",
|
"canceled": "Cancel·lat",
|
||||||
"inProgress": "En curs",
|
"inProgress": "En curs",
|
||||||
"totalPrs": "PRs Totals",
|
"totalPrs": "PRs Totals",
|
||||||
"myPrs": "Les meves PRs",
|
"myPrs": "Les meves PRs",
|
||||||
"approved": "Approved"
|
"approved": "Aprovat"
|
||||||
},
|
},
|
||||||
"gamedig": {
|
"gamedig": {
|
||||||
"status": "Status",
|
"status": "Estat",
|
||||||
"online": "Online",
|
"online": "En línia",
|
||||||
"offline": "Offline",
|
"offline": "Desconnectat",
|
||||||
"name": "Nom",
|
"name": "Nom",
|
||||||
"map": "Mapa",
|
"map": "Mapa",
|
||||||
"currentPlayers": "Jugadors actuals",
|
"currentPlayers": "Jugadors actuals",
|
||||||
"players": "Players",
|
"players": "Jugadors",
|
||||||
"maxPlayers": "Màxim de jugadors",
|
"maxPlayers": "Màxim de jugadors",
|
||||||
"bots": "Bots",
|
"bots": "Bots",
|
||||||
"ping": "Ping"
|
"ping": "Latència"
|
||||||
},
|
},
|
||||||
"urbackup": {
|
"urbackup": {
|
||||||
"ok": "Ok",
|
"ok": "Ok",
|
||||||
@@ -894,39 +902,39 @@
|
|||||||
},
|
},
|
||||||
"mealie": {
|
"mealie": {
|
||||||
"recipes": "Receptes",
|
"recipes": "Receptes",
|
||||||
"users": "Users",
|
"users": "Usuaris",
|
||||||
"categories": "Categories",
|
"categories": "Categories",
|
||||||
"tags": "Etiquetes"
|
"tags": "Etiquetes"
|
||||||
},
|
},
|
||||||
"openmediavault": {
|
"openmediavault": {
|
||||||
"downloading": "Descarregant",
|
"downloading": "Descarregant",
|
||||||
"total": "Total",
|
"total": "Total",
|
||||||
"running": "Running",
|
"running": "En execució",
|
||||||
"stopped": "Stopped",
|
"stopped": "Aturat",
|
||||||
"passed": "Passed",
|
"passed": "Aprovat",
|
||||||
"failed": "Failed"
|
"failed": "Error"
|
||||||
},
|
},
|
||||||
"openwrt": {
|
"openwrt": {
|
||||||
"uptime": "Uptime",
|
"uptime": "Temps actiu",
|
||||||
"cpuLoad": "Càrrega mitjana de CPU (5min)",
|
"cpuLoad": "Càrrega mitjana de CPU (5min)",
|
||||||
"up": "Up",
|
"up": "En línia",
|
||||||
"down": "Down",
|
"down": "Desconnectat",
|
||||||
"bytesTx": "Enviat",
|
"bytesTx": "Enviat",
|
||||||
"bytesRx": "Received"
|
"bytesRx": "Rebut"
|
||||||
},
|
},
|
||||||
"uptimerobot": {
|
"uptimerobot": {
|
||||||
"status": "Status",
|
"status": "Estat",
|
||||||
"uptime": "Uptime",
|
"uptime": "Temps actiu",
|
||||||
"lastDown": "Darrera Inactivitat",
|
"lastDown": "Darrera Inactivitat",
|
||||||
"downDuration": "Duració d'Inactivitat",
|
"downDuration": "Duració d'Inactivitat",
|
||||||
"sitesUp": "Sites Up",
|
"sitesUp": "Pàgines actives",
|
||||||
"sitesDown": "Sites Down",
|
"sitesDown": "Pàgines inactives",
|
||||||
"paused": "Paused",
|
"paused": "Pausat",
|
||||||
"notyetchecked": "Sense verificar",
|
"notyetchecked": "Sense verificar",
|
||||||
"up": "Up",
|
"up": "En línia",
|
||||||
"seemsdown": "Sembla caigut",
|
"seemsdown": "Sembla caigut",
|
||||||
"down": "Down",
|
"down": "Desconnectat",
|
||||||
"unknown": "Unknown"
|
"unknown": "Desconegut"
|
||||||
},
|
},
|
||||||
"calendar": {
|
"calendar": {
|
||||||
"inCinemas": "En cines",
|
"inCinemas": "En cines",
|
||||||
@@ -934,7 +942,7 @@
|
|||||||
"digitalRelease": "Estrena digital",
|
"digitalRelease": "Estrena digital",
|
||||||
"noEventsToday": "Cap esdeveniment per avui!",
|
"noEventsToday": "Cap esdeveniment per avui!",
|
||||||
"noEventsFound": "No s'han trobat esdeveniments",
|
"noEventsFound": "No s'han trobat esdeveniments",
|
||||||
"errorWhenLoadingData": "Error when loading calendar data"
|
"errorWhenLoadingData": "Error carregant dades del calendari"
|
||||||
},
|
},
|
||||||
"romm": {
|
"romm": {
|
||||||
"platforms": "Plataformes",
|
"platforms": "Plataformes",
|
||||||
@@ -945,50 +953,50 @@
|
|||||||
"totalfilesize": "Tamany total"
|
"totalfilesize": "Tamany total"
|
||||||
},
|
},
|
||||||
"mailcow": {
|
"mailcow": {
|
||||||
"domains": "Domains",
|
"domains": "Dominis",
|
||||||
"mailboxes": "Bústies",
|
"mailboxes": "Bústies",
|
||||||
"mails": "Correus",
|
"mails": "Correus",
|
||||||
"storage": "Storage"
|
"storage": "Emmagatzematge"
|
||||||
},
|
},
|
||||||
"maintainerr": {
|
"maintainerr": {
|
||||||
"itemsHandled": "Items Handled",
|
"itemsHandled": "Items gestionats",
|
||||||
"episodesHandled": "Episodes Handled",
|
"episodesHandled": "Episodis gestionats",
|
||||||
"moviesHandled": "Movies Handled",
|
"moviesHandled": "Pel·lícules gestionades",
|
||||||
"showsHandled": "Shows Handled",
|
"showsHandled": "Sèries gestionades",
|
||||||
"seasonsHandled": "Seasons Handled",
|
"seasonsHandled": "Temporades gestionades",
|
||||||
"reclaimable": "Reclaimable",
|
"reclaimable": "Reclamable",
|
||||||
"movieReclaimable": "Movie Reclaimable",
|
"movieReclaimable": "Pel·lícula reclamable",
|
||||||
"showReclaimable": "Show Reclaimable",
|
"showReclaimable": "Sèrie reclamable",
|
||||||
"totalCapacity": "Total Capacity"
|
"totalCapacity": "Capacitat total"
|
||||||
},
|
},
|
||||||
"netdata": {
|
"netdata": {
|
||||||
"warnings": "Avisos",
|
"warnings": "Avisos",
|
||||||
"criticals": "Crítics"
|
"criticals": "Crítics"
|
||||||
},
|
},
|
||||||
"ntfy": {
|
"ntfy": {
|
||||||
"title": "Title",
|
"title": "Títol",
|
||||||
"priority": "Priority",
|
"priority": "Prioritat",
|
||||||
"lastReceived": "Last Received",
|
"lastReceived": "Rebut per últim cop",
|
||||||
"message": "Message",
|
"message": "Missatge",
|
||||||
"tags": "Tags",
|
"tags": "Etiquetes",
|
||||||
"none": "None",
|
"none": "Cap",
|
||||||
"min": "Min",
|
"min": "Min",
|
||||||
"low": "Low",
|
"low": "Baix",
|
||||||
"default": "Default",
|
"default": "Per defecte",
|
||||||
"high": "High",
|
"high": "Alt",
|
||||||
"urgent": "Urgent"
|
"urgent": "Urgent"
|
||||||
},
|
},
|
||||||
"plantit": {
|
"plantit": {
|
||||||
"events": "Esdeveniments",
|
"events": "Esdeveniments",
|
||||||
"plants": "Plantes",
|
"plants": "Plantes",
|
||||||
"photos": "Photos",
|
"photos": "Fotos",
|
||||||
"species": "Espècies"
|
"species": "Espècies"
|
||||||
},
|
},
|
||||||
"gitea": {
|
"gitea": {
|
||||||
"notifications": "Notificacions",
|
"notifications": "Notificacions",
|
||||||
"issues": "Issues",
|
"issues": "Problemes",
|
||||||
"pulls": "Sol·licitud de Canvis",
|
"pulls": "Sol·licitud de Canvis",
|
||||||
"repositories": "Repositories"
|
"repositories": "Repositoris"
|
||||||
},
|
},
|
||||||
"stash": {
|
"stash": {
|
||||||
"scenes": "Escenes",
|
"scenes": "Escenes",
|
||||||
@@ -1002,13 +1010,13 @@
|
|||||||
"galleries": "Biblioteques",
|
"galleries": "Biblioteques",
|
||||||
"performers": "Intèrprets",
|
"performers": "Intèrprets",
|
||||||
"studios": "Estudis",
|
"studios": "Estudis",
|
||||||
"movies": "Movies",
|
"movies": "Pel·lícules",
|
||||||
"tags": "Tags",
|
"tags": "Etiquetes",
|
||||||
"oCount": "O Count"
|
"oCount": "O Count"
|
||||||
},
|
},
|
||||||
"tandoor": {
|
"tandoor": {
|
||||||
"users": "Users",
|
"users": "Usuaris",
|
||||||
"recipes": "Recipes",
|
"recipes": "Receptes",
|
||||||
"keywords": "Paraules claus"
|
"keywords": "Paraules claus"
|
||||||
},
|
},
|
||||||
"homebox": {
|
"homebox": {
|
||||||
@@ -1016,17 +1024,17 @@
|
|||||||
"totalWithWarranty": "Amb Garantia",
|
"totalWithWarranty": "Amb Garantia",
|
||||||
"locations": "Ubicacions",
|
"locations": "Ubicacions",
|
||||||
"labels": "Etiquetes",
|
"labels": "Etiquetes",
|
||||||
"users": "Users",
|
"users": "Usuaris",
|
||||||
"totalValue": "Valor total"
|
"totalValue": "Valor total"
|
||||||
},
|
},
|
||||||
"crowdsec": {
|
"crowdsec": {
|
||||||
"alerts": "Alerts",
|
"alerts": "Alertes",
|
||||||
"bans": "Prohibicions"
|
"bans": "Prohibicions"
|
||||||
},
|
},
|
||||||
"wgeasy": {
|
"wgeasy": {
|
||||||
"connected": "Connected",
|
"connected": "Connectat",
|
||||||
"enabled": "Enabled",
|
"enabled": "Habilitat",
|
||||||
"disabled": "Disabled",
|
"disabled": "Deshabilitat",
|
||||||
"total": "Total"
|
"total": "Total"
|
||||||
},
|
},
|
||||||
"swagdashboard": {
|
"swagdashboard": {
|
||||||
@@ -1036,9 +1044,9 @@
|
|||||||
"banned": "Bloquejat"
|
"banned": "Bloquejat"
|
||||||
},
|
},
|
||||||
"myspeed": {
|
"myspeed": {
|
||||||
"ping": "Ping",
|
"ping": "Latència",
|
||||||
"download": "Download",
|
"download": "Baixada",
|
||||||
"upload": "Upload"
|
"upload": "Pujada"
|
||||||
},
|
},
|
||||||
"stocks": {
|
"stocks": {
|
||||||
"stocks": "Accions",
|
"stocks": "Accions",
|
||||||
@@ -1135,120 +1143,120 @@
|
|||||||
"tags": "Etiquetes"
|
"tags": "Etiquetes"
|
||||||
},
|
},
|
||||||
"slskd": {
|
"slskd": {
|
||||||
"slskStatus": "Network",
|
"slskStatus": "Xarxa",
|
||||||
"connected": "Connected",
|
"connected": "Connectat",
|
||||||
"disconnected": "Disconnected",
|
"disconnected": "Desconnectat",
|
||||||
"updateStatus": "Update",
|
"updateStatus": "Actualitza",
|
||||||
"update_yes": "Available",
|
"update_yes": "Disponible",
|
||||||
"update_no": "Up to Date",
|
"update_no": "Actualitzat",
|
||||||
"downloads": "Downloads",
|
"downloads": "Descàrregues",
|
||||||
"uploads": "Uploads",
|
"uploads": "Pujades",
|
||||||
"sharedFiles": "Files"
|
"sharedFiles": "Fitxers"
|
||||||
},
|
},
|
||||||
"jellystat": {
|
"jellystat": {
|
||||||
"songs": "Songs",
|
"songs": "Cançons",
|
||||||
"movies": "Movies",
|
"movies": "Pel·lícules",
|
||||||
"episodes": "Episodes",
|
"episodes": "Episodis",
|
||||||
"other": "Other"
|
"other": "Altres"
|
||||||
},
|
},
|
||||||
"checkmk": {
|
"checkmk": {
|
||||||
"serviceErrors": "Service issues",
|
"serviceErrors": "Errors de servei",
|
||||||
"hostErrors": "Host issues"
|
"hostErrors": "Problemes de l'hoste"
|
||||||
},
|
},
|
||||||
"komodo": {
|
"komodo": {
|
||||||
"total": "Total",
|
"total": "Total",
|
||||||
"running": "Running",
|
"running": "En execució",
|
||||||
"stopped": "Stopped",
|
"stopped": "Aturat",
|
||||||
"down": "Down",
|
"down": "Fora de servei",
|
||||||
"unhealthy": "Unhealthy",
|
"unhealthy": "No saludable",
|
||||||
"unknown": "Unknown",
|
"unknown": "Desconegut",
|
||||||
"servers": "Servers",
|
"servers": "Servidors",
|
||||||
"stacks": "Stacks",
|
"stacks": "Stacks",
|
||||||
"containers": "Containers"
|
"containers": "Contenidors"
|
||||||
},
|
},
|
||||||
"filebrowser": {
|
"filebrowser": {
|
||||||
"available": "Available",
|
"available": "Disponible",
|
||||||
"used": "Used",
|
"used": "Utilitzat",
|
||||||
"total": "Total"
|
"total": "Total"
|
||||||
},
|
},
|
||||||
"wallos": {
|
"wallos": {
|
||||||
"activeSubscriptions": "Subscriptions",
|
"activeSubscriptions": "Subscripcions",
|
||||||
"thisMonthlyCost": "This Month",
|
"thisMonthlyCost": "Aquest mes",
|
||||||
"nextMonthlyCost": "Next Month",
|
"nextMonthlyCost": "Mes que ve",
|
||||||
"previousMonthlyCost": "Prev. Month",
|
"previousMonthlyCost": "Mes passat",
|
||||||
"nextRenewingSubscription": "Next Payment"
|
"nextRenewingSubscription": "Proper pagament"
|
||||||
},
|
},
|
||||||
"unraid": {
|
"unraid": {
|
||||||
"STARTED": "Started",
|
"STARTED": "Iniciat",
|
||||||
"STOPPED": "Stopped",
|
"STOPPED": "Aturat",
|
||||||
"NEW_ARRAY": "New Array",
|
"NEW_ARRAY": "Nou array",
|
||||||
"RECON_DISK": "Reconstructing Disk",
|
"RECON_DISK": "Reconstruint disc",
|
||||||
"DISABLE_DISK": "Disk Disabled",
|
"DISABLE_DISK": "Disc desactivat",
|
||||||
"SWAP_DSBL": "Swap Disable",
|
"SWAP_DSBL": "Swap desactivat",
|
||||||
"INVALID_EXPANSION": "Invalid Expansion",
|
"INVALID_EXPANSION": "Expansió invàlida",
|
||||||
"PARITY_NOT_BIGGEST": "Parity Not Biggest",
|
"PARITY_NOT_BIGGEST": "Paritat no del més gran",
|
||||||
"TOO_MANY_MISSING_DISKS": "Too Many Missing Disks",
|
"TOO_MANY_MISSING_DISKS": "Falten masses discs",
|
||||||
"NEW_DISK_TOO_SMALL": "New Disk Too Small",
|
"NEW_DISK_TOO_SMALL": "Nou disc massa petit",
|
||||||
"NO_DATA_DISKS": "No Data Disks",
|
"NO_DATA_DISKS": "Sense discs de dades",
|
||||||
"notifications": "Notifications",
|
"notifications": "Notificacions",
|
||||||
"status": "Status",
|
"status": "Estat",
|
||||||
"cpu": "CPU",
|
"cpu": "CPU",
|
||||||
"memoryUsed": "Memory Used",
|
"memoryUsed": "Memòria emprada",
|
||||||
"memoryAvailable": "Memory Available",
|
"memoryAvailable": "Memòria disponible",
|
||||||
"arrayUsed": "Array Used",
|
"arrayUsed": "Array en ús",
|
||||||
"arrayFree": "Array Free",
|
"arrayFree": "Array lliure",
|
||||||
"poolUsed": "{{pool}} Used",
|
"poolUsed": "{{pool}} en ús",
|
||||||
"poolFree": "{{pool}} Free"
|
"poolFree": "{{pool}} lliure"
|
||||||
},
|
},
|
||||||
"backrest": {
|
"backrest": {
|
||||||
"num_plans": "Plans",
|
"num_plans": "Plans",
|
||||||
"num_success_30": "Successes",
|
"num_success_30": "Èxits",
|
||||||
"num_failure_30": "Failures",
|
"num_failure_30": "Fallades",
|
||||||
"num_success_latest": "Succeeding",
|
"num_success_latest": "Triomfant",
|
||||||
"num_failure_latest": "Failing",
|
"num_failure_latest": "Fallant",
|
||||||
"bytes_added_30": "Bytes Added"
|
"bytes_added_30": "Bytes afegits"
|
||||||
},
|
},
|
||||||
"yourspotify": {
|
"yourspotify": {
|
||||||
"songs": "Songs",
|
"songs": "Cançons",
|
||||||
"time": "Time",
|
"time": "Temps",
|
||||||
"artists": "Artists"
|
"artists": "Artistes"
|
||||||
},
|
},
|
||||||
"arcane": {
|
"arcane": {
|
||||||
"containers": "Containers",
|
"containers": "Contenidors",
|
||||||
"images": "Images",
|
"images": "Imatges",
|
||||||
"image_updates": "Image Updates",
|
"image_updates": "Actualitzacions d'imatges",
|
||||||
"images_unused": "Unused",
|
"images_unused": "Sense utilitzar",
|
||||||
"environment_required": "Environment ID Required"
|
"environment_required": "ID d'entorn requerit"
|
||||||
},
|
},
|
||||||
"dockhand": {
|
"dockhand": {
|
||||||
"running": "Running",
|
"running": "En execució",
|
||||||
"stopped": "Stopped",
|
"stopped": "Aturats",
|
||||||
"cpu": "CPU",
|
"cpu": "CPU",
|
||||||
"memory": "Memory",
|
"memory": "Memòria",
|
||||||
"images": "Images",
|
"images": "Imatges",
|
||||||
"volumes": "Volumes",
|
"volumes": "Volums",
|
||||||
"events_today": "Events Today",
|
"events_today": "Esdeveniments avui",
|
||||||
"pending_updates": "Pending Updates",
|
"pending_updates": "Actualitzacions pendents",
|
||||||
"stacks": "Stacks",
|
"stacks": "Stacks",
|
||||||
"paused": "Paused",
|
"paused": "Pausats",
|
||||||
"total": "Total",
|
"total": "Total",
|
||||||
"environment_not_found": "Environment Not Found"
|
"environment_not_found": "Entorn no trobat"
|
||||||
},
|
},
|
||||||
"sparkyfitness": {
|
"sparkyfitness": {
|
||||||
"eaten": "Eaten",
|
"eaten": "Menjat",
|
||||||
"burned": "Burned",
|
"burned": "Cremat",
|
||||||
"remaining": "Remaining",
|
"remaining": "Restant",
|
||||||
"steps": "Steps"
|
"steps": "Passes"
|
||||||
},
|
},
|
||||||
"syncthing": {
|
"syncthing": {
|
||||||
"connected": "Connected",
|
"connected": "Connectat",
|
||||||
"synced": "Synced",
|
"synced": "Sincronitzat",
|
||||||
"errors": "Errors",
|
"errors": "Errors",
|
||||||
"storage": "Storage"
|
"storage": "Emmagatzematge"
|
||||||
},
|
},
|
||||||
"sportarr": {
|
"sportarr": {
|
||||||
"wanted": "Wanted",
|
"wanted": "Volgut",
|
||||||
"queued": "Queued",
|
"queued": "Encuat",
|
||||||
"leagues": "Leagues"
|
"leagues": "Lligues"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
"seconds": "s"
|
"seconds": "s"
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"signout": "Sign out"
|
"signout": "Odhlásit se"
|
||||||
},
|
},
|
||||||
"widget": {
|
"widget": {
|
||||||
"missing_type": "Chybí typ widgetu: {{type}}",
|
"missing_type": "Chybí typ widgetu: {{type}}",
|
||||||
@@ -39,7 +39,9 @@
|
|||||||
"wait": "Počkejte prosím"
|
"wait": "Počkejte prosím"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"placeholder": "Hledat…"
|
"placeholder": "Hledat…",
|
||||||
|
"provider": "Search engine: {{name}}",
|
||||||
|
"select_provider": "Select search engine"
|
||||||
},
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
"cpu": "Využití procesoru",
|
"cpu": "Využití procesoru",
|
||||||
@@ -75,13 +77,13 @@
|
|||||||
"no_data": "Nejsou k dispozici žádná data úložiště"
|
"no_data": "Nejsou k dispozici žádná data úložiště"
|
||||||
},
|
},
|
||||||
"duplicati": {
|
"duplicati": {
|
||||||
"jobs": "Jobs",
|
"jobs": "Úlohy",
|
||||||
"stored": "Stored",
|
"stored": "Uloženo",
|
||||||
"lastBackup": "Last Run",
|
"lastBackup": "Naposledy spuštěno",
|
||||||
"nextRun": "Next Run",
|
"nextRun": "Další spuštění",
|
||||||
"running": "Running",
|
"running": "Běží",
|
||||||
"warnings": "Warnings",
|
"warnings": "Upozornění",
|
||||||
"errors": "Errors"
|
"errors": "Chyby"
|
||||||
},
|
},
|
||||||
"docker": {
|
"docker": {
|
||||||
"rx": "RX",
|
"rx": "RX",
|
||||||
@@ -97,7 +99,12 @@
|
|||||||
"unhealthy": "Nezdravý",
|
"unhealthy": "Nezdravý",
|
||||||
"not_found": "Nenalezen",
|
"not_found": "Nenalezen",
|
||||||
"exited": "Ukončen",
|
"exited": "Ukončen",
|
||||||
"partial": "Částečný"
|
"partial": "Částečný",
|
||||||
|
"created": "Created",
|
||||||
|
"paused": "Paused",
|
||||||
|
"restarting": "Restarting",
|
||||||
|
"removing": "Removing",
|
||||||
|
"dead": "Dead"
|
||||||
},
|
},
|
||||||
"ping": {
|
"ping": {
|
||||||
"error": "Chyba",
|
"error": "Chyba",
|
||||||
@@ -133,7 +140,7 @@
|
|||||||
"series": "Seriály",
|
"series": "Seriály",
|
||||||
"episodes": "Epizody",
|
"episodes": "Epizody",
|
||||||
"songs": "Skladby",
|
"songs": "Skladby",
|
||||||
"albums": "Albums"
|
"albums": "Alba"
|
||||||
},
|
},
|
||||||
"esphome": {
|
"esphome": {
|
||||||
"offline": "Offline",
|
"offline": "Offline",
|
||||||
@@ -471,7 +478,8 @@
|
|||||||
"authentik": {
|
"authentik": {
|
||||||
"users": "Uživatelé",
|
"users": "Uživatelé",
|
||||||
"loginsLast24H": "Příhlášení (24h)",
|
"loginsLast24H": "Příhlášení (24h)",
|
||||||
"failedLoginsLast24H": "Neúspěšná přihlášení (24h)"
|
"failedLoginsLast24H": "Neúspěšná přihlášení (24h)",
|
||||||
|
"authorizationsLast24H": "Autorizace (24h)"
|
||||||
},
|
},
|
||||||
"proxmox": {
|
"proxmox": {
|
||||||
"mem": "Využití paměti",
|
"mem": "Využití paměti",
|
||||||
@@ -951,15 +959,15 @@
|
|||||||
"storage": "Úložiště"
|
"storage": "Úložiště"
|
||||||
},
|
},
|
||||||
"maintainerr": {
|
"maintainerr": {
|
||||||
"itemsHandled": "Items Handled",
|
"itemsHandled": "Zpracované položky",
|
||||||
"episodesHandled": "Episodes Handled",
|
"episodesHandled": "Zpracované epizody",
|
||||||
"moviesHandled": "Movies Handled",
|
"moviesHandled": "Zpracované filmy",
|
||||||
"showsHandled": "Shows Handled",
|
"showsHandled": "Zpracované seriály",
|
||||||
"seasonsHandled": "Seasons Handled",
|
"seasonsHandled": "Zpracované série",
|
||||||
"reclaimable": "Reclaimable",
|
"reclaimable": "Uvolnitelné",
|
||||||
"movieReclaimable": "Movie Reclaimable",
|
"movieReclaimable": "Uvolnitelné filmy",
|
||||||
"showReclaimable": "Show Reclaimable",
|
"showReclaimable": "Uvolnitelné seriály",
|
||||||
"totalCapacity": "Total Capacity"
|
"totalCapacity": "Celková kapacita"
|
||||||
},
|
},
|
||||||
"netdata": {
|
"netdata": {
|
||||||
"warnings": "Upozornění",
|
"warnings": "Upozornění",
|
||||||
@@ -1241,14 +1249,14 @@
|
|||||||
"steps": "Kroky"
|
"steps": "Kroky"
|
||||||
},
|
},
|
||||||
"syncthing": {
|
"syncthing": {
|
||||||
"connected": "Connected",
|
"connected": "Připojeno",
|
||||||
"synced": "Synced",
|
"synced": "Synchronizováno",
|
||||||
"errors": "Errors",
|
"errors": "Chyby",
|
||||||
"storage": "Storage"
|
"storage": "Úložiště"
|
||||||
},
|
},
|
||||||
"sportarr": {
|
"sportarr": {
|
||||||
"wanted": "Wanted",
|
"wanted": "Požadované",
|
||||||
"queued": "Queued",
|
"queued": "Ve frontě",
|
||||||
"leagues": "Leagues"
|
"leagues": "Ligy"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,9 @@
|
|||||||
"wait": "Vent venligst"
|
"wait": "Vent venligst"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"placeholder": "Søg…"
|
"placeholder": "Søg…",
|
||||||
|
"provider": "Search engine: {{name}}",
|
||||||
|
"select_provider": "Select search engine"
|
||||||
},
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
"cpu": "CPU",
|
"cpu": "CPU",
|
||||||
@@ -97,7 +99,12 @@
|
|||||||
"unhealthy": "Usund",
|
"unhealthy": "Usund",
|
||||||
"not_found": "Ikke Fundet",
|
"not_found": "Ikke Fundet",
|
||||||
"exited": "Forladt",
|
"exited": "Forladt",
|
||||||
"partial": "Delvis"
|
"partial": "Delvis",
|
||||||
|
"created": "Created",
|
||||||
|
"paused": "Paused",
|
||||||
|
"restarting": "Restarting",
|
||||||
|
"removing": "Removing",
|
||||||
|
"dead": "Dead"
|
||||||
},
|
},
|
||||||
"ping": {
|
"ping": {
|
||||||
"error": "Error",
|
"error": "Error",
|
||||||
@@ -471,7 +478,8 @@
|
|||||||
"authentik": {
|
"authentik": {
|
||||||
"users": "Users",
|
"users": "Users",
|
||||||
"loginsLast24H": "Login (24 timer)",
|
"loginsLast24H": "Login (24 timer)",
|
||||||
"failedLoginsLast24H": "Mislykkede logins (24 timer)"
|
"failedLoginsLast24H": "Mislykkede logins (24 timer)",
|
||||||
|
"authorizationsLast24H": "Auths (24h)"
|
||||||
},
|
},
|
||||||
"proxmox": {
|
"proxmox": {
|
||||||
"mem": "MEM",
|
"mem": "MEM",
|
||||||
|
|||||||
@@ -39,7 +39,9 @@
|
|||||||
"wait": "Bitte warten"
|
"wait": "Bitte warten"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"placeholder": "Suche…"
|
"placeholder": "Suche…",
|
||||||
|
"provider": "Search engine: {{name}}",
|
||||||
|
"select_provider": "Select search engine"
|
||||||
},
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
"cpu": "CPU",
|
"cpu": "CPU",
|
||||||
@@ -97,7 +99,12 @@
|
|||||||
"unhealthy": "Fehlerhaft",
|
"unhealthy": "Fehlerhaft",
|
||||||
"not_found": "Nicht gefunden",
|
"not_found": "Nicht gefunden",
|
||||||
"exited": "Beendet",
|
"exited": "Beendet",
|
||||||
"partial": "Teilweise"
|
"partial": "Teilweise",
|
||||||
|
"created": "Created",
|
||||||
|
"paused": "Paused",
|
||||||
|
"restarting": "Restarting",
|
||||||
|
"removing": "Removing",
|
||||||
|
"dead": "Dead"
|
||||||
},
|
},
|
||||||
"ping": {
|
"ping": {
|
||||||
"error": "Fehler",
|
"error": "Fehler",
|
||||||
@@ -471,7 +478,8 @@
|
|||||||
"authentik": {
|
"authentik": {
|
||||||
"users": "Benutzer",
|
"users": "Benutzer",
|
||||||
"loginsLast24H": "Anmeldungen (24 h)",
|
"loginsLast24H": "Anmeldungen (24 h)",
|
||||||
"failedLoginsLast24H": "Fehlversuche (24 h)"
|
"failedLoginsLast24H": "Fehlversuche (24 h)",
|
||||||
|
"authorizationsLast24H": "Auths (24h)"
|
||||||
},
|
},
|
||||||
"proxmox": {
|
"proxmox": {
|
||||||
"mem": "RAM",
|
"mem": "RAM",
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
"seconds": "s"
|
"seconds": "s"
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"signout": "Sign out"
|
"signout": "Αποσύνδεση"
|
||||||
},
|
},
|
||||||
"widget": {
|
"widget": {
|
||||||
"missing_type": "Λείπει ο τύπος widget: {{type}}",
|
"missing_type": "Λείπει ο τύπος widget: {{type}}",
|
||||||
@@ -39,7 +39,9 @@
|
|||||||
"wait": "Παρακαλώ περιμένετε"
|
"wait": "Παρακαλώ περιμένετε"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"placeholder": "Αναζήτηση…"
|
"placeholder": "Αναζήτηση…",
|
||||||
|
"provider": "Search engine: {{name}}",
|
||||||
|
"select_provider": "Επιλέξτε τις μηχανές αναζήτησης"
|
||||||
},
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
"cpu": "Επεξεργαστής",
|
"cpu": "Επεξεργαστής",
|
||||||
@@ -64,24 +66,24 @@
|
|||||||
"wlan_devices": "WLAN Συσκευές",
|
"wlan_devices": "WLAN Συσκευές",
|
||||||
"lan_users": "LAN Χρήστες",
|
"lan_users": "LAN Χρήστες",
|
||||||
"wlan_users": "WLAN Χρήστες",
|
"wlan_users": "WLAN Χρήστες",
|
||||||
"up": "UP",
|
"up": "ΕΠΑΝΩ",
|
||||||
"down": "ΚΑΤΩ",
|
"down": "ΚΑΤΩ",
|
||||||
"wait": "Please wait",
|
"wait": "Παρακαλώ περιμένετε",
|
||||||
"empty_data": "Άγνωστη κατάσταση υποσυστήματος"
|
"empty_data": "Άγνωστη κατάσταση υποσυστήματος"
|
||||||
},
|
},
|
||||||
"unifi_drive": {
|
"unifi_drive": {
|
||||||
"healthy": "Healthy",
|
"healthy": "Υγειές",
|
||||||
"degraded": "Degraded",
|
"degraded": "Degraded",
|
||||||
"no_data": "No storage data available"
|
"no_data": "No storage data available"
|
||||||
},
|
},
|
||||||
"duplicati": {
|
"duplicati": {
|
||||||
"jobs": "Jobs",
|
"jobs": "Εργασίες",
|
||||||
"stored": "Stored",
|
"stored": "Αποθηκευμένα",
|
||||||
"lastBackup": "Last Run",
|
"lastBackup": "Τελευταία εκτέλεση",
|
||||||
"nextRun": "Next Run",
|
"nextRun": "Επόμενη εκτέλεση",
|
||||||
"running": "Running",
|
"running": "Εκτελείται",
|
||||||
"warnings": "Warnings",
|
"warnings": "Προειδοποιήσεις",
|
||||||
"errors": "Errors"
|
"errors": "Σφάλματα"
|
||||||
},
|
},
|
||||||
"docker": {
|
"docker": {
|
||||||
"rx": "RX",
|
"rx": "RX",
|
||||||
@@ -97,10 +99,15 @@
|
|||||||
"unhealthy": "Άρρωστο",
|
"unhealthy": "Άρρωστο",
|
||||||
"not_found": "Δεν βρέθηκε",
|
"not_found": "Δεν βρέθηκε",
|
||||||
"exited": "Έκλεισε",
|
"exited": "Έκλεισε",
|
||||||
"partial": "Μερικό"
|
"partial": "Μερικό",
|
||||||
|
"created": "Δημιουργήθηκε",
|
||||||
|
"paused": "Σε παύση",
|
||||||
|
"restarting": "Γίνεται επανεκκίνηση",
|
||||||
|
"removing": "Αφαιρείται",
|
||||||
|
"dead": "Dead"
|
||||||
},
|
},
|
||||||
"ping": {
|
"ping": {
|
||||||
"error": "Error",
|
"error": "Σφάλμα",
|
||||||
"ping": "Ping",
|
"ping": "Ping",
|
||||||
"down": "Ping down",
|
"down": "Ping down",
|
||||||
"up": "Ping up",
|
"up": "Ping up",
|
||||||
@@ -108,11 +115,11 @@
|
|||||||
},
|
},
|
||||||
"siteMonitor": {
|
"siteMonitor": {
|
||||||
"http_status": "Κατάσταση HTTP",
|
"http_status": "Κατάσταση HTTP",
|
||||||
"error": "Error",
|
"error": "Σφάλμα",
|
||||||
"response": "Απόκριση",
|
"response": "Απόκριση",
|
||||||
"down": "Down",
|
"down": "Down",
|
||||||
"up": "Up",
|
"up": "Up",
|
||||||
"not_available": "Not Available"
|
"not_available": "Μη Διαθέσιμο"
|
||||||
},
|
},
|
||||||
"emby": {
|
"emby": {
|
||||||
"playing": "Παίζει",
|
"playing": "Παίζει",
|
||||||
@@ -125,7 +132,7 @@
|
|||||||
"songs": "Τραγούδια"
|
"songs": "Τραγούδια"
|
||||||
},
|
},
|
||||||
"jellyfin": {
|
"jellyfin": {
|
||||||
"playing": "Playing",
|
"playing": "Γίνεται αναπαραγωγή",
|
||||||
"transcoding": "Transcoding",
|
"transcoding": "Transcoding",
|
||||||
"bitrate": "Bitrate",
|
"bitrate": "Bitrate",
|
||||||
"no_active": "No Active Streams",
|
"no_active": "No Active Streams",
|
||||||
@@ -471,7 +478,8 @@
|
|||||||
"authentik": {
|
"authentik": {
|
||||||
"users": "Users",
|
"users": "Users",
|
||||||
"loginsLast24H": "Συνδέσεις (24h)",
|
"loginsLast24H": "Συνδέσεις (24h)",
|
||||||
"failedLoginsLast24H": "Αποτυχημένες Συνδέσεις (24h)"
|
"failedLoginsLast24H": "Αποτυχημένες Συνδέσεις (24h)",
|
||||||
|
"authorizationsLast24H": "Auths (24h)"
|
||||||
},
|
},
|
||||||
"proxmox": {
|
"proxmox": {
|
||||||
"mem": "MEM",
|
"mem": "MEM",
|
||||||
|
|||||||
@@ -39,7 +39,9 @@
|
|||||||
"wait": "Please wait"
|
"wait": "Please wait"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"placeholder": "Search…"
|
"placeholder": "Search…",
|
||||||
|
"provider": "Search engine: {{name}}",
|
||||||
|
"select_provider": "Select search engine"
|
||||||
},
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
"cpu": "CPU",
|
"cpu": "CPU",
|
||||||
@@ -97,7 +99,12 @@
|
|||||||
"unhealthy": "Unhealthy",
|
"unhealthy": "Unhealthy",
|
||||||
"not_found": "Not Found",
|
"not_found": "Not Found",
|
||||||
"exited": "Exited",
|
"exited": "Exited",
|
||||||
"partial": "Partial"
|
"partial": "Partial",
|
||||||
|
"created": "Created",
|
||||||
|
"paused": "Paused",
|
||||||
|
"restarting": "Restarting",
|
||||||
|
"removing": "Removing",
|
||||||
|
"dead": "Dead"
|
||||||
},
|
},
|
||||||
"ping": {
|
"ping": {
|
||||||
"error": "Error",
|
"error": "Error",
|
||||||
@@ -471,7 +478,8 @@
|
|||||||
"authentik": {
|
"authentik": {
|
||||||
"users": "Users",
|
"users": "Users",
|
||||||
"loginsLast24H": "Logins (24h)",
|
"loginsLast24H": "Logins (24h)",
|
||||||
"failedLoginsLast24H": "Failed Logins (24h)"
|
"failedLoginsLast24H": "Failed Logins (24h)",
|
||||||
|
"authorizationsLast24H": "Auths (24h)"
|
||||||
},
|
},
|
||||||
"proxmox": {
|
"proxmox": {
|
||||||
"mem": "MEM",
|
"mem": "MEM",
|
||||||
|
|||||||
@@ -39,7 +39,9 @@
|
|||||||
"wait": "Bonvolu atendi"
|
"wait": "Bonvolu atendi"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"placeholder": "Serĉi…"
|
"placeholder": "Serĉi…",
|
||||||
|
"provider": "Search engine: {{name}}",
|
||||||
|
"select_provider": "Select search engine"
|
||||||
},
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
"cpu": "Ĉefprocesoro",
|
"cpu": "Ĉefprocesoro",
|
||||||
@@ -97,7 +99,12 @@
|
|||||||
"unhealthy": "Malsana",
|
"unhealthy": "Malsana",
|
||||||
"not_found": "Ne trovita",
|
"not_found": "Ne trovita",
|
||||||
"exited": "Eliris",
|
"exited": "Eliris",
|
||||||
"partial": "Parta"
|
"partial": "Parta",
|
||||||
|
"created": "Created",
|
||||||
|
"paused": "Paused",
|
||||||
|
"restarting": "Restarting",
|
||||||
|
"removing": "Removing",
|
||||||
|
"dead": "Dead"
|
||||||
},
|
},
|
||||||
"ping": {
|
"ping": {
|
||||||
"error": "Error",
|
"error": "Error",
|
||||||
@@ -471,7 +478,8 @@
|
|||||||
"authentik": {
|
"authentik": {
|
||||||
"users": "Users",
|
"users": "Users",
|
||||||
"loginsLast24H": "Logins (24h)",
|
"loginsLast24H": "Logins (24h)",
|
||||||
"failedLoginsLast24H": "Failed Logins (24h)"
|
"failedLoginsLast24H": "Failed Logins (24h)",
|
||||||
|
"authorizationsLast24H": "Auths (24h)"
|
||||||
},
|
},
|
||||||
"proxmox": {
|
"proxmox": {
|
||||||
"mem": "MEM",
|
"mem": "MEM",
|
||||||
|
|||||||
@@ -39,7 +39,9 @@
|
|||||||
"wait": "Espera, por favor"
|
"wait": "Espera, por favor"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"placeholder": "Buscar…"
|
"placeholder": "Buscar…",
|
||||||
|
"provider": "Motor de búsqueda: {{name}}",
|
||||||
|
"select_provider": "Seleccionar motores de búsqueda"
|
||||||
},
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
"cpu": "CPU",
|
"cpu": "CPU",
|
||||||
@@ -97,7 +99,12 @@
|
|||||||
"unhealthy": "No saludable",
|
"unhealthy": "No saludable",
|
||||||
"not_found": "No encontrado",
|
"not_found": "No encontrado",
|
||||||
"exited": "Terminado",
|
"exited": "Terminado",
|
||||||
"partial": "Parcial"
|
"partial": "Parcial",
|
||||||
|
"created": "Created",
|
||||||
|
"paused": "Paused",
|
||||||
|
"restarting": "Restarting",
|
||||||
|
"removing": "Removing",
|
||||||
|
"dead": "Dead"
|
||||||
},
|
},
|
||||||
"ping": {
|
"ping": {
|
||||||
"error": "Error",
|
"error": "Error",
|
||||||
@@ -133,7 +140,7 @@
|
|||||||
"series": "Series",
|
"series": "Series",
|
||||||
"episodes": "Episodios",
|
"episodes": "Episodios",
|
||||||
"songs": "Canciones",
|
"songs": "Canciones",
|
||||||
"albums": "Albums"
|
"albums": "Álbumes"
|
||||||
},
|
},
|
||||||
"esphome": {
|
"esphome": {
|
||||||
"offline": "Fuera de línea",
|
"offline": "Fuera de línea",
|
||||||
@@ -471,7 +478,8 @@
|
|||||||
"authentik": {
|
"authentik": {
|
||||||
"users": "Usuarios",
|
"users": "Usuarios",
|
||||||
"loginsLast24H": "Inicios de sesión (24h)",
|
"loginsLast24H": "Inicios de sesión (24h)",
|
||||||
"failedLoginsLast24H": "Inicios de sesión fallidos (24h)"
|
"failedLoginsLast24H": "Inicios de sesión fallidos (24h)",
|
||||||
|
"authorizationsLast24H": "Autenticaciones (24 h)"
|
||||||
},
|
},
|
||||||
"proxmox": {
|
"proxmox": {
|
||||||
"mem": "MEM",
|
"mem": "MEM",
|
||||||
|
|||||||
@@ -39,7 +39,9 @@
|
|||||||
"wait": "Itxaron mesedez"
|
"wait": "Itxaron mesedez"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"placeholder": "Bilatu…"
|
"placeholder": "Bilatu…",
|
||||||
|
"provider": "Search engine: {{name}}",
|
||||||
|
"select_provider": "Select search engine"
|
||||||
},
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
"cpu": "CPU",
|
"cpu": "CPU",
|
||||||
@@ -97,7 +99,12 @@
|
|||||||
"unhealthy": "Unhealthy",
|
"unhealthy": "Unhealthy",
|
||||||
"not_found": "Not Found",
|
"not_found": "Not Found",
|
||||||
"exited": "Exited",
|
"exited": "Exited",
|
||||||
"partial": "Partial"
|
"partial": "Partial",
|
||||||
|
"created": "Created",
|
||||||
|
"paused": "Paused",
|
||||||
|
"restarting": "Restarting",
|
||||||
|
"removing": "Removing",
|
||||||
|
"dead": "Dead"
|
||||||
},
|
},
|
||||||
"ping": {
|
"ping": {
|
||||||
"error": "Error",
|
"error": "Error",
|
||||||
@@ -471,7 +478,8 @@
|
|||||||
"authentik": {
|
"authentik": {
|
||||||
"users": "Users",
|
"users": "Users",
|
||||||
"loginsLast24H": "Logins (24h)",
|
"loginsLast24H": "Logins (24h)",
|
||||||
"failedLoginsLast24H": "Failed Logins (24h)"
|
"failedLoginsLast24H": "Failed Logins (24h)",
|
||||||
|
"authorizationsLast24H": "Auths (24h)"
|
||||||
},
|
},
|
||||||
"proxmox": {
|
"proxmox": {
|
||||||
"mem": "MEM",
|
"mem": "MEM",
|
||||||
|
|||||||
@@ -39,7 +39,9 @@
|
|||||||
"wait": "Odota, ole hyvä"
|
"wait": "Odota, ole hyvä"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"placeholder": "Hae…"
|
"placeholder": "Hae…",
|
||||||
|
"provider": "Search engine: {{name}}",
|
||||||
|
"select_provider": "Select search engine"
|
||||||
},
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
"cpu": "CPU",
|
"cpu": "CPU",
|
||||||
@@ -97,7 +99,12 @@
|
|||||||
"unhealthy": "Unhealthy",
|
"unhealthy": "Unhealthy",
|
||||||
"not_found": "Not Found",
|
"not_found": "Not Found",
|
||||||
"exited": "Exited",
|
"exited": "Exited",
|
||||||
"partial": "Partial"
|
"partial": "Partial",
|
||||||
|
"created": "Created",
|
||||||
|
"paused": "Paused",
|
||||||
|
"restarting": "Restarting",
|
||||||
|
"removing": "Removing",
|
||||||
|
"dead": "Dead"
|
||||||
},
|
},
|
||||||
"ping": {
|
"ping": {
|
||||||
"error": "Error",
|
"error": "Error",
|
||||||
@@ -471,7 +478,8 @@
|
|||||||
"authentik": {
|
"authentik": {
|
||||||
"users": "Users",
|
"users": "Users",
|
||||||
"loginsLast24H": "Kirjautumisia (24h)",
|
"loginsLast24H": "Kirjautumisia (24h)",
|
||||||
"failedLoginsLast24H": "Epäonnistuneita kirjautumisia (24h)"
|
"failedLoginsLast24H": "Epäonnistuneita kirjautumisia (24h)",
|
||||||
|
"authorizationsLast24H": "Auths (24h)"
|
||||||
},
|
},
|
||||||
"proxmox": {
|
"proxmox": {
|
||||||
"mem": "MEM",
|
"mem": "MEM",
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
"seconds": "s"
|
"seconds": "s"
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"signout": "Sign out"
|
"signout": "Se déconnecter"
|
||||||
},
|
},
|
||||||
"widget": {
|
"widget": {
|
||||||
"missing_type": "Type de widget manquant : {{type}}",
|
"missing_type": "Type de widget manquant : {{type}}",
|
||||||
@@ -39,7 +39,9 @@
|
|||||||
"wait": "Veuillez patienter"
|
"wait": "Veuillez patienter"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"placeholder": "Recherche…"
|
"placeholder": "Recherche…",
|
||||||
|
"provider": "Moteur de recherche : {{name}}",
|
||||||
|
"select_provider": "Sélectionnez un moteur de recherche"
|
||||||
},
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
"cpu": "CPU",
|
"cpu": "CPU",
|
||||||
@@ -97,7 +99,12 @@
|
|||||||
"unhealthy": "Mauvaise santé",
|
"unhealthy": "Mauvaise santé",
|
||||||
"not_found": "Inconnu",
|
"not_found": "Inconnu",
|
||||||
"exited": "Arrêté",
|
"exited": "Arrêté",
|
||||||
"partial": "Partiel"
|
"partial": "Partiel",
|
||||||
|
"created": "Créé",
|
||||||
|
"paused": "En pause",
|
||||||
|
"restarting": "Redémarrage",
|
||||||
|
"removing": "Suppression",
|
||||||
|
"dead": "Mort"
|
||||||
},
|
},
|
||||||
"ping": {
|
"ping": {
|
||||||
"error": "Erreur",
|
"error": "Erreur",
|
||||||
@@ -471,7 +478,8 @@
|
|||||||
"authentik": {
|
"authentik": {
|
||||||
"users": "Utilisateurs",
|
"users": "Utilisateurs",
|
||||||
"loginsLast24H": "Connexions (24 h)",
|
"loginsLast24H": "Connexions (24 h)",
|
||||||
"failedLoginsLast24H": "Connexions échouées (24 h)"
|
"failedLoginsLast24H": "Connexions échouées (24 h)",
|
||||||
|
"authorizationsLast24H": "Authentifications (24h)"
|
||||||
},
|
},
|
||||||
"proxmox": {
|
"proxmox": {
|
||||||
"mem": "RAM",
|
"mem": "RAM",
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
"seconds": "שניות"
|
"seconds": "שניות"
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"signout": "Sign out"
|
"signout": "התנתק"
|
||||||
},
|
},
|
||||||
"widget": {
|
"widget": {
|
||||||
"missing_type": "סוג ווידג'ט חסר: {{type}}",
|
"missing_type": "סוג ווידג'ט חסר: {{type}}",
|
||||||
@@ -39,7 +39,9 @@
|
|||||||
"wait": "המתן בבקשה"
|
"wait": "המתן בבקשה"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"placeholder": "חיפוש…"
|
"placeholder": "חיפוש…",
|
||||||
|
"provider": "מנוע חיפוש: {{name}}",
|
||||||
|
"select_provider": "בחר מנוע חיפוש"
|
||||||
},
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
"cpu": "מעבד",
|
"cpu": "מעבד",
|
||||||
@@ -70,18 +72,18 @@
|
|||||||
"empty_data": "מצב תת-מערכת לא ידוע"
|
"empty_data": "מצב תת-מערכת לא ידוע"
|
||||||
},
|
},
|
||||||
"unifi_drive": {
|
"unifi_drive": {
|
||||||
"healthy": "Healthy",
|
"healthy": "בריא",
|
||||||
"degraded": "Degraded",
|
"degraded": "ירוד",
|
||||||
"no_data": "No storage data available"
|
"no_data": "אין אחסון זמין"
|
||||||
},
|
},
|
||||||
"duplicati": {
|
"duplicati": {
|
||||||
"jobs": "Jobs",
|
"jobs": "משימות",
|
||||||
"stored": "Stored",
|
"stored": "שמור",
|
||||||
"lastBackup": "Last Run",
|
"lastBackup": "Last Run",
|
||||||
"nextRun": "Next Run",
|
"nextRun": "Next Run",
|
||||||
"running": "Running",
|
"running": "Running",
|
||||||
"warnings": "Warnings",
|
"warnings": "הזהרות",
|
||||||
"errors": "Errors"
|
"errors": "שגיאות"
|
||||||
},
|
},
|
||||||
"docker": {
|
"docker": {
|
||||||
"rx": "RX",
|
"rx": "RX",
|
||||||
@@ -97,7 +99,12 @@
|
|||||||
"unhealthy": "לא בריא",
|
"unhealthy": "לא בריא",
|
||||||
"not_found": "לא נמצא",
|
"not_found": "לא נמצא",
|
||||||
"exited": "יצא",
|
"exited": "יצא",
|
||||||
"partial": "חלקי"
|
"partial": "חלקי",
|
||||||
|
"created": "Created",
|
||||||
|
"paused": "Paused",
|
||||||
|
"restarting": "Restarting",
|
||||||
|
"removing": "Removing",
|
||||||
|
"dead": "Dead"
|
||||||
},
|
},
|
||||||
"ping": {
|
"ping": {
|
||||||
"error": "שגיאה",
|
"error": "שגיאה",
|
||||||
@@ -125,15 +132,15 @@
|
|||||||
"songs": "שירים"
|
"songs": "שירים"
|
||||||
},
|
},
|
||||||
"jellyfin": {
|
"jellyfin": {
|
||||||
"playing": "Playing",
|
"playing": "מנגן",
|
||||||
"transcoding": "Transcoding",
|
"transcoding": "Transcoding",
|
||||||
"bitrate": "Bitrate",
|
"bitrate": "Bitrate",
|
||||||
"no_active": "No Active Streams",
|
"no_active": "אין מדיה פעילה",
|
||||||
"movies": "Movies",
|
"movies": "סרטים",
|
||||||
"series": "Series",
|
"series": "סדרות",
|
||||||
"episodes": "Episodes",
|
"episodes": "פרקים",
|
||||||
"songs": "Songs",
|
"songs": "שירים",
|
||||||
"albums": "Albums"
|
"albums": "אלבומים"
|
||||||
},
|
},
|
||||||
"esphome": {
|
"esphome": {
|
||||||
"offline": "מכובה",
|
"offline": "מכובה",
|
||||||
@@ -308,11 +315,11 @@
|
|||||||
"available": "זמין"
|
"available": "זמין"
|
||||||
},
|
},
|
||||||
"seerr": {
|
"seerr": {
|
||||||
"pending": "Pending",
|
"pending": "ממתין",
|
||||||
"approved": "Approved",
|
"approved": "מאושר",
|
||||||
"available": "Available",
|
"available": "זמין",
|
||||||
"completed": "Completed",
|
"completed": "הסתיים",
|
||||||
"processing": "Processing",
|
"processing": "מעבד",
|
||||||
"issues": "Open Issues"
|
"issues": "Open Issues"
|
||||||
},
|
},
|
||||||
"netalertx": {
|
"netalertx": {
|
||||||
@@ -357,16 +364,16 @@
|
|||||||
"address": "כתובת",
|
"address": "כתובת",
|
||||||
"expires": "תאריך תפוגה",
|
"expires": "תאריך תפוגה",
|
||||||
"never": "אף פעם",
|
"never": "אף פעם",
|
||||||
"user": "User",
|
"user": "משתמש",
|
||||||
"hostname": "Hostname",
|
"hostname": "Hostname",
|
||||||
"name": "Name",
|
"name": "שם",
|
||||||
"client_version": "Client Version",
|
"client_version": "Client Version",
|
||||||
"os": "OS",
|
"os": "מערכת הפעלה",
|
||||||
"created": "Created",
|
"created": "יוצר",
|
||||||
"authorized": "Authorized",
|
"authorized": "מורשה",
|
||||||
"is_external": "Is External",
|
"is_external": "האם חיצוני",
|
||||||
"update_available": "Update Available",
|
"update_available": "עדכון זמין",
|
||||||
"tags": "Tags",
|
"tags": "תגיות",
|
||||||
"last_seen": "נראה לאחרונה",
|
"last_seen": "נראה לאחרונה",
|
||||||
"now": "כעת",
|
"now": "כעת",
|
||||||
"years": "{{number}}y",
|
"years": "{{number}}y",
|
||||||
@@ -376,8 +383,8 @@
|
|||||||
"minutes": "{{number}}m",
|
"minutes": "{{number}}m",
|
||||||
"seconds": "{{number}}s",
|
"seconds": "{{number}}s",
|
||||||
"ago": "{{value}} Ago",
|
"ago": "{{value}} Ago",
|
||||||
"true": "Yes",
|
"true": "כן",
|
||||||
"false": "No"
|
"false": "לא"
|
||||||
},
|
},
|
||||||
"technitium": {
|
"technitium": {
|
||||||
"totalQueries": "שאילתות",
|
"totalQueries": "שאילתות",
|
||||||
@@ -471,7 +478,8 @@
|
|||||||
"authentik": {
|
"authentik": {
|
||||||
"users": "משתמשים",
|
"users": "משתמשים",
|
||||||
"loginsLast24H": "כניסות (24h)",
|
"loginsLast24H": "כניסות (24h)",
|
||||||
"failedLoginsLast24H": "כניסות שנכשלו (24h)"
|
"failedLoginsLast24H": "כניסות שנכשלו (24h)",
|
||||||
|
"authorizationsLast24H": "Auths (24h)"
|
||||||
},
|
},
|
||||||
"proxmox": {
|
"proxmox": {
|
||||||
"mem": "זיכרון",
|
"mem": "זיכרון",
|
||||||
@@ -481,7 +489,7 @@
|
|||||||
},
|
},
|
||||||
"pulse": {
|
"pulse": {
|
||||||
"nodes": "Nodes",
|
"nodes": "Nodes",
|
||||||
"vms": "VMs",
|
"vms": "מכונות וירטואליות",
|
||||||
"lxcs": "LXCs"
|
"lxcs": "LXCs"
|
||||||
},
|
},
|
||||||
"glances": {
|
"glances": {
|
||||||
@@ -581,7 +589,7 @@
|
|||||||
"up": "למעלה",
|
"up": "למעלה",
|
||||||
"pending": "ממתין",
|
"pending": "ממתין",
|
||||||
"down": "למטה",
|
"down": "למטה",
|
||||||
"ok": "Ok"
|
"ok": "אוקיי"
|
||||||
},
|
},
|
||||||
"healthchecks": {
|
"healthchecks": {
|
||||||
"new": "חדש",
|
"new": "חדש",
|
||||||
@@ -650,9 +658,9 @@
|
|||||||
},
|
},
|
||||||
"pangolin": {
|
"pangolin": {
|
||||||
"orgs": "Orgs",
|
"orgs": "Orgs",
|
||||||
"sites": "Sites",
|
"sites": "אתרים",
|
||||||
"resources": "Resources",
|
"resources": "משאבים",
|
||||||
"targets": "Targets",
|
"targets": "מטרות",
|
||||||
"traffic": "Traffic",
|
"traffic": "Traffic",
|
||||||
"in": "In",
|
"in": "In",
|
||||||
"out": "Out"
|
"out": "Out"
|
||||||
@@ -754,7 +762,7 @@
|
|||||||
"volumeAvailable": "זמין"
|
"volumeAvailable": "זמין"
|
||||||
},
|
},
|
||||||
"dispatcharr": {
|
"dispatcharr": {
|
||||||
"channels": "Channels",
|
"channels": "ערוצים",
|
||||||
"streams": "Streams"
|
"streams": "Streams"
|
||||||
},
|
},
|
||||||
"mylar": {
|
"mylar": {
|
||||||
@@ -1179,9 +1187,9 @@
|
|||||||
"nextRenewingSubscription": "תשלום הבא"
|
"nextRenewingSubscription": "תשלום הבא"
|
||||||
},
|
},
|
||||||
"unraid": {
|
"unraid": {
|
||||||
"STARTED": "Started",
|
"STARTED": "התחיל",
|
||||||
"STOPPED": "Stopped",
|
"STOPPED": "נעצר",
|
||||||
"NEW_ARRAY": "New Array",
|
"NEW_ARRAY": "מערך חדש",
|
||||||
"RECON_DISK": "Reconstructing Disk",
|
"RECON_DISK": "Reconstructing Disk",
|
||||||
"DISABLE_DISK": "Disk Disabled",
|
"DISABLE_DISK": "Disk Disabled",
|
||||||
"SWAP_DSBL": "Swap Disable",
|
"SWAP_DSBL": "Swap Disable",
|
||||||
@@ -1193,31 +1201,31 @@
|
|||||||
"notifications": "Notifications",
|
"notifications": "Notifications",
|
||||||
"status": "Status",
|
"status": "Status",
|
||||||
"cpu": "CPU",
|
"cpu": "CPU",
|
||||||
"memoryUsed": "Memory Used",
|
"memoryUsed": "זיכרון בשימוש",
|
||||||
"memoryAvailable": "Memory Available",
|
"memoryAvailable": "זיכרון זמין",
|
||||||
"arrayUsed": "Array Used",
|
"arrayUsed": "Array Used",
|
||||||
"arrayFree": "Array Free",
|
"arrayFree": "Array Free",
|
||||||
"poolUsed": "{{pool}} Used",
|
"poolUsed": "{{pool}} Used",
|
||||||
"poolFree": "{{pool}} Free"
|
"poolFree": "{{pool}} Free"
|
||||||
},
|
},
|
||||||
"backrest": {
|
"backrest": {
|
||||||
"num_plans": "Plans",
|
"num_plans": "תוכניות",
|
||||||
"num_success_30": "Successes",
|
"num_success_30": "הצלחות",
|
||||||
"num_failure_30": "Failures",
|
"num_failure_30": "כשלונות",
|
||||||
"num_success_latest": "Succeeding",
|
"num_success_latest": "Succeeding",
|
||||||
"num_failure_latest": "Failing",
|
"num_failure_latest": "Failing",
|
||||||
"bytes_added_30": "Bytes Added"
|
"bytes_added_30": "Bytes Added"
|
||||||
},
|
},
|
||||||
"yourspotify": {
|
"yourspotify": {
|
||||||
"songs": "Songs",
|
"songs": "שירים",
|
||||||
"time": "Time",
|
"time": "זמן",
|
||||||
"artists": "Artists"
|
"artists": "אומנים"
|
||||||
},
|
},
|
||||||
"arcane": {
|
"arcane": {
|
||||||
"containers": "Containers",
|
"containers": "Containers",
|
||||||
"images": "Images",
|
"images": "Images",
|
||||||
"image_updates": "Image Updates",
|
"image_updates": "Image Updates",
|
||||||
"images_unused": "Unused",
|
"images_unused": "לא בשימוש",
|
||||||
"environment_required": "Environment ID Required"
|
"environment_required": "Environment ID Required"
|
||||||
},
|
},
|
||||||
"dockhand": {
|
"dockhand": {
|
||||||
@@ -1227,7 +1235,7 @@
|
|||||||
"memory": "Memory",
|
"memory": "Memory",
|
||||||
"images": "Images",
|
"images": "Images",
|
||||||
"volumes": "Volumes",
|
"volumes": "Volumes",
|
||||||
"events_today": "Events Today",
|
"events_today": "אירועים היום",
|
||||||
"pending_updates": "Pending Updates",
|
"pending_updates": "Pending Updates",
|
||||||
"stacks": "Stacks",
|
"stacks": "Stacks",
|
||||||
"paused": "Paused",
|
"paused": "Paused",
|
||||||
@@ -1235,16 +1243,16 @@
|
|||||||
"environment_not_found": "Environment Not Found"
|
"environment_not_found": "Environment Not Found"
|
||||||
},
|
},
|
||||||
"sparkyfitness": {
|
"sparkyfitness": {
|
||||||
"eaten": "Eaten",
|
"eaten": "נאכלו",
|
||||||
"burned": "Burned",
|
"burned": "נשרפו",
|
||||||
"remaining": "Remaining",
|
"remaining": "נותרו",
|
||||||
"steps": "Steps"
|
"steps": "צעדים"
|
||||||
},
|
},
|
||||||
"syncthing": {
|
"syncthing": {
|
||||||
"connected": "Connected",
|
"connected": "Connected",
|
||||||
"synced": "Synced",
|
"synced": "מסונכרן",
|
||||||
"errors": "Errors",
|
"errors": "שגיאות",
|
||||||
"storage": "Storage"
|
"storage": "אחסון"
|
||||||
},
|
},
|
||||||
"sportarr": {
|
"sportarr": {
|
||||||
"wanted": "Wanted",
|
"wanted": "Wanted",
|
||||||
|
|||||||
@@ -39,7 +39,9 @@
|
|||||||
"wait": "Please wait"
|
"wait": "Please wait"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"placeholder": "Search…"
|
"placeholder": "Search…",
|
||||||
|
"provider": "Search engine: {{name}}",
|
||||||
|
"select_provider": "Select search engine"
|
||||||
},
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
"cpu": "CPU",
|
"cpu": "CPU",
|
||||||
@@ -97,7 +99,12 @@
|
|||||||
"unhealthy": "Unhealthy",
|
"unhealthy": "Unhealthy",
|
||||||
"not_found": "Not Found",
|
"not_found": "Not Found",
|
||||||
"exited": "Exited",
|
"exited": "Exited",
|
||||||
"partial": "Partial"
|
"partial": "Partial",
|
||||||
|
"created": "Created",
|
||||||
|
"paused": "Paused",
|
||||||
|
"restarting": "Restarting",
|
||||||
|
"removing": "Removing",
|
||||||
|
"dead": "Dead"
|
||||||
},
|
},
|
||||||
"ping": {
|
"ping": {
|
||||||
"error": "Error",
|
"error": "Error",
|
||||||
@@ -471,7 +478,8 @@
|
|||||||
"authentik": {
|
"authentik": {
|
||||||
"users": "Users",
|
"users": "Users",
|
||||||
"loginsLast24H": "Logins (24h)",
|
"loginsLast24H": "Logins (24h)",
|
||||||
"failedLoginsLast24H": "Failed Logins (24h)"
|
"failedLoginsLast24H": "Failed Logins (24h)",
|
||||||
|
"authorizationsLast24H": "Auths (24h)"
|
||||||
},
|
},
|
||||||
"proxmox": {
|
"proxmox": {
|
||||||
"mem": "MEM",
|
"mem": "MEM",
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
"seconds": "s"
|
"seconds": "s"
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"signout": "Sign out"
|
"signout": "Odjavi se"
|
||||||
},
|
},
|
||||||
"widget": {
|
"widget": {
|
||||||
"missing_type": "Nedostajuća vrsta widgeta: {{type}}",
|
"missing_type": "Nedostajuća vrsta widgeta: {{type}}",
|
||||||
@@ -39,7 +39,9 @@
|
|||||||
"wait": "Pričekaj"
|
"wait": "Pričekaj"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"placeholder": "Traži …"
|
"placeholder": "Traži …",
|
||||||
|
"provider": "Tražilica: {{name}}",
|
||||||
|
"select_provider": "Odaberi tražilicu"
|
||||||
},
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
"cpu": "CPU",
|
"cpu": "CPU",
|
||||||
@@ -97,7 +99,12 @@
|
|||||||
"unhealthy": "Nefunkcionalno",
|
"unhealthy": "Nefunkcionalno",
|
||||||
"not_found": "Nepronađeno",
|
"not_found": "Nepronađeno",
|
||||||
"exited": "Zatoreno",
|
"exited": "Zatoreno",
|
||||||
"partial": "Djelomično"
|
"partial": "Djelomično",
|
||||||
|
"created": "Created",
|
||||||
|
"paused": "Paused",
|
||||||
|
"restarting": "Restarting",
|
||||||
|
"removing": "Removing",
|
||||||
|
"dead": "Dead"
|
||||||
},
|
},
|
||||||
"ping": {
|
"ping": {
|
||||||
"error": "Greška",
|
"error": "Greška",
|
||||||
@@ -133,7 +140,7 @@
|
|||||||
"series": "Serije",
|
"series": "Serije",
|
||||||
"episodes": "Epizode",
|
"episodes": "Epizode",
|
||||||
"songs": "Pjesme",
|
"songs": "Pjesme",
|
||||||
"albums": "Albums"
|
"albums": "Albumi"
|
||||||
},
|
},
|
||||||
"esphome": {
|
"esphome": {
|
||||||
"offline": "Offline",
|
"offline": "Offline",
|
||||||
@@ -471,7 +478,8 @@
|
|||||||
"authentik": {
|
"authentik": {
|
||||||
"users": "Korisnici",
|
"users": "Korisnici",
|
||||||
"loginsLast24H": "Prijave (24 h)",
|
"loginsLast24H": "Prijave (24 h)",
|
||||||
"failedLoginsLast24H": "Neuspjele prijave (24 h)"
|
"failedLoginsLast24H": "Neuspjele prijave (24 h)",
|
||||||
|
"authorizationsLast24H": "Autorizacije (24h)"
|
||||||
},
|
},
|
||||||
"proxmox": {
|
"proxmox": {
|
||||||
"mem": "MEM",
|
"mem": "MEM",
|
||||||
@@ -951,15 +959,15 @@
|
|||||||
"storage": "Spremište"
|
"storage": "Spremište"
|
||||||
},
|
},
|
||||||
"maintainerr": {
|
"maintainerr": {
|
||||||
"itemsHandled": "Items Handled",
|
"itemsHandled": "Obrađene stavke",
|
||||||
"episodesHandled": "Episodes Handled",
|
"episodesHandled": "Obrađene epizode",
|
||||||
"moviesHandled": "Movies Handled",
|
"moviesHandled": "Obrađeni filmovi",
|
||||||
"showsHandled": "Shows Handled",
|
"showsHandled": "Obrađene emisije",
|
||||||
"seasonsHandled": "Seasons Handled",
|
"seasonsHandled": "Obrađene sezone",
|
||||||
"reclaimable": "Reclaimable",
|
"reclaimable": "Reclaimable",
|
||||||
"movieReclaimable": "Movie Reclaimable",
|
"movieReclaimable": "Movie Reclaimable",
|
||||||
"showReclaimable": "Show Reclaimable",
|
"showReclaimable": "Show Reclaimable",
|
||||||
"totalCapacity": "Total Capacity"
|
"totalCapacity": "Ukupni kapacitet"
|
||||||
},
|
},
|
||||||
"netdata": {
|
"netdata": {
|
||||||
"warnings": "Upozorenja",
|
"warnings": "Upozorenja",
|
||||||
@@ -1247,8 +1255,8 @@
|
|||||||
"storage": "Spremište"
|
"storage": "Spremište"
|
||||||
},
|
},
|
||||||
"sportarr": {
|
"sportarr": {
|
||||||
"wanted": "Wanted",
|
"wanted": "Željeno",
|
||||||
"queued": "Queued",
|
"queued": "U redu čekanja",
|
||||||
"leagues": "Leagues"
|
"leagues": "Lige"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,9 @@
|
|||||||
"wait": "Kérjük várjon"
|
"wait": "Kérjük várjon"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"placeholder": "Keresés…"
|
"placeholder": "Keresés…",
|
||||||
|
"provider": "Search engine: {{name}}",
|
||||||
|
"select_provider": "Select search engine"
|
||||||
},
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
"cpu": "Processzor",
|
"cpu": "Processzor",
|
||||||
@@ -97,7 +99,12 @@
|
|||||||
"unhealthy": "Egészségtelen",
|
"unhealthy": "Egészségtelen",
|
||||||
"not_found": "Nem található",
|
"not_found": "Nem található",
|
||||||
"exited": "Kilépett",
|
"exited": "Kilépett",
|
||||||
"partial": "Részleges"
|
"partial": "Részleges",
|
||||||
|
"created": "Created",
|
||||||
|
"paused": "Paused",
|
||||||
|
"restarting": "Restarting",
|
||||||
|
"removing": "Removing",
|
||||||
|
"dead": "Dead"
|
||||||
},
|
},
|
||||||
"ping": {
|
"ping": {
|
||||||
"error": "Hiba",
|
"error": "Hiba",
|
||||||
@@ -471,7 +478,8 @@
|
|||||||
"authentik": {
|
"authentik": {
|
||||||
"users": "Felhasználók",
|
"users": "Felhasználók",
|
||||||
"loginsLast24H": "Bejelentkezések (24 óra)",
|
"loginsLast24H": "Bejelentkezések (24 óra)",
|
||||||
"failedLoginsLast24H": "Sikertelen bejelentkezések (24h)"
|
"failedLoginsLast24H": "Sikertelen bejelentkezések (24h)",
|
||||||
|
"authorizationsLast24H": "Auths (24h)"
|
||||||
},
|
},
|
||||||
"proxmox": {
|
"proxmox": {
|
||||||
"mem": "MEM",
|
"mem": "MEM",
|
||||||
|
|||||||
@@ -39,7 +39,9 @@
|
|||||||
"wait": "Harap tunggu"
|
"wait": "Harap tunggu"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"placeholder": "Telusuri…"
|
"placeholder": "Telusuri…",
|
||||||
|
"provider": "Search engine: {{name}}",
|
||||||
|
"select_provider": "Select search engine"
|
||||||
},
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
"cpu": "CPU",
|
"cpu": "CPU",
|
||||||
@@ -97,7 +99,12 @@
|
|||||||
"unhealthy": "Tidak Lancar",
|
"unhealthy": "Tidak Lancar",
|
||||||
"not_found": "Tidak Ditemukan",
|
"not_found": "Tidak Ditemukan",
|
||||||
"exited": "Terkeluar",
|
"exited": "Terkeluar",
|
||||||
"partial": "Sebagian"
|
"partial": "Sebagian",
|
||||||
|
"created": "Created",
|
||||||
|
"paused": "Paused",
|
||||||
|
"restarting": "Restarting",
|
||||||
|
"removing": "Removing",
|
||||||
|
"dead": "Dead"
|
||||||
},
|
},
|
||||||
"ping": {
|
"ping": {
|
||||||
"error": "Error",
|
"error": "Error",
|
||||||
@@ -471,7 +478,8 @@
|
|||||||
"authentik": {
|
"authentik": {
|
||||||
"users": "Users",
|
"users": "Users",
|
||||||
"loginsLast24H": "Login (24j)",
|
"loginsLast24H": "Login (24j)",
|
||||||
"failedLoginsLast24H": "Login Gagal (24j)"
|
"failedLoginsLast24H": "Login Gagal (24j)",
|
||||||
|
"authorizationsLast24H": "Auths (24h)"
|
||||||
},
|
},
|
||||||
"proxmox": {
|
"proxmox": {
|
||||||
"mem": "MEM",
|
"mem": "MEM",
|
||||||
|
|||||||
@@ -39,7 +39,9 @@
|
|||||||
"wait": "Attendi per favore"
|
"wait": "Attendi per favore"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"placeholder": "Cerca…"
|
"placeholder": "Cerca…",
|
||||||
|
"provider": "Search engine: {{name}}",
|
||||||
|
"select_provider": "Select search engine"
|
||||||
},
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
"cpu": "CPU",
|
"cpu": "CPU",
|
||||||
@@ -97,7 +99,12 @@
|
|||||||
"unhealthy": "Non sano",
|
"unhealthy": "Non sano",
|
||||||
"not_found": "Non trovato",
|
"not_found": "Non trovato",
|
||||||
"exited": "Uscito",
|
"exited": "Uscito",
|
||||||
"partial": "Parziale"
|
"partial": "Parziale",
|
||||||
|
"created": "Created",
|
||||||
|
"paused": "Paused",
|
||||||
|
"restarting": "Restarting",
|
||||||
|
"removing": "Removing",
|
||||||
|
"dead": "Dead"
|
||||||
},
|
},
|
||||||
"ping": {
|
"ping": {
|
||||||
"error": "Errore",
|
"error": "Errore",
|
||||||
@@ -471,7 +478,8 @@
|
|||||||
"authentik": {
|
"authentik": {
|
||||||
"users": "Utenti",
|
"users": "Utenti",
|
||||||
"loginsLast24H": "Accessi (24h)",
|
"loginsLast24H": "Accessi (24h)",
|
||||||
"failedLoginsLast24H": "Accessi Falliti (24h)"
|
"failedLoginsLast24H": "Accessi Falliti (24h)",
|
||||||
|
"authorizationsLast24H": "Auths (24h)"
|
||||||
},
|
},
|
||||||
"proxmox": {
|
"proxmox": {
|
||||||
"mem": "MEM",
|
"mem": "MEM",
|
||||||
|
|||||||
@@ -39,7 +39,9 @@
|
|||||||
"wait": "お待ちください"
|
"wait": "お待ちください"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"placeholder": "検索…"
|
"placeholder": "検索…",
|
||||||
|
"provider": "Search engine: {{name}}",
|
||||||
|
"select_provider": "Select search engine"
|
||||||
},
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
"cpu": "CPU",
|
"cpu": "CPU",
|
||||||
@@ -97,7 +99,12 @@
|
|||||||
"unhealthy": "非健全",
|
"unhealthy": "非健全",
|
||||||
"not_found": "不明",
|
"not_found": "不明",
|
||||||
"exited": "停止しました",
|
"exited": "停止しました",
|
||||||
"partial": "部分的"
|
"partial": "部分的",
|
||||||
|
"created": "Created",
|
||||||
|
"paused": "Paused",
|
||||||
|
"restarting": "Restarting",
|
||||||
|
"removing": "Removing",
|
||||||
|
"dead": "Dead"
|
||||||
},
|
},
|
||||||
"ping": {
|
"ping": {
|
||||||
"error": "エラー",
|
"error": "エラー",
|
||||||
@@ -471,7 +478,8 @@
|
|||||||
"authentik": {
|
"authentik": {
|
||||||
"users": "Users",
|
"users": "Users",
|
||||||
"loginsLast24H": "ログイン (24時間)",
|
"loginsLast24H": "ログイン (24時間)",
|
||||||
"failedLoginsLast24H": "ログイン失敗(24時間)"
|
"failedLoginsLast24H": "ログイン失敗(24時間)",
|
||||||
|
"authorizationsLast24H": "Auths (24h)"
|
||||||
},
|
},
|
||||||
"proxmox": {
|
"proxmox": {
|
||||||
"mem": "MEM",
|
"mem": "MEM",
|
||||||
|
|||||||
@@ -39,7 +39,9 @@
|
|||||||
"wait": "잠시만 기다리세요"
|
"wait": "잠시만 기다리세요"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"placeholder": "검색…"
|
"placeholder": "검색…",
|
||||||
|
"provider": "검색 엔진: {{name}}",
|
||||||
|
"select_provider": "검색 엔진 선택"
|
||||||
},
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
"cpu": "CPU",
|
"cpu": "CPU",
|
||||||
@@ -97,7 +99,12 @@
|
|||||||
"unhealthy": "비정상",
|
"unhealthy": "비정상",
|
||||||
"not_found": "찾을 수 없음",
|
"not_found": "찾을 수 없음",
|
||||||
"exited": "종료됨",
|
"exited": "종료됨",
|
||||||
"partial": "부분적"
|
"partial": "부분적",
|
||||||
|
"created": "Created",
|
||||||
|
"paused": "Paused",
|
||||||
|
"restarting": "Restarting",
|
||||||
|
"removing": "Removing",
|
||||||
|
"dead": "Dead"
|
||||||
},
|
},
|
||||||
"ping": {
|
"ping": {
|
||||||
"error": "오류",
|
"error": "오류",
|
||||||
@@ -471,7 +478,8 @@
|
|||||||
"authentik": {
|
"authentik": {
|
||||||
"users": "사용자",
|
"users": "사용자",
|
||||||
"loginsLast24H": "로그인 (24시간)",
|
"loginsLast24H": "로그인 (24시간)",
|
||||||
"failedLoginsLast24H": "실패한 로그인 (24시간)"
|
"failedLoginsLast24H": "실패한 로그인 (24시간)",
|
||||||
|
"authorizationsLast24H": "인증(24시간)"
|
||||||
},
|
},
|
||||||
"proxmox": {
|
"proxmox": {
|
||||||
"mem": "메모리",
|
"mem": "메모리",
|
||||||
|
|||||||
@@ -39,7 +39,9 @@
|
|||||||
"wait": "Lūdzu, uzgaidiet"
|
"wait": "Lūdzu, uzgaidiet"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"placeholder": "Meklēt…"
|
"placeholder": "Meklēt…",
|
||||||
|
"provider": "Search engine: {{name}}",
|
||||||
|
"select_provider": "Select search engine"
|
||||||
},
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
"cpu": "CPU",
|
"cpu": "CPU",
|
||||||
@@ -97,7 +99,12 @@
|
|||||||
"unhealthy": "Unhealthy",
|
"unhealthy": "Unhealthy",
|
||||||
"not_found": "Not Found",
|
"not_found": "Not Found",
|
||||||
"exited": "Exited",
|
"exited": "Exited",
|
||||||
"partial": "Partial"
|
"partial": "Partial",
|
||||||
|
"created": "Created",
|
||||||
|
"paused": "Paused",
|
||||||
|
"restarting": "Restarting",
|
||||||
|
"removing": "Removing",
|
||||||
|
"dead": "Dead"
|
||||||
},
|
},
|
||||||
"ping": {
|
"ping": {
|
||||||
"error": "Error",
|
"error": "Error",
|
||||||
@@ -471,7 +478,8 @@
|
|||||||
"authentik": {
|
"authentik": {
|
||||||
"users": "Users",
|
"users": "Users",
|
||||||
"loginsLast24H": "Logins (24h)",
|
"loginsLast24H": "Logins (24h)",
|
||||||
"failedLoginsLast24H": "Failed Logins (24h)"
|
"failedLoginsLast24H": "Failed Logins (24h)",
|
||||||
|
"authorizationsLast24H": "Auths (24h)"
|
||||||
},
|
},
|
||||||
"proxmox": {
|
"proxmox": {
|
||||||
"mem": "MEM",
|
"mem": "MEM",
|
||||||
|
|||||||
@@ -39,7 +39,9 @@
|
|||||||
"wait": "Sila tunggu"
|
"wait": "Sila tunggu"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"placeholder": "Carian…"
|
"placeholder": "Carian…",
|
||||||
|
"provider": "Search engine: {{name}}",
|
||||||
|
"select_provider": "Select search engine"
|
||||||
},
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
"cpu": "CPU",
|
"cpu": "CPU",
|
||||||
@@ -97,7 +99,12 @@
|
|||||||
"unhealthy": "Kurang sihat",
|
"unhealthy": "Kurang sihat",
|
||||||
"not_found": "Tidak dijumpai",
|
"not_found": "Tidak dijumpai",
|
||||||
"exited": "Dimatikan",
|
"exited": "Dimatikan",
|
||||||
"partial": "Sebahagian"
|
"partial": "Sebahagian",
|
||||||
|
"created": "Created",
|
||||||
|
"paused": "Paused",
|
||||||
|
"restarting": "Restarting",
|
||||||
|
"removing": "Removing",
|
||||||
|
"dead": "Dead"
|
||||||
},
|
},
|
||||||
"ping": {
|
"ping": {
|
||||||
"error": "Error",
|
"error": "Error",
|
||||||
@@ -471,7 +478,8 @@
|
|||||||
"authentik": {
|
"authentik": {
|
||||||
"users": "Users",
|
"users": "Users",
|
||||||
"loginsLast24H": "Logmasuk (24j)",
|
"loginsLast24H": "Logmasuk (24j)",
|
||||||
"failedLoginsLast24H": "Logmasuk Gagal (24j)"
|
"failedLoginsLast24H": "Logmasuk Gagal (24j)",
|
||||||
|
"authorizationsLast24H": "Auths (24h)"
|
||||||
},
|
},
|
||||||
"proxmox": {
|
"proxmox": {
|
||||||
"mem": "MEM",
|
"mem": "MEM",
|
||||||
|
|||||||
@@ -39,7 +39,9 @@
|
|||||||
"wait": "Even geduld"
|
"wait": "Even geduld"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"placeholder": "Zoeken…"
|
"placeholder": "Zoeken…",
|
||||||
|
"provider": "Search engine: {{name}}",
|
||||||
|
"select_provider": "Select search engine"
|
||||||
},
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
"cpu": "CPU",
|
"cpu": "CPU",
|
||||||
@@ -97,7 +99,12 @@
|
|||||||
"unhealthy": "Ongezond",
|
"unhealthy": "Ongezond",
|
||||||
"not_found": "Niet Gevonden",
|
"not_found": "Niet Gevonden",
|
||||||
"exited": "Gestopt",
|
"exited": "Gestopt",
|
||||||
"partial": "Gedeeltelijk"
|
"partial": "Gedeeltelijk",
|
||||||
|
"created": "Created",
|
||||||
|
"paused": "Paused",
|
||||||
|
"restarting": "Restarting",
|
||||||
|
"removing": "Removing",
|
||||||
|
"dead": "Dead"
|
||||||
},
|
},
|
||||||
"ping": {
|
"ping": {
|
||||||
"error": "Fout",
|
"error": "Fout",
|
||||||
@@ -471,7 +478,8 @@
|
|||||||
"authentik": {
|
"authentik": {
|
||||||
"users": "Gebruikers",
|
"users": "Gebruikers",
|
||||||
"loginsLast24H": "Logins (24u)",
|
"loginsLast24H": "Logins (24u)",
|
||||||
"failedLoginsLast24H": "Mislukte Logins (24u)"
|
"failedLoginsLast24H": "Mislukte Logins (24u)",
|
||||||
|
"authorizationsLast24H": "Auths (24h)"
|
||||||
},
|
},
|
||||||
"proxmox": {
|
"proxmox": {
|
||||||
"mem": "GEH",
|
"mem": "GEH",
|
||||||
|
|||||||
@@ -39,7 +39,9 @@
|
|||||||
"wait": "Vennligst vent"
|
"wait": "Vennligst vent"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"placeholder": "Søk…"
|
"placeholder": "Søk…",
|
||||||
|
"provider": "Search engine: {{name}}",
|
||||||
|
"select_provider": "Select search engine"
|
||||||
},
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
"cpu": "CPU",
|
"cpu": "CPU",
|
||||||
@@ -97,7 +99,12 @@
|
|||||||
"unhealthy": "Usunn",
|
"unhealthy": "Usunn",
|
||||||
"not_found": "Not Found",
|
"not_found": "Not Found",
|
||||||
"exited": "Exited",
|
"exited": "Exited",
|
||||||
"partial": "Delvis"
|
"partial": "Delvis",
|
||||||
|
"created": "Created",
|
||||||
|
"paused": "Paused",
|
||||||
|
"restarting": "Restarting",
|
||||||
|
"removing": "Removing",
|
||||||
|
"dead": "Dead"
|
||||||
},
|
},
|
||||||
"ping": {
|
"ping": {
|
||||||
"error": "Error",
|
"error": "Error",
|
||||||
@@ -471,7 +478,8 @@
|
|||||||
"authentik": {
|
"authentik": {
|
||||||
"users": "Users",
|
"users": "Users",
|
||||||
"loginsLast24H": "Logins (24h)",
|
"loginsLast24H": "Logins (24h)",
|
||||||
"failedLoginsLast24H": "Mislykket innlogginger (24t)"
|
"failedLoginsLast24H": "Mislykket innlogginger (24t)",
|
||||||
|
"authorizationsLast24H": "Auths (24h)"
|
||||||
},
|
},
|
||||||
"proxmox": {
|
"proxmox": {
|
||||||
"mem": "MEM",
|
"mem": "MEM",
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
"seconds": "s"
|
"seconds": "s"
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"signout": "Sign out"
|
"signout": "Wyloguj się"
|
||||||
},
|
},
|
||||||
"widget": {
|
"widget": {
|
||||||
"missing_type": "Brakujący typ widżetu: {{type}}",
|
"missing_type": "Brakujący typ widżetu: {{type}}",
|
||||||
@@ -39,7 +39,9 @@
|
|||||||
"wait": "Proszę czekać"
|
"wait": "Proszę czekać"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"placeholder": "Szukaj…"
|
"placeholder": "Szukaj…",
|
||||||
|
"provider": "Wyszukiwarka: {{name}}",
|
||||||
|
"select_provider": "Wybierz wyszukiwarkę"
|
||||||
},
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
"cpu": "Procesor",
|
"cpu": "Procesor",
|
||||||
@@ -97,7 +99,12 @@
|
|||||||
"unhealthy": "Niezdrowy",
|
"unhealthy": "Niezdrowy",
|
||||||
"not_found": "Nie znaleziono",
|
"not_found": "Nie znaleziono",
|
||||||
"exited": "Zakończony",
|
"exited": "Zakończony",
|
||||||
"partial": "Częściowy"
|
"partial": "Częściowy",
|
||||||
|
"created": "Created",
|
||||||
|
"paused": "Paused",
|
||||||
|
"restarting": "Restarting",
|
||||||
|
"removing": "Removing",
|
||||||
|
"dead": "Dead"
|
||||||
},
|
},
|
||||||
"ping": {
|
"ping": {
|
||||||
"error": "Błąd",
|
"error": "Błąd",
|
||||||
@@ -133,7 +140,7 @@
|
|||||||
"series": "Seriale",
|
"series": "Seriale",
|
||||||
"episodes": "Odcinki",
|
"episodes": "Odcinki",
|
||||||
"songs": "Piosenki",
|
"songs": "Piosenki",
|
||||||
"albums": "Albums"
|
"albums": "Albumy"
|
||||||
},
|
},
|
||||||
"esphome": {
|
"esphome": {
|
||||||
"offline": "Offline",
|
"offline": "Offline",
|
||||||
@@ -471,7 +478,8 @@
|
|||||||
"authentik": {
|
"authentik": {
|
||||||
"users": "Użytkownicy",
|
"users": "Użytkownicy",
|
||||||
"loginsLast24H": "Logowania (24h)",
|
"loginsLast24H": "Logowania (24h)",
|
||||||
"failedLoginsLast24H": "Nieudane logowania (24h)"
|
"failedLoginsLast24H": "Nieudane logowania (24h)",
|
||||||
|
"authorizationsLast24H": "Uwierzytelnienia (24h)"
|
||||||
},
|
},
|
||||||
"proxmox": {
|
"proxmox": {
|
||||||
"mem": "RAM",
|
"mem": "RAM",
|
||||||
|
|||||||
+437
-429
File diff suppressed because it is too large
Load Diff
@@ -39,7 +39,9 @@
|
|||||||
"wait": "Por favor, aguarde"
|
"wait": "Por favor, aguarde"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"placeholder": "Pesquisar…"
|
"placeholder": "Pesquisar…",
|
||||||
|
"provider": "Search engine: {{name}}",
|
||||||
|
"select_provider": "Select search engine"
|
||||||
},
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
"cpu": "CPU",
|
"cpu": "CPU",
|
||||||
@@ -97,7 +99,12 @@
|
|||||||
"unhealthy": "Não-saudável",
|
"unhealthy": "Não-saudável",
|
||||||
"not_found": "Não Encontrado",
|
"not_found": "Não Encontrado",
|
||||||
"exited": "Encerrado",
|
"exited": "Encerrado",
|
||||||
"partial": "Parcial"
|
"partial": "Parcial",
|
||||||
|
"created": "Created",
|
||||||
|
"paused": "Paused",
|
||||||
|
"restarting": "Restarting",
|
||||||
|
"removing": "Removing",
|
||||||
|
"dead": "Dead"
|
||||||
},
|
},
|
||||||
"ping": {
|
"ping": {
|
||||||
"error": "Erro",
|
"error": "Erro",
|
||||||
@@ -471,7 +478,8 @@
|
|||||||
"authentik": {
|
"authentik": {
|
||||||
"users": "Usuários",
|
"users": "Usuários",
|
||||||
"loginsLast24H": "Inícios de sessão (24h)",
|
"loginsLast24H": "Inícios de sessão (24h)",
|
||||||
"failedLoginsLast24H": "Inícios de sessão falhados (24h)"
|
"failedLoginsLast24H": "Inícios de sessão falhados (24h)",
|
||||||
|
"authorizationsLast24H": "Auths (24h)"
|
||||||
},
|
},
|
||||||
"proxmox": {
|
"proxmox": {
|
||||||
"mem": "MEM",
|
"mem": "MEM",
|
||||||
|
|||||||
@@ -39,7 +39,9 @@
|
|||||||
"wait": "Va rugăm așteptați"
|
"wait": "Va rugăm așteptați"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"placeholder": "Caută…"
|
"placeholder": "Caută…",
|
||||||
|
"provider": "Search engine: {{name}}",
|
||||||
|
"select_provider": "Select search engine"
|
||||||
},
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
"cpu": "Procesor",
|
"cpu": "Procesor",
|
||||||
@@ -97,7 +99,12 @@
|
|||||||
"unhealthy": "Nesănătos",
|
"unhealthy": "Nesănătos",
|
||||||
"not_found": "Negăsit",
|
"not_found": "Negăsit",
|
||||||
"exited": "Ieşit",
|
"exited": "Ieşit",
|
||||||
"partial": "Parțial"
|
"partial": "Parțial",
|
||||||
|
"created": "Created",
|
||||||
|
"paused": "Paused",
|
||||||
|
"restarting": "Restarting",
|
||||||
|
"removing": "Removing",
|
||||||
|
"dead": "Dead"
|
||||||
},
|
},
|
||||||
"ping": {
|
"ping": {
|
||||||
"error": "Error",
|
"error": "Error",
|
||||||
@@ -471,7 +478,8 @@
|
|||||||
"authentik": {
|
"authentik": {
|
||||||
"users": "Users",
|
"users": "Users",
|
||||||
"loginsLast24H": "Autentificări (24h)",
|
"loginsLast24H": "Autentificări (24h)",
|
||||||
"failedLoginsLast24H": "Conectări eșuate (24h)"
|
"failedLoginsLast24H": "Conectări eșuate (24h)",
|
||||||
|
"authorizationsLast24H": "Auths (24h)"
|
||||||
},
|
},
|
||||||
"proxmox": {
|
"proxmox": {
|
||||||
"mem": "MEM",
|
"mem": "MEM",
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
"seconds": "сек"
|
"seconds": "сек"
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"signout": "Sign out"
|
"signout": "Выход"
|
||||||
},
|
},
|
||||||
"widget": {
|
"widget": {
|
||||||
"missing_type": "Отсутствует тип виджета: {{type}}",
|
"missing_type": "Отсутствует тип виджета: {{type}}",
|
||||||
@@ -39,7 +39,9 @@
|
|||||||
"wait": "Пожалуйста, подождите"
|
"wait": "Пожалуйста, подождите"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"placeholder": "Поиск…"
|
"placeholder": "Поиск…",
|
||||||
|
"provider": "Поисковая система: {{name}}",
|
||||||
|
"select_provider": "Выберите поисковую систему"
|
||||||
},
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
"cpu": "ЦП",
|
"cpu": "ЦП",
|
||||||
@@ -75,13 +77,13 @@
|
|||||||
"no_data": "Нет доступных данных о хранилище"
|
"no_data": "Нет доступных данных о хранилище"
|
||||||
},
|
},
|
||||||
"duplicati": {
|
"duplicati": {
|
||||||
"jobs": "Jobs",
|
"jobs": "Работы",
|
||||||
"stored": "Stored",
|
"stored": "Сохранено",
|
||||||
"lastBackup": "Last Run",
|
"lastBackup": "Последний запуск",
|
||||||
"nextRun": "Next Run",
|
"nextRun": "Следующий запуск",
|
||||||
"running": "Running",
|
"running": "Запущено",
|
||||||
"warnings": "Warnings",
|
"warnings": "Предупреждения",
|
||||||
"errors": "Errors"
|
"errors": "Ошибки"
|
||||||
},
|
},
|
||||||
"docker": {
|
"docker": {
|
||||||
"rx": "RX",
|
"rx": "RX",
|
||||||
@@ -97,7 +99,12 @@
|
|||||||
"unhealthy": "Нездоровый",
|
"unhealthy": "Нездоровый",
|
||||||
"not_found": "Не найдено",
|
"not_found": "Не найдено",
|
||||||
"exited": "Вышел",
|
"exited": "Вышел",
|
||||||
"partial": "Частичный"
|
"partial": "Частичный",
|
||||||
|
"created": "Создан",
|
||||||
|
"paused": "Приостановлен",
|
||||||
|
"restarting": "Перезапуск",
|
||||||
|
"removing": "Удаление",
|
||||||
|
"dead": "Мёртв"
|
||||||
},
|
},
|
||||||
"ping": {
|
"ping": {
|
||||||
"error": "Ошибка",
|
"error": "Ошибка",
|
||||||
@@ -133,7 +140,7 @@
|
|||||||
"series": "Сериалы",
|
"series": "Сериалы",
|
||||||
"episodes": "Эпизоды",
|
"episodes": "Эпизоды",
|
||||||
"songs": "Песни",
|
"songs": "Песни",
|
||||||
"albums": "Albums"
|
"albums": "Альбомы"
|
||||||
},
|
},
|
||||||
"esphome": {
|
"esphome": {
|
||||||
"offline": "Не в сети",
|
"offline": "Не в сети",
|
||||||
@@ -235,17 +242,17 @@
|
|||||||
"rutorrent": {
|
"rutorrent": {
|
||||||
"active": "Активно",
|
"active": "Активно",
|
||||||
"upload": "Отдача",
|
"upload": "Отдача",
|
||||||
"download": "Скачивание"
|
"download": "Загрузка"
|
||||||
},
|
},
|
||||||
"transmission": {
|
"transmission": {
|
||||||
"download": "Скачивание",
|
"download": "Загрузка",
|
||||||
"upload": "Отдача",
|
"upload": "Отдача",
|
||||||
"leech": "Лич",
|
"leech": "Лич",
|
||||||
"seed": "Сид"
|
"seed": "Сид"
|
||||||
},
|
},
|
||||||
"qbittorrent": {
|
"qbittorrent": {
|
||||||
"download": "Скачивание",
|
"download": "Загрузка",
|
||||||
"upload": "Загрузка",
|
"upload": "Отдача",
|
||||||
"leech": "Лич",
|
"leech": "Лич",
|
||||||
"seed": "Сид"
|
"seed": "Сид"
|
||||||
},
|
},
|
||||||
@@ -258,8 +265,8 @@
|
|||||||
"invalid": "Некорректный"
|
"invalid": "Некорректный"
|
||||||
},
|
},
|
||||||
"deluge": {
|
"deluge": {
|
||||||
"download": "Скачивание",
|
"download": "Загрузка",
|
||||||
"upload": "Загрузка",
|
"upload": "Отдача",
|
||||||
"leech": "Лич",
|
"leech": "Лич",
|
||||||
"seed": "Сид"
|
"seed": "Сид"
|
||||||
},
|
},
|
||||||
@@ -268,8 +275,8 @@
|
|||||||
"cachemissbytes": "Мисс байты кэша"
|
"cachemissbytes": "Мисс байты кэша"
|
||||||
},
|
},
|
||||||
"downloadstation": {
|
"downloadstation": {
|
||||||
"download": "Скачивание",
|
"download": "Загрузка",
|
||||||
"upload": "Загрузка",
|
"upload": "Отдача",
|
||||||
"leech": "Лич",
|
"leech": "Лич",
|
||||||
"seed": "Сид"
|
"seed": "Сид"
|
||||||
},
|
},
|
||||||
@@ -334,8 +341,8 @@
|
|||||||
"latency": "Задержка"
|
"latency": "Задержка"
|
||||||
},
|
},
|
||||||
"speedtest": {
|
"speedtest": {
|
||||||
"upload": "Загрузка",
|
"upload": "Отдача",
|
||||||
"download": "Скачивание",
|
"download": "Загрузка",
|
||||||
"ping": "Пинг"
|
"ping": "Пинг"
|
||||||
},
|
},
|
||||||
"portainer": {
|
"portainer": {
|
||||||
@@ -471,7 +478,8 @@
|
|||||||
"authentik": {
|
"authentik": {
|
||||||
"users": "Пользователи",
|
"users": "Пользователи",
|
||||||
"loginsLast24H": "Входы (24ч)",
|
"loginsLast24H": "Входы (24ч)",
|
||||||
"failedLoginsLast24H": "Неудачные входы (24ч)"
|
"failedLoginsLast24H": "Неудачные входы (24ч)",
|
||||||
|
"authorizationsLast24H": "Входы (24ч)"
|
||||||
},
|
},
|
||||||
"proxmox": {
|
"proxmox": {
|
||||||
"mem": "Память",
|
"mem": "Память",
|
||||||
@@ -480,9 +488,9 @@
|
|||||||
"vms": "Виртуальные машины"
|
"vms": "Виртуальные машины"
|
||||||
},
|
},
|
||||||
"pulse": {
|
"pulse": {
|
||||||
"nodes": "Nodes",
|
"nodes": "Узлы",
|
||||||
"vms": "VMs",
|
"vms": "Виртуальные машины",
|
||||||
"lxcs": "LXCs"
|
"lxcs": "Контейнеры LXC"
|
||||||
},
|
},
|
||||||
"glances": {
|
"glances": {
|
||||||
"cpu": "ЦП",
|
"cpu": "ЦП",
|
||||||
@@ -581,7 +589,7 @@
|
|||||||
"up": "В сети",
|
"up": "В сети",
|
||||||
"pending": "Ожидают",
|
"pending": "Ожидают",
|
||||||
"down": "Не в сети",
|
"down": "Не в сети",
|
||||||
"ok": "Ok"
|
"ok": "OK"
|
||||||
},
|
},
|
||||||
"healthchecks": {
|
"healthchecks": {
|
||||||
"new": "Новый",
|
"new": "Новый",
|
||||||
@@ -649,7 +657,7 @@
|
|||||||
"total": "Всего"
|
"total": "Всего"
|
||||||
},
|
},
|
||||||
"pangolin": {
|
"pangolin": {
|
||||||
"orgs": "Orgs",
|
"orgs": "Организации",
|
||||||
"sites": "Сайты",
|
"sites": "Сайты",
|
||||||
"resources": "Ресурсы",
|
"resources": "Ресурсы",
|
||||||
"targets": "Цели",
|
"targets": "Цели",
|
||||||
@@ -687,7 +695,7 @@
|
|||||||
"limit": "Лимит"
|
"limit": "Лимит"
|
||||||
},
|
},
|
||||||
"opnsense": {
|
"opnsense": {
|
||||||
"cpu": "ЦП",
|
"cpu": "Нагрузка ЦП",
|
||||||
"memory": "Активно ОЗУ",
|
"memory": "Активно ОЗУ",
|
||||||
"wanUpload": "WAN Загрузка",
|
"wanUpload": "WAN Загрузка",
|
||||||
"wanDownload": "WAN скачивание"
|
"wanDownload": "WAN скачивание"
|
||||||
@@ -822,7 +830,7 @@
|
|||||||
"gross_percent_today": "Сегодня",
|
"gross_percent_today": "Сегодня",
|
||||||
"gross_percent_1y": "Один год",
|
"gross_percent_1y": "Один год",
|
||||||
"gross_percent_max": "Все время",
|
"gross_percent_max": "Все время",
|
||||||
"net_worth": "Net Worth"
|
"net_worth": "Общая стоимость"
|
||||||
},
|
},
|
||||||
"audiobookshelf": {
|
"audiobookshelf": {
|
||||||
"podcasts": "Подкасты",
|
"podcasts": "Подкасты",
|
||||||
@@ -951,15 +959,15 @@
|
|||||||
"storage": "Хранилище"
|
"storage": "Хранилище"
|
||||||
},
|
},
|
||||||
"maintainerr": {
|
"maintainerr": {
|
||||||
"itemsHandled": "Items Handled",
|
"itemsHandled": "Элементы обрабатываются",
|
||||||
"episodesHandled": "Episodes Handled",
|
"episodesHandled": "Серии обрабатываются",
|
||||||
"moviesHandled": "Movies Handled",
|
"moviesHandled": "Фильмы обрабатываются",
|
||||||
"showsHandled": "Shows Handled",
|
"showsHandled": "Шоу обрабатываются",
|
||||||
"seasonsHandled": "Seasons Handled",
|
"seasonsHandled": "Сезоны обрабатываются",
|
||||||
"reclaimable": "Reclaimable",
|
"reclaimable": "Восстановленные",
|
||||||
"movieReclaimable": "Movie Reclaimable",
|
"movieReclaimable": "Восстановленный фильм",
|
||||||
"showReclaimable": "Show Reclaimable",
|
"showReclaimable": "Восстановленное шоу",
|
||||||
"totalCapacity": "Total Capacity"
|
"totalCapacity": "Общая ёмкость"
|
||||||
},
|
},
|
||||||
"netdata": {
|
"netdata": {
|
||||||
"warnings": "Предупреждения",
|
"warnings": "Предупреждения",
|
||||||
@@ -968,7 +976,7 @@
|
|||||||
"ntfy": {
|
"ntfy": {
|
||||||
"title": "Название",
|
"title": "Название",
|
||||||
"priority": "Приоритет",
|
"priority": "Приоритет",
|
||||||
"lastReceived": "Last Received",
|
"lastReceived": "Последние полученные",
|
||||||
"message": "Сообщение",
|
"message": "Сообщение",
|
||||||
"tags": "Теги",
|
"tags": "Теги",
|
||||||
"none": "Отсутствует",
|
"none": "Отсутствует",
|
||||||
@@ -1105,7 +1113,7 @@
|
|||||||
"apps": "Приложения",
|
"apps": "Приложения",
|
||||||
"synced": "Синхронизированные",
|
"synced": "Синхронизированные",
|
||||||
"outOfSync": "Не синхронизированные",
|
"outOfSync": "Не синхронизированные",
|
||||||
"healthy": "Healthy",
|
"healthy": "Здоров",
|
||||||
"degraded": "Деградированные",
|
"degraded": "Деградированные",
|
||||||
"progressing": "Выполняются",
|
"progressing": "Выполняются",
|
||||||
"missing": "Отсутствует",
|
"missing": "Отсутствует",
|
||||||
@@ -1152,18 +1160,18 @@
|
|||||||
"other": "Другое"
|
"other": "Другое"
|
||||||
},
|
},
|
||||||
"checkmk": {
|
"checkmk": {
|
||||||
"serviceErrors": "Service issues",
|
"serviceErrors": "Проблемы сервиса",
|
||||||
"hostErrors": "Host issues"
|
"hostErrors": "Проблемы узла"
|
||||||
},
|
},
|
||||||
"komodo": {
|
"komodo": {
|
||||||
"total": "Всего",
|
"total": "Всего",
|
||||||
"running": "Запущено",
|
"running": "Запущено",
|
||||||
"stopped": "Остановлено",
|
"stopped": "Остановлено",
|
||||||
"down": "Не в сети",
|
"down": "Не в сети",
|
||||||
"unhealthy": "Unhealthy",
|
"unhealthy": "Неисправен",
|
||||||
"unknown": "Неизвестно",
|
"unknown": "Неизвестно",
|
||||||
"servers": "Серверы",
|
"servers": "Серверы",
|
||||||
"stacks": "Stacks",
|
"stacks": "Стеки",
|
||||||
"containers": "Контейнеры"
|
"containers": "Контейнеры"
|
||||||
},
|
},
|
||||||
"filebrowser": {
|
"filebrowser": {
|
||||||
@@ -1186,7 +1194,7 @@
|
|||||||
"DISABLE_DISK": "Диск отключен",
|
"DISABLE_DISK": "Диск отключен",
|
||||||
"SWAP_DSBL": "Swap отключён",
|
"SWAP_DSBL": "Swap отключён",
|
||||||
"INVALID_EXPANSION": "Неверное Расширение",
|
"INVALID_EXPANSION": "Неверное Расширение",
|
||||||
"PARITY_NOT_BIGGEST": "Parity Not Biggest",
|
"PARITY_NOT_BIGGEST": "Четность не наибольшая",
|
||||||
"TOO_MANY_MISSING_DISKS": "Слишком много отсутствующих дисков",
|
"TOO_MANY_MISSING_DISKS": "Слишком много отсутствующих дисков",
|
||||||
"NEW_DISK_TOO_SMALL": "Новый диск слишком мал",
|
"NEW_DISK_TOO_SMALL": "Новый диск слишком мал",
|
||||||
"NO_DATA_DISKS": "Нет дисков данных",
|
"NO_DATA_DISKS": "Нет дисков данных",
|
||||||
@@ -1195,18 +1203,18 @@
|
|||||||
"cpu": "ЦП",
|
"cpu": "ЦП",
|
||||||
"memoryUsed": "Использовано ОЗУ",
|
"memoryUsed": "Использовано ОЗУ",
|
||||||
"memoryAvailable": "Доступная память",
|
"memoryAvailable": "Доступная память",
|
||||||
"arrayUsed": "Array Used",
|
"arrayUsed": "Используемый массив",
|
||||||
"arrayFree": "Array Free",
|
"arrayFree": "Свободный массив",
|
||||||
"poolUsed": "{{pool}} Использовано",
|
"poolUsed": "{{pool}} Использовано",
|
||||||
"poolFree": "{{pool}} Свободно"
|
"poolFree": "{{pool}} Свободно"
|
||||||
},
|
},
|
||||||
"backrest": {
|
"backrest": {
|
||||||
"num_plans": "Plans",
|
"num_plans": "Планы",
|
||||||
"num_success_30": "Successes",
|
"num_success_30": "Успешные",
|
||||||
"num_failure_30": "Ошибки",
|
"num_failure_30": "Ошибки",
|
||||||
"num_success_latest": "Succeeding",
|
"num_success_latest": "Успешно",
|
||||||
"num_failure_latest": "Failing",
|
"num_failure_latest": "Неудачные",
|
||||||
"bytes_added_30": "Bytes Added"
|
"bytes_added_30": "Байты добавлены"
|
||||||
},
|
},
|
||||||
"yourspotify": {
|
"yourspotify": {
|
||||||
"songs": "Треков",
|
"songs": "Треков",
|
||||||
@@ -1235,20 +1243,20 @@
|
|||||||
"environment_not_found": "Среда не найдена"
|
"environment_not_found": "Среда не найдена"
|
||||||
},
|
},
|
||||||
"sparkyfitness": {
|
"sparkyfitness": {
|
||||||
"eaten": "Eaten",
|
"eaten": "Съедено",
|
||||||
"burned": "Burned",
|
"burned": "Сожжено",
|
||||||
"remaining": "Remaining",
|
"remaining": "Осталось",
|
||||||
"steps": "Steps"
|
"steps": "Шаги"
|
||||||
},
|
},
|
||||||
"syncthing": {
|
"syncthing": {
|
||||||
"connected": "Connected",
|
"connected": "Подключено",
|
||||||
"synced": "Synced",
|
"synced": "Синхронизировано",
|
||||||
"errors": "Errors",
|
"errors": "Ошибки",
|
||||||
"storage": "Storage"
|
"storage": "Хранилище"
|
||||||
},
|
},
|
||||||
"sportarr": {
|
"sportarr": {
|
||||||
"wanted": "Wanted",
|
"wanted": "Требуется",
|
||||||
"queued": "Queued",
|
"queued": "В очереди",
|
||||||
"leagues": "Leagues"
|
"leagues": "Лиги"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
"seconds": "s"
|
"seconds": "s"
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"signout": "Sign out"
|
"signout": "Odhlásiť sa"
|
||||||
},
|
},
|
||||||
"widget": {
|
"widget": {
|
||||||
"missing_type": "Chýba typ widgetu: {{type}}",
|
"missing_type": "Chýba typ widgetu: {{type}}",
|
||||||
@@ -39,7 +39,9 @@
|
|||||||
"wait": "Počkajte prosím"
|
"wait": "Počkajte prosím"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"placeholder": "Hľadať…"
|
"placeholder": "Hľadať…",
|
||||||
|
"provider": "Vyhľadávač: {{name}}",
|
||||||
|
"select_provider": "Vyberte vyhľadávač"
|
||||||
},
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
"cpu": "CPU",
|
"cpu": "CPU",
|
||||||
@@ -97,7 +99,12 @@
|
|||||||
"unhealthy": "Nezdravý",
|
"unhealthy": "Nezdravý",
|
||||||
"not_found": "Nenájdené",
|
"not_found": "Nenájdené",
|
||||||
"exited": "Ukončené",
|
"exited": "Ukončené",
|
||||||
"partial": "Čiastočný"
|
"partial": "Čiastočný",
|
||||||
|
"created": "Created",
|
||||||
|
"paused": "Paused",
|
||||||
|
"restarting": "Restarting",
|
||||||
|
"removing": "Removing",
|
||||||
|
"dead": "Dead"
|
||||||
},
|
},
|
||||||
"ping": {
|
"ping": {
|
||||||
"error": "Chyba",
|
"error": "Chyba",
|
||||||
@@ -133,7 +140,7 @@
|
|||||||
"series": "Seriálov",
|
"series": "Seriálov",
|
||||||
"episodes": "Epizód",
|
"episodes": "Epizód",
|
||||||
"songs": "Skladieb",
|
"songs": "Skladieb",
|
||||||
"albums": "Albums"
|
"albums": "Albumov"
|
||||||
},
|
},
|
||||||
"esphome": {
|
"esphome": {
|
||||||
"offline": "Nedostupné",
|
"offline": "Nedostupné",
|
||||||
@@ -471,7 +478,8 @@
|
|||||||
"authentik": {
|
"authentik": {
|
||||||
"users": "Používateľov",
|
"users": "Používateľov",
|
||||||
"loginsLast24H": "Prihlás. (24 hod.)",
|
"loginsLast24H": "Prihlás. (24 hod.)",
|
||||||
"failedLoginsLast24H": "Neúspešné prihlás. (24 hod.)"
|
"failedLoginsLast24H": "Neúspešné prihlás. (24 hod.)",
|
||||||
|
"authorizationsLast24H": "Auths (24h)"
|
||||||
},
|
},
|
||||||
"proxmox": {
|
"proxmox": {
|
||||||
"mem": "RAM",
|
"mem": "RAM",
|
||||||
@@ -959,7 +967,7 @@
|
|||||||
"reclaimable": "Reclaimable",
|
"reclaimable": "Reclaimable",
|
||||||
"movieReclaimable": "Movie Reclaimable",
|
"movieReclaimable": "Movie Reclaimable",
|
||||||
"showReclaimable": "Show Reclaimable",
|
"showReclaimable": "Show Reclaimable",
|
||||||
"totalCapacity": "Total Capacity"
|
"totalCapacity": "Celková kapacita"
|
||||||
},
|
},
|
||||||
"netdata": {
|
"netdata": {
|
||||||
"warnings": "Upozornenia",
|
"warnings": "Upozornenia",
|
||||||
@@ -1248,7 +1256,7 @@
|
|||||||
},
|
},
|
||||||
"sportarr": {
|
"sportarr": {
|
||||||
"wanted": "Wanted",
|
"wanted": "Wanted",
|
||||||
"queued": "Queued",
|
"queued": "V poradí",
|
||||||
"leagues": "Leagues"
|
"leagues": "Leagues"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,9 @@
|
|||||||
"wait": "Prosimo počakajte"
|
"wait": "Prosimo počakajte"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"placeholder": "Iskanje…"
|
"placeholder": "Iskanje…",
|
||||||
|
"provider": "Search engine: {{name}}",
|
||||||
|
"select_provider": "Select search engine"
|
||||||
},
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
"cpu": "CPU",
|
"cpu": "CPU",
|
||||||
@@ -97,7 +99,12 @@
|
|||||||
"unhealthy": "Ni zdrav",
|
"unhealthy": "Ni zdrav",
|
||||||
"not_found": "Ni najden",
|
"not_found": "Ni najden",
|
||||||
"exited": "V izhodu",
|
"exited": "V izhodu",
|
||||||
"partial": "Delni"
|
"partial": "Delni",
|
||||||
|
"created": "Created",
|
||||||
|
"paused": "Paused",
|
||||||
|
"restarting": "Restarting",
|
||||||
|
"removing": "Removing",
|
||||||
|
"dead": "Dead"
|
||||||
},
|
},
|
||||||
"ping": {
|
"ping": {
|
||||||
"error": "Error",
|
"error": "Error",
|
||||||
@@ -471,7 +478,8 @@
|
|||||||
"authentik": {
|
"authentik": {
|
||||||
"users": "Users",
|
"users": "Users",
|
||||||
"loginsLast24H": "Prijave (24h)",
|
"loginsLast24H": "Prijave (24h)",
|
||||||
"failedLoginsLast24H": "Neveljavne prijave (24h)"
|
"failedLoginsLast24H": "Neveljavne prijave (24h)",
|
||||||
|
"authorizationsLast24H": "Auths (24h)"
|
||||||
},
|
},
|
||||||
"proxmox": {
|
"proxmox": {
|
||||||
"mem": "MEM",
|
"mem": "MEM",
|
||||||
|
|||||||
@@ -39,7 +39,9 @@
|
|||||||
"wait": "Молим сачекајте"
|
"wait": "Молим сачекајте"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"placeholder": "Претражи…"
|
"placeholder": "Претражи…",
|
||||||
|
"provider": "Search engine: {{name}}",
|
||||||
|
"select_provider": "Select search engine"
|
||||||
},
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
"cpu": "Процесор",
|
"cpu": "Процесор",
|
||||||
@@ -97,7 +99,12 @@
|
|||||||
"unhealthy": "Нездравих",
|
"unhealthy": "Нездравих",
|
||||||
"not_found": "Није пронађено",
|
"not_found": "Није пронађено",
|
||||||
"exited": "Напуштених",
|
"exited": "Напуштених",
|
||||||
"partial": "Делимично"
|
"partial": "Делимично",
|
||||||
|
"created": "Created",
|
||||||
|
"paused": "Paused",
|
||||||
|
"restarting": "Restarting",
|
||||||
|
"removing": "Removing",
|
||||||
|
"dead": "Dead"
|
||||||
},
|
},
|
||||||
"ping": {
|
"ping": {
|
||||||
"error": "Грешка",
|
"error": "Грешка",
|
||||||
@@ -471,7 +478,8 @@
|
|||||||
"authentik": {
|
"authentik": {
|
||||||
"users": "Корисника",
|
"users": "Корисника",
|
||||||
"loginsLast24H": "Пријаве (24ч)",
|
"loginsLast24H": "Пријаве (24ч)",
|
||||||
"failedLoginsLast24H": "Неуспешне пријаве (24ч)"
|
"failedLoginsLast24H": "Неуспешне пријаве (24ч)",
|
||||||
|
"authorizationsLast24H": "Auths (24h)"
|
||||||
},
|
},
|
||||||
"proxmox": {
|
"proxmox": {
|
||||||
"mem": "Меморија",
|
"mem": "Меморија",
|
||||||
|
|||||||
@@ -39,7 +39,9 @@
|
|||||||
"wait": "Vänligen vänta"
|
"wait": "Vänligen vänta"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"placeholder": "Sök…"
|
"placeholder": "Sök…",
|
||||||
|
"provider": "Search engine: {{name}}",
|
||||||
|
"select_provider": "Select search engine"
|
||||||
},
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
"cpu": "CPU",
|
"cpu": "CPU",
|
||||||
@@ -97,7 +99,12 @@
|
|||||||
"unhealthy": "Unhealthy",
|
"unhealthy": "Unhealthy",
|
||||||
"not_found": "Not Found",
|
"not_found": "Not Found",
|
||||||
"exited": "Exited",
|
"exited": "Exited",
|
||||||
"partial": "Partial"
|
"partial": "Partial",
|
||||||
|
"created": "Created",
|
||||||
|
"paused": "Paused",
|
||||||
|
"restarting": "Restarting",
|
||||||
|
"removing": "Removing",
|
||||||
|
"dead": "Dead"
|
||||||
},
|
},
|
||||||
"ping": {
|
"ping": {
|
||||||
"error": "Error",
|
"error": "Error",
|
||||||
@@ -471,7 +478,8 @@
|
|||||||
"authentik": {
|
"authentik": {
|
||||||
"users": "Users",
|
"users": "Users",
|
||||||
"loginsLast24H": "Inloggningar (24h)",
|
"loginsLast24H": "Inloggningar (24h)",
|
||||||
"failedLoginsLast24H": "Misslyckade inloggningar (24h)"
|
"failedLoginsLast24H": "Misslyckade inloggningar (24h)",
|
||||||
|
"authorizationsLast24H": "Auths (24h)"
|
||||||
},
|
},
|
||||||
"proxmox": {
|
"proxmox": {
|
||||||
"mem": "MEM",
|
"mem": "MEM",
|
||||||
|
|||||||
@@ -39,7 +39,9 @@
|
|||||||
"wait": "దయచేసి వేచి ఉండండి"
|
"wait": "దయచేసి వేచి ఉండండి"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"placeholder": "వెతకండి…"
|
"placeholder": "వెతకండి…",
|
||||||
|
"provider": "Search engine: {{name}}",
|
||||||
|
"select_provider": "Select search engine"
|
||||||
},
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
"cpu": "సీపియూ",
|
"cpu": "సీపియూ",
|
||||||
@@ -97,7 +99,12 @@
|
|||||||
"unhealthy": "Unhealthy",
|
"unhealthy": "Unhealthy",
|
||||||
"not_found": "Not Found",
|
"not_found": "Not Found",
|
||||||
"exited": "Exited",
|
"exited": "Exited",
|
||||||
"partial": "Partial"
|
"partial": "Partial",
|
||||||
|
"created": "Created",
|
||||||
|
"paused": "Paused",
|
||||||
|
"restarting": "Restarting",
|
||||||
|
"removing": "Removing",
|
||||||
|
"dead": "Dead"
|
||||||
},
|
},
|
||||||
"ping": {
|
"ping": {
|
||||||
"error": "Error",
|
"error": "Error",
|
||||||
@@ -471,7 +478,8 @@
|
|||||||
"authentik": {
|
"authentik": {
|
||||||
"users": "Users",
|
"users": "Users",
|
||||||
"loginsLast24H": "లాగిన్లు (24గం)",
|
"loginsLast24H": "లాగిన్లు (24గం)",
|
||||||
"failedLoginsLast24H": "విఫలమైన లాగిన్లు (24గం)"
|
"failedLoginsLast24H": "విఫలమైన లాగిన్లు (24గం)",
|
||||||
|
"authorizationsLast24H": "Auths (24h)"
|
||||||
},
|
},
|
||||||
"proxmox": {
|
"proxmox": {
|
||||||
"mem": "MEM",
|
"mem": "MEM",
|
||||||
|
|||||||
@@ -39,7 +39,9 @@
|
|||||||
"wait": "โปรดรอ"
|
"wait": "โปรดรอ"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"placeholder": "ค้นหา…"
|
"placeholder": "ค้นหา…",
|
||||||
|
"provider": "Search engine: {{name}}",
|
||||||
|
"select_provider": "Select search engine"
|
||||||
},
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
"cpu": "ซีพียู",
|
"cpu": "ซีพียู",
|
||||||
@@ -97,7 +99,12 @@
|
|||||||
"unhealthy": "Unhealthy",
|
"unhealthy": "Unhealthy",
|
||||||
"not_found": "Not Found",
|
"not_found": "Not Found",
|
||||||
"exited": "Exited",
|
"exited": "Exited",
|
||||||
"partial": "Partial"
|
"partial": "Partial",
|
||||||
|
"created": "Created",
|
||||||
|
"paused": "Paused",
|
||||||
|
"restarting": "Restarting",
|
||||||
|
"removing": "Removing",
|
||||||
|
"dead": "Dead"
|
||||||
},
|
},
|
||||||
"ping": {
|
"ping": {
|
||||||
"error": "Error",
|
"error": "Error",
|
||||||
@@ -471,7 +478,8 @@
|
|||||||
"authentik": {
|
"authentik": {
|
||||||
"users": "Users",
|
"users": "Users",
|
||||||
"loginsLast24H": "Logins (24h)",
|
"loginsLast24H": "Logins (24h)",
|
||||||
"failedLoginsLast24H": "Failed Logins (24h)"
|
"failedLoginsLast24H": "Failed Logins (24h)",
|
||||||
|
"authorizationsLast24H": "Auths (24h)"
|
||||||
},
|
},
|
||||||
"proxmox": {
|
"proxmox": {
|
||||||
"mem": "MEM",
|
"mem": "MEM",
|
||||||
|
|||||||
@@ -39,7 +39,9 @@
|
|||||||
"wait": "Lütfen bekleyin"
|
"wait": "Lütfen bekleyin"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"placeholder": "Ara…"
|
"placeholder": "Ara…",
|
||||||
|
"provider": "Search engine: {{name}}",
|
||||||
|
"select_provider": "Select search engine"
|
||||||
},
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
"cpu": "İşlemci",
|
"cpu": "İşlemci",
|
||||||
@@ -97,7 +99,12 @@
|
|||||||
"unhealthy": "Sağlıksız",
|
"unhealthy": "Sağlıksız",
|
||||||
"not_found": "Bulunamadı",
|
"not_found": "Bulunamadı",
|
||||||
"exited": "Kapandı",
|
"exited": "Kapandı",
|
||||||
"partial": "Kısmi"
|
"partial": "Kısmi",
|
||||||
|
"created": "Created",
|
||||||
|
"paused": "Paused",
|
||||||
|
"restarting": "Restarting",
|
||||||
|
"removing": "Removing",
|
||||||
|
"dead": "Dead"
|
||||||
},
|
},
|
||||||
"ping": {
|
"ping": {
|
||||||
"error": "Hata",
|
"error": "Hata",
|
||||||
@@ -471,7 +478,8 @@
|
|||||||
"authentik": {
|
"authentik": {
|
||||||
"users": "Kullanıcılar",
|
"users": "Kullanıcılar",
|
||||||
"loginsLast24H": "Girişler (24 Saat)",
|
"loginsLast24H": "Girişler (24 Saat)",
|
||||||
"failedLoginsLast24H": "Başarısız Girişler (24 Saat)"
|
"failedLoginsLast24H": "Başarısız Girişler (24 Saat)",
|
||||||
|
"authorizationsLast24H": "Auths (24h)"
|
||||||
},
|
},
|
||||||
"proxmox": {
|
"proxmox": {
|
||||||
"mem": "Bellek",
|
"mem": "Bellek",
|
||||||
|
|||||||
@@ -39,7 +39,9 @@
|
|||||||
"wait": "Будь ласка, зачекайте"
|
"wait": "Будь ласка, зачекайте"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"placeholder": "Пошук…"
|
"placeholder": "Пошук…",
|
||||||
|
"provider": "Search engine: {{name}}",
|
||||||
|
"select_provider": "Select search engine"
|
||||||
},
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
"cpu": "ЦП",
|
"cpu": "ЦП",
|
||||||
@@ -97,7 +99,12 @@
|
|||||||
"unhealthy": "Нездоровий",
|
"unhealthy": "Нездоровий",
|
||||||
"not_found": "Не знайдено",
|
"not_found": "Не знайдено",
|
||||||
"exited": "Вийшов",
|
"exited": "Вийшов",
|
||||||
"partial": "Частковий"
|
"partial": "Частковий",
|
||||||
|
"created": "Created",
|
||||||
|
"paused": "Paused",
|
||||||
|
"restarting": "Restarting",
|
||||||
|
"removing": "Removing",
|
||||||
|
"dead": "Dead"
|
||||||
},
|
},
|
||||||
"ping": {
|
"ping": {
|
||||||
"error": "Помилка",
|
"error": "Помилка",
|
||||||
@@ -471,7 +478,8 @@
|
|||||||
"authentik": {
|
"authentik": {
|
||||||
"users": "Користувачі",
|
"users": "Користувачі",
|
||||||
"loginsLast24H": "Вхід (протягом доби)",
|
"loginsLast24H": "Вхід (протягом доби)",
|
||||||
"failedLoginsLast24H": "Невдалі входи (протягом доби)"
|
"failedLoginsLast24H": "Невдалі входи (протягом доби)",
|
||||||
|
"authorizationsLast24H": "Auths (24h)"
|
||||||
},
|
},
|
||||||
"proxmox": {
|
"proxmox": {
|
||||||
"mem": "Пам'ять",
|
"mem": "Пам'ять",
|
||||||
|
|||||||
+208
-200
@@ -21,7 +21,7 @@
|
|||||||
"seconds": "giây"
|
"seconds": "giây"
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"signout": "Sign out"
|
"signout": "Đăng xuất"
|
||||||
},
|
},
|
||||||
"widget": {
|
"widget": {
|
||||||
"missing_type": "Thiếu loại Widget: {{type}}",
|
"missing_type": "Thiếu loại Widget: {{type}}",
|
||||||
@@ -39,7 +39,9 @@
|
|||||||
"wait": "Vui lòng chờ"
|
"wait": "Vui lòng chờ"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"placeholder": "Tìm kiếm…"
|
"placeholder": "Tìm kiếm…",
|
||||||
|
"provider": "Công cụ tìm kiếm: {{name}}",
|
||||||
|
"select_provider": "Chọn công cụ tìm kiếm"
|
||||||
},
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
"cpu": "CPU",
|
"cpu": "CPU",
|
||||||
@@ -48,7 +50,7 @@
|
|||||||
"free": "Dư",
|
"free": "Dư",
|
||||||
"used": "Đã dùng",
|
"used": "Đã dùng",
|
||||||
"load": "\n",
|
"load": "\n",
|
||||||
"temp": "TEMP",
|
"temp": "Tạm thời",
|
||||||
"max": "Tối đa",
|
"max": "Tối đa",
|
||||||
"uptime": "UP"
|
"uptime": "UP"
|
||||||
},
|
},
|
||||||
@@ -64,24 +66,24 @@
|
|||||||
"wlan_devices": "Thiết bị trong mạng WLAN",
|
"wlan_devices": "Thiết bị trong mạng WLAN",
|
||||||
"lan_users": "Người dùng mạng LAN",
|
"lan_users": "Người dùng mạng LAN",
|
||||||
"wlan_users": "Người dùng mạng WLAN",
|
"wlan_users": "Người dùng mạng WLAN",
|
||||||
"up": "UP",
|
"up": "Hoạt động",
|
||||||
"down": "DOWN",
|
"down": "Không hoạt động",
|
||||||
"wait": "Vui lòng chờ",
|
"wait": "Vui lòng chờ",
|
||||||
"empty_data": "Trạng thái hệ thống phụ không xác định"
|
"empty_data": "Trạng thái hệ thống phụ không xác định"
|
||||||
},
|
},
|
||||||
"unifi_drive": {
|
"unifi_drive": {
|
||||||
"healthy": "Healthy",
|
"healthy": "Bình thường",
|
||||||
"degraded": "Degraded",
|
"degraded": "Suy giảm",
|
||||||
"no_data": "No storage data available"
|
"no_data": "Không có dữ liệu"
|
||||||
},
|
},
|
||||||
"duplicati": {
|
"duplicati": {
|
||||||
"jobs": "Jobs",
|
"jobs": "Tác vụ",
|
||||||
"stored": "Stored",
|
"stored": "Đã lưu",
|
||||||
"lastBackup": "Last Run",
|
"lastBackup": "Lần chạy cuối",
|
||||||
"nextRun": "Next Run",
|
"nextRun": "Lần chạy tiếp theo",
|
||||||
"running": "Running",
|
"running": "Đang chạy",
|
||||||
"warnings": "Warnings",
|
"warnings": "Cảnh báo",
|
||||||
"errors": "Errors"
|
"errors": "Lỗi"
|
||||||
},
|
},
|
||||||
"docker": {
|
"docker": {
|
||||||
"rx": "RX",
|
"rx": "RX",
|
||||||
@@ -96,8 +98,13 @@
|
|||||||
"starting": "Đang bắt đầu",
|
"starting": "Đang bắt đầu",
|
||||||
"unhealthy": "Bất thường",
|
"unhealthy": "Bất thường",
|
||||||
"not_found": "Không tìm thấy",
|
"not_found": "Không tìm thấy",
|
||||||
"exited": "Exited",
|
"exited": "Đã thoát",
|
||||||
"partial": "Partial"
|
"partial": "Một phần",
|
||||||
|
"created": "Created",
|
||||||
|
"paused": "Paused",
|
||||||
|
"restarting": "Restarting",
|
||||||
|
"removing": "Removing",
|
||||||
|
"dead": "Dead"
|
||||||
},
|
},
|
||||||
"ping": {
|
"ping": {
|
||||||
"error": "Lỗi",
|
"error": "Lỗi",
|
||||||
@@ -118,35 +125,35 @@
|
|||||||
"playing": "Đang chơi",
|
"playing": "Đang chơi",
|
||||||
"transcoding": "Chuyển định dạng",
|
"transcoding": "Chuyển định dạng",
|
||||||
"bitrate": "Bitrate",
|
"bitrate": "Bitrate",
|
||||||
"no_active": "No Active Streams",
|
"no_active": "Không có luồng đang hoạt động",
|
||||||
"movies": "Phim ảnh",
|
"movies": "Phim ảnh",
|
||||||
"series": "Series",
|
"series": "Series",
|
||||||
"episodes": "Episodes",
|
"episodes": "Tập",
|
||||||
"songs": "Bài hát"
|
"songs": "Bài hát"
|
||||||
},
|
},
|
||||||
"jellyfin": {
|
"jellyfin": {
|
||||||
"playing": "Playing",
|
"playing": "Đang phát",
|
||||||
"transcoding": "Transcoding",
|
"transcoding": "Đang chuyển mã",
|
||||||
"bitrate": "Bitrate",
|
"bitrate": "Tốc độ bit",
|
||||||
"no_active": "No Active Streams",
|
"no_active": "Không có luồng đang hoạt động",
|
||||||
"movies": "Movies",
|
"movies": "Phim",
|
||||||
"series": "Series",
|
"series": "Series",
|
||||||
"episodes": "Episodes",
|
"episodes": "Tập",
|
||||||
"songs": "Songs",
|
"songs": "Bài hát",
|
||||||
"albums": "Albums"
|
"albums": "Album"
|
||||||
},
|
},
|
||||||
"esphome": {
|
"esphome": {
|
||||||
"offline": "Offline",
|
"offline": "Offline",
|
||||||
"offline_alt": "Offline",
|
"offline_alt": "Offline",
|
||||||
"online": "Online",
|
"online": "Online",
|
||||||
"total": "Total",
|
"total": "Tổng cộng",
|
||||||
"unknown": "Không xác định"
|
"unknown": "Không xác định"
|
||||||
},
|
},
|
||||||
"evcc": {
|
"evcc": {
|
||||||
"pv_power": "Production",
|
"pv_power": "Công suất phát",
|
||||||
"battery_soc": "Pin",
|
"battery_soc": "Pin",
|
||||||
"grid_power": "Lưới",
|
"grid_power": "Lưới",
|
||||||
"home_power": "Consumption",
|
"home_power": "Công suất tiêu thụ",
|
||||||
"charge_power": "Bộ sạc",
|
"charge_power": "Bộ sạc",
|
||||||
"kilowatt": "kW"
|
"kilowatt": "kW"
|
||||||
},
|
},
|
||||||
@@ -154,7 +161,7 @@
|
|||||||
"download": "Tải xuống",
|
"download": "Tải xuống",
|
||||||
"upload": "Tải lên",
|
"upload": "Tải lên",
|
||||||
"leech": "",
|
"leech": "",
|
||||||
"seed": "Seed"
|
"seed": "Seeder"
|
||||||
},
|
},
|
||||||
"freshrss": {
|
"freshrss": {
|
||||||
"subscriptions": "Đăng ký",
|
"subscriptions": "Đăng ký",
|
||||||
@@ -173,115 +180,115 @@
|
|||||||
"maxDown": "Tải xuống tối đa",
|
"maxDown": "Tải xuống tối đa",
|
||||||
"maxUp": "Tải lên tối đa",
|
"maxUp": "Tải lên tối đa",
|
||||||
"down": "Down",
|
"down": "Down",
|
||||||
"up": "Up",
|
"up": "Tải lên",
|
||||||
"received": "Received",
|
"received": "Đã nhận",
|
||||||
"sent": "Sent",
|
"sent": "Đã gửi",
|
||||||
"externalIPAddress": "Ext. IP",
|
"externalIPAddress": "Địa chỉ IP bên ngoài",
|
||||||
"externalIPv6Address": "Ext. IPv6",
|
"externalIPv6Address": "Địa chỉ IPv6 bên ngoài",
|
||||||
"externalIPv6Prefix": "Ext. IPv6-Prefix"
|
"externalIPv6Prefix": "Tiền tố IPv6 bên ngoài"
|
||||||
},
|
},
|
||||||
"caddy": {
|
"caddy": {
|
||||||
"upstreams": "Upstreams",
|
"upstreams": "Máy chủ backend",
|
||||||
"requests": "Current requests",
|
"requests": "Yêu cầu hiện tại",
|
||||||
"requests_failed": "Failed requests"
|
"requests_failed": "Yêu cầu thất bại"
|
||||||
},
|
},
|
||||||
"changedetectionio": {
|
"changedetectionio": {
|
||||||
"totalObserved": "Total Observed",
|
"totalObserved": "Tổng số đã theo dõi",
|
||||||
"diffsDetected": "Diffs Detected"
|
"diffsDetected": "Số thay đổi đã phát hiện"
|
||||||
},
|
},
|
||||||
"channelsdvrserver": {
|
"channelsdvrserver": {
|
||||||
"shows": "Shows",
|
"shows": "Chương trình",
|
||||||
"recordings": "Recordings",
|
"recordings": "Bản ghi",
|
||||||
"scheduled": "Scheduled",
|
"scheduled": "Đã lên lịch",
|
||||||
"passes": "Passes"
|
"passes": "Quy tắc ghi"
|
||||||
},
|
},
|
||||||
"tautulli": {
|
"tautulli": {
|
||||||
"playing": "Playing",
|
"playing": "Đang phát",
|
||||||
"transcoding": "Transcoding",
|
"transcoding": "Đang chuyển mã",
|
||||||
"bitrate": "Bitrate",
|
"bitrate": "Tốc độ bit",
|
||||||
"no_active": "No Active Streams",
|
"no_active": "Không có luồng đang hoạt động",
|
||||||
"plex_connection_error": "Check Plex Connection"
|
"plex_connection_error": "Kiểm tra kết nối Plex"
|
||||||
},
|
},
|
||||||
"tracearr": {
|
"tracearr": {
|
||||||
"no_active": "No Active Streams",
|
"no_active": "Không có luồng đang hoạt động",
|
||||||
"streams": "Streams",
|
"streams": "Luồng",
|
||||||
"transcodes": "Transcodes",
|
"transcodes": "Luồng chuyển mã",
|
||||||
"directplay": "Direct Play",
|
"directplay": "Phát trực tiếp",
|
||||||
"bitrate": "Bitrate"
|
"bitrate": "Tốc độ bit"
|
||||||
},
|
},
|
||||||
"omada": {
|
"omada": {
|
||||||
"connectedAp": "Connected APs",
|
"connectedAp": "AP đã kết nối",
|
||||||
"activeUser": "Active devices",
|
"activeUser": "Thiết bị đang hoạt động",
|
||||||
"alerts": "Alerts",
|
"alerts": "Cảnh báo",
|
||||||
"connectedGateways": "Connected gateways",
|
"connectedGateways": "Gateway đã kết nối",
|
||||||
"connectedSwitches": "Connected switches"
|
"connectedSwitches": "Switch đã kết nối"
|
||||||
},
|
},
|
||||||
"nzbget": {
|
"nzbget": {
|
||||||
"rate": "Rate",
|
"rate": "Tốc độ",
|
||||||
"remaining": "Remaining",
|
"remaining": "Còn lại",
|
||||||
"downloaded": "Đã tải"
|
"downloaded": "Đã tải"
|
||||||
},
|
},
|
||||||
"plex": {
|
"plex": {
|
||||||
"streams": "Active Streams",
|
"streams": "Luồng đang hoạt động",
|
||||||
"albums": "Albums",
|
"albums": "Album",
|
||||||
"movies": "Movies",
|
"movies": "Phim",
|
||||||
"tv": "TV Shows"
|
"tv": "Chương trình TV"
|
||||||
},
|
},
|
||||||
"sabnzbd": {
|
"sabnzbd": {
|
||||||
"rate": "Rate",
|
"rate": "Tốc độ",
|
||||||
"queue": "Hàng chờ",
|
"queue": "Hàng chờ",
|
||||||
"timeleft": "Thời gian còn lại"
|
"timeleft": "Thời gian còn lại"
|
||||||
},
|
},
|
||||||
"rutorrent": {
|
"rutorrent": {
|
||||||
"active": "Hoạt động",
|
"active": "Hoạt động",
|
||||||
"upload": "Upload",
|
"upload": "Tải lên",
|
||||||
"download": "Download"
|
"download": "Tải xuống"
|
||||||
},
|
},
|
||||||
"transmission": {
|
"transmission": {
|
||||||
"download": "Download",
|
"download": "Tải xuống",
|
||||||
"upload": "Upload",
|
"upload": "Tải lên",
|
||||||
"leech": "Leech",
|
"leech": "Leecher",
|
||||||
"seed": "Seed"
|
"seed": "Seeder"
|
||||||
},
|
},
|
||||||
"qbittorrent": {
|
"qbittorrent": {
|
||||||
"download": "Download",
|
"download": "Tải xuống",
|
||||||
"upload": "Upload",
|
"upload": "Tải lên",
|
||||||
"leech": "Leech",
|
"leech": "Leecher",
|
||||||
"seed": "Seed"
|
"seed": "Seeder"
|
||||||
},
|
},
|
||||||
"qnap": {
|
"qnap": {
|
||||||
"cpuUsage": "CPU Usage",
|
"cpuUsage": "Mức sử dụng CPU",
|
||||||
"memUsage": "MEM Usage",
|
"memUsage": "Mức sử dụng bộ nhớ",
|
||||||
"systemTempC": "System Temp",
|
"systemTempC": "Nhiệt độ hệ thống",
|
||||||
"poolUsage": "Pool Usage",
|
"poolUsage": "Mức sử dụng pool",
|
||||||
"volumeUsage": "Volume Usage",
|
"volumeUsage": "Mức sử dụng volume",
|
||||||
"invalid": "Invalid"
|
"invalid": "Không hợp lệ"
|
||||||
},
|
},
|
||||||
"deluge": {
|
"deluge": {
|
||||||
"download": "Download",
|
"download": "Tải xuống",
|
||||||
"upload": "Upload",
|
"upload": "Tải lên",
|
||||||
"leech": "Leech",
|
"leech": "Leecher",
|
||||||
"seed": "Seed"
|
"seed": "Seeder"
|
||||||
},
|
},
|
||||||
"develancacheui": {
|
"develancacheui": {
|
||||||
"cachehitbytes": "Cache Hit Bytes",
|
"cachehitbytes": "Dung lượng cache hit",
|
||||||
"cachemissbytes": "Cache Miss Bytes"
|
"cachemissbytes": "Dung lượng cache miss"
|
||||||
},
|
},
|
||||||
"downloadstation": {
|
"downloadstation": {
|
||||||
"download": "Download",
|
"download": "Tải xuống",
|
||||||
"upload": "Upload",
|
"upload": "Tải lên",
|
||||||
"leech": "Leech",
|
"leech": "Leecher",
|
||||||
"seed": "Seed"
|
"seed": "Seeder"
|
||||||
},
|
},
|
||||||
"sonarr": {
|
"sonarr": {
|
||||||
"wanted": "Wanted",
|
"wanted": "Cần tải",
|
||||||
"queued": "Queued",
|
"queued": "Queued",
|
||||||
"series": "Series",
|
"series": "Phim bộ",
|
||||||
"queue": "Queue",
|
"queue": "Queue",
|
||||||
"unknown": "Unknown"
|
"unknown": "Unknown"
|
||||||
},
|
},
|
||||||
"radarr": {
|
"radarr": {
|
||||||
"wanted": "Wanted",
|
"wanted": "Cần tải",
|
||||||
"missing": "Missing",
|
"missing": "Missing",
|
||||||
"queued": "Queued",
|
"queued": "Queued",
|
||||||
"movies": "Movies",
|
"movies": "Movies",
|
||||||
@@ -289,12 +296,12 @@
|
|||||||
"unknown": "Unknown"
|
"unknown": "Unknown"
|
||||||
},
|
},
|
||||||
"lidarr": {
|
"lidarr": {
|
||||||
"wanted": "Wanted",
|
"wanted": "Cần tải",
|
||||||
"queued": "Queued",
|
"queued": "Queued",
|
||||||
"artists": "Artists"
|
"artists": "Artists"
|
||||||
},
|
},
|
||||||
"readarr": {
|
"readarr": {
|
||||||
"wanted": "Wanted",
|
"wanted": "Cần tải",
|
||||||
"queued": "Queued",
|
"queued": "Queued",
|
||||||
"books": "Sách"
|
"books": "Sách"
|
||||||
},
|
},
|
||||||
@@ -334,66 +341,66 @@
|
|||||||
"latency": "Latency"
|
"latency": "Latency"
|
||||||
},
|
},
|
||||||
"speedtest": {
|
"speedtest": {
|
||||||
"upload": "Upload",
|
"upload": "Tải lên",
|
||||||
"download": "Download",
|
"download": "Tải xuống",
|
||||||
"ping": "Ping"
|
"ping": "Ping"
|
||||||
},
|
},
|
||||||
"portainer": {
|
"portainer": {
|
||||||
"running": "Running",
|
"running": "Đang chạy",
|
||||||
"stopped": "Stopped",
|
"stopped": "Đã dừng",
|
||||||
"total": "Total"
|
"total": "Tổng cộng"
|
||||||
},
|
},
|
||||||
"suwayomi": {
|
"suwayomi": {
|
||||||
"download": "Downloaded",
|
"download": "Đã tải xuống",
|
||||||
"nondownload": "Non-Downloaded",
|
"nondownload": "Chưa tải xuống",
|
||||||
"read": "Read",
|
"read": "Đã đọc",
|
||||||
"unread": "Unread",
|
"unread": "Chưa đọc",
|
||||||
"downloadedread": "Downloaded & Read",
|
"downloadedread": "Đã tải xuống & Đã đọc",
|
||||||
"downloadedunread": "Downloaded & Unread",
|
"downloadedunread": "Đã tải xuống & Chưa đọc",
|
||||||
"nondownloadedread": "Non-Downloaded & Read",
|
"nondownloadedread": "Chưa tải xuống & Đã đọc",
|
||||||
"nondownloadedunread": "Non-Downloaded & Unread"
|
"nondownloadedunread": "Chưa tải xuống & Chưa đọc"
|
||||||
},
|
},
|
||||||
"tailscale": {
|
"tailscale": {
|
||||||
"address": "Address",
|
"address": "Địa chỉ",
|
||||||
"expires": "Expires",
|
"expires": "Hết hạn",
|
||||||
"never": "Never",
|
"never": "Không bao giờ",
|
||||||
"user": "User",
|
"user": "Người dùng",
|
||||||
"hostname": "Hostname",
|
"hostname": "Tên máy chủ",
|
||||||
"name": "Name",
|
"name": "Tên",
|
||||||
"client_version": "Client Version",
|
"client_version": "Phiên bản máy khách",
|
||||||
"os": "OS",
|
"os": "Hệ điều hành",
|
||||||
"created": "Created",
|
"created": "Đã tạo",
|
||||||
"authorized": "Authorized",
|
"authorized": "Đã cấp quyền",
|
||||||
"is_external": "Is External",
|
"is_external": "Là bên ngoài",
|
||||||
"update_available": "Update Available",
|
"update_available": "Có bản cập nhật",
|
||||||
"tags": "Tags",
|
"tags": "Thẻ",
|
||||||
"last_seen": "Last Seen",
|
"last_seen": "Lần cuối nhìn thấy",
|
||||||
"now": "Now",
|
"now": "Bây giờ",
|
||||||
"years": "{{number}}y",
|
"years": "{{number}} năm",
|
||||||
"weeks": "{{number}}w",
|
"weeks": "{{number}} tuần",
|
||||||
"days": "{{number}}d",
|
"days": "{{number}} ngày",
|
||||||
"hours": "{{number}}h",
|
"hours": "{{number}} giờ",
|
||||||
"minutes": "{{number}}m",
|
"minutes": "{{number}} phút",
|
||||||
"seconds": "{{number}}s",
|
"seconds": "{{number}} giây",
|
||||||
"ago": "{{value}} Ago",
|
"ago": "{{value}} trước",
|
||||||
"true": "Yes",
|
"true": "Có",
|
||||||
"false": "No"
|
"false": "Không"
|
||||||
},
|
},
|
||||||
"technitium": {
|
"technitium": {
|
||||||
"totalQueries": "Queries",
|
"totalQueries": "Tổng số truy vấn",
|
||||||
"totalNoError": "Success",
|
"totalNoError": "Thành công",
|
||||||
"totalServerFailure": "Failures",
|
"totalServerFailure": "Lỗi máy chủ",
|
||||||
"totalNxDomain": "NX Domains",
|
"totalNxDomain": "NX Domain",
|
||||||
"totalRefused": "Refused",
|
"totalRefused": "Bị từ chối",
|
||||||
"totalAuthoritative": "Authoritative",
|
"totalAuthoritative": "Authoritative",
|
||||||
"totalRecursive": "Recursive",
|
"totalRecursive": "Recursive",
|
||||||
"totalCached": "Cached",
|
"totalCached": "Được lưu trong bộ nhớ đệm",
|
||||||
"totalBlocked": "Blocked",
|
"totalBlocked": "Bị chặn",
|
||||||
"totalDropped": "Dropped",
|
"totalDropped": "Bị loại bỏ",
|
||||||
"totalClients": "Clients"
|
"totalClients": "Số máy khách"
|
||||||
},
|
},
|
||||||
"tdarr": {
|
"tdarr": {
|
||||||
"queue": "Queue",
|
"queue": "Hàng đợi",
|
||||||
"processed": "Processed",
|
"processed": "Processed",
|
||||||
"errored": "Errored",
|
"errored": "Errored",
|
||||||
"saved": "Saved"
|
"saved": "Saved"
|
||||||
@@ -410,7 +417,7 @@
|
|||||||
"unknown": "Unknown"
|
"unknown": "Unknown"
|
||||||
},
|
},
|
||||||
"navidrome": {
|
"navidrome": {
|
||||||
"nothing_streaming": "No Active Streams",
|
"nothing_streaming": "Không có luồng đang hoạt động",
|
||||||
"please_wait": "Please Wait"
|
"please_wait": "Please Wait"
|
||||||
},
|
},
|
||||||
"npm": {
|
"npm": {
|
||||||
@@ -453,9 +460,9 @@
|
|||||||
"domain_count": "Domains"
|
"domain_count": "Domains"
|
||||||
},
|
},
|
||||||
"medusa": {
|
"medusa": {
|
||||||
"wanted": "Wanted",
|
"wanted": "Cần tải",
|
||||||
"queued": "Queued",
|
"queued": "Queued",
|
||||||
"series": "Series"
|
"series": "Phim bộ"
|
||||||
},
|
},
|
||||||
"minecraft": {
|
"minecraft": {
|
||||||
"players": "Players",
|
"players": "Players",
|
||||||
@@ -471,66 +478,67 @@
|
|||||||
"authentik": {
|
"authentik": {
|
||||||
"users": "Users",
|
"users": "Users",
|
||||||
"loginsLast24H": "Logins (24h)",
|
"loginsLast24H": "Logins (24h)",
|
||||||
"failedLoginsLast24H": "Failed Logins (24h)"
|
"failedLoginsLast24H": "Failed Logins (24h)",
|
||||||
|
"authorizationsLast24H": "Lượt ủy quyền (24 giờ)"
|
||||||
},
|
},
|
||||||
"proxmox": {
|
"proxmox": {
|
||||||
"mem": "MEM",
|
"mem": "Bộ nhớ",
|
||||||
"cpu": "CPU",
|
"cpu": "CPU",
|
||||||
"lxc": "LXC",
|
"lxc": "LXC",
|
||||||
"vms": "VMs"
|
"vms": "VM"
|
||||||
},
|
},
|
||||||
"pulse": {
|
"pulse": {
|
||||||
"nodes": "Nodes",
|
"nodes": "Node",
|
||||||
"vms": "VMs",
|
"vms": "VM",
|
||||||
"lxcs": "LXCs"
|
"lxcs": "LXC"
|
||||||
},
|
},
|
||||||
"glances": {
|
"glances": {
|
||||||
"cpu": "CPU",
|
"cpu": "CPU",
|
||||||
"load": "Load",
|
"load": "Tải hệ thống",
|
||||||
"wait": "Please wait",
|
"wait": "Vui lòng đợi",
|
||||||
"temp": "TEMP",
|
"temp": "Nhiệt độ",
|
||||||
"_temp": "Temp",
|
"_temp": "Nhiệt độ",
|
||||||
"warn": "Warn",
|
"warn": "Cảnh báo",
|
||||||
"uptime": "UP",
|
"uptime": "Thời gian hoạt động",
|
||||||
"total": "Total",
|
"total": "Tổng cộng",
|
||||||
"free": "Free",
|
"free": "Còn trống",
|
||||||
"used": "Used",
|
"used": "Đã sử dụng",
|
||||||
"days": "d",
|
"days": "ngày",
|
||||||
"hours": "h",
|
"hours": "giờ",
|
||||||
"crit": "Crit",
|
"crit": "Nghiêm trọng",
|
||||||
"read": "Read",
|
"read": "Đọc",
|
||||||
"write": "Write",
|
"write": "Ghi",
|
||||||
"gpu": "GPU",
|
"gpu": "GPU",
|
||||||
"mem": "Mem",
|
"mem": "Bộ nhớ",
|
||||||
"swap": "Swap"
|
"swap": "Swap"
|
||||||
},
|
},
|
||||||
"quicklaunch": {
|
"quicklaunch": {
|
||||||
"bookmark": "Bookmark",
|
"bookmark": "Dấu trang",
|
||||||
"service": "Service",
|
"service": "Dịch vụ",
|
||||||
"search": "Search",
|
"search": "Tìm kiếm",
|
||||||
"custom": "Custom",
|
"custom": "Tùy chỉnh",
|
||||||
"visit": "Visit",
|
"visit": "Truy cập",
|
||||||
"url": "URL",
|
"url": "URL",
|
||||||
"searchsuggestion": "Suggestion"
|
"searchsuggestion": "Gợi ý"
|
||||||
},
|
},
|
||||||
"wmo": {
|
"wmo": {
|
||||||
"0-day": "Sunny",
|
"0-day": "Trời nắng",
|
||||||
"0-night": "Clear",
|
"0-night": "Trời quang",
|
||||||
"1-day": "Mainly Sunny",
|
"1-day": "Chủ yếu có nắng",
|
||||||
"1-night": "Mainly Clear",
|
"1-night": "Chủ yếu trời quang",
|
||||||
"2-day": "Partly Cloudy",
|
"2-day": "Có mây từng phần",
|
||||||
"2-night": "Partly Cloudy",
|
"2-night": "Có mây từng phần",
|
||||||
"3-day": "Cloudy",
|
"3-day": "Nhiều mây",
|
||||||
"3-night": "Cloudy",
|
"3-night": "Nhiều mây",
|
||||||
"45-day": "Foggy",
|
"45-day": "Có sương mù",
|
||||||
"45-night": "Foggy",
|
"45-night": "Có sương mù",
|
||||||
"48-day": "Foggy",
|
"48-day": "Có sương mù",
|
||||||
"48-night": "Foggy",
|
"48-night": "Có sương mù",
|
||||||
"51-day": "Light Drizzle",
|
"51-day": "Mưa phùn nhẹ",
|
||||||
"51-night": "Light Drizzle",
|
"51-night": "Mưa phùn nhẹ",
|
||||||
"53-day": "Drizzle",
|
"53-day": "Mưa phùn",
|
||||||
"53-night": "Drizzle",
|
"53-night": "Mưa phùn",
|
||||||
"55-day": "Heavy Drizzle",
|
"55-day": "Mưa phùn lớn",
|
||||||
"55-night": "Heavy Drizzle",
|
"55-night": "Heavy Drizzle",
|
||||||
"56-day": "Light Freezing Drizzle",
|
"56-day": "Light Freezing Drizzle",
|
||||||
"56-night": "Light Freezing Drizzle",
|
"56-night": "Light Freezing Drizzle",
|
||||||
@@ -738,14 +746,14 @@
|
|||||||
"m": "m"
|
"m": "m"
|
||||||
},
|
},
|
||||||
"atsumeru": {
|
"atsumeru": {
|
||||||
"series": "Series",
|
"series": "Bộ truyện",
|
||||||
"archives": "Archives",
|
"archives": "Archives",
|
||||||
"chapters": "Chapters",
|
"chapters": "Chapters",
|
||||||
"categories": "Categories"
|
"categories": "Categories"
|
||||||
},
|
},
|
||||||
"komga": {
|
"komga": {
|
||||||
"libraries": "Libraries",
|
"libraries": "Libraries",
|
||||||
"series": "Series",
|
"series": "Bộ truyện",
|
||||||
"books": "Books"
|
"books": "Books"
|
||||||
},
|
},
|
||||||
"diskstation": {
|
"diskstation": {
|
||||||
@@ -758,9 +766,9 @@
|
|||||||
"streams": "Streams"
|
"streams": "Streams"
|
||||||
},
|
},
|
||||||
"mylar": {
|
"mylar": {
|
||||||
"series": "Series",
|
"series": "Phim bộ",
|
||||||
"issues": "Issues",
|
"issues": "Issues",
|
||||||
"wanted": "Wanted"
|
"wanted": "Cần tải"
|
||||||
},
|
},
|
||||||
"photoprism": {
|
"photoprism": {
|
||||||
"albums": "Albums",
|
"albums": "Albums",
|
||||||
@@ -843,7 +851,7 @@
|
|||||||
"books": "Books",
|
"books": "Books",
|
||||||
"authors": "Authors",
|
"authors": "Authors",
|
||||||
"categories": "Categories",
|
"categories": "Categories",
|
||||||
"series": "Series"
|
"series": "Bộ sách"
|
||||||
},
|
},
|
||||||
"booklore": {
|
"booklore": {
|
||||||
"libraries": "Libraries",
|
"libraries": "Libraries",
|
||||||
@@ -858,7 +866,7 @@
|
|||||||
"downloadSpeed": "Speed"
|
"downloadSpeed": "Speed"
|
||||||
},
|
},
|
||||||
"kavita": {
|
"kavita": {
|
||||||
"seriesCount": "Series",
|
"seriesCount": "Số bộ truyện",
|
||||||
"totalFiles": "Files"
|
"totalFiles": "Files"
|
||||||
},
|
},
|
||||||
"azuredevops": {
|
"azuredevops": {
|
||||||
@@ -1247,7 +1255,7 @@
|
|||||||
"storage": "Storage"
|
"storage": "Storage"
|
||||||
},
|
},
|
||||||
"sportarr": {
|
"sportarr": {
|
||||||
"wanted": "Wanted",
|
"wanted": "Cần tải",
|
||||||
"queued": "Queued",
|
"queued": "Queued",
|
||||||
"leagues": "Leagues"
|
"leagues": "Leagues"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,9 @@
|
|||||||
"wait": "請稍後"
|
"wait": "請稍後"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"placeholder": "搜索緊…"
|
"placeholder": "搜索緊…",
|
||||||
|
"provider": "Search engine: {{name}}",
|
||||||
|
"select_provider": "Select search engine"
|
||||||
},
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
"cpu": "CPU",
|
"cpu": "CPU",
|
||||||
@@ -97,7 +99,12 @@
|
|||||||
"unhealthy": "不健康的",
|
"unhealthy": "不健康的",
|
||||||
"not_found": "未找到",
|
"not_found": "未找到",
|
||||||
"exited": "已退出",
|
"exited": "已退出",
|
||||||
"partial": "部分"
|
"partial": "部分",
|
||||||
|
"created": "Created",
|
||||||
|
"paused": "Paused",
|
||||||
|
"restarting": "Restarting",
|
||||||
|
"removing": "Removing",
|
||||||
|
"dead": "Dead"
|
||||||
},
|
},
|
||||||
"ping": {
|
"ping": {
|
||||||
"error": "Error",
|
"error": "Error",
|
||||||
@@ -471,7 +478,8 @@
|
|||||||
"authentik": {
|
"authentik": {
|
||||||
"users": "Users",
|
"users": "Users",
|
||||||
"loginsLast24H": "登錄( 24小时)",
|
"loginsLast24H": "登錄( 24小时)",
|
||||||
"failedLoginsLast24H": "登錄失敗( 24鐘頭)"
|
"failedLoginsLast24H": "登錄失敗( 24鐘頭)",
|
||||||
|
"authorizationsLast24H": "Auths (24h)"
|
||||||
},
|
},
|
||||||
"proxmox": {
|
"proxmox": {
|
||||||
"mem": "MEM",
|
"mem": "MEM",
|
||||||
|
|||||||
@@ -39,7 +39,9 @@
|
|||||||
"wait": "请稍候"
|
"wait": "请稍候"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"placeholder": "搜索…"
|
"placeholder": "搜索…",
|
||||||
|
"provider": "Search engine: {{name}}",
|
||||||
|
"select_provider": "Select search engine"
|
||||||
},
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
"cpu": "CPU",
|
"cpu": "CPU",
|
||||||
@@ -97,7 +99,12 @@
|
|||||||
"unhealthy": "不健康",
|
"unhealthy": "不健康",
|
||||||
"not_found": "未找到",
|
"not_found": "未找到",
|
||||||
"exited": "已退出",
|
"exited": "已退出",
|
||||||
"partial": "部分"
|
"partial": "部分",
|
||||||
|
"created": "Created",
|
||||||
|
"paused": "Paused",
|
||||||
|
"restarting": "Restarting",
|
||||||
|
"removing": "Removing",
|
||||||
|
"dead": "Dead"
|
||||||
},
|
},
|
||||||
"ping": {
|
"ping": {
|
||||||
"error": "错误",
|
"error": "错误",
|
||||||
@@ -471,7 +478,8 @@
|
|||||||
"authentik": {
|
"authentik": {
|
||||||
"users": "Users",
|
"users": "Users",
|
||||||
"loginsLast24H": "登录 (24h)",
|
"loginsLast24H": "登录 (24h)",
|
||||||
"failedLoginsLast24H": "登录失败 (24h)"
|
"failedLoginsLast24H": "登录失败 (24h)",
|
||||||
|
"authorizationsLast24H": "Auths (24h)"
|
||||||
},
|
},
|
||||||
"proxmox": {
|
"proxmox": {
|
||||||
"mem": "内存",
|
"mem": "内存",
|
||||||
|
|||||||
@@ -39,7 +39,9 @@
|
|||||||
"wait": "請稍候"
|
"wait": "請稍候"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"placeholder": "搜尋…"
|
"placeholder": "搜尋…",
|
||||||
|
"provider": "Search engine: {{name}}",
|
||||||
|
"select_provider": "Select search engine"
|
||||||
},
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
"cpu": "CPU",
|
"cpu": "CPU",
|
||||||
@@ -97,7 +99,12 @@
|
|||||||
"unhealthy": "不健康的",
|
"unhealthy": "不健康的",
|
||||||
"not_found": "找不到",
|
"not_found": "找不到",
|
||||||
"exited": "已退出",
|
"exited": "已退出",
|
||||||
"partial": "部分"
|
"partial": "部分",
|
||||||
|
"created": "Created",
|
||||||
|
"paused": "Paused",
|
||||||
|
"restarting": "Restarting",
|
||||||
|
"removing": "Removing",
|
||||||
|
"dead": "Dead"
|
||||||
},
|
},
|
||||||
"ping": {
|
"ping": {
|
||||||
"error": "Error",
|
"error": "Error",
|
||||||
@@ -471,7 +478,8 @@
|
|||||||
"authentik": {
|
"authentik": {
|
||||||
"users": "Users",
|
"users": "Users",
|
||||||
"loginsLast24H": "登入 (過去 24 小時)",
|
"loginsLast24H": "登入 (過去 24 小時)",
|
||||||
"failedLoginsLast24H": "登入失敗 (過去 24 小時)"
|
"failedLoginsLast24H": "登入失敗 (過去 24 小時)",
|
||||||
|
"authorizationsLast24H": "Auths (24h)"
|
||||||
},
|
},
|
||||||
"proxmox": {
|
"proxmox": {
|
||||||
"mem": "MEM",
|
"mem": "MEM",
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ describe("pages/api/auth/[...nextauth]", () => {
|
|||||||
it("routes sanitized NextAuth logs through the Homepage logger", async () => {
|
it("routes sanitized NextAuth logs through the Homepage logger", async () => {
|
||||||
const mod = await import("pages/api/auth/[...nextauth]");
|
const mod = await import("pages/api/auth/[...nextauth]");
|
||||||
const sensitiveMetadata = {
|
const sensitiveMetadata = {
|
||||||
|
error: Object.assign(new Error("State cookie was missing."), { access_token: "sensitive-access-token" }),
|
||||||
clientSecret: "sensitive-client-secret",
|
clientSecret: "sensitive-client-secret",
|
||||||
access_token: "sensitive-access-token",
|
access_token: "sensitive-access-token",
|
||||||
id_token: "sensitive-id-token",
|
id_token: "sensitive-id-token",
|
||||||
@@ -82,7 +83,7 @@ describe("pages/api/auth/[...nextauth]", () => {
|
|||||||
mod.authOptions.logger.warn("NEXTAUTH_URL", sensitiveMetadata);
|
mod.authOptions.logger.warn("NEXTAUTH_URL", sensitiveMetadata);
|
||||||
mod.authOptions.logger.debug("OAUTH_CALLBACK_RESPONSE", sensitiveMetadata);
|
mod.authOptions.logger.debug("OAUTH_CALLBACK_RESPONSE", sensitiveMetadata);
|
||||||
|
|
||||||
expect(errorMock).toHaveBeenCalledWith("%s", "OAUTH_CALLBACK_ERROR");
|
expect(errorMock).toHaveBeenCalledWith("%s: %s", "OAUTH_CALLBACK_ERROR", "State cookie was missing.");
|
||||||
expect(warnMock).toHaveBeenCalledWith("%s", "NEXTAUTH_URL");
|
expect(warnMock).toHaveBeenCalledWith("%s", "NEXTAUTH_URL");
|
||||||
expect(debugMock).toHaveBeenCalledWith("%s", "OAUTH_CALLBACK_RESPONSE");
|
expect(debugMock).toHaveBeenCalledWith("%s", "OAUTH_CALLBACK_RESPONSE");
|
||||||
expect(JSON.stringify([...errorMock.mock.calls, ...warnMock.mock.calls, ...debugMock.mock.calls])).not.toContain(
|
expect(JSON.stringify([...errorMock.mock.calls, ...warnMock.mock.calls, ...debugMock.mock.calls])).not.toContain(
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
import createMockRes from "test-utils/create-mock-res";
|
||||||
|
|
||||||
|
const { getDockerStats, logger } = vi.hoisted(() => ({
|
||||||
|
getDockerStats: vi.fn(),
|
||||||
|
logger: { error: vi.fn() },
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("utils/docker/stats", () => ({ getDockerStats }));
|
||||||
|
|
||||||
|
vi.mock("utils/logger", () => ({
|
||||||
|
default: () => logger,
|
||||||
|
}));
|
||||||
|
|
||||||
|
import handler from "pages/api/docker/stats";
|
||||||
|
|
||||||
|
describe("pages/api/docker/stats", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns the stats map for the requested server", async () => {
|
||||||
|
getDockerStats.mockResolvedValue({ stats: { app: { cpu: 20, mem: 900, rx: 4, tx: 6 } } });
|
||||||
|
|
||||||
|
const req = { query: { server: "local" } };
|
||||||
|
const res = createMockRes();
|
||||||
|
|
||||||
|
await handler(req, res);
|
||||||
|
|
||||||
|
expect(getDockerStats).toHaveBeenCalledWith("local");
|
||||||
|
expect(res.statusCode).toBe(200);
|
||||||
|
expect(res.body).toEqual({ stats: { app: { cpu: 20, mem: 900, rx: 4, tx: 6 } } });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns 500 when the stats lookup reports an error", async () => {
|
||||||
|
getDockerStats.mockResolvedValue({ error: "query failed" });
|
||||||
|
|
||||||
|
const req = { query: { server: "local" } };
|
||||||
|
const res = createMockRes();
|
||||||
|
|
||||||
|
await handler(req, res);
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(500);
|
||||||
|
expect(res.body).toEqual({ error: "query failed" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("logs and returns 500 when the stats lookup throws", async () => {
|
||||||
|
getDockerStats.mockRejectedValue(new Error("boom"));
|
||||||
|
|
||||||
|
const req = { query: { server: "local" } };
|
||||||
|
const res = createMockRes();
|
||||||
|
|
||||||
|
await handler(req, res);
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(500);
|
||||||
|
expect(res.body).toEqual({ error: { message: "boom" } });
|
||||||
|
expect(logger.error).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,153 +0,0 @@
|
|||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
||||||
|
|
||||||
import createMockRes from "test-utils/create-mock-res";
|
|
||||||
|
|
||||||
const { state, DockerCtor, getDockerArguments, logger } = vi.hoisted(() => {
|
|
||||||
const state = {
|
|
||||||
docker: null,
|
|
||||||
dockerArgs: { conn: { socketPath: "/var/run/docker.sock" }, swarm: false },
|
|
||||||
};
|
|
||||||
|
|
||||||
function DockerCtor() {
|
|
||||||
return state.docker;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
state,
|
|
||||||
DockerCtor,
|
|
||||||
getDockerArguments: vi.fn(() => state.dockerArgs),
|
|
||||||
logger: { error: vi.fn() },
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
vi.mock("dockerode", () => ({
|
|
||||||
default: DockerCtor,
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("utils/config/docker", () => ({
|
|
||||||
default: getDockerArguments,
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("utils/logger", () => ({
|
|
||||||
default: () => logger,
|
|
||||||
}));
|
|
||||||
|
|
||||||
import handler from "pages/api/docker/stats/[...service]";
|
|
||||||
|
|
||||||
describe("pages/api/docker/stats/[...service]", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
vi.clearAllMocks();
|
|
||||||
state.dockerArgs = { conn: { socketPath: "/var/run/docker.sock" }, swarm: false };
|
|
||||||
state.docker = {
|
|
||||||
listContainers: vi.fn(),
|
|
||||||
getContainer: vi.fn(),
|
|
||||||
listTasks: vi.fn(),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns 400 when container name/server params are missing", async () => {
|
|
||||||
const req = { query: { service: [] } };
|
|
||||||
const res = createMockRes();
|
|
||||||
|
|
||||||
await handler(req, res);
|
|
||||||
|
|
||||||
expect(res.statusCode).toBe(400);
|
|
||||||
expect(res.body).toEqual({ error: "docker query parameters are required" });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns 500 when docker returns a non-array containers payload", async () => {
|
|
||||||
state.docker.listContainers.mockResolvedValue(Buffer.from("bad"));
|
|
||||||
|
|
||||||
const req = { query: { service: ["c", "local"] } };
|
|
||||||
const res = createMockRes();
|
|
||||||
|
|
||||||
await handler(req, res);
|
|
||||||
|
|
||||||
expect(res.statusCode).toBe(500);
|
|
||||||
expect(res.body).toEqual({ error: "query failed" });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns stats for an existing container", async () => {
|
|
||||||
state.docker.listContainers.mockResolvedValue([{ Names: ["/myapp"], Id: "cid1" }]);
|
|
||||||
const containerStats = { cpu_stats: { cpu_usage: { total_usage: 1 } } };
|
|
||||||
state.docker.getContainer.mockReturnValue({
|
|
||||||
stats: vi.fn().mockResolvedValue(containerStats),
|
|
||||||
});
|
|
||||||
|
|
||||||
const req = { query: { service: ["myapp", "local"] } };
|
|
||||||
const res = createMockRes();
|
|
||||||
|
|
||||||
await handler(req, res);
|
|
||||||
|
|
||||||
expect(res.statusCode).toBe(200);
|
|
||||||
expect(res.body).toEqual({ stats: containerStats });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("uses swarm tasks to locate a container and reports a friendly error when stats cannot be retrieved", async () => {
|
|
||||||
state.dockerArgs.swarm = true;
|
|
||||||
state.docker.listContainers.mockResolvedValue([{ Names: ["/other"], Id: "local1" }]);
|
|
||||||
state.docker.listTasks.mockResolvedValue([
|
|
||||||
{ Status: { ContainerStatus: { ContainerID: "local1" } } },
|
|
||||||
{ Status: { ContainerStatus: { ContainerID: "remote1" } } },
|
|
||||||
]);
|
|
||||||
state.docker.getContainer.mockReturnValue({
|
|
||||||
stats: vi.fn().mockRejectedValue(new Error("nope")),
|
|
||||||
});
|
|
||||||
|
|
||||||
const req = { query: { service: ["svc", "local"] } };
|
|
||||||
const res = createMockRes();
|
|
||||||
|
|
||||||
await handler(req, res);
|
|
||||||
|
|
||||||
expect(res.statusCode).toBe(200);
|
|
||||||
expect(res.body).toEqual({ error: "Unable to retrieve stats" });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns stats for a swarm task container when present locally", async () => {
|
|
||||||
state.dockerArgs.swarm = true;
|
|
||||||
state.docker.listContainers.mockResolvedValue([{ Names: ["/other"], Id: "local1" }]);
|
|
||||||
state.docker.listTasks.mockResolvedValue([{ Status: { ContainerStatus: { ContainerID: "local1" } } }]);
|
|
||||||
|
|
||||||
const containerStats = { cpu_stats: { cpu_usage: { total_usage: 2 } } };
|
|
||||||
state.docker.getContainer.mockReturnValue({
|
|
||||||
stats: vi.fn().mockResolvedValue(containerStats),
|
|
||||||
});
|
|
||||||
|
|
||||||
const req = { query: { service: ["svc", "local"] } };
|
|
||||||
const res = createMockRes();
|
|
||||||
|
|
||||||
await handler(req, res);
|
|
||||||
|
|
||||||
expect(res.statusCode).toBe(200);
|
|
||||||
expect(res.body).toEqual({ stats: containerStats });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns 404 when no container or swarm task is found", async () => {
|
|
||||||
state.dockerArgs.swarm = true;
|
|
||||||
state.docker.listContainers.mockResolvedValue([{ Names: ["/other"], Id: "local1" }]);
|
|
||||||
state.docker.listTasks.mockResolvedValue([]);
|
|
||||||
|
|
||||||
const req = { query: { service: ["missing", "local"] } };
|
|
||||||
const res = createMockRes();
|
|
||||||
|
|
||||||
await handler(req, res);
|
|
||||||
|
|
||||||
expect(res.statusCode).toBe(404);
|
|
||||||
expect(res.body).toEqual({ error: "not found" });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("logs and returns 500 when the docker query throws", async () => {
|
|
||||||
getDockerArguments.mockImplementationOnce(() => {
|
|
||||||
throw new Error("boom");
|
|
||||||
});
|
|
||||||
|
|
||||||
const req = { query: { service: ["myapp", "local"] } };
|
|
||||||
const res = createMockRes();
|
|
||||||
|
|
||||||
await handler(req, res);
|
|
||||||
|
|
||||||
expect(res.statusCode).toBe(500);
|
|
||||||
expect(res.body).toEqual({ error: { message: "boom" } });
|
|
||||||
expect(logger.error).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,211 +0,0 @@
|
|||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
||||||
|
|
||||||
import createMockRes from "test-utils/create-mock-res";
|
|
||||||
|
|
||||||
const { state, DockerCtor, getDockerArguments, logger } = vi.hoisted(() => {
|
|
||||||
const state = {
|
|
||||||
docker: null,
|
|
||||||
dockerCtorArgs: [],
|
|
||||||
dockerArgs: { conn: { socketPath: "/var/run/docker.sock" }, swarm: false },
|
|
||||||
};
|
|
||||||
|
|
||||||
function DockerCtor(conn) {
|
|
||||||
state.dockerCtorArgs.push(conn);
|
|
||||||
return state.docker;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
state,
|
|
||||||
DockerCtor,
|
|
||||||
getDockerArguments: vi.fn(() => state.dockerArgs),
|
|
||||||
logger: { error: vi.fn() },
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
vi.mock("dockerode", () => ({
|
|
||||||
default: DockerCtor,
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("utils/config/docker", () => ({
|
|
||||||
default: getDockerArguments,
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("utils/logger", () => ({
|
|
||||||
default: () => logger,
|
|
||||||
}));
|
|
||||||
|
|
||||||
import handler from "pages/api/docker/status/[...service]";
|
|
||||||
|
|
||||||
describe("pages/api/docker/status/[...service]", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
vi.clearAllMocks();
|
|
||||||
state.dockerCtorArgs.length = 0;
|
|
||||||
state.dockerArgs = { conn: { socketPath: "/var/run/docker.sock" }, swarm: false };
|
|
||||||
state.docker = {
|
|
||||||
listContainers: vi.fn(),
|
|
||||||
getContainer: vi.fn(),
|
|
||||||
getService: vi.fn(),
|
|
||||||
listTasks: vi.fn(),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns 400 when container name/server params are missing", async () => {
|
|
||||||
const req = { query: { service: [] } };
|
|
||||||
const res = createMockRes();
|
|
||||||
|
|
||||||
await handler(req, res);
|
|
||||||
|
|
||||||
expect(res.statusCode).toBe(400);
|
|
||||||
expect(res.body).toEqual({ error: "docker query parameters are required" });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns 500 when docker returns a non-array containers payload", async () => {
|
|
||||||
state.docker.listContainers.mockResolvedValue(Buffer.from("bad"));
|
|
||||||
|
|
||||||
const req = { query: { service: ["c", "local"] } };
|
|
||||||
const res = createMockRes();
|
|
||||||
|
|
||||||
await handler(req, res);
|
|
||||||
|
|
||||||
expect(res.statusCode).toBe(500);
|
|
||||||
expect(res.body).toEqual({ error: "query failed" });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("inspects an existing container and returns status + health", async () => {
|
|
||||||
state.docker.listContainers.mockResolvedValue([{ Names: ["/myapp"], Id: "cid1" }]);
|
|
||||||
state.docker.getContainer.mockReturnValue({
|
|
||||||
inspect: vi.fn().mockResolvedValue({ State: { Status: "running", Health: { Status: "healthy" } } }),
|
|
||||||
});
|
|
||||||
|
|
||||||
const req = { query: { service: ["myapp", "local"] } };
|
|
||||||
const res = createMockRes();
|
|
||||||
|
|
||||||
await handler(req, res);
|
|
||||||
|
|
||||||
expect(getDockerArguments).toHaveBeenCalledWith("local");
|
|
||||||
expect(state.dockerCtorArgs).toHaveLength(1);
|
|
||||||
expect(res.statusCode).toBe(200);
|
|
||||||
expect(res.body).toEqual({ status: "running", health: "healthy" });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns 404 when container does not exist and swarm is disabled", async () => {
|
|
||||||
state.docker.listContainers.mockResolvedValue([{ Names: ["/other"], Id: "cid1" }]);
|
|
||||||
|
|
||||||
const req = { query: { service: ["missing", "local"] } };
|
|
||||||
const res = createMockRes();
|
|
||||||
|
|
||||||
await handler(req, res);
|
|
||||||
|
|
||||||
expect(res.statusCode).toBe(404);
|
|
||||||
expect(res.body).toEqual({ status: "not found" });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("reports replicated swarm service status based on desired replicas", async () => {
|
|
||||||
state.dockerArgs.swarm = true;
|
|
||||||
state.docker.listContainers.mockResolvedValue([{ Names: ["/other"], Id: "cid1" }]);
|
|
||||||
state.docker.getService.mockReturnValue({
|
|
||||||
inspect: vi.fn().mockResolvedValue({ Spec: { Mode: { Replicated: { Replicas: "2" } } } }),
|
|
||||||
});
|
|
||||||
state.docker.listTasks.mockResolvedValue([{ Status: {} }, { Status: {} }]);
|
|
||||||
|
|
||||||
const req = { query: { service: ["svc", "local"] } };
|
|
||||||
const res = createMockRes();
|
|
||||||
|
|
||||||
await handler(req, res);
|
|
||||||
|
|
||||||
expect(res.statusCode).toBe(200);
|
|
||||||
expect(res.body).toEqual({ status: "running 2/2" });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("reports partial status for replicated services with fewer running tasks", async () => {
|
|
||||||
state.dockerArgs.swarm = true;
|
|
||||||
state.docker.listContainers.mockResolvedValue([{ Names: ["/other"], Id: "cid1" }]);
|
|
||||||
state.docker.getService.mockReturnValue({
|
|
||||||
inspect: vi.fn().mockResolvedValue({ Spec: { Mode: { Replicated: { Replicas: "3" } } } }),
|
|
||||||
});
|
|
||||||
state.docker.listTasks.mockResolvedValue([{ Status: {} }]);
|
|
||||||
|
|
||||||
const req = { query: { service: ["svc", "local"] } };
|
|
||||||
const res = createMockRes();
|
|
||||||
|
|
||||||
await handler(req, res);
|
|
||||||
|
|
||||||
expect(res.statusCode).toBe(200);
|
|
||||||
expect(res.body).toEqual({ status: "partial 1/3" });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("handles global services by inspecting a local task container when possible", async () => {
|
|
||||||
state.dockerArgs.swarm = true;
|
|
||||||
state.docker.listContainers.mockResolvedValue([{ Names: ["/other"], Id: "local1" }]);
|
|
||||||
state.docker.getService.mockReturnValue({
|
|
||||||
inspect: vi.fn().mockResolvedValue({ Spec: { Mode: { Global: {} } } }),
|
|
||||||
});
|
|
||||||
state.docker.listTasks.mockResolvedValue([
|
|
||||||
{ Status: { ContainerStatus: { ContainerID: "local1" }, State: "running" } },
|
|
||||||
]);
|
|
||||||
state.docker.getContainer.mockReturnValue({
|
|
||||||
inspect: vi.fn().mockResolvedValue({ State: { Status: "running", Health: { Status: "unhealthy" } } }),
|
|
||||||
});
|
|
||||||
|
|
||||||
const req = { query: { service: ["svc", "local"] } };
|
|
||||||
const res = createMockRes();
|
|
||||||
|
|
||||||
await handler(req, res);
|
|
||||||
|
|
||||||
expect(res.statusCode).toBe(200);
|
|
||||||
expect(res.body).toEqual({ status: "running", health: "unhealthy" });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("falls back to task status when global service container inspect fails", async () => {
|
|
||||||
state.dockerArgs.swarm = true;
|
|
||||||
state.docker.listContainers.mockResolvedValue([{ Names: ["/other"], Id: "local1" }]);
|
|
||||||
state.docker.getService.mockReturnValue({
|
|
||||||
inspect: vi.fn().mockResolvedValue({ Spec: { Mode: { Global: {} } } }),
|
|
||||||
});
|
|
||||||
state.docker.listTasks.mockResolvedValue([
|
|
||||||
{ Status: { ContainerStatus: { ContainerID: "local1" }, State: "pending" } },
|
|
||||||
]);
|
|
||||||
state.docker.getContainer.mockReturnValue({
|
|
||||||
inspect: vi.fn().mockRejectedValue(new Error("nope")),
|
|
||||||
});
|
|
||||||
|
|
||||||
const req = { query: { service: ["svc", "local"] } };
|
|
||||||
const res = createMockRes();
|
|
||||||
|
|
||||||
await handler(req, res);
|
|
||||||
|
|
||||||
expect(res.statusCode).toBe(200);
|
|
||||||
expect(res.body).toEqual({ status: "pending" });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns 404 when swarm is enabled but the service does not exist", async () => {
|
|
||||||
state.dockerArgs.swarm = true;
|
|
||||||
state.docker.listContainers.mockResolvedValue([{ Names: ["/other"], Id: "cid1" }]);
|
|
||||||
state.docker.getService.mockReturnValue({
|
|
||||||
inspect: vi.fn().mockRejectedValue(new Error("not found")),
|
|
||||||
});
|
|
||||||
|
|
||||||
const req = { query: { service: ["svc", "local"] } };
|
|
||||||
const res = createMockRes();
|
|
||||||
|
|
||||||
await handler(req, res);
|
|
||||||
|
|
||||||
expect(res.statusCode).toBe(404);
|
|
||||||
expect(res.body).toEqual({ status: "not found" });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("logs and returns 500 when the docker query throws", async () => {
|
|
||||||
getDockerArguments.mockImplementationOnce(() => {
|
|
||||||
throw new Error("boom");
|
|
||||||
});
|
|
||||||
|
|
||||||
const req = { query: { service: ["svc", "local"] } };
|
|
||||||
const res = createMockRes();
|
|
||||||
|
|
||||||
await handler(req, res);
|
|
||||||
|
|
||||||
expect(res.statusCode).toBe(500);
|
|
||||||
expect(res.body).toEqual({ error: { message: "boom" } });
|
|
||||||
expect(logger.error).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,265 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
import createMockRes from "test-utils/create-mock-res";
|
||||||
|
|
||||||
|
const { state, DockerCtor, getDockerArguments, containersFromConfig, hasHomepageLabels, getSettings, logger } =
|
||||||
|
vi.hoisted(() => {
|
||||||
|
const state = {
|
||||||
|
docker: null,
|
||||||
|
containers: [],
|
||||||
|
health: {},
|
||||||
|
containers: [],
|
||||||
|
health: {},
|
||||||
|
dockerArgs: { conn: { socketPath: "/var/run/docker.sock" }, swarm: false },
|
||||||
|
};
|
||||||
|
|
||||||
|
function DockerCtor() {
|
||||||
|
return state.docker;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
state,
|
||||||
|
DockerCtor,
|
||||||
|
getDockerArguments: vi.fn(() => state.dockerArgs),
|
||||||
|
containersFromConfig: vi.fn(async () => new Set()),
|
||||||
|
hasHomepageLabels: vi.fn(() => false),
|
||||||
|
getSettings: vi.fn(() => ({ instanceName: undefined })),
|
||||||
|
logger: { error: vi.fn() },
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mock("dockerode", () => ({
|
||||||
|
default: DockerCtor,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("utils/config/docker", () => ({
|
||||||
|
default: getDockerArguments,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("utils/config/service-helpers", () => ({
|
||||||
|
containersFromConfig,
|
||||||
|
hasHomepageLabels,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("utils/config/config", () => ({
|
||||||
|
getSettings,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("utils/logger", () => ({
|
||||||
|
default: () => logger,
|
||||||
|
}));
|
||||||
|
|
||||||
|
import handler from "pages/api/docker/statuses";
|
||||||
|
|
||||||
|
describe("pages/api/docker/statuses", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
state.dockerArgs = { conn: { socketPath: "/var/run/docker.sock" }, swarm: false };
|
||||||
|
state.containers = [];
|
||||||
|
state.health = {};
|
||||||
|
state.docker = {
|
||||||
|
listContainers: vi.fn(async (options) => {
|
||||||
|
const health = options?.filters?.health?.[0];
|
||||||
|
return health ? (state.health[health] ?? []) : state.containers;
|
||||||
|
}),
|
||||||
|
listServices: vi.fn(),
|
||||||
|
listTasks: vi.fn(),
|
||||||
|
};
|
||||||
|
containersFromConfig.mockResolvedValue(new Set());
|
||||||
|
hasHomepageLabels.mockReturnValue(false);
|
||||||
|
getSettings.mockReturnValue({ instanceName: undefined });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns configured container statuses with health from the docker health filters", async () => {
|
||||||
|
containersFromConfig.mockResolvedValue(new Set(["glance", "share"]));
|
||||||
|
state.containers = [
|
||||||
|
{ Names: ["/glance"], Id: "cid-glance", State: "running" },
|
||||||
|
{ Names: ["/share"], Id: "cid-share", State: "exited" },
|
||||||
|
];
|
||||||
|
state.health = { healthy: [{ Id: "cid-glance" }] };
|
||||||
|
|
||||||
|
const req = { query: { server: "local" } };
|
||||||
|
const res = createMockRes();
|
||||||
|
|
||||||
|
await handler(req, res);
|
||||||
|
|
||||||
|
expect(getDockerArguments).toHaveBeenCalledWith("local");
|
||||||
|
expect(containersFromConfig).toHaveBeenCalledWith("local");
|
||||||
|
// one list plus one per health state, constant regardless of container count
|
||||||
|
expect(state.docker.listContainers).toHaveBeenCalledTimes(4);
|
||||||
|
expect(state.docker.listServices).not.toHaveBeenCalled();
|
||||||
|
expect(res.statusCode).toBe(200);
|
||||||
|
expect(res.body).toEqual({
|
||||||
|
statuses: {
|
||||||
|
glance: { status: "running", health: "healthy" },
|
||||||
|
share: { status: "exited" },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("takes health from the filter results rather than the status string", async () => {
|
||||||
|
containersFromConfig.mockResolvedValue(new Set(["app"]));
|
||||||
|
state.containers = [{ Names: ["/app"], Id: "cid1", State: "running", Status: "Up 2 hours (healthy)" }];
|
||||||
|
state.health = { unhealthy: [{ Id: "cid1" }] };
|
||||||
|
|
||||||
|
const req = { query: { server: "local" } };
|
||||||
|
const res = createMockRes();
|
||||||
|
|
||||||
|
await handler(req, res);
|
||||||
|
|
||||||
|
expect(res.body).toEqual({ statuses: { app: { status: "running", health: "unhealthy" } } });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("omits health when the docker daemon rejects the health filter", async () => {
|
||||||
|
containersFromConfig.mockResolvedValue(new Set(["app"]));
|
||||||
|
state.containers = [{ Names: ["/app"], Id: "cid1", State: "running", Status: "Up 2 hours (healthy)" }];
|
||||||
|
state.docker.listContainers.mockImplementation(async (options) => {
|
||||||
|
if (options?.filters?.health) throw new Error("filter unsupported");
|
||||||
|
return state.containers;
|
||||||
|
});
|
||||||
|
|
||||||
|
const req = { query: { server: "local" } };
|
||||||
|
const res = createMockRes();
|
||||||
|
|
||||||
|
await handler(req, res);
|
||||||
|
|
||||||
|
expect(res.body).toEqual({ statuses: { app: { status: "running" } } });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("omits containers that are neither configured nor labelled for homepage", async () => {
|
||||||
|
containersFromConfig.mockResolvedValue(new Set(["glance"]));
|
||||||
|
state.containers = [
|
||||||
|
{ Names: ["/glance"], State: "running", Status: "Up" },
|
||||||
|
{ Names: ["/secret-db"], State: "running", Status: "Up" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const req = { query: { server: "local" } };
|
||||||
|
const res = createMockRes();
|
||||||
|
|
||||||
|
await handler(req, res);
|
||||||
|
|
||||||
|
expect(res.body).toEqual({ statuses: { glance: { status: "running" } } });
|
||||||
|
expect(res.body.statuses["secret-db"]).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("includes containers discovered through homepage labels", async () => {
|
||||||
|
hasHomepageLabels.mockImplementation((labels) => labels?.["homepage.name"] !== undefined);
|
||||||
|
state.containers = [
|
||||||
|
{ Names: ["/labelled"], State: "running", Status: "Up", Labels: { "homepage.name": "App" } },
|
||||||
|
{ Names: ["/unlabelled"], State: "running", Status: "Up", Labels: {} },
|
||||||
|
];
|
||||||
|
|
||||||
|
const req = { query: { server: "local" } };
|
||||||
|
const res = createMockRes();
|
||||||
|
|
||||||
|
await handler(req, res);
|
||||||
|
|
||||||
|
expect(res.body).toEqual({ statuses: { labelled: { status: "running" } } });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("includes swarm services when the server is in swarm mode", async () => {
|
||||||
|
state.dockerArgs.swarm = true;
|
||||||
|
containersFromConfig.mockResolvedValue(new Set(["web", "api"]));
|
||||||
|
state.containers = [{ Names: ["/web"], Id: "cid1", State: "running", Status: "Up" }];
|
||||||
|
state.docker.listServices.mockResolvedValue([
|
||||||
|
{ ID: "sid", Spec: { Name: "api", Mode: { Replicated: { Replicas: "2" } } } },
|
||||||
|
]);
|
||||||
|
state.docker.listTasks.mockResolvedValue([
|
||||||
|
{ ServiceID: "sid", Status: {} },
|
||||||
|
{ ServiceID: "sid", Status: {} },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const req = { query: { server: "swarm" } };
|
||||||
|
const res = createMockRes();
|
||||||
|
|
||||||
|
await handler(req, res);
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(200);
|
||||||
|
expect(res.body).toEqual({
|
||||||
|
statuses: {
|
||||||
|
web: { status: "running" },
|
||||||
|
api: { status: "running 2/2" },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("omits swarm services that are neither configured nor labelled", async () => {
|
||||||
|
state.dockerArgs.swarm = true;
|
||||||
|
containersFromConfig.mockResolvedValue(new Set());
|
||||||
|
state.containers = [];
|
||||||
|
state.docker.listServices.mockResolvedValue([
|
||||||
|
{ ID: "sid", Spec: { Name: "internal", Mode: { Replicated: { Replicas: "1" } } } },
|
||||||
|
]);
|
||||||
|
state.docker.listTasks.mockResolvedValue([{ ServiceID: "sid", Status: {} }]);
|
||||||
|
|
||||||
|
const req = { query: { server: "swarm" } };
|
||||||
|
const res = createMockRes();
|
||||||
|
|
||||||
|
await handler(req, res);
|
||||||
|
|
||||||
|
expect(res.body).toEqual({ statuses: {} });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("includes swarm services discovered through homepage labels", async () => {
|
||||||
|
state.dockerArgs.swarm = true;
|
||||||
|
hasHomepageLabels.mockImplementation((labels) => labels?.["homepage.name"] !== undefined);
|
||||||
|
state.containers = [];
|
||||||
|
state.docker.listServices.mockResolvedValue([
|
||||||
|
{
|
||||||
|
ID: "sid",
|
||||||
|
Spec: { Name: "api", Labels: { "homepage.name": "Api" }, Mode: { Replicated: { Replicas: "1" } } },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
state.docker.listTasks.mockResolvedValue([{ ServiceID: "sid", Status: {} }]);
|
||||||
|
|
||||||
|
const req = { query: { server: "swarm" } };
|
||||||
|
const res = createMockRes();
|
||||||
|
|
||||||
|
await handler(req, res);
|
||||||
|
|
||||||
|
expect(res.body).toEqual({ statuses: { api: { status: "running 1/1" } } });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns only listed containers when swarm queries fail", async () => {
|
||||||
|
state.dockerArgs.swarm = true;
|
||||||
|
containersFromConfig.mockResolvedValue(new Set(["web"]));
|
||||||
|
state.containers = [{ Names: ["/web"], Id: "cid1", State: "running", Status: "Up" }];
|
||||||
|
state.docker.listServices.mockRejectedValue(new Error("no services"));
|
||||||
|
state.docker.listTasks.mockRejectedValue(new Error("no tasks"));
|
||||||
|
|
||||||
|
const req = { query: { server: "swarm" } };
|
||||||
|
const res = createMockRes();
|
||||||
|
|
||||||
|
await handler(req, res);
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(200);
|
||||||
|
expect(res.body).toEqual({ statuses: { web: { status: "running" } } });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns 500 when docker returns a non-array containers payload", async () => {
|
||||||
|
state.containers = Buffer.from("bad");
|
||||||
|
|
||||||
|
const req = { query: { server: "local" } };
|
||||||
|
const res = createMockRes();
|
||||||
|
|
||||||
|
await handler(req, res);
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(500);
|
||||||
|
expect(res.body).toEqual({ error: "query failed" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("logs and returns 500 when the docker query throws", async () => {
|
||||||
|
getDockerArguments.mockImplementationOnce(() => {
|
||||||
|
throw new Error("boom");
|
||||||
|
});
|
||||||
|
|
||||||
|
const req = { query: { server: "local" } };
|
||||||
|
const res = createMockRes();
|
||||||
|
|
||||||
|
await handler(req, res);
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(500);
|
||||||
|
expect(res.body).toEqual({ error: { message: "boom" } });
|
||||||
|
expect(logger.error).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,26 +1,48 @@
|
|||||||
// @vitest-environment jsdom
|
// @vitest-environment jsdom
|
||||||
|
|
||||||
import { render, screen, waitFor } from "@testing-library/react";
|
import { render, screen, waitFor } from "@testing-library/react";
|
||||||
import { describe, expect, it, vi } from "vitest";
|
import { StrictMode } from "react";
|
||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
const { getSettingsMock } = vi.hoisted(() => ({
|
const { getSettingsMock, authOptionsMock, signInMock, replaceMock, routerQuery } = vi.hoisted(() => ({
|
||||||
getSettingsMock: vi.fn(),
|
getSettingsMock: vi.fn(),
|
||||||
|
authOptionsMock: vi.fn(),
|
||||||
|
signInMock: vi.fn(),
|
||||||
|
replaceMock: vi.fn(),
|
||||||
|
routerQuery: {},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("utils/config/config", () => ({
|
vi.mock("utils/config/config", () => ({
|
||||||
getSettings: getSettingsMock,
|
getSettings: getSettingsMock,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock("pages/api/auth/[...nextauth]", () => ({
|
||||||
|
get authOptions() {
|
||||||
|
return authOptionsMock();
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
vi.mock("next/router", () => ({
|
vi.mock("next/router", () => ({
|
||||||
useRouter: () => ({
|
useRouter: () => ({
|
||||||
query: {},
|
query: routerQuery,
|
||||||
|
replace: replaceMock,
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import { getProviders } from "next-auth/react";
|
vi.mock("next-auth/react", () => ({ signIn: signInMock }));
|
||||||
|
|
||||||
import SignInPage, { getServerSideProps } from "pages/auth/signin";
|
import SignInPage, { getServerSideProps } from "pages/auth/signin";
|
||||||
|
|
||||||
|
const OIDC_PROVIDERS = { "homepage-oidc": { id: "homepage-oidc", name: "Homepage OIDC", type: "oauth" } };
|
||||||
|
const SETTINGS = { theme: "dark", color: "slate", title: "Homepage" };
|
||||||
|
|
||||||
describe("pages/auth/signin", () => {
|
describe("pages/auth/signin", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
Object.keys(routerQuery).forEach((key) => delete routerQuery[key]);
|
||||||
|
window.sessionStorage.clear();
|
||||||
|
});
|
||||||
|
|
||||||
it("renders an error state when no providers are configured", async () => {
|
it("renders an error state when no providers are configured", async () => {
|
||||||
render(
|
render(
|
||||||
<SignInPage
|
<SignInPage
|
||||||
@@ -33,7 +55,7 @@ describe("pages/auth/signin", () => {
|
|||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.getByText("Authentication not configured")).toBeInTheDocument();
|
expect(screen.getByText("Authentication error")).toBeInTheDocument();
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(document.documentElement.classList.contains("dark")).toBe(true);
|
expect(document.documentElement.classList.contains("dark")).toBe(true);
|
||||||
@@ -60,8 +82,69 @@ describe("pages/auth/signin", () => {
|
|||||||
expect(screen.getByRole("button", { name: /login via oidc/i })).toBeInTheDocument();
|
expect(screen.getByRole("button", { name: /login via oidc/i })).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("redirects to the provider when auto-login is enabled", () => {
|
||||||
|
routerQuery.callbackUrl = "/some/page";
|
||||||
|
|
||||||
|
render(<SignInPage providers={OIDC_PROVIDERS} settings={SETTINGS} autoLogin />);
|
||||||
|
|
||||||
|
expect(signInMock).toHaveBeenCalledWith("homepage-oidc", { callbackUrl: "/some/page" });
|
||||||
|
expect(screen.getByText(/redirecting to homepage oidc/i)).toBeInTheDocument();
|
||||||
|
expect(screen.queryByRole("button", { name: /login via/i })).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not auto-login when the provider returned an error", () => {
|
||||||
|
routerQuery.error = "OAuthCallback";
|
||||||
|
|
||||||
|
render(<SignInPage providers={OIDC_PROVIDERS} settings={SETTINGS} autoLogin />);
|
||||||
|
|
||||||
|
expect(signInMock).not.toHaveBeenCalled();
|
||||||
|
expect(screen.getByRole("button", { name: /login via homepage oidc/i })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not auto-login when it is explicitly disabled in the url", () => {
|
||||||
|
routerQuery.autologin = "0";
|
||||||
|
|
||||||
|
render(<SignInPage providers={OIDC_PROVIDERS} settings={SETTINGS} autoLogin />);
|
||||||
|
|
||||||
|
expect(signInMock).not.toHaveBeenCalled();
|
||||||
|
expect(screen.getByRole("button", { name: /login via homepage oidc/i })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("redirects once under strict mode, rather than tripping its own loop guard", () => {
|
||||||
|
render(
|
||||||
|
<StrictMode>
|
||||||
|
<SignInPage providers={OIDC_PROVIDERS} settings={SETTINGS} autoLogin />
|
||||||
|
</StrictMode>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(signInMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(replaceMock).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("stops auto-login and hands back the page when the session never sticks", () => {
|
||||||
|
routerQuery.callbackUrl = "/some/page";
|
||||||
|
|
||||||
|
render(<SignInPage providers={OIDC_PROVIDERS} settings={SETTINGS} autoLogin />);
|
||||||
|
render(<SignInPage providers={OIDC_PROVIDERS} settings={SETTINGS} autoLogin />);
|
||||||
|
|
||||||
|
expect(signInMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(replaceMock).toHaveBeenCalledWith("/auth/signin?autologin=0&callbackUrl=%2Fsome%2Fpage");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not auto-login the password provider", () => {
|
||||||
|
render(
|
||||||
|
<SignInPage
|
||||||
|
providers={{ credentials: { id: "credentials", name: "Password", type: "credentials" } }}
|
||||||
|
settings={SETTINGS}
|
||||||
|
autoLogin
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(signInMock).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it("getServerSideProps returns providers and only public sign-in settings", async () => {
|
it("getServerSideProps returns providers and only public sign-in settings", async () => {
|
||||||
getProviders.mockResolvedValueOnce({ foo: { id: "foo", name: "Foo" } });
|
authOptionsMock.mockReturnValueOnce({ providers: [{ id: "foo", name: "Foo", type: "oauth" }] });
|
||||||
getSettingsMock.mockReturnValueOnce({
|
getSettingsMock.mockReturnValueOnce({
|
||||||
theme: "dark",
|
theme: "dark",
|
||||||
color: "slate",
|
color: "slate",
|
||||||
@@ -79,11 +162,11 @@ describe("pages/auth/signin", () => {
|
|||||||
|
|
||||||
const res = await getServerSideProps({});
|
const res = await getServerSideProps({});
|
||||||
|
|
||||||
expect(getProviders).toHaveBeenCalled();
|
|
||||||
expect(getSettingsMock).toHaveBeenCalled();
|
expect(getSettingsMock).toHaveBeenCalled();
|
||||||
expect(res).toEqual({
|
expect(res).toEqual({
|
||||||
props: {
|
props: {
|
||||||
providers: { foo: { id: "foo", name: "Foo" } },
|
autoLogin: false,
|
||||||
|
providers: { foo: { id: "foo", name: "Foo", type: "oauth" } },
|
||||||
settings: {
|
settings: {
|
||||||
theme: "dark",
|
theme: "dark",
|
||||||
color: "slate",
|
color: "slate",
|
||||||
@@ -96,4 +179,64 @@ describe("pages/auth/signin", () => {
|
|||||||
expect(res.props.settings).not.toHaveProperty("providers");
|
expect(res.props.settings).not.toHaveProperty("providers");
|
||||||
expect(res.props.settings).not.toHaveProperty("layout");
|
expect(res.props.settings).not.toHaveProperty("layout");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("getServerSideProps enables auto-login from the environment", async () => {
|
||||||
|
authOptionsMock.mockReturnValueOnce({ providers: [] });
|
||||||
|
getSettingsMock.mockReturnValueOnce({ theme: "dark" });
|
||||||
|
vi.stubEnv("HOMEPAGE_OIDC_AUTO_LOGIN", "true");
|
||||||
|
|
||||||
|
const res = await getServerSideProps({});
|
||||||
|
|
||||||
|
expect(res.props.autoLogin).toBe(true);
|
||||||
|
vi.unstubAllEnvs();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("getServerSideProps falls back to no providers when auth options fail to load", async () => {
|
||||||
|
const consoleError = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||||
|
authOptionsMock.mockImplementationOnce(() => {
|
||||||
|
throw new Error("Homepage auth is enabled but HOMEPAGE_EXTERNAL_URL (or NEXTAUTH_URL) is missing.");
|
||||||
|
});
|
||||||
|
getSettingsMock.mockReturnValueOnce({ theme: "dark" });
|
||||||
|
|
||||||
|
const res = await getServerSideProps({});
|
||||||
|
|
||||||
|
expect(res.props.providers).toEqual({});
|
||||||
|
expect(consoleError).toHaveBeenCalled();
|
||||||
|
consoleError.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("getServerSideProps passes only id, name and type from each provider", async () => {
|
||||||
|
authOptionsMock.mockReturnValueOnce({
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
id: "homepage-oidc",
|
||||||
|
name: "Homepage OIDC",
|
||||||
|
type: "oauth",
|
||||||
|
issuer: "https://oidc.example",
|
||||||
|
clientId: "canary-client-id",
|
||||||
|
clientSecret: "canary-client-secret",
|
||||||
|
wellKnown: "https://oidc.example/.well-known/openid-configuration",
|
||||||
|
authorization: { params: { scope: "openid email profile" } },
|
||||||
|
profile: () => ({}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "credentials",
|
||||||
|
name: "Password",
|
||||||
|
type: "credentials",
|
||||||
|
credentials: { password: { label: "Password", type: "password" } },
|
||||||
|
authorize: () => null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
getSettingsMock.mockReturnValueOnce({ theme: "dark" });
|
||||||
|
|
||||||
|
const res = await getServerSideProps({});
|
||||||
|
|
||||||
|
expect(res.props.providers).toEqual({
|
||||||
|
"homepage-oidc": { id: "homepage-oidc", name: "Homepage OIDC", type: "oauth" },
|
||||||
|
credentials: { id: "credentials", name: "Password", type: "credentials" },
|
||||||
|
});
|
||||||
|
// These props get serialized into the sign-in page, which is unauthenticated
|
||||||
|
expect(JSON.stringify(res.props)).not.toMatch(/canary-client-secret|canary-client-id|oidc\.example/);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// @vitest-environment jsdom
|
// @vitest-environment jsdom
|
||||||
|
|
||||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
import { ColorContext } from "utils/contexts/color";
|
import { ColorContext } from "utils/contexts/color";
|
||||||
@@ -25,6 +25,7 @@ const {
|
|||||||
throwIn: null,
|
throwIn: null,
|
||||||
validateData: [],
|
validateData: [],
|
||||||
hashData: null,
|
hashData: null,
|
||||||
|
hashConfig: null,
|
||||||
mutateHash: vi.fn(),
|
mutateHash: vi.fn(),
|
||||||
servicesData: [],
|
servicesData: [],
|
||||||
bookmarksData: [],
|
bookmarksData: [],
|
||||||
@@ -59,9 +60,12 @@ const {
|
|||||||
const serverSideTranslations = vi.fn(async (language) => ({ _translations: language }));
|
const serverSideTranslations = vi.fn(async (language) => ({ _translations: language }));
|
||||||
const logger = { error: vi.fn() };
|
const logger = { error: vi.fn() };
|
||||||
|
|
||||||
const useSWR = vi.fn((key) => {
|
const useSWR = vi.fn((key, config) => {
|
||||||
if (key === "/api/validate") return { data: state.validateData };
|
if (key === "/api/validate") return { data: state.validateData };
|
||||||
if (key === "/api/hash") return { data: state.hashData, mutate: state.mutateHash };
|
if (key === "/api/hash") {
|
||||||
|
state.hashConfig = config;
|
||||||
|
return { data: state.hashData, mutate: state.mutateHash };
|
||||||
|
}
|
||||||
if (key === "/api/services") return { data: state.servicesData };
|
if (key === "/api/services") return { data: state.servicesData };
|
||||||
if (key === "/api/bookmarks") return { data: state.bookmarksData };
|
if (key === "/api/bookmarks") return { data: state.bookmarksData };
|
||||||
if (key === "/api/widgets") return { data: state.widgetsData };
|
if (key === "/api/widgets") return { data: state.widgetsData };
|
||||||
@@ -172,6 +176,7 @@ describe("pages/index getStaticProps", () => {
|
|||||||
state.throwIn = null;
|
state.throwIn = null;
|
||||||
state.validateData = [];
|
state.validateData = [];
|
||||||
state.hashData = null;
|
state.hashData = null;
|
||||||
|
state.hashConfig = null;
|
||||||
state.servicesData = [];
|
state.servicesData = [];
|
||||||
state.bookmarksData = [];
|
state.bookmarksData = [];
|
||||||
state.widgetsData = [];
|
state.widgetsData = [];
|
||||||
@@ -351,6 +356,7 @@ describe("pages/index Index routing + SWR branches", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await renderIndex({ initialSettings: { title: "Homepage", layout: {} }, settings: { layout: {} } });
|
await renderIndex({ initialSettings: { title: "Homepage", layout: {} }, settings: { layout: {} } });
|
||||||
|
act(() => state.hashConfig.onSuccess(state.hashData));
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(fetchSpy).toHaveBeenCalledWith("/api/revalidate");
|
expect(fetchSpy).toHaveBeenCalledWith("/api/revalidate");
|
||||||
@@ -379,11 +385,24 @@ describe("pages/index Index routing + SWR branches", () => {
|
|||||||
localStorage.removeItem("hash");
|
localStorage.removeItem("hash");
|
||||||
|
|
||||||
await renderIndex({ initialSettings: { title: "Homepage", layout: {} }, settings: { layout: {} } });
|
await renderIndex({ initialSettings: { title: "Homepage", layout: {} }, settings: { layout: {} } });
|
||||||
|
act(() => state.hashConfig.onSuccess(state.hashData));
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(localStorage.getItem("hash")).toBe("first-hash");
|
expect(localStorage.getItem("hash")).toBe("first-hash");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("ignores a hash response without a hash", async () => {
|
||||||
|
state.validateData = [];
|
||||||
|
localStorage.setItem("hash", "old-hash");
|
||||||
|
|
||||||
|
await renderIndex({ initialSettings: { title: "Homepage", layout: {} }, settings: { layout: {} } });
|
||||||
|
|
||||||
|
expect(() => act(() => state.hashConfig.onSuccess(null))).not.toThrow();
|
||||||
|
expect(() => act(() => state.hashConfig.onSuccess({}))).not.toThrow();
|
||||||
|
expect(localStorage.getItem("hash")).toBe("old-hash");
|
||||||
|
expect(document.querySelector(".animate-spin")).toBeFalsy();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("pages/index Home behavior", () => {
|
describe("pages/index Home behavior", () => {
|
||||||
@@ -419,11 +438,27 @@ describe("pages/index Home behavior", () => {
|
|||||||
|
|
||||||
fireEvent.keyDown(document.body, { key: "a" });
|
fireEvent.keyDown(document.body, { key: "a" });
|
||||||
expect(screen.getByTestId("quicklaunch")).toHaveTextContent("open:3");
|
expect(screen.getByTestId("quicklaunch")).toHaveTextContent("open:3");
|
||||||
|
expect(state.quickLaunchProps.searchString).toBe("a");
|
||||||
|
|
||||||
fireEvent.keyDown(document.body, { key: "Escape" });
|
fireEvent.keyDown(document.body, { key: "Escape" });
|
||||||
expect(screen.getByTestId("quicklaunch")).toHaveTextContent("closed:3");
|
expect(screen.getByTestId("quicklaunch")).toHaveTextContent("closed:3");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("opens search on space without seeding the query with whitespace", async () => {
|
||||||
|
await renderIndex({
|
||||||
|
initialSettings: { title: "Homepage", layout: {} },
|
||||||
|
settings: { title: "Homepage", layout: {}, language: "en" },
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(state.quickLaunchProps).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
fireEvent.keyDown(document.body, { key: " " });
|
||||||
|
expect(screen.getByTestId("quicklaunch")).toHaveTextContent("open:3");
|
||||||
|
expect(state.quickLaunchProps.searchString).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
it("renders services and bookmark groups when present", async () => {
|
it("renders services and bookmark groups when present", async () => {
|
||||||
await renderIndex({
|
await renderIndex({
|
||||||
initialSettings: { title: "Homepage", layout: {} },
|
initialSettings: { title: "Homepage", layout: {} },
|
||||||
@@ -489,7 +524,7 @@ describe("pages/index Home behavior", () => {
|
|||||||
state.widgetsData = [];
|
state.widgetsData = [];
|
||||||
|
|
||||||
const { setTheme, setColor, setSettings } = await renderIndex({
|
const { setTheme, setColor, setSettings } = await renderIndex({
|
||||||
initialSettings: { title: "Homepage", layout: {} },
|
initialSettings: { title: "Homepage", layout: {}, favicon: "/x.ico" },
|
||||||
settings: {
|
settings: {
|
||||||
title: "Homepage",
|
title: "Homepage",
|
||||||
layout: {},
|
layout: {},
|
||||||
@@ -498,7 +533,6 @@ describe("pages/index Home behavior", () => {
|
|||||||
color: "emerald",
|
color: "emerald",
|
||||||
disableIndexing: true,
|
disableIndexing: true,
|
||||||
base: "/base/",
|
base: "/base/",
|
||||||
favicon: "/x.ico",
|
|
||||||
},
|
},
|
||||||
theme: "dark",
|
theme: "dark",
|
||||||
color: "slate",
|
color: "slate",
|
||||||
@@ -516,6 +550,20 @@ describe("pages/index Home behavior", () => {
|
|||||||
expect(document.querySelector('link[rel="icon"]')?.getAttribute("href")).toBe("/x.ico");
|
expect(document.querySelector('link[rel="icon"]')?.getAttribute("href")).toBe("/x.ico");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Safari reads the head without running JS, so the favicon has to be there before the
|
||||||
|
// settings context is populated, and mask-icon must not be
|
||||||
|
it("renders a custom favicon before the settings context is populated", async () => {
|
||||||
|
await renderIndex({
|
||||||
|
initialSettings: { title: "Homepage", layout: {}, favicon: "/x.ico" },
|
||||||
|
settings: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(document.querySelector('link[rel="icon"]')?.getAttribute("href")).toBe("/x.ico");
|
||||||
|
expect(document.querySelector('link[rel="apple-touch-icon"]')?.getAttribute("href")).toBe("/x.ico");
|
||||||
|
expect(document.querySelector('link[rel="mask-icon"]')).toBeNull();
|
||||||
|
expect(document.querySelector('link[rel="shortcut icon"]')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
it("marks information widgets as right-aligned for known widget types", async () => {
|
it("marks information widgets as right-aligned for known widget types", async () => {
|
||||||
await renderIndex({
|
await renderIndex({
|
||||||
initialSettings: { title: "Homepage", layout: {} },
|
initialSettings: { title: "Homepage", layout: {} },
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { Disclosure, Transition } from "@headlessui/react";
|
import { Disclosure, Transition } from "@headlessui/react";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
import { MdKeyboardArrowDown } from "react-icons/md";
|
||||||
|
|
||||||
import List from "components/bookmarks/list";
|
import List from "components/bookmarks/list";
|
||||||
import ErrorBoundary from "components/errorboundry";
|
import ErrorBoundary from "components/errorboundry";
|
||||||
import ResolvedIcon from "components/resolvedicon";
|
import ResolvedIcon from "components/resolvedicon";
|
||||||
import { useEffect, useRef } from "react";
|
|
||||||
import { MdKeyboardArrowDown } from "react-icons/md";
|
|
||||||
|
|
||||||
export default function BookmarksGroup({
|
export default function BookmarksGroup({
|
||||||
bookmarks,
|
bookmarks,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import ResolvedIcon from "components/resolvedicon";
|
|
||||||
import { useContext } from "react";
|
import { useContext } from "react";
|
||||||
|
|
||||||
|
import ResolvedIcon from "components/resolvedicon";
|
||||||
import { SettingsContext } from "utils/contexts/settings";
|
import { SettingsContext } from "utils/contexts/settings";
|
||||||
|
|
||||||
export default function Item({ bookmark, iconOnly = false }) {
|
export default function Item({ bookmark, iconOnly = false }) {
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import Item from "components/bookmarks/item";
|
|
||||||
|
|
||||||
import { columnMap } from "../../utils/layout/columns";
|
import { columnMap } from "../../utils/layout/columns";
|
||||||
|
|
||||||
|
import Item from "components/bookmarks/item";
|
||||||
|
|
||||||
export default function List({ bookmarks, layout, bookmarksStyle }) {
|
export default function List({ bookmarks, layout, bookmarksStyle }) {
|
||||||
let classes = layout?.style === "row" ? `grid ${columnMap[layout?.columns]} gap-x-2` : "flex flex-col bookmark-list";
|
let classes = layout?.style === "row" ? `grid ${columnMap[layout?.columns]} gap-x-2` : "flex flex-col bookmark-list";
|
||||||
const style = {};
|
const style = {};
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ export default class ErrorBoundary extends React.Component {
|
|||||||
|
|
||||||
// You can also log error messages to an error reporting service here
|
// You can also log error messages to an error reporting service here
|
||||||
if (error || errorInfo) {
|
if (error || errorInfo) {
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.error("component error: %s, info: %s", error, errorInfo);
|
console.error("component error: %s, info: %s", error, errorInfo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
/* eslint-disable @next/next/no-img-element */
|
/* eslint-disable @next/next/no-img-element */
|
||||||
/* eslint-disable jsx-a11y/alt-text */
|
/* eslint-disable jsx-a11y/alt-text */
|
||||||
import { useContext, useEffect, useRef } from "react";
|
import { useContext, useEffect, useRef } from "react";
|
||||||
import { ColorContext } from "utils/contexts/color";
|
|
||||||
|
|
||||||
|
import { ColorContext } from "utils/contexts/color";
|
||||||
import themes from "utils/styles/themes";
|
import themes from "utils/styles/themes";
|
||||||
|
|
||||||
export function Svg({ svgRef = null }) {
|
export function Svg({ svgRef = null }) {
|
||||||
|
|||||||
+115
-95
@@ -3,11 +3,12 @@ import { useTranslation } from "next-i18next/pages";
|
|||||||
import { useCallback, useContext, useEffect, useRef, useState } from "react";
|
import { useCallback, useContext, useEffect, useRef, useState } from "react";
|
||||||
import { FiSearch } from "react-icons/fi";
|
import { FiSearch } from "react-icons/fi";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { SettingsContext } from "utils/contexts/settings";
|
|
||||||
|
|
||||||
import ResolvedIcon from "./resolvedicon";
|
import ResolvedIcon from "./resolvedicon";
|
||||||
import { getStoredProvider, searchProviders } from "./widgets/search/search";
|
import { getStoredProvider, searchProviders } from "./widgets/search/search";
|
||||||
|
|
||||||
|
import { SettingsContext } from "utils/contexts/settings";
|
||||||
|
|
||||||
const MOBILE_BUTTON_POSITIONS = {
|
const MOBILE_BUTTON_POSITIONS = {
|
||||||
"top-left": "top-4 left-4",
|
"top-left": "top-4 left-4",
|
||||||
"top-right": "top-4 right-4",
|
"top-right": "top-4 right-4",
|
||||||
@@ -15,6 +16,69 @@ const MOBILE_BUTTON_POSITIONS = {
|
|||||||
"bottom-right": "bottom-4 right-4",
|
"bottom-right": "bottom-4 right-4",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function parseUrl(searchString) {
|
||||||
|
if (!/.+[.:].+/.test(searchString)) return null; // basic test for probably a url
|
||||||
|
|
||||||
|
try {
|
||||||
|
return new URL(searchString.toLowerCase().startsWith("http") ? searchString : `https://${searchString}`);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSearchResults({
|
||||||
|
hideVisitURL,
|
||||||
|
searchDescriptions,
|
||||||
|
searchProvider,
|
||||||
|
searchString,
|
||||||
|
searchSuggestions,
|
||||||
|
servicesAndBookmarks,
|
||||||
|
t,
|
||||||
|
url,
|
||||||
|
}) {
|
||||||
|
if (searchString.trim().length === 0) return [];
|
||||||
|
|
||||||
|
const results = servicesAndBookmarks.flatMap((result) => {
|
||||||
|
const nameMatch = result.name.toLowerCase().includes(searchString);
|
||||||
|
const descriptionMatch = searchDescriptions && result.description?.toLowerCase().includes(searchString);
|
||||||
|
|
||||||
|
if (!nameMatch && !descriptionMatch) return [];
|
||||||
|
return [{ ...result, ...(searchDescriptions && { priority: nameMatch ? 2 : +descriptionMatch }) }];
|
||||||
|
});
|
||||||
|
|
||||||
|
if (searchDescriptions) {
|
||||||
|
results.sort((a, b) => b.priority - a.priority);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchProvider) {
|
||||||
|
results.push({
|
||||||
|
href: searchProvider.url + encodeURIComponent(searchString),
|
||||||
|
name: `${searchProvider.name ?? t("quicklaunch.custom")} ${t("quicklaunch.search")}`,
|
||||||
|
type: "search",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (searchProvider.showSearchSuggestions && searchProvider.suggestionUrl && searchSuggestions[1]) {
|
||||||
|
results.push(
|
||||||
|
...searchSuggestions[1].map((suggestion) => ({
|
||||||
|
href: searchProvider.url + encodeURIComponent(suggestion),
|
||||||
|
name: suggestion,
|
||||||
|
type: "searchSuggestion",
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hideVisitURL && url) {
|
||||||
|
results.unshift({
|
||||||
|
href: url.toString(),
|
||||||
|
name: `${t("quicklaunch.visit")} URL`,
|
||||||
|
type: "url",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
export default function QuickLaunch({ servicesAndBookmarks, searchString, setSearchString, isOpen, setSearching }) {
|
export default function QuickLaunch({ servicesAndBookmarks, searchString, setSearchString, isOpen, setSearching }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -23,9 +87,8 @@ export default function QuickLaunch({ servicesAndBookmarks, searchString, setSea
|
|||||||
|
|
||||||
const searchField = useRef();
|
const searchField = useRef();
|
||||||
|
|
||||||
const [results, setResults] = useState([]);
|
|
||||||
const [currentItemIndex, setCurrentItemIndex] = useState(null);
|
const [currentItemIndex, setCurrentItemIndex] = useState(null);
|
||||||
const [url, setUrl] = useState(null);
|
const url = parseUrl(searchString);
|
||||||
const [searchSuggestions, setSearchSuggestions] = useState([]);
|
const [searchSuggestions, setSearchSuggestions] = useState([]);
|
||||||
|
|
||||||
const { data: widgets } = useSWR("/api/widgets");
|
const { data: widgets } = useSWR("/api/widgets");
|
||||||
@@ -61,8 +124,8 @@ export default function QuickLaunch({ servicesAndBookmarks, searchString, setSea
|
|||||||
? MOBILE_BUTTON_POSITIONS[settings.quicklaunch.mobileButtonPosition]
|
? MOBILE_BUTTON_POSITIONS[settings.quicklaunch.mobileButtonPosition]
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
function openCurrentItem(newWindow) {
|
function openCurrentItem(newWindow, index = activeItemIndex) {
|
||||||
const result = results[currentItemIndex];
|
const result = results[index];
|
||||||
window.open(
|
window.open(
|
||||||
result.href,
|
result.href,
|
||||||
newWindow ? "_blank" : (result.target ?? searchProvider?.target ?? settings.target ?? "_blank"),
|
newWindow ? "_blank" : (result.target ?? searchProvider?.target ?? settings.target ?? "_blank"),
|
||||||
@@ -81,17 +144,9 @@ export default function QuickLaunch({ servicesAndBookmarks, searchString, setSea
|
|||||||
|
|
||||||
function handleSearchChange(event) {
|
function handleSearchChange(event) {
|
||||||
const rawSearchString = event.target.value;
|
const rawSearchString = event.target.value;
|
||||||
try {
|
setCurrentItemIndex(null);
|
||||||
if (!/.+[.:].+/g.test(rawSearchString)) throw new Error(); // basic test for probably a url
|
// urls keep their casing, everything else is lowercased for matching
|
||||||
let urlString = rawSearchString;
|
setSearchString(parseUrl(rawSearchString) ? rawSearchString : rawSearchString.toLowerCase());
|
||||||
if (urlString.toLowerCase().indexOf("http") !== 0) urlString = `https://${rawSearchString}`;
|
|
||||||
setUrl(new URL(urlString)); // basic validation
|
|
||||||
setSearchString(rawSearchString);
|
|
||||||
return;
|
|
||||||
} catch (e) {
|
|
||||||
setUrl(null);
|
|
||||||
}
|
|
||||||
setSearchString(rawSearchString.toLowerCase());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSearchKeyDown(event) {
|
function handleSearchKeyDown(event) {
|
||||||
@@ -103,18 +158,19 @@ export default function QuickLaunch({ servicesAndBookmarks, searchString, setSea
|
|||||||
} else if (event.key === "Enter" && results.length) {
|
} else if (event.key === "Enter" && results.length) {
|
||||||
closeAndReset();
|
closeAndReset();
|
||||||
openCurrentItem(event.metaKey);
|
openCurrentItem(event.metaKey);
|
||||||
} else if (event.key === "ArrowDown" && results[currentItemIndex + 1]) {
|
} else if (event.key === "ArrowDown" && results[activeItemIndex + 1]) {
|
||||||
setCurrentItemIndex(currentItemIndex + 1);
|
setCurrentItemIndex(activeItemIndex + 1);
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
} else if (event.key === "ArrowUp" && currentItemIndex > 0) {
|
} else if (event.key === "ArrowUp" && activeItemIndex > 0) {
|
||||||
setCurrentItemIndex(currentItemIndex - 1);
|
setCurrentItemIndex(activeItemIndex - 1);
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
} else if (
|
} else if (
|
||||||
event.key === "ArrowRight" &&
|
event.key === "ArrowRight" &&
|
||||||
results[currentItemIndex] &&
|
results[activeItemIndex] &&
|
||||||
results[currentItemIndex].type === "searchSuggestion"
|
results[activeItemIndex].type === "searchSuggestion"
|
||||||
) {
|
) {
|
||||||
setSearchString(results[currentItemIndex].name);
|
setCurrentItemIndex(null);
|
||||||
|
setSearchString(results[activeItemIndex].name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,7 +180,8 @@ export default function QuickLaunch({ servicesAndBookmarks, searchString, setSea
|
|||||||
|
|
||||||
function handleItemClick(event) {
|
function handleItemClick(event) {
|
||||||
closeAndReset();
|
closeAndReset();
|
||||||
openCurrentItem(event.metaKey);
|
// in case hover doesnt fire, use the clicked item, not the highlighted one
|
||||||
|
openCurrentItem(event.metaKey, parseInt(event.currentTarget.dataset.index, 10));
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleItemKeyDown(event) {
|
function handleItemKeyDown(event) {
|
||||||
@@ -138,33 +195,17 @@ export default function QuickLaunch({ servicesAndBookmarks, searchString, setSea
|
|||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
searchString.trim().length === 0 ||
|
||||||
|
!searchProvider?.showSearchSuggestions ||
|
||||||
|
!searchProvider.suggestionUrl ||
|
||||||
|
searchString.trim() === searchSuggestions[0]?.trim()
|
||||||
|
) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
|
|
||||||
if (searchString.trim().length === 0) setResults([]);
|
|
||||||
else {
|
|
||||||
let newResults = servicesAndBookmarks.filter((r) => {
|
|
||||||
const nameMatch = r.name.toLowerCase().includes(searchString);
|
|
||||||
let descriptionMatch;
|
|
||||||
if (searchDescriptions) {
|
|
||||||
descriptionMatch = r.description?.toLowerCase().includes(searchString);
|
|
||||||
r.priority = nameMatch ? 2 * +nameMatch : +descriptionMatch; // eslint-disable-line no-param-reassign
|
|
||||||
}
|
|
||||||
return nameMatch || descriptionMatch;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (searchDescriptions) {
|
|
||||||
newResults = newResults.sort((a, b) => b.priority - a.priority);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (searchProvider) {
|
|
||||||
newResults.push({
|
|
||||||
href: searchProvider.url + encodeURIComponent(searchString),
|
|
||||||
name: `${searchProvider.name ?? t("quicklaunch.custom")} ${t("quicklaunch.search")}`,
|
|
||||||
type: "search",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (searchProvider.showSearchSuggestions && searchProvider.suggestionUrl) {
|
|
||||||
if (searchString.trim() !== searchSuggestions[0]?.trim()) {
|
|
||||||
fetch(
|
fetch(
|
||||||
`/api/search/searchSuggestion?query=${encodeURIComponent(searchString)}&providerName=${
|
`/api/search/searchSuggestion?query=${encodeURIComponent(searchString)}&providerName=${
|
||||||
searchProvider.name ?? "Custom"
|
searchProvider.name ?? "Custom"
|
||||||
@@ -175,51 +216,30 @@ export default function QuickLaunch({ servicesAndBookmarks, searchString, setSea
|
|||||||
const newSearchSuggestions = await searchSuggestionResult.json();
|
const newSearchSuggestions = await searchSuggestionResult.json();
|
||||||
|
|
||||||
if (newSearchSuggestions) {
|
if (newSearchSuggestions) {
|
||||||
if (newSearchSuggestions[1].length > 4) {
|
setCurrentItemIndex(null);
|
||||||
newSearchSuggestions[1] = newSearchSuggestions[1].splice(0, 4);
|
setSearchSuggestions([newSearchSuggestions[0], newSearchSuggestions[1].slice(0, 4)]);
|
||||||
}
|
|
||||||
setSearchSuggestions(newSearchSuggestions);
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
// If there is an error, just ignore it. There just will be no search suggestions.
|
// If there is an error, just ignore it. There just will be no search suggestions.
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
if (searchSuggestions[1]) {
|
return () => abortController.abort();
|
||||||
newResults = newResults.concat(
|
}, [searchProvider, searchString, searchSuggestions]);
|
||||||
searchSuggestions[1].map((suggestion) => ({
|
|
||||||
href: searchProvider.url + encodeURIComponent(suggestion),
|
|
||||||
name: suggestion,
|
|
||||||
type: "searchSuggestion",
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hideVisitURL && url) {
|
const results = getSearchResults({
|
||||||
newResults.unshift({
|
hideVisitURL,
|
||||||
href: url.toString(),
|
searchDescriptions,
|
||||||
name: `${t("quicklaunch.visit")} URL`,
|
searchProvider,
|
||||||
type: "url",
|
searchString,
|
||||||
|
searchSuggestions,
|
||||||
|
servicesAndBookmarks,
|
||||||
|
t,
|
||||||
|
url,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
setResults(newResults);
|
const activeItemIndex = currentItemIndex ?? (results.length ? 0 : null);
|
||||||
|
|
||||||
if (newResults.length) {
|
|
||||||
setCurrentItemIndex(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
abortController.abort();
|
|
||||||
};
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [searchString, servicesAndBookmarks, searchDescriptions, hideVisitURL, searchSuggestions, searchProvider, url]);
|
|
||||||
|
|
||||||
const [hidden, setHidden] = useState(true);
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function handleBackdropClick(event) {
|
function handleBackdropClick(event) {
|
||||||
if (event.target?.tagName === "DIV") closeAndReset();
|
if (event.target?.tagName === "DIV") closeAndReset();
|
||||||
@@ -228,14 +248,11 @@ export default function QuickLaunch({ servicesAndBookmarks, searchString, setSea
|
|||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
searchField.current.focus();
|
searchField.current.focus();
|
||||||
document.body.addEventListener("click", handleBackdropClick);
|
document.body.addEventListener("click", handleBackdropClick);
|
||||||
setHidden(false);
|
|
||||||
} else {
|
} else {
|
||||||
document.body.removeEventListener("click", handleBackdropClick);
|
|
||||||
searchField.current.blur();
|
searchField.current.blur();
|
||||||
setTimeout(() => {
|
|
||||||
setHidden(true);
|
|
||||||
}, 300); // disable on close
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return () => document.body.removeEventListener("click", handleBackdropClick);
|
||||||
}, [isOpen, closeAndReset]);
|
}, [isOpen, closeAndReset]);
|
||||||
|
|
||||||
function highlightText(text) {
|
function highlightText(text) {
|
||||||
@@ -244,7 +261,6 @@ export default function QuickLaunch({ servicesAndBookmarks, searchString, setSea
|
|||||||
<span>
|
<span>
|
||||||
{parts.map((part, i) =>
|
{parts.map((part, i) =>
|
||||||
part.toLowerCase() === searchString.toLowerCase() ? (
|
part.toLowerCase() === searchString.toLowerCase() ? (
|
||||||
// eslint-disable-next-line react/no-array-index-key
|
|
||||||
<span key={`${searchString}_${i}`} className="bg-theme-300/10">
|
<span key={`${searchString}_${i}`} className="bg-theme-300/10">
|
||||||
{part}
|
{part}
|
||||||
</span>
|
</span>
|
||||||
@@ -260,13 +276,17 @@ export default function QuickLaunch({ servicesAndBookmarks, searchString, setSea
|
|||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"relative z-40 ease-in-out duration-300 transition-opacity",
|
"relative z-40 ease-in-out",
|
||||||
hidden && !isOpen && "hidden",
|
isOpen ? "visible opacity-100" : "invisible opacity-0 pointer-events-none",
|
||||||
!hidden && isOpen && "opacity-100",
|
|
||||||
!isOpen && "opacity-0",
|
|
||||||
)}
|
)}
|
||||||
|
style={{
|
||||||
|
transitionProperty: "opacity, visibility",
|
||||||
|
transitionDuration: "300ms, 0s",
|
||||||
|
transitionDelay: isOpen ? "0s, 0s" : "0s, 300ms",
|
||||||
|
}}
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
|
aria-hidden={!isOpen}
|
||||||
>
|
>
|
||||||
<div className="fixed inset-0 bg-gray-500 opacity-50" />
|
<div className="fixed inset-0 bg-gray-500 opacity-50" />
|
||||||
<div className="fixed inset-0 z-20 overflow-y-auto">
|
<div className="fixed inset-0 z-20 overflow-y-auto">
|
||||||
@@ -298,7 +318,7 @@ export default function QuickLaunch({ servicesAndBookmarks, searchString, setSea
|
|||||||
onKeyDown={handleItemKeyDown}
|
onKeyDown={handleItemKeyDown}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"flex flex-row w-full items-center justify-between rounded-md text-sm md:text-xl py-2 px-4 cursor-pointer text-theme-700 dark:text-theme-200",
|
"flex flex-row w-full items-center justify-between rounded-md text-sm md:text-xl py-2 px-4 cursor-pointer text-theme-700 dark:text-theme-200",
|
||||||
i === currentItemIndex && "bg-theme-300/50 dark:bg-theme-700/50",
|
i === activeItemIndex && "bg-theme-300/50 dark:bg-theme-700/50",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex flex-row items-center mr-4 pointer-events-none">
|
<div className="flex flex-row items-center mr-4 pointer-events-none">
|
||||||
|
|||||||
@@ -46,6 +46,17 @@ function Wrapper({ servicesAndBookmarks = [], initialOpen = true } = {}) {
|
|||||||
const [isOpen, setSearching] = useState(initialOpen);
|
const [isOpen, setSearching] = useState(initialOpen);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
data-testid="seed-key"
|
||||||
|
onClick={() => {
|
||||||
|
setSearchString((current) => `${current}g`);
|
||||||
|
setSearching(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
seed
|
||||||
|
</button>
|
||||||
<QuickLaunch
|
<QuickLaunch
|
||||||
servicesAndBookmarks={servicesAndBookmarks}
|
servicesAndBookmarks={servicesAndBookmarks}
|
||||||
searchString={searchString}
|
searchString={searchString}
|
||||||
@@ -53,6 +64,7 @@ function Wrapper({ servicesAndBookmarks = [], initialOpen = true } = {}) {
|
|||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
setSearching={setSearching}
|
setSearching={setSearching}
|
||||||
/>
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,6 +163,35 @@ describe("components/quicklaunch", () => {
|
|||||||
openSpy.mockRestore();
|
openSpy.mockRestore();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not carry a previous url result into a search seeded by a keypress", async () => {
|
||||||
|
renderWithProviders(<Wrapper />, {
|
||||||
|
settings: {
|
||||||
|
target: "_self",
|
||||||
|
quicklaunch: {
|
||||||
|
provider: "duckduckgo",
|
||||||
|
showSearchSuggestions: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const input = screen.getByPlaceholderText("Search");
|
||||||
|
await waitFor(() => expect(input).toHaveFocus());
|
||||||
|
|
||||||
|
fireEvent.change(input, { target: { value: "example.com" } });
|
||||||
|
expect(await screen.findByText("quicklaunch.visit URL")).toBeInTheDocument();
|
||||||
|
|
||||||
|
fireEvent.keyDown(input, { key: "Escape" });
|
||||||
|
await act(async () => {
|
||||||
|
await new Promise((r) => setTimeout(r, 350));
|
||||||
|
});
|
||||||
|
|
||||||
|
// reopen by "typing" a character, the way pages/index does
|
||||||
|
fireEvent.click(screen.getByTestId("seed-key"));
|
||||||
|
|
||||||
|
expect(input).toHaveValue("g");
|
||||||
|
expect(screen.queryByText("quicklaunch.visit URL")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
it("closes on Escape and clears the search string after the timeout", async () => {
|
it("closes on Escape and clears the search string after the timeout", async () => {
|
||||||
renderWithProviders(<Wrapper />, {
|
renderWithProviders(<Wrapper />, {
|
||||||
settings: {
|
settings: {
|
||||||
@@ -222,6 +263,37 @@ describe("components/quicklaunch", () => {
|
|||||||
openSpy.mockRestore();
|
openSpy.mockRestore();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("opens the clicked result even without a preceding hover event", async () => {
|
||||||
|
const openSpy = vi.spyOn(window, "open").mockImplementation(() => null);
|
||||||
|
|
||||||
|
renderWithProviders(
|
||||||
|
<Wrapper
|
||||||
|
servicesAndBookmarks={[
|
||||||
|
{ name: "Alpha", href: "https://alpha.example" },
|
||||||
|
{ name: "Alpine", href: "https://alpine.example" },
|
||||||
|
{ name: "Almond", href: "https://almond.example" },
|
||||||
|
]}
|
||||||
|
/>,
|
||||||
|
{ settings: { target: "_self", quicklaunch: { showSearchSuggestions: false } } },
|
||||||
|
);
|
||||||
|
|
||||||
|
const input = screen.getByPlaceholderText("Search");
|
||||||
|
await waitFor(() => expect(input).toHaveFocus());
|
||||||
|
|
||||||
|
fireEvent.change(input, { target: { value: "al" } });
|
||||||
|
await waitFor(() => expect(document.querySelector('button[data-index="2"]')).toBeTruthy());
|
||||||
|
|
||||||
|
// touch devices don't fire mouseEnter, so the highlighted item is still the first one
|
||||||
|
fireEvent.click(document.querySelector('button[data-index="2"]'));
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await new Promise((r) => setTimeout(r, 350));
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(openSpy).toHaveBeenCalledWith("https://almond.example", "_self", "noreferrer");
|
||||||
|
openSpy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
it("handles Escape on a result button (not just the input)", async () => {
|
it("handles Escape on a result button (not just the input)", async () => {
|
||||||
renderWithProviders(<Wrapper servicesAndBookmarks={[{ name: "Alpha", href: "https://alpha.example" }]} />, {
|
renderWithProviders(<Wrapper servicesAndBookmarks={[{ name: "Alpha", href: "https://alpha.example" }]} />, {
|
||||||
settings: { quicklaunch: { showSearchSuggestions: false } },
|
settings: { quicklaunch: { showSearchSuggestions: false } },
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { useContext } from "react";
|
import { useContext } from "react";
|
||||||
|
|
||||||
import { SettingsContext } from "utils/contexts/settings";
|
import { SettingsContext } from "utils/contexts/settings";
|
||||||
import { ThemeContext } from "utils/contexts/theme";
|
import { ThemeContext } from "utils/contexts/theme";
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { Disclosure, Transition } from "@headlessui/react";
|
import { Disclosure, Transition } from "@headlessui/react";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import ResolvedIcon from "components/resolvedicon";
|
|
||||||
import List from "components/services/list";
|
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import { MdKeyboardArrowDown } from "react-icons/md";
|
import { MdKeyboardArrowDown } from "react-icons/md";
|
||||||
|
|
||||||
import { columnMap } from "../../utils/layout/columns";
|
import { columnMap } from "../../utils/layout/columns";
|
||||||
|
|
||||||
|
import ResolvedIcon from "components/resolvedicon";
|
||||||
|
import List from "components/services/list";
|
||||||
|
|
||||||
export default function ServicesGroup({
|
export default function ServicesGroup({
|
||||||
group,
|
group,
|
||||||
layout,
|
layout,
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import ResolvedIcon from "components/resolvedicon";
|
|
||||||
import { useContext, useState } from "react";
|
import { useContext, useState } from "react";
|
||||||
import { SettingsContext } from "utils/contexts/settings";
|
|
||||||
import Docker from "widgets/docker/component";
|
|
||||||
import Kubernetes from "widgets/kubernetes/component";
|
|
||||||
import ProxmoxVM from "widgets/proxmoxvm/component";
|
|
||||||
|
|
||||||
import KubernetesStatus from "./kubernetes-status";
|
import KubernetesStatus from "./kubernetes-status";
|
||||||
import Ping from "./ping";
|
import Ping from "./ping";
|
||||||
@@ -13,6 +8,12 @@ import SiteMonitor from "./site-monitor";
|
|||||||
import Status from "./status";
|
import Status from "./status";
|
||||||
import Widget from "./widget";
|
import Widget from "./widget";
|
||||||
|
|
||||||
|
import ResolvedIcon from "components/resolvedicon";
|
||||||
|
import { SettingsContext } from "utils/contexts/settings";
|
||||||
|
import Docker from "widgets/docker/component";
|
||||||
|
import Kubernetes from "widgets/kubernetes/component";
|
||||||
|
import ProxmoxVM from "widgets/proxmoxvm/component";
|
||||||
|
|
||||||
export default function Item({ service, groupName, useEqualHeights }) {
|
export default function Item({ service, groupName, useEqualHeights }) {
|
||||||
const hasLink = service.href && service.href !== "#";
|
const hasLink = service.href && service.href !== "#";
|
||||||
const { settings } = useContext(SettingsContext);
|
const { settings } = useContext(SettingsContext);
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import Item from "components/services/item";
|
|
||||||
|
|
||||||
import { columnMap } from "../../utils/layout/columns";
|
import { columnMap } from "../../utils/layout/columns";
|
||||||
|
|
||||||
|
import Item from "components/services/item";
|
||||||
|
|
||||||
export default function List({ groupName, services, layout, useEqualHeights, header }) {
|
export default function List({ groupName, services, layout, useEqualHeights, header }) {
|
||||||
return (
|
return (
|
||||||
<ul
|
<ul
|
||||||
|
|||||||
@@ -1,20 +1,38 @@
|
|||||||
import { useTranslation } from "next-i18next/pages";
|
import { useTranslation } from "next-i18next/pages";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
|
||||||
|
const WARNING_CLASS = "text-orange-400/50 dark:text-orange-400/80";
|
||||||
|
|
||||||
|
// docker container states other than running
|
||||||
|
const STATE_LABELS = {
|
||||||
|
created: "docker.created",
|
||||||
|
dead: "docker.dead",
|
||||||
|
exited: "docker.exited",
|
||||||
|
"not found": "docker.not_found",
|
||||||
|
paused: "docker.paused",
|
||||||
|
removing: "docker.removing",
|
||||||
|
restarting: "docker.restarting",
|
||||||
|
};
|
||||||
|
|
||||||
|
// states that mean something is wrong rather than merely not started
|
||||||
|
const WARNING_STATES = new Set(["dead", "exited", "not found", "restarting"]);
|
||||||
|
|
||||||
export default function Status({ service, style }) {
|
export default function Status({ service, style }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const { data, error } = useSWR(`/api/docker/status/${service.container}/${service.server || ""}`);
|
const { data: response, error } = useSWR(`/api/docker/statuses?server=${encodeURIComponent(service.server || "")}`);
|
||||||
|
const statusError = error ?? response?.error;
|
||||||
|
const { statuses } = response ?? {};
|
||||||
|
const data = statuses ? (statuses[service.container] ?? { status: "not found" }) : undefined;
|
||||||
|
|
||||||
let statusLabel = t("docker.unknown");
|
let statusLabel = t("docker.unknown");
|
||||||
let backgroundClass = "px-1.5 py-0.5 bg-theme-500/10 dark:bg-theme-900/50";
|
let backgroundClass = "px-1.5 py-0.5 bg-theme-500/10 dark:bg-theme-900/50";
|
||||||
let colorClass = "text-black/20 dark:text-white/40 ";
|
let colorClass = "text-black/20 dark:text-white/40 ";
|
||||||
|
|
||||||
if (error) {
|
if (statusError) {
|
||||||
statusLabel = t("docker.error");
|
statusLabel = t("docker.error");
|
||||||
colorClass = "text-rose-500/80";
|
colorClass = "text-rose-500/80";
|
||||||
} else if (data) {
|
} else if (data?.status?.includes("running")) {
|
||||||
if (data.status?.includes("running")) {
|
|
||||||
colorClass = "text-emerald-500/80";
|
colorClass = "text-emerald-500/80";
|
||||||
|
|
||||||
if (!data.health) {
|
if (!data.health) {
|
||||||
@@ -29,17 +47,15 @@ export default function Status({ service, style }) {
|
|||||||
|
|
||||||
if (data.health === "unhealthy") {
|
if (data.health === "unhealthy") {
|
||||||
statusLabel = t("docker.unhealthy");
|
statusLabel = t("docker.unhealthy");
|
||||||
colorClass = "text-orange-400/50 dark:text-orange-400/80";
|
colorClass = WARNING_CLASS;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
} else if (data?.status?.startsWith("partial")) {
|
||||||
|
statusLabel = data.status.replace("partial", t("docker.partial"));
|
||||||
if (data.status === "not found" || data.status === "exited" || data.status?.startsWith("partial")) {
|
colorClass = WARNING_CLASS;
|
||||||
if (data.status === "not found") statusLabel = t("docker.not_found");
|
} else if (data && STATE_LABELS[data.status]) {
|
||||||
else if (data.status === "exited") statusLabel = t("docker.exited");
|
statusLabel = t(STATE_LABELS[data.status]);
|
||||||
else statusLabel = data.status.replace("partial", t("docker.partial"));
|
if (WARNING_STATES.has(data.status)) colorClass = WARNING_CLASS;
|
||||||
colorClass = "text-orange-400/50 dark:text-orange-400/80";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (style === "dot") {
|
if (style === "dot") {
|
||||||
|
|||||||
@@ -21,8 +21,11 @@ describe("components/services/status", () => {
|
|||||||
|
|
||||||
render(<Status service={{ container: "c", server: "s" }} />);
|
render(<Status service={{ container: "c", server: "s" }} />);
|
||||||
|
|
||||||
expect(useSWR).toHaveBeenCalledWith("/api/docker/status/c/s");
|
expect(useSWR).toHaveBeenCalledWith("/api/docker/statuses?server=s");
|
||||||
expect(screen.getByText("docker.unknown")).toBeInTheDocument();
|
expect(screen.getByText("docker.unknown")).toBeInTheDocument();
|
||||||
|
|
||||||
|
render(<Status service={{ container: "c" }} />);
|
||||||
|
expect(useSWR).toHaveBeenCalledWith("/api/docker/statuses?server=");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders error when SWR fails", () => {
|
it("renders error when SWR fails", () => {
|
||||||
@@ -33,38 +36,87 @@ describe("components/services/status", () => {
|
|||||||
expect(screen.getByText("docker.error")).toBeInTheDocument();
|
expect(screen.getByText("docker.error")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("renders error when the api returns an error payload with a 200-parsed body", () => {
|
||||||
|
useSWR.mockReturnValue({ data: { error: "query failed" }, error: undefined });
|
||||||
|
|
||||||
|
render(<Status service={{ container: "c", server: "s" }} />);
|
||||||
|
|
||||||
|
expect(screen.getByText("docker.error")).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText("docker.not_found")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
it("renders healthy/unhealthy and partial/exited/not found statuses", () => {
|
it("renders healthy/unhealthy and partial/exited/not found statuses", () => {
|
||||||
useSWR.mockReturnValue({ data: { status: "running", health: "healthy" }, error: undefined });
|
useSWR.mockReturnValue({ data: { statuses: { c: { status: "running", health: "healthy" } } }, error: undefined });
|
||||||
render(<Status service={{ container: "c", server: "s" }} />);
|
render(<Status service={{ container: "c", server: "s" }} />);
|
||||||
expect(screen.getByText("docker.healthy")).toBeInTheDocument();
|
expect(screen.getByText("docker.healthy")).toBeInTheDocument();
|
||||||
|
|
||||||
useSWR.mockReturnValue({ data: { status: "running", health: "unhealthy" }, error: undefined });
|
useSWR.mockReturnValue({ data: { statuses: { c: { status: "running", health: "unhealthy" } } }, error: undefined });
|
||||||
render(<Status service={{ container: "c", server: "s" }} />);
|
render(<Status service={{ container: "c", server: "s" }} />);
|
||||||
expect(screen.getByText("docker.unhealthy")).toBeInTheDocument();
|
expect(screen.getByText("docker.unhealthy")).toBeInTheDocument();
|
||||||
|
|
||||||
useSWR.mockReturnValue({ data: { status: "partial 1/2" }, error: undefined });
|
useSWR.mockReturnValue({ data: { statuses: { c: { status: "partial 1/2" } } }, error: undefined });
|
||||||
render(<Status service={{ container: "c", server: "s" }} />);
|
render(<Status service={{ container: "c", server: "s" }} />);
|
||||||
expect(screen.getByText("docker.partial 1/2")).toBeInTheDocument();
|
expect(screen.getByText("docker.partial 1/2")).toBeInTheDocument();
|
||||||
|
|
||||||
useSWR.mockReturnValue({ data: { status: "exited" }, error: undefined });
|
useSWR.mockReturnValue({ data: { statuses: { c: { status: "exited" } } }, error: undefined });
|
||||||
render(<Status service={{ container: "c", server: "s" }} />);
|
render(<Status service={{ container: "c", server: "s" }} />);
|
||||||
expect(screen.getByText("docker.exited")).toBeInTheDocument();
|
expect(screen.getByText("docker.exited")).toBeInTheDocument();
|
||||||
|
|
||||||
useSWR.mockReturnValue({ data: { status: "not found" }, error: undefined });
|
useSWR.mockReturnValue({ data: { statuses: {} }, error: undefined });
|
||||||
render(<Status service={{ container: "c", server: "s" }} />);
|
render(<Status service={{ container: "c", server: "s" }} />);
|
||||||
expect(screen.getByText("docker.not_found")).toBeInTheDocument();
|
expect(screen.getByText("docker.not_found")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders starting health when container is running and starting", () => {
|
it("renders starting health when container is running and starting", () => {
|
||||||
useSWR.mockReturnValue({ data: { status: "running", health: "starting" }, error: undefined });
|
useSWR.mockReturnValue({ data: { statuses: { c: { status: "running", health: "starting" } } }, error: undefined });
|
||||||
|
|
||||||
render(<Status service={{ container: "c", server: "s" }} />);
|
render(<Status service={{ container: "c", server: "s" }} />);
|
||||||
|
|
||||||
expect(screen.getByText("docker.starting")).toBeInTheDocument();
|
expect(screen.getByText("docker.starting")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("renders the remaining container states instead of falling through to unknown", () => {
|
||||||
|
const states = {
|
||||||
|
created: "docker.created",
|
||||||
|
paused: "docker.paused",
|
||||||
|
restarting: "docker.restarting",
|
||||||
|
removing: "docker.removing",
|
||||||
|
dead: "docker.dead",
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.entries(states).forEach(([status, label]) => {
|
||||||
|
useSWR.mockReturnValue({ data: { statuses: { c: { status } } }, error: undefined });
|
||||||
|
render(<Status service={{ container: "c", server: "s" }} />);
|
||||||
|
expect(screen.getByText(label)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(screen.queryByText("docker.unknown")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("colors problem states as warnings and merely stopped states neutrally", () => {
|
||||||
|
useSWR.mockReturnValue({ data: { statuses: { c: { status: "restarting" } } }, error: undefined });
|
||||||
|
const { container: restarting } = render(<Status service={{ container: "c", server: "s" }} style="dot" />);
|
||||||
|
expect(restarting.querySelector(".bg-orange-400")).toBeInTheDocument();
|
||||||
|
|
||||||
|
useSWR.mockReturnValue({ data: { statuses: { c: { status: "paused" } } }, error: undefined });
|
||||||
|
const { container: paused } = render(<Status service={{ container: "c", server: "s" }} style="dot" />);
|
||||||
|
expect(paused.querySelector(".bg-orange-400")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not surface health for containers that are not running", () => {
|
||||||
|
useSWR.mockReturnValue({
|
||||||
|
data: { statuses: { c: { status: "paused", health: "unhealthy" } } },
|
||||||
|
error: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<Status service={{ container: "c", server: "s" }} />);
|
||||||
|
|
||||||
|
expect(screen.getByText("docker.paused")).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText("docker.unhealthy")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
it("renders a dot when style is dot", () => {
|
it("renders a dot when style is dot", () => {
|
||||||
useSWR.mockReturnValue({ data: { status: "running" }, error: undefined });
|
useSWR.mockReturnValue({ data: { statuses: { c: { status: "running" } } }, error: undefined });
|
||||||
|
|
||||||
const { container } = render(<Status service={{ container: "c", server: "s" }} style="dot" />);
|
const { container } = render(<Status service={{ container: "c", server: "s" }} style="dot" />);
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import ErrorBoundary from "components/errorboundry";
|
|
||||||
import { useTranslation } from "next-i18next/pages";
|
import { useTranslation } from "next-i18next/pages";
|
||||||
|
|
||||||
|
import ErrorBoundary from "components/errorboundry";
|
||||||
import components from "widgets/components";
|
import components from "widgets/components";
|
||||||
|
|
||||||
export default function Widget({ widget, service }) {
|
export default function Widget({ widget, service }) {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { useContext, useMemo } from "react";
|
import { useContext, useMemo } from "react";
|
||||||
import { SettingsContext } from "utils/contexts/settings";
|
|
||||||
|
|
||||||
import Error from "./error";
|
import Error from "./error";
|
||||||
import { BlockHighlightContext } from "./highlight-context";
|
import { BlockHighlightContext } from "./highlight-context";
|
||||||
|
|
||||||
|
import { SettingsContext } from "utils/contexts/settings";
|
||||||
import { buildHighlightConfig } from "utils/highlights";
|
import { buildHighlightConfig } from "utils/highlights";
|
||||||
|
|
||||||
const ALIASED_WIDGETS = {
|
const ALIASED_WIDGETS = {
|
||||||
|
|||||||
@@ -13,13 +13,13 @@ export default function Error({ error }) {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
if (typeof error === "string") {
|
if (typeof error === "string") {
|
||||||
error = { message: error }; // eslint-disable-line no-param-reassign
|
error = { message: error };
|
||||||
} else if (typeof error === "number") {
|
} else if (typeof error === "number") {
|
||||||
error = { message: `Error ${error}` }; // eslint-disable-line no-param-reassign
|
error = { message: `Error ${error}` };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error?.data?.error) {
|
if (error?.data?.error) {
|
||||||
error = error.data.error; // eslint-disable-line no-param-reassign
|
error = error.data.error;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { useContext } from "react";
|
import { useContext } from "react";
|
||||||
|
|
||||||
import { TabContext } from "utils/contexts/tab";
|
import { TabContext } from "utils/contexts/tab";
|
||||||
|
|
||||||
function slugify(tabName) {
|
function slugify(tabName) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Popover, Transition } from "@headlessui/react";
|
|||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { Fragment, useContext } from "react";
|
import { Fragment, useContext } from "react";
|
||||||
import { IoColorPalette } from "react-icons/io5";
|
import { IoColorPalette } from "react-icons/io5";
|
||||||
|
|
||||||
import { ColorContext } from "utils/contexts/color";
|
import { ColorContext } from "utils/contexts/color";
|
||||||
|
|
||||||
const colors = [
|
const colors = [
|
||||||
|
|||||||
@@ -12,7 +12,11 @@ export default function SignOut() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="signout" className="rounded-full flex align-middle self-center mr-3">
|
<div id="signout" className="rounded-full flex align-middle self-center mr-3">
|
||||||
<button type="button" onClick={() => signOut({ callbackUrl: "/" })} className="outline-hidden">
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => signOut({ callbackUrl: "/auth/signin?autologin=0" })}
|
||||||
|
className="outline-hidden"
|
||||||
|
>
|
||||||
<MdLogout className="text-theme-800 dark:text-theme-200 w-6 h-6 cursor-pointer" aria-hidden="true" />
|
<MdLogout className="text-theme-800 dark:text-theme-200 w-6 h-6 cursor-pointer" aria-hidden="true" />
|
||||||
<span className="sr-only">{t("auth.signout")}</span>
|
<span className="sr-only">{t("auth.signout")}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ describe("components/toggles/signout", () => {
|
|||||||
const { getByRole } = render(<SignOut />);
|
const { getByRole } = render(<SignOut />);
|
||||||
fireEvent.click(getByRole("button"));
|
fireEvent.click(getByRole("button"));
|
||||||
|
|
||||||
expect(signOut).toHaveBeenCalledWith({ callbackUrl: "/" });
|
// Not "/", which would bounce straight back into the provider when auto-login is on
|
||||||
|
expect(signOut).toHaveBeenCalledWith({ callbackUrl: "/auth/signin?autologin=0" });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useContext } from "react";
|
import { useContext } from "react";
|
||||||
import { MdDarkMode, MdLightMode, MdToggleOff, MdToggleOn } from "react-icons/md";
|
import { MdDarkMode, MdLightMode, MdToggleOff, MdToggleOn } from "react-icons/md";
|
||||||
|
|
||||||
import { ThemeContext } from "utils/contexts/theme";
|
import { ThemeContext } from "utils/contexts/theme";
|
||||||
|
|
||||||
export default function ThemeToggle() {
|
export default function ThemeToggle() {
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { useTranslation } from "next-i18next/pages";
|
import { useTranslation } from "next-i18next/pages";
|
||||||
import { useEffect, useState } from "react";
|
import { useMemo } from "react";
|
||||||
|
|
||||||
import Container from "../widget/container";
|
import Container from "../widget/container";
|
||||||
import Raw from "../widget/raw";
|
import Raw from "../widget/raw";
|
||||||
|
|
||||||
|
import useCurrentTime from "utils/hooks/use-current-time";
|
||||||
|
|
||||||
const textSizes = {
|
const textSizes = {
|
||||||
"4xl": "text-4xl",
|
"4xl": "text-4xl",
|
||||||
"3xl": "text-3xl",
|
"3xl": "text-3xl",
|
||||||
@@ -18,17 +20,10 @@ const textSizes = {
|
|||||||
export default function DateTime({ options }) {
|
export default function DateTime({ options }) {
|
||||||
const { text_size: textSize, locale, format } = options;
|
const { text_size: textSize, locale, format } = options;
|
||||||
const { i18n } = useTranslation();
|
const { i18n } = useTranslation();
|
||||||
const [date, setDate] = useState("");
|
|
||||||
const dateLocale = locale ?? i18n.language;
|
const dateLocale = locale ?? i18n.language;
|
||||||
|
const currentTime = useCurrentTime(1000);
|
||||||
useEffect(() => {
|
const dateFormatter = useMemo(() => new Intl.DateTimeFormat(dateLocale, { ...format }), [dateLocale, format]);
|
||||||
const dateFormat = new Intl.DateTimeFormat(dateLocale, { ...format });
|
const date = currentTime === null ? "" : dateFormatter.format(new Date(currentTime));
|
||||||
setDate(dateFormat.format(new Date()));
|
|
||||||
const interval = setInterval(() => {
|
|
||||||
setDate(dateFormat.format(new Date()));
|
|
||||||
}, 1000);
|
|
||||||
return () => clearInterval(interval);
|
|
||||||
}, [date, setDate, dateLocale, format]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container options={options} additionalClassNames="information-widget-datetime">
|
<Container options={options} additionalClassNames="information-widget-datetime">
|
||||||
|
|||||||
@@ -4,13 +4,14 @@ import { useContext } from "react";
|
|||||||
import { FaMemory, FaRegClock, FaThermometerHalf } from "react-icons/fa";
|
import { FaMemory, FaRegClock, FaThermometerHalf } from "react-icons/fa";
|
||||||
import { FiCpu, FiHardDrive } from "react-icons/fi";
|
import { FiCpu, FiHardDrive } from "react-icons/fi";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { SettingsContext } from "utils/contexts/settings";
|
|
||||||
|
|
||||||
import Error from "../widget/error";
|
import Error from "../widget/error";
|
||||||
import Resource from "../widget/resource";
|
import Resource from "../widget/resource";
|
||||||
import Resources from "../widget/resources";
|
import Resources from "../widget/resources";
|
||||||
import WidgetLabel from "../widget/widget_label";
|
import WidgetLabel from "../widget/widget_label";
|
||||||
|
|
||||||
|
import { SettingsContext } from "utils/contexts/settings";
|
||||||
|
|
||||||
const defaultCpuSensorLabels = ["cpu_thermal", "Core", "Tctl", "Temperature"];
|
const defaultCpuSensorLabels = ["cpu_thermal", "Core", "Tctl", "Temperature"];
|
||||||
|
|
||||||
function convertToFahrenheit(t) {
|
function convertToFahrenheit(t) {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import ResolvedIcon from "components/resolvedicon";
|
|
||||||
|
|
||||||
import Container from "../widget/container";
|
import Container from "../widget/container";
|
||||||
import Raw from "../widget/raw";
|
import Raw from "../widget/raw";
|
||||||
|
|
||||||
|
import ResolvedIcon from "components/resolvedicon";
|
||||||
|
|
||||||
export default function Logo({ options }) {
|
export default function Logo({ options }) {
|
||||||
return (
|
return (
|
||||||
<Container
|
<Container
|
||||||
|
|||||||
@@ -7,10 +7,9 @@ import Resource from "../widget/resource";
|
|||||||
|
|
||||||
export default function Network({ options, refresh = 1500 }) {
|
export default function Network({ options, refresh = 1500 }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
// eslint-disable-next-line no-param-reassign
|
const network = options.network === true ? "default" : options.network;
|
||||||
if (options.network === true) options.network = "default";
|
|
||||||
|
|
||||||
const { data, error } = useSWR(`/api/widgets/resources?type=network&interfaceName=${options.network}`, {
|
const { data, error } = useSWR(`/api/widgets/resources?type=network&interfaceName=${network}`, {
|
||||||
refreshInterval: refresh,
|
refreshInterval: refresh,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -22,10 +22,12 @@ describe("components/widgets/resources/network", () => {
|
|||||||
|
|
||||||
it("normalizes options.network=true to default interfaceName in the request", () => {
|
it("normalizes options.network=true to default interfaceName in the request", () => {
|
||||||
useSWR.mockReturnValue({ data: undefined, error: undefined });
|
useSWR.mockReturnValue({ data: undefined, error: undefined });
|
||||||
|
const options = Object.freeze({ network: true });
|
||||||
|
|
||||||
render(<Network options={{ network: true }} />);
|
render(<Network options={options} />);
|
||||||
|
|
||||||
expect(useSWR).toHaveBeenCalledWith(expect.stringContaining("interfaceName=default"), expect.any(Object));
|
expect(useSWR).toHaveBeenCalledWith(expect.stringContaining("interfaceName=default"), expect.any(Object));
|
||||||
|
expect(options.network).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders rates and usage percentage when data is present", () => {
|
it("renders rates and usage percentage when data is present", () => {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
} from "@headlessui/react";
|
} from "@headlessui/react";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { useTranslation } from "next-i18next/pages";
|
import { useTranslation } from "next-i18next/pages";
|
||||||
import { Fragment, useEffect, useState } from "react";
|
import { Fragment, useEffect, useMemo, useState, useSyncExternalStore } from "react";
|
||||||
import { BiLogoBing } from "react-icons/bi";
|
import { BiLogoBing } from "react-icons/bi";
|
||||||
import { FiSearch } from "react-icons/fi";
|
import { FiSearch } from "react-icons/fi";
|
||||||
import { SiBaidu, SiBrave, SiDuckduckgo, SiGoogle } from "react-icons/si";
|
import { SiBaidu, SiBrave, SiDuckduckgo, SiGoogle } from "react-icons/si";
|
||||||
@@ -57,12 +57,12 @@ export const searchProviders = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
function getAvailableProviderIds(options) {
|
function getAvailableProviderIds(provider) {
|
||||||
if (options.provider && Array.isArray(options.provider)) {
|
if (provider && Array.isArray(provider)) {
|
||||||
return options.provider.filter((value) => searchProviders.hasOwnProperty(value));
|
return provider.filter((value) => searchProviders.hasOwnProperty(value));
|
||||||
}
|
}
|
||||||
if (options.provider && searchProviders[options.provider]) {
|
if (provider && searchProviders[provider]) {
|
||||||
return [options.provider];
|
return [provider];
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -79,23 +79,32 @@ export function getStoredProvider() {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function subscribeToStoredProvider(onStoreChange) {
|
||||||
|
const handleStorage = (event) => {
|
||||||
|
if (event.key === localStorageKey) onStoreChange();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("storage", handleStorage);
|
||||||
|
return () => window.removeEventListener("storage", handleStorage);
|
||||||
|
}
|
||||||
|
|
||||||
export default function Search({ options }) {
|
export default function Search({ options }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const availableProviderIds = getAvailableProviderIds(options) ?? [];
|
// options is a fresh object each render, so memo on provider itself
|
||||||
|
const availableProviderIds = useMemo(() => getAvailableProviderIds(options.provider) ?? [], [options.provider]);
|
||||||
|
const storedProvider = useSyncExternalStore(subscribeToStoredProvider, getStoredProvider, () => null);
|
||||||
|
const storedProviderId = Object.keys(searchProviders).find(
|
||||||
|
(providerId) => searchProviders[providerId] === storedProvider,
|
||||||
|
);
|
||||||
|
const initialProvider = availableProviderIds.includes(storedProviderId)
|
||||||
|
? storedProvider
|
||||||
|
: searchProviders[availableProviderIds[0] ?? "google"];
|
||||||
|
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
const [selectedProvider, setSelectedProvider] = useState(searchProviders[availableProviderIds[0] ?? "google"]);
|
const [providerOverride, setProviderOverride] = useState(null);
|
||||||
const [searchSuggestions, setSearchSuggestions] = useState([]);
|
const [searchSuggestions, setSearchSuggestions] = useState([]);
|
||||||
|
const selectedProvider = providerOverride ?? initialProvider;
|
||||||
useEffect(() => {
|
|
||||||
const storedProvider = getStoredProvider();
|
|
||||||
let storedProviderKey = null;
|
|
||||||
storedProviderKey = Object.keys(searchProviders).find((pkey) => searchProviders[pkey] === storedProvider);
|
|
||||||
if (storedProvider && availableProviderIds.includes(storedProviderKey)) {
|
|
||||||
setSelectedProvider(storedProvider);
|
|
||||||
}
|
|
||||||
}, [availableProviderIds]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
@@ -129,9 +138,9 @@ export default function Search({ options }) {
|
|||||||
};
|
};
|
||||||
}, [selectedProvider, options, query, searchSuggestions]);
|
}, [selectedProvider, options, query, searchSuggestions]);
|
||||||
|
|
||||||
let currentSuggestion;
|
|
||||||
|
|
||||||
function doSearch(value) {
|
function doSearch(value) {
|
||||||
|
if (!value) return;
|
||||||
|
|
||||||
const q = encodeURIComponent(value);
|
const q = encodeURIComponent(value);
|
||||||
const { url } = selectedProvider;
|
const { url } = selectedProvider;
|
||||||
if (url) {
|
if (url) {
|
||||||
@@ -141,13 +150,11 @@ export default function Search({ options }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setQuery("");
|
setQuery("");
|
||||||
currentSuggestion = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSearchKeyDown = (event) => {
|
const handleSearchKeyDown = (event) => {
|
||||||
const useSuggestion = searchSuggestions.length && currentSuggestion;
|
if (event.key === "Enter" && !searchSuggestions[1]?.length) {
|
||||||
if (event.key === "Enter") {
|
doSearch(event.target.value);
|
||||||
doSearch(useSuggestion ? currentSuggestion : event.target.value);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -156,7 +163,7 @@ export default function Search({ options }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const onChangeProvider = (provider) => {
|
const onChangeProvider = (provider) => {
|
||||||
setSelectedProvider(provider);
|
setProviderOverride(provider);
|
||||||
localStorage.setItem(localStorageKey, provider.name);
|
localStorage.setItem(localStorageKey, provider.name);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -165,7 +172,7 @@ export default function Search({ options }) {
|
|||||||
<Raw>
|
<Raw>
|
||||||
<div className="flex-col relative h-8 my-4 min-w-fit z-20">
|
<div className="flex-col relative h-8 my-4 min-w-fit z-20">
|
||||||
<div className="flex absolute inset-y-0 left-0 items-center pl-3 pointer-events-none w-full text-theme-800 dark:text-white" />
|
<div className="flex absolute inset-y-0 left-0 items-center pl-3 pointer-events-none w-full text-theme-800 dark:text-white" />
|
||||||
<Combobox value={query}>
|
<Combobox value={query} onChange={doSearch}>
|
||||||
<ComboboxInput
|
<ComboboxInput
|
||||||
type="text"
|
type="text"
|
||||||
className="
|
className="
|
||||||
@@ -177,6 +184,7 @@ export default function Search({ options }) {
|
|||||||
focus:border-theme-500 dark:focus:border-white/50
|
focus:border-theme-500 dark:focus:border-white/50
|
||||||
border border-theme-300 dark:border-theme-200/50"
|
border border-theme-300 dark:border-theme-200/50"
|
||||||
placeholder={t("search.placeholder")}
|
placeholder={t("search.placeholder")}
|
||||||
|
aria-label={t("search.placeholder")}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
setQuery(event.target.value);
|
setQuery(event.target.value);
|
||||||
}}
|
}}
|
||||||
@@ -184,7 +192,6 @@ export default function Search({ options }) {
|
|||||||
autoCapitalize="off"
|
autoCapitalize="off"
|
||||||
autoCorrect="off"
|
autoCorrect="off"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
// eslint-disable-next-line jsx-a11y/no-autofocus
|
|
||||||
autoFocus={options.focus}
|
autoFocus={options.focus}
|
||||||
onBlur={(e) => e.preventDefault()}
|
onBlur={(e) => e.preventDefault()}
|
||||||
onKeyDown={handleSearchKeyDown}
|
onKeyDown={handleSearchKeyDown}
|
||||||
@@ -204,8 +211,8 @@ export default function Search({ options }) {
|
|||||||
bg-theme-600/40 dark:bg-white/10
|
bg-theme-600/40 dark:bg-white/10
|
||||||
focus:ring-theme-500 dark:focus:ring-white/50"
|
focus:ring-theme-500 dark:focus:ring-white/50"
|
||||||
>
|
>
|
||||||
<selectedProvider.icon className="text-white w-3 h-3" />
|
<selectedProvider.icon className="text-white w-3 h-3" aria-hidden="true" />
|
||||||
<span className="sr-only">{t("search.search")}</span>
|
<span className="sr-only">{t("search.provider", { name: selectedProvider.name })}</span>
|
||||||
</ListboxButton>
|
</ListboxButton>
|
||||||
</div>
|
</div>
|
||||||
<Transition
|
<Transition
|
||||||
@@ -218,6 +225,7 @@ export default function Search({ options }) {
|
|||||||
leaveTo="transform opacity-0 scale-95"
|
leaveTo="transform opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<ListboxOptions
|
<ListboxOptions
|
||||||
|
aria-label={t("search.select_provider")}
|
||||||
className="absolute right-0 z-10 mt-1 origin-top-right rounded-md
|
className="absolute right-0 z-10 mt-1 origin-top-right rounded-md
|
||||||
bg-theme-100 dark:bg-theme-600 shadow-lg
|
bg-theme-100 dark:bg-theme-600 shadow-lg
|
||||||
ring-1 ring-black ring-opacity-5 focus:outline-hidden"
|
ring-1 ring-black ring-opacity-5 focus:outline-hidden"
|
||||||
@@ -234,7 +242,8 @@ export default function Search({ options }) {
|
|||||||
active ? "bg-theme-600/10 dark:bg-white/10 dark:text-gray-900" : "dark:text-gray-100",
|
active ? "bg-theme-600/10 dark:bg-white/10 dark:text-gray-900" : "dark:text-gray-100",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<p.icon className="h-4 w-4 mx-4 my-2" />
|
<p.icon className="h-4 w-4 mx-4 my-2" aria-hidden="true" />
|
||||||
|
<span className="sr-only">{p.name}</span>
|
||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
</ListboxOption>
|
</ListboxOption>
|
||||||
@@ -248,19 +257,12 @@ export default function Search({ options }) {
|
|||||||
{searchSuggestions[1]?.length > 0 && (
|
{searchSuggestions[1]?.length > 0 && (
|
||||||
<ComboboxOptions className="mt-1 rounded-md bg-theme-50 dark:bg-theme-800 border border-theme-300 dark:border-theme-200/30 cursor-pointer shadow-lg">
|
<ComboboxOptions className="mt-1 rounded-md bg-theme-50 dark:bg-theme-800 border border-theme-300 dark:border-theme-200/30 cursor-pointer shadow-lg">
|
||||||
<div className="p-1 bg-white/50 dark:bg-white/10 text-theme-900/90 dark:text-white/90 text-xs">
|
<div className="p-1 bg-white/50 dark:bg-white/10 text-theme-900/90 dark:text-white/90 text-xs">
|
||||||
<ComboboxOption key={query} value={query} />
|
<ComboboxOption key={query} value={query}>
|
||||||
|
<span className="sr-only">{query}</span>
|
||||||
|
</ComboboxOption>
|
||||||
{searchSuggestions[1].map((suggestion) => (
|
{searchSuggestions[1].map((suggestion) => (
|
||||||
<ComboboxOption
|
<ComboboxOption key={suggestion} value={suggestion} className="flex w-full">
|
||||||
key={suggestion}
|
{({ active }) => (
|
||||||
value={suggestion}
|
|
||||||
onMouseDown={() => {
|
|
||||||
doSearch(suggestion);
|
|
||||||
}}
|
|
||||||
className="flex w-full"
|
|
||||||
>
|
|
||||||
{({ active }) => {
|
|
||||||
if (active) currentSuggestion = suggestion;
|
|
||||||
return (
|
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"px-2 py-1 rounded-md w-full flex-nowrap",
|
"px-2 py-1 rounded-md w-full flex-nowrap",
|
||||||
@@ -272,8 +274,7 @@ export default function Search({ options }) {
|
|||||||
{suggestion.indexOf(query) === 0 ? suggestion.substring(query.length) : suggestion}
|
{suggestion.indexOf(query) === 0 ? suggestion.substring(query.length) : suggestion}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
)}
|
||||||
}}
|
|
||||||
</ComboboxOption>
|
</ComboboxOption>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user