diff --git a/.env.example b/.env.example index 36c5d22..5459364 100644 --- a/.env.example +++ b/.env.example @@ -8,3 +8,10 @@ # Sentry-project aan op https://sentry.io en plak hier de DSN van dat project. # Laat leeg/weg om Sentry lokaal uit te schakelen (alleen console-logging blijft actief). VITE_SENTRY_DSN= + +# Self-hosted Umami analytics (zie operations/monitoring/umami-setup.md). +# Geen van beide is een secret (client-side zichtbaar in de pagina-broncode). +# Laat leeg/weg om analytics lokaal uit te schakelen; in `pnpm dev` wordt het +# script sowieso nooit geladen, ongeacht deze waarden (zie UmamiAnalytics.tsx). +VITE_UMAMI_SCRIPT_URL=https://analytics.slpsoftware.nl/script.js +VITE_UMAMI_WEBSITE_ID= diff --git a/.gitea/workflows/continuous_integration.yaml b/.gitea/workflows/continuous_integration.yaml index a9af9bf..f640f25 100644 --- a/.gitea/workflows/continuous_integration.yaml +++ b/.gitea/workflows/continuous_integration.yaml @@ -92,8 +92,9 @@ jobs: run: pnpm install --frozen-lockfile - name: Build - run: pnpm run build env: + VITE_UMAMI_SCRIPT_URL: ${{ vars.VITE_UMAMI_SCRIPT_URL }} + VITE_UMAMI_WEBSITE_ID: ${{ vars.VITE_UMAMI_WEBSITE_ID }} # 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 @@ -104,6 +105,7 @@ jobs: # Zolang er nog geen aparte productie-build/deploy bestaat, is dit altijd # gelijk aan DEPLOY_ENVIRONMENT ('test'). VITE_APP_ENV: ${{ env.DEPLOY_ENVIRONMENT }} + run: pnpm run build - name: Upload build artifact uses: actions/upload-artifact@v3 diff --git a/aidlc-docs/features/react-frontend/aidlc-state.md b/aidlc-docs/features/react-frontend/aidlc-state.md index 3a09376..0ac318a 100644 --- a/aidlc-docs/features/react-frontend/aidlc-state.md +++ b/aidlc-docs/features/react-frontend/aidlc-state.md @@ -6,7 +6,7 @@ - **Project Type**: Greenfield - **Start Date**: 2026-07-18T00:37:00Z - **Current Stage**: Complete (post-completion follow-ups tracked in `audit.md` / `operations/production-readiness-checklist.md`) -- **Branch**: feature/sentry_error_logging +- **Branch**: feature/analytics_uptime_dashboard ## Workspace State - **Existing Code**: No diff --git a/aidlc-docs/features/react-frontend/audit.md b/aidlc-docs/features/react-frontend/audit.md index b4ba3db..0f2a59f 100644 --- a/aidlc-docs/features/react-frontend/audit.md +++ b/aidlc-docs/features/react-frontend/audit.md @@ -564,3 +564,75 @@ **Context**: Closes out the Sentry logging-destination follow-up. Three open follow-ups remain per `production-readiness-checklist.md`: production hosting/deploy, analytics/uptime dashboard tool selection, and a formal Lighthouse performance check. --- + +## Post-Completion Follow-up: Analytics + Uptime Dashboard Tools Decided (Umami self-hosted + UptimeRobot) + +**Timestamp**: 2026-07-25T16:10:00Z + +**User Input**: "Analytics/uptime dashboard" open item chosen next, after closing the Sentry logging follow-up on its own branch. Clarified: Umami (self-hosted, needs Podman help), UptimeRobot, hosted on the webserver Pi (Pi Main), dashboard reachable via `analytics.slpsoftware.nl`, Podman already available on that Pi. + +**AI Response**: "Created feature branch `feature/analytics_uptime_dashboard` from `master` (independent of the not-yet-merged `feature/sentry_error_logging` branch). Added `src/components/UmamiAnalytics.tsx`, which injects the Umami tracking script client-side, gated on `VITE_UMAMI_SCRIPT_URL`/`VITE_UMAMI_WEBSITE_ID` (typed in a new `src/vite-env.d.ts`) and skipped entirely during local development, mirroring the existing Sentry-DSN pattern; mounted it in `RootLayout.tsx`, with unit tests (`UmamiAnalytics.test.tsx`, 4 tests). Wired both variables into `continuous_integration.yaml`'s `Build` step as Gitea repository variables (not secrets). Added a self-hosted Umami example setup: `operations/deployment/umami/podman-compose.yml.example` + `.env.example` (Umami + Postgres, bound to the Pi's internal IP only) and `operations/deployment/nginx/analytics-nginx.conf.example` (reverse-proxy Pi forwarding `analytics.slpsoftware.nl` to Pi Main's Umami port, with certbot SSL, same pattern as the test-environment config). Wrote a new step-by-step `operations/monitoring/umami-setup.md` covering container setup, systemd auto-start (rootless Podman + `loginctl enable-linger`), reverse proxy/SSL, website registration in the Umami UI, and wiring the resulting website ID into the build. Updated `monitoring-plan.md`, `monitoring-setup.md`, and `production-readiness-checklist.md` to mark the analytics/uptime tool-selection open item as resolved, listing the remaining manual deployment steps." + +**Context**: Post-workflow follow-up (feature already marked Complete); addresses the "Analytics/uptime dashboard" open item from `production-readiness-checklist.md`. Work done on `feature/analytics_uptime_dashboard`. `pnpm run build`, `pnpm run lint`, and `pnpm run test` to be verified before committing. + +--- + +## Post-Completion Follow-up: Umami Moved to a Dedicated `umami` User (Podman Isolation Pattern) + +**Timestamp**: 2026-07-25T21:20:00Z + +**User Input**: "Ja, ik wil wel meer de kant op gaan van de verschillende self-hosted services als losse users. Maakt mij flexibeler als ik later meer wil ookal is een raspberry pi beperkt in rekenkracht" (confirming, after a prior explanation, the preference for a dedicated Linux user per self-hosted service instead of a shared folder under the existing user account). + +**AI Response**: "Reworked `operations/monitoring/umami-setup.md` to run Umami under a new dedicated `umami` Linux user instead of the existing main user account: added a new "Waarom een dedicated user" section explaining the rootless-Podman isolation rationale, and a new step 0 covering `useradd --create-home`, verifying/adding a subuid/subgid range (`/etc/subuid`/`/etc/subgid`), and `loginctl enable-linger umami` so rootless Podman services keep running without an active login. Updated step 1 (container setup) and step 2 (systemd auto-start) to run as the `umami` user (`sudo -iu umami`), with paths now under `/home/umami/umami/`. Extended the outstanding manual steps list with the new user-creation step, and added a closing note documenting this as a reusable pattern for future self-hosted services on the same or other Pi's." + +**Context**: Post-workflow follow-up (feature already marked Complete); continues the `feature/analytics_uptime_dashboard` branch, refining the Umami deployment approach before it has been executed on the Pi. Documentation-only change (no code/build impact). + +--- + +## Post-Completion Follow-up: Umami External Port Changed (3000 → 3001) to Avoid Conflict with Gitea + +**Timestamp**: 2026-07-25T22:39:00Z + +**User Input**: "de poort 3000 is la in gebruik. volgens mij door gitea: Error: unable to start container ...: rootlessport listen tcp 192.168.1.103:3000: bind: address already in use" + +**AI Response**: "Confirmed the reported cause: Gitea already occupies port 3000 system-wide on the same Pi, so even though Umami's `podman-compose.yml.example` binds specifically to `192.168.1.103:3000` (not `0.0.0.0`), it still conflicts because Gitea's own binding already covers that IP/port combination. Changed the external port mapping to `192.168.1.103:3001:3000` (container still listens internally on 3000), and updated all references accordingly: `nginx/analytics-nginx.conf.example` (`proxy_pass` target + comments), `monitoring-setup.md` (decision summary), and `umami-setup.md` (heartbeat-check `curl` command, with an explanatory note)." + +**Context**: Post-workflow follow-up (feature already marked Complete); continues the `feature/analytics_uptime_dashboard` branch, fixing a port conflict discovered before the Umami deployment has been executed on the Pi. Documentation/example-config-only change (no code/build impact). + +--- + +## Post-Completion Follow-up: Clarified `podman-compose down`/`up -d` Needed After Config Changes to an Already-Created Container + +**Timestamp**: 2026-07-25T22:57:00Z + +**User Input**: "maakt het uit dat ik al podman-compose heb uitgevoerd? moet ik nu eerst de container verwijderen?" (asking whether it matters that `podman-compose up -d` had already been run with the old port-3000 config, before the port-3001 fix). + +**AI Response**: "Explained that a port mapping is set at container-creation time and is not live-reloaded by `podman-compose up -d` on an already-existing container, so after updating the local `podman-compose.yml` to the new port, a `podman-compose down` followed by `podman-compose up -d` is required to recreate the container with the corrected mapping (database data is preserved via the persistent volume). Added a callout to `umami-setup.md` documenting this `down`/`up` requirement for future config changes (e.g. another port change), so this is not only explained in chat but also captured for later reference." + +**Context**: Post-workflow follow-up (feature already marked Complete); continues the `feature/analytics_uptime_dashboard` branch, clarifying the deployment steps before the Umami container has successfully started on the Pi. Documentation-only change (no code/build impact). + +--- + +## Post-Completion Follow-up: ufw Firewall Rule Missing for Port 3001 (Connection Reset by Peer) + +**Timestamp**: 2026-07-25T23:05:00Z + +**User Input**: "ik krijg nu: curl http://192.168.1.103:3001/api/heartbeat curl: (56) Recv failure: Connection reset by peer" + +**AI Response**: "Diagnosed via `podman ps`/`podman logs`/`ss -tlnp` that the Umami container and rootlessport forwarder were both healthy and correctly listening on `192.168.1.103:3001`, ruling out an application/container issue. Root cause found in `sudo ufw status verbose`: an explicit `ALLOW` rule exists for `3000/tcp` from `192.168.1.102` (the reverse-proxy Pi), but no equivalent rule exists for `3001/tcp`, and ufw's default incoming policy is `deny` — so the request is blocked even when curled from Pi Main itself, since it targets the real host IP rather than loopback. Added a new firewall step to `umami-setup.md` (`sudo ufw allow from 192.168.1.102 to any port 3001 proto tcp` + `ufw reload`), inserted before the heartbeat-check step, plus a troubleshooting tip to test via `localhost:3001` (bypasses the firewall) to isolate firewall vs. container issues. Added the missing ufw rule to the outstanding manual steps list." + +**Context**: Post-workflow follow-up (feature already marked Complete); continues the `feature/analytics_uptime_dashboard` branch, resolving a connectivity blocker discovered while the user was testing the Umami container on Pi Main. Documentation-only change (no code/build impact); the actual `ufw allow` command still needs to be run by the user on Pi Main. + +--- + +## Post-Completion Follow-up: "Failed to connect to bus: No medium found" When Setting Up the systemd User Service + +**Timestamp**: 2026-07-25T23:18:00Z + +**User Input**: "ik krijg het volgende al sik de service probeer aan te maken en deamon reload probeer te doen: systemctl --user daemon-reload Failed to connect to bus: No medium found" + +**AI Response**: "Explained that this error means no D-Bus/systemd user session exists yet for the `umami` user, most likely because `sudo -iu umami` does not register a full PAM login session the way an interactive login does — so `/run/user/` (and the bus socket inside it) is missing, even with linger already enabled. Added a troubleshooting callout to `umami-setup.md` right before the systemd-service step: verify `Linger=yes` via `loginctl show-user umami`, re-run `loginctl enable-linger umami` if not, explicitly start `user@.service`, re-login and export `XDG_RUNTIME_DIR` manually, with `machinectl shell umami@ /bin/bash` offered as a more robust alternative to `sudo -iu`/`su -` if the issue persists." + +**Context**: Post-workflow follow-up (feature already marked Complete); continues the `feature/analytics_uptime_dashboard` branch, resolving a blocker discovered while the user was setting up the systemd auto-start service on Pi Main. Documentation-only change (no code/build impact); the user still needs to apply the fix on Pi Main. + +--- 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 fb207af..bf6c87b 100644 --- a/aidlc-docs/features/react-frontend/operations/deployment/deployment-instructions.md +++ b/aidlc-docs/features/react-frontend/operations/deployment/deployment-instructions.md @@ -39,8 +39,8 @@ Deze secrets heten `PI_MAIN_*` (niet `PI_TEST_*`), omdat dezelfde Pi (Pi Main) e ## 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. 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). +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 is de versie van vóór certbot (alleen poort 80, geen SSL), 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 herschrijft dit bestand automatisch met de HTTPS-configuratie en de HTTP→HTTPS-redirect — zie `operations/deployment/nginx/reverse-proxy-nginx.conf.post-certbot.example` voor hoe het er dan uitziet (referentie, niet zelf kopiëren). 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`). diff --git a/aidlc-docs/features/react-frontend/operations/deployment/nginx/analytics-nginx.conf.example b/aidlc-docs/features/react-frontend/operations/deployment/nginx/analytics-nginx.conf.example new file mode 100644 index 0000000..3cfc57c --- /dev/null +++ b/aidlc-docs/features/react-frontend/operations/deployment/nginx/analytics-nginx.conf.example @@ -0,0 +1,47 @@ +# Voorbeeldconfiguratie voor de nginx reverse proxy op de reverse-proxy-Pi +# (dezelfde Pi als reverse-proxy-nginx.conf.example) om het zelf-gehoste +# Umami-analyticsdashboard bereikbaar te maken via analytics.slpsoftware.nl. +# +# Umami zelf draait als podman-container op de webserver-Pi (Pi Main, +# 192.168.1.103:3001 — zie ../umami/podman-compose.yml.example) en heeft dus +# GEEN eigen nginx-configuratie op die Pi nodig; Umami serveert zijn eigen +# dashboard + tracking-script rechtstreeks via die poort. Let op: de externe +# poort is bewust 3001 (niet het gebruikelijke 3000), omdat Gitea op diezelfde +# Pi al poort 3000 systeembreed bezet. +# +# Dit is de versie die je gebruikt VOORDAT certbot gedraaid heeft: alleen +# poort 80, geen SSL. Certbot heeft dit HTTP-server-block namelijk nodig om +# de ACME-challenge te kunnen afhandelen en zal, zodra je hem draait, dit +# bestand zelf herschrijven om er de HTTPS-configuratie en de HTTP→HTTPS- +# redirect aan toe te voegen. Zie analytics-nginx.conf.post-certbot.example +# voor hoe het bestand er na die stap uit gaat zien (puur ter referentie — +# dat bestand hoef je niet zelf te kopiëren, certbot genereert het). +# +# Kopieer dit bestand handmatig naar bijvoorbeeld +# /etc/nginx/sites-available/slpsoftware-analytics.conf op de reverse-proxy-Pi, +# maak een symlink in sites-enabled, herlaad nginx, en draai dan pas certbot: +# sudo certbot --nginx -d analytics.slpsoftware.nl +# Zorg dat het DNS-record voor analytics.slpsoftware.nl al naar het publieke +# IP van deze Pi wijst voordat je certbot draait. +server { + listen 80; + listen [::]:80; + + server_name analytics.slpsoftware.nl; + + error_log /var/log/nginx/slpsoftware-analytics_error.log; + access_log /var/log/nginx/slpsoftware-analytics_access.log; + + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + location / { + proxy_pass http://192.168.1.103:3001; + + 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/analytics-nginx.conf.post-certbot.example b/aidlc-docs/features/react-frontend/operations/deployment/nginx/analytics-nginx.conf.post-certbot.example new file mode 100644 index 0000000..0aaee86 --- /dev/null +++ b/aidlc-docs/features/react-frontend/operations/deployment/nginx/analytics-nginx.conf.post-certbot.example @@ -0,0 +1,50 @@ +# REFERENTIE ALLEEN — dit bestand hoef je niet handmatig te kopiëren. +# +# Dit toont hoe /etc/nginx/sites-available/slpsoftware-analytics.conf op de +# reverse-proxy-Pi er automatisch uit komt te zien NADAT je certbot hebt +# gedraaid (`sudo certbot --nginx -d analytics.slpsoftware.nl`) op basis van +# analytics-nginx.conf.example. Certbot voegt zelf de HTTPS-configuratie en +# het HTTP→HTTPS-redirect-blok toe (herkenbaar aan de "managed by Certbot" +# commentaren), en zet de error_log/access_log en het proxy_pass-blok gewoon +# over naar het nieuwe HTTPS-serverblok. +server { + + server_name analytics.slpsoftware.nl; + + error_log /var/log/nginx/slpsoftware-analytics_error.log; + access_log /var/log/nginx/slpsoftware-analytics_access.log; + + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + location / { + proxy_pass http://192.168.1.103:3001; + + 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/analytics.slpsoftware.nl/fullchain.pem; # managed by Certbot + ssl_certificate_key /etc/letsencrypt/live/analytics.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 = analytics.slpsoftware.nl) { + return 301 https://$host$request_uri; + } # managed by Certbot + + + listen 80; + listen [::]:80; + + server_name analytics.slpsoftware.nl; + return 404; # managed by Certbot +} 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 6c53633..8eca6af 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 @@ -3,21 +3,21 @@ # doorstuurt naar de webserver-Pi op 192.168.1.103:80) — voor de TESTOMGEVING, # bereikbaar via het domein test.slpsoftware.nl. # -# Kopieer dit bestand handmatig naar bijvoorbeeld -# /etc/nginx/sites-available/slpsoftware-test.conf op de reverse-proxy-Pi, -# maak een symlink in sites-enabled, en herlaad nginx. +# Dit is de versie die je gebruikt VOORDAT certbot gedraaid heeft: alleen +# poort 80, geen SSL. Certbot heeft dit HTTP-server-block namelijk nodig om +# de ACME-challenge te kunnen afhandelen en zal, zodra je hem draait, dit +# bestand zelf herschrijven om er de HTTPS-configuratie en de HTTP→HTTPS- +# redirect aan toe te voegen. Zie reverse-proxy-nginx.conf.post-certbot.example +# voor hoe het bestand er na die stap uit gaat zien (puur ter referentie — +# dat bestand hoef je niet zelf te kopiëren, certbot genereert het). # -# SSL-certificaat: net als voor de productiesite wordt dit aangevraagd met -# certbot (Let's Encrypt) rechtstreeks op deze reverse-proxy-Pi, bijvoorbeeld: +# Kopieer dit bestand handmatig naar +# /etc/nginx/sites-available/slpsoftware-test.conf op de reverse-proxy-Pi, +# maak een symlink in sites-enabled, herlaad nginx, en draai dan pas certbot: # 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. # -# 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`- @@ -25,6 +25,8 @@ # 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; @@ -60,25 +62,4 @@ server { 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/reverse-proxy-nginx.conf.post-certbot.example b/aidlc-docs/features/react-frontend/operations/deployment/nginx/reverse-proxy-nginx.conf.post-certbot.example new file mode 100644 index 0000000..370c630 --- /dev/null +++ b/aidlc-docs/features/react-frontend/operations/deployment/nginx/reverse-proxy-nginx.conf.post-certbot.example @@ -0,0 +1,55 @@ +# REFERENTIE ALLEEN — dit bestand hoef je niet handmatig te kopiëren. +# +# Dit toont hoe /etc/nginx/sites-available/slpsoftware-test.conf op de +# reverse-proxy-Pi er automatisch uit komt te zien NADAT je certbot hebt +# gedraaid (`sudo certbot --nginx -d test.slpsoftware.nl`) op basis van +# reverse-proxy-nginx.conf.example. Certbot voegt zelf de HTTPS-configuratie +# en het HTTP→HTTPS-redirect-blok toe (herkenbaar aan de "managed by Certbot" +# commentaren), en zet de error_log/access_log en overige location-blocks +# gewoon over naar het nieuwe HTTPS-serverblok. +server { + + 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 /sentry-tunnel { + proxy_pass https://o4511795618185216.ingest.de.sentry.io/api/4511795622838352/envelope/; + proxy_set_header Host o4511795618185216.ingest.de.sentry.io; + proxy_ssl_server_name on; + } + + 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 +} 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 70b5dbe..5b4831a 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 @@ -16,6 +16,9 @@ server { server_name test.slpsoftware.nl; + error_log /var/log/nginx/slpsoftware-test-webserver_error.log; + access_log /var/log/nginx/slpsoftware-test-webserver_access.log; + root /mnt/storage1/www/html/test/slpsoftware; index index.html; diff --git a/aidlc-docs/features/react-frontend/operations/deployment/umami/.env.example b/aidlc-docs/features/react-frontend/operations/deployment/umami/.env.example new file mode 100644 index 0000000..64a5d50 --- /dev/null +++ b/aidlc-docs/features/react-frontend/operations/deployment/umami/.env.example @@ -0,0 +1,10 @@ +# Kopieer dit bestand naar `.env` naast podman-compose.yml op de Pi (NIET committen — +# bevat straks echte wachtwoorden/secrets). + +# Willekeurig, sterk wachtwoord voor de Umami PostgreSQL-database. +# Genereren met bv.: openssl rand -base64 32 +POSTGRES_PASSWORD= + +# Willekeurige, sterke random string die Umami gebruikt om sessies te ondertekenen. +# Genereren met bv.: openssl rand -base64 32 +APP_SECRET= diff --git a/aidlc-docs/features/react-frontend/operations/deployment/umami/podman-compose.yml.example b/aidlc-docs/features/react-frontend/operations/deployment/umami/podman-compose.yml.example new file mode 100644 index 0000000..f48537a --- /dev/null +++ b/aidlc-docs/features/react-frontend/operations/deployment/umami/podman-compose.yml.example @@ -0,0 +1,58 @@ +# Voorbeeld podman-compose bestand om Umami (self-hosted analytics) te draaien +# op de webserver-Pi (Pi Main, 192.168.1.103), naast de al draaiende nginx die +# de website zelf serveert. +# +# Vereist: podman + podman-compose (`pip3 install --user podman-compose`) op de Pi. +# +# Gebruik: +# 1. Kopieer dit bestand naar bv. ~/umami/podman-compose.yml op de Pi. +# 2. Kopieer .env.example naast dit bestand naar .env en vul een echte +# POSTGRES_PASSWORD en APP_SECRET in (zie .env.example voor hoe je die genereert). +# 3. cd ~/umami && podman-compose up -d +# 4. Zie umami-setup.md voor het instellen van auto-start na reboot (systemd) +# en het aanmaken van een website in de Umami-UI om het website-ID te krijgen. + +services: + umami: + image: docker.umami.is/umami-software/umami:postgresql-latest + # Alleen op het interne (LAN) IP van deze Pi binden, niet op 0.0.0.0 — zelfde + # aanpak als webserver-nginx.conf.example, zodat deze poort niet per ongeluk + # vanaf buiten het netwerk bereikbaar is. Externe poort is 3001 (i.p.v. het + # gebruikelijke 3000), omdat Gitea op dezelfde Pi al poort 3000 systeembreed + # bezet (ook op dit interne IP, ongeacht dat Umami een specifiek IP opgeeft). + # De container zelf luistert intern nog gewoon op 3000. De reverse-proxy-Pi + # stuurt analytics.slpsoftware.nl door naar 192.168.1.103:3001 (zie + # ../nginx/analytics-nginx.conf.example). + ports: + - "192.168.1.103:3001:3000" + environment: + DATABASE_URL: postgresql://umami:${POSTGRES_PASSWORD}@db:5432/umami + DATABASE_TYPE: postgresql + APP_SECRET: ${APP_SECRET} + depends_on: + db: + condition: service_healthy + restart: always + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:3000/api/heartbeat"] + interval: 10s + timeout: 5s + retries: 5 + + db: + image: docker.io/postgres:15-alpine + environment: + POSTGRES_DB: umami + POSTGRES_USER: umami + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + volumes: + - umami-db-data:/var/lib/postgresql/data + restart: always + healthcheck: + test: ["CMD-SHELL", "pg_isready -U umami -d umami"] + interval: 10s + timeout: 5s + retries: 5 + +volumes: + umami-db-data: 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 9f8285d..11e4bf0 100644 --- a/aidlc-docs/features/react-frontend/operations/monitoring/monitoring-plan.md +++ b/aidlc-docs/features/react-frontend/operations/monitoring/monitoring-plan.md @@ -17,8 +17,8 @@ Client-side errors (JavaScript crashes, broken links) are logged. **Decided**: S ### Dashboards A combination of: -- **Website analytics** (visitors, page views, basic engagement) — e.g. a simple/free tool such as Plausible, Umami, or Google Analytics/Search Console. -- **Uptime dashboard** (site reachability) — e.g. an external monitoring service such as UptimeRobot or Better Uptime. +- **Website analytics**: **decided** — self-hosted Umami (Podman on the webserver Pi, see `operations/monitoring/umami-setup.md`), chosen over GA4/hosted Plausible for privacy-friendliness and no recurring cost. +- **Uptime dashboard**: **decided** — UptimeRobot, chosen over Better Uptime for its established free tier. (Original Question 6 = C, "Both (analytics + uptime dashboard)".) @@ -28,7 +28,7 @@ A combination of: ## Open Action Items 1. ~~Decide logging destination~~ — **Resolved and verified**: Sentry free tier, wired into `ErrorBoundary`/`main.tsx` (see `monitoring-setup.md`), including tracing, environment/release tags, and a tunnel to bypass ad-blocker blocking. The user confirmed errors, logs, and metrics are received correctly, both locally and on the test environment. The Gitea Actions variable `VITE_SENTRY_DSN` still needs to be created by the user with a real Sentry project DSN for the test/production build; until then that build simply skips Sentry initialization and only console-logging is active (local development already has its own DSN via `.env.local`). -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). +2. ~~Pick concrete analytics + uptime tools~~ — **Resolved**: self-hosted Umami + UptimeRobot (see `monitoring-setup.md` and `umami-setup.md`). Remaining manual follow-ups: actually deploying the Umami containers on the Pi, registering DNS/SSL for `analytics.slpsoftware.nl`, creating the UptimeRobot monitor, and setting the `VITE_UMAMI_SCRIPT_URL`/`VITE_UMAMI_WEBSITE_ID` Gitea repository variables. ## Rationale Given this is a simple static marketing site with no backend and no existing monitoring, the aim is lightweight, low/no-cost observability: enough to know if the site is down (uptime) and how it's being used (analytics), plus a documented (if not yet finalized) path for capturing client-side errors. Alerting was deliberately left out to avoid over-engineering a notification pipeline before there's a concrete trigger/audience for it. 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 60f7594..eaeb7f9 100644 --- a/aidlc-docs/features/react-frontend/operations/monitoring/monitoring-setup.md +++ b/aidlc-docs/features/react-frontend/operations/monitoring/monitoring-setup.md @@ -39,38 +39,50 @@ Both the console and Sentry are now active (original Question 3 resolved as a co ## Dashboards -### Website analytics -Pick one (all have generous free tiers suitable for a small marketing site): +### Website analytics — decided: self-hosted Umami +**Decided**: self-hosted Umami, running via Podman on the webserver Pi (Pi Main, +`192.168.1.103:3001` — note: not the default 3000, since Gitea already occupies +that port on the same Pi), reachable for the dashboard itself via `analytics.slpsoftware.nl` +(reverse-proxied + SSL via certbot, same pattern as `test.slpsoftware.nl`). Chosen over +GA4 for privacy-friendliness (typically no cookie banner needed) and full self-hosted +control, and over a hosted Umami/Plausible plan to avoid recurring cost. -| Option | Notes | -|---|---| -| Plausible / Umami | Privacy-friendly, lightweight, no cookie banner typically required; self-hosted or low-cost hosted tier | -| Google Analytics (GA4) / Search Console | Free, widely known, but heavier script and involves third-party data sharing (cookie/consent implications) | +- Full step-by-step setup: `operations/monitoring/umami-setup.md` (Podman/compose files, + systemd auto-start, reverse proxy + SSL, website registration). +- Example config: `operations/deployment/umami/podman-compose.yml.example` + + `.env.example`; `operations/deployment/nginx/analytics-nginx.conf.example`. +- The tracking script is injected client-side by `src/components/UmamiAnalytics.tsx`, + gated on two build-time variables (`VITE_UMAMI_SCRIPT_URL`, `VITE_UMAMI_WEBSITE_ID`), + wired into `continuous_integration.yaml`'s `Build` step as Gitea repository variables, + not secrets (same pattern as `VITE_SENTRY_DSN`). It never loads during local + development (`pnpm dev`), even if those variables happen to be set, so local testing + never pollutes visitor analytics. +- Key metrics to surface: unique visitors, page views per route (Home, Packages, etc. — + see `frontend-components.md`), and referral sources. -**Setup (once a tool is picked)**: -1. Create an account/site entry with the chosen provider and obtain the tracking snippet or `