# 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.)