From 3c263a708d001cf6dc331ad72ac397c8e986c225 Mon Sep 17 00:00:00 2001 From: IliesAllali Date: Fri, 9 Oct 2026 14:24:19 +0200 Subject: [PATCH 01/11] =?UTF-8?q?Docker=20:=20images=20API=20et=20front=20?= =?UTF-8?q?publi=C3=A9es=20sur=20ghcr.io,=20installation=20sans=20cloner?= =?UTF-8?q?=20le=20d=C3=A9p=C3=B4t?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Après chaque CI verte sur main (et sur un tag vX.Y.Z), le workflow Images construit parenthese-api et parenthese-web en amd64 et arm64, les pousse sur ghcr.io, puis teste une installation dans un dossier vide avec docker-compose.yml et .env pour seuls fichiers. docker-compose.yml tire ces images (PARENTHESE_VERSION, latest par défaut) et garde le build depuis les sources en repli. Demande faite sur r/GenealogieFR. Co-Authored-By: Claude Opus 5.5 --- .env.example | 5 ++ .github/workflows/images.yml | 149 +++++++++++++++++++++++++++++++++++ README.fr.md | 14 +++- README.md | 14 +++- docker-compose.yml | 5 ++ 5 files changed, 183 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/images.yml diff --git a/.env.example b/.env.example index c6dcdee..f6b17af 100644 --- a/.env.example +++ b/.env.example @@ -34,6 +34,11 @@ CORS_ORIGIN=http://localhost # MEDIA_QUOTA_PER_TREE_MB=5120 # MEDIA_PENDING_QUOTA_PER_TREE_MB=500 +# ── Images ──────────────────────────────────────────────────────────────────── +# Version des images ghcr.io/iliesallali/parenthese-api et parenthese-web : latest (dernière +# version de main), ou une version figée, par exemple 1.0. +# PARENTHESE_VERSION=latest + # ── Web ─────────────────────────────────────────────────────────────────────── # Port publié sur la machine hôte (80 = http://localhost sans préciser de port). WEB_PORT=80 diff --git a/.github/workflows/images.yml b/.github/workflows/images.yml new file mode 100644 index 0000000..9e10e15 --- /dev/null +++ b/.github/workflows/images.yml @@ -0,0 +1,149 @@ +name: Images + +# Publie les images Docker de l'API et du front sur ghcr.io, pour un auto-hébergement sans le code +# source : docker-compose.yml + .env suffisent. +# - après une CI réussie sur un push sur main : tags `latest` et `sha-` +# - sur un tag de version vX.Y.Z : tags `X.Y.Z` et `X.Y` +# Puis l'installation est testée dans un dossier vide, avec ces deux seuls fichiers. +on: + workflow_run: + workflows: + - CI + types: + - completed + branches: + - main + push: + tags: + - 'v*' + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: images-${{ github.event.workflow_run.head_sha || github.ref }} + cancel-in-progress: false + +jobs: + publish: + name: Build and push (${{ matrix.image }}) + if: >- + github.repository == 'IliesAllali/parenthese' && + ( + github.event_name != 'workflow_run' || + ( + github.event.workflow_run.conclusion == 'success' && + github.event.workflow_run.event == 'push' && + github.event.workflow_run.head_repository.full_name == 'IliesAllali/parenthese' + ) + ) + runs-on: ubuntu-latest + timeout-minutes: 60 + permissions: + contents: read + packages: write + strategy: + fail-fast: false + matrix: + include: + - image: parenthese-api + context: backend + file: backend/Dockerfile + - image: parenthese-web + context: . + file: Dockerfile.web + steps: + # Sur workflow_run, le commit exact que la CI a validé, pas la tête de main au moment du run. + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0 + with: + ref: ${{ github.event.workflow_run.head_sha || github.sha }} + persist-credentials: false + + - name: Short commit + id: commit + run: echo "sha=$(git rev-parse --short=7 HEAD)" >> "$GITHUB_OUTPUT" + + - uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0 + + - uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1 + + - uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Tags and labels + id: meta + uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 + with: + images: ghcr.io/iliesallali/${{ matrix.image }} + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=raw,value=latest,enable=${{ github.event_name == 'workflow_run' }} + type=raw,value=sha-${{ steps.commit.outputs.sha }} + labels: | + org.opencontainers.image.revision=${{ github.event.workflow_run.head_sha || github.sha }} + org.opencontainers.image.licenses=PolyForm-Noncommercial-1.0.0 + + - name: Build and push + uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0 + with: + context: ${{ matrix.context }} + file: ${{ matrix.file }} + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha,scope=${{ matrix.image }} + cache-to: type=gha,mode=max,scope=${{ matrix.image }} + + compose-only: + name: Install from the images only + needs: publish + runs-on: ubuntu-latest + timeout-minutes: 20 + permissions: + contents: read + packages: read + steps: + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0 + with: + ref: ${{ github.event.workflow_run.head_sha || github.sha }} + path: src + persist-credentials: false + + # Tant que les paquets ne sont pas publics, le pull demande une connexion. + - uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Empty folder with docker-compose.yml and .env only + run: | + set -euo pipefail + mkdir parenthese + cp src/docker-compose.yml src/.env.example parenthese/ + cd parenthese + cp .env.example .env + sed -i "s|^POSTGRES_PASSWORD=.*|POSTGRES_PASSWORD=$(openssl rand -hex 24)|" .env + sed -i "s|^JWT_SECRET=.*|JWT_SECRET=$(openssl rand -base64 48)|" .env + echo "PARENTHESE_VERSION=sha-$(git -C ../src rev-parse --short=7 HEAD)" >> .env + ls -A + + - name: docker compose up -d (pull, no build) + working-directory: parenthese + run: docker compose up -d --pull always --no-build --wait --wait-timeout 300 + + - name: Smoke test through the web port + run: bash src/tools/docker-smoke.sh http://localhost + + - name: Containers and logs on failure + if: failure() + working-directory: parenthese + run: | + docker compose ps -a + docker compose logs --no-color --timestamps diff --git a/README.fr.md b/README.fr.md index b77a9af..03d06c1 100644 --- a/README.fr.md +++ b/README.fr.md @@ -32,6 +32,14 @@ cd parenthese cp .env.example .env ``` +Le clone est facultatif. Les images de l'API et du front sont publiées sur le registre de GitHub (`ghcr.io/iliesallali/parenthese-api` et `ghcr.io/iliesallali/parenthese-web`, en amd64 et arm64), deux fichiers suffisent donc : + +```bash +mkdir parenthese && cd parenthese +curl -fsSLO https://raw.githubusercontent.com/IliesAllali/parenthese/main/docker-compose.yml +curl -fsSL https://raw.githubusercontent.com/IliesAllali/parenthese/main/.env.example -o .env +``` + Ouvrez `.env` et remplissez les deux valeurs vides : - `POSTGRES_PASSWORD` : un mot de passe pour la base (lettres et chiffres). @@ -43,7 +51,7 @@ Puis : docker compose up -d ``` -Le premier démarrage construit les images, applique les migrations de la base, puis sert l'application sur [http://localhost](http://localhost). Créez votre compte depuis l'écran d'accueil : le premier compte est un compte comme les autres, il n'y a pas d'administrateur global. +Le premier démarrage télécharge les images (ou les construit depuis les sources si elles sont introuvables), applique les migrations de la base, puis sert l'application sur [http://localhost](http://localhost). Créez votre compte depuis l'écran d'accueil : le premier compte est un compte comme les autres, il n'y a pas d'administrateur global. Trois conteneurs tournent : `db` (PostgreSQL 16), `api` (l'API Node) et `web` (nginx qui sert le front et relaie l'API). Seul `web` est exposé, sur le port choisi par `WEB_PORT` (80 par défaut). Si ce port est déjà pris, changez `WEB_PORT` dans `.env` et adaptez `CORS_ORIGIN` en conséquence. @@ -88,9 +96,11 @@ Le script `deploy/backup.sh` fait la même chose (dump PostgreSQL, archive des m ### Mettre à jour ```bash -git pull && docker compose up -d --build +docker compose pull && docker compose up -d ``` +`latest` suit la branche main. Pour rester sur une version, renseignez `PARENTHESE_VERSION` dans `.env` (par exemple `1.0`). Depuis un clone, `git pull && docker compose up -d --build` reconstruit depuis les sources. + Les migrations de base de données s'appliquent toutes seules au démarrage de l'API. Faites une sauvegarde avant, par habitude. ## Développer diff --git a/README.md b/README.md index 9f1ff19..218661e 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,14 @@ cd parenthese cp .env.example .env ``` +You can also skip the clone. The API and front-end images are published on GitHub's registry (`ghcr.io/iliesallali/parenthese-api` and `ghcr.io/iliesallali/parenthese-web`, for amd64 and arm64), so two files are enough: + +```bash +mkdir parenthese && cd parenthese +curl -fsSLO https://raw.githubusercontent.com/IliesAllali/parenthese/main/docker-compose.yml +curl -fsSL https://raw.githubusercontent.com/IliesAllali/parenthese/main/.env.example -o .env +``` + Open `.env` and fill in the two empty values: - `POSTGRES_PASSWORD`: a password for the database (letters and digits only). @@ -49,7 +57,7 @@ Then: docker compose up -d ``` -The first start builds the images, applies the database migrations, then serves the app on [http://localhost](http://localhost). Create your account from the home screen. The first account is an account like any other, there is no global administrator. +The first start downloads the images (or builds them from the source when they can't be pulled), applies the database migrations, then serves the app on [http://localhost](http://localhost). Create your account from the home screen. The first account is an account like any other, there is no global administrator. Three containers run: `db` (PostgreSQL 16), `api` (the Node API) and `web` (nginx, which serves the front end and proxies the API). Only `web` is exposed, on the port set by `WEB_PORT` (80 by default). If that port is taken, change `WEB_PORT` in `.env` and update `CORS_ORIGIN` to match. @@ -94,9 +102,11 @@ The `deploy/backup.sh` script does the same (PostgreSQL dump, media archive, che ### Updating ```bash -git pull && docker compose up -d --build +docker compose pull && docker compose up -d ``` +`latest` follows the main branch. To stay on a version, set `PARENTHESE_VERSION` in `.env` (for example `1.0`). From a clone, `git pull && docker compose up -d --build` builds from the source instead. + Database migrations apply on their own when the API starts. Make a backup first, as a habit. ## Development diff --git a/docker-compose.yml b/docker-compose.yml index 6332f84..341bb3e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,6 +4,9 @@ # docker compose logs -f api suivre l'API # docker compose down arrêter (les volumes db-data et media sont conservés) # bash tools/docker-smoke.sh vérifier l'installation +# Les images de l'API et du front sont publiées sur ghcr.io (version choisie par PARENTHESE_VERSION, +# `latest` par défaut) : ce fichier et le .env suffisent, sans le code. Si l'image est introuvable, +# compose la construit depuis les sources (dépôt cloné) ; `--build` force cette construction. name: parenthese @@ -24,6 +27,7 @@ services: retries: 12 api: + image: ghcr.io/iliesallali/parenthese-api:${PARENTHESE_VERSION:-latest} build: ./backend restart: unless-stopped depends_on: @@ -49,6 +53,7 @@ services: start_period: 30s web: + image: ghcr.io/iliesallali/parenthese-web:${PARENTHESE_VERSION:-latest} build: context: . dockerfile: Dockerfile.web From 52fff2b037d0640d4aa8cbd0768804a8ff88f7bd Mon Sep 17 00:00:00 2001 From: IliesAllali Date: Fri, 9 Oct 2026 15:21:08 +0200 Subject: [PATCH 02/11] =?UTF-8?q?D=C3=A9pendances=20du=2009/10=20et=20vers?= =?UTF-8?q?ion=201.0.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fast-jwt 6.3.4 (cache du vérificateur, jetons expirés sans iat), source-map-js 1.2.2 et postcss-selector-parser 7.1.6 forcé sous Tailwind 3 (CSS produit identique octet pour octet). Landing en Vite 8 et plugin-react 6, mêmes navigateurs cibles que le front, rendu comparé à l'écran au build Vite 7 en 1920 et 390 px. Groupes mineurs front (#50) et landing (#38). Reprend #38, #41, #46, #49, #50, #51. CHANGELOG 1.0.2. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 17 + backend/package-lock.json | 453 ++++++---- frontend/package-lock.json | 85 +- frontend/package.json | 17 +- landing/package-lock.json | 1698 ++++++++++-------------------------- landing/package.json | 9 +- landing/vite.config.js | 2 + 7 files changed, 810 insertions(+), 1471 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a6145b..170da9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,23 @@ Notable changes to Parenthèse. Dates are in YYYY-MM-DD format. +## 1.0.2 (2026-10-09) + +### Self-hosting + +- Docker images for the API and the front end are published on GitHub's registry, for amd64 and arm64: `ghcr.io/iliesallali/parenthese-api` and `ghcr.io/iliesallali/parenthese-web`. `docker-compose.yml` and `.env` are enough, no clone needed. `PARENTHESE_VERSION` in `.env` pins a version (`latest` follows main). Each publication is tested by an install in an empty folder with those two files only. +- Creating a tree works on an instance served over plain HTTP on a local network. The browser only offers `crypto.randomUUID` over HTTPS or on localhost, and the sharing password now falls back to `crypto.getRandomValues` (#44, fixed in #45 by @coutadeurf). + +### Security + +- fast-jwt 6.3.4: its verifier cache could accept an expired token that has no `iat` claim. +- Build tools only, nothing that runs in the browser or on the server: source-map-js 1.2.2 and postcss-selector-parser 7.1.6 (CPU exhaustion on crafted input). + +### Dependencies + +- Landing on Vite 8 and @vitejs/plugin-react 6, with the same target browsers as before, like the app. +- Minor and patch updates (lucide, posthog-js, ESLint, PostCSS). + ## 1.0.1 (2026-10-01) Security release, from an independent review of 1.0.0. diff --git a/backend/package-lock.json b/backend/package-lock.json index efe4602..993a881 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -479,7 +479,9 @@ } }, "node_modules/@fastify/ajv-compiler": { - "version": "4.0.5", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-4.0.6.tgz", + "integrity": "sha512-NtuzM0SfaMJbGlnjr9LWQUN5LzgSrbB8tf/wRZNas+4E1O/Nmzl53e7ruT61HDZyRCJGC6FxIogmNZO1c5ETBA==", "funding": [ { "type": "github", @@ -494,13 +496,13 @@ "dependencies": { "ajv": "^8.12.0", "ajv-formats": "^3.0.1", - "fast-uri": "^3.0.0" + "fast-uri": "^4.0.0" } }, "node_modules/@fastify/cors": { - "version": "11.3.0", - "resolved": "https://registry.npmjs.org/@fastify/cors/-/cors-11.3.0.tgz", - "integrity": "sha512-ggQGua+xHv1MvePbPr0v//xLYEsCXbWspquXCJS9Ot5YoRXq8J8ZWzHnxDBVnbtXosvistXo6LtNzOJswf64Fw==", + "version": "11.3.1", + "resolved": "https://registry.npmjs.org/@fastify/cors/-/cors-11.3.1.tgz", + "integrity": "sha512-gZXNzd6FLHzOIfvoK4+Az3Wko2V6Y0MIsLxpyo4AEYl7a9q1JBPwPaxLea4nkr++8FCiJiPpYYo6E02uZW0dqw==", "funding": [ { "type": "github", @@ -519,6 +521,8 @@ }, "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", @@ -532,7 +536,9 @@ "license": "MIT" }, "node_modules/@fastify/fast-json-stringify-compiler": { - "version": "5.0.3", + "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", @@ -545,11 +551,13 @@ ], "license": "MIT", "dependencies": { - "fast-json-stringify": "^6.0.0" + "fast-json-stringify": "^7.0.0" } }, "node_modules/@fastify/forwarded": { - "version": "3.0.1", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@fastify/forwarded/-/forwarded-3.0.2.tgz", + "integrity": "sha512-NE8HgKLgYejV9lDpqkEFaDKMLYelJBVfHekhB0UKvX0ghagXRJqg68feg8er1NPXxG4N9i6vPxzt8E+3wHfcmA==", "funding": [ { "type": "github", @@ -563,9 +571,9 @@ "license": "MIT" }, "node_modules/@fastify/jwt": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/@fastify/jwt/-/jwt-10.2.2.tgz", - "integrity": "sha512-UOYY5db2ttuWk2FcN5L6rawE0OFa4+QRJdsYEiHCBmf1GFLC9/k73f/mmv2dxIhS0b/02/62T0Hs6sk+w18Tyg==", + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/@fastify/jwt/-/jwt-10.2.4.tgz", + "integrity": "sha512-xaRULl7ZLLmmclAWc0gaNHCrK32A+uho8wMJkHfxQ+EXIXIBdCLnbe/89uTlNbQoR2w4i/p24DzOuldvM52ETA==", "funding": [ { "type": "github", @@ -587,6 +595,8 @@ }, "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", @@ -603,7 +613,9 @@ } }, "node_modules/@fastify/proxy-addr": { - "version": "5.1.0", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@fastify/proxy-addr/-/proxy-addr-5.1.1.tgz", + "integrity": "sha512-zv07Y9GEuDsJPegZoDFd4SDWaZOW8N2pa0GSrYmKpId/tjt1Hgo3BjZBVjdVpfVrHaA+Qv5jawtS2O50J5xM9g==", "funding": [ { "type": "github", @@ -621,9 +633,9 @@ } }, "node_modules/@fastify/rate-limit": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/@fastify/rate-limit/-/rate-limit-11.2.0.tgz", - "integrity": "sha512-X7osJd4XSvMoejYrnJkSZYYjY1eNYoBqhjlzf1RakC2204qExFqZFTKj5+T7VuzA/iUI9Z3UoSqQRkB2HpG0oQ==", + "version": "11.2.1", + "resolved": "https://registry.npmjs.org/@fastify/rate-limit/-/rate-limit-11.2.1.tgz", + "integrity": "sha512-uTQjii3tJUnb6ZyEMcunuvKE3FDKNZEaPpxdyWADDgthHnUV1swdvwuiZfi3DDDgxm1iBrhOkQ/w0yDBQ8qJtA==", "funding": [ { "type": "github", @@ -1172,15 +1184,17 @@ }, "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/@oxc-project/types": { - "version": "0.152.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.152.0.tgz", - "integrity": "sha512-oM/5rLBm2tPkg0iBgkH/FOeR3PCDpY19GTgAZjMFM8h9WI9VW7cLgzp6nwtarYKmovavIQZ+Fe/RKX/8C8O/Rw==", + "version": "0.153.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.153.0.tgz", + "integrity": "sha512-jSNrRIyvPVLL5E1z/tS5zLAprosGz/FuI1PWwEOz59j3E4TTTzSpqKb/RDGqJhhW0QHYISGXxq582wGx6T11Tw==", "dev": true, "license": "MIT", "peer": true, @@ -1190,10 +1204,14 @@ }, "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/@prisma/client": { - "version": "6.19.2", + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.19.3.tgz", + "integrity": "sha512-mKq3jQFhjvko5LTJFHGilsuQs+W+T3Gm451NzuTDGQxwCzwXHYnIu2zGkRoW+Exq3Rob7yp2MfzSrdIiZVhrBg==", "hasInstallScript": true, "license": "Apache-2.0", "engines": { @@ -1276,9 +1294,9 @@ } }, "node_modules/@rolldown/binding-android-arm-eabi": { - "version": "1.2.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.12.tgz", - "integrity": "sha512-dB/a1214qKfHMXCpgqR4OZT+jS4kTyEXbQGJPqzobt5EwH5rX080pxE37alt3RzvR1bf1Yz/yGqRfrYAxuPw0A==", + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.13.tgz", + "integrity": "sha512-Vu/NICrgdRvUOeaJZIyB2FP63s6fO/4ud5l8dXmxDx9yDyn+mN0y+gsvsVviSli+T361Aqo0h46EuMoobrwawA==", "cpu": [ "arm" ], @@ -1294,9 +1312,9 @@ } }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.2.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.12.tgz", - "integrity": "sha512-7KHFgQ5VJxIHcLlrwrc3Xbds7oTNQT7Pgi9gQCJKrd2VGab/UksIOYp6VD8MzCstGxOKMgNamPwUCfxPdP1OHg==", + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.13.tgz", + "integrity": "sha512-6LEoIrGy5vvCd2boNnv2soPSUZj/PRvhp8R/t9KlX+mIZxThIE/ZqxrGXxmuNX0FuvQFvLdZr7S4HrYpYBVhEQ==", "cpu": [ "arm64" ], @@ -1312,9 +1330,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.2.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.12.tgz", - "integrity": "sha512-3YIhqHD96nA5SaYNRBR16HnGv4oavZvXfD/ayHM+oYZ0WD/8lBAtf6zQua4kEyAvpqrluKXl0lnOBoiNby7x9w==", + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.13.tgz", + "integrity": "sha512-CaJSRzlQ6Q+zkDMVA9OkVxBjVoPRCDEE3v0FSJ9kIQBgRjFApA3Si4ijhbNVUji7ZJt+PfO0e46qy4nPInHUPA==", "cpu": [ "arm64" ], @@ -1330,9 +1348,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.2.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.12.tgz", - "integrity": "sha512-UuuJ35MFw4gmFOrE9pEqIV+K3syIKveph+Qc1/ljHZVdoDW4pz/JHR/eMVom+TZGl/5OOvGJOWaOCVt3ZfqhxA==", + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.13.tgz", + "integrity": "sha512-e4r340I5hxSzJkD3YJBxgzq/rJ3wwxwdLLb2FPt1Zpyem20nrQPT11DDEEkazwxaHumH8wpglsp/Ozgj/wT6mw==", "cpu": [ "x64" ], @@ -1348,9 +1366,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.2.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.12.tgz", - "integrity": "sha512-uMvssit0a4W+/7D8CbHUvG719mH3R2jwXAlh/XcPvuHTE0g++LymF88DCGNX0HM2rBOn0xrzgXktIB6fLSJBTQ==", + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.13.tgz", + "integrity": "sha512-hoBWDZ4/K/mJKVMbsgUd1jfpA2DiOrJ54mpegObU9TBHBBrtX4FLIXJJQRDPZ/2Dn91GvPob+Z2hwKJzf4E5SQ==", "cpu": [ "x64" ], @@ -1366,9 +1384,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.2.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.12.tgz", - "integrity": "sha512-XcFu0R0xWnwzSf4IQgFH1rJIckPN1pLy2R+4r9IDB7Yfu/ys9cVqfa4pBrMHj7a3gl8mIR4nRNPg0e5IvEVs6g==", + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.13.tgz", + "integrity": "sha512-oF9fZu3vTWnTiVAkDFxU9PJLt1nh20K9RRpuHsR7ZnE2L7XookBbDBVLojCVSLGbvtSlxeUNnjAj6DbNsKyzbg==", "cpu": [ "arm" ], @@ -1384,9 +1402,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.2.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.12.tgz", - "integrity": "sha512-260UrKgn8tz39ak+SMDOirKzr7V04M9dWPw5llW00SwBivCZoWcRBKV1d8cXnRkUmSZA3BdiUmBHWk7734Ulpw==", + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.13.tgz", + "integrity": "sha512-DTpb/+jMiqWMPgphn4ksW1zW+XLJ6dJJ9FMvnIk9I291LhCctMPhMYJrP5t9p6C9qa3KXadDdUEZHBaPh3kNxQ==", "cpu": [ "arm64" ], @@ -1402,9 +1420,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.2.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.12.tgz", - "integrity": "sha512-5YK1I9SqDkbPgc1IA8BgDl34suqUS2q0KWnBrirm0E51YjOs6eo6dV6jbQfNE/argHRSvd0QUGgtpIoYx+WWpw==", + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.13.tgz", + "integrity": "sha512-xU+Kvv9N2A3SY35zFh1FWWNjxbadE/BKeqnkXe7vScU4GZsw757BlVmDLITZEq8DPpk6m7uoDYkK7P+x/uU5Ow==", "cpu": [ "arm64" ], @@ -1420,9 +1438,9 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.2.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.12.tgz", - "integrity": "sha512-Rkcrmp7eFRg74yL5fXEU91JEWbdEPLevWwGtXpmhbjlD1StScbWTmO94Bhly+Mo+ketKYkdmM1vNUKeWSlx8cQ==", + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.13.tgz", + "integrity": "sha512-XMk/hnhh25oQ756Lclb1Q11h4ll0xC0Esa5w4Vzp+mAE2S3AGJgeM0SonrIgY8fHhjarb6hcoTuwoFG0VVP2ig==", "cpu": [ "ppc64" ], @@ -1438,9 +1456,9 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.2.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.12.tgz", - "integrity": "sha512-qvK4DuAsQc2BSjlx+Xr+IzOIvvxbGZqxFwdWfG6F518Erj0GGISyQbJ6pIappnOxlNPzNHvo/L0BwB30GZ+zVw==", + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.13.tgz", + "integrity": "sha512-U1BoTHYmlywE2n3HUWjzZ+8EQsKr+OwpxYuDWaubx42tUL8QSL7nOqRz/NVHanibbFP3M1Rxuprz/wNcbMKjqw==", "cpu": [ "s390x" ], @@ -1456,9 +1474,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.2.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.12.tgz", - "integrity": "sha512-Q9uLBO53Xd4QIq1WOycVQyPP1O4HhraEV2qqb3uTrnVw6QZih9duY4vNXOivL1xoUS1/z+W8eF4NMfl2a8Sdjw==", + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.13.tgz", + "integrity": "sha512-J4cm80Pc6wEwV5AZ42o62sIQgDk+Y7R2s646jIwuvO+4V46QB58N4vVuMPWCBaNgursXLpmUWJV//WQAkPvHBQ==", "cpu": [ "x64" ], @@ -1474,9 +1492,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.2.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.12.tgz", - "integrity": "sha512-3IBxWFMjbOZskDPKv8Lf9BCnahlKuHthWkYnyIxOH/QcJrFcS4EmcenthApkwr/5+nEqZlLzeYbxeMaX7A5u4g==", + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.13.tgz", + "integrity": "sha512-W0euZS40U4V6JkmEnVCIX9mcsL6cbXQ4X8c2w+MMOIjFg68TXUW+u3RT+2e1t1pv9mg/EP3pDtK3POXdQ7Rxyw==", "cpu": [ "x64" ], @@ -1492,9 +1510,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.2.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.12.tgz", - "integrity": "sha512-xtX61xg4LKPkPWilZU1ynKClz5Gj4bf74LML4r3eVLWumKnGjoEr1OSHQhMdbBDoYTi+yjrujvpZe2pUnqCrrA==", + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.13.tgz", + "integrity": "sha512-do/AF/4n9MRshia3a5aXoy8xM2c6mgfT6D7H/dY6my5z+hfCIR4ab7mk4IRj++nsEQpKy39kd0DQnDq+r692tw==", "cpu": [ "arm64" ], @@ -1510,9 +1528,9 @@ } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.2.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.12.tgz", - "integrity": "sha512-At7fPB6PCaIjzgIhEZFxuT+BBFqiQibJDT4d3PhiR3f4E7bbMZF4aKblbFfEM3sETRDd1YiQx/+U/g/B/ou5Ew==", + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.13.tgz", + "integrity": "sha512-6FtUf2cJZobutJdxnGYX8z0vtbgqU0b2v8vfBhkXb9wp2MppMFtkunE+IaOntyi01rkIQoPW2GHABtfzsC0BRQ==", "cpu": [ "arm64" ], @@ -1528,9 +1546,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.2.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.12.tgz", - "integrity": "sha512-WIw2haVKwjuYdXkHaoC0mF8Le71TuCBxjrdKqLbJGctbBABj+ClfmNvtbOnzpq3RokNo5+V1qhtSzJyXorsklQ==", + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.13.tgz", + "integrity": "sha512-UKhFaZTeTcbKrb3iU82m17jVcPEAoqDkCN5MBqgPyZSwp/cQmib21crTS0DdH7T1B7wZRWEI89YQAOJMet5yhg==", "cpu": [ "x64" ], @@ -1586,11 +1604,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.10.13", + "version": "24.19.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.19.1.tgz", + "integrity": "sha512-aS3/DG0oM05K0RIXXP+hKjinGG5IgSSVGzswZxW3O0sS3pH4/fycXundUC9XsszgKCk4gHXylTEK6hyFxVxnoQ==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.16.0" + "undici-types": ">=7.24.0 <7.24.7" } }, "node_modules/@typescript/typescript-aix-ppc64": { @@ -1953,6 +1973,8 @@ }, "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/ajv": { @@ -1973,6 +1995,8 @@ }, "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" @@ -1986,6 +2010,22 @@ } } }, + "node_modules/ajv/node_modules/fast-uri": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", + "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/asn1.js": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", @@ -2010,13 +2050,27 @@ }, "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.1.0", + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/avvio/-/avvio-9.3.0.tgz", + "integrity": "sha512-g2tQ7LE7oOSqDfwEm3M+ZCMTJc7KiZCdJ4UwyZJb5ckTKyYu50OYmvv0mCFXPuYXoM4zkSt8zM9XQ9KCvxA74A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], "license": "MIT", "dependencies": { "@fastify/error": "^4.0.0", @@ -2025,6 +2079,8 @@ }, "node_modules/bcryptjs": { "version": "3.0.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz", + "integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==", "license": "BSD-3-Clause", "bin": { "bcrypt": "bin/bcrypt" @@ -2079,9 +2135,9 @@ } }, "node_modules/chai": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", - "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.3.0.tgz", + "integrity": "sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==", "dev": true, "license": "MIT", "engines": { @@ -2133,6 +2189,8 @@ }, "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" @@ -2161,6 +2219,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" @@ -2183,9 +2243,9 @@ } }, "node_modules/dotenv": { - "version": "18.0.5", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-18.0.5.tgz", - "integrity": "sha512-aBrGvt6KhjxbEnatqMWOMidftwsrCiCvqh1yNIGB9QyF0J+VcWkKDMsvYOEm7ZS82oiV+BT1sTOtIlAmUlTpwQ==", + "version": "18.0.6", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-18.0.6.tgz", + "integrity": "sha512-ZMVmohV6CvAJtj++pZNyggluUt6hbRdQAxc2+Z635RL6/AG/ruXv5lHnybUFoxMPL/yrTL/tsF4paZu2l7RA6Q==", "license": "BSD-2-Clause", "bin": { "dotenv": "dist/index.cjs" @@ -2297,9 +2357,9 @@ } }, "node_modules/exsolve": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.1.1.tgz", - "integrity": "sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.1.3.tgz", + "integrity": "sha512-kuzL9zherLQoqDeQaS55AiiIg1kb4KYXaSusTtgZTPlvKP2gGCI82LNmBNh5RjDVUXIcmgx+/Mx7vPwRzBbhJA==", "devOptional": true, "license": "MIT" }, @@ -2334,10 +2394,14 @@ }, "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-stringify": { - "version": "6.3.0", + "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", @@ -2353,15 +2417,15 @@ "@fastify/merge-json-schemas": "^0.2.0", "ajv": "^8.12.0", "ajv-formats": "^3.0.1", - "fast-uri": "^3.0.0", + "fast-uri": "^4.0.0", "json-schema-ref-resolver": "^3.0.0", "rfdc": "^1.2.0" } }, "node_modules/fast-jwt": { - "version": "6.3.3", - "resolved": "https://registry.npmjs.org/fast-jwt/-/fast-jwt-6.3.3.tgz", - "integrity": "sha512-pQDXx7IHeZT4jSmpE9o80RrBqfrG4fPrl8anazSM5vErIdK1iCc13z/EWX+H0j7liWSRnwTpHswIKMeLYGAckw==", + "version": "6.3.4", + "resolved": "https://registry.npmjs.org/fast-jwt/-/fast-jwt-6.3.4.tgz", + "integrity": "sha512-wq1bK/Pi0OARHYrEkXyTH793Xbqx4dHxBVxYWWHymqFxSEmhllkDj5ZCvbYZcfwUQmitjnBKSgy7nv7Duxg2BQ==", "license": "Apache-2.0", "dependencies": { "@lukeed/ms": "^2.0.2", @@ -2384,9 +2448,9 @@ } }, "node_modules/fast-uri": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", - "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-4.2.1.tgz", + "integrity": "sha512-TmHQgewjHtMq1E5QKA0tOE0yeYGQs25KZC/ziJpubRtWI15W92e6vPFydWOeZBVROSHBYw/QhD9d5OefdD6LDg==", "funding": [ { "type": "github", @@ -2401,6 +2465,8 @@ }, "node_modules/fastfall": { "version": "1.5.1", + "resolved": "https://registry.npmjs.org/fastfall/-/fastfall-1.5.1.tgz", + "integrity": "sha512-KH6p+Z8AKPXnmA7+Iz2Lh8ARCMr+8WNPVludm1LGkZoD2MjY6LVnRMtTKhkdzI+jr0RzQWXKzKyBJm1zoHEL4Q==", "license": "MIT", "dependencies": { "reusify": "^1.0.0" @@ -2458,48 +2524,10 @@ ], "license": "MIT" }, - "node_modules/fastify/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/fastify/node_modules/fast-uri": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-4.2.1.tgz", - "integrity": "sha512-TmHQgewjHtMq1E5QKA0tOE0yeYGQs25KZC/ziJpubRtWI15W92e6vPFydWOeZBVROSHBYw/QhD9d5OefdD6LDg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" - }, "node_modules/fastparallel": { "version": "2.4.1", + "resolved": "https://registry.npmjs.org/fastparallel/-/fastparallel-2.4.1.tgz", + "integrity": "sha512-qUmhxPgNHmvRjZKBFUNI0oZuuH9OlSIOXmJ98lhKPxMZZ7zS/Fi0wRHOihDSz0R1YiIOjxzOY4bq65YTcdBi2Q==", "license": "ISC", "dependencies": { "reusify": "^1.0.4", @@ -2507,7 +2535,9 @@ } }, "node_modules/fastq": { - "version": "1.20.1", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.3.tgz", + "integrity": "sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw==", "license": "ISC", "dependencies": { "reusify": "^1.0.4" @@ -2515,6 +2545,8 @@ }, "node_modules/fastseries": { "version": "1.7.2", + "resolved": "https://registry.npmjs.org/fastseries/-/fastseries-1.7.2.tgz", + "integrity": "sha512-dTPFrPGS8SNSzAt7u/CbMKCJ3s01N04s4JFbORHcmyvVfVKmbhMD1VtRbh5enGHxkaQDqWyLefiKOGGmohGDDQ==", "license": "ISC", "dependencies": { "reusify": "^1.0.0", @@ -2593,23 +2625,27 @@ "license": "ISC" }, "node_modules/ip-address": { - "version": "10.7.2", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.2.tgz", - "integrity": "sha512-7H/2gFSIitxc0hG3nOI1glS8QLo/EHBFFLk8vEUjXY/xu0AdL8jZ9U1IzO2PUm0d2D/ofQcAifb0g6OBkt8U7w==", + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.3.tgz", + "integrity": "sha512-A1kdq/tSb5QjvKvAMgIoEvDBIgL7qaqVP/jkvSwYYRZ9iEzvPpopxp2wQfu3SuZRHtpHNxMn8Fs0bS+gf5Xmwg==", "license": "MIT", "engines": { "node": ">= 12" } }, "node_modules/ipaddr.js": { - "version": "2.3.0", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.5.0.tgz", + "integrity": "sha512-aq+t5NAc+cS6rZQQVWC2x98CPqGtKKTMDd4Gaodv0wShnItdKg/51djkGJ1hqH+Oy0ivDftCbSLCQob8zso01w==", "license": "MIT", "engines": { "node": ">= 10" } }, "node_modules/jiti": { - "version": "2.6.1", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", "devOptional": true, "license": "MIT", "bin": { @@ -2618,6 +2654,8 @@ }, "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", @@ -2635,10 +2673,14 @@ }, "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/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", @@ -2658,6 +2700,8 @@ }, "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", @@ -2944,9 +2988,9 @@ } }, "node_modules/magic-string": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.2.tgz", - "integrity": "sha512-vG+rjFRj1PqdIBozIxAGMjPlOhaVe+GXpbttY/iSK7rGcJRMlwNJO7dcUwmUqkymsFLJiNGI06t4D7Fr7yRC9g==", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.3.tgz", + "integrity": "sha512-z12OxmaPGE0F4xlpGdjuAUckipLpQlTAuAmyGhNoD7ODpYUzvDfvtxUbjM/jwznyP178oju/KDdyi220wNJ0RQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2960,16 +3004,18 @@ "license": "ISC" }, "node_modules/mnemonist": { - "version": "0.40.0", + "version": "0.40.5", + "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.40.5.tgz", + "integrity": "sha512-egXDkYJsKZCizjk8FydZ3g8TOigBwt+dIaehgKcQSAE3DCkCTWG4N94trvwR1ZYeAHo1exxE+ZhZOUuj01HdNw==", "license": "MIT", "dependencies": { "obliterator": "^2.0.4" } }, "node_modules/nanoid": { - "version": "3.3.19", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", - "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", + "version": "3.3.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", "dev": true, "funding": [ { @@ -3020,6 +3066,8 @@ }, "node_modules/obliterator": { "version": "2.0.5", + "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-2.0.5.tgz", + "integrity": "sha512-42CPE9AhahZRsMNslczq0ctAEtqk8Eka26QofnqC346BZdHDySk3LWka23LI7ULIw11NmltpiLagIq8gBozxTw==", "license": "MIT" }, "node_modules/obug": { @@ -3045,6 +3093,8 @@ }, "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" @@ -3052,6 +3102,8 @@ }, "node_modules/pathe": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "devOptional": true, "license": "MIT" }, @@ -3084,7 +3136,9 @@ } }, "node_modules/pino": { - "version": "10.3.1", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-10.4.0.tgz", + "integrity": "sha512-bk1ZMTwG/Vymx+zPoa28MhNzucFvXJWp3csrSuxuHPSMeUettaCoQ5Jvg07mtC4JGNpcfTwz41oVCnNgBK9+vA==", "license": "MIT", "dependencies": { "@pinojs/redact": "^0.4.0", @@ -3094,7 +3148,7 @@ "pino-std-serializers": "^7.0.0", "process-warning": "^5.0.0", "quick-format-unescaped": "^4.0.3", - "real-require": "^0.2.0", + "real-require": "^1.0.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^4.0.1", "thread-stream": "^4.0.0" @@ -3105,6 +3159,8 @@ }, "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" @@ -3112,6 +3168,8 @@ }, "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/pkg-types": { @@ -3134,9 +3192,9 @@ "license": "MIT" }, "node_modules/postcss": { - "version": "8.5.28", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", - "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "version": "8.5.29", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.29.tgz", + "integrity": "sha512-49cGhUbXj8Qenv0iTMxA1cFBzxXoctpC9Ujd77t1WcbJIr6nF/eI7g/8MgxrYldFRuAXvja7xQRwavoW7kgrxQ==", "dev": true, "funding": [ { @@ -3155,9 +3213,9 @@ "license": "MIT", "peer": true, "dependencies": { - "nanoid": "^3.3.18", + "nanoid": "^3.3.19", "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" + "source-map-js": "^1.2.2" }, "engines": { "node": "^10 || ^12 || >=14" @@ -3224,6 +3282,8 @@ }, "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/rc9": { @@ -3252,14 +3312,15 @@ } }, "node_modules/real-require": { - "version": "0.2.0", - "license": "MIT", - "engines": { - "node": ">= 12.13.0" - } + "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/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" @@ -3276,6 +3337,8 @@ }, "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", @@ -3284,17 +3347,19 @@ }, "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/rolldown": { - "version": "1.2.12", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.12.tgz", - "integrity": "sha512-8wafseiaG80xmXSfqidUNqZcylTlhmPZZt+za2m+js2sFZ8dTNlhIOV2WcbIPx2hgwPBJpEUGFAMZ9bgBBLTSQ==", + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.13.tgz", + "integrity": "sha512-/5apOkwWxtACcQtZ5WlrAsKS4JwXVThQPJZ3RqIruRY9UG9M8e0VlXN068xum/JRKA4aIUoJ13ghe/kGCvidiQ==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@oxc-project/types": "=0.152.0", + "@oxc-project/types": "=0.153.0", "@rolldown/pluginutils": "^1.0.0" }, "bin": { @@ -3304,21 +3369,21 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm-eabi": "1.2.12", - "@rolldown/binding-android-arm64": "1.2.12", - "@rolldown/binding-darwin-arm64": "1.2.12", - "@rolldown/binding-darwin-x64": "1.2.12", - "@rolldown/binding-freebsd-x64": "1.2.12", - "@rolldown/binding-linux-arm-gnueabihf": "1.2.12", - "@rolldown/binding-linux-arm64-gnu": "1.2.12", - "@rolldown/binding-linux-arm64-musl": "1.2.12", - "@rolldown/binding-linux-ppc64-gnu": "1.2.12", - "@rolldown/binding-linux-s390x-gnu": "1.2.12", - "@rolldown/binding-linux-x64-gnu": "1.2.12", - "@rolldown/binding-linux-x64-musl": "1.2.12", - "@rolldown/binding-openharmony-arm64": "1.2.12", - "@rolldown/binding-win32-arm64-msvc": "1.2.12", - "@rolldown/binding-win32-x64-msvc": "1.2.12" + "@rolldown/binding-android-arm-eabi": "1.2.13", + "@rolldown/binding-android-arm64": "1.2.13", + "@rolldown/binding-darwin-arm64": "1.2.13", + "@rolldown/binding-darwin-x64": "1.2.13", + "@rolldown/binding-freebsd-x64": "1.2.13", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.13", + "@rolldown/binding-linux-arm64-gnu": "1.2.13", + "@rolldown/binding-linux-arm64-musl": "1.2.13", + "@rolldown/binding-linux-ppc64-gnu": "1.2.13", + "@rolldown/binding-linux-s390x-gnu": "1.2.13", + "@rolldown/binding-linux-x64-gnu": "1.2.13", + "@rolldown/binding-linux-x64-musl": "1.2.13", + "@rolldown/binding-openharmony-arm64": "1.2.13", + "@rolldown/binding-win32-arm64-msvc": "1.2.13", + "@rolldown/binding-win32-x64-msvc": "1.2.13" } }, "node_modules/safe-buffer": { @@ -3365,6 +3430,8 @@ }, "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" @@ -3378,6 +3445,8 @@ }, "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", @@ -3404,6 +3473,8 @@ }, "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/sharp": { @@ -3457,6 +3528,8 @@ }, "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" @@ -3475,20 +3548,24 @@ }, "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/std-env": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", - "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.3.0.tgz", + "integrity": "sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==", "dev": true, "license": "MIT" }, "node_modules/steed": { "version": "1.1.3", + "resolved": "https://registry.npmjs.org/steed/-/steed-1.1.3.tgz", + "integrity": "sha512-EUkci0FAUiE4IvGTSKcDJIQ/eRUP2JJb56+fvZ4sdnguLTqIdKjSxUe138poW8mkvKWXW2sFPrgTsxqoISnmoA==", "license": "MIT", "dependencies": { "fastfall": "^1.5.0", @@ -3499,19 +3576,21 @@ } }, "node_modules/thread-stream": { - "version": "4.0.0", + "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": "^0.2.0" + "real-require": "^1.0.0" }, "engines": { "node": ">=20" } }, "node_modules/tinybench": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.1.4.tgz", - "integrity": "sha512-9APumHG7r4yOk4X4WlkmE71aZcv1gvin1czO3OQ1U9iJcFA5Ja/ygyb0vPOVHTthFozUYs8CLoLUlM8grb2lTQ==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.2.1.tgz", + "integrity": "sha512-IAbQaPJIIdhVxi0mqy9lez1wEwje0UQf9F1vwtbelEG5Nbgn0nPOxCvU+mNeNS9gyJTAhrHp74CbUaSynQpZcQ==", "dev": true, "license": "MIT", "engines": { @@ -3546,10 +3625,12 @@ } }, "node_modules/toad-cache": { - "version": "3.7.0", + "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": ">=12" + "node": ">=20" } }, "node_modules/tslib": { @@ -3614,22 +3695,24 @@ } }, "node_modules/undici-types": { - "version": "7.16.0", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", "dev": true, "license": "MIT" }, "node_modules/vite": { - "version": "8.3.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.1.tgz", - "integrity": "sha512-/bvH9E9tmCXRGp2uXY3WbOldqpTwFkbha/8ANaEQ6VkxhH60KyqLwgZq6lG2y+4uT55x9+9eUHMpQ7uGnOCKjA==", + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.4.tgz", + "integrity": "sha512-IftLE8phJhdNRIGNEV4JCCvfNScoZQVfcXaN8TQropWGJtOHcu+iAB9pHcuGvmEb/kddw0LBe/jS8ztdC6xdiQ==", "dev": true, "license": "MIT", "peer": true, "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.7", - "postcss": "^8.5.28", - "rolldown": "~1.2.9", + "postcss": "^8.5.29", + "rolldown": "~1.2.12", "tinyglobby": "^0.2.17" }, "bin": { @@ -3795,6 +3878,8 @@ }, "node_modules/xtend": { "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", "license": "MIT", "engines": { "node": ">=0.4" diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 82f4872..ccfeea5 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,9 +8,9 @@ "name": "frontend", "version": "0.0.0", "dependencies": { - "@lucide/icons": "^1.49.0", - "lucide-react": "^1.49.0", - "posthog-js": "^1.435.6", + "@lucide/icons": "^1.52.0", + "lucide-react": "^1.52.0", + "posthog-js": "^1.436.0", "react": "^19.3.0", "react-dom": "^19.3.0" }, @@ -18,15 +18,15 @@ "@eslint/js": "^10.0.1", "@types/react": "^19.3.0", "@types/react-dom": "^19.3.0", - "@vitejs/plugin-react": "^6.1.1", + "@vitejs/plugin-react": "^6.1.2", "autoprefixer": "^10.6.1", - "eslint": "^10.11.0", + "eslint": "^10.12.0", "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.7", "globals": "^17.13.0", - "postcss": "^8.5.6", + "postcss": "^8.5.29", "tailwindcss": "^3.4.19", - "vite": "^8.3.1", + "vite": "^8.3.2", "vitest": "^5.0.3" } }, @@ -566,9 +566,9 @@ "license": "MIT" }, "node_modules/@lucide/icons": { - "version": "1.49.0", - "resolved": "https://registry.npmjs.org/@lucide/icons/-/icons-1.49.0.tgz", - "integrity": "sha512-TasECItykhUz9AGH+gKpujDlj+VsDeCZeO4RKvvFDPEyhS/+dtCQvQPMq7do4kwCZOX/IIuSZS6vvB8BFpb2hA==", + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/@lucide/icons/-/icons-1.52.0.tgz", + "integrity": "sha512-nO0tVQ0mzkLAI3PrB1uErtE8o+5wvmJTPTYYOp6GZ2tzunQa+VoauqYCVL0mIbHhJ3MBz7FiDObz6kViMl9nrA==", "license": "ISC" }, "node_modules/@nodelib/fs.scandir": { @@ -627,9 +627,9 @@ } }, "node_modules/@posthog/core": { - "version": "1.55.3", - "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.55.3.tgz", - "integrity": "sha512-XnmG9jo7WlxTeN4oCNWH4ESLQHR6ZPZ+mcq+8JxU62k5i1BAe3BSrybnmXHjqKp7e3rUvLK2bOzGZGSlfHvlNA==", + "version": "1.56.0", + "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.56.0.tgz", + "integrity": "sha512-WNlOoFQbOZyJzX54DhAslSpTjwDyXX5VLuRFNJjv172uOE9qHVUvtZAPP9dHZdq2HZkkLjbY27idH7UhzKSBVA==", "license": "MIT", "dependencies": { "@posthog/types": "^1.413.0" @@ -970,9 +970,9 @@ "optional": true }, "node_modules/@vitejs/plugin-react": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.1.1.tgz", - "integrity": "sha512-yxLaQV9gkhS8ezJqCM6+ndU7mDY6gqAg75NQ+0IjwEI8IYOmQCgkRwHKVSfWXW076DsqMo0Dk+0FK1U+M5RgFw==", + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.1.2.tgz", + "integrity": "sha512-fhJNGUdscQa4HkI0xFxwonWyhnVReCPDbXI7FH7jmzpnVCrFQI7PRrhWecO263610vOrs9tr2Y4xcf8uAdFxbw==", "dev": true, "license": "MIT", "dependencies": { @@ -984,7 +984,7 @@ "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", - "oxc-transform-react": "^0.145.0", + "oxc-transform-react": "^0.152.0", "vite": "^8.0.0" }, "peerDependenciesMeta": { @@ -1498,9 +1498,9 @@ } }, "node_modules/eslint": { - "version": "10.11.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.11.0.tgz", - "integrity": "sha512-P7a6UEEqb9G95MYAtqkmsTbVXIYyzIfl6NGOIJk162PaahFxFyeGcrlXYFSiagECg4sEm8IseJdZBKR3rx6MsQ==", + "version": "10.12.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.12.0.tgz", + "integrity": "sha512-npHjrHb2o4ehH3mE+YuxABR+0gyb0aVWCIolgJDArwCiHEIsonBm3ZKjel5b0U5+a+MbEvIyRCGCzD3eKKK2NA==", "dev": true, "license": "MIT", "workspaces": [ @@ -2417,9 +2417,9 @@ } }, "node_modules/lucide-react": { - "version": "1.49.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.49.0.tgz", - "integrity": "sha512-ApfaNM4/7jBrVopqLJ84Jz8TMQ1Q1JeZ1PmKh25aeS3E1FTVcMkSh1bEQB7dfzdIrwXRP8p3z4AJjTIHHb5IIA==", + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.52.0.tgz", + "integrity": "sha512-TUgPtl5ZI9WgxOcbu4ckaC5B3l1qxPQrCMgb6OfUB9owx/OvSJfcf93SMgUWAxRw5/1XDfh9uI5F5iNGXCcGDQ==", "license": "ISC", "peerDependencies": { "@types/react": "*", @@ -2694,9 +2694,9 @@ } }, "node_modules/postcss": { - "version": "8.5.28", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", - "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "version": "8.5.29", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.29.tgz", + "integrity": "sha512-49cGhUbXj8Qenv0iTMxA1cFBzxXoctpC9Ujd77t1WcbJIr6nF/eI7g/8MgxrYldFRuAXvja7xQRwavoW7kgrxQ==", "dev": true, "funding": [ { @@ -2714,9 +2714,9 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.18", + "nanoid": "^3.3.19", "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" + "source-map-js": "^1.2.2" }, "engines": { "node": "^10 || ^12 || >=14" @@ -2832,9 +2832,9 @@ } }, "node_modules/postcss-selector-parser": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", - "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.6.tgz", + "integrity": "sha512-7qASPzhKF2l2KLboRZux8CCTRMdGiV08vWmyKzPz22qZ7ZjQBOeY7rNzNoCLSUiftJ7HUq0GERHmxw/t0dCdMw==", "dev": true, "license": "MIT", "dependencies": { @@ -2852,13 +2852,13 @@ "dev": true }, "node_modules/posthog-js": { - "version": "1.435.6", - "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.435.6.tgz", - "integrity": "sha512-PdBZw1Np4Ch2ZzvQ/Ij0CTBrJSkSeEmDJsyJfbaipTYH8XJKJk45BYLtNgL4z9DlevRcYJxhwb15VrDzbw7shA==", + "version": "1.436.0", + "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.436.0.tgz", + "integrity": "sha512-AfwpUoc+PmBsQM1ghMGyMzQYDL+JziUOEGBGvJCB3TWJ8dsBDA020pg/Wo5L8+xF38hD6bKH4FtX8swjarMEnw==", "license": "(Apache-2.0 AND MIT)", "dependencies": { "@posthog/browser-common": "^0.9.1", - "@posthog/core": "^1.55.3", + "@posthog/core": "^1.56.0", "@posthog/types": "^1.413.0", "core-js": "^3.49.0", "dompurify": "^3.4.13", @@ -3144,10 +3144,11 @@ } }, "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==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -3366,16 +3367,16 @@ "dev": true }, "node_modules/vite": { - "version": "8.3.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.1.tgz", - "integrity": "sha512-/bvH9E9tmCXRGp2uXY3WbOldqpTwFkbha/8ANaEQ6VkxhH60KyqLwgZq6lG2y+4uT55x9+9eUHMpQ7uGnOCKjA==", + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.2.tgz", + "integrity": "sha512-SQr1x6W5vVSbROg7vsyXIaxK9b0G7zsT68acdWWRmnBUsgDieLCRG+Rep9WdZgcposvv/GSnr4GUUBqB3vXq6w==", "dev": true, "license": "MIT", "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.7", "postcss": "^8.5.28", - "rolldown": "~1.2.9", + "rolldown": "~1.2.11", "tinyglobby": "^0.2.17" }, "bin": { diff --git a/frontend/package.json b/frontend/package.json index 1aad05e..cb6217b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,9 +12,9 @@ "test:watch": "vitest" }, "dependencies": { - "@lucide/icons": "^1.49.0", - "lucide-react": "^1.49.0", - "posthog-js": "^1.435.6", + "@lucide/icons": "^1.52.0", + "lucide-react": "^1.52.0", + "posthog-js": "^1.436.0", "react": "^19.3.0", "react-dom": "^19.3.0" }, @@ -22,15 +22,18 @@ "@eslint/js": "^10.0.1", "@types/react": "^19.3.0", "@types/react-dom": "^19.3.0", - "@vitejs/plugin-react": "^6.1.1", + "@vitejs/plugin-react": "^6.1.2", "autoprefixer": "^10.6.1", - "eslint": "^10.11.0", + "eslint": "^10.12.0", "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.7", "globals": "^17.13.0", - "postcss": "^8.5.6", + "postcss": "^8.5.29", "tailwindcss": "^3.4.19", - "vite": "^8.3.1", + "vite": "^8.3.2", "vitest": "^5.0.3" + }, + "overrides": { + "postcss-selector-parser": "^7.1.6" } } diff --git a/landing/package-lock.json b/landing/package-lock.json index 515d249..9ca7711 100644 --- a/landing/package-lock.json +++ b/landing/package-lock.json @@ -8,16 +8,16 @@ "name": "landing", "version": "0.0.0", "dependencies": { - "posthog-js": "^1.435.6", + "posthog-js": "^1.435.8", "react": "^19.3.0", "react-dom": "^19.3.0" }, "devDependencies": { - "@vitejs/plugin-react": "^5.1.1", + "@vitejs/plugin-react": "^6.1.2", "autoprefixer": "^10.6.1", "postcss": "^8.5.6", "tailwindcss": "^3.4.19", - "vite": "^7.3.1" + "vite": "^8.3.4" } }, "node_modules/@alloc/quick-lru": { @@ -33,730 +33,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "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.8", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", - "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.8", - "@babel/types": "^7.29.8", - "@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.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", - "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", - "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.9", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.9.tgz", - "integrity": "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.29.8" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", - "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", - "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-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.8", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", - "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/generator": "^7.29.8", - "@babel/helper-globals": "^7.29.7", - "@babel/parser": "^7.29.8", - "@babel/template": "^7.29.7", - "@babel/types": "^7.29.8", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.29.8", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", - "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", - "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/@esbuild/aix-ppc64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", - "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", - "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", - "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", - "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", - "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", - "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", - "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", - "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", - "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", - "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", - "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", - "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", - "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", - "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", - "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", - "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", - "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", - "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", - "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", - "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", - "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", - "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", - "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", - "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", - "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", - "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -768,17 +44,6 @@ "@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", @@ -845,6 +110,16 @@ "node": ">= 8" } }, + "node_modules/@oxc-project/types": { + "version": "0.153.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.153.0.tgz", + "integrity": "sha512-jSNrRIyvPVLL5E1z/tS5zLAprosGz/FuI1PWwEOz59j3E4TTTzSpqKb/RDGqJhhW0QHYISGXxq582wGx6T11Tw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, "node_modules/@posthog/browser-common": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/@posthog/browser-common/-/browser-common-0.9.1.tgz", @@ -870,17 +145,10 @@ "integrity": "sha512-tYAVzNojWckuG/wxBB0fwE70sFruQQq4xbhkci7eH2ARU3mMfUTkvpN1HwBMpm0AtbxidLiH0IFvagmdJ8opPw==", "license": "MIT" }, - "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.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", - "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.13.tgz", + "integrity": "sha512-Vu/NICrgdRvUOeaJZIyB2FP63s6fO/4ud5l8dXmxDx9yDyn+mN0y+gsvsVviSli+T361Aqo0h46EuMoobrwawA==", "cpu": [ "arm" ], @@ -889,12 +157,15 @@ "optional": true, "os": [ "android" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", - "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.13.tgz", + "integrity": "sha512-6LEoIrGy5vvCd2boNnv2soPSUZj/PRvhp8R/t9KlX+mIZxThIE/ZqxrGXxmuNX0FuvQFvLdZr7S4HrYpYBVhEQ==", "cpu": [ "arm64" ], @@ -903,12 +174,15 @@ "optional": true, "os": [ "android" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", - "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.13.tgz", + "integrity": "sha512-CaJSRzlQ6Q+zkDMVA9OkVxBjVoPRCDEE3v0FSJ9kIQBgRjFApA3Si4ijhbNVUji7ZJt+PfO0e46qy4nPInHUPA==", "cpu": [ "arm64" ], @@ -917,12 +191,15 @@ "optional": true, "os": [ "darwin" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", - "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.13.tgz", + "integrity": "sha512-e4r340I5hxSzJkD3YJBxgzq/rJ3wwxwdLLb2FPt1Zpyem20nrQPT11DDEEkazwxaHumH8wpglsp/Ozgj/wT6mw==", "cpu": [ "x64" ], @@ -931,26 +208,15 @@ "optional": true, "os": [ "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", - "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", - "cpu": [ - "arm64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", - "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.13.tgz", + "integrity": "sha512-hoBWDZ4/K/mJKVMbsgUd1jfpA2DiOrJ54mpegObU9TBHBBrtX4FLIXJJQRDPZ/2Dn91GvPob+Z2hwKJzf4E5SQ==", "cpu": [ "x64" ], @@ -959,12 +225,15 @@ "optional": true, "os": [ "freebsd" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", - "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.13.tgz", + "integrity": "sha512-oF9fZu3vTWnTiVAkDFxU9PJLt1nh20K9RRpuHsR7ZnE2L7XookBbDBVLojCVSLGbvtSlxeUNnjAj6DbNsKyzbg==", "cpu": [ "arm" ], @@ -973,26 +242,15 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", - "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", - "cpu": [ - "arm" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", - "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.13.tgz", + "integrity": "sha512-DTpb/+jMiqWMPgphn4ksW1zW+XLJ6dJJ9FMvnIk9I291LhCctMPhMYJrP5t9p6C9qa3KXadDdUEZHBaPh3kNxQ==", "cpu": [ "arm64" ], @@ -1001,12 +259,15 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", - "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.13.tgz", + "integrity": "sha512-xU+Kvv9N2A3SY35zFh1FWWNjxbadE/BKeqnkXe7vScU4GZsw757BlVmDLITZEq8DPpk6m7uoDYkK7P+x/uU5Ow==", "cpu": [ "arm64" ], @@ -1015,40 +276,15 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", - "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", - "cpu": [ - "loong64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", - "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", - "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.13.tgz", + "integrity": "sha512-XMk/hnhh25oQ756Lclb1Q11h4ll0xC0Esa5w4Vzp+mAE2S3AGJgeM0SonrIgY8fHhjarb6hcoTuwoFG0VVP2ig==", "cpu": [ "ppc64" ], @@ -1057,54 +293,15 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", - "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", - "cpu": [ - "ppc64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", - "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", - "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", - "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.13.tgz", + "integrity": "sha512-U1BoTHYmlywE2n3HUWjzZ+8EQsKr+OwpxYuDWaubx42tUL8QSL7nOqRz/NVHanibbFP3M1Rxuprz/wNcbMKjqw==", "cpu": [ "s390x" ], @@ -1113,12 +310,15 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", - "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.13.tgz", + "integrity": "sha512-J4cm80Pc6wEwV5AZ42o62sIQgDk+Y7R2s646jIwuvO+4V46QB58N4vVuMPWCBaNgursXLpmUWJV//WQAkPvHBQ==", "cpu": [ "x64" ], @@ -1127,12 +327,15 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", - "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.13.tgz", + "integrity": "sha512-W0euZS40U4V6JkmEnVCIX9mcsL6cbXQ4X8c2w+MMOIjFg68TXUW+u3RT+2e1t1pv9mg/EP3pDtK3POXdQ7Rxyw==", "cpu": [ "x64" ], @@ -1141,26 +344,15 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", - "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", - "cpu": [ - "x64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", - "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.13.tgz", + "integrity": "sha512-do/AF/4n9MRshia3a5aXoy8xM2c6mgfT6D7H/dY6my5z+hfCIR4ab7mk4IRj++nsEQpKy39kd0DQnDq+r692tw==", "cpu": [ "arm64" ], @@ -1169,12 +361,15 @@ "optional": true, "os": [ "openharmony" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", - "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.13.tgz", + "integrity": "sha512-6FtUf2cJZobutJdxnGYX8z0vtbgqU0b2v8vfBhkXb9wp2MppMFtkunE+IaOntyi01rkIQoPW2GHABtfzsC0BRQ==", "cpu": [ "arm64" ], @@ -1183,26 +378,15 @@ "optional": true, "os": [ "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", - "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", - "cpu": [ - "ia32" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", - "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.13.tgz", + "integrity": "sha512-UKhFaZTeTcbKrb3iU82m17jVcPEAoqDkCN5MBqgPyZSwp/cQmib21crTS0DdH7T1B7wZRWEI89YQAOJMet5yhg==", "cpu": [ "x64" ], @@ -1211,71 +395,15 @@ "optional": true, "os": [ "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", - "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", - "cpu": [ - "x64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "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": "*" + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "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/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", "dev": true, "license": "MIT" }, @@ -1287,24 +415,33 @@ "optional": true }, "node_modules/@vitejs/plugin-react": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.4.tgz", - "integrity": "sha512-VIcFLdRi/VYRU8OL/puL7QXMYafHmqOnwTZY50U1JPlCNj30PxCMx65c494b1K9be9hX83KVt0+gTEwTWLqToA==", + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.1.2.tgz", + "integrity": "sha512-fhJNGUdscQa4HkI0xFxwonWyhnVReCPDbXI7FH7jmzpnVCrFQI7PRrhWecO263610vOrs9tr2Y4xcf8uAdFxbw==", "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" + "@rolldown/pluginutils": "^1.0.1" }, "engines": { "node": "^20.19.0 || >=22.12.0" }, "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "oxc-transform-react": "^0.152.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "oxc-transform-react": { + "optional": true + } } }, "node_modules/any-promise": { @@ -1524,13 +661,6 @@ "node": ">= 6" } }, - "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/core-js": { "version": "3.50.0", "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.50.0.tgz", @@ -1558,22 +688,14 @@ "node": ">=4" } }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "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==", "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, + "license": "Apache-2.0", "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "node": ">=8" } }, "node_modules/didyoumean": { @@ -1606,48 +728,6 @@ "dev": true, "license": "ISC" }, - "node_modules/esbuild": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", - "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.28.2", - "@esbuild/android-arm": "0.28.2", - "@esbuild/android-arm64": "0.28.2", - "@esbuild/android-x64": "0.28.2", - "@esbuild/darwin-arm64": "0.28.2", - "@esbuild/darwin-x64": "0.28.2", - "@esbuild/freebsd-arm64": "0.28.2", - "@esbuild/freebsd-x64": "0.28.2", - "@esbuild/linux-arm": "0.28.2", - "@esbuild/linux-arm64": "0.28.2", - "@esbuild/linux-ia32": "0.28.2", - "@esbuild/linux-loong64": "0.28.2", - "@esbuild/linux-mips64el": "0.28.2", - "@esbuild/linux-ppc64": "0.28.2", - "@esbuild/linux-riscv64": "0.28.2", - "@esbuild/linux-s390x": "0.28.2", - "@esbuild/linux-x64": "0.28.2", - "@esbuild/netbsd-arm64": "0.28.2", - "@esbuild/netbsd-x64": "0.28.2", - "@esbuild/openbsd-arm64": "0.28.2", - "@esbuild/openbsd-x64": "0.28.2", - "@esbuild/openharmony-arm64": "0.28.2", - "@esbuild/sunos-x64": "0.28.2", - "@esbuild/win32-arm64": "0.28.2", - "@esbuild/win32-ia32": "0.28.2", - "@esbuild/win32-x64": "0.28.2" - } - }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -1756,16 +836,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "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/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -1864,37 +934,265 @@ "jiti": "bin/jiti.js" } }, - "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==", + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", "dev": true, - "license": "MIT" - }, - "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" + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" }, "engines": { - "node": ">=6" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" } }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=6" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, "node_modules/lilconfig": { @@ -1917,16 +1215,6 @@ "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/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -1951,13 +1239,6 @@ "node": ">=8.6" } }, - "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/mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", @@ -2077,9 +1358,9 @@ } }, "node_modules/postcss": { - "version": "8.5.28", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", - "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "version": "8.5.29", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.29.tgz", + "integrity": "sha512-49cGhUbXj8Qenv0iTMxA1cFBzxXoctpC9Ujd77t1WcbJIr6nF/eI7g/8MgxrYldFRuAXvja7xQRwavoW7kgrxQ==", "dev": true, "funding": [ { @@ -2097,9 +1378,9 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.18", + "nanoid": "^3.3.19", "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" + "source-map-js": "^1.2.2" }, "engines": { "node": "^10 || ^12 || >=14" @@ -2219,9 +1500,9 @@ } }, "node_modules/postcss-selector-parser": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", - "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.6.tgz", + "integrity": "sha512-7qASPzhKF2l2KLboRZux8CCTRMdGiV08vWmyKzPz22qZ7ZjQBOeY7rNzNoCLSUiftJ7HUq0GERHmxw/t0dCdMw==", "dev": true, "license": "MIT", "dependencies": { @@ -2240,9 +1521,9 @@ "license": "MIT" }, "node_modules/posthog-js": { - "version": "1.435.6", - "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.435.6.tgz", - "integrity": "sha512-PdBZw1Np4Ch2ZzvQ/Ij0CTBrJSkSeEmDJsyJfbaipTYH8XJKJk45BYLtNgL4z9DlevRcYJxhwb15VrDzbw7shA==", + "version": "1.435.8", + "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.435.8.tgz", + "integrity": "sha512-rUrv0kKhD72Wfu++pBlupvrpt1Z60uTAgNUChAH232a05bbRBzV0P4JqbuZEAaaUXumhuB3a8r+iFMzm/2l1Ow==", "license": "(Apache-2.0 AND MIT)", "dependencies": { "@posthog/browser-common": "^0.9.1", @@ -2335,16 +1616,6 @@ "react": "^19.3.0" } }, - "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/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -2400,49 +1671,38 @@ "node": ">=0.10.0" } }, - "node_modules/rollup": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", - "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "node_modules/rolldown": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.13.tgz", + "integrity": "sha512-/5apOkwWxtACcQtZ5WlrAsKS4JwXVThQPJZ3RqIruRY9UG9M8e0VlXN068xum/JRKA4aIUoJ13ghe/kGCvidiQ==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.8" + "@oxc-project/types": "=0.153.0", + "@rolldown/pluginutils": "^1.0.0" }, "bin": { - "rollup": "dist/bin/rollup" + "rolldown": "bin/cli.mjs" }, "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" + "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.59.0", - "@rollup/rollup-android-arm64": "4.59.0", - "@rollup/rollup-darwin-arm64": "4.59.0", - "@rollup/rollup-darwin-x64": "4.59.0", - "@rollup/rollup-freebsd-arm64": "4.59.0", - "@rollup/rollup-freebsd-x64": "4.59.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", - "@rollup/rollup-linux-arm-musleabihf": "4.59.0", - "@rollup/rollup-linux-arm64-gnu": "4.59.0", - "@rollup/rollup-linux-arm64-musl": "4.59.0", - "@rollup/rollup-linux-loong64-gnu": "4.59.0", - "@rollup/rollup-linux-loong64-musl": "4.59.0", - "@rollup/rollup-linux-ppc64-gnu": "4.59.0", - "@rollup/rollup-linux-ppc64-musl": "4.59.0", - "@rollup/rollup-linux-riscv64-gnu": "4.59.0", - "@rollup/rollup-linux-riscv64-musl": "4.59.0", - "@rollup/rollup-linux-s390x-gnu": "4.59.0", - "@rollup/rollup-linux-x64-gnu": "4.59.0", - "@rollup/rollup-linux-x64-musl": "4.59.0", - "@rollup/rollup-openbsd-x64": "4.59.0", - "@rollup/rollup-openharmony-arm64": "4.59.0", - "@rollup/rollup-win32-arm64-msvc": "4.59.0", - "@rollup/rollup-win32-ia32-msvc": "4.59.0", - "@rollup/rollup-win32-x64-gnu": "4.59.0", - "@rollup/rollup-win32-x64-msvc": "4.59.0", - "fsevents": "~2.3.2" + "@rolldown/binding-android-arm-eabi": "1.2.13", + "@rolldown/binding-android-arm64": "1.2.13", + "@rolldown/binding-darwin-arm64": "1.2.13", + "@rolldown/binding-darwin-x64": "1.2.13", + "@rolldown/binding-freebsd-x64": "1.2.13", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.13", + "@rolldown/binding-linux-arm64-gnu": "1.2.13", + "@rolldown/binding-linux-arm64-musl": "1.2.13", + "@rolldown/binding-linux-ppc64-gnu": "1.2.13", + "@rolldown/binding-linux-s390x-gnu": "1.2.13", + "@rolldown/binding-linux-x64-gnu": "1.2.13", + "@rolldown/binding-linux-x64-musl": "1.2.13", + "@rolldown/binding-openharmony-arm64": "1.2.13", + "@rolldown/binding-win32-arm64-msvc": "1.2.13", + "@rolldown/binding-win32-x64-msvc": "1.2.13" } }, "node_modules/run-parallel": { @@ -2475,20 +1735,10 @@ "integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==", "license": "MIT" }, - "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/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==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -2593,14 +1843,14 @@ } }, "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "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.3" + "picomatch": "^4.0.4" }, "engines": { "node": ">=12.0.0" @@ -2699,18 +1949,17 @@ "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==", + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.4.tgz", + "integrity": "sha512-IftLE8phJhdNRIGNEV4JCCvfNScoZQVfcXaN8TQropWGJtOHcu+iAB9pHcuGvmEb/kddw0LBe/jS8ztdC6xdiQ==", "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" + "lightningcss": "^1.33.0", + "picomatch": "^4.0.7", + "postcss": "^8.5.29", + "rolldown": "~1.2.12", + "tinyglobby": "^0.2.17" }, "bin": { "vite": "bin/vite.js" @@ -2726,9 +1975,10 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.7.1", + "esbuild": "^0.27.0 || ^0.28.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", @@ -2741,15 +1991,18 @@ "@types/node": { "optional": true }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, "jiti": { "optional": true }, "less": { "optional": true }, - "lightningcss": { - "optional": true - }, "sass": { "optional": true }, @@ -2773,24 +2026,6 @@ } } }, - "node_modules/vite/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/vite/node_modules/picomatch": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", @@ -2816,13 +2051,6 @@ "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-6.2.1.tgz", "integrity": "sha512-rLcLXA2sx6+9dE88NHFubwTtGxpK4yYBLj6qHPdFoCaLr0cXGb4efOqtKLlm4loGA4OEKHIQKMKzZkKyOh5ctw==", "license": "Apache-2.0" - }, - "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" } } } diff --git a/landing/package.json b/landing/package.json index a52efa3..0732140 100644 --- a/landing/package.json +++ b/landing/package.json @@ -10,15 +10,18 @@ "test": "node --test" }, "dependencies": { - "posthog-js": "^1.435.6", + "posthog-js": "^1.435.8", "react": "^19.3.0", "react-dom": "^19.3.0" }, "devDependencies": { - "@vitejs/plugin-react": "^5.1.1", + "@vitejs/plugin-react": "^6.1.2", "autoprefixer": "^10.6.1", "postcss": "^8.5.6", "tailwindcss": "^3.4.19", - "vite": "^7.3.1" + "vite": "^8.3.4" + }, + "overrides": { + "postcss-selector-parser": "^7.1.6" } } diff --git a/landing/vite.config.js b/landing/vite.config.js index 927fd48..d65ddb9 100644 --- a/landing/vite.config.js +++ b/landing/vite.config.js @@ -4,6 +4,8 @@ import react from '@vitejs/plugin-react' export default defineConfig({ plugins: [react()], build: { + // Mêmes navigateurs qu'avec Vite 7, comme le front (Vite 8 relève sa cible par défaut) + target: ['chrome107', 'edge107', 'firefox104', 'safari16'], outDir: 'dist', emptyOutDir: true, }, From 2e638d8a433a5aab5534d2c03b2ca0fd614e5ffe Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 9 Oct 2026 13:23:34 +0000 Subject: [PATCH 03/11] Bump posthog-js Bumps the landing-minor-patch group with 1 update in the /landing directory: [posthog-js](https://github.com/PostHog/posthog-js). Updates `posthog-js` from 1.435.8 to 1.438.0 - [Release notes](https://github.com/PostHog/posthog-js/releases) - [Changelog](https://github.com/PostHog/posthog-js/blob/main/CHANGELOG.md) - [Commits](https://github.com/PostHog/posthog-js/compare/posthog-js@1.435.8...posthog-js@1.438.0) --- updated-dependencies: - dependency-name: posthog-js dependency-version: 1.435.8 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: landing-minor-patch ... Signed-off-by: dependabot[bot] --- landing/package-lock.json | 26 +++++++++++++------------- landing/package.json | 2 +- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/landing/package-lock.json b/landing/package-lock.json index 9ca7711..88a8d99 100644 --- a/landing/package-lock.json +++ b/landing/package-lock.json @@ -8,7 +8,7 @@ "name": "landing", "version": "0.0.0", "dependencies": { - "posthog-js": "^1.435.8", + "posthog-js": "^1.438.0", "react": "^19.3.0", "react-dom": "^19.3.0" }, @@ -131,18 +131,18 @@ } }, "node_modules/@posthog/core": { - "version": "1.55.3", - "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.55.3.tgz", - "integrity": "sha512-XnmG9jo7WlxTeN4oCNWH4ESLQHR6ZPZ+mcq+8JxU62k5i1BAe3BSrybnmXHjqKp7e3rUvLK2bOzGZGSlfHvlNA==", + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.57.0.tgz", + "integrity": "sha512-ZpIWCSkog0v4whufAXetagBsVLqE1Zjl5DsHC2squTy8nS/vqZ9W5JvWkJFgHK8hk5x1gdwMoXY7FK2S0YRT1Q==", "license": "MIT", "dependencies": { - "@posthog/types": "^1.413.0" + "@posthog/types": "^1.415.0" } }, "node_modules/@posthog/types": { - "version": "1.413.0", - "resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.413.0.tgz", - "integrity": "sha512-tYAVzNojWckuG/wxBB0fwE70sFruQQq4xbhkci7eH2ARU3mMfUTkvpN1HwBMpm0AtbxidLiH0IFvagmdJ8opPw==", + "version": "1.415.0", + "resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.415.0.tgz", + "integrity": "sha512-LGVu1f8r1Hi97yCwXg6PJn7chIwjxosAkkNrh7RIDRtFC5Kc+rkqHqFv73KrjEgCj7Se+0OR5esI4Tg0io6kuA==", "license": "MIT" }, "node_modules/@rolldown/binding-android-arm-eabi": { @@ -1521,14 +1521,14 @@ "license": "MIT" }, "node_modules/posthog-js": { - "version": "1.435.8", - "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.435.8.tgz", - "integrity": "sha512-rUrv0kKhD72Wfu++pBlupvrpt1Z60uTAgNUChAH232a05bbRBzV0P4JqbuZEAaaUXumhuB3a8r+iFMzm/2l1Ow==", + "version": "1.438.0", + "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.438.0.tgz", + "integrity": "sha512-8dmylTANRuoEyY+4f6Tj1ScAIEehrUZufh0zNzYedn371jMxDNJD2yq1FMxzSS7RfTxjYl36kC8McwEZ4BFzLQ==", "license": "(Apache-2.0 AND MIT)", "dependencies": { "@posthog/browser-common": "^0.9.1", - "@posthog/core": "^1.55.3", - "@posthog/types": "^1.413.0", + "@posthog/core": "^1.57.0", + "@posthog/types": "^1.415.0", "core-js": "^3.49.0", "dompurify": "^3.4.13", "fflate": "^0.4.8", diff --git a/landing/package.json b/landing/package.json index 0732140..b7a1de3 100644 --- a/landing/package.json +++ b/landing/package.json @@ -10,7 +10,7 @@ "test": "node --test" }, "dependencies": { - "posthog-js": "^1.435.8", + "posthog-js": "^1.438.0", "react": "^19.3.0", "react-dom": "^19.3.0" }, From ada823ab471bf21752e98a4857a644b08cb29752 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 9 Oct 2026 13:23:56 +0000 Subject: [PATCH 04/11] Bump the frontend-minor-patch group across 1 directory with 2 updates Bumps the frontend-minor-patch group with 2 updates in the /frontend directory: [posthog-js](https://github.com/PostHog/posthog-js) and [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite). Updates `posthog-js` from 1.436.0 to 1.438.0 - [Release notes](https://github.com/PostHog/posthog-js/releases) - [Changelog](https://github.com/PostHog/posthog-js/blob/main/CHANGELOG.md) - [Commits](https://github.com/PostHog/posthog-js/compare/posthog-js@1.436.0...posthog-js@1.438.0) Updates `vite` from 8.3.2 to 8.3.3 - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/v8.3.3/packages/vite) --- updated-dependencies: - dependency-name: posthog-js dependency-version: 1.438.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: frontend-minor-patch - dependency-name: vite dependency-version: 8.3.3 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: frontend-minor-patch ... Signed-off-by: dependabot[bot] --- frontend/package-lock.json | 34 +++++++++++++++++----------------- frontend/package.json | 4 ++-- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index ccfeea5..f547957 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -10,7 +10,7 @@ "dependencies": { "@lucide/icons": "^1.52.0", "lucide-react": "^1.52.0", - "posthog-js": "^1.436.0", + "posthog-js": "^1.438.0", "react": "^19.3.0", "react-dom": "^19.3.0" }, @@ -26,7 +26,7 @@ "globals": "^17.13.0", "postcss": "^8.5.29", "tailwindcss": "^3.4.19", - "vite": "^8.3.2", + "vite": "^8.3.3", "vitest": "^5.0.3" } }, @@ -627,18 +627,18 @@ } }, "node_modules/@posthog/core": { - "version": "1.56.0", - "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.56.0.tgz", - "integrity": "sha512-WNlOoFQbOZyJzX54DhAslSpTjwDyXX5VLuRFNJjv172uOE9qHVUvtZAPP9dHZdq2HZkkLjbY27idH7UhzKSBVA==", + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.57.0.tgz", + "integrity": "sha512-ZpIWCSkog0v4whufAXetagBsVLqE1Zjl5DsHC2squTy8nS/vqZ9W5JvWkJFgHK8hk5x1gdwMoXY7FK2S0YRT1Q==", "license": "MIT", "dependencies": { - "@posthog/types": "^1.413.0" + "@posthog/types": "^1.415.0" } }, "node_modules/@posthog/types": { - "version": "1.413.0", - "resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.413.0.tgz", - "integrity": "sha512-tYAVzNojWckuG/wxBB0fwE70sFruQQq4xbhkci7eH2ARU3mMfUTkvpN1HwBMpm0AtbxidLiH0IFvagmdJ8opPw==", + "version": "1.415.0", + "resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.415.0.tgz", + "integrity": "sha512-LGVu1f8r1Hi97yCwXg6PJn7chIwjxosAkkNrh7RIDRtFC5Kc+rkqHqFv73KrjEgCj7Se+0OR5esI4Tg0io6kuA==", "license": "MIT" }, "node_modules/@rolldown/binding-android-arm-eabi": { @@ -2852,14 +2852,14 @@ "dev": true }, "node_modules/posthog-js": { - "version": "1.436.0", - "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.436.0.tgz", - "integrity": "sha512-AfwpUoc+PmBsQM1ghMGyMzQYDL+JziUOEGBGvJCB3TWJ8dsBDA020pg/Wo5L8+xF38hD6bKH4FtX8swjarMEnw==", + "version": "1.438.0", + "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.438.0.tgz", + "integrity": "sha512-8dmylTANRuoEyY+4f6Tj1ScAIEehrUZufh0zNzYedn371jMxDNJD2yq1FMxzSS7RfTxjYl36kC8McwEZ4BFzLQ==", "license": "(Apache-2.0 AND MIT)", "dependencies": { "@posthog/browser-common": "^0.9.1", - "@posthog/core": "^1.56.0", - "@posthog/types": "^1.413.0", + "@posthog/core": "^1.57.0", + "@posthog/types": "^1.415.0", "core-js": "^3.49.0", "dompurify": "^3.4.13", "fflate": "^0.4.8", @@ -3367,9 +3367,9 @@ "dev": true }, "node_modules/vite": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.2.tgz", - "integrity": "sha512-SQr1x6W5vVSbROg7vsyXIaxK9b0G7zsT68acdWWRmnBUsgDieLCRG+Rep9WdZgcposvv/GSnr4GUUBqB3vXq6w==", + "version": "8.3.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.3.tgz", + "integrity": "sha512-cTAldKPImjg6c+gk48U19POPn3GCBzZwpdsN8ZMEEcbpes+6/wvfqUd0C2y3qYY4wsj8PwgFwrZ/1jBPVttMSg==", "dev": true, "license": "MIT", "dependencies": { diff --git a/frontend/package.json b/frontend/package.json index cb6217b..d6b914e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,7 +14,7 @@ "dependencies": { "@lucide/icons": "^1.52.0", "lucide-react": "^1.52.0", - "posthog-js": "^1.436.0", + "posthog-js": "^1.438.0", "react": "^19.3.0", "react-dom": "^19.3.0" }, @@ -30,7 +30,7 @@ "globals": "^17.13.0", "postcss": "^8.5.29", "tailwindcss": "^3.4.19", - "vite": "^8.3.2", + "vite": "^8.3.3", "vitest": "^5.0.3" }, "overrides": { From 91abe476148a3132a51983816b31ec81b79f090b Mon Sep 17 00:00:00 2001 From: IliesAllali Date: Fri, 9 Oct 2026 17:42:14 +0200 Subject: [PATCH 05/11] Docker : image tout-en-un pour Unraid et les NAS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Une seule image (PostgreSQL 15, API, nginx) qui crée sa base et ses secrets dans /data au premier démarrage. Publiée par images.yml sous ghcr.io/iliesallali/parenthese-aio, testée en CI (démarrage à vide, redémarrage, test de fumée). Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 41 ++++++++++++++ .github/workflows/images.yml | 4 ++ CHANGELOG.md | 6 ++ Dockerfile.aio | 94 +++++++++++++++++++++++++++++++ Dockerfile.aio.dockerignore | 20 +++++++ README.fr.md | 10 ++++ README.md | 10 ++++ deploy/aio/entrypoint.sh | 106 +++++++++++++++++++++++++++++++++++ 8 files changed, 291 insertions(+) create mode 100644 Dockerfile.aio create mode 100644 Dockerfile.aio.dockerignore create mode 100755 deploy/aio/entrypoint.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 43a1fd4..96efca6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -191,3 +191,44 @@ jobs: - name: Arrêt et suppression des volumes if: always() run: docker compose down -v --remove-orphans + + docker-aio: + name: Docker tout-en-un (Unraid, NAS) + runs-on: ubuntu-latest + timeout-minutes: 30 + env: + SMOKE_WAIT_SECONDS: 180 + steps: + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0 + with: + persist-credentials: false + + - name: Construction + run: DOCKER_BUILDKIT=1 docker build -f Dockerfile.aio -t parenthese-aio:test . + + - name: Démarrage, sans aucune variable + run: docker run -d --name aio -p 8080:80 -v aio-data:/data parenthese-aio:test + + - name: Test de fumée par le port web + run: bash tools/docker-smoke.sh http://localhost:8080 + + # Les secrets et la base de /data doivent servir tels quels au démarrage suivant (mise à jour, reboot). + - name: Redémarrage puis nouveau test + run: | + set -euo pipefail + docker restart aio + bash tools/docker-smoke.sh http://localhost:8080 + docker logs aio 2>&1 | grep -c "première installation" | grep -qx 1 + + - name: Journaux en cas d'échec + if: failure() + run: | + docker ps -a + docker logs --timestamps aio || true + docker exec aio cat /data/postgres.log || true + + - name: Arrêt et suppression du volume + if: always() + run: | + docker rm -f aio || true + docker volume rm aio-data || true diff --git a/.github/workflows/images.yml b/.github/workflows/images.yml index 9e10e15..b56340d 100644 --- a/.github/workflows/images.yml +++ b/.github/workflows/images.yml @@ -53,6 +53,10 @@ jobs: - image: parenthese-web context: . file: Dockerfile.web + # Tout-en-un (base, API, front) pour Unraid et les NAS + - image: parenthese-aio + context: . + file: Dockerfile.aio steps: # Sur workflow_run, le commit exact que la CI a validé, pas la tête de main au moment du run. - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index 170da9e..e0fd1ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ Notable changes to Parenthèse. Dates are in YYYY-MM-DD format. +## Unreleased + +### Self-hosting + +- Single-container image `ghcr.io/iliesallali/parenthese-aio` (PostgreSQL, API and front end), for Unraid and NAS systems that install one container per app. It needs no configuration: the database, its password and the session key are created in `/data` on first start. Tested in CI by a fresh start, a restart and the smoke test. + ## 1.0.2 (2026-10-09) ### Self-hosting diff --git a/Dockerfile.aio b/Dockerfile.aio new file mode 100644 index 0000000..8fa1e3c --- /dev/null +++ b/Dockerfile.aio @@ -0,0 +1,94 @@ +# syntax=docker/dockerfile:1 +# Parenthèse en un seul conteneur : PostgreSQL, API et front servi par nginx. Contexte de build : la racine du dépôt. +# Pour les systèmes qui installent une application par conteneur (Unraid, NAS). docker-compose.yml reste +# l'installation de référence, avec un conteneur par service. +# +# docker run -d --name parenthese -p 8080:80 -v parenthese-data:/data ghcr.io/iliesallali/parenthese-aio +# +# Tout ce qui doit survivre à une mise à jour vit dans /data : base (postgres/), photos et vidéos (media/), +# secrets générés au premier démarrage (secrets/). Voir deploy/aio/entrypoint.sh. + +# Étape 1 : API (mêmes étapes que backend/Dockerfile). +FROM node:24-bookworm-slim AS api-build + +RUN apt-get update \ + && apt-get install -y --no-install-recommends openssl ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +COPY backend/package.json backend/package-lock.json ./ +COPY backend/prisma ./prisma +RUN npm ci --no-audit --no-fund +RUN npx prisma generate + +COPY backend/tsconfig.json ./ +COPY backend/src ./src +RUN npm run build + + +# Étape 2 : front (mêmes étapes que Dockerfile.web). +FROM node:24-alpine AS web-build + +WORKDIR /app + +COPY frontend/package.json frontend/package-lock.json ./ +RUN npm ci + +COPY frontend/ ./ + +ENV VITE_API_BASE_URL="" +RUN npm run build + + +# Étape 3 : image finale. +FROM node:24-bookworm-slim + +# postgresql-common d'abord, pour qu'il ne crée pas de base par défaut à l'installation de PostgreSQL : +# la base vit dans /data et naît au premier démarrage. +RUN apt-get update \ + && apt-get install -y --no-install-recommends postgresql-common \ + && sed -i 's/^#\?\s*create_main_cluster.*/create_main_cluster = false/' /etc/postgresql-common/createcluster.conf \ + && apt-get install -y --no-install-recommends postgresql-15 nginx openssl ca-certificates tini \ + && rm -rf /var/lib/apt/lists/* \ + && rm -f /etc/nginx/sites-enabled/default \ + && mkdir -p /run/postgresql && chown postgres:postgres /run/postgresql + +WORKDIR /app + +COPY backend/package.json backend/package-lock.json ./ +RUN npm ci --omit=dev --no-audit --no-fund \ + && npm cache clean --force + +# CLI Prisma à part, à la version du package-lock (voir backend/Dockerfile). +RUN PRISMA_VERSION="$(node -p "require('./package-lock.json').packages['node_modules/prisma'].version")" \ + && mkdir -p /opt/prisma \ + && cd /opt/prisma \ + && echo '{"private":true}' > package.json \ + && npm install --no-audit --no-fund "prisma@${PRISMA_VERSION}" \ + && npm cache clean --force + +COPY --from=api-build /app/node_modules/.prisma ./node_modules/.prisma +COPY backend/prisma ./prisma +COPY --from=api-build /app/dist ./dist + +# nginx : la configuration du conteneur web de compose, l'API étant ici sur la même machine. +COPY deploy/nginx.docker.conf /etc/nginx/conf.d/default.conf +RUN sed -i 's|http://api:4000|http://127.0.0.1:4000|g' /etc/nginx/conf.d/default.conf \ + && ! grep -q 'api:4000' /etc/nginx/conf.d/default.conf +COPY --from=web-build /app/dist /usr/share/nginx/html + +COPY deploy/aio/entrypoint.sh /usr/local/bin/parenthese-aio +RUN chmod 755 /usr/local/bin/parenthese-aio + +ENV NODE_ENV=production \ + CHECKPOINT_DISABLE=1 \ + PATH=/opt/prisma/node_modules/.bin:/usr/lib/postgresql/15/bin:$PATH + +VOLUME /data +EXPOSE 80 + +HEALTHCHECK --interval=15s --timeout=5s --start-period=60s --retries=5 \ + CMD node -e "fetch('http://127.0.0.1/health').then((r) => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))" + +ENTRYPOINT ["tini", "--", "/usr/local/bin/parenthese-aio"] diff --git a/Dockerfile.aio.dockerignore b/Dockerfile.aio.dockerignore new file mode 100644 index 0000000..be5cfb4 --- /dev/null +++ b/Dockerfile.aio.dockerignore @@ -0,0 +1,20 @@ +# Contexte de build de Dockerfile.aio (BuildKit lit ce fichier à la place de .dockerignore) : +# l'API, le front, la configuration nginx et le script de démarrage +* +!backend +!frontend +!deploy/nginx.docker.conf +!deploy/aio +backend/node_modules +backend/dist +backend/.local +backend/storage +backend/test +backend/openapi +backend/.env +backend/.env.* +frontend/node_modules +frontend/dist +frontend/.env +frontend/.env.* +**/*.log diff --git a/README.fr.md b/README.fr.md index 03d06c1..9b9d9b8 100644 --- a/README.fr.md +++ b/README.fr.md @@ -71,6 +71,16 @@ docker compose down # arrêt, les données sont conservées Pour vérifier qu'une installation fonctionne de bout en bout, lancez `bash tools/docker-smoke.sh http://localhost` (avec le port si `WEB_PORT` n'est pas 80). Le script a besoin de `curl` et `jq`. Il passe par le port web comme un navigateur : page d'accueil, santé de l'API, création d'un compte, connexion, création puis suppression d'un arbre de test. Le compte de test (`smoke-...@example.com`) reste en base. +### Un seul conteneur (Unraid, NAS) + +Pour les systèmes qui installent un conteneur par application, `ghcr.io/iliesallali/parenthese-aio` fait tourner la base, l'API et le front ensemble : + +```bash +docker run -d --name parenthese --restart unless-stopped -p 8080:80 -v /chemin/vers/appdata/parenthese:/data ghcr.io/iliesallali/parenthese-aio +``` + +Rien à remplir. Au premier démarrage, le conteneur crée dans `/data` la base, son mot de passe et la clé des sessions, puis les réutilise. Les photos et vidéos vont dans `/data/media`. Sauvegardez tout le dossier `/data` conteneur arrêté, ou exportez la base pendant qu'il tourne avec `docker exec parenthese runuser -u postgres -- pg_dump parenthese > parenthese.sql`. La base est ici PostgreSQL 15, l'installation compose utilise la 16. + ### Où sont les données Deux volumes Docker, conservés entre les redémarrages et les mises à jour : diff --git a/README.md b/README.md index 218661e..73aedee 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,16 @@ docker compose down # stop, data is kept To check that an installation works end to end, run `bash tools/docker-smoke.sh http://localhost` (add the port if `WEB_PORT` is not 80). The script needs `curl` and `jq`. It goes through the web port like a browser: home page, API health, account creation, sign-in, creating then deleting a test tree. The test account (`smoke-...@example.com`) stays in the database. +### Single container (Unraid, NAS) + +For systems that install one container per app, `ghcr.io/iliesallali/parenthese-aio` runs the database, the API and the front end together: + +```bash +docker run -d --name parenthese --restart unless-stopped -p 8080:80 -v /path/to/appdata/parenthese:/data ghcr.io/iliesallali/parenthese-aio +``` + +There is nothing to fill in. On first start the container creates the database, its password and the session key in `/data`, and reuses them afterwards. Photos and videos go to `/data/media`. Back up the whole `/data` folder with the container stopped, or dump the database while it runs with `docker exec parenthese runuser -u postgres -- pg_dump parenthese > parenthese.sql`. The database is PostgreSQL 15 here, the compose setup uses 16. + ### Where the data lives Two Docker volumes, kept across restarts and updates: diff --git a/deploy/aio/entrypoint.sh b/deploy/aio/entrypoint.sh new file mode 100755 index 0000000..f422077 --- /dev/null +++ b/deploy/aio/entrypoint.sh @@ -0,0 +1,106 @@ +#!/bin/bash +# Démarrage de l'image tout-en-un (Dockerfile.aio) : PostgreSQL, puis l'API, puis nginx. +# +# Au premier démarrage, crée dans /data la base, le mot de passe de la base et la clé de signature des sessions +# (JWT_SECRET), gardés pour les démarrages suivants. Une variable JWT_SECRET passée au conteneur l'emporte sur +# la clé générée. Si l'un des trois processus s'arrête, le conteneur s'arrête, et le redémarrage automatique +# de Docker le relance. +set -euo pipefail + +DATA_DIR=/data +PGDATA="$DATA_DIR/postgres" +MEDIA_DIR="$DATA_DIR/media" +SECRETS_DIR="$DATA_DIR/secrets" +DB_NAME=parenthese +DB_USER=parenthese + +log() { echo "[parenthese] $*"; } + +mkdir -p "$MEDIA_DIR" "$SECRETS_DIR" /run/postgresql +chmod 700 "$SECRETS_DIR" +chown postgres:postgres /run/postgresql +chown node:node "$MEDIA_DIR" + +# Secrets générés une fois, lisibles par root seulement. +secret() { + local file="$SECRETS_DIR/$1" + if [ ! -s "$file" ]; then + (umask 077 && openssl rand -hex "$2" > "$file") + log "secret $1 créé dans $file" + fi + cat "$file" +} + +DB_PASSWORD="$(secret db_password 24)" +if [ -z "${JWT_SECRET:-}" ]; then + JWT_SECRET="$(secret jwt_secret 48)" +fi + +# PostgreSQL, à l'écoute de la machine seule. +FIRST_START=0 +if [ ! -s "$PGDATA/PG_VERSION" ]; then + log "première installation : création de la base dans $PGDATA" + mkdir -p "$PGDATA" + chown postgres:postgres "$PGDATA" + chmod 700 "$PGDATA" + runuser -u postgres -- initdb -D "$PGDATA" --encoding=UTF8 --locale=C.UTF-8 \ + --auth-local=peer --auth-host=scram-sha-256 >/dev/null + FIRST_START=1 +fi +chown -R postgres:postgres "$PGDATA" +chmod 700 "$PGDATA" + +runuser -u postgres -- pg_ctl -D "$PGDATA" -w -t 60 \ + -o "-c listen_addresses=127.0.0.1 -c unix_socket_directories=/run/postgresql" \ + -l "$DATA_DIR/postgres.log" start >/dev/null + +if [ "$FIRST_START" = 1 ]; then + runuser -u postgres -- psql -q -v ON_ERROR_STOP=1 -c "CREATE ROLE $DB_USER LOGIN" + runuser -u postgres -- createdb -O "$DB_USER" "$DB_NAME" +fi +# Le mot de passe suit le fichier de /data à chaque démarrage (hexadécimal, sans guillemet possible). +runuser -u postgres -- psql -q -v ON_ERROR_STOP=1 -c "ALTER ROLE $DB_USER PASSWORD '$DB_PASSWORD'" + +export NODE_ENV=production +export PORT=4000 +export HOST=127.0.0.1 +export DATABASE_URL="postgresql://$DB_USER:$DB_PASSWORD@127.0.0.1:5432/$DB_NAME" +export MEDIA_STORAGE_PATH="$MEDIA_DIR" +export JWT_SECRET + +stop_all() { + log "arrêt" + [ -n "${NGINX_PID:-}" ] && kill -QUIT "$NGINX_PID" 2>/dev/null || true + [ -n "${API_PID:-}" ] && kill -TERM "$API_PID" 2>/dev/null || true + wait 2>/dev/null || true + runuser -u postgres -- pg_ctl -D "$PGDATA" -m fast -w stop >/dev/null 2>&1 || true +} +trap 'stop_all; exit 0' TERM INT + +cd /app +log "migrations" +runuser -u node -- env PATH="$PATH" prisma migrate deploy + +log "démarrage de l'API" +runuser -u node -- env PATH="$PATH" node dist/server.js & +API_PID=$! + +for _ in $(seq 1 60); do + if node -e "fetch('http://127.0.0.1:4000/health').then((r) => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"; then + break + fi + kill -0 "$API_PID" 2>/dev/null || { log "l'API s'est arrêtée au démarrage"; stop_all; exit 1; } + sleep 1 +done + +log "démarrage de nginx" +nginx -g 'daemon off;' & +NGINX_PID=$! + +log "prêt sur le port 80" +set +e +wait -n "$API_PID" "$NGINX_PID" +status=$? +log "un processus s'est arrêté (code $status)" +stop_all +exit "$status" From cea8d1db8f70aab394edf37b4bcd7f20a657b4d9 Mon Sep 17 00:00:00 2001 From: IliesAllali Date: Fri, 9 Oct 2026 17:46:58 +0200 Subject: [PATCH 06/11] =?UTF-8?q?Docker=20tout-en-un=20:=20journal=20Postg?= =?UTF-8?q?reSQL=20dans=20le=20dossier=20de=20la=20base=20(=C3=A9criture?= =?UTF-8?q?=20refus=C3=A9e=20=C3=A0=20la=20racine=20de=20/data)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 2 +- deploy/aio/entrypoint.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 96efca6..81bc18b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -225,7 +225,7 @@ jobs: run: | docker ps -a docker logs --timestamps aio || true - docker exec aio cat /data/postgres.log || true + docker run --rm -v aio-data:/data busybox cat /data/postgres/postgres.log || true - name: Arrêt et suppression du volume if: always() diff --git a/deploy/aio/entrypoint.sh b/deploy/aio/entrypoint.sh index f422077..88d740f 100755 --- a/deploy/aio/entrypoint.sh +++ b/deploy/aio/entrypoint.sh @@ -52,7 +52,7 @@ chmod 700 "$PGDATA" runuser -u postgres -- pg_ctl -D "$PGDATA" -w -t 60 \ -o "-c listen_addresses=127.0.0.1 -c unix_socket_directories=/run/postgresql" \ - -l "$DATA_DIR/postgres.log" start >/dev/null + -l "$PGDATA/postgres.log" start >/dev/null if [ "$FIRST_START" = 1 ]; then runuser -u postgres -- psql -q -v ON_ERROR_STOP=1 -c "CREATE ROLE $DB_USER LOGIN" From 30d015f0839aa1bc24ed24105b78ce3669db512b Mon Sep 17 00:00:00 2001 From: IliesAllali Date: Fri, 9 Oct 2026 17:51:29 +0200 Subject: [PATCH 07/11] Docker tout-en-un : journal sur stderr, il polluait le mot de passe lu par $(secret ...) Co-Authored-By: Claude Opus 5.5 --- deploy/aio/entrypoint.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/aio/entrypoint.sh b/deploy/aio/entrypoint.sh index 88d740f..edf3efe 100755 --- a/deploy/aio/entrypoint.sh +++ b/deploy/aio/entrypoint.sh @@ -14,7 +14,7 @@ SECRETS_DIR="$DATA_DIR/secrets" DB_NAME=parenthese DB_USER=parenthese -log() { echo "[parenthese] $*"; } +log() { echo "[parenthese] $*" >&2; } mkdir -p "$MEDIA_DIR" "$SECRETS_DIR" /run/postgresql chmod 700 "$SECRETS_DIR" From 333a11e84b1988a3d69b4c43de523dd3b099de9b Mon Sep 17 00:00:00 2001 From: IliesAllali Date: Sat, 10 Oct 2026 16:15:46 +0200 Subject: [PATCH 08/11] Docker : le front lit l'adresse de l'API dans API_UPSTREAM MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le conteneur web relayait l'API vers `api:4000`, écrit en dur. Sur un réseau partagé entre plusieurs applications (Umbrel), ce nom peut appartenir à une autre. L'adresse vient maintenant de API_UPSTREAM, `api:4000` par défaut : rien ne change pour les installations existantes. L'image tout-en-un garde sa propre configuration. La CI lance un second front qui joint l'API par le nom de son conteneur et repasse le test de fumée. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 16 +++++++++++++++- CHANGELOG.md | 1 + Dockerfile.web | 9 ++++++++- README.fr.md | 2 +- README.md | 2 +- 5 files changed, 26 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 81bc18b..e2f6480 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -182,15 +182,29 @@ jobs: - name: Test de fumée par le port web run: bash tools/docker-smoke.sh http://localhost:8080 + # Un second front qui joint l'API par le nom de son conteneur (API_UPSTREAM), comme sur Umbrel. + - name: Front avec une autre adresse d'API + run: | + WEB_IMAGE="$(docker compose config --images | grep parenthese-web)" + API_CONTAINER="$(docker compose ps -q api | xargs docker inspect -f '{{.Name}}' | tr -d /)" + docker run -d --name web-upstream --network parenthese_default -p 8081:80 \ + -e API_UPSTREAM="$API_CONTAINER:4000" "$WEB_IMAGE" + sleep 3 + docker exec web-upstream grep -q "http://$API_CONTAINER:4000" /etc/nginx/conf.d/default.conf + bash tools/docker-smoke.sh http://localhost:8081 + - name: État et journaux en cas d'échec if: failure() run: | docker compose ps -a docker compose logs --no-color --timestamps + docker logs web-upstream || true - name: Arrêt et suppression des volumes if: always() - run: docker compose down -v --remove-orphans + run: | + docker rm -f web-upstream || true + docker compose down -v --remove-orphans docker-aio: name: Docker tout-en-un (Unraid, NAS) diff --git a/CHANGELOG.md b/CHANGELOG.md index e0fd1ba..8ab5f4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ Notable changes to Parenthèse. Dates are in YYYY-MM-DD format. ### Self-hosting - Single-container image `ghcr.io/iliesallali/parenthese-aio` (PostgreSQL, API and front end), for Unraid and NAS systems that install one container per app. It needs no configuration: the database, its password and the session key are created in `/data` on first start. Tested in CI by a fresh start, a restart and the smoke test. +- The `parenthese-web` image reads the API address from `API_UPSTREAM` (`api:4000` by default, unchanged for existing installs). Set it when the name `api` is already taken on the Docker network, for example on Umbrel where apps share one network. ## 1.0.2 (2026-10-09) diff --git a/Dockerfile.web b/Dockerfile.web index 64c0dde..146c5fc 100644 --- a/Dockerfile.web +++ b/Dockerfile.web @@ -19,7 +19,14 @@ RUN npm run build FROM nginx:1.30-alpine -COPY deploy/nginx.docker.conf /etc/nginx/conf.d/default.conf +# Adresse de l'API lue au démarrage du conteneur, `api:4000` par défaut (le service de docker-compose.yml). +# À changer quand le nom `api` est déjà pris sur le réseau, comme sur Umbrel où les apps partagent un réseau. +# L'image nginx passe les fichiers de /etc/nginx/templates par envsubst et n'y remplace que les variables +# d'environnement définies : $host, $uri et les autres variables de nginx restent telles quelles. +ENV API_UPSTREAM=api:4000 +COPY deploy/nginx.docker.conf /etc/nginx/templates/default.conf.template +RUN sed -i 's|http://api:4000|http://${API_UPSTREAM}|g' /etc/nginx/templates/default.conf.template \ + && ! grep -q 'api:4000' /etc/nginx/templates/default.conf.template COPY --from=build /app/dist /usr/share/nginx/html EXPOSE 80 diff --git a/README.fr.md b/README.fr.md index 9b9d9b8..910b661 100644 --- a/README.fr.md +++ b/README.fr.md @@ -53,7 +53,7 @@ docker compose up -d Le premier démarrage télécharge les images (ou les construit depuis les sources si elles sont introuvables), applique les migrations de la base, puis sert l'application sur [http://localhost](http://localhost). Créez votre compte depuis l'écran d'accueil : le premier compte est un compte comme les autres, il n'y a pas d'administrateur global. -Trois conteneurs tournent : `db` (PostgreSQL 16), `api` (l'API Node) et `web` (nginx qui sert le front et relaie l'API). Seul `web` est exposé, sur le port choisi par `WEB_PORT` (80 par défaut). Si ce port est déjà pris, changez `WEB_PORT` dans `.env` et adaptez `CORS_ORIGIN` en conséquence. +Trois conteneurs tournent : `db` (PostgreSQL 16), `api` (l'API Node) et `web` (nginx qui sert le front et relaie l'API). Seul `web` est exposé, sur le port choisi par `WEB_PORT` (80 par défaut). Si ce port est déjà pris, changez `WEB_PORT` dans `.env` et adaptez `CORS_ORIGIN` en conséquence. Le conteneur `web` trouve l'API à `api:4000` ; si vous lancez les images sous d'autres noms de service, donnez `API_UPSTREAM` à `web` (par exemple `API_UPSTREAM=monapi:4000`). Pour ouvrir l'instance à la famille depuis Internet, placez un reverse proxy HTTPS devant le port `web` (Caddy, Traefik, nginx) et mettez l'adresse publique dans `CORS_ORIGIN`, par exemple `https://arbre.votre-domaine.fr`. diff --git a/README.md b/README.md index 73aedee..d9b947d 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ docker compose up -d The first start downloads the images (or builds them from the source when they can't be pulled), applies the database migrations, then serves the app on [http://localhost](http://localhost). Create your account from the home screen. The first account is an account like any other, there is no global administrator. -Three containers run: `db` (PostgreSQL 16), `api` (the Node API) and `web` (nginx, which serves the front end and proxies the API). Only `web` is exposed, on the port set by `WEB_PORT` (80 by default). If that port is taken, change `WEB_PORT` in `.env` and update `CORS_ORIGIN` to match. +Three containers run: `db` (PostgreSQL 16), `api` (the Node API) and `web` (nginx, which serves the front end and proxies the API). Only `web` is exposed, on the port set by `WEB_PORT` (80 by default). If that port is taken, change `WEB_PORT` in `.env` and update `CORS_ORIGIN` to match. The `web` container finds the API at `api:4000`; if you run the images under other service names, set `API_UPSTREAM` on `web` (for example `API_UPSTREAM=myapi:4000`). To open the instance to your family over the Internet, put an HTTPS reverse proxy in front of the `web` port (Caddy, Traefik, nginx) and set the public address in `CORS_ORIGIN`, for example `https://tree.your-domain.com`. From 7becb821a333941195126d5a6e3a88d5254f0733 Mon Sep 17 00:00:00 2001 From: IliesAllali Date: Sat, 10 Oct 2026 16:18:02 +0200 Subject: [PATCH 09/11] CHANGELOG : 1.0.3 Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ab5f4d..548bce6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ Notable changes to Parenthèse. Dates are in YYYY-MM-DD format. -## Unreleased +## 1.0.3 (2026-10-10) ### Self-hosting From 83443ac9dc17f5fb7f7907378c4890c5f3dbc11d Mon Sep 17 00:00:00 2001 From: IliesAllali Date: Sun, 11 Oct 2026 18:54:56 +0200 Subject: [PATCH 10/11] =?UTF-8?q?Premi=C3=A8res=20photos=20:=20import=20d?= =?UTF-8?q?=C3=A8s=20la=20cr=C3=A9ation,=20photos=20du=20logiciel=20retrou?= =?UTF-8?q?v=C3=A9es,=20premiers=20visages?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Création d'arbre : « J'ai déjà un arbre » importe le .ged sans fiche « vous » en double. - Import GEDCOM : lit les photos citées (OBJE / FILE, en ligne ou par renvoi, _PRIM d'abord) et renvoie leur chemin ; rien n'est gardé côté serveur. - Fenêtre « Il manque les visages » après un import : dossier des photos apparié par nom de fichier, cinq visages à demander (famille de « Moi », sinon les plus jeunes). - Portrait vide chez le propriétaire : cadre cliquable sur la fiche hors édition, « + photo » au survol sur l'arbre. - Photos de plus de 5 Mo réduites à 2000 px avant l'envoi ; portraits à 120 envois par minute. - PostHog : gedcom_imported, portraits_bulk_placed, portrait_added (effectifs arrondis). - Corrigé : une union à un seul parent connu relie ses enfants à ce parent, sous un seul « Inconnu », au lieu de les laisser sans lien. - Corrigé : réimporter un GEDCOM reprend les personnes, unions et liens déjà présents au lieu de tout doubler. Recette locale (vraie API, vraie base) : 29/29 sur le parcours photos, 47/47 sur la recette de non-régression des parcours. Tests 123 serveur, 107 app, lint et build propres. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 17 ++ backend/src/lib/gedcom-photos.ts | 56 ++++ backend/src/routes/gedcom.ts | 98 ++++-- backend/src/routes/media.ts | 5 +- backend/test/gedcom-photos.test.ts | 74 +++++ frontend/src/App.jsx | 178 ++++++++++- frontend/src/components/AdminPanel.jsx | 18 +- frontend/src/components/Galaxy.jsx | 6 +- frontend/src/components/PersonCard.css | 10 + frontend/src/components/PersonCard.jsx | 43 ++- frontend/src/components/PhotoStartPanel.css | 61 ++++ frontend/src/components/PhotoStartPanel.jsx | 283 ++++++++++++++++++ .../src/components/TreeCreationWizard.jsx | 53 +++- frontend/src/components/galaxy/elkLayout.js | 15 +- frontend/src/components/galaxy/renderers.js | 36 ++- frontend/src/data/mockData.js | 28 +- frontend/src/data/mockData.test.js | 21 ++ frontend/src/i18n/en/index.js | 2 + frontend/src/i18n/en/photos.js | 56 ++++ frontend/src/utils/photoImport.js | 179 +++++++++++ frontend/src/utils/photoImport.test.js | 77 +++++ 21 files changed, 1244 insertions(+), 72 deletions(-) create mode 100644 backend/src/lib/gedcom-photos.ts create mode 100644 backend/test/gedcom-photos.test.ts create mode 100644 frontend/src/components/PhotoStartPanel.css create mode 100644 frontend/src/components/PhotoStartPanel.jsx create mode 100644 frontend/src/i18n/en/photos.js create mode 100644 frontend/src/utils/photoImport.js create mode 100644 frontend/src/utils/photoImport.test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 548bce6..4114143 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,23 @@ Notable changes to Parenthèse. Dates are in YYYY-MM-DD format. +## Unreleased + +### First photos + +- Tree creation offers "I already have a tree": the .ged file is imported right away, without creating a "you" entry that would end up duplicated. +- GEDCOM import reads the photos each person had in the original software (`OBJE` / `FILE`, inline or by reference, primary photo first). After an import, the owner picks the folder where those photos live and Parenthèse places each portrait by file name. Paths stay on the device, nothing is stored on the server. +- After an import, a window suggests five faces to start with: the close family of the person marked "Me", otherwise the most recent generations. +- For the owner, an empty portrait invites a photo: the empty frame on a profile is a button outside edit mode, and hovering an empty portrait in the tree shows "+ photo". +- Phone photos over 5 MB are scaled down to 2000 px before upload instead of being refused. +- Owner portrait uploads are limited to 120 per minute (was 30), still within the tree's storage quota. +- Anonymous events: `gedcom_imported`, `portraits_bulk_placed`, `portrait_added` (rounded counts, no names, paths or tree address). + +### Fixes + +- Children of a family with only one known parent (common in GEDCOM files) are linked to that parent, under a single "Unknown" partner, instead of floating in the tree without a link. +- Importing a GEDCOM file into a tree that already holds it no longer duplicates everyone: people with the same first name, last name, birth and death dates are reused, and existing unions and parent links are kept. + ## 1.0.3 (2026-10-10) ### Self-hosting diff --git a/backend/src/lib/gedcom-photos.ts b/backend/src/lib/gedcom-photos.ts new file mode 100644 index 0000000..91ee298 --- /dev/null +++ b/backend/src/lib/gedcom-photos.ts @@ -0,0 +1,56 @@ +// Photos citées par un fichier GEDCOM. Le fichier ne contient pas les images, seulement leur chemin sur +// l'ordinateur de la personne (« C:\Généalogie\Photos\jean.jpg »). L'import renvoie ce chemin pour chaque +// personne, et l'app les relie aux fichiers du dossier que la personne choisit, sans rien garder en base. + +export type GedcomPhotoNode = { + xref?: string + tag: string + value: string + children: GedcomPhotoNode[] +} + +const IMAGE_EXTENSIONS = new Set(['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'tif', 'tiff', 'heic', 'heif']) +const MAX_PATH_LENGTH = 500 + +function child(node: GedcomPhotoNode | undefined, tag: string): GedcomPhotoNode | undefined { + return node?.children.find((c) => c.tag === tag) +} + +function isImagePath(value: string): boolean { + if (!value || /^[a-z][a-z0-9+.-]*:\/\//i.test(value)) return false + const name = value.split(/[\\/]/).pop() || '' + const dot = name.lastIndexOf('.') + return dot > 0 && IMAGE_EXTENSIONS.has(name.slice(dot + 1).toLowerCase()) +} + +// Un OBJE est soit écrit sous la personne (« 1 OBJE » puis « 2 FILE »), soit un renvoi (« 1 OBJE @M1@ ») +// vers un enregistrement « 0 @M1@ OBJE » en fin de fichier. Les logiciels marquent la photo principale +// par _PRIM Y (sous OBJE ou sous FILE) : elle passe devant, sinon la première image citée. +export function photoPathForPerson( + indi: GedcomPhotoNode, + objeByXref: Map, +): string | null { + let first: string | null = null + for (const ref of indi.children) { + if (ref.tag !== 'OBJE') continue + const pointer = ref.value.match(/^@([^@]+)@$/) + const obje = pointer ? objeByXref.get(pointer[1]) : ref + if (!obje) continue + const isPrimary = (node: GedcomPhotoNode | undefined) => child(node, '_PRIM')?.value?.toUpperCase().startsWith('Y') + for (const file of obje.children.filter((c) => c.tag === 'FILE')) { + const filePath = file.value.trim() + if (!isImagePath(filePath) || filePath.length > MAX_PATH_LENGTH) continue + if (isPrimary(ref) || isPrimary(obje) || isPrimary(file)) return filePath + first ??= filePath + } + } + return first +} + +export function indexObjeRecords(roots: GedcomPhotoNode[]): Map { + const map = new Map() + for (const node of roots) { + if (node.tag === 'OBJE' && node.xref) map.set(node.xref, node) + } + return map +} diff --git a/backend/src/routes/gedcom.ts b/backend/src/routes/gedcom.ts index 8795b55..d5789a4 100644 --- a/backend/src/routes/gedcom.ts +++ b/backend/src/routes/gedcom.ts @@ -2,6 +2,7 @@ import type { FastifyPluginAsync } from 'fastify' import { z } from 'zod' import { UPLOAD_BODY_LIMIT } from '../config/env.js' +import { indexObjeRecords, photoPathForPerson } from '../lib/gedcom-photos.js' import type { MembershipRole } from '../types/auth.js' const treeIdParamSchema = z.object({ @@ -52,7 +53,7 @@ type GedcomNode = { children: GedcomNode[] } -function parseGedcom(text: string): GedcomNode[] { +export function parseGedcom(text: string): GedcomNode[] { const lines = text.split(/\r\n|\r|\n/) const roots: GedcomNode[] = [] const stack: GedcomNode[] = [] @@ -176,6 +177,18 @@ function parseGedcomName(value: string): { firstName: string; lastName: string } return { firstName: value.trim(), lastName: '' } } +// Réimport : une personne du fichier déjà présente dans l'arbre (même prénom, nom, naissance et décès) +// est reprise au lieu d'être créée une seconde fois +function personKey(firstName: string, lastName: string, birthDate: Date | null, deathDate: Date | null): string { + const norm = (value: string) => value.normalize('NFC').trim().toLowerCase() + const day = (value: Date | null) => (value ? value.toISOString().slice(0, 10) : '') + return [norm(firstName), norm(lastName), day(birthDate), day(deathDate)].join('|') +} + +function pairKey(a: string, b: string | null): string { + return b ? [a, b].sort().join('|') : `${a}|` +} + function sanitizeXref(id: string): string { return id.replace(/[^A-Za-z0-9_]/g, '_') } @@ -466,7 +479,30 @@ export const gedcomRoutes: FastifyPluginAsync = async (app) => { } const xrefToPersonId = new Map() + const objeByXref = indexObjeRecords(roots) + const photoRefs: Array<{ personId: string; file: string }> = [] + + // Ce que l'arbre contient déjà : un second import du même fichier complète l'arbre sans rien doubler. + // Chaque personne existante ne sert qu'une fois, deux fiches identiques du fichier restent deux personnes. + const [existingPersons, existingUnions, existingLinks] = await Promise.all([ + app.prisma.person.findMany({ + where: { treeId, deletedAt: null }, + select: { id: true, firstName: true, lastName: true, birthDate: true, deathDate: true, avatarPath: true }, + }), + app.prisma.union.findMany({ where: { treeId, deletedAt: null }, select: { id: true, partner1PersonId: true, partner2PersonId: true } }), + app.prisma.parentChildLink.findMany({ where: { treeId, deletedAt: null }, select: { parentPersonId: true, childPersonId: true } }), + ]) + const reusablePersons = new Map>() + for (const person of existingPersons) { + const key = personKey(person.firstName, person.lastName, person.birthDate, person.deathDate) + if (!reusablePersons.has(key)) reusablePersons.set(key, []) + reusablePersons.get(key)!.push({ id: person.id, hasAvatar: Boolean(person.avatarPath) }) + } + const unionByPair = new Map(existingUnions.map((u) => [pairKey(u.partner1PersonId, u.partner2PersonId), u.id])) + const linkKeys = new Set(existingLinks.map((l) => `${l.parentPersonId}:${l.childPersonId}`)) + let personsCreated = 0 + let personsReused = 0 let unionsCreated = 0 let linksCreated = 0 @@ -498,14 +534,29 @@ export const gedcomRoutes: FastifyPluginAsync = async (app) => { notes = clip([noteNode.value, ...contLines].join('\n'), 2000) } + const personFirstName = (firstName || '?').slice(0, 120) + const personLastName = (lastName || '').slice(0, 120) + const birthDate = isoDateToDbDate(birthDateIso) + const deathDate = isoDateToDbDate(deathDateIso) + const photoPath = photoPathForPerson(indi, objeByXref) + + const existing = reusablePersons.get(personKey(personFirstName, personLastName, birthDate, deathDate))?.shift() + if (existing) { + xrefToPersonId.set(indi.xref!, existing.id) + personsReused++ + // Une personne qui a déjà son portrait le garde + if (photoPath && !existing.hasAvatar) photoRefs.push({ personId: existing.id, file: photoPath }) + continue + } + const person = await tx.person.create({ data: { treeId, - firstName: (firstName || '?').slice(0, 120), - lastName: (lastName || '').slice(0, 120), + firstName: personFirstName, + lastName: personLastName, sex, - birthDate: isoDateToDbDate(birthDateIso), - deathDate: isoDateToDbDate(deathDateIso), + birthDate, + deathDate, birthPlace, profession, notes, @@ -516,6 +567,7 @@ export const gedcomRoutes: FastifyPluginAsync = async (app) => { xrefToPersonId.set(indi.xref!, person.id) personsCreated++ + if (photoPath) photoRefs.push({ personId: person.id, file: photoPath }) } for (const fam of famNodes) { @@ -530,16 +582,22 @@ export const gedcomRoutes: FastifyPluginAsync = async (app) => { const marrNode = findChild(fam, 'MARR') const marriageDateIso = gedcomDateToIso(findChild(marrNode, 'DATE')?.value ?? '') - const union = await tx.union.create({ - data: { - treeId, - partner1PersonId: partner1Id ?? partner2Id!, - partner2PersonId: partner1Id ? (partner2Id ?? null) : null, - startDate: isoDateToDbDate(marriageDateIso), - }, - }) - - unionsCreated++ + const firstPartner = partner1Id ?? partner2Id! + const secondPartner = partner1Id ? (partner2Id ?? null) : null + let unionId = unionByPair.get(pairKey(firstPartner, secondPartner)) + if (!unionId) { + const union = await tx.union.create({ + data: { + treeId, + partner1PersonId: firstPartner, + partner2PersonId: secondPartner, + startDate: isoDateToDbDate(marriageDateIso), + }, + }) + unionId = union.id + unionByPair.set(pairKey(firstPartner, secondPartner), unionId) + unionsCreated++ + } const chilNodes = findAllChildren(fam, 'CHIL') const seenChildren = new Set() @@ -555,14 +613,17 @@ export const gedcomRoutes: FastifyPluginAsync = async (app) => { const parentIds = [partner1Id, partner2Id].filter((id): id is string => Boolean(id)) for (const parentId of parentIds) { + const linkKey = `${parentId}:${childId}` + if (linkKeys.has(linkKey)) continue await tx.parentChildLink.create({ data: { treeId, parentPersonId: parentId, childPersonId: childId, - viaUnionId: union.id, + viaUnionId: unionId, }, }) + linkKeys.add(linkKey) linksCreated++ } } @@ -576,7 +637,7 @@ export const gedcomRoutes: FastifyPluginAsync = async (app) => { action: 'gedcom_imported', entityType: 'tree', entityId: treeId, - payloadJson: { personsCreated, unionsCreated, linksCreated }, + payloadJson: { personsCreated, personsReused, unionsCreated, linksCreated, photoRefs: photoRefs.length }, }, }) }, { timeout: 60000 }) @@ -584,6 +645,7 @@ export const gedcomRoutes: FastifyPluginAsync = async (app) => { importInProgress = false } - return reply.send({ personsCreated, unionsCreated, linksCreated }) + // photoRefs : chemin de la photo que le logiciel d'origine associait à chaque personne (rien n'est gardé) + return reply.send({ personsCreated, personsReused, unionsCreated, linksCreated, photoRefs }) }) } diff --git a/backend/src/routes/media.ts b/backend/src/routes/media.ts index 0488fef..e77c0d8 100644 --- a/backend/src/routes/media.ts +++ b/backend/src/routes/media.ts @@ -186,6 +186,9 @@ async function exceedsStorageQuota(app: FastifyRequest['server'], treeId: string // Seules les routes d'envoi acceptent un corps de 12 Mo (fichier en base64), le reste de l'API est à 1 Mo (app.ts) const UPLOAD_RATE_LIMIT = { bodyLimit: UPLOAD_BODY_LIMIT, config: { rateLimit: { max: 30, timeWindow: '1 minute' } } } +// Portraits : réservés au propriétaire, mais l'app peut en placer quelques centaines d'un coup (photos +// retrouvées après un import GEDCOM), envoyées une par une. Le quota de stockage de l'arbre s'applique. +const AVATAR_RATE_LIMIT = { bodyLimit: UPLOAD_BODY_LIMIT, config: { rateLimit: { max: 120, timeWindow: '1 minute' } } } async function requireAdminMembership(app: FastifyRequest['server'], userId: string, treeId: string) { const membership = await app.prisma.treeMembership.findUnique({ @@ -461,7 +464,7 @@ export const mediaRoutes: FastifyPluginAsync = async (app) => { return reply.code(201).send({ media, status: media.status }) }) - app.post('/trees/:id/persons/:personId/avatar', UPLOAD_RATE_LIMIT, async (request, reply) => { + app.post('/trees/:id/persons/:personId/avatar', AVATAR_RATE_LIMIT, async (request, reply) => { if (!request.actor || request.actor.kind !== 'user') { return reply.code(401).send({ error: 'authentication_required' }) } diff --git a/backend/test/gedcom-photos.test.ts b/backend/test/gedcom-photos.test.ts new file mode 100644 index 0000000..b639177 --- /dev/null +++ b/backend/test/gedcom-photos.test.ts @@ -0,0 +1,74 @@ +import { describe, expect, it } from 'vitest' + +import { indexObjeRecords, photoPathForPerson } from '../src/lib/gedcom-photos' +import { parseGedcom } from '../src/routes/gedcom' + +function photosOf(text: string): Record { + const roots = parseGedcom(text) + const objes = indexObjeRecords(roots) + const out: Record = {} + for (const indi of roots.filter((n) => n.tag === 'INDI' && n.xref)) { + out[indi.xref!] = photoPathForPerson(indi, objes) + } + return out +} + +describe('photos citées par un GEDCOM', () => { + it('lit un OBJE écrit sous la personne (GEDCOM 5.5)', () => { + const text = [ + '0 @I1@ INDI', + '1 NAME Jean /Dupont/', + '1 OBJE', + '2 FORM jpg', + '2 FILE C:\\Genealogie\\Photos\\jean.jpg', + ].join('\n') + expect(photosOf(text)).toEqual({ I1: 'C:\\Genealogie\\Photos\\jean.jpg' }) + }) + + it('suit un renvoi vers un enregistrement OBJE (Gramps, GEDCOM 5.5.1)', () => { + const text = [ + '0 @I1@ INDI', + '1 NAME Marie /Martin/', + '1 OBJE @M7@', + '0 @M7@ OBJE', + '1 FILE photos/marie 1920.png', + '2 FORM png', + ].join('\n') + expect(photosOf(text)).toEqual({ I1: 'photos/marie 1920.png' }) + }) + + it('préfère la photo marquée principale, sinon la première image', () => { + const text = [ + '0 @I1@ INDI', + '1 OBJE', + '2 FILE acte-naissance.pdf', + '1 OBJE', + '2 FILE groupe.jpg', + '1 OBJE', + '2 FILE portrait.jpg', + '2 _PRIM Y', + '0 @I2@ INDI', + '1 OBJE', + '2 FILE acte.pdf', + '1 OBJE', + '2 FILE premier.JPEG', + '1 OBJE', + '2 FILE second.jpg', + ].join('\n') + expect(photosOf(text)).toEqual({ I1: 'portrait.jpg', I2: 'premier.JPEG' }) + }) + + it('ignore les adresses web, les documents et les renvois cassés', () => { + const text = [ + '0 @I1@ INDI', + '1 OBJE', + '2 FILE https://exemple.org/photo.jpg', + '1 OBJE @M404@', + '1 OBJE', + '2 FILE registre.pdf', + '0 @I2@ INDI', + '1 NAME Sans /Photo/', + ].join('\n') + expect(photosOf(text)).toEqual({ I1: null, I2: null }) + }) +}) diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 6d1fb8c..57b551c 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -12,6 +12,7 @@ import AccessGate from './components/AccessGate' import AccountDashboard from './components/AccountDashboard' import AccountPanel from './components/AccountPanel' import TreeCreationWizard from './components/TreeCreationWizard' +import PhotoStartPanel from './components/PhotoStartPanel.jsx' import ContributionPanel from './components/ContributionPanel' import EditModeOverlay from './components/EditSessionBar.jsx' import EditConfirmModal from './components/EditConfirmModal' @@ -33,6 +34,7 @@ import { batchAnnotations, deletePersonAvatar, getTree, + importGedcom, linkTreeAccess, updateTree, rotateTreePasswords, @@ -53,7 +55,8 @@ import { useEditMode } from './hooks/useEditMode' import { useAnnotations } from './hooks/useAnnotations' import AnnotationToolbar from './components/annotations/AnnotationToolbar' import TextInputOverlay from './components/annotations/TextInputOverlay' -import { filiations, medias, persons, unions } from './data/mockData' +import { filiations, getChildren, getParents, getPersonUnions, medias, persons, unions } from './data/mockData' +import { loadPhotoRefs, pickFirstFaces, prepareImageForUpload, savePhotoRefs, sizeBucket } from './utils/photoImport' import { fileToBase64, getAccountErrorMessage } from './utils/errorMessages' import { getSiblingParentLinks } from './utils/familyLinks' import { getUploadMimeType, validateMediaFile } from './utils/mediaUpload' @@ -476,6 +479,9 @@ function App() { }) const [adminPanelVisible, setAdminPanelVisible] = useState(false) + // Premiers visages : fenêtre ouverte après un import GEDCOM, ou depuis les paramètres de l'arbre + const [photoStart, setPhotoStart] = useState({ visible: false, personsImported: null, refs: [], faces: [] }) + const photoStartDirtyRef = useRef(false) const [showConfirmModal, setShowConfirmModal] = useState(false) const [showDraftBanner, setShowDraftBanner] = useState(false) const [accountPanelVisible, setAccountPanelVisible] = useState(false) @@ -494,6 +500,7 @@ function App() { const [treeWizardVisible, setTreeWizardVisible] = useState(false) const [treeWizardLoading, setTreeWizardLoading] = useState(false) const [treeWelcomeVisible, setTreeWelcomeVisible] = useState(false) + const [treeImportError, setTreeImportError] = useState('') const [contribInviteState, setContribInviteState] = useState({ shareUrl: '', knownPasswords: { @@ -553,6 +560,30 @@ function App() { persons.find((candidate) => String(candidate.id) === String(linkedSelfPersonId)) || null ), [linkedSelfPersonId, tree.graphRevision, tree.treeContext.treeId]) + // Les visages à demander en premier : la famille proche de « Moi » (ou de la personne de départ), + // sinon les générations les plus récentes + const computeFirstFaces = useCallback(() => pickFirstFaces({ + persons, + selfId: linkedSelfPersonId || tree.treeContext.rootPersonId || null, + getParents, + getChildren, + getPartners: (id) => getPersonUnions(id) + .map((union) => (union.partner1Id === id ? union.partner2Id : union.partner1Id)) + .map((partnerId) => persons.find((person) => person.id === partnerId)) + .filter(Boolean), + }), [linkedSelfPersonId, tree.treeContext.rootPersonId]) + + const openPhotoStart = useCallback(({ personsImported = null, refs = null, treeId = null } = {}) => { + photoStartDirtyRef.current = false + setPhotoStart({ + visible: true, + personsImported, + refs: refs ?? loadPhotoRefs(treeId || tree.treeContext.treeId), + faces: computeFirstFaces(), + }) + }, [computeFirstFaces, tree.treeContext.treeId]) + + const accountDisplayName = useMemo(() => { if (linkedSelfPerson) { const fullName = `${linkedSelfPerson.firstName || ''} ${linkedSelfPerson.lastName || ''}`.trim() @@ -1475,27 +1506,28 @@ function App() { const accessToken = tree.treeContext.accessToken || auth.userAuth.token if (!accessToken || !tree.treeContext.treeId) return - const validationError = validateMediaFile(file, 'photo') + setAvatarActionState({ loading: true, error: '' }) + const prepared = await prepareImageForUpload(file) + const validationError = validateMediaFile(prepared, 'photo') if (validationError) { setAvatarActionState({ loading: false, error: validationError }) return } - setAvatarActionState({ loading: true, error: '' }) - try { - const dataBase64 = await fileToBase64(file) + const dataBase64 = await fileToBase64(prepared) await uploadPersonAvatar( tree.treeContext.treeId, personId, { - fileName: file.name || 'avatar.jpg', - mimeType: getUploadMimeType(file, 'photo'), - sizeBytes: file.size, + fileName: prepared.name || 'avatar.jpg', + mimeType: getUploadMimeType(prepared, 'photo'), + sizeBytes: prepared.size, dataBase64, }, accessToken, ) + trackAppEvent('portrait_added', { via: 'card' }) await refreshTreeAndKeepSelection(personId, { relayout: true }) setAvatarActionState({ loading: false, error: '' }) } catch (error) { @@ -2285,6 +2317,49 @@ function App() { ) trackAppEvent('tree_created', { tree_id: toOpaqueTreeId(createdTree.id) }) + // Arbre déjà tenu dans un logiciel : on importe son fichier au lieu de commencer par « vous », + // sinon la personne saisie ici existerait deux fois après l'import + if (payload.gedcomFile) { + let imported = null + let importError = '' + try { + const content = await payload.gedcomFile.text() + imported = content.trim() + ? await importGedcom(createdTree.id, content, token) + : null + if (!imported) importError = t('Le fichier GEDCOM est vide.') + } catch (error) { + importError = getAccountErrorMessage(error) || t("Impossible d'importer ce fichier GEDCOM.") + } + + await auth.loadAccountTrees(token) + await tree.openTreeGraph(createdTree.id, token, { + treeName: createdTree.name || payload.treeName, + treeDescription: createdTree.description || '', + role: 'owner', + accessMode: 'member', + rootPersonId: null, + promptWizard: false, + }) + await auth.persistLastOpenedTree({ treeId: createdTree.id, accessMode: 'member' }) + setTreeWizardVisible(false) + + if (imported) { + const refs = Array.isArray(imported.photoRefs) ? imported.photoRefs : [] + savePhotoRefs(createdTree.id, refs) + trackAppEvent('gedcom_imported', { + persons: sizeBucket(imported.personsCreated || 0), + photo_refs: sizeBucket(refs.length), + via: 'wizard', + }) + openPhotoStart({ personsImported: (imported.personsCreated || 0) + (imported.personsReused || 0), refs }) + } else { + // L'arbre existe déjà : on l'ouvre vide et on dit où réessayer, plutôt que d'en créer un second + setTreeImportError(importError) + } + return null + } + const createdSelf = await createPerson( createdTree.id, { @@ -2435,8 +2510,71 @@ function App() { } } - const handleGedcomImported = useCallback(async () => { + const handleGedcomImported = useCallback(async (result, treeIdOverride = null) => { await refreshTreeAndKeepSelection(null, { relayout: true }) + const refs = Array.isArray(result?.photoRefs) ? result.photoRefs : [] + savePhotoRefs(treeIdOverride || tree.treeContext.treeId, refs) + trackAppEvent('gedcom_imported', { + persons: sizeBucket(result?.personsCreated || 0), + photo_refs: sizeBucket(refs.length), + }) + setAdminPanelVisible(false) + openPhotoStart({ personsImported: (result?.personsCreated || 0) + (result?.personsReused || 0), refs }) + }, [refreshTreeAndKeepSelection, openPhotoStart, tree.treeContext.treeId]) + + // Un portrait envoyé sans recharger l'arbre (rechargé une fois, à la fermeture de la fenêtre). + // Renvoie un message d'erreur, ou '' si c'est fait. + const sendPortrait = useCallback(async (personId, file) => { + const accessToken = tree.treeContext.accessToken || auth.userAuth.token + if (!accessToken || !tree.treeContext.treeId) return t('Session expirée, reconnectez-vous.') + const prepared = await prepareImageForUpload(file) + const validationError = validateMediaFile(prepared, 'photo') + if (validationError) return validationError + const payload = { + fileName: prepared.name || 'avatar.jpg', + mimeType: getUploadMimeType(prepared, 'photo'), + sizeBytes: prepared.size, + dataBase64: await fileToBase64(prepared), + } + // Trop d'envois d'un coup (429) : on attend et on réessaie, deux fois au plus + for (let attempt = 0; ; attempt += 1) { + try { + await uploadPersonAvatar(tree.treeContext.treeId, personId, payload, accessToken) + photoStartDirtyRef.current = true + return '' + } catch (error) { + if (error?.status === 429 && attempt < 2) { + await new Promise((resolve) => window.setTimeout(resolve, 15000)) + continue + } + return getAccountErrorMessage(error) + } + } + }, [auth.userAuth.token, tree.treeContext.accessToken, tree.treeContext.treeId]) + + const handlePhotoStartUploadOne = useCallback(async (personId, file) => { + const error = await sendPortrait(personId, file) + if (!error) trackAppEvent('portrait_added', { via: 'first_faces' }) + return error + }, [sendPortrait]) + + const handlePhotoStartBulk = useCallback(async (matches, onProgress) => { + const placedIds = [] + for (let i = 0; i < matches.length; i += 1) { + const error = await sendPortrait(matches[i].personId, matches[i].file) + if (!error) placedIds.push(matches[i].personId) + onProgress?.(i + 1) + } + trackAppEvent('portraits_bulk_placed', { placed: sizeBucket(placedIds.length), failed: sizeBucket(matches.length - placedIds.length) }) + return { placedIds } + }, [sendPortrait]) + + const handlePhotoStartClose = useCallback(async () => { + setPhotoStart((current) => ({ ...current, visible: false })) + if (photoStartDirtyRef.current) { + photoStartDirtyRef.current = false + await refreshTreeAndKeepSelection(null, { relayout: true, avatarCacheBust: Date.now() }) + } }, [refreshTreeAndKeepSelection]) const handleSelectSelfPerson = (personId) => { @@ -2529,6 +2667,16 @@ function App() { )} + {treeImportError && ( +
+ {t("Votre arbre est créé, mais l'import n'a pas abouti.")} + {` ${treeImportError} `} + {t("Vous pouvez réessayer depuis les paramètres de l'arbre.")} + +
+ )} {/* Bouton premier ajout (arbre vide) */} {isEmptyTree && ( + + + )} +
{t('Télécharger en GEDCOM')} diff --git a/frontend/src/components/Galaxy.jsx b/frontend/src/components/Galaxy.jsx index 816ce80..6d69962 100644 --- a/frontend/src/components/Galaxy.jsx +++ b/frontend/src/components/Galaxy.jsx @@ -29,12 +29,15 @@ const MAX_SCALE = 3.2 const FOCUS_MIN_READABLE_SCALE = 0.5 const FOCUS_SCALE = 0.95 -const Galaxy = ({ onPersonSelect, onMediaSelect, selectedPersonId, searchHighlightIds = [], graphRevision = 0, annotationHandlers = null, transformReadRef = null, zoomApiRef = null, focusPersonId = null, assetRevision = 0 }) => { +const Galaxy = ({ onPersonSelect, onMediaSelect, selectedPersonId, searchHighlightIds = [], graphRevision = 0, annotationHandlers = null, transformReadRef = null, zoomApiRef = null, focusPersonId = null, assetRevision = 0, photoHint = false }) => { const canvasRef = useRef(null) // Lu au calcul du cadrage seulement : changer de personne de départ ne recadre pas une vue en cours const focusPersonIdRef = useRef(focusPersonId) // Mis à jour hors du rendu, avant l'effet de cadrage déclaré plus bas (même phase, ordre de déclaration) useEffect(() => { focusPersonIdRef.current = focusPersonId }, [focusPersonId]) + // Propriétaire : un portrait vide survolé montre « + photo » + const photoHintRef = useRef(photoHint) + useEffect(() => { photoHintRef.current = photoHint }, [photoHint]) const layoutDataRef = useRef(null) const [layoutReady, setLayoutReady] = useState(false) const transformRef = useRef({ x: 0, y: 0, scale: 1 }) @@ -571,6 +574,7 @@ const Galaxy = ({ onPersonSelect, onMediaSelect, selectedPersonId, searchHighlig mediaHoverState: mediaHoverState.current, entrance, entranceActive, elapsed, view, + photoHint: photoHintRef.current, }) drawRenvoiPills(ctx) // Annotations dessinées APRES les noeuds pour que stickers/texte soient au-dessus diff --git a/frontend/src/components/PersonCard.css b/frontend/src/components/PersonCard.css index 9405517..e1cd883 100644 --- a/frontend/src/components/PersonCard.css +++ b/frontend/src/components/PersonCard.css @@ -44,6 +44,16 @@ } .pc-photo img { width: 100%; height: 100%; object-fit: cover; display: block; } .pc-photo-initials { font: 400 30px/1 var(--pz-serif); color: var(--pz-a700); } +.pc-photo--add { + border: 1.5px dashed var(--pz-a300); background: var(--pz-a50); box-shadow: none; cursor: pointer; + align-content: center; gap: 5px; padding: 6px; color: var(--pz-a700); + font: 500 11.5px/1.15 var(--pz-sans); text-align: center; + transition: background-color 160ms ease, border-color 160ms ease, transform 140ms var(--pz-ease); +} +@media (hover: hover) and (pointer: fine) { .pc-photo--add:hover { background: var(--pz-a100); border-color: var(--pz-a500); } } +.pc-photo--add:active:not(:disabled) { transform: scale(.97); } +.pc-photo--add:focus-visible { outline: 2px solid var(--pz-a500); outline-offset: 3px; } +.pc-photo--add:disabled { opacity: .6; cursor: default; } .pc-photo-actions { display: grid; gap: 4px; justify-items: center; } .pc-photo-btn { border: 0; background: none; padding: 2px 4px; cursor: pointer; diff --git a/frontend/src/components/PersonCard.jsx b/frontend/src/components/PersonCard.jsx index bf35fdb..80e678d 100644 --- a/frontend/src/components/PersonCard.jsx +++ b/frontend/src/components/PersonCard.jsx @@ -1,5 +1,5 @@ import { useState, useEffect, useRef, forwardRef } from 'react' -import { Save, Trash2, Plus, X, Search, Check, Play } from 'lucide-react' +import { Save, Trash2, Plus, X, Search, Check, Play, ImagePlus } from 'lucide-react' import { getParents, getChildren, getPersonUnions, getPersonMedias, filiations, persons, unions } from '../data/mockData' import GeoMediaThumbnail from './GeoMediaThumbnail' import './PersonCard.css' @@ -333,21 +333,36 @@ const PersonCard = ({ {/* ── En-tête : visage, nom, repères ── */}
-
- {person.photo - ? {`${person.firstName} - : {initials}} -
+ {onAvatarUpload && ( + + )} + {/* Portrait vide chez le propriétaire : le cadre lui-même invite à poser la photo, sans passer par l'édition */} + {!person.photo && onAvatarUpload && !editMode ? ( + + ) : ( +
+ {person.photo + ? {`${person.firstName} + : {initials}} +
+ )} {editMode && onAvatarUpload && (
- diff --git a/frontend/src/components/PhotoStartPanel.css b/frontend/src/components/PhotoStartPanel.css new file mode 100644 index 0000000..b557ca3 --- /dev/null +++ b/frontend/src/components/PhotoStartPanel.css @@ -0,0 +1,61 @@ +/* Premiers visages après un import : retrouver les photos du logiciel, puis cinq portraits à demander */ + +.psp { width: min(100%, 560px); gap: 22px; } +.psp .pz-title em { font-style: italic; color: var(--pz-a700); } + +.psp-block { display: grid; gap: 12px; } +.psp-block--folder { + padding: 18px; + background: var(--pz-a50); + border: 1px solid var(--pz-a100); + border-radius: var(--pz-r-m); +} +.psp-h { margin: 0; font: 500 16px/1.35 var(--pz-sans); color: var(--pz-ink); } +.psp-p { margin: 0; font: 400 15px/1.55 var(--pz-sans); color: var(--pz-ink-2); } +.psp-p strong { font-weight: 600; color: var(--pz-ink); } +.psp-block .pz-hint { overflow-wrap: anywhere; } +.psp-folder-btn { justify-self: start; } +.psp-row-actions { display: flex; flex-wrap: wrap; gap: 8px; } +.psp-found { display: grid; gap: 10px; justify-items: start; } + +.psp-progress { display: grid; gap: 8px; } +.psp-bar { height: 8px; border-radius: 999px; background: var(--pz-surface); box-shadow: inset 0 0 0 1px var(--pz-a100); overflow: hidden; } +.psp-bar span { display: block; height: 100%; background: var(--pz-a500); border-radius: inherit; transition: width 240ms var(--pz-ease); } + +.psp-faces { list-style: none; margin: 0; padding: 0; display: grid; gap: 4px; } +.psp-face { + display: flex; + align-items: center; + gap: 14px; + min-height: 62px; + padding: 6px 4px; + border-bottom: 1px solid var(--pz-line-soft); +} +.psp-face:last-child { border-bottom: 0; } +.psp-face-photo { + flex: none; + width: 48px; + height: 48px; + border-radius: 50%; + display: grid; + place-items: center; + overflow: hidden; + background: var(--pz-bg); + border: 1.5px dashed var(--pz-a300); + font: 400 16px var(--pz-serif); + color: var(--pz-muted); +} +.psp-face-photo.is-set { border: 2px solid #fff; box-shadow: 0 0 0 1px var(--pz-line), var(--pz-shadow); } +.psp-face-photo img { width: 100%; height: 100%; object-fit: cover; display: block; } +.psp-face-id { flex: 1; min-width: 0; display: grid; gap: 1px; } +.psp-face-name { font: 400 17px/1.25 var(--pz-serif); color: var(--pz-ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.psp-face-years { font: 400 12.5px var(--pz-sans); color: var(--pz-muted); } +.psp-face-error { font: 400 12.5px/1.4 var(--pz-sans); color: var(--pz-danger); } +.psp-face-done { color: var(--pz-success); } +.psp-face .pz-btn { flex: none; gap: 6px; } + +@media (max-width: 560px) { + .psp { gap: 18px; } + .psp-block--folder { padding: 16px; } + .psp-face .pz-btn--soft { padding: 0 12px; } +} diff --git a/frontend/src/components/PhotoStartPanel.jsx b/frontend/src/components/PhotoStartPanel.jsx new file mode 100644 index 0000000..1a224bd --- /dev/null +++ b/frontend/src/components/PhotoStartPanel.jsx @@ -0,0 +1,283 @@ +import { useEffect, useMemo, useRef, useState } from 'react' +import { Check, FolderOpen, ImagePlus, X } from 'lucide-react' +import './PhotoStartPanel.css' +import PzBusy from './PzBusy.jsx' +import { t } from '../i18n/index.js' +import { matchPhotoFiles } from '../utils/photoImport.js' + +function withEmphasis(text) { + const [before, emphasized = '', after = ''] = text.split(/<\/?em>/) + return <>{before}{emphasized}{after} +} + +function lifeYears(person) { + if (!person.birthYear) return '' + return person.deathYear ? `${person.birthYear} – ${person.deathYear}` : String(person.birthYear) +} + +// Singulier ou pluriel, les deux formes passent par t() +function plural(n, one, many, vars = {}) { + return n === 1 ? t(one, vars) : t(many, { n, ...vars }) +} + +function initialsOf(person) { + return `${person.firstName?.[0] || ''}${person.lastName?.[0] || ''}`.toUpperCase() || '?' +} + +// Dossier où le logiciel d'origine rangeait la première photo citée, pour aider à le retrouver +function folderOf(filePath) { + const parts = String(filePath || '').split(/([\\/])/) + if (parts.length < 3) return '' + return parts.slice(0, -2).join('') +} + +// Après un import GEDCOM (ou depuis les paramètres) : retrouver les photos que le logiciel connaissait, +// et demander les premiers visages un par un. +export default function PhotoStartPanel({ + visible, + personsImported = null, + photoRefs = [], + faces = [], + onUploadOne, + onBulkPlace, + onRefsChange, + onClose, +}) { + const folderInputRef = useRef(null) + const faceInputRef = useRef(null) + const faceTargetRef = useRef(null) + const [remaining, setRemaining] = useState(photoRefs) + const [found, setFound] = useState(null) + const [bulk, setBulk] = useState({ running: false, done: 0, total: 0, placed: null, failed: 0 }) + const [faceState, setFaceState] = useState({}) + const [shownFaces, setShownFaces] = useState(faces) + const [placedTotal, setPlacedTotal] = useState(0) + + // La liste des visages et les chemins sont figés à l'ouverture : une ligne ne disparaît pas sous le doigt + useEffect(() => { + if (!visible) return + setRemaining(photoRefs) + setFound(null) + setBulk({ running: false, done: 0, total: 0, placed: null, failed: 0 }) + setFaceState({}) + setShownFaces(faces) + setPlacedTotal(0) + }, [visible]) // eslint-disable-line react-hooks/exhaustive-deps + + useEffect(() => { + if (!visible) return undefined + const onKey = (e) => { if (e.key === 'Escape' && !bulk.running) onClose?.() } + document.addEventListener('keydown', onKey) + return () => document.removeEventListener('keydown', onKey) + }, [visible, onClose, bulk.running]) + + // Les aperçus locaux sont libérés quand la fenêtre se ferme + const previewsRef = useRef([]) + useEffect(() => { + if (visible) return undefined + previewsRef.current.forEach((url) => URL.revokeObjectURL(url)) + previewsRef.current = [] + return undefined + }, [visible]) + + const exampleFolder = useMemo(() => folderOf(remaining[0]?.file), [remaining]) + + if (!visible) return null + + const handleFolder = (event) => { + const files = Array.from(event.target.files || []) + event.target.value = '' + if (files.length === 0) return + setBulk((b) => ({ ...b, placed: null, failed: 0 })) + setFound(matchPhotoFiles(remaining, files)) + } + + const handlePlace = async () => { + if (!found?.matches.length || bulk.running) return + const total = found.matches.length + setBulk({ running: true, done: 0, total, placed: null, failed: 0 }) + const result = await onBulkPlace(found.matches, (done) => setBulk((b) => ({ ...b, done }))) + const placedIds = new Set(result.placedIds) + // Une personne de la liste des visages dont la photo vient d'être placée passe en « Ajoutée » + const shownIds = new Set(shownFaces.map((p) => String(p.id))) + const placedFaces = {} + for (const match of found.matches) { + if (!placedIds.has(match.personId) || !shownIds.has(String(match.personId))) continue + const preview = URL.createObjectURL(match.file) + previewsRef.current.push(preview) + placedFaces[match.personId] = { busy: false, error: '', preview, fromFolder: true } + } + setFaceState((s) => ({ ...s, ...placedFaces })) + const nextRemaining = remaining.filter((ref) => !placedIds.has(ref.personId)) + setRemaining(nextRemaining) + onRefsChange?.(nextRemaining) + setFound(null) + setPlacedTotal((n) => n + placedIds.size) + setBulk({ running: false, done: total, total, placed: placedIds.size, failed: total - placedIds.size }) + } + + const pickFace = (personId) => { + faceTargetRef.current = personId + faceInputRef.current?.click() + } + + const handleFaceFile = async (event) => { + const file = event.target.files?.[0] + event.target.value = '' + const personId = faceTargetRef.current + if (!file || !personId) return + setFaceState((s) => ({ ...s, [personId]: { ...s[personId], busy: true, error: '' } })) + const error = await onUploadOne(personId, file) + setFaceState((s) => { + const previous = s[personId] + if (error) return { ...s, [personId]: { ...previous, busy: false, error } } + const preview = URL.createObjectURL(file) + previewsRef.current.push(preview) + return { ...s, [personId]: { busy: false, error: '', preview } } + }) + } + + const refsKnown = photoRefs.length > 0 + const addedCount = Object.values(faceState).filter((s) => s?.preview && !s.fromFolder).length + placedTotal + + return ( +
+
+ +
+ {personsImported != null &&

{t('Import terminé')}

} +

{withEmphasis(t('Il manque les visages'))}

+

+ {personsImported != null + ? t('{n} personnes sont dans votre arbre. Avec une photo, la famille se reconnaît tout de suite.', { n: personsImported.toLocaleString() }) + : t('Avec une photo, la famille se reconnaît tout de suite.')} +

+
+ + {refsKnown && ( +
+

{t('Vos photos sont déjà sur votre ordinateur')}

+ {remaining.length > 0 ? ( + <> +

+ {plural(remaining.length, 'Votre logiciel associait une photo à 1 personne. Choisissez le dossier où il la range, Parenthèse place le portrait.', 'Votre logiciel associait une photo à {n} personnes. Choisissez le dossier où il les range, Parenthèse place chaque portrait.')} +

+ {exampleFolder && ( +

{t('Il les rangeait dans {folder}', { folder: exampleFolder })}

+ )} + + ) : ( +

{t('Toutes les photos de votre logiciel sont placées.')}

+ )} + + + + {bulk.running ? ( +
+
+

{t('Envoi de {done} sur {total}', { done: bulk.done, total: bulk.total })}

+
+ ) : found ? ( +
+ {found.matches.length > 0 ? ( + <> +

{plural(found.matches.length, '1 photo retrouvée sur {k}.', '{n} photos retrouvées sur {k}.', { k: remaining.length })}

+
+ + +
+ + ) : ( + <> +

{t("Aucune photo de ce dossier ne correspond. Essayez le dossier indiqué plus haut, ou celui qui le contient.")}

+ + + )} +
+ ) : remaining.length > 0 ? ( + + ) : null} + + {bulk.placed != null && !bulk.running && ( +

+ {plural(bulk.placed, '1 portrait placé.', '{n} portraits placés.')} + {bulk.failed > 0 && ` ${t("{f} n'ont pas pu être envoyés (format non pris en charge ou fichier trop lourd).", { f: bulk.failed })}`} +

+ )} + {!bulk.running && remaining.length > 0 && ( +

{t('Seules les photos qui correspondent à une personne sont envoyées, vers votre arbre uniquement.')}

+ )} +
+ )} + + {shownFaces.length > 0 && ( +
+

{refsKnown ? t('Ou commencez par ces visages') : t('Commencez par ces visages')}

+ +
    + {shownFaces.map((person) => { + const state = faceState[person.id] || {} + const years = lifeYears(person) + return ( +
  • + + + {`${person.firstName} ${person.lastName || ''}`.trim()} + {years && {years}} + {state.error && {state.error}} + + {state.preview && !state.busy ? ( + + ) : ( + + )} +
  • + ) + })} +
+
+ )} + +
+

+ {addedCount > 0 ? plural(addedCount, '1 photo ajoutée.', '{n} photos ajoutées.') : t('Vous pourrez en ajouter plus tard depuis chaque fiche.')} +

+ +
+
+
+ ) +} diff --git a/frontend/src/components/TreeCreationWizard.jsx b/frontend/src/components/TreeCreationWizard.jsx index 4310770..9d1407c 100644 --- a/frontend/src/components/TreeCreationWizard.jsx +++ b/frontend/src/components/TreeCreationWizard.jsx @@ -1,5 +1,5 @@ import { useEffect, useMemo, useState } from 'react' -import { ImagePlus } from 'lucide-react' +import { FileUp, ImagePlus } from 'lucide-react' import './TreeCreationWizard.css' import PzBusy from './PzBusy.jsx' import { t } from '../i18n/index.js' @@ -10,6 +10,9 @@ const INITIAL_STATE = { selfLastName: '', selfBirthYear: '', photoFile: null, + // « J'ai déjà un arbre » : fichier GEDCOM exporté d'un logiciel, importé à la création + start: 'self', + gedcomFile: null, } // Titre traduit d'un bloc : le passage entre et garde son italique @@ -46,8 +49,9 @@ function TreeCreationWizard({ const canContinue = useMemo(() => form.treeName.trim().length >= 2 && !loading, [form.treeName, loading]) const canCreate = useMemo(() => { if (loading) return false + if (form.start === 'import') return Boolean(form.gedcomFile) return form.selfFirstName.trim().length > 0 && form.selfLastName.trim().length > 0 - }, [form.selfFirstName, form.selfLastName, loading]) + }, [form.selfFirstName, form.selfLastName, form.start, form.gedcomFile, loading]) if (!visible) { return null @@ -68,6 +72,11 @@ function TreeCreationWizard({ if (!canCreate) return setError('') + if (form.start === 'import') { + const submitError = await onSubmit({ treeName: form.treeName.trim(), gedcomFile: form.gedcomFile }) + if (submitError) setError(submitError) + return + } const submitError = await onSubmit({ treeName: form.treeName.trim(), selfFirstName: form.selfFirstName.trim(), @@ -123,6 +132,40 @@ function TreeCreationWizard({ ) : (
+
+ + +
+ + {form.start === 'import' ? ( + <> +
+

{withEmphasis(t('Importez votre arbre'))}

+

{t('Heredis, Geneanet, Gramps, MyHeritage ou Ancestry savent exporter un fichier .ged. Parenthèse reprend les personnes, les liens et les dates, puis vous aide à retrouver les photos.')}

+
+
+ + updateField('gedcomFile', event.target.files?.[0] || null)} + /> +
+ + ) : ( + <>

{withEmphasis(t('Commencez par vous'))}

{t("L'arbre « {name} » démarre avec votre fiche. Vous ajouterez les autres ensuite.", { name: form.treeName.trim() })}

@@ -186,6 +229,8 @@ function TreeCreationWizard({ onChange={(event) => updateField('photoFile', event.target.files?.[0] || null)} />
+ + )} {error &&
{error}
} @@ -194,7 +239,9 @@ function TreeCreationWizard({ {t('Retour')}
diff --git a/frontend/src/components/galaxy/elkLayout.js b/frontend/src/components/galaxy/elkLayout.js index ba2ce5a..6f32e97 100644 --- a/frontend/src/components/galaxy/elkLayout.js +++ b/frontend/src/components/galaxy/elkLayout.js @@ -311,13 +311,16 @@ export function computeLayout(graphData = null) { allNodes.push(node) } - // Unions virtuelles (parent unique → inconnu) + // Unions virtuelles (parent unique → inconnu) : une par parent, ses enfants sans autre parent connu + // partagent le même « Inconnu » (frères et sœurs d'un GEDCOM où un seul parent est renseigné) const virtualUnions = [] + const virtualByParent = new Set() normalizedFiliations.forEach(f => { - if (!f.unionId && f.parentId) { + if (!f.unionId && f.parentId && !virtualByParent.has(f.parentId)) { + virtualByParent.add(f.parentId) virtualUnions.push({ - elkId: `vu-${f.id}`, - unknownElkId: `unknown-${f.id}`, + elkId: `vu-${f.parentId}`, + unknownElkId: `unknown-${f.parentId}`, parentId: f.parentId, filiation: f, }) @@ -382,7 +385,7 @@ export function computeLayout(graphData = null) { // union/vu elkId → [child elkId] const childrenBySourceId = new Map() normalizedFiliations.forEach(f => { - const sourceId = f.unionId ? `u-${f.unionId}` : `vu-${f.id}` + const sourceId = f.unionId ? `u-${f.unionId}` : `vu-${f.parentId}` const targetId = `p-${f.childId}` if (!childrenBySourceId.has(sourceId)) childrenBySourceId.set(sourceId, []) childrenBySourceId.get(sourceId).push(targetId) @@ -515,7 +518,7 @@ export function computeLayout(graphData = null) { .map(e => ({ ...e })) normalizedFiliations.forEach(f => { - const sourceId = f.unionId ? `u-${f.unionId}` : `vu-${f.id}` + const sourceId = f.unionId ? `u-${f.unionId}` : `vu-${f.parentId}` const targetId = `p-${f.childId}` if (!validNodeIds.has(sourceId) || !validNodeIds.has(targetId)) return allEdges.push({ diff --git a/frontend/src/components/galaxy/renderers.js b/frontend/src/components/galaxy/renderers.js index b278f23..00fcbff 100644 --- a/frontend/src/components/galaxy/renderers.js +++ b/frontend/src/components/galaxy/renderers.js @@ -426,7 +426,7 @@ function measureTextCached(ctx, text, fontKey) { // ============================================================ function drawPersonNode(ctx, node, pos, nodeProgress, { nodeRandomData, imageCache, hoverScales, selectedGlowScales, searchGlowScales, - mediaHoverState, entrance, entranceActive, elapsed, view = null, + mediaHoverState, entrance, entranceActive, elapsed, view = null, photoHint = false, }) { const { cx, cy } = pos const person = getPersonById(node._data.id) || node._data @@ -562,7 +562,7 @@ function drawPersonNode(ctx, node, pos, nodeProgress, { ctx.shadowOffsetY = shadowY } - drawFrame(ctx, frameType, person, img) + drawFrame(ctx, frameType, person, img, photoHint && !img ? hs : 0) ctx.restore() // --- Labels (estompés puis absents en dézoom) --- @@ -880,7 +880,7 @@ function resolveFrameType(person, img) { return fallback } -function drawFrame(ctx, frameType, person, img) { +function drawFrame(ctx, frameType, person, img, hint = 0) { if (frameType === 'polaroid') { const pw = PERSON_R + 4 const ph = PERSON_R + 4 @@ -898,7 +898,7 @@ function drawFrame(ctx, frameType, person, img) { ctx.beginPath() ctx.roundRect(-pw, -ph, pw * 2, ph * 2, 2) ctx.clip() - drawPersonImage(ctx, img, person, -pw, -ph, pw * 2, ph * 2) + drawPersonImage(ctx, img, person, -pw, -ph, pw * 2, ph * 2, hint) } else if (frameType === 'square') { const s = PERSON_R + 4 const pad = 3 @@ -914,7 +914,7 @@ function drawFrame(ctx, frameType, person, img) { ctx.beginPath() ctx.roundRect(-s, -s, s * 2, s * 2, 2) ctx.clip() - drawPersonImage(ctx, img, person, -s, -s, s * 2, s * 2) + drawPersonImage(ctx, img, person, -s, -s, s * 2, s * 2, hint) } else if (frameType === 'rect') { const ratio = img?.naturalWidth && img?.naturalHeight ? img.naturalWidth / img.naturalHeight @@ -936,7 +936,7 @@ function drawFrame(ctx, frameType, person, img) { ctx.beginPath() ctx.roundRect(-rw, -rh, rw * 2, rh * 2, 3) ctx.clip() - drawPersonImage(ctx, img, person, -rw, -rh, rw * 2, rh * 2) + drawPersonImage(ctx, img, person, -rw, -rh, rw * 2, rh * 2, hint) } else { ctx.fillStyle = '#FFFFFF' ctx.beginPath() @@ -951,14 +951,14 @@ function drawFrame(ctx, frameType, person, img) { ctx.beginPath() ctx.arc(0, 0, PERSON_R, 0, Math.PI * 2) ctx.clip() - drawPersonImage(ctx, img, person, -PERSON_R, -PERSON_R, PERSON_R * 2, PERSON_R * 2) + drawPersonImage(ctx, img, person, -PERSON_R, -PERSON_R, PERSON_R * 2, PERSON_R * 2, hint) } } // ============================================================ // Dessiner l'image ou le placeholder initiales // ============================================================ -function drawPersonImage(ctx, img, person, x, y, w, h) { +function drawPersonImage(ctx, img, person, x, y, w, h, hint = 0) { if (img) { drawImageCover(ctx, img, x, y, w, h) if (!person.isAlive) { @@ -968,11 +968,27 @@ function drawPersonImage(ctx, img, person, x, y, w, h) { } else { ctx.fillStyle = person.isAlive ? COLORS.nodeAlive : COLORS.nodeDead ctx.fillRect(x, y, w, h) - ctx.fillStyle = COLORS.text - ctx.font = '400 16px "Newsreader", Georgia, serif' ctx.textAlign = 'center' ctx.textBaseline = 'middle' + // Au survol, chez le propriétaire, les initiales cèdent la place à « + photo » + const baseAlpha = ctx.globalAlpha + if (hint > 0.01) { + ctx.fillStyle = `rgba(248, 228, 219, ${hint})` + ctx.fillRect(x, y, w, h) + } + ctx.globalAlpha = baseAlpha * (1 - hint) + ctx.fillStyle = COLORS.text + ctx.font = '400 16px "Newsreader", Georgia, serif' ctx.fillText((person.firstName[0] || '') + (person.lastName[0] || ''), 0, 0) + if (hint > 0.01) { + ctx.globalAlpha = baseAlpha * hint + ctx.fillStyle = '#93402A' + ctx.font = '300 22px "DM Sans", system-ui, sans-serif' + ctx.fillText('+', 0, -6) + ctx.font = '500 9.5px "DM Sans", system-ui, sans-serif' + ctx.fillText(t('photo'), 0, 10) + } + ctx.globalAlpha = baseAlpha } } diff --git a/frontend/src/data/mockData.js b/frontend/src/data/mockData.js index b57a489..7a1084d 100644 --- a/frontend/src/data/mockData.js +++ b/frontend/src/data/mockData.js @@ -162,22 +162,32 @@ export function loadGraphData(graph, options = {}) { const unionIdSet = new Set(unions.map((union) => union.id)) const seenUnionChildKey = new Set() + const seenDirectKey = new Set() filiations = remoteFiliations - .map((filiation, index) => ({ - id: filiation.id, - childId: filiation.childPersonId, - unionId: filiation.viaUnionId || null, - parentId: filiation.viaUnionId ? null : filiation.parentPersonId, - parentageType: filiation.parentageType || 'biologique', - displayOrder: Number.isFinite(filiation.displayOrder) ? filiation.displayOrder : index + 1, - })) + .map((filiation, index) => { + // Union à un seul parent connu (fréquente dans un GEDCOM) : l'union est écartée plus haut, l'enfant + // est relié directement à ce parent, sinon il flotterait dans l'arbre sans aucun lien + const viaKeptUnion = filiation.viaUnionId && unionIdSet.has(filiation.viaUnionId) + return { + id: filiation.id, + childId: filiation.childPersonId, + unionId: viaKeptUnion ? filiation.viaUnionId : null, + parentId: viaKeptUnion ? null : filiation.parentPersonId, + parentageType: filiation.parentageType || 'biologique', + displayOrder: Number.isFinite(filiation.displayOrder) ? filiation.displayOrder : index + 1, + } + }) .filter((filiation) => { if (!personIdSet.has(filiation.childId)) { return false } if (!filiation.unionId) { - return filiation.parentId ? personIdSet.has(filiation.parentId) : false + if (!filiation.parentId || !personIdSet.has(filiation.parentId)) return false + const key = `${filiation.parentId}:${filiation.childId}` + if (seenDirectKey.has(key)) return false + seenDirectKey.add(key) + return true } if (!unionIdSet.has(filiation.unionId)) { diff --git a/frontend/src/data/mockData.test.js b/frontend/src/data/mockData.test.js index 5caa7fe..91ee070 100644 --- a/frontend/src/data/mockData.test.js +++ b/frontend/src/data/mockData.test.js @@ -50,3 +50,24 @@ describe('loadGraphData avatar mapping', () => { expect(getPersonMedias('p-2')[0]?.source).toBe('Album familial') }) }) + +describe('loadGraphData, union à un seul parent', () => { + it('relie les enfants directement au parent connu au lieu de les perdre', async () => { + const { getParents, getChildren } = await import('./mockData') + const person = (id, firstName) => ({ id, firstName, lastName: 'Lemaire', birthDate: null, deathDate: null, notes: null, avatarUrlPath: null }) + loadGraphData({ + persons: [person('julien', 'Julien'), person('hugo', 'Hugo'), person('lea', 'Léa')], + unions: [{ id: 'u-seul', partner1PersonId: 'julien', partner2PersonId: null }], + filiations: [ + { id: 'f-1', parentPersonId: 'julien', childPersonId: 'hugo', viaUnionId: 'u-seul' }, + { id: 'f-2', parentPersonId: 'julien', childPersonId: 'lea', viaUnionId: 'u-seul' }, + { id: 'f-3', parentPersonId: 'julien', childPersonId: 'lea', viaUnionId: 'u-seul' }, + ], + medias: [], + annotations: [], + }) + + expect(getParents('hugo').map((p) => p.id)).toEqual(['julien']) + expect(getChildren('julien').map((p) => p.id).sort()).toEqual(['hugo', 'lea']) + }) +}) diff --git a/frontend/src/i18n/en/index.js b/frontend/src/i18n/en/index.js index 175e2c1..a6c7d4e 100644 --- a/frontend/src/i18n/en/index.js +++ b/frontend/src/i18n/en/index.js @@ -4,6 +4,7 @@ import arbre from './arbre.js' import fiche from './fiche.js' import contribution from './contribution.js' import compte from './compte.js' +import photos from './photos.js' export default { ...app, @@ -11,5 +12,6 @@ export default { ...fiche, ...contribution, ...compte, + ...photos, } diff --git a/frontend/src/i18n/en/photos.js b/frontend/src/i18n/en/photos.js new file mode 100644 index 0000000..c4b462c --- /dev/null +++ b/frontend/src/i18n/en/photos.js @@ -0,0 +1,56 @@ +// Traductions anglaises, clé = texte français exact passé à t(). +// Lot « photos » : premiers visages après un import, photos retrouvées, import dès la création de l'arbre. +export default { + // Fenêtre des premiers visages (PhotoStartPanel) + "Import terminé": "Import complete", + "Il manque les visages": "The faces are missing", + "{n} personnes sont dans votre arbre. Avec une photo, la famille se reconnaît tout de suite.": "{n} people are in your tree. With a photo, the family recognises each other straight away.", + "Avec une photo, la famille se reconnaît tout de suite.": "With a photo, the family recognises each other straight away.", + "Vos photos sont déjà sur votre ordinateur": "Your photos are already on your computer", + "Votre logiciel associait une photo à 1 personne. Choisissez le dossier où il la range, Parenthèse place le portrait.": "Your software had a photo for 1 person. Choose the folder where it keeps it, and Parenthèse places the portrait.", + "Votre logiciel associait une photo à {n} personnes. Choisissez le dossier où il les range, Parenthèse place chaque portrait.": "Your software had a photo for {n} people. Choose the folder where it keeps them, and Parenthèse places each portrait.", + "Il les rangeait dans {folder}": "It kept them in {folder}", + "Toutes les photos de votre logiciel sont placées.": "All the photos from your software are in place.", + "Envoi de {done} sur {total}": "Sending {done} of {total}", + "1 photo retrouvée sur {k}.": "1 photo found out of {k}.", + "{n} photos retrouvées sur {k}.": "{n} photos found out of {k}.", + "Placer ce portrait": "Place this portrait", + "Placer les {n} portraits": "Place the {n} portraits", + "Un autre dossier": "Another folder", + "Aucune photo de ce dossier ne correspond. Essayez le dossier indiqué plus haut, ou celui qui le contient.": "No photo in this folder matches. Try the folder shown above, or the one that contains it.", + "Choisir un autre dossier": "Choose another folder", + "Choisir le dossier des photos": "Choose the photo folder", + "1 portrait placé.": "1 portrait placed.", + "{n} portraits placés.": "{n} portraits placed.", + "{f} n'ont pas pu être envoyés (format non pris en charge ou fichier trop lourd).": "{f} couldn't be sent (unsupported format or file too large).", + "Seules les photos qui correspondent à une personne sont envoyées, vers votre arbre uniquement.": "Only the photos that match a person are sent, and only to your tree.", + "Ou commencez par ces visages": "Or start with these faces", + "Commencez par ces visages": "Start with these faces", + "Envoi": "Sending", + "Ajoutée": "Added", + "1 photo ajoutée.": "1 photo added.", + "{n} photos ajoutées.": "{n} photos added.", + "Vous pourrez en ajouter plus tard depuis chaque fiche.": "You can add more later from each profile.", + "Voir l'arbre": "See the tree", + "Session expirée, reconnectez-vous.": "Session expired, please sign in again.", + + // Portrait vide sur l'arbre, au survol + "photo": "photo", + + // Paramètres de l'arbre + "Ajouter des portraits": "Add portraits", + "Retrouver les photos de votre ancien logiciel, ou ajouter les premiers visages un par un.": "Bring back the photos from your old software, or add the first faces one by one.", + + // Création de l'arbre, « J'ai déjà un arbre » + "Point de départ": "Starting point", + "Je pars de moi": "Start with me", + "J'ai déjà un arbre": "I already have a tree", + "Importez votre arbre": "Import your tree", + "Heredis, Geneanet, Gramps, MyHeritage ou Ancestry savent exporter un fichier .ged. Parenthèse reprend les personnes, les liens et les dates, puis vous aide à retrouver les photos.": "Heredis, Geneanet, Gramps, MyHeritage and Ancestry can all export a .ged file. Parenthèse brings in the people, links and dates, then helps you find the photos.", + "Choisir le fichier .ged": "Choose the .ged file", + "Cliquez pour en choisir un autre": "Click to choose another one", + "Exporté depuis le logiciel où vous tenez votre arbre": "Exported from the software where you keep your tree", + "Importer mon arbre": "Import my tree", + "Votre arbre est créé, mais l'import n'a pas abouti.": "Your tree is created, but the import didn't go through.", + "Vous pouvez réessayer depuis les paramètres de l'arbre.": "You can try again from the tree settings.", +} diff --git a/frontend/src/utils/photoImport.js b/frontend/src/utils/photoImport.js new file mode 100644 index 0000000..644540f --- /dev/null +++ b/frontend/src/utils/photoImport.js @@ -0,0 +1,179 @@ +// Photos après un import GEDCOM : relier les chemins cités par le logiciel d'origine aux fichiers du dossier +// choisi par la personne, et choisir les premiers visages à demander. + +import { MAX_IMAGE_BYTES } from './mediaUpload.js' + +const MAX_SIDE = 2000 +const JPEG_QUALITY = 0.88 + +function normalizeName(value) { + return String(value || '').normalize('NFC').toLowerCase() +} + +// Dernier élément d'un chemin Windows, macOS ou Linux +export function baseNameOf(filePath) { + return String(filePath || '').split(/[\\/]/).filter(Boolean).pop() || '' +} + +function pathParts(filePath) { + return normalizeName(filePath).split(/[\\/]/).filter(Boolean) +} + +// Nombre de dossiers communs en partant du nom de fichier : départage deux « portrait.jpg » +function commonSuffixLength(a, b) { + const pa = pathParts(a) + const pb = pathParts(b) + let n = 0 + while (n < pa.length && n < pb.length && pa[pa.length - 1 - n] === pb[pb.length - 1 - n]) n += 1 + return n +} + +// photoRefs : [{ personId, file }] renvoyé par l'import. files : les File du dossier choisi +// (webkitRelativePath renseigné). Renvoie les paires trouvées et les références sans fichier. +export function matchPhotoFiles(photoRefs, files) { + const byName = new Map() + for (const file of files || []) { + const key = normalizeName(file.name) + if (!key) continue + if (!byName.has(key)) byName.set(key, []) + byName.get(key).push(file) + } + + const matches = [] + const missing = [] + for (const ref of photoRefs || []) { + const candidates = byName.get(normalizeName(baseNameOf(ref.file))) || [] + if (candidates.length === 0) { + missing.push(ref) + continue + } + let best = candidates[0] + let bestScore = -1 + for (const candidate of candidates) { + const score = commonSuffixLength(ref.file, candidate.webkitRelativePath || candidate.name) + if (score > bestScore) { + best = candidate + bestScore = score + } + } + matches.push({ personId: ref.personId, file: best }) + } + return { matches, missing } +} + +// Les premiers visages à demander : la famille proche de « Moi » si on la connaît, sinon les générations +// les plus récentes, dont les photos sont sous la main. Seulement des personnes sans portrait. +export function pickFirstFaces({ persons, selfId = null, getParents, getChildren, getPartners, limit = 5 }) { + const withoutPhoto = (persons || []).filter((p) => !p.photo) + if (withoutPhoto.length === 0) return [] + const wanted = new Set(withoutPhoto.map((p) => String(p.id))) + const byId = new Map((persons || []).map((p) => [String(p.id), p])) + + const picked = [] + if (selfId && byId.has(String(selfId))) { + const seen = new Set([String(selfId)]) + let frontier = [String(selfId)] + if (wanted.has(String(selfId))) picked.push(byId.get(String(selfId))) + while (frontier.length > 0 && picked.length < limit) { + const next = [] + for (const id of frontier) { + const around = [ + ...(getParents?.(id) || []), + ...(getPartners?.(id) || []), + ...(getChildren?.(id) || []), + ] + for (const person of around) { + const key = String(person?.id ?? '') + if (!key || seen.has(key)) continue + seen.add(key) + next.push(key) + } + } + const ring = next + .filter((key) => wanted.has(key)) + .map((key) => byId.get(key)) + .sort(byRecentBirth) + for (const person of ring) { + if (picked.length >= limit) break + picked.push(person) + } + frontier = next + } + } + + if (picked.length < limit) { + const already = new Set(picked.map((p) => String(p.id))) + const rest = withoutPhoto.filter((p) => !already.has(String(p.id))).sort(byRecentBirth) + picked.push(...rest.slice(0, limit - picked.length)) + } + return picked +} + +function byRecentBirth(a, b) { + const ya = Number.isFinite(a?.birthYear) ? a.birthYear : -Infinity + const yb = Number.isFinite(b?.birthYear) ? b.birthYear : -Infinity + if (ya !== yb) return yb - ya + if (Boolean(a?.isAlive) !== Boolean(b?.isAlive)) return a?.isAlive ? -1 : 1 + return 0 +} + +// Une photo de téléphone dépasse souvent 5 Mo : elle est réduite à 2000 px en JPEG avant l'envoi. +// Un format que le navigateur ne sait pas lire (HEIC sur Chrome) part tel quel, le serveur tranche. +export async function prepareImageForUpload(file) { + if (!file || !String(file.type || '').startsWith('image/') || file.type === 'image/gif') return file + let bitmap + try { + bitmap = await createImageBitmap(file, { imageOrientation: 'from-image' }) + } catch { + return file + } + const largest = Math.max(bitmap.width, bitmap.height) + const isSimpleFormat = ['image/jpeg', 'image/png', 'image/webp'].includes(file.type) + if (largest <= MAX_SIDE && file.size <= MAX_IMAGE_BYTES && isSimpleFormat) { + bitmap.close?.() + return file + } + const ratio = Math.min(1, MAX_SIDE / largest) + const canvas = document.createElement('canvas') + canvas.width = Math.max(1, Math.round(bitmap.width * ratio)) + canvas.height = Math.max(1, Math.round(bitmap.height * ratio)) + canvas.getContext('2d').drawImage(bitmap, 0, 0, canvas.width, canvas.height) + bitmap.close?.() + const blob = await new Promise((resolve) => canvas.toBlob(resolve, 'image/jpeg', JPEG_QUALITY)) + if (!blob) return file + const name = baseNameOf(file.name).replace(/\.[^.]+$/, '') || 'photo' + return new File([blob], `${name}.jpg`, { type: 'image/jpeg' }) +} + +// Les chemins de l'import, gardés sur l'appareil pour rouvrir l'étape plus tard (rien côté serveur) +const REFS_KEY = (treeId) => `parenthese_photo_refs:${treeId}` + +export function savePhotoRefs(treeId, refs) { + if (!treeId) return + try { + if (refs?.length) localStorage.setItem(REFS_KEY(treeId), JSON.stringify(refs)) + else localStorage.removeItem(REFS_KEY(treeId)) + } catch { + // Stockage plein ou indisponible : l'étape ne se rouvrira pas, l'import reste fait + } +} + +export function loadPhotoRefs(treeId) { + if (!treeId) return [] + try { + const parsed = JSON.parse(localStorage.getItem(REFS_KEY(treeId)) || '[]') + return Array.isArray(parsed) ? parsed.filter((r) => r && r.personId && r.file) : [] + } catch { + return [] + } +} + +// Effectifs arrondis pour PostHog : un ordre de grandeur, jamais un compte exact d'un arbre +export function sizeBucket(n) { + if (!n) return '0' + if (n < 10) return '1-9' + if (n < 50) return '10-49' + if (n < 200) return '50-199' + if (n < 1000) return '200-999' + return '1000+' +} diff --git a/frontend/src/utils/photoImport.test.js b/frontend/src/utils/photoImport.test.js new file mode 100644 index 0000000..c0185be --- /dev/null +++ b/frontend/src/utils/photoImport.test.js @@ -0,0 +1,77 @@ +import { describe, expect, it } from 'vitest' +import { baseNameOf, matchPhotoFiles, pickFirstFaces, sizeBucket } from './photoImport.js' + +const file = (name, webkitRelativePath = name) => ({ name, webkitRelativePath }) + +describe('baseNameOf', () => { + it('lit le nom de fichier d\'un chemin Windows, macOS ou relatif', () => { + expect(baseNameOf('C:\\Genealogie\\Photos\\jean.jpg')).toBe('jean.jpg') + expect(baseNameOf('/Users/anne/Heredis/marie.png')).toBe('marie.png') + expect(baseNameOf('photos/robert 1920.jpg')).toBe('robert 1920.jpg') + }) +}) + +describe('matchPhotoFiles', () => { + it('relie chaque chemin du GEDCOM au fichier du même nom, sans tenir compte de la casse', () => { + const refs = [ + { personId: 'a', file: 'C:\\Photos\\Jean.JPG' }, + { personId: 'b', file: 'C:\\Photos\\marie.jpg' }, + { personId: 'c', file: 'C:\\Photos\\absente.jpg' }, + ] + const { matches, missing } = matchPhotoFiles(refs, [file('jean.jpg', 'Photos/jean.jpg'), file('marie.jpg', 'Photos/marie.jpg')]) + expect(matches.map((m) => [m.personId, m.file.name])).toEqual([['a', 'jean.jpg'], ['b', 'marie.jpg']]) + expect(missing.map((m) => m.personId)).toEqual(['c']) + }) + + it('départage deux fichiers du même nom par les dossiers du chemin', () => { + const refs = [{ personId: 'a', file: 'D:\\Arbre\\Dupont\\portrait.jpg' }] + const files = [file('portrait.jpg', 'Arbre/Martin/portrait.jpg'), file('portrait.jpg', 'Arbre/Dupont/portrait.jpg')] + expect(matchPhotoFiles(refs, files).matches[0].file.webkitRelativePath).toBe('Arbre/Dupont/portrait.jpg') + }) + + it('rapproche un nom accentué écrit sous deux formes Unicode', () => { + const refs = [{ personId: 'a', file: 'Hélène.jpg' }] + const decomposed = 'He\u0301le\u0300ne.jpg' + expect(matchPhotoFiles(refs, [file(decomposed)]).matches).toHaveLength(1) + }) +}) + +describe('pickFirstFaces', () => { + const people = [ + { id: 'moi', firstName: 'Lou', birthYear: 1990, isAlive: true }, + { id: 'mere', firstName: 'Anne', birthYear: 1962, isAlive: true }, + { id: 'pere', firstName: 'Marc', birthYear: 1960, isAlive: true, photo: '/p.jpg' }, + { id: 'fils', firstName: 'Tom', birthYear: 2020, isAlive: true }, + { id: 'aieul', firstName: 'Jules', birthYear: 1880, isAlive: false }, + { id: 'cousin', firstName: 'Paul', birthYear: 1995, isAlive: true }, + ] + const byId = new Map(people.map((p) => [p.id, p])) + const parents = { moi: ['mere', 'pere'], mere: ['aieul'] } + const children = { moi: ['fils'], mere: ['moi'], pere: ['moi'], aieul: ['mere'] } + const helpers = { + getParents: (id) => (parents[id] || []).map((x) => byId.get(x)), + getChildren: (id) => (children[id] || []).map((x) => byId.get(x)), + getPartners: () => [], + } + + it('part de « Moi » puis de ses proches, sans les personnes qui ont déjà un portrait', () => { + const picked = pickFirstFaces({ persons: people, selfId: 'moi', ...helpers, limit: 3 }) + expect(picked.map((p) => p.id)).toEqual(['moi', 'fils', 'mere']) + }) + + it('sans « Moi », prend les naissances les plus récentes', () => { + const picked = pickFirstFaces({ persons: people, ...helpers, limit: 3 }) + expect(picked.map((p) => p.id)).toEqual(['fils', 'cousin', 'moi']) + }) + + it('complète avec les plus récents quand la famille proche ne suffit pas', () => { + const picked = pickFirstFaces({ persons: people, selfId: 'aieul', ...helpers, limit: 3 }) + expect(picked.map((p) => p.id)).toEqual(['aieul', 'mere', 'moi']) + }) +}) + +describe('sizeBucket', () => { + it('arrondit les effectifs envoyés à PostHog', () => { + expect([0, 3, 12, 80, 450, 2697].map(sizeBucket)).toEqual(['0', '1-9', '10-49', '50-199', '200-999', '1000+']) + }) +}) From a91474f0e6caaa01b567e124ff11ad6f6900a572 Mon Sep 17 00:00:00 2001 From: IliesAllali Date: Sun, 11 Oct 2026 20:38:04 +0200 Subject: [PATCH 11/11] fix(docker): 120 s de marge au healthcheck de l'API au premier lancement MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Les migrations Prisma passent avant que l'API réponde. Avec 30 s, le conteneur web, qui attend une API saine, abandonnait au premier docker compose up (signalé sur r/GenealogieFR le 11/10). Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + docker-compose.yml | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4114143..6636000 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ Notable changes to Parenthèse. Dates are in YYYY-MM-DD format. ### Fixes +- Docker: on a first `docker compose up`, the API now gets 120 s to run its migrations before its health check counts (was 30 s). The web container waits for a healthy API and used to give up, so it had to be started again by hand. - Children of a family with only one known parent (common in GEDCOM files) are linked to that parent, under a single "Unknown" partner, instead of floating in the tree without a link. - Importing a GEDCOM file into a tree that already holds it no longer duplicates everyone: people with the same first name, last name, birth and death dates are reused, and existing unions and parent links are kept. diff --git a/docker-compose.yml b/docker-compose.yml index 341bb3e..0e19bb0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -50,7 +50,9 @@ services: interval: 15s timeout: 5s retries: 5 - start_period: 30s + # Premier lancement : les migrations passent avant que l'API réponde, 30 s ne suffisaient pas + # et le conteneur web, qui attend une API prête, abandonnait (signalé sur Reddit le 11/10). + start_period: 120s web: image: ghcr.io/iliesallali/parenthese-web:${PARENTHESE_VERSION:-latest}