Compare commits
43 commits
feature/ne
...
main
Author | SHA1 | Date | |
---|---|---|---|
![]() |
49281b30c2 | ||
![]() |
914cd71c76 | ||
![]() |
5e01eb4a8d | ||
![]() |
7edcf6b047 | ||
![]() |
0f3fc77ddf | ||
![]() |
77ed445da1 | ||
![]() |
708e6fa789 | ||
![]() |
9afa40a6b7 | ||
![]() |
04ae39e144 | ||
![]() |
a3e00e96c9 | ||
![]() |
251881a051 | ||
![]() |
9112030275 | ||
![]() |
2f89b12f0d | ||
![]() |
b2a914eb2a | ||
![]() |
4c45c6453f | ||
![]() |
b3414fc35f | ||
![]() |
259f0f8ce9 | ||
![]() |
97218e9cea | ||
![]() |
3e0054069d | ||
![]() |
a3fcc3ef51 | ||
![]() |
dd5144f03a | ||
![]() |
90c12abf87 | ||
![]() |
507e72407f | ||
![]() |
5bc6730e8b | ||
![]() |
110ebe920e | ||
![]() |
95d66707f5 | ||
![]() |
518ed7fc4e | ||
![]() |
acafbb5100 | ||
![]() |
fb9ebf18ba | ||
![]() |
6b35443100 | ||
![]() |
c2729e302d | ||
![]() |
e98b5e2233 | ||
![]() |
0931f5c5a6 | ||
![]() |
6d21ea9ba3 | ||
![]() |
5bf6f30e2b | ||
![]() |
66fbe9f670 | ||
![]() |
6316de6fa6 | ||
![]() |
7f50f6cfaa | ||
![]() |
c9991bc2a2 | ||
![]() |
e28faf6b98 | ||
![]() |
17b9b7e523 | ||
![]() |
d5608c46c3 | ||
![]() |
428680b5df |
74 changed files with 2476 additions and 2765 deletions
6
.github/PULL_REQUEST_TEMPLATE.md
vendored
6
.github/PULL_REQUEST_TEMPLATE.md
vendored
|
@ -25,7 +25,7 @@ What type of change does your PR introduce to Homepage?
|
|||
|
||||
## Checklist:
|
||||
|
||||
- [ ] If adding a service widget or a change that requires it, I have added corresponding documentation changes.
|
||||
- [ ] If adding a new widget I have reviewed the [guidelines](https://gethomepage.dev/latest/more/development/#service-widget-guidelines).
|
||||
- [ ] I have checked that all code style checks pass using pre-commit hooks and linting checks with `pnpm lint` (see development guidelines).
|
||||
- [ ] If applicable, I have added corresponding documentation changes.
|
||||
- [ ] If applicable, I have reviewed the [feature](https://gethomepage.dev/latest/more/development/#new-feature-guidelines) and / or [service widget guidelines](https://gethomepage.dev/latest/more/development/#service-widget-guidelines).
|
||||
- [ ] I have checked that all code style checks pass using [pre-commit hooks](https://gethomepage.dev/latest/more/development/#code-formatting-with-pre-commit-hooks) and [linting checks](https://gethomepage.dev/latest/more/development/#code-linting).
|
||||
- [ ] If applicable, I have tested my code for new features & regressions on both mobile & desktop devices, using the latest version of major browsers.
|
||||
|
|
31
.github/workflows/crowdin.yml
vendored
Normal file
31
.github/workflows/crowdin.yml
vendored
Normal file
|
@ -0,0 +1,31 @@
|
|||
name: Crowdin Action
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '2 */12 * * *'
|
||||
push:
|
||||
paths: [
|
||||
'/public/locales/en/**',
|
||||
]
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
synchronize-with-crowdin:
|
||||
name: Crowdin Sync
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: crowdin action
|
||||
uses: crowdin/github-action@v1
|
||||
with:
|
||||
upload_translations: false
|
||||
download_translations: true
|
||||
crowdin_branch_name: main
|
||||
localization_branch_name: l10n_main
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
|
||||
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
8
.github/workflows/docker-publish.yml
vendored
8
.github/workflows/docker-publish.yml
vendored
|
@ -9,7 +9,9 @@ on:
|
|||
schedule:
|
||||
- cron: '20 0 * * *'
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
branches:
|
||||
- main
|
||||
- feature/**
|
||||
# Publish semver tags as releases.
|
||||
tags: [ 'v*.*.*' ]
|
||||
paths-ignore:
|
||||
|
@ -39,7 +41,7 @@ jobs:
|
|||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Install python
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.x
|
||||
-
|
||||
|
@ -117,7 +119,7 @@ jobs:
|
|||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
push: ${{ github.event_name != 'pull_request' && !(github.event_name == 'push' && startsWith(github.ref, 'refs/heads/feature')) }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
|
|
6
.github/workflows/docs-publish.yml
vendored
6
.github/workflows/docs-publish.yml
vendored
|
@ -27,7 +27,7 @@ jobs:
|
|||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Install python
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.x
|
||||
-
|
||||
|
@ -42,7 +42,7 @@ jobs:
|
|||
- pre-commit
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.x
|
||||
- run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV
|
||||
|
@ -67,7 +67,7 @@ jobs:
|
|||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: main
|
||||
- uses: actions/setup-python@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.x
|
||||
- run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
project_id_env: CROWDIN_PROJECT_ID
|
||||
api_token_env: CROWDIN_PERSONAL_TOKEN
|
||||
preserve_hierarchy: true
|
||||
files:
|
||||
- source: /public/locales/en/*.json
|
||||
translation: /public/locales/%osx_locale%/%original_file_name%
|
||||
|
|
|
@ -153,7 +153,7 @@ labels:
|
|||
- homepage.widget.fields=["field1","field2"] # optional
|
||||
```
|
||||
|
||||
You can add specify fields for e.g. the [CustomAPI](/widgets/services/customapi) widget by using array-style dot notation:
|
||||
You can add specify fields for e.g. the [CustomAPI](../widgets/services/customapi.md) widget by using array-style dot notation:
|
||||
|
||||
```yaml
|
||||
labels:
|
||||
|
@ -201,6 +201,22 @@ In order to detect every service within the Docker swarm it is necessary that se
|
|||
...
|
||||
```
|
||||
|
||||
## Multiple Homepage Instances
|
||||
|
||||
The optional field `instanceName` can be configured in [settings.md](settings.md#instance-name) to differentiate between multiple homepage instances.
|
||||
|
||||
To limit a label to an instance, insert `.instance.{{instanceName}}` after the `homepage` prefix.
|
||||
|
||||
```yaml
|
||||
labels:
|
||||
- homepage.group=Media
|
||||
- homepage.name=Emby
|
||||
- homepage.icon=emby.png
|
||||
- homepage.instance.internal.href=http://emby.lan/
|
||||
- homepage.instance.public.href=https://emby.mydomain.com/
|
||||
- homepage.description=Media server
|
||||
```
|
||||
|
||||
## Ordering
|
||||
|
||||
As of v0.6.4 discovered services can include an optional `weight` field to determine sorting such that:
|
||||
|
|
|
@ -229,6 +229,28 @@ disableCollapse: true
|
|||
|
||||
By default the feature is enabled.
|
||||
|
||||
### Use Equal Height Cards
|
||||
|
||||
You can enable equal height cards for groups of services, this will make all cards in a row the same height.
|
||||
|
||||
Global setting in `settings.yaml`:
|
||||
|
||||
```yaml
|
||||
useEqualHeights: true
|
||||
```
|
||||
|
||||
Per layout group in `settings.yaml`:
|
||||
|
||||
```yaml
|
||||
useEqualHeights: false
|
||||
layout:
|
||||
...
|
||||
Group Name:
|
||||
useEqualHeights: true # overrides global setting
|
||||
```
|
||||
|
||||
By default the feature is disabled
|
||||
|
||||
## Header Style
|
||||
|
||||
There are currently 4 options for header styles, you can see each one below.
|
||||
|
@ -404,6 +426,16 @@ or per-service (`services.yaml`) with:
|
|||
|
||||
If you have both set, the per-service settings take precedence.
|
||||
|
||||
## Instance Name
|
||||
|
||||
Name used by automatic docker service discovery to differentiate between multiple homepage instances.
|
||||
|
||||
For example:
|
||||
|
||||
```yaml
|
||||
instanceName: public
|
||||
```
|
||||
|
||||
## Hide Widget Error Messages
|
||||
|
||||
Hide the visible API error messages either globally in `settings.yaml`:
|
||||
|
|
|
@ -39,11 +39,16 @@ Once installed, hooks will run when you commit. If the formatting isn't quite ri
|
|||
|
||||
See the [pre-commit documentation](https://pre-commit.com/#install) to get started.
|
||||
|
||||
## New Feature Guidelines
|
||||
|
||||
- New features should be linked to an existing feature request with at least 5 'up-votes'. The purpose of this requirement is to avoid the addition (and maintenance) of features that might only benefit a small number of users.
|
||||
- If you have ideas for a larger feature, please open a discussion first.
|
||||
|
||||
## Service Widget Guidelines
|
||||
|
||||
To ensure cohesiveness of various widgets, the following should be used as a guide for developing new widgets:
|
||||
|
||||
- Please only submit widgets that have been requested and have at least 5 'up-votes'
|
||||
- Please only submit widgets that have been requested and have at least 5 'up-votes'. The purpose of this requirement is to avoid the addition (and maintenance) of service widgets that might only benefit a small number of users.
|
||||
- Widgets should be only one row of blocks
|
||||
- Widgets should be no more than 4 blocks wide
|
||||
- Minimize the number of API calls
|
||||
|
|
|
@ -6,6 +6,10 @@ hide:
|
|||
- navigation
|
||||
---
|
||||
|
||||
## Introducing the Homepage AI Bot
|
||||
|
||||
Thanks to the generous folks at [Glime](https://glimelab.ai), Homepage is now equipped with a pretty helpful AI-powered bot. The bot has full knowledge of our docs, GitHub issues and discussions and great at answering specific questions about setting up your Homepage. To use the bot, just hit the 'Ask AI' button on any page in our docs or check out the [#ai-support channel on Discord](https://discord.com/channels/1019316731635834932/1177885603552038993)!
|
||||
|
||||
## General Troubleshooting Tips
|
||||
|
||||
- For API errors, clicking the "API Error Information" button in the widget will usually show some helpful information as to whether the issue is reaching the service host, an authentication issue, etc.
|
||||
|
|
35
docs/scripts/extra.js
Normal file
35
docs/scripts/extra.js
Normal file
|
@ -0,0 +1,35 @@
|
|||
var glimeScript;
|
||||
var glimeStyles = [];
|
||||
document$.subscribe(function () {
|
||||
if (!glimeScript) {
|
||||
glimeScript = document.createElement("script");
|
||||
glimeScript.setAttribute("src", "https://cdn.glimelab.ai/widget/1.0.0/widget.js");
|
||||
glimeScript.setAttribute("onload", "onGlimeLoad()");
|
||||
document.head.appendChild(glimeScript);
|
||||
} else {
|
||||
var newGlimeStyle = document.createElement("style");
|
||||
document.head.appendChild(newGlimeStyle);
|
||||
var i = 0;
|
||||
glimeStyles.forEach((rule) => {
|
||||
newGlimeStyle.sheet.insertRule(rule.cssText, i);
|
||||
i++;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
onGlimeLoad = () => {
|
||||
window.glime.init("Bl3mlvfCnTnRm5");
|
||||
setTimeout(() => {
|
||||
const sheets = document.styleSheets;
|
||||
[...sheets].forEach((sheet) => {
|
||||
if (!sheet.href) {
|
||||
[...sheet.cssRules].forEach((rule) => {
|
||||
if (!rule || rule.href || !rule.selectorText) return;
|
||||
if (rule.selectorText.indexOf(".css-") === 0 || rule.selectorText.indexOf("glime") > -1) {
|
||||
glimeStyles.push(rule);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}, 1000);
|
||||
};
|
|
@ -18,3 +18,7 @@
|
|||
border-color: var(--md-default-bg-color--lighter);
|
||||
}
|
||||
}
|
||||
|
||||
#glimeRoot * {
|
||||
font-family: var(--md-text-font) !important;
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ Homepage has two types of widgets: info and service. Below we'll cover each type
|
|||
|
||||
## Service Widgets
|
||||
|
||||
Service widgets are used to display the status of a service, often a web service or API. Services (and their widgets) are defined in your `services.yml` file. Here's an example:
|
||||
Service widgets are used to display the status of a service, often a web service or API. Services (and their widgets) are defined in your `services.yaml` file. Here's an example:
|
||||
|
||||
```yaml
|
||||
- Plex:
|
||||
|
@ -24,7 +24,7 @@ Service widgets are used to display the status of a service, often a web service
|
|||
|
||||
## Info Widgets
|
||||
|
||||
Info widgets are used to display information in the header, often about your system or environment. Info widgets are defined your `widgets.yml` file. Here's an example:
|
||||
Info widgets are used to display information in the header, often about your system or environment. Info widgets are defined your `widgets.yaml` file. Here's an example:
|
||||
|
||||
```yaml
|
||||
- openmeteo:
|
||||
|
|
|
@ -26,4 +26,12 @@ It can show the aggregate metrics and/or the individual node metrics.
|
|||
- node2
|
||||
```
|
||||
|
||||
The Longhorn URL and credentials are stored in the `providers` section of the `settings.yaml`.
|
||||
The Longhorn URL and credentials are stored in the `providers` section of the `settings.yaml`. e.g.:
|
||||
|
||||
```yaml
|
||||
providers:
|
||||
longhorn:
|
||||
username: "longhorn-username" # optional
|
||||
password: "very-secret-longhorn-password" # optional
|
||||
url: https://longhorn.aesop.network
|
||||
```
|
||||
|
|
|
@ -15,13 +15,20 @@ widget:
|
|||
firstDayInWeek: sunday # optional - defaults to monday
|
||||
view: monthly # optional - possible values monthly, agenda
|
||||
maxEvents: 10 # optional - defaults to 10
|
||||
showTime: true # optional - show time for event happening today - defaults to false
|
||||
integrations: # optional
|
||||
- type: sonarr # active widget type that is currently enabled on homepage - possible values: radarr, sonarr, lidarr, readarr
|
||||
- type: sonarr # active widget type that is currently enabled on homepage - possible values: radarr, sonarr, lidarr, readarr, ical
|
||||
service_group: Media # group name where widget exists
|
||||
service_name: Sonarr # service name for that widget
|
||||
color: teal # optional - defaults to pre-defined color for the service (teal for sonarr)
|
||||
params: # optional - additional params for the service
|
||||
unmonitored: true # optional - defaults to false, used with *arr stack
|
||||
- type: ical # Show calendar events from another service
|
||||
url: https://domain.url/with/link/to.ics # URL with calendar events
|
||||
name: My Events # required - name for these calendar events
|
||||
color: zinc # optional - defaults to pre-defined color for the service (zinc for ical)
|
||||
params: # optional - additional params for the service
|
||||
showName: true # optional - show name before event title in event line - defaults to false
|
||||
```
|
||||
|
||||
## Agenda
|
||||
|
@ -33,6 +40,8 @@ widget:
|
|||
type: calendar
|
||||
view: agenda
|
||||
maxEvents: 10 # optional - defaults to 10
|
||||
showTime: true # optional - show time for event happening today - defaults to false
|
||||
previousDays: 3 # optional - shows events since three days ago - defaults to 0
|
||||
integrations: # same as in Monthly view example
|
||||
```
|
||||
|
||||
|
@ -41,3 +50,7 @@ widget:
|
|||
Currently integrated widgets are [sonarr](sonarr.md), [radarr](radarr.md), [lidarr](lidarr.md) and [readarr](readarr.md).
|
||||
|
||||
Supported colors can be found on [color palette](../../configs/settings.md#color-palette).
|
||||
|
||||
### iCal
|
||||
|
||||
This custom integration allows you to show events from any calendar that supports iCal format, for example, Google Calendar (go to `Settings`, select specific calendar, go to `Integrate calendar`, copy URL from `Public Address in iCal format`).
|
||||
|
|
|
@ -3,7 +3,7 @@ title: Calibre-web
|
|||
description: Calibre-web Widget Configuration
|
||||
---
|
||||
|
||||
**Note: this widget requires a feature of calibre-web that has not yet been distributed in versioned release. The code is contained in ["nightly" lsio builds after 25/8/23](https://hub.docker.com/layers/linuxserver/calibre-web/nightly/images/sha256-b27cbe5d17503de38135d925e226eb3e5ba04c558dbc865dc85d77824d35d7e2) or running the calibre-web source code including commit [0499e57](https://github.com/janeczku/calibre-web/commit/0499e578cdd45db656da34cd2d7152c8d88ceb23).**
|
||||
**Note: widget requires calibre-web ≥ v0.6.21.**
|
||||
|
||||
Allowed fields: `["books", "authors", "categories", "series"]`.
|
||||
|
||||
|
|
22
docs/widgets/services/fritzbox.md
Normal file
22
docs/widgets/services/fritzbox.md
Normal file
|
@ -0,0 +1,22 @@
|
|||
---
|
||||
title: FRITZ!Box
|
||||
description: FRITZ!Box Widget Configuration
|
||||
---
|
||||
|
||||
Application access & UPnP must be activated on your device:
|
||||
|
||||
```
|
||||
Home Network > Network > Network Settings > Access Settings in the Home Network
|
||||
[x] Allow access for applications
|
||||
[x] Transmit status information over UPnP
|
||||
```
|
||||
|
||||
Credentials are not needed and, as such, you may want to consider using `http` instead of `https` as those requests are significantly faster.
|
||||
|
||||
Allowed fields (limited to a max of 4): `["connectionStatus", "upTime", "maxDown", "maxUp", "down", "up", "received", "sent", "externalIPAddress"]`.
|
||||
|
||||
```yaml
|
||||
widget:
|
||||
type: fritzbox
|
||||
url: http://192.168.178.1
|
||||
```
|
|
@ -3,12 +3,14 @@ title: Gluetun
|
|||
description: Gluetun Widget Configuration
|
||||
---
|
||||
|
||||
Requires [HTTP control server options](https://github.com/qdm12/gluetun-wiki/blob/main/setup/advanced/control-server.md) to be enabled.
|
||||
!!! note
|
||||
|
||||
Requires [HTTP control server options](https://github.com/qdm12/gluetun-wiki/blob/main/setup/advanced/control-server.md) to be enabled. By default this runs on port `8000`.
|
||||
|
||||
Allowed fields: `["public_ip", "region", "country"]`.
|
||||
|
||||
```yaml
|
||||
widget:
|
||||
type: gluetun
|
||||
url: http://gluetun.host.or.ip
|
||||
url: http://gluetun.host.or.ip:port
|
||||
```
|
||||
|
|
|
@ -27,7 +27,7 @@ widget:
|
|||
src: http://example.com
|
||||
classes: h-60 sm:h-60 md:h-60 lg:h-60 xl:h-60 2xl:h-72 # optional, use tailwind height classes, see https://tailwindcss.com/docs/height
|
||||
referrerPolicy: same-origin # optional, no default
|
||||
allowPolicy: autoplay fullscreen gamepad # optional, no default
|
||||
allowPolicy: autoplay; fullscreen; gamepad # optional, no default
|
||||
allowFullscreen: false # optional, default: true
|
||||
loadingStrategy: eager # optional, default: eager
|
||||
allowScrolling: no # optional, default: yes
|
||||
|
|
|
@ -53,6 +53,7 @@ nav:
|
|||
- widgets/services/fileflows.md
|
||||
- widgets/services/flood.md
|
||||
- widgets/services/freshrss.md
|
||||
- widgets/services/fritzbox.md
|
||||
- widgets/services/gamedig.md
|
||||
- widgets/services/ghostfolio.md
|
||||
- widgets/services/glances.md
|
||||
|
@ -191,6 +192,8 @@ theme:
|
|||
|
||||
extra_css:
|
||||
- "stylesheets/extra.css"
|
||||
extra_javascript:
|
||||
- "scripts/extra.js"
|
||||
|
||||
extra:
|
||||
version:
|
||||
|
|
|
@ -4,7 +4,6 @@ const { i18n } = require("./next-i18next.config");
|
|||
const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
output: "standalone",
|
||||
swcMinify: false,
|
||||
images: {
|
||||
domains: ["cdn.jsdelivr.net"],
|
||||
unoptimized: true,
|
||||
|
|
1026
package-lock.json
generated
1026
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -12,6 +12,7 @@
|
|||
"dependencies": {
|
||||
"@headlessui/react": "^1.7.2",
|
||||
"@kubernetes/client-node": "^0.17.1",
|
||||
"cal-parser": "^1.0.2",
|
||||
"classnames": "^2.3.2",
|
||||
"compare-versions": "^5.0.1",
|
||||
"dockerode": "^3.3.4",
|
||||
|
@ -23,7 +24,7 @@
|
|||
"luxon": "^3.4.3",
|
||||
"memory-cache": "^0.2.0",
|
||||
"minecraft-ping-js": "^1.0.2",
|
||||
"next": "^14.0.2",
|
||||
"next": "^12.3.1",
|
||||
"next-i18next": "^12.0.1",
|
||||
"ping": "^0.4.4",
|
||||
"pretty-bytes": "^6.0.0",
|
||||
|
@ -45,7 +46,7 @@
|
|||
"autoprefixer": "^10.4.12",
|
||||
"eslint": "^8.24.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-config-next": "^14.0.2",
|
||||
"eslint-config-next": "^12.3.1",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.6.1",
|
||||
|
|
1057
pnpm-lock.yaml
generated
1057
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
@ -402,316 +402,316 @@
|
|||
},
|
||||
"wmo": {
|
||||
"0-day": "Sonnig",
|
||||
"0-night": "Clear",
|
||||
"1-day": "Mainly Sunny",
|
||||
"1-night": "Mainly Clear",
|
||||
"2-day": "Partly Cloudy",
|
||||
"2-night": "Partly Cloudy",
|
||||
"3-day": "Cloudy",
|
||||
"3-night": "Cloudy",
|
||||
"45-day": "Foggy",
|
||||
"45-night": "Foggy",
|
||||
"48-day": "Foggy",
|
||||
"48-night": "Foggy",
|
||||
"51-day": "Light Drizzle",
|
||||
"51-night": "Light Drizzle",
|
||||
"53-day": "Drizzle",
|
||||
"53-night": "Drizzle",
|
||||
"55-day": "Heavy Drizzle",
|
||||
"55-night": "Heavy Drizzle",
|
||||
"56-day": "Light Freezing Drizzle",
|
||||
"56-night": "Light Freezing Drizzle",
|
||||
"57-day": "Freezing Drizzle",
|
||||
"57-night": "Freezing Drizzle",
|
||||
"61-day": "Light Rain",
|
||||
"61-night": "Light Rain",
|
||||
"63-day": "Rain",
|
||||
"63-night": "Rain",
|
||||
"65-day": "Heavy Rain",
|
||||
"65-night": "Heavy Rain",
|
||||
"66-day": "Freezing Rain",
|
||||
"66-night": "Freezing Rain",
|
||||
"67-day": "Freezing Rain",
|
||||
"67-night": "Freezing Rain",
|
||||
"71-day": "Light Snow",
|
||||
"71-night": "Light Snow",
|
||||
"73-day": "Snow",
|
||||
"73-night": "Snow",
|
||||
"75-day": "Heavy Snow",
|
||||
"75-night": "Heavy Snow",
|
||||
"77-day": "Snow Grains",
|
||||
"77-night": "Snow Grains",
|
||||
"80-day": "Light Showers",
|
||||
"80-night": "Light Showers",
|
||||
"81-day": "Showers",
|
||||
"81-night": "Showers",
|
||||
"82-day": "Heavy Showers",
|
||||
"82-night": "Heavy Showers",
|
||||
"85-day": "Snow Showers",
|
||||
"85-night": "Snow Showers",
|
||||
"86-day": "Snow Showers",
|
||||
"86-night": "Snow Showers",
|
||||
"95-day": "Thunderstorm",
|
||||
"95-night": "Thunderstorm",
|
||||
"96-day": "Thunderstorm With Hail",
|
||||
"96-night": "Thunderstorm With Hail",
|
||||
"99-day": "Thunderstorm With Hail",
|
||||
"99-night": "Thunderstorm With Hail"
|
||||
"0-night": "Helder",
|
||||
"1-day": "Hoofsaaklik sonnig",
|
||||
"1-night": "Hoofsaaklik Helder",
|
||||
"2-day": "Gedeeltelik Bewolk",
|
||||
"2-night": "Gedeeltelik Bewolk",
|
||||
"3-day": "Bewolk",
|
||||
"3-night": "Bewolk",
|
||||
"45-day": "Mistig",
|
||||
"45-night": "Mistig",
|
||||
"48-day": "Mistig",
|
||||
"48-night": "Mistig",
|
||||
"51-day": "Ligte Motrëen",
|
||||
"51-night": "Ligte Motrëen",
|
||||
"53-day": "Motrëen",
|
||||
"53-night": "Motrëen",
|
||||
"55-day": "Swaar Motrëen",
|
||||
"55-night": "Swaar Motrëen",
|
||||
"56-day": "Ligte Ysige Motreën",
|
||||
"56-night": "Ligte Ysige Motreën",
|
||||
"57-day": "Ysige Motreën",
|
||||
"57-night": "Ysige Motreën",
|
||||
"61-day": "Ligte Rëen",
|
||||
"61-night": "Ligte Rëen",
|
||||
"63-day": "Rëen",
|
||||
"63-night": "Rëen",
|
||||
"65-day": "Swaar Rëen",
|
||||
"65-night": "Swaar Rëen",
|
||||
"66-day": "Ysige Rëen",
|
||||
"66-night": "Ysige Rëen",
|
||||
"67-day": "Ysige Rëen",
|
||||
"67-night": "Ysige Rëen",
|
||||
"71-day": "Ligte Sneeu",
|
||||
"71-night": "Ligte Sneeu",
|
||||
"73-day": "Sneeu",
|
||||
"73-night": "Sneeu",
|
||||
"75-day": "Swaar Sneeu",
|
||||
"75-night": "Swaar Sneeu",
|
||||
"77-day": "Sneeu Korrels",
|
||||
"77-night": "Sneeu Korrels",
|
||||
"80-day": "Ligte Buie",
|
||||
"80-night": "Ligte Buie",
|
||||
"81-day": "Buie",
|
||||
"81-night": "Buie",
|
||||
"82-day": "Swaar Buie",
|
||||
"82-night": "Swaar Buie",
|
||||
"85-day": "Sneeu Buie",
|
||||
"85-night": "Sneeu Buie",
|
||||
"86-day": "Sneeu Buie",
|
||||
"86-night": "Sneeu Buie",
|
||||
"95-day": "Donderstorm",
|
||||
"95-night": "Donderstorm",
|
||||
"96-day": "Donderstorm Met Hael",
|
||||
"96-night": "Donderstorm Met Hael",
|
||||
"99-day": "Donderstorm Met Hael",
|
||||
"99-night": "Donderstorm Met Hael"
|
||||
},
|
||||
"homebridge": {
|
||||
"available_update": "System",
|
||||
"updates": "Updates",
|
||||
"update_available": "Update Available",
|
||||
"up_to_date": "Up to Date",
|
||||
"child_bridges": "Child Bridges",
|
||||
"available_update": "Stelsel",
|
||||
"updates": "Opdatering",
|
||||
"update_available": "Opdatering Beskikbaar",
|
||||
"up_to_date": "Op Datum",
|
||||
"child_bridges": "Kinderbrug",
|
||||
"child_bridges_status": "{{ok}}/{{total}}",
|
||||
"up": "Op",
|
||||
"pending": "Afwagtend",
|
||||
"down": "Af"
|
||||
},
|
||||
"healthchecks": {
|
||||
"new": "New",
|
||||
"new": "Nuut",
|
||||
"up": "Aanlyn",
|
||||
"grace": "In Grace Period",
|
||||
"grace": "In Grasietydperk",
|
||||
"down": "Vanlyn",
|
||||
"paused": "Paused",
|
||||
"paused": "Onderbreek",
|
||||
"status": "Status",
|
||||
"last_ping": "Last Ping",
|
||||
"never": "No pings yet"
|
||||
"last_ping": "Laaste Pieng",
|
||||
"never": "Nog geen pienge nie"
|
||||
},
|
||||
"watchtower": {
|
||||
"containers_scanned": "Scanned",
|
||||
"containers_updated": "Updated",
|
||||
"containers_failed": "Failed"
|
||||
"containers_scanned": "Geskandeer",
|
||||
"containers_updated": "Opgedateer",
|
||||
"containers_failed": "Misluk"
|
||||
},
|
||||
"autobrr": {
|
||||
"approvedPushes": "Goedgekeur",
|
||||
"rejectedPushes": "Rejected",
|
||||
"rejectedPushes": "Verwerp",
|
||||
"filters": "Filters",
|
||||
"indexers": "Indekseerders"
|
||||
},
|
||||
"tubearchivist": {
|
||||
"downloads": "Tou",
|
||||
"videos": "Videos",
|
||||
"channels": "Channels",
|
||||
"playlists": "Playlists"
|
||||
"channels": "Kanale",
|
||||
"playlists": "Snitlyste"
|
||||
},
|
||||
"truenas": {
|
||||
"load": "System Load",
|
||||
"uptime": "Uptime",
|
||||
"load": "Stelsellading",
|
||||
"uptime": "Optyd",
|
||||
"alerts": "Waarskuwings",
|
||||
"time": "{{value, number(style: unit; unitDisplay: long;)}}"
|
||||
},
|
||||
"pyload": {
|
||||
"speed": "Speed",
|
||||
"speed": "Spoed",
|
||||
"active": "Aktief",
|
||||
"queue": "Tou",
|
||||
"total": "Totaal"
|
||||
},
|
||||
"gluetun": {
|
||||
"public_ip": "Public IP",
|
||||
"region": "Region",
|
||||
"country": "Country"
|
||||
"public_ip": "Publieke IP",
|
||||
"region": "Streek",
|
||||
"country": "Land"
|
||||
},
|
||||
"hdhomerun": {
|
||||
"channels": "Channels",
|
||||
"channels": "Kanale",
|
||||
"hd": "HD"
|
||||
},
|
||||
"scrutiny": {
|
||||
"passed": "Passed",
|
||||
"failed": "Failed",
|
||||
"passed": "Geslaag",
|
||||
"failed": "Misluk",
|
||||
"unknown": "Onbekend"
|
||||
},
|
||||
"paperlessngx": {
|
||||
"inbox": "Inbox",
|
||||
"inbox": "Inmandjie",
|
||||
"total": "Totaal"
|
||||
},
|
||||
"nextdns": {
|
||||
"wait": "Wag Asseblief",
|
||||
"no_devices": "No Device Data Received"
|
||||
"no_devices": "Geen Toesteldata Ontvang Nie"
|
||||
},
|
||||
"mikrotik": {
|
||||
"cpuLoad": "CPU Load",
|
||||
"memoryUsed": "Memory Used",
|
||||
"uptime": "Uptime",
|
||||
"cpuLoad": "SVE-lading",
|
||||
"memoryUsed": "Geheue Gebruik",
|
||||
"uptime": "Optyd",
|
||||
"numberOfLeases": "Leases"
|
||||
},
|
||||
"xteve": {
|
||||
"streams_all": "All Streams",
|
||||
"streams_all": "Alle Strome",
|
||||
"streams_active": "Aktiewe Strome",
|
||||
"streams_xepg": "XEPG Channels"
|
||||
"streams_xepg": "XEPG Kanale"
|
||||
},
|
||||
"opendtu": {
|
||||
"yieldDay": "Today",
|
||||
"absolutePower": "Power",
|
||||
"relativePower": "Power %",
|
||||
"limit": "Limit"
|
||||
"yieldDay": "Vandag",
|
||||
"absolutePower": "Krag",
|
||||
"relativePower": "Krag %",
|
||||
"limit": "Limiet"
|
||||
},
|
||||
"opnsense": {
|
||||
"cpu": "CPU Load",
|
||||
"memory": "Active Memory",
|
||||
"wanUpload": "WAN Upload",
|
||||
"wanDownload": "WAN Download"
|
||||
"cpu": "SVE-lading",
|
||||
"memory": "Aktiewe Geheue",
|
||||
"wanUpload": "WAN Oplaai",
|
||||
"wanDownload": "WAN Aflaai"
|
||||
},
|
||||
"moonraker": {
|
||||
"printer_state": "Printer State",
|
||||
"print_status": "Print Status",
|
||||
"print_progress": "Progress",
|
||||
"layers": "Layers"
|
||||
"printer_state": "Staat van Bladsydrukker",
|
||||
"print_status": "Staat Van Druk",
|
||||
"print_progress": "Vordering",
|
||||
"layers": "Lae"
|
||||
},
|
||||
"octoprint": {
|
||||
"printer_state": "Status",
|
||||
"temp_tool": "Tool temp",
|
||||
"temp_bed": "Bed temp",
|
||||
"job_completion": "Completion"
|
||||
"temp_tool": "Gereedskap Temperatuur",
|
||||
"temp_bed": "Bed Temperatuur",
|
||||
"job_completion": "Afhandeling"
|
||||
},
|
||||
"cloudflared": {
|
||||
"origin_ip": "Origin IP",
|
||||
"origin_ip": "Oorsprong IP",
|
||||
"status": "Status"
|
||||
},
|
||||
"pfsense": {
|
||||
"load": "Load Avg",
|
||||
"memory": "Mem Usage",
|
||||
"load": "Las Gem",
|
||||
"memory": "Mem Gebruik",
|
||||
"wanStatus": "WAN Status",
|
||||
"up": "Op",
|
||||
"down": "Af",
|
||||
"temp": "Temp",
|
||||
"disk": "Disk Usage",
|
||||
"disk": "Skyfgebruik",
|
||||
"wanIP": "WAN IP"
|
||||
},
|
||||
"proxmoxbackupserver": {
|
||||
"datastore_usage": "Datastore",
|
||||
"failed_tasks_24h": "Failed Tasks 24h",
|
||||
"datastore_usage": "Datastoor",
|
||||
"failed_tasks_24h": "Mislukte Take 24h",
|
||||
"cpu_usage": "SVE",
|
||||
"memory_usage": "Memory"
|
||||
"memory_usage": "Geheue"
|
||||
},
|
||||
"immich": {
|
||||
"users": "Gebruikers",
|
||||
"photos": "Photos",
|
||||
"photos": "Foto's",
|
||||
"videos": "Videos",
|
||||
"storage": "Storage"
|
||||
"storage": "Bergplek"
|
||||
},
|
||||
"uptimekuma": {
|
||||
"up": "Sites Up",
|
||||
"down": "Sites Down",
|
||||
"uptime": "Uptime",
|
||||
"incident": "Incident",
|
||||
"up": "Werwe Op",
|
||||
"down": "Werwe Af",
|
||||
"uptime": "Optyd",
|
||||
"incident": "Voorval",
|
||||
"m": "m"
|
||||
},
|
||||
"atsumeru": {
|
||||
"series": "Reekse",
|
||||
"archives": "Archives",
|
||||
"chapters": "Chapters",
|
||||
"categories": "Categories"
|
||||
"archives": "Argiewe",
|
||||
"chapters": "Hoofstukke",
|
||||
"categories": "Kategorieë"
|
||||
},
|
||||
"komga": {
|
||||
"libraries": "Libraries",
|
||||
"libraries": "Biblioteke",
|
||||
"series": "Reekse",
|
||||
"books": "Boeke"
|
||||
},
|
||||
"diskstation": {
|
||||
"days": "Daë",
|
||||
"uptime": "Uptime",
|
||||
"uptime": "Optyd",
|
||||
"volumeAvailable": "Beskikbaar"
|
||||
},
|
||||
"mylar": {
|
||||
"series": "Reekse",
|
||||
"issues": "Issues",
|
||||
"issues": "Kwessies",
|
||||
"wanted": "Gesoek"
|
||||
},
|
||||
"photoprism": {
|
||||
"albums": "Albums",
|
||||
"photos": "Photos",
|
||||
"photos": "Foto's",
|
||||
"videos": "Videos",
|
||||
"people": "People"
|
||||
"people": "Mense"
|
||||
},
|
||||
"fileflows": {
|
||||
"queue": "Tou",
|
||||
"processing": "Verwerking",
|
||||
"processed": "Verwerk",
|
||||
"time": "Time"
|
||||
"time": "Tyd"
|
||||
},
|
||||
"grafana": {
|
||||
"dashboards": "Dashboards",
|
||||
"datasources": "Data Sources",
|
||||
"totalalerts": "Total Alerts",
|
||||
"alertstriggered": "Alerts Triggered"
|
||||
"datasources": "Databronne",
|
||||
"totalalerts": "Totale Waarskuwings",
|
||||
"alertstriggered": "Waarskuwings Geaktiveer"
|
||||
},
|
||||
"nextcloud": {
|
||||
"cpuload": "Cpu Load",
|
||||
"memoryusage": "Memory Usage",
|
||||
"freespace": "Free Space",
|
||||
"activeusers": "Active Users",
|
||||
"numfiles": "Files",
|
||||
"numshares": "Shared Items"
|
||||
"cpuload": "Cpu Las",
|
||||
"memoryusage": "Geheuegebruik",
|
||||
"freespace": "Gratis Spasie",
|
||||
"activeusers": "Aktiewe Gebruikers",
|
||||
"numfiles": "Lêers",
|
||||
"numshares": "Gedeelde Items"
|
||||
},
|
||||
"kopia": {
|
||||
"status": "Status",
|
||||
"size": "Size",
|
||||
"lastrun": "Last Run",
|
||||
"nextrun": "Next Run",
|
||||
"failed": "Failed"
|
||||
"size": "Grootte",
|
||||
"lastrun": "Laaste Iterasie",
|
||||
"nextrun": "Volgende Iterasie",
|
||||
"failed": "Misluk"
|
||||
},
|
||||
"unmanic": {
|
||||
"active_workers": "Active Workers",
|
||||
"total_workers": "Total Workers",
|
||||
"records_total": "Queue Length"
|
||||
"active_workers": "Aktiewe Werkers",
|
||||
"total_workers": "Totale Werkers",
|
||||
"records_total": "Toulengte"
|
||||
},
|
||||
"pterodactyl": {
|
||||
"servers": "Servers",
|
||||
"nodes": "Nodes"
|
||||
"servers": "Bedieners",
|
||||
"nodes": "Nodusse"
|
||||
},
|
||||
"prometheus": {
|
||||
"targets_up": "Targets Up",
|
||||
"targets_down": "Targets Down",
|
||||
"targets_total": "Total Targets"
|
||||
"targets_up": "Teikens Op",
|
||||
"targets_down": "Teikens Af",
|
||||
"targets_total": "Totale Teikens"
|
||||
},
|
||||
"ghostfolio": {
|
||||
"gross_percent_today": "Today",
|
||||
"gross_percent_1y": "One year",
|
||||
"gross_percent_max": "All time"
|
||||
"gross_percent_today": "Vandag",
|
||||
"gross_percent_1y": "Een jaar",
|
||||
"gross_percent_max": "Alle tyd"
|
||||
},
|
||||
"audiobookshelf": {
|
||||
"podcasts": "Podcasts",
|
||||
"podcasts": "Podsendinge",
|
||||
"books": "Boeke",
|
||||
"podcastsDuration": "Duration",
|
||||
"booksDuration": "Duration"
|
||||
"podcastsDuration": "Duur",
|
||||
"booksDuration": "Duur"
|
||||
},
|
||||
"homeassistant": {
|
||||
"people_home": "People Home",
|
||||
"lights_on": "Lights On",
|
||||
"switches_on": "Switches On"
|
||||
"people_home": "Mense Tuis",
|
||||
"lights_on": "Ligte Aan",
|
||||
"switches_on": "Skakels Aan"
|
||||
},
|
||||
"whatsupdocker": {
|
||||
"monitoring": "Monitoring",
|
||||
"updates": "Updates"
|
||||
"monitoring": "Monitering",
|
||||
"updates": "Opdatering"
|
||||
},
|
||||
"calibreweb": {
|
||||
"books": "Boeke",
|
||||
"authors": "Authors",
|
||||
"categories": "Categories",
|
||||
"authors": "Skrywers",
|
||||
"categories": "Kategorieë",
|
||||
"series": "Reekse"
|
||||
},
|
||||
"jdownloader": {
|
||||
"downloadCount": "Tou",
|
||||
"downloadBytesRemaining": "Oorblywende",
|
||||
"downloadTotalBytes": "Size",
|
||||
"downloadSpeed": "Speed"
|
||||
"downloadTotalBytes": "Grootte",
|
||||
"downloadSpeed": "Spoed"
|
||||
},
|
||||
"kavita": {
|
||||
"seriesCount": "Reekse",
|
||||
"totalFiles": "Files"
|
||||
"totalFiles": "Lêers"
|
||||
},
|
||||
"azuredevops": {
|
||||
"result": "Result",
|
||||
"result": "Uitslag",
|
||||
"status": "Status",
|
||||
"buildId": "Build ID",
|
||||
"succeeded": "Succeeded",
|
||||
"notStarted": "Not Started",
|
||||
"failed": "Failed",
|
||||
"canceled": "Canceled",
|
||||
"inProgress": "In Progress",
|
||||
"totalPrs": "Total PRs",
|
||||
"buildId": "Bou ID",
|
||||
"succeeded": "Suksesvol",
|
||||
"notStarted": "Nie Begin Nie",
|
||||
"failed": "Misluk",
|
||||
"canceled": "Gekanselleer",
|
||||
"inProgress": "Besig",
|
||||
"totalPrs": "Totale PRs",
|
||||
"myPrs": "My PRs",
|
||||
"approved": "Goedgekeur"
|
||||
},
|
||||
|
@ -719,52 +719,52 @@
|
|||
"status": "Status",
|
||||
"online": "Aanlyn",
|
||||
"offline": "Vanlyn",
|
||||
"name": "Name",
|
||||
"map": "Map",
|
||||
"currentPlayers": "Current players",
|
||||
"name": "Naam",
|
||||
"map": "Kaart",
|
||||
"currentPlayers": "Huidige Spelers",
|
||||
"players": "Spelers",
|
||||
"maxPlayers": "Max players",
|
||||
"maxPlayers": "Maks spelers",
|
||||
"bots": "Bots",
|
||||
"ping": "Pieng"
|
||||
},
|
||||
"urbackup": {
|
||||
"ok": "Ok",
|
||||
"errored": "Errors",
|
||||
"noRecent": "Out of Date",
|
||||
"totalUsed": "Used Storage"
|
||||
"errored": "Foute",
|
||||
"noRecent": "Verouderd",
|
||||
"totalUsed": "Gebruikte Bergplek"
|
||||
},
|
||||
"mealie": {
|
||||
"recipes": "Recipes",
|
||||
"recipes": "Resepte",
|
||||
"users": "Gebruikers",
|
||||
"categories": "Categories",
|
||||
"tags": "Tags"
|
||||
"categories": "Kategorieë",
|
||||
"tags": "Merkers"
|
||||
},
|
||||
"openmediavault": {
|
||||
"downloading": "Downloading",
|
||||
"downloading": "Aflaai",
|
||||
"total": "Totaal",
|
||||
"running": "Lopend",
|
||||
"stopped": "Gestop",
|
||||
"passed": "Passed",
|
||||
"failed": "Failed"
|
||||
"passed": "Geslaag",
|
||||
"failed": "Misluk"
|
||||
},
|
||||
"uptimerobot": {
|
||||
"status": "Status",
|
||||
"uptime": "Uptime",
|
||||
"lastDown": "Last Downtime",
|
||||
"downDuration": "Downtime Duration",
|
||||
"sitesUp": "Sites Up",
|
||||
"sitesDown": "Sites Down",
|
||||
"paused": "Paused",
|
||||
"notyetchecked": "Not Yet Checked",
|
||||
"uptime": "Optyd",
|
||||
"lastDown": "Laaste Stilstand",
|
||||
"downDuration": "Stilstand Duur",
|
||||
"sitesUp": "Werwe Op",
|
||||
"sitesDown": "Werwe Af",
|
||||
"paused": "Onderbreek",
|
||||
"notyetchecked": "Nog Nie Nagegaan Nie",
|
||||
"up": "Op",
|
||||
"seemsdown": "Seems Down",
|
||||
"seemsdown": "Lyk Af",
|
||||
"down": "Af",
|
||||
"unknown": "Onbekend"
|
||||
},
|
||||
"calendar": {
|
||||
"inCinemas": "In cinemas",
|
||||
"physicalRelease": "Physical release",
|
||||
"digitalRelease": "Digital release",
|
||||
"noEventsToday": "No events for today!"
|
||||
"inCinemas": "In fliekteaters",
|
||||
"physicalRelease": "Fisiese Vrylating",
|
||||
"digitalRelease": "Digitale Vrylating",
|
||||
"noEventsToday": "Geen gebeure vir vandag nie!"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,8 +18,8 @@
|
|||
"api_error": "API خطأ",
|
||||
"information": "معلومات",
|
||||
"status": "الحالة",
|
||||
"url": "URL",
|
||||
"raw_error": "Raw Error",
|
||||
"url": "الرابط",
|
||||
"raw_error": "خطأ خام",
|
||||
"response_data": "بيانات الاستجابة"
|
||||
},
|
||||
"weather": {
|
||||
|
@ -38,118 +38,118 @@
|
|||
"free": "متاح",
|
||||
"used": "مستخدم",
|
||||
"load": "الضغط",
|
||||
"temp": "TEMP",
|
||||
"max": "Max",
|
||||
"uptime": "UP",
|
||||
"months": "mo",
|
||||
"days": "d",
|
||||
"hours": "h",
|
||||
"minutes": "m"
|
||||
"temp": "مؤقت",
|
||||
"max": "الحد الأقصى",
|
||||
"uptime": "تعمل",
|
||||
"months": "ش",
|
||||
"days": "ي",
|
||||
"hours": "س",
|
||||
"minutes": "د"
|
||||
},
|
||||
"unifi": {
|
||||
"users": "المستخدمون",
|
||||
"uptime": "مدة تشغيل النظام",
|
||||
"days": "أيام",
|
||||
"wan": "WAN",
|
||||
"lan": "LAN",
|
||||
"wlan": "WLAN",
|
||||
"wan": "الشبكة الواسعة",
|
||||
"lan": "الشبكة المحلية",
|
||||
"wlan": "الشبكة المحلية اللاسلكية",
|
||||
"devices": "الأجهزة",
|
||||
"lan_devices": "LAN أجهزة",
|
||||
"wlan_devices": "WLAN أجهزة",
|
||||
"lan_users": "LAN مستخدمين",
|
||||
"wlan_users": "WLAN مستخدمين",
|
||||
"up": "UP",
|
||||
"up": "تعمل",
|
||||
"down": "لا يعمل",
|
||||
"wait": "الرجاء الإنتظار",
|
||||
"empty_data": "Subsystem status unknown"
|
||||
"empty_data": "حالة النظام الفرعي غير معروفة"
|
||||
},
|
||||
"docker": {
|
||||
"rx": "RX",
|
||||
"tx": "TX",
|
||||
"rx": "استقبال",
|
||||
"tx": "ارسال",
|
||||
"mem": "الذاكرة",
|
||||
"cpu": "المعالج",
|
||||
"running": "Running",
|
||||
"running": "قيد التشغيل",
|
||||
"offline": "غير متصل",
|
||||
"error": "خطأ",
|
||||
"unknown": "مجهول",
|
||||
"healthy": "Healthy",
|
||||
"starting": "Starting",
|
||||
"unhealthy": "Unhealthy",
|
||||
"not_found": "Not Found",
|
||||
"exited": "Exited",
|
||||
"partial": "Partial"
|
||||
"healthy": "سليم",
|
||||
"starting": "يبدأ التشغيل",
|
||||
"unhealthy": "غير صحّي",
|
||||
"not_found": "غير موجود",
|
||||
"exited": "خرجت",
|
||||
"partial": "جزئي"
|
||||
},
|
||||
"ping": {
|
||||
"error": "خطأ",
|
||||
"ping": "Ping",
|
||||
"down": "Down",
|
||||
"up": "Up",
|
||||
"not_available": "Not Available"
|
||||
"ping": "بينغ",
|
||||
"down": "لا يعمل",
|
||||
"up": "يعمل",
|
||||
"not_available": "غير مُـتوفـّر"
|
||||
},
|
||||
"siteMonitor": {
|
||||
"http_status": "HTTP status",
|
||||
"http_status": "حالة HTTP",
|
||||
"error": "خطأ",
|
||||
"response": "Response",
|
||||
"down": "Down",
|
||||
"up": "Up",
|
||||
"not_available": "Not Available"
|
||||
"response": "الرد",
|
||||
"down": "لا يعمل",
|
||||
"up": "يعمل",
|
||||
"not_available": "غير مُـتوفـّر"
|
||||
},
|
||||
"emby": {
|
||||
"playing": "يعمل الآن",
|
||||
"transcoding": "التحويل",
|
||||
"bitrate": "معدل البت",
|
||||
"no_active": "No Active Streams",
|
||||
"movies": "Movies",
|
||||
"series": "Series",
|
||||
"episodes": "Episodes",
|
||||
"songs": "Songs"
|
||||
"no_active": "لا يوجد بث نشط",
|
||||
"movies": "أفلام",
|
||||
"series": "مسلسلات",
|
||||
"episodes": "حلقات",
|
||||
"songs": "أغاني"
|
||||
},
|
||||
"evcc": {
|
||||
"pv_power": "Production",
|
||||
"battery_soc": "Battery",
|
||||
"grid_power": "Grid",
|
||||
"home_power": "Consumption",
|
||||
"charge_power": "Charger",
|
||||
"watt_hour": "Wh"
|
||||
"pv_power": "إنتاج",
|
||||
"battery_soc": "البطارية",
|
||||
"grid_power": "شبكة",
|
||||
"home_power": "الاستهلاك",
|
||||
"charge_power": "شاحن",
|
||||
"watt_hour": "واط ساعة"
|
||||
},
|
||||
"flood": {
|
||||
"download": "التنزيل",
|
||||
"upload": "التحميل",
|
||||
"leech": "Leech",
|
||||
"seed": "Seed"
|
||||
"leech": "القرناء",
|
||||
"seed": "البذور"
|
||||
},
|
||||
"freshrss": {
|
||||
"subscriptions": "Subscriptions",
|
||||
"unread": "Unread"
|
||||
"subscriptions": "الاشتراكات",
|
||||
"unread": "غير مقروءة"
|
||||
},
|
||||
"caddy": {
|
||||
"upstreams": "Upstreams",
|
||||
"requests": "Current requests",
|
||||
"requests_failed": "Failed requests"
|
||||
"upstreams": "تدفق",
|
||||
"requests": "طلبات الحالية",
|
||||
"requests_failed": "طلبات فشلت"
|
||||
},
|
||||
"changedetectionio": {
|
||||
"totalObserved": "مجموع الملاحظات",
|
||||
"diffsDetected": "Diffs Detected"
|
||||
"diffsDetected": "الاختلافات المكتشفة"
|
||||
},
|
||||
"channelsdvrserver": {
|
||||
"shows": "Shows",
|
||||
"recordings": "Recordings",
|
||||
"scheduled": "Scheduled",
|
||||
"passes": "Passes"
|
||||
"shows": "برامج",
|
||||
"recordings": "التسجيلات",
|
||||
"scheduled": "مجدولة",
|
||||
"passes": "تمريرات"
|
||||
},
|
||||
"tautulli": {
|
||||
"playing": "يعمل الآن",
|
||||
"transcoding": "التحويل",
|
||||
"bitrate": "معدل البت",
|
||||
"no_active": "No Active Streams",
|
||||
"plex_connection_error": "Check Plex Connection"
|
||||
"no_active": "لا يوجد بث نشط",
|
||||
"plex_connection_error": "تحقق من الاتصال بـ Plex"
|
||||
},
|
||||
"omada": {
|
||||
"connectedAp": "المتصلة APs",
|
||||
"activeUser": "الأجهزة النشطة",
|
||||
"alerts": "تنبيهات",
|
||||
"connectedGateway": "Connected gateways",
|
||||
"connectedSwitches": "Connected switches"
|
||||
"connectedGateway": "البوابات المتصلة",
|
||||
"connectedSwitches": "مفاتيح التبديل المتصلة"
|
||||
},
|
||||
"nzbget": {
|
||||
"rate": "معدل",
|
||||
|
@ -157,9 +157,9 @@
|
|||
"downloaded": "مُنزل"
|
||||
},
|
||||
"plex": {
|
||||
"streams": "Active Streams",
|
||||
"albums": "Albums",
|
||||
"movies": "Movies",
|
||||
"streams": "بث نشيطٌ",
|
||||
"albums": "ألبومات",
|
||||
"movies": "أفلام",
|
||||
"tv": "مسلسلات"
|
||||
},
|
||||
"sabnzbd": {
|
||||
|
@ -175,39 +175,39 @@
|
|||
"transmission": {
|
||||
"download": "التنزيل",
|
||||
"upload": "التحميل",
|
||||
"leech": "Leech",
|
||||
"seed": "Seed"
|
||||
"leech": "القرناء",
|
||||
"seed": "البذور"
|
||||
},
|
||||
"qbittorrent": {
|
||||
"download": "التنزيل",
|
||||
"upload": "التحميل",
|
||||
"leech": "Leech",
|
||||
"seed": "Seed"
|
||||
"leech": "القرناء",
|
||||
"seed": "البذور"
|
||||
},
|
||||
"qnap": {
|
||||
"cpuUsage": "CPU Usage",
|
||||
"memUsage": "MEM Usage",
|
||||
"systemTempC": "System Temp",
|
||||
"poolUsage": "Pool Usage",
|
||||
"volumeUsage": "Volume Usage",
|
||||
"invalid": "Invalid"
|
||||
"cpuUsage": "استهلاك المعالج",
|
||||
"memUsage": "استخدام الذاكرة العشوائية",
|
||||
"systemTempC": "درجة حرارة النظام",
|
||||
"poolUsage": "استخدام التجمع",
|
||||
"volumeUsage": "استخدام حجم القرص",
|
||||
"invalid": "غير صحيح"
|
||||
},
|
||||
"deluge": {
|
||||
"download": "التنزيل",
|
||||
"upload": "التحميل",
|
||||
"leech": "Leech",
|
||||
"seed": "Seed"
|
||||
"leech": "القرناء",
|
||||
"seed": "البذور"
|
||||
},
|
||||
"downloadstation": {
|
||||
"download": "التنزيل",
|
||||
"upload": "التحميل",
|
||||
"leech": "Leech",
|
||||
"seed": "Seed"
|
||||
"leech": "القرناء",
|
||||
"seed": "البذور"
|
||||
},
|
||||
"sonarr": {
|
||||
"wanted": "مطلوب",
|
||||
"queued": "في الإنتظار",
|
||||
"series": "Series",
|
||||
"series": "مسلسلات",
|
||||
"queue": "إنتظار",
|
||||
"unknown": "مجهول"
|
||||
},
|
||||
|
@ -215,14 +215,14 @@
|
|||
"wanted": "مطلوب",
|
||||
"missing": "مفقود",
|
||||
"queued": "في الإنتظار",
|
||||
"movies": "Movies",
|
||||
"movies": "أفلام",
|
||||
"queue": "إنتظار",
|
||||
"unknown": "مجهول"
|
||||
},
|
||||
"lidarr": {
|
||||
"wanted": "مطلوب",
|
||||
"queued": "في الإنتظار",
|
||||
"artists": "Artists"
|
||||
"artists": "فنانين"
|
||||
},
|
||||
"readarr": {
|
||||
"wanted": "مطلوب",
|
||||
|
@ -251,14 +251,14 @@
|
|||
},
|
||||
"pialert": {
|
||||
"total": "المجموع",
|
||||
"connected": "Connected",
|
||||
"new_devices": "New Devices",
|
||||
"down_alerts": "Down Alerts"
|
||||
"connected": "متصل",
|
||||
"new_devices": "أجهزة جديدة",
|
||||
"down_alerts": "تنبيهات تعطل الخوادم"
|
||||
},
|
||||
"pihole": {
|
||||
"queries": "الاستعلامات",
|
||||
"blocked": "محظور",
|
||||
"blocked_percent": "Blocked %",
|
||||
"blocked_percent": "تم حظر %",
|
||||
"gravity": "الجاذبية"
|
||||
},
|
||||
"adguard": {
|
||||
|
@ -270,26 +270,26 @@
|
|||
"speedtest": {
|
||||
"upload": "التحميل",
|
||||
"download": "التنزيل",
|
||||
"ping": "Ping"
|
||||
"ping": "بينغ"
|
||||
},
|
||||
"portainer": {
|
||||
"running": "Running",
|
||||
"running": "قيد التشغيل",
|
||||
"stopped": "متوقف",
|
||||
"total": "المجموع"
|
||||
},
|
||||
"tailscale": {
|
||||
"address": "Address",
|
||||
"expires": "Expires",
|
||||
"never": "Never",
|
||||
"last_seen": "Last Seen",
|
||||
"now": "Now",
|
||||
"years": "{{number}}y",
|
||||
"weeks": "{{number}}w",
|
||||
"days": "{{number}}d",
|
||||
"hours": "{{number}}h",
|
||||
"minutes": "{{number}}m",
|
||||
"seconds": "{{number}}s",
|
||||
"ago": "{{value}} Ago"
|
||||
"address": "عنوان",
|
||||
"expires": "تنتهي",
|
||||
"never": "مطلقاً",
|
||||
"last_seen": "آخر ظهور",
|
||||
"now": "الآن",
|
||||
"years": "{{number}}س",
|
||||
"weeks": "{{number}}أ",
|
||||
"days": "{{number}}ي",
|
||||
"hours": "{{number}}س",
|
||||
"minutes": "{{number}}د",
|
||||
"seconds": "{{number}}ث",
|
||||
"ago": "منذ {{value}}"
|
||||
},
|
||||
"tdarr": {
|
||||
"queue": "إنتظار",
|
||||
|
@ -303,7 +303,7 @@
|
|||
"middleware": "الوسيطة"
|
||||
},
|
||||
"navidrome": {
|
||||
"nothing_streaming": "No Active Streams",
|
||||
"nothing_streaming": "لا يوجد بث نشط",
|
||||
"please_wait": "الرجاء الإنتظار"
|
||||
},
|
||||
"npm": {
|
||||
|
@ -316,7 +316,7 @@
|
|||
"1hour": "١ ساعة",
|
||||
"1day": "١ يوم",
|
||||
"7days": "٧ أيام",
|
||||
"30days": "٣٠ يوم"
|
||||
"30days": "30 يوماً"
|
||||
},
|
||||
"gotify": {
|
||||
"apps": "التطبيقات",
|
||||
|
@ -325,41 +325,41 @@
|
|||
},
|
||||
"prowlarr": {
|
||||
"enableIndexers": "مفهرسات",
|
||||
"numberOfGrabs": "Grabs",
|
||||
"numberOfGrabs": "مساكات",
|
||||
"numberOfQueries": "الاستعلامات",
|
||||
"numberOfFailGrabs": "Fail Grabs",
|
||||
"numberOfFailGrabs": "إخفاقات في الالتقاط",
|
||||
"numberOfFailQueries": "فشل الاستعلامات"
|
||||
},
|
||||
"jackett": {
|
||||
"configured": "Configured",
|
||||
"configured": "مهيأ",
|
||||
"errored": "خطأ"
|
||||
},
|
||||
"strelaysrv": {
|
||||
"numActiveSessions": "الجلسات",
|
||||
"numConnections": "التوصيلات",
|
||||
"dataRelayed": "Relayed",
|
||||
"dataRelayed": "منقول(ة)",
|
||||
"transferRate": "معدل"
|
||||
},
|
||||
"mastodon": {
|
||||
"user_count": "المستخدمون",
|
||||
"status_count": "Posts",
|
||||
"domain_count": "Domains"
|
||||
"status_count": "منشورات",
|
||||
"domain_count": "مجالات"
|
||||
},
|
||||
"medusa": {
|
||||
"wanted": "مطلوب",
|
||||
"queued": "في الإنتظار",
|
||||
"series": "Series"
|
||||
"series": "مسلسلات"
|
||||
},
|
||||
"minecraft": {
|
||||
"players": "Players",
|
||||
"version": "Version",
|
||||
"players": "مشغلات",
|
||||
"version": "الإصدار",
|
||||
"status": "الحالة",
|
||||
"up": "Online",
|
||||
"up": "مُتّصل",
|
||||
"down": "غير متصل"
|
||||
},
|
||||
"miniflux": {
|
||||
"read": "قراءة",
|
||||
"unread": "Unread"
|
||||
"unread": "غير مقروءة"
|
||||
},
|
||||
"authentik": {
|
||||
"users": "المستخدمون",
|
||||
|
@ -369,36 +369,36 @@
|
|||
"proxmox": {
|
||||
"mem": "الذاكرة",
|
||||
"cpu": "المعالج",
|
||||
"lxc": "LXC",
|
||||
"vms": "VMs"
|
||||
"lxc": "حاويات لينكس",
|
||||
"vms": "أجهزة ظاهرية"
|
||||
},
|
||||
"glances": {
|
||||
"cpu": "المعالج",
|
||||
"load": "الضغط",
|
||||
"wait": "الرجاء الإنتظار",
|
||||
"temp": "TEMP",
|
||||
"_temp": "Temp",
|
||||
"warn": "Warn",
|
||||
"uptime": "UP",
|
||||
"temp": "مؤقت",
|
||||
"_temp": "درجة الحرارة",
|
||||
"warn": "تنبية",
|
||||
"uptime": "تعمل",
|
||||
"total": "المجموع",
|
||||
"free": "متاح",
|
||||
"used": "مستخدم",
|
||||
"days": "d",
|
||||
"hours": "h",
|
||||
"crit": "Crit",
|
||||
"days": "ي",
|
||||
"hours": "س",
|
||||
"crit": "حساس",
|
||||
"read": "قراءة",
|
||||
"write": "Write",
|
||||
"gpu": "GPU",
|
||||
"mem": "Mem",
|
||||
"swap": "Swap"
|
||||
"write": "الكتابة",
|
||||
"gpu": "كرت الشاشة",
|
||||
"mem": "الذاكرة",
|
||||
"swap": "ذاكرة سواب"
|
||||
},
|
||||
"quicklaunch": {
|
||||
"bookmark": "مفضلة",
|
||||
"service": "خدمة",
|
||||
"search": "Search",
|
||||
"custom": "Custom",
|
||||
"visit": "Visit",
|
||||
"url": "URL"
|
||||
"search": "البحث",
|
||||
"custom": "مُخصّص",
|
||||
"visit": "زيارة",
|
||||
"url": "الرابط"
|
||||
},
|
||||
"wmo": {
|
||||
"0-day": "مشمس",
|
||||
|
@ -463,24 +463,24 @@
|
|||
"updates": "تحديثات",
|
||||
"update_available": "تحديث متاح",
|
||||
"up_to_date": "حتى الآن",
|
||||
"child_bridges": "Child Bridges",
|
||||
"child_bridges": "الجسور الأطفال",
|
||||
"child_bridges_status": "{{ok}}/{{total}}",
|
||||
"up": "Up",
|
||||
"up": "يعمل",
|
||||
"pending": "معلق",
|
||||
"down": "Down"
|
||||
"down": "لا يعمل"
|
||||
},
|
||||
"healthchecks": {
|
||||
"new": "New",
|
||||
"up": "Online",
|
||||
"grace": "In Grace Period",
|
||||
"new": "جديد(ة)",
|
||||
"up": "مُتّصل",
|
||||
"grace": "في فترة السماح",
|
||||
"down": "غير متصل",
|
||||
"paused": "Paused",
|
||||
"paused": "متوقف",
|
||||
"status": "الحالة",
|
||||
"last_ping": "Last Ping",
|
||||
"never": "No pings yet"
|
||||
"last_ping": "آخر Ping",
|
||||
"never": "لا توجد بنغات بعد"
|
||||
},
|
||||
"watchtower": {
|
||||
"containers_scanned": "Scanned",
|
||||
"containers_scanned": "مفحوصة",
|
||||
"containers_updated": "محدث",
|
||||
"containers_failed": "فشل"
|
||||
},
|
||||
|
@ -515,7 +515,7 @@
|
|||
},
|
||||
"hdhomerun": {
|
||||
"channels": "القنوات",
|
||||
"hd": "HD"
|
||||
"hd": "جودة HD"
|
||||
},
|
||||
"scrutiny": {
|
||||
"passed": "إجتاز",
|
||||
|
@ -534,18 +534,18 @@
|
|||
"cpuLoad": "حمل المعالج",
|
||||
"memoryUsed": "الذاكرة الستخدمة",
|
||||
"uptime": "مدة التشغيل",
|
||||
"numberOfLeases": "Leases"
|
||||
"numberOfLeases": "إيجارات"
|
||||
},
|
||||
"xteve": {
|
||||
"streams_all": "All Streams",
|
||||
"streams_active": "Active Streams",
|
||||
"streams_all": "جميع البث",
|
||||
"streams_active": "بث نشيطٌ",
|
||||
"streams_xepg": "XEPG قنوات"
|
||||
},
|
||||
"opendtu": {
|
||||
"yieldDay": "Today",
|
||||
"absolutePower": "Power",
|
||||
"relativePower": "Power %",
|
||||
"limit": "Limit"
|
||||
"yieldDay": "اليوم",
|
||||
"absolutePower": "القوة",
|
||||
"relativePower": "قوة %",
|
||||
"limit": "الحد الأقصى"
|
||||
},
|
||||
"opnsense": {
|
||||
"cpu": "حمل المعالج",
|
||||
|
@ -566,47 +566,47 @@
|
|||
"job_completion": "إتمام"
|
||||
},
|
||||
"cloudflared": {
|
||||
"origin_ip": "Origin IP",
|
||||
"origin_ip": "IP الأصل",
|
||||
"status": "الحالة"
|
||||
},
|
||||
"pfsense": {
|
||||
"load": "Load Avg",
|
||||
"memory": "Mem Usage",
|
||||
"wanStatus": "WAN Status",
|
||||
"up": "Up",
|
||||
"down": "Down",
|
||||
"temp": "Temp",
|
||||
"disk": "Disk Usage",
|
||||
"wanIP": "WAN IP"
|
||||
"load": "معدل التحميل",
|
||||
"memory": "استخدام الذاكرة العشوائية",
|
||||
"wanStatus": "حالة الشبكة الواسعة",
|
||||
"up": "يعمل",
|
||||
"down": "لا يعمل",
|
||||
"temp": "درجة الحرارة",
|
||||
"disk": "استخدام القرص",
|
||||
"wanIP": "IP الشبكة الواسعة"
|
||||
},
|
||||
"proxmoxbackupserver": {
|
||||
"datastore_usage": "Datastore",
|
||||
"failed_tasks_24h": "Failed Tasks 24h",
|
||||
"datastore_usage": "مخزن البيانات",
|
||||
"failed_tasks_24h": "المهام الفاشلة 24 ساعة",
|
||||
"cpu_usage": "المعالج",
|
||||
"memory_usage": "Memory"
|
||||
"memory_usage": "الذاكرة"
|
||||
},
|
||||
"immich": {
|
||||
"users": "المستخدمون",
|
||||
"photos": "Photos",
|
||||
"photos": "الصور",
|
||||
"videos": "الفيديوهات",
|
||||
"storage": "Storage"
|
||||
"storage": "التخزين"
|
||||
},
|
||||
"uptimekuma": {
|
||||
"up": "Sites Up",
|
||||
"down": "Sites Down",
|
||||
"up": "المواقع تعمل",
|
||||
"down": "مواقع لا تعمل",
|
||||
"uptime": "مدة التشغيل",
|
||||
"incident": "Incident",
|
||||
"m": "m"
|
||||
"incident": "حادثة",
|
||||
"m": "د"
|
||||
},
|
||||
"atsumeru": {
|
||||
"series": "Series",
|
||||
"archives": "Archives",
|
||||
"chapters": "Chapters",
|
||||
"categories": "Categories"
|
||||
"series": "مسلسلات",
|
||||
"archives": "الأرشيف",
|
||||
"chapters": "الفصول",
|
||||
"categories": "التصنيفات"
|
||||
},
|
||||
"komga": {
|
||||
"libraries": "Libraries",
|
||||
"series": "Series",
|
||||
"libraries": "المكتبات",
|
||||
"series": "مسلسلات",
|
||||
"books": "كتب"
|
||||
},
|
||||
"diskstation": {
|
||||
|
@ -615,134 +615,134 @@
|
|||
"volumeAvailable": "متاح"
|
||||
},
|
||||
"mylar": {
|
||||
"series": "Series",
|
||||
"issues": "Issues",
|
||||
"series": "مسلسلات",
|
||||
"issues": "المُشكِلات",
|
||||
"wanted": "مطلوب"
|
||||
},
|
||||
"photoprism": {
|
||||
"albums": "Albums",
|
||||
"photos": "Photos",
|
||||
"albums": "ألبومات",
|
||||
"photos": "الصور",
|
||||
"videos": "الفيديوهات",
|
||||
"people": "People"
|
||||
"people": "أشخاص"
|
||||
},
|
||||
"fileflows": {
|
||||
"queue": "إنتظار",
|
||||
"processing": "معالجة",
|
||||
"processed": "معالجة",
|
||||
"time": "Time"
|
||||
"time": "الوقت"
|
||||
},
|
||||
"grafana": {
|
||||
"dashboards": "Dashboards",
|
||||
"datasources": "Data Sources",
|
||||
"totalalerts": "Total Alerts",
|
||||
"alertstriggered": "Alerts Triggered"
|
||||
"dashboards": "لوحات المعلومات",
|
||||
"datasources": "مصادر البيانات",
|
||||
"totalalerts": "إجمالي التنبيهات",
|
||||
"alertstriggered": "تنبيهات مفعلة"
|
||||
},
|
||||
"nextcloud": {
|
||||
"cpuload": "Cpu Load",
|
||||
"memoryusage": "Memory Usage",
|
||||
"freespace": "Free Space",
|
||||
"activeusers": "Active Users",
|
||||
"numfiles": "Files",
|
||||
"numshares": "Shared Items"
|
||||
"cpuload": "حمل المعالج",
|
||||
"memoryusage": "استخدام الذاكرة",
|
||||
"freespace": "مساحة فارغة",
|
||||
"activeusers": "مستخدمين نشطين",
|
||||
"numfiles": "ملفات",
|
||||
"numshares": "عناصر مشتركة"
|
||||
},
|
||||
"kopia": {
|
||||
"status": "الحالة",
|
||||
"size": "Size",
|
||||
"lastrun": "Last Run",
|
||||
"nextrun": "Next Run",
|
||||
"size": "حجم",
|
||||
"lastrun": "آخر تشغيل",
|
||||
"nextrun": "التشغيل التالي",
|
||||
"failed": "فشل"
|
||||
},
|
||||
"unmanic": {
|
||||
"active_workers": "Active Workers",
|
||||
"total_workers": "Total Workers",
|
||||
"records_total": "Queue Length"
|
||||
"active_workers": "العمال النشطون",
|
||||
"total_workers": "مجموع العمال",
|
||||
"records_total": "طول الصف"
|
||||
},
|
||||
"pterodactyl": {
|
||||
"servers": "Servers",
|
||||
"nodes": "Nodes"
|
||||
"servers": "السيرفرات",
|
||||
"nodes": "عقد"
|
||||
},
|
||||
"prometheus": {
|
||||
"targets_up": "Targets Up",
|
||||
"targets_down": "Targets Down",
|
||||
"targets_total": "Total Targets"
|
||||
"targets_up": "أهداف تعمل",
|
||||
"targets_down": "الأهداف لا تعمل",
|
||||
"targets_total": "الأهداف الإجمالية"
|
||||
},
|
||||
"ghostfolio": {
|
||||
"gross_percent_today": "Today",
|
||||
"gross_percent_1y": "One year",
|
||||
"gross_percent_max": "All time"
|
||||
"gross_percent_today": "اليوم",
|
||||
"gross_percent_1y": "سنة",
|
||||
"gross_percent_max": "كل الوقت"
|
||||
},
|
||||
"audiobookshelf": {
|
||||
"podcasts": "Podcasts",
|
||||
"podcasts": "بودكاست",
|
||||
"books": "كتب",
|
||||
"podcastsDuration": "Duration",
|
||||
"booksDuration": "Duration"
|
||||
"podcastsDuration": "المدة",
|
||||
"booksDuration": "المدة"
|
||||
},
|
||||
"homeassistant": {
|
||||
"people_home": "People Home",
|
||||
"lights_on": "Lights On",
|
||||
"switches_on": "Switches On"
|
||||
"people_home": "أشخاص في المنزل",
|
||||
"lights_on": "أضواء مضاءة",
|
||||
"switches_on": "مفاتيح قيد التشغيل"
|
||||
},
|
||||
"whatsupdocker": {
|
||||
"monitoring": "Monitoring",
|
||||
"monitoring": "المراقبة",
|
||||
"updates": "تحديثات"
|
||||
},
|
||||
"calibreweb": {
|
||||
"books": "كتب",
|
||||
"authors": "Authors",
|
||||
"categories": "Categories",
|
||||
"series": "Series"
|
||||
"authors": "المؤلفون",
|
||||
"categories": "التصنيفات",
|
||||
"series": "مسلسلات"
|
||||
},
|
||||
"jdownloader": {
|
||||
"downloadCount": "إنتظار",
|
||||
"downloadBytesRemaining": "متبقي",
|
||||
"downloadTotalBytes": "Size",
|
||||
"downloadTotalBytes": "حجم",
|
||||
"downloadSpeed": "السرعة"
|
||||
},
|
||||
"kavita": {
|
||||
"seriesCount": "Series",
|
||||
"totalFiles": "Files"
|
||||
"seriesCount": "مسلسلات",
|
||||
"totalFiles": "ملفات"
|
||||
},
|
||||
"azuredevops": {
|
||||
"result": "Result",
|
||||
"result": "نتيجة",
|
||||
"status": "الحالة",
|
||||
"buildId": "Build ID",
|
||||
"succeeded": "Succeeded",
|
||||
"notStarted": "Not Started",
|
||||
"buildId": "معرف البناء",
|
||||
"succeeded": "تم بنجاح",
|
||||
"notStarted": "لم يبدأ",
|
||||
"failed": "فشل",
|
||||
"canceled": "Canceled",
|
||||
"inProgress": "In Progress",
|
||||
"totalPrs": "Total PRs",
|
||||
"myPrs": "My PRs",
|
||||
"canceled": "ملغى",
|
||||
"inProgress": "قيد التنفيذ",
|
||||
"totalPrs": "المجموع الكلي للPRs",
|
||||
"myPrs": "الPRs الشخصية",
|
||||
"approved": "مصدق"
|
||||
},
|
||||
"gamedig": {
|
||||
"status": "الحالة",
|
||||
"online": "Online",
|
||||
"online": "مُتّصل",
|
||||
"offline": "غير متصل",
|
||||
"name": "Name",
|
||||
"map": "Map",
|
||||
"currentPlayers": "Current players",
|
||||
"players": "Players",
|
||||
"maxPlayers": "Max players",
|
||||
"bots": "Bots",
|
||||
"ping": "Ping"
|
||||
"name": "الاسم",
|
||||
"map": "خريطة",
|
||||
"currentPlayers": "المشغلات الحالية",
|
||||
"players": "مشغلات",
|
||||
"maxPlayers": "الحد الأقصى للمشغلات",
|
||||
"bots": "بوتات",
|
||||
"ping": "بينغ"
|
||||
},
|
||||
"urbackup": {
|
||||
"ok": "Ok",
|
||||
"errored": "Errors",
|
||||
"noRecent": "Out of Date",
|
||||
"totalUsed": "Used Storage"
|
||||
"ok": "تمام",
|
||||
"errored": "أخطاء",
|
||||
"noRecent": "غير محدّث",
|
||||
"totalUsed": "التخزين المستخدم"
|
||||
},
|
||||
"mealie": {
|
||||
"recipes": "Recipes",
|
||||
"recipes": "وصفات",
|
||||
"users": "المستخدمون",
|
||||
"categories": "Categories",
|
||||
"tags": "Tags"
|
||||
"categories": "التصنيفات",
|
||||
"tags": "التصنيفات"
|
||||
},
|
||||
"openmediavault": {
|
||||
"downloading": "Downloading",
|
||||
"downloading": "جاري التنزيل",
|
||||
"total": "المجموع",
|
||||
"running": "Running",
|
||||
"running": "قيد التشغيل",
|
||||
"stopped": "متوقف",
|
||||
"passed": "إجتاز",
|
||||
"failed": "فشل"
|
||||
|
@ -750,21 +750,21 @@
|
|||
"uptimerobot": {
|
||||
"status": "الحالة",
|
||||
"uptime": "مدة التشغيل",
|
||||
"lastDown": "Last Downtime",
|
||||
"downDuration": "Downtime Duration",
|
||||
"sitesUp": "Sites Up",
|
||||
"sitesDown": "Sites Down",
|
||||
"paused": "Paused",
|
||||
"notyetchecked": "Not Yet Checked",
|
||||
"up": "Up",
|
||||
"seemsdown": "Seems Down",
|
||||
"down": "Down",
|
||||
"lastDown": "فترة التعطّل الأخيرة",
|
||||
"downDuration": "مدة التعطل",
|
||||
"sitesUp": "المواقع تعمل",
|
||||
"sitesDown": "مواقع لا تعمل",
|
||||
"paused": "متوقف",
|
||||
"notyetchecked": "لم يتم التحقق بعد",
|
||||
"up": "يعمل",
|
||||
"seemsdown": "يبدو أنه معطل",
|
||||
"down": "لا يعمل",
|
||||
"unknown": "مجهول"
|
||||
},
|
||||
"calendar": {
|
||||
"inCinemas": "In cinemas",
|
||||
"physicalRelease": "Physical release",
|
||||
"digitalRelease": "Digital release",
|
||||
"noEventsToday": "No events for today!"
|
||||
"inCinemas": "في دور السينما",
|
||||
"physicalRelease": "الإصدار المادي",
|
||||
"digitalRelease": "الإصدار الرقمي",
|
||||
"noEventsToday": "لا توجد أحداث اليوم!"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -122,6 +122,24 @@
|
|||
"subscriptions": "Abonnements",
|
||||
"unread": "Ungelesen"
|
||||
},
|
||||
"fritzbox": {
|
||||
"connectionStatus": "Status",
|
||||
"connectionStatusUnconfigured": "Unkonfiguriert",
|
||||
"connectionStatusConnecting": "Verbinde",
|
||||
"connectionStatusAuthenticating": "Authenifiziere",
|
||||
"connectionStatusPendingDisconnect": "Anstehende Trennung",
|
||||
"connectionStatusDisconnecting": "Trenne",
|
||||
"connectionStatusDisconnected": "Getrennt",
|
||||
"connectionStatusConnected": "Verbunden",
|
||||
"uptime": "Betriebszeit",
|
||||
"maxDown": "Max. Down",
|
||||
"maxUp": "Max. Up",
|
||||
"down": "Down",
|
||||
"up": "Up",
|
||||
"received": "Empfangen",
|
||||
"sent": "Gesendet",
|
||||
"externalIPAddress": "Ext. IP"
|
||||
},
|
||||
"caddy": {
|
||||
"upstreams": "Upstreams",
|
||||
"requests": "Aktuelle Anfragen",
|
||||
|
@ -129,7 +147,7 @@
|
|||
},
|
||||
"changedetectionio": {
|
||||
"totalObserved": "Gesamt beobachtet",
|
||||
"diffsDetected": "Erkannte Differenzen"
|
||||
"diffsDetected": "Erkannte Änderungen"
|
||||
},
|
||||
"channelsdvrserver": {
|
||||
"shows": "Serien",
|
||||
|
@ -328,7 +346,7 @@
|
|||
"numberOfGrabs": "Abrufungen",
|
||||
"numberOfQueries": "Anfragen",
|
||||
"numberOfFailGrabs": "Fehlgeschlagene Abrufungen",
|
||||
"numberOfFailQueries": "Fehlgeschlagene Abfragen"
|
||||
"numberOfFailQueries": "Fehlgeschlagene Anfragen"
|
||||
},
|
||||
"jackett": {
|
||||
"configured": "Konfiguriert",
|
||||
|
|
|
@ -48,7 +48,7 @@
|
|||
},
|
||||
"unifi": {
|
||||
"users": "Users",
|
||||
"uptime": "System Uptime",
|
||||
"uptime": "Uptime",
|
||||
"days": "Days",
|
||||
"wan": "WAN",
|
||||
"lan": "LAN",
|
||||
|
@ -122,6 +122,24 @@
|
|||
"subscriptions": "Subscriptions",
|
||||
"unread": "Unread"
|
||||
},
|
||||
"fritzbox": {
|
||||
"connectionStatus": "Status",
|
||||
"connectionStatusUnconfigured": "Unconfigured",
|
||||
"connectionStatusConnecting": "Connecting",
|
||||
"connectionStatusAuthenticating": "Authenticating",
|
||||
"connectionStatusPendingDisconnect": "Pending Disconnect",
|
||||
"connectionStatusDisconnecting": "Disconnecting",
|
||||
"connectionStatusDisconnected": "Disconnected",
|
||||
"connectionStatusConnected": "Connected",
|
||||
"uptime": "Uptime",
|
||||
"maxDown": "Max. Down",
|
||||
"maxUp": "Max. Up",
|
||||
"down": "Down",
|
||||
"up": "Up",
|
||||
"received": "Received",
|
||||
"sent": "Sent",
|
||||
"externalIPAddress": "Ext. IP"
|
||||
},
|
||||
"caddy": {
|
||||
"upstreams": "Upstreams",
|
||||
"requests": "Current requests",
|
||||
|
@ -765,6 +783,7 @@
|
|||
"inCinemas": "In cinemas",
|
||||
"physicalRelease": "Physical release",
|
||||
"digitalRelease": "Digital release",
|
||||
"noEventsToday": "No events for today!"
|
||||
"noEventsToday": "No events for today!",
|
||||
"noEventsFound": "No events found"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -115,7 +115,7 @@
|
|||
"flood": {
|
||||
"download": "Descarga",
|
||||
"upload": "Subida",
|
||||
"leech": "Depender",
|
||||
"leech": "Descargas",
|
||||
"seed": "Semillas"
|
||||
},
|
||||
"freshrss": {
|
||||
|
@ -175,13 +175,13 @@
|
|||
"transmission": {
|
||||
"download": "Descarga",
|
||||
"upload": "Subida",
|
||||
"leech": "Depender",
|
||||
"leech": "Descargas",
|
||||
"seed": "Semillas"
|
||||
},
|
||||
"qbittorrent": {
|
||||
"download": "Descarga",
|
||||
"upload": "Subida",
|
||||
"leech": "Depender",
|
||||
"leech": "Descargas",
|
||||
"seed": "Semillas"
|
||||
},
|
||||
"qnap": {
|
||||
|
@ -195,13 +195,13 @@
|
|||
"deluge": {
|
||||
"download": "Descarga",
|
||||
"upload": "Subida",
|
||||
"leech": "Depender",
|
||||
"leech": "Descargas",
|
||||
"seed": "Semillas"
|
||||
},
|
||||
"downloadstation": {
|
||||
"download": "Descarga",
|
||||
"upload": "Subida",
|
||||
"leech": "Depender",
|
||||
"leech": "Descargas",
|
||||
"seed": "Semillas"
|
||||
},
|
||||
"sonarr": {
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
"widget": {
|
||||
"missing_type": "Tipo del Widget Mancante: {{type}}",
|
||||
"api_error": "Errore API",
|
||||
"information": "Informazione",
|
||||
"information": "Informazioni",
|
||||
"status": "Stato",
|
||||
"url": "URL",
|
||||
"raw_error": "Errore non processato",
|
||||
|
@ -87,9 +87,9 @@
|
|||
"not_available": "Non disponibile"
|
||||
},
|
||||
"siteMonitor": {
|
||||
"http_status": "HTTP status",
|
||||
"http_status": "Stato HTTP",
|
||||
"error": "Errore",
|
||||
"response": "Response",
|
||||
"response": "Risposta",
|
||||
"down": "Down",
|
||||
"up": "Up",
|
||||
"not_available": "Non disponibile"
|
||||
|
@ -115,7 +115,7 @@
|
|||
"flood": {
|
||||
"download": "Download",
|
||||
"upload": "Upload",
|
||||
"leech": "In scaricamento",
|
||||
"leech": "In download",
|
||||
"seed": "Seed"
|
||||
},
|
||||
"freshrss": {
|
||||
|
@ -128,7 +128,7 @@
|
|||
"requests_failed": "Richieste fallite"
|
||||
},
|
||||
"changedetectionio": {
|
||||
"totalObserved": "Totale Osservato",
|
||||
"totalObserved": "Totale Osservati",
|
||||
"diffsDetected": "Differenze Rilevate"
|
||||
},
|
||||
"channelsdvrserver": {
|
||||
|
@ -160,7 +160,7 @@
|
|||
"streams": "Trasmissioni attive",
|
||||
"albums": "Album",
|
||||
"movies": "Film",
|
||||
"tv": "Programma televisivo"
|
||||
"tv": "Programmi televisivi"
|
||||
},
|
||||
"sabnzbd": {
|
||||
"rate": "Rapporto",
|
||||
|
@ -175,13 +175,13 @@
|
|||
"transmission": {
|
||||
"download": "Download",
|
||||
"upload": "Upload",
|
||||
"leech": "In scaricamento",
|
||||
"leech": "In download",
|
||||
"seed": "Seed"
|
||||
},
|
||||
"qbittorrent": {
|
||||
"download": "Download",
|
||||
"upload": "Upload",
|
||||
"leech": "In scaricamento",
|
||||
"leech": "In download",
|
||||
"seed": "Seed"
|
||||
},
|
||||
"qnap": {
|
||||
|
@ -195,13 +195,13 @@
|
|||
"deluge": {
|
||||
"download": "Download",
|
||||
"upload": "Upload",
|
||||
"leech": "In scaricamento",
|
||||
"leech": "In download",
|
||||
"seed": "Seed"
|
||||
},
|
||||
"downloadstation": {
|
||||
"download": "Download",
|
||||
"upload": "Upload",
|
||||
"leech": "In scaricamento",
|
||||
"leech": "In download",
|
||||
"seed": "Seed"
|
||||
},
|
||||
"sonarr": {
|
||||
|
@ -283,10 +283,10 @@
|
|||
"never": "Mai",
|
||||
"last_seen": "Ultima visualizzazione",
|
||||
"now": "Adesso",
|
||||
"years": "{{number}}y",
|
||||
"weeks": "{{number}}w",
|
||||
"days": "{{number}}d",
|
||||
"hours": "{{number}}h",
|
||||
"years": "{{number}}a",
|
||||
"weeks": "{{number}}st",
|
||||
"days": "{{number}}g",
|
||||
"hours": "{{number}}o",
|
||||
"minutes": "{{number}}m",
|
||||
"seconds": "{{number}}s",
|
||||
"ago": "{{value}} Fa"
|
||||
|
@ -342,7 +342,7 @@
|
|||
},
|
||||
"mastodon": {
|
||||
"user_count": "Utenti",
|
||||
"status_count": "Posts",
|
||||
"status_count": "Messaggi",
|
||||
"domain_count": "Domini"
|
||||
},
|
||||
"medusa": {
|
||||
|
@ -764,7 +764,7 @@
|
|||
"calendar": {
|
||||
"inCinemas": "Al cinema",
|
||||
"physicalRelease": "Release fisici",
|
||||
"digitalRelease": "Digital release",
|
||||
"noEventsToday": "No events for today!"
|
||||
"digitalRelease": "Versione digitale",
|
||||
"noEventsToday": "Nessun evento per oggi!"
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -59,7 +59,7 @@
|
|||
"lan_users": "LAN uporabniki",
|
||||
"wlan_users": "WLAN uporabniki",
|
||||
"up": "Gor",
|
||||
"down": "Dol",
|
||||
"down": "DOL",
|
||||
"wait": "Prosimo počakajte",
|
||||
"empty_data": "Neznani status podsistema"
|
||||
},
|
||||
|
@ -82,17 +82,17 @@
|
|||
"ping": {
|
||||
"error": "Napaka",
|
||||
"ping": "Ping",
|
||||
"down": "Down",
|
||||
"up": "Up",
|
||||
"not_available": "Not Available"
|
||||
"down": "Nepovezan",
|
||||
"up": "Povezan",
|
||||
"not_available": "Ni na voljo"
|
||||
},
|
||||
"siteMonitor": {
|
||||
"http_status": "HTTP status",
|
||||
"error": "Napaka",
|
||||
"response": "Response",
|
||||
"down": "Down",
|
||||
"up": "Up",
|
||||
"not_available": "Not Available"
|
||||
"response": "Odgovor",
|
||||
"down": "Nepovezan",
|
||||
"up": "Povezan",
|
||||
"not_available": "Ni na voljo"
|
||||
},
|
||||
"emby": {
|
||||
"playing": "Predvaja",
|
||||
|
@ -465,9 +465,9 @@
|
|||
"up_to_date": "Posodobljeno",
|
||||
"child_bridges": "Otroški mostovi",
|
||||
"child_bridges_status": "{{ok}}/{{total}}",
|
||||
"up": "Up",
|
||||
"up": "Povezan",
|
||||
"pending": "V teku",
|
||||
"down": "Down"
|
||||
"down": "Nepovezan"
|
||||
},
|
||||
"healthchecks": {
|
||||
"new": "Nov",
|
||||
|
@ -542,9 +542,9 @@
|
|||
"streams_xepg": "XEPG kanali"
|
||||
},
|
||||
"opendtu": {
|
||||
"yieldDay": "Today",
|
||||
"absolutePower": "Power",
|
||||
"relativePower": "Power %",
|
||||
"yieldDay": "Danes",
|
||||
"absolutePower": "Napajanje",
|
||||
"relativePower": "Napajanje %",
|
||||
"limit": "Limit"
|
||||
},
|
||||
"opnsense": {
|
||||
|
@ -572,9 +572,9 @@
|
|||
"pfsense": {
|
||||
"load": "Povp. obremenitev",
|
||||
"memory": "Poraba spomina",
|
||||
"wanStatus": "WAN Status",
|
||||
"up": "Up",
|
||||
"down": "Down",
|
||||
"wanStatus": "WAN status",
|
||||
"up": "Povezan",
|
||||
"down": "Nepovezan",
|
||||
"temp": "Temp",
|
||||
"disk": "Poraba diska",
|
||||
"wanIP": "WAN IP"
|
||||
|
@ -667,7 +667,7 @@
|
|||
"targets_total": "Skupaj tarč"
|
||||
},
|
||||
"ghostfolio": {
|
||||
"gross_percent_today": "Today",
|
||||
"gross_percent_today": "Danes",
|
||||
"gross_percent_1y": "Eno leto",
|
||||
"gross_percent_max": "Celoten čas"
|
||||
},
|
||||
|
@ -750,21 +750,21 @@
|
|||
"uptimerobot": {
|
||||
"status": "Stanje",
|
||||
"uptime": "Čas delovanja",
|
||||
"lastDown": "Last Downtime",
|
||||
"downDuration": "Downtime Duration",
|
||||
"lastDown": "Zadnjič nepovezan",
|
||||
"downDuration": "Dolžina izpada",
|
||||
"sitesUp": "Deluje",
|
||||
"sitesDown": "Ne deluje",
|
||||
"paused": "Pavziran",
|
||||
"notyetchecked": "Not Yet Checked",
|
||||
"up": "Up",
|
||||
"seemsdown": "Seems Down",
|
||||
"down": "Down",
|
||||
"notyetchecked": "Še nepreverjeno",
|
||||
"up": "Povezan",
|
||||
"seemsdown": "Ne deluje",
|
||||
"down": "Nepovezan",
|
||||
"unknown": "Neznano"
|
||||
},
|
||||
"calendar": {
|
||||
"inCinemas": "In cinemas",
|
||||
"physicalRelease": "Physical release",
|
||||
"digitalRelease": "Digital release",
|
||||
"noEventsToday": "No events for today!"
|
||||
"inCinemas": "V kinu",
|
||||
"physicalRelease": "Fizična izdaja",
|
||||
"digitalRelease": "Digitalna izdaja",
|
||||
"noEventsToday": "Za danes ni dogodkov!"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -51,8 +51,8 @@
|
|||
"uptime": "系統運作時間",
|
||||
"days": "天",
|
||||
"wan": "WAN",
|
||||
"lan": "LAN",
|
||||
"wlan": "WLAN",
|
||||
"lan": "區域網路",
|
||||
"wlan": "無線區域網路",
|
||||
"devices": "設備",
|
||||
"lan_devices": "有線設備",
|
||||
"wlan_devices": "無線設備",
|
||||
|
@ -81,18 +81,18 @@
|
|||
},
|
||||
"ping": {
|
||||
"error": "錯誤",
|
||||
"ping": "Ping",
|
||||
"ping": "延遲",
|
||||
"down": "Down",
|
||||
"up": "Up",
|
||||
"not_available": "Not Available"
|
||||
"not_available": "不可用"
|
||||
},
|
||||
"siteMonitor": {
|
||||
"http_status": "HTTP status",
|
||||
"http_status": "HTTP 狀態",
|
||||
"error": "錯誤",
|
||||
"response": "Response",
|
||||
"response": "回應",
|
||||
"down": "Down",
|
||||
"up": "Up",
|
||||
"not_available": "Not Available"
|
||||
"not_available": "不可用"
|
||||
},
|
||||
"emby": {
|
||||
"playing": "正在播放",
|
||||
|
@ -270,7 +270,7 @@
|
|||
"speedtest": {
|
||||
"upload": "上傳速率",
|
||||
"download": "下載速率",
|
||||
"ping": "Ping"
|
||||
"ping": "延遲"
|
||||
},
|
||||
"portainer": {
|
||||
"running": "執行中",
|
||||
|
@ -542,7 +542,7 @@
|
|||
"streams_xepg": "XEPG頻道"
|
||||
},
|
||||
"opendtu": {
|
||||
"yieldDay": "Today",
|
||||
"yieldDay": "今日",
|
||||
"absolutePower": "Power",
|
||||
"relativePower": "Power %",
|
||||
"limit": "Limit"
|
||||
|
@ -667,7 +667,7 @@
|
|||
"targets_total": "目標總數"
|
||||
},
|
||||
"ghostfolio": {
|
||||
"gross_percent_today": "Today",
|
||||
"gross_percent_today": "今日",
|
||||
"gross_percent_1y": "一年",
|
||||
"gross_percent_max": "所有時間"
|
||||
},
|
||||
|
@ -725,10 +725,10 @@
|
|||
"players": "玩家",
|
||||
"maxPlayers": "玩家數上限",
|
||||
"bots": "機器人",
|
||||
"ping": "Ping"
|
||||
"ping": "延遲"
|
||||
},
|
||||
"urbackup": {
|
||||
"ok": "Ok",
|
||||
"ok": "確定",
|
||||
"errored": "錯誤",
|
||||
"noRecent": "已過時",
|
||||
"totalUsed": "已使用空間"
|
||||
|
@ -765,6 +765,6 @@
|
|||
"inCinemas": "In cinemas",
|
||||
"physicalRelease": "Physical release",
|
||||
"digitalRelease": "Digital release",
|
||||
"noEventsToday": "No events for today!"
|
||||
"noEventsToday": "今日無事件"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { useContext } from "react";
|
||||
import Image from "next/image";
|
||||
import Image from "next/future/image";
|
||||
|
||||
import { SettingsContext } from "utils/contexts/settings";
|
||||
import { ThemeContext } from "utils/contexts/theme";
|
||||
|
|
|
@ -6,7 +6,7 @@ import { MdKeyboardArrowDown } from "react-icons/md";
|
|||
import List from "components/services/list";
|
||||
import ResolvedIcon from "components/resolvedicon";
|
||||
|
||||
export default function ServicesGroup({ group, services, layout, fiveColumns, disableCollapse }) {
|
||||
export default function ServicesGroup({ group, services, layout, fiveColumns, disableCollapse, useEqualHeights }) {
|
||||
const panel = useRef();
|
||||
|
||||
return (
|
||||
|
@ -62,7 +62,7 @@ export default function ServicesGroup({ group, services, layout, fiveColumns, di
|
|||
}}
|
||||
>
|
||||
<Disclosure.Panel className="transition-all overflow-hidden duration-300 ease-out" ref={panel} static>
|
||||
<List group={group} services={services.services} layout={layout} />
|
||||
<List group={group} services={services.services} layout={layout} useEqualHeights={useEqualHeights} />
|
||||
</Disclosure.Panel>
|
||||
</Transition>
|
||||
</>
|
||||
|
|
|
@ -12,7 +12,7 @@ import Kubernetes from "widgets/kubernetes/component";
|
|||
import { SettingsContext } from "utils/contexts/settings";
|
||||
import ResolvedIcon from "components/resolvedicon";
|
||||
|
||||
export default function Item({ service, group }) {
|
||||
export default function Item({ service, group, useEqualHeights }) {
|
||||
const hasLink = service.href && service.href !== "#";
|
||||
const { settings } = useContext(SettingsContext);
|
||||
const showStats = service.showStats === false ? false : settings.showStats;
|
||||
|
@ -37,7 +37,8 @@ export default function Item({ service, group }) {
|
|||
className={classNames(
|
||||
settings.cardBlur !== undefined && `backdrop-blur${settings.cardBlur.length ? "-" : ""}${settings.cardBlur}`,
|
||||
hasLink && "cursor-pointer",
|
||||
"transition-all h-15 mb-2 p-1 rounded-md font-medium text-theme-700 dark:text-theme-200 dark:hover:text-theme-300 shadow-md shadow-theme-900/10 dark:shadow-theme-900/20 bg-theme-100/20 hover:bg-theme-300/20 dark:bg-white/5 dark:hover:bg-white/10 relative overflow-clip service-card",
|
||||
useEqualHeights && "h-[calc(100%-0.5rem)]",
|
||||
"transition-all mb-2 p-1 rounded-md font-medium text-theme-700 dark:text-theme-200 dark:hover:text-theme-300 shadow-md shadow-theme-900/10 dark:shadow-theme-900/20 bg-theme-100/20 hover:bg-theme-300/20 dark:bg-white/5 dark:hover:bg-white/10 relative overflow-clip service-card",
|
||||
)}
|
||||
>
|
||||
<div className="flex select-none z-0 service-title">
|
||||
|
|
|
@ -4,7 +4,7 @@ import { columnMap } from "../../utils/layout/columns";
|
|||
|
||||
import Item from "components/services/item";
|
||||
|
||||
export default function List({ group, services, layout }) {
|
||||
export default function List({ group, services, layout, useEqualHeights }) {
|
||||
return (
|
||||
<ul
|
||||
className={classNames(
|
||||
|
@ -13,7 +13,12 @@ export default function List({ group, services, layout }) {
|
|||
)}
|
||||
>
|
||||
{services.map((service) => (
|
||||
<Item key={service.container ?? service.app ?? service.name} service={service} group={group} />
|
||||
<Item
|
||||
key={service.container ?? service.app ?? service.name}
|
||||
service={service}
|
||||
group={group}
|
||||
useEqualHeights={layout?.useEqualHeights ?? useEqualHeights}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
|
|
|
@ -12,7 +12,6 @@ import { ColorProvider } from "utils/contexts/color";
|
|||
import { ThemeProvider } from "utils/contexts/theme";
|
||||
import { SettingsProvider } from "utils/contexts/settings";
|
||||
import { TabProvider } from "utils/contexts/tab";
|
||||
import { EventProvider } from "utils/contexts/calendar";
|
||||
|
||||
function MyApp({ Component, pageProps }) {
|
||||
return (
|
||||
|
@ -32,9 +31,7 @@ function MyApp({ Component, pageProps }) {
|
|||
<ThemeProvider>
|
||||
<SettingsProvider>
|
||||
<TabProvider>
|
||||
<EventProvider>
|
||||
<Component {...pageProps} />
|
||||
</EventProvider>
|
||||
<Component {...pageProps} />
|
||||
</TabProvider>
|
||||
</SettingsProvider>
|
||||
</ThemeProvider>
|
||||
|
|
|
@ -10,7 +10,6 @@ export default function Document() {
|
|||
/>
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<link rel="manifest" href="/site.webmanifest?v=4" crossOrigin="use-credentials" />
|
||||
<link rel="mask-icon" href="/safari-pinned-tab.svg?v=4" color="#1e9cd7" />
|
||||
</Head>
|
||||
<body>
|
||||
<Main />
|
||||
|
|
|
@ -223,7 +223,10 @@ function Home({ initialSettings }) {
|
|||
useEffect(() => {
|
||||
function handleKeyDown(e) {
|
||||
if (e.target.tagName === "BODY" || e.target.id === "inner_wrapper") {
|
||||
if (e.key.length === 1 && e.key.match(/(\w|\s)/g) && !(e.altKey || e.ctrlKey || e.metaKey || e.shiftKey)) {
|
||||
if (
|
||||
(e.key.length === 1 && e.key.match(/(\w|\s)/g) && !(e.altKey || e.ctrlKey || e.metaKey || e.shiftKey)) ||
|
||||
(e.key === "v" && (e.ctrlKey || e.metaKey))
|
||||
) {
|
||||
setSearching(true);
|
||||
} else if (e.key === "Escape") {
|
||||
setSearchString("");
|
||||
|
@ -304,6 +307,7 @@ function Home({ initialSettings }) {
|
|||
layout={settings.layout?.[group.name]}
|
||||
fiveColumns={settings.fiveColumns}
|
||||
disableCollapse={settings.disableCollapse}
|
||||
useEqualHeights={settings.useEqualHeights}
|
||||
/>
|
||||
) : (
|
||||
<BookmarksGroup
|
||||
|
@ -352,6 +356,7 @@ function Home({ initialSettings }) {
|
|||
settings.layout,
|
||||
settings.fiveColumns,
|
||||
settings.disableCollapse,
|
||||
settings.useEqualHeights,
|
||||
settings.cardBlur,
|
||||
initialSettings.layout,
|
||||
]);
|
||||
|
@ -363,8 +368,8 @@ function Home({ initialSettings }) {
|
|||
{settings.base && <base href={settings.base} />}
|
||||
{settings.favicon ? (
|
||||
<>
|
||||
<link rel="apple-touch-icon" sizes="180x180" href={settings.favicon} />
|
||||
<link rel="icon" href={settings.favicon} />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href={settings.favicon} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
|
@ -372,6 +377,7 @@ function Home({ initialSettings }) {
|
|||
<link rel="shortcut icon" href="/homepage.ico" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png?v=4" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png?v=4" />
|
||||
<link rel="mask-icon" href="/safari-pinned-tab.svg?v=4" color="#1e9cd7" />
|
||||
</>
|
||||
)}
|
||||
<meta name="msapplication-TileColor" content={themes[settings.color || "slate"][settings.theme || "dark"]} />
|
||||
|
@ -487,7 +493,7 @@ export default function Wrapper({ initialSettings, fallback }) {
|
|||
rgb(var(--bg-color) / ${opacityValue}),
|
||||
rgb(var(--bg-color) / ${opacityValue})
|
||||
),
|
||||
url(${backgroundImage})`;
|
||||
url('${backgroundImage}')`;
|
||||
wrappedStyle.backgroundPosition = "center";
|
||||
wrappedStyle.backgroundSize = "cover";
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
# For configuration options and examples, please see:
|
||||
# https://gethomepage.dev/latest/configs/widgets
|
||||
# https://gethomepage.dev/latest/configs/service-widgets
|
||||
|
||||
- resources:
|
||||
cpu: true
|
||||
|
|
|
@ -14,13 +14,13 @@
|
|||
--color-logo-stop: 128 128 128 / 40%;
|
||||
}
|
||||
|
||||
.theme-white .bg-theme-100\/20,
|
||||
.theme-white .dark\:bg-white\/5 {
|
||||
.theme-white .bg-theme-100\/20:not([class^="backdrop-blur"]),
|
||||
.theme-white .dark\:bg-white\/5:not([class^="backdrop-blur"]) {
|
||||
background-color: rgb(245, 245, 245);
|
||||
}
|
||||
|
||||
.theme-white .bg-theme-100\/20:hover,
|
||||
.theme-white .dark\:bg-white\/5:hover {
|
||||
.theme-white .bg-theme-100\/20:hover:not([class^="backdrop-blur"]),
|
||||
.theme-white .dark\:bg-white\/5:hover:not([class^="backdrop-blur"]) {
|
||||
background-color: rgb(250, 250, 250);
|
||||
}
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ import Docker from "dockerode";
|
|||
import { CustomObjectsApi, NetworkingV1Api, ApiextensionsV1Api } from "@kubernetes/client-node";
|
||||
|
||||
import createLogger from "utils/logger";
|
||||
import checkAndCopyConfig, { CONF_DIR, substituteEnvironmentVars } from "utils/config/config";
|
||||
import checkAndCopyConfig, { CONF_DIR, getSettings, substituteEnvironmentVars } from "utils/config/config";
|
||||
import getDockerArguments from "utils/config/docker";
|
||||
import getKubeConfig from "utils/config/kubernetes";
|
||||
import * as shvl from "utils/config/shvl";
|
||||
|
@ -59,6 +59,8 @@ export async function servicesFromDocker() {
|
|||
return [];
|
||||
}
|
||||
|
||||
const { instanceName } = getSettings();
|
||||
|
||||
const serviceServers = await Promise.all(
|
||||
Object.keys(servers).map(async (serverName) => {
|
||||
try {
|
||||
|
@ -82,6 +84,13 @@ export async function servicesFromDocker() {
|
|||
|
||||
Object.keys(containerLabels).forEach((label) => {
|
||||
if (label.startsWith("homepage.")) {
|
||||
let value = label.replace("homepage.", "");
|
||||
if (instanceName && value.startsWith(`instance.${instanceName}.`)) {
|
||||
value = value.replace(`instance.${instanceName}.`, "");
|
||||
} else if (value.startsWith("instance.")) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!constructedService) {
|
||||
constructedService = {
|
||||
container: containerName.replace(/^\//, ""),
|
||||
|
@ -89,11 +98,7 @@ export async function servicesFromDocker() {
|
|||
type: "service",
|
||||
};
|
||||
}
|
||||
shvl.set(
|
||||
constructedService,
|
||||
label.replace("homepage.", ""),
|
||||
substituteEnvironmentVars(containerLabels[label]),
|
||||
);
|
||||
shvl.set(constructedService, value, substituteEnvironmentVars(containerLabels[label]));
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -252,7 +257,7 @@ export async function servicesFromKubernetes() {
|
|||
constructedService.external =
|
||||
String(ingress.metadata.annotations[`${ANNOTATION_BASE}/external`]).toLowerCase() === "true";
|
||||
}
|
||||
if (ingress.metadata.annotations[`${ANNOTATION_BASE}/pod-selector`]) {
|
||||
if (ingress.metadata.annotations[`${ANNOTATION_BASE}/pod-selector`] !== undefined) {
|
||||
constructedService.podSelector = ingress.metadata.annotations[`${ANNOTATION_BASE}/pod-selector`];
|
||||
}
|
||||
if (ingress.metadata.annotations[`${ANNOTATION_BASE}/ping`]) {
|
||||
|
@ -331,48 +336,89 @@ export function cleanServiceGroups(groups) {
|
|||
|
||||
if (cleanedService.widget) {
|
||||
// whitelisted set of keys to pass to the frontend
|
||||
// alphabetical, grouped by widget(s)
|
||||
const {
|
||||
type, // all widgets
|
||||
// all widgets
|
||||
fields,
|
||||
hideErrors,
|
||||
server, // docker widget
|
||||
container,
|
||||
currency, // coinmarketcap widget
|
||||
symbols,
|
||||
slugs,
|
||||
defaultinterval,
|
||||
site, // unifi widget
|
||||
namespace, // kubernetes widget
|
||||
app,
|
||||
podSelector,
|
||||
wan, // opnsense widget, pfsense widget
|
||||
enableBlocks, // emby/jellyfin
|
||||
enableNowPlaying,
|
||||
volume, // diskstation widget,
|
||||
enableQueue, // sonarr/radarr
|
||||
node, // Proxmox
|
||||
snapshotHost, // kopia
|
||||
snapshotPath,
|
||||
userEmail, // azuredevops
|
||||
type,
|
||||
|
||||
// azuredevops
|
||||
repositoryId,
|
||||
metric, // glances
|
||||
chart, // glances
|
||||
stream, // mjpeg
|
||||
fit,
|
||||
method, // openmediavault widget
|
||||
mappings, // customapi widget
|
||||
refreshInterval,
|
||||
integrations, // calendar widget
|
||||
userEmail,
|
||||
|
||||
// calendar
|
||||
firstDayInWeek,
|
||||
view,
|
||||
integrations,
|
||||
maxEvents,
|
||||
src, // iframe widget
|
||||
classes,
|
||||
referrerPolicy,
|
||||
allowPolicy,
|
||||
showTime,
|
||||
previousDays,
|
||||
view,
|
||||
|
||||
// coinmarketcap
|
||||
currency,
|
||||
defaultinterval,
|
||||
slugs,
|
||||
symbols,
|
||||
|
||||
// customapi
|
||||
mappings,
|
||||
|
||||
// diskstation
|
||||
volume,
|
||||
|
||||
// docker
|
||||
container,
|
||||
server,
|
||||
|
||||
// emby, jellyfin
|
||||
enableBlocks,
|
||||
enableNowPlaying,
|
||||
|
||||
// glances
|
||||
chart,
|
||||
metric,
|
||||
pointsLimit,
|
||||
|
||||
// glances, customapi, iframe
|
||||
refreshInterval,
|
||||
|
||||
// iframe
|
||||
allowFullscreen,
|
||||
loadingStrategy,
|
||||
allowPolicy,
|
||||
allowScrolling,
|
||||
classes,
|
||||
loadingStrategy,
|
||||
referrerPolicy,
|
||||
src,
|
||||
|
||||
// kopia
|
||||
snapshotHost,
|
||||
snapshotPath,
|
||||
|
||||
// kubernetes
|
||||
app,
|
||||
namespace,
|
||||
podSelector,
|
||||
|
||||
// mjpeg
|
||||
fit,
|
||||
stream,
|
||||
|
||||
// openmediavault
|
||||
method,
|
||||
|
||||
// opnsense, pfsense
|
||||
wan,
|
||||
|
||||
// proxmox
|
||||
node,
|
||||
|
||||
// sonarr, radarr
|
||||
enableQueue,
|
||||
|
||||
// unifi
|
||||
site,
|
||||
} = cleanedService.widget;
|
||||
|
||||
let fieldsList = fields;
|
||||
|
@ -454,6 +500,8 @@ export function cleanServiceGroups(groups) {
|
|||
} else {
|
||||
cleanedService.widget.chart = true;
|
||||
}
|
||||
if (refreshInterval) cleanedService.widget.refreshInterval = refreshInterval;
|
||||
if (pointsLimit) cleanedService.widget.pointsLimit = pointsLimit;
|
||||
}
|
||||
if (type === "mjpeg") {
|
||||
if (stream) cleanedService.widget.stream = stream;
|
||||
|
@ -471,6 +519,8 @@ export function cleanServiceGroups(groups) {
|
|||
if (firstDayInWeek) cleanedService.widget.firstDayInWeek = firstDayInWeek;
|
||||
if (view) cleanedService.widget.view = view;
|
||||
if (maxEvents) cleanedService.widget.maxEvents = maxEvents;
|
||||
if (previousDays) cleanedService.widget.previousDays = previousDays;
|
||||
if (showTime) cleanedService.widget.showTime = showTime;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
import { createContext, useState, useMemo } from "react";
|
||||
|
||||
export const EventContext = createContext();
|
||||
|
||||
export function EventProvider({ initialEvent, children }) {
|
||||
const [events, setEvents] = useState({});
|
||||
|
||||
if (initialEvent) {
|
||||
setEvents(initialEvent);
|
||||
}
|
||||
|
||||
const value = useMemo(() => ({ events, setEvents }), [events]);
|
||||
|
||||
return <EventContext.Provider value={value}>{children}</EventContext.Provider>;
|
||||
}
|
|
@ -1,45 +1,11 @@
|
|||
import { useContext, useState } from "react";
|
||||
import { DateTime } from "luxon";
|
||||
import classNames from "classnames";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { IoMdCheckmarkCircleOutline } from "react-icons/io";
|
||||
|
||||
import { EventContext } from "../../utils/contexts/calendar";
|
||||
import Event from "./event";
|
||||
|
||||
export function Event({ event, colorVariants, showDate = false }) {
|
||||
const [hover, setHover] = useState(false);
|
||||
const { i18n } = useTranslation();
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex flex-row text-theme-700 dark:text-theme-200 items-center text-xs text-left h-5 rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1"
|
||||
onMouseEnter={() => setHover(!hover)}
|
||||
onMouseLeave={() => setHover(!hover)}
|
||||
>
|
||||
<span className="ml-2 w-10">
|
||||
<span>
|
||||
{showDate &&
|
||||
event.date.setLocale(i18n.language).startOf("day").toLocaleString({ month: "short", day: "numeric" })}
|
||||
</span>
|
||||
</span>
|
||||
<span className="ml-2 h-2 w-2">
|
||||
<span className={classNames("block w-2 h-2 rounded", colorVariants[event.color] ?? "gray")} />
|
||||
</span>
|
||||
<div className="ml-2 h-5 text-left relative truncate" style={{ width: "70%" }}>
|
||||
<div className="absolute mt-0.5 text-xs">{hover && event.additional ? event.additional : event.title}</div>
|
||||
</div>
|
||||
{event.isCompleted && (
|
||||
<span className="text-xs mr-1 ml-auto z-10">
|
||||
<IoMdCheckmarkCircleOutline />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Agenda({ service, colorVariants, showDate }) {
|
||||
export default function Agenda({ service, colorVariants, events, showDate }) {
|
||||
const { widget } = service;
|
||||
const { events } = useContext(EventContext);
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (!showDate) {
|
||||
|
@ -48,7 +14,9 @@ export default function Agenda({ service, colorVariants, showDate }) {
|
|||
|
||||
const eventsArray = Object.keys(events)
|
||||
.filter(
|
||||
(eventKey) => showDate.startOf("day").toUnixInteger() <= events[eventKey].date?.startOf("day").toUnixInteger(),
|
||||
(eventKey) =>
|
||||
showDate.minus({ days: widget?.previousDays ?? 0 }).startOf("day").ts <=
|
||||
events[eventKey].date?.startOf("day").ts,
|
||||
)
|
||||
.map((eventKey) => events[eventKey])
|
||||
.sort((a, b) => a.date - b.date)
|
||||
|
@ -57,10 +25,8 @@ export default function Agenda({ service, colorVariants, showDate }) {
|
|||
if (!eventsArray.length) {
|
||||
return (
|
||||
<div className="text-center">
|
||||
<div className="p-2 ">
|
||||
<div
|
||||
className={classNames("flex flex-col pt-1 pb-1", !eventsArray.length && !events.length && "animate-pulse")}
|
||||
>
|
||||
<div className="pl-2 pr-2">
|
||||
<div className={classNames("flex flex-col", !eventsArray.length && !events.length && "animate-pulse")}>
|
||||
<Event
|
||||
key="no-event"
|
||||
event={{
|
||||
|
@ -80,16 +46,17 @@ export default function Agenda({ service, colorVariants, showDate }) {
|
|||
const eventsByDay = days.map((d) => eventsArray.filter((e) => e.date.startOf("day").ts === d));
|
||||
|
||||
return (
|
||||
<div className="p-2">
|
||||
<div className={classNames("flex flex-col pt-1 pb-1", !eventsArray.length && !events.length && "animate-pulse")}>
|
||||
<div className="pl-1 pr-1 pb-1">
|
||||
<div className={classNames("flex flex-col", !eventsArray.length && !events.length && "animate-pulse")}>
|
||||
{eventsByDay.map((eventsDay, i) => (
|
||||
<div key={days[i]}>
|
||||
{eventsDay.map((event, j) => (
|
||||
<Event
|
||||
key={`event${event.title}-${event.date}`}
|
||||
key={`event-agenda-${event.title}-${event.date}-${event.additional}`}
|
||||
event={event}
|
||||
colorVariants={colorVariants}
|
||||
showDate={j === 0}
|
||||
showTime={widget?.showTime && event.date.startOf("day").ts === showDate.startOf("day").ts}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
@ -40,6 +40,7 @@ export default function Component({ service }) {
|
|||
const { widget } = service;
|
||||
const { i18n } = useTranslation();
|
||||
const [showDate, setShowDate] = useState(null);
|
||||
const [events, setEvents] = useState({});
|
||||
const currentDate = DateTime.now().setLocale(i18n.language).startOf("day");
|
||||
const { settings } = useContext(SettingsContext);
|
||||
|
||||
|
@ -69,9 +70,9 @@ export default function Component({ service }) {
|
|||
?.filter((integration) => integration?.type)
|
||||
.map((integration) => ({
|
||||
service: dynamic(() => import(`./integrations/${integration.type}`)),
|
||||
widget: integration,
|
||||
widget: { ...widget, ...integration },
|
||||
})) ?? [],
|
||||
[widget.integrations],
|
||||
[widget],
|
||||
);
|
||||
|
||||
return (
|
||||
|
@ -80,13 +81,14 @@ export default function Component({ service }) {
|
|||
<div className="sticky top-0">
|
||||
{integrations.map((integration) => {
|
||||
const Integration = integration.service;
|
||||
const key = integration.widget.type + integration.widget.service_name + integration.widget.service_group;
|
||||
const key = `integration-${integration.widget.type}-${integration.widget.service_name}-${integration.widget.service_group}-${integration.widget.name}`;
|
||||
|
||||
return (
|
||||
<Integration
|
||||
key={key}
|
||||
config={integration.widget}
|
||||
params={params}
|
||||
setEvents={setEvents}
|
||||
hideErrors={settings.hideErrors}
|
||||
className="fixed bottom-0 left-0 bg-red-500 w-screen h-12"
|
||||
/>
|
||||
|
@ -95,8 +97,10 @@ export default function Component({ service }) {
|
|||
</div>
|
||||
{(!widget?.view || widget?.view === "monthly") && (
|
||||
<Monthly
|
||||
key={`monthly-${showDate?.toFormat("yyyy-MM-dd")}`}
|
||||
service={service}
|
||||
colorVariants={colorVariants}
|
||||
events={events}
|
||||
showDate={showDate}
|
||||
setShowDate={setShowDate}
|
||||
className="flex"
|
||||
|
@ -104,8 +108,10 @@ export default function Component({ service }) {
|
|||
)}
|
||||
{widget?.view === "agenda" && (
|
||||
<Agenda
|
||||
key={`agenda-${showDate?.toFormat("yyyy-MM-dd")}`}
|
||||
service={service}
|
||||
colorVariants={colorVariants}
|
||||
events={events}
|
||||
showDate={showDate}
|
||||
setShowDate={setShowDate}
|
||||
className="flex"
|
||||
|
|
41
src/widgets/calendar/event.jsx
Normal file
41
src/widgets/calendar/event.jsx
Normal file
|
@ -0,0 +1,41 @@
|
|||
import { useState } from "react";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { DateTime } from "luxon";
|
||||
import classNames from "classnames";
|
||||
import { IoMdCheckmarkCircleOutline } from "react-icons/io";
|
||||
|
||||
export default function Event({ event, colorVariants, showDate = false, showTime = false, showDateColumn = true }) {
|
||||
const [hover, setHover] = useState(false);
|
||||
const { i18n } = useTranslation();
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex flex-row text-theme-700 dark:text-theme-200 items-center text-xs relative h-5 w-full rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1"
|
||||
onMouseEnter={() => setHover(!hover)}
|
||||
onMouseLeave={() => setHover(!hover)}
|
||||
key={`event-${event.title}-${event.date}-${event.additional}`}
|
||||
>
|
||||
{showDateColumn && (
|
||||
<span className="ml-2 w-10">
|
||||
<span>
|
||||
{(showDate || showTime) &&
|
||||
event.date
|
||||
.setLocale(i18n.language)
|
||||
.toLocaleString(showTime ? DateTime.TIME_24_SIMPLE : { month: "short", day: "numeric" })}
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
<span className="ml-2 h-2 w-2">
|
||||
<span className={classNames("block w-2 h-2 rounded", colorVariants[event.color] ?? "gray")} />
|
||||
</span>
|
||||
<div className="ml-2 h-5 text-left relative truncate" style={{ width: "70%" }}>
|
||||
<div className="absolute mt-0.5 text-xs">{hover && event.additional ? event.additional : event.title}</div>
|
||||
</div>
|
||||
{event.isCompleted && (
|
||||
<span className="text-xs mr-1 ml-auto z-10">
|
||||
<IoMdCheckmarkCircleOutline />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
58
src/widgets/calendar/integrations/ical.jsx
Normal file
58
src/widgets/calendar/integrations/ical.jsx
Normal file
|
@ -0,0 +1,58 @@
|
|||
import { DateTime } from "luxon";
|
||||
import { parseString } from "cal-parser";
|
||||
import { useEffect } from "react";
|
||||
import { useTranslation } from "next-i18next";
|
||||
|
||||
import useWidgetAPI from "../../../utils/proxy/use-widget-api";
|
||||
import Error from "../../../components/services/widget/error";
|
||||
|
||||
export default function Integration({ config, params, setEvents, hideErrors }) {
|
||||
const { t } = useTranslation();
|
||||
const { data: icalData, error: icalError } = useWidgetAPI(config, config.name, {
|
||||
refreshInterval: 300000, // 5 minutes
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
let parsedIcal;
|
||||
|
||||
if (!icalError && icalData && !icalData.error) {
|
||||
parsedIcal = parseString(icalData.data);
|
||||
if (parsedIcal.events.length === 0) {
|
||||
icalData.error = { message: `'${config.name}': ${t("calendar.noEventsFound")}` };
|
||||
}
|
||||
}
|
||||
|
||||
if (icalError || !parsedIcal) {
|
||||
return;
|
||||
}
|
||||
|
||||
const eventsToAdd = {};
|
||||
const events = parsedIcal?.getEventsBetweenDates(
|
||||
DateTime.fromISO(params.start).toJSDate(),
|
||||
DateTime.fromISO(params.end).toJSDate(),
|
||||
);
|
||||
|
||||
events?.forEach((event) => {
|
||||
let title = `${event?.summary?.value}`;
|
||||
if (config?.params?.showName) {
|
||||
title = `${config.name}: ${title}`;
|
||||
}
|
||||
|
||||
event.matchingDates.forEach((date) => {
|
||||
eventsToAdd[event?.uid?.value] = {
|
||||
title,
|
||||
date: DateTime.fromJSDate(date),
|
||||
color: config?.color ?? "zinc",
|
||||
isCompleted: DateTime.fromJSDate(date) < DateTime.now(),
|
||||
additional: event.location?.value,
|
||||
type: "ical",
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
setEvents((prevEvents) => ({ ...prevEvents, ...eventsToAdd }));
|
||||
}, [icalData, icalError, config, params, setEvents, t]);
|
||||
|
||||
const error = icalError ?? icalData?.error;
|
||||
return error && !hideErrors && <Error error={{ message: `${config.type}: ${error.message ?? error}` }} />;
|
||||
}
|
|
@ -1,12 +1,10 @@
|
|||
import { DateTime } from "luxon";
|
||||
import { useContext, useEffect } from "react";
|
||||
import { useEffect } from "react";
|
||||
|
||||
import useWidgetAPI from "../../../utils/proxy/use-widget-api";
|
||||
import { EventContext } from "../../../utils/contexts/calendar";
|
||||
import Error from "../../../components/services/widget/error";
|
||||
|
||||
export default function Integration({ config, params, hideErrors = false }) {
|
||||
const { setEvents } = useContext(EventContext);
|
||||
export default function Integration({ config, params, setEvents, hideErrors = false }) {
|
||||
const { data: lidarrData, error: lidarrError } = useWidgetAPI(config, "calendar", {
|
||||
...params,
|
||||
includeArtist: "false",
|
||||
|
|
|
@ -1,14 +1,12 @@
|
|||
import { DateTime } from "luxon";
|
||||
import { useEffect, useContext } from "react";
|
||||
import { useEffect } from "react";
|
||||
import { useTranslation } from "next-i18next";
|
||||
|
||||
import useWidgetAPI from "../../../utils/proxy/use-widget-api";
|
||||
import { EventContext } from "../../../utils/contexts/calendar";
|
||||
import Error from "../../../components/services/widget/error";
|
||||
|
||||
export default function Integration({ config, params, hideErrors = false }) {
|
||||
export default function Integration({ config, params, setEvents, hideErrors = false }) {
|
||||
const { t } = useTranslation();
|
||||
const { setEvents } = useContext(EventContext);
|
||||
const { data: radarrData, error: radarrError } = useWidgetAPI(config, "calendar", {
|
||||
...params,
|
||||
...(config?.params ?? {}),
|
||||
|
@ -25,27 +23,35 @@ export default function Integration({ config, params, hideErrors = false }) {
|
|||
const physicalTitle = `${event.title} - ${t("calendar.physicalRelease")}`;
|
||||
const digitalTitle = `${event.title} - ${t("calendar.digitalRelease")}`;
|
||||
|
||||
eventsToAdd[cinemaTitle] = {
|
||||
title: cinemaTitle,
|
||||
date: DateTime.fromISO(event.inCinemas),
|
||||
color: config?.color ?? "amber",
|
||||
isCompleted: event.isAvailable,
|
||||
additional: "",
|
||||
};
|
||||
eventsToAdd[physicalTitle] = {
|
||||
title: physicalTitle,
|
||||
date: DateTime.fromISO(event.physicalRelease),
|
||||
color: config?.color ?? "cyan",
|
||||
isCompleted: event.isAvailable,
|
||||
additional: "",
|
||||
};
|
||||
eventsToAdd[digitalTitle] = {
|
||||
title: digitalTitle,
|
||||
date: DateTime.fromISO(event.digitalRelease),
|
||||
color: config?.color ?? "emerald",
|
||||
isCompleted: event.isAvailable,
|
||||
additional: "",
|
||||
};
|
||||
if (event.inCinemas) {
|
||||
eventsToAdd[cinemaTitle] = {
|
||||
title: cinemaTitle,
|
||||
date: DateTime.fromISO(event.inCinemas),
|
||||
color: config?.color ?? "amber",
|
||||
isCompleted: event.hasFile,
|
||||
additional: "",
|
||||
};
|
||||
}
|
||||
|
||||
if (event.physicalRelease) {
|
||||
eventsToAdd[physicalTitle] = {
|
||||
title: physicalTitle,
|
||||
date: DateTime.fromISO(event.physicalRelease),
|
||||
color: config?.color ?? "cyan",
|
||||
isCompleted: event.hasFile,
|
||||
additional: "",
|
||||
};
|
||||
}
|
||||
|
||||
if (event.digitalRelease) {
|
||||
eventsToAdd[digitalTitle] = {
|
||||
title: digitalTitle,
|
||||
date: DateTime.fromISO(event.digitalRelease),
|
||||
color: config?.color ?? "emerald",
|
||||
isCompleted: event.hasFile,
|
||||
additional: "",
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
setEvents((prevEvents) => ({ ...prevEvents, ...eventsToAdd }));
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
import { DateTime } from "luxon";
|
||||
import { useEffect, useContext } from "react";
|
||||
import { useEffect } from "react";
|
||||
|
||||
import useWidgetAPI from "../../../utils/proxy/use-widget-api";
|
||||
import { EventContext } from "../../../utils/contexts/calendar";
|
||||
import Error from "../../../components/services/widget/error";
|
||||
|
||||
export default function Integration({ config, params, hideErrors = false }) {
|
||||
const { setEvents } = useContext(EventContext);
|
||||
export default function Integration({ config, params, setEvents, hideErrors = false }) {
|
||||
const { data: readarrData, error: readarrError } = useWidgetAPI(config, "calendar", {
|
||||
...params,
|
||||
includeAuthor: "true",
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
import { DateTime } from "luxon";
|
||||
import { useEffect, useContext } from "react";
|
||||
import { useEffect } from "react";
|
||||
|
||||
import useWidgetAPI from "../../../utils/proxy/use-widget-api";
|
||||
import { EventContext } from "../../../utils/contexts/calendar";
|
||||
import Error from "../../../components/services/widget/error";
|
||||
|
||||
export default function Integration({ config, params, hideErrors = false }) {
|
||||
const { setEvents } = useContext(EventContext);
|
||||
export default function Integration({ config, params, setEvents, hideErrors = false }) {
|
||||
const { data: sonarrData, error: sonarrError } = useWidgetAPI(config, "calendar", {
|
||||
...params,
|
||||
includeSeries: "true",
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import { useContext, useMemo } from "react";
|
||||
import { useMemo } from "react";
|
||||
import { DateTime, Info } from "luxon";
|
||||
import classNames from "classnames";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { IoMdCheckmarkCircleOutline } from "react-icons/io";
|
||||
|
||||
import { EventContext } from "../../utils/contexts/calendar";
|
||||
import Event from "./event";
|
||||
|
||||
const cellStyle = "relative w-10 flex items-center justify-center flex-col";
|
||||
const monthButton = "pl-6 pr-6 ml-2 mr-2 hover:bg-theme-100/20 dark:hover:bg-white/5 rounded-md cursor-pointer";
|
||||
|
@ -32,11 +31,11 @@ export function Day({ weekNumber, weekday, events, colorVariants, showDate, setS
|
|||
|
||||
// selected same day style
|
||||
style +=
|
||||
displayDate.toFormat("MM-dd-yyyy") === showDate.toFormat("MM-dd-yyyy")
|
||||
displayDate.startOf("day").ts === showDate.startOf("day").ts
|
||||
? "text-black-500 bg-theme-100/20 dark:bg-white/10 rounded-md "
|
||||
: "";
|
||||
|
||||
if (displayDate.toFormat("MM-dd-yyyy") === currentDate.toFormat("MM-dd-yyyy")) {
|
||||
if (displayDate.startOf("day").ts === currentDate.startOf("day").ts) {
|
||||
// today style
|
||||
style += "text-black-500 bg-theme-100/20 dark:bg-black/20 rounded-md ";
|
||||
} else {
|
||||
|
@ -61,7 +60,7 @@ export function Day({ weekNumber, weekday, events, colorVariants, showDate, setS
|
|||
.slice(0, 4)
|
||||
.map((event) => (
|
||||
<span
|
||||
key={event.date.toLocaleString() + event.color + event.title}
|
||||
key={`${event.date.ts}+${event.color}-${event.title}-${event.additional}`}
|
||||
className={classNames("inline-flex h-1 w-1 m-0.5 rounded", colorVariants[event.color] ?? "gray")}
|
||||
/>
|
||||
))}
|
||||
|
@ -70,25 +69,6 @@ export function Day({ weekNumber, weekday, events, colorVariants, showDate, setS
|
|||
);
|
||||
}
|
||||
|
||||
export function Event({ event }) {
|
||||
return (
|
||||
<div
|
||||
key={event.title}
|
||||
className="text-theme-700 dark:text-theme-200 text-xs relative h-5 w-full rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1"
|
||||
>
|
||||
<span className="absolute left-2 text-left text-xs mt-[2px] truncate text-ellipsis" style={{ width: "96%" }}>
|
||||
{event.title}
|
||||
{event.additional ? ` - ${event.additional}` : ""}
|
||||
</span>
|
||||
{event.isCompleted && (
|
||||
<span className="text-right text-xs flex justify-end mr-1 mt-1 z-10 ">
|
||||
<IoMdCheckmarkCircleOutline />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const dayInWeekId = {
|
||||
monday: 1,
|
||||
tuesday: 2,
|
||||
|
@ -99,10 +79,9 @@ const dayInWeekId = {
|
|||
sunday: 7,
|
||||
};
|
||||
|
||||
export default function Monthly({ service, colorVariants, showDate, setShowDate }) {
|
||||
export default function Monthly({ service, colorVariants, events, showDate, setShowDate }) {
|
||||
const { widget } = service;
|
||||
const { i18n } = useTranslation();
|
||||
const { events } = useContext(EventContext);
|
||||
const currentDate = DateTime.now().setLocale(i18n.language).startOf("day");
|
||||
|
||||
const dayNames = Info.weekdays("short", { locale: i18n.language });
|
||||
|
@ -161,7 +140,7 @@ export default function Monthly({ service, colorVariants, showDate, setShowDate
|
|||
</span>
|
||||
</div>
|
||||
|
||||
<div className="p-2 w-full">
|
||||
<div className="pl-1 pr-1 pb-1 w-full">
|
||||
<div className="flex justify-between flex-wrap">
|
||||
{dayNames.map((name) => (
|
||||
<span key={name} className={classNames(cellStyle)} style={{ width: "14%" }}>
|
||||
|
@ -172,7 +151,7 @@ export default function Monthly({ service, colorVariants, showDate, setShowDate
|
|||
|
||||
<div
|
||||
className={classNames(
|
||||
"flex justify-between flex-wrap",
|
||||
"flex justify-between flex-wrap pb-1",
|
||||
!eventsArray.length && widget?.integrations?.length && "animate-pulse",
|
||||
)}
|
||||
>
|
||||
|
@ -191,12 +170,18 @@ export default function Monthly({ service, colorVariants, showDate, setShowDate
|
|||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col pt-1 pb-1">
|
||||
<div className="flex flex-col">
|
||||
{eventsArray
|
||||
?.filter((event) => showDate.startOf("day").toUnixInteger() === event.date?.startOf("day").toUnixInteger())
|
||||
?.filter((event) => showDate.startOf("day").ts === event.date?.startOf("day").ts)
|
||||
.slice(0, widget?.maxEvents ?? 10)
|
||||
.map((event) => (
|
||||
<Event key={`event${event.title}-${event.additional}`} event={event} />
|
||||
<Event
|
||||
key={`event-monthly-${event.title}-${event.date}-${event.additional}`}
|
||||
event={event}
|
||||
colorVariants={colorVariants}
|
||||
showDateColumn={widget?.showTime ?? false}
|
||||
showTime={widget?.showTime && event.date.startOf("day").ts === showDate.startOf("day").ts}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
|
33
src/widgets/calendar/proxy.js
Normal file
33
src/widgets/calendar/proxy.js
Normal file
|
@ -0,0 +1,33 @@
|
|||
import getServiceWidget from "utils/config/service-helpers";
|
||||
import { httpProxy } from "utils/proxy/http";
|
||||
import createLogger from "utils/logger";
|
||||
|
||||
const logger = createLogger("calendarProxyHandler");
|
||||
|
||||
export default async function calendarProxyHandler(req, res) {
|
||||
const { group, service, endpoint } = req.query;
|
||||
|
||||
if (group && service) {
|
||||
const widget = await getServiceWidget(group, service);
|
||||
const integration = widget.integrations?.find((i) => i.name === endpoint);
|
||||
|
||||
if (integration) {
|
||||
if (!integration.url) {
|
||||
return res.status(403).json({ error: "No integration URL specified" });
|
||||
}
|
||||
|
||||
const [status, contentType, data] = await httpProxy(integration.url);
|
||||
|
||||
if (contentType) res.setHeader("Content-Type", contentType);
|
||||
|
||||
if (status !== 200) {
|
||||
logger.debug(`HTTTP ${status} retrieving data from integration URL ${integration.url} : ${data}`);
|
||||
return res.status(status).send(data);
|
||||
}
|
||||
|
||||
return res.status(status).json({ data: data.toString() });
|
||||
}
|
||||
}
|
||||
|
||||
return res.status(400).json({ error: "Invalid integration" });
|
||||
}
|
8
src/widgets/calendar/widget.js
Normal file
8
src/widgets/calendar/widget.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
import calendarProxyHandler from "./proxy";
|
||||
|
||||
const widget = {
|
||||
api: "{url}",
|
||||
proxyHandler: calendarProxyHandler,
|
||||
};
|
||||
|
||||
export default widget;
|
|
@ -28,7 +28,7 @@ export default function Component({ service }) {
|
|||
let diffsDetected = 0;
|
||||
|
||||
Object.keys(data).forEach((key) => {
|
||||
if (data[key].last_changed > 0 && data[key].last_checked === data[key].last_changed) {
|
||||
if (data[key].last_changed > 0 && data[key].last_checked === data[key].last_changed && !data[key].viewed) {
|
||||
diffsDetected += 1;
|
||||
}
|
||||
});
|
||||
|
|
|
@ -27,6 +27,7 @@ const components = {
|
|||
fileflows: dynamic(() => import("./fileflows/component")),
|
||||
flood: dynamic(() => import("./flood/component")),
|
||||
freshrss: dynamic(() => import("./freshrss/component")),
|
||||
fritzbox: dynamic(() => import("./fritzbox/component")),
|
||||
gamedig: dynamic(() => import("./gamedig/component")),
|
||||
ghostfolio: dynamic(() => import("./ghostfolio/component")),
|
||||
glances: dynamic(() => import("./glances/component")),
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import useSWR from "swr";
|
||||
import { useTranslation } from "next-i18next";
|
||||
|
||||
import { calculateCPUPercent, calculateUsedMemory } from "./stats-helpers";
|
||||
import { calculateCPUPercent, calculateUsedMemory, calculateThroughput } from "./stats-helpers";
|
||||
|
||||
import Container from "components/services/widget/container";
|
||||
import Block from "components/services/widget/block";
|
||||
|
@ -41,7 +41,7 @@ export default function Component({ service }) {
|
|||
);
|
||||
}
|
||||
|
||||
const network = statsData.stats.networks?.eth0 || statsData.stats.networks?.network;
|
||||
const { rxBytes, txBytes } = calculateThroughput(statsData.stats);
|
||||
|
||||
return (
|
||||
<Container service={service}>
|
||||
|
@ -49,10 +49,10 @@ export default function Component({ service }) {
|
|||
{statsData.stats.memory_stats.usage && (
|
||||
<Block label="docker.mem" value={t("common.bytes", { value: calculateUsedMemory(statsData.stats) })} />
|
||||
)}
|
||||
{network && (
|
||||
{statsData.stats.networks && (
|
||||
<>
|
||||
<Block label="docker.rx" value={t("common.bytes", { value: network.rx_bytes })} />
|
||||
<Block label="docker.tx" value={t("common.bytes", { value: network.tx_bytes })} />
|
||||
<Block label="docker.rx" value={t("common.bytes", { value: rxBytes })} />
|
||||
<Block label="docker.tx" value={t("common.bytes", { value: txBytes })} />
|
||||
</>
|
||||
)}
|
||||
</Container>
|
||||
|
|
|
@ -16,3 +16,18 @@ export function calculateUsedMemory(stats) {
|
|||
stats.memory_stats.usage - (stats.memory_stats.total_inactive_file ?? stats.memory_stats.stats?.inactive_file ?? 0)
|
||||
);
|
||||
}
|
||||
|
||||
export function calculateThroughput(stats) {
|
||||
let rxBytes = 0;
|
||||
let txBytes = 0;
|
||||
if (stats.networks?.network) {
|
||||
rxBytes = stats.networks?.network.rx_bytes;
|
||||
txBytes = stats.networks?.network.tx_bytes;
|
||||
} else if (stats.networks && Array.isArray(Object.values(stats.networks))) {
|
||||
Object.values(stats.networks).forEach((containerInterface) => {
|
||||
rxBytes += containerInterface.rx_bytes;
|
||||
txBytes += containerInterface.tx_bytes;
|
||||
});
|
||||
}
|
||||
return { rxBytes, txBytes };
|
||||
}
|
||||
|
|
69
src/widgets/fritzbox/component.jsx
Normal file
69
src/widgets/fritzbox/component.jsx
Normal file
|
@ -0,0 +1,69 @@
|
|||
import { useTranslation } from "next-i18next";
|
||||
|
||||
import Container from "components/services/widget/container";
|
||||
import Block from "components/services/widget/block";
|
||||
import useWidgetAPI from "utils/proxy/use-widget-api";
|
||||
|
||||
export const fritzboxDefaultFields = ["connectionStatus", "uptime", "maxDown", "maxUp"];
|
||||
|
||||
const formatUptime = (timestamp) => {
|
||||
const hours = Math.floor(timestamp / 3600);
|
||||
const minutes = Math.floor((timestamp % 3600) / 60);
|
||||
const seconds = timestamp % 60;
|
||||
|
||||
const hourDuration = hours > 0 ? `${hours}h` : "00h";
|
||||
const minDuration = minutes > 0 ? `${minutes}m` : "00m";
|
||||
const secDuration = seconds > 0 ? `${seconds}s` : "00s";
|
||||
|
||||
return hourDuration + minDuration + secDuration;
|
||||
};
|
||||
|
||||
export default function Component({ service }) {
|
||||
const { t } = useTranslation();
|
||||
const { widget } = service;
|
||||
const { data: fritzboxData, error: fritzboxError } = useWidgetAPI(widget, "status");
|
||||
|
||||
if (fritzboxError) {
|
||||
return <Container service={service} error={fritzboxError} />;
|
||||
}
|
||||
|
||||
// Default fields
|
||||
if (!widget.fields?.length > 0) {
|
||||
widget.fields = fritzboxDefaultFields;
|
||||
}
|
||||
const MAX_ALLOWED_FIELDS = 4;
|
||||
// Limits max number of displayed fields
|
||||
if (widget.fields?.length > MAX_ALLOWED_FIELDS) {
|
||||
widget.fields = widget.fields.slice(0, MAX_ALLOWED_FIELDS);
|
||||
}
|
||||
|
||||
if (!fritzboxData) {
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="fritzbox.connectionStatus" />
|
||||
<Block label="fritzbox.uptime" />
|
||||
<Block label="fritzbox.maxDown" />
|
||||
<Block label="fritzbox.maxUp" />
|
||||
<Block label="fritzbox.down" />
|
||||
<Block label="fritzbox.up" />
|
||||
<Block label="fritzbox.received" />
|
||||
<Block label="fritzbox.sent" />
|
||||
<Block label="fritzbox.externalIPAddress" />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="fritzbox.connectionStatus" value={t(`fritzbox.connectionStatus${fritzboxData.connectionStatus}`)} />
|
||||
<Block label="fritzbox.uptime" value={formatUptime(fritzboxData.uptime)} />
|
||||
<Block label="fritzbox.maxDown" value={t("common.byterate", { value: fritzboxData.maxDown / 8, decimals: 1 })} />
|
||||
<Block label="fritzbox.maxUp" value={t("common.byterate", { value: fritzboxData.maxUp / 8, decimals: 1 })} />
|
||||
<Block label="fritzbox.down" value={t("common.byterate", { value: fritzboxData.down, decimals: 1 })} />
|
||||
<Block label="fritzbox.up" value={t("common.byterate", { value: fritzboxData.up, decimals: 1 })} />
|
||||
<Block label="fritzbox.received" value={t("common.bytes", { value: fritzboxData.received })} />
|
||||
<Block label="fritzbox.sent" value={t("common.bytes", { value: fritzboxData.sent })} />
|
||||
<Block label="fritzbox.externalIPAddress" value={fritzboxData.externalIPAddress} />
|
||||
</Container>
|
||||
);
|
||||
}
|
96
src/widgets/fritzbox/proxy.js
Normal file
96
src/widgets/fritzbox/proxy.js
Normal file
|
@ -0,0 +1,96 @@
|
|||
import { xml2json } from "xml-js";
|
||||
|
||||
import { fritzboxDefaultFields } from "./component";
|
||||
|
||||
import { httpProxy } from "utils/proxy/http";
|
||||
import getServiceWidget from "utils/config/service-helpers";
|
||||
import createLogger from "utils/logger";
|
||||
|
||||
const logger = createLogger("fritzboxProxyHandler");
|
||||
|
||||
async function requestEndpoint(apiBaseUrl, service, action) {
|
||||
const servicePath = service === "WANIPConnection" ? "WANIPConn1" : "WANCommonIFC1";
|
||||
const params = {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "text/xml; charset='utf-8'",
|
||||
SoapAction: `urn:schemas-upnp-org:service:${service}:1#${action}`,
|
||||
},
|
||||
body:
|
||||
"<?xml version='1.0' encoding='utf-8'?>" +
|
||||
"<s:Envelope s:encodingStyle='http://schemas.xmlsoap.org/soap/encoding/' xmlns:s='http://schemas.xmlsoap.org/soap/envelope/'>" +
|
||||
"<s:Body>" +
|
||||
`<u:${action} xmlns:u='urn:schemas-upnp-org:service:${service}:1' />` +
|
||||
"</s:Body>" +
|
||||
"</s:Envelope>",
|
||||
};
|
||||
const apiUrl = `${apiBaseUrl}/igdupnp/control/${servicePath}`;
|
||||
const [status, , data] = await httpProxy(apiUrl, params);
|
||||
if (status !== 200) {
|
||||
logger.debug(`HTTP ${status} performing SoapRequest for ${service}->${action}`, data);
|
||||
throw new Error(`Failed fetching '${action}'`);
|
||||
}
|
||||
const response = {};
|
||||
try {
|
||||
const jsonData = JSON.parse(xml2json(data));
|
||||
const responseElements = jsonData?.elements[0]?.elements[0]?.elements[0]?.elements || [];
|
||||
responseElements.forEach((element) => {
|
||||
response[element.name] = element.elements[0]?.text || "";
|
||||
});
|
||||
} catch (e) {
|
||||
logger.debug(`Failed parsing ${service}->${action} response:`, data);
|
||||
throw new Error(`Failed parsing '${action}' response`);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
export default async function fritzboxProxyHandler(req, res) {
|
||||
const { group, service } = req.query;
|
||||
const serviceWidget = await getServiceWidget(group, service);
|
||||
|
||||
if (!serviceWidget) {
|
||||
res.status(500).json({ error: "Service widget not found" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!serviceWidget.url) {
|
||||
res.status(500).json({ error: "Service widget url not configured" });
|
||||
return;
|
||||
}
|
||||
|
||||
const serviceWidgetUrl = new URL(serviceWidget.url);
|
||||
const port = serviceWidgetUrl.protocol === "https:" ? 49443 : 49000;
|
||||
const apiBaseUrl = `${serviceWidgetUrl.protocol}//${serviceWidgetUrl.hostname}:${port}`;
|
||||
|
||||
if (!serviceWidget.fields?.length > 0) {
|
||||
serviceWidget.fields = fritzboxDefaultFields;
|
||||
}
|
||||
const requestStatusInfo = ["connectionStatus", "uptime"].some((field) => serviceWidget.fields.includes(field));
|
||||
const requestLinkProperties = ["maxDown", "maxUp"].some((field) => serviceWidget.fields.includes(field));
|
||||
const requestAddonInfos = ["down", "up", "received", "sent"].some((field) => serviceWidget.fields.includes(field));
|
||||
const requestExternalIPAddress = ["externalIPAddress"].some((field) => serviceWidget.fields.includes(field));
|
||||
|
||||
await Promise.all([
|
||||
requestStatusInfo ? requestEndpoint(apiBaseUrl, "WANIPConnection", "GetStatusInfo") : null,
|
||||
requestLinkProperties ? requestEndpoint(apiBaseUrl, "WANCommonInterfaceConfig", "GetCommonLinkProperties") : null,
|
||||
requestAddonInfos ? requestEndpoint(apiBaseUrl, "WANCommonInterfaceConfig", "GetAddonInfos") : null,
|
||||
requestExternalIPAddress ? requestEndpoint(apiBaseUrl, "WANIPConnection", "GetExternalIPAddress") : null,
|
||||
])
|
||||
.then(([statusInfo, linkProperties, addonInfos, externalIPAddress]) => {
|
||||
res.status(200).json({
|
||||
connectionStatus: statusInfo?.NewConnectionStatus || "Unconfigured",
|
||||
uptime: statusInfo?.NewUptime || 0,
|
||||
maxDown: linkProperties?.NewLayer1DownstreamMaxBitRate || 0,
|
||||
maxUp: linkProperties?.NewLayer1UpstreamMaxBitRate || 0,
|
||||
down: addonInfos?.NewByteReceiveRate || 0,
|
||||
up: addonInfos?.NewByteSendRate || 0,
|
||||
received: addonInfos?.NewX_AVM_DE_TotalBytesReceived64 || 0,
|
||||
sent: addonInfos?.NewX_AVM_DE_TotalBytesSent64 || 0,
|
||||
externalIPAddress: externalIPAddress?.NewExternalIPAddress || null,
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
res.status(500).json({ error: error.message });
|
||||
});
|
||||
}
|
7
src/widgets/fritzbox/widget.js
Normal file
7
src/widgets/fritzbox/widget.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
import fritzboxProxyHandler from "./proxy";
|
||||
|
||||
const widget = {
|
||||
proxyHandler: fritzboxProxyHandler,
|
||||
};
|
||||
|
||||
export default widget;
|
|
@ -10,17 +10,18 @@ import useWidgetAPI from "utils/proxy/use-widget-api";
|
|||
|
||||
const Chart = dynamic(() => import("../components/chart"), { ssr: false });
|
||||
|
||||
const pointsLimit = 15;
|
||||
const defaultPointsLimit = 15;
|
||||
const defaultInterval = 1000;
|
||||
|
||||
export default function Component({ service }) {
|
||||
const { t } = useTranslation();
|
||||
const { widget } = service;
|
||||
const { chart } = widget;
|
||||
const { chart, refreshInterval = defaultInterval, pointsLimit = defaultPointsLimit } = widget;
|
||||
|
||||
const [dataPoints, setDataPoints] = useState(new Array(pointsLimit).fill({ value: 0 }, 0, pointsLimit));
|
||||
|
||||
const { data, error } = useWidgetAPI(service.widget, "cpu", {
|
||||
refreshInterval: 1000,
|
||||
refreshInterval: Math.max(defaultInterval, refreshInterval),
|
||||
});
|
||||
|
||||
const { data: systemData, error: systemError } = useWidgetAPI(service.widget, "system");
|
||||
|
@ -35,7 +36,7 @@ export default function Component({ service }) {
|
|||
return newDataPoints;
|
||||
});
|
||||
}
|
||||
}, [data]);
|
||||
}, [data, pointsLimit]);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
|
|
|
@ -10,12 +10,13 @@ import useWidgetAPI from "utils/proxy/use-widget-api";
|
|||
|
||||
const ChartDual = dynamic(() => import("../components/chart_dual"), { ssr: false });
|
||||
|
||||
const pointsLimit = 15;
|
||||
const defaultPointsLimit = 15;
|
||||
const defaultInterval = 1000;
|
||||
|
||||
export default function Component({ service }) {
|
||||
const { t } = useTranslation();
|
||||
const { widget } = service;
|
||||
const { chart } = widget;
|
||||
const { chart, refreshInterval = defaultInterval, pointsLimit = defaultPointsLimit } = widget;
|
||||
const [, diskName] = widget.metric.split(":");
|
||||
|
||||
const [dataPoints, setDataPoints] = useState(
|
||||
|
@ -24,7 +25,7 @@ export default function Component({ service }) {
|
|||
const [ratePoints, setRatePoints] = useState(new Array(pointsLimit).fill({ a: 0, b: 0 }, 0, pointsLimit));
|
||||
|
||||
const { data, error } = useWidgetAPI(service.widget, "diskio", {
|
||||
refreshInterval: 1000,
|
||||
refreshInterval: Math.max(defaultInterval, refreshInterval),
|
||||
});
|
||||
|
||||
const calculateRates = (d) =>
|
||||
|
@ -45,7 +46,7 @@ export default function Component({ service }) {
|
|||
return newDataPoints;
|
||||
});
|
||||
}
|
||||
}, [data, diskName]);
|
||||
}, [data, diskName, pointsLimit]);
|
||||
|
||||
useEffect(() => {
|
||||
setRatePoints(calculateRates(dataPoints));
|
||||
|
|
|
@ -6,14 +6,16 @@ import Block from "../components/block";
|
|||
|
||||
import useWidgetAPI from "utils/proxy/use-widget-api";
|
||||
|
||||
const defaultInterval = 1000;
|
||||
|
||||
export default function Component({ service }) {
|
||||
const { t } = useTranslation();
|
||||
const { widget } = service;
|
||||
const { chart } = widget;
|
||||
const { chart, refreshInterval = defaultInterval } = widget;
|
||||
const [, fsName] = widget.metric.split("fs:");
|
||||
|
||||
const { data, error } = useWidgetAPI(widget, "fs", {
|
||||
refreshInterval: 1000,
|
||||
refreshInterval: Math.max(defaultInterval, refreshInterval),
|
||||
});
|
||||
|
||||
if (error) {
|
||||
|
|
|
@ -10,18 +10,19 @@ import useWidgetAPI from "utils/proxy/use-widget-api";
|
|||
|
||||
const ChartDual = dynamic(() => import("../components/chart_dual"), { ssr: false });
|
||||
|
||||
const pointsLimit = 15;
|
||||
const defaultPointsLimit = 15;
|
||||
const defaultInterval = 1000;
|
||||
|
||||
export default function Component({ service }) {
|
||||
const { t } = useTranslation();
|
||||
const { widget } = service;
|
||||
const { chart } = widget;
|
||||
const { chart, refreshInterval = defaultInterval, pointsLimit = defaultPointsLimit } = widget;
|
||||
const [, gpuName] = widget.metric.split(":");
|
||||
|
||||
const [dataPoints, setDataPoints] = useState(new Array(pointsLimit).fill({ a: 0, b: 0 }, 0, pointsLimit));
|
||||
|
||||
const { data, error } = useWidgetAPI(widget, "gpu", {
|
||||
refreshInterval: 1000,
|
||||
refreshInterval: Math.max(defaultInterval, refreshInterval),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -39,7 +40,7 @@ export default function Component({ service }) {
|
|||
});
|
||||
}
|
||||
}
|
||||
}, [data, gpuName]);
|
||||
}, [data, gpuName, pointsLimit]);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
|
|
|
@ -69,16 +69,19 @@ function Mem({ quicklookData, className = "" }) {
|
|||
);
|
||||
}
|
||||
|
||||
const defaultInterval = 1000;
|
||||
const defaultSystemInterval = 30000; // This data (OS, hostname, distribution) is usually super stable.
|
||||
|
||||
export default function Component({ service }) {
|
||||
const { widget } = service;
|
||||
const { chart } = widget;
|
||||
const { chart, refreshInterval = defaultInterval } = widget;
|
||||
|
||||
const { data: quicklookData, errorL: quicklookError } = useWidgetAPI(service.widget, "quicklook", {
|
||||
refreshInterval: 1000,
|
||||
refreshInterval,
|
||||
});
|
||||
|
||||
const { data: systemData, errorL: systemError } = useWidgetAPI(service.widget, "system", {
|
||||
refreshInterval: 30000,
|
||||
refreshInterval: defaultSystemInterval,
|
||||
});
|
||||
|
||||
if (quicklookError) {
|
||||
|
|
|
@ -10,17 +10,19 @@ import useWidgetAPI from "utils/proxy/use-widget-api";
|
|||
|
||||
const ChartDual = dynamic(() => import("../components/chart_dual"), { ssr: false });
|
||||
|
||||
const pointsLimit = 15;
|
||||
const defaultPointsLimit = 15;
|
||||
const defaultInterval = (isChart) => (isChart ? 1000 : 5000);
|
||||
|
||||
export default function Component({ service }) {
|
||||
const { t } = useTranslation();
|
||||
const { widget } = service;
|
||||
const { chart } = widget;
|
||||
const { refreshInterval = defaultInterval(chart), pointsLimit = defaultPointsLimit } = widget;
|
||||
|
||||
const [dataPoints, setDataPoints] = useState(new Array(pointsLimit).fill({ value: 0 }, 0, pointsLimit));
|
||||
|
||||
const { data, error } = useWidgetAPI(service.widget, "mem", {
|
||||
refreshInterval: chart ? 1000 : 5000,
|
||||
refreshInterval: Math.max(defaultInterval(chart), refreshInterval),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -33,7 +35,7 @@ export default function Component({ service }) {
|
|||
return newDataPoints;
|
||||
});
|
||||
}
|
||||
}, [data]);
|
||||
}, [data, pointsLimit]);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
|
|
|
@ -10,18 +10,21 @@ import useWidgetAPI from "utils/proxy/use-widget-api";
|
|||
|
||||
const ChartDual = dynamic(() => import("../components/chart_dual"), { ssr: false });
|
||||
|
||||
const pointsLimit = 15;
|
||||
const defaultPointsLimit = 15;
|
||||
const defaultInterval = (isChart) => (isChart ? 1000 : 5000);
|
||||
|
||||
export default function Component({ service }) {
|
||||
const { t } = useTranslation();
|
||||
const { widget } = service;
|
||||
const { chart, metric } = widget;
|
||||
const { refreshInterval = defaultInterval(chart), pointsLimit = defaultPointsLimit } = widget;
|
||||
|
||||
const [, interfaceName] = metric.split(":");
|
||||
|
||||
const [dataPoints, setDataPoints] = useState(new Array(pointsLimit).fill({ value: 0 }, 0, pointsLimit));
|
||||
|
||||
const { data, error } = useWidgetAPI(widget, "network", {
|
||||
refreshInterval: chart ? 1000 : 5000,
|
||||
refreshInterval: Math.max(defaultInterval(chart), refreshInterval),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -44,7 +47,7 @@ export default function Component({ service }) {
|
|||
});
|
||||
}
|
||||
}
|
||||
}, [data, interfaceName]);
|
||||
}, [data, interfaceName, pointsLimit]);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
|
|
|
@ -17,13 +17,15 @@ const statusMap = {
|
|||
X: <ResolvedIcon icon="mdi-rhombus-outline" width={32} height={32} />, // dead
|
||||
};
|
||||
|
||||
const defaultInterval = 1000;
|
||||
|
||||
export default function Component({ service }) {
|
||||
const { t } = useTranslation();
|
||||
const { widget } = service;
|
||||
const { chart } = widget;
|
||||
const { chart, refreshInterval = defaultInterval } = widget;
|
||||
|
||||
const { data, error } = useWidgetAPI(service.widget, "processlist", {
|
||||
refreshInterval: 1000,
|
||||
refreshInterval: Math.max(defaultInterval, refreshInterval),
|
||||
});
|
||||
|
||||
if (error) {
|
||||
|
|
|
@ -10,18 +10,19 @@ import useWidgetAPI from "utils/proxy/use-widget-api";
|
|||
|
||||
const Chart = dynamic(() => import("../components/chart"), { ssr: false });
|
||||
|
||||
const pointsLimit = 15;
|
||||
const defaultPointsLimit = 15;
|
||||
const defaultInterval = 1000;
|
||||
|
||||
export default function Component({ service }) {
|
||||
const { t } = useTranslation();
|
||||
const { widget } = service;
|
||||
const { chart } = widget;
|
||||
const { chart, refreshInterval = defaultInterval, pointsLimit = defaultPointsLimit } = widget;
|
||||
const [, sensorName] = widget.metric.split(":");
|
||||
|
||||
const [dataPoints, setDataPoints] = useState(new Array(pointsLimit).fill({ value: 0 }, 0, pointsLimit));
|
||||
|
||||
const { data, error } = useWidgetAPI(service.widget, "sensors", {
|
||||
refreshInterval: 1000,
|
||||
refreshInterval: Math.max(defaultInterval, refreshInterval),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -35,7 +36,7 @@ export default function Component({ service }) {
|
|||
return newDataPoints;
|
||||
});
|
||||
}
|
||||
}, [data, sensorName]);
|
||||
}, [data, sensorName, pointsLimit]);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
|
|
|
@ -40,17 +40,17 @@ export default function Component({ service }) {
|
|||
if (!data) {
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label={t("healthchecks.status")} />
|
||||
<Block label={t("healthchecks.last_ping")} />
|
||||
<Block label="healthchecks.status" />
|
||||
<Block label="healthchecks.last_ping" />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label={t("healthchecks.status")} value={t(`healthchecks.${data.status}`)} />
|
||||
<Block label="healthchecks.status" value={t(`healthchecks.${data.status}`)} />
|
||||
<Block
|
||||
label={t("healthchecks.last_ping")}
|
||||
label="healthchecks.last_ping"
|
||||
value={data.last_ping ? formatDate(data.last_ping) : t("healthchecks.never")}
|
||||
/>
|
||||
</Container>
|
||||
|
|
|
@ -21,7 +21,7 @@ export default function Component({ service }) {
|
|||
return <Container service={service} error={statsError ?? statusError} />;
|
||||
}
|
||||
|
||||
if (statusData && statusData.status !== "running") {
|
||||
if (statusData && !(statusData.status.includes("running") || statusData.status.includes("partial"))) {
|
||||
return (
|
||||
<Container>
|
||||
<Block label={t("widget.status")} value={t("docker.offline")} />
|
||||
|
|
|
@ -29,7 +29,7 @@ export default function Component({ service }) {
|
|||
<Container service={service}>
|
||||
<Block label="strelaysrv.numActiveSessions" value={t("common.number", { value: statsData.numActiveSessions })} />
|
||||
<Block label="strelaysrv.numConnections" value={t("common.number", { value: statsData.numConnections })} />
|
||||
<Block label={t("strelaysrv.dataRelayed")} value={t("common.bytes", { value: statsData.bytesProxied })} />
|
||||
<Block label="strelaysrv.dataRelayed" value={t("common.bytes", { value: statsData.bytesProxied })} />
|
||||
<Block
|
||||
label="strelaysrv.transferRate"
|
||||
value={t("common.bitrate", { value: statsData.kbps10s1m5m15m30m60m[5] })}
|
||||
|
|
|
@ -6,6 +6,7 @@ import autobrr from "./autobrr/widget";
|
|||
import azuredevops from "./azuredevops/widget";
|
||||
import bazarr from "./bazarr/widget";
|
||||
import caddy from "./caddy/widget";
|
||||
import calendar from "./calendar/widget";
|
||||
import calibreweb from "./calibreweb/widget";
|
||||
import changedetectionio from "./changedetectionio/widget";
|
||||
import channelsdvrserver from "./channelsdvrserver/widget";
|
||||
|
@ -20,6 +21,7 @@ import evcc from "./evcc/widget";
|
|||
import fileflows from "./fileflows/widget";
|
||||
import flood from "./flood/widget";
|
||||
import freshrss from "./freshrss/widget";
|
||||
import fritzbox from "./fritzbox/widget";
|
||||
import gamedig from "./gamedig/widget";
|
||||
import ghostfolio from "./ghostfolio/widget";
|
||||
import glances from "./glances/widget";
|
||||
|
@ -121,6 +123,7 @@ const widgets = {
|
|||
fileflows,
|
||||
flood,
|
||||
freshrss,
|
||||
fritzbox,
|
||||
gamedig,
|
||||
ghostfolio,
|
||||
glances,
|
||||
|
@ -131,6 +134,7 @@ const widgets = {
|
|||
homeassistant,
|
||||
homebridge,
|
||||
healthchecks,
|
||||
ical: calendar,
|
||||
immich,
|
||||
jackett,
|
||||
jdownloader,
|
||||
|
|
Loading…
Add table
Reference in a new issue