diff --git a/apps/tasks/src/undici-log-agent-override.ts b/apps/tasks/src/undici-log-agent-override.ts index 79d443e860..21465e9926 100644 --- a/apps/tasks/src/undici-log-agent-override.ts +++ b/apps/tasks/src/undici-log-agent-override.ts @@ -22,7 +22,7 @@ class LoggingAgent extends Agent { url.searchParams.set(key, "REDACTED"); }); - logger.info( + logger.debug( `Dispatching request ${url.toString().replaceAll("=&", "&")} (${Object.keys(options.headers as object).length} headers)`, ); return super.dispatch(options, handler); diff --git a/packages/api/src/router/widgets/hardware-usage.ts b/packages/api/src/router/widgets/hardware-usage.ts new file mode 100644 index 0000000000..a2f38b5857 --- /dev/null +++ b/packages/api/src/router/widgets/hardware-usage.ts @@ -0,0 +1,53 @@ +import { observable } from "@trpc/server/observable"; + +import type { CpuLoad, MemoryLoad, NetworkLoad, ServerInfo } from "@homarr/integrations"; +import { createItemAndIntegrationChannel } from "@homarr/redis"; + +import { createOneIntegrationMiddleware } from "../../middlewares/integration"; +import { createTRPCRouter, publicProcedure } from "../../trpc"; + +export const hardwareUsageRouter = createTRPCRouter({ + getServerInfo: publicProcedure + .unstable_concat(createOneIntegrationMiddleware("query", "getDashDot")) + .query(async ({ ctx }) => { + const channel = createItemAndIntegrationChannel<{ + info: ServerInfo; + }>("hardwareUsage", ctx.integration.id); + const data = await channel.getAsync(); + return { + info: data?.data.info ?? ({} as ServerInfo), + }; + }), + getHardwareInformationHistory: publicProcedure + .unstable_concat(createOneIntegrationMiddleware("query", "getDashDot")) + .query(async ({ ctx }) => { + const channel = createItemAndIntegrationChannel<{ + cpuLoad: CpuLoad; + memoryLoad: MemoryLoad; + networkLoad: NetworkLoad; + }>("hardwareUsage", ctx.integration.id); + const data = await channel.getAsync(); + return { + cpuLoad: data?.data.cpuLoad ?? ({} as CpuLoad), + memoryLoad: data?.data.memoryLoad ?? ({} as MemoryLoad), + networkLoad: data?.data.networkLoad ?? ({} as NetworkLoad), + }; + }), + subscribeCpu: publicProcedure + .unstable_concat(createOneIntegrationMiddleware("query", "getDashDot")) + .subscription(({ ctx }) => { + return observable<{ cpuLoad: CpuLoad; memoryLoad: MemoryLoad; networkLoad: NetworkLoad }>((emit) => { + const channel = createItemAndIntegrationChannel<{ + cpuLoad: CpuLoad; + memoryLoad: MemoryLoad; + networkLoad: NetworkLoad; + }>("hardwareUsage", ctx.integration.id); + const unsubscribe = channel.subscribe((data) => { + emit.next(data); + }); + return () => { + unsubscribe(); + }; + }); + }), +}); diff --git a/packages/api/src/router/widgets/index.ts b/packages/api/src/router/widgets/index.ts index 7f030a4bb9..a475e9c120 100644 --- a/packages/api/src/router/widgets/index.ts +++ b/packages/api/src/router/widgets/index.ts @@ -3,6 +3,7 @@ import { appRouter } from "./app"; import { calendarRouter } from "./calendar"; import { dnsHoleRouter } from "./dns-hole"; import { downloadsRouter } from "./downloads"; +import { hardwareUsageRouter } from "./hardware-usage"; import { healthMonitoringRouter } from "./health-monitoring"; import { indexerManagerRouter } from "./indexer-manager"; import { mediaRequestsRouter } from "./media-requests"; @@ -20,6 +21,7 @@ export const widgetRouter = createTRPCRouter({ smartHome: smartHomeRouter, mediaServer: mediaServerRouter, calendar: calendarRouter, + hardwareUsage: hardwareUsageRouter, downloads: downloadsRouter, mediaRequests: mediaRequestsRouter, rssFeed: rssFeedRouter, diff --git a/packages/cron-jobs-core/src/creator.ts b/packages/cron-jobs-core/src/creator.ts index 62607377ee..bf2267bf81 100644 --- a/packages/cron-jobs-core/src/creator.ts +++ b/packages/cron-jobs-core/src/creator.ts @@ -34,7 +34,7 @@ const createCallback = { + const itemsForIntegration = await db.query.items.findMany({ + where: eq(items.kind, "hardwareUsage"), + with: { + integrations: { + with: { + integration: { + with: { + secrets: { + columns: { + kind: true, + value: true, + }, + }, + }, + }, + }, + }, + }, + }); + + for (const itemForIntegration of itemsForIntegration) { + for (const integration of itemForIntegration.integrations) { + const dashDotIntegration = new DashDotIntegration({ + ...integration.integration, + decryptedSecrets: integration.integration.secrets.map((secret) => ({ + ...secret, + value: decryptSecret(secret.value), + })), + }); + + const info = await dashDotIntegration.getInfoAsync(); + const cpuLoad = await dashDotIntegration.getCurrentCpuLoadAsync(); + const memoryLoad = await dashDotIntegration.getCurrentMemoryLoadAsync(); + const networkLoad = await dashDotIntegration.getCurrentNetworkLoadAsync(); + + const cache = createItemAndIntegrationChannel<{ + info: ServerInfo; + cpuLoad: CpuLoad; + memoryLoad: MemoryLoad; + networkLoad: NetworkLoad; + }>("hardwareUsage", integration.integrationId); + await cache.setAsync({ + memoryLoad, + networkLoad, + cpuLoad, + info, + }); + await cache.publishAndUpdateLastStateAsync({ + cpuLoad, + networkLoad, + memoryLoad, + info, + }); + } + } +}); diff --git a/packages/definitions/src/integration.ts b/packages/definitions/src/integration.ts index 8603f148a9..0712aa78e2 100644 --- a/packages/definitions/src/integration.ts +++ b/packages/definitions/src/integration.ts @@ -12,7 +12,7 @@ export const integrationSecretKinds = objectKeys(integrationSecretKindObject); interface integrationDefinition { name: string; iconUrl: string; - secretKinds: AtLeastOneOf; // at least one secret kind set is required + secretKinds: IntegrationSecretKind[][]; category: AtLeastOneOf; } @@ -119,6 +119,12 @@ export const integrationDefs = { iconUrl: "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/home-assistant.png", category: ["smartHomeServer"], }, + getDashDot: { + name: "Dash.", + secretKinds: [[]], + category: ["hardware"], + iconUrl: "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/dashdot.png", + }, openmediavault: { name: "OpenMediaVault", secretKinds: [["username", "password"]], @@ -175,4 +181,5 @@ export type IntegrationCategory = | "torrent" | "smartHomeServer" | "indexerManager" - | "healthMonitoring"; + | "healthMonitoring" + | "hardware"; diff --git a/packages/definitions/src/widget.ts b/packages/definitions/src/widget.ts index 637f20e3fe..b5298da23c 100644 --- a/packages/definitions/src/widget.ts +++ b/packages/definitions/src/widget.ts @@ -11,6 +11,7 @@ export const widgetKinds = [ "smartHome-executeAutomation", "mediaServer", "calendar", + "hardwareUsage", "downloads", "mediaRequests-requestList", "mediaRequests-requestStats", diff --git a/packages/integrations/src/base/creator.ts b/packages/integrations/src/base/creator.ts index 5894aea9eb..504765a2ae 100644 --- a/packages/integrations/src/base/creator.ts +++ b/packages/integrations/src/base/creator.ts @@ -4,6 +4,7 @@ import type { Integration as DbIntegration } from "@homarr/db/schema/sqlite"; import type { IntegrationKind, IntegrationSecretKind } from "@homarr/definitions"; import { AdGuardHomeIntegration } from "../adguard-home/adguard-home-integration"; +import { DashDotIntegration } from "../dashdot/dashdot-integration"; import { DelugeIntegration } from "../download-client/deluge/deluge-integration"; import { NzbGetIntegration } from "../download-client/nzbget/nzbget-integration"; import { QBitTorrentIntegration } from "../download-client/qbittorrent/qbittorrent-integration"; @@ -63,5 +64,6 @@ export const integrationCreators = { jellyseerr: JellyseerrIntegration, overseerr: OverseerrIntegration, prowlarr: ProwlarrIntegration, + getDashDot: DashDotIntegration, openmediavault: OpenMediaVaultIntegration, } satisfies Partial Integration>>; diff --git a/packages/integrations/src/base/integration.ts b/packages/integrations/src/base/integration.ts index 69b346e231..69e000f944 100644 --- a/packages/integrations/src/base/integration.ts +++ b/packages/integrations/src/base/integration.ts @@ -29,6 +29,14 @@ export abstract class Integration { return secret.value; } + protected appendPathToUrlWithEndingSlash(basename: string, path: string) { + if (basename.endsWith("/")) { + return `${basename}${path}`; + } + + return `${basename}/${path}`; + } + /** * Test the connection to the integration * @throws {IntegrationTestConnectionError} if the connection fails diff --git a/packages/integrations/src/dashdot/dashdot-integration.ts b/packages/integrations/src/dashdot/dashdot-integration.ts new file mode 100644 index 0000000000..8d28fa8c2e --- /dev/null +++ b/packages/integrations/src/dashdot/dashdot-integration.ts @@ -0,0 +1,93 @@ +import { Integration } from "../base/integration"; +import type { CpuLoad } from "../interfaces/hardware-usage/cpu-load"; +import type { MemoryLoad } from "../interfaces/hardware-usage/memory-load"; +import type { NetworkLoad } from "../interfaces/hardware-usage/network-load"; +import type { ServerInfo } from "../interfaces/hardware-usage/server-info"; + +export class DashDotIntegration extends Integration { + public async testConnectionAsync(): Promise { + const response = await fetch(this.appendPathToUrlWithEndingSlash(this.integration.url, "info")); + await response.json(); + } + + public async getInfoAsync(): Promise { + const infoResponse = await fetch(this.appendPathToUrlWithEndingSlash(this.integration.url, "info")); + const serverInfo = (await infoResponse.json()) as InternalServerInfo; + return { + maxAvailableMemoryBytes: serverInfo.ram.size, + }; + } + + public async getCurrentCpuLoadAsync(): Promise { + const cpu = await fetch(this.appendPathToUrlWithEndingSlash(this.integration.url, "load/cpu")); + const data = (await cpu.json()) as CpuLoadApi[]; + return { + sumLoad: data.reduce((acc, current) => acc + current.load, 0) / data.length, + }; + } + + public async getCurrentMemoryLoadAsync(): Promise { + const memoryLoad = await fetch(this.appendPathToUrlWithEndingSlash(this.integration.url, "load/ram")); + const data = (await memoryLoad.json()) as MemoryLoadApi; + return { + loadInBytes: data.load, + }; + } + + public async getCurrentNetworkLoadAsync(): Promise { + const memoryLoad = await fetch(this.appendPathToUrlWithEndingSlash(this.integration.url, "load/network")); + const data = (await memoryLoad.json()) as NetworkLoadApi; + return { + down: data.down, + up: data.up, + }; + } +} + +/** + * CPU load per core + */ +interface CpuLoadApi { + load: number; +} + +interface MemoryLoadApi { + load: number; +} + +interface NetworkLoadApi { + up: number; + down: number; +} + +interface InternalServerInfo { + ram: { + /** + * Available memory in bytes + */ + size: number; + }; + storage: { + /** + * Size of storage in bytes + */ + size: number; + disks: { + /** + * Name of the device, e.g. sda + */ + device: string; + + /** + * Brand name of the device + */ + brand: string; + + /** + * Type of the device. + * See option "physical" of https://systeminformation.io/filesystem.html + */ + type: string; + }[]; + }[]; +} diff --git a/packages/integrations/src/index.ts b/packages/integrations/src/index.ts index 8a0556c6ba..c857abb6ad 100644 --- a/packages/integrations/src/index.ts +++ b/packages/integrations/src/index.ts @@ -8,9 +8,10 @@ export { TransmissionIntegration } from "./download-client/transmission/transmis export { HomeAssistantIntegration } from "./homeassistant/homeassistant-integration"; export { DownloadClientIntegration } from "./interfaces/downloads/download-client-integration"; export { JellyfinIntegration } from "./jellyfin/jellyfin-integration"; +export { SonarrIntegration } from "./media-organizer/sonarr/sonarr-integration"; +export { DashDotIntegration } from "./dashdot/dashdot-integration"; export { JellyseerrIntegration } from "./jellyseerr/jellyseerr-integration"; export { RadarrIntegration } from "./media-organizer/radarr/radarr-integration"; -export { SonarrIntegration } from "./media-organizer/sonarr/sonarr-integration"; export { OpenMediaVaultIntegration } from "./openmediavault/openmediavault-integration"; export { OverseerrIntegration } from "./overseerr/overseerr-integration"; export { PiHoleIntegration } from "./pi-hole/pi-hole-integration"; @@ -22,6 +23,10 @@ export type { IntegrationInput } from "./base/integration"; export type { DownloadClientJobsAndStatus } from "./interfaces/downloads/download-client-data"; export type { ExtendedDownloadClientItem } from "./interfaces/downloads/download-client-items"; export type { ExtendedClientStatus } from "./interfaces/downloads/download-client-status"; +export type { CpuLoad } from "./interfaces/hardware-usage/cpu-load"; +export type { MemoryLoad } from "./interfaces/hardware-usage/memory-load"; +export type { NetworkLoad } from "./interfaces/hardware-usage/network-load"; +export type { ServerInfo } from "./interfaces/hardware-usage/server-info"; export type { HealthMonitoring } from "./interfaces/health-monitoring/healt-monitoring"; export { MediaRequestStatus } from "./interfaces/media-requests/media-request"; export type { MediaRequestList, MediaRequestStats } from "./interfaces/media-requests/media-request"; diff --git a/packages/integrations/src/interfaces/hardware-usage/cpu-load.ts b/packages/integrations/src/interfaces/hardware-usage/cpu-load.ts new file mode 100644 index 0000000000..969083c8c4 --- /dev/null +++ b/packages/integrations/src/interfaces/hardware-usage/cpu-load.ts @@ -0,0 +1,3 @@ +export interface CpuLoad { + sumLoad: number; +} diff --git a/packages/integrations/src/interfaces/hardware-usage/memory-load.ts b/packages/integrations/src/interfaces/hardware-usage/memory-load.ts new file mode 100644 index 0000000000..f8e460e3c6 --- /dev/null +++ b/packages/integrations/src/interfaces/hardware-usage/memory-load.ts @@ -0,0 +1,3 @@ +export interface MemoryLoad { + loadInBytes: number; +} diff --git a/packages/integrations/src/interfaces/hardware-usage/network-load.ts b/packages/integrations/src/interfaces/hardware-usage/network-load.ts new file mode 100644 index 0000000000..caf0ebf5fe --- /dev/null +++ b/packages/integrations/src/interfaces/hardware-usage/network-load.ts @@ -0,0 +1,4 @@ +export interface NetworkLoad { + up: number; + down: number; +} diff --git a/packages/integrations/src/interfaces/hardware-usage/server-info.ts b/packages/integrations/src/interfaces/hardware-usage/server-info.ts new file mode 100644 index 0000000000..b5864b55ba --- /dev/null +++ b/packages/integrations/src/interfaces/hardware-usage/server-info.ts @@ -0,0 +1,3 @@ +export interface ServerInfo { + maxAvailableMemoryBytes: number; +} diff --git a/packages/old-import/src/widgets/definitions/index.ts b/packages/old-import/src/widgets/definitions/index.ts index e74c5c98c7..425ef60f70 100644 --- a/packages/old-import/src/widgets/definitions/index.ts +++ b/packages/old-import/src/widgets/definitions/index.ts @@ -67,6 +67,7 @@ export const widgetKindMapping = { "mediaRequests-requestStats": "media-requests-stats", indexerManager: "indexer-manager", healthMonitoring: "health-monitoring", + hardwareUsage: "dashdot", } satisfies Record; // Use null for widgets that did not exist in oldmarr // TODO: revert assignment so that only old widgets are needed in the object, diff --git a/packages/old-import/src/widgets/options.ts b/packages/old-import/src/widgets/options.ts index badf319816..faeb02cab9 100644 --- a/packages/old-import/src/widgets/options.ts +++ b/packages/old-import/src/widgets/options.ts @@ -112,6 +112,7 @@ const optionMapping: OptionMapping = { fileSystem: (oldOptions) => oldOptions.fileSystem, }, app: null, + hardwareUsage: {}, }; /** diff --git a/packages/translation/src/lang/en.ts b/packages/translation/src/lang/en.ts index 0a87bbb6ba..799aacd663 100644 --- a/packages/translation/src/lang/en.ts +++ b/packages/translation/src/lang/en.ts @@ -1248,6 +1248,11 @@ export default { description: "Show the current streams on your media servers", option: {}, }, + hardwareUsage: { + name: "Hardware Usage", + description: "Monitor hardware usage (eg. CPU, RAM, network) in real time", + option: {}, + }, downloads: { name: "Download Client", description: "Allows you to view and manage your Downloads from both Torrent and Usenet clients.", @@ -1968,6 +1973,9 @@ export default { rssFeeds: { label: "RSS feeds", }, + hardwareUsage: { + label: "Hardware Usage", + }, indexerManager: { label: "Indexer Manager", }, diff --git a/packages/widgets/package.json b/packages/widgets/package.json index d87eb102ac..a278704609 100644 --- a/packages/widgets/package.json +++ b/packages/widgets/package.json @@ -40,6 +40,9 @@ "@homarr/validation": "workspace:^0.1.0", "@mantine/core": "^7.13.4", "@mantine/hooks": "^7.13.4", + "@nivo/bar": "^0.87.0", + "@nivo/core": "^0.87.0", + "@nivo/line": "^0.87.0", "@tabler/icons-react": "^3.21.0", "@tiptap/extension-color": "2.9.1", "@tiptap/extension-highlight": "2.9.1", diff --git a/packages/widgets/src/hardware-usage/component.tsx b/packages/widgets/src/hardware-usage/component.tsx new file mode 100644 index 0000000000..ac6037e413 --- /dev/null +++ b/packages/widgets/src/hardware-usage/component.tsx @@ -0,0 +1,62 @@ +import { Stack } from "@mantine/core"; +import { useListState } from "@mantine/hooks"; + +import { clientApi } from "@homarr/api/client"; +import type { CpuLoad, MemoryLoad, NetworkLoad } from "@homarr/integrations"; + +import type { WidgetComponentProps } from "../definition"; +import { NoIntegrationSelectedError } from "../errors"; +import { CpuGraph } from "./graphs/cpu-graph"; +import { MemoryGraph } from "./graphs/memory-graph"; +import { NetworkGraph } from "./graphs/network-graph"; + +export default function HardwareUsageWidget({ integrationIds }: WidgetComponentProps<"hardwareUsage">) { + const [hardwareUsageHistory] = clientApi.widget.hardwareUsage.getHardwareInformationHistory.useSuspenseQuery( + { + integrationId: integrationIds[0] ?? "", + }, + {}, + ); + + const [serverInfo] = clientApi.widget.hardwareUsage.getServerInfo.useSuspenseQuery({ + integrationId: integrationIds[0] ?? "", + }); + + const [hardwareUsage, hardwareUsageHandlers] = useListState<{ + cpuLoad: CpuLoad; + memoryLoad: MemoryLoad; + networkLoad: NetworkLoad; + }>([hardwareUsageHistory]); + + clientApi.widget.hardwareUsage.subscribeCpu.useSubscription( + { + integrationId: integrationIds[0] ?? "", + }, + { + onData: (data) => { + hardwareUsageHandlers.append(data); + if (hardwareUsage.length > 15) { + hardwareUsageHandlers.shift(); + } + }, + }, + ); + + if (integrationIds.length != 1) { + throw new NoIntegrationSelectedError(); + } + + const hasLast = hardwareUsage.length > 0; + + return ( + + usage.cpuLoad)} hasLast={hasLast} /> + usage.memoryLoad)} + maxAvailableBytes={serverInfo.info.maxAvailableMemoryBytes} + hasLast={hasLast} + /> + usage.networkLoad)} hasLast={hasLast} /> + + ); +} diff --git a/packages/widgets/src/hardware-usage/graphs/cpu-graph.tsx b/packages/widgets/src/hardware-usage/graphs/cpu-graph.tsx new file mode 100644 index 0000000000..73ad2de745 --- /dev/null +++ b/packages/widgets/src/hardware-usage/graphs/cpu-graph.tsx @@ -0,0 +1,71 @@ +import { Paper, Text } from "@mantine/core"; +import { ResponsiveLine } from "@nivo/line"; + +import type { CpuLoad } from "@homarr/integrations"; + +import { GraphWrapper } from "./wrapper"; + +interface CpuGraphProps { + cpuLoad: CpuLoad[]; + hasLast: boolean; +} + +export const CpuGraph = ({ cpuLoad, hasLast }: CpuGraphProps) => { + const data = [ + { + id: "cpuLoad", + color: "red", + data: cpuLoad.map((usage, index) => ({ + x: `${index}`, + y: usage.sumLoad, + })), + }, + ]; + return ( + 0 ? cpuLoad[cpuLoad.length - 1]?.sumLoad.toFixed(2) : 0}%`} + showSubtitle={hasLast} + > + { + if (slice.points.length === 0) { + return null; + } + return ( + + + {/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */} + {slice.points[0]!.data.yFormatted}% + + + ); + }} + curve={"monotoneX"} + yFormat=" >-.2f" + axisTop={null} + axisRight={null} + axisBottom={null} + axisLeft={null} + enablePoints={false} + enableTouchCrosshair={true} + enableGridX={false} + enableGridY={false} + enableCrosshair={true} + useMesh={true} + animate={false} + /> + + ); +}; diff --git a/packages/widgets/src/hardware-usage/graphs/memory-graph.tsx b/packages/widgets/src/hardware-usage/graphs/memory-graph.tsx new file mode 100644 index 0000000000..4db3352226 --- /dev/null +++ b/packages/widgets/src/hardware-usage/graphs/memory-graph.tsx @@ -0,0 +1,73 @@ +import { Paper, Text } from "@mantine/core"; +import { ResponsiveLine } from "@nivo/line"; + +import { humanFileSize } from "@homarr/common"; +import type { MemoryLoad } from "@homarr/integrations"; + +import { GraphWrapper } from "./wrapper"; + +interface MemoryGraphProps { + memoryLoad: MemoryLoad[]; + hasLast: boolean; + maxAvailableBytes: number; +} + +export const MemoryGraph = ({ memoryLoad, hasLast, maxAvailableBytes }: MemoryGraphProps) => { + const data = [ + { + id: "memoryLoad", + color: "red", + data: memoryLoad.map((usage, index) => ({ + x: `${index}`, + y: usage.loadInBytes, + })), + }, + ]; + return ( + 0 ? (memoryLoad[memoryLoad.length - 1]?.loadInBytes ?? 0) : 0)} / ${humanFileSize(maxAvailableBytes)}`} + showSubtitle={hasLast} + > + { + if (slice.points.length === 0) { + return null; + } + return ( + + + {/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */} + {humanFileSize(slice.points[0]!.data.y.valueOf() as number)} + + + ); + }} + curve={"monotoneX"} + yFormat=" >-.2f" + axisTop={null} + axisRight={null} + axisBottom={null} + axisLeft={null} + enablePoints={false} + enableTouchCrosshair={true} + enableGridX={false} + enableGridY={false} + enableCrosshair={true} + useMesh={true} + animate={false} + /> + + ); +}; diff --git a/packages/widgets/src/hardware-usage/graphs/network-graph.tsx b/packages/widgets/src/hardware-usage/graphs/network-graph.tsx new file mode 100644 index 0000000000..948c6311a7 --- /dev/null +++ b/packages/widgets/src/hardware-usage/graphs/network-graph.tsx @@ -0,0 +1,104 @@ +import type { ReactNode } from "react"; +import { Group, Paper, Stack, Text } from "@mantine/core"; +import { ResponsiveLine } from "@nivo/line"; +import { IconDownload, IconUpload } from "@tabler/icons-react"; + +import { humanFileSize } from "@homarr/common"; +import type { NetworkLoad } from "@homarr/integrations"; + +import { GraphWrapper } from "./wrapper"; + +interface MemoryGraphProps { + networkLoad: NetworkLoad[]; + hasLast: boolean; +} + +export const NetworkGraph = ({ networkLoad, hasLast }: MemoryGraphProps) => { + const data = [ + { + id: "networkUp", + color: "red", + data: networkLoad.map((usage, index) => ({ + x: `${index}`, + y: usage.up, + })), + }, + { + id: "networkDown", + color: "green", + data: networkLoad.map((usage, index) => ({ + x: `${index}`, + y: usage.down, + })), + }, + ]; + + const lastDatapoint = networkLoad.length > 0 ? networkLoad[networkLoad.length - 1] : undefined; + const subtitle: ReactNode = lastDatapoint ? ( + + + + {humanFileSize(Math.round(lastDatapoint.up))}ps + + + + {humanFileSize(Math.round(lastDatapoint.down))}ps + + + ) : ( + <> + ); + + return ( + + { + if (slice.points.length != 2) { + return null; + } + return ( + + + + + + {humanFileSize(Math.round(slice.points[1]?.data.y.valueOf() as number))} + + + + + + {humanFileSize(Math.round(slice.points[0]?.data.y.valueOf() as number))} + + + + + ); + }} + curve={"monotoneX"} + yFormat=" >-.2f" + axisTop={null} + axisRight={null} + axisBottom={null} + axisLeft={null} + enablePoints={false} + enableTouchCrosshair={true} + enableGridX={false} + enableGridY={false} + enableCrosshair={true} + useMesh={true} + animate={false} + /> + + ); +}; diff --git a/packages/widgets/src/hardware-usage/graphs/wrapper.tsx b/packages/widgets/src/hardware-usage/graphs/wrapper.tsx new file mode 100644 index 0000000000..9cdd8921f5 --- /dev/null +++ b/packages/widgets/src/hardware-usage/graphs/wrapper.tsx @@ -0,0 +1,22 @@ +import type { ReactNode } from "react"; +import { Group, Paper, Text } from "@mantine/core"; + +interface GraphWrapperProps { + children: ReactNode; + title: string; + subtitle?: string | ReactNode; + showSubtitle?: boolean; + height?: number; +} + +export const GraphWrapper = ({ showSubtitle, subtitle, title, children, height = 125 }: GraphWrapperProps) => { + return ( + + + {title} + {showSubtitle && {subtitle}} + + {children} + + ); +}; diff --git a/packages/widgets/src/hardware-usage/index.ts b/packages/widgets/src/hardware-usage/index.ts new file mode 100644 index 0000000000..8f28528cba --- /dev/null +++ b/packages/widgets/src/hardware-usage/index.ts @@ -0,0 +1,10 @@ +import { IconVideo } from "@tabler/icons-react"; + +import { createWidgetDefinition } from "../definition"; +import { optionsBuilder } from "../options"; + +export const { componentLoader, definition } = createWidgetDefinition("hardwareUsage", { + icon: IconVideo, + supportedIntegrations: ["getDashDot"], + options: optionsBuilder.from(() => ({})), +}).withDynamicImport(() => import("./component")); diff --git a/packages/widgets/src/index.tsx b/packages/widgets/src/index.tsx index 1f3050285f..c88674379d 100644 --- a/packages/widgets/src/index.tsx +++ b/packages/widgets/src/index.tsx @@ -12,6 +12,7 @@ import type { WidgetComponentProps } from "./definition"; import * as dnsHoleControls from "./dns-hole/controls"; import * as dnsHoleSummary from "./dns-hole/summary"; import * as downloads from "./downloads"; +import * as hardwareUsage from "./hardware-usage"; import * as healthMonitoring from "./health-monitoring"; import * as iframe from "./iframe"; import type { WidgetImportRecord } from "./import"; @@ -45,6 +46,7 @@ export const widgetImports = { "smartHome-executeAutomation": smartHomeExecuteAutomation, mediaServer, calendar, + hardwareUsage, downloads, "mediaRequests-requestList": mediaRequestsList, "mediaRequests-requestStats": mediaRequestsStats, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 500739d11c..25adf1f700 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,7 +24,7 @@ importers: version: 4.3.3(vite@5.4.5(@types/node@22.8.6)(sass@1.80.6)(sugarss@4.0.1(postcss@8.4.47))(terser@5.32.0)) '@vitest/coverage-v8': specifier: ^2.1.4 - version: 2.1.4(vitest@2.1.4) + version: 2.1.4(vitest@2.1.4(@types/node@22.8.6)(@vitest/ui@2.1.4)(jsdom@25.0.1)(sass@1.80.6)(sugarss@4.0.1(postcss@8.4.47))(terser@5.32.0)) '@vitest/ui': specifier: ^2.1.4 version: 2.1.4(vitest@2.1.4) @@ -864,7 +864,7 @@ importers: version: 0.27.1 drizzle-orm: specifier: ^0.36.0 - version: 0.36.0(@libsql/client-wasm@0.14.0)(@types/better-sqlite3@7.6.11)(@types/react@18.3.12)(better-sqlite3@11.5.0)(mysql2@3.11.3)(react@18.3.1) + version: 0.36.0(@libsql/client-wasm@0.14.0)(@prisma/client@5.16.1)(@types/better-sqlite3@7.6.11)(@types/react@18.3.12)(better-sqlite3@11.5.0)(mysql2@3.11.3)(react@18.3.1) mysql2: specifier: 3.11.3 version: 3.11.3 @@ -1569,6 +1569,15 @@ importers: '@mantine/hooks': specifier: ^7.13.4 version: 7.13.4(react@18.3.1) + '@nivo/bar': + specifier: ^0.87.0 + version: 0.87.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@nivo/core': + specifier: ^0.87.0 + version: 0.87.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@nivo/line': + specifier: ^0.87.0 + version: 0.87.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@tabler/icons-react': specifier: ^3.21.0 version: 3.21.0(react@18.3.1) @@ -2913,6 +2922,54 @@ packages: cpu: [x64] os: [win32] + '@nivo/annotations@0.87.0': + resolution: {integrity: sha512-4Xk/soEmi706iOKszjX1EcGLBNIvhMifCYXOuLIFlMAXqhw1x2YS7PxickVSskdSzJCwJX4NgQ/R/9u6nxc5OA==} + peerDependencies: + react: '>= 16.14.0 < 19.0.0' + + '@nivo/axes@0.87.0': + resolution: {integrity: sha512-zCRBfiRKJi+xOxwxH5Pxq/8+yv3fAYDl4a1F2Ssnp5gMIobwzVsdearvsm5B04e9bfy3ZXTL7KgbkEkSAwu6SA==} + peerDependencies: + react: '>= 16.14.0 < 19.0.0' + + '@nivo/bar@0.87.0': + resolution: {integrity: sha512-r/MEVCNAHKfmsy1Fb+JztVczOhIEtAx4VFs2XUbn9YpEDgxydavUJyfoy5/nGq6h5jG1/t47cfB4nZle7c0fyQ==} + peerDependencies: + react: '>= 16.14.0 < 19.0.0' + + '@nivo/colors@0.87.0': + resolution: {integrity: sha512-S4pZzRGKK23t8XAjQMhML6wwsfKO9nH03xuyN4SvCodNA/Dmdys9xV+9Dg/VILTzvzsBTBGTX0dFBg65WoKfVg==} + peerDependencies: + react: '>= 16.14.0 < 19.0.0' + + '@nivo/core@0.87.0': + resolution: {integrity: sha512-yEQWJn7QjWnbmCZccBCo4dligNyNyz3kgyV9vEtcaB1iGeKhg55RJEAlCOul+IDgSCSPFci2SxTmipE6LZEZCg==} + peerDependencies: + react: '>= 16.14.0 < 19.0.0' + + '@nivo/legends@0.87.0': + resolution: {integrity: sha512-bVJCeqEmK4qHrxNaPU/+hXUd/yaKlcQ0yrsR18ewoknVX+pgvbe/+tRKJ+835JXlvRijYIuqwK1sUJQIxyB7oA==} + peerDependencies: + react: '>= 16.14.0 < 19.0.0' + + '@nivo/line@0.87.0': + resolution: {integrity: sha512-Ki/WDd8ZU8VWScW4ZeKUFCXRdAEg8nrS+F+jdfJDPxyxUMHZJCAbrXrnsExcEQLOaDQ2aU/bijEMiDS8/dJzuA==} + peerDependencies: + react: '>= 16.14.0 < 19.0.0' + + '@nivo/scales@0.87.0': + resolution: {integrity: sha512-IHdY9w2em/xpWurcbhUR3cUA1dgbY06rU8gmA/skFCwf3C4Da3Rqwr0XqvxmkDC+EdT/iFljMbLst7VYiCnSdw==} + + '@nivo/tooltip@0.87.0': + resolution: {integrity: sha512-nZJWyRIt/45V/JBdJ9ksmNm1LFfj59G1Dy9wB63Icf2YwyBT+J+zCzOGXaY7gxCxgF1mnSL3dC7fttcEdXyN/g==} + peerDependencies: + react: '>= 16.14.0 < 19.0.0' + + '@nivo/voronoi@0.87.0': + resolution: {integrity: sha512-Tg+9YnCX8LKsEwZMY1Q83mWiVFiyU2smxrO3JaC9vzjIh/2A/bkNPwC6BdmRaQMvY1jngKs+WKDnNxSQWFSOEg==} + peerDependencies: + react: '>= 16.14.0 < 19.0.0' + '@noble/hashes@1.5.0': resolution: {integrity: sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA==} engines: {node: ^14.21.3 || >=16} @@ -3033,6 +3090,42 @@ packages: '@popperjs/core@2.11.8': resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} + '@prisma/client@5.16.1': + resolution: {integrity: sha512-wM9SKQjF0qLxdnOZIVAIMKiz6Hu7vDt4FFAih85K1dk/Rr2mdahy6d3QP41K62N9O0DJJA//gUDA3Mp49xsKIg==} + engines: {node: '>=16.13'} + peerDependencies: + prisma: '*' + peerDependenciesMeta: + prisma: + optional: true + + '@react-spring/animated@9.7.4': + resolution: {integrity: sha512-7As+8Pty2QlemJ9O5ecsuPKjmO0NKvmVkRR1n6mEotFgWar8FKuQt2xgxz3RTgxcccghpx1YdS1FCdElQNexmQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + + '@react-spring/core@9.7.4': + resolution: {integrity: sha512-GzjA44niEJBFUe9jN3zubRDDDP2E4tBlhNlSIkTChiNf9p4ZQlgXBg50qbXfSXHQPHak/ExYxwhipKVsQ/sUTw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + + '@react-spring/rafz@9.7.4': + resolution: {integrity: sha512-mqDI6rW0Ca8IdryOMiXRhMtVGiEGLIO89vIOyFQXRIwwIMX30HLya24g9z4olDvFyeDW3+kibiKwtZnA4xhldA==} + + '@react-spring/shared@9.7.4': + resolution: {integrity: sha512-bEPI7cQp94dOtCFSEYpxvLxj0+xQfB5r9Ru1h8OMycsIq7zFZon1G0sHrBLaLQIWeMCllc4tVDYRTLIRv70C8w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + + '@react-spring/types@9.7.4': + resolution: {integrity: sha512-iQVztO09ZVfsletMiY+DpT/JRiBntdsdJ4uqk3UJFhrhS8mIC9ZOZbmfGSRs/kdbNPQkVyzucceDicQ/3Mlj9g==} + + '@react-spring/web@9.7.4': + resolution: {integrity: sha512-UMvCZp7I5HCVIleSa4BwbNxynqvj+mJjG2m20VO2yPoi2pnCYANy58flvz9v/YcXTAvsmL655FV3pm5fbr6akA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + '@remirror/core-constants@3.0.0': resolution: {integrity: sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==} @@ -3597,6 +3690,39 @@ packages: '@types/css-modules@1.0.5': resolution: {integrity: sha512-oeKafs/df9lwOvtfiXVliZsocFVOexK9PZtLQWuPeuVCFR7jwiqlg60lu80JTe5NFNtH3tnV6Fs/ySR8BUPHAw==} + '@types/d3-color@3.1.3': + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + + '@types/d3-delaunay@6.0.4': + resolution: {integrity: sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==} + + '@types/d3-format@1.4.5': + resolution: {integrity: sha512-mLxrC1MSWupOSncXN/HOlWUAAIffAEBaI4+PKy2uMPsKe4FNZlk7qrbTjmzJXITQQqBHivaks4Td18azgqnotA==} + + '@types/d3-path@3.1.0': + resolution: {integrity: sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ==} + + '@types/d3-scale-chromatic@3.0.3': + resolution: {integrity: sha512-laXM4+1o5ImZv3RpFAsTRn3TEkzqkytiOY0Dz0sq5cnd1dtNlk6sHLon4OvqaiJb28T0S/TdsBI3Sjsy+keJrw==} + + '@types/d3-scale@4.0.8': + resolution: {integrity: sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==} + + '@types/d3-shape@3.1.6': + resolution: {integrity: sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA==} + + '@types/d3-time-format@2.3.4': + resolution: {integrity: sha512-xdDXbpVO74EvadI3UDxjxTdR6QIxm1FKzEA/+F8tL4GWWUg/hgvBqf6chql64U5A9ZUGWo7pEu4eNlyLwbKdhg==} + + '@types/d3-time-format@3.0.4': + resolution: {integrity: sha512-or9DiDnYI1h38J9hxKEsw513+KVuFbEVhl7qdxcaudoiqWWepapUen+2vAriFGexr6W5+P4l9+HJrB39GG+oRg==} + + '@types/d3-time@1.1.4': + resolution: {integrity: sha512-JIvy2HjRInE+TXOmIGN5LCmeO0hkFZx5f9FZ7kiN+D+YTcc8pptsiLiuHsvwxwC7VVKmJ2ExHUgNlAiV7vQM9g==} + + '@types/d3-time@3.0.3': + resolution: {integrity: sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw==} + '@types/docker-modem@3.0.6': resolution: {integrity: sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg==} @@ -4516,6 +4642,57 @@ packages: csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + d3-array@2.12.1: + resolution: {integrity: sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==} + + d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-delaunay@6.0.4: + resolution: {integrity: sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==} + engines: {node: '>=12'} + + d3-format@1.4.5: + resolution: {integrity: sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ==} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + + d3-scale-chromatic@3.1.0: + resolution: {integrity: sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==} + engines: {node: '>=12'} + + d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + + d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + + d3-time-format@3.0.0: + resolution: {integrity: sha512-UXJh6EKsHBTjopVqZBhFysQcoXSv/5yLONZvkQ5Kk3qbwiUYkdX17Xa1PT6U1ZWXGGfB1ey5L8dKMlFq2DO0Ag==} + + d3-time@1.1.0: + resolution: {integrity: sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA==} + + d3-time@2.1.1: + resolution: {integrity: sha512-/eIQe/eR4kCQwq7yxi7z4c6qEXf2IYGcjoWB5OOQy4Tq9Uv39/947qlDcN2TLkiTzQWzvnsuYPB9TrWaNfipKQ==} + + d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + damerau-levenshtein@1.0.8: resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} @@ -4600,6 +4777,9 @@ packages: resolution: {integrity: sha512-wH9xOVHnczo9jN2IW68BabcecVPxacIA3g/7z6vhSU/4stOKQzeCRK0yD0A24WiAAUJmmVpWqrERcTxnLo3AnA==} engines: {node: '>=8'} + delaunator@5.0.1: + resolution: {integrity: sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==} + delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} @@ -5248,9 +5428,6 @@ packages: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} - get-func-name@2.0.2: - resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} - get-intrinsic@1.2.4: resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==} engines: {node: '>= 0.4'} @@ -5514,6 +5691,13 @@ packages: resolution: {integrity: sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==} engines: {node: '>= 0.4'} + internmap@1.0.1: + resolution: {integrity: sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==} + + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + intl-messageformat@10.7.1: resolution: {integrity: sha512-xQuJW2WcyzNJZWUu5xTVPOmNSA1Sowuu/NKFdUid5Fxx/Yl6/s4DefTU/y7zy+irZLDmFGmTLtnM8FqpN05wlA==} @@ -5920,9 +6104,6 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true - loupe@3.1.1: - resolution: {integrity: sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==} - loupe@3.1.2: resolution: {integrity: sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==} @@ -6954,6 +7135,9 @@ packages: deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true + robust-predicates@3.0.2: + resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==} + rollup@4.21.3: resolution: {integrity: sha512-7sqRtBNnEbcBtMeRVc6VRsJMmpI+JU1z9VTvW8D4gXIYQFz0aLcsE6rRkyghZkLfEgUZgVvOG7A5CVz/VW5GIA==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -9108,6 +9292,138 @@ snapshots: '@next/swc-win32-x64-msvc@14.2.16': optional: true + '@nivo/annotations@0.87.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@nivo/colors': 0.87.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@nivo/core': 0.87.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-spring/web': 9.7.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + lodash: 4.17.21 + react: 18.3.1 + transitivePeerDependencies: + - react-dom + + '@nivo/axes@0.87.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@nivo/core': 0.87.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@nivo/scales': 0.87.0 + '@react-spring/web': 9.7.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@types/d3-format': 1.4.5 + '@types/d3-time-format': 2.3.4 + d3-format: 1.4.5 + d3-time-format: 3.0.0 + react: 18.3.1 + transitivePeerDependencies: + - react-dom + + '@nivo/bar@0.87.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@nivo/annotations': 0.87.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@nivo/axes': 0.87.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@nivo/colors': 0.87.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@nivo/core': 0.87.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@nivo/legends': 0.87.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@nivo/scales': 0.87.0 + '@nivo/tooltip': 0.87.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-spring/web': 9.7.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@types/d3-scale': 4.0.8 + '@types/d3-shape': 3.1.6 + d3-scale: 4.0.2 + d3-shape: 3.2.0 + lodash: 4.17.21 + react: 18.3.1 + transitivePeerDependencies: + - react-dom + + '@nivo/colors@0.87.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@nivo/core': 0.87.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@types/d3-color': 3.1.3 + '@types/d3-scale': 4.0.8 + '@types/d3-scale-chromatic': 3.0.3 + '@types/prop-types': 15.7.12 + d3-color: 3.1.0 + d3-scale: 4.0.2 + d3-scale-chromatic: 3.1.0 + lodash: 4.17.21 + prop-types: 15.8.1 + react: 18.3.1 + transitivePeerDependencies: + - react-dom + + '@nivo/core@0.87.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@nivo/tooltip': 0.87.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-spring/web': 9.7.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@types/d3-shape': 3.1.6 + d3-color: 3.1.0 + d3-format: 1.4.5 + d3-interpolate: 3.0.1 + d3-scale: 4.0.2 + d3-scale-chromatic: 3.1.0 + d3-shape: 3.2.0 + d3-time-format: 3.0.0 + lodash: 4.17.21 + prop-types: 15.8.1 + react: 18.3.1 + transitivePeerDependencies: + - react-dom + + '@nivo/legends@0.87.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@nivo/colors': 0.87.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@nivo/core': 0.87.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@types/d3-scale': 4.0.8 + d3-scale: 4.0.2 + react: 18.3.1 + transitivePeerDependencies: + - react-dom + + '@nivo/line@0.87.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@nivo/annotations': 0.87.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@nivo/axes': 0.87.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@nivo/colors': 0.87.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@nivo/core': 0.87.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@nivo/legends': 0.87.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@nivo/scales': 0.87.0 + '@nivo/tooltip': 0.87.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@nivo/voronoi': 0.87.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-spring/web': 9.7.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + d3-shape: 3.2.0 + react: 18.3.1 + transitivePeerDependencies: + - react-dom + + '@nivo/scales@0.87.0': + dependencies: + '@types/d3-scale': 4.0.8 + '@types/d3-time': 1.1.4 + '@types/d3-time-format': 3.0.4 + d3-scale: 4.0.2 + d3-time: 1.1.0 + d3-time-format: 3.0.0 + lodash: 4.17.21 + + '@nivo/tooltip@0.87.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@nivo/core': 0.87.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-spring/web': 9.7.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + transitivePeerDependencies: + - react-dom + + '@nivo/voronoi@0.87.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@nivo/core': 0.87.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@nivo/tooltip': 0.87.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@types/d3-delaunay': 6.0.4 + '@types/d3-scale': 4.0.8 + d3-delaunay: 6.0.4 + d3-scale: 4.0.2 + react: 18.3.1 + transitivePeerDependencies: + - react-dom + '@noble/hashes@1.5.0': {} '@nodelib/fs.scandir@2.1.5': @@ -9204,6 +9520,41 @@ snapshots: '@popperjs/core@2.11.8': {} + '@prisma/client@5.16.1': + optional: true + + '@react-spring/animated@9.7.4(react@18.3.1)': + dependencies: + '@react-spring/shared': 9.7.4(react@18.3.1) + '@react-spring/types': 9.7.4 + react: 18.3.1 + + '@react-spring/core@9.7.4(react@18.3.1)': + dependencies: + '@react-spring/animated': 9.7.4(react@18.3.1) + '@react-spring/shared': 9.7.4(react@18.3.1) + '@react-spring/types': 9.7.4 + react: 18.3.1 + + '@react-spring/rafz@9.7.4': {} + + '@react-spring/shared@9.7.4(react@18.3.1)': + dependencies: + '@react-spring/rafz': 9.7.4 + '@react-spring/types': 9.7.4 + react: 18.3.1 + + '@react-spring/types@9.7.4': {} + + '@react-spring/web@9.7.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@react-spring/animated': 9.7.4(react@18.3.1) + '@react-spring/core': 9.7.4(react@18.3.1) + '@react-spring/shared': 9.7.4(react@18.3.1) + '@react-spring/types': 9.7.4 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + '@remirror/core-constants@3.0.0': {} '@rollup/pluginutils@5.1.0(rollup@4.21.3)': @@ -10012,6 +10363,32 @@ snapshots: '@types/css-modules@1.0.5': {} + '@types/d3-color@3.1.3': {} + + '@types/d3-delaunay@6.0.4': {} + + '@types/d3-format@1.4.5': {} + + '@types/d3-path@3.1.0': {} + + '@types/d3-scale-chromatic@3.0.3': {} + + '@types/d3-scale@4.0.8': + dependencies: + '@types/d3-time': 3.0.3 + + '@types/d3-shape@3.1.6': + dependencies: + '@types/d3-path': 3.1.0 + + '@types/d3-time-format@2.3.4': {} + + '@types/d3-time-format@3.0.4': {} + + '@types/d3-time@1.1.4': {} + + '@types/d3-time@3.0.3': {} + '@types/docker-modem@3.0.6': dependencies: '@types/node': 22.8.6 @@ -10272,7 +10649,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitest/coverage-v8@2.1.4(vitest@2.1.4)': + '@vitest/coverage-v8@2.1.4(vitest@2.1.4(@types/node@22.8.6)(@vitest/ui@2.1.4)(jsdom@25.0.1)(sass@1.80.6)(sugarss@4.0.1(postcss@8.4.47))(terser@5.32.0))': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 0.2.3 @@ -10826,7 +11203,7 @@ snapshots: assertion-error: 2.0.1 check-error: 2.1.1 deep-eql: 5.0.2 - loupe: 3.1.1 + loupe: 3.1.2 pathval: 2.0.0 chalk-scripts@1.2.8: @@ -11095,6 +11472,59 @@ snapshots: csstype@3.1.3: {} + d3-array@2.12.1: + dependencies: + internmap: 1.0.1 + + d3-array@3.2.4: + dependencies: + internmap: 2.0.3 + + d3-color@3.1.0: {} + + d3-delaunay@6.0.4: + dependencies: + delaunator: 5.0.1 + + d3-format@1.4.5: {} + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-path@3.1.0: {} + + d3-scale-chromatic@3.1.0: + dependencies: + d3-color: 3.1.0 + d3-interpolate: 3.0.1 + + d3-scale@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-format: 1.4.5 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 3.0.0 + + d3-shape@3.2.0: + dependencies: + d3-path: 3.1.0 + + d3-time-format@3.0.0: + dependencies: + d3-time: 2.1.1 + + d3-time@1.1.0: {} + + d3-time@2.1.1: + dependencies: + d3-array: 2.12.1 + + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + damerau-levenshtein@1.0.8: {} data-uri-to-buffer@6.0.2: {} @@ -11179,6 +11609,10 @@ snapshots: rimraf: 3.0.2 slash: 3.0.0 + delaunator@5.0.1: + dependencies: + robust-predicates: 3.0.2 + delayed-stream@1.0.0: {} delegates@1.0.0: {} @@ -11291,9 +11725,10 @@ snapshots: transitivePeerDependencies: - supports-color - drizzle-orm@0.36.0(@libsql/client-wasm@0.14.0)(@types/better-sqlite3@7.6.11)(@types/react@18.3.12)(better-sqlite3@11.5.0)(mysql2@3.11.3)(react@18.3.1): + drizzle-orm@0.36.0(@libsql/client-wasm@0.14.0)(@prisma/client@5.16.1)(@types/better-sqlite3@7.6.11)(@types/react@18.3.12)(better-sqlite3@11.5.0)(mysql2@3.11.3)(react@18.3.1): optionalDependencies: '@libsql/client-wasm': 0.14.0 + '@prisma/client': 5.16.1 '@types/better-sqlite3': 7.6.11 '@types/react': 18.3.12 better-sqlite3: 11.5.0 @@ -11951,8 +12386,6 @@ snapshots: get-caller-file@2.0.5: {} - get-func-name@2.0.2: {} - get-intrinsic@1.2.4: dependencies: es-errors: 1.3.0 @@ -12258,6 +12691,10 @@ snapshots: hasown: 2.0.2 side-channel: 1.0.6 + internmap@1.0.1: {} + + internmap@2.0.3: {} + intl-messageformat@10.7.1: dependencies: '@formatjs/ecma402-abstract': 2.2.0 @@ -12675,10 +13112,6 @@ snapshots: dependencies: js-tokens: 4.0.0 - loupe@3.1.1: - dependencies: - get-func-name: 2.0.2 - loupe@3.1.2: {} lower-case-first@1.0.2: @@ -13785,6 +14218,8 @@ snapshots: dependencies: glob: 7.2.3 + robust-predicates@3.0.2: {} + rollup@4.21.3: dependencies: '@types/estree': 1.0.5