Plan: Website-Analyse Pflicht-Aenderungen aus der Rechtspruefung
This commit is contained in:
1 parent
9449542686
commit
c2cbc4156f
1 file changed
+323
@@ -0,0 +1,323 @@
|
||||
# Website-Analyse – Pflicht-Änderungen aus der Rechtsprüfung: 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:** Die technischen Voraussetzungen der geprüften Rechtstexte (Fassung 2026-10-02) herstellen, damit `ANALYSE_ENABLED=on` ohne falsche Angaben möglich ist: Einwilligung und Widerspruch wirken im Browser, der Messdienst speichert weniger und löscht verlässlich, jede Beendigung wird durchgesetzt, Einschalten ist nachweisbar (Historie, Textform für Mitarbeiter), die geprüften Texte sind veröffentlicht und gehen als PDF an den Kunden.
|
||||
|
||||
**Architecture:** Änderungen in vier Bereichen auf dem bestehenden Stand (`feature/website-analyse` = live `main` 30412cb): (1) Client `analytics/client/` (`w.js`, `core.js`, committetes `dist/`), (2) Messdienst `analytics/src/` (Annahme, Nachtjob, Skript-Auslieferung, Admin-Route), (3) Portal `server/` + `public/` (Aktivierung, Historie, Texte, Mails, Tickets, Beendigungswege), Runner `runner/` (Skript-URL, Zusatzseiten-Vorlage), (4) WOMS (Textform-Formular). Ein Datenmodell-Schritt (`scripts/analyse-setup.mjs`) kommt vor dem Portal-Code live.
|
||||
|
||||
**Tech Stack:** Node 24 (Messdienst, `node:sqlite`, `node --test`), Node 20 (Portal/Runner), vanilla JS (Portal-UI, Client), React + `node --test` (WOMS; `package.json` nutzt `node --test tests/*.test.mjs tests/unit/*.test.mjs`, nicht vitest), Chrome headless (PDF).
|
||||
|
||||
**Spec:** `/root/reports/website-analyse-2026-10-01/rechtstexte-geprueft/technik-aenderungen.md` (T1–T22), `/root/reports/website-analyse-2026-10-01/rechtstexte-geprueft/00-gutachten.md` (Abschnitt „Entscheidungen für dich“, E1–E6, und P-Tabelle). Texte: `/root/reports/website-analyse-2026-10-01/rechtstexte-geprueft/banner-portal-mailtexte.md` (im Plan nur als „§ n“ zitiert, nicht neu getippt), `avv-website-analyse.{md,html}`, `leistungsbeschreibung-website-analyse.{md,html}`, `datenschutz-analyse.html`. Bisheriger Plan/Design: `docs/superpowers/plans/2026-10-01-website-analyse.md`, `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 `/home/webklar/apps/ticket.webklar.com/dev-analyse`. Quelle der Textdateien: `RT = /root/reports/website-analyse-2026-10-01/rechtstexte-geprueft`.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Umfang: T1–T11, T13–T17, T19 (T5 und T18 sind Compose-Änderungen und gehören in den Livegang L1). Nicht im Plan: T12, T20, T21, T22 (siehe „Außerhalb des Plans“).
|
||||
- Fassung überall `'2026-10-02'`: `config.analyse.avvVersion`, `BANNER_VERSION` im Client, Dateinamen der veröffentlichten Texte, Textform-Mindestdatum.
|
||||
- Entscheidungen: E1=B (Ping nur mit Seitenadresse ohne Einwilligung), E2 (WEBklar fügt den Hinweis in die Datenschutzerklärung ein, keine Anrechnung; externer Generator ⇒ Baustein per Chat), E3 (Mitarbeiter schaltet nur mit Textform ein: Pflichtfelder Datum und Weg), E4 (Du-Anrede auf Kunden-Websites), E5 (Tageswerte nach 25 Monaten löschen), E6 (nur Text, keine Technik).
|
||||
- Subagents pushen nie (kein `git push`, auch keine Tags pushen). Lokale Commits und lokale Tags sind erlaubt. Ich (Controller) pushe auch nicht auf `main`; das macht der Nutzer (L2, L4).
|
||||
- `ANALYSE_ENABLED` bleibt `off` bis L5. Nichts live schalten, keine Compose-Datei ändern (liegt außerhalb der Worktrees).
|
||||
- Texte deutsch, Du-Anrede im Portal und auf den Kunden-Websites; kein `confirm()`. Texte aus `banner-portal-mailtexte.md` werden 1:1 aus dem jeweiligen Abschnitt kopiert, nicht umformuliert. Neue Texte, die dort fehlen (Fehlermeldungen, Ticket-Details für „veröffentlichen“), sind im Plan mit „NEU“ markiert und kurz zu halten; der Nutzer sieht sie im Review.
|
||||
- Keine neuen Abhängigkeiten im Messdienst und im Client-Skript. Keine IP, kein User-Agent gespeichert oder geloggt.
|
||||
- Datenbank-Schreibzugriffe im Portal nur auf Attribute, die `scripts/analyse-setup.mjs` anlegt (Test-Mock erzwingt das).
|
||||
- Commit-Messages deutsch, Präfix „Analyse: …“, enden mit der Attribution-Zeile der Sitzung (`Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>`).
|
||||
- Testkommandos: Messdienst `cd analytics && npm test`; Runner `cd runner && npm test`; Portal im Worktree-Wurzelordner `npm test`; WOMS `npm test`. Nach jeder Task müssen alle vier Suiten grün sein, in denen die Task Dateien berührt.
|
||||
|
||||
## Review Focus
|
||||
|
||||
1. Nach „Ablehnen“, bei GPC/DNT und bei gespeichertem „Ablehnen“ verlässt kein `pv`/`bn`/`en`/`ck`/`ct` mehr den Browser, auch nicht über `pushState`/`popstate`/`pageshow` oder andere Tabs; vor „Annehmen“ weder `ref`/`vp` noch ein Lesezugriff auf `document.referrer`/`innerWidth`. → Task 1
|
||||
2. Keine millisekundengenauen Zeitstempel (`events_raw`, `clicks`) und keine kennungsartigen Pfadsegmente in der Datenbank; nichts davon ist umgehbar über den neuen Typ `cx`. → Task 2
|
||||
3. Jede Beendigung (Ausschalten, Abo-Ende, Kunde/Projekt weg, Domain/Öffentlichkeit weg, Löschfrist, Widerruf Wechsel) setzt `active=false` + `offSince` im Messdienst; danach liefert `w.js?s=` ein leeres Skript; Tageswerte > 25 Monate und gelöschte Zeilen sind aus Datei und WAL verschwunden. → Task 3, 4, 9
|
||||
4. Einschalten ohne Firma/Name oder gültige E-Mail ist unmöglich (statt stillem „aktiv“); Mitarbeiter-Weg ohne Datum und Weg unmöglich; Einwilligungs-Historie ist nur anfügbar. → Task 5, 7, 10
|
||||
5. Veröffentlichte Texte, PDFs, K18-Anhänge, Banner-Text und Zusatzseiten-Vorlage sind byte-/textgleich zur geprüften Fassung 2026-10-02; keine Entwurfsseite bleibt öffentlich. → Task 6, 8
|
||||
|
||||
---
|
||||
|
||||
## Verifizierter Stand (Abweichungen zu `technik-aenderungen.md`)
|
||||
|
||||
Die Zeilenangaben stimmen mit dem Live-Code überein (`w.js:44,55,65,155,162-168,177-231,240-245`; `ingest.js:129,142,151,160,167`; `jobs.js:70-84`; `db.js:57`; `analyse.js:94-102,104-121,123-140,200-218,221-242`; `funktionen.js:9,595,608,619,763`). Befunde, die der Plan zusätzlich berücksichtigt:
|
||||
|
||||
- `activateAnalyse` setzt bei erneutem Einschalten `activatedMailAt` zurück, **nicht** `privacyTicketId` ⇒ nach Aus/An gäbe es kein neues Ticket. Task 5/8 setzt es zurück.
|
||||
- Der tägliche `runAboCheck` läuft schon heute unabhängig vom Schalter (`server/index.js:175`). Der neue Abgleich (Task 9) hängt sich dort ein.
|
||||
- `retention.js` ruft `siteFeatures.disable()`, das über `stopAnalyseCollecting` nur `putSite(..., active:false)` mit Domains aus dem Projekt versucht; fehlt das Projekt, scheitert es still (nur `console.warn`). Task 9 ersetzt das durch einen domainlosen Aufruf.
|
||||
- Im Portal-Code gibt es keinen Pfad, der `websiteProjects` löscht oder `liveDomain` schreibt (nur WOMS/Appwrite-Konsole). Gelöschte Projekte und geänderte Live-Domains werden deshalb über den täglichen Abgleich erfasst; Subdomain-/Öffentlichkeitsänderungen laufen über `giteaAdmin.updateWebsiteProject` (dort Hook).
|
||||
- Es gibt keine Historien-Sammlung; `portalFeatures` hat nur das aktuelle Dokument. Neue Collection `analyseConsents` (Task 5).
|
||||
- Der Client sendet heute über `navigator.sendBeacon` (`w.js:39`); die Test-Harness (`analytics/test/client.test.js`, `harness()`) fängt nur `sendBeacon` ab und muss auf `fetch` umgestellt werden.
|
||||
- Die veröffentlichte AVV-Seite hat den Titel „Entwurf – vor Verwendung anwaltlich prüfen lassen“, `test/avvPage.test.js` prüft genau das und kippt bewusst (Task 6).
|
||||
- `runner/test/analytics-inject.test.js` erwartet im Zusatzseiten-Text „Cookie-Einstellungen“; die geprüfte Vorlage sagt „Datenschutz-Einstellungen“.
|
||||
- PDFs: `scripts/build-legal-pdfs.sh` (Chrome headless, Chromium unter `~/.cache/ms-playwright/chromium-1234/chrome-linux64/chrome` vorhanden, PDFs werden eingecheckt, Commit d4241d2 war so entstanden). Das Skript erzeugt alle Vertrags-PDFs auf einmal; Task 6 ergänzt einen Modus `analyse`, damit andere PDFs unverändert bleiben. K01 hängt PDFs bereits über `sendCustomerMail(..., { attachments:[{filename, content, contentType}] })` an; K18 nutzt denselben Weg.
|
||||
|
||||
---
|
||||
|
||||
## Teil A – Messdienst und Client
|
||||
|
||||
### Task 1: Client – Einwilligung vor Senden (T1, T2-Client, T4-Client, T15, T19, Banner-Texte aus T10)
|
||||
|
||||
**Files:** `analytics/client/core.js`, `analytics/client/w.js`, `analytics/client/dist/w.js` + `dist/h.js` (neu bauen: `node client/build.js`), `analytics/test/client.test.js`.
|
||||
|
||||
**Produces (core.js):** `export const BANNER_VERSION = '2026-10-02'`; Textkonstanten `BANNER_TEXT` (Fließtext bis einschließlich „Details: “, 1:1 aus `banner-portal-mailtexte.md` § 1), `BANNER_LINK_LABEL = 'Datenschutz'`, `BANNER_ARIA = 'Einwilligung zur Reichweitenmessung'`, `SETTINGS_LABEL = 'Datenschutz-Einstellungen'`. `readConsent(storage, now)` liefert nur noch Einträge mit `c.bv === BANNER_VERSION` und gibt `{ v:1, a, t, bv }` zurück; ältere Einträge ohne/mit anderer `bv` zählen als nicht vorhanden (Besucher wird neu gefragt). Schlüssel `wk-consent` bleibt.
|
||||
|
||||
**Ablauf in `w.js` (neu geordnet):**
|
||||
1. Skript-/siteKey-Prüfung und Heatmap-Modus bleiben vorn, unverändert.
|
||||
2. `optOut` (GPC/`doNotTrack`/`msDoNotTrack`) und `stored = readConsent(...)` werden **vor** jedem Senden und vor jedem Hook berechnet. `optOut` ⇒ `return`: kein `pv`, kein Banner, kein Einstellungsknopf, kein `pageshow`/`popstate`/`pushState`-Hook.
|
||||
3. `choiceNow()` liest die Wahl bei jedem Aufruf frisch (Speicher, sonst `memChoice`); `counting() = choiceNow() !== 'reject'`.
|
||||
4. `sendPv(first)` bricht ohne `counting()` ab. Nutzlast ohne Wahl exakt `{ t:'pv', site, path }`; erst bei `choiceNow()==='accept'` kommen `ref` (nur `first`) und `vp` hinzu. `document.referrer` und `innerWidth` werden ausschließlich in diesem Zweig gelesen.
|
||||
5. Hooks (`pageshow`, `pushState`-Wrapper, `popstate`) nur installieren, wenn beim Start `choiceNow() !== 'reject'`; `scheduleNav()` prüft `counting()` im Timer-Callback erneut. `ensureHooks()` ist idempotent (spätes „Annehmen“ nach gespeichertem „Ablehnen“ installiert sie nachträglich).
|
||||
6. `choose('reject')`: Wahl speichern, genau ein `bn` mit `a:'reject'`, `stopTracking()`, danach sendet nichts mehr (auch nicht `pv` per Seitenwechsel). `choose('accept')`: `bn accept`, `startTracking()`, dann `onAccept()`: wurde auf dieser Seite schon ein `pv` gesendet, genau einmal `cx` `{ t:'cx', site, path:lastPath, ref:refHost(DOC.referrer, ownHost()), vp:vp() }` (Flag `cxSent`), sonst `sendPv(true)` mit `ref`/`vp`.
|
||||
7. `storage`-Handler: `reject` ⇒ `stopTracking()` (Zählen endet über `counting()`), `accept` ⇒ `onAccept()`, geleerter Eintrag ⇒ unverändert „keine Wahl“.
|
||||
8. Gespeichertes „Ablehnen“: nichts senden, nur `addSettingsLink()` und `[data-webklar-consent]`-Delegation; gespeichertes „Annehmen“: `pv` mit `ref`+`vp` wie bisher, kein Banner, kein `cx`.
|
||||
9. `send(obj)`: ausschließlich `fetch(endpoint, { method:'POST', body, keepalive:true, credentials:'omit', headers:{ 'Content-Type':'text/plain' } }).catch(()=>{})`; kein `navigator.sendBeacon` mehr.
|
||||
10. Banner: `aria-label` = `BANNER_ARIA`, Fließtext = `BANNER_TEXT` + Link (`BANNER_LINK_LABEL` → `/datenschutz-analyse.html`), Knopf zum Wiederöffnen = `SETTINGS_LABEL`. `choose()` speichert `{ v:1, a, t, bv: BANNER_VERSION }`.
|
||||
11. T19: Solange das Banner offen ist, setzt `openBanner()` `DOC.body.style.paddingBottom` auf die Banner-Höhe (`bannerHost.getBoundingClientRect().height || offsetHeight`, aufgerundet, in px; bei `resize` nachziehen) und stellt beim Schließen den vorherigen Inline-Wert wieder her. Einstellungsknopf: `min-width:24px;min-height:32px;padding:8px 12px;font-size:13px`.
|
||||
|
||||
**Tests (zuerst, müssen scheitern; `harness()` auf `fetch`-Capture umbauen, `document.referrer` und `win.innerWidth` als zählende Getter):**
|
||||
- [ ] `core`: `readConsent` verwirft `{v:1,a:'accept',t}` ohne `bv`, verwirft `bv:'2026-01-01'`, akzeptiert `bv:'2026-10-02'`; abgelaufen (≥ 6 Monate) weiter `null`.
|
||||
- [ ] GPC (`navigator.globalPrivacyControl=true`) und DNT `'1'`: `h.sent.length === 0`; `h.winL.pageshow`/`h.winL.popstate` undefiniert; `history.pushState` ist die Original-Funktion; `h.body.children.length === 0` (kein Banner, kein Einstellungsknopf).
|
||||
- [ ] Gespeichertes `{a:'reject',bv}`: `sent` leer, nur Einstellungsknopf in `body`, keine `pageshow`/`popstate`-Listener; Klick auf den Knopf öffnet das Banner ohne Senden; dort „Annehmen“ ⇒ Typen `['bn:accept','pv']` und das `pv` trägt `ref` und `vp`.
|
||||
- [ ] Keine Wahl: erstes Ereignis ist `deepEqual({t:'pv', site, path:'/start'})` (genau diese drei Schlüssel), danach `bn:shown`; Zähler für `document.referrer` und `innerWidth` = 0.
|
||||
- [ ] „Ablehnen“: Typen enden auf genau ein `bn:reject`; danach `pushState` + Timer feuern, `pageshow{persisted:true}`, Scroll, Klick, `pagehide` ⇒ nichts Neues. Zweiter Tab: Speicher auf reject setzen + `storage`-Ereignis, danach Seitenwechsel ⇒ nichts Neues; Seitenwechsel-Timer, der vor dem Ablehnen geplant war, sendet nichts.
|
||||
- [ ] „Annehmen“ auf laufender Seite (zuvor `pv` ohne Daten): Reihenfolge `bn:accept`, `cx` mit `ref === refHost('https://www.google.com/','mein.de')` und `vp:'d'`; zweites „Annehmen“ nach Widerruf sendet kein zweites `cx` für dieselbe Seite.
|
||||
- [ ] Gespeichertes „Annehmen“: erstes `pv` hat `ref` und `vp`, kein `cx`.
|
||||
- [ ] T15: `navigator.sendBeacon` ist ein Spy und wird nie aufgerufen; `fetch` bekommt `keepalive:true`, `credentials:'omit'`, `method:'POST'`, Header `Content-Type: text/plain`.
|
||||
- [ ] T19: Fake-`offsetHeight=90` ⇒ `body.style.paddingBottom === '90px'` bei offenem Banner, nach Schließen wieder der Ausgangswert; Shadow-CSS des Einstellungsknopfs enthält `min-width:24px` und `min-height:32px`.
|
||||
- [ ] Banner-Aufbau: `aria-label === BANNER_ARIA`, Text beginnt mit `BANNER_TEXT`, Knopfname `SETTINGS_LABEL`; Konstanten stimmen mit § 1 überein (Test liest `RT/banner-portal-mailtexte.md` nicht, sondern vergleicht in Task 6 gegen die Archivdatei).
|
||||
- [ ] Bestehende Tests anpassen, die bewusst kippen: „GPC und DNT ⇒ nur pv“ (jetzt: nichts), „ohne Einwilligung nur pv + bn shown“ (jetzt `pv` ohne `ref`/`vp`), sendBeacon-Erwartungen; `dist ist aktuell` nach Neubau grün.
|
||||
- [ ] FAIL → implementieren → `cd analytics && node client/build.js && npm test` PASS → Commit „Analyse: Client prüft Einwilligung vor dem Senden, Fassung der Banner-Wahl, fetch ohne Cookies“
|
||||
|
||||
### Task 2: Messdienst – Annahme (T2-Server, T3, T14)
|
||||
|
||||
**Files:** `analytics/src/ingest.js`, `analytics/src/jobs.js` (nur `aggregateRows`), `analytics/src/heat.js` (Sortierung), `analytics/test/ingest.test.js`, `analytics/test/stats.test.js`.
|
||||
|
||||
**Produces:**
|
||||
- `FIELDS.pv = ['t','site','path','ref','vp']` bleibt, `vp` ist optional: fehlt `vp` (nicht vorhanden/`undefined`), speichert `store()` `vp = NULL`; ein vorhandener, ungültiger `vp` verwirft das Ereignis wie bisher.
|
||||
- `FIELDS.cx = ['t','site','path','ref','vp']`; `store()`: `vp` gültig Pflicht, `ref` über `cleanRef`, Zeile `type='cx'`, `ref`, `vp`, `country`/`device` leer.
|
||||
- T3: alle gespeicherten Zeitstempel `Math.floor(t / HOUR) * HOUR` (`pv`, `cx`, `en`, `ct` in `events_raw`; `ck` in `clicks`). Berliner Offsets sind ganze Stunden, daher bleibt jeder Zeitstempel im selben Berliner Tag; `heat.js` sortiert `ORDER BY ts DESC, id DESC`.
|
||||
- T14: `cleanPath()` ersetzt Segmente, die kennungsartig aussehen, durch `:id`: Segment (auch nach `decodeURIComponent`, Fehler ⇒ Rohwert) mit Ziffernfolge `/\d{6,}/`, UUID `/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i` oder enthält `@` (E-Mail, auch `%40`). Danach weiter Länge ≤ 300 und Steuerzeichen-Prüfung. Gilt für alle Typen (`pv`, `cx`, `en`, `ck`, `ct`).
|
||||
- `jobs.js aggregateRows`: `pv` zählt `pv_total` und `pv_path`/`pv_country`/`pv_device` wie bisher, `pv_ref` aber **nur** wenn `r.vp != null` (nur Besucher mit Einwilligung); `cx` zählt ausschließlich `pv_ref` (`r.ref || ''`), nie `pv_total`.
|
||||
|
||||
**Tests:**
|
||||
- [ ] `pv` ohne `ref`/`vp` (`{t:'pv',site,path:'/'}`) ⇒ 1 Zeile, `vp IS NULL`, `ref IS NULL`, `device` aus User-Agent, `country` aus Geo.
|
||||
- [ ] `pv` mit `vp:'x'` ⇒ verworfen; `cx` ohne gültiges `vp` ⇒ verworfen; `cx` mit `ref:'https://www.google.com/x'` ⇒ Zeile `type='cx'`, `ref='www.google.com'`; `cx` mit eigener Domain als `ref` ⇒ `ref IS NULL`.
|
||||
- [ ] Zeitstempel: `now = Date.UTC(2026,9,1,10,30,45)` ⇒ für `pv`, `cx`, `en`, `ct` und `clicks.ts` jeweils `=== Date.UTC(2026,9,1,10,0,0)`; kein Wert mit `ts % 3600000 !== 0`.
|
||||
- [ ] Pfade: `/kunde/123456789/rechnung` ⇒ `/kunde/:id/rechnung`; `/u/3f2b8c1e-5d4a-4e0b-9a7c-1b2c3d4e5f60` ⇒ `/u/:id`; `/mail/max%40example.com` und `/mail/max@example.com` ⇒ `/mail/:id`; unverändert bleiben `/preise/2026`, `/blog/12345`, `/`; Query/Hash weiter abgeschnitten; gilt auch für `en`, `ck`, `ct`.
|
||||
- [ ] Verdichtung: Zeilen `pv(vp NULL)` + `cx(ref 'google.com')` ⇒ `pv_total` 1, `pv_ref` genau `{google.com:1}` (kein `''`-Schlüssel); `pv(vp 'd', ref NULL)` ⇒ `pv_ref {'':1}`; `cx` erhöht `pv_total` nie; Stats-Endpunkt liefert `refs` entsprechend (live und aus `daily`, nie doppelt).
|
||||
- [ ] Heatmap: zwei Klicks gleicher Stunde, `LIMIT 1` liefert den mit höherer `id`.
|
||||
- [ ] FAIL → implementieren → `cd analytics && npm test` PASS → Commit „Analyse: Messdienst: Ping ohne Einwilligung, Zeitstempel auf Stunde, Pfade ohne Kennungen“
|
||||
|
||||
### Task 3: Messdienst – Aufbewahrung, sicheres Löschen, Export (T6-Messdienst, T7, E5, T16)
|
||||
|
||||
**Files:** `analytics/src/db.js`, `analytics/src/jobs.js`, `analytics/scripts/export-site.mjs` (neu), `analytics/test/stats.test.js` (oder neu `analytics/test/retention.test.js`), `analytics/test/export.test.js` (neu).
|
||||
|
||||
**Produces:**
|
||||
- `db.js openDb`: nach `journal_mode = WAL` zusätzlich `db.exec('PRAGMA secure_delete = ON')`.
|
||||
- `jobs.js`: `export const DAILY_MONTHS = 25`; `dailyCutoffDay(now) ⇒ 'YYYY-MM-DD'` = Berliner Tag von `now` minus 25 Kalendermonate (Tag am Monatsende auf den letzten Tag des Zielmonats begrenzt). `runNightly`: nach der Verdichtung `DELETE FROM daily WHERE day < ?` und `DELETE FROM agg_state WHERE day < ?` mit diesem Tag (alle Websites). Verwaiste Einträge: (a) `UPDATE sites SET offSince = ? WHERE active = 0 AND offSince IS NULL` (Frist beginnt dann bei Entdeckung), (b) Zeilen in `events_raw`/`clicks`/`daily`/`agg_state`, deren `site` in `sites` fehlt, werden gelöscht. Am Ende (nach allen Transaktionen) `try { db.exec('PRAGMA wal_checkpoint(TRUNCATE)') } catch (e) { log }`. Rückgabe erweitert: `{ aggregated, sitesDeleted, dailyDeleted, orphansMarked, orphanRowsDeleted }`; `scheduleNightly` loggt die neuen Zähler.
|
||||
- `analytics/scripts/export-site.mjs`: `exportSite({ dbPath, siteKey, outDir })` öffnet die Datenbank **nur lesend** (`new DatabaseSync(dbPath, { readOnly: true })`) und schreibt drei CSV-Dateien (RFC 4180, UTF-8, `\r\n`): `<siteKey>-tageswerte.csv` (Kopf `day,dim,key,count`), `<siteKey>-ereignisse.csv` (Kopf `time,type,path,vp,ref,country,device,visitor,sec,scroll,kind,host`, `time` als ISO-UTC), `<siteKey>-klicks.csv` (Kopf `time,path,vp,x,y,sel`). Zellen, die mit `=`, `+`, `-` oder `@` beginnen, bekommen ein vorangestelltes `'`. CLI: `node scripts/export-site.mjs <siteKey> [--db <Pfad>] [--out <Ordner>]`; Standard `--db /srv/webklar-analytics/analytics.db`, Exit-Code 1 bei ungültigem Schlüssel oder unbekannter Website.
|
||||
|
||||
**Tests:**
|
||||
- [ ] `dailyCutoffDay(Date.UTC(2026,9,1,10))` = `'2024-09-01'`; `dailyCutoffDay` für 2026-03-31 = `'2024-02-29'`.
|
||||
- [ ] `daily`-Zeilen mit `day` `'2024-08-31'` werden gelöscht, `'2024-09-01'` bleibt; zugehörige `agg_state`-Zeilen ebenso; `dailyDeleted` zählt.
|
||||
- [ ] Website `active=0, offSince NULL` ⇒ nach Lauf `offSince` gesetzt (= `new Date(now).toISOString()`), zweiter Lauf ändert den Wert nicht; Website mit gesetztem `offSince` > 60 Tage wird weiter komplett gelöscht.
|
||||
- [ ] Zeilen ohne `sites`-Eintrag werden gelöscht, Zeilen von Websites mit Eintrag bleiben.
|
||||
- [ ] `secure_delete`: Datei-DB im Temp-Ordner, Marker-String `'GEHEIMER-PFAD-4711'` als `events_raw.path` einfügen, löschen, `wal_checkpoint(TRUNCATE)`; Rohbytes der `.db`-Datei enthalten den Marker nicht; `PRAGMA secure_delete` liefert 1.
|
||||
- [ ] `runNightly` auf Datei-DB: danach `statSync(path + '-wal').size === 0`.
|
||||
- [ ] Export: 3 Dateien mit den exakten Kopfzeilen; Zeilenzahlen = Zeilen der Website (andere Website erscheint nicht); Zelle `a,"b"\nc` korrekt gequotet; `sel` `'=1+1'` ⇒ `'=1+1`; Aufruf mit unbekanntem Schlüssel ⇒ Exit 1 ohne Dateien; Datei-DB bleibt byte-gleich (Hash vor/nach); Schreibversuch über die geöffnete Verbindung wirft.
|
||||
- [ ] FAIL → implementieren → `cd analytics && npm test` PASS → Commit „Analyse: Tageswerte nach 25 Monaten löschen, secure_delete, CSV-Export“
|
||||
|
||||
### Task 4: Leeres Skript für ausgeschaltete Websites, siteKey in der Skript-URL (T13a)
|
||||
|
||||
**Files:** `analytics/src/assets.js`, `analytics/src/server.js`, `runner/src/analytics-inject.js` (`scriptTag`), `analytics/test/assets.test.js`, `analytics/test/sites.test.js`, `runner/test/analytics-inject.test.js`, `runner/test/jobs.test.js` (falls Tag erwartet).
|
||||
|
||||
**Produces:**
|
||||
- `createAssets({ distDir, log, isActive })`: `/a/w.js` liefert nur dann das echte Skript, wenn `?s=` dem Muster `^[a-z0-9]{16,40}$` entspricht **und** `isActive(s)` wahr ist; sonst (ohne `s`, ungültig, unbekannt, ausgeschaltet) HTTP 200 mit Körper `/* WEBklar Analyse: aus */\n`, Header `Content-Type: text/javascript; charset=utf-8`, `Cache-Control: public, max-age=60`, `X-Content-Type-Options: nosniff`, `Cross-Origin-Resource-Policy: cross-origin`, `Access-Control-Allow-Origin: *`, `Vary: Accept-Encoding`. Echtes Skript und `/a/h.js` behalten ihre bisherigen Header (`max-age=3600`, ETag, gzip). 503 bei fehlender Datei bleibt.
|
||||
- `server.js`: `createAssets({ distDir, isActive: (k) => !!db.prepare('SELECT 1 FROM sites WHERE siteKey = ? AND active = 1').get(k) })`.
|
||||
- Runner: `scriptTag()` ergibt `<script data-wk-analytics src="${baseUrl}w.js?s=${siteKey}" data-site="${siteKey}" data-color="${color}" defer></script>`. `w.js` entfernt die Query bereits (`src.replace(/[?#].*$/,'')`), `data-site` bleibt für Alt-Fassungen. Alte Tags ohne `?s=` werden beim nächsten Neubau ersetzt (bestehende Ersetzungslogik).
|
||||
|
||||
**Tests:**
|
||||
- [ ] Aktive Website, `GET /a/w.js?s=<key>` ⇒ echtes Skript, `max-age=3600`, ETag, `304` bei `If-None-Match`.
|
||||
- [ ] Unbekannter Schlüssel, ungültiger Schlüssel (`?s=ABC`), kein `s`, ausgeschaltete Website ⇒ Körper `/* WEBklar Analyse: aus */\n`, Status 200, `Cache-Control: public, max-age=60`, kein `Content-Encoding`, kein ETag.
|
||||
- [ ] Ende-zu-Ende mit Admin- und Public-Server auf einer DB: `PUT active:true` ⇒ echtes Skript; `PUT active:false` ⇒ sofort leer; erneut `true` ⇒ echt.
|
||||
- [ ] Runner: Tag enthält `w.js?s=abcdef0123456789abcd`; zweimal ausführen ⇒ ein Tag; alter Tag ohne `?s=` wird ersetzt; ungültige Eingaben werfen weiter.
|
||||
- [ ] FAIL → implementieren → `cd analytics && npm test` und `cd runner && npm test` PASS → Commit „Analyse: Skript nur für aktive Websites, leeres Skript sonst, siteKey in der Skript-URL“
|
||||
|
||||
---
|
||||
|
||||
## Teil B – Portal und Runner
|
||||
|
||||
### Task 5: Datenmodell, Einwilligungs-Historie, Aktivierung mit Nachweis und Sperre (T8 Server, T9 Server)
|
||||
|
||||
**Files:** `scripts/analyse-setup.mjs`, `server/config.js`, `server/services/analysePrivacy.js` (neu, rein, ohne Imports), `server/services/analyseConsent.js` (neu), `server/services/analyse.js`, `server/services/previewDeploy.js` (`buildAnalytics`), `server/routes/funktionen.js`, `server/routes/admin/analyse.js`, `test/analyseSetup.test.js`, `test/analyse.test.js`, `test/analyseRoutes.test.js`, `test/previewDeploy.test.js`, ggf. `test/support/appwriteMock.js` (neue Collection im Mock-Schema).
|
||||
|
||||
**Produces:**
|
||||
- `config.analyse.avvVersion = '2026-10-02'`; `config.collections.analyseConsents = process.env.APPWRITE_COLLECTION_ANALYSE_CONSENTS || 'analyseConsents'`.
|
||||
- `analyse-setup.mjs`: portalFeatures-Attribute zusätzlich `avvConsentDate` (10), `avvConsentChannel` (10), `removalTicketId` (40), `publishTicketId` (40) (alle `string`, optional; gesamt 12). Neue Collection `analyseConsents` (Datenbank wie portalFeatures, nur Server-Key, keine Client-Rechte) mit Attributen `customerId` 36, `projectId` 36, `siteKey` 40, `event` 8 (`on`/`off`), `avvVersion` 20, `via` 8 (`portal`/`woms`), `acceptedAt` 40, `acceptedBy` 100, `textformDate` 10, `textformChannel` 10 und Index auf `projectId`. Das Skript bleibt idempotent; der Client erhält `ensureCollection(id, name)` und nimmt die Collection-ID als Parameter.
|
||||
- `analysePrivacy.js`: `analysePrivacyData(customer) ⇒ { ok, company, address, email, missing }` mit derselben Bereinigung wie heute in `buildAnalytics` (Steuerzeichen raus, Firma = `companyName || name` ≤ 200, Adresse `street, PLZ Ort` ≤ 300, E-Mail ≤ 200 und `^[^\s@]+@[^\s@]+\.[^\s@]+$`); `missing ⊆ ['firma','email']`; `ok` ⇔ `missing` leer. `buildAnalytics` ruft sie auf (kein zweiter Regelsatz).
|
||||
- `analyseConsent.js`: `recordConsent(entry) ⇒ Dokument` (Appwrite `createDocument` mit `ID.unique()`), `listConsents(projectId)`. Es gibt bewusst keine Änderungs- und Löschfunktion.
|
||||
- `analyse.js`:
|
||||
- `validateTextform({ date, channel }, { today, minDate })` mit `date` `YYYY-MM-DD` (echtes Datum, nicht nach `today` in Berlin, nicht vor `minDate = config.analyse.avvVersion`), `channel ∈ {'chat','email'}`; Fehler 400.
|
||||
- `activateAnalyse({ customerId, projectId, accentColor, consent, by = 'customer', actor, textform })`: Kunde (`by==='customer'`) braucht `consent.accepted === true`, `consent.avvVersion === config.analyse.avvVersion` und `actor` mit `name || email`; Mitarbeiter braucht gültiges `textform` und trägt seinen Namen in `by`. Vor `putSite`: `analysePrivacyData(await customerOf(customerId))` muss `ok` sein, sonst 409 mit NEU-Text „Für die Zusatzseite fehlen in Deinen Kundendaten Firma oder Name und eine gültige E-Mail-Adresse. Schreib uns im Chat, wir ergänzen sie.“ (WOMS-Variante: Fehlt-Hinweis mit Feldnamen). Beim Wechsel `off→active`: `putSite` → `recordConsent({ event:'on', via, avvVersion, acceptedAt:now, acceptedBy, textformDate, textformChannel, customerId, projectId, siteKey })` (schlägt es fehl: `putSite(..., active:false)` zurücknehmen und Fehler werfen) → `updateDocument` mit `avvVersion`, `avvAcceptedAt`, `avvAcceptedBy` (= Name/E-Mail der Person bzw. Mitarbeitername), `avvConsentDate`/`avvConsentChannel` (leer beim Kunden-Weg), `activatedAt`, `offSince:''`, `activatedMailAt:''`, `privacyTicketId:''`, `removalTicketId:''`, `publishTicketId:''`.
|
||||
- `deactivateAnalyse` schreibt zusätzlich `recordConsent({ event:'off', via, acceptedBy })` (Fehler dabei nur loggen, Ausschalten darf nicht scheitern).
|
||||
- Routen: `POST /api/funktionen/analyse/activate` übergibt `actor = { name: req.session.name, email: req.session.email }`. `POST /api/admin/analyse/:projectId` Body `{ on, textformDate, textformChannel, audienceConfirmed }`: `on:true` verlangt `audienceConfirmed === true`, gültiges Datum und Weg (sonst 400, NEU-Texte kurz); `avvConfirmedVerbally` wird nicht mehr akzeptiert (400 „Zustimmung in Textform nötig“). `GET /api/admin/analyse/:projectId` liefert zusätzlich `avvConsentDate`, `avvConsentChannel`, `privacy: { ok, missing }`. `GET /api/funktionen` (`analyseView`) liefert zusätzlich `controller: { company, address, email, ok, missing }`.
|
||||
|
||||
**Tests:**
|
||||
- [ ] Setup: `ANALYSE_ATTRIBUTES` hat 12 Einträge mit den Größen aus der Liste; `main` mit Mock legt nur fehlende Attribute an; Collection `analyseConsents` wird angelegt, wenn sie fehlt, und übersprungen, wenn sie existiert; `--check` legt nichts an und meldet `missing`.
|
||||
- [ ] `analysePrivacyData`: Firma leer + Name leer ⇒ `missing ['firma']`; nur `name` ⇒ `company === name`; E-Mail `'abc'` ⇒ `missing ['email']`; Steuerzeichen/Zeilenumbruch in der Firma werden zu Leerzeichen; Adresse nur mit `street` ohne Ort ⇒ nur Straße.
|
||||
- [ ] `buildAnalytics`: Kunde mit nur `name` ⇒ `privacy.company === name`; ungültige E-Mail ⇒ `null`.
|
||||
- [ ] Aktivieren ohne Firma/Name oder mit `email:'x'` ⇒ 409, **kein** `putSite`, **kein** Ticket, **keine** K18, Feld `state` bleibt `off`.
|
||||
- [ ] Kunde aktiviert: ein Historien-Dokument `event:'on', via:'portal', avvVersion:'2026-10-02', acceptedBy:'<Name>'`; `acceptedAt` ISO; Portal-Dokument trägt dieselben Werte; zweites Aktivieren (schon aktiv) ⇒ kein weiteres Historien-Dokument; Farbwechsel allein schreibt keines.
|
||||
- [ ] Aus → An: neues Historien-Dokument, `privacyTicketId`/`removalTicketId`/`publishTicketId` zurückgesetzt; `listConsents` zeigt `on`,`off`,`on` in zeitlicher Folge; das Modul exportiert nur `recordConsent`, `listConsents`.
|
||||
- [ ] `recordConsent` wirft ⇒ `putSite` wird mit `active:false` zurückgenommen, Dokument bleibt `off`.
|
||||
- [ ] `validateTextform`: `'2026-10-01'` (vor Fassung) ⇒ 400; Datum in der Zukunft ⇒ 400; `'2026-02-30'` ⇒ 400; Weg `'telefon'` ⇒ 400; `{date:'2026-10-02', channel:'email'}` ok.
|
||||
- [ ] Admin-Route: `on:true` ohne `textformDate` ⇒ 400; ohne `textformChannel` ⇒ 400; ohne `audienceConfirmed` ⇒ 400; mit altem `avvConfirmedVerbally:true` ⇒ 400; gültig ⇒ 200 und Historie `via:'woms'`, `acceptedBy` = Mitarbeitername, `textformDate`/`textformChannel` gesetzt, Portal-Dokument `avvConsentDate`/`avvConsentChannel`.
|
||||
- [ ] `GET /api/funktionen`: `controller.ok === false` und `missing` gefüllt, wenn Angaben fehlen; `GET /api/admin/analyse/:id` enthält `privacy`.
|
||||
- [ ] FAIL → implementieren → `npm test` PASS → Commit „Analyse: Einwilligungs-Historie, Nachweis beim Einschalten, Sperre ohne Verantwortlichen-Angaben, Textform für Mitarbeiter“
|
||||
|
||||
### Task 6: Rechtstexte veröffentlichen, Zusatzseiten-Vorlage, Archiv (T10 Dateien, T11)
|
||||
|
||||
**Files:** `public/rechtliches/avv-website-analyse-2026-10-02.html`, `public/rechtliches/leistungsbeschreibung-website-analyse-2026-10-02.html`, `public/rechtliches/avv-website-analyse-2026-10-02.pdf`, `public/rechtliches/leistungsbeschreibung-website-analyse-2026-10-02.pdf`, `public/rechtliches/avv-website-analyse.html` (entfernen), `server/services/analyseLegal.js` (neu), `server/index.js` (Route), `scripts/build-legal-pdfs.sh`, `runner/src/templates/datenschutz-analyse.html`, `docs/rechtliches/avv-website-analyse.md`, `docs/rechtliches/leistungsbeschreibung-website-analyse.md`, `docs/rechtliches/archiv/` (neu), `test/avvPage.test.js`, `test/legalTexts.test.js` (neu `test/analyseLegal.test.js`), `runner/test/analytics-inject.test.js`.
|
||||
|
||||
**Schritte und Produces:**
|
||||
- Kopieren (Bytes unverändert) aus `RT`: `avv-website-analyse.html` → `public/rechtliches/avv-website-analyse-2026-10-02.html`, `leistungsbeschreibung-website-analyse.html` → `public/rechtliches/leistungsbeschreibung-website-analyse-2026-10-02.html`, `avv-website-analyse.md`/`leistungsbeschreibung-website-analyse.md` → `docs/rechtliches/` (ersetzen die Entwürfe), `datenschutz-analyse.html` → `runner/src/templates/datenschutz-analyse.html`. Die bisherige Entwurfsseite `public/rechtliches/avv-website-analyse.html` wird gelöscht (T11).
|
||||
- `analyseLegal.js`: `legalPaths(version) ⇒ { avv:'/rechtliches/avv-website-analyse-<version>.html', lb:'/rechtliches/leistungsbeschreibung-website-analyse-<version>.html' }`, `legalPdfFiles(version) ⇒ { avv:{filename:'AVV-Website-Analyse-<version>.pdf', path}, lb:{filename:'Leistungsbeschreibung-Website-Analyse-<version>.pdf', path} }`, `legalPdfAttachments(version) ⇒ Promise<[{filename,content,contentType:'application/pdf'},…]>` (wirft bei unlesbarer Datei).
|
||||
- Route in `server/index.js` vor `express.static`: `GET /rechtliches/avv-website-analyse.html` und `GET /rechtliches/leistungsbeschreibung-website-analyse.html` liefern `res.sendFile` der Datei zur aktuellen `config.analyse.avvVersion` (Adressen ohne Fassung zeigen immer die aktuelle geprüfte Fassung; eine neue Fassung = neue Dateien + Konfiguration).
|
||||
- `build-legal-pdfs.sh`: Aufruf `scripts/build-legal-pdfs.sh analyse` erzeugt nur die beiden PDFs aus den versionierten HTML-Dateien (Standard `all` wie bisher); PDFs werden eingecheckt. Ausführen: `CHROME=$(ls -d ~/.cache/ms-playwright/chromium-*/chrome-linux64/chrome | tail -1) scripts/build-legal-pdfs.sh analyse`.
|
||||
- Archiv (T4): `docs/rechtliches/archiv/README.md` mit Tabelle „Fassung | gültig ab | Banner-Text | Zusatzseite“; `docs/rechtliches/archiv/banner-2026-10-02.md` enthält Fließtext (§ 1), `aria-label`, Schaltflächennamen, `BANNER_VERSION`; `docs/rechtliches/archiv/datenschutz-analyse-2026-10-02.html` ist eine Kopie der Vorlage. „Gültig ab“ bleibt `offen bis Livegang` und wird in L2/L5 eingetragen. Lokales Git-Tag `analyse-texte-2026-10-02` auf den Commit dieser Task (nicht pushen; L2 pusht der Nutzer).
|
||||
|
||||
**Tests:**
|
||||
- [ ] `test/avvPage.test.js` neu: beide versionierten HTML-Dateien existieren, enthalten `Fassung 2026-10-02`, enthalten weder `<!--` noch `Entwurf` noch `Prüfpunkt`; `public/rechtliches/avv-website-analyse.html` existiert **nicht** mehr als Datei; AVV-Seite enthält die Überschrift `12. Haftung` (wie bisher).
|
||||
- [ ] `GET /rechtliches/avv-website-analyse.html` und `…/leistungsbeschreibung-website-analyse.html` über die Express-App liefern exakt die Bytes der Dateien für `config.analyse.avvVersion`; `…-2026-10-02.html` ebenfalls 200; `…-2026-01-01.html` 404.
|
||||
- [ ] PDFs: beginnen mit `%PDF`, Größe > 20 KB, `legalPdfAttachments('2026-10-02')` liefert genau 2 Einträge mit den Dateinamen oben.
|
||||
- [ ] Banner-Archiv: `archiv/banner-2026-10-02.md` enthält `BANNER_TEXT` und `BANNER_ARIA` aus `analytics/client/core.js` (Import der Konstanten) wortgleich; `README.md` nennt `BANNER_VERSION`.
|
||||
- [ ] Runner-Test `Datenschutzseite…`: Erwartete Abschnittswörter auf „Datenschutz-Einstellungen“ statt „Cookie-Einstellungen“ ändern; zusätzlich: Seite enthält `Stand: 02.10.2026`, keine `{{`-Reste, `{{#address}}` ohne Adresse entfernt, `<!--` nirgends.
|
||||
- [ ] `public/funktionen.js` `AVV_URL`-Test aus `avvPage.test.js` wird in Task 7 angepasst; hier nur entfernen/ersetzen durch Existenzprüfung der versionierten Dateien.
|
||||
- [ ] `npm test` und `cd runner && npm test` PASS → Commit „Analyse: geprüfte Rechtstexte Fassung 2026-10-02 veröffentlichen (HTML, PDF, Zusatzseiten-Vorlage, Archiv)“
|
||||
|
||||
### Task 7: Portal-Oberfläche (T8 Frontend, T10 Portal-Vorschau)
|
||||
|
||||
**Files:** `public/funktionen.js`, `public/analyse-view.js`, `server/routes/funktionen.js` (`analyseView`: `legal`), `test/avvPage.test.js`, `test/analyseView.test.js`, `test/funktionenTiles.test.js`, optional `test/ui/funktionen.ui.mjs` (Playwright, manuell: `node test/ui/funktionen.ui.mjs`).
|
||||
|
||||
**Produces:**
|
||||
- `analyseView` liefert zusätzlich `legal: { avv, lb }` (Pfade aus `legalPaths(config.analyse.avvVersion)`), `controller` kommt aus Task 5.
|
||||
- `public/analyse-view.js`: `BANNER_PREVIEW_TEXT` (= `BANNER_TEXT` aus `core.js`), `controllerLines(controller) ⇒ string[]` (Firma, Anschrift falls vorhanden, E-Mail) für die Vorschau, `AVV_CHECK_*`-Konstanten nicht nötig.
|
||||
- `public/funktionen.js`: `AVV_URL` und Leistungsbeschreibungs-Link kommen aus `a.legal` (Konstante `AVV_URL` entfällt). Texte, jeweils aus `banner-portal-mailtexte.md`: Panel-Aufzählung (§ 2, ersetzt die drei bisherigen Listenpunkte, Zeile ~608), Vorschau der Verantwortlichen-Angaben vor dem Haken (§ 2; Schreib-uns-Hinweis nur wenn `controller.ok`), Haken mit zwei Links und Kinder-Satz (§ 2, ersetzt Zeile ~619), `bannerPreview` zeigt `BANNER_PREVIEW_TEXT` plus Link und Knopf-Beschriftungen („Ablehnen“, „Annehmen“) (§ 1), Hinweis vor dem Ausschalten (§ 2, ersetzt Zeile ~763), Bestätigung nach „Ja, ausschalten“ (§ 2; Bestätigungstext statt sofortigem `reload()`, mit Knopf „OK“, der danach `reload()` ruft). Ist `controller.ok === false`, bleibt „Analyse einschalten“ gesperrt und ein Hinweis (NEU-Text wie in Task 5) erklärt, was fehlt. `consent: { accepted:true, avvVersion }` bleibt.
|
||||
|
||||
**Tests:**
|
||||
- [ ] Parität: `BANNER_PREVIEW_TEXT === BANNER_TEXT` (beide aus den Modulen importiert); Preview-HTML enthält `Ablehnen` und `Annehmen`.
|
||||
- [ ] Quelltext-Prüfung wie in `avvPage.test.js`: `funktionen.js` enthält keine der alten Formulierungen („anonym“, „Cookie-Einstellungen“, „Die bisherigen Daten bleiben noch 60 Tage erhalten“, „Verarbeitung der Analysedaten gemäß Auftragsverarbeitungsvertrag“) und verweist nicht mehr auf `AVV_URL = '/rechtliches/avv-website-analyse.html'`.
|
||||
- [ ] Render-Test (`funktionenTiles`-Muster, `render()` mit Fixture `analyse:{state:'off', eligible:true, avvVersion:'2026-10-02', legal:{…}, controller:{ok:true, company:'Muster GmbH', address:'Weg 1, 12345 Ort', email:'info@muster.de'}}`): HTML enthält die zwei Links `href` aus `legal`, den Text „Fassung 2026-10-02“, „Muster GmbH“ und „info@muster.de“; mit `controller.ok:false` ist der Knopf `disabled` und der Fehlt-Hinweis sichtbar.
|
||||
- [ ] Ausschalt-Ablauf (Fake-`fetch`): nach „Ja, ausschalten“ erscheint der Bestätigungstext aus § 2, erst „OK“ lädt neu.
|
||||
- [ ] `controllerLines`: ohne Adresse nur 2 Zeilen; HTML-Maskierung im Vorschau-Text (`esc`) für `<`/`&` in der Firma.
|
||||
- [ ] `npm test` PASS; Playwright-Skript einmal laufen lassen (Screenshots 390 px und Desktop nach `/root/reports/website-analyse-2026-10-01/`) → Commit „Analyse: Portal-Texte, Verantwortlichen-Vorschau und Bestätigungen nach geprüftem Wortlaut“
|
||||
|
||||
### Task 8: Mails K18/K19, Ticket-Texte, Neubau-Ergebnis (T10 Mails, T13b/c, T17)
|
||||
|
||||
**Files:** `server/services/analyse.js`, `server/services/analyseTexts.js` (neu), `server/mail-templates/K18 Analyse aktiv.html`, `server/mail-templates/K19 Analyse beendet.html`, `test/analyse.test.js`, `test/mailTemplates.test.js`.
|
||||
|
||||
**Produces:**
|
||||
- `analyseTexts.js`: Konstanten `PRIVACY_SNIPPET_DU` und `PRIVACY_SNIPPET_SIE` (aus `banner-portal-mailtexte.md` § 7, Fließtext ohne Markdown-Sternchen, Überschrift „Reichweitenmessung“ eigene Zeile), Funktionen `privacyTicketDetails({ name, domains, rebuildNote })` (Kopfzeilen `Funktion`, `Projekt`, `Domain(s)`, `Zusatzseite`, danach die vier Punkte aus § 6 „Beim Einschalten“, darunter beide Bausteine) und `removalTicketDetails({ name, domains })` (Kopfzeilen + die drei Punkte aus § 6 „Beim Ausschalten“); `publishTicketDetails({ name, domains, reason })` (NEU, kurz: Website neu veröffentlichen, damit Skript und Zusatzseite erscheinen; `reason` ∈ `kein_repo|preview_aus|fehler`).
|
||||
- `rebuildSafe(project)` gibt `{ ok, reason }` zurück: `kein_repo` (kein `repoFullName`), `preview_aus` (`previewMode === 'off'`), `fehler` (Ausnahme oder `result.error`), sonst `ok:true`.
|
||||
- `privacyTicket` (T17) nutzt `privacyTicketDetails`, Thema bleibt „Datenschutzerklärung von <Projekt> um Hinweis auf Analyse ergänzen“. Beim Einschalten: erst `rebuildSafe`, dann Ticket; ist der Neubau nicht möglich oder gescheitert, zusätzlich Ticket `Website-Analyse von <Projekt> veröffentlichen` (Marker `publishTicketId`, genau eines je Aktivierung).
|
||||
- `deactivateAnalyse`: nach `rebuildSafe` Ticket „Website-Analyse von <Projekt> entfernen“ (Details aus § 6 „Beim Ausschalten“; Marker `removalTicketId`, genau eines je Beendigung; Ticket-Fehler loggen, nie das Ausschalten verhindern). Gilt für Kunde, Mitarbeiter und `runAboCheck` (geht über `deactivateAnalyse`).
|
||||
- `mailSafe(customerId, template, project, { values, blocks, attachments })`: K18 bekommt `legalPdfAttachments(config.analyse.avvVersion)`; sind sie unlesbar, wird nicht gesendet (`false`, Marker `activatedMailAt` bleibt leer ⇒ Wiederholung beim nächsten Abgleich, Task 9). Zusätzliche Werte K18: `fassung`, `zeitpunkt` (Berlin, `TT.MM.JJJJ, HH:MM Uhr` über `berlinDateTime`), `person`, `datum` (`TT.MM.JJJJ`), `weg` (`Chat im Kundenportal` bzw. `E-Mail`), `avv_url`, `lb_url` (absolut mit `config.publicBaseUrl`); Blöcke `portal_weg` (Kunde) und `textform_weg` (Mitarbeiter).
|
||||
- K18: Absatz nach „Hallo {{kunde_name}},“ durch § 4 ersetzen, Tabelle um die Zeilen „Vereinbart“ und „Zugestimmt“ (§ 4) erweitern, Kopfkommentar `Platzhalter` und `Optionale Blöcke` nachziehen (`mailTemplates.test.js` prüft Kopfkommentar gegen Vorlage). K19: Absatz durch § 5 ersetzen.
|
||||
|
||||
**Tests:**
|
||||
- [ ] Einschalten (Kunde): K18 verschickt, `attachments.length === 2`, Dateinamen `AVV-Website-Analyse-2026-10-02.pdf` und `Leistungsbeschreibung-Website-Analyse-2026-10-02.pdf`, Inhalte beginnen mit `%PDF`; HTML enthält `2026-10-02`, die beiden absoluten Links und die Zeile mit `durch <Name>`; Text enthält den Einleitungssatz aus § 4 („Das Einwilligungsbanner erscheint mit der nächsten Veröffentlichung“).
|
||||
- [ ] Einschalten (Mitarbeiter, `textform {date:'2026-10-03', channel:'email'}`): Mail enthält `in Textform am 03.10.2026 per E-Mail`.
|
||||
- [ ] PDFs unlesbar (Pfad per Testschalter ungültig) ⇒ keine Mail, `activatedMailAt` leer, Aktivierung selbst bleibt gültig.
|
||||
- [ ] Ticket beim Einschalten: Details enthalten „Textbaustein“, „offensichtlicher Widerspruch“, „externer Generator“, „Stundenkontingent“ und den Anfang beider Bausteine („Wir messen mit einer eigenen Reichweitenmessung ohne Cookies“, „Solange Sie im Banner nichts ausgewählt haben“); enthält **nicht** mehr „Satz + Link in die bestehende Datenschutzerklärung einfügen“.
|
||||
- [ ] Ticket beim Ausschalten (Kunde, Mitarbeiter, Abo-Ende über `runAboCheck`): genau ein Ticket mit Thema `Website-Analyse von Bäckerei entfernen`; zweimal ausschalten ⇒ ein Ticket; Aus → An → Aus ⇒ zwei Tickets; Details enthalten „fünf Werktagen“ und „Stundenkontingent“.
|
||||
- [ ] `rebuildSafe`: Projekt ohne `repoFullName` ⇒ `reason 'kein_repo'`, Rebuild-Stub nicht aufgerufen, beim Einschalten zusätzlich Ticket `… veröffentlichen`; `previewMode:'off'` ⇒ `preview_aus`; Rebuild wirft ⇒ `fehler`; erfolgreicher Rebuild ⇒ kein Veröffentlichen-Ticket.
|
||||
- [ ] K19-Text enthält „keine Daten mehr gespeichert“ und „46. Tag“ (§ 5), nicht mehr „Der Banner und die Messung sind von Deiner Website entfernt“.
|
||||
- [ ] `mailTemplates.test.js` grün (Kopfkommentar, Platzhalter, Blöcke, Textfassung ohne `{{`).
|
||||
- [ ] `npm test` PASS → Commit „Analyse: K18 mit PDFs und Nachweis, K19 neu, Tickets für Einschalten, Ausschalten und Veröffentlichen“
|
||||
|
||||
### Task 9: Alle Beendigungswege setzen `active=false` + `offSince` (T6-Portal)
|
||||
|
||||
**Files:** `analytics/src/server.js`, `analytics/test/sites.test.js`, `server/services/analyticsClient.js`, `server/services/analyse.js`, `server/services/siteFeatures.js`, `server/services/customerAdmin.js`, `server/services/giteaAdmin.js` (`updateWebsiteProject`), `server/routes/abo.js` (`withdrawOrder`), `server/index.js`, `test/analyticsClient.test.js`, `test/analyse.test.js`, `test/retention.test.js`, `test/aboRoutes.test.js`, `test/giteaAdmin.test.js`, `test/analyticsContract.test.js`.
|
||||
|
||||
**Produces:**
|
||||
- Messdienst: Admin-Route `POST /a/admin/sites/:siteKey/off` (Bearer wie alle Admin-Routen, 401 sonst; setzt `active=0` und `offSince` einmalig auf jetzt, ändert einen vorhandenen `offSince` nicht; unbekannter Schlüssel 404; ruft `onSitesChanged`). Portal-Client `analyticsClient.stopSite(siteKey)` (404 zählt als erledigt) und im Vertragstest `test/analyticsContract.test.js` ergänzt.
|
||||
- `analyse.js`: `endAnalyse({ doc, project, reason, now, by, tickets = true, rebuild = true })` als gemeinsamer Kern (Messdienst `stopSite`, Dokument `state:'off'` + `offSince`, optional Neubau und Entfernen-Ticket, wenn Projekt und Kunde noch existieren); `deactivateAnalyse` ruft ihn. `endAnalyseForCustomer(customerId, { reason, mail })` beendet alle aktiven Analysen des Kunden (bei `mail:true` K19). `analyseOnProjectChanged(projectId)`: aktives Dokument; fällt die Berechtigung weg (`nicht_oeffentlich`, `keine_subdomain`) ⇒ `endAnalyse(reason:'projekt_nicht_mehr_oeffentlich')`, sonst `putSite` mit neuen `analyseDomains(project)`. `runAnalyseReconcile({ now })` (täglich nach `runAboCheck`, `server/index.js`): für Analyse-Dokumente `active` mit fehlendem Projekt oder fehlendem Kunden ⇒ `endAnalyse(reason:'projekt_geloescht'|'kunde_geloescht', tickets:false, rebuild:false)`; für `active` mit vorhandenem Projekt `putSite` mit aktuellen Domains (fängt geänderte `liveDomain`); für `off` und `disabled` `stopSite` (idempotent); für `active` ohne `activatedMailAt` K18 erneut versuchen.
|
||||
- Hooks: `customerAdmin.deleteCustomerWithPortalAccess` ruft vor dem Löschen `endAnalyseForCustomer(customerId, { reason:'kunde_geloescht', mail:false })` (Fehler loggen); `giteaAdmin.updateWebsiteProject` ruft nach dem Speichern `analyseOnProjectChanged` (dynamischer Import wegen Zyklus, Fehler loggen); `siteFeatures.stopAnalyseCollecting` nutzt `stopSite` statt `putSite` mit Projekt-Domains (damit Löschfrist auch ohne Projekt wirkt); `withdrawOrder` ruft bei `type === 'wechsel'` und `inTime` `endAnalyseForCustomer(sub.customerId, { reason:'widerruf_wechsel', mail:true })` in `try/catch` (der Widerruf darf nie daran scheitern).
|
||||
|
||||
**Tests:**
|
||||
- [ ] Messdienst: `POST …/off` ohne Token 401; unbekannt 404; bekannt ⇒ `active=0`, `offSince` gesetzt, zweiter Aufruf lässt den ersten Wert; direkt danach verwirft `/a/e` Ereignisse (Cache invalidiert) und `/a/w.js?s=` liefert das leere Skript.
|
||||
- [ ] Kunde gelöscht (`deleteCustomerWithPortalAccess`): `stopSite` für jeden aktiven Analyse-Schlüssel des Kunden, Dokument `state:'off'`, **kein** Ticket, **keine** Mail, Löschen des Kunden gelingt auch wenn `stopSite` wirft.
|
||||
- [ ] Projekt gelöscht (Dokument aus `websiteProjects` entfernt): `runAnalyseReconcile` ⇒ `stopSite`, `state:'off'`, `offSince` gesetzt, kein Ticket; Kunde fehlt ⇒ dasselbe; vorhandenes Projekt mit geänderter `liveDomain` ⇒ `putSite` mit neuer Domainliste; `off`-Dokument ⇒ `stopSite` aufgerufen.
|
||||
- [ ] Domain/Öffentlichkeit: `updateWebsiteProject` mit `isPublic:false` ⇒ Analyse beendet (`stopSite`, `off`, Entfernen-Ticket); mit neuer `subdomain` ⇒ `putSite` mit `<neu>.project.webklar.com`; Fehler im Hook ändert das Ergebnis des Projekt-Updates nicht.
|
||||
- [ ] Löschfrist: `applyDeletions` mit Analyse-Dokument, dessen Projekt nicht mehr existiert ⇒ Dokument `disabled`, Messdienst-Stub bekam `stopSite(siteKey)` (nicht `putSite`).
|
||||
- [ ] Widerruf Wechsel (`POST /public/withdraw`, `order:'wechsel'`, in der Frist, Kunde hat aktive Analyse): Analyse `off`, `stopSite` aufgerufen, K19 gesendet; Widerruf außerhalb der Frist oder `order:'zusatzspeicher'` ändert nichts; wirft `endAnalyseForCustomer`, bleibt der Widerruf `200`.
|
||||
- [ ] K18-Wiederholung: aktives Dokument ohne `activatedMailAt` und lesbaren PDFs ⇒ Abgleich sendet K18 genau einmal und setzt den Marker.
|
||||
- [ ] `npm test` und `cd analytics && npm test` PASS → Commit „Analyse: alle Beendigungswege schalten den Messdienst ab (Kunde/Projekt weg, Domain, Löschfrist, Widerruf Wechsel)“
|
||||
|
||||
---
|
||||
|
||||
## Teil C – WOMS
|
||||
|
||||
### Task 10: WOMS – Zustimmung in Textform (T9)
|
||||
|
||||
**Files (WOMS-Worktree):** `src/lib/analyseView.js`, `src/components/AnalyseRow.jsx`, `src/lib/analyseApi.js`, `tests/unit/analyseView.test.mjs`.
|
||||
|
||||
**Produces:**
|
||||
- `analyseApi.setAnalyse(projectId, { on, textformDate, textformChannel, audienceConfirmed })` sendet genau diese Felder (kein `avvConfirmedVerbally` mehr).
|
||||
- `analyseView.js`: `avvLabel` entfällt; neu `textformLabel(version)` (Kästchentext aus `banner-portal-mailtexte.md` § 3, Links werden in der Komponente gesetzt), `canSubmitTextform({ confirmed, date, channel, today, minDate }) ⇒ boolean` (alle drei Pflichtangaben, `date ≥ minDate` = Fassungsdatum, `date ≤ today`, `channel ∈ {'chat','email'}`), `channelLabel(channel)` („Chat im Kundenportal“/„E-Mail“), `analyseStatus` zeigt bei `active` zusätzlich „Zustimmung in Textform am TT.MM.JJJJ per <Weg>“, wenn `avvConsentDate` vorhanden ist.
|
||||
- `AnalyseRow.jsx`: Einschalte-Zeile zeigt Kästchen (§ 3) mit Links auf AVV und Leistungsbeschreibung (aus `data.legal`, absolut `https://project.webklar.com…`), Pflichtfeld „Datum der Zustimmung“ (`<input type="date" required max=heute min=Fassungsdatum>`), Pflichtauswahl „Weg“ (Radio „Chat im Kundenportal“ / „E-Mail“); Knopf „Einschalten“ bleibt gesperrt, bis `canSubmitTextform`. Aufklappbar „Textvorlage für den Chat“ (§ 3, `{Link AVV}`/`{Link Leistungsbeschreibung}` mit den echten Links gefüllt) mit Kopieren-Knopf. Ist `data.privacy.ok === false`, Einschalten gesperrt und Hinweis, welches Feld fehlt (`firma`/`email`).
|
||||
|
||||
**Tests:**
|
||||
- [ ] `canSubmitTextform`: ohne Häkchen, ohne Datum, ohne Weg, Datum vor `2026-10-02`, Datum morgen ⇒ je `false`; alle gültig ⇒ `true`.
|
||||
- [ ] `channelLabel('chat') === 'Chat im Kundenportal'`, `channelLabel('email') === 'E-Mail'`.
|
||||
- [ ] `analyseStatus({enabled:true,state:'active',eligible:true,activatedAt:'2026-10-03T08:00:00Z',avvConsentDate:'2026-10-02',avvConsentChannel:'email'}).text` enthält `An seit 03.10.2026` und `Zustimmung in Textform am 02.10.2026 per E-Mail`.
|
||||
- [ ] `setAnalyse` (Fetch-Stub): Body-JSON hat `on:true,textformDate,textformChannel,audienceConfirmed:true` und kein `avvConfirmedVerbally`.
|
||||
- [ ] `npm test` und `npm run build` PASS → Commit „Analyse: Einschalten im WOMS nur mit Datum und Weg der Zustimmung in Textform“
|
||||
|
||||
---
|
||||
|
||||
## Livegang (jeder Schritt braucht das OK des Nutzers; Reihenfolge einhalten)
|
||||
|
||||
- **L1 Vorbereitung auf dem Server (ohne Portal-Code):**
|
||||
1. Schema: im Portal-Umfeld (Appwrite-Key) `node scripts/analyse-setup.mjs --check`, dann `node scripts/analyse-setup.mjs` ausführen (Attribute an `portalFeatures` + Collection `analyseConsents`). Muss **vor** L2 passieren, weil der neue Portal-Code diese Felder schreibt.
|
||||
2. `/home/webklar/apps/docker-compose.yml`, Dienst `analytics` (Zeilen ~1786-1814): unter `mem_limit: 384m` die Zeile `memswap_limit: 384m` ergänzen (T5). Kommentar zum Traefik-Zugriffsprotokoll ergänzen (T18): am Traefik-Block (Zeilen ~15-56) „Zugriffsprotokoll bleibt aus“ und an den Labels von `webklar-analytics` „falls je eingeschaltet: `traefik.http.routers.webklar-analytics.observability.accessLogs=false` setzen“. `docker compose config -q` prüfen, `docker compose up -d analytics`, danach `docker inspect webklar-analytics --format '{{.HostConfig.MemorySwap}}'` = `402653184` (384 MiB).
|
||||
3. Rollback: Zeile `memswap_limit` entfernen, `up -d analytics`; Schema-Skript ist additiv.
|
||||
- **L2 Portal-Push durch den Nutzer** (meine Pushes auf `main` sind blockiert): Branch `feature/website-analyse` in `main` übernehmen und pushen, zusätzlich Tag `analyse-texte-2026-10-02` (lokal angelegt in Task 6) pushen; `deploy.sh` startet bei `analytics/`-Änderung den Messdienst neu. Prüfen: `curl -s https://project.webklar.com/a/health` ⇒ `{"ok":true}`; `curl -s 'https://project.webklar.com/a/w.js?s=unbekannt00000000000'` ⇒ `/* WEBklar Analyse: aus */`; `https://project.webklar.com/rechtliches/avv-website-analyse.html` und `…/leistungsbeschreibung-website-analyse.html` zeigen die geprüfte Fassung 2026-10-02 ohne „Entwurf“/„Prüfpunkt“; `…-2026-10-02.pdf` laden; `ANALYSE_ENABLED` weiter `off` (Analyse-Routen antworten 409). `docs/rechtliches/archiv/README.md`: „gültig ab“ mit dem Tag des Einschaltens (L5) nachtragen.
|
||||
- **L3 Browser-Probe auf einer öffentlichen Wegwerf-Website** (statische Testseite unter `*.project.webklar.com`, Skript-Tag von Hand mit `w.js?s=<Probe-Schlüssel>`; Website beim Messdienst per Admin-Token (`PUT /a/admin/sites/<key>`, interner Port) angelegt, Portal-Aktivierung nicht nötig). Frisches Chrome-Profil, Netzwerk-Tab offen; vorher im selben Profil im Kundenportal anmelden, damit Portal-Cookies existieren. Belege als Screenshots/Netzwerk-Export nach `/root/reports/website-analyse-2026-10-01/livegang/`:
|
||||
1. Ohne Wahl: genau ein `POST /a/e` mit Körper `{"t":"pv","site":"…","path":"/…"}` (keine Felder `ref`, `vp`) und ein `bn shown`; **kein** `Cookie`-Header an `project.webklar.com/a/e`; Banner-Text, `aria-label`, Platz unten (Footer-Links erreichbar) und Knopf „Datenschutz-Einstellungen“ ≥ 24 × 24 px prüfen.
|
||||
2. „Ablehnen“: genau ein `bn reject`; danach Seitenwechsel (Link, `pushState`-Seite falls vorhanden, Zurück-Taste, Scrollen, Klicken) und Neuladen ⇒ **keine** weiteren `/a/e`-Anfragen, nur Knopf „Datenschutz-Einstellungen“.
|
||||
3. GPC: Browser mit `Sec-GPC`/`navigator.globalPrivacyControl === true` (Firefox `privacy.globalprivacycontrol.enabled`) ⇒ **null** `/a/e`-Anfragen, kein Banner, kein Knopf.
|
||||
4. „Annehmen“ auf laufender Seite: `bn accept`, `cx` mit `ref` und `vp`, später `ck`/`ct`/`en`; neue Banner-Fassung testen, indem im Speicher `wk-consent` ohne `bv` gesetzt wird ⇒ Banner erscheint erneut.
|
||||
5. Datenbank: `docker exec webklar-analytics node -e "…SELECT COUNT(*) FROM events_raw WHERE ts % 3600000 != 0…"` ⇒ 0 (alle Zeitstempel voll Stunde, auch `clicks`); unbefragtes `pv` hat `vp IS NULL` und `ref IS NULL`; Pfad mit 7-stelliger Zahl erscheint als `:id`.
|
||||
6. Export: `docker exec webklar-analytics node scripts/export-site.mjs <key> --out /srv/webklar-analytics/exports` ⇒ drei CSVs mit Daten; danach den Ordner löschen.
|
||||
7. Ausschalten: `PUT /a/admin/sites/<key>` mit `active:false` ⇒ `curl -sI 'https://project.webklar.com/a/w.js?s=<key>'` zeigt den leeren Körper und `Cache-Control: public, max-age=60`; nach > 60 Sekunden Seite neu laden (Cache leeren) ⇒ kein Banner, keine `/a/e`-Anfragen; `offSince` gesetzt.
|
||||
8. Aufräumen: `DELETE /a/admin/sites/<key>`, Wegwerf-Website entfernen, Protokolle der Probe löschen.
|
||||
- **L4 WOMS-Push durch den Nutzer:** WOMS-Branch nach dem üblichen Weg (Test, dann main) pushen und ausrollen. WOMS muss zeitnah zu L2 live gehen, weil die neue Portal-Route das alte `avvConfirmedVerbally` ablehnt (Schalter ist bis L5 aus, es gibt keinen Schaden).
|
||||
- **L5 Einschalten (Nutzer):**
|
||||
1. Nutzer lädt AVV und Leistungsbeschreibung Fassung 2026-10-02 in WOMS „Rechtliches“ hoch (anwaltliche Freigabe vorausgesetzt).
|
||||
2. `ANALYSE_ENABLED=on` im Portal-Umfeld setzen, Portal neu starten.
|
||||
3. Echter Kundentest mit einer Kunden-Website mit Wartungs-Abo: Einschalten im Portal ⇒ K18 mit beiden PDFs, Historie-Eintrag, Ticket mit Textbaustein, Neubau, `w.js?s=` und `datenschutz-analyse.html` auf der Seite; Mitarbeiter-Weg an einem zweiten Testprojekt (Datum + Weg Pflicht, K18 mit „in Textform am …“); Ausschalten ⇒ Bestätigungstext, Ticket „Website-Analyse von … entfernen“, nach Neubau kein Banner.
|
||||
4. Rückfall: `ANALYSE_ENABLED=off` (Routen 409, bestehende Analysen laufen weiter und werden vom täglichen Abgleich weiter beendet, wenn das Abo endet).
|
||||
|
||||
---
|
||||
|
||||
## Außerhalb des Plans
|
||||
|
||||
- **T12:** Hetzner-Konto prüfen (Backups für `ubuntu-16gb-webklar-v1`, AV-Vertrag) – vom Server aus nicht prüfbar, Aufgabe des Nutzers.
|
||||
- **T20:** Portal-Zustimmung zu einer neuen Fassung für laufende Analysen (Hinweis plus Haken); bis dahin Zustimmung in Textform nach AVV Nr. 12 Abs. 2. Technisch erst vor der ersten Fassungsänderung nötig.
|
||||
- **T21:** `journald` begrenzen (`MaxRetentionSec=14day`, `MaxFileSec=1day`, `journalctl --vacuum-time=14d`); betrifft Abo-Bedingungen v5, nicht die Analyse.
|
||||
- **T22:** Datenbank-Export `/opt/webklar-backup/db-dump.sh` für SQLite-Anwendungen prüfen; betrifft Abo-Bedingungen v5.
|
||||
- Weiter nicht Teil des Plans: `abo-bedingungen-v6-vorschlag.md` (reiner Text), E6 (nur Text in der Leistungsbeschreibung: drei Monate Frist, Sonderkündigungsrecht, keine Technik), anwaltliche Abnahme der Texte, „gültig ab“-Eintrag im Archiv (L2/L5). T5 und T18 stehen im Livegang L1.
|
||||
|
||||
## Offene Annahmen (kurz prüfen, bevor die Task startet)
|
||||
|
||||
- WOMS-Tests laufen über `node --test` (nicht vitest) – Befehle im Plan entsprechend.
|
||||
- Das Ticket-Feld `details` der Collection `workorders` muss die beiden Textbausteine (ca. 2 KB) aufnehmen; sonst in Task 8 nur den Du-Baustein ins Ticket und den Sie-Baustein in einen zweiten Absatz-Hinweis „liegt in `server/services/analyseTexts.js`“ legen (vor Task 8 mit `grep details APPWRITE_SCHEMA.md` bzw. Appwrite prüfen).
|
||||
- `runDeploy` meldet nur synchron erkennbare Fehler (`result.error`); spätere Runner-Fehler bleiben über `previewState: 'failed'` sichtbar und lösen kein zusätzliches „veröffentlichen“-Ticket aus.
|
||||
- Heatmap-Links für Seiten, deren Pfad durch T14 zu `:id` wurde, führen auf eine nicht existierende Seite; das ist Absicht (Pfade mit Kennungen sind nicht auswertbar) und braucht keine Sonderbehandlung.
|
||||
- Der echte `w.js` bleibt 3600 s im Browser-Cache; wer vor dem Ausschalten geladen hat, sieht das Banner bis zu eine Stunde lang weiter, der Messdienst speichert dann aber nichts mehr (`active=0`). Das erfüllt LB Nr. 7 Abs. 2 („spätestens innerhalb von fünf Werktagen“). Soll es kürzer sein, `max-age` für das echte Skript in Task 4 auf 300 s setzen.
|
||||
|
||||
### Critical Files for Implementation
|
||||
- /home/webklar/apps/kundenbereich.webklar.com/dev-analyse/analytics/client/w.js
|
||||
- /home/webklar/apps/kundenbereich.webklar.com/dev-analyse/analytics/src/ingest.js
|
||||
- /home/webklar/apps/kundenbereich.webklar.com/dev-analyse/server/services/analyse.js
|
||||
- /home/webklar/apps/kundenbereich.webklar.com/dev-analyse/scripts/analyse-setup.mjs
|
||||
- /home/webklar/apps/ticket.webklar.com/dev-analyse/src/components/AnalyseRow.jsx
|
||||
Reference in new issue
Block a user