Merge pull request #502 from benphelps/widget-data-validation

Feature: basic widget data validation, improved error display
This commit is contained in:
shamoon 2022-11-18 22:15:26 -08:00 committed by GitHub
commit b0fc8098a7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
70 changed files with 259 additions and 123 deletions

View File

@ -13,7 +13,11 @@
"widget": { "widget": {
"missing_type": "Missing Widget Type: {{type}}", "missing_type": "Missing Widget Type: {{type}}",
"api_error": "API Error", "api_error": "API Error",
"status": "Status" "information": "Information",
"status": "Status",
"url": "URL",
"raw_error": "Raw Error",
"response_data": "Response Data"
}, },
"weather": { "weather": {
"current": "Current Location", "current": "Current Location",

View File

@ -85,7 +85,7 @@ export default function Item({ service }) {
{service.container && service.server && ( {service.container && service.server && (
<div <div
className={classNames( className={classNames(
statsOpen && !statsClosing ? "max-h-[55px] opacity-100" : " max-h-[0] opacity-0", statsOpen && !statsClosing ? "max-h-[110px] opacity-100" : " max-h-[0] opacity-0",
"w-full overflow-hidden transition-all duration-300 ease-in-out" "w-full overflow-hidden transition-all duration-300 ease-in-out"
)} )}
> >

View File

@ -1,10 +1,8 @@
import Error from "./error";
export default function Container({ error = false, children, service }) { export default function Container({ error = false, children, service }) {
if (error) { if (error) {
return ( return <Error error={error} />
<div className="bg-theme-200/50 dark:bg-theme-900/20 rounded m-1 flex-1 flex flex-col items-center justify-center p-1">
<div className="font-thin text-sm">{error}</div>
</div>
);
} }
let visibleChildren = children; let visibleChildren = children;

View File

@ -0,0 +1,50 @@
import { useTranslation } from "react-i18next";
import { IoAlertCircle } from "react-icons/io5";
function displayError(error) {
return JSON.stringify(error[1] ? error[1] : error, null, 4);
}
function displayData(data) {
return (data.type === 'Buffer') ? Buffer.from(data).toString() : JSON.stringify(data, 4);
}
export default function Error({ error }) {
const { t } = useTranslation();
if (error?.data?.error) {
error = error.data.error; // eslint-disable-line no-param-reassign
}
return (
<details className="px-1 pb-1">
<summary className="block text-center mt-1 mb-0 mx-auto p-3 rounded bg-rose-900/80 hover:bg-rose-900/95 text-theme-900 cursor-pointer">
<div className="flex items-center justify-center text-xs font-bold">
<IoAlertCircle className="mr-1 w-5 h-5"/>{t("widget.api_error")} {error.message && t("widget.information")}
</div>
</summary>
<div className="bg-white dark:bg-theme-200/50 mt-2 rounded text-rose-900 text-xs font-mono whitespace-pre-wrap break-all">
<ul className="p-4">
{error.message && <li>
<span className="text-black">{t("widget.api_error")}:</span> {error.message}
</li>}
{error.url && <li className="mt-2">
<span className="text-black">{t("widget.url")}:</span> {error.url}
</li>}
{error.rawError && <li className="mt-2">
<span className="text-black">{t("widget.raw_error")}:</span>
<div className="ml-2">
{displayError(error.rawError)}
</div>
</li>}
{error.data && <li className="mt-2">
<span className="text-black">{t("widget.response_data")}:</span>
<div className="ml-2">
{displayData(error.data)}
</div>
</li>}
</ul>
</div>
</details>
);
}

View File

@ -12,7 +12,7 @@ export default function Widget({ options }) {
options.type = "unifi_console"; options.type = "unifi_console";
const { data: statsData, error: statsError } = useWidgetAPI(options, "stat/sites", { index: options.index }); const { data: statsData, error: statsError } = useWidgetAPI(options, "stat/sites", { index: options.index });
if (statsError || statsData?.error) { if (statsError) {
return ( return (
<div className="flex flex-col justify-center first:ml-0 ml-4"> <div className="flex flex-col justify-center first:ml-0 ml-4">
<div className="flex flex-row items-center justify-end"> <div className="flex flex-row items-center justify-end">

View File

@ -46,7 +46,7 @@ export default async function handler(req, res) {
}); });
} catch { } catch {
res.status(500).send({ res.status(500).send({
error: "unknown error", error: {message: "Unknown error"},
}); });
} }
} }

View File

@ -1,5 +1,6 @@
import getServiceWidget from "utils/config/service-helpers"; import getServiceWidget from "utils/config/service-helpers";
import { formatApiCall } from "utils/proxy/api-helpers"; import { formatApiCall } from "utils/proxy/api-helpers";
import validateWidgetData from "utils/proxy/validate-widget-data";
import { httpProxy } from "utils/proxy/http"; import { httpProxy } from "utils/proxy/http";
import createLogger from "utils/logger"; import createLogger from "utils/logger";
import widgets from "widgets/widgets"; import widgets from "widgets/widgets";
@ -54,6 +55,10 @@ export default async function credentialedProxyHandler(req, res) {
logger.debug("HTTP Error %d calling %s//%s%s...", status, url.protocol, url.hostname, url.pathname); logger.debug("HTTP Error %d calling %s//%s%s...", status, url.protocol, url.hostname, url.pathname);
} }
if (!validateWidgetData(widget, endpoint, data)) {
return res.status(500).json({error: {message: "Invalid data", url, data}});
}
if (contentType) res.setHeader("Content-Type", contentType); if (contentType) res.setHeader("Content-Type", contentType);
return res.status(status).send(data); return res.status(status).send(data);
} }

View File

@ -1,5 +1,6 @@
import getServiceWidget from "utils/config/service-helpers"; import getServiceWidget from "utils/config/service-helpers";
import { formatApiCall } from "utils/proxy/api-helpers"; import { formatApiCall } from "utils/proxy/api-helpers";
import validateWidgetData from "utils/proxy/validate-widget-data";
import { httpProxy } from "utils/proxy/http"; import { httpProxy } from "utils/proxy/http";
import createLogger from "utils/logger"; import createLogger from "utils/logger";
import widgets from "widgets/widgets"; import widgets from "widgets/widgets";
@ -32,6 +33,11 @@ export default async function genericProxyHandler(req, res, map) {
}); });
let resultData = data; let resultData = data;
if (!validateWidgetData(widget, endpoint, resultData)) {
return res.status(status).json({error: {message: "Invalid data", url, data: resultData}});
}
if (status === 200 && map) { if (status === 200 && map) {
resultData = map(data); resultData = map(data);
} }
@ -44,6 +50,7 @@ export default async function genericProxyHandler(req, res, map) {
if (status >= 400) { if (status >= 400) {
logger.debug("HTTP Error %d calling %s//%s%s...", status, url.protocol, url.hostname, url.pathname); logger.debug("HTTP Error %d calling %s//%s%s...", status, url.protocol, url.hostname, url.pathname);
return res.status(status).json({error: {message: "HTTP Error", url, data}});
} }
return res.status(status).send(resultData); return res.status(status).send(resultData);

View File

@ -98,6 +98,6 @@ export async function httpProxy(url, params = {}) {
catch (err) { catch (err) {
logger.error("Error calling %s//%s%s...", url.protocol, url.hostname, url.pathname); logger.error("Error calling %s//%s%s...", url.protocol, url.hostname, url.pathname);
logger.error(err); logger.error(err);
return [500, "application/json", { error: "Unexpected error" }, null]; return [500, "application/json", { error: {message: err?.message ?? "Unknown error", url, rawError: err} }, null];
} }
} }

View File

@ -3,5 +3,11 @@ import useSWR from "swr";
import { formatProxyUrl } from "./api-helpers"; import { formatProxyUrl } from "./api-helpers";
export default function useWidgetAPI(widget, ...options) { export default function useWidgetAPI(widget, ...options) {
return useSWR(formatProxyUrl(widget, ...options)); const config = {};
if (options?.refreshInterval) {
config.refreshInterval = options.refreshInterval;
}
const { data, error } = useSWR(formatProxyUrl(widget, ...options), config);
// make the data error the top-level error
return { data, error: data?.error ?? error }
} }

View File

@ -0,0 +1,22 @@
import widgets from "widgets/widgets";
export default function validateWidgetData(widget, endpoint, data) {
let valid = true;
let dataParsed;
try {
dataParsed = JSON.parse(data);
} catch (e) {
valid = false;
}
if (dataParsed) {
const validate = widgets[widget.type]?.mappings?.[endpoint]?.validate;
validate.forEach(key => {
if (dataParsed[key] === undefined) {
valid = false;
}
});
}
return valid;
}

View File

@ -12,7 +12,7 @@ export default function Component({ service }) {
const { data: adguardData, error: adguardError } = useWidgetAPI(widget, "stats"); const { data: adguardData, error: adguardError } = useWidgetAPI(widget, "stats");
if (adguardError) { if (adguardError) {
return <Container error={t("widget.api_error")} />; return <Container error={adguardError} />;
} }
if (!adguardData) { if (!adguardData) {

View File

@ -14,7 +14,8 @@ export default function Component({ service }) {
const { data: failedLoginsData, error: failedLoginsError } = useWidgetAPI(widget, "login_failed"); const { data: failedLoginsData, error: failedLoginsError } = useWidgetAPI(widget, "login_failed");
if (usersError || loginsError || failedLoginsError) { if (usersError || loginsError || failedLoginsError) {
return <Container error={t("widget.api_error")} />; const finalError = usersError ?? loginsError ?? failedLoginsError;
return <Container error={finalError} />;
} }
if (!usersData || !loginsData || !failedLoginsData) { if (!usersData || !loginsData || !failedLoginsData) {

View File

@ -14,7 +14,8 @@ export default function Component({ service }) {
const { data: indexersData, error: indexersError } = useWidgetAPI(widget, "indexers"); const { data: indexersData, error: indexersError } = useWidgetAPI(widget, "indexers");
if (statsError || filtersError || indexersError) { if (statsError || filtersError || indexersError) {
return <Container error={t("widget.api_error")} />; const finalError = statsError ?? filtersError ?? indexersError;
return <Container error={finalError} />;
} }
if (!statsData || !filtersData || !indexersData) { if (!statsData || !filtersData || !indexersData) {

View File

@ -7,6 +7,10 @@ const widget = {
mappings: { mappings: {
stats: { stats: {
endpoint: "release/stats", endpoint: "release/stats",
validate: [
"push_approved_count",
"push_rejected_count"
]
}, },
filters: { filters: {
endpoint: "filters", endpoint: "filters",

View File

@ -12,8 +12,9 @@ export default function Component({ service }) {
const { data: episodesData, error: episodesError } = useWidgetAPI(widget, "episodes"); const { data: episodesData, error: episodesError } = useWidgetAPI(widget, "episodes");
const { data: moviesData, error: moviesError } = useWidgetAPI(widget, "movies"); const { data: moviesData, error: moviesError } = useWidgetAPI(widget, "movies");
if (episodesError || moviesError) { if (moviesError || episodesError) {
return <Container error="widget.api_error" />; const finalError = moviesError ?? episodesError;
return <Container error={finalError} />;
} }
if (!episodesData || !moviesData) { if (!episodesData || !moviesData) {

View File

@ -9,12 +9,11 @@ export default function Component({ service }) {
const { widget } = service; const { widget } = service;
const { data } = useWidgetAPI(widget, "info"); const { data, error } = useWidgetAPI(widget, "info");
if (!data) { if (error) {
return <Container error="widget.api_error" />; return <Container error={error} />;
} }
const totalObserved = Object.keys(data).length; const totalObserved = Object.keys(data).length;
let diffsDetected = 0; let diffsDetected = 0;

View File

@ -37,7 +37,7 @@ export default function Component({ service }) {
} }
if (statsError) { if (statsError) {
return <Container error={t("widget.api_error")} />; return <Container error={statsError} />;
} }
if (!statsData || !dateRange) { if (!statsData || !dateRange) {

View File

@ -17,8 +17,9 @@ export default function Component({ service }) {
const { data: statsData, error: statsError } = useSWR(`/api/docker/stats/${widget.container}/${widget.server || ""}`); const { data: statsData, error: statsError } = useSWR(`/api/docker/stats/${widget.container}/${widget.server || ""}`);
if (statsError || statusError) { if (statsError || statsData?.error || statusError || statusData?.error) {
return <Container error={t("widget.api_error")} />; const finalError = statsError ?? statsData?.error ?? statusError ?? statusData?.error;
return <Container error={finalError} />;
} }
if (statusData && statusData.status !== "running") { if (statusData && statusData.status !== "running") {

View File

@ -1,10 +1,10 @@
import useSWR from "swr";
import { useTranslation } from "next-i18next"; import { useTranslation } from "next-i18next";
import { BsVolumeMuteFill, BsFillPlayFill, BsPauseFill, BsCpu, BsFillCpuFill } from "react-icons/bs"; import { BsVolumeMuteFill, BsFillPlayFill, BsPauseFill, BsCpu, BsFillCpuFill } from "react-icons/bs";
import { MdOutlineSmartDisplay } from "react-icons/md"; import { MdOutlineSmartDisplay } from "react-icons/md";
import Container from "components/services/widget/container"; import Container from "components/services/widget/container";
import { formatProxyUrl, formatProxyUrlWithSegments } from "utils/proxy/api-helpers"; import { formatProxyUrlWithSegments } from "utils/proxy/api-helpers";
import useWidgetAPI from "utils/proxy/use-widget-api";
function ticksToTime(ticks) { function ticksToTime(ticks) {
const milliseconds = ticks / 10000; const milliseconds = ticks / 10000;
@ -157,7 +157,7 @@ export default function Component({ service }) {
data: sessionsData, data: sessionsData,
error: sessionsError, error: sessionsError,
mutate: sessionMutate, mutate: sessionMutate,
} = useSWR(formatProxyUrl(widget, "Sessions"), { } = useWidgetAPI(widget, "Sessions", {
refreshInterval: 5000, refreshInterval: 5000,
}); });
@ -171,8 +171,8 @@ export default function Component({ service }) {
}); });
} }
if (sessionsError || sessionsData?.error) { if (sessionsError) {
return <Container error={t("widget.api_error")} />; return <Container error={sessionsError} />;
} }
if (!sessionsData) { if (!sessionsData) {

View File

@ -1,18 +1,14 @@
import { useTranslation } from "next-i18next";
import Container from "components/services/widget/container"; import Container from "components/services/widget/container";
import Block from "components/services/widget/block"; import Block from "components/services/widget/block";
import useWidgetAPI from "utils/proxy/use-widget-api"; import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) { export default function Component({ service }) {
const { t } = useTranslation();
const { widget } = service; const { widget } = service;
const { data: gluetunData, error: gluetunError } = useWidgetAPI(widget, "ip"); const { data: gluetunData, error: gluetunError } = useWidgetAPI(widget, "ip");
if (gluetunError) { if (gluetunError) {
return <Container error={t("widget.api_error")} />; return <Container error={gluetunError} />;
} }
if (!gluetunData) { if (!gluetunData) {

View File

@ -7,6 +7,11 @@ const widget = {
mappings: { mappings: {
ip: { ip: {
endpoint: "publicip/ip", endpoint: "publicip/ip",
validate: [
"public_ip",
"region",
"country"
]
}, },
}, },
}; };

View File

@ -1,12 +1,8 @@
import { useTranslation } from "next-i18next";
import Container from "components/services/widget/container"; import Container from "components/services/widget/container";
import Block from "components/services/widget/block"; import Block from "components/services/widget/block";
import useWidgetAPI from "utils/proxy/use-widget-api"; import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) { export default function Component({ service }) {
const { t } = useTranslation();
const { widget } = service; const { widget } = service;
const { data: appsData, error: appsError } = useWidgetAPI(widget, "application"); const { data: appsData, error: appsError } = useWidgetAPI(widget, "application");
@ -14,7 +10,8 @@ export default function Component({ service }) {
const { data: clientsData, error: clientsError } = useWidgetAPI(widget, "client"); const { data: clientsData, error: clientsError } = useWidgetAPI(widget, "client");
if (appsError || messagesError || clientsError) { if (appsError || messagesError || clientsError) {
return <Container error={t("widget.api_error")} />; const finalError = appsError ?? messagesError ?? clientsError;
return <Container error={finalError} />;
} }

View File

@ -11,8 +11,8 @@ export default function Component({ service }) {
const { data: homebridgeData, error: homebridgeError } = useWidgetAPI(widget, "info"); const { data: homebridgeData, error: homebridgeError } = useWidgetAPI(widget, "info");
if (homebridgeError || homebridgeData?.error) { if (homebridgeError) {
return <Container error={t("widget.api_error")} />; return <Container error={homebridgeError} />;
} }
if (!homebridgeData) { if (!homebridgeData) {

View File

@ -12,7 +12,7 @@ export default function Component({ service }) {
const { data: indexersData, error: indexersError } = useWidgetAPI(widget, "indexers"); const { data: indexersData, error: indexersError } = useWidgetAPI(widget, "indexers");
if (indexersError) { if (indexersError) {
return <Container error={t("widget.api_error")} />; return <Container error={indexersError} />;
} }
if (!indexersData) { if (!indexersData) {

View File

@ -1,18 +1,14 @@
import { useTranslation } from "next-i18next";
import Container from "components/services/widget/container"; import Container from "components/services/widget/container";
import Block from "components/services/widget/block"; import Block from "components/services/widget/block";
import useWidgetAPI from "utils/proxy/use-widget-api"; import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) { export default function Component({ service }) {
const { t } = useTranslation();
const { widget } = service; const { widget } = service;
const { data: statsData, error: statsError } = useWidgetAPI(widget, "request/count"); const { data: statsData, error: statsError } = useWidgetAPI(widget, "request/count");
if (statsError) { if (statsError) {
return <Container error={t("widget.api_error")} />; return <Container error={statsError} />;
} }
if (!statsData) { if (!statsData) {

View File

@ -7,6 +7,11 @@ const widget = {
mappings: { mappings: {
"request/count": { "request/count": {
endpoint: "request/count", endpoint: "request/count",
validate: [
"pending",
"approved",
"available"
]
}, },
}, },
}; };

View File

@ -14,7 +14,8 @@ export default function Component({ service }) {
const { data: queueData, error: queueError } = useWidgetAPI(widget, "queue/status"); const { data: queueData, error: queueError } = useWidgetAPI(widget, "queue/status");
if (albumsError || wantedError || queueError) { if (albumsError || wantedError || queueError) {
return <Container error={t("widget.api_error")} />; const finalError = albumsError ?? wantedError ?? queueError;
return <Container error={finalError} />;
} }
if (!albumsData || !wantedData || !queueData) { if (!albumsData || !wantedData || !queueData) {

View File

@ -12,7 +12,7 @@ export default function Component({ service }) {
const { data: statsData, error: statsError } = useWidgetAPI(widget, "instance"); const { data: statsData, error: statsError } = useWidgetAPI(widget, "instance");
if (statsError) { if (statsError) {
return <Container error={t("widget.api_error")} />; return <Container error={statsError} />;
} }
if (!statsData) { if (!statsData) {

View File

@ -26,8 +26,8 @@ export default function Component({ service }) {
const { data: navidromeData, error: navidromeError } = useWidgetAPI(widget, "getNowPlaying"); const { data: navidromeData, error: navidromeError } = useWidgetAPI(widget, "getNowPlaying");
if (navidromeError || navidromeData?.error || navidromeData?.["subsonic-response"]?.error) { if (navidromeError || navidromeData?.["subsonic-response"]?.error) {
return <Container error={t("widget.api_error")} />; return <Container error={navidromeError ?? navidromeData?.["subsonic-response"]?.error} />;
} }
if (!navidromeData) { if (!navidromeData) {

View File

@ -1,18 +1,14 @@
import { useTranslation } from "next-i18next";
import Container from "components/services/widget/container"; import Container from "components/services/widget/container";
import Block from "components/services/widget/block"; import Block from "components/services/widget/block";
import useWidgetAPI from "utils/proxy/use-widget-api"; import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) { export default function Component({ service }) {
const { t } = useTranslation();
const { widget } = service; const { widget } = service;
const { data: infoData, error: infoError } = useWidgetAPI(widget, "nginx/proxy-hosts"); const { data: infoData, error: infoError } = useWidgetAPI(widget, "nginx/proxy-hosts");
if (infoError || infoData?.error) { if (infoError) {
return <Container error={t("widget.api_error")} />; return <Container error={infoError} />;
} }
if (!infoData) { if (!infoData) {

View File

@ -12,7 +12,7 @@ export default function Component({ service }) {
const { data: statusData, error: statusError } = useWidgetAPI(widget, "status"); const { data: statusData, error: statusError } = useWidgetAPI(widget, "status");
if (statusError) { if (statusError) {
return <Container error={t("widget.api_error")} />; return <Container error={statusError} />;
} }
if (!statusData) { if (!statusData) {

View File

@ -1,18 +1,14 @@
import { useTranslation } from "next-i18next";
import Container from "components/services/widget/container"; import Container from "components/services/widget/container";
import Block from "components/services/widget/block"; import Block from "components/services/widget/block";
import useWidgetAPI from "utils/proxy/use-widget-api"; import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) { export default function Component({ service }) {
const { t } = useTranslation();
const { widget } = service; const { widget } = service;
const { data: statsData, error: statsError } = useWidgetAPI(widget, "Request/count"); const { data: statsData, error: statsError } = useWidgetAPI(widget, "Request/count");
if (statsError) { if (statsError) {
return <Container error={t("widget.api_error")} />; return <Container error={statsError} />;
} }
if (!statsData) { if (!statsData) {

View File

@ -1,18 +1,14 @@
import { useTranslation } from "next-i18next";
import Container from "components/services/widget/container"; import Container from "components/services/widget/container";
import Block from "components/services/widget/block"; import Block from "components/services/widget/block";
import useWidgetAPI from "utils/proxy/use-widget-api"; import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) { export default function Component({ service }) {
const { t } = useTranslation();
const { widget } = service; const { widget } = service;
const { data: statsData, error: statsError } = useWidgetAPI(widget, "request/count"); const { data: statsData, error: statsError } = useWidgetAPI(widget, "request/count");
if (statsError) { if (statsError) {
return <Container error={t("widget.api_error")} />; return <Container error={statsError} />;
} }
if (!statsData) { if (!statsData) {

View File

@ -7,6 +7,11 @@ const widget = {
mappings: { mappings: {
"request/count": { "request/count": {
endpoint: "request/count", endpoint: "request/count",
validate: [
"pending",
"approved",
"available",
],
}, },
}, },
}; };

View File

@ -12,7 +12,7 @@ export default function Component({ service }) {
const { data: piholeData, error: piholeError } = useWidgetAPI(widget, "api.php"); const { data: piholeData, error: piholeError } = useWidgetAPI(widget, "api.php");
if (piholeError) { if (piholeError) {
return <Container error={t("widget.api_error")} />; return <Container error={piholeError} />;
} }
if (!piholeData) { if (!piholeData) {

View File

@ -7,6 +7,11 @@ const widget = {
mappings: { mappings: {
"api.php": { "api.php": {
endpoint: "api.php", endpoint: "api.php",
validate: [
"dns_queries_today",
"ads_blocked_today",
"domains_being_blocked"
]
}, },
}, },
}; };

View File

@ -1,21 +1,20 @@
import useSWR from "swr";
import { useTranslation } from "next-i18next"; import { useTranslation } from "next-i18next";
import Block from "components/services/widget/block"; import Block from "components/services/widget/block";
import Container from "components/services/widget/container"; import Container from "components/services/widget/container";
import { formatProxyUrl } from "utils/proxy/api-helpers"; import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) { export default function Component({ service }) {
const { t } = useTranslation(); const { t } = useTranslation();
const { widget } = service; const { widget } = service;
const { data: plexData, error: plexAPIError } = useSWR(formatProxyUrl(widget, "unified"), { const { data: plexData, error: plexAPIError } = useWidgetAPI(widget, "unified", {
refreshInterval: 5000, refreshInterval: 5000,
}); });
if (plexAPIError) { if (plexAPIError) {
return <Container error={t("widget.api_error")} />; return <Container error={plexAPIError} />;
} }
if (!plexData) { if (!plexData) {

View File

@ -44,7 +44,7 @@ async function fetchFromPlexAPI(endpoint, widget) {
if (status !== 200) { if (status !== 200) {
logger.error("HTTP %d communicating with Plex. Data: %s", status, data.toString()); logger.error("HTTP %d communicating with Plex. Data: %s", status, data.toString());
return [status, data.toString()]; return [status, data];
} }
try { try {
@ -65,6 +65,11 @@ export default async function plexProxyHandler(req, res) {
logger.debug("Getting streams from Plex API"); logger.debug("Getting streams from Plex API");
let streams; let streams;
let [status, apiData] = await fetchFromPlexAPI("/status/sessions", widget); let [status, apiData] = await fetchFromPlexAPI("/status/sessions", widget);
if (status !== 200) {
return res.status(status).json({error: {message: "HTTP error communicating with Plex API", data: Buffer.from(apiData).toString()}});
}
if (apiData && apiData.MediaContainer) { if (apiData && apiData.MediaContainer) {
streams = apiData.MediaContainer._attributes.size; streams = apiData.MediaContainer._attributes.size;
} }

View File

@ -14,7 +14,7 @@ export default function Component({ service }) {
}); });
if (containersError) { if (containersError) {
return <Container error={t("widget.api_error")} />; return <Container error={containersError} />;
} }
if (!containersData) { if (!containersData) {

View File

@ -1,19 +1,16 @@
import { useTranslation } from "next-i18next";
import Container from "components/services/widget/container"; import Container from "components/services/widget/container";
import Block from "components/services/widget/block"; import Block from "components/services/widget/block";
import useWidgetAPI from "utils/proxy/use-widget-api"; import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) { export default function Component({ service }) {
const { t } = useTranslation();
const { widget } = service; const { widget } = service;
const { data: indexersData, error: indexersError } = useWidgetAPI(widget, "indexer"); const { data: indexersData, error: indexersError } = useWidgetAPI(widget, "indexer");
const { data: grabsData, error: grabsError } = useWidgetAPI(widget, "indexerstats"); const { data: grabsData, error: grabsError } = useWidgetAPI(widget, "indexerstats");
if (indexersError || grabsError) { if (indexersError || grabsError) {
return <Container error={t("widget.api_error")} />; const finalError = indexersError ?? grabsError;
return <Container error={finalError} />;
} }
if (!indexersData || !grabsData) { if (!indexersData || !grabsData) {

View File

@ -16,7 +16,7 @@ export default function Component({ service }) {
const { data: clusterData, error: clusterError } = useWidgetAPI(widget, "cluster/resources"); const { data: clusterData, error: clusterError } = useWidgetAPI(widget, "cluster/resources");
if (clusterError) { if (clusterError) {
return <Container error={t("widget.api_error")} />; return <Container error={clusterError} />;
} }
if (!clusterData || !clusterData.data) { if (!clusterData || !clusterData.data) {

View File

@ -9,8 +9,8 @@ export default function Component({ service }) {
const { widget } = service; const { widget } = service;
const { data: pyloadData, error: pyloadError } = useWidgetAPI(widget, "status"); const { data: pyloadData, error: pyloadError } = useWidgetAPI(widget, "status");
if (pyloadError || pyloadData?.error) { if (pyloadError) {
return <Container error={t("widget.api_error")} />; return <Container error={pyloadError} />;
} }
if (!pyloadData) { if (!pyloadData) {

View File

@ -84,9 +84,9 @@ export default async function pyloadProxyHandler(req, res) {
if (data?.error || status !== 200) { if (data?.error || status !== 200) {
try { try {
return res.status(status).send(Buffer.from(data).toString()); return res.status(status).send({error: {message: "HTTP error communicating with Plex API", data: Buffer.from(data).toString()}});
} catch (e) { } catch (e) {
return res.status(status).send(data); return res.status(status).send({error: {message: "HTTP error communicating with Plex API", data}});
} }
} }
@ -95,7 +95,7 @@ export default async function pyloadProxyHandler(req, res) {
} }
} catch (e) { } catch (e) {
logger.error(e); logger.error(e);
return res.status(500).send(e.toString()); return res.status(500).send({error: {message: `Error communicating with Plex API: ${e.toString()}`}});
} }
return res.status(400).json({ error: 'Invalid proxy service type' }); return res.status(400).json({ error: 'Invalid proxy service type' });

View File

@ -12,7 +12,7 @@ export default function Component({ service }) {
const { data: torrentData, error: torrentError } = useWidgetAPI(widget, "torrents/info"); const { data: torrentData, error: torrentError } = useWidgetAPI(widget, "torrents/info");
if (torrentError) { if (torrentError) {
return <Container error={t("widget.api_error")} />; return <Container error={torrentError} />;
} }
if (!torrentData) { if (!torrentData) {

View File

@ -1,19 +1,16 @@
import { useTranslation } from "next-i18next";
import Container from "components/services/widget/container"; import Container from "components/services/widget/container";
import Block from "components/services/widget/block"; import Block from "components/services/widget/block";
import useWidgetAPI from "utils/proxy/use-widget-api"; import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) { export default function Component({ service }) {
const { t } = useTranslation();
const { widget } = service; const { widget } = service;
const { data: moviesData, error: moviesError } = useWidgetAPI(widget, "movie"); const { data: moviesData, error: moviesError } = useWidgetAPI(widget, "movie");
const { data: queuedData, error: queuedError } = useWidgetAPI(widget, "queue/status"); const { data: queuedData, error: queuedError } = useWidgetAPI(widget, "queue/status");
if (moviesError || queuedError) { if (moviesError || queuedError) {
return <Container error={t("widget.api_error")} />; const finalError = moviesError ?? queuedError;
return <Container error={finalError} />;
} }
if (!moviesData || !queuedData) { if (!moviesData || !queuedData) {

View File

@ -16,6 +16,9 @@ const widget = {
}, },
"queue/status": { "queue/status": {
endpoint: "queue/status", endpoint: "queue/status",
validate: [
"totalCount"
]
}, },
}, },
}; };

View File

@ -14,7 +14,8 @@ export default function Component({ service }) {
const { data: queueData, error: queueError } = useWidgetAPI(widget, "queue/status"); const { data: queueData, error: queueError } = useWidgetAPI(widget, "queue/status");
if (booksError || wantedError || queueError) { if (booksError || wantedError || queueError) {
return <Container error={t("widget.api_error")} />; const finalError = booksError ?? wantedError ?? queueError;
return <Container error={finalError} />;
} }
if (!booksData || !wantedData || !queueData) { if (!booksData || !wantedData || !queueData) {

View File

@ -12,7 +12,7 @@ export default function Component({ service }) {
const { data: statusData, error: statusError } = useWidgetAPI(widget); const { data: statusData, error: statusError } = useWidgetAPI(widget);
if (statusError) { if (statusError) {
return <Container error={t("widget.api_error")} />; return <Container error={statusError} />;
} }
if (!statusData) { if (!statusData) {

View File

@ -22,7 +22,7 @@ export default function Component({ service }) {
const { data: queueData, error: queueError } = useWidgetAPI(widget, "queue"); const { data: queueData, error: queueError } = useWidgetAPI(widget, "queue");
if (queueError) { if (queueError) {
return <Container error={t("widget.api_error")} />; return <Container error={queueError} />;
} }
if (!queueData) { if (!queueData) {

View File

@ -7,6 +7,9 @@ const widget = {
mappings: { mappings: {
queue: { queue: {
endpoint: "queue", endpoint: "queue",
validate: [
"queue"
]
}, },
}, },
}; };

View File

@ -1,12 +1,8 @@
import { useTranslation } from "next-i18next";
import Container from "components/services/widget/container"; import Container from "components/services/widget/container";
import Block from "components/services/widget/block"; import Block from "components/services/widget/block";
import useWidgetAPI from "utils/proxy/use-widget-api"; import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) { export default function Component({ service }) {
const { t } = useTranslation();
const { widget } = service; const { widget } = service;
const { data: wantedData, error: wantedError } = useWidgetAPI(widget, "wanted/missing"); const { data: wantedData, error: wantedError } = useWidgetAPI(widget, "wanted/missing");
@ -14,7 +10,8 @@ export default function Component({ service }) {
const { data: seriesData, error: seriesError } = useWidgetAPI(widget, "series"); const { data: seriesData, error: seriesError } = useWidgetAPI(widget, "series");
if (wantedError || queuedError || seriesError) { if (wantedError || queuedError || seriesError) {
return <Container error={t("widget.api_error")} />; const finalError = wantedError ?? queuedError ?? seriesError;
return <Container error={finalError} />;
} }
if (!wantedData || !queuedData || !seriesData) { if (!wantedData || !queuedData || !seriesData) {

View File

@ -11,12 +11,21 @@ const widget = {
map: (data) => ({ map: (data) => ({
total: asJson(data).length, total: asJson(data).length,
}), }),
validate: [
"total"
]
}, },
queue: { queue: {
endpoint: "queue", endpoint: "queue",
validate: [
"totalRecords"
]
}, },
"wanted/missing": { "wanted/missing": {
endpoint: "wanted/missing", endpoint: "wanted/missing",
validate: [
"totalRecords"
]
}, },
}, },
}; };

View File

@ -11,8 +11,8 @@ export default function Component({ service }) {
const { data: speedtestData, error: speedtestError } = useWidgetAPI(widget, "speedtest/latest"); const { data: speedtestData, error: speedtestError } = useWidgetAPI(widget, "speedtest/latest");
if (speedtestError || (speedtestData && !speedtestData.data)) { if (speedtestError) {
return <Container error={t("widget.api_error")} />; return <Container error={speedtestError} />;
} }
if (!speedtestData) { if (!speedtestData) {

View File

@ -7,6 +7,9 @@ const widget = {
mappings: { mappings: {
"speedtest/latest": { "speedtest/latest": {
endpoint: "speedtest/latest", endpoint: "speedtest/latest",
validate: [
"data"
]
}, },
}, },
}; };

View File

@ -12,7 +12,7 @@ export default function Component({ service }) {
const { data: statsData, error: statsError } = useWidgetAPI(widget, "status"); const { data: statsData, error: statsError } = useWidgetAPI(widget, "status");
if (statsError) { if (statsError) {
return <Container error={t("widget.api_error")} />; return <Container error={statsError} />;
} }
if (!statsData) { if (!statsData) {

View File

@ -7,6 +7,11 @@ const widget = {
mappings: { mappings: {
status: { status: {
endpoint: "status", endpoint: "status",
validate: [
"numActiveSessions",
"numConnections",
"bytesProxied"
]
}, },
}, },
}; };

View File

@ -1,11 +1,10 @@
/* eslint-disable camelcase */ /* eslint-disable camelcase */
import useSWR from "swr";
import { useTranslation } from "next-i18next"; import { useTranslation } from "next-i18next";
import { BsFillPlayFill, BsPauseFill, BsCpu, BsFillCpuFill } from "react-icons/bs"; import { BsFillPlayFill, BsPauseFill, BsCpu, BsFillCpuFill } from "react-icons/bs";
import { MdOutlineSmartDisplay, MdSmartDisplay } from "react-icons/md"; import { MdOutlineSmartDisplay, MdSmartDisplay } from "react-icons/md";
import Container from "components/services/widget/container"; import Container from "components/services/widget/container";
import { formatProxyUrl } from "utils/proxy/api-helpers"; import useWidgetAPI from "utils/proxy/use-widget-api";
function millisecondsToTime(milliseconds) { function millisecondsToTime(milliseconds) {
const seconds = Math.floor((milliseconds / 1000) % 60); const seconds = Math.floor((milliseconds / 1000) % 60);
@ -119,12 +118,12 @@ export default function Component({ service }) {
const { widget } = service; const { widget } = service;
const { data: activityData, error: activityError } = useSWR(formatProxyUrl(widget, "get_activity"), { const { data: activityData, error: activityError } = useWidgetAPI(widget, "get_activity", {
refreshInterval: 5000, refreshInterval: 5000,
}); });
if (activityError) { if (activityError) {
return <Container error={t("widget.api_error")} />; return <Container error={activityError} />;
} }
if (!activityData) { if (!activityData) {

View File

@ -1,18 +1,14 @@
import { useTranslation } from "next-i18next";
import Container from "components/services/widget/container"; import Container from "components/services/widget/container";
import Block from "components/services/widget/block"; import Block from "components/services/widget/block";
import useWidgetAPI from "utils/proxy/use-widget-api"; import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) { export default function Component({ service }) {
const { t } = useTranslation();
const { widget } = service; const { widget } = service;
const { data: traefikData, error: traefikError } = useWidgetAPI(widget, "overview"); const { data: traefikData, error: traefikError } = useWidgetAPI(widget, "overview");
if (traefikError) { if (traefikError) {
return <Container error={t("widget.api_error")} />; return <Container error={traefikError} />;
} }
if (!traefikData) { if (!traefikData) {

View File

@ -7,6 +7,9 @@ const widget = {
mappings: { mappings: {
overview: { overview: {
endpoint: "overview", endpoint: "overview",
validate: [
"http"
]
}, },
}, },
}; };

View File

@ -12,7 +12,7 @@ export default function Component({ service }) {
const { data: torrentData, error: torrentError } = useWidgetAPI(widget); const { data: torrentData, error: torrentError } = useWidgetAPI(widget);
if (torrentError) { if (torrentError) {
return <Container error={t("widget.api_error")} />; return <Container error={torrentError} />;
} }
if (!torrentData) { if (!torrentData) {

View File

@ -68,6 +68,7 @@ export default async function transmissionProxyHandler(req, res) {
if (status !== 200) { if (status !== 200) {
logger.error("Error getting data from Transmission: %d. Data: %s", status, data); logger.error("Error getting data from Transmission: %d. Data: %s", status, data);
return res.status(500).send({error: {message:"Error getting data from Transmission", url, data}});
} }
if (contentType) res.setHeader("Content-Type", contentType); if (contentType) res.setHeader("Content-Type", contentType);

View File

@ -42,7 +42,8 @@ export default function Component({ service }) {
const { data: statusData, error: statusError } = useWidgetAPI(widget, "status"); const { data: statusData, error: statusError } = useWidgetAPI(widget, "status");
if (alertError || statusError) { if (alertError || statusError) {
return <Container error={t("widget.api_error")} />; const finalError = alertError ?? statusError;
return <Container error={finalError} />;
} }
if (!alertData || !statusData) { if (!alertData || !statusData) {

View File

@ -14,6 +14,10 @@ const widget = {
}, },
status: { status: {
endpoint: "system/info", endpoint: "system/info",
validate: [
"loadavg",
"uptime_seconds"
]
}, },
}, },
}; };

View File

@ -15,7 +15,8 @@ export default function Component({ service }) {
const { data: playlistsData, error: playlistsError } = useWidgetAPI(widget, "playlists"); const { data: playlistsData, error: playlistsError } = useWidgetAPI(widget, "playlists");
if (downloadsError || videosError || channelsError || playlistsError) { if (downloadsError || videosError || channelsError || playlistsError) {
return <Container error={t("widget.api_error")} />; const finalError = downloadsError ?? videosError ?? channelsError ?? playlistsError;
return <Container error={finalError} />;
} }
if (!downloadsData || !videosData || !channelsData || !playlistsData) { if (!downloadsData || !videosData || !channelsData || !playlistsData) {

View File

@ -7,15 +7,27 @@ const widget = {
mappings: { mappings: {
downloads: { downloads: {
endpoint: "download", endpoint: "download",
validate: [
"paginate",
]
}, },
videos: { videos: {
endpoint: "video", endpoint: "video",
validate: [
"paginate",
]
}, },
channels: { channels: {
endpoint: "channel", endpoint: "channel",
validate: [
"paginate",
]
}, },
playlists: { playlists: {
endpoint: "playlist", endpoint: "playlist",
validate: [
"paginate",
]
}, },
}, },
}; };

View File

@ -11,8 +11,8 @@ export default function Component({ service }) {
const { data: statsData, error: statsError } = useWidgetAPI(widget, "stat/sites"); const { data: statsData, error: statsError } = useWidgetAPI(widget, "stat/sites");
if (statsError || statsData?.error) { if (statsError) {
return <Container error={t("widget.api_error")} />; return <Container error={statsError} />;
} }
const defaultSite = statsData?.data?.find(s => s.name === "default"); const defaultSite = statsData?.data?.find(s => s.name === "default");

View File

@ -74,7 +74,7 @@ export default async function unifiProxyHandler(req, res) {
// don't make two requests each time data from Unifi is required // don't make two requests each time data from Unifi is required
[status, contentType, data, responseHeaders] = await httpProxy(widget.url); [status, contentType, data, responseHeaders] = await httpProxy(widget.url);
prefix = ""; prefix = "";
if (responseHeaders["x-csrf-token"]) { if (responseHeaders?.["x-csrf-token"]) {
prefix = udmpPrefix; prefix = udmpPrefix;
} }
cache.put(prefixCacheKey, prefix); cache.put(prefixCacheKey, prefix);
@ -88,13 +88,14 @@ export default async function unifiProxyHandler(req, res) {
setCookieHeader(url, params); setCookieHeader(url, params);
[status, contentType, data, responseHeaders] = await httpProxy(url, params); [status, contentType, data, responseHeaders] = await httpProxy(url, params);
if (status === 401) { if (status === 401) {
logger.debug("Unifi isn't logged in or rejected the reqeust, attempting login."); logger.debug("Unifi isn't logged in or rejected the reqeust, attempting login.");
[status, contentType, data, responseHeaders] = await login(widget); [status, contentType, data, responseHeaders] = await login(widget);
if (status !== 200) { if (status !== 200) {
logger.error("HTTP %d logging in to Unifi. Data: %s", status, data); logger.error("HTTP %d logging in to Unifi. Data: %s", status, data);
return res.status(status).end(data); return res.status(status).json({error: {message: `HTTP Error ${status}`, url, data}});
} }
const json = JSON.parse(data.toString()); const json = JSON.parse(data.toString());
@ -112,6 +113,7 @@ export default async function unifiProxyHandler(req, res) {
if (status !== 200) { if (status !== 200) {
logger.error("HTTP %d getting data from Unifi endpoint %s. Data: %s", status, url.href, data); logger.error("HTTP %d getting data from Unifi endpoint %s. Data: %s", status, url.href, data);
return res.status(status).json({error: {message: `HTTP Error ${status}`, url, data}});
} }
if (contentType) res.setHeader("Content-Type", contentType); if (contentType) res.setHeader("Content-Type", contentType);

View File

@ -12,8 +12,8 @@ export default function Component({ service }) {
const { data: watchData, error: watchError } = useWidgetAPI(widget, "watchtower"); const { data: watchData, error: watchError } = useWidgetAPI(widget, "watchtower");
if (watchError || !watchData) { if (watchError) {
return <Container error={t("widget.api_error")} />; return <Container error={watchError} />;
} }
if (!watchData) { if (!watchData) {

View File

@ -33,15 +33,16 @@ export default async function watchtowerProxyHandler(req, res) {
if (status !== 200 || !data) { if (status !== 200 || !data) {
logger.error("Error getting data from WatchTower: %d. Data: %s", status, data); logger.error("Error getting data from WatchTower: %d. Data: %s", status, data);
return res.status(status).json({error: {message: `HTTP Error ${status}`, url, data}});
} }
const cleanData = data.toString().split("\n").filter(s => s.startsWith("watchtower")) const cleanData = data.toString().split("\n").filter(s => s.startsWith("watchtower"));
const jsonRes = {} const jsonRes = {}
cleanData.map(e => e.split(" ")).forEach(strArray => { cleanData.map(e => e.split(" ")).forEach(strArray => {
const [key, value] = strArray const [key, value] = strArray
jsonRes[key] = value jsonRes[key] = value
}) });
if (contentType) res.setHeader("Content-Type", contentType); if (contentType) res.setHeader("Content-Type", contentType);
return res.status(status).send(jsonRes); return res.status(status).send(jsonRes);