From 586e3cc85db16676685fc03463759b472a8ff27a Mon Sep 17 00:00:00 2001 From: knso Date: Wed, 30 Sep 2026 16:17:25 +0000 Subject: [PATCH] Spec: Funktionen-Tab und News-Pinnwand Co-Authored-By: Claude Opus 5.5 --- .../2026-09-30-funktionen-news-design.md | 209 ++++++++++++++++++ 1 file changed, 209 insertions(+) create mode 100644 docs/superpowers/specs/2026-09-30-funktionen-news-design.md diff --git a/docs/superpowers/specs/2026-09-30-funktionen-news-design.md b/docs/superpowers/specs/2026-09-30-funktionen-news-design.md new file mode 100644 index 0000000..6d349ef --- /dev/null +++ b/docs/superpowers/specs/2026-09-30-funktionen-news-design.md @@ -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 – “, `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//.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 `
`, 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.)