mirror of
https://github.com/gethomepage/homepage.git
synced 2025-12-07 09:35:54 -08:00
Run pre-commit hooks over existing codebase
Co-Authored-By: Ben Phelps <ben@phelps.io>
This commit is contained in:
@@ -14,7 +14,7 @@ export default function Component({ service }) {
|
||||
if (adguardError) {
|
||||
return <Container service={service} error={adguardError} />;
|
||||
}
|
||||
|
||||
|
||||
if (!adguardData) {
|
||||
return (
|
||||
<Container service={service}>
|
||||
|
||||
@@ -6,9 +6,9 @@ const widget = {
|
||||
|
||||
mappings: {
|
||||
info: {
|
||||
endpoint: "info"
|
||||
}
|
||||
endpoint: "info",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default widget;
|
||||
export default widget;
|
||||
|
||||
@@ -10,11 +10,10 @@ export default function Component({ service }) {
|
||||
const { widget } = service;
|
||||
const { data: librariesData, error: librariesError } = useWidgetAPI(widget, "libraries");
|
||||
|
||||
|
||||
if (librariesError) {
|
||||
return <Container service={service} error={librariesError} />;
|
||||
}
|
||||
|
||||
|
||||
if (!librariesData) {
|
||||
return (
|
||||
<Container service={service}>
|
||||
@@ -25,9 +24,9 @@ export default function Component({ service }) {
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
const podcastLibraries = librariesData.filter(l => l.mediaType === "podcast");
|
||||
const bookLibraries = librariesData.filter(l => l.mediaType === "book");
|
||||
|
||||
const podcastLibraries = librariesData.filter((l) => l.mediaType === "podcast");
|
||||
const bookLibraries = librariesData.filter((l) => l.mediaType === "book");
|
||||
|
||||
const totalPodcasts = podcastLibraries.reduce((total, pL) => parseInt(pL.stats?.totalItems, 10) + total, 0);
|
||||
const totalBooks = bookLibraries.reduce((total, bL) => parseInt(bL.stats?.totalItems, 10) + total, 0);
|
||||
@@ -38,9 +37,25 @@ export default function Component({ service }) {
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="audiobookshelf.podcasts" value={t("common.number", { value: totalPodcasts })} />
|
||||
<Block label="audiobookshelf.podcastsDuration" value={t("common.number", { value: totalPodcastsDuration / 60, maximumFractionDigits: 0, style: "unit", unit: "minute" })} />
|
||||
<Block
|
||||
label="audiobookshelf.podcastsDuration"
|
||||
value={t("common.number", {
|
||||
value: totalPodcastsDuration / 60,
|
||||
maximumFractionDigits: 0,
|
||||
style: "unit",
|
||||
unit: "minute",
|
||||
})}
|
||||
/>
|
||||
<Block label="audiobookshelf.books" value={t("common.number", { value: totalBooks })} />
|
||||
<Block label="audiobookshelf.booksDuration" value={t("common.number", { value: totalBooksDuration / 60, maximumFractionDigits: 0, style: "unit", unit: "minute" })} />
|
||||
<Block
|
||||
label="audiobookshelf.booksDuration"
|
||||
value={t("common.number", {
|
||||
value: totalBooksDuration / 60,
|
||||
maximumFractionDigits: 0,
|
||||
style: "unit",
|
||||
unit: "minute",
|
||||
})}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ const logger = createLogger(proxyName);
|
||||
async function retrieveFromAPI(url, key) {
|
||||
const headers = {
|
||||
"content-type": "application/json",
|
||||
"Authorization": `Bearer ${key}`
|
||||
Authorization: `Bearer ${key}`,
|
||||
};
|
||||
|
||||
const [status, , data] = await httpProxy(url, { headers });
|
||||
@@ -48,17 +48,22 @@ export default async function audiobookshelfProxyHandler(req, res) {
|
||||
const url = new URL(formatApiCall(apiURL, { endpoint, ...widget }));
|
||||
const libraryData = await retrieveFromAPI(url, widget.key);
|
||||
|
||||
const libraryStats = await Promise.all(libraryData.libraries.map(async l => {
|
||||
const stats = await retrieveFromAPI(new URL(formatApiCall(apiURL, { endpoint: `libraries/${l.id}/stats`, ...widget })), widget.key);
|
||||
return {
|
||||
...l,
|
||||
stats
|
||||
};
|
||||
}));
|
||||
|
||||
const libraryStats = await Promise.all(
|
||||
libraryData.libraries.map(async (l) => {
|
||||
const stats = await retrieveFromAPI(
|
||||
new URL(formatApiCall(apiURL, { endpoint: `libraries/${l.id}/stats`, ...widget })),
|
||||
widget.key,
|
||||
);
|
||||
return {
|
||||
...l,
|
||||
stats,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
return res.status(200).send(libraryStats);
|
||||
} catch (e) {
|
||||
logger.error(e.message);
|
||||
return res.status(500).send({error: {message: e.message}});
|
||||
return res.status(500).send({ error: { message: e.message } });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,4 +11,4 @@ const widget = {
|
||||
},
|
||||
};
|
||||
|
||||
export default widget;
|
||||
export default widget;
|
||||
|
||||
@@ -31,11 +31,11 @@ export default function Component({ service }) {
|
||||
const yesterday = new Date(Date.now()).setHours(-24);
|
||||
const loginsLast24H = loginsData.reduce(
|
||||
(total, current) => (current.x_cord >= yesterday ? total + current.y_cord : total),
|
||||
0
|
||||
0,
|
||||
);
|
||||
const failedLoginsLast24H = failedLoginsData.reduce(
|
||||
(total, current) => (current.x_cord >= yesterday ? total + current.y_cord : total),
|
||||
0
|
||||
0,
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -7,10 +7,7 @@ const widget = {
|
||||
mappings: {
|
||||
stats: {
|
||||
endpoint: "release/stats",
|
||||
validate: [
|
||||
"push_approved_count",
|
||||
"push_rejected_count"
|
||||
]
|
||||
validate: ["push_approved_count", "push_rejected_count"],
|
||||
},
|
||||
filters: {
|
||||
endpoint: "filters",
|
||||
|
||||
@@ -12,14 +12,11 @@ export default function Component({ service }) {
|
||||
const { data: prData, error: prError } = useWidgetAPI(widget, includePR ? "pr" : null);
|
||||
const { data: pipelineData, error: pipelineError } = useWidgetAPI(widget, "pipeline");
|
||||
|
||||
if (
|
||||
pipelineError ||
|
||||
(includePR && (prError || prData?.errorCode !== undefined))
|
||||
) {
|
||||
if (pipelineError || (includePR && (prError || prData?.errorCode !== undefined))) {
|
||||
let finalError = pipelineError ?? prError;
|
||||
if (includePR && prData?.errorCode !== null) {
|
||||
// pr call failed possibly with more specific message
|
||||
finalError = { message: prData?.message ?? 'Error communicating with Azure API' }
|
||||
finalError = { message: prData?.message ?? "Error communicating with Azure API" };
|
||||
}
|
||||
return <Container service={service} error={finalError} />;
|
||||
}
|
||||
@@ -42,24 +39,27 @@ export default function Component({ service }) {
|
||||
) : (
|
||||
<Block label="azuredevops.status" value={t(`azuredevops.${pipelineData.value[0].status.toString()}`)} />
|
||||
)}
|
||||
|
||||
|
||||
{includePR && <Block label="azuredevops.totalPrs" value={t("common.number", { value: prData.count })} />}
|
||||
{includePR && <Block
|
||||
{includePR && (
|
||||
<Block
|
||||
label="azuredevops.myPrs"
|
||||
value={t("common.number", {
|
||||
value: prData.value?.filter((item) => item.createdBy.uniqueName.toLowerCase() === userEmail.toLowerCase())
|
||||
.length,
|
||||
})}
|
||||
/>}
|
||||
{includePR && <Block
|
||||
label="azuredevops.approved"
|
||||
value={t("common.number", {
|
||||
value: prData.value
|
||||
?.filter((item) => item.createdBy.uniqueName.toLowerCase() === userEmail.toLowerCase())
|
||||
.filter((item) => item.reviewers.some((reviewer) => [5,10].includes(reviewer.vote))).length
|
||||
})}
|
||||
/>}
|
||||
|
||||
/>
|
||||
)}
|
||||
{includePR && (
|
||||
<Block
|
||||
label="azuredevops.approved"
|
||||
value={t("common.number", {
|
||||
value: prData.value
|
||||
?.filter((item) => item.createdBy.uniqueName.toLowerCase() === userEmail.toLowerCase())
|
||||
.filter((item) => item.reviewers.some((reviewer) => [5, 10].includes(reviewer.vote))).length,
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,11 +6,11 @@ const widget = {
|
||||
|
||||
mappings: {
|
||||
pr: {
|
||||
endpoint: "git/repositories/{repositoryId}/pullrequests"
|
||||
endpoint: "git/repositories/{repositoryId}/pullrequests",
|
||||
},
|
||||
|
||||
|
||||
pipeline: {
|
||||
endpoint: "build/Builds?branchName={branchName}&definitions={definitionId}"
|
||||
endpoint: "build/Builds?branchName={branchName}&definitions={definitionId}",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -10,14 +10,14 @@ export default function Component({ service }) {
|
||||
const { widget } = service;
|
||||
const { data: resultData, error: resultError } = useWidgetAPI(widget, "result");
|
||||
|
||||
|
||||
if (resultError) {
|
||||
return <Container service={service} error={resultError} />;
|
||||
}
|
||||
|
||||
if (!resultData) {
|
||||
return (
|
||||
<Container service={service}>,
|
||||
<Container service={service}>
|
||||
,
|
||||
<Block label="caddy.upstreams" />
|
||||
<Block label="caddy.requests" />
|
||||
<Block label="caddy.requests_failed" />
|
||||
|
||||
@@ -18,30 +18,42 @@ export default function Component({ service }) {
|
||||
}
|
||||
|
||||
return {
|
||||
start: showDate.minus({months: 3}).toFormat("yyyy-MM-dd"),
|
||||
end: showDate.plus({months: 3}).toFormat("yyyy-MM-dd"),
|
||||
unmonitored: 'false',
|
||||
start: showDate.minus({ months: 3 }).toFormat("yyyy-MM-dd"),
|
||||
end: showDate.plus({ months: 3 }).toFormat("yyyy-MM-dd"),
|
||||
unmonitored: "false",
|
||||
};
|
||||
}, [showDate]);
|
||||
|
||||
// Load active integrations
|
||||
const integrations = useMemo(() => widget.integrations?.map(integration => ({
|
||||
service: dynamic(() => import(`./integrations/${integration?.type}`)),
|
||||
widget: integration,
|
||||
})) ?? [], [widget.integrations]);
|
||||
const integrations = useMemo(
|
||||
() =>
|
||||
widget.integrations?.map((integration) => ({
|
||||
service: dynamic(() => import(`./integrations/${integration?.type}`)),
|
||||
widget: integration,
|
||||
})) ?? [],
|
||||
[widget.integrations],
|
||||
);
|
||||
|
||||
return <Container service={service}>
|
||||
<div className="flex flex-col w-full">
|
||||
<div className="sticky top-0">
|
||||
{integrations.map(integration => {
|
||||
const Integration = integration.service;
|
||||
const key = integration.widget.type + integration.widget.service_name + integration.widget.service_group;
|
||||
return (
|
||||
<Container service={service}>
|
||||
<div className="flex flex-col w-full">
|
||||
<div className="sticky top-0">
|
||||
{integrations.map((integration) => {
|
||||
const Integration = integration.service;
|
||||
const key = integration.widget.type + integration.widget.service_name + integration.widget.service_group;
|
||||
|
||||
return <Integration key={key} config={integration.widget} params={params}
|
||||
className="fixed bottom-0 left-0 bg-red-500 w-screen h-12" />
|
||||
})}
|
||||
return (
|
||||
<Integration
|
||||
key={key}
|
||||
config={integration.widget}
|
||||
params={params}
|
||||
className="fixed bottom-0 left-0 bg-red-500 w-screen h-12"
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<MonthlyView service={service} className="flex" />
|
||||
</div>
|
||||
<MonthlyView service={service} className="flex"/>
|
||||
</div>
|
||||
</Container>;
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,9 +7,11 @@ import Error from "../../../components/services/widget/error";
|
||||
|
||||
export default function Integration({ config, params }) {
|
||||
const { setEvents } = useContext(EventContext);
|
||||
const { data: lidarrData, error: lidarrError } = useWidgetAPI(config, "calendar",
|
||||
{ ...params, includeArtist: 'false', ...config?.params ?? {} }
|
||||
);
|
||||
const { data: lidarrData, error: lidarrError } = useWidgetAPI(config, "calendar", {
|
||||
...params,
|
||||
includeArtist: "false",
|
||||
...(config?.params ?? {}),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!lidarrData || lidarrError) {
|
||||
@@ -18,19 +20,19 @@ export default function Integration({ config, params }) {
|
||||
|
||||
const eventsToAdd = {};
|
||||
|
||||
lidarrData?.forEach(event => {
|
||||
lidarrData?.forEach((event) => {
|
||||
const title = `${event.artist.artistName} - ${event.title}`;
|
||||
|
||||
eventsToAdd[title] = {
|
||||
title,
|
||||
date: DateTime.fromISO(event.releaseDate),
|
||||
color: config?.color ?? 'green'
|
||||
color: config?.color ?? "green",
|
||||
};
|
||||
})
|
||||
});
|
||||
|
||||
setEvents((prevEvents) => ({ ...prevEvents, ...eventsToAdd }));
|
||||
}, [lidarrData, lidarrError, config, setEvents]);
|
||||
|
||||
const error = lidarrError ?? lidarrData?.error;
|
||||
return error && <Error error={{ message: `${config.type}: ${error.message ?? error}`}} />
|
||||
return error && <Error error={{ message: `${config.type}: ${error.message ?? error}` }} />;
|
||||
}
|
||||
|
||||
@@ -9,9 +9,10 @@ import Error from "../../../components/services/widget/error";
|
||||
export default function Integration({ config, params }) {
|
||||
const { t } = useTranslation();
|
||||
const { setEvents } = useContext(EventContext);
|
||||
const { data: radarrData, error: radarrError } = useWidgetAPI(config, "calendar",
|
||||
{ ...params, ...config?.params ?? {} }
|
||||
);
|
||||
const { data: radarrData, error: radarrError } = useWidgetAPI(config, "calendar", {
|
||||
...params,
|
||||
...(config?.params ?? {}),
|
||||
});
|
||||
useEffect(() => {
|
||||
if (!radarrData || radarrError) {
|
||||
return;
|
||||
@@ -19,7 +20,7 @@ export default function Integration({ config, params }) {
|
||||
|
||||
const eventsToAdd = {};
|
||||
|
||||
radarrData?.forEach(event => {
|
||||
radarrData?.forEach((event) => {
|
||||
const cinemaTitle = `${event.title} - ${t("calendar.inCinemas")}`;
|
||||
const physicalTitle = `${event.title} - ${t("calendar.physicalRelease")}`;
|
||||
const digitalTitle = `${event.title} - ${t("calendar.digitalRelease")}`;
|
||||
@@ -27,23 +28,23 @@ export default function Integration({ config, params }) {
|
||||
eventsToAdd[cinemaTitle] = {
|
||||
title: cinemaTitle,
|
||||
date: DateTime.fromISO(event.inCinemas),
|
||||
color: config?.color ?? 'amber'
|
||||
color: config?.color ?? "amber",
|
||||
};
|
||||
eventsToAdd[physicalTitle] = {
|
||||
title: physicalTitle,
|
||||
date: DateTime.fromISO(event.physicalRelease),
|
||||
color: config?.color ?? 'cyan'
|
||||
color: config?.color ?? "cyan",
|
||||
};
|
||||
eventsToAdd[digitalTitle] = {
|
||||
title: digitalTitle,
|
||||
date: DateTime.fromISO(event.digitalRelease),
|
||||
color: config?.color ?? 'emerald'
|
||||
color: config?.color ?? "emerald",
|
||||
};
|
||||
})
|
||||
});
|
||||
|
||||
setEvents((prevEvents) => ({ ...prevEvents, ...eventsToAdd }));
|
||||
}, [radarrData, radarrError, config, setEvents, t]);
|
||||
|
||||
const error = radarrError ?? radarrData?.error;
|
||||
return error && <Error error={{ message: `${config.type}: ${error.message ?? error}`}} />
|
||||
return error && <Error error={{ message: `${config.type}: ${error.message ?? error}` }} />;
|
||||
}
|
||||
|
||||
@@ -7,9 +7,11 @@ import Error from "../../../components/services/widget/error";
|
||||
|
||||
export default function Integration({ config, params }) {
|
||||
const { setEvents } = useContext(EventContext);
|
||||
const { data: readarrData, error: readarrError } = useWidgetAPI(config, "calendar",
|
||||
{ ...params, includeAuthor: 'true', ...config?.params ?? {} },
|
||||
);
|
||||
const { data: readarrData, error: readarrError } = useWidgetAPI(config, "calendar", {
|
||||
...params,
|
||||
includeAuthor: "true",
|
||||
...(config?.params ?? {}),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!readarrData || readarrError) {
|
||||
@@ -18,20 +20,20 @@ export default function Integration({ config, params }) {
|
||||
|
||||
const eventsToAdd = {};
|
||||
|
||||
readarrData?.forEach(event => {
|
||||
const authorName = event.author?.authorName ?? event.authorTitle.replace(event.title, '');
|
||||
const title = `${authorName} - ${event.title} ${event?.seriesTitle ? `(${event.seriesTitle})` : ''} `;
|
||||
readarrData?.forEach((event) => {
|
||||
const authorName = event.author?.authorName ?? event.authorTitle.replace(event.title, "");
|
||||
const title = `${authorName} - ${event.title} ${event?.seriesTitle ? `(${event.seriesTitle})` : ""} `;
|
||||
|
||||
eventsToAdd[title] = {
|
||||
title,
|
||||
date: DateTime.fromISO(event.releaseDate),
|
||||
color: config?.color ?? 'rose'
|
||||
color: config?.color ?? "rose",
|
||||
};
|
||||
})
|
||||
});
|
||||
|
||||
setEvents((prevEvents) => ({ ...prevEvents, ...eventsToAdd }));
|
||||
}, [readarrData, readarrError, config, setEvents]);
|
||||
|
||||
const error = readarrError ?? readarrData?.error;
|
||||
return error && <Error error={{ message: `${config.type}: ${error.message ?? error}`}} />
|
||||
return error && <Error error={{ message: `${config.type}: ${error.message ?? error}` }} />;
|
||||
}
|
||||
|
||||
@@ -7,9 +7,13 @@ import Error from "../../../components/services/widget/error";
|
||||
|
||||
export default function Integration({ config, params }) {
|
||||
const { setEvents } = useContext(EventContext);
|
||||
const { data: sonarrData, error: sonarrError } = useWidgetAPI(config, "calendar",
|
||||
{ ...params, includeSeries: 'true', includeEpisodeFile: 'false', includeEpisodeImages: 'false', ...config?.params ?? {} }
|
||||
);
|
||||
const { data: sonarrData, error: sonarrError } = useWidgetAPI(config, "calendar", {
|
||||
...params,
|
||||
includeSeries: "true",
|
||||
includeEpisodeFile: "false",
|
||||
includeEpisodeImages: "false",
|
||||
...(config?.params ?? {}),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!sonarrData || sonarrError) {
|
||||
@@ -18,19 +22,19 @@ export default function Integration({ config, params }) {
|
||||
|
||||
const eventsToAdd = {};
|
||||
|
||||
sonarrData?.forEach(event => {
|
||||
sonarrData?.forEach((event) => {
|
||||
const title = `${event.series.title ?? event.title} - S${event.seasonNumber}E${event.episodeNumber}`;
|
||||
|
||||
eventsToAdd[title] = {
|
||||
title,
|
||||
date: DateTime.fromISO(event.airDateUtc),
|
||||
color: config?.color ?? 'teal'
|
||||
color: config?.color ?? "teal",
|
||||
};
|
||||
})
|
||||
});
|
||||
|
||||
setEvents((prevEvents) => ({ ...prevEvents, ...eventsToAdd }));
|
||||
}, [sonarrData, sonarrError, config, setEvents]);
|
||||
|
||||
const error = sonarrError ?? sonarrData?.error;
|
||||
return error && <Error error={{ message: `${config.type}: ${error.message ?? error}`}} />
|
||||
return error && <Error error={{ message: `${config.type}: ${error.message ?? error}` }} />;
|
||||
}
|
||||
|
||||
@@ -7,30 +7,47 @@ import { EventContext, ShowDateContext } from "../../utils/contexts/calendar";
|
||||
|
||||
const colorVariants = {
|
||||
// https://tailwindcss.com/docs/content-configuration#dynamic-class-names
|
||||
amber: "bg-amber-500", blue: "bg-blue-500", cyan: "bg-cyan-500",
|
||||
emerald: "bg-emerald-500", fuchsia: "bg-fuchsia-500", gray: "bg-gray-500",
|
||||
green: "bg-green-500", indigo: "bg-indigo-500", lime: "bg-lime-500",
|
||||
neutral: "bg-neutral-500", orange: "bg-orange-500", pink: "bg-pink-500",
|
||||
purple: "bg-purple-500", red: "bg-red-500", rose: "bg-rose-500",
|
||||
sky: "bg-sky-500", slate: "bg-slate-500", stone: "bg-stone-500",
|
||||
teal: "bg-teal-500", violet: "bg-violet-500", white: "bg-white-500",
|
||||
yellow: "bg-yellow-500", zinc: "bg-zinc-500",
|
||||
}
|
||||
amber: "bg-amber-500",
|
||||
blue: "bg-blue-500",
|
||||
cyan: "bg-cyan-500",
|
||||
emerald: "bg-emerald-500",
|
||||
fuchsia: "bg-fuchsia-500",
|
||||
gray: "bg-gray-500",
|
||||
green: "bg-green-500",
|
||||
indigo: "bg-indigo-500",
|
||||
lime: "bg-lime-500",
|
||||
neutral: "bg-neutral-500",
|
||||
orange: "bg-orange-500",
|
||||
pink: "bg-pink-500",
|
||||
purple: "bg-purple-500",
|
||||
red: "bg-red-500",
|
||||
rose: "bg-rose-500",
|
||||
sky: "bg-sky-500",
|
||||
slate: "bg-slate-500",
|
||||
stone: "bg-stone-500",
|
||||
teal: "bg-teal-500",
|
||||
violet: "bg-violet-500",
|
||||
white: "bg-white-500",
|
||||
yellow: "bg-yellow-500",
|
||||
zinc: "bg-zinc-500",
|
||||
};
|
||||
|
||||
const cellStyle = "relative w-10 flex items-center justify-center flex-col";
|
||||
const monthButton = "pl-6 pr-6 ml-2 mr-2 hover:bg-theme-100/20 dark:hover:bg-white/5 rounded-md cursor-pointer"
|
||||
const monthButton = "pl-6 pr-6 ml-2 mr-2 hover:bg-theme-100/20 dark:hover:bg-white/5 rounded-md cursor-pointer";
|
||||
|
||||
export function Day({ weekNumber, weekday, events }) {
|
||||
const currentDate = DateTime.now();
|
||||
const { showDate, setShowDate } = useContext(ShowDateContext);
|
||||
|
||||
const cellDate = showDate.set({ weekday, weekNumber }).startOf("day");
|
||||
const filteredEvents = events?.filter(event => event.date?.startOf("day").toUnixInteger() === cellDate.toUnixInteger());
|
||||
const filteredEvents = events?.filter(
|
||||
(event) => event.date?.startOf("day").toUnixInteger() === cellDate.toUnixInteger(),
|
||||
);
|
||||
|
||||
const dayStyles = (displayDate) => {
|
||||
let style = "h-9 ";
|
||||
|
||||
if ([6,7].includes(displayDate.weekday)) {
|
||||
if ([6, 7].includes(displayDate.weekday)) {
|
||||
// weekend style
|
||||
style += "text-red-500 ";
|
||||
// different month style
|
||||
@@ -41,7 +58,10 @@ export function Day({ weekNumber, weekday, events }) {
|
||||
}
|
||||
|
||||
// selected same day style
|
||||
style += displayDate.toFormat("MM-dd-yyyy") === showDate.toFormat("MM-dd-yyyy") ? "text-black-500 bg-theme-100/20 dark:bg-white/10 rounded-md " : "";
|
||||
style +=
|
||||
displayDate.toFormat("MM-dd-yyyy") === showDate.toFormat("MM-dd-yyyy")
|
||||
? "text-black-500 bg-theme-100/20 dark:bg-white/10 rounded-md "
|
||||
: "";
|
||||
|
||||
if (displayDate.toFormat("MM-dd-yyyy") === currentDate.toFormat("MM-dd-yyyy")) {
|
||||
// today style
|
||||
@@ -51,38 +71,55 @@ export function Day({ weekNumber, weekday, events }) {
|
||||
}
|
||||
|
||||
return style;
|
||||
}
|
||||
};
|
||||
|
||||
return <button
|
||||
key={`day${weekday}${weekNumber}}`} type="button" className={classNames(dayStyles(cellDate), cellStyle)}
|
||||
style={{ width: "14%" }} onClick={() => setShowDate(cellDate)}
|
||||
>
|
||||
{cellDate.day}
|
||||
<span className="flex justify-center items-center absolute w-full -mb-6">
|
||||
{filteredEvents && filteredEvents.slice(0, 4).map(event => <span
|
||||
key={event.date.toLocaleString() + event.color + event.title}
|
||||
className={classNames(
|
||||
"inline-flex h-1 w-1 m-0.5 rounded",
|
||||
colorVariants[event.color] ?? "gray"
|
||||
)}
|
||||
/>)}
|
||||
</span>
|
||||
</button>
|
||||
return (
|
||||
<button
|
||||
key={`day${weekday}${weekNumber}}`}
|
||||
type="button"
|
||||
className={classNames(dayStyles(cellDate), cellStyle)}
|
||||
style={{ width: "14%" }}
|
||||
onClick={() => setShowDate(cellDate)}
|
||||
>
|
||||
{cellDate.day}
|
||||
<span className="flex justify-center items-center absolute w-full -mb-6">
|
||||
{filteredEvents &&
|
||||
filteredEvents
|
||||
.slice(0, 4)
|
||||
.map((event) => (
|
||||
<span
|
||||
key={event.date.toLocaleString() + event.color + event.title}
|
||||
className={classNames("inline-flex h-1 w-1 m-0.5 rounded", colorVariants[event.color] ?? "gray")}
|
||||
/>
|
||||
))}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export function Event({ event }) {
|
||||
return <div
|
||||
key={event.title}
|
||||
className="text-theme-700 dark:text-theme-200 text-xs relative h-5 w-full rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1"
|
||||
><span className="absolute left-2 text-left text-xs mt-[2px] truncate text-ellipsis" style={{width: '96%'}}>{event.title}</span>
|
||||
</div>
|
||||
return (
|
||||
<div
|
||||
key={event.title}
|
||||
className="text-theme-700 dark:text-theme-200 text-xs relative h-5 w-full rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1"
|
||||
>
|
||||
<span className="absolute left-2 text-left text-xs mt-[2px] truncate text-ellipsis" style={{ width: "96%" }}>
|
||||
{event.title}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const dayInWeekId = {
|
||||
monday: 1, tuesday: 2, wednesday: 3, thursday: 4, friday: 5, saturday: 6, sunday: 7
|
||||
monday: 1,
|
||||
tuesday: 2,
|
||||
wednesday: 3,
|
||||
thursday: 4,
|
||||
friday: 5,
|
||||
saturday: 6,
|
||||
sunday: 7,
|
||||
};
|
||||
|
||||
|
||||
export default function MonthlyView({ service }) {
|
||||
const { widget } = service;
|
||||
const { i18n } = useTranslation();
|
||||
@@ -94,17 +131,19 @@ export default function MonthlyView({ service }) {
|
||||
if (!showDate) {
|
||||
setShowDate(currentDate);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
const dayNames = Info.weekdays("short", { locale: i18n.language });
|
||||
|
||||
const firstDayInWeekCalendar = widget?.firstDayInWeek ? widget?.firstDayInWeek?.toLowerCase() : "monday";
|
||||
for (let i = 1; i < dayInWeekId[firstDayInWeekCalendar]; i+=1) {
|
||||
for (let i = 1; i < dayInWeekId[firstDayInWeekCalendar]; i += 1) {
|
||||
dayNames.push(dayNames.shift());
|
||||
}
|
||||
|
||||
const daysInWeek = useMemo(() => [ ...Array(7).keys() ].map( i => i + dayInWeekId[firstDayInWeekCalendar]
|
||||
), [firstDayInWeekCalendar]);
|
||||
const daysInWeek = useMemo(
|
||||
() => [...Array(7).keys()].map((i) => i + dayInWeekId[firstDayInWeekCalendar]),
|
||||
[firstDayInWeekCalendar],
|
||||
);
|
||||
|
||||
if (!showDate) {
|
||||
return <div className="w-full text-center" />;
|
||||
@@ -113,42 +152,78 @@ export default function MonthlyView({ service }) {
|
||||
const firstWeek = DateTime.local(showDate.year, showDate.month, 1).setLocale(i18n.language);
|
||||
|
||||
const weekIncrementChange = dayInWeekId[firstDayInWeekCalendar] > firstWeek.weekday ? -1 : 0;
|
||||
let weekNumbers = [ ...Array(Math.ceil(5) + 1).keys() ]
|
||||
.map(i => firstWeek.weekNumber + weekIncrementChange + i);
|
||||
let weekNumbers = [...Array(Math.ceil(5) + 1).keys()].map((i) => firstWeek.weekNumber + weekIncrementChange + i);
|
||||
|
||||
if (weekNumbers.includes(55)) {
|
||||
// if we went too far with the weeks, it's the beginning of the year
|
||||
weekNumbers = weekNumbers.map(weekNum => weekNum-52 );
|
||||
weekNumbers = weekNumbers.map((weekNum) => weekNum - 52);
|
||||
}
|
||||
|
||||
const eventsArray = Object.keys(events).map(eventKey => events[eventKey]);
|
||||
const eventsArray = Object.keys(events).map((eventKey) => events[eventKey]);
|
||||
|
||||
return <div className="w-full text-center">
|
||||
<div className="flex-col">
|
||||
<span><button type="button" onClick={ () => setShowDate(showDate.minus({ months: 1 }).startOf("day")) } className={classNames(monthButton)}><</button></span>
|
||||
<span><button type="button" onClick={ () => setShowDate(currentDate.startOf("day")) }>{ showDate.setLocale(i18n.language).toFormat("MMMM y") }</button></span>
|
||||
<span><button type="button" onClick={ () => setShowDate(showDate.plus({ months: 1 }).startOf("day")) } className={classNames(monthButton)}>></button></span>
|
||||
</div>
|
||||
|
||||
<div className="p-2 w-full">
|
||||
<div className="flex justify-between flex-wrap">
|
||||
{ dayNames.map(name => <span key={name} className={classNames(cellStyle)} style={{ width: "14%" }}>{name}</span>) }
|
||||
return (
|
||||
<div className="w-full text-center">
|
||||
<div className="flex-col">
|
||||
<span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowDate(showDate.minus({ months: 1 }).startOf("day"))}
|
||||
className={classNames(monthButton)}
|
||||
>
|
||||
<
|
||||
</button>
|
||||
</span>
|
||||
<span>
|
||||
<button type="button" onClick={() => setShowDate(currentDate.startOf("day"))}>
|
||||
{showDate.setLocale(i18n.language).toFormat("MMMM y")}
|
||||
</button>
|
||||
</span>
|
||||
<span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowDate(showDate.plus({ months: 1 }).startOf("day"))}
|
||||
className={classNames(monthButton)}
|
||||
>
|
||||
>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className={classNames(
|
||||
"flex justify-between flex-wrap",
|
||||
!eventsArray.length && widget?.integrations?.length && "animate-pulse"
|
||||
)}>{weekNumbers.map(weekNumber =>
|
||||
daysInWeek.map(dayInWeek =>
|
||||
<Day key={`week${weekNumber}day${dayInWeek}}`} weekNumber={weekNumber} weekday={dayInWeek} events={eventsArray} />
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
<div className="p-2 w-full">
|
||||
<div className="flex justify-between flex-wrap">
|
||||
{dayNames.map((name) => (
|
||||
<span key={name} className={classNames(cellStyle)} style={{ width: "14%" }}>
|
||||
{name}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col pt-1 pb-1">
|
||||
{eventsArray?.filter(event => showDate.startOf("day").toUnixInteger() === event.date?.startOf("day").toUnixInteger())
|
||||
.map(event => <Event key={`event${event.title}`} event={event} />)}
|
||||
<div
|
||||
className={classNames(
|
||||
"flex justify-between flex-wrap",
|
||||
!eventsArray.length && widget?.integrations?.length && "animate-pulse",
|
||||
)}
|
||||
>
|
||||
{weekNumbers.map((weekNumber) =>
|
||||
daysInWeek.map((dayInWeek) => (
|
||||
<Day
|
||||
key={`week${weekNumber}day${dayInWeek}}`}
|
||||
weekNumber={weekNumber}
|
||||
weekday={dayInWeek}
|
||||
events={eventsArray}
|
||||
/>
|
||||
)),
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col pt-1 pb-1">
|
||||
{eventsArray
|
||||
?.filter((event) => showDate.startOf("day").toUnixInteger() === event.date?.startOf("day").toUnixInteger())
|
||||
.map((event) => (
|
||||
<Event key={`event${event.title}`} event={event} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -24,7 +24,10 @@ export default function Component({ service }) {
|
||||
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="cloudflared.status" value={statsData.result.status.charAt(0).toUpperCase() + statsData.result.status.slice(1)} />
|
||||
<Block
|
||||
label="cloudflared.status"
|
||||
value={statsData.result.status.charAt(0).toUpperCase() + statsData.result.status.slice(1)}
|
||||
/>
|
||||
<Block label="cloudflared.origin_ip" value={originIP} />
|
||||
</Container>
|
||||
);
|
||||
|
||||
@@ -5,12 +5,9 @@ const widget = {
|
||||
proxyHandler: credentialedProxyHandler,
|
||||
|
||||
mappings: {
|
||||
"cfd_tunnel": {
|
||||
cfd_tunnel: {
|
||||
endpoint: "cfd_tunnel",
|
||||
validate: [
|
||||
"success",
|
||||
"result"
|
||||
]
|
||||
validate: ["success", "result"],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -27,7 +27,7 @@ export default function Component({ service }) {
|
||||
|
||||
const params = {
|
||||
convert: `${currencyCode}`,
|
||||
}
|
||||
};
|
||||
|
||||
// slugs >> symbols, not both
|
||||
if (slugs?.length) {
|
||||
@@ -59,7 +59,9 @@ export default function Component({ service }) {
|
||||
}
|
||||
|
||||
const { data } = statsData;
|
||||
const validCryptos = Object.values(data).filter(crypto => crypto.quote[currencyCode][`percent_change_${dateRange}`] !== null)
|
||||
const validCryptos = Object.values(data).filter(
|
||||
(crypto) => crypto.quote[currencyCode][`percent_change_${dateRange}`] !== null,
|
||||
);
|
||||
|
||||
return (
|
||||
<Container service={service}>
|
||||
@@ -84,9 +86,7 @@ export default function Component({ service }) {
|
||||
</div>
|
||||
<div
|
||||
className={`font-bold w-10 mr-2 ${
|
||||
crypto.quote[currencyCode][`percent_change_${dateRange}`] > 0
|
||||
? "text-emerald-300"
|
||||
: "text-rose-300"
|
||||
crypto.quote[currencyCode][`percent_change_${dateRange}`] > 0 ? "text-emerald-300" : "text-rose-300"
|
||||
}`}
|
||||
>
|
||||
{crypto.quote[currencyCode][`percent_change_${dateRange}`].toFixed(2)}%
|
||||
|
||||
@@ -7,7 +7,7 @@ import useWidgetAPI from "utils/proxy/use-widget-api";
|
||||
function getValue(field, data) {
|
||||
let value = data;
|
||||
let lastField = field;
|
||||
let key = '';
|
||||
let key = "";
|
||||
|
||||
while (typeof lastField === "object") {
|
||||
key = Object.keys(lastField)[0] ?? null;
|
||||
@@ -20,7 +20,7 @@ function getValue(field, data) {
|
||||
lastField = lastField[key];
|
||||
}
|
||||
|
||||
if (typeof value === 'undefined') {
|
||||
if (typeof value === "undefined") {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -43,35 +43,35 @@ function formatValue(t, mapping, rawValue) {
|
||||
// Scale the value. Accepts either a number to multiply by or a string
|
||||
// like "12/345".
|
||||
const scale = mapping?.scale;
|
||||
if (typeof scale === 'number') {
|
||||
if (typeof scale === "number") {
|
||||
value *= scale;
|
||||
} else if (typeof scale === 'string') {
|
||||
const parts = scale.split('/');
|
||||
} else if (typeof scale === "string") {
|
||||
const parts = scale.split("/");
|
||||
const numerator = parts[0] ? parseFloat(parts[0]) : 1;
|
||||
const denominator = parts[1] ? parseFloat(parts[1]) : 1;
|
||||
value = value * numerator / denominator;
|
||||
value = (value * numerator) / denominator;
|
||||
}
|
||||
|
||||
// Format the value using a known type.
|
||||
switch (mapping?.format) {
|
||||
case 'number':
|
||||
case "number":
|
||||
value = t("common.number", { value: parseInt(value, 10) });
|
||||
break;
|
||||
case 'float':
|
||||
case "float":
|
||||
value = t("common.number", { value });
|
||||
break;
|
||||
case 'percent':
|
||||
case "percent":
|
||||
value = t("common.percent", { value });
|
||||
break;
|
||||
case 'bytes':
|
||||
case "bytes":
|
||||
value = t("common.bytes", { value });
|
||||
break;
|
||||
case 'bitrate':
|
||||
case "bitrate":
|
||||
value = t("common.bitrate", { value });
|
||||
break;
|
||||
case 'text':
|
||||
case "text":
|
||||
default:
|
||||
// nothing
|
||||
// nothing
|
||||
}
|
||||
|
||||
// Apply fixed suffix.
|
||||
@@ -100,18 +100,22 @@ export default function Component({ service }) {
|
||||
if (!customData) {
|
||||
return (
|
||||
<Container service={service}>
|
||||
{ mappings.slice(0,4).map(item => <Block label={item.label} key={item.label} />) }
|
||||
{mappings.slice(0, 4).map((item) => (
|
||||
<Block label={item.label} key={item.label} />
|
||||
))}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Container service={service}>
|
||||
{ mappings.slice(0,4).map(mapping => <Block
|
||||
label={mapping.label}
|
||||
key={mapping.label}
|
||||
value={formatValue(t, mapping, getValue(mapping.field, customData))}
|
||||
/>) }
|
||||
{mappings.slice(0, 4).map((mapping) => (
|
||||
<Block
|
||||
label={mapping.label}
|
||||
key={mapping.label}
|
||||
value={formatValue(t, mapping, getValue(mapping.field, customData))}
|
||||
/>
|
||||
))}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,8 +8,17 @@ const logger = createLogger("delugeProxyHandler");
|
||||
|
||||
const dataMethod = "web.update_ui";
|
||||
const dataParams = [
|
||||
["queue", "name", "total_wanted", "state", "progress", "download_payload_rate", "upload_payload_rate", "total_remaining"],
|
||||
{}
|
||||
[
|
||||
"queue",
|
||||
"name",
|
||||
"total_wanted",
|
||||
"state",
|
||||
"progress",
|
||||
"download_payload_rate",
|
||||
"upload_payload_rate",
|
||||
"total_remaining",
|
||||
],
|
||||
{},
|
||||
];
|
||||
const loginMethod = "auth.login";
|
||||
|
||||
@@ -45,7 +54,7 @@ export default async function delugeProxyHandler(req, res) {
|
||||
return res.status(400).json({ error: "Invalid proxy service type" });
|
||||
}
|
||||
|
||||
const api = widgets?.[widget.type]?.api
|
||||
const api = widgets?.[widget.type]?.api;
|
||||
const url = new URL(formatApiCall(api, { ...widget }));
|
||||
|
||||
let [status, contentType, data] = await sendRpc(url, dataMethod, dataParams);
|
||||
|
||||
@@ -12,7 +12,7 @@ export default function Component({ service }) {
|
||||
const { data: utilizationData, error: utilizationError } = useWidgetAPI(widget, "utilization");
|
||||
|
||||
if (storageError || infoError || utilizationError) {
|
||||
return <Container service={service} error={ storageError ?? infoError ?? utilizationError } />;
|
||||
return <Container service={service} error={storageError ?? infoError ?? utilizationError} />;
|
||||
}
|
||||
|
||||
if (!storageData || !infoData || !utilizationData) {
|
||||
@@ -30,10 +30,12 @@ export default function Component({ service }) {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const [hour, minutes, seconds] = infoData.data.up_time.split(":");
|
||||
const days = Math.floor(hour / 24);
|
||||
const uptime = `${ t("common.number", { value: days }) } ${ t("diskstation.days") }`;
|
||||
const uptime = `${t("common.number", { value: days })} ${t("diskstation.days")}`;
|
||||
|
||||
// storage info
|
||||
const volume = widget.volume ? storageData.data.vol_info?.find(vol => vol.name === widget.volume) : storageData.data.vol_info?.[0];
|
||||
const volume = widget.volume
|
||||
? storageData.data.vol_info?.find((vol) => vol.name === widget.volume)
|
||||
: storageData.data.vol_info?.[0];
|
||||
const usedBytes = parseFloat(volume?.used_size);
|
||||
const totalBytes = parseFloat(volume?.total_size);
|
||||
const freeBytes = totalBytes - usedBytes;
|
||||
@@ -41,14 +43,18 @@ export default function Component({ service }) {
|
||||
// utilization info
|
||||
const { cpu, memory } = utilizationData.data;
|
||||
const cpuLoad = parseFloat(cpu.user_load) + parseFloat(cpu.system_load);
|
||||
const memoryUsage = 100 - ((100 * (parseFloat(memory.avail_real) + parseFloat(memory.cached))) / parseFloat(memory.total_real));
|
||||
const memoryUsage =
|
||||
100 - (100 * (parseFloat(memory.avail_real) + parseFloat(memory.cached))) / parseFloat(memory.total_real);
|
||||
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="diskstation.uptime" value={ uptime } />
|
||||
<Block label="diskstation.volumeAvailable" value={ t("common.bbytes", { value: freeBytes, maximumFractionDigits: 1 }) } />
|
||||
<Block label="resources.cpu" value={ t("common.percent", { value: cpuLoad }) } />
|
||||
<Block label="resources.mem" value={ t("common.percent", { value: memoryUsage }) } />
|
||||
<Block label="diskstation.uptime" value={uptime} />
|
||||
<Block
|
||||
label="diskstation.volumeAvailable"
|
||||
value={t("common.bbytes", { value: freeBytes, maximumFractionDigits: 1 })}
|
||||
/>
|
||||
<Block label="resources.cpu" value={t("common.percent", { value: cpuLoad })} />
|
||||
<Block label="resources.mem" value={t("common.percent", { value: memoryUsage })} />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import synologyProxyHandler from '../../utils/proxy/handlers/synology'
|
||||
import synologyProxyHandler from "../../utils/proxy/handlers/synology";
|
||||
|
||||
const widget = {
|
||||
// cgiPath and maxVersion are discovered at runtime, don't supply
|
||||
@@ -6,21 +6,21 @@ const widget = {
|
||||
proxyHandler: synologyProxyHandler,
|
||||
|
||||
mappings: {
|
||||
"system_storage": {
|
||||
system_storage: {
|
||||
apiName: "SYNO.Core.System",
|
||||
apiMethod: "info&type=\"storage\"",
|
||||
endpoint: "system_storage"
|
||||
apiMethod: 'info&type="storage"',
|
||||
endpoint: "system_storage",
|
||||
},
|
||||
"system_info": {
|
||||
system_info: {
|
||||
apiName: "SYNO.Core.System",
|
||||
apiMethod: "info",
|
||||
endpoint: "system_info"
|
||||
endpoint: "system_info",
|
||||
},
|
||||
"utilization": {
|
||||
utilization: {
|
||||
apiName: "SYNO.Core.System.Utilization",
|
||||
apiMethod: "get",
|
||||
endpoint: "utilization"
|
||||
}
|
||||
endpoint: "utilization",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ export default function Component({ service }) {
|
||||
const { widget } = service;
|
||||
|
||||
const { data: statusData, error: statusError } = useSWR(
|
||||
`api/docker/status/${widget.container}/${widget.server || ""}`
|
||||
`api/docker/status/${widget.container}/${widget.server || ""}`,
|
||||
);
|
||||
|
||||
const { data: statsData, error: statsError } = useSWR(`api/docker/stats/${widget.container}/${widget.server || ""}`);
|
||||
@@ -46,9 +46,9 @@ export default function Component({ service }) {
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="docker.cpu" value={t("common.percent", { value: calculateCPUPercent(statsData.stats) })} />
|
||||
{statsData.stats.memory_stats.usage &&
|
||||
{statsData.stats.memory_stats.usage && (
|
||||
<Block label="docker.mem" value={t("common.bytes", { value: calculateUsedMemory(statsData.stats) })} />
|
||||
}
|
||||
)}
|
||||
{network && (
|
||||
<>
|
||||
<Block label="docker.rx" value={t("common.bytes", { value: network.rx_bytes })} />
|
||||
|
||||
@@ -12,5 +12,7 @@ export function calculateCPUPercent(stats) {
|
||||
|
||||
export function calculateUsedMemory(stats) {
|
||||
// see https://github.com/docker/cli/blob/dcc161076861177b5eef6cb321722520db3184e7/cli/command/container/stats_helpers.go#L239
|
||||
return stats.memory_stats.usage - (stats.memory_stats.total_inactive_file ?? stats.memory_stats.stats.inactive_file ?? 0)
|
||||
}
|
||||
return (
|
||||
stats.memory_stats.usage - (stats.memory_stats.total_inactive_file ?? stats.memory_stats.stats.inactive_file ?? 0)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import synologyProxyHandler from '../../utils/proxy/handlers/synology'
|
||||
import synologyProxyHandler from "../../utils/proxy/handlers/synology";
|
||||
|
||||
const widget = {
|
||||
// cgiPath and maxVersion are discovered at runtime, don't supply
|
||||
@@ -6,10 +6,10 @@ const widget = {
|
||||
proxyHandler: synologyProxyHandler,
|
||||
|
||||
mappings: {
|
||||
"list": {
|
||||
list: {
|
||||
apiName: "SYNO.DownloadStation.Task",
|
||||
apiMethod: "list&additional=transfer",
|
||||
endpoint: "list"
|
||||
endpoint: "list",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -33,9 +33,12 @@ function SingleSessionEntry({ playCommand, session }) {
|
||||
PlayState: { PositionTicks, IsPaused, IsMuted },
|
||||
} = session;
|
||||
|
||||
const RunTimeTicks = session.NowPlayingItem?.RunTimeTicks ?? session.NowPlayingItem?.CurrentProgram?.RunTimeTicks ?? 0;
|
||||
const RunTimeTicks =
|
||||
session.NowPlayingItem?.RunTimeTicks ?? session.NowPlayingItem?.CurrentProgram?.RunTimeTicks ?? 0;
|
||||
|
||||
const { IsVideoDirect, VideoDecoderIsHardware, VideoEncoderIsHardware } = session?.TranscodingInfo || { IsVideoDirect: true }; // if no transcodinginfo its videodirect
|
||||
const { IsVideoDirect, VideoDecoderIsHardware, VideoEncoderIsHardware } = session?.TranscodingInfo || {
|
||||
IsVideoDirect: true,
|
||||
}; // if no transcodinginfo its videodirect
|
||||
|
||||
const percent = Math.min(1, PositionTicks / RunTimeTicks) * 100;
|
||||
|
||||
@@ -100,9 +103,12 @@ function SessionEntry({ playCommand, session }) {
|
||||
PlayState: { PositionTicks, IsPaused, IsMuted },
|
||||
} = session;
|
||||
|
||||
const RunTimeTicks = session.NowPlayingItem?.RunTimeTicks ?? session.NowPlayingItem?.CurrentProgram?.RunTimeTicks ?? 0;
|
||||
const RunTimeTicks =
|
||||
session.NowPlayingItem?.RunTimeTicks ?? session.NowPlayingItem?.CurrentProgram?.RunTimeTicks ?? 0;
|
||||
|
||||
const { IsVideoDirect, VideoDecoderIsHardware, VideoEncoderIsHardware } = session?.TranscodingInfo || { IsVideoDirect: true }; // if no transcodinginfo its videodirect
|
||||
const { IsVideoDirect, VideoDecoderIsHardware, VideoEncoderIsHardware } = session?.TranscodingInfo || {
|
||||
IsVideoDirect: true,
|
||||
}; // if no transcodinginfo its videodirect
|
||||
|
||||
const percent = Math.min(1, PositionTicks / RunTimeTicks) * 100;
|
||||
|
||||
@@ -153,27 +159,27 @@ function CountBlocks({ service, countData }) {
|
||||
const { t } = useTranslation();
|
||||
// allows filtering
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
if (service.widget?.type === 'jellyfin') service.widget.type = 'emby'
|
||||
if (service.widget?.type === "jellyfin") service.widget.type = "emby";
|
||||
|
||||
if (!countData) {
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="emby.movies" />
|
||||
<Block label="emby.series" />
|
||||
<Block label="emby.episodes" />
|
||||
<Block label="emby.episodes" />
|
||||
<Block label="emby.songs" />
|
||||
</Container>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="emby.movies" value={t("common.number", { value: countData.MovieCount })} />
|
||||
<Block label="emby.series" value={t("common.number", { value: countData.SeriesCount })} />
|
||||
<Block label="emby.episodes" value={t("common.number", { value: countData.EpisodeCount })} />
|
||||
<Block label="emby.episodes" value={t("common.number", { value: countData.EpisodeCount })} />
|
||||
<Block label="emby.songs" value={t("common.number", { value: countData.SongCount })} />
|
||||
</Container>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default function Component({ service }) {
|
||||
@@ -189,11 +195,9 @@ export default function Component({ service }) {
|
||||
refreshInterval: 5000,
|
||||
});
|
||||
|
||||
const {
|
||||
data: countData,
|
||||
error: countError,
|
||||
} = useWidgetAPI(widget, "Count", {
|
||||
refreshInterval: 60000,});
|
||||
const { data: countData, error: countError } = useWidgetAPI(widget, "Count", {
|
||||
refreshInterval: 60000,
|
||||
});
|
||||
|
||||
async function handlePlayCommand(session, command) {
|
||||
const url = formatProxyUrlWithSegments(widget, "PlayControl", {
|
||||
@@ -209,21 +213,23 @@ export default function Component({ service }) {
|
||||
return <Container service={service} error={sessionsError ?? countError} />;
|
||||
}
|
||||
|
||||
const enableBlocks = service.widget?.enableBlocks
|
||||
const enableNowPlaying = service.widget?.enableNowPlaying ?? true
|
||||
const enableBlocks = service.widget?.enableBlocks;
|
||||
const enableNowPlaying = service.widget?.enableNowPlaying ?? true;
|
||||
|
||||
if (!sessionsData || !countData) {
|
||||
return (
|
||||
<>
|
||||
{enableBlocks && <CountBlocks service={service} countData={null} />}
|
||||
{enableNowPlaying && <div className="flex flex-col pb-1">
|
||||
<div className="text-theme-700 dark:text-theme-200 text-xs relative h-5 w-full rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1">
|
||||
<span className="absolute left-2 text-xs mt-[2px]">-</span>
|
||||
</div>
|
||||
<div className="text-theme-700 dark:text-theme-200 text-xs relative h-5 w-full rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1">
|
||||
<span className="absolute left-2 text-xs mt-[2px]">-</span>
|
||||
</div>
|
||||
</div>}
|
||||
{enableBlocks && <CountBlocks service={service} countData={null} />}
|
||||
{enableNowPlaying && (
|
||||
<div className="flex flex-col pb-1">
|
||||
<div className="text-theme-700 dark:text-theme-200 text-xs relative h-5 w-full rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1">
|
||||
<span className="absolute left-2 text-xs mt-[2px]">-</span>
|
||||
</div>
|
||||
<div className="text-theme-700 dark:text-theme-200 text-xs relative h-5 w-full rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1">
|
||||
<span className="absolute left-2 text-xs mt-[2px]">-</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -240,58 +246,56 @@ export default function Component({ service }) {
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
|
||||
if (playing.length === 0) {
|
||||
return (
|
||||
<>
|
||||
{enableBlocks && <CountBlocks service={service} countData={countData} />}
|
||||
<div className="flex flex-col pb-1 mx-1">
|
||||
<div className="text-theme-700 dark:text-theme-200 text-xs relative h-5 w-full rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1">
|
||||
<span className="absolute left-2 text-xs mt-[2px]">{t("emby.no_active")}</span>
|
||||
{enableBlocks && <CountBlocks service={service} countData={countData} />}
|
||||
<div className="flex flex-col pb-1 mx-1">
|
||||
<div className="text-theme-700 dark:text-theme-200 text-xs relative h-5 w-full rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1">
|
||||
<span className="absolute left-2 text-xs mt-[2px]">{t("emby.no_active")}</span>
|
||||
</div>
|
||||
<div className="text-theme-700 dark:text-theme-200 text-xs relative h-5 w-full rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1">
|
||||
<span className="absolute left-2 text-xs mt-[2px]">-</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-theme-700 dark:text-theme-200 text-xs relative h-5 w-full rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1">
|
||||
<span className="absolute left-2 text-xs mt-[2px]">-</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
if (playing.length === 1) {
|
||||
const session = playing[0];
|
||||
return (
|
||||
<>
|
||||
{enableBlocks && <CountBlocks service={service} countData={countData} />}
|
||||
<div className="flex flex-col pb-1 mx-1">
|
||||
<SingleSessionEntry
|
||||
playCommand={(currentSession, command) => handlePlayCommand(currentSession, command)}
|
||||
session={session}
|
||||
/>
|
||||
</div>
|
||||
{enableBlocks && <CountBlocks service={service} countData={countData} />}
|
||||
<div className="flex flex-col pb-1 mx-1">
|
||||
<SingleSessionEntry
|
||||
playCommand={(currentSession, command) => handlePlayCommand(currentSession, command)}
|
||||
session={session}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
if (playing.length > 0)
|
||||
return (
|
||||
<>
|
||||
{enableBlocks && <CountBlocks service={service} countData={countData} />}
|
||||
<div className="flex flex-col pb-1 mx-1">
|
||||
{playing.map((session) => (
|
||||
<SessionEntry
|
||||
key={session.Id}
|
||||
playCommand={(currentSession, command) => handlePlayCommand(currentSession, command)}
|
||||
session={session}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
{enableBlocks && <CountBlocks service={service} countData={countData} />}
|
||||
<div className="flex flex-col pb-1 mx-1">
|
||||
{playing.map((session) => (
|
||||
<SessionEntry
|
||||
key={session.Id}
|
||||
playCommand={(currentSession, command) => handlePlayCommand(currentSession, command)}
|
||||
session={session}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (enableBlocks) {
|
||||
return (
|
||||
<CountBlocks service={service} countData={countData} />
|
||||
)
|
||||
return <CountBlocks service={service} countData={countData} />;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,12 +10,7 @@ const widget = {
|
||||
},
|
||||
Count: {
|
||||
endpoint: "Items/Counts",
|
||||
segments: [
|
||||
"MovieCount",
|
||||
"SeriesCount",
|
||||
"EpisodeCount",
|
||||
"SongCount"
|
||||
]
|
||||
segments: ["MovieCount", "SeriesCount", "EpisodeCount", "SongCount"],
|
||||
},
|
||||
PlayControl: {
|
||||
method: "POST",
|
||||
|
||||
@@ -16,21 +16,34 @@ export default function Component({ service }) {
|
||||
|
||||
if (!stateData) {
|
||||
return (
|
||||
<Container service={service}>,
|
||||
<Container service={service}>
|
||||
,
|
||||
<Block label="evcc.pv_power" />
|
||||
<Block label="evcc.grid_power" />
|
||||
<Block label="evcc.home_power" />
|
||||
<Block label="evcc.charge_power"/>
|
||||
<Block label="evcc.charge_power" />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="evcc.pv_power" value={`${t("common.number", { value: stateData.result.pvPower })} ${t("evcc.watt_hour")}`} />
|
||||
<Block label="evcc.grid_power" value={`${t("common.number", { value: stateData.result.gridPower })} ${t("evcc.watt_hour")}`} />
|
||||
<Block label="evcc.home_power" value={`${t("common.number", { value: stateData.result.homePower })} ${t("evcc.watt_hour")}`} />
|
||||
<Block label="evcc.charge_power" value={`${t("common.number", { value: stateData.result.loadpoints[0].chargePower })} ${t("evcc.watt_hour")}`} />
|
||||
<Block
|
||||
label="evcc.pv_power"
|
||||
value={`${t("common.number", { value: stateData.result.pvPower })} ${t("evcc.watt_hour")}`}
|
||||
/>
|
||||
<Block
|
||||
label="evcc.grid_power"
|
||||
value={`${t("common.number", { value: stateData.result.gridPower })} ${t("evcc.watt_hour")}`}
|
||||
/>
|
||||
<Block
|
||||
label="evcc.home_power"
|
||||
value={`${t("common.number", { value: stateData.result.homePower })} ${t("evcc.watt_hour")}`}
|
||||
/>
|
||||
<Block
|
||||
label="evcc.charge_power"
|
||||
value={`${t("common.number", { value: stateData.result.loadpoints[0].chargePower })} ${t("evcc.watt_hour")}`}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@ const widget = {
|
||||
mappings: {
|
||||
state: {
|
||||
endpoint: "state",
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default widget;
|
||||
export default widget;
|
||||
|
||||
@@ -25,7 +25,7 @@ export default function Component({ service }) {
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="fileflows.queue" value={t("common.number", { value: fileflowsData.queue })} />
|
||||
|
||||
@@ -5,10 +5,10 @@ const widget = {
|
||||
proxyHandler: genericProxyHandler,
|
||||
|
||||
mappings: {
|
||||
"status": {
|
||||
status: {
|
||||
endpoint: "status",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default widget;
|
||||
export default widget;
|
||||
|
||||
@@ -12,7 +12,7 @@ export default function Component({ service }) {
|
||||
const { data: torrentData, error: torrentError } = useWidgetAPI(widget, "torrents");
|
||||
|
||||
if (torrentError || !torrentData?.torrents) {
|
||||
return <Container service={service} error={torrentError ?? {message: "No torrent data returned"}} />;
|
||||
return <Container service={service} error={torrentError ?? { message: "No torrent data returned" }} />;
|
||||
}
|
||||
|
||||
if (!torrentData || !torrentData.torrents) {
|
||||
@@ -31,16 +31,16 @@ export default function Component({ service }) {
|
||||
let completed = 0;
|
||||
let leech = 0;
|
||||
|
||||
Object.values(torrentData.torrents).forEach(torrent => {
|
||||
Object.values(torrentData.torrents).forEach((torrent) => {
|
||||
rateDl += torrent.downRate;
|
||||
rateUl += torrent.upRate;
|
||||
if(torrent.status.includes('complete')){
|
||||
if (torrent.status.includes("complete")) {
|
||||
completed += 1;
|
||||
}
|
||||
if(torrent.status.includes('downloading')){
|
||||
if (torrent.status.includes("downloading")) {
|
||||
leech += 1;
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
return (
|
||||
<Container service={service}>
|
||||
|
||||
@@ -9,16 +9,16 @@ async function login(widget) {
|
||||
logger.debug("flood is rejecting the request, logging in.");
|
||||
const loginUrl = new URL(`${widget.url}/api/auth/authenticate`).toString();
|
||||
|
||||
const loginParams = {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: null
|
||||
const loginParams = {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: null,
|
||||
};
|
||||
|
||||
if (widget.username && widget.password) {
|
||||
loginParams.body = JSON.stringify({
|
||||
"username": widget.username,
|
||||
"password": widget.password
|
||||
username: widget.username,
|
||||
password: widget.password,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -12,21 +12,25 @@ const logger = createLogger(proxyName);
|
||||
|
||||
async function login(widget, service) {
|
||||
const endpoint = "accounts/ClientLogin";
|
||||
const api = widgets?.[widget.type]?.api
|
||||
const api = widgets?.[widget.type]?.api;
|
||||
const loginUrl = new URL(formatApiCall(api, { endpoint, ...widget }));
|
||||
const headers = { "Content-Type": "application/x-www-form-urlencoded" };
|
||||
|
||||
const [, , data,] = await httpProxy(loginUrl, {
|
||||
const [, , data] = await httpProxy(loginUrl, {
|
||||
method: "POST",
|
||||
body: new URLSearchParams({
|
||||
Email: widget.username,
|
||||
Passwd: widget.password
|
||||
Passwd: widget.password,
|
||||
}).toString(),
|
||||
headers,
|
||||
});
|
||||
|
||||
try {
|
||||
const [, token] = data.toString().split("\n").find(line => line.startsWith("Auth=")).split("=")
|
||||
const [, token] = data
|
||||
.toString()
|
||||
.split("\n")
|
||||
.find((line) => line.startsWith("Auth="))
|
||||
.split("=");
|
||||
cache.put(`${sessionTokenCacheKey}.${service}`, token);
|
||||
return { token };
|
||||
} catch (e) {
|
||||
@@ -39,8 +43,8 @@ async function login(widget, service) {
|
||||
async function apiCall(widget, endpoint, service) {
|
||||
const key = `${sessionTokenCacheKey}.${service}`;
|
||||
const headers = {
|
||||
"Authorization": `GoogleLogin auth=${cache.get(key)}`,
|
||||
}
|
||||
Authorization: `GoogleLogin auth=${cache.get(key)}`,
|
||||
};
|
||||
const url = new URL(formatApiCall(widgets[widget.type].api, { endpoint, ...widget }));
|
||||
const method = "GET";
|
||||
|
||||
@@ -92,6 +96,6 @@ export default async function freshrssProxyHandler(req, res) {
|
||||
|
||||
return res.status(200).send({
|
||||
subscriptions: subscriptionData?.subscriptions.length,
|
||||
unread: unreadCountData?.max
|
||||
unread: unreadCountData?.max,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5,9 +5,9 @@ const widget = {
|
||||
proxyHandler: freshrssProxyHandler,
|
||||
mappings: {
|
||||
info: {
|
||||
endpoint: "/"
|
||||
}
|
||||
}
|
||||
endpoint: "/",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default widget;
|
||||
|
||||
@@ -9,7 +9,7 @@ export default function Component({ service }) {
|
||||
const { data: serverData, error: serverError } = useWidgetAPI(widget, "status");
|
||||
const { t } = useTranslation();
|
||||
|
||||
if(serverError){
|
||||
if (serverError) {
|
||||
return <Container service={service} error={serverError} />;
|
||||
}
|
||||
|
||||
@@ -26,26 +26,32 @@ export default function Component({ service }) {
|
||||
if (!serverData) {
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="gamedig.status"/>
|
||||
<Block label="gamedig.name"/>
|
||||
<Block label="gamedig.map"/>
|
||||
<Block label="gamedig.currentPlayers" />
|
||||
<Block label="gamedig.players" />
|
||||
<Block label="gamedig.maxPlayers" />
|
||||
<Block label="gamedig.bots" />
|
||||
<Block label="gamedig.ping" />
|
||||
<Block label="gamedig.status" />
|
||||
<Block label="gamedig.name" />
|
||||
<Block label="gamedig.map" />
|
||||
<Block label="gamedig.currentPlayers" />
|
||||
<Block label="gamedig.players" />
|
||||
<Block label="gamedig.maxPlayers" />
|
||||
<Block label="gamedig.bots" />
|
||||
<Block label="gamedig.ping" />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
const status = serverData.online ? <span className="text-green-500">{t("gamedig.online")}</span> : <span className="text-red-500">{t("gamedig.offline")}</span>;
|
||||
const status = serverData.online ? (
|
||||
<span className="text-green-500">{t("gamedig.online")}</span>
|
||||
) : (
|
||||
<span className="text-red-500">{t("gamedig.offline")}</span>
|
||||
);
|
||||
const name = serverData.online ? serverData.name : "-";
|
||||
const map = serverData.online ? serverData.map : "-";
|
||||
const currentPlayers = serverData.online ? `${serverData.players} / ${serverData.maxplayers}` : "-";
|
||||
const players = serverData.online ? `${serverData.players}` : "-";
|
||||
const maxPlayers = serverData.online ? `${serverData.maxplayers}` : "-";
|
||||
const bots = serverData.online ? `${serverData.bots}` : "-";
|
||||
const ping = serverData.online ? `${t("common.ms", { value: serverData.ping, style: "unit", unit: "millisecond" })}` : "-";
|
||||
const ping = serverData.online
|
||||
? `${t("common.ms", { value: serverData.ping, style: "unit", unit: "millisecond" })}`
|
||||
: "-";
|
||||
|
||||
return (
|
||||
<Container service={service}>
|
||||
|
||||
@@ -6,32 +6,32 @@ const logger = createLogger(proxyName);
|
||||
const gamedig = require("gamedig");
|
||||
|
||||
export default async function gamedigProxyHandler(req, res) {
|
||||
const { group, service } = req.query;
|
||||
const serviceWidget = await getServiceWidget(group, service);
|
||||
const url = new URL(serviceWidget.url);
|
||||
const { group, service } = req.query;
|
||||
const serviceWidget = await getServiceWidget(group, service);
|
||||
const url = new URL(serviceWidget.url);
|
||||
|
||||
try {
|
||||
const serverData = await gamedig.query({
|
||||
type: serviceWidget.serverType,
|
||||
host: url.hostname,
|
||||
port: url.port,
|
||||
givenPortOnly: true,
|
||||
});
|
||||
try {
|
||||
const serverData = await gamedig.query({
|
||||
type: serviceWidget.serverType,
|
||||
host: url.hostname,
|
||||
port: url.port,
|
||||
givenPortOnly: true,
|
||||
});
|
||||
|
||||
res.status(200).send({
|
||||
online: true,
|
||||
name: serverData.name,
|
||||
map: serverData.map,
|
||||
players: serverData.players.length,
|
||||
maxplayers: serverData.maxplayers,
|
||||
bots: serverData.bots.length,
|
||||
ping: serverData.ping,
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error(e);
|
||||
res.status(200).send({
|
||||
online: true,
|
||||
name: serverData.name,
|
||||
map: serverData.map,
|
||||
players: serverData.players.length,
|
||||
maxplayers: serverData.maxplayers,
|
||||
bots: serverData.bots.length,
|
||||
ping: serverData.ping,
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error(e);
|
||||
|
||||
res.status(200).send({
|
||||
online: false
|
||||
});
|
||||
}
|
||||
res.status(200).send({
|
||||
online: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import gamedigProxyHandler from "./proxy";
|
||||
|
||||
const widget = {
|
||||
proxyHandler: gamedigProxyHandler
|
||||
}
|
||||
proxyHandler: gamedigProxyHandler,
|
||||
};
|
||||
|
||||
export default widget;
|
||||
|
||||
@@ -5,7 +5,10 @@ import Block from "components/services/widget/block";
|
||||
import useWidgetAPI from "utils/proxy/use-widget-api";
|
||||
|
||||
function getPerformancePercent(t, performanceRange) {
|
||||
return `${(performanceRange.performance.currentGrossPerformancePercent > 0 ? "+" : "")}${t("common.percent", { value: performanceRange.performance.currentGrossPerformancePercent * 100, maximumFractionDigits: 2 })}`
|
||||
return `${performanceRange.performance.currentGrossPerformancePercent > 0 ? "+" : ""}${t("common.percent", {
|
||||
value: performanceRange.performance.currentGrossPerformancePercent * 100,
|
||||
maximumFractionDigits: 2,
|
||||
})}`;
|
||||
}
|
||||
|
||||
export default function Component({ service }) {
|
||||
@@ -17,7 +20,7 @@ export default function Component({ service }) {
|
||||
const { data: performanceMax, error: ghostfolioErrorMax } = useWidgetAPI(widget, "max");
|
||||
|
||||
if (ghostfolioErrorToday || ghostfolioErrorYear || ghostfolioErrorMax) {
|
||||
const finalError = ghostfolioErrorToday ?? ghostfolioErrorYear ?? ghostfolioErrorMax
|
||||
const finalError = ghostfolioErrorToday ?? ghostfolioErrorYear ?? ghostfolioErrorMax;
|
||||
return <Container service={service} error={finalError} />;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,13 +6,13 @@ const widget = {
|
||||
|
||||
mappings: {
|
||||
today: {
|
||||
endpoint: "1d"
|
||||
endpoint: "1d",
|
||||
},
|
||||
year: {
|
||||
endpoint: "1y"
|
||||
endpoint: "1y",
|
||||
},
|
||||
max: {
|
||||
endpoint: "max"
|
||||
endpoint: "max",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
export default function Block({ position, children }) {
|
||||
return (
|
||||
<div className={`absolute ${position} z-20 text-sm pointer-events-none`}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
return <div className={`absolute ${position} z-20 text-sm pointer-events-none`}>{children}</div>;
|
||||
}
|
||||
|
||||
@@ -14,8 +14,8 @@ class Chart extends PureComponent {
|
||||
<AreaChart data={dataPoints}>
|
||||
<defs>
|
||||
<linearGradient id="color" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="rgb(var(--color-500))" stopOpacity={0.4}/>
|
||||
<stop offset="95%" stopColor="rgb(var(--color-500))" stopOpacity={0.1}/>
|
||||
<stop offset="5%" stopColor="rgb(var(--color-500))" stopOpacity={0.4} />
|
||||
<stop offset="95%" stopColor="rgb(var(--color-500))" stopOpacity={0.1} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<Area
|
||||
@@ -24,7 +24,8 @@ class Chart extends PureComponent {
|
||||
type="monotoneX"
|
||||
dataKey="value"
|
||||
stroke="rgb(var(--color-500))"
|
||||
fillOpacity={1} fill="url(#color)"
|
||||
fillOpacity={1}
|
||||
fill="url(#color)"
|
||||
baseLine={0}
|
||||
/>
|
||||
<Tooltip
|
||||
@@ -34,7 +35,7 @@ class Chart extends PureComponent {
|
||||
classNames="rounded-md text-xs p-0.5"
|
||||
contentStyle={{
|
||||
backgroundColor: "rgb(var(--color-800))",
|
||||
color: "rgb(var(--color-100))"
|
||||
color: "rgb(var(--color-100))",
|
||||
}}
|
||||
/>
|
||||
</AreaChart>
|
||||
|
||||
@@ -11,15 +11,15 @@ class ChartDual extends PureComponent {
|
||||
<div className="absolute -top-1 -left-1 h-[120px] w-[calc(100%+0.5em)] z-0">
|
||||
<div className="overflow-clip z-10 w-full h-full">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<AreaChart data={dataPoints} stackOffset={stackOffset ?? "none"}>
|
||||
<AreaChart data={dataPoints} stackOffset={stackOffset ?? "none"}>
|
||||
<defs>
|
||||
<linearGradient id="colorA" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="rgb(var(--color-800))" stopOpacity={0.8}/>
|
||||
<stop offset="95%" stopColor="rgb(var(--color-800))" stopOpacity={0.5}/>
|
||||
<stop offset="5%" stopColor="rgb(var(--color-800))" stopOpacity={0.8} />
|
||||
<stop offset="95%" stopColor="rgb(var(--color-800))" stopOpacity={0.5} />
|
||||
</linearGradient>
|
||||
<linearGradient id="colorB" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="rgb(var(--color-500))" stopOpacity={0.4}/>
|
||||
<stop offset="95%" stopColor="rgb(var(--color-500))" stopOpacity={0.1}/>
|
||||
<stop offset="5%" stopColor="rgb(var(--color-500))" stopOpacity={0.4} />
|
||||
<stop offset="95%" stopColor="rgb(var(--color-500))" stopOpacity={0.1} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
@@ -30,7 +30,8 @@ class ChartDual extends PureComponent {
|
||||
type="monotoneX"
|
||||
dataKey="a"
|
||||
stroke="rgb(var(--color-700))"
|
||||
fillOpacity={1} fill="url(#colorA)"
|
||||
fillOpacity={1}
|
||||
fill="url(#colorA)"
|
||||
/>
|
||||
<Area
|
||||
name={label[1]}
|
||||
@@ -39,7 +40,8 @@ class ChartDual extends PureComponent {
|
||||
type="monotoneX"
|
||||
dataKey="b"
|
||||
stroke="rgb(var(--color-500))"
|
||||
fillOpacity={1} fill="url(#colorB)"
|
||||
fillOpacity={1}
|
||||
fill="url(#colorB)"
|
||||
/>
|
||||
<Tooltip
|
||||
allowEscapeViewBox={{ x: false, y: false }}
|
||||
@@ -48,9 +50,8 @@ class ChartDual extends PureComponent {
|
||||
classNames="rounded-md text-xs p-0.5"
|
||||
contentStyle={{
|
||||
backgroundColor: "rgb(var(--color-800))",
|
||||
color: "rgb(var(--color-100))"
|
||||
color: "rgb(var(--color-100))",
|
||||
}}
|
||||
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
|
||||
@@ -3,8 +3,8 @@ export default function Container({ children, chart = true, className = "" }) {
|
||||
<div>
|
||||
{children}
|
||||
<div className={`absolute top-0 right-0 bottom-0 left-0 overflow-clip pointer-events-none ${className}`} />
|
||||
{ chart && <div className="h-[68px] overflow-clip" /> }
|
||||
{ !chart && <div className="h-[16px] overflow-clip" /> }
|
||||
{chart && <div className="h-[68px] overflow-clip" />}
|
||||
{!chart && <div className="h-[16px] overflow-clip" />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,9 @@ export default function Tooltip({ active, payload, formatter }) {
|
||||
<div className="bg-theme-800/80 rounded-md text-theme-200 px-2 py-0">
|
||||
{payload.map((pld, id) => (
|
||||
<div key={Math.random()} className="first-of-type:pt-0 pt-0.5">
|
||||
<div>{formatter(pld.value)} {payload[id].name}</div>
|
||||
<div>
|
||||
{formatter(pld.value)} {payload[id].name}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -12,4 +14,4 @@ export default function Tooltip({ active, payload, formatter }) {
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,7 +3,5 @@ import { useTranslation } from "next-i18next";
|
||||
export default function Error() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return <div className="absolute bottom-2 left-2 z-20 text-red-400 text-xs opacity-75">
|
||||
{t("widget.api_error")}
|
||||
</div>;
|
||||
return <div className="absolute bottom-2 left-2 z-20 text-red-400 text-xs opacity-75">{t("widget.api_error")}</div>;
|
||||
}
|
||||
|
||||
@@ -19,87 +19,86 @@ export default function Component({ service }) {
|
||||
|
||||
const [dataPoints, setDataPoints] = useState(new Array(pointsLimit).fill({ value: 0 }, 0, pointsLimit));
|
||||
|
||||
const { data, error } = useWidgetAPI(service.widget, 'cpu', {
|
||||
const { data, error } = useWidgetAPI(service.widget, "cpu", {
|
||||
refreshInterval: 1000,
|
||||
});
|
||||
|
||||
const { data: systemData, error: systemError } = useWidgetAPI(service.widget, 'system');
|
||||
const { data: systemData, error: systemError } = useWidgetAPI(service.widget, "system");
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
setDataPoints((prevDataPoints) => {
|
||||
const newDataPoints = [...prevDataPoints, { value: data.total }];
|
||||
if (newDataPoints.length > pointsLimit) {
|
||||
newDataPoints.shift();
|
||||
}
|
||||
return newDataPoints;
|
||||
if (newDataPoints.length > pointsLimit) {
|
||||
newDataPoints.shift();
|
||||
}
|
||||
return newDataPoints;
|
||||
});
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
if (error) {
|
||||
return <Container chart={chart}><Error error={error} /></Container>;
|
||||
return (
|
||||
<Container chart={chart}>
|
||||
<Error error={error} />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return <Container chart={chart}><Block position="bottom-3 left-3">-</Block></Container>;
|
||||
return (
|
||||
<Container chart={chart}>
|
||||
<Block position="bottom-3 left-3">-</Block>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Container chart={chart}>
|
||||
{ chart && (
|
||||
{chart && (
|
||||
<Chart
|
||||
dataPoints={dataPoints}
|
||||
label={[t("resources.used")]}
|
||||
formatter={(value) => t("common.number", {
|
||||
value,
|
||||
style: "unit",
|
||||
unit: "percent",
|
||||
maximumFractionDigits: 0,
|
||||
})}
|
||||
formatter={(value) =>
|
||||
t("common.number", {
|
||||
value,
|
||||
style: "unit",
|
||||
unit: "percent",
|
||||
maximumFractionDigits: 0,
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{ !chart && systemData && !systemError && (
|
||||
{!chart && systemData && !systemError && (
|
||||
<Block position="top-3 right-3">
|
||||
<div className="text-xs opacity-50">
|
||||
{systemData.linux_distro && `${systemData.linux_distro} - ` }
|
||||
{systemData.os_version && systemData.os_version }
|
||||
{systemData.linux_distro && `${systemData.linux_distro} - `}
|
||||
{systemData.os_version && systemData.os_version}
|
||||
</div>
|
||||
</Block>
|
||||
)}
|
||||
|
||||
{systemData && !systemError && (
|
||||
<Block position="bottom-3 left-3">
|
||||
{systemData.linux_distro && chart && (
|
||||
<div className="text-xs opacity-50">
|
||||
{systemData.linux_distro}
|
||||
</div>
|
||||
)}
|
||||
{systemData.linux_distro && chart && <div className="text-xs opacity-50">{systemData.linux_distro}</div>}
|
||||
|
||||
{systemData.os_version && chart && (
|
||||
<div className="text-xs opacity-50">
|
||||
{systemData.os_version}
|
||||
</div>
|
||||
)}
|
||||
{systemData.os_version && chart && <div className="text-xs opacity-50">{systemData.os_version}</div>}
|
||||
|
||||
{systemData.hostname && (
|
||||
<div className="text-xs opacity-50">
|
||||
{systemData.hostname}
|
||||
</div>
|
||||
)}
|
||||
{systemData.hostname && <div className="text-xs opacity-50">{systemData.hostname}</div>}
|
||||
</Block>
|
||||
)}
|
||||
|
||||
<Block position="bottom-3 right-3">
|
||||
<div className="text-xs font-bold opacity-75">
|
||||
{t("common.number", {
|
||||
value: data.total,
|
||||
style: "unit",
|
||||
unit: "percent",
|
||||
maximumFractionDigits: 0,
|
||||
})} {t("resources.used")}
|
||||
</div>
|
||||
{t("common.number", {
|
||||
value: data.total,
|
||||
style: "unit",
|
||||
unit: "percent",
|
||||
maximumFractionDigits: 0,
|
||||
})}{" "}
|
||||
{t("resources.used")}
|
||||
</div>
|
||||
</Block>
|
||||
</Container>
|
||||
);
|
||||
|
||||
@@ -16,19 +16,22 @@ export default function Component({ service }) {
|
||||
const { t } = useTranslation();
|
||||
const { widget } = service;
|
||||
const { chart } = widget;
|
||||
const [, diskName] = widget.metric.split(':');
|
||||
const [, diskName] = widget.metric.split(":");
|
||||
|
||||
const [dataPoints, setDataPoints] = useState(new Array(pointsLimit).fill({ read_bytes: 0, write_bytes: 0, time_since_update: 0 }, 0, pointsLimit));
|
||||
const [dataPoints, setDataPoints] = useState(
|
||||
new Array(pointsLimit).fill({ read_bytes: 0, write_bytes: 0, time_since_update: 0 }, 0, pointsLimit),
|
||||
);
|
||||
const [ratePoints, setRatePoints] = useState(new Array(pointsLimit).fill({ a: 0, b: 0 }, 0, pointsLimit));
|
||||
|
||||
const { data, error } = useWidgetAPI(service.widget, 'diskio', {
|
||||
const { data, error } = useWidgetAPI(service.widget, "diskio", {
|
||||
refreshInterval: 1000,
|
||||
});
|
||||
|
||||
const calculateRates = (d) => d.map(item => ({
|
||||
a: item.read_bytes / item.time_since_update,
|
||||
b: item.write_bytes / item.time_since_update
|
||||
}));
|
||||
const calculateRates = (d) =>
|
||||
d.map((item) => ({
|
||||
a: item.read_bytes / item.time_since_update,
|
||||
b: item.write_bytes / item.time_since_update,
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
@@ -36,10 +39,10 @@ export default function Component({ service }) {
|
||||
|
||||
setDataPoints((prevDataPoints) => {
|
||||
const newDataPoints = [...prevDataPoints, diskData];
|
||||
if (newDataPoints.length > pointsLimit) {
|
||||
newDataPoints.shift();
|
||||
}
|
||||
return newDataPoints;
|
||||
if (newDataPoints.length > pointsLimit) {
|
||||
newDataPoints.shift();
|
||||
}
|
||||
return newDataPoints;
|
||||
});
|
||||
}
|
||||
}, [data, diskName]);
|
||||
@@ -49,17 +52,29 @@ export default function Component({ service }) {
|
||||
}, [dataPoints]);
|
||||
|
||||
if (error) {
|
||||
return <Container chart={chart}><Error error={error} /></Container>;
|
||||
return (
|
||||
<Container chart={chart}>
|
||||
<Error error={error} />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return <Container chart={chart}><Block position="bottom-3 left-3">-</Block></Container>;
|
||||
return (
|
||||
<Container chart={chart}>
|
||||
<Block position="bottom-3 left-3">-</Block>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
const diskData = data.find((item) => item.disk_name === diskName);
|
||||
|
||||
if (!diskData) {
|
||||
return <Container chart={chart}><Block position="bottom-3 left-3">-</Block></Container>;
|
||||
return (
|
||||
<Container chart={chart}>
|
||||
<Block position="bottom-3 left-3">-</Block>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
const diskRates = calculateRates(dataPoints);
|
||||
@@ -67,14 +82,16 @@ export default function Component({ service }) {
|
||||
|
||||
return (
|
||||
<Container chart={chart}>
|
||||
{ chart && (
|
||||
{chart && (
|
||||
<ChartDual
|
||||
dataPoints={ratePoints}
|
||||
label={[t("glances.read"), t("glances.write")]}
|
||||
max={diskData.critical}
|
||||
formatter={(value) => t("common.bitrate", {
|
||||
value,
|
||||
})}
|
||||
formatter={(value) =>
|
||||
t("common.bitrate", {
|
||||
value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -83,12 +100,14 @@ export default function Component({ service }) {
|
||||
<div className="text-xs opacity-50 text-right">
|
||||
{t("common.bitrate", {
|
||||
value: currentRate.a,
|
||||
})} {t("glances.read")}
|
||||
})}{" "}
|
||||
{t("glances.read")}
|
||||
</div>
|
||||
<div className="text-xs opacity-50 text-right">
|
||||
{t("common.bitrate", {
|
||||
value: currentRate.b,
|
||||
})} {t("glances.write")}
|
||||
})}{" "}
|
||||
{t("glances.write")}
|
||||
</div>
|
||||
</Block>
|
||||
)}
|
||||
|
||||
@@ -10,43 +10,59 @@ export default function Component({ service }) {
|
||||
const { t } = useTranslation();
|
||||
const { widget } = service;
|
||||
const { chart } = widget;
|
||||
const [, fsName] = widget.metric.split('fs:');
|
||||
const [, fsName] = widget.metric.split("fs:");
|
||||
|
||||
const { data, error } = useWidgetAPI(widget, 'fs', {
|
||||
const { data, error } = useWidgetAPI(widget, "fs", {
|
||||
refreshInterval: 1000,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return <Container chart={chart}><Error error={error} /></Container>;
|
||||
return (
|
||||
<Container chart={chart}>
|
||||
<Error error={error} />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return <Container chart={chart}><Block position="bottom-3 left-3">-</Block></Container>;
|
||||
return (
|
||||
<Container chart={chart}>
|
||||
<Block position="bottom-3 left-3">-</Block>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
const fsData = data.find((item) => item[item.key] === fsName);
|
||||
|
||||
if (!fsData) {
|
||||
return <Container chart={chart}><Block position="bottom-3 left-3">-</Block></Container>;
|
||||
return (
|
||||
<Container chart={chart}>
|
||||
<Block position="bottom-3 left-3">-</Block>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Container chart={chart}>
|
||||
{ chart && (
|
||||
{chart && (
|
||||
<div className="absolute top-0 left-0 right-0 bottom-0">
|
||||
<div style={{
|
||||
height: `${Math.max(20, (fsData.size/fsData.free))}%`,
|
||||
}} className="absolute bottom-0 border-t border-t-theme-500 bg-gradient-to-b from-theme-500/40 to-theme-500/10 w-full" />
|
||||
<div
|
||||
style={{
|
||||
height: `${Math.max(20, fsData.size / fsData.free)}%`,
|
||||
}}
|
||||
className="absolute bottom-0 border-t border-t-theme-500 bg-gradient-to-b from-theme-500/40 to-theme-500/10 w-full"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Block position="bottom-3 left-3">
|
||||
{ fsData.used && chart && (
|
||||
{fsData.used && chart && (
|
||||
<div className="text-xs opacity-50">
|
||||
{t("common.bbytes", {
|
||||
value: fsData.used,
|
||||
maximumFractionDigits: 0,
|
||||
})} {t("resources.used")}
|
||||
})}{" "}
|
||||
{t("resources.used")}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -54,18 +70,20 @@ export default function Component({ service }) {
|
||||
{t("common.bbytes", {
|
||||
value: fsData.free,
|
||||
maximumFractionDigits: 1,
|
||||
})} {t("resources.free")}
|
||||
})}{" "}
|
||||
{t("resources.free")}
|
||||
</div>
|
||||
</Block>
|
||||
|
||||
{ !chart && (
|
||||
{!chart && (
|
||||
<Block position="top-3 right-3">
|
||||
{fsData.used && (
|
||||
<div className="text-xs opacity-50">
|
||||
{t("common.bbytes", {
|
||||
value: fsData.used,
|
||||
maximumFractionDigits: 0,
|
||||
})} {t("resources.used")}
|
||||
})}{" "}
|
||||
{t("resources.used")}
|
||||
</div>
|
||||
)}
|
||||
</Block>
|
||||
@@ -76,7 +94,8 @@ export default function Component({ service }) {
|
||||
{t("common.bbytes", {
|
||||
value: fsData.size,
|
||||
maximumFractionDigits: 1,
|
||||
})} {t("resources.total")}
|
||||
})}{" "}
|
||||
{t("resources.total")}
|
||||
</div>
|
||||
</Block>
|
||||
</Container>
|
||||
|
||||
@@ -16,11 +16,11 @@ export default function Component({ service }) {
|
||||
const { t } = useTranslation();
|
||||
const { widget } = service;
|
||||
const { chart } = widget;
|
||||
const [, gpuName] = widget.metric.split(':');
|
||||
const [, gpuName] = widget.metric.split(":");
|
||||
|
||||
const [dataPoints, setDataPoints] = useState(new Array(pointsLimit).fill({ a: 0, b: 0 }, 0, pointsLimit));
|
||||
|
||||
const { data, error } = useWidgetAPI(widget, 'gpu', {
|
||||
const { data, error } = useWidgetAPI(widget, "gpu", {
|
||||
refreshInterval: 1000,
|
||||
});
|
||||
|
||||
@@ -32,68 +32,80 @@ export default function Component({ service }) {
|
||||
if (gpuData) {
|
||||
setDataPoints((prevDataPoints) => {
|
||||
const newDataPoints = [...prevDataPoints, { a: gpuData.mem, b: gpuData.proc }];
|
||||
if (newDataPoints.length > pointsLimit) {
|
||||
newDataPoints.shift();
|
||||
}
|
||||
return newDataPoints;
|
||||
if (newDataPoints.length > pointsLimit) {
|
||||
newDataPoints.shift();
|
||||
}
|
||||
return newDataPoints;
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [data, gpuName]);
|
||||
|
||||
if (error) {
|
||||
return <Container chart={chart}><Error error={error} /></Container>;
|
||||
return (
|
||||
<Container chart={chart}>
|
||||
<Error error={error} />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return <Container chart={chart}><Block position="bottom-3 left-3">-</Block></Container>;
|
||||
return (
|
||||
<Container chart={chart}>
|
||||
<Block position="bottom-3 left-3">-</Block>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line eqeqeq
|
||||
const gpuData = data.find((item) => item[item.key] == gpuName);
|
||||
|
||||
if (!gpuData) {
|
||||
return <Container chart={chart}><Block position="bottom-3 left-3">-</Block></Container>;
|
||||
return (
|
||||
<Container chart={chart}>
|
||||
<Block position="bottom-3 left-3">-</Block>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Container chart={chart}>
|
||||
{ chart && (
|
||||
<ChartDual
|
||||
{chart && (
|
||||
<ChartDual
|
||||
dataPoints={dataPoints}
|
||||
label={[t("glances.mem"), t("glances.gpu")]}
|
||||
stack={['mem', 'proc']}
|
||||
formatter={(value) => t("common.percent", {
|
||||
value,
|
||||
maximumFractionDigits: 1,
|
||||
})}
|
||||
stack={["mem", "proc"]}
|
||||
formatter={(value) =>
|
||||
t("common.percent", {
|
||||
value,
|
||||
maximumFractionDigits: 1,
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{ chart && (
|
||||
{chart && (
|
||||
<Block position="bottom-3 left-3">
|
||||
{gpuData && gpuData.name && (
|
||||
<div className="text-xs opacity-50">
|
||||
{gpuData.name}
|
||||
</div>
|
||||
)}
|
||||
{gpuData && gpuData.name && <div className="text-xs opacity-50">{gpuData.name}</div>}
|
||||
|
||||
<div className="text-xs opacity-50">
|
||||
{t("common.number", {
|
||||
value: gpuData.mem,
|
||||
maximumFractionDigits: 1,
|
||||
})}% {t("resources.mem")}
|
||||
})}
|
||||
% {t("resources.mem")}
|
||||
</div>
|
||||
</Block>
|
||||
)}
|
||||
|
||||
{ !chart && (
|
||||
{!chart && (
|
||||
<Block position="bottom-3 left-3">
|
||||
<div className="text-xs opacity-50">
|
||||
{t("common.number", {
|
||||
value: gpuData.temperature,
|
||||
maximumFractionDigits: 1,
|
||||
})}° C
|
||||
})}
|
||||
° C
|
||||
</div>
|
||||
</Block>
|
||||
)}
|
||||
@@ -105,36 +117,33 @@ export default function Component({ service }) {
|
||||
{t("common.number", {
|
||||
value: gpuData.proc,
|
||||
maximumFractionDigits: 1,
|
||||
})}% {t("glances.gpu")}
|
||||
})}
|
||||
% {t("glances.gpu")}
|
||||
</div>
|
||||
)}
|
||||
{ !chart && (
|
||||
<>•</>
|
||||
)}
|
||||
{!chart && <>•</>}
|
||||
<div className="inline-block ml-1">
|
||||
{t("common.number", {
|
||||
value: gpuData.proc,
|
||||
maximumFractionDigits: 1,
|
||||
})}% {t("glances.gpu")}
|
||||
})}
|
||||
% {t("glances.gpu")}
|
||||
</div>
|
||||
</div>
|
||||
</Block>
|
||||
|
||||
<Block position="top-3 right-3">
|
||||
{ chart && (
|
||||
{chart && (
|
||||
<div className="text-xs opacity-50">
|
||||
{t("common.number", {
|
||||
value: gpuData.temperature,
|
||||
maximumFractionDigits: 1,
|
||||
})}° C
|
||||
})}
|
||||
° C
|
||||
</div>
|
||||
)}
|
||||
|
||||
{gpuData && gpuData.name && !chart && (
|
||||
<div className="text-xs opacity-50">
|
||||
{gpuData.name}
|
||||
</div>
|
||||
)}
|
||||
{gpuData && gpuData.name && !chart && <div className="text-xs opacity-50">{gpuData.name}</div>}
|
||||
</Block>
|
||||
</Container>
|
||||
);
|
||||
|
||||
@@ -6,58 +6,66 @@ import Block from "../components/block";
|
||||
|
||||
import useWidgetAPI from "utils/proxy/use-widget-api";
|
||||
|
||||
|
||||
function Swap({ quicklookData, className = "" }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return quicklookData && quicklookData.swap !== 0 && (
|
||||
<div className="text-xs flex place-content-between">
|
||||
<div className={className}>{t("glances.swap")}</div>
|
||||
<div className={className}>
|
||||
{t("common.number", {
|
||||
value: quicklookData.swap,
|
||||
style: "unit",
|
||||
unit: "percent",
|
||||
maximumFractionDigits: 0,
|
||||
})}
|
||||
return (
|
||||
quicklookData &&
|
||||
quicklookData.swap !== 0 && (
|
||||
<div className="text-xs flex place-content-between">
|
||||
<div className={className}>{t("glances.swap")}</div>
|
||||
<div className={className}>
|
||||
{t("common.number", {
|
||||
value: quicklookData.swap,
|
||||
style: "unit",
|
||||
unit: "percent",
|
||||
maximumFractionDigits: 0,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function CPU({ quicklookData, className = "" }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return quicklookData && quicklookData.cpu && (
|
||||
<div className="text-xs flex place-content-between">
|
||||
<div className={className}>{t("glances.cpu")}</div>
|
||||
<div className={className}>
|
||||
{t("common.number", {
|
||||
value: quicklookData.cpu,
|
||||
style: "unit",
|
||||
unit: "percent",
|
||||
maximumFractionDigits: 0,
|
||||
})}
|
||||
return (
|
||||
quicklookData &&
|
||||
quicklookData.cpu && (
|
||||
<div className="text-xs flex place-content-between">
|
||||
<div className={className}>{t("glances.cpu")}</div>
|
||||
<div className={className}>
|
||||
{t("common.number", {
|
||||
value: quicklookData.cpu,
|
||||
style: "unit",
|
||||
unit: "percent",
|
||||
maximumFractionDigits: 0,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function Mem({ quicklookData, className = "" }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return quicklookData && quicklookData.mem && (
|
||||
<div className="text-xs flex place-content-between">
|
||||
<div className={className}>{t("glances.mem")}</div>
|
||||
<div className={className}>
|
||||
{t("common.number", {
|
||||
value: quicklookData.mem,
|
||||
style: "unit",
|
||||
unit: "percent",
|
||||
maximumFractionDigits: 0,
|
||||
})}
|
||||
return (
|
||||
quicklookData &&
|
||||
quicklookData.mem && (
|
||||
<div className="text-xs flex place-content-between">
|
||||
<div className={className}>{t("glances.mem")}</div>
|
||||
<div className={className}>
|
||||
{t("common.number", {
|
||||
value: quicklookData.mem,
|
||||
style: "unit",
|
||||
unit: "percent",
|
||||
maximumFractionDigits: 0,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -65,20 +73,28 @@ export default function Component({ service }) {
|
||||
const { widget } = service;
|
||||
const { chart } = widget;
|
||||
|
||||
const { data: quicklookData, errorL: quicklookError } = useWidgetAPI(service.widget, 'quicklook', {
|
||||
const { data: quicklookData, errorL: quicklookError } = useWidgetAPI(service.widget, "quicklook", {
|
||||
refreshInterval: 1000,
|
||||
});
|
||||
|
||||
const { data: systemData, errorL: systemError } = useWidgetAPI(service.widget, 'system', {
|
||||
const { data: systemData, errorL: systemError } = useWidgetAPI(service.widget, "system", {
|
||||
refreshInterval: 30000,
|
||||
});
|
||||
|
||||
if (quicklookError) {
|
||||
return <Container chart={chart}><Error error={quicklookError} /></Container>;
|
||||
return (
|
||||
<Container chart={chart}>
|
||||
<Error error={quicklookError} />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
if (systemError) {
|
||||
return <Container chart={chart}><Error error={systemError} /></Container>;
|
||||
return (
|
||||
<Container chart={chart}>
|
||||
<Error error={systemError} />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
const dataCharts = [];
|
||||
@@ -95,45 +111,25 @@ export default function Component({ service }) {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<Container chart={chart} className="bg-gradient-to-br from-theme-500/30 via-theme-600/20 to-theme-700/10">
|
||||
<Block position="top-3 right-3">
|
||||
{quicklookData && quicklookData.cpu_name && chart && (
|
||||
<div className="text-[0.6rem] opacity-50">
|
||||
{quicklookData.cpu_name}
|
||||
</div>
|
||||
<div className="text-[0.6rem] opacity-50">{quicklookData.cpu_name}</div>
|
||||
)}
|
||||
|
||||
{ !chart && quicklookData?.swap === 0 && (
|
||||
<div className="text-[0.6rem] opacity-50">
|
||||
{quicklookData.cpu_name}
|
||||
</div>
|
||||
{!chart && quicklookData?.swap === 0 && (
|
||||
<div className="text-[0.6rem] opacity-50">{quicklookData.cpu_name}</div>
|
||||
)}
|
||||
|
||||
<div className="w-[4rem]">
|
||||
{ !chart && <Swap quicklookData={quicklookData} className="opacity-25" /> }
|
||||
</div>
|
||||
<div className="w-[4rem]">{!chart && <Swap quicklookData={quicklookData} className="opacity-25" />}</div>
|
||||
</Block>
|
||||
|
||||
|
||||
{chart && (
|
||||
<Block position="bottom-3 left-3">
|
||||
{systemData && systemData.linux_distro && (
|
||||
<div className="text-xs opacity-50">
|
||||
{systemData.linux_distro}
|
||||
</div>
|
||||
)}
|
||||
{systemData && systemData.os_version && (
|
||||
<div className="text-xs opacity-50">
|
||||
{systemData.os_version}
|
||||
</div>
|
||||
)}
|
||||
{systemData && systemData.hostname && (
|
||||
<div className="text-xs opacity-75">
|
||||
{systemData.hostname}
|
||||
</div>
|
||||
)}
|
||||
{systemData && systemData.linux_distro && <div className="text-xs opacity-50">{systemData.linux_distro}</div>}
|
||||
{systemData && systemData.os_version && <div className="text-xs opacity-50">{systemData.os_version}</div>}
|
||||
{systemData && systemData.hostname && <div className="text-xs opacity-75">{systemData.hostname}</div>}
|
||||
</Block>
|
||||
)}
|
||||
|
||||
@@ -144,12 +140,12 @@ export default function Component({ service }) {
|
||||
)}
|
||||
|
||||
<Block position="bottom-3 right-3 w-[4rem]">
|
||||
{ chart && <CPU quicklookData={quicklookData} className="opacity-50" /> }
|
||||
{chart && <CPU quicklookData={quicklookData} className="opacity-50" />}
|
||||
|
||||
{ chart && <Mem quicklookData={quicklookData} className="opacity-50" /> }
|
||||
{ !chart && <Mem quicklookData={quicklookData} className="opacity-75" /> }
|
||||
{chart && <Mem quicklookData={quicklookData} className="opacity-50" />}
|
||||
{!chart && <Mem quicklookData={quicklookData} className="opacity-75" />}
|
||||
|
||||
{ chart && <Swap quicklookData={quicklookData} className="opacity-50" /> }
|
||||
{chart && <Swap quicklookData={quicklookData} className="opacity-50" />}
|
||||
</Block>
|
||||
</Container>
|
||||
);
|
||||
|
||||
@@ -17,10 +17,9 @@ export default function Component({ service }) {
|
||||
const { widget } = service;
|
||||
const { chart } = widget;
|
||||
|
||||
|
||||
const [dataPoints, setDataPoints] = useState(new Array(pointsLimit).fill({ value: 0 }, 0, pointsLimit));
|
||||
|
||||
const { data, error } = useWidgetAPI(service.widget, 'mem', {
|
||||
const { data, error } = useWidgetAPI(service.widget, "mem", {
|
||||
refreshInterval: chart ? 1000 : 5000,
|
||||
});
|
||||
|
||||
@@ -28,34 +27,44 @@ export default function Component({ service }) {
|
||||
if (data) {
|
||||
setDataPoints((prevDataPoints) => {
|
||||
const newDataPoints = [...prevDataPoints, { a: data.used, b: data.free }];
|
||||
if (newDataPoints.length > pointsLimit) {
|
||||
newDataPoints.shift();
|
||||
}
|
||||
return newDataPoints;
|
||||
if (newDataPoints.length > pointsLimit) {
|
||||
newDataPoints.shift();
|
||||
}
|
||||
return newDataPoints;
|
||||
});
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
if (error) {
|
||||
return <Container chart={chart}><Error error={error} /></Container>;
|
||||
return (
|
||||
<Container chart={chart}>
|
||||
<Error error={error} />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return <Container chart={chart}><Block position="bottom-3 left-3">-</Block></Container>;
|
||||
return (
|
||||
<Container chart={chart}>
|
||||
<Block position="bottom-3 left-3">-</Block>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Container chart={chart} >
|
||||
<Container chart={chart}>
|
||||
{chart && (
|
||||
<ChartDual
|
||||
dataPoints={dataPoints}
|
||||
max={data.total}
|
||||
label={[t("resources.used"), t("resources.free")]}
|
||||
formatter={(value) => t("common.bytes", {
|
||||
value,
|
||||
maximumFractionDigits: 0,
|
||||
binary: true,
|
||||
})}
|
||||
formatter={(value) =>
|
||||
t("common.bytes", {
|
||||
value,
|
||||
maximumFractionDigits: 0,
|
||||
binary: true,
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -67,7 +76,8 @@ export default function Component({ service }) {
|
||||
value: data.free,
|
||||
maximumFractionDigits: 1,
|
||||
binary: true,
|
||||
})} {t("resources.free")}
|
||||
})}{" "}
|
||||
{t("resources.free")}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -77,13 +87,14 @@ export default function Component({ service }) {
|
||||
value: data.total,
|
||||
maximumFractionDigits: 1,
|
||||
binary: true,
|
||||
})} {t("resources.total")}
|
||||
})}{" "}
|
||||
{t("resources.total")}
|
||||
</div>
|
||||
)}
|
||||
</Block>
|
||||
)}
|
||||
|
||||
{ !chart && (
|
||||
{!chart && (
|
||||
<Block position="top-3 right-3">
|
||||
{data.free && (
|
||||
<div className="text-xs opacity-50">
|
||||
@@ -91,7 +102,8 @@ export default function Component({ service }) {
|
||||
value: data.free,
|
||||
maximumFractionDigits: 1,
|
||||
binary: true,
|
||||
})} {t("resources.free")}
|
||||
})}{" "}
|
||||
{t("resources.free")}
|
||||
</div>
|
||||
)}
|
||||
</Block>
|
||||
@@ -103,7 +115,8 @@ export default function Component({ service }) {
|
||||
value: data.used,
|
||||
maximumFractionDigits: 1,
|
||||
binary: true,
|
||||
})} {t("resources.used")}
|
||||
})}{" "}
|
||||
{t("resources.used")}
|
||||
</div>
|
||||
</Block>
|
||||
</Container>
|
||||
|
||||
@@ -16,11 +16,11 @@ export default function Component({ service }) {
|
||||
const { t } = useTranslation();
|
||||
const { widget } = service;
|
||||
const { chart, metric } = widget;
|
||||
const [, interfaceName] = metric.split(':');
|
||||
const [, interfaceName] = metric.split(":");
|
||||
|
||||
const [dataPoints, setDataPoints] = useState(new Array(pointsLimit).fill({ value: 0 }, 0, pointsLimit));
|
||||
|
||||
const { data, error } = useWidgetAPI(widget, 'network', {
|
||||
const { data, error } = useWidgetAPI(widget, "network", {
|
||||
refreshInterval: chart ? 1000 : 5000,
|
||||
});
|
||||
|
||||
@@ -30,64 +30,81 @@ export default function Component({ service }) {
|
||||
|
||||
if (interfaceData) {
|
||||
setDataPoints((prevDataPoints) => {
|
||||
const newDataPoints = [...prevDataPoints, { a: (interfaceData.rx * 8) / interfaceData.time_since_update, b: (interfaceData.tx * 8) / interfaceData.time_since_update }];
|
||||
if (newDataPoints.length > pointsLimit) {
|
||||
newDataPoints.shift();
|
||||
}
|
||||
return newDataPoints;
|
||||
const newDataPoints = [
|
||||
...prevDataPoints,
|
||||
{
|
||||
a: (interfaceData.rx * 8) / interfaceData.time_since_update,
|
||||
b: (interfaceData.tx * 8) / interfaceData.time_since_update,
|
||||
},
|
||||
];
|
||||
if (newDataPoints.length > pointsLimit) {
|
||||
newDataPoints.shift();
|
||||
}
|
||||
return newDataPoints;
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [data, interfaceName]);
|
||||
|
||||
if (error) {
|
||||
return <Container chart={chart}><Error error={error} /></Container>;
|
||||
return (
|
||||
<Container chart={chart}>
|
||||
<Error error={error} />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return <Container chart={chart}><Block position="bottom-3 left-3">-</Block></Container>;
|
||||
return (
|
||||
<Container chart={chart}>
|
||||
<Block position="bottom-3 left-3">-</Block>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
const interfaceData = data.find((item) => item[item.key] === interfaceName);
|
||||
|
||||
if (!interfaceData) {
|
||||
return <Container chart={chart}><Block position="bottom-3 left-3">-</Block></Container>;
|
||||
return (
|
||||
<Container chart={chart}>
|
||||
<Block position="bottom-3 left-3">-</Block>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Container chart={chart}>
|
||||
{ chart && (
|
||||
{chart && (
|
||||
<ChartDual
|
||||
dataPoints={dataPoints}
|
||||
label={[t("docker.rx"), t("docker.tx")]}
|
||||
formatter={(value) => t("common.bitrate", {
|
||||
value,
|
||||
maximumFractionDigits: 0,
|
||||
})}
|
||||
formatter={(value) =>
|
||||
t("common.bitrate", {
|
||||
value,
|
||||
maximumFractionDigits: 0,
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Block position="bottom-3 left-3">
|
||||
{interfaceData && interfaceData.interface_name && chart && (
|
||||
<div className="text-xs opacity-50">
|
||||
{interfaceData.interface_name}
|
||||
</div>
|
||||
<div className="text-xs opacity-50">{interfaceData.interface_name}</div>
|
||||
)}
|
||||
|
||||
<div className="text-xs opacity-75">
|
||||
{t("common.bitrate", {
|
||||
value: (interfaceData.rx * 8) / interfaceData.time_since_update,
|
||||
maximumFractionDigits: 0,
|
||||
})} {t("docker.rx")}
|
||||
})}{" "}
|
||||
{t("docker.rx")}
|
||||
</div>
|
||||
</Block>
|
||||
|
||||
{ !chart && (
|
||||
{!chart && (
|
||||
<Block position="top-3 right-3">
|
||||
{interfaceData && interfaceData.interface_name && (
|
||||
<div className="text-xs opacity-50">
|
||||
{interfaceData.interface_name}
|
||||
</div>
|
||||
<div className="text-xs opacity-50">{interfaceData.interface_name}</div>
|
||||
)}
|
||||
</Block>
|
||||
)}
|
||||
@@ -97,7 +114,8 @@ export default function Component({ service }) {
|
||||
{t("common.bitrate", {
|
||||
value: (interfaceData.tx * 8) / interfaceData.time_since_update,
|
||||
maximumFractionDigits: 0,
|
||||
})} {t("docker.tx")}
|
||||
})}{" "}
|
||||
{t("docker.tx")}
|
||||
</div>
|
||||
</Block>
|
||||
</Container>
|
||||
|
||||
@@ -8,13 +8,13 @@ import useWidgetAPI from "utils/proxy/use-widget-api";
|
||||
import ResolvedIcon from "components/resolvedicon";
|
||||
|
||||
const statusMap = {
|
||||
"R": <ResolvedIcon icon="mdi-circle" width={32} height={32} />, // running
|
||||
"S": <ResolvedIcon icon="mdi-circle-outline" width={32} height={32} />, // sleeping
|
||||
"D": <ResolvedIcon icon="mdi-circle-double" width={32} height={32} />, // disk sleep
|
||||
"Z": <ResolvedIcon icon="mdi-circle-opacity" width={32} height={32} />, // zombie
|
||||
"T": <ResolvedIcon icon="mdi-decagram-outline" width={32} height={32} />, // traced
|
||||
"t": <ResolvedIcon icon="mdi-hexagon-outline" width={32} height={32} />, // traced
|
||||
"X": <ResolvedIcon icon="mdi-rhombus-outline" width={32} height={32} />, // dead
|
||||
R: <ResolvedIcon icon="mdi-circle" width={32} height={32} />, // running
|
||||
S: <ResolvedIcon icon="mdi-circle-outline" width={32} height={32} />, // sleeping
|
||||
D: <ResolvedIcon icon="mdi-circle-double" width={32} height={32} />, // disk sleep
|
||||
Z: <ResolvedIcon icon="mdi-circle-opacity" width={32} height={32} />, // zombie
|
||||
T: <ResolvedIcon icon="mdi-decagram-outline" width={32} height={32} />, // traced
|
||||
t: <ResolvedIcon icon="mdi-hexagon-outline" width={32} height={32} />, // traced
|
||||
X: <ResolvedIcon icon="mdi-rhombus-outline" width={32} height={32} />, // dead
|
||||
};
|
||||
|
||||
export default function Component({ service }) {
|
||||
@@ -22,16 +22,24 @@ export default function Component({ service }) {
|
||||
const { widget } = service;
|
||||
const { chart } = widget;
|
||||
|
||||
const { data, error } = useWidgetAPI(service.widget, 'processlist', {
|
||||
const { data, error } = useWidgetAPI(service.widget, "processlist", {
|
||||
refreshInterval: 1000,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return <Container chart={chart}><Error error={error} /></Container>;
|
||||
return (
|
||||
<Container chart={chart}>
|
||||
<Error error={error} />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return <Container chart={chart}><Block position="bottom-3 left-3">-</Block></Container>;
|
||||
return (
|
||||
<Container chart={chart}>
|
||||
<Block position="bottom-3 left-3">-</Block>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
data.splice(chart ? 5 : 1);
|
||||
@@ -48,19 +56,21 @@ export default function Component({ service }) {
|
||||
|
||||
<Block position="bottom-4 right-3 left-3">
|
||||
<div className="pointer-events-none text-theme-900 dark:text-theme-200">
|
||||
{ data.map((item) => <div key={item.pid} className="text-[0.75rem] h-[0.8rem]">
|
||||
<div className="flex items-center">
|
||||
<div className="w-3 h-3 mr-1.5 opacity-50">
|
||||
{statusMap[item.status]}
|
||||
{data.map((item) => (
|
||||
<div key={item.pid} className="text-[0.75rem] h-[0.8rem]">
|
||||
<div className="flex items-center">
|
||||
<div className="w-3 h-3 mr-1.5 opacity-50">{statusMap[item.status]}</div>
|
||||
<div className="opacity-75 grow">{item.name}</div>
|
||||
<div className="opacity-25 w-14 text-right">{item.cpu_percent.toFixed(1)}%</div>
|
||||
<div className="opacity-25 w-14 text-right">
|
||||
{t("common.bytes", {
|
||||
value: item.memory_info[0],
|
||||
maximumFractionDigits: 0,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className="opacity-75 grow">{item.name}</div>
|
||||
<div className="opacity-25 w-14 text-right">{item.cpu_percent.toFixed(1)}%</div>
|
||||
<div className="opacity-25 w-14 text-right">{t("common.bytes", {
|
||||
value: item.memory_info[0],
|
||||
maximumFractionDigits: 0,
|
||||
})}</div>
|
||||
</div>
|
||||
</div>) }
|
||||
))}
|
||||
</div>
|
||||
</Block>
|
||||
</Container>
|
||||
|
||||
@@ -16,11 +16,11 @@ export default function Component({ service }) {
|
||||
const { t } = useTranslation();
|
||||
const { widget } = service;
|
||||
const { chart } = widget;
|
||||
const [, sensorName] = widget.metric.split(':');
|
||||
const [, sensorName] = widget.metric.split(":");
|
||||
|
||||
const [dataPoints, setDataPoints] = useState(new Array(pointsLimit).fill({ value: 0 }, 0, pointsLimit));
|
||||
|
||||
const { data, error } = useWidgetAPI(service.widget, 'sensors', {
|
||||
const { data, error } = useWidgetAPI(service.widget, "sensors", {
|
||||
refreshInterval: 1000,
|
||||
});
|
||||
|
||||
@@ -29,38 +29,52 @@ export default function Component({ service }) {
|
||||
const sensorData = data.find((item) => item.label === sensorName);
|
||||
setDataPoints((prevDataPoints) => {
|
||||
const newDataPoints = [...prevDataPoints, { value: sensorData.value }];
|
||||
if (newDataPoints.length > pointsLimit) {
|
||||
newDataPoints.shift();
|
||||
}
|
||||
return newDataPoints;
|
||||
if (newDataPoints.length > pointsLimit) {
|
||||
newDataPoints.shift();
|
||||
}
|
||||
return newDataPoints;
|
||||
});
|
||||
}
|
||||
}, [data, sensorName]);
|
||||
|
||||
if (error) {
|
||||
return <Container chart={chart}><Error error={error} /></Container>;
|
||||
return (
|
||||
<Container chart={chart}>
|
||||
<Error error={error} />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return <Container chart={chart}><Block position="bottom-3 left-3">-</Block></Container>;
|
||||
return (
|
||||
<Container chart={chart}>
|
||||
<Block position="bottom-3 left-3">-</Block>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
const sensorData = data.find((item) => item.label === sensorName);
|
||||
|
||||
if (!sensorData) {
|
||||
return <Container chart={chart}><Block position="bottom-3 left-3">-</Block></Container>;
|
||||
return (
|
||||
<Container chart={chart}>
|
||||
<Block position="bottom-3 left-3">-</Block>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Container chart={chart}>
|
||||
{ chart && (
|
||||
{chart && (
|
||||
<Chart
|
||||
dataPoints={dataPoints}
|
||||
label={[sensorData.unit]}
|
||||
max={sensorData.critical}
|
||||
formatter={(value) => t("common.number", {
|
||||
value,
|
||||
})}
|
||||
formatter={(value) =>
|
||||
t("common.number", {
|
||||
value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -80,18 +94,20 @@ export default function Component({ service }) {
|
||||
)}
|
||||
|
||||
<Block position="bottom-3 right-3">
|
||||
<div className="text-xs opacity-50">
|
||||
{sensorData.warning && !chart && (
|
||||
<>
|
||||
{t("glances.warn")} {sensorData.warning} {sensorData.unit}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs opacity-75">
|
||||
{t("glances.temp")} {t("common.number", {
|
||||
value: sensorData.value,
|
||||
})} {sensorData.unit}
|
||||
</div>
|
||||
<div className="text-xs opacity-50">
|
||||
{sensorData.warning && !chart && (
|
||||
<>
|
||||
{t("glances.warn")} {sensorData.warning} {sensorData.unit}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs opacity-75">
|
||||
{t("glances.temp")}{" "}
|
||||
{t("common.number", {
|
||||
value: sensorData.value,
|
||||
})}{" "}
|
||||
{sensorData.unit}
|
||||
</div>
|
||||
</Block>
|
||||
</Container>
|
||||
);
|
||||
|
||||
@@ -7,11 +7,7 @@ const widget = {
|
||||
mappings: {
|
||||
ip: {
|
||||
endpoint: "publicip/ip",
|
||||
validate: [
|
||||
"public_ip",
|
||||
"region",
|
||||
"country"
|
||||
]
|
||||
validate: ["public_ip", "region", "country"],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -14,7 +14,6 @@ export default function Component({ service }) {
|
||||
return <Container service={service} error={finalError} />;
|
||||
}
|
||||
|
||||
|
||||
if (!appsData || !messagesData || !clientsData) {
|
||||
return (
|
||||
<Container service={service}>
|
||||
|
||||
@@ -31,7 +31,10 @@ export default function Component({ service }) {
|
||||
<Block label="grafana.dashboards" value={t("common.number", { value: statsData.dashboards })} />
|
||||
<Block label="grafana.datasources" value={t("common.number", { value: statsData.datasources })} />
|
||||
<Block label="grafana.totalalerts" value={t("common.number", { value: statsData.alerts })} />
|
||||
<Block label="grafana.alertstriggered" value={t("common.number", { value: alertsData.filter(a => a.state === "alerting").length })} />
|
||||
<Block
|
||||
label="grafana.alertstriggered"
|
||||
value={t("common.number", { value: alertsData.filter((a) => a.state === "alerting").length })}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,11 +10,9 @@ const widget = {
|
||||
},
|
||||
stats: {
|
||||
endpoint: "admin/stats",
|
||||
validate: [
|
||||
"dashboards"
|
||||
]
|
||||
validate: ["dashboards"],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default widget;
|
||||
export default widget;
|
||||
|
||||
@@ -20,13 +20,12 @@ export default function Component({ service }) {
|
||||
);
|
||||
}
|
||||
|
||||
const hdChannels = channelsData?.filter((channel) => channel.HD === 1);
|
||||
const hdChannels = channelsData?.filter((channel) => channel.HD === 1);
|
||||
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="hdhomerun.channels" value={channelsData.length } />
|
||||
<Block label="hdhomerun.channels" value={channelsData.length} />
|
||||
<Block label="hdhomerun.hd" value={hdChannels.length} />
|
||||
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,9 +5,9 @@ const widget = {
|
||||
proxyHandler: genericProxyHandler,
|
||||
|
||||
mappings: {
|
||||
"lineup": {
|
||||
lineup: {
|
||||
endpoint: "lineup.json",
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -15,11 +15,15 @@ function formatDate(dateString) {
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
};
|
||||
|
||||
if (date.getFullYear() === now.getFullYear() && date.getMonth() === now.getMonth() && date.getDate() === now.getDate()) {
|
||||
|
||||
if (
|
||||
date.getFullYear() === now.getFullYear() &&
|
||||
date.getMonth() === now.getMonth() &&
|
||||
date.getDate() === now.getDate()
|
||||
) {
|
||||
dateOptions = { timeStyle: "short" };
|
||||
}
|
||||
|
||||
|
||||
return new Intl.DateTimeFormat(i18n.language, dateOptions).format(date);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,10 +7,7 @@ const widget = {
|
||||
mappings: {
|
||||
checks: {
|
||||
endpoint: "checks",
|
||||
validate: [
|
||||
"status",
|
||||
"last_ping",
|
||||
]
|
||||
validate: ["status", "last_ping"],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -9,8 +9,12 @@ export default function Component({ service }) {
|
||||
if (error) {
|
||||
return <Container service={service} error={error} />;
|
||||
}
|
||||
|
||||
return <Container service={service}>
|
||||
{data?.map(d => <Block label={d.label} value={d.value} key={d.label} />)}
|
||||
</Container>;
|
||||
|
||||
return (
|
||||
<Container service={service}>
|
||||
{data?.map((d) => (
|
||||
<Block label={d.label} value={d.value} key={d.label} />
|
||||
))}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,21 +7,27 @@ const logger = createLogger("homeassistantProxyHandler");
|
||||
const defaultQueries = [
|
||||
{
|
||||
template: "{{ states.person|selectattr('state','equalto','home')|list|length }} / {{ states.person|list|length }}",
|
||||
label: "homeassistant.people_home"
|
||||
label: "homeassistant.people_home",
|
||||
},
|
||||
{
|
||||
template: "{{ states.light|selectattr('state','equalto','on')|list|length }} / {{ states.light|list|length }}",
|
||||
label: "homeassistant.lights_on"
|
||||
label: "homeassistant.lights_on",
|
||||
},
|
||||
{
|
||||
template: "{{ states.switch|selectattr('state','equalto','on')|list|length }} / {{ states.switch|list|length }}",
|
||||
label: "homeassistant.switches_on"
|
||||
}
|
||||
label: "homeassistant.switches_on",
|
||||
},
|
||||
];
|
||||
|
||||
function formatOutput(output, data) {
|
||||
return output.replace(/\{.*?\}/g,
|
||||
(match) => match.replace(/\{|\}/g, "").split(".").reduce((o, p) => o ? o[p] : "", data) ?? "");
|
||||
return output.replace(
|
||||
/\{.*?\}/g,
|
||||
(match) =>
|
||||
match
|
||||
.replace(/\{|\}/g, "")
|
||||
.split(".")
|
||||
.reduce((o, p) => (o ? o[p] : ""), data) ?? "",
|
||||
);
|
||||
}
|
||||
|
||||
async function getQuery(query, { url, key }) {
|
||||
@@ -31,15 +37,15 @@ async function getQuery(query, { url, key }) {
|
||||
return {
|
||||
result: await httpProxy(new URL(`${url}/api/states/${state}`), {
|
||||
headers,
|
||||
method: "GET"
|
||||
method: "GET",
|
||||
}),
|
||||
output: (data) => {
|
||||
const jsonData = JSON.parse(data);
|
||||
return {
|
||||
label: formatOutput(label ?? "{attributes.friendly_name}", jsonData),
|
||||
value: formatOutput(value ?? "{state} {attributes.unit_of_measurement}", jsonData)
|
||||
value: formatOutput(value ?? "{state} {attributes.unit_of_measurement}", jsonData),
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
if (template) {
|
||||
@@ -47,9 +53,9 @@ async function getQuery(query, { url, key }) {
|
||||
result: await httpProxy(new URL(`${url}/api/template`), {
|
||||
headers,
|
||||
method: "POST",
|
||||
body: JSON.stringify({ template })
|
||||
body: JSON.stringify({ template }),
|
||||
}),
|
||||
output: (data) => ({ label, value: data.toString() })
|
||||
output: (data) => ({ label, value: data.toString() }),
|
||||
};
|
||||
}
|
||||
return { result: [500, null, { error: { message: `invalid query ${JSON.stringify(query)}` } }] };
|
||||
@@ -68,12 +74,12 @@ export default async function homeassistantProxyHandler(req, res) {
|
||||
logger.debug("Invalid or missing widget for service '%s' in group '%s'", service, group);
|
||||
return res.status(400).json({ error: "Invalid proxy service type" });
|
||||
}
|
||||
|
||||
|
||||
let queries = defaultQueries;
|
||||
if (!widget.fields && widget.custom) {
|
||||
if (typeof widget.custom === 'string') {
|
||||
if (typeof widget.custom === "string") {
|
||||
try {
|
||||
widget.custom = JSON.parse(widget.custom)
|
||||
widget.custom = JSON.parse(widget.custom);
|
||||
} catch (error) {
|
||||
logger.debug("Error parsing HASS widget custom label: %s", JSON.stringify(error));
|
||||
return res.status(400).json({ error: "Error parsing widget custom label" });
|
||||
@@ -82,16 +88,18 @@ export default async function homeassistantProxyHandler(req, res) {
|
||||
queries = widget.custom.slice(0, 4);
|
||||
}
|
||||
|
||||
const results = await Promise.all(queries.map(q => getQuery(q, widget)));
|
||||
const results = await Promise.all(queries.map((q) => getQuery(q, widget)));
|
||||
|
||||
const err = results.find(r => r.result[2]?.error);
|
||||
const err = results.find((r) => r.result[2]?.error);
|
||||
if (err) {
|
||||
const [status, , data] = err.result;
|
||||
return res.status(status).send(data);
|
||||
}
|
||||
|
||||
return res.status(200).send(results.map(r => {
|
||||
const [status, , data] = r.result;
|
||||
return status === 200 ? r.output(data) : { label: status, value: data.toString() };
|
||||
}));
|
||||
return res.status(200).send(
|
||||
results.map((r) => {
|
||||
const [status, , data] = r.result;
|
||||
return status === 200 ? r.output(data) : { label: status, value: data.toString() };
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -27,25 +27,24 @@ export default function Component({ service }) {
|
||||
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block
|
||||
label="widget.status"
|
||||
value={t(`homebridge.${homebridgeData.status}`)}
|
||||
/>
|
||||
<Block label="widget.status" value={t(`homebridge.${homebridgeData.status}`)} />
|
||||
<Block
|
||||
label="homebridge.updates"
|
||||
value={
|
||||
(homebridgeData.updateAvailable || homebridgeData.plugins?.updatesAvailable)
|
||||
homebridgeData.updateAvailable || homebridgeData.plugins?.updatesAvailable
|
||||
? t("homebridge.update_available")
|
||||
: t("homebridge.up_to_date")}
|
||||
: t("homebridge.up_to_date")
|
||||
}
|
||||
/>
|
||||
{homebridgeData?.childBridges?.total > 0 &&
|
||||
{homebridgeData?.childBridges?.total > 0 && (
|
||||
<Block
|
||||
label="homebridge.child_bridges"
|
||||
value={t("homebridge.child_bridges_status", {
|
||||
total: homebridgeData.childBridges.total,
|
||||
ok: homebridgeData.childBridges.running
|
||||
ok: homebridgeData.childBridges.running,
|
||||
})}
|
||||
/>}
|
||||
/>
|
||||
)}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ const logger = createLogger(proxyName);
|
||||
|
||||
async function login(widget, service) {
|
||||
const endpoint = "auth/login";
|
||||
const api = widgets?.[widget.type]?.api
|
||||
const api = widgets?.[widget.type]?.api;
|
||||
const loginUrl = new URL(formatApiCall(api, { endpoint, ...widget }));
|
||||
const loginBody = { username: widget.username, password: widget.password };
|
||||
const headers = { "Content-Type": "application/json" };
|
||||
@@ -25,8 +25,8 @@ async function login(widget, service) {
|
||||
|
||||
try {
|
||||
const { access_token: accessToken, expires_in: expiresIn } = JSON.parse(data.toString());
|
||||
|
||||
cache.put(`${sessionTokenCacheKey}.${service}`, 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);
|
||||
@@ -39,8 +39,8 @@ async function apiCall(widget, endpoint, service) {
|
||||
const key = `${sessionTokenCacheKey}.${service}`;
|
||||
const headers = {
|
||||
"content-type": "application/json",
|
||||
"Authorization": `Bearer ${cache.get(key)}`,
|
||||
}
|
||||
Authorization: `Bearer ${cache.get(key)}`,
|
||||
};
|
||||
|
||||
const url = new URL(formatApiCall(widgets[widget.type].api, { endpoint, ...widget }));
|
||||
const method = "GET";
|
||||
@@ -95,14 +95,14 @@ export default async function homebridgeProxyHandler(req, res) {
|
||||
const { data: pluginsData } = await apiCall(widget, "plugins", service);
|
||||
|
||||
return res.status(200).send({
|
||||
status: statusData?.status,
|
||||
updateAvailable: versionData?.updateAvailable,
|
||||
plugins: {
|
||||
updatesAvailable: pluginsData?.filter(p => p.updateAvailable).length,
|
||||
},
|
||||
childBridges: {
|
||||
running: childBridgeData?.filter(cb => cb.status === "ok").length,
|
||||
total: childBridgeData?.length
|
||||
}
|
||||
status: statusData?.status,
|
||||
updateAvailable: versionData?.updateAvailable,
|
||||
plugins: {
|
||||
updatesAvailable: pluginsData?.filter((p) => p.updateAvailable).length,
|
||||
},
|
||||
childBridges: {
|
||||
running: childBridgeData?.filter((cb) => cb.status === "ok").length,
|
||||
total: childBridgeData?.length,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ const widget = {
|
||||
mappings: {
|
||||
info: {
|
||||
endpoint: "/",
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -30,17 +30,19 @@ export default function Component({ service }) {
|
||||
<Block label="immich.users" value={immichData.usageByUser.length} />
|
||||
<Block label="immich.photos" value={immichData.photos} />
|
||||
<Block label="immich.videos" value={immichData.videos} />
|
||||
<Block label="immich.storage"
|
||||
<Block
|
||||
label="immich.storage"
|
||||
value={
|
||||
// backwards-compatible e.g. '9 GiB'
|
||||
immichData.usage.toString().toLowerCase().includes('b') ?
|
||||
immichData.usage :
|
||||
t("common.bytes", {
|
||||
value: immichData.usage,
|
||||
maximumFractionDigits: 1,
|
||||
binary: true // match immich
|
||||
})
|
||||
} />
|
||||
immichData.usage.toString().toLowerCase().includes("b")
|
||||
? immichData.usage
|
||||
: t("common.bytes", {
|
||||
value: immichData.usage,
|
||||
maximumFractionDigits: 1,
|
||||
binary: true, // match immich
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,35 +5,38 @@ import Container from "components/services/widget/container";
|
||||
import useWidgetAPI from "utils/proxy/use-widget-api";
|
||||
|
||||
export default function Component({ service }) {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { widget } = service;
|
||||
const { widget } = service;
|
||||
|
||||
const { data: jdownloaderData, error: jdownloaderAPIError } = useWidgetAPI(widget, "unified", {
|
||||
refreshInterval: 30000,
|
||||
});
|
||||
const { data: jdownloaderData, error: jdownloaderAPIError } = useWidgetAPI(widget, "unified", {
|
||||
refreshInterval: 30000,
|
||||
});
|
||||
|
||||
if (jdownloaderAPIError) {
|
||||
return <Container service={service} error={jdownloaderAPIError} />;
|
||||
}
|
||||
|
||||
if (!jdownloaderData) {
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="jdownloader.downloadCount" />
|
||||
<Block label="jdownloader.downloadTotalBytes" />
|
||||
<Block label="jdownloader.downloadBytesRemaining" />
|
||||
<Block label="jdownloader.downloadSpeed" />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
if (jdownloaderAPIError) {
|
||||
return <Container service={service} error={jdownloaderAPIError} />;
|
||||
}
|
||||
|
||||
if (!jdownloaderData) {
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="jdownloader.downloadCount" value={t("common.number", { value: jdownloaderData.downloadCount })} />
|
||||
<Block label="jdownloader.downloadTotalBytes" value={t("common.bytes", { value: jdownloaderData.totalBytes })} />
|
||||
<Block label="jdownloader.downloadBytesRemaining" value={t("common.bytes", { value: jdownloaderData.bytesRemaining })} />
|
||||
<Block label="jdownloader.downloadSpeed" value={t("common.byterate", { value: jdownloaderData.totalSpeed })} />
|
||||
</Container>
|
||||
<Container service={service}>
|
||||
<Block label="jdownloader.downloadCount" />
|
||||
<Block label="jdownloader.downloadTotalBytes" />
|
||||
<Block label="jdownloader.downloadBytesRemaining" />
|
||||
<Block label="jdownloader.downloadSpeed" />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="jdownloader.downloadCount" value={t("common.number", { value: jdownloaderData.downloadCount })} />
|
||||
<Block label="jdownloader.downloadTotalBytes" value={t("common.bytes", { value: jdownloaderData.totalBytes })} />
|
||||
<Block
|
||||
label="jdownloader.downloadBytesRemaining"
|
||||
value={t("common.bytes", { value: jdownloaderData.bytesRemaining })}
|
||||
/>
|
||||
<Block label="jdownloader.downloadSpeed" value={t("common.byterate", { value: jdownloaderData.totalSpeed })} />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
import crypto from 'crypto';
|
||||
import querystring from 'querystring';
|
||||
import crypto from "crypto";
|
||||
import querystring from "querystring";
|
||||
|
||||
import { sha256, uniqueRid, validateRid, createEncryptionToken, decrypt, encrypt } from "./tools"
|
||||
import { sha256, uniqueRid, validateRid, createEncryptionToken, decrypt, encrypt } from "./tools";
|
||||
|
||||
import getServiceWidget from "utils/config/service-helpers";
|
||||
import { httpProxy } from "utils/proxy/http";
|
||||
@@ -12,183 +12,173 @@ const proxyName = "jdownloaderProxyHandler";
|
||||
const logger = createLogger(proxyName);
|
||||
|
||||
async function getWidget(req) {
|
||||
const { group, service } = req.query;
|
||||
if (!group || !service) {
|
||||
logger.debug("Invalid or missing service '%s' or group '%s'", service, group);
|
||||
return null;
|
||||
}
|
||||
const widget = await getServiceWidget(group, service);
|
||||
if (!widget) {
|
||||
logger.debug("Invalid or missing widget for service '%s' in group '%s'", service, group);
|
||||
return null;
|
||||
}
|
||||
const { group, service } = req.query;
|
||||
if (!group || !service) {
|
||||
logger.debug("Invalid or missing service '%s' or group '%s'", service, group);
|
||||
return null;
|
||||
}
|
||||
const widget = await getServiceWidget(group, service);
|
||||
if (!widget) {
|
||||
logger.debug("Invalid or missing widget for service '%s' in group '%s'", service, group);
|
||||
return null;
|
||||
}
|
||||
|
||||
return widget;
|
||||
return widget;
|
||||
}
|
||||
|
||||
async function login(loginSecret, deviceSecret, params) {
|
||||
const rid = uniqueRid();
|
||||
const path = `/my/connect?${querystring.stringify({ ...params, rid })}`;
|
||||
const rid = uniqueRid();
|
||||
const path = `/my/connect?${querystring.stringify({ ...params, rid })}`;
|
||||
|
||||
const signature = crypto
|
||||
.createHmac('sha256', loginSecret)
|
||||
.update(path)
|
||||
.digest('hex');
|
||||
const url = `${new URL(`https://api.jdownloader.org${path}&signature=${signature}`)}`
|
||||
const signature = crypto.createHmac("sha256", loginSecret).update(path).digest("hex");
|
||||
const url = `${new URL(`https://api.jdownloader.org${path}&signature=${signature}`)}`;
|
||||
|
||||
const [status, contentType, data] = await httpProxy(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
const [status, contentType, data] = await httpProxy(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (status !== 200) {
|
||||
logger.error("HTTP %d communicating with jdownloader. Data: %s", status, data.toString());
|
||||
return [status, data];
|
||||
}
|
||||
if (status !== 200) {
|
||||
logger.error("HTTP %d communicating with jdownloader. Data: %s", status, data.toString());
|
||||
return [status, data];
|
||||
}
|
||||
|
||||
try {
|
||||
const decryptedData = JSON.parse(decrypt(data.toString(), loginSecret))
|
||||
const sessionToken = decryptedData.sessiontoken;
|
||||
validateRid(decryptedData, rid);
|
||||
const serverEncryptionToken = createEncryptionToken(loginSecret, sessionToken);
|
||||
const deviceEncryptionToken = createEncryptionToken(deviceSecret, sessionToken);
|
||||
return [status, decryptedData, contentType, serverEncryptionToken, deviceEncryptionToken, sessionToken];
|
||||
} catch (e) {
|
||||
logger.error("Error decoding jdownloader API data. Data: %s", data.toString());
|
||||
return [status, null];
|
||||
}
|
||||
try {
|
||||
const decryptedData = JSON.parse(decrypt(data.toString(), loginSecret));
|
||||
const sessionToken = decryptedData.sessiontoken;
|
||||
validateRid(decryptedData, rid);
|
||||
const serverEncryptionToken = createEncryptionToken(loginSecret, sessionToken);
|
||||
const deviceEncryptionToken = createEncryptionToken(deviceSecret, sessionToken);
|
||||
return [status, decryptedData, contentType, serverEncryptionToken, deviceEncryptionToken, sessionToken];
|
||||
} catch (e) {
|
||||
logger.error("Error decoding jdownloader API data. Data: %s", data.toString());
|
||||
return [status, null];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function getDevice(serverEncryptionToken, deviceName, params) {
|
||||
const rid = uniqueRid();
|
||||
const path = `/my/listdevices?${querystring.stringify({ ...params, rid })}`;
|
||||
const signature = crypto
|
||||
.createHmac('sha256', serverEncryptionToken)
|
||||
.update(path)
|
||||
.digest('hex');
|
||||
const url = `${new URL(`https://api.jdownloader.org${path}&signature=${signature}`)}`
|
||||
const rid = uniqueRid();
|
||||
const path = `/my/listdevices?${querystring.stringify({ ...params, rid })}`;
|
||||
const signature = crypto.createHmac("sha256", serverEncryptionToken).update(path).digest("hex");
|
||||
const url = `${new URL(`https://api.jdownloader.org${path}&signature=${signature}`)}`;
|
||||
|
||||
const [status, , data] = await httpProxy(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
const [status, , data] = await httpProxy(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (status !== 200) {
|
||||
logger.error("HTTP %d communicating with jdownloader. Data: %s", status, data.toString());
|
||||
return [status, data];
|
||||
}
|
||||
|
||||
try {
|
||||
const decryptedData = JSON.parse(decrypt(data.toString(), serverEncryptionToken))
|
||||
const filteredDevice = decryptedData.list.filter(device => device.name === deviceName);
|
||||
return [status, filteredDevice[0].id];
|
||||
} catch (e) {
|
||||
logger.error("Error decoding jdownloader API data. Data: %s", data.toString());
|
||||
return [status, null];
|
||||
}
|
||||
if (status !== 200) {
|
||||
logger.error("HTTP %d communicating with jdownloader. Data: %s", status, data.toString());
|
||||
return [status, data];
|
||||
}
|
||||
|
||||
try {
|
||||
const decryptedData = JSON.parse(decrypt(data.toString(), serverEncryptionToken));
|
||||
const filteredDevice = decryptedData.list.filter((device) => device.name === deviceName);
|
||||
return [status, filteredDevice[0].id];
|
||||
} catch (e) {
|
||||
logger.error("Error decoding jdownloader API data. Data: %s", data.toString());
|
||||
return [status, null];
|
||||
}
|
||||
}
|
||||
|
||||
function createBody(rid, query, params) {
|
||||
const baseBody = {
|
||||
apiVer: 1,
|
||||
rid,
|
||||
url: query
|
||||
};
|
||||
return params ? { ...baseBody, params: [JSON.stringify(params)] } : baseBody;
|
||||
const baseBody = {
|
||||
apiVer: 1,
|
||||
rid,
|
||||
url: query,
|
||||
};
|
||||
return params ? { ...baseBody, params: [JSON.stringify(params)] } : baseBody;
|
||||
}
|
||||
|
||||
async function queryPackages(deviceEncryptionToken, deviceId, sessionToken, params) {
|
||||
const rid = uniqueRid();
|
||||
const body = encrypt(JSON.stringify(createBody(rid, '/downloadsV2/queryPackages', params)), deviceEncryptionToken);
|
||||
const url = `${new URL(`https://api.jdownloader.org/t_${encodeURI(sessionToken)}_${encodeURI(deviceId)}/downloadsV2/queryPackages`)}`
|
||||
const [status, , data] = await httpProxy(url, {
|
||||
method: 'POST',
|
||||
body,
|
||||
});
|
||||
const rid = uniqueRid();
|
||||
const body = encrypt(JSON.stringify(createBody(rid, "/downloadsV2/queryPackages", params)), deviceEncryptionToken);
|
||||
const url = `${new URL(
|
||||
`https://api.jdownloader.org/t_${encodeURI(sessionToken)}_${encodeURI(deviceId)}/downloadsV2/queryPackages`,
|
||||
)}`;
|
||||
const [status, , data] = await httpProxy(url, {
|
||||
method: "POST",
|
||||
body,
|
||||
});
|
||||
|
||||
if (status !== 200) {
|
||||
logger.error("HTTP %d communicating with jdownloader. Data: %s", status, data.toString());
|
||||
return [status, data];
|
||||
}
|
||||
|
||||
try {
|
||||
const decryptedData = JSON.parse(decrypt(data.toString(), deviceEncryptionToken))
|
||||
return decryptedData.data;
|
||||
} catch (e) {
|
||||
logger.error("Error decoding JDRss jdownloader data. Data: %s", data.toString());
|
||||
return [status, null];
|
||||
}
|
||||
if (status !== 200) {
|
||||
logger.error("HTTP %d communicating with jdownloader. Data: %s", status, data.toString());
|
||||
return [status, data];
|
||||
}
|
||||
|
||||
try {
|
||||
const decryptedData = JSON.parse(decrypt(data.toString(), deviceEncryptionToken));
|
||||
return decryptedData.data;
|
||||
} catch (e) {
|
||||
logger.error("Error decoding JDRss jdownloader data. Data: %s", data.toString());
|
||||
return [status, null];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default async function jdownloaderProxyHandler(req, res) {
|
||||
const widget = await getWidget(req);
|
||||
const widget = await getWidget(req);
|
||||
|
||||
if (!widget) {
|
||||
return res.status(400).json({ error: "Invalid proxy service type" });
|
||||
if (!widget) {
|
||||
return res.status(400).json({ error: "Invalid proxy service type" });
|
||||
}
|
||||
logger.debug("Getting data from JDRss API");
|
||||
const { username } = widget;
|
||||
const { password } = widget;
|
||||
|
||||
const appKey = "homepage";
|
||||
const loginSecret = sha256(`${username}${password}server`);
|
||||
const deviceSecret = sha256(`${username}${password}device`);
|
||||
const email = username;
|
||||
|
||||
const loginData = await login(loginSecret, deviceSecret, {
|
||||
appKey,
|
||||
email,
|
||||
});
|
||||
|
||||
const deviceData = await getDevice(loginData[3], widget.client, {
|
||||
sessiontoken: loginData[5],
|
||||
});
|
||||
|
||||
const packageStatus = await queryPackages(loginData[4], deviceData[1], loginData[5], {
|
||||
bytesLoaded: true,
|
||||
bytesTotal: true,
|
||||
comment: false,
|
||||
enabled: true,
|
||||
eta: false,
|
||||
priority: false,
|
||||
finished: true,
|
||||
running: true,
|
||||
speed: true,
|
||||
status: true,
|
||||
childCount: false,
|
||||
hosts: false,
|
||||
saveTo: false,
|
||||
maxResults: -1,
|
||||
startAt: 0,
|
||||
});
|
||||
|
||||
let totalLoaded = 0;
|
||||
let totalBytes = 0;
|
||||
let totalSpeed = 0;
|
||||
packageStatus.forEach((file) => {
|
||||
totalBytes += file.bytesTotal;
|
||||
totalLoaded += file.bytesLoaded;
|
||||
if (file.finished !== true && file.speed) {
|
||||
totalSpeed += file.speed;
|
||||
}
|
||||
logger.debug("Getting data from JDRss API");
|
||||
const { username } = widget
|
||||
const { password } = widget
|
||||
});
|
||||
|
||||
const appKey = "homepage"
|
||||
const loginSecret = sha256(`${username}${password}server`)
|
||||
const deviceSecret = sha256(`${username}${password}device`)
|
||||
const email = username;
|
||||
|
||||
const loginData = await login(loginSecret, deviceSecret, {
|
||||
appKey,
|
||||
email
|
||||
})
|
||||
|
||||
const deviceData = await getDevice(loginData[3], widget.client, {
|
||||
sessiontoken: loginData[5]
|
||||
})
|
||||
|
||||
const packageStatus = await queryPackages(loginData[4], deviceData[1], loginData[5], {
|
||||
"bytesLoaded": true,
|
||||
"bytesTotal": true,
|
||||
"comment": false,
|
||||
"enabled": true,
|
||||
"eta": false,
|
||||
"priority": false,
|
||||
"finished": true,
|
||||
"running": true,
|
||||
"speed": true,
|
||||
"status": true,
|
||||
"childCount": false,
|
||||
"hosts": false,
|
||||
"saveTo": false,
|
||||
"maxResults": -1,
|
||||
"startAt": 0,
|
||||
}
|
||||
)
|
||||
|
||||
let totalLoaded = 0;
|
||||
let totalBytes = 0;
|
||||
let totalSpeed = 0;
|
||||
packageStatus.forEach(file => {
|
||||
totalBytes += file.bytesTotal;
|
||||
totalLoaded += file.bytesLoaded;
|
||||
if (file.finished !== true && file.speed) {
|
||||
totalSpeed += file.speed;
|
||||
}
|
||||
});
|
||||
|
||||
const data = {
|
||||
downloadCount: packageStatus.length,
|
||||
bytesRemaining: totalBytes - totalLoaded,
|
||||
totalBytes,
|
||||
totalSpeed
|
||||
};
|
||||
|
||||
return res.send(data);
|
||||
const data = {
|
||||
downloadCount: packageStatus.length,
|
||||
bytesRemaining: totalBytes - totalLoaded,
|
||||
totalBytes,
|
||||
totalSpeed,
|
||||
};
|
||||
|
||||
return res.send(data);
|
||||
}
|
||||
|
||||
@@ -1,55 +1,48 @@
|
||||
import crypto from 'crypto';
|
||||
import crypto from "crypto";
|
||||
|
||||
export function sha256(data) {
|
||||
return crypto
|
||||
.createHash('sha256')
|
||||
.update(data)
|
||||
.digest();
|
||||
return crypto.createHash("sha256").update(data).digest();
|
||||
}
|
||||
|
||||
export function uniqueRid() {
|
||||
return Math.floor(Math.random() * 10e12);
|
||||
return Math.floor(Math.random() * 10e12);
|
||||
}
|
||||
|
||||
export function validateRid(decryptedData, rid) {
|
||||
if (decryptedData.rid !== rid) {
|
||||
throw new Error('RequestID mismatch');
|
||||
}
|
||||
return decryptedData;
|
||||
|
||||
if (decryptedData.rid !== rid) {
|
||||
throw new Error("RequestID mismatch");
|
||||
}
|
||||
return decryptedData;
|
||||
}
|
||||
|
||||
export function decrypt(data, ivKey) {
|
||||
const iv = ivKey.slice(0, ivKey.length / 2);
|
||||
const key = ivKey.slice(ivKey.length / 2, ivKey.length);
|
||||
const cipher = crypto.createDecipheriv('aes-128-cbc', key, iv);
|
||||
return Buffer.concat([
|
||||
cipher.update(data, 'base64'),
|
||||
cipher.final()
|
||||
]).toString();
|
||||
const iv = ivKey.slice(0, ivKey.length / 2);
|
||||
const key = ivKey.slice(ivKey.length / 2, ivKey.length);
|
||||
const cipher = crypto.createDecipheriv("aes-128-cbc", key, iv);
|
||||
return Buffer.concat([cipher.update(data, "base64"), cipher.final()]).toString();
|
||||
}
|
||||
|
||||
export function createEncryptionToken(oldTokenBuff, updateToken) {
|
||||
const updateTokenBuff = Buffer.from(updateToken, 'hex');
|
||||
const mergedBuffer = Buffer.concat([oldTokenBuff, updateTokenBuff], oldTokenBuff.length + updateTokenBuff.length);
|
||||
return sha256(mergedBuffer);
|
||||
const updateTokenBuff = Buffer.from(updateToken, "hex");
|
||||
const mergedBuffer = Buffer.concat([oldTokenBuff, updateTokenBuff], oldTokenBuff.length + updateTokenBuff.length);
|
||||
return sha256(mergedBuffer);
|
||||
}
|
||||
|
||||
export function encrypt(data, ivKey) {
|
||||
if (typeof data !== 'string') {
|
||||
throw new Error('data no es un string');
|
||||
}
|
||||
if (!(ivKey instanceof Buffer)) {
|
||||
throw new Error('ivKey no es un buffer');
|
||||
}
|
||||
if (ivKey.length !== 32) {
|
||||
throw new Error('ivKey tiene que tener tamaño 32');
|
||||
}
|
||||
const stringIVKey = ivKey.toString('hex');
|
||||
const stringIV = stringIVKey.substring(0, stringIVKey.length / 2);
|
||||
const stringKey = stringIVKey.substring(stringIVKey.length / 2, stringIVKey.length);
|
||||
const iv = Buffer.from(stringIV, 'hex');
|
||||
const key = Buffer.from(stringKey, 'hex');
|
||||
const cipher = crypto.createCipheriv('aes-128-cbc', key, iv);
|
||||
return cipher.update(data, 'utf8', 'base64') + cipher.final('base64');
|
||||
}
|
||||
if (typeof data !== "string") {
|
||||
throw new Error("data no es un string");
|
||||
}
|
||||
if (!(ivKey instanceof Buffer)) {
|
||||
throw new Error("ivKey no es un buffer");
|
||||
}
|
||||
if (ivKey.length !== 32) {
|
||||
throw new Error("ivKey tiene que tener tamaño 32");
|
||||
}
|
||||
const stringIVKey = ivKey.toString("hex");
|
||||
const stringIV = stringIVKey.substring(0, stringIVKey.length / 2);
|
||||
const stringKey = stringIVKey.substring(stringIVKey.length / 2, stringIVKey.length);
|
||||
const iv = Buffer.from(stringIV, "hex");
|
||||
const key = Buffer.from(stringKey, "hex");
|
||||
const cipher = crypto.createCipheriv("aes-128-cbc", key, iv);
|
||||
return cipher.update(data, "utf8", "base64") + cipher.final("base64");
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import jdownloaderProxyHandler from "./proxy";
|
||||
|
||||
const widget = {
|
||||
api: "https://api.jdownloader.org/{endpoint}/&signature={signature}",
|
||||
proxyHandler: jdownloaderProxyHandler,
|
||||
api: "https://api.jdownloader.org/{endpoint}/&signature={signature}",
|
||||
proxyHandler: jdownloaderProxyHandler,
|
||||
|
||||
mappings: {
|
||||
unified: {
|
||||
endpoint: "/",
|
||||
signature: "",
|
||||
},
|
||||
mappings: {
|
||||
unified: {
|
||||
endpoint: "/",
|
||||
signature: "",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default widget;
|
||||
export default widget;
|
||||
|
||||
@@ -7,11 +7,7 @@ const widget = {
|
||||
mappings: {
|
||||
"request/count": {
|
||||
endpoint: "request/count",
|
||||
validate: [
|
||||
"pending",
|
||||
"approved",
|
||||
"available"
|
||||
]
|
||||
validate: ["pending", "approved", "available"],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -5,29 +5,29 @@ import Block from "components/services/widget/block";
|
||||
import useWidgetAPI from "utils/proxy/use-widget-api";
|
||||
|
||||
export default function Component({ service }) {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { widget } = service;
|
||||
const { widget } = service;
|
||||
|
||||
const { data: kavitaData, error: kavitaError } = useWidgetAPI(widget, "info");
|
||||
const { data: kavitaData, error: kavitaError } = useWidgetAPI(widget, "info");
|
||||
|
||||
if (kavitaError) {
|
||||
return <Container service={service} error={kavitaError} />;
|
||||
}
|
||||
|
||||
if (!kavitaData) {
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="kavita.seriesCount" />
|
||||
<Block label="kavita.totalFiles" />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
if (kavitaError) {
|
||||
return <Container service={service} error={kavitaError} />;
|
||||
}
|
||||
|
||||
if (!kavitaData) {
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="kavita.seriesCount" value={t("common.number", { value: kavitaData.seriesCount })} />
|
||||
<Block label="kavita.totalFiles" value={t("common.number", { value: kavitaData.totalFiles })} />
|
||||
</Container>
|
||||
<Container service={service}>
|
||||
<Block label="kavita.seriesCount" />
|
||||
<Block label="kavita.totalFiles" />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="kavita.seriesCount" value={t("common.number", { value: kavitaData.seriesCount })} />
|
||||
<Block label="kavita.totalFiles" value={t("common.number", { value: kavitaData.totalFiles })} />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,12 +12,12 @@ const logger = createLogger(proxyName);
|
||||
|
||||
async function login(widget, service) {
|
||||
const endpoint = "Account/login";
|
||||
const api = widgets?.[widget.type]?.api
|
||||
const api = widgets?.[widget.type]?.api;
|
||||
const loginUrl = new URL(formatApiCall(api, { endpoint, ...widget }));
|
||||
const loginBody = { username: widget.username, password: widget.password };
|
||||
const headers = { "Content-Type": "application/json", "accept": "text/plain" };
|
||||
const headers = { "Content-Type": "application/json", accept: "text/plain" };
|
||||
|
||||
const [, , data,] = await httpProxy(loginUrl, {
|
||||
const [, , data] = await httpProxy(loginUrl, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(loginBody),
|
||||
headers,
|
||||
@@ -38,8 +38,8 @@ async function apiCall(widget, endpoint, service) {
|
||||
const key = `${sessionTokenCacheKey}.${service}`;
|
||||
const headers = {
|
||||
"content-type": "application/json",
|
||||
"Authorization": `Bearer ${cache.get(key)}`,
|
||||
}
|
||||
Authorization: `Bearer ${cache.get(key)}`,
|
||||
};
|
||||
|
||||
const url = new URL(formatApiCall(widgets[widget.type].api, { endpoint, ...widget }));
|
||||
const method = "GET";
|
||||
@@ -91,6 +91,6 @@ export default async function KavitaProxyHandler(req, res) {
|
||||
|
||||
return res.status(200).send({
|
||||
seriesCount: statsData?.seriesCount,
|
||||
totalFiles: statsData?.totalFiles
|
||||
totalFiles: statsData?.totalFiles,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import kavitaProxyHandler from "./proxy";
|
||||
|
||||
const widget = {
|
||||
api: "{url}/api/{endpoint}",
|
||||
api: "{url}/api/{endpoint}",
|
||||
proxyHandler: kavitaProxyHandler,
|
||||
mappings: {
|
||||
info: {
|
||||
endpoint: "/"
|
||||
}
|
||||
}
|
||||
endpoint: "/",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default widget;
|
||||
|
||||
@@ -14,17 +14,13 @@ const widget = {
|
||||
},
|
||||
series: {
|
||||
endpoint: "series",
|
||||
validate: [
|
||||
"totalElements"
|
||||
]
|
||||
validate: ["totalElements"],
|
||||
},
|
||||
books: {
|
||||
endpoint: "books",
|
||||
validate: [
|
||||
"totalElements"
|
||||
]
|
||||
validate: ["totalElements"],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default widget;
|
||||
export default widget;
|
||||
|
||||
@@ -45,8 +45,8 @@ export default function Component({ service }) {
|
||||
const snapshotPath = service.widget?.snapshotPath;
|
||||
|
||||
const source = statusData?.sources
|
||||
.filter(el => snapshotHost ? el.source.host === snapshotHost : true)
|
||||
.filter(el => snapshotPath ? el.source.path === snapshotPath : true)[0];
|
||||
.filter((el) => (snapshotHost ? el.source.host === snapshotHost : true))
|
||||
.filter((el) => (snapshotPath ? el.source.path === snapshotPath : true))[0];
|
||||
|
||||
if (!statusData || !source) {
|
||||
return (
|
||||
@@ -59,15 +59,19 @@ export default function Component({ service }) {
|
||||
);
|
||||
}
|
||||
|
||||
const lastRun = source.lastSnapshot.stats.errorCount === 0 ? new Date(source.lastSnapshot.startTime) : t("kopia.failed");
|
||||
const lastRun =
|
||||
source.lastSnapshot.stats.errorCount === 0 ? new Date(source.lastSnapshot.startTime) : t("kopia.failed");
|
||||
const nextTime = source.nextSnapshotTime ? new Date(source.nextSnapshotTime) : null;
|
||||
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="kopia.status" value={ source.status } />
|
||||
<Block label="kopia.size" value={t("common.bbytes", { value: source.lastSnapshot.stats.totalSize, maximumFractionDigits: 1 })} />
|
||||
<Block label="kopia.lastrun" value={ relativeDate(lastRun) } />
|
||||
{nextTime && <Block label="kopia.nextrun" value={ relativeDate(nextTime) } />}
|
||||
<Block label="kopia.status" value={source.status} />
|
||||
<Block
|
||||
label="kopia.size"
|
||||
value={t("common.bbytes", { value: source.lastSnapshot.stats.totalSize, maximumFractionDigits: 1 })}
|
||||
/>
|
||||
<Block label="kopia.lastrun" value={relativeDate(lastRun)} />
|
||||
{nextTime && <Block label="kopia.nextrun" value={relativeDate(nextTime)} />}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,4 +11,4 @@ const widget = {
|
||||
},
|
||||
};
|
||||
|
||||
export default widget;
|
||||
export default widget;
|
||||
|
||||
@@ -10,10 +10,12 @@ export default function Component({ service }) {
|
||||
const { widget } = service;
|
||||
const podSelectorString = widget.podSelector !== undefined ? `podSelector=${widget.podSelector}` : "";
|
||||
const { data: statusData, error: statusError } = useSWR(
|
||||
`/api/kubernetes/status/${widget.namespace}/${widget.app}?${podSelectorString}`);
|
||||
`/api/kubernetes/status/${widget.namespace}/${widget.app}?${podSelectorString}`,
|
||||
);
|
||||
|
||||
const { data: statsData, error: statsError } = useSWR(
|
||||
`/api/kubernetes/stats/${widget.namespace}/${widget.app}?${podSelectorString}`);
|
||||
`/api/kubernetes/stats/${widget.namespace}/${widget.app}?${podSelectorString}`,
|
||||
);
|
||||
|
||||
if (statsError || statusError) {
|
||||
return <Container service={service} error={statsError ?? statusError} />;
|
||||
@@ -38,10 +40,12 @@ export default function Component({ service }) {
|
||||
|
||||
return (
|
||||
<Container service={service}>
|
||||
{statsData.stats.cpuLimit && (
|
||||
{(statsData.stats.cpuLimit && (
|
||||
<Block label="docker.cpu" value={t("common.percent", { value: statsData.stats.cpuUsage })} />
|
||||
) || (
|
||||
<Block label="docker.cpu" value={t("common.number", { value: statsData.stats.cpu, maximumFractionDigits: 4 })}
|
||||
)) || (
|
||||
<Block
|
||||
label="docker.cpu"
|
||||
value={t("common.number", { value: statsData.stats.cpu, maximumFractionDigits: 4 })}
|
||||
/>
|
||||
)}
|
||||
<Block label="docker.mem" value={t("common.bytes", { value: statsData.stats.mem })} />
|
||||
|
||||
@@ -30,4 +30,4 @@ export default function Component({ service }) {
|
||||
<Block label="mealie.tags" value={mealieData.totalTags} />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,4 +5,4 @@ const widget = {
|
||||
proxyHandler: credentialedProxyHandler,
|
||||
};
|
||||
|
||||
export default widget;
|
||||
export default widget;
|
||||
|
||||
@@ -7,17 +7,13 @@ const widget = {
|
||||
mappings: {
|
||||
stats: {
|
||||
endpoint: "?cmd=shows.stats",
|
||||
validate: [
|
||||
"data"
|
||||
]
|
||||
validate: ["data"],
|
||||
},
|
||||
future: {
|
||||
endpoint: "?cmd=future",
|
||||
validate: [
|
||||
"data"
|
||||
]
|
||||
}
|
||||
}
|
||||
validate: ["data"],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default widget;
|
||||
|
||||
@@ -14,7 +14,7 @@ export default function Component({ service }) {
|
||||
|
||||
if (statsError || leasesError) {
|
||||
const finalError = statsError ?? leasesError;
|
||||
return <Container service={service} error={ finalError } />;
|
||||
return <Container service={service} error={finalError} />;
|
||||
}
|
||||
|
||||
if (!statsData || !leasesData) {
|
||||
@@ -28,14 +28,14 @@ export default function Component({ service }) {
|
||||
);
|
||||
}
|
||||
|
||||
const memoryUsed = 100 - (statsData['free-memory'] / statsData['total-memory'])*100
|
||||
const memoryUsed = 100 - (statsData["free-memory"] / statsData["total-memory"]) * 100;
|
||||
|
||||
const numberOfLeases = leasesData.length
|
||||
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.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>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import genericProxyHandler from "utils/proxy/handlers/generic";
|
||||
|
||||
const widget = {
|
||||
@@ -8,16 +7,11 @@ const widget = {
|
||||
mappings: {
|
||||
system: {
|
||||
endpoint: "system/resource",
|
||||
validate: [
|
||||
"cpu-load",
|
||||
"free-memory",
|
||||
"total-memory",
|
||||
"uptime"
|
||||
]
|
||||
validate: ["cpu-load", "free-memory", "total-memory", "uptime"],
|
||||
},
|
||||
leases: {
|
||||
endpoint: "ip/dhcp-server/lease",
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -8,26 +8,28 @@ export default function Component({ service }) {
|
||||
const { widget } = service;
|
||||
const { data: serverData, error: serverError } = useWidgetAPI(widget, "status");
|
||||
const { t } = useTranslation();
|
||||
|
||||
if(serverError){
|
||||
|
||||
if (serverError) {
|
||||
return <Container service={service} error={serverError} />;
|
||||
}
|
||||
if (!serverData) {
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="minecraft.status"/>
|
||||
<Block label="minecraft.players" />
|
||||
<Block label="minecraft.version" />
|
||||
<Block label="minecraft.status" />
|
||||
<Block label="minecraft.players" />
|
||||
<Block label="minecraft.version" />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
const statusIndicator = serverData.online ?
|
||||
<span className="text-green-500">{t("minecraft.up")}</span>:
|
||||
<span className="text-red-500">{t("minecraft.down")}</span>;
|
||||
|
||||
const statusIndicator = serverData.online ? (
|
||||
<span className="text-green-500">{t("minecraft.up")}</span>
|
||||
) : (
|
||||
<span className="text-red-500">{t("minecraft.down")}</span>
|
||||
);
|
||||
const players = serverData.players ? `${serverData.players.online} / ${serverData.players.max}` : "-";
|
||||
const version = serverData.version || "-";
|
||||
|
||||
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="minecraft.status" value={statusIndicator} />
|
||||
@@ -36,4 +38,3 @@ export default function Component({ service }) {
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,22 +7,22 @@ const proxyName = "minecraftProxyHandler";
|
||||
const logger = createLogger(proxyName);
|
||||
|
||||
export default async function minecraftProxyHandler(req, res) {
|
||||
const { group, service } = req.query;
|
||||
const serviceWidget = await getServiceWidget(group, service);
|
||||
const url = new URL(serviceWidget.url);
|
||||
try {
|
||||
const pingResponse = await pingWithPromise(url.hostname, url.port || 25565);
|
||||
res.status(200).send({
|
||||
version: pingResponse.version.name,
|
||||
online: true,
|
||||
players: pingResponse.players
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error(e);
|
||||
res.status(200).send({
|
||||
version: undefined,
|
||||
online: false,
|
||||
players: undefined
|
||||
});
|
||||
}
|
||||
const { group, service } = req.query;
|
||||
const serviceWidget = await getServiceWidget(group, service);
|
||||
const url = new URL(serviceWidget.url);
|
||||
try {
|
||||
const pingResponse = await pingWithPromise(url.hostname, url.port || 25565);
|
||||
res.status(200).send({
|
||||
version: pingResponse.version.name,
|
||||
online: true,
|
||||
players: pingResponse.players,
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error(e);
|
||||
res.status(200).send({
|
||||
version: undefined,
|
||||
online: false,
|
||||
players: undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import minecraftProxyHandler from "./proxy";
|
||||
|
||||
const widget = {
|
||||
proxyHandler: minecraftProxyHandler
|
||||
}
|
||||
proxyHandler: minecraftProxyHandler,
|
||||
};
|
||||
|
||||
export default widget;
|
||||
|
||||
@@ -4,16 +4,16 @@ 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)
|
||||
unread: Object.values(asJson(data).unreads).reduce((acc, i) => acc + i, 0),
|
||||
}),
|
||||
},
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default widget;
|
||||
|
||||
@@ -30,7 +30,16 @@ export default function Component({ service }) {
|
||||
}
|
||||
`}</style>
|
||||
<div className="absolute top-0 bottom-0 right-0 left-0">
|
||||
<Image layout="fill" objectFit="fill" className="blur-md" src={stream} alt="stream" onError={(e) => {e.target.parentElement.parentElement.className='tv-static'}} />
|
||||
<Image
|
||||
layout="fill"
|
||||
objectFit="fill"
|
||||
className="blur-md"
|
||||
src={stream}
|
||||
alt="stream"
|
||||
onError={(e) => {
|
||||
e.target.parentElement.parentElement.className = "tv-static";
|
||||
}}
|
||||
/>
|
||||
<Image layout="fill" objectFit={fit} className="drop-shadow-2xl" src={stream} alt="stream" />
|
||||
</div>
|
||||
<div className="absolute top-0 right-0 bottom-0 left-0 overflow-clip shadow-[inset_0_0_200px_#000] shadow-theme-700/10 dark:shadow-theme-900/10" />
|
||||
|
||||
@@ -44,7 +44,10 @@ export default function Component({ service }) {
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="moonraker.layers" value={`${currentLayer} / ${totalLayer}`} />
|
||||
<Block label="moonraker.print_progress" value={t("common.percent", { value: (displayStatus.result.status.display_status.progress * 100) })} />
|
||||
<Block
|
||||
label="moonraker.print_progress"
|
||||
value={t("common.percent", { value: displayStatus.result.status.display_status.progress * 100 })}
|
||||
/>
|
||||
<Block label="moonraker.print_status" value={printStats.result.status.print_stats.state} />
|
||||
</Container>
|
||||
);
|
||||
|
||||
@@ -32,7 +32,7 @@ export default function Component({ service }) {
|
||||
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="mylar.series" value={t("common.number", { value: seriesData.data.length })} />
|
||||
<Block label="mylar.series" value={t("common.number", { value: seriesData.data.length })} />
|
||||
<Block label="mylar.issues" value={t("common.number", { value: totalIssues })} />
|
||||
<Block label="mylar.wanted" value={t("common.number", { value: wantedData.issues.length })} />
|
||||
</Container>
|
||||
|
||||
@@ -6,15 +6,15 @@ const widget = {
|
||||
|
||||
mappings: {
|
||||
issues: {
|
||||
endpoint: "getIndex"
|
||||
endpoint: "getIndex",
|
||||
},
|
||||
series: {
|
||||
endpoint: "seriesjsonListing"
|
||||
endpoint: "seriesjsonListing",
|
||||
},
|
||||
wanted: {
|
||||
endpoint: "getWanted"
|
||||
endpoint: "getWanted",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default widget;
|
||||
export default widget;
|
||||
|
||||
@@ -31,17 +31,13 @@ export default function Component({ service }) {
|
||||
}
|
||||
|
||||
if (!navidromeData) {
|
||||
return (
|
||||
<SinglePlayingEntry entry={{ title: t("navidrome.please_wait") }} />
|
||||
);
|
||||
return <SinglePlayingEntry entry={{ title: t("navidrome.please_wait") }} />;
|
||||
}
|
||||
|
||||
const { nowPlaying } = navidromeData["subsonic-response"];
|
||||
if (!nowPlaying.entry) {
|
||||
// nothing playing
|
||||
return (
|
||||
<SinglePlayingEntry entry={{ title: t("navidrome.nothing_streaming") }} />
|
||||
);
|
||||
return <SinglePlayingEntry entry={{ title: t("navidrome.nothing_streaming") }} />;
|
||||
}
|
||||
|
||||
const nowPlayingEntries = Object.values(nowPlaying.entry);
|
||||
|
||||
@@ -5,7 +5,7 @@ const widget = {
|
||||
proxyHandler: genericProxyHandler,
|
||||
|
||||
mappings: {
|
||||
"getNowPlaying": {
|
||||
getNowPlaying: {
|
||||
endpoint: "getNowPlaying",
|
||||
},
|
||||
},
|
||||
|
||||
@@ -22,12 +22,12 @@ export default function Component({ service }) {
|
||||
// If all fields are enabled, drop cpuload and memoryusage
|
||||
if (widget.fields.length === 6) return [false, false];
|
||||
|
||||
const hasCpuLoad = widget.fields?.includes('cpuload');
|
||||
const hasMemoryUsage = widget.fields?.includes('memoryusage');
|
||||
|
||||
const hasCpuLoad = widget.fields?.includes("cpuload");
|
||||
const hasMemoryUsage = widget.fields?.includes("memoryusage");
|
||||
|
||||
// If (for some reason) 5 fields are set, drop memoryusage
|
||||
if (hasCpuLoad && hasMemoryUsage) return [true, false];
|
||||
return [!hasCpuLoad, !hasMemoryUsage]
|
||||
return [!hasCpuLoad, !hasMemoryUsage];
|
||||
}, [widget.fields]);
|
||||
|
||||
if (nextcloudError) {
|
||||
@@ -48,13 +48,21 @@ export default function Component({ service }) {
|
||||
}
|
||||
|
||||
const { nextcloud: nextcloudInfo, activeUsers } = nextcloudData.ocs.data;
|
||||
const memoryUsage = 100 * ((parseFloat(nextcloudInfo.system.mem_total) - parseFloat(nextcloudInfo.system.mem_free)) / parseFloat(nextcloudInfo.system.mem_total));
|
||||
const memoryUsage =
|
||||
100 *
|
||||
((parseFloat(nextcloudInfo.system.mem_total) - parseFloat(nextcloudInfo.system.mem_free)) /
|
||||
parseFloat(nextcloudInfo.system.mem_total));
|
||||
|
||||
return (
|
||||
<Container service={service}>
|
||||
{showCpuLoad && <Block label="nextcloud.cpuload" value={t("common.percent", { value: nextcloudInfo.system.cpuload[0] })} />}
|
||||
{showMemoryUsage && <Block label="nextcloud.memoryusage" value={t("common.percent", { value:memoryUsage })} />}
|
||||
<Block label="nextcloud.freespace" value={t("common.bbytes", { value: nextcloudInfo.system.freespace, maximumFractionDigits: 1 })} />
|
||||
{showCpuLoad && (
|
||||
<Block label="nextcloud.cpuload" value={t("common.percent", { value: nextcloudInfo.system.cpuload[0] })} />
|
||||
)}
|
||||
{showMemoryUsage && <Block label="nextcloud.memoryusage" value={t("common.percent", { value: memoryUsage })} />}
|
||||
<Block
|
||||
label="nextcloud.freespace"
|
||||
value={t("common.bbytes", { value: nextcloudInfo.system.freespace, maximumFractionDigits: 1 })}
|
||||
/>
|
||||
<Block label="nextcloud.activeusers" value={t("common.number", { value: activeUsers.last24hours })} />
|
||||
<Block label="nextcloud.numfiles" value={t("common.number", { value: nextcloudInfo.storage.num_files })} />
|
||||
<Block label="nextcloud.numshares" value={t("common.number", { value: nextcloudInfo.shares.num_shares })} />
|
||||
|
||||
@@ -33,7 +33,9 @@ export default function Component({ service }) {
|
||||
|
||||
return (
|
||||
<Container service={service}>
|
||||
{nextdnsData.data.map(d => <Block key={d.status} label={d.status} value={t("common.number", { value: d.queries })} />)}
|
||||
{nextdnsData.data.map((d) => (
|
||||
<Block key={d.status} label={d.status} value={t("common.number", { value: d.queries })} />
|
||||
))}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,9 +7,7 @@ const widget = {
|
||||
mappings: {
|
||||
"analytics/status": {
|
||||
endpoint: "analytics/status",
|
||||
validate: [
|
||||
"data",
|
||||
]
|
||||
validate: ["data"],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user