mirror of
https://github.com/gethomepage/homepage.git
synced 2026-04-06 18:21:19 -07:00
Merge branch 'gethomepage:dev' into integration
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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}`, {
|
||||
|
||||
@@ -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={
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 ? (
|
||||
<>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
|
||||
@@ -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}`}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ const dataParams = [
|
||||
"download_payload_rate",
|
||||
"upload_payload_rate",
|
||||
"total_remaining",
|
||||
"eta",
|
||||
],
|
||||
{},
|
||||
];
|
||||
|
||||
@@ -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" />}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}`}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user