Spec: Funktionen-Tab und News-Pinnwand
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
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.)
|
||||
Reference in new issue
Block a user