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

15 KiB
Raw Blame History

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 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.