Spec: Analyse-Übersicht neu (Cockpit)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-04 21:34:43 +00:00
1 parent 75794f14d5
commit 2f9dfd8ad5
1 file changed
+239
@@ -0,0 +1,239 @@
# Analyse-Übersicht neu („Cockpit“) – Design
Stand: 2026-10-04 · Status: Aufbau, Design-Richtung und Abschnitte 1–3 vom Nutzer im Chat freigegeben, Spec zur Durchsicht
Baut auf der Website-Analyse auf (Spec `2026-10-01-website-analyse-design.md`, live seit 01.10.2026).
Recherche: `/root/reports/analyse-ueberarbeitung-2026-10-04/recherche.md` (30 Tools, 25 Features, UI-Muster).
Entwürfe: `/root/reports/analyse-ueberarbeitung-2026-10-04/entwurf-a-cockpit.png` (gewählt), `entwurf-b-bericht.png`.
Regel des Nutzers: bauen → live (Nutzer pusht `main`) → Nutzer testet live.
## Ziel
Die Analyse-Seite im Kundenportal (Funktionen → Analyse) soll für kleine Betriebe ohne Analytics-Kenntnisse
**auf einen Blick** sagen, wie die Website läuft: ein Satz in Klartext, Vergleich zum Vorzeitraum,
Kontakt-Aktionen als Hauptzahl. Das Interface darf sich vom restlichen Portal abheben, muss aber
aufgeräumt und einheitlich sein.
## Entscheidungen (vom Nutzer, 04.10.2026)
| # | Entscheidung |
|---|---|
| U1 | Umfang: nur das Paket **„Übersicht neu“** (Kennzahlen mit Vorzeitraum-Vergleich und Mini-Verlauf, Klartext-Zusammenfassung, Kontakt-Aktionen als Hauptzahl, Datenbasis-Hinweis). **Nicht** in diesem Projekt: Besucher-Details (Wochentag × Uhrzeit, Kanäle, Zeit je Seite), Website-Check (Erreichbarkeit, SSL, Links, SEO), Bericht per Mail. |
| U2 | Design-Richtung **A „Cockpit“**: dunkel wie das Portal, eigene Akzentfarbe Türkis nur innerhalb der Analyse, leuchtender Block „Das Wichtigste“. |
| U3 | Aufbau mit drei Reitern **Übersicht · Heatmap · Einstellungen**; Heatmap und „Ausschalten“ hängen nicht mehr unten an der Übersicht. |
| U4 | Nur **vorhandene Daten**. Keine Änderung an Messskript (`w.js`, `h.js`), Banner, AVV, Leistungsbeschreibung, Datenschutz-Zusatzseite oder Abo-Bedingungen. |
| U5 | Klartext entsteht durch **feste Regeln im Browser**, keine KI, kein externer Dienst. |
Rechtliche Einordnung (keine Rechtsberatung): Der Vergleich nutzt die Tageswerte des Vorzeitraums (bis zu 180
Tage zurück; Tageswerte werden 25 Monate aufbewahrt). Durchschnitte und Tageswerte für Besucher, Kontakte und
Verweildauer stammen aus bereits erhobenen und verdichteten Daten (`daily`: `visitors`, `ct_kind`,
`en_count`, `en_sec_sum`, `scroll_sum`). Die Leistungsbeschreibung (§ 5: „Kennzahlen und Diagramme für die
letzten 7, 30 oder 90 Tage“) deckt das ab; kein neuer Text, keine neue Einwilligung.
## 1. Daten: Statistik-Antwort des Collectors
`GET /a/admin/stats/:siteKey?range=7|30|90` (interner Listener, `analytics/src/stats.js`) liefert **zusätzlich**
zu den bisherigen Feldern (alle bisherigen Felder bleiben unverändert, damit der Wechsel abwärtskompatibel ist):
```js
{
range: 30,
from: '2026-09-05', to: '2026-10-04', // Berliner Kalendertage des Zeitraums (to = heute)
firstDay: '2026-08-20', // frühester Berliner Tag mit Daten dieser Website ('' = keine)
totals: {
pv, visitors, contacts, consentRate, // wie bisher
engaged, // Zahl der en-Ereignisse (Seitenbesuche mit Zustimmung und Abschluss)
avgSec, // Ø Sekunden pro Seitenbesuch = en_sec_sum / en_count, gerundet; null wenn engaged = 0
avgScroll, // Ø Scrolltiefe in % = scroll_sum / en_count, gerundet; null wenn engaged = 0
},
series: [{ day, pv, visitors, contacts, avgSec }], // bisher nur {day, pv}; avgSec je Tag null ohne en
pages, refs, countries, devices, contacts, banner, // unverändert
previous: {
from: '2026-08-06', to: '2026-09-04', // die `range` Kalendertage direkt vor `from`
complete: true, // firstDay !== '' && firstDay <= previous.from
totals: { pv, visitors, contacts, consentRate, engaged, avgSec, avgScroll },
series: [{ day, pv, visitors, contacts, avgSec }],
contacts: [{ key, count }], // je Art, für die Veränderung pro Zeile
},
}
```
Regeln:
- Tageslisten werden **kalendarisch** gebildet (Berliner Tage, wie `dayList`), nicht durch Abziehen von
`range × 24 h` – sonst verrutscht der Vorzeitraum bei der Zeitumstellung um einen Tag.
- Gemeinsame Hilfsfunktion sammelt die Dimensionen für eine beliebige Tagesliste: verdichtete Tage aus `daily`,
nicht verdichtete Tage live aus `events_raw` (wie heute). Kein Tag wird doppelt gezählt.
- `firstDay` = kleinster Tag aus `daily` und `events_raw` dieser Website (`MIN(day)` bzw. `dayOf(MIN(ts))`).
- `visitors` bleibt die Summe der Tages-Besucher (Salz wechselt täglich, kein tagesübergreifender Abgleich).
- Der Portal-Server reicht die Antwort unverändert durch (`GET /api/funktionen/analyse/stats`).
Portal-Server, einzige weitere Änderung: `analyseView` (Route `GET /api/funktionen`) gibt zusätzlich
`activatedAt` aus dem Analyse-Dokument zurück (`null`, wenn nicht aktiv).
## 2. Oberfläche
### 2.1 Aufbau der Seite (Zustand „aktiv“)
```
[ Kopfleiste: ● Website-Name · „Analyse aktiv · Stand: heute, 19:42 Uhr“ (Website-Auswahl) [7 | 30 | 90 Tage] ]
[ Reiter: Übersicht | Heatmap | Einstellungen ]
Übersicht:
[ Das Wichtigste · 5. Sep. – 4. Okt. — Klartext-Satz, darunter bis zu 4 Hinweise mit Symbol ]
[ Seitenaufrufe ] [ Besucher* ] [ Kontakt-Aktionen* ] [ Ø Zeit pro Seite* ] je: Zahl, ▲/▼-Chip, „vs. <Vorwert>“, Mini-Verlauf
[ Seitenaufrufe pro Tag (Linie + gestrichelter Vorzeitraum) ] [ Kontakt-Aktionen: Anrufe, E-Mails, WhatsApp, Links nach außen + Quote ]
[ Was angesehen wird: Seiten | Herkunft ] [ Wer zu Besuch ist: Geräte | Länder ]
[ Datenbasis: Ring mit Zustimmungsquote + Erklärung ] [ Heatmap: Vorschaubild + „Öffnen“ → Reiter Heatmap ]
Fußzeile: „Länderdaten: IP-Geolocation by DB-IP“
```
- **Kopfleiste**: Statuspunkt (türkis = aktiv). Bei `devMode` statt „Analyse aktiv“ gelb: „Im Dev-Modus wird
nicht gemessen.“ „Stand“ = Ladezeitpunkt der Daten. Bei mehreren Websites ein Auswahlfeld in der Leiste
(ersetzt das bisherige `fn-project`-Panel). Der Zeitraum-Schalter erscheint nur im Reiter Übersicht
(die Heatmap zeigt fest 30 Tage).
- **Reiter**: `role="tablist"`, Zustand bleibt beim Wechsel von Zeitraum oder Website erhalten
(`analyseSel.tab`), der URL-Hash bleibt `#analyse`. Auf dem Handy horizontal wischbar.
- **Das Wichtigste**: siehe 2.3. Unter 30 Seitenaufrufen im Zeitraum: „Sobald ein paar mehr Besuche da sind,
steht hier das Wichtigste über deine Website in zwei, drei Sätzen.“
- **Kennzahlen-Kacheln** (4): Seitenaufrufe, Besucher*, Kontakt-Aktionen*, Ø Zeit pro Seite* (Format `m:ss`,
darunter klein „Ø 64 % gescrollt“). Chip mit Veränderung (2.4). Mini-Verlauf aus `series` (Fläche + Linie,
ohne Achsen). Ohne Vergleich steht statt Chip „Vergleich ab TT.MM.“ (2.4).
- **Diagramm**: Seitenaufrufe pro Tag, aktuelle Linie türkis mit Fläche, Vorzeitraum gestrichelt grau (nach
Tagesindex ausgerichtet; nur wenn `previous.complete`). Legende „30 Tage“ / „Zeitraum davor“. Antippen oder
Überfahren eines Tages zeigt eine Blase „Sa., 19.09.: 371 Aufrufe (davor 210)“. SVG behält `<title>`.
- **Kontakt-Aktionen**: Summe im Kopf, Zeilen Anrufe (`tel`), E-Mails (`mail`), WhatsApp (`wa`), Links nach außen
(`out`) mit Symbol, Zahl und Veränderungs-Chip; Arten mit 0 in beiden Zeiträumen entfallen. Darunter die
Quote: „Auf 100 Besucher kommen 7,8 Kontakt-Aktionen (davor 6,5).“ (erst ab 30 Besuchern; Kontakt-Aktionen sind
Klicks, keine Personen – daher „pro 100 Besucher“, nicht „x % der Besucher“). Ohne Kontakte: „Noch keine
Kontakt-Aktionen. Sie zählen, wenn jemand auf deiner Website auf Telefonnummer, E-Mail-Adresse oder
WhatsApp tippt.“
- **Ranglisten** in zwei Karten mit kleinem Umschalter: „Was angesehen wird“ (Seiten | Herkunft), „Wer zu Besuch
ist“ (Geräte | Länder). Zeile = Balken als Hintergrund des Namens, Zahl, Anteil in %. Erst 6 Zeilen, „Alle
anzeigen“ klappt bis 20 auf. `/` heißt „Startseite“. Herkunft mit lesbaren Namen für bekannte Hosts
(google.* → Google, bing.com → Bing, instagram.com → Instagram, facebook.com/m.facebook.com/l.facebook.com →
Facebook, duckduckgo.com → DuckDuckGo, ecosia.org → Ecosia), sonst der Host; `''` → „Direkt oder unbekannt“.
- **Datenbasis** (ersetzt Banner-Liste und Sternchen-Fußnote): Ring mit `consentRate`, Text „61 % haben dem
Banner zugestimmt (2.410 von 3.950 Entscheidungen). Seitenaufrufe zählen wir vollständig; Besucher,
Kontakt-Aktionen und Zeit pro Seite (*) nur bei Zustimmung.“ Ohne Entscheidungen: „Noch hat niemand über das
Banner entschieden.“
- **Heatmap-Karte**: kleines statisches Vorschaubild (CSS-Verlauf, keine echte Website), Satz, Knopf „Öffnen“
wechselt in den Reiter Heatmap.
### 2.2 Reiter Heatmap und Einstellungen, andere Zustände
- **Heatmap**: die bestehende Heatmap in Geräterahmen (`renderAnalyseHeat`, `geraete.js/.css`, iframes,
postMessage-Zähler, 20-s-Rückfall „In neuem Tab öffnen“) unverändert im Verhalten, optisch im Cockpit-Stil
(Seitenauswahl und Computer/Tablet-Schalter in einer Leiste). Die iframes laden erst, wenn der Reiter
geöffnet wird.
- **Einstellungen**: Karte „Status“ (aktiv seit `activatedAt`, Banner-Farbe als Farbfeld, Fassung
`avvVersion`), Karte „Unterlagen“ (Links AVV und Leistungsbeschreibung aus `legal`), Karte „Analyse
ausschalten“ (`renderAnalyseOff`, Texte und Ablauf unverändert).
- **Einschalten** (`renderAnalyseActivation`), **gesperrt**, **nicht verfügbar**, **unbekannt**, **noch nicht
freigeschaltet**: gleicher Cockpit-Rahmen (Kopfleiste ohne Zeitraum und Reiter). Alle Texte, Links, Häkchen,
Farbwahl und Banner-Vorschau bleiben **wörtlich** wie heute (Rechtsrunde 01./02.10.).
- **Laden**: Platzhalter-Kacheln in der Form der echten Kacheln statt „Lade…“. **Fehler 503**: Karte
„Auswertung gerade nicht erreichbar“ mit Knopf „Neu laden“.
### 2.3 Klartext-Regeln („Das Wichtigste“)
Reine Funktion `summarize(stats)` → `{ lead, items: [{ icon, text }] }` in `public/analyse-view.js`
(ohne DOM, Node-testbar). Zahlen deutsch formatiert. Nur bei `totals.pv >= 30`, sonst Leerzustand (2.1).
**Satz** (`lead`):
- Vergleich möglich (2.4): „Deine Website wurde in den letzten N Tagen **X-mal** aufgerufen – **Y % mehr** /
**Y % weniger** als in den N Tagen davor.“ Bei |Veränderung| < 5 %: „… – etwa so oft wie in den N Tagen
davor.“ Bei kleinen Zahlen (2.4) absolut: „… – **12 Aufrufe mehr** als in den N Tagen davor.“
- Kein Vergleich: „Deine Website wurde in den letzten N Tagen **X-mal** aufgerufen.“ Liegt `firstDay` im
Zeitraum: „Seit dem Start der Analyse am TT.MM. wurde deine Website **X-mal** aufgerufen.“
**Hinweise** (`items`, in dieser Reihenfolge, nur wenn die Bedingung erfüllt ist, höchstens 4):
1. Kontakte (`contacts >= 1`): „N-mal hat dich jemand direkt kontaktiert, meist per Telefon (96).“ + bei
Vergleich und Vorwert ≥ 1: „Das sind 45 mehr als zuvor.“ / „… weniger …“ / „Genauso oft wie zuvor.“
Bei 0 Kontakten und `visitors >= 30`: „Von den Besuchern mit Zustimmung hat noch niemand über die Website
angerufen oder geschrieben.“ (Kontakt-Aktionen zählen nur bei Zustimmung.)
2. Wochentag (mindestens 7 Tage mit Daten im Zeitraum und `pv >= 70`): Ø Aufrufe je Wochentag über die Tage
ab `firstDay`; stärkster Wochentag, wenn sein Schnitt ≥ 1,2 × Gesamtschnitt: „Samstag ist im Schnitt dein
stärkster Tag.“ Zusätzlich Spitzentag, wenn ≥ 1,8 × Tagesschnitt: „Spitzentag war Sa., 19.09. mit 371
Aufrufen.“ (beides in einem Hinweis, wenn beides zutrifft).
3. Herkunft (erster Eintrag in `refs` mit Host, Anteil ≥ 20 % der Herkunfts-Summe): „Die meisten, die über
einen Link kamen, kommen von Google (880).“
4. Gerät (`m` oder `d` ≥ 55 % der Geräte-Summe): „66 % der Aufrufe kommen vom Handy.“ / „… vom Computer.“
5. Seite (`pages[0]`): „Am meisten angesehen: die Startseite, danach /preise (1.310).“ bzw. „Am meisten
angesehen: /preise (1.310 Aufrufe).“
### 2.4 Vergleich und Veränderungs-Chips
- Vergleich möglich, wenn `previous.complete` und `previous.totals.pv >= 20`. Sonst kein Chip; Kacheln zeigen
„Vergleich ab TT.MM.“ mit TT.MM. = `firstDay` + (2 × range − 1) Tage; ohne `firstDay` gar nichts. Fehlt
`previous` ganz (alter Collector), wie „kein Vergleich“ ohne Datum.
- Funktion `delta(cur, prev, { kind })` → `{ dir: 'up'|'down'|'flat', text, aria }`:
- Zählwerte: Prozent (ganzzahlig, „▲ 14 %“), wenn `prev >= 100` und `cur >= 100`; sonst absolut („▲ 12“,
„▼ 3“). Gleich → „± 0“ (`flat`). |Prozent| < 1 → `flat` „± 0 %“.
- Sekunden: absolut, „▲ 6 s“ / „▼ 1:05“ (ab 60 s als m:ss).
- `aria`: „plus 14 Prozent gegenüber dem Zeitraum davor“ usw. Farbe nie alleiniger Träger (Pfeile ▲▼).
- Farben: mehr = türkis, weniger = rosé, gleich = grau. Bewusst **keine** Wertung „gut/schlecht“ (weniger
Zeit pro Seite kann gut sein).
### 2.5 Optik („Cockpit“)
- Alles unter einem Wurzelelement `.an` mit eigenen CSS-Variablen; außerhalb ändert sich nichts.
Akzent `--an-acc: #2dd4bf`, stark `#5eead4`, weich `rgba(45,212,191,.1)`; weniger `#fda4af`;
Karten `rgba(14,16,20,.92)`, Rand `rgba(255,255,255,.09)`, Radius 16 px, Abstände 14 px.
- Kartentitel einheitlich: 13 px, fett, Großbuchstaben mit Sperrung, gedämpft; Zahlen `tabular-nums`.
- „Das Wichtigste“: Verlauf türkis → violett, türkiser Rand, Satz 21 px.
- Desktop: Raster wie Entwurf A (4 Kacheln; Diagramm 1,65 : 1 Kontakte; 2 × 2 Karten). Diagramm und
Kontaktkarte gleich hoch, das Diagramm füllt die Höhe.
- Handy (≤ 880 px, App-Ansicht): Kopfleiste zweizeilig, Zeitraum-Schalter volle Breite, Kacheln 2 × 2, Hinweise
einspaltig, alle Karten untereinander. Keine waagrechte Seiten-Scrollleiste.
- `prefers-reduced-motion`: keine Übergänge.
## 3. Code-Aufteilung
`public/funktionen.js` (980 Zeilen) wird entlastet:
- **`public/analyse.js`** (neu): gesamte Analyse-Oberfläche – `initAnalyse`, `renderAnalyseActivation`,
`renderAnalyseOff`, Übersicht, Heatmap, Einstellungen. `app.js` lädt `/analyse.js` für die Ansicht `analyse`.
- **`public/fn-util.js`** (neu): `esc`, `safeUrl`, `request` – bisher lokal in `funktionen.js`, jetzt von
beiden Modulen importiert.
- **`public/analyse-view.js`**: bekommt die reinen Funktionen `summarize`, `delta`, `fmtDuration`,
`contactRate`, `compareFrom`, `refName`, `weekdayStats` sowie `analyseOf`, `analyseState`,
`pickAnalyseProject` (bisher in `funktionen.js`, von der Kachel und der Analyse gebraucht).
- **`public/analyse.css`** (neu, in `dashboard.html` eingebunden): alle `.an`-Regeln; die alten
`.an-*`-Regeln in `funktionen.css` entfallen, soweit sie nur die Analyse betreffen.
- Kachel „Analyse“ auf der Funktionen-Seite bleibt in `funktionen.js` (unverändert im Verhalten).
- Bestehende Element-IDs, die Tests und Abläufe nutzen, bleiben: `#analyse-body`, `#an-stage`, `#an-preview`,
`#an-avv`, `#an-on`, `#an-error`, `#an-off-*`, `#an-heat-page`.
## 4. Tests
- **Collector** (`analytics/test/stats.test.js`): `previous` mit richtigen Tagen und Summen; `complete`
false/true je nach `firstDay`; `firstDay` aus `daily` und Rohdaten; `series` mit visitors/contacts/avgSec;
`avgSec`/`avgScroll` null ohne en; Vorzeitraum über eine Zeitumstellung (Ende Oktober) lückenlos und ohne
Überlappung; nichts doppelt (verdichtet + live). Vertragstest `test/analyticsContract.test.js` um die neuen
Felder ergänzen.
- **Portal, rein** (`test/analyseInsights.test.js`, neu): `summarize` für Wachstum, Rückgang, gleich, kleine
Zahlen, ohne Vergleich, neu gestartet, zu wenig Daten, ohne Kontakte, Wochentag/Spitzentag, Herkunft, Gerät,
höchstens 4 Hinweise; `delta` (Prozent/absolut/flat/Sekunden/aria); `compareFrom`; `contactRate`;
`refName`; HTML-Maskierung aller Werte, die aus Daten kommen (Seitenpfade, Hosts).
- **Portal, bestehend**: `funktionenTiles.test.js` und `analyseView.test.js` auf `analyse.js`/`fn-util.js`
umstellen; alle bisherigen Text-Prüfungen (Einschalten, Ausschalten) laufen unverändert weiter.
`analyseRoutes.test.js`: `activatedAt` in der Ansicht.
- **Browser** (`test/ui/analyse.ui.mjs`, erweitert; Playwright mit gemockter API): Desktop 1440 und Handy 402 ×
Zustände aktiv (volle Daten), aktiv neu (ohne Vergleich), wenig Daten, Dev-Modus, Einschalten, fehlende
Angaben, 503; Reiterwechsel; Heatmap-iframes erst nach Reiterwechsel; keine `pageerror`; keine waagrechte
Scrollleiste. Screenshots nach `/root/reports/analyse-ueberarbeitung-2026-10-04/screens/`.
- Gesamte Suiten: `npm test` (Portal) und `analytics: npm test` grün; Node 20 (Portal) und Node 24 (Collector).
## 5. Livegang
1. Nutzer pusht `feature/analyse-uebersicht` nach `main` (mein Push nach `main` ist im Auto-Modus gesperrt).
2. `deploy.sh` startet Portal und – weil `analytics/` geändert – den Collector neu. Keine Schema-Änderung,
keine `.env`-Änderung.
3. Prüfung durch mich: Collector `/a/health`, Stats-Abruf einer echten aktiven Website im Container
(neue Felder vorhanden, alte unverändert), Portal-Bundle enthält `analyse.js`, Ansicht lädt ohne Fehler.
4. Nutzer testet live mit einer Website mit aktiver Analyse.
Rückweg: Portal und Collector sind gegenseitig abwärtskompatibel (alte Oberfläche ignoriert neue Felder, neue
Oberfläche kommt ohne `previous` aus); Zurückrollen = Revert-Commit auf `main`.
## Nicht in diesem Projekt
Besucher-Details, Website-Check, Bericht per Mail (U1); Änderungen an Messskript, Banner und Rechtstexten (U4);
WOMS; Anpassen der Banner-Farbe nach dem Einschalten; Export.