diff --git a/apps/tasks/package.json b/apps/tasks/package.json index 10f7f3dea..7059522a5 100644 --- a/apps/tasks/package.json +++ b/apps/tasks/package.json @@ -30,6 +30,7 @@ "@homarr/icons": "workspace:^0.1.0", "@homarr/integrations": "workspace:^0.1.0", "@homarr/log": "workspace:^", + "@homarr/ping": "workspace:^0.1.0", "@homarr/redis": "workspace:^0.1.0", "@homarr/server-settings": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0", diff --git a/packages/api/package.json b/packages/api/package.json index 68463e574..d7b8c3d52 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -35,6 +35,7 @@ "@homarr/log": "workspace:^", "@homarr/old-import": "workspace:^0.1.0", "@homarr/old-schema": "workspace:^0.1.0", + "@homarr/ping": "workspace:^0.1.0", "@homarr/redis": "workspace:^0.1.0", "@homarr/request-handler": "workspace:^0.1.0", "@homarr/server-settings": "workspace:^0.1.0", diff --git a/packages/api/src/router/test/widgets/app.spec.ts b/packages/api/src/router/test/widgets/app.spec.ts new file mode 100644 index 000000000..bd5265ff6 --- /dev/null +++ b/packages/api/src/router/test/widgets/app.spec.ts @@ -0,0 +1,53 @@ +import { describe, expect, test, vi } from "vitest"; + +import type { Session } from "@homarr/auth"; +import { createDb } from "@homarr/db/test"; +import * as ping from "@homarr/ping"; + +import { appRouter } from "../../widgets/app"; + +// Mock the auth module to return an empty session +vi.mock("@homarr/auth", () => ({ auth: () => ({}) as Session })); +vi.mock("@homarr/ping", () => ({ sendPingRequestAsync: async () => await Promise.resolve(null) })); + +describe("ping should call sendPingRequestAsync with url and return result", () => { + test("ping with error response should return error and url", async () => { + // Arrange + const spy = vi.spyOn(ping, "sendPingRequestAsync"); + const url = "http://localhost"; + const db = createDb(); + const caller = appRouter.createCaller({ + db, + deviceType: undefined, + session: null, + }); + spy.mockImplementation(() => Promise.resolve({ error: "error" })); + + // Act + const result = await caller.ping({ url }); + + // Assert + expect(result.url).toBe(url); + expect("error" in result).toBe(true); + }); + + test("ping with success response should return statusCode and url", async () => { + // Arrange + const spy = vi.spyOn(ping, "sendPingRequestAsync"); + const url = "http://localhost"; + const db = createDb(); + const caller = appRouter.createCaller({ + db, + deviceType: undefined, + session: null, + }); + spy.mockImplementation(() => Promise.resolve({ statusCode: 200, durationMs: 123 })); + + // Act + const result = await caller.ping({ url }); + + // Assert + expect(result.url).toBe(url); + expect("statusCode" in result).toBe(true); + }); +}); diff --git a/packages/api/src/router/widgets/app.ts b/packages/api/src/router/widgets/app.ts index a337b2e54..fa2ed05b4 100644 --- a/packages/api/src/router/widgets/app.ts +++ b/packages/api/src/router/widgets/app.ts @@ -1,12 +1,20 @@ import { observable } from "@trpc/server/observable"; import { z } from "zod"; -import { pingUrlChannel } from "@homarr/redis"; -import { pingRequestHandler } from "@homarr/request-handler/ping"; +import { sendPingRequestAsync } from "@homarr/ping"; +import { pingChannel, pingUrlChannel } from "@homarr/redis"; import { createTRPCRouter, publicProcedure } from "../../trpc"; export const appRouter = createTRPCRouter({ + ping: publicProcedure.input(z.object({ url: z.string() })).query(async ({ input }) => { + const pingResult = await sendPingRequestAsync(input.url); + + return { + url: input.url, + ...pingResult, + }; + }), updatedPing: publicProcedure .input( z.object({ @@ -15,20 +23,16 @@ export const appRouter = createTRPCRouter({ ) .subscription(async ({ input }) => { await pingUrlChannel.addAsync(input.url); - const innerHandler = pingRequestHandler.handler({ url: input.url }); + + const pingResult = await sendPingRequestAsync(input.url); return observable<{ url: string; statusCode: number; durationMs: number } | { url: string; error: string }>( (emit) => { - // Run ping request in background - void innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false }).then(({ data }) => { - emit.next({ url: input.url, ...data }); - }); - - const unsubscribe = innerHandler.subscribe((pingResponse) => { - emit.next({ - url: input.url, - ...pingResponse, - }); + emit.next({ url: input.url, ...pingResult }); + const unsubscribe = pingChannel.subscribe((message) => { + // Only emit if same url + if (message.url !== input.url) return; + emit.next(message); }); return () => { diff --git a/packages/certificates/src/server.ts b/packages/certificates/src/server.ts index 419a382c0..a138f4039 100644 --- a/packages/certificates/src/server.ts +++ b/packages/certificates/src/server.ts @@ -6,6 +6,7 @@ import { Agent as HttpsAgent } from "node:https"; import path from "node:path"; import { checkServerIdentity, rootCertificates } from "node:tls"; import axios from "axios"; +import type { RequestInfo, RequestInit, Response } from "undici"; import { fetch } from "undici"; import { env } from "@homarr/common/env"; @@ -131,8 +132,8 @@ export const createAxiosCertificateInstanceAsync = async ( }); }; -export const fetchWithTrustedCertificatesAsync: typeof fetch = async (url, options) => { - const agent = await createCertificateAgentAsync(); +export const fetchWithTrustedCertificatesAsync = async (url: RequestInfo, options?: RequestInit): Promise => { + const agent = await createCertificateAgentAsync(undefined); return fetch(url, { ...options, dispatcher: agent, diff --git a/packages/cron-jobs/package.json b/packages/cron-jobs/package.json index 1165d0b74..f833d63f6 100644 --- a/packages/cron-jobs/package.json +++ b/packages/cron-jobs/package.json @@ -32,6 +32,7 @@ "@homarr/icons": "workspace:^0.1.0", "@homarr/integrations": "workspace:^0.1.0", "@homarr/log": "workspace:^0.1.0", + "@homarr/ping": "workspace:^0.1.0", "@homarr/redis": "workspace:^0.1.0", "@homarr/request-handler": "workspace:^0.1.0", "@homarr/server-settings": "workspace:^0.1.0", diff --git a/packages/cron-jobs/src/jobs/ping.ts b/packages/cron-jobs/src/jobs/ping.ts index 52a85b2c5..318db80bd 100644 --- a/packages/cron-jobs/src/jobs/ping.ts +++ b/packages/cron-jobs/src/jobs/ping.ts @@ -2,8 +2,8 @@ import { EVERY_MINUTE } from "@homarr/cron-jobs-core/expressions"; import { db } from "@homarr/db"; import { getServerSettingByKeyAsync } from "@homarr/db/queries"; import { logger } from "@homarr/log"; -import { pingUrlChannel } from "@homarr/redis"; -import { pingRequestHandler } from "@homarr/request-handler/ping"; +import { sendPingRequestAsync } from "@homarr/ping"; +import { pingChannel, pingUrlChannel } from "@homarr/redis"; import { createCronJob } from "../lib"; @@ -28,6 +28,16 @@ export const pingJob = createCronJob("ping", EVERY_MINUTE, { }); const pingAsync = async (url: string) => { - const handler = pingRequestHandler.handler({ url }); - await handler.getCachedOrUpdatedDataAsync({ forceUpdate: true }); + const pingResult = await sendPingRequestAsync(url); + + if ("statusCode" in pingResult) { + logger.debug(`executed ping for url ${url} with status code ${pingResult.statusCode}`); + } else { + logger.error(`Executing ping for url ${url} failed with error: ${pingResult.error}`); + } + + await pingChannel.publishAsync({ + url, + ...pingResult, + }); }; diff --git a/packages/definitions/src/docs/homarr-docs-sitemap.ts b/packages/definitions/src/docs/homarr-docs-sitemap.ts index 1dc481a49..378a96c83 100644 --- a/packages/definitions/src/docs/homarr-docs-sitemap.ts +++ b/packages/definitions/src/docs/homarr-docs-sitemap.ts @@ -39,22 +39,16 @@ export type HomarrDocumentationPath = | "/search" | "/docs/tags" | "/docs/tags/active-directory" - | "/docs/tags/ad-guard" - | "/docs/tags/ad-guard-home" | "/docs/tags/administration" | "/docs/tags/advanced" | "/docs/tags/analytics" | "/docs/tags/api" | "/docs/tags/apps" | "/docs/tags/background" - | "/docs/tags/banner" - | "/docs/tags/blocking" | "/docs/tags/boards" - | "/docs/tags/bookmark" | "/docs/tags/bookmarks" | "/docs/tags/caddy" | "/docs/tags/certificates" - | "/docs/tags/checklist" | "/docs/tags/code" | "/docs/tags/community" | "/docs/tags/configuration" @@ -64,63 +58,37 @@ export type HomarrDocumentationPath = | "/docs/tags/database" | "/docs/tags/developer" | "/docs/tags/development" - | "/docs/tags/dns" | "/docs/tags/docker" | "/docs/tags/donation" | "/docs/tags/edit-mode" | "/docs/tags/env" | "/docs/tags/environment-variables" - | "/docs/tags/feeds" - | "/docs/tags/finance" | "/docs/tags/getting-started" | "/docs/tags/google" - | "/docs/tags/grafana" | "/docs/tags/groups" - | "/docs/tags/hardware" - | "/docs/tags/health" | "/docs/tags/help" | "/docs/tags/icon-picker" | "/docs/tags/icon-repositories" | "/docs/tags/icons" - | "/docs/tags/iframe" - | "/docs/tags/images" | "/docs/tags/installation" - | "/docs/tags/integrade" | "/docs/tags/integration" | "/docs/tags/integrations" | "/docs/tags/interface" - | "/docs/tags/jellyserr" | "/docs/tags/jobs" | "/docs/tags/layout" | "/docs/tags/ldap" - | "/docs/tags/links" - | "/docs/tags/lists" | "/docs/tags/management" - | "/docs/tags/market" | "/docs/tags/media" - | "/docs/tags/minecraft" - | "/docs/tags/monitoring" - | "/docs/tags/network" - | "/docs/tags/news" - | "/docs/tags/notebook" - | "/docs/tags/notes" | "/docs/tags/oidc" | "/docs/tags/open-collective" - | "/docs/tags/open-media-vault" - | "/docs/tags/overseerr" | "/docs/tags/permissions" | "/docs/tags/pgid" - | "/docs/tags/pi-hole" | "/docs/tags/ping" | "/docs/tags/programming" - | "/docs/tags/proxmox" | "/docs/tags/proxy" | "/docs/tags/puid" - | "/docs/tags/releases" - | "/docs/tags/repositories" | "/docs/tags/responsive" | "/docs/tags/roles" - | "/docs/tags/rss" | "/docs/tags/search" | "/docs/tags/search-engines" | "/docs/tags/security" @@ -128,24 +96,15 @@ export type HomarrDocumentationPath = | "/docs/tags/seo" | "/docs/tags/server" | "/docs/tags/settings" - | "/docs/tags/sinkhole" | "/docs/tags/sso" - | "/docs/tags/stocks" - | "/docs/tags/system" - | "/docs/tags/table" | "/docs/tags/tasks" | "/docs/tags/technical-documentation" - | "/docs/tags/text" - | "/docs/tags/torrent" | "/docs/tags/traefik" | "/docs/tags/translations" - | "/docs/tags/unifi-controller" | "/docs/tags/unraid" | "/docs/tags/uploads" - | "/docs/tags/usenet" | "/docs/tags/users" | "/docs/tags/variables" - | "/docs/tags/widgets" | "/docs/advanced/command-line" | "/docs/advanced/command-line/fix-usernames" | "/docs/advanced/command-line/password-recovery" @@ -188,17 +147,38 @@ export type HomarrDocumentationPath = | "/docs/getting-started/installation/source" | "/docs/getting-started/installation/synology" | "/docs/getting-started/installation/unraid" - | "/docs/integrations/cloud" - | "/docs/integrations/containers" - | "/docs/integrations/dns" - | "/docs/integrations/hardware" + | "/docs/integrations/adguard-home" + | "/docs/integrations/codeberg" + | "/docs/integrations/dash-dot" + | "/docs/integrations/deluge" + | "/docs/integrations/docker-hub" + | "/docs/integrations/docker" + | "/docs/integrations/emby" + | "/docs/integrations/github" + | "/docs/integrations/gitlab" + | "/docs/integrations/home-assistant" + | "/docs/integrations/jellyfin" + | "/docs/integrations/jellyseerr" | "/docs/integrations/kubernetes" - | "/docs/integrations/media-requester" - | "/docs/integrations/media-server" - | "/docs/integrations/network" - | "/docs/integrations/servarr" - | "/docs/integrations/torrent" - | "/docs/integrations/usenet" + | "/docs/integrations/lidarr" + | "/docs/integrations/nextcloud" + | "/docs/integrations/npm" + | "/docs/integrations/ntfy" + | "/docs/integrations/nzbget" + | "/docs/integrations/open-media-vault" + | "/docs/integrations/overseerr" + | "/docs/integrations/pi-hole" + | "/docs/integrations/plex" + | "/docs/integrations/prowlarr" + | "/docs/integrations/proxmox" + | "/docs/integrations/q-bittorent" + | "/docs/integrations/radarr" + | "/docs/integrations/readarr" + | "/docs/integrations/sabnzbd" + | "/docs/integrations/sonarr" + | "/docs/integrations/tdarr" + | "/docs/integrations/transmission" + | "/docs/integrations/unifi-controller" | "/docs/management/api" | "/docs/management/apps" | "/docs/management/boards" @@ -209,23 +189,32 @@ export type HomarrDocumentationPath = | "/docs/management/settings" | "/docs/management/tasks" | "/docs/management/users" + | "/docs/widgets/app" | "/docs/widgets/bookmarks" | "/docs/widgets/calendar" | "/docs/widgets/clock" - | "/docs/widgets/dns-hole" + | "/docs/widgets/dns-hole-controls" + | "/docs/widgets/dns-hole-summary" + | "/docs/widgets/docker-containers" | "/docs/widgets/downloads" | "/docs/widgets/health-monitoring" - | "/docs/widgets/home-assistant" | "/docs/widgets/iframe" | "/docs/widgets/indexer-manager" - | "/docs/widgets/media-requests" + | "/docs/widgets/media-releases" + | "/docs/widgets/media-request-list" + | "/docs/widgets/media-request-stats" | "/docs/widgets/media-server" + | "/docs/widgets/media-transcoding" | "/docs/widgets/minecraft-server-status" - | "/docs/widgets/network-controller" + | "/docs/widgets/network-controller-status" + | "/docs/widgets/network-controller-summary" | "/docs/widgets/notebook" + | "/docs/widgets/notifications" | "/docs/widgets/releases" - | "/docs/widgets/rss" - | "/docs/widgets/stocks" + | "/docs/widgets/rss-feed" + | "/docs/widgets/smart-home-entity-state" + | "/docs/widgets/smart-home-execute-automation" + | "/docs/widgets/stock-price" | "/docs/widgets/video" | "/docs/widgets/weather" | "" diff --git a/packages/ping/eslint.config.js b/packages/ping/eslint.config.js new file mode 100644 index 000000000..f7a5a7d36 --- /dev/null +++ b/packages/ping/eslint.config.js @@ -0,0 +1,4 @@ +import baseConfig from "@homarr/eslint-config/base"; + +/** @type {import('typescript-eslint').Config} */ +export default [...baseConfig]; diff --git a/packages/ping/index.ts b/packages/ping/index.ts new file mode 100644 index 000000000..3bd16e178 --- /dev/null +++ b/packages/ping/index.ts @@ -0,0 +1 @@ +export * from "./src"; diff --git a/packages/ping/package.json b/packages/ping/package.json new file mode 100644 index 000000000..3a87c2aa7 --- /dev/null +++ b/packages/ping/package.json @@ -0,0 +1,36 @@ +{ + "name": "@homarr/ping", + "version": "0.1.0", + "private": true, + "license": "Apache-2.0", + "type": "module", + "exports": { + ".": "./index.ts" + }, + "typesVersions": { + "*": { + "*": [ + "src/*" + ] + } + }, + "scripts": { + "clean": "rm -rf .turbo node_modules", + "format": "prettier --check . --ignore-path ../../.gitignore", + "lint": "eslint", + "typecheck": "tsc --noEmit" + }, + "prettier": "@homarr/prettier-config", + "dependencies": { + "@homarr/certificates": "workspace:^0.1.0", + "@homarr/common": "workspace:^0.1.0", + "@homarr/log": "workspace:^0.1.0" + }, + "devDependencies": { + "@homarr/eslint-config": "workspace:^0.2.0", + "@homarr/prettier-config": "workspace:^0.1.0", + "@homarr/tsconfig": "workspace:^0.1.0", + "eslint": "^9.32.0", + "typescript": "^5.8.3" + } +} diff --git a/packages/ping/src/index.ts b/packages/ping/src/index.ts new file mode 100644 index 000000000..1bf624706 --- /dev/null +++ b/packages/ping/src/index.ts @@ -0,0 +1,37 @@ +import { fetch } from "undici"; + +import { extractErrorMessage } from "@homarr/common"; +import { LoggingAgent } from "@homarr/common/server"; +import { logger } from "@homarr/log"; + +export const sendPingRequestAsync = async (url: string) => { + try { + const controller = new AbortController(); + + // 10 seconds timeout: + const timeoutId = setTimeout(() => controller.abort(), 10000); + const start = performance.now(); + + return await fetch(url, { + dispatcher: new LoggingAgent({ + connect: { + rejectUnauthorized: false, // Ping should always work, even with untrusted certificates + }, + }), + signal: controller.signal, + }) + .finally(() => { + clearTimeout(timeoutId); + }) + .then((response) => { + const end = performance.now(); + const durationMs = end - start; + return { statusCode: response.status, durationMs }; + }); + } catch (error) { + logger.error(new Error(`Failed to send ping request to "${url}"`, { cause: error })); + return { + error: extractErrorMessage(error), + }; + } +}; diff --git a/packages/ping/tsconfig.json b/packages/ping/tsconfig.json new file mode 100644 index 000000000..612bef8df --- /dev/null +++ b/packages/ping/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "@homarr/tsconfig/base.json", + "compilerOptions": { + "types": ["node"], + "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json" + }, + "include": ["*.ts", "src"], + "exclude": ["node_modules"] +} diff --git a/packages/redis/src/index.ts b/packages/redis/src/index.ts index 918fbee09..0c00b1464 100644 --- a/packages/redis/src/index.ts +++ b/packages/redis/src/index.ts @@ -1,4 +1,4 @@ -import { LogLevel } from "@homarr/log/constants"; +import type { LogLevel } from "@homarr/log/constants"; import { createListChannel, createQueueChannel, createSubPubChannel } from "./lib/channel"; @@ -14,6 +14,10 @@ export { createGetSetChannel, } from "./lib/channel"; +export const exampleChannel = createSubPubChannel<{ message: string }>("example"); +export const pingChannel = createSubPubChannel< + { url: string; statusCode: number; durationMs: number } | { url: string; error: string } +>("ping"); export const pingUrlChannel = createListChannel("ping-url"); export const homeAssistantEntityState = createSubPubChannel<{ diff --git a/packages/request-handler/src/ping.ts b/packages/request-handler/src/ping.ts deleted file mode 100644 index a53930b2c..000000000 --- a/packages/request-handler/src/ping.ts +++ /dev/null @@ -1,50 +0,0 @@ -import dayjs from "dayjs"; -import duration from "dayjs/plugin/duration"; -import { fetch } from "undici"; - -import { extractErrorMessage } from "@homarr/common"; -import { LoggingAgent } from "@homarr/common/server"; -import { logger } from "@homarr/log"; - -import { createCachedWidgetRequestHandler } from "./lib/cached-widget-request-handler"; - -dayjs.extend(duration); - -type PingResponse = - | { - statusCode: number; - durationMs: number; - } - | { - error: string; - }; -export const pingRequestHandler = createCachedWidgetRequestHandler({ - queryKey: "pingResult", - widgetKind: "app", - async requestAsync(input) { - return await sendPingRequestAsync(input.url); - }, - cacheDuration: dayjs.duration(1, "minute"), -}); - -const sendPingRequestAsync = async (url: string) => { - try { - const start = performance.now(); - return await fetch(url, { - dispatcher: new LoggingAgent({ - connect: { - rejectUnauthorized: false, - }, - }), - }).then((response) => { - const end = performance.now(); - logger.debug(`Ping request succeeded url="${url}" status="${response.status}" duration="${end - start}ms"`); - return { statusCode: response.status, durationMs: end - start }; - }); - } catch (error) { - logger.error(new Error(`Failed to send ping request to url="${url}"`, { cause: error })); - return { - error: extractErrorMessage(error), - }; - } -}; diff --git a/packages/widgets/src/app/component.tsx b/packages/widgets/src/app/component.tsx index e6f3d70ee..a89532063 100644 --- a/packages/widgets/src/app/component.tsx +++ b/packages/widgets/src/app/component.tsx @@ -1,20 +1,25 @@ "use client"; import type { PropsWithChildren } from "react"; +import { Suspense } from "react"; import { Flex, Text, Tooltip, UnstyledButton } from "@mantine/core"; +import { IconLoader } from "@tabler/icons-react"; import combineClasses from "clsx"; import { clientApi } from "@homarr/api/client"; import { useRequiredBoard } from "@homarr/boards/context"; import { useSettings } from "@homarr/settings"; import { useRegisterSpotlightContextResults } from "@homarr/spotlight"; +import { useI18n } from "@homarr/translation/client"; import { MaskedOrNormalImage } from "@homarr/ui"; import type { WidgetComponentProps } from "../definition"; import classes from "./app.module.css"; +import { PingDot } from "./ping/ping-dot"; import { PingIndicator } from "./ping/ping-indicator"; export default function AppWidget({ options, isEditMode, height, width }: WidgetComponentProps<"app">) { + const t = useI18n(); const settings = useSettings(); const board = useRequiredBoard(); const [app] = clientApi.app.byId.useSuspenseQuery( @@ -92,7 +97,9 @@ export default function AppWidget({ options, isEditMode, height, width }: Widget {options.pingEnabled && !settings.forceDisableStatus && !board.disableStatus && app.href ? ( - + }> + + ) : null} ); diff --git a/packages/widgets/src/app/ping/ping-indicator.tsx b/packages/widgets/src/app/ping/ping-indicator.tsx index a00ec2640..a74a6c79f 100644 --- a/packages/widgets/src/app/ping/ping-indicator.tsx +++ b/packages/widgets/src/app/ping/ping-indicator.tsx @@ -1,9 +1,8 @@ import { useState } from "react"; -import { IconCheck, IconLoader, IconX } from "@tabler/icons-react"; +import { IconCheck, IconX } from "@tabler/icons-react"; import type { RouterOutputs } from "@homarr/api"; import { clientApi } from "@homarr/api/client"; -import { useI18n } from "@homarr/translation/client"; import { PingDot } from "./ping-dot"; @@ -12,8 +11,17 @@ interface PingIndicatorProps { } export const PingIndicator = ({ href }: PingIndicatorProps) => { - const t = useI18n(); - const [pingResult, setPingResult] = useState(null); + const [ping] = clientApi.widget.app.ping.useSuspenseQuery( + { + url: href, + }, + { + refetchOnMount: false, + refetchOnWindowFocus: false, + }, + ); + + const [pingResult, setPingResult] = useState(ping); clientApi.widget.app.updatedPing.useSubscription( { url: href }, @@ -24,10 +32,6 @@ export const PingIndicator = ({ href }: PingIndicatorProps) => { }, ); - if (!pingResult) { - return ; - } - const isError = "error" in pingResult || pingResult.statusCode >= 500; return ( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a9275abe6..297472022 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -380,6 +380,9 @@ importers: '@homarr/log': specifier: workspace:^ version: link:../../packages/log + '@homarr/ping': + specifier: workspace:^0.1.0 + version: link:../../packages/ping '@homarr/redis': specifier: workspace:^0.1.0 version: link:../../packages/redis @@ -578,6 +581,9 @@ importers: '@homarr/old-schema': specifier: workspace:^0.1.0 version: link:../old-schema + '@homarr/ping': + specifier: workspace:^0.1.0 + version: link:../ping '@homarr/redis': specifier: workspace:^0.1.0 version: link:../redis @@ -1013,6 +1019,9 @@ importers: '@homarr/log': specifier: workspace:^0.1.0 version: link:../log + '@homarr/ping': + specifier: workspace:^0.1.0 + version: link:../ping '@homarr/redis': specifier: workspace:^0.1.0 version: link:../redis @@ -1738,6 +1747,34 @@ importers: specifier: ^5.8.3 version: 5.8.3 + packages/ping: + dependencies: + '@homarr/certificates': + specifier: workspace:^0.1.0 + version: link:../certificates + '@homarr/common': + specifier: workspace:^0.1.0 + version: link:../common + '@homarr/log': + specifier: workspace:^0.1.0 + version: link:../log + devDependencies: + '@homarr/eslint-config': + specifier: workspace:^0.2.0 + version: link:../../tooling/eslint + '@homarr/prettier-config': + specifier: workspace:^0.1.0 + version: link:../../tooling/prettier + '@homarr/tsconfig': + specifier: workspace:^0.1.0 + version: link:../../tooling/typescript + eslint: + specifier: ^9.32.0 + version: 9.32.0 + typescript: + specifier: ^5.8.3 + version: 5.8.3 + packages/redis: dependencies: '@homarr/common':