Spec: Funktionen-Tab und News-Pinnwand

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
knsoandClaude Opus 5.5 committed 2026-09-30 16:17:25 +00:00
1 parent 7ae0ffdd3a
commit 586e3cc85d
1 file changed
+209
@@ -0,0 +1,209 @@
# 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 erlischt, sobald
der Einbau vollständig erbracht ist (§ 356 Abs. 4 BGB).“ Einwilligung wird mit Zeitstempel und
Textfassung am Feature-Dokument gespeichert (wie die Abo-Consent-Metadaten).
- 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 € 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.)