diff --git a/k8s/base/kustomization.yaml b/k8s/base/kustomization.yaml
index ea5fa72..b0a67de 100644
--- a/k8s/base/kustomization.yaml
+++ b/k8s/base/kustomization.yaml
@@ -3,7 +3,5 @@ kind: Kustomization
resources:
- userbots.yaml
- - common-secret.yaml
+ - panel.yaml
- common-config.yaml
- - forust-secrets.yaml
- - anna-secrets.yaml
diff --git a/k8s/base/panel.yaml b/k8s/base/panel.yaml
new file mode 100644
index 0000000..4f738c8
--- /dev/null
+++ b/k8s/base/panel.yaml
@@ -0,0 +1,264 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: userbot
+---
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: userbot-panel
+ namespace: userbot
+---
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: userbot-runtime
+ namespace: userbot
+automountServiceAccountToken: false
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: userbot-common-config
+ namespace: userbot
+data:
+ DATABASE_TYPE: ""
+ DATABASE_NAME: ""
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+ name: userbot-panel
+ namespace: userbot
+rules:
+ - apiGroups:
+ - apps
+ resources:
+ - deployments
+ verbs:
+ - get
+ - list
+ - watch
+ - create
+ - patch
+ - delete
+ - apiGroups:
+ - apps
+ resources:
+ - deployments/scale
+ verbs:
+ - get
+ - patch
+ - update
+ - apiGroups:
+ - ""
+ resources:
+ - pods
+ verbs:
+ - get
+ - list
+ - watch
+ - apiGroups:
+ - ""
+ resources:
+ - pods/log
+ verbs:
+ - get
+ - apiGroups:
+ - ""
+ resources:
+ - persistentvolumeclaims
+ verbs:
+ - get
+ - list
+ - watch
+ - create
+ - delete
+ - apiGroups:
+ - ""
+ resources:
+ - secrets
+ verbs:
+ - get
+ - create
+ - delete
+ - apiGroups:
+ - ""
+ resources:
+ - configmaps
+ verbs:
+ - get
+ - apiGroups:
+ - metrics.k8s.io
+ resources:
+ - pods
+ verbs:
+ - get
+ - list
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+ name: userbot-panel
+ namespace: userbot
+subjects:
+ - kind: ServiceAccount
+ name: userbot-panel
+ namespace: userbot
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: Role
+ name: userbot-panel
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+ name: userbot-panel-legacy
+ namespace: default
+rules:
+ - apiGroups:
+ - apps
+ resources:
+ - deployments
+ verbs:
+ - get
+ - list
+ - watch
+ - patch
+ - apiGroups:
+ - apps
+ resources:
+ - deployments/scale
+ verbs:
+ - get
+ - patch
+ - update
+ - apiGroups:
+ - ""
+ resources:
+ - pods
+ verbs:
+ - get
+ - list
+ - watch
+ - apiGroups:
+ - ""
+ resources:
+ - pods/log
+ verbs:
+ - get
+ - apiGroups:
+ - ""
+ resources:
+ - persistentvolumeclaims
+ verbs:
+ - get
+ - list
+ - apiGroups:
+ - metrics.k8s.io
+ resources:
+ - pods
+ verbs:
+ - get
+ - list
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+ name: userbot-panel-legacy
+ namespace: default
+subjects:
+ - kind: ServiceAccount
+ name: userbot-panel
+ namespace: userbot
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: Role
+ name: userbot-panel-legacy
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: userbot-panel
+ namespace: userbot
+ labels:
+ app: userbot-panel
+spec:
+ replicas: 1
+ strategy:
+ type: Recreate
+ selector:
+ matchLabels:
+ app: userbot-panel
+ template:
+ metadata:
+ labels:
+ app: userbot-panel
+ spec:
+ serviceAccountName: userbot-panel
+ containers:
+ - name: userbot-panel
+ image: gcr.forust.xyz/forust/userbot-panel:latest
+ imagePullPolicy: Always
+ ports:
+ - name: http
+ containerPort: 8080
+ env:
+ - name: USERBOT_NAMESPACE
+ value: userbot
+ - name: USERBOT_LEGACY_NAMESPACES
+ value: default
+ - name: USERBOT_IMAGE
+ value: gcr.forust.xyz/forust/userbot:latest
+ - name: USERBOT_STORAGE_CLASS
+ value: local-path-retain
+ - name: USERBOT_DOWNLOADS_HOST_PATH
+ value: /srv/homelab/userbot/Downloads
+ resources:
+ requests:
+ cpu: 20m
+ memory: 96Mi
+ limits:
+ cpu: 300m
+ memory: 384Mi
+ readinessProbe:
+ httpGet:
+ path: /api/health
+ port: http
+ initialDelaySeconds: 3
+ periodSeconds: 10
+ livenessProbe:
+ httpGet:
+ path: /api/health
+ port: http
+ initialDelaySeconds: 10
+ periodSeconds: 30
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: userbot-panel
+ namespace: userbot
+spec:
+ type: ClusterIP
+ selector:
+ app: userbot-panel
+ ports:
+ - name: http
+ port: 8080
+ targetPort: http
+---
+apiVersion: traefik.io/v1alpha1
+kind: IngressRoute
+metadata:
+ name: userbot-panel-route
+ namespace: userbot
+spec:
+ entryPoints:
+ - websecure
+ routes:
+ - match: Host(`userbot.workstation.internal`)
+ kind: Rule
+ services:
+ - name: userbot-panel
+ port: 8080
diff --git a/k8s/base/userbots.yaml b/k8s/base/userbots.yaml
index 74618d9..95664c0 100644
--- a/k8s/base/userbots.yaml
+++ b/k8s/base/userbots.yaml
@@ -4,8 +4,14 @@ metadata:
name: forust-userbot-deployment
labels:
app: forust-userbot
+ app.kubernetes.io/name: userbot
+ app.kubernetes.io/instance: forust
+ annotations:
+ userbot.forust.xyz/display-name: forust
+ userbot.forust.xyz/legacy: "true"
+ userbot.forust.xyz/credentials-secret: userbot-forust-secrets
+ userbot.forust.xyz/pvc: forust-pvc
spec:
- replicas: 1
selector:
matchLabels:
app: forust-userbot
@@ -13,6 +19,8 @@ spec:
metadata:
labels:
app: forust-userbot
+ annotations:
+ kubectl.kubernetes.io/restartedAt: "2026-07-25T20:24:51+02:00"
spec:
containers:
- name: forust-userbot
@@ -33,6 +41,8 @@ spec:
- secretRef:
name: userbot-forust-secrets
volumeMounts:
+ - name: downloads
+ mountPath: /app/downloads
- name: forust-storage
mountPath: /app/data
subPath: data
@@ -40,6 +50,10 @@ spec:
mountPath: /app/logs
subPath: logs
volumes:
+ - name: downloads
+ hostPath:
+ path: /srv/homelab/userbot/Downloads
+ type: DirectoryOrCreate
- name: forust-storage
persistentVolumeClaim:
claimName: forust-pvc
@@ -62,8 +76,14 @@ metadata:
name: anna-userbot-deployment
labels:
app: anna-userbot
+ app.kubernetes.io/name: userbot
+ app.kubernetes.io/instance: anna
+ annotations:
+ userbot.forust.xyz/display-name: anna
+ userbot.forust.xyz/legacy: "true"
+ userbot.forust.xyz/credentials-secret: userbot-anna-secrets
+ userbot.forust.xyz/pvc: anna-pvc
spec:
- replicas: 1
selector:
matchLabels:
app: anna-userbot
@@ -71,6 +91,8 @@ spec:
metadata:
labels:
app: anna-userbot
+ annotations:
+ kubectl.kubernetes.io/restartedAt: "2026-07-25T20:24:51+02:00"
spec:
containers:
- name: anna-userbot
@@ -91,6 +113,8 @@ spec:
- secretRef:
name: userbot-anna-secrets
volumeMounts:
+ - name: downloads
+ mountPath: /app/downloads
- name: anna-storage
mountPath: /app/data
subPath: data
@@ -98,6 +122,10 @@ spec:
mountPath: /app/logs
subPath: logs
volumes:
+ - name: downloads
+ hostPath:
+ path: /srv/homelab/userbot/Downloads
+ type: DirectoryOrCreate
- name: anna-storage
persistentVolumeClaim:
claimName: anna-pvc
diff --git a/panel/.dockerignore b/panel/.dockerignore
new file mode 100644
index 0000000..225aa88
--- /dev/null
+++ b/panel/.dockerignore
@@ -0,0 +1,7 @@
+backend/.venv
+frontend/node_modules
+frontend/dist
+__pycache__
+.pytest_cache
+.ruff_cache
+*.pyc
diff --git a/panel/.gitignore b/panel/.gitignore
new file mode 100644
index 0000000..9674ad6
--- /dev/null
+++ b/panel/.gitignore
@@ -0,0 +1,7 @@
+backend/.venv/
+frontend/node_modules/
+frontend/dist/
+.pytest_cache/
+.ruff_cache/
+__pycache__/
+*.pyc
diff --git a/panel/Dockerfile b/panel/Dockerfile
new file mode 100644
index 0000000..ba66193
--- /dev/null
+++ b/panel/Dockerfile
@@ -0,0 +1,32 @@
+FROM node:22-alpine AS frontend
+
+WORKDIR /src
+COPY frontend/package*.json ./
+RUN npm ci
+COPY frontend/ ./
+RUN npm run build
+
+FROM python:3.13-slim AS python-builder
+
+WORKDIR /build
+COPY backend/requirements.txt ./
+RUN apt-get update \
+ && apt-get install -y --no-install-recommends build-essential \
+ && pip install --no-cache-dir --prefix=/install -r requirements.txt \
+ && rm -rf /var/lib/apt/lists/*
+
+FROM python:3.13-slim
+
+ENV PYTHONDONTWRITEBYTECODE=1 \
+ PYTHONUNBUFFERED=1 \
+ PANEL_STATIC_DIR=/app/static
+
+WORKDIR /app
+COPY --from=python-builder /install /usr/local
+COPY backend/app ./app
+COPY --from=frontend /src/dist ./static
+
+USER 65532:65532
+EXPOSE 8080
+
+CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8080"]
diff --git a/panel/README.md b/panel/README.md
new file mode 100644
index 0000000..f34349c
--- /dev/null
+++ b/panel/README.md
@@ -0,0 +1,43 @@
+# Userbot Control Panel
+
+Локальная панель управляет userbot-инстансами напрямую через Kubernetes API.
+Новые аккаунты создаются в namespace `userbot`; существующие `forust` и `anna`
+остаются в `default` и принимаются под управление только через metadata labels.
+
+## Первый запуск
+
+Скопировать общий Secret в новый namespace, не печатая его значения:
+
+```bash
+kubectl create namespace userbot --dry-run=client -o yaml | kubectl apply -f -
+kubectl get secret userbot-common-secrets -n default -o json \
+ | jq 'del(.metadata.annotations,.metadata.creationTimestamp,.metadata.resourceVersion,.metadata.uid,.metadata.managedFields) | .metadata.namespace = "userbot"' \
+ | kubectl apply -f -
+```
+
+Затем применить манифесты:
+
+```bash
+kubectl apply -f userbot/k8s/base/panel.yaml
+kubectl apply -f userbot/k8s/base/userbots.yaml
+kubectl rollout status deployment/userbot-panel -n userbot
+```
+
+Панель доступна только через внутренний Traefik route:
+`https://userbot.workstation.internal`.
+
+## Локальная разработка
+
+```bash
+cd userbot/panel/frontend
+npm install
+npm run dev
+```
+
+Backend использует in-cluster config, а вне кластера — текущий kubeconfig:
+
+```bash
+cd userbot/panel/backend
+uv run --with-requirements requirements-dev.txt \
+ uvicorn app.main:app --reload --port 8080
+```
diff --git a/panel/backend/app/__init__.py b/panel/backend/app/__init__.py
new file mode 100644
index 0000000..8fed5c4
--- /dev/null
+++ b/panel/backend/app/__init__.py
@@ -0,0 +1 @@
+"""Userbot Kubernetes control panel backend."""
diff --git a/panel/backend/app/auth_service.py b/panel/backend/app/auth_service.py
new file mode 100644
index 0000000..b820c0f
--- /dev/null
+++ b/panel/backend/app/auth_service.py
@@ -0,0 +1,202 @@
+from __future__ import annotations
+
+import asyncio
+import secrets
+from contextlib import suppress
+from dataclasses import dataclass
+from datetime import UTC, datetime, timedelta
+
+from pyrogram import Client
+from pyrogram.errors import (
+ ApiIdInvalid,
+ FloodWait,
+ PasswordHashInvalid,
+ PhoneCodeExpired,
+ PhoneCodeInvalid,
+ PhoneNumberInvalid,
+ RPCError,
+ SessionPasswordNeeded,
+ Unauthorized,
+)
+
+from .errors import PanelError
+from .models import AccountBase, PhoneStart, StringSessionStart
+
+
+@dataclass
+class AuthFlow:
+ flow_id: str
+ account: PhoneStart
+ client: Client
+ phone_code_hash: str
+ expires_at: datetime
+
+
+@dataclass
+class AuthorizedAccount:
+ account: AccountBase
+ session_string: str
+
+
+class TelegramAuthService:
+ def __init__(self, ttl_seconds: int = 600) -> None:
+ self.ttl = timedelta(seconds=ttl_seconds)
+ self.flows: dict[str, AuthFlow] = {}
+ self._lock = asyncio.Lock()
+
+ async def start_phone(self, account: PhoneStart) -> str:
+ await self._cleanup_expired()
+ flow_id = secrets.token_urlsafe(24)
+ telegram = Client(
+ f"auth-{flow_id}",
+ api_id=account.api_id,
+ api_hash=account.api_hash,
+ in_memory=True,
+ no_updates=True,
+ )
+ try:
+ await telegram.connect()
+ sent = await telegram.send_code(account.phone)
+ except Exception as exc:
+ await self._disconnect(telegram)
+ raise self._translate(exc) from exc
+
+ flow = AuthFlow(
+ flow_id=flow_id,
+ account=account,
+ client=telegram,
+ phone_code_hash=sent.phone_code_hash,
+ expires_at=datetime.now(UTC) + self.ttl,
+ )
+ async with self._lock:
+ self.flows[flow_id] = flow
+ return flow_id
+
+ async def submit_code(
+ self,
+ flow_id: str,
+ code: str,
+ ) -> AuthorizedAccount | None:
+ flow = await self._get(flow_id)
+ try:
+ await flow.client.sign_in(
+ flow.account.phone,
+ flow.phone_code_hash,
+ code,
+ )
+ except SessionPasswordNeeded:
+ return None
+ except PhoneCodeExpired as exc:
+ await self._discard(flow)
+ raise self._translate(exc) from exc
+ except Exception as exc:
+ raise self._translate(exc) from exc
+ return await self._finish(flow)
+
+ async def submit_password(
+ self,
+ flow_id: str,
+ password: str,
+ ) -> AuthorizedAccount:
+ flow = await self._get(flow_id)
+ try:
+ await flow.client.check_password(password)
+ except Exception as exc:
+ raise self._translate(exc) from exc
+ return await self._finish(flow)
+
+ async def validate_string_session(
+ self,
+ account: StringSessionStart,
+ ) -> AuthorizedAccount:
+ telegram = Client(
+ f"validate-{secrets.token_urlsafe(12)}",
+ api_id=account.api_id,
+ api_hash=account.api_hash,
+ session_string=account.session_string,
+ in_memory=True,
+ no_updates=True,
+ )
+ try:
+ await telegram.connect()
+ await telegram.get_me()
+ session_string = await telegram.export_session_string()
+ except Exception as exc:
+ raise self._translate(exc, session=True) from exc
+ finally:
+ await self._disconnect(telegram)
+ return AuthorizedAccount(account=account, session_string=session_string)
+
+ async def close(self) -> None:
+ async with self._lock:
+ flows = list(self.flows.values())
+ self.flows.clear()
+ await asyncio.gather(
+ *(self._disconnect(flow.client) for flow in flows),
+ return_exceptions=True,
+ )
+
+ async def _get(self, flow_id: str) -> AuthFlow:
+ await self._cleanup_expired()
+ async with self._lock:
+ flow = self.flows.get(flow_id)
+ if flow is None:
+ raise PanelError(410, "Authorization flow expired; start again")
+ return flow
+
+ async def _finish(self, flow: AuthFlow) -> AuthorizedAccount:
+ try:
+ await flow.client.get_me()
+ session_string = await flow.client.export_session_string()
+ except Exception as exc:
+ raise self._translate(exc) from exc
+ finally:
+ async with self._lock:
+ self.flows.pop(flow.flow_id, None)
+ await self._disconnect(flow.client)
+ return AuthorizedAccount(account=flow.account, session_string=session_string)
+
+ async def _discard(self, flow: AuthFlow) -> None:
+ async with self._lock:
+ self.flows.pop(flow.flow_id, None)
+ await self._disconnect(flow.client)
+
+ async def _cleanup_expired(self) -> None:
+ now = datetime.now(UTC)
+ async with self._lock:
+ expired = [
+ self.flows.pop(flow_id)
+ for flow_id, flow in list(self.flows.items())
+ if flow.expires_at <= now
+ ]
+ if expired:
+ await asyncio.gather(
+ *(self._disconnect(flow.client) for flow in expired),
+ return_exceptions=True,
+ )
+
+ @staticmethod
+ async def _disconnect(telegram: Client) -> None:
+ with suppress(Exception):
+ if telegram.is_connected:
+ await telegram.disconnect()
+
+ @staticmethod
+ def _translate(exc: Exception, *, session: bool = False) -> PanelError:
+ if isinstance(exc, FloodWait):
+ return PanelError(429, f"Telegram rate limit; retry in {exc.value} seconds")
+ if isinstance(exc, ApiIdInvalid):
+ return PanelError(422, "Telegram API ID or API Hash is invalid")
+ if isinstance(exc, PhoneNumberInvalid):
+ return PanelError(422, "Phone number is invalid")
+ if isinstance(exc, PhoneCodeInvalid):
+ return PanelError(422, "Telegram code is invalid")
+ if isinstance(exc, PhoneCodeExpired):
+ return PanelError(410, "Telegram code expired; start again")
+ if isinstance(exc, PasswordHashInvalid):
+ return PanelError(422, "2FA password is invalid")
+ if session and isinstance(exc, (Unauthorized, RPCError)):
+ return PanelError(422, "StringSession is invalid or expired")
+ if isinstance(exc, RPCError):
+ return PanelError(422, "Telegram rejected the authorization request")
+ return PanelError(503, "Telegram authorization is unavailable")
diff --git a/panel/backend/app/config.py b/panel/backend/app/config.py
new file mode 100644
index 0000000..c0b42b2
--- /dev/null
+++ b/panel/backend/app/config.py
@@ -0,0 +1,46 @@
+from __future__ import annotations
+
+import os
+from dataclasses import dataclass
+from pathlib import Path
+
+
+@dataclass(frozen=True)
+class Settings:
+ namespace: str = os.environ.get("USERBOT_NAMESPACE", "userbot")
+ legacy_namespaces: tuple[str, ...] = tuple(
+ value.strip()
+ for value in os.environ.get("USERBOT_LEGACY_NAMESPACES", "default").split(",")
+ if value.strip()
+ )
+ image: str = os.environ.get(
+ "USERBOT_IMAGE",
+ "gcr.forust.xyz/forust/userbot:latest",
+ )
+ common_secret: str = os.environ.get(
+ "USERBOT_COMMON_SECRET",
+ "userbot-common-secrets",
+ )
+ common_config: str = os.environ.get(
+ "USERBOT_COMMON_CONFIG",
+ "userbot-common-config",
+ )
+ storage_class: str = os.environ.get(
+ "USERBOT_STORAGE_CLASS",
+ "local-path-retain",
+ )
+ downloads_host_path: str = os.environ.get(
+ "USERBOT_DOWNLOADS_HOST_PATH",
+ "/srv/homelab/userbot/Downloads",
+ )
+ static_dir: Path = Path(os.environ.get("PANEL_STATIC_DIR", "/app/static"))
+ auth_ttl_seconds: int = int(os.environ.get("PANEL_AUTH_TTL_SECONDS", "600"))
+ default_storage: str = os.environ.get("USERBOT_DEFAULT_STORAGE", "1Gi")
+ default_cpu_limit: str = os.environ.get("USERBOT_DEFAULT_CPU_LIMIT", "300m")
+ default_memory_limit: str = os.environ.get(
+ "USERBOT_DEFAULT_MEMORY_LIMIT",
+ "1536Mi",
+ )
+
+
+settings = Settings()
diff --git a/panel/backend/app/errors.py b/panel/backend/app/errors.py
new file mode 100644
index 0000000..0a579ca
--- /dev/null
+++ b/panel/backend/app/errors.py
@@ -0,0 +1,5 @@
+class PanelError(Exception):
+ def __init__(self, status_code: int, detail: str) -> None:
+ super().__init__(detail)
+ self.status_code = status_code
+ self.detail = detail
diff --git a/panel/backend/app/kubernetes_service.py b/panel/backend/app/kubernetes_service.py
new file mode 100644
index 0000000..fde97d2
--- /dev/null
+++ b/panel/backend/app/kubernetes_service.py
@@ -0,0 +1,522 @@
+from __future__ import annotations
+
+from datetime import UTC, datetime
+from typing import Any
+
+from kubernetes import client, config
+from kubernetes.client.exceptions import ApiException
+
+from .config import Settings
+from .errors import PanelError
+from .models import AccountBase, InstanceSummary
+
+MANAGED_LABEL = "app.kubernetes.io/name=userbot"
+INSTANCE_LABEL = "app.kubernetes.io/instance"
+MANAGED_BY_LABEL = "app.kubernetes.io/managed-by"
+DISPLAY_ANNOTATION = "userbot.forust.xyz/display-name"
+LEGACY_ANNOTATION = "userbot.forust.xyz/legacy"
+CREDENTIALS_ANNOTATION = "userbot.forust.xyz/credentials-secret"
+PVC_ANNOTATION = "userbot.forust.xyz/pvc"
+RESTART_ANNOTATION = "userbot.forust.xyz/restarted-at"
+
+
+def _selector(labels: dict[str, str] | None) -> str:
+ return ",".join(f"{key}={value}" for key, value in (labels or {}).items())
+
+
+def _as_datetime(value: Any) -> datetime | None:
+ if value is None:
+ return None
+ if isinstance(value, datetime):
+ return value
+ return getattr(value, "replace", lambda **_: None)(tzinfo=UTC)
+
+
+class KubernetesService:
+ def __init__(
+ self,
+ settings: Settings,
+ *,
+ core: client.CoreV1Api | None = None,
+ apps: client.AppsV1Api | None = None,
+ custom: client.CustomObjectsApi | None = None,
+ ) -> None:
+ self.settings = settings
+ if core is None or apps is None:
+ try:
+ config.load_incluster_config()
+ except config.ConfigException:
+ config.load_kube_config()
+ self.core = core or client.CoreV1Api()
+ self.apps = apps or client.AppsV1Api()
+ self.custom = custom or client.CustomObjectsApi()
+
+ def ensure_prerequisites(self) -> None:
+ try:
+ self.core.read_namespaced_secret(
+ self.settings.common_secret,
+ self.settings.namespace,
+ )
+ self.core.read_namespaced_config_map(
+ self.settings.common_config,
+ self.settings.namespace,
+ )
+ except ApiException as exc:
+ if exc.status == 404:
+ raise PanelError(
+ 503,
+ "Userbot common Secret or ConfigMap is missing in the userbot namespace",
+ ) from exc
+ raise self._api_error(exc, "Could not verify userbot prerequisites") from exc
+
+ def list_instances(
+ self,
+ query: str = "",
+ status: str = "",
+ ) -> list[InstanceSummary]:
+ instances: list[InstanceSummary] = []
+ for namespace in (self.settings.namespace, *self.settings.legacy_namespaces):
+ try:
+ deployments = self.apps.list_namespaced_deployment(
+ namespace,
+ label_selector=MANAGED_LABEL,
+ ).items
+ except ApiException as exc:
+ raise self._api_error(exc, f"Could not list Deployments in {namespace}") from exc
+ instances.extend(self._summarize(namespace, deployment) for deployment in deployments)
+
+ query = query.strip().lower()
+ if query:
+ instances = [
+ item
+ for item in instances
+ if query in item.instance_id.lower()
+ or query in item.display_name.lower()
+ or query in (item.pod or "").lower()
+ ]
+ if status:
+ instances = [item for item in instances if item.status == status]
+ return sorted(instances, key=lambda item: (item.legacy, item.instance_id))
+
+ def get_instance(self, instance_id: str) -> InstanceSummary:
+ namespace, deployment = self._find_deployment(instance_id)
+ return self._summarize(namespace, deployment)
+
+ def assert_available(self, instance_id: str) -> None:
+ names = self._resource_names(instance_id)
+ checks = (
+ (self.apps.read_namespaced_deployment, names["deployment"], "Deployment"),
+ (self.core.read_namespaced_secret, names["secret"], "Secret"),
+ (self.core.read_namespaced_persistent_volume_claim, names["pvc"], "PVC"),
+ )
+ for read, name, kind in checks:
+ try:
+ read(name, self.settings.namespace)
+ except ApiException as exc:
+ if exc.status == 404:
+ continue
+ raise self._api_error(exc, f"Could not check {kind} {name}") from exc
+ raise PanelError(409, f"{kind} {name} already exists")
+
+ def provision(self, account: AccountBase, session_string: str) -> InstanceSummary:
+ self.ensure_prerequisites()
+ self.assert_available(account.instance_id)
+ names = self._resource_names(account.instance_id)
+ created: list[tuple[str, str]] = []
+ try:
+ self.core.create_namespaced_secret(
+ self.settings.namespace,
+ self._secret(account, session_string, names),
+ )
+ created.append(("secret", names["secret"]))
+ self.core.create_namespaced_persistent_volume_claim(
+ self.settings.namespace,
+ self._pvc(account, names),
+ )
+ created.append(("pvc", names["pvc"]))
+ self.apps.create_namespaced_deployment(
+ self.settings.namespace,
+ self._deployment(account, names),
+ )
+ created.append(("deployment", names["deployment"]))
+ except ApiException as exc:
+ self._rollback(created)
+ raise self._api_error(exc, "Could not create userbot instance") from exc
+ return self.get_instance(account.instance_id)
+
+ def scale(self, instance_id: str, replicas: int) -> InstanceSummary:
+ namespace, deployment = self._find_deployment(instance_id)
+ try:
+ self.apps.patch_namespaced_deployment_scale(
+ deployment.metadata.name,
+ namespace,
+ {"spec": {"replicas": replicas}},
+ )
+ except ApiException as exc:
+ raise self._api_error(exc, "Could not scale userbot instance") from exc
+ return self.get_instance(instance_id)
+
+ def restart(self, instance_id: str) -> InstanceSummary:
+ namespace, deployment = self._find_deployment(instance_id)
+ if (deployment.spec.replicas or 0) == 0:
+ raise PanelError(409, "Stopped instance cannot be restarted")
+ timestamp = datetime.now(UTC).isoformat()
+ try:
+ self.apps.patch_namespaced_deployment(
+ deployment.metadata.name,
+ namespace,
+ {
+ "spec": {
+ "template": {
+ "metadata": {
+ "annotations": {RESTART_ANNOTATION: timestamp},
+ }
+ }
+ }
+ },
+ )
+ except ApiException as exc:
+ raise self._api_error(exc, "Could not restart userbot instance") from exc
+ return self.get_instance(instance_id)
+
+ def logs(self, instance_id: str, tail: int = 250) -> str:
+ namespace, deployment = self._find_deployment(instance_id)
+ pods = self._pods_for_deployment(namespace, deployment)
+ if not pods:
+ raise PanelError(409, "Userbot Pod is not running")
+ pod = pods[0]
+ container = deployment.spec.template.spec.containers[0].name
+ try:
+ return self.core.read_namespaced_pod_log(
+ pod.metadata.name,
+ namespace,
+ container=container,
+ tail_lines=max(1, min(tail, 1000)),
+ timestamps=True,
+ )
+ except ApiException as exc:
+ raise self._api_error(exc, "Could not read userbot logs") from exc
+
+ def delete(self, instance_id: str, *, delete_data: bool) -> None:
+ namespace, deployment = self._find_deployment(instance_id)
+ annotations = deployment.metadata.annotations or {}
+ if annotations.get(LEGACY_ANNOTATION) == "true" or namespace != self.settings.namespace:
+ raise PanelError(409, "Legacy instances cannot be deleted from the panel")
+
+ names = self._resource_names(instance_id)
+ secret_name = annotations.get(CREDENTIALS_ANNOTATION, names["secret"])
+ pvc_name = annotations.get(PVC_ANNOTATION, names["pvc"])
+ operations = [
+ (
+ self.apps.delete_namespaced_deployment,
+ (deployment.metadata.name, namespace),
+ {"propagation_policy": "Foreground"},
+ ),
+ (self.core.delete_namespaced_secret, (secret_name, namespace), {}),
+ ]
+ if delete_data:
+ operations.append(
+ (
+ self.core.delete_namespaced_persistent_volume_claim,
+ (pvc_name, namespace),
+ {},
+ )
+ )
+ for delete_resource, args, kwargs in operations:
+ try:
+ delete_resource(*args, **kwargs)
+ except ApiException as exc:
+ if exc.status != 404:
+ raise self._api_error(exc, "Could not delete userbot instance") from exc
+
+ def _find_deployment(self, instance_id: str) -> tuple[str, Any]:
+ selector = f"{MANAGED_LABEL},{INSTANCE_LABEL}={instance_id}"
+ for namespace in (self.settings.namespace, *self.settings.legacy_namespaces):
+ try:
+ items = self.apps.list_namespaced_deployment(
+ namespace,
+ label_selector=selector,
+ ).items
+ except ApiException as exc:
+ raise self._api_error(exc, "Could not find userbot instance") from exc
+ if items:
+ return namespace, items[0]
+ raise PanelError(404, f"Instance {instance_id} does not exist")
+
+ def _summarize(self, namespace: str, deployment: Any) -> InstanceSummary:
+ labels = deployment.metadata.labels or {}
+ annotations = deployment.metadata.annotations or {}
+ instance_id = labels.get(INSTANCE_LABEL, deployment.metadata.name)
+ legacy = annotations.get(LEGACY_ANNOTATION) == "true"
+ pods = self._pods_for_deployment(namespace, deployment)
+ pod = pods[0] if pods else None
+ desired = deployment.spec.replicas or 0
+ ready = False
+ reason: str | None = None
+ restarts = 0
+ updated_at = deployment.metadata.creation_timestamp
+
+ if pod is not None:
+ statuses = pod.status.container_statuses or []
+ ready = bool(statuses) and all(item.ready for item in statuses)
+ restarts = sum(item.restart_count for item in statuses)
+ for item in statuses:
+ state = item.state
+ if state and state.waiting and state.waiting.reason:
+ reason = state.waiting.reason
+ break
+ if state and state.terminated and state.terminated.reason:
+ reason = state.terminated.reason
+ break
+ updated_at = pod.status.start_time or pod.metadata.creation_timestamp
+
+ if desired == 0:
+ status = "stopped"
+ elif reason in {
+ "CrashLoopBackOff",
+ "Error",
+ "ImagePullBackOff",
+ "ErrImagePull",
+ "CreateContainerConfigError",
+ "RunContainerError",
+ } or (pod is not None and pod.status.phase == "Failed"):
+ status = "error"
+ elif ready and (deployment.status.available_replicas or 0) > 0:
+ status = "running"
+ else:
+ status = "pending"
+ reason = reason or (pod.status.phase if pod is not None else "Scheduling")
+
+ container_spec = deployment.spec.template.spec.containers[0]
+ limits = (container_spec.resources.limits or {}) if container_spec.resources else {}
+ pvc_name = annotations.get(PVC_ANNOTATION) or self._deployment_pvc(deployment)
+ storage = self._pvc_storage(namespace, pvc_name)
+ cpu_usage, memory_usage = self._pod_metrics(namespace, pod.metadata.name if pod else None)
+ return InstanceSummary(
+ instance_id=instance_id,
+ display_name=annotations.get(DISPLAY_ANNOTATION, instance_id),
+ namespace=namespace,
+ deployment=deployment.metadata.name,
+ pod=pod.metadata.name if pod else None,
+ status=status,
+ reason=reason,
+ ready=ready,
+ restarts=restarts,
+ pvc=pvc_name,
+ storage=storage,
+ image=container_spec.image,
+ cpu_limit=limits.get("cpu"),
+ memory_limit=limits.get("memory"),
+ cpu_usage=cpu_usage,
+ memory_usage=memory_usage,
+ updated_at=_as_datetime(updated_at),
+ legacy=legacy,
+ deletable=not legacy and namespace == self.settings.namespace,
+ )
+
+ def _pods_for_deployment(self, namespace: str, deployment: Any) -> list[Any]:
+ selector = _selector(deployment.spec.selector.match_labels)
+ try:
+ pods = self.core.list_namespaced_pod(
+ namespace,
+ label_selector=selector,
+ ).items
+ except ApiException as exc:
+ raise self._api_error(exc, "Could not list userbot Pods") from exc
+ return sorted(
+ pods,
+ key=lambda pod: pod.metadata.creation_timestamp or datetime.min.replace(tzinfo=UTC),
+ reverse=True,
+ )
+
+ def _pod_metrics(self, namespace: str, pod_name: str | None) -> tuple[str | None, str | None]:
+ if not pod_name:
+ return None, None
+ try:
+ metrics = self.custom.get_namespaced_custom_object(
+ "metrics.k8s.io",
+ "v1beta1",
+ namespace,
+ "pods",
+ pod_name,
+ )
+ except (ApiException, AttributeError):
+ return None, None
+ containers = metrics.get("containers", [])
+ cpu = containers[0].get("usage", {}).get("cpu") if containers else None
+ memory = containers[0].get("usage", {}).get("memory") if containers else None
+ return cpu, memory
+
+ def _pvc_storage(self, namespace: str, pvc_name: str | None) -> str | None:
+ if not pvc_name:
+ return None
+ try:
+ pvc = self.core.read_namespaced_persistent_volume_claim(pvc_name, namespace)
+ except ApiException:
+ return None
+ requests = pvc.spec.resources.requests or {}
+ return requests.get("storage")
+
+ @staticmethod
+ def _deployment_pvc(deployment: Any) -> str | None:
+ for volume in deployment.spec.template.spec.volumes or []:
+ if volume.persistent_volume_claim:
+ return volume.persistent_volume_claim.claim_name
+ return None
+
+ @staticmethod
+ def _resource_names(instance_id: str) -> dict[str, str]:
+ return {
+ "deployment": f"userbot-{instance_id}",
+ "secret": f"userbot-{instance_id}-credentials",
+ "pvc": f"userbot-{instance_id}-data",
+ }
+
+ def _metadata(
+ self,
+ account: AccountBase,
+ names: dict[str, str],
+ resource_name: str,
+ ) -> client.V1ObjectMeta:
+ return client.V1ObjectMeta(
+ name=resource_name,
+ namespace=self.settings.namespace,
+ labels={
+ "app.kubernetes.io/name": "userbot",
+ INSTANCE_LABEL: account.instance_id,
+ MANAGED_BY_LABEL: "userbot-panel",
+ },
+ annotations={
+ DISPLAY_ANNOTATION: account.display_name,
+ CREDENTIALS_ANNOTATION: names["secret"],
+ PVC_ANNOTATION: names["pvc"],
+ },
+ )
+
+ def _secret(
+ self,
+ account: AccountBase,
+ session_string: str,
+ names: dict[str, str],
+ ) -> client.V1Secret:
+ return client.V1Secret(
+ metadata=self._metadata(account, names, names["secret"]),
+ type="Opaque",
+ string_data={
+ "API_ID": str(account.api_id),
+ "API_HASH": account.api_hash,
+ "STRINGSESSION": session_string,
+ },
+ )
+
+ def _pvc(
+ self,
+ account: AccountBase,
+ names: dict[str, str],
+ ) -> client.V1PersistentVolumeClaim:
+ return client.V1PersistentVolumeClaim(
+ metadata=self._metadata(account, names, names["pvc"]),
+ spec=client.V1PersistentVolumeClaimSpec(
+ access_modes=["ReadWriteOnce"],
+ storage_class_name=self.settings.storage_class,
+ resources=client.V1VolumeResourceRequirements(
+ requests={"storage": account.resources.storage},
+ ),
+ ),
+ )
+
+ def _deployment(
+ self,
+ account: AccountBase,
+ names: dict[str, str],
+ ) -> client.V1Deployment:
+ pod_labels = {
+ "app.kubernetes.io/name": "userbot",
+ INSTANCE_LABEL: account.instance_id,
+ MANAGED_BY_LABEL: "userbot-panel",
+ }
+ container = client.V1Container(
+ name="userbot",
+ image=self.settings.image,
+ image_pull_policy="Always",
+ env_from=[
+ client.V1EnvFromSource(
+ secret_ref=client.V1SecretEnvSource(name=self.settings.common_secret)
+ ),
+ client.V1EnvFromSource(
+ config_map_ref=client.V1ConfigMapEnvSource(name=self.settings.common_config)
+ ),
+ client.V1EnvFromSource(
+ secret_ref=client.V1SecretEnvSource(name=names["secret"])
+ ),
+ ],
+ resources=client.V1ResourceRequirements(
+ requests={"cpu": "80m", "memory": "512Mi"},
+ limits={
+ "cpu": account.resources.cpu_limit,
+ "memory": account.resources.memory_limit,
+ },
+ ),
+ volume_mounts=[
+ client.V1VolumeMount(name="data", mount_path="/app/data"),
+ client.V1VolumeMount(name="downloads", mount_path="/app/downloads"),
+ ],
+ )
+ pod_spec = client.V1PodSpec(
+ service_account_name="userbot-runtime",
+ automount_service_account_token=False,
+ containers=[container],
+ termination_grace_period_seconds=30,
+ volumes=[
+ client.V1Volume(
+ name="data",
+ persistent_volume_claim=client.V1PersistentVolumeClaimVolumeSource(
+ claim_name=names["pvc"]
+ ),
+ ),
+ client.V1Volume(
+ name="downloads",
+ host_path=client.V1HostPathVolumeSource(
+ path=self.settings.downloads_host_path,
+ type="DirectoryOrCreate",
+ ),
+ ),
+ ],
+ )
+ return client.V1Deployment(
+ metadata=self._metadata(account, names, names["deployment"]),
+ spec=client.V1DeploymentSpec(
+ replicas=1,
+ strategy=client.V1DeploymentStrategy(type="Recreate"),
+ selector=client.V1LabelSelector(match_labels=pod_labels),
+ template=client.V1PodTemplateSpec(
+ metadata=client.V1ObjectMeta(labels=pod_labels),
+ spec=pod_spec,
+ ),
+ ),
+ )
+
+ def _rollback(self, created: list[tuple[str, str]]) -> None:
+ for kind, name in reversed(created):
+ try:
+ if kind == "deployment":
+ self.apps.delete_namespaced_deployment(name, self.settings.namespace)
+ elif kind == "pvc":
+ self.core.delete_namespaced_persistent_volume_claim(
+ name,
+ self.settings.namespace,
+ )
+ else:
+ self.core.delete_namespaced_secret(name, self.settings.namespace)
+ except ApiException:
+ pass
+
+ @staticmethod
+ def _api_error(exc: ApiException, detail: str) -> PanelError:
+ if exc.status == 403:
+ return PanelError(503, f"{detail}: Kubernetes RBAC denied the operation")
+ if exc.status == 409:
+ return PanelError(409, f"{detail}: resource conflict")
+ if exc.status == 404:
+ return PanelError(404, f"{detail}: resource not found")
+ return PanelError(503, detail)
diff --git a/panel/backend/app/main.py b/panel/backend/app/main.py
new file mode 100644
index 0000000..f077739
--- /dev/null
+++ b/panel/backend/app/main.py
@@ -0,0 +1,198 @@
+from __future__ import annotations
+
+from contextlib import asynccontextmanager
+from typing import Annotated
+
+from fastapi import FastAPI, Query, Request
+from fastapi.exceptions import RequestValidationError
+from fastapi.responses import FileResponse, JSONResponse, Response
+from fastapi.staticfiles import StaticFiles
+
+from .auth_service import TelegramAuthService
+from .config import settings
+from .errors import PanelError
+from .kubernetes_service import KubernetesService
+from .models import (
+ AuthResult,
+ CodeSubmit,
+ DeleteRequest,
+ InstanceSummary,
+ PasswordSubmit,
+ PhoneStart,
+ StringSessionStart,
+)
+
+
+@asynccontextmanager
+async def lifespan(app: FastAPI):
+ app.state.kubernetes = KubernetesService(settings)
+ app.state.telegram = TelegramAuthService(settings.auth_ttl_seconds)
+ yield
+ await app.state.telegram.close()
+
+
+app = FastAPI(
+ title="Userbot Kubernetes Control",
+ version="1.0.0",
+ lifespan=lifespan,
+)
+
+
+@app.exception_handler(PanelError)
+async def panel_error_handler(_request: Request, exc: PanelError) -> JSONResponse:
+ return JSONResponse(status_code=exc.status_code, content={"detail": exc.detail})
+
+
+@app.exception_handler(RequestValidationError)
+async def validation_error_handler(
+ _request: Request,
+ exc: RequestValidationError,
+) -> JSONResponse:
+ errors = [
+ {
+ "loc": error.get("loc", ()),
+ "msg": error.get("msg", "Invalid value"),
+ "type": error.get("type", "value_error"),
+ }
+ for error in exc.errors()
+ ]
+ return JSONResponse(status_code=422, content={"detail": errors})
+
+
+def kube(request: Request) -> KubernetesService:
+ return request.app.state.kubernetes
+
+
+def telegram(request: Request) -> TelegramAuthService:
+ return request.app.state.telegram
+
+
+@app.get("/api/health")
+def health(request: Request) -> dict[str, object]:
+ service = kube(request)
+ try:
+ service.apps.list_namespaced_deployment(
+ settings.namespace,
+ limit=1,
+ )
+ except Exception:
+ return {"ok": False, "kubernetes": False}
+ return {"ok": True, "kubernetes": True}
+
+
+@app.get("/api/instances", response_model=list[InstanceSummary])
+def list_instances(
+ request: Request,
+ query: str = "",
+ status: str = "",
+) -> list[InstanceSummary]:
+ return kube(request).list_instances(query=query, status=status)
+
+
+@app.get("/api/instances/{instance_id}", response_model=InstanceSummary)
+def get_instance(instance_id: str, request: Request) -> InstanceSummary:
+ return kube(request).get_instance(instance_id)
+
+
+@app.get("/api/instances/{instance_id}/logs")
+def get_logs(
+ instance_id: str,
+ request: Request,
+ tail: Annotated[int, Query(ge=1, le=1000)] = 250,
+) -> dict[str, str]:
+ return {"logs": kube(request).logs(instance_id, tail)}
+
+
+@app.post("/api/instances/{instance_id}/start", response_model=InstanceSummary)
+def start_instance(instance_id: str, request: Request) -> InstanceSummary:
+ return kube(request).scale(instance_id, 1)
+
+
+@app.post("/api/instances/{instance_id}/stop", response_model=InstanceSummary)
+def stop_instance(instance_id: str, request: Request) -> InstanceSummary:
+ return kube(request).scale(instance_id, 0)
+
+
+@app.post("/api/instances/{instance_id}/restart", response_model=InstanceSummary)
+def restart_instance(instance_id: str, request: Request) -> InstanceSummary:
+ return kube(request).restart(instance_id)
+
+
+@app.post("/api/instances/{instance_id}/delete", status_code=204)
+def delete_instance(
+ instance_id: str,
+ payload: DeleteRequest,
+ request: Request,
+) -> Response:
+ if payload.confirmation != instance_id:
+ raise PanelError(422, "Type the instance id exactly to confirm deletion")
+ kube(request).delete(instance_id, delete_data=payload.delete_data)
+ return Response(status_code=204)
+
+
+@app.post("/api/auth/phone/start", response_model=AuthResult)
+async def auth_phone_start(
+ payload: PhoneStart,
+ request: Request,
+) -> AuthResult:
+ service = kube(request)
+ service.ensure_prerequisites()
+ service.assert_available(payload.instance_id)
+ flow_id = await telegram(request).start_phone(payload)
+ return AuthResult(status="code_required", flow_id=flow_id)
+
+
+@app.post("/api/auth/phone/{flow_id}/code", response_model=AuthResult)
+async def auth_phone_code(
+ flow_id: str,
+ payload: CodeSubmit,
+ request: Request,
+) -> AuthResult:
+ authorized = await telegram(request).submit_code(flow_id, payload.code)
+ if authorized is None:
+ return AuthResult(status="password_required", flow_id=flow_id)
+ instance = kube(request).provision(authorized.account, authorized.session_string)
+ return AuthResult(status="ready", instance=instance)
+
+
+@app.post("/api/auth/phone/{flow_id}/password", response_model=AuthResult)
+async def auth_phone_password(
+ flow_id: str,
+ payload: PasswordSubmit,
+ request: Request,
+) -> AuthResult:
+ authorized = await telegram(request).submit_password(flow_id, payload.password)
+ instance = kube(request).provision(authorized.account, authorized.session_string)
+ return AuthResult(status="ready", instance=instance)
+
+
+@app.post("/api/auth/string-session", response_model=AuthResult)
+async def auth_string_session(
+ payload: StringSessionStart,
+ request: Request,
+) -> AuthResult:
+ service = kube(request)
+ service.ensure_prerequisites()
+ service.assert_available(payload.instance_id)
+ authorized = await telegram(request).validate_string_session(payload)
+ instance = service.provision(authorized.account, authorized.session_string)
+ return AuthResult(status="ready", instance=instance)
+
+
+static_dir = settings.static_dir
+assets_dir = static_dir / "assets"
+if assets_dir.exists():
+ app.mount("/assets", StaticFiles(directory=assets_dir), name="assets")
+
+
+@app.get("/", include_in_schema=False)
+def index() -> FileResponse:
+ return FileResponse(static_dir / "index.html")
+
+
+@app.get("/{path:path}", include_in_schema=False)
+def spa_fallback(path: str) -> FileResponse:
+ candidate = (static_dir / path).resolve()
+ if candidate.is_file() and static_dir.resolve() in candidate.parents:
+ return FileResponse(candidate)
+ return FileResponse(static_dir / "index.html")
diff --git a/panel/backend/app/models.py b/panel/backend/app/models.py
new file mode 100644
index 0000000..9a59d3e
--- /dev/null
+++ b/panel/backend/app/models.py
@@ -0,0 +1,89 @@
+from __future__ import annotations
+
+from datetime import datetime
+from typing import Literal
+
+from pydantic import BaseModel, Field, field_validator
+
+INSTANCE_PATTERN = r"^[a-z0-9](?:[a-z0-9-]{0,38}[a-z0-9])?$"
+
+
+class Resources(BaseModel):
+ storage: str = "1Gi"
+ cpu_limit: str = "300m"
+ memory_limit: str = "1536Mi"
+
+
+class AccountBase(BaseModel):
+ instance_id: str = Field(pattern=INSTANCE_PATTERN, max_length=40)
+ display_name: str = Field(min_length=1, max_length=80)
+ api_id: int = Field(gt=0)
+ api_hash: str = Field(min_length=16, max_length=128)
+ resources: Resources = Field(default_factory=Resources)
+
+ @field_validator("display_name", "api_hash")
+ @classmethod
+ def strip_text(cls, value: str) -> str:
+ return value.strip()
+
+
+class PhoneStart(AccountBase):
+ phone: str = Field(min_length=7, max_length=32)
+
+ @field_validator("phone")
+ @classmethod
+ def normalize_phone(cls, value: str) -> str:
+ value = value.strip()
+ if not value.startswith("+"):
+ raise ValueError("phone must use international format")
+ return value
+
+
+class StringSessionStart(AccountBase):
+ session_string: str = Field(min_length=32)
+
+
+class CodeSubmit(BaseModel):
+ code: str = Field(min_length=3, max_length=12)
+
+ @field_validator("code")
+ @classmethod
+ def normalize_code(cls, value: str) -> str:
+ return "".join(value.split())
+
+
+class PasswordSubmit(BaseModel):
+ password: str = Field(min_length=1, max_length=256)
+
+
+class DeleteRequest(BaseModel):
+ confirmation: str
+ delete_data: bool = False
+
+
+class InstanceSummary(BaseModel):
+ instance_id: str
+ display_name: str
+ namespace: str
+ deployment: str
+ pod: str | None = None
+ status: Literal["running", "stopped", "pending", "error"]
+ reason: str | None = None
+ ready: bool = False
+ restarts: int = 0
+ pvc: str | None = None
+ storage: str | None = None
+ image: str
+ cpu_limit: str | None = None
+ memory_limit: str | None = None
+ cpu_usage: str | None = None
+ memory_usage: str | None = None
+ updated_at: datetime | None = None
+ legacy: bool = False
+ deletable: bool = True
+
+
+class AuthResult(BaseModel):
+ status: Literal["code_required", "password_required", "ready"]
+ flow_id: str | None = None
+ instance: InstanceSummary | None = None
diff --git a/panel/backend/requirements-dev.txt b/panel/backend/requirements-dev.txt
new file mode 100644
index 0000000..eea8e35
--- /dev/null
+++ b/panel/backend/requirements-dev.txt
@@ -0,0 +1,4 @@
+-r requirements.txt
+httpx==0.28.1
+pytest==8.4.1
+pytest-asyncio==1.1.0
diff --git a/panel/backend/requirements.txt b/panel/backend/requirements.txt
new file mode 100644
index 0000000..c7fd098
--- /dev/null
+++ b/panel/backend/requirements.txt
@@ -0,0 +1,5 @@
+fastapi==0.115.12
+kubernetes==33.1.0
+pyrofork==2.3.68
+tgcrypto==1.2.5
+uvicorn[standard]==0.34.2
diff --git a/panel/backend/tests/test_auth_service.py b/panel/backend/tests/test_auth_service.py
new file mode 100644
index 0000000..a17af68
--- /dev/null
+++ b/panel/backend/tests/test_auth_service.py
@@ -0,0 +1,84 @@
+from types import SimpleNamespace
+
+import pytest
+from app.auth_service import TelegramAuthService
+from app.models import PhoneStart, StringSessionStart
+
+
+class FakeClient:
+ instances = []
+ requires_password = False
+
+ def __init__(self, *_args, **kwargs):
+ self.kwargs = kwargs
+ self.is_connected = False
+ self.disconnected = False
+ self.__class__.instances.append(self)
+
+ async def connect(self):
+ self.is_connected = True
+
+ async def disconnect(self):
+ self.is_connected = False
+ self.disconnected = True
+
+ async def send_code(self, _phone):
+ return SimpleNamespace(phone_code_hash="hash")
+
+ async def sign_in(self, *_args):
+ if self.requires_password:
+ from pyrogram.errors import SessionPasswordNeeded
+
+ raise SessionPasswordNeeded()
+
+ async def check_password(self, _password):
+ return None
+
+ async def get_me(self):
+ return SimpleNamespace(id=1)
+
+ async def export_session_string(self):
+ return "exported-session"
+
+
+def phone_payload() -> PhoneStart:
+ return PhoneStart(
+ instance_id="test",
+ display_name="Test",
+ api_id=123,
+ api_hash="0123456789abcdef",
+ phone="+421900000000",
+ )
+
+
+@pytest.mark.asyncio
+async def test_phone_code_success_closes_and_forgets_client(monkeypatch) -> None:
+ monkeypatch.setattr("app.auth_service.Client", FakeClient)
+ auth = TelegramAuthService()
+
+ flow_id = await auth.start_phone(phone_payload())
+ result = await auth.submit_code(flow_id, "12345")
+
+ assert result.session_string == "exported-session"
+ assert flow_id not in auth.flows
+ assert FakeClient.instances[-1].disconnected is True
+ assert FakeClient.instances[-1].kwargs["in_memory"] is True
+ assert FakeClient.instances[-1].kwargs["no_updates"] is True
+
+
+@pytest.mark.asyncio
+async def test_string_session_is_validated_and_closed(monkeypatch) -> None:
+ monkeypatch.setattr("app.auth_service.Client", FakeClient)
+ auth = TelegramAuthService()
+ payload = StringSessionStart(
+ instance_id="test",
+ display_name="Test",
+ api_id=123,
+ api_hash="0123456789abcdef",
+ session_string="x" * 64,
+ )
+
+ result = await auth.validate_string_session(payload)
+
+ assert result.session_string == "exported-session"
+ assert FakeClient.instances[-1].disconnected is True
diff --git a/panel/backend/tests/test_kubernetes_service.py b/panel/backend/tests/test_kubernetes_service.py
new file mode 100644
index 0000000..c782f41
--- /dev/null
+++ b/panel/backend/tests/test_kubernetes_service.py
@@ -0,0 +1,179 @@
+from datetime import UTC, datetime
+from types import SimpleNamespace
+from unittest.mock import Mock
+
+import pytest
+from app.config import Settings
+from app.errors import PanelError
+from app.kubernetes_service import KubernetesService
+from app.models import AccountBase
+from kubernetes.client.exceptions import ApiException
+
+
+def service() -> KubernetesService:
+ core = Mock()
+ apps = Mock()
+ custom = Mock()
+ custom.get_namespaced_custom_object.side_effect = ApiException(status=404)
+ return KubernetesService(Settings(), core=core, apps=apps, custom=custom)
+
+
+def account() -> AccountBase:
+ return AccountBase(
+ instance_id="test-account",
+ display_name="Test Account",
+ api_id=12345,
+ api_hash="0123456789abcdef0123456789abcdef",
+ )
+
+
+def test_renders_managed_resources_without_leaking_credentials() -> None:
+ kube = service()
+ names = kube._resource_names("test-account")
+ secret = kube._secret(account(), "SESSION", names)
+ pvc = kube._pvc(account(), names)
+ deployment = kube._deployment(account(), names)
+
+ assert secret.string_data == {
+ "API_ID": "12345",
+ "API_HASH": "0123456789abcdef0123456789abcdef",
+ "STRINGSESSION": "SESSION",
+ }
+ assert pvc.spec.storage_class_name == "local-path-retain"
+ assert pvc.spec.resources.requests["storage"] == "1Gi"
+ assert deployment.spec.strategy.type == "Recreate"
+ assert deployment.spec.replicas == 1
+ assert deployment.spec.template.spec.automount_service_account_token is False
+ assert deployment.spec.template.spec.service_account_name == "userbot-runtime"
+ assert deployment.spec.template.spec.volumes[1].host_path.path.endswith("/Downloads")
+ assert deployment.spec.template.spec.containers[0].resources.requests == {
+ "cpu": "80m",
+ "memory": "512Mi",
+ }
+
+
+def test_collision_is_reported_before_auth() -> None:
+ kube = service()
+ kube.apps.read_namespaced_deployment.return_value = object()
+
+ with pytest.raises(PanelError) as error:
+ kube.assert_available("test-account")
+
+ assert error.value.status_code == 409
+ assert "Deployment" in error.value.detail
+
+
+def test_partial_provision_rolls_back_only_created_resources() -> None:
+ kube = service()
+ kube.ensure_prerequisites = Mock()
+ kube.assert_available = Mock()
+ kube.core.create_namespaced_persistent_volume_claim.side_effect = ApiException(status=500)
+
+ with pytest.raises(PanelError):
+ kube.provision(account(), "SESSION")
+
+ kube.core.delete_namespaced_secret.assert_called_once_with(
+ "userbot-test-account-credentials",
+ "userbot",
+ )
+ kube.core.delete_namespaced_persistent_volume_claim.assert_not_called()
+ kube.apps.delete_namespaced_deployment.assert_not_called()
+
+
+def test_delete_retains_pvc_unless_explicitly_requested() -> None:
+ kube = service()
+ deployment = SimpleNamespace(
+ metadata=SimpleNamespace(
+ name="userbot-test-account",
+ annotations={
+ "userbot.forust.xyz/credentials-secret": "credentials",
+ "userbot.forust.xyz/pvc": "data",
+ },
+ )
+ )
+ kube._find_deployment = Mock(return_value=("userbot", deployment))
+
+ kube.delete("test-account", delete_data=False)
+
+ kube.apps.delete_namespaced_deployment.assert_called_once()
+ kube.core.delete_namespaced_secret.assert_called_once_with("credentials", "userbot")
+ kube.core.delete_namespaced_persistent_volume_claim.assert_not_called()
+
+ kube.delete("test-account", delete_data=True)
+ kube.core.delete_namespaced_persistent_volume_claim.assert_called_once_with(
+ "data",
+ "userbot",
+ )
+
+
+def test_legacy_delete_is_blocked() -> None:
+ kube = service()
+ deployment = SimpleNamespace(
+ metadata=SimpleNamespace(
+ name="forust-userbot-deployment",
+ annotations={"userbot.forust.xyz/legacy": "true"},
+ )
+ )
+ kube._find_deployment = Mock(return_value=("default", deployment))
+
+ with pytest.raises(PanelError) as error:
+ kube.delete("forust", delete_data=False)
+
+ assert error.value.status_code == 409
+
+
+def pod(*, ready: bool, phase: str = "Running", reason: str | None = None):
+ waiting = SimpleNamespace(reason=reason) if reason else None
+ state = SimpleNamespace(waiting=waiting, terminated=None)
+ status = SimpleNamespace(
+ container_statuses=[
+ SimpleNamespace(ready=ready, restart_count=2, state=state),
+ ],
+ phase=phase,
+ start_time=datetime.now(UTC),
+ )
+ return SimpleNamespace(
+ metadata=SimpleNamespace(name="pod-1", creation_timestamp=datetime.now(UTC)),
+ status=status,
+ )
+
+
+def deployment(replicas: int = 1):
+ resources = SimpleNamespace(limits={"cpu": "300m", "memory": "1536Mi"})
+ container = SimpleNamespace(image="userbot:latest", resources=resources)
+ template_spec = SimpleNamespace(containers=[container], volumes=[])
+ return SimpleNamespace(
+ metadata=SimpleNamespace(
+ name="userbot-test-account",
+ labels={"app.kubernetes.io/instance": "test-account"},
+ annotations={},
+ creation_timestamp=datetime.now(UTC),
+ ),
+ spec=SimpleNamespace(
+ replicas=replicas,
+ selector=SimpleNamespace(match_labels={"app": "test"}),
+ template=SimpleNamespace(spec=template_spec),
+ ),
+ status=SimpleNamespace(available_replicas=1 if replicas else 0),
+ )
+
+
+@pytest.mark.parametrize(
+ ("replicas", "pod_value", "expected"),
+ [
+ (0, None, "stopped"),
+ (1, pod(ready=True), "running"),
+ (1, pod(ready=False, phase="Pending"), "pending"),
+ (1, pod(ready=False, reason="CrashLoopBackOff"), "error"),
+ (1, pod(ready=False, phase="Failed"), "error"),
+ ],
+)
+def test_status_classification(replicas, pod_value, expected) -> None:
+ kube = service()
+ kube._pods_for_deployment = Mock(return_value=[pod_value] if pod_value else [])
+ kube._pvc_storage = Mock(return_value=None)
+ kube._pod_metrics = Mock(return_value=(None, None))
+
+ result = kube._summarize("userbot", deployment(replicas))
+
+ assert result.status == expected
diff --git a/panel/backend/tests/test_models.py b/panel/backend/tests/test_models.py
new file mode 100644
index 0000000..7c7864e
--- /dev/null
+++ b/panel/backend/tests/test_models.py
@@ -0,0 +1,44 @@
+import pytest
+from app.main import validation_error_handler
+from app.models import AccountBase, DeleteRequest
+from fastapi.exceptions import RequestValidationError
+from pydantic import ValidationError
+
+
+@pytest.mark.parametrize(
+ "instance_id",
+ ["Upper", "has_space", "-leading", "trailing-", "x" * 41],
+)
+def test_invalid_instance_ids(instance_id: str) -> None:
+ with pytest.raises(ValidationError):
+ AccountBase(
+ instance_id=instance_id,
+ display_name="Test",
+ api_id=1,
+ api_hash="0123456789abcdef",
+ )
+
+
+def test_delete_data_defaults_to_false() -> None:
+ request = DeleteRequest(confirmation="test")
+ assert request.delete_data is False
+
+
+@pytest.mark.asyncio
+async def test_validation_response_does_not_echo_secret_input() -> None:
+ sensitive_value = "-".join(("very", "private", "string", "session"))
+ error = RequestValidationError(
+ [
+ {
+ "type": "string_too_short",
+ "loc": ("body", "session_string"),
+ "msg": "String should have at least 32 characters",
+ "input": sensitive_value,
+ }
+ ]
+ )
+
+ response = await validation_error_handler(None, error)
+
+ assert sensitive_value.encode() not in response.body
+ assert b'"input"' not in response.body
diff --git a/panel/frontend/index.html b/panel/frontend/index.html
new file mode 100644
index 0000000..6b909f0
--- /dev/null
+++ b/panel/frontend/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ Userbot · Kubernetes Control
+
+
+
+
+
+
diff --git a/panel/frontend/package-lock.json b/panel/frontend/package-lock.json
new file mode 100644
index 0000000..bf33df6
--- /dev/null
+++ b/panel/frontend/package-lock.json
@@ -0,0 +1,3970 @@
+{
+ "name": "userbot-panel",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "userbot-panel",
+ "version": "1.0.0",
+ "dependencies": {
+ "lucide-svelte": "^0.468.0",
+ "svelte": "^5.16.0"
+ },
+ "devDependencies": {
+ "@sveltejs/vite-plugin-svelte": "^5.0.0",
+ "@testing-library/jest-dom": "^6.6.3",
+ "@testing-library/svelte": "^5.2.6",
+ "@types/node": "^22.10.5",
+ "jsdom": "^25.0.1",
+ "svelte-check": "^4.7.3",
+ "typescript": "~5.7.2",
+ "vite": "^6.0.7",
+ "vitest": "^2.1.8"
+ }
+ },
+ "node_modules/@adobe/css-tools": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.5.0.tgz",
+ "integrity": "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@asamuzakjp/css-color": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz",
+ "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@csstools/css-calc": "^2.1.3",
+ "@csstools/css-color-parser": "^3.0.9",
+ "@csstools/css-parser-algorithms": "^3.0.4",
+ "@csstools/css-tokenizer": "^3.0.3",
+ "lru-cache": "^10.4.3"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
+ "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.29.7",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
+ "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/runtime": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz",
+ "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@csstools/color-helpers": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz",
+ "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT-0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@csstools/css-calc": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz",
+ "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@csstools/css-parser-algorithms": "^3.0.5",
+ "@csstools/css-tokenizer": "^3.0.4"
+ }
+ },
+ "node_modules/@csstools/css-color-parser": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz",
+ "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@csstools/color-helpers": "^5.1.0",
+ "@csstools/css-calc": "^2.1.4"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@csstools/css-parser-algorithms": "^3.0.5",
+ "@csstools/css-tokenizer": "^3.0.4"
+ }
+ },
+ "node_modules/@csstools/css-parser-algorithms": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz",
+ "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@csstools/css-tokenizer": "^3.0.4"
+ }
+ },
+ "node_modules/@csstools/css-tokenizer": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz",
+ "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
+ "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
+ "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
+ "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
+ "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
+ "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
+ "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
+ "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
+ "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
+ "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
+ "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
+ "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
+ "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
+ "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
+ "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
+ "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
+ "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
+ "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
+ "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
+ "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
+ "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
+ "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.62.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.3.tgz",
+ "integrity": "sha512-c0wdcekXtQvvn5Tsrk/+op/gUArrbWaFduBnTLP2l1cKLSQs4diMWjJw3m6A0DdzT8dAAX95KpkJ3qynCePbmw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.62.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.3.tgz",
+ "integrity": "sha512-3YjElDdWN+qXAFbJ/CzPV+0wspLqh54k/I6GfdYtEJRqg7buSgc1yPM3B+93j1M4neobtkATHZTmxK2AMVGfnA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.62.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.3.tgz",
+ "integrity": "sha512-Pch2pFNOxxz1hTjypIdPyRTR6riiwRl84+VcN9djS680fw+Co1nAJINrdpqp7KV0NvyuU8ilZXZCjd7ykJl1GQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.62.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.3.tgz",
+ "integrity": "sha512-LEuncFUHFiF8t4yZVZvvZA1wk0pjAscRnsrn1EfTEmN4HXotBi2YtcnLRyaK6UbuczW7xZS5ES+81Rdz8Z0T6g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.62.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.3.tgz",
+ "integrity": "sha512-zvBUvsQUpOWALdDsk6qbS8bXf2VxmPisuudNDrY7x0p0jBdsoZl8HsHczIOgkQiZldmcacMKtBzpoGVNeIe2bQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.62.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.3.tgz",
+ "integrity": "sha512-C2KmNrcSem/AMg984H/dev+si0lieQGdXdR/lYGJnuumXnFb9Y7QdiI62obFdLlxRYLBv4P0eUVIDbD4c1vVvw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.62.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.3.tgz",
+ "integrity": "sha512-ggXnsTAEzNQx74XpunRsiZ9aBZDsI7XIa0hm2nzR9f4WzH5/f/d73ZSDaC5ejJ8YLY4NW+V3wr0tjOaeCq8hqA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.62.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.3.tgz",
+ "integrity": "sha512-2vng+FlzNUhKZxtej3IUqJgbZoQk2M/dwQM20+ULV0R/E/8tr9/P6uEf2iiGIk4HL0zMKh5Jry7mUHdUOvyGgA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.62.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.3.tgz",
+ "integrity": "sha512-LLLFZKt4/Nraf9rxDkhiU8QVgLF4WmCkfr0L4fj0fPfIZFBib0DeiFk1hhaYKd03LFAFJcxHslhDFlNJLylf5Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.62.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.3.tgz",
+ "integrity": "sha512-WJkdQCvS9sWNOUBJZfQRKpZGFBztRzcowI+nndmflKgU4XY+3a420FgTOSKTsVqJbnzSxeT4vaJalpOaPo2YCQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.62.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.3.tgz",
+ "integrity": "sha512-PwHXCCS2n64/1Ot6rP1YEYA02MGYBcQlr8CSZZyrUG2O7NH6NklYmvr9v3Jy+5e/eDeNchc/ukmKJi9LuflMIQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.62.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.3.tgz",
+ "integrity": "sha512-vUjxINQu3RC8NZS3ykk1gN65gIz8pAopOq2HXuZhiIxHdx7TFvDG+jgrdSgInu1Eza4/Rfi2VzZgyIgEH4WOaw==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.62.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.3.tgz",
+ "integrity": "sha512-wzko4aJ13+0G3kGnviCg5gnXFKd40izKsrf2uOw12US4XqprkDrmwOpeW14aSNa37V8bfPcz5Fkob6LZ3BAPmA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.62.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.3.tgz",
+ "integrity": "sha512-8120ue0JUMSwy11stlwnfdX3pPd+WZYGCDBwEHWtIHi6pOpZmsEF5QKB7a/UN+XFdqvobxz98kv8RTqikyCEBw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.62.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.3.tgz",
+ "integrity": "sha512-XLFHnR3tXMjbOCh2vtVJHmxt+995uJsTERQyseFDRA0xxMxyTZPLa3OIUlyFaO4mF/Lu0FjmWHCuPXJT1n/IOg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.62.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.3.tgz",
+ "integrity": "sha512-se6yXvNGMIl0f+RQzyh7XAmia8/9kplQx424wnG2w0C1oi6XgO6Y8otKhdXFHbHs88Ihavzmvh1NWjuovE76BQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.62.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.3.tgz",
+ "integrity": "sha512-gNoxRefktVIiGflpONuxWWXZAzIQG++z9qHO3xKwk4WdDMuQja3JHGfE1u0i3PfPDyvhypdk+WrgIJqLhGG7sg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.62.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.3.tgz",
+ "integrity": "sha512-V4KtWtQfAFMU7+9/A/VDps/VI8CHd3cYz0L8sgJzz8qK7eY7wI4ruFD82UYIYvW9Z4DtlTfhQcsl4XyPHW5uSg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.62.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.3.tgz",
+ "integrity": "sha512-LBx9LYXvj2CBkMkjLdNAWLwH0MLMin7do2VcVo9kVPibGLkY0BQQut2fv7NVqkXqZ/CrAu9LqDHVV1xHCMpCPw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.62.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.3.tgz",
+ "integrity": "sha512-ABVf3Q0RCu7NcyCCOZQI0pJ3GuSdfSl8EXcy88QtdceIMIoCUdfhsJChZ64L9zVM2aJHjde1Bhn5uqSRcX9ySA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.62.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.3.tgz",
+ "integrity": "sha512-+2Cy/ldweGBLlPIKsQLF8U5N44a0KDdbrk1rAjHOM9M2K+kGdIVjHLmmrZIcx+9Ny3ke/1JomCsDI1ocb11+sg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.62.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.3.tgz",
+ "integrity": "sha512-dtZvzc8BedpSaFNy75x6uiWwAGTH+aZHDtdrqP6qk+WcLJrfti6sGje1ZJ9UxyzDLF23d/mV+PaMwuC0hL7UVA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.62.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.3.tgz",
+ "integrity": "sha512-Rj8Ra4noo+aYy7sKBggCx0407mws34kAb1ySyWuq5DAtFBQdkSwnsjCgPrhPe9cvgBKZIukpE+CVHvORCS93kQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.62.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.3.tgz",
+ "integrity": "sha512-vp7N084ew/odXn2gi/mzm9mUkQu9l6AiN6dt4IeUM2Uvm9o+cVmP+YkqbMOteLbiGgqBBlJZjIMYVCfOOIVbVQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.62.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.3.tgz",
+ "integrity": "sha512-MOG/3gTOn4Fwf574RVOaY61I5o6P90legkFADiTyn1hyjNydT+cerU2rLUwPdZkKKyJ+iT+K9p7WXK4LM1Ka6g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@sveltejs/acorn-typescript": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.11.tgz",
+ "integrity": "sha512-LFuZUkjJ9iF7JZye/aG5XM0SFcQ5VyL0oVX4WJ9dc0Va3R3s0OauX1BESVCb+YN/ol8TAfqGDDAQsTG627Y5kw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^8.9.0"
+ }
+ },
+ "node_modules/@sveltejs/load-config": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/@sveltejs/load-config/-/load-config-0.2.1.tgz",
+ "integrity": "sha512-5m3B2cbqQ4TbwW6Xkh66Ntw6dD7gNc77cCxABTTesWcq9jxIzMgTk97pZx5vEtvQx8iokgi7GIphqZe+PGwcZA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 18.0.0"
+ }
+ },
+ "node_modules/@sveltejs/vite-plugin-svelte": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-5.1.1.tgz",
+ "integrity": "sha512-Y1Cs7hhTc+a5E9Va/xwKlAJoariQyHY+5zBgCZg4PFWNYQ1nMN9sjK1zhw1gK69DuqVP++sht/1GZg1aRwmAXQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@sveltejs/vite-plugin-svelte-inspector": "^4.0.1",
+ "debug": "^4.4.1",
+ "deepmerge": "^4.3.1",
+ "kleur": "^4.1.5",
+ "magic-string": "^0.30.17",
+ "vitefu": "^1.0.6"
+ },
+ "engines": {
+ "node": "^18.0.0 || ^20.0.0 || >=22"
+ },
+ "peerDependencies": {
+ "svelte": "^5.0.0",
+ "vite": "^6.0.0"
+ }
+ },
+ "node_modules/@sveltejs/vite-plugin-svelte-inspector": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-4.0.1.tgz",
+ "integrity": "sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.3.7"
+ },
+ "engines": {
+ "node": "^18.0.0 || ^20.0.0 || >=22"
+ },
+ "peerDependencies": {
+ "@sveltejs/vite-plugin-svelte": "^5.0.0",
+ "svelte": "^5.0.0",
+ "vite": "^6.0.0"
+ }
+ },
+ "node_modules/@testing-library/dom": {
+ "version": "10.4.1",
+ "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
+ "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.10.4",
+ "@babel/runtime": "^7.12.5",
+ "@types/aria-query": "^5.0.1",
+ "aria-query": "5.3.0",
+ "dom-accessibility-api": "^0.5.9",
+ "lz-string": "^1.5.0",
+ "picocolors": "1.1.1",
+ "pretty-format": "^27.0.2"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@testing-library/dom/node_modules/aria-query": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
+ "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "dequal": "^2.0.3"
+ }
+ },
+ "node_modules/@testing-library/dom/node_modules/dom-accessibility-api": {
+ "version": "0.5.16",
+ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
+ "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@testing-library/jest-dom": {
+ "version": "6.9.1",
+ "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz",
+ "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@adobe/css-tools": "^4.4.0",
+ "aria-query": "^5.0.0",
+ "css.escape": "^1.5.1",
+ "dom-accessibility-api": "^0.6.3",
+ "picocolors": "^1.1.1",
+ "redent": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=14",
+ "npm": ">=6",
+ "yarn": ">=1"
+ }
+ },
+ "node_modules/@testing-library/svelte": {
+ "version": "5.4.2",
+ "resolved": "https://registry.npmjs.org/@testing-library/svelte/-/svelte-5.4.2.tgz",
+ "integrity": "sha512-4o31E4HGo5BU5KwPkulNRocEden+7Tt9JYm9uhln5ajF7DULeyFA46BBWVfKJ8Ms9B3JmOFPTIiVamH7n3KpuQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@testing-library/dom": "9.x.x || 10.x.x",
+ "@testing-library/svelte-core": "1.1.3"
+ },
+ "engines": {
+ "node": ">= 10"
+ },
+ "peerDependencies": {
+ "svelte": "^3 || ^4 || ^5 || ^5.0.0-next.0",
+ "vite": "*",
+ "vitest": "*"
+ },
+ "peerDependenciesMeta": {
+ "vite": {
+ "optional": true
+ },
+ "vitest": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@testing-library/svelte-core": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@testing-library/svelte-core/-/svelte-core-1.1.3.tgz",
+ "integrity": "sha512-KkMAvXeWorxN2Yn0kdC1lfoAItxpoj4uOWzxK5leDrNxonLvS5nwBFvztrroyTszQ0Wf/EU6iLT8JhY5qcn22g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=16"
+ },
+ "peerDependencies": {
+ "svelte": "^3 || ^4 || ^5 || ^5.0.0-next.0"
+ }
+ },
+ "node_modules/@types/aria-query": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
+ "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
+ "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "22.20.1",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz",
+ "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~6.21.0"
+ }
+ },
+ "node_modules/@types/trusted-types": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
+ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
+ "license": "MIT"
+ },
+ "node_modules/@vitest/expect": {
+ "version": "2.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz",
+ "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/spy": "2.1.9",
+ "@vitest/utils": "2.1.9",
+ "chai": "^5.1.2",
+ "tinyrainbow": "^1.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/pretty-format": {
+ "version": "2.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz",
+ "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tinyrainbow": "^1.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/runner": {
+ "version": "2.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz",
+ "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/utils": "2.1.9",
+ "pathe": "^1.1.2"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/snapshot": {
+ "version": "2.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz",
+ "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "2.1.9",
+ "magic-string": "^0.30.12",
+ "pathe": "^1.1.2"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/spy": {
+ "version": "2.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz",
+ "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tinyspy": "^3.0.2"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/utils": {
+ "version": "2.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz",
+ "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "2.1.9",
+ "loupe": "^3.1.2",
+ "tinyrainbow": "^1.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.17.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz",
+ "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==",
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/agent-base": {
+ "version": "7.1.4",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
+ "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
+ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/aria-query": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
+ "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/assertion-error": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
+ "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/axobject-query": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
+ "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/cac": {
+ "version": "6.7.14",
+ "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
+ "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/chai": {
+ "version": "5.3.3",
+ "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz",
+ "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "assertion-error": "^2.0.1",
+ "check-error": "^2.1.1",
+ "deep-eql": "^5.0.1",
+ "loupe": "^3.1.0",
+ "pathval": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/check-error": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz",
+ "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 16"
+ }
+ },
+ "node_modules/chokidar": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
+ "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "readdirp": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 14.16.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/clsx": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/css.escape": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
+ "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cssstyle": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz",
+ "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@asamuzakjp/css-color": "^3.2.0",
+ "rrweb-cssom": "^0.8.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/cssstyle/node_modules/rrweb-cssom": {
+ "version": "0.8.0",
+ "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz",
+ "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/data-urls": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz",
+ "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-mimetype": "^4.0.0",
+ "whatwg-url": "^14.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/decimal.js": {
+ "version": "10.6.0",
+ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
+ "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/deep-eql": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
+ "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/deepmerge": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
+ "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/dequal": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
+ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/devalue": {
+ "version": "5.8.2",
+ "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.8.2.tgz",
+ "integrity": "sha512-DObPPAfdtFbXjxLqK8s2Xk9ZuWz5+ZoFEhC7J76es4GU/rEiXwHTmbImoCdyoCOcBH1UF3+Cz6Z2sYD4hyl5TA==",
+ "license": "MIT"
+ },
+ "node_modules/dom-accessibility-api": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz",
+ "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/entities": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
+ "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-module-lexer": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
+ "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
+ "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
+ "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.25.12",
+ "@esbuild/android-arm": "0.25.12",
+ "@esbuild/android-arm64": "0.25.12",
+ "@esbuild/android-x64": "0.25.12",
+ "@esbuild/darwin-arm64": "0.25.12",
+ "@esbuild/darwin-x64": "0.25.12",
+ "@esbuild/freebsd-arm64": "0.25.12",
+ "@esbuild/freebsd-x64": "0.25.12",
+ "@esbuild/linux-arm": "0.25.12",
+ "@esbuild/linux-arm64": "0.25.12",
+ "@esbuild/linux-ia32": "0.25.12",
+ "@esbuild/linux-loong64": "0.25.12",
+ "@esbuild/linux-mips64el": "0.25.12",
+ "@esbuild/linux-ppc64": "0.25.12",
+ "@esbuild/linux-riscv64": "0.25.12",
+ "@esbuild/linux-s390x": "0.25.12",
+ "@esbuild/linux-x64": "0.25.12",
+ "@esbuild/netbsd-arm64": "0.25.12",
+ "@esbuild/netbsd-x64": "0.25.12",
+ "@esbuild/openbsd-arm64": "0.25.12",
+ "@esbuild/openbsd-x64": "0.25.12",
+ "@esbuild/openharmony-arm64": "0.25.12",
+ "@esbuild/sunos-x64": "0.25.12",
+ "@esbuild/win32-arm64": "0.25.12",
+ "@esbuild/win32-ia32": "0.25.12",
+ "@esbuild/win32-x64": "0.25.12"
+ }
+ },
+ "node_modules/esm-env": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz",
+ "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==",
+ "license": "MIT"
+ },
+ "node_modules/esrap": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.3.0.tgz",
+ "integrity": "sha512-GQ/7RN8uOtEfNpzZzBMTzW9JBcX42oaSVtPzdF+6cEL8pqIL094iUpr9jzYGn4O4P/1S60dJ6izyT8F4LYARng==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.4.15"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/types": "^8.2.0"
+ },
+ "peerDependenciesMeta": {
+ "@typescript-eslint/types": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/estree-walker": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
+ "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0"
+ }
+ },
+ "node_modules/expect-type": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz",
+ "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/form-data": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz",
+ "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.4",
+ "mime-types": "^2.1.35"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
+ "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/html-encoding-sniffer": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
+ "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-encoding": "^3.1.1"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/http-proxy-agent": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
+ "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/https-proxy-agent": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+ "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.2",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/indent-string": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
+ "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-potential-custom-element-name": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
+ "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/is-reference": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
+ "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.6"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/jsdom": {
+ "version": "25.0.1",
+ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-25.0.1.tgz",
+ "integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cssstyle": "^4.1.0",
+ "data-urls": "^5.0.0",
+ "decimal.js": "^10.4.3",
+ "form-data": "^4.0.0",
+ "html-encoding-sniffer": "^4.0.0",
+ "http-proxy-agent": "^7.0.2",
+ "https-proxy-agent": "^7.0.5",
+ "is-potential-custom-element-name": "^1.0.1",
+ "nwsapi": "^2.2.12",
+ "parse5": "^7.1.2",
+ "rrweb-cssom": "^0.7.1",
+ "saxes": "^6.0.0",
+ "symbol-tree": "^3.2.4",
+ "tough-cookie": "^5.0.0",
+ "w3c-xmlserializer": "^5.0.0",
+ "webidl-conversions": "^7.0.0",
+ "whatwg-encoding": "^3.1.1",
+ "whatwg-mimetype": "^4.0.0",
+ "whatwg-url": "^14.0.0",
+ "ws": "^8.18.0",
+ "xml-name-validator": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "canvas": "^2.11.2"
+ },
+ "peerDependenciesMeta": {
+ "canvas": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/kleur": {
+ "version": "4.1.5",
+ "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
+ "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/locate-character": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz",
+ "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==",
+ "license": "MIT"
+ },
+ "node_modules/loupe": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz",
+ "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/lucide-svelte": {
+ "version": "0.468.0",
+ "resolved": "https://registry.npmjs.org/lucide-svelte/-/lucide-svelte-0.468.0.tgz",
+ "integrity": "sha512-n0ecAFtCY5LEeL+PJ1Xj4n3c2gzj8tMpak0KMGnvoSJEjCsCnRB0mekBtJZAo7beyynW9Qj5Um1KfMBAeTNplw==",
+ "deprecated": "Package deprecated. Please use @lucide/svelte instead.",
+ "license": "ISC",
+ "peerDependencies": {
+ "svelte": "^3 || ^4 || ^5.0.0-next.42"
+ }
+ },
+ "node_modules/lz-string": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
+ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "lz-string": "bin/bin.js"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/min-indent": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
+ "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/mri": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
+ "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.16",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz",
+ "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/nwsapi": {
+ "version": "2.2.24",
+ "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.24.tgz",
+ "integrity": "sha512-7YRhZ3jS45LwmSCT4b2sVFHt/WuovaktDU07QrtOBY2PXskss5a9jfmR9jptyumwXST+rFjrmppMY1KT/yn35A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/parse5": {
+ "version": "7.3.0",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
+ "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "entities": "^6.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
+ "node_modules/pathe": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz",
+ "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/pathval": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz",
+ "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14.16"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
+ "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.23",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz",
+ "integrity": "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.16",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/pretty-format": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
+ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1",
+ "ansi-styles": "^5.0.0",
+ "react-is": "^17.0.1"
+ },
+ "engines": {
+ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
+ }
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/react-is": {
+ "version": "17.0.2",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
+ "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/readdirp": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
+ "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14.18.0"
+ },
+ "funding": {
+ "type": "individual",
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/redent": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
+ "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "indent-string": "^4.0.0",
+ "strip-indent": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.62.3",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.3.tgz",
+ "integrity": "sha512-Gu0c0iH9FzgX1L1t7ByIbbS3Vmdz+6KHm/EsqmmC71gUQ82yvZRkTK6XzrFObSka91WUVdynqp6nsfilzr5k6Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.9"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.62.3",
+ "@rollup/rollup-android-arm64": "4.62.3",
+ "@rollup/rollup-darwin-arm64": "4.62.3",
+ "@rollup/rollup-darwin-x64": "4.62.3",
+ "@rollup/rollup-freebsd-arm64": "4.62.3",
+ "@rollup/rollup-freebsd-x64": "4.62.3",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.62.3",
+ "@rollup/rollup-linux-arm-musleabihf": "4.62.3",
+ "@rollup/rollup-linux-arm64-gnu": "4.62.3",
+ "@rollup/rollup-linux-arm64-musl": "4.62.3",
+ "@rollup/rollup-linux-loong64-gnu": "4.62.3",
+ "@rollup/rollup-linux-loong64-musl": "4.62.3",
+ "@rollup/rollup-linux-ppc64-gnu": "4.62.3",
+ "@rollup/rollup-linux-ppc64-musl": "4.62.3",
+ "@rollup/rollup-linux-riscv64-gnu": "4.62.3",
+ "@rollup/rollup-linux-riscv64-musl": "4.62.3",
+ "@rollup/rollup-linux-s390x-gnu": "4.62.3",
+ "@rollup/rollup-linux-x64-gnu": "4.62.3",
+ "@rollup/rollup-linux-x64-musl": "4.62.3",
+ "@rollup/rollup-openbsd-x64": "4.62.3",
+ "@rollup/rollup-openharmony-arm64": "4.62.3",
+ "@rollup/rollup-win32-arm64-msvc": "4.62.3",
+ "@rollup/rollup-win32-ia32-msvc": "4.62.3",
+ "@rollup/rollup-win32-x64-gnu": "4.62.3",
+ "@rollup/rollup-win32-x64-msvc": "4.62.3",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/rrweb-cssom": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz",
+ "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/sade": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz",
+ "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mri": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/saxes": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
+ "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "xmlchars": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=v12.22.7"
+ }
+ },
+ "node_modules/siginfo": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
+ "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/stackback": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
+ "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/std-env": {
+ "version": "3.10.0",
+ "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
+ "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/strip-indent": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
+ "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "min-indent": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/svelte": {
+ "version": "5.56.8",
+ "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.56.8.tgz",
+ "integrity": "sha512-PY8LOw7xP6c8IOiVqdo0sbbZVYhXRSfklOQLAUyGBKqjTX0wx/z4l/9J+PmBpmlLnxzEb1NqltxQ5/wZme/Cmg==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/remapping": "^2.3.4",
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@sveltejs/acorn-typescript": "^1.0.10",
+ "@types/estree": "^1.0.5",
+ "@types/trusted-types": "^2.0.7",
+ "acorn": "^8.12.1",
+ "aria-query": "5.3.1",
+ "axobject-query": "^4.1.0",
+ "clsx": "^2.1.1",
+ "devalue": "^5.8.1",
+ "esm-env": "^1.2.1",
+ "esrap": "^2.2.12",
+ "is-reference": "^3.0.3",
+ "locate-character": "^3.0.0",
+ "magic-string": "^0.30.11",
+ "zimmerframe": "^1.1.2"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/svelte-check": {
+ "version": "4.7.3",
+ "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.7.3.tgz",
+ "integrity": "sha512-DHdTCGX62R0fCxBEaT+USdASAnoaRBaaNczkRJl0K7o3WyoCeVUbVxo6fKqpOll/B+WMWCsiFK0eFrJSNBKZIg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "^0.3.25",
+ "@sveltejs/load-config": "^0.2.0",
+ "chokidar": "^4.0.1",
+ "fdir": "^6.2.0",
+ "picocolors": "^1.0.0",
+ "sade": "^1.7.4"
+ },
+ "bin": {
+ "svelte-check": "bin/svelte-check"
+ },
+ "engines": {
+ "node": ">= 18.0.0"
+ },
+ "peerDependencies": {
+ "svelte": "^4.0.0 || ^5.0.0-next.0",
+ "typescript": ">=5.0.0"
+ }
+ },
+ "node_modules/svelte/node_modules/aria-query": {
+ "version": "5.3.1",
+ "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz",
+ "integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/symbol-tree": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
+ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinybench": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
+ "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinyexec": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",
+ "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.17",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
+ "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tinypool": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz",
+ "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ }
+ },
+ "node_modules/tinyrainbow": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz",
+ "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tinyspy": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz",
+ "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tldts": {
+ "version": "6.1.86",
+ "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz",
+ "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tldts-core": "^6.1.86"
+ },
+ "bin": {
+ "tldts": "bin/cli.js"
+ }
+ },
+ "node_modules/tldts-core": {
+ "version": "6.1.86",
+ "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz",
+ "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tough-cookie": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz",
+ "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "tldts": "^6.1.32"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/tr46": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
+ "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "punycode": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.7.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",
+ "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/vite": {
+ "version": "6.4.3",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz",
+ "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.25.0",
+ "fdir": "^6.4.4",
+ "picomatch": "^4.0.2",
+ "postcss": "^8.5.3",
+ "rollup": "^4.34.9",
+ "tinyglobby": "^0.2.13"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
+ "jiti": ">=1.21.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vite-node": {
+ "version": "2.1.9",
+ "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz",
+ "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cac": "^6.7.14",
+ "debug": "^4.3.7",
+ "es-module-lexer": "^1.5.4",
+ "pathe": "^1.1.2",
+ "vite": "^5.0.0"
+ },
+ "bin": {
+ "vite-node": "vite-node.mjs"
+ },
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/vite-node/node_modules/@esbuild/aix-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vite-node/node_modules/@esbuild/android-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
+ "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vite-node/node_modules/@esbuild/android-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
+ "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vite-node/node_modules/@esbuild/android-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
+ "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vite-node/node_modules/@esbuild/darwin-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
+ "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vite-node/node_modules/@esbuild/darwin-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
+ "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vite-node/node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
+ "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vite-node/node_modules/@esbuild/freebsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
+ "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vite-node/node_modules/@esbuild/linux-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
+ "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vite-node/node_modules/@esbuild/linux-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
+ "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vite-node/node_modules/@esbuild/linux-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
+ "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vite-node/node_modules/@esbuild/linux-loong64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
+ "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vite-node/node_modules/@esbuild/linux-mips64el": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
+ "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vite-node/node_modules/@esbuild/linux-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vite-node/node_modules/@esbuild/linux-riscv64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
+ "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vite-node/node_modules/@esbuild/linux-s390x": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
+ "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vite-node/node_modules/@esbuild/linux-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
+ "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vite-node/node_modules/@esbuild/netbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vite-node/node_modules/@esbuild/openbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vite-node/node_modules/@esbuild/sunos-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
+ "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vite-node/node_modules/@esbuild/win32-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
+ "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vite-node/node_modules/@esbuild/win32-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
+ "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vite-node/node_modules/@esbuild/win32-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
+ "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vite-node/node_modules/esbuild": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
+ "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.21.5",
+ "@esbuild/android-arm": "0.21.5",
+ "@esbuild/android-arm64": "0.21.5",
+ "@esbuild/android-x64": "0.21.5",
+ "@esbuild/darwin-arm64": "0.21.5",
+ "@esbuild/darwin-x64": "0.21.5",
+ "@esbuild/freebsd-arm64": "0.21.5",
+ "@esbuild/freebsd-x64": "0.21.5",
+ "@esbuild/linux-arm": "0.21.5",
+ "@esbuild/linux-arm64": "0.21.5",
+ "@esbuild/linux-ia32": "0.21.5",
+ "@esbuild/linux-loong64": "0.21.5",
+ "@esbuild/linux-mips64el": "0.21.5",
+ "@esbuild/linux-ppc64": "0.21.5",
+ "@esbuild/linux-riscv64": "0.21.5",
+ "@esbuild/linux-s390x": "0.21.5",
+ "@esbuild/linux-x64": "0.21.5",
+ "@esbuild/netbsd-x64": "0.21.5",
+ "@esbuild/openbsd-x64": "0.21.5",
+ "@esbuild/sunos-x64": "0.21.5",
+ "@esbuild/win32-arm64": "0.21.5",
+ "@esbuild/win32-ia32": "0.21.5",
+ "@esbuild/win32-x64": "0.21.5"
+ }
+ },
+ "node_modules/vite-node/node_modules/vite": {
+ "version": "5.4.21",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
+ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.21.3",
+ "postcss": "^8.4.43",
+ "rollup": "^4.20.0"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || >=20.0.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vitefu": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.3.tgz",
+ "integrity": "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==",
+ "dev": true,
+ "license": "MIT",
+ "workspaces": [
+ "tests/deps/*",
+ "tests/projects/*",
+ "tests/projects/workspace/packages/*"
+ ],
+ "peerDependencies": {
+ "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "vite": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vitest": {
+ "version": "2.1.9",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz",
+ "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/expect": "2.1.9",
+ "@vitest/mocker": "2.1.9",
+ "@vitest/pretty-format": "^2.1.9",
+ "@vitest/runner": "2.1.9",
+ "@vitest/snapshot": "2.1.9",
+ "@vitest/spy": "2.1.9",
+ "@vitest/utils": "2.1.9",
+ "chai": "^5.1.2",
+ "debug": "^4.3.7",
+ "expect-type": "^1.1.0",
+ "magic-string": "^0.30.12",
+ "pathe": "^1.1.2",
+ "std-env": "^3.8.0",
+ "tinybench": "^2.9.0",
+ "tinyexec": "^0.3.1",
+ "tinypool": "^1.0.1",
+ "tinyrainbow": "^1.2.0",
+ "vite": "^5.0.0",
+ "vite-node": "2.1.9",
+ "why-is-node-running": "^2.3.0"
+ },
+ "bin": {
+ "vitest": "vitest.mjs"
+ },
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@edge-runtime/vm": "*",
+ "@types/node": "^18.0.0 || >=20.0.0",
+ "@vitest/browser": "2.1.9",
+ "@vitest/ui": "2.1.9",
+ "happy-dom": "*",
+ "jsdom": "*"
+ },
+ "peerDependenciesMeta": {
+ "@edge-runtime/vm": {
+ "optional": true
+ },
+ "@types/node": {
+ "optional": true
+ },
+ "@vitest/browser": {
+ "optional": true
+ },
+ "@vitest/ui": {
+ "optional": true
+ },
+ "happy-dom": {
+ "optional": true
+ },
+ "jsdom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/aix-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/android-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
+ "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/android-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
+ "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/android-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
+ "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/darwin-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
+ "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/darwin-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
+ "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
+ "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/freebsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
+ "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/linux-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
+ "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/linux-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
+ "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/linux-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
+ "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/linux-loong64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
+ "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/linux-mips64el": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
+ "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/linux-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/linux-riscv64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
+ "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/linux-s390x": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
+ "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/linux-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
+ "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/netbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/openbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/sunos-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
+ "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/win32-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
+ "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/win32-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
+ "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/win32-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
+ "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/vitest/node_modules/@vitest/mocker": {
+ "version": "2.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz",
+ "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/spy": "2.1.9",
+ "estree-walker": "^3.0.3",
+ "magic-string": "^0.30.12"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "msw": "^2.4.9",
+ "vite": "^5.0.0"
+ },
+ "peerDependenciesMeta": {
+ "msw": {
+ "optional": true
+ },
+ "vite": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vitest/node_modules/esbuild": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
+ "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.21.5",
+ "@esbuild/android-arm": "0.21.5",
+ "@esbuild/android-arm64": "0.21.5",
+ "@esbuild/android-x64": "0.21.5",
+ "@esbuild/darwin-arm64": "0.21.5",
+ "@esbuild/darwin-x64": "0.21.5",
+ "@esbuild/freebsd-arm64": "0.21.5",
+ "@esbuild/freebsd-x64": "0.21.5",
+ "@esbuild/linux-arm": "0.21.5",
+ "@esbuild/linux-arm64": "0.21.5",
+ "@esbuild/linux-ia32": "0.21.5",
+ "@esbuild/linux-loong64": "0.21.5",
+ "@esbuild/linux-mips64el": "0.21.5",
+ "@esbuild/linux-ppc64": "0.21.5",
+ "@esbuild/linux-riscv64": "0.21.5",
+ "@esbuild/linux-s390x": "0.21.5",
+ "@esbuild/linux-x64": "0.21.5",
+ "@esbuild/netbsd-x64": "0.21.5",
+ "@esbuild/openbsd-x64": "0.21.5",
+ "@esbuild/sunos-x64": "0.21.5",
+ "@esbuild/win32-arm64": "0.21.5",
+ "@esbuild/win32-ia32": "0.21.5",
+ "@esbuild/win32-x64": "0.21.5"
+ }
+ },
+ "node_modules/vitest/node_modules/vite": {
+ "version": "5.4.21",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
+ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.21.3",
+ "postcss": "^8.4.43",
+ "rollup": "^4.20.0"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || >=20.0.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/w3c-xmlserializer": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
+ "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "xml-name-validator": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/webidl-conversions": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
+ "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/whatwg-encoding": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
+ "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
+ "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "iconv-lite": "0.6.3"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/whatwg-mimetype": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
+ "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/whatwg-url": {
+ "version": "14.2.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
+ "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tr46": "^5.1.0",
+ "webidl-conversions": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/why-is-node-running": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
+ "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "siginfo": "^2.0.0",
+ "stackback": "0.0.2"
+ },
+ "bin": {
+ "why-is-node-running": "cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ws": {
+ "version": "8.21.1",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz",
+ "integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/xml-name-validator": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
+ "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/xmlchars": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
+ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/zimmerframe": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz",
+ "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==",
+ "license": "MIT"
+ }
+ }
+}
diff --git a/panel/frontend/package.json b/panel/frontend/package.json
new file mode 100644
index 0000000..9c30d10
--- /dev/null
+++ b/panel/frontend/package.json
@@ -0,0 +1,26 @@
+{
+ "name": "userbot-panel",
+ "private": true,
+ "version": "1.0.0",
+ "type": "module",
+ "scripts": {
+ "build": "vite build",
+ "dev": "vite --host 0.0.0.0",
+ "test": "vitest run"
+ },
+ "dependencies": {
+ "lucide-svelte": "^0.468.0",
+ "svelte": "^5.16.0"
+ },
+ "devDependencies": {
+ "@sveltejs/vite-plugin-svelte": "^5.0.0",
+ "@testing-library/jest-dom": "^6.6.3",
+ "@testing-library/svelte": "^5.2.6",
+ "@types/node": "^22.10.5",
+ "jsdom": "^25.0.1",
+ "svelte-check": "^4.7.3",
+ "typescript": "~5.7.2",
+ "vite": "^6.0.7",
+ "vitest": "^2.1.8"
+ }
+}
diff --git a/panel/frontend/src/App.svelte b/panel/frontend/src/App.svelte
new file mode 100644
index 0000000..4fd83b5
--- /dev/null
+++ b/panel/frontend/src/App.svelte
@@ -0,0 +1,211 @@
+
+
+
+ {
+ addOpen = true;
+ menuOpen = false;
+ }}
+ on:close={() => (menuOpen = false)}
+ />
+ {#if menuOpen}
+
+ {/if}
+
+
+
+
+
+
+
+
Инстансы
+
Telegram-аккаунты и их состояние в Kubernetes
+
+
+
+
+ Всего{instances.length}
+ Работают{running}
+ Остановлены{stopped}
+ Перезапуски{restarts}
+
+
+
+
+
+
+
+
+ {#if error}
+
+ {error}
+
+
+ {/if}
+
+ {#if loading}
+
Загружаем инстансы…
+ {:else if visibleInstances.length}
+
(selected = event.detail)}
+ on:action={(event) => runAction(event.detail.instance, event.detail.action)}
+ />
+ {:else}
+
+
+
{instances.length ? 'Ничего не найдено' : 'Инстансов пока нет'}
+
{instances.length ? 'Измените поиск или фильтр.' : 'Добавьте первый Telegram-аккаунт.'}
+ {#if !instances.length}
+
+ {/if}
+
+ {/if}
+
+
+
+
+ (addOpen = false)} on:created={(event) => created(event.detail)} />
+ (selected = null)}
+ on:action={(event) => runAction(event.detail.instance, event.detail.action)}
+ on:delete={(event) => deleteInstance(event.detail.instance, event.detail.deleteData)}
+/>
diff --git a/panel/frontend/src/components/AddAccountDrawer.svelte b/panel/frontend/src/components/AddAccountDrawer.svelte
new file mode 100644
index 0000000..8abf351
--- /dev/null
+++ b/panel/frontend/src/components/AddAccountDrawer.svelte
@@ -0,0 +1,354 @@
+
+
+{#if open}
+
+
+{/if}
diff --git a/panel/frontend/src/components/DetailsDrawer.svelte b/panel/frontend/src/components/DetailsDrawer.svelte
new file mode 100644
index 0000000..5dd5c67
--- /dev/null
+++ b/panel/frontend/src/components/DetailsDrawer.svelte
@@ -0,0 +1,204 @@
+
+
+{#if instance}
+ dispatch('close')} aria-hidden="true">
+
+{/if}
diff --git a/panel/frontend/src/components/InstanceTable.svelte b/panel/frontend/src/components/InstanceTable.svelte
new file mode 100644
index 0000000..5a398d6
--- /dev/null
+++ b/panel/frontend/src/components/InstanceTable.svelte
@@ -0,0 +1,114 @@
+
+
+
+
+ ИНСТАНС
+ СТАТУС
+ POD
+ РЕСТАРТЫ
+ ХРАНИЛИЩЕ
+ ОБНОВЛЁН
+ ДЕЙСТВИЯ
+
+
+ {#each instances as instance (instance.namespace + instance.instance_id)}
+
dispatch('select', instance)}
+ on:keydown={(event) => {
+ if (event.key === 'Enter' || event.key === ' ') dispatch('select', instance);
+ }}
+ role="button"
+ tabindex="0"
+ aria-label={`Открыть ${instance.display_name}`}
+ >
+
+ {instance.display_name}
+ {instance.instance_id} · {instance.namespace}
+
+
+
+ {instance.pod || instance.reason || 'Нет pod'}
+
+
{instance.restarts}
+
{instance.storage || '—'}
+
{relativeTime(instance.updated_at)}
+
+
+
+ {#if instance.status === 'stopped'}
+
+ {:else}
+
+ {/if}
+
+
+
+ {/each}
+
diff --git a/panel/frontend/src/components/Sidebar.svelte b/panel/frontend/src/components/Sidebar.svelte
new file mode 100644
index 0000000..0afbeb5
--- /dev/null
+++ b/panel/frontend/src/components/Sidebar.svelte
@@ -0,0 +1,39 @@
+
+
+
diff --git a/panel/frontend/src/components/StatusDot.svelte b/panel/frontend/src/components/StatusDot.svelte
new file mode 100644
index 0000000..56de618
--- /dev/null
+++ b/panel/frontend/src/components/StatusDot.svelte
@@ -0,0 +1,17 @@
+
+
+
+
+ {labels[status]}
+
diff --git a/panel/frontend/src/lib/api.ts b/panel/frontend/src/lib/api.ts
new file mode 100644
index 0000000..44d8b12
--- /dev/null
+++ b/panel/frontend/src/lib/api.ts
@@ -0,0 +1,76 @@
+import type { AccountDraft, AuthResult, Instance } from "./types";
+
+async function request(path: string, init?: RequestInit): Promise {
+ const response = await fetch(path, init);
+ if (!response.ok) {
+ const payload = await response.json().catch(() => ({}));
+ throw new Error(payload.detail || response.statusText || "Request failed");
+ }
+ if (response.status === 204) return undefined as T;
+ return response.json();
+}
+
+const json = (body: unknown): RequestInit => ({
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(body),
+});
+
+export const api = {
+ instances(query = "", status = "") {
+ const params = new URLSearchParams();
+ if (query) params.set("query", query);
+ if (status) params.set("status", status);
+ const suffix = params.size ? `?${params}` : "";
+ return request(`/api/instances${suffix}`);
+ },
+ instance(id: string) {
+ return request(`/api/instances/${encodeURIComponent(id)}`);
+ },
+ logs(id: string) {
+ return request<{ logs: string }>(`/api/instances/${encodeURIComponent(id)}/logs`);
+ },
+ action(id: string, action: "start" | "stop" | "restart") {
+ return request(`/api/instances/${encodeURIComponent(id)}/${action}`, { method: "POST" });
+ },
+ delete(id: string, deleteData: boolean) {
+ return request(
+ `/api/instances/${encodeURIComponent(id)}/delete`,
+ json({ confirmation: id, delete_data: deleteData }),
+ );
+ },
+ startPhone(draft: AccountDraft) {
+ return request(
+ "/api/auth/phone/start",
+ json({
+ ...basePayload(draft),
+ phone: draft.phone,
+ }),
+ );
+ },
+ submitCode(flowId: string, code: string) {
+ return request(`/api/auth/phone/${encodeURIComponent(flowId)}/code`, json({ code }));
+ },
+ submitPassword(flowId: string, password: string) {
+ return request(`/api/auth/phone/${encodeURIComponent(flowId)}/password`, json({ password }));
+ },
+ stringSession(draft: AccountDraft) {
+ return request(
+ "/api/auth/string-session",
+ json({
+ ...basePayload(draft),
+ session_string: draft.session_string,
+ }),
+ );
+ },
+};
+
+function basePayload(draft: AccountDraft) {
+ return {
+ instance_id: draft.instance_id,
+ display_name: draft.display_name,
+ api_id: Number(draft.api_id),
+ api_hash: draft.api_hash,
+ resources: draft.resources,
+ };
+}
diff --git a/panel/frontend/src/lib/instances.test.ts b/panel/frontend/src/lib/instances.test.ts
new file mode 100644
index 0000000..7fe866c
--- /dev/null
+++ b/panel/frontend/src/lib/instances.test.ts
@@ -0,0 +1,43 @@
+import { describe, expect, it } from "vitest";
+
+import { filterInstances, relativeTime } from "./instances";
+import type { Instance } from "./types";
+
+const instance = (overrides: Partial): Instance => ({
+ instance_id: "forust",
+ display_name: "Forust",
+ namespace: "userbot",
+ deployment: "userbot-forust",
+ pod: "userbot-forust-abc",
+ status: "running",
+ reason: null,
+ ready: true,
+ restarts: 0,
+ pvc: "userbot-forust-data",
+ storage: "1Gi",
+ image: "userbot:latest",
+ cpu_limit: "300m",
+ memory_limit: "1536Mi",
+ cpu_usage: null,
+ memory_usage: null,
+ updated_at: null,
+ legacy: false,
+ deletable: true,
+ ...overrides,
+});
+
+describe("filterInstances", () => {
+ it("filters by status and searchable fields", () => {
+ const items = [
+ instance({ instance_id: "forust" }),
+ instance({ instance_id: "anna", display_name: "Anna", status: "stopped" }),
+ ];
+ expect(filterInstances(items, "ann", "stopped").map((item) => item.instance_id)).toEqual(["anna"]);
+ });
+});
+
+describe("relativeTime", () => {
+ it("formats recent timestamps", () => {
+ expect(relativeTime("2026-01-01T00:00:00Z", Date.parse("2026-01-01T00:00:45Z"))).toBe("45 сек назад");
+ });
+});
diff --git a/panel/frontend/src/lib/instances.ts b/panel/frontend/src/lib/instances.ts
new file mode 100644
index 0000000..34b77ac
--- /dev/null
+++ b/panel/frontend/src/lib/instances.ts
@@ -0,0 +1,24 @@
+import type { Instance } from "./types";
+
+export function filterInstances(instances: Instance[], query: string, status: string): Instance[] {
+ const normalized = query.trim().toLowerCase();
+ return instances.filter((instance) => {
+ const matchesStatus = !status || instance.status === status;
+ const matchesQuery =
+ !normalized ||
+ instance.instance_id.toLowerCase().includes(normalized) ||
+ instance.display_name.toLowerCase().includes(normalized) ||
+ (instance.pod || "").toLowerCase().includes(normalized);
+ return matchesStatus && matchesQuery;
+ });
+}
+
+export function relativeTime(value: string | null, now = Date.now()): string {
+ if (!value) return "—";
+ const seconds = Math.max(0, Math.round((now - new Date(value).getTime()) / 1000));
+ if (seconds < 60) return `${seconds} сек назад`;
+ const minutes = Math.floor(seconds / 60);
+ if (minutes < 60) return `${minutes} мин назад`;
+ const hours = Math.floor(minutes / 60);
+ return `${hours} ч назад`;
+}
diff --git a/panel/frontend/src/lib/types.ts b/panel/frontend/src/lib/types.ts
new file mode 100644
index 0000000..06d14a2
--- /dev/null
+++ b/panel/frontend/src/lib/types.ts
@@ -0,0 +1,45 @@
+export type InstanceStatus = "running" | "stopped" | "pending" | "error";
+
+export interface Instance {
+ instance_id: string;
+ display_name: string;
+ namespace: string;
+ deployment: string;
+ pod: string | null;
+ status: InstanceStatus;
+ reason: string | null;
+ ready: boolean;
+ restarts: number;
+ pvc: string | null;
+ storage: string | null;
+ image: string;
+ cpu_limit: string | null;
+ memory_limit: string | null;
+ cpu_usage: string | null;
+ memory_usage: string | null;
+ updated_at: string | null;
+ legacy: boolean;
+ deletable: boolean;
+}
+
+export interface Resources {
+ storage: string;
+ cpu_limit: string;
+ memory_limit: string;
+}
+
+export interface AccountDraft {
+ instance_id: string;
+ display_name: string;
+ api_id: string;
+ api_hash: string;
+ phone: string;
+ session_string: string;
+ resources: Resources;
+}
+
+export interface AuthResult {
+ status: "code_required" | "password_required" | "ready";
+ flow_id?: string;
+ instance?: Instance;
+}
diff --git a/panel/frontend/src/main.ts b/panel/frontend/src/main.ts
new file mode 100644
index 0000000..ff37c97
--- /dev/null
+++ b/panel/frontend/src/main.ts
@@ -0,0 +1,8 @@
+import { mount } from "svelte";
+
+import App from "./App.svelte";
+import "./styles.css";
+
+mount(App, {
+ target: document.getElementById("app")!,
+});
diff --git a/panel/frontend/src/styles.css b/panel/frontend/src/styles.css
new file mode 100644
index 0000000..1f3dc8d
--- /dev/null
+++ b/panel/frontend/src/styles.css
@@ -0,0 +1,1235 @@
+:root {
+ font-family:
+ Inter,
+ "SF Pro Display",
+ "Helvetica Neue",
+ -apple-system,
+ BlinkMacSystemFont,
+ "Segoe UI",
+ sans-serif;
+ color: #f5f5f5;
+ background: #080808;
+ font-synthesis: none;
+ --bg: #080808;
+ --sidebar: #0b0b0b;
+ --surface: #101010;
+ --surface-2: #151515;
+ --surface-3: #1a1a1a;
+ --line: #2b2b2b;
+ --line-soft: rgba(255, 255, 255, 0.09);
+ --text: #f5f5f5;
+ --text-2: #b7b7b7;
+ --muted: #747474;
+ --accent-soft: rgba(255, 255, 255, 0.08);
+ --radius-sm: 6px;
+ --radius-md: 8px;
+ --sidebar-width: 204px;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+html,
+body,
+#app {
+ min-width: 320px;
+ min-height: 100%;
+ margin: 0;
+}
+
+body {
+ min-height: 100vh;
+ background: var(--bg);
+ color: var(--text);
+ font-size: 13px;
+}
+
+button,
+input,
+select,
+textarea {
+ font: inherit;
+}
+
+button {
+ color: inherit;
+}
+
+button,
+select {
+ cursor: pointer;
+}
+
+button:disabled {
+ cursor: not-allowed;
+ opacity: 0.42;
+}
+
+.app-shell {
+ display: grid;
+ min-height: 100vh;
+ grid-template-columns: var(--sidebar-width) minmax(0, 1fr);
+}
+
+aside[aria-label="Навигация"] {
+ position: fixed;
+ z-index: 20;
+ inset: 0 auto 0 0;
+ display: flex;
+ width: var(--sidebar-width);
+ flex-direction: column;
+ border-right: 1px solid var(--line);
+ background: var(--sidebar);
+ padding: 17px 10px 15px;
+}
+
+.brand {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ min-height: 43px;
+ padding: 0 9px;
+}
+
+.brand div {
+ display: grid;
+ gap: 2px;
+}
+
+.brand strong {
+ font-size: 16px;
+ font-weight: 700;
+ letter-spacing: -0.02em;
+}
+
+.brand span,
+.cluster-state span {
+ color: var(--muted);
+ font-size: 11px;
+}
+
+.brand .mobile-close {
+ display: none;
+ margin-left: auto;
+}
+
+nav {
+ display: grid;
+ gap: 7px;
+ margin-top: 29px;
+}
+
+.nav-item {
+ display: flex;
+ height: 45px;
+ align-items: center;
+ gap: 11px;
+ border: 1px solid transparent;
+ border-radius: var(--radius-sm);
+ background: transparent;
+ color: #9b9b9b;
+ padding: 0 12px;
+ text-align: left;
+ font-size: 12px;
+ font-weight: 600;
+}
+
+.nav-item:hover {
+ color: var(--text);
+ background: rgba(255, 255, 255, 0.035);
+}
+
+.nav-item.active {
+ border-color: #3b3b3b;
+ color: var(--text);
+ background: var(--accent-soft);
+}
+
+.cluster-state {
+ display: flex;
+ align-items: flex-start;
+ gap: 9px;
+ margin-top: auto;
+ border-top: 1px solid var(--line);
+ padding: 14px 9px 0;
+}
+
+.cluster-state div {
+ display: grid;
+ gap: 3px;
+}
+
+.cluster-state strong {
+ font-size: 11px;
+ font-weight: 600;
+}
+
+.cluster-dot {
+ width: 9px;
+ height: 9px;
+ margin-top: 2px;
+ border-radius: 50%;
+ background: #8fce63;
+ box-shadow: 0 0 0 3px rgba(143, 206, 99, 0.05);
+}
+
+.workspace {
+ min-width: 0;
+ grid-column: 2;
+}
+
+.topbar {
+ position: sticky;
+ z-index: 10;
+ top: 0;
+ display: flex;
+ height: 57px;
+ align-items: center;
+ justify-content: space-between;
+ border-bottom: 1px solid var(--line);
+ background: rgba(8, 8, 8, 0.96);
+ padding: 0 22px;
+ backdrop-filter: blur(12px);
+}
+
+.live-state {
+ display: flex;
+ align-items: center;
+ gap: 9px;
+ color: var(--text-2);
+ font-size: 11px;
+ font-weight: 600;
+}
+
+.live-state span {
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ background: #bdbdbd;
+}
+
+.topbar-actions {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+
+.namespace {
+ display: flex;
+ height: 35px;
+ align-items: center;
+ gap: 7px;
+ border: 1px solid var(--line);
+ border-radius: var(--radius-sm);
+ color: var(--text-2);
+ padding: 0 12px;
+}
+
+button.primary,
+.primary {
+ display: inline-flex;
+ min-height: 35px;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+ border: 1px solid #e9e9e9;
+ border-radius: var(--radius-sm);
+ background: #ededed;
+ color: #111;
+ padding: 0 14px;
+ font-size: 12px;
+ font-weight: 650;
+}
+
+button.primary:hover {
+ background: #fff;
+}
+
+.content {
+ width: 100%;
+ max-width: 1280px;
+ margin: 0 auto;
+ padding: 21px 22px 48px;
+}
+
+.page-heading h1 {
+ margin: 0;
+ font-size: 23px;
+ font-weight: 700;
+ letter-spacing: -0.025em;
+}
+
+.page-heading p,
+.drawer-header p {
+ margin: 5px 0 0;
+ color: var(--muted);
+ font-size: 11px;
+}
+
+.summary-strip {
+ display: grid;
+ min-height: 62px;
+ grid-template-columns: repeat(4, 1fr);
+ margin-top: 18px;
+ border: 1px solid var(--line);
+ border-radius: var(--radius-md);
+ background: var(--surface);
+}
+
+.summary-strip div {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ border-right: 1px solid var(--line);
+ padding: 0 22px;
+}
+
+.summary-strip div:last-child {
+ border-right: 0;
+}
+
+.summary-strip span {
+ color: var(--muted);
+ font-size: 11px;
+}
+
+.summary-strip strong {
+ font-size: 18px;
+ font-weight: 650;
+}
+
+.toolbar {
+ display: grid;
+ grid-template-columns: minmax(230px, 330px) 160px 36px;
+ gap: 10px;
+ margin-top: 17px;
+}
+
+.search-field,
+.toolbar select {
+ height: 36px;
+ border: 1px solid var(--line);
+ border-radius: var(--radius-sm);
+ background: var(--surface);
+ color: var(--text-2);
+}
+
+.search-field {
+ display: flex;
+ align-items: center;
+ gap: 9px;
+ padding: 0 12px;
+}
+
+.search-field input {
+ width: 100%;
+ border: 0;
+ outline: 0;
+ background: transparent;
+ color: var(--text);
+}
+
+.search-field input::placeholder {
+ color: var(--muted);
+}
+
+.toolbar select {
+ appearance: none;
+ background-image:
+ linear-gradient(45deg, transparent 50%, #777 50%), linear-gradient(135deg, #777 50%, transparent 50%);
+ background-position:
+ calc(100% - 15px) 15px,
+ calc(100% - 11px) 15px;
+ background-repeat: no-repeat;
+ background-size: 4px 4px;
+ padding: 0 30px 0 12px;
+}
+
+.icon-button {
+ display: inline-flex;
+ width: 32px;
+ height: 32px;
+ flex: 0 0 auto;
+ align-items: center;
+ justify-content: center;
+ border: 1px solid var(--line);
+ border-radius: var(--radius-sm);
+ background: var(--surface);
+ color: var(--text-2);
+ padding: 0;
+}
+
+.icon-button:hover:not(:disabled) {
+ border-color: #4a4a4a;
+ color: var(--text);
+ background: var(--surface-2);
+}
+
+.icon-button.borderless {
+ border-color: transparent;
+ background: transparent;
+}
+
+.refresh {
+ width: 36px;
+ height: 36px;
+}
+
+.table-frame {
+ overflow: hidden;
+ margin-top: 14px;
+ border: 1px solid var(--line);
+ border-radius: var(--radius-md);
+ background: var(--surface);
+}
+
+.table-grid {
+ display: grid;
+ grid-template-columns:
+ minmax(125px, 1.15fr) minmax(100px, 0.8fr) minmax(170px, 1.45fr)
+ 70px 85px 105px 132px;
+ align-items: center;
+ gap: 10px;
+}
+
+.table-head {
+ min-height: 35px;
+ border-bottom: 1px solid var(--line);
+ color: var(--muted);
+ padding: 0 12px;
+ font-size: 9px;
+ font-weight: 650;
+}
+
+.table-head .right {
+ text-align: right;
+}
+
+.instance-row {
+ width: 100%;
+ min-height: 58px;
+ border: 0;
+ border-bottom: 1px solid var(--line);
+ background: transparent;
+ color: var(--text-2);
+ padding: 0 10px 0 12px;
+ text-align: left;
+ font-size: 11px;
+}
+
+.instance-row:last-child {
+ border-bottom: 0;
+}
+
+.instance-row:hover,
+.instance-row:focus-visible {
+ outline: 0;
+ background: rgba(255, 255, 255, 0.045);
+}
+
+.instance-row.error-row {
+ background: rgba(255, 255, 255, 0.025);
+}
+
+.instance-name {
+ display: grid;
+ gap: 4px;
+ min-width: 0;
+}
+
+.instance-name strong {
+ overflow: hidden;
+ color: var(--text);
+ font-size: 12px;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.instance-name small {
+ overflow: hidden;
+ color: var(--muted);
+ font-size: 10px;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.pod-name {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.row-actions {
+ display: flex;
+ justify-content: flex-end;
+ gap: 5px;
+}
+
+.row-actions .icon-button {
+ width: 29px;
+ height: 29px;
+}
+
+.status {
+ display: inline-flex;
+ align-items: center;
+ gap: 7px;
+ color: var(--text-2);
+ font-size: 11px;
+}
+
+.status-dot {
+ width: 8px;
+ height: 8px;
+ border: 1px solid #8b8b8b;
+ border-radius: 50%;
+}
+
+.status-running .status-dot {
+ border-color: #d0d0d0;
+ background: #d0d0d0;
+}
+
+.status-pending .status-dot {
+ border-color: #9a9a9a;
+ background: linear-gradient(90deg, #9a9a9a 50%, transparent 50%);
+}
+
+.status-error {
+ color: #ddd;
+}
+
+.status-error .status-dot {
+ border-color: #eee;
+ box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.12);
+}
+
+.center-state,
+.empty-state {
+ display: flex;
+ min-height: 300px;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ color: var(--muted);
+ gap: 9px;
+ text-align: center;
+}
+
+.empty-state h2 {
+ margin: 4px 0 0;
+ color: var(--text);
+ font-size: 15px;
+}
+
+.empty-state p {
+ margin: 0 0 8px;
+}
+
+.error-banner {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-top: 13px;
+ border: 1px solid #414141;
+ border-radius: var(--radius-sm);
+ background: #151515;
+ padding: 10px 12px;
+ color: #d8d8d8;
+}
+
+.error-banner button,
+.text-button,
+.back-link {
+ border: 0;
+ background: transparent;
+ color: var(--text-2);
+ padding: 0;
+ font-size: 11px;
+ text-decoration: underline;
+ text-underline-offset: 3px;
+}
+
+.drawer-backdrop {
+ position: fixed;
+ z-index: 39;
+ inset: 0;
+ background: rgba(0, 0, 0, 0.58);
+}
+
+.drawer {
+ position: fixed;
+ z-index: 40;
+ top: 0;
+ right: 0;
+ display: flex;
+ width: min(460px, 100vw);
+ height: 100dvh;
+ flex-direction: column;
+ border-left: 1px solid var(--line);
+ background: #0d0d0d;
+ box-shadow: -20px 0 60px rgba(0, 0, 0, 0.42);
+ animation: drawer-in 160ms ease-out;
+}
+
+.details-drawer {
+ width: min(420px, 100vw);
+}
+
+.drawer-header {
+ display: flex;
+ min-height: 76px;
+ align-items: flex-start;
+ justify-content: space-between;
+ border-bottom: 1px solid var(--line);
+ padding: 20px 22px 16px;
+}
+
+.drawer-header h2 {
+ margin: 0;
+ font-size: 20px;
+ letter-spacing: -0.025em;
+}
+
+.drawer-header .status {
+ margin-top: 8px;
+}
+
+.steps {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ margin: 0;
+ border-bottom: 1px solid var(--line);
+ padding: 0 22px;
+ list-style: none;
+}
+
+.steps li {
+ position: relative;
+ display: flex;
+ height: 55px;
+ align-items: center;
+ gap: 7px;
+ border-bottom: 2px solid transparent;
+ color: var(--muted);
+ font-size: 10px;
+ font-weight: 600;
+}
+
+.steps li span {
+ display: inline-flex;
+ width: 17px;
+ height: 17px;
+ align-items: center;
+ justify-content: center;
+ border: 1px solid #4a4a4a;
+ border-radius: 50%;
+ font-size: 9px;
+}
+
+.steps li.active {
+ border-color: #ededed;
+ color: var(--text);
+}
+
+.steps li.active span,
+.steps li.complete span {
+ border-color: #ededed;
+ background: #ededed;
+ color: #111;
+}
+
+.drawer-form {
+ overflow-y: auto;
+ padding: 17px 22px 20px;
+}
+
+fieldset {
+ display: grid;
+ gap: 11px;
+ margin: 0;
+ border: 0;
+ border-bottom: 1px solid var(--line);
+ padding: 0 0 15px;
+}
+
+fieldset + fieldset {
+ margin-top: 15px;
+}
+
+legend,
+.drawer-scroll h3,
+.auth-step h3,
+.creating-state h3 {
+ margin: 0 0 10px;
+ color: var(--text);
+ font-size: 12px;
+ font-weight: 650;
+}
+
+label {
+ display: grid;
+ gap: 6px;
+ color: var(--text-2);
+ font-size: 10px;
+ font-weight: 550;
+}
+
+label small {
+ color: var(--muted);
+ font-size: 9px;
+ font-weight: 400;
+ line-height: 1.45;
+}
+
+input,
+select,
+textarea {
+ width: 100%;
+ border: 1px solid #343434;
+ border-radius: var(--radius-sm);
+ outline: 0;
+ background: #121212;
+ color: var(--text);
+ padding: 0 11px;
+ font-size: 11px;
+ transition: border-color 120ms ease;
+}
+
+input,
+select {
+ height: 37px;
+}
+
+textarea {
+ resize: vertical;
+ min-height: 68px;
+ padding-top: 10px;
+}
+
+input:focus,
+select:focus,
+textarea:focus {
+ border-color: #777;
+}
+
+.two-columns,
+.three-columns {
+ display: grid;
+ gap: 10px;
+}
+
+.two-columns {
+ grid-template-columns: 0.75fr 1.25fr;
+}
+
+.three-columns {
+ grid-template-columns: repeat(3, 1fr);
+}
+
+.input-with-action {
+ position: relative;
+ display: block;
+}
+
+.input-with-action input {
+ padding-right: 38px;
+}
+
+.input-with-action button {
+ position: absolute;
+ top: 1px;
+ right: 1px;
+ display: inline-flex;
+ width: 36px;
+ height: 35px;
+ align-items: center;
+ justify-content: center;
+ border: 0;
+ background: transparent;
+ color: var(--muted);
+}
+
+.segmented {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ overflow: hidden;
+ border: 1px solid #343434;
+ border-radius: var(--radius-sm);
+}
+
+.segmented button {
+ height: 35px;
+ border: 0;
+ border-right: 1px solid #343434;
+ background: transparent;
+ color: var(--muted);
+ font-size: 10px;
+ font-weight: 600;
+}
+
+.segmented button:last-child {
+ border-right: 0;
+}
+
+.segmented button.active {
+ background: var(--accent-soft);
+ color: var(--text);
+ box-shadow: inset 0 0 0 1px #777;
+}
+
+.form-actions {
+ display: grid;
+ grid-template-columns: 0.8fr 1.2fr;
+ gap: 10px;
+ margin-top: 16px;
+}
+
+.form-actions > button:not(.primary),
+.drawer-actions > button,
+.danger-button {
+ display: flex;
+ min-height: 37px;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+ border: 1px solid #343434;
+ border-radius: var(--radius-sm);
+ background: #111;
+ color: var(--text-2);
+ font-size: 10px;
+ font-weight: 600;
+}
+
+.form-error {
+ margin: 12px 0 0;
+ border-left: 2px solid #ddd;
+ color: #ddd;
+ padding-left: 9px;
+ font-size: 10px;
+ line-height: 1.45;
+}
+
+.auth-step {
+ padding: 24px 22px;
+}
+
+.auth-step h3 {
+ margin-top: 34px;
+ font-size: 17px;
+}
+
+.auth-step > p {
+ margin: -3px 0 20px;
+ color: var(--muted);
+ font-size: 11px;
+}
+
+.back-link {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ text-decoration: none;
+}
+
+.auth-step form {
+ display: grid;
+ gap: 13px;
+}
+
+.code-input {
+ height: 51px;
+ letter-spacing: 0.35em;
+ font-size: 20px;
+ text-align: center;
+}
+
+.full {
+ width: 100%;
+}
+
+.creating-state {
+ display: flex;
+ flex: 1;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ color: var(--muted);
+ text-align: center;
+}
+
+.creating-state h3 {
+ margin: 16px 0 0;
+ font-size: 16px;
+}
+
+.creating-state p {
+ max-width: 270px;
+ line-height: 1.55;
+}
+
+.drawer-scroll {
+ overflow-y: auto;
+ flex: 1;
+ padding: 0 22px;
+}
+
+.drawer-scroll section {
+ border-bottom: 1px solid var(--line);
+ padding: 19px 0;
+}
+
+.resource-grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 8px;
+}
+
+.resource {
+ display: grid;
+ gap: 8px;
+ min-height: 69px;
+ border: 1px solid var(--line);
+ border-radius: var(--radius-sm);
+ background: var(--surface);
+ padding: 11px;
+}
+
+.resource > span,
+.resource small {
+ color: var(--muted);
+ font-size: 9px;
+ font-weight: 500;
+}
+
+.resource strong {
+ font-size: 12px;
+}
+
+.metadata-list {
+ display: grid;
+ gap: 10px;
+ margin: 0;
+}
+
+.metadata-list div {
+ display: grid;
+ gap: 5px;
+}
+
+.metadata-list dt {
+ color: var(--muted);
+ font-size: 9px;
+}
+
+.metadata-list dd {
+ display: flex;
+ min-width: 0;
+ min-height: 34px;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+ margin: 0;
+ border: 1px solid var(--line);
+ border-radius: var(--radius-sm);
+ background: var(--surface);
+ color: var(--text-2);
+ padding: 0 10px;
+ font-size: 10px;
+}
+
+.metadata-list dd span {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.metadata-list dd.wrap {
+ overflow-wrap: anywhere;
+ padding-block: 8px;
+}
+
+.copy-button {
+ display: inline-flex;
+ align-items: center;
+ border: 0;
+ background: transparent;
+ color: var(--muted);
+ padding: 0;
+}
+
+.section-title {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.logs-section pre {
+ overflow: auto;
+ max-height: 250px;
+ margin: 0;
+ border: 1px solid var(--line);
+ border-radius: var(--radius-sm);
+ background: #080808;
+ color: #aaa;
+ padding: 11px;
+ font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
+ font-size: 9px;
+ line-height: 1.55;
+ white-space: pre-wrap;
+}
+
+.danger-zone p {
+ color: var(--muted);
+ font-size: 10px;
+ line-height: 1.5;
+}
+
+.checkbox-row {
+ display: flex;
+ grid-template-columns: auto 1fr;
+ align-items: center;
+ gap: 8px;
+ margin-top: 10px;
+}
+
+.checkbox-row input {
+ width: 14px;
+ height: 14px;
+}
+
+.danger-button {
+ width: 100%;
+ margin-top: 11px;
+}
+
+.drawer-actions {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 8px;
+ border-top: 1px solid var(--line);
+ padding: 13px 22px 16px;
+}
+
+.drawer-actions .primary {
+ min-height: 37px;
+}
+
+.drawer-actions .delete-trigger {
+ grid-column: span 2;
+ color: #8b8b8b;
+}
+
+.mobile-menu,
+.mobile-overlay {
+ display: none;
+}
+
+.spin {
+ animation: spin 850ms linear infinite;
+}
+
+@keyframes spin {
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@keyframes drawer-in {
+ from {
+ transform: translateX(18px);
+ opacity: 0.6;
+ }
+}
+
+@media (max-width: 980px) {
+ .table-grid {
+ grid-template-columns: minmax(120px, 1fr) 100px minmax(145px, 1.25fr) 60px 75px 116px;
+ }
+
+ .table-grid > :nth-child(6) {
+ display: none;
+ }
+
+ .summary-strip div {
+ padding: 0 14px;
+ }
+}
+
+@media (max-width: 760px) {
+ .app-shell {
+ display: block;
+ }
+
+ aside[aria-label="Навигация"] {
+ width: 240px;
+ transform: translateX(-100%);
+ transition: transform 160ms ease;
+ }
+
+ aside[aria-label="Навигация"].open {
+ transform: translateX(0);
+ }
+
+ .brand .mobile-close {
+ display: inline-flex;
+ border: 0;
+ background: transparent;
+ color: var(--muted);
+ }
+
+ .mobile-overlay {
+ position: fixed;
+ z-index: 19;
+ inset: 0;
+ display: block;
+ border: 0;
+ background: rgba(0, 0, 0, 0.65);
+ }
+
+ .workspace {
+ width: 100%;
+ }
+
+ .mobile-menu {
+ display: inline-flex;
+ }
+
+ .topbar {
+ height: 55px;
+ padding: 0 13px;
+ }
+
+ .live-state {
+ display: none;
+ }
+
+ .namespace {
+ display: none;
+ }
+
+ .topbar-actions {
+ margin-left: auto;
+ }
+
+ .content {
+ padding: 18px 13px 32px;
+ }
+
+ .summary-strip {
+ grid-template-columns: 1fr 1fr;
+ }
+
+ .summary-strip div {
+ min-height: 53px;
+ border-bottom: 1px solid var(--line);
+ }
+
+ .summary-strip div:nth-child(2) {
+ border-right: 0;
+ }
+
+ .summary-strip div:nth-child(3),
+ .summary-strip div:nth-child(4) {
+ border-bottom: 0;
+ }
+
+ .toolbar {
+ grid-template-columns: 1fr 120px 36px;
+ }
+
+ .table-head {
+ display: none;
+ }
+
+ .table-frame {
+ border-radius: var(--radius-sm);
+ }
+
+ .instance-row.table-grid {
+ display: grid;
+ min-height: 75px;
+ grid-template-columns: minmax(0, 1fr) auto;
+ gap: 7px 12px;
+ padding: 10px 9px 10px 12px;
+ }
+
+ .instance-row > :nth-child(2) {
+ justify-self: end;
+ }
+
+ .instance-row > :nth-child(3) {
+ grid-column: 1;
+ color: var(--muted);
+ font-size: 9px;
+ }
+
+ .instance-row > :nth-child(4),
+ .instance-row > :nth-child(5),
+ .instance-row > :nth-child(6) {
+ display: none;
+ }
+
+ .row-actions {
+ grid-column: 2;
+ grid-row: 2;
+ }
+
+ .row-actions .icon-button:first-child,
+ .row-actions .icon-button:last-child {
+ display: none;
+ }
+
+ .drawer,
+ .details-drawer {
+ width: 100vw;
+ }
+}
+
+@media (max-width: 480px) {
+ .toolbar {
+ grid-template-columns: 1fr 36px;
+ }
+
+ .toolbar select {
+ grid-column: 1;
+ grid-row: 2;
+ }
+
+ .toolbar .refresh {
+ grid-column: 2;
+ grid-row: 1;
+ }
+
+ .two-columns {
+ grid-template-columns: 1fr;
+ }
+
+ .three-columns {
+ gap: 7px;
+ }
+
+ .drawer-header,
+ .drawer-form,
+ .auth-step,
+ .drawer-actions {
+ padding-inline: 16px;
+ }
+
+ .steps,
+ .drawer-scroll {
+ padding-inline: 16px;
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ *,
+ *::before,
+ *::after {
+ scroll-behavior: auto !important;
+ animation-duration: 0.01ms !important;
+ animation-iteration-count: 1 !important;
+ transition-duration: 0.01ms !important;
+ }
+}
diff --git a/panel/frontend/src/test/setup.ts b/panel/frontend/src/test/setup.ts
new file mode 100644
index 0000000..f149f27
--- /dev/null
+++ b/panel/frontend/src/test/setup.ts
@@ -0,0 +1 @@
+import "@testing-library/jest-dom/vitest";
diff --git a/panel/frontend/svelte.config.js b/panel/frontend/svelte.config.js
new file mode 100644
index 0000000..d0e6448
--- /dev/null
+++ b/panel/frontend/svelte.config.js
@@ -0,0 +1,5 @@
+import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
+
+export default {
+ preprocess: vitePreprocess(),
+};
diff --git a/panel/frontend/tsconfig.json b/panel/frontend/tsconfig.json
new file mode 100644
index 0000000..de8f686
--- /dev/null
+++ b/panel/frontend/tsconfig.json
@@ -0,0 +1,17 @@
+{
+ "compilerOptions": {
+ "allowJs": true,
+ "checkJs": true,
+ "esModuleInterop": true,
+ "forceConsistentCasingInFileNames": true,
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "skipLibCheck": true,
+ "sourceMap": true,
+ "strict": true,
+ "target": "ES2022",
+ "module": "ESNext",
+ "types": ["svelte", "vite/client", "vitest/globals"]
+ },
+ "include": ["src/**/*.ts", "src/**/*.svelte", "vite.config.ts"]
+}
diff --git a/panel/frontend/vite.config.ts b/panel/frontend/vite.config.ts
new file mode 100644
index 0000000..d1d3221
--- /dev/null
+++ b/panel/frontend/vite.config.ts
@@ -0,0 +1,15 @@
+import { svelte } from "@sveltejs/vite-plugin-svelte";
+import { defineConfig } from "vite";
+
+export default defineConfig({
+ plugins: [svelte()],
+ build: {
+ outDir: "dist",
+ emptyOutDir: true,
+ },
+ server: {
+ proxy: {
+ "/api": "http://127.0.0.1:8080",
+ },
+ },
+});
diff --git a/panel/frontend/vitest.config.ts b/panel/frontend/vitest.config.ts
new file mode 100644
index 0000000..69cf664
--- /dev/null
+++ b/panel/frontend/vitest.config.ts
@@ -0,0 +1,8 @@
+import { defineConfig } from "vitest/config";
+
+export default defineConfig({
+ test: {
+ environment: "jsdom",
+ setupFiles: ["./src/test/setup.ts"],
+ },
+});