Run pre-commit hooks over existing codebase

Co-Authored-By: Ben Phelps <ben@phelps.io>
This commit is contained in:
shamoon
2023-10-17 23:26:55 -07:00
parent fa50bbad9c
commit 19c25713c4
387 changed files with 4785 additions and 4109 deletions

View File

@@ -14,7 +14,7 @@ export default function Component({ service }) {
if (adguardError) {
return <Container service={service} error={adguardError} />;
}
if (!adguardData) {
return (
<Container service={service}>

View File

@@ -6,9 +6,9 @@ const widget = {
mappings: {
info: {
endpoint: "info"
}
endpoint: "info",
},
},
};
export default widget;
export default widget;

View File

@@ -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>
);
}

View File

@@ -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 } });
}
}

View File

@@ -11,4 +11,4 @@ const widget = {
},
};
export default widget;
export default widget;

View File

@@ -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 (

View File

@@ -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",

View File

@@ -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>
);
}

View File

@@ -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}",
},
},
};

View File

@@ -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" />

View File

@@ -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>
);
}

View File

@@ -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}` }} />;
}

View File

@@ -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}` }} />;
}

View File

@@ -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}` }} />;
}

View File

@@ -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}` }} />;
}

View File

@@ -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)}>&lt;</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)}>&gt;</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)}
>
&lt;
</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)}
>
&gt;
</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>
);
}

View File

@@ -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>
);

View File

@@ -5,12 +5,9 @@ const widget = {
proxyHandler: credentialedProxyHandler,
mappings: {
"cfd_tunnel": {
cfd_tunnel: {
endpoint: "cfd_tunnel",
validate: [
"success",
"result"
]
validate: ["success", "result"],
},
},
};

View File

@@ -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)}%

View File

@@ -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>
);
}

View File

@@ -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);

View File

@@ -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>
);
}

View File

@@ -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",
},
},
};

View File

@@ -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 })} />

View File

@@ -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)
);
}

View File

@@ -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",
},
},
};

View File

@@ -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} />;
}
}

View File

@@ -10,12 +10,7 @@ const widget = {
},
Count: {
endpoint: "Items/Counts",
segments: [
"MovieCount",
"SeriesCount",
"EpisodeCount",
"SongCount"
]
segments: ["MovieCount", "SeriesCount", "EpisodeCount", "SongCount"],
},
PlayControl: {
method: "POST",

View File

@@ -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>
);
}
}

View File

@@ -7,8 +7,8 @@ const widget = {
mappings: {
state: {
endpoint: "state",
}
},
},
};
export default widget;
export default widget;

View File

@@ -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 })} />

View File

@@ -5,10 +5,10 @@ const widget = {
proxyHandler: genericProxyHandler,
mappings: {
"status": {
status: {
endpoint: "status",
},
},
};
export default widget;
export default widget;

View File

@@ -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}>

View File

@@ -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,
});
}

View File

@@ -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,
});
}

View File

@@ -5,9 +5,9 @@ const widget = {
proxyHandler: freshrssProxyHandler,
mappings: {
info: {
endpoint: "/"
}
}
endpoint: "/",
},
},
};
export default widget;

View File

@@ -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}>

View File

@@ -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,
});
}
}

View File

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

View File

@@ -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} />;
}

View File

@@ -6,13 +6,13 @@ const widget = {
mappings: {
today: {
endpoint: "1d"
endpoint: "1d",
},
year: {
endpoint: "1y"
endpoint: "1y",
},
max: {
endpoint: "max"
endpoint: "max",
},
},
};

View File

@@ -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>;
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>
);
}

View File

@@ -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;
};
}

View File

@@ -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>;
}

View File

@@ -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>
);

View File

@@ -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>
)}

View File

@@ -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>

View File

@@ -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,
})}&deg; C
})}
&deg; 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 && (
<>&bull;</>
)}
{!chart && <>&bull;</>}
<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,
})}&deg; C
})}
&deg; 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>
);

View File

@@ -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>
);

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>
);

View File

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

View File

@@ -14,7 +14,6 @@ export default function Component({ service }) {
return <Container service={service} error={finalError} />;
}
if (!appsData || !messagesData || !clientsData) {
return (
<Container service={service}>

View File

@@ -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>
);
}

View File

@@ -10,11 +10,9 @@ const widget = {
},
stats: {
endpoint: "admin/stats",
validate: [
"dashboards"
]
validate: ["dashboards"],
},
},
};
export default widget;
export default widget;

View File

@@ -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>
);
}

View File

@@ -5,9 +5,9 @@ const widget = {
proxyHandler: genericProxyHandler,
mappings: {
"lineup": {
lineup: {
endpoint: "lineup.json",
}
},
},
};

View File

@@ -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);
}

View File

@@ -7,10 +7,7 @@ const widget = {
mappings: {
checks: {
endpoint: "checks",
validate: [
"status",
"last_ping",
]
validate: ["status", "last_ping"],
},
},
};

View File

@@ -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>
);
}

View File

@@ -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() };
}),
);
}

View File

@@ -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>
);
}

View File

@@ -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,
},
});
}

View File

@@ -7,7 +7,7 @@ const widget = {
mappings: {
info: {
endpoint: "/",
}
},
},
};

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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);
}

View File

@@ -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");
}

View File

@@ -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;

View File

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

View File

@@ -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>
);
}

View File

@@ -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,
});
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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>
);
}

View File

@@ -11,4 +11,4 @@ const widget = {
},
};
export default widget;
export default widget;

View File

@@ -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 })} />

View File

@@ -30,4 +30,4 @@ export default function Component({ service }) {
<Block label="mealie.tags" value={mealieData.totalTags} />
</Container>
);
}
}

View File

@@ -5,4 +5,4 @@ const widget = {
proxyHandler: credentialedProxyHandler,
};
export default widget;
export default widget;

View File

@@ -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;

View File

@@ -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>

View File

@@ -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",
}
},
},
};

View File

@@ -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>
);
}

View File

@@ -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,
});
}
}

View File

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

View File

@@ -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;

View File

@@ -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" />

View File

@@ -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>
);

View File

@@ -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>

View File

@@ -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;

View File

@@ -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);

View File

@@ -5,7 +5,7 @@ const widget = {
proxyHandler: genericProxyHandler,
mappings: {
"getNowPlaying": {
getNowPlaying: {
endpoint: "getNowPlaying",
},
},

View File

@@ -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 })} />

View File

@@ -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>
);
}

View File

@@ -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