style: auto-fix lint and format

This commit is contained in:
ajnart authored and github-actions[bot] committed 2026-06-09 16:10:56 +00:00
1 parent c9f003dadd
commit 2b4a18385d
10 files changed
+152 -164

No files matched your search

@@ -20,7 +20,7 @@ export const createItemCallback =
(previous: Board): Board => {
const firstSection = previous.sections
.filter((section): section is EmptySection => section.kind === "empty")
.sort((sectionA, sectionB) => sectionA.yOffset - sectionB.yOffset)
.toSorted((sectionA, sectionB) => sectionA.yOffset - sectionB.yOffset)
.at(0);
if (!firstSection) return previous;
@@ -20,7 +20,10 @@ import type {
const mediaTypeCountField = {
book: "totalAudiobooks",
podcast: "totalPodcasts",
} as const satisfies Record<AudiobookshelfLibrary["mediaType"], keyof Pick<AudiobookshelfDashboardData, "totalAudiobooks" | "totalPodcasts">>;
} as const satisfies Record<
AudiobookshelfLibrary["mediaType"],
keyof Pick<AudiobookshelfDashboardData, "totalAudiobooks" | "totalPodcasts">
>;
export class AudiobookshelfIntegration extends Integration {
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
@@ -1,4 +1,3 @@
import { ParseError, ResponseError } from "@homarr/common/server";
import { createLogger } from "@homarr/core/infrastructure/logs";
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
@@ -7,11 +6,7 @@ import type { IntegrationTestingInput } from "../base/integration";
import { Integration } from "../base/integration";
import { TestConnectionError } from "../base/test-connection/test-connection-error";
import type { TestingResult } from "../base/test-connection/test-connection-service";
import type {
NavidromeDashboardData,
NavidromeNowPlayingEntry,
SubsonicResponseBody,
} from "./navidrome-types";
import type { NavidromeDashboardData, NavidromeNowPlayingEntry, SubsonicResponseBody } from "./navidrome-types";
import { subsonicResponseSchema } from "./navidrome-types";
const logger = createLogger({ module: "navidromeIntegration" });
@@ -60,9 +55,7 @@ export class NavidromeIntegration extends Integration {
return TestConnectionError.UnauthorizedResult(401);
}
return TestConnectionError.UnknownResult(
new Error(subsonicResponse.error?.message ?? "Subsonic request failed"),
);
return TestConnectionError.UnknownResult(new Error(subsonicResponse.error?.message ?? "Subsonic request failed"));
}
return { success: true };
@@ -52,48 +52,48 @@ describe("NavidromeIntegration.getDashboardDataAsync", () => {
songCount,
}));
mockFetch.mockImplementation(
(url) => {
const urlStr = toUrlString(url);
if (urlStr.includes("getArtists")) {
return Promise.resolve(
new Response(subsonicOk({ artists: { index: [{ name: "A", artist: [{ id: "1", name: "Artist" }] }] } }), { status: 200 }),
) as unknown as ReturnType<typeof fetchWithTrustedCertificatesAsync>;
}
if (urlStr.includes("getAlbumList2")) {
const offsetMatch = urlStr.match(/offset=(\d+)/);
const offset = Number(offsetMatch?.[1] ?? 0);
if (offset === 0) {
return Promise.resolve(
new Response(subsonicOk({ albumList2: { album: makeAlbums(500, 10) } }), { status: 200 }),
) as unknown as ReturnType<typeof fetchWithTrustedCertificatesAsync>;
}
if (offset === 500) {
return Promise.resolve(
new Response(subsonicOk({ albumList2: { album: makeAlbums(200, 5) } }), { status: 200 }),
) as unknown as ReturnType<typeof fetchWithTrustedCertificatesAsync>;
}
return Promise.resolve(
new Response(subsonicOk({ albumList2: {} }), { status: 200 }),
) as unknown as ReturnType<typeof fetchWithTrustedCertificatesAsync>;
}
if (urlStr.includes("getNowPlaying")) {
return Promise.resolve(
new Response(subsonicOk({ nowPlaying: {} }), { status: 200 }),
) as unknown as ReturnType<typeof fetchWithTrustedCertificatesAsync>;
}
mockFetch.mockImplementation((url) => {
const urlStr = toUrlString(url);
if (urlStr.includes("getArtists")) {
return Promise.resolve(
new Response(subsonicOk({}), { status: 200 }),
new Response(subsonicOk({ artists: { index: [{ name: "A", artist: [{ id: "1", name: "Artist" }] }] } }), {
status: 200,
}),
) as unknown as ReturnType<typeof fetchWithTrustedCertificatesAsync>;
},
);
}
if (urlStr.includes("getAlbumList2")) {
const offsetMatch = urlStr.match(/offset=(\d+)/);
const offset = Number(offsetMatch?.[1] ?? 0);
if (offset === 0) {
return Promise.resolve(
new Response(subsonicOk({ albumList2: { album: makeAlbums(500, 10) } }), { status: 200 }),
) as unknown as ReturnType<typeof fetchWithTrustedCertificatesAsync>;
}
if (offset === 500) {
return Promise.resolve(
new Response(subsonicOk({ albumList2: { album: makeAlbums(200, 5) } }), { status: 200 }),
) as unknown as ReturnType<typeof fetchWithTrustedCertificatesAsync>;
}
return Promise.resolve(new Response(subsonicOk({ albumList2: {} }), { status: 200 })) as unknown as ReturnType<
typeof fetchWithTrustedCertificatesAsync
>;
}
if (urlStr.includes("getNowPlaying")) {
return Promise.resolve(new Response(subsonicOk({ nowPlaying: {} }), { status: 200 })) as unknown as ReturnType<
typeof fetchWithTrustedCertificatesAsync
>;
}
return Promise.resolve(new Response(subsonicOk({}), { status: 200 })) as unknown as ReturnType<
typeof fetchWithTrustedCertificatesAsync
>;
});
const integration = createIntegration();
const result = await integration.getDashboardDataAsync();
@@ -105,33 +105,31 @@ describe("NavidromeIntegration.getDashboardDataAsync", () => {
});
test("handles empty library gracefully", async () => {
mockFetch.mockImplementation(
(url) => {
const urlStr = toUrlString(url);
if (urlStr.includes("getArtists")) {
return Promise.resolve(
new Response(subsonicFailed("Library not found or empty"), { status: 200 }),
) as unknown as ReturnType<typeof fetchWithTrustedCertificatesAsync>;
}
if (urlStr.includes("getAlbumList2")) {
return Promise.resolve(
new Response(subsonicFailed("Library not found or empty"), { status: 200 }),
) as unknown as ReturnType<typeof fetchWithTrustedCertificatesAsync>;
}
if (urlStr.includes("getNowPlaying")) {
return Promise.resolve(
new Response(subsonicFailed("Library not found or empty"), { status: 200 }),
) as unknown as ReturnType<typeof fetchWithTrustedCertificatesAsync>;
}
mockFetch.mockImplementation((url) => {
const urlStr = toUrlString(url);
if (urlStr.includes("getArtists")) {
return Promise.resolve(
new Response(subsonicOk({}), { status: 200 }),
new Response(subsonicFailed("Library not found or empty"), { status: 200 }),
) as unknown as ReturnType<typeof fetchWithTrustedCertificatesAsync>;
},
);
}
if (urlStr.includes("getAlbumList2")) {
return Promise.resolve(
new Response(subsonicFailed("Library not found or empty"), { status: 200 }),
) as unknown as ReturnType<typeof fetchWithTrustedCertificatesAsync>;
}
if (urlStr.includes("getNowPlaying")) {
return Promise.resolve(
new Response(subsonicFailed("Library not found or empty"), { status: 200 }),
) as unknown as ReturnType<typeof fetchWithTrustedCertificatesAsync>;
}
return Promise.resolve(new Response(subsonicOk({}), { status: 200 })) as unknown as ReturnType<
typeof fetchWithTrustedCertificatesAsync
>;
});
const integration = createIntegration();
const result = await integration.getDashboardDataAsync();
@@ -145,9 +143,9 @@ describe("NavidromeIntegration.getDashboardDataAsync", () => {
test("throws on non-empty-library subsonic error", async () => {
mockFetch.mockImplementation(
() =>
Promise.resolve(
new Response(subsonicFailed("Permission denied"), { status: 200 }),
) as unknown as ReturnType<typeof fetchWithTrustedCertificatesAsync>,
Promise.resolve(new Response(subsonicFailed("Permission denied"), { status: 200 })) as unknown as ReturnType<
typeof fetchWithTrustedCertificatesAsync
>,
);
const integration = createIntegration();
@@ -155,37 +153,38 @@ describe("NavidromeIntegration.getDashboardDataAsync", () => {
});
test("now playing maps entries correctly", async () => {
mockFetch.mockImplementation(
(url) => {
const urlStr = toUrlString(url);
mockFetch.mockImplementation((url) => {
const urlStr = toUrlString(url);
if (urlStr.includes("getNowPlaying")) {
return Promise.resolve(
new Response(subsonicOk({
if (urlStr.includes("getNowPlaying")) {
return Promise.resolve(
new Response(
subsonicOk({
nowPlaying: {
entry: { title: "Song", artist: "Band", album: "LP", username: "user1", playerName: "Chrome" },
},
}), { status: 200 }),
) as unknown as ReturnType<typeof fetchWithTrustedCertificatesAsync>;
}
if (urlStr.includes("getArtists")) {
return Promise.resolve(
new Response(subsonicOk({ artists: {} }), { status: 200 }),
) as unknown as ReturnType<typeof fetchWithTrustedCertificatesAsync>;
}
if (urlStr.includes("getAlbumList2")) {
return Promise.resolve(
new Response(subsonicOk({ albumList2: {} }), { status: 200 }),
) as unknown as ReturnType<typeof fetchWithTrustedCertificatesAsync>;
}
return Promise.resolve(
new Response(subsonicOk({}), { status: 200 }),
}),
{ status: 200 },
),
) as unknown as ReturnType<typeof fetchWithTrustedCertificatesAsync>;
},
);
}
if (urlStr.includes("getArtists")) {
return Promise.resolve(new Response(subsonicOk({ artists: {} }), { status: 200 })) as unknown as ReturnType<
typeof fetchWithTrustedCertificatesAsync
>;
}
if (urlStr.includes("getAlbumList2")) {
return Promise.resolve(new Response(subsonicOk({ albumList2: {} }), { status: 200 })) as unknown as ReturnType<
typeof fetchWithTrustedCertificatesAsync
>;
}
return Promise.resolve(new Response(subsonicOk({}), { status: 200 })) as unknown as ReturnType<
typeof fetchWithTrustedCertificatesAsync
>;
});
const integration = createIntegration();
const result = await integration.getDashboardDataAsync();
@@ -2,7 +2,10 @@ import { z } from "zod/v4";
export const paperlessNgxStatisticsSchema = z.object({
documents_total: z.number(),
documents_inbox: z.number().nullable().transform((value) => value ?? 0),
documents_inbox: z
.number()
.nullable()
.transform((value) => value ?? 0),
});
export const paperlessNgxPaginatedCountSchema = z.object({
@@ -80,21 +80,19 @@ describe("UptimeKumaIntegration.getDashboardDataAsync", () => {
};
const mockResponses = () => {
mockFetch.mockImplementation(
(url) => {
const urlStr = toUrlString(url);
if (urlStr.includes("/heartbeat/")) {
return Promise.resolve(
new Response(JSON.stringify(heartbeatResponse), { status: 200 }),
) as unknown as ReturnType<typeof fetchWithTrustedCertificatesAsync>;
}
mockFetch.mockImplementation((url) => {
const urlStr = toUrlString(url);
if (urlStr.includes("/heartbeat/")) {
return Promise.resolve(
new Response(JSON.stringify(statusPageResponse), { status: 200 }),
new Response(JSON.stringify(heartbeatResponse), { status: 200 }),
) as unknown as ReturnType<typeof fetchWithTrustedCertificatesAsync>;
},
);
}
return Promise.resolve(
new Response(JSON.stringify(statusPageResponse), { status: 200 }),
) as unknown as ReturnType<typeof fetchWithTrustedCertificatesAsync>;
});
};
test("aggregates monitor counts correctly", async () => {
@@ -123,21 +121,22 @@ describe("UptimeKumaIntegration.getDashboardDataAsync", () => {
uptimeList: { "10_24": 1 },
};
mockFetch.mockImplementation(
(url) => {
const urlStr = toUrlString(url);
if (urlStr.includes("/heartbeat/")) {
return Promise.resolve(
new Response(JSON.stringify(unknownHeartbeat), { status: 200 }),
) as unknown as ReturnType<typeof fetchWithTrustedCertificatesAsync>;
}
mockFetch.mockImplementation((url) => {
const urlStr = toUrlString(url);
if (urlStr.includes("/heartbeat/")) {
return Promise.resolve(
new Response(JSON.stringify({
publicGroupList: [{ id: 1, name: "G", monitorList: [{ id: 10, name: "Svc" }] }],
}), { status: 200 }),
new Response(JSON.stringify(unknownHeartbeat), { status: 200 }),
) as unknown as ReturnType<typeof fetchWithTrustedCertificatesAsync>;
},
);
}
return Promise.resolve(
new Response(
JSON.stringify({
publicGroupList: [{ id: 1, name: "G", monitorList: [{ id: 10, name: "Svc" }] }],
}),
{ status: 200 },
),
) as unknown as ReturnType<typeof fetchWithTrustedCertificatesAsync>;
});
const integration = createIntegration([{ kind: "slug", value: "default" }]);
const result = await integration.getDashboardDataAsync();
@@ -63,7 +63,9 @@ export function AudioStatsContent({
const nowPlayingTracks =
navidromeStats?.nowPlaying.slice(
0,
nowPlayingLimitByListVisibility[String(options.showNowPlayingList) as keyof typeof nowPlayingLimitByListVisibility],
nowPlayingLimitByListVisibility[
String(options.showNowPlayingList) as keyof typeof nowPlayingLimitByListVisibility
],
) ?? [];
const hasContent = visibleStats.length > 0 || showNowPlayingSection;
@@ -76,10 +78,7 @@ export function AudioStatsContent({
style={{ "--stat-cols": gridCols } as CSSProperties}
>
{visibleStats.map(({ optionKey, statKey, value, Icon }) => (
<div
key={optionKey}
className={`${classes.statTile} ${statTileClassByCompact[compactKey]}`}
>
<div key={optionKey} className={`${classes.statTile} ${statTileClassByCompact[compactKey]}`}>
<Icon className={classes.statIcon} size={iconSize} stroke={1.5} />
<span className={classes.statValue}>{value}</span>
<span className={classes.statLabel}>{t(statKey as never)}</span>
@@ -95,10 +94,9 @@ export function AudioStatsContent({
<span className={classes.nowPlayingTitle}>{t("nowPlaying" as never)}</span>
{!options.showNowPlayingList && (
<Badge className={classes.nowPlayingCount} variant="light" size="sm">
{(t as unknown as (key: string, params?: { count: number }) => string)(
"nowPlayingCount",
{ count: navidromeStats.nowPlaying.length },
)}
{(t as unknown as (key: string, params?: { count: number }) => string)("nowPlayingCount", {
count: navidromeStats.nowPlaying.length,
})}
</Badge>
)}
</div>
@@ -54,7 +54,11 @@ describe("getVisibleStats", () => {
};
test("returns only enabled navidrome stats", () => {
const result = getVisibleStats("navidrome", { showArtists: true, showAlbums: false, showSongs: true }, navidromeStats);
const result = getVisibleStats(
"navidrome",
{ showArtists: true, showAlbums: false, showSongs: true },
navidromeStats,
);
expect(result).toHaveLength(2);
expect(result.map((s) => s.statKey)).toEqual(["artists", "songs"]);
expect(result[0]?.value).toBe(100);
@@ -64,7 +68,13 @@ describe("getVisibleStats", () => {
test("returns only enabled audiobookshelf stats", () => {
const result = getVisibleStats(
"audiobookshelf",
{ showLibraryCount: true, showAudiobooks: true, showPodcasts: false, showListeningTime: false, showActiveSessions: true },
{
showLibraryCount: true,
showAudiobooks: true,
showPodcasts: false,
showListeningTime: false,
showActiveSessions: true,
},
audiobookshelfStats,
);
expect(result).toHaveLength(3);
@@ -77,11 +87,7 @@ describe("getVisibleStats", () => {
});
test("formats listening time as duration string", () => {
const result = getVisibleStats(
"audiobookshelf",
{ showListeningTime: true },
audiobookshelfStats,
);
const result = getVisibleStats("audiobookshelf", { showListeningTime: true }, audiobookshelfStats);
expect(result).toHaveLength(1);
expect(typeof result[0]?.value).toBe("string");
});
@@ -1,14 +1,9 @@
"use client";
import { Fragment, type CSSProperties } from "react";
import { Fragment } from "react";
import type { CSSProperties } from "react";
import { RingProgress, Text } from "@mantine/core";
import {
IconFileDescription,
IconFileText,
IconInbox,
IconTag,
IconUsers,
} from "@tabler/icons-react";
import { IconFileDescription, IconFileText, IconInbox, IconTag, IconUsers } from "@tabler/icons-react";
import { clientApi } from "@homarr/api/client";
import { useScopedI18n } from "@homarr/translation/client";
@@ -74,11 +69,7 @@ const heroPartVisibility = {
ring: "showInboxRing",
} as const;
export default function PaperlessNgxWidget({
integrationIds,
options,
width,
}: WidgetComponentProps<"paperlessNgx">) {
export default function PaperlessNgxWidget({ integrationIds, options, width }: WidgetComponentProps<"paperlessNgx">) {
const t = useScopedI18n("widget.paperlessNgx");
const [stats] = clientApi.widget.paperlessNgx.getStats.useSuspenseQuery({
integrationId: integrationIds[0] ?? "",
@@ -150,9 +141,7 @@ export default function PaperlessNgxWidget({
{showHero && (
<div className={`${classes.hero} ${heroLayoutClass} ${heroRingClass}`}>
{visibleHeroParts.map(([partKey]) => (
<Fragment key={partKey}>
{heroPartRenderers[partKey as keyof typeof heroPartRenderers]}
</Fragment>
<Fragment key={partKey}>{heroPartRenderers[partKey as keyof typeof heroPartRenderers]}</Fragment>
))}
</div>
)}
@@ -94,9 +94,7 @@ export default function UptimeKumaWidget({
throw new NoIntegrationDataError();
}
return (
<UptimeKumaContent integrationIds={integrationIds} isEditMode={isEditMode} options={options} width={width} />
);
return <UptimeKumaContent integrationIds={integrationIds} isEditMode={isEditMode} options={options} width={width} />;
}
interface UptimeKumaContentProps {