Merge branch 'dev' into feature/next-14
This commit is contained in:
commit
2877d99ed2
215 files changed
+13540
-5081
No files matched your search
@@ -20,7 +20,7 @@ export default function BookmarksGroup({ bookmarks, layout, disableCollapse, gro
|
||||
className={classNames(
|
||||
"bookmark-group",
|
||||
layout?.style === "row" ? "basis-full" : "basis-full md:basis-1/4 lg:basis-1/5 xl:basis-1/6",
|
||||
layout?.header === false ? "flex-1 px-1 -my-1" : "flex-1 p-1",
|
||||
layout?.header === false ? "flex-1 px-1 -my-1 overflow-hidden" : "flex-1 p-1 overflow-hidden",
|
||||
)}
|
||||
>
|
||||
<Disclosure defaultOpen={!(layout?.initiallyCollapsed ?? groupsInitiallyCollapsed) ?? true}>
|
||||
|
||||
@@ -13,6 +13,7 @@ export default function Item({ bookmark }) {
|
||||
<a
|
||||
href={bookmark.href}
|
||||
title={bookmark.name}
|
||||
rel="noreferrer"
|
||||
target={bookmark.target ?? settings.target ?? "_blank"}
|
||||
className={classNames(
|
||||
settings.cardBlur !== undefined && `backdrop-blur${settings.cardBlur.length ? "-" : ""}${settings.cardBlur}`,
|
||||
@@ -28,9 +29,9 @@ export default function Item({ bookmark }) {
|
||||
)}
|
||||
{!bookmark.icon && bookmark.abbr}
|
||||
</div>
|
||||
<div className="flex-1 flex items-center justify-between rounded-r-md bookmark-text">
|
||||
<div className="flex-1 grow pl-3 py-2 text-xs bookmark-name">{bookmark.name}</div>
|
||||
<div className="px-2 py-2 truncate text-theme-500 dark:text-theme-300 text-xs bookmark-description">
|
||||
<div className="flex-1 overflow-hidden flex items-center justify-between rounded-r-md bookmark-text">
|
||||
<div className="pl-3 py-2 text-xs bookmark-name">{bookmark.name}</div>
|
||||
<div className="shrink truncate px-2 py-2 text-theme-500 dark:text-theme-300 text-xs bookmark-description">
|
||||
{description}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -98,6 +98,12 @@ export default function QuickLaunch({ servicesAndBookmarks, searchString, setSea
|
||||
} else if (event.key === "ArrowUp" && currentItemIndex > 0) {
|
||||
setCurrentItemIndex(currentItemIndex - 1);
|
||||
event.preventDefault();
|
||||
} else if (
|
||||
event.key === "ArrowRight" &&
|
||||
results[currentItemIndex] &&
|
||||
results[currentItemIndex].type === "searchSuggestion"
|
||||
) {
|
||||
setSearchString(results[currentItemIndex].name);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,35 @@ export default function ResolvedIcon({ icon, width = 32, height = 32, alt = "log
|
||||
// check mdi- or si- prefixed icons
|
||||
const prefix = icon.split("-")[0];
|
||||
|
||||
if (prefix === "sh") {
|
||||
const iconName = icon.replace("sh-", "").replace(".svg", "").replace(".png", "").replace(".webp", "");
|
||||
|
||||
let extension;
|
||||
if (icon.endsWith(".svg")) {
|
||||
extension = "svg";
|
||||
} else if (icon.endsWith(".webp")) {
|
||||
extension = "webp";
|
||||
} else {
|
||||
extension = "png";
|
||||
}
|
||||
|
||||
return (
|
||||
<Image
|
||||
src={`https://cdn.jsdelivr.net/gh/selfhst/icons@main/${extension}/${iconName}.${extension}`}
|
||||
width={width}
|
||||
height={height}
|
||||
style={{
|
||||
width,
|
||||
height,
|
||||
objectFit: "contain",
|
||||
maxHeight: "100%",
|
||||
maxWidth: "100%",
|
||||
}}
|
||||
alt={alt}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (prefix in iconSetURLs) {
|
||||
// default to theme setting
|
||||
let iconName = icon.replace(`${prefix}-`, "").replace(".svg", "");
|
||||
|
||||
@@ -12,6 +12,10 @@ function displayData(data) {
|
||||
export default function Error({ error }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (typeof error === "string") {
|
||||
error = { message: error }; // eslint-disable-line no-param-reassign
|
||||
}
|
||||
|
||||
if (error?.data?.error) {
|
||||
error = error.data.error; // eslint-disable-line no-param-reassign
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ export function slugifyAndEncode(tabName) {
|
||||
export default function Tab({ tab }) {
|
||||
const { activeTab, setActiveTab } = useContext(TabContext);
|
||||
|
||||
const matchesTab = decodeURI(activeTab) === slugify(tab);
|
||||
const matchesTab = decodeURIComponent(activeTab) === slugify(tab);
|
||||
|
||||
return (
|
||||
<li
|
||||
|
||||
@@ -25,7 +25,7 @@ export default function Uptime({ refresh = 1500 }) {
|
||||
return (
|
||||
<Resource
|
||||
icon={FaRegClock}
|
||||
value={t("common.uptime", { value: data.uptime })}
|
||||
value={t("common.duration", { value: data.uptime })}
|
||||
label={t("resources.uptime")}
|
||||
percentage={percent}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import useSWR from "swr";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { FaChartLine } from "react-icons/fa6";
|
||||
|
||||
import Error from "../widget/error";
|
||||
import Container from "../widget/container";
|
||||
import PrimaryText from "../widget/primary_text";
|
||||
import WidgetIcon from "../widget/widget_icon";
|
||||
import Raw from "../widget/raw";
|
||||
|
||||
export default function Widget({ options }) {
|
||||
const { t, i18n } = useTranslation();
|
||||
|
||||
const [viewingPercentChange, setViewingPercentChange] = useState(false);
|
||||
|
||||
const { color } = options;
|
||||
|
||||
const { data, error } = useSWR(
|
||||
`/api/widgets/stocks?${new URLSearchParams({ lang: i18n.language, ...options }).toString()}`,
|
||||
);
|
||||
|
||||
if (error || data?.error) {
|
||||
return <Error options={options} />;
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return (
|
||||
<Container>
|
||||
<WidgetIcon icon={FaChartLine} />
|
||||
<PrimaryText>{t("stocks.loading")}...</PrimaryText>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
if (data) {
|
||||
return (
|
||||
<Container options={options} additionalClassNames="information-widget-stocks">
|
||||
<Raw>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setViewingPercentChange(!viewingPercentChange)}
|
||||
className="flex items-center w-full h-full hover:outline-none focus:outline-none"
|
||||
>
|
||||
<FaChartLine className="flex-none w-5 h-5 text-theme-800 dark:text-theme-200 mr-2" />
|
||||
<div className="flex flex-wrap items-center gap-0.5">
|
||||
{data.stocks.map(
|
||||
(stock) =>
|
||||
stock && (
|
||||
<div
|
||||
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>
|
||||
{!viewingPercentChange ? (
|
||||
<span
|
||||
className={
|
||||
color !== false
|
||||
? `text-xs ${stock.percentChange < 0 ? "text-rose-300/70" : "text-emerald-300/70"}`
|
||||
: "text-theme-800/70 dark:text-theme-200/50 text-xs"
|
||||
}
|
||||
>
|
||||
{stock.currentPrice !== null
|
||||
? t("common.number", {
|
||||
value: stock.currentPrice,
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
})
|
||||
: t("widget.api_error")}
|
||||
</span>
|
||||
) : (
|
||||
<span
|
||||
className={
|
||||
color !== false
|
||||
? `text-xs ${stock.percentChange < 0 ? "text-rose-300/70" : "text-emerald-300/70"}`
|
||||
: "text-theme-800/70 dark:text-theme-200/70 text-xs"
|
||||
}
|
||||
>
|
||||
{stock.percentChange !== null ? `${stock.percentChange}%` : t("widget.api_error")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
</Raw>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ const widgetMappings = {
|
||||
openmeteo: dynamic(() => import("components/widgets/openmeteo/openmeteo")),
|
||||
longhorn: dynamic(() => import("components/widgets/longhorn/longhorn")),
|
||||
kubernetes: dynamic(() => import("components/widgets/kubernetes/kubernetes")),
|
||||
stocks: dynamic(() => import("components/widgets/stocks/stocks")),
|
||||
};
|
||||
|
||||
export default function Widget({ widget, style }) {
|
||||
|
||||
@@ -8,7 +8,7 @@ export default function Document() {
|
||||
name="description"
|
||||
content="A highly customizable homepage (or startpage / application dashboard) with Docker and service API integrations."
|
||||
/>
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<link rel="manifest" href="/site.webmanifest?v=4" crossOrigin="use-credentials" />
|
||||
</Head>
|
||||
<body>
|
||||
|
||||
@@ -48,8 +48,8 @@ export default async function handler(req, res) {
|
||||
logger.error(`no pods found with namespace=${namespace} and labelSelector=${labelSelector}`);
|
||||
return;
|
||||
}
|
||||
const someReady = pods.find((pod) => pod.status.phase === "Running");
|
||||
const allReady = pods.every((pod) => pod.status.phase === "Running");
|
||||
const someReady = pods.find((pod) => ["Succeeded", "Running"].includes(pod.status.phase));
|
||||
const allReady = pods.every((pod) => ["Succeeded", "Running"].includes(pod.status.phase));
|
||||
let status = "down";
|
||||
if (allReady) {
|
||||
status = "running";
|
||||
|
||||
@@ -21,7 +21,7 @@ export default async function handler(req, res) {
|
||||
|
||||
if (!widget) {
|
||||
logger.debug("Unknown proxy service type: %s", type);
|
||||
return res.status(403).json({ error: "Unkown proxy service type" });
|
||||
return res.status(403).json({ error: "Unknown proxy service type" });
|
||||
}
|
||||
|
||||
const serviceProxyHandler = widget.proxyHandler || genericProxyHandler;
|
||||
@@ -107,7 +107,7 @@ export default async function handler(req, res) {
|
||||
}
|
||||
|
||||
logger.debug("Unknown proxy service type: %s", type);
|
||||
return res.status(403).json({ error: "Unkown proxy service type" });
|
||||
return res.status(403).json({ error: "Unknown proxy service type" });
|
||||
} catch (e) {
|
||||
if (e) logger.error(e);
|
||||
return res.status(500).send({ error: "Unexpected error" });
|
||||
|
||||
@@ -22,11 +22,12 @@ export default async function handler(req, res) {
|
||||
.then((response) => response.body)
|
||||
.catch((error) => {
|
||||
logger.error("Error getting ingresses: %d %s %s", error.statusCode, error.body, error.response);
|
||||
logger.debug(error);
|
||||
return null;
|
||||
});
|
||||
if (!nodes) {
|
||||
return res.status(500).send({
|
||||
error: "unknown error",
|
||||
error: "An error occurred while fetching nodes, check logs for more details.",
|
||||
});
|
||||
}
|
||||
let cpuTotal = 0;
|
||||
|
||||
@@ -2,7 +2,7 @@ import cachedFetch from "utils/proxy/cached-fetch";
|
||||
|
||||
export default async function handler(req, res) {
|
||||
const { latitude, longitude, units, cache, timezone } = req.query;
|
||||
const degrees = units === "imperial" ? "fahrenheit" : "celsius";
|
||||
const degrees = units === "metric" ? "celsius" : "fahrenheit";
|
||||
const timezeone = timezone ?? "auto";
|
||||
const apiUrl = `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}&daily=sunrise,sunset¤t_weather=true&temperature_unit=${degrees}&timezone=${timezeone}`;
|
||||
return res.send(await cachedFetch(apiUrl, cache));
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { existsSync } from "fs";
|
||||
|
||||
import createLogger from "utils/logger";
|
||||
|
||||
const logger = createLogger("resources");
|
||||
|
||||
const si = require("systeminformation");
|
||||
|
||||
export default async function handler(req, res) {
|
||||
@@ -23,26 +27,31 @@ export default async function handler(req, res) {
|
||||
}
|
||||
|
||||
const fsSize = await si.fsSize();
|
||||
|
||||
logger.debug("fsSize:", JSON.stringify(fsSize));
|
||||
return res.status(200).json({
|
||||
drive: fsSize.find((fs) => fs.mount === target) ?? fsSize.find((fs) => fs.mount === "/"),
|
||||
});
|
||||
}
|
||||
|
||||
if (type === "memory") {
|
||||
const memory = await si.mem();
|
||||
logger.debug("memory:", JSON.stringify(memory));
|
||||
return res.status(200).json({
|
||||
memory: await si.mem(),
|
||||
memory,
|
||||
});
|
||||
}
|
||||
|
||||
if (type === "cputemp") {
|
||||
const cputemp = await si.cpuTemperature();
|
||||
logger.debug("cputemp:", JSON.stringify(cputemp));
|
||||
return res.status(200).json({
|
||||
cputemp: await si.cpuTemperature(),
|
||||
cputemp,
|
||||
});
|
||||
}
|
||||
|
||||
if (type === "uptime") {
|
||||
const timeData = await si.time();
|
||||
logger.debug("timeData:", JSON.stringify(timeData));
|
||||
return res.status(200).json({
|
||||
uptime: timeData.uptime,
|
||||
});
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import cachedFetch from "utils/proxy/cached-fetch";
|
||||
import { getSettings } from "utils/config/config";
|
||||
|
||||
export default async function handler(req, res) {
|
||||
const { watchlist, provider, cache } = req.query;
|
||||
|
||||
if (!watchlist) {
|
||||
return res.status(400).json({ error: "Missing watchlist" });
|
||||
}
|
||||
|
||||
const watchlistArr = watchlist.split(",") || [watchlist];
|
||||
|
||||
if (!watchlistArr.length || watchlistArr[0] === "null" || !watchlistArr[0]) {
|
||||
return res.status(400).json({ error: "Missing watchlist" });
|
||||
}
|
||||
|
||||
if (watchlistArr.length > 8) {
|
||||
return res.status(400).json({ error: "Max items in watchlist is 8" });
|
||||
}
|
||||
|
||||
const hasDuplicates = new Set(watchlistArr).size !== watchlistArr.length;
|
||||
|
||||
if (hasDuplicates) {
|
||||
return res.status(400).json({ error: "Watchlist contains duplicates" });
|
||||
}
|
||||
|
||||
if (!provider) {
|
||||
return res.status(400).json({ error: "Missing provider" });
|
||||
}
|
||||
|
||||
if (provider !== "finnhub") {
|
||||
return res.status(400).json({ error: "Invalid provider" });
|
||||
}
|
||||
|
||||
const providersInConfig = getSettings()?.providers;
|
||||
|
||||
let apiKey;
|
||||
Object.entries(providersInConfig).forEach(([key, val]) => {
|
||||
if (key === provider) apiKey = val;
|
||||
});
|
||||
|
||||
if (typeof apiKey === "undefined") {
|
||||
return res.status(400).json({ error: "Missing or invalid API Key for provider" });
|
||||
}
|
||||
|
||||
if (provider === "finnhub") {
|
||||
// Finnhub allows up to 30 calls/second
|
||||
// https://finnhub.io/docs/api/rate-limit
|
||||
const results = await Promise.all(
|
||||
watchlistArr.map(async (ticker) => {
|
||||
if (!ticker) {
|
||||
return { ticker: null, currentPrice: null, percentChange: null };
|
||||
}
|
||||
// https://finnhub.io/docs/api/quote
|
||||
const apiUrl = `https://finnhub.io/api/v1/quote?symbol=${ticker}&token=${apiKey}`;
|
||||
// Finnhub free accounts allow up to 60 calls/minute
|
||||
// https://finnhub.io/pricing
|
||||
const { c, dp } = await cachedFetch(apiUrl, cache || 1);
|
||||
|
||||
// API sometimes returns 200, but values returned are `null`
|
||||
if (c === null || dp === null) {
|
||||
return { ticker, currentPrice: null, percentChange: null };
|
||||
}
|
||||
|
||||
// Rounding percentage, but we want it back to a number for comparison
|
||||
return { ticker, currentPrice: c.toFixed(2), percentChange: parseFloat(dp.toFixed(2)) };
|
||||
}),
|
||||
);
|
||||
|
||||
return res.send({
|
||||
stocks: results,
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(400).json({ error: "Invalid configuration" });
|
||||
}
|
||||
+1
-1
@@ -393,7 +393,7 @@ function Home({ initialSettings }) {
|
||||
<div
|
||||
id="information-widgets"
|
||||
className={classNames(
|
||||
"flex flex-row flex-wrap justify-between",
|
||||
"flex flex-row flex-wrap justify-between z-20",
|
||||
headerStyles[headerStyle],
|
||||
settings.cardBlur !== undefined &&
|
||||
headerStyle === "boxed" &&
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
# For configuration options and examples, please see:
|
||||
# https://gethomepage.dev/latest/configs/bookmarks
|
||||
# https://gethomepage.dev/configs/bookmarks
|
||||
|
||||
- Developer:
|
||||
- Github:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
# For configuration options and examples, please see:
|
||||
# https://gethomepage.dev/latest/configs/docker/
|
||||
# https://gethomepage.dev/configs/docker/
|
||||
|
||||
# my-docker:
|
||||
# host: 127.0.0.1
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
# For configuration options and examples, please see:
|
||||
# https://gethomepage.dev/latest/configs/services
|
||||
# https://gethomepage.dev/configs/services
|
||||
|
||||
- My First Group:
|
||||
- My First Service:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
# For configuration options and examples, please see:
|
||||
# https://gethomepage.dev/latest/configs/settings
|
||||
# https://gethomepage.dev/configs/settings
|
||||
|
||||
providers:
|
||||
openweathermap: openweathermapapikey
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
# For configuration options and examples, please see:
|
||||
# https://gethomepage.dev/latest/configs/service-widgets
|
||||
# https://gethomepage.dev/configs/service-widgets
|
||||
|
||||
- resources:
|
||||
cpu: true
|
||||
|
||||
@@ -198,6 +198,7 @@ export async function servicesFromKubernetes() {
|
||||
.then((response) => response.body)
|
||||
.catch((error) => {
|
||||
logger.error("Error getting ingresses: %d %s %s", error.statusCode, error.body, error.response);
|
||||
logger.debug(error);
|
||||
return null;
|
||||
});
|
||||
|
||||
@@ -215,6 +216,7 @@ export async function servicesFromKubernetes() {
|
||||
error.body,
|
||||
error.response,
|
||||
);
|
||||
logger.debug(error);
|
||||
}
|
||||
|
||||
return [];
|
||||
@@ -231,6 +233,7 @@ export async function servicesFromKubernetes() {
|
||||
error.body,
|
||||
error.response,
|
||||
);
|
||||
logger.debug(error);
|
||||
}
|
||||
|
||||
return [];
|
||||
@@ -300,6 +303,7 @@ export async function servicesFromKubernetes() {
|
||||
constructedService = JSON.parse(substituteEnvironmentVars(JSON.stringify(constructedService)));
|
||||
} catch (e) {
|
||||
logger.error("Error attempting k8s environment variable substitution.");
|
||||
logger.debug(e);
|
||||
}
|
||||
|
||||
return constructedService;
|
||||
@@ -364,6 +368,9 @@ export function cleanServiceGroups(groups) {
|
||||
repositoryId,
|
||||
userEmail,
|
||||
|
||||
// beszel
|
||||
systemId,
|
||||
|
||||
// calendar
|
||||
firstDayInWeek,
|
||||
integrations,
|
||||
@@ -399,7 +406,10 @@ export function cleanServiceGroups(groups) {
|
||||
expandOneStreamToTwoRows,
|
||||
showEpisodeNumber,
|
||||
|
||||
// glances, pihole
|
||||
// frigate
|
||||
enableRecentEvents,
|
||||
|
||||
// glances, immich, mealie, pihole, pfsense
|
||||
version,
|
||||
|
||||
// glances
|
||||
@@ -408,7 +418,7 @@ export function cleanServiceGroups(groups) {
|
||||
pointsLimit,
|
||||
diskUnits,
|
||||
|
||||
// glances, customapi, iframe
|
||||
// glances, customapi, iframe, prometheusmetric
|
||||
refreshInterval,
|
||||
|
||||
// hdhomerun
|
||||
@@ -435,6 +445,9 @@ export function cleanServiceGroups(groups) {
|
||||
namespace,
|
||||
podSelector,
|
||||
|
||||
// lubelogger
|
||||
vehicleID,
|
||||
|
||||
// mjpeg
|
||||
fit,
|
||||
stream,
|
||||
@@ -448,6 +461,9 @@ export function cleanServiceGroups(groups) {
|
||||
// opnsense, pfsense
|
||||
wan,
|
||||
|
||||
// prometheusmetric
|
||||
metrics,
|
||||
|
||||
// proxmox
|
||||
node,
|
||||
|
||||
@@ -457,6 +473,10 @@ export function cleanServiceGroups(groups) {
|
||||
// sonarr, radarr
|
||||
enableQueue,
|
||||
|
||||
// stocks
|
||||
watchlist,
|
||||
showUSMarketStatus,
|
||||
|
||||
// truenas
|
||||
enablePools,
|
||||
nasType,
|
||||
@@ -464,8 +484,17 @@ export function cleanServiceGroups(groups) {
|
||||
// unifi
|
||||
site,
|
||||
|
||||
// vikunja
|
||||
enableTaskList,
|
||||
|
||||
// wgeasy
|
||||
threshold,
|
||||
|
||||
// technitium
|
||||
range,
|
||||
|
||||
// spoolman
|
||||
spoolIds,
|
||||
} = cleanedService.widget;
|
||||
|
||||
let fieldsList = fields;
|
||||
@@ -491,6 +520,10 @@ export function cleanServiceGroups(groups) {
|
||||
if (repositoryId) cleanedService.widget.repositoryId = repositoryId;
|
||||
}
|
||||
|
||||
if (type === "beszel") {
|
||||
if (systemId) cleanedService.widget.systemId = systemId;
|
||||
}
|
||||
|
||||
if (type === "coinmarketcap") {
|
||||
if (currency) cleanedService.widget.currency = currency;
|
||||
if (symbols) cleanedService.widget.symbols = symbols;
|
||||
@@ -551,8 +584,8 @@ export function cleanServiceGroups(groups) {
|
||||
if (snapshotHost) cleanedService.widget.snapshotHost = snapshotHost;
|
||||
if (snapshotPath) cleanedService.widget.snapshotPath = snapshotPath;
|
||||
}
|
||||
if (["glances", "pihole"].includes(type)) {
|
||||
if (version) cleanedService.widget.version = version;
|
||||
if (["glances", "immich", "mealie", "pfsense", "pihole"].includes(type)) {
|
||||
if (version) cleanedService.widget.version = parseInt(version, 10);
|
||||
}
|
||||
if (type === "glances") {
|
||||
if (metric) cleanedService.widget.metric = metric;
|
||||
@@ -600,9 +633,32 @@ export function cleanServiceGroups(groups) {
|
||||
cleanedService.widget.bitratePrecision = parseInt(bitratePrecision, 10);
|
||||
}
|
||||
}
|
||||
if (type === "stocks") {
|
||||
if (watchlist) cleanedService.widget.watchlist = watchlist;
|
||||
if (showUSMarketStatus) cleanedService.widget.showUSMarketStatus = showUSMarketStatus;
|
||||
}
|
||||
if (type === "wgeasy") {
|
||||
if (threshold !== undefined) cleanedService.widget.threshold = parseInt(threshold, 10);
|
||||
}
|
||||
if (type === "frigate") {
|
||||
if (enableRecentEvents !== undefined) cleanedService.widget.enableRecentEvents = enableRecentEvents;
|
||||
}
|
||||
if (type === "technitium") {
|
||||
if (range !== undefined) cleanedService.widget.range = range;
|
||||
}
|
||||
if (type === "lubelogger") {
|
||||
if (vehicleID !== undefined) cleanedService.widget.vehicleID = parseInt(vehicleID, 10);
|
||||
}
|
||||
if (type === "vikunja") {
|
||||
if (enableTaskList !== undefined) cleanedService.widget.enableTaskList = !!enableTaskList;
|
||||
}
|
||||
if (type === "prometheusmetric") {
|
||||
if (metrics) cleanedService.widget.metrics = metrics;
|
||||
if (refreshInterval) cleanedService.widget.refreshInterval = refreshInterval;
|
||||
}
|
||||
if (type === "spoolman") {
|
||||
if (spoolIds !== undefined) cleanedService.widget.spoolIds = spoolIds;
|
||||
}
|
||||
}
|
||||
|
||||
return cleanedService;
|
||||
|
||||
@@ -3,6 +3,7 @@ import { formatApiCall, sanitizeErrorURL } from "utils/proxy/api-helpers";
|
||||
import validateWidgetData from "utils/proxy/validate-widget-data";
|
||||
import { httpProxy } from "utils/proxy/http";
|
||||
import createLogger from "utils/logger";
|
||||
import { getSettings } from "utils/config/config";
|
||||
import widgets from "widgets/widgets";
|
||||
|
||||
const logger = createLogger("credentialedProxyHandler");
|
||||
@@ -24,14 +25,30 @@ export default async function credentialedProxyHandler(req, res, map) {
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
|
||||
if (widget.type === "coinmarketcap") {
|
||||
if (widget.type === "stocks") {
|
||||
const { providers } = getSettings();
|
||||
if (widget.provider === "finnhub" && providers?.finnhub) {
|
||||
headers["X-Finnhub-Token"] = `${providers?.finnhub}`;
|
||||
}
|
||||
} else if (widget.type === "coinmarketcap") {
|
||||
headers["X-CMC_PRO_API_KEY"] = `${widget.key}`;
|
||||
} else if (widget.type === "gotify") {
|
||||
headers["X-gotify-Key"] = `${widget.key}`;
|
||||
} else if (
|
||||
["authentik", "cloudflared", "ghostfolio", "mealie", "tailscale", "tandoor", "pterodactyl"].includes(
|
||||
widget.type,
|
||||
)
|
||||
[
|
||||
"argocd",
|
||||
"authentik",
|
||||
"cloudflared",
|
||||
"ghostfolio",
|
||||
"headscale",
|
||||
"linkwarden",
|
||||
"mealie",
|
||||
"netalertx",
|
||||
"tailscale",
|
||||
"tandoor",
|
||||
"pterodactyl",
|
||||
"vikunja",
|
||||
].includes(widget.type)
|
||||
) {
|
||||
headers.Authorization = `Bearer ${widget.key}`;
|
||||
} else if (widget.type === "truenas") {
|
||||
@@ -71,6 +88,14 @@ export default async function credentialedProxyHandler(req, res, map) {
|
||||
headers.Key = `${widget.key}`;
|
||||
} else if (widget.type === "myspeed") {
|
||||
headers.Password = `${widget.password}`;
|
||||
} else if (widget.type === "esphome") {
|
||||
if (widget.key) {
|
||||
headers.Cookie = `authenticated=${widget.key}`;
|
||||
}
|
||||
} else if (widget.type === "wgeasy") {
|
||||
headers.Authorization = widget.password;
|
||||
} else if (widget.type === "gitlab") {
|
||||
headers["PRIVATE-TOKEN"] = widget.key;
|
||||
} else {
|
||||
headers["X-API-Key"] = `${widget.key}`;
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ export default async function genericProxyHandler(req, res, map) {
|
||||
formatApiCall(widgets[widget.type].api, { endpoint, ...widget }).replace(/(?<=\?.*)\?/g, "&"),
|
||||
);
|
||||
|
||||
const headers = req.extraHeaders ?? widget.headers ?? {};
|
||||
const headers = req.extraHeaders ?? widget.headers ?? widgets[widget.type].headers ?? {};
|
||||
|
||||
if (widget.username && widget.password) {
|
||||
headers.Authorization = `Basic ${Buffer.from(`${widget.username}:${widget.password}`).toString("base64")}`;
|
||||
@@ -75,7 +75,13 @@ export default async function genericProxyHandler(req, res, map) {
|
||||
url.port ? `:${url.port}` : "",
|
||||
url.pathname,
|
||||
);
|
||||
return res.status(status).json({ error: { message: "HTTP Error", url: sanitizeErrorURL(url), resultData } });
|
||||
return res.status(status).json({
|
||||
error: {
|
||||
message: "HTTP Error",
|
||||
url: sanitizeErrorURL(url),
|
||||
resultData: Buffer.isBuffer(resultData) ? Buffer.from(resultData).toString() : resultData,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(status).send(resultData);
|
||||
|
||||
@@ -8,14 +8,18 @@ import widgets from "widgets/widgets";
|
||||
|
||||
const logger = createLogger("jsonrpcProxyHandler");
|
||||
|
||||
export async function sendJsonRpcRequest(url, method, params, username, password) {
|
||||
export async function sendJsonRpcRequest(url, method, params, widget) {
|
||||
const headers = {
|
||||
"content-type": "application/json",
|
||||
accept: "application/json",
|
||||
};
|
||||
|
||||
if (username && password) {
|
||||
headers.authorization = `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`;
|
||||
if (widget?.username && widget?.password) {
|
||||
headers.Authorization = `Basic ${Buffer.from(`${widget.username}:${widget.password}`).toString("base64")}`;
|
||||
}
|
||||
|
||||
if (widget?.key) {
|
||||
headers.Authorization = `Bearer ${widget.key}`;
|
||||
}
|
||||
|
||||
const client = new JSONRPCClient(async (rpcRequest) => {
|
||||
@@ -67,6 +71,9 @@ export default async function jsonrpcProxyHandler(req, res) {
|
||||
const widget = await getServiceWidget(group, service);
|
||||
const api = widgets?.[widget.type]?.api;
|
||||
|
||||
const [, mapping] = Object.entries(widgets?.[widget.type]?.mappings).find(([, value]) => value.endpoint === method);
|
||||
const params = mapping?.params ?? null;
|
||||
|
||||
if (!api) {
|
||||
return res.status(403).json({ error: "Service does not support API calls" });
|
||||
}
|
||||
@@ -74,8 +81,7 @@ export default async function jsonrpcProxyHandler(req, res) {
|
||||
if (widget) {
|
||||
const url = formatApiCall(api, { ...widget });
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const [status, contentType, data] = await sendJsonRpcRequest(url, method, null, widget.username, widget.password);
|
||||
const [status, , data] = await sendJsonRpcRequest(url, method, params, widget);
|
||||
return res.status(status).end(data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
/* eslint-disable no-console */
|
||||
import widgets from "widgets/widgets";
|
||||
import createLogger from "utils/logger";
|
||||
|
||||
const logger = createLogger("validateWidgetData");
|
||||
|
||||
export default function validateWidgetData(widget, endpoint, data) {
|
||||
let valid = true;
|
||||
@@ -10,8 +13,13 @@ export default function validateWidgetData(widget, endpoint, data) {
|
||||
try {
|
||||
dataParsed = JSON.parse(data);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
valid = false;
|
||||
try {
|
||||
// try once more stripping whitespace
|
||||
dataParsed = JSON.parse(data.toString().replace(/\s/g, ""));
|
||||
} catch (e2) {
|
||||
error = e || e2;
|
||||
valid = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +36,7 @@ export default function validateWidgetData(widget, endpoint, data) {
|
||||
}
|
||||
|
||||
if (!valid) {
|
||||
console.warn(
|
||||
logger.error(
|
||||
`Invalid data for widget '${widget.type}' endpoint '${endpoint}':\nExpected:${mapping?.validate}\nParse error: ${
|
||||
error ?? "none"
|
||||
}\nData: ${JSON.stringify(data)}`,
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
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 { widget } = service;
|
||||
|
||||
if (!widget.fields) {
|
||||
widget.fields = ["apps", "synced", "outOfSync", "healthy"];
|
||||
}
|
||||
|
||||
const MAX_ALLOWED_FIELDS = 4;
|
||||
if (widget.fields.length > MAX_ALLOWED_FIELDS) {
|
||||
widget.fields = widget.fields.slice(0, MAX_ALLOWED_FIELDS);
|
||||
}
|
||||
|
||||
const { data: appsData, error: appsError } = useWidgetAPI(widget, "applications");
|
||||
|
||||
const appCounts = widget.fields.map((status) => {
|
||||
if (status === "apps") {
|
||||
return { status, count: appsData?.items?.length };
|
||||
}
|
||||
const count = appsData?.items?.filter(
|
||||
(item) =>
|
||||
item.status?.sync?.status.toLowerCase() === status.toLowerCase() ||
|
||||
item.status?.health?.status.toLowerCase() === status.toLowerCase(),
|
||||
).length;
|
||||
return { status, count };
|
||||
});
|
||||
|
||||
if (appsError) {
|
||||
return <Container service={service} error={appsError} />;
|
||||
}
|
||||
|
||||
if (!appsData) {
|
||||
return (
|
||||
<Container service={service}>
|
||||
{appCounts.map((a) => (
|
||||
<Block label={`argocd.${a.status}`} key={a.status} />
|
||||
))}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Container service={service}>
|
||||
{appCounts.map((a) => (
|
||||
<Block label={`argocd.${a.status}`} key={a.status} value={a.count} />
|
||||
))}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import credentialedProxyHandler from "utils/proxy/handlers/credentialed";
|
||||
|
||||
const widget = {
|
||||
api: "{url}/api/v1/{endpoint}",
|
||||
proxyHandler: credentialedProxyHandler,
|
||||
|
||||
mappings: {
|
||||
applications: {
|
||||
endpoint: "applications",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default widget;
|
||||
@@ -39,21 +39,15 @@ export default function Component({ service }) {
|
||||
<Block label="audiobookshelf.podcasts" value={t("common.number", { value: totalPodcasts })} />
|
||||
<Block
|
||||
label="audiobookshelf.podcastsDuration"
|
||||
value={t("common.number", {
|
||||
value: totalPodcastsDuration / 60,
|
||||
maximumFractionDigits: 0,
|
||||
style: "unit",
|
||||
unit: "minute",
|
||||
value={t("common.duration", {
|
||||
value: totalPodcastsDuration,
|
||||
})}
|
||||
/>
|
||||
<Block label="audiobookshelf.books" value={t("common.number", { value: totalBooks })} />
|
||||
<Block
|
||||
label="audiobookshelf.booksDuration"
|
||||
value={t("common.number", {
|
||||
value: totalBooksDuration / 60,
|
||||
maximumFractionDigits: 0,
|
||||
style: "unit",
|
||||
unit: "minute",
|
||||
value={t("common.duration", {
|
||||
value: totalBooksDuration,
|
||||
})}
|
||||
/>
|
||||
</Container>
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
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 default function Component({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { widget } = service;
|
||||
const { systemId } = widget;
|
||||
|
||||
const { data: systems, error: systemsError } = useWidgetAPI(widget, "systems");
|
||||
|
||||
const MAX_ALLOWED_FIELDS = 4;
|
||||
if (!widget.fields?.length > 0) {
|
||||
widget.fields = systemId ? ["name", "status", "cpu", "memory"] : ["systems", "up"];
|
||||
}
|
||||
if (widget.fields?.length > MAX_ALLOWED_FIELDS) {
|
||||
widget.fields = widget.fields.slice(0, MAX_ALLOWED_FIELDS);
|
||||
}
|
||||
|
||||
if (systemsError) {
|
||||
return <Container service={service} error={systemsError} />;
|
||||
}
|
||||
|
||||
if (!systems) {
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="beszel.systems" />
|
||||
<Block label="beszel.up" />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
if (systemId) {
|
||||
const system = systems.items.find((item) => item.id === systemId);
|
||||
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="beszel.name" value={system.name} />
|
||||
<Block label="beszel.status" value={t(`beszel.${system.status}`)} />
|
||||
<Block label="beszel.updated" value={t("common.relativeDate", { value: system.updated })} />
|
||||
<Block label="beszel.cpu" value={t("common.percent", { value: system.info.cpu, maximumFractionDigits: 2 })} />
|
||||
<Block label="beszel.memory" value={t("common.percent", { value: system.info.mp, maximumFractionDigits: 2 })} />
|
||||
<Block label="beszel.disk" value={t("common.percent", { value: system.info.dp, maximumFractionDigits: 2 })} />
|
||||
<Block label="beszel.network" value={t("common.percent", { value: system.info.b, maximumFractionDigits: 2 })} />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
const upTotal = systems.items.filter((item) => item.status === "up").length;
|
||||
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="beszel.systems" value={systems.totalItems} />
|
||||
<Block label="beszel.up" value={`${upTotal} / ${systems.totalItems}`} />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import cache from "memory-cache";
|
||||
|
||||
import getServiceWidget from "utils/config/service-helpers";
|
||||
import { formatApiCall } from "utils/proxy/api-helpers";
|
||||
import { httpProxy } from "utils/proxy/http";
|
||||
import widgets from "widgets/widgets";
|
||||
import createLogger from "utils/logger";
|
||||
|
||||
const proxyName = "beszelProxyHandler";
|
||||
const tokenCacheKey = `${proxyName}__token`;
|
||||
const logger = createLogger(proxyName);
|
||||
|
||||
async function login(loginUrl, username, password, service) {
|
||||
const authResponse = await httpProxy(loginUrl, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ identity: username, password }),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
const status = authResponse[0];
|
||||
let data = authResponse[2];
|
||||
try {
|
||||
data = JSON.parse(Buffer.from(authResponse[2]).toString());
|
||||
|
||||
if (status === 200) {
|
||||
cache.put(`${tokenCacheKey}.${service}`, data.token);
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error(`Error ${status} logging into beszel`, JSON.stringify(authResponse[2]));
|
||||
}
|
||||
return [status, data.token ?? data];
|
||||
}
|
||||
|
||||
export default async function beszelProxyHandler(req, res) {
|
||||
const { group, service, endpoint } = req.query;
|
||||
|
||||
if (group && service) {
|
||||
const widget = await getServiceWidget(group, service);
|
||||
|
||||
if (!widgets?.[widget.type]?.api) {
|
||||
return res.status(403).json({ error: "Service does not support API calls" });
|
||||
}
|
||||
|
||||
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 status;
|
||||
let data;
|
||||
|
||||
let token = cache.get(`${tokenCacheKey}.${service}`);
|
||||
if (!token) {
|
||||
[status, token] = await login(loginUrl, widget.username, widget.password, service);
|
||||
if (status !== 200) {
|
||||
logger.debug(`HTTP ${status} logging into npm api: ${token}`);
|
||||
return res.status(status).send(token);
|
||||
}
|
||||
}
|
||||
|
||||
[status, , data] = await httpProxy(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (status === 403) {
|
||||
logger.debug(`HTTP ${status} retrieving data from npm api, 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}`);
|
||||
return res.status(status).send(data);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
[status, , data] = await httpProxy(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (status !== 200) {
|
||||
return res.status(status).send(data);
|
||||
}
|
||||
|
||||
return res.send(data);
|
||||
}
|
||||
}
|
||||
|
||||
return res.status(400).json({ error: "Invalid proxy service type" });
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import beszelProxyHandler from "./proxy";
|
||||
|
||||
const widget = {
|
||||
api: "{url}/api/{endpoint}",
|
||||
proxyHandler: beszelProxyHandler,
|
||||
|
||||
mappings: {
|
||||
systems: {
|
||||
endpoint: "collections/systems/records?page=1&perPage=500&sort=%2Bcreated",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default widget;
|
||||
@@ -16,7 +16,7 @@ export default function Event({ event, colorVariants, showDate = false, showTime
|
||||
key={`event-${event.title}-${event.date}-${event.additional}`}
|
||||
>
|
||||
{showDateColumn && (
|
||||
<span className="ml-2 w-10">
|
||||
<span className="ml-2 w-12">
|
||||
<span>
|
||||
{(showDate || showTime) &&
|
||||
event.date
|
||||
|
||||
@@ -21,7 +21,7 @@ export default async function calendarProxyHandler(req, res) {
|
||||
if (contentType) res.setHeader("Content-Type", contentType);
|
||||
|
||||
if (status !== 200) {
|
||||
logger.debug(`HTTTP ${status} retrieving data from integration URL ${integration.url} : ${data}`);
|
||||
logger.debug(`HTTP ${status} retrieving data from integration URL ${integration.url} : ${data}`);
|
||||
return res.status(status).send(data);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,12 +2,14 @@ import dynamic from "next/dynamic";
|
||||
|
||||
const components = {
|
||||
adguard: dynamic(() => import("./adguard/component")),
|
||||
argocd: dynamic(() => import("./argocd/component")),
|
||||
atsumeru: dynamic(() => import("./atsumeru/component")),
|
||||
audiobookshelf: dynamic(() => import("./audiobookshelf/component")),
|
||||
authentik: dynamic(() => import("./authentik/component")),
|
||||
autobrr: dynamic(() => import("./autobrr/component")),
|
||||
azuredevops: dynamic(() => import("./azuredevops/component")),
|
||||
bazarr: dynamic(() => import("./bazarr/component")),
|
||||
beszel: dynamic(() => import("./beszel/component")),
|
||||
caddy: dynamic(() => import("./caddy/component")),
|
||||
calendar: dynamic(() => import("./calendar/component")),
|
||||
calibreweb: dynamic(() => import("./calibreweb/component")),
|
||||
@@ -19,6 +21,7 @@ const components = {
|
||||
iframe: dynamic(() => import("./iframe/component")),
|
||||
customapi: dynamic(() => import("./customapi/component")),
|
||||
deluge: dynamic(() => import("./deluge/component")),
|
||||
develancacheui: dynamic(() => import("./develancacheui/component")),
|
||||
diskstation: dynamic(() => import("./diskstation/component")),
|
||||
downloadstation: dynamic(() => import("./downloadstation/component")),
|
||||
docker: dynamic(() => import("./docker/component")),
|
||||
@@ -29,16 +32,19 @@ const components = {
|
||||
fileflows: dynamic(() => import("./fileflows/component")),
|
||||
flood: dynamic(() => import("./flood/component")),
|
||||
freshrss: dynamic(() => import("./freshrss/component")),
|
||||
frigate: dynamic(() => import("./frigate/component")),
|
||||
fritzbox: dynamic(() => import("./fritzbox/component")),
|
||||
gamedig: dynamic(() => import("./gamedig/component")),
|
||||
gatus: dynamic(() => import("./gatus/component")),
|
||||
ghostfolio: dynamic(() => import("./ghostfolio/component")),
|
||||
gitea: dynamic(() => import("./gitea/component")),
|
||||
gitlab: dynamic(() => import("./gitlab/component")),
|
||||
glances: dynamic(() => import("./glances/component")),
|
||||
gluetun: dynamic(() => import("./gluetun/component")),
|
||||
gotify: dynamic(() => import("./gotify/component")),
|
||||
grafana: dynamic(() => import("./grafana/component")),
|
||||
hdhomerun: dynamic(() => import("./hdhomerun/component")),
|
||||
headscale: dynamic(() => import("./headscale/component")),
|
||||
peanut: dynamic(() => import("./peanut/component")),
|
||||
homeassistant: dynamic(() => import("./homeassistant/component")),
|
||||
homebox: dynamic(() => import("./homebox/component")),
|
||||
@@ -53,6 +59,9 @@ const components = {
|
||||
komga: dynamic(() => import("./komga/component")),
|
||||
kopia: dynamic(() => import("./kopia/component")),
|
||||
lidarr: dynamic(() => import("./lidarr/component")),
|
||||
linkwarden: dynamic(() => import("./linkwarden/component")),
|
||||
lubelogger: dynamic(() => import("./lubelogger/component")),
|
||||
mailcow: dynamic(() => import("./mailcow/component")),
|
||||
mastodon: dynamic(() => import("./mastodon/component")),
|
||||
mealie: dynamic(() => import("./mealie/component")),
|
||||
medusa: dynamic(() => import("./medusa/component")),
|
||||
@@ -88,6 +97,7 @@ const components = {
|
||||
plex: dynamic(() => import("./plex/component")),
|
||||
portainer: dynamic(() => import("./portainer/component")),
|
||||
prometheus: dynamic(() => import("./prometheus/component")),
|
||||
prometheusmetric: dynamic(() => import("./prometheusmetric/component")),
|
||||
prowlarr: dynamic(() => import("./prowlarr/component")),
|
||||
proxmox: dynamic(() => import("./proxmox/component")),
|
||||
pterodactyl: dynamic(() => import("./pterodactyl/component")),
|
||||
@@ -102,12 +112,16 @@ const components = {
|
||||
scrutiny: dynamic(() => import("./scrutiny/component")),
|
||||
sonarr: dynamic(() => import("./sonarr/component")),
|
||||
speedtest: dynamic(() => import("./speedtest/component")),
|
||||
spoolman: dynamic(() => import("./spoolman/component")),
|
||||
stash: dynamic(() => import("./stash/component")),
|
||||
stocks: dynamic(() => import("./stocks/component")),
|
||||
strelaysrv: dynamic(() => import("./strelaysrv/component")),
|
||||
swagdashboard: dynamic(() => import("./swagdashboard/component")),
|
||||
suwayomi: dynamic(() => import("./suwayomi/component")),
|
||||
tailscale: dynamic(() => import("./tailscale/component")),
|
||||
tandoor: dynamic(() => import("./tandoor/component")),
|
||||
tautulli: dynamic(() => import("./tautulli/component")),
|
||||
technitium: dynamic(() => import("./technitium/component")),
|
||||
tdarr: dynamic(() => import("./tdarr/component")),
|
||||
traefik: dynamic(() => import("./traefik/component")),
|
||||
transmission: dynamic(() => import("./transmission/component")),
|
||||
@@ -118,10 +132,12 @@ const components = {
|
||||
uptimekuma: dynamic(() => import("./uptimekuma/component")),
|
||||
uptimerobot: dynamic(() => import("./uptimerobot/component")),
|
||||
urbackup: dynamic(() => import("./urbackup/component")),
|
||||
vikunja: dynamic(() => import("./vikunja/component")),
|
||||
watchtower: dynamic(() => import("./watchtower/component")),
|
||||
wgeasy: dynamic(() => import("./wgeasy/component")),
|
||||
whatsupdocker: dynamic(() => import("./whatsupdocker/component")),
|
||||
xteve: dynamic(() => import("./xteve/component")),
|
||||
zabbix: dynamic(() => import("./zabbix/component")),
|
||||
};
|
||||
|
||||
export default components;
|
||||
@@ -9,6 +9,11 @@ function getValue(field, data) {
|
||||
let lastField = field;
|
||||
let key = "";
|
||||
|
||||
// Support APIs that return arrays or scalars directly.
|
||||
if (typeof field === "undefined") {
|
||||
return value;
|
||||
}
|
||||
|
||||
while (typeof lastField === "object") {
|
||||
key = Object.keys(lastField)[0] ?? null;
|
||||
|
||||
@@ -27,6 +32,16 @@ function getValue(field, data) {
|
||||
return value[lastField] ?? null;
|
||||
}
|
||||
|
||||
function getSize(data) {
|
||||
if (Array.isArray(data) || typeof data === "string") {
|
||||
return data.length;
|
||||
} else if (typeof data === "object" && data !== null) {
|
||||
return Object.keys(data).length;
|
||||
}
|
||||
|
||||
return NaN;
|
||||
}
|
||||
|
||||
function formatValue(t, mapping, rawValue) {
|
||||
let value = rawValue;
|
||||
|
||||
@@ -85,6 +100,9 @@ function formatValue(t, mapping, rawValue) {
|
||||
numeric: mapping?.numeric,
|
||||
});
|
||||
break;
|
||||
case "size":
|
||||
value = t("common.number", { value: getSize(value) });
|
||||
break;
|
||||
case "text":
|
||||
default:
|
||||
// nothing
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
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 default function Component({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { widget } = service;
|
||||
|
||||
const { data: downloadStatsData, error: downloadStatsError } = useWidgetAPI(widget, "stats");
|
||||
|
||||
if (downloadStatsError) {
|
||||
return <Container service={service} error={downloadStatsError} />;
|
||||
}
|
||||
|
||||
if (!downloadStatsData) {
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="develancacheui.cachehitbytes" />
|
||||
<Block label="develancacheui.cachemissbytes" />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block
|
||||
label="develancacheui.cachehitbytes"
|
||||
value={t("common.bytes", { value: downloadStatsData.totalCacheHitBytes })}
|
||||
/>
|
||||
<Block
|
||||
label="develancacheui.cachemissbytes"
|
||||
value={t("common.bytes", { value: downloadStatsData.totalCacheMissBytes })}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import genericProxyHandler from "utils/proxy/handlers/generic";
|
||||
|
||||
const widget = {
|
||||
api: "{url}/{endpoint}",
|
||||
proxyHandler: genericProxyHandler,
|
||||
|
||||
mappings: {
|
||||
stats: {
|
||||
endpoint: "DownloadStats/GetTotalDownloadStats",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default widget;
|
||||
@@ -1,8 +1,8 @@
|
||||
import genericProxyHandler from "utils/proxy/handlers/generic";
|
||||
import credentialedProxyHandler from "utils/proxy/handlers/credentialed";
|
||||
|
||||
const widget = {
|
||||
api: "{url}/ping",
|
||||
proxyHandler: genericProxyHandler,
|
||||
proxyHandler: credentialedProxyHandler,
|
||||
};
|
||||
|
||||
export default widget;
|
||||
@@ -0,0 +1,70 @@
|
||||
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 default function Component({ service }) {
|
||||
const { t } = useTranslation();
|
||||
const { widget } = service;
|
||||
|
||||
const { data, error } = useWidgetAPI(widget, "stats");
|
||||
const { data: eventsData, error: eventsError } = useWidgetAPI(widget, "events");
|
||||
|
||||
if (error) {
|
||||
return <Container service={service} error={error} />;
|
||||
}
|
||||
|
||||
if (eventsError) {
|
||||
return <Container service={service} error={eventsError} />;
|
||||
}
|
||||
|
||||
if (!data || !eventsData) {
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="frigate.cameras" />
|
||||
<Block label="frigate.uptime" />
|
||||
<Block label="frigate.version" />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container service={service}>
|
||||
<Block
|
||||
label="frigate.cameras"
|
||||
value={t("common.number", {
|
||||
value: data.num_cameras,
|
||||
})}
|
||||
/>
|
||||
<Block
|
||||
label="frigate.uptime"
|
||||
value={t("common.duration", {
|
||||
value: data.uptime,
|
||||
})}
|
||||
/>
|
||||
<Block label="frigate.version" value={data.version} />
|
||||
</Container>
|
||||
{widget.enableRecentEvents &&
|
||||
eventsData?.map((event) => (
|
||||
<div
|
||||
key={event.id}
|
||||
className="text-theme-700 dark:text-theme-200 _relative h-5 rounded-md bg-theme-200/50 dark:bg-theme-900/20 m-1 px-1 flex"
|
||||
>
|
||||
<div className="text-xs z-10 self-center ml-2 relative h-4 grow mr-2">
|
||||
<div className="absolute w-full h-4 whitespace-nowrap text-ellipsis overflow-hidden text-left">
|
||||
{event.camera} ({event.label} {t("common.percent", { value: event.score * 100 })})
|
||||
</div>
|
||||
</div>
|
||||
<div className="self-center text-xs flex justify-end mr-1.5 pl-1 z-10 text-ellipsis overflow-hidden whitespace-nowrap">
|
||||
{t("common.date", {
|
||||
value: event.start_time,
|
||||
formatParams: { value: { timeStyle: "short", dateStyle: "medium" } },
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { asJson } from "utils/proxy/api-helpers";
|
||||
import genericProxyHandler from "utils/proxy/handlers/generic";
|
||||
|
||||
const widget = {
|
||||
api: "{url}/api/{endpoint}",
|
||||
proxyHandler: genericProxyHandler,
|
||||
|
||||
mappings: {
|
||||
stats: {
|
||||
endpoint: "stats",
|
||||
map: (data) => {
|
||||
const jsonData = asJson(data);
|
||||
return {
|
||||
num_cameras: jsonData?.cameras !== undefined ? Object.keys(jsonData?.cameras).length : 0,
|
||||
uptime: jsonData?.service?.uptime,
|
||||
version: jsonData?.service.version,
|
||||
};
|
||||
},
|
||||
},
|
||||
events: {
|
||||
endpoint: "events",
|
||||
map: (data) =>
|
||||
asJson(data)
|
||||
.slice(0, 5)
|
||||
.map((event) => ({
|
||||
id: event.id,
|
||||
camera: event.camera,
|
||||
label: event.label,
|
||||
start_time: new Date(event.start_time * 1000),
|
||||
thumbnail: event.thumbnail,
|
||||
score: event.data.score,
|
||||
type: event.data.type,
|
||||
})),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default widget;
|
||||
@@ -44,7 +44,7 @@ export default function Component({ service }) {
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="fritzbox.connectionStatus" value={t(`fritzbox.connectionStatus${fritzboxData.connectionStatus}`)} />
|
||||
<Block label="fritzbox.uptime" value={t("common.uptime", { value: fritzboxData.uptime })} />
|
||||
<Block label="fritzbox.uptime" value={t("common.duration", { value: 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 })} />
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { GameDig } from "gamedig";
|
||||
|
||||
import createLogger from "utils/logger";
|
||||
import getServiceWidget from "utils/config/service-helpers";
|
||||
|
||||
const proxyName = "gamedigProxyHandler";
|
||||
const logger = createLogger(proxyName);
|
||||
const gamedig = require("gamedig");
|
||||
|
||||
export default async function gamedigProxyHandler(req, res) {
|
||||
const { group, service } = req.query;
|
||||
@@ -11,11 +12,12 @@ export default async function gamedigProxyHandler(req, res) {
|
||||
const url = new URL(serviceWidget.url);
|
||||
|
||||
try {
|
||||
const serverData = await gamedig.query({
|
||||
const serverData = await GameDig.query({
|
||||
type: serviceWidget.serverType,
|
||||
host: url.hostname,
|
||||
port: url.port,
|
||||
givenPortOnly: true,
|
||||
checkOldIDs: true,
|
||||
});
|
||||
|
||||
res.status(200).send({
|
||||
|
||||
@@ -6,7 +6,9 @@ import useWidgetAPI from "utils/proxy/use-widget-api";
|
||||
|
||||
function getPerformancePercent(t, performanceRange) {
|
||||
// ghostfolio v2.79.0 changed to grossPerformancePercentage
|
||||
// ghostfolio v2.106.0 changed to netPerformancePercentageWithCurrencyEffect
|
||||
const percent =
|
||||
performanceRange.performance.netPerformancePercentageWithCurrencyEffect ??
|
||||
performanceRange.performance.grossPerformancePercentage ??
|
||||
performanceRange.performance.currentGrossPerformancePercent;
|
||||
return `${percent > 0 ? "+" : ""}${t("common.percent", {
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
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 default function Component({ service }) {
|
||||
const { t } = useTranslation();
|
||||
const { widget } = service;
|
||||
|
||||
const { data: gitlabCounts, error: gitlabCountsError } = useWidgetAPI(widget, "counts");
|
||||
|
||||
if (gitlabCountsError) {
|
||||
return <Container service={service} error={gitlabCountsError} />;
|
||||
}
|
||||
|
||||
if (!gitlabCounts) {
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="gitlab.groups" />
|
||||
<Block label="gitlab.issues" />
|
||||
<Block label="gitlab.merges" />
|
||||
<Block label="gitlab.projects" />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="gitlab.groups" value={t("common.number", { value: gitlabCounts.groups_count })} />
|
||||
<Block label="gitlab.issues" value={t("common.number", { value: gitlabCounts.issues_count })} />
|
||||
<Block label="gitlab.merges" value={t("common.number", { value: gitlabCounts.merge_requests_count })} />
|
||||
<Block label="gitlab.projects" value={t("common.number", { value: gitlabCounts.projects_count })} />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import credentialedProxyHandler from "utils/proxy/handlers/credentialed";
|
||||
|
||||
const widget = {
|
||||
api: "{url}/api/v4/{endpoint}",
|
||||
proxyHandler: credentialedProxyHandler,
|
||||
mappings: {
|
||||
counts: {
|
||||
endpoint: "users/{user_id}/associations_count",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default widget;
|
||||
@@ -58,7 +58,7 @@ export default function Component({ service }) {
|
||||
<div className="text-xs opacity-50">
|
||||
{t(diskUnits, {
|
||||
value: fsData.used,
|
||||
maximumFractionDigits: 0,
|
||||
maximumFractionDigits: 1,
|
||||
})}{" "}
|
||||
{t("resources.used")}
|
||||
</div>
|
||||
@@ -79,7 +79,7 @@ export default function Component({ service }) {
|
||||
<div className="text-xs opacity-50">
|
||||
{t(diskUnits, {
|
||||
value: fsData.used,
|
||||
maximumFractionDigits: 0,
|
||||
maximumFractionDigits: 1,
|
||||
})}{" "}
|
||||
{t("resources.used")}
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import genericProxyHandler from "utils/proxy/handlers/generic";
|
||||
import credentialedProxyHandler from "utils/proxy/handlers/credentialed";
|
||||
|
||||
const widget = {
|
||||
api: "{url}/v1/{endpoint}",
|
||||
proxyHandler: genericProxyHandler,
|
||||
proxyHandler: credentialedProxyHandler,
|
||||
|
||||
mappings: {
|
||||
ip: {
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
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 default function Component({ service }) {
|
||||
const { t } = useTranslation();
|
||||
const { widget } = service;
|
||||
|
||||
const { data: nodeData, error: nodeError } = useWidgetAPI(widget, "node");
|
||||
|
||||
if (nodeError) {
|
||||
return <Container service={service} error={nodeError} />;
|
||||
}
|
||||
|
||||
if (!nodeData) {
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="headscale.name" />
|
||||
<Block label="headscale.address" />
|
||||
<Block label="headscale.last_seen" />
|
||||
<Block label="headscale.status" />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
const {
|
||||
givenName,
|
||||
ipAddresses: [address],
|
||||
lastSeen,
|
||||
online,
|
||||
} = nodeData.node;
|
||||
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="headscale.name" value={givenName} />
|
||||
<Block label="headscale.address" value={address} />
|
||||
<Block label="headscale.last_seen" value={t("common.relativeDate", { value: lastSeen })} />
|
||||
<Block label="headscale.status" value={t(online ? "headscale.online" : "headscale.offline")} />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import credentialedProxyHandler from "utils/proxy/handlers/credentialed";
|
||||
|
||||
const widget = {
|
||||
api: "{url}/api/v1/{endpoint}/{nodeId}",
|
||||
proxyHandler: credentialedProxyHandler,
|
||||
|
||||
mappings: {
|
||||
node: {
|
||||
endpoint: "node",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default widget;
|
||||
@@ -8,10 +8,19 @@ export default function Component({ service }) {
|
||||
const { t } = useTranslation();
|
||||
const { widget } = service;
|
||||
|
||||
const { data: versionData, error: versionError } = useWidgetAPI(widget, "version");
|
||||
// see https://github.com/gethomepage/homepage/issues/2282
|
||||
const endpoint = versionData?.major >= 1 && versionData?.minor > 84 ? "statistics" : "stats";
|
||||
const { data: immichData, error: immichError } = useWidgetAPI(widget, endpoint);
|
||||
const { version = 1 } = widget;
|
||||
|
||||
const versionEndpoint = version === 2 ? "version_v2" : "version";
|
||||
|
||||
const { data: versionData, error: versionError } = useWidgetAPI(widget, versionEndpoint);
|
||||
|
||||
let statsEndpoint = version === 2 ? "statistics_v2" : "stats";
|
||||
if (version === 1) {
|
||||
// see https://github.com/gethomepage/homepage/issues/2282
|
||||
statsEndpoint =
|
||||
versionData?.major > 1 || (versionData?.major === 1 && versionData?.minor > 84) ? "statistics" : "stats";
|
||||
}
|
||||
const { data: immichData, error: immichError } = useWidgetAPI(widget, statsEndpoint);
|
||||
|
||||
if (immichError || versionError || immichData?.statusCode === 401) {
|
||||
return <Container service={service} error={immichData ?? immichError ?? versionError} />;
|
||||
|
||||
@@ -1,18 +1,24 @@
|
||||
import credentialedProxyHandler from "utils/proxy/handlers/credentialed";
|
||||
|
||||
const widget = {
|
||||
api: "{url}/api/server-info/{endpoint}",
|
||||
api: "{url}/api/{endpoint}",
|
||||
proxyHandler: credentialedProxyHandler,
|
||||
|
||||
mappings: {
|
||||
version: {
|
||||
endpoint: "version",
|
||||
endpoint: "server-info/version",
|
||||
},
|
||||
statistics: {
|
||||
endpoint: "statistics",
|
||||
endpoint: "server-info/statistics",
|
||||
},
|
||||
stats: {
|
||||
endpoint: "stats",
|
||||
endpoint: "server-info/stats",
|
||||
},
|
||||
version_v2: {
|
||||
endpoint: "server/version",
|
||||
},
|
||||
statistics_v2: {
|
||||
endpoint: "server/statistics",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
|
||||
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 { widget } = service;
|
||||
|
||||
const [stats, setStats] = useState({
|
||||
totalLinks: null,
|
||||
collections: { total: null },
|
||||
tags: { total: null },
|
||||
});
|
||||
|
||||
const { data: collectionsStatsData, error: collectionsStatsError } = useWidgetAPI(widget, "collections");
|
||||
const { data: tagsStatsData, error: tagsStatsError } = useWidgetAPI(widget, "tags");
|
||||
|
||||
useEffect(() => {
|
||||
if (collectionsStatsData?.response && tagsStatsData?.response) {
|
||||
setStats({
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
totalLinks: collectionsStatsData.response.reduce((sum, collection) => sum + (collection._count?.links || 0), 0),
|
||||
collections: {
|
||||
total: collectionsStatsData.response.length,
|
||||
},
|
||||
tags: {
|
||||
total: tagsStatsData.response.length,
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [collectionsStatsData, tagsStatsData]);
|
||||
|
||||
if (collectionsStatsError || tagsStatsError) {
|
||||
return <Container service={service} error={collectionsStatsError || tagsStatsError} />;
|
||||
}
|
||||
|
||||
if (!tagsStatsData || !collectionsStatsData) {
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="linkwarden.links" />
|
||||
<Block label="linkwarden.collections" />
|
||||
<Block label="linkwarden.tags" />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="linkwarden.links" value={stats.totalLinks} />
|
||||
<Block label="linkwarden.collections" value={stats.collections.total} />
|
||||
<Block label="linkwarden.tags" value={stats.tags.total} />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import credentialedProxyHandler from "utils/proxy/handlers/credentialed";
|
||||
|
||||
const widget = {
|
||||
api: "{url}/api/v1/{endpoint}",
|
||||
proxyHandler: credentialedProxyHandler,
|
||||
|
||||
mappings: {
|
||||
collections: {
|
||||
endpoint: "collections",
|
||||
},
|
||||
tags: {
|
||||
endpoint: "tags",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default widget;
|
||||
@@ -0,0 +1,65 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
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;
|
||||
let { data: vehicleInfo, error } = useWidgetAPI(widget, "vehicleinfo");
|
||||
|
||||
if (error) {
|
||||
return <Container service={service} error={error} />;
|
||||
}
|
||||
|
||||
if (!vehicleInfo) {
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="lubelogger.vehicles" />
|
||||
<Block label="lubelogger.serviceRecords" />
|
||||
<Block label="lubelogger.reminders" />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
const { vehicleID } = widget;
|
||||
if (vehicleID) {
|
||||
vehicleInfo = vehicleInfo.filter((v) => v.vehicleData.id === vehicleID);
|
||||
}
|
||||
const totalReminders = vehicleInfo.reduce(
|
||||
(acc, val) => acc + val.veryUrgentReminderCount + val.urgentReminderCount + val.notUrgentReminderCount,
|
||||
0,
|
||||
);
|
||||
const totalServiceRecords = vehicleInfo.reduce((acc, val) => acc + val.serviceRecordCount, 0);
|
||||
|
||||
if (vehicleID) {
|
||||
if (vehicleInfo.length === 0) {
|
||||
error = { message: "Vehicle not found" };
|
||||
return <Container service={service} error={error} />;
|
||||
}
|
||||
|
||||
const nextReminder = vehicleInfo[0].nextReminder
|
||||
? t("common.date", { value: vehicleInfo[0].nextReminder.dueDate })
|
||||
: t("lubelogger.none");
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block
|
||||
label="lubelogger.vehicle"
|
||||
value={`${vehicleInfo[0].vehicleData.year} ${vehicleInfo[0].vehicleData.model}`}
|
||||
/>
|
||||
<Block label="lubelogger.serviceRecords" value={totalServiceRecords} />
|
||||
<Block label="lubelogger.reminders" value={totalReminders} />
|
||||
<Block label="lubelogger.nextReminder" value={nextReminder} />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="lubelogger.vehicles" value={vehicleInfo.length} />
|
||||
<Block label="lubelogger.serviceRecords" value={totalServiceRecords} />
|
||||
<Block label="lubelogger.reminders" value={totalReminders} />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import genericProxyHandler from "utils/proxy/handlers/generic";
|
||||
|
||||
const widget = {
|
||||
api: "{url}/api/{endpoint}",
|
||||
proxyHandler: genericProxyHandler,
|
||||
|
||||
mappings: {
|
||||
vehicleinfo: {
|
||||
endpoint: "vehicle/info",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default widget;
|
||||
@@ -0,0 +1,40 @@
|
||||
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 default function Component({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { widget } = service;
|
||||
const { data: resultData, error: resultError } = useWidgetAPI(widget, "domains");
|
||||
|
||||
if (resultError || (resultData && Object.keys(resultData).length === 0)) {
|
||||
return <Container service={service} error={resultError ?? { message: "No domains found" }} />;
|
||||
}
|
||||
|
||||
if (!resultData) {
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="mailcow.mailboxes" />
|
||||
<Block label="mailcow.aliases" />
|
||||
<Block label="mailcow.quarantined" />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
const domains = resultData.length;
|
||||
const mailboxes = resultData.reduce((acc, val) => acc + parseInt(val.mboxes_in_domain, 10), 0);
|
||||
const mails = resultData.reduce((acc, val) => acc + parseInt(val.msgs_total, 10), 0);
|
||||
const storage = resultData.reduce((acc, val) => acc + parseInt(val.bytes_total, 10), 0);
|
||||
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="mailcow.domains" value={t("common.number", { value: domains })} />
|
||||
<Block label="mailcow.mailboxes" value={t("common.number", { value: mailboxes })} />
|
||||
<Block label="mailcow.mails" value={t("common.number", { value: mails })} />
|
||||
<Block label="mailcow.storage" value={t("common.bytes", { value: storage })} />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import credentialedProxyHandler from "../../utils/proxy/handlers/credentialed";
|
||||
|
||||
const widget = {
|
||||
api: "{url}/api/v1/get/{endpoint}",
|
||||
proxyHandler: credentialedProxyHandler,
|
||||
|
||||
mappings: {
|
||||
domains: {
|
||||
endpoint: "domain/all",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default widget;
|
||||
@@ -1,17 +1,20 @@
|
||||
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 default function Component({ service }) {
|
||||
const { t } = useTranslation();
|
||||
const { widget } = service;
|
||||
const version = widget.version ?? 1;
|
||||
const { data, error } = useWidgetAPI(widget, version === 1 ? "statisticsv1" : "statisticsv2");
|
||||
|
||||
const { data: mealieData, error: mealieError } = useWidgetAPI(widget);
|
||||
|
||||
if (mealieError || mealieData?.statusCode === 401) {
|
||||
return <Container service={service} error={mealieError ?? mealieData} />;
|
||||
if (error) {
|
||||
return <Container service={service} error={error} />;
|
||||
}
|
||||
|
||||
if (!mealieData) {
|
||||
if (!data) {
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="mealie.recipes" />
|
||||
@@ -21,13 +24,12 @@ export default function Component({ service }) {
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="mealie.recipes" value={mealieData.totalRecipes} />
|
||||
<Block label="mealie.users" value={mealieData.totalUsers} />
|
||||
<Block label="mealie.categories" value={mealieData.totalCategories} />
|
||||
<Block label="mealie.tags" value={mealieData.totalTags} />
|
||||
<Block label="mealie.recipes" value={t("common.number", { value: data.totalRecipes })} />
|
||||
<Block label="mealie.users" value={t("common.number", { value: data.totalUsers })} />
|
||||
<Block label="mealie.categories" value={t("common.number", { value: data.totalCategories })} />
|
||||
<Block label="mealie.tags" value={t("common.number", { value: data.totalTags })} />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,17 @@
|
||||
import credentialedProxyHandler from "utils/proxy/handlers/credentialed";
|
||||
|
||||
const widget = {
|
||||
api: "{url}/api/groups/statistics",
|
||||
api: "{url}/api/{endpoint}",
|
||||
proxyHandler: credentialedProxyHandler,
|
||||
|
||||
mappings: {
|
||||
statisticsv1: {
|
||||
endpoint: "groups/statistics",
|
||||
},
|
||||
statisticsv2: {
|
||||
endpoint: "households/statistics",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default widget;
|
||||
@@ -10,7 +10,7 @@ const widget = {
|
||||
validate: ["cpu-load", "free-memory", "total-memory", "uptime"],
|
||||
},
|
||||
leases: {
|
||||
endpoint: "ip/dhcp-server/lease",
|
||||
endpoint: "ip/dhcp-server/lease?.proplist=address",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import genericProxyHandler from "utils/proxy/handlers/generic";
|
||||
import credentialedProxyHandler from "utils/proxy/handlers/credentialed";
|
||||
|
||||
const widget = {
|
||||
api: "{url}/php/server/devices.php?action=getDevicesTotals",
|
||||
proxyHandler: genericProxyHandler,
|
||||
proxyHandler: credentialedProxyHandler,
|
||||
|
||||
mappings: {
|
||||
data: {
|
||||
|
||||
@@ -21,8 +21,8 @@ export default function Component({ service }) {
|
||||
);
|
||||
}
|
||||
|
||||
const enabled = infoData.filter((c) => c.enabled === 1).length;
|
||||
const disabled = infoData.filter((c) => c.enabled === 0).length;
|
||||
const enabled = infoData.filter((c) => !!c.enabled).length;
|
||||
const disabled = infoData.filter((c) => !c.enabled).length;
|
||||
const total = infoData.length;
|
||||
|
||||
return (
|
||||
|
||||
@@ -30,7 +30,7 @@ async function login(loginUrl, username, password, service) {
|
||||
cache.put(`${tokenCacheKey}.${service}`, data.token, expiration - 5 * 60 * 1000); // expiration -5 minutes
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error(`Error ${status} logging into npm`, authResponse[2]);
|
||||
logger.error(`Error ${status} logging into npm`, JSON.stringify(authResponse[2]));
|
||||
}
|
||||
return [status, data.token ?? data];
|
||||
}
|
||||
@@ -50,19 +50,18 @@ export default async function npmProxyHandler(req, res) {
|
||||
const loginUrl = `${widget.url}/api/tokens`;
|
||||
|
||||
let status;
|
||||
let contentType;
|
||||
let data;
|
||||
|
||||
let token = cache.get(`${tokenCacheKey}.${service}`);
|
||||
if (!token) {
|
||||
[status, token] = await login(loginUrl, widget.username, widget.password, service);
|
||||
if (status !== 200) {
|
||||
logger.debug(`HTTTP ${status} logging into npm api: ${token}`);
|
||||
logger.debug(`HTTP ${status} logging into npm api: ${token}`);
|
||||
return res.status(status).send(token);
|
||||
}
|
||||
}
|
||||
|
||||
[status, contentType, data] = await httpProxy(url, {
|
||||
[status, , data] = await httpProxy(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -71,17 +70,17 @@ export default async function npmProxyHandler(req, res) {
|
||||
});
|
||||
|
||||
if (status === 403) {
|
||||
logger.debug(`HTTTP ${status} retrieving data from npm api, logging in and trying again.`);
|
||||
logger.debug(`HTTP ${status} retrieving data from npm api, logging in and trying again.`);
|
||||
cache.del(`${tokenCacheKey}.${service}`);
|
||||
[status, token] = await login(loginUrl, widget.username, widget.password, service);
|
||||
|
||||
if (status !== 200) {
|
||||
logger.debug(`HTTTP ${status} logging into npm api: ${data}`);
|
||||
logger.debug(`HTTP ${status} logging into npm api: ${data}`);
|
||||
return res.status(status).send(data);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
[status, contentType, data] = await httpProxy(url, {
|
||||
[status, , data] = await httpProxy(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
|
||||
@@ -4,6 +4,12 @@ const widget = {
|
||||
api: "{url}/jsonrpc",
|
||||
proxyHandler: jsonrpcProxyHandler,
|
||||
allowedEndpoints: /status/,
|
||||
|
||||
mappings: {
|
||||
status: {
|
||||
endpoint: "status",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default widget;
|
||||
@@ -138,7 +138,7 @@ export default async function omadaProxyHandler(req, res) {
|
||||
const sitesResponseData = JSON.parse(data);
|
||||
|
||||
if (status !== 200 || sitesResponseData.errorCode > 0) {
|
||||
logger.debug(`HTTTP ${status} getting sites list: ${sitesResponseData.msg}`);
|
||||
logger.debug(`HTTP ${status} getting sites list: ${sitesResponseData.msg}`);
|
||||
return res
|
||||
.status(status)
|
||||
.json({ error: { message: "Error getting sites list", url, data: sitesResponseData } });
|
||||
|
||||
@@ -20,7 +20,7 @@ export default function Component({ service }) {
|
||||
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="openwrt.uptime" value={t("common.uptime", { value: uptime })} />
|
||||
<Block label="openwrt.uptime" value={t("common.duration", { value: uptime })} />
|
||||
<Block label="openwrt.cpuLoad" value={cpuLoad} />
|
||||
</Container>
|
||||
);
|
||||
|
||||
@@ -9,8 +9,12 @@ export default function Component({ service }) {
|
||||
|
||||
const { widget } = service;
|
||||
|
||||
const { data: systemData, error: systemError } = useWidgetAPI(widget, "system");
|
||||
const { data: interfaceData, error: interfaceError } = useWidgetAPI(widget, "interface");
|
||||
const version = widget.version ?? 1;
|
||||
const { data: systemData, error: systemError } = useWidgetAPI(widget, version === 1 ? "system" : "systemv2");
|
||||
const { data: interfaceData, error: interfaceError } = useWidgetAPI(
|
||||
widget,
|
||||
version === 1 ? "interface" : "interfacev2",
|
||||
);
|
||||
|
||||
const showWanIP = widget.fields?.filter((f) => f !== "wanIP").length <= 4 && widget.fields?.includes("wanIP");
|
||||
const showDiskUsage = widget.fields?.filter((f) => f !== "disk").length <= 4 && widget.fields?.includes("disk");
|
||||
@@ -34,14 +38,20 @@ export default function Component({ service }) {
|
||||
}
|
||||
|
||||
const wan = interfaceData.data.filter((l) => l.hwif === widget.wan)[0];
|
||||
let memUsage = systemData?.data.mem_usage;
|
||||
let diskUsage = systemData.data.disk_usage;
|
||||
if (version === 1) {
|
||||
memUsage *= 100;
|
||||
diskUsage *= 100;
|
||||
}
|
||||
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="pfsense.load" value={systemData.data.load_avg[0]} />
|
||||
<Block
|
||||
label="pfsense.memory"
|
||||
value={t("common.percent", { value: (systemData.data.mem_usage * 100).toFixed(2) })}
|
||||
label="pfsense.load"
|
||||
value={version === 1 ? systemData.data.load_avg[0] : systemData.data.cpu_load_avg[0]}
|
||||
/>
|
||||
<Block label="pfsense.memory" value={t("common.percent", { value: memUsage.toFixed(2) })} />
|
||||
<Block
|
||||
label="pfsense.temp"
|
||||
value={t("common.number", { value: systemData.data.temp_c, style: "unit", unit: "celsius" })}
|
||||
@@ -57,12 +67,7 @@ export default function Component({ service }) {
|
||||
}
|
||||
/>
|
||||
{showWanIP && <Block label="pfsense.wanIP" value={wan.ipaddr} />}
|
||||
{showDiskUsage && (
|
||||
<Block
|
||||
label="pfsense.disk"
|
||||
value={t("common.percent", { value: (systemData.data.disk_usage * 100).toFixed(2) })}
|
||||
/>
|
||||
)}
|
||||
{showDiskUsage && <Block label="pfsense.disk" value={t("common.percent", { value: diskUsage.toFixed(2) })} />}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -1,16 +1,24 @@
|
||||
import genericProxyHandler from "utils/proxy/handlers/generic";
|
||||
|
||||
const widget = {
|
||||
api: "{url}/api/v1/{endpoint}",
|
||||
api: "{url}/api/{endpoint}",
|
||||
proxyHandler: genericProxyHandler,
|
||||
|
||||
mappings: {
|
||||
system: {
|
||||
endpoint: "status/system",
|
||||
endpoint: "v1/status/system",
|
||||
validate: ["data"],
|
||||
},
|
||||
interface: {
|
||||
endpoint: "status/interface",
|
||||
endpoint: "v1/status/interface",
|
||||
validate: ["data"],
|
||||
},
|
||||
systemv2: {
|
||||
endpoint: "v2/status/system",
|
||||
validate: ["data"],
|
||||
},
|
||||
interfacev2: {
|
||||
endpoint: "v2/status/interfaces?limit=0&offset=0",
|
||||
validate: ["data"],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -32,6 +32,11 @@ export default async function photoprismProxyHandler(req, res) {
|
||||
username: widget.username,
|
||||
password: widget.password,
|
||||
});
|
||||
} else if (widget.key) {
|
||||
params.headers.Authorization = `Bearer ${widget.key}`;
|
||||
params.body = JSON.stringify({
|
||||
authToken: widget.key,
|
||||
});
|
||||
}
|
||||
|
||||
const [status, contentType, data] = await httpProxy(url, params);
|
||||
|
||||
@@ -57,23 +57,27 @@ export default async function piholeProxyHandler(req, res) {
|
||||
}
|
||||
|
||||
// pihole v6
|
||||
if (!cache.get(`${sessionSIDCacheKey}.${service}`)) {
|
||||
if (!cache.get(`${sessionSIDCacheKey}.${service}`) && widget.key) {
|
||||
await login(widget, service);
|
||||
}
|
||||
|
||||
const sid = cache.get(`${sessionSIDCacheKey}.${service}`);
|
||||
if (!sid) {
|
||||
if (widget.key && !sid) {
|
||||
return res.status(500).json({ error: "Failed to authenticate with Pi-hole" });
|
||||
}
|
||||
|
||||
try {
|
||||
logger.debug("Calling Pi-hole API endpoint: %s", endpoint);
|
||||
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
if (sid) {
|
||||
headers["X-FTL-SID"] = sid;
|
||||
} else {
|
||||
logger.debug("Pi-hole request is unauthenticated");
|
||||
}
|
||||
[status, , data] = await httpProxy(formatApiCall(widgets[widget.type].api, { ...widget, endpoint }), {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-FTL-SID": sid,
|
||||
},
|
||||
headers,
|
||||
});
|
||||
|
||||
if (status !== 200) {
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
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";
|
||||
|
||||
function formatValue(t, metric, rawValue) {
|
||||
if (!metric?.format) return rawValue;
|
||||
if (!rawValue) return "-";
|
||||
|
||||
let value = rawValue;
|
||||
|
||||
// Scale the value. Accepts either a number to multiply by or a string
|
||||
// like "12/345".
|
||||
const scale = metric?.format?.scale;
|
||||
if (typeof scale === "number") {
|
||||
value *= scale;
|
||||
} else if (typeof scale === "string" && scale.includes("/")) {
|
||||
const parts = scale.split("/");
|
||||
const numerator = parts[0] ? parseFloat(parts[0]) : 1;
|
||||
const denominator = parts[1] ? parseFloat(parts[1]) : 1;
|
||||
value = (value * numerator) / denominator;
|
||||
} else {
|
||||
value = parseFloat(value);
|
||||
}
|
||||
|
||||
// Format the value using a known type and optional options.
|
||||
switch (metric?.format?.type) {
|
||||
case "text":
|
||||
break;
|
||||
default:
|
||||
value = t(`common.${metric.format.type}`, { value, ...metric.format?.options });
|
||||
}
|
||||
|
||||
// Apply fixed prefix.
|
||||
const prefix = metric?.format?.prefix;
|
||||
if (prefix) {
|
||||
value = `${prefix}${value}`;
|
||||
}
|
||||
|
||||
// Apply fixed suffix.
|
||||
const suffix = metric?.format?.suffix;
|
||||
if (suffix) {
|
||||
value = `${value}${suffix}`;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
export default function Component({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { widget } = service;
|
||||
|
||||
const { metrics = [], refreshInterval = 10000 } = widget;
|
||||
|
||||
let prometheusmetricError;
|
||||
|
||||
const prometheusmetricData = new Map(
|
||||
metrics.slice(0, 4).map((metric) => {
|
||||
// disable the rule that hooks should not be called from a callback,
|
||||
// because we don't need a strong guarantee of hook execution order here.
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const { data: resultData, error: resultError } = useWidgetAPI(widget, "query", {
|
||||
query: metric.query,
|
||||
refreshInterval: Math.max(1000, metric.refreshInterval ?? refreshInterval),
|
||||
});
|
||||
if (resultError) {
|
||||
prometheusmetricError = resultError;
|
||||
}
|
||||
return [metric.key ?? metric.label, resultData];
|
||||
}),
|
||||
);
|
||||
|
||||
if (prometheusmetricError) {
|
||||
return <Container service={service} error={prometheusmetricError} />;
|
||||
}
|
||||
|
||||
if (!prometheusmetricData) {
|
||||
return (
|
||||
<Container service={service}>
|
||||
{metrics.slice(0, 4).map((item) => (
|
||||
<Block label={item.label} key={item.label} />
|
||||
))}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
function getResultValue(data) {
|
||||
// Fetches the first metric result from the Prometheus query result data.
|
||||
// The first element in the result value is the timestamp which is ignored here.
|
||||
const resultType = data?.data?.resultType;
|
||||
const result = data?.data?.result;
|
||||
|
||||
switch (resultType) {
|
||||
case "vector":
|
||||
return result?.[0]?.value?.[1];
|
||||
case "scalar":
|
||||
return result?.[1];
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Container service={service}>
|
||||
{metrics.map((metric) => (
|
||||
<Block
|
||||
label={metric.label}
|
||||
key={metric.key ?? metric.label}
|
||||
value={formatValue(t, metric, getResultValue(prometheusmetricData.get(metric.key ?? metric.label)))}
|
||||
/>
|
||||
))}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import genericProxyHandler from "utils/proxy/handlers/generic";
|
||||
|
||||
const widget = {
|
||||
api: "{url}/api/v1/{endpoint}",
|
||||
proxyHandler: genericProxyHandler,
|
||||
|
||||
mappings: {
|
||||
query: {
|
||||
method: "GET",
|
||||
endpoint: "query",
|
||||
params: ["query"],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default widget;
|
||||
@@ -29,7 +29,7 @@ export default function Component({ service }) {
|
||||
);
|
||||
}
|
||||
|
||||
const datastoreUsage = (datastoreData.data[0].used / datastoreData.data[0].total) * 100;
|
||||
const datastoreUsage = datastoreData.data ? (datastoreData.data[0].used / datastoreData.data[0].total) * 100 : 0;
|
||||
const cpuUsage = hostData.data.cpu * 100;
|
||||
const memoryUsage = (hostData.data.memory.used / hostData.data.memory.total) * 100;
|
||||
const failedTasks = tasksData.total >= 100 ? "99+" : tasksData.total;
|
||||
|
||||
@@ -15,7 +15,7 @@ async function fetchFromPyloadAPI(url, sessionId, params, service) {
|
||||
const options = {
|
||||
body: params
|
||||
? Object.keys(params)
|
||||
.map((prop) => `${prop}=${params[prop]}`)
|
||||
.map((prop) => `${prop}=${encodeURIComponent(params[prop])}`)
|
||||
.join("&")
|
||||
: `session=${sessionId}`,
|
||||
method: "POST",
|
||||
|
||||
@@ -12,7 +12,10 @@ const widget = {
|
||||
wanted: jsonArrayFilter(data, (item) => item.monitored && !item.hasFile && item.isAvailable).length,
|
||||
have: jsonArrayFilter(data, (item) => item.hasFile).length,
|
||||
missing: jsonArrayFilter(data, (item) => item.monitored && !item.hasFile).length,
|
||||
all: asJson(data),
|
||||
all: asJson(data).map((entry) => ({
|
||||
title: entry.title,
|
||||
id: entry.id,
|
||||
})),
|
||||
}),
|
||||
},
|
||||
"queue/status": {
|
||||
|
||||
@@ -4,31 +4,46 @@ import Container from "components/services/widget/container";
|
||||
import Block from "components/services/widget/block";
|
||||
import useWidgetAPI from "utils/proxy/use-widget-api";
|
||||
|
||||
const ROMM_DEFAULT_FIELDS = ["platforms", "totalRoms", "saves", "states"];
|
||||
const MAX_ALLOWED_FIELDS = 4;
|
||||
|
||||
export default function Component({ service }) {
|
||||
const { widget } = service;
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { data: response, error: responseError } = useWidgetAPI(widget, "statistics");
|
||||
|
||||
if (responseError) {
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="Error" value={responseError.message} />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
if (responseError) {
|
||||
return <Container service={service} error={responseError} />;
|
||||
}
|
||||
|
||||
if (response) {
|
||||
const platforms = response.filter((x) => x.rom_count !== 0).length;
|
||||
const totalRoms = response.reduce((total, stat) => total + stat.rom_count, 0);
|
||||
if (!widget.fields?.length > 0) {
|
||||
widget.fields = ROMM_DEFAULT_FIELDS;
|
||||
} else if (widget.fields.length > MAX_ALLOWED_FIELDS) {
|
||||
widget.fields = widget.fields.slice(0, MAX_ALLOWED_FIELDS);
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="romm.platforms" value={t("common.number", { value: platforms })} />
|
||||
<Block label="romm.totalRoms" value={t("common.number", { value: totalRoms })} />
|
||||
<Block label="romm.platforms" />
|
||||
<Block label="romm.totalRoms" />
|
||||
<Block label="romm.saves" />
|
||||
<Block label="romm.states" />
|
||||
<Block label="romm.screenshots" />
|
||||
<Block label="romm.totalfilesize" />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
if (response) {
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="romm.platforms" value={t("common.number", { value: response.PLATFORMS })} />
|
||||
<Block label="romm.totalRoms" value={t("common.number", { value: response.ROMS })} />
|
||||
<Block label="romm.saves" value={t("common.number", { value: response.SAVES })} />
|
||||
<Block label="romm.states" value={t("common.number", { value: response.STATES })} />
|
||||
<Block label="romm.screenshots" value={t("common.number", { value: response.SCREENSHOTS })} />
|
||||
<Block label="romm.totalfilesize" value={t("common.bytes", { value: response.FILESIZE })} />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ const widget = {
|
||||
|
||||
mappings: {
|
||||
statistics: {
|
||||
endpoint: "platforms",
|
||||
endpoint: "stats",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
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 default function Component({ service }) {
|
||||
const { t } = useTranslation();
|
||||
const { widget } = service;
|
||||
|
||||
// eslint-disable-next-line prefer-const
|
||||
let { data: spoolData, error: spoolError } = useWidgetAPI(widget, "spools");
|
||||
|
||||
if (spoolError) {
|
||||
return <Container service={service} error={spoolError} />;
|
||||
}
|
||||
|
||||
if (!spoolData) {
|
||||
const nBlocksGuess = widget.spoolIds?.length ?? 4;
|
||||
return (
|
||||
<Container service={service}>
|
||||
{[...Array(nBlocksGuess)].map((_, i) => (
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
<Block key={i} label="spoolman.loading" />
|
||||
))}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
if (spoolData.error || spoolData.message) {
|
||||
return <Container service={service} error={spoolData?.error ?? spoolData} />;
|
||||
}
|
||||
|
||||
if (spoolData.length === 0) {
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="spoolman.noSpools" />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
if (widget.spoolIds?.length) {
|
||||
spoolData = spoolData.filter((spool) => widget.spoolIds.includes(spool.id));
|
||||
}
|
||||
|
||||
if (spoolData.length > 4) {
|
||||
spoolData = spoolData.slice(0, 4);
|
||||
}
|
||||
|
||||
return (
|
||||
<Container service={service}>
|
||||
{spoolData.map((spool) => (
|
||||
<Block
|
||||
key={spool.id}
|
||||
label={spool.filament.name}
|
||||
value={t("common.percent", {
|
||||
value: (spool.remaining_weight / spool.initial_weight) * 100,
|
||||
})}
|
||||
/>
|
||||
))}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import credentialedProxyHandler from "utils/proxy/handlers/credentialed";
|
||||
|
||||
const widget = {
|
||||
api: "{url}/api/v1/{endpoint}",
|
||||
proxyHandler: credentialedProxyHandler,
|
||||
|
||||
mappings: {
|
||||
spools: {
|
||||
endpoint: "spool",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default widget;
|
||||
@@ -46,12 +46,12 @@ export default function Component({ service }) {
|
||||
<Block label="stash.scenes" value={t("common.number", { value: stats.scene_count })} />
|
||||
<Block label="stash.scenesPlayed" value={t("common.number", { value: stats.scenes_played })} />
|
||||
<Block label="stash.playCount" value={t("common.number", { value: stats.total_play_count })} />
|
||||
<Block label="stash.playDuration" value={t("common.uptime", { value: stats.total_play_duration })} />
|
||||
<Block label="stash.playDuration" value={t("common.duration", { value: stats.total_play_duration })} />
|
||||
<Block
|
||||
label="stash.sceneSize"
|
||||
value={t("common.bbytes", { value: stats.scenes_size, maximumFractionDigits: 1 })}
|
||||
/>
|
||||
<Block label="stash.sceneDuration" value={t("common.uptime", { value: stats.scenes_duration })} />
|
||||
<Block label="stash.sceneDuration" value={t("common.duration", { value: stats.scenes_duration })} />
|
||||
|
||||
<Block label="stash.images" value={t("common.number", { value: stats.image_count })} />
|
||||
<Block
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import { useTranslation } from "next-i18next";
|
||||
import classNames from "classnames";
|
||||
|
||||
import Container from "components/services/widget/container";
|
||||
import Block from "components/services/widget/block";
|
||||
import useWidgetAPI from "utils/proxy/use-widget-api";
|
||||
|
||||
function MarketStatus({ service }) {
|
||||
const { t } = useTranslation();
|
||||
const { widget } = service;
|
||||
|
||||
const { data, error } = useWidgetAPI(widget, "status", {
|
||||
exchange: "US",
|
||||
});
|
||||
|
||||
if (error || data?.error) {
|
||||
return <Container service={service} error={error} />;
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block value={t("stocks.loading")} />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
const { isOpen } = data;
|
||||
|
||||
if (isOpen) {
|
||||
return (
|
||||
<span className="inline-flex items-center rounded-md bg-green-500/10 px-2 py-1 text-xs font-medium text-green-400/90 ring-1 ring-inset ring-green-500/20">
|
||||
{t("stocks.open")}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<span className="inline-flex items-center rounded-md bg-red-400/10 px-2 py-1 text-xs font-medium text-red-400/60 ring-1 ring-inset ring-red-400/10">
|
||||
{t("stocks.closed")}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function StockItem({ service, ticker }) {
|
||||
const { t } = useTranslation();
|
||||
const { widget } = service;
|
||||
|
||||
const { data, error } = useWidgetAPI(widget, "quote", { symbol: ticker });
|
||||
|
||||
if (error || data?.error) {
|
||||
return <Container service={service} error={error} />;
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block value={t("stocks.loading")} />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-theme-200/50 dark:bg-theme-900/20 rounded flex flex-1 items-center justify-between m-1 p-1 text-xs">
|
||||
<span className="font-thin ml-2 flex-none">{ticker}</span>
|
||||
<div className="flex items-center flex-row-reverse mr-2 text-right">
|
||||
<span className={`font-bold ml-2 w-10 ${data.dp > 0 ? "text-emerald-300" : "text-rose-300"}`}>
|
||||
{data.dp?.toFixed(2) ? `${data.dp?.toFixed(2)}%` : t("widget.api_error")}
|
||||
</span>
|
||||
<span className="font-bold">
|
||||
{data.c
|
||||
? t("common.number", {
|
||||
value: data?.c,
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
})
|
||||
: t("widget.api_error")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Component({ service }) {
|
||||
const { t } = useTranslation();
|
||||
const { widget } = service;
|
||||
const { watchlist, showUSMarketStatus } = widget;
|
||||
|
||||
if (!watchlist || !watchlist.length || watchlist.length > 28 || new Set(watchlist).size !== watchlist.length) {
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block value={t("stocks.invalidConfiguration")} />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Container service={service}>
|
||||
<div className={classNames(service.description ? "-top-10" : "-top-8", "absolute right-1 z-20")}>
|
||||
{showUSMarketStatus === true && <MarketStatus service={service} />}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col w-full">
|
||||
{watchlist.map((ticker) => (
|
||||
<StockItem key={ticker} service={service} ticker={ticker} />
|
||||
))}
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import credentialedProxyHandler from "utils/proxy/handlers/credentialed";
|
||||
|
||||
const widget = {
|
||||
api: `https://finnhub.io/api/{endpoint}`,
|
||||
proxyHandler: credentialedProxyHandler,
|
||||
|
||||
mappings: {
|
||||
quote: {
|
||||
// https://finnhub.io/docs/api/quote
|
||||
endpoint: "v1/quote",
|
||||
params: ["symbol"],
|
||||
},
|
||||
status: {
|
||||
// https://finnhub.io/docs/api/market-status
|
||||
endpoint: "v1/stock/market-status",
|
||||
params: ["exchange"],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default widget;
|
||||
@@ -0,0 +1,40 @@
|
||||
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 default function Component({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { widget } = service;
|
||||
|
||||
const { data: suwayomiData, error: suwayomiError } = useWidgetAPI(widget);
|
||||
|
||||
if (suwayomiError) {
|
||||
return <Container service={service} error={suwayomiError} />;
|
||||
}
|
||||
|
||||
if (!suwayomiData) {
|
||||
if (!widget.fields || widget.fields.length === 0) {
|
||||
widget.fields = ["download", "nondownload", "read", "unread"];
|
||||
} else if (widget.fields.length > 4) {
|
||||
widget.fields = widget.fields.slice(0, 4);
|
||||
}
|
||||
return (
|
||||
<Container service={service}>
|
||||
{widget.fields.map((field) => (
|
||||
<Block key={field} label={`suwayomi.${field}`} />
|
||||
))}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Container service={service}>
|
||||
{suwayomiData.map((data) => (
|
||||
<Block key={data.label} label={data.label} value={t("common.number", { value: data.count })} />
|
||||
))}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
import { httpProxy } from "utils/proxy/http";
|
||||
import { formatApiCall } from "utils/proxy/api-helpers";
|
||||
import getServiceWidget from "utils/config/service-helpers";
|
||||
import createLogger from "utils/logger";
|
||||
import widgets from "widgets/widgets";
|
||||
|
||||
const proxyName = "suwayomiProxyHandler";
|
||||
const logger = createLogger(proxyName);
|
||||
|
||||
const countsToExtract = {
|
||||
download: {
|
||||
condition: (c) => c.isDownloaded,
|
||||
gqlCondition: "isDownloaded: true",
|
||||
},
|
||||
nondownload: {
|
||||
condition: (c) => !c.isDownloaded,
|
||||
gqlCondition: "isDownloaded: false",
|
||||
},
|
||||
read: {
|
||||
condition: (c) => c.isRead,
|
||||
gqlCondition: "isRead: true",
|
||||
},
|
||||
unread: {
|
||||
condition: (c) => !c.isRead,
|
||||
gqlCondition: "isRead: false",
|
||||
},
|
||||
downloadedread: {
|
||||
condition: (c) => c.isDownloaded && c.isRead,
|
||||
gqlCondition: "isDownloaded: true, isRead: true",
|
||||
},
|
||||
downloadedunread: {
|
||||
condition: (c) => c.isDownloaded && !c.isRead,
|
||||
gqlCondition: "isDownloaded: true, isRead: false",
|
||||
},
|
||||
nondownloadedread: {
|
||||
condition: (c) => !c.isDownloaded && c.isRead,
|
||||
gqlCondition: "isDownloaded: false, isRead: true",
|
||||
},
|
||||
nondownloadedunread: {
|
||||
condition: (c) => !c.isDownloaded && !c.isRead,
|
||||
gqlCondition: "isDownloaded: false, isRead: false",
|
||||
},
|
||||
};
|
||||
|
||||
function makeBody(fields, category = "all") {
|
||||
if (Number.isNaN(Number(category))) {
|
||||
let query = "";
|
||||
fields.forEach((field) => {
|
||||
query += `
|
||||
${field}: chapters(
|
||||
condition: {${countsToExtract[field].gqlCondition}}
|
||||
filter: {inLibrary: {equalTo: true}}
|
||||
) {
|
||||
totalCount
|
||||
}`;
|
||||
});
|
||||
return JSON.stringify({
|
||||
operationName: "Counts",
|
||||
query: `
|
||||
query Counts {
|
||||
${query}
|
||||
}`,
|
||||
});
|
||||
}
|
||||
|
||||
return JSON.stringify({
|
||||
operationName: "category",
|
||||
query: `
|
||||
query category($id: Int!) {
|
||||
category(id: $id) {
|
||||
# name
|
||||
mangas {
|
||||
nodes {
|
||||
chapters {
|
||||
nodes {
|
||||
isRead
|
||||
isDownloaded
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
variables: {
|
||||
id: Number(category),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function extractCounts(responseJSON, fields) {
|
||||
if (!("category" in responseJSON.data)) {
|
||||
return fields.map((field) => ({
|
||||
count: responseJSON.data[field].totalCount,
|
||||
label: `suwayomi.${field}`,
|
||||
}));
|
||||
}
|
||||
const tmp = responseJSON.data.category.mangas.nodes.reduce(
|
||||
(accumulator, manga) => {
|
||||
manga.chapters.nodes.forEach((chapter) => {
|
||||
fields.forEach((field, i) => {
|
||||
if (countsToExtract[field].condition(chapter)) {
|
||||
accumulator[i] += 1;
|
||||
}
|
||||
});
|
||||
});
|
||||
return accumulator;
|
||||
},
|
||||
[0, 0, 0, 0],
|
||||
);
|
||||
return fields.map((field, i) => ({
|
||||
count: tmp[i],
|
||||
label: `suwayomi.${field}`,
|
||||
}));
|
||||
}
|
||||
|
||||
export default async function suwayomiProxyHandler(req, res) {
|
||||
const { group, service, endpoint } = req.query;
|
||||
|
||||
if (!group || !service) {
|
||||
logger.debug("Invalid or missing service '%s' or group '%s'", service, group);
|
||||
return res.status(400).json({ error: "Invalid proxy service type" });
|
||||
}
|
||||
|
||||
const widget = await getServiceWidget(group, service);
|
||||
|
||||
if (!widget) {
|
||||
logger.debug("Invalid or missing widget for service '%s' in group '%s'", service, group);
|
||||
return res.status(400).json({ error: "Invalid proxy service type" });
|
||||
}
|
||||
|
||||
if (!widget.fields || widget.fields.length === 0) {
|
||||
widget.fields = ["download", "nondownload", "read", "unread"];
|
||||
} else if (widget.fields.length > 4) {
|
||||
widget.fields = widget.fields.slice(0, 4);
|
||||
}
|
||||
|
||||
const url = new URL(formatApiCall(widgets[widget.type].api, { endpoint, ...widget }));
|
||||
|
||||
const body = makeBody(widget.fields, widget.category);
|
||||
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
|
||||
if (widget.username && widget.password) {
|
||||
headers.Authorization = `Basic ${Buffer.from(`${widget.username}:${widget.password}`).toString("base64")}`;
|
||||
}
|
||||
|
||||
const [status, contentType, data] = await httpProxy(url, {
|
||||
method: "POST",
|
||||
body,
|
||||
headers,
|
||||
});
|
||||
|
||||
if (status === 401) {
|
||||
logger.error("Invalid or missing username or password for service '%s' in group '%s'", service, group);
|
||||
return res.status(status).send({ error: { message: "401: unauthorized, username or password is incorrect." } });
|
||||
}
|
||||
|
||||
if (status !== 200) {
|
||||
logger.error(
|
||||
"Error getting data from Suwayomi for service '%s' in group '%s': %d. Data: %s",
|
||||
service,
|
||||
group,
|
||||
status,
|
||||
data,
|
||||
);
|
||||
return res.status(status).send({ error: { message: "Error getting data. body: %s, data: %s", body, data } });
|
||||
}
|
||||
|
||||
const returnData = extractCounts(JSON.parse(data), widget.fields);
|
||||
|
||||
if (contentType) res.setHeader("Content-Type", contentType);
|
||||
return res.status(status).send(returnData);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import suwayomiProxyHandler from "./proxy";
|
||||
|
||||
const widget = {
|
||||
api: "{url}/api/graphql",
|
||||
proxyHandler: suwayomiProxyHandler,
|
||||
};
|
||||
|
||||
export default widget;
|
||||
@@ -205,7 +205,7 @@ export default function Component({ service }) {
|
||||
<div className="flex flex-col pb-1 mx-1">
|
||||
{playing.map((session) => (
|
||||
<SessionEntry
|
||||
key={session.Id}
|
||||
key={session.session_key}
|
||||
session={session}
|
||||
enableUser={enableUser}
|
||||
showEpisodeNumber={showEpisodeNumber}
|
||||
|
||||
@@ -21,9 +21,13 @@ export default async function tdarrProxyHandler(req, res) {
|
||||
logger.debug("Invalid or missing widget for service '%s' in group '%s'", service, group);
|
||||
return res.status(400).json({ error: "Invalid proxy service type" });
|
||||
}
|
||||
|
||||
const headers = {
|
||||
"content-type": "application/json",
|
||||
};
|
||||
if (widget.key) {
|
||||
headers["x-api-key"] = `${widget.key}`;
|
||||
}
|
||||
const url = new URL(formatApiCall(widgets[widget.type].api, { endpoint: undefined, ...widget }));
|
||||
|
||||
const [status, contentType, data] = await httpProxy(url, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
@@ -33,9 +37,7 @@ export default async function tdarrProxyHandler(req, res) {
|
||||
docID: "statistics",
|
||||
},
|
||||
}),
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
headers,
|
||||
});
|
||||
|
||||
if (status !== 200) {
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
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";
|
||||
|
||||
const MAX_ALLOWED_FIELDS = 4;
|
||||
|
||||
export const technitiumDefaultFields = ["totalQueries", "totalAuthoritative", "totalCached", "totalServerFailure"];
|
||||
|
||||
export default function Component({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { widget } = service;
|
||||
|
||||
const params = {
|
||||
type: widget.range ?? "LastHour",
|
||||
};
|
||||
|
||||
const { data: statsData, error: statsError } = useWidgetAPI(widget, "stats", params);
|
||||
|
||||
// Default fields
|
||||
if (!widget.fields?.length > 0) {
|
||||
widget.fields = technitiumDefaultFields;
|
||||
}
|
||||
|
||||
// Limits max number of displayed fields
|
||||
if (widget.fields?.length > MAX_ALLOWED_FIELDS) {
|
||||
widget.fields = widget.fields.slice(0, MAX_ALLOWED_FIELDS);
|
||||
}
|
||||
|
||||
if (statsError) {
|
||||
return <Container service={service} error={statsError} />;
|
||||
}
|
||||
|
||||
if (!statsData) {
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="technitium.totalQueries" />
|
||||
<Block label="technitium.totalNoError" />
|
||||
<Block label="technitium.totalServerFailure" />
|
||||
<Block label="technitium.totalNxDomain" />
|
||||
<Block label="technitium.totalRefused" />
|
||||
<Block label="technitium.totalAuthoritative" />
|
||||
<Block label="technitium.totalRecursive" />
|
||||
<Block label="technitium.totalCached" />
|
||||
<Block label="technitium.totalBlocked" />
|
||||
<Block label="technitium.totalDropped" />
|
||||
<Block label="technitium.totalClients" />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
function toPercent(value, total) {
|
||||
return t("common.percent", {
|
||||
value: !Number.isNaN(value / total) ? 100 * (value / total) : 0,
|
||||
maximumFractionDigits: 2,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="technitium.totalQueries" value={`${t("common.number", { value: statsData.totalQueries })}`} />
|
||||
<Block
|
||||
label="technitium.totalNoError"
|
||||
value={`${t("common.number", { value: statsData.totalNoError })} (${toPercent(
|
||||
statsData.totalNoError,
|
||||
statsData.totalQueries,
|
||||
)})`}
|
||||
/>
|
||||
<Block
|
||||
label="technitium.totalServerFailure"
|
||||
value={`${t("common.number", { value: statsData.totalServerFailure })} (${toPercent(
|
||||
statsData.totalServerFailure,
|
||||
statsData.totalQueries,
|
||||
)})`}
|
||||
/>
|
||||
<Block
|
||||
label="technitium.totalNxDomain"
|
||||
value={`${t("common.number", { value: statsData.totalNxDomain })} (${toPercent(
|
||||
statsData.totalNxDomain,
|
||||
statsData.totalQueries,
|
||||
)})`}
|
||||
/>
|
||||
<Block
|
||||
label="technitium.totalRefused"
|
||||
value={`${t("common.number", { value: statsData.totalRefused })} (${toPercent(
|
||||
statsData.totalRefused,
|
||||
statsData.totalQueries,
|
||||
)})`}
|
||||
/>
|
||||
<Block
|
||||
label="technitium.totalAuthoritative"
|
||||
value={`${t("common.number", { value: statsData.totalAuthoritative })} (${toPercent(
|
||||
statsData.totalAuthoritative,
|
||||
statsData.totalQueries,
|
||||
)})`}
|
||||
/>
|
||||
<Block
|
||||
label="technitium.totalRecursive"
|
||||
value={`${t("common.number", { value: statsData.totalRecursive })} (${toPercent(
|
||||
statsData.totalRecursive,
|
||||
statsData.totalQueries,
|
||||
)})`}
|
||||
/>
|
||||
<Block
|
||||
label="technitium.totalCached"
|
||||
value={`${t("common.number", { value: statsData.totalCached })} (${toPercent(
|
||||
statsData.totalCached,
|
||||
statsData.totalQueries,
|
||||
)})`}
|
||||
/>
|
||||
<Block
|
||||
label="technitium.totalBlocked"
|
||||
value={`${t("common.number", { value: statsData.totalBlocked })} (${toPercent(
|
||||
statsData.totalBlocked,
|
||||
statsData.totalQueries,
|
||||
)})`}
|
||||
/>
|
||||
<Block
|
||||
label="technitium.totalDropped"
|
||||
value={`${t("common.number", { value: statsData.totalDropped })} (${toPercent(
|
||||
statsData.totalDropped,
|
||||
statsData.totalQueries,
|
||||
)})`}
|
||||
/>
|
||||
<Block label="technitium.totalClients" value={`${t("common.number", { value: statsData.totalClients })}`} />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import genericProxyHandler from "utils/proxy/handlers/generic";
|
||||
import { asJson } from "utils/proxy/api-helpers";
|
||||
|
||||
const widget = {
|
||||
api: "{url}/api/{endpoint}?token={key}&utc=true",
|
||||
proxyHandler: genericProxyHandler,
|
||||
mappings: {
|
||||
stats: {
|
||||
endpoint: "dashboard/stats/get",
|
||||
validate: ["response", "status"],
|
||||
params: ["type"],
|
||||
map: (data) => asJson(data).response?.stats,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default widget;
|
||||
@@ -35,7 +35,7 @@ export default function Component({ service }) {
|
||||
<>
|
||||
<Container service={service}>
|
||||
<Block label="truenas.load" value={t("common.number", { value: statusData.loadavg[0] })} />
|
||||
<Block label="truenas.uptime" value={t("common.uptime", { value: statusData.uptime_seconds })} />
|
||||
<Block label="truenas.uptime" value={t("common.duration", { value: statusData.uptime_seconds })} />
|
||||
<Block label="truenas.alerts" value={t("common.number", { value: alertData.pending })} />
|
||||
</Container>
|
||||
{enablePools &&
|
||||
|
||||
@@ -25,7 +25,7 @@ const widget = {
|
||||
healthy: entry.healthy,
|
||||
allocated: entry.allocated,
|
||||
free: entry.free,
|
||||
data: entry.topology.data,
|
||||
data: entry.topology?.data ?? [],
|
||||
})),
|
||||
},
|
||||
},
|
||||
|
||||
@@ -32,16 +32,10 @@ export default function Component({ service }) {
|
||||
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block
|
||||
label="tubearchivist.downloads"
|
||||
value={t("common.number", { value: downloadsData?.paginate?.total_hits })}
|
||||
/>
|
||||
<Block label="tubearchivist.videos" value={t("common.number", { value: videosData?.paginate?.total_hits })} />
|
||||
<Block label="tubearchivist.channels" value={t("common.number", { value: channelsData?.paginate?.total_hits })} />
|
||||
<Block
|
||||
label="tubearchivist.playlists"
|
||||
value={t("common.number", { value: playlistsData?.paginate?.total_hits })}
|
||||
/>
|
||||
<Block label="tubearchivist.downloads" value={t("common.number", { value: downloadsData.pending ?? 0 })} />
|
||||
<Block label="tubearchivist.videos" value={t("common.number", { value: videosData.doc_count ?? 0 })} />
|
||||
<Block label="tubearchivist.channels" value={t("common.number", { value: channelsData.doc_count ?? 0 })} />
|
||||
<Block label="tubearchivist.playlists" value={t("common.number", { value: playlistsData.doc_count ?? 0 })} />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -6,20 +6,19 @@ const widget = {
|
||||
|
||||
mappings: {
|
||||
downloads: {
|
||||
endpoint: "download",
|
||||
validate: ["paginate"],
|
||||
endpoint: "stats/download",
|
||||
},
|
||||
videos: {
|
||||
endpoint: "video",
|
||||
validate: ["paginate"],
|
||||
endpoint: "stats/video",
|
||||
validate: ["doc_count"],
|
||||
},
|
||||
channels: {
|
||||
endpoint: "channel",
|
||||
validate: ["paginate"],
|
||||
endpoint: "stats/channel",
|
||||
validate: ["doc_count"],
|
||||
},
|
||||
playlists: {
|
||||
endpoint: "playlist",
|
||||
validate: ["paginate"],
|
||||
endpoint: "stats/playlist",
|
||||
validate: ["doc_count"],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// import credentialedProxyHandler from "utils/proxy/handlers/credentialed";
|
||||
import genericProxyHandler from "utils/proxy/handlers/generic";
|
||||
|
||||
const widget = {
|
||||
|
||||
@@ -58,7 +58,7 @@ export default function Component({ service }) {
|
||||
break;
|
||||
case 2:
|
||||
status = t("uptimerobot.up");
|
||||
uptime = t("common.uptime", { value: monitor.logs[0].duration });
|
||||
uptime = t("common.duration", { value: monitor.logs[0].duration });
|
||||
logIndex = 1;
|
||||
break;
|
||||
case 8:
|
||||
@@ -73,7 +73,7 @@ export default function Component({ service }) {
|
||||
}
|
||||
|
||||
const lastDown = new Date(monitor.logs[logIndex].datetime * 1000).toLocaleString();
|
||||
const downDuration = t("common.uptime", { value: monitor.logs[logIndex].duration });
|
||||
const downDuration = t("common.duration", { value: monitor.logs[logIndex].duration });
|
||||
const hideDown = logIndex === 1 && monitor.logs[logIndex].type !== 1;
|
||||
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
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 default function Component({ service }) {
|
||||
const { t } = useTranslation();
|
||||
const { widget } = service;
|
||||
|
||||
const { data: projectsData, error: projectsError } = useWidgetAPI(widget, "projects");
|
||||
const { data: tasksData, error: tasksError } = useWidgetAPI(widget, "tasks");
|
||||
|
||||
if (projectsError || tasksError) {
|
||||
return <Container service={service} error={projectsError ?? tasksError} />;
|
||||
}
|
||||
|
||||
if (!projectsData || !tasksData) {
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="vikunja.projects" />
|
||||
<Block label="vikunja.tasks7d" />
|
||||
<Block label="vikunja.tasksOverdue" />
|
||||
<Block label="vikunja.tasksInProgress" />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
const projects = projectsData.filter((project) => project.id > 0); // saved filters have id < 0
|
||||
|
||||
const oneWeekFromNow = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
|
||||
const tasksWithDueDate = tasksData.filter((task) => !task.dueDateIsDefault);
|
||||
const tasks7d = tasksWithDueDate.filter((task) => new Date(task.dueDate) <= oneWeekFromNow);
|
||||
const tasksOverdue = tasksWithDueDate.filter((task) => new Date(task.dueDate) <= new Date(Date.now()));
|
||||
const tasksInProgress = tasksData.filter((task) => task.inProgress);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container service={service}>
|
||||
<Block label="vikunja.projects" value={t("common.number", { value: projects.length })} />
|
||||
<Block label="vikunja.tasks7d" value={t("common.number", { value: tasks7d.length })} />
|
||||
<Block label="vikunja.tasksOverdue" value={t("common.number", { value: tasksOverdue.length })} />
|
||||
<Block label="vikunja.tasksInProgress" value={t("common.number", { value: tasksInProgress.length })} />
|
||||
</Container>
|
||||
{widget.enableTaskList &&
|
||||
tasksData.slice(0, 5).map((task) => (
|
||||
<div
|
||||
key={task.id}
|
||||
className="text-theme-700 dark:text-theme-200 relative h-5 rounded-md bg-theme-200/50 dark:bg-theme-900/20 m-1 px-1 flex"
|
||||
>
|
||||
<div className="text-xs z-10 self-center ml-2 relative h-4 grow mr-2">
|
||||
<div className="absolute w-full h-4 whitespace-nowrap text-ellipsis overflow-hidden text-left">
|
||||
{task.title}
|
||||
</div>
|
||||
</div>
|
||||
{!task.dueDateIsDefault && (
|
||||
<div className="self-center text-xs flex justify-end mr-1.5 pl-1 z-10 text-ellipsis overflow-hidden whitespace-nowrap">
|
||||
{t("common.relativeDate", {
|
||||
value: task.dueDate,
|
||||
formatParams: { value: { style: "narrow", numeric: "auto" } },
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import credentialedProxyHandler from "utils/proxy/handlers/credentialed";
|
||||
import { asJson } from "utils/proxy/api-helpers";
|
||||
|
||||
const widget = {
|
||||
api: `{url}/api/v1/{endpoint}`,
|
||||
proxyHandler: credentialedProxyHandler,
|
||||
|
||||
mappings: {
|
||||
projects: {
|
||||
endpoint: "projects",
|
||||
},
|
||||
tasks: {
|
||||
endpoint: "tasks/all?filter=done%3Dfalse&sort_by=due_date",
|
||||
map: (data) =>
|
||||
asJson(data).map((task) => ({
|
||||
id: task.id,
|
||||
title: task.title,
|
||||
priority: task.priority,
|
||||
dueDate: task.due_date,
|
||||
dueDateIsDefault: task.due_date === "0001-01-01T00:00:00Z",
|
||||
inProgress: task.percent_done > 0 && task.percent_done < 1,
|
||||
})),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default widget;
|
||||
@@ -5,14 +5,14 @@ import useWidgetAPI from "utils/proxy/use-widget-api";
|
||||
export default function Component({ service }) {
|
||||
const { widget } = service;
|
||||
|
||||
const { data: infoData, error: infoError } = useWidgetAPI(widget);
|
||||
const { data: infoData, error: infoError } = useWidgetAPI(widget, "client");
|
||||
|
||||
if (!widget.fields) {
|
||||
widget.fields = ["connected", "enabled", "total"];
|
||||
}
|
||||
|
||||
if (infoError) {
|
||||
return <Container service={service} error={infoError} />;
|
||||
if (infoError || infoData?.statusCode > 400) {
|
||||
return <Container service={service} error={infoError ?? { message: infoData.statusMessage, data: infoData }} />;
|
||||
}
|
||||
|
||||
if (!infoData) {
|
||||
@@ -38,7 +38,7 @@ export default function Component({ service }) {
|
||||
<Container service={service}>
|
||||
<Block label="wgeasy.connected" value={connected} />
|
||||
<Block label="wgeasy.enabled" value={enabled} />
|
||||
<Block label="wgeasy.diabled" value={disabled} />
|
||||
<Block label="wgeasy.disabled" value={disabled} />
|
||||
<Block label="wgeasy.total" value={infoData.length} />
|
||||
</Container>
|
||||
);
|
||||
|
||||
Loaded 100 of 215 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user