Merge branch 'main' into kubernetes

This commit is contained in:
James Wynn 2022-12-31 11:19:42 -06:00
commit e15ba1c82c
86 changed files with 3221 additions and 583 deletions

View file

@ -1,9 +1,9 @@
import Image from "next/future/image";
export default function ResolvedIcon({ icon }) {
export default function ResolvedIcon({ icon, width = 32, height = 32 }) {
// direct or relative URLs
if (icon.startsWith("http") || icon.startsWith("/")) {
return <Image src={`${icon}`} width={32} height={32} alt="logo" />;
return <Image src={`${icon}`} width={width} height={height} alt="logo" />;
}
// mdi- prefixed, material design icons
@ -12,8 +12,8 @@ export default function ResolvedIcon({ icon }) {
return (
<div
style={{
width: 32,
height: 32,
width,
height,
maxWidth: '100%',
maxHeight: '100%',
background: "linear-gradient(180deg, rgb(var(--color-logo-start)), rgb(var(--color-logo-stop)))",
@ -29,8 +29,8 @@ export default function ResolvedIcon({ icon }) {
return (
<Image
src={`https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/${iconName}.png`}
width={32}
height={32}
width={width}
height={height}
alt="logo"
/>
);

View file

@ -3,8 +3,6 @@ import useSWR from "swr";
import { compareVersions } from "compare-versions";
import { MdNewReleases } from "react-icons/md";
import cachedFetch from "utils/proxy/cached-fetch";
export default function Version() {
const { t, i18n } = useTranslation();
@ -12,9 +10,7 @@ export default function Version() {
const revision = process.env.NEXT_PUBLIC_REVISION?.length ? process.env.NEXT_PUBLIC_REVISION : "dev";
const version = process.env.NEXT_PUBLIC_VERSION?.length ? process.env.NEXT_PUBLIC_VERSION : "dev";
const cachedFetcher = (resource) => cachedFetch(resource, 5).then((res) => res.json());
const { data: releaseData } = useSWR("https://api.github.com/repos/benphelps/homepage/releases", cachedFetcher);
const { data: releaseData } = useSWR("/api/releases");
// use Intl.DateTimeFormat to format the date
const formatDate = (date) => {
@ -48,7 +44,7 @@ export default function Version() {
</span>
{version === "main" || version === "dev" || version === "nightly"
? null
: releaseData &&
: releaseData && latestRelease &&
compareVersions(latestRelease.tag_name, version) > 0 && (
<a
href={latestRelease.html_url}

View file

@ -1,56 +1,62 @@
export default function Logo() {
import ResolvedIcon from "components/resolvedicon"
export default function Logo({ options }) {
return (
<div className="w-12 h-12 flex flex-row items-center align-middle mr-3 self-center">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 1024 1024"
style={{
enableBackground: "new 0 0 1024 1024",
}}
xmlSpace="preserve"
className="w-full h-full"
>
<style>
{
".st0{display:none}.st3{stroke-linecap:square}.st3,.st4{fill:none;stroke:#fff;stroke-miterlimit:10}.st6{display:inline;fill:#333}.st7{fill:#fff}"
}
</style>
<g id="Icon">
<path
d="M771.9 191c27.7 0 50.1 26.5 50.1 59.3v186.4l-100.2.3V250.3c0-32.8 22.4-59.3 50.1-59.3z"
style={{
fill: "rgba(var(--color-logo-start))",
}}
/>
<linearGradient
id="homepage_logo_gradient"
gradientUnits="userSpaceOnUse"
x1={200.746}
y1={225.015}
x2={764.986}
y2={789.255}
>
<stop
offset={0}
{options.icon ?
<ResolvedIcon icon={options.icon} width={48} height={48} /> :
// fallback to homepage logo
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 1024 1024"
style={{
enableBackground: "new 0 0 1024 1024",
}}
xmlSpace="preserve"
className="w-full h-full"
>
<style>
{
".st0{display:none}.st3{stroke-linecap:square}.st3,.st4{fill:none;stroke:#fff;stroke-miterlimit:10}.st6{display:inline;fill:#333}.st7{fill:#fff}"
}
</style>
<g id="Icon">
<path
d="M771.9 191c27.7 0 50.1 26.5 50.1 59.3v186.4l-100.2.3V250.3c0-32.8 22.4-59.3 50.1-59.3z"
style={{
stopColor: "rgba(var(--color-logo-start))",
fill: "rgba(var(--color-logo-start))",
}}
/>
<stop
offset={1}
<linearGradient
id="homepage_logo_gradient"
gradientUnits="userSpaceOnUse"
x1={200.746}
y1={225.015}
x2={764.986}
y2={789.255}
>
<stop
offset={0}
style={{
stopColor: "rgba(var(--color-logo-start))",
}}
/>
<stop
offset={1}
style={{
stopColor: "rgba(var(--color-logo-stop))",
}}
/>
</linearGradient>
<path
d="M721.8 250.3c0-32.7 22.4-59.3 50.1-59.3H253.1c-27.7 0-50.1 26.5-50.1 59.3v582.2l90.2-75.7-.1-130.3H375v61.8l88-73.8 258.8 217.9V250.6"
style={{
stopColor: "rgba(var(--color-logo-stop))",
fill: "url(#homepage_logo_gradient)",
}}
/>
</linearGradient>
<path
d="M721.8 250.3c0-32.7 22.4-59.3 50.1-59.3H253.1c-27.7 0-50.1 26.5-50.1 59.3v582.2l90.2-75.7-.1-130.3H375v61.8l88-73.8 258.8 217.9V250.6"
style={{
fill: "url(#homepage_logo_gradient)",
}}
/>
</g>
</svg>
</g>
</svg>
}
</div>
);
)
}

View file

@ -38,7 +38,7 @@ export default function Cpu({ expanded }) {
<div className="pr-1">{t("resources.load")}</div>
</div>
)}
<UsageBar percent={100} />
<UsageBar percent={0} />
</div>
</div>
);

View file

@ -38,7 +38,7 @@ export default function Disk({ options, expanded }) {
<div className="pr-1">{t("resources.total")}</div>
</span>
)}
<UsageBar percent={100} />
<UsageBar percent={0} />
</div>
</div>
);

View file

@ -38,7 +38,7 @@ export default function Memory({ expanded }) {
<div className="pr-1">{t("resources.total")}</div>
</span>
)}
<UsageBar percent={100} />
<UsageBar percent={0} />
</div>
</div>
);

View file

@ -14,7 +14,8 @@ export default async function handler(req, res) {
}
try {
const docker = new Docker(getDockerArguments(containerServer));
const dockerArgs = getDockerArguments(containerServer);
const docker = new Docker(dockerArgs.conn);
const containers = await docker.listContainers({
all: true,
});
@ -31,18 +32,44 @@ export default async function handler(req, res) {
const containerNames = containers.map((container) => container.Names[0].replace(/^\//, ""));
const containerExists = containerNames.includes(containerName);
if (!containerExists) {
res.status(200).send({
error: "not found",
if (containerExists) {
const container = docker.getContainer(containerName);
const stats = await container.stats({ stream: false });
res.status(200).json({
stats,
});
return;
}
const container = docker.getContainer(containerName);
const stats = await container.stats({ stream: false });
// Try with a service deployed in Docker Swarm, if enabled
if (dockerArgs.swarm) {
const tasks = await docker.listTasks({
filters: {
service: [containerName],
// A service can have several offline containers, so we only look for an active one.
"desired-state": ["running"],
},
})
.catch(() => []);
res.status(200).json({
stats,
// For now we are only interested in the first one (in case replicas > 1).
// TODO: Show the result for all replicas/containers?
const taskContainerId = tasks.at(0)?.Status?.ContainerStatus?.ContainerID;
if (taskContainerId) {
const container = docker.getContainer(taskContainerId);
const stats = await container.stats({ stream: false });
res.status(200).json({
stats,
});
return;
}
}
res.status(200).send({
error: "not found",
});
} catch {
res.status(500).send({

View file

@ -13,7 +13,8 @@ export default async function handler(req, res) {
}
try {
const docker = new Docker(getDockerArguments(containerServer));
const dockerArgs = getDockerArguments(containerServer);
const docker = new Docker(dockerArgs.conn);
const containers = await docker.listContainers({
all: true,
});
@ -29,18 +30,43 @@ export default async function handler(req, res) {
const containerNames = containers.map((container) => container.Names[0].replace(/^\//, ""));
const containerExists = containerNames.includes(containerName);
if (!containerExists) {
return res.status(200).send({
error: "not found",
if (containerExists) {
const container = docker.getContainer(containerName);
const info = await container.inspect();
return res.status(200).json({
status: info.State.Status,
health: info.State.Health?.Status,
});
}
const container = docker.getContainer(containerName);
const info = await container.inspect();
if (dockerArgs.swarm) {
const tasks = await docker.listTasks({
filters: {
service: [containerName],
// A service can have several offline containers, we only look for an active one.
"desired-state": ["running"],
},
})
.catch(() => []);
return res.status(200).json({
status: info.State.Status,
health: info.State.Health?.Status
// For now we are only interested in the first one (in case replicas > 1).
// TODO: Show the result for all replicas/containers?
const taskContainerId = tasks.at(0)?.Status?.ContainerStatus?.ContainerID;
if (taskContainerId) {
const container = docker.getContainer(taskContainerId);
const info = await container.inspect();
return res.status(200).json({
status: info.State.Status,
health: info.State.Health?.Status,
});
}
}
return res.status(200).send({
error: "not found",
});
} catch {
return res.status(500).send({

View file

@ -0,0 +1,6 @@
import cachedFetch from "utils/proxy/cached-fetch";
export default async function handler(req, res) {
const releasesURL = "https://api.github.com/repos/benphelps/homepage/releases";
return res.send(await cachedFetch(releasesURL, 5));
}

View file

@ -56,9 +56,12 @@ export async function servicesResponse() {
try {
discoveredDockerServices = cleanServiceGroups(await servicesFromDocker());
if (discoveredDockerServices?.length === 0) {
console.debug("No containers were found with homepage labels.");
}
} catch (e) {
console.error("Failed to discover services, please check docker.yaml for errors or remove example entries.");
if (e) console.error(e);
if (e) console.error(e.toString());
discoveredDockerServices = [];
}
@ -66,7 +69,7 @@ export async function servicesResponse() {
discoveredKubernetesServices = cleanServiceGroups(await servicesFromKubernetes());
} catch (e) {
console.error("Failed to discover services, please check kubernetes.yaml for errors or remove example entries.");
if (e) console.error(e);
if (e) console.error(e.toString());
discoveredKubernetesServices = [];
}
@ -74,7 +77,7 @@ export async function servicesResponse() {
configuredServices = cleanServiceGroups(await servicesFromConfig());
} catch (e) {
console.error("Failed to load services.yaml, please check for errors");
if (e) console.error(e);
if (e) console.error(e.toString());
configuredServices = [];
}
@ -82,7 +85,7 @@ export async function servicesResponse() {
initialSettings = await getSettings();
} catch (e) {
console.error("Failed to load settings.yaml, please check for errors");
if (e) console.error(e);
if (e) console.error(e.toString());
initialSettings = {};
}

View file

@ -22,11 +22,14 @@ export default function getDockerArguments(server) {
if (servers[server]) {
if (servers[server].socket) {
return { socketPath: servers[server].socket };
return { conn: { socketPath: servers[server].socket }, swarm: !!servers[server].swarm };
}
if (servers[server].host) {
return { host: servers[server].host, port: servers[server].port || null };
return {
conn: { host: servers[server].host, port: servers[server].port || null },
swarm: !!servers[server].swarm,
};
}
return servers[server];

View file

@ -49,36 +49,41 @@ export async function servicesFromDocker() {
const serviceServers = await Promise.all(
Object.keys(servers).map(async (serverName) => {
const docker = new Docker(getDockerArguments(serverName));
const containers = await docker.listContainers({
all: true,
});
// bad docker connections can result in a <Buffer ...> object?
// in any case, this ensures the result is the expected array
if (!Array.isArray(containers)) {
return [];
}
const discovered = containers.map((container) => {
let constructedService = null;
Object.keys(container.Labels).forEach((label) => {
if (label.startsWith("homepage.")) {
if (!constructedService) {
constructedService = {
container: container.Names[0].replace(/^\//, ""),
server: serverName,
};
}
shvl.set(constructedService, label.replace("homepage.", ""), container.Labels[label]);
}
try {
const docker = new Docker(getDockerArguments(serverName).conn);
const containers = await docker.listContainers({
all: true,
});
return constructedService;
});
// bad docker connections can result in a <Buffer ...> object?
// in any case, this ensures the result is the expected array
if (!Array.isArray(containers)) {
return [];
}
return { server: serverName, services: discovered.filter((filteredService) => filteredService) };
const discovered = containers.map((container) => {
let constructedService = null;
Object.keys(container.Labels).forEach((label) => {
if (label.startsWith("homepage.")) {
if (!constructedService) {
constructedService = {
container: container.Names[0].replace(/^\//, ""),
server: serverName,
};
}
shvl.set(constructedService, label.replace("homepage.", ""), container.Labels[label]);
}
});
return constructedService;
});
return { server: serverName, services: discovered.filter((filteredService) => filteredService) };
} catch (e) {
// a server failed, but others may succeed
return { server: serverName, services: [] };
}
})
);

View file

@ -7,7 +7,7 @@ import widgets from "widgets/widgets";
const logger = createLogger("credentialedProxyHandler");
export default async function credentialedProxyHandler(req, res) {
export default async function credentialedProxyHandler(req, res, map) {
const { group, service, endpoint } = req.query;
if (group && service) {
@ -36,6 +36,8 @@ export default async function credentialedProxyHandler(req, res) {
headers["X-API-Token"] = `${widget.key}`;
} else if (widget.type === "tubearchivist") {
headers.Authorization = `Token ${widget.key}`;
} else if (widget.type === "miniflux") {
headers["X-Auth-Token"] = `${widget.key}`;
} else {
headers["X-API-Key"] = `${widget.key}`;
}
@ -47,6 +49,8 @@ export default async function credentialedProxyHandler(req, res) {
headers,
});
let resultData = data;
if (status === 204 || status === 304) {
return res.status(status).end();
}
@ -59,8 +63,12 @@ export default async function credentialedProxyHandler(req, res) {
return res.status(500).json({error: {message: "Invalid data", url, data}});
}
if (status === 200 && map) {
resultData = map(data);
}
if (contentType) res.setHeader("Content-Type", contentType);
return res.status(status).send(data);
return res.status(status).send(resultData);
}
}

View file

@ -8,7 +8,7 @@ const components = {
changedetectionio: dynamic(() => import("./changedetectionio/component")),
coinmarketcap: dynamic(() => import("./coinmarketcap/component")),
deluge: dynamic(() => import("./deluge/component")),
diskstation: dynamic(() => import("./diskstation/component")),
downloadstation: dynamic(() => import("./downloadstation/component")),
docker: dynamic(() => import("./docker/component")),
kubernetes: dynamic(() => import("./kubernetes/component")),
emby: dynamic(() => import("./emby/component")),
@ -22,10 +22,15 @@ const components = {
jellyseerr: dynamic(() => import("./jellyseerr/component")),
lidarr: dynamic(() => import("./lidarr/component")),
mastodon: dynamic(() => import("./mastodon/component")),
miniflux: dynamic(() => import("./miniflux/component")),
mikrotik: dynamic(() => import("./mikrotik/component")),
navidrome: dynamic(() => import("./navidrome/component")),
nextdns: dynamic(() => import("./nextdns/component")),
npm: dynamic(() => import("./npm/component")),
nzbget: dynamic(() => import("./nzbget/component")),
omada: dynamic(() => import("./omada/component")),
ombi: dynamic(() => import("./ombi/component")),
opnsense: dynamic(() => import("./opnsense/component")),
overseerr: dynamic(() => import("./overseerr/component")),
paperlessngx: dynamic(() => import("./paperlessngx/component")),
pihole: dynamic(() => import("./pihole/component")),
@ -44,12 +49,14 @@ const components = {
speedtest: dynamic(() => import("./speedtest/component")),
strelaysrv: dynamic(() => import("./strelaysrv/component")),
tautulli: dynamic(() => import("./tautulli/component")),
tdarr: dynamic(() => import("./tdarr/component")),
traefik: dynamic(() => import("./traefik/component")),
transmission: dynamic(() => import("./transmission/component")),
tubearchivist: dynamic(() => import("./tubearchivist/component")),
truenas: dynamic(() => import("./truenas/component")),
unifi: dynamic(() => import("./unifi/component")),
watchtower: dynamic(() => import("./watchtower/component")),
xteve: dynamic(() => import("./xteve/component")),
};
export default components;

View file

@ -17,10 +17,10 @@ export default function Component({ service }) {
if (!tasks) {
return (
<Container service={service}>
<Block label="diskstation.leech" />
<Block label="diskstation.download" />
<Block label="diskstation.seed" />
<Block label="diskstation.upload" />
<Block label="downloadstation.leech" />
<Block label="downloadstation.download" />
<Block label="downloadstation.seed" />
<Block label="downloadstation.upload" />
</Container>
);
}
@ -32,10 +32,10 @@ export default function Component({ service }) {
return (
<Container service={service}>
<Block label="diskstation.leech" value={t("common.number", { value: leech })} />
<Block label="diskstation.download" value={t("common.bitrate", { value: rateDl })} />
<Block label="diskstation.seed" value={t("common.number", { value: completed })} />
<Block label="diskstation.upload" value={t("common.bitrate", { value: rateUl })} />
<Block label="downloadstation.leech" value={t("common.number", { value: leech })} />
<Block label="downloadstation.download" value={t("common.bitrate", { value: rateDl })} />
<Block label="downloadstation.seed" value={t("common.number", { value: completed })} />
<Block label="downloadstation.upload" value={t("common.bitrate", { value: rateUl })} />
</Container>
);
}

View file

@ -4,7 +4,7 @@ import createLogger from "utils/logger";
import widgets from "widgets/widgets";
import getServiceWidget from "utils/config/service-helpers";
const logger = createLogger("diskstationProxyHandler");
const logger = createLogger("downloadstationProxyHandler");
const authApi = "{url}/webapi/auth.cgi?api=SYNO.API.Auth&version=2&method=login&account={username}&passwd={password}&session=DownloadStation&format=cookie"
async function login(widget) {
@ -34,7 +34,7 @@ async function login(widget) {
return [status, contentType, data];
}
export default async function diskstationProxyHandler(req, res) {
export default async function downloadstationProxyHandler(req, res) {
const { group, service, endpoint } = req.query;
if (!group || !service) {
@ -56,7 +56,7 @@ export default async function diskstationProxyHandler(req, res) {
const json = JSON.parse(data.toString());
if (json?.success !== true) {
logger.debug("Logging in to DiskStation");
logger.debug("Logging in to DownloadStation");
[status, contentType, data] = await login(widget);
if (status !== 200) {
return res.status(status).end(data)

View file

@ -1,8 +1,8 @@
import diskstationProxyHandler from "./proxy";
import downloadstationProxyHandler from "./proxy";
const widget = {
api: "{url}/webapi/DownloadStation/task.cgi?api=SYNO.DownloadStation.Task&version=1&method={endpoint}",
proxyHandler: diskstationProxyHandler,
proxyHandler: downloadstationProxyHandler,
mappings: {
"list": {

View file

@ -10,7 +10,7 @@ const proxyName = "homebridgeProxyHandler";
const sessionTokenCacheKey = `${proxyName}__sessionToken`;
const logger = createLogger(proxyName);
async function login(widget) {
async function login(widget, service) {
const endpoint = "auth/login";
const api = widgets?.[widget.type]?.api
const loginUrl = new URL(formatApiCall(api, { endpoint, ...widget }));
@ -26,7 +26,7 @@ async function login(widget) {
try {
const { access_token: accessToken, expires_in: expiresIn } = JSON.parse(data.toString());
cache.put(sessionTokenCacheKey, accessToken, (expiresIn * 1000) - 5 * 60 * 1000); // expiresIn (s) - 5m
cache.put(`${sessionTokenCacheKey}.${service}`, accessToken, (expiresIn * 1000) - 5 * 60 * 1000); // expiresIn (s) - 5m
return { accessToken };
} catch (e) {
logger.error("Unable to login to Homebridge API: %s", e);
@ -35,10 +35,11 @@ async function login(widget) {
return { accessToken: false };
}
async function apiCall(widget, endpoint) {
async function apiCall(widget, endpoint, service) {
const key = `${sessionTokenCacheKey}.${service}`;
const headers = {
"content-type": "application/json",
"Authorization": `Bearer ${cache.get(sessionTokenCacheKey)}`,
"Authorization": `Bearer ${cache.get(key)}`,
}
const url = new URL(formatApiCall(widgets[widget.type].api, { endpoint, ...widget }));
@ -51,7 +52,7 @@ async function apiCall(widget, endpoint) {
if (status === 401) {
logger.debug("Homebridge API rejected the request, attempting to obtain new session token");
const { accessToken } = login(widget);
const { accessToken } = login(widget, service);
headers.Authorization = `Bearer ${accessToken}`;
// retry the request, now with the new session token
@ -83,14 +84,14 @@ export default async function homebridgeProxyHandler(req, res) {
return res.status(400).json({ error: "Invalid proxy service type" });
}
if (!cache.get(sessionTokenCacheKey)) {
await login(widget);
if (!cache.get(`${sessionTokenCacheKey}.${service}`)) {
await login(widget, service);
}
const { data: statusData } = await apiCall(widget, "status/homebridge");
const { data: versionData } = await apiCall(widget, "status/homebridge-version");
const { data: childBridgeData } = await apiCall(widget, "status/homebridge/child-bridges");
const { data: pluginsData } = await apiCall(widget, "plugins");
const { data: statusData } = await apiCall(widget, "status/homebridge", service);
const { data: versionData } = await apiCall(widget, "status/homebridge-version", service);
const { data: childBridgeData } = await apiCall(widget, "status/homebridge/child-bridges", service);
const { data: pluginsData } = await apiCall(widget, "plugins", service);
return res.status(200).send({
status: statusData?.status,

View file

@ -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: statsData, error: statsError } = useWidgetAPI(widget, "system");
const { data: leasesData, error: leasesError } = useWidgetAPI(widget, "leases");
if (statsError || leasesError) {
const finalError = statsError ?? leasesError;
return <Container error={ finalError } />;
}
if (!statsData || !leasesData) {
return (
<Container service={service}>
<Block label="mikrotik.uptime" />
<Block label="mikrotik.cpuLoad" />
<Block label="mikrotik.memoryUsed" />
<Block label="mikrotik.numberOfLeases" />
</Container>
);
}
const memoryUsed = 100 - (statsData['free-memory'] / statsData['total-memory'])*100
const numberOfLeases = leasesData.length
return (
<Container service={service}>
<Block label="mikrotik.uptime" value={ statsData.uptime } />
<Block label="mikrotik.cpuLoad" value={t("common.percent", { value: statsData['cpu-load'] })} />
<Block label="mikrotik.memoryUsed" value={t("common.percent", { value: memoryUsed })} />
<Block label="mikrotik.numberOfLeases" value={t("common.number", { value: numberOfLeases })} />
</Container>
);
}

View file

@ -0,0 +1,24 @@
import genericProxyHandler from "utils/proxy/handlers/generic";
const widget = {
api: "{url}/rest/{endpoint}",
proxyHandler: genericProxyHandler,
mappings: {
system: {
endpoint: "system/resource",
validate: [
"cpu-load",
"free-memory",
"total-memory",
"uptime"
]
},
leases: {
endpoint: "ip/dhcp-server/lease",
}
},
};
export default widget;

View file

@ -0,0 +1,33 @@
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: minifluxData, error: minifluxError } = useWidgetAPI(widget, "counters");
if (minifluxError) {
return <Container error={minifluxError} />;
}
if (!minifluxData) {
return (
<Container service={service}>
<Block label="miniflux.unread" />
<Block label="miniflux.read" />
</Container>
);
}
return (
<Container service={service}>
<Block label="miniflux.unread" value={t("common.number", { value: minifluxData.unread })} />
<Block label="miniflux.read" value={t("common.number", { value: minifluxData.read })} />
</Container>
);
}

View file

@ -0,0 +1,19 @@
import { asJson } from "utils/proxy/api-helpers";
import credentialedProxyHandler from "utils/proxy/handlers/credentialed";
const widget = {
api: "{url}/v1/{endpoint}",
proxyHandler: credentialedProxyHandler,
mappings: {
counters: {
endpoint: "feeds/counters",
map: (data) => ({
read: Object.values(asJson(data).reads).reduce((acc, i) => acc + i, 0),
unread: Object.values(asJson(data).unreads).reduce((acc, i) => acc + i, 0)
}),
},
}
};
export default widget;

View file

@ -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: nextdnsData, error: nextdnsError } = useWidgetAPI(widget, "analytics/status");
if (nextdnsError) {
return <Container error={nextdnsError} />;
}
if (!nextdnsData) {
return (
<Container service={service}>
<Block key="status" label="widget.status" value={t("nextdns.wait")} />
</Container>
);
}
if (!nextdnsData?.data?.length) {
return (
<Container service={service}>
<Block key="status" label="widget.status" value={t("nextdns.no_devices")} />
</Container>
);
}
return (
<Container service={service}>
{nextdnsData.data.map(d => <Block key={d.status} label={d.status} value={t("common.number", { value: d.queries })} />)}
</Container>
);
}

View file

@ -0,0 +1,17 @@
import credentialedProxyHandler from "utils/proxy/handlers/credentialed";
const widget = {
api: "https://api.nextdns.io/profiles/{profile}/{endpoint}",
proxyHandler: credentialedProxyHandler,
mappings: {
"analytics/status": {
endpoint: "analytics/status",
validate: [
"data",
]
},
},
};
export default widget;

View file

@ -10,7 +10,7 @@ const proxyName = "npmProxyHandler";
const tokenCacheKey = `${proxyName}__token`;
const logger = createLogger(proxyName);
async function login(loginUrl, username, password) {
async function login(loginUrl, username, password, service) {
const authResponse = await httpProxy(loginUrl, {
method: "POST",
body: JSON.stringify({ identity: username, secret: password }),
@ -27,7 +27,7 @@ async function login(loginUrl, username, password) {
if (status === 200) {
const expiration = new Date(data.expires) - Date.now();
cache.put(tokenCacheKey, data.token, expiration - (5 * 60 * 1000)); // expiration -5 minutes
cache.put(`${tokenCacheKey}.${service}`, data.token, expiration - (5 * 60 * 1000)); // expiration -5 minutes
}
} catch (e) {
logger.error(`Error ${status} logging into npm`, authResponse[2]);
@ -53,9 +53,9 @@ export default async function npmProxyHandler(req, res) {
let contentType;
let data;
let token = cache.get(tokenCacheKey);
let token = cache.get(`${tokenCacheKey}.${service}`);
if (!token) {
[status, token] = await login(loginUrl, widget.username, widget.password);
[status, token] = await login(loginUrl, widget.username, widget.password, service);
if (status !== 200) {
logger.debug(`HTTTP ${status} logging into npm api: ${token}`);
return res.status(status).send(token);
@ -72,8 +72,8 @@ export default async function npmProxyHandler(req, res) {
if (status === 403) {
logger.debug(`HTTTP ${status} retrieving data from npm api, logging in and trying again.`);
cache.del(tokenCacheKey);
[status, token] = await login(loginUrl, widget.username, widget.password);
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}`);

View file

@ -0,0 +1,39 @@
import { useTranslation } from "next-i18next";
import useWidgetAPI from "../../utils/proxy/use-widget-api";
import Container from "../../components/services/widget/container";
import Block from "../../components/services/widget/block";
export default function Component({ service }) {
const { t } = useTranslation();
const { widget } = service;
const { data: omadaData, error: omadaAPIError } = useWidgetAPI(widget, {
refreshInterval: 5000,
});
if (omadaAPIError) {
return <Container error={omadaAPIError} />;
}
if (!omadaData) {
return (
<Container service={service}>
<Block label="omada.connectedAp" />
<Block label="omada.activeUser" />
<Block label="omada.alerts" />
</Container>
);
}
return (
<Container service={service}>
<Block label="omada.connectedAp" value={t( "common.number", { value: omadaData.connectedAp})} />
<Block label="omada.activeUser" value={t( "common.number", { value: omadaData.activeUser })} />
<Block label="omada.alerts" value={t( "common.number", { value: omadaData.alerts })} />
{ omadaData.connectedGateways > 0 && <Block label="omada.connectedGateway" value={t("common.number", { value: omadaData.connectedGateways})} /> }
{ omadaData.connectedSwitches > 0 && <Block label="omada.connectedSwitches" value={t("common.number", { value: omadaData.connectedSwitches})} /> }
</Container>
);
}

252
src/widgets/omada/proxy.js Normal file
View file

@ -0,0 +1,252 @@
import { httpProxy } from "utils/proxy/http";
import getServiceWidget from "utils/config/service-helpers";
import createLogger from "utils/logger";
const proxyName = "omadaProxyHandler";
const logger = createLogger(proxyName);
async function login(loginUrl, username, password, controllerVersionMajor) {
const params = {
username,
password
}
if (controllerVersionMajor === 3) {
params.method = "login";
params.params = {
name: username,
password
};
}
// eslint-disable-next-line no-unused-vars
const [status, contentType, data] = await httpProxy(loginUrl, {
method: "POST",
body: JSON.stringify(params),
headers: {
"Content-Type": "application/json",
},
});
return [status, JSON.parse(data.toString())];
}
export default async function omadaProxyHandler(req, res) {
const { group, service } = req.query;
if (group && service) {
const widget = await getServiceWidget(group, service);
if (widget) {
const { url } = widget;
const controllerInfoURL = `${url}/api/info`;
let [status, contentType, data] = await httpProxy(controllerInfoURL, {
headers: {
"Content-Type": "application/json",
},
});
if (status !== 200) {
logger.error("Unable to retrieve Omada controller info");
return res.status(status).json({error: {message: `HTTP Error ${status}`, url: controllerInfoURL, data}});
}
let cId;
let controllerVersion;
try {
cId = JSON.parse(data).result.omadacId;
controllerVersion = JSON.parse(data).result.controllerVer;
} catch (e) {
controllerVersion = "3.2.x"
}
const controllerVersionMajor = parseInt(controllerVersion.split('.')[0], 10)
if (![3,4,5].includes(controllerVersionMajor)) {
return res.status(500).json({error: {message: "Error determining controller version", data}});
}
let loginUrl;
switch (controllerVersionMajor) {
case 3:
loginUrl = `${url}/api/user/login?ajax`;
break;
case 4:
loginUrl = `${url}/api/v2/login`;
break;
case 5:
loginUrl = `${url}/${cId}/api/v2/login`;
break;
default:
break;
}
const [loginStatus, loginResponseData] = await login(loginUrl, widget.username, widget.password, controllerVersionMajor);
if (loginStatus !== 200 || loginResponseData.errorCode > 0) {
return res.status(status).json({error: {message: "Error logging in to Oamda controller", url: loginUrl, data: loginResponseData}});
}
const { token } = loginResponseData.result;
let sitesUrl;
let body = {};
let params = { token };
let headers = { "Csrf-Token": token };
let method = "GET";
switch (controllerVersionMajor) {
case 3:
sitesUrl = `${url}/web/v1/controller?ajax=&token=${token}`;
body = {
"method": "getUserSites",
"params": {
"userName": widget.username
}
};
method = "POST";
break;
case 4:
sitesUrl = `${url}/api/v2/sites?token=${token}&currentPage=1&currentPageSize=1000`;
break;
case 5:
sitesUrl = `${url}/${cId}/api/v2/sites?token=${token}&currentPage=1&currentPageSize=1000`;
break;
default:
break;
}
[status, contentType, data] = await httpProxy(sitesUrl, {
method,
params,
body: JSON.stringify(body),
headers,
});
const sitesResponseData = JSON.parse(data);
if (status !== 200 || sitesResponseData.errorCode > 0) {
logger.debug(`HTTTP ${status} getting sites list: ${sitesResponseData.msg}`);
return res.status(status).json({error: {message: "Error getting sites list", url, data: sitesResponseData}});
}
const site = (controllerVersionMajor === 3) ?
sitesResponseData.result.siteList.find(s => s.name === widget.site):
sitesResponseData.result.data.find(s => s.name === widget.site);
if (!site) {
return res.status(status).json({error: {message: `Site ${widget.site} is not found`, url: sitesUrl, data}});
}
let siteResponseData;
let connectedAp;
let activeUser;
let connectedSwitches;
let connectedGateways;
let alerts;
if (controllerVersionMajor === 3) {
// Omada v3 controller requires switching site
const switchUrl = `${url}/web/v1/controller?ajax=&token=${token}`;
method = "POST";
body = {
method: "switchSite",
params: {
siteName: site.siteName,
userName: widget.username
}
};
headers = { "Content-Type": "application/json" };
params = { token };
[status, contentType, data] = await httpProxy(switchUrl, {
method,
params,
body: JSON.stringify(body),
headers,
});
const switchResponseData = JSON.parse(data);
if (status !== 200 || switchResponseData.errorCode > 0) {
logger.error(`HTTP ${status} getting sites list: ${data}`);
return res.status(status).json({error: {message: "Error switching site", url: switchUrl, data}});
}
const statsUrl = `${url}/web/v1/controller?getGlobalStat=&token=${token}`;
[status, contentType, data] = await httpProxy(statsUrl, {
method,
params,
body: JSON.stringify({
"method": "getGlobalStat",
}),
headers
});
siteResponseData = JSON.parse(data);
if (status !== 200 || siteResponseData.errorCode > 0) {
return res.status(status).json({error: {message: "Error getting stats", url: statsUrl, data}});
}
connectedAp = siteResponseData.result.connectedAp;
activeUser = siteResponseData.result.activeUser;
alerts = siteResponseData.result.alerts;
} else if (controllerVersionMajor === 4 || controllerVersionMajor === 5) {
const siteName = (controllerVersionMajor === 5) ? site.id : site.key;
const siteStatsUrl = (controllerVersionMajor === 4) ?
`${url}/api/v2/sites/${siteName}/dashboard/overviewDiagram?token=${token}&currentPage=1&currentPageSize=1000` :
`${url}/${cId}/api/v2/sites/${siteName}/dashboard/overviewDiagram?token=${token}&currentPage=1&currentPageSize=1000`;
[status, contentType, data] = await httpProxy(siteStatsUrl, {
headers: {
"Csrf-Token": token,
},
});
siteResponseData = JSON.parse(data);
if (status !== 200 || siteResponseData.errorCode > 0) {
logger.debug(`HTTP ${status} getting stats for site ${widget.site} with message ${siteResponseData.msg}`);
return res.status(500).send(data);
}
const alertUrl = (controllerVersionMajor === 4) ?
`${url}/api/v2/sites/${siteName}/alerts/num?token=${token}&currentPage=1&currentPageSize=1000` :
`${url}/${cId}/api/v2/sites/${siteName}/alerts/num?token=${token}&currentPage=1&currentPageSize=1000`;
// eslint-disable-next-line no-unused-vars
[status, contentType, data] = await httpProxy(alertUrl, {
headers: {
"Csrf-Token": token,
},
});
const alertResponseData = JSON.parse(data);
activeUser = siteResponseData.result.totalClientNum;
connectedAp = siteResponseData.result.connectedApNum;
connectedGateways = siteResponseData.result.connectedGatewayNum;
connectedSwitches = siteResponseData.result.connectedSwitchNum;
alerts = alertResponseData.result.alertNum;
}
return res.send(JSON.stringify({
connectedAp,
activeUser,
alerts,
connectedGateways,
connectedSwitches,
}));
}
}
return res.status(400).json({ error: "Invalid proxy service type" });
}

View file

@ -0,0 +1,7 @@
import omadaProxyHandler from "./proxy";
const widget = {
proxyHandler: omadaProxyHandler,
};
export default widget;

View file

@ -0,0 +1,48 @@
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: activityData, error: activityError } = useWidgetAPI(widget, "activity");
const { data: interfaceData, error: interfaceError } = useWidgetAPI(widget, "interface");
if (activityError || interfaceError) {
const finalError = activityError ?? interfaceError;
return <Container error={ finalError } />;
}
if (!activityData || !interfaceData) {
return (
<Container service={service}>
<Block label="opnsense.cpu" />
<Block label="opnsense.memory" />
<Block label="opnsense.wanUpload" />
<Block label="opnsense.wanDownload" />
</Container>
);
}
const cpuIdle = activityData.headers[2].match(/ ([0-9.]+)% idle/)[1];
const cpu = 100 - parseFloat(cpuIdle);
const memory = activityData.headers[3].match(/Mem: (.+) Active,/)[1];
const wanUpload = interfaceData.interfaces.wan['bytes transmitted'];
const wanDownload = interfaceData.interfaces.wan['bytes received'];
return (
<Container service={service}>
<Block label="opnsense.cpu" value={t("common.percent", { value: cpu.toFixed(2) })} />
<Block label="opnsense.memory" value={memory} />
<Block label="opnsense.wanUpload" value={t("common.bytes", { value: wanUpload })} />
<Block label="opnsense.wanDownload" value={t("common.bytes", { value: wanDownload })} />
</Container>
);
}

View file

@ -0,0 +1,24 @@
import genericProxyHandler from "utils/proxy/handlers/generic";
const widget = {
api: "{url}/api/{endpoint}",
proxyHandler: genericProxyHandler,
mappings: {
activity: {
endpoint: "diagnostics/activity/getActivity",
validate: [
"headers"
]
},
interface: {
endpoint: "diagnostics/traffic/interface",
validate: [
"interfaces"
]
}
},
};
export default widget;

View file

@ -1,8 +1,11 @@
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: statsData, error: statsError } = useWidgetAPI(widget, "request/count");
@ -24,10 +27,10 @@ export default function Component({ service }) {
return (
<Container service={service}>
<Block label="overseerr.pending" value={statsData.pending} />
<Block label="overseerr.processing" value={statsData.processing} />
<Block label="overseerr.approved" value={statsData.approved} />
<Block label="overseerr.available" value={statsData.available} />
<Block label="overseerr.pending" value={t("common.number", { value: statsData.pending })} />
<Block label="overseerr.processing" value={t("common.number", { value: statsData.processing })} />
<Block label="overseerr.approved" value={t("common.number", { value: statsData.approved })} />
<Block label="overseerr.available" value={t("common.number", { value: statsData.available })} />
</Container>
);
}

View file

@ -9,7 +9,7 @@ export default function Component({ service }) {
const { widget } = service;
const { data: piholeData, error: piholeError } = useWidgetAPI(widget, "api.php");
const { data: piholeData, error: piholeError } = useWidgetAPI(widget, "summaryRaw");
if (piholeError) {
return <Container error={piholeError} />;
@ -27,9 +27,9 @@ export default function Component({ service }) {
return (
<Container service={service}>
<Block label="pihole.queries" value={t("common.number", { value: piholeData.dns_queries_today })} />
<Block label="pihole.blocked" value={t("common.number", { value: piholeData.ads_blocked_today })} />
<Block label="pihole.gravity" value={t("common.number", { value: piholeData.domains_being_blocked })} />
<Block label="pihole.queries" value={t("common.number", { value: parseInt(piholeData.dns_queries_today, 10) })} />
<Block label="pihole.blocked" value={t("common.number", { value: parseInt(piholeData.ads_blocked_today, 10) })} />
<Block label="pihole.gravity" value={t("common.number", { value: parseInt(piholeData.domains_being_blocked, 10) })} />
</Container>
);
}

View file

@ -1,12 +1,12 @@
import genericProxyHandler from "utils/proxy/handlers/generic";
const widget = {
api: "{url}/admin/{endpoint}",
api: "{url}/admin/api.php?{endpoint}&auth={key}",
proxyHandler: genericProxyHandler,
mappings: {
"api.php": {
endpoint: "api.php",
"summaryRaw": {
endpoint: "summaryRaw",
validate: [
"dns_queries_today",
"ads_blocked_today",

View file

@ -58,6 +58,9 @@ async function fetchFromPlexAPI(endpoint, widget) {
export default async function plexProxyHandler(req, res) {
const widget = await getWidget(req);
const { service } = req.query;
if (!widget) {
return res.status(400).json({ error: "Invalid proxy service type" });
}
@ -74,23 +77,24 @@ export default async function plexProxyHandler(req, res) {
streams = apiData.MediaContainer._attributes.size;
}
let libraries = cache.get(librariesCacheKey);
let libraries = cache.get(`${librariesCacheKey}.${service}`);
if (libraries === null) {
logger.debug("Getting libraries from Plex API");
[status, apiData] = await fetchFromPlexAPI("/library/sections", widget);
if (apiData && apiData.MediaContainer) {
libraries = apiData.MediaContainer.Directory;
cache.put(librariesCacheKey, libraries, 1000 * 60 * 60 * 6);
libraries = [].concat(apiData.MediaContainer.Directory);
cache.put(`${librariesCacheKey}.${service}`, libraries, 1000 * 60 * 60 * 6);
}
}
let movies = cache.get(moviesCacheKey);
let tv = cache.get(tvCacheKey);
let movies = cache.get(`${moviesCacheKey}.${service}`);
let tv = cache.get(`${tvCacheKey}.${service}`);
if (movies === null || tv === null) {
movies = 0;
tv = 0;
logger.debug("Getting movie + tv counts from Plex API");
libraries.filter(l => ["movie", "show"].includes(l._attributes.type)).forEach(async (library) => {
const movieTVLibraries = libraries.filter(l => ["movie", "show"].includes(l._attributes.type));
await Promise.all(movieTVLibraries.map(async (library) => {
[status, apiData] = await fetchFromPlexAPI(`/library/sections/${library._attributes.key}/all`, widget);
if (apiData && apiData.MediaContainer) {
const size = parseInt(apiData.MediaContainer._attributes.size, 10);
@ -100,9 +104,9 @@ export default async function plexProxyHandler(req, res) {
tv += size;
}
}
cache.put(tvCacheKey, tv, 1000 * 60 * 10);
cache.put(moviesCacheKey, movies, 1000 * 60 * 10);
});
}));
cache.put(`${tvCacheKey}.${service}`, tv, 1000 * 60 * 10);
cache.put(`${moviesCacheKey}.${service}`, movies, 1000 * 60 * 10);
}
const data = {

View file

@ -1,8 +1,11 @@
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;
const { data: indexersData, error: indexersError } = useWidgetAPI(widget, "indexer");
@ -40,11 +43,11 @@ export default function Component({ service }) {
return (
<Container service={service}>
<Block label="prowlarr.enableIndexers" value={indexers.length} />
<Block label="prowlarr.numberOfGrabs" value={numberOfGrabs} />
<Block label="prowlarr.numberOfQueries" value={numberOfQueries} />
<Block label="prowlarr.numberOfFailGrabs" value={numberOfFailedGrabs} />
<Block label="prowlarr.numberOfFailQueries" value={numberOfFailedQueries} />
<Block label="prowlarr.enableIndexers" value={t("common.number", { value: indexers.length })} />
<Block label="prowlarr.numberOfGrabs" value={t("common.number", { value: numberOfGrabs })} />
<Block label="prowlarr.numberOfQueries" value={t("common.number", { value: numberOfQueries })} />
<Block label="prowlarr.numberOfFailGrabs" value={t("common.number", { value: numberOfFailedGrabs })} />
<Block label="prowlarr.numberOfFailQueries" value={t("common.number", { value: numberOfFailedQueries })} />
</Container>
);
}

View file

@ -11,7 +11,7 @@ const logger = createLogger(proxyName);
const sessionCacheKey = `${proxyName}__sessionId`;
const isNgCacheKey = `${proxyName}__isNg`;
async function fetchFromPyloadAPI(url, sessionId, params) {
async function fetchFromPyloadAPI(url, sessionId, params, service) {
const options = {
body: params
? Object.keys(params)
@ -25,10 +25,10 @@ async function fetchFromPyloadAPI(url, sessionId, params) {
};
// see https://github.com/benphelps/homepage/issues/517
const isNg = cache.get(isNgCacheKey);
const isNg = cache.get(`${isNgCacheKey}.${service}`);
if (isNg && !params) {
delete options.body;
options.headers.Cookie = cache.get(sessionCacheKey);
options.headers.Cookie = cache.get(`${sessionCacheKey}.${service}`);
}
// eslint-disable-next-line no-unused-vars
@ -43,19 +43,19 @@ async function fetchFromPyloadAPI(url, sessionId, params) {
return [status, returnData, responseHeaders];
}
async function login(loginUrl, username, password = '') {
const [status, sessionId, responseHeaders] = await fetchFromPyloadAPI(loginUrl, null, { username, password });
async function login(loginUrl, service, username, password = '') {
const [status, sessionId, responseHeaders] = await fetchFromPyloadAPI(loginUrl, null, { username, password }, service);
// this API actually returns status 200 even on login failure
if (status !== 200 || sessionId === false) {
logger.error(`HTTP ${status} logging into Pyload API, returned: ${JSON.stringify(sessionId)}`);
} else if (responseHeaders['set-cookie']?.join().includes('pyload_session')) {
// Support pyload-ng, see https://github.com/benphelps/homepage/issues/517
cache.put(isNgCacheKey, true);
cache.put(`${isNgCacheKey}.${service}`, true);
const sessionCookie = responseHeaders['set-cookie'][0];
cache.put(sessionCacheKey, sessionCookie, 60 * 60 * 23 * 1000); // cache for 23h
cache.put(`${sessionCacheKey}.${service}`, sessionCookie, 60 * 60 * 23 * 1000); // cache for 23h
} else {
cache.put(sessionCacheKey, sessionId);
cache.put(`${sessionCacheKey}.${service}`, sessionId);
}
return sessionId;
@ -72,21 +72,21 @@ export default async function pyloadProxyHandler(req, res) {
const url = new URL(formatApiCall(widgets[widget.type].api, { endpoint, ...widget }));
const loginUrl = `${widget.url}/api/login`;
let sessionId = cache.get(sessionCacheKey) ?? await login(loginUrl, widget.username, widget.password);
let [status, data] = await fetchFromPyloadAPI(url, sessionId);
let sessionId = cache.get(`${sessionCacheKey}.${service}`) ?? await login(loginUrl, service, widget.username, widget.password);
let [status, data] = await fetchFromPyloadAPI(url, sessionId, null, service);
if (status === 403 || status === 401) {
logger.info('Failed to retrieve data from Pyload API, trying to login again...');
cache.del(sessionCacheKey);
sessionId = await login(loginUrl, widget.username, widget.password);
[status, data] = await fetchFromPyloadAPI(url, sessionId);
cache.del(`${sessionCacheKey}.${service}`);
sessionId = await login(loginUrl, service, widget.username, widget.password);
[status, data] = await fetchFromPyloadAPI(url, sessionId, null, service);
}
if (data?.error || status !== 200) {
try {
return res.status(status).send({error: {message: "HTTP error communicating with Plex API", data: Buffer.from(data).toString()}});
return res.status(status).send({error: {message: "HTTP error communicating with Pyload API", data: Buffer.from(data).toString()}});
} catch (e) {
return res.status(status).send({error: {message: "HTTP error communicating with Plex API", data}});
return res.status(status).send({error: {message: "HTTP error communicating with Pyload API", data}});
}
}
@ -95,7 +95,7 @@ export default async function pyloadProxyHandler(req, res) {
}
} catch (e) {
logger.error(e);
return res.status(500).send({error: {message: `Error communicating with Plex API: ${e.toString()}`}});
return res.status(500).send({error: {message: `Error communicating with Pyload API: ${e.toString()}`}});
}
return res.status(400).json({ error: 'Invalid proxy service type' });

View file

@ -44,9 +44,9 @@ export default function Component({ service }) {
return (
<Container service={service}>
<Block label="qbittorrent.leech" value={t("common.number", { value: leech })} />
<Block label="qbittorrent.download" value={t("common.bitrate", { value: rateDl })} />
<Block label="qbittorrent.download" value={t("common.bibyterate", { value: rateDl, decimals: 1 })} />
<Block label="qbittorrent.seed" value={t("common.number", { value: completed })} />
<Block label="qbittorrent.upload" value={t("common.bitrate", { value: rateUl })} />
<Block label="qbittorrent.upload" value={t("common.bibyterate", { value: rateUl, decimals: 1 })} />
</Container>
);
}

View file

@ -1,8 +1,11 @@
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: moviesData, error: moviesError } = useWidgetAPI(widget, "movie");
@ -26,10 +29,10 @@ export default function Component({ service }) {
return (
<Container service={service}>
<Block label="radarr.wanted" value={moviesData.wanted} />
<Block label="radarr.missing" value={moviesData.missing} />
<Block label="radarr.queued" value={queuedData.totalCount} />
<Block label="radarr.movies" value={moviesData.have} />
<Block label="radarr.wanted" value={t("common.number", { value: moviesData.wanted })} />
<Block label="radarr.missing" value={t("common.number", { value: moviesData.missing })} />
<Block label="radarr.queued" value={t("common.number", { value: queuedData.totalCount })} />
<Block label="radarr.movies" value={t("common.number", { value: moviesData.have })} />
</Container>
);
}

View file

@ -1,8 +1,11 @@
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: wantedData, error: wantedError } = useWidgetAPI(widget, "wanted/missing");
@ -26,9 +29,9 @@ export default function Component({ service }) {
return (
<Container service={service}>
<Block label="sonarr.wanted" value={wantedData.totalRecords} />
<Block label="sonarr.queued" value={queuedData.totalRecords} />
<Block label="sonarr.series" value={seriesData.total} />
<Block label="sonarr.wanted" value={t("common.number", { value: wantedData.totalRecords })} />
<Block label="sonarr.queued" value={t("common.number", { value: queuedData.totalRecords })} />
<Block label="sonarr.series" value={t("common.number", { value: seriesData.total })} />
</Container>
);
}

View file

@ -29,9 +29,9 @@ export default function Component({ service }) {
<Container service={service}>
<Block
label="speedtest.download"
value={t("common.bitrate", { value: speedtestData.data.download * 1024 * 1024 })}
value={t("common.bitrate", { value: speedtestData.data.download * 1000 * 1000 })}
/>
<Block label="speedtest.upload" value={t("common.bitrate", { value: speedtestData.data.upload * 1024 * 1024 })} />
<Block label="speedtest.upload" value={t("common.bitrate", { value: speedtestData.data.upload * 1000 * 1000 })} />
<Block
label="speedtest.ping"
value={t("common.ms", {

View file

@ -0,0 +1,42 @@
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: tdarrData, error: tdarrError } = useWidgetAPI(widget);
if (tdarrError) {
return <Container error={tdarrError} />;
}
if (!tdarrData) {
return (
<Container service={service}>
<Block label="tdarr.queue" />
<Block label="tdarr.processed" />
<Block label="tdarr.errored" />
<Block label="tdarr.saved" />
</Container>
);
}
const queue = parseInt(tdarrData.table1Count, 10) + parseInt(tdarrData.table4Count, 10);
const processed = parseInt(tdarrData.table2Count, 10) + parseInt(tdarrData.table5Count, 10);
const errored = parseInt(tdarrData.table3Count, 10) + parseInt(tdarrData.table6Count, 10);
const saved = parseFloat(tdarrData.sizeDiff, 10) * 1000000000;
return (
<Container service={service}>
<Block label="tdarr.queue" value={t("common.number", { value: queue })} />
<Block label="tdarr.processed" value={t("common.number", { value: processed })} />
<Block label="tdarr.errored" value={t("common.number", { value: errored })} />
<Block label="tdarr.saved" value={t("common.bytes", { value: saved })} />
</Container>
);
}

View file

@ -0,0 +1,48 @@
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 = "tdarrProxyHandler";
const logger = createLogger(proxyName);
export default async function tdarrProxyHandler(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" });
}
const url = new URL(formatApiCall(widgets[widget.type].api, { endpoint, ...widget }));
const [status, contentType, data] = await httpProxy(url, {
method: "POST",
body: JSON.stringify({
"data": {
"collection": "StatisticsJSONDB",
"mode": "getById",
"docID": "statistics"
},
}),
headers: {
"content-type": "application/json",
},
});
if (status !== 200) {
logger.error("Error getting data from Tdarr: %d. Data: %s", status, data);
return res.status(500).send({error: {message:"Error getting data from Tdarr", url, data}});
}
if (contentType) res.setHeader("Content-Type", contentType);
return res.status(status).send(data);
}

View file

@ -0,0 +1,8 @@
import tdarrProxyHandler from "./proxy";
const widget = {
api: "{url}/api/v2/cruddb",
proxyHandler: tdarrProxyHandler,
};
export default widget;

View file

@ -25,12 +25,12 @@ export default async function transmissionProxyHandler(req, res) {
return res.status(400).json({ error: "Invalid proxy service type" });
}
let headers = cache.get(headerCacheKey);
let headers = cache.get(`${headerCacheKey}.${service}`);
if (!headers) {
headers = {
"content-type": "application/json",
}
cache.put(headerCacheKey, headers);
cache.put(`${headerCacheKey}.${service}`, headers);
}
const url = new URL(formatApiCall(widgets[widget.type].api, { endpoint, ...widget }));
@ -55,7 +55,7 @@ export default async function transmissionProxyHandler(req, res) {
if (status === 409) {
logger.debug("Transmission is rejecting the request, but returning a CSRF token");
headers[csrfHeaderName] = responseHeaders[csrfHeaderName];
cache.put(headerCacheKey, headers);
cache.put(`${headerCacheKey}.${service}`, headers);
// retry the request, now with the CSRF token
[status, contentType, data, responseHeaders] = await httpProxy(url, {

View file

@ -58,6 +58,7 @@ async function login(widget) {
export default async function unifiProxyHandler(req, res) {
const widget = await getWidget(req);
const { service } = req.query;
if (!widget) {
return res.status(400).json({ error: "Invalid proxy service type" });
}
@ -68,7 +69,7 @@ export default async function unifiProxyHandler(req, res) {
}
let [status, contentType, data, responseHeaders] = [];
let prefix = cache.get(prefixCacheKey);
let prefix = cache.get(`${prefixCacheKey}.${service}`);
if (prefix === null) {
// auto detect if we're talking to a UDM Pro, and cache the result so that we
// don't make two requests each time data from Unifi is required
@ -77,7 +78,7 @@ export default async function unifiProxyHandler(req, res) {
if (responseHeaders?.["x-csrf-token"]) {
prefix = udmpPrefix;
}
cache.put(prefixCacheKey, prefix);
cache.put(`${prefixCacheKey}.${service}`, prefix);
}
widget.prefix = prefix;

View file

@ -5,7 +5,7 @@ import bazarr from "./bazarr/widget";
import changedetectionio from "./changedetectionio/widget";
import coinmarketcap from "./coinmarketcap/widget";
import deluge from "./deluge/widget";
import diskstation from "./diskstation/widget";
import downloadstation from "./downloadstation/widget";
import emby from "./emby/widget";
import flood from "./flood/widget";
import gluetun from "./gluetun/widget";
@ -16,10 +16,15 @@ import jackett from "./jackett/widget";
import jellyseerr from "./jellyseerr/widget";
import lidarr from "./lidarr/widget";
import mastodon from "./mastodon/widget";
import miniflux from "./miniflux/widget";
import mikrotik from "./mikrotik/widget";
import navidrome from "./navidrome/widget";
import nextdns from "./nextdns/widget";
import npm from "./npm/widget";
import nzbget from "./nzbget/widget";
import omada from "./omada/widget";
import ombi from "./ombi/widget";
import opnsense from "./opnsense/widget";
import overseerr from "./overseerr/widget";
import paperlessngx from "./paperlessngx/widget";
import pihole from "./pihole/widget";
@ -38,12 +43,14 @@ import sonarr from "./sonarr/widget";
import speedtest from "./speedtest/widget";
import strelaysrv from "./strelaysrv/widget";
import tautulli from "./tautulli/widget";
import tdarr from "./tdarr/widget";
import traefik from "./traefik/widget";
import transmission from "./transmission/widget";
import tubearchivist from "./tubearchivist/widget";
import truenas from "./truenas/widget";
import unifi from "./unifi/widget";
import watchtower from './watchtower/widget'
import xteve from './xteve/widget'
const widgets = {
adguard,
@ -53,7 +60,8 @@ const widgets = {
changedetectionio,
coinmarketcap,
deluge,
diskstation,
diskstation: downloadstation,
downloadstation,
emby,
flood,
gluetun,
@ -65,10 +73,15 @@ const widgets = {
jellyseerr,
lidarr,
mastodon,
miniflux,
mikrotik,
navidrome,
nextdns,
npm,
nzbget,
omada,
ombi,
opnsense,
overseerr,
paperlessngx,
pihole,
@ -87,6 +100,7 @@ const widgets = {
speedtest,
strelaysrv,
tautulli,
tdarr,
traefik,
transmission,
tubearchivist,
@ -94,6 +108,7 @@ const widgets = {
unifi,
unifi_console: unifi,
watchtower,
xteve,
};
export default widgets;

View file

@ -0,0 +1,35 @@
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: xteveData, error: xteveError } = useWidgetAPI(widget, "api");
if (xteveError) {
return <Container error={xteveError} />;
}
if (!xteveData) {
return (
<Container service={service}>
<Block label="xteve.streams_all" />
<Block label="xteve.streams_active " />
<Block label="xteve.streams_xepg" />
</Container>
);
}
return (
<Container service={service}>
<Block label="xteve.streams_all" value={t("common.number", { value: xteveData["streams.all"] ?? 0 })} />
<Block label="xteve.streams_active" value={t("common.number", { value: xteveData["streams.active"] ?? 0 })} />
<Block label="xteve.streams_xepg" value={t("common.number", { value: xteveData["streams.xepg"] ?? 0 })} />
</Container>
);
}

View file

@ -0,0 +1,63 @@
import { formatApiCall } from "utils/proxy/api-helpers";
import { httpProxy } from "utils/proxy/http";
import createLogger from "utils/logger";
import widgets from "widgets/widgets";
import getServiceWidget from "utils/config/service-helpers";
const logger = createLogger("xteveProxyHandler");
export default async function xteveProxyHandler(req, res) {
const { group, service, endpoint } = req.query;
if (!group || !service) {
return res.status(400).json({ error: "Invalid proxy service type" });
}
const widget = await getServiceWidget(group, service);
const api = widgets?.[widget.type]?.api;
if (!api) {
return res.status(403).json({ error: "Service does not support API calls" });
}
const url = formatApiCall(api, { endpoint, ...widget });
const method = "POST";
const payload = { cmd: "status" };
if (widget.username && widget.password) {
// eslint-disable-next-line no-unused-vars
const [status, contentType, data] = await httpProxy(url, {
method,
body: JSON.stringify({
cmd: "login",
username: widget.username,
password: widget.password,
})
});
if (status !== 200) {
logger.debug("Error logging into xteve", status, url);
return res.status(status).json({error: {message: `HTTP Error ${status} logging into xteve`, url, data}});
}
const json = JSON.parse(data.toString());
if (json?.status !== true) {
return res.status(401).json({error: {message: "Authentication failed", url, data}});
}
payload.token = json.token;
}
const [status, contentType, data] = await httpProxy(url, {
method,
body: JSON.stringify(payload)
});
if (status !== 200) {
logger.debug("Error %d calling xteve endpoint %s", status, url);
return res.status(status).json({error: {message: `HTTP Error ${status}`, url, data}});
}
if (contentType) res.setHeader("Content-Type", contentType);
return res.status(status).send(data);
}

View file

@ -0,0 +1,14 @@
import xteveProxyHandler from "./proxy";
const widget = {
api: "{url}/{endpoint}",
proxyHandler: xteveProxyHandler,
mappings: {
"api": {
endpoint: "api/",
},
},
};
export default widget;