Feature/analytics uptime dashboard #6
@@ -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=
|
||||||
@@ -92,6 +92,9 @@ jobs:
|
|||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
- name: Build
|
- 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
|
run: pnpm run build
|
||||||
|
|
||||||
- name: Upload build artifact
|
- name: Upload build artifact
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
- **Feature Slug**: react-frontend
|
- **Feature Slug**: react-frontend
|
||||||
- **Project Type**: Greenfield
|
- **Project Type**: Greenfield
|
||||||
- **Start Date**: 2026-07-18T00:37:00Z
|
- **Start Date**: 2026-07-18T00:37:00Z
|
||||||
- **Current Stage**: OPERATIONS - Monitoring Setup (in progress)
|
- **Current Stage**: Complete (post-completion follow-ups tracked in `audit.md` / `operations/production-readiness-checklist.md`)
|
||||||
- **Branch**: master
|
- **Branch**: feature/analytics_uptime_dashboard
|
||||||
|
|
||||||
## Workspace State
|
## Workspace State
|
||||||
- **Existing Code**: No
|
- **Existing Code**: No
|
||||||
|
|||||||
@@ -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`.
|
**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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|||||||
+57
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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=
|
||||||
+55
@@ -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:
|
||||||
@@ -17,8 +17,8 @@ Client-side errors (JavaScript crashes, broken links) should be logged, but the
|
|||||||
|
|
||||||
### Dashboards
|
### Dashboards
|
||||||
A combination of:
|
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.
|
- **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** (site reachability) — e.g. an external monitoring service such as UptimeRobot or Better Uptime.
|
- **Uptime dashboard**: **decided** — UptimeRobot, chosen over Better Uptime for its established free tier.
|
||||||
|
|
||||||
(Original Question 6 = C, "Both (analytics + uptime dashboard)".)
|
(Original Question 6 = C, "Both (analytics + uptime dashboard)".)
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ A combination of:
|
|||||||
|
|
||||||
## Open Action Items
|
## 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**: 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
|
## 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.
|
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.
|
||||||
|
|||||||
@@ -25,38 +25,49 @@ The destination was not finalized (original Question 3 = C). Two supported optio
|
|||||||
|
|
||||||
## Dashboards
|
## Dashboards
|
||||||
|
|
||||||
### Website analytics
|
### Website analytics — decided: self-hosted Umami
|
||||||
Pick one (all have generous free tiers suitable for a small marketing site):
|
**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 |
|
- Full step-by-step setup: `operations/monitoring/umami-setup.md` (Podman/compose files,
|
||||||
|---|---|
|
systemd auto-start, reverse proxy + SSL, website registration).
|
||||||
| Plausible / Umami | Privacy-friendly, lightweight, no cookie banner typically required; self-hosted or low-cost hosted tier |
|
- Example config: `operations/deployment/umami/podman-compose.yml.example` +
|
||||||
| Google Analytics (GA4) / Search Console | Free, widely known, but heavier script and involves third-party data sharing (cookie/consent implications) |
|
`.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)**:
|
### Uptime dashboard — decided: UptimeRobot
|
||||||
1. Create an account/site entry with the chosen provider and obtain the tracking snippet or `<script>` tag.
|
**Decided**: UptimeRobot (free tier: up to 50 monitors, 5-minute check interval, optional
|
||||||
2. Add the snippet to `index.html` (or load it conditionally in `src/main.tsx`) — this is a documentation/config task, not something the current codebase needs restructuring for.
|
e-mail notification on downtime — opportunistic, not a designed alerting feature per
|
||||||
3. Key metrics to surface: unique visitors, page views per route (Home, Packages, etc. — see `frontend-components.md`), and referral sources.
|
`monitoring-plan.md`). Chosen over Better Uptime for its long-standing free tier and
|
||||||
|
simplicity for a single low-traffic site.
|
||||||
|
|
||||||
### Uptime dashboard
|
**Setup**:
|
||||||
Pick one:
|
1. Create a free UptimeRobot account (https://uptimerobot.com).
|
||||||
|
2. Register `test.slpsoftware.nl` as an HTTP(S) monitor now (checking for a `200`
|
||||||
|
response); add the production URL once hosting is finalized (see
|
||||||
|
`operations/deployment/deployment-plan.md` "Open Item") — still an open follow-up.
|
||||||
|
3. Optional: publish a public status page if desired for transparency to visitors.
|
||||||
|
4. Key metric to surface: uptime percentage / current status.
|
||||||
|
|
||||||
| Option | Notes |
|
No application code changes are needed for UptimeRobot — it works purely by polling the
|
||||||
|---|---|
|
public URL from the outside, entirely independent of the site's own codebase.
|
||||||
| UptimeRobot | Free tier: up to 50 monitors, 5-minute check interval, optional e-mail notification on downtime (opportunistic, not a designed alerting feature per `monitoring-plan.md`) |
|
|
||||||
| Better Uptime | Free tier available; similar capability, nicer public status page option |
|
|
||||||
|
|
||||||
**Setup (once a tool is picked)**:
|
|
||||||
1. Register the production URL (once hosting is finalized — see `operations/deployment/deployment-plan.md` "Open Item") as an HTTP(S) monitor, checking for a `200` response.
|
|
||||||
2. Optional: publish a public status page if desired for transparency to visitors.
|
|
||||||
3. Key metric to surface: uptime percentage / current status.
|
|
||||||
|
|
||||||
## Summary Table
|
## Summary Table
|
||||||
|
|
||||||
| Concern | Approach | Status |
|
| Concern | Approach | Status |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Client-side errors | Logging (console today; Sentry free tier optional later) | Destination open item |
|
| Client-side errors | Logging (console today; Sentry free tier optional later) | Destination open item |
|
||||||
| Visitor/usage insight | Analytics dashboard (Plausible/Umami/GA4) | Tool selection open item |
|
| Visitor/usage insight | Analytics dashboard (self-hosted Umami) | Tool decided; deployment (Podman on Pi Main + `analytics.slpsoftware.nl`) is a manual follow-up, see `umami-setup.md` |
|
||||||
| Site reachability | Uptime dashboard (UptimeRobot/Better Uptime) | Tool selection + production URL open item |
|
| Site reachability | Uptime dashboard (UptimeRobot) | Tool decided; account creation + production URL are manual follow-ups |
|
||||||
| Alerting | Out of scope | Not configured |
|
| Alerting | Out of scope | Not configured |
|
||||||
| Shared infrastructure reuse | Out of scope | None exists yet |
|
| Shared infrastructure reuse | Out of scope | None exists yet |
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
# Umami Self-Hosted Setup (Analytics)
|
||||||
|
|
||||||
|
Stapsgewijze handleiding om Umami (self-hosted website-analytics) op te zetten op de
|
||||||
|
webserver-Pi (Pi Main, `192.168.1.103`) met Podman, en bereikbaar te maken via
|
||||||
|
`analytics.slpsoftware.nl`. Dit vult het "Analytics/uptime dashboard" open item uit
|
||||||
|
`operations/production-readiness-checklist.md` in voor het analytics-gedeelte.
|
||||||
|
|
||||||
|
## Vereisten
|
||||||
|
- Podman is al aanwezig op de Pi (bevestigd door de gebruiker).
|
||||||
|
- `podman-compose` geïnstalleerd: `pip3 install --user podman-compose` (of via de package
|
||||||
|
manager van je distro, indien beschikbaar).
|
||||||
|
- DNS: een A-record voor `analytics.slpsoftware.nl` dat naar het publieke IP van de
|
||||||
|
reverse-proxy-Pi wijst (dezelfde Pi die ook `test.slpsoftware.nl` afhandelt).
|
||||||
|
|
||||||
|
## 1. Umami + database opzetten (op de webserver-Pi)
|
||||||
|
1. Maak een map aan, bv. `~/umami/`, en kopieer daarin:
|
||||||
|
- `operations/deployment/umami/podman-compose.yml.example` → `~/umami/podman-compose.yml`
|
||||||
|
- `operations/deployment/umami/.env.example` → `~/umami/.env`
|
||||||
|
2. Vul in `~/umami/.env` een echte `POSTGRES_PASSWORD` en `APP_SECRET` in (bv. via
|
||||||
|
`openssl rand -base64 32` voor beide — gebruik twee verschillende waarden).
|
||||||
|
3. Start de containers:
|
||||||
|
```bash
|
||||||
|
cd ~/umami
|
||||||
|
podman-compose up -d
|
||||||
|
```
|
||||||
|
4. Controleer dat Umami draait en bereikbaar is op het interne netwerk:
|
||||||
|
```bash
|
||||||
|
curl http://192.168.1.103:3000/api/heartbeat
|
||||||
|
```
|
||||||
|
Dit zou een JSON-antwoord met `"ok"` moeten teruggeven.
|
||||||
|
|
||||||
|
## 2. Automatisch starten na reboot (systemd user service)
|
||||||
|
Podman-compose start niet vanzelf op na een herstart van de Pi, tenzij je dit expliciet
|
||||||
|
instelt. Omdat rootless Podman hier al gebruikt wordt, is een systemd **user**-service de
|
||||||
|
eenvoudigste aanpak:
|
||||||
|
|
||||||
|
1. Zorg dat de gebruiker-sessie blijft "linger-en" na uitloggen/reboot:
|
||||||
|
```bash
|
||||||
|
sudo loginctl enable-linger $(whoami)
|
||||||
|
```
|
||||||
|
2. Maak `~/.config/systemd/user/umami.service` aan:
|
||||||
|
```ini
|
||||||
|
[Unit]
|
||||||
|
Description=Umami analytics (podman-compose)
|
||||||
|
After=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
WorkingDirectory=%h/umami
|
||||||
|
ExecStart=/usr/bin/podman-compose up
|
||||||
|
ExecStop=/usr/bin/podman-compose down
|
||||||
|
Restart=on-failure
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
|
```
|
||||||
|
3. Activeer en start de service:
|
||||||
|
```bash
|
||||||
|
systemctl --user daemon-reload
|
||||||
|
systemctl --user enable --now umami.service
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Reverse proxy + SSL (op de reverse-proxy-Pi)
|
||||||
|
1. Kopieer `operations/deployment/nginx/analytics-nginx.conf.example` naar bv.
|
||||||
|
`/etc/nginx/sites-available/slpsoftware-analytics.conf` op de reverse-proxy-Pi, en maak
|
||||||
|
een symlink in `sites-enabled`.
|
||||||
|
2. Zorg dat het DNS-record voor `analytics.slpsoftware.nl` al actief is, en draai dan:
|
||||||
|
```bash
|
||||||
|
sudo certbot --nginx -d analytics.slpsoftware.nl
|
||||||
|
```
|
||||||
|
3. Herlaad nginx: `sudo nginx -t && sudo systemctl reload nginx`.
|
||||||
|
4. Test: open `https://analytics.slpsoftware.nl` in de browser — je zou het
|
||||||
|
Umami-inlogscherm moeten zien (standaard inloggegevens: `admin` / `umami`, **direct
|
||||||
|
wijzigen na eerste login**).
|
||||||
|
|
||||||
|
## 4. Website registreren in Umami en het website-ID ophalen
|
||||||
|
1. Log in op `https://analytics.slpsoftware.nl` en wijzig direct het standaardwachtwoord.
|
||||||
|
2. Ga naar **Settings → Websites → Add website** en vul in:
|
||||||
|
- Name: `SLP Software` (of naar keuze)
|
||||||
|
- Domain: het domein van de daadwerkelijke website (bv. `slpsoftware.nl` of
|
||||||
|
`test.slpsoftware.nl`, afhankelijk van welke omgeving je eerst wilt meten)
|
||||||
|
3. Na het opslaan toont Umami een **Website ID** (een UUID) — dit heb je nodig voor de
|
||||||
|
volgende stap.
|
||||||
|
|
||||||
|
## 5. Tracking script koppelen aan de website (build-configuratie)
|
||||||
|
De React-app (`src/components/UmamiAnalytics.tsx`) injecteert het Umami tracking-script
|
||||||
|
automatisch, mits de volgende twee build-time variabelen zijn ingesteld — beide zijn
|
||||||
|
**geen secrets** (client-side zichtbaar), dus als Gitea Actions **repository variables**
|
||||||
|
(niet secrets), net als `VITE_SENTRY_DSN`:
|
||||||
|
|
||||||
|
| Variabele | Waarde |
|
||||||
|
|---|---|
|
||||||
|
| `VITE_UMAMI_SCRIPT_URL` | `https://analytics.slpsoftware.nl/script.js` |
|
||||||
|
| `VITE_UMAMI_WEBSITE_ID` | het Website ID uit stap 4 |
|
||||||
|
|
||||||
|
Stel deze in via **Gitea → Repository Settings → Actions → Variables**. Zodra beide
|
||||||
|
bestaan, pakt de eerstvolgende build ze automatisch op; zonder deze variabelen slaat de
|
||||||
|
app het inladen van het script gewoon over (geen crash, geen tracking).
|
||||||
|
|
||||||
|
**Lokaal (`pnpm dev`)**: het tracking-script wordt hier bewust nooit geladen (zie
|
||||||
|
`UmamiAnalytics.tsx`), zodat lokaal testen de bezoekersstatistieken niet vervuilt. Wil je
|
||||||
|
dit toch lokaal testen, zet dan tijdelijk beide waarden in `.env.local` (zie
|
||||||
|
`.env.example`) én verwijder tijdelijk de `import.meta.env.DEV`-check.
|
||||||
|
|
||||||
|
## Openstaande handmatige stappen
|
||||||
|
- [ ] `podman-compose up -d` daadwerkelijk draaien op Pi Main.
|
||||||
|
- [ ] Systemd user-service instellen voor auto-start na reboot.
|
||||||
|
- [ ] DNS-record + certbot voor `analytics.slpsoftware.nl` op de reverse-proxy-Pi.
|
||||||
|
- [ ] Standaard Umami-wachtwoord direct wijzigen na eerste login.
|
||||||
|
- [ ] Website aanmaken in Umami en het Website ID overnemen.
|
||||||
|
- [ ] `VITE_UMAMI_SCRIPT_URL` en `VITE_UMAMI_WEBSITE_ID` als Gitea repository variables instellen.
|
||||||
@@ -20,5 +20,5 @@
|
|||||||
- **Open Follow-ups**:
|
- **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")
|
- 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 (browser console only vs. an external service such as Sentry free tier) — see `monitoring-setup.md`
|
||||||
- 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
|
- ~~Pick and configure the concrete analytics tool and uptime dashboard tool~~ — **Resolved**: self-hosted Umami (Podman on Pi Main) + UptimeRobot decided; see `monitoring-setup.md` and the new `umami-setup.md`. Remaining manual follow-ups: actually deploy the Umami containers, set up `analytics.slpsoftware.nl` DNS/SSL, create the UptimeRobot monitor, and register 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`)
|
- Run a formal Lighthouse performance check before the first real production deployment (flagged as not yet run in `build-and-test-summary.md`)
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import { Nav } from './Nav';
|
import { Nav } from './Nav';
|
||||||
import { Footer } from './Footer';
|
import { Footer } from './Footer';
|
||||||
|
import { UmamiAnalytics } from './UmamiAnalytics';
|
||||||
|
|
||||||
export function RootLayout({ children }: { children: ReactNode }) {
|
export function RootLayout({ children }: { children: ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<div className="font-sans min-h-screen bg-bg text-text">
|
<div className="font-sans min-h-screen bg-bg text-text">
|
||||||
|
<UmamiAnalytics />
|
||||||
<Nav />
|
<Nav />
|
||||||
{children}
|
{children}
|
||||||
<Footer />
|
<Footer />
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
const SCRIPT_ELEMENT_ID = 'umami-analytics-script';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injects the self-hosted Umami tracking script (see
|
||||||
|
* aidlc-docs/features/react-frontend/operations/monitoring/umami-setup.md) when both
|
||||||
|
* VITE_UMAMI_SCRIPT_URL and VITE_UMAMI_WEBSITE_ID are configured at build time.
|
||||||
|
*
|
||||||
|
* Silently does nothing if either variable is missing (e.g. local development, or a
|
||||||
|
* build for which the Umami instance/website has not been set up yet) — safe default,
|
||||||
|
* no crash on missing config, mirroring the existing Sentry DSN pattern.
|
||||||
|
*
|
||||||
|
* Skipped during local development (`pnpm dev`) by default so local testing does not
|
||||||
|
* pollute production/test visitor analytics.
|
||||||
|
*/
|
||||||
|
export function UmamiAnalytics() {
|
||||||
|
useEffect(() => {
|
||||||
|
const scriptUrl = import.meta.env.VITE_UMAMI_SCRIPT_URL;
|
||||||
|
const websiteId = import.meta.env.VITE_UMAMI_WEBSITE_ID;
|
||||||
|
|
||||||
|
if (import.meta.env.DEV || !scriptUrl || !websiteId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.getElementById(SCRIPT_ELEMENT_ID)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.id = SCRIPT_ELEMENT_ID;
|
||||||
|
script.src = scriptUrl;
|
||||||
|
script.defer = true;
|
||||||
|
script.setAttribute('data-website-id', websiteId);
|
||||||
|
document.head.appendChild(script);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.getElementById(SCRIPT_ELEMENT_ID)?.remove();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { render, cleanup } from '@testing-library/react';
|
||||||
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import { UmamiAnalytics } from '../UmamiAnalytics';
|
||||||
|
|
||||||
|
const SCRIPT_ID = 'umami-analytics-script';
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cleanup();
|
||||||
|
document.getElementById(SCRIPT_ID)?.remove();
|
||||||
|
vi.unstubAllEnvs();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('UmamiAnalytics', () => {
|
||||||
|
it('does not inject a script tag when the env vars are missing', () => {
|
||||||
|
vi.stubEnv('VITE_UMAMI_SCRIPT_URL', '');
|
||||||
|
vi.stubEnv('VITE_UMAMI_WEBSITE_ID', '');
|
||||||
|
|
||||||
|
render(<UmamiAnalytics />);
|
||||||
|
|
||||||
|
expect(document.getElementById(SCRIPT_ID)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not inject a script tag during local development, even if configured', () => {
|
||||||
|
vi.stubEnv('DEV', true);
|
||||||
|
vi.stubEnv('VITE_UMAMI_SCRIPT_URL', 'https://analytics.slpsoftware.nl/script.js');
|
||||||
|
vi.stubEnv('VITE_UMAMI_WEBSITE_ID', 'test-website-id');
|
||||||
|
|
||||||
|
render(<UmamiAnalytics />);
|
||||||
|
|
||||||
|
expect(document.getElementById(SCRIPT_ID)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('injects the tracking script with the configured URL and website id when built for test/production', () => {
|
||||||
|
vi.stubEnv('DEV', false);
|
||||||
|
vi.stubEnv('VITE_UMAMI_SCRIPT_URL', 'https://analytics.slpsoftware.nl/script.js');
|
||||||
|
vi.stubEnv('VITE_UMAMI_WEBSITE_ID', 'test-website-id');
|
||||||
|
|
||||||
|
render(<UmamiAnalytics />);
|
||||||
|
|
||||||
|
const script = document.getElementById(SCRIPT_ID) as HTMLScriptElement | null;
|
||||||
|
expect(script).not.toBeNull();
|
||||||
|
expect(script?.src).toBe('https://analytics.slpsoftware.nl/script.js');
|
||||||
|
expect(script?.getAttribute('data-website-id')).toBe('test-website-id');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders nothing visible', () => {
|
||||||
|
const { container } = render(<UmamiAnalytics />);
|
||||||
|
expect(container).toBeEmptyDOMElement();
|
||||||
|
});
|
||||||
|
});
|
||||||
Vendored
+12
@@ -0,0 +1,12 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
interface ImportMetaEnv {
|
||||||
|
/** Umami tracking script URL (self-hosted), e.g. https://analytics.slpsoftware.nl/script.js */
|
||||||
|
readonly VITE_UMAMI_SCRIPT_URL?: string;
|
||||||
|
/** Umami website ID, created manually in the Umami dashboard for this site. */
|
||||||
|
readonly VITE_UMAMI_WEBSITE_ID?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImportMeta {
|
||||||
|
readonly env: ImportMetaEnv;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user