diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..1fe4f49 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,15 @@ + +# Docker build context — keep the image small and free of secrets +node_modules +.next +.git +.env +.env.* +!.env.example +coverage +.preview +e2e +tests +backups +*.md +!README.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 68dfdb4..bdcb5ee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,3 +49,17 @@ jobs: - run: npm run typecheck - run: npm run coverage - run: npm run build + + docker: + name: Docker image + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: docker/setup-buildx-action@v3 + - name: Build the runtime image + uses: docker/build-push-action@v6 + with: + context: . + push: false + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/DEPLOY.md b/DEPLOY.md new file mode 100644 index 0000000..6b02bc3 --- /dev/null +++ b/DEPLOY.md @@ -0,0 +1,124 @@ +# Déploiement + +Procédures d'exploitation pour `horaires.ita-ito.com`. Le [README](README.md) +couvre l'installation locale et la configuration d'Authentik. + +## Première installation sur le VPS + +Prérequis : Docker, Docker Compose, et un Traefik déjà en route possédant le +réseau externe nommé par `TRAEFIK_NETWORK`. + +```bash +git clone /opt/horaires && cd /opt/horaires + +cp .env.example .env +sed -i "s|^AUTH_SECRET=.*|AUTH_SECRET=$(openssl rand -base64 33)|" .env +sed -i "s|^POSTGRES_PASSWORD=.*|POSTGRES_PASSWORD=$(openssl rand -base64 24)|" .env +# Renseignez APP_DOMAIN, AUTH_URL, AUTH_AUTHENTIK_*, TRANSLATION_API_KEY. + +mkdir -p backups +docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build --wait +``` + +Les migrations s'appliquent au démarrage. Si elles échouent, le conteneur +s'arrête **avant** de servir quoi que ce soit, plutôt que d'exposer une base +incohérente : `docker compose logs app` dira pourquoi. + +Ensuite : + +```bash +docker compose exec app node .migrator/node_modules/prisma/build/index.js migrate status +curl -fsS https://horaires.ita-ito.com/api/health +``` + +Puis, une fois connecté : *Paramètres* pour le canton et les intervalles de +réveil, *Fériés* → **Synchroniser maintenant**, *Horaires* pour la vraie +semaine, et enfin l'appairage de l'écran (README). + +## Mise à jour + +```bash +cd /opt/horaires +./scripts/backup.sh # ou attendre le dump nocturne +git pull +docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build --wait +docker compose logs -f app # ^C une fois « démarrage de l'application » +``` + +`--wait` rend la main seulement quand le *healthcheck* passe. S'il ne passe pas, +la mise à jour a échoué et l'ancien conteneur a déjà été remplacé : voir le +retour arrière ci-dessous. + +**Avant une mise à jour qui touche au rendu** (polices, gabarit, logo), +sachez que le nom de fichier de l'image change et que tous les écrans +redessineront une fois. C'est sans gravité, seulement un cycle de batterie. + +## Sauvegarde + +Le service `backup` dépose un dump compressé par jour dans `./backups` et purge +au-delà de `BACKUP_KEEP_DAYS` (14 par défaut). + +Dump immédiat : + +```bash +docker compose exec -T db pg_dump -U horaires horaires | gzip > backups/horaires-$(date +%Y%m%d-%H%M%S).sql.gz +``` + +Ces fichiers contiennent les horaires, les messages et le journal d'audit. Ils +ne contiennent **aucun secret** : les jetons d'appareil n'y figurent que sous +forme d'empreintes, et les identifiants Authentik vivent dans `.env`, qui n'est +pas dans les sauvegardes. Sauvegardez `.env` séparément, ailleurs. + +## Restauration + +```bash +docker compose -f docker-compose.yml -f docker-compose.prod.yml stop app +gunzip -c backups/horaires-20260920-030000.sql.gz \ + | docker compose exec -T db psql -U horaires -d horaires +docker compose -f docker-compose.yml -f docker-compose.prod.yml start app +``` + +Après une restauration, les écrans appairés depuis le dump ne le sont plus du +point de vue de la base : ils recevront un 401 et devront être réappairés par le +portail captif. Rien d'autre ne se perd. + +## Retour arrière + +L'image porte la version du dépôt, donc revenir en arrière veut dire revenir au +commit : + +```bash +git log --oneline -10 +git checkout +docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build --wait +``` + +**Si la mise à jour comportait une migration**, revenir au code ne suffit pas : +le schéma est déjà migré. Prisma ne défait pas une migration. Restaurez alors le +dump pris avant la mise à jour, puis reprenez le code. C'est la raison d'être de +la sauvegarde préalable. + +## Ce qu'il faut surveiller + +| Signe | Où | Que faire | +|---|---|---| +| L'écran n'a pas donné signe de vie | alerte du tableau de bord | batterie, puis Wi-Fi de la boutique | +| Synchro des fériés en échec | alerte, page Fériés | le cache local tient ; relancer à la main | +| Messages sans traduction | alerte, page Messages | `npm run translate:test` pour savoir pourquoi | +| Conteneur *unhealthy* | `docker compose ps` | `docker compose logs app` | + +## Le panneau tombe sur le certificat TLS + +Certains firmwares ESP32 échouent sur une chaîne de certificats que tous les +navigateurs acceptent. Si l'écran n'arrive à joindre le serveur qu'en clair, +`DEVICE_ALLOW_HTTP=true` existe — mais c'est le dernier recours, pas le premier. + +Dans l'ordre : + +1. Vérifiez que l'écran atteint bien le domaine : `docker compose logs app | grep /api/setup`. +2. Vérifiez la chaîne servie : `openssl s_client -connect horaires.ita-ito.com:443 -servername horaires.ita-ito.com | head -20`. Une chaîne incomplète est le cas le plus fréquent et se corrige côté Traefik, pas côté écran. +3. En dernier recours seulement, exposez un hôte virtuel en clair réservé aux + routes `/api/setup`, `/api/display`, `/api/log` et `/api/device/image/*`. + Le jeton d'appareil circulerait alors en clair : il est distinct de tout + autre secret et révocable depuis *Paramètres → Appareils*, ce qui rend + l'arbitrage tenable — mais l'administration, elle, reste en TLS. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2318cea --- /dev/null +++ b/Dockerfile @@ -0,0 +1,86 @@ +# syntax=docker/dockerfile:1 + +# --------------------------------------------------------------------------- +# deps — install once, cached on the lockfile alone +# --------------------------------------------------------------------------- +FROM node:22-alpine AS deps +WORKDIR /app +COPY package.json package-lock.json ./ +RUN npm ci + +# --------------------------------------------------------------------------- +# build — generate the Prisma client, then compile +# --------------------------------------------------------------------------- +FROM node:22-alpine AS build +WORKDIR /app +ENV NEXT_TELEMETRY_DISABLED=1 +COPY --from=deps /app/node_modules ./node_modules +COPY . . +# The client is generated into lib/generated and imported by the app, so it +# must exist before the build traces its dependencies. +RUN npx prisma generate && npm run build + +# --------------------------------------------------------------------------- +# migrator — the Prisma CLI, on its own, with its own dependency tree +# +# The CLI cannot simply be copied out of the build stage: it needs transitive +# dependencies that are scattered through a hoisted node_modules, and copying +# the whole tree would double the image. Installing it alone keeps it small and +# keeps it working. The version is read from our own package.json so it cannot +# drift from the client the application was built against. dotenv comes along +# because prisma.config.ts imports it; in a container the environment is +# already populated, but the file is shared with local development. +# --------------------------------------------------------------------------- +FROM node:22-alpine AS migrator +WORKDIR /cli +COPY package.json ./ +RUN PRISMA_VERSION="$(node -p "require('./package.json').dependencies.prisma")" \ + && rm package.json \ + && npm init -y > /dev/null \ + && npm install --no-audit --no-fund --omit=dev "prisma@${PRISMA_VERSION}" dotenv + +# --------------------------------------------------------------------------- +# runner — standalone output plus what the migrations need +# --------------------------------------------------------------------------- +FROM node:22-alpine AS runner +WORKDIR /app + +ENV NODE_ENV=production \ + NEXT_TELEMETRY_DISABLED=1 \ + PORT=3010 \ + HOSTNAME=0.0.0.0 + +# curl for the healthcheck, tzdata so Europe/Zurich is a real zone rather than +# a name Intl quietly falls back to UTC for. +RUN apk add --no-cache curl tzdata + +COPY --from=build /app/.next/standalone ./ +COPY --from=build /app/.next/static ./.next/static +COPY --from=build /app/public ./public + +# `prisma migrate deploy` runs at startup, so the CLI, the schema, the +# migrations and the config travel with the image — all of them together under +# .migrator, which the entrypoint uses as its working directory. +# +# Keeping the config beside the CLI rather than at the application root is +# what makes the isolation hold: `prisma.config.ts` imports `prisma/config` +# and `dotenv`, and both resolve from the tree next to it. Splitting them +# would mean copying the CLI's dependencies into the application's own +# node_modules one failure at a time. +COPY --from=migrator /cli/node_modules ./.migrator/node_modules +COPY --from=build /app/prisma ./.migrator/prisma +COPY --from=build /app/prisma.config.ts ./.migrator/prisma.config.ts + +COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh +RUN chmod +x /usr/local/bin/docker-entrypoint.sh + +# The image ships with a non-root user; use it. +USER node + +EXPOSE 3010 + +HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \ + CMD curl -fsS http://127.0.0.1:3010/api/health || exit 1 + +ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"] +CMD ["node", "server.js"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..6953a15 --- /dev/null +++ b/README.md @@ -0,0 +1,171 @@ +# Horaires ITA ITO + +Panneau d'administration des horaires d'ouverture de la boutique **ITA ITO**, et +serveur d'affichage pour un écran e-ink **TRMNL 7,5" (kit DIY Seeed)** posé en +vitrine. + +Changer un horaire depuis un téléphone, debout dans la boutique, en deux clics — +et que l'écran suive tout seul, jours fériés genevois et message bilingue +compris. + +``` + Écran e-ink (ESP32, Wi-Fi boutique) + │ GET /api/setup · GET /api/display · POST /api/log + │ GET /api/device/image/.bmp + ▼ + ┌──────────────────────────────────────────────┐ + │ Next.js 16 + PostgreSQL 16 │ + │ horaires.ita-ito.com │ + └──────────────────────────────────────────────┘ + ▲ │ + │ OIDC ├─► api.loxi.ch (traduction FR→EN) + Navigateur admin └─► openholidaysapi.org (jours fériés CH-GE) + auth.loxi.ch +``` + +## Ce qu'il faut savoir avant tout + +**L'écran ne reçoit rien.** Il dort sur batterie, se réveille, demande quoi +afficher, et se rendort. On ne peut pas lui pousser une mise à jour : le seul +levier est la durée de son sommeil, réglable dans *Paramètres → Écran*. Une +modification apparaît donc au réveil suivant, pas immédiatement. C'est la nature +du BYOS, pas une limitation de cette application. + +**Le nom du fichier image est le hash de son contenu.** Si rien n'a changé, +l'écran reconnaît le nom, ne redessine pas, et économise sa batterie. C'est +pourquoi le rendu doit rester reproductible à l'octet près — d'où les polices +et le logo versionnés dans `public/`. + +## Prérequis + +- Docker et Docker Compose +- Node.js ≥ 20.9 (seulement pour le développement hors conteneur) +- Une application OIDC dans Authentik (voir plus bas) +- Un écran TRMNL 7,5" dont le portail captif accepte un serveur personnalisé + +## Installation locale + +```bash +cp .env.example .env +sed -i "s|^AUTH_SECRET=.*|AUTH_SECRET=$(openssl rand -base64 33)|" .env +# Renseignez ensuite POSTGRES_PASSWORD, AUTH_AUTHENTIK_*, TRANSLATION_API_KEY. + +docker compose up -d db --wait # PostgreSQL seul +npm install +npx prisma migrate deploy +npm run seed # horaires de démonstration, idempotent +npm run dev # http://localhost:3010 +``` + +Le port 3010 n'est pas un caprice : 3000 est déjà pris par une autre pile sur la +machine de développement. + +Pour faire tourner l'application elle aussi en conteneur : + +```bash +docker compose up -d --build --wait +``` + +### Commandes utiles + +| Commande | Effet | +|---|---| +| `npm run dev` | serveur de développement sur 3010 | +| `npm test` | toute la suite (unitaire + intégration) | +| `npm run coverage` | idem, avec les seuils de couverture | +| `npm run lint` / `npm run typecheck` | ESLint / TypeScript strict | +| `npm run screen:preview` | rend l'écran dans `.preview/`, sans appareil | +| `npm run brand` | régénère le logo depuis le CDN de la boutique | +| `npm run holidays:sync` | synchronise les jours fériés maintenant | +| `npm run translate:test "texte"` | vérifie le service de traduction | + +Les tests d'intégration ont besoin d'une base à eux — `TEST_DATABASE_URL`. Ils +la vident entièrement, donc elle ne doit jamais désigner une base qui contient +quoi que ce soit d'utile. Sans cette variable, ils se désactivent plutôt que de +détruire quelque chose. + +```bash +docker compose exec db psql -U horaires -d postgres -c "create database horaires_test owner horaires;" +DATABASE_URL="$TEST_DATABASE_URL" npx prisma migrate deploy +``` + +## Configuration d'Authentik + +1. **Groupe** — créez `horaires-admins` et ajoutez-y les personnes qui peuvent + modifier les horaires. **Ne liez pas ce groupe à l'application** : seuls les + administrateurs pourraient alors se connecter, et le rôle lecture seule + deviendrait inatteignable. Pour restreindre l'accès, créez un groupe plus + large et liez celui-là. + +2. **Provider** — *OAuth2/OpenID Provider* : + + | Champ | Valeur | + |---|---| + | Client type | Confidential | + | Signing Key | n'importe quel certificat — **obligatoire** | + | Redirect URIs (Strict) | `https://horaires.ita-ito.com/api/auth/callback/authentik`
`http://localhost:3010/api/auth/callback/authentik` | + | Scopes | `openid`, `email`, `profile` (les valeurs par défaut) | + | Subject mode | Based on the User's Email | + | Include claims in id_token | activé | + + Le claim `groups` arrive via le scope `profile` : aucun mapping à créer. Rien + à activer pour le PKCE, Authentik annonce `S256` et Auth.js l'utilise seul. + +3. **Application** — slug `horaires-ita-ito`, liée au provider. Le slug + détermine l'issuer, qui doit correspondre **au caractère près, barre oblique + finale comprise**. + +4. **Vérification**, avant même de lancer l'application : + + ```bash + curl -s https://auth.loxi.ch/application/o/horaires-ita-ito/.well-known/openid-configuration | jq .issuer + ``` + + La valeur doit être identique à `AUTH_AUTHENTIK_ISSUER`. C'est la première + cause d'échec de connexion, et elle se diagnostique mal depuis l'application. + +**Le rôle est fixé à la connexion.** Retirer quelqu'un du groupe ne coupe pas sa +session en cours : l'effet arrive à la reconnexion, ou au bout de huit heures. +Si un compte apparaît en lecture seule alors qu'il ne devrait pas, le tableau de +bord affiche les groupes réellement reçus et celui qui était attendu. + +## Appairage de l'écran + +Aucun reflashage. Le firmware d'origine suffit. + +1. Maintenez le bouton au dos de l'écran **5 secondes** : il ouvre un réseau + Wi-Fi nommé `TRMNL`. +2. Connectez-vous-y depuis un téléphone ; le portail s'ouvre tout seul. +3. Donnez le Wi-Fi de la boutique, puis **Advanced → Custom Server → Yes**. +4. Saisissez `https://horaires.ita-ito.com` — **sans barre oblique finale**. + L'URL exacte est rappelée dans *Paramètres → Appareils*. +5. Validez. L'écran s'enregistre seul au premier appel et affiche les horaires. + +Il apparaît ensuite dans *Paramètres → Appareils* avec son identifiant, sa +dernière visite, son firmware et sa batterie. + +> **Si *Custom Server* n'apparaît pas** dans le portail, le firmware livré avec +> le kit est une version qui ne l'expose pas. Il faut alors flasher le firmware +> TRMNL officiel — ce qui sort du cadre de ce dépôt. Vérifiez ce point avant de +> déployer quoi que ce soit. + +## Mise en production + +Voir **[DEPLOY.md](DEPLOY.md)** : première installation, mises à jour, +sauvegardes, restauration et retour arrière. + +## Dépannage + +| Symptôme | Cause la plus probable | +|---|---| +| Connexion refusée en boucle | `AUTH_AUTHENTIK_ISSUER` ne correspond pas exactement à l'issuer annoncé | +| « Compte en lecture seule » | le compte n'est pas dans `AUTHENTIK_ADMIN_GROUP` ; le tableau de bord affiche les groupes reçus | +| L'écran reste blanc | il n'a pas encore atteint le serveur : vérifiez l'URL du portail captif, sans barre oblique finale | +| L'écran n'a pas suivi une modification | normal jusqu'à son prochain réveil ; le tableau de bord indique quand il est attendu | +| « La traduction a échoué » | `TRANSLATION_API_KEY` absente ou révoquée — `npm run translate:test` le dit précisément | +| Jours fériés absents | lancez `npm run holidays:sync` ; la page Fériés affiche la dernière synchro réussie | +| Aperçu d'écran en erreur | le rendu a besoin de `public/fonts` et `public/brand` ; ils sont versionnés, vérifiez qu'ils sont présents | + +## Licence + +Propriétaire. Voir [LICENSE](LICENSE). diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..ba983ff --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,84 @@ +# Production overlay: the application is published by an existing Traefik +# rather than on a host port. +# +# docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build --wait +# +# Traefik must already be running and own the external network named by +# TRAEFIK_NETWORK. Nothing here holds a secret: every value comes from .env, +# which is never committed. +# +# Using Nginx Proxy Manager instead? Delete the labels and the `edge` network, +# keep the published port from docker-compose.yml bound to 127.0.0.1, and point +# a proxy host at it. The forwarded headers matter either way: the application +# builds the image URL handed to the panel from them, so X-Forwarded-Proto and +# X-Forwarded-Host must both reach it or the device will be sent to the wrong +# scheme. + +services: + db: + # The database is reached only over the compose network. + ports: !override [] + + app: + ports: !override [] + networks: + - default + - edge + # The database is the only thing worth persisting; the application writes + # nothing to its own filesystem. + read_only: true + tmpfs: + - /tmp + labels: + traefik.enable: "true" + traefik.docker.network: ${TRAEFIK_NETWORK:-web} + traefik.http.routers.horaires.rule: Host(`${APP_DOMAIN:?Set APP_DOMAIN in .env}`) + traefik.http.routers.horaires.entrypoints: ${TRAEFIK_ENTRYPOINT:-websecure} + traefik.http.routers.horaires.tls: "true" + traefik.http.routers.horaires.tls.certresolver: ${TRAEFIK_CERTRESOLVER:-myresolver} + traefik.http.routers.horaires.middlewares: horaires-hsts + traefik.http.services.horaires.loadbalancer.server.port: "3010" + # The admin is only ever served over TLS; say so to the browsers. + # Note the panel is NOT a browser: if its firmware trips over the + # certificate chain, see DEPLOY.md before reaching for DEVICE_ALLOW_HTTP. + traefik.http.middlewares.horaires-hsts.headers.stsSeconds: "31536000" + traefik.http.middlewares.horaires-hsts.headers.stsIncludeSubdomains: "true" + + backup: + # A nightly dump kept for two weeks. Small, boring, and the only thing + # standing between a bad migration and retyping a year of opening hours. + image: postgres:16-alpine + restart: unless-stopped + security_opt: + - no-new-privileges:true + depends_on: + db: + condition: service_healthy + environment: + PGPASSWORD: ${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD in .env} + POSTGRES_USER: ${POSTGRES_USER:-horaires} + POSTGRES_DB: ${POSTGRES_DB:-horaires} + BACKUP_KEEP_DAYS: ${BACKUP_KEEP_DAYS:-14} + volumes: + - ./backups:/backups + entrypoint: + - /bin/sh + - -c + - | + while true; do + stamp="$$(date +%Y%m%d-%H%M%S)" + if pg_dump -h db -U "$$POSTGRES_USER" -d "$$POSTGRES_DB" \ + | gzip > "/backups/horaires-$$stamp.sql.gz"; then + echo "[backup] /backups/horaires-$$stamp.sql.gz" + else + echo "[backup] échec du dump $$stamp" >&2 + rm -f "/backups/horaires-$$stamp.sql.gz" + fi + find /backups -name 'horaires-*.sql.gz' -mtime "+$$BACKUP_KEEP_DAYS" -delete + sleep 86400 + done + +networks: + edge: + external: true + name: ${TRAEFIK_NETWORK:-web} diff --git a/docker-compose.yml b/docker-compose.yml index f054d20..7198158 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -26,5 +26,38 @@ services: timeout: 5s retries: 20 + app: + build: + context: . + target: runner + image: ita-ito-horaires:${APP_VERSION:-dev} + <<: [*restart, *hardening] + cap_drop: [ALL] + depends_on: + db: + condition: service_healthy + environment: + # Inside the compose network the database is reachable by service name, + # whatever DATABASE_URL says for host-side tooling. + DATABASE_URL: postgresql://${POSTGRES_USER:-horaires}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB:-horaires}?schema=public + TZ: ${TZ:-Europe/Zurich} + AUTH_URL: ${AUTH_URL:?Set AUTH_URL in .env} + AUTH_SECRET: ${AUTH_SECRET:?Set AUTH_SECRET in .env} + AUTH_AUTHENTIK_ID: ${AUTH_AUTHENTIK_ID:-} + AUTH_AUTHENTIK_SECRET: ${AUTH_AUTHENTIK_SECRET:-} + AUTH_AUTHENTIK_ISSUER: ${AUTH_AUTHENTIK_ISSUER:-} + AUTHENTIK_DISPLAY_NAME: ${AUTHENTIK_DISPLAY_NAME:-Loxi} + AUTHENTIK_ADMIN_GROUP: ${AUTHENTIK_ADMIN_GROUP:-horaires-admins} + DEVICE_IMAGE_FORMAT: ${DEVICE_IMAGE_FORMAT:-bmp} + DEVICE_ALLOW_HTTP: ${DEVICE_ALLOW_HTTP:-false} + TRANSLATION_API_URL: ${TRANSLATION_API_URL:-} + TRANSLATION_API_KEY: ${TRANSLATION_API_KEY:-} + TRANSLATION_API_FLAVOR: ${TRANSLATION_API_FLAVOR:-loxi} + TRANSLATION_MODEL_NAME: ${TRANSLATION_MODEL_NAME:-haiku} + TRANSLATION_TIMEOUT_MS: ${TRANSLATION_TIMEOUT_MS:-30000} + HOLIDAYS_API_URL: ${HOLIDAYS_API_URL:-https://openholidaysapi.org} + ports: + - "${APP_BIND:-127.0.0.1}:${APP_PORT:-3010}:3010" + volumes: pgdata: diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100755 index 0000000..652ce75 --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,17 @@ +#!/bin/sh +set -e + +# Migrations run before the first request, every start. `migrate deploy` only +# applies what is pending, so restarting a healthy container is a no-op. +# +# Run from .migrator: the Prisma CLI, its dependencies, the schema and +# prisma.config.ts all live there, so every import resolves without reaching +# into the application's own node_modules. +echo "[entrypoint] application des migrations…" +if ! (cd /app/.migrator && node node_modules/prisma/build/index.js migrate deploy); then + echo "[entrypoint] échec des migrations — arrêt avant de servir une base incohérente" >&2 + exit 1 +fi + +echo "[entrypoint] démarrage de l'application sur le port ${PORT:-3010}" +exec "$@" diff --git a/lib/db.ts b/lib/db.ts index f0e2c20..8c1c3d0 100644 --- a/lib/db.ts +++ b/lib/db.ts @@ -2,19 +2,53 @@ import { PrismaPg } from '@prisma/adapter-pg'; import { PrismaClient } from '@/lib/generated/prisma/client'; -const connectionString = process.env.DATABASE_URL; +/** + * The Prisma client, created on first use rather than on import. + * + * Next imports every route module while collecting page data at build time, in + * a container that has no database and no DATABASE_URL. A module that threw on + * import would fail the build with an error pointing at whichever route + * happened to be analysed first, which says nothing useful. Creating the + * client lazily moves that failure to the first actual query, where the + * message is about the database because the problem is. + */ -if (!connectionString) { - throw new Error('DATABASE_URL is not set'); -} - -// Next.js hot-reloads modules in development; without this the dev server -// would open a new pool on every edit until Postgres refuses connections. const globalForPrisma = globalThis as unknown as { prisma?: PrismaClient }; -export const prisma = - globalForPrisma.prisma ?? new PrismaClient({ adapter: new PrismaPg({ connectionString }) }); +function client(): PrismaClient { + if (globalForPrisma.prisma) { + return globalForPrisma.prisma; + } -if (process.env.NODE_ENV !== 'production') { - globalForPrisma.prisma = prisma; + const connectionString = process.env.DATABASE_URL; + if (!connectionString) { + throw new Error( + 'DATABASE_URL n’est pas défini : impossible de joindre la base de données.', + ); + } + + const created = new PrismaClient({ adapter: new PrismaPg({ connectionString }) }); + + // Next hot-reloads modules in development; without this the dev server would + // open a new pool on every edit until Postgres refuses connections. + if (process.env.NODE_ENV !== 'production') { + globalForPrisma.prisma = created; + } + return created; } + +let singleton: PrismaClient | undefined; + +function instance(): PrismaClient { + singleton ??= client(); + return singleton; +} + +export const prisma: PrismaClient = new Proxy({} as PrismaClient, { + get(_target, property) { + const target = instance(); + const value = Reflect.get(target, property) as unknown; + // Model delegates and methods both need their original `this`. + return typeof value === 'function' ? value.bind(target) : value; + }, +}); diff --git a/package-lock.json b/package-lock.json index 32bf1ce..746b468 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "next-auth": "^5.0.0-beta.32", "node-cron": "^4.6.0", "pg": "^8.23.0", + "prisma": "^7.10.0", "react": "^19.3.0", "react-dom": "^19.3.0", "satori": "^0.33.4", @@ -41,7 +42,6 @@ "eslint-config-next": "^16.3.5", "msw": "^2.15.0", "postcss": "^8.5.28", - "prisma": "^7.10.0", "sharp": "^0.35.4", "tailwindcss": "^4.3.3", "tsx": "^4.23.15", @@ -258,7 +258,7 @@ "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, + "devOptional": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -268,7 +268,7 @@ "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, + "devOptional": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -302,7 +302,7 @@ "version": "7.29.9", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.9.tgz", "integrity": "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/types": "^7.29.8" @@ -352,7 +352,7 @@ "version": "7.29.8", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.29.7", @@ -376,14 +376,12 @@ "version": "0.4.3", "resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.4.3.tgz", "integrity": "sha512-ichuWTgtd4mOM1G4SpyGJa5trT03lWbMypDV0fUXUCXg5hiHqVAz/bZyV68NqmkLB7WcYmj1RMJVSp8HV/v/ZQ==", - "devOptional": true, "license": "Apache-2.0" }, "node_modules/@electric-sql/pglite-socket": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@electric-sql/pglite-socket/-/pglite-socket-0.1.3.tgz", "integrity": "sha512-LAciWM0M1dCL8hlsxu2venbVZcdxema0BtDfpWYVqr+Y468UADw0pFWidhKw1M8sfJ8rdLT71tjMmnirf/IZRQ==", - "devOptional": true, "license": "Apache-2.0", "bin": { "pglite-server": "dist/scripts/server.js" @@ -396,7 +394,6 @@ "version": "0.3.3", "resolved": "https://registry.npmjs.org/@electric-sql/pglite-tools/-/pglite-tools-0.3.3.tgz", "integrity": "sha512-AlzLJTRJ8+UFgK8CmxIpyIpJ0+YaFw02IiOSdYrqxwPXdSyeIShz8aa9Tq+tYFXdPwcaMp/Fc80mQZ1dkOQ/wg==", - "devOptional": true, "license": "Apache-2.0", "peerDependencies": { "@electric-sql/pglite": "0.4.3" @@ -2227,7 +2224,6 @@ "version": "7.10.0", "resolved": "https://registry.npmjs.org/@prisma/config/-/config-7.10.0.tgz", "integrity": "sha512-Rcg828gIRE3HOQ3pOATFjV5d/P0U9OIobxhd/IMxlfWjA4vru0eGwb0AIwFw0rmcLMVShohZYWPixVxkBHsxUA==", - "devOptional": true, "license": "Apache-2.0", "dependencies": { "c12": "3.3.4", @@ -2246,7 +2242,6 @@ "version": "0.24.17", "resolved": "https://registry.npmjs.org/@prisma/dev/-/dev-0.24.17.tgz", "integrity": "sha512-UvdZzmpFwknnfreh6Jije84ekkYGPYEJhXG1tFzCsCfQyzJifrOo38eZc0qajzvaC6OLUOrN9ML5XfCnEZL9DA==", - "devOptional": true, "license": "ISC", "dependencies": { "@electric-sql/pglite": "0.4.3", @@ -2270,7 +2265,6 @@ "version": "3.10.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", - "devOptional": true, "license": "MIT" }, "node_modules/@prisma/driver-adapter-utils": { @@ -2286,7 +2280,6 @@ "version": "7.10.0", "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-7.10.0.tgz", "integrity": "sha512-KNumN6NHFwybvfdYzTee9pqwx5PvknpWAaHn6L5NsbrKdl+SQrsVZs9opKs6U6SAsvB26HDt3WybRjOhgoWOYQ==", - "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -2300,14 +2293,12 @@ "version": "7.10.0-4.0edf323efd1d98336f3f0a68684b56f689b900d3", "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.10.0-4.0edf323efd1d98336f3f0a68684b56f689b900d3.tgz", "integrity": "sha512-8OJ6RuZTZ06eFUOtBwxVmv8XMmOW6HWN5F+uxUbZkGxR0Bfab1dfAdXaHPmR5mb59E+fmUo8IOzXlbLY1SClbw==", - "devOptional": true, "license": "Apache-2.0" }, "node_modules/@prisma/engines/node_modules/@prisma/get-platform": { "version": "7.10.0", "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.10.0.tgz", "integrity": "sha512-0bra1LFYi8xNw0yqV62bHJNQk4BKleOngZiqPWIQc7a3+9q6rqsnqJ15BuepP8943PFMvtmgnP52juZsyYkA6w==", - "devOptional": true, "license": "Apache-2.0", "dependencies": { "@prisma/debug": "7.10.0" @@ -2317,7 +2308,6 @@ "version": "7.10.0", "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-7.10.0.tgz", "integrity": "sha512-Zqyu8DY14t6W/xwmAxUYWCXtHrvQnSvT644EAZSsdM8NSmCS74vJJbBKdVsK3ucFpnUWkEpbO1a0CxJXrg130g==", - "devOptional": true, "license": "Apache-2.0", "dependencies": { "@prisma/debug": "7.10.0", @@ -2329,7 +2319,6 @@ "version": "7.10.0", "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.10.0.tgz", "integrity": "sha512-0bra1LFYi8xNw0yqV62bHJNQk4BKleOngZiqPWIQc7a3+9q6rqsnqJ15BuepP8943PFMvtmgnP52juZsyYkA6w==", - "devOptional": true, "license": "Apache-2.0", "dependencies": { "@prisma/debug": "7.10.0" @@ -2339,7 +2328,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.2.0.tgz", "integrity": "sha512-k1V0l0Td1732EHpAfi2eySTezyllok9dXb6UQanajkJQzPUGi3vO2z7jdkz67SypFTdmbnyGYxvEvYZdZsMAVA==", - "devOptional": true, "license": "Apache-2.0", "dependencies": { "@prisma/debug": "7.2.0" @@ -2349,21 +2337,18 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.2.0.tgz", "integrity": "sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw==", - "devOptional": true, "license": "Apache-2.0" }, "node_modules/@prisma/query-plan-executor": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@prisma/query-plan-executor/-/query-plan-executor-7.2.0.tgz", "integrity": "sha512-EOZmNzcV8uJ0mae3DhTsiHgoNCuu1J9mULQpGCh62zN3PxPTd+qI9tJvk5jOst8WHKQNwJWR3b39t0XvfBB0WQ==", - "devOptional": true, "license": "Apache-2.0" }, "node_modules/@prisma/streams-local": { "version": "0.1.11", "resolved": "https://registry.npmjs.org/@prisma/streams-local/-/streams-local-0.1.11.tgz", "integrity": "sha512-0TcebL559MByKqTJ+SsrFIEg228iw8UCVRFckzgfRSiJqczhs+MuAgWOF9lnOIV/IVqvu+KMnFTH0eDeTQMpUg==", - "devOptional": true, "license": "Apache-2.0", "dependencies": { "ajv": "^8.12.0", @@ -2380,7 +2365,6 @@ "version": "8.20.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", - "devOptional": true, "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", @@ -2397,14 +2381,12 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "devOptional": true, "license": "MIT" }, "node_modules/@prisma/studio-core": { "version": "0.33.0", "resolved": "https://registry.npmjs.org/@prisma/studio-core/-/studio-core-0.33.0.tgz", "integrity": "sha512-V2fX/nKEymNTrHXwfP26PGjoLStO35Ogu+ex7CFJbLrMYEcZxxZpiSNOs7px23Hk5mzLWvM5RsqG6Ka+rha+wg==", - "devOptional": true, "license": "Apache-2.0", "dependencies": { "@radix-ui/react-toggle": "1.1.10", @@ -2433,14 +2415,12 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", - "devOptional": true, "license": "MIT" }, "node_modules/@radix-ui/react-compose-refs": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", - "devOptional": true, "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -2456,7 +2436,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "devOptional": true, "license": "MIT", "dependencies": { "@radix-ui/react-slot": "1.2.3" @@ -2480,7 +2459,6 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "devOptional": true, "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" @@ -2499,7 +2477,6 @@ "version": "1.1.10", "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz", "integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==", - "devOptional": true, "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.3", @@ -2525,7 +2502,6 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", - "devOptional": true, "license": "MIT", "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", @@ -2545,7 +2521,6 @@ "version": "0.0.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", - "devOptional": true, "license": "MIT", "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" @@ -2564,7 +2539,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", - "devOptional": true, "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -3080,7 +3054,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "devOptional": true, "license": "MIT" }, "node_modules/@swc/helpers": { @@ -3389,35 +3362,30 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.0.3.tgz", "integrity": "sha512-Reoy+pKnvsksN0lQUlcH6dOGjRZ/3WRwXR//m+/8lt1BXeI4xyaUZoqULNjyXXRuh0Mj4LNpkCvhUpQlY3X5xQ==", - "devOptional": true, "license": "MIT" }, "node_modules/@types/d3-color": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.0.tgz", "integrity": "sha512-HKuicPHJuvPgCD+np6Se9MQvS6OCbJmOjGvylzMJRlDwUXjKTTXs6Pwgk79O09Vj/ho3u1ofXnhFOaEWWPrlwA==", - "devOptional": true, "license": "MIT" }, "node_modules/@types/d3-delaunay": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.1.tgz", "integrity": "sha512-tLxQ2sfT0p6sxdG75c6f/ekqxjyYR0+LwPrsO1mbC9YDBzPJhs2HbJJRrn8Ez1DBoHRo2yx7YEATI+8V1nGMnQ==", - "devOptional": true, "license": "MIT" }, "node_modules/@types/d3-format": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.1.tgz", "integrity": "sha512-5KY70ifCCzorkLuIkDe0Z9YTf9RR2CjBX1iaJG+rgM/cPP+sO+q9YdQ9WdhQcgPj1EQiJ2/0+yUkkziTG6Lubg==", - "devOptional": true, "license": "MIT" }, "node_modules/@types/d3-geo": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", - "devOptional": true, "license": "MIT", "dependencies": { "@types/geojson": "*" @@ -3427,7 +3395,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.1.tgz", "integrity": "sha512-jx5leotSeac3jr0RePOH1KdR9rISG91QIE4Q2PYTu4OymLTZfA3SrnURSLzKH48HmXVUru50b8nje4E79oQSQw==", - "devOptional": true, "license": "MIT", "dependencies": { "@types/d3-color": "*" @@ -3437,14 +3404,12 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", - "devOptional": true, "license": "MIT" }, "node_modules/@types/d3-scale": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.2.tgz", "integrity": "sha512-Yk4htunhPAwN0XGlIwArRomOjdoBFXC3+kCxK2Ubg7I9shQlVSJy/pG/Ht5ASN+gdMIalpk8TJ5xV74jFsetLA==", - "devOptional": true, "license": "MIT", "dependencies": { "@types/d3-time": "*" @@ -3454,7 +3419,6 @@ "version": "3.1.7", "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", - "devOptional": true, "license": "MIT", "dependencies": { "@types/d3-path": "*" @@ -3464,14 +3428,12 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.0.tgz", "integrity": "sha512-sZLCdHvBUcNby1cB6Fd3ZBrABbjz3v1Vm90nysCQ6Vt7vd6e/h9Lt7SiJUoEX0l4Dzc7P5llKyhqSi1ycSf1Hg==", - "devOptional": true, "license": "MIT" }, "node_modules/@types/d3-time-format": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-2.1.0.tgz", "integrity": "sha512-/myT3I7EwlukNOX2xVdMzb8FRgNzRMpsZddwst9Ld/VFe6LyJyRp0s32l/V9XoUzk+Gqu56F/oGk6507+8BxrA==", - "devOptional": true, "license": "MIT" }, "node_modules/@types/deep-eql": { @@ -3492,7 +3454,6 @@ "version": "7946.0.16", "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", - "devOptional": true, "license": "MIT" }, "node_modules/@types/json-schema": { @@ -3513,7 +3474,6 @@ "version": "4.17.25", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.25.tgz", "integrity": "sha512-+K1NIO8I+F9/wNulfVvu23QYd0Pe9/OCqRrim4NoYIf1VoEDL90Ve4ClzpyqBLc7NpGGWRvYNCKZ1BE/Jpf8dQ==", - "devOptional": true, "license": "MIT" }, "node_modules/@types/node": { @@ -3547,7 +3507,6 @@ "version": "19.3.0", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz", "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==", - "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -3557,7 +3516,7 @@ "version": "19.3.0", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.3.0.tgz", "integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==", - "dev": true, + "devOptional": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.3.0" @@ -4138,7 +4097,6 @@ "version": "4.0.1-alpha.0", "resolved": "https://registry.npmjs.org/@visx/curve/-/curve-4.0.1-alpha.0.tgz", "integrity": "sha512-jRu61Uz274pV1zyioXmboyrLutYbnKsgjj4njSGCnhdXj5GkZvZbg+ThDb6oOzoAnJOBRLz4rzPlWvNJOzuVMg==", - "devOptional": true, "license": "MIT", "dependencies": { "@visx/vendor": "4.0.0-alpha.0" @@ -4148,7 +4106,6 @@ "version": "4.0.1-alpha.0", "resolved": "https://registry.npmjs.org/@visx/event/-/event-4.0.1-alpha.0.tgz", "integrity": "sha512-EQqCMSv/s8NbFjo+hz3FKsvvYfP+2QslsFJ/24/O5l/W+7UC6J6aAvO0ujVwrTwdYbuQ+vhxKi1xdPdKR/qj1g==", - "devOptional": true, "license": "MIT", "dependencies": { "@types/react": "*", @@ -4159,7 +4116,6 @@ "version": "4.0.1-alpha.0", "resolved": "https://registry.npmjs.org/@visx/grid/-/grid-4.0.1-alpha.0.tgz", "integrity": "sha512-rycutGmTHO+znNdPumheWMglm7YfpffvRwUkVy5zy4WoORIuKTMkDxwnOzHG2xMxU3EE/YCd37xFV5AxA30yeg==", - "devOptional": true, "license": "MIT", "dependencies": { "@types/react": "*", @@ -4178,7 +4134,6 @@ "version": "4.0.1-alpha.0", "resolved": "https://registry.npmjs.org/@visx/group/-/group-4.0.1-alpha.0.tgz", "integrity": "sha512-V19l7iQ7jccBv8kao/EByuI6o4xtxzzLV9nqVI1hRvmdzTVsuLpqlwzYCZUXJaTVvUWf8s4D2SQFjGkj/Nw+0w==", - "devOptional": true, "license": "MIT", "dependencies": { "@types/react": "*", @@ -4192,14 +4147,12 @@ "version": "4.0.1-alpha.0", "resolved": "https://registry.npmjs.org/@visx/point/-/point-4.0.1-alpha.0.tgz", "integrity": "sha512-ijTfr/Nx09f03vIj9nyTr3z4Xth4Y75427UaogJh6dnIRLMEFHQOwNu791sbfiNj0a+ZXuaE32h0vKrFe4/8Qg==", - "devOptional": true, "license": "MIT" }, "node_modules/@visx/responsive": { "version": "4.0.1-alpha.0", "resolved": "https://registry.npmjs.org/@visx/responsive/-/responsive-4.0.1-alpha.0.tgz", "integrity": "sha512-o+1zGywQZY0+yOx3Iw87wc4bbPJRr/HnIukTwfOz4UVyj9pB1OQNVHB7OORO1+LBHJceWpB31co/ZV9KHncKrA==", - "devOptional": true, "license": "MIT", "dependencies": { "@types/lodash": "^4.17.13", @@ -4214,7 +4167,6 @@ "version": "4.0.1-alpha.0", "resolved": "https://registry.npmjs.org/@visx/scale/-/scale-4.0.1-alpha.0.tgz", "integrity": "sha512-nzjeE87vFSAXGWFiiNfBpNLAf0Q8Qmf6syvKLjqNi4kGZkdhbUll3E/59YsgWXmjM8+llPLWzGsP+JPvo5eq1A==", - "devOptional": true, "license": "MIT", "dependencies": { "@visx/vendor": "4.0.0-alpha.0" @@ -4224,7 +4176,6 @@ "version": "4.0.1-alpha.0", "resolved": "https://registry.npmjs.org/@visx/shape/-/shape-4.0.1-alpha.0.tgz", "integrity": "sha512-62QeiVNmPlterQGwhkEDcbq7M0MqY0lBsK5QKXtM9ZoPZWkuGV3aykA3+Xu20B2FAvyJq4LqJzBc7Sxr+EAdbA==", - "devOptional": true, "license": "MIT", "dependencies": { "@types/lodash": "^4.17.13", @@ -4244,7 +4195,6 @@ "version": "4.0.0-alpha.0", "resolved": "https://registry.npmjs.org/@visx/vendor/-/vendor-4.0.0-alpha.0.tgz", "integrity": "sha512-6I+MuqXBcv9jnlcVowHoHKSdk9gXTWkHLKyqBwRWg7LY6A3Ei8SHfubpqGV5rBUSppxMq2RszPJUS6w+H0YgmQ==", - "devOptional": true, "license": "MIT and ISC", "dependencies": { "@types/d3-array": "3.0.3", @@ -4276,7 +4226,6 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.1.tgz", "integrity": "sha512-gUY/qeHq/yNqqoCKNq4vtpFLdoCdvyNpWoC/KNjhGbhDuQpAM9sIQQKkXSNpXa9h5KySs/gzm7R88WkUutgwWQ==", - "devOptional": true, "license": "ISC", "dependencies": { "internmap": "1 - 2" @@ -4731,7 +4680,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", - "devOptional": true, "license": "MIT", "engines": { "node": ">= 6.0.0" @@ -4792,7 +4740,6 @@ "version": "2.10.0", "resolved": "https://registry.npmjs.org/better-result/-/better-result-2.10.0.tgz", "integrity": "sha512-oQhh0y1qo2/ZKdAAEvHZAqKKiHOFU5k/bW96fE2ScgQOVkJRiHwB+nOS1SgFsYqRlxMDWvefXi9Q3px7QvgNDw==", - "devOptional": true, "license": "MIT" }, "node_modules/brace-expansion": { @@ -4859,7 +4806,6 @@ "version": "3.3.4", "resolved": "https://registry.npmjs.org/c12/-/c12-3.3.4.tgz", "integrity": "sha512-cM0ApFQSBXuourJejzwv/AuPRvAxordTyParRVcHjjtXirtkzM0uK2L9TTn9s0cXZbG7E55jCivRQzoxYmRAlA==", - "devOptional": true, "license": "MIT", "dependencies": { "chokidar": "^5.0.0", @@ -4888,7 +4834,6 @@ "version": "17.4.2", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", - "devOptional": true, "license": "BSD-2-Clause", "engines": { "node": ">=12" @@ -5017,7 +4962,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", - "devOptional": true, "license": "MIT", "dependencies": { "readdirp": "^5.0.0" @@ -5033,7 +4977,6 @@ "version": "2.5.1", "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", - "devOptional": true, "license": "MIT" }, "node_modules/cli-width": { @@ -5097,7 +5040,6 @@ "version": "0.2.4", "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", - "devOptional": true, "license": "MIT" }, "node_modules/convert-source-map": { @@ -5125,7 +5067,6 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "devOptional": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -5181,14 +5122,12 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "devOptional": true, "license": "MIT" }, "node_modules/d3-array": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", - "devOptional": true, "license": "ISC", "dependencies": { "internmap": "1 - 2" @@ -5201,7 +5140,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", - "devOptional": true, "license": "ISC", "engines": { "node": ">=12" @@ -5211,7 +5149,6 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.2.tgz", "integrity": "sha512-IMLNldruDQScrcfT+MWnazhHbDJhcRJyOEBAJfwQnHle1RPh6WDuLvxNArUju2VSMSUuKlY5BGHRJ2cYyoFLQQ==", - "devOptional": true, "license": "ISC", "dependencies": { "delaunator": "5" @@ -5224,7 +5161,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", - "devOptional": true, "license": "ISC", "engines": { "node": ">=12" @@ -5234,7 +5170,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.0.tgz", "integrity": "sha512-JEo5HxXDdDYXCaWdwLRt79y7giK8SbhZJbFWXqbRTolCHFI5jRqteLzCsq51NKbUoX0PjBVSohxrx+NoOUujYA==", - "devOptional": true, "license": "ISC", "dependencies": { "d3-array": "2.5.0 - 3" @@ -5247,7 +5182,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", - "devOptional": true, "license": "ISC", "dependencies": { "d3-color": "1 - 3" @@ -5260,7 +5194,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", - "devOptional": true, "license": "ISC", "engines": { "node": ">=12" @@ -5270,7 +5203,6 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", - "devOptional": true, "license": "ISC", "dependencies": { "d3-array": "2.10.0 - 3", @@ -5287,7 +5219,6 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", - "devOptional": true, "license": "ISC", "dependencies": { "d3-path": "^3.1.0" @@ -5300,7 +5231,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", - "devOptional": true, "license": "ISC", "dependencies": { "d3-array": "2 - 3" @@ -5313,7 +5243,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", - "devOptional": true, "license": "ISC", "dependencies": { "d3-time": "1 - 3" @@ -5431,7 +5360,6 @@ "version": "7.1.5", "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", - "devOptional": true, "license": "BSD-3-Clause", "engines": { "node": ">=16.0.0" @@ -5477,14 +5405,12 @@ "version": "6.1.7", "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", - "devOptional": true, "license": "MIT" }, "node_modules/delaunator": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz", "integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==", - "devOptional": true, "license": "ISC", "dependencies": { "robust-predicates": "^3.0.2" @@ -5494,7 +5420,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", - "devOptional": true, "license": "Apache-2.0", "engines": { "node": ">=0.10" @@ -5504,7 +5429,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", - "devOptional": true, "license": "MIT" }, "node_modules/detect-libc": { @@ -5564,7 +5488,6 @@ "version": "3.20.0", "resolved": "https://registry.npmjs.org/effect/-/effect-3.20.0.tgz", "integrity": "sha512-qMLfDJscrNG8p/aw+IkT9W7fgj50Z4wG5bLBy0Txsxz8iUHjDIkOgO3SV0WZfnQbNG2VJYb0b+rDLMrhM4+Krw==", - "devOptional": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.0.0", @@ -5582,7 +5505,6 @@ "version": "0.11.1", "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.11.1.tgz", "integrity": "sha512-zxxR9k+rx5ktMwT/FwyLdPCrq7xN6e4VGGHH8hA01vVYKjTFik7nHOxBnAYtrgYUB1RpAiLvA1/U2YraWxyKKg==", - "devOptional": true, "license": "EPL-2.0" }, "node_modules/emoji-regex": { @@ -5605,7 +5527,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==", - "devOptional": true, "license": "MIT", "engines": { "node": ">=14" @@ -5629,7 +5550,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==", - "devOptional": true, "license": "MIT", "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" @@ -6438,14 +6358,12 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.1.1.tgz", "integrity": "sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g==", - "devOptional": true, "license": "MIT" }, "node_modules/fast-check": { "version": "3.23.2", "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==", - "devOptional": true, "funding": [ { "type": "individual", @@ -6468,14 +6386,12 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==", - "devOptional": true, "license": "MIT" }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "devOptional": true, "license": "MIT" }, "node_modules/fast-glob": { @@ -6526,7 +6442,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", - "devOptional": true, "license": "MIT", "dependencies": { "fast-decode-uri-component": "^1.0.1" @@ -6553,7 +6468,6 @@ "version": "3.1.8", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==", - "devOptional": true, "funding": [ { "type": "github", @@ -6622,7 +6536,6 @@ "version": "9.7.0", "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-9.7.0.tgz", "integrity": "sha512-f2JHn75x2JlwUwLenZypgczR7YWMb/uO9BvUXtus+JMgkbIkLADd38cI4EiV+OQqrGo1Zlq6V8wnqMJ8e62wUQ==", - "devOptional": true, "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", @@ -6691,7 +6604,6 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "devOptional": true, "license": "ISC", "dependencies": { "cross-spawn": "^7.0.6", @@ -6767,7 +6679,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", - "devOptional": true, "license": "MIT", "dependencies": { "is-property": "^1.0.2" @@ -6832,7 +6743,6 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/get-port-please/-/get-port-please-3.2.0.tgz", "integrity": "sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A==", - "devOptional": true, "license": "MIT" }, "node_modules/get-proto": { @@ -6884,7 +6794,6 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/giget/-/giget-3.3.1.tgz", "integrity": "sha512-r+mvuDjrjMpsdw46Kmeydb8bdHm7wOKw8wNBtTndkjbPjgAp5oUJUxRE76wZFknxIPokfWvep2qSXK37aXE6zg==", - "devOptional": true, "license": "MIT", "bin": { "giget": "dist/cli.mjs" @@ -6950,21 +6859,18 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "devOptional": true, "license": "ISC" }, "node_modules/grammex": { "version": "3.1.13", "resolved": "https://registry.npmjs.org/grammex/-/grammex-3.1.13.tgz", "integrity": "sha512-LnPnhOBLEJEVKS8WFDVaA397L9Kq55Q9oSITJiVLHVdhAclfUkWzQv74KhvZHKL2Q09Pb1XdsrOsZ4LfTFFTEg==", - "devOptional": true, "license": "MIT" }, "node_modules/graphmatch": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/graphmatch/-/graphmatch-1.1.1.tgz", "integrity": "sha512-5ykVn/EXM1hF0XCaWh05VbYvEiOL2lY1kBxZtaYsyvjp7cmWOU1XsAdfQBwClraEofXDT197lFbXOEVMHpvQOg==", - "devOptional": true, "license": "MIT" }, "node_modules/graphql": { @@ -7128,7 +7034,6 @@ "version": "0.7.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", - "devOptional": true, "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -7197,7 +7102,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", - "devOptional": true, "license": "ISC", "engines": { "node": ">=12" @@ -7497,7 +7401,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", - "devOptional": true, "license": "MIT" }, "node_modules/is-regex": { @@ -7656,7 +7559,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "devOptional": true, "license": "ISC" }, "node_modules/istanbul-lib-coverage": { @@ -7720,7 +7622,6 @@ "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": { "jiti": "lib/jiti-cli.mjs" @@ -8163,7 +8064,6 @@ "version": "4.18.1", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", - "devOptional": true, "license": "MIT" }, "node_modules/lodash.merge": { @@ -8177,7 +8077,6 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", - "devOptional": true, "license": "Apache-2.0" }, "node_modules/loose-envify": { @@ -8214,7 +8113,6 @@ "version": "1.1.5", "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.5.tgz", "integrity": "sha512-5J9ysMYUpYIg9RF2vJpy9SinEmSviFSe0GyPpCQ4L5QSkLAgeLXlTAOu2ZwWUU5m+0SBl6gUU1R1ZQB3aKypfA==", - "devOptional": true, "license": "MIT", "engines": { "bun": ">=1.0.0", @@ -8240,7 +8138,7 @@ "version": "0.5.5", "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.5.tgz", "integrity": "sha512-UicdXN8zQ3JHlxVq+28afMXPr1z7WNY6+7EJnzTdQWkTAlMLF5fNCCKxJHBQwGaNGR11581EiQmQzx73+MvszA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.29.7", @@ -8390,7 +8288,6 @@ "version": "3.15.3", "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.15.3.tgz", "integrity": "sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg==", - "devOptional": true, "license": "MIT", "dependencies": { "aws-ssl-profiles": "^1.1.1", @@ -8411,7 +8308,6 @@ "version": "1.1.6", "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz", "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==", - "devOptional": true, "license": "MIT", "dependencies": { "lru.min": "^1.1.0" @@ -8767,7 +8663,6 @@ "version": "2.0.12", "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.12.tgz", "integrity": "sha512-65S/5gk9YSsaRjcyf7Nfa6h/d3E8/1gslpXfI4W7Dxn/oap8IKRuNT5VXkLQ1YFKIEg4apRY4Pj6aiwFzrDdmw==", - "devOptional": true, "license": "MIT" }, "node_modules/optionator": { @@ -8889,7 +8784,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "devOptional": true, "license": "MIT", "engines": { "node": ">=8" @@ -8913,14 +8807,12 @@ "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" }, "node_modules/perfect-debounce": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz", "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==", - "devOptional": true, "license": "MIT" }, "node_modules/pg": { @@ -9044,7 +8936,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.3.tgz", "integrity": "sha512-j/lCFdcppV0JxWpCEITdbDltBxPP6cHT+yNJ6Go2OgoSA9518X847X9z0p6LtA4Nc16+eQzCZjRrWanTGvHJ5w==", - "devOptional": true, "license": "MIT", "dependencies": { "confbox": "^0.3.1", @@ -9056,7 +8947,6 @@ "version": "0.3.1", "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.3.1.tgz", "integrity": "sha512-cKUSoKa8YxFZZSmraVi7onONx3amu77ngK3kGpsYHDH7drPwCRkQE1RYMPlLRrMtnciRj274XNRxcHxnKmDSnA==", - "devOptional": true, "license": "MIT" }, "node_modules/possible-typed-array-names": { @@ -9108,7 +8998,6 @@ "version": "3.4.7", "resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.7.tgz", "integrity": "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==", - "devOptional": true, "license": "Unlicense", "engines": { "node": ">=12" @@ -9190,7 +9079,6 @@ "version": "7.10.0", "resolved": "https://registry.npmjs.org/prisma/-/prisma-7.10.0.tgz", "integrity": "sha512-o0ornyJOWgygVAzGCpr8PdXV8EJLHyVGDDUr/voBQt8Azzw8cYTByzzPGcA/m4tCkPcnJA8raEOv2CslsKhPEw==", - "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -9236,7 +9124,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", - "devOptional": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", @@ -9248,7 +9135,6 @@ "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "devOptional": true, "license": "ISC" }, "node_modules/punycode": { @@ -9265,7 +9151,6 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", - "devOptional": true, "funding": [ { "type": "individual", @@ -9303,7 +9188,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/rc9/-/rc9-3.1.0.tgz", "integrity": "sha512-ufjkNVzbRHKcCOmTahZkmVsyc3W+MSk3jY03m+a7tGHkIsdVMG9l10/3HvFbWkkKzY5VFp3pkRsIo/UYgmFL7Q==", - "devOptional": true, "license": "MIT", "dependencies": { "defu": "^6.1.7", @@ -9342,7 +9226,6 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.1.1.tgz", "integrity": "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==", - "devOptional": true, "license": "MIT", "engines": { "node": ">= 20.19.0" @@ -9400,7 +9283,6 @@ "version": "2.33.4", "resolved": "https://registry.npmjs.org/remeda/-/remeda-2.33.4.tgz", "integrity": "sha512-ygHswjlc/opg2VrtiYvUOPLjxjtdKvjGz1/plDhkG66hjNjFr1xmfrs2ClNFo/E6TyUFiwYNh53bKV26oBoMGQ==", - "devOptional": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/remeda" @@ -9420,7 +9302,6 @@ "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==", - "devOptional": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -9474,7 +9355,6 @@ "version": "0.5.0", "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", "integrity": "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==", - "devOptional": true, "license": "MIT", "engines": { "node": ">=10" @@ -9484,7 +9364,6 @@ "version": "0.12.0", "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", - "devOptional": true, "license": "MIT", "engines": { "node": ">= 4" @@ -9512,7 +9391,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz", "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==", - "devOptional": true, "license": "Unlicense" }, "node_modules/rolldown": { @@ -9632,7 +9510,6 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-5.1.1.tgz", "integrity": "sha512-mOSBvHGDZMuIEZMdOz/aCEYDCv0E7nfcNsIhUF+/P+xC7Hyf3FkvymqgPbg9D1EdSGu+uKbJgy09K/RKKc7kJA==", - "devOptional": true, "funding": [ { "type": "github", @@ -9655,7 +9532,6 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "devOptional": true, "license": "MIT" }, "node_modules/satori": { @@ -9704,8 +9580,7 @@ "node_modules/seq-queue": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", - "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==", - "devOptional": true + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" }, "node_modules/set-cookie-parser": { "version": "3.1.2", @@ -9817,7 +9692,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "devOptional": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -9830,7 +9704,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "devOptional": true, "license": "MIT", "engines": { "node": ">=8" @@ -9923,7 +9796,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "devOptional": true, "license": "ISC", "engines": { "node": ">=14" @@ -9954,7 +9826,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", - "devOptional": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -10729,7 +10600,6 @@ "version": "1.4.2", "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.4.2.tgz", "integrity": "sha512-gjdCvJ6d3RyHAneqxMYMW9QMCwYMb3jpOO0IyHZV1bnRHFBHrX3VkIILt5XYR0WhwHiH7Mty8ovuPZ/O3gamrg==", - "devOptional": true, "license": "MIT", "peerDependencies": { "typescript": ">=5" @@ -11199,7 +11069,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "devOptional": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -11423,7 +11292,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/zeptomatch/-/zeptomatch-2.1.0.tgz", "integrity": "sha512-KiGErG2J0G82LSpniV0CtIzjlJ10E04j02VOudJsPyPwNZgGnRKQy7I1R7GMyg/QswnE4l7ohSGrQbQbjXPPDA==", - "devOptional": true, "license": "MIT", "dependencies": { "grammex": "^3.1.11", diff --git a/package.json b/package.json index 02fa246..282aea0 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "next-auth": "^5.0.0-beta.32", "node-cron": "^4.6.0", "pg": "^8.23.0", + "prisma": "^7.10.0", "react": "^19.3.0", "react-dom": "^19.3.0", "satori": "^0.33.4", @@ -55,7 +56,6 @@ "eslint-config-next": "^16.3.5", "msw": "^2.15.0", "postcss": "^8.5.28", - "prisma": "^7.10.0", "sharp": "^0.35.4", "tailwindcss": "^4.3.3", "tsx": "^4.23.15", diff --git a/prisma.config.ts b/prisma.config.ts index 45702d5..5620f6d 100644 --- a/prisma.config.ts +++ b/prisma.config.ts @@ -1,6 +1,6 @@ import 'dotenv/config'; import path from 'node:path'; -import { defineConfig, env } from 'prisma/config'; +import { defineConfig } from 'prisma/config'; // Prisma 7 keeps the connection URL out of the schema file, so it lives here. export default defineConfig({ @@ -10,6 +10,10 @@ export default defineConfig({ seed: 'tsx prisma/seed.ts', }, datasource: { - url: env('DATABASE_URL'), + // Read directly rather than through prisma's env() helper, which throws + // when the variable is absent. `prisma generate` needs no database and + // runs in a build container that has none; `migrate deploy` still fails + // loudly on an empty URL, which is where the error belongs. + url: process.env.DATABASE_URL ?? '', }, }); diff --git a/scripts/backup.sh b/scripts/backup.sh new file mode 100755 index 0000000..090fe89 --- /dev/null +++ b/scripts/backup.sh @@ -0,0 +1,15 @@ +#!/bin/sh +# Dump immédiat de la base, dans ./backups. +# +# Le service `backup` en fait un par nuit; celui-ci sert avant une mise à jour, +# qui est le seul moment où l'on regrette de ne pas en avoir pris un. +set -e + +directory="${BACKUP_DIR:-./backups}" +mkdir -p "$directory" +target="$directory/horaires-$(date +%Y%m%d-%H%M%S).sql.gz" + +docker compose exec -T db pg_dump -U "${POSTGRES_USER:-horaires}" "${POSTGRES_DB:-horaires}" \ + | gzip > "$target" + +echo "$target ($(du -h "$target" | cut -f1))"