15 KiB
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;bnohne weitere Felder;en/ck/ctnur 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
- Ohne „Annehmen“ verlässt kein
en/ck/ctden Browser (auch nicht nach Widerruf). → Task 6 - Fremde Origin oder unbekannter siteKey ⇒ nichts gespeichert. → Task 2
- Analyse aus / Abo weg ⇒ nach Neubau kein Skript und keine Zusatzseite mehr im Ordner. → Task 7, 10
- Heatmap-Token einer Website liefert keine Punkte einer anderen Website. → Task 4
- 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 testgrü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-Dienstanalytics(node:24-alpine, Code ro, webklar-network, Traefik-RouterHost(project.webklar.com) && PathPrefix(/a/)Priorität > Portal, kein IP-Access-Log),geo-update.sh+ monatlicher Cron, Schema-Skript,deploy.shNeustart beianalytics/-Ä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.