Compare commits
No commits in common. "main" and "v0.8.2" have entirely different histories.
27 changed files with 37 additions and 337 deletions
6
.github/PULL_REQUEST_TEMPLATE.md
vendored
6
.github/PULL_REQUEST_TEMPLATE.md
vendored
|
@ -25,7 +25,7 @@ What type of change does your PR introduce to Homepage?
|
|||
|
||||
## Checklist:
|
||||
|
||||
- [ ] If 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 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 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
31
.github/workflows/crowdin.yml
vendored
|
@ -1,31 +0,0 @@
|
|||
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 }}
|
2
.github/workflows/docker-publish.yml
vendored
2
.github/workflows/docker-publish.yml
vendored
|
@ -41,7 +41,7 @@ jobs:
|
|||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Install python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.x
|
||||
-
|
||||
|
|
6
.github/workflows/docs-publish.yml
vendored
6
.github/workflows/docs-publish.yml
vendored
|
@ -27,7 +27,7 @@ jobs:
|
|||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Install python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.x
|
||||
-
|
||||
|
@ -42,7 +42,7 @@ jobs:
|
|||
- pre-commit
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
- uses: actions/setup-python@v4
|
||||
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@v5
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.x
|
||||
- run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
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%
|
||||
|
|
|
@ -229,28 +229,6 @@ 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.
|
||||
|
|
|
@ -39,16 +39,11 @@ 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'. The purpose of this requirement is to avoid the addition (and maintenance) of service widgets that might only benefit a small number of users.
|
||||
- Please only submit widgets that have been requested and have at least 5 'up-votes'
|
||||
- Widgets should be only one row of blocks
|
||||
- Widgets should be no more than 4 blocks wide
|
||||
- Minimize the number of API calls
|
||||
|
|
|
@ -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.yaml` 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.yml` 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.yaml` 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.yml` file. Here's an example:
|
||||
|
||||
```yaml
|
||||
- openmeteo:
|
||||
|
|
|
@ -3,7 +3,7 @@ title: Calibre-web
|
|||
description: Calibre-web Widget Configuration
|
||||
---
|
||||
|
||||
**Note: widget requires calibre-web ≥ v0.6.21.**
|
||||
**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).**
|
||||
|
||||
Allowed fields: `["books", "authors", "categories", "series"]`.
|
||||
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
---
|
||||
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
|
||||
```
|
|
@ -27,7 +27,7 @@ widget:
|
|||
src: http://example.com
|
||||
classes: h-60 sm:h-60 md:h-60 lg:h-60 xl:h-60 2xl:h-72 # optional, use tailwind height classes, see https://tailwindcss.com/docs/height
|
||||
referrerPolicy: same-origin # optional, no default
|
||||
allowPolicy: autoplay; fullscreen; gamepad # optional, no default
|
||||
allowPolicy: autoplay fullscreen gamepad # optional, no default
|
||||
allowFullscreen: false # optional, default: true
|
||||
loadingStrategy: eager # optional, default: eager
|
||||
allowScrolling: no # optional, default: yes
|
||||
|
|
|
@ -53,7 +53,6 @@ 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
|
||||
|
|
|
@ -122,24 +122,6 @@
|
|||
"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",
|
||||
|
|
|
@ -48,7 +48,7 @@
|
|||
},
|
||||
"unifi": {
|
||||
"users": "Users",
|
||||
"uptime": "Uptime",
|
||||
"uptime": "System Uptime",
|
||||
"days": "Days",
|
||||
"wan": "WAN",
|
||||
"lan": "LAN",
|
||||
|
@ -122,24 +122,6 @@
|
|||
"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",
|
||||
|
|
|
@ -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, useEqualHeights }) {
|
||||
export default function ServicesGroup({ group, services, layout, fiveColumns, disableCollapse }) {
|
||||
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} useEqualHeights={useEqualHeights} />
|
||||
<List group={group} services={services.services} layout={layout} />
|
||||
</Disclosure.Panel>
|
||||
</Transition>
|
||||
</>
|
||||
|
|
|
@ -12,7 +12,7 @@ import Kubernetes from "widgets/kubernetes/component";
|
|||
import { SettingsContext } from "utils/contexts/settings";
|
||||
import ResolvedIcon from "components/resolvedicon";
|
||||
|
||||
export default function Item({ service, group, useEqualHeights }) {
|
||||
export default function Item({ service, group }) {
|
||||
const hasLink = service.href && service.href !== "#";
|
||||
const { settings } = useContext(SettingsContext);
|
||||
const showStats = service.showStats === false ? false : settings.showStats;
|
||||
|
@ -37,8 +37,7 @@ export default function Item({ service, group, useEqualHeights }) {
|
|||
className={classNames(
|
||||
settings.cardBlur !== undefined && `backdrop-blur${settings.cardBlur.length ? "-" : ""}${settings.cardBlur}`,
|
||||
hasLink && "cursor-pointer",
|
||||
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",
|
||||
"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",
|
||||
)}
|
||||
>
|
||||
<div className="flex select-none z-0 service-title">
|
||||
|
|
|
@ -4,7 +4,7 @@ import { columnMap } from "../../utils/layout/columns";
|
|||
|
||||
import Item from "components/services/item";
|
||||
|
||||
export default function List({ group, services, layout, useEqualHeights }) {
|
||||
export default function List({ group, services, layout }) {
|
||||
return (
|
||||
<ul
|
||||
className={classNames(
|
||||
|
@ -13,12 +13,7 @@ export default function List({ group, services, layout, useEqualHeights }) {
|
|||
)}
|
||||
>
|
||||
{services.map((service) => (
|
||||
<Item
|
||||
key={service.container ?? service.app ?? service.name}
|
||||
service={service}
|
||||
group={group}
|
||||
useEqualHeights={layout?.useEqualHeights ?? useEqualHeights}
|
||||
/>
|
||||
<Item key={service.container ?? service.app ?? service.name} service={service} group={group} />
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
|
|
|
@ -307,7 +307,6 @@ function Home({ initialSettings }) {
|
|||
layout={settings.layout?.[group.name]}
|
||||
fiveColumns={settings.fiveColumns}
|
||||
disableCollapse={settings.disableCollapse}
|
||||
useEqualHeights={settings.useEqualHeights}
|
||||
/>
|
||||
) : (
|
||||
<BookmarksGroup
|
||||
|
@ -356,7 +355,6 @@ function Home({ initialSettings }) {
|
|||
settings.layout,
|
||||
settings.fiveColumns,
|
||||
settings.disableCollapse,
|
||||
settings.useEqualHeights,
|
||||
settings.cardBlur,
|
||||
initialSettings.layout,
|
||||
]);
|
||||
|
@ -364,12 +362,12 @@ function Home({ initialSettings }) {
|
|||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{settings.title || "Homepage"}</title>
|
||||
{settings.base && <base href={settings.base} />}
|
||||
{settings.favicon ? (
|
||||
<title>{initialSettings.title || "Homepage"}</title>
|
||||
{initialSettings.base && <base href={initialSettings.base} />}
|
||||
{initialSettings.favicon ? (
|
||||
<>
|
||||
<link rel="icon" href={settings.favicon} />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href={settings.favicon} />
|
||||
<link rel="icon" href={initialSettings.favicon} />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href={initialSettings.favicon} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
|
@ -380,8 +378,11 @@ function Home({ initialSettings }) {
|
|||
<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"]} />
|
||||
<meta name="theme-color" content={themes[settings.color || "slate"][settings.theme || "dark"]} />
|
||||
<meta
|
||||
name="msapplication-TileColor"
|
||||
content={themes[initialSettings.color || "slate"][initialSettings.theme || "dark"]}
|
||||
/>
|
||||
<meta name="theme-color" content={themes[initialSettings.color || "slate"][initialSettings.theme || "dark"]} />
|
||||
</Head>
|
||||
|
||||
<link rel="preload" href="/api/config/custom.css" as="fetch" crossOrigin="anonymous" />
|
||||
|
@ -493,7 +494,7 @@ export default function Wrapper({ initialSettings, fallback }) {
|
|||
rgb(var(--bg-color) / ${opacityValue}),
|
||||
rgb(var(--bg-color) / ${opacityValue})
|
||||
),
|
||||
url('${backgroundImage}')`;
|
||||
url(${backgroundImage})`;
|
||||
wrappedStyle.backgroundPosition = "center";
|
||||
wrappedStyle.backgroundSize = "cover";
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
# For configuration options and examples, please see:
|
||||
# https://gethomepage.dev/latest/configs/service-widgets
|
||||
# https://gethomepage.dev/latest/configs/widgets
|
||||
|
||||
- resources:
|
||||
cpu: true
|
||||
|
|
|
@ -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 && !data[key].viewed) {
|
||||
if (data[key].last_changed > 0 && data[key].last_checked === data[key].last_changed) {
|
||||
diffsDetected += 1;
|
||||
}
|
||||
});
|
||||
|
|
|
@ -27,7 +27,6 @@ const components = {
|
|||
fileflows: dynamic(() => import("./fileflows/component")),
|
||||
flood: dynamic(() => import("./flood/component")),
|
||||
freshrss: dynamic(() => import("./freshrss/component")),
|
||||
fritzbox: dynamic(() => import("./fritzbox/component")),
|
||||
gamedig: dynamic(() => import("./gamedig/component")),
|
||||
ghostfolio: dynamic(() => import("./ghostfolio/component")),
|
||||
glances: dynamic(() => import("./glances/component")),
|
||||
|
|
|
@ -1,69 +0,0 @@
|
|||
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>
|
||||
);
|
||||
}
|
|
@ -1,96 +0,0 @@
|
|||
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 });
|
||||
});
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
import fritzboxProxyHandler from "./proxy";
|
||||
|
||||
const widget = {
|
||||
proxyHandler: fritzboxProxyHandler,
|
||||
};
|
||||
|
||||
export default widget;
|
|
@ -40,17 +40,17 @@ export default function Component({ service }) {
|
|||
if (!data) {
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="healthchecks.status" />
|
||||
<Block label="healthchecks.last_ping" />
|
||||
<Block label={t("healthchecks.status")} />
|
||||
<Block label={t("healthchecks.last_ping")} />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="healthchecks.status" value={t(`healthchecks.${data.status}`)} />
|
||||
<Block label={t("healthchecks.status")} value={t(`healthchecks.${data.status}`)} />
|
||||
<Block
|
||||
label="healthchecks.last_ping"
|
||||
label={t("healthchecks.last_ping")}
|
||||
value={data.last_ping ? formatDate(data.last_ping) : t("healthchecks.never")}
|
||||
/>
|
||||
</Container>
|
||||
|
|
|
@ -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="strelaysrv.dataRelayed" value={t("common.bytes", { value: statsData.bytesProxied })} />
|
||||
<Block label={t("strelaysrv.dataRelayed")} value={t("common.bytes", { value: statsData.bytesProxied })} />
|
||||
<Block
|
||||
label="strelaysrv.transferRate"
|
||||
value={t("common.bitrate", { value: statsData.kbps10s1m5m15m30m60m[5] })}
|
||||
|
|
|
@ -21,7 +21,6 @@ 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";
|
||||
|
@ -123,7 +122,6 @@ const widgets = {
|
|||
fileflows,
|
||||
flood,
|
||||
freshrss,
|
||||
fritzbox,
|
||||
gamedig,
|
||||
ghostfolio,
|
||||
glances,
|
||||
|
|
Loading…
Add table
Reference in a new issue