feat(analytics): Plausible über PLAUSIBLE_ENABLED und PLAUSIBLE_HOST steuerbar
Runtime-Konfiguration im Frontend-Container trennt Prod und Staging; Staging deaktiviert Analytics standardmäßig. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -56,3 +56,9 @@ VAPID_SUBJECT=mailto:support@kapteins-daagbok.eu
|
||||
NTFY_SERVER=https://ntfy.sh
|
||||
NTFY_TOPIC=kapteins-daagbok-feedback
|
||||
NTFY_TOKEN=tk_example_ntfy_access_token
|
||||
|
||||
# Plausible Analytics (frontend container — see docs/plausible-events.md)
|
||||
# Production: PLAUSIBLE_ENABLED=true, data-domain = current hostname (kapteins-daagbok.eu)
|
||||
# Staging: PLAUSIBLE_ENABLED=false (default in docker-compose.staging.yml)
|
||||
PLAUSIBLE_ENABLED=true
|
||||
PLAUSIBLE_HOST=https://plausible.elpatron.me
|
||||
|
||||
+7
-4
@@ -18,15 +18,18 @@ RUN npm run build
|
||||
FROM nginx:1.25-alpine
|
||||
WORKDIR /usr/share/nginx/html
|
||||
|
||||
# Copy custom Nginx configuration
|
||||
COPY client/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
RUN apk add --no-cache gettext
|
||||
|
||||
COPY client/nginx.conf.template /etc/nginx/templates/default.conf.template
|
||||
COPY client/docker-entrypoint.sh /docker-entrypoint.sh
|
||||
RUN chmod +x /docker-entrypoint.sh
|
||||
|
||||
# Copy built assets from builder
|
||||
COPY --from=builder /app/dist .
|
||||
|
||||
# Expose HTTP port
|
||||
EXPOSE 80
|
||||
|
||||
# Health check to verify Nginx is actively running
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=3s --retries=3 \
|
||||
CMD wget --no-verbose --tries=1 --spider http://127.0.0.1:80/ || exit 1
|
||||
|
||||
ENTRYPOINT ["/docker-entrypoint.sh"]
|
||||
|
||||
Executable
+26
@@ -0,0 +1,26 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
PLAUSIBLE_ENABLED="${PLAUSIBLE_ENABLED:-true}"
|
||||
PLAUSIBLE_HOST="${PLAUSIBLE_HOST:-https://plausible.elpatron.me}"
|
||||
PLAUSIBLE_HOST="${PLAUSIBLE_HOST%/}"
|
||||
|
||||
case "$(printf '%s' "$PLAUSIBLE_ENABLED" | tr '[:upper:]' '[:lower:]')" in
|
||||
true|1|yes)
|
||||
PLAUSIBLE_ENABLED_JSON=true
|
||||
PLAUSIBLE_CSP=" ${PLAUSIBLE_HOST}"
|
||||
;;
|
||||
*)
|
||||
PLAUSIBLE_ENABLED_JSON=false
|
||||
PLAUSIBLE_CSP=""
|
||||
;;
|
||||
esac
|
||||
|
||||
export PLAUSIBLE_CSP
|
||||
envsubst '${PLAUSIBLE_CSP}' < /etc/nginx/templates/default.conf.template > /etc/nginx/conf.d/default.conf
|
||||
|
||||
cat > /usr/share/nginx/html/runtime-config.json <<EOF
|
||||
{"plausibleEnabled":${PLAUSIBLE_ENABLED_JSON},"plausibleHost":"${PLAUSIBLE_HOST}"}
|
||||
EOF
|
||||
|
||||
exec nginx -g 'daemon off;'
|
||||
+1
-1
@@ -22,6 +22,7 @@
|
||||
<meta name="apple-mobile-web-app-title" content="Daagbok" />
|
||||
<meta name="theme-color" content="#0b0c10" />
|
||||
<script src="/appearance-bootstrap.js"></script>
|
||||
<script src="/plausible-bootstrap.js"></script>
|
||||
<script src="/bootstrap-watchdog.js"></script>
|
||||
<link rel="apple-touch-icon" href="/logo.png" />
|
||||
<meta property="og:type" content="website" />
|
||||
@@ -38,7 +39,6 @@
|
||||
<meta name="twitter:description" content="Kostenlos und werbefrei: sicheres, E2E-verschlüsseltes Logbuch für Skipper. Reisetage, GPS-Tracks, Crew- und Schiffsdaten – Passkey-Anmeldung und Offline-PWA." />
|
||||
<meta name="twitter:image" content="https://kapteins-daagbok.eu/logo.png" />
|
||||
<meta name="twitter:image:alt" content="Kapteins Daagbok Logo" />
|
||||
<script defer data-domain="kapteins-daagbok.eu" src="https://plausible.elpatron.me/js/script.tagged-events.js"></script>
|
||||
<title>Kapteins Daagbok – Kostenloses digitales Yacht-Logbuch (werbefrei)</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
+2
-51
@@ -1,51 +1,2 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
client_max_body_size 50M;
|
||||
|
||||
# Security headers (TLS/HSTS at NPM — see docs/deployment/npm-security.md)
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
add_header Permissions-Policy "camera=(self), geolocation=(self), microphone=(self)" always;
|
||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self' https://plausible.elpatron.me; connect-src 'self' https://plausible.elpatron.me; img-src 'self' data: blob: https://*.tile.openstreetmap.org; media-src 'self' blob: data:; style-src 'self' 'unsafe-inline'; font-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self';" always;
|
||||
|
||||
# Service worker and app shell must revalidate so PWA updates are detected
|
||||
location ~* ^/(sw\.js|workbox-.*\.js|manifest\.webmanifest|version\.json)$ {
|
||||
root /usr/share/nginx/html;
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
add_header Permissions-Policy "camera=(self), geolocation=(self), microphone=(self)" always;
|
||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self' https://plausible.elpatron.me; connect-src 'self' https://plausible.elpatron.me; img-src 'self' data: blob: https://*.tile.openstreetmap.org; media-src 'self' blob: data:; style-src 'self' 'unsafe-inline'; font-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self';" always;
|
||||
}
|
||||
|
||||
location = /index.html {
|
||||
root /usr/share/nginx/html;
|
||||
add_header Cache-Control "no-cache, must-revalidate" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
add_header Permissions-Policy "camera=(self), geolocation=(self), microphone=(self)" always;
|
||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self' https://plausible.elpatron.me; connect-src 'self' https://plausible.elpatron.me; img-src 'self' data: blob: https://*.tile.openstreetmap.org; media-src 'self' blob: data:; style-src 'self' 'unsafe-inline'; font-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self';" always;
|
||||
}
|
||||
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://backend:5000/api/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
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;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
}
|
||||
}
|
||||
# Generated at container start from PLAUSIBLE_* — see client/nginx.conf.template and docker-entrypoint.sh
|
||||
# Local Docker Compose uses the template via client/Dockerfile entrypoint.
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
client_max_body_size 50M;
|
||||
|
||||
# Security headers (TLS/HSTS at NPM — see docs/deployment/npm-security.md)
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
add_header Permissions-Policy "camera=(self), geolocation=(self), microphone=(self)" always;
|
||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self'${PLAUSIBLE_CSP}; connect-src 'self'${PLAUSIBLE_CSP}; img-src 'self' data: blob: https://*.tile.openstreetmap.org; media-src 'self' blob: data:; style-src 'self' 'unsafe-inline'; font-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self';" always;
|
||||
|
||||
# Service worker and app shell must revalidate so PWA updates are detected
|
||||
location ~* ^/(sw\.js|workbox-.*\.js|manifest\.webmanifest|version\.json)$ {
|
||||
root /usr/share/nginx/html;
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
add_header Permissions-Policy "camera=(self), geolocation=(self), microphone=(self)" always;
|
||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self'${PLAUSIBLE_CSP}; connect-src 'self'${PLAUSIBLE_CSP}; img-src 'self' data: blob: https://*.tile.openstreetmap.org; media-src 'self' blob: data:; style-src 'self' 'unsafe-inline'; font-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self';" always;
|
||||
}
|
||||
|
||||
location = /index.html {
|
||||
root /usr/share/nginx/html;
|
||||
add_header Cache-Control "no-cache, must-revalidate" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
add_header Permissions-Policy "camera=(self), geolocation=(self), microphone=(self)" always;
|
||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self'${PLAUSIBLE_CSP}; connect-src 'self'${PLAUSIBLE_CSP}; img-src 'self' data: blob: https://*.tile.openstreetmap.org; media-src 'self' blob: data:; style-src 'self' 'unsafe-inline'; font-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self';" always;
|
||||
}
|
||||
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://backend:5000/api/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
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;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Loads Plausible when enabled via /runtime-config.json (from .env in Docker / Vite dev).
|
||||
* data-domain is always the current hostname (prod vs staging).
|
||||
*/
|
||||
(function () {
|
||||
function load(cfg) {
|
||||
if (!cfg || !cfg.plausibleEnabled || !cfg.plausibleHost) return
|
||||
var host = String(cfg.plausibleHost).replace(/\/$/, '')
|
||||
if (!host) return
|
||||
var s = document.createElement('script')
|
||||
s.defer = true
|
||||
s.dataset.domain = window.location.hostname
|
||||
s.src = host + '/js/script.tagged-events.js'
|
||||
document.head.appendChild(s)
|
||||
}
|
||||
|
||||
fetch('/runtime-config.json', { cache: 'no-store' })
|
||||
.then(function (r) {
|
||||
return r.ok ? r.json() : null
|
||||
})
|
||||
.then(load)
|
||||
.catch(function () {
|
||||
/* analytics optional */
|
||||
})
|
||||
})()
|
||||
@@ -33,8 +33,36 @@ function versionJsonPlugin(version: string): Plugin {
|
||||
}
|
||||
}
|
||||
|
||||
function readPlausibleConfig(): { plausibleEnabled: boolean; plausibleHost: string } {
|
||||
const host = (process.env.PLAUSIBLE_HOST || 'https://plausible.elpatron.me').replace(/\/$/, '')
|
||||
const flag = (process.env.PLAUSIBLE_ENABLED ?? 'true').trim().toLowerCase()
|
||||
const plausibleEnabled = !['false', '0', 'no'].includes(flag)
|
||||
return { plausibleEnabled, plausibleHost: host }
|
||||
}
|
||||
|
||||
function runtimeConfigPlugin(): Plugin {
|
||||
return {
|
||||
name: 'runtime-config',
|
||||
configureServer(server) {
|
||||
server.middlewares.use((req, res, next) => {
|
||||
if (req.url !== '/runtime-config.json') return next()
|
||||
res.setHeader('Content-Type', 'application/json')
|
||||
res.end(`${JSON.stringify(readPlausibleConfig())}\n`)
|
||||
})
|
||||
},
|
||||
writeBundle(options) {
|
||||
const outDir = options.dir ?? resolve(__dirname, 'dist')
|
||||
writeFileSync(
|
||||
resolve(outDir, 'runtime-config.json'),
|
||||
`${JSON.stringify(readPlausibleConfig(), null, 2)}\n`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
envDir: resolve(__dirname, '..'),
|
||||
test: {
|
||||
environment: 'happy-dom',
|
||||
include: ['src/**/*.test.ts']
|
||||
@@ -59,6 +87,7 @@ export default defineConfig({
|
||||
plugins: [
|
||||
react(),
|
||||
versionJsonPlugin(readAppVersion()),
|
||||
runtimeConfigPlugin(),
|
||||
VitePWA({
|
||||
strategies: 'injectManifest',
|
||||
srcDir: 'src',
|
||||
|
||||
@@ -57,6 +57,9 @@ services:
|
||||
APP_VERSION: ${APP_VERSION:-0.1.0.0-dev}
|
||||
container_name: daagbox-staging-frontend
|
||||
restart: always
|
||||
environment:
|
||||
PLAUSIBLE_ENABLED: ${PLAUSIBLE_ENABLED:-false}
|
||||
PLAUSIBLE_HOST: ${PLAUSIBLE_HOST:-https://plausible.elpatron.me}
|
||||
ports:
|
||||
- "80:80"
|
||||
depends_on:
|
||||
|
||||
@@ -58,6 +58,9 @@ services:
|
||||
APP_VERSION: ${APP_VERSION:-0.1.0.0-dev}
|
||||
container_name: daagbox-prod-frontend
|
||||
restart: always
|
||||
environment:
|
||||
PLAUSIBLE_ENABLED: ${PLAUSIBLE_ENABLED:-true}
|
||||
PLAUSIBLE_HOST: ${PLAUSIBLE_HOST:-https://plausible.elpatron.me}
|
||||
ports:
|
||||
- "80:80"
|
||||
depends_on:
|
||||
|
||||
@@ -40,13 +40,20 @@ TRUST_PROXY=1
|
||||
## Security-Header
|
||||
|
||||
- **HSTS, CSP (optional restriktiver):** können in NPM unter „Custom Headers“ oder im Advanced-Block gesetzt werden.
|
||||
- **Basis-Header** für statische Dateien setzt [`client/nginx.conf`](../../client/nginx.conf) (X-Content-Type-Options, X-Frame-Options, Referrer-Policy, CSP inkl. Plausible).
|
||||
- **Basis-Header** für statische Dateien setzt [`client/nginx.conf.template`](../../client/nginx.conf.template) via Container-Entrypoint (X-Content-Type-Options, X-Frame-Options, Referrer-Policy, CSP optional inkl. Plausible).
|
||||
|
||||
### Plausible Analytics
|
||||
|
||||
Script-Host: `https://plausible.elpatron.me` — in CSP als `script-src` und `connect-src` erlaubt. Gemessene Site: `data-domain="kapteins-daagbok.eu"`.
|
||||
Konfiguration über `.env` (Frontend-Container):
|
||||
|
||||
Optional später: `analytics.kapteins-daagbok.eu` als Alias auf dieselbe Plausible-Instanz.
|
||||
```env
|
||||
PLAUSIBLE_ENABLED=true
|
||||
PLAUSIBLE_HOST=https://plausible.elpatron.me
|
||||
```
|
||||
|
||||
Staging-Default: `PLAUSIBLE_ENABLED=false` in [`docker-compose.staging.yml`](../../docker-compose.staging.yml).
|
||||
|
||||
Script-Host wird in CSP (`script-src`, `connect-src`) nur bei `PLAUSIBLE_ENABLED=true` freigegeben. `data-domain` ist immer der aktuelle Hostname (Prod vs. Staging getrennt, wenn Staging aktiviert wird).
|
||||
|
||||
## Nach Deploy prüfen
|
||||
|
||||
|
||||
@@ -47,6 +47,10 @@ SESSION_SECRET=<generiert>
|
||||
|
||||
NTFY_SERVER=https://ntfy.sh
|
||||
NTFY_TOPIC=kapteins-daagbok-staging-feedback
|
||||
|
||||
# Analytics aus (Staging soll Prod-Statistik nicht verfälschen)
|
||||
PLAUSIBLE_ENABLED=false
|
||||
PLAUSIBLE_HOST=https://plausible.elpatron.me
|
||||
```
|
||||
|
||||
Optional: `VAPID_*`, `OpenWeatherMapAPIKey`, `OpenRouterAPIKey`, `ADMIN_USER_IDS`, `NTFY_TOKEN`.
|
||||
|
||||
@@ -1,12 +1,21 @@
|
||||
# Plausible Custom Events
|
||||
|
||||
Kapteins Daagbok nutzt [Plausible Analytics](https://plausible.io/) mit dem Script `script.tagged-events.js` auf der Domain `kapteins-daagbok.eu`. Custom Events werden über `window.plausible()` ausgelöst (siehe `client/src/services/analytics.ts`).
|
||||
Kapteins Daagbok nutzt [Plausible Analytics](https://plausible.io/) mit dem Script `script.tagged-events.js`. Custom Events werden über `window.plausible()` ausgelöst (siehe `client/src/services/analytics.ts`).
|
||||
|
||||
**Konfiguration** (`.env`, Frontend-Container / Vite-Dev):
|
||||
|
||||
```env
|
||||
PLAUSIBLE_ENABLED=true # Staging: false
|
||||
PLAUSIBLE_HOST=https://plausible.elpatron.me
|
||||
```
|
||||
|
||||
Das Script wird über `plausible-bootstrap.js` geladen; `data-domain` ist der aktuelle Hostname. CSP in Nginx enthält `PLAUSIBLE_HOST` nur wenn aktiviert.
|
||||
|
||||
**Datenschutz:** Es werden keine personenbezogenen Daten in Event-Properties übermittelt (keine Nutzernamen, Hafennamen, Koordinaten o.ä.).
|
||||
|
||||
## Setup
|
||||
|
||||
1. Script in `client/index.html` (bereits eingebunden)
|
||||
1. `PLAUSIBLE_*` in `.env` setzen (Prod: enabled, Staging: disabled empfohlen)
|
||||
2. Nach Deploy: Goals im Plausible-Dashboard anlegen — **Namen müssen exakt mit der Event-Spalte „Event name“ übereinstimmen** (Title Case, Leerzeichen)
|
||||
|
||||
## Event-Übersicht
|
||||
|
||||
Reference in New Issue
Block a user