Spec: Admin-Panel ins Ticketsystem (Kundenansicht, Protokoll, Team-Schnittstelle, 4 Etappen)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-01 19:11:32 +00:00
1 parent 546d3c84d0
commit 5fb39f1a1b
1 file changed
+309
@@ -0,0 +1,309 @@
# Admin-Panel ins Ticketsystem – Design
Stand: 2026-10-01 · Status: vom Nutzer im Chat freigegeben (Abschnitte 1–5), Spec zur Durchsicht
Betrifft zwei Repos: **Portal** `knso/Webklar-Kundenbereich` (project.webklar.com, Branch `main`) und
**WOMS** `JUSN/tickte-system` (ticket.webklar.com, Branch `test`).
## Ziel
project.webklar.com ist nur noch die Oberfläche für Kunden. Das Admin-Panel (`public/admin.html`) wird
abgeschafft. Mitarbeiter arbeiten ausschließlich im Ticketsystem (WOMS) und kommen nur noch für zwei Dinge
auf project.webklar.com: eine **Preview** ansehen oder das **Portal eines Kunden so öffnen, wie er es sieht**
(z. B. bei Fernwartung am Telefon), jeweils per Klick aus dem Ticketsystem.
## Entscheidungen (vom Nutzer)
| # | Entscheidung |
|---|---|
| E1 | Admin-Panel fällt komplett weg. „Übersicht“ und „Projekte“ des Panels ersatzlos (Projekte gibt es im WOMS). |
| E2 | Kundenansicht: **Variante A**. Erlaubt ist, was den Kunden nichts kostet; gesperrt bleiben Verträge, Geld und Passwort. Jede Aktion wird **protokolliert**. |
| E3 | **Alle Mitarbeiter gleich**: Chat, Gutschriften, Abo-Angebote, E-Mail-Speicher und Kundenansicht für jeden Mitarbeiter, nicht nur Admins. |
| E4 | Rechnungsanalyse (`kalkulation.html`) **erstmal raus**; Code bleibt in Git, kommt zurück, wenn die Leads wieder laufen. |
| E5 | **Alles zu einem Kunden an einem Ort**: neue Tabs auf der WOMS-Kundenseite; Sidebar-Unterpunkte unter „Kunden“ sind Sammellisten. |
| E6 | Technischer **Weg 1**: Der Portal-Server bleibt das Backend (Stripe, SMTP, Dateien), WOMS bekommt die Oberflächen. |
| E7 | Ausnahmen bei den Sammellisten: **Chat** ist ein Messenger mit zwei Spalten; **E-Mail-Speicher** hakt man direkt in der Liste ab. |
| E8 | In der Kundenansicht sind auch **Erklärungen im Namen des Kunden** gesperrt (Chat als Kunde, Gutschrift beantragen, Rückfrage beantworten). In der Praxis wird damit heute nur **News** frei. |
| E9 | Kundenansicht endet nach **2 Stunden**; Protokoll wird nach **12 Monaten** gelöscht; Datenschutzerklärung bekommt dazu einen Satz. |
| E10 | Mitarbeiter-Login auf project.webklar.com wird gesperrt (Hinweis + Link ins Ticketsystem). |
| E11 | Team-Mails: Texte und Design bleiben, nur Links (und Linktext „Admin-Bereich“ → „Ticketsystem“). |
| E12 | Umsetzung in **4 Etappen**; das Admin-Panel bleibt bis Etappe 4 in Betrieb. |
## Ist-Zustand (kurz)
- Admin-Panel: Tabs Übersicht, Projekte, Kunden, Chat, Gutschriften, Abo-Angebote, E-Mail-Speicher. Es spricht
`/api/portal-admin/*` mit Cookie-Sitzung (`requireAdminSession`, `server/routes/portalAdmin.js:38`). Jede
Mitarbeiter-Anmeldung im Portal (Eintrag in `employees` **oder** Label `admin`) bekommt diese Sitzung.
- WOMS ruft das Portal heute schon cross-origin mit Appwrite-JWT auf (`/api/admin/*`, `requireAppwriteStaff`,
verlangt Label `admin`): Finanzen, Kunden anlegen/löschen, Projekte, Services, Analyse.
- „Als Kunde ansehen“ gibt es (`POST /api/portal-admin/view-as/:id`); jede Schreib-Route sperrt sich dort einzeln.
- WOMS: Sidebar ist eine flache Liste (`src/components/Navbar.jsx:57-69`), Kundenseite
`src/pages/CustomerDetailPage.jsx` mit Tabs aus `src/lib/customerTabs.js` (Tab nur im React-State).
## 1. WOMS: Sidebar und Kundenseite
### Sidebar
```
Kunden
Chat ● n ungelesene Kundennachrichten
Gutschriften ● n offene Anträge
Abo-Angebote
E-Mail-Speicher ● n offene Speicher-Aufgaben
```
- Einträge in `links` bekommen optional `children`; Unterpunkte sind eingerückt und immer sichtbar. Eingeklappt
erscheinen sie als Symbole unter dem Kunden-Symbol. Zahlen nur, wenn > 0.
- Zahlen kommen aus einem Hook, der **alle 60 s** fragt, solange der Browser-Tab sichtbar ist
(`document.visibilityState`). Fehler lassen die letzte Zahl stehen.
- Der Knopf **„Projekt-Admin“** entfällt (Etappe 4).
- Routen: `/customers/chat` (optional `?kunde=<id>`), `/customers/gutschriften`, `/customers/angebote`,
`/customers/email-speicher`. Statische Segmente gehen in react-router v6 vor `/customers/:id`.
### Kundenseite
Tabs: `Übersicht · Tickets · Rechnungen · Projekte · Chat · Abo & Angebote · Gutschriften · Funktionen · Protokoll`
- „Rechnungen“ bleibt **nur für Admins** (die Finanzen-API ist admin-only). Alle neuen Tabs sehen alle Mitarbeiter.
- „Uebersicht“ wird zu „Übersicht“.
- Der Tab steht in der Adresse: `/customers/<id>?tab=chat|abo|gutschriften|funktionen|protokoll|…`; beim
Gutschriften-Tab zusätzlich `&antrag=<id>` (öffnet den Antrag direkt). Unbekannte Tabs → Übersicht (wie heute
`resolveCustomerTab`).
- Kopfzeile: Knopf **„Kundenansicht öffnen“** (neuer Browser-Tab). Ohne Portal-Zugang
(`portalAccessEnabled && appwriteUserId` fehlt) ausgegraut mit Hinweis „Kein Portal-Zugang“.
- Karte „Portal-Zugang“: Link „Portal öffnen“ (führte nur zum Login) wird durch denselben Knopf ersetzt; neu ist
die Zeile **„Letzter Login“**.
## 2. WOMS: die Bereiche
Grundsatz: **Funktionsgleich zum heutigen Admin-Panel**, nur im WOMS-Look. Keine neuen Funktionen. Mails an
Kunden und Stripe-Buchungen bleiben exakt wie heute (gleiche Server-Logik).
### Chat (Messenger `/customers/chat` und Kunden-Tab „Chat“)
- Links: Gesprächsliste (ungelesene oben, letzte Nachricht, Zeit) und Feld „Neue Nachricht an Kunde…“
(Kundenauswahl). Rechts: Gespräch. Im Kunden-Tab steckt dasselbe Gespräch ohne Liste.
- Anhänge wie heute: Bild, Kamerafoto, Datei, Sprachnachricht; Lightbox, Audio-Player, Download.
- Eingabezeile und Sprechblasen: WOMS lädt **`chat-composer.js` und `chat-composer.css` zur Laufzeit von
project.webklar.com** (eine Quelle, kein Auseinanderlaufen) und bettet `ChatComposer.create(...)` /
`ChatComposer.renderMessage(...)` in eine React-Komponente ein. Farben gleicht eine kleine WOMS-CSS an.
Der Composer bekommt eine neue Option für Anfrage-Header (Bearer-JWT beim Hochladen und Entfernen); ohne
die Option verhält er sich wie bisher (Kundenseite unverändert).
- Nach dem Senden zeigt WOMS das Ergebnis des Mail-Hinweises an den Kunden (K17) wie heute im Panel.
- Neue Nachrichten im offenen Gespräch: Abfrage alle 15 s (wie das Panel heute, nur bei sichtbarem Tab);
gelesene Kundennachrichten werden wie heute als gelesen markiert.
### Gutschriften (Sammelliste und Kunden-Tab)
- Filter „Offen“ / „Alle“; im Kunden-Tab nur die Anträge dieses Kunden.
- Detail wie heute: Antrag, Nachweise (öffnen), automatische Prüfung, Rechner (Rechnungsbetrag, Rechnung gilt
für, Bezahlt bis, Übernahme ab), weitere Anträge des Kunden, Verlauf, Stripe-Guthaben.
- Aktionen wie heute: „Genehmigen & gutschreiben“, „Rückfrage senden“, „Ablehnen“, „Jetzt in Stripe
gutschreiben“, „In Stripe öffnen“.
### Abo & Angebote (Sammelliste „Abo-Angebote“ und Kunden-Tab)
- Kunden-Tab oben: **Abo-Status** (Abo, Leistungsbeginn, nächste Abbuchung, gekündigt zum, Speicher), sichtbar
für alle Mitarbeiter. Darunter die Angebote dieses Kunden und „Neues Angebot“ (Kunde vorbelegt).
- Sammelliste: Filter „Offen & aktiv“ / „Alle“, „Neues Angebot“ mit Kundenauswahl, Zurückziehen,
Mail erneut senden. Coupon in Stripe wie heute.
### E-Mail-Speicher (nur Sammelliste)
- Gesamtübersicht, fällige konsoleH-Aufgaben mit Anleitung und Knopf „Erledigt“ direkt in der Zeile,
gescheiterte Änderungen der letzten 30 Tage.
### Funktionen (Kunden-Tab)
- Pro Projekt News an/aus, wie heute im Kunden-Tab des Panels.
### Protokoll (Kunden-Tab)
- Einträge der Kundenansicht dieses Kunden, neueste zuerst: Zeit, Mitarbeiter, Aktion, Detail.
### Ersatzlos weg
Übersicht (Statistik, letzte Logins), Projekt- und Kundentabelle des Panels, Invoice-Ninja-Rechnungsliste
(`GET /customers/:id/invoices`), Kunden-Aktivität (`/customers/:id/activity`, im WOMS-Tab „Tickets“ enthalten),
Kundennummer-Bearbeitung des Panels (Stammdaten bearbeitet WOMS schon selbst).
## 3. Portal: Kundenansicht und Protokoll
### Öffnen
1. WOMS öffnet synchron einen leeren Tab (gegen Pop-up-Sperren), ruft
`POST /api/team/customers/:id/kundenansicht` (Bearer-JWT) und setzt danach die Adresse des Tabs.
2. Der Server prüft: Kunde existiert und hat Portal-Zugang. Er erzeugt einen **Einmal-Link**
(`staffPreviewTokens`, neuer Zweck `kundenansicht`, 60 s, einmal einlösbar) und antwortet mit
`{ openUrl: https://project.webklar.com/api/auth/kundenansicht?token=… }`.
3. `GET /api/auth/kundenansicht` löst das Token ein, setzt die Portal-Sitzung
`{ role:'admin', viewAs:true, viewId, viewExpiresAt, appwriteUserId, adminName, adminEmail, customerId, name, email }`
(`viewId` = zufällige Kennung, `viewExpiresAt` = jetzt + 2 h, Cookie-Laufzeit ebenfalls 2 h) und ein
**Preview-Cookie für genau diesen Kunden** (Rolle Kunde, 2 h), damit Previews aus der Kundenansicht
funktionieren. Protokoll: `geoeffnet`. Weiter auf `/dashboard.html`.
4. Abgelaufenes oder verbrauchtes Token: Hinweisseite „Link abgelaufen, bitte im Ticketsystem erneut öffnen“.
### Banner und Ende
- Banner oben: „Kundenansicht: *Kunde* · geöffnet von *Mitarbeiter* · Beenden“.
- „Beenden“: `POST /api/auth/kundenansicht/ende` → Protokoll `beendet`, Portal- und Preview-Cookie löschen,
Weiterleitung auf `https://ticket.webklar.com/customers/<id>` (Basis aus neuer Variable `TICKET_BASE_URL`).
- Nach 2 h: jede Anfrage antwortet `401 { code:'view_expired' }`, Cookies werden gelöscht, Protokoll
`abgelaufen`; das Dashboard zeigt „Kundenansicht abgelaufen – im Ticketsystem erneut öffnen“.
### Schutz vor Verwechslung (ein Browser = eine Portal-Sitzung)
- `/api/auth/me` liefert in der Kundenansicht `viewId`. Alle API-Aufrufe des Dashboards (`app.js`,
`funktionen.js`, `analyse-view.js`, Composer) senden den Header `X-Kundenansicht: <viewId>`.
- Server-Middleware vor allen Kunden-Routen: In einer Kundenansicht mit abweichender Kennung → `409
{ code:'view_replaced', kunde:<Name> }`. Schreibende Anfragen **ohne** Kennung → ebenfalls 409.
- Das Dashboard rendert bei 409 nichts Neues, sondern zeigt nur: „Diese Ansicht wurde durch die
Kundenansicht von *B* ersetzt. Tab schließen.“
### Was erlaubt ist (fail-closed)
- Neue zentrale Middleware: In einer Kundenansicht ist **jede nicht-lesende Anfrage gesperrt** (403
„In der Kundenansicht nicht möglich“), außer einer ausdrücklichen Erlaubnisliste:
- News: anlegen, Bild hochladen, ändern, löschen (`/api/news` POST/PATCH/DELETE; `rejectViewAs` entfällt dort).
- `POST /api/funktionen/analyse/heatmap-link` (nur ein Ansichts-Link, kostet nichts).
- Ende der Kundenansicht und Abmelden.
- Damit bleiben gesperrt: Abo bestellen/verwalten/wechseln, Speicher, News kaufen, Analyse an/aus (AVV-Zustimmung),
Passwort, Chat senden/Anhänge, Gutschrift beantragen/antworten – und **jede künftige Schreib-Route**, bis sie
bewusst auf die Liste kommt. Die bestehenden Einzelsperren bleiben als zweite Sicherung stehen.
- `POST /api/chat/read` fällt unter die Sperre: Die Kundenansicht markiert Team-Nachrichten **nicht** mehr als
„vom Kunden gelesen“ (Korrektur).
- Lesende Anfragen mit Nebenwirkung, die bleiben dürfen: Abgleich offener Wechsel (`GET /api/abo`),
Freischalten fertiger Funktionen und Rechnungs-Links (`GET /api/funktionen`) – reine Synchronisierung.
- Das Dashboard blendet gesperrte Bedienelemente in der Kundenansicht weiter aus bzw. deaktiviert sie (wie heute).
### Protokoll
- Neue Appwrite-Collection **`customerViewLog`**: `customerId` (string 64, Pflicht), `staffUserId` (string 64),
`staffName` (string 128), `action` (string 32), `detail` (string 300); Index `customerId` + `$createdAt`.
Nur der Server-Schlüssel schreibt/liest (keine Client-Rechte).
- Aktionen: `geoeffnet`, `beendet`, `abgelaufen`, `news_angelegt`, `news_bild`, `news_geaendert`,
`news_geloescht` (Detail: Titel bzw. Anfang des Textes).
- Geschrieben **vom Server** im Anschluss an die erfolgreiche Aktion; schlägt das Schreiben fehl, wird das
geloggt (Server-Log), die Aktion selbst bleibt gültig.
- Löschen: täglicher Aufräumlauf im Portal-Server, Einträge älter als 12 Monate.
- Anlegen: Setup-Skript mit dem Server-Schlüssel; fehlt dem Schlüssel das Recht, legt der Nutzer die Collection
nach Vorgabe in der Appwrite-Konsole an.
### Datenschutzerklärung (Portal, Abschnitt 6)
Der Satz „Um Sie zu unterstützen, können unsere Mitarbeiter Ihr Portal so ansehen, wie Sie es sehen. Eine Nachricht
in Ihrem Namen senden können sie dabei nicht.“ wird ersetzt durch:
> Um Sie zu unterstützen, können unsere Mitarbeiter Ihr Portal so öffnen, wie Sie es sehen, und dabei für Sie
> News-Beiträge anlegen, ändern oder löschen. Bestellungen, Zahlungen, Änderungen an Ihrem Passwort sowie
> Nachrichten oder Anträge in Ihrem Namen sind dabei ausgeschlossen. Jedes Öffnen und jede Änderung halten wir
> mit Mitarbeiter und Zeitpunkt fest und löschen diese Einträge nach 12 Monaten. Rechtsgrundlage dafür ist
> Art. 6 Abs. 1 lit. f DSGVO; unser berechtigtes Interesse ist die Nachvollziehbarkeit von Zugriffen auf Ihr Konto.
„Stand“ aktualisieren, PDF-Fassung `docs/WEBklar-Datenschutzerklaerung.pdf` neu erzeugen (wie in d4241d2).
Geht mit Etappe 1 live.
## 4. Portal: Schnittstelle für das Ticketsystem und Abschaltung
### Neuer Eingang `/api/team/*`
- Middleware `requireStaffJwt`: prüft `Authorization: Bearer <Appwrite-JWT>` über Appwrite `/account` (wie
`requireAppwriteStaff`), dann `resolveStaffAccount` (Eintrag in `employees` **oder** Label `admin`); sonst 403.
Sie stellt die Mitarbeiter-Identität in derselben Form bereit wie die bisherige Admin-Sitzung
(`appwriteUserId`, `name`, `email`, `isAdmin`), damit die bestehenden Handler unverändert bleiben.
Die 2FA ist damit abgedeckt: Appwrite beantwortet `/account` ohne abgeschlossenen zweiten Faktor mit 401
(`user_more_factors_required`), die Middleware lässt dann nichts durch.
- Routen (bestehende Logik, neu eingehängt; während Etappe 1–3 parallel zu `/api/portal-admin/*`):
| Neu unter `/api/team` | Herkunft |
|---|---|
| `GET /chats`, `GET /chats/unread-count`, `GET/POST /chats/:id/messages`, `POST /chats/:id/attachments`, `GET/DELETE /chats/:id/attachments/:att`, `POST /chats/:id/read` | `portalAdmin.js` (Chat-Teil wird eigenes Modul) |
| `/takeover-credits/*` (+ Filter `?customerId=`) | `takeoverAdminRouter` |
| `/offers/*` (+ Filter `?customerId=`) | `offersAdminRouter` |
| `/storage-tasks/*` + neu `GET /storage-tasks/open-count` (ohne Stripe-Abgleich, für die Sidebar-Zahl) | `storageTasksAdminRouter` |
| `/funktionen/:customerId[/news]` | `funktionenAdmin` |
| `GET /customers/:id/abo` | Logik von `/api/admin/customers/:id/abo`, jetzt für alle Mitarbeiter |
| `GET /customers/:id/portal` → `{ hasAccess, lastLoginAt }` | neu (`customerPortalAccess`) |
| `POST /customers/:id/kundenansicht` | neu (ersetzt `view-as`) |
| `GET /customers/:id/protokoll` | neu |
| `POST /preview-link` | Logik von `/api/admin/preview-open`, jetzt für alle Mitarbeiter |
- **Dateien ohne Header** (Bilder, Sprachnachrichten, Downloads im Chat; Nachweise bei Gutschriften): Die
Antworten enthalten **signierte Links** (HMAC mit dem Server-Geheimnis, gebunden an Kunde + Datei, 15 min
gültig). Die Datei-Routen akzeptieren Bearer-JWT **oder** gültige Signatur.
- CORS: erlaubte Header zusätzlich `X-File-Name`, `X-Attachment-Kind`, `X-Duration-Ms`, `X-Image-Width`,
`X-Image-Height` (Chat-Upload). Herkunft weiter nur `https://ticket.webklar.com` (+ localhost).
- WOMS-Seite: ein gemeinsamer Helfer `teamRequest` nach dem Muster von `src/lib/financeRequest.js`
(JWT-Cache, einmal neu versuchen bei 401, deutsche Fehlertexte).
### Previews aus dem Ticketsystem
- Die Preview-Knöpfe im WOMS (`PreviewLinkButton`) sind heute einfache Links und funktionieren nur dank der
Mitarbeiter-Anmeldung im Portal. Sie holen sich ab Etappe 1 einen Einmal-Link über `POST /api/team/preview-link`
(leerer Tab synchron, Adresse danach setzen).
### Was in Etappe 4 wegfällt
- `public/admin.html`, `public/admin.js` (und nur dort genutzte Styles), `public/kalkulation.{html,js,css}`.
- `/api/portal-admin/*` komplett, darin auch: overview, projects, customers (Liste, PATCH, activity, invoices),
appointments, `customers/:id/appointment`, `GET customers/:id`, `view-as`, `exit-view-as`.
- `/api/admin/switch-link`, `GET /api/auth/staff-switch`, Zweck `switch` in `staffPreviewTokens`,
`/api/admin/preview-open` (ersetzt durch `/api/team/preview-link`). Die Einlöse-Route
`GET /api/auth/staff-preview` **bleibt**: sie löst auch die Links von `/api/team/preview-link` ein.
- Mitarbeiter-Login: `POST /api/auth/login` erkennt Mitarbeiter wie heute (`resolveStaffAccount`), beendet die
gerade erzeugte Appwrite-Sitzung und antwortet `403 { error:'Mitarbeiter arbeiten im Ticketsystem.', ticketUrl }`;
die Login-Seite zeigt den Hinweis mit Link. `/api/auth/login/mfa`, `/login/mfa/resend`, `staffMfa` und
`startStaffPortalSession` entfallen. Kundenlogin unverändert.
- Alte Mitarbeiter-Sitzungen (Rolle admin ohne Kundenansicht): Dashboard und Login leiten ins Ticketsystem.
- WOMS (vorher live): Knopf „Projekt-Admin“ und `openProjectAdmin()` entfernen.
- Team-Mails:
- Fußzeilen-Link „Admin-Bereich“ in 15 Vorlagen → „Ticketsystem“, `https://ticket.webklar.com/customers`.
- T01 „Neue Chatnachricht“: Knopf „Im Ticketsystem antworten →“ → `/customers/chat?kunde=<customerId>`
(neuer Platzhalter).
- T10/T11: `admin_link` → `/customers/<customerId>?tab=gutschriften&antrag=<id>`.
- Code-Kommentare/Texte, die „Admin-Panel“ nennen, werden angepasst.
## 5. Etappen und Livegang
Jede Etappe ist für sich livefähig; das Admin-Panel läuft bis Etappe 4 weiter.
| Etappe | Inhalt |
|---|---|
| **1 Fundament + Kundenansicht** | `/api/team`-Eingang (Middleware, CORS, `teamRequest`), Kundenansicht (Einmal-Link, Sitzung 2 h, Banner, Verwechslungsschutz, fail-closed-Sperre, News frei, Chat-Lesestatus), Protokoll (Collection, Schreiben, Aufräumen, Tab), Datenschutz-Satz, Previews per Einmal-Link, WOMS: Tab in der Adresse, „Übersicht“, Knopf „Kundenansicht öffnen“, Letzter Login, Abo-Status-Route. |
| **2 Chat** | Chat-Routen unter `/api/team`, signierte Datei-Links, Composer-Option für Header, WOMS-Messenger, Kunden-Tab, Sidebar mit Unterpunkten und Chat-Zahl. |
| **3 Gutschriften, Abo & Angebote, E-Mail-Speicher, Funktionen** | Routen unter `/api/team` (+ Kundenfilter, `open-count`), signierte Nachweis-Links, Sammellisten und Kunden-Tabs, restliche Sidebar-Zahlen. |
| **4 Abschalten** | WOMS: „Projekt-Admin“ entfernen (zuerst live). Portal: Admin-Panel, Rechnungsanalyse, `/api/portal-admin`, Switch/Preview-open-Altlasten, Mitarbeiter-Login sperren, Mail-Links. |
- Reihenfolge je Etappe: **Portal zuerst** (abwärtskompatibel), dann WOMS. Ausnahme Etappe 4: WOMS zuerst.
- Pushen macht der Nutzer (Portal `…:main`, WOMS `…:test`; WOMS deployt jeden gepushten Branch, also nie
Feature-Branches pushen). Danach prüfe ich den Live-Stand lesend.
- Arbeitsorte: Portal-Worktree `dev-admin-umzug` (Branch `feature/admin-umzug`), WOMS-Worktree gleichen Namens.
## 6. Tests
- **Portal:** bestehende Unit-Tests (`node --test`) plus neue:
- `requireStaffJwt`: ohne JWT 401, ungültig 401, Nicht-Mitarbeiter 403, Mitarbeiter ohne Label `admin` erlaubt.
- Kundenansicht: Token einmalig und 60 s; Sitzung endet nach 2 h; **jede** Schreib-Route außerhalb der
Erlaubnisliste liefert 403 (Test läuft über die registrierten Express-Routen, damit neue Routen automatisch
erfasst werden); News-Aktionen erlaubt und protokolliert; `POST /api/chat/read` gesperrt.
- Verwechslung: Tab-Kennung A nach Wechsel zu B → 409, keine Daten von B.
- Signierte Links: falscher Kunde, falsche Datei, abgelaufen → 403.
- Aufräumlauf löscht nur Einträge > 12 Monate.
- Abgeschottete Testumgebung (Appwrite-Nachbau, Stripe-Attrappe, Mail-Falle) für Gutschrift/Angebot über
`/api/team` mit Bearer-JWT.
- **WOMS:** Unit-Tests für reine Logik (`customerTabs` mit neuen Tabs/URL, Sidebar-Kinder, signierte-Link-Helfer,
`teamRequest`); Playwright mit nachgebauten Schnittstellen unter Speicherbremse (`systemd-run … MemoryMax`).
- **Nach jedem Livegang:** lesender Live-Check (Routen antworten 401 ohne JWT, CORS-Preflight 204, ausgelieferte
Bundles enthalten die neuen Teile). Keine echten Kundendaten verändern; Testdaten klar benannt und danach löschen.
## Fehlerbehandlung
- JWT abgelaufen → `teamRequest` holt ein neues und versucht einmal erneut; danach Fehlermeldung.
- Portal nicht erreichbar → jede Ansicht zeigt eine Fehlerzeile mit „Erneut versuchen“; Sidebar-Zahlen behalten
den letzten Stand.
- Kunde ohne Portal-Zugang → Knopf ausgegraut; Server antwortet zusätzlich 409 „Kein Portal-Zugang“.
## Nicht Teil dieser Spec
- Rechnungsanalyse/Kalkulation im WOMS (E4, später eigenes Vorhaben).
- Neue Funktionen in Chat, Gutschriften, Angeboten oder E-Mail-Speicher.
- Änderungen an Kunden-Logins, Kunden-Mails (K01–K19) oder an der WOMS-2FA.