From e0ab03ea7bc5b0358045459c807fd6506fa2d50b Mon Sep 17 00:00:00 2001 From: Sebastian Mohr Date: Fri, 18 Jul 2025 13:46:22 +0200 Subject: [PATCH 1/8] Added database models for push notifications. Also added them to generated frontend types. --- .../beets_flask/database/models/__init__.py | 4 + backend/beets_flask/database/models/push.py | 119 ++++++++++++++++++ backend/generate_types.py | 7 ++ frontend/src/pythonTypes.ts | 19 +++ 4 files changed, 149 insertions(+) create mode 100644 backend/beets_flask/database/models/push.py diff --git a/backend/beets_flask/database/models/__init__.py b/backend/beets_flask/database/models/__init__.py index 0e0f4a34..c1403bc1 100644 --- a/backend/beets_flask/database/models/__init__.py +++ b/backend/beets_flask/database/models/__init__.py @@ -1,4 +1,5 @@ from .base import Base +from .push import PushSettings, PushSubscription, PushWebHook from .states import CandidateStateInDb, FolderInDb, SessionStateInDb, TaskStateInDb __all__ = [ @@ -7,4 +8,7 @@ "SessionStateInDb", "TaskStateInDb", "CandidateStateInDb", + "PushSubscription", + "PushSettings", + "PushWebHook", ] diff --git a/backend/beets_flask/database/models/push.py b/backend/beets_flask/database/models/push.py new file mode 100644 index 00000000..3e89b67c --- /dev/null +++ b/backend/beets_flask/database/models/push.py @@ -0,0 +1,119 @@ +from __future__ import annotations + +from typing import Any + +from sqlalchemy import ForeignKey +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from .base import Base + + +class PushSettings(Base): + """Represents options for a push subscription. + + This class is used to store additional options for a push subscription, + such as when to trigger this notification. + """ + + __tablename__ = "push_settings" + + is_active: Mapped[bool] + + def __init__(self, is_active: bool = True): + super().__init__() + self.is_active = is_active + + def update_from_dict(self, data: dict[str, Any]): + """Update the PushSettings instance from a dictionary. + + None values are ignored. + """ + is_active = data.get("is_active") + self.is_active = is_active if is_active is not None else self.is_active + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> PushSettings: + """Create a PushSettings instance from a dictionary.""" + instance = cls() + instance.update_from_dict(data) + return instance + + +class PushSubscription(Base): + """Represents a push subscription. + + Expects push subscriptions in Web Push API, + https://developer.mozilla.org/en-US/docs/Web/API/Push_API. + """ + + __tablename__ = "push_subscription" + + # id==endpoint + keys: Mapped[dict[str, str]] + expiration_time: Mapped[int | None] + + # Settings on when to trigger this + settings_id: Mapped[str] = mapped_column(ForeignKey("push_settings.id"), index=True) + settings: Mapped[PushSettings] = relationship(foreign_keys=[settings_id]) + + def __init__( + self, + id: str, + keys: dict[str, str] | None = None, + expiration_time: int | None = None, + settings: PushSettings | None = None, + ): + super().__init__(id=id) + self.keys = keys or {} + self.expiration_time = expiration_time + self.settings = settings or PushSettings() + + @property + def endpoint(self) -> str: + """ + Convenience property to get the id. + + Note: Although the id is just the endpoint, when querying the db, you **must** use `PushSubscription.id == endpoint`. Sqlalchemy does not resolve properties. + """ + return self.id + + +class PushWebHook(Base): + """Webhook handlers for push notifications. + + Additionally to :class:`PushSubscription`, this class can be used to handle push notifications + to generic endpoints, such as a webhook URL. + """ + + __tablename__ = "push_webhooks" + + # Rquired fields for push webhooks + url: Mapped[str] + method: Mapped[str] # e.g., "POST", "GET" + + # Optional fields for push webhooks + headers: Mapped[dict[str, str] | None] + params: Mapped[dict[str, str] | None] + body: Mapped[dict[str, Any] | None] + + # Settings on when to trigger this + settings_id: Mapped[str] = mapped_column(ForeignKey("push_settings.id"), index=True) + settings: Mapped[PushSettings] = relationship(foreign_keys=[settings_id]) + + def __init__( + self, + url: str, + method: str = "POST", + headers: dict[str, str] | None = None, + params: dict[str, str] | None = None, + body: dict[str, Any] | None = None, + settings: PushSettings | None = None, + ): + """Initialize a PushWebHooks instance.""" + super().__init__() + self.url = url + self.method = method + self.headers = headers + self.params = params + self.body = body + self.settings = settings or PushSettings() diff --git a/backend/generate_types.py b/backend/generate_types.py index 9b6a1486..1773ef5d 100644 --- a/backend/generate_types.py +++ b/backend/generate_types.py @@ -1,6 +1,7 @@ from py2ts.builder import TSBuilder from beets_flask.disk import Archive, File, FileSystemItem, Folder +from beets_flask.database.models.push import PushSubscription, PushWebHook from beets_flask.importer.states import ( SerializedSessionState, ) @@ -75,5 +76,11 @@ builder.add(FileSystemUpdate) +# ---------------------------- Push/Notifications ---------------------------- # + +builder.add(PushSubscription, exclude={"settings_id"}) +builder.add(PushWebHook, exclude={"settings_id"}) + + builder.save_file("../frontend/src/pythonTypes.ts") print("✅ Typescript types generated successfully!") diff --git a/frontend/src/pythonTypes.ts b/frontend/src/pythonTypes.ts index 24c68478..10176db5 100644 --- a/frontend/src/pythonTypes.ts +++ b/frontend/src/pythonTypes.ts @@ -29,6 +29,25 @@ export interface Search { search_album: null | string; } +export interface PushWebHook extends Base { + url: string; + method: string; + headers: Record | null; + params: Record | null; + body: Record | null; + settings: PushSettings; +} + +export interface PushSubscription extends Base { + keys: Record; + expiration_time: null | number; + settings: PushSettings; +} + +export interface PushSettings extends Base { + is_active: boolean; +} + export interface LibraryStats { libraryPath: string; items: number; From 579a6ce5d1e34ad2facb39ffe3c9b856bc60c90f Mon Sep 17 00:00:00 2001 From: Sebastian Mohr Date: Fri, 18 Jul 2025 13:49:33 +0200 Subject: [PATCH 2/8] Created function to get config dir. --- backend/beets_flask/config/beets_config.py | 17 +++++++++++++---- backend/beets_flask/watchdog/inbox.py | 4 ++-- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/backend/beets_flask/config/beets_config.py b/backend/beets_flask/config/beets_config.py index 9b2e1031..1a0d79df 100644 --- a/backend/beets_flask/config/beets_config.py +++ b/backend/beets_flask/config/beets_config.py @@ -26,6 +26,7 @@ """ import os +from pathlib import Path from beets import IncludeLazyConfig as BeetsConfig from beets.plugins import load_plugins @@ -89,9 +90,7 @@ def reset(self): # Load config from default location (set via env var) # if it is set otherwise use the default location - ib_folder = os.getenv("BEETSFLASKDIR") - if ib_folder is None: - ib_folder = os.path.expanduser("~/.config/beets-flask") + ib_folder = get_bf_config_dir() ib_config_path = os.path.join(ib_folder, "config.yaml") # Check if the user config exists @@ -193,6 +192,16 @@ def get_config(force_refresh=False) -> InteractiveBeetsConfig: return config -__all__ = ["refresh_config", "get_config"] +def get_bf_config_dir() -> Path: + """Get the path to the beets-flask config directory.""" + # This is the directory where the config.yaml file is stored + # and where the vapid keys are stored. + + return Path( + os.getenv("BEETSFLASKDIR", os.path.expanduser("~/.config/beets-flask")) + ).resolve() + + +__all__ = ["refresh_config", "get_config", "get_bf_config_dir"] # raise NotImplementedError("This module should not be imported.") diff --git a/backend/beets_flask/watchdog/inbox.py b/backend/beets_flask/watchdog/inbox.py index f9a4aad1..d4937649 100644 --- a/backend/beets_flask/watchdog/inbox.py +++ b/backend/beets_flask/watchdog/inbox.py @@ -204,10 +204,10 @@ async def auto_tag(folder_path: Path, inbox_kind: str | None = None): # ------------------------------------------------------------------------------------ # -def get_inbox_for_path(path: str | Path): +def get_inbox_for_path(path: str | Path) -> OrderedDict | None: if isinstance(path, str): path = Path(path) - inbox = None + inbox: OrderedDict | None = None for i in get_inboxes(): ipath = Path(i["path"]) if path.is_relative_to(ipath) or path == ipath: From dd26169bdc813b77a0fb9cf23d04f4c793c9e5cc Mon Sep 17 00:00:00 2001 From: Sebastian Mohr Date: Fri, 18 Jul 2025 14:50:04 +0200 Subject: [PATCH 3/8] Added notification routes and frontend queries and mutations for these routes. --- backend/beets_flask/server/routes/__init__.py | 4 + .../server/routes/db_models/push.py | 182 +++++++++++++ .../server/routes/notifications.py | 99 +++++++ backend/pyproject.toml | 1 + frontend/src/api/notifications.ts | 248 ++++++++++++++++++ 5 files changed, 534 insertions(+) create mode 100644 backend/beets_flask/server/routes/db_models/push.py create mode 100644 backend/beets_flask/server/routes/notifications.py create mode 100644 frontend/src/api/notifications.ts diff --git a/backend/beets_flask/server/routes/__init__.py b/backend/beets_flask/server/routes/__init__.py index f0843e2c..aece2a80 100644 --- a/backend/beets_flask/server/routes/__init__.py +++ b/backend/beets_flask/server/routes/__init__.py @@ -8,6 +8,7 @@ from .inbox import inbox_bp from .library import library_bp from .monitor import monitor_bp +from .notifications import register_notifications backend_bp = Blueprint("backend", __name__, url_prefix="/api_v1") @@ -26,6 +27,9 @@ def register_routes(app: Quart): # to api blueprint i.e. /api_v1/session, /api_v1/task & /api_v1/candidate register_state_models(backend_bp) + # notifications/ + register_notifications(backend_bp) + app.register_blueprint(backend_bp) app.register_blueprint(frontend_bp) diff --git a/backend/beets_flask/server/routes/db_models/push.py b/backend/beets_flask/server/routes/db_models/push.py new file mode 100644 index 00000000..6a56c5c1 --- /dev/null +++ b/backend/beets_flask/server/routes/db_models/push.py @@ -0,0 +1,182 @@ +from typing import Any + +from quart import request + +from beets_flask.database import db_session_factory +from beets_flask.database.models import PushSettings, PushSubscription, PushWebHook +from beets_flask.server.routes.exception import InvalidUsageException +from beets_flask.server.utility import pop_query_param + +from .base import ModelAPIBlueprint + + +class WebHookBlueprint(ModelAPIBlueprint[PushWebHook]): + def __init__(self): + super().__init__(PushWebHook, url_prefix="/webhook") + self.register_routes() + + def register_routes(self): + """Register the routes for the blueprint.""" + super()._register_routes() + self.blueprint.route("/", methods=["POST"])(self.upsert) + + async def upsert(self): + """Upsert a push webhook.""" + params = await request.get_json() + id, url, method, headers, params, body, settings = self._parse_webhook_params( + params + ) + + if not url: + raise InvalidUsageException( + "Missing 'url' parameter in webhook subscription", + status_code=400, + ) + if not method: + raise InvalidUsageException( + "Missing 'method' parameter in webhook subscription", + status_code=400, + ) + + # Upsert webhook + with db_session_factory() as db_session: + if id: + # update + if web_hook := PushWebHook.get_by( + PushWebHook.id == id, session=db_session + ): + web_hook.url = url + web_hook.method = method + web_hook.headers = headers + web_hook.params = params + web_hook.body = body + if settings: + web_hook.settings.update_from_dict(settings) + db_session.commit() + return web_hook.to_dict(), 200 + else: + raise InvalidUsageException( + f"Webhook with id '{id}' does not exist", + status_code=404, + ) + # insert + web_hook = PushWebHook( + url=url, + method=method, + headers=headers, + params=params, + body=body, + settings=PushSettings.from_dict(settings or {}), + ) + db_session.add(web_hook) + db_session.commit() + + return web_hook.to_dict(), 201 + + def _parse_webhook_params(self, params: Any): + """Parse the parameters for the webhook subscription.""" + if not isinstance(params, dict): + raise InvalidUsageException( + "Invalid parameters provided for webhook subscription", + status_code=400, + ) + + # Standard PushSubscription fields + id = pop_query_param(params, "id", str, default=None) + url = pop_query_param(params, "url", str, default=None) + method = pop_query_param(params, "method", str, default=None) + + # Optional fields for webhook + headers = pop_query_param(params, "headers", dict, default=None) + params = pop_query_param(params, "params", dict, default=None) + body = pop_query_param(params, "body", dict, default=None) + + # Settings for the webhook + settings = pop_query_param(params, "settings", dict, default=None) + + return ( + id, + url, + method, + headers, + params, + body, + settings, + ) + + +class SubscriptionBlueprint(ModelAPIBlueprint[PushSubscription]): + def __init__(self): + super().__init__(PushSubscription, url_prefix="/subscription") + self.register_routes() + + def register_routes(self): + """Register the routes for the blueprint.""" + super()._register_routes() + self.blueprint.route("/", methods=["POST"])(self.upsert) + + async def upsert(self): + """Upsert a push subscription.""" + params = await request.get_json() + endpoint, expiration_time, keys, settings = self._parse_subscription_params( + params + ) + + if not endpoint or not keys: + raise InvalidUsageException( + "Missing 'endpoint' or 'keys' parameter in subscription", + status_code=400, + ) + + # Upsert subscription (id == endpoint) + with db_session_factory() as db_session: + if subscription := PushSubscription.get_by( + PushSubscription.id == endpoint, session=db_session + ): + # update + subscription.keys = keys + subscription.expiration_time = expiration_time + if settings: + subscription.settings.update_from_dict(settings) + db_session.commit() + return subscription.to_dict(), 200 + + # insert + subscription = PushSubscription( + id=endpoint, + keys=keys, + expiration_time=expiration_time, + settings=PushSettings.from_dict(settings or {}), + ) + db_session.add(subscription) + db_session.commit() + return subscription.to_dict(), 201 + + def _parse_subscription_params(self, params: Any): + """Parse the parameters for the push API.""" + if not isinstance(params, dict): + raise InvalidUsageException( + "Invalid parameters provided for subscription", + status_code=400, + ) + + # Standard PushSubscription fields + endpoint = pop_query_param(params, "endpoint", str, default=None) + expiration_time = pop_query_param(params, "expirationTime", int, default=None) + keys = pop_query_param(params, "keys", dict, default=None) + + # Extra options for the subscription (i.e. which notifications to receive) + settings = pop_query_param(params, "settings", dict, default=None) + + if len(params) > 0: + raise InvalidUsageException( + "Invalid parameters provided for subscription", + status_code=400, + ) + + return ( + endpoint, + expiration_time, + keys, + settings, + ) diff --git a/backend/beets_flask/server/routes/notifications.py b/backend/beets_flask/server/routes/notifications.py new file mode 100644 index 00000000..31a58522 --- /dev/null +++ b/backend/beets_flask/server/routes/notifications.py @@ -0,0 +1,99 @@ +"""Notifications api. + +Notification allow to receive updates about changes in the backend. +For instance new tags generated or new imported albums. + +Currently we support web push notifications and arbitrary webhooks. +""" + +from __future__ import annotations + +import base64 +import json +import select +from typing import Literal, NamedTuple, TypedDict + +import ecdsa +from pywebpush import WebPushException, webpush +from quart import Blueprint, Quart, request + +from beets_flask.config.beets_config import get_bf_config_dir +from beets_flask.database import db_session_factory +from beets_flask.database.models import PushSubscription +from beets_flask.logger import log +from beets_flask.server.routes.exception import InvalidUsageException +from beets_flask.server.routes.inbox import get_inbox_for_path + +from .db_models.push import SubscriptionBlueprint, WebHookBlueprint + +notification_bp = Blueprint("notifications", __name__, url_prefix="/notifications") + +# -------------------------------- Vapid keys -------------------------------- # +# Vapid keys are used for web push notifications to authenticate the sender. +# TODO: Add a way to add a sub information to the VAPID keys, e.g. an email address. +# Maybe config? + + +@notification_bp.route("/vapid_key", methods=["GET"]) +def get_vapid_key(): + """Get the public VAPID key for web push notifications.""" + keypair = get_vapid_keypair() + + return ( + {"key": keypair.public}, + 200, + {"Content-Type": "application/json"}, + ) + + +class VapidKeyPair(NamedTuple): + private: str + public: str + + +def get_vapid_keypair(): + """Get the VAPID keys for web push notifications.""" + file = get_bf_config_dir() / "vapid_keys.json" + if not file.exists(): + # If the file does not exist, generate a new keypair + keys = _generate_vapid_keypair() + with open(file, "w") as f: + f.write( + json.dumps( + { + "private_key": keys.private, + "public_key": keys.public, + } + ) + ) + return keys + with open(file, "r") as f: + data = json.load(f) + return VapidKeyPair( + private=data["private_key"], + public=data["public_key"], + ) + + +def _generate_vapid_keypair(): + # See https://datatracker.ietf.org/doc/rfc8292/ + pk = ecdsa.SigningKey.generate(curve=ecdsa.NIST256p) + vk = pk.get_verifying_key() + + return VapidKeyPair( + private=base64.urlsafe_b64encode(pk.to_string()).strip(b"=").decode("utf-8"), + public=base64.urlsafe_b64encode(b"\x04" + vk.to_string()) # type: ignore + .strip(b"=") + .decode("utf-8"), + ) + + +# --------------------------------- Register --------------------------------- # + + +def register_notifications(app: Blueprint | Quart): + """Register the push models with the app.""" + + notification_bp.register_blueprint(WebHookBlueprint().blueprint) + notification_bp.register_blueprint(SubscriptionBlueprint().blueprint) + app.register_blueprint(notification_bp) diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 9ac20566..5ef9423e 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -40,6 +40,7 @@ dependencies = [ "aiofiles", "numpy", "pandas", + "ecdsa", "typing_extensions", ] diff --git a/frontend/src/api/notifications.ts b/frontend/src/api/notifications.ts new file mode 100644 index 00000000..03e98566 --- /dev/null +++ b/frontend/src/api/notifications.ts @@ -0,0 +1,248 @@ +import { queryOptions, UseMutationOptions } from "@tanstack/react-query"; + +import { + PushSettings, + PushSubscription as PyPushSubscription, + PushWebHook as PyPushWebHook, +} from "@/pythonTypes"; + +import { APIError, queryClient } from "./common"; + +class PushSubscriptionError extends Error { + constructor(message: string) { + super(message); + this.name = "PushSubscriptionError"; + } +} + +/* -------------------------------- webhooks -------------------------------- */ + +export interface PushWebHookSubscribeRequest + extends Omit { + settings: null | PushSettings; + id?: string; // Optional ID for upsert +} + +/** Upsert a webhook + * can be used to update an existing webhook or create a new one. + */ +export const subscribeWebhookMutationOptions: UseMutationOptions< + PyPushWebHook, + APIError | PushSubscriptionError, + PushWebHookSubscribeRequest +> = { + mutationKey: ["webhook", "upsert"], + mutationFn: async (params: PushWebHookSubscribeRequest) => { + const response = await fetch("/notifications/webhook/", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(params), + }); + if (!response.ok) { + throw new PushSubscriptionError( + `Failed to subscribe webhook: ${response.statusText}` + ); + } + const webHook = (await response.json()) as PyPushWebHook; + + // We might have some webhook data in the cache already + queryClient.setQueryData( + webhookQueryOptions(webHook.id).queryKey, + webHook + ); + + // TODO: Update the infinite query for webhooks + + return (await response.json()) as PyPushWebHook; + }, +}; + +/** Remove a webhook */ +export const unsubscribeWebhookMutationOptions: UseMutationOptions< + void, + APIError | PushSubscriptionError, + string +> = { + mutationKey: ["webhook", "unsubscribe"], + mutationFn: async (id: string) => { + await fetch(`/notifications/webhook/${id}`, { + method: "DELETE", + }); + queryClient.removeQueries(webhookQueryOptions(id)); + + // TODO: Update the infinite query for webhooks + }, +}; + +export const webhookQueryOptions = (id: string) => + queryOptions({ + queryKey: ["webhook", id], + queryFn: async () => { + const response = await fetch(`/notifications/webhook/${id}`); + + return (await response.json()) as PyPushWebHook; + }, + }); + +// TODO: Infinity query for all webhooks + +/* ------------------------------ web push api ------------------------------ */ + +export interface PushSubscriptionUpsertRequest + extends Omit { + settings: null | PushSettings; + endpoint: string; +} + +export interface PushSubscriptionReturn { + server: PyPushSubscription; + subscription: PushSubscription; +} + +/** Subscribing using a web hook is a bit more difficult in comparison + * to adding a webhook. + * + * We need to do a key exchange with the server to get the public VAPID key + * and then use the PushManager to subscribe to the push notifications. + * Per VAPID key their can only be one subscription, so we dont need to + * pass the id. + * + * We expect this function to be called in the main thread, not in a service worker. + */ +export const subscribePushMutationOptions: UseMutationOptions< + PushSubscriptionReturn, + APIError | PushSubscriptionError +> = { + mutationKey: ["subscription", "upsert"], + mutationFn: async () => { + // Get server's public VAPID key + const keyResponse = await fetch("/notifications/vapid_key"); + const { key } = (await keyResponse.json()) as { key: string }; + + // Get pushManager from the service worker registration + const registration = await navigator.serviceWorker.ready; + if (!registration.pushManager) { + console.warn( + "Push Manager is not available in this service worker registration.", + registration + ); + throw new PushSubscriptionError("PushManager unavailable"); + } + + // Subscribe to push notifications using the PushManager + const subscription = await registration.pushManager.subscribe({ + userVisibleOnly: true, + applicationServerKey: key, // This is the public vapid key + }); + + // Send the subscription to the server to allow the server to + // send notifications + const response = await fetch("/notifications/subscription/", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(subscription.toJSON()), + }); + + return { + server: (await response.json()) as PyPushSubscription, + subscription, + }; + }, +}; + +export const unsubscribePushMutationOptions: UseMutationOptions< + void, + APIError | PushSubscriptionError +> = { + mutationKey: ["subscription", "unsubscribe"], + mutationFn: async () => { + // Get pushManager from the service worker registration + const registration = await navigator.serviceWorker.ready; + if (!registration.pushManager) { + console.warn( + "Push Manager is not available in this service worker registration.", + registration + ); + throw new PushSubscriptionError("PushManager unavailable"); + } + + // Get the current subscription + const subscription = await registration.pushManager.getSubscription(); + if (!subscription) { + console.warn("No push subscription found."); + return; + } + + // Notify the server that we unsubscribed + await fetch(`/notifications/${encodeURIComponent(subscription.endpoint)}`, { + method: "DELETE", + }); + await invalidatePushQuery(); + + // Unsubscribe from push notifications locally + await subscription.unsubscribe(); + }, +}; + +/** Push subscription for the current service worker registration. + * + * This does not do a network request, it is just a helper function to get the + * PushManager from the service worker registration. + * + * May return null if there is no registered push subscription! + */ +export const pushQueryOptions = queryOptions< + PushSubscriptionReturn | null, + APIError | PushSubscriptionError +>({ + queryKey: ["subscription"], + queryFn: async () => { + if (!("serviceWorker" in navigator)) { + console.warn( + "Service Worker and therefore Push Notifications are not supported in this browser." + ); + throw new PushSubscriptionError(`Browser lacks Service Worker support. Please use a + modern browser that supports the Push API. You might need to host this application + on a secure context (HTTPS or use localhost).`); + } + + // Get pushManager from the service worker registration + const registration = await navigator.serviceWorker.ready; + if (!registration.pushManager) { + console.warn( + "Push Manager is not available in this service worker registration.", + registration + ); + throw new PushSubscriptionError("PushManager unavailable."); + } + + const localSubscription = await registration.pushManager.getSubscription(); + + if (!localSubscription) { + console.warn("No push subscription found."); + return null; + } + + // Get the server subscription + const response = await fetch( + `/notifications/${encodeURIComponent(localSubscription.endpoint)}` + ); + const data = (await response.json()) as PyPushSubscription; + return { + server: data, + subscription: localSubscription, + }; + }, + staleTime: Infinity, + retry: false, +}); + +async function invalidatePushQuery() { + await queryClient + .cancelQueries(pushQueryOptions) + .then(() => queryClient.invalidateQueries(pushQueryOptions)); +} From 0fcaac2cc818243599a364b87773dbdd2988f101 Mon Sep 17 00:00:00 2001 From: Sebastian Mohr Date: Sun, 20 Jul 2025 22:45:16 +0200 Subject: [PATCH 4/8] renamed notifications models, created a components folder and moved components out of debug page. --- .../beets_flask/database/models/__init__.py | 6 +- backend/beets_flask/database/models/push.py | 38 +- .../server/routes/db_models/base.py | 4 +- .../server/routes/db_models/push.py | 26 +- .../server/routes/notifications.py | 103 + backend/generate_types.py | 4 +- backend/pyproject.toml | 1 + frontend/package.json | 1 + frontend/pnpm-lock.yaml | 2190 +++++++++++++++-- frontend/src/api/notifications.ts | 208 +- frontend/src/components/common/strings.tsx | 39 + .../src/components/notifications/push.tsx | 292 +++ .../src/components/notifications/settings.tsx | 37 + .../useNotificationPermission.ts | 70 + .../src/components/notifications/webhooks.tsx | 589 +++++ frontend/src/main.tsx | 40 + frontend/src/pythonTypes.ts | 30 +- frontend/src/routeTree.gen.ts | 26 + frontend/src/routes/_frontpage/index.tsx | 7 +- .../src/routes/settings/notifications.tsx | 96 + frontend/src/worker.ts | 169 ++ frontend/vite.config.ts | 16 + 22 files changed, 3758 insertions(+), 234 deletions(-) create mode 100644 frontend/src/components/notifications/push.tsx create mode 100644 frontend/src/components/notifications/settings.tsx create mode 100644 frontend/src/components/notifications/useNotificationPermission.ts create mode 100644 frontend/src/components/notifications/webhooks.tsx create mode 100644 frontend/src/routes/settings/notifications.tsx create mode 100644 frontend/src/worker.ts diff --git a/backend/beets_flask/database/models/__init__.py b/backend/beets_flask/database/models/__init__.py index c1403bc1..db4f7937 100644 --- a/backend/beets_flask/database/models/__init__.py +++ b/backend/beets_flask/database/models/__init__.py @@ -1,5 +1,5 @@ from .base import Base -from .push import PushSettings, PushSubscription, PushWebHook +from .push import SubscriptionSettings, PushSubscription, WebhookSubscription from .states import CandidateStateInDb, FolderInDb, SessionStateInDb, TaskStateInDb __all__ = [ @@ -9,6 +9,6 @@ "TaskStateInDb", "CandidateStateInDb", "PushSubscription", - "PushSettings", - "PushWebHook", + "SubscriptionSettings", + "WebhookSubscription", ] diff --git a/backend/beets_flask/database/models/push.py b/backend/beets_flask/database/models/push.py index 3e89b67c..12ddfc6b 100644 --- a/backend/beets_flask/database/models/push.py +++ b/backend/beets_flask/database/models/push.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Any +from typing import Any, Mapping from sqlalchemy import ForeignKey from sqlalchemy.orm import Mapped, mapped_column, relationship @@ -8,7 +8,7 @@ from .base import Base -class PushSettings(Base): +class SubscriptionSettings(Base): """Represents options for a push subscription. This class is used to store additional options for a push subscription, @@ -32,7 +32,7 @@ def update_from_dict(self, data: dict[str, Any]): self.is_active = is_active if is_active is not None else self.is_active @classmethod - def from_dict(cls, data: dict[str, Any]) -> PushSettings: + def from_dict(cls, data: dict[str, Any]) -> SubscriptionSettings: """Create a PushSettings instance from a dictionary.""" instance = cls() instance.update_from_dict(data) @@ -54,19 +54,21 @@ class PushSubscription(Base): # Settings on when to trigger this settings_id: Mapped[str] = mapped_column(ForeignKey("push_settings.id"), index=True) - settings: Mapped[PushSettings] = relationship(foreign_keys=[settings_id]) + settings: Mapped[SubscriptionSettings] = relationship( + foreign_keys=[settings_id], cascade="all" + ) def __init__( self, id: str, keys: dict[str, str] | None = None, expiration_time: int | None = None, - settings: PushSettings | None = None, + settings: SubscriptionSettings | None = None, ): super().__init__(id=id) self.keys = keys or {} self.expiration_time = expiration_time - self.settings = settings or PushSettings() + self.settings = settings or SubscriptionSettings() @property def endpoint(self) -> str: @@ -77,8 +79,15 @@ def endpoint(self) -> str: """ return self.id + def to_dict(self) -> Mapping: + col_map = super().to_dict() + return { + **col_map, + "settings": self.settings.to_dict(), + } -class PushWebHook(Base): + +class WebhookSubscription(Base): """Webhook handlers for push notifications. Additionally to :class:`PushSubscription`, this class can be used to handle push notifications @@ -98,7 +107,9 @@ class PushWebHook(Base): # Settings on when to trigger this settings_id: Mapped[str] = mapped_column(ForeignKey("push_settings.id"), index=True) - settings: Mapped[PushSettings] = relationship(foreign_keys=[settings_id]) + settings: Mapped[SubscriptionSettings] = relationship( + foreign_keys=[settings_id], cascade="all" + ) def __init__( self, @@ -107,7 +118,7 @@ def __init__( headers: dict[str, str] | None = None, params: dict[str, str] | None = None, body: dict[str, Any] | None = None, - settings: PushSettings | None = None, + settings: SubscriptionSettings | None = None, ): """Initialize a PushWebHooks instance.""" super().__init__() @@ -116,4 +127,11 @@ def __init__( self.headers = headers self.params = params self.body = body - self.settings = settings or PushSettings() + self.settings = settings or SubscriptionSettings() + + def to_dict(self) -> Mapping: + col_map = super().to_dict() + return { + **col_map, + "settings": self.settings.to_dict(), + } diff --git a/backend/beets_flask/server/routes/db_models/base.py b/backend/beets_flask/server/routes/db_models/base.py index 90c612b6..bd83f130 100644 --- a/backend/beets_flask/server/routes/db_models/base.py +++ b/backend/beets_flask/server/routes/db_models/base.py @@ -41,8 +41,8 @@ def __init__(self, model: type[T], url_prefix: str | None = None): def _register_routes(self) -> None: """Register the routes for the blueprint.""" self.blueprint.route("/", methods=["GET"])(self.get_all) - self.blueprint.route("/id/", methods=["GET"])(self.get_by_id) - self.blueprint.route("/id/", methods=["DELETE"])(self.delete_by_id) + self.blueprint.route("/id/", methods=["GET"])(self.get_by_id) + self.blueprint.route("/id/", methods=["DELETE"])(self.delete_by_id) async def get_all(self): params = dict(request.args) diff --git a/backend/beets_flask/server/routes/db_models/push.py b/backend/beets_flask/server/routes/db_models/push.py index 6a56c5c1..7b172a57 100644 --- a/backend/beets_flask/server/routes/db_models/push.py +++ b/backend/beets_flask/server/routes/db_models/push.py @@ -3,19 +3,22 @@ from quart import request from beets_flask.database import db_session_factory -from beets_flask.database.models import PushSettings, PushSubscription, PushWebHook +from beets_flask.database.models import ( + SubscriptionSettings, + PushSubscription, + WebhookSubscription, +) from beets_flask.server.routes.exception import InvalidUsageException from beets_flask.server.utility import pop_query_param from .base import ModelAPIBlueprint -class WebHookBlueprint(ModelAPIBlueprint[PushWebHook]): +class WebHookBlueprint(ModelAPIBlueprint[WebhookSubscription]): def __init__(self): - super().__init__(PushWebHook, url_prefix="/webhook") - self.register_routes() + super().__init__(WebhookSubscription, url_prefix="/webhook") - def register_routes(self): + def _register_routes(self): """Register the routes for the blueprint.""" super()._register_routes() self.blueprint.route("/", methods=["POST"])(self.upsert) @@ -42,8 +45,8 @@ async def upsert(self): with db_session_factory() as db_session: if id: # update - if web_hook := PushWebHook.get_by( - PushWebHook.id == id, session=db_session + if web_hook := WebhookSubscription.get_by( + WebhookSubscription.id == id, session=db_session ): web_hook.url = url web_hook.method = method @@ -60,13 +63,13 @@ async def upsert(self): status_code=404, ) # insert - web_hook = PushWebHook( + web_hook = WebhookSubscription( url=url, method=method, headers=headers, params=params, body=body, - settings=PushSettings.from_dict(settings or {}), + settings=SubscriptionSettings.from_dict(settings or {}), ) db_session.add(web_hook) db_session.commit() @@ -108,9 +111,8 @@ def _parse_webhook_params(self, params: Any): class SubscriptionBlueprint(ModelAPIBlueprint[PushSubscription]): def __init__(self): super().__init__(PushSubscription, url_prefix="/subscription") - self.register_routes() - def register_routes(self): + def _register_routes(self): """Register the routes for the blueprint.""" super()._register_routes() self.blueprint.route("/", methods=["POST"])(self.upsert) @@ -146,7 +148,7 @@ async def upsert(self): id=endpoint, keys=keys, expiration_time=expiration_time, - settings=PushSettings.from_dict(settings or {}), + settings=SubscriptionSettings.from_dict(settings or {}), ) db_session.add(subscription) db_session.commit() diff --git a/backend/beets_flask/server/routes/notifications.py b/backend/beets_flask/server/routes/notifications.py index 31a58522..999eca17 100644 --- a/backend/beets_flask/server/routes/notifications.py +++ b/backend/beets_flask/server/routes/notifications.py @@ -97,3 +97,106 @@ def register_notifications(app: Blueprint | Quart): notification_bp.register_blueprint(WebHookBlueprint().blueprint) notification_bp.register_blueprint(SubscriptionBlueprint().blueprint) app.register_blueprint(notification_bp) + + +# ----------------------------------- Tests ---------------------------------- # + + +@notification_bp.route("/notify_test", methods=["POST"]) +async def notify_test(): + """Send a test notification to all subscribed clients.""" + params = await request.get_json() or {} + + inbox = get_inbox_for_path( + "/music/inbox/to/album", + ) + # Create a test notification + notification = TaggedNotification( + hash="test_hash", + path="/music/inbox/to/album", + type="tagged", + nCandidates=10, + bestCandidate="Test Album - Test Artist", + bestCandidateMatch=0.95, + inboxPath=inbox["path"] if inbox else None, + ) + + if len(params) > 0: + raise InvalidUsageException( + "Invalid parameters provided for notification", + status_code=400, + ) + + # Push the notification + push_notification(notification) + + return "Test notification sent successfully", 200 + + +from sqlalchemy import select + + +def push_notification( + notification: Notification, +): + """Push a notification to all subscribed clients.""" + vapid_keys = get_vapid_keypair() + + # Get all subscriptions from the database + with db_session_factory() as db_session: + stmt = select(PushSubscription) + subscriptions = db_session.scalars(stmt).all() + + if not subscriptions: + return + + # Try to send the notification to each subscription + # it the status is 410 Gone, remove the subscription + + for subscription in subscriptions: + keys = subscription.keys + try: + webpush( + subscription_info={ + "endpoint": subscription.id, + "keys": {k: v for k, v in keys.items()}, + }, + data=json.dumps(notification), + vapid_private_key=vapid_keys.private, + vapid_claims={"sub": "mailto:test@test.de"}, + ) + except WebPushException as e: + if e.response is not None and e.response.status_code == 410: + # Subscription is no longer valid, remove it + instance = PushSubscription.get_by( + PushSubscription.id == subscription.id, + session=db_session, + ) + if instance: + db_session.delete(instance) + db_session.commit() + else: + log.exception( + f"Failed to send notification to {subscription.id}", e + ) + log.debug(f"Notification sent to {len(subscriptions)} subscribers") + + +# ---------------------------- Notification types ---------------------------- # + + +class Notification(TypedDict): + """Notification for tagged albums.""" + + hash: str + path: str + + +class TaggedNotification(Notification): + """Notification for tagged albums with a type.""" + + type: Literal["tagged"] + nCandidates: int + bestCandidate: str # title - artist + bestCandidateMatch: float # match percentage (1.0 = 100%) + inboxPath: str | None # path to the inbox this folder is in diff --git a/backend/generate_types.py b/backend/generate_types.py index 1773ef5d..b1f96e44 100644 --- a/backend/generate_types.py +++ b/backend/generate_types.py @@ -1,7 +1,7 @@ from py2ts.builder import TSBuilder from beets_flask.disk import Archive, File, FileSystemItem, Folder -from beets_flask.database.models.push import PushSubscription, PushWebHook +from beets_flask.database.models.push import PushSubscription, WebhookSubscription from beets_flask.importer.states import ( SerializedSessionState, ) @@ -79,7 +79,7 @@ # ---------------------------- Push/Notifications ---------------------------- # builder.add(PushSubscription, exclude={"settings_id"}) -builder.add(PushWebHook, exclude={"settings_id"}) +builder.add(WebhookSubscription, exclude={"settings_id"}) builder.save_file("../frontend/src/pythonTypes.ts") diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 5ef9423e..b0aefd20 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -41,6 +41,7 @@ dependencies = [ "numpy", "pandas", "ecdsa", + "pywebpush", "typing_extensions", ] diff --git a/frontend/package.json b/frontend/package.json index 54d79caf..00563fe5 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -59,6 +59,7 @@ "typescript": "^5.8.3", "typescript-eslint": "^8.33.0", "vite": "^6.3.5", + "vite-plugin-pwa": "^1.0.1", "vite-plugin-svgr": "^4.3.0", "vite-tsconfig-paths": "^4.3.2" }, diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index c0b0b599..c6d7c162 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -83,7 +83,7 @@ importers: version: 5.78.0(eslint@9.28.0(jiti@1.21.7))(typescript@5.8.3) '@tanstack/router-vite-plugin': specifier: ^1.120.13 - version: 1.120.13(@tanstack/react-router@1.120.13(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(vite@6.3.5(@types/node@24.0.0)(jiti@1.21.7)(sass@1.89.1)(tsx@4.19.4)) + version: 1.120.13(@tanstack/react-router@1.120.13(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(vite@6.3.5(@types/node@24.0.0)(jiti@1.21.7)(sass@1.89.1)(terser@5.43.1)(tsx@4.19.4)) '@types/diff': specifier: ^5.2.3 version: 5.2.3 @@ -107,10 +107,10 @@ importers: version: 8.33.0(eslint@9.28.0(jiti@1.21.7))(typescript@5.8.3) '@vitejs/plugin-react': specifier: ^4.5.0 - version: 4.5.0(vite@6.3.5(@types/node@24.0.0)(jiti@1.21.7)(sass@1.89.1)(tsx@4.19.4)) + version: 4.5.0(vite@6.3.5(@types/node@24.0.0)(jiti@1.21.7)(sass@1.89.1)(terser@5.43.1)(tsx@4.19.4)) '@vitejs/plugin-react-swc': specifier: ^3.10.0 - version: 3.10.0(vite@6.3.5(@types/node@24.0.0)(jiti@1.21.7)(sass@1.89.1)(tsx@4.19.4)) + version: 3.10.0(vite@6.3.5(@types/node@24.0.0)(jiti@1.21.7)(sass@1.89.1)(terser@5.43.1)(tsx@4.19.4)) babel-plugin-react-compiler: specifier: 19.1.0-rc.2 version: 19.1.0-rc.2 @@ -146,13 +146,16 @@ importers: version: 8.33.0(eslint@9.28.0(jiti@1.21.7))(typescript@5.8.3) vite: specifier: ^6.3.5 - version: 6.3.5(@types/node@24.0.0)(jiti@1.21.7)(sass@1.89.1)(tsx@4.19.4) + version: 6.3.5(@types/node@24.0.0)(jiti@1.21.7)(sass@1.89.1)(terser@5.43.1)(tsx@4.19.4) + vite-plugin-pwa: + specifier: ^1.0.1 + version: 1.0.1(vite@6.3.5(@types/node@24.0.0)(jiti@1.21.7)(sass@1.89.1)(terser@5.43.1)(tsx@4.19.4))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.3.0) vite-plugin-svgr: specifier: ^4.3.0 - version: 4.3.0(rollup@4.40.2)(typescript@5.8.3)(vite@6.3.5(@types/node@24.0.0)(jiti@1.21.7)(sass@1.89.1)(tsx@4.19.4)) + version: 4.3.0(rollup@2.79.2)(typescript@5.8.3)(vite@6.3.5(@types/node@24.0.0)(jiti@1.21.7)(sass@1.89.1)(terser@5.43.1)(tsx@4.19.4)) vite-tsconfig-paths: specifier: ^4.3.2 - version: 4.3.2(typescript@5.8.3)(vite@6.3.5(@types/node@24.0.0)(jiti@1.21.7)(sass@1.89.1)(tsx@4.19.4)) + version: 4.3.2(typescript@5.8.3)(vite@6.3.5(@types/node@24.0.0)(jiti@1.21.7)(sass@1.89.1)(terser@5.43.1)(tsx@4.19.4)) packages: @@ -160,6 +163,12 @@ packages: resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} + '@apideck/better-ajv-errors@0.3.6': + resolution: {integrity: sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA==} + engines: {node: '>=10'} + peerDependencies: + ajv: '>=8' + '@babel/code-frame@7.27.1': resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} @@ -168,6 +177,10 @@ packages: resolution: {integrity: sha512-TUtMJYRPyUb/9aU8f3K0mjmjf6M9N5Woshn2CS6nqJSeJtTtQcpLUXjGt9vbF8ZGff0El99sWkLgzwW3VXnxZQ==} engines: {node: '>=6.9.0'} + '@babel/compat-data@7.28.0': + resolution: {integrity: sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==} + engines: {node: '>=6.9.0'} + '@babel/core@7.27.1': resolution: {integrity: sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==} engines: {node: '>=6.9.0'} @@ -184,6 +197,10 @@ packages: resolution: {integrity: sha512-xnlJYj5zepml8NXtjkG0WquFUv8RskFqyFcVgTBp5k+NaA/8uw/K+OSVf8AMGw5e9HKP2ETd5xpK5MLZQD6b4Q==} engines: {node: '>=6.9.0'} + '@babel/generator@7.28.0': + resolution: {integrity: sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==} + engines: {node: '>=6.9.0'} + '@babel/helper-annotate-as-pure@7.27.3': resolution: {integrity: sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==} engines: {node: '>=6.9.0'} @@ -198,6 +215,21 @@ packages: peerDependencies: '@babel/core': ^7.0.0 + '@babel/helper-create-regexp-features-plugin@7.27.1': + resolution: {integrity: sha512-uVDC72XVf8UbrH5qQTc18Agb8emwjTiZrQE11Nv3CuBEZmVvTwwE9CBUEvHku06gQCAyYf8Nv6ja1IN+6LMbxQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-define-polyfill-provider@0.6.5': + resolution: {integrity: sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + '@babel/helper-member-expression-to-functions@7.27.1': resolution: {integrity: sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==} engines: {node: '>=6.9.0'} @@ -226,6 +258,12 @@ packages: resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} engines: {node: '>=6.9.0'} + '@babel/helper-remap-async-to-generator@7.27.1': + resolution: {integrity: sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + '@babel/helper-replace-supers@7.27.1': resolution: {integrity: sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==} engines: {node: '>=6.9.0'} @@ -248,6 +286,10 @@ packages: resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} engines: {node: '>=6.9.0'} + '@babel/helper-wrap-function@7.27.1': + resolution: {integrity: sha512-NFJK2sHUvrjo8wAU/nQTWU890/zB2jj0qBcCbZbbf+005cAsv6tMjXz31fBign6M5ov1o0Bllu+9nbqkfsjjJQ==} + engines: {node: '>=6.9.0'} + '@babel/helpers@7.27.1': resolution: {integrity: sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ==} engines: {node: '>=6.9.0'} @@ -266,6 +308,59 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + '@babel/parser@7.28.0': + resolution: {integrity: sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.27.1': + resolution: {integrity: sha512-QPG3C9cCVRQLxAVwmefEmwdTanECuUBMQZ/ym5kiw3XKCGA7qkuQLcjWWHcrD/GKbn/WmJwaezfuuAOcyKlRPA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.27.1': + resolution: {integrity: sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.27.1': + resolution: {integrity: sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.27.1': + resolution: {integrity: sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.13.0 + + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.27.1': + resolution: {integrity: sha512-6BpaYGDavZqkI6yT+KSPdpZFfpnd68UKXbcjI9pJ13pvHhPrCKWOOLp+ysvMeA+DxnhuPpgIaRpxRxo5A9t5jw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2': + resolution: {integrity: sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-assertions@7.27.1': + resolution: {integrity: sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-attributes@7.27.1': + resolution: {integrity: sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-syntax-jsx@7.27.1': resolution: {integrity: sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==} engines: {node: '>=6.9.0'} @@ -278,177 +373,502 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-private-methods@7.27.1': - resolution: {integrity: sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA==} + '@babel/plugin-syntax-unicode-sets-regex@7.18.6': + resolution: {integrity: sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-arrow-functions@7.27.1': + resolution: {integrity: sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-react-jsx-self@7.27.1': - resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} + '@babel/plugin-transform-async-generator-functions@7.28.0': + resolution: {integrity: sha512-BEOdvX4+M765icNPZeidyADIvQ1m1gmunXufXxvRESy/jNNyfovIqUyE7MVgGBjWktCoJlzvFA1To2O4ymIO3Q==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-react-jsx-source@7.27.1': - resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} + '@babel/plugin-transform-async-to-generator@7.27.1': + resolution: {integrity: sha512-NREkZsZVJS4xmTr8qzE5y8AfIPqsdQfRuUiLRTEzb7Qii8iFWCyDKaUV2c0rCuh4ljDZ98ALHP/PetiBV2nddA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/runtime@7.27.1': - resolution: {integrity: sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==} + '@babel/plugin-transform-block-scoped-functions@7.27.1': + resolution: {integrity: sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==} engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 - '@babel/template@7.27.2': - resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} + '@babel/plugin-transform-block-scoping@7.28.0': + resolution: {integrity: sha512-gKKnwjpdx5sER/wl0WN0efUBFzF/56YZO0RJrSYP4CljXnP31ByY7fol89AzomdlLNzI36AvOTmYHsnZTCkq8Q==} engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 - '@babel/traverse@7.27.1': - resolution: {integrity: sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg==} + '@babel/plugin-transform-class-properties@7.27.1': + resolution: {integrity: sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA==} engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 - '@babel/traverse@7.27.4': - resolution: {integrity: sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA==} + '@babel/plugin-transform-class-static-block@7.27.1': + resolution: {integrity: sha512-s734HmYU78MVzZ++joYM+NkJusItbdRcbm+AGRgJCt3iA+yux0QpD9cBVdz3tKyrjVYWRl7j0mHSmv4lhV0aoA==} engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.12.0 - '@babel/types@7.27.1': - resolution: {integrity: sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==} + '@babel/plugin-transform-classes@7.28.0': + resolution: {integrity: sha512-IjM1IoJNw72AZFlj33Cu8X0q2XK/6AaVC3jQu+cgQ5lThWD5ajnuUAml80dqRmOhmPkTH8uAwnpMu9Rvj0LTRA==} engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 - '@babel/types@7.27.3': - resolution: {integrity: sha512-Y1GkI4ktrtvmawoSq+4FCVHNryea6uR+qUQy0AGxLSsjCX0nVmkYQMBLHDkXZuo5hGx7eYdnIaslsdBFm7zbUw==} + '@babel/plugin-transform-computed-properties@7.27.1': + resolution: {integrity: sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw==} engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 - '@dnd-kit/accessibility@3.1.1': - resolution: {integrity: sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==} + '@babel/plugin-transform-destructuring@7.28.0': + resolution: {integrity: sha512-v1nrSMBiKcodhsyJ4Gf+Z0U/yawmJDBOTpEB3mcQY52r9RIyPneGyAS/yM6seP/8I+mWI3elOMtT5dB8GJVs+A==} + engines: {node: '>=6.9.0'} peerDependencies: - react: '>=16.8.0' + '@babel/core': ^7.0.0-0 - '@dnd-kit/core@6.3.1': - resolution: {integrity: sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==} + '@babel/plugin-transform-dotall-regex@7.27.1': + resolution: {integrity: sha512-gEbkDVGRvjj7+T1ivxrfgygpT7GUd4vmODtYpbs0gZATdkX8/iSnOtZSxiZnsgm1YjTgjI6VKBGSJJevkrclzw==} + engines: {node: '>=6.9.0'} peerDependencies: - react: '>=16.8.0' - react-dom: '>=16.8.0' + '@babel/core': ^7.0.0-0 - '@dnd-kit/sortable@10.0.0': - resolution: {integrity: sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==} + '@babel/plugin-transform-duplicate-keys@7.27.1': + resolution: {integrity: sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==} + engines: {node: '>=6.9.0'} peerDependencies: - '@dnd-kit/core': ^6.3.0 - react: '>=16.8.0' + '@babel/core': ^7.0.0-0 - '@dnd-kit/utilities@3.2.2': - resolution: {integrity: sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==} + '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.27.1': + resolution: {integrity: sha512-hkGcueTEzuhB30B3eJCbCYeCaaEQOmQR0AdvzpD4LoN0GXMWzzGSuRrxR2xTnCrvNbVwK9N6/jQ92GSLfiZWoQ==} + engines: {node: '>=6.9.0'} peerDependencies: - react: '>=16.8.0' + '@babel/core': ^7.0.0 - '@emotion/babel-plugin@11.13.5': - resolution: {integrity: sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==} + '@babel/plugin-transform-dynamic-import@7.27.1': + resolution: {integrity: sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 - '@emotion/cache@11.14.0': - resolution: {integrity: sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==} + '@babel/plugin-transform-explicit-resource-management@7.28.0': + resolution: {integrity: sha512-K8nhUcn3f6iB+P3gwCv/no7OdzOZQcKchW6N389V6PD8NUWKZHzndOd9sPDVbMoBsbmjMqlB4L9fm+fEFNVlwQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 - '@emotion/hash@0.9.2': - resolution: {integrity: sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==} + '@babel/plugin-transform-exponentiation-operator@7.27.1': + resolution: {integrity: sha512-uspvXnhHvGKf2r4VVtBpeFnuDWsJLQ6MF6lGJLC89jBR1uoVeqM416AZtTuhTezOfgHicpJQmoD5YUakO/YmXQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 - '@emotion/is-prop-valid@1.3.1': - resolution: {integrity: sha512-/ACwoqx7XQi9knQs/G0qKvv5teDMhD7bXYns9N/wM8ah8iNb8jZ2uNO0YOgiq2o2poIvVtJS2YALasQuMSQ7Kw==} + '@babel/plugin-transform-export-namespace-from@7.27.1': + resolution: {integrity: sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 - '@emotion/memoize@0.9.0': - resolution: {integrity: sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==} + '@babel/plugin-transform-for-of@7.27.1': + resolution: {integrity: sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 - '@emotion/react@11.14.0': - resolution: {integrity: sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==} + '@babel/plugin-transform-function-name@7.27.1': + resolution: {integrity: sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==} + engines: {node: '>=6.9.0'} peerDependencies: - '@types/react': '*' - react: '>=16.8.0' - peerDependenciesMeta: - '@types/react': - optional: true + '@babel/core': ^7.0.0-0 - '@emotion/serialize@1.3.3': - resolution: {integrity: sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==} + '@babel/plugin-transform-json-strings@7.27.1': + resolution: {integrity: sha512-6WVLVJiTjqcQauBhn1LkICsR2H+zm62I3h9faTDKt1qP4jn2o72tSvqMwtGFKGTpojce0gJs+76eZ2uCHRZh0Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 - '@emotion/sheet@1.4.0': - resolution: {integrity: sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==} + '@babel/plugin-transform-literals@7.27.1': + resolution: {integrity: sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 - '@emotion/styled@11.14.0': - resolution: {integrity: sha512-XxfOnXFffatap2IyCeJyNov3kiDQWoR08gPUQxvbL7fxKryGBKUZUkG6Hz48DZwVrJSVh9sJboyV1Ds4OW6SgA==} + '@babel/plugin-transform-logical-assignment-operators@7.27.1': + resolution: {integrity: sha512-SJvDs5dXxiae4FbSL1aBJlG4wvl594N6YEVVn9e3JGulwioy6z3oPjx/sQBO3Y4NwUu5HNix6KJ3wBZoewcdbw==} + engines: {node: '>=6.9.0'} peerDependencies: - '@emotion/react': ^11.0.0-rc.0 - '@types/react': '*' - react: '>=16.8.0' - peerDependenciesMeta: - '@types/react': - optional: true + '@babel/core': ^7.0.0-0 - '@emotion/unitless@0.10.0': - resolution: {integrity: sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==} + '@babel/plugin-transform-member-expression-literals@7.27.1': + resolution: {integrity: sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 - '@emotion/use-insertion-effect-with-fallbacks@1.2.0': - resolution: {integrity: sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==} + '@babel/plugin-transform-modules-amd@7.27.1': + resolution: {integrity: sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==} + engines: {node: '>=6.9.0'} peerDependencies: - react: '>=16.8.0' + '@babel/core': ^7.0.0-0 - '@emotion/utils@1.4.2': - resolution: {integrity: sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==} + '@babel/plugin-transform-modules-commonjs@7.27.1': + resolution: {integrity: sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 - '@emotion/weak-memoize@0.4.0': - resolution: {integrity: sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==} + '@babel/plugin-transform-modules-systemjs@7.27.1': + resolution: {integrity: sha512-w5N1XzsRbc0PQStASMksmUeqECuzKuTJer7kFagK8AXgpCMkeDMO5S+aaFb7A51ZYDF7XI34qsTX+fkHiIm5yA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 - '@esbuild/aix-ppc64@0.25.4': - resolution: {integrity: sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] + '@babel/plugin-transform-modules-umd@7.27.1': + resolution: {integrity: sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 - '@esbuild/aix-ppc64@0.25.5': - resolution: {integrity: sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] + '@babel/plugin-transform-named-capturing-groups-regex@7.27.1': + resolution: {integrity: sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 - '@esbuild/android-arm64@0.25.4': - resolution: {integrity: sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==} - engines: {node: '>=18'} - cpu: [arm64] - os: [android] + '@babel/plugin-transform-new-target@7.27.1': + resolution: {integrity: sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 - '@esbuild/android-arm64@0.25.5': - resolution: {integrity: sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [android] + '@babel/plugin-transform-nullish-coalescing-operator@7.27.1': + resolution: {integrity: sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 - '@esbuild/android-arm@0.25.4': - resolution: {integrity: sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==} - engines: {node: '>=18'} - cpu: [arm] - os: [android] + '@babel/plugin-transform-numeric-separator@7.27.1': + resolution: {integrity: sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 - '@esbuild/android-arm@0.25.5': - resolution: {integrity: sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==} - engines: {node: '>=18'} - cpu: [arm] - os: [android] + '@babel/plugin-transform-object-rest-spread@7.28.0': + resolution: {integrity: sha512-9VNGikXxzu5eCiQjdE4IZn8sb9q7Xsk5EXLDBKUYg1e/Tve8/05+KJEtcxGxAgCY5t/BpKQM+JEL/yT4tvgiUA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 - '@esbuild/android-x64@0.25.4': - resolution: {integrity: sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [android] + '@babel/plugin-transform-object-super@7.27.1': + resolution: {integrity: sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 - '@esbuild/android-x64@0.25.5': - resolution: {integrity: sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==} - engines: {node: '>=18'} - cpu: [x64] - os: [android] + '@babel/plugin-transform-optional-catch-binding@7.27.1': + resolution: {integrity: sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 - '@esbuild/darwin-arm64@0.25.4': - resolution: {integrity: sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==} - engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] + '@babel/plugin-transform-optional-chaining@7.27.1': + resolution: {integrity: sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-parameters@7.27.7': + resolution: {integrity: sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-private-methods@7.27.1': + resolution: {integrity: sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-private-property-in-object@7.27.1': + resolution: {integrity: sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-property-literals@7.27.1': + resolution: {integrity: sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-self@7.27.1': + resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.27.1': + resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-regenerator@7.28.0': + resolution: {integrity: sha512-LOAozRVbqxEVjSKfhGnuLoE4Kz4Oc5UJzuvFUhSsQzdCdaAQu06mG8zDv2GFSerM62nImUZ7K92vxnQcLSDlCQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-regexp-modifiers@7.27.1': + resolution: {integrity: sha512-TtEciroaiODtXvLZv4rmfMhkCv8jx3wgKpL68PuiPh2M4fvz5jhsA7697N1gMvkvr/JTF13DrFYyEbY9U7cVPA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-reserved-words@7.27.1': + resolution: {integrity: sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-shorthand-properties@7.27.1': + resolution: {integrity: sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-spread@7.27.1': + resolution: {integrity: sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-sticky-regex@7.27.1': + resolution: {integrity: sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-template-literals@7.27.1': + resolution: {integrity: sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-typeof-symbol@7.27.1': + resolution: {integrity: sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-escapes@7.27.1': + resolution: {integrity: sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-property-regex@7.27.1': + resolution: {integrity: sha512-uW20S39PnaTImxp39O5qFlHLS9LJEmANjMG7SxIhap8rCHqu0Ik+tLEPX5DKmHn6CsWQ7j3lix2tFOa5YtL12Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-regex@7.27.1': + resolution: {integrity: sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-sets-regex@7.27.1': + resolution: {integrity: sha512-EtkOujbc4cgvb0mlpQefi4NTPBzhSIevblFevACNLUspmrALgmEBdL/XfnyyITfd8fKBZrZys92zOWcik7j9Tw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/preset-env@7.28.0': + resolution: {integrity: sha512-VmaxeGOwuDqzLl5JUkIRM1X2Qu2uKGxHEQWh+cvvbl7JuJRgKGJSfsEF/bUaxFhJl/XAyxBe7q7qSuTbKFuCyg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/preset-modules@0.1.6-no-external-plugins': + resolution: {integrity: sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==} + peerDependencies: + '@babel/core': ^7.0.0-0 || ^8.0.0-0 <8.0.0 + + '@babel/runtime@7.27.1': + resolution: {integrity: sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.27.2': + resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.27.1': + resolution: {integrity: sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.27.4': + resolution: {integrity: sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.28.0': + resolution: {integrity: sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.27.1': + resolution: {integrity: sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.27.3': + resolution: {integrity: sha512-Y1GkI4ktrtvmawoSq+4FCVHNryea6uR+qUQy0AGxLSsjCX0nVmkYQMBLHDkXZuo5hGx7eYdnIaslsdBFm7zbUw==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.28.0': + resolution: {integrity: sha512-jYnje+JyZG5YThjHiF28oT4SIZLnYOcSBb6+SDaFIyzDVSkXQmQQYclJ2R+YxcdmK0AX6x1E5OQNtuh3jHDrUg==} + engines: {node: '>=6.9.0'} + + '@dnd-kit/accessibility@3.1.1': + resolution: {integrity: sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==} + peerDependencies: + react: '>=16.8.0' + + '@dnd-kit/core@6.3.1': + resolution: {integrity: sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@dnd-kit/sortable@10.0.0': + resolution: {integrity: sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==} + peerDependencies: + '@dnd-kit/core': ^6.3.0 + react: '>=16.8.0' + + '@dnd-kit/utilities@3.2.2': + resolution: {integrity: sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==} + peerDependencies: + react: '>=16.8.0' + + '@emotion/babel-plugin@11.13.5': + resolution: {integrity: sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==} + + '@emotion/cache@11.14.0': + resolution: {integrity: sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==} + + '@emotion/hash@0.9.2': + resolution: {integrity: sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==} + + '@emotion/is-prop-valid@1.3.1': + resolution: {integrity: sha512-/ACwoqx7XQi9knQs/G0qKvv5teDMhD7bXYns9N/wM8ah8iNb8jZ2uNO0YOgiq2o2poIvVtJS2YALasQuMSQ7Kw==} + + '@emotion/memoize@0.9.0': + resolution: {integrity: sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==} + + '@emotion/react@11.14.0': + resolution: {integrity: sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==} + peerDependencies: + '@types/react': '*' + react: '>=16.8.0' + peerDependenciesMeta: + '@types/react': + optional: true + + '@emotion/serialize@1.3.3': + resolution: {integrity: sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==} + + '@emotion/sheet@1.4.0': + resolution: {integrity: sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==} + + '@emotion/styled@11.14.0': + resolution: {integrity: sha512-XxfOnXFffatap2IyCeJyNov3kiDQWoR08gPUQxvbL7fxKryGBKUZUkG6Hz48DZwVrJSVh9sJboyV1Ds4OW6SgA==} + peerDependencies: + '@emotion/react': ^11.0.0-rc.0 + '@types/react': '*' + react: '>=16.8.0' + peerDependenciesMeta: + '@types/react': + optional: true + + '@emotion/unitless@0.10.0': + resolution: {integrity: sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==} + + '@emotion/use-insertion-effect-with-fallbacks@1.2.0': + resolution: {integrity: sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==} + peerDependencies: + react: '>=16.8.0' + + '@emotion/utils@1.4.2': + resolution: {integrity: sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==} + + '@emotion/weak-memoize@0.4.0': + resolution: {integrity: sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==} + + '@esbuild/aix-ppc64@0.25.4': + resolution: {integrity: sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.25.5': + resolution: {integrity: sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.4': + resolution: {integrity: sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.25.5': + resolution: {integrity: sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.4': + resolution: {integrity: sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.25.5': + resolution: {integrity: sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.4': + resolution: {integrity: sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.25.5': + resolution: {integrity: sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.4': + resolution: {integrity: sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] '@esbuild/darwin-arm64@0.25.5': resolution: {integrity: sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==} @@ -754,6 +1174,9 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} + '@jridgewell/gen-mapping@0.3.12': + resolution: {integrity: sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==} + '@jridgewell/gen-mapping@0.3.8': resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} engines: {node: '>=6.0.0'} @@ -766,12 +1189,18 @@ packages: resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} engines: {node: '>=6.0.0'} + '@jridgewell/source-map@0.3.10': + resolution: {integrity: sha512-0pPkgz9dY+bijgistcTTJ5mR+ocqRXLuhXHYdzoMmmoJ2C9S46RCm2GMUbatPEUK9Yjy26IrAy8D/M00lLkv+Q==} + '@jridgewell/sourcemap-codec@1.5.0': resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + '@jridgewell/trace-mapping@0.3.29': + resolution: {integrity: sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==} + '@lucide/lab@0.1.2': resolution: {integrity: sha512-VprF2BJa7ZuTGOhUd5cf8tHJXyL63wdxcGieAiVVoR9hO0YmPsnZO0AGqDiX2/br+/MC6n8BoJcmPilltOXIJA==} @@ -955,17 +1384,57 @@ packages: '@rolldown/pluginutils@1.0.0-beta.9': resolution: {integrity: sha512-e9MeMtVWo186sgvFFJOPGy7/d2j2mZhLJIdVW0C/xDluuOvymEATqz6zKsP0ZmXGzQtqlyjz5sC1sYQUoJG98w==} - '@rollup/pluginutils@5.1.4': - resolution: {integrity: sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==} + '@rollup/plugin-babel@5.3.1': + resolution: {integrity: sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==} + engines: {node: '>= 10.0.0'} + peerDependencies: + '@babel/core': ^7.0.0 + '@types/babel__core': ^7.1.9 + rollup: ^1.20.0||^2.0.0 + peerDependenciesMeta: + '@types/babel__core': + optional: true + + '@rollup/plugin-node-resolve@15.3.1': + resolution: {integrity: sha512-tgg6b91pAybXHJQMAAwW9VuWBO6Thi+q7BCNARLwSqlmsHz0XYURtGvh/AuwSADXSI4h/2uHbs7s4FzlZDGSGA==} engines: {node: '>=14.0.0'} peerDependencies: - rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + rollup: ^2.78.0||^3.0.0||^4.0.0 peerDependenciesMeta: rollup: optional: true - '@rollup/rollup-android-arm-eabi@4.40.2': - resolution: {integrity: sha512-JkdNEq+DFxZfUwxvB58tHMHBHVgX23ew41g1OQinthJ+ryhdRk67O31S7sYw8u2lTjHUPFxwar07BBt1KHp/hg==} + '@rollup/plugin-replace@2.4.2': + resolution: {integrity: sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg==} + peerDependencies: + rollup: ^1.20.0 || ^2.0.0 + + '@rollup/plugin-terser@0.4.4': + resolution: {integrity: sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/pluginutils@3.1.0': + resolution: {integrity: sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==} + engines: {node: '>= 8.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0 + + '@rollup/pluginutils@5.1.4': + resolution: {integrity: sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/rollup-android-arm-eabi@4.40.2': + resolution: {integrity: sha512-JkdNEq+DFxZfUwxvB58tHMHBHVgX23ew41g1OQinthJ+ryhdRk67O31S7sYw8u2lTjHUPFxwar07BBt1KHp/hg==} cpu: [arm] os: [android] @@ -1067,6 +1536,9 @@ packages: '@socket.io/component-emitter@3.1.2': resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} + '@surma/rollup-plugin-off-main-thread@2.2.3': + resolution: {integrity: sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==} + '@svgr/babel-plugin-add-jsx-attribute@8.0.0': resolution: {integrity: sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==} engines: {node: '>=14'} @@ -1321,6 +1793,9 @@ packages: '@types/diff@5.2.3': resolution: {integrity: sha512-K0Oqlrq3kQMaO2RhfrNQX5trmt+XLyom88zS0u84nnIcLvFnRUMRRHmrGny5GSM+kNO9IZLARsdQHDzkhAgmrQ==} + '@types/estree@0.0.39': + resolution: {integrity: sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==} + '@types/estree@1.0.7': resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==} @@ -1352,6 +1827,12 @@ packages: '@types/react@18.3.23': resolution: {integrity: sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==} + '@types/resolve@1.20.2': + resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} + + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + '@typescript-eslint/eslint-plugin@8.33.0': resolution: {integrity: sha512-CACyQuqSHt7ma3Ns601xykeBK/rDeZa3w6IS6UtMQbixO5DWy+8TilKkviGDH6jtWCo8FGRKEK5cLLkPvEammQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1466,6 +1947,9 @@ packages: ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + ajv@8.17.1: + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -1517,6 +2001,13 @@ packages: resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} engines: {node: '>= 0.4'} + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + + at-least-node@1.0.0: + resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} + engines: {node: '>= 4.0.0'} + available-typed-arrays@1.0.7: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} @@ -1528,6 +2019,21 @@ packages: resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} engines: {node: '>=10', npm: '>=6'} + babel-plugin-polyfill-corejs2@0.4.14: + resolution: {integrity: sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + babel-plugin-polyfill-corejs3@0.13.0: + resolution: {integrity: sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + babel-plugin-polyfill-regenerator@0.6.5: + resolution: {integrity: sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + babel-plugin-react-compiler@19.1.0-rc.2: resolution: {integrity: sha512-kSNA//p5fMO6ypG8EkEVPIqAjwIXm5tMjfD1XRPL/sRjYSbJ6UsvORfaeolNWnZ9n310aM0xJP7peW26BuCVzA==} @@ -1553,6 +2059,14 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + browserslist@4.25.1: + resolution: {integrity: sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -1576,6 +2090,9 @@ packages: caniuse-lite@1.0.30001717: resolution: {integrity: sha512-auPpttCq6BDEG8ZAuHJIplGw6GODhjw+/11e7IjpnYCxZcW/ONgPs0KVBJ0d1bY3e2+7PRe5RCLyP+PfwVgkYw==} + caniuse-lite@1.0.30001727: + resolution: {integrity: sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -1603,6 +2120,13 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + + common-tags@1.8.2: + resolution: {integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==} + engines: {node: '>=4.0.0'} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -1612,6 +2136,9 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + core-js-compat@3.44.0: + resolution: {integrity: sha512-JepmAj2zfl6ogy34qfWtcE7nHKAJnKsQFRn++scjVS2bZFllwptzw61BZcZFYBPpUznLfAvh0LGhxKppk04ClA==} + cosmiconfig@7.1.0: resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} engines: {node: '>=10'} @@ -1629,6 +2156,10 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + crypto-random-string@2.0.0: + resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==} + engines: {node: '>=8'} + csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} @@ -1674,6 +2205,10 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + define-data-property@1.1.4: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} engines: {node: '>= 0.4'} @@ -1709,9 +2244,17 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} + ejs@3.1.10: + resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==} + engines: {node: '>=0.10.0'} + hasBin: true + electron-to-chromium@1.5.151: resolution: {integrity: sha512-Rl6uugut2l9sLojjS4H4SAr3A4IgACMLgpuEMPYCVcKydzfyPrn5absNRju38IhQOf/NwjJY8OGWjlteqYeBCA==} + electron-to-chromium@1.5.181: + resolution: {integrity: sha512-+ISMj8OIQ+0qEeDj14Rt8WwcTOiqHyAB+5bnK1K7xNNLjBJ4hRCQfUkw8RWtcLbfBzDwc15ZnKH0c7SNOfwiyA==} + emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -1845,6 +2388,9 @@ packages: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} + estree-walker@1.0.1: + resolution: {integrity: sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==} + estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} @@ -1865,6 +2411,9 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-uri@3.0.6: + resolution: {integrity: sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==} + fastq@1.19.1: resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} @@ -1880,6 +2429,9 @@ packages: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} + filelist@1.0.4: + resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==} + fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} @@ -1902,6 +2454,13 @@ packages: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} + fs-extra@9.1.0: + resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} + engines: {node: '>=10'} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -1929,6 +2488,9 @@ packages: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} + get-own-enumerable-property-symbols@3.0.2: + resolution: {integrity: sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==} + get-proto@1.0.1: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} @@ -1948,6 +2510,10 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported + globals@11.12.0: resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} engines: {node: '>=4'} @@ -1971,6 +2537,9 @@ packages: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} @@ -2010,6 +2579,9 @@ packages: hoist-non-react-statics@3.3.2: resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + idb@7.1.1: + resolution: {integrity: sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -2029,6 +2601,13 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + internal-slot@1.1.0: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} @@ -2096,6 +2675,9 @@ packages: resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} engines: {node: '>= 0.4'} + is-module@1.0.0: + resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==} + is-number-object@1.1.1: resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} engines: {node: '>= 0.4'} @@ -2104,10 +2686,18 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-obj@1.0.1: + resolution: {integrity: sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==} + engines: {node: '>=0.10.0'} + is-regex@1.2.1: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} + is-regexp@1.0.0: + resolution: {integrity: sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==} + engines: {node: '>=0.10.0'} + is-set@2.0.3: resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} engines: {node: '>= 0.4'} @@ -2116,6 +2706,10 @@ packages: resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} engines: {node: '>= 0.4'} + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + is-string@1.1.1: resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} engines: {node: '>= 0.4'} @@ -2150,6 +2744,11 @@ packages: resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} engines: {node: '>= 0.4'} + jake@10.9.2: + resolution: {integrity: sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==} + engines: {node: '>=10'} + hasBin: true + jiti@1.21.7: resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} hasBin: true @@ -2161,6 +2760,11 @@ packages: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true + jsesc@3.0.2: + resolution: {integrity: sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==} + engines: {node: '>=6'} + hasBin: true + jsesc@3.1.0: resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} engines: {node: '>=6'} @@ -2175,6 +2779,12 @@ packages: json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-schema@0.4.0: + resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} @@ -2183,6 +2793,13 @@ packages: engines: {node: '>=6'} hasBin: true + jsonfile@6.1.0: + resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + + jsonpointer@5.0.1: + resolution: {integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==} + engines: {node: '>=0.10.0'} + jsx-ast-utils@3.3.5: resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} engines: {node: '>=4.0'} @@ -2190,6 +2807,10 @@ packages: keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + leven@3.1.0: + resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} + engines: {node: '>=6'} + levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} @@ -2201,9 +2822,18 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + lodash.debounce@4.0.8: + resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} + lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lodash.sortby@4.7.0: + resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true @@ -2219,6 +2849,9 @@ packages: peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + magic-string@0.25.9: + resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==} + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -2237,6 +2870,10 @@ packages: minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + minimatch@5.1.6: + resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} + engines: {node: '>=10'} + minimatch@9.0.5: resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} engines: {node: '>=16 || 14 >=14.17'} @@ -2293,6 +2930,9 @@ packages: resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} engines: {node: '>= 0.4'} + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -2321,6 +2961,10 @@ packages: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} @@ -2360,6 +3004,14 @@ packages: engines: {node: '>=14'} hasBin: true + pretty-bytes@5.6.0: + resolution: {integrity: sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==} + engines: {node: '>=6'} + + pretty-bytes@6.1.1: + resolution: {integrity: sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==} + engines: {node: ^14.13.1 || >=16.0.0} + prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} @@ -2370,6 +3022,9 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + randombytes@2.1.0: + resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} + react-dom@19.1.0: resolution: {integrity: sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==} peerDependencies: @@ -2425,14 +3080,36 @@ packages: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} + regenerate-unicode-properties@10.2.0: + resolution: {integrity: sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==} + engines: {node: '>=4'} + + regenerate@1.4.2: + resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==} + regexp.prototype.flags@1.5.4: resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} engines: {node: '>= 0.4'} + regexpu-core@6.2.0: + resolution: {integrity: sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==} + engines: {node: '>=4'} + + regjsgen@0.8.0: + resolution: {integrity: sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==} + + regjsparser@0.12.0: + resolution: {integrity: sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==} + hasBin: true + require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -2453,6 +3130,11 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + rollup@2.79.2: + resolution: {integrity: sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==} + engines: {node: '>=10.0.0'} + hasBin: true + rollup@4.40.2: resolution: {integrity: sha512-tfUOg6DTP4rhQ3VjOO6B4wyrJnGOX85requAXvqYTHsOgb2TFJdZ3aWpT8W2kPoypSGP7dZUyzxJ9ee4buM5Fg==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -2465,6 +3147,9 @@ packages: resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} engines: {node: '>=0.4'} + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + safe-push-apply@1.0.0: resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} engines: {node: '>= 0.4'} @@ -2495,6 +3180,9 @@ packages: engines: {node: '>=10'} hasBin: true + serialize-javascript@6.0.2: + resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -2531,6 +3219,9 @@ packages: resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} engines: {node: '>= 0.4'} + smob@1.5.0: + resolution: {integrity: sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==} + snake-case@3.0.4: resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==} @@ -2546,10 +3237,25 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + source-map@0.5.7: resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} engines: {node: '>=0.10.0'} + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + source-map@0.8.0-beta.0: + resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} + engines: {node: '>= 8'} + + sourcemap-codec@1.4.8: + resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} + deprecated: Please use @jridgewell/sourcemap-codec instead + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -2573,10 +3279,18 @@ packages: resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} engines: {node: '>= 0.4'} + stringify-object@3.3.0: + resolution: {integrity: sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==} + engines: {node: '>=4'} + strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} + strip-comments@2.0.1: + resolution: {integrity: sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==} + engines: {node: '>=10'} + strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} @@ -2595,6 +3309,19 @@ packages: svg-parser@2.0.4: resolution: {integrity: sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==} + temp-dir@2.0.0: + resolution: {integrity: sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==} + engines: {node: '>=8'} + + tempy@0.6.0: + resolution: {integrity: sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw==} + engines: {node: '>=10'} + + terser@5.43.1: + resolution: {integrity: sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==} + engines: {node: '>=10'} + hasBin: true + tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} @@ -2609,6 +3336,9 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + tr46@1.0.1: + resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} + ts-api-utils@2.1.0: resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} engines: {node: '>=18.12'} @@ -2637,6 +3367,10 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} + type-fest@0.16.0: + resolution: {integrity: sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==} + engines: {node: '>=10'} + typed-array-buffer@1.0.3: resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} engines: {node: '>= 0.4'} @@ -2672,10 +3406,38 @@ packages: undici-types@7.8.0: resolution: {integrity: sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==} + unicode-canonical-property-names-ecmascript@2.0.1: + resolution: {integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==} + engines: {node: '>=4'} + + unicode-match-property-ecmascript@2.0.0: + resolution: {integrity: sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==} + engines: {node: '>=4'} + + unicode-match-property-value-ecmascript@2.2.0: + resolution: {integrity: sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==} + engines: {node: '>=4'} + + unicode-property-aliases-ecmascript@2.1.0: + resolution: {integrity: sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==} + engines: {node: '>=4'} + + unique-string@2.0.0: + resolution: {integrity: sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==} + engines: {node: '>=8'} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + unplugin@2.3.5: resolution: {integrity: sha512-RyWSb5AHmGtjjNQ6gIlA67sHOsWpsbWpwDokLwTcejVdOjEkJZh7QKu14J00gDDVSh8kGH4KYC/TNBceXFZhtw==} engines: {node: '>=18.12.0'} + upath@1.2.0: + resolution: {integrity: sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==} + engines: {node: '>=4'} + update-browserslist-db@1.1.3: resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} hasBin: true @@ -2690,6 +3452,18 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + vite-plugin-pwa@1.0.1: + resolution: {integrity: sha512-STyUomQbydj7vGamtgQYIJI0YsUZ3T4pJLGBQDQPhzMse6aGSncmEN21OV35PrFsmCvmtiH+Nu1JS1ke4RqBjQ==} + engines: {node: '>=16.0.0'} + peerDependencies: + '@vite-pwa/assets-generator': ^1.0.0 + vite: ^3.1.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + workbox-build: ^7.3.0 + workbox-window: ^7.3.0 + peerDependenciesMeta: + '@vite-pwa/assets-generator': + optional: true + vite-plugin-svgr@4.3.0: resolution: {integrity: sha512-Jy9qLB2/PyWklpYy0xk0UU3TlU0t2UMpJXZvf+hWII1lAmRHrOUKi11Uw8N3rxoNk7atZNYO3pR3vI1f7oi+6w==} peerDependencies: @@ -2746,9 +3520,15 @@ packages: wavesurfer.js@7.9.5: resolution: {integrity: sha512-ioOG9chuAn0bF2NYYKkZtaxjcQK/hFskLg8ViLYbJHhWPk1N5wWtuqVhqeh2ZWT2SK3t0E8UkD7lLDLuZQQaSA==} + webidl-conversions@4.0.2: + resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} + webpack-virtual-modules@0.6.2: resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + whatwg-url@7.1.0: + resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} + which-boxed-primitive@1.1.1: resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} engines: {node: '>= 0.4'} @@ -2774,10 +3554,62 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + workbox-background-sync@7.3.0: + resolution: {integrity: sha512-PCSk3eK7Mxeuyatb22pcSx9dlgWNv3+M8PqPaYDokks8Y5/FX4soaOqj3yhAZr5k6Q5JWTOMYgaJBpbw11G9Eg==} + + workbox-broadcast-update@7.3.0: + resolution: {integrity: sha512-T9/F5VEdJVhwmrIAE+E/kq5at2OY6+OXXgOWQevnubal6sO92Gjo24v6dCVwQiclAF5NS3hlmsifRrpQzZCdUA==} + + workbox-build@7.3.0: + resolution: {integrity: sha512-JGL6vZTPlxnlqZRhR/K/msqg3wKP+m0wfEUVosK7gsYzSgeIxvZLi1ViJJzVL7CEeI8r7rGFV973RiEqkP3lWQ==} + engines: {node: '>=16.0.0'} + + workbox-cacheable-response@7.3.0: + resolution: {integrity: sha512-eAFERIg6J2LuyELhLlmeRcJFa5e16Mj8kL2yCDbhWE+HUun9skRQrGIFVUagqWj4DMaaPSMWfAolM7XZZxNmxA==} + + workbox-core@7.3.0: + resolution: {integrity: sha512-Z+mYrErfh4t3zi7NVTvOuACB0A/jA3bgxUN3PwtAVHvfEsZxV9Iju580VEETug3zYJRc0Dmii/aixI/Uxj8fmw==} + + workbox-expiration@7.3.0: + resolution: {integrity: sha512-lpnSSLp2BM+K6bgFCWc5bS1LR5pAwDWbcKt1iL87/eTSJRdLdAwGQznZE+1czLgn/X05YChsrEegTNxjM067vQ==} + + workbox-google-analytics@7.3.0: + resolution: {integrity: sha512-ii/tSfFdhjLHZ2BrYgFNTrb/yk04pw2hasgbM70jpZfLk0vdJAXgaiMAWsoE+wfJDNWoZmBYY0hMVI0v5wWDbg==} + + workbox-navigation-preload@7.3.0: + resolution: {integrity: sha512-fTJzogmFaTv4bShZ6aA7Bfj4Cewaq5rp30qcxl2iYM45YD79rKIhvzNHiFj1P+u5ZZldroqhASXwwoyusnr2cg==} + + workbox-precaching@7.3.0: + resolution: {integrity: sha512-ckp/3t0msgXclVAYaNndAGeAoWQUv7Rwc4fdhWL69CCAb2UHo3Cef0KIUctqfQj1p8h6aGyz3w8Cy3Ihq9OmIw==} + + workbox-range-requests@7.3.0: + resolution: {integrity: sha512-EyFmM1KpDzzAouNF3+EWa15yDEenwxoeXu9bgxOEYnFfCxns7eAxA9WSSaVd8kujFFt3eIbShNqa4hLQNFvmVQ==} + + workbox-recipes@7.3.0: + resolution: {integrity: sha512-BJro/MpuW35I/zjZQBcoxsctgeB+kyb2JAP5EB3EYzePg8wDGoQuUdyYQS+CheTb+GhqJeWmVs3QxLI8EBP1sg==} + + workbox-routing@7.3.0: + resolution: {integrity: sha512-ZUlysUVn5ZUzMOmQN3bqu+gK98vNfgX/gSTZ127izJg/pMMy4LryAthnYtjuqcjkN4HEAx1mdgxNiKJMZQM76A==} + + workbox-strategies@7.3.0: + resolution: {integrity: sha512-tmZydug+qzDFATwX7QiEL5Hdf7FrkhjaF9db1CbB39sDmEZJg3l9ayDvPxy8Y18C3Y66Nrr9kkN1f/RlkDgllg==} + + workbox-streams@7.3.0: + resolution: {integrity: sha512-SZnXucyg8x2Y61VGtDjKPO5EgPUG5NDn/v86WYHX+9ZqvAsGOytP0Jxp1bl663YUuMoXSAtsGLL+byHzEuMRpw==} + + workbox-sw@7.3.0: + resolution: {integrity: sha512-aCUyoAZU9IZtH05mn0ACUpyHzPs0lMeJimAYkQkBsOWiqaJLgusfDCR+yllkPkFRxWpZKF8vSvgHYeG7LwhlmA==} + + workbox-window@7.3.0: + resolution: {integrity: sha512-qW8PDy16OV1UBaUNGlTVcepzrlzyzNW/ZJvFQQs2j2TzGsg6IKjcpZC1RSquqQnTOafl5pCj5bGfAHlCjOOjdA==} + wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + ws@8.17.1: resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==} engines: {node: '>=10.0.0'} @@ -2836,6 +3668,13 @@ snapshots: '@jridgewell/gen-mapping': 0.3.8 '@jridgewell/trace-mapping': 0.3.25 + '@apideck/better-ajv-errors@0.3.6(ajv@8.17.1)': + dependencies: + ajv: 8.17.1 + json-schema: 0.4.0 + jsonpointer: 5.0.1 + leven: 3.1.0 + '@babel/code-frame@7.27.1': dependencies: '@babel/helper-validator-identifier': 7.27.1 @@ -2844,6 +3683,8 @@ snapshots: '@babel/compat-data@7.27.2': {} + '@babel/compat-data@7.28.0': {} + '@babel/core@7.27.1': dependencies: '@ampproject/remapping': 2.3.0 @@ -2900,6 +3741,14 @@ snapshots: '@jridgewell/trace-mapping': 0.3.25 jsesc: 3.1.0 + '@babel/generator@7.28.0': + dependencies: + '@babel/parser': 7.28.0 + '@babel/types': 7.28.0 + '@jridgewell/gen-mapping': 0.3.12 + '@jridgewell/trace-mapping': 0.3.29 + jsesc: 3.1.0 + '@babel/helper-annotate-as-pure@7.27.3': dependencies: '@babel/types': 7.27.3 @@ -2925,6 +3774,39 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/helper-create-class-features-plugin@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-member-expression-to-functions': 7.27.1 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/helper-replace-supers': 7.27.1(@babel/core@7.27.4) + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/traverse': 7.27.1 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-create-regexp-features-plugin@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-annotate-as-pure': 7.27.3 + regexpu-core: 6.2.0 + semver: 6.3.1 + + '@babel/helper-define-polyfill-provider@0.6.5(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-plugin-utils': 7.27.1 + debug: 4.4.1 + lodash.debounce: 4.0.8 + resolve: 1.22.10 + transitivePeerDependencies: + - supports-color + + '@babel/helper-globals@7.28.0': {} + '@babel/helper-member-expression-to-functions@7.27.1': dependencies: '@babel/traverse': 7.27.1 @@ -2963,6 +3845,15 @@ snapshots: '@babel/helper-plugin-utils@7.27.1': {} + '@babel/helper-remap-async-to-generator@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-wrap-function': 7.27.1 + '@babel/traverse': 7.28.0 + transitivePeerDependencies: + - supports-color + '@babel/helper-replace-supers@7.27.1(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 @@ -2972,6 +3863,15 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/helper-replace-supers@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-member-expression-to-functions': 7.27.1 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/traverse': 7.27.1 + transitivePeerDependencies: + - supports-color + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': dependencies: '@babel/traverse': 7.27.1 @@ -2985,51 +3885,534 @@ snapshots: '@babel/helper-validator-option@7.27.1': {} + '@babel/helper-wrap-function@7.27.1': + dependencies: + '@babel/template': 7.27.2 + '@babel/traverse': 7.28.0 + '@babel/types': 7.28.0 + transitivePeerDependencies: + - supports-color + '@babel/helpers@7.27.1': dependencies: '@babel/template': 7.27.2 '@babel/types': 7.27.3 - '@babel/helpers@7.27.4': + '@babel/helpers@7.27.4': + dependencies: + '@babel/template': 7.27.2 + '@babel/types': 7.27.3 + + '@babel/parser@7.27.2': + dependencies: + '@babel/types': 7.27.1 + + '@babel/parser@7.27.4': + dependencies: + '@babel/types': 7.27.3 + + '@babel/parser@7.28.0': + dependencies: + '@babel/types': 7.28.0 + + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/traverse': 7.28.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/plugin-transform-optional-chaining': 7.27.1(@babel/core@7.27.4) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/traverse': 7.28.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + + '@babel/plugin-syntax-import-assertions@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-import-attributes@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.27.4) + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-arrow-functions@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-async-generator-functions@7.28.0(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.27.4) + '@babel/traverse': 7.28.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-async-to-generator@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.27.4) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-block-scoped-functions@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-block-scoping@7.28.0(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-class-properties@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.27.4) + '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-class-static-block@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.27.4) + '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-classes@7.28.0(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-globals': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-replace-supers': 7.27.1(@babel/core@7.27.4) + '@babel/traverse': 7.28.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-computed-properties@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/template': 7.27.2 + + '@babel/plugin-transform-destructuring@7.28.0(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/traverse': 7.28.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-dotall-regex@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.27.4) + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-duplicate-keys@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.27.4) + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-dynamic-import@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-explicit-resource-management@7.28.0(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-destructuring': 7.28.0(@babel/core@7.27.4) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-exponentiation-operator@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-export-namespace-from@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-for-of@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-function-name@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/traverse': 7.28.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-json-strings@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-literals@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-logical-assignment-operators@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-member-expression-literals@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-modules-amd@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-module-transforms': 7.27.3(@babel/core@7.27.4) + '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-commonjs@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-module-transforms': 7.27.3(@babel/core@7.27.4) + '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-systemjs@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-module-transforms': 7.27.3(@babel/core@7.27.4) + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + '@babel/traverse': 7.28.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-umd@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-module-transforms': 7.27.3(@babel/core@7.27.4) + '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-named-capturing-groups-regex@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.27.4) + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-new-target@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-nullish-coalescing-operator@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-numeric-separator@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-object-rest-spread@7.28.0(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-destructuring': 7.28.0(@babel/core@7.27.4) + '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.27.4) + '@babel/traverse': 7.28.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-object-super@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-replace-supers': 7.27.1(@babel/core@7.27.4) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-optional-catch-binding@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-optional-chaining@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-parameters@7.27.7(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-private-methods@7.27.1(@babel/core@7.27.1)': + dependencies: + '@babel/core': 7.27.1 + '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.27.1) + '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-private-methods@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.27.4) + '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-private-property-in-object@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.27.4) + '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-property-literals@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.27.1)': + dependencies: + '@babel/core': 7.27.1 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.27.1)': + dependencies: + '@babel/core': 7.27.1 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-regenerator@7.28.0(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-regexp-modifiers@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.27.4) + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-reserved-words@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-shorthand-properties@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-spread@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-sticky-regex@7.27.1(@babel/core@7.27.4)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-template-literals@7.27.1(@babel/core@7.27.4)': dependencies: - '@babel/template': 7.27.2 - '@babel/types': 7.27.3 + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 - '@babel/parser@7.27.2': + '@babel/plugin-transform-typeof-symbol@7.27.1(@babel/core@7.27.4)': dependencies: - '@babel/types': 7.27.1 + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 - '@babel/parser@7.27.4': + '@babel/plugin-transform-unicode-escapes@7.27.1(@babel/core@7.27.4)': dependencies: - '@babel/types': 7.27.3 + '@babel/core': 7.27.4 + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.27.4)': + '@babel/plugin-transform-unicode-property-regex@7.27.1(@babel/core@7.27.4)': dependencies: '@babel/core': 7.27.4 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.27.4) '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.27.4)': + '@babel/plugin-transform-unicode-regex@7.27.1(@babel/core@7.27.4)': dependencies: '@babel/core': 7.27.4 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.27.4) '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-private-methods@7.27.1(@babel/core@7.27.1)': + '@babel/plugin-transform-unicode-sets-regex@7.27.1(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.27.1 - '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.27.1) + '@babel/core': 7.27.4 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.27.4) '@babel/helper-plugin-utils': 7.27.1 - transitivePeerDependencies: - - supports-color - '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.27.1)': + '@babel/preset-env@7.28.0(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.27.1 + '@babel/compat-data': 7.28.0 + '@babel/core': 7.27.4 + '@babel/helper-compilation-targets': 7.27.2 '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-validator-option': 7.27.1 + '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-bugfix-safari-class-field-initializer-scope': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.27.4) + '@babel/plugin-syntax-import-assertions': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-syntax-import-attributes': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.27.4) + '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-async-generator-functions': 7.28.0(@babel/core@7.27.4) + '@babel/plugin-transform-async-to-generator': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-block-scoped-functions': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-block-scoping': 7.28.0(@babel/core@7.27.4) + '@babel/plugin-transform-class-properties': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-class-static-block': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-classes': 7.28.0(@babel/core@7.27.4) + '@babel/plugin-transform-computed-properties': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-destructuring': 7.28.0(@babel/core@7.27.4) + '@babel/plugin-transform-dotall-regex': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-duplicate-keys': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-duplicate-named-capturing-groups-regex': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-dynamic-import': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-explicit-resource-management': 7.28.0(@babel/core@7.27.4) + '@babel/plugin-transform-exponentiation-operator': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-export-namespace-from': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-for-of': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-function-name': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-json-strings': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-literals': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-logical-assignment-operators': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-member-expression-literals': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-modules-amd': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-modules-commonjs': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-modules-systemjs': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-modules-umd': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-named-capturing-groups-regex': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-new-target': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-nullish-coalescing-operator': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-numeric-separator': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-object-rest-spread': 7.28.0(@babel/core@7.27.4) + '@babel/plugin-transform-object-super': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-optional-catch-binding': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-optional-chaining': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.27.4) + '@babel/plugin-transform-private-methods': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-private-property-in-object': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-property-literals': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-regenerator': 7.28.0(@babel/core@7.27.4) + '@babel/plugin-transform-regexp-modifiers': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-reserved-words': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-shorthand-properties': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-spread': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-sticky-regex': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-template-literals': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-typeof-symbol': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-unicode-escapes': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-unicode-property-regex': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-unicode-regex': 7.27.1(@babel/core@7.27.4) + '@babel/plugin-transform-unicode-sets-regex': 7.27.1(@babel/core@7.27.4) + '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.27.4) + babel-plugin-polyfill-corejs2: 0.4.14(@babel/core@7.27.4) + babel-plugin-polyfill-corejs3: 0.13.0(@babel/core@7.27.4) + babel-plugin-polyfill-regenerator: 0.6.5(@babel/core@7.27.4) + core-js-compat: 3.44.0 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color - '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.27.1)': + '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.27.4)': dependencies: - '@babel/core': 7.27.1 + '@babel/core': 7.27.4 '@babel/helper-plugin-utils': 7.27.1 + '@babel/types': 7.28.0 + esutils: 2.0.3 '@babel/runtime@7.27.1': {} @@ -3063,6 +4446,18 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/traverse@7.28.0': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.0 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.28.0 + '@babel/template': 7.27.2 + '@babel/types': 7.28.0 + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + '@babel/types@7.27.1': dependencies: '@babel/helper-string-parser': 7.27.1 @@ -3073,6 +4468,11 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 + '@babel/types@7.28.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + '@dnd-kit/accessibility@3.1.1(react@19.1.0)': dependencies: react: 19.1.0 @@ -3388,6 +4788,11 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} + '@jridgewell/gen-mapping@0.3.12': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping': 0.3.29 + '@jridgewell/gen-mapping@0.3.8': dependencies: '@jridgewell/set-array': 1.2.1 @@ -3398,6 +4803,11 @@ snapshots: '@jridgewell/set-array@1.2.1': {} + '@jridgewell/source-map@0.3.10': + dependencies: + '@jridgewell/gen-mapping': 0.3.12 + '@jridgewell/trace-mapping': 0.3.29 + '@jridgewell/sourcemap-codec@1.5.0': {} '@jridgewell/trace-mapping@0.3.25': @@ -3405,6 +4815,11 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping@0.3.29': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + '@lucide/lab@0.1.2': {} '@mui/core-downloads-tracker@7.1.1': {} @@ -3563,13 +4978,55 @@ snapshots: '@rolldown/pluginutils@1.0.0-beta.9': {} - '@rollup/pluginutils@5.1.4(rollup@4.40.2)': + '@rollup/plugin-babel@5.3.1(@babel/core@7.27.4)(@types/babel__core@7.20.5)(rollup@2.79.2)': + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-module-imports': 7.27.1 + '@rollup/pluginutils': 3.1.0(rollup@2.79.2) + rollup: 2.79.2 + optionalDependencies: + '@types/babel__core': 7.20.5 + transitivePeerDependencies: + - supports-color + + '@rollup/plugin-node-resolve@15.3.1(rollup@2.79.2)': + dependencies: + '@rollup/pluginutils': 5.1.4(rollup@2.79.2) + '@types/resolve': 1.20.2 + deepmerge: 4.3.1 + is-module: 1.0.0 + resolve: 1.22.10 + optionalDependencies: + rollup: 2.79.2 + + '@rollup/plugin-replace@2.4.2(rollup@2.79.2)': + dependencies: + '@rollup/pluginutils': 3.1.0(rollup@2.79.2) + magic-string: 0.25.9 + rollup: 2.79.2 + + '@rollup/plugin-terser@0.4.4(rollup@2.79.2)': + dependencies: + serialize-javascript: 6.0.2 + smob: 1.5.0 + terser: 5.43.1 + optionalDependencies: + rollup: 2.79.2 + + '@rollup/pluginutils@3.1.0(rollup@2.79.2)': + dependencies: + '@types/estree': 0.0.39 + estree-walker: 1.0.1 + picomatch: 2.3.1 + rollup: 2.79.2 + + '@rollup/pluginutils@5.1.4(rollup@2.79.2)': dependencies: '@types/estree': 1.0.7 estree-walker: 2.0.2 picomatch: 4.0.2 optionalDependencies: - rollup: 4.40.2 + rollup: 2.79.2 '@rollup/rollup-android-arm-eabi@4.40.2': optional: true @@ -3633,6 +5090,13 @@ snapshots: '@socket.io/component-emitter@3.1.2': {} + '@surma/rollup-plugin-off-main-thread@2.2.3': + dependencies: + ejs: 3.1.10 + json5: 2.2.3 + magic-string: 0.25.9 + string.prototype.matchall: 4.0.12 + '@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 @@ -3821,7 +5285,7 @@ snapshots: optionalDependencies: '@tanstack/react-router': 1.120.13(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@tanstack/router-plugin@1.120.13(@tanstack/react-router@1.120.13(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(vite@6.3.5(@types/node@24.0.0)(jiti@1.21.7)(sass@1.89.1)(tsx@4.19.4))': + '@tanstack/router-plugin@1.120.13(@tanstack/react-router@1.120.13(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(vite@6.3.5(@types/node@24.0.0)(jiti@1.21.7)(sass@1.89.1)(terser@5.43.1)(tsx@4.19.4))': dependencies: '@babel/core': 7.27.4 '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.27.4) @@ -3842,7 +5306,7 @@ snapshots: zod: 3.25.48 optionalDependencies: '@tanstack/react-router': 1.120.13(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - vite: 6.3.5(@types/node@24.0.0)(jiti@1.21.7)(sass@1.89.1)(tsx@4.19.4) + vite: 6.3.5(@types/node@24.0.0)(jiti@1.21.7)(sass@1.89.1)(terser@5.43.1)(tsx@4.19.4) transitivePeerDependencies: - supports-color @@ -3853,9 +5317,9 @@ snapshots: ansis: 3.17.0 diff: 7.0.0 - '@tanstack/router-vite-plugin@1.120.13(@tanstack/react-router@1.120.13(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(vite@6.3.5(@types/node@24.0.0)(jiti@1.21.7)(sass@1.89.1)(tsx@4.19.4))': + '@tanstack/router-vite-plugin@1.120.13(@tanstack/react-router@1.120.13(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(vite@6.3.5(@types/node@24.0.0)(jiti@1.21.7)(sass@1.89.1)(terser@5.43.1)(tsx@4.19.4))': dependencies: - '@tanstack/router-plugin': 1.120.13(@tanstack/react-router@1.120.13(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(vite@6.3.5(@types/node@24.0.0)(jiti@1.21.7)(sass@1.89.1)(tsx@4.19.4)) + '@tanstack/router-plugin': 1.120.13(@tanstack/react-router@1.120.13(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(vite@6.3.5(@types/node@24.0.0)(jiti@1.21.7)(sass@1.89.1)(terser@5.43.1)(tsx@4.19.4)) transitivePeerDependencies: - '@rsbuild/core' - '@tanstack/react-router' @@ -3893,6 +5357,8 @@ snapshots: '@types/diff@5.2.3': {} + '@types/estree@0.0.39': {} + '@types/estree@1.0.7': {} '@types/json-schema@7.0.15': {} @@ -3922,6 +5388,10 @@ snapshots: '@types/prop-types': 15.7.14 csstype: 3.1.3 + '@types/resolve@1.20.2': {} + + '@types/trusted-types@2.0.7': {} + '@typescript-eslint/eslint-plugin@8.33.0(@typescript-eslint/parser@8.33.0(eslint@9.28.0(jiti@1.21.7))(typescript@5.8.3))(eslint@9.28.0(jiti@1.21.7))(typescript@5.8.3)': dependencies: '@eslint-community/regexpp': 4.12.1 @@ -4051,15 +5521,15 @@ snapshots: '@typescript-eslint/types': 8.33.0 eslint-visitor-keys: 4.2.0 - '@vitejs/plugin-react-swc@3.10.0(vite@6.3.5(@types/node@24.0.0)(jiti@1.21.7)(sass@1.89.1)(tsx@4.19.4))': + '@vitejs/plugin-react-swc@3.10.0(vite@6.3.5(@types/node@24.0.0)(jiti@1.21.7)(sass@1.89.1)(terser@5.43.1)(tsx@4.19.4))': dependencies: '@rolldown/pluginutils': 1.0.0-beta.9 '@swc/core': 1.11.24 - vite: 6.3.5(@types/node@24.0.0)(jiti@1.21.7)(sass@1.89.1)(tsx@4.19.4) + vite: 6.3.5(@types/node@24.0.0)(jiti@1.21.7)(sass@1.89.1)(terser@5.43.1)(tsx@4.19.4) transitivePeerDependencies: - '@swc/helpers' - '@vitejs/plugin-react@4.5.0(vite@6.3.5(@types/node@24.0.0)(jiti@1.21.7)(sass@1.89.1)(tsx@4.19.4))': + '@vitejs/plugin-react@4.5.0(vite@6.3.5(@types/node@24.0.0)(jiti@1.21.7)(sass@1.89.1)(terser@5.43.1)(tsx@4.19.4))': dependencies: '@babel/core': 7.27.1 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.27.1) @@ -4067,7 +5537,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.9 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 6.3.5(@types/node@24.0.0)(jiti@1.21.7)(sass@1.89.1)(tsx@4.19.4) + vite: 6.3.5(@types/node@24.0.0)(jiti@1.21.7)(sass@1.89.1)(terser@5.43.1)(tsx@4.19.4) transitivePeerDependencies: - supports-color @@ -4090,6 +5560,13 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 + ajv@8.17.1: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.0.6 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + ansi-regex@5.0.1: {} ansi-styles@4.3.0: @@ -4162,6 +5639,10 @@ snapshots: async-function@1.0.0: {} + async@3.2.6: {} + + at-least-node@1.0.0: {} + available-typed-arrays@1.0.7: dependencies: possible-typed-array-names: 1.1.0 @@ -4181,6 +5662,30 @@ snapshots: cosmiconfig: 7.1.0 resolve: 1.22.10 + babel-plugin-polyfill-corejs2@0.4.14(@babel/core@7.27.4): + dependencies: + '@babel/compat-data': 7.28.0 + '@babel/core': 7.27.4 + '@babel/helper-define-polyfill-provider': 0.6.5(@babel/core@7.27.4) + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + babel-plugin-polyfill-corejs3@0.13.0(@babel/core@7.27.4): + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-define-polyfill-provider': 0.6.5(@babel/core@7.27.4) + core-js-compat: 3.44.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-polyfill-regenerator@0.6.5(@babel/core@7.27.4): + dependencies: + '@babel/core': 7.27.4 + '@babel/helper-define-polyfill-provider': 0.6.5(@babel/core@7.27.4) + transitivePeerDependencies: + - supports-color + babel-plugin-react-compiler@19.1.0-rc.2: dependencies: '@babel/types': 7.27.1 @@ -4209,6 +5714,15 @@ snapshots: node-releases: 2.0.19 update-browserslist-db: 1.1.3(browserslist@4.24.5) + browserslist@4.25.1: + dependencies: + caniuse-lite: 1.0.30001727 + electron-to-chromium: 1.5.181 + node-releases: 2.0.19 + update-browserslist-db: 1.1.3(browserslist@4.25.1) + + buffer-from@1.1.2: {} + call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -4232,6 +5746,8 @@ snapshots: caniuse-lite@1.0.30001717: {} + caniuse-lite@1.0.30001727: {} + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 @@ -4267,12 +5783,20 @@ snapshots: color-name@1.1.4: {} + commander@2.20.3: {} + + common-tags@1.8.2: {} + concat-map@0.0.1: {} convert-source-map@1.9.0: {} convert-source-map@2.0.0: {} + core-js-compat@3.44.0: + dependencies: + browserslist: 4.25.1 + cosmiconfig@7.1.0: dependencies: '@types/parse-json': 4.0.2 @@ -4296,6 +5820,8 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + crypto-random-string@2.0.0: {} + csstype@3.1.3: {} data-view-buffer@1.0.2: @@ -4330,6 +5856,8 @@ snapshots: deep-is@0.1.4: {} + deepmerge@4.3.1: {} + define-data-property@1.1.4: dependencies: es-define-property: 1.0.1 @@ -4369,8 +5897,14 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 + ejs@3.1.10: + dependencies: + jake: 10.9.2 + electron-to-chromium@1.5.151: {} + electron-to-chromium@1.5.181: {} + emoji-regex@8.0.0: {} engine.io-client@6.6.3: @@ -4664,6 +6198,8 @@ snapshots: estraverse@5.3.0: {} + estree-walker@1.0.1: {} + estree-walker@2.0.2: {} esutils@2.0.3: {} @@ -4682,6 +6218,8 @@ snapshots: fast-levenshtein@2.0.6: {} + fast-uri@3.0.6: {} + fastq@1.19.1: dependencies: reusify: 1.1.0 @@ -4694,6 +6232,10 @@ snapshots: dependencies: flat-cache: 4.0.1 + filelist@1.0.4: + dependencies: + minimatch: 5.1.6 + fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 @@ -4716,6 +6258,15 @@ snapshots: dependencies: is-callable: 1.2.7 + fs-extra@9.1.0: + dependencies: + at-least-node: 1.0.0 + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + + fs.realpath@1.0.0: {} + fsevents@2.3.3: optional: true @@ -4749,6 +6300,8 @@ snapshots: hasown: 2.0.2 math-intrinsics: 1.1.0 + get-own-enumerable-property-symbols@3.0.2: {} + get-proto@1.0.1: dependencies: dunder-proto: 1.0.1 @@ -4772,6 +6325,15 @@ snapshots: dependencies: is-glob: 4.0.3 + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + globals@11.12.0: {} globals@14.0.0: {} @@ -4787,6 +6349,8 @@ snapshots: gopd@1.2.0: {} + graceful-fs@4.2.11: {} + graphemer@1.4.0: {} has-bigints@1.1.0: {} @@ -4821,6 +6385,8 @@ snapshots: dependencies: react-is: 16.13.1 + idb@7.1.1: {} + ignore@5.3.2: {} ignore@7.0.5: {} @@ -4834,6 +6400,13 @@ snapshots: imurmurhash@0.1.4: {} + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + internal-slot@1.1.0: dependencies: es-errors: 1.3.0 @@ -4907,6 +6480,8 @@ snapshots: is-map@2.0.3: {} + is-module@1.0.0: {} + is-number-object@1.1.1: dependencies: call-bound: 1.0.4 @@ -4914,6 +6489,8 @@ snapshots: is-number@7.0.0: {} + is-obj@1.0.1: {} + is-regex@1.2.1: dependencies: call-bound: 1.0.4 @@ -4921,12 +6498,16 @@ snapshots: has-tostringtag: 1.0.2 hasown: 2.0.2 + is-regexp@1.0.0: {} + is-set@2.0.3: {} is-shared-array-buffer@1.0.4: dependencies: call-bound: 1.0.4 + is-stream@2.0.1: {} + is-string@1.1.1: dependencies: call-bound: 1.0.4 @@ -4966,6 +6547,13 @@ snapshots: has-symbols: 1.1.0 set-function-name: 2.0.2 + jake@10.9.2: + dependencies: + async: 3.2.6 + chalk: 4.1.2 + filelist: 1.0.4 + minimatch: 3.1.2 + jiti@1.21.7: optional: true @@ -4975,6 +6563,8 @@ snapshots: dependencies: argparse: 2.0.1 + jsesc@3.0.2: {} + jsesc@3.1.0: {} json-buffer@3.0.1: {} @@ -4983,10 +6573,22 @@ snapshots: json-schema-traverse@0.4.1: {} + json-schema-traverse@1.0.0: {} + + json-schema@0.4.0: {} + json-stable-stringify-without-jsonify@1.0.1: {} json5@2.2.3: {} + jsonfile@6.1.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + jsonpointer@5.0.1: {} + jsx-ast-utils@3.3.5: dependencies: array-includes: 3.1.8 @@ -4998,6 +6600,8 @@ snapshots: dependencies: json-buffer: 3.0.1 + leven@3.1.0: {} + levn@0.4.1: dependencies: prelude-ls: 1.2.1 @@ -5009,8 +6613,14 @@ snapshots: dependencies: p-locate: 5.0.0 + lodash.debounce@4.0.8: {} + lodash.merge@4.6.2: {} + lodash.sortby@4.7.0: {} + + lodash@4.17.21: {} + loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 @@ -5027,6 +6637,10 @@ snapshots: dependencies: react: 19.1.0 + magic-string@0.25.9: + dependencies: + sourcemap-codec: 1.4.8 + math-intrinsics@1.1.0: {} memoize-one@5.2.1: {} @@ -5042,6 +6656,10 @@ snapshots: dependencies: brace-expansion: 1.1.11 + minimatch@5.1.6: + dependencies: + brace-expansion: 2.0.1 + minimatch@9.0.5: dependencies: brace-expansion: 2.0.1 @@ -5100,6 +6718,10 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 + once@1.4.0: + dependencies: + wrappy: 1.0.2 + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -5136,6 +6758,8 @@ snapshots: path-exists@4.0.0: {} + path-is-absolute@1.0.1: {} + path-key@3.1.1: {} path-parse@1.0.7: {} @@ -5160,6 +6784,10 @@ snapshots: prettier@3.5.3: {} + pretty-bytes@5.6.0: {} + + pretty-bytes@6.1.1: {} + prop-types@15.8.1: dependencies: loose-envify: 1.4.0 @@ -5170,6 +6798,10 @@ snapshots: queue-microtask@1.2.3: {} + randombytes@2.1.0: + dependencies: + safe-buffer: 5.2.1 + react-dom@19.1.0(react@19.1.0): dependencies: react: 19.1.0 @@ -5226,6 +6858,12 @@ snapshots: get-proto: 1.0.1 which-builtin-type: 1.2.1 + regenerate-unicode-properties@10.2.0: + dependencies: + regenerate: 1.4.2 + + regenerate@1.4.2: {} + regexp.prototype.flags@1.5.4: dependencies: call-bind: 1.0.8 @@ -5235,8 +6873,25 @@ snapshots: gopd: 1.2.0 set-function-name: 2.0.2 + regexpu-core@6.2.0: + dependencies: + regenerate: 1.4.2 + regenerate-unicode-properties: 10.2.0 + regjsgen: 0.8.0 + regjsparser: 0.12.0 + unicode-match-property-ecmascript: 2.0.0 + unicode-match-property-value-ecmascript: 2.2.0 + + regjsgen@0.8.0: {} + + regjsparser@0.12.0: + dependencies: + jsesc: 3.0.2 + require-directory@2.1.1: {} + require-from-string@2.0.2: {} + resolve-from@4.0.0: {} resolve-pkg-maps@1.0.0: {} @@ -5255,6 +6910,10 @@ snapshots: reusify@1.1.0: {} + rollup@2.79.2: + optionalDependencies: + fsevents: 2.3.3 + rollup@4.40.2: dependencies: '@types/estree': 1.0.7 @@ -5293,6 +6952,8 @@ snapshots: has-symbols: 1.1.0 isarray: 2.0.5 + safe-buffer@5.2.1: {} + safe-push-apply@1.0.0: dependencies: es-errors: 1.3.0 @@ -5320,6 +6981,10 @@ snapshots: semver@7.7.2: {} + serialize-javascript@6.0.2: + dependencies: + randombytes: 2.1.0 + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -5376,6 +7041,8 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 + smob@1.5.0: {} + snake-case@3.0.4: dependencies: dot-case: 3.0.4 @@ -5401,8 +7068,21 @@ snapshots: source-map-js@1.2.1: {} + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + source-map@0.5.7: {} + source-map@0.6.1: {} + + source-map@0.8.0-beta.0: + dependencies: + whatwg-url: 7.1.0 + + sourcemap-codec@1.4.8: {} + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -5453,10 +7133,18 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 + stringify-object@3.3.0: + dependencies: + get-own-enumerable-property-symbols: 3.0.2 + is-obj: 1.0.1 + is-regexp: 1.0.0 + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 + strip-comments@2.0.1: {} + strip-json-comments@3.1.1: {} stylis@4.2.0: {} @@ -5469,6 +7157,22 @@ snapshots: svg-parser@2.0.4: {} + temp-dir@2.0.0: {} + + tempy@0.6.0: + dependencies: + is-stream: 2.0.1 + temp-dir: 2.0.0 + type-fest: 0.16.0 + unique-string: 2.0.0 + + terser@5.43.1: + dependencies: + '@jridgewell/source-map': 0.3.10 + acorn: 8.14.1 + commander: 2.20.3 + source-map-support: 0.5.21 + tiny-invariant@1.3.3: {} tiny-warning@1.0.3: {} @@ -5482,6 +7186,10 @@ snapshots: dependencies: is-number: 7.0.0 + tr46@1.0.1: + dependencies: + punycode: 2.3.1 + ts-api-utils@2.1.0(typescript@5.8.3): dependencies: typescript: 5.8.3 @@ -5503,6 +7211,8 @@ snapshots: dependencies: prelude-ls: 1.2.1 + type-fest@0.16.0: {} + typed-array-buffer@1.0.3: dependencies: call-bound: 1.0.4 @@ -5557,18 +7267,43 @@ snapshots: undici-types@7.8.0: {} + unicode-canonical-property-names-ecmascript@2.0.1: {} + + unicode-match-property-ecmascript@2.0.0: + dependencies: + unicode-canonical-property-names-ecmascript: 2.0.1 + unicode-property-aliases-ecmascript: 2.1.0 + + unicode-match-property-value-ecmascript@2.2.0: {} + + unicode-property-aliases-ecmascript@2.1.0: {} + + unique-string@2.0.0: + dependencies: + crypto-random-string: 2.0.0 + + universalify@2.0.1: {} + unplugin@2.3.5: dependencies: acorn: 8.14.1 picomatch: 4.0.2 webpack-virtual-modules: 0.6.2 + upath@1.2.0: {} + update-browserslist-db@1.1.3(browserslist@4.24.5): dependencies: browserslist: 4.24.5 escalade: 3.2.0 picocolors: 1.1.1 + update-browserslist-db@1.1.3(browserslist@4.25.1): + dependencies: + browserslist: 4.25.1 + escalade: 3.2.0 + picocolors: 1.1.1 + uri-js@4.4.1: dependencies: punycode: 2.3.1 @@ -5577,29 +7312,40 @@ snapshots: dependencies: react: 19.1.0 - vite-plugin-svgr@4.3.0(rollup@4.40.2)(typescript@5.8.3)(vite@6.3.5(@types/node@24.0.0)(jiti@1.21.7)(sass@1.89.1)(tsx@4.19.4)): + vite-plugin-pwa@1.0.1(vite@6.3.5(@types/node@24.0.0)(jiti@1.21.7)(sass@1.89.1)(terser@5.43.1)(tsx@4.19.4))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.3.0): + dependencies: + debug: 4.4.1 + pretty-bytes: 6.1.1 + tinyglobby: 0.2.13 + vite: 6.3.5(@types/node@24.0.0)(jiti@1.21.7)(sass@1.89.1)(terser@5.43.1)(tsx@4.19.4) + workbox-build: 7.3.0(@types/babel__core@7.20.5) + workbox-window: 7.3.0 + transitivePeerDependencies: + - supports-color + + vite-plugin-svgr@4.3.0(rollup@2.79.2)(typescript@5.8.3)(vite@6.3.5(@types/node@24.0.0)(jiti@1.21.7)(sass@1.89.1)(terser@5.43.1)(tsx@4.19.4)): dependencies: - '@rollup/pluginutils': 5.1.4(rollup@4.40.2) + '@rollup/pluginutils': 5.1.4(rollup@2.79.2) '@svgr/core': 8.1.0(typescript@5.8.3) '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@5.8.3)) - vite: 6.3.5(@types/node@24.0.0)(jiti@1.21.7)(sass@1.89.1)(tsx@4.19.4) + vite: 6.3.5(@types/node@24.0.0)(jiti@1.21.7)(sass@1.89.1)(terser@5.43.1)(tsx@4.19.4) transitivePeerDependencies: - rollup - supports-color - typescript - vite-tsconfig-paths@4.3.2(typescript@5.8.3)(vite@6.3.5(@types/node@24.0.0)(jiti@1.21.7)(sass@1.89.1)(tsx@4.19.4)): + vite-tsconfig-paths@4.3.2(typescript@5.8.3)(vite@6.3.5(@types/node@24.0.0)(jiti@1.21.7)(sass@1.89.1)(terser@5.43.1)(tsx@4.19.4)): dependencies: debug: 4.4.0 globrex: 0.1.2 tsconfck: 3.1.5(typescript@5.8.3) optionalDependencies: - vite: 6.3.5(@types/node@24.0.0)(jiti@1.21.7)(sass@1.89.1)(tsx@4.19.4) + vite: 6.3.5(@types/node@24.0.0)(jiti@1.21.7)(sass@1.89.1)(terser@5.43.1)(tsx@4.19.4) transitivePeerDependencies: - supports-color - typescript - vite@6.3.5(@types/node@24.0.0)(jiti@1.21.7)(sass@1.89.1)(tsx@4.19.4): + vite@6.3.5(@types/node@24.0.0)(jiti@1.21.7)(sass@1.89.1)(terser@5.43.1)(tsx@4.19.4): dependencies: esbuild: 0.25.4 fdir: 6.4.4(picomatch@4.0.2) @@ -5612,12 +7358,21 @@ snapshots: fsevents: 2.3.3 jiti: 1.21.7 sass: 1.89.1 + terser: 5.43.1 tsx: 4.19.4 wavesurfer.js@7.9.5: {} + webidl-conversions@4.0.2: {} + webpack-virtual-modules@0.6.2: {} + whatwg-url@7.1.0: + dependencies: + lodash.sortby: 4.7.0 + tr46: 1.0.1 + webidl-conversions: 4.0.2 + which-boxed-primitive@1.1.1: dependencies: is-bigint: 1.1.0 @@ -5665,12 +7420,127 @@ snapshots: word-wrap@1.2.5: {} + workbox-background-sync@7.3.0: + dependencies: + idb: 7.1.1 + workbox-core: 7.3.0 + + workbox-broadcast-update@7.3.0: + dependencies: + workbox-core: 7.3.0 + + workbox-build@7.3.0(@types/babel__core@7.20.5): + dependencies: + '@apideck/better-ajv-errors': 0.3.6(ajv@8.17.1) + '@babel/core': 7.27.4 + '@babel/preset-env': 7.28.0(@babel/core@7.27.4) + '@babel/runtime': 7.27.1 + '@rollup/plugin-babel': 5.3.1(@babel/core@7.27.4)(@types/babel__core@7.20.5)(rollup@2.79.2) + '@rollup/plugin-node-resolve': 15.3.1(rollup@2.79.2) + '@rollup/plugin-replace': 2.4.2(rollup@2.79.2) + '@rollup/plugin-terser': 0.4.4(rollup@2.79.2) + '@surma/rollup-plugin-off-main-thread': 2.2.3 + ajv: 8.17.1 + common-tags: 1.8.2 + fast-json-stable-stringify: 2.1.0 + fs-extra: 9.1.0 + glob: 7.2.3 + lodash: 4.17.21 + pretty-bytes: 5.6.0 + rollup: 2.79.2 + source-map: 0.8.0-beta.0 + stringify-object: 3.3.0 + strip-comments: 2.0.1 + tempy: 0.6.0 + upath: 1.2.0 + workbox-background-sync: 7.3.0 + workbox-broadcast-update: 7.3.0 + workbox-cacheable-response: 7.3.0 + workbox-core: 7.3.0 + workbox-expiration: 7.3.0 + workbox-google-analytics: 7.3.0 + workbox-navigation-preload: 7.3.0 + workbox-precaching: 7.3.0 + workbox-range-requests: 7.3.0 + workbox-recipes: 7.3.0 + workbox-routing: 7.3.0 + workbox-strategies: 7.3.0 + workbox-streams: 7.3.0 + workbox-sw: 7.3.0 + workbox-window: 7.3.0 + transitivePeerDependencies: + - '@types/babel__core' + - supports-color + + workbox-cacheable-response@7.3.0: + dependencies: + workbox-core: 7.3.0 + + workbox-core@7.3.0: {} + + workbox-expiration@7.3.0: + dependencies: + idb: 7.1.1 + workbox-core: 7.3.0 + + workbox-google-analytics@7.3.0: + dependencies: + workbox-background-sync: 7.3.0 + workbox-core: 7.3.0 + workbox-routing: 7.3.0 + workbox-strategies: 7.3.0 + + workbox-navigation-preload@7.3.0: + dependencies: + workbox-core: 7.3.0 + + workbox-precaching@7.3.0: + dependencies: + workbox-core: 7.3.0 + workbox-routing: 7.3.0 + workbox-strategies: 7.3.0 + + workbox-range-requests@7.3.0: + dependencies: + workbox-core: 7.3.0 + + workbox-recipes@7.3.0: + dependencies: + workbox-cacheable-response: 7.3.0 + workbox-core: 7.3.0 + workbox-expiration: 7.3.0 + workbox-precaching: 7.3.0 + workbox-routing: 7.3.0 + workbox-strategies: 7.3.0 + + workbox-routing@7.3.0: + dependencies: + workbox-core: 7.3.0 + + workbox-strategies@7.3.0: + dependencies: + workbox-core: 7.3.0 + + workbox-streams@7.3.0: + dependencies: + workbox-core: 7.3.0 + workbox-routing: 7.3.0 + + workbox-sw@7.3.0: {} + + workbox-window@7.3.0: + dependencies: + '@types/trusted-types': 2.0.7 + workbox-core: 7.3.0 + wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 string-width: 4.2.3 strip-ansi: 6.0.1 + wrappy@1.0.2: {} + ws@8.17.1: {} xmlhttprequest-ssl@2.1.2: {} diff --git a/frontend/src/api/notifications.ts b/frontend/src/api/notifications.ts index 03e98566..d3fe4e89 100644 --- a/frontend/src/api/notifications.ts +++ b/frontend/src/api/notifications.ts @@ -1,14 +1,18 @@ -import { queryOptions, UseMutationOptions } from "@tanstack/react-query"; +import { + infiniteQueryOptions, + queryOptions, + UseMutationOptions, +} from "@tanstack/react-query"; import { - PushSettings, PushSubscription as PyPushSubscription, - PushWebHook as PyPushWebHook, + SubscriptionSettings, + WebhookSubscription, } from "@/pythonTypes"; import { APIError, queryClient } from "./common"; -class PushSubscriptionError extends Error { +export class PushSubscriptionError extends Error { constructor(message: string) { super(message); this.name = "PushSubscriptionError"; @@ -17,22 +21,22 @@ class PushSubscriptionError extends Error { /* -------------------------------- webhooks -------------------------------- */ -export interface PushWebHookSubscribeRequest - extends Omit { - settings: null | PushSettings; +export interface WebhookSubscribeRequest + extends Omit { + settings: null | SubscriptionSettings; id?: string; // Optional ID for upsert } /** Upsert a webhook * can be used to update an existing webhook or create a new one. */ -export const subscribeWebhookMutationOptions: UseMutationOptions< - PyPushWebHook, +export const upsertWebhookMutationOptions: UseMutationOptions< + WebhookSubscription, APIError | PushSubscriptionError, - PushWebHookSubscribeRequest + WebhookSubscribeRequest > = { mutationKey: ["webhook", "upsert"], - mutationFn: async (params: PushWebHookSubscribeRequest) => { + mutationFn: async (params: WebhookSubscribeRequest | WebhookSubscription) => { const response = await fetch("/notifications/webhook/", { method: "POST", headers: { @@ -45,54 +49,120 @@ export const subscribeWebhookMutationOptions: UseMutationOptions< `Failed to subscribe webhook: ${response.statusText}` ); } - const webHook = (await response.json()) as PyPushWebHook; - // We might have some webhook data in the cache already - queryClient.setQueryData( - webhookQueryOptions(webHook.id).queryKey, - webHook + return (await response.json()) as WebhookSubscription; + }, + onSuccess: async (data) => { + await queryClient.invalidateQueries(webhookQueryOptions(data.id)); + await queryClient.invalidateQueries(webhooksInfiniteQueryOptions()); + }, + onMutate: (params) => { + // if id is given we do an update, otherwise we do an insert + if (!params.id) { + return; + } + + // Update the id cache optimistically + const dataBefore = queryClient.getQueryData( + webhookQueryOptions(params.id).queryKey ); + if (dataBefore) { + queryClient.setQueryData( + webhookQueryOptions(params.id).queryKey, + { + ...dataBefore, + ...params, + settings: params.settings || dataBefore.settings, + } + ); + } - // TODO: Update the infinite query for webhooks + // Update the webhooks list cache optimistically + const currentWebhooks = queryClient.getQueryData( + webhooksInfiniteQueryOptions().queryKey + ); - return (await response.json()) as PyPushWebHook; + if (currentWebhooks) { + console.log("Updating webhooks list cache optimistically", currentWebhooks); + const updatedWebhooks = currentWebhooks.pages.map((page) => { + return { + ...page, + items: page.items.map((webhook) => + webhook.id === params.id + ? { + ...webhook, + ...params, + settings: params.settings || webhook.settings, + } + : webhook + ), + }; + }); + queryClient.setQueryData(webhooksInfiniteQueryOptions().queryKey, { + ...currentWebhooks, + pages: updatedWebhooks, + }); + } }, }; /** Remove a webhook */ -export const unsubscribeWebhookMutationOptions: UseMutationOptions< +export const deleteWebhookMutationOptions: UseMutationOptions< void, APIError | PushSubscriptionError, string > = { - mutationKey: ["webhook", "unsubscribe"], + mutationKey: ["webhook", "delete"], mutationFn: async (id: string) => { - await fetch(`/notifications/webhook/${id}`, { + await fetch(`/notifications/webhook/id/${id}`, { method: "DELETE", }); queryClient.removeQueries(webhookQueryOptions(id)); - // TODO: Update the infinite query for webhooks + await queryClient.invalidateQueries(webhooksInfiniteQueryOptions()); }, }; export const webhookQueryOptions = (id: string) => - queryOptions({ + queryOptions({ queryKey: ["webhook", id], queryFn: async () => { - const response = await fetch(`/notifications/webhook/${id}`); + const response = await fetch(`/notifications/webhook/id/${id}`); - return (await response.json()) as PyPushWebHook; + return (await response.json()) as WebhookSubscription; }, }); -// TODO: Infinity query for all webhooks +export const webhooksInfiniteQueryOptions = () => { + const params = new URLSearchParams(); + params.set("n_items", "20"); // Set the number of items per page + + const initUrl = `/notifications/webhook/?${params.toString()}`; + + return infiniteQueryOptions({ + queryKey: ["webhooks"], + queryFn: async ({ pageParam }) => { + const response = await fetch(pageParam.replace("/api_v1", "")); + + return (await response.json()) as { + items: WebhookSubscription[]; + next: string | null; + }; + }, + initialPageParam: initUrl, + getNextPageParam: (lastPage) => lastPage.next, + select: (data) => { + console.log("Selected webhooks data", data); + return data.pages.flatMap((p) => p.items || []); + }, + }); +}; /* ------------------------------ web push api ------------------------------ */ export interface PushSubscriptionUpsertRequest extends Omit { - settings: null | PushSettings; + settings: null | SubscriptionSettings; endpoint: string; } @@ -147,11 +217,77 @@ export const subscribePushMutationOptions: UseMutationOptions< body: JSON.stringify(subscription.toJSON()), }); + const sub = { + server: (await response.json()) as PyPushSubscription, + subscription, + }; + await invalidatePushQuery(sub); + return sub; + }, +}; + +export const updatePushMutationOptions: UseMutationOptions< + PushSubscriptionReturn, + APIError | PushSubscriptionError, + SubscriptionSettings +> = { + mutationKey: ["subscription", "update"], + mutationFn: async (settings: SubscriptionSettings) => { + // Get pushManager from the service worker registration + const registration = await navigator.serviceWorker.ready; + if (!registration.pushManager) { + console.warn( + "Push Manager is not available in this service worker registration.", + registration + ); + throw new PushSubscriptionError("PushManager unavailable"); + } + // Get the current subscription + const subscription = await registration.pushManager.getSubscription(); + if (!subscription) { + console.warn("No push subscription found."); + throw new PushSubscriptionError("No push subscription found."); + } + // Notify the server that we updated the subscription + const response = await fetch(`/notifications/subscription`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + ...subscription.toJSON(), + settings, + } as PushSubscriptionUpsertRequest), + }); + return { server: (await response.json()) as PyPushSubscription, subscription, }; }, + onMutate: (settings) => { + // Optimistically update the subscription in the cache + const currentSubscription = queryClient.getQueryData( + pushQueryOptions.queryKey + ); + if (currentSubscription) { + const updatedSubscription = { + ...currentSubscription, + server: { + ...currentSubscription.server, + settings, + }, + }; + queryClient.setQueryData( + pushQueryOptions.queryKey, + updatedSubscription + ); + } + }, + onSettled: async (data) => { + // Invalidate the query to ensure we have the latest data + await invalidatePushQuery(data); + }, }; export const unsubscribePushMutationOptions: UseMutationOptions< @@ -178,9 +314,12 @@ export const unsubscribePushMutationOptions: UseMutationOptions< } // Notify the server that we unsubscribed - await fetch(`/notifications/${encodeURIComponent(subscription.endpoint)}`, { - method: "DELETE", - }); + await fetch( + `/notifications/subscription/id/${encodeURIComponent(subscription.endpoint)}`, + { + method: "DELETE", + } + ); await invalidatePushQuery(); // Unsubscribe from push notifications locally @@ -229,7 +368,7 @@ export const pushQueryOptions = queryOptions< // Get the server subscription const response = await fetch( - `/notifications/${encodeURIComponent(localSubscription.endpoint)}` + `/notifications/subscription/id/${encodeURIComponent(localSubscription.endpoint)}` ); const data = (await response.json()) as PyPushSubscription; return { @@ -241,7 +380,12 @@ export const pushQueryOptions = queryOptions< retry: false, }); -async function invalidatePushQuery() { +async function invalidatePushQuery(sub: PushSubscriptionReturn | null = null) { + queryClient.setQueryData( + pushQueryOptions.queryKey, + sub + ); + await queryClient .cancelQueries(pushQueryOptions) .then(() => queryClient.invalidateQueries(pushQueryOptions)); diff --git a/frontend/src/components/common/strings.tsx b/frontend/src/components/common/strings.tsx index fc087bc3..73a4e181 100644 --- a/frontend/src/components/common/strings.tsx +++ b/frontend/src/components/common/strings.tsx @@ -1,3 +1,42 @@ export function capitalizeFirstLetter(string: string) { return string.charAt(0).toUpperCase() + string.slice(1); } + +// Regular expression for validating IPv4 addresses +const ipv4Pattern = /^(\d{1,3}\.){3}\d{1,3}$/; +// Regular expression for validating IPv6 addresses +const ipv6Pattern = /^(?:[a-fA-F0-9]{1,4}:){7}[a-fA-F0-9]{1,4}$|::1$/; +// Regular expression for validating hostnames +const hostnamePattern = /^[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$/; +// Regular expression for validating simple hostnames (without TLD) +const simpleHostnamePattern = /^[a-zA-Z0-9-]+$/; + +/** + * Checks if a string is a valid URL. + * @param {string} str - The string to validate as a URL. + * @returns {boolean} - Returns `true` if the string is a valid URL, otherwise `false`. + */ +export function isValidUrl(str: string) { + // Check if the string is empty + if (!str || str.trim() === "") { + return false; + } + // Check if the string matches the IPv4 pattern + if ( + ipv4Pattern.test(str) || + ipv6Pattern.test(str) || + hostnamePattern.test(str) || + simpleHostnamePattern.test(str) + ) { + return true; + } + + try { + // Attempt to create a URL object from the string + new URL(str); + return true; + } catch (_err) { + // If an error is thrown, the string is not a valid URL + return false; + } +} diff --git a/frontend/src/components/notifications/push.tsx b/frontend/src/components/notifications/push.tsx new file mode 100644 index 00000000..eb761d61 --- /dev/null +++ b/frontend/src/components/notifications/push.tsx @@ -0,0 +1,292 @@ +import { + BellIcon, + BellOffIcon, + BellPlusIcon, + CheckIcon, + SettingsIcon, +} from "lucide-react"; +import { ReactNode, useState } from "react"; +import { + Alert, + AlertTitle, + Box, + Button, + DialogContent, + Link, + Typography, + useTheme, +} from "@mui/material"; +import { useMutation, useQuery } from "@tanstack/react-query"; + +import { APIError } from "@/api/common"; +import { + pushQueryOptions, + PushSubscriptionError, + PushSubscriptionReturn, + subscribePushMutationOptions, + unsubscribePushMutationOptions, + updatePushMutationOptions, +} from "@/api/notifications"; +import { SubscriptionSettings } from "@/pythonTypes"; + +import { NotificationsSettings } from "./settings"; +import { useNotificationPermission } from "./useNotificationPermission"; + +import { Dialog } from "../common/dialogs"; + +/** This component handles the subscription to notifications. + * + * We try to keep it simple and only show one button at a time to the user. + * + * Notifications consist of two parts, + * 1. The browser's Notification API, which allows us to show notifications to the user. + * 2. The Push API, which allows us to send notifications from the server to the browser. + * + * The browser needs to support both APIs, and the user needs to grant permission. + */ +export function SubscribeToPushNotifications() { + const theme = useTheme(); + const { permission, available, requestPermission } = useNotificationPermission(); + const { data: subscription, error: subscriptionError } = useQuery(pushQueryOptions); + + const alerts: Array = []; + + if (!available) { + alerts.push( + + Notification API not available + Notifications are not available in this browser. Please use a modern + browser that supports the Notifications API. You might need to host this + application on a secure context (HTTPS or use localhost). + + ); + } + + if (subscriptionError) { + if (subscriptionError instanceof PushSubscriptionError) { + alerts.push( + + Push API not available + {subscriptionError.message} + + ); + } else if ( + subscriptionError instanceof APIError && + subscriptionError.statusCode !== 404 // 404 is expected if the subscription does not exist on the server + ) { + alerts.push( + + Failed to fetch subscription + {subscriptionError.message} + + ); + } + } + if (permission === "denied") { + alerts.push( + + Notification permission denied + You have denied permission to receive notifications. You may need to + change this in your browser settings manually if you want to receive + notifications. +
    + +
  • Chrome instructions
  • + + +
  • Firefox instructions
  • + +
+
+ ); + } + + // Return early if there are any alerts to show (only error or warning alerts) + if (alerts.length > 0) { + return ( + + {alerts} + + ); + } + + return ( + + {permission === "granted" && !subscription && ( + }> + Notification permissions granted + You have granted permission to receive notifications. + + )} + + {permission === "granted" && !subscription && ( + + Not subscribed to push service + You are not subscribed to the push service. You will not receive + updates from the server. Click the button below to subscribe! + + )} + + {permission === "prompt" || !subscription ? ( + + ) : ( + + + + + )} + + + ); +} + +function UnsubscribeButton() { + const theme = useTheme(); + const { mutate, isPending } = useMutation(unsubscribePushMutationOptions); + // TODO: error handling + + return ( + + ); +} + +function RequestPermissionAndSubscribeButton({ + requestPermission, +}: { + requestPermission?: () => Promise; +}) { + const theme = useTheme(); + const { mutate, isPending } = useMutation(subscribePushMutationOptions); + // TODO: error handling + + return ( + + ); +} + +function PushSettings({ subscription }: { subscription: PushSubscriptionReturn }) { + const theme = useTheme(); + const [open, setOpen] = useState(false); + + const { mutateAsync: updatePush } = useMutation(updatePushMutationOptions); + + const [subSettingsCopy, setSubSettingsCopy] = useState( + subscription.server.settings + ); + + return ( + <> + + + setOpen(false)} + title_icon={} + title="Configure Push Notifications" + > + + + Configure your push notification settings below. These settings + will be applied to your current device subscription only. + + + + + + + + + + + ); +} diff --git a/frontend/src/components/notifications/settings.tsx b/frontend/src/components/notifications/settings.tsx new file mode 100644 index 00000000..c3915ec4 --- /dev/null +++ b/frontend/src/components/notifications/settings.tsx @@ -0,0 +1,37 @@ +import { Checkbox, FormControl, FormControlLabel, FormHelperText } from "@mui/material"; + +import { SubscriptionSettings } from "@/pythonTypes"; + +/** Shared settings form for push and webhooks */ +export const NotificationsSettings = ({ + settings, + setSettings, +}: { + settings: SubscriptionSettings; + setSettings: (settings: SubscriptionSettings) => void; +}) => { + return ( + <> + + { + setSettings({ + ...settings, + is_active: e.target.checked, + }); + }} + /> + } + label="Enabled" + /> + + If enabled, the server will send notifications to your device. + + + + ); +}; diff --git a/frontend/src/components/notifications/useNotificationPermission.ts b/frontend/src/components/notifications/useNotificationPermission.ts new file mode 100644 index 00000000..594a5230 --- /dev/null +++ b/frontend/src/components/notifications/useNotificationPermission.ts @@ -0,0 +1,70 @@ +import { useCallback, useEffect, useState } from "react"; + +export function useNotificationPermission() { + const [available, setAvailable] = useState(true); + const [permission, setPermission] = useState("prompt"); + + useEffect(() => { + // Check if the Notifications API is available + if (!("Notification" in window)) { + setAvailable(false); + return; + } + const isSecureContext = window.isSecureContext; + if (!isSecureContext) { + console.log("Notifications are blocked: Must use HTTPS (or localhost)."); + setAvailable(false); + return; + } + }, []); + + useEffect(() => { + if (!("navigator" in window)) return; + + const abortController = new AbortController(); + + navigator.permissions + .query({ name: "notifications" }) + .then((result) => { + setPermission(result.state); + + // Listen for permission changes + result.addEventListener( + "change", + () => { + setPermission(result.state); + }, + { signal: abortController.signal } + ); + }) + .catch((error) => { + console.error("Failed to query notification permissions:", error); + setPermission("denied"); + }); + + return () => { + // Cleanup the event listener + abortController.abort(); + }; + }, []); + + const requestPermission = useCallback(async () => { + if (!available) { + throw new Error("Notifications are not available in this browser."); + } + + const permissionResult = await Notification.requestPermission(); + + if (permissionResult !== "granted") { + throw new Error("Notification permission was not granted."); + } + + setPermission(permissionResult); + }, [available]); + + return { + available, + permission, + requestPermission, + }; +} diff --git a/frontend/src/components/notifications/webhooks.tsx b/frontend/src/components/notifications/webhooks.tsx new file mode 100644 index 00000000..adb9b1de --- /dev/null +++ b/frontend/src/components/notifications/webhooks.tsx @@ -0,0 +1,589 @@ +/** This file includes all components for creating + * and managing webhooks in the notification system. + * + * Main Components: + * - WebhookList: Displays a list of existing webhooks. [exported] + * - AddButton: Button to add a new webhook. + * - * + * - DeleteIconButton: Button to delete a webhook. + * - ConfigIconButton: Button to configure a webhook. + * - * + * + * * - Editor: Component to edit or create a webhook. + */ + +import { + PlusIcon, + SaveIcon, + SettingsIcon, + Trash2Icon, + WebhookIcon, +} from "lucide-react"; +import { useState } from "react"; +import { + Box, + BoxProps, + Button, + ButtonProps, + DialogContent, + FormControl, + FormHelperText, + IconButton, + InputLabel, + MenuItem, + Select, + TextField, + TextFieldProps, + Typography, + useTheme, +} from "@mui/material"; +import { useInfiniteQuery, useMutation } from "@tanstack/react-query"; + +import { + deleteWebhookMutationOptions, + upsertWebhookMutationOptions, + webhooksInfiniteQueryOptions, + WebhookSubscribeRequest, +} from "@/api/notifications"; +import { WebhookSubscription } from "@/pythonTypes"; + +import { Dialog } from "../common/dialogs"; +import { isValidUrl } from "../common/strings"; + +export function WebHookList() { + // In theory we support paging, but in practice it is very unlikely that + // there will be more than a few webhooks, so we use an infinite query + const { data: webhooks } = useInfiniteQuery(webhooksInfiniteQueryOptions()); + + return ( + div:nth-of-type(odd)": { + background: `linear-gradient( + 90deg, + rgba(0, 0, 0, 0.01) 0%, + rgba(0, 0, 0, 0.2) 50%, + rgba(0, 0, 0, 0.01) 100% + )`, + }, + }} + > + + {webhooks && webhooks.length > 0 ? ( + <> + + Method + + + URL + + + Actions + + + ) : ( + + No webhooks configured. + + )} + + + {webhooks?.map((hook) => ( + + + {hook.method} + + + {hook.url} + + + + + + + ))} + + + + ); +} + +/** Button to add a new webhook + * + * This button opens a dialog to create a new webhook. + */ +function AddButton(props: ButtonProps) { + const theme = useTheme(); + const [open, setOpen] = useState(false); + const { mutateAsync: addWebhook } = useMutation(upsertWebhookMutationOptions); + + const [newHook, setNewHook] = useState({ + method: "POST", + url: "", + settings: null, + body: null, + headers: null, + params: null, + }); + + function resetNewHook() { + setNewHook({ + method: "POST", + url: "", + settings: null, + body: null, + headers: null, + params: null, + }); + } + + return ( + <> + + + { + if (reason === "backdropClick") return; + setOpen(false); + }} + title="New Webhook" + title_icon={} + > + + + + + + + + + + ); +} + +/** Button to delete a webhook + * + * This button opens a confirmation dialog to delete the webhook. + */ +function DeleteIconButton({ webhookId }: { webhookId: string }) { + const theme = useTheme(); + const [open, setOpen] = useState(false); + const { mutateAsync: deleteWebhook, isPending } = useMutation( + deleteWebhookMutationOptions + ); + + const handleClick = async (e: React.MouseEvent) => { + if (!e.shiftKey) { + e.preventDefault(); + setOpen(true); + return; + } + await deleteWebhook(webhookId); + setOpen(false); + }; + + return ( + <> + + + + setOpen(false)} + title="Delete webhook? " + title_icon={} + color="secondary" + > + + + Are you sure you want to delete this webhook? This action cannot + be undone. + + + + + + + + ); +} + +function SettingsIconButton({ webhook }: { webhook: WebhookSubscription }) { + const theme = useTheme(); + const [open, setOpen] = useState(false); + + const { mutateAsync: updateWebhook } = useMutation(upsertWebhookMutationOptions); + + const [webhookCopy, setWebhookCopy] = useState< + WebhookSubscription | WebhookSubscribeRequest + >(webhook); + + return ( + <> + setOpen(true)}> + + + + setOpen(false)} + title="Configure Webhook" + title_icon={} + > + + + + + + + + + + ); +} + +/** Allows to edit a webhook + * can also be used to create a new webhook. + */ +function Editor({ + webhook, + setWebhook, +}: { + webhook: WebhookSubscription | WebhookSubscribeRequest; + setWebhook: (webhook: WebhookSubscription | WebhookSubscribeRequest) => void; +}) { + const validUrl = isValidUrl(webhook.url); + + return ( + <> + + A webhook sends real-time data to a specified URL when an event occurs. + Configure it by selecting an accessible URL, choosing an HTTP method + (e.g., POST, GET), and adding optional headers for authentication or + metadata. Most use POST with a JSON payload, but follow your server's + API requirements. + + + + Endpoint + + + 0} + label="URL" + value={webhook.url} + onChange={(e) => + setWebhook({ ...webhook, url: e.target.value }) + } + placeholder="localhost:4000" + /> + + {!validUrl && webhook.url.length > 0 + ? "Please enter a valid URL." + : "The URL to send the webhook to. This should be an accessible URL that your server can reach."} + + + + Method + + + The HTTP method to use for the webhook. Most webhooks use POST, + but you can use any method that your server supports. + + + + + + Headers + + setWebhook({ ...webhook, headers: value })} + /> + + + + Settings + + {webhook.settings?.is_active} + + + ); +} + +/* --------------------------------- Helpers -------------------------------- */ + +/** Two textfields that allow to set key value pairs of a dictionary */ +function RecordFieldsInput({ + value, + onChange, +}: { + value: Record | null; + onChange: (value: Record | null) => void; +}) { + const theme = useTheme(); + const [kv, setKv] = useState({ key: "", value: "" }); + + return ( + <> + {Object.entries(value || {}).map(([key, val], index) => ( + + + + ))} + setKv({ ...kv, key: e.target.value }), + }} + props2={{ + value: kv.value, + onChange: (e) => setKv({ ...kv, value: e.target.value }), + }} + > + + + + ); +} + +function JoinedTextInputs({ + props1, + props2, + children, + disabled = false, + ...props +}: { + props1?: TextFieldProps; + props2?: TextFieldProps; + disabled?: boolean; +} & BoxProps) { + return ( + + + + {children} + + ); +} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 02878478..57ed6644 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -127,3 +127,43 @@ if (!rootElement.innerHTML) { ); } + +// Service Worker registration + +if ( + "serviceWorker" in navigator && + (window.location.protocol === "https:" || window.location.hostname === "localhost") +) { + window.addEventListener("load", () => { + navigator.serviceWorker + .register( + import.meta.env.MODE === "production" + ? "/worker.js" + : "/dev-sw.js?dev-sw", + { + scope: "/", + type: "module", + } + ) + .then((registration) => { + console.log( + "[SW registration]", + "Successfully registered service worker:", + registration.scope, + registration + ); + }) + .catch((err) => { + console.error( + "[SW registration]", + "Service worker registration failed:", + err + ); + }); + }); +} else { + console.warn( + "[SW registration]", + "Service worker is not supported or not secure. Skipping registration." + ); +} diff --git a/frontend/src/pythonTypes.ts b/frontend/src/pythonTypes.ts index 10176db5..bec14f2b 100644 --- a/frontend/src/pythonTypes.ts +++ b/frontend/src/pythonTypes.ts @@ -6,6 +6,15 @@ export type File = FileSystemItem; +export interface WebhookSubscription extends Base { + url: string; + method: string; + headers: Record | null; + params: Record | null; + body: Record | null; + settings: SubscriptionSettings; +} + export interface SerializedSessionState { id: string; created_at: Date; @@ -29,23 +38,14 @@ export interface Search { search_album: null | string; } -export interface PushWebHook extends Base { - url: string; - method: string; - headers: Record | null; - params: Record | null; - body: Record | null; - settings: PushSettings; +export interface SubscriptionSettings extends Base { + is_active: boolean; } export interface PushSubscription extends Base { keys: Record; expiration_time: null | number; - settings: PushSettings; -} - -export interface PushSettings extends Base { - is_active: boolean; + settings: SubscriptionSettings; } export interface LibraryStats { @@ -96,6 +96,12 @@ export interface FileSystemUpdate { event: "file_system_update"; } +export interface Base { + id: string; + created_at: Date; + updated_at: Date; +} + export interface Archive extends FileSystemItem { is_album: boolean; } diff --git a/frontend/src/routeTree.gen.ts b/frontend/src/routeTree.gen.ts index 23652c60..8168e60a 100644 --- a/frontend/src/routeTree.gen.ts +++ b/frontend/src/routeTree.gen.ts @@ -16,6 +16,7 @@ import { Route as SessiondraftIndexImport } from './routes/sessiondraft/index' import { Route as InboxIndexImport } from './routes/inbox/index' import { Route as DebugIndexImport } from './routes/debug/index' import { Route as FrontpageIndexImport } from './routes/_frontpage/index' +import { Route as SettingsNotificationsImport } from './routes/settings/notifications' import { Route as LibrarySearchImport } from './routes/library/search' import { Route as DebugSortablemultiImport } from './routes/debug/sortable_multi' import { Route as DebugSortableImport } from './routes/debug/sortable' @@ -73,6 +74,12 @@ const FrontpageIndexRoute = FrontpageIndexImport.update({ getParentRoute: () => rootRoute, } as any) +const SettingsNotificationsRoute = SettingsNotificationsImport.update({ + id: '/settings/notifications', + path: '/settings/notifications', + getParentRoute: () => rootRoute, +} as any) + const LibrarySearchRoute = LibrarySearchImport.update({ id: '/library/search', path: '/library/search', @@ -272,6 +279,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof LibrarySearchImport parentRoute: typeof rootRoute } + '/settings/notifications': { + id: '/settings/notifications' + path: '/settings/notifications' + fullPath: '/settings/notifications' + preLoaderRoute: typeof SettingsNotificationsImport + parentRoute: typeof rootRoute + } '/_frontpage/': { id: '/_frontpage/' path: '/' @@ -500,6 +514,7 @@ export interface FileRoutesByFullPath { '/debug/sortable': typeof DebugSortableRoute '/debug/sortable_multi': typeof DebugSortablemultiRoute '/library/search': typeof LibrarySearchRoute + '/settings/notifications': typeof SettingsNotificationsRoute '/': typeof FrontpageIndexRoute '/debug': typeof DebugIndexRoute '/inbox': typeof InboxIndexRoute @@ -532,6 +547,7 @@ export interface FileRoutesByTo { '/debug/sortable': typeof DebugSortableRoute '/debug/sortable_multi': typeof DebugSortablemultiRoute '/library/search': typeof LibrarySearchRoute + '/settings/notifications': typeof SettingsNotificationsRoute '/': typeof FrontpageIndexRoute '/debug': typeof DebugIndexRoute '/inbox': typeof InboxIndexRoute @@ -563,6 +579,7 @@ export interface FileRoutesById { '/debug/sortable': typeof DebugSortableRoute '/debug/sortable_multi': typeof DebugSortablemultiRoute '/library/search': typeof LibrarySearchRoute + '/settings/notifications': typeof SettingsNotificationsRoute '/_frontpage/': typeof FrontpageIndexRoute '/debug/': typeof DebugIndexRoute '/inbox/': typeof InboxIndexRoute @@ -597,6 +614,7 @@ export interface FileRouteTypes { | '/debug/sortable' | '/debug/sortable_multi' | '/library/search' + | '/settings/notifications' | '/' | '/debug' | '/inbox' @@ -628,6 +646,7 @@ export interface FileRouteTypes { | '/debug/sortable' | '/debug/sortable_multi' | '/library/search' + | '/settings/notifications' | '/' | '/debug' | '/inbox' @@ -657,6 +676,7 @@ export interface FileRouteTypes { | '/debug/sortable' | '/debug/sortable_multi' | '/library/search' + | '/settings/notifications' | '/_frontpage/' | '/debug/' | '/inbox/' @@ -690,6 +710,7 @@ export interface RootRouteChildren { DebugSortableRoute: typeof DebugSortableRoute DebugSortablemultiRoute: typeof DebugSortablemultiRoute LibrarySearchRoute: typeof LibrarySearchRoute + SettingsNotificationsRoute: typeof SettingsNotificationsRoute FrontpageIndexRoute: typeof FrontpageIndexRoute DebugIndexRoute: typeof DebugIndexRoute InboxIndexRoute: typeof InboxIndexRoute @@ -715,6 +736,7 @@ const rootRouteChildren: RootRouteChildren = { DebugSortableRoute: DebugSortableRoute, DebugSortablemultiRoute: DebugSortablemultiRoute, LibrarySearchRoute: LibrarySearchRoute, + SettingsNotificationsRoute: SettingsNotificationsRoute, FrontpageIndexRoute: FrontpageIndexRoute, DebugIndexRoute: DebugIndexRoute, InboxIndexRoute: InboxIndexRoute, @@ -751,6 +773,7 @@ export const routeTree = rootRoute "/debug/sortable", "/debug/sortable_multi", "/library/search", + "/settings/notifications", "/_frontpage/", "/debug/", "/inbox/", @@ -787,6 +810,9 @@ export const routeTree = rootRoute "/library/search": { "filePath": "library/search.tsx" }, + "/settings/notifications": { + "filePath": "settings/notifications.tsx" + }, "/_frontpage/": { "filePath": "_frontpage/index.tsx" }, diff --git a/frontend/src/routes/_frontpage/index.tsx b/frontend/src/routes/_frontpage/index.tsx index f7edf26e..d0d3979d 100644 --- a/frontend/src/routes/_frontpage/index.tsx +++ b/frontend/src/routes/_frontpage/index.tsx @@ -1,10 +1,11 @@ -import { BookOpenIcon, BugIcon, GithubIcon } from "lucide-react"; +import { BookOpenIcon, BugIcon, GithubIcon, SettingsIcon } from "lucide-react"; import { Box, Link, Typography, useTheme } from "@mui/material"; import { useSuspenseQuery } from "@tanstack/react-query"; import { createFileRoute } from "@tanstack/react-router"; import { inboxStatsQueryOptions } from "@/api/inbox"; import { libraryStatsQueryOptions } from "@/api/library"; +import { Link as TanLink } from "@/components/common/link"; import { PageWrapper } from "@/components/common/page"; import { InboxStatsCard, LibraryStatsCard } from "@/components/frontpage/statsCard"; @@ -189,6 +190,10 @@ function Footer() { {versionString} © 2025 P. Spitzner & S. Mohr + + + + + + Notifications Settings + + + Configure how your self-hosted beets-flask instance can send + notifications to external systems. This includes webhooks for real-time + updates and push notifications for browser alerts. + + + + + + + ); +} + +function WebhooksCard() { + const theme = useTheme(); + return ( + + + + + + Webhooks + + + + Webhooks let your self-hosted beets-flask instance notify external + systems immediately when key events occur, like new tags or imported + items. Instead of polling, you can push structured data to your own + services or infrastructure in real time. + + + + + ); +} + +function PushCard() { + const theme = useTheme(); + return ( + + + + + + Push + + + + Push notifications allow your self-hosted beets-flask instance to + send real-time alerts directly to your browser, keeping you updated + on important events like new imports or tag changes. + + + + + ); +} diff --git a/frontend/src/worker.ts b/frontend/src/worker.ts new file mode 100644 index 00000000..6d2c3471 --- /dev/null +++ b/frontend/src/worker.ts @@ -0,0 +1,169 @@ +/** Service worker. injected with vite-pwa-plugin */ +/// + +import { assertUnreachable } from "./components/common/debugging/typing"; +import { TaggedNotification } from "./pythonTypes"; + +// Trickery to get the correct type for the service worker global scope +const worker: ServiceWorkerGlobalScope = self as unknown as ServiceWorkerGlobalScope; + +type PushData = TaggedNotification; + +// Typing is not up to date, e.g. actions are missing +interface NotificationOptionsEx extends NotificationOptions { + actions?: { + action: string; + title: string; + icon: string; + type?: string; // Optional, but can be used to specify the type of action + }[]; +} + +function pushDataToNotificationOptions( + data: PushData +): [string, NotificationOptionsEx] { + switch (data.type) { + case "tagged": + return [ + `Tagging '${data.path.replace(data.inboxPath || "", "...")}' completed!`, + { + body: `Found ${data.nCandidates} candidates\n\tbest: ${data.bestCandidate} (${Math.round(data.bestCandidateMatch * 100)}%)`, + tag: data.hash, // Use the hash as the tag for uniqueness we can update the notification later if there + //an import + icon: "/logo_flask.svg", + actions: [ + { + action: "open-folder", + title: "Review tagged folder", + icon: "/tag.svg", + }, + ], + }, + ]; + default: + return assertUnreachable(data.type); + } +} +/** Handles if we receive push notifications. + * This should be called if our server sends a push message. + */ +worker.addEventListener("push", (event) => { + const data: PushData | null = (event.data?.json() as PushData) ?? null; + + if (!data) { + // This should not happen, but if it does, we log a warning and show a generic notification + console.warn("Push message without data", event); + return worker.registration.showNotification("Huh?"); + } + + const args = pushDataToNotificationOptions(data); + args[1].data = data; // Store the data in the notification for later use + + event.waitUntil( + isClientFocused().then((clientIsFocused) => { + // If the client is focused, we don't need to show a notification + if (clientIsFocused) { + console.debug("Client is focused, not showing notification."); + return; + } else { + return worker.registration.showNotification(...args); + } + }) + ); +}); + +/** Handles notification clicks. + * Called if the user clicks on a notification. + */ +worker.addEventListener("notificationclick", (event) => { + const data = event.notification.data as PushData | null; + const clickedNotification = event.notification; + + if (!data) { + console.warn("Notification click without data", event); + clickedNotification.close(); + return; + } + + if (event.action == "open-folder") { + event.waitUntil(openInboxFolder(data.path, data.hash)); + } else if (event.action == "open-inbox") { + event.waitUntil(openUrl("/inbox")); + } else { + // If no action is specified, we just open the homepage + event.waitUntil(openUrl("/")); + } + + clickedNotification.close(); +}); + +/* --------------------------------- helpers -------------------------------- */ + +/** Open the inbox details for a specific + * folder path and hash in the browser. + */ +async function openInboxFolder(path: string, hash: string) { + const allClients = await worker.clients.matchAll({ + type: "window", + }); + + // See if we already have a client open that can handle the request + const clientMatch = allClients.find((client) => { + const url = new URL(client.url); + return ( + url.pathname.startsWith("/inbox/folder/") && + url.pathname.includes(encodeURIComponent(path)) + ); + }); + if (clientMatch) { + await clientMatch.focus(); + return; + } + + // If we don't have a client open, let's open a new one + await worker.clients.openWindow( + `/inbox/folder/${encodeURIComponent(path)}/${encodeURIComponent(hash)}` + ); +} + +async function openUrl(url: string) { + const allClients = await worker.clients.matchAll({ + type: "window", + }); + + // See if we already have a client open that can handle the request + const clientMatch = allClients.find((client) => { + const clientUrl = new URL(client.url); + return clientUrl.pathname === url; + }); + + if (clientMatch) { + // If we found a matching client, focus it + await clientMatch.focus(); + } else { + // If not, open a new window with the URL + await worker.clients.openWindow(url); + } +} + +/** Checks if a client is currently focused */ +function isClientFocused() { + return worker.clients + .matchAll({ + type: "window", + includeUncontrolled: true, + }) + .then((windowClients) => { + let clientIsFocused = false; + + for (let i = 0; i < windowClients.length; i++) { + const windowClient = windowClients[i]; + if (windowClient.focused) { + clientIsFocused = true; + break; + } + } + + return clientIsFocused; + }); +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index d80f0f17..dbcdb485 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -4,6 +4,7 @@ import reactDev from "@vitejs/plugin-react-swc"; import { TanStackRouterVite } from "@tanstack/router-vite-plugin"; import tsconfigPaths from "vite-tsconfig-paths"; import svgr from "vite-plugin-svgr"; +import { VitePWA } from "vite-plugin-pwa"; const ReactCompilerConfig = { target: "19", // '17' | '18' | '19' @@ -28,6 +29,21 @@ export default defineConfig(({ mode }) => { }) : reactDev(), svgr(), + // Service worker registration + VitePWA({ + srcDir: "src", + filename: "worker.ts", + strategies: "injectManifest", + injectRegister: null, // we do it manually + manifest: false, + injectManifest: { + injectionPoint: undefined, + }, + devOptions: { + enabled: true, + type: "module", + }, + }), ], // not minifying helped when debugging in production mode // we can enable this again when the code base is a bit more mature. From ebea0af0324fbace9f46e969077adcbfa330c695 Mon Sep 17 00:00:00 2001 From: Sebastian Mohr Date: Thu, 31 Jul 2025 23:07:57 +0200 Subject: [PATCH 5/8] Generalized notification display and added test routes for webhooks and push notifications (wip). --- .../beets_flask/database/models/__init__.py | 2 +- .../models/{push.py => notifications.py} | 11 +- .../server/routes/db_models/push.py | 48 ++-- .../server/routes/notifications.py | 254 +++++++++++++++--- backend/generate_types.py | 22 +- frontend/eslint.config.js | 2 +- frontend/src/api/notifications.ts | 29 +- frontend/src/components/common/strings.tsx | 2 +- .../src/components/notifications/push.tsx | 2 +- .../src/components/notifications/settings.tsx | 2 +- .../src/components/notifications/webhooks.tsx | 56 +++- .../{pythonTypes.ts => pythonTypes/index.ts} | 25 -- frontend/src/pythonTypes/notifications.ts | 83 ++++++ frontend/src/worker.ts | 87 +++--- 14 files changed, 460 insertions(+), 165 deletions(-) rename backend/beets_flask/database/models/{push.py => notifications.py} (94%) rename frontend/src/{pythonTypes.ts => pythonTypes/index.ts} (92%) create mode 100644 frontend/src/pythonTypes/notifications.ts diff --git a/backend/beets_flask/database/models/__init__.py b/backend/beets_flask/database/models/__init__.py index db4f7937..fa21ff0f 100644 --- a/backend/beets_flask/database/models/__init__.py +++ b/backend/beets_flask/database/models/__init__.py @@ -1,5 +1,5 @@ from .base import Base -from .push import SubscriptionSettings, PushSubscription, WebhookSubscription +from .notifications import PushSubscription, SubscriptionSettings, WebhookSubscription from .states import CandidateStateInDb, FolderInDb, SessionStateInDb, TaskStateInDb __all__ = [ diff --git a/backend/beets_flask/database/models/push.py b/backend/beets_flask/database/models/notifications.py similarity index 94% rename from backend/beets_flask/database/models/push.py rename to backend/beets_flask/database/models/notifications.py index 12ddfc6b..aabfc742 100644 --- a/backend/beets_flask/database/models/push.py +++ b/backend/beets_flask/database/models/notifications.py @@ -1,6 +1,7 @@ from __future__ import annotations -from typing import Any, Mapping +from enum import Enum +from typing import Any, Literal, Mapping from sqlalchemy import ForeignKey from sqlalchemy.orm import Mapped, mapped_column, relationship @@ -87,6 +88,10 @@ def to_dict(self) -> Mapping: } +class WebhookType(Enum): + WEBPUSH = 0 + + class WebhookSubscription(Base): """Webhook handlers for push notifications. @@ -96,7 +101,8 @@ class WebhookSubscription(Base): __tablename__ = "push_webhooks" - # Rquired fields for push webhooks + type: Mapped[WebhookType] + # Required fields for push webhooks url: Mapped[str] method: Mapped[str] # e.g., "POST", "GET" @@ -122,6 +128,7 @@ def __init__( ): """Initialize a PushWebHooks instance.""" super().__init__() + self.type = WebhookType.WEBPUSH self.url = url self.method = method self.headers = headers diff --git a/backend/beets_flask/server/routes/db_models/push.py b/backend/beets_flask/server/routes/db_models/push.py index 7b172a57..8d95fe60 100644 --- a/backend/beets_flask/server/routes/db_models/push.py +++ b/backend/beets_flask/server/routes/db_models/push.py @@ -4,8 +4,8 @@ from beets_flask.database import db_session_factory from beets_flask.database.models import ( - SubscriptionSettings, PushSubscription, + SubscriptionSettings, WebhookSubscription, ) from beets_flask.server.routes.exception import InvalidUsageException @@ -26,21 +26,10 @@ def _register_routes(self): async def upsert(self): """Upsert a push webhook.""" params = await request.get_json() - id, url, method, headers, params, body, settings = self._parse_webhook_params( + id, url, method, headers, params, body, settings = self.parse_webhook_params( params ) - if not url: - raise InvalidUsageException( - "Missing 'url' parameter in webhook subscription", - status_code=400, - ) - if not method: - raise InvalidUsageException( - "Missing 'method' parameter in webhook subscription", - status_code=400, - ) - # Upsert webhook with db_session_factory() as db_session: if id: @@ -76,7 +65,8 @@ async def upsert(self): return web_hook.to_dict(), 201 - def _parse_webhook_params(self, params: Any): + @staticmethod + def parse_webhook_params(params: Any): """Parse the parameters for the webhook subscription.""" if not isinstance(params, dict): raise InvalidUsageException( @@ -97,6 +87,17 @@ def _parse_webhook_params(self, params: Any): # Settings for the webhook settings = pop_query_param(params, "settings", dict, default=None) + if not url: + raise InvalidUsageException( + "Missing 'url' parameter in webhook subscription", + status_code=400, + ) + if not method: + raise InvalidUsageException( + "Missing 'method' parameter in webhook subscription", + status_code=400, + ) + return ( id, url, @@ -108,7 +109,7 @@ def _parse_webhook_params(self, params: Any): ) -class SubscriptionBlueprint(ModelAPIBlueprint[PushSubscription]): +class PushBlueprint(ModelAPIBlueprint[PushSubscription]): def __init__(self): super().__init__(PushSubscription, url_prefix="/subscription") @@ -120,16 +121,10 @@ def _register_routes(self): async def upsert(self): """Upsert a push subscription.""" params = await request.get_json() - endpoint, expiration_time, keys, settings = self._parse_subscription_params( + endpoint, expiration_time, keys, settings = self.parse_subscription_params( params ) - if not endpoint or not keys: - raise InvalidUsageException( - "Missing 'endpoint' or 'keys' parameter in subscription", - status_code=400, - ) - # Upsert subscription (id == endpoint) with db_session_factory() as db_session: if subscription := PushSubscription.get_by( @@ -154,7 +149,8 @@ async def upsert(self): db_session.commit() return subscription.to_dict(), 201 - def _parse_subscription_params(self, params: Any): + @staticmethod + def parse_subscription_params(params: Any): """Parse the parameters for the push API.""" if not isinstance(params, dict): raise InvalidUsageException( @@ -176,6 +172,12 @@ def _parse_subscription_params(self, params: Any): status_code=400, ) + if not endpoint or not keys: + raise InvalidUsageException( + "Missing 'endpoint' or 'keys' parameter in subscription", + status_code=400, + ) + return ( endpoint, expiration_time, diff --git a/backend/beets_flask/server/routes/notifications.py b/backend/beets_flask/server/routes/notifications.py index 999eca17..0135f639 100644 --- a/backend/beets_flask/server/routes/notifications.py +++ b/backend/beets_flask/server/routes/notifications.py @@ -11,20 +11,25 @@ import base64 import json import select -from typing import Literal, NamedTuple, TypedDict +from typing import Literal, NamedTuple, NotRequired, TypedDict +import aiohttp import ecdsa from pywebpush import WebPushException, webpush from quart import Blueprint, Quart, request from beets_flask.config.beets_config import get_bf_config_dir from beets_flask.database import db_session_factory -from beets_flask.database.models import PushSubscription +from beets_flask.database.models import ( + PushSubscription, + SubscriptionSettings, + WebhookSubscription, +) from beets_flask.logger import log from beets_flask.server.routes.exception import InvalidUsageException from beets_flask.server.routes.inbox import get_inbox_for_path -from .db_models.push import SubscriptionBlueprint, WebHookBlueprint +from .db_models.push import PushBlueprint, WebHookBlueprint notification_bp = Blueprint("notifications", __name__, url_prefix="/notifications") @@ -94,50 +99,154 @@ def _generate_vapid_keypair(): def register_notifications(app: Blueprint | Quart): """Register the push models with the app.""" - notification_bp.register_blueprint(WebHookBlueprint().blueprint) - notification_bp.register_blueprint(SubscriptionBlueprint().blueprint) + webhook_bp = WebHookBlueprint() + webhook_bp.blueprint.route("/test", methods=["POST"])(test_webhook) + + push_bp = PushBlueprint() + push_bp.blueprint.route("/test", methods=["POST"])(test_push) + + notification_bp.register_blueprint(webhook_bp.blueprint) + notification_bp.register_blueprint(push_bp.blueprint) app.register_blueprint(notification_bp) -# ----------------------------------- Tests ---------------------------------- # +# ------------------------- Tests subscription -------------------------------- # + + +async def test_webhook(): + """Send a test notification a given webhook.""" + params = await request.get_json() + id, url, method, headers, params, body, settings = ( + WebHookBlueprint.parse_webhook_params(params) + ) + + test_subscriber = WebhookSubscription( + url=url, + method=method, + headers=headers, + params=params, + body=body, + settings=SubscriptionSettings.from_dict(settings or {}), + ) + + try: + await send_test_notification(test_subscriber) + log.info(f"Test notification sent to {url}") + except Exception as e: + log.exception(f"Failed to send test notification to {url}", e) + log.info(f"Failed to send test notification to {url}") + return ( + { + "status": "error", + "error": str(e), + }, + 200, + ) + return {"status": "ok"}, 200 -@notification_bp.route("/notify_test", methods=["POST"]) -async def notify_test(): - """Send a test notification to all subscribed clients.""" - params = await request.get_json() or {} - inbox = get_inbox_for_path( - "/music/inbox/to/album", +async def test_push(): + """Send a test notification to a given push subscription.""" + params = await request.get_json() + endpoint, expiration_time, keys, settings = PushBlueprint.parse_subscription_params( + params ) - # Create a test notification - notification = TaggedNotification( - hash="test_hash", - path="/music/inbox/to/album", - type="tagged", - nCandidates=10, - bestCandidate="Test Album - Test Artist", - bestCandidateMatch=0.95, - inboxPath=inbox["path"] if inbox else None, + + test_subscriber = PushSubscription( + id=endpoint, + keys=keys, + expiration_time=expiration_time, + settings=SubscriptionSettings.from_dict(settings or {}), ) - if len(params) > 0: + try: + await send_test_notification(test_subscriber) + log.info(f"Test notification sent to {endpoint}") + except ConnectionError as e: + log.exception(f"Failed to send test notification to {endpoint}", e) + log.info(f"Failed to send test notification to {endpoint}") raise InvalidUsageException( - "Invalid parameters provided for notification", - status_code=400, + f"Failed to send test notification to {endpoint}: {e}", + status_code=500, ) - # Push the notification - push_notification(notification) + return {"status": "ok"}, 200 + + +async def send_test_notification(subscription: PushSubscription | WebhookSubscription): + notification: PushNotification = PushNotification( + title="Test Notification", + options=PushNotificationOptions( + body="This is a test notification. If you see this, the subscription is working.", + renotify=True, + requireInteraction=True, + tag="test-notification", + actions=[ + PushAction( + action="default", + title="View home", + ), + PushAction( + action="open-inbox", + title="Open Inbox", + ), + ], + ), + ) - return "Test notification sent successfully", 200 + async with aiohttp.ClientSession() as session: + await send_notification( + notification=notification, subscription=subscription, session=session + ) + + +async def send_notification( + notification: PushNotification, + subscription: PushSubscription | WebhookSubscription, + session: aiohttp.ClientSession, +): + """Send a notification to a given subscription.""" + + if isinstance(subscription, PushSubscription): + # TODO: figure out async webpush + return + + # WebhookSubscription + # if we ever add other types, they should be handled here + + url = subscription.url + if not url.startswith(("http://", "https://")): + url = f"http://{url}" + + try: + response = await session.request( + method=subscription.method, + url=url, + headers=subscription.headers, + params=subscription.params, + json=notification, + ) + except aiohttp.ClientConnectorError as e: + original_exception = e.os_error if e.os_error else e + + # user friendly error message + user_friendly_error = f"Unable to connect to {url}. Please check the URL and your network connection." + + log.exception(f"Connection error", original_exception) + + raise Exception( + user_friendly_error, + ) from e + + response.raise_for_status() from sqlalchemy import select def push_notification( - notification: Notification, + notification: PushNotification, ): """Push a notification to all subscribed clients.""" vapid_keys = get_vapid_keypair() @@ -185,18 +294,89 @@ def push_notification( # ---------------------------- Notification types ---------------------------- # -class Notification(TypedDict): - """Notification for tagged albums.""" +class PushNotification(TypedDict): + """Notification for push subscriptions. + + Format is very similar to the Web Push API. + See https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration/showNotification + """ + + title: str + options: NotRequired[PushNotificationOptions] + - hash: str - path: str +class PushNotificationOptions(TypedDict): + """Options for the push notification. + As always the web sucks! Many of these options + are not supported by all browsers, especially + safari. As always fuck you apple! + """ -class TaggedNotification(Notification): + actions: NotRequired[list[PushAction]] + # list of actions for the notification + + badge: NotRequired[str] + # URL to the badge icon + + body: NotRequired[str] + # body text of the notification + + data: NotRequired[TaggedData] + # Data is used for additional features in beets-flask, + + icon: NotRequired[str] + # URL to the icon for the notification + + image: NotRequired[str] + # URL to an image for the notification + + renotify: NotRequired[bool] + # whether to renotify the user if the notification is already visible + + requireInteraction: NotRequired[bool] + # whether the notification should require user interaction + + tag: NotRequired[str] + # tag for the notification, used to replace existing notifications with the same tag + + vibrate: NotRequired[list[int]] + # vibration pattern for the notification + + +class PushAction(TypedDict): + """Action for the push notification. + + We support some custom actions for the notifications, + such as viewing an album or opening a folder or the inbox. + See worker.ts for the list of supported actions. + """ + + action: str + title: str + + icon: NotRequired[str] + # URL to the icon for the action + + +# Extra data send with the notification. + + +class TaggedData(TypedDict): """Notification for tagged albums with a type.""" type: Literal["tagged"] - nCandidates: int - bestCandidate: str # title - artist - bestCandidateMatch: float # match percentage (1.0 = 100%) - inboxPath: str | None # path to the inbox this folder is in + path: str # path to the folder with the tagged album/item + hash: str # hash of the album/item, used to identify it + + nCandidates: int # number of candidates found + bestCandidate: Candidate + + +class Candidate(TypedDict): + """Candidate for a tagged album.""" + + title: str + artist: str | None + match: float # match percentage (1.0 = 100%) + source: str | None # source of the candidate, e.g. "discogs", "musicbrainz" diff --git a/backend/generate_types.py b/backend/generate_types.py index b1f96e44..426d8b95 100644 --- a/backend/generate_types.py +++ b/backend/generate_types.py @@ -1,7 +1,10 @@ from py2ts.builder import TSBuilder +from beets_flask.database.models.notifications import ( + PushSubscription, + WebhookSubscription, +) from beets_flask.disk import Archive, File, FileSystemItem, Folder -from beets_flask.database.models.push import PushSubscription, WebhookSubscription from beets_flask.importer.states import ( SerializedSessionState, ) @@ -20,6 +23,7 @@ ItemResponseMinimal, ) from beets_flask.server.routes.library.stats import LibraryStats +from beets_flask.server.routes.notifications import PushNotification from beets_flask.server.websocket.status import ( FileSystemUpdate, FolderStatusUpdate, @@ -76,11 +80,19 @@ builder.add(FileSystemUpdate) +builder.save_file("../frontend/src/pythonTypes/index.ts") +print("✅ Typescript types generated successfully!") + + # ---------------------------- Push/Notifications ---------------------------- # -builder.add(PushSubscription, exclude={"settings_id"}) -builder.add(WebhookSubscription, exclude={"settings_id"}) +# We place all notification related types in another file to keep a cleaner structure +notification_builder = TSBuilder() +notification_builder.add(PushSubscription, exclude={"settings_id"}) +notification_builder.add(WebhookSubscription, exclude={"settings_id"}) +notification_builder.add(PushNotification) +notification_builder.add(InboxStats) -builder.save_file("../frontend/src/pythonTypes.ts") -print("✅ Typescript types generated successfully!") +notification_builder.save_file("../frontend/src/pythonTypes/notifications.ts") +print("✅ Notification types generated successfully!") diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js index 3c78e700..4dbf1f59 100644 --- a/frontend/eslint.config.js +++ b/frontend/eslint.config.js @@ -48,7 +48,7 @@ const tslint = { export default ts.config( // global ignores { - ignores: ["dist/", "node_modules/", "src/pythonTypes.d.ts", "dev-dist/"], + ignores: ["dist/", "node_modules/", "src/pythonTypes/*.ts", "dev-dist/"], }, // apply eslint to js files diff --git a/frontend/src/api/notifications.ts b/frontend/src/api/notifications.ts index d3fe4e89..34455ed8 100644 --- a/frontend/src/api/notifications.ts +++ b/frontend/src/api/notifications.ts @@ -8,7 +8,7 @@ import { PushSubscription as PyPushSubscription, SubscriptionSettings, WebhookSubscription, -} from "@/pythonTypes"; +} from "@/pythonTypes/notifications"; import { APIError, queryClient } from "./common"; @@ -22,7 +22,10 @@ export class PushSubscriptionError extends Error { /* -------------------------------- webhooks -------------------------------- */ export interface WebhookSubscribeRequest - extends Omit { + extends Omit< + WebhookSubscription, + "settings" | "id" | "created_at" | "updated_at" | "type" + > { settings: null | SubscriptionSettings; id?: string; // Optional ID for upsert } @@ -158,6 +161,28 @@ export const webhooksInfiniteQueryOptions = () => { }); }; +export const testWebhookMutationOptions: UseMutationOptions< + { status: "ok" } | { status: "error"; error: string }, + APIError | PushSubscriptionError, + WebhookSubscribeRequest +> = { + mutationKey: ["webhook", "test"], + mutationFn: async (data: WebhookSubscribeRequest | WebhookSubscription) => { + const response = await fetch(`/notifications/webhook/test`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(data), + }); + const result = (await response.json()) as + | { status: "ok" } + | { status: "error"; error: string }; + console.log("Test webhook result", result); + return result; + }, +}; + /* ------------------------------ web push api ------------------------------ */ export interface PushSubscriptionUpsertRequest diff --git a/frontend/src/components/common/strings.tsx b/frontend/src/components/common/strings.tsx index 73a4e181..c5663366 100644 --- a/frontend/src/components/common/strings.tsx +++ b/frontend/src/components/common/strings.tsx @@ -35,7 +35,7 @@ export function isValidUrl(str: string) { // Attempt to create a URL object from the string new URL(str); return true; - } catch (_err) { + } catch { // If an error is thrown, the string is not a valid URL return false; } diff --git a/frontend/src/components/notifications/push.tsx b/frontend/src/components/notifications/push.tsx index eb761d61..81af754c 100644 --- a/frontend/src/components/notifications/push.tsx +++ b/frontend/src/components/notifications/push.tsx @@ -27,7 +27,7 @@ import { unsubscribePushMutationOptions, updatePushMutationOptions, } from "@/api/notifications"; -import { SubscriptionSettings } from "@/pythonTypes"; +import { SubscriptionSettings } from "@/pythonTypes/notifications"; import { NotificationsSettings } from "./settings"; import { useNotificationPermission } from "./useNotificationPermission"; diff --git a/frontend/src/components/notifications/settings.tsx b/frontend/src/components/notifications/settings.tsx index c3915ec4..795ea0b6 100644 --- a/frontend/src/components/notifications/settings.tsx +++ b/frontend/src/components/notifications/settings.tsx @@ -1,6 +1,6 @@ import { Checkbox, FormControl, FormControlLabel, FormHelperText } from "@mui/material"; -import { SubscriptionSettings } from "@/pythonTypes"; +import { SubscriptionSettings } from "@/pythonTypes/notifications"; /** Shared settings form for push and webhooks */ export const NotificationsSettings = ({ diff --git a/frontend/src/components/notifications/webhooks.tsx b/frontend/src/components/notifications/webhooks.tsx index adb9b1de..c3ee7dde 100644 --- a/frontend/src/components/notifications/webhooks.tsx +++ b/frontend/src/components/notifications/webhooks.tsx @@ -19,7 +19,8 @@ import { Trash2Icon, WebhookIcon, } from "lucide-react"; -import { useState } from "react"; +import test from "node:test"; +import { useEffect, useState } from "react"; import { Box, BoxProps, @@ -41,11 +42,12 @@ import { useInfiniteQuery, useMutation } from "@tanstack/react-query"; import { deleteWebhookMutationOptions, + testWebhookMutationOptions, upsertWebhookMutationOptions, webhooksInfiniteQueryOptions, WebhookSubscribeRequest, } from "@/api/notifications"; -import { WebhookSubscription } from "@/pythonTypes"; +import { WebhookSubscription } from "@/pythonTypes/notifications"; import { Dialog } from "../common/dialogs"; import { isValidUrl } from "../common/strings"; @@ -224,7 +226,7 @@ function AddButton(props: ButtonProps) { justifyContent: "space-between", }} > - + + ); +} + function SettingsIconButton({ webhook }: { webhook: WebhookSubscription }) { const theme = useTheme(); const [open, setOpen] = useState(false); @@ -345,16 +384,7 @@ function SettingsIconButton({ webhook }: { webhook: WebhookSubscription }) { justifyContent: "space-between", }} > - +