From 7e53d4d7e2b1d1f973cf1161f9c133ac287e3109 Mon Sep 17 00:00:00 2001 From: Sluijsens Date: Sat, 25 Jul 2026 13:09:38 +0200 Subject: [PATCH 01/11] Verduidelijk pad-verschil tussen deploy_path en nginx root in documentatie Co-authored-by: Junie --- aidlc-docs/features/react-frontend/audit.md | 24 +++++++ .../deployment/deployment-instructions.md | 18 ++++-- .../operations/deployment/deployment-plan.md | 8 +-- ...everse-proxy-nginx-production.conf.example | 55 ---------------- .../nginx/reverse-proxy-nginx.conf.example | 64 +++++++++++-------- .../nginx/webserver-nginx.conf.example | 23 ++++--- .../operations/deployment/rollback-plan.md | 2 +- 7 files changed, 91 insertions(+), 103 deletions(-) delete mode 100644 aidlc-docs/features/react-frontend/operations/deployment/nginx/reverse-proxy-nginx-production.conf.example diff --git a/aidlc-docs/features/react-frontend/audit.md b/aidlc-docs/features/react-frontend/audit.md index 02bb0b6..c04c7fe 100644 --- a/aidlc-docs/features/react-frontend/audit.md +++ b/aidlc-docs/features/react-frontend/audit.md @@ -432,3 +432,27 @@ **Context**: Post-workflow follow-up (feature already marked Complete); reverts the previous change and documents remaining open questions about the PR triggering issue that require checking the Gitea instance directly. Committed on `feature/updates_gitea_workflow`. --- + +## Post-Completion Follow-up: SCP Method Fixed (Docker/Podman Issue) & nginx Configs Finalized + +**Timestamp**: 2026-07-25T12:22:00Z + +**User Input**: "ik heb de deploy-workflow iets aangepast. De originele manier werkte niet met Docker/Podman. De huidige manier werkt wel. Kan je dat doorvoeren door de documentatie? Verder heb ik in `reverse-proxy-nginx.conf.example` en `webserver-nginx.conf.example` onder de comment '# Gebruikte versie' de daadwerkelijke versie staan. Dit werkt nu. Kan je de configuraties controleren en als example opnemen ipv jouw variant? Controleer even of alle headers goed genoemd zijn of dat je andere foutjes ziet. [...] De productieversie mag je van mij ook weg doen voor nu." + +**AI Response**: "Confirmed the user's own fix to `deploy.yaml` (already merged to `master` externally): the `appleboy/scp-action` Docker-container-action failed on the self-hosted Podman-based runner (`failed to attach to container: unable to upgrade to tcp, received 409`); replaced with a plain `run:` step using `sshpass`+`scp`. Updated `deployment-instructions.md`, `deployment-plan.md` and `rollback-plan.md` to describe this. Replaced both nginx example files with only the user's 'Gebruikte versie' configs (removed the earlier draft variants), found and fixed a header typo (`X-Real_IP` should be `X-Real-IP`) in `reverse-proxy-nginx.conf.example`, and flagged a path discrepancy: the pipeline uploads to `/html/test/slpsoftware` while nginx's `root` is `/mnt/storage1/www/html/test/slpsoftware` (documented as needing a symlink/mount check on the Pi). Deleted `nginx/reverse-proxy-nginx-production.conf.example` per explicit request." + +**Context**: Post-workflow follow-up (feature already marked Complete); reconciles documentation with manual workflow/nginx changes made directly by the user outside this session. Changes not yet committed/pushed. + +--- + +## Post-Completion Follow-up: Path Difference Between `deploy_path` and nginx `root` Clarified + +**Timestamp**: 2026-07-25T12:33:00Z + +**User Input**: "Ik zie dat je het pad-verschil hebt opgemerkt van wat er in de nginx config staat en wat er wordt meegegeven in de deploy-workflow. Het pad op de server zelf en wat gebruikt wordt voor de nginx configuratie is het volledige pad `/mnt/storage1/www/html/test/slpsoftware` terwijl de ftp gebruiker als root heeft `/mnt/storage1/www` dus het pad voor deployment is `/html/test/slpsoftware`. Misschien dat dat verschil wat mooier kan worden verwoord in de documentatie" + +**AI Response**: "Reworded the earlier 'path discrepancy — verify on the Pi' warning in `deployment-instructions.md` and `rollback-plan.md`: this is not a potential bug requiring verification, but expected behavior, since the SCP/SSH user's root is `/mnt/storage1/www` (comparable to an FTP chroot). `deploy_path` (`/html/test/slpsoftware`) is therefore the correct path relative to that user's root, which on the Pi's actual filesystem resolves to the full path `/mnt/storage1/www/html/test/slpsoftware` used as nginx's `root`." + +**Context**: Post-workflow follow-up (feature already marked Complete); clarifies wording of the previous follow-up's path-discrepancy note per user feedback. Changes not yet committed/pushed. + +--- diff --git a/aidlc-docs/features/react-frontend/operations/deployment/deployment-instructions.md b/aidlc-docs/features/react-frontend/operations/deployment/deployment-instructions.md index 40f5e91..fb207af 100644 --- a/aidlc-docs/features/react-frontend/operations/deployment/deployment-instructions.md +++ b/aidlc-docs/features/react-frontend/operations/deployment/deployment-instructions.md @@ -13,7 +13,10 @@ Sinds deze stap wordt er automatisch gedeployed naar een **testomgeving**: een R - `prepare` → `build` (uploadt de artifact, naam/pad uit `env.ARTIFACT_NAME`/`env.ARTIFACT_PATH`) → `test` (lint + unit tests) - Een losse `config`-job zet deze `env`-waarden om in job-outputs (zie hieronder waarom dat nodig is). - `deploy-test` (alleen bij `workflow_dispatch` of een push naar `master`) roept `deploy.yaml` aan met `artifact_name`/`environment`/`deploy_path` afkomstig van `needs.config.outputs.*` (dus indirect uit het `env:`-blok). -- `deploy.yaml` — download de artifact (naam/lokaal pad = `inputs.artifact_name`) en upload de inhoud via `appleboy/scp-action` naar de opgegeven `deploy_path` op de host uit de meegegeven secrets. +- `deploy.yaml` — download de artifact (naam/lokaal pad = `inputs.artifact_name`) en upload de inhoud via een `scp`-commando (met `sshpass` voor het wachtwoord) in een gewone shell-stap naar de opgegeven `deploy_path` op de host uit de meegegeven secrets. + +### Waarom `sshpass`/`scp` in een shell-stap in plaats van de `appleboy/scp-action` Docker-action? +De oorspronkelijke aanpak gebruikte de `appleboy/scp-action` (een Docker-container-action). Dit werkte niet op deze zelf-gehoste Gitea-runner: de stap faalde met `failed to attach to container: unable to upgrade to tcp, received 409`, een bekende beperking van Podman's Docker-compatibele API, die het attach/log-streaming-mechanisme voor container-based actions niet volledig ondersteunt. De huidige aanpak (een normale `run:`-stap die `sshpass` installeert en zelf `scp` aanroept) heeft geen geneste container nodig en werkt daardoor wel. ### Waarom een aparte `config`-job in plaats van rechtstreeks het `env:`-blok? Gitea/GitHub Actions ondersteunt geen `env`-context in de `with:`-sectie waarmee een reusable workflow wordt aangeroepen (`jobs..with`) — dat werkt alléén binnen `jobs..steps`. De oplossing is een klein voorloop-job (`config`) dat de gewenste `env`-waarden via `$GITHUB_OUTPUT` naar job-outputs schrijft; die outputs (`needs.config.outputs.*`) zijn wél bruikbaar in `jobs..with`. Zo hoef je, om de artifact-naam/pad of de testdeploy-bestemming te wijzigen, alléén het `env:`-blok bovenaan `continuous_integration.yaml` aan te passen — niet de `deploy-test`-job zelf. @@ -35,11 +38,12 @@ Deze secrets heten `PI_MAIN_*` (niet `PI_TEST_*`), omdat dezelfde Pi (Pi Main) e - **Productie** (nog niet automatisch gedeployed, maar domein al bekend): `slpsoftware.nl` (en `www.slpsoftware.nl`) → zelfde reverse-proxy-Pi, zodra productie wordt opgezet. ## Eenmalige Setup — nginx & SSL op de Raspberry Pi's -1. Kopieer `operations/deployment/nginx/webserver-nginx.conf.example` naar `/etc/nginx/sites-available/` op de webserver-Pi, maak een symlink in `sites-enabled/`, en herlaad nginx. -2. Kopieer `operations/deployment/nginx/reverse-proxy-nginx.conf.example` naar `/etc/nginx/sites-available/slpsoftware-test.conf` op de reverse-proxy-Pi, maak een symlink in `sites-enabled/`, en herlaad nginx. Dit bestand gebruikt al `test.slpsoftware.nl` als `server_name`. +1. Kopieer `operations/deployment/nginx/webserver-nginx.conf.example` naar `/etc/nginx/sites-available/` op de webserver-Pi, maak een symlink in `sites-enabled/`, en herlaad nginx. Dit bestand is de daadwerkelijk in gebruik zijnde configuratie (`server_name test.slpsoftware.nl`, luistert op poort 80, serveert vanaf `/mnt/storage1/www/html/test/slpsoftware`). +2. Kopieer `operations/deployment/nginx/reverse-proxy-nginx.conf.example` naar `/etc/nginx/sites-available/slpsoftware-test.conf` op de reverse-proxy-Pi, maak een symlink in `sites-enabled/`, en herlaad nginx. Ook dit bestand is de daadwerkelijk in gebruik zijnde (door certbot beheerde) configuratie, met `server_name test.slpsoftware.nl`. 3. Vraag op de reverse-proxy-Pi een SSL-certificaat aan met certbot (Let's Encrypt), nadat het DNS-record klopt: `sudo certbot --nginx -d test.slpsoftware.nl`. Certbot regelt automatisch de HTTPS-configuratie en de HTTP→HTTPS-redirect (net zoals je gewend bent van certbot). -4. Zorg dat de map `/html/test/slpsoftware` bestaat op de webserver-Pi en schrijfbaar is voor de gebruiker `webadmin` (bijv. `sudo mkdir -p /html/test/slpsoftware && sudo chown webadmin:webadmin /html/test/slpsoftware`). -5. Voor later, wanneer productie wordt opgezet: zie `operations/deployment/nginx/reverse-proxy-nginx-production.conf.example` (domein `slpsoftware.nl`, certbot-commando alvast gedocumenteerd). +4. Zorg dat de map `/mnt/storage1/www/html/test/slpsoftware` bestaat op de webserver-Pi en schrijfbaar is voor de gebruiker `webadmin` (bijv. `sudo mkdir -p /mnt/storage1/www/html/test/slpsoftware && sudo chown webadmin:webadmin /mnt/storage1/www/html/test/slpsoftware`). + +> **Waarom `deploy_path` en de nginx `root` niet hetzelfde pad zijn**: de pipeline uploadt via SCP naar `deploy_path` = `/html/test/slpsoftware` (zie `env.DEPLOY_PATH` in `continuous_integration.yaml`), terwijl de nginx `root` in `webserver-nginx.conf.example` het volledige pad `/mnt/storage1/www/html/test/slpsoftware` is. Dit is geen fout of inconsistentie: de SSH/SCP-gebruiker (`webadmin`) heeft `/mnt/storage1/www` als root (vergelijkbaar met een FTP-chroot), dus vanuit het perspectief van deze gebruiker is `/html/test/slpsoftware` het juiste (relatieve) pad, terwijl dat op het bestandssysteem van de Pi zelf overeenkomt met het volledige pad `/mnt/storage1/www/html/test/slpsoftware` dat nginx als `root` gebruikt. Kortom: `deploy_path` (`/html/test/slpsoftware`) + de root van de `webadmin`-gebruiker (`/mnt/storage1/www`) = de nginx `root` (`/mnt/storage1/www/html/test/slpsoftware`). ## How to Deploy to Test ### Automatisch @@ -55,5 +59,5 @@ Merge een pull request naar `master` — de `deploy-test` job draait dan automat 2. Open de testomgeving in de browser (via het adres/IP dat je bij de reverse-proxy hebt ingesteld) en controleer dat de site correct laadt (check de browserconsole op fouten, zoals in de handmatige smoke test in `construction/build-and-test/integration-test-instructions.md`). ## Future Work -- **Van wachtwoord naar SSH-key**: vervang `password: ${{ secrets.PI_MAIN_PASSWORD }}` in `deploy.yaml` door `key: ${{ secrets.PI_MAIN_SSH_KEY }}` (een nieuwe secret met de private key-inhoud), en zet de bijbehorende public key in `~/.ssh/authorized_keys` van de `webadmin`-gebruiker op de webserver-Pi. Verwijder daarna het wachtwoord-secret. -- **Productie-omgeving**: voeg een `deploy-production`-job toe zodra de definitieve productiehosting bekend is (zie `deployment-plan.md`'s "Open Item"), en pas `nginx/reverse-proxy-nginx-production.conf.example` (domein `slpsoftware.nl`) toe zodra de webserver-locatie voor productie vastligt. +- **Van wachtwoord naar SSH-key**: vervang `sshpass -p "${{ secrets.PI_MAIN_PASSWORD }}" scp ...` in `deploy.yaml` door een `scp`-commando met `-i ` (een nieuwe secret `PI_MAIN_SSH_KEY` die je eerst als bestand wegschrijft in de run-stap), en zet de bijbehorende public key in `~/.ssh/authorized_keys` van de `webadmin`-gebruiker op de webserver-Pi. Verwijder daarna het wachtwoord-secret. +- **Productie-omgeving**: voeg een `deploy-production`-job toe zodra de definitieve productiehosting bekend is (zie `deployment-plan.md`'s "Open Item"). De productie-nginx-voorbeeldconfiguratie is op verzoek van de gebruiker verwijderd totdat er een goed-werkende versie is; die kan later opnieuw opgebouwd worden naar analogie van `nginx/webserver-nginx.conf.example` en `nginx/reverse-proxy-nginx.conf.example`, met domein `slpsoftware.nl`. diff --git a/aidlc-docs/features/react-frontend/operations/deployment/deployment-plan.md b/aidlc-docs/features/react-frontend/operations/deployment/deployment-plan.md index 38938b8..9bd188f 100644 --- a/aidlc-docs/features/react-frontend/operations/deployment/deployment-plan.md +++ b/aidlc-docs/features/react-frontend/operations/deployment/deployment-plan.md @@ -11,13 +11,13 @@ Sinds deze stap is er een echte, geautomatiseerde upload naar een **testomgeving 1. Bij elke pull request draait automatisch de build/test/lint-gate (`prepare` → `build` → `test`), zodat merge requests direct gevalideerd worden. 2. Zodra een pull request naar `master` gemerged wordt (of de workflow handmatig via `workflow_dispatch` gestart wordt), draait aanvullend de `deploy-test` job. 3. `deploy-test` roept de herbruikbare `deploy.yaml` workflow aan met `artifact_name`/`environment`/`deploy_path`, en geeft via `secrets: inherit` de Pi-inloggegevens door. Deze drie waarden (samen met de artifact-naam/pad die de `build`-job gebruikt) staan als variabelen in het `env:`-blok bovenaan `continuous_integration.yaml` (`ARTIFACT_NAME`, `ARTIFACT_PATH`, `DEPLOY_ENVIRONMENT`, `DEPLOY_PATH`), en worden via een kleine `config`-job als job-outputs doorgegeven aan `deploy-test` (nodig omdat de `env`-context zelf niet werkt in de `with:`-sectie van een reusable-workflow-aanroep). -4. `deploy.yaml` downloadt de artifact en uploadt de inhoud via SCP (wachtwoord-login) naar de webserver-Pi op het interne netwerk (`192.168.1.103`, poort `2224`). -5. nginx op de webserver-Pi serveert de bestanden vanaf `/html/test/slpsoftware`; de reverse-proxy-Pi stuurt binnenkomend verkeer door naar deze webserver-Pi. Voorbeeldconfiguraties staan in `operations/deployment/nginx/`. +4. `deploy.yaml` downloadt de artifact en uploadt de inhoud via een `scp`-commando (met `sshpass` voor het wachtwoord) in een gewone shell-stap naar de webserver-Pi op het interne netwerk (`192.168.1.103`, poort `2224`). Dit vervangt de eerdere `appleboy/scp-action` (Docker-container-action), die faalde op de zelf-gehoste Podman-runner (`failed to attach to container: unable to upgrade to tcp, received 409`). +5. nginx op de webserver-Pi serveert de bestanden vanaf `/mnt/storage1/www/html/test/slpsoftware`; de reverse-proxy-Pi stuurt binnenkomend verkeer door naar deze webserver-Pi. Voorbeeldconfiguraties staan in `operations/deployment/nginx/` en zijn de daadwerkelijk in gebruik zijnde configuraties (niet langer illustratieve concepten). 6. De reverse-proxy-Pi is ook verantwoordelijk voor SSL: certificaten worden net als voorheen aangevraagd via certbot (Let's Encrypt) en HTTP-verkeer wordt doorverwezen naar HTTPS. ## Environments - **Test** (nieuw, geautomatiseerd): zoals hierboven beschreven — de enige omgeving die op dit moment daadwerkelijk automatisch gedeployed wordt. Domeinnaam: `test.slpsoftware.nl` (SSL via certbot op de reverse-proxy-Pi). -- **Productie**: nog niet geautomatiseerd. Zodra de definitieve productiehosting bekend is, kan een vergelijkbare `deploy-production`-job worden toegevoegd die `deploy.yaml` aanroept met `environment: production` en de productie-secrets/pad. Domeinnaam ligt al vast: `slpsoftware.nl` (SSL eveneens via certbot; zie `nginx/reverse-proxy-nginx-production.conf.example`). +- **Productie**: nog niet geautomatiseerd. Zodra de definitieve productiehosting bekend is, kan een vergelijkbare `deploy-production`-job worden toegevoegd die `deploy.yaml` aanroept met `environment: production` en de productie-secrets/pad. Domeinnaam ligt al vast: `slpsoftware.nl` (SSL eveneens via certbot). Er is (nog) geen productie-nginx-voorbeeldconfiguratie; deze is op verzoek verwijderd totdat er een goed-werkende, foutloze versie is, en kan later opnieuw opgebouwd worden naar analogie van de testomgeving-configuraties. ## Automation Level Volledig geautomatiseerd voor de testomgeving: build, test, lint én upload naar de test-Pi gebeuren zonder handmatige tussenstap, zodra er gemerged wordt naar `master` (of handmatig getriggerd wordt). Alleen productie is nog niet geautomatiseerd. @@ -37,7 +37,7 @@ Deze secrets heten bewust `PI_MAIN_*` in plaats van `PI_TEST_*`: alle webhosts g Dit is bewust wachtwoord-authenticatie (voor nu, zoals gekozen), zodat de testomgeving snel werkend is. Zie "Future Work" in `deployment-instructions.md` voor de overstap naar SSH-key-authenticatie. ## Open Item — Productie-deploy Nog Niet Geautomatiseerd -Zodra de definitieve productiehosting bekend is (en of dit dezelfde soort Raspberry Pi-opstelling is, of een externe hostingpartij), voeg een `deploy-production`-job toe aan `continuous_integration.yaml` die `deploy.yaml` aanroept met `environment: production`. Zolang het dezelfde webhost (Pi Main) blijft, kunnen de bestaande `PI_MAIN_*` secrets hergebruikt worden; pas dit pas aan naar omgeving-specifieke secrets als productie daadwerkelijk op een andere host komt. Domeinnaam (`slpsoftware.nl`) en SSL-aanpak (certbot/Let's Encrypt op de reverse-proxy-Pi) liggen al vast, zie `nginx/reverse-proxy-nginx-production.conf.example`. +Zodra de definitieve productiehosting bekend is (en of dit dezelfde soort Raspberry Pi-opstelling is, of een externe hostingpartij), voeg een `deploy-production`-job toe aan `continuous_integration.yaml` die `deploy.yaml` aanroept met `environment: production`. Zolang het dezelfde webhost (Pi Main) blijft, kunnen de bestaande `PI_MAIN_*` secrets hergebruikt worden; pas dit pas aan naar omgeving-specifieke secrets als productie daadwerkelijk op een andere host komt. Domeinnaam (`slpsoftware.nl`) en SSL-aanpak (certbot/Let's Encrypt op de reverse-proxy-Pi) liggen al vast; er is bewust (nog) geen productie-nginx-voorbeeldconfiguratie, deze wordt later opnieuw opgebouwd zodra er een goed-werkende, foutloze versie is. ## Verified Build Prerequisite Dit plan bouwt voort op de Build and Test-stage (`construction/build-and-test/build-and-test-summary.md`): `pnpm run build` produceert een statische `dist/`-bundel zonder server-side vereisten, geschikt om direct door nginx geserveerd te worden. diff --git a/aidlc-docs/features/react-frontend/operations/deployment/nginx/reverse-proxy-nginx-production.conf.example b/aidlc-docs/features/react-frontend/operations/deployment/nginx/reverse-proxy-nginx-production.conf.example deleted file mode 100644 index 5e1dfd6..0000000 --- a/aidlc-docs/features/react-frontend/operations/deployment/nginx/reverse-proxy-nginx-production.conf.example +++ /dev/null @@ -1,55 +0,0 @@ -# Voorbeeldconfiguratie voor de nginx reverse proxy op de Raspberry Pi voor de -# PRODUCTIEOMGEVING, bereikbaar via het domein slpsoftware.nl. -# -# LET OP: dit bestand is nog niet in gebruik — de productie-deploy is nog niet -# geautomatiseerd (zie deployment-plan.md's "Open Item — Productie-deploy Nog -# Niet Geautomatiseerd"). Dit voorbeeld ligt hier alvast klaar zodat het -# domein en SSL-certificaat (net als bij de testomgeving via certbot) bekend -# zijn wanneer de productie-deploy wordt opgezet. Vervang -# en door de daadwerkelijke waarden zodra die bekend -# zijn (mogelijk dezelfde webserver-Pi op een ander poortnummer/pad, of een -# aparte Pi/host). -# -# Kopieer dit bestand handmatig naar bijvoorbeeld -# /etc/nginx/sites-available/slpsoftware-production.conf op de reverse-proxy-Pi, -# maak een symlink in sites-enabled, en herlaad nginx. -# -# SSL-certificaat aanvragen met certbot (Let's Encrypt), nadat het DNS-record -# voor slpsoftware.nl naar het publieke IP van deze Pi wijst: -# sudo certbot --nginx -d slpsoftware.nl -d www.slpsoftware.nl - -server { - listen 80; - listen [::]:80; - - server_name slpsoftware.nl www.slpsoftware.nl; - - location /.well-known/acme-challenge/ { - root /var/www/certbot; - } - - location / { - return 301 https://$host$request_uri; - } -} - -server { - listen 443 ssl; - listen [::]:443 ssl; - - server_name slpsoftware.nl www.slpsoftware.nl; - - ssl_certificate /etc/letsencrypt/live/slpsoftware.nl/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/slpsoftware.nl/privkey.pem; - include /etc/letsencrypt/options-ssl-nginx.conf; - ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; - - location / { - proxy_pass http://:; - - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } -} diff --git a/aidlc-docs/features/react-frontend/operations/deployment/nginx/reverse-proxy-nginx.conf.example b/aidlc-docs/features/react-frontend/operations/deployment/nginx/reverse-proxy-nginx.conf.example index cbfcdbb..15b20a6 100644 --- a/aidlc-docs/features/react-frontend/operations/deployment/nginx/reverse-proxy-nginx.conf.example +++ b/aidlc-docs/features/react-frontend/operations/deployment/nginx/reverse-proxy-nginx.conf.example @@ -11,46 +11,56 @@ # certbot (Let's Encrypt) rechtstreeks op deze reverse-proxy-Pi, bijvoorbeeld: # sudo certbot --nginx -d test.slpsoftware.nl # Zorg dat het DNS-record voor test.slpsoftware.nl al naar het publieke IP van -# deze Pi wijst voordat je certbot draait. Certbot vult automatisch het -# onderstaande `listen 443 ssl` server-block aan (of maakt het aan) en -# schrijft de HTTP-server hieronder om naar een 301-redirect. Het onderstaande -# is dus vooral illustratief voor de eindsituatie. - -# HTTP: alleen gebruikt voor de Let's Encrypt ACME-challenge en om door te -# verwijzen naar HTTPS. Na het draaien van certbot ziet dit block er zo uit. +# deze Pi wijst voordat je certbot draait. +# +# Dit is de daadwerkelijk in gebruik zijnde configuratie op de reverse-proxy-Pi, +# zoals door certbot gegenereerd/beheerd. Het HTTP-server-block onderaan (dat +# doorverwijst naar HTTPS, behalve voor de ACME-challenge) en de #-commentaren +# "managed by Certbot" zijn automatisch door certbot toegevoegd/aangepast. +# +# LET OP - gevonden foutje: de header hieronder heet `X-Real-IP` (met +# koppelteken), niet `X-Real_IP` (met underscore). Een header met een +# underscore is geen geldige/herkenbare naam voor de standaard `$http_x_real_ip`- +# achtige verwerking; nginx stuurt de header wel door zoals opgegeven, maar +# de ontvangende applicatie/module verwacht het koppelteken-formaat. Corrigeer +# dit op de Pi zelf naar `X-Real-IP` zodra je de config daar aanpast. server { - listen 80; - listen [::]:80; server_name test.slpsoftware.nl; + error_log /var/log/nginx/slpsoftware-test_error.log; + access_log /var/log/nginx/slpsoftware-test_access.log; + location /.well-known/acme-challenge/ { root /var/www/certbot; } - location / { - return 301 https://$host$request_uri; - } -} - -# HTTPS: het daadwerkelijke reverse-proxy-verkeer naar de webserver-Pi. -server { - listen 443 ssl; - listen [::]:443 ssl; - - server_name test.slpsoftware.nl; - - ssl_certificate /etc/letsencrypt/live/test.slpsoftware.nl/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/test.slpsoftware.nl/privkey.pem; - include /etc/letsencrypt/options-ssl-nginx.conf; - ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; - location / { proxy_pass http://192.168.1.103:80; - proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } + + listen 443 ssl; # managed by Certbot + listen [::]:443 ssl; # managed by Certbot + ssl_certificate /etc/letsencrypt/live/test.slpsoftware.nl/fullchain.pem; # managed by Certbot + ssl_certificate_key /etc/letsencrypt/live/test.slpsoftware.nl/privkey.pem; # managed by Certbot + include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot + ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot + } + +server { + if ($host = test.slpsoftware.nl) { + return 301 https://$host$request_uri; + } # managed by Certbot + + + listen 80; + listen [::]:80; + + server_name test.slpsoftware.nl; + return 404; # managed by Certbot +} \ No newline at end of file diff --git a/aidlc-docs/features/react-frontend/operations/deployment/nginx/webserver-nginx.conf.example b/aidlc-docs/features/react-frontend/operations/deployment/nginx/webserver-nginx.conf.example index f3b63fc..70b5dbe 100644 --- a/aidlc-docs/features/react-frontend/operations/deployment/nginx/webserver-nginx.conf.example +++ b/aidlc-docs/features/react-frontend/operations/deployment/nginx/webserver-nginx.conf.example @@ -3,21 +3,20 @@ # # Deze Pi is NIET rechtstreeks vanaf het internet bereikbaar; de andere # Raspberry Pi (met de nginx reverse proxy, zie reverse-proxy-nginx.conf.example) -# stuurt inkomend verkeer door naar deze server op het interne netwerk. +# stuurt inkomend verkeer door naar deze server op het interne netwerk (poort 80). # +# Dit is de daadwerkelijk in gebruik zijnde configuratie op de webserver-Pi. # Kopieer dit bestand handmatig naar bijvoorbeeld # /etc/nginx/sites-available/slpsoftware-test.conf op de webserver-Pi, # maak een symlink in sites-enabled, en herlaad nginx (`sudo nginx -t && sudo systemctl reload nginx`). server { - # Alleen luisteren op het interne (LAN) IP-adres van deze Pi, niet op 0.0.0.0, - # zodat deze poort niet per ongeluk vanaf buiten het netwerk bereikbaar is. - listen 192.168.1.103:80; + listen 80; listen [::]:80; - server_name _; + server_name test.slpsoftware.nl; - root /html/test/slpsoftware; + root /mnt/storage1/www/html/test/slpsoftware; index index.html; # De React-app gebruikt client-side routing (React Router), dus onbekende @@ -34,6 +33,12 @@ server { } # Doorgeven van het echte client-IP en protocol, afkomstig van de reverse proxy. - set_real_ip_from 192.168.1.0/24; - real_ip_header X-Forwarded-For; -} + # Uitgecommentarieerd: de reverse-proxy Pi geeft het echte client-IP al door + # via de X-Forwarded-For header hierboven; deze twee directieven zouden nginx + # dat IP ook laten gebruiken als "remote address" in de eigen access-log van + # deze Pi. Zonder deze regels tonen de logs van deze webserver-Pi het interne + # IP van de reverse-proxy Pi als bezoeker-IP (functioneel geen probleem, wel + # minder nauwkeurig voor logging/analyse op deze Pi zelf). + #set_real_ip_from 192.168.1.0/24; + #real_ip_header X-Forwarded-For; +} \ No newline at end of file diff --git a/aidlc-docs/features/react-frontend/operations/deployment/rollback-plan.md b/aidlc-docs/features/react-frontend/operations/deployment/rollback-plan.md index 40b1e2d..fb401e7 100644 --- a/aidlc-docs/features/react-frontend/operations/deployment/rollback-plan.md +++ b/aidlc-docs/features/react-frontend/operations/deployment/rollback-plan.md @@ -6,7 +6,7 @@ Sinds deze stap wordt de testomgeving automatisch gedeployed door de `deploy-tes ## Rolling Back the Live Site (Test-omgeving) 1. Identificeer de laatste bekend-goede commit op `master` (of een eerdere `release/*`-branch/tag) die succesvol gedeployed was. 2. Trigger de **Continuous Integration** workflow handmatig (`workflow_dispatch`) tegen die commit/branch/tag in Gitea Actions. -3. De `deploy-test` job uploadt automatisch de resulterende `dist/`-build naar `/html/test/slpsoftware` op de webserver-Pi, en overschrijft daarmee de huidige (foutieve) bestanden. +3. De `deploy-test` job uploadt automatisch de resulterende `dist/`-build via SCP naar `/html/test/slpsoftware` op de webserver-Pi — dit relatieve pad staat t.o.v. de root van de SCP-gebruiker (`/mnt/storage1/www`), waardoor nginx dezelfde bestanden serveert vanaf het volledige pad `/mnt/storage1/www/html/test/slpsoftware` (zie de uitleg hierover in `deployment-instructions.md`) — en overschrijft daarmee de huidige (foutieve) bestanden. 4. Verify de live testomgeving reflecteert de teruggedraaide versie (via het adres achter de reverse proxy). ## Keeping Rollback Possible From 7680feb29ffa04badc1653a2112ca8a1d33baa07 Mon Sep 17 00:00:00 2001 From: Sluijsens Date: Sat, 25 Jul 2026 13:15:18 +0200 Subject: [PATCH 02/11] Voeg Sentry-foutregistratie toe als gekozen logging-bestemming Co-authored-by: Junie --- .gitea/workflows/continuous_integration.yaml | 6 ++ aidlc-docs/features/react-frontend/audit.md | 12 +++ .../operations/monitoring/monitoring-plan.md | 4 +- .../operations/monitoring/monitoring-setup.md | 19 ++--- .../production-readiness-checklist.md | 2 +- package.json | 1 + pnpm-lock.yaml | 78 +++++++++++++++++++ src/components/ErrorBoundary.tsx | 2 + .../__tests__/ErrorBoundary.test.tsx | 61 +++++++++++++++ src/main.tsx | 6 ++ src/vite-env.d.ts | 10 +++ tsconfig.tsbuildinfo | 2 +- 12 files changed, 190 insertions(+), 13 deletions(-) create mode 100644 src/components/__tests__/ErrorBoundary.test.tsx create mode 100644 src/vite-env.d.ts diff --git a/.gitea/workflows/continuous_integration.yaml b/.gitea/workflows/continuous_integration.yaml index 2add76f..8bd7bc9 100644 --- a/.gitea/workflows/continuous_integration.yaml +++ b/.gitea/workflows/continuous_integration.yaml @@ -93,6 +93,12 @@ jobs: - name: Build run: pnpm run build + env: + # Sentry DSN is niet gevoelig (veilig om in de client-bundle te zitten), + # daarom een Gitea Actions "vars"-waarde i.p.v. een secret. Optioneel: + # als deze niet is ingesteld, wordt Sentry-logging simpelweg overgeslagen + # (zie src/main.tsx) en blijft alleen console-logging actief. + VITE_SENTRY_DSN: ${{ vars.VITE_SENTRY_DSN }} - name: Upload build artifact uses: actions/upload-artifact@v3 diff --git a/aidlc-docs/features/react-frontend/audit.md b/aidlc-docs/features/react-frontend/audit.md index c04c7fe..bcd13bb 100644 --- a/aidlc-docs/features/react-frontend/audit.md +++ b/aidlc-docs/features/react-frontend/audit.md @@ -456,3 +456,15 @@ **Context**: Post-workflow follow-up (feature already marked Complete); clarifies wording of the previous follow-up's path-discrepancy note per user feedback. Changes not yet committed/pushed. --- + +## Post-Completion Follow-up: Client-Side Error Logging Destination Decided (Sentry) + +**Timestamp**: 2026-07-25T12:55:00Z + +**User Input**: "Als het goed is stonden er nog een paar punten open? Zullen we het volgende punt oppakken? Kan je ook een feature-branch aanmaken met een passende naam? [...]" — followed by selecting "Logging-bestemming kiezen" and then "Sentry (free tier)" as the concrete destination. + +**AI Response**: "Picked up the next open follow-up from `production-readiness-checklist.md`: the client-side error logging destination. Created feature branch `feature/sentry_error_logging` (carrying over the previously uncommitted path-documentation changes, committed there first). Added `@sentry/react` as a dependency, initialized it in `src/main.tsx` via `Sentry.init({ dsn: import.meta.env.VITE_SENTRY_DSN })` (no-op if the DSN is unset), added `src/vite-env.d.ts` for `import.meta.env` typing, and hooked `ErrorBoundary.componentDidCatch` to also call `Sentry.captureException`. Wired the DSN as a Gitea Actions repository variable (`vars.VITE_SENTRY_DSN`, not a secret) into the `Build` step of `continuous_integration.yaml`. Updated `monitoring-plan.md`, `monitoring-setup.md`, and `production-readiness-checklist.md` to mark this open item resolved, with the remaining manual step (creating the actual `VITE_SENTRY_DSN` Gitea variable with a real Sentry DSN) called out explicitly." + +**Context**: Post-workflow follow-up (feature already marked Complete); resolves open follow-up #2 from `production-readiness-checklist.md`. Work done on new branch `feature/sentry_error_logging`. + +--- diff --git a/aidlc-docs/features/react-frontend/operations/monitoring/monitoring-plan.md b/aidlc-docs/features/react-frontend/operations/monitoring/monitoring-plan.md index dc3b4c7..3ecef2a 100644 --- a/aidlc-docs/features/react-frontend/operations/monitoring/monitoring-plan.md +++ b/aidlc-docs/features/react-frontend/operations/monitoring/monitoring-plan.md @@ -13,7 +13,7 @@ Initial answers to the monitoring plan were contradictory: Question 2 selected " ## Chosen Approach(es) ### Logging -Client-side errors (JavaScript crashes, broken links) should be logged, but the concrete destination is not yet decided (original Question 3 = C, "not yet determined"). This is tracked as an **open action item** below rather than blocking this stage. +Client-side errors (JavaScript crashes, broken links) are logged. **Decided**: Sentry free tier (in addition to the browser console) — see the follow-up in `audit.md` and the implementation in `monitoring-setup.md`. ### Dashboards A combination of: @@ -27,7 +27,7 @@ A combination of: - **Reuse of existing infrastructure**: this feature does not plug into any pre-existing shared monitoring (original Question 7 = A) — there is none yet. Should a shared back-end/CMS monitoring stack be introduced later, this can be revisited. ## Open Action Items -1. **Decide logging destination**: choose between "browser console only" (no central storage, manual debugging) or a free/low-cost external error-tracking service (e.g. Sentry free tier) once this becomes a priority. Until decided, `monitoring-setup.md` documents both options so either can be adopted without re-doing this stage. +1. ~~Decide logging destination~~ — **Resolved**: Sentry free tier, wired into `ErrorBoundary`/`main.tsx` (see `monitoring-setup.md`). The Gitea Actions variable `VITE_SENTRY_DSN` still needs to be created by the user with a real Sentry project DSN; until then the build simply skips Sentry initialization and only console-logging is active. 2. **Pick concrete analytics + uptime tools**: `monitoring-setup.md` lists candidate free-tier tools; final tool selection/account creation is a manual follow-up outside this workflow (no code changes required to swap providers, since neither is wired into the codebase yet beyond an optional embed snippet). ## Rationale diff --git a/aidlc-docs/features/react-frontend/operations/monitoring/monitoring-setup.md b/aidlc-docs/features/react-frontend/operations/monitoring/monitoring-setup.md index 7ecd24b..81371b2 100644 --- a/aidlc-docs/features/react-frontend/operations/monitoring/monitoring-setup.md +++ b/aidlc-docs/features/react-frontend/operations/monitoring/monitoring-setup.md @@ -9,16 +9,17 @@ Concrete setup steps for the approaches chosen in `monitoring-plan.md`: **Loggin - Broken/failed navigation (e.g. an unexpected router error). - No user PII, form input, or sensitive data should ever be logged — this is a public marketing site, but keep this discipline regardless. -### Destination — open decision -The destination was not finalized (original Question 3 = C). Two supported options, either of which can be adopted later without further design work: +### Destination — decided: Sentry free tier + console +Both the console and Sentry are now active (original Question 3 resolved as a combination): -**Option 1: Browser console only (default today)** -- No code changes needed — errors already surface via `console.error` inside the existing `ErrorBoundary`. -- Zero cost, but not centrally visible; only useful for manual debugging (e.g. via a user's screenshot or a support request). +**Console (always on)** +- Errors already surface via `console.error` inside `ErrorBoundary.componentDidCatch` — unchanged, zero cost, useful for local/manual debugging. -**Option 2: External error-tracking service (e.g. Sentry free tier)** -- When decided, add `@sentry/react` as a dependency, initialize it once in the app entry point (e.g. `src/main.tsx`) with the project DSN, and report caught errors from the `ErrorBoundary`'s `componentDidCatch`/`onError` hook to Sentry in addition to the console. -- Store the DSN as a Gitea Actions variable (or a build-time `.env` value, since it's not a secret — Sentry DSNs are safe to expose client-side) and inject it via Vite's `import.meta.env`. +**Sentry free tier (implemented)** +- `@sentry/react` is a dependency; `src/main.tsx` calls `Sentry.init({ dsn: import.meta.env.VITE_SENTRY_DSN })` at startup, but only when a DSN is present — if not configured, Sentry is silently skipped and only console-logging remains active (safe default, no crash on missing config). +- `ErrorBoundary.componentDidCatch` calls `Sentry.captureException(error, { extra: { componentStack: info.componentStack } })` in addition to `console.error`. +- The DSN is injected at build time via Vite's `import.meta.env.VITE_SENTRY_DSN` (typed in `src/vite-env.d.ts`). It is **not** a secret (Sentry DSNs are safe to expose client-side), so it is passed as a **Gitea Actions repository variable** (`vars.VITE_SENTRY_DSN`, not a secret) to the `Build` step in `continuous_integration.yaml`. +- **Manual follow-up required**: create a free Sentry project (https://sentry.io) for this app, copy its DSN, and set it as the `VITE_SENTRY_DSN` repository variable in Gitea (Repository Settings → Actions → Variables). Until that variable is set, the build still succeeds and the site still works — Sentry reporting simply stays inactive. - Free tier limits (error volume, retention) are typically sufficient for a low-traffic marketing site. **Log level strategy**: only errors are logged (no verbose/info-level client logging) — this is a static site with no meaningful "business events" beyond page views, which are covered by analytics (see Dashboards below), not logging. @@ -55,7 +56,7 @@ Pick one: | Concern | Approach | Status | |---|---|---| -| Client-side errors | Logging (console today; Sentry free tier optional later) | Destination open item | +| Client-side errors | Logging (console + Sentry free tier) | Implemented; `VITE_SENTRY_DSN` Gitea variable still needs to be created by the user | | Visitor/usage insight | Analytics dashboard (Plausible/Umami/GA4) | Tool selection open item | | Site reachability | Uptime dashboard (UptimeRobot/Better Uptime) | Tool selection + production URL open item | | Alerting | Out of scope | Not configured | diff --git a/aidlc-docs/features/react-frontend/operations/production-readiness-checklist.md b/aidlc-docs/features/react-frontend/operations/production-readiness-checklist.md index 6a7dbec..13876b9 100644 --- a/aidlc-docs/features/react-frontend/operations/production-readiness-checklist.md +++ b/aidlc-docs/features/react-frontend/operations/production-readiness-checklist.md @@ -19,6 +19,6 @@ - **Ready for Production**: Yes with caveats - **Open Follow-ups**: - Finalize the hosting/domain setup and extend `.gitea/workflows/deploy.yml`'s `deploy` job to actually upload `dist/` to the host, instead of only packaging it as a downloadable artifact (see `deployment-plan.md` "Open Item") - - Decide the client-side error logging destination (browser console only vs. an external service such as Sentry free tier) — see `monitoring-setup.md` + - ~~Decide the client-side error logging destination~~ — **Resolved**: console + Sentry free tier, implemented (see `monitoring-setup.md`); remaining manual step is creating the `VITE_SENTRY_DSN` Gitea Actions variable - Pick and configure the concrete analytics tool (e.g. Plausible/Umami/GA4) and uptime dashboard tool (e.g. UptimeRobot/Better Uptime), including registering the final production URL once hosting is finalized - Run a formal Lighthouse performance check before the first real production deployment (flagged as not yet run in `build-and-test-summary.md`) diff --git a/package.json b/package.json index 235a97f..454ff70 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "@fontsource/instrument-sans": "^5.1.0", "@fontsource/jetbrains-mono": "^5.1.1", "@fontsource/sora": "^5.1.0", + "@sentry/react": "^10.68.0", "@tanstack/react-query": "^5.59.0", "@tanstack/react-router": "^1.79.0", "react": "^18.3.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 73fc582..66043af 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: '@fontsource/sora': specifier: ^5.1.0 version: 5.2.8 + '@sentry/react': + specifier: ^10.68.0 + version: 10.68.0(react@18.3.1) '@tanstack/react-query': specifier: ^5.59.0 version: 5.101.2(react@18.3.1) @@ -592,6 +595,40 @@ packages: cpu: [x64] os: [win32] + '@sentry/browser-utils@10.68.0': + resolution: {integrity: sha512-be8VtdjCngKc77cstJeV+gO15iH+blyXpBDk8yOehmtX4BkFO33mfTMNCWVR2LA0oOxjIHWRAhf77fIUEhzxPg==} + engines: {node: '>=18'} + + '@sentry/browser@10.68.0': + resolution: {integrity: sha512-8xVgk7oG2lajXnbXF6a7H1xMZ/U6icqSldHGzQu1+bajfrK8Gan9ULG/Xsj1VM1LlNeK6/7znDJ3u1jgvIwznw==} + engines: {node: '>=18'} + + '@sentry/conventions@0.16.0': + resolution: {integrity: sha512-fO9PLmHdVURcSPUpWCItWAtgKiMwGdJHbovoSEyLplX5sxs2ugvI4CBPTrkkgqhObnZOD0CnWBKDzSVQYBKEyQ==} + engines: {node: '>=14'} + + '@sentry/core@10.68.0': + resolution: {integrity: sha512-5Amhx8ltVz7vb1bRGyf3c4J69/iHW8R/H+SJxTRILHlsSOBrnVVc/IQEYDC6PTRdRdZ3x2u7RVjxZi2Mhe525g==} + engines: {node: '>=18'} + + '@sentry/feedback@10.68.0': + resolution: {integrity: sha512-XbdcXiBnpC3vgw46eHOPeD/ZQ+XzluP75ubdUcaPDW02hCh2nsdXiwjZ2DBImbpvIpTbJgjHf/sIlHWvcZJ2Mg==} + engines: {node: '>=18'} + + '@sentry/react@10.68.0': + resolution: {integrity: sha512-rIq4QR4ScMHHx9JJZv7Jgw31bMdUVJMx+ykHIJb7htjY6mj78sjKs+KpCsMDnvJxDhSmvftGM1KfKD4BggL7OQ==} + engines: {node: '>=18'} + peerDependencies: + react: ^16.14.0 || 17.x || 18.x || 19.x + + '@sentry/replay-canvas@10.68.0': + resolution: {integrity: sha512-HusYcr+He+ohnUDHunYrc5St6vdDnBXpUAndnT5ReyUMVSCiWKfY3paXowU/0787HwYfxdcpZgwC5u79+XbEIg==} + engines: {node: '>=18'} + + '@sentry/replay@10.68.0': + resolution: {integrity: sha512-ZoG2n16vbkx4GWSCnLIqUUN9xlUmccQFbQ2US2rhruQeHTUnHl/ukr8NHOQXZaEbwKyMkX9bEMwfmZHJm+wSTQ==} + engines: {node: '>=18'} + '@tanstack/history@1.162.0': resolution: {integrity: sha512-79pf/RkhteYZTRgcR4F9kbk84P2N8rugQJswxfIqovlbRiT3yI7eBE+5QorIrZaOKktsgzRlXh1l/du/xpl4iA==} engines: {node: '>=20.19'} @@ -2280,6 +2317,47 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.62.2': optional: true + '@sentry/browser-utils@10.68.0': + dependencies: + '@sentry/conventions': 0.16.0 + '@sentry/core': 10.68.0 + + '@sentry/browser@10.68.0': + dependencies: + '@sentry/browser-utils': 10.68.0 + '@sentry/conventions': 0.16.0 + '@sentry/core': 10.68.0 + '@sentry/feedback': 10.68.0 + '@sentry/replay': 10.68.0 + '@sentry/replay-canvas': 10.68.0 + + '@sentry/conventions@0.16.0': {} + + '@sentry/core@10.68.0': + dependencies: + '@sentry/conventions': 0.16.0 + + '@sentry/feedback@10.68.0': + dependencies: + '@sentry/core': 10.68.0 + + '@sentry/react@10.68.0(react@18.3.1)': + dependencies: + '@sentry/browser': 10.68.0 + '@sentry/conventions': 0.16.0 + '@sentry/core': 10.68.0 + react: 18.3.1 + + '@sentry/replay-canvas@10.68.0': + dependencies: + '@sentry/core': 10.68.0 + '@sentry/replay': 10.68.0 + + '@sentry/replay@10.68.0': + dependencies: + '@sentry/browser-utils': 10.68.0 + '@sentry/core': 10.68.0 + '@tanstack/history@1.162.0': {} '@tanstack/query-core@5.101.2': {} diff --git a/src/components/ErrorBoundary.tsx b/src/components/ErrorBoundary.tsx index 182aa7f..3acecf2 100644 --- a/src/components/ErrorBoundary.tsx +++ b/src/components/ErrorBoundary.tsx @@ -1,5 +1,6 @@ import { Component } from 'react'; import type { ErrorInfo, ReactNode } from 'react'; +import * as Sentry from '@sentry/react'; interface ErrorBoundaryProps { children: ReactNode; @@ -22,6 +23,7 @@ export class ErrorBoundary extends Component ({ + captureException: vi.fn(), +})); + +function ThrowingChild(): never { + throw new Error('boom'); +} + +describe('ErrorBoundary', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('renders children when there is no error', () => { + render( + +

Alles werkt

+
, + ); + expect(screen.getByText('Alles werkt')).toBeInTheDocument(); + }); + + it('renders the generic fallback message when a child throws', () => { + // Suppress the expected React error boundary console.error noise for this test. + const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + + render( + + + , + ); + + expect(screen.getByTestId('error-boundary-fallback')).toBeInTheDocument(); + expect(screen.getByText('Er ging iets mis. Probeer de pagina te vernieuwen.')).toBeInTheDocument(); + + consoleErrorSpy.mockRestore(); + }); + + it('reports the caught error to Sentry', () => { + const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + + render( + + + , + ); + + expect(Sentry.captureException).toHaveBeenCalledTimes(1); + expect(Sentry.captureException).toHaveBeenCalledWith( + expect.any(Error), + expect.objectContaining({ extra: expect.objectContaining({ componentStack: expect.any(String) }) }), + ); + + consoleErrorSpy.mockRestore(); + }); +}); diff --git a/src/main.tsx b/src/main.tsx index 0811297..c9cc812 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,10 +1,16 @@ import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; import { RouterProvider } from '@tanstack/react-router'; +import * as Sentry from '@sentry/react'; import './fonts'; import './index.css'; import { router } from './router'; +const sentryDsn = import.meta.env.VITE_SENTRY_DSN; +if (sentryDsn) { + Sentry.init({ dsn: sentryDsn }); +} + const rootElement = document.getElementById('root'); if (!rootElement) { throw new Error('Root element "#root" not found in index.html'); diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000..231ace6 --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1,10 @@ +/// + +interface ImportMetaEnv { + /** Sentry DSN (Data Source Name) for client-side error reporting. Not a secret — safe to expose in the client bundle. */ + readonly VITE_SENTRY_DSN?: string; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +} diff --git a/tsconfig.tsbuildinfo b/tsconfig.tsbuildinfo index e203f3a..afd3167 100644 --- a/tsconfig.tsbuildinfo +++ b/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/fonts.ts","./src/main.tsx","./src/queryclient.ts","./src/router.tsx","./src/components/aboutsection.tsx","./src/components/contactsection.tsx","./src/components/errorboundary.tsx","./src/components/footer.tsx","./src/components/hero.tsx","./src/components/nav.tsx","./src/components/packagecard.tsx","./src/components/packagessection.tsx","./src/components/processsection.tsx","./src/components/processstep.tsx","./src/components/rootlayout.tsx","./src/components/themetoggle.tsx","./src/components/__tests__/nav.test.tsx","./src/components/__tests__/packagessection.test.tsx","./src/data/content.ts","./src/hooks/usepackagesquery.ts","./src/routes/__root.tsx","./src/routes/index.tsx","./src/test/setup.ts","./src/theme/themeprovider.tsx","./src/theme/tokens.ts","./src/theme/__tests__/themeprovider.test.tsx","./src/utils/scrolltohash.ts"],"version":"5.9.3"} \ No newline at end of file +{"root":["./src/fonts.ts","./src/main.tsx","./src/queryclient.ts","./src/router.tsx","./src/vite-env.d.ts","./src/components/aboutsection.tsx","./src/components/contactsection.tsx","./src/components/errorboundary.tsx","./src/components/footer.tsx","./src/components/hero.tsx","./src/components/nav.tsx","./src/components/packagecard.tsx","./src/components/packagessection.tsx","./src/components/processsection.tsx","./src/components/processstep.tsx","./src/components/rootlayout.tsx","./src/components/themetoggle.tsx","./src/components/__tests__/errorboundary.test.tsx","./src/components/__tests__/nav.test.tsx","./src/components/__tests__/packagessection.test.tsx","./src/data/content.ts","./src/hooks/usepackagesquery.ts","./src/routes/__root.tsx","./src/routes/index.tsx","./src/test/setup.ts","./src/theme/themeprovider.tsx","./src/theme/tokens.ts","./src/theme/__tests__/themeprovider.test.tsx","./src/utils/scrolltohash.ts"],"version":"5.9.3"} \ No newline at end of file From 524b5a6dc77b26bc4003f192aba2156d5af21e9f Mon Sep 17 00:00:00 2001 From: Sluijsens Date: Sat, 25 Jul 2026 13:35:09 +0200 Subject: [PATCH 03/11] Voeg tracing en environment/release-tags toe aan Sentry-configuratie Co-authored-by: Junie --- aidlc-docs/features/react-frontend/audit.md | 12 ++++++++++++ .../operations/monitoring/monitoring-setup.md | 5 ++++- src/main.tsx | 9 ++++++++- src/vite-env.d.ts | 3 +++ tsconfig.node.tsbuildinfo | 2 +- vite.config.js | 5 +++++ vite.config.ts | 5 +++++ 7 files changed, 38 insertions(+), 3 deletions(-) diff --git a/aidlc-docs/features/react-frontend/audit.md b/aidlc-docs/features/react-frontend/audit.md index bcd13bb..42883b8 100644 --- a/aidlc-docs/features/react-frontend/audit.md +++ b/aidlc-docs/features/react-frontend/audit.md @@ -468,3 +468,15 @@ **Context**: Post-workflow follow-up (feature already marked Complete); resolves open follow-up #2 from `production-readiness-checklist.md`. Work done on new branch `feature/sentry_error_logging`. --- + +## Post-Completion Follow-up: Sentry Setup Checked Against Official Instrumentation Guide + +**Timestamp**: 2026-07-25T13:20:00Z + +**User Input**: "Ik zie dat sentry al helemaal is ingericht, maar ik kreeg vanuit Sentry zelf de volgende prompt voor het opzetten. Kan je kijken of je misschien dingen hebt gemist of dat er alsnog wat nodig is: Use curl to download, read and follow https://skills.sentry.dev/instrument to set up the Sentry React SDK." + +**AI Response**: "Fetched and read the official Sentry instrumentation guide and its React/Vite-specific reference. Compared it against the existing setup and found the following gaps: (1) no tracing/performance monitoring configured (the guide's recommended default is errors + tracing, not errors alone), (2) no `environment`/`release` tags, (3) no TanStack Router tracing integration, (4) no automatic source map upload (stack traces would show minified code in production). Asked the user which gaps to address; they chose tracing and environment/release tags, explicitly declining source maps for now. Implemented: added `tanstackRouterBrowserTracingIntegration(router)` + `tracesSampleRate: 1.0` and `environment: import.meta.env.MODE` + `release: __APP_VERSION__` to `Sentry.init()` in `src/main.tsx`; `__APP_VERSION__` is injected at build time from `package.json`'s version via a new `define` block in `vite.config.ts` (typed in `src/vite-env.d.ts`). Updated `monitoring-setup.md` to describe the new tracing/environment/release behavior and explicitly note that source map upload was deliberately left out of scope (would require a new Sentry auth-token/org/project secret)." + +**Context**: Post-workflow follow-up (feature already marked Complete); continues the `feature/sentry_error_logging` branch with a gap-check against Sentry's own official setup guide. Build, lint and tests re-verified successfully after the change. + +--- diff --git a/aidlc-docs/features/react-frontend/operations/monitoring/monitoring-setup.md b/aidlc-docs/features/react-frontend/operations/monitoring/monitoring-setup.md index 81371b2..8672604 100644 --- a/aidlc-docs/features/react-frontend/operations/monitoring/monitoring-setup.md +++ b/aidlc-docs/features/react-frontend/operations/monitoring/monitoring-setup.md @@ -16,11 +16,14 @@ Both the console and Sentry are now active (original Question 3 resolved as a co - Errors already surface via `console.error` inside `ErrorBoundary.componentDidCatch` — unchanged, zero cost, useful for local/manual debugging. **Sentry free tier (implemented)** -- `@sentry/react` is a dependency; `src/main.tsx` calls `Sentry.init({ dsn: import.meta.env.VITE_SENTRY_DSN })` at startup, but only when a DSN is present — if not configured, Sentry is silently skipped and only console-logging remains active (safe default, no crash on missing config). +- `@sentry/react` is a dependency; `src/main.tsx` calls `Sentry.init({ ... })` at startup, but only when a DSN is present — if not configured, Sentry is silently skipped and only console-logging remains active (safe default, no crash on missing config). - `ErrorBoundary.componentDidCatch` calls `Sentry.captureException(error, { extra: { componentStack: info.componentStack } })` in addition to `console.error`. +- **Tracing/performance** is also enabled (Sentry's recommended default alongside error monitoring, per the official React SDK setup guide): `tanstackRouterBrowserTracingIntegration(router)` is wired up so route navigations are captured as transactions, with `tracesSampleRate: 1.0` (capture all — appropriate for a low-traffic marketing site; lower this if traffic grows significantly). +- **Environment/release tagging**: `environment` is set to `import.meta.env.MODE` (e.g. `production`) and `release` is set to the app version from `package.json` (injected at build time via `vite.config.ts`'s `define: { __APP_VERSION__ }`), so events in Sentry can be filtered/grouped per environment and per shipped version. - The DSN is injected at build time via Vite's `import.meta.env.VITE_SENTRY_DSN` (typed in `src/vite-env.d.ts`). It is **not** a secret (Sentry DSNs are safe to expose client-side), so it is passed as a **Gitea Actions repository variable** (`vars.VITE_SENTRY_DSN`, not a secret) to the `Build` step in `continuous_integration.yaml`. - **Manual follow-up required**: create a free Sentry project (https://sentry.io) for this app, copy its DSN, and set it as the `VITE_SENTRY_DSN` repository variable in Gitea (Repository Settings → Actions → Variables). Until that variable is set, the build still succeeds and the site still works — Sentry reporting simply stays inactive. - Free tier limits (error volume, retention) are typically sufficient for a low-traffic marketing site. +- **Not (yet) implemented, by explicit choice**: automatic source map upload (via `@sentry/vite-plugin`), which the official Sentry setup guide also recommends so stack traces show real source code instead of minified code. This requires a Sentry auth token/org/project as a new Gitea secret; deliberately left out of scope for now — revisit if readable production stack traces become a priority. **Log level strategy**: only errors are logged (no verbose/info-level client logging) — this is a static site with no meaningful "business events" beyond page views, which are covered by analytics (see Dashboards below), not logging. diff --git a/src/main.tsx b/src/main.tsx index c9cc812..6d31e5d 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -8,7 +8,14 @@ import { router } from './router'; const sentryDsn = import.meta.env.VITE_SENTRY_DSN; if (sentryDsn) { - Sentry.init({ dsn: sentryDsn }); + Sentry.init({ + dsn: sentryDsn, + environment: import.meta.env.MODE, + release: __APP_VERSION__, + integrations: [Sentry.tanstackRouterBrowserTracingIntegration(router)], + // Low-traffic marketing site: capture all traces (lower this if traffic grows significantly). + tracesSampleRate: 1.0, + }); } const rootElement = document.getElementById('root'); diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index 231ace6..3b59f68 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -8,3 +8,6 @@ interface ImportMetaEnv { interface ImportMeta { readonly env: ImportMetaEnv; } + +/** App version at build time (from package.json), injected via vite.config.ts `define`. Used as the Sentry `release` tag. */ +declare const __APP_VERSION__: string; diff --git a/tsconfig.node.tsbuildinfo b/tsconfig.node.tsbuildinfo index e787043..4cf94f9 100644 --- a/tsconfig.node.tsbuildinfo +++ b/tsconfig.node.tsbuildinfo @@ -1 +1 @@ -{"fileNames":["./node_modules/typescript/lib/lib.d.ts","./node_modules/typescript/lib/lib.es5.d.ts","./node_modules/typescript/lib/lib.dom.d.ts","./node_modules/typescript/lib/lib.webworker.importscripts.d.ts","./node_modules/typescript/lib/lib.scripthost.d.ts","./node_modules/typescript/lib/lib.decorators.d.ts","./node_modules/typescript/lib/lib.decorators.legacy.d.ts","./node_modules/@types/estree/index.d.ts","./node_modules/rollup/dist/rollup.d.ts","./node_modules/rollup/dist/parseast.d.ts","./node_modules/vite/types/hmrpayload.d.ts","./node_modules/vite/types/customevent.d.ts","./node_modules/vite/types/hot.d.ts","./node_modules/vite/dist/node/types.d-agj9qkwt.d.ts","./node_modules/esbuild/lib/main.d.ts","./node_modules/source-map-js/source-map.d.ts","./node_modules/postcss/lib/previous-map.d.ts","./node_modules/postcss/lib/input.d.ts","./node_modules/postcss/lib/css-syntax-error.d.ts","./node_modules/postcss/lib/declaration.d.ts","./node_modules/postcss/lib/root.d.ts","./node_modules/postcss/lib/warning.d.ts","./node_modules/postcss/lib/lazy-result.d.ts","./node_modules/postcss/lib/no-work-result.d.ts","./node_modules/postcss/lib/processor.d.ts","./node_modules/postcss/lib/result.d.ts","./node_modules/postcss/lib/document.d.ts","./node_modules/postcss/lib/rule.d.ts","./node_modules/postcss/lib/node.d.ts","./node_modules/postcss/lib/comment.d.ts","./node_modules/postcss/lib/container.d.ts","./node_modules/postcss/lib/at-rule.d.ts","./node_modules/postcss/lib/list.d.ts","./node_modules/postcss/lib/postcss.d.ts","./node_modules/postcss/lib/postcss.d.mts","./node_modules/vite/dist/node/runtime.d.ts","./node_modules/vite/types/importglob.d.ts","./node_modules/vite/types/metadata.d.ts","./node_modules/vite/dist/node/index.d.ts","./node_modules/@vitest/pretty-format/dist/index.d.ts","./node_modules/@vitest/utils/dist/types.d.ts","./node_modules/@vitest/utils/dist/helpers.d.ts","./node_modules/tinyrainbow/dist/index-c1cfc5e9.d.ts","./node_modules/tinyrainbow/dist/node.d.ts","./node_modules/@vitest/utils/dist/index.d.ts","./node_modules/@vitest/runner/dist/tasks-3znpj1lr.d.ts","./node_modules/@vitest/utils/dist/types-bxe-2udy.d.ts","./node_modules/@vitest/utils/dist/diff.d.ts","./node_modules/@vitest/runner/dist/types.d.ts","./node_modules/@vitest/utils/dist/error.d.ts","./node_modules/@vitest/runner/dist/index.d.ts","./node_modules/vitest/dist/chunks/environment.looobwuu.d.ts","./node_modules/@vitest/snapshot/dist/environment-ddx0edty.d.ts","./node_modules/@vitest/snapshot/dist/rawsnapshot-cpnkto81.d.ts","./node_modules/@vitest/snapshot/dist/index.d.ts","./node_modules/@vitest/snapshot/dist/environment.d.ts","./node_modules/vitest/dist/chunks/config.cy0c388z.d.ts","./node_modules/vite-node/dist/trace-mapping.d-dlvdeqop.d.ts","./node_modules/vite-node/dist/index-z0r8hvru.d.ts","./node_modules/vite-node/dist/index.d.ts","./node_modules/@vitest/utils/dist/source-map.d.ts","./node_modules/vite-node/dist/client.d.ts","./node_modules/vite-node/dist/server.d.ts","./node_modules/@vitest/runner/dist/utils.d.ts","./node_modules/tinybench/dist/index.d.ts","./node_modules/vitest/dist/chunks/benchmark.geerunq4.d.ts","./node_modules/@vitest/snapshot/dist/manager.d.ts","./node_modules/vitest/dist/chunks/reporters.nr4dxcka.d.ts","./node_modules/vitest/dist/chunks/vite.czkp4x9w.d.ts","./node_modules/vitest/dist/config.d.ts","./node_modules/vitest/config.d.ts","./node_modules/@babel/types/lib/index.d.ts","./node_modules/@types/babel__generator/index.d.ts","./node_modules/@babel/parser/typings/babel-parser.d.ts","./node_modules/@types/babel__template/index.d.ts","./node_modules/@types/babel__traverse/index.d.ts","./node_modules/@types/babel__core/index.d.ts","./node_modules/@vitejs/plugin-react/dist/index.d.ts","./vite.config.ts","./node_modules/@types/aria-query/index.d.ts","./node_modules/@types/prop-types/index.d.ts","./node_modules/@types/react/global.d.ts","./node_modules/csstype/index.d.ts","./node_modules/@types/react/index.d.ts","./node_modules/@types/react-dom/index.d.ts"],"fileIdsList":[[72],[72,73,74,75,76],[72,74],[84],[81,82,83],[39,69,77],[45,46,48,49,50],[45],[45,46,48],[45,46],[53],[40,53,54],[40,53],[40,47],[41],[40,41,42,44],[40],[31],[29,31],[20,28,29,30,32,34],[18],[21,26,31,34],[17,34],[21,22,25,26,27,34],[21,22,23,25,26,34],[18,19,20,21,22,26,27,28,30,31,32,34],[34],[16,18,19,20,21,22,23,25,26,27,28,29,30,31,32,33],[16,34],[21,23,24,26,27,34],[25,34],[26,27,31,34],[19,29],[9,38],[8,9],[43],[58,59],[58],[39,58,59,69],[9,10,11,12,13,14,15,35,36,37,38],[11,12,13,14],[11,12,13],[11],[12],[9],[70],[51,64,65],[40,51,55,56],[39,40,45,51,52,55,57,60,61,62,63,66,67,69],[39,68],[39,40,45,51,52,55,56,57,60,61,62,63,64,65,66,67,68,69],[39,69,71,78]],"fileInfos":[{"version":"a7297ff837fcdf174a9524925966429eb8e5feecc2cc55cc06574e6b092c1eaa","impliedFormat":1},{"version":"c430d44666289dae81f30fa7b2edebf186ecc91a2d4c71266ea6ae76388792e1","affectsGlobalScope":true,"impliedFormat":1},{"version":"080941d9f9ff9307f7e27a83bcd888b7c8270716c39af943532438932ec1d0b9","affectsGlobalScope":true,"impliedFormat":1},{"version":"80e18897e5884b6723488d4f5652167e7bb5024f946743134ecc4aa4ee731f89","affectsGlobalScope":true,"impliedFormat":1},{"version":"cd034f499c6cdca722b60c04b5b1b78e058487a7085a8e0d6fb50809947ee573","affectsGlobalScope":true,"impliedFormat":1},{"version":"8e7f8264d0fb4c5339605a15daadb037bf238c10b654bb3eee14208f860a32ea","affectsGlobalScope":true,"impliedFormat":1},{"version":"782dec38049b92d4e85c1585fbea5474a219c6984a35b004963b00beb1aab538","affectsGlobalScope":true,"impliedFormat":1},{"version":"751764bb94219b4ce8f5475dc35d3de2e432fea01a0c9610cd7f69ad05e398c6","impliedFormat":1},{"version":"ee70b8037ecdf0de6c04f35277f253663a536d7e38f1539d270e4e916d225a3f","affectsGlobalScope":true,"impliedFormat":1},{"version":"a660aa95476042d3fdcc1343cf6bb8fdf24772d31712b1db321c5a4dcc325434","impliedFormat":1},{"version":"282f98006ed7fa9bb2cd9bdbe2524595cfc4bcd58a0bb3232e4519f2138df811","impliedFormat":1},{"version":"6222e987b58abfe92597e1273ad7233626285bc2d78409d4a7b113d81a83496b","impliedFormat":1},{"version":"cbe726263ae9a7bf32352380f7e8ab66ee25b3457137e316929269c19e18a2be","impliedFormat":1},{"version":"8b96046bf5fb0a815cba6b0880d9f97b7f3a93cf187e8dcfe8e2792e97f38f87","impliedFormat":99},{"version":"bacf2c84cf448b2cd02c717ad46c3d7fd530e0c91282888c923ad64810a4d511","affectsGlobalScope":true,"impliedFormat":1},{"version":"402e5c534fb2b85fa771170595db3ac0dd532112c8fa44fc23f233bc6967488b","impliedFormat":1},{"version":"52dcc257df5119fb66d864625112ce5033ac51a4c2afe376a0b299d2f7f76e4a","impliedFormat":1},{"version":"e5bab5f871ef708d52d47b3e5d0aa72a08ee7a152f33931d9a60809711a2a9a3","impliedFormat":1},{"version":"e16dc2a81595736024a206c7d5c8a39bfe2e6039208ef29981d0d95434ba8fcf","impliedFormat":1},{"version":"cc4a4903fb698ca1d961d4c10dce658aa3a479faf40509d526f122b044eaf6a4","impliedFormat":1},{"version":"19ee8416e6473ed6c7adb868fa796b5653cf0fa2a337658e677eaa0d134388c3","impliedFormat":1},{"version":"1328ab4e442614b28cdb3d4b414cf68325c0da0dca07287a338d0654b7a00261","impliedFormat":1},{"version":"a039dc21f045919f3cbee2ec13812cc6cc3eebc99dae4be00973230f468d19a6","impliedFormat":1},{"version":"3fbe57af01460e49dcd29df55d6931e1672bc6f1be0fb073d11410bc16f9037d","impliedFormat":1},{"version":"f760be449e8562ec5c09bb5187e8e1eabf3c113c0c58cddda53ef8c69f3e2131","impliedFormat":1},{"version":"44325ed13294fce6ab825b82947bbeed2611db7dad9d9135260192f375e5a189","impliedFormat":1},{"version":"e392e8fb5b514eafc585601c1d781485aa6dd6a320e75daf1064a4c6918a1b45","impliedFormat":1},{"version":"46e4a36e8ddbdfb4e7330e11c81c970dc8b218611df9183d39c41c5f8c653b55","impliedFormat":1},{"version":"370bde134aa8c2abc926d0e99d3a4d5d5dba65c6ee65459137e4f02670cbf841","impliedFormat":1},{"version":"6332f565867cf4a740a70e30f31cefba37ef7cebcf74f22eab8d744fde6d193e","impliedFormat":1},{"version":"2977b7884aedc895a1d0c9c210c7cf3272c29d6959a08a6fa3ff71e0aff08175","impliedFormat":1},{"version":"17f2922d41ddd032830a91371c948cd9ce903b35c95adca72271a54584f19b0b","impliedFormat":1},{"version":"3eed76ede2a1a14d7c9bb0a642041282dcc264811139d3dd275c9fe14efc9840","impliedFormat":1},{"version":"354a7f8e1287d9d6b7561bc97fdd8cbc2f7c1dd79e4cb37b942e8a5cfaff1085","impliedFormat":1},{"version":"8d369483f0c2b9ee388129cfdb6a43bc8112b377e86a41884bd06e19ce04f4c1","impliedFormat":99},{"version":"82e687ebd99518bc63ea04b0c3810fb6e50aa6942decd0ca6f7a56d9b9a212a6","impliedFormat":99},{"version":"7f698624bbbb060ece7c0e51b7236520ebada74b747d7523c7df376453ed6fea","impliedFormat":1},{"version":"8f07f2b6514744ac96e51d7cb8518c0f4de319471237ea10cf688b8d0e9d0225","impliedFormat":1},{"version":"257b83faa134d971c738a6b9e4c47e59bb7b23274719d92197580dd662bfafc3","impliedFormat":99},{"version":"d2e64a6f25013b099e83bfadb2c388d7bef3e8f3fdb25528225bbc841e7e7e3a","impliedFormat":99},{"version":"369ba5259e66ca8c7d35e3234f7a2a0863a770fdb8266505747c65cf346a0804","impliedFormat":99},{"version":"64d984f55025daf604f670b7dfd090ea765f2098aee871174ef2ee3e94479098","impliedFormat":99},{"version":"f147b6710441cf3ec3234adf63b0593ce5e8c9b692959d21d3babc8454bcf743","impliedFormat":99},{"version":"e96d5373a66c2cfbbc7e6642cf274055aa2c7ff6bd37be7480c66faf9804db6d","impliedFormat":99},{"version":"02bcdd7a76c5c1c485cbf05626d24c86ac8f9a1d8dc31f8924108bbaa4cf3ba9","impliedFormat":99},{"version":"c874ab6feac6e0fdf9142727c9a876065777a5392f14b0bbcf869b1e69eb46b5","impliedFormat":99},{"version":"7c553fc9e34773ddbaabe0fa1367d4b109101d0868a008f11042bee24b5a925d","impliedFormat":99},{"version":"9962ce696fbdce2421d883ca4b062a54f982496625437ae4d3633376c5ad4a80","impliedFormat":99},{"version":"e3ea467c4a7f743f3548c9ed61300591965b1d12c08c8bb9aaff8a002ba95fce","impliedFormat":99},{"version":"4c17183a07a63bea2653fbfc0a942b027160ddbee823024789a415f9589de327","impliedFormat":99},{"version":"3e2203c892297ea44b87470fde51b3d48cfe3eeb6901995de429539462894464","impliedFormat":99},{"version":"c84bf7a4abc5e7fdf45971a71b25b0e0d34ccd5e720a866dd78bb71d60d41a3f","impliedFormat":99},{"version":"e01ea380015ed698c3c0e2ccd0db72f3fc3ef1abc4519f122aa1c1a8d419a505","impliedFormat":99},{"version":"5ada1f8a9580c0f7478fe03ae3e07e958f0b79bdfb9dd50eeb98c1324f40011b","impliedFormat":99},{"version":"a8301dc90b4bd9fba333226ee0f1681aeeff1bd90233a8f647e687cb4b7d3521","impliedFormat":99},{"version":"e3225dc0bec183183509d290f641786245e6652bc3dce755f7ef404060693c35","impliedFormat":99},{"version":"09a03870ed8c55d7453bc9ad684df88965f2f770f987481ca71b8a09be5205bc","impliedFormat":99},{"version":"e6233e1c976265e85aa8ad76c3881febe6264cb06ae3136f0257e1eab4a6cc5a","impliedFormat":99},{"version":"2cdd50ddc49e2d608ee848fc4ab0db9a2716624fabb4209c7c683d87e54d79c5","impliedFormat":99},{"version":"e431d664338b8470abb1750d699c7dfcebb1a25434559ef85bb96f1e82de5972","impliedFormat":99},{"version":"2c4254139d037c3caca66ce291c1308c1b5092cfcb151eb25980db932dd3b01a","impliedFormat":99},{"version":"970ae00ed018cb96352dc3f37355ef9c2d9f8aa94d7174ccd6d0ed855e462097","impliedFormat":99},{"version":"d2f8dee457ef7660b604226d471d55d927c3051766bdd80353553837492635c3","impliedFormat":99},{"version":"110a503289a2ef76141ffff3ffceb9a1c3662c32748eb9f6777a2bd0866d6fb1","impliedFormat":99},{"version":"69bf2422313487956e4dacf049f30cb91b34968912058d244cb19e4baa24da97","impliedFormat":99},{"version":"310e6b62c493ce991624169a1c1904015769d947be88dc67e00adc7ebebcfa87","impliedFormat":99},{"version":"62fefda288160bf6e435b21cc03d3fbac11193d8d3bd0e82d86623cca7691c29","impliedFormat":99},{"version":"fcc46a8bcbf9bef21023bba1995160a25f0bc590ca3563ec44c315b4f4c1b18a","impliedFormat":99},{"version":"669573548930fb7d0a0761b827e203dc623581e21febf0be80fb02414f217d74","impliedFormat":99},{"version":"f974db5be6d7428044e19c2848e72495a9b56a8d33b6fcab392e7fb5328eb8df","impliedFormat":99},{"version":"aa348c4fb2f8ac77df855f07fb66281c9f6e71746fdff3b13c7932aa7642b788","impliedFormat":99},{"version":"556ccd493ec36c7d7cb130d51be66e147b91cc1415be383d71da0f1e49f742a9","impliedFormat":1},{"version":"b6d03c9cfe2cf0ba4c673c209fcd7c46c815b2619fd2aad59fc4229aaef2ed43","impliedFormat":1},{"version":"95aba78013d782537cc5e23868e736bec5d377b918990e28ed56110e3ae8b958","impliedFormat":1},{"version":"670a76db379b27c8ff42f1ba927828a22862e2ab0b0908e38b671f0e912cc5ed","impliedFormat":1},{"version":"13b77ab19ef7aadd86a1e54f2f08ea23a6d74e102909e3c00d31f231ed040f62","impliedFormat":1},{"version":"069bebfee29864e3955378107e243508b163e77ab10de6a5ee03ae06939f0bb9","impliedFormat":1},{"version":"26e0ffceb2198feb1ef460d5d14111c69ad07d44c5a67fd4bfeb74c969aa9afb","impliedFormat":99},{"version":"9a04927b52b7f34c22ed35822a79367898ddb8882f75000690b29689175b020c","signature":"4b96dd19fd2949d28ce80e913412b0026dc421e5bf6c31d87c7b5eb11b5753b4"},{"version":"ae77d81a5541a8abb938a0efedf9ac4bea36fb3a24cc28cfa11c598863aba571","impliedFormat":1},{"version":"87d9d29dbc745f182683f63187bf3d53fd8673e5fca38ad5eaab69798ed29fbc","impliedFormat":1},{"version":"eb5b19b86227ace1d29ea4cf81387279d04bb34051e944bc53df69f58914b788","affectsGlobalScope":true,"impliedFormat":1},{"version":"ac51dd7d31333793807a6abaa5ae168512b6131bd41d9c5b98477fc3b7800f9f","impliedFormat":1},{"version":"09ddcfcfbe77a8232d155ca1030005106b1328f6210df43629d0be750da07c16","affectsGlobalScope":true,"impliedFormat":1},{"version":"17ed71200119e86ccef2d96b73b02ce8854b76ad6bd21b5021d4269bec527b5f","impliedFormat":1}],"root":[79],"options":{"allowSyntheticDefaultImports":true,"composite":true,"module":99,"skipLibCheck":true},"referencedMap":[[74,1],[77,2],[73,1],[75,3],[76,1],[85,4],[84,5],[78,6],[51,7],[46,8],[49,9],[64,10],[56,11],[55,12],[67,12],[54,13],[48,14],[50,14],[42,15],[45,16],[61,15],[47,17],[32,18],[30,19],[31,20],[19,21],[20,19],[27,22],[18,23],[23,24],[24,25],[29,26],[35,27],[34,28],[17,29],[25,30],[26,31],[21,32],[28,18],[22,33],[10,34],[9,35],[44,36],[62,37],[59,38],[60,37],[63,39],[39,40],[36,41],[14,42],[12,43],[13,44],[38,45],[71,46],[66,47],[57,48],[68,49],[69,50],[70,51],[79,52]],"latestChangedDtsFile":"./vite.config.d.ts","version":"5.9.3"} \ No newline at end of file +{"fileNames":["./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es5.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.dom.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.webworker.importscripts.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.scripthost.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.decorators.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.decorators.legacy.d.ts","./node_modules/.pnpm/@types+estree@1.0.9/node_modules/@types/estree/index.d.ts","./node_modules/.pnpm/rollup@4.62.2/node_modules/rollup/dist/rollup.d.ts","./node_modules/.pnpm/rollup@4.62.2/node_modules/rollup/dist/parseast.d.ts","./node_modules/.pnpm/vite@5.4.21/node_modules/vite/types/hmrpayload.d.ts","./node_modules/.pnpm/vite@5.4.21/node_modules/vite/types/customevent.d.ts","./node_modules/.pnpm/vite@5.4.21/node_modules/vite/types/hot.d.ts","./node_modules/.pnpm/vite@5.4.21/node_modules/vite/dist/node/types.d-agj9qkwt.d.ts","./node_modules/.pnpm/esbuild@0.21.5/node_modules/esbuild/lib/main.d.ts","./node_modules/.pnpm/source-map-js@1.2.1/node_modules/source-map-js/source-map.d.ts","./node_modules/.pnpm/postcss@8.5.19/node_modules/postcss/lib/previous-map.d.ts","./node_modules/.pnpm/postcss@8.5.19/node_modules/postcss/lib/input.d.ts","./node_modules/.pnpm/postcss@8.5.19/node_modules/postcss/lib/css-syntax-error.d.ts","./node_modules/.pnpm/postcss@8.5.19/node_modules/postcss/lib/declaration.d.ts","./node_modules/.pnpm/postcss@8.5.19/node_modules/postcss/lib/root.d.ts","./node_modules/.pnpm/postcss@8.5.19/node_modules/postcss/lib/warning.d.ts","./node_modules/.pnpm/postcss@8.5.19/node_modules/postcss/lib/lazy-result.d.ts","./node_modules/.pnpm/postcss@8.5.19/node_modules/postcss/lib/no-work-result.d.ts","./node_modules/.pnpm/postcss@8.5.19/node_modules/postcss/lib/processor.d.ts","./node_modules/.pnpm/postcss@8.5.19/node_modules/postcss/lib/result.d.ts","./node_modules/.pnpm/postcss@8.5.19/node_modules/postcss/lib/document.d.ts","./node_modules/.pnpm/postcss@8.5.19/node_modules/postcss/lib/rule.d.ts","./node_modules/.pnpm/postcss@8.5.19/node_modules/postcss/lib/node.d.ts","./node_modules/.pnpm/postcss@8.5.19/node_modules/postcss/lib/comment.d.ts","./node_modules/.pnpm/postcss@8.5.19/node_modules/postcss/lib/container.d.ts","./node_modules/.pnpm/postcss@8.5.19/node_modules/postcss/lib/at-rule.d.ts","./node_modules/.pnpm/postcss@8.5.19/node_modules/postcss/lib/list.d.ts","./node_modules/.pnpm/postcss@8.5.19/node_modules/postcss/lib/postcss.d.ts","./node_modules/.pnpm/postcss@8.5.19/node_modules/postcss/lib/postcss.d.mts","./node_modules/.pnpm/vite@5.4.21/node_modules/vite/dist/node/runtime.d.ts","./node_modules/.pnpm/vite@5.4.21/node_modules/vite/types/importglob.d.ts","./node_modules/.pnpm/vite@5.4.21/node_modules/vite/types/metadata.d.ts","./node_modules/.pnpm/vite@5.4.21/node_modules/vite/dist/node/index.d.ts","./node_modules/.pnpm/@vitest+pretty-format@2.1.9/node_modules/@vitest/pretty-format/dist/index.d.ts","./node_modules/.pnpm/@vitest+utils@2.1.9/node_modules/@vitest/utils/dist/types.d.ts","./node_modules/.pnpm/@vitest+utils@2.1.9/node_modules/@vitest/utils/dist/helpers.d.ts","./node_modules/.pnpm/tinyrainbow@1.2.0/node_modules/tinyrainbow/dist/index-c1cfc5e9.d.ts","./node_modules/.pnpm/tinyrainbow@1.2.0/node_modules/tinyrainbow/dist/node.d.ts","./node_modules/.pnpm/@vitest+utils@2.1.9/node_modules/@vitest/utils/dist/index.d.ts","./node_modules/.pnpm/@vitest+runner@2.1.9/node_modules/@vitest/runner/dist/tasks-3znpj1lr.d.ts","./node_modules/.pnpm/@vitest+utils@2.1.9/node_modules/@vitest/utils/dist/types-bxe-2udy.d.ts","./node_modules/.pnpm/@vitest+utils@2.1.9/node_modules/@vitest/utils/dist/diff.d.ts","./node_modules/.pnpm/@vitest+runner@2.1.9/node_modules/@vitest/runner/dist/types.d.ts","./node_modules/.pnpm/@vitest+utils@2.1.9/node_modules/@vitest/utils/dist/error.d.ts","./node_modules/.pnpm/@vitest+runner@2.1.9/node_modules/@vitest/runner/dist/index.d.ts","./node_modules/.pnpm/vitest@2.1.9_jsdom@25.0.1/node_modules/vitest/dist/chunks/environment.looobwuu.d.ts","./node_modules/.pnpm/@vitest+snapshot@2.1.9/node_modules/@vitest/snapshot/dist/environment-ddx0edty.d.ts","./node_modules/.pnpm/@vitest+snapshot@2.1.9/node_modules/@vitest/snapshot/dist/rawsnapshot-cpnkto81.d.ts","./node_modules/.pnpm/@vitest+snapshot@2.1.9/node_modules/@vitest/snapshot/dist/index.d.ts","./node_modules/.pnpm/@vitest+snapshot@2.1.9/node_modules/@vitest/snapshot/dist/environment.d.ts","./node_modules/.pnpm/vitest@2.1.9_jsdom@25.0.1/node_modules/vitest/dist/chunks/config.cy0c388z.d.ts","./node_modules/.pnpm/vite-node@2.1.9/node_modules/vite-node/dist/trace-mapping.d-dlvdeqop.d.ts","./node_modules/.pnpm/vite-node@2.1.9/node_modules/vite-node/dist/index-z0r8hvru.d.ts","./node_modules/.pnpm/vite-node@2.1.9/node_modules/vite-node/dist/index.d.ts","./node_modules/.pnpm/@vitest+utils@2.1.9/node_modules/@vitest/utils/dist/source-map.d.ts","./node_modules/.pnpm/vite-node@2.1.9/node_modules/vite-node/dist/client.d.ts","./node_modules/.pnpm/vite-node@2.1.9/node_modules/vite-node/dist/server.d.ts","./node_modules/.pnpm/@vitest+runner@2.1.9/node_modules/@vitest/runner/dist/utils.d.ts","./node_modules/.pnpm/tinybench@2.9.0/node_modules/tinybench/dist/index.d.ts","./node_modules/.pnpm/vitest@2.1.9_jsdom@25.0.1/node_modules/vitest/dist/chunks/benchmark.geerunq4.d.ts","./node_modules/.pnpm/@vitest+snapshot@2.1.9/node_modules/@vitest/snapshot/dist/manager.d.ts","./node_modules/.pnpm/vitest@2.1.9_jsdom@25.0.1/node_modules/vitest/dist/chunks/reporters.nr4dxcka.d.ts","./node_modules/.pnpm/vitest@2.1.9_jsdom@25.0.1/node_modules/vitest/dist/chunks/vite.czkp4x9w.d.ts","./node_modules/.pnpm/vitest@2.1.9_jsdom@25.0.1/node_modules/vitest/dist/config.d.ts","./node_modules/.pnpm/vitest@2.1.9_jsdom@25.0.1/node_modules/vitest/config.d.ts","./node_modules/.pnpm/@babel+types@7.29.7/node_modules/@babel/types/lib/index.d.ts","./node_modules/.pnpm/@types+babel__generator@7.27.0/node_modules/@types/babel__generator/index.d.ts","./node_modules/.pnpm/@babel+parser@7.29.7/node_modules/@babel/parser/typings/babel-parser.d.ts","./node_modules/.pnpm/@types+babel__template@7.4.4/node_modules/@types/babel__template/index.d.ts","./node_modules/.pnpm/@types+babel__traverse@7.28.0/node_modules/@types/babel__traverse/index.d.ts","./node_modules/.pnpm/@types+babel__core@7.20.5/node_modules/@types/babel__core/index.d.ts","./node_modules/.pnpm/@vitejs+plugin-react@4.7.0_vite@5.4.21/node_modules/@vitejs/plugin-react/dist/index.d.ts","./package.json","./vite.config.ts","./node_modules/.pnpm/@types+react@18.3.31/node_modules/@types/react/global.d.ts","./node_modules/.pnpm/csstype@3.2.3/node_modules/csstype/index.d.ts","./node_modules/.pnpm/@types+prop-types@15.7.15/node_modules/@types/prop-types/index.d.ts","./node_modules/.pnpm/@types+react@18.3.31/node_modules/@types/react/index.d.ts","./node_modules/.pnpm/@types+react-dom@18.3.7_@types+react@18.3.31/node_modules/@types/react-dom/index.d.ts"],"fileIdsList":[[72],[72,73,74,75,76],[72,74],[84],[81,82,83],[39,69,77],[45,46,48,49,50],[45],[45,46,48],[45,46],[53],[40,53,54],[40,53],[40,47],[41],[40,41,42,44],[40],[31],[29,31],[20,28,29,30,32,34],[18],[21,26,31,34],[17,34],[21,22,25,26,27,34],[21,22,23,25,26,34],[18,19,20,21,22,26,27,28,30,31,32,34],[34],[16,18,19,20,21,22,23,25,26,27,28,29,30,31,32,33],[16,34],[21,23,24,26,27,34],[25,34],[26,27,31,34],[19,29],[9,38],[8,9],[43],[58,59],[58],[39,58,59,69],[9,10,11,12,13,14,15,35,36,37,38],[11,12,13,14],[11,12,13],[11],[12],[9],[70],[51,64,65],[40,51,55,56],[39,40,45,51,52,55,57,60,61,62,63,66,67,69],[39,68],[39,40,45,51,52,55,56,57,60,61,62,63,64,65,66,67,68,69],[39,69,71,78,79]],"fileInfos":[{"version":"a7297ff837fcdf174a9524925966429eb8e5feecc2cc55cc06574e6b092c1eaa","impliedFormat":1},{"version":"c430d44666289dae81f30fa7b2edebf186ecc91a2d4c71266ea6ae76388792e1","affectsGlobalScope":true,"impliedFormat":1},{"version":"080941d9f9ff9307f7e27a83bcd888b7c8270716c39af943532438932ec1d0b9","affectsGlobalScope":true,"impliedFormat":1},{"version":"80e18897e5884b6723488d4f5652167e7bb5024f946743134ecc4aa4ee731f89","affectsGlobalScope":true,"impliedFormat":1},{"version":"cd034f499c6cdca722b60c04b5b1b78e058487a7085a8e0d6fb50809947ee573","affectsGlobalScope":true,"impliedFormat":1},{"version":"8e7f8264d0fb4c5339605a15daadb037bf238c10b654bb3eee14208f860a32ea","affectsGlobalScope":true,"impliedFormat":1},{"version":"782dec38049b92d4e85c1585fbea5474a219c6984a35b004963b00beb1aab538","affectsGlobalScope":true,"impliedFormat":1},{"version":"751764bb94219b4ce8f5475dc35d3de2e432fea01a0c9610cd7f69ad05e398c6","impliedFormat":1},{"version":"ee70b8037ecdf0de6c04f35277f253663a536d7e38f1539d270e4e916d225a3f","affectsGlobalScope":true,"impliedFormat":1},{"version":"a660aa95476042d3fdcc1343cf6bb8fdf24772d31712b1db321c5a4dcc325434","impliedFormat":1},{"version":"282f98006ed7fa9bb2cd9bdbe2524595cfc4bcd58a0bb3232e4519f2138df811","impliedFormat":1},{"version":"6222e987b58abfe92597e1273ad7233626285bc2d78409d4a7b113d81a83496b","impliedFormat":1},{"version":"cbe726263ae9a7bf32352380f7e8ab66ee25b3457137e316929269c19e18a2be","impliedFormat":1},{"version":"8b96046bf5fb0a815cba6b0880d9f97b7f3a93cf187e8dcfe8e2792e97f38f87","impliedFormat":99},{"version":"bacf2c84cf448b2cd02c717ad46c3d7fd530e0c91282888c923ad64810a4d511","affectsGlobalScope":true,"impliedFormat":1},{"version":"402e5c534fb2b85fa771170595db3ac0dd532112c8fa44fc23f233bc6967488b","impliedFormat":1},{"version":"52dcc257df5119fb66d864625112ce5033ac51a4c2afe376a0b299d2f7f76e4a","impliedFormat":1},{"version":"e5bab5f871ef708d52d47b3e5d0aa72a08ee7a152f33931d9a60809711a2a9a3","impliedFormat":1},{"version":"e16dc2a81595736024a206c7d5c8a39bfe2e6039208ef29981d0d95434ba8fcf","impliedFormat":1},{"version":"cc4a4903fb698ca1d961d4c10dce658aa3a479faf40509d526f122b044eaf6a4","impliedFormat":1},{"version":"19ee8416e6473ed6c7adb868fa796b5653cf0fa2a337658e677eaa0d134388c3","impliedFormat":1},{"version":"1328ab4e442614b28cdb3d4b414cf68325c0da0dca07287a338d0654b7a00261","impliedFormat":1},{"version":"a039dc21f045919f3cbee2ec13812cc6cc3eebc99dae4be00973230f468d19a6","impliedFormat":1},{"version":"3fbe57af01460e49dcd29df55d6931e1672bc6f1be0fb073d11410bc16f9037d","impliedFormat":1},{"version":"f760be449e8562ec5c09bb5187e8e1eabf3c113c0c58cddda53ef8c69f3e2131","impliedFormat":1},{"version":"44325ed13294fce6ab825b82947bbeed2611db7dad9d9135260192f375e5a189","impliedFormat":1},{"version":"e392e8fb5b514eafc585601c1d781485aa6dd6a320e75daf1064a4c6918a1b45","impliedFormat":1},{"version":"46e4a36e8ddbdfb4e7330e11c81c970dc8b218611df9183d39c41c5f8c653b55","impliedFormat":1},{"version":"370bde134aa8c2abc926d0e99d3a4d5d5dba65c6ee65459137e4f02670cbf841","impliedFormat":1},{"version":"6332f565867cf4a740a70e30f31cefba37ef7cebcf74f22eab8d744fde6d193e","impliedFormat":1},{"version":"2977b7884aedc895a1d0c9c210c7cf3272c29d6959a08a6fa3ff71e0aff08175","impliedFormat":1},{"version":"17f2922d41ddd032830a91371c948cd9ce903b35c95adca72271a54584f19b0b","impliedFormat":1},{"version":"3eed76ede2a1a14d7c9bb0a642041282dcc264811139d3dd275c9fe14efc9840","impliedFormat":1},{"version":"354a7f8e1287d9d6b7561bc97fdd8cbc2f7c1dd79e4cb37b942e8a5cfaff1085","impliedFormat":1},{"version":"8d369483f0c2b9ee388129cfdb6a43bc8112b377e86a41884bd06e19ce04f4c1","impliedFormat":99},{"version":"82e687ebd99518bc63ea04b0c3810fb6e50aa6942decd0ca6f7a56d9b9a212a6","impliedFormat":99},{"version":"7f698624bbbb060ece7c0e51b7236520ebada74b747d7523c7df376453ed6fea","impliedFormat":1},{"version":"8f07f2b6514744ac96e51d7cb8518c0f4de319471237ea10cf688b8d0e9d0225","impliedFormat":1},{"version":"257b83faa134d971c738a6b9e4c47e59bb7b23274719d92197580dd662bfafc3","impliedFormat":99},{"version":"d2e64a6f25013b099e83bfadb2c388d7bef3e8f3fdb25528225bbc841e7e7e3a","impliedFormat":99},{"version":"369ba5259e66ca8c7d35e3234f7a2a0863a770fdb8266505747c65cf346a0804","impliedFormat":99},{"version":"64d984f55025daf604f670b7dfd090ea765f2098aee871174ef2ee3e94479098","impliedFormat":99},{"version":"f147b6710441cf3ec3234adf63b0593ce5e8c9b692959d21d3babc8454bcf743","impliedFormat":99},{"version":"e96d5373a66c2cfbbc7e6642cf274055aa2c7ff6bd37be7480c66faf9804db6d","impliedFormat":99},{"version":"02bcdd7a76c5c1c485cbf05626d24c86ac8f9a1d8dc31f8924108bbaa4cf3ba9","impliedFormat":99},{"version":"c874ab6feac6e0fdf9142727c9a876065777a5392f14b0bbcf869b1e69eb46b5","impliedFormat":99},{"version":"7c553fc9e34773ddbaabe0fa1367d4b109101d0868a008f11042bee24b5a925d","impliedFormat":99},{"version":"9962ce696fbdce2421d883ca4b062a54f982496625437ae4d3633376c5ad4a80","impliedFormat":99},{"version":"e3ea467c4a7f743f3548c9ed61300591965b1d12c08c8bb9aaff8a002ba95fce","impliedFormat":99},{"version":"4c17183a07a63bea2653fbfc0a942b027160ddbee823024789a415f9589de327","impliedFormat":99},{"version":"3e2203c892297ea44b87470fde51b3d48cfe3eeb6901995de429539462894464","impliedFormat":99},{"version":"c84bf7a4abc5e7fdf45971a71b25b0e0d34ccd5e720a866dd78bb71d60d41a3f","impliedFormat":99},{"version":"e01ea380015ed698c3c0e2ccd0db72f3fc3ef1abc4519f122aa1c1a8d419a505","impliedFormat":99},{"version":"5ada1f8a9580c0f7478fe03ae3e07e958f0b79bdfb9dd50eeb98c1324f40011b","impliedFormat":99},{"version":"a8301dc90b4bd9fba333226ee0f1681aeeff1bd90233a8f647e687cb4b7d3521","impliedFormat":99},{"version":"e3225dc0bec183183509d290f641786245e6652bc3dce755f7ef404060693c35","impliedFormat":99},{"version":"09a03870ed8c55d7453bc9ad684df88965f2f770f987481ca71b8a09be5205bc","impliedFormat":99},{"version":"e6233e1c976265e85aa8ad76c3881febe6264cb06ae3136f0257e1eab4a6cc5a","impliedFormat":99},{"version":"2cdd50ddc49e2d608ee848fc4ab0db9a2716624fabb4209c7c683d87e54d79c5","impliedFormat":99},{"version":"e431d664338b8470abb1750d699c7dfcebb1a25434559ef85bb96f1e82de5972","impliedFormat":99},{"version":"2c4254139d037c3caca66ce291c1308c1b5092cfcb151eb25980db932dd3b01a","impliedFormat":99},{"version":"970ae00ed018cb96352dc3f37355ef9c2d9f8aa94d7174ccd6d0ed855e462097","impliedFormat":99},{"version":"d2f8dee457ef7660b604226d471d55d927c3051766bdd80353553837492635c3","impliedFormat":99},{"version":"110a503289a2ef76141ffff3ffceb9a1c3662c32748eb9f6777a2bd0866d6fb1","impliedFormat":99},{"version":"69bf2422313487956e4dacf049f30cb91b34968912058d244cb19e4baa24da97","impliedFormat":99},{"version":"310e6b62c493ce991624169a1c1904015769d947be88dc67e00adc7ebebcfa87","impliedFormat":99},{"version":"62fefda288160bf6e435b21cc03d3fbac11193d8d3bd0e82d86623cca7691c29","impliedFormat":99},{"version":"fcc46a8bcbf9bef21023bba1995160a25f0bc590ca3563ec44c315b4f4c1b18a","impliedFormat":99},{"version":"669573548930fb7d0a0761b827e203dc623581e21febf0be80fb02414f217d74","impliedFormat":99},{"version":"f974db5be6d7428044e19c2848e72495a9b56a8d33b6fcab392e7fb5328eb8df","impliedFormat":99},{"version":"aa348c4fb2f8ac77df855f07fb66281c9f6e71746fdff3b13c7932aa7642b788","impliedFormat":99},{"version":"556ccd493ec36c7d7cb130d51be66e147b91cc1415be383d71da0f1e49f742a9","impliedFormat":1},{"version":"b6d03c9cfe2cf0ba4c673c209fcd7c46c815b2619fd2aad59fc4229aaef2ed43","impliedFormat":1},{"version":"95aba78013d782537cc5e23868e736bec5d377b918990e28ed56110e3ae8b958","impliedFormat":1},{"version":"670a76db379b27c8ff42f1ba927828a22862e2ab0b0908e38b671f0e912cc5ed","impliedFormat":1},{"version":"13b77ab19ef7aadd86a1e54f2f08ea23a6d74e102909e3c00d31f231ed040f62","impliedFormat":1},{"version":"069bebfee29864e3955378107e243508b163e77ab10de6a5ee03ae06939f0bb9","impliedFormat":1},{"version":"26e0ffceb2198feb1ef460d5d14111c69ad07d44c5a67fd4bfeb74c969aa9afb","impliedFormat":99},{"version":"7e8fba2ecc8d49458ffb23e8099e880efe1539b8180aeda54ee9d513f9bb031c","signature":"16b3cc2375d893e1db7eb58e788f6485f01452ac966a9dd8c9af43bd75fb70f7"},{"version":"f173182d841242dadf59f2e88f55fcf2f8e81f5ce3061e4d05043b1161eee069","signature":"4b96dd19fd2949d28ce80e913412b0026dc421e5bf6c31d87c7b5eb11b5753b4"},{"version":"eb5b19b86227ace1d29ea4cf81387279d04bb34051e944bc53df69f58914b788","affectsGlobalScope":true,"impliedFormat":1},{"version":"ac51dd7d31333793807a6abaa5ae168512b6131bd41d9c5b98477fc3b7800f9f","impliedFormat":1},{"version":"87d9d29dbc745f182683f63187bf3d53fd8673e5fca38ad5eaab69798ed29fbc","impliedFormat":1},{"version":"09ddcfcfbe77a8232d155ca1030005106b1328f6210df43629d0be750da07c16","affectsGlobalScope":true,"impliedFormat":1},{"version":"17ed71200119e86ccef2d96b73b02ce8854b76ad6bd21b5021d4269bec527b5f","impliedFormat":1}],"root":[80],"options":{"allowSyntheticDefaultImports":true,"composite":true,"module":99,"skipLibCheck":true},"referencedMap":[[74,1],[77,2],[73,1],[75,3],[76,1],[85,4],[84,5],[78,6],[51,7],[46,8],[49,9],[64,10],[56,11],[55,12],[67,12],[54,13],[48,14],[50,14],[42,15],[45,16],[61,15],[47,17],[32,18],[30,19],[31,20],[19,21],[20,19],[27,22],[18,23],[23,24],[24,25],[29,26],[35,27],[34,28],[17,29],[25,30],[26,31],[21,32],[28,18],[22,33],[10,34],[9,35],[44,36],[62,37],[59,38],[60,37],[63,39],[39,40],[36,41],[14,42],[12,43],[13,44],[38,45],[71,46],[66,47],[57,48],[68,49],[69,50],[70,51],[80,52]],"latestChangedDtsFile":"./vite.config.d.ts","version":"5.9.3"} \ No newline at end of file diff --git a/vite.config.js b/vite.config.js index c0d1c7a..2ea16d7 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,9 +1,14 @@ /// import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; +import { version } from './package.json'; // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], + define: { + // Injected at build time, used as the Sentry `release` tag (see src/main.tsx). + __APP_VERSION__: JSON.stringify(version), + }, test: { environment: 'jsdom', globals: true, diff --git a/vite.config.ts b/vite.config.ts index b3d7471..9a1d7aa 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,10 +1,15 @@ /// import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; +import { version } from './package.json'; // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], + define: { + // Injected at build time, used as the Sentry `release` tag (see src/main.tsx). + __APP_VERSION__: JSON.stringify(version), + }, test: { environment: 'jsdom', globals: true, From c90300b9a46a22f844f4f79514770d7facd3e1ab Mon Sep 17 00:00:00 2001 From: Sluijsens Date: Sat, 25 Jul 2026 13:46:43 +0200 Subject: [PATCH 04/11] Add temporary Sentry test button (dev/test only) Co-authored-by: Junie --- .gitea/workflows/continuous_integration.yaml | 5 ++ aidlc-docs/features/react-frontend/audit.md | 12 +++++ .../operations/monitoring/monitoring-setup.md | 7 +++ src/components/RootLayout.tsx | 2 + src/components/SentryTestButton.tsx | 47 +++++++++++++++++++ .../__tests__/SentryTestButton.test.tsx | 30 ++++++++++++ src/main.tsx | 2 + src/vite-env.d.ts | 2 + 8 files changed, 107 insertions(+) create mode 100644 src/components/SentryTestButton.tsx create mode 100644 src/components/__tests__/SentryTestButton.test.tsx diff --git a/.gitea/workflows/continuous_integration.yaml b/.gitea/workflows/continuous_integration.yaml index 8bd7bc9..a9af9bf 100644 --- a/.gitea/workflows/continuous_integration.yaml +++ b/.gitea/workflows/continuous_integration.yaml @@ -99,6 +99,11 @@ jobs: # als deze niet is ingesteld, wordt Sentry-logging simpelweg overgeslagen # (zie src/main.tsx) en blijft alleen console-logging actief. VITE_SENTRY_DSN: ${{ vars.VITE_SENTRY_DSN }} + # Build-time tag die bepaalt of dev/test-only UI (zoals de tijdelijke + # SentryTestButton) zichtbaar is; zie src/components/SentryTestButton.tsx. + # Zolang er nog geen aparte productie-build/deploy bestaat, is dit altijd + # gelijk aan DEPLOY_ENVIRONMENT ('test'). + VITE_APP_ENV: ${{ env.DEPLOY_ENVIRONMENT }} - name: Upload build artifact uses: actions/upload-artifact@v3 diff --git a/aidlc-docs/features/react-frontend/audit.md b/aidlc-docs/features/react-frontend/audit.md index 42883b8..b5f0005 100644 --- a/aidlc-docs/features/react-frontend/audit.md +++ b/aidlc-docs/features/react-frontend/audit.md @@ -480,3 +480,15 @@ **Context**: Post-workflow follow-up (feature already marked Complete); continues the `feature/sentry_error_logging` branch with a gap-check against Sentry's own official setup guide. Build, lint and tests re-verified successfully after the change. --- + +## Post-Completion Follow-up: Temporary Sentry Test Button Added (Dev/Test Only) + +**Timestamp**: 2026-07-25T13:41:00Z + +**User Input**: "Ik heb hier een code snippet met een intentionele fout om sentry te kunnen testen. Kan je die ergens toevoegen zodat we tijdelijk een test hebben of misschien iets slims bedenken zodat het alleen in development- en testomgevingen zichtbaar is" (a `Sentry.logger.info` + `Sentry.metrics.count` + throw snippet for a "Break the world" button). + +**AI Response**: "Added `src/components/SentryTestButton.tsx`, mounted globally via `RootLayout.tsx`. Extracted the click logic into an exported `handleSentryTestErrorClick` function for reliable unit testing (React error boundaries don't catch event-handler errors, so the throw is intentionally left uncaught for Sentry's global handler to pick up). Added `enableLogs: true` to `Sentry.init()` in `main.tsx`, required for `Sentry.logger.*` calls. Introduced a new build-time `VITE_APP_ENV` variable (typed in `vite-env.d.ts`, set to `DEPLOY_ENVIRONMENT` i.e. `test` in the `Build` step of `continuous_integration.yaml`) so the button is only visible during local development (`import.meta.env.DEV`) or when explicitly built for the test environment — hidden by default, including in any future production build. Added `SentryTestButton.test.tsx` (2 tests) and updated `monitoring-setup.md` with a new subsection documenting this temporary tool and noting it should be removed once Sentry has been confirmed to receive test errors/logs/metrics end-to-end." + +**Context**: Post-workflow follow-up (feature already marked Complete); continues the `feature/sentry_error_logging` branch. `pnpm run build`, `pnpm run lint` (0 errors, pre-existing-style warnings only) and `pnpm run test` (16/16 tests) all succeeded. Changes not yet committed/pushed. + +--- diff --git a/aidlc-docs/features/react-frontend/operations/monitoring/monitoring-setup.md b/aidlc-docs/features/react-frontend/operations/monitoring/monitoring-setup.md index 8672604..15bdd54 100644 --- a/aidlc-docs/features/react-frontend/operations/monitoring/monitoring-setup.md +++ b/aidlc-docs/features/react-frontend/operations/monitoring/monitoring-setup.md @@ -27,6 +27,13 @@ Both the console and Sentry are now active (original Question 3 resolved as a co **Log level strategy**: only errors are logged (no verbose/info-level client logging) — this is a static site with no meaningful "business events" beyond page views, which are covered by analytics (see Dashboards below), not logging. +### Temporary manual test tool: `SentryTestButton` +- `src/components/SentryTestButton.tsx` renders a "Break the world" button, mounted globally via `RootLayout.tsx`, used to manually verify that errors, logs (`Sentry.logger.info`), and metrics (`Sentry.metrics.count`) actually arrive in Sentry end-to-end. +- **Visibility**: only shown during local development (`pnpm dev`, via Vite's `import.meta.env.DEV`) and in the test environment (via the new build-time `VITE_APP_ENV` variable, set to `test` by `continuous_integration.yaml`'s `Build` step). It is hidden by default (including in any future production build) unless one of those conditions is explicitly true. +- `Sentry.logger.*` requires `enableLogs: true` in `Sentry.init()` (`src/main.tsx`) — added specifically to support this test button (and any future structured logging). +- The thrown error is **intentionally uncaught**: React error boundaries do not catch errors thrown from event handlers (only render/lifecycle errors), so this relies on Sentry's own global `window.onerror` handler, exactly like the official Sentry test snippet. +- This is a temporary verification tool, not a permanent feature — remove `SentryTestButton` (and its usage in `RootLayout.tsx`) once Sentry has been confirmed to receive test errors/logs/metrics end-to-end. + ## Dashboards ### Website analytics diff --git a/src/components/RootLayout.tsx b/src/components/RootLayout.tsx index 9f82fa4..908b24c 100644 --- a/src/components/RootLayout.tsx +++ b/src/components/RootLayout.tsx @@ -1,6 +1,7 @@ import type { ReactNode } from 'react'; import { Nav } from './Nav'; import { Footer } from './Footer'; +import { SentryTestButton } from './SentryTestButton'; export function RootLayout({ children }: { children: ReactNode }) { return ( @@ -8,6 +9,7 @@ export function RootLayout({ children }: { children: ReactNode }) {