Files
Webklar-Kundenbereich/docs/superpowers/specs/2026-09-30-funktionen-news-design.md

215 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Funktionen-Tab und News-Pinnwand – Design
Stand: 2026-09-30 · Status: vom Nutzer im Chat freigegeben (Abschnitte 1–6), Spec zur Durchsicht
Teilprojekte: **A** Portal-Umbau + **B** News (diese Spec). **C** Analyse mit Cookie-Banner ist ausdrücklich
nicht Teil dieser Spec und bekommt eine eigene.
## Ziel
Kunden können im Kundenportal Zusatzfunktionen für ihre Website buchen. Die erste ist **News**: der
Kunde schreibt im Portal kurze Beiträge (Text, Foto oder beides), die auf seiner Website als Pinnwand
erscheinen – jeder Besucher sieht sie. Referenz für Optik und Bedienung ist der freigegebene Entwurf
„Schatzkiste Pinnwand-Entwurf“ (https://claude.ai/artifact/E1ad9iK7P91Su2GrLKJQMm, Quelle
`/root/reports/emmas-news-entwurf/pinnwand-entwurf.html`, Kork-Hintergrund mit Holzrahmen #A8733F).
## Entscheidungen (vom Nutzer)
| # | Entscheidung |
|---|---|
| E1 | Tab „Fortschritt“ entfällt, sein Inhalt wandert unter „Projekte“. |
| E2 | Neuer Tab heißt **Funktionen**. Später kommen weitere Funktionen dazu (z. B. Terminplaner). |
| E3 | News ist standardmäßig **aus**. **Jeder Kunde** kann es buchen, ein Abo ist nicht nötig. |
| E4 | Preis: **30 € einmalig für den Einbau**, die Nutzung danach ist **kostenlos**. Kein Abo, kein Testmonat. |
| E5 | Mit der Buchung wird ein **Ticket** eröffnet; erst wenn es geschlossen ist, ist News für den Kunden aktiv. Es gibt keine feste Fertigstellungszeit, nur eine grobe Einschätzung. |
| E6 | Anzeigedauer **pro Beitrag**: 1 Woche · 2 Wochen · 1 Monat (Vorauswahl) · 3 Monate · bis ich ihn abnehme · bis zu einem Datum. Abgelaufenes bleibt im Portal unter „Abgelaufen“ und lässt sich wieder aufhängen. |
| E7 | Website zeigt die **neuesten 6** Beiträge, ältere über „Ältere Neuigkeiten ansehen“. |
| E8 | Auslieferung über **Weg 1**: öffentliche Lese-Schnittstelle im Portal + Skript auf der Kundenwebsite. |
| E9 | Admin kann News ohne Zahlung aktivieren (Pilot Emma's Schatzkiste, Kulanz). |
## A. Portal-Umbau
**Navigation** (Seitenleiste am Desktop, Tab-Leiste unten in der App-Ansicht ≤ 880 px):
Projekte · **Funktionen** · Rechnungen · Chat · Verträge.
**Projekte** (`#view-projects`): unter den Projektkarten eine Sektion „Was wir zuletzt erledigt haben“
mit dem bisherigen Inhalt von `loadCustomerActivity` (Karte je Auftrag mit Status, Fortschrittsleiste,
Arbeiten). Standardmäßig die 3 neuesten Aufträge, darunter „Alle anzeigen“. `#view-activity` und der
Nav-Eintrag entfallen. `VIEW_HASHES`/Router: der Hash `#fortschritt` öffnet Projekte und scrollt zur
Sektion (alte Links aus Mails und Lesezeichen). `APP_PARENT_LABEL.activity` entfällt.
**Funktionen** (`#view-funktionen`, Hash `#funktionen`): Kachel-Übersicht, eine Kachel je Funktion.
- **News**: Status-Pille *Aus* / *In Einrichtung* / *Aktiv* und der passende Knopf
(„Einrichten lassen · 30 €“ / „Status ansehen“ / „Beiträge schreiben“).
- **Terminplaner**, **Analyse**: Pille „Kommt bald“, ohne Aktion.
- News öffnet `#view-news` (Hash `#news`); in der App-Ansicht Unterseite mit „Zurück zu Funktionen“
(`APP_PARENT.news = 'funktionen'`), am Desktop normale Ansicht.
- Hat der Kunde mehrere Website-Projekte, gilt News **pro Projekt**; die News-Ansicht hat dann oben eine
Projektauswahl, die Kachel zeigt „aktiv für 1 von 2 Websites“.
## B1. Buchen und Freischalten
**Zustände** je (Kunde, Projekt) im vorhandenen Collection `portalFeatures`
(`featureKey = 'news'`, `projectId`, `enabled`, `unlockedByPurchase`, `purchaseStatus`). Ergänzt werden
Attribute: `state` (`off` | `ordered` | `setup` | `active` | `disabled`), `ticketId`, `stripeSessionId`,
`stripeInvoiceId`, `orderedAt`, `activatedAt`, `siteKey`. `enabled` bleibt als abgeleitetes Feld
(`state === 'active'`) für Rückwärtskompatibilität von `GET /api/features`.
```
off ──Checkout bezahlt──▶ setup ──Ticket geschlossen──▶ active
│ ▲ │
└──Admin „aktivieren“──────┴──────────────────────────────┘ (Admin kann direkt active setzen)
active ──Admin „abschalten“ / Kunde verlässt WEBklar──▶ disabled
```
`ordered` ist der kurze Zustand zwischen Checkout-Start und Zahlungseingang (Checkout verfallen ⇒ zurück
auf `off`).
**Buchungsseite** (in `#view-news`, solange `off`):
- Was der Kunde bekommt: Pinnwand-Sektion im Stil seiner Website, Einbau durch uns, Beiträge danach
jederzeit selbst schreiben, dauerhaft ohne weitere Kosten.
- Grobe Zeiteinschätzung: „meist innerhalb von 1–2 Wochen“ (Text in `config`, kein Versprechen).
- Projektauswahl bei mehreren Websites.
- Pflicht-Checkbox (Widerruf, analog Abo-Bestellung): „Ich verlange ausdrücklich, dass WEBklar vor Ablauf
der Widerrufsfrist mit dem Einbau beginnt. Mir ist bekannt, dass mein Widerrufsrecht mit der vollständigen
Erbringung des Einbaus erlischt (§ 356 Abs. 5 Nr. 2 BGB). Widerrufe ich vor der vollständigen Erbringung,
schulde ich einen angemessenen Betrag für die bis dahin erbrachte Leistung (§ 357a Abs. 2 BGB).“ Einwilligung
wird mit Zeitstempel und Textfassung am Feature-Dokument gespeichert (wie die Abo-Consent-Metadaten).
*Korrekturvermerk 2026-10-05 (Terminplaner TP2, Task F2): Ursprünglich stand hier „… erlischt, sobald der Einbau
vollständig erbracht ist (§ 356 Abs. 4 BGB).“ Zitat und Wertersatz-Satz sind korrigiert bzw. ergänzt (Entwurf bis
zum Befund der Kanzlei, Kanzlei-Paket Frage Q3). Bestehende Bestellungen behalten den damals gespeicherten Text
und die damalige Fassung.*
- Knopf „**Zahlungspflichtig bestellen · 30 €**“ ⇒ `POST /api/funktionen/news/checkout`.
**Zahlung**: Stripe Checkout `mode: 'payment'`, einmaliger Preis über `ensurePrice('news_setup')`
(Katalog in `config`/`services/products.js`, 30,00 €; *Korrekturvermerk 2026-10-05: ohne Umsatzsteuer gemäß § 19 UStG, nicht „brutto“*), `invoice_creation.enabled = true`,
Zahlarten wie beim Abo (Karte, SEPA). Metadaten `{ customerId, projectId, featureKey: 'news' }`.
**Stripe-Webhook** `checkout.session.completed` (bzw. `checkout.session.async_payment_succeeded` bei SEPA):
1. Feature-Dokument auf `setup`, `stripeInvoiceId` merken.
2. WOMS-Ticket anlegen (neue Funktion `createFeatureTicket` in `ticketAdmin.js`, gleiche Felder wie
`ensureProjectTicket`): `topic` „News-Pinnwand einbauen – <Projekt>“, `type` 'Webpage',
`status` 'Open', `details` mit Projekt, Website-Domain, siteKey und Link zur Spec/Einbauanleitung.
`ticketId` am Feature-Dokument.
3. Mails: Kunde „Deine Bestellung: News-Pinnwand“ (neue Vorlage im Stil von K01), Team-Hinweis (T-Vorlage).
Idempotent über `stripeSessionId` (Webhook kann mehrfach kommen).
**Aktivieren**: Hintergrund-Job alle 5 Minuten (neben den vorhandenen `setInterval`-Jobs in
`server/index.js`) plus sofortige Prüfung beim Öffnen von Funktionen: Features in `setup`, deren Ticket
`status === 'Closed'` hat ⇒ `active`, `activatedAt`, Mail an Kunden „Deine Pinnwand ist bereit“.
Wird ein Ticket wieder geöffnet, bleibt die Funktion aktiv (kein Zurückschalten).
**Rechnung**: kommt per Stripe-Mail; zusätzlich Link „Rechnung ansehen“ in der News-Ansicht
(`hosted_invoice_url`). Der Tab „Rechnungen“ (Invoice Ninja) bleibt unverändert.
**Admin** (`admin.html`, Kundenansicht): je Projekt Zeile „News“ mit Zustand und Knöpfen
„Aktivieren (ohne Zahlung)“ / „Abschalten“. Protokolliert wer/wann.
## B2. Editor im Portal (`#view-news`, Zustand `active`)
Aufbau wie der Entwurf („Meine Pinnwand“):
- Schritt 1: Art – Text · Foto · Foto + Text.
- Schritt 2: Foto (Datei; am Handy zusätzlich „Foto aufnehmen“ per `capture`), Überschrift (≤ 60 Zeichen,
bei Foto „Bildunterschrift“), Text (≤ 400 Zeichen, Zähler), Zettelfarbe (Sonnengelb, Himmelblau,
Koralle, Weiß), **Anzeigedauer** (E6; „bis zu einem Datum“ öffnet Datumsfeld, frühestens morgen,
höchstens 1 Jahr).
- Live-Vorschau „so sehen es alle“ (Desktop rechts, sticky; Handy aufklappbar).
- Knopf „Aufhängen“; beim Ändern „Speichern“.
- Liste „Hängt gerade“ (Ändern, Abnehmen) und „Abgelaufen“ (Wieder aufhängen = neue Dauer wählen, Löschen
mit Rückfrage in der Seite, kein `confirm()`).
- Oben „Auf meiner Website ansehen“ (Link auf die Live-Domain des Projekts).
- Zustand `setup`: Statuskarte „Wir bauen deine Pinnwand ein“ mit Ticketnummer, Bestelldatum,
Zeiteinschätzung, Rechnungslink, „Frage dazu? Chat öffnen“.
**API** (alle `requireSession`, Kunde muss Eigentümer des Projekts sein, Feature `active`):
- `GET /api/funktionen` – Zustände aller Funktionen je Projekt.
- `POST /api/funktionen/news/checkout` – `{ projectId, consent }` ⇒ `{ url }`.
- `GET /api/news?projectId=` – eigene Beiträge (hängend + abgelaufen).
- `POST /api/news` – neu; `PATCH /api/news/:id` – ändern/abnehmen/wieder aufhängen; `DELETE /api/news/:id`.
- `POST /api/news/image` – Upload (multipart), Antwort `{ imageId }`.
Rate-Limit: höchstens 30 Schreibvorgänge und 20 Uploads pro Kunde und Stunde.
## B3. Speicherung
**Collection `newsPosts`** (Schema-Skript wie `portal-setup.mjs`):
`customerId`, `projectId`, `type` (`note`|`photo`|`combo`), `title` (60), `text` (400), `imageId`,
`paper` (`sun`|`sky`|`coral`|`white`), `visibleFrom` (ISO), `visibleUntil` (ISO oder leer = bis abgenommen),
`takenDownAt` (ISO oder leer), `createdAt`, `updatedAt`. Index auf (`projectId`, `visibleFrom`).
„Hängt“ ⇔ `takenDownAt` leer ∧ `visibleFrom ≤ jetzt` ∧ (`visibleUntil` leer ∨ `visibleUntil > jetzt`).
**Bilder**: `upload-data/news/<projectId>/<uuid>.webp` (Muster wie `chatAttachments.js`).
Annahme: JPEG, PNG, WebP, HEIC ≤ 10 MB. Verarbeitung mit `sharp` (neue Abhängigkeit): EXIF-Rotation
anwenden, **alle Metadaten entfernen** (GPS!), längste Kante ≤ 1600 px, WebP q≈80; zusätzlich eine
Vorschau ≤ 480 px. Nicht verwendete Uploads werden nach 24 h gelöscht (wie Chat). Beim Löschen eines
Beitrags wird das Bild mitgelöscht.
**Aufbewahrung**: Beiträge bleiben, bis der Kunde sie löscht. Bei `disabled` werden sie nicht mehr
ausgeliefert; die Löschung folgt der bestehenden Kundendaten-Frist in `services/retention.js`
(Erweiterung um `newsPosts` + Bilder).
## B4. Anzeige auf der Kundenwebsite
**siteKey**: zufälliger, nicht erratbarer Schlüssel je Feature-Dokument (z. B. 16 Zeichen base32), damit
keine Projekt- oder Kunden-IDs öffentlich werden.
**Öffentliche Schnittstelle** (ohne Login, nur GET):
- `GET /api/public/news/:siteKey?page=1` ⇒ `{ posts: [{ id, type, title, text, paper, date, image: { src, w, h } }], hasMore }`,
6 pro Seite, neueste zuerst. Nur hängende Beiträge; Feature nicht `active` ⇒ `{ posts: [], hasMore: false }`.
Keine internen Felder. `Cache-Control: public, max-age=60`, CORS `*` (nur GET), ETag.
- `GET /api/public/news/:siteKey/img/:imageId(.webp|-s.webp)` ⇒ Bild, nur wenn der Beitrag hängt;
`Cache-Control: public, max-age=86400`, `X-Content-Type-Options: nosniff`.
- Rate-Limit pro IP großzügig (z. B. 120/min), damit nichts missbraucht wird.
**Einbau-Skript** `https://project.webklar.com/embed/news.js` (statisch aus `public/embed/`):
- Sucht `<div data-webklar-news="<siteKey>">`, lädt die Beiträge, rendert Zettel (Text-Zettel, Polaroid,
Polaroid + Zettel) wie im Entwurf inkl. „Neu“-Schild am neuesten, Leerzustand „Hier hängt bald was Neues.“,
„Ältere Neuigkeiten ansehen“ (lädt Seite 2 nach), Handy: Wisch-Reihe.
- Optik über CSS-Variablen, die die Seite setzt (`--wkn-board`, `--wkn-frame`, `--wkn-font-display`,
`--wkn-font-body`, Zettelfarben); Standard = Kork + Holzrahmen.
- Kein Cookie, kein Storage, keine weiteren Drittanfragen. Text wird als Text eingesetzt (kein innerHTML
mit Kundentext). Ohne JS oder bei Fehler bleibt der statische Leerzustand im HTML stehen.
- Der Einbau (die 30 €) = Sektion im Stil der Kundenseite + `div` + Skript + Datenschutz-Absatz.
**Datenschutz**:
- Kundenwebsite: Absatz „Neuigkeiten“ (Abruf von project.webklar.com; bei bei uns gehosteten Seiten ist
WEBklar ohnehin Hoster/Auftragsverarbeiter; keine Cookies).
- Portal-Datenschutzerklärung: Abschnitt zu News-Beiträgen und Fotos (Zweck, Speicherort, Löschung,
Entfernen der Metadaten, öffentliche Sichtbarkeit auf der eigenen Website).
- Beide Texte werden als Entwurf formuliert und dem Nutzer zur Freigabe vorgelegt.
**Pilot**: Emma's Schatzkiste – Admin aktiviert News ohne Zahlung, Einbau in Kork-Optik auf der Startseite
(Position nach dem Hero, Nav-Punkt „Neuigkeiten“).
## Fehlerfälle
- Checkout abgebrochen/verfallen ⇒ `off`, Kunde kann neu buchen.
- Webhook doppelt ⇒ idempotent über `stripeSessionId`.
- SEPA-Zahlung scheitert nachträglich ⇒ `off`, Ticket bekommt Notiz, Team-Mail.
- Ticket gelöscht statt geschlossen ⇒ Feature bleibt `setup`, Team-Hinweis im Admin („Ticket fehlt“).
- Upload zu groß / falscher Typ ⇒ Meldung am Feld („Das Foto ist größer als 10 MB. Bitte ein kleineres wählen.“).
- Appwrite nicht erreichbar ⇒ Portal zeigt Fehler am Editor; öffentliche Schnittstelle liefert `503`,
das Skript lässt den Leerzustand stehen.
## Tests
- `npm test` (node --test), mit den vorhandenen Test-Doubles (`test/support/` appwriteMock, fakeStripe):
Zustandsübergänge inkl. Idempotenz, Ticket geschlossen ⇒ aktiv, Ablauf-Logik „hängt“ an Grenzen
(Zeitzonen Europe/Berlin), öffentliche Schnittstelle gibt nur Freigegebenes und keine internen Felder aus,
Bildverarbeitung entfernt EXIF/GPS, Rechte (fremdes Projekt ⇒ 403), Rate-Limits.
- Playwright auf Fixture-Server: Projekte mit Fortschritt-Sektion, `#fortschritt`-Umleitung, Funktionen-
Kacheln in allen drei Zuständen, Buchungsseite, Editor (PC + Handy), `news.js` auf einer Testseite
(leer, 1 Beitrag, 8 Beiträge mit „Ältere“), ohne JS.
- Stripe-Checkout gegen fakeStripe, wie bei den Abo-Tests.
## Nicht Teil dieser Spec
- Teilprojekt C „Analyse“ (Cookie-Banner, Einwilligung, Auswertung).
- Terminplaner.
- Mehrsprachigkeit, Kommentare/Likes unter Beiträgen, geplante Veröffentlichung in der Zukunft.
## Offene Punkte zur Freigabe
1. Texte: Buchungsseite, Widerrufs-Checkbox, Kunden-/Team-Mails, Datenschutz-Absätze (Portal + Website) –
Entwurf kommt mit dem Plan.
2. Braucht es einen Zusatz in den AGB/Vertragsunterlagen für die Einrichtungsleistung „News“, oder
reicht die Leistungsbeschreibung auf der Buchungsseite? (Empfehlung: kurze Leistungsbeschreibung als
eigenes Dokument „Funktionen – Leistungsbeschreibung“, auf der Buchungsseite verlinkt.)