Compare commits

..
Author SHA1 Message Date
manuel-rw 514bfdcf87 style: auto-fix lint and format 2026-07-01 18:53:07 +02:00
Manuel 5ec38d6136 feat: add pushover integration 2026-07-01 18:53:07 +02:00
homarr-renovate[bot] dd2eb3aa66 chore(deps): update dependency posthog-js to ^1.395.0 (#6150)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-07-01 15:51:52 +00:00
homarr-renovate[bot] aec6bb18c5 chore(deps): update dependency @scalar/api-reference-react to ^0.9.49 (#6146)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-07-01 12:21:19 +00:00
Thomas f9da71a19b fix: quick wins — calendar, about, glances, admin UX, releases, iframe, notes (#6116) 2026-07-01 13:57:39 +02:00
46 changed files with 530 additions and 578 deletions

No files matched your search

+1 -1
View File
@@ -1,7 +1,7 @@
{
"recommendations": [
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"oxc.oxc-vscode",
"yoavbls.pretty-ts-errors",
"million.million-lint",
"lokalise.i18n-ally"
+1
View File
@@ -42,6 +42,7 @@ RUN mkdir -p /var/cache/nginx && \
COPY --from=builder /app/apps/nextjs/next.config.ts .
COPY --from=builder /app/apps/nextjs/package.json .
COPY --from=builder /app/pnpm-workspace.yaml ./pnpm-workspace.yaml
COPY --from=builder /app/node_modules/better-sqlite3/build/Release/better_sqlite3.node /app/build/better_sqlite3.node
@@ -0,0 +1,42 @@
---
title: 'Pushover'
description: "Pushover is a service for receiving real-time push notifications on your Android, iPhone, iPad, and Desktop devices."
hide_title: true
---
import { notificationsWidget } from '@site/docs/widgets/notifications';
import { AddingIntegration } from '@site/src/components/integrations/adding';
import { IntegrationHeader } from '@site/src/components/integrations/header';
import { IntegrationSecrets } from '@site/src/components/integrations/secrets';
import { IntegrationCapabilites } from '@site/src/components/integrations/widgets';
import { pushoverIntegration } from '.';
<IntegrationHeader
integration={pushoverIntegration}
categories={['Notifications', 'Messaging']}
/>
### Widgets & Capabilities
<IntegrationCapabilites
items={[{
widget: notificationsWidget
}]}
/>
### Adding the integration
<AddingIntegration />
### Secrets
<IntegrationSecrets secrets={[{
credentials: ['apiKey', 'userKey'],
steps: [
{
title: 'API Token (Application Key)',
description: 'Register your application on the [Pushover website](https://pushover.net/apps) and copy the API token/application key.'
},
{
title: 'User Key',
description: 'Your user key is viewable on the [Pushover dashboard](https://pushover.net/) when logged in. It is a 30-character string (e.g. `uQiRzpo4DXghDmr9QzzfQu27cmVRsG`).'
}
]
}]} />
@@ -0,0 +1,8 @@
import { IntegrationDefinition } from "@site/src/types";
export const pushoverIntegration: IntegrationDefinition = {
name: "Pushover",
description: "Pushover is a service for receiving real-time push notifications on your devices.",
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/pushover.svg",
path: "../../integrations/pushover",
};
@@ -15,6 +15,7 @@ import { notificationsWidget } from '.';
import { ntfyIntegration } from '@site/docs/integrations/ntfy';
import { nextcloudIntegration } from '@site/docs/integrations/nextcloud';
import { gotifyIntegration } from '@site/docs/integrations/gotify';
import { pushoverIntegration } from '@site/docs/integrations/pushover';
<WidgetHeader
widget={notificationsWidget}
@@ -35,7 +36,8 @@ import notificationsScreenshot from './img/notifications.png';
items={[
{ integration: ntfyIntegration },
{ integration: nextcloudIntegration },
{ integration: gotifyIntegration }
{ integration: gotifyIntegration },
{ integration: pushoverIntegration }
]}
/>
@@ -10,6 +10,7 @@ import { extractBaseUrlFromHeaders } from "@homarr/common";
import { getScopedI18n } from "@homarr/translation/server";
import { createMetaTitle } from "~/metadata";
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
import { extractMcpTools } from "~/app/api/mcp/_extract-tools";
import { ApiKeysManagement } from "./components/api-keys";
import { ApiPageTabs } from "./components/api-page-tabs";
@@ -79,17 +80,20 @@ export default async function ApiPage() {
const toolGroups = getMcpToolGroups();
return (
<Stack>
<ApiPageTabs
documentationLabel={t("documentation.label")}
apiKeyLabel={t("apiKey.label")}
mcpLabel={t("mcp.label")}
documentationPanel={<ScalarApiReference document={document} />}
authenticationPanel={<ApiKeysManagement apiKeys={apiKeys} />}
baseUrl={baseUrl}
hasApiKeys={apiKeys.length > 0}
toolGroups={toolGroups}
/>
</Stack>
<>
<DynamicBreadcrumb />
<Stack>
<ApiPageTabs
documentationLabel={t("documentation.label")}
apiKeyLabel={t("apiKey.label")}
mcpLabel={t("mcp.label")}
documentationPanel={<ScalarApiReference document={document} />}
authenticationPanel={<ApiKeysManagement apiKeys={apiKeys} />}
baseUrl={baseUrl}
hasApiKeys={apiKeys.length > 0}
toolGroups={toolGroups}
/>
</Stack>
</>
);
}
@@ -6,6 +6,7 @@ import { dbEnv } from "@homarr/core/infrastructure/db/env";
import { getScopedI18n } from "@homarr/translation/server";
import { createMetaTitle } from "~/metadata";
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
import { BackupExportCard } from "./_components/backup-export-card";
import { BackupImportCard } from "./_components/backup-import-card";
@@ -31,10 +32,13 @@ export default async function BackupPage() {
const t = await getScopedI18n("management.page.tool.backup");
return (
<Stack>
<Title>{t("title")}</Title>
<BackupExportCard />
<BackupImportCard />
</Stack>
<>
<DynamicBreadcrumb />
<Stack>
<Title>{t("title")}</Title>
<BackupExportCard />
<BackupImportCard />
</Stack>
</>
);
}
@@ -7,6 +7,7 @@ import { getI18n, getScopedI18n } from "@homarr/translation/server";
import { Link } from "@homarr/ui";
import { ManageContainer } from "~/components/manage/manage-container";
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
import { NavigationLink } from "./_navigation";
interface LayoutProps {
@@ -24,6 +25,7 @@ export default async function Layout(props: PropsWithChildren<LayoutProps>) {
return (
<ManageContainer size="xl">
<DynamicBreadcrumb dynamicMappings={new Map([[params.id, group.name]])} />
<Grid>
<GridCol span={12}>
<Group justify="space-between" align="center">
@@ -10,3 +10,8 @@
background-color: var(--background-color) !important;
border-color: var(--border-color) !important;
}
.itemCard:global(.iframe-wrapper) {
background-color: transparent !important;
border-color: transparent !important;
}
@@ -14,6 +14,8 @@ interface DynamicBreadcrumbProps {
nonInteractable?: string[];
}
const categorySegments = new Set(["tools"]);
/**
* Breadcrumb is client side rendered. Elements are automatically
* calculated and translated using dynamic keys.
@@ -60,8 +62,11 @@ export const DynamicBreadcrumb = ({
const translationKey = `${pathnameParts.slice(0, index + 1).join(".")}`;
const mappedValue = dynamicMappings?.get(pathnamePart);
const isNonInteractable =
nonInteractable?.includes(pathnamePart) === true || categorySegments.has(pathnamePart);
if (mappedValue) {
if (nonInteractable?.includes(pathnamePart)) {
if (isNonInteractable) {
return <Text key={href}>{mappedValue}</Text>;
}
return (
@@ -71,7 +76,7 @@ export const DynamicBreadcrumb = ({
);
}
if (nonInteractable?.includes(pathnamePart)) {
if (isNonInteractable) {
return <Text key={href}>{t(`${translationKey}.label` as TranslationKeys)}</Text>;
}
+15 -7
View File
@@ -35,14 +35,22 @@ export const getDependenciesAsync = async (): Promise<PackageJsonDependencies> =
type DependencyCatalog = Map<string, string>;
const parseDependencyCatalogsAsync = async (): Promise<DependencyCatalog> => {
const currentDir = path.dirname(fileURLToPath(import.meta.url));
const workspaceConfigContent = await fsPromises.readFile(
path.join(currentDir, "..", "..", "..", "..", "pnpm-workspace.yaml"),
"utf-8",
);
const workspaceConfig: unknown = parseYaml(workspaceConfigContent);
const parseResult = workspaceConfigCatalogSchema.parse(workspaceConfig);
const workspaceConfigPath = path.join(currentDir, "..", "..", "..", "..", "pnpm-workspace.yaml");
return new Map(Object.entries(parseResult.catalog ?? {}));
try {
const workspaceConfigContent = await fsPromises.readFile(workspaceConfigPath, "utf-8");
const workspaceConfig: unknown = parseYaml(workspaceConfigContent);
const parseResult = workspaceConfigCatalogSchema.parse(workspaceConfig);
return new Map(Object.entries(parseResult.catalog ?? {}));
} catch (error) {
// The workspace config is absent in Docker (only the runner stage is shipped),
// so a missing file is expected. Surface any other failure (e.g. malformed YAML).
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
return new Map();
}
throw error;
}
};
const workspaceConfigCatalogSchema = z.object({
@@ -1,27 +0,0 @@
import { z } from "zod/v4";
import { createOneIntegrationMiddleware } from "../../middlewares/integration";
import { createTRPCRouter, publicProcedure } from "../../trpc";
export const dawarichRouter = createTRPCRouter({
getStatistics: publicProcedure.concat(createOneIntegrationMiddleware("query", "dawarich")).query(async ({ ctx }) => {
const { dawarichStatisticsRequestHandler } = await import("@homarr/request-handler/dawarich");
const innerHandler = dawarichStatisticsRequestHandler.handler(ctx.integration, {});
const data = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
return data.data;
}),
getPlaces: publicProcedure
.concat(createOneIntegrationMiddleware("query", "dawarich"))
.input(
z.object({
limit: z.number().min(1).max(50).default(10),
}),
)
.query(async ({ ctx, input }) => {
const { dawarichPlacesRequestHandler } = await import("@homarr/request-handler/dawarich");
const innerHandler = dawarichPlacesRequestHandler.handler(ctx.integration, {});
const { data } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
return data.slice(0, input.limit);
}),
});
-1
View File
@@ -39,5 +39,4 @@ export const widgetRouter = createTRPCRouter({
vpn: lazy(() => import("./vpn").then((mod) => mod.vpnRouter)),
ups: lazy(() => import("./ups").then((mod) => mod.upsRouter)),
customApi: lazy(() => import("./custom-api").then((mod) => mod.customApiRouter)),
dawarich: lazy(() => import("./dawarich").then((mod) => mod.dawarichRouter)),
});
@@ -326,6 +326,7 @@ export type HomarrDocumentationPath =
| "/docs/integrations/quay"
| "/docs/integrations/ntfy"
| "/docs/integrations/gotify"
| "/docs/integrations/pushover"
| "/docs/integrations/ical"
| "/docs/integrations/anchor"
| "/docs/integrations/truenas"
@@ -343,7 +344,6 @@ export type HomarrDocumentationPath =
| "/docs/integrations/gluetun"
| "/docs/integrations/peanut"
| "/docs/integrations/beszel"
| "/docs/integrations/dawarich"
| "/docs/widgets/clock"
| "/docs/widgets/weather"
| "/docs/widgets/app"
@@ -41,6 +41,7 @@ export const integrationDocSlugs: Record<IntegrationKind, string | null> = {
quay: "quay",
ntfy: "ntfy",
gotify: "gotify",
pushover: "pushover",
ical: "ical",
anchor: "anchor",
truenas: "truenas",
@@ -59,6 +60,5 @@ export const integrationDocSlugs: Record<IntegrationKind, string | null> = {
peaNut: "peanut",
beszel: "beszel",
archiveTeamWarrior: null,
dawarich: "dawarich",
mock: null,
};
@@ -51,6 +51,4 @@ export const widgetDocSlugs: Record<WidgetKind, string | null> = {
beszelAlerts: "beszel-alerts",
beszelSystemStats: "beszel-system-stats",
customApi: "custom-api",
dawarichStatistics: null,
dawarichPlaces: null,
};
+9 -8
View File
@@ -18,6 +18,7 @@ export const integrationSecretKindObject = {
githubAppId: { isPublic: true, multiline: false },
githubInstallationId: { isPublic: true, multiline: false },
slug: { isPublic: true, multiline: false },
userKey: { isPublic: false, multiline: false },
} satisfies Record<string, { isPublic: boolean; multiline: boolean }>;
export const integrationSecretKinds = objectKeys(integrationSecretKindObject);
@@ -362,6 +363,14 @@ export const integrationDefs = {
category: ["notifications"],
documentationUrl: createDocumentationLink("/docs/integrations/gotify"),
},
pushover: {
name: "Pushover",
secretKinds: [["apiKey", "userKey"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/pushover.svg",
category: ["notifications"],
defaultUrl: "https://api.pushover.net",
documentationUrl: createDocumentationLink("/docs/integrations/pushover"),
},
ical: {
name: "iCal",
secretKinds: [["url"]],
@@ -504,13 +513,6 @@ export const integrationDefs = {
defaultUrl: "http://localhost:8001",
documentationUrl: createDocumentationLink("/docs/integrations/archiveteam-warrior"),
},
dawarich: {
name: "Dawarch",
secretKinds: [["apiKey"]],
iconUrl: "https://cdn.jsdelivr.net/gh/selfhst/icons/png/dawarich.png",
category: ["geolocationTracking"],
documentationUrl: createDocumentationLink("/docs/integrations/dawarich"),
},
// This integration only returns mock data, it is used during development (but can also be used in production by directly going to the create page)
mock: {
name: "Mock",
@@ -624,7 +626,6 @@ export const integrationCategories = [
"documents",
"mediaLibrary",
"uptimeMonitoring",
"geolocationTracking",
] as const;
export type IntegrationCategory = (typeof integrationCategories)[number];
@@ -40,8 +40,6 @@ export const widgetIntegrationSupport: Partial<Record<WidgetKind, readonly Integ
audioStats: ["navidrome", "audiobookshelf"],
vpn: getIntegrationKindsByCategory("vpn"),
anchorNote: ["anchor"],
dawarichStatistics: ["dawarich"],
dawarichPlaces: ["dawarich"],
};
export const getWidgetKindsForIntegration = (integrationKind: IntegrationKind): WidgetKind[] => {
-2
View File
@@ -49,8 +49,6 @@ export const widgetKinds = [
"beszelAlerts",
"beszelSystemStats",
"customApi",
"dawarichStatistics",
"dawarichPlaces",
] as const;
export type WidgetKind = (typeof widgetKinds)[number];
+2 -2
View File
@@ -39,6 +39,7 @@ import { NextcloudIntegration } from "../nextcloud/nextcloud.integration";
import { NPMIntegration } from "../npm/npm-integration";
import { GotifyIntegration } from "../gotify/gotify-integration";
import { NTFYIntegration } from "../ntfy/ntfy-integration";
import { PushoverIntegration } from "../pushover/pushover-integration";
import { OpenMediaVaultIntegration } from "../openmediavault/openmediavault-integration";
import { OPNsenseIntegration } from "../opnsense/opnsense-integration";
import { OverseerrIntegration } from "../overseerr/overseerr-integration";
@@ -60,7 +61,6 @@ import { UptimeKumaIntegration } from "../uptime-kuma/uptime-kuma-integration";
import { UnifiControllerIntegration } from "../unifi-controller/unifi-controller-integration";
import { UnraidIntegration } from "../unraid/unraid-integration";
import type { Integration, IntegrationInput } from "./integration";
import { DawarichIntegration } from "../dawarich/dawarich-integration";
export const createIntegrationAsync = async <TKind extends keyof typeof integrationCreators>(
integration: IntegrationInput & { kind: TKind },
@@ -126,6 +126,7 @@ export const integrationCreators = {
quay: QuayIntegration,
ntfy: NTFYIntegration,
gotify: GotifyIntegration,
pushover: PushoverIntegration,
mock: MockIntegration,
truenas: TrueNasIntegration,
unraid: UnraidIntegration,
@@ -144,7 +145,6 @@ export const integrationCreators = {
uptimeKuma: UptimeKumaIntegration,
peaNut: PeaNutIntegration,
beszel: BeszelIntegration,
dawarich: DawarichIntegration,
} satisfies Record<IntegrationKind, IntegrationInstance | [(input: IntegrationInput) => Promise<Integration>]>;
type IntegrationInstanceOfKind<TKind extends keyof typeof integrationCreators> = {
@@ -1,52 +0,0 @@
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
import { ResponseError } from "@homarr/common/server";
import type { IntegrationInput, IntegrationTestingInput } from "../base/integration";
import { Integration } from "../base/integration";
import { dawarichPlacesSchema, dawarichStatisticsSchema } from "./dawarich-schemas";
import type { z } from "zod/v4";
import type { TestingResult } from "../base/test-connection/test-connection-service";
import { TestConnectionError } from "../base/test-connection/test-connection-error";
export class DawarichIntegration extends Integration {
constructor(integration: IntegrationInput) {
super(integration);
}
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
const response = await fetchWithTrustedCertificatesAsync(this.url(`/api/v1/users/me`), {
headers: { Authorization: `Bearer ${this.getSecretValue("apiKey")}` },
});
if (response.status === 200) {
return { success: true };
}
return TestConnectionError.StatusResult(response);
}
async getStatisticsAsync(): Promise<{
totalDistanceKm: number;
totalPointsTracked: number;
totalReverseGeocodedPoints: number;
totalCountriesVisited: number;
totalCitiesVisited: number;
}> {
const response = await fetchWithTrustedCertificatesAsync(this.url("/api/v1/stats"), {
headers: { Authorization: `Bearer ${this.getSecretValue("apiKey")}` },
});
if (!response.ok) throw new ResponseError(response);
return dawarichStatisticsSchema.parseAsync(await response.json());
}
async getPlacesAsync(): Promise<z.infer<typeof dawarichPlacesSchema>> {
const response = await fetchWithTrustedCertificatesAsync(this.url("/api/v1/places"), {
headers: { Authorization: `Bearer ${this.getSecretValue("apiKey")}` },
});
if (!response.ok) throw new ResponseError(response);
return dawarichPlacesSchema.parseAsync(await response.json());
}
}
@@ -1,31 +0,0 @@
import { z } from "zod/v4";
export const dawarichStatisticsSchema = z.object({
totalDistanceKm: z.number(),
totalPointsTracked: z.number(),
totalReverseGeocodedPoints: z.number(),
totalCountriesVisited: z.number(),
totalCitiesVisited: z.number(),
});
export const dawarichPlaceTagSchema = z.object({
id: z.number(),
name: z.string(),
icon: z.string(),
color: z.string(),
});
export const dawarichPlaceSchema = z.object({
id: z.number(),
name: z.string(),
latitude: z.number(),
longitude: z.number(),
source: z.string().optional(),
icon: z.string().nullable(),
color: z.string().nullable(),
visits_count: z.number(),
created_at: z.string(),
tags: z.array(dawarichPlaceTagSchema),
});
export const dawarichPlacesSchema = z.array(dawarichPlaceSchema);
@@ -1,5 +0,0 @@
import type { z } from "zod/v4";
import type { dawarichPlacesSchema, dawarichStatisticsSchema } from "./dawarich-schemas";
export type DawarichStatistics = z.infer<typeof dawarichStatisticsSchema>;
export type DawarichPlace = z.infer<typeof dawarichPlacesSchema>[number];
@@ -35,7 +35,7 @@ export class GlancesIntegration extends Integration implements ISystemHealthMoni
if (session == null) throw new Error("Session was unexpectitly null");
const stats = await this.getAllStatsAsync();
const [stats, cpuTemp] = await Promise.all([this.getAllStatsAsync(), this.getCpuTempAsync()]);
return {
cpuUtilization: stats.cpu.total,
@@ -58,7 +58,7 @@ export class GlancesIntegration extends Integration implements ISystemHealthMoni
cpuModelName: stats.quicklook?.cpu_name ?? "Unknown",
loadAverage: null,
smart: [],
cpuTemp: undefined,
cpuTemp,
gpu: stats.gpu.map((gpu) => ({
gpuId: gpu.gpu_id,
name: gpu.name,
@@ -80,6 +80,23 @@ export class GlancesIntegration extends Integration implements ISystemHealthMoni
return await response.text();
}
private async getCpuTempAsync(): Promise<number | undefined> {
// CPU temperature is optional data, so a transport error or payload drift on
// /api/4/sensors must not fail the whole getSystemInfoAsync() call.
try {
const response = await fetchWithTrustedCertificatesAsync(this.url("/api/4/sensors"));
if (!response.ok) {
return undefined;
}
const sensors = await sensorsSchema.parseAsync(await response.json());
return parseGlancesCpuTempFromSensors(sensors);
} catch {
return undefined;
}
}
private async getAllStatsAsync() {
const response = await fetchWithTrustedCertificatesAsync(this.url("/api/4/all"));
@@ -173,3 +190,33 @@ const allSchema = z.object({
)
.default([]),
});
const sensorsSchema = z.array(
z.object({
label: z.string(),
value: z.number(),
type: z.string().optional(),
}),
);
const cpuTempLabelPriority = ["CPU", "Package id 0"] as const;
export const parseGlancesCpuTempFromSensors = (sensors: z.infer<typeof sensorsSchema>): number | undefined => {
const temperatureSensors = sensors.filter(
(sensor) => sensor.type === "temperature_core" || sensor.type === "temperature",
);
for (const label of cpuTempLabelPriority) {
const match = temperatureSensors.find((sensor) => sensor.label === label);
if (match) {
return match.value;
}
}
const firstCore = temperatureSensors[0];
if (firstCore) {
return firstCore.value;
}
return undefined;
};
+1 -2
View File
@@ -19,6 +19,7 @@ export { ReadarrIntegration } from "./media-organizer/readarr/readarr-integratio
export { SonarrIntegration } from "./media-organizer/sonarr/sonarr-integration";
export { NextcloudIntegration } from "./nextcloud/nextcloud.integration";
export { GotifyIntegration } from "./gotify/gotify-integration";
export { PushoverIntegration } from "./pushover/pushover-integration";
export { NTFYIntegration } from "./ntfy/ntfy-integration";
export { OpenMediaVaultIntegration } from "./openmediavault/openmediavault-integration";
export { GlancesIntegration } from "./glances/glances-integration";
@@ -41,7 +42,6 @@ export { NavidromeIntegration } from "./navidrome/navidrome-integration";
export { UptimeKumaIntegration } from "./uptime-kuma/uptime-kuma-integration";
export { UmamiIntegration } from "./umami/umami-integration";
export { PeaNutIntegration } from "./peanut/peanut-integration";
export { DawarichIntegration } from "./dawarich/dawarich-integration";
// Types
export type { IntegrationInput } from "./base/integration";
@@ -87,7 +87,6 @@ export type { AudiobookshelfDashboardData } from "./audiobookshelf/audiobookshel
export type { NavidromeDashboardData } from "./navidrome/navidrome-types";
export type { UptimeKumaDashboardData } from "./uptime-kuma/uptime-kuma-types";
export type { UmamiVisitorStats } from "./umami/umami-types";
export type { DawarichStatistics, DawarichPlace } from "./dawarich/dawarich-types";
// Schemas
export { anchorNotesListInputSchema } from "./anchor/anchor-types";
@@ -69,36 +69,24 @@ export class LidarrIntegration extends Integration implements ICalendarIntegrati
private getLinksForLidarrCalendarEvent = (event: z.infer<typeof lidarrCalendarEventSchema>) => {
const links: CalendarLink[] = [];
// titleSlug is only present when Lidarr returns it; guard so a missing value
// neither breaks the link nor drops the event.
if (event.artist.titleSlug) {
links.push({
href: this.externalUrl(`/artist/${event.artist.titleSlug}`).toString(),
name: "Lidarr",
logo: "/images/apps/lidarr.svg",
color: undefined,
isDark: true,
});
}
for (const link of event.artist.links) {
switch (link.name) {
case "vgmdb":
links.push({
href: link.url,
name: "VgmDB",
color: "#f5c518",
isDark: false,
logo: "/images/apps/vgmdb.svg",
});
break;
case "imdb":
links.push({
href: link.url,
name: "IMDb",
color: "#f5c518",
isDark: false,
logo: "/images/apps/imdb.png",
});
break;
case "last":
links.push({
href: link.url,
name: "LastFM",
color: "#cf222a",
isDark: false,
logo: "/images/apps/lastfm.svg",
});
break;
const externalLink = lidarrExternalCalendarLinks[link.name];
if (!externalLink) {
continue;
}
links.push({ ...externalLink, href: link.url });
}
return links;
@@ -109,7 +97,7 @@ export class LidarrIntegration extends Integration implements ICalendarIntegrati
): z.infer<typeof lidarrCalendarEventSchema>["images"][number] | undefined => {
const flatImages = [...event.images];
const sortedImages = flatImages.sort(
const sortedImages = flatImages.toSorted(
(imageA, imageB) =>
mediaOrganizerPriorities.indexOf(imageA.coverType) - mediaOrganizerPriorities.indexOf(imageB.coverType),
);
@@ -149,6 +137,16 @@ const lidarrCalendarEventSchema = z.object({
title: z.string(),
overview: z.string().optional(),
images: lidarrCalendarEventImageSchema,
artist: z.object({ links: z.array(z.object({ url: z.string().url(), name: z.string() })), artistName: z.string() }),
artist: z.object({
links: z.array(z.object({ url: z.string().url(), name: z.string() })),
artistName: z.string(),
titleSlug: z.string().optional(),
}),
releaseDate: z.string().transform((value) => new Date(value)),
});
const lidarrExternalCalendarLinks: Record<string, Omit<CalendarLink, "href">> = {
vgmdb: { name: "VgmDB", color: "#f5c518", isDark: false, logo: "/images/apps/vgmdb.svg" },
imdb: { name: "IMDb", color: "#f5c518", isDark: false, logo: "/images/apps/imdb.png" },
last: { name: "LastFM", color: "#cf222a", isDark: false, logo: "/images/apps/lastfm.svg" },
};
@@ -0,0 +1,38 @@
import { ResponseError } from "@homarr/common/server";
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
import { Integration } from "../base/integration";
import type { IntegrationTestingInput } from "../base/integration";
import type { TestingResult } from "../base/test-connection/test-connection-service";
import type { Notification } from "../interfaces/notifications/notification-types";
import type { INotificationsIntegration } from "../interfaces/notifications/notifications-integration";
import { pushoverLimitsResponseSchema } from "./pushover-schema";
export class PushoverIntegration extends Integration implements INotificationsIntegration {
public async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
const token = this.getSecretValue("apiKey");
const url = this.url("/1/apps/limits.json", { token });
const response = await input.fetchAsync(url);
if (!response.ok) throw new ResponseError(response);
const result = await pushoverLimitsResponseSchema.safeParseAsync(await response.json());
if (!result.success) throw new Error(`Failed to parse Pushover response: ${result.error.message}`);
if (result.data.status !== 1) throw new Error("Pushover API returned non-success status");
return { success: true };
}
public async getNotificationsAsync(): Promise<Notification[]> {
const token = this.getSecretValue("apiKey");
const url = this.url("/1/apps/limits.json", { token });
const response = await fetchWithTrustedCertificatesAsync(url);
if (!response.ok) throw new ResponseError(response);
const result = await pushoverLimitsResponseSchema.safeParseAsync(await response.json());
if (!result.success) throw new Error(`Failed to parse Pushover response: ${result.error.message}`);
return [];
}
}
@@ -0,0 +1,9 @@
import { z } from "zod/v4";
export const pushoverLimitsResponseSchema = z.object({
status: z.number(),
request: z.string(),
limit: z.number().optional(),
remaining: z.number().optional(),
reset: z.number().optional(),
});
-1
View File
@@ -25,4 +25,3 @@ export * from "./paperless-ngx/paperless-ngx-types";
export * from "./umami/umami-types";
export * from "./gluetun/gluetun-types";
export * from "./beszel/beszel-types";
export * from "./dawarich/dawarich-types";
@@ -1,11 +1,11 @@
// @vitest-environment node
import { describe, expect, test, vi } from "vitest";
import { beforeEach, describe, expect, test, vi } from "vitest";
import { createDb } from "@homarr/db/test";
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
import { GlancesIntegration } from "../src/glances/glances-integration";
import { GlancesIntegration, parseGlancesCpuTempFromSensors } from "../src/glances/glances-integration";
// ─── module mocks ────────────────────────────────────────────────────────────
@@ -64,6 +64,19 @@ function makeResponse(body: unknown) {
} as never;
}
function mockGlancesFetch(statsBody: unknown, sensorsBody: unknown) {
mockFetch.mockImplementation(((url: string | URL) => {
const urlString = url.toString();
if (urlString.endsWith("/api/4/all")) {
return Promise.resolve(makeResponse(statsBody));
}
if (urlString.endsWith("/api/4/sensors")) {
return Promise.resolve(makeResponse(sensorsBody));
}
return Promise.reject(new Error(`Unexpected fetch URL: ${urlString}`));
}) as typeof fetchWithTrustedCertificatesAsync);
}
const integrationInput = {
id: "test-glances",
name: "Glances",
@@ -75,8 +88,12 @@ const integrationInput = {
// ─── tests ───────────────────────────────────────────────────────────────────
describe("GlancesIntegration schema", () => {
beforeEach(() => {
mockFetch.mockReset();
});
test("parses stats when quicklook is present", async () => {
mockFetch.mockResolvedValueOnce(makeResponse({ ...baseStats, quicklook: { cpu_name: "Apple M1" } }));
mockGlancesFetch({ ...baseStats, quicklook: { cpu_name: "Apple M1" } }, []);
const integration = new GlancesIntegration(integrationInput);
const result = await integration.getSystemInfoAsync();
@@ -86,7 +103,7 @@ describe("GlancesIntegration schema", () => {
});
test("returns Unknown cpuModelName when quicklook is absent (macOS scenario)", async () => {
mockFetch.mockResolvedValueOnce(makeResponse(baseStats));
mockGlancesFetch(baseStats, []);
const integration = new GlancesIntegration(integrationInput);
const result = await integration.getSystemInfoAsync();
@@ -95,9 +112,41 @@ describe("GlancesIntegration schema", () => {
});
test("does not throw a Zod parse error when quicklook is missing", async () => {
mockFetch.mockResolvedValueOnce(makeResponse(baseStats));
mockGlancesFetch(baseStats, []);
const integration = new GlancesIntegration(integrationInput);
await expect(integration.getSystemInfoAsync()).resolves.not.toThrow();
});
test("reads CPU temperature from sensors", async () => {
mockGlancesFetch(baseStats, [
{ label: "CPU", unit: "C", value: 58, type: "temperature_core" },
{ label: "Core 0", unit: "C", value: 54, type: "temperature_core" },
]);
const integration = new GlancesIntegration(integrationInput);
const result = await integration.getSystemInfoAsync();
expect(result.cpuTemp).toBe(58);
});
});
describe("parseGlancesCpuTempFromSensors", () => {
test("prefers CPU label over core sensors", () => {
const result = parseGlancesCpuTempFromSensors([
{ label: "Core 0", value: 54, type: "temperature_core" },
{ label: "CPU", value: 58, type: "temperature_core" },
]);
expect(result).toBe(58);
});
test("falls back to Package id 0", () => {
const result = parseGlancesCpuTempFromSensors([
{ label: "Core 0", value: 54, type: "temperature_core" },
{ label: "Package id 0", value: 62, type: "temperature_core" },
]);
expect(result).toBe(62);
});
});
@@ -0,0 +1,76 @@
// @vitest-environment node
import { describe, expect, test, vi } from "vitest";
import { createDb } from "@homarr/db/test";
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
import { LidarrIntegration } from "../src/media-organizer/lidarr/lidarr-integration";
vi.mock("@homarr/db", async (importActual) => {
const actual = await importActual<typeof import("@homarr/db")>();
return { ...actual, db: createDb() };
});
vi.mock("@homarr/core/infrastructure/certificates", async (importActual) => {
const actual = await importActual<typeof import("@homarr/core/infrastructure/certificates")>();
return {
...actual,
getTrustedCertificateHostnamesAsync: vi.fn().mockResolvedValue([]),
};
});
vi.mock("@homarr/core/infrastructure/http", () => ({
fetchWithTrustedCertificatesAsync: vi.fn(),
}));
const mockFetch = vi.mocked(fetchWithTrustedCertificatesAsync);
const integrationInput = {
id: "test-lidarr",
name: "Lidarr",
url: "http://localhost:8686",
decryptedSecrets: [{ kind: "apiKey" as const, value: "test-key" }],
externalUrl: null,
};
describe("LidarrIntegration calendar", () => {
test("includes Lidarr artist link in calendar events", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: () =>
Promise.resolve([
{
title: "Test Album",
overview: "Overview",
images: [],
artist: {
artistName: "Test Artist",
titleSlug: "test-artist",
links: [{ name: "last", url: "https://last.fm/music/Test+Artist" }],
},
releaseDate: "2026-01-01T00:00:00Z",
},
]),
} as never);
const integration = new LidarrIntegration(integrationInput);
const events = await integration.getCalendarEventsAsync(new Date("2026-01-01"), new Date("2026-01-31"));
expect(events[0]?.links[0]).toMatchObject({
name: "Lidarr",
href: "http://localhost:8686/artist/test-artist",
logo: "/images/apps/lidarr.svg",
});
// The existing external artist link must survive the Lidarr-link prepend.
expect(events[0]?.links).toEqual(
expect.arrayContaining([
expect.objectContaining({
name: "LastFM",
href: "https://last.fm/music/Test+Artist",
logo: "/images/apps/lastfm.svg",
}),
]),
);
});
});
-32
View File
@@ -1,32 +0,0 @@
import dayjs from "dayjs";
import { createIntegrationAsync } from "@homarr/integrations";
import type { DawarichPlace, DawarichStatistics } from "@homarr/integrations";
import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler";
export const dawarichStatisticsRequestHandler = createCachedIntegrationRequestHandler<
DawarichStatistics,
"dawarich",
Record<string, never>
>({
async requestAsync(integration) {
const instance = await createIntegrationAsync(integration);
return await instance.getStatisticsAsync();
},
cacheDuration: dayjs.duration(5, "minutes"),
queryKey: "dawarichStatistics",
});
export const dawarichPlacesRequestHandler = createCachedIntegrationRequestHandler<
DawarichPlace[],
"dawarich",
Record<string, never>
>({
async requestAsync(integration) {
const instance = await createIntegrationAsync(integration);
return await instance.getPlacesAsync();
},
cacheDuration: dayjs.duration(5, "minutes"),
queryKey: "dawarichPlaces",
});
-36
View File
@@ -3545,42 +3545,6 @@
"executeSuccess": "Action executed successfully",
"executeFailed": "Action failed. Check the widget configuration."
},
"dawarichStatistics": {
"name": "Dawarich Statistics",
"description": "Display travel statistics from Dawarich",
"option": {
"showTotalDistance": {
"label": "Show total distance"
},
"showTotalPoints": {
"label": "Show total points tracked"
},
"showReverseGeocoded": {
"label": "Show reverse geocoded points"
},
"showCountries": {
"label": "Show countries visited"
},
"showCities": {
"label": "Show cities visited"
}
},
"totalDistance": "Total distance",
"totalPoints": "Points tracked",
"reverseGeocoded": "Reverse geocoded",
"countries": "Countries",
"cities": "Cities"
},
"dawarichPlaces": {
"name": "Dawarich Places",
"description": "Display recently visited places from Dawarich",
"option": {
"maxPlaces": {
"label": "Max places to show"
}
},
"visits": "visits"
},
"beszelSystemTable": {
"name": "Beszel Systems (Table)",
"description": "Table view of all Beszel-monitored systems with sortable columns and status indicators",
+2
View File
@@ -8,6 +8,8 @@
transition: scale 0.2s ease-in-out;
user-select: none;
-webkit-touch-callout: none;
-webkit-user-drag: none;
touch-action: manipulation;
}
.appWithUrl:hover > .appIcon {
+1
View File
@@ -87,6 +87,7 @@ export default function AppWidget({ options, isEditMode, height, width }: Widget
justify="center"
align="center"
gap={isColumnLayout ? 0 : "sm"}
onContextMenu={isEditMode ? (e) => e.preventDefault() : undefined}
>
<Stack gap={0}>
{options.showTitle && (
@@ -1,10 +0,0 @@
.placeItem {
padding: 8px 12px;
border-radius: var(--mantine-radius-sm);
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
transition: all 0.2s ease;
&:hover {
background-color: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-5));
}
}
@@ -1,40 +0,0 @@
"use client";
import { Anchor, Group, Stack, Text } from "@mantine/core";
import { IconMapPin } from "@tabler/icons-react";
import { clientApi } from "@homarr/api/client";
import { useI18n } from "@homarr/translation/client";
import { WidgetEmptyState } from "../common/empty-state";
import type { WidgetComponentProps } from "../definition";
import classes from "./component.module.css";
export default function DawarichPlacesWidget({ integrationIds, options }: WidgetComponentProps<"dawarichPlaces">) {
const t = useI18n();
const { data: places } = clientApi.widget.dawarich.getPlaces.useQuery(
{
integrationId: integrationIds[0] ?? "",
limit: options.maxPlaces,
},
{ staleTime: 5 * 60 * 1000 },
);
if (!places || places.length === 0) return <WidgetEmptyState />;
return (
<Stack gap="xs" h="100%" p="md">
{places.map((place) => (
<Group key={place.id} gap="sm" align="center" className={classes.placeItem}>
<IconMapPin size={16} />
<Text size="sm" fw={500} style={{ flex: 1 }}>
{place.name}
</Text>
<Text size="xs" c="dimmed">
{place.visits_count} {t("widget.dawarichPlaces.visits")}
</Text>
</Group>
))}
</Stack>
);
}
@@ -1,16 +0,0 @@
import { IconMapPin } from "@tabler/icons-react";
import z from "zod";
import { createWidgetDefinition } from "../definition";
import { optionsBuilder } from "../options";
export const { definition, componentLoader } = createWidgetDefinition("dawarichPlaces", {
icon: IconMapPin,
supportedIntegrations: ["dawarich"],
integrationsRequired: true,
createOptions() {
return optionsBuilder.from((factory) => ({
maxPlaces: factory.number({ defaultValue: 10, validate: z.number().min(1).max(50) }),
}));
},
}).withDynamicImport(() => import("./component"));
@@ -1,10 +0,0 @@
.statItem {
padding: 12px;
border-radius: var(--mantine-radius-md);
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
transition: all 0.2s ease;
&:hover {
background-color: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-5));
}
}
@@ -1,88 +0,0 @@
"use client";
import { Group, Stack, Text } from "@mantine/core";
import { IconMap, IconMapPin, IconRoute, IconWorld, IconBuildingSkyscraper } from "@tabler/icons-react";
import { clientApi } from "@homarr/api/client";
import { useI18n } from "@homarr/translation/client";
import { WidgetEmptyState } from "../common/empty-state";
import type { WidgetComponentProps } from "../definition";
import classes from "./component.module.css";
export default function DawarichStatisticsWidget({
integrationIds,
options,
}: WidgetComponentProps<"dawarichStatistics">) {
const t = useI18n();
const { data: stats } = clientApi.widget.dawarich.getStatistics.useQuery(
{
integrationId: integrationIds[0] ?? "",
},
{ staleTime: 5 * 60 * 1000 },
);
if (!stats) return <WidgetEmptyState />;
return (
<Stack gap="md" h="100%" p="md">
{options.showTotalDistance && (
<StatItem
icon={<IconRoute size={20} />}
label={t("widget.dawarichStatistics.totalDistance")}
value={`${stats.totalDistanceKm.toLocaleString(undefined, { maximumFractionDigits: 1 })} km`}
/>
)}
{options.showTotalPoints && (
<StatItem
icon={<IconMap size={20} />}
label={t("widget.dawarichStatistics.totalPoints")}
value={stats.totalPointsTracked.toLocaleString()}
/>
)}
{options.showReverseGeocoded && (
<StatItem
icon={<IconMapPin size={20} />}
label={t("widget.dawarichStatistics.reverseGeocoded")}
value={stats.totalReverseGeocodedPoints.toLocaleString()}
/>
)}
{options.showCountries && (
<StatItem
icon={<IconWorld size={20} />}
label={t("widget.dawarichStatistics.countries")}
value={stats.totalCountriesVisited.toLocaleString()}
/>
)}
{options.showCities && (
<StatItem
icon={<IconBuildingSkyscraper size={20} />}
label={t("widget.dawarichStatistics.cities")}
value={stats.totalCitiesVisited.toLocaleString()}
/>
)}
</Stack>
);
}
interface StatItemProps {
icon: React.ReactNode;
label: string;
value: string | number;
}
function StatItem({ icon, label, value }: StatItemProps) {
return (
<Group justify="space-between" align="center" className={classes.statItem}>
<Group gap="sm" align="center">
{icon}
<Text size="sm" fw={500}>
{label}
</Text>
</Group>
<Text size="sm" fw={700} c="var(--mantine-primary-color)">
{value}
</Text>
</Group>
);
}
@@ -1,19 +0,0 @@
import { IconChartBar } from "@tabler/icons-react";
import { createWidgetDefinition } from "../definition";
import { optionsBuilder } from "../options";
export const { definition, componentLoader } = createWidgetDefinition("dawarichStatistics", {
icon: IconChartBar,
supportedIntegrations: ["dawarich"],
integrationsRequired: true,
createOptions() {
return optionsBuilder.from((factory) => ({
showTotalDistance: factory.switch({ defaultValue: true }),
showTotalPoints: factory.switch({ defaultValue: true }),
showReverseGeocoded: factory.switch({ defaultValue: true }),
showCountries: factory.switch({ defaultValue: true }),
showCities: factory.switch({ defaultValue: true }),
}));
},
}).withDynamicImport(() => import("./component"));
-4
View File
@@ -60,8 +60,6 @@ import * as beszelSystemStats from "./beszel-system-stats";
import * as video from "./video";
import * as weather from "./weather";
import * as customApi from "./custom-api";
import * as dawarichStatistics from "./dawarich-statistics";
import * as dawarichPlaces from "./dawarich-places";
export type {
WidgetDefinition,
@@ -123,8 +121,6 @@ export const widgetImports = {
beszelAlerts,
beszelSystemStats,
customApi,
dawarichStatistics,
dawarichPlaces,
} satisfies WidgetImportRecord;
export type WidgetImports = typeof widgetImports;
+15 -1
View File
@@ -1,5 +1,6 @@
"use client";
import type { MouseEvent as ReactMouseEvent } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import {
ActionIcon,
@@ -263,8 +264,21 @@ export function Notebook({ options, setOptions, isEditMode, boardId, itemId }: W
setIsEditing(handleEditToggleCallback);
}, [setIsEditing, handleEditToggleCallback]);
const handleDoubleClick = useCallback(
(event: ReactMouseEvent<HTMLDivElement>) => {
if (!canChange || isEditing) return;
// Ignore double-clicks bubbling up from interactive controls (e.g. the
// edit/save ActionIcon), which would otherwise toggle edit mode twice.
if (event.target instanceof Element && event.target.closest("button, a")) {
return;
}
setIsEditing(handleEditToggleCallback);
},
[canChange, isEditing, setIsEditing, handleEditToggleCallback],
);
return (
<Box h="100%">
<Box h="100%" onDoubleClick={handleDoubleClick}>
<RichTextEditor
p={0}
mt={0}
+2 -2
View File
@@ -247,7 +247,7 @@ export default function ReleasesWidget({ options }: WidgetComponentProps<"releas
!hasError &&
formatter.relativeTime(repository.latestReleaseAt, {
now,
style: "narrow",
style: "long",
})}
</Text>
{hasError ? (
@@ -564,7 +564,7 @@ const ExpandedDisplay = ({
<Text className="releases-repository-expanded-createdAt-date" span fw={700}>
{formatter.relativeTime(repository.createdAt, {
now,
style: "narrow",
style: "long",
})}
</Text>
</Text>
+131 -116
View File
@@ -106,8 +106,8 @@ catalogs:
specifier: ^1.61.1
version: 1.61.1
'@scalar/api-reference-react':
specifier: ^0.9.48
version: 0.9.48
specifier: ^0.9.49
version: 0.9.49
'@semantic-release/changelog':
specifier: ^6.0.3
version: 6.0.3
@@ -445,7 +445,7 @@ catalogs:
specifier: ^1.18.0
version: 1.18.0
posthog-js:
specifier: ^1.393.5
specifier: ^1.395.0
version: 1.395.0
posthog-node:
specifier: ^4.18.0
@@ -705,7 +705,7 @@ importers:
version: 0.99.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@scalar/api-reference-react':
specifier: 'catalog:'
version: 0.9.48(nprogress@0.2.0)(react@19.2.7)(tailwindcss@4.3.1)(typescript@6.0.3)(zod@4.4.3)
version: 0.9.49(nprogress@0.2.0)(react@19.2.7)(tailwindcss@4.3.1)(typescript@6.0.3)(zod@4.4.3)
'@signalwire/docusaurus-plugin-llms-txt':
specifier: ^1.2.2
version: 1.2.2(@docusaurus/core@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.15))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3))
@@ -916,7 +916,7 @@ importers:
version: 1.29.0(zod@4.4.3)
'@scalar/api-reference-react':
specifier: 'catalog:'
version: 0.9.48(nprogress@0.2.0)(react@19.2.7)(tailwindcss@4.3.1)(typescript@6.0.3)(zod@4.4.3)
version: 0.9.49(nprogress@0.2.0)(react@19.2.7)(tailwindcss@4.3.1)(typescript@6.0.3)(zod@4.4.3)
'@tabler/icons-react':
specifier: 'catalog:'
version: 3.44.0(react@19.2.7)
@@ -6707,46 +6707,50 @@ packages:
'@rvf/set-get@7.0.1':
resolution: {integrity: sha512-GkTSn9K1GrTYoTUqlUs36k6nJnzjQaFBTTEIqUYmzBcsGsoJM8xG7EAx2WLHWAA4QzFjcwWUSHQ3vM3Fbw50Tg==}
'@scalar/agent-chat@0.12.12':
resolution: {integrity: sha512-9PxyiNnSXuMDXcjPXgvFAPgfvUUsH/iS80Fpa0GGl3lO3ThsGDL2Lr+Y1UjxG1Tc2huGE/EDiUuT5osz9RwzMw==}
'@scalar/agent-chat@0.12.13':
resolution: {integrity: sha512-JPjwnOqIRb/TF+0bVNL7rg9SSjce3p0G++GF8uNvoGthixWuQxzo2pems4npZwe6Aw57icRTsZ3GBFgs9VmJvw==}
engines: {node: '>=22'}
'@scalar/api-client@3.11.0':
resolution: {integrity: sha512-6nxj8Saw/sfUM6xDxbMDfKKLDC933Vtig6m8hB0pSfXSlwXEvA5C9mUNO8ytodGPqBc/wphgFqTpfahOxp7R0w==}
'@scalar/api-client@3.12.0':
resolution: {integrity: sha512-NBZTMuqG9Qu+C0bUtMJPQEbGBLC5skXj55A/oDSzPwvTmWKV6I9Xc4wcrqVZMMidx315uC/WWyrB9WCUMgNi3Q==}
engines: {node: '>=22'}
'@scalar/api-reference-react@0.9.48':
resolution: {integrity: sha512-NmdQcmMDXIhKPIDRlA5m7cxGUAuiK42jOhaRsTT05TOcPXTF1lazcuN25o3ghevKtznIDqT+2/y/IYhbWS/UBw==}
'@scalar/api-reference-react@0.9.49':
resolution: {integrity: sha512-dtOrWZ++rFfsihNZKZ2pJ722bSkI3qp5CZKdiOY98PoJUWwPoSxT7C3NAn4NX59jQ0kuLhWDEELDpAcA779cvA==}
engines: {node: '>=22'}
peerDependencies:
react: ^18.0.0 || ^19.0.0
'@scalar/api-reference@1.61.0':
resolution: {integrity: sha512-r0U9Nqh3yV0BnU5mV2J/wDUcqwWorAUzXThHq0pbT09OzoKYO7asNB+d+bX5xhlLSvIuCMe5lAppQRfjlXzxWg==}
'@scalar/api-reference@1.62.0':
resolution: {integrity: sha512-QYPqMPfy4vyVLysHeSFZsD+717ua/gHIhYYu7UjqPzJUOVh2R+/9hINKKzvoP13lQWCQTmuzs2VEl8rQOIfDjw==}
engines: {node: '>=22'}
'@scalar/code-highlight@0.3.6':
resolution: {integrity: sha512-G/EtLF6a3IFGfIv5b7Uj5v/yXFY9+ktViVkfAkSoWJdHNBNk0S/Rb2OgRq24kp0D2TDXhLYF3E9OSHzquS7WqQ==}
'@scalar/blocks@0.1.0':
resolution: {integrity: sha512-oFnIa6dAkCt0N5yw5aqCABbP7r7YsI7ERNrGtlTDdrCteDRatMJg9y03LT+9Vhf2/K3VHWrdCZMruUdfU1uPzQ==}
engines: {node: '>=22'}
'@scalar/components@0.27.3':
resolution: {integrity: sha512-WiCMId4MFsbne1iy5N36Ej4fM+1TX6gg4DkbjWSNPTDtVWLMQ6VHdl46fxC6yOCtO59awJ7Ex6oKpbaXQsZiCA==}
'@scalar/code-highlight@0.4.0':
resolution: {integrity: sha512-ajUQ9oq5MwVHrXGze0SZVatgSXP/WbN4qgE0usYDFt1XuJ+O56TaBsCWb043r3Y9ZnHSnK9GHreVaLOxXsbf1A==}
engines: {node: '>=22'}
'@scalar/helpers@0.8.2':
resolution: {integrity: sha512-qNbqUjSB3S4Gr4A0oANcm5G1Ip+EqBxICYKhe9YzmnaBpbmW6shxqpiivApTvvuDf+uIhR3uMwWyVQbYcGLsxA==}
'@scalar/components@0.27.4':
resolution: {integrity: sha512-1sbsMYGJcWiQ+5GouI1hHCn1VrytHPhzhHhCAZTXxi1Tw4SzsPSQuACARehXca+w2p7RK9ayGRYQ+K5bX18SaQ==}
engines: {node: '>=22'}
'@scalar/helpers@0.9.0':
resolution: {integrity: sha512-M34CLRCttqC1bXthI/QSzQj0s5C6nrU2PFWf/vOT3RpycbiGDGQbqR+5RfFzpOIQvRqbHfNdcRbeiZBw+vCbkQ==}
engines: {node: '>=22'}
'@scalar/icons@0.7.3':
resolution: {integrity: sha512-5uSUvumj6yJEAZT7/MKpgrkNl76waDXUpu0kUBBFJ83GhZirBIK+Z9SktShEvJT0+rk/j9Zaer3BHoEhYwAEBQ==}
engines: {node: '>=22'}
'@scalar/json-magic@0.12.16':
resolution: {integrity: sha512-w8cDbZhHCzmIblWx92IVWoAXsbI4Fz3m++jiBANTSO1hgphF6UqEPQiCt3wnMPaxaanjMQxjS/iBk1UGXR2EGA==}
'@scalar/json-magic@0.12.17':
resolution: {integrity: sha512-Vw2nrUDIjhvMP6vxFtkiiFlabJ6SyTtfn1BsOxgnr1hIB+/rkngMguiDzl5em21VjyfFGIoADia+QWKM2hdcdA==}
engines: {node: '>=22'}
'@scalar/oas-utils@0.19.0':
resolution: {integrity: sha512-t9CBiH0we2rKlFaFJUt+dOAKYQPNhHdrrfX9Wx1Yfg9og1NM2H2BQSO7NoCaUN/+G86GNWIm+fr5R8iCM+/psw==}
'@scalar/oas-utils@0.19.1':
resolution: {integrity: sha512-FflVWiFDm0s4a+FqKJ0uYsVhDzbAtVyXSyUWsGe9BGPPQSXYYvVEVp9H2ir4xueaw4TZb1cDEZbteHLx7h7vqg==}
engines: {node: '>=22'}
'@scalar/openapi-types@0.9.1':
@@ -6757,27 +6761,27 @@ packages:
resolution: {integrity: sha512-D5b0rGLLZgmkO9mdW2j/ND1KBlH1u3RCpr87HPxv9P9ZSr6PtM5iLqFOJq0ACiaHjY2mikCrxgDmnUEhTzRpHQ==}
engines: {node: '>=22'}
'@scalar/schemas@0.6.0':
resolution: {integrity: sha512-Oacaq7RTLhe3vWOoNc8tZufqthetu1VjWkdrGMMYoVSyMBQpvDI48gwBBQO6+iySeNpk9y8NHaGxqhHeoLbDiQ==}
'@scalar/schemas@0.7.0':
resolution: {integrity: sha512-Roj0e7S29yTbGojwdx/b8C1H5arVN4h/VSPrtQ5vb9NT2ZAbrfPQVmpQYrF845rKOWX7kSzIvyBwrKNEMIAkxA==}
engines: {node: '>=22'}
'@scalar/sidebar@0.9.24':
resolution: {integrity: sha512-whfvictmBQrtdNANLzDe4cXNlhj3p1RUtSbwiSERf5TSYCQwU5J2JAG2zUPUJhNdpNUzg1wdQnE5Q3lp+MAcHQ==}
'@scalar/sidebar@0.9.25':
resolution: {integrity: sha512-RoMKTvCo801kj5J59Myqhc6YYtvjlXGNvP7qbj/TY2hfQ4XS5dH5S7t8JKJje+78MLC2Xg9OsEmCjv+gb8ta4w==}
engines: {node: '>=22'}
'@scalar/snippetz@0.9.18':
resolution: {integrity: sha512-C0lflx3Az9epM4dFsDl73w5fxgyw68ZMTr8W0fkLHJvWTM8q26jjDDrgDCDivG0SBc2UUs3xRKSjjsfC6TkZKw==}
'@scalar/snippetz@0.9.19':
resolution: {integrity: sha512-RVTZQqUxOiBhAuPywDmser1xOatVj7HCNmg+qpW8172MsMpx00lRGZ940WVAhjHgKglTkwHPaTdVX7gQeDrFUQ==}
engines: {node: '>=22'}
'@scalar/themes@0.16.1':
resolution: {integrity: sha512-yTMjqlTAYZrlJsHIuDiCVJjMJZ1UzL33C/60c88JYuvuuwSXm15TAEcLXkRrPa7u8PPz/llKGgWjMEiMhsOAGA==}
'@scalar/themes@0.16.2':
resolution: {integrity: sha512-4mAn7z2W5/ASi1OF06dqf04xjxTHnMzESC2E+qVMukJsEsV4kDlYSIfm/g5hwWxhqWsBMjorF9Dtlqd0ycJ92g==}
engines: {node: '>=22'}
'@scalar/typebox@0.1.3':
resolution: {integrity: sha512-lU055AUccECZMIfGA0z/C1StYmboAYIPJLDFBzOO81yXBi35Pxdq+I4fWX6iUZ8qcoHneiLGk9jAUM1rA93iEg==}
'@scalar/types@0.15.0':
resolution: {integrity: sha512-Rn9XAFrnQL4cAINCyNlPdvSbd3+Cvqgv2SBX1J8WgtZLxqC32+8Ptmns51Lvn6zKeiX+mFHk1UdvIWmqiG1xRg==}
'@scalar/types@0.16.0':
resolution: {integrity: sha512-26OSrvZjKWZ7F236wWmJajBGDVFJuvXFJqKPFqbt/PxlgZKXQXfJsZorASRQmdNogT576nxYalQ7oaYWEnQwfw==}
engines: {node: '>=22'}
'@scalar/use-codemirror@0.14.12':
@@ -6796,8 +6800,8 @@ packages:
resolution: {integrity: sha512-tpmmG+/xRE2Kn9RpflU3AIyZv08v10+E1ZrJCx7z6+/91zHVxy0M73kC1LT4/8PbYNt85ywyC8+n+D99JdMcGA==}
engines: {node: '>=20'}
'@scalar/workspace-store@0.54.5':
resolution: {integrity: sha512-MMaELRxKNwJ1dxbDcFNRwh229jAX5DQhkQpHgDSbr+oAt6AqFf2BTr9Fr/8kbLIJK+L4P2C8rANE8xrnqjKGLg==}
'@scalar/workspace-store@0.55.0':
resolution: {integrity: sha512-4EkaLfONW8qu2vigJopbjg9ahE/Z9Q+IInnVoUTZfxLp4rb2Z3ztx8LmQOym8DPpv62BvuMtJcNdQOnQ6tVLHA==}
engines: {node: '>=22'}
'@schummar/icu-type-parser@1.21.5':
@@ -12013,11 +12017,6 @@ packages:
resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
nanoid@5.1.15:
resolution: {integrity: sha512-kBg3RpGtIe+RpTbyXwoI6pk5yD7KUiI3sygUqgeBMRst42KmhB4RZC7eiO9Wa1HIpaCCtpE2DJ6OI4Wi5ebwFw==}
engines: {node: ^18 || >=20}
hasBin: true
nanoid@5.1.16:
resolution: {integrity: sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ==}
engines: {node: ^18 || >=20}
@@ -20120,21 +20119,21 @@ snapshots:
'@rvf/set-get@7.0.1': {}
'@scalar/agent-chat@0.12.12(nprogress@0.2.0)(tailwindcss@4.3.1)(typescript@6.0.3)(zod@4.4.3)':
'@scalar/agent-chat@0.12.13(nprogress@0.2.0)(tailwindcss@4.3.1)(typescript@6.0.3)(zod@4.4.3)':
dependencies:
'@ai-sdk/vue': 3.0.33(vue@3.5.35(typescript@6.0.3))(zod@4.4.3)
'@scalar/api-client': 3.11.0(nprogress@0.2.0)(tailwindcss@4.3.1)(typescript@6.0.3)
'@scalar/components': 0.27.3(tailwindcss@4.3.1)(typescript@6.0.3)
'@scalar/helpers': 0.8.2
'@scalar/api-client': 3.12.0(nprogress@0.2.0)(tailwindcss@4.3.1)(typescript@6.0.3)
'@scalar/components': 0.27.4(tailwindcss@4.3.1)(typescript@6.0.3)
'@scalar/helpers': 0.9.0
'@scalar/icons': 0.7.3(typescript@6.0.3)
'@scalar/json-magic': 0.12.16
'@scalar/json-magic': 0.12.17
'@scalar/openapi-types': 0.9.1
'@scalar/schemas': 0.6.0
'@scalar/themes': 0.16.1
'@scalar/types': 0.15.0
'@scalar/schemas': 0.7.0
'@scalar/themes': 0.16.2
'@scalar/types': 0.16.0
'@scalar/use-toasts': 0.10.2(typescript@6.0.3)
'@scalar/validation': 0.6.0
'@scalar/workspace-store': 0.54.5(typescript@6.0.3)
'@scalar/workspace-store': 0.55.0(typescript@6.0.3)
'@vueuse/core': 13.9.0(vue@3.5.35(typescript@6.0.3))
ai: 6.0.33(zod@4.4.3)
js-base64: 3.7.8
@@ -20158,33 +20157,32 @@ snapshots:
- universal-cookie
- zod
'@scalar/api-client@3.11.0(nprogress@0.2.0)(tailwindcss@4.3.1)(typescript@6.0.3)':
'@scalar/api-client@3.12.0(nprogress@0.2.0)(tailwindcss@4.3.1)(typescript@6.0.3)':
dependencies:
'@headlessui/tailwindcss': 0.2.2(tailwindcss@4.3.1)
'@headlessui/vue': 1.7.23(vue@3.5.35(typescript@6.0.3))
'@scalar/components': 0.27.3(tailwindcss@4.3.1)(typescript@6.0.3)
'@scalar/helpers': 0.8.2
'@scalar/blocks': 0.1.0(tailwindcss@4.3.1)(typescript@6.0.3)
'@scalar/components': 0.27.4(tailwindcss@4.3.1)(typescript@6.0.3)
'@scalar/helpers': 0.9.0
'@scalar/icons': 0.7.3(typescript@6.0.3)
'@scalar/json-magic': 0.12.16
'@scalar/oas-utils': 0.19.0(typescript@6.0.3)
'@scalar/oas-utils': 0.19.1(typescript@6.0.3)
'@scalar/openapi-types': 0.9.1
'@scalar/sidebar': 0.9.24(tailwindcss@4.3.1)(typescript@6.0.3)
'@scalar/snippetz': 0.9.18
'@scalar/themes': 0.16.1
'@scalar/sidebar': 0.9.25(tailwindcss@4.3.1)(typescript@6.0.3)
'@scalar/snippetz': 0.9.19
'@scalar/themes': 0.16.2
'@scalar/typebox': 0.1.3
'@scalar/types': 0.15.0
'@scalar/types': 0.16.0
'@scalar/use-codemirror': 0.14.12(typescript@6.0.3)
'@scalar/use-hooks': 0.4.7(typescript@6.0.3)
'@scalar/use-toasts': 0.10.2(typescript@6.0.3)
'@scalar/workspace-store': 0.54.5(typescript@6.0.3)
'@types/har-format': 1.2.16
'@scalar/workspace-store': 0.55.0(typescript@6.0.3)
'@vueuse/core': 13.9.0(vue@3.5.35(typescript@6.0.3))
'@vueuse/integrations': 13.9.0(focus-trap@7.8.0)(fuse.js@7.4.2)(nprogress@0.2.0)(vue@3.5.35(typescript@6.0.3))
focus-trap: 7.8.0
fuse.js: 7.4.2
js-base64: 3.7.8
jsonc-parser: 3.3.1
nanoid: 5.1.15
nanoid: 5.1.16
pretty-ms: 9.3.0
radix-vue: 1.9.17(vue@3.5.35(typescript@6.0.3))
set-cookie-parser: 3.1.0
@@ -20207,10 +20205,10 @@ snapshots:
- typescript
- universal-cookie
'@scalar/api-reference-react@0.9.48(nprogress@0.2.0)(react@19.2.7)(tailwindcss@4.3.1)(typescript@6.0.3)(zod@4.4.3)':
'@scalar/api-reference-react@0.9.49(nprogress@0.2.0)(react@19.2.7)(tailwindcss@4.3.1)(typescript@6.0.3)(zod@4.4.3)':
dependencies:
'@scalar/api-reference': 1.61.0(nprogress@0.2.0)(tailwindcss@4.3.1)(typescript@6.0.3)(zod@4.4.3)
'@scalar/types': 0.15.0
'@scalar/api-reference': 1.62.0(nprogress@0.2.0)(tailwindcss@4.3.1)(typescript@6.0.3)(zod@4.4.3)
'@scalar/types': 0.16.0
react: 19.2.7
transitivePeerDependencies:
- '@vue/composition-api'
@@ -20229,30 +20227,31 @@ snapshots:
- universal-cookie
- zod
'@scalar/api-reference@1.61.0(nprogress@0.2.0)(tailwindcss@4.3.1)(typescript@6.0.3)(zod@4.4.3)':
'@scalar/api-reference@1.62.0(nprogress@0.2.0)(tailwindcss@4.3.1)(typescript@6.0.3)(zod@4.4.3)':
dependencies:
'@headlessui/vue': 1.7.23(vue@3.5.35(typescript@6.0.3))
'@scalar/agent-chat': 0.12.12(nprogress@0.2.0)(tailwindcss@4.3.1)(typescript@6.0.3)(zod@4.4.3)
'@scalar/api-client': 3.11.0(nprogress@0.2.0)(tailwindcss@4.3.1)(typescript@6.0.3)
'@scalar/code-highlight': 0.3.6
'@scalar/components': 0.27.3(tailwindcss@4.3.1)(typescript@6.0.3)
'@scalar/helpers': 0.8.2
'@scalar/agent-chat': 0.12.13(nprogress@0.2.0)(tailwindcss@4.3.1)(typescript@6.0.3)(zod@4.4.3)
'@scalar/api-client': 3.12.0(nprogress@0.2.0)(tailwindcss@4.3.1)(typescript@6.0.3)
'@scalar/blocks': 0.1.0(tailwindcss@4.3.1)(typescript@6.0.3)
'@scalar/code-highlight': 0.4.0
'@scalar/components': 0.27.4(tailwindcss@4.3.1)(typescript@6.0.3)
'@scalar/helpers': 0.9.0
'@scalar/icons': 0.7.3(typescript@6.0.3)
'@scalar/oas-utils': 0.19.0(typescript@6.0.3)
'@scalar/schemas': 0.6.0
'@scalar/sidebar': 0.9.24(tailwindcss@4.3.1)(typescript@6.0.3)
'@scalar/snippetz': 0.9.18
'@scalar/themes': 0.16.1
'@scalar/types': 0.15.0
'@scalar/oas-utils': 0.19.1(typescript@6.0.3)
'@scalar/schemas': 0.7.0
'@scalar/sidebar': 0.9.25(tailwindcss@4.3.1)(typescript@6.0.3)
'@scalar/snippetz': 0.9.19
'@scalar/themes': 0.16.2
'@scalar/types': 0.16.0
'@scalar/use-hooks': 0.4.7(typescript@6.0.3)
'@scalar/use-toasts': 0.10.2(typescript@6.0.3)
'@scalar/validation': 0.6.0
'@scalar/workspace-store': 0.54.5(typescript@6.0.3)
'@scalar/workspace-store': 0.55.0(typescript@6.0.3)
'@unhead/vue': 2.1.15(vue@3.5.35(typescript@6.0.3))
'@vueuse/core': 13.9.0(vue@3.5.35(typescript@6.0.3))
fuse.js: 7.4.2
microdiff: 1.5.0
nanoid: 5.1.15
nanoid: 5.1.16
vue: 3.5.35(typescript@6.0.3)
yaml: 2.9.0
transitivePeerDependencies:
@@ -20272,7 +20271,25 @@ snapshots:
- universal-cookie
- zod
'@scalar/code-highlight@0.3.6':
'@scalar/blocks@0.1.0(tailwindcss@4.3.1)(typescript@6.0.3)':
dependencies:
'@scalar/components': 0.27.4(tailwindcss@4.3.1)(typescript@6.0.3)
'@scalar/helpers': 0.9.0
'@scalar/icons': 0.7.3(typescript@6.0.3)
'@scalar/snippetz': 0.9.19
'@scalar/themes': 0.16.2
'@scalar/types': 0.16.0
'@scalar/workspace-store': 0.55.0(typescript@6.0.3)
'@types/har-format': 1.2.16
js-base64: 3.7.8
vue: 3.5.35(typescript@6.0.3)
transitivePeerDependencies:
- '@vue/composition-api'
- supports-color
- tailwindcss
- typescript
'@scalar/code-highlight@0.4.0':
dependencies:
hast-util-to-text: 4.0.2
highlight.js: 11.11.1
@@ -20292,16 +20309,16 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@scalar/components@0.27.3(tailwindcss@4.3.1)(typescript@6.0.3)':
'@scalar/components@0.27.4(tailwindcss@4.3.1)(typescript@6.0.3)':
dependencies:
'@floating-ui/utils': 0.2.10
'@floating-ui/vue': 1.1.9(vue@3.5.35(typescript@6.0.3))
'@headlessui/tailwindcss': 0.2.2(tailwindcss@4.3.1)
'@headlessui/vue': 1.7.23(vue@3.5.35(typescript@6.0.3))
'@scalar/code-highlight': 0.3.6
'@scalar/helpers': 0.8.2
'@scalar/code-highlight': 0.4.0
'@scalar/helpers': 0.9.0
'@scalar/icons': 0.7.3(typescript@6.0.3)
'@scalar/themes': 0.16.1
'@scalar/themes': 0.16.2
'@scalar/use-hooks': 0.4.7(typescript@6.0.3)
'@vueuse/core': 13.9.0(vue@3.5.35(typescript@6.0.3))
cva: 1.0.0-beta.4(typescript@6.0.3)
@@ -20314,7 +20331,7 @@ snapshots:
- tailwindcss
- typescript
'@scalar/helpers@0.8.2': {}
'@scalar/helpers@0.9.0': {}
'@scalar/icons@0.7.3(typescript@6.0.3)':
dependencies:
@@ -20325,18 +20342,18 @@ snapshots:
transitivePeerDependencies:
- typescript
'@scalar/json-magic@0.12.16':
'@scalar/json-magic@0.12.17':
dependencies:
'@scalar/helpers': 0.8.2
'@scalar/helpers': 0.9.0
pathe: 2.0.3
yaml: 2.9.0
'@scalar/oas-utils@0.19.0(typescript@6.0.3)':
'@scalar/oas-utils@0.19.1(typescript@6.0.3)':
dependencies:
'@scalar/helpers': 0.8.2
'@scalar/themes': 0.16.1
'@scalar/types': 0.15.0
'@scalar/workspace-store': 0.54.5(typescript@6.0.3)
'@scalar/helpers': 0.9.0
'@scalar/themes': 0.16.2
'@scalar/types': 0.16.0
'@scalar/workspace-store': 0.55.0(typescript@6.0.3)
flatted: 3.4.2
vue: 3.5.35(typescript@6.0.3)
yaml: 2.9.0
@@ -20349,19 +20366,19 @@ snapshots:
dependencies:
'@scalar/openapi-types': 0.9.1
'@scalar/schemas@0.6.0':
'@scalar/schemas@0.7.0':
dependencies:
'@scalar/helpers': 0.8.2
'@scalar/helpers': 0.9.0
'@scalar/validation': 0.6.0
'@scalar/sidebar@0.9.24(tailwindcss@4.3.1)(typescript@6.0.3)':
'@scalar/sidebar@0.9.25(tailwindcss@4.3.1)(typescript@6.0.3)':
dependencies:
'@scalar/components': 0.27.3(tailwindcss@4.3.1)(typescript@6.0.3)
'@scalar/helpers': 0.8.2
'@scalar/components': 0.27.4(tailwindcss@4.3.1)(typescript@6.0.3)
'@scalar/helpers': 0.9.0
'@scalar/icons': 0.7.3(typescript@6.0.3)
'@scalar/themes': 0.16.1
'@scalar/themes': 0.16.2
'@scalar/use-hooks': 0.4.7(typescript@6.0.3)
'@scalar/workspace-store': 0.54.5(typescript@6.0.3)
'@scalar/workspace-store': 0.55.0(typescript@6.0.3)
vue: 3.5.35(typescript@6.0.3)
transitivePeerDependencies:
- '@vue/composition-api'
@@ -20369,24 +20386,24 @@ snapshots:
- tailwindcss
- typescript
'@scalar/snippetz@0.9.18':
'@scalar/snippetz@0.9.19':
dependencies:
'@scalar/helpers': 0.8.2
'@scalar/types': 0.15.0
'@scalar/helpers': 0.9.0
'@scalar/types': 0.16.0
js-base64: 3.7.8
stringify-object: 6.0.0
'@scalar/themes@0.16.1':
'@scalar/themes@0.16.2':
dependencies:
nanoid: 5.1.15
nanoid: 5.1.16
'@scalar/typebox@0.1.3': {}
'@scalar/types@0.15.0':
'@scalar/types@0.16.0':
dependencies:
'@scalar/helpers': 0.8.2
nanoid: 5.1.15
type-fest: 5.6.0
'@scalar/helpers': 0.9.0
nanoid: 5.1.16
type-fest: 5.7.0
zod: 4.4.3
'@scalar/use-codemirror@0.14.12(typescript@6.0.3)':
@@ -20429,18 +20446,18 @@ snapshots:
'@scalar/validation@0.6.0': {}
'@scalar/workspace-store@0.54.5(typescript@6.0.3)':
'@scalar/workspace-store@0.55.0(typescript@6.0.3)':
dependencies:
'@scalar/helpers': 0.8.2
'@scalar/json-magic': 0.12.16
'@scalar/helpers': 0.9.0
'@scalar/json-magic': 0.12.17
'@scalar/openapi-upgrader': 0.2.9
'@scalar/schemas': 0.6.0
'@scalar/snippetz': 0.9.18
'@scalar/schemas': 0.7.0
'@scalar/snippetz': 0.9.19
'@scalar/typebox': 0.1.3
'@scalar/types': 0.15.0
'@scalar/types': 0.16.0
'@scalar/validation': 0.6.0
js-base64: 3.7.8
type-fest: 5.6.0
type-fest: 5.7.0
vue: 3.5.35(typescript@6.0.3)
yaml: 2.9.0
transitivePeerDependencies:
@@ -26342,8 +26359,6 @@ snapshots:
nanoid@3.3.12: {}
nanoid@5.1.15: {}
nanoid@5.1.16: {}
nanostores@1.3.0: {}
+2 -2
View File
@@ -88,11 +88,11 @@ catalog:
"@types/prismjs": ^1.26.6
"@types/react": 19.2.17
"@types/react-dom": 19.2.3
"@scalar/api-reference-react": ^0.9.48
"@scalar/api-reference-react": ^0.9.49
"@types/video.js": ^7.3.58
"@types/ws": ^8.18.1
"@types/xml2js": ^0.4.14
posthog-js: ^1.393.5
posthog-js: ^1.395.0
posthog-node: ^4.18.0
"@vitejs/plugin-react": ^6.0.3
"@vitest/coverage-v8": ^4.1.9