From 08b489e3058b199140a990ebab4e7a8162b967f6 Mon Sep 17 00:00:00 2001 From: Sluijsens Date: Sat, 25 Jul 2026 19:59:40 +0200 Subject: [PATCH] Voeg self-hosted Umami analytics + UptimeRobot uptime dashboard toe Co-authored-by: Junie --- .env.example | 11 ++ .gitea/workflows/continuous_integration.yaml | 3 + .../features/react-frontend/aidlc-state.md | 4 +- aidlc-docs/features/react-frontend/audit.md | 12 ++ .../nginx/analytics-nginx.conf.example | 57 +++++++++ .../operations/deployment/umami/.env.example | 10 ++ .../umami/podman-compose.yml.example | 55 +++++++++ .../operations/monitoring/monitoring-plan.md | 6 +- .../operations/monitoring/monitoring-setup.md | 57 +++++---- .../operations/monitoring/umami-setup.md | 110 ++++++++++++++++++ .../production-readiness-checklist.md | 2 +- src/components/RootLayout.tsx | 2 + src/components/UmamiAnalytics.tsx | 43 +++++++ .../__tests__/UmamiAnalytics.test.tsx | 50 ++++++++ src/vite-env.d.ts | 12 ++ 15 files changed, 405 insertions(+), 29 deletions(-) create mode 100644 .env.example create mode 100644 aidlc-docs/features/react-frontend/operations/deployment/nginx/analytics-nginx.conf.example create mode 100644 aidlc-docs/features/react-frontend/operations/deployment/umami/.env.example create mode 100644 aidlc-docs/features/react-frontend/operations/deployment/umami/podman-compose.yml.example create mode 100644 aidlc-docs/features/react-frontend/operations/monitoring/umami-setup.md create mode 100644 src/components/UmamiAnalytics.tsx create mode 100644 src/components/__tests__/UmamiAnalytics.test.tsx create mode 100644 src/vite-env.d.ts diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..c305541 --- /dev/null +++ b/.env.example @@ -0,0 +1,11 @@ +# Voorbeeldbestand voor lokale environment-variabelen (Vite). +# Kopieer dit naar `.env.local` (die is al gitignored via de "*.local" regel) +# en vul je eigen waarden in. `.env.local` overschrijft niets in Gitea CI/CD — +# die gebruikt zijn eigen repository variables/secrets (zie continuous_integration.yaml). + +# 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 2add76f..956e7af 100644 --- a/.gitea/workflows/continuous_integration.yaml +++ b/.gitea/workflows/continuous_integration.yaml @@ -92,6 +92,9 @@ jobs: run: pnpm install --frozen-lockfile - name: Build + env: + VITE_UMAMI_SCRIPT_URL: ${{ vars.VITE_UMAMI_SCRIPT_URL }} + VITE_UMAMI_WEBSITE_ID: ${{ vars.VITE_UMAMI_WEBSITE_ID }} run: pnpm run build - name: Upload build artifact diff --git a/aidlc-docs/features/react-frontend/aidlc-state.md b/aidlc-docs/features/react-frontend/aidlc-state.md index 6de86e5..0ac318a 100644 --- a/aidlc-docs/features/react-frontend/aidlc-state.md +++ b/aidlc-docs/features/react-frontend/aidlc-state.md @@ -5,8 +5,8 @@ - **Feature Slug**: react-frontend - **Project Type**: Greenfield - **Start Date**: 2026-07-18T00:37:00Z -- **Current Stage**: OPERATIONS - Monitoring Setup (in progress) -- **Branch**: master +- **Current Stage**: Complete (post-completion follow-ups tracked in `audit.md` / `operations/production-readiness-checklist.md`) +- **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 02bb0b6..bfdd3a9 100644 --- a/aidlc-docs/features/react-frontend/audit.md +++ b/aidlc-docs/features/react-frontend/audit.md @@ -432,3 +432,15 @@ **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: 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. + +--- 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..092e0d5 --- /dev/null +++ b/aidlc-docs/features/react-frontend/operations/deployment/nginx/analytics-nginx.conf.example @@ -0,0 +1,57 @@ +# 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:3000 — 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. +# +# Kopieer dit bestand handmatig naar bijvoorbeeld +# /etc/nginx/sites-available/slpsoftware-analytics.conf op de reverse-proxy-Pi, +# maak een symlink in sites-enabled, en herlaad nginx. +# +# SSL-certificaat: net als voor test.slpsoftware.nl wordt dit aangevraagd met +# certbot (Let's Encrypt), bijvoorbeeld: +# 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. + +# 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. +server { + listen 80; + listen [::]:80; + + server_name analytics.slpsoftware.nl; + + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + location / { + return 301 https://$host$request_uri; + } +} + +# HTTPS: het daadwerkelijke reverse-proxy-verkeer naar Umami op de webserver-Pi. +server { + listen 443 ssl; + listen [::]:443 ssl; + + server_name analytics.slpsoftware.nl; + + ssl_certificate /etc/letsencrypt/live/analytics.slpsoftware.nl/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/analytics.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:3000; + + 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/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..3bbadbf --- /dev/null +++ b/aidlc-docs/features/react-frontend/operations/deployment/umami/podman-compose.yml.example @@ -0,0 +1,55 @@ +# 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. De reverse-proxy-Pi stuurt + # analytics.slpsoftware.nl door naar 192.168.1.103:3000 (zie + # ../nginx/analytics-nginx.conf.example). + ports: + - "192.168.1.103:3000: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 dc3b4c7..2fd7878 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) should be logged, but the ### 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**: 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. -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 7ecd24b..fa84db3 100644 --- a/aidlc-docs/features/react-frontend/operations/monitoring/monitoring-setup.md +++ b/aidlc-docs/features/react-frontend/operations/monitoring/monitoring-setup.md @@ -25,38 +25,49 @@ The destination was not finalized (original Question 3 = C). Two supported optio ## 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:3000`), 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 `