Merge branch 'gethomepage:dev' into integration

This commit is contained in:
djeinstine
2024-12-22 08:50:54 +01:00
committed by GitHub
70 changed files with 337 additions and 242 deletions

View File

@@ -129,7 +129,7 @@ export default function QuickLaunch({ servicesAndBookmarks, searchString, setSea
useEffect(() => {
const abortController = new AbortController();
if (searchString.length === 0) setResults([]);
if (searchString.trim().length === 0) setResults([]);
else {
let newResults = servicesAndBookmarks.filter((r) => {
const nameMatch = r.name.toLowerCase().includes(searchString);

View File

@@ -94,6 +94,7 @@ export default function Search({ options }) {
if (
options.showSearchSuggestions &&
(selectedProvider.suggestionUrl || options.suggestionUrl) && // custom providers pass url via options
query.trim().length > 0 &&
query.trim() !== searchSuggestions[0]
) {
fetch(`/api/search/searchSuggestion?query=${encodeURIComponent(query)}&providerName=${selectedProvider.name}`, {

View File

@@ -51,7 +51,9 @@ export default function Widget({ options }) {
key={stock.ticker}
className="rounded h-full text-xs px-1 w-[4.75rem] flex flex-col items-center justify-center"
>
<span className="text-theme-800 dark:text-theme-200 text-xs">{stock.ticker}</span>
<span className="text-theme-800 dark:text-theme-200 text-xs">
{stock.ticker.split(":").pop()}
</span>
{!viewingPercentChange ? (
<span
className={

View File

@@ -1,9 +1,14 @@
import cachedFetch from "utils/proxy/cached-fetch";
import { getSettings } from "utils/config/config";
import createLogger from "utils/logger";
const logger = createLogger("stocks");
export default async function handler(req, res) {
const { watchlist, provider, cache } = req.query;
logger.debug("Stocks API request: %o", { watchlist, provider, cache });
if (!watchlist) {
return res.status(400).json({ error: "Missing watchlist" });
}
@@ -56,6 +61,7 @@ export default async function handler(req, res) {
// Finnhub free accounts allow up to 60 calls/minute
// https://finnhub.io/pricing
const { c, dp } = await cachedFetch(apiUrl, cache || 1);
logger.debug("Finnhub API response for %s: %o", ticker, { c, dp });
// API sometimes returns 200, but values returned are `null`
if (c === null || dp === null) {

View File

@@ -359,7 +359,7 @@ function Home({ initialSettings }) {
return (
<>
<Head>
<title>{settings.title || "Homepage"}</title>
<title>{initialSettings.title || "Homepage"}</title>
{settings.base && <base href={settings.base} />}
{settings.favicon ? (
<>

View File

@@ -158,7 +158,7 @@ export async function servicesResponse() {
const discoveredKubernetesGroup = findGroupByName(discoveredKubernetesServices, groupName) || {
services: [],
};
const configuredGroup = findGroupByName(configuredServices, groupName) || { services: [] };
const configuredGroup = findGroupByName(configuredServices, groupName) || { services: [], groups: [] };
const mergedGroup = {
name: groupName,
@@ -171,7 +171,7 @@ export async function servicesResponse() {
if (definedLayouts) {
const layoutIndex = definedLayouts.findIndex((layout) => layout === mergedGroup.name);
if (layoutIndex > -1) sortedGroups[layoutIndex] = mergedGroup;
else if (configuredGroup.name) {
else if (configuredGroup.parent) {
// this is a nested group, so find the parent group and merge the services
mergeSubgroups(configuredServices, mergedGroup);
} else unsortedGroups.push(mergedGroup);

View File

@@ -24,6 +24,10 @@ function parseServicesToGroups(services) {
const serviceGroupServices = [];
serviceGroup[name].forEach((entries) => {
const entryName = Object.keys(entries)[0];
if (!entries[entryName]) {
logger.warn(`Error parsing service "${entryName}" from config. Ensure required fields are present.`);
return;
}
if (Array.isArray(entries[entryName])) {
groups = groups.concat(parseServicesToGroups([{ [entryName]: entries[entryName] }]));
} else {
@@ -105,7 +109,11 @@ export async function servicesFromDocker() {
type: "service",
};
}
shvl.set(constructedService, value, substituteEnvironmentVars(containerLabels[label]));
let substitutedVal = substituteEnvironmentVars(containerLabels[label]);
if (value === "widget.version") {
substitutedVal = parseInt(substitutedVal, 10);
}
shvl.set(constructedService, value, substitutedVal);
}
});
@@ -316,6 +324,9 @@ export function cleanServiceGroups(groups) {
mappings,
display,
// deluge, qbittorrent
enableLeechProgress,
// diskstation
volume,
@@ -335,7 +346,7 @@ export function cleanServiceGroups(groups) {
// frigate
enableRecentEvents,
// glances, immich, mealie, pihole, pfsense
// beszel, glances, immich, mealie, pihole, pfsense
version,
// glances
@@ -483,6 +494,9 @@ export function cleanServiceGroups(groups) {
if (allowScrolling) widget.allowScrolling = allowScrolling;
if (refreshInterval) widget.refreshInterval = refreshInterval;
}
if (["deluge", "qbittorrent"].includes(type)) {
if (enableLeechProgress !== undefined) widget.enableLeechProgress = JSON.parse(enableLeechProgress);
}
if (["opnsense", "pfsense"].includes(type)) {
if (wan) widget.wan = wan;
}
@@ -510,7 +524,7 @@ export function cleanServiceGroups(groups) {
if (snapshotHost) widget.snapshotHost = snapshotHost;
if (snapshotPath) widget.snapshotPath = snapshotPath;
}
if (["glances", "immich", "mealie", "pfsense", "pihole"].includes(type)) {
if (["beszel", "glances", "immich", "mealie", "pfsense", "pihole"].includes(type)) {
if (version) widget.version = parseInt(version, 10);
}
if (type === "glances") {
@@ -603,6 +617,7 @@ export function findGroupByName(groups, name) {
} else if (group.groups) {
const foundGroup = findGroupByName(group.groups, name);
if (foundGroup) {
foundGroup.parent = group;
return foundGroup;
}
}

View File

@@ -89,7 +89,9 @@ export default async function credentialedProxyHandler(req, res, map) {
} else if (widget.type === "myspeed") {
headers.Password = `${widget.password}`;
} else if (widget.type === "esphome") {
if (widget.key) {
if (widget.username && widget.password) {
headers.Authorization = `Basic ${Buffer.from(`${widget.username}:${widget.password}`).toString("base64")}`;
} else if (widget.key) {
headers.Cookie = `authenticated=${widget.key}`;
}
} else if (widget.type === "wgeasy") {

View File

@@ -45,7 +45,12 @@ export default async function beszelProxyHandler(req, res) {
if (widget) {
const url = new URL(formatApiCall(widgets[widget.type].api, { endpoint, ...widget }));
const loginUrl = formatApiCall(widgets[widget.type].api, { endpoint: "admins/auth-with-password", ...widget });
let authEndpointVersion = "authv1";
if (widget.version === 2) authEndpointVersion = "authv2";
const loginUrl = formatApiCall(widgets[widget.type].api, {
endpoint: widgets[widget.type].mappings[authEndpointVersion].endpoint,
...widget,
});
let status;
let data;
@@ -54,7 +59,7 @@ export default async function beszelProxyHandler(req, res) {
if (!token) {
[status, token] = await login(loginUrl, widget.username, widget.password, service);
if (status !== 200) {
logger.debug(`HTTP ${status} logging into npm api: ${token}`);
logger.debug(`HTTP ${status} logging into Beszel: ${token}`);
return res.status(status).send(token);
}
}
@@ -68,12 +73,12 @@ export default async function beszelProxyHandler(req, res) {
});
if (status === 403) {
logger.debug(`HTTP ${status} retrieving data from npm api, logging in and trying again.`);
logger.debug(`HTTP ${status} retrieving data from Beszel, logging in and trying again.`);
cache.del(`${tokenCacheKey}.${service}`);
[status, token] = await login(loginUrl, widget.username, widget.password, service);
if (status !== 200) {
logger.debug(`HTTP ${status} logging into npm api: ${data}`);
logger.debug(`HTTP ${status} logging into Beszel: ${data}`);
return res.status(status).send(data);
}

View File

@@ -5,6 +5,12 @@ const widget = {
proxyHandler: beszelProxyHandler,
mappings: {
authv1: {
endpoint: "admins/auth-with-password",
},
authv2: {
endpoint: "collections/_superusers/auth-with-password",
},
systems: {
endpoint: "collections/systems/records?page=1&perPage=500&sort=%2Bcreated",
},

View File

@@ -1,5 +1,7 @@
import { useTranslation } from "next-i18next";
import QueueEntry from "../../components/widgets/queue/queueEntry";
import Container from "components/services/widget/container";
import Block from "components/services/widget/block";
import useWidgetAPI from "utils/proxy/use-widget-api";
@@ -32,21 +34,38 @@ export default function Component({ service }) {
let rateDl = 0;
let rateUl = 0;
let completed = 0;
const leechTorrents = [];
for (let i = 0; i < keys.length; i += 1) {
const torrent = torrents[keys[i]];
rateDl += torrent.download_payload_rate;
rateUl += torrent.upload_payload_rate;
completed += torrent.total_remaining === 0 ? 1 : 0;
if (torrent.state === "Downloading") {
leechTorrents.push(torrent);
}
}
const leech = keys.length - completed || 0;
return (
<Container service={service}>
<Block label="deluge.leech" value={t("common.number", { value: leech })} />
<Block label="deluge.download" value={t("common.byterate", { value: rateDl })} />
<Block label="deluge.seed" value={t("common.number", { value: completed })} />
<Block label="deluge.upload" value={t("common.byterate", { value: rateUl })} />
</Container>
<>
<Container service={service}>
<Block label="deluge.leech" value={t("common.number", { value: leech })} />
<Block label="deluge.download" value={t("common.byterate", { value: rateDl })} />
<Block label="deluge.seed" value={t("common.number", { value: completed })} />
<Block label="deluge.upload" value={t("common.byterate", { value: rateUl })} />
</Container>
{widget?.enableLeechProgress &&
leechTorrents.map((queueEntry) => (
<QueueEntry
progress={queueEntry.progress}
timeLeft={t("common.duration", { value: queueEntry.eta })}
title={queueEntry.name}
activity={queueEntry.state}
key={`${queueEntry.name}-${queueEntry.total_remaining}`}
/>
))}
</>
);
}

View File

@@ -17,6 +17,7 @@ const dataParams = [
"download_payload_rate",
"upload_payload_rate",
"total_remaining",
"eta",
],
{},
];

View File

@@ -1,4 +1,5 @@
import { useContext } from "react";
import classNames from "classnames";
import Error from "./error";
@@ -17,7 +18,7 @@ export default function Container({ children, widget, error = null, chart = true
}
return (
<div>
<div className={classNames("service-container", chart ? "chart relative h-[120px]" : "")}>
{children}
<div className={`absolute top-0 right-0 bottom-0 left-0 overflow-clip pointer-events-none ${className}`} />
{chart && <div className="h-[68px] overflow-clip" />}

View File

@@ -63,7 +63,7 @@ export default function Component({ service }) {
<div className="opacity-25 w-14 text-right">{item.cpu_percent.toFixed(1)}%</div>
<div className="opacity-25 w-14 text-right">
{t("common.bytes", {
value: item.memory_info[memoryInfoKey],
value: item.memory_info[memoryInfoKey] ?? item.memory_info.wset,
maximumFractionDigits: 0,
})}
</div>

View File

@@ -1,12 +1,13 @@
import { useTranslation } from "next-i18next";
import QueueEntry from "../../components/widgets/queue/queueEntry";
import Container from "components/services/widget/container";
import Block from "components/services/widget/block";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
const { t } = useTranslation();
const { widget } = service;
const { data: torrentData, error: torrentError } = useWidgetAPI(widget, "torrents");
@@ -29,6 +30,7 @@ export default function Component({ service }) {
let rateDl = 0;
let rateUl = 0;
let completed = 0;
const leechTorrents = [];
for (let i = 0; i < torrentData.length; i += 1) {
const torrent = torrentData[i];
@@ -37,16 +39,31 @@ export default function Component({ service }) {
if (torrent.progress === 1) {
completed += 1;
}
if (torrent.state.includes("DL") || torrent.state === "downloading") {
leechTorrents.push(torrent);
}
}
const leech = torrentData.length - completed;
return (
<Container service={service}>
<Block label="qbittorrent.leech" value={t("common.number", { value: leech })} />
<Block label="qbittorrent.download" value={t("common.bibyterate", { value: rateDl, decimals: 1 })} />
<Block label="qbittorrent.seed" value={t("common.number", { value: completed })} />
<Block label="qbittorrent.upload" value={t("common.bibyterate", { value: rateUl, decimals: 1 })} />
</Container>
<>
<Container service={service}>
<Block label="qbittorrent.leech" value={t("common.number", { value: leech })} />
<Block label="qbittorrent.download" value={t("common.bibyterate", { value: rateDl, decimals: 1 })} />
<Block label="qbittorrent.seed" value={t("common.number", { value: completed })} />
<Block label="qbittorrent.upload" value={t("common.bibyterate", { value: rateUl, decimals: 1 })} />
</Container>
{widget?.enableLeechProgress &&
leechTorrents.map((queueEntry) => (
<QueueEntry
progress={queueEntry.progress * 100}
timeLeft={t("common.duration", { value: queueEntry.eta })}
title={queueEntry.name}
activity={queueEntry.state}
key={`${queueEntry.name}-${queueEntry.amount_left}`}
/>
))}
</>
);
}