diff --git a/.dockerignore b/.dockerignore index 239ad9ea..2914ba74 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,23 +1,9 @@ .git -.venv -venv -__pycache__ -*.pyc -.pytest_cache - -frontend/node_modules -frontend/dist -frontend/*.tsbuildinfo -backend/.pytest_cache -backend/**/*.pyc -backend/**/__pycache__ - -storage -postgres-data -datasets/raw -datasets/processed -datasets/cache -exports -models - -.env +node_modules +dist +coverage +playwright-report +test-results +data +.env* +*.log diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..1014ba78 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false diff --git a/.env.example b/.env.example index 716e7ce8..b6c4c4c5 100644 --- a/.env.example +++ b/.env.example @@ -1,153 +1,100 @@ -# Backend -GEOINTEL_ENV=development -GEOINTEL_API_PREFIX=/api/v1 -DATABASE_URL=postgresql+psycopg://geointel:geointel@localhost:5432/geointel?connect_timeout=1 -STORAGE_ROOT=./storage -MAX_UPLOAD_MB=500 -CORS_ORIGINS=http://localhost:1202,http://127.0.0.1:1202 -ORTHOPHOTO_ENABLED=true -ORTHOPHOTO_WMS_URL=https://geo.api.vlaanderen.be/OMWRGBMRVL/wms -ORTHOPHOTO_WMS_LAYER=Ortho -ORTHOPHOTO_RESOLUTION_M=1.0 -ORTHOPHOTO_MIN_SIDE_M=128 -ORTHOPHOTO_MAX_SIDE_M=1024 -ORTHOPHOTO_CACHE_TTL_HOURS=24 -SOURCE_CATALOG_PROBE_ENABLED=true -SOURCE_CATALOG_GRB_WFS_URL=https://geo.api.vlaanderen.be/GRB/wfs -GRB_ENABLED=true -GRB_OGC_API_URL=https://geo.api.vlaanderen.be/GRB/ogc/features/v1 -GRB_MIN_SIDE_M=10 -GRB_MAX_SIDE_M=20000 -GRB_PAGE_SIZE=1000 -GRB_MAX_PAGES=200 -GRB_MAX_FEATURES=150000 -GRB_TIMEOUT_SECONDS=180 -GRB_MAX_RESPONSE_MB=20 -GRB_MAX_TOTAL_RESPONSE_MB=256 -GRB_CACHE_TTL_HOURS=24 -OFFICIAL_VECTOR_ENABLED=true -BWK_WFS_URL=https://geo.api.vlaanderen.be/BWK/wfs -DOV_SOIL_WFS_URL=https://www.dov.vlaanderen.be/geoserver/wfs -SPW_PICC_ENABLED=true -SPW_PICC_MAPSERVER_URL=https://geoservices.wallonie.be/arcgis/rest/services/TOPOGRAPHIE/PICC_VDIFF/MapServer -SPW_FLOOD_HAZARD_ENABLED=true -SPW_FLOOD_HAZARD_MAPSERVER_URL=https://geoservices.wallonie.be/arcgis/rest/services/EAU/ALEA_INOND/MapServer -URBIS_ENABLED=true -URBIS_WFS_URL=https://geoservices-vector.irisnet.be/geoserver/urbisvector/ows -OFFICIAL_VECTOR_MIN_SIDE_M=10 -OFFICIAL_VECTOR_MAX_SIDE_M=20000 -OFFICIAL_VECTOR_PAGE_SIZE=1000 -OFFICIAL_VECTOR_MAX_PAGES=200 -OFFICIAL_VECTOR_MAX_FEATURES=100000 -OFFICIAL_VECTOR_TIMEOUT_SECONDS=180 -OFFICIAL_VECTOR_MAX_RESPONSE_MB=20 -OFFICIAL_VECTOR_MAX_TOTAL_RESPONSE_MB=256 -OFFICIAL_VECTOR_CACHE_TTL_HOURS=24 -SOURCE_CATALOG_STATBEL_DCAT_URL=https://doc.statbel.be/publications/DCAT/DCAT_opendata_datasets.ttl -SOURCE_CATALOG_STATBEL_MAX_RESPONSE_MB=5 -SOURCE_CATALOG_ALZ_RELEASE_URL=https://landbouwcijfers.vlaanderen.be/open-geodata-landbouwgebruikspercelen -SOURCE_CATALOG_PROBE_TIMEOUT_SECONDS=10 -SOURCE_CATALOG_PROBE_MAX_RESPONSE_MB=2 -SOURCE_CATALOG_PROBE_CACHE_TTL_SECONDS=900 -DHMV_ENABLED=true -DHMV_WCS_URL=https://geo.api.vlaanderen.be/DHMV/wcs -DHMV_RESOLUTION_M=5.0 -DHMV_MIN_SIDE_M=10 -DHMV_MAX_SIDE_M=20000 -DHMV_MAX_PIXELS=12000000 -DHMV_TIMEOUT_SECONDS=300 -DHMV_MAX_RESPONSE_MB=160 -FLOOD_HAZARD_ENABLED=true -FLOOD_HAZARD_WCS_URL=https://geoservice.waterinfo.be/OGRK/wcs -FLOOD_HAZARD_RESOLUTION_M=5.0 -FLOOD_HAZARD_MIN_SIDE_M=10 -FLOOD_HAZARD_MAX_SIDE_M=20000 -FLOOD_HAZARD_MAX_PIXELS=12000000 -FLOOD_HAZARD_TIMEOUT_SECONDS=300 -FLOOD_HAZARD_MAX_RESPONSE_MB=160 -BATHYMETRY_PROFILES_ENABLED=true -BATHYMETRY_PROFILES_LAYER_URL=https://vha.waterinfo.be/arcgis/rest/services/digitale_atlas/MapServer/0 -BATHYMETRY_WATERCOURSE_LAYER_URL=https://vha.waterinfo.be/arcgis/rest/services/digitale_atlas/MapServer/1 -BATHYMETRY_PROFILES_PAGE_SIZE=1000 -BATHYMETRY_PROFILES_MAX_FEATURES=50000 -BATHYMETRY_PROFILES_TIMEOUT_SECONDS=120 -BATHYMETRY_PROFILES_MAX_RESPONSE_MB=32 -MDK_BATHYMETRY_PROBE_ENABLED=true -MDK_BATHYMETRY_WCS_URL=https://bathy.agentschapmdk.be/spatialfusionserver/services/ows/wcs/EL_wcs -MDK_BATHYMETRY_PROBE_TIMEOUT_SECONDS=20 -MDK_BATHYMETRY_PROBE_MAX_RESPONSE_MB=4 -# Bounded MDK acquisition stays fail-closed until the readiness probe reports -# "reachable" and an advertised coverage id is configured explicitly. -MDK_BATHYMETRY_ACQUISITION_ENABLED=false -MDK_BATHYMETRY_COVERAGE_ID= -MDK_BATHYMETRY_REQUEST_CRS=EPSG:4326 -MDK_BATHYMETRY_MAX_BBOX_DEG2=0.25 -MDK_BATHYMETRY_ACQUISITION_TIMEOUT_SECONDS=120 -MDK_BATHYMETRY_ACQUISITION_MAX_RESPONSE_MB=160 -THEMATIC_RASTER_ENABLED=true -THEMATIC_RASTER_WCS_URL=https://www.mercator.vlaanderen.be/raadpleegdienstenmercatorpubliek/wcs -THEMATIC_RASTER_MIN_SIDE_M=100 -THEMATIC_RASTER_MAX_SIDE_M=60000 -THEMATIC_RASTER_MAX_PIXELS=30000000 -THEMATIC_RASTER_TIMEOUT_SECONDS=300 -THEMATIC_RASTER_MAX_RESPONSE_MB=160 -WALOUS_ENABLED=true -WALOUS_SOURCE_DIR=/app/storage/source-cache/walous -WALOUS_ANALYSIS_RESOLUTION_M=10 -WALOUS_MAX_SIDE_M=60000 -WALOUS_MAX_PIXELS=36000000 -YOLO_ENABLED=false -YOLO_MODELS_DIR=/app/models -YOLO_MODEL_PATH= -YOLO_MODEL_ID=yolo-configured -YOLO_MODEL_DISPLAY_NAME=Configured YOLO detector -YOLO_MODEL_VERSION= -YOLO_CONFIG_DIR=./storage/ultralytics -YOLO_DEVICE=cpu -YOLO_IMAGE_SIZE=640 -YOLO_MAX_TILES=100 -YOLO_MAX_DETECTIONS=1000 -YOLO_DUPLICATE_IOU_THRESHOLD=0.5 -YOLO_BATCH_SIZE=1 +# Application (local-only by default) +HOST=127.0.0.1 +PORT=3000 +DATABASE_PATH=./data/dockdeck.db +INTEGRATION_ENV_PATH=./data/integrations.env +POLLING_INTERVAL_MS=30000 -# Local segmentation models. GeoIntel never downloads model weights -# automatically; point these to existing local files to enable inference. -YOLO_SEG_ENABLED=false -YOLO_SEG_MODEL_PATH= -YOLO_SEG_MODEL_ID=yolo-seg-configured -YOLO_SEG_MODEL_DISPLAY_NAME=Configured YOLO segmentation -YOLO_SEG_MODEL_VERSION= -SAM_ENABLED=false -SAM_MODEL_PATH= -SAM_MODEL_ID=sam-configured -SAM_MODEL_DISPLAY_NAME=Configured SAM segmentation -SAM_MODEL_VERSION= -SEGMENTATION_MAX_MASKS_PER_TILE=300 -SEGMENTATION_DUPLICATE_IOU_THRESHOLD=0.5 -ENABLE_GRB_WFS=false -GRB_WFS_URL= -OSM_OVERPASS_URL=https://overpass-api.de/api/interpreter +# Docker Compose publication (use the Unraid LAN IP for a trusted-LAN deployment) +DOCKDECK_BIND_ADDRESS=127.0.0.1 +DOCKDECK_PORT=1218 +DOCKDECK_INTERNAL_SUBNET=172.31.240.0/28 +DOCKDECK_EGRESS_SUBNET=172.31.240.16/28 -# Install backend raster dependencies when needed: -# python -m pip install rasterio +# Discovery. MOCK_DISCOVERY is intended for local development and tests only. +MOCK_DISCOVERY=true +APPOPS_URL= +DOCKER_PROXY_URL= +UNRAID_TEMPLATES_PATH=/unraid-templates +UNRAID_ICONS_SOURCE_PATH=/unraid-icons-source +UNRAID_ICONS_PATH=/unraid-icons-cache +UNRAID_ICONS_DIR=/boot/config/plugins/dockerMan/images +UNRAID_HOST=tower.local +UNRAID_URL=http://tower.local:5000 +UNRAID_RUNTIME_PATH=/unraid-runtime +UNRAID_HWMON_PATH=/host-hwmon +UNRAID_CPUINFO_PATH=/host-proc/cpuinfo +UNRAID_MEMINFO_PATH=/host-proc/meminfo +UNRAID_UPS_STATUS_PATH= -# Frontend -VITE_API_BASE_URL= -VITE_API_PROXY_TARGET=http://localhost:8000 -# Leave empty to use the local/demo OpenStreetMap fallback with visible attribution. -# Set this to a managed MapLibre style URL for production or heavier tile traffic. -VITE_MAP_STYLE_URL= +# Optional read-only Nginx Proxy Manager connection. Keep credentials server-side. +NPM_URL= +NPM_TOKEN= +NPM_USERNAME= +NPM_PASSWORD= -# Docker Compose / Unraid -GEOINTEL_FRONTEND_PORT=1202 -GEOINTEL_BACKEND_PORT=8000 -GEOINTEL_INSTALL_AI=false -GEOINTEL_STORAGE_PATH=./storage -GEOINTEL_BACKUPS_PATH=./backups -GEOINTEL_MODELS_PATH=./models -GEOINTEL_POSTGIS_DATA_PATH=./postgres-data -GEOINTEL_POSTGRES_DB=geointel -GEOINTEL_POSTGRES_USER=geointel -GEOINTEL_POSTGRES_PASSWORD=geointel -GEOINTEL_CORS_ORIGINS=http://localhost:1202,http://127.0.0.1:1202 -GEOINTEL_MAX_UPLOAD_MB=500 +# Optional provider-aware widget APIs. Use dedicated read-only or minimum-scope +# credentials where the provider supports them. Values never reach the browser. +ADGUARD_URL= +ADGUARD_USERNAME= +ADGUARD_PASSWORD= +PLEX_URL= +PLEX_TOKEN= +IMMICH_URL= +IMMICH_API_KEY= +HOME_ASSISTANT_URL= +HOME_ASSISTANT_TOKEN= +OLLAMA_URL= +GLANCES_URL= +TAUTULLI_URL= +TAUTULLI_API_KEY= +PAPERLESS_URL= +PAPERLESS_TOKEN= +SONARR_URL= +SONARR_API_KEY= +RADARR_URL= +RADARR_API_KEY= +LIDARR_URL= +LIDARR_API_KEY= +GITEA_WIDGET_URL= +GITEA_WIDGET_TOKEN= +JELLYFIN_URL= +JELLYFIN_API_KEY= +SEERR_URL= +SEERR_API_KEY= +PROWLARR_URL= +PROWLARR_API_KEY= +AUTHENTIK_URL= +AUTHENTIK_TOKEN= +PORTAINER_URL= +PORTAINER_API_KEY= +GRAFANA_URL= +GRAFANA_TOKEN= +PROMETHEUS_URL= +NEXTCLOUD_URL= +NEXTCLOUD_USERNAME= +NEXTCLOUD_APP_PASSWORD= +AUDIOBOOKSHELF_URL= +AUDIOBOOKSHELF_TOKEN= +NETDATA_URL= +PEERTUBE_URL= + +# Apps whose native authentication requires non-GET requests can use a tiny +# read-only bridge returning { "summary": "...", "stats": [{ "label": "...", "value": "..." }] }. +DELUGE_METRICS_URL= +DELUGE_METRICS_TOKEN= +QBITTORRENT_METRICS_URL= +QBITTORRENT_METRICS_TOKEN= +JDOWNLOADER_METRICS_URL= +JDOWNLOADER_METRICS_TOKEN= +TDARR_METRICS_URL= +TDARR_METRICS_TOKEN= +BAZARR_METRICS_URL= +BAZARR_METRICS_TOKEN= +VAULTWARDEN_METRICS_URL= +VAULTWARDEN_METRICS_TOKEN= + +# Optional one-time private repository initialization +GITEA_URL= +GITEA_OWNER= +GITEA_TOKEN= diff --git a/.gitattributes b/.gitattributes index 72744fa6..13d18b90 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,13 +1,5 @@ -*.sh text eol=lf -deploy/unraid/gosu-setpriv text eol=lf -*.py text eol=lf -*.yml text eol=lf -*.yaml text eol=lf -*.toml text eol=lf -*.ini text eol=lf -Dockerfile text eol=lf -*.md text eol=lf -*.tsx text eol=lf -*.ts text eol=lf -*.css text eol=lf -*.json text eol=lf +* text=auto eol=lf +*.png binary +*.jpg binary +*.jpeg binary +*.webp binary diff --git a/.gitignore b/.gitignore index 6a412359..bc0f0a99 100644 --- a/.gitignore +++ b/.gitignore @@ -1,56 +1,15 @@ -# Python -__pycache__/ -*.py[cod] -.venv/ -venv/ -.env -*.egg-info/ -.pytest_cache/ -.ruff_cache/ - -# Node node_modules/ dist/ -build/ +coverage/ +playwright-report/ +test-results/ +data/*.db +data/*.db-* +data/wallpaper.* +.env +.env.* +!.env.example +*.log *.tsbuildinfo - -# Large local data -/artifacts/ -/.cache/ -/datasets/raw/* -/datasets/processed/* -/datasets/cache/* -/storage/uploads/* -/storage/tiles/* -/storage/masks/* -/storage/reports/* -/storage/exports/* -/storage/rasters/* -/storage/models/* -/storage/operator-data/* -/storage/operator-evidence/* -/storage/release-evidence/* -/storage/previews/* -/storage/training/* -/storage/ultralytics/* -/exports/* -/models/* -/backend/storage/uploads/* -/backend/storage/tiles/* -/backend/storage/masks/* -/backend/storage/reports/* -/backend/storage/exports/* -/backups/* -/postgres-data/* - -# Keep folder placeholders -!**/.gitkeep -!**/README.md - -# Runtime-generated operator documentation is not repository documentation. -/storage/operator-data/README.md - -# OS/editor .DS_Store -.vscode/ -.idea/ +.codex-input/ diff --git a/AGENTS.md b/AGENTS.md index d9450a2a..84ed7459 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,44 +1,59 @@ -# AI Agent Instructions for GeoIntel +# AGENTS.md -## Project identity +Project: DockDeck -GeoIntel is a GeoAI Workbench for Belgium and the Belgian North Sea, not a -generic CRUD app and not a generic dashboard. Mol and the Kempen remain golden -regression areas, not the product boundary. +Dit project gebruikt het Codex Project Operating System. -## Required behavior +## Werkmodus -- Read `docs/CODEX_BOOTSTRAP_PROMPT.md` first. -- Respect `docs/RC_SCOPE_FREEZE_BELGIUM_NORTH_SEA.md`. -- Use `docs/API_CONTRACTS.md` as source of truth for endpoints. -- Use `docs/DATABASE_IMPLEMENTATION_PLAN.md` as source of truth for persistence. -- Use `docs/DEFINITION_OF_DONE.md` to decide whether work is complete. +Volg deze stappen bij elke wijziging: begrijpen, ontwerpen, plannen, bouwen, testen, verbeteren, documenteren, committen. -## Agent roles +Startmodus: MVP bouwen -### Architecture Agent +- Codex mag scaffolden, bouwen, testen, committen en pushen wanneer de repo en remote duidelijk zijn. +- Bouw de kleinste bruikbare verticale flow eerst. +- Vraag alleen om hulp bij ontbrekende credentials, onduidelijke productkeuzes of risicovolle externe acties. -Owns repository layout, API contracts, database migrations and service boundaries. +## Autonomie -### GIS Agent +- Werk zelfstandig door waar de intentie duidelijk is. +- Stel alleen vragen wanneer een keuze het productgedrag, de architectuur of de veiligheid wezenlijk verandert. +- Houd wijzigingen klein, toetsbaar en passend bij het bestaande project. +- Gebruik de vastgelegde runtime-, sync-, persistentie-, deploy- en repo-keuzes als startpunt. +- Werk projectdocumentatie bij zodra besluiten, risico's of verificatie veranderen. -Owns GeoPandas, Shapely, Rasterio, CRS, clipping, buffering, spatial joins and metadata extraction. +## Autonomiegrenzen -### AI Agent +- Commit en push alleen wanneer de repo en remote duidelijk zijn. +- Deploy alleen wanneer het deploymentdoel, poort en credentials expliciet bekend zijn. +- Vraag om input bij ontbrekende credentials, betaalde diensten of destructieve acties. +- Stop en vraag bevestiging wanneer een actie gebruikersdata kan wijzigen, verwijderen of publiceren. -Owns YOLO/SAM abstractions, inference contracts, model configuration, detection/segmentation persistence and `not_configured` behavior. +## Veiligheid -### QA Agent +- Commit nooit secrets. +- Plaats geen API keys, tokens, wachtwoorden of klantdata in code, tests of logs. +- Verwacht credentials: Ja. +- Security/privacy notities: Gebruik uitsluitend environmentvariabelen voor gevoelige configuratie. Voorzie minimaal variabelen zoals GITEA_URL, GITEA_TOKEN en GITEA_OWNER voor repository-initialisatie en optionele NPM_URL, NPM_USERNAME en NPM_PASSWORD of een ondersteund read-only token voor Nginx Proxy Manager. Gebruik voor de Docker-koppeling geen onbeperkte socketmount in de applicatiecontainer, maar een afzonderlijke proxy met alleen de minimaal noodzakelijke read-endpoints. Sla secrets nooit op in SQLite, exports, logs, frontendbundels, testfixtures of Git. Toon gevoelige integratiewaarden gemaskeerd in de UI. De app mag geen Docker-mutaties uitvoeren en mag nooit containers starten, stoppen, herstarten, verwijderen of aanpassen. De MVP heeft geen eigen authenticatie en moet daarom standaard alleen op een lokaal bindadres of afgeschermd Docker-netwerk worden gepubliceerd. Externe toegang via Nginx Proxy Manager wordt alleen voorbereid; correcte authenticatie en toegangscontrole via bijvoorbeeld Authentik blijven de verantwoordelijkheid van een latere deploymentstap. -Owns tests, QA/QC metrics, regression checks and acceptance criteria. +## Quality Gate -### Frontend Agent +- Run de relevante tests voor elke gedragswijziging. +- Run lint/typecheck voor overdracht of commit. +- Noteer testresultaten in TEST_LOG.md. +- Laat bekende risico's achter in RISKS.md en HANDOFF.md. +- Verplichte finale gate: `npm run format:check`, `npm run lint`, `npm run typecheck`, `npm test`, `npm run test:e2e`, `npm run test:security`, `npm run build` en `npm audit --audit-level=moderate`. +- Valideer `docker compose config` en de imagebuild zodra een Docker-runtime beschikbaar is. -Owns React, TypeScript, MapLibre, API client, UI states and workbench UX. +## Productgrenzen -## Never do this +- Houd Docker-, Unraid- en NPM-integraties read-only. +- Voeg nooit Docker start/stop/restart/delete/exec- of andere beheeracties toe. +- Nieuwe discovery-records blijven standaard verborgen; integratiefouten mogen opgeslagen navigatie nooit blokkeren. +- Secrets blijven uitsluitend in environmentvariabelen en mogen niet in API-responses, SQLite, exports, logs, fixtures of browserbundels terechtkomen. +- De app heeft geen authenticatie en blijft standaard op `127.0.0.1` gebonden. -- Do not fake production AI outputs. -- Do not silently skip geospatial validation. -- Do not add auth/multi-user/LiDAR/training before V1 foundation is stable. -- Do not remove documentation to avoid conflicts. +## Resume Instructions + +- Lees AGENTS.md, PROJECT_BRIEF.md, REQUIREMENTS.md, SPEC.md, PLAN.md, DECISIONS.md, TEST_LOG.md, RISKS.md en HANDOFF.md voordat je verder bouwt. +- Vat de huidige staat kort samen, bepaal de volgende stap uit PLAN.md en werk daarna de documentatie bij. diff --git a/DECISIONS.md b/DECISIONS.md new file mode 100644 index 00000000..a302cd97 --- /dev/null +++ b/DECISIONS.md @@ -0,0 +1,65 @@ +# DECISIONS.md + +Project: DockDeck + +## Decision Log + +| ID | Decision | Rationale | Status | +| ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------- | +| D-001 | DockDeck is een interactieve webapp. | Past bij browserstartpagina en lokaal beheer. | Accepted | +| D-002 | TypeScript, React/Vite, Fastify, Drizzle/SQLite, Zod, Vitest en Playwright in één repository. | Kleinste stack die de bindende brief volledig en testbaar dekt. | Accepted | +| D-003 | Fastify serveert in productie REST en de gebouwde React-client. | Eén container en één poort beperken Unraid-complexiteit. | Accepted | +| D-004 | Statuspolling is configureerbaar, standaard 30 seconden, en pauzeert op verborgen tabs. | Actuele status zonder flikkering of onnodige achtergrondbelasting. | Accepted | +| D-005 | SQLite staat in `/data/dockdeck.db`; een idempotente migratie draait vóór serverstart. | Eenvoudige persistente back-up zonder externe database. | Accepted | +| D-006 | De hoofdapp krijgt geen Docker-socket; discovery verloopt via een server-side GET-adapter en alleen de optionele fallbackproxy ziet de socket. | Handhaaft de expliciete veiligheidsgrens. | Accepted | +| D-007 | Git gebruikt lokaal `main`; private Gitea-initialisatie gebeurt via een apart environmentgestuurd script. | Credentials waren lokaal niet beschikbaar en worden nooit opgeslagen. | Accepted | +| D-008 | Integraties rapporteren `connected`, `degraded` of `disabled`; opgeslagen navigatie blijft beschikbaar. | Externe uitval mag het dashboard niet blokkeren. | Accepted | +| D-009 | Exportformaat versie 1 bevat alleen gebruikersconfiguratie, nooit secrets. | Veilige en toekomstig migreerbare back-up. | Accepted | +| D-010 | Visuele voorkeuren worden als uitbreidbare velden in SQLite opgeslagen en bij oudere databases idempotent toegevoegd. | Personalisatie blijft na updates behouden zonder een breaking exportwijziging. | Accepted | +| D-011 | DM Sans en Newsreader worden lokaal uit het applicatiepakket geladen. | De startpagina blijft visueel stabiel en heeft geen externe fontrequest nodig. | Accepted | +| D-012 | Servicewidgets gebruiken read-only runtimegegevens voor maximaal acht expliciet gekozen containers; standaard via AppOps en in de optionele fallback via Dockerstats. | Levert CPU-, geheugen- en statusinformatie zonder appcredentials of Dockerbeheerrechten. | Accepted | +| D-013 | Favorieten leiden zonder override hun icoon af als `/favicon.ico` en vallen bij fouten terug op een monogram. | Automatische first-party iconen zonder externe favicon- of trackingdienst. | Accepted | +| D-014 | De standaard Unraid-widget combineert AppOps-inventaris met minimale hostmounts; providers gebruiken expliciete server-side minimum-scope credentials en runtimefallbacks. | Levert echte dienstfunctionaliteit zonder container-environments te inspecteren, secrets op te slaan of de read-only grens te verruimen. | Accepted | +| D-015 | Providercredentials kunnen in Settings worden geschreven naar een atomisch vervangen `0600`-environmentbestand in het persistente volume; de API echoot waarden nooit terug. | Maakt app-specifieke widgets configureerbaar zonder secrets in SQLite, export, logs, frontendbundel of Git te plaatsen. | Accepted | +| D-016 | Widget Studio gebruikt niet-persistente capabilitymetadata; provideradapters mogen de ontdekte lokale app-origin als URL gebruiken en vragen alleen ontbrekende secrets. | Maakt rijke widgets vindbaar en vaak zero-config zonder dubbel URL-beheer of automatische wijziging van gebruikersvoorkeuren. | Accepted | +| D-017 | Een uit DockDeck verwijderde app wordt op technische containernaam persistent onderdrukt; de Docker-container zelf blijft onaangeraakt. | Geeft echte, restartbestendige opruiming binnen het platform zonder de read-only Dockergrens te doorbreken. | Accepted | +| D-018 | De zoekindex is volledig afgeleid uit de actuele dashboardpayload en bevat ook verborgen apps, aliases, URL's, categorieën, favorieten en de Unraid-host. | Hernoemen en configuratiewijzigingen worden direct vindbaar zonder een aparte stale zoekdatabase. | Accepted | +| D-019 | Widgetformaat en informatielagen zijn expliciete, persistente app-/hostvelden; providerdetectie gebruikt servicetokens en sluit supportcontainers uit. | Geeft echte inhoudscontrole en voorkomt dat brede substringmatches een verkeerde providerkaart opleveren. | Accepted | +| D-020 | Widgetvolgorde staat los van apptegelvolgorde; directe editbediening gebruikt semantische knoppen en blijft zonder drag-and-drop volledig bruikbaar. | Behoudt voorspelbare toetsenbord- en schermlezervolgorde in responsive dense grids. | Accepted | +| D-021 | App- en servergrafieken bewaren maximaal zestig punten alleen in serverprocesgeheugen; iedere widget heeft een begrensde refreshcadans, configureerbaar venster en expliciete freshness. | Geeft nuttige korte trends zonder een monitoringdatabase of onbegrensde hostbelasting. | Accepted | +| D-022 | Unraid-native status komt uit specifieke read-only runtime- en hwmonmounts, nooit uit een verruimde Dockerproxy of mutatie-API. | Levert array- en hostdiepgang met een minimale, controleerbare trust boundary. | Accepted | +| D-023 | Beveiligde applicaties zonder GET-only native API gebruiken optioneel een door de gebruiker beheerde metrics bridge. | Houdt DockDeck aantoonbaar GET-only en maakt toch app-specifieke widgets mogelijk. | Accepted | +| D-024 | Layoutpresets zijn afzonderlijke records en exports; toepassen wijzigt alleen widgetpresentatie, nooit appzichtbaarheid, credentials of containers. | Maakt contextwissels veilig en draagbaar zonder een volledige configuratierestore. | Accepted | +| D-025 | De PWA-serviceworker cachet uitsluitend de applicatieshell en weigert API-caching; diagnostics zijn secretvrij. | Voorkomt dat operationele data of credentials als offline autoriteit in de browser blijven staan. | Accepted | +| D-026 | Iedere ontdekte app krijgt capabilitymetadata en een optionele GET-only JSON-koppeling wanneer geen native adapter bestaat. | Dekt ook eigen en niche-apps af zonder onveilige beheer-API's, containerspecifieke secrets of generieke schijnmetrics. | Accepted | +| D-027 | Zichtbaarheid en volgorde van appmetrics worden per widget als labels opgeslagen en met live providerlabels samengevoegd. | Laat inhoud aanpassen zonder het opslagmodel aan iedere nieuwe provider of metric te koppelen. | Accepted | +| D-028 | Tower-identiteit is gebruikersconfiguratie; een ontbrekende poort in de Unraid-basis-URL wordt als `5000` geïnterpreteerd. | Herstelt de werkelijke lokale WebUI-link en houdt toekomstige hostnaam- of poortwijzigingen in de UI beheerbaar. | Accepted | +| D-029 | De standaard Unraid-deployment bestaat uit één branded DockDeck-container en gebruikt de bestaande AppOps GET-inventaris; een losse socketproxy is uitsluitend een optionele overlay. | Voldoet aan eenvoudig Unraid-beheer zonder de Docker-socket of mutatierechten aan DockDeck te geven. | Accepted | +| D-030 | Capabilitymetadata bepaalt ook de niet-gekoppelde kaartinhoud; ontbrekende appdata wordt expliciet als te koppelen getoond en nooit vervangen door generieke schijnmetrics. | Houdt iedere widget domeinspecifiek zonder waarden te verzinnen en scheidt echte Docker-infrastructuurtelemetrie duidelijk. | Accepted | +| D-031 | Unraid-listmetadata gebruikt een echt transparant PNG-bestand, ook al blijft de PWA vrij om het schaalbare SVG-asset te gebruiken. | Sluit aan op Unraids vaste `.png`-cache en voorkomt een onzichtbaar icoon door een fout MIME-/bestandsformaat. | Accepted | +| D-032 | De Unraid-deployment ververst na een icoonwijziging zowel Dockermans persistente als actieve RAM-cache en valideert vooraf het echte PNG-MIME-type. | DockerMan hergebruikt anders een bestaand actief cachebestand, waarna zijn `onerror` het bedoelde icoon stilzwijgend door `question.png` vervangt. | Accepted | +| D-033 | DockDeck Canon v1 is de bindende visuele bron; conflicterende Obsidian Control-, Core Modernist- en oudere Stitch-varianten worden niet gecombineerd. | Eén expliciet ontwerpsysteem voorkomt een hybride interface en houdt tokens, componentvormen en responsive gedrag onderhoudbaar. | Accepted | +| D-034 | Launcherinhoud en Quick Launch vormen de primaire dashboardhiërarchie; bestaande read-only widgets, presets en diagnostics blijven behouden onder een secundair uitklapbaar inzicht en Algemeen. | Behoudt alle operationele functionaliteit zonder de rustige dagelijkse startflow van Canon v1 te verdringen. | Accepted | +| D-035 | Geist wordt lokaal gebundeld en alle Canon-kleuren, spacing, radii en states lopen via semantische tokens voor dark, light en Harbor. | Voorkomt externe fontafhankelijkheden en maakt themagedrag consistent zonder Canon-waarden door component-CSS te verspreiden. | Accepted | +| D-036 | Playwright draait lokaal op de geïsoleerde API-/webpoorten 3101 en 5174, configureerbaar via environmentvariabelen. | Vermijdt botsing met de bestaande WSL-relay op poort 3000 zonder de productiestandaardpoort of runtimearchitectuur te wijzigen. | Accepted | +| D-037 | De Stitch-launcher combineert op desktop een permanente favorietenrail met zichtbare read-only servicepanelen; op tablet en mobiel blijven favorieten en apps vóór de panelen staan. | Herstelt de sterke dagelijkse navigatie en widgetmogelijkheden uit het oorspronkelijke DockDeck zonder de rustige mobiele Canon-hiërarchie te verliezen. | Accepted | +| D-038 | Appiconen gebruiken eerst expliciete Unraid-/gebruikersmetadata, daarna een lokaal gebundeld merkicoon en ten slotte een semantisch service-icoon; lettermonogrammen vervallen. | Geeft iedere app een herkenbare visuele identiteit zonder CDN-afhankelijkheid en houdt onbekende of eigen diensten toch betekenisvol. | Accepted | +| D-039 | Instellingen zijn centraal doorzoekbaar; verversingsinterval en verminderde beweging zijn expliciete, persistente voorkeuren die ook in versie 1-export/import meegaan. | Maakt de volledige instellingenoppervlakte vindbaar en voorkomt dat bereikbaarheid of polling alleen als verborgen backendcontract bestaan. | Accepted | +| D-040 | Nieuwe discovery-records krijgen een persistente `discoveredAt`-tijd en `isNew`-status; bestaande databases migreren records als erkend en een zichtbaarheidshandeling erkent een nieuw record. | Houdt de ontdekbanner en het filter “Nieuw ontdekt” waarheidsgetrouw zonder bewust verborgen bestaande apps als nieuw te presenteren. | Accepted | +| D-041 | Dashboard en instellingen delen voortaan dezelfde gecentreerde platformcanvas; vanaf 1900 px wordt de widgetlaag automatisch als rechterdock geopend en de appgrid vult ultrawide ruimte vloeiend. | Herstelt de visuele consistentie met de nette instellingenzijbalk, gebruikt brede schermen doelgericht en houdt widgets zichtbaar zonder de launcher te verdringen. | Accepted | +| D-042 | Een geslaagde Docker/AppOps-inventaris is autoritatief: ontbrekende apprecords worden automatisch uit DockDeck verwijderd en naamvergelijking is hoofdletterongevoelig; bij een degraded inventaris wordt niets opgeschoond. | Houdt Instellingen gelijk aan de werkelijk bestaande containers, herstelt actuele status zonder duplicaten en voorkomt dat tijdelijke integratie-uitval opgeslagen configuratie wist. | Accepted | +| D-043 | Unraid DockerMan-iconen worden uit een minimale read-only hostmount naar een tijdelijke containercache gekopieerd; daarna draait de applicatie als de niet-geprivilegieerde `dockdeck`-gebruiker en serveert alleen gekende afbeeldingsformaten. | De persistente Unraid-iconen zijn root-only en de runtimecache is niet volledig. Een begrensde initkopie levert alle echte iconen zonder hostpermissies te wijzigen of blijvende rootrechten te geven. | Accepted | +| D-044 | Slimme categorie-indeling is een expliciete gebruikersactie met geordende, geteste serviceregels; alleen bekende services worden verplaatst en onbekende containers en favorieten blijven ongewijzigd. | Corrigeert aantoonbare bulkfouten zonder automatische heartbeats of toekomstige discovery de bewuste persoonlijke indeling van onbekende apps te laten overschrijven. | Accepted | + +| D-045 | Wallpapers worden als één gevalideerd lokaal PNG/JPEG/WebP-bestand naast de database opgeslagen; weerdata komt server-side en tien minuten gecachet van Open-Meteo. Publieke healthroutes leveren een zero-secret status, terwijl tokens alleen optionele detailstatistieken ontsluiten. | Houdt beelddata en integraties uit SQLite, exports en browserbundels, voorkomt secretlekken en levert toch een rijke dagelijkse startpagina met minimale externe belasting. | Accepted | +| D-046 | Een wallpaper wordt in een sticky stage ter hoogte van het zichtbare centrale viewport gerenderd. Passing, X-/Y-focus en zoom zijn afzonderlijke persistente voorkeuren met `contain` als veilige standaard. | Voorkomt dat lange dashboardinhoud de afbeelding sterk afsnijdt, houdt de volledige afbeelding standaard zichtbaar en geeft de gebruiker gecontroleerde compositievrijheid zonder het bronbestand te wijzigen. | Accepted | +| D-047 | Zodra een wallpaper bestaat, vervangt die de achtergrondpreset op het volledige platformvlak onder de topbalk. Een scherpe instelbare beeldlaag ligt boven een zachte `cover`-kopie; rails, widgets en tegels zijn leesbare glaslagen erboven. | Laat de afbeelding ononderbroken van de linker- tot de rechterkolom lopen en tegelijk volledig zichtbaar blijven bij `contain`, zonder Blauwdruk-, Aurora- of Minimaal-lagen door de wallpaper heen te mengen. | Accepted | +| D-048 | Canonieke categorie-iconen gebruiken één gedeelde catalogus voor keuzelijst én renderer; iedere huidige categorie resolveert naar een unieke Lucide-component en nieuwe categorieën kiezen eerst een ongebruikt icoon. | Voorkomt dat geldige opgeslagen waarden stil naar hetzelfde generieke `Layers3`-beeld terugvallen en houdt toekomstige categorieën visueel onderscheidbaar zonder data te herschrijven. | Accepted | + +## Decision Rules + +| Rule | Reason | +| --------------------------------------------------------------- | ------------------------------------- | +| Leg architectuurkeuzes vast voordat ze breed doorwerken. | Voorkomt impliciete afhankelijkheden. | +| Kies de kleinste oplossing die aan de must-haves voldoet. | Beschermt scope en snelheid. | +| Heropen beslissingen wanneer requirements of risico's wijzigen. | Houdt het dossier betrouwbaar. | diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..db961210 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,29 @@ +# syntax=docker/dockerfile:1.7 +FROM node:22-alpine AS build +WORKDIR /app +RUN apk add --no-cache python3 make g++ +COPY package.json package-lock.json ./ +RUN npm ci +COPY . . +RUN npm run build && npm prune --omit=dev + +FROM node:22-alpine AS runtime +ENV NODE_ENV=production \ + HOST=0.0.0.0 \ + PORT=3000 \ + DATABASE_PATH=/data/dockdeck.db +WORKDIR /app +RUN apk add --no-cache su-exec && \ + addgroup -S dockdeck && \ + adduser -S -G dockdeck dockdeck && \ + mkdir -p /data /host-proc /unraid-icons-cache && \ + chown dockdeck:dockdeck /data /unraid-icons-cache +COPY --from=build --chown=dockdeck:dockdeck /app/package.json ./package.json +COPY --from=build --chown=dockdeck:dockdeck /app/node_modules ./node_modules +COPY --from=build --chown=dockdeck:dockdeck /app/dist ./dist +COPY docker-entrypoint.sh /usr/local/bin/dockdeck-entrypoint +RUN chmod 0755 /usr/local/bin/dockdeck-entrypoint +EXPOSE 3000 +HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 CMD wget -qO- http://127.0.0.1:3000/api/health >/dev/null || exit 1 +ENTRYPOINT ["dockdeck-entrypoint"] +CMD ["node", "dist/server/index.js"] diff --git a/EXPORT_QUALITY.md b/EXPORT_QUALITY.md new file mode 100644 index 00000000..65ceb385 --- /dev/null +++ b/EXPORT_QUALITY.md @@ -0,0 +1,26 @@ +# EXPORT_QUALITY.md + +Project: DockDeck +Generated at: 2026-07-13T18:54:16.667Z +Exportprofiel: Codex volledig +Codex startmodus: MVP bouwen + +## Status + +- Resultaat: Klaar voor Codex +- Samenvatting: De huidige export bevat de verwachte bestanden, metadata en startinstructies. + +## Checks + +| Check | Status | +| ---------------------------------------- | ------ | +| Vereiste input ingevuld | Pass | +| Intake gereed voor autonome start | Pass | +| Bestandenset gecontroleerd | Pass | +| Metadata klopt met profiel en startmodus | Pass | +| Startprompt aanwezig | Pass | +| Autonomiegrenzen aanwezig | Pass | + +## Blokkades + +- Geen blokkades. diff --git a/HANDOFF.md b/HANDOFF.md new file mode 100644 index 00000000..209547bf --- /dev/null +++ b/HANDOFF.md @@ -0,0 +1,173 @@ +# HANDOFF.md + +Project: DockDeck +Updated: 2026-07-22 + +## Current status + +Release `9236094` draait vanaf `main` op `192.168.10.150:1218`. De bestaande `.env` bleef byte-identiek, het named volume, de configuratie en de geüploade wallpaper van 3.245.835 bytes zijn behouden. Image `1cf11fc02e7e…` is healthy met 0 herstarts. DockDeck bevat exact 90 bestaande containers, 48 zichtbare apps, 16 categorieën en 1 favoriet. De zeven livepanelen (zes apps plus Tower) zijn allemaal connected. + +## Current update + +De actuele pass corrigeert de resterende widescreenpunten uit een echte Chrome-audit op 3440×1215. De rechter widgetdock is 540 px breed, zichtbare widget- en instellingentekst blijft minimaal 11–12 px, extreem lange beheeroppervlakken zijn begrensd en Quick Launch laadt het echte favoriete favicon. De expliciete actie `Slim herindelen` gebruikt geteste serviceregels en laat onbekende containers en favorieten ongemoeid. Live zijn 14 foutieve categorieën gecorrigeerd; onder meer Deluge/JDownloader2 staan bij Downloads, Glances/unraid-stats bij Monitoring en Nginx Proxy Manager bij Network & Admin. + +De premium afwerkingspass is live voltooid en op 3440×1215 in Chrome gecontroleerd. App Beheer gebruikt een expliciete 52px-track met een knop die 24px tussen links en rechts beweegt. De vier genoemde beveiligings-/automatiseringsdiensten gebruiken compacte launchertegels. De topbalk toont gecachete actuele Open-Meteo-data voor Mol; Weergave bevat locatie-instellingen en een gevalideerde lokale wallpaper-upload. De placeholders DockDeck, GPU-Hot en Deluge zijn in het widgetdock vervangen door werkende compacte Authentik-, NPM- en Vaultwardenpanelen. Plex en Gitea hebben echte read-only providerdata; Home Assistant blijft correct offline zolang de container niet draait. + +De daaropvolgende achtergrondfix herstelt de feitelijke rendering van Aurora, Blauwdruk, Minimaal en de geüploade wallpaper. De persistentie en wallpaper-API waren correct; een niet-bestaande CSS-token maakte de samengestelde achtergrondregels ongeldig en een te zware overlay hield de afbeelding vrijwel volledig tegen. De achtergrond wordt nu rechtstreeks op de centrale canvas samengesteld, met een afzonderlijke leesbare wallpaper-overlay per preset en expliciete E2E-dekking. + +De live controle op 3440×1215 heeft alle drie presets via de instellingeninterface doorlopen. Hun berekende canvaslagen zijn onderling verschillend, de bestaande JPEG blijft in iedere preset aanwezig, er is geen horizontale overflow en Chrome rapporteert geen fouten. Blauwdruk is als actieve gebruikersvoorkeur hersteld. + +De daaropvolgende wallpaperstudio corrigeert de resterende uitsnede: de afbeelding wordt voortaan tegen het zichtbare centrale viewport geschaald in plaats van tegen de volledige scrollhoogte. `Passend` is de veilige standaard; `Vullend`, horizontale/verticale focus en 80–160% zoom zijn via een gecentreerde live preview instelbaar. Deze waarden zijn server-side gevalideerd, persistent, legacy-compatibel en exporteerbaar. + +De live Chrome-hercontrole op 3440×1215 meet een wallpaperstage van 2381×1143 binnen de 3015 px hoge dashboardcanvas en een gecentreerde studiopreview van 1600×686. De afbeelding is volledig geladen met `Passend`, 50/50-focus en 100% zoom; er is geen horizontale overflow en Chrome rapporteert geen fouten. De draftflow `Vullend` → `Herstellen` is getest zonder de opgeslagen voorkeur of wallpaper te wijzigen. + +De aansluitende platformpass haalt de wallpaper uit de middenkolom. De fixed stage begint exact onder de 72px-topbalk, bestrijkt de volledige viewportbreedte en -hoogte, blijft bij scrollen staan en schakelt de presetcompositie uit zolang een wallpaper bestaat. Een ambient `cover`-kopie vult ultrawide zijruimte achter het scherpe instelbare beeld; de linkerrail, apptegels en rechter widgetdock zijn doorlopende glaslagen erboven. De preview weerspiegelt dezelfde compositie. + +De live Chrome-audit op 3440×1215 meet de stage van x=0/y=72 tot de volledige 3425×1143 browsercontentruimte. Bij 900 px scroll blijft de stage exact op y=72, de presetcanvas is transparant, beide zijpanelen gebruiken 62% glas en er is geen horizontale overflow. Chrome rapporteert geen applicatiefouten; alleen de geïnstalleerde WideFrog-extensie schreef één eigen debug-timeout. + +De categorie-icooncorrectie vult de frontendcatalogus aan met de tien eerder ontbrekende geldige waarden. Daardoor tonen Media, Downloads, Books & Documents, Photos, Home & Automation, Security & Identity, Network & Admin, Monitoring, Development & Projects, AI & ML, Databases & Storage, Infrastructure, Games & Emulation, Tools & Productivity, Experiments & Archived en Favorites ieder een eigen passend pictogram. Nieuwe categorieën kiezen automatisch een nog ongebruikte optie. + +De live Chrome-hercontrole bevestigt 16 categoriekaarten, 16 verschillende opgeslagen waarden en 16 verschillende gerenderde SVG-signaturen. De categorie-iconen zijn zowel in Instellingen als bij de dashboardsecties visueel onderscheidend; Chrome rapporteert 0 waarschuwingen of fouten. + +## What works + +- DockDeck Canon v1 als geïntegreerd productoppervlak, gebaseerd op de expliciete Canon-bronnen uit ZIP en Stitch MCP; oudere conflicterende designrichtingen zijn genegeerd. +- Lokale merkassets voor app/favicon/wordmark en lokaal gebundeld Geist; geen CDN- of tijdelijke beeldafhankelijkheid. +- Launcher-first dashboards met Canon-topbar, ontdekbanner, categoriechips, apptegels en states; desktop voegt een sticky favorietenrail met klok, deckstatus en widgetbeheer toe. +- Echte Unraid-/gebruikersiconen blijven eerste keuze; herkenbare lokale merkiconen en semantische service-iconen vervangen alle lettermonogrammen. +- Quick Launch als modal command palette met Apps, Favorieten en Google, toetsenbordnavigatie, Enter/Escape en focusstijl. +- Compact appsbeheer met filters, zichtbaarheid en paneelkeuze in de rij; naam/categorie/URL/icoon/sortering/verwijderen in een focus-trapped drawer met focusherstel. +- Geïsoleerde E2E-poorten 3101/5174 zodat lokale diensten op 3000 de browsertests niet blokkeren. +- Centrale instellingenzoeker met sectiegerichte trefwoorden en scrollherstel bij sectiewissel. +- Persistente verminderde beweging en configureerbare globale pollingcadans, inclusief legacy-databasemigratie en export/import. +- Canon-categoriekaarten met echte iconen/itemtellingen en veilige back-upimport met JSON-/groottevalidatie, dropzone en herstelbevestiging. +- Expliciete slimme categorieorganisatie voor bekende homelabservices, met behoud van onbekende persoonlijke toewijzingen en favorieten. + +- Single-container read-only discovery via AppOps GET, met een gesaneerde Unraid XML/WebUI-metadataweergave en alleen voor andere hosts een optionele restricted-proxyoverlay. +- Nieuwe containers standaard verborgen; online/offline-status en 30-secondenpolling die pauzeert op verborgen tabs. +- Verdwenen containers worden na een geslaagde autoritatieve inventarislezing automatisch uit DockDeck verwijderd; degraded inventarischecks verwijderen niets. Terugkerende/actief geworden containers en hoofdlettervarianten krijgen bij iedere heartbeat hun actuele status zonder dubbele offline records. +- Volledig appbeheer: zichtbaarheid, naam, categorie, sortering, lokale/externe URL-override en icoonfallback. +- Apps kunnen permanent uit DockDeck worden verwijderd en blijven op technische containernaam onderdrukt bij volgende discovery; Docker blijft onaangeraakt. +- Categorieën, favorieten, totaaloverzicht en categorie-tabs. +- Zoekoverlay voor apps/favorieten plus Google-fallback; alle doelen in een nieuw tabblad. +- De zoekindex bevat actuele namen, technische namen, categorieën, images, URL's, verborgen apps, favorieten en Tower; hernoemen werkt zonder stale index. +- Porcelain, Midnight en Harbor, vier accenten, drie dichtheden, drie contentbreedtes en drie achtergronden; alle voorkeuren zijn persistent. +- Vloeiende mobiele, tablet-, desktop- en ultrawide-layouts met optionele hero/statusweergave en lokale fonts. +- Doorzoekbaar appbeheer met zichtbaarheidsfilters, volledig favorietenbeheer en configureerbare categorie-iconen. +- Configureerbare read-only servicewidgets voor maximaal acht containers met compact/standaard/breed formaat, app-specifieke informatieblokken en eerlijke provider-/infrastructuurstatus. +- Onafhankelijke widgetvolgorde, statselectie/-volgorde/-limiet, metricvolgorde, value/gauge/progress-presentatie, custom label, waarschuwing op iedere numerieke metric, refreshcadans, configureerbare lijn-/vlak-/staafgrafiek tot 60 punten en directe toetsenbordtoegankelijke dashboardeditmodus. +- Layoutpresets kunnen los van de volledige backup worden opgeslagen, toegepast, verwijderd, geëxporteerd en geïmporteerd. +- Een standaard inschakelbare Unraid-hostwidget met eigen formaat-/inhoudskeuzes, selecteerbare serverstats en configureerbare grafieken voor containers, images, geheugen, opslag, temperatuur, parity en UPS. +- Native Unraid array-, parity-, disk-/pool-, temperatuur- en optionele UPS-data via minimale read-only mounts. +- Provider-aware read-only verrijking voor AdGuard, Plex, Immich, Home Assistant en Sonarr/Radarr/Lidarr; zonder credential blijft iedere kaart werken met Dockerstats. +- Uitgebreide GET-only providers voor Gitea, Jellyfin, Seerr, Prowlarr, Authentik, NPM, Portainer, Grafana, Prometheus en Nextcloud, plus configureerbare GET-only bridges voor Deluge, qBittorrent, JDownloader, Tdarr, Bazarr en Vaultwarden. +- Widget Studio met zoeken, aanbevolen/alle/actieve filters, zichtbare metricfeatures, credentialstatus, acht-panelencapaciteit en directe activatie. +- Widget Studio dekt alle 97 opgeslagen apps met 97 expliciete live-inventarisprofielen voor applicaties, API's, frontends, workers, databases, caches, mail, GPU, queues en eigen projecten; er rest geen generiek profiel. +- Per app zijn metriclabels zichtbaar/verbergbaar, tot 24 labels rangschikbaar en persistent in SQLite, presets en exports. +- Apps zonder native provider ondersteunen een optionele GET-only JSON-URL en bearer-token rechtstreeks vanuit hun Widget Studio-kaart; secrets blijven server-side. +- Audiobookshelf, Netdata en PeerTube hebben aanvullende native GET-only adapters. +- Tower-naam en Unraid WebUI-URL zijn persistent instelbaar; een URL zonder poort krijgt standaard `5000`. +- Nieuwe providerwidgets voor Ollama en Glances (zero-config via ontdekte URL), plus Tautulli en Paperless-ngx met minimum-scope credential. +- Extra app-specifieke Dockerpresentaties voor AI-runtimes, documentworkflows en beveiligde diensten. +- Provider-specifieke, gemaskeerde credentialformulieren onder Settings → Integrations; writes zijn same-origin, worden nooit geëchood of geëxporteerd en testen de verbinding direct. +- App-specifieke fallbackkaarten: JDownloader toont actuele download-/uploadsnelheid en AdGuard DNS-verkeer plus een duidelijke API-lockstatus; andere servicetypen krijgen eigen labels en betekenisvolle workloadweergave. +- Automatische first-party favicons voor favorieten, met handmatige override en lokale merk-/semantische fallback. +- Informatief desktopzijpaneel met klok, deck health en snelle links; bredere content- en tegelverdeling op 2560/3440-schermen. +- Favorieten staan als faviconlinks in het scrollbare desktopzijpaneel en de Tower-titel opent rechtstreeks de geconfigureerde Unraid-URL. +- Dashboard en instellingen schalen op dezelfde platformbreedte tot ultrawide; vanaf 1900 px staat de widgetlaag als rechterdock open en gebruikt de appgrid de extra ruimte met begrensde kaartbreedtes. +- Quick Launch toont zonder zoekterm functionele snelkoppelingskaarten en gebruikt bij favorietresultaten dezelfde echte favicon-/override-/fallbackketen als het dashboard. +- Dashboardcategorieknoppen filteren in zowel overzichts- als categoriemodus; de geselecteerde categorie beperkt tegelijk apps en favorieten. +- De topbalk behoudt op Dashboard en Instellingen dezelfde merk-, zoek-, navigatie- en actiekolommen. Dashboard/Instellingen hebben zichtbare hover- en focusstates en Instellingen biedt ook handmatige statusverversing. +- Integraties gebruikt geen compacte kaartmatrix meer maar brede configuratieregels met leesbare URL-/credentialvelden. Paneelstudio toont een duidelijke kies-toevoegen-afstemmenflow en één brede service per rij. +- SQLite-persistentie, idempotente eerste migratie en versie 1 export/import zonder secrets. +- Optionele read-only NPM-hostlezing en ondubbelzinnige matching met veilige fallback. +- Productiebuild, health endpoint, Dockerfile, hardened Compose en veilige Gitea-init/pushscript. +- Installable PWA-shell zonder API-cache, freshness/stale-cache en secretvrije operationele diagnostics. + +## Directe Stitch-pass + +- Het dashboard volgt nu rechtstreeks de Canon-volgorde: compacte topbar, begroeting/ontdekking, categoriepilletjes, favorieten, standaard servicekaarten en brede featurekaarten. +- Quick Launch gebruikt de centrale Canon-sheet met gegroepeerde Apps, Favorieten en Google, inclusief toetsenbordselectie. +- App Beheer is de Canon-tabel met status, categorie, zichtbaarheid en actie; de editor is een vaste rechter drawer met zichtbaarheid, categorie en echte URL-contracten. +- Weergave zet de drie themapreviews en de Canon-keuzes voor dashboardmodus/grid-dichtheid in de eerste viewport; aanvullende bestaande voorkeuren blijven lager beschikbaar. +- De mobiele compositie gebruikt de Canon-ontdekkaart, aparte zoekbalk, ronde categorieknoppen, touchvriendelijke kaartfamilies en vaste ondernavigatie. +- Read-only servicepanelen zijn opnieuw een zichtbare Stitch-sectie op desktop en staan op compactere viewports na de launcherinhoud; de volledige Paneelstudio blijft rechtstreeks bereikbaar onder Instellingen → Algemeen. +- Finale screenshots staan in `docs/design/dockdeck-canon-v1/implementation/` en worden door Playwright op 390×844, 768×1024, 1440×900 en 3440×1440 vastgelegd. + +## Run instructions + +Lokaal: + +```bash +cp .env.example .env +npm install +npm run dev +``` + +Open `http://127.0.0.1:5173`. Met `MOCK_DISCOVERY=true` worden deterministische voorbeeldcontainers gebruikt. Lokale data staat in `./data/dockdeck.db`. + +Unraid gebruikt `deploy/unraid.env.example` als `.env`: + +```dotenv +DOCKDECK_BIND_ADDRESS=192.168.10.150 +DOCKDECK_PORT=1218 +DOCKDECK_EGRESS_SUBNET=172.31.240.16/28 +APPOPS_URL=http://192.168.10.150:1216 +UNRAID_TEMPLATES_DIR=/mnt/user/appdata/dockdeck/unraid-templates +UNRAID_HOST=192.168.10.150 +``` + +```bash +python3 deploy/sanitize-unraid-templates.py \ + /boot/config/plugins/dockerMan/templates-user \ + /mnt/user/appdata/dockdeck/unraid-templates +docker compose up -d --build +docker compose ps +sh deploy/refresh-unraid-icon.sh +``` + +De deployment staat in `/mnt/user/appdata/dockdeck`. De base Compose bevat één service/container met de naam `DockDeck`; `docker-compose.socket-proxy.yml` is alleen een fallback voor een host zonder AppOps. De standaardpoort is `1218` en bindt zonder override alleen op `127.0.0.1`. De huidige deployment bindt bewust aan `192.168.10.150` voor het vertrouwde LAN. Bescherm iedere reverse-proxyroute, want de MVP heeft geen login. Persistente data staat in named volume `dockdeck_dockdeck_data`: configuratie in `/data/dockdeck.db` en via Settings ingevoerde secrets in `/data/integrations.env` (`0600`). Bescherm ruwe volumeback-ups. + +## Validation + +```bash +npm run format:check +npm run lint +npm run typecheck +npm test +npm run test:security +npm run test:e2e +npm run build +npm audit --audit-level=moderate +``` + +Laatste gate: format, lint en typecheck groen; 48/48 unit-/integratietests; 5/5 securitytests; 12 uitgevoerde en 12 project-gededupliceerde Playwright-checks op vier viewports; production build; 0 kwetsbaarheden. Op Tower zijn `docker compose config --quiet`, imagebuild en `up -d` geslaagd. De live container is healthy met 0 herstarts, AppOps leest 90 containers GET-only en de browseraudit rapporteert 0 consolefouten/-warnings. + +### Latest local gate + +Format, lint en typecheck zijn groen; 48/48 unit-/integratietests; 5/5 securitytests; 12 uitgevoerde en 12 project-gededupliceerde Playwright-checks op vier viewports; production build; 0 kwetsbaarheden. De clientbuild is 364,55 kB JS (113,69 kB gzip) en 170,68 kB CSS (28,53 kB gzip); de serverbundle is 167,2 kB. De live browseraudit op 1440×900 mat op beide pagina's topbar `left=32`/`height=72`, settings-sidebar `left=0`, geen overflow en een werkende hoverstate. De schone live tab rapporteerde 0 consolefouten/-warnings; alle 5 lokaal ontdekte Unraid-iconen gaven HTTP 200. + +## Environment + +- App: `HOST`, `PORT`, `DATABASE_PATH`, `INTEGRATION_ENV_PATH`, `POLLING_INTERVAL_MS`. +- Deployment: `DOCKDECK_BIND_ADDRESS`, `DOCKDECK_PORT`, `DOCKDECK_EGRESS_SUBNET`. +- Discovery: standaard `APPOPS_URL`; optioneel `DOCKER_PROXY_URL` via de fallbackoverlay; daarnaast `UNRAID_TEMPLATES_PATH`, `UNRAID_ICONS_SOURCE_PATH`, `UNRAID_ICONS_PATH`, `UNRAID_HOST` en `UNRAID_URL`. `MOCK_DISCOVERY` is alleen voor dev/test. +- Optioneel NPM: aanbevolen `NPM_URL` + `NPM_TOKEN`, of `NPM_USERNAME` + `NPM_PASSWORD`. +- Optionele widgets: bestaande providercredentials, `AUDIOBOOKSHELF_TOKEN` en per-app `CUSTOM_WIDGET__URL`/`_TOKEN`; alles is via Widget Studio instelbaar. URL's worden waar mogelijk uit discovery afgeleid. Ollama, Glances, Netdata en PeerTube hebben standaard geen credential nodig. +- Repository: `GITEA_URL`, `GITEA_OWNER`, `GITEA_TOKEN` voor `npm run gitea:init`. + +## Repository status + +- Lokale Git-branch: `codex/stitch-canon-ui-upgrade`. +- Remote: `origin` is geconfigureerd als `gitea-widefrog:NuklearRabbit/DockDeck.git`. +- Private repository: `NuklearRabbit/DockDeck`; SSH-auth werkt en `codex/stitch-canon-ui-upgrade` volgt de gelijknamige gepushte `origin`-branch. + +## Open external checks + +1. Indien gewenst: NPM-token instellen en één ondubbelzinnige externe host controleren. +2. Na wijzigingen aan Unraid-templates: de sanitizer opnieuw uitvoeren en DockDeck synchroniseren. +3. De huidige testrelease zelf beoordelen op `http://192.168.10.150:1218`. De live codevervanging naar commit `41942d5` is uitgevoerd via SSH-alias `unraid-widefrog`; image `af96b8ac7a04...` draait healthy met 0 restarts. De live configuratiecategorieën zijn herverdeeld over 16 categorieën; export-backup staat lokaal in `.codex-input/dockdeck-categories-before-20260722-052942.json`. + +## Rollback + +Gebruik de vorige stabiele Git-commit/image. Herstel `dockdeck_dockdeck_data` uit een beveiligde volumeback-up of importeer een versie 1 configuratie-export. De JSON-export bevat geen secrets; herstel die via Settings/deploymentenvironment of uit de apart beschermde volumeback-up. diff --git a/PLAN.md b/PLAN.md new file mode 100644 index 00000000..bcc24605 --- /dev/null +++ b/PLAN.md @@ -0,0 +1,52 @@ +# PLAN.md + +Goal: De eerste bruikbare versie draait als Docker-container op Unraid en kan als browserstartpagina worden ingesteld. Na de eerste start ontdekt de app de beschikbare Docker-containers, toont ze in een instellingenoverzicht en laat de gebruiker selecteren welke apps op het dashboard verschijnen. Zichtbare apps staan netjes gegroepeerd in configureerbare categorieën, tonen naam, icoon en actuele online- of offline-status en openen hun lokale WebUI in een nieuw tabblad. Favoriete websites kunnen handmatig worden toegevoegd. De zoekbalk vindt apps en favorieten en kan rechtstreeks een Google-zoekopdracht starten. Alle instellingen blijven na een herstart behouden en kunnen worden geëxporteerd en opnieuw geïmporteerd. De totale ervaring moet eruitzien als een afgewerkt premium product dat de gebruiker met plezier dagelijks als startpagina gebruikt. + +## Execution Rules + +- Startmodus: MVP bouwen +- Codex mag scaffolden, bouwen, testen, committen en pushen wanneer de repo en remote duidelijk zijn. +- Bouw de kleinste bruikbare verticale flow eerst. +- Vraag alleen om hulp bij ontbrekende credentials, onduidelijke productkeuzes of risicovolle externe acties. +- Werk in kleine, verifieerbare stappen. +- Houd requirements, spec, beslissingen, risico's en handoff actueel. +- Respecteer runtime-, sync-, persistentie-, deploy- en repo-keuzes uit SPEC.md en DECISIONS.md. +- Test voor overdracht en commit. +- Commit pas wanneer de relevante quality gate is gehaald. + +## Task Table + +| Phase | Task | Done When | Status | +| -------- | ------------------------------------------------------------------- | ------------------------------------------ | ------ | +| Intake | Valideer briefing, aannames en open vragen. | PROJECT_BRIEF.md is actueel. | Done | +| Design | Werk requirements en SPEC.md bij. | Belangrijke keuzes staan in DECISIONS.md. | Done | +| Build | Implementeer de kleinste bruikbare verticale stap. | Must-haves worden aantoonbaar ondersteund. | Done | +| Test | Run unit, integratie en handmatige checks passend bij de wijziging. | TEST_LOG.md bevat resultaat en datum. | Done | +| Document | Werk handoff, risico's en gebruikersnotities bij. | Volgende stap is duidelijk. | Done | +| Commit | Review diff en commit zonder secrets. | Commit bevat alleen bedoelde wijzigingen. | Done | + +## Current Phase + +De volledige visuele audit van DockDeck Canon v1 is afgerond en releasecommit `5e411d1` draait op Unraid via `192.168.10.150:1218`. De dagelijkse launcher gebruikt de Stitch-hiërarchie met desktopfavorietenrail, echte appiconen, een compacte uitklapbare widgetlaag en een viewportbrede Quick Launch. Alle instellingen blijven beschikbaar, maar grote catalogi en geavanceerde personalisatie zijn progressief ontsloten; mobiel en tablet gebruiken een horizontale instellingenbalk. Lokale quality gate, Compose-config, imagebuild, persistentie, read-only integraties en finale live browseraudit zijn groen. + +## Current Phase Update + +De inventaris- en instellingenpass is live afgerond. Een verbonden AppOps/Docker-heartbeat heeft 139 historische records naar exact 90 bestaande containers gereconcilieerd: 0 stale namen, 0 ontbrekende namen, 0 dubbele namen en 0 statusafwijkingen. Dashboardfilters werken onafhankelijk van de weergavemodus. Dashboard en Instellingen delen exact dezelfde topbalkgeometrie en navigatiestates; Integraties gebruikt leesbare formulierregels en Paneelstudio een brede driestappenflow. Alle lokaal ontdekte Unraid-iconen geven live HTTP 200 vanuit de begrensde tijdelijke cache. + +De daaropvolgende echte Chrome-audit op 3440×1215 vond geen overflow of browserfouten, maar wel resterende microtypografie van 6–8 px, te lange instellingenrijen en foutieve categorieën voor onder meer Deluge, JDownloader2, Glances en Nginx Proxy Manager. Release `16bd8c7` corrigeert dit live met een 540px-widgetdock, leesbare cascade guard, begrensde instellingenoppervlakken en expliciete veilige categorieorganisatie. De lokale gate, Tower Compose/imagebuild, read-only heartbeat en tweede live Chrome-audit zijn groen; de testtab blijft open voor gebruikerscontrole. + +De huidige afwerkingspass voegt een echte links/rechts-schakelaar toe aan App Beheer, maakt Home Assistant, Authentik, Vaultwarden en Nginx Proxy Manager compact, en verrijkt de centrale launcher met subtiele omgevingsdiepte, een lokale wallpaper-upload en een configureerbare Open-Meteo-badge voor Mol. Publieke GET-healthchecks geven Authentik, NPM, Vaultwarden en Home Assistant zonder secret al een eerlijke status; minimum-scope tokens blijven optioneel voor detailstatistieken. + +De achtergrondcorrectie maakt de drie opgeslagen achtergrondmodi daadwerkelijk zichtbaar en rendert de lokale wallpaper in de centrale dashboardcanvas. Een end-to-end browsertest wisselt alle presets, vergelijkt hun berekende achtergronden, uploadt via de echte instellingenflow en controleert vervolgens `has-wallpaper` en de server-URL in de zichtbare CSS-compositie. + +De responsieve wallpaperpass koppelt het beeld los van de totale scrollhoogte en rendert het in een sticky viewportstage binnen het centrale vlak. Weergave bevat nu een begrensde live preview met Passend/Vullend, horizontale en verticale focus, zoom en herstel; alle keuzes migreren idempotent, blijven persistent en reizen mee in versie 1-back-ups. + +De platformbrede wallpaperpass verplaatst die stage uit de middenkolom naar een fixed laag van direct onder de topbalk tot de viewportonderrand. Een wallpaper neutraliseert voortaan de actieve achtergrondpreset, loopt achter linkerrail, appcanvas en rechter widgetdock door en gebruikt bij `Passend` een zachte ambient-kopie om ultrawide zijruimte zonder harde kleurvlakken te vullen. + +De categorie-icoonpass verbindt alle zestien live opgeslagen iconen met een eigen Lucide-component en dezelfde volledige keuzecatalogus in Instellingen. Nieuwe categorieën starten met een nog ongebruikt pictogram; onbekende geïmporteerde waarden behouden een veilige generieke fallback. + +## Verification Rules + +- Testing expectations: Unit tests voor URL-normalisatie, categorie- en sorteervoorwaarden, zoekresultaten, configuratievalidatie, import- en exportversies, container-naar-Nginx-hostmatching en permissiegrenzen; backend-integratietests met gemockte Unraid-templategegevens, Docker Socket Proxy en Nginx Proxy Manager; SQLite-migratie- en persistentietests; tests die aantonen dat geen Docker-mutatie-endpoints of beheeracties worden gebruikt; browsertests voor eerste ingebruikname, containers zichtbaar maken, categorieën beheren, favorieten toevoegen, zoeken, Google-fallback, thema wisselen, weergavemodus wijzigen, URL openen en configuratie exporteren en importeren; visuele UI-smoketests voor kernschermen zoals dashboard, categorie-overzicht en instellingen; Docker-buildtest en Compose-smoketest; een live read-only validatie op Unraid waarbij discovery, status en lokale WebUI-URL's worden gecontroleerd zonder containers te wijzigen. +- Quality bar: De interface moet eruitzien als een professioneel premium product en niet als een simpel hobbydashboard. Verwacht een moderne, luxueuze en verzorgde UI met uitgebalanceerde witruimte, consistente kaartcomponenten, hoogwaardige iconografie, duidelijke typografie, nette schaduwen of diepte waar passend, subtiele animaties, verzorgde empty states, sterke visuele hiërarchie en een kalme maar rijke look-and-feel. De belangrijkste startpagina moet snel laden en direct bruikbaar zijn wanneer integraties traag of tijdelijk offline zijn. Fouten moeten begrijpelijk worden getoond zonder technische stacktraces. Ontbrekende iconen of URL's moeten nette fallbacks hebben. Instellingen moeten veilig opgeslagen worden en bij foutieve invoer gevalideerde feedback geven. De interface moet responsief zijn voor desktop, tablet en basisgebruik op mobiel. Toetsenbordnavigatie, zichtbare focus, semantische HTML en voldoende kleurcontrast zijn vereist. Statuspolling mag geen zichtbare flikkering veroorzaken en moet stoppen wanneer de pagina niet actief is of verstandig worden vertraagd. De code moet modulair, getypeerd, gedocumenteerd en zonder onnodige complexiteit zijn. Esthetische kwaliteit is voor deze MVP een kernvereiste en niet louter een extra. +- Noteer niet-geteste onderdelen expliciet in TEST_LOG.md. diff --git a/PROJECT_BRIEF.md b/PROJECT_BRIEF.md new file mode 100644 index 00000000..b5b85c82 --- /dev/null +++ b/PROJECT_BRIEF.md @@ -0,0 +1,39 @@ +# PROJECT_BRIEF.md + +## Project + +- Projectnaam: DockDeck +- Korte beschrijving: Een premium ogend persoonlijk startdashboard voor Unraid dat Docker-apps automatisch ontdekt, stijlvol groepeert en snelle navigatie biedt naar lokale diensten en eigen favorieten. +- Projectidee: Bouw een persoonlijke webapp die als standaard startpagina van de browser dient en een prachtig, professioneel en strak afgewerkt overzicht geeft van de diensten op de Unraid-server. De app ontdekt Docker-containers automatisch via Unraid en een beperkte read-only Docker-socketproxy, maar toont nieuwe containers niet automatisch op het hoofddashboard. In het instellingenmenu kan de gebruiker per container zichtbaarheid, categorie, sortering, icoon, naam en URL beheren. Voor Docker-apps wordt standaard de lokale WebUI-URL uit de Unraid-configuratie gebruikt. Daarnaast kunnen handmatig favoriete externe websites worden toegevoegd met naam, URL, icoon, categorie en sorteerpositie. Het dashboard bevat een centrale zoekbalk die zowel zichtbare Docker-apps als favorieten doorzoekt en, wanneer geen lokaal resultaat wordt gekozen, de invoer als Google-zoekopdracht in een nieuw tabblad opent. De gebruiker kan kiezen tussen één overzicht met categorieën onder elkaar en een weergave met afzonderlijke categoriepagina's of tabbladen. Thema's, weergavevoorkeuren en openingsgedrag worden via instellingen beheerd. Externe toegang kan optioneel worden ingeschakeld; wanneer die actief is, haalt een read-only koppeling met Nginx Proxy Manager beschikbare externe URL's op en koppelt die aan de juiste apps, met steeds een handmatige override als terugval. De UI moet aanvoelen als een hoogwaardige, moderne productinterface met sterke visuele hiërarchie, nette details, subtiele animaties en een verzorgde dashboardervaring. +- Probleem: De gebruiker heeft meerdere Docker-diensten op een Unraid-server en moet momenteel afzonderlijke URL's onthouden, bladwijzers beheren of via verschillende beheerinterfaces navigeren. Bestaande startpagina's sluiten niet volledig aan op de gewenste combinatie van automatische Unraid-detectie, configureerbare zichtbaarheid, categorie-indeling, statusweergave, lokale en externe URL's, favoriete websites, geïntegreerde zoekfunctie en een echt professioneel ogende premium UI. +- Doelgebruikers: Eén primaire gebruiker: de eigenaar en beheerder van de Unraid-server. Deze gebruiker verwacht zonder technische omwegen snel naar lokale Docker-diensten en favoriete websites te kunnen navigeren, zelf te bepalen welke apps zichtbaar zijn, categorieën en sortering te beheren, de startpagina visueel aan te passen en optioneel lokale of externe app-URL's te gebruiken. Er zijn in de MVP geen afzonderlijke gebruikersrollen, accounts of gedeelde profielen. +- Gewenste uitkomst: De eerste bruikbare versie draait als Docker-container op Unraid en kan als browserstartpagina worden ingesteld. Na de eerste start ontdekt de app de beschikbare Docker-containers, toont ze in een instellingenoverzicht en laat de gebruiker selecteren welke apps op het dashboard verschijnen. Zichtbare apps staan netjes gegroepeerd in configureerbare categorieën, tonen naam, icoon en actuele online- of offline-status en openen hun lokale WebUI in een nieuw tabblad. Favoriete websites kunnen handmatig worden toegevoegd. De zoekbalk vindt apps en favorieten en kan rechtstreeks een Google-zoekopdracht starten. Alle instellingen blijven na een herstart behouden en kunnen worden geëxporteerd en opnieuw geïmporteerd. De totale ervaring moet eruitzien als een afgewerkt premium product dat de gebruiker met plezier dagelijks als startpagina gebruikt. +- Succescriteria: De MVP is klaar wanneer: de applicatie via Docker Compose op Unraid kan worden gestart; beschikbare Docker-containers automatisch worden ontdekt zonder schrijf- of beheerrechten op Docker; nieuw ontdekte containers standaard verborgen blijven; de gebruiker containers zichtbaar of verborgen kan maken; naam, icoon, categorie, sortering en lokale URL per app kunnen worden aangepast; de lokale WebUI-URL waar mogelijk automatisch uit Unraid-templategegevens wordt afgeleid; zichtbare apps naam, icoon en correcte online- of offline-status tonen; status bij het openen en daarna ongeveer elke 30 seconden wordt vernieuwd; categorieën kunnen worden aangemaakt, hernoemd, gesorteerd en toegewezen; zowel een volledig overzicht als categoriegebaseerde navigatie configureerbaar is; favorieten met naam, URL, icoon, categorie en sortering kunnen worden beheerd; de zoekbalk apps en favorieten doorzoekt en overige invoer naar Google stuurt; alle links in een nieuw tabblad openen; licht, donker en minstens één aanvullend thema selecteerbaar zijn; configuratie persistent blijft na container- en serverherstart; back-up en herstel via export en import werken; de app uitsluitend voor lokaal gebruik is ingericht en geen container start-, stop-, restart- of wijzigingsacties bevat; een private Gitea-repository automatisch wordt aangemaakt, lokaal wordt geïnitialiseerd en een eerste bruikbare commit bevat; en de UI een consistent, professioneel en visueel hoogwaardig geheel vormt met nette kaartlay-outs, sterke typografie, duidelijke statusindicatoren, vloeiende maar subtiele interacties en een verzorgde instellingenervaring. + +## Scope + +- Must-haves: Automatische read-only ontdekking van Unraid Docker-containers; nieuwe containers standaard verborgen; instellingenpagina voor zichtbaarheid, naam, icoon, categorie, sortering en URL-overrides; automatische afleiding van lokale WebUI-URL's uit Unraid-templategegevens; read-only containerstatus met verversing bij laden en ongeveer elke 30 seconden; configureerbare categorieën; configureerbare keuze tussen één totaaloverzicht en categoriepagina's of tabbladen; handmatige favorieten voor externe websites; centrale zoekbalk voor apps, favorieten en Google; openen van alle doelen in een nieuw tabblad; licht, donker en aanvullende ingebouwde thema's; persistent bewaren van instellingen in SQLite; export en import van configuratie; Docker- en Docker Compose-configuratie voor Unraid; beperkte Docker-socketproxy zonder beheerrechten; optionele read-only Nginx Proxy Manager-koppeling voor externe URL's met handmatige fallback; duidelijke foutstatus wanneer Unraid, Docker-proxy of Nginx Proxy Manager tijdelijk onbereikbaar is; automatische initialisatie van een private Gitea-repository via environmentvariabelen; en een uitzonderlijk verzorgde professionele UI met premium dashboardlook, consistente componenten, strak kaartdesign, goede witruimte, duidelijke visuele hiërarchie, subtiele hover- en transition-effecten, nette iconografie en een afgewerkte instellingenomgeving. +- Nice-to-haves: Drag-and-drop sortering van apps en categorieën; automatisch herkennen van bekende applicatie-iconen; favicon-import voor favorieten; meerdere aanvullende kleurthema's; compacte en ruime tegelweergave; achtergrondafbeeldingen of subtiele dashboard-achtergronden; sneltoets om de zoekbalk te focussen; recente of meest gebruikte apps; optionele statuslatentie en responstijd; import van bestaande browserbladwijzers; meerdere dashboards of profielen; PWA-installatie; mobiele optimalisaties buiten de basisresponsiviteit; geavanceerde automatische matching tussen Nginx Proxy Manager-hosts en Docker-containers; handmatige statuscontrole voor favoriete websites; extra micro-interacties en visuele personalisatieopties zolang die de strakke premium uitstraling ondersteunen. +- Buiten scope: Containers starten, stoppen, herstarten, verwijderen of wijzigen; Docker Compose-stacks beheren; Unraid-systeembeheer; terminal- of shelltoegang; logviewer; resourcegrafieken en uitgebreide monitoring; notificaties en incidentbeheer; publieke internettoegang configureren; Authentik- of andere SSO-integratie; eigen gebruikersaccounts, rollen of multi-userondersteuning; opslag van echte API-tokens in de repository of browser; automatische wijzigingen aan Nginx Proxy Manager; cloudhosting; mobiele native apps; AI-functionaliteit; synchronisatie tussen meerdere Unraid-servers; automatische publicatie van diensten naar het internet. +- Constraints: De MVP moet als Docker-container op de bestaande Unraid-server draaien en alleen vanaf het lokale netwerk bereikbaar zijn. De app moet zonder eigen login bruikbaar zijn, omdat netwerktoegang de primaire beveiligingsgrens vormt. Alle Docker-toegang moet read-only verlopen via een beperkte socketproxy; de hoofdcontainer krijgt geen directe onbeperkte Docker-socket. Unraid-templategegevens mogen alleen worden gelezen. Nginx Proxy Manager wordt uitsluitend read-only benaderd. De applicatie moet bruikbaar blijven wanneer Nginx Proxy Manager niet is geconfigureerd of tijdelijk niet bereikbaar is. Er mogen geen betaalde externe diensten nodig zijn. Alle configuratie moet persistent blijven bij herstart en eenvoudig te back-uppen zijn. Codex moet een nieuwe private repository in de persoonlijke Gitea-omgeving aanmaken en mag geen secrets hardcoderen. Visuele kwaliteit is belangrijk, maar de UI mag niet onnodig zwaar of traag worden; professionele polish moet samengaan met vlotte prestaties als browserstartpagina. + +## Eerste Aannames + +| Aanname | Status | +| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ | +| Het projecttype is webapp. | Te valideren | +| De voorkeursstack is TypeScript als hoofdtaal; React met Vite voor de frontend; Node.js met Fastify voor de backend; SQLite met Drizzle ORM voor persistentie en migraties; gedeelde types en validaties met Zod; een eenvoudige REST-API tussen frontend en backend; een zorgvuldig opgebouwd design system met design tokens, CSS-variabelen en herbruikbare UI-componenten; een moderne componentbibliotheek of headless aanpak voor toegankelijke basiscomponenten; iconenset van hoge kwaliteit; subtiele motion via lichte animatielogica; Docker multi-stage build en Docker Compose voor Unraid; Vitest voor unit- en integratietests; Playwright voor browsertests. Gebruik een overzichtelijke repository met duidelijk gescheiden frontend-, backend- en shared-mappen. Richt de frontend expliciet in op een premium dashboardervaring met consistente spacing, kaartsystemen, typografie, kleurgebruik, statusbadges en instellingenpanelen.. | Te valideren | +| Runtime: Interactieve app. | Te valideren | +| Update/sync: Polling. | Te valideren | +| Persistentie: SQLite. | Te valideren | +| Deployment: Docker op Unraid. | Te valideren | +| Repository: Repo nog nodig. | Te valideren | +| De primaire gebruikers zijn Eén primaire gebruiker: de eigenaar en beheerder van de Unraid-server. Deze gebruiker verwacht zonder technische omwegen snel naar lokale Docker-diensten en favoriete websites te kunnen navigeren, zelf te bepalen welke apps zichtbaar zijn, categorieën en sortering te beheren, de startpagina visueel aan te passen en optioneel lokale of externe app-URL's te gebruiken. Er zijn in de MVP geen afzonderlijke gebruikersrollen, accounts of gedeelde profielen.. | Te valideren | + +## Open Questions + +| Vraag | Waarom dit nodig is | +| ----------------------------------------------------------------- | ------------------------------------ | +| Welke randgevallen moeten expliciet ondersteund worden? | Maakt requirements toetsbaar. | +| Welke data mag lokaal, extern of helemaal niet opgeslagen worden? | Bepaalt security en privacy ontwerp. | +| Welke definitie van klaar geldt voor de eerste release? | Houdt scope en planning scherp. | diff --git a/README.md b/README.md index 378dbf5a..827c0beb 100644 --- a/README.md +++ b/README.md @@ -1,292 +1,139 @@ -# GeoIntel Belgium and the Belgian North Sea +# DockDeck -GeoIntel is a map-first GeoAI Workbench for Belgium and the Belgian North Sea. -It combines governed official-source coverage, raster/vector processing, -historical comparison, computer vision, QA/QC and geospatial exports. +DockDeck is a calm, premium personal start dashboard for Unraid. Its normal Unraid profile is one branded application container, reads container inventory through the existing AppOps GET API and enriches it with sanitized local WebUI metadata. -Mol and the Kempen remain deep regression and model-validation references. The -release scope is all of Belgium plus legally labelled Belgian maritime zones; -source coverage remains explicit per theme and jurisdiction. +## What the MVP includes -GeoIntel is not a generic dashboard or chatbot. The core product is: +- automatic read-only container discovery with new apps hidden by default; +- live online/offline status and 30-second visibility-aware polling; +- editable app name, icon fallback, category, order, local URL and external URL fallback; +- configurable categories, favorites and overview/category navigation; +- fast local search with a Google fallback, always opening in a new tab; +- Porcelain, Midnight and Harbor themes, four accents, three density and content-width profiles, and selectable backgrounds; +- responsive layouts from compact mobile screens through tablets, desktops and ultrawide displays; +- optional dashboard hero and status badges, plus editable category and favorite icons; +- up to eight configurable service widgets with independent order, compact/standard/wide layouts, custom labels, metric order, per-metric value/gauge/progress presentation, primary metric, any-signal warning thresholds and refresh cadence; +- line, area or bar charts with a selectable app/provider series, legend and 10–60 in-memory samples; +- a default native Unraid widget with configurable statistics and charts for Docker inventory, array, parity, disk/pool, temperature and optional UPS status; +- rich GET-only providers for AdGuard, Plex, Immich, Home Assistant, Sonarr/Radarr/Lidarr, Ollama, Glances, Tautulli, Paperless, Gitea, Jellyfin, Seerr, Prowlarr, Authentik, NPM, Portainer, Grafana, Prometheus and Nextcloud; +- optional GET-only metrics bridges for Deluge, qBittorrent, JDownloader, Tdarr, Bazarr and Vaultwarden; +- a searchable Widget Studio that explains available metrics, recommends useful panels and activates up to eight widgets; +- persistent per-app metric visibility, order, presentation and card limits, with a tailored profile for every discovered app even before its optional data endpoint is connected; +- native Audiobookshelf, Netdata and PeerTube readers plus an optional GET-only JSON endpoint for every custom or niche app; +- an editable Tower name and Unraid WebUI URL that defaults to port `5000`; +- masked provider credential forms in Settings, with immediate read-only connection testing and no secret echo; +- automatic first-party favorite favicons with a resilient local monogram fallback; +- a useful desktop sidebar with time, deck health and quick service links; +- SQLite persistence plus versioned JSON export/import; +- reusable layout presets with independent import/export, direct dashboard edit mode and secret-free diagnostics; +- an installable PWA shell that explicitly excludes runtime API data from caching; +- optional read-only Nginx Proxy Manager host matching; +- one branded, read-only DockDeck container with a true transparent PNG Unraid-list icon, WebUI metadata and no Docker-socket mount. -> data → processing → geospatial output → QA/QC → export +## Local development -## Current milestone - -**v1.0.0 - Belgium/North Sea release** - -The canonical release controls are: - -- `docs/00-start/START_HERE.md` -- `docs/RC_SCOPE_FREEZE_BELGIUM_NORTH_SEA.md` -- `docs/RC_ROADMAP_BELGIUM_NORTH_SEA.md` -- `docs/RELEASE_RUNBOOK.md` -- `docs/KNOWN_LIMITATIONS.md` -- `docs/DEFINITION_OF_DONE.md` - -Older milestone and sprint handoff files remain historical evidence. They do -not override the active national/maritime scope freeze or RC roadmap. - -## Core V1 vertical slice - -The first implementation target is: - -1. Project + Area creation. -2. Dataset registration/upload and metadata extraction. -3. Reference building layer loading. -4. Predicted detection layer loading/import. -5. QA/QC matching against reference polygons. -6. Metrics and false positive/false negative outputs. -7. GeoJSON export. -8. Minimal map/workbench UI. - -## Primary stack - -- Frontend: React, TypeScript, MapLibre GL, Deck.gl, Tailwind. -- Backend: FastAPI, Python. -- Database: PostgreSQL + PostGIS. -- GIS processing: GeoPandas, Shapely, Rasterio, PyProj, GDAL. -- AI: PyTorch, Ultralytics YOLO, SAM-compatible architecture. -- Jobs: Redis + RQ. -- Storage: local filesystem first, MinIO-compatible later. - -## Codex instructions - -Codex must start with: - -1. `docs/00-start/START_HERE.md` -2. `prompts/codex/M11_ARCHITECT_MASTER_PROMPT.md` - -Then follow the build order in: - -- `docs/build/BUILD_ORDER_DEPENDENCY_GRAPH.md` -- `docs/build/CODEX_OPERATING_SYSTEM.md` - -Before every implementation pass, run available preflight/smoke scripts where applicable. - -## Repo principle - -This is a documentation-driven engineering repo. The documentation is not decorative; it is the control system for autonomous implementation. - -## Fastest Day 1 command path +Requirements: Node.js 22 or newer and npm 10 or newer. ```bash -make readiness +cp .env.example .env +npm install +npm run dev ``` -## Unraid / Tower deployment +Open `http://127.0.0.1:5173`. The example environment enables deterministic discovery fixtures. Runtime data is written to `./data/dockdeck.db`. -GeoIntel runs on Unraid as an all-in-one DockerMan-native container. The container embeds PostGIS, runs the FastAPI backend internally, and serves the frontend through nginx on one editable web port. - -Unraid template assets live in: - -- `deploy/unraid/geointel.env.example` -- `deploy/unraid/geointel-unraid-template.xml` -- `deploy/unraid/geointel-icon.svg` -- `deploy/unraid/geointel-icon.png` -- `docker-compose.unraid.yml` - -Copy the Unraid env template to `.env` in the checkout and edit ports/paths there: +Quality commands: ```bash -cd /mnt/user/appdata/geointel -cp deploy/unraid/geointel.env.example .env -nano .env -docker build -f deploy/unraid/Dockerfile.all-in-one -t geointel-all-in-one:latest . -bash deploy/unraid/run-dockerman-container.sh -``` - -Common editable values: - -```env -GEOINTEL_FRONTEND_PORT=1202 -GEOINTEL_STORAGE_PATH=/mnt/user/appdata/geointel/storage -GEOINTEL_POSTGIS_DATA_PATH=/mnt/user/appdata/geointel/postgres-data -``` - -The backend and PostGIS ports are intentionally not exposed to the LAN in the all-in-one runtime. See `deploy/unraid/README.md` for full setup, port-change and cleanup notes. - -On Tower/Unraid, `scripts/deploy_tower.ps1` and `scripts/deploy_tower.sh` validate the Compose reference but build with plain `docker build`, then automatically install the editable DockerMan template as `/boot/config/plugins/dockerMan/templates-user/my-geointel.xml`, install the PNG icon as `/boot/config/plugins/dockerMan/images/geointel-icon.png`, remove any old Compose-owned `geointel` container and start the final container with DockerMan labels. - -## Sprint 2 quick start - -- Update dependencies: - -```bash -python -m pip install -e backend/.[dev] -cd frontend && npm install -``` - -- Run full readiness checks (with no scope expansion): - -```bash -python -m compileall backend/app -cd backend && python -m pytest -cd ../frontend && npm run typecheck && npm run build -bash scripts/run_readiness_check.sh -``` - -- Raster workflow validation command (backend only): - -```bash -bash scripts/smoke_backend_import.sh -cd backend && python -c "from app.main import app; print(app.title)" -``` - -If `rasterio` is not installed, raster metadata endpoints return `RASTER_PROCESSING_UNAVAILABLE` and the frontend displays the -state as failed until the dependency is added. - -## Sprint 4 raster foundation - -- Raster operations now support: - - raster metadata extraction, - - raster preview generation, - - raster clip by area (with provenance on derived datasets), - - raster tile generation with manifest output. -- Raster services are dependency-aware: - - if `rasterio` is unavailable, endpoints return `RASTER_PROCESSING_UNAVAILABLE`. - - if preview dependencies (`numpy`, `pillow`) are unavailable, preview generation is unavailable with a clear error. -- Enable raster stack explicitly when needed: - -```bash -cd backend && python -m pip install -e .[dev,raster] -``` - -## Sprint 5 raster analytics hardening - -- Added raster band statistics (min/max/mean/std, nodata ratio/count, valid pixel count, dtype, optional histograms). -- Added raster reproject workflow with CRS validation and provenance persistence. -- Extended tile manifest expectations (`tile_set_id`, `tile_size`, `overlap`, `bounds`, `source_raster_id`, `tile_paths`, `tile_server`). -- Clarified raster operation availability in frontend/backend docs (`RASTER_PROCESSING_UNAVAILABLE` and invalid-CRS cases). - -- Raster workflow command set (where available): - -```bash -cd backend -python -m pip install -e .[dev,raster] -python -m pytest -cd ../frontend +npm run format:check +npm run lint npm run typecheck +npm test +npm run test:e2e npm run build ``` -Then give Codex the prompt in: +## Unraid / Docker Compose -- `prompts/codex/final/DAY_1_MASTER_PROMPT.md` +Create an `.env` beside `docker-compose.yml`: - -## M13 Codex optimization - -For the first serious Codex build run, use: - -- `prompts/codex/m13/DAY_1_OPTIMIZED_MASTER_PROMPT.md` - -Codex should also use the relevant reusable skill under `skills/` for each implementation pass. Validate the optimization assets with: - -```bash -make m13 +```dotenv +DOCKDECK_BIND_ADDRESS=192.168.10.150 +DOCKDECK_PORT=1218 +DOCKDECK_EGRESS_SUBNET=172.31.240.16/28 +APPOPS_URL=http://192.168.10.150:1216 +UNRAID_TEMPLATES_DIR=/mnt/user/appdata/dockdeck/unraid-templates +UNRAID_HOST=192.168.10.150 +UNRAID_URL=http://192.168.10.150:5000 ``` -The full readiness path remains: +For the current Unraid host, `deploy/unraid.env.example` contains this non-secret profile and can be copied to `.env`. It pins a small egress subnet because this Unraid host has exhausted its automatic address pools; change it if the host network layout changes. + +Unraid keeps its source templates root-only and templates can contain sensitive values. Create DockDeck's sanitized metadata mirror before starting Compose; the helper copies only `Name`, `WebUI` and `Icon`: ```bash -make readiness +python3 deploy/sanitize-unraid-templates.py \ + /boot/config/plugins/dockerMan/templates-user \ + /mnt/user/appdata/dockdeck/unraid-templates ``` - -## M14 Build Launch - -For the first serious implementation run, use: - -- `docs/40-build-launch/SPRINT_1_SCOPE_FREEZE.md` -- `docs/40-build-launch/BUILD_SUCCESS_DEFINITION.md` -- `docs/40-build-launch/CODEX_STOP_RULES.md` -- `prompts/codex/m14/CODEX_FIRST_DAY_MASTER_PROMPT.md` - -Validate launch assets with: - -```bash -make m14 -``` - -Full readiness remains: - -```bash -make readiness -``` - -## Sprint 1 execution (Sprint 1 only) - -From a clean machine: - -```bash -cd backend && python -m pip install -e .[dev] -cd .. -make backend-install -make frontend-install -make readiness -``` - -Copy `.env.example` to `.env` only when you want local overrides. Docker Compose has safe defaults for the local PostGIS/backend/frontend stack and does not require a root `.env` file to exist. - -With Docker Compose, open the workbench at `http://localhost:1202`. - -The Docker frontend is served by nginx and proxies `/api` and `/health` to the backend container, so browser clients should use the frontend URL only, for example `http://192.168.10.150:1202` on a LAN host. - -Runtime containers include healthchecks for PostGIS, backend and frontend. After -startup, inspect them with: +Then run. The base Compose file creates exactly one container named `DockDeck`: ```bash +docker compose up -d --build docker compose ps +sudo sh deploy/refresh-unraid-icon.sh ``` -Verify the browser-facing API proxy after rebuilding Docker images: +The final helper validates that the configured `` URL returns a real PNG and replaces both DockDeck-specific caches used by Unraid DockerMan: the persistent cache and the active RAM cache. Run it after changing the icon or recreating the container. It never changes Docker state or touches another container's files. An explicit URL can be supplied as the second argument when no user template exists. + +On a host without AppOps, use the optional hardened fallback overlay. Only that additional proxy sees the socket and it disables POST and all unused API families: ```bash -bash scripts/verify_browser_runtime.sh http://localhost:1202 http://localhost:8000/health +docker compose -f docker-compose.yml -f docker-compose.socket-proxy.yml up -d --build ``` -Verify the Docker GIS runtime after rebuilding the backend image: +By default Compose publishes DockDeck only on `127.0.0.1:1218`. For this Unraid host, set `DOCKDECK_BIND_ADDRESS=192.168.10.150` so the trusted LAN can reach `http://192.168.10.150:1218`. DockDeck has no built-in authentication; do not publish it directly to the internet. -```bash -bash scripts/verify_gis_runtime.sh http://localhost:1202 -``` +Persistent configuration lives in the named volume `dockdeck_data`. Normal preferences use `/data/dockdeck.db`; credentials entered through Settings use the permission-restricted `/data/integrations.env`. Protect volume backups because that environment file contains plaintext secrets. The JSON export never includes it. -On the LAN host use the published browser URL, for example: +## Environment variables -```bash -bash scripts/verify_gis_runtime.sh http://192.168.10.150:1202 -``` +| Variable | Purpose | +| ------------------------------------------------------- | --------------------------------------------------------------------------------------------- | +| `DATABASE_PATH` | SQLite file path; defaults to `./data/dockdeck.db` locally and `/data/dockdeck.db` in Docker. | +| `INTEGRATION_ENV_PATH` | Permission-restricted environment file used for credentials entered in Settings. | +| `DOCKDECK_BIND_ADDRESS`, `DOCKDECK_PORT` | Host interface and port published by Compose; safely default to `127.0.0.1:1218`. | +| `DOCKDECK_EGRESS_SUBNET` | Explicit small subnet for the app's outbound network. | +| `APPOPS_URL` | Existing AppOps origin used for GET-only inventory and measured CPU/memory status. | +| `DOCKER_PROXY_URL` | Optional internal URL used only by the socket-proxy fallback overlay. | +| `UNRAID_TEMPLATES_PATH` | Read-only XML template mount inside the app. | +| `UNRAID_HOST` | Host used to resolve `[IP]` in WebUI templates; defaults to `tower.local`. | +| `UNRAID_URL` | Initial Tower/Unraid link; Settings can override it and a missing port defaults to `5000`. | +| `UNRAID_RUNTIME_PATH`, `UNRAID_HWMON_PATH` | Container paths for minimum read-only Unraid runtime and hardware-monitor mounts. | +| `UNRAID_UPS_STATUS_PATH` | Optional read-only APC/UPS status file inside the app container. | +| `MOCK_DISCOVERY` | Enables deterministic local/test fixtures; never enable for production discovery. | +| `NPM_URL`, `NPM_TOKEN` | Optional server-only Nginx Proxy Manager read credential. | +| `NPM_USERNAME`, `NPM_PASSWORD` | Optional server-only alternative for requesting an NPM session token. | +| `ADGUARD_URL`, `ADGUARD_USERNAME`, `ADGUARD_PASSWORD` | Optional AdGuard Home statistics for its selected widget. | +| `PLEX_URL`, `PLEX_TOKEN` | Optional Plex sessions, transcodes and library count. | +| `IMMICH_URL`, `IMMICH_API_KEY` | Optional Immich photo, video and storage totals. | +| `HOME_ASSISTANT_URL`, `HOME_ASSISTANT_TOKEN` | Optional Home Assistant automation, light and availability totals. | +| `OLLAMA_URL`, `GLANCES_URL` | Optional overrides; their discovered local app URLs provide zero-config read-only metrics. | +| `TAUTULLI_URL`, `TAUTULLI_API_KEY` | Optional Plex activity, transcode and bandwidth metrics. | +| `PAPERLESS_URL`, `PAPERLESS_TOKEN` | Optional Paperless document, inbox and archive-size metrics. | +| `AUDIOBOOKSHELF_URL`, `AUDIOBOOKSHELF_TOKEN` | Optional library, book, podcast and item totals. | +| `NETDATA_URL`, `PEERTUBE_URL` | Optional discovered-URL overrides for their native GET-only readers. | +| `SONARR_*`, `RADARR_*`, `LIDARR_*` | Optional provider URL and API key pairs for queue and version data. | +| `GITEA_WIDGET_*`, `JELLYFIN_*`, `SEERR_*`, `PROWLARR_*` | Optional server-side URL/token pairs for native GET-only metrics. | +| `AUTHENTIK_*`, `PORTAINER_*`, `GRAFANA_*` | Optional server-side URL/token pairs for administrative summary metrics. | +| `PROMETHEUS_URL`, `NEXTCLOUD_*`, `NPM_*` | Optional read-only monitoring, cloud and proxy metrics. | +| `*_METRICS_URL`, `*_METRICS_TOKEN` | Optional GET-only bridge for providers whose native API would require POST-based control. | +| `CUSTOM_WIDGET__URL`, `CUSTOM_WIDGET__TOKEN` | Settings-managed GET-only JSON metrics endpoint and optional bearer token for any other app. | +| `GITEA_URL`, `GITEA_OWNER`, `GITEA_TOKEN` | Used only by `npm run gitea:init` to create/push the private repository. | -Load the explicit offline demo workflow: +Provider secrets are never stored in SQLite, JSON exports, logs or tracked files and are never returned to the browser. Values entered in Settings are persisted as environment assignments in `INTEGRATION_ENV_PATH` with mode `0600`; use DockDeck only on a trusted LAN or behind authenticated HTTPS, and protect raw volume backups. -```bash -curl -X POST http://192.168.10.150:1202/api/v1/demo/workflow -``` +## Repository initialization -If `/api/v1/projects` returns frontend HTML instead of a JSON envelope, rebuild -and restart the frontend container. - -Useful direct verification commands: - -```bash -python -m compileall backend/app -cd backend && python -c "from app.main import app; print(app.title)" -python -m pytest -cd ../frontend && npm run typecheck -cd ../frontend && npm run build -docker compose config -bash scripts/run_readiness_check.sh -``` - -If `make` or `docker` are unavailable in your shell, run the equivalent script entrypoints directly: - -```bash -bash scripts/backend_install.sh -bash scripts/backend_test.sh -bash scripts/frontend_install.sh -bash scripts/frontend_typecheck.sh -bash scripts/frontend_build.sh -bash scripts/run_readiness_check.sh -``` +After a passing quality gate and with the three Gitea variables exported, run `npm run gitea:init`. The script refuses an unexpected existing remote, creates only a private repository, keeps the token out of Git configuration, and pushes `main` through a temporary authorization header. diff --git a/REQUIREMENTS.md b/REQUIREMENTS.md new file mode 100644 index 00000000..39f7f45f --- /dev/null +++ b/REQUIREMENTS.md @@ -0,0 +1,88 @@ +# REQUIREMENTS.md + +Project: DockDeck + +## Functional Requirements + +Implementatiestatus 2026-07-14: FR-1 tot en met FR-19 zijn gebouwd en lokaal geverifieerd. De private Gitea-repository bestaat en productie draait op Unraid-poort `1218`. Alleen optionele integratiecredentials en een geauthenticeerde externe proxyroute blijven omgevingskeuzes; zie `TEST_LOG.md` en `docs/IMPLEMENTATION_STATUS.md`. + +| ID | Requirement | Source | Status | +| ---- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | ------ | +| FR-1 | Implementeer de must-haves: Automatische read-only ontdekking van Unraid Docker-containers; nieuwe containers standaard verborgen; instellingenpagina voor zichtbaarheid, naam, icoon, categorie, sortering en URL-overrides; automatische afleiding van lokale WebUI-URL's uit Unraid-templategegevens; read-only containerstatus met verversing bij laden en ongeveer elke 30 seconden; configureerbare categorieën; configureerbare keuze tussen één totaaloverzicht en categoriepagina's of tabbladen; handmatige favorieten voor externe websites; centrale zoekbalk voor apps, favorieten en Google; openen van alle doelen in een nieuw tabblad; licht, donker en aanvullende ingebouwde thema's; persistent bewaren van instellingen in SQLite; export en import van configuratie; Docker- en Docker Compose-configuratie voor Unraid; beperkte Docker-socketproxy zonder beheerrechten; optionele read-only Nginx Proxy Manager-koppeling voor externe URL's met handmatige fallback; duidelijke foutstatus wanneer Unraid, Docker-proxy of Nginx Proxy Manager tijdelijk onbereikbaar is; automatische initialisatie van een private Gitea-repository via environmentvariabelen; en een uitzonderlijk verzorgde professionele UI met premium dashboardlook, consistente componenten, strak kaartdesign, goede witruimte, duidelijke visuele hiërarchie, subtiele hover- en transition-effecten, nette iconografie en een afgewerkte instellingenomgeving. | Intake | Done | +| FR-2 | Ondersteun het kernidee: Bouw een persoonlijke webapp die als standaard startpagina van de browser dient en een prachtig, professioneel en strak afgewerkt overzicht geeft van de diensten op de Unraid-server. De app ontdekt Docker-containers automatisch via Unraid en een beperkte read-only Docker-socketproxy, maar toont nieuwe containers niet automatisch op het hoofddashboard. In het instellingenmenu kan de gebruiker per container zichtbaarheid, categorie, sortering, icoon, naam en URL beheren. Voor Docker-apps wordt standaard de lokale WebUI-URL uit de Unraid-configuratie gebruikt. Daarnaast kunnen handmatig favoriete externe websites worden toegevoegd met naam, URL, icoon, categorie en sorteerpositie. Het dashboard bevat een centrale zoekbalk die zowel zichtbare Docker-apps als favorieten doorzoekt en, wanneer geen lokaal resultaat wordt gekozen, de invoer als Google-zoekopdracht in een nieuw tabblad opent. De gebruiker kan kiezen tussen één overzicht met categorieën onder elkaar en een weergave met afzonderlijke categoriepagina's of tabbladen. Thema's, weergavevoorkeuren en openingsgedrag worden via instellingen beheerd. Externe toegang kan optioneel worden ingeschakeld; wanneer die actief is, haalt een read-only koppeling met Nginx Proxy Manager beschikbare externe URL's op en koppelt die aan de juiste apps, met steeds een handmatige override als terugval. De UI moet aanvoelen als een hoogwaardige, moderne productinterface met sterke visuele hiërarchie, nette details, subtiele animaties en een verzorgde dashboardervaring. | Intake | Done | + +## Users And Roles + +- Primaire gebruikers/rollen: Eén primaire gebruiker: de eigenaar en beheerder van de Unraid-server. Deze gebruiker verwacht zonder technische omwegen snel naar lokale Docker-diensten en favoriete websites te kunnen navigeren, zelf te bepalen welke apps zichtbaar zijn, categorieën en sortering te beheren, de startpagina visueel aan te passen en optioneel lokale of externe app-URL's te gebruiken. Er zijn in de MVP geen afzonderlijke gebruikersrollen, accounts of gedeelde profielen. +- Beheerrollen en permissies: Nog in te vullen. + +## Core Workflows + +1. Gebruiker start met: Bouw een persoonlijke webapp die als standaard startpagina van de browser dient en een prachtig, professioneel en strak afgewerkt overzicht geeft van de diensten op de Unraid-server. De app ontdekt Docker-containers automatisch via Unraid en een beperkte read-only Docker-socketproxy, maar toont nieuwe containers niet automatisch op het hoofddashboard. In het instellingenmenu kan de gebruiker per container zichtbaarheid, categorie, sortering, icoon, naam en URL beheren. Voor Docker-apps wordt standaard de lokale WebUI-URL uit de Unraid-configuratie gebruikt. Daarnaast kunnen handmatig favoriete externe websites worden toegevoegd met naam, URL, icoon, categorie en sorteerpositie. Het dashboard bevat een centrale zoekbalk die zowel zichtbare Docker-apps als favorieten doorzoekt en, wanneer geen lokaal resultaat wordt gekozen, de invoer als Google-zoekopdracht in een nieuw tabblad opent. De gebruiker kan kiezen tussen één overzicht met categorieën onder elkaar en een weergave met afzonderlijke categoriepagina's of tabbladen. Thema's, weergavevoorkeuren en openingsgedrag worden via instellingen beheerd. Externe toegang kan optioneel worden ingeschakeld; wanneer die actief is, haalt een read-only koppeling met Nginx Proxy Manager beschikbare externe URL's op en koppelt die aan de juiste apps, met steeds een handmatige override als terugval. De UI moet aanvoelen als een hoogwaardige, moderne productinterface met sterke visuele hiërarchie, nette details, subtiele animaties en een verzorgde dashboardervaring. +2. Gebruiker voert of beheert de belangrijkste gegevens: Docker-apps met container-ID, technische naam, weergavenaam, afbeelding, ontdekt icoon, online- of offline-status, lokale WebUI-URL, optionele externe URL, zichtbaarheid, categorie, sorteerpositie, detectiebron en laatste statuscontrole; categorieën met naam, icoon, sorteerpositie en weergave-instellingen; favorieten met naam, URL, icoon, categorie en sorteerpositie; dashboardinstellingen met gekozen thema, weergavemodus, openingsgedrag, zoekprovider, pollinginterval en externe-toegangsschakelaar; integratie-instellingen zonder geheime waarden; import- en exportbestand met versieerbaar configuratieschema; synchronisatiestatus en beperkte foutinformatie voor Unraid, Docker-proxy en Nginx Proxy Manager. +3. Systeem levert de gewenste uitkomst: De eerste bruikbare versie draait als Docker-container op Unraid en kan als browserstartpagina worden ingesteld. Na de eerste start ontdekt de app de beschikbare Docker-containers, toont ze in een instellingenoverzicht en laat de gebruiker selecteren welke apps op het dashboard verschijnen. Zichtbare apps staan netjes gegroepeerd in configureerbare categorieën, tonen naam, icoon en actuele online- of offline-status en openen hun lokale WebUI in een nieuw tabblad. Favoriete websites kunnen handmatig worden toegevoegd. De zoekbalk vindt apps en favorieten en kan rechtstreeks een Google-zoekopdracht starten. Alle instellingen blijven na een herstart behouden en kunnen worden geëxporteerd en opnieuw geïmporteerd. De totale ervaring moet eruitzien als een afgewerkt premium product dat de gebruiker met plezier dagelijks als startpagina gebruikt. + +## Non-Functional Requirements + +- Quality bar: De interface moet eruitzien als een professioneel premium product en niet als een simpel hobbydashboard. Verwacht een moderne, luxueuze en verzorgde UI met uitgebalanceerde witruimte, consistente kaartcomponenten, hoogwaardige iconografie, duidelijke typografie, nette schaduwen of diepte waar passend, subtiele animaties, verzorgde empty states, sterke visuele hiërarchie en een kalme maar rijke look-and-feel. De belangrijkste startpagina moet snel laden en direct bruikbaar zijn wanneer integraties traag of tijdelijk offline zijn. Fouten moeten begrijpelijk worden getoond zonder technische stacktraces. Ontbrekende iconen of URL's moeten nette fallbacks hebben. Instellingen moeten veilig opgeslagen worden en bij foutieve invoer gevalideerde feedback geven. De interface moet responsief zijn voor desktop, tablet en basisgebruik op mobiel. Toetsenbordnavigatie, zichtbare focus, semantische HTML en voldoende kleurcontrast zijn vereist. Statuspolling mag geen zichtbare flikkering veroorzaken en moet stoppen wanneer de pagina niet actief is of verstandig worden vertraagd. De code moet modulair, getypeerd, gedocumenteerd en zonder onnodige complexiteit zijn. Esthetische kwaliteit is voor deze MVP een kernvereiste en niet louter een extra. +- Testing expectations: Unit tests voor URL-normalisatie, categorie- en sorteervoorwaarden, zoekresultaten, configuratievalidatie, import- en exportversies, container-naar-Nginx-hostmatching en permissiegrenzen; backend-integratietests met gemockte Unraid-templategegevens, Docker Socket Proxy en Nginx Proxy Manager; SQLite-migratie- en persistentietests; tests die aantonen dat geen Docker-mutatie-endpoints of beheeracties worden gebruikt; browsertests voor eerste ingebruikname, containers zichtbaar maken, categorieën beheren, favorieten toevoegen, zoeken, Google-fallback, thema wisselen, weergavemodus wijzigen, URL openen en configuratie exporteren en importeren; visuele UI-smoketests voor kernschermen zoals dashboard, categorie-overzicht en instellingen; Docker-buildtest en Compose-smoketest; een live read-only validatie op Unraid waarbij discovery, status en lokale WebUI-URL's worden gecontroleerd zonder containers te wijzigen. +- Performance, toegankelijkheid en observability: Lokale fonts, een compacte productieclient, zichtbare toetsenbordfocus, semantische navigatiestatus, gereduceerde animatie waar gevraagd, visibility-aware polling, gestructureerde serverlogs en `/api/health`. De UI wordt zonder horizontale overflow getest op 390×844, 768×1024, 1440×900 en 3440×1440. + +## UX Expectations + +- UX verwachtingen: Een rustige premium startpagina die van smalle telefoon tot ultrawide proportioneel schaalt. De gebruiker kan thema, accentkleur, dichtheid, contentbreedte, achtergrond, wallpaperpassing/focus/zoom, dashboardhero, statusbadges en overzichtsmodus aanpassen; voorkeuren blijven persistent. De wallpapereditor toont vóór toepassen een realistische preview van het volledige platformvlak. Een geüploade wallpaper heeft voorrang op Aurora, Blauwdruk en Minimaal en loopt ononderbroken onder alle dashboardkolommen. +- Belangrijkste schermen en interacties: Dashboard met hero, zoekbalk, live overzicht, responsieve apptegels en maximaal acht configureerbare servicewidgets met read-only containerstatistieken; instellingen voor uiterlijk, apps, categorieën, favorieten, integraties en back-up. Appbeheer ondersteunt zoeken, zichtbaarheids- en widgetfilters; iedere canonieke categorie heeft een afzonderlijk semantisch pictogram, categorie-iconen zijn bewerkbaar en favorieticonen worden automatisch first-party geladen met een handmatige override. + +## Data + +- Data involved: Docker-apps met container-ID, technische naam, weergavenaam, afbeelding, ontdekt icoon, online- of offline-status, lokale WebUI-URL, optionele externe URL, zichtbaarheid, categorie, sorteerpositie, detectiebron en laatste statuscontrole; categorieën met naam, icoon, sorteerpositie en weergave-instellingen; favorieten met naam, URL, icoon, categorie en sorteerpositie; dashboardinstellingen met gekozen thema, weergavemodus, openingsgedrag, zoekprovider, pollinginterval en externe-toegangsschakelaar; integratie-instellingen zonder geheime waarden; import- en exportbestand met versieerbaar configuratieschema; synchronisatiestatus en beperkte foutinformatie voor Unraid, Docker-proxy en Nginx Proxy Manager. +- Integrations: Unraid Docker-templatebestanden of beschikbare Unraid-metadata voor containernaam, WebUI en iconen; een beperkte read-only Docker Socket Proxy voor containerinventaris en runtime-status; Nginx Proxy Manager API in read-only modus voor optionele externe hostnamen en URL's; Google Search via een gewone zoek-URL zonder API-key; Gitea API voor het aanmaken van een private repository en het configureren van de Git-remote. + +## Deployment And Runtime + +- Preferred stack/runtime: TypeScript als hoofdtaal; React met Vite voor de frontend; Node.js met Fastify voor de backend; SQLite met Drizzle ORM voor persistentie en migraties; gedeelde types en validaties met Zod; een eenvoudige REST-API tussen frontend en backend; een zorgvuldig opgebouwd design system met design tokens, CSS-variabelen en herbruikbare UI-componenten; een moderne componentbibliotheek of headless aanpak voor toegankelijke basiscomponenten; iconenset van hoge kwaliteit; subtiele motion via lichte animatielogica; Docker multi-stage build en Docker Compose voor Unraid; Vitest voor unit- en integratietests; Playwright voor browsertests. Gebruik een overzichtelijke repository met duidelijk gescheiden frontend-, backend- en shared-mappen. Richt de frontend expliciet in op een premium dashboardervaring met consistente spacing, kaartsystemen, typografie, kleurgebruik, statusbadges en instellingenpanelen. +- Runtime model: Interactieve app +- Update/sync strategy: Polling +- Persistence: SQLite +- Deployment target: Docker op Unraid +- Repository state: private Gitea-repository `NuklearRabbit/DockDeck`; lokale `main` volgt de remote. +- Omgevingen: lokale development, geïsoleerde Vitest/Playwright-tests en productie op Unraid `192.168.10.150:1218`. + +## Security And Privacy + +- Credentials verwacht: Ja +- Security/privacy notes: Gebruik uitsluitend environmentvariabelen voor gevoelige configuratie. Voorzie minimaal variabelen zoals GITEA_URL, GITEA_TOKEN en GITEA_OWNER voor repository-initialisatie en optionele NPM_URL, NPM_USERNAME en NPM_PASSWORD of een ondersteund read-only token voor Nginx Proxy Manager. Gebruik voor de Docker-koppeling geen onbeperkte socketmount in de applicatiecontainer, maar een afzonderlijke proxy met alleen de minimaal noodzakelijke read-endpoints. Sla secrets nooit op in SQLite, exports, logs, frontendbundels, testfixtures of Git. Toon gevoelige integratiewaarden gemaskeerd in de UI. De app mag geen Docker-mutaties uitvoeren en mag nooit containers starten, stoppen, herstarten, verwijderen of aanpassen. De MVP heeft geen eigen authenticatie en moet daarom standaard alleen op een lokaal bindadres of afgeschermd Docker-netwerk worden gepubliceerd. Externe toegang via Nginx Proxy Manager wordt alleen voorbereid; correcte authenticatie en toegangscontrole via bijvoorbeeld Authentik blijven de verantwoordelijkheid van een latere deploymentstap. +- Secrets mogen nooit worden gecommit. + +## Out Of Scope + +Containers starten, stoppen, herstarten, verwijderen of wijzigen; Docker Compose-stacks beheren; Unraid-systeembeheer; terminal- of shelltoegang; logviewer; resourcegrafieken en uitgebreide monitoring; notificaties en incidentbeheer; publieke internettoegang configureren; Authentik- of andere SSO-integratie; eigen gebruikersaccounts, rollen of multi-userondersteuning; opslag van echte API-tokens in de repository of browser; automatische wijzigingen aan Nginx Proxy Manager; cloudhosting; mobiele native apps; AI-functionaliteit; synchronisatie tussen meerdere Unraid-servers; automatische publicatie van diensten naar het internet. + +## Open Questions + +- Welke requirements zijn release-blocking? +- Welke externe systemen moeten eerst gevalideerd worden? + +## Widget Extension 2026-07-14 + +- FR-3 (Done): toon standaard een configureerbare Unraid-hostwidget met host-, CPU-, geheugen-, image- en containerinformatie uit Docker `/info`. +- FR-4 (Done): verrijk geselecteerde AdGuard-, Plex-, Immich-, Home Assistant-, Sonarr-, Radarr- en Lidarr-widgets met provider-specifieke read-only gegevens wanneer uitsluitend server-side environmentcredentials beschikbaar zijn. +- FR-5 (Done): een ontbrekende of falende provider-API valt altijd terug op generieke Dockerstats en blokkeert discovery of navigatie nooit. +- FR-6 (Done): apps kunnen uit DockDeck worden verwijderd zonder Docker te wijzigen; de technische containernaam blijft persistent onderdrukt bij discovery en wordt meegenomen in back-ups. +- FR-7 (Done): de dashboardzoekfunctie indexeert actuele appnamen, technische namen, categorieën, URL's, images, favorieten en de Unraid-host en beweegt direct mee met configuratiewijzigingen. +- FR-8 (Done): iedere servicewidget ondersteunt persistent een compact, standaard of breed formaat en afzonderlijke inhoudslagen voor samenvatting, appmetrics, Dockertelemetrie en voortgang. +- FR-9 (Done): de Unraid-hostwidget heeft eigen persistente formaat- en inhoudsinstellingen; alle widgetconfiguratie blijft exporteerbaar, legacy-compatibel en responsief. +- FR-10 (Done): widgets hebben een onafhankelijke volgorde met toetsenbordbediening en een directe dashboardbewerkingsmodus. +- FR-11 (Done): iedere widget ondersteunt metricvolgorde, primaire metric, aangepast label, waarschuwingsdrempel, sparkline en een veilige eigen verversingscadans. +- FR-12 (Done): de Unraid-hostwidget leest read-only array-, parity-, disk-/pool-, temperatuur- en optionele UPS-status uit minimaal gemounte hostbronnen. +- FR-13 (Done): app-specifieke providers zijn uitgebreid met Gitea, Jellyfin, Seerr, Prowlarr, Authentik, NPM, Portainer, Grafana, Prometheus en Nextcloud; beveiligde POST-gebaseerde API's gebruiken optionele GET-only metrics bridges. +- FR-14 (Done): widgets tonen expliciet `fresh`, `stale` of `unavailable` en behouden laatst succesvolle providergegevens bij tijdelijke uitval. +- FR-15 (Done): layoutpresets kunnen afzonderlijk worden opgeslagen, toegepast, verwijderd, geëxporteerd en geïmporteerd. +- FR-16 (Done): het platform biedt secretvrije diagnostics en een installable PWA-shell die nooit `/api`-responses cachet. +- FR-17 (Done): de Unraid-hostnaam en WebUI-URL zijn persistent instelbaar onder Widgets; Tower gebruikt standaard expliciet poort `5000` en de aangepaste waarde stuurt zowel de hostkaart als de dynamische zoekindex. +- FR-18 (Done): iedere ontdekte app staat in Widget Studio. Native providers krijgen gedocumenteerde read-only adapters; overige apps krijgen een optionele, server-side GET-only JSON-koppeling met configureerbare URL en bearer-token. +- FR-19 (Done): per widget kan de gebruiker afzonderlijke appmetrics tonen/verbergen, tot 24 metrics rangschikken, de primaire metric kiezen en alle keuzes meenemen in presets en back-ups. De live inventaris heeft specifieke profielen voor applicaties, workers, databases, caches, queues, mail, GPU, media, downloads en eigen projecten. +- FR-20 (Done): de standaard Unraid-installatie bestaat uit precies één container met de vaste naam `DockDeck`, eigen icoon en correcte WebUI op poort `1218`. Discovery gebruikt uitsluitend GET-verzoeken naar de bestaande AppOps-inventaris en mount nooit de Docker-socket; voor hosts zonder AppOps blijft een expliciete, afgescheiden socket-proxyoverlay beschikbaar. +- FR-21 (Done): iedere appwidget kan een echte provider- of infrastructuurreeks als lijn-, vlak- of staafgrafiek tonen, 10–60 samples kiezen, de legenda schakelen, het aantal stats begrenzen, iedere metric als waarde/gauge/progress presenteren en op iedere numerieke metric boven of onder een configureerbare drempel waarschuwen. +- FR-22 (Done): de Tower-widget ondersteunt dezelfde diepe configuratie voor zichtbare serverstats, volgorde, statlimiet en grafieken van containers, images, opslag, temperatuur, parity en UPS. Provider- en serverhistoriek blijft maximaal 60 punten uitsluitend in procesgeheugen. +- FR-23 (Done): Unraid krijgt een werkelijk PNG-gecodeerd transparant DockDeck-icoon; de Compose-label en het gebruikerssjabloon verwijzen niet langer naar SVG-inhoud die Unraid foutief als `.png` cachet, en de deployment ververst zowel de persistente als actieve DockerMan-cache. diff --git a/RISKS.md b/RISKS.md new file mode 100644 index 00000000..836aa3e6 --- /dev/null +++ b/RISKS.md @@ -0,0 +1,75 @@ +# RISKS.md + +## Aanvulling 2026-07-22 + +- Open-Meteo is een externe, ongeauthenticeerde databron. DockDeck cachet tien minuten, gebruikt een korte timeout en laat alleen de weerbadge zonder actuele waarde bij uitval; navigatie, widgets en instellingen blijven volledig bruikbaar. +- Een wallpaper kan de leesbaarheid verminderen. DockDeck beperkt bestandstype en grootte, bewaart het bestand lokaal, gebruikt een rustige uniforme contrastlaag en maakt rails, widgets en tegels doorschijnende glasoppervlakken; de gebruiker kan de afbeelding direct verwijderen. Bij `Passend` vult een vervaagde kopie de ultrawide zijruimte, terwijl het scherpe hoofdbeeld volledig zichtbaar blijft. +- `Passend` behoudt de volledige afbeelding en kan bij een afwijkende beeldverhouding rustige zij- of bovenruimte tonen; `Vullend` benut het vlak volledig maar kan randen afsnijden. De preview, focusregelaars en begrensde zoom maken deze afweging expliciet zonder het originele bestand te bewerken. + +Project: DockDeck +Updated: 2026-07-22 + +## Assumptions + +- Eén lokale beheerder; geen accounts of multi-userrollen in de MVP. +- Docker- en Unraid-metadata zijn read-only en bereikbaar vanuit het afgeschermde Compose-netwerk. +- Nginx Proxy Manager is optioneel; een handmatige/lokale URL blijft altijd de fallback. +- SQLite is de persistente bron voor normale configuratie; providercredentials uit Settings staan afzonderlijk als environmentassignments in het named volume. + +## Open risks + +- Unraid WebUI-templatevelden verschillen per communitytemplate. De parser ondersteunt de gangbare `[IP]`, `[HOST]`, `[PORT]` en `[PORT:n]` vormen; onbekende vormen vragen een handmatige URL-override. +- Een container zonder WebUI of icoon gebruikt een nette fallback maar kan pas navigeren na een URL-override. +- Automatische NPM-matching accepteert alleen één ondubbelzinnige hostname; complexe namen blijven bewust handmatig. +- DockDeck heeft geen eigen authenticatie. De standaardbind is daarom `127.0.0.1`; een LAN-bind of reverse proxy vereist bewuste netwerktoegangscontrole. +- Credentialinvoer over de huidige LAN-HTTP-deployment is niet transportversleuteld. Gebruik dit scherm alleen op het vertrouwde LAN; zet voor toegang buiten dat segment een geauthenticeerde HTTPS-reverse proxy in. +- `/data/integrations.env` bevat plaintext providercredentials met bestandmodus `0600`. JSON-exports sluiten dit bestand uit, maar een ruwe volumeback-up bevat het wel en moet als secretmateriaal worden beschermd. +- De Unraid-host heeft zijn automatische Docker-address-pools opgebruikt. DockDeck gebruikt daarom twee expliciete, kleine subnetten (`172.31.240.0/28` en `172.31.240.16/28`); controleer deze opnieuw bij netwerkherconfiguratie. +- Unraid-templates kunnen secrets bevatten. De app krijgt uitsluitend een gegenereerde metadata-mirror met `Name`, `WebUI` en `Icon`; voer de sanitizer opnieuw uit nadat templates wijzigen. +- Niet iedere website publiceert een bruikbaar icoon op `/favicon.ico`; zulke favorieten en apps vallen terug op een lokaal merk- of semantisch service-icoon en kunnen in Settings nog steeds een expliciete icoon-URL krijgen. +- De standaard single-containerdeployment is voor discovery afhankelijk van de bestaande AppOps-gateway. Bij uitval blijven alle opgeslagen navigatie en instellingen bruikbaar en krijgt de integratie een degraded status; discovery hervat automatisch zodra AppOps terug is. +- AppOps levert actuele CPU- en geheugenwaarden, maar momenteel geen netwerkdoorvoer of PID-aantal. Native providers en GET-only metrics bridges blijven die app-specifieke lagen leveren; zonder zo'n koppeling toont DockDeck voor die velden een eerlijke unavailable-status. +- Provider-API's verschillen per versie en vereisen credentials met uiteenlopende scopes. DockDeck gebruikt alleen gedocumenteerde GET-routes en korte timeouts; zonder endpoint toont de kaart haar eigen capabilitylabels met een expliciete connectiestatus naast gescheiden echte Dockertelemetrie. Configureer waar mogelijk een afzonderlijke minimum-scope token en roteer die periodiek via Settings of de deploymentenvironment. +- Custom widgetendpoints zijn een expliciet lokaal vertrouwensbesluit: DockDeck doet alleen een GET met korte timeout, begrenst labels/waarden en echoot tokens niet, maar de gebruiker blijft verantwoordelijk voor een read-only endpoint en een minimum-scope token. Gebruik geen URL die bij een GET een mutatie veroorzaakt. +- App-specifieke profielen beschrijven mogelijke metriekvelden; eigen applicaties moeten het gedocumenteerde JSON-contract zelf aanbieden voordat deze waarden live worden. Zonder endpoint blijft de kaart volledig bruikbaar met read-only Dockertelemetrie. +- Grafiekhistoriek is bewust procesgeheugen: maximaal zestig samples per reeks, geen geheimen en geen langdurige opslag. Een containerrestart wist de trend zonder configuratie of navigatie te verliezen. +- Unraid cachet containericonen tweemaal onder een vaste `.png`-naam: persistent in `/var/lib/docker/unraid/images` en actief onder `/usr/local/emhttp/state`. Alleen de persistente cache vervangen is onvoldoende zolang de actieve cache bestaat; `deploy/refresh-unraid-icon.sh` valideert daarom het echte PNG-MIME-type en vervangt uitsluitend beide DockDeck-bestanden. +- Gemengde widgetformaten gebruiken CSS-grid-dense plaatsing. De onafhankelijke widgetvolgorde blijft gelijk aan de DOM-/toetsenbordvolgorde; directe editknoppen behouden deze toegankelijkheidsregel zonder drag-and-drop. +- Unraid hwmon-classlinks kunnen buiten de beperkte mount wijzen. DockDeck behandelt iedere onleesbare sensor afzonderlijk als optioneel en behoudt array-/disktemperaturen uit `disks.ini`; de Dockerproxy wordt niet verruimd. +- Ollama en Glances gebruiken standaard de ontdekte lokale app-origin zonder credential. Een reverse-proxysubpad of afwijkende API-root vraagt een expliciete URL-override in Settings; een fout valt terug op app-specifieke Dockertelemetrie. +- Tautulli plaatst zijn API-key volgens het officiële contract in de querystring van de uitgaande LAN-request. DockDeck logt die URL niet, maar de doelserver kan dat wel doen; gebruik daarom een afzonderlijke key en bescherm targetlogs. +- De huidige AdGuard API is met Basic Auth beveiligd en JDownloader publiceert alleen noVNC; zonder aanvullende credentials/API-activering tonen deze kaarten daarom veilige app-specifieke live Dockertelemetrie (waaronder transfersnelheid), maar geen querylog, blocktotalen of downloadqueue. +- Een verwijderde app blijft onderdrukt zolang de technische containernaam gelijk blijft. Als Docker de container later bewust onder een andere naam aanbiedt, behandelt DockDeck dit als een nieuw ontdekte app en blijft die standaard verborgen. +- LAN-adresdetectie gebruikt alleen expliciete `br0`-netwerken. Afwijkende macvlan-netwerknamen of meerdere gepubliceerde poorten blijven bewust handmatig overschrijfbaar om geen onzekere URL te kiezen. +- Nieuwe apps blijven als “nieuw ontdekt” gemarkeerd totdat hun zichtbaarheid bewust wordt gewijzigd. Dat is een eenvoudige erkenningshandeling zonder aparte inbox; een gebruiker die alleen andere metadata bewerkt, blijft de ontdekmarkering zien. +- De directe Stitch-pass ligt bewust als laatste, geïsoleerde presentatielaag in `stitch-direct.css` boven de oudere stylesheets. Dat houdt de gedragswijziging klein en reviewbaar, maar een toekomstige frontendopschoning kan de nu overschreven legacyregels veilig verwijderen zodra alle zeldzame settingsstates opnieuw visueel zijn geïnventariseerd. +- De herkenbare merkfallbacks komen uit de vastgepinde MIT-package `@icons-pack/react-simple-icons@13.8.0`. Expliciete appiconen uit Unraid of Settings blijven leidend; niet-afgedekte eigen diensten gebruiken bewust een semantisch Lucide-icoon. +- De lokale Windows-omgeving heeft geen `docker`-CLI/runtime. Compose-config en de klassieke imagebuild zijn daarom op Tower uitgevoerd en voor release `72879e1` groen; de bekende inconsistente BuildKit-cache blijft hostbrede technische schuld. + +- De gedeelde ultrawide-canvas en rechter widgetdock zijn bewust in dezelfde presentatielaag geimplementeerd. De E2E-suite valideert geen horizontale overflow en open dock-state op 3440 px, maar zeldzame combinaties van veel brede widgets kunnen later nog een fijnere masonry-/virtualisatielaag vragen. +- Automatische inventarisopschoning verwijdert alleen DockDeck-records die ontbreken in een aantoonbaar verbonden AppOps/Docker-respons. Een foutief maar succesvol leeg antwoord van de externe inventarisbron zou daardoor als autoritatief gelden; maak voor deployment een secretvrije export en bewaak de gerapporteerde containertelling. De Docker-containers zelf worden nooit gewijzigd. +- De Unraid-iconcache wordt bij containerstart uit de root-only DockerMan-iconenmap gevuld en is bewust niet persistent. Een gewijzigd icoon wordt daarom na een containerrecreate zichtbaar; de hostbron blijft read-only en het blijvende applicatieproces draait als UID 100. +- De slimme categorieactie is bewust niet onderdeel van discovery of heartbeat. Nieuwe of hernoemde niche-services blijven in hun bestaande categorie tot de beheerder de actie opnieuw uitvoert of ze handmatig wijzigt; regels moeten bij nieuwe canonical servicetypen expliciet en getest worden uitgebreid. +- Een geïmporteerd of handmatig opgeslagen categorie-icoon buiten de ondersteunde catalogus gebruikt bewust `Layers3` als fallback. Het blijft in Instellingen als aangepast zichtbaar en kan daar zonder dataverlies naar een van de afzonderlijke ondersteunde pictogrammen worden omgezet. + +## Blockers + +- Geen releaseblockers. Live NPM/providerdiepgang en UPS-status wachten uitsluitend op optionele minimum-scope omgevingsconfiguratie. + +## Technical debt + +- De eerste migratie is idempotente SQL naast Drizzle-schema's; bij een volgende schemawijziging hoort een genummerde migratietabel/runner. +- NPM-auth met username/password vraagt tijdelijk een token op; een expliciet read-only `NPM_TOKEN` blijft de aanbevolen productieconfiguratie. +- Playwright gebruikt één sequentiële testdatabase. Een toekomstige grotere suite kan per worker een geïsoleerde databasefixture gebruiken. +- De voorkeurenmigratie voegt kolommen idempotent toe aan bestaande databases. Bij verdere schema-uitbreiding blijft een genummerde migratierunner de gewenste vervolgstap. +- De Unraid-host heeft een inconsistente BuildKit-cachelaag. DockDeck kon veilig met de klassieke builder worden gebouwd; herstel van de hostbrede BuildKit-cache valt buiten dit project. + +## Security and privacy + +- DockDeck leest nooit container-environments of applicatieconfiguratie om providercredentials automatisch te vinden; provider-URL's en tokens worden expliciet als server-side environmentvariabelen aangeleverd. + +- Secrets bestaan uitsluitend als procesenvironment en, bij invoer via Settings, in het permission-restricted environmentbestand. Ze worden niet in SQLite of JSON-export opgeslagen en nooit via een API-response teruggegeven. +- De appcontainer krijgt geen Docker-socket. De standaardroute leest alleen AppOps `GET /api/containers`; de optionele fallbackproxy is niet gepubliceerd, heeft `POST=0`, minimale GET-families en een intern netwerk. +- De app definieert geen start-, stop-, restart-, delete-, exec- of andere Docker-mutaties. +- NPM wordt alleen gelezen; DockDeck wijzigt of publiceert geen proxyhosts. +- De sanitizer schrijft alleen toegestane metadata en verifieert de resulterende XML voordat DockDeck die leest. diff --git a/SPEC.md b/SPEC.md new file mode 100644 index 00000000..0d5f7847 --- /dev/null +++ b/SPEC.md @@ -0,0 +1,88 @@ +# SPEC.md + +## Summary + +Bouw een persoonlijke webapp die als standaard startpagina van de browser dient en een prachtig, professioneel en strak afgewerkt overzicht geeft van de diensten op de Unraid-server. De app ontdekt Docker-containers automatisch via Unraid en een beperkte read-only Docker-socketproxy, maar toont nieuwe containers niet automatisch op het hoofddashboard. In het instellingenmenu kan de gebruiker per container zichtbaarheid, categorie, sortering, icoon, naam en URL beheren. Voor Docker-apps wordt standaard de lokale WebUI-URL uit de Unraid-configuratie gebruikt. Daarnaast kunnen handmatig favoriete externe websites worden toegevoegd met naam, URL, icoon, categorie en sorteerpositie. Het dashboard bevat een centrale zoekbalk die zowel zichtbare Docker-apps als favorieten doorzoekt en, wanneer geen lokaal resultaat wordt gekozen, de invoer als Google-zoekopdracht in een nieuw tabblad opent. De gebruiker kan kiezen tussen één overzicht met categorieën onder elkaar en een weergave met afzonderlijke categoriepagina's of tabbladen. Thema's, weergavevoorkeuren en openingsgedrag worden via instellingen beheerd. Externe toegang kan optioneel worden ingeschakeld; wanneer die actief is, haalt een read-only koppeling met Nginx Proxy Manager beschikbare externe URL's op en koppelt die aan de juiste apps, met steeds een handmatige override als terugval. De UI moet aanvoelen als een hoogwaardige, moderne productinterface met sterke visuele hiërarchie, nette details, subtiele animaties en een verzorgde dashboardervaring. + +## Architecture + +- Project type: webapp +- Preferred stack: TypeScript als hoofdtaal; React met Vite voor de frontend; Node.js met Fastify voor de backend; SQLite met Drizzle ORM voor persistentie en migraties; gedeelde types en validaties met Zod; een eenvoudige REST-API tussen frontend en backend; een zorgvuldig opgebouwd design system met design tokens, CSS-variabelen en herbruikbare UI-componenten; een moderne componentbibliotheek of headless aanpak voor toegankelijke basiscomponenten; iconenset van hoge kwaliteit; subtiele motion via lichte animatielogica; Docker multi-stage build en Docker Compose voor Unraid; Vitest voor unit- en integratietests; Playwright voor browsertests. Gebruik een overzichtelijke repository met duidelijk gescheiden frontend-, backend- en shared-mappen. Richt de frontend expliciet in op een premium dashboardervaring met consistente spacing, kaartsystemen, typografie, kleurgebruik, statusbadges en instellingenpanelen. +- Runtime model: Interactieve app +- Update/sync strategy: Polling +- Persistence: SQLite +- Deployment target: Docker op Unraid +- Deployment profile: de standaard Compose-installatie maakt precies één branded container `DockDeck` op poort `1218`. Containerinventaris en CPU/geheugenstatus komen via GET van de bestaande AppOps-gateway; de applicatie mount nooit de Docker-socket. Een afzonderlijke beperkte socketproxy blijft alleen als optionele fallbackoverlay beschikbaar. +- Repository state: private Gitea-repository `NuklearRabbit/DockDeck`; `main` volgt de remote. +- Constraints: De MVP moet als Docker-container op de bestaande Unraid-server draaien en alleen vanaf het lokale netwerk bereikbaar zijn. De app moet zonder eigen login bruikbaar zijn, omdat netwerktoegang de primaire beveiligingsgrens vormt. Alle Docker-toegang moet read-only verlopen via een beperkte socketproxy; de hoofdcontainer krijgt geen directe onbeperkte Docker-socket. Unraid-templategegevens mogen alleen worden gelezen. Nginx Proxy Manager wordt uitsluitend read-only benaderd. De applicatie moet bruikbaar blijven wanneer Nginx Proxy Manager niet is geconfigureerd of tijdelijk niet bereikbaar is. Er mogen geen betaalde externe diensten nodig zijn. Alle configuratie moet persistent blijven bij herstart en eenvoudig te back-uppen zijn. Codex moet een nieuwe private repository in de persoonlijke Gitea-omgeving aanmaken en mag geen secrets hardcoderen. Visuele kwaliteit is belangrijk, maar de UI mag niet onnodig zwaar of traag worden; professionele polish moet samengaan met vlotte prestaties als browserstartpagina. + +## Module Boundaries + +| Module | Responsibility | Notes | +| -------------- | ------------------------------------------- | --------------------------- | +| UI / Interface | Gebruikersinteractie en invoer verzamelen. | Afstemmen op projecttype. | +| Domain | Businessregels en pure transformaties. | Houd afhankelijkheden laag. | +| Infrastructure | Data, integraties en runtime adapters. | Isoleer externe systemen. | +| Documentation | Projectdossier, beslissingen en overdracht. | Altijd actueel houden. | + +## Dataflow + +```text +Gebruiker -> Interface -> Domain logic -> Data/integraties -> Resultaat -> Documentatie/testlog +``` + +## Interfaces And Contracts + +- Inputdata: Docker-apps met container-ID, technische naam, weergavenaam, afbeelding, ontdekt icoon, online- of offline-status, lokale WebUI-URL, optionele externe URL, zichtbaarheid, categorie, sorteerpositie, detectiebron en laatste statuscontrole; categorieën met naam, icoon, sorteerpositie en weergave-instellingen; favorieten met naam, URL, icoon, categorie en sorteerpositie; dashboardinstellingen met gekozen thema, weergavemodus, openingsgedrag, zoekprovider, pollinginterval en externe-toegangsschakelaar; integratie-instellingen zonder geheime waarden; import- en exportbestand met versieerbaar configuratieschema; synchronisatiestatus en beperkte foutinformatie voor Unraid, Docker-proxy en Nginx Proxy Manager. +- Integraties: Unraid Docker-templatebestanden of beschikbare Unraid-metadata voor containernaam, WebUI en iconen; een beperkte read-only Docker Socket Proxy voor containerinventaris en runtime-status; Nginx Proxy Manager API in read-only modus voor optionele externe hostnamen en URL's; Google Search via een gewone zoek-URL zonder API-key; Gitea API voor het aanmaken van een private repository en het configureren van de Git-remote. +- Update/sync: Polling +- Persistence: SQLite +- API/UI contracten: JSON REST onder `/api`: dashboardaggregaat, discovery-sync, gevalideerde PATCH voor apps/preferences, CRUD voor categorieën/favorieten, een same-origin credential-write-endpoint en versie 1 export/import. Zod valideert iedere mutatiegrens; credentialwrites antwoorden uitsluitend met `saved: true` en geen response bevat credentialwaarden. +- Categorie-icooncontract: één gedeelde frontendcatalogus voedt de renderer en de instellingenkeuzelijst. Alle canonieke categoriewaarden verwijzen naar een afzonderlijke Lucide-component; onbekende geïmporteerde waarden vallen terug op `Layers3` en nieuwe categorieën krijgen eerst een nog ongebruikt ondersteund pictogram. +- Visuele voorkeuren: `theme`, `accent`, `density`, `contentWidth`, `backgroundStyle`, `wallpaperFit`, `wallpaperPositionX`, `wallpaperPositionY`, `wallpaperZoom`, `showHero`, `showStatus`, `viewMode`, `pollingInterval` en `externalAccess` worden server-side gevalideerd en in SQLite bewaard. Oudere databases krijgen ontbrekende voorkeurkolommen idempotent; versie 1 exports blijven achterwaarts compatibel. De wallpaper schaalt tegen het volledige zichtbare platformviewport onder de topbalk, niet tegen de scrollhoogte of alleen de middenkolom, en vervangt daar de actieve achtergrondpreset. +- Responsive contract: het document krijgt alleen gevalideerde `data-*` attributen voor de visuele tokens. De grid gebruikt vloeiende kolommen en begrensde contentprofielen voor mobiel, tablet, desktop en ultrawide. DM Sans en Newsreader worden lokaal gebundeld. +- Widgetcontract: apps hebben persistente velden voor inschakeling, formaat, onafhankelijke volgorde, informatielagen, custom label, metricvolgorde, primaire metric, statlimiet, per-metric value/gauge/progress-presentatie, boven-/onderdrempel, grafiekreeks, lijn-/vlak-/staaftype, legenda, samplevenster en een begrensde verversingscadans. De discoverylaag leest voor maximaal acht geselecteerde containers alleen `GET /containers/{id}/stats`, houdt maximaal zestig punten uitsluitend in procesgeheugen bij en geeft freshness plus CPU-, geheugen-, netwerk-, proces- en uptimetelemetrie door. Een mislukte statsread blokkeert navigatie of discovery niet. +- Hostwidgetcontract: `showServerWidget`, formaat, volgorde, statselectie/-volgorde/-limiet, configureerbare grafiek en capaciteit-, detail-, voortgangs- en Unraid-native lagen staan standaard aan en worden persistent en exporteerbaar opgeslagen. De hostkaart combineert Docker `GET /info` met read-only mounts voor Unraid runtimebestanden en hwmon; er is geen shell-, socket- of beheeractie. Array-, parity-, opslag-, temperatuur- en optionele UPS-data worden tot een klein presentatiecontract en maximaal zestig geheugenpunten gereduceerd. +- Providerwidgetcontract: de adapterlaag ondersteunt AdGuard Home, Plex, Immich, Home Assistant, Sonarr/Radarr/Lidarr, Ollama, Glances, Tautulli, Paperless-ngx, Gitea, Jellyfin, Seerr, Prowlarr, Authentik, NPM, Portainer, Grafana, Prometheus, Nextcloud, Audiobookshelf, Netdata en PeerTube. Alle netwerkrequests zijn GET en time-bounded. Deluge, qBittorrent, JDownloader, Tdarr, Bazarr en Vaultwarden kunnen een door de gebruiker geconfigureerde GET-only metrics bridge lezen omdat hun native beheer-API geen zuivere GET-only grens biedt. Iedere overige ontdekte app krijgt een eigen allowlisted `CUSTOM_WIDGET__URL` en optionele bearer-token voor een klein JSON-contract met `summary` en maximaal twaalf `stats`; waarden blijven server-side en worden begrensd. Laatst succesvolle data blijft bij fouten als stale cache beschikbaar; app-specifieke Dockerstats blijven de fallback. +- Widgetinhoudcontract: Widget Studio publiceert voor iedere app metriekdefinities met betekenis, groep, unit, grafiekgeschiktheid, standaardpresentatie en aanbevolen zichtbaarheid. De gebruiker kan tot 24 labels persistent verbergen, rangschikken en presenteren; runtime-metrics die een gekoppelde app aanvullend levert worden dynamisch toegevoegd. Zonder koppeling blijven dezelfde app-specifieke labels zichtbaar met een expliciete connectiestatus; Dockertelemetrie wordt niet als fictieve appstat vermomd. Layoutpresets en volledige back-ups bewaren deze keuzes legacy-compatibel. +- Serveridentiteitscontract: `serverName` en `serverUrl` staan in SQLite, export/import en Settings. Zonder opgeslagen URL wordt `UNRAID_URL` of `UNRAID_HOST` gebruikt en, wanneer geen poort is opgegeven, poort `5000` toegevoegd. Dezelfde actuele waarden voeden hostkaart en zoekresultaten. +- Single-containercontract: het AppOps-contract leest uitsluitend `GET /api/containers`, reduceert de response direct tot noodzakelijke inventaris- en metriekvelden en rapporteert uitval als degraded zonder opgeslagen navigatie te blokkeren. CPU- en geheugencapaciteit komen uit twee specifieke read-only `/proc`-bestandsmounts. +- Layoutcontract: `/api/widget-layout` bewaart de afzonderlijke widgetvolgorde. Layoutpresets hebben een eigen gevalideerd record en eigen import/export-API, onafhankelijk van de volledige versie 1-back-up. +- Operationscontract: `/api/diagnostics` retourneert uitsluitend health-, integratie-, discovery- en configuratiestatus zonder secrets of ruwe providerpayloads. De PWA-cache bevat alleen statische shellassets en sluit iedere `/api/`-request expliciet uit. +- Faviconcontract: een expliciete favorieticoon-URL blijft leidend. Zonder override probeert de browser alleen `/favicon.ico`; een fout levert een lokaal monogram en veroorzaakt geen server-side fetch of third-party request. +- Appverwijdercontract: `DELETE /api/apps/:id` verwijdert uitsluitend het DockDeck-record en schrijft de technische containernaam naar `removed_apps`. Discovery slaat die naam voortaan over; Docker en Unraid worden nooit gemuteerd. Versie 1-back-ups mogen de optionele `removedApps`-lijst bevatten. +- Zoekcontract: de zoekindex wordt uitsluitend afgeleid van de actuele dashboardpayload en indexeert zowel zichtbare als verborgen apps, weergave- en technische namen, categorieën, images, URL-varianten, favorieten en de Unraid-host. Lokaal resultaat, toetsenbordselectie en expliciete Google-fallback blijven afzonderlijke acties. +- URL-resolutiecontract: `[IP]` gebruikt voor een expliciet `br0`-netwerk het LAN-adres van de container en anders `UNRAID_HOST`. Een exact gepubliceerde poort blijft leidend; bij een verouderde templatepoort mag één ondubbelzinnige gepubliceerde TCP-poort als fallback dienen. Overrides blijven altijd leidend. + +## File Structure + +- Broncode, tests en documentatie worden gescheiden per verantwoordelijkheid. +- Nieuwe modules krijgen colocated tests waar dat past bij de stack. + +## Error Handling + +- Valideer gebruikersinput dicht bij de grens van het systeem. +- Toon herstelbare fouten duidelijk en log technische details zonder secrets. + +## Security And Privacy + +- Credentials expected: Ja +- Security/privacy notes: Gebruik uitsluitend environmentvariabelen voor gevoelige configuratie. Voorzie minimaal variabelen zoals GITEA_URL, GITEA_TOKEN en GITEA_OWNER voor repository-initialisatie en optionele NPM_URL, NPM_USERNAME en NPM_PASSWORD of een ondersteund read-only token voor Nginx Proxy Manager. Gebruik voor de Docker-koppeling geen onbeperkte socketmount in de applicatiecontainer, maar een afzonderlijke proxy met alleen de minimaal noodzakelijke read-endpoints. Sla secrets nooit op in SQLite, exports, logs, frontendbundels, testfixtures of Git. Toon gevoelige integratiewaarden gemaskeerd in de UI. De app mag geen Docker-mutaties uitvoeren en mag nooit containers starten, stoppen, herstarten, verwijderen of aanpassen. De MVP heeft geen eigen authenticatie en moet daarom standaard alleen op een lokaal bindadres of afgeschermd Docker-netwerk worden gepubliceerd. Externe toegang via Nginx Proxy Manager wordt alleen voorbereid; correcte authenticatie en toegangscontrole via bijvoorbeeld Authentik blijven de verantwoordelijkheid van een latere deploymentstap. + +## Test Strategy + +- Testing expectations: Unit tests voor URL-normalisatie, categorie- en sorteervoorwaarden, zoekresultaten, configuratievalidatie, import- en exportversies, container-naar-Nginx-hostmatching en permissiegrenzen; backend-integratietests met gemockte Unraid-templategegevens, Docker Socket Proxy en Nginx Proxy Manager; SQLite-migratie- en persistentietests; tests die aantonen dat geen Docker-mutatie-endpoints of beheeracties worden gebruikt; browsertests voor eerste ingebruikname, containers zichtbaar maken, categorieën beheren, favorieten toevoegen, zoeken, Google-fallback, thema wisselen, weergavemodus wijzigen, URL openen en configuratie exporteren en importeren; visuele UI-smoketests voor kernschermen zoals dashboard, categorie-overzicht en instellingen; Docker-buildtest en Compose-smoketest; een live read-only validatie op Unraid waarbij discovery, status en lokale WebUI-URL's worden gecontroleerd zonder containers te wijzigen. +- Quality bar: De interface moet eruitzien als een professioneel premium product en niet als een simpel hobbydashboard. Verwacht een moderne, luxueuze en verzorgde UI met uitgebalanceerde witruimte, consistente kaartcomponenten, hoogwaardige iconografie, duidelijke typografie, nette schaduwen of diepte waar passend, subtiele animaties, verzorgde empty states, sterke visuele hiërarchie en een kalme maar rijke look-and-feel. De belangrijkste startpagina moet snel laden en direct bruikbaar zijn wanneer integraties traag of tijdelijk offline zijn. Fouten moeten begrijpelijk worden getoond zonder technische stacktraces. Ontbrekende iconen of URL's moeten nette fallbacks hebben. Instellingen moeten veilig opgeslagen worden en bij foutieve invoer gevalideerde feedback geven. De interface moet responsief zijn voor desktop, tablet en basisgebruik op mobiel. Toetsenbordnavigatie, zichtbare focus, semantische HTML en voldoende kleurcontrast zijn vereist. Statuspolling mag geen zichtbare flikkering veroorzaken en moet stoppen wanneer de pagina niet actief is of verstandig worden vertraagd. De code moet modulair, getypeerd, gedocumenteerd en zonder onnodige complexiteit zijn. Esthetische kwaliteit is voor deze MVP een kernvereiste en niet louter een extra. + +## Deployment Design + +- Runtime/stack: TypeScript als hoofdtaal; React met Vite voor de frontend; Node.js met Fastify voor de backend; SQLite met Drizzle ORM voor persistentie en migraties; gedeelde types en validaties met Zod; een eenvoudige REST-API tussen frontend en backend; een zorgvuldig opgebouwd design system met design tokens, CSS-variabelen en herbruikbare UI-componenten; een moderne componentbibliotheek of headless aanpak voor toegankelijke basiscomponenten; iconenset van hoge kwaliteit; subtiele motion via lichte animatielogica; Docker multi-stage build en Docker Compose voor Unraid; Vitest voor unit- en integratietests; Playwright voor browsertests. Gebruik een overzichtelijke repository met duidelijk gescheiden frontend-, backend- en shared-mappen. Richt de frontend expliciet in op een premium dashboardervaring met consistente spacing, kaartsystemen, typografie, kleurgebruik, statusbadges en instellingenpanelen. +- Deployment target: Docker op Unraid +- Repository state: private Gitea-repository `NuklearRabbit/DockDeck`, remote en deployment zijn operationeel. +- Build, release en rollback stappen: `npm run build`; lokaal `npm start`; op Unraid `docker compose up -d --build`. Rollback via vorige Git-commit/image en herstel van het `dockdeck_data` volume of een versie 1 export. + +## Open Design Questions + +- Opgelost: SQLite met named volume is voldoende voor de eerste release. +- Opgelost: Unraid, Docker Proxy en NPM hebben deterministische adapters/fixtures. +- Opgelost: gestructureerde Fastify-requestlogs plus niet-gevoelige integratiestatus en `/api/health` vormen de minimale observability. diff --git a/START_PROMPT.md b/START_PROMPT.md new file mode 100644 index 00000000..7f1c150a --- /dev/null +++ b/START_PROMPT.md @@ -0,0 +1,24 @@ +# START_PROMPT.md + +Start dit project volgens mijn Codex Project Operating System. +Gebruik de bestanden in deze map als projectdossier. + +Project: DockDeck +Projectidee: Bouw een persoonlijke webapp die als standaard startpagina van de browser dient en een prachtig, professioneel en strak afgewerkt overzicht geeft van de diensten op de Unraid-server. De app ontdekt Docker-containers automatisch via Unraid en een beperkte read-only Docker-socketproxy, maar toont nieuwe containers niet automatisch op het hoofddashboard. In het instellingenmenu kan de gebruiker per container zichtbaarheid, categorie, sortering, icoon, naam en URL beheren. Voor Docker-apps wordt standaard de lokale WebUI-URL uit de Unraid-configuratie gebruikt. Daarnaast kunnen handmatig favoriete externe websites worden toegevoegd met naam, URL, icoon, categorie en sorteerpositie. Het dashboard bevat een centrale zoekbalk die zowel zichtbare Docker-apps als favorieten doorzoekt en, wanneer geen lokaal resultaat wordt gekozen, de invoer als Google-zoekopdracht in een nieuw tabblad opent. De gebruiker kan kiezen tussen één overzicht met categorieën onder elkaar en een weergave met afzonderlijke categoriepagina's of tabbladen. Thema's, weergavevoorkeuren en openingsgedrag worden via instellingen beheerd. Externe toegang kan optioneel worden ingeschakeld; wanneer die actief is, haalt een read-only koppeling met Nginx Proxy Manager beschikbare externe URL's op en koppelt die aan de juiste apps, met steeds een handmatige override als terugval. De UI moet aanvoelen als een hoogwaardige, moderne productinterface met sterke visuele hiërarchie, nette details, subtiele animaties en een verzorgde dashboardervaring. +Startmodus: MVP bouwen + +Belangrijke intakekeuzes: + +- Runtime model: Interactieve app +- Update/sync strategy: Polling +- Persistence: SQLite +- Deployment target: Docker op Unraid +- Repository state: Repo nog nodig + +Lees voordat je bouwt: AGENTS.md, PROJECT_BRIEF.md, REQUIREMENTS.md, SPEC.md, PLAN.md, DECISIONS.md, TEST_LOG.md, RISKS.md en HANDOFF.md. + +- Codex mag scaffolden, bouwen, testen, committen en pushen wanneer de repo en remote duidelijk zijn. +- Start met de kleinste werkende MVP-flow en verifieer die end-to-end. + +Stel alleen vragen wanneer dat nodig is om een wezenlijke product-, architectuur- of veiligheidskeuze te maken. +Werk daarna de docs bij, bouw de kleinste nuttige stap, test aantoonbaar, commit zonder secrets en rond af met een heldere handoff. diff --git a/TEST_LOG.md b/TEST_LOG.md new file mode 100644 index 00000000..5e153eea --- /dev/null +++ b/TEST_LOG.md @@ -0,0 +1,365 @@ +# TEST_LOG.md + +## Unieke categorie-iconen - 2026-07-22 + +| Check | Resultaat | +| ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | +| Live inventaris | Pass; 16 categorieën bevatten al 16 unieke semantische waarden, waarvan 10 eerder niet door de frontendrenderer werden herkend | +| Renderer en instellingen | Pass; één catalogus levert 19 opties en alle 16 canonieke categorieën resolven naar een afzonderlijke Lucide-component | +| Nieuwe categorie | Pass; kiest automatisch het eerste nog ongebruikte ondersteunde pictogram | +| Onbekende importwaarde | Pass; veilige `Layers3`-fallback blijft behouden en verschijnt als Aangepast in Instellingen | +| `npm run format:check` / lint / typecheck | Pass | +| Unit/integratie / security | Pass; 57/57 en 5/5 | +| Playwright | Pass; 13 uitgevoerd en 15 project-gededupliceerd op desktop, tablet, mobiel en ultrawide | +| Build / audit | Pass; JS 377,08 kB, CSS 187,64 kB, server 182,0 kB en 0 kwetsbaarheden | +| React-review | Pass; named components, stabiele catalogus, afgeleide icoonkeuze en behouden semantische SVG-presentatie | + +De release draait live op Tower met image `sha256:1cf11fc02e7e…`, is healthy en heeft 0 herstarts. De `.env` en bestaande JPEG-wallpaper van 3.245.835 bytes bleven byte-identiek. Chrome bevestigt in Categorieën 16 opgeslagen icoonwaarden, 16 gerenderde SVG's en 16 unieke SVG-signaturen; het dashboard toont dezelfde semantische pictogrammen en rapporteert 0 browserwaarschuwingen of -fouten. Er was geen databasewijziging nodig: de bestaande unieke waarden worden uitsluitend voortaan correct gerenderd. + +## Platformbrede wallpaperlaag - 2026-07-22 + +| Check | Resultaat | +| ----------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | +| Geometrie | Pass; stage start op 72 px, eindigt op de viewportonderrand en is exact viewportbreed vanaf x=0 | +| Presetprioriteit | Pass; canvas heeft bij een wallpaper `background-image: none` en een transparante achtergrond | +| Compositie | Pass; scherp instelbaar beeld plus zachte `cover`-ambientlaag, met glasoppervlakken voor linkerrail, tegels en rechter widgetdock | +| Scrollgedrag | Pass; fixed stage blijft onder de topbalk staan terwijl de dashboardinhoud onafhankelijk doorloopt | +| `npm run format:check` / lint / typecheck | Pass | +| Unit/integratie / security | Pass; 54/54 en 5/5 | +| Playwright | Pass; 13 uitgevoerd en 15 project-gededupliceerd op desktop, tablet, mobiel en ultrawide; geen horizontale overflow | +| Build / audit | Pass; JS 375,95 kB, CSS 187,64 kB, server 182,0 kB en 0 kwetsbaarheden | +| React-review | Pass; decoratieve ambientbeelden zijn verborgen, de bestaande semantiek en lokale draftstate blijven intact | + +De live Tower-release draait op image `sha256:f5d62a9e30f6…`, is healthy en heeft 0 herstarts. De `.env` en bestaande JPEG-wallpaper van 3.245.835 bytes bleven byte-identiek; Unraid, AppOps en alle zeven livepanelen zijn connected. Chrome bevestigt op 3440×1215 een fixed stage van x=0/y=72 tot de volledige 3425×1143 browsercontentruimte. Na 900 px scroll blijft y=72 behouden, de presetcanvas blijft transparant, beide zijpanelen gebruiken 62% glas en er is geen horizontale overflow of applicatiefout. + +## Responsieve wallpaperstudio - 2026-07-22 + +| Check | Resultaat | +| ----------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | +| Reproductie | Pass; `cover` schaalde tegen een 3077×3015 contentcanvas in plaats van het zichtbare centrale viewport van 3077×1143 | +| Viewportstage | Pass; wallpaper schaalt tegen `100svh - 72px`, blijft tijdens scrollen in het centrale vlak en verstoort het ultrawide dock niet | +| Preview en bediening | Pass; Passend/Vullend, X-/Y-focus, 80–160% zoom, herstel en expliciet toepassen | +| Persistentie/migratie/export | Pass; vier nieuwe gevalideerde voorkeuren, legacydefaults en versie 1 round-trip | +| `npm run format:check` / lint / typecheck | Pass | +| Unit/integratie / security | Pass; 54/54 en 5/5 | +| Playwright | Pass; 13 uitgevoerd en 15 project-gededupliceerd op desktop, tablet, mobiel en ultrawide | +| Build / audit | Pass; JS 375,56 kB, CSS 185,13 kB, server 182,0 kB en 0 kwetsbaarheden | +| React-review | Pass; lokale draftstate, volledige dependencies, semantische controls, toetsenbordbediening en decoratieve alttekst | + +De live Tower-release draait op image `sha256:606051e4f146…`, is healthy en heeft 0 herstarts. De `.env` en bestaande JPEG-wallpaper van 3.245.835 bytes bleven byte-identiek. Chrome bevestigt op 3440×1215 een wallpaperstage van 2381×1143 binnen een 3015 px hoge scrollcanvas: `Passend`, 50/50-focus en 100% zoom, zonder horizontale overflow of browserfouten. De gecentreerde studio-preview is 1600×686; `Vullend` wijzigt alleen de draft en `Herstellen` zet die terug zonder de live voorkeur te overschrijven. + +## Achtergrond- en wallpaperherstel - 2026-07-22 + +| Check | Resultaat | +| ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Reproductie live | Pass; voorkeur `grid` en JPEG-wallpaper van 3,2 MB waren opgeslagen, maar de CSS-compositie werd ongeldig verklaard door een ontbrekende semantische token | +| Achtergrondpresets | Pass; Aurora, Blauwdruk en Minimaal leveren drie verschillende berekende canvasachtergronden | +| Wallpaperflow | Pass; upload via Instellingen verschijnt als preview, zet `has-wallpaper` en wordt als `/api/wallpaper` in de zichtbare canvas gerenderd | +| `npm run format:check` / lint / typecheck | Pass | +| Unit/integratie / security | Pass; 54/54 en 5/5 | +| Playwright | Pass; 13 uitgevoerd en 15 project-gededupliceerd, inclusief de nieuwe end-to-end achtergrondflow | +| Build / audit | Pass; JS 371,81 kB, CSS 180,35 kB, server 180,6 kB en 0 kwetsbaarheden | +| Live Unraid-release | Pass; image `sha256:977791a84355…` healthy met 0 herstarts, `.env` byte-identiek en bestaande JPEG-wallpaper van 3.245.835 bytes behouden | +| Live Chrome-audit | Pass op 3440×1215; alle presets visueel en berekend verschillend, wallpaper in iedere compositie, 0 overflow en 0 browserfouten | + +De audit heeft via de echte instellingeninterface Aurora, Minimaal en Blauwdruk geactiveerd. De wallpaper bleef tijdens iedere wissel zichtbaar; de canvascomposities gebruikten respectievelijk kleurlichten, een rustige overlay en een raster van 40 px. Blauwdruk is na de controle als actieve gebruikersvoorkeur hersteld. + +## Premium wallpaper-, weer- en controlpass - 2026-07-22 + +| Check | Resultaat | +| ---------------------------- | ---------------------------------------------------------------------------------------------------- | +| Typecheck en unit/integratie | Pass; 54/54 tests inclusief weer-cache en wallpaperformaten | +| Playwright | Pass; 12 uitgevoerd, 12 project-gededupliceerd; desktop/tablet/mobile/3440px, 0 browserfouten | +| Toggle-geometrie | Pass; E2E bevestigt meer dan 18px verplaatsing tussen beide zichtbare posities | +| Visuele screenshots | Pass; weerbadge, compacte Home Assistant-tegel, consistente rails en geen horizontale overflow | +| React-review | Pass; effectcleanup, locatie-afhankelijke revalidatie, semantische switches en server-side datafetch | + +De finale lint, typecheck, unit/integratie, Playwright, security, build en audit zijn groen. Build: client-JS 371,81 kB (115,70 kB gzip), CSS 178,48 kB (30,05 kB gzip), server 180,6 kB; `npm audit` meldt 0 kwetsbaarheden. Tower `docker compose config` en imagebuild zijn groen; image `a371cab602ce…` draait healthy met 0 restarts en een byte-identieke `.env`. Live wallpapercyclus: PUT 201, GET 200, DELETE 204. Open-Meteo gaf voor Mol 21,2 °C/bewolkt. Alle zes appwidgets zijn connected; de ultrawide Chrome-audit toont 0 functionele fouten. + +Project: DockDeck +Laatste gate: 2026-07-22 + +## Autoritatieve inventarisheartbeat en instellingen-UX — 2026-07-22 + +| Check | Resultaat | +| ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | +| `npm run format:check` | Pass, alle bestanden Prettier-conform | +| `npm run lint` | Pass, 0 waarschuwingen | +| `npm run typecheck` | Pass | +| `npm test` | Pass, 48/48; inclusief connected-only opschoning, degraded behoud, deduplicatie en read-only/case-insensitieve Unraid-iconen | +| `npm run test:security` | Pass, 5/5 | +| `npm run test:e2e` | Pass, 12 uitgevoerd / 12 project-gededupliceerd op vier viewports; dashboardcategoriefilter en 0 console-/paginafouten | +| `npm run build` | Pass, JS 364,55 kB / 113,69 kB gzip; CSS 170,68 kB / 28,53 kB gzip; server 167,2 kB | +| `npm audit --audit-level=moderate` | Pass, 0 kwetsbaarheden | +| In-app browseraudit | Pass op 1440×900: topbars beide `left=32`/`height=72`, filter reduceert tot één groep, verticale Integraties, 14px kleine tekst, brede Paneelstudio | +| React-review | Pass: single-flight heartbeat, cleanup van timer/listeners, afgeleide filterstate, semantische knoppen, stabiele ids en lokaal begrensde state | + +De browseraudit mat geen horizontale overflow en bevatte geen browserwarnings of -errors. De opschoonregel wijzigt uitsluitend DockDecks SQLite-records na een verbonden inventarisrespons; Docker blijft read-only en een degraded heartbeat verwijdert niets. + +Live release `5e411d1`: Compose-config en imagebuild groen; image `sha256:2d320ff25b66eb84e3fdad27f5437ad8c9732e99891ceb14d714e95655c6db2c`; container healthy met 0 herstarts en PID 1 als UID 100. De secretvrije pre-sync-export bevat 139 apps, 16 categorieën en 1 favoriet (SHA-256 `35d1cf987511177a7246e906afd27f42f87f714f4b621358aaa21ab22b5873ea`). Na de verbonden heartbeat: 90 apps, 50 zichtbaar, 16 categorieën, 0 stale/ontbrekende/dubbele namen en 0 statusafwijkingen tegenover AppOps. Alle 5 lokale Unraid-iconen geven HTTP 200 met een afbeelding-MIME-type. + +## Finale visuele audit — 2026-07-22 + +| Check | Resultaat | +| ---------------------------------- | --------------------------------------------------------------------------------------------------------------------- | +| `npm run format:check` | Pass, alle bestanden Prettier-conform | +| `npm run lint` | Pass, 0 waarschuwingen | +| `npm run typecheck` | Pass | +| `npm test` | Pass, 42/42; inclusief persistente echte ontdekstatus en legacy-migratie | +| `npm run test:security` | Pass, 5/5 | +| `npm run test:e2e` | Pass, 11 uitgevoerd / 9 project-gededupliceerd op vier viewports; 0 console-/paginafouten | +| `npm run build` | Pass, JS 361,49 kB / 113,01 kB gzip; CSS 147,26 kB / 25,19 kB gzip; server 164,6 kB | +| `npm audit --audit-level=moderate` | Pass, 0 kwetsbaarheden | +| In-app Chrome-audit | Pass: dashboard, Quick Launch en alle zeven instellingensecties; 390, 768, 1440 en 3440 px; geen horizontale overflow | +| React-review | Pass: afgeleide catalogusstate, begrensde renders, effect-cleanup, semantische controls, stabiele keys en portalfocus | +| Secret-/mutatiescan | Pass; geen toegevoegde secrets, externe UI-assets of Docker-mutaties | +| Lokale Docker-validatie | Niet beschikbaar: Windows-omgeving heeft geen Docker-CLI; Compose en imagebuild worden op Tower uitgevoerd | + +De audit heeft de operationele servicepanelen onder een compacte maar zichtbare **Live inzicht**-rij geplaatst, zonder de Paneelstudio of configuratie te verwijderen. Appbeheer gebruikt maximaal twintig rijen per pagina en een echte `isNew`-status; Widget Studio toont twaalf mogelijkheden per tranche. Geavanceerde weergavekeuzes en favoriete-editorvelden blijven volledig werkend via expliciete uitklapsecties. De Quick Launch-portal vult op ultrawide het volledige viewport en herstelt de invoerfocus na de portalovergang. + +### Live release `72879e1` + +| Check | Resultaat | +| ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | +| Pre-deploybehoud | Secretvrije export opgeslagen; `.env`-SHA-256 vóór/staging/na identiek; named volume `dockdeck_dockdeck_data` behouden | +| `docker compose config --quiet` | Pass op huidige en gestagede bron | +| Klassieke Docker-imagebuild | Pass; image `sha256:5025a6599410…`, bundels identiek aan de lokale build | +| Compose release | Pass; één `DockDeck`-container, healthy, 0 herstarts op `192.168.10.150:1218` | +| Persistentie/schema | Pass; 138 apps, 55 zichtbaar, 1 favoriet, 3 categorieën, 6 appwidgets en dark theme; alle API-records hebben `isNew` | +| Read-only integraties | Pass; 233 templates, 3 array-/poolgroepen en 89 AppOps-containers connected; NPM bewust disabled | +| Live Chrome-audit | Pass op 3440×1271: favorietenrail, echte iconen, 0 monogrammen, Quick Launch 3440 px met focus, alle zeven instellingensecties en 0 overflow | + +De vorige bronversie blijft recoverable onder `/mnt/user/appdata/dockdeck-prev-20260722-025420`; de secretvrije pre-release-export en behoudsmetingen staan afgeschermd onder `/mnt/user/appdata/dockdeck-release-backups/72879e1-20260722-025420`. + +## Checks + +| Check | Command / evidence | Result | +| -------------------------- | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | +| Dependency-installatie | `npm install` | Pass | +| Formatting | `npm run format:check` | Pass | +| Lint | `npm run lint` | Pass, 0 warnings | +| TypeScript | `npm run typecheck` | Pass | +| Unit/integratie/migratie | `npm test` | Pass, 42/42; inclusief app-/serverhistoriek, widgetpersistentie, capabilityprofielen en duale Unraid-icooncachehelper | +| Read-only security | `npm run test:security` | Pass, 5/5; inclusief GET-only providergrens, serviceworker zonder API-cache en cachehelper zonder Docker-mutaties | +| Browserflows | `npm run test:e2e` | Pass, 10 uitgevoerd / 18 bewust overgeslagen; vier viewports, advanced widgets, presets, diagnostics en editmodus | +| Production build | `npm run build` | Pass; client 305.49 kB / 91.03 kB gzip, CSS 69.77 kB / 12.85 kB gzip, serverbundle 163.0 kB | +| Dependency security | `npm audit --audit-level=moderate` | Pass, 0 vulnerabilities | +| Production smoke | `PORT=3100 npm start` + HTTP checks | Pass: health `ok`, HTML 200, 4 mockapps, alle nieuw verborgen | +| Visuele browseraudit | In-app Chromium, echte lokale UI | Pass: diepe widgetinstellingen op 375×812 en 2560×1440; mobiele codecontract-overflow hersteld, 0 consolefouten | +| Secret-/mutatiescan | `rg` plus securitytests | Pass; alleen verwachte envnamen en socketmount in de aparte proxy | +| Unraid Compose-config | `docker compose config --quiet` | Pass op Tower/Unraid | +| Unraid image/build | Klassieke Docker-builder | Pass; de BuildKit-cache op de host was inconsistent, de klassieke builder bouwde `dockdeck:local` | +| Unraid live smoke | Compose, health en HTTP API | Pass: healthy, 0 restarts, `192.168.10.150:1218`, persistent named volume | +| Live read-only discovery | Socketproxy + metadata-mirror | Pass: 77 containers, 228 bruikbare templates; 230 templates gesaneerd en geverifieerd tot Name/WebUI/Icon | +| Responsive live release | Unraid Compose + Chromium | Pass: `192.168.10.150:1218`, healthy, 0 restarts, 77 apps, nieuwe voorkeurvelden en geen browserconsolefouten | +| Live servicewidget | Docker stats + Chromium 2560×1080 | Pass: AdGuard gemigreerd, CPU/geheugen/netwerk/uptime gevuld, integraties connected, geen overflow/fouten | +| Providerwidget release | Vitest + in-app Chromium | Pass: Unraid-hostwidget en providerfallback op 390×844 en 2560×1440; geen overflow, overlay of consolefouten | +| React review | React best-practices checklist | Pass: named configurator, stabiele keys, geen extra effects, semantische buttons/switches en zichtbare focus | +| Live hostwidget release | Unraid Compose + API + Chromium | Pass: healthy op `192.168.10.150:1218`; Tower, 77 containers, 72 running, 20 cores, 62 GB, geen overflow/fouten | +| Live tailored widgets | API-delta + Chromium desktop/mobile | Pass: AdGuard DNS-layout/API-lock, JDownloader down-/uploadsnelheid, correcte links, 0 restarts en geen overflow/fouten | +| Live credential settings | API + in-app Chromium | Pass: AdGuard toont alleen ontbrekende username/password, password masking, geen overflow/consolefouten, geen dummywrite | +| Widget Studio lokaal | In-app Chromium + Playwright | Pass: catalogus/filter/search op desktop, responsive op vier viewports, gelijke kaarten, geen overflow/consolefouten | +| Widget Studio live | Unraid API + Compose health | Pass: 41 capabilities voor 82 apps, 2 bestaande widgets behouden, healthy met 0 restarts; credentialstore niet aangemaakt | +| Navigatie/beheer lokaal | Vitest + Playwright + Chromium | Pass: 30 tests, 8 browserchecks; remove-suppressie, rename-search, Tower, Google, sidebarfavorieten en 390px zonder overflow | +| Navigatie/beheer live | Unraid API + in-app Chromium | Pass: healthy/0 restarts; Tower-search/link, Tweakers-sidebar, gecorrigeerde AdGuard- en Immich-URL's, geen fouten/overflow | +| Widgetconfiguratie lokaal | Vitest + Playwright + agent-browser | Pass: drie formaten, inhoudslagen, import/export, 390/3440 px en geen overflow | +| Widgetconfiguratie live | Tower Compose + API + browser | Pass: healthy/0 restarts op `:1218`; 80 apps, 55 zichtbaar, 5 widgets; juiste AnythingLLM/Immich-attributie | +| Volledige widgetroadmap | Vitest + Playwright + API | Pass: onafhankelijke volgorde, metriccompositie, presets, freshness, cadence, warnings, sparklines, PWA en diagnostics | +| Native Unraid live | Read-only mounts + API | Pass: array STARTED, parity healthy/0 fouten, 3 opslaggroepen, 57 °C hoogste meting; integratie connected | +| Finale live responsiviteit | Chromium 390×844 en 3440×1440 | Pass: 7 live panelen incl. Tower, geen positieve overflow, geen Vite-overlay en PWA-manifest aanwezig | +| Persoonlijke widgetdiepte | API + Vitest + Chromium | Pass: 80/80 capabilities, 55 custom GET-profielen, 23 native providers en gescheiden app/worker/database-metrics | +| Tower poort/configuratie | SQLite + live API + Chromium | Pass: naam/URL zichtbaar in Settings; voorkeur en hostkaart wijzen naar `http://192.168.10.150:5000` | +| Finale Unraid release | Compose config/build/up + health | Pass: klassieke imagebuild, healthy, 0 restarts, 80 apps/55 zichtbaar/6 appwidgets; named-volumeconfiguratie behouden | +| Single-container release | AppOps + Compose + API + browser | Pass: exact één `DockDeck`, oude proxy verwijderd, 83 live containers, icoon/WebUI correct, data-ID en 55 zichtbare apps behouden | +| Diepe widgetconfiguratie | Vitest + Playwright + browser | Pass: statselectie/-limiet/-stijl, elke numerieke waarschuwing, 3 grafiektypes, 60-punt provider- en Towerhistoriek | +| App-specifieke fallback | Capabilityaudit + browser | Pass: kaarten behouden eigen metrics zonder endpoint en tonen eerlijke `Connect data`; alle geaudite generieke profielen weg | +| Unraid PNG-icoon | Asset, HTTP, beide caches en Chrome | Pass live: actieve SVG-cache vervangen; Unraid rendert nu de echte 512×512 PNG in DockDecks rij zonder `question.png`-fallback | +| Finale capabilityaudit | Live API + capabilityvergelijking | Pass: 97 opgeslagen apps, 97 capabilityprofielen, nul generieke profielen; BlockPilot en LumaOps expliciet gedekt | +| Diepe widgetrelease live | Compose + API + in-app Chromium | Pass: healthy/0 restarts, 74 ontdekt/69 actief, 55 zichtbaar, 6 appwidgets plus Tower, geen overflow of browserconsolefouten | + +## Scenario coverage + +- Deterministische discovery, Unraid XML/WebUI-portvervanging en NPM-hostmatching. +- Nieuwe containers verborgen; zichtbaarheid, naam, categorie, order en URL-override. +- Categorieën en favorieten aanmaken/bewerken/verwijderen. +- Volledige dynamische zoekindex voor zichtbare/verborgen apps, actuele namen, technische namen, categorieën, images, URL's, favorieten en Tower; expliciete Google-fallback in nieuw tabblad. +- Persistente DockDeck-only appverwijdering met rediscoveryonderdrukking en back-updekking; geen Docker-mutatie. +- Alle drie thema's, overzicht/categorienavigatie en mobiele navigatie. +- Persistente accent-, dichtheid-, contentbreedte-, achtergrond-, hero- en statusvoorkeuren. +- Responsieve grids en navigatie op mobiel, tablet, desktop en ultrawide; lokale fonts zonder externe request. +- Automatische first-party faviconafleiding met lokale merk-/semantische fallback en handmatige override; lettermonogrammen zijn niet meer nodig. +- Per-app widgetkeuze, limiet van acht, legacy-migratie en generieke read-only Dockerstats met degraded fallback. +- Persistente widgetformaten en inhoudslagen voor app- en Unraidpanelen, refreshmomenten en dichte responsieve plaatsing. +- Standaard Unraid-hostwidget via Docker `/info` en provider-aware AdGuard/Plex/Immich/Home Assistant/*arr-presentatie met server-only credentials. +- Gemaskeerde providercredentialinvoer via Settings, allowlist per provider, HTTP(S)-URL-validatie, same-origin write, atomische `0600`-environmentpersistentie en directe hertest zonder secret echo. +- Widget Studio capabilitycatalogus met aanbevolen/alle/actieve filters, acht-panelenlimiet, directe sync en vervangbare connected credentials. +- Volledige 80/80 capabilitycatalogus met app-specifieke metricdefinities, per-metric zichtbaarheid/volgorde en een begrensd custom GET-only JSON-contract. +- Persistente Tower-naam/URL met poort `5000` als standaard en dezelfde actuele waarde in kaart en zoekindex. +- Zero-config Ollama-/Glances-API's, Tautulli-/Paperless-adapters en app-specifieke AI/document/security-Dockerfallbacks. +- Per-servicetype fallbackpresentatie met berekende live netwerkdelta's; JDownloader gebruikt download-/uploadsnelheid en AdGuard DNS-traffic in plaats van generieke cumulatieve netwerkbytes. +- Zijpaneel met klok, deck health, favorieten en snelle servicelinks; gebalanceerde tegelbreedtes op 2560 en 3440 pixels. +- Statuspolling, offline container en integratie-degradatie. +- SQLite-migratie, restartpersistentie, container-ID-wijziging en export/import versie 1. +- Geen Docker-mutatie-endpoint of Docker-beheeractie. + +## Open external checks + +- Live Nginx Proxy Manager: credentials ontbreken; adapter en degradatie zijn wel gemockt/getest. +- Optionele providercredentials: alleen nodig voor de diepste API-metrics van beveiligde diensten; Dockerfallbacks blijven beschikbaar. + +## Latest quality gate + +De volledige lokale Canon v1-gate is groen: `format:check`, lint, typecheck, 42/42 unit- en integratietests, 5/5 afzonderlijke securitytests, 7 uitgevoerde en 9 doelbewust project-gededupliceerde Playwright-checks op desktop/tablet/mobile/ultrawide, production build en 0 kwetsbaarheden. De browserflows eindigen met 0 `console.error`- of ongehanteelde paginafouten. De clientbuild is 312,99 kB JS (93,53 kB gzip) en 94,91 kB CSS (17,51 kB gzip); de serverbundle is 163,9 kB. In-app Chromium verifieerde daarnaast eerste-use, gevuld dashboard, Quick Launch, apps-instellingen, mobiele drawer en dark/light op 390×844 en 1440×900. De vaste testpoortblokkade op 3000 is opgelost met geïsoleerde poorten 3101/5174 en een schone Canon-testdatabase. Docker Compose en imagebuild zijn niet opnieuw uitgevoerd omdat in deze Windows-omgeving geen `docker`-CLI/runtime beschikbaar is; de eerdere Tower-verificatie blijft ongewijzigd. + +## Canon v1 gate — 2026-07-21 + +| Check | Resultaat | +| -------------------------------------------- | ------------------------------------------------------------------------------------------------------- | +| `npm run format:check` | Pass, alle bestanden Prettier-conform | +| `npm run lint` | Pass, 0 waarschuwingen | +| `npm run typecheck` | Pass | +| `npm test` | Pass, 42/42 | +| `npm run test:security` | Pass, 5/5 | +| `npm run test:e2e` | Pass, 7 uitgevoerd / 9 gededupliceerd over vier viewports; 0 console-/paginafouten | +| `npm run build` | Pass, JS 312,99 kB / 93,53 kB gzip; CSS 94,91 kB / 17,51 kB gzip; server 163,9 kB | +| `npm audit --audit-level=moderate` | Pass, 0 kwetsbaarheden | +| In-app browserreview | Pass: 390×844 en 1440×900, dark/light, dashboard, Quick Launch, apps, drawer, geen horizontale overflow | +| Secret-/mutatiescan | Pass; geen toegevoegde secrets of Docker-mutaties | +| `docker compose config --quiet` / imagebuild | Niet uitgevoerd: `docker` is lokaal niet geïnstalleerd | + +## Directe Stitch-pass — 2026-07-21 + +| Check | Resultaat | +| ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | +| `npm run format:check` | Pass, alle bestanden Prettier-conform | +| `npm run lint` | Pass, 0 waarschuwingen | +| `npm run typecheck` | Pass | +| `npm test` | Pass, 42/42 | +| `npm run test:security` | Pass, 5/5 | +| `npm run test:e2e` | Pass, 11 uitgevoerd / 9 project-gededupliceerd; 390×844, 768×1024, 1440×900 en 3440×1440; 0 console-/paginafouten | +| `npm run build` | Pass, JS 312,49 kB / 93,60 kB gzip; CSS 125,83 kB / 22,07 kB gzip; server 163,9 kB | +| `npm audit --audit-level=moderate` | Pass, 0 kwetsbaarheden | +| In-app browserreview | Pass: gevuld donker dashboard, Quick Launch, App Beheer, editordrawer en Weergave; muis/toetsenbord en focus bevestigd | +| React-review | Pass: afgeleide state, effect-cleanup, semantische links/buttons, stabiele sleutels en toegankelijke switches | + +De CSS-toename is de expliciete directe Canon-compositielaag. Zij vervangt geen runtime- of backendlogica en bevat geen externe assets. De finale E2E-test schrijft de actuele referentiebeelden naar `docs/design/dockdeck-canon-v1/implementation/`. + +## Stitch launcherrail, iconen en widgets — 2026-07-22 + +| Check | Resultaat | +| ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | +| `npm run format:check` | Pass, alle bestanden Prettier-conform | +| `npm run lint` | Pass, 0 waarschuwingen | +| `npm run typecheck` | Pass | +| `npm test` | Pass, 42/42 | +| `npm run test:security` | Pass, 5/5 | +| `npm run test:e2e` | Pass, 11 uitgevoerd / 9 project-gededupliceerd op 390×844, 768×1024, 1440×900 en 3440×1440; 0 console-/paginafouten | +| `npm run build` | Pass, JS 353,39 kB / 110,42 kB gzip; CSS 135,18 kB / 23,34 kB gzip; server 163,9 kB | +| `npm audit --audit-level=moderate` | Pass na transitieve `fast-uri`-lockfile-update, 0 kwetsbaarheden | +| In-app browserreview | Pass: desktopfavorietenrail, echte Google/YouTube/GitHub/Plex/Immich/Home Assistant-iconen, zichtbare widgets en mobiele volgorde | +| React-review | Pass: zelfstandig railcomponent, effect-cleanup, afgeleide state, semantische links/buttons en getypeerde instellingenroute | +| Docker Compose/imagebuild | Niet uitgevoerd: in de lokale Windows-omgeving is geen `docker`-CLI/runtime beschikbaar | + +De browsertest valideert aanvullend dat de desktoprail en servicepanelen zichtbaar zijn, dat appkaarten geen `.app-monogram` meer renderen en dat de Paneelstudio via de rail rechtstreeks in `Instellingen → Algemeen` opent. De nieuwe merkiconen zijn lokaal gebundeld; expliciete Unraid-/gebruikersiconen blijven de eerste bron. + +## Widescreen leesbaarheids- en categorieaudit — 2026-07-22 + +| Check | Resultaat | +| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| `npm run format:check` | Pass, alle bestanden Prettier-conform | +| `npm run lint` | Pass, 0 waarschuwingen | +| `npm run typecheck` | Pass | +| `npm test` | Pass, 51/51; inclusief categorie-inferentie, behoud van onbekende toewijzingen en de expliciete organisatie-API | +| `npm run test:security` | Pass, 5/5 | +| `npm run test:e2e` | Pass, 12 uitgevoerd / 12 bewust overgeslagen over desktop, tablet, mobiel en ultrawide; 0 testfouten | +| `npm run build` | Pass, JS 365,35 kB / 113,90 kB gzip; CSS 173,48 kB / 29,03 kB gzip; server 170,8 kB | +| `npm audit --audit-level=moderate` | Pass, 0 kwetsbaarheden | +| React-review | Pass: afgeleide categorieanalyse, expliciete semantische actie, stabiele sleutels, geen extra effecten en ongewijzigde cleanup | +| Chrome audit vóór correctie | 3440×1215, 0 horizontale overflow en 0 consolefouten; wel widgetmetadata van 8 px, Paneelstudio tot 6 px en foutieve bulkcategorieën | + +De pass vergroot de ultrawide widgetdock, maakt widget- en instellingenmicrotypografie leesbaar en begrenst extreem lange beheer-, integratie- en back-uprijen. De categorieactie gebruikt uitsluitend bestaande categorieën en laat onbekende services onaangeroerd. + +### Live verificatie + +| Check | Resultaat | +| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | +| Secretvrij herstelpunt | `.codex-input/dockdeck-before-widescreen-audit-20260722-140834.json`, 80.809 bytes | +| `.env`-behoud | SHA-256 voor en na bronvervanging identiek: `e41e1b63…` | +| `docker compose config` | Pass vóór en na bronvervanging | +| Imagebuild en health | Pass, image `cc3a4b56fdfb…`, container healthy, 0 restarts | +| Read-only heartbeat | Pass, exact 90 bestaande containers en 50 zichtbare apps | +| Slimme categorieorganisatie | Pass, 14 gecorrigeerd, 76 behouden, 0 onbekend; 16 bestaande categorieën | +| Chrome 3440×1215 | Pass, dock 540 px, 0 horizontale overflow, 0 kapotte images, geen applicatieconsolefouten | +| Interacties | Pass: Downloads-filter toont exact Deluge/JDownloader2/slskd; instellingenzoeker, categoriebeheer en Quick Launch-favicon werken live | + +De enige browserlogregel kwam van de WideFrog Chrome-extensie zelf (`keyflow timeout` op een titelupdate), niet van DockDeck. De live DockDeck-tab is als testdashboard open gebleven. + +## Volledige instellingen- en releasepass — 2026-07-22 + +| Check | Resultaat | +| ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | +| `npm run format:check` | Pass, alle bestanden Prettier-conform | +| `npm run lint` | Pass, 0 waarschuwingen | +| `npm run typecheck` | Pass | +| `npm test` | Pass, 42/42; inclusief legacy-migratie en export/import van `reducedMotion` en `pollingIntervalMs` | +| `npm run test:security` | Pass, 5/5 | +| `npm run test:e2e` | Pass, 11 uitgevoerd / 9 project-gededupliceerd op vier viewports; instellingenzoeker en voorkeurpersistentie; 0 browserfouten | +| `npm run build` | Pass, JS 357,89 kB / 111,86 kB gzip; CSS 139,39 kB / 24,06 kB gzip; server 164,2 kB | +| `npm audit --audit-level=moderate` | Pass, 0 kwetsbaarheden | +| In-app browserreview | Pass: Apps, Categorieën, Weergave en Back-up; zoekfilter, kaartlayout, dropzone, scrollherstel en 0 horizontale overflow op 1280 | +| React-review | Pass: afgeleide zoekstate, correcte hookafhankelijkheden, getypeerde drag-events en semantische buttons/switches | + +De instellingenmatrix dekt iedere persistente voorkeur, app-/favoriet-/categoriebeheer, widgets, integratiecredentials en back-up/export. Fictieve online- of back-uphistoriek is bewust niet toegevoegd; de UI toont alleen werkelijk beschikbare gegevens. + +### Live Unraid-release + +| Check | Resultaat | +| --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | +| Pre-deploybehoud | Secretvrije export gemaakt; `.env`-SHA-256 voor/na bronkopie identiek; named volume `dockdeck_dockdeck_data` behouden | +| `docker compose config` | Pass vóór en na bronvervanging | +| Klassieke Docker-imagebuild | Pass; image `14497e8a384c…`, client-/serverbundels identiek aan lokale build | +| Compose release | Pass; commit `3ffd39b`, één `DockDeck`-container, healthy, 0 restarts op `192.168.10.150:1218` | +| Persistentie na migratie | Pass; 55 zichtbare apps, 1 favoriet, 3 categorieën, 6 appwidgets en dark/30 s behouden; sync voegde 3 nieuwe records standaard verborgen toe | +| Read-only integraties | Pass; 233 templates, 3 array-/poolgroepen, 90 AppOps-containers GET-only en AdGuard connected; NPM bewust disabled | +| Live in-app browser | Pass; favorietenrail, echte iconen, 7 panelen, instellingenzoeker/controls, scrolltop 0 en geen horizontale overflow | + +De enige foutregel in de eerste vijf minuten was een door de releasecheck zelf veroorzaakte lege POST zonder JSON-contenttype (verwachte HTTP 415); de onmiddellijk herhaalde geldige synchronisatie gaf HTTP 200 en alle daaropvolgende health-/dashboardchecks zijn groen. + +## Stitch consistentie- en ultrawidepass - 2026-07-22 + +| Check | Resultaat | +| ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | +| `npm run format:check` | Pass, alle bestanden Prettier-conform | +| `npm run lint` | Pass, 0 waarschuwingen | +| `npm run typecheck` | Pass | +| `npm test` | Pass, 42/42 | +| `npm run test:security` | Pass, 5/5 | +| `npm run test:e2e` | Pass, 11 uitgevoerd / 9 project-gededupliceerd op desktop/tablet/mobile/ultrawide; 0 browserfouten | +| `npm run build` | Pass, JS 363,25 kB / 113,46 kB gzip; CSS 155,55 kB / 26,33 kB gzip; server 164,6 kB | +| `npm audit --audit-level=moderate` | Pass, 0 kwetsbaarheden | +| React-review | Pass: lokale state voor widgetdock, media-query cleanup, afgeleide Quick Launch-snelkoppelingen en semantische linkcontrols | +| Secret-/mutatiescan | Pass; geen toegevoegde secrets, externe UI-assets of Docker-mutaties | + +Deze pass maakt de platformlayout consistent tussen dashboard en instellingen, vergroot de instellingen-typografie, gebruikt ultrawide-detectie om widgets als rechterdock te tonen, laat Quick Launch-favorieten met echte iconen renderen en vervangt de lege Quick Launch-uitleg door functionele snelkoppelingskaarten. Loopback-iconen uit discovery worden op een LAN-host naar de actuele DockDeck-host herschreven, zodat appiconen zoals Widefrog niet meer naar client-localhost wijzen. De E2E-suite valideert daarnaast dat de ultrawide-widgetdock automatisch open staat en rechts van de dashboardgroepen ligt zonder horizontale overflow. + +## Edge-anchored Stitch shell en rijkere Dockerkaarten - 2026-07-22 + +| Check | Resultaat | +| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | +| `npm run format:check` | Pass, alle bestanden Prettier-conform | +| `npm run lint` | Pass, 0 waarschuwingen | +| `npm run typecheck` | Pass | +| `npm test` | Pass, 42/42 | +| `npm run test:security` | Pass, 5/5 | +| `npm run test:e2e` | Pass, 11 uitgevoerd / 9 project-gededupliceerd op desktop/tablet/mobile/ultrawide | +| `npm run build` | Pass, JS 363,25 kB / 113,46 kB gzip; CSS 162,12 kB / 27,27 kB gzip; server 164,6 kB | +| `npm audit --audit-level=moderate` | Pass, 0 kwetsbaarheden | +| Chrome visuele audit | Pass: dashboardrail `left=0`, settings-sidebar `left=0`, ultrawide widgetdock `right=3440`, 0 horizontale overflow op 1920 en 3440 px | + +Deze pass verwijdert de zwevende dashboard-shell op desktop: de linkerfavorietenrail staat tegen de viewportzijde en gebruikt dezelfde oppervlakte, padding en typografische taal als de instellingenrail. Op ultrawide staat het widgetpaneel als rechterdock tegen de rechter viewportzijde. Docker-snelkoppelingen zijn visueel zwaardere Stitch-servicekaarten met grotere iconen, statuschips, accentrail, arrow affordance, subtiel verloop en diepere hoverrespons. + +### Live categorie-indeling - 2026-07-22 + +Voor de live DockDeck-config op `http://192.168.10.150:1218` is vooraf een secretvrije export gemaakt naar `.codex-input/dockdeck-categories-before-20260722-052942.json`. Daarna zijn 139 apps via de DockDeck-config-API over 16 categorieën verdeeld; verificatie: 0 apps zonder categorie. Categorieaantallen: Media 17, Downloads 2, Books & Documents 7, Photos 14, Home & Automation 2, Security & Identity 4, Network & Admin 4, Monitoring 7, Development & Projects 45, AI & ML 3, Databases & Storage 12, Infrastructure 16, Games & Emulation 5, Tools & Productivity 1, Experiments & Archived 0, Favorites 0. + +Live codevervanging is daarna alsnog uitgevoerd via de bestaande SSH-alias `unraid-widefrog` (`root`, sleutel `widefrog_unraid_deploy`). De deploymentmap is een uitgepakte source-copy en geen git-worktree; daarom is commit `41942d5` via een checksum-gecontroleerd `git archive` naar Tower gekopieerd en over de source uitgepakt. `.env` bleef byte-identiek (`e41e1b63...` voor en na), `docker compose config --quiet` slaagde, `docker compose up -d --build` bouwde image `af96b8ac7a04...` en de `DockDeck`-container staat healthy met 0 restarts op `192.168.10.150:1218`. Na een geldige read-only sync zijn Unraid templates, Unraid runtime, AppOps inventory en AdGuard Home widget connected; dashboardverificatie: 139 apps, 55 zichtbaar, 16 categorieën, 0 zonder categorie. diff --git a/backend/docker_start.sh b/backend/docker_start.sh old mode 100755 new mode 100644 diff --git a/deploy/dockdeck-unraid-template.xml b/deploy/dockdeck-unraid-template.xml new file mode 100644 index 00000000..a8e27d16 --- /dev/null +++ b/deploy/dockdeck-unraid-template.xml @@ -0,0 +1,31 @@ + + + DockDeck + dockdeck:local + bridge + sh + false + http://192.168.10.150:3000/NuklearRabbit/DockDeck + http://192.168.10.150:3000/NuklearRabbit/DockDeck + DockDeck is a premium, read-only personal start dashboard for Unraid. This single-container profile uses the existing AppOps inventory API and never mounts the Docker socket. + Tools:Status: + http://[IP]:[PORT:1218] + http://192.168.10.150:1218/dockdeck-icon.png + --read-only --security-opt=no-new-privileges:true + + + 1784060000 + + + Unraid AppOps Gateway on port 1216 + 1218 + /mnt/user/appdata/dockdeck/data + /mnt/user/appdata/dockdeck/unraid-templates + /var/local/emhttp + /sys/class/hwmon + /proc/cpuinfo + /proc/meminfo + http://192.168.10.150:1216 + 192.168.10.150 + http://192.168.10.150:5000 + diff --git a/deploy/refresh-unraid-icon.sh b/deploy/refresh-unraid-icon.sh new file mode 100755 index 00000000..8931d101 --- /dev/null +++ b/deploy/refresh-unraid-icon.sh @@ -0,0 +1,48 @@ +#!/bin/sh + +set -eu + +container_name="${1:-DockDeck}" +icon_url="${2:-}" +template_path="/boot/config/plugins/dockerMan/templates-user/my-${container_name}.xml" +persistent_dir="/var/lib/docker/unraid/images" +runtime_dir="/usr/local/emhttp/state/plugins/dynamix.docker.manager/images" + +fail() { + printf 'DockDeck icon refresh failed: %s\n' "$1" >&2 + exit 1 +} + +[ "$(id -u)" -eq 0 ] || fail "run this helper as root on the Unraid host" + +case "$container_name" in + "" | *[!A-Za-z0-9_.-]*) fail "invalid container name" ;; +esac + +if [ -z "$icon_url" ] && [ -f "$template_path" ]; then + icon_url="$(sed -n 's#.*\(.*\).*#\1#p' "$template_path" | head -n 1)" +fi + +case "$icon_url" in + http://* | https://*) ;; + *) fail "provide an HTTP(S) icon URL or a user template containing " ;; +esac + +temporary_icon="$(mktemp /tmp/dockdeck-icon.XXXXXX)" +trap 'rm -f "$temporary_icon"' EXIT HUP INT TERM + +curl --fail --silent --show-error --location \ + --connect-timeout 5 --max-time 20 \ + "$icon_url" --output "$temporary_icon" + +[ "$(file -b --mime-type "$temporary_icon")" = "image/png" ] || \ + fail "the downloaded asset is not a real PNG" + +mkdir -p "$persistent_dir" "$runtime_dir" +install -m 0644 -o root -g root \ + "$temporary_icon" "$persistent_dir/${container_name}-icon.png" +install -m 0644 -o root -g root \ + "$temporary_icon" "$runtime_dir/${container_name}-icon.png" + +printf 'Refreshed both Unraid icon caches for %s from %s\n' \ + "$container_name" "$icon_url" diff --git a/deploy/sanitize-unraid-templates.py b/deploy/sanitize-unraid-templates.py new file mode 100644 index 00000000..d21a9177 --- /dev/null +++ b/deploy/sanitize-unraid-templates.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +"""Copy only non-sensitive DockDeck metadata from Unraid templates.""" + +from __future__ import annotations + +import argparse +from pathlib import Path +import xml.etree.ElementTree as ET + + +ALLOWED_FIELDS = ("Name", "WebUI", "Icon") + + +def sanitize(source_dir: Path, destination_dir: Path) -> int: + destination_dir.mkdir(parents=True, exist_ok=True) + written = 0 + + for source in sorted(source_dir.glob("*.xml")): + try: + root = ET.parse(source).getroot() + except (ET.ParseError, OSError): + continue + + output = ET.Element("Container") + for field in ALLOWED_FIELDS: + value = root.findtext(field) + if value: + ET.SubElement(output, field).text = value.strip() + + if output.findtext("Name"): + destination = destination_dir / source.name + ET.ElementTree(output).write( + destination, + encoding="utf-8", + xml_declaration=True, + ) + destination.chmod(0o644) + written_tags = { + child.tag for child in ET.parse(destination).getroot() + } + if not written_tags.issubset(ALLOWED_FIELDS): + destination.unlink(missing_ok=True) + raise RuntimeError( + f"Unexpected field in sanitized template: {source.name}" + ) + written += 1 + + return written + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("source", type=Path) + parser.add_argument("destination", type=Path) + args = parser.parse_args() + count = sanitize(args.source, args.destination) + print(f"Sanitized and verified {count} Unraid templates") + + +if __name__ == "__main__": + main() diff --git a/deploy/unraid.env.example b/deploy/unraid.env.example new file mode 100644 index 00000000..54eed2a6 --- /dev/null +++ b/deploy/unraid.env.example @@ -0,0 +1,9 @@ +DOCKDECK_BIND_ADDRESS=192.168.10.150 +DOCKDECK_PORT=1218 +DOCKDECK_EGRESS_SUBNET=172.31.240.16/28 +UNRAID_TEMPLATES_DIR=/mnt/user/appdata/dockdeck/unraid-templates +UNRAID_HOST=192.168.10.150 +UNRAID_URL=http://192.168.10.150:5000 +UNRAID_RUNTIME_DIR=/var/local/emhttp +UNRAID_HWMON_DIR=/sys/class/hwmon +APPOPS_URL=http://192.168.10.150:1216 diff --git a/docker-compose.socket-proxy.yml b/docker-compose.socket-proxy.yml new file mode 100644 index 00000000..359728d0 --- /dev/null +++ b/docker-compose.socket-proxy.yml @@ -0,0 +1,56 @@ +services: + dockdeck: + depends_on: + dockerproxy: + condition: service_started + environment: + APPOPS_URL: "" + DOCKER_PROXY_URL: http://dockerproxy:2375 + networks: + - dockdeck_internal + + dockerproxy: + image: ghcr.io/tecnativa/docker-socket-proxy:latest + restart: unless-stopped + environment: + LOG_LEVEL: warning + CONTAINERS: 1 + INFO: 1 + PING: 1 + VERSION: 1 + POST: 0 + AUTH: 0 + BUILD: 0 + COMMIT: 0 + CONFIGS: 0 + DISTRIBUTION: 0 + EVENTS: 0 + EXEC: 0 + IMAGES: 0 + NETWORKS: 0 + NODES: 0 + PLUGINS: 0 + SECRETS: 0 + SERVICES: 0 + SESSION: 0 + SWARM: 0 + SYSTEM: 0 + TASKS: 0 + VOLUMES: 0 + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + networks: + - dockdeck_internal + read_only: true + tmpfs: + - /run + - /tmp + security_opt: + - no-new-privileges:true + +networks: + dockdeck_internal: + internal: true + ipam: + config: + - subnet: ${DOCKDECK_INTERNAL_SUBNET:-172.31.240.0/28} diff --git a/docker-compose.yml b/docker-compose.yml index 39b4507d..440bcb2f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,183 +1,119 @@ services: - db: - image: postgis/postgis:16-3.4 - environment: - POSTGRES_DB: ${GEOINTEL_POSTGRES_DB:-geointel} - POSTGRES_USER: ${GEOINTEL_POSTGRES_USER:-geointel} - POSTGRES_PASSWORD: ${GEOINTEL_POSTGRES_PASSWORD:-geointel} - volumes: - - geointel_postgis:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${GEOINTEL_POSTGRES_USER:-geointel} -d ${GEOINTEL_POSTGRES_DB:-geointel}"] - interval: 5s - timeout: 5s - retries: 10 - - backend: + dockdeck: + container_name: DockDeck build: - context: ./backend - args: - GEOINTEL_INSTALL_AI: ${GEOINTEL_INSTALL_AI:-false} + context: . + image: dockdeck:local + restart: unless-stopped + labels: + org.opencontainers.image.title: DockDeck + org.opencontainers.image.description: Premium read-only Unraid start dashboard + org.opencontainers.image.source: http://192.168.10.150:3000/NuklearRabbit/DockDeck + net.unraid.docker.managed: dockerman + net.unraid.docker.webui: "http://${UNRAID_HOST:-tower.local}:${DOCKDECK_PORT:-1218}" + net.unraid.docker.icon: "http://${UNRAID_HOST:-tower.local}:${DOCKDECK_PORT:-1218}/dockdeck-icon.png" environment: - DATABASE_URL: postgresql+psycopg://${GEOINTEL_POSTGRES_USER:-geointel}:${GEOINTEL_POSTGRES_PASSWORD:-geointel}@db:5432/${GEOINTEL_POSTGRES_DB:-geointel} - STORAGE_ROOT: /app/storage - CORS_ORIGINS: ${GEOINTEL_CORS_ORIGINS:-http://localhost:1202,http://127.0.0.1:1202} - MAX_UPLOAD_MB: ${GEOINTEL_MAX_UPLOAD_MB:-500} - ORTHOPHOTO_ENABLED: ${ORTHOPHOTO_ENABLED:-true} - ORTHOPHOTO_WMS_URL: ${ORTHOPHOTO_WMS_URL:-https://geo.api.vlaanderen.be/OMWRGBMRVL/wms} - ORTHOPHOTO_WMS_LAYER: ${ORTHOPHOTO_WMS_LAYER:-Ortho} - ORTHOPHOTO_RESOLUTION_M: ${ORTHOPHOTO_RESOLUTION_M:-1.0} - ORTHOPHOTO_MIN_SIDE_M: ${ORTHOPHOTO_MIN_SIDE_M:-128} - ORTHOPHOTO_MAX_SIDE_M: ${ORTHOPHOTO_MAX_SIDE_M:-1024} - ORTHOPHOTO_CACHE_TTL_HOURS: ${ORTHOPHOTO_CACHE_TTL_HOURS:-24} - SOURCE_CATALOG_PROBE_ENABLED: ${SOURCE_CATALOG_PROBE_ENABLED:-true} - SOURCE_CATALOG_GRB_WFS_URL: ${SOURCE_CATALOG_GRB_WFS_URL:-https://geo.api.vlaanderen.be/GRB/wfs} - GRB_ENABLED: ${GRB_ENABLED:-true} - GRB_OGC_API_URL: ${GRB_OGC_API_URL:-https://geo.api.vlaanderen.be/GRB/ogc/features/v1} - GRB_MIN_SIDE_M: ${GRB_MIN_SIDE_M:-10} - GRB_MAX_SIDE_M: ${GRB_MAX_SIDE_M:-20000} - GRB_PAGE_SIZE: ${GRB_PAGE_SIZE:-1000} - GRB_MAX_PAGES: ${GRB_MAX_PAGES:-200} - GRB_MAX_FEATURES: ${GRB_MAX_FEATURES:-150000} - GRB_TIMEOUT_SECONDS: ${GRB_TIMEOUT_SECONDS:-180} - GRB_MAX_RESPONSE_MB: ${GRB_MAX_RESPONSE_MB:-20} - GRB_MAX_TOTAL_RESPONSE_MB: ${GRB_MAX_TOTAL_RESPONSE_MB:-256} - GRB_CACHE_TTL_HOURS: ${GRB_CACHE_TTL_HOURS:-24} - OFFICIAL_VECTOR_ENABLED: ${OFFICIAL_VECTOR_ENABLED:-true} - BWK_WFS_URL: ${BWK_WFS_URL:-https://geo.api.vlaanderen.be/BWK/wfs} - DOV_SOIL_WFS_URL: ${DOV_SOIL_WFS_URL:-https://www.dov.vlaanderen.be/geoserver/wfs} - SPW_PICC_ENABLED: ${SPW_PICC_ENABLED:-true} - SPW_PICC_MAPSERVER_URL: ${SPW_PICC_MAPSERVER_URL:-https://geoservices.wallonie.be/arcgis/rest/services/TOPOGRAPHIE/PICC_VDIFF/MapServer} - SPW_FLOOD_HAZARD_ENABLED: ${SPW_FLOOD_HAZARD_ENABLED:-true} - SPW_FLOOD_HAZARD_MAPSERVER_URL: ${SPW_FLOOD_HAZARD_MAPSERVER_URL:-https://geoservices.wallonie.be/arcgis/rest/services/EAU/ALEA_INOND/MapServer} - URBIS_ENABLED: ${URBIS_ENABLED:-true} - URBIS_WFS_URL: ${URBIS_WFS_URL:-https://geoservices-vector.irisnet.be/geoserver/urbisvector/ows} - OFFICIAL_VECTOR_MIN_SIDE_M: ${OFFICIAL_VECTOR_MIN_SIDE_M:-10} - OFFICIAL_VECTOR_MAX_SIDE_M: ${OFFICIAL_VECTOR_MAX_SIDE_M:-20000} - OFFICIAL_VECTOR_PAGE_SIZE: ${OFFICIAL_VECTOR_PAGE_SIZE:-1000} - OFFICIAL_VECTOR_MAX_PAGES: ${OFFICIAL_VECTOR_MAX_PAGES:-200} - OFFICIAL_VECTOR_MAX_FEATURES: ${OFFICIAL_VECTOR_MAX_FEATURES:-100000} - OFFICIAL_VECTOR_TIMEOUT_SECONDS: ${OFFICIAL_VECTOR_TIMEOUT_SECONDS:-180} - OFFICIAL_VECTOR_MAX_RESPONSE_MB: ${OFFICIAL_VECTOR_MAX_RESPONSE_MB:-20} - OFFICIAL_VECTOR_MAX_TOTAL_RESPONSE_MB: ${OFFICIAL_VECTOR_MAX_TOTAL_RESPONSE_MB:-256} - OFFICIAL_VECTOR_CACHE_TTL_HOURS: ${OFFICIAL_VECTOR_CACHE_TTL_HOURS:-24} - SOURCE_CATALOG_STATBEL_DCAT_URL: ${SOURCE_CATALOG_STATBEL_DCAT_URL:-https://doc.statbel.be/publications/DCAT/DCAT_opendata_datasets.ttl} - SOURCE_CATALOG_STATBEL_MAX_RESPONSE_MB: ${SOURCE_CATALOG_STATBEL_MAX_RESPONSE_MB:-5} - SOURCE_CATALOG_ALZ_RELEASE_URL: ${SOURCE_CATALOG_ALZ_RELEASE_URL:-https://landbouwcijfers.vlaanderen.be/open-geodata-landbouwgebruikspercelen} - SOURCE_CATALOG_PROBE_TIMEOUT_SECONDS: ${SOURCE_CATALOG_PROBE_TIMEOUT_SECONDS:-10} - SOURCE_CATALOG_PROBE_MAX_RESPONSE_MB: ${SOURCE_CATALOG_PROBE_MAX_RESPONSE_MB:-2} - SOURCE_CATALOG_PROBE_CACHE_TTL_SECONDS: ${SOURCE_CATALOG_PROBE_CACHE_TTL_SECONDS:-900} - DHMV_ENABLED: ${DHMV_ENABLED:-true} - DHMV_WCS_URL: ${DHMV_WCS_URL:-https://geo.api.vlaanderen.be/DHMV/wcs} - DHMV_RESOLUTION_M: ${DHMV_RESOLUTION_M:-5.0} - DHMV_MIN_SIDE_M: ${DHMV_MIN_SIDE_M:-10} - DHMV_MAX_SIDE_M: ${DHMV_MAX_SIDE_M:-20000} - DHMV_MAX_PIXELS: ${DHMV_MAX_PIXELS:-12000000} - DHMV_TIMEOUT_SECONDS: ${DHMV_TIMEOUT_SECONDS:-300} - DHMV_MAX_RESPONSE_MB: ${DHMV_MAX_RESPONSE_MB:-160} - FLOOD_HAZARD_ENABLED: ${FLOOD_HAZARD_ENABLED:-true} - FLOOD_HAZARD_WCS_URL: ${FLOOD_HAZARD_WCS_URL:-https://geoservice.waterinfo.be/OGRK/wcs} - FLOOD_HAZARD_RESOLUTION_M: ${FLOOD_HAZARD_RESOLUTION_M:-5.0} - FLOOD_HAZARD_MIN_SIDE_M: ${FLOOD_HAZARD_MIN_SIDE_M:-10} - FLOOD_HAZARD_MAX_SIDE_M: ${FLOOD_HAZARD_MAX_SIDE_M:-20000} - FLOOD_HAZARD_MAX_PIXELS: ${FLOOD_HAZARD_MAX_PIXELS:-12000000} - FLOOD_HAZARD_TIMEOUT_SECONDS: ${FLOOD_HAZARD_TIMEOUT_SECONDS:-300} - FLOOD_HAZARD_MAX_RESPONSE_MB: ${FLOOD_HAZARD_MAX_RESPONSE_MB:-160} - BATHYMETRY_PROFILES_ENABLED: ${BATHYMETRY_PROFILES_ENABLED:-true} - BATHYMETRY_PROFILES_LAYER_URL: ${BATHYMETRY_PROFILES_LAYER_URL:-https://vha.waterinfo.be/arcgis/rest/services/digitale_atlas/MapServer/0} - BATHYMETRY_WATERCOURSE_LAYER_URL: ${BATHYMETRY_WATERCOURSE_LAYER_URL:-https://vha.waterinfo.be/arcgis/rest/services/digitale_atlas/MapServer/1} - BATHYMETRY_PROFILES_PAGE_SIZE: ${BATHYMETRY_PROFILES_PAGE_SIZE:-1000} - BATHYMETRY_PROFILES_MAX_FEATURES: ${BATHYMETRY_PROFILES_MAX_FEATURES:-50000} - BATHYMETRY_PROFILES_TIMEOUT_SECONDS: ${BATHYMETRY_PROFILES_TIMEOUT_SECONDS:-120} - BATHYMETRY_PROFILES_MAX_RESPONSE_MB: ${BATHYMETRY_PROFILES_MAX_RESPONSE_MB:-32} - MDK_BATHYMETRY_PROBE_ENABLED: ${MDK_BATHYMETRY_PROBE_ENABLED:-true} - MDK_BATHYMETRY_WCS_URL: ${MDK_BATHYMETRY_WCS_URL:-https://bathy.agentschapmdk.be/spatialfusionserver/services/ows/wcs/EL_wcs} - MDK_BATHYMETRY_PROBE_TIMEOUT_SECONDS: ${MDK_BATHYMETRY_PROBE_TIMEOUT_SECONDS:-20} - MDK_BATHYMETRY_PROBE_MAX_RESPONSE_MB: ${MDK_BATHYMETRY_PROBE_MAX_RESPONSE_MB:-4} - MDK_BATHYMETRY_ACQUISITION_ENABLED: ${MDK_BATHYMETRY_ACQUISITION_ENABLED:-false} - MDK_BATHYMETRY_COVERAGE_ID: ${MDK_BATHYMETRY_COVERAGE_ID:-} - MDK_BATHYMETRY_REQUEST_CRS: ${MDK_BATHYMETRY_REQUEST_CRS:-EPSG:4326} - MDK_BATHYMETRY_MAX_BBOX_DEG2: ${MDK_BATHYMETRY_MAX_BBOX_DEG2:-0.25} - MDK_BATHYMETRY_ACQUISITION_TIMEOUT_SECONDS: ${MDK_BATHYMETRY_ACQUISITION_TIMEOUT_SECONDS:-120} - MDK_BATHYMETRY_ACQUISITION_MAX_RESPONSE_MB: ${MDK_BATHYMETRY_ACQUISITION_MAX_RESPONSE_MB:-160} - THEMATIC_RASTER_ENABLED: ${THEMATIC_RASTER_ENABLED:-true} - THEMATIC_RASTER_WCS_URL: ${THEMATIC_RASTER_WCS_URL:-https://www.mercator.vlaanderen.be/raadpleegdienstenmercatorpubliek/wcs} - THEMATIC_RASTER_MIN_SIDE_M: ${THEMATIC_RASTER_MIN_SIDE_M:-100} - THEMATIC_RASTER_MAX_SIDE_M: ${THEMATIC_RASTER_MAX_SIDE_M:-60000} - THEMATIC_RASTER_MAX_PIXELS: ${THEMATIC_RASTER_MAX_PIXELS:-30000000} - THEMATIC_RASTER_TIMEOUT_SECONDS: ${THEMATIC_RASTER_TIMEOUT_SECONDS:-300} - THEMATIC_RASTER_MAX_RESPONSE_MB: ${THEMATIC_RASTER_MAX_RESPONSE_MB:-160} - WALOUS_ENABLED: ${WALOUS_ENABLED:-true} - WALOUS_SOURCE_DIR: ${WALOUS_SOURCE_DIR:-/app/storage/source-cache/walous} - WALOUS_ANALYSIS_RESOLUTION_M: ${WALOUS_ANALYSIS_RESOLUTION_M:-10} - WALOUS_MAX_SIDE_M: ${WALOUS_MAX_SIDE_M:-60000} - WALOUS_MAX_PIXELS: ${WALOUS_MAX_PIXELS:-36000000} - YOLO_ENABLED: ${YOLO_ENABLED:-false} - YOLO_MODELS_DIR: ${YOLO_MODELS_DIR:-/app/models} - YOLO_MODEL_PATH: ${YOLO_MODEL_PATH:-} - YOLO_MODEL_ID: ${YOLO_MODEL_ID:-yolo-configured} - YOLO_MODEL_DISPLAY_NAME: ${YOLO_MODEL_DISPLAY_NAME:-Configured YOLO detector} - YOLO_MODEL_VERSION: ${YOLO_MODEL_VERSION:-} - YOLO_CONFIG_DIR: ${YOLO_CONFIG_DIR:-/app/storage/ultralytics} - YOLO_DEVICE: ${YOLO_DEVICE:-cpu} - YOLO_IMAGE_SIZE: ${YOLO_IMAGE_SIZE:-640} - YOLO_MAX_TILES: ${YOLO_MAX_TILES:-100} - YOLO_MAX_DETECTIONS: ${YOLO_MAX_DETECTIONS:-1000} - YOLO_DUPLICATE_IOU_THRESHOLD: ${YOLO_DUPLICATE_IOU_THRESHOLD:-0.5} - YOLO_BATCH_SIZE: ${YOLO_BATCH_SIZE:-1} - YOLO_SEG_ENABLED: ${YOLO_SEG_ENABLED:-false} - YOLO_SEG_MODEL_PATH: ${YOLO_SEG_MODEL_PATH:-} - YOLO_SEG_MODEL_ID: ${YOLO_SEG_MODEL_ID:-yolo-seg-configured} - YOLO_SEG_MODEL_DISPLAY_NAME: ${YOLO_SEG_MODEL_DISPLAY_NAME:-Configured YOLO segmentation} - YOLO_SEG_MODEL_VERSION: ${YOLO_SEG_MODEL_VERSION:-} - SAM_ENABLED: ${SAM_ENABLED:-false} - SAM_MODEL_PATH: ${SAM_MODEL_PATH:-} - SAM_MODEL_ID: ${SAM_MODEL_ID:-sam-configured} - SAM_MODEL_DISPLAY_NAME: ${SAM_MODEL_DISPLAY_NAME:-Configured SAM segmentation} - SAM_MODEL_VERSION: ${SAM_MODEL_VERSION:-} - SEGMENTATION_MAX_MASKS_PER_TILE: ${SEGMENTATION_MAX_MASKS_PER_TILE:-300} - SEGMENTATION_DUPLICATE_IOU_THRESHOLD: ${SEGMENTATION_DUPLICATE_IOU_THRESHOLD:-0.5} - GEOINTEL_RECONCILE_INTERRUPTED_RUNS_ON_STARTUP: ${GEOINTEL_RECONCILE_INTERRUPTED_RUNS_ON_STARTUP:-true} - OLLAMA_ENABLED: ${OLLAMA_ENABLED:-false} - OLLAMA_BASE_URL: ${OLLAMA_BASE_URL:-http://host.docker.internal:11434} - OLLAMA_DEFAULT_MODEL: ${OLLAMA_DEFAULT_MODEL:-qwen3.5:9b} - OLLAMA_TIMEOUT_SECONDS: ${OLLAMA_TIMEOUT_SECONDS:-120} - OLLAMA_MAX_OUTPUT_TOKENS: ${OLLAMA_MAX_OUTPUT_TOKENS:-1200} - OLLAMA_CONTEXT_TOKENS: ${OLLAMA_CONTEXT_TOKENS:-16384} + HOST: 0.0.0.0 + PORT: 3000 + DATABASE_PATH: /data/dockdeck.db + INTEGRATION_ENV_PATH: /data/integrations.env + APPOPS_URL: ${APPOPS_URL:-} + DOCKER_PROXY_URL: ${DOCKER_PROXY_URL:-} + UNRAID_TEMPLATES_PATH: /unraid-templates + UNRAID_ICONS_SOURCE_PATH: /unraid-icons-source + UNRAID_ICONS_PATH: /unraid-icons-cache + UNRAID_RUNTIME_PATH: /unraid-runtime + UNRAID_HWMON_PATH: /host-hwmon + UNRAID_CPUINFO_PATH: /host-proc/cpuinfo + UNRAID_MEMINFO_PATH: /host-proc/meminfo + UNRAID_UPS_STATUS_PATH: ${UNRAID_UPS_STATUS_PATH:-} + UNRAID_HOST: ${UNRAID_HOST:-tower.local} + UNRAID_URL: ${UNRAID_URL:-} + MOCK_DISCOVERY: "false" + POLLING_INTERVAL_MS: ${POLLING_INTERVAL_MS:-30000} + NPM_URL: ${NPM_URL:-} + NPM_TOKEN: ${NPM_TOKEN:-} + NPM_USERNAME: ${NPM_USERNAME:-} + NPM_PASSWORD: ${NPM_PASSWORD:-} + ADGUARD_URL: ${ADGUARD_URL:-} + ADGUARD_USERNAME: ${ADGUARD_USERNAME:-} + ADGUARD_PASSWORD: ${ADGUARD_PASSWORD:-} + PLEX_URL: ${PLEX_URL:-} + PLEX_TOKEN: ${PLEX_TOKEN:-} + IMMICH_URL: ${IMMICH_URL:-} + IMMICH_API_KEY: ${IMMICH_API_KEY:-} + HOME_ASSISTANT_URL: ${HOME_ASSISTANT_URL:-} + HOME_ASSISTANT_TOKEN: ${HOME_ASSISTANT_TOKEN:-} + OLLAMA_URL: ${OLLAMA_URL:-} + GLANCES_URL: ${GLANCES_URL:-} + TAUTULLI_URL: ${TAUTULLI_URL:-} + TAUTULLI_API_KEY: ${TAUTULLI_API_KEY:-} + PAPERLESS_URL: ${PAPERLESS_URL:-} + PAPERLESS_TOKEN: ${PAPERLESS_TOKEN:-} + SONARR_URL: ${SONARR_URL:-} + SONARR_API_KEY: ${SONARR_API_KEY:-} + RADARR_URL: ${RADARR_URL:-} + RADARR_API_KEY: ${RADARR_API_KEY:-} + LIDARR_URL: ${LIDARR_URL:-} + LIDARR_API_KEY: ${LIDARR_API_KEY:-} + GITEA_WIDGET_URL: ${GITEA_WIDGET_URL:-} + GITEA_WIDGET_TOKEN: ${GITEA_WIDGET_TOKEN:-} + JELLYFIN_URL: ${JELLYFIN_URL:-} + JELLYFIN_API_KEY: ${JELLYFIN_API_KEY:-} + SEERR_URL: ${SEERR_URL:-} + SEERR_API_KEY: ${SEERR_API_KEY:-} + PROWLARR_URL: ${PROWLARR_URL:-} + PROWLARR_API_KEY: ${PROWLARR_API_KEY:-} + AUTHENTIK_URL: ${AUTHENTIK_URL:-} + AUTHENTIK_TOKEN: ${AUTHENTIK_TOKEN:-} + PORTAINER_URL: ${PORTAINER_URL:-} + PORTAINER_API_KEY: ${PORTAINER_API_KEY:-} + GRAFANA_URL: ${GRAFANA_URL:-} + GRAFANA_TOKEN: ${GRAFANA_TOKEN:-} + PROMETHEUS_URL: ${PROMETHEUS_URL:-} + NEXTCLOUD_URL: ${NEXTCLOUD_URL:-} + NEXTCLOUD_USERNAME: ${NEXTCLOUD_USERNAME:-} + NEXTCLOUD_APP_PASSWORD: ${NEXTCLOUD_APP_PASSWORD:-} + AUDIOBOOKSHELF_URL: ${AUDIOBOOKSHELF_URL:-} + AUDIOBOOKSHELF_TOKEN: ${AUDIOBOOKSHELF_TOKEN:-} + NETDATA_URL: ${NETDATA_URL:-} + PEERTUBE_URL: ${PEERTUBE_URL:-} + DELUGE_METRICS_URL: ${DELUGE_METRICS_URL:-} + DELUGE_METRICS_TOKEN: ${DELUGE_METRICS_TOKEN:-} + QBITTORRENT_METRICS_URL: ${QBITTORRENT_METRICS_URL:-} + QBITTORRENT_METRICS_TOKEN: ${QBITTORRENT_METRICS_TOKEN:-} + JDOWNLOADER_METRICS_URL: ${JDOWNLOADER_METRICS_URL:-} + JDOWNLOADER_METRICS_TOKEN: ${JDOWNLOADER_METRICS_TOKEN:-} + TDARR_METRICS_URL: ${TDARR_METRICS_URL:-} + TDARR_METRICS_TOKEN: ${TDARR_METRICS_TOKEN:-} + BAZARR_METRICS_URL: ${BAZARR_METRICS_URL:-} + BAZARR_METRICS_TOKEN: ${BAZARR_METRICS_TOKEN:-} + VAULTWARDEN_METRICS_URL: ${VAULTWARDEN_METRICS_URL:-} + VAULTWARDEN_METRICS_TOKEN: ${VAULTWARDEN_METRICS_TOKEN:-} ports: - - "${GEOINTEL_BACKEND_PORT:-8000}:8000" + - "${DOCKDECK_BIND_ADDRESS:-127.0.0.1}:${DOCKDECK_PORT:-1218}:3000" volumes: - - ${GEOINTEL_STORAGE_PATH:-./storage}:/app/storage - - ${GEOINTEL_MODELS_PATH:-./models}:/app/models - - ${GEOINTEL_BACKUPS_PATH:-./backups}:/app/backups:ro - - ./fixtures:/app/fixtures:ro - extra_hosts: - - "host.docker.internal:host-gateway" - command: sh /app/docker_start.sh - depends_on: - db: - condition: service_healthy - healthcheck: - test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health/ready', timeout=5).read()\""] - interval: 10s - timeout: 5s - retries: 12 - start_period: 30s + - dockdeck_data:/data + - ${UNRAID_TEMPLATES_DIR:-./fixtures/unraid-templates}:/unraid-templates:ro + - ${UNRAID_ICONS_DIR:-/boot/config/plugins/dockerMan/images}:/unraid-icons-source:ro + - ${UNRAID_RUNTIME_DIR:-/var/local/emhttp}:/unraid-runtime:ro + - ${UNRAID_HWMON_DIR:-/sys/class/hwmon}:/host-hwmon:ro + - ${UNRAID_CPUINFO_FILE:-/proc/cpuinfo}:/host-proc/cpuinfo:ro + - ${UNRAID_MEMINFO_FILE:-/proc/meminfo}:/host-proc/meminfo:ro + networks: + - dockdeck_egress + read_only: true + tmpfs: + - /tmp + - /unraid-icons-cache:size=64m,mode=0755 + security_opt: + - no-new-privileges:true - frontend: - build: - context: ./frontend - ports: - - "${GEOINTEL_FRONTEND_PORT:-1202}:80" - depends_on: - backend: - condition: service_healthy - healthcheck: - test: ["CMD-SHELL", "wget -q -O - http://127.0.0.1/health/ready | grep -q '\"status\":\"ok\"'"] - interval: 10s - timeout: 5s - retries: 12 - start_period: 10s +networks: + dockdeck_egress: + ipam: + config: + - subnet: ${DOCKDECK_EGRESS_SUBNET:-172.31.240.16/28} volumes: - geointel_postgis: + dockdeck_data: diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100644 index 00000000..e6563f22 --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,17 @@ +#!/bin/sh +set -eu + +icon_source="${UNRAID_ICONS_SOURCE_PATH:-/unraid-icons-source}" +icon_cache="${UNRAID_ICONS_PATH:-/unraid-icons-cache}" + +if [ -d "$icon_source" ] && [ -d "$icon_cache" ]; then + find "$icon_source" -maxdepth 1 -type f \( \ + -iname '*.gif' -o -iname '*.ico' -o -iname '*.jpeg' -o \ + -iname '*.jpg' -o -iname '*.png' -o -iname '*.svg' -o \ + -iname '*.webp' \ + \) -exec cp -f '{}' "$icon_cache/" \; + find "$icon_cache" -maxdepth 1 -type f -exec chmod 0644 '{}' \; + chown -R dockdeck:dockdeck "$icon_cache" +fi + +exec su-exec dockdeck "$@" diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index b63a45e4..d8660e2e 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1,148 +1,45 @@ -# Architecture +# DockDeck architecture -## 1. Overzicht +## Runtime -GeoIntel bestaat uit: - -- React/TypeScript frontend -- FastAPI backend -- PostgreSQL/PostGIS database -- background job queue -- file/object storage -- GIS processing services -- AI inference services - -## 2. Hoofdcomponenten +DockDeck is one deployable Node.js process. Fastify owns the REST API and serves the compiled React client in production. React/Vite provides the development UI. SQLite is mounted at `/data`; Drizzle ORM owns typed reads and writes while a small idempotent SQL migration creates the first schema before Fastify starts. ```text -Frontend - ↓ REST/WebSocket -FastAPI Backend - ↓ -PostgreSQL + PostGIS - ↓ -Storage: uploads, processed rasters, tiles, masks, exports - ↓ -Workers: GIS processing, AI inference, QA/QC, export +Browser → Fastify API → domain rules → Drizzle → SQLite + ↘ discovery service → AppOps inventory (GET only) + → Unraid XML/runtime mounts (read only) + → NPM API (read only, optional) ``` -## 3. Frontend +## Trust boundaries -Aanbevolen stack: +- The `dockdeck` container never receives `/var/run/docker.sock`. +- The standard deployment is exactly one `DockDeck` container. A separate `dockerproxy` exists only in the optional fallback overlay; there it is private, has `POST=0` and disables all mutation families. +- Unraid templates are mounted read-only. +- NPM, Gitea and provider credentials exist only as server-side process environment. Values entered in Settings are persisted in the permission-restricted environment file and remain excluded from API responses, SQLite and JSON exports. +- Fastify validates every user-controlled mutation with Zod. External integration errors are reduced to non-sensitive connection states. +- New discoveries are inserted with `visible=false`. Subsequent syncs update runtime fields but preserve user choices and overrides. +- A DockDeck-only app removal records the technical name in `removed_apps`; later discovery ignores that name and never calls a Docker mutation endpoint. -- React -- TypeScript -- MapLibre GL -- Deck.gl -- Tailwind -- TanStack Query -- Zustand of vergelijkbare lichte state store -- Recharts voor eenvoudige grafieken +## Modules -Belangrijke principes: +- `src/shared`: Zod contracts and pure URL/search/matching/sorting rules. +- URL resolution prefers an explicit `br0` LAN address, then the configured Unraid host, and uses a single unambiguous published TCP port when template metadata is stale. +- Dashboard search is a derived client index over the latest payload, so renames, removals, category changes and URL changes require no separate search persistence. +- `src/server/database.ts`: persistence, migration, import/export and user preference invariants. +- Widget layout is stored as validated size/content, metric order/visibility/style, statistic limit, warning and chart fields on app and preference records. Legacy rows receive safe standard/all-content defaults; version 1 backups remain compatible because added backup fields are optional on import. +- `src/server/integrations`: read-only external adapters, provider capability metadata and deterministic mocks. Provider URLs default to an already discovered local app origin; explicit environment overrides remain available. Provider recognition is token-based and excludes known support-container roles. +- `src/server/integrations/unraid-status.ts`: parseert een minimale read-only view van Unraid runtime- en hwmondata naar array-, parity-, opslag-, temperatuur- en optionele UPS-metrics. +- Provideradapters bewaren alleen de laatst succesvolle gereduceerde metricresponse en een korte tijdreeks in procesgeheugen; geen van beide overleeft een procesrestart. +- `src/server/app.ts`: narrow REST surface; no Docker control routes. +- `src/client`: accessible responsive UI and design tokens. -- kaart centraal, maar analysepanelen even belangrijk -- labs per workflow -- duidelijke jobstatus -- outputs altijd exporteerbaar -- geen verborgen mockgedrag +## Layout and offline model -## 4. Backend +Widgetvolgorde staat los van apptegelvolgorde. App- en hostrecords bewaren hun eigen volgorde; layoutpresets bevatten alleen presentatievelden, inclusief metricselectie, value/gauge/progress, waarschuwing en lijn-/vlak-/staafgrafiek. Provider- en hosthistoriek bevat maximaal zestig punten per numerieke reeks en blijft uitsluitend in procesgeheugen. Het toepassen van een preset kan geen appzichtbaarheid, navigatie-URL, credential of Dockerstaat wijzigen. -Aanbevolen stack: +De serviceworker cachet alleen statische applicatieshellassets. Requests onder `/api/` worden volledig overgeslagen, zodat offline content nooit als actuele runtime- of providerdata wordt gepresenteerd. -- FastAPI -- SQLAlchemy 2.x -- GeoAlchemy2 -- Alembic -- Pydantic -- RQ/Celery -- Rasterio -- GeoPandas -- Shapely -- PyProj -- NumPy -- OpenCV -- Ultralytics/PyTorch +## Failure behavior -## 5. Database - -PostgreSQL met PostGIS is verplicht voor: - -- projectgebieden -- vectorfeatures -- detectiepolygonen -- segmentatiepolygonen -- spatial joins -- intersects -- IoU berekeningen -- bounds queries - -## 6. Storage - -Bewaar grote bestanden niet in de database. - -Opslagcategorieën: - -- originele uploads -- verwerkte rasters -- raster tiles -- masks -- model outputs -- exports -- rapporten - -Database bewaart metadata en paden. - -## 7. Jobs - -Langlopende processen moeten via background jobs: - -- raster metadata extraction -- raster clipping -- raster tiling -- vector import -- AI inference -- segmentation polygonize -- QA/QC -- change detection -- export generation - -## 8. AI Inference - -Inference pipeline: - -```text -Raster dataset -→ clip to area -→ tile raster -→ normalize/preprocess -→ model inference -→ convert pixel coords to geospatial coords -→ merge/filter outputs -→ save detections/segmentations -→ expose as map layer -``` - -## 9. CRS-regels - -- Alle interne geometrieën worden opgeslagen in PostGIS met bekende SRID. -- Voor metrische berekeningen wordt een geschikte projectie gebruikt. -- API-output naar frontend mag in EPSG:4326 of WebMercator-compatible formaat. -- Elke dataset zonder CRS krijgt status `needs_crs_review`. - -## 10. Developmentstrategie - -Bouwvolgorde: - -1. backend foundation -2. database schema -3. project/area API -4. dataset upload en metadata -5. frontend workspace en kaart -6. vector import -7. raster import -8. processing jobs -9. detection lab -10. QA/QC -11. export +Saved data is always returned independently of integration health. A failed AppOps inventory, missing Unraid mount or unavailable NPM instance produces a degraded integration card/banner without blocking navigation. Polling runs only while the browser tab is visible. AppOps inventory is read immediately; its slower all-container resource sample refreshes in the background so it cannot delay navigation. diff --git a/docs/EXTERNAL_ACCESS.md b/docs/EXTERNAL_ACCESS.md new file mode 100644 index 00000000..755f8c3f --- /dev/null +++ b/docs/EXTERNAL_ACCESS.md @@ -0,0 +1,29 @@ +# Authenticated external access + +DockDeck has no built-in authentication. Keep its Compose bind on `127.0.0.1` or a trusted LAN address unless an authenticated HTTPS reverse proxy protects every route. + +## Recommended topology + +```text +Internet -> HTTPS reverse proxy -> Authentik forward auth -> DockDeck :1218 +Trusted LAN -----------------------------------------------> DockDeck :1218 +``` + +## Minimum requirements + +1. Terminate TLS with a valid certificate. +2. Require authentication before proxying `/`, static assets and every `/api/*` route. +3. Do not create an unauthenticated exception for `/api/diagnostics`, integration settings or exports. +4. Preserve the original host and forwarding headers. +5. Restrict the upstream to `http://192.168.10.150:1218`; never publish the internal Docker socket proxy. +6. Use a separate minimum-scope account/token for each provider and protect `/data/integrations.env` backups. + +## Validation checklist + +- An incognito request is redirected to authentication before DockDeck HTML or API JSON is returned. +- A signed-in user can load the dashboard, search, edit a harmless presentation preference and read diagnostics. +- Signing out invalidates both page and API access. +- The browser reports HTTPS without mixed content. +- The reverse proxy does not cache `/api/*` responses. + +Nginx Proxy Manager discovery in DockDeck remains read-only and does not create or change proxy hosts. Proxy and Authentik configuration therefore stay an explicit deployment responsibility. diff --git a/docs/IMPLEMENTATION_STATUS.md b/docs/IMPLEMENTATION_STATUS.md new file mode 100644 index 00000000..1a437bda --- /dev/null +++ b/docs/IMPLEMENTATION_STATUS.md @@ -0,0 +1,37 @@ +# DockDeck implementation status + +Updated: 2026-07-22 + +| MVP criterion | Status | Evidence | +| ------------------------------------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------- | +| Read-only Docker/Unraid discovery | Live verified | AppOps GET adapter, optional restricted proxy overlay, mocks and adapter tests. | +| New containers hidden | Built | Repository insert invariant and API/database tests. | +| App visibility/name/category/order/URL management | Built | Settings UI, Zod API and persistence tests. | +| Persistent app removal | Verified | DockDeck-only delete endpoint, `removed_apps` suppression and rediscovery/export coverage. | +| Local WebUI detection | Built | Unraid XML parser, port substitution and unit tests. | +| Status and ~30 second polling | Built | Runtime state mapping and visibility-aware client interval. | +| Categories and both dashboard views | Built | CRUD API, settings and responsive overview/tab UI. | +| Favorites | Built | CRUD API, settings, dashboard tiles and search index. | +| Search and Google fallback | Verified | Dynamic full-payload index, keyboard selection, explicit Google action and browser flow. | +| Links open in new tabs | Built | All tiles/results use `_blank` with `noopener noreferrer`. | +| Three themes | Built | Porcelain, Midnight and Harbor tokens and controls. | +| Persistent appearance customization | Verified | Theme, mode, accent, density, width, background, hero, status, reduced motion and polling with migration/export coverage. | +| Searchable settings | Verified | Global section search, accessible controls and automatic scroll restoration on section changes. | +| Read-only service widgets | Verified | Per-app opt-in, eight-widget limit, domain data, three sizes, four layers and configurable presentation. | +| Configurable Tower identity | Live verified | Persistent server name/URL in Settings, search and host card; live URL is `192.168.10.150:5000`. | +| Automatic favorite favicons | Verified | First-party `/favicon.ico` derivation, explicit override and monogram fallback. | +| SQLite persistence | Built | Named Docker volume and restart persistence test. | +| Export/import | Built | Versioned Zod schema, API and settings flow; secrets excluded. | +| Read-only NPM integration | Built | Optional server-side adapter, unambiguous matching and fallback. | +| Degraded states | Built | Adapter isolation, health states, banner and integration panel. | +| Docker/Unraid packaging | Live verified | Commit `3ffd39b`, one healthy `DockDeck` on `192.168.10.150:1218`; 137 stored/55 visible apps and 90 GET-only containers. | +| Private Gitea repository | Verified | Private `NuklearRabbit/DockDeck`; local `main` tracks `origin/main`. | +| Professional responsive UI | Live verified | 390px through ultrawide plus live 1280×720; adaptive cards, persistent favorites sidebar and no overflow. | +| Independent widget composition | Verified | Order, size, content, labels, metric styles/limits, any-signal warnings, cadence, charts and edit mode. | +| Native Unraid health | Live verified | Array STARTED, parity healthy, three storage groups and temperatures through read-only mounts. | +| App-specific provider breadth | Live verified | 97/97 stored apps have an explicit domain profile; zero generic profiles remain in the live capability audit. | +| Per-app metric composition | Live verified | Labels can be selected, ordered, styled, limited, charted and monitored and survive presets/export/restart. | +| Configurable Unraid trends | Live verified | Container, image, storage, temperature, parity and UPS series with statistic selection and 60-point memory. | +| Unraid Docker-list icon | Live verified | DockerMan visibly renders the 512×512 PNG after both persistent and active caches are refreshed. | +| Layout presets | Verified | Create, apply, delete and independent import/export with database/API/browser coverage. | +| PWA and diagnostics | Live verified | Installable shell excludes `/api`; diagnostics are secret-free and report native/integration health. | diff --git a/docs/PLATFORM_AUDIT.md b/docs/PLATFORM_AUDIT.md new file mode 100644 index 00000000..85834527 --- /dev/null +++ b/docs/PLATFORM_AUDIT.md @@ -0,0 +1,75 @@ +# DockDeck platform audit + +Updated: 2026-07-15 + +## Executive assessment + +DockDeck is now a complete daily-use dashboard rather than a launcher with generic telemetry. Navigation, app management, responsive presentation, widget composition, service-specific data, Unraid host depth, freshness, portability and diagnostics form one coherent read-only product. The P0, P1 and P2 roadmap from the previous audit is implemented. + +## Current scorecard + +| Area | Assessment | Evidence | +| --------------------- | ---------------------- | ---------------------------------------------------------------------------------------------------------------- | +| Navigation and search | Complete | Dynamic index follows renames, removals, URLs, categories, favorites and Tower. | +| Responsive shell | Complete | Mobile, tablet, desktop and ultrawide layouts have no positive horizontal overflow. | +| App management | Complete | Rename, override, categorize, hide and persistent DockDeck-only removal. | +| Widget composition | Complete | Order, three sizes, content layers, per-metric visibility/order/style, limits, labels, warnings and edit mode. | +| Widget data depth | Complete framework | Live audit: 97/97 apps have a specific capability profile, zero generic profiles; 23 native providers. | +| Unraid host depth | Complete | AppOps plus array/parity/storage/temperature/UPS, selectable stats and configurable server trend charts. | +| Freshness and trends | Complete | Fresh/stale/unavailable semantics, cadence, stale cache and selectable line/area/bar charts up to sixty samples. | +| Portability | Complete | Full secret-free backup plus independent layout preset import/export. | +| Operations | Complete for LAN | Secret-free diagnostics, health endpoint, PWA shell and authenticated reverse-proxy guide. | +| Security | Strong for trusted LAN | Single socketless container, GET-only integrations, masked secrets and API-excluding service worker. | + +## Delivered roadmap + +### Personal widget depth + +- Persistent Tower name and URL in Settings with port `5000` as the default Unraid WebUI route. +- Capability coverage expanded from a provider shortlist to every discovered app. +- Native Audiobookshelf, Netdata and PeerTube readers added to the existing provider set. +- Domain profiles for the actual live inventory, including app/API/frontend/worker/database/cache, mail, GPU, download, media, document and custom-project workloads. +- Per-app metric visibility and ordering, including dynamically returned labels and preset/export persistence. +- Per-app value/gauge/progress presentation, statistic limits, line/area/bar charts, legends and selectable 10–60 sample windows. +- Warning rules can target any numeric infrastructure or provider metric and trigger above or below the configured value. +- Capability labels remain app-specific before an endpoint is connected; absent values say `Connect data` instead of showing unrelated generic Docker labels. +- BlockPilot root/runtime/API/web/Minecraft/ViaProxy, LumaOps, Chimera, OpenRGB, Porkfolio and unnamed legacy workloads now have explicit profiles rather than the last generic service profile. +- Tower exposes selectable/ordered server statistics and charts for containers, images, storage, temperature, parity and UPS. +- A constrained custom contract (`summary` plus at most twelve stats) for apps without a native read API; endpoint and optional bearer token are configured in Widget Studio and remain server-side. + +### P0 + +- Independent widget order with keyboard-accessible move controls. +- Native Unraid adapter for array state, parity, disk/pool usage, temperatures and optional UPS data. +- App-specific Gitea adapter and GET-only metrics bridges for Deluge, JDownloader and Tdarr. +- Explicit freshness states with last-successful provider cache. + +### P1 + +- Metric order, primary metric, custom labels and numeric provider/infrastructure warning thresholds. +- Saved, applicable, removable and portable layout presets. +- Safe per-widget refresh cadence between 15 seconds and 5 minutes. +- Direct dashboard edit mode with move, resize and hide controls. +- Provider expansion for Jellyfin, Seerr, Prowlarr, Authentik, NPM, Portainer, Grafana, Prometheus and Nextcloud, plus bridges for qBittorrent, Bazarr and Vaultwarden. + +### P2 + +- Selectable line, area and bar charts with at most sixty in-memory points and no monitoring database. +- Installable PWA application shell; API responses are never cached. +- Independent preset export/import. +- Secret-free diagnostics and an authenticated external-access guide. + +## Remaining environment-dependent opportunities + +- Fill provider credentials or bridge URLs in Settings only for services whose protected API data is desired. Safe Docker fallback remains available without them. +- Configure `UNRAID_UPS_STATUS_PATH` only when a stable read-only APC/UPS status file is available on the host. +- Enable external access only behind authenticated HTTPS; DockDeck deliberately has no built-in user system. +- Persistent long-term history, alerts and notifications remain intentionally outside scope. They would turn DockDeck into a monitoring platform and need separate retention and delivery decisions. + +## Guardrails + +- A widget is app-specific only when its metrics describe the real workload of that app. +- Every external request remains GET-only, time-bounded and server-side. +- Provider failure never blocks navigation and always falls back to safe telemetry. +- Compact panels answer one question, standard panels a few, and wide panels may expose deeper context. +- Layout remains fully usable without drag-and-drop and is tested at both mobile and ultrawide sizes. diff --git a/docs/design/DESIGN_SYSTEM.md b/docs/design/DESIGN_SYSTEM.md new file mode 100644 index 00000000..e27dd523 --- /dev/null +++ b/docs/design/DESIGN_SYSTEM.md @@ -0,0 +1,9 @@ +# DockDeck design system + +De visuele basis is **DockDeck Canon v1**. Het onderhoudbare systeem, de bronselectie en de mapping naar echte productflows staan in: + +- [`dockdeck-canon-v1/DESIGN_SYSTEM.md`](./dockdeck-canon-v1/DESIGN_SYSTEM.md) +- [`dockdeck-canon-v1/SOURCE_MANIFEST.md`](./dockdeck-canon-v1/SOURCE_MANIFEST.md) +- [`dockdeck-canon-v1/IMPLEMENTATION_MAPPING.md`](./dockdeck-canon-v1/IMPLEMENTATION_MAPPING.md) + +Gebruik in productcode uitsluitend semantische tokens en lokale assets. Behoud dark en light themes, Nederlandse terminologie, echte API-data en de read-only productgrenzen. diff --git a/docs/design/STITCH_CANON_UI_UPGRADE_PLAN.md b/docs/design/STITCH_CANON_UI_UPGRADE_PLAN.md new file mode 100644 index 00000000..75ca60a3 --- /dev/null +++ b/docs/design/STITCH_CANON_UI_UPGRADE_PLAN.md @@ -0,0 +1,68 @@ +# Stitch Canon UI-upgradeplan + +Datum: 2026-07-21 +Branch: `codex/stitch-canon-ui-upgrade` + +## Nulmeting + +DockDeck is een React/Vite-single-page-app met Fastify REST, SQLite en gedeelde Zod-contracten. `App.tsx` beheert dashboard/settings, polling, themadata-attributen en toasts. `Dashboard.tsx` groepeert echte apps en favorieten. `Settings.tsx` bevat de bestaande beheerflows; widgetconfiguratie blijft in afzonderlijke componenten. Er is geen router: schermwisseling is lokale React-state. + +De bestaande functionaliteit is operationeel, maar de interface wijkt zichtbaar van Canon v1 af: Engelse copy, een permanente brede dashboardrail, een prominente monitoringlaag vóór de launcherinhoud en volledig uitgeklapte appformulieren. De baseline is opgeslagen onder `.codex-input/baseline/`. + +Baselinechecks: + +- format, lint en typecheck: geslaagd; +- unit/integratie: 42/42 geslaagd; +- security: 5/5 geslaagd; +- build en audit: geslaagd, 0 kwetsbaarheden; +- E2E-start: omgevingsblokkade doordat een WSL-relay poort 3000 bezet; de upgrade maakt de testpoort configureerbaar. + +## Routes en workflows + +| Oppervlak | Werkelijke workflow | Canonvertaling | +| ----------------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------ | +| Dashboard | apps/favorieten openen, categorieën, sync, widgets | compacte topbar, Quick Launch eerst, favorieten en apps als launcher; widgets lager en optioneel | +| Quick Launch | apps/favorieten/Tower/Google, toetsenbord | gegroepeerde command-palette met Apps, Favorieten en Google | +| Apps | zoeken, zichtbaarheid, widget, naam, categorie, URL's, icoon, sortering, verwijderen | compacte beheerregels met filters en een app-editor drawer | +| Favorieten | toevoegen, wijzigen, verwijderen | Canon-lijst plus rustig formulier/drawerpatroon | +| Categorieën | toevoegen, wijzigen, icoon, sortering, verwijderen | Canon-lijst met duidelijke aantallen en inline-acties | +| Weergave | thema, accent, dichtheid, breedte, achtergrond, hero/status, modus | visuele keuzevelden en previews binnen dezelfde settings-shell | +| Widgets/operaties | bestaande read-only panelen, presets en diagnostics | behouden onder Algemeen als secundaire geavanceerde functies | +| Integraties | status en gemaskeerde credentials | doelgerichte read-only statuskaarten zonder secret-echo | +| Back-up | export/import versie 1 | Canon back-up- en hersteloppervlak, zonder fictieve historie | + +## Componentstrategie + +Hergebruikt: API-client, contracten, URL-resolutie, iconpipeline, widgetconfigurators, server- en servicewidgets, persistence en alle backendvalidatie. + +Herbouwd of toegevoegd: semantische tokenlaag, lokale merkassets, `DashboardTopBar`, dashboardcompositie, categorie-navigatie, app-/favoriettegels, Quick Launch-groepering, settings-shell, compacte appbeheerregels, app-editor drawer, banners, empty/loading/degraded states en mobiele navigatie. + +## Tokens en responsiviteit + +Canon v1 gebruikt een 8px-basis, diepe navy, tonale oppervlakken, lavendel voor actie/focus en groen uitsluitend voor positieve status. Dark, light en het bestaande aanvullende thema blijven functioneel via semantische CSS-variabelen. Bestaande accenten blijven beschikbaar maar worden op de Canon-tokenlaag geprojecteerd. + +- 1920×1080 en 1440×900: max. 1280px launcherinhoud, compacte topbar, vier appkolommen waar passend. +- 768–1024px: twee kolommen, compacte settingsnavigatie. +- 390×844: echte mobiele compositie, horizontaal scrollbare categorieën, één/twee kolommen naar inhoud, vaste ondernavigatie. +- Geen horizontale overflow; minimum klikdoel 44px; `prefers-reduced-motion` blijft leidend. + +## Risico's en mitigatie + +- De huidige widgetdiepgang is groter dan de statische Canon-scope. De functies blijven bestaan maar verdwijnen uit de primaire launcherhiërarchie. +- `Settings.tsx` is groot. De wijziging beperkt backend/API-impact en splitst nieuwe UI-primitives en drawerlogica waar dat reviewbaarheid verbetert. +- E2E gebruikt een vaste poort. De configuratie krijgt een geïsoleerde testpoort zonder productieruntime te wijzigen. +- Externe appiconen kunnen falen. De bestaande veilige monogramfallback blijft behouden. + +## Validatie + +Twee visuele rondes vergelijken dark/light dashboard, Quick Launch, settings/apps, editor en mobiel met de referenties. Daarna volgen alle verplichte scripts, browserconsole/netwerkcontrole, overflowchecks op 390×844, 768×1024, 1440×900 en 1920×1080, Compose-config en een Docker-imagebuild wanneer de lokale Docker-runtime beschikbaar is. + +## Niet-doelen + +Geen Docker-mutaties, containerbeheer, accounts, terminal/logviewer, nieuwe backendintegraties, fictieve data, automatische back-uphistorie of externe tijdelijke assets. + +## Resultaat + +Afgerond op 2026-07-21. De Canon-tokenlaag, lokale Geist-fonts, merkassets, topbar, launcherhiërarchie, Quick Launch, compacte settings-shell, app-editor drawer en responsive mobiele compositie zijn in de bestaande React/Fastify-app geïntegreerd. De E2E-runtime gebruikt geïsoleerde poorten 3101/5174 zodat een lokale WSL-relay op 3000 de suite niet meer blokkeert. De finale gates en eventuele resterende omgevingsbeperkingen staan in `TEST_LOG.md`, `RISKS.md` en `HANDOFF.md`. + +Een aanvullende fidelity-pass heeft de eerder nog eigen DockDeck-interpretatie vervangen door de daadwerkelijke `canon_*`-compositie. Stitch bepaalt nu vrijwel volledig layout, density, kaartfamilies, navigatie, overlay, settings-tabel, drawer en mobiele hiërarchie; alleen echte data, bestaande validatie en operationele workflows wijken bewust af van de statische voorbeelden. diff --git a/docs/design/dockdeck-canon-v1/DESIGN_SYSTEM.md b/docs/design/dockdeck-canon-v1/DESIGN_SYSTEM.md new file mode 100644 index 00000000..dcb196ca --- /dev/null +++ b/docs/design/dockdeck-canon-v1/DESIGN_SYSTEM.md @@ -0,0 +1,30 @@ +# DockDeck Canon v1 + +DockDeck is een rustige, premium digitale thuisbasis. De interface is een launcher, geen beheer- of monitoringconsole. + +## Fundament + +- 8px spacingbasis; 16px mobiel zijmarge, 24px desktopgutter, 1280px launchermaximum. +- Geist-achtige lokaal gebundelde sans-serif, sentence case en volledig Nederlandse copy met `je/jouw`. +- Dark-first diepe navy `#0b1326`; tonale oppervlakken en subtiele 1px-randen. +- Lavendel `#818cf8` voor merk, focus, selectie en primaire acties. +- Groen `#10b981` uitsluitend voor positieve status/bevestiging; rood voor offline/fout, slate voor onbekend. +- 8px radius voor controls, 12–16px voor grotere oppervlakken, pills alleen waar de vorm betekenis heeft. +- Geen zware schaduwen of permanente pulserende statusanimaties. + +## Semantische tokens + +Productcode gebruikt tokens voor `background`, `surface`, `surface-elevated`, `border`, `text`, `text-muted`, `primary`, `focus`, `success`, `warning`, `error`, `unknown`, `hover`, `active` en `disabled`. Thema's wijzigen de tokenwaarden, niet de componentstructuur. + +## Componentregels + +- Apptegels tonen icoon, naam en `Online`, `Offline` of `Status onbekend`; geen poort, image, CPU, RAM of uptime. +- Favoriettegels delen dezelfde visuele familie zonder runtimestatus. +- Quick Launch groepeert Apps, Favorieten en Google en ondersteunt muis en toetsenbord. +- Settings gebruikt één shell met Algemeen, Apps, Favorieten, Categorieën, Weergave, Integraties en Back-up en herstel. +- Overlays hebben Escape, focusherstel en een zichtbaar sluitdoel; drawer op desktop, schermvullend op mobiel. +- Status wordt nooit uitsluitend met kleur gecommuniceerd. + +## Assets + +Alle merkassets, fonts en productie-iconen zijn lokaal. Stitch-screenshots zijn alleen ontwerpdocumentatie; HTML/CDN/Google-hosted exportassets worden niet uitgevoerd of gebundeld. diff --git a/docs/design/dockdeck-canon-v1/IMPLEMENTATION_MAPPING.md b/docs/design/dockdeck-canon-v1/IMPLEMENTATION_MAPPING.md new file mode 100644 index 00000000..27e1ea5c --- /dev/null +++ b/docs/design/dockdeck-canon-v1/IMPLEMENTATION_MAPPING.md @@ -0,0 +1,37 @@ +# Implementatiemapping — DockDeck Canon v1 + +| Canonreferentie | DockDeck-oppervlak | Implementatiekeuze | +| --------------------------------------------- | ------------------------------- | ------------------------------------------------------------------------------------------------------------------ | +| Dashboard 1440/1920 donker | primaire startpagina | compacte topbar, begroeting, categorieën, desktopfavorietenrail, apptegels en uitklapbaar live inzicht; echte data | +| Dashboard licht | light theme | dezelfde hiërarchie en componentvormen op lichte semantische tokens | +| Dashboard categorie-modus | bestaande `viewMode=categories` | categoriechips sturen de bestaande filterlogica | +| Quick Launch resultaten/geen lokaal resultaat | `SearchOverlay` | viewportbrede portal met Apps/Favorieten/Google, pijlen, Enter, Escape en herstelde toetsenbordfocus | +| Instellingen Apps | `Settings` / apps | compacte rijen, echte ontdekstatus, filters, paginering en status/zichtbaarheid afzonderlijk | +| App-editor drawer | app PATCH-contract | naam, zichtbaarheid/categorie, lokale/externe URL, icoon en sortering in toegankelijke drawer | +| Instellingen Favorieten | favoriete CRUD | Canon-lijst en formulier met bestaande validatie | +| Instellingen Categorieën | categorie CRUD | bestaande naam/icoon/sortering/verwijderen in rustige lijst | +| Instellingen Weergave | preferences PATCH | thema, modus en dichtheid eerst; accent, breedte, achtergrond en details progressief ontsloten | +| Instellingen Integraties | status/credential-API | uitsluitend bestaande read-only integraties; secrets gemaskeerd | +| Back-up en herstel | `/api/export` en `/api/import` | bestaande versie 1-flow, geen fictieve historie | +| Mobiel 390×844 | responsive dashboard/settings | mobiele topbar, touchcategorieën, compacte tegels en ondernavigatie | + +## Bewuste afwijkingen + +- Bestaande read-only widgets, presets en diagnostics blijven beschikbaar onder Algemeen omdat dit operationele productfunctionaliteit is; ze krijgen geen voorrang boven de launcher. +- De statische voorbeelden gebruiken andere apps en aantallen; DockDeck toont uitsluitend API-data. +- Canon-copy over Docker Proxy wordt niet letterlijk gebruikt omdat productie via AppOps kan ontdekken; integratietekst volgt de echte runtime. +- Het logo wordt vereenvoudigd voor 16–48px en als lokaal SVG/PNG geleverd; de bronafbeelding wordt niet uitgesneden of gehotlinkt. + +## Implementatiestatus + +Alle bovenstaande kernoppervlakken zijn op 2026-07-21 geïmplementeerd. Het dashboard gebruikt de echte API-payload, Quick Launch groepeert server/apps/favorieten/Google, appbeheer gebruikt compacte regels plus een focus-trapped drawer en de bestaande geavanceerde widget- en diagnosticaflows staan onder **Algemeen**. De statische Canon-voorbeelden zijn dus niet als los prototype of hardcoded datalaag overgenomen. + +Na de eerste review is een tweede, directere Stitch-pass uitgevoerd. `stitch-direct.css` is de finale compositielaag en neemt nu de daadwerkelijke Canon-hiërarchie over: 64px topbar, begroeting met compacte ontdekbanner, pilnavigatie, vaste favorietkaart, vierkoloms servicekaarten, tweekoloms featurekaarten, de volledige Quick Launch-sheet, App Beheer als vijfkolomstabel en de 570px app-editor. Op mobiel worden de ontdekkaart, zoekbalk, ronde categorieknoppen, favorietegels, compacte servicerijen en vaste ondernavigatie rechtstreeks uit `canon_mobiel_dashboard_390_844` gevolgd. + +Het operationele widgetblok blijft in de normale launcher zichtbaar als compacte rij **Live inzicht** en klapt op verzoek open. De volledige Paneelstudio, presets en diagnostics blijven onder **Algemeen** beschikbaar, zodat de dagelijkse launcher rustig blijft zonder functionaliteit te verbergen. + +De visuele eindcontrole staat in [`implementation/README.md`](./implementation/README.md). Daarbij is ook de eerste-use toestand met alle nieuw ontdekte apps verborgen gecontroleerd; de gevulde screenshots gebruiken uitsluitend de geïsoleerde mockdatabase onder `.codex-input/`. + +## Consistentiepass 2026-07-22 + +Dashboard en instellingen delen nu dezelfde platformgeometrie tot 3040 px, met een schaalbare linkerrail en een rechter widgetdock dat vanaf 1900 px automatisch opent. De instellingenkopie en form controls zijn vergroot voor leesbaarheid, apptegels hebben sterkere iconografie en actieaffordances, Quick Launch gebruikt echte favorieticonen en de lege zoekstaat bevat directe snelkoppelingen in plaats van alleen uitlegtekst. diff --git a/docs/design/dockdeck-canon-v1/SOURCE_MANIFEST.md b/docs/design/dockdeck-canon-v1/SOURCE_MANIFEST.md new file mode 100644 index 00000000..adeb0855 --- /dev/null +++ b/docs/design/dockdeck-canon-v1/SOURCE_MANIFEST.md @@ -0,0 +1,23 @@ +# Source manifest — DockDeck Canon v1 + +## ZIP + +- Bron: `C:\Users\Jens\Downloads\stitch_dockdeck_premium_interface_system (2).zip` +- SHA-256: `C5A76D95680550A74024F967F86553A008E5657C05833EE13C042C4F493BD441` +- Tijdelijke extractie: `.codex-input/stitch-dockdeck-canon-v1/` (genegeerd door Git) +- Primair: `dockdeck_canon_v1/DESIGN.md`, `dockdeck_canon_logo/screen.png` en alle `canon_*`-schermen. +- Aanvullende controle: `design.md_dockdeck_canon_v1.md`. +- Bewust genegeerd: `core_modernist/`, `obsidian_control/`, niet-`canon_*` schermen, `*_definitief` varianten en conflicterende oudere designsystemen. + +## Stitch MCP + +- Beschikbaar op 2026-07-21. +- Project: `projects/11417268779858336248` — **DockDeck Premium Interface System**. +- Project bijgewerkt: 2026-07-21T19:14:24.395101Z. +- Gebruikt design system: `assets/d3776abdbc9040bcac889ef9b20c3eb8` — **DockDeck Canon v1**, versie 1. +- Gecontroleerde metadata: Geist, dark-first, 8px afronding, `#818cf8` primair, `#10b981` positief, `#0b1326` basis en 1280px contentmaximum. +- De actuele `CANON — ...` schermen zijn vergeleken; niet-canonieke oudere schermen en de systemen Obsidian Control/Core Modernist zijn niet als productbron gebruikt. + +## Opgenomen referenties + +De map `screens/` bevat alleen dashboard dark/light, Quick Launch-resultaten, instellingen Apps, mobiel dashboard en de logo-referentie. HTML-export en externe Google-assets zijn niet opgenomen of gebruikt als productieafhankelijkheid. diff --git a/docs/design/dockdeck-canon-v1/implementation/README.md b/docs/design/dockdeck-canon-v1/implementation/README.md new file mode 100644 index 00000000..ced43d93 --- /dev/null +++ b/docs/design/dockdeck-canon-v1/implementation/README.md @@ -0,0 +1,20 @@ +# Canon v1 implementatiebeelden + +Deze beelden zijn op 2026-07-22 rechtstreeks uit de lokale applicatie vastgelegd met deterministische mockdiscovery. Ze bevatten geen credentials of productiegegevens. De actuele dashboards tonen de Stitch-launcherworkspace met desktopfavorietenrail, herkenbare appiconen en zichtbare read-only servicepanelen; de E2E-suite legt ze opnieuw vast. + +| Oppervlak | Beeld | +| ----------------------------------------- | ---------------------------------------------------------------------------------------- | +| Dashboard licht, desktop 1440×900 | [`dashboard-light-desktop-1440x900.png`](./dashboard-light-desktop-1440x900.png) | +| Dashboard donker, desktop 1440×900 | [`dashboard-dark-desktop-1440x900.png`](./dashboard-dark-desktop-1440x900.png) | +| Quick Launch licht, desktop 1440×900 | [`quick-launch-light-desktop-1440x900.png`](./quick-launch-light-desktop-1440x900.png) | +| Apps-instellingen licht, desktop 1440×900 | [`settings-apps-light-desktop-1440x900.png`](./settings-apps-light-desktop-1440x900.png) | +| Dashboard licht, mobiel 390×844 | [`dashboard-light-mobile-390x844.png`](./dashboard-light-mobile-390x844.png) | +| App-editor drawer, mobiel 390×844 | [`app-drawer-mobile-390x844.png`](./app-drawer-mobile-390x844.png) | +| Dashboard donker, mobiel 390×844 | [`dashboard-dark-mobile-390x844.png`](./dashboard-dark-mobile-390x844.png) | +| Dashboard donker, tablet 768×1024 | [`dashboard-dark-tablet-768x1024.png`](./dashboard-dark-tablet-768x1024.png) | +| Dashboard donker, ultrawide 3440×1440 | [`dashboard-dark-ultrawide-3440x1440.png`](./dashboard-dark-ultrawide-3440x1440.png) | +| Quick Launch donker, desktop 1440×900 | [`quick-launch-dark-desktop-1440x900.png`](./quick-launch-dark-desktop-1440x900.png) | +| Apps-instellingen donker, desktop | [`settings-apps-dark-desktop-1440x900.png`](./settings-apps-dark-desktop-1440x900.png) | +| App-editor donker, desktop | [`app-drawer-dark-desktop-1440x900.png`](./app-drawer-dark-desktop-1440x900.png) | + +De geïsoleerde E2E-database wordt alleen voor visuele verificatie aangepast om drie apps en drie favorieten zichtbaar te maken. De productregel blijft ongewijzigd: nieuw ontdekte apps zijn standaard verborgen. De finale audit controleert bovendien alle instellingensecties, de horizontale tablet-/mobiele navigatie, echte appiconen, de uitklapbare widgetlaag en een Quick Launch die ook op ultrawide het volledige viewport afdekt. diff --git a/docs/design/dockdeck-canon-v1/implementation/app-drawer-dark-desktop-1440x900.png b/docs/design/dockdeck-canon-v1/implementation/app-drawer-dark-desktop-1440x900.png new file mode 100644 index 00000000..732a38a6 Binary files /dev/null and b/docs/design/dockdeck-canon-v1/implementation/app-drawer-dark-desktop-1440x900.png differ diff --git a/docs/design/dockdeck-canon-v1/implementation/app-drawer-mobile-390x844.png b/docs/design/dockdeck-canon-v1/implementation/app-drawer-mobile-390x844.png new file mode 100644 index 00000000..8a005593 Binary files /dev/null and b/docs/design/dockdeck-canon-v1/implementation/app-drawer-mobile-390x844.png differ diff --git a/docs/design/dockdeck-canon-v1/implementation/dashboard-dark-desktop-1440x900.png b/docs/design/dockdeck-canon-v1/implementation/dashboard-dark-desktop-1440x900.png new file mode 100644 index 00000000..56f7e39e Binary files /dev/null and b/docs/design/dockdeck-canon-v1/implementation/dashboard-dark-desktop-1440x900.png differ diff --git a/docs/design/dockdeck-canon-v1/implementation/dashboard-dark-mobile-390x844.png b/docs/design/dockdeck-canon-v1/implementation/dashboard-dark-mobile-390x844.png new file mode 100644 index 00000000..7f30601f Binary files /dev/null and b/docs/design/dockdeck-canon-v1/implementation/dashboard-dark-mobile-390x844.png differ diff --git a/docs/design/dockdeck-canon-v1/implementation/dashboard-dark-tablet-768x1024.png b/docs/design/dockdeck-canon-v1/implementation/dashboard-dark-tablet-768x1024.png new file mode 100644 index 00000000..d37fc03b Binary files /dev/null and b/docs/design/dockdeck-canon-v1/implementation/dashboard-dark-tablet-768x1024.png differ diff --git a/docs/design/dockdeck-canon-v1/implementation/dashboard-dark-ultrawide-3440x1440.png b/docs/design/dockdeck-canon-v1/implementation/dashboard-dark-ultrawide-3440x1440.png new file mode 100644 index 00000000..a8681450 Binary files /dev/null and b/docs/design/dockdeck-canon-v1/implementation/dashboard-dark-ultrawide-3440x1440.png differ diff --git a/docs/design/dockdeck-canon-v1/implementation/dashboard-light-desktop-1440x900.png b/docs/design/dockdeck-canon-v1/implementation/dashboard-light-desktop-1440x900.png new file mode 100644 index 00000000..6547702d Binary files /dev/null and b/docs/design/dockdeck-canon-v1/implementation/dashboard-light-desktop-1440x900.png differ diff --git a/docs/design/dockdeck-canon-v1/implementation/dashboard-light-mobile-390x844.png b/docs/design/dockdeck-canon-v1/implementation/dashboard-light-mobile-390x844.png new file mode 100644 index 00000000..c889ae75 Binary files /dev/null and b/docs/design/dockdeck-canon-v1/implementation/dashboard-light-mobile-390x844.png differ diff --git a/docs/design/dockdeck-canon-v1/implementation/quick-launch-dark-desktop-1440x900.png b/docs/design/dockdeck-canon-v1/implementation/quick-launch-dark-desktop-1440x900.png new file mode 100644 index 00000000..94d3031f Binary files /dev/null and b/docs/design/dockdeck-canon-v1/implementation/quick-launch-dark-desktop-1440x900.png differ diff --git a/docs/design/dockdeck-canon-v1/implementation/quick-launch-light-desktop-1440x900.png b/docs/design/dockdeck-canon-v1/implementation/quick-launch-light-desktop-1440x900.png new file mode 100644 index 00000000..5ddfb1dc Binary files /dev/null and b/docs/design/dockdeck-canon-v1/implementation/quick-launch-light-desktop-1440x900.png differ diff --git a/docs/design/dockdeck-canon-v1/implementation/settings-apps-dark-desktop-1440x900.png b/docs/design/dockdeck-canon-v1/implementation/settings-apps-dark-desktop-1440x900.png new file mode 100644 index 00000000..adebd51b Binary files /dev/null and b/docs/design/dockdeck-canon-v1/implementation/settings-apps-dark-desktop-1440x900.png differ diff --git a/docs/design/dockdeck-canon-v1/implementation/settings-apps-light-desktop-1440x900.png b/docs/design/dockdeck-canon-v1/implementation/settings-apps-light-desktop-1440x900.png new file mode 100644 index 00000000..e5d2f921 Binary files /dev/null and b/docs/design/dockdeck-canon-v1/implementation/settings-apps-light-desktop-1440x900.png differ diff --git a/docs/design/dockdeck-canon-v1/screens/dashboard-dark-1440x900.png b/docs/design/dockdeck-canon-v1/screens/dashboard-dark-1440x900.png new file mode 100644 index 00000000..164054c6 Binary files /dev/null and b/docs/design/dockdeck-canon-v1/screens/dashboard-dark-1440x900.png differ diff --git a/docs/design/dockdeck-canon-v1/screens/dashboard-light-1440x900.png b/docs/design/dockdeck-canon-v1/screens/dashboard-light-1440x900.png new file mode 100644 index 00000000..512c113b Binary files /dev/null and b/docs/design/dockdeck-canon-v1/screens/dashboard-light-1440x900.png differ diff --git a/docs/design/dockdeck-canon-v1/screens/dashboard-mobile-390x844.png b/docs/design/dockdeck-canon-v1/screens/dashboard-mobile-390x844.png new file mode 100644 index 00000000..b98be4bb Binary files /dev/null and b/docs/design/dockdeck-canon-v1/screens/dashboard-mobile-390x844.png differ diff --git a/docs/design/dockdeck-canon-v1/screens/logo-reference.png b/docs/design/dockdeck-canon-v1/screens/logo-reference.png new file mode 100644 index 00000000..817aa59c Binary files /dev/null and b/docs/design/dockdeck-canon-v1/screens/logo-reference.png differ diff --git a/docs/design/dockdeck-canon-v1/screens/quick-launch-results.png b/docs/design/dockdeck-canon-v1/screens/quick-launch-results.png new file mode 100644 index 00000000..f1b2163c Binary files /dev/null and b/docs/design/dockdeck-canon-v1/screens/quick-launch-results.png differ diff --git a/docs/design/dockdeck-canon-v1/screens/settings-apps-1440x900.png b/docs/design/dockdeck-canon-v1/screens/settings-apps-1440x900.png new file mode 100644 index 00000000..118c393a Binary files /dev/null and b/docs/design/dockdeck-canon-v1/screens/settings-apps-1440x900.png differ diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 00000000..6bfc4bb4 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,34 @@ +import js from "@eslint/js"; +import globals from "globals"; +import reactHooks from "eslint-plugin-react-hooks"; +import reactRefresh from "eslint-plugin-react-refresh"; +import tseslint from "typescript-eslint"; + +export default tseslint.config( + { ignores: ["dist", "coverage", "playwright-report", "test-results"] }, + js.configs.recommended, + ...tseslint.configs.recommended, + { + files: ["scripts/**/*.mjs"], + languageOptions: { + ecmaVersion: 2022, + globals: { ...globals.node, fetch: "readonly" }, + }, + }, + { + files: ["**/*.{ts,tsx}"], + languageOptions: { + ecmaVersion: 2022, + globals: { ...globals.browser, ...globals.node }, + }, + plugins: { "react-hooks": reactHooks, "react-refresh": reactRefresh }, + rules: { + ...reactHooks.configs.recommended.rules, + "react-refresh/only-export-components": [ + "warn", + { allowConstantExport: true }, + ], + "@typescript-eslint/no-explicit-any": "off", + }, + }, +); diff --git a/fixtures/unraid-templates/README.md b/fixtures/unraid-templates/README.md new file mode 100644 index 00000000..9b65edbd --- /dev/null +++ b/fixtures/unraid-templates/README.md @@ -0,0 +1,3 @@ +# Unraid template fixture mount + +This empty directory makes local Compose validation possible. On Unraid, set `UNRAID_TEMPLATES_DIR=/boot/config/plugins/dockerMan/templates-user` so DockDeck can read the real XML templates. diff --git a/index.html b/index.html new file mode 100644 index 00000000..3506ccda --- /dev/null +++ b/index.html @@ -0,0 +1,19 @@ + + + + + + + + + + DockDeck + + +
+ + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..002ddcf1 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,7340 @@ +{ + "name": "dockdeck", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "dockdeck", + "version": "0.1.0", + "dependencies": { + "@fastify/static": "^10.1.0", + "@fontsource-variable/geist": "^5.3.0", + "@fontsource/newsreader": "^5.2.10", + "@icons-pack/react-simple-icons": "13.8.0", + "better-sqlite3": "^12.2.0", + "drizzle-orm": "^0.45.2", + "fastify": "^5.6.1", + "lucide-react": "^0.468.0", + "react": "^19.1.1", + "react-dom": "^19.1.1", + "zod": "^4.1.5" + }, + "devDependencies": { + "@eslint/js": "^9.35.0", + "@playwright/test": "^1.55.0", + "@testing-library/jest-dom": "^6.8.0", + "@testing-library/react": "^16.3.0", + "@types/better-sqlite3": "^7.6.13", + "@types/node": "^24.3.0", + "@types/react": "^19.1.12", + "@types/react-dom": "^19.1.9", + "@vitejs/plugin-react": "^5.0.2", + "concurrently": "^9.2.1", + "esbuild": "^0.25.9", + "eslint": "^9.35.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.20", + "globals": "^16.3.0", + "jsdom": "^26.1.0", + "prettier": "^3.6.2", + "tsx": "^4.20.5", + "typescript": "~5.8.3", + "typescript-eslint": "^8.42.0", + "vite": "^7.1.4", + "vitest": "^3.2.4" + }, + "engines": { + "node": ">=22" + } + }, + "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/@asamuzakjp/css-color/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/@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/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "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/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-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/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "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/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.6.tgz", + "integrity": "sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.3.0", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.5.tgz", + "integrity": "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@fastify/accept-negotiator": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@fastify/accept-negotiator/-/accept-negotiator-2.0.1.tgz", + "integrity": "sha512-/c/TW2bO/v9JeEgoD/g1G5GxGeCF1Hafdf79WPmUlgYiBXummY0oX3VVq4yFkKKVBKDNlaDUYoab7g38RpPqCQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/ajv-compiler": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-4.0.5.tgz", + "integrity": "sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "fast-uri": "^3.0.0" + } + }, + "node_modules/@fastify/ajv-compiler/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@fastify/ajv-compiler/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/@fastify/error": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@fastify/error/-/error-4.2.0.tgz", + "integrity": "sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/fast-json-stringify-compiler": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-5.1.0.tgz", + "integrity": "sha512-PxcYtKLbQ8Z+yApiqjK8FwxIwvEj38k2OiLc17u8dkJSlmfi2wHHPaSnaoqBPQqtvF8YVsDgDpP2snDCfFrpfw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "fast-json-stringify": "^7.0.0" + } + }, + "node_modules/@fastify/forwarded": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@fastify/forwarded/-/forwarded-3.0.1.tgz", + "integrity": "sha512-JqDochHFqXs3C3Ml3gOY58zM7OqO9ENqPo0UqAjAjH8L01fRZqwX9iLeX34//kiJubF7r2ZQHtBRU36vONbLlw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/merge-json-schemas": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.2.1.tgz", + "integrity": "sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/@fastify/proxy-addr": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@fastify/proxy-addr/-/proxy-addr-5.1.0.tgz", + "integrity": "sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/forwarded": "^3.0.0", + "ipaddr.js": "^2.1.0" + } + }, + "node_modules/@fastify/send": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@fastify/send/-/send-4.1.0.tgz", + "integrity": "sha512-TMYeQLCBSy2TOFmV95hQWkiTYgC/SEx7vMdV+wnZVX4tt8VBLKzmH8vV9OzJehV0+XBfg+WxPMt5wp+JBUKsVw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@lukeed/ms": "^2.0.2", + "escape-html": "~1.0.3", + "fast-decode-uri-component": "^1.0.1", + "http-errors": "^2.0.0", + "mime": "^3" + } + }, + "node_modules/@fastify/static": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/@fastify/static/-/static-10.1.0.tgz", + "integrity": "sha512-iK/8TvRM/EgNOyQL+EpWu+x3aR6o4GWt+UI+27zmE7w6t/6d80mXqOtWLdEKQ13vL/g1Jry0ae2icj6GP7tGzA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/accept-negotiator": "^2.0.0", + "@fastify/error": "^4.0.0", + "@fastify/send": "^4.0.0", + "content-disposition": "^2.0.1", + "fastify-plugin": "^6.0.0", + "fastq": "^1.17.1", + "glob": "^13.0.0" + } + }, + "node_modules/@fontsource-variable/geist": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@fontsource-variable/geist/-/geist-5.3.0.tgz", + "integrity": "sha512-j0m+vLQuG5XAYoHtGCVu0spvlGreR3EzpECUVzkFmI1mTVnAO38l/NEPDCFgZ177JxzYJCLSmTQibIiYPilGrA==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@fontsource/newsreader": { + "version": "5.2.10", + "resolved": "https://registry.npmjs.org/@fontsource/newsreader/-/newsreader-5.2.10.tgz", + "integrity": "sha512-TFaYzoFhDqarUyV2yYjgZZEwT4bpaj6sGBnXSnFknQ/QB8/9LzfY6IO9+inHOX4zzPp87Z7/KuG1OI5gr91Q3A==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@icons-pack/react-simple-icons": { + "version": "13.8.0", + "resolved": "https://registry.npmjs.org/@icons-pack/react-simple-icons/-/react-simple-icons-13.8.0.tgz", + "integrity": "sha512-iZrhL1fSklfCCVn68IYHaAoKfcby3RakUTn2tRPyHBkhr2tkYqeQbjJWf+NizIYBzKBn2IarDJXmTdXd6CuEfw==", + "license": "MIT", + "peerDependencies": { + "react": "^16.13 || ^17 || ^18 || ^19" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@lukeed/ms": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@lukeed/ms/-/ms-2.0.2.tgz", + "integrity": "sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", + "license": "MIT" + }, + "node_modules/@playwright/test": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.1.tgz", + "integrity": "sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.61.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", + "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "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", + "peer": true, + "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/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/jest-dom/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/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "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", + "peer": true + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/better-sqlite3": { + "version": "7.6.13", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", + "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz", + "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.17", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", + "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.64.0.tgz", + "integrity": "sha512-CGvQPBxN3wZLu6Rz2kFUpZeoCm78xUic92ck39KPePkO1NPOwjCqdQnm5Q87tpWw9vcBvW8XLrDXjH9PWYtJ3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.64.0", + "@typescript-eslint/type-utils": "8.64.0", + "@typescript-eslint/utils": "8.64.0", + "@typescript-eslint/visitor-keys": "8.64.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.64.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz", + "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.64.0.tgz", + "integrity": "sha512-KA0OshtlcCCXmbfqyZkM5pV3/WNraJf7DkJRLpyrmwPtud57H5BDX7C3k0LPSPxpprfRL+cJDGabF10mvNCoCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.64.0", + "@typescript-eslint/types": "8.64.0", + "@typescript-eslint/typescript-estree": "8.64.0", + "@typescript-eslint/visitor-keys": "8.64.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.64.0.tgz", + "integrity": "sha512-tk4WpOJ6IEbGrVHaNmM0YRrwAD3exZlIK3iadQNAxh4YKk6jvUQ4ecq18n+v7+meh+cJ3j+D8nbk8sRKhlwLQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.64.0", + "@typescript-eslint/types": "^8.64.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.64.0.tgz", + "integrity": "sha512-CXEaFdYXjSTgKhisNkwCcJwTP8Pl+fmRrEQrri4nm3vU743bALrxzLmq7fHG/7e6a5xO0lDYeURpZmBuhHk54w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.64.0", + "@typescript-eslint/visitor-keys": "8.64.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.64.0.tgz", + "integrity": "sha512-2yo8rRNKuzbVWQp5kslhANqZ2uDAeROQHBRZNPu8JDsHmeFNj/XJJhX/FhNUWmkHHvoNsKa6+tHJiig87EzsQw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.64.0.tgz", + "integrity": "sha512-XWG4Fmmv/6SvyS9nH8jWrKs6terwJvE8cyRt1CzYYqzp9OrPhCT4cMc/f7C6RZCwG+qMmiffJS1/qJP8G1URtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.64.0", + "@typescript-eslint/typescript-estree": "8.64.0", + "@typescript-eslint/utils": "8.64.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.64.0.tgz", + "integrity": "sha512-qjhfuTfLXjA4IOzXvz0rTjT01BqEiIgPoUeMwiEjnaHKJMTNo8rH5pYW1a2L/0Dnux2fPC85AeyJoWaGa8WxTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.64.0.tgz", + "integrity": "sha512-Pztpsn1aCE1oWDvDEfUk31nngvvF7vUB5SwHFEaZIFpvw7WJtqUHHL4plBZDA9HfWJJjL13BdG0YrJInTUvoVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.64.0", + "@typescript-eslint/tsconfig-utils": "8.64.0", + "@typescript-eslint/types": "8.64.0", + "@typescript-eslint/visitor-keys": "8.64.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", + "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.64.0.tgz", + "integrity": "sha512-aJUGVB3+U0htrrCjoA8qukw8cm8fNCGAxK/tVoS70k8aeb7DETKeFozRiVFIwEeN9WJLsjaP3ph8I60tY2XZoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.64.0", + "@typescript-eslint/types": "8.64.0", + "@typescript-eslint/typescript-estree": "8.64.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.64.0.tgz", + "integrity": "sha512-mrtuL8Nsn6gi2H4mo5KMTp823M+3Q19Ew/i+Zlikq20tIMm99C3Ez0dCmkWWnxut20esQvTg8aUSEhMcAOXhEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.64.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", + "integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.29.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-rc.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz", + "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz", + "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.7", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", + "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz", + "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.7", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz", + "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz", + "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz", + "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/abstract-logging": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", + "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.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/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "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": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "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/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/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/avvio": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/avvio/-/avvio-9.2.0.tgz", + "integrity": "sha512-2t/sy01ArdHHE0vRH5Hsay+RtCZt3dLPji7W7/MMOCEgze5b7SNDC4j5H6FnVgPkI1MTNFGzHdHrVXDDl7QSSQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/error": "^4.0.0", + "fastq": "^1.17.1" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.43", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.43.tgz", + "integrity": "sha512-AjYpR78kDWAY3Efj+cDTFH9t9SCoL7OoTp1BOb0mQV7S+6CiLwnWM3FyxhJtdPufDFKzmCSFoUncKjWgJEZTCQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/better-sqlite3": { + "version": "12.11.1", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.11.1.tgz", + "integrity": "sha512-dq9AtApgg5PGFtBzPFSBl3HZQjHok5gaQCM6zh2Yk0aSmDCs1CbnVI8/HgASQkNKsWFpseIO9beg5xxpYhbIfA==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": "20.x || 22.x || 23.x || 24.x || 25.x || 26.x" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browserslist": { + "version": "4.28.6", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.6.tgz", + "integrity": "sha512-FQBYNK15VMslhLHpA7+n+n1GOlF1kId2xcCg7/j95f24AOF6VDYMNH4mFxF7KuaTdv627faazpOAjFzMrfJOUw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.42", + "caniuse-lite": "^1.0.30001803", + "electron-to-chromium": "^1.5.389", + "node-releases": "^2.0.51", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "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/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001805", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001805.tgz", + "integrity": "sha512-52noaS3DubycKSXaU30TwPGIp+POyQSUVa5jBEq3vkRkY0kjyb3LQgvhU6WGyCcyXqVLWO0Cw0Q6BSdD0kUfVA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "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/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/concurrently": { + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.4.tgz", + "integrity": "sha512-TZ0CEhyzvFjgtAvHTusDMgj7wNdihCh7LLLrzdUOXIhdlnL2JBBGA9eJxR24rtqgmdjh3OA3hrN1rCHj6HM8qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "4.1.2", + "rxjs": "7.8.2", + "shell-quote": "1.9.0", + "supports-color": "8.1.1", + "tree-kill": "1.2.2", + "yargs": "17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/content-disposition": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-2.0.1.tgz", + "integrity": "sha512-e+H0ZXHSWYrENhQzw1LPuP4oF5MzVKmDU6d3hxlvaPEYLLg62MxtQNPRx4SYSuYJSBUgnQIG4HIN2tEtNv7Dog==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 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/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "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/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "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", + "peer": true + }, + "node_modules/drizzle-orm": { + "version": "0.45.2", + "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.45.2.tgz", + "integrity": "sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q==", + "license": "Apache-2.0", + "peerDependencies": { + "@aws-sdk/client-rds-data": ">=3", + "@cloudflare/workers-types": ">=4", + "@electric-sql/pglite": ">=0.2.0", + "@libsql/client": ">=0.10.0", + "@libsql/client-wasm": ">=0.10.0", + "@neondatabase/serverless": ">=0.10.0", + "@op-engineering/op-sqlite": ">=2", + "@opentelemetry/api": "^1.4.1", + "@planetscale/database": ">=1.13", + "@prisma/client": "*", + "@tidbcloud/serverless": "*", + "@types/better-sqlite3": "*", + "@types/pg": "*", + "@types/sql.js": "*", + "@upstash/redis": ">=1.34.7", + "@vercel/postgres": ">=0.8.0", + "@xata.io/client": "*", + "better-sqlite3": ">=7", + "bun-types": "*", + "expo-sqlite": ">=14.0.0", + "gel": ">=2", + "knex": "*", + "kysely": "*", + "mysql2": ">=2", + "pg": ">=8", + "postgres": ">=3", + "sql.js": ">=1", + "sqlite3": ">=5" + }, + "peerDependenciesMeta": { + "@aws-sdk/client-rds-data": { + "optional": true + }, + "@cloudflare/workers-types": { + "optional": true + }, + "@electric-sql/pglite": { + "optional": true + }, + "@libsql/client": { + "optional": true + }, + "@libsql/client-wasm": { + "optional": true + }, + "@neondatabase/serverless": { + "optional": true + }, + "@op-engineering/op-sqlite": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@prisma/client": { + "optional": true + }, + "@tidbcloud/serverless": { + "optional": true + }, + "@types/better-sqlite3": { + "optional": true + }, + "@types/pg": { + "optional": true + }, + "@types/sql.js": { + "optional": true + }, + "@upstash/redis": { + "optional": true + }, + "@vercel/postgres": { + "optional": true + }, + "@xata.io/client": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "bun-types": { + "optional": true + }, + "expo-sqlite": { + "optional": true + }, + "gel": { + "optional": true + }, + "knex": { + "optional": true + }, + "kysely": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "pg": { + "optional": true + }, + "postgres": { + "optional": true + }, + "prisma": { + "optional": true + }, + "sql.js": { + "optional": true + }, + "sqlite3": { + "optional": true + } + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.389", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.389.tgz", + "integrity": "sha512-cEto7aeOqBfU1D+c5py5pE+ooscKE75JifxLBdFUZsqAxRS6y7kebtxAZvICszSl05gPjYHDTjY+lXpyGvpJbg==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "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-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/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/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.5.tgz", + "integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.6", + "@eslint/js": "9.39.5", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.26", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz", + "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "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/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "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/fast-decode-uri-component": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", + "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stringify": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-7.0.1.tgz", + "integrity": "sha512-eRSayARSbbwlBjpP4vnTTIRD5QPcIrmihPxDeN1DtKnHPg66UuJLx+8hlK1kaFdjvzyQ/dzALoi4vwAQ+T+iZA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/merge-json-schemas": "^0.2.0", + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "fast-uri": "^4.0.0", + "json-schema-ref-resolver": "^3.0.0", + "rfdc": "^1.2.0" + } + }, + "node_modules/fast-json-stringify/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/fast-json-stringify/node_modules/ajv/node_modules/fast-uri": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz", + "integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fast-json-stringify/node_modules/fast-uri": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-4.1.1.tgz", + "integrity": "sha512-YPOs1zD5TG2+EZt+r88LwF6mclA7TPkpwMP7ZN3TO2HiHS8TXvq7QA/17iJsV9dubcLo/f8eEYqMBruyQV21hQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fast-json-stringify/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-querystring": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", + "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", + "license": "MIT", + "dependencies": { + "fast-decode-uri-component": "^1.0.1" + } + }, + "node_modules/fast-uri": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz", + "integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastify": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/fastify/-/fastify-5.10.0.tgz", + "integrity": "sha512-A9L0ziuWGQHgEEVgF3davQ9vbD93IuX+lo2IsxapQmu5b/Y/ynn9m9K5JHt9dvyJXOFc5iN0Zk5GHEOqnzhWjg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/ajv-compiler": "^4.0.5", + "@fastify/error": "^4.0.0", + "@fastify/fast-json-stringify-compiler": "^5.0.0", + "@fastify/proxy-addr": "^5.0.0", + "abstract-logging": "^2.0.1", + "avvio": "^9.0.0", + "fast-json-stringify": "^7.0.0", + "find-my-way": "^9.6.0", + "light-my-request": "^6.0.0", + "pino": "^9.14.0 || ^10.1.0", + "process-warning": "^5.0.0", + "rfdc": "^1.3.1", + "secure-json-parse": "^4.0.0", + "semver": "^7.6.0", + "toad-cache": "^3.7.0" + } + }, + "node_modules/fastify-plugin": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-6.0.0.tgz", + "integrity": "sha512-fZOty7z3O7vOliF6d8bHE3wiEh1KcNnKEQensSgTk9C1DvN6nRLS++XVd86v33Hw/8u9Un8A1zDrQ8ujcQDHEg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/fastify/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "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/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/find-my-way": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-9.6.0.tgz", + "integrity": "sha512-Zf4Xve4RymLl7NgaavNebZ01joJ8MfVerOG43wy7SHLO+r+K0C6d/SE0BiR7AV5V1VOCFlOP7ecdo+I4qmiHrQ==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-querystring": "^1.0.0", + "safe-regex2": "^5.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", + "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "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/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.4.0.tgz", + "integrity": "sha512-9VGk3HGanVE6JoZXHiCpnGy5X0jYDnN4EA4lntFPj+1vIWlFhIylq2CrrCOJH9EAhc5CYhq18F2Av2tgoAPsYQ==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "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/js-yaml": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdom": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-ref-resolver": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-3.0.0.tgz", + "integrity": "sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/light-my-request": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-6.6.0.tgz", + "integrity": "sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause", + "dependencies": { + "cookie": "^1.0.1", + "process-warning": "^4.0.0", + "set-cookie-parser": "^2.6.0" + } + }, + "node_modules/light-my-request/node_modules/process-warning": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.1.tgz", + "integrity": "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "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": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.468.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.468.0.tgz", + "integrity": "sha512-6koYRhnM2N0GGZIdXzSeiNwguv1gt/FAjZOiPl76roBi3xKEXa4WmfpxgQwTTL4KipXjefrnf3oV4IsYhi4JFA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" + } + }, + "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", + "peer": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "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/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-abi": { + "version": "3.94.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.94.0.tgz", + "integrity": "sha512-W5ZNO5KRPB5TkYmGVD9F6YqhsglXJzE6etpbmT+f6EQElhiX/UTG551cnsRGvLG3fyZEg9HwaDmNmj5nwJ4z9g==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-abi/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-releases": { + "version": "2.0.51", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz", + "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "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/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "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/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "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/pino": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz", + "integrity": "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==", + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^3.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^4.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz", + "integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", + "license": "MIT" + }, + "node_modules/playwright": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz", + "integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.61.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz", + "integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/postcss": { + "version": "8.5.19", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.19.tgz", + "integrity": "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==", + "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.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.9.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.5.tgz", + "integrity": "sha512-/FVl766LpUfB5vXgCYOYa0MeV/441Ia99AeICQIQFTY/Nw0roZwULcXpku5i1/m5kt/baz+s4Zogspd839HSMg==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "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", + "peer": true, + "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/pretty-format/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", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/process-warning": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", + "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "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/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", + "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", + "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.7" + } + }, + "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", + "peer": true + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "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/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/ret": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", + "integrity": "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "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.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "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/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-regex2": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-5.1.1.tgz", + "integrity": "sha512-mOSBvHGDZMuIEZMdOz/aCEYDCv0E7nfcNsIhUF+/P+xC7Hyf3FkvymqgPbg9D1EdSGu+uKbJgy09K/RKKc7kJA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "ret": "~0.5.0" + }, + "bin": { + "safe-regex2": "bin/safe-regex2.js" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "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/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/secure-json-parse": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz", + "integrity": "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.9.0.tgz", + "integrity": "sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/sonic-boom": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "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/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "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/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "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/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "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/tar-fs": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz", + "integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/thread-stream": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.2.0.tgz", + "integrity": "sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==", + "license": "MIT", + "dependencies": { + "real-require": "^1.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/thread-stream/node_modules/real-require": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-1.0.0.tgz", + "integrity": "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==", + "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": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "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/toad-cache": { + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.4.tgz", + "integrity": "sha512-m1TdR/rvT7kgGJZhspNtXdsdYk0fddFpJJFlG5s+UkPFo6lkLoZ3YLOaovPYjq1R75NP5JfeTlSHaOsE09peCg==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "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/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/tsx": { + "version": "4.23.1", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.1.tgz", + "integrity": "sha512-GQHnkIfxyx1wYCOS/wonik5MVRZU9hi1TEZmzGZSCJB1y9YgoZ8H6itNE/u4suE+yLmOzuE4E5S4TZ/ZX2wcWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tsx/node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/tsx/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/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.64.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.64.0.tgz", + "integrity": "sha512-0qg+pDNMnqYzqH9AnNK+39tejHvsShUOUUoRUgtnTGE7QuMZhiFDnozq8nHJVq+Wae6NMLKNWLg5WmkcC/ndyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.64.0", + "@typescript-eslint/parser": "8.64.0", + "@typescript-eslint/typescript-estree": "8.64.0", + "@typescript-eslint/utils": "8.64.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/vite": { + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", + "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0 || ^0.28.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/vite/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/vitest": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz", + "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.7", + "@vitest/mocker": "3.2.7", + "@vitest/pretty-format": "^3.2.7", + "@vitest/runner": "3.2.7", + "@vitest/snapshot": "3.2.7", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.7", + "@vitest/ui": "3.2.7", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "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/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "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/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "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/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..1371b0f7 --- /dev/null +++ b/package.json @@ -0,0 +1,62 @@ +{ + "name": "dockdeck", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "concurrently -k -n api,web -c cyan,magenta \"tsx watch src/server/index.ts\" \"vite --host 127.0.0.1\"", + "dev:server": "tsx watch src/server/index.ts", + "dev:web": "vite --host 127.0.0.1", + "build": "tsc -b && vite build && esbuild src/server/index.ts --bundle --platform=node --format=esm --packages=external --outfile=dist/server/index.js", + "start": "node dist/server/index.js", + "format": "prettier --write .", + "format:check": "prettier --check .", + "lint": "eslint . --max-warnings=0", + "typecheck": "tsc -b --pretty false", + "test": "vitest run", + "test:watch": "vitest", + "test:e2e": "playwright test", + "test:security": "vitest run src/tests/security.test.ts", + "gitea:init": "node scripts/init-gitea.mjs" + }, + "dependencies": { + "@fastify/static": "^10.1.0", + "@fontsource-variable/geist": "^5.3.0", + "@fontsource/newsreader": "^5.2.10", + "@icons-pack/react-simple-icons": "13.8.0", + "better-sqlite3": "^12.2.0", + "drizzle-orm": "^0.45.2", + "fastify": "^5.6.1", + "lucide-react": "^0.468.0", + "react": "^19.1.1", + "react-dom": "^19.1.1", + "zod": "^4.1.5" + }, + "devDependencies": { + "@eslint/js": "^9.35.0", + "@playwright/test": "^1.55.0", + "@testing-library/jest-dom": "^6.8.0", + "@testing-library/react": "^16.3.0", + "@types/better-sqlite3": "^7.6.13", + "@types/node": "^24.3.0", + "@types/react": "^19.1.12", + "@types/react-dom": "^19.1.9", + "@vitejs/plugin-react": "^5.0.2", + "concurrently": "^9.2.1", + "esbuild": "^0.25.9", + "eslint": "^9.35.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.20", + "globals": "^16.3.0", + "jsdom": "^26.1.0", + "prettier": "^3.6.2", + "tsx": "^4.20.5", + "typescript": "~5.8.3", + "typescript-eslint": "^8.42.0", + "vite": "^7.1.4", + "vitest": "^3.2.4" + }, + "engines": { + "node": ">=22" + } +} diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 00000000..60ca4ca8 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,59 @@ +import { defineConfig, devices } from "@playwright/test"; + +export default defineConfig({ + testDir: "./tests", + fullyParallel: false, + workers: 1, + retries: 0, + reporter: "list", + use: { + baseURL: "http://127.0.0.1:5174", + trace: "retain-on-failure", + }, + webServer: { + command: "npm run dev", + url: "http://127.0.0.1:3101/api/health", + reuseExistingServer: false, + timeout: 120_000, + env: { + MOCK_DISCOVERY: "true", + MOCK_WEATHER: "true", + DATABASE_PATH: "./data/e2e-canon-v1.db", + HOST: "127.0.0.1", + PORT: "3101", + VITE_API_PORT: "3101", + VITE_PORT: "5174", + }, + }, + projects: [ + { + name: "desktop-chromium", + use: { + ...devices["Desktop Chrome"], + viewport: { width: 1440, height: 900 }, + }, + }, + { + name: "tablet-chromium", + use: { + ...devices["Desktop Chrome"], + viewport: { width: 768, height: 1024 }, + }, + }, + { + name: "mobile-chromium", + use: { + ...devices["Pixel 7"], + viewport: { width: 390, height: 844 }, + deviceScaleFactor: 1, + }, + }, + { + name: "ultrawide-chromium", + use: { + ...devices["Desktop Chrome"], + viewport: { width: 3440, height: 1440 }, + }, + }, + ], +}); diff --git a/project-launchpad.json b/project-launchpad.json new file mode 100644 index 00000000..3bfd9804 --- /dev/null +++ b/project-launchpad.json @@ -0,0 +1,40 @@ +{ + "schemaVersion": 1, + "generatedAt": "2026-07-13T18:54:16.667Z", + "rootFolderName": "dockdeck", + "exportProfile": { + "id": "codex-full", + "label": "Codex volledig" + }, + "codexStartMode": { + "id": "build-mvp", + "label": "MVP bouwen" + }, + "input": { + "projectName": "DockDeck", + "shortDescription": "Een premium ogend persoonlijk startdashboard voor Unraid dat Docker-apps automatisch ontdekt, stijlvol groepeert en snelle navigatie biedt naar lokale diensten en eigen favorieten.", + "projectIdea": "Bouw een persoonlijke webapp die als standaard startpagina van de browser dient en een prachtig, professioneel en strak afgewerkt overzicht geeft van de diensten op de Unraid-server. De app ontdekt Docker-containers automatisch via Unraid en een beperkte read-only Docker-socketproxy, maar toont nieuwe containers niet automatisch op het hoofddashboard. In het instellingenmenu kan de gebruiker per container zichtbaarheid, categorie, sortering, icoon, naam en URL beheren. Voor Docker-apps wordt standaard de lokale WebUI-URL uit de Unraid-configuratie gebruikt. Daarnaast kunnen handmatig favoriete externe websites worden toegevoegd met naam, URL, icoon, categorie en sorteerpositie. Het dashboard bevat een centrale zoekbalk die zowel zichtbare Docker-apps als favorieten doorzoekt en, wanneer geen lokaal resultaat wordt gekozen, de invoer als Google-zoekopdracht in een nieuw tabblad opent. De gebruiker kan kiezen tussen één overzicht met categorieën onder elkaar en een weergave met afzonderlijke categoriepagina's of tabbladen. Thema's, weergavevoorkeuren en openingsgedrag worden via instellingen beheerd. Externe toegang kan optioneel worden ingeschakeld; wanneer die actief is, haalt een read-only koppeling met Nginx Proxy Manager beschikbare externe URL's op en koppelt die aan de juiste apps, met steeds een handmatige override als terugval. De UI moet aanvoelen als een hoogwaardige, moderne productinterface met sterke visuele hiërarchie, nette details, subtiele animaties en een verzorgde dashboardervaring.", + "targetUsers": "Eén primaire gebruiker: de eigenaar en beheerder van de Unraid-server. Deze gebruiker verwacht zonder technische omwegen snel naar lokale Docker-diensten en favoriete websites te kunnen navigeren, zelf te bepalen welke apps zichtbaar zijn, categorieën en sortering te beheren, de startpagina visueel aan te passen en optioneel lokale of externe app-URL's te gebruiken. Er zijn in de MVP geen afzonderlijke gebruikersrollen, accounts of gedeelde profielen.", + "problemToSolve": "De gebruiker heeft meerdere Docker-diensten op een Unraid-server en moet momenteel afzonderlijke URL's onthouden, bladwijzers beheren of via verschillende beheerinterfaces navigeren. Bestaande startpagina's sluiten niet volledig aan op de gewenste combinatie van automatische Unraid-detectie, configureerbare zichtbaarheid, categorie-indeling, statusweergave, lokale en externe URL's, favoriete websites, geïntegreerde zoekfunctie en een echt professioneel ogende premium UI.", + "desiredOutcome": "De eerste bruikbare versie draait als Docker-container op Unraid en kan als browserstartpagina worden ingesteld. Na de eerste start ontdekt de app de beschikbare Docker-containers, toont ze in een instellingenoverzicht en laat de gebruiker selecteren welke apps op het dashboard verschijnen. Zichtbare apps staan netjes gegroepeerd in configureerbare categorieën, tonen naam, icoon en actuele online- of offline-status en openen hun lokale WebUI in een nieuw tabblad. Favoriete websites kunnen handmatig worden toegevoegd. De zoekbalk vindt apps en favorieten en kan rechtstreeks een Google-zoekopdracht starten. Alle instellingen blijven na een herstart behouden en kunnen worden geëxporteerd en opnieuw geïmporteerd. De totale ervaring moet eruitzien als een afgewerkt premium product dat de gebruiker met plezier dagelijks als startpagina gebruikt.", + "successCriteria": "De MVP is klaar wanneer: de applicatie via Docker Compose op Unraid kan worden gestart; beschikbare Docker-containers automatisch worden ontdekt zonder schrijf- of beheerrechten op Docker; nieuw ontdekte containers standaard verborgen blijven; de gebruiker containers zichtbaar of verborgen kan maken; naam, icoon, categorie, sortering en lokale URL per app kunnen worden aangepast; de lokale WebUI-URL waar mogelijk automatisch uit Unraid-templategegevens wordt afgeleid; zichtbare apps naam, icoon en correcte online- of offline-status tonen; status bij het openen en daarna ongeveer elke 30 seconden wordt vernieuwd; categorieën kunnen worden aangemaakt, hernoemd, gesorteerd en toegewezen; zowel een volledig overzicht als categoriegebaseerde navigatie configureerbaar is; favorieten met naam, URL, icoon, categorie en sortering kunnen worden beheerd; de zoekbalk apps en favorieten doorzoekt en overige invoer naar Google stuurt; alle links in een nieuw tabblad openen; licht, donker en minstens één aanvullend thema selecteerbaar zijn; configuratie persistent blijft na container- en serverherstart; back-up en herstel via export en import werken; de app uitsluitend voor lokaal gebruik is ingericht en geen container start-, stop-, restart- of wijzigingsacties bevat; een private Gitea-repository automatisch wordt aangemaakt, lokaal wordt geïnitialiseerd en een eerste bruikbare commit bevat; en de UI een consistent, professioneel en visueel hoogwaardig geheel vormt met nette kaartlay-outs, sterke typografie, duidelijke statusindicatoren, vloeiende maar subtiele interacties en een verzorgde instellingenervaring.", + "mustHaves": "Automatische read-only ontdekking van Unraid Docker-containers; nieuwe containers standaard verborgen; instellingenpagina voor zichtbaarheid, naam, icoon, categorie, sortering en URL-overrides; automatische afleiding van lokale WebUI-URL's uit Unraid-templategegevens; read-only containerstatus met verversing bij laden en ongeveer elke 30 seconden; configureerbare categorieën; configureerbare keuze tussen één totaaloverzicht en categoriepagina's of tabbladen; handmatige favorieten voor externe websites; centrale zoekbalk voor apps, favorieten en Google; openen van alle doelen in een nieuw tabblad; licht, donker en aanvullende ingebouwde thema's; persistent bewaren van instellingen in SQLite; export en import van configuratie; Docker- en Docker Compose-configuratie voor Unraid; beperkte Docker-socketproxy zonder beheerrechten; optionele read-only Nginx Proxy Manager-koppeling voor externe URL's met handmatige fallback; duidelijke foutstatus wanneer Unraid, Docker-proxy of Nginx Proxy Manager tijdelijk onbereikbaar is; automatische initialisatie van een private Gitea-repository via environmentvariabelen; en een uitzonderlijk verzorgde professionele UI met premium dashboardlook, consistente componenten, strak kaartdesign, goede witruimte, duidelijke visuele hiërarchie, subtiele hover- en transition-effecten, nette iconografie en een afgewerkte instellingenomgeving.", + "niceToHaves": "Drag-and-drop sortering van apps en categorieën; automatisch herkennen van bekende applicatie-iconen; favicon-import voor favorieten; meerdere aanvullende kleurthema's; compacte en ruime tegelweergave; achtergrondafbeeldingen of subtiele dashboard-achtergronden; sneltoets om de zoekbalk te focussen; recente of meest gebruikte apps; optionele statuslatentie en responstijd; import van bestaande browserbladwijzers; meerdere dashboards of profielen; PWA-installatie; mobiele optimalisaties buiten de basisresponsiviteit; geavanceerde automatische matching tussen Nginx Proxy Manager-hosts en Docker-containers; handmatige statuscontrole voor favoriete websites; extra micro-interacties en visuele personalisatieopties zolang die de strakke premium uitstraling ondersteunen.", + "outOfScope": "Containers starten, stoppen, herstarten, verwijderen of wijzigen; Docker Compose-stacks beheren; Unraid-systeembeheer; terminal- of shelltoegang; logviewer; resourcegrafieken en uitgebreide monitoring; notificaties en incidentbeheer; publieke internettoegang configureren; Authentik- of andere SSO-integratie; eigen gebruikersaccounts, rollen of multi-userondersteuning; opslag van echte API-tokens in de repository of browser; automatische wijzigingen aan Nginx Proxy Manager; cloudhosting; mobiele native apps; AI-functionaliteit; synchronisatie tussen meerdere Unraid-servers; automatische publicatie van diensten naar het internet.", + "constraints": "De MVP moet als Docker-container op de bestaande Unraid-server draaien en alleen vanaf het lokale netwerk bereikbaar zijn. De app moet zonder eigen login bruikbaar zijn, omdat netwerktoegang de primaire beveiligingsgrens vormt. Alle Docker-toegang moet read-only verlopen via een beperkte socketproxy; de hoofdcontainer krijgt geen directe onbeperkte Docker-socket. Unraid-templategegevens mogen alleen worden gelezen. Nginx Proxy Manager wordt uitsluitend read-only benaderd. De applicatie moet bruikbaar blijven wanneer Nginx Proxy Manager niet is geconfigureerd of tijdelijk niet bereikbaar is. Er mogen geen betaalde externe diensten nodig zijn. Alle configuratie moet persistent blijven bij herstart en eenvoudig te back-uppen zijn. Codex moet een nieuwe private repository in de persoonlijke Gitea-omgeving aanmaken en mag geen secrets hardcoderen. Visuele kwaliteit is belangrijk, maar de UI mag niet onnodig zwaar of traag worden; professionele polish moet samengaan met vlotte prestaties als browserstartpagina.", + "projectType": "webapp", + "runtimeModel": "interactive-app", + "updateStrategy": "polling", + "persistenceModel": "sqlite", + "deploymentTarget": "docker-unraid", + "repositoryState": "repo-needed", + "preferredStack": "TypeScript als hoofdtaal; React met Vite voor de frontend; Node.js met Fastify voor de backend; SQLite met Drizzle ORM voor persistentie en migraties; gedeelde types en validaties met Zod; een eenvoudige REST-API tussen frontend en backend; een zorgvuldig opgebouwd design system met design tokens, CSS-variabelen en herbruikbare UI-componenten; een moderne componentbibliotheek of headless aanpak voor toegankelijke basiscomponenten; iconenset van hoge kwaliteit; subtiele motion via lichte animatielogica; Docker multi-stage build en Docker Compose voor Unraid; Vitest voor unit- en integratietests; Playwright voor browsertests. Gebruik een overzichtelijke repository met duidelijk gescheiden frontend-, backend- en shared-mappen. Richt de frontend expliciet in op een premium dashboardervaring met consistente spacing, kaartsystemen, typografie, kleurgebruik, statusbadges en instellingenpanelen.", + "dataInvolved": "Docker-apps met container-ID, technische naam, weergavenaam, afbeelding, ontdekt icoon, online- of offline-status, lokale WebUI-URL, optionele externe URL, zichtbaarheid, categorie, sorteerpositie, detectiebron en laatste statuscontrole; categorieën met naam, icoon, sorteerpositie en weergave-instellingen; favorieten met naam, URL, icoon, categorie en sorteerpositie; dashboardinstellingen met gekozen thema, weergavemodus, openingsgedrag, zoekprovider, pollinginterval en externe-toegangsschakelaar; integratie-instellingen zonder geheime waarden; import- en exportbestand met versieerbaar configuratieschema; synchronisatiestatus en beperkte foutinformatie voor Unraid, Docker-proxy en Nginx Proxy Manager.", + "integrations": "Unraid Docker-templatebestanden of beschikbare Unraid-metadata voor containernaam, WebUI en iconen; een beperkte read-only Docker Socket Proxy voor containerinventaris en runtime-status; Nginx Proxy Manager API in read-only modus voor optionele externe hostnamen en URL's; Google Search via een gewone zoek-URL zonder API-key; Gitea API voor het aanmaken van een private repository en het configureren van de Git-remote.", + "credentialsExpected": true, + "securityPrivacyNotes": "Gebruik uitsluitend environmentvariabelen voor gevoelige configuratie. Voorzie minimaal variabelen zoals GITEA_URL, GITEA_TOKEN en GITEA_OWNER voor repository-initialisatie en optionele NPM_URL, NPM_USERNAME en NPM_PASSWORD of een ondersteund read-only token voor Nginx Proxy Manager. Gebruik voor de Docker-koppeling geen onbeperkte socketmount in de applicatiecontainer, maar een afzonderlijke proxy met alleen de minimaal noodzakelijke read-endpoints. Sla secrets nooit op in SQLite, exports, logs, frontendbundels, testfixtures of Git. Toon gevoelige integratiewaarden gemaskeerd in de UI. De app mag geen Docker-mutaties uitvoeren en mag nooit containers starten, stoppen, herstarten, verwijderen of aanpassen. De MVP heeft geen eigen authenticatie en moet daarom standaard alleen op een lokaal bindadres of afgeschermd Docker-netwerk worden gepubliceerd. Externe toegang via Nginx Proxy Manager wordt alleen voorbereid; correcte authenticatie en toegangscontrole via bijvoorbeeld Authentik blijven de verantwoordelijkheid van een latere deploymentstap.", + "qualityBar": "De interface moet eruitzien als een professioneel premium product en niet als een simpel hobbydashboard. Verwacht een moderne, luxueuze en verzorgde UI met uitgebalanceerde witruimte, consistente kaartcomponenten, hoogwaardige iconografie, duidelijke typografie, nette schaduwen of diepte waar passend, subtiele animaties, verzorgde empty states, sterke visuele hiërarchie en een kalme maar rijke look-and-feel. De belangrijkste startpagina moet snel laden en direct bruikbaar zijn wanneer integraties traag of tijdelijk offline zijn. Fouten moeten begrijpelijk worden getoond zonder technische stacktraces. Ontbrekende iconen of URL's moeten nette fallbacks hebben. Instellingen moeten veilig opgeslagen worden en bij foutieve invoer gevalideerde feedback geven. De interface moet responsief zijn voor desktop, tablet en basisgebruik op mobiel. Toetsenbordnavigatie, zichtbare focus, semantische HTML en voldoende kleurcontrast zijn vereist. Statuspolling mag geen zichtbare flikkering veroorzaken en moet stoppen wanneer de pagina niet actief is of verstandig worden vertraagd. De code moet modulair, getypeerd, gedocumenteerd en zonder onnodige complexiteit zijn. Esthetische kwaliteit is voor deze MVP een kernvereiste en niet louter een extra.", + "testingExpectations": "Unit tests voor URL-normalisatie, categorie- en sorteervoorwaarden, zoekresultaten, configuratievalidatie, import- en exportversies, container-naar-Nginx-hostmatching en permissiegrenzen; backend-integratietests met gemockte Unraid-templategegevens, Docker Socket Proxy en Nginx Proxy Manager; SQLite-migratie- en persistentietests; tests die aantonen dat geen Docker-mutatie-endpoints of beheeracties worden gebruikt; browsertests voor eerste ingebruikname, containers zichtbaar maken, categorieën beheren, favorieten toevoegen, zoeken, Google-fallback, thema wisselen, weergavemodus wijzigen, URL openen en configuratie exporteren en importeren; visuele UI-smoketests voor kernschermen zoals dashboard, categorie-overzicht en instellingen; Docker-buildtest en Compose-smoketest; een live read-only validatie op Unraid waarbij discovery, status en lokale WebUI-URL's worden gecontroleerd zonder containers te wijzigen.", + "knownRisks": "Unraid-templatebestanden en WebUI-velden kunnen per container of template verschillen, waardoor URL-detectie fallbacks en handmatige overrides nodig zijn. Niet iedere container heeft een WebUI, correct icoon of eenvoudig herkenbare poort. Het automatisch koppelen van Nginx Proxy Manager-hosts aan containers kan dubbelzinnig zijn en moet daarom transparant, overschrijfbaar en niet-destructief blijven. Een lokale app zonder login is alleen veilig zolang netwerktoegang correct is afgeschermd. Toegang tot Docker-informatie blijft gevoelig, zelfs in read-only modus, waardoor de socketproxy strikt moet worden beperkt. Gitea-repositorycreatie kan mislukken door ontbrekende rechten, een bestaande naam of onbereikbaarheid; de lokale repository moet dan intact blijven en een duidelijke herstelstap tonen. Hoge UI-ambities kunnen de scope doen groeien door eindeloos finetunen van styling en details; daarom moet de premium uitstraling via een consistent design system en duidelijke acceptatiecriteria bewaakt worden zonder de kernfunctionaliteit te vertragen." + } +} diff --git a/public/dockdeck-icon.png b/public/dockdeck-icon.png new file mode 100644 index 00000000..75ef90ce Binary files /dev/null and b/public/dockdeck-icon.png differ diff --git a/public/dockdeck-icon.svg b/public/dockdeck-icon.svg new file mode 100644 index 00000000..f18f72ca --- /dev/null +++ b/public/dockdeck-icon.svg @@ -0,0 +1,21 @@ + + DockDeck + + + + + + + + + + + + + + + + + + + diff --git a/public/dockdeck-mark-dark.svg b/public/dockdeck-mark-dark.svg new file mode 100644 index 00000000..91f46a1b --- /dev/null +++ b/public/dockdeck-mark-dark.svg @@ -0,0 +1,6 @@ + + DockDeck beeldmerk voor donkere achtergrond + + + + diff --git a/public/dockdeck-mark-light.svg b/public/dockdeck-mark-light.svg new file mode 100644 index 00000000..715b57a6 --- /dev/null +++ b/public/dockdeck-mark-light.svg @@ -0,0 +1,6 @@ + + DockDeck beeldmerk voor lichte achtergrond + + + + diff --git a/public/dockdeck-mark-monochrome.svg b/public/dockdeck-mark-monochrome.svg new file mode 100644 index 00000000..da802407 --- /dev/null +++ b/public/dockdeck-mark-monochrome.svg @@ -0,0 +1,6 @@ + + DockDeck monochroom beeldmerk + + + + diff --git a/public/dockdeck-mark.svg b/public/dockdeck-mark.svg new file mode 100644 index 00000000..09526d8e --- /dev/null +++ b/public/dockdeck-mark.svg @@ -0,0 +1,7 @@ + + DockDeck + + + + + diff --git a/public/dockdeck-wordmark.svg b/public/dockdeck-wordmark.svg new file mode 100644 index 00000000..bede9662 --- /dev/null +++ b/public/dockdeck-wordmark.svg @@ -0,0 +1,9 @@ + + DockDeck woordmerk + + + + + + DockDeck + diff --git a/public/manifest.webmanifest b/public/manifest.webmanifest new file mode 100644 index 00000000..8beb741a --- /dev/null +++ b/public/manifest.webmanifest @@ -0,0 +1,17 @@ +{ + "name": "DockDeck", + "short_name": "DockDeck", + "description": "Je persoonlijke startpunt voor apps en favorieten.", + "start_url": "/", + "display": "standalone", + "background_color": "#0b1326", + "theme_color": "#818cf8", + "icons": [ + { + "src": "/dockdeck-mark.svg", + "sizes": "any", + "type": "image/svg+xml", + "purpose": "any maskable" + } + ] +} diff --git a/public/sw.js b/public/sw.js new file mode 100644 index 00000000..85f45cf3 --- /dev/null +++ b/public/sw.js @@ -0,0 +1,58 @@ +/* global self, caches, fetch, URL */ + +const CACHE = "dockdeck-shell-v1"; +const SHELL = ["/", "/manifest.webmanifest", "/dockdeck-icon.svg"]; + +self.addEventListener("install", (event) => { + event.waitUntil(caches.open(CACHE).then((cache) => cache.addAll(SHELL))); + self.skipWaiting(); +}); + +self.addEventListener("activate", (event) => { + event.waitUntil( + caches + .keys() + .then((keys) => + Promise.all( + keys.filter((key) => key !== CACHE).map((key) => caches.delete(key)), + ), + ), + ); + self.clients.claim(); +}); + +self.addEventListener("fetch", (event) => { + const request = event.request; + const url = new URL(request.url); + if ( + request.method !== "GET" || + url.origin !== self.location.origin || + url.pathname.startsWith("/api/") + ) + return; + if (request.mode === "navigate") { + event.respondWith( + fetch(request) + .then((response) => { + const copy = response.clone(); + caches.open(CACHE).then((cache) => cache.put("/", copy)); + return response; + }) + .catch(() => caches.match("/")), + ); + return; + } + event.respondWith( + caches.match(request).then( + (cached) => + cached || + fetch(request).then((response) => { + if (response.ok) + caches + .open(CACHE) + .then((cache) => cache.put(request, response.clone())); + return response; + }), + ), + ); +}); diff --git a/scripts/backend_dev.sh b/scripts/backend_dev.sh old mode 100755 new mode 100644 diff --git a/scripts/backend_install.sh b/scripts/backend_install.sh old mode 100755 new mode 100644 diff --git a/scripts/backend_test.sh b/scripts/backend_test.sh old mode 100755 new mode 100644 diff --git a/scripts/capture_workbench_screenshots.sh b/scripts/capture_workbench_screenshots.sh old mode 100755 new mode 100644 diff --git a/scripts/check_repo_structure.sh b/scripts/check_repo_structure.sh old mode 100755 new mode 100644 diff --git a/scripts/codex_pass_end_check.sh b/scripts/codex_pass_end_check.sh old mode 100755 new mode 100644 diff --git a/scripts/codex_preflight.sh b/scripts/codex_preflight.sh old mode 100755 new mode 100644 diff --git a/scripts/contract_drift_grep.sh b/scripts/contract_drift_grep.sh old mode 100755 new mode 100644 diff --git a/scripts/deploy_tower.sh b/scripts/deploy_tower.sh old mode 100755 new mode 100644 diff --git a/scripts/frontend_build.sh b/scripts/frontend_build.sh old mode 100755 new mode 100644 diff --git a/scripts/frontend_dev.sh b/scripts/frontend_dev.sh old mode 100755 new mode 100644 diff --git a/scripts/frontend_install.sh b/scripts/frontend_install.sh old mode 100755 new mode 100644 diff --git a/scripts/frontend_typecheck.sh b/scripts/frontend_typecheck.sh old mode 100755 new mode 100644 diff --git a/scripts/init-gitea.mjs b/scripts/init-gitea.mjs new file mode 100644 index 00000000..5c7f039b --- /dev/null +++ b/scripts/init-gitea.mjs @@ -0,0 +1,86 @@ +import { execFileSync } from "node:child_process"; + +const required = ["GITEA_URL", "GITEA_OWNER", "GITEA_TOKEN"]; +const missing = required.filter((name) => !process.env[name]); +if (missing.length) { + console.error( + `Missing required environment variables: ${missing.join(", ")}`, + ); + process.exit(2); +} + +const base = process.env.GITEA_URL.replace(/\/$/, ""); +const owner = process.env.GITEA_OWNER; +const token = process.env.GITEA_TOKEN; +const name = "DockDeck"; +const headers = { + Authorization: `token ${token}`, + "content-type": "application/json", +}; + +let response = await fetch( + `${base}/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}`, + { headers }, +); +if (response.status === 404) { + response = await fetch(`${base}/api/v1/user/repos`, { + method: "POST", + headers, + body: JSON.stringify({ + name, + private: true, + auto_init: false, + description: "A premium personal launchpad for Unraid services.", + }), + }); +} +if (!response.ok) { + console.error( + `Gitea repository initialization failed with HTTP ${response.status}.`, + ); + process.exit(1); +} + +const repository = await response.json(); +if (!repository.private || repository.owner?.login !== owner) { + console.error( + "Gitea returned a repository with an unexpected owner or visibility. Aborting.", + ); + process.exit(1); +} + +const remoteUrl = `${base}/${encodeURIComponent(owner)}/${name}.git`; +let currentRemote = ""; +try { + currentRemote = execFileSync("git", ["remote", "get-url", "origin"], { + encoding: "utf8", + }).trim(); +} catch { + /* no origin yet */ +} +if (currentRemote && currentRemote !== remoteUrl) { + console.error( + "An origin remote already exists with a different URL. No changes were made.", + ); + process.exit(1); +} +if (!currentRemote) + execFileSync("git", ["remote", "add", "origin", remoteUrl], { + stdio: "inherit", + }); + +execFileSync( + "git", + [ + "-c", + `http.extraHeader=Authorization: token ${token}`, + "push", + "-u", + "origin", + "main", + ], + { + stdio: ["ignore", "inherit", "inherit"], + }, +); +console.log(`Private repository ready at ${base}/${owner}/${name}`); diff --git a/scripts/live_migration_smoke.sh b/scripts/live_migration_smoke.sh old mode 100755 new mode 100644 diff --git a/scripts/m7_self_review.sh b/scripts/m7_self_review.sh old mode 100755 new mode 100644 diff --git a/scripts/run_readiness_check.sh b/scripts/run_readiness_check.sh old mode 100755 new mode 100644 diff --git a/scripts/smoke_backend_import.sh b/scripts/smoke_backend_import.sh old mode 100755 new mode 100644 diff --git a/scripts/smoke_day1.sh b/scripts/smoke_day1.sh old mode 100755 new mode 100644 diff --git a/scripts/smoke_m10.sh b/scripts/smoke_m10.sh old mode 100755 new mode 100644 diff --git a/scripts/verify_ai_handoff_interactions.sh b/scripts/verify_ai_handoff_interactions.sh old mode 100755 new mode 100644 diff --git a/scripts/verify_browser_runtime.sh b/scripts/verify_browser_runtime.sh old mode 100755 new mode 100644 diff --git a/scripts/verify_demo_cleanup_dry_run.sh b/scripts/verify_demo_cleanup_dry_run.sh old mode 100755 new mode 100644 diff --git a/scripts/verify_demo_export_workflow.sh b/scripts/verify_demo_export_workflow.sh old mode 100755 new mode 100644 diff --git a/scripts/verify_gis_runtime.sh b/scripts/verify_gis_runtime.sh old mode 100755 new mode 100644 diff --git a/src/client/App.tsx b/src/client/App.tsx new file mode 100644 index 00000000..bb4b2924 --- /dev/null +++ b/src/client/App.tsx @@ -0,0 +1,210 @@ +import { + Check, + Grid2X2, + LayoutDashboard, + Settings as SettingsIcon, +} from "lucide-react"; +import { + useCallback, + useEffect, + useLayoutEffect, + useRef, + useState, +} from "react"; +import type { DashboardPayload } from "../shared/contracts"; +import { api } from "./api"; +import { Dashboard, DashboardSkeleton } from "./components/Dashboard"; +import { Settings, type SettingsSection } from "./components/Settings"; +import { Brand } from "./components/Brand"; + +type Page = "dashboard" | "settings"; + +export function App() { + const [page, setPage] = useState("dashboard"); + const [settingsSection, setSettingsSection] = + useState("apps"); + const [data, setData] = useState(null); + const [error, setError] = useState(null); + const [syncing, setSyncing] = useState(false); + const [toast, setToast] = useState(null); + const toastTimerRef = useRef(undefined); + const syncInFlightRef = useRef | null>(null); + const theme = data?.preferences.theme; + const accent = data?.preferences.accent; + const density = data?.preferences.density; + const contentWidth = data?.preferences.contentWidth; + const backgroundStyle = data?.preferences.backgroundStyle; + const reducedMotion = data?.preferences.reducedMotion; + const pollingIntervalMs = data + ? Math.min( + data.preferences.pollingIntervalMs, + ...data.apps + .filter((app) => app.widgetEnabled) + .map((app) => app.widgetRefreshIntervalMs), + ) + : undefined; + + const load = useCallback(async () => { + try { + const payload = await api.dashboard(); + setData(payload); + setError(null); + } catch (loadError) { + setError( + loadError instanceof Error + ? loadError.message + : "DockDeck is niet bereikbaar.", + ); + } + }, []); + + const sync = useCallback(async () => { + if (syncInFlightRef.current) return syncInFlightRef.current; + const request = (async () => { + setSyncing(true); + try { + await api.sync(); + await load(); + } catch (syncError) { + setError( + syncError instanceof Error + ? syncError.message + : "De actuele status kon niet worden vernieuwd.", + ); + } finally { + setSyncing(false); + syncInFlightRef.current = null; + } + })(); + syncInFlightRef.current = request; + return request; + }, [load]); + + useEffect(() => { + void load().then(sync); + }, [load, sync]); + + useLayoutEffect(() => { + if (!theme || !accent || !density || !contentWidth || !backgroundStyle) + return; + const root = document.documentElement; + root.dataset.theme = theme; + root.dataset.accent = accent; + root.dataset.density = density; + root.dataset.contentWidth = contentWidth; + root.dataset.background = backgroundStyle; + root.dataset.reducedMotion = String(Boolean(reducedMotion)); + }, [accent, backgroundStyle, contentWidth, density, reducedMotion, theme]); + + useEffect(() => { + if (!pollingIntervalMs) return; + const refreshWhenVisible = () => { + if (document.visibilityState === "visible") void sync(); + }; + const timer = window.setInterval(refreshWhenVisible, pollingIntervalMs); + document.addEventListener("visibilitychange", refreshWhenVisible); + window.addEventListener("online", refreshWhenVisible); + return () => { + window.clearInterval(timer); + document.removeEventListener("visibilitychange", refreshWhenVisible); + window.removeEventListener("online", refreshWhenVisible); + }; + }, [pollingIntervalMs, sync]); + + const notify = (message: string) => { + window.clearTimeout(toastTimerRef.current); + setToast(message); + toastTimerRef.current = window.setTimeout(() => setToast(null), 2400); + }; + + useEffect( + () => () => { + window.clearTimeout(toastTimerRef.current); + }, + [], + ); + + if (!data && !error) return ; + if (!data && error) + return ( +
+ +

VERBINDING VERBROKEN

+

DockDeck is even niet bereikbaar.

+

{error}

+ +
+ ); + if (!data) return null; + + return ( +
+ + Naar inhoud + + {page === "dashboard" ? ( + { + setSettingsSection(section); + setPage("settings"); + }} + /> + ) : ( + setPage("dashboard")} + syncing={syncing} + onSync={sync} + onChanged={load} + notify={notify} + /> + )} + {toast && ( +
+ + {toast} +
+ )} + +
+ ); +} diff --git a/src/client/api.ts b/src/client/api.ts new file mode 100644 index 00000000..388c240f --- /dev/null +++ b/src/client/api.ts @@ -0,0 +1,126 @@ +import type { + AppPatch, + Category, + CategoryOrganizationResult, + CategoryInput, + DashboardPayload, + Favorite, + FavoriteInput, + LayoutPreset, + LayoutPresetImport, + PreferencesPatch, + SystemDiagnostics, + WallpaperDescriptor, + WeatherMetric, + WidgetLayoutInput, +} from "../shared/contracts"; + +async function request(path: string, init?: RequestInit): Promise { + const response = await fetch(path, { + ...init, + headers: init?.body + ? { "content-type": "application/json", ...init.headers } + : init?.headers, + }); + if (!response.ok) { + const body = (await response.json().catch(() => null)) as { + error?: string; + } | null; + throw new Error(body?.error ?? "DockDeck kon de wijziging niet opslaan."); + } + return response.status === 204 + ? (undefined as T) + : ((await response.json()) as T); +} + +export const api = { + dashboard: () => request("/api/dashboard"), + weather: () => request("/api/weather"), + uploadWallpaper: async (file: File) => { + const response = await fetch("/api/wallpaper", { + method: "PUT", + headers: { "content-type": file.type }, + body: file, + }); + if (!response.ok) { + const body = (await response.json().catch(() => null)) as { + error?: string; + } | null; + throw new Error(body?.error ?? "Wallpaper kon niet worden opgeslagen."); + } + return (await response.json()) as WallpaperDescriptor; + }, + removeWallpaper: () => request("/api/wallpaper", { method: "DELETE" }), + sync: () => request("/api/discovery/sync", { method: "POST" }), + updateApp: (id: string, patch: AppPatch) => + request(`/api/apps/${id}`, { + method: "PATCH", + body: JSON.stringify(patch), + }), + deleteApp: (id: string) => request(`/api/apps/${id}`, { method: "DELETE" }), + createCategory: (input: CategoryInput) => + request("/api/categories", { + method: "POST", + body: JSON.stringify(input), + }), + updateCategory: (id: string, input: Partial) => + request(`/api/categories/${id}`, { + method: "PATCH", + body: JSON.stringify(input), + }), + deleteCategory: (id: string) => + request(`/api/categories/${id}`, { method: "DELETE" }), + organizeCategories: () => + request("/api/categories/organize", { + method: "POST", + }), + createFavorite: (input: FavoriteInput) => + request("/api/favorites", { + method: "POST", + body: JSON.stringify(input), + }), + updateFavorite: (id: string, input: Partial) => + request(`/api/favorites/${id}`, { + method: "PATCH", + body: JSON.stringify(input), + }), + deleteFavorite: (id: string) => + request(`/api/favorites/${id}`, { method: "DELETE" }), + updatePreferences: (patch: PreferencesPatch) => + request("/api/preferences", { + method: "PATCH", + body: JSON.stringify(patch), + }), + updateWidgetLayout: (input: WidgetLayoutInput) => + request<{ saved: true }>("/api/widget-layout", { + method: "PUT", + body: JSON.stringify(input), + }), + createLayoutPreset: (name: string) => + request("/api/layout-presets", { + method: "POST", + body: JSON.stringify({ name }), + }), + applyLayoutPreset: (id: string) => + request<{ applied: true }>(`/api/layout-presets/${id}/apply`, { + method: "POST", + }), + deleteLayoutPreset: (id: string) => + request(`/api/layout-presets/${id}`, { method: "DELETE" }), + importLayoutPreset: (payload: LayoutPresetImport) => + request("/api/layout-presets/import", { + method: "POST", + body: JSON.stringify(payload), + }), + diagnostics: () => request("/api/diagnostics"), + updateIntegrationCredentials: (id: string, values: Record) => + request<{ saved: true }>( + `/api/integrations/${encodeURIComponent(id)}/credentials`, + { + method: "PUT", + body: JSON.stringify({ values }), + }, + ), + importBackup: (payload: unknown) => + request("/api/import", { method: "POST", body: JSON.stringify(payload) }), +}; diff --git a/src/client/canon.css b/src/client/canon.css new file mode 100644 index 00000000..a7a03a53 --- /dev/null +++ b/src/client/canon.css @@ -0,0 +1,1373 @@ +/* Canon v1 presentation layer. Component logic continues to use the existing + product contracts; this file only maps those surfaces to semantic tokens. */ + +html { + scroll-behavior: smooth; + background: var(--color-background); +} + +body { + font-family: "Geist Variable", Geist, ui-sans-serif, system-ui, sans-serif; + background: var(--color-background); + color: var(--color-text); +} + +button, +input, +select, +textarea { + font-family: inherit; +} + +button:focus-visible, +a:focus-visible, +input:focus-visible, +select:focus-visible, +summary:focus-visible { + outline: 2px solid var(--color-focus); + outline-offset: 3px; +} + +.brand-lockup { + display: inline-flex; + align-items: center; + gap: var(--space-3); + text-align: left; +} + +.brand-lockup img { + width: 40px; + height: 40px; + flex: 0 0 40px; +} + +.brand-lockup > span { + min-width: 0; + display: grid; +} + +.brand-lockup strong { + color: var(--color-text); + font-size: 18px; + letter-spacing: -0.035em; +} + +.brand-lockup small { + margin-top: 2px; + color: var(--color-text-faint); + font-size: 10px; +} + +.brand-lockup.compact img { + width: 34px; + height: 34px; + flex-basis: 34px; +} + +.brand-lockup.compact strong { + font-size: 20px; +} + +.dashboard, +.settings-page { + width: 100%; + min-height: 100vh; + margin: 0; + padding: 0; +} + +.dashboard-topbar, +.settings-topbar { + position: sticky; + z-index: 50; + top: 0; + width: 100%; + height: 72px; + display: grid; + grid-template-columns: auto minmax(280px, 560px) 1fr auto; + align-items: center; + gap: var(--space-6); + border-bottom: 1px solid var(--color-border); + padding: 0 max(var(--space-6), calc((100vw - 1440px) / 2)); + background: color-mix(in srgb, var(--color-background) 92%, transparent); + backdrop-filter: blur(18px); +} + +.brand-button, +.settings-brand { + display: inline-flex; + border: 0; + padding: 0; + background: transparent; + cursor: pointer; +} + +.dashboard-topbar nav, +.settings-topbar nav { + justify-self: end; + display: flex; + align-items: center; + gap: var(--space-2); +} + +.dashboard-topbar nav button, +.settings-topbar nav button, +.settings-topbar nav span { + min-height: 38px; + display: inline-flex; + align-items: center; + border: 0; + border-bottom: 2px solid transparent; + padding: 0 10px; + color: var(--color-text-muted); + background: transparent; + font-size: 13px; + font-weight: 600; +} + +.dashboard-topbar nav button { + cursor: pointer; +} + +.dashboard-topbar nav button:hover, +.dashboard-topbar nav button.active, +.settings-topbar nav span { + border-bottom-color: var(--color-primary); + color: var(--color-text); +} + +.topbar-actions { + display: flex; + align-items: center; + gap: var(--space-2); +} + +.icon-button { + width: 42px; + height: 42px; + border: 1px solid transparent; + border-radius: var(--radius-control); + color: var(--color-text-muted); + background: transparent; + box-shadow: none; +} + +.icon-button:hover { + border-color: var(--color-border); + color: var(--color-text); + background: var(--color-hover); + transform: none; +} + +.dashboard-canvas { + width: min(calc(100% - 48px), var(--content-canon)); + margin: 0 auto; + padding: 42px 0 48px; +} + +.dashboard-hero, +.dashboard-toolbar, +.degraded-banner, +.widgets-section, +.category-tabs, +.dashboard-groups, +.empty-state, +.dashboard-footer, +.favorites-section { + max-width: none; +} + +.dashboard-hero { + grid-template-columns: minmax(0, 1fr) minmax(320px, 430px); + align-items: center; + gap: var(--space-12); + margin: 0 0 28px; +} + +.dashboard-hero::after { + display: none; +} + +.hero-copy h1 { + max-width: none; + color: var(--color-text); + font-size: clamp(34px, 4vw, 48px); + font-weight: 650; + line-height: 1.08; + letter-spacing: -0.045em; +} + +.hero-intro { + margin-top: var(--space-3); + color: var(--color-text-muted); + font-size: 16px; + line-height: 1.5; +} + +.eyebrow, +.section-kicker { + color: var(--color-primary); + font-size: 11px; + font-weight: 650; + letter-spacing: 0.12em; +} + +.hero-overview { + max-width: none; + border-color: var(--color-border); + border-radius: var(--radius-panel); + padding: var(--space-4); + background: var(--color-surface); + box-shadow: none; + backdrop-filter: none; +} + +.hero-metrics > span { + border-color: var(--color-border); + border-radius: var(--radius-control); + background: var(--color-surface-elevated); +} + +.discovery-banner { + width: fit-content; + min-width: 310px; + min-height: 52px; + display: flex; + align-items: center; + gap: var(--space-3); + margin: -8px 0 28px auto; + border: 1px solid + color-mix(in srgb, var(--color-success) 50%, var(--color-border)); + border-radius: var(--radius-surface); + padding: 10px 14px; + color: var(--color-success); + background: var(--color-success-soft); + cursor: pointer; +} + +.discovery-banner > span { + flex: 1; + display: grid; + text-align: left; +} + +.discovery-banner small { + font-size: 9px; + font-weight: 700; + letter-spacing: 0.11em; +} + +.discovery-banner strong { + margin-top: 2px; + color: var(--color-text); + font-size: 13px; +} + +.degraded-banner { + margin: 0 0 var(--space-6); + border-color: color-mix( + in srgb, + var(--color-warning) 35%, + var(--color-border) + ); + border-radius: var(--radius-control); + background: var(--color-warning-soft); +} + +.topbar-search, +.topbar-search .search-shell { + min-width: 0; + width: 100%; + margin: 0; +} + +.search-shell { + z-index: 80; +} + +.search-bar { + position: relative; + z-index: 2; + height: 44px; + gap: var(--space-3); + border-color: var(--color-border); + border-radius: var(--radius-control); + padding: 0 12px 0 15px; + background: var(--color-surface-elevated); + box-shadow: none; + backdrop-filter: none; +} + +.search-bar input { + color: var(--color-text); + font-size: 14px; +} + +.search-bar:focus-within { + border-color: var(--color-primary); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-focus) 18%, transparent); +} + +.search-shell.expanded { + position: fixed; + inset: 0; + z-index: 100; + display: flex; + flex-direction: column; + align-items: center; + padding: min(14vh, 130px) 20px 40px; +} + +/* A backdrop-filter creates a containing block for fixed descendants. Disable + it while Quick Launch is open so the dialog can cover the full viewport. */ +.dashboard-topbar:has(.search-shell.expanded) { + backdrop-filter: none; +} + +.search-shell.expanded .search-backdrop { + position: absolute; + inset: 0; + z-index: 0; + border: 0; + background: color-mix(in srgb, var(--color-background) 76%, transparent); + backdrop-filter: blur(12px); + cursor: default; +} + +.search-shell.expanded .search-bar, +.search-results { + width: min(100%, 720px); +} + +.search-shell.expanded .search-bar { + height: 66px; + border-radius: var(--radius-surface) var(--radius-surface) 0 0; + border-color: var(--color-border-strong); + background: var(--color-surface); + box-shadow: var(--shadow-overlay); +} + +.search-results { + position: relative; + inset: auto; + top: auto; + z-index: 2; + max-height: min(66vh, 640px); + overflow-y: auto; + border-color: var(--color-border-strong); + border-top: 0; + border-radius: 0 0 var(--radius-surface) var(--radius-surface); + padding: var(--space-2) var(--space-2) 0; + background: var(--color-surface); + box-shadow: var(--shadow-overlay); +} + +.result-group { + padding: var(--space-2) 0; +} + +.result-group h2 { + margin: 0; + padding: 8px 12px 6px; + color: var(--color-text-faint); + font-size: 11px; + font-weight: 650; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.search-results a, +.search-results button.google-result { + min-height: 62px; + border: 0 !important; + border-radius: var(--radius-control) !important; + padding: 8px 12px; + color: var(--color-text); +} + +.search-results a:hover, +.search-results button:hover, +.search-results a.selected, +.search-results button.selected { + background: var(--color-active); +} + +.search-hint { + display: flex; + align-items: center; + gap: var(--space-3); + padding: 22px 14px; + color: var(--color-primary); +} + +.search-hint > span { + display: grid; +} + +.search-hint small { + margin-top: 3px; + color: var(--color-text-muted); +} + +.search-shortcuts { + position: sticky; + bottom: 0; + display: flex; + gap: var(--space-5); + margin: var(--space-2) calc(var(--space-2) * -1) 0; + border-top: 1px solid var(--color-border); + padding: 12px 16px; + color: var(--color-text-muted); + background: var(--color-surface-elevated); + font-size: 11px; +} + +.search-shortcuts span { + display: inline-flex; + align-items: center; + gap: 6px; +} + +kbd { + border-color: var(--color-border-strong); + color: var(--color-text-muted); + background: var(--color-surface-muted); + font-family: inherit; +} + +.category-tabs { + margin: 0 0 38px; + gap: var(--space-2); + padding: 2px; +} + +.category-tabs button { + min-height: 40px; + border-color: var(--color-border); + border-radius: 999px; + padding: 0 16px; + color: var(--color-text-muted); + background: var(--color-surface-elevated); + font-size: 13px; +} + +.category-tabs button.active { + border-color: var(--color-primary); + color: var(--color-background); + background: var(--color-primary); + box-shadow: none; +} + +.favorites-section, +.app-section { + margin: 0 0 42px; +} + +.favorites-section > header, +.app-section > header { + display: flex; + align-items: end; + justify-content: space-between; + padding: 0 2px 14px; +} + +.favorites-section h2, +.app-section h2 { + display: flex; + align-items: center; + gap: var(--space-2); + margin: 0; + font-size: 22px; + font-weight: 600; + letter-spacing: -0.025em; +} + +.favorites-section h2 svg, +.app-section h2 svg { + color: var(--color-primary); +} + +.favorite-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: var(--space-4); +} + +.app-grid { + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: var(--space-4); +} + +.app-tile { + min-width: 0; + min-height: 148px; + display: grid; + grid-template-columns: auto 1fr auto; + grid-template-rows: auto 1fr; + align-items: start; + gap: var(--space-3); + border-color: var(--color-border); + border-radius: var(--radius-surface); + padding: var(--space-5); + background: var(--color-surface); + box-shadow: none; + backdrop-filter: none; +} + +.app-tile:hover { + border-color: color-mix( + in srgb, + var(--color-primary) 55%, + var(--color-border) + ); + background: var(--color-hover); + box-shadow: var(--shadow-interactive); + transform: translateY(-2px); +} + +.app-tile .tile-copy { + grid-column: 1 / 3; + grid-row: 2; + align-self: end; +} + +.tile-copy strong { + font-size: 16px; + font-weight: 600; +} + +.tile-copy small { + margin-top: 4px; + color: var(--color-text-muted); + font-size: 12px; +} + +.app-tile .status-dot { + grid-column: 3; + grid-row: 1; + width: 9px; + height: 9px; + margin: 6px; + border: 0; + box-shadow: none; +} + +.status-dot.online, +.inline-status.online > span { + background: var(--color-success); +} + +.status-dot.offline, +.inline-status.offline > span { + background: var(--color-error); +} + +.status-dot.unknown, +.inline-status.unknown > span { + background: var(--color-unknown); +} + +.app-tile .tile-arrow { + grid-column: 3; + grid-row: 2; + align-self: end; +} + +.favorite-grid .app-tile { + min-height: 88px; + display: flex; + align-items: center; + padding: var(--space-4); +} + +.favorite-grid .app-tile .tile-copy { + display: grid; +} + +.favorite-grid .app-tile .tile-arrow { + margin-left: auto; +} + +.dashboard-insights { + margin: 8px 0 36px; + border: 1px solid var(--color-border); + border-radius: var(--radius-panel); + background: var(--color-surface); +} + +.dashboard-insights > summary { + min-height: 74px; + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-6); + padding: 16px 20px; + color: var(--color-text); + cursor: pointer; +} + +.dashboard-insights > summary > span { + display: flex; + align-items: center; + gap: var(--space-2); + font-weight: 600; +} + +.dashboard-insights > summary svg { + color: var(--color-primary); +} + +.dashboard-insights > summary small { + color: var(--color-text-muted); +} + +.dashboard-insights .widgets-section { + margin: 0; + border-top: 1px solid var(--color-border); + padding: 20px; +} + +.dashboard-footer { + color: var(--color-text-muted); +} + +.primary-button { + min-height: 42px; + border-color: var(--color-primary); + border-radius: var(--radius-control); + color: #0b1326; + background: var(--color-primary); + box-shadow: none; +} + +.secondary-button { + min-height: 42px; + border-color: var(--color-border); + border-radius: var(--radius-control); + background: var(--color-surface); +} + +.settings-topbar { + grid-template-columns: auto 1fr auto auto; + padding-inline: max(var(--space-6), calc((100vw - 1440px) / 2)); +} + +.settings-topbar nav { + justify-self: start; +} + +.settings-topbar .back-button { + min-height: 40px; + border: 1px solid var(--color-border); + border-radius: var(--radius-control); + padding: 0 12px; +} + +.settings-layout { + width: min(calc(100% - 48px), 1440px); + max-width: 1440px; + grid-template-columns: 260px minmax(0, 1fr); + min-height: calc(100vh - 72px); +} + +.settings-sidebar { + border-color: var(--color-border); + padding: 42px 24px 36px 0; +} + +.settings-sidebar h1 { + margin-bottom: 28px; + color: var(--color-text-muted); + font-size: 14px; + font-weight: 450; + letter-spacing: 0; +} + +.settings-sidebar nav button { + min-height: 46px; + border-radius: var(--radius-control); + color: var(--color-text-muted); + font-size: 13px; +} + +.settings-sidebar nav button.active { + color: #0b1326; + background: var(--color-primary); + box-shadow: none; +} + +.settings-sidebar nav button.active svg { + color: #0b1326; +} + +.settings-content { + padding: 44px 0 80px 48px; +} + +.settings-heading { + max-width: 720px; + margin-bottom: 24px; +} + +.settings-heading h2 { + margin-bottom: 8px; + font-size: 36px; + font-weight: 650; + letter-spacing: -0.04em; +} + +.settings-heading > p:last-child { + color: var(--color-text-muted); + font-size: 15px; +} + +.settings-subnav { + display: inline-flex; + gap: 4px; + margin: 0 0 28px; + border: 1px solid var(--color-border); + border-radius: var(--radius-control); + padding: 4px; + background: var(--color-surface); +} + +.settings-subnav button { + min-height: 38px; + display: inline-flex; + align-items: center; + gap: 7px; + border: 0; + border-radius: 6px; + padding: 0 12px; + color: var(--color-text-muted); + background: transparent; + cursor: pointer; +} + +.settings-subnav button.active { + color: var(--color-text); + background: var(--color-active); +} + +.settings-summary span { + min-height: 42px; + border-color: var(--color-border); + border-radius: var(--radius-control); + color: var(--color-text-muted); + background: var(--color-surface); + font-size: 11px; +} + +.management-toolbar { + margin-bottom: 10px; +} + +input, +select { + min-height: 42px; + border-color: var(--color-border); + border-radius: var(--radius-control); + color: var(--color-text); + background: var(--color-surface); + font-size: 12px; +} + +input:focus, +select:focus { + border-color: var(--color-focus); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-focus) 16%, transparent); +} + +label > span:first-child, +.setting-label { + color: var(--color-text-muted); + font-size: 10px; + letter-spacing: 0.06em; +} + +.filter-pills { + border-color: var(--color-border); + border-radius: var(--radius-control); + background: var(--color-surface); +} + +.filter-pills button { + min-height: 34px; + font-size: 10px; +} + +.filter-pills button.active { + color: var(--color-background); + background: var(--color-primary); + box-shadow: none; +} + +.app-management-row { + min-height: 78px; + display: grid; + grid-template-columns: + minmax(220px, 1.5fr) minmax(120px, 0.65fr) minmax(130px, 0.65fr) + auto; + align-items: center; + gap: var(--space-4); + border: 1px solid var(--color-border); + border-radius: var(--radius-surface); + padding: 12px 14px; + background: var(--color-surface); +} + +.app-management-row .management-title { + min-width: 0; +} + +.app-management-row .management-title > span:nth-child(2) { + flex: 1; +} + +.inline-status, +.category-badge { + display: inline-flex; + align-items: center; + gap: 7px; + width: fit-content; + min-height: 30px; + border: 1px solid var(--color-border); + border-radius: 999px; + padding: 0 10px; + color: var(--color-text-muted); + font-size: 11px; +} + +.inline-status > span { + width: 7px; + height: 7px; + border-radius: 50%; +} + +.app-row-actions { + display: flex; + justify-content: end; + align-items: center; + gap: 7px; +} + +.visibility-toggle, +.widget-toggle, +.edit-app-button { + min-height: 36px; + border-color: var(--color-border); + border-radius: var(--radius-control); + padding: 0 10px; + color: var(--color-text-muted); + background: var(--color-surface-elevated); + font-size: 10px; +} + +.edit-app-button { + display: inline-flex; + align-items: center; + gap: 5px; + border: 1px solid var(--color-border); + cursor: pointer; +} + +.visibility-toggle.on { + border-color: color-mix( + in srgb, + var(--color-success) 45%, + var(--color-border) + ); + color: var(--color-success); + background: var(--color-success-soft); +} + +.drawer-layer { + position: fixed; + inset: 0; + z-index: 120; + display: flex; + justify-content: flex-end; +} + +.drawer-backdrop { + position: absolute; + inset: 0; + border: 0; + background: rgb(3 8 22 / 0.68); + backdrop-filter: blur(8px); +} + +.app-editor-drawer { + position: relative; + z-index: 1; + width: min(100%, 620px); + height: 100%; + display: grid; + grid-template-rows: auto minmax(0, 1fr) auto; + border-left: 1px solid var(--color-border-strong); + background: var(--color-background); + box-shadow: var(--shadow-overlay); + animation: drawer-in 180ms ease-out both; +} + +.app-editor-drawer > header, +.app-editor-drawer > footer { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-4); + border-bottom: 1px solid var(--color-border); + padding: 18px 22px; + background: var(--color-surface); +} + +.app-editor-drawer > footer { + justify-content: flex-end; + border-top: 1px solid var(--color-border); + border-bottom: 0; +} + +.drawer-app-title { + display: flex; + align-items: center; + gap: var(--space-3); +} + +.drawer-app-title > span { + display: grid; +} + +.drawer-app-title small { + color: var(--color-primary); + font-size: 9px; + font-weight: 700; + letter-spacing: 0.1em; +} + +.drawer-app-title h2 { + margin: 2px 0 0; + font-size: 20px; +} + +.drawer-content { + overflow-y: auto; + padding: 22px; +} + +.drawer-section { + display: grid; + gap: var(--space-4); + border-bottom: 1px solid var(--color-border); + padding: 0 0 24px; + margin-bottom: 24px; +} + +.drawer-section:last-child { + border-bottom: 0; +} + +.drawer-section > div:first-child h3 { + margin: 0; + font-size: 15px; +} + +.drawer-section > div:first-child p { + margin: 4px 0 0; + color: var(--color-text-muted); + font-size: 12px; + line-height: 1.5; +} + +.drawer-section.two-column { + grid-template-columns: 1fr 1fr; +} + +.drawer-section.two-column > div:first-child { + grid-column: 1 / -1; +} + +.discovered-value { + display: grid; + gap: 6px; + margin: 0; + border: 1px solid var(--color-border); + border-radius: var(--radius-control); + padding: 12px; + background: var(--color-surface-elevated); +} + +.discovered-value span { + color: var(--color-text-muted); + font-size: 10px; +} + +.discovered-value code { + overflow-wrap: anywhere; + color: var(--color-text); + font-size: 12px; +} + +.danger-button { + min-height: 42px; + display: inline-flex; + align-items: center; + gap: 7px; + border: 1px solid + color-mix(in srgb, var(--color-error) 45%, var(--color-border)); + border-radius: var(--radius-control); + padding: 0 13px; + color: var(--color-error); + background: var(--color-error-soft); + cursor: pointer; +} + +.toast { + border-color: color-mix( + in srgb, + var(--color-success) 45%, + var(--color-border) + ); + border-radius: var(--radius-control); + color: var(--color-text); + background: var(--color-surface); +} + +.mobile-nav { + display: none; +} + +@keyframes drawer-in { + from { + transform: translateX(32px); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } +} + +@media (max-width: 1120px) { + .dashboard-topbar { + grid-template-columns: auto minmax(260px, 1fr) auto; + } + + .dashboard-topbar nav { + display: none; + } + + .app-grid { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + + .app-management-row { + grid-template-columns: minmax(220px, 1fr) auto auto; + } + + .app-management-row .category-badge { + display: none; + } +} + +@media (max-width: 800px) { + .dashboard, + .settings-page { + padding-bottom: 84px; + } + + .dashboard-topbar { + height: auto; + grid-template-columns: 1fr auto; + gap: 10px; + padding: 12px 20px; + } + + .topbar-search { + grid-column: 1 / -1; + grid-row: 2; + } + + .dashboard-canvas { + width: min(calc(100% - 40px), var(--content-canon)); + padding-top: 30px; + } + + .dashboard-hero { + grid-template-columns: 1fr; + gap: 18px; + } + + .hero-overview { + max-width: none; + } + + .app-grid, + .favorite-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .settings-topbar { + grid-template-columns: 1fr auto auto; + padding-inline: 20px; + } + + .settings-topbar nav { + display: none; + } + + .settings-layout { + width: min(calc(100% - 40px), 760px); + display: block; + padding: 0; + } + + .settings-sidebar { + border: 0; + padding: 28px 0 0; + } + + .settings-sidebar nav { + display: flex; + overflow-x: auto; + padding-bottom: 8px; + } + + .settings-sidebar nav button { + flex: 0 0 auto; + } + + .settings-content { + padding: 28px 0 50px; + } + + .app-management-row { + grid-template-columns: minmax(190px, 1fr) auto; + } + + .app-management-row > .inline-status { + display: none; + } + + .mobile-nav { + position: fixed; + inset: auto 0 0; + z-index: 70; + height: 72px; + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 0; + border: 0; + border-top: 1px solid var(--color-border); + border-radius: 0; + padding: 6px 10px 8px; + background: color-mix(in srgb, var(--color-background) 95%, transparent); + box-shadow: none; + backdrop-filter: blur(18px); + transform: none; + left: 0; + } + + .mobile-nav button { + width: auto; + height: 56px; + flex-direction: column; + gap: 3px; + border-radius: var(--radius-control); + color: var(--color-text-muted); + font-size: 10px; + } + + .mobile-nav button.active { + color: var(--color-text); + background: var(--color-active); + } +} + +@media (max-width: 560px) { + .dashboard-topbar { + padding-inline: 16px; + } + + .brand-lockup.compact strong { + font-size: 18px; + } + + .dashboard-canvas { + width: calc(100% - 32px); + padding-top: 22px; + } + + .dashboard-hero { + display: none; + } + + .discovery-banner { + width: 100%; + min-width: 0; + min-height: 82px; + margin: 0 0 22px; + padding: 14px 16px; + } + + .discovery-banner strong { + font-size: 16px; + } + + .category-tabs { + margin-inline: -16px; + padding-inline: 16px; + } + + .category-tabs button { + min-width: 76px; + min-height: 54px; + justify-content: center; + border-radius: 14px; + padding-inline: 12px; + } + + .category-tabs button span { + display: none; + } + + .favorite-grid, + .app-grid { + grid-template-columns: 1fr; + } + + .favorite-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .favorite-grid .app-tile { + min-height: 132px; + display: grid; + align-content: space-between; + } + + .favorite-grid .app-tile .tile-copy { + grid-column: 1 / -1; + } + + .app-tile { + min-height: 82px; + grid-template-columns: auto 1fr auto; + grid-template-rows: auto; + align-items: center; + padding: 14px 16px; + } + + .app-tile .tile-copy, + .app-tile .status-dot, + .app-tile .tile-arrow { + grid-row: 1; + } + + .app-tile .tile-copy { + grid-column: 2; + align-self: center; + } + + .app-tile .status-dot { + grid-column: 3; + } + + .app-tile .tile-arrow { + display: none; + } + + .dashboard-insights > summary { + align-items: flex-start; + flex-direction: column; + gap: 4px; + } + + .search-shell.expanded { + padding: 16px; + justify-content: center; + } + + .search-shell.expanded .search-bar { + height: 58px; + } + + .search-results { + max-height: 70vh; + } + + .search-shortcuts { + gap: 8px; + justify-content: space-between; + font-size: 9px; + } + + .settings-topbar { + padding-inline: 16px; + } + + .settings-topbar .back-button { + width: 42px; + overflow: hidden; + padding: 0 11px; + white-space: nowrap; + } + + .settings-layout { + width: calc(100% - 32px); + } + + .settings-heading h2 { + font-size: 30px; + } + + .settings-summary { + display: grid; + grid-template-columns: repeat(3, 1fr); + } + + .settings-summary span { + min-width: 0; + justify-content: center; + overflow: hidden; + padding-inline: 8px; + text-overflow: ellipsis; + white-space: nowrap; + } + + .app-management-row { + display: block; + } + + .app-row-actions { + justify-content: flex-start; + margin-top: 12px; + overflow-x: auto; + } + + .drawer-section.two-column { + grid-template-columns: 1fr; + } + + .drawer-section.two-column > div:first-child { + grid-column: auto; + } + + .app-editor-drawer > footer { + align-items: stretch; + flex-direction: column-reverse; + } + + .app-editor-drawer > footer button { + justify-content: center; + width: 100%; + } +} + +@media (prefers-reduced-motion: reduce) { + html { + scroll-behavior: auto; + } + .app-editor-drawer { + animation: none; + } +} diff --git a/src/client/categoryIcons.ts b/src/client/categoryIcons.ts new file mode 100644 index 00000000..adce5da4 --- /dev/null +++ b/src/client/categoryIcons.ts @@ -0,0 +1,79 @@ +import { + Activity, + Archive, + Bot, + Box, + BriefcaseBusiness, + Code2, + Database, + Download, + FileText, + Gamepad2, + Home, + Image as ImageIcon, + Layers3, + Network, + Play, + Server, + ShieldCheck, + Star, + Wrench, +} from "lucide-react"; + +export const categoryIconOptions = [ + { id: "Layers3", label: "Lagen" }, + { id: "Play", label: "Media" }, + { id: "Download", label: "Downloads" }, + { id: "FileText", label: "Documenten" }, + { id: "Image", label: "Foto's" }, + { id: "Home", label: "Thuis & automatisering" }, + { id: "ShieldCheck", label: "Beveiliging & identiteit" }, + { id: "Network", label: "Netwerk & beheer" }, + { id: "Activity", label: "Monitoring" }, + { id: "Code2", label: "Ontwikkeling" }, + { id: "Bot", label: "AI & machine learning" }, + { id: "Database", label: "Databases & opslag" }, + { id: "Server", label: "Infrastructuur" }, + { id: "Gamepad2", label: "Games & emulatie" }, + { id: "Wrench", label: "Gereedschap" }, + { id: "Archive", label: "Experimenten & archief" }, + { id: "Star", label: "Favorieten" }, + { id: "BriefcaseBusiness", label: "Werk" }, + { id: "Box", label: "Algemeen" }, +] as const; + +const categoryIconComponents = { + Layers3, + Play, + Download, + FileText, + Image: ImageIcon, + Home, + ShieldCheck, + Network, + Activity, + Code2, + Bot, + Database, + Server, + Gamepad2, + Wrench, + Archive, + Star, + BriefcaseBusiness, + Box, +} as const; + +export function resolveCategoryIcon(name: string) { + return ( + categoryIconComponents[name as keyof typeof categoryIconComponents] ?? + Layers3 + ); +} + +export function nextAvailableCategoryIcon(usedIcons: Iterable) { + const used = new Set(usedIcons); + return ( + categoryIconOptions.find((option) => !used.has(option.id))?.id ?? "Layers3" + ); +} diff --git a/src/client/components/AppTile.tsx b/src/client/components/AppTile.tsx new file mode 100644 index 00000000..a84a41ce --- /dev/null +++ b/src/client/components/AppTile.tsx @@ -0,0 +1,127 @@ +import { ArrowUpRight, Box, Globe2 } from "lucide-react"; +import type { DockApp, Favorite } from "../../shared/contracts"; +import { effectiveAppUrl, favoriteIconUrl } from "../../shared/domain"; +import { ServiceIcon } from "./ServiceIcon"; + +const serviceDescriptions: Array<[RegExp, string]> = [ + [/plex/i, "Media server & streaming"], + [/jellyfin/i, "De vrije software mediaserver"], + [/sonarr/i, "PVR voor tv-series"], + [/radarr/i, "PVR voor films"], + [/home.?assistant|ha-core/i, "Centrale automatisering"], + [/adguard|pihole|pi-hole/i, "Netwerkbeveiliging & filter"], + [/nextcloud/i, "Bestandshosting"], + [/gitea|github|gitlab/i, "Code en samenwerking"], + [/immich/i, "Foto's en herinneringen"], + [/paperless/i, "Documenten en archief"], +]; + +function describeService(app: DockApp, categoryName: string) { + const haystack = `${app.displayName} ${app.technicalName}`; + return ( + serviceDescriptions.find(([pattern]) => pattern.test(haystack))?.[1] ?? + `${categoryName} service` + ); +} + +function statusLabel(status: DockApp["status"]) { + return status === "online" + ? "Online" + : status === "offline" + ? "Offline" + : "Status onbekend"; +} + +export function AppTile({ + app, + externalAccess, + showStatus, + index, + categoryName, + variant = "standard", +}: { + app: DockApp; + externalAccess: boolean; + showStatus: boolean; + index: number; + categoryName: string; + variant?: "standard" | "feature"; +}) { + const url = effectiveAppUrl(app, externalAccess); + const content = ( + <> + + + {app.displayName} + {describeService(app, categoryName)} + + {showStatus && ( + + {statusLabel(app.status)} + + )} +