Files
Webklar-Kundenbereich/docs/superpowers/plans/2026-10-01-website-analyse.md
T
2026-10-01 09:18:23 +00:00

138 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Website-Analyse – Umsetzungsplan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Kunden mit Wartungs-Abo schalten im Portal eine Website-Analyse ein; Banner + Skript werden vom
Runner eingesetzt, ein eigener Sammeldienst zählt datenschutzkonform, das Portal zeigt Zahlen und Heatmap.
**Architecture:** Neuer Dienst `analytics/` (Node 24, `node:sqlite`, keine Abhängigkeiten) unter
`https://project.webklar.com/a/…`; Client-Skripte `w.js`/`h.js` liefert er aus. Der Runner setzt das
Skript beim Veröffentlichen ein. Das Portal verwaltet Freischaltung (portalFeatures), spricht den
Sammeldienst per Admin-Token an und zeigt die Auswertung. WOMS bekommt einen Schalter.
**Tech Stack:** Node 24 (Sammeldienst), Node 20 (Portal/Runner), vanilla JS (Portal-UI, Client-Skript),
React (WOMS), Playwright (Prüfung).
**Spec:** `docs/superpowers/specs/2026-10-01-website-analyse-design.md`
**Worktrees:** Portal/Runner/Analytics `/home/webklar/apps/kundenbereich.webklar.com/dev-analyse`
(Branch `feature/website-analyse`). WOMS: `git -C /home/webklar/apps/ticket.webklar.com/build worktree add
-b feature/website-analyse ../dev-analyse origin/test`, node_modules skip-worktree + eigenes `npm install`.
## Global Constraints
- Nur Wartungs-Abo: `productKey === 'wartung'` und Status ∈ `ACTIVE_STATUSES` (stripeBilling.js). Hosting ausgeschlossen.
- Nur öffentliche Projekte (`isPublic === true`) mit Subdomain; Domains der Website = `<subdomain>.project.webklar.com` + `liveDomain` (falls gesetzt).
- Ereignisse exakt laut Spec-Tabelle (`pv`, `bn`, `en`, `ck`, `ct`) mit genau den dort genannten Feldern; `bn` ohne weitere Felder; `en`/`ck`/`ct` nur nach Annehmen.
- Keine IP, kein User-Agent gespeichert; keine Cookies; Banner-Wahl nur `localStorage['wk-consent'] = {v:1,a,t}`, 6 Monate gültig; GPC/DNT ⇒ wie Ablehnen ohne Banner.
- Besucher-Kennung `HMAC-SHA256(tagesSalz, siteKey|ip|ua)` → 16 Hex, Salz nur RAM, Wechsel 00:00 Europe/Berlin.
- Rohdaten/Klicks 30 Tage; Websites mit `offSince` > 60 Tage komplett löschen; Job täglich 02:30 Europe/Berlin.
- Limits: Body ≤ 4 KB, path ≤ 300, sel ≤ 200, sec 0–3600, scroll 0–100, x/y 0–1, 120 Ereignisse/Min je (site, IP).
- Banner: Leiste unten, „Ablehnen“ und „Annehmen“ gleich groß/gleich auffällig, Link „Datenschutz“ → `/datenschutz-analyse.html`; Akzentfarbe nur für „Annehmen“ (Kontrast ≥ 4,5:1, sonst Fallback `#111111`); Handy (< 768 px) kompakte Karte; Footer-Link „Cookie-Einstellungen“.
- siteKey `^[a-z0-9]{16,40}$`, Farbe `^#[0-9a-f]{6}$`.
- Heatmap-Link: HMAC-Token, 30 Minuten gültig, enthält siteKey, path, vp, exp.
- Schalter `ANALYSE_ENABLED` (Vorgabe off) ⇒ Kachel bleibt „Kommt bald“, Routen 409.
- Texte deutsch (Du-Form im Portal), kein `confirm()`. Nichts pushen/live schalten.
## Review Focus
1. Ohne „Annehmen“ verlässt kein `en`/`ck`/`ct` den Browser (auch nicht nach Widerruf). → Task 6
2. Fremde Origin oder unbekannter siteKey ⇒ nichts gespeichert. → Task 2
3. Analyse aus / Abo weg ⇒ nach Neubau kein Skript und keine Zusatzseite mehr im Ordner. → Task 7, 10
4. Heatmap-Token einer Website liefert keine Punkte einer anderen Website. → Task 4
5. Kontakt-Klick speichert nie Telefonnummer/E-Mail-Adresse. → Task 6
---
## Sammeldienst (`analytics/`)
### Task 1: Gerüst, Datenbank, Website-Register
**Files:** `analytics/package.json` (type module, test `node --test test/*.test.js`), `src/config.js`, `src/db.js`, `src/server.js`, `src/index.js`, `test/sites.test.js`.
**Produces:** `loadConfig(env) ⇒ { port: 8090, dataDir: '/srv/webklar-analytics', adminToken, heatSecret, geoFile, baseHost: 'project.webklar.com' }`; `openDb(path) ⇒ db` (Schema: `sites(siteKey PK, projectId, domains JSON, active INT, offSince TEXT)`, `events_raw(id, site, ts, type, path, vp, ref, country, device, visitor, sec, scroll, kind, host)`, `clicks(id, site, ts, path, vp, x, y, sel)`, `daily(site, day, dim, key, count, PK(site,day,dim,key))`); Admin-Routen (Bearer adminToken, timingSafeEqual): `PUT /a/admin/sites/:siteKey {projectId, domains[], active}`, `DELETE /a/admin/sites/:siteKey`, `GET /a/health`.
- [ ] Tests: 401 ohne/mit falschem Token; PUT legt an/ändert; active=false setzt offSince einmalig; ungültiger siteKey 400; Domains nur Hostnamen.
- [ ] FAIL → implementieren → PASS → Commit „Analytics: Geruest, Datenbank, Website-Register“
### Task 2: Annahme `POST /a/e`
**Files:** `src/ingest.js`, `src/ua.js`, `src/salt.js`, `test/ingest.test.js`.
**Produces:** `classifyUa(ua) ⇒ { bot:boolean, device:'m'|'t'|'d' }` (kleine eigene Regeln, gängige Bots); `createSalt({ now }) ⇒ { visitorId(site, ip, ua) }` (Rotation 00:00 Berlin); Handler: Body ≤ 4 KB text/plain JSON; Website aktiv + Origin/Referer-Host ∈ domains, sonst 204 ohne Speichern; Felder streng nach Global Constraints validieren/kürzen; Land über `geo.lookup(ip)` (Task 3, injizierbar); Limit 120/Min je (site, ip); CORS-Header nur für passende Origin; Antwort 204; nie `Set-Cookie`.
- [ ] Tests: pv gespeichert ohne visitor; bn nur mit a; en/ck/ct mit visitor; fremde Origin ⇒ nichts; Bot ⇒ nichts; 121. Ereignis ⇒ verworfen; Query/Hash aus path entfernt; ref nur Host und nicht eigene Domain; kein IP/UA in der Datenbank (Tabelleninhalte prüfen); Salz wechselt um Mitternacht Berlin.
- [ ] FAIL → implementieren → PASS → Commit „Analytics: Ereignisse annehmen“
### Task 3: Länder (DB-IP Lite)
**Files:** `src/geo.js`, `scripts/geo-update.sh` (lädt monatliche CSV `dbip-country-lite-YYYY-MM.csv.gz` nach `dataDir/geo.csv`), `test/geo.test.js`.
**Produces:** `loadGeo(file) ⇒ { lookup(ip) ⇒ 'DE'|…|'' }` – IPv4 und IPv6 als BigInt-Bereiche, sortiert, Binärsuche; fehlende Datei ⇒ '' für alles.
- [ ] Tests mit kleiner Fixture-CSV (v4, v6, Grenzen, unbekannt). FAIL → implementieren → PASS → Commit „Analytics: Laenderzuordnung“
### Task 4: Verdichtung, Löschung, Statistik, Heatmap-Daten
**Files:** `src/jobs.js`, `src/stats.js`, `src/heat.js`, `test/stats.test.js`.
**Produces:** `runNightly(db, { now })` (Rohdaten > 1 Tag in `daily` verdichten: dims `pv_path`, `pv_ref`, `pv_country`, `pv_device`, `pv_total`, `visitors`, `en_sec_sum`, `en_count`, `scroll_sum`, `ct_kind`, `bn_shown|accept|reject`; Rohdaten/Klicks > 30 Tage löschen; Websites offSince > 60 Tage: alle Zeilen löschen); `GET /a/admin/stats/:siteKey?range=7|30|90` ⇒ `{ totals:{pv, visitors, contacts, consentRate}, series:[{day,pv}], pages, refs, countries, devices, contacts, banner }` (heute aus Rohdaten + Vergangenheit aus daily); `GET /a/heat?token=` (Token HMAC heatSecret: siteKey|path|vp|exp, abgelaufen/falsch ⇒ 403) ⇒ `{ points:[{x,y}] }` max 5000 neueste.
- [ ] Tests: Verdichtung + idempotent; 31 Tage alte Rohdaten weg; Website nach 61 Tagen weg; Stats-Form; consentRate = accept/(accept+reject); Token fremder Website/abgelaufen ⇒ 403.
- [ ] FAIL → implementieren → PASS → Commit „Analytics: Verdichtung, Statistik, Heatmap-Daten“
### Task 5: Auslieferung der Client-Skripte
**Files:** `src/assets.js`, Route `GET /a/w.js`, `GET /a/h.js` (Cache-Control `public, max-age=3600`, `Content-Type: text/javascript`), Quellen in `client/` (Task 6).
- [ ] Test: beide Dateien ausgeliefert, richtige Header, ETag. → Commit „Analytics: Skripte ausliefern“
### Task 6: Client-Skript `w.js` und Overlay `h.js`
**Files:** `analytics/client/w.js`, `analytics/client/h.js`, `analytics/client/core.js` (reine Funktionen, auch von Tests importiert), `test/client.test.js`.
**Produces (core):** `vpClass(width)`, `cleanPath(href)`, `refHost(referrer, ownHost)`, `cssPath(el)` (≤ 200, ohne IDs mit Zahlen-Hashes), `contactKind(a)` ⇒ `'tel'|'mail'|'wa'|'out'|null` (wa: wa.me / api.whatsapp.com), `readConsent(storage, now)`, `contrastOk(hex)`.
`w.js`: liest `data-site`/`data-color` aus dem eigenen `<script>`; GPC/DNT ⇒ reject ohne Banner; sendet `pv`; Banner laut Global Constraints (zugänglich: Buttons, Fokus, `role="dialog"`, `aria-label`); `bn` bei Anzeige/Klick; nach accept Listener für Klick (`ck`, `ct`), Verlassen (`en` via `visibilitychange`/`pagehide`); Footer-Link bzw. `[data-webklar-consent]` öffnet Banner erneut; Widerruf entfernt Listener sofort; `?__wk_heat=` ⇒ lädt `h.js`, entfernt den Parameter per `history.replaceState`, sendet in diesem Modus nichts.
`h.js`: holt Punkte, zeichnet Canvas-Overlay (radiale Verläufe, Deckkraft), Leiste oben mit Gerätewahl-Hinweis und „Schließen“.
- [ ] Tests (Node, core): alle reinen Funktionen inkl. Grenzfälle. Verhalten von w.js/h.js prüft Task 15 im Browser.
- [ ] Commit „Analytics: Banner-Skript und Heatmap-Overlay“
## Runner
### Task 7: Einsetzen beim Veröffentlichen
**Files:** `runner/src/validate.js` (Feld `analytics` streng prüfen: siteKey, color, privacy {company ≤ 200, address ≤ 300, email ≤ 200, kein Steuerzeichen}), `runner/src/analytics-inject.js`, Aufruf in `jobs.js publish` (auf dem `.new`-Ordner vor dem Tausch), `runner/test/analytics-inject.test.js`.
**Produces:** `injectAnalytics(dir, analytics, { baseUrl })`: in jede `*.html` (rekursiv, ohne node_modules) vor `</head>` (sonst am Anfang von `<body>`, sonst Dateianfang) genau einmal `<script src="https://project.webklar.com/a/w.js" data-site=".." data-color=".." defer></script>`; erzeugt `datenschutz-analyse.html` aus Vorlage `runner/src/templates/datenschutz-analyse.html` (Platzhalter HTML-maskiert; Task 7 legt die Vorlage mit vorläufigem Text und allen Platzhaltern an, Task 13 schreibt den endgültigen Inhalt); ohne `analytics` nichts tun (ein früher eingesetztes Skript existiert im neuen Ordner ohnehin nicht, da aus der Quelle neu gebaut).
- [ ] Tests: Einsetzen in mehrere Dateien, kein Doppel, ohne head, Maskierung (`<script>` im Firmennamen), ohne analytics unverändert, Dev-Modus unberührt.
- [ ] Commit „Runner: Analyse-Skript und Datenschutz-Zusatzseite einsetzen“
## Portal
### Task 8: Schema, Konfiguration, Analytics-Client
**Files:** `scripts/analyse-setup.mjs` (portalFeatures: `accentColor` 7, `avvVersion` 20, `avvAcceptedAt` 40, `avvAcceptedBy` 100, `activatedAt` 40, `offSince` 40, `privacyTicketId` 40; limit 200), `server/config.js` (`analyse: { enabled: ANALYSE_ENABLED==='on', url: ANALYTICS_URL||'http://webklar-analytics:8090', adminToken, heatSecret, avvVersion: '2026-10-01' }`), `server/services/analyticsClient.js` (`putSite`, `deleteSite`, `getStats`, `heatToken(siteKey,path,vp,now)`), Tests.
- [ ] Commit „Analyse: Schema, Konfiguration, Client“
### Task 9: Freischalt-Logik
**Files:** `server/services/analyse.js`, `test/analyse.test.js`.
**Produces:** `analyseEligibility(customerId, projectId) ⇒ { ok, reason }` (Eigentum, Wartungs-Abo aktiv, isPublic, Subdomain); `activateAnalyse({ customerId, projectId, accentColor, consent, by })` (siteKey aus portalFeatures-Dokument `featureKey:'analyse'` wiederverwenden/neu, Zustand active, AVV-Felder, `putSite` mit Domains, Neubau über `runDeploy(..., { project })`, Ticket über `createFeatureTicket` mit `featureKey:'analyse'` und Topic „Datenschutzerklärung von <Projekt> um Hinweis auf Analyse ergänzen“, Mail K18 „Deine Website-Analyse ist aktiv“); `deactivateAnalyse(...)` (off, offSince, putSite active false, Neubau); `runAboCheck({ now })` (täglich: aktiv ohne Wartungs-Abo ⇒ deactivate + Mail K19 „Website-Analyse beendet“). Idempotent.
- [ ] Tests: ohne Abo/Hosting-Abo ⇒ abgelehnt; nicht öffentlich ⇒ abgelehnt; AVV fehlt ⇒ 400; doppelt aktivieren ⇒ ein Ticket; deaktivieren ⇒ offSince; Abo-Ende-Job.
- [ ] Commit „Analyse: Freischalten, Abschalten, Abo-Pruefung“
### Task 10: Runner-Auftrag mit Analyse-Feld
**Files:** `server/services/previewDeploy.js` (runDeployViaRunner + registerDevRound unverändert für dev), Test.
Im Build-Auftrag `analytics` setzen, wenn portalFeatures(analyse) `active` und Projekt öffentlich: `{ siteKey, color, privacy: { company: customer.companyName||customer.name, address: street + PLZ Ort, email: customer.email } }`; sonst `null`.
- [ ] Tests: aktiv+öffentlich ⇒ Feld; aus ⇒ null; nicht öffentlich ⇒ null. Commit „Analyse: Runner-Auftrag“
### Task 11: Routen
**Files:** `server/routes/funktionen.js` (`GET` liefert Analyse-Zustand je Projekt + eligibility; `POST /analyse/activate`, `/analyse/deactivate`, `GET /analyse/stats?projectId&range`, `POST /analyse/heatmap-link {projectId,path,vp}` ⇒ `{ url }`), `server/routes/admin/analyse.js` (`POST /api/admin/analyse/:projectId { on, avvConfirmedVerbally }`), Schalter aus ⇒ 409; Tests.
- [ ] Commit „Analyse: Routen fuer Portal und WOMS“
### Task 12: Portal-Oberfläche
**Files:** `public/funktionen.js` (+ CSS), neue Ansicht `#view-analyse` (Hash `#analyse`, App-Unterseite wie News).
Kachel: Zustände (kein Abo / nicht öffentlich / aus / aktiv); Einschalt-Dialog (Erklärung, Farbwahl + Live-Vorschau des Banners, AVV-Haken mit Link zur AVV-Fassung, Knopf); Auswertung: Zeitraum, Kennzahl-Karten, SVG-Liniendiagramm, Listen (Seiten, Herkunft, Länder mit Flaggen-Emoji, Geräte, Kontakt-Klicks, Banner), Hinweis zu Einwilligung, Heatmap-Auswahl (Seite aus Top-Seiten, Gerät) + Knopf; DB-IP-Namensnennung klein unten; „Analyse ausschalten“ mit In-Seite-Bestätigung. 390 px tauglich.
- [ ] `npm test` grün; Commit „Analyse: Portal-Kachel und Auswertung“
## Rechtstexte und WOMS
### Task 13: Rechtstexte (Entwürfe)
**Files:** `docs/rechtliches/avv-website-analyse.md`, `leistungsbeschreibung-website-analyse.md`, `abo-bedingungen-ergaenzung-analyse.md`, Vorlage `runner/src/templates/datenschutz-analyse.html` (Inhalt laut Spec, Platzhalter `{{company}}`, `{{address}}`, `{{email}}`), Absatz in `public/datenschutz.html` (WEBklar als Auftragsverarbeiter für Kunden-Websites).
- [ ] Commit „Analyse: Rechtstexte (Entwuerfe, anwaltlich zu pruefen)“
### Task 14: WOMS-Schalter und Services
**Files (WOMS):** `src/lib/analyseApi.js`, Zeile in `src/components/AssignedProjectCard.jsx` („Website-Analyse: an/aus“, Einschalten nur mit Haken „Kunde hat AVV bestätigt“), Services-Statuslabel für `analyse` aktiv.
- [ ] `npm test` + Build; Commit „Analyse: Schalter im WOMS“
### Task 15: Browser-Prüfung (Playwright)
Testseite + Sammeldienst lokal (Testdaten-Verzeichnis), echtes `w.js`: ohne Annehmen nur `pv`+`bn` im Netzwerk; Annehmen ⇒ `ck`/`ct`/`en`; Ablehnen + Widerruf; GPC; Banner 390 px; Heatmap-Overlay mit Token; Portal-Ansichten mit Fixtures. Screenshots `/root/reports/website-analyse-2026-10-01/`.
## Livegang (jeder Schritt mit OK des Nutzers)
- **L1** `/srv/webklar-analytics`, `.env` (`ANALYTICS_ADMIN_TOKEN`, `ANALYTICS_HEAT_SECRET`), Compose-Dienst `analytics` (node:24-alpine, Code ro, webklar-network, Traefik-Router `Host(project.webklar.com) && PathPrefix(/a/)` Priorität > Portal, kein IP-Access-Log), `geo-update.sh` + monatlicher Cron, Schema-Skript, `deploy.sh` Neustart bei `analytics/`-Änderung.
- **L2** Portal/Runner/Analytics pushen (`ANALYSE_ENABLED=off`).
- **L3** Probe: öffentliche Wegwerf-Website, Analyse per Admin an, Banner/Ereignisse/Statistik/Heatmap prüfen, Abschalten ⇒ Skript weg, aufräumen.
- **L4** WOMS pushen; Nutzer lädt Rechtstexte in „Rechtliches“; `ANALYSE_ENABLED=on`.
- **L5** Live-Test des Nutzers mit einer echten Kunden-Website mit Wartungs-Abo.