From d6dde5fc4178d5d25179122b70ee20a30c5d6e23 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Mon, 19 Jan 2026 07:46:11 -0800 Subject: [PATCH 01/19] Documentaiton: clarify backend port usage in NetAlertX widget docs --- docs/widgets/services/netalertx.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/widgets/services/netalertx.md b/docs/widgets/services/netalertx.md index a67de624c..5c28deeca 100644 --- a/docs/widgets/services/netalertx.md +++ b/docs/widgets/services/netalertx.md @@ -19,7 +19,7 @@ Provide the `API_TOKEN` (f.k.a. `SYNC_api_token`) as the `key` in your config. ```yaml widget: type: netalertx - url: http://ip:port + url: http://ip:port # backend port key: yournetalertxapitoken version: 2 # optional, default is 1 ``` From f524531a13d3aef7b319366476601399bd931c34 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Mon, 19 Jan 2026 07:49:46 -0800 Subject: [PATCH 02/19] Fix truenas proxy widget logging --- src/widgets/truenas/proxy.js | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/src/widgets/truenas/proxy.js b/src/widgets/truenas/proxy.js index ebc5299ef..334b373bc 100644 --- a/src/widgets/truenas/proxy.js +++ b/src/widgets/truenas/proxy.js @@ -25,9 +25,6 @@ function waitForEvent(ws, handler, { event = "message", parseJson = true } = {}) } else if (typeof payload === "string") { parsed = JSON.parse(payload); } - logger.info("Received TrueNAS websocket message: %o", parsed); - } else { - logger.info("Received TrueNAS websocket message: %o", payload); } const handlerResult = handler(parsed); if (handlerResult !== undefined) { @@ -52,7 +49,7 @@ function waitForEvent(ws, handler, { event = "message", parseJson = true } = {}) const handleClose = () => { cleanup(); - logger.error("TrueNAS websocket connection closed unexpectedly"); + logger.debug("TrueNAS websocket connection closed unexpectedly"); reject(new Error("TrueNAS websocket closed the connection")); }; @@ -73,7 +70,6 @@ let nextId = 1; async function sendMethod(ws, method, params = []) { const id = nextId++; const payload = { jsonrpc: "2.0", id, method, params }; - logger.info("Sending TrueNAS websocket method %s with id %d", method, id); ws.send(JSON.stringify(payload)); return waitForEvent(ws, (message) => { @@ -92,7 +88,7 @@ async function authenticate(ws, widget) { if (apiKeyResult === true) return; logger.warn("TrueNAS API key authentication failed, falling back to username/password when available."); } catch (err) { - logger.warn("TrueNAS API key authentication failed: %s", err?.message ?? err); + logger.error("TrueNAS API key authentication failed: %s", err?.message ?? err); } } @@ -141,13 +137,9 @@ export default async function truenasProxyHandler(req, res, map) { let data; const wsUrl = new URL(formatApiCall(widgets[widget.type].wsAPI, { ...widget })); const useSecure = wsUrl.protocol === "https:" || Boolean(widget.key); // API key requires secure connection - if (useSecure && wsUrl.protocol !== "https:") - logger.info("Upgrading TrueNAS websocket connection to secure wss://"); wsUrl.protocol = useSecure ? "wss:" : "ws:"; - logger.info("Connecting to TrueNAS websocket at %s", wsUrl); const ws = new WebSocket(wsUrl, { rejectUnauthorized: false }); await waitForEvent(ws, () => true, { event: "open", parseJson: false }); // wait for open - logger.info("Connected to TrueNAS websocket at %s", wsUrl); try { await authenticate(ws, widget); data = await sendMethod(ws, wsMethod); @@ -166,7 +158,7 @@ export default async function truenasProxyHandler(req, res, map) { if (err?.status) { return res.status(err.status).json({ error: err.message }); } - logger.warn("Websocket call for TrueNAS failed: %s", err?.message ?? err); + logger.error("Websocket call for TrueNAS failed: %s", err?.message ?? err); return res.status(500).json({ error: err?.message ?? "TrueNAS websocket call failed" }); } } From 6b6090e303f2880100520a5d8fe7d0b28975769b Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Tue, 20 Jan 2026 16:43:01 -0800 Subject: [PATCH 03/19] Documentation: use blurred image for bkgd instead of filter --- docs/stylesheets/extra.css | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css index 7f299d2ec..43d9bb0a4 100644 --- a/docs/stylesheets/extra.css +++ b/docs/stylesheets/extra.css @@ -104,7 +104,7 @@ body { background-color: transparent !important; - background-image: url("https://raw.githubusercontent.com/gethomepage/homepage/main/docs/assets/blossom_valley.jpg"); + background-image: url("https://raw.githubusercontent.com/gethomepage/homepage/main/docs/assets/blossom_valley_blur.jpg"); background-size: cover; background-attachment: fixed; background-position: center; @@ -119,20 +119,6 @@ body[data-md-color-scheme="default"] { color: rgba(255, 255, 255, 1); } -.blur-overlay { - z-index: -1; - position: fixed; - width: 100%; - height: 100%; - background: hsl(0deg 0% 0% / 10%); - backdrop-filter: blur(128px); - -webkit-backdrop-filter: blur(128px); -} - -[data-md-color-scheme="default"] .blur-overlay { - background: hsla(0, 0%, 0%, 0); -} - .md-nav--lifted > .md-nav__list > .md-nav__item--active > .md-nav__link, .md-nav--secondary .md-nav__title { background: none; From 6c945d657377be163550abc72a9d3432a1255cae Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Fri, 23 Jan 2026 13:20:56 -0800 Subject: [PATCH 04/19] Feature: dockhand service widget (#6229) --- docs/widgets/services/dockhand.md | 20 +++++ docs/widgets/services/index.md | 1 + mkdocs.yml | 1 + public/locales/en/common.json | 14 ++++ src/utils/config/service-helpers.js | 6 ++ src/widgets/components.js | 1 + src/widgets/dockhand/component.jsx | 123 ++++++++++++++++++++++++++++ src/widgets/dockhand/proxy.js | 70 ++++++++++++++++ src/widgets/dockhand/widget.js | 14 ++++ src/widgets/widgets.js | 2 + 10 files changed, 252 insertions(+) create mode 100644 docs/widgets/services/dockhand.md create mode 100644 src/widgets/dockhand/component.jsx create mode 100644 src/widgets/dockhand/proxy.js create mode 100644 src/widgets/dockhand/widget.js diff --git a/docs/widgets/services/dockhand.md b/docs/widgets/services/dockhand.md new file mode 100644 index 000000000..7267d43bd --- /dev/null +++ b/docs/widgets/services/dockhand.md @@ -0,0 +1,20 @@ +--- +title: Dockhand +description: Dockhand Widget Configuration +--- + +Learn more about [Dockhand](https://dockhand.pro/). + +Note: The widget currently supports Dockhand's **local** authentication only. + +**Allowed fields:** (max 4): `running`, `stopped`, `paused`, `total`, `cpu`, `memory`, `images`, `volumes`, `events_today`, `pending_updates`, `stacks`. +**Default fields:** `running`, `total`, `cpu`, `memory`. + +```yaml +widget: + type: dockhand + url: http://localhost:3001 + environment: local # optional: name or id; aggregates all when omitted + username: your-user # required for local auth + password: your-pass # required for local auth +``` diff --git a/docs/widgets/services/index.md b/docs/widgets/services/index.md index 8110a7fc4..190dddec7 100644 --- a/docs/widgets/services/index.md +++ b/docs/widgets/services/index.md @@ -32,6 +32,7 @@ You can also find a list of all available service widgets in the sidebar navigat - [Deluge](deluge.md) - [DeveLanCacheUI](develancacheui.md) - [DiskStation](diskstation.md) +- [Dockhand](dockhand.md) - [DownloadStation](downloadstation.md) - [Emby](emby.md) - [ESPHome](esphome.md) diff --git a/mkdocs.yml b/mkdocs.yml index 2a21cbfc6..a7bc0d87b 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -56,6 +56,7 @@ nav: - widgets/services/deluge.md - widgets/services/develancacheui.md - widgets/services/diskstation.md + - widgets/services/dockhand.md - widgets/services/downloadstation.md - widgets/services/emby.md - widgets/services/esphome.md diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 7adcd4c12..8a34ae361 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -1136,5 +1136,19 @@ "songs": "Songs", "time": "Time", "artists": "Artists" + }, + "dockhand": { + "running": "Running", + "stopped": "Stopped", + "cpu": "CPU", + "memory": "Memory", + "images": "Images", + "volumes": "Volumes", + "events_today": "Events Today", + "pending_updates": "Pending Updates", + "stacks": "Stacks", + "paused": "Paused", + "total": "Total", + "environment_not_found": "Environment Not Found" } } diff --git a/src/utils/config/service-helpers.js b/src/utils/config/service-helpers.js index dc141cd91..d358f9d2f 100644 --- a/src/utils/config/service-helpers.js +++ b/src/utils/config/service-helpers.js @@ -298,6 +298,9 @@ export function cleanServiceGroups(groups) { container, server, + // dockhand + environment, + // emby, jellyfin enableBlocks, enableNowPlaying, @@ -605,6 +608,9 @@ export function cleanServiceGroups(groups) { if (showTime) widget.showTime = showTime; if (timezone) widget.timezone = timezone; } + if (type === "dockhand") { + if (environment) widget.environment = environment; + } if (type === "hdhomerun") { if (tuner !== undefined) widget.tuner = tuner; } diff --git a/src/widgets/components.js b/src/widgets/components.js index 30bafd50a..7cb304755 100644 --- a/src/widgets/components.js +++ b/src/widgets/components.js @@ -29,6 +29,7 @@ const components = { diskstation: dynamic(() => import("./diskstation/component")), downloadstation: dynamic(() => import("./downloadstation/component")), docker: dynamic(() => import("./docker/component")), + dockhand: dynamic(() => import("./dockhand/component")), kubernetes: dynamic(() => import("./kubernetes/component")), emby: dynamic(() => import("./emby/component")), esphome: dynamic(() => import("./esphome/component")), diff --git a/src/widgets/dockhand/component.jsx b/src/widgets/dockhand/component.jsx new file mode 100644 index 000000000..50ccff180 --- /dev/null +++ b/src/widgets/dockhand/component.jsx @@ -0,0 +1,123 @@ +import Block from "components/services/widget/block"; +import Container from "components/services/widget/container"; +import { useTranslation } from "next-i18next"; + +import useWidgetAPI from "utils/proxy/use-widget-api"; + +const MAX_FIELDS = 4; + +export default function Component({ service }) { + const { t } = useTranslation(); + const { widget } = service; + + if (!widget.fields) { + widget.fields = ["running", "total", "cpu", "memory"]; + } else if (widget.fields.length > MAX_FIELDS) { + widget.fields = widget.fields.slice(0, MAX_FIELDS); + } + + const { data: stats, error: statsError } = useWidgetAPI(widget, "dashboard/stats"); + + if (statsError) { + return ; + } + + if (!stats) { + return ( + + + + + + + + + + + + + + ); + } + + let running; + let stopped; + let paused; + let totalContainers; + let pendingUpdates; + let cpuPercent; + let memoryPercent; + let imagesTotal; + let volumesTotal; + let stacksRunning; + let stacksTotal; + let eventsToday; + + if (widget?.environment) { + const environment = stats.find( + (env) => + env?.name?.toString().toLowerCase() === widget?.environment.toString().toLowerCase() || + env?.id?.toString() === widget?.environment.toString(), + ); + if (environment) { + running = environment?.containers?.running; + stopped = environment?.containers?.stopped ?? (environment?.containers?.total ?? 0) - (running ?? 0); + paused = environment?.containers?.paused; + pendingUpdates = environment?.containers?.pendingUpdates; + totalContainers = environment?.containers?.total; + cpuPercent = environment?.metrics?.cpuPercent; + memoryPercent = environment?.metrics?.memoryPercent; + imagesTotal = environment?.images?.total; + volumesTotal = environment?.volumes?.total; + stacksRunning = environment?.stacks?.running; + stacksTotal = environment?.stacks?.total; + eventsToday = environment?.events?.today; + } else { + return ( + + ); + } + } + + if (running === undefined) { + // Aggregate across all environments + running = stats.reduce((sum, env) => sum + (env?.containers?.running ?? 0), 0); + totalContainers = stats.reduce((sum, env) => sum + (env?.containers?.total ?? 0), 0); + stopped = totalContainers - running; + paused = stats.reduce((sum, env) => sum + (env?.containers?.paused ?? 0), 0); + pendingUpdates = stats.reduce((sum, env) => sum + (env?.containers?.pendingUpdates ?? 0), 0); + const totalCpu = stats.reduce((sum, env) => sum + (env?.metrics?.cpuPercent ?? 0), 0); + const totalMemory = stats.reduce((sum, env) => sum + (env?.metrics?.memoryPercent ?? 0), 0); + const envCount = stats.length; + cpuPercent = envCount > 0 ? totalCpu / envCount : 0; + memoryPercent = envCount > 0 ? totalMemory / envCount : 0; + imagesTotal = stats.reduce((sum, env) => sum + (env?.images?.total ?? 0), 0); + volumesTotal = stats.reduce((sum, env) => sum + (env?.volumes?.total ?? 0), 0); + stacksRunning = stats.reduce((sum, env) => sum + (env?.stacks?.running ?? 0), 0); + stacksTotal = stats.reduce((sum, env) => sum + (env?.stacks?.total ?? 0), 0); + eventsToday = stats.reduce((sum, env) => sum + (env?.events?.today ?? 0), 0); + } + + return ( + + + + + + + + + + + + + + ); +} diff --git a/src/widgets/dockhand/proxy.js b/src/widgets/dockhand/proxy.js new file mode 100644 index 000000000..c93e7bf39 --- /dev/null +++ b/src/widgets/dockhand/proxy.js @@ -0,0 +1,70 @@ +import getServiceWidget from "utils/config/service-helpers"; +import createLogger from "utils/logger"; +import { formatApiCall, sanitizeErrorURL } from "utils/proxy/api-helpers"; +import { httpProxy } from "utils/proxy/http"; +import widgets from "widgets/widgets"; + +const logger = createLogger("dockhandProxyHandler"); + +async function login(widget) { + if (!widget.username || !widget.password) return false; + + const baseUrl = widget.url?.replace(/\/+$/, ""); + const [status] = await httpProxy(`${baseUrl}/api/auth/login`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ username: widget.username, password: widget.password }), + }); + + return status === 200; +} + +export default async function dockhandProxyHandler(req, res) { + const { group, service, endpoint, index } = req.query; + + if (!group || !service) { + logger.debug("Invalid or missing service '%s' or group '%s'", service, group); + return res.status(400).json({ error: "Invalid proxy service type" }); + } + + const widget = await getServiceWidget(group, service, index); + + if (!widget) { + logger.debug("Invalid or missing widget for service '%s' in group '%s'", service, group); + return res.status(400).json({ error: "Invalid proxy service type" }); + } + + const url = new URL(formatApiCall(widgets[widget.type].api, { endpoint, ...widget })); + + let [status, contentType, data] = await httpProxy(url, { + method: req.method, + }); + + // Attempt login and retrying once + if (status === 401) { + const loggedIn = await login(widget); + if (loggedIn) { + [status, contentType, data] = await httpProxy(url, { + method: req.method, + }); + } + } + + let resultData = data; + + if (status >= 400) { + logger.error("HTTP Error %d calling %s", status, url.toString()); + return res.status(status).json({ + error: { + message: "HTTP Error", + url: sanitizeErrorURL(url), + data: Buffer.isBuffer(resultData) ? Buffer.from(resultData).toString() : resultData, + }, + }); + } + + if (contentType) res.setHeader("Content-Type", contentType); + return res.status(status).send(resultData); +} diff --git a/src/widgets/dockhand/widget.js b/src/widgets/dockhand/widget.js new file mode 100644 index 000000000..c4279a12a --- /dev/null +++ b/src/widgets/dockhand/widget.js @@ -0,0 +1,14 @@ +import dockhandProxyHandler from "./proxy"; + +const widget = { + api: "{url}/api/{endpoint}", + proxyHandler: dockhandProxyHandler, + + mappings: { + "dashboard/stats": { + endpoint: "dashboard/stats", + }, + }, +}; + +export default widget; diff --git a/src/widgets/widgets.js b/src/widgets/widgets.js index f7947de78..eeeae0cc1 100644 --- a/src/widgets/widgets.js +++ b/src/widgets/widgets.js @@ -23,6 +23,7 @@ import customapi from "./customapi/widget"; import deluge from "./deluge/widget"; import develancacheui from "./develancacheui/widget"; import diskstation from "./diskstation/widget"; +import dockhand from "./dockhand/widget"; import downloadstation from "./downloadstation/widget"; import emby from "./emby/widget"; import esphome from "./esphome/widget"; @@ -171,6 +172,7 @@ const widgets = { deluge, develancacheui, diskstation, + dockhand, downloadstation, emby, esphome, From 1aec61811fae27a98f1a028801ba493bc1b9c125 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Mon, 26 Jan 2026 10:27:36 -0800 Subject: [PATCH 05/19] Enhancement: handle Vikunja v1rc4 breaking changes (#6234) --- docs/widgets/services/vikunja.md | 6 ++++++ src/utils/config/service-helpers.js | 1 + src/widgets/vikunja/component.jsx | 6 +++++- src/widgets/vikunja/widget.js | 23 ++++++++++++++--------- 4 files changed, 26 insertions(+), 10 deletions(-) diff --git a/docs/widgets/services/vikunja.md b/docs/widgets/services/vikunja.md index 94b990557..048a19c05 100644 --- a/docs/widgets/services/vikunja.md +++ b/docs/widgets/services/vikunja.md @@ -9,10 +9,16 @@ Allowed fields: `["projects", "tasks7d", "tasksOverdue", "tasksInProgress"]`. A list of the next 5 tasks ordered by due date is disabled by default, but can be enabled with the `enableTaskList` option. +| Vikunja Version | Homepage Widget Version | +| --------------- | ----------------------- | +| < v1.0.0-rc4 | 1 (default) | +| >= v1.0.0-rc4 | 2 | + ```yaml widget: type: vikunja url: http[s]://vikunja.host.or.ip[:port] key: vikunjaapikey enableTaskList: true # optional, defaults to false + version: 2 # optional, defaults to 1 ``` diff --git a/src/utils/config/service-helpers.js b/src/utils/config/service-helpers.js index d358f9d2f..95cdc7538 100644 --- a/src/utils/config/service-helpers.js +++ b/src/utils/config/service-helpers.js @@ -569,6 +569,7 @@ export function cleanServiceGroups(groups) { "wgeasy", "grafana", "gluetun", + "vikunja", ].includes(type) ) { if (version) widget.version = parseInt(version, 10); diff --git a/src/widgets/vikunja/component.jsx b/src/widgets/vikunja/component.jsx index 1afccd38d..9aae02da0 100644 --- a/src/widgets/vikunja/component.jsx +++ b/src/widgets/vikunja/component.jsx @@ -8,11 +8,15 @@ export default function Component({ service }) { const { t } = useTranslation(); const { widget } = service; + const version = widget.version ?? 1; + const { data: projectsData, error: projectsError } = useWidgetAPI(widget, "projects"); - const { data: tasksData, error: tasksError } = useWidgetAPI(widget, "tasks"); + const { data: tasksData, error: tasksError } = useWidgetAPI(widget, version === 2 ? "tasks_v2" : "tasks"); if (projectsError || tasksError) { return ; + } else if (projectsData?.message || tasksData?.message) { + return ; } if (!projectsData || !tasksData) { diff --git a/src/widgets/vikunja/widget.js b/src/widgets/vikunja/widget.js index 8e5e680a7..147e4a141 100644 --- a/src/widgets/vikunja/widget.js +++ b/src/widgets/vikunja/widget.js @@ -1,6 +1,15 @@ import { asJson } from "utils/proxy/api-helpers"; import credentialedProxyHandler from "utils/proxy/handlers/credentialed"; +const map = asJson(data).map((task) => ({ + id: task.id, + title: task.title, + priority: task.priority, + dueDate: task.due_date, + dueDateIsDefault: task.due_date === "0001-01-01T00:00:00Z", + inProgress: task.percent_done > 0 && task.percent_done < 1, +})); + const widget = { api: `{url}/api/v1/{endpoint}`, proxyHandler: credentialedProxyHandler, @@ -11,15 +20,11 @@ const widget = { }, tasks: { endpoint: "tasks/all?filter=done%3Dfalse&sort_by=due_date", - map: (data) => - asJson(data).map((task) => ({ - id: task.id, - title: task.title, - priority: task.priority, - dueDate: task.due_date, - dueDateIsDefault: task.due_date === "0001-01-01T00:00:00Z", - inProgress: task.percent_done > 0 && task.percent_done < 1, - })), + map: map, + }, + tasks_v2: { + endpoint: "tasks?filter=done%3Dfalse&sort_by=due_date", + map: map, }, }, }; From ca9506e485246108999070c5a5611b802bd15f74 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Tue, 27 Jan 2026 07:18:24 -0800 Subject: [PATCH 06/19] Fix vikunja map function --- src/widgets/vikunja/widget.js | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/widgets/vikunja/widget.js b/src/widgets/vikunja/widget.js index 147e4a141..055d28751 100644 --- a/src/widgets/vikunja/widget.js +++ b/src/widgets/vikunja/widget.js @@ -1,14 +1,15 @@ import { asJson } from "utils/proxy/api-helpers"; import credentialedProxyHandler from "utils/proxy/handlers/credentialed"; -const map = asJson(data).map((task) => ({ - id: task.id, - title: task.title, - priority: task.priority, - dueDate: task.due_date, - dueDateIsDefault: task.due_date === "0001-01-01T00:00:00Z", - inProgress: task.percent_done > 0 && task.percent_done < 1, -})); +const map = (data) => + asJson(data).map((task) => ({ + id: task.id, + title: task.title, + priority: task.priority, + dueDate: task.due_date, + dueDateIsDefault: task.due_date === "0001-01-01T00:00:00Z", + inProgress: task.percent_done > 0 && task.percent_done < 1, + })); const widget = { api: `{url}/api/v1/{endpoint}`, From c86a007ed007a6cbce4a686db5e2e29ec1925fbe Mon Sep 17 00:00:00 2001 From: Kristiyan Nikolov <1583751+kpau@users.noreply.github.com> Date: Wed, 28 Jan 2026 20:36:17 +0200 Subject: [PATCH 07/19] Enhancement: Add support for PWA icons and shortcuts (#6235) Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com> --- docs/configs/settings.md | 52 ++++++++++++++++++++++++++++++++++ src/pages/site.webmanifest.jsx | 5 +++- 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/docs/configs/settings.md b/docs/configs/settings.md index a3ab2981d..192e75cc9 100644 --- a/docs/configs/settings.md +++ b/docs/configs/settings.md @@ -123,6 +123,58 @@ blockHighlights: Any unspecified level falls back to the built-in defaults. +## Progressive Web App (PWA) + +A progressive web app is an app that can be installed on a device and provide user experience like a native app. Homepage comes with built-in support for PWA with some default configurations, but you can customize them. + +More information on PWAs can be found in [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps). + +## App icons + +You can set custom icons for installable apps. More information about how you can set them can be found in the [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Manifest/Reference/icons). + +The default value is the Homepage icon in sizes 192x192 and 512x512. + +```yaml +pwa: + icons: + - src: https://developer.mozilla.org/favicon-192x192.png + type: image/png + sizes: 192x192 + - src: https://developer.mozilla.org/favicon-512x512.png + type: image/png + sizes: 512x512 +``` + +For icon `src` you can pass either full URL or a local path relative to the `/app/public` directory. See [Background Image](#background-image) for more detailed information on how to provide your own files. + +### Shortcuts + +Shortcuts can e used to specify links to tabs, to be preselected when the homepage is opened as an app. +More information about how you can set them can be found in the [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Manifest/Reference/shortcuts). + +```yaml +pwa: + shortcuts: + - name: First + url: "/#first" # opens the first tab + - name: Second + url: "/#second" # opens the second tab + - name: Third + url: "/#third" # opens the third tab +``` + +### Other PWA configurations + +Homepage sets few other PWA configurations, that are based on global settings in `settings.yaml`: + +- `name`, `short_name` - Both equal to the [`title`](#title) setting. +- `theme_color`, `background_color` - Both based on the [`color`](#color-palette) and [`theme`](#theme) settings. +- `display` - It is always set to "standalone". +- `start_url` - Equal to the [`startUrl`](#start-url) setting. + +More information for wach of the PWA configurations can be found in the [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Manifest/Reference). + ## Layout You can configure service and bookmarks sections to be either "column" or "row" based layouts, like so: diff --git a/src/pages/site.webmanifest.jsx b/src/pages/site.webmanifest.jsx index 93dfdef55..bfaa92a27 100644 --- a/src/pages/site.webmanifest.jsx +++ b/src/pages/site.webmanifest.jsx @@ -8,10 +8,12 @@ export async function getServerSideProps({ res }) { const color = settings.color || "slate"; const theme = settings.theme || "dark"; + const pwa = settings.pwa || {}; + const manifest = { name: settings.title || "Homepage", short_name: settings.title || "Homepage", - icons: [ + icons: pwa.icons || [ { src: "/android-chrome-192x192.png?v=2", sizes: "192x192", @@ -23,6 +25,7 @@ export async function getServerSideProps({ res }) { type: "image/png", }, ], + shortcuts: pwa.shortcuts, theme_color: themes[color][theme], background_color: themes[color][theme], display: "standalone", From 79b63e4099064935d2391a6addc4baea48b26446 Mon Sep 17 00:00:00 2001 From: muertocaloh <9284052+muertocaloh@users.noreply.github.com> Date: Thu, 29 Jan 2026 20:16:07 +0100 Subject: [PATCH 08/19] Feature: Dispatcharr widget (#6035) Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com> --- docs/widgets/services/dispatcharr.md | 17 ++++ docs/widgets/services/index.md | 1 + mkdocs.yml | 1 + public/locales/en/common.json | 4 + src/utils/config/service-helpers.js | 6 ++ src/widgets/components.js | 1 + src/widgets/dispatcharr/component.jsx | 61 +++++++++++++ src/widgets/dispatcharr/proxy.js | 119 ++++++++++++++++++++++++++ src/widgets/dispatcharr/widget.js | 20 +++++ src/widgets/widgets.js | 2 + 10 files changed, 232 insertions(+) create mode 100644 docs/widgets/services/dispatcharr.md create mode 100644 src/widgets/dispatcharr/component.jsx create mode 100644 src/widgets/dispatcharr/proxy.js create mode 100644 src/widgets/dispatcharr/widget.js diff --git a/docs/widgets/services/dispatcharr.md b/docs/widgets/services/dispatcharr.md new file mode 100644 index 000000000..f31fbcdaf --- /dev/null +++ b/docs/widgets/services/dispatcharr.md @@ -0,0 +1,17 @@ +--- +title: Dispatcharr +description: Dispatcharr Widget Configuration +--- + +Learn more about [Dispatcharr](https://github.com/Dispatcharr/Dispatcharr). + +Allowed fields: `["channels", "streams"]`. + +```yaml +widget: + type: dispatcharr + url: http://dispatcharr.host.or.ip + username: username + password: password + enableActiveStreams: true # optional, defaults to false +``` diff --git a/docs/widgets/services/index.md b/docs/widgets/services/index.md index 190dddec7..8b43802e9 100644 --- a/docs/widgets/services/index.md +++ b/docs/widgets/services/index.md @@ -32,6 +32,7 @@ You can also find a list of all available service widgets in the sidebar navigat - [Deluge](deluge.md) - [DeveLanCacheUI](develancacheui.md) - [DiskStation](diskstation.md) +- [Dispatcharr](dispatcharr.md) - [Dockhand](dockhand.md) - [DownloadStation](downloadstation.md) - [Emby](emby.md) diff --git a/mkdocs.yml b/mkdocs.yml index a7bc0d87b..30dad803f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -56,6 +56,7 @@ nav: - widgets/services/deluge.md - widgets/services/develancacheui.md - widgets/services/diskstation.md + - widgets/services/dispatcharr.md - widgets/services/dockhand.md - widgets/services/downloadstation.md - widgets/services/emby.md diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 8a34ae361..c002e2771 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -705,6 +705,10 @@ "uptime": "Uptime", "volumeAvailable": "Available" }, + "dispatcharr": { + "channels": "Channels", + "streams": "Streams" + }, "mylar": { "series": "Series", "issues": "Issues", diff --git a/src/utils/config/service-helpers.js b/src/utils/config/service-helpers.js index 95cdc7538..42e85243b 100644 --- a/src/utils/config/service-helpers.js +++ b/src/utils/config/service-helpers.js @@ -294,6 +294,9 @@ export function cleanServiceGroups(groups) { // diskstation volume, + // dispatcharr + enableActiveStreams, + // docker container, server, @@ -547,6 +550,9 @@ export function cleanServiceGroups(groups) { if (["diskstation", "qnap"].includes(type)) { if (volume) widget.volume = volume; } + if (["dispatcharr"].includes(type)) { + if (enableActiveStreams) widget.enableActiveStreams = !!JSON.parse(enableActiveStreams); + } if (type === "gamedig") { if (gameToken) widget.gameToken = gameToken; } diff --git a/src/widgets/components.js b/src/widgets/components.js index 7cb304755..e69a985c5 100644 --- a/src/widgets/components.js +++ b/src/widgets/components.js @@ -27,6 +27,7 @@ const components = { deluge: dynamic(() => import("./deluge/component")), develancacheui: dynamic(() => import("./develancacheui/component")), diskstation: dynamic(() => import("./diskstation/component")), + dispatcharr: dynamic(() => import("./dispatcharr/component")), downloadstation: dynamic(() => import("./downloadstation/component")), docker: dynamic(() => import("./docker/component")), dockhand: dynamic(() => import("./dockhand/component")), diff --git a/src/widgets/dispatcharr/component.jsx b/src/widgets/dispatcharr/component.jsx new file mode 100644 index 000000000..29a6c0936 --- /dev/null +++ b/src/widgets/dispatcharr/component.jsx @@ -0,0 +1,61 @@ +import Block from "components/services/widget/block"; +import Container from "components/services/widget/container"; +import { useTranslation } from "next-i18next"; + +import useWidgetAPI from "utils/proxy/use-widget-api"; + +function StreamEntry({ title, clients, bitrate }) { + return ( +
+
+
+ {title} - Clients: {clients} +
+
+
+ {bitrate} +
+
+ ); +} + +export default function Component({ service }) { + const { t } = useTranslation(); + + const { widget } = service; + + const { data: channels, error: channelsError } = useWidgetAPI(widget, "channels"); + const { data: streams, error: streamsError } = useWidgetAPI(widget, "streams"); + + if (channelsError || streamsError) { + return ; + } + + if (!channels || !streams) { + return ( + + + + + ); + } + + return ( + <> + + + + + {widget?.enableActiveStreams && + streams?.channels && + streams.channels.map((activeStream) => ( + + ))} + + ); +} diff --git a/src/widgets/dispatcharr/proxy.js b/src/widgets/dispatcharr/proxy.js new file mode 100644 index 000000000..45175983a --- /dev/null +++ b/src/widgets/dispatcharr/proxy.js @@ -0,0 +1,119 @@ +import cache from "memory-cache"; + +import getServiceWidget from "utils/config/service-helpers"; +import createLogger from "utils/logger"; +import { formatApiCall } from "utils/proxy/api-helpers"; +import { httpProxy } from "utils/proxy/http"; +import widgets from "widgets/widgets"; + +const proxyName = "dispatcharrProxyHandler"; +const tokenCacheKey = `${proxyName}__token`; +const logger = createLogger(proxyName); + +async function login(loginUrl, username, password, service) { + const authResponse = await httpProxy(loginUrl, { + method: "POST", + body: JSON.stringify({ username, password }), + headers: { + "Content-Type": "application/json", + }, + }); + + const status = authResponse[0]; + let data = authResponse[2]; + try { + data = JSON.parse(Buffer.from(authResponse[2]).toString()); + + if (status === 200) { + cache.put(`${tokenCacheKey}.${service}`, data.access); + } else { + throw new Error(`HTTP ${status} logging into dispatcharr`); + } + } catch (e) { + logger.error(`Error ${status} logging into dispatcharr`, JSON.stringify(data)); + return [status, null]; + } + return [status, data.access]; +} + +export default async function dispatcharrProxyHandler(req, res) { + const { group, service, endpoint, index } = req.query; + + if (group && service) { + const widget = await getServiceWidget(group, service, index); + + if (!widgets?.[widget.type]?.api) { + return res.status(403).json({ error: "Service does not support API calls" }); + } + + if (widget) { + const url = new URL(formatApiCall(widgets[widget.type].api, { endpoint, ...widget })); + const loginUrl = formatApiCall(widgets[widget.type].api, { + endpoint: widgets[widget.type].mappings["token"].endpoint, + ...widget, + }); + + let status; + let data; + + let token = cache.get(`${tokenCacheKey}.${service}`); + if (!token) { + [status, token] = await login(loginUrl, widget.username, widget.password, service); + if (!token) { + logger.debug(`HTTP ${status} logging into Dispatcharr}`); + return res.status(status).send({ error: "Failed to authenticate with Dispatcharr" }); + } + } + + [status, , data] = await httpProxy(url, { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + }); + + const badRequest = [400, 401, 403].includes(status); + let isEmpty = false; + + try { + const json = JSON.parse(data.toString("utf-8")); + isEmpty = Array.isArray(json.items) && json.items.length === 0; + } catch (err) { + logger.error("Failed to parse Dispatcharr response JSON:", err); + } + + if (badRequest || isEmpty) { + if (badRequest) { + logger.debug(`HTTP ${status} retrieving data from Dispatcharr, logging in and trying again.`); + } else { + logger.debug(`Received empty list from Dispatcharr, logging in and trying again.`); + } + cache.del(`${tokenCacheKey}.${service}`); + [status, token] = await login(loginUrl, widget.username, widget.password, service); + + if (status !== 200) { + logger.debug(`HTTP ${status} logging into Dispatcharr: ${JSON.stringify(data)}`); + return res.status(status).send(data); + } + + // eslint-disable-next-line no-unused-vars + [status, , data] = await httpProxy(url, { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + }); + } + + if (status !== 200) { + return res.status(status).send(data); + } + + return res.send(data); + } + } + + return res.status(400).json({ error: "Invalid proxy service type" }); +} diff --git a/src/widgets/dispatcharr/widget.js b/src/widgets/dispatcharr/widget.js new file mode 100644 index 000000000..6e69b7aff --- /dev/null +++ b/src/widgets/dispatcharr/widget.js @@ -0,0 +1,20 @@ +import dispatcharrProxyHandler from "./proxy"; + +const widget = { + api: "{url}/{endpoint}", + proxyHandler: dispatcharrProxyHandler, + + mappings: { + token: { + endpoint: "api/accounts/token/", + }, + channels: { + endpoint: "api/channels/channels/", + }, + streams: { + endpoint: "proxy/ts/status", + }, + }, +}; + +export default widget; diff --git a/src/widgets/widgets.js b/src/widgets/widgets.js index eeeae0cc1..2029a82c6 100644 --- a/src/widgets/widgets.js +++ b/src/widgets/widgets.js @@ -23,6 +23,7 @@ import customapi from "./customapi/widget"; import deluge from "./deluge/widget"; import develancacheui from "./develancacheui/widget"; import diskstation from "./diskstation/widget"; +import dispatcharr from "./dispatcharr/widget"; import dockhand from "./dockhand/widget"; import downloadstation from "./downloadstation/widget"; import emby from "./emby/widget"; @@ -172,6 +173,7 @@ const widgets = { deluge, develancacheui, diskstation, + dispatcharr, dockhand, downloadstation, emby, From 4ebc24a1b45136cf6598ca678dc917247f698d00 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Fri, 30 Jan 2026 22:05:19 -0800 Subject: [PATCH 09/19] Enhancement: support jellyfin 10.12 breaking API changes (#6252) --- docs/widgets/services/jellyfin.md | 6 + public/locales/en/common.json | 10 + src/utils/config/service-helpers.js | 1 + src/widgets/components.js | 2 +- src/widgets/emby/component.jsx | 3 - src/widgets/jellyfin/component.jsx | 345 ++++++++++++++++++++++++++++ src/widgets/jellyfin/proxy.js | 69 ++++++ src/widgets/jellyfin/widget.js | 43 ++++ src/widgets/widgets.js | 3 +- 9 files changed, 477 insertions(+), 5 deletions(-) create mode 100644 src/widgets/jellyfin/component.jsx create mode 100644 src/widgets/jellyfin/proxy.js create mode 100644 src/widgets/jellyfin/widget.js diff --git a/docs/widgets/services/jellyfin.md b/docs/widgets/services/jellyfin.md index 667930e3c..8849e0645 100644 --- a/docs/widgets/services/jellyfin.md +++ b/docs/widgets/services/jellyfin.md @@ -9,11 +9,17 @@ You can create an API key from inside Jellyfin at `Settings > Advanced > Api Key As of v0.6.11 the widget supports fields `["movies", "series", "episodes", "songs"]`. These blocks are disabled by default but can be enabled with the `enableBlocks` option, and the "Now Playing" feature (enabled by default) can be disabled with the `enableNowPlaying` option. +| Jellyfin Version | Homepage Widget Version | +| ---------------- | ----------------------- | +| < 10.12 | 1 (default) | +| >= 10.12 | 2 | + ```yaml widget: type: jellyfin url: http://jellyfin.host.or.ip key: apikeyapikeyapikeyapikeyapikey + version: 2 # optional, default is 1 enableBlocks: true # optional, defaults to false enableNowPlaying: true # optional, defaults to true enableUser: true # optional, defaults to false diff --git a/public/locales/en/common.json b/public/locales/en/common.json index c002e2771..2f655bf32 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -107,6 +107,16 @@ "episodes": "Episodes", "songs": "Songs" }, + "jellyfin": { + "playing": "Playing", + "transcoding": "Transcoding", + "bitrate": "Bitrate", + "no_active": "No Active Streams", + "movies": "Movies", + "series": "Series", + "episodes": "Episodes", + "songs": "Songs" + }, "esphome": { "offline": "Offline", "offline_alt": "Offline", diff --git a/src/utils/config/service-helpers.js b/src/utils/config/service-helpers.js index 42e85243b..75740c3ef 100644 --- a/src/utils/config/service-helpers.js +++ b/src/utils/config/service-helpers.js @@ -566,6 +566,7 @@ export function cleanServiceGroups(groups) { "beszel", "glances", "immich", + "jellyfin", "komga", "mealie", "netalertx", diff --git a/src/widgets/components.js b/src/widgets/components.js index e69a985c5..c114a82a5 100644 --- a/src/widgets/components.js +++ b/src/widgets/components.js @@ -63,7 +63,7 @@ const components = { immich: dynamic(() => import("./immich/component")), jackett: dynamic(() => import("./jackett/component")), jdownloader: dynamic(() => import("./jdownloader/component")), - jellyfin: dynamic(() => import("./emby/component")), + jellyfin: dynamic(() => import("./jellyfin/component")), jellyseerr: dynamic(() => import("./jellyseerr/component")), jellystat: dynamic(() => import("./jellystat/component")), kavita: dynamic(() => import("./kavita/component")), diff --git a/src/widgets/emby/component.jsx b/src/widgets/emby/component.jsx index b03a00415..39a347185 100644 --- a/src/widgets/emby/component.jsx +++ b/src/widgets/emby/component.jsx @@ -176,9 +176,6 @@ function SessionEntry({ playCommand, session, enableUser, showEpisodeNumber, ena function CountBlocks({ service, countData }) { const { t } = useTranslation(); - // allows filtering - // eslint-disable-next-line no-param-reassign - if (service.widget?.type === "jellyfin") service.widget.type = "emby"; if (!countData) { return ( diff --git a/src/widgets/jellyfin/component.jsx b/src/widgets/jellyfin/component.jsx new file mode 100644 index 000000000..591ee73de --- /dev/null +++ b/src/widgets/jellyfin/component.jsx @@ -0,0 +1,345 @@ +import Block from "components/services/widget/block"; +import Container from "components/services/widget/container"; +import { useTranslation } from "next-i18next"; +import { BsCpu, BsFillCpuFill, BsFillPlayFill, BsPauseFill, BsVolumeMuteFill } from "react-icons/bs"; +import { MdOutlineSmartDisplay } from "react-icons/md"; + +import { getURLSearchParams } from "utils/proxy/api-helpers"; +import useWidgetAPI from "utils/proxy/use-widget-api"; + +function ticksToTime(ticks) { + const milliseconds = ticks / 10000; + const seconds = Math.floor((milliseconds / 1000) % 60); + const minutes = Math.floor((milliseconds / (1000 * 60)) % 60); + const hours = Math.floor((milliseconds / (1000 * 60 * 60)) % 24); + return { hours, minutes, seconds }; +} + +function ticksToString(ticks) { + const { hours, minutes, seconds } = ticksToTime(ticks); + const parts = []; + if (hours > 0) { + parts.push(hours); + } + parts.push(minutes); + parts.push(seconds); + + return parts.map((part) => part.toString().padStart(2, "0")).join(":"); +} + +function generateStreamTitle(session, enableUser, showEpisodeNumber) { + const { + NowPlayingItem: { Name, SeriesName, Type, ParentIndexNumber, IndexNumber, AlbumArtist, Album }, + UserName, + } = session; + let streamTitle = ""; + + if (Type === "Episode" && showEpisodeNumber) { + const seasonStr = ParentIndexNumber ? `S${ParentIndexNumber.toString().padStart(2, "0")}` : ""; + const episodeStr = IndexNumber ? `E${IndexNumber.toString().padStart(2, "0")}` : ""; + streamTitle = `${SeriesName}: ${seasonStr} ยท ${episodeStr} - ${Name}`; + } else if (Type === "Audio") { + streamTitle = `${AlbumArtist} - ${Album} - ${Name}`; + } else { + streamTitle = `${Name}${SeriesName ? ` - ${SeriesName}` : ""}`; + } + + return enableUser ? `${streamTitle} (${UserName})` : streamTitle; +} + +function SingleSessionEntry({ playCommand, session, enableUser, showEpisodeNumber, enableMediaControl }) { + const { + PlayState: { PositionTicks, IsPaused, IsMuted }, + } = session; + + const RunTimeTicks = + session.NowPlayingItem?.RunTimeTicks ?? session.NowPlayingItem?.CurrentProgram?.RunTimeTicks ?? 0; + + const { IsVideoDirect, VideoDecoderIsHardware, VideoEncoderIsHardware } = session?.TranscodingInfo || { + IsVideoDirect: true, + }; // if no transcodinginfo its videodirect + + const percent = Math.min(1, PositionTicks / RunTimeTicks) * 100; + + const streamTitle = generateStreamTitle(session, enableUser, showEpisodeNumber); + return ( + <> +
+
+
+ {streamTitle} +
+
+
+ {IsVideoDirect && } + {!IsVideoDirect && (!VideoDecoderIsHardware || !VideoEncoderIsHardware) && } + {!IsVideoDirect && VideoDecoderIsHardware && VideoEncoderIsHardware && ( + + )} +
+
+ +
+
+
+ {enableMediaControl && IsPaused && ( + { + playCommand(session, "Unpause"); + }} + className="inline-block w-4 h-4 cursor-pointer -mt-[1px] mr-1 opacity-80" + /> + )} + {enableMediaControl && !IsPaused && ( + { + playCommand(session, "Pause"); + }} + className="inline-block w-4 h-4 cursor-pointer -mt-[1px] mr-1 opacity-80" + /> + )} +
+
+
{IsMuted && }
+
+ {ticksToString(PositionTicks)} + / + {ticksToString(RunTimeTicks)} +
+
+ + ); +} + +function SessionEntry({ playCommand, session, enableUser, showEpisodeNumber, enableMediaControl }) { + const { + PlayState: { PositionTicks, IsPaused, IsMuted }, + } = session; + + const RunTimeTicks = + session.NowPlayingItem?.RunTimeTicks ?? session.NowPlayingItem?.CurrentProgram?.RunTimeTicks ?? 0; + + const { IsVideoDirect, VideoDecoderIsHardware, VideoEncoderIsHardware } = session?.TranscodingInfo || { + IsVideoDirect: true, + }; // if no transcodinginfo its videodirect + + const streamTitle = generateStreamTitle(session, enableUser, showEpisodeNumber); + + const percent = Math.min(1, PositionTicks / RunTimeTicks) * 100; + + return ( +
+
+
+ {enableMediaControl && IsPaused && ( + { + playCommand(session, "Unpause"); + }} + className="inline-block w-4 h-4 cursor-pointer -mt-[1px] mr-1 opacity-80" + /> + )} + {enableMediaControl && !IsPaused && ( + { + playCommand(session, "Pause"); + }} + className="inline-block w-4 h-4 cursor-pointer -mt-[1px] mr-1 opacity-80" + /> + )} +
+
+
+ {streamTitle} +
+
+
{IsMuted && }
+
{ticksToString(PositionTicks)}
+
+ {IsVideoDirect && } + {!IsVideoDirect && (!VideoDecoderIsHardware || !VideoEncoderIsHardware) && } + {!IsVideoDirect && VideoDecoderIsHardware && VideoEncoderIsHardware && } +
+
+ ); +} + +function CountBlocks({ service, countData }) { + const { t } = useTranslation(); + + if (!countData) { + return ( + + + + + + + ); + } + + return ( + + + + + + + ); +} + +export default function Component({ service }) { + const { t } = useTranslation(); + + const { widget } = service; + const version = widget?.version ?? 1; + const useJellyfinV2 = version === 2; + const sessionsEndpoint = useJellyfinV2 ? "SessionsV2" : "Sessions"; + const countEndpoint = useJellyfinV2 ? "CountV2" : "Count"; + const commandMap = { + Pause: useJellyfinV2 ? "PauseV2" : "Pause", + Unpause: useJellyfinV2 ? "UnpauseV2" : "Unpause", + }; + const enableNowPlaying = service.widget?.enableNowPlaying ?? true; + + const { + data: sessionsData, + error: sessionsError, + mutate: sessionMutate, + } = useWidgetAPI(widget, enableNowPlaying ? sessionsEndpoint : "", { + refreshInterval: enableNowPlaying ? 5000 : undefined, + }); + + const { data: countData, error: countError } = useWidgetAPI(widget, countEndpoint, { + refreshInterval: 60000, + }); + + async function handlePlayCommand(session, command) { + const mappedCommand = commandMap[command] ?? command; + const params = getURLSearchParams(widget, mappedCommand); + params.append( + "segments", + JSON.stringify({ + sessionId: session.Id, + }), + ); + const url = `/api/services/proxy?${params.toString()}`; + await fetch(url, { + method: "POST", + }).then(() => { + sessionMutate(); + }); + } + + if (sessionsError || countError) { + return ; + } + + const enableBlocks = service.widget?.enableBlocks; + const enableMediaControl = service.widget?.enableMediaControl !== false; // default is true + const enableUser = !!service.widget?.enableUser; // default is false + const expandOneStreamToTwoRows = service.widget?.expandOneStreamToTwoRows !== false; // default is true + const showEpisodeNumber = !!service.widget?.showEpisodeNumber; // default is false + + if ((enableNowPlaying && !sessionsData) || !countData) { + return ( + <> + {enableBlocks && } + {enableNowPlaying && ( +
+
+ - +
+ {expandOneStreamToTwoRows && ( +
+ - +
+ )} +
+ )} + + ); + } + + if (enableNowPlaying) { + const playing = sessionsData + .filter((session) => session?.NowPlayingItem) + .sort((a, b) => { + if (a.PlayState.PositionTicks > b.PlayState.PositionTicks) { + return 1; + } + if (a.PlayState.PositionTicks < b.PlayState.PositionTicks) { + return -1; + } + return 0; + }); + + if (playing.length === 0) { + return ( + <> + {enableBlocks && } +
+
+ {t("jellyfin.no_active")} +
+ {expandOneStreamToTwoRows && ( +
+ - +
+ )} +
+ + ); + } + + if (expandOneStreamToTwoRows && playing.length === 1) { + const session = playing[0]; + return ( + <> + {enableBlocks && } +
+ handlePlayCommand(currentSession, command)} + session={session} + enableUser={enableUser} + showEpisodeNumber={showEpisodeNumber} + enableMediaControl={enableMediaControl} + /> +
+ + ); + } + + return ( + <> + {enableBlocks && } +
+ {playing.map((session) => ( + handlePlayCommand(currentSession, command)} + session={session} + enableUser={enableUser} + showEpisodeNumber={showEpisodeNumber} + enableMediaControl={enableMediaControl} + /> + ))} +
+ + ); + } + + if (enableBlocks) { + return ; + } +} diff --git a/src/widgets/jellyfin/proxy.js b/src/widgets/jellyfin/proxy.js new file mode 100644 index 000000000..28f57d5ce --- /dev/null +++ b/src/widgets/jellyfin/proxy.js @@ -0,0 +1,69 @@ +import getServiceWidget from "utils/config/service-helpers"; +import createLogger from "utils/logger"; +import { formatApiCall, sanitizeErrorURL } from "utils/proxy/api-helpers"; +import { httpProxy } from "utils/proxy/http"; +import validateWidgetData from "utils/proxy/validate-widget-data"; +import widgets from "widgets/widgets"; + +const logger = createLogger("jellyfinProxyHandler"); + +export default async function jellyfinProxyHandler(req, res, map) { + const { group, service, endpoint, index } = req.query; + + if (!group || !service) { + logger.debug("Invalid or missing proxy service type '%s' in group '%s'", service, group); + return res.status(400).json({ error: "Invalid proxy service type" }); + } + + const widget = await getServiceWidget(group, service, index); + + if (!widget || !widgets?.[widget.type]?.api) { + logger.debug("Invalid or missing proxy service type '%s' in group '%s'", service, group); + return res.status(403).json({ error: "Service does not support API calls" }); + } + + const url = new URL(formatApiCall(widgets[widget.type].api, { endpoint, ...widget })); + + const deviceIdRaw = widget.deviceId ?? `${widget.service_group || "group"}-${widget.service_name || "service"}`; + const deviceId = encodeURIComponent(deviceIdRaw); + const authHeader = `MediaBrowser Token="${encodeURIComponent( + widget.key, + )}", Client="Homepage", Device="Homepage", DeviceId="${deviceId}", Version="1.0.0"`; + + const headers = { + Authorization: authHeader, + }; + + const params = { + method: req.method, + withCredentials: true, + credentials: "include", + headers, + }; + + const [status, contentType, data] = await httpProxy(url, params); + + let resultData = data; + + if (resultData.error?.url) { + resultData.error.url = sanitizeErrorURL(url); + } + + if (status === 204 || status === 304) { + return res.status(status).end(); + } + + if (status >= 400) { + logger.error("HTTP Error %d calling %s", status, url.toString()); + } + + if (status === 200) { + if (!validateWidgetData(widget, endpoint, resultData)) { + return res.status(500).json({ error: { message: "Invalid data", url: sanitizeErrorURL(url), data: resultData } }); + } + if (map) resultData = map(resultData); + } + + if (contentType) res.setHeader("Content-Type", contentType); + return res.status(status).send(resultData); +} diff --git a/src/widgets/jellyfin/widget.js b/src/widgets/jellyfin/widget.js new file mode 100644 index 000000000..a11121359 --- /dev/null +++ b/src/widgets/jellyfin/widget.js @@ -0,0 +1,43 @@ +import jellyfinProxyHandler from "./proxy"; + +const widget = { + api: "{url}/{endpoint}", + proxyHandler: jellyfinProxyHandler, + mappings: { + Sessions: { + endpoint: "emby/Sessions?api_key={key}", + }, + Count: { + endpoint: "emby/Items/Counts?api_key={key}", + }, + Unpause: { + method: "POST", + endpoint: "emby/Sessions/{sessionId}/Playing/Unpause?api_key={key}", + segments: ["sessionId"], + }, + Pause: { + method: "POST", + endpoint: "emby/Sessions/{sessionId}/Playing/Pause?api_key={key}", + segments: ["sessionId"], + }, + // V2 Endpoints + SessionsV2: { + endpoint: "Sessions", + }, + CountV2: { + endpoint: "Items/Counts", + }, + UnpauseV2: { + method: "POST", + endpoint: "Sessions/{sessionId}/Playing/Unpause", + segments: ["sessionId"], + }, + PauseV2: { + method: "POST", + endpoint: "Sessions/{sessionId}/Playing/Pause", + segments: ["sessionId"], + }, + }, +}; + +export default widget; diff --git a/src/widgets/widgets.js b/src/widgets/widgets.js index 2029a82c6..26235729b 100644 --- a/src/widgets/widgets.js +++ b/src/widgets/widgets.js @@ -54,6 +54,7 @@ import homebridge from "./homebridge/widget"; import immich from "./immich/widget"; import jackett from "./jackett/widget"; import jdownloader from "./jdownloader/widget"; +import jellyfin from "./jellyfin/widget"; import jellyseerr from "./jellyseerr/widget"; import jellystat from "./jellystat/widget"; import karakeep from "./karakeep/widget"; @@ -207,7 +208,7 @@ const widgets = { immich, jackett, jdownloader, - jellyfin: emby, + jellyfin, jellyseerr, jellystat, kavita, From 062b1bcfbbae2b411c7c71adebc847bb0286a2ae Mon Sep 17 00:00:00 2001 From: Zhelyan Radoev Date: Sun, 1 Feb 2026 22:26:51 +0200 Subject: [PATCH 10/19] Fix: fix authentik widget login counts for v2 api (#6257) --- src/widgets/authentik/component.jsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/widgets/authentik/component.jsx b/src/widgets/authentik/component.jsx index 5103af108..dac25617c 100644 --- a/src/widgets/authentik/component.jsx +++ b/src/widgets/authentik/component.jsx @@ -47,8 +47,13 @@ export default function Component({ service }) { ); break; case 2: - loginsLast24H = loginsData[0]?.count || 0; - failedLoginsLast24H = failedLoginsData[0]?.count || 0; + loginsLast24H = + loginsData.reduce?.( + (total, current) => (current?.count && current?.action === "login" ? total + current.count : total), + 0, + ) || 0; + failedLoginsLast24H = + failedLoginsData.reduce?.((total, current) => (current?.count ? total + current.count : total), 0) || 0; break; } From 9cdb70527bb1a29663ba956d763626468c00c652 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Feb 2026 20:36:13 +0000 Subject: [PATCH 11/19] Chore(deps): Bump swr from 2.3.3 to 2.4.0 (#6260) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- pnpm-lock.yaml | 15 ++++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index ebd3fc22b..28de7a495 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "react-i18next": "^15.5.3", "react-icons": "^5.5.0", "recharts": "^3.1.2", - "swr": "^2.3.3", + "swr": "^2.4.0", "systeminformation": "^5.27.11", "tough-cookie": "^6.0.0", "urbackup-server-api": "^0.91.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 72f620316..3c838a81d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -81,8 +81,8 @@ importers: specifier: ^3.1.2 version: 3.1.2(@types/react@19.0.10)(react-dom@18.3.1(react@18.3.1))(react-is@18.3.1)(react@18.3.1)(redux@5.0.1) swr: - specifier: ^2.3.3 - version: 2.3.3(react@18.3.1) + specifier: ^2.4.0 + version: 2.4.0(react@18.3.1) systeminformation: specifier: ^5.27.11 version: 5.27.11 @@ -2779,8 +2779,8 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - swr@2.3.3: - resolution: {integrity: sha512-dshNvs3ExOqtZ6kJBaAsabhPdHyeY4P2cKwRCniDVifBMoG/SVI7tfLWqPXriVspf2Rg4tPzXJTnwaihIeFw2A==} + swr@2.4.0: + resolution: {integrity: sha512-sUlC20T8EOt1pHmDiqueUWMmRRX03W7w5YxovWX7VR2KHEPCTMly85x05vpkP5i6Bu4h44ePSMD9Tc+G2MItFw==} peerDependencies: react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -2823,6 +2823,7 @@ packages: tar@7.4.3: resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==} engines: {node: '>=18'} + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exhorbitant rates) by contacting i@izs.me telnet-client@2.2.6: resolution: {integrity: sha512-ZUYrLsPtQupQww3eSEORDVOb6ztdtKEghya6TVXPo2tg/UQq2pn5rHhvwuUvyYpbnsoqdNY1fyD1GNkXHR8dYA==} @@ -5516,7 +5517,7 @@ snapshots: dependencies: '@types/use-sync-external-store': 0.0.6 react: 18.3.1 - use-sync-external-store: 1.6.0(react@18.3.1) + use-sync-external-store: 1.5.0(react@18.3.1) optionalDependencies: '@types/react': 19.0.10 redux: 5.0.1 @@ -5904,11 +5905,11 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - swr@2.3.3(react@18.3.1): + swr@2.4.0(react@18.3.1): dependencies: dequal: 2.0.3 react: 18.3.1 - use-sync-external-store: 1.5.0(react@18.3.1) + use-sync-external-store: 1.6.0(react@18.3.1) synckit@0.11.11: dependencies: From 5c15466ac49de747323b275ec9638c31cacfe8c5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Feb 2026 20:46:59 +0000 Subject: [PATCH 12/19] Chore(deps): Bump winston from 3.17.0 to 3.19.0 (#6264) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- pnpm-lock.yaml | 87 ++++++++++++++++++++++---------------------------- 2 files changed, 40 insertions(+), 49 deletions(-) diff --git a/package.json b/package.json index 28de7a495..272879f2f 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "systeminformation": "^5.27.11", "tough-cookie": "^6.0.0", "urbackup-server-api": "^0.91.0", - "winston": "^3.17.0", + "winston": "^3.19.0", "ws": "^8.18.3", "xml-js": "^1.6.11" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3c838a81d..e4a39f545 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -93,8 +93,8 @@ importers: specifier: ^0.91.0 version: 0.91.0 winston: - specifier: ^3.17.0 - version: 3.17.0 + specifier: ^3.19.0 + version: 3.19.0 ws: specifier: ^8.18.3 version: 8.18.3 @@ -180,8 +180,8 @@ packages: resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} engines: {node: '>=0.1.90'} - '@dabh/diagnostics@2.0.3': - resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==} + '@dabh/diagnostics@2.0.8': + resolution: {integrity: sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==} '@emnapi/core@1.4.0': resolution: {integrity: sha512-H+N/FqT07NmLmt6OFFtDfwe8PNygprzBikrEMyQfgqSmT0vzE515Pz7R8izwB9q/zsH/MA64AKoul3sA6/CzVg==} @@ -635,6 +635,9 @@ packages: resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==} engines: {node: '>=14.16'} + '@so-ric/colorspace@1.1.6': + resolution: {integrity: sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==} + '@standard-schema/spec@1.0.0': resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} @@ -1151,27 +1154,28 @@ packages: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} - color-convert@1.9.3: - resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} - color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} - color-name@1.1.3: - resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + color-convert@3.1.3: + resolution: {integrity: sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==} + engines: {node: '>=14.6'} color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - color-string@1.9.1: - resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + color-name@2.1.0: + resolution: {integrity: sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==} + engines: {node: '>=12.20'} - color@3.2.1: - resolution: {integrity: sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==} + color-string@2.1.4: + resolution: {integrity: sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==} + engines: {node: '>=18'} - colorspace@1.1.4: - resolution: {integrity: sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==} + color@5.0.3: + resolution: {integrity: sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==} + engines: {node: '>=18'} combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} @@ -1821,9 +1825,6 @@ packages: resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} engines: {node: '>= 0.4'} - is-arrayish@0.3.2: - resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} - is-async-function@2.1.1: resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} engines: {node: '>= 0.4'} @@ -2661,9 +2662,6 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} - simple-swizzle@0.2.2: - resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} - source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -2996,8 +2994,8 @@ packages: resolution: {integrity: sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==} engines: {node: '>= 12.0.0'} - winston@3.17.0: - resolution: {integrity: sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==} + winston@3.19.0: + resolution: {integrity: sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==} engines: {node: '>= 12.0.0'} word-wrap@1.2.5: @@ -3075,9 +3073,9 @@ snapshots: '@colors/colors@1.6.0': {} - '@dabh/diagnostics@2.0.3': + '@dabh/diagnostics@2.0.8': dependencies: - colorspace: 1.1.4 + '@so-ric/colorspace': 1.1.6 enabled: 2.0.0 kuler: 2.0.0 @@ -3511,6 +3509,11 @@ snapshots: '@sindresorhus/is@5.6.0': {} + '@so-ric/colorspace@1.1.6': + dependencies: + color: 5.0.3 + text-hex: 1.0.0 + '@standard-schema/spec@1.0.0': {} '@standard-schema/utils@0.3.0': {} @@ -4042,32 +4045,26 @@ snapshots: clsx@2.1.1: {} - color-convert@1.9.3: - dependencies: - color-name: 1.1.3 - color-convert@2.0.1: dependencies: color-name: 1.1.4 - color-name@1.1.3: {} + color-convert@3.1.3: + dependencies: + color-name: 2.1.0 color-name@1.1.4: {} - color-string@1.9.1: - dependencies: - color-name: 1.1.4 - simple-swizzle: 0.2.2 + color-name@2.1.0: {} - color@3.2.1: + color-string@2.1.4: dependencies: - color-convert: 1.9.3 - color-string: 1.9.1 + color-name: 2.1.0 - colorspace@1.1.4: + color@5.0.3: dependencies: - color: 3.2.1 - text-hex: 1.0.0 + color-convert: 3.1.3 + color-string: 2.1.4 combined-stream@1.0.8: dependencies: @@ -4918,8 +4915,6 @@ snapshots: call-bound: 1.0.4 get-intrinsic: 1.3.0 - is-arrayish@0.3.2: {} - is-async-function@2.1.1: dependencies: async-function: 1.0.0 @@ -5770,10 +5765,6 @@ snapshots: signal-exit@4.1.0: {} - simple-swizzle@0.2.2: - dependencies: - is-arrayish: 0.3.2 - source-map-js@1.2.1: {} split-ca@1.0.1: {} @@ -6189,10 +6180,10 @@ snapshots: readable-stream: 3.6.2 triple-beam: 1.4.1 - winston@3.17.0: + winston@3.19.0: dependencies: '@colors/colors': 1.6.0 - '@dabh/diagnostics': 2.0.3 + '@dabh/diagnostics': 2.0.8 async: 3.2.6 is-stream: 2.0.1 logform: 2.7.0 From 64c81615ec8325fe82a630dc6f1e756b5bc3ec8b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Feb 2026 13:02:36 -0800 Subject: [PATCH 13/19] Chore(deps-dev): Bump next-js and eslint-config-next from 15.2.4 to 15.5.11 (#6261) Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com> --- package.json | 4 ++-- pnpm-lock.yaml | 42 +++++++++++++++++++++--------------------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/package.json b/package.json index 272879f2f..56b80b07f 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "luxon": "^3.6.1", "memory-cache": "^0.2.0", "minecraftstatuspinger": "^1.2.2", - "next": "^15.5.9", + "next": "^15.5.11", "next-i18next": "^12.1.0", "ping": "^0.4.4", "pretty-bytes": "^7.1.0", @@ -47,7 +47,7 @@ "@tailwindcss/forms": "^0.5.10", "@tailwindcss/postcss": "^4.1.18", "eslint": "^9.25.1", - "eslint-config-next": "^15.2.4", + "eslint-config-next": "^15.5.11", "eslint-config-prettier": "^10.1.8", "eslint-plugin-import": "^2.32.0", "eslint-plugin-jsx-a11y": "^6.10.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e4a39f545..300c9574f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -51,11 +51,11 @@ importers: specifier: ^1.2.2 version: 1.2.2 next: - specifier: ^15.5.9 - version: 15.5.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: ^15.5.11 + version: 15.5.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1) next-i18next: specifier: ^12.1.0 - version: 12.1.0(next@15.5.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 12.1.0(next@15.5.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) ping: specifier: ^0.4.4 version: 0.4.4 @@ -112,8 +112,8 @@ importers: specifier: ^9.25.1 version: 9.25.1(jiti@2.6.1) eslint-config-next: - specifier: ^15.2.4 - version: 15.2.4(eslint@9.25.1(jiti@2.6.1))(typescript@5.7.3) + specifier: ^15.5.11 + version: 15.5.11(eslint@9.25.1(jiti@2.6.1))(typescript@5.7.3) eslint-config-prettier: specifier: ^10.1.8 version: 10.1.8(eslint@9.25.1(jiti@2.6.1)) @@ -469,11 +469,11 @@ packages: '@napi-rs/wasm-runtime@0.2.8': resolution: {integrity: sha512-OBlgKdX7gin7OIq4fadsjpg+cp2ZphvAIKucHsNfTdJiqdOmOEwQd/bHi0VwNrcw5xpBJyUw6cK/QilCqy1BSg==} - '@next/env@15.5.9': - resolution: {integrity: sha512-4GlTZ+EJM7WaW2HEZcyU317tIQDjkQIyENDLxYJfSWlfqguN+dHkZgyQTV/7ykvobU7yEH5gKvreNrH4B6QgIg==} + '@next/env@15.5.11': + resolution: {integrity: sha512-g9s5SS9gC7GJCEOR3OV3zqs7C5VddqxP9X+/6BpMbdXRkqsWfFf2CJPBZNvNEtAkKTNuRgRXAgNxSAXzfLdaTg==} - '@next/eslint-plugin-next@15.2.4': - resolution: {integrity: sha512-O8ScvKtnxkp8kL9TpJTTKnMqlkZnS+QxwoQnJwPGBxjBbzd6OVVPEJ5/pMNrktSyXQD/chEfzfFzYLM6JANOOQ==} + '@next/eslint-plugin-next@15.5.11': + resolution: {integrity: sha512-tS/HYQOjIoX9ZNDQitba/baS8sTvo3ekY6Vgdx5lmhN4jov082bdApIChXr94qhMZHvEciz9DZglFFnhguQp/A==} '@next/swc-darwin-arm64@15.5.7': resolution: {integrity: sha512-IZwtxCEpI91HVU/rAUOOobWSZv4P2DeTtNaCdHqLcTJU4wdNXgAySvKa/qJCgR5m6KI8UsKDXtO2B31jcaw1Yw==} @@ -1416,8 +1416,8 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} - eslint-config-next@15.2.4: - resolution: {integrity: sha512-v4gYjd4eYIme8qzaJItpR5MMBXJ0/YV07u7eb50kEnlEmX7yhOjdUdzz70v4fiINYRjLf8X8TbogF0k7wlz6sA==} + eslint-config-next@15.5.11: + resolution: {integrity: sha512-RQNY69VUv0BzXkLEKDh/OPUzA+krFOnYRxO0JA3UsW429ovLa2nXx8kZuXCl18P27PyJBdS3qgJJkIhi9H8SuQ==} peerDependencies: eslint: ^7.23.0 || ^8.0.0 || ^9.0.0 typescript: '>=3.3.1' @@ -2214,8 +2214,8 @@ packages: next: '>= 10.0.0' react: '>= 16.8.0' - next@15.5.9: - resolution: {integrity: sha512-agNLK89seZEtC5zUHwtut0+tNrc0Xw4FT/Dg+B/VLEo9pAcS9rtTKpek3V6kVcVwsB2YlqMaHdfZL4eLEVYuCg==} + next@15.5.11: + resolution: {integrity: sha512-L2KPiKmqTDpRdeVDdPjhf43g2/VPe0NCNndq7OKDCgOLWtxe1kbr/zXGIZtYY7kZEAjRf7Bj/mwUFSr+tYC2Yg==} engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} hasBin: true peerDependencies: @@ -3370,9 +3370,9 @@ snapshots: '@tybys/wasm-util': 0.9.0 optional: true - '@next/env@15.5.9': {} + '@next/env@15.5.11': {} - '@next/eslint-plugin-next@15.2.4': + '@next/eslint-plugin-next@15.5.11': dependencies: fast-glob: 3.3.1 @@ -4405,9 +4405,9 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-config-next@15.2.4(eslint@9.25.1(jiti@2.6.1))(typescript@5.7.3): + eslint-config-next@15.5.11(eslint@9.25.1(jiti@2.6.1))(typescript@5.7.3): dependencies: - '@next/eslint-plugin-next': 15.2.4 + '@next/eslint-plugin-next': 15.5.11 '@rushstack/eslint-patch': 1.11.0 '@typescript-eslint/eslint-plugin': 8.29.0(@typescript-eslint/parser@8.29.0(eslint@9.25.1(jiti@2.6.1))(typescript@5.7.3))(eslint@9.25.1(jiti@2.6.1))(typescript@5.7.3) '@typescript-eslint/parser': 8.29.0(eslint@9.25.1(jiti@2.6.1))(typescript@5.7.3) @@ -5246,7 +5246,7 @@ snapshots: net@1.0.2: {} - next-i18next@12.1.0(next@15.5.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + next-i18next@12.1.0(next@15.5.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@babel/runtime': 7.26.9 '@types/hoist-non-react-statics': 3.3.6 @@ -5254,16 +5254,16 @@ snapshots: hoist-non-react-statics: 3.3.2 i18next: 21.10.0 i18next-fs-backend: 1.2.0 - next: 15.5.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + next: 15.5.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-i18next: 11.18.6(i18next@21.10.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) transitivePeerDependencies: - react-dom - react-native - next@15.5.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + next@15.5.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@next/env': 15.5.9 + '@next/env': 15.5.11 '@swc/helpers': 0.5.15 caniuse-lite: 1.0.30001760 postcss: 8.4.31 From 7e3fa9767941c0ca53b96922051599a4f980afb2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Feb 2026 21:23:48 +0000 Subject: [PATCH 14/19] Chore(deps-dev): Bump tailwindcss from 4.0.9 to 4.1.18 (#6262) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- pnpm-lock.yaml | 21 ++++++++------------- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index 56b80b07f..3367979fb 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "prettier": "^3.7.3", "prettier-plugin-organize-imports": "^4.3.0", "tailwind-scrollbar": "^4.0.2", - "tailwindcss": "^4.0.9", + "tailwindcss": "^4.1.18", "typescript": "^5.7.3" }, "optionalDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 300c9574f..b09f71001 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -104,7 +104,7 @@ importers: devDependencies: '@tailwindcss/forms': specifier: ^0.5.10 - version: 0.5.10(tailwindcss@4.0.9) + version: 0.5.10(tailwindcss@4.1.18) '@tailwindcss/postcss': specifier: ^4.1.18 version: 4.1.18 @@ -143,10 +143,10 @@ importers: version: 4.3.0(prettier@3.7.3)(typescript@5.7.3) tailwind-scrollbar: specifier: ^4.0.2 - version: 4.0.2(react@18.3.1)(tailwindcss@4.0.9) + version: 4.0.2(react@18.3.1)(tailwindcss@4.1.18) tailwindcss: - specifier: ^4.0.9 - version: 4.0.9 + specifier: ^4.1.18 + version: 4.1.18 typescript: specifier: ^5.7.3 version: 5.7.3 @@ -2801,9 +2801,6 @@ packages: peerDependencies: tailwindcss: 4.x - tailwindcss@4.0.9: - resolution: {integrity: sha512-12laZu+fv1ONDRoNR9ipTOpUD7RN9essRVkX36sjxuRUInpN7hIiHN4lBd/SIFjbISvnXzp8h/hXzmU8SQQYhw==} - tailwindcss@4.1.18: resolution: {integrity: sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==} @@ -3530,10 +3527,10 @@ snapshots: dependencies: defer-to-connect: 2.0.1 - '@tailwindcss/forms@0.5.10(tailwindcss@4.0.9)': + '@tailwindcss/forms@0.5.10(tailwindcss@4.1.18)': dependencies: mini-svg-data-uri: 1.4.4 - tailwindcss: 4.0.9 + tailwindcss: 4.1.18 '@tailwindcss/node@4.1.18': dependencies: @@ -5910,15 +5907,13 @@ snapshots: tabbable@6.3.0: {} - tailwind-scrollbar@4.0.2(react@18.3.1)(tailwindcss@4.0.9): + tailwind-scrollbar@4.0.2(react@18.3.1)(tailwindcss@4.1.18): dependencies: prism-react-renderer: 2.4.1(react@18.3.1) - tailwindcss: 4.0.9 + tailwindcss: 4.1.18 transitivePeerDependencies: - react - tailwindcss@4.0.9: {} - tailwindcss@4.1.18: {} tapable@2.3.0: {} From 1233b5e80337e52a30e962e3d9e4f45b586ad946 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Feb 2026 21:32:49 +0000 Subject: [PATCH 15/19] Chore(deps): Bump i18next from 25.5.3 to 25.8.0 (#6263) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- pnpm-lock.yaml | 28 ++++++++++++++-------------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/package.json b/package.json index 3367979fb..526c5ab59 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "dockerode": "^4.0.7", "follow-redirects": "^1.15.11", "gamedig": "^5.3.2", - "i18next": "^25.5.3", + "i18next": "^25.8.0", "ical.js": "^2.1.0", "js-yaml": "^4.1.1", "json-rpc-2.0": "^1.7.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b09f71001..846274977 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -30,8 +30,8 @@ importers: specifier: ^5.3.2 version: 5.3.2 i18next: - specifier: ^25.5.3 - version: 25.5.3(typescript@5.7.3) + specifier: ^25.8.0 + version: 25.8.0(typescript@5.7.3) ical.js: specifier: ^2.1.0 version: 2.1.0 @@ -73,7 +73,7 @@ importers: version: 18.3.1(react@18.3.1) react-i18next: specifier: ^15.5.3 - version: 15.5.3(i18next@25.5.3(typescript@5.7.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.3) + version: 15.5.3(i18next@25.8.0(typescript@5.7.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.3) react-icons: specifier: ^5.5.0 version: 5.5.0(react@18.3.1) @@ -169,8 +169,8 @@ packages: resolution: {integrity: sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==} engines: {node: '>=6.9.0'} - '@babel/runtime@7.28.4': - resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==} + '@babel/runtime@7.28.6': + resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==} engines: {node: '>=6.9.0'} '@balena/dockerignore@1.0.2': @@ -1777,8 +1777,8 @@ packages: i18next@21.10.0: resolution: {integrity: sha512-YeuIBmFsGjUfO3qBmMOc0rQaun4mIpGKET5WDwvu8lU7gvwpcariZLNtL0Fzj+zazcHUrlXHiptcFhBMFaxzfg==} - i18next@25.5.3: - resolution: {integrity: sha512-joFqorDeQ6YpIXni944upwnuHBf5IoPMuqAchGVeQLdWC2JOjxgM9V8UGLhNIIH/Q8QleRxIi0BSRQehSrDLcg==} + i18next@25.8.0: + resolution: {integrity: sha512-urrg4HMFFMQZ2bbKRK7IZ8/CTE7D8H4JRlAwqA2ZwDRFfdd0K/4cdbNNLgfn9mo+I/h9wJu61qJzH7jCFAhUZQ==} peerDependencies: typescript: ^5 peerDependenciesMeta: @@ -3064,7 +3064,7 @@ snapshots: '@babel/runtime@7.27.6': {} - '@babel/runtime@7.28.4': {} + '@babel/runtime@7.28.6': {} '@balena/dockerignore@1.0.2': {} @@ -4869,11 +4869,11 @@ snapshots: i18next@21.10.0: dependencies: - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.28.6 - i18next@25.5.3(typescript@5.7.3): + i18next@25.8.0(typescript@5.7.3): dependencies: - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.28.6 optionalDependencies: typescript: 5.7.3 @@ -5480,18 +5480,18 @@ snapshots: react-i18next@11.18.6(i18next@21.10.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.28.6 html-parse-stringify: 3.0.1 i18next: 21.10.0 react: 18.3.1 optionalDependencies: react-dom: 18.3.1(react@18.3.1) - react-i18next@15.5.3(i18next@25.5.3(typescript@5.7.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.3): + react-i18next@15.5.3(i18next@25.8.0(typescript@5.7.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.3): dependencies: '@babel/runtime': 7.27.6 html-parse-stringify: 3.0.1 - i18next: 25.5.3(typescript@5.7.3) + i18next: 25.8.0(typescript@5.7.3) react: 18.3.1 optionalDependencies: react-dom: 18.3.1(react@18.3.1) From 97e909ebf4e150996b1aef02e9b4fc9e84c3f20a Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Mon, 2 Feb 2026 15:18:30 -0800 Subject: [PATCH 16/19] Chore: move to eslint (#6270) --- .eslintrc.json | 42 --------------- eslint.config.mjs | 69 +++++++++++++++++++++++++ next-i18next.config.js | 4 +- package.json | 5 +- pnpm-lock.yaml | 48 +++++++++++++++-- src/middleware.js | 1 - src/pages/api/ping.js | 4 +- src/utils/config/api-response.js | 2 - src/utils/config/config.js | 1 - src/utils/config/shvl.js | 2 - src/utils/layout/columns.js | 1 - src/utils/logger.js | 4 +- src/utils/proxy/cookie-jar.js | 1 - src/utils/proxy/handlers/jsonrpc.js | 1 - src/utils/proxy/handlers/synology.js | 3 +- src/utils/proxy/http.js | 2 - src/utils/proxy/validate-widget-data.js | 1 - src/widgets/beszel/proxy.js | 1 - src/widgets/deluge/proxy.js | 1 - src/widgets/dispatcharr/proxy.js | 1 - src/widgets/flood/proxy.js | 1 - src/widgets/homebridge/proxy.js | 2 +- src/widgets/jdownloader/proxy.js | 1 - src/widgets/npm/proxy.js | 1 - src/widgets/omada/proxy.js | 2 - src/widgets/plex/proxy.js | 1 - src/widgets/pyload/proxy.js | 2 - src/widgets/qbittorrent/proxy.js | 1 - src/widgets/xteve/proxy.js | 1 - 29 files changed, 121 insertions(+), 85 deletions(-) delete mode 100644 .eslintrc.json create mode 100644 eslint.config.mjs diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index d29adc331..000000000 --- a/.eslintrc.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "extends": [ - "next/core-web-vitals", - "prettier", - "plugin:react-hooks/recommended" - ], - "plugins": ["prettier"], - "rules": { - "import/no-cycle": [ - "error", - { - "maxDepth": 1 - } - ], - "import/order": [ - "error", - { - "newlines-between": "always" - } - ], - "no-else-return": [ - "error", - { - "allowElseIf": true - } - ] - }, - "settings": { - "import/resolver": { - "node": { - "paths": ["src"] - } - } - }, - "parserOptions": { - "ecmaVersion": 6, - "sourceType": "module", - "ecmaFeatures": { - "modules": true - } - } -} diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 000000000..4b8958ef0 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,69 @@ +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +import { fixupConfigRules } from "@eslint/compat"; +import { FlatCompat } from "@eslint/eslintrc"; +import js from "@eslint/js"; +import prettier from "eslint-plugin-prettier"; +import { defineConfig, globalIgnores } from "eslint/config"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + allConfig: js.configs.all, +}); + +export default defineConfig([ + { + extends: fixupConfigRules(compat.extends("next/core-web-vitals", "prettier", "plugin:react-hooks/recommended")), + + plugins: { + prettier, + }, + + languageOptions: { + ecmaVersion: 6, + sourceType: "module", + + parserOptions: { + ecmaFeatures: { + modules: true, + }, + }, + }, + + settings: { + "import/resolver": { + node: { + paths: ["src"], + }, + }, + }, + + rules: { + "import/no-cycle": [ + "error", + { + maxDepth: 1, + }, + ], + + "import/order": [ + "error", + { + "newlines-between": "always", + }, + ], + + "no-else-return": [ + "error", + { + allowElseIf: true, + }, + ], + }, + }, + globalIgnores(["./config/", "./.venv/", "./.next/", "./site/"]), +]); diff --git a/next-i18next.config.js b/next-i18next.config.js index f6968dc35..cef9e3988 100644 --- a/next-i18next.config.js +++ b/next-i18next.config.js @@ -1,6 +1,5 @@ // prettyBytes taken from https://github.com/sindresorhus/pretty-bytes -/* eslint-disable no-param-reassign */ const BYTE_UNITS = ["B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; const BIBYTE_UNITS = ["B", "kiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"]; @@ -37,7 +36,6 @@ function prettyBytes(number, options) { ...options, }; - // eslint-disable-next-line no-nested-ternary const UNITS = options.bits ? (options.binary ? BIBIT_UNITS : BIT_UNITS) : options.binary ? BIBYTE_UNITS : BYTE_UNITS; if (options.signed && number === 0) { @@ -45,7 +43,7 @@ function prettyBytes(number, options) { } const isNegative = number < 0; - // eslint-disable-next-line no-nested-ternary + const prefix = isNegative ? "-" : options.signed ? "+" : ""; if (isNegative) { diff --git a/package.json b/package.json index 526c5ab59..2b10428dc 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "dev": "next dev", "build": "next build", "start": "next start", - "lint": "next lint", + "lint": "eslint .", "telemetry": "next telemetry disable" }, "dependencies": { @@ -44,6 +44,9 @@ "xml-js": "^1.6.11" }, "devDependencies": { + "@eslint/compat": "^2.0.2", + "@eslint/eslintrc": "^3.3.3", + "@eslint/js": "^9.39.2", "@tailwindcss/forms": "^0.5.10", "@tailwindcss/postcss": "^4.1.18", "eslint": "^9.25.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 846274977..b3b3820c7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -102,6 +102,15 @@ importers: specifier: ^1.6.11 version: 1.6.11 devDependencies: + '@eslint/compat': + specifier: ^2.0.2 + version: 2.0.2(eslint@9.25.1(jiti@2.6.1)) + '@eslint/eslintrc': + specifier: ^3.3.3 + version: 3.3.3 + '@eslint/js': + specifier: ^9.39.2 + version: 9.39.2 '@tailwindcss/forms': specifier: ^0.5.10 version: 0.5.10(tailwindcss@4.1.18) @@ -202,6 +211,15 @@ packages: resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + '@eslint/compat@2.0.2': + resolution: {integrity: sha512-pR1DoD0h3HfF675QZx0xsyrsU8q70Z/plx7880NOhS02NuWLgBCOMDL787nUeQ7EWLkxv3bPQJaarjcPQb2Dwg==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + peerDependencies: + eslint: ^8.40 || 9 || 10 + peerDependenciesMeta: + eslint: + optional: true + '@eslint/config-array@0.20.0': resolution: {integrity: sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -214,14 +232,22 @@ packages: resolution: {integrity: sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/eslintrc@3.3.1': - resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} + '@eslint/core@1.1.0': + resolution: {integrity: sha512-/nr9K9wkr3P1EzFTdFdMoLuo1PmIxjmwvPozwoSodjNBdefGujXQUF93u1DDZpEaTuDvMsIQddsd35BwtrW9Xw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/eslintrc@3.3.3': + resolution: {integrity: sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/js@9.25.1': resolution: {integrity: sha512-dEIwmjntEx8u3Uvv+kr3PDeeArL8Hw07H9kyYxCjnM9pBjfEhk6uLXSchxxzgiwtRhhzVzqmUSDFBOi1TuZ7qg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/js@9.39.2': + resolution: {integrity: sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/object-schema@2.1.6': resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -3099,6 +3125,12 @@ snapshots: '@eslint-community/regexpp@4.12.1': {} + '@eslint/compat@2.0.2(eslint@9.25.1(jiti@2.6.1))': + dependencies: + '@eslint/core': 1.1.0 + optionalDependencies: + eslint: 9.25.1(jiti@2.6.1) + '@eslint/config-array@0.20.0': dependencies: '@eslint/object-schema': 2.1.6 @@ -3113,10 +3145,14 @@ snapshots: dependencies: '@types/json-schema': 7.0.15 - '@eslint/eslintrc@3.3.1': + '@eslint/core@1.1.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.3': dependencies: ajv: 6.12.6 - debug: 4.4.0 + debug: 4.4.1 espree: 10.3.0 globals: 14.0.0 ignore: 5.3.2 @@ -3129,6 +3165,8 @@ snapshots: '@eslint/js@9.25.1': {} + '@eslint/js@9.39.2': {} + '@eslint/object-schema@2.1.6': {} '@eslint/plugin-kit@0.2.8': @@ -4559,7 +4597,7 @@ snapshots: '@eslint/config-array': 0.20.0 '@eslint/config-helpers': 0.2.2 '@eslint/core': 0.13.0 - '@eslint/eslintrc': 3.3.1 + '@eslint/eslintrc': 3.3.3 '@eslint/js': 9.25.1 '@eslint/plugin-kit': 0.2.8 '@humanfs/node': 0.16.6 diff --git a/src/middleware.js b/src/middleware.js index f16b51745..39452f6cc 100644 --- a/src/middleware.js +++ b/src/middleware.js @@ -10,7 +10,6 @@ export function middleware(req) { allowedHosts = allowedHosts.concat(process.env.HOMEPAGE_ALLOWED_HOSTS.split(",")); } if (!allowAll && (!host || !allowedHosts.includes(host))) { - // eslint-disable-next-line no-console console.error( `Host validation failed for: ${host}. Hint: Set the HOMEPAGE_ALLOWED_HOSTS environment variable to allow requests from this host / port.`, ); diff --git a/src/pages/api/ping.js b/src/pages/api/ping.js index 8ef64ffc9..07a40346e 100644 --- a/src/pages/api/ping.js +++ b/src/pages/api/ping.js @@ -28,9 +28,7 @@ export default async function handler(req, res) { try { // maintain backwards compatibility with old ping where may be http://... hostname = new URL(pingHostOrURL).hostname; - } catch (e) { - // eslint-disable-line no-empty - } + } catch (e) {} try { const response = await ping.probe(hostname); diff --git a/src/utils/config/api-response.js b/src/utils/config/api-response.js index b7b670cd0..cbc382f24 100644 --- a/src/utils/config/api-response.js +++ b/src/utils/config/api-response.js @@ -1,4 +1,3 @@ -/* eslint-disable no-console */ import { promises as fs } from "fs"; import path from "path"; @@ -100,7 +99,6 @@ function convertLayoutGroupToGroup(name, layoutGroup) { function mergeSubgroups(configuredGroups, mergedGroup) { configuredGroups.forEach((group) => { if (group.name === mergedGroup.name) { - // eslint-disable-next-line no-param-reassign group.services = mergedGroup.services; } else if (group.groups) { mergeSubgroups(group.groups, mergedGroup); diff --git a/src/utils/config/config.js b/src/utils/config/config.js index 60ac087aa..22f0e2c73 100644 --- a/src/utils/config/config.js +++ b/src/utils/config/config.js @@ -1,4 +1,3 @@ -/* eslint-disable no-console */ import { copyFileSync, existsSync, mkdirSync, readFileSync } from "fs"; import { join } from "path"; diff --git a/src/utils/config/shvl.js b/src/utils/config/shvl.js index 6ece8c2b9..284b48a2f 100644 --- a/src/utils/config/shvl.js +++ b/src/utils/config/shvl.js @@ -1,5 +1,3 @@ -/* eslint-disable */ - /* Code primarely based on shvl repository: https://github.com/robinvdvleuten/shvl diff --git a/src/utils/layout/columns.js b/src/utils/layout/columns.js index 2de4eb8b9..7b0dd8c67 100644 --- a/src/utils/layout/columns.js +++ b/src/utils/layout/columns.js @@ -1,4 +1,3 @@ -// eslint-disable-next-line import/prefer-default-export export const columnMap = [ "grid-cols-1 md:grid-cols-1 lg:grid-cols-1", "grid-cols-1 md:grid-cols-1 lg:grid-cols-1", diff --git a/src/utils/logger.js b/src/utils/logger.js index a3a6ee870..74dfaf9bc 100644 --- a/src/utils/logger.js +++ b/src/utils/logger.js @@ -1,4 +1,3 @@ -/* eslint-disable no-console */ import { format as utilFormat } from "node:util"; import winston from "winston"; @@ -9,10 +8,9 @@ let winstonLogger; function combineMessageAndSplat() { return { - // eslint-disable-next-line no-unused-vars transform: (info, opts) => { // combine message and args if any - // eslint-disable-next-line no-param-reassign + info.message = utilFormat(info.message, ...(info[Symbol.for("splat")] || [])); return info; }, diff --git a/src/utils/proxy/cookie-jar.js b/src/utils/proxy/cookie-jar.js index baea21d5b..3161d1d2f 100644 --- a/src/utils/proxy/cookie-jar.js +++ b/src/utils/proxy/cookie-jar.js @@ -1,4 +1,3 @@ -/* eslint-disable no-param-reassign */ import { Cookie, CookieJar } from "tough-cookie"; const cookieJar = new CookieJar(); diff --git a/src/utils/proxy/handlers/jsonrpc.js b/src/utils/proxy/handlers/jsonrpc.js index bdb10e022..27c2b324f 100644 --- a/src/utils/proxy/handlers/jsonrpc.js +++ b/src/utils/proxy/handlers/jsonrpc.js @@ -30,7 +30,6 @@ export async function sendJsonRpcRequest(url, method, params, widget) { body, }; - // eslint-disable-next-line no-unused-vars const [status, contentType, data] = await httpProxy(url, httpRequestParams); if (status === 200) { const json = JSON.parse(data.toString()); diff --git a/src/utils/proxy/handlers/synology.js b/src/utils/proxy/handlers/synology.js index 6fe98dce9..4941ba11f 100644 --- a/src/utils/proxy/handlers/synology.js +++ b/src/utils/proxy/handlers/synology.js @@ -48,7 +48,7 @@ async function getApiInfo(serviceWidget, apiName, serviceName) { } const infoUrl = formatApiCall(INFO_ENDPOINT, serviceWidget); - // eslint-disable-next-line no-unused-vars + const [status, contentType, data] = await httpProxy(infoUrl); if (status === 200) { @@ -74,7 +74,6 @@ async function getApiInfo(serviceWidget, apiName, serviceName) { async function handleUnsuccessfulResponse(serviceWidget, url, serviceName) { logger.debug(`Attempting login to ${serviceWidget.type}`); - // eslint-disable-next-line no-unused-vars const [apiPath, maxVersion] = await getApiInfo(serviceWidget, AUTH_API_NAME, serviceName); const authArgs = { path: apiPath ?? "entry.cgi", maxVersion: maxVersion ?? 7, ...serviceWidget }; diff --git a/src/utils/proxy/http.js b/src/utils/proxy/http.js index 5db97c60a..d61cc5c7f 100644 --- a/src/utils/proxy/http.js +++ b/src/utils/proxy/http.js @@ -1,5 +1,3 @@ -/* eslint-disable prefer-promise-reject-errors */ -/* eslint-disable no-param-reassign */ import { createUnzip, constants as zlibConstants } from "node:zlib"; import { http, https } from "follow-redirects"; diff --git a/src/utils/proxy/validate-widget-data.js b/src/utils/proxy/validate-widget-data.js index de2a3c4ed..b38ef4f79 100644 --- a/src/utils/proxy/validate-widget-data.js +++ b/src/utils/proxy/validate-widget-data.js @@ -1,4 +1,3 @@ -/* eslint-disable no-console */ import createLogger from "utils/logger"; import widgets from "widgets/widgets"; diff --git a/src/widgets/beszel/proxy.js b/src/widgets/beszel/proxy.js index 93a2385c6..8521062cc 100644 --- a/src/widgets/beszel/proxy.js +++ b/src/widgets/beszel/proxy.js @@ -97,7 +97,6 @@ export default async function beszelProxyHandler(req, res) { return res.status(status).send(data); } - // eslint-disable-next-line no-unused-vars [status, , data] = await httpProxy(url, { method: "GET", headers: { diff --git a/src/widgets/deluge/proxy.js b/src/widgets/deluge/proxy.js index ef255160a..cd21cddfa 100644 --- a/src/widgets/deluge/proxy.js +++ b/src/widgets/deluge/proxy.js @@ -65,7 +65,6 @@ export default async function delugeProxyHandler(req, res) { return res.status(status).end(data); } - // eslint-disable-next-line no-unused-vars [status, contentType, data] = await sendRpc(url, dataMethod, dataParams); } diff --git a/src/widgets/dispatcharr/proxy.js b/src/widgets/dispatcharr/proxy.js index 45175983a..a0e73eb3f 100644 --- a/src/widgets/dispatcharr/proxy.js +++ b/src/widgets/dispatcharr/proxy.js @@ -97,7 +97,6 @@ export default async function dispatcharrProxyHandler(req, res) { return res.status(status).send(data); } - // eslint-disable-next-line no-unused-vars [status, , data] = await httpProxy(url, { method: "GET", headers: { diff --git a/src/widgets/flood/proxy.js b/src/widgets/flood/proxy.js index 5e5335ae1..730ca5ed6 100644 --- a/src/widgets/flood/proxy.js +++ b/src/widgets/flood/proxy.js @@ -22,7 +22,6 @@ async function login(widget) { }); } - // eslint-disable-next-line no-unused-vars const [status, contentType, data] = await httpProxy(loginUrl, loginParams); return [status, data]; } diff --git a/src/widgets/homebridge/proxy.js b/src/widgets/homebridge/proxy.js index 675e2976c..02fcd7837 100644 --- a/src/widgets/homebridge/proxy.js +++ b/src/widgets/homebridge/proxy.js @@ -16,7 +16,7 @@ async function login(widget, service) { const loginUrl = new URL(formatApiCall(api, { endpoint, ...widget })); const loginBody = { username: widget.username.toString(), password: widget.password.toString() }; const headers = { "Content-Type": "application/json" }; - // eslint-disable-next-line no-unused-vars + const [status, contentType, data, responseHeaders] = await httpProxy(loginUrl, { method: "POST", body: JSON.stringify(loginBody), diff --git a/src/widgets/jdownloader/proxy.js b/src/widgets/jdownloader/proxy.js index d5d5ac3d7..d16ac7d3b 100644 --- a/src/widgets/jdownloader/proxy.js +++ b/src/widgets/jdownloader/proxy.js @@ -1,4 +1,3 @@ -/* eslint-disable no-underscore-dangle */ import crypto from "crypto"; import querystring from "querystring"; diff --git a/src/widgets/npm/proxy.js b/src/widgets/npm/proxy.js index 793077829..5b7c5a8b4 100644 --- a/src/widgets/npm/proxy.js +++ b/src/widgets/npm/proxy.js @@ -79,7 +79,6 @@ export default async function npmProxyHandler(req, res) { return res.status(status).send(data); } - // eslint-disable-next-line no-unused-vars [status, , data] = await httpProxy(url, { method: "GET", headers: { diff --git a/src/widgets/omada/proxy.js b/src/widgets/omada/proxy.js index f8a586705..1151826da 100644 --- a/src/widgets/omada/proxy.js +++ b/src/widgets/omada/proxy.js @@ -20,7 +20,6 @@ async function login(loginUrl, username, password, controllerVersionMajor) { }; } - // eslint-disable-next-line no-unused-vars const [status, contentType, data] = await httpProxy(loginUrl, { method: "POST", body: JSON.stringify(params), @@ -234,7 +233,6 @@ export default async function omadaProxyHandler(req, res) { ? `${url}/api/v2/sites/${siteName}/alerts/num?token=${token}¤tPage=1¤tPageSize=1000` : `${url}/${cId}/api/v2/sites/${siteName}/alerts/num?token=${token}¤tPage=1¤tPageSize=1000`; - // eslint-disable-next-line no-unused-vars [status, contentType, data] = await httpProxy(alertUrl, { headers: { "Csrf-Token": token, diff --git a/src/widgets/plex/proxy.js b/src/widgets/plex/proxy.js index f21d07d03..a7580ea37 100644 --- a/src/widgets/plex/proxy.js +++ b/src/widgets/plex/proxy.js @@ -1,4 +1,3 @@ -/* eslint-disable no-underscore-dangle */ import cache from "memory-cache"; import { xml2json } from "xml-js"; diff --git a/src/widgets/pyload/proxy.js b/src/widgets/pyload/proxy.js index 7bfbd46cb..57bd68fd7 100644 --- a/src/widgets/pyload/proxy.js +++ b/src/widgets/pyload/proxy.js @@ -40,7 +40,6 @@ async function fetchFromPyloadAPI(url, sessionId, params, service) { options.headers.Cookie = cache.get(`${sessionCacheKey}.${service}`); } - // eslint-disable-next-line no-unused-vars const [status, contentType, data, responseHeaders] = await httpProxy(url, options); const returnData = parsePyloadResponse(url, data); return [status, returnData, responseHeaders]; @@ -66,7 +65,6 @@ async function fetchFromPyloadAPIBasic(url, params, username, password) { options.body = JSON.stringify(params); } - // eslint-disable-next-line no-unused-vars const [status, contentType, data, responseHeaders] = await httpProxy(parsedUrl, options); const returnData = parsePyloadResponse(parsedUrl, data); return [status, returnData, responseHeaders]; diff --git a/src/widgets/qbittorrent/proxy.js b/src/widgets/qbittorrent/proxy.js index 8f1874bf5..4bb883023 100644 --- a/src/widgets/qbittorrent/proxy.js +++ b/src/widgets/qbittorrent/proxy.js @@ -15,7 +15,6 @@ async function login(widget) { body: loginBody, }; - // eslint-disable-next-line no-unused-vars const [status, contentType, data] = await httpProxy(loginUrl, loginParams); return [status, data]; } diff --git a/src/widgets/xteve/proxy.js b/src/widgets/xteve/proxy.js index 53d82bc4a..cbd731297 100644 --- a/src/widgets/xteve/proxy.js +++ b/src/widgets/xteve/proxy.js @@ -24,7 +24,6 @@ export default async function xteveProxyHandler(req, res) { const payload = { cmd: "status" }; if (widget.username && widget.password) { - // eslint-disable-next-line no-unused-vars const [status, contentType, data] = await httpProxy(url, { method, body: JSON.stringify({ From 99f1540d8c51e843ccbd767ce4f3dd32270d35c5 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin <3811295@gmail.com> Date: Tue, 3 Feb 2026 08:16:46 +0300 Subject: [PATCH 17/19] Enhancement: DNS fallback for Alpine/musl compatibility (#6265) Signed-off-by: Aleksei Sviridkin Co-authored-by: Claude Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com> --- src/utils/proxy/http.js | 123 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 122 insertions(+), 1 deletion(-) diff --git a/src/utils/proxy/http.js b/src/utils/proxy/http.js index d61cc5c7f..c05102142 100644 --- a/src/utils/proxy/http.js +++ b/src/utils/proxy/http.js @@ -1,3 +1,5 @@ +import dns from "node:dns"; +import net from "node:net"; import { createUnzip, constants as zlibConstants } from "node:zlib"; import { http, https } from "follow-redirects"; @@ -106,10 +108,129 @@ export async function cachedRequest(url, duration = 5, ua = "homepage") { return data; } +// Custom DNS lookup that falls back to Node.js c-ares resolver (dns.resolve) +// when system getaddrinfo (dns.lookup) fails with ENOTFOUND/EAI_NONAME. +// Fixes DNS resolution issues with Alpine/musl libc in k8s +const FALLBACK_CODES = new Set(["ENOTFOUND", "EAI_NONAME"]); + +function homepageDNSLookupFn() { + const normalizeOptions = (options) => { + if (typeof options === "number") { + return { family: options, all: false, lookupOptions: { family: options } }; + } + + const normalized = options ?? {}; + return { + family: normalized.family, + all: Boolean(normalized.all), + lookupOptions: normalized, + }; + }; + + return (hostname, options, callback) => { + // Handle case where options is the callback (2-argument form) + if (typeof options === "function") { + callback = options; + options = {}; + } + + const { family, all, lookupOptions } = normalizeOptions(options); + const sendResponse = (addr, fam) => { + if (all) { + let addresses = addr; + if (!Array.isArray(addresses)) { + addresses = [{ address: addresses, family: fam }]; + } else if (addresses.length && typeof addresses[0] === "string") { + addresses = addresses.map((a) => ({ address: a, family: fam })); + } + + callback(null, addresses); + } else { + callback(null, addr, fam); + } + }; + + // If hostname is already an IP address, return it directly + const ipVersion = net.isIP(hostname); + if (ipVersion) { + sendResponse(hostname, ipVersion); + return; + } + + // Try dns.lookup first (preserves /etc/hosts behavior) + dns.lookup(hostname, lookupOptions, (lookupErr, address, lookupFamily) => { + if (!lookupErr) { + sendResponse(address, lookupFamily); + return; + } + + // ENOTFOUND or EAI_NONAME will try fallback, otherwise return error here + if (!FALLBACK_CODES.has(lookupErr.code)) { + callback(lookupErr); + return; + } + + const finalize = (addresses, resolvedFamily) => { + // Finalize the resolution and call the callback + if (!addresses || addresses.length === 0) { + const err = new Error(`No addresses found for hostname: ${hostname}`); + err.code = "ENOTFOUND"; + callback(err); + return; + } + + logger.debug("DNS fallback to c-ares resolver succeeded for %s", hostname); + + sendResponse(addresses, resolvedFamily); + }; + + const resolveOnce = (fn, resolvedFamily, onFail) => { + // attempt resolution with a specific resolver + fn(hostname, (err, addresses) => { + if (!err) { + finalize(addresses, resolvedFamily); + return; + } + onFail(err); + }); + }; + + const handleFallbackFailure = (resolveErr) => { + // handle final fallback failure with full context + logger.debug( + "DNS fallback failed for %s: lookup error=%s, resolve error=%s", + hostname, + lookupErr.code, + resolveErr?.code, + ); + callback(resolveErr || lookupErr); + }; + + // Fallback to c-ares (dns.resolve*). If family isn't specified, try v4 then v6. + if (family === 6) { + resolveOnce(dns.resolve6, 6, handleFallbackFailure); + return; + } + + if (family === 4) { + resolveOnce(dns.resolve4, 4, handleFallbackFailure); + return; + } + + resolveOnce(dns.resolve4, 4, () => { + resolveOnce(dns.resolve6, 6, handleFallbackFailure); + }); + }); + }; +} + export async function httpProxy(url, params = {}) { const constructedUrl = new URL(url); const disableIpv6 = process.env.HOMEPAGE_PROXY_DISABLE_IPV6 === "true"; - const agentOptions = disableIpv6 ? { family: 4, autoSelectFamily: false } : { autoSelectFamilyAttemptTimeout: 500 }; + const agentOptions = { + ...(disableIpv6 ? { family: 4, autoSelectFamily: false } : { autoSelectFamilyAttemptTimeout: 500 }), + lookup: homepageDNSLookupFn(), + }; let request = null; if (constructedUrl.protocol === "https:") { From 7d019185a3abc16011df052505aea172d70b1e52 Mon Sep 17 00:00:00 2001 From: Kyle Mendell Date: Wed, 4 Feb 2026 20:07:07 -0600 Subject: [PATCH 18/19] Feature: arcane service widget (#6274) Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com> --- docs/widgets/services/arcane.md | 18 +++++++ docs/widgets/services/index.md | 1 + mkdocs.yml | 1 + public/locales/en/common.json | 7 +++ src/utils/config/service-helpers.js | 7 +++ src/widgets/arcane/component.jsx | 73 +++++++++++++++++++++++++++++ src/widgets/arcane/widget.js | 24 ++++++++++ src/widgets/components.js | 1 + src/widgets/widgets.js | 2 + 9 files changed, 134 insertions(+) create mode 100644 docs/widgets/services/arcane.md create mode 100644 src/widgets/arcane/component.jsx create mode 100644 src/widgets/arcane/widget.js diff --git a/docs/widgets/services/arcane.md b/docs/widgets/services/arcane.md new file mode 100644 index 000000000..c8d88207f --- /dev/null +++ b/docs/widgets/services/arcane.md @@ -0,0 +1,18 @@ +--- +title: Arcane +description: Arcane Widget Configuration +--- + +Learn more about [Arcane](https://github.com/getarcaneapp/arcane). + +**Allowed fields** (max 4): `running`, `stopped`, `total`, `images`, `images_used`, `images_unused`, `image_updates`. +**Default fields**: `running`, `stopped`, `total`, `image_updates`. + +```yaml +widget: + type: arcane + url: http://localhost:3552 + env: 0 # required, 0 is Arcane default local environment + key: your-api-key + fields: ["running", "stopped", "total", "image_updates"] # optional +``` diff --git a/docs/widgets/services/index.md b/docs/widgets/services/index.md index 8b43802e9..4aa67bdd6 100644 --- a/docs/widgets/services/index.md +++ b/docs/widgets/services/index.md @@ -9,6 +9,7 @@ You can also find a list of all available service widgets in the sidebar navigat - [Adguard Home](adguard-home.md) - [APC UPS](apcups.md) +- [Arcane](arcane.md) - [ArgoCD](argocd.md) - [Atsumeru](atsumeru.md) - [Audiobookshelf](audiobookshelf.md) diff --git a/mkdocs.yml b/mkdocs.yml index 30dad803f..6b240aee4 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -33,6 +33,7 @@ nav: - widgets/services/index.md - widgets/services/adguard-home.md - widgets/services/apcups.md + - widgets/services/arcane.md - widgets/services/argocd.md - widgets/services/atsumeru.md - widgets/services/audiobookshelf.md diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 2f655bf32..08fed5656 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -1151,6 +1151,13 @@ "time": "Time", "artists": "Artists" }, + "arcane": { + "containers": "Containers", + "images": "Images", + "image_updates": "Image Updates", + "images_unused": "Unused", + "environment_required": "Environment ID Required" + }, "dockhand": { "running": "Running", "stopped": "Stopped", diff --git a/src/utils/config/service-helpers.js b/src/utils/config/service-helpers.js index 75740c3ef..7e913c981 100644 --- a/src/utils/config/service-helpers.js +++ b/src/utils/config/service-helpers.js @@ -258,6 +258,9 @@ export function cleanServiceGroups(groups) { highlight, type, + // arcane + env, + // azuredevops repositoryId, userEmail, @@ -472,6 +475,10 @@ export function cleanServiceGroups(groups) { if (repositoryId) widget.repositoryId = repositoryId; } + if (type === "arcane") { + if (env !== undefined) widget.env = env; + } + if (type === "beszel") { if (systemId) widget.systemId = systemId; } diff --git a/src/widgets/arcane/component.jsx b/src/widgets/arcane/component.jsx new file mode 100644 index 000000000..d8a085156 --- /dev/null +++ b/src/widgets/arcane/component.jsx @@ -0,0 +1,73 @@ +import Block from "components/services/widget/block"; +import Container from "components/services/widget/container"; +import { useTranslation } from "next-i18next"; + +import useWidgetAPI from "utils/proxy/use-widget-api"; + +const MAX_FIELDS = 4; + +export default function Component({ service }) { + const { t } = useTranslation(); + const { widget } = service; + + if (!widget.fields) { + widget.fields = ["running", "stopped", "total", "image_updates"]; + } else if (widget.fields.length > MAX_FIELDS) { + widget.fields = widget.fields.slice(0, MAX_FIELDS); + } + + if (widget?.env == null || widget.env === "") { + return ; + } + + const { data: containers, error: containersError } = useWidgetAPI(widget, "containers"); + const { data: images, error: imagesError } = useWidgetAPI(widget, "images"); + const { data: updates, error: updatesError } = useWidgetAPI(widget, "updates"); + + const error = containersError ?? imagesError ?? updatesError; + if (error) { + return ; + } + + if (!containers || !images || !updates) { + return ( + + + + + + + + + + ); + } + + const runningContainers = containers?.runningContainers ?? 0; + const totalContainers = containers?.totalContainers ?? 0; + const stoppedContainers = containers?.stoppedContainers ?? 0; + const totalImages = images?.totalImages ?? 0; + const imagesInuse = images?.imagesInuse ?? 0; + const imagesUnused = images?.imagesUnused ?? 0; + const imagesWithUpdates = updates?.imagesWithUpdates ?? 0; + + return ( + + + + + + + + + + ); +} diff --git a/src/widgets/arcane/widget.js b/src/widgets/arcane/widget.js new file mode 100644 index 000000000..f71802140 --- /dev/null +++ b/src/widgets/arcane/widget.js @@ -0,0 +1,24 @@ +import { asJson } from "utils/proxy/api-helpers"; +import credentialedProxyHandler from "utils/proxy/handlers/credentialed"; + +const widget = { + api: "{url}/api/{endpoint}", + proxyHandler: credentialedProxyHandler, + + mappings: { + containers: { + endpoint: "environments/{env}/containers/counts", + map: (data) => asJson(data).data, + }, + images: { + endpoint: "environments/{env}/images/counts", + map: (data) => asJson(data).data, + }, + updates: { + endpoint: "environments/{env}/image-updates/summary", + map: (data) => asJson(data).data, + }, + }, +}; + +export default widget; diff --git a/src/widgets/components.js b/src/widgets/components.js index c114a82a5..61585f5f6 100644 --- a/src/widgets/components.js +++ b/src/widgets/components.js @@ -3,6 +3,7 @@ import dynamic from "next/dynamic"; const components = { adguard: dynamic(() => import("./adguard/component")), apcups: dynamic(() => import("./apcups/component")), + arcane: dynamic(() => import("./arcane/component")), argocd: dynamic(() => import("./argocd/component")), atsumeru: dynamic(() => import("./atsumeru/component")), audiobookshelf: dynamic(() => import("./audiobookshelf/component")), diff --git a/src/widgets/widgets.js b/src/widgets/widgets.js index 26235729b..5142ee23c 100644 --- a/src/widgets/widgets.js +++ b/src/widgets/widgets.js @@ -1,5 +1,6 @@ import adguard from "./adguard/widget"; import apcups from "./apcups/widget"; +import arcane from "./arcane/widget"; import argocd from "./argocd/widget"; import atsumeru from "./atsumeru/widget"; import audiobookshelf from "./audiobookshelf/widget"; @@ -152,6 +153,7 @@ import zabbix from "./zabbix/widget"; const widgets = { adguard, apcups, + arcane, argocd, atsumeru, audiobookshelf, From 872a3600aaf4ee6ac8dc85b1dda480e831b02766 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Wed, 4 Feb 2026 19:58:39 -0800 Subject: [PATCH 19/19] Chore: homepage tests (#6278) --- .codecov.yml | 21 + .github/PULL_REQUEST_TEMPLATE.md | 1 + .github/workflows/test.yml | 37 + docs/widgets/authoring/getting-started.md | 26 + docs/widgets/authoring/proxies.md | 15 + eslint.config.mjs | 11 +- package.json | 10 +- pnpm-lock.yaml | 1522 +++++++++++++++++ src/__tests__/pages/_app.test.jsx | 37 + src/__tests__/pages/_document.test.jsx | 24 + src/__tests__/pages/api/bookmarks.test.js | 30 + src/__tests__/pages/api/config/[path].test.js | 87 + .../api/docker/stats/[...service].test.js | 153 ++ .../api/docker/status/[...service].test.js | 211 +++ src/__tests__/pages/api/hash.test.js | 64 + src/__tests__/pages/api/healthcheck.test.js | 16 + .../api/kubernetes/stats/[...service].test.js | 210 +++ .../kubernetes/status/[...service].test.js | 121 ++ src/__tests__/pages/api/ping.test.js | 80 + .../api/proxmox/stats/[...service].test.js | 148 ++ src/__tests__/pages/api/releases.test.js | 46 + src/__tests__/pages/api/revalidate.test.js | 29 + .../pages/api/search/searchSuggestion.test.js | 106 ++ .../pages/api/services/index.test.js | 30 + .../pages/api/services/proxy.test.js | 347 ++++ src/__tests__/pages/api/siteMonitor.test.js | 103 ++ src/__tests__/pages/api/theme.test.js | 41 + src/__tests__/pages/api/validate.test.js | 30 + .../pages/api/widgets/glances.test.js | 123 ++ src/__tests__/pages/api/widgets/index.test.js | 30 + .../pages/api/widgets/kubernetes.test.js | 204 +++ .../pages/api/widgets/longhorn.test.js | 126 ++ .../pages/api/widgets/openmeteo.test.js | 52 + .../pages/api/widgets/openweathermap.test.js | 122 ++ .../pages/api/widgets/resources.test.js | 140 ++ .../pages/api/widgets/stocks.test.js | 117 ++ .../pages/api/widgets/weather.test.js | 98 ++ src/__tests__/pages/browserconfig.xml.test.js | 42 + src/__tests__/pages/index.test.jsx | 533 ++++++ src/__tests__/pages/robots.txt.test.js | 45 + src/__tests__/pages/site.webmanifest.test.js | 96 ++ src/components/bookmarks/group.jsx | 2 +- src/components/bookmarks/group.test.jsx | 86 + .../bookmarks/group.transition.test.jsx | 95 + src/components/bookmarks/item.test.jsx | 41 + src/components/bookmarks/list.test.jsx | 38 + src/components/errorboundry.test.jsx | 38 + src/components/favicon.test.jsx | 74 + src/components/quicklaunch.test.jsx | 390 +++++ src/components/resolvedicon.test.jsx | 82 + src/components/services/dropdown.test.jsx | 56 + src/components/services/group.jsx | 2 +- src/components/services/group.test.jsx | 87 + .../services/group.transition.test.jsx | 92 + src/components/services/item.test.jsx | 247 +++ .../services/kubernetes-status.test.jsx | 65 + src/components/services/list.test.jsx | 35 + src/components/services/ping.test.jsx | 76 + .../services/proxmox-status.test.jsx | 75 + src/components/services/site-monitor.test.jsx | 88 + src/components/services/status.test.jsx | 74 + src/components/services/widget.jsx | 3 +- src/components/services/widget.test.jsx | 38 + src/components/services/widget/block.test.jsx | 41 + .../services/widget/container.test.jsx | 86 + src/components/services/widget/error.test.jsx | 45 + .../widget/highlight-context.test.jsx | 29 + src/components/tab.test.jsx | 32 + src/components/toggles/color.test.jsx | 59 + src/components/toggles/revalidate.test.jsx | 27 + src/components/toggles/theme.test.jsx | 46 + src/components/version.test.jsx | 85 + .../widgets/datetime/datetime.test.jsx | 32 + .../widgets/glances/glances.test.jsx | 166 ++ .../widgets/greeting/greeting.test.jsx | 20 + .../widgets/kubernetes/kubernetes.test.jsx | 55 + .../widgets/kubernetes/node.test.jsx | 35 + src/components/widgets/logo/logo.test.jsx | 26 + .../widgets/longhorn/longhorn.test.jsx | 72 + src/components/widgets/longhorn/node.test.jsx | 32 + .../widgets/openmeteo/openmeteo.test.jsx | 135 ++ .../widgets/openweathermap/weather.test.jsx | 141 ++ .../widgets/queue/queueEntry.test.jsx | 20 + src/components/widgets/resources/cpu.test.jsx | 55 + .../widgets/resources/cputemp.test.jsx | 53 + .../widgets/resources/disk.test.jsx | 53 + .../widgets/resources/memory.test.jsx | 53 + .../widgets/resources/network.test.jsx | 57 + .../widgets/resources/resources.test.jsx | 49 + .../widgets/resources/uptime.test.jsx | 54 + .../widgets/resources/usage-bar.test.jsx | 18 + src/components/widgets/search/search.jsx | 8 +- src/components/widgets/search/search.test.jsx | 198 +++ src/components/widgets/stocks/stocks.test.jsx | 72 + .../unifi_console/unifi_console.test.jsx | 261 +++ .../widgets/weather/weather.test.jsx | 146 ++ src/components/widgets/widget.test.jsx | 58 + .../widgets/widget/container.test.jsx | 76 + .../widgets/widget/container_button.test.jsx | 23 + .../widgets/widget/container_form.test.jsx | 23 + .../widgets/widget/container_link.test.jsx | 24 + src/components/widgets/widget/error.test.jsx | 15 + .../widgets/widget/primary_text.test.jsx | 13 + src/components/widgets/widget/raw.test.jsx | 20 + .../widgets/widget/resource.test.jsx | 38 + .../widgets/widget/resources.test.jsx | 31 + .../widgets/widget/secondary_text.test.jsx | 13 + .../widgets/widget/widget_icon.test.jsx | 30 + .../widgets/widget/widget_label.test.jsx | 13 + src/middleware.test.js | 72 + src/pages/api/search/searchSuggestion.js | 4 + src/pages/api/widgets/longhorn.js | 15 +- src/pages/api/widgets/resources.js | 4 +- src/pages/api/widgets/stocks.js | 3 +- src/test-utils/create-mock-res.js | 36 + src/test-utils/render-with-providers.jsx | 13 + src/test-utils/widget-assertions.js | 4 + src/test-utils/widget-config.js | 52 + src/utils/config/api-response.js | 2 +- src/utils/config/api-response.test.js | 265 +++ src/utils/config/config.check-copy.test.js | 90 + src/utils/config/config.test.js | 59 + src/utils/config/docker.js | 14 +- src/utils/config/docker.test.js | 109 ++ src/utils/config/kubernetes.test.js | 108 ++ src/utils/config/proxmox.test.js | 41 + src/utils/config/service-helpers.js | 3 +- src/utils/config/service-helpers.test.js | 587 +++++++ src/utils/config/shvl.test.js | 30 + src/utils/config/widget-helpers.test.js | 88 + src/utils/contexts/color.jsx | 9 +- src/utils/contexts/color.test.jsx | 53 + src/utils/contexts/settings.jsx | 10 +- src/utils/contexts/settings.test.jsx | 33 + src/utils/contexts/tab.jsx | 10 +- src/utils/contexts/tab.test.jsx | 33 + src/utils/contexts/theme.jsx | 9 +- src/utils/contexts/theme.test.jsx | 64 + src/utils/highlights.test.js | 191 +++ src/utils/hooks/window-focus.test.jsx | 27 + src/utils/kubernetes/export.test.js | 28 + src/utils/kubernetes/httproute-list.test.js | 106 ++ src/utils/kubernetes/ingress-list.js | 2 +- src/utils/kubernetes/ingress-list.test.js | 79 + src/utils/kubernetes/resource-helpers.test.js | 199 +++ src/utils/kubernetes/traefik-list.test.js | 111 ++ src/utils/kubernetes/utils.test.js | 24 + src/utils/layout/columns.test.js | 12 + src/utils/logger.test.js | 182 ++ src/utils/proxy/api-helpers.test.js | 92 + src/utils/proxy/cookie-jar.test.js | 45 + src/utils/proxy/handlers/credentialed.js | 5 + src/utils/proxy/handlers/credentialed.test.js | 404 +++++ src/utils/proxy/handlers/generic.test.js | 256 +++ src/utils/proxy/handlers/jsonrpc.test.js | 219 +++ src/utils/proxy/handlers/synology.js | 6 +- src/utils/proxy/handlers/synology.test.js | 380 ++++ src/utils/proxy/http.js | 9 +- src/utils/proxy/http.test.js | 423 +++++ src/utils/proxy/use-widget-api.test.js | 49 + src/utils/proxy/validate-widget-data.test.js | 44 + src/utils/styles/themes.test.js | 17 + src/utils/weather/condition-map.test.js | 15 + .../weather/openmeteo-condition-map.test.js | 15 + src/utils/weather/owm-condition-map.test.js | 15 + src/widgets/adguard/component.test.jsx | 63 + src/widgets/adguard/widget.test.js | 12 + src/widgets/apcups/component.test.jsx | 48 + src/widgets/apcups/proxy.test.js | 88 + src/widgets/apcups/widget.test.js | 13 + src/widgets/arcane/component.test.jsx | 123 ++ src/widgets/arcane/widget.test.js | 11 + src/widgets/argocd/component.test.jsx | 65 + src/widgets/argocd/widget.test.js | 11 + src/widgets/atsumeru/component.test.jsx | 51 + src/widgets/atsumeru/widget.test.js | 11 + src/widgets/audiobookshelf/component.test.jsx | 62 + src/widgets/audiobookshelf/proxy.test.js | 67 + src/widgets/audiobookshelf/widget.test.js | 12 + src/widgets/authentik/component.test.jsx | 108 ++ src/widgets/authentik/widget.test.js | 13 + src/widgets/autobrr/component.test.jsx | 60 + src/widgets/autobrr/widget.test.js | 11 + src/widgets/azuredevops/component.test.jsx | 101 ++ src/widgets/azuredevops/widget.test.js | 11 + src/widgets/backrest/component.test.jsx | 81 + src/widgets/backrest/proxy.js | 4 +- src/widgets/backrest/proxy.test.js | 212 +++ src/widgets/backrest/widget.test.js | 13 + src/widgets/bazarr/component.test.jsx | 56 + src/widgets/bazarr/widget.test.js | 16 + src/widgets/beszel/component.test.jsx | 97 ++ src/widgets/beszel/proxy.test.js | 117 ++ src/widgets/beszel/widget.test.js | 11 + src/widgets/booklore/component.test.jsx | 50 + src/widgets/booklore/proxy.test.js | 98 ++ src/widgets/booklore/widget.test.js | 11 + src/widgets/caddy/component.test.jsx | 64 + src/widgets/caddy/widget.test.js | 11 + src/widgets/calendar/agenda.test.jsx | 64 + src/widgets/calendar/component.jsx | 3 +- src/widgets/calendar/component.test.jsx | 89 + src/widgets/calendar/event.test.jsx | 56 + .../calendar/integrations/ical.test.jsx | 64 + .../calendar/integrations/lidarr.test.jsx | 39 + .../calendar/integrations/radarr.test.jsx | 49 + .../calendar/integrations/readarr.test.jsx | 48 + .../calendar/integrations/sonarr.test.jsx | 48 + src/widgets/calendar/monthly.test.jsx | 71 + src/widgets/calendar/proxy.test.js | 95 + src/widgets/calendar/widget.test.js | 12 + src/widgets/calibreweb/component.test.jsx | 48 + src/widgets/calibreweb/widget.test.js | 11 + .../changedetectionio/component.test.jsx | 56 + src/widgets/changedetectionio/widget.test.js | 11 + .../channelsdvrserver/component.test.jsx | 55 + src/widgets/channelsdvrserver/widget.test.js | 11 + src/widgets/checkmk/component.test.jsx | 69 + src/widgets/checkmk/widget.test.js | 11 + src/widgets/cloudflared/component.test.jsx | 68 + src/widgets/cloudflared/widget.test.js | 11 + src/widgets/coinmarketcap/component.test.jsx | 86 + src/widgets/coinmarketcap/widget.test.js | 11 + src/widgets/crowdsec/component.test.jsx | 61 + src/widgets/crowdsec/proxy.test.js | 92 + src/widgets/crowdsec/widget.test.js | 11 + src/widgets/customapi/component.test.jsx | 202 +++ src/widgets/customapi/widget.test.js | 11 + src/widgets/deluge/component.test.jsx | 84 + src/widgets/deluge/proxy.test.js | 63 + src/widgets/deluge/widget.test.js | 11 + src/widgets/develancacheui/component.test.jsx | 44 + src/widgets/develancacheui/widget.test.js | 11 + src/widgets/diskstation/component.test.jsx | 61 + src/widgets/diskstation/widget.test.js | 11 + src/widgets/dispatcharr/component.test.jsx | 54 + src/widgets/dispatcharr/proxy.test.js | 108 ++ src/widgets/dispatcharr/widget.test.js | 11 + src/widgets/docker/component.test.jsx | 61 + src/widgets/docker/stats-helpers.test.js | 55 + src/widgets/dockhand/component.test.jsx | 61 + src/widgets/dockhand/proxy.test.js | 82 + src/widgets/dockhand/widget.test.js | 11 + .../downloadstation/component.test.jsx | 62 + src/widgets/downloadstation/widget.test.js | 11 + src/widgets/emby/component.test.jsx | 103 ++ src/widgets/emby/widget.test.js | 11 + src/widgets/esphome/component.test.jsx | 57 + src/widgets/esphome/widget.test.js | 11 + src/widgets/evcc/component.test.jsx | 72 + src/widgets/evcc/widget.test.js | 11 + src/widgets/filebrowser/component.test.jsx | 62 + src/widgets/filebrowser/proxy.test.js | 79 + src/widgets/filebrowser/widget.test.js | 11 + src/widgets/fileflows/component.test.jsx | 71 + src/widgets/fileflows/widget.test.js | 11 + src/widgets/firefly/component.test.jsx | 74 + src/widgets/firefly/widget.test.js | 11 + src/widgets/flood/component.test.jsx | 68 + src/widgets/flood/proxy.test.js | 69 + src/widgets/flood/widget.test.js | 11 + src/widgets/freshrss/component.test.jsx | 64 + src/widgets/freshrss/proxy.test.js | 112 ++ src/widgets/freshrss/widget.test.js | 11 + src/widgets/frigate/component.test.jsx | 83 + src/widgets/frigate/proxy.js | 7 +- src/widgets/frigate/proxy.test.js | 251 +++ src/widgets/frigate/widget.test.js | 11 + src/widgets/fritzbox/component.test.jsx | 90 + src/widgets/fritzbox/proxy.test.js | 76 + src/widgets/fritzbox/widget.test.js | 11 + src/widgets/gamedig/component.test.jsx | 72 + src/widgets/gamedig/proxy.test.js | 67 + src/widgets/gamedig/widget.test.js | 11 + src/widgets/gatus/component.test.jsx | 66 + src/widgets/gatus/widget.test.js | 11 + src/widgets/ghostfolio/component.test.jsx | 93 + src/widgets/ghostfolio/widget.test.js | 11 + src/widgets/gitea/component.test.jsx | 75 + src/widgets/gitea/widget.test.js | 11 + src/widgets/gitlab/component.test.jsx | 66 + src/widgets/gitlab/widget.test.js | 11 + src/widgets/glances/component.test.jsx | 59 + src/widgets/glances/components/block.test.jsx | 21 + src/widgets/glances/components/chart.test.jsx | 31 + .../glances/components/chart_dual.test.jsx | 38 + .../glances/components/container.test.jsx | 37 + .../components/custom_tooltip.test.jsx | 29 + src/widgets/glances/components/error.test.jsx | 13 + .../glances/metrics/containers.test.jsx | 24 + src/widgets/glances/metrics/cpu.test.jsx | 22 + src/widgets/glances/metrics/disk.test.jsx | 25 + src/widgets/glances/metrics/fs.test.jsx | 25 + src/widgets/glances/metrics/gpu.test.jsx | 25 + src/widgets/glances/metrics/info.test.jsx | 20 + src/widgets/glances/metrics/memory.test.jsx | 22 + src/widgets/glances/metrics/net.test.jsx | 23 + src/widgets/glances/metrics/process.test.jsx | 22 + src/widgets/glances/metrics/sensor.test.jsx | 25 + src/widgets/glances/widget.test.js | 13 + src/widgets/gluetun/component.test.jsx | 75 + src/widgets/gluetun/widget.test.js | 11 + src/widgets/gotify/component.test.jsx | 70 + src/widgets/gotify/widget.test.js | 11 + src/widgets/grafana/component.test.jsx | 95 + src/widgets/grafana/widget.test.js | 11 + src/widgets/hdhomerun/component.test.jsx | 80 + src/widgets/hdhomerun/widget.test.js | 11 + src/widgets/headscale/component.test.jsx | 78 + src/widgets/headscale/widget.test.js | 11 + src/widgets/healthchecks/component.test.jsx | 82 + src/widgets/healthchecks/widget.test.js | 11 + src/widgets/homeassistant/component.test.jsx | 47 + src/widgets/homeassistant/proxy.test.js | 60 + src/widgets/homeassistant/widget.test.js | 11 + src/widgets/homebox/component.test.jsx | 78 + src/widgets/homebox/proxy.test.js | 73 + src/widgets/homebox/widget.test.js | 11 + src/widgets/homebridge/component.test.jsx | 63 + src/widgets/homebridge/proxy.test.js | 96 ++ src/widgets/homebridge/widget.test.js | 11 + src/widgets/iframe/component.test.jsx | 31 + src/widgets/iframe/widget.test.js | 11 + src/widgets/immich/component.test.jsx | 73 + src/widgets/immich/widget.test.js | 13 + src/widgets/jackett/component.test.jsx | 71 + src/widgets/jackett/proxy.test.js | 79 + src/widgets/jackett/widget.test.js | 11 + src/widgets/jdownloader/component.test.jsx | 83 + src/widgets/jdownloader/proxy.test.js | 79 + src/widgets/jdownloader/tools.test.js | 39 + src/widgets/jdownloader/widget.test.js | 11 + src/widgets/jellyfin/component.test.jsx | 92 + src/widgets/jellyfin/proxy.test.js | 102 ++ src/widgets/jellyfin/widget.test.js | 11 + src/widgets/jellyseerr/component.test.jsx | 63 + src/widgets/jellyseerr/widget.test.js | 11 + src/widgets/jellystat/component.test.jsx | 65 + src/widgets/jellystat/widget.test.js | 11 + src/widgets/karakeep/component.test.jsx | 79 + src/widgets/karakeep/widget.test.js | 11 + src/widgets/kavita/component.test.jsx | 51 + src/widgets/kavita/proxy.test.js | 97 ++ src/widgets/kavita/widget.test.js | 11 + src/widgets/komga/component.test.jsx | 67 + src/widgets/komga/proxy.test.js | 76 + src/widgets/komga/widget.test.js | 11 + src/widgets/komodo/component.test.jsx | 80 + src/widgets/komodo/proxy.test.js | 72 + src/widgets/komodo/widget.test.js | 11 + src/widgets/kopia/component.test.jsx | 83 + src/widgets/kopia/widget.test.js | 11 + src/widgets/kubernetes/component.test.jsx | 68 + src/widgets/lidarr/component.test.jsx | 69 + src/widgets/lidarr/widget.test.js | 11 + src/widgets/linkwarden/component.jsx | 36 +- src/widgets/linkwarden/component.test.jsx | 76 + src/widgets/linkwarden/widget.test.js | 11 + src/widgets/lubelogger/component.test.jsx | 106 ++ src/widgets/lubelogger/widget.test.js | 11 + src/widgets/mailcow/component.test.jsx | 73 + src/widgets/mailcow/widget.test.js | 11 + src/widgets/mastodon/component.test.jsx | 69 + src/widgets/mastodon/widget.test.js | 11 + src/widgets/mealie/component.test.jsx | 57 + src/widgets/mealie/widget.test.js | 11 + src/widgets/medusa/component.test.jsx | 81 + src/widgets/medusa/widget.test.js | 11 + src/widgets/mikrotik/component.test.jsx | 82 + src/widgets/mikrotik/widget.test.js | 11 + src/widgets/minecraft/component.test.jsx | 65 + src/widgets/minecraft/proxy.test.js | 66 + src/widgets/minecraft/widget.test.js | 11 + src/widgets/miniflux/component.test.jsx | 56 + src/widgets/miniflux/widget.test.js | 11 + src/widgets/mjpeg/component.test.jsx | 31 + src/widgets/mjpeg/widget.test.js | 11 + src/widgets/moonraker/component.test.jsx | 90 + src/widgets/moonraker/widget.test.js | 11 + src/widgets/mylar/component.test.jsx | 68 + src/widgets/mylar/widget.test.js | 11 + src/widgets/myspeed/component.test.jsx | 59 + src/widgets/myspeed/widget.test.js | 11 + src/widgets/navidrome/component.test.jsx | 53 + src/widgets/navidrome/widget.test.js | 11 + src/widgets/netalertx/component.test.jsx | 55 + src/widgets/netalertx/widget.test.js | 11 + src/widgets/netdata/component.test.jsx | 59 + src/widgets/netdata/widget.test.js | 11 + src/widgets/nextcloud/component.test.jsx | 83 + src/widgets/nextcloud/widget.test.js | 11 + src/widgets/nextdns/component.test.jsx | 53 + src/widgets/nextdns/widget.test.js | 11 + src/widgets/npm/component.test.jsx | 61 + src/widgets/npm/proxy.test.js | 114 ++ src/widgets/npm/widget.test.js | 11 + src/widgets/nzbget/component.test.jsx | 61 + src/widgets/nzbget/widget.test.js | 11 + src/widgets/octoprint/component.test.jsx | 76 + src/widgets/octoprint/widget.test.js | 11 + src/widgets/omada/component.test.jsx | 71 + src/widgets/omada/proxy.test.js | 321 ++++ src/widgets/omada/widget.test.js | 11 + src/widgets/ombi/component.test.jsx | 58 + src/widgets/ombi/widget.test.js | 11 + src/widgets/opendtu/component.test.jsx | 71 + src/widgets/opendtu/widget.test.js | 11 + src/widgets/openmediavault/component.test.jsx | 38 + .../downloader_get_downloadlist.test.jsx | 42 + .../methods/services_get_status.test.jsx | 35 + .../methods/smart_get_list.test.jsx | 41 + src/widgets/openmediavault/proxy.js | 2 +- src/widgets/openmediavault/proxy.test.js | 291 ++++ src/widgets/openmediavault/widget.test.js | 11 + src/widgets/openwrt/component.test.jsx | 26 + .../openwrt/methods/interface.test.jsx | 37 + src/widgets/openwrt/methods/system.test.jsx | 33 + src/widgets/openwrt/proxy.test.js | 59 + src/widgets/openwrt/widget.test.js | 11 + src/widgets/opnsense/component.test.jsx | 86 + src/widgets/opnsense/widget.test.js | 11 + src/widgets/overseerr/component.test.jsx | 60 + src/widgets/overseerr/widget.test.js | 11 + src/widgets/pangolin/component.test.jsx | 124 ++ src/widgets/pangolin/widget.test.js | 11 + src/widgets/paperlessngx/component.test.jsx | 69 + src/widgets/paperlessngx/widget.test.js | 11 + src/widgets/peanut/component.test.jsx | 52 + src/widgets/peanut/widget.test.js | 11 + src/widgets/pfsense/component.test.jsx | 76 + src/widgets/pfsense/widget.test.js | 11 + src/widgets/photoprism/component.test.jsx | 62 + src/widgets/photoprism/proxy.test.js | 41 + src/widgets/photoprism/widget.test.js | 11 + src/widgets/pihole/component.test.jsx | 93 + src/widgets/pihole/proxy.test.js | 174 ++ src/widgets/pihole/widget.test.js | 11 + src/widgets/plantit/component.test.jsx | 63 + src/widgets/plantit/widget.js | 7 - src/widgets/plantit/widget.test.js | 11 + src/widgets/plex/component.test.jsx | 60 + src/widgets/plex/proxy.test.js | 93 + src/widgets/plex/widget.test.js | 11 + src/widgets/portainer/component.test.jsx | 86 + src/widgets/portainer/widget.test.js | 11 + src/widgets/prometheus/component.test.jsx | 63 + src/widgets/prometheus/widget.test.js | 11 + .../prometheusmetric/component.test.jsx | 115 ++ src/widgets/prometheusmetric/widget.test.js | 11 + src/widgets/prowlarr/component.test.jsx | 68 + src/widgets/prowlarr/widget.test.js | 11 + src/widgets/proxmox/component.test.jsx | 63 + src/widgets/proxmox/widget.test.js | 11 + .../proxmoxbackupserver/component.test.jsx | 77 + .../proxmoxbackupserver/widget.test.js | 11 + src/widgets/proxmoxvm/component.test.jsx | 49 + src/widgets/pterodactyl/component.test.jsx | 55 + src/widgets/pterodactyl/widget.test.js | 11 + src/widgets/pyload/component.test.jsx | 60 + src/widgets/pyload/proxy.test.js | 105 ++ src/widgets/pyload/widget.test.js | 11 + src/widgets/qbittorrent/component.test.jsx | 63 + src/widgets/qbittorrent/proxy.test.js | 67 + src/widgets/qbittorrent/widget.test.js | 11 + src/widgets/qnap/component.test.jsx | 66 + src/widgets/qnap/proxy.test.js | 82 + src/widgets/qnap/widget.test.js | 11 + src/widgets/radarr/component.test.jsx | 65 + src/widgets/radarr/widget.test.js | 11 + src/widgets/readarr/component.test.jsx | 54 + src/widgets/readarr/widget.test.js | 11 + src/widgets/romm/component.test.jsx | 65 + src/widgets/romm/widget.test.js | 11 + src/widgets/rutorrent/component.test.jsx | 63 + src/widgets/rutorrent/proxy.test.js | 48 + src/widgets/rutorrent/widget.test.js | 11 + src/widgets/sabnzbd/component.test.jsx | 62 + src/widgets/sabnzbd/widget.test.js | 11 + src/widgets/scrutiny/component.test.jsx | 69 + src/widgets/scrutiny/widget.test.js | 11 + src/widgets/slskd/component.test.jsx | 75 + src/widgets/slskd/widget.test.js | 11 + src/widgets/sonarr/component.test.jsx | 74 + src/widgets/sonarr/widget.test.js | 11 + src/widgets/speedtest/component.test.jsx | 52 + src/widgets/speedtest/widget.test.js | 11 + src/widgets/spoolman/component.test.jsx | 63 + src/widgets/spoolman/widget.test.js | 11 + src/widgets/stash/component.test.jsx | 59 + src/widgets/stash/widget.test.js | 11 + src/widgets/stocks/component.test.jsx | 43 + src/widgets/stocks/widget.test.js | 11 + src/widgets/strelaysrv/component.test.jsx | 53 + src/widgets/strelaysrv/widget.test.js | 11 + src/widgets/suwayomi/component.test.jsx | 55 + src/widgets/suwayomi/proxy.test.js | 68 + src/widgets/suwayomi/widget.test.js | 11 + src/widgets/swagdashboard/component.test.jsx | 53 + src/widgets/swagdashboard/widget.test.js | 11 + src/widgets/tailscale/component.test.jsx | 63 + src/widgets/tailscale/widget.test.js | 11 + src/widgets/tandoor/component.test.jsx | 53 + src/widgets/tandoor/widget.test.js | 11 + src/widgets/tautulli/component.test.jsx | 69 + src/widgets/tautulli/widget.test.js | 11 + src/widgets/tdarr/component.test.jsx | 64 + src/widgets/tdarr/proxy.test.js | 50 + src/widgets/tdarr/widget.test.js | 11 + src/widgets/technitium/component.test.jsx | 66 + src/widgets/technitium/widget.test.js | 11 + src/widgets/traefik/component.test.jsx | 52 + src/widgets/traefik/widget.test.js | 11 + src/widgets/transmission/component.test.jsx | 61 + src/widgets/transmission/proxy.test.js | 79 + src/widgets/transmission/widget.test.js | 11 + src/widgets/trilium/component.test.jsx | 52 + src/widgets/trilium/widget.test.js | 11 + src/widgets/truenas/component.test.jsx | 81 + src/widgets/truenas/pool.test.jsx | 19 + src/widgets/truenas/proxy.test.js | 94 + src/widgets/truenas/widget.test.js | 11 + src/widgets/tubearchivist/component.test.jsx | 57 + src/widgets/tubearchivist/widget.test.js | 11 + src/widgets/unifi/component.test.jsx | 82 + src/widgets/unifi/proxy.test.js | 92 + src/widgets/unifi/widget.test.js | 11 + src/widgets/unmanic/component.test.jsx | 49 + src/widgets/unmanic/widget.test.js | 11 + src/widgets/unraid/component.test.jsx | 67 + src/widgets/unraid/proxy.test.js | 85 + src/widgets/unraid/widget.test.js | 12 + src/widgets/uptimekuma/component.test.jsx | 66 + src/widgets/uptimekuma/widget.test.js | 11 + src/widgets/uptimerobot/component.test.jsx | 48 + src/widgets/uptimerobot/widget.test.js | 11 + src/widgets/urbackup/component.test.jsx | 95 + src/widgets/urbackup/proxy.test.js | 120 ++ src/widgets/urbackup/widget.test.js | 11 + src/widgets/vikunja/component.test.jsx | 74 + src/widgets/vikunja/widget.test.js | 11 + src/widgets/wallos/component.test.jsx | 65 + src/widgets/wallos/widget.test.js | 11 + src/widgets/watchtower/component.test.jsx | 52 + src/widgets/watchtower/proxy.test.js | 64 + src/widgets/watchtower/widget.test.js | 11 + src/widgets/wgeasy/component.test.jsx | 64 + src/widgets/wgeasy/widget.test.js | 11 + src/widgets/whatsupdocker/component.test.jsx | 50 + src/widgets/whatsupdocker/widget.test.js | 11 + src/widgets/xteve/component.test.jsx | 52 + src/widgets/xteve/proxy.test.js | 101 ++ src/widgets/xteve/widget.test.js | 11 + src/widgets/yourspotify/component.test.jsx | 59 + src/widgets/yourspotify/widget.test.js | 11 + src/widgets/zabbix/component.test.jsx | 65 + src/widgets/zabbix/widget.test.js | 11 + vitest.config.mjs | 54 + vitest.setup.js | 32 + 558 files changed, 32606 insertions(+), 84 deletions(-) create mode 100644 .codecov.yml create mode 100644 .github/workflows/test.yml create mode 100644 src/__tests__/pages/_app.test.jsx create mode 100644 src/__tests__/pages/_document.test.jsx create mode 100644 src/__tests__/pages/api/bookmarks.test.js create mode 100644 src/__tests__/pages/api/config/[path].test.js create mode 100644 src/__tests__/pages/api/docker/stats/[...service].test.js create mode 100644 src/__tests__/pages/api/docker/status/[...service].test.js create mode 100644 src/__tests__/pages/api/hash.test.js create mode 100644 src/__tests__/pages/api/healthcheck.test.js create mode 100644 src/__tests__/pages/api/kubernetes/stats/[...service].test.js create mode 100644 src/__tests__/pages/api/kubernetes/status/[...service].test.js create mode 100644 src/__tests__/pages/api/ping.test.js create mode 100644 src/__tests__/pages/api/proxmox/stats/[...service].test.js create mode 100644 src/__tests__/pages/api/releases.test.js create mode 100644 src/__tests__/pages/api/revalidate.test.js create mode 100644 src/__tests__/pages/api/search/searchSuggestion.test.js create mode 100644 src/__tests__/pages/api/services/index.test.js create mode 100644 src/__tests__/pages/api/services/proxy.test.js create mode 100644 src/__tests__/pages/api/siteMonitor.test.js create mode 100644 src/__tests__/pages/api/theme.test.js create mode 100644 src/__tests__/pages/api/validate.test.js create mode 100644 src/__tests__/pages/api/widgets/glances.test.js create mode 100644 src/__tests__/pages/api/widgets/index.test.js create mode 100644 src/__tests__/pages/api/widgets/kubernetes.test.js create mode 100644 src/__tests__/pages/api/widgets/longhorn.test.js create mode 100644 src/__tests__/pages/api/widgets/openmeteo.test.js create mode 100644 src/__tests__/pages/api/widgets/openweathermap.test.js create mode 100644 src/__tests__/pages/api/widgets/resources.test.js create mode 100644 src/__tests__/pages/api/widgets/stocks.test.js create mode 100644 src/__tests__/pages/api/widgets/weather.test.js create mode 100644 src/__tests__/pages/browserconfig.xml.test.js create mode 100644 src/__tests__/pages/index.test.jsx create mode 100644 src/__tests__/pages/robots.txt.test.js create mode 100644 src/__tests__/pages/site.webmanifest.test.js create mode 100644 src/components/bookmarks/group.test.jsx create mode 100644 src/components/bookmarks/group.transition.test.jsx create mode 100644 src/components/bookmarks/item.test.jsx create mode 100644 src/components/bookmarks/list.test.jsx create mode 100644 src/components/errorboundry.test.jsx create mode 100644 src/components/favicon.test.jsx create mode 100644 src/components/quicklaunch.test.jsx create mode 100644 src/components/resolvedicon.test.jsx create mode 100644 src/components/services/dropdown.test.jsx create mode 100644 src/components/services/group.test.jsx create mode 100644 src/components/services/group.transition.test.jsx create mode 100644 src/components/services/item.test.jsx create mode 100644 src/components/services/kubernetes-status.test.jsx create mode 100644 src/components/services/list.test.jsx create mode 100644 src/components/services/ping.test.jsx create mode 100644 src/components/services/proxmox-status.test.jsx create mode 100644 src/components/services/site-monitor.test.jsx create mode 100644 src/components/services/status.test.jsx create mode 100644 src/components/services/widget.test.jsx create mode 100644 src/components/services/widget/block.test.jsx create mode 100644 src/components/services/widget/container.test.jsx create mode 100644 src/components/services/widget/error.test.jsx create mode 100644 src/components/services/widget/highlight-context.test.jsx create mode 100644 src/components/tab.test.jsx create mode 100644 src/components/toggles/color.test.jsx create mode 100644 src/components/toggles/revalidate.test.jsx create mode 100644 src/components/toggles/theme.test.jsx create mode 100644 src/components/version.test.jsx create mode 100644 src/components/widgets/datetime/datetime.test.jsx create mode 100644 src/components/widgets/glances/glances.test.jsx create mode 100644 src/components/widgets/greeting/greeting.test.jsx create mode 100644 src/components/widgets/kubernetes/kubernetes.test.jsx create mode 100644 src/components/widgets/kubernetes/node.test.jsx create mode 100644 src/components/widgets/logo/logo.test.jsx create mode 100644 src/components/widgets/longhorn/longhorn.test.jsx create mode 100644 src/components/widgets/longhorn/node.test.jsx create mode 100644 src/components/widgets/openmeteo/openmeteo.test.jsx create mode 100644 src/components/widgets/openweathermap/weather.test.jsx create mode 100644 src/components/widgets/queue/queueEntry.test.jsx create mode 100644 src/components/widgets/resources/cpu.test.jsx create mode 100644 src/components/widgets/resources/cputemp.test.jsx create mode 100644 src/components/widgets/resources/disk.test.jsx create mode 100644 src/components/widgets/resources/memory.test.jsx create mode 100644 src/components/widgets/resources/network.test.jsx create mode 100644 src/components/widgets/resources/resources.test.jsx create mode 100644 src/components/widgets/resources/uptime.test.jsx create mode 100644 src/components/widgets/resources/usage-bar.test.jsx create mode 100644 src/components/widgets/search/search.test.jsx create mode 100644 src/components/widgets/stocks/stocks.test.jsx create mode 100644 src/components/widgets/unifi_console/unifi_console.test.jsx create mode 100644 src/components/widgets/weather/weather.test.jsx create mode 100644 src/components/widgets/widget.test.jsx create mode 100644 src/components/widgets/widget/container.test.jsx create mode 100644 src/components/widgets/widget/container_button.test.jsx create mode 100644 src/components/widgets/widget/container_form.test.jsx create mode 100644 src/components/widgets/widget/container_link.test.jsx create mode 100644 src/components/widgets/widget/error.test.jsx create mode 100644 src/components/widgets/widget/primary_text.test.jsx create mode 100644 src/components/widgets/widget/raw.test.jsx create mode 100644 src/components/widgets/widget/resource.test.jsx create mode 100644 src/components/widgets/widget/resources.test.jsx create mode 100644 src/components/widgets/widget/secondary_text.test.jsx create mode 100644 src/components/widgets/widget/widget_icon.test.jsx create mode 100644 src/components/widgets/widget/widget_label.test.jsx create mode 100644 src/middleware.test.js create mode 100644 src/test-utils/create-mock-res.js create mode 100644 src/test-utils/render-with-providers.jsx create mode 100644 src/test-utils/widget-assertions.js create mode 100644 src/test-utils/widget-config.js create mode 100644 src/utils/config/api-response.test.js create mode 100644 src/utils/config/config.check-copy.test.js create mode 100644 src/utils/config/config.test.js create mode 100644 src/utils/config/docker.test.js create mode 100644 src/utils/config/kubernetes.test.js create mode 100644 src/utils/config/proxmox.test.js create mode 100644 src/utils/config/service-helpers.test.js create mode 100644 src/utils/config/shvl.test.js create mode 100644 src/utils/config/widget-helpers.test.js create mode 100644 src/utils/contexts/color.test.jsx create mode 100644 src/utils/contexts/settings.test.jsx create mode 100644 src/utils/contexts/tab.test.jsx create mode 100644 src/utils/contexts/theme.test.jsx create mode 100644 src/utils/highlights.test.js create mode 100644 src/utils/hooks/window-focus.test.jsx create mode 100644 src/utils/kubernetes/export.test.js create mode 100644 src/utils/kubernetes/httproute-list.test.js create mode 100644 src/utils/kubernetes/ingress-list.test.js create mode 100644 src/utils/kubernetes/resource-helpers.test.js create mode 100644 src/utils/kubernetes/traefik-list.test.js create mode 100644 src/utils/kubernetes/utils.test.js create mode 100644 src/utils/layout/columns.test.js create mode 100644 src/utils/logger.test.js create mode 100644 src/utils/proxy/api-helpers.test.js create mode 100644 src/utils/proxy/cookie-jar.test.js create mode 100644 src/utils/proxy/handlers/credentialed.test.js create mode 100644 src/utils/proxy/handlers/generic.test.js create mode 100644 src/utils/proxy/handlers/jsonrpc.test.js create mode 100644 src/utils/proxy/handlers/synology.test.js create mode 100644 src/utils/proxy/http.test.js create mode 100644 src/utils/proxy/use-widget-api.test.js create mode 100644 src/utils/proxy/validate-widget-data.test.js create mode 100644 src/utils/styles/themes.test.js create mode 100644 src/utils/weather/condition-map.test.js create mode 100644 src/utils/weather/openmeteo-condition-map.test.js create mode 100644 src/utils/weather/owm-condition-map.test.js create mode 100644 src/widgets/adguard/component.test.jsx create mode 100644 src/widgets/adguard/widget.test.js create mode 100644 src/widgets/apcups/component.test.jsx create mode 100644 src/widgets/apcups/proxy.test.js create mode 100644 src/widgets/apcups/widget.test.js create mode 100644 src/widgets/arcane/component.test.jsx create mode 100644 src/widgets/arcane/widget.test.js create mode 100644 src/widgets/argocd/component.test.jsx create mode 100644 src/widgets/argocd/widget.test.js create mode 100644 src/widgets/atsumeru/component.test.jsx create mode 100644 src/widgets/atsumeru/widget.test.js create mode 100644 src/widgets/audiobookshelf/component.test.jsx create mode 100644 src/widgets/audiobookshelf/proxy.test.js create mode 100644 src/widgets/audiobookshelf/widget.test.js create mode 100644 src/widgets/authentik/component.test.jsx create mode 100644 src/widgets/authentik/widget.test.js create mode 100644 src/widgets/autobrr/component.test.jsx create mode 100644 src/widgets/autobrr/widget.test.js create mode 100644 src/widgets/azuredevops/component.test.jsx create mode 100644 src/widgets/azuredevops/widget.test.js create mode 100644 src/widgets/backrest/component.test.jsx create mode 100644 src/widgets/backrest/proxy.test.js create mode 100644 src/widgets/backrest/widget.test.js create mode 100644 src/widgets/bazarr/component.test.jsx create mode 100644 src/widgets/bazarr/widget.test.js create mode 100644 src/widgets/beszel/component.test.jsx create mode 100644 src/widgets/beszel/proxy.test.js create mode 100644 src/widgets/beszel/widget.test.js create mode 100644 src/widgets/booklore/component.test.jsx create mode 100644 src/widgets/booklore/proxy.test.js create mode 100644 src/widgets/booklore/widget.test.js create mode 100644 src/widgets/caddy/component.test.jsx create mode 100644 src/widgets/caddy/widget.test.js create mode 100644 src/widgets/calendar/agenda.test.jsx create mode 100644 src/widgets/calendar/component.test.jsx create mode 100644 src/widgets/calendar/event.test.jsx create mode 100644 src/widgets/calendar/integrations/ical.test.jsx create mode 100644 src/widgets/calendar/integrations/lidarr.test.jsx create mode 100644 src/widgets/calendar/integrations/radarr.test.jsx create mode 100644 src/widgets/calendar/integrations/readarr.test.jsx create mode 100644 src/widgets/calendar/integrations/sonarr.test.jsx create mode 100644 src/widgets/calendar/monthly.test.jsx create mode 100644 src/widgets/calendar/proxy.test.js create mode 100644 src/widgets/calendar/widget.test.js create mode 100644 src/widgets/calibreweb/component.test.jsx create mode 100644 src/widgets/calibreweb/widget.test.js create mode 100644 src/widgets/changedetectionio/component.test.jsx create mode 100644 src/widgets/changedetectionio/widget.test.js create mode 100644 src/widgets/channelsdvrserver/component.test.jsx create mode 100644 src/widgets/channelsdvrserver/widget.test.js create mode 100644 src/widgets/checkmk/component.test.jsx create mode 100644 src/widgets/checkmk/widget.test.js create mode 100644 src/widgets/cloudflared/component.test.jsx create mode 100644 src/widgets/cloudflared/widget.test.js create mode 100644 src/widgets/coinmarketcap/component.test.jsx create mode 100644 src/widgets/coinmarketcap/widget.test.js create mode 100644 src/widgets/crowdsec/component.test.jsx create mode 100644 src/widgets/crowdsec/proxy.test.js create mode 100644 src/widgets/crowdsec/widget.test.js create mode 100644 src/widgets/customapi/component.test.jsx create mode 100644 src/widgets/customapi/widget.test.js create mode 100644 src/widgets/deluge/component.test.jsx create mode 100644 src/widgets/deluge/proxy.test.js create mode 100644 src/widgets/deluge/widget.test.js create mode 100644 src/widgets/develancacheui/component.test.jsx create mode 100644 src/widgets/develancacheui/widget.test.js create mode 100644 src/widgets/diskstation/component.test.jsx create mode 100644 src/widgets/diskstation/widget.test.js create mode 100644 src/widgets/dispatcharr/component.test.jsx create mode 100644 src/widgets/dispatcharr/proxy.test.js create mode 100644 src/widgets/dispatcharr/widget.test.js create mode 100644 src/widgets/docker/component.test.jsx create mode 100644 src/widgets/docker/stats-helpers.test.js create mode 100644 src/widgets/dockhand/component.test.jsx create mode 100644 src/widgets/dockhand/proxy.test.js create mode 100644 src/widgets/dockhand/widget.test.js create mode 100644 src/widgets/downloadstation/component.test.jsx create mode 100644 src/widgets/downloadstation/widget.test.js create mode 100644 src/widgets/emby/component.test.jsx create mode 100644 src/widgets/emby/widget.test.js create mode 100644 src/widgets/esphome/component.test.jsx create mode 100644 src/widgets/esphome/widget.test.js create mode 100644 src/widgets/evcc/component.test.jsx create mode 100644 src/widgets/evcc/widget.test.js create mode 100644 src/widgets/filebrowser/component.test.jsx create mode 100644 src/widgets/filebrowser/proxy.test.js create mode 100644 src/widgets/filebrowser/widget.test.js create mode 100644 src/widgets/fileflows/component.test.jsx create mode 100644 src/widgets/fileflows/widget.test.js create mode 100644 src/widgets/firefly/component.test.jsx create mode 100644 src/widgets/firefly/widget.test.js create mode 100644 src/widgets/flood/component.test.jsx create mode 100644 src/widgets/flood/proxy.test.js create mode 100644 src/widgets/flood/widget.test.js create mode 100644 src/widgets/freshrss/component.test.jsx create mode 100644 src/widgets/freshrss/proxy.test.js create mode 100644 src/widgets/freshrss/widget.test.js create mode 100644 src/widgets/frigate/component.test.jsx create mode 100644 src/widgets/frigate/proxy.test.js create mode 100644 src/widgets/frigate/widget.test.js create mode 100644 src/widgets/fritzbox/component.test.jsx create mode 100644 src/widgets/fritzbox/proxy.test.js create mode 100644 src/widgets/fritzbox/widget.test.js create mode 100644 src/widgets/gamedig/component.test.jsx create mode 100644 src/widgets/gamedig/proxy.test.js create mode 100644 src/widgets/gamedig/widget.test.js create mode 100644 src/widgets/gatus/component.test.jsx create mode 100644 src/widgets/gatus/widget.test.js create mode 100644 src/widgets/ghostfolio/component.test.jsx create mode 100644 src/widgets/ghostfolio/widget.test.js create mode 100644 src/widgets/gitea/component.test.jsx create mode 100644 src/widgets/gitea/widget.test.js create mode 100644 src/widgets/gitlab/component.test.jsx create mode 100644 src/widgets/gitlab/widget.test.js create mode 100644 src/widgets/glances/component.test.jsx create mode 100644 src/widgets/glances/components/block.test.jsx create mode 100644 src/widgets/glances/components/chart.test.jsx create mode 100644 src/widgets/glances/components/chart_dual.test.jsx create mode 100644 src/widgets/glances/components/container.test.jsx create mode 100644 src/widgets/glances/components/custom_tooltip.test.jsx create mode 100644 src/widgets/glances/components/error.test.jsx create mode 100644 src/widgets/glances/metrics/containers.test.jsx create mode 100644 src/widgets/glances/metrics/cpu.test.jsx create mode 100644 src/widgets/glances/metrics/disk.test.jsx create mode 100644 src/widgets/glances/metrics/fs.test.jsx create mode 100644 src/widgets/glances/metrics/gpu.test.jsx create mode 100644 src/widgets/glances/metrics/info.test.jsx create mode 100644 src/widgets/glances/metrics/memory.test.jsx create mode 100644 src/widgets/glances/metrics/net.test.jsx create mode 100644 src/widgets/glances/metrics/process.test.jsx create mode 100644 src/widgets/glances/metrics/sensor.test.jsx create mode 100644 src/widgets/glances/widget.test.js create mode 100644 src/widgets/gluetun/component.test.jsx create mode 100644 src/widgets/gluetun/widget.test.js create mode 100644 src/widgets/gotify/component.test.jsx create mode 100644 src/widgets/gotify/widget.test.js create mode 100644 src/widgets/grafana/component.test.jsx create mode 100644 src/widgets/grafana/widget.test.js create mode 100644 src/widgets/hdhomerun/component.test.jsx create mode 100644 src/widgets/hdhomerun/widget.test.js create mode 100644 src/widgets/headscale/component.test.jsx create mode 100644 src/widgets/headscale/widget.test.js create mode 100644 src/widgets/healthchecks/component.test.jsx create mode 100644 src/widgets/healthchecks/widget.test.js create mode 100644 src/widgets/homeassistant/component.test.jsx create mode 100644 src/widgets/homeassistant/proxy.test.js create mode 100644 src/widgets/homeassistant/widget.test.js create mode 100644 src/widgets/homebox/component.test.jsx create mode 100644 src/widgets/homebox/proxy.test.js create mode 100644 src/widgets/homebox/widget.test.js create mode 100644 src/widgets/homebridge/component.test.jsx create mode 100644 src/widgets/homebridge/proxy.test.js create mode 100644 src/widgets/homebridge/widget.test.js create mode 100644 src/widgets/iframe/component.test.jsx create mode 100644 src/widgets/iframe/widget.test.js create mode 100644 src/widgets/immich/component.test.jsx create mode 100644 src/widgets/immich/widget.test.js create mode 100644 src/widgets/jackett/component.test.jsx create mode 100644 src/widgets/jackett/proxy.test.js create mode 100644 src/widgets/jackett/widget.test.js create mode 100644 src/widgets/jdownloader/component.test.jsx create mode 100644 src/widgets/jdownloader/proxy.test.js create mode 100644 src/widgets/jdownloader/tools.test.js create mode 100644 src/widgets/jdownloader/widget.test.js create mode 100644 src/widgets/jellyfin/component.test.jsx create mode 100644 src/widgets/jellyfin/proxy.test.js create mode 100644 src/widgets/jellyfin/widget.test.js create mode 100644 src/widgets/jellyseerr/component.test.jsx create mode 100644 src/widgets/jellyseerr/widget.test.js create mode 100644 src/widgets/jellystat/component.test.jsx create mode 100644 src/widgets/jellystat/widget.test.js create mode 100644 src/widgets/karakeep/component.test.jsx create mode 100644 src/widgets/karakeep/widget.test.js create mode 100644 src/widgets/kavita/component.test.jsx create mode 100644 src/widgets/kavita/proxy.test.js create mode 100644 src/widgets/kavita/widget.test.js create mode 100644 src/widgets/komga/component.test.jsx create mode 100644 src/widgets/komga/proxy.test.js create mode 100644 src/widgets/komga/widget.test.js create mode 100644 src/widgets/komodo/component.test.jsx create mode 100644 src/widgets/komodo/proxy.test.js create mode 100644 src/widgets/komodo/widget.test.js create mode 100644 src/widgets/kopia/component.test.jsx create mode 100644 src/widgets/kopia/widget.test.js create mode 100644 src/widgets/kubernetes/component.test.jsx create mode 100644 src/widgets/lidarr/component.test.jsx create mode 100644 src/widgets/lidarr/widget.test.js create mode 100644 src/widgets/linkwarden/component.test.jsx create mode 100644 src/widgets/linkwarden/widget.test.js create mode 100644 src/widgets/lubelogger/component.test.jsx create mode 100644 src/widgets/lubelogger/widget.test.js create mode 100644 src/widgets/mailcow/component.test.jsx create mode 100644 src/widgets/mailcow/widget.test.js create mode 100644 src/widgets/mastodon/component.test.jsx create mode 100644 src/widgets/mastodon/widget.test.js create mode 100644 src/widgets/mealie/component.test.jsx create mode 100644 src/widgets/mealie/widget.test.js create mode 100644 src/widgets/medusa/component.test.jsx create mode 100644 src/widgets/medusa/widget.test.js create mode 100644 src/widgets/mikrotik/component.test.jsx create mode 100644 src/widgets/mikrotik/widget.test.js create mode 100644 src/widgets/minecraft/component.test.jsx create mode 100644 src/widgets/minecraft/proxy.test.js create mode 100644 src/widgets/minecraft/widget.test.js create mode 100644 src/widgets/miniflux/component.test.jsx create mode 100644 src/widgets/miniflux/widget.test.js create mode 100644 src/widgets/mjpeg/component.test.jsx create mode 100644 src/widgets/mjpeg/widget.test.js create mode 100644 src/widgets/moonraker/component.test.jsx create mode 100644 src/widgets/moonraker/widget.test.js create mode 100644 src/widgets/mylar/component.test.jsx create mode 100644 src/widgets/mylar/widget.test.js create mode 100644 src/widgets/myspeed/component.test.jsx create mode 100644 src/widgets/myspeed/widget.test.js create mode 100644 src/widgets/navidrome/component.test.jsx create mode 100644 src/widgets/navidrome/widget.test.js create mode 100644 src/widgets/netalertx/component.test.jsx create mode 100644 src/widgets/netalertx/widget.test.js create mode 100644 src/widgets/netdata/component.test.jsx create mode 100644 src/widgets/netdata/widget.test.js create mode 100644 src/widgets/nextcloud/component.test.jsx create mode 100644 src/widgets/nextcloud/widget.test.js create mode 100644 src/widgets/nextdns/component.test.jsx create mode 100644 src/widgets/nextdns/widget.test.js create mode 100644 src/widgets/npm/component.test.jsx create mode 100644 src/widgets/npm/proxy.test.js create mode 100644 src/widgets/npm/widget.test.js create mode 100644 src/widgets/nzbget/component.test.jsx create mode 100644 src/widgets/nzbget/widget.test.js create mode 100644 src/widgets/octoprint/component.test.jsx create mode 100644 src/widgets/octoprint/widget.test.js create mode 100644 src/widgets/omada/component.test.jsx create mode 100644 src/widgets/omada/proxy.test.js create mode 100644 src/widgets/omada/widget.test.js create mode 100644 src/widgets/ombi/component.test.jsx create mode 100644 src/widgets/ombi/widget.test.js create mode 100644 src/widgets/opendtu/component.test.jsx create mode 100644 src/widgets/opendtu/widget.test.js create mode 100644 src/widgets/openmediavault/component.test.jsx create mode 100644 src/widgets/openmediavault/methods/downloader_get_downloadlist.test.jsx create mode 100644 src/widgets/openmediavault/methods/services_get_status.test.jsx create mode 100644 src/widgets/openmediavault/methods/smart_get_list.test.jsx create mode 100644 src/widgets/openmediavault/proxy.test.js create mode 100644 src/widgets/openmediavault/widget.test.js create mode 100644 src/widgets/openwrt/component.test.jsx create mode 100644 src/widgets/openwrt/methods/interface.test.jsx create mode 100644 src/widgets/openwrt/methods/system.test.jsx create mode 100644 src/widgets/openwrt/proxy.test.js create mode 100644 src/widgets/openwrt/widget.test.js create mode 100644 src/widgets/opnsense/component.test.jsx create mode 100644 src/widgets/opnsense/widget.test.js create mode 100644 src/widgets/overseerr/component.test.jsx create mode 100644 src/widgets/overseerr/widget.test.js create mode 100644 src/widgets/pangolin/component.test.jsx create mode 100644 src/widgets/pangolin/widget.test.js create mode 100644 src/widgets/paperlessngx/component.test.jsx create mode 100644 src/widgets/paperlessngx/widget.test.js create mode 100644 src/widgets/peanut/component.test.jsx create mode 100644 src/widgets/peanut/widget.test.js create mode 100644 src/widgets/pfsense/component.test.jsx create mode 100644 src/widgets/pfsense/widget.test.js create mode 100644 src/widgets/photoprism/component.test.jsx create mode 100644 src/widgets/photoprism/proxy.test.js create mode 100644 src/widgets/photoprism/widget.test.js create mode 100644 src/widgets/pihole/component.test.jsx create mode 100644 src/widgets/pihole/proxy.test.js create mode 100644 src/widgets/pihole/widget.test.js create mode 100644 src/widgets/plantit/component.test.jsx create mode 100644 src/widgets/plantit/widget.test.js create mode 100644 src/widgets/plex/component.test.jsx create mode 100644 src/widgets/plex/proxy.test.js create mode 100644 src/widgets/plex/widget.test.js create mode 100644 src/widgets/portainer/component.test.jsx create mode 100644 src/widgets/portainer/widget.test.js create mode 100644 src/widgets/prometheus/component.test.jsx create mode 100644 src/widgets/prometheus/widget.test.js create mode 100644 src/widgets/prometheusmetric/component.test.jsx create mode 100644 src/widgets/prometheusmetric/widget.test.js create mode 100644 src/widgets/prowlarr/component.test.jsx create mode 100644 src/widgets/prowlarr/widget.test.js create mode 100644 src/widgets/proxmox/component.test.jsx create mode 100644 src/widgets/proxmox/widget.test.js create mode 100644 src/widgets/proxmoxbackupserver/component.test.jsx create mode 100644 src/widgets/proxmoxbackupserver/widget.test.js create mode 100644 src/widgets/proxmoxvm/component.test.jsx create mode 100644 src/widgets/pterodactyl/component.test.jsx create mode 100644 src/widgets/pterodactyl/widget.test.js create mode 100644 src/widgets/pyload/component.test.jsx create mode 100644 src/widgets/pyload/proxy.test.js create mode 100644 src/widgets/pyload/widget.test.js create mode 100644 src/widgets/qbittorrent/component.test.jsx create mode 100644 src/widgets/qbittorrent/proxy.test.js create mode 100644 src/widgets/qbittorrent/widget.test.js create mode 100644 src/widgets/qnap/component.test.jsx create mode 100644 src/widgets/qnap/proxy.test.js create mode 100644 src/widgets/qnap/widget.test.js create mode 100644 src/widgets/radarr/component.test.jsx create mode 100644 src/widgets/radarr/widget.test.js create mode 100644 src/widgets/readarr/component.test.jsx create mode 100644 src/widgets/readarr/widget.test.js create mode 100644 src/widgets/romm/component.test.jsx create mode 100644 src/widgets/romm/widget.test.js create mode 100644 src/widgets/rutorrent/component.test.jsx create mode 100644 src/widgets/rutorrent/proxy.test.js create mode 100644 src/widgets/rutorrent/widget.test.js create mode 100644 src/widgets/sabnzbd/component.test.jsx create mode 100644 src/widgets/sabnzbd/widget.test.js create mode 100644 src/widgets/scrutiny/component.test.jsx create mode 100644 src/widgets/scrutiny/widget.test.js create mode 100644 src/widgets/slskd/component.test.jsx create mode 100644 src/widgets/slskd/widget.test.js create mode 100644 src/widgets/sonarr/component.test.jsx create mode 100644 src/widgets/sonarr/widget.test.js create mode 100644 src/widgets/speedtest/component.test.jsx create mode 100644 src/widgets/speedtest/widget.test.js create mode 100644 src/widgets/spoolman/component.test.jsx create mode 100644 src/widgets/spoolman/widget.test.js create mode 100644 src/widgets/stash/component.test.jsx create mode 100644 src/widgets/stash/widget.test.js create mode 100644 src/widgets/stocks/component.test.jsx create mode 100644 src/widgets/stocks/widget.test.js create mode 100644 src/widgets/strelaysrv/component.test.jsx create mode 100644 src/widgets/strelaysrv/widget.test.js create mode 100644 src/widgets/suwayomi/component.test.jsx create mode 100644 src/widgets/suwayomi/proxy.test.js create mode 100644 src/widgets/suwayomi/widget.test.js create mode 100644 src/widgets/swagdashboard/component.test.jsx create mode 100644 src/widgets/swagdashboard/widget.test.js create mode 100644 src/widgets/tailscale/component.test.jsx create mode 100644 src/widgets/tailscale/widget.test.js create mode 100644 src/widgets/tandoor/component.test.jsx create mode 100644 src/widgets/tandoor/widget.test.js create mode 100644 src/widgets/tautulli/component.test.jsx create mode 100644 src/widgets/tautulli/widget.test.js create mode 100644 src/widgets/tdarr/component.test.jsx create mode 100644 src/widgets/tdarr/proxy.test.js create mode 100644 src/widgets/tdarr/widget.test.js create mode 100644 src/widgets/technitium/component.test.jsx create mode 100644 src/widgets/technitium/widget.test.js create mode 100644 src/widgets/traefik/component.test.jsx create mode 100644 src/widgets/traefik/widget.test.js create mode 100644 src/widgets/transmission/component.test.jsx create mode 100644 src/widgets/transmission/proxy.test.js create mode 100644 src/widgets/transmission/widget.test.js create mode 100644 src/widgets/trilium/component.test.jsx create mode 100644 src/widgets/trilium/widget.test.js create mode 100644 src/widgets/truenas/component.test.jsx create mode 100644 src/widgets/truenas/pool.test.jsx create mode 100644 src/widgets/truenas/proxy.test.js create mode 100644 src/widgets/truenas/widget.test.js create mode 100644 src/widgets/tubearchivist/component.test.jsx create mode 100644 src/widgets/tubearchivist/widget.test.js create mode 100644 src/widgets/unifi/component.test.jsx create mode 100644 src/widgets/unifi/proxy.test.js create mode 100644 src/widgets/unifi/widget.test.js create mode 100644 src/widgets/unmanic/component.test.jsx create mode 100644 src/widgets/unmanic/widget.test.js create mode 100644 src/widgets/unraid/component.test.jsx create mode 100644 src/widgets/unraid/proxy.test.js create mode 100644 src/widgets/unraid/widget.test.js create mode 100644 src/widgets/uptimekuma/component.test.jsx create mode 100644 src/widgets/uptimekuma/widget.test.js create mode 100644 src/widgets/uptimerobot/component.test.jsx create mode 100644 src/widgets/uptimerobot/widget.test.js create mode 100644 src/widgets/urbackup/component.test.jsx create mode 100644 src/widgets/urbackup/proxy.test.js create mode 100644 src/widgets/urbackup/widget.test.js create mode 100644 src/widgets/vikunja/component.test.jsx create mode 100644 src/widgets/vikunja/widget.test.js create mode 100644 src/widgets/wallos/component.test.jsx create mode 100644 src/widgets/wallos/widget.test.js create mode 100644 src/widgets/watchtower/component.test.jsx create mode 100644 src/widgets/watchtower/proxy.test.js create mode 100644 src/widgets/watchtower/widget.test.js create mode 100644 src/widgets/wgeasy/component.test.jsx create mode 100644 src/widgets/wgeasy/widget.test.js create mode 100644 src/widgets/whatsupdocker/component.test.jsx create mode 100644 src/widgets/whatsupdocker/widget.test.js create mode 100644 src/widgets/xteve/component.test.jsx create mode 100644 src/widgets/xteve/proxy.test.js create mode 100644 src/widgets/xteve/widget.test.js create mode 100644 src/widgets/yourspotify/component.test.jsx create mode 100644 src/widgets/yourspotify/widget.test.js create mode 100644 src/widgets/zabbix/component.test.jsx create mode 100644 src/widgets/zabbix/widget.test.js create mode 100644 vitest.config.mjs create mode 100644 vitest.setup.js diff --git a/.codecov.yml b/.codecov.yml new file mode 100644 index 000000000..d9b99dd7a --- /dev/null +++ b/.codecov.yml @@ -0,0 +1,21 @@ +codecov: + require_ci_to_pass: true + +coverage: + precision: 2 + round: down + range: "0...100" + status: + project: + default: + target: 100% + threshold: 25% + patch: + default: + target: 100% + threshold: 25% + +comment: + layout: "reach,diff,flags,files" + behavior: default + require_changes: false diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 2d517ff3c..a9d002c37 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -35,6 +35,7 @@ What type of change does your PR introduce to Homepage? ## Checklist: - [ ] If applicable, I have added corresponding documentation changes. +- [ ] If applicable, I have added or updated tests for new features and bug fixes. - [ ] If applicable, I have reviewed the [feature / enhancement](https://gethomepage.dev/more/development/#new-feature-guidelines) and / or [service widget guidelines](https://gethomepage.dev/more/development/#service-widget-guidelines). - [ ] I have checked that all code style checks pass using [pre-commit hooks](https://gethomepage.dev/more/development/#code-formatting-with-pre-commit-hooks) and [linting checks](https://gethomepage.dev/more/development/#code-linting). - [ ] If applicable, I have tested my code for new features & regressions on both mobile & desktop devices, using the latest version of major browsers. diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 000000000..b716aab83 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,37 @@ +name: Tests + +on: + pull_request: + push: + workflow_dispatch: + +jobs: + vitest: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + shard: [1, 2, 3, 4] + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + with: + version: 9 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: pnpm + + - run: pnpm install --frozen-lockfile + # Run Vitest directly so `--shard` is parsed as an option + - run: pnpm -s exec vitest run --coverage --shard ${{ matrix.shard }}/4 --pool forks + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: ./coverage/lcov.info + flags: vitest,shard-${{ matrix.shard }} + name: vitest-shard-${{ matrix.shard }} + fail_ci_if_error: true diff --git a/docs/widgets/authoring/getting-started.md b/docs/widgets/authoring/getting-started.md index fbf7b7f03..d93b01862 100644 --- a/docs/widgets/authoring/getting-started.md +++ b/docs/widgets/authoring/getting-started.md @@ -33,6 +33,32 @@ Once dependencies have been installed you can lint your code with pnpm lint ``` +## Testing + +Homepage uses [Vitest](https://vitest.dev/) for unit and component tests. + +Run the test suite: + +```bash +pnpm test +``` + +Run the test suite with coverage: + +```bash +pnpm test:coverage +``` + +### What tests to include + +- New or updated widgets should generally include a component test near the widget component (for example `src/widgets//component.test.jsx`) that covers realistic behavior: loading/placeholder state, error state, and a representative "happy path" render. +- If you add or change a widget definition file (`src/widgets//widget.js`), add/update its corresponding unit test (`src/widgets//widget.test.js`) to cover the config/mapping behavior. +- If your widget requires a custom proxy (`src/widgets//proxy.js`), add a proxy unit test (`src/widgets//proxy.test.js`) that validates: + - request construction (URL, query params, headers/auth) + - response mapping (what the widget consumes) + - error pathways (upstream error, unexpected payloads) +- Avoid placing test files under `src/pages/**` (Next.js treats files there as routes). Page tests should live under `src/__tests__/pages/**`. + ## Code formatting with pre-commit hooks To ensure a consistent style and formatting across the project source, the project utilizes Git [`pre-commit`](https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks) hooks to perform some formatting and linting before a commit is allowed. diff --git a/docs/widgets/authoring/proxies.md b/docs/widgets/authoring/proxies.md index a8b8073e0..97b0d4c42 100644 --- a/docs/widgets/authoring/proxies.md +++ b/docs/widgets/authoring/proxies.md @@ -201,3 +201,18 @@ export default async function customProxyHandler(req, res, map) { ``` Proxy handlers are a complex topic and require a good understanding of JavaScript and the Homepage codebase. If you are new to Homepage, we recommend using the built-in proxy handlers. + +## Testing proxy handlers + +Proxy handlers are a common source of regressions because they deal with authentication, request formatting, and sometimes odd upstream API behavior. + +When you add a new proxy handler or custom widget proxy, include tests that focus on behavior: + +- **Request construction:** the correct URL/path, query params, headers, and auth (and that secrets are not accidentally logged). +- **Response mapping:** the payload shape expected by the widget/component (including optional/missing fields). +- **Error handling:** upstream non-200s, invalid JSON, timeouts, and unexpected payloads should produce a predictable result. + +Test locations: + +- Shared handlers live in `src/utils/proxy/handlers/*.js` with tests alongside them (for example `src/utils/proxy/handlers/generic.test.js`). +- Widget-specific proxies live in `src/widgets//proxy.js` with tests in `src/widgets//proxy.test.js`. diff --git a/eslint.config.mjs b/eslint.config.mjs index 4b8958ef0..3629f9176 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -65,5 +65,14 @@ export default defineConfig([ ], }, }, - globalIgnores(["./config/", "./.venv/", "./.next/", "./site/"]), + // Vitest tests often intentionally place imports after `vi.mock(...)` to ensure + // modules under test see the mocked dependencies. `import/order` can't safely + // auto-fix those cases, so disable it for test files. + { + files: ["src/**/*.test.{js,jsx}", "src/**/*.spec.{js,jsx}"], + rules: { + "import/order": "off", + }, + }, + globalIgnores(["./config/", "./coverage/", "./.venv/", "./.next/", "./site/"]), ]); diff --git a/package.json b/package.json index 2b10428dc..9e768b0f3 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,9 @@ "build": "next build", "start": "next start", "lint": "eslint .", + "test": "vitest run", + "test:coverage": "vitest run --coverage", + "test:watch": "vitest", "telemetry": "next telemetry disable" }, "dependencies": { @@ -49,6 +52,9 @@ "@eslint/js": "^9.39.2", "@tailwindcss/forms": "^0.5.10", "@tailwindcss/postcss": "^4.1.18", + "@testing-library/jest-dom": "^6.8.0", + "@testing-library/react": "^16.3.0", + "@vitest/coverage-v8": "^3.2.4", "eslint": "^9.25.1", "eslint-config-next": "^15.5.11", "eslint-config-prettier": "^10.1.8", @@ -57,12 +63,14 @@ "eslint-plugin-prettier": "^5.5.4", "eslint-plugin-react": "^7.37.4", "eslint-plugin-react-hooks": "^5.2.0", + "jsdom": "^26.1.0", "postcss": "^8.5.6", "prettier": "^3.7.3", "prettier-plugin-organize-imports": "^4.3.0", "tailwind-scrollbar": "^4.0.2", "tailwindcss": "^4.1.18", - "typescript": "^5.7.3" + "typescript": "^5.7.3", + "vitest": "^3.2.4" }, "optionalDependencies": { "osx-temperature-sensor": "^1.0.8" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b3b3820c7..84e4dabaf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -117,6 +117,15 @@ importers: '@tailwindcss/postcss': specifier: ^4.1.18 version: 4.1.18 + '@testing-library/jest-dom': + specifier: ^6.8.0 + version: 6.9.1 + '@testing-library/react': + specifier: ^16.3.0 + version: 16.3.2(@testing-library/dom@10.4.1)(@types/react@19.0.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@vitest/coverage-v8': + specifier: ^3.2.4 + version: 3.2.4(vitest@3.2.4(@types/node@24.1.0)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.2)) eslint: specifier: ^9.25.1 version: 9.25.1(jiti@2.6.1) @@ -141,6 +150,9 @@ importers: eslint-plugin-react-hooks: specifier: ^5.2.0 version: 5.2.0(eslint@9.25.1(jiti@2.6.1)) + jsdom: + specifier: ^26.1.0 + version: 26.1.0 postcss: specifier: ^8.5.6 version: 8.5.6 @@ -159,6 +171,9 @@ importers: typescript: specifier: ^5.7.3 version: 5.7.3 + vitest: + specifier: ^3.2.4 + version: 3.2.4(@types/node@24.1.0)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.2) optionalDependencies: osx-temperature-sensor: specifier: ^1.0.8 @@ -166,10 +181,37 @@ importers: packages: + '@adobe/css-tools@4.4.4': + resolution: {integrity: sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==} + '@alloc/quick-lru@5.2.0': resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + + '@asamuzakjp/css-color@3.2.0': + resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} + + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.0': + resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==} + engines: {node: '>=6.0.0'} + hasBin: true + '@babel/runtime@7.26.9': resolution: {integrity: sha512-aA63XwOkcl4xxQa3HjPMqOP6LiK0ZDv3mUPYEFXkpHbaFjtGggE1A61FjFzJnB+p7/oy2gA8E+rcBNl/zC1tMg==} engines: {node: '>=6.9.0'} @@ -182,13 +224,49 @@ packages: resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==} engines: {node: '>=6.9.0'} + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + '@balena/dockerignore@1.0.2': resolution: {integrity: sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==} + '@bcoe/v8-coverage@1.0.2': + resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} + engines: {node: '>=18'} + '@colors/colors@1.6.0': resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} engines: {node: '>=0.1.90'} + '@csstools/color-helpers@5.1.0': + resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} + engines: {node: '>=18'} + + '@csstools/css-calc@2.1.4': + resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-color-parser@3.1.0': + resolution: {integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-parser-algorithms@3.0.5': + resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-tokenizer@3.0.4': + resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} + engines: {node: '>=18'} + '@dabh/diagnostics@2.0.8': resolution: {integrity: sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==} @@ -201,6 +279,162 @@ packages: '@emnapi/wasi-threads@1.0.1': resolution: {integrity: sha512-iIBu7mwkq4UQGeMEM8bLwNK962nXdhodeScX4slfQnRhEMMzvYivHhutCIk8uojvmASXXPC2WNEjwxFWk72Oqw==} + '@esbuild/aix-ppc64@0.27.2': + resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.2': + resolution: {integrity: sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.2': + resolution: {integrity: sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.2': + resolution: {integrity: sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.2': + resolution: {integrity: sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.2': + resolution: {integrity: sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.2': + resolution: {integrity: sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.2': + resolution: {integrity: sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.2': + resolution: {integrity: sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.2': + resolution: {integrity: sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.2': + resolution: {integrity: sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.2': + resolution: {integrity: sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.2': + resolution: {integrity: sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.2': + resolution: {integrity: sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.2': + resolution: {integrity: sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.2': + resolution: {integrity: sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.2': + resolution: {integrity: sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.2': + resolution: {integrity: sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.2': + resolution: {integrity: sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.2': + resolution: {integrity: sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.2': + resolution: {integrity: sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.2': + resolution: {integrity: sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.2': + resolution: {integrity: sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.2': + resolution: {integrity: sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.2': + resolution: {integrity: sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.2': + resolution: {integrity: sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@eslint-community/eslint-utils@4.6.1': resolution: {integrity: sha512-KTsJMmobmbrFLe3LDh0PC2FXpcSYJt/MLjlkh/9LEnmKYLSYmT/0EW9JWANjeoemiuZrmogti0tW5Ch+qNUYDw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -458,6 +692,10 @@ packages: resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} engines: {node: '>=18.0.0'} + '@istanbuljs/schema@0.1.3': + resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} + engines: {node: '>=8'} + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -651,6 +889,131 @@ packages: react-redux: optional: true + '@rollup/rollup-android-arm-eabi@4.57.1': + resolution: {integrity: sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.57.1': + resolution: {integrity: sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.57.1': + resolution: {integrity: sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.57.1': + resolution: {integrity: sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.57.1': + resolution: {integrity: sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.57.1': + resolution: {integrity: sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.57.1': + resolution: {integrity: sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.57.1': + resolution: {integrity: sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.57.1': + resolution: {integrity: sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.57.1': + resolution: {integrity: sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.57.1': + resolution: {integrity: sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loong64-musl@4.57.1': + resolution: {integrity: sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.57.1': + resolution: {integrity: sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-ppc64-musl@4.57.1': + resolution: {integrity: sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.57.1': + resolution: {integrity: sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.57.1': + resolution: {integrity: sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.57.1': + resolution: {integrity: sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.57.1': + resolution: {integrity: sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.57.1': + resolution: {integrity: sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openbsd-x64@4.57.1': + resolution: {integrity: sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.57.1': + resolution: {integrity: sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.57.1': + resolution: {integrity: sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.57.1': + resolution: {integrity: sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.57.1': + resolution: {integrity: sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.57.1': + resolution: {integrity: sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==} + cpu: [x64] + os: [win32] + '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} @@ -782,9 +1145,38 @@ packages: '@tanstack/virtual-core@3.13.12': resolution: {integrity: sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==} + '@testing-library/dom@10.4.1': + resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} + engines: {node: '>=18'} + + '@testing-library/jest-dom@6.9.1': + resolution: {integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==} + engines: {node: '>=14', npm: '>=6', yarn: '>=1'} + + '@testing-library/react@16.3.2': + resolution: {integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==} + engines: {node: '>=18'} + peerDependencies: + '@testing-library/dom': ^10.0.0 + '@types/react': ^18.0.0 || ^19.0.0 + '@types/react-dom': ^18.0.0 || ^19.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@tybys/wasm-util@0.9.0': resolution: {integrity: sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==} + '@types/aria-query@5.0.4': + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@types/d3-array@3.2.1': resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==} @@ -812,9 +1204,15 @@ packages: '@types/d3-timer@3.0.2': resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/estree@1.0.7': resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==} + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/hoist-non-react-statics@3.3.6': resolution: {integrity: sha512-lPByRJUer/iN/xa4qpyL0qmL11DqNW81iU/IG1S3uvRUq4oKagz8VCxZjiWkumgt66YT3vOdDgZ0o32sGKtCEw==} @@ -982,6 +1380,44 @@ packages: cpu: [x64] os: [win32] + '@vitest/coverage-v8@3.2.4': + resolution: {integrity: sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==} + peerDependencies: + '@vitest/browser': 3.2.4 + vitest: 3.2.4 + peerDependenciesMeta: + '@vitest/browser': + optional: true + + '@vitest/expect@3.2.4': + resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} + + '@vitest/mocker@3.2.4': + resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@3.2.4': + resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} + + '@vitest/runner@3.2.4': + resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==} + + '@vitest/snapshot@3.2.4': + resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==} + + '@vitest/spy@3.2.4': + resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} + + '@vitest/utils@3.2.4': + resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -992,6 +1428,10 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} @@ -1007,6 +1447,10 @@ packages: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + ansi-styles@6.2.1: resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} engines: {node: '>=12'} @@ -1017,6 +1461,9 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + aria-query@5.3.2: resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} engines: {node: '>= 0.4'} @@ -1060,9 +1507,16 @@ packages: asn1@0.2.6: resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + ast-types-flow@0.0.8: resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==} + ast-v8-to-istanbul@0.3.11: + resolution: {integrity: sha512-Qya9fkoofMjCBNVdWINMjB5KZvkYfaO9/anwkWnjxibpWUxo5iHl2sOdP7/uAqaRuUYuoo8rDwnbaaKVFxoUvw==} + async-function@1.0.0: resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} engines: {node: '>= 0.4'} @@ -1124,6 +1578,10 @@ packages: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + cacheable-lookup@7.0.0: resolution: {integrity: sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==} engines: {node: '>=14.16'} @@ -1155,10 +1613,18 @@ packages: caniuse-lite@1.0.30001760: resolution: {integrity: sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==} + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + chownr@1.1.4: resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} @@ -1231,6 +1697,13 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + css.escape@1.5.1: + resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} + + cssstyle@4.6.0: + resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==} + engines: {node: '>=18'} + csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} @@ -1281,6 +1754,10 @@ packages: damerau-levenshtein@1.0.8: resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} + data-urls@5.0.0: + resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} + engines: {node: '>=18'} + data-view-buffer@1.0.2: resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} engines: {node: '>= 0.4'} @@ -1322,10 +1799,17 @@ packages: decimal.js-light@2.5.1: resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + decompress-response@6.0.0: resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} engines: {node: '>=10'} + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -1369,6 +1853,12 @@ packages: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} + dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + + dom-accessibility-api@0.6.3: + resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -1395,6 +1885,10 @@ packages: resolution: {integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==} engines: {node: '>=10.13.0'} + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + es-abstract@1.23.9: resolution: {integrity: sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==} engines: {node: '>= 0.4'} @@ -1415,6 +1909,9 @@ packages: resolution: {integrity: sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==} engines: {node: '>= 0.4'} + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-object-atoms@1.1.1: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} @@ -1434,6 +1931,11 @@ packages: es-toolkit@1.39.10: resolution: {integrity: sha512-E0iGnTtbDhkeczB0T+mxmoVlT4YNweEKBLq7oaU4p11mecdsZpNWOglI4895Vh4usbQ+LsJiuLuI2L0Vdmfm2w==} + esbuild@0.27.2: + resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -1574,6 +2076,9 @@ packages: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} @@ -1585,6 +2090,10 @@ packages: eventemitter3@5.0.1: resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -1620,6 +2129,15 @@ packages: picomatch: optional: true + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + fecha@4.2.3: resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} @@ -1673,6 +2191,11 @@ packages: fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} @@ -1783,6 +2306,13 @@ packages: hoist-non-react-statics@3.3.2: resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + html-encoding-sniffer@4.0.0: + resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} + engines: {node: '>=18'} + + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + html-parse-stringify@3.0.1: resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==} @@ -1793,10 +2323,18 @@ packages: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + http2-wrapper@2.2.1: resolution: {integrity: sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==} engines: {node: '>=10.19.0'} + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + i18next-fs-backend@1.2.0: resolution: {integrity: sha512-pUx3AcgXCbur0jpFA7U67Z2RJflAcIi698Y8VL+phdOqUchahxriV3Cs+M6UkPNQSS/zPEzWLfdJ8EgjB7HVxg==} @@ -1814,6 +2352,10 @@ packages: ical.js@2.1.0: resolution: {integrity: sha512-BOVfrH55xQ6kpS3muGvIXIg2l7p+eoe12/oS7R5yrO3TL/j/bLsR0PR+tYQESFbyTbvGgPHn9zQ6tI4FWyuSaQ==} + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + iconv-lite@0.7.0: resolution: {integrity: sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==} engines: {node: '>=0.10.0'} @@ -1836,6 +2378,10 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} @@ -1918,6 +2464,9 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + is-regex@1.2.1: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} @@ -1975,6 +2524,22 @@ packages: peerDependencies: ws: '*' + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-lib-source-maps@5.0.6: + resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==} + engines: {node: '>=10'} + + istanbul-reports@3.2.0: + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} + engines: {node: '>=8'} + iterator.prototype@1.1.5: resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} engines: {node: '>= 0.4'} @@ -1989,13 +2554,28 @@ packages: jose@5.10.0: resolution: {integrity: sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==} + js-tokens@10.0.0: + resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + js-yaml@4.1.1: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true + jsdom@26.1.0: + resolution: {integrity: sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==} + engines: {node: '>=18'} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + jsep@1.4.0: resolution: {integrity: sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==} engines: {node: '>= 10.16.0'} @@ -2133,6 +2713,9 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + lowercase-keys@3.0.0: resolution: {integrity: sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -2144,9 +2727,20 @@ packages: resolution: {integrity: sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ==} engines: {node: '>=12'} + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + magicast@0.3.5: + resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -2178,6 +2772,10 @@ packages: resolution: {integrity: sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + minecraftstatuspinger@1.2.2: resolution: {integrity: sha512-3PDWcifjw6cliGnGqw0+nJVWWPOcpLDyNLh4D84vCNzPD2h9REbN5Ne11I//CMkIu5xJiIuyGwI44gyRYYbpuw==} engines: {node: '>=14.0.0'} @@ -2274,6 +2872,9 @@ packages: resolution: {integrity: sha512-X06Mfd/5aKsRHc0O0J5CUedwnPmnDtLF2+nq+KN9KSDlJHkPuh0JUviWjEWMe0SW/9TDdSLVPuk7L5gGTIA1/w==} engines: {node: '>=14.16'} + nwsapi@2.2.23: + resolution: {integrity: sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==} + oauth4webapi@3.3.0: resolution: {integrity: sha512-ZlozhPlFfobzh3hB72gnBFLjXpugl/dljz1fJSRdqaV2r3D5dmi5lg2QWI0LmUYuazmE+b5exsloEv6toUtw9g==} @@ -2350,6 +2951,9 @@ packages: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -2365,6 +2969,13 @@ packages: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -2376,6 +2987,10 @@ packages: resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} engines: {node: '>=12'} + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + ping@0.4.4: resolution: {integrity: sha512-56ZMC0j7SCsMMLdOoUg12VZCfj/+ZO+yfOSjaNCRrmZZr6GLbN2X/Ui56T15dI8NhiHckaw5X2pvyfAomanwqQ==} engines: {node: '>=4.0.0'} @@ -2419,6 +3034,10 @@ packages: resolution: {integrity: sha512-nODzvTiYVRGRqAOvE84Vk5JDPyyxsVk0/fbA/bq7RqlnhksGpset09XTxbpvLTIjoaF7K8Z8DG8yHtKGTPSYRw==} engines: {node: '>=20'} + pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + prism-react-renderer@2.4.1: resolution: {integrity: sha512-ey8Ls/+Di31eqzUxC46h8MksNuGx/n0AAC8uKpwFau4RPDYLuE3EXTp8N8G2vX2N7UC/+IXeNUnlWBGGcAG+Ig==} peerDependencies: @@ -2494,6 +3113,9 @@ packages: react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} @@ -2531,6 +3153,10 @@ packages: react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-is: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + redent@3.0.0: + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} + redux-thunk@3.1.0: resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==} peerDependencies: @@ -2591,6 +3217,14 @@ packages: resolution: {integrity: sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==} hasBin: true + rollup@4.57.1: + resolution: {integrity: sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + rrweb-cssom@0.8.0: + resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -2625,6 +3259,10 @@ packages: sax@1.4.1: resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==} + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + scheduler@0.23.2: resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} @@ -2684,6 +3322,9 @@ packages: resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} engines: {node: '>= 0.4'} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@4.1.0: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} @@ -2705,10 +3346,16 @@ packages: stack-trace@0.0.10: resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==} + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + statuses@2.0.2: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + stop-iteration-iterator@1.1.0: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} @@ -2775,10 +3422,17 @@ packages: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} + strip-indent@3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + strip-literal@3.1.0: + resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + strnum@2.1.1: resolution: {integrity: sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==} @@ -2808,6 +3462,9 @@ packages: peerDependencies: react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + synckit@0.11.11: resolution: {integrity: sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==} engines: {node: ^14.18.0 || >=16.0.0} @@ -2849,19 +3506,52 @@ packages: telnet-client@2.2.6: resolution: {integrity: sha512-ZUYrLsPtQupQww3eSEORDVOb6ztdtKEghya6TVXPo2tg/UQq2pn5rHhvwuUvyYpbnsoqdNY1fyD1GNkXHR8dYA==} + test-exclude@7.0.1: + resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==} + engines: {node: '>=18'} + text-hex@1.0.0: resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==} tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + tinyglobby@0.2.12: resolution: {integrity: sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww==} engines: {node: '>=12.0.0'} + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + + tinyspy@4.0.4: + resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} + engines: {node: '>=14.0.0'} + + tldts-core@6.1.86: + resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==} + tldts-core@7.0.12: resolution: {integrity: sha512-3K76aXywJFduGRsOYoY5JzINLs/WMlOkeDwPL+8OCPq2Rh39gkSDtWAxdJQlWjpun/xF/LHf29yqCi6VC/rHDA==} + tldts@6.1.86: + resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==} + hasBin: true + tldts@7.0.12: resolution: {integrity: sha512-M9ZQBPp6FyqhMcl233vHYyYRkxXOA1SKGlnq13S0mJdUhRSwr2w6I8rlchPL73wBwRlyIZpFvpu2VcdSMWLYXw==} hasBin: true @@ -2881,6 +3571,10 @@ packages: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} + tough-cookie@5.1.2: + resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} + engines: {node: '>=16'} + tough-cookie@6.0.0: resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==} engines: {node: '>=16'} @@ -2888,6 +3582,10 @@ packages: tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + tr46@5.1.1: + resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} + engines: {node: '>=18'} + triple-beam@1.4.1: resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==} engines: {node: '>= 14.0.0'} @@ -2978,13 +3676,107 @@ packages: victory-vendor@37.3.6: resolution: {integrity: sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==} + vite-node@3.2.4: + resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + + vite@7.3.1: + resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@3.2.4: + resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.2.4 + '@vitest/ui': 3.2.4 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + void-elements@3.1.0: resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} engines: {node: '>=0.10.0'} + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + + whatwg-encoding@3.1.1: + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation + + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + + whatwg-url@14.2.0: + resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} + engines: {node: '>=18'} + whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} @@ -3013,6 +3805,11 @@ packages: engines: {node: '>= 8'} hasBin: true + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + winston-transport@4.9.0: resolution: {integrity: sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==} engines: {node: '>= 12.0.0'} @@ -3052,10 +3849,17 @@ packages: resolution: {integrity: sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==} hasBin: true + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + xmlbuilder@8.2.2: resolution: {integrity: sha512-eKRAFz04jghooy8muekqzo8uCSVNeyRedbuJrp0fovbLIi7wlsYtdUn3vBAAPq2Y3/0xMz2WMEUQ8yhVVO9Stw==} engines: {node: '>=4.0'} + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + xmlrpc@1.3.2: resolution: {integrity: sha512-jQf5gbrP6wvzN71fgkcPPkF4bF/Wyovd7Xdff8d6/ihxYmgETQYSuTc+Hl+tsh/jmgPLro/Aro48LMFlIyEKKQ==} engines: {node: '>=0.8', npm: '>=1.0.0'} @@ -3082,8 +3886,37 @@ packages: snapshots: + '@adobe/css-tools@4.4.4': {} + '@alloc/quick-lru@5.2.0': {} + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@asamuzakjp/css-color@3.2.0': + dependencies: + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + lru-cache: 10.4.3 + + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/parser@7.29.0': + dependencies: + '@babel/types': 7.29.0 + '@babel/runtime@7.26.9': dependencies: regenerator-runtime: 0.14.1 @@ -3092,10 +3925,37 @@ snapshots: '@babel/runtime@7.28.6': {} + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + '@balena/dockerignore@1.0.2': {} + '@bcoe/v8-coverage@1.0.2': {} + '@colors/colors@1.6.0': {} + '@csstools/color-helpers@5.1.0': {} + + '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/color-helpers': 5.1.0 + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-tokenizer@3.0.4': {} + '@dabh/diagnostics@2.0.8': dependencies: '@so-ric/colorspace': 1.1.6 @@ -3118,6 +3978,84 @@ snapshots: tslib: 2.8.1 optional: true + '@esbuild/aix-ppc64@0.27.2': + optional: true + + '@esbuild/android-arm64@0.27.2': + optional: true + + '@esbuild/android-arm@0.27.2': + optional: true + + '@esbuild/android-x64@0.27.2': + optional: true + + '@esbuild/darwin-arm64@0.27.2': + optional: true + + '@esbuild/darwin-x64@0.27.2': + optional: true + + '@esbuild/freebsd-arm64@0.27.2': + optional: true + + '@esbuild/freebsd-x64@0.27.2': + optional: true + + '@esbuild/linux-arm64@0.27.2': + optional: true + + '@esbuild/linux-arm@0.27.2': + optional: true + + '@esbuild/linux-ia32@0.27.2': + optional: true + + '@esbuild/linux-loong64@0.27.2': + optional: true + + '@esbuild/linux-mips64el@0.27.2': + optional: true + + '@esbuild/linux-ppc64@0.27.2': + optional: true + + '@esbuild/linux-riscv64@0.27.2': + optional: true + + '@esbuild/linux-s390x@0.27.2': + optional: true + + '@esbuild/linux-x64@0.27.2': + optional: true + + '@esbuild/netbsd-arm64@0.27.2': + optional: true + + '@esbuild/netbsd-x64@0.27.2': + optional: true + + '@esbuild/openbsd-arm64@0.27.2': + optional: true + + '@esbuild/openbsd-x64@0.27.2': + optional: true + + '@esbuild/openharmony-arm64@0.27.2': + optional: true + + '@esbuild/sunos-x64@0.27.2': + optional: true + + '@esbuild/win32-arm64@0.27.2': + optional: true + + '@esbuild/win32-ia32@0.27.2': + optional: true + + '@esbuild/win32-x64@0.27.2': + optional: true + '@eslint-community/eslint-utils@4.6.1(eslint@9.25.1(jiti@2.6.1))': dependencies: eslint: 9.25.1(jiti@2.6.1) @@ -3344,6 +4282,8 @@ snapshots: dependencies: minipass: 7.1.2 + '@istanbuljs/schema@0.1.3': {} + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -3538,6 +4478,81 @@ snapshots: react: 18.3.1 react-redux: 9.2.0(@types/react@19.0.10)(react@18.3.1)(redux@5.0.1) + '@rollup/rollup-android-arm-eabi@4.57.1': + optional: true + + '@rollup/rollup-android-arm64@4.57.1': + optional: true + + '@rollup/rollup-darwin-arm64@4.57.1': + optional: true + + '@rollup/rollup-darwin-x64@4.57.1': + optional: true + + '@rollup/rollup-freebsd-arm64@4.57.1': + optional: true + + '@rollup/rollup-freebsd-x64@4.57.1': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.57.1': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.57.1': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.57.1': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.57.1': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.57.1': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.57.1': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-x64-musl@4.57.1': + optional: true + + '@rollup/rollup-openbsd-x64@4.57.1': + optional: true + + '@rollup/rollup-openharmony-arm64@4.57.1': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.57.1': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.57.1': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.57.1': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.57.1': + optional: true + '@rtsao/scc@1.1.0': {} '@rushstack/eslint-patch@1.11.0': {} @@ -3647,11 +4662,47 @@ snapshots: '@tanstack/virtual-core@3.13.12': {} + '@testing-library/dom@10.4.1': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/runtime': 7.28.6 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + picocolors: 1.1.1 + pretty-format: 27.5.1 + + '@testing-library/jest-dom@6.9.1': + dependencies: + '@adobe/css-tools': 4.4.4 + aria-query: 5.3.2 + css.escape: 1.5.1 + dom-accessibility-api: 0.6.3 + picocolors: 1.1.1 + redent: 3.0.0 + + '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react@19.0.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.28.6 + '@testing-library/dom': 10.4.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 19.0.10 + '@tybys/wasm-util@0.9.0': dependencies: tslib: 2.8.1 optional: true + '@types/aria-query@5.0.4': {} + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + '@types/d3-array@3.2.1': {} '@types/d3-color@3.1.3': {} @@ -3676,8 +4727,12 @@ snapshots: '@types/d3-timer@3.0.2': {} + '@types/deep-eql@4.0.2': {} + '@types/estree@1.0.7': {} + '@types/estree@1.0.8': {} + '@types/hoist-non-react-statics@3.3.6': dependencies: '@types/react': 19.0.10 @@ -3851,12 +4906,75 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.3.3': optional: true + '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/node@24.1.0)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.2))': + dependencies: + '@ampproject/remapping': 2.3.0 + '@bcoe/v8-coverage': 1.0.2 + ast-v8-to-istanbul: 0.3.11 + debug: 4.4.1 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 5.0.6 + istanbul-reports: 3.2.0 + magic-string: 0.30.21 + magicast: 0.3.5 + std-env: 3.10.0 + test-exclude: 7.0.1 + tinyrainbow: 2.0.0 + vitest: 3.2.4(@types/node@24.1.0)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.2) + transitivePeerDependencies: + - supports-color + + '@vitest/expect@3.2.4': + dependencies: + '@types/chai': 5.2.3 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.3.3 + tinyrainbow: 2.0.0 + + '@vitest/mocker@3.2.4(vite@7.3.1(@types/node@24.1.0)(jiti@2.6.1)(lightningcss@1.30.2))': + dependencies: + '@vitest/spy': 3.2.4 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.3.1(@types/node@24.1.0)(jiti@2.6.1)(lightningcss@1.30.2) + + '@vitest/pretty-format@3.2.4': + dependencies: + tinyrainbow: 2.0.0 + + '@vitest/runner@3.2.4': + dependencies: + '@vitest/utils': 3.2.4 + pathe: 2.0.3 + strip-literal: 3.1.0 + + '@vitest/snapshot@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@3.2.4': + dependencies: + tinyspy: 4.0.4 + + '@vitest/utils@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + loupe: 3.2.1 + tinyrainbow: 2.0.0 + acorn-jsx@5.3.2(acorn@8.14.1): dependencies: acorn: 8.14.1 acorn@8.14.1: {} + agent-base@7.1.4: {} + ajv@6.12.6: dependencies: fast-deep-equal: 3.1.3 @@ -3872,12 +4990,18 @@ snapshots: dependencies: color-convert: 2.0.1 + ansi-styles@5.2.0: {} + ansi-styles@6.2.1: {} any-promise@1.3.0: {} argparse@2.0.1: {} + aria-query@5.3.0: + dependencies: + dequal: 2.0.3 + aria-query@5.3.2: {} array-buffer-byte-length@1.0.2: @@ -3960,8 +5084,16 @@ snapshots: dependencies: safer-buffer: 2.1.2 + assertion-error@2.0.1: {} + ast-types-flow@0.0.8: {} + ast-v8-to-istanbul@0.3.11: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + estree-walker: 3.0.3 + js-tokens: 10.0.0 + async-function@1.0.0: {} async-mutex@0.5.0: @@ -4021,6 +5153,8 @@ snapshots: bytes@3.1.2: {} + cac@6.7.14: {} + cacheable-lookup@7.0.0: {} cacheable-request@10.2.14: @@ -4059,11 +5193,21 @@ snapshots: caniuse-lite@1.0.30001760: {} + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 supports-color: 7.2.0 + check-error@2.1.3: {} + chownr@1.1.4: {} chownr@3.0.0: {} @@ -4127,6 +5271,13 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + css.escape@1.5.1: {} + + cssstyle@4.6.0: + dependencies: + '@asamuzakjp/css-color': 3.2.0 + rrweb-cssom: 0.8.0 + csstype@3.1.3: {} d3-array@3.2.4: @@ -4169,6 +5320,11 @@ snapshots: damerau-levenshtein@1.0.8: {} + data-urls@5.0.0: + dependencies: + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + data-view-buffer@1.0.2: dependencies: call-bound: 1.0.4 @@ -4201,10 +5357,14 @@ snapshots: decimal.js-light@2.5.1: {} + decimal.js@10.6.0: {} + decompress-response@6.0.0: dependencies: mimic-response: 3.1.0 + deep-eql@5.0.2: {} + deep-is@0.1.4: {} defer-to-connect@2.0.1: {} @@ -4254,6 +5414,10 @@ snapshots: dependencies: esutils: 2.0.3 + dom-accessibility-api@0.5.16: {} + + dom-accessibility-api@0.6.3: {} + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -4279,6 +5443,8 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.3.0 + entities@6.0.1: {} + es-abstract@1.23.9: dependencies: array-buffer-byte-length: 1.0.2 @@ -4413,6 +5579,8 @@ snapshots: iterator.prototype: 1.1.5 safe-array-concat: 1.1.3 + es-module-lexer@1.7.0: {} + es-object-atoms@1.1.1: dependencies: es-errors: 1.3.0 @@ -4436,6 +5604,35 @@ snapshots: es-toolkit@1.39.10: {} + esbuild@0.27.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.2 + '@esbuild/android-arm': 0.27.2 + '@esbuild/android-arm64': 0.27.2 + '@esbuild/android-x64': 0.27.2 + '@esbuild/darwin-arm64': 0.27.2 + '@esbuild/darwin-x64': 0.27.2 + '@esbuild/freebsd-arm64': 0.27.2 + '@esbuild/freebsd-x64': 0.27.2 + '@esbuild/linux-arm': 0.27.2 + '@esbuild/linux-arm64': 0.27.2 + '@esbuild/linux-ia32': 0.27.2 + '@esbuild/linux-loong64': 0.27.2 + '@esbuild/linux-mips64el': 0.27.2 + '@esbuild/linux-ppc64': 0.27.2 + '@esbuild/linux-riscv64': 0.27.2 + '@esbuild/linux-s390x': 0.27.2 + '@esbuild/linux-x64': 0.27.2 + '@esbuild/netbsd-arm64': 0.27.2 + '@esbuild/netbsd-x64': 0.27.2 + '@esbuild/openbsd-arm64': 0.27.2 + '@esbuild/openbsd-x64': 0.27.2 + '@esbuild/openharmony-arm64': 0.27.2 + '@esbuild/sunos-x64': 0.27.2 + '@esbuild/win32-arm64': 0.27.2 + '@esbuild/win32-ia32': 0.27.2 + '@esbuild/win32-x64': 0.27.2 + escalade@3.2.0: {} escape-string-regexp@4.0.0: {} @@ -4648,12 +5845,18 @@ snapshots: estraverse@5.3.0: {} + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + esutils@2.0.3: {} event-to-promise@0.7.0: {} eventemitter3@5.0.1: {} + expect-type@1.3.0: {} + fast-deep-equal@3.1.3: {} fast-diff@1.3.0: {} @@ -4690,6 +5893,10 @@ snapshots: optionalDependencies: picomatch: 4.0.2 + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + fecha@4.2.3: {} file-entry-cache@8.0.0: @@ -4736,6 +5943,9 @@ snapshots: fs-constants@1.0.0: {} + fsevents@2.3.3: + optional: true + function-bind@1.1.2: {} function.prototype.name@1.1.8: @@ -4884,6 +6094,12 @@ snapshots: dependencies: react-is: 16.13.1 + html-encoding-sniffer@4.0.0: + dependencies: + whatwg-encoding: 3.1.1 + + html-escaper@2.0.2: {} + html-parse-stringify@3.0.1: dependencies: void-elements: 3.1.0 @@ -4898,11 +6114,25 @@ snapshots: statuses: 2.0.2 toidentifier: 1.0.1 + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + http2-wrapper@2.2.1: dependencies: quick-lru: 5.1.1 resolve-alpn: 1.2.1 + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + i18next-fs-backend@1.2.0: {} i18next@21.10.0: @@ -4917,6 +6147,10 @@ snapshots: ical.js@2.1.0: {} + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + iconv-lite@0.7.0: dependencies: safer-buffer: 2.1.2 @@ -4934,6 +6168,8 @@ snapshots: imurmurhash@0.1.4: {} + indent-string@4.0.0: {} + inherits@2.0.4: {} internal-slot@1.1.0: @@ -5018,6 +6254,8 @@ snapshots: is-number@7.0.0: {} + is-potential-custom-element-name@1.0.1: {} + is-regex@1.2.1: dependencies: call-bound: 1.0.4 @@ -5071,6 +6309,27 @@ snapshots: dependencies: ws: 8.18.3 + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-lib-source-maps@5.0.6: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + debug: 4.4.1 + istanbul-lib-coverage: 3.2.2 + transitivePeerDependencies: + - supports-color + + istanbul-reports@3.2.0: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + iterator.prototype@1.1.5: dependencies: define-data-property: 1.1.4 @@ -5090,12 +6349,43 @@ snapshots: jose@5.10.0: {} + js-tokens@10.0.0: {} + js-tokens@4.0.0: {} + js-tokens@9.0.1: {} + js-yaml@4.1.1: dependencies: argparse: 2.0.1 + jsdom@26.1.0: + dependencies: + cssstyle: 4.6.0 + data-urls: 5.0.0 + decimal.js: 10.6.0 + html-encoding-sniffer: 4.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.23 + parse5: 7.3.0 + rrweb-cssom: 0.8.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 5.1.2 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 3.1.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + ws: 8.18.3 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + jsep@1.4.0: {} json-buffer@3.0.1: {} @@ -5212,16 +6502,30 @@ snapshots: dependencies: js-tokens: 4.0.0 + loupe@3.2.1: {} + lowercase-keys@3.0.0: {} lru-cache@10.4.3: {} luxon@3.6.1: {} + lz-string@1.5.0: {} + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + magicast@0.3.5: + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + source-map-js: 1.2.1 + + make-dir@4.0.0: + dependencies: + semver: 7.7.3 + math-intrinsics@1.1.0: {} memory-cache@0.2.0: {} @@ -5243,6 +6547,8 @@ snapshots: mimic-response@4.0.0: {} + min-indent@1.0.1: {} + minecraftstatuspinger@1.2.2: {} mini-svg-data-uri@1.4.4: {} @@ -5325,6 +6631,8 @@ snapshots: normalize-url@8.1.0: {} + nwsapi@2.2.23: {} + oauth4webapi@3.3.0: {} object-assign@4.1.1: {} @@ -5415,6 +6723,10 @@ snapshots: dependencies: callsites: 3.1.0 + parse5@7.3.0: + dependencies: + entities: 6.0.1 + path-exists@4.0.0: {} path-key@3.1.1: {} @@ -5426,12 +6738,18 @@ snapshots: lru-cache: 10.4.3 minipass: 7.1.2 + pathe@2.0.3: {} + + pathval@2.0.1: {} + picocolors@1.1.1: {} picomatch@2.3.1: {} picomatch@4.0.2: {} + picomatch@4.0.3: {} + ping@0.4.4: {} possible-typed-array-names@1.1.0: {} @@ -5463,6 +6781,12 @@ snapshots: pretty-bytes@7.1.0: {} + pretty-format@27.5.1: + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + prism-react-renderer@2.4.1(react@18.3.1): dependencies: '@types/prismjs': 1.26.5 @@ -5541,6 +6865,8 @@ snapshots: react-is@16.13.1: {} + react-is@17.0.2: {} + react-is@18.3.1: {} react-redux@9.2.0(@types/react@19.0.10)(react@18.3.1)(redux@5.0.1): @@ -5599,6 +6925,11 @@ snapshots: - '@types/react' - redux + redent@3.0.0: + dependencies: + indent-string: 4.0.0 + strip-indent: 3.0.0 + redux-thunk@3.1.0(redux@5.0.1): dependencies: redux: 5.0.1 @@ -5661,6 +6992,39 @@ snapshots: dependencies: glob: 10.4.5 + rollup@4.57.1: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.57.1 + '@rollup/rollup-android-arm64': 4.57.1 + '@rollup/rollup-darwin-arm64': 4.57.1 + '@rollup/rollup-darwin-x64': 4.57.1 + '@rollup/rollup-freebsd-arm64': 4.57.1 + '@rollup/rollup-freebsd-x64': 4.57.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.57.1 + '@rollup/rollup-linux-arm-musleabihf': 4.57.1 + '@rollup/rollup-linux-arm64-gnu': 4.57.1 + '@rollup/rollup-linux-arm64-musl': 4.57.1 + '@rollup/rollup-linux-loong64-gnu': 4.57.1 + '@rollup/rollup-linux-loong64-musl': 4.57.1 + '@rollup/rollup-linux-ppc64-gnu': 4.57.1 + '@rollup/rollup-linux-ppc64-musl': 4.57.1 + '@rollup/rollup-linux-riscv64-gnu': 4.57.1 + '@rollup/rollup-linux-riscv64-musl': 4.57.1 + '@rollup/rollup-linux-s390x-gnu': 4.57.1 + '@rollup/rollup-linux-x64-gnu': 4.57.1 + '@rollup/rollup-linux-x64-musl': 4.57.1 + '@rollup/rollup-openbsd-x64': 4.57.1 + '@rollup/rollup-openharmony-arm64': 4.57.1 + '@rollup/rollup-win32-arm64-msvc': 4.57.1 + '@rollup/rollup-win32-ia32-msvc': 4.57.1 + '@rollup/rollup-win32-x64-gnu': 4.57.1 + '@rollup/rollup-win32-x64-msvc': 4.57.1 + fsevents: 2.3.3 + + rrweb-cssom@0.8.0: {} + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -5696,6 +7060,10 @@ snapshots: sax@1.4.1: {} + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + scheduler@0.23.2: dependencies: loose-envify: 1.4.0 @@ -5798,6 +7166,8 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} + signal-exit@4.1.0: {} source-map-js@1.2.1: {} @@ -5816,8 +7186,12 @@ snapshots: stack-trace@0.0.10: {} + stackback@0.0.2: {} + statuses@2.0.2: {} + std-env@3.10.0: {} + stop-iteration-iterator@1.1.0: dependencies: es-errors: 1.3.0 @@ -5916,8 +7290,16 @@ snapshots: strip-bom@3.0.0: {} + strip-indent@3.0.0: + dependencies: + min-indent: 1.0.1 + strip-json-comments@3.1.1: {} + strip-literal@3.1.0: + dependencies: + js-tokens: 9.0.1 + strnum@2.1.1: {} styled-jsx@5.1.6(react@18.3.1): @@ -5937,6 +7319,8 @@ snapshots: react: 18.3.1 use-sync-external-store: 1.6.0(react@18.3.1) + symbol-tree@3.2.4: {} + synckit@0.11.11: dependencies: '@pkgr/core': 0.2.9 @@ -5985,17 +7369,44 @@ snapshots: net: 1.0.2 stream: 0.0.2 + test-exclude@7.0.1: + dependencies: + '@istanbuljs/schema': 0.1.3 + glob: 10.4.5 + minimatch: 9.0.5 + text-hex@1.0.0: {} tiny-invariant@1.3.3: {} + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + tinyglobby@0.2.12: dependencies: fdir: 6.4.3(picomatch@4.0.2) picomatch: 4.0.2 + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + tinypool@1.1.1: {} + + tinyrainbow@2.0.0: {} + + tinyspy@4.0.4: {} + + tldts-core@6.1.86: {} + tldts-core@7.0.12: {} + tldts@6.1.86: + dependencies: + tldts-core: 6.1.86 + tldts@7.0.12: dependencies: tldts-core: 7.0.12 @@ -6012,12 +7423,20 @@ snapshots: toidentifier@1.0.1: {} + tough-cookie@5.1.2: + dependencies: + tldts: 6.1.86 + tough-cookie@6.0.0: dependencies: tldts: 7.0.12 tr46@0.0.3: {} + tr46@5.1.1: + dependencies: + punycode: 2.3.1 + triple-beam@1.4.1: {} ts-api-utils@2.1.0(typescript@5.7.3): @@ -6144,10 +7563,104 @@ snapshots: d3-time: 3.1.0 d3-timer: 3.0.1 + vite-node@3.2.4(@types/node@24.1.0)(jiti@2.6.1)(lightningcss@1.30.2): + dependencies: + cac: 6.7.14 + debug: 4.4.1 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 7.3.1(@types/node@24.1.0)(jiti@2.6.1)(lightningcss@1.30.2) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vite@7.3.1(@types/node@24.1.0)(jiti@2.6.1)(lightningcss@1.30.2): + dependencies: + esbuild: 0.27.2 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.57.1 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 24.1.0 + fsevents: 2.3.3 + jiti: 2.6.1 + lightningcss: 1.30.2 + + vitest@3.2.4(@types/node@24.1.0)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.2): + dependencies: + '@types/chai': 5.2.3 + '@vitest/expect': 3.2.4 + '@vitest/mocker': 3.2.4(vite@7.3.1(@types/node@24.1.0)(jiti@2.6.1)(lightningcss@1.30.2)) + '@vitest/pretty-format': 3.2.4 + '@vitest/runner': 3.2.4 + '@vitest/snapshot': 3.2.4 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.3.3 + debug: 4.4.1 + expect-type: 1.3.0 + magic-string: 0.30.21 + pathe: 2.0.3 + picomatch: 4.0.2 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.15 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 7.3.1(@types/node@24.1.0)(jiti@2.6.1)(lightningcss@1.30.2) + vite-node: 3.2.4(@types/node@24.1.0)(jiti@2.6.1)(lightningcss@1.30.2) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 24.1.0 + jsdom: 26.1.0 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + void-elements@3.1.0: {} + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + webidl-conversions@3.0.1: {} + webidl-conversions@7.0.0: {} + + whatwg-encoding@3.1.1: + dependencies: + iconv-lite: 0.6.3 + + whatwg-mimetype@4.0.0: {} + + whatwg-url@14.2.0: + dependencies: + tr46: 5.1.1 + webidl-conversions: 7.0.0 + whatwg-url@5.0.0: dependencies: tr46: 0.0.3 @@ -6207,6 +7720,11 @@ snapshots: dependencies: isexe: 2.0.0 + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + winston-transport@4.9.0: dependencies: logform: 2.7.0 @@ -6249,8 +7767,12 @@ snapshots: dependencies: sax: 1.4.1 + xml-name-validator@5.0.0: {} + xmlbuilder@8.2.2: {} + xmlchars@2.2.0: {} + xmlrpc@1.3.2: dependencies: sax: 1.2.4 diff --git a/src/__tests__/pages/_app.test.jsx b/src/__tests__/pages/_app.test.jsx new file mode 100644 index 000000000..ba7b10c38 --- /dev/null +++ b/src/__tests__/pages/_app.test.jsx @@ -0,0 +1,37 @@ +// @vitest-environment jsdom + +import { render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; + +// Next's Head implementation relies on internal Next contexts; stub it for unit tests. +vi.mock("next/head", () => ({ + default: ({ children }) => <>{children}, +})); + +vi.mock("utils/contexts/color", () => ({ + ColorProvider: ({ children }) => <>{children}, +})); +vi.mock("utils/contexts/theme", () => ({ + ThemeProvider: ({ children }) => <>{children}, +})); +vi.mock("utils/contexts/settings", () => ({ + SettingsProvider: ({ children }) => <>{children}, +})); +vi.mock("utils/contexts/tab", () => ({ + TabProvider: ({ children }) => <>{children}, +})); + +import App from "pages/_app.jsx"; + +describe("pages/_app", () => { + it("renders the active page component with pageProps", () => { + function Page({ message }) { + return
msg:{message}
; + } + + render(); + + expect(screen.getByText("msg:hello")).toBeInTheDocument(); + expect(document.querySelector('meta[name="viewport"]')).toBeTruthy(); + }); +}); diff --git a/src/__tests__/pages/_document.test.jsx b/src/__tests__/pages/_document.test.jsx new file mode 100644 index 000000000..3ceeed9f2 --- /dev/null +++ b/src/__tests__/pages/_document.test.jsx @@ -0,0 +1,24 @@ +import { renderToStaticMarkup } from "react-dom/server"; +import { describe, expect, it, vi } from "vitest"; + +vi.mock("next/document", () => ({ + Html: ({ children }) =>
{children}
, + Head: ({ children }) =>
{children}
, + Main: () =>
, + NextScript: () =>