Compare commits

..

43 commits

Author SHA1 Message Date
shamoon
49281b30c2
Fix block label translation in strelaysrv widget 2023-12-06 20:54:32 -08:00
Lawton Manning
914cd71c76
Fix: remove translation on block labels in healthchecks widget (#2440) 2023-12-06 20:53:13 -08:00
José Marques
5e01eb4a8d
Feature: setting for equal height cards (#2432) 2023-12-06 14:52:02 -08:00
dependabot[bot]
7edcf6b047
Bump actions/setup-python from 4 to 5 (#2435)
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 4 to 5.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-06 09:15:14 -08:00
Thorben
0f3fc77ddf
Enhancement: improve fritzbox proxy perfomance (#2429)
---------

Co-authored-by: Thorben Grove <thorben.grove@tui.de>
Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
2023-12-05 00:44:13 -08:00
Ashley Buckingham
77ed445da1
Documentation: correct doc link in skeleton yaml (#2428) 2023-12-04 16:03:58 -08:00
shamoon
708e6fa789 Update crowdin.yml 2023-12-03 07:34:52 -08:00
shamoon
9afa40a6b7
Chore: migrate crowdin to GHA (#2421) 2023-12-03 07:33:43 -08:00
Kamil Markowicz
04ae39e144
Documentation: Update minimum calibre-web version reference (#2412)
---------

Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
2023-12-01 19:38:31 -08:00
Sean Kelly
a3e00e96c9
Change: changedetection.io widget dont count diff if viewed (#2401) 2023-11-29 13:06:06 -08:00
SOOROS
251881a051
Documentation: add semicolons to iframe example allow option (#2400) 2023-11-29 11:56:18 -08:00
shamoon
9112030275 Fix: quote background image URL
See #2396
2023-11-27 20:06:39 -08:00
shamoon
2f89b12f0d Revert "Fix: revert to using initialSettings in head"
This reverts commit e28faf6b98.
2023-11-27 19:04:50 -08:00
shamoon
b2a914eb2a Update guidelines 2023-11-27 08:19:13 -08:00
Thorben
4c45c6453f
Feature: Fritz!Box Widget (#2387)
* Feature: Fritz!Box Widget

* Use i18n

* code style & formatting

---------

Co-authored-by: Thorben Grove <thorben.grove@tui.de>
Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
2023-11-26 20:48:54 -08:00
shamoon
b3414fc35f Documentation: Fix services yaml file name 2023-11-26 13:24:03 -08:00
COxDE
259f0f8ce9
Documentation: fix typo in widgets file name (#2393) 2023-11-26 13:21:59 -08:00
shamoon
97218e9cea Update PULL_REQUEST_TEMPLATE.md 2023-11-26 10:03:35 -08:00
Nick
3e0054069d Change Unifi Widget "System Uptime" to "Uptime" (#2389)
---------

Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
2023-11-26 07:20:13 -08:00
shamoon
a3fcc3ef51 Hack reload glime styles 2023-11-25 10:48:40 -08:00
shamoon
dd5144f03a
Chore: enable swc, again (#2385) 2023-11-25 10:25:24 -08:00
shamoon
90c12abf87 Revert "Chore: enable swc (#2311)"
This reverts commit 0931f5c5a6.
2023-11-25 09:29:18 -08:00
shamoon
507e72407f Add AI bot info to docs 2023-11-25 08:54:38 -08:00
Ben Phelps
5bc6730e8b
New Crowdin updates (#2295) 2023-11-25 08:23:55 -08:00
shamoon
110ebe920e
Documentation: Add ai bot (#2383) 2023-11-25 08:22:16 -08:00
Denis Papec
95d66707f5
Feature: Implement iCal integration for calendar, improve styling (#2376)
* Feature: Implement iCal integration, improve calendar/agenda styling

* Delete calendar.jsx

* Calendar proxy handler

* code style

* Add some basic error handling

---------

Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
2023-11-25 08:17:25 -08:00
Denis Papec
518ed7fc4e
Feature: Support previous days option in Calendar Agenda (#2375)
Signed-off-by: Denis Papec <denis.papec@gmail.com>
2023-11-24 20:32:38 -08:00
Denis Papec
acafbb5100
Enhancement: Improvements to calendar Radarr release logic (#2374)
Signed-off-by: Denis Papec <denis.papec@gmail.com>
2023-11-24 20:32:04 -08:00
shamoon
fb9ebf18ba
Fix: show mem / cpu stats for k8s partial health status (#2378) 2023-11-24 20:29:23 -08:00
shamoon
6b35443100
Fix: dont ignore empty string for kubernetes podSelector (#2372) 2023-11-24 16:15:42 -08:00
nioKi
c2729e302d
Enhancement: Add configurable refresh interval and max points for glances services (#2363)
---------

Co-authored-by: Quentin de Grandmaison <quentin.degrandmaison@7speaking.com>
Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
2023-11-22 12:35:54 -08:00
Joe Stump
e98b5e2233
Documentation: Add details to Gluetun widget docs (#2357)
---------

Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
2023-11-21 11:41:56 -08:00
shamoon
0931f5c5a6
Chore: enable swc (#2311) 2023-11-20 19:34:56 -08:00
Timo
6d21ea9ba3
Enhancement: Add service discovery labels support for multiple homepage instances (#2340) 2023-11-20 19:27:19 -08:00
Aesop7
5bf6f30e2b
Documentation: add Longhorn example (#2353) 2023-11-20 07:35:21 -08:00
shamoon
66fbe9f670
Fix: Disable override white bkgd with card blur (#2336) 2023-11-17 07:26:22 -08:00
shamoon
6316de6fa6 Fix: Lint docker stats throughput PR 2023-11-17 00:03:56 -08:00
shamoon
7f50f6cfaa
Fix: sum throughput data for docker stats (#2334) 2023-11-16 23:55:04 -08:00
shamoon
c9991bc2a2
Fix: dont set pinned icon if custom favicon (#2326) 2023-11-14 22:10:37 -08:00
shamoon
e28faf6b98 Fix: revert to using initialSettings in head 2023-11-14 22:09:18 -08:00
Faqar
17b9b7e523
Enhancement: open the searchbox on paste (#2320)
* Open the searchbox when detecting Ctrl-V.

* support macOS, codestyle

---------

Co-authored-by: Joschka <kontakt@greiner-it.de>
Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
2023-11-12 19:40:09 -08:00
shamoon
d5608c46c3 Dont push docker for feature builds 2023-11-11 21:46:40 -08:00
shamoon
428680b5df Run ci on feature/ branches 2023-11-11 21:42:13 -08:00
74 changed files with 2476 additions and 2765 deletions

View file

@ -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
View 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 }}

View file

@ -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: |

View file

@ -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

View file

@ -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%

View file

@ -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:

View file

@ -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`:

View file

@ -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

View file

@ -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
View 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);
};

View file

@ -18,3 +18,7 @@
border-color: var(--md-default-bg-color--lighter);
}
}
#glimeRoot * {
font-family: var(--md-text-font) !important;
}

View file

@ -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:

View file

@ -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
```

View file

@ -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`).

View file

@ -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"]`.

View 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
```

View file

@ -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
```

View file

@ -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

View file

@ -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:

View file

@ -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

File diff suppressed because it is too large Load diff

View file

@ -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

File diff suppressed because it is too large Load diff

View file

@ -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!"
}
}

View file

@ -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": "لا توجد أحداث اليوم!"
}
}

View file

@ -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",

View file

@ -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"
}
}

View file

@ -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": {

View file

@ -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

View file

@ -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!"
}
}

View file

@ -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": "今日無事件"
}
}

View file

@ -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";

View file

@ -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>
</>

View file

@ -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">

View file

@ -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>
);

View file

@ -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>

View file

@ -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 />

View file

@ -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";
}

View file

@ -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

View file

@ -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);
}

View file

@ -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;
}
}

View file

@ -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>;
}

View file

@ -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>

View file

@ -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"

View 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>
);
}

View 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}` }} />;
}

View file

@ -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",

View file

@ -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 }));

View file

@ -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",

View file

@ -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",

View file

@ -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>

View 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" });
}

View file

@ -0,0 +1,8 @@
import calendarProxyHandler from "./proxy";
const widget = {
api: "{url}",
proxyHandler: calendarProxyHandler,
};
export default widget;

View file

@ -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;
}
});

View file

@ -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")),

View file

@ -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>

View file

@ -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 };
}

View 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>
);
}

View 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 });
});
}

View file

@ -0,0 +1,7 @@
import fritzboxProxyHandler from "./proxy";
const widget = {
proxyHandler: fritzboxProxyHandler,
};
export default widget;

View file

@ -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 (

View file

@ -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));

View file

@ -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) {

View file

@ -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 (

View file

@ -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) {

View file

@ -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 (

View file

@ -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 (

View file

@ -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) {

View file

@ -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 (

View file

@ -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>

View file

@ -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")} />

View file

@ -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] })}

View file

@ -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,