Änderungsprotokoll
Was ist neu in SW deck
Fixed — e2e-Suite erstmals grün (2026-07-25)
`data.message || t.…` steht an 15 Stellen; die englische API-Meldung ist
immer gesetzt und gewinnt deshalb ausnahmslos — der deutsche Text war
überall toter Code. Im **Kundenportal** stand dadurch „Project not found"
statt der deutschen Meldung. Behoben an den drei durch Tests belegten
Stellen: Portal-Projektdetail, Login (Better Auth liefert „Invalid email or
password"; fehlgeschlagene Anmeldungen werden bewusst nicht weiter
aufgeschlüsselt) und Reset-Password (lokalisiert jetzt über den stabilen
`code`, nicht über den Meldungstext). Die übrigen 12 Fundstellen stehen im
Backlog #7.2.
das Endlos-Skelett ersetzt hat. Ein Fix hat den nächsten Fehler freigelegt.
Changed — Abnahmelauf nach CI verlagert
Der e2e-Volllauf läuft nicht mehr lokal. `test.yml` triggert jetzt zusätzlich
per `workflow_dispatch`; dort laufen frische Postgres, frische Server, keine
parallelen Edits — und `STORAGE_PROVIDER: local`, der Production-R2-Bucket
wird also nicht angefasst.
Der Grund ist gemessen, nicht vermutet. Gleicher Code, drei Umgebungen:
| Lauf | failed | passed | Laufzeit |
|---|---|---|---|
| Lokal, mit Edits während des Laufs | 139 | 302 | 44,3 min |
| Lokal, ohne Edits | 56 | 385 | 28,7 min |
| CI | 16 | 425 | 27,4 min |
40 der 56 lokalen Failures kamen von Serverneustarts (`nest --watch` nach
einem Edit um 21:47:46) und einem Next-Worker-Crash bei erschöpftem Swap
(22:06:14, Swap auf 2,6 von 32 GB) — nicht vom Code. Die Suite läuft mit
`workers: 1` über eine halbe Stunde gegen genau eine API- und eine
Web-Instanz; jeder Neustart trifft alles, was gerade in Flight ist, und das
Symptom ist nie „Server weg", sondern ein fehlender String oder ein nackter
Timeout.
**Endstand: 432 passed, 24 skipped, 1 flaky, 4,1 min.**
Fixed — 16 echte e2e-Failures abgearbeitet
Telemetrie-Consent-Banner prüften, was `features.ts` bewusst ausblendet
(Task 5). Neu: `e2e/feature-flags.ts` liest die Flags aus der Quelle, damit
der Skip automatisch verschwindet, wenn ein Flag zurückgedreht wird — und
laut scheitert, wenn eines umbenannt wird, statt Tests still verschwinden zu
lassen. Der Telemetrie-**Negativ**test kam in denselben Skip: bei
ausgeschaltetem Flag war er grün, ohne irgendetwas zu prüfen.
`/^post$/i` → `/^senden$/i`, `title="View as customer"` →
`"Als Kunde ansehen"`, Such-Placeholder u. a.
`${testIdPrefix}-${file.id}`, das Dashboard übergibt `"file"` — der Test
suchte `file-row-`, einen Präfix, den es nie gab.
der Kachel; `MaterialTiles` rendert `description` gar nicht. Die
API-Assertion zwei Zeilen darunter prüfte dasselbe bereits korrekt.
Admin-Liste und öffnete sie im Portal, wo der Nutzer kein Kunde ist. Jetzt
über `/projects/mine`.
Nicht angefasst: die englischen Selektoren in `invoices`, `documents`,
`notes`, `tasks`, `team`, `client-profiles`. Sie sind grün, weil die
Admin-Oberfläche gemischt bleiben darf — eine Sammelersetzung hätte dort
funktionierende Tests zerstört.
Fixed — Nachrunde zum Production-Review 2026-07-25
Drei Fehlschläge der e2e-Suite waren keine Test-, sondern Produktfehler:
Route band `@Query() pagination: PaginationQueryDto` und daneben ein
separates `@Query("category")`. Die globale `ValidationPipe` läuft mit
`forbidNonWhitelisted: true` und validiert den **kompletten** Query-String
gegen die eine gebundene DTO — `?category=…` starb mit `property category
should not exist`, bevor der Handler lief. Der Kategoriefilter aus Task 07
war damit für jeden Aufrufer tot; unentdeckt, weil die Web-UI Materialien
clientseitig filtert und den Parameter nie sendet. Behoben über
`ListProjectFilesQueryDto extends PaginationQueryDto`. Neuer
Architektur-Test `common/dto/query-binding.spec.ts` verhindert das Muster
künftig (gegen den echten Defekt rot verifiziert, nicht nur grün angenommen).
`if (!project) return <ProjectDetailSkeleton />` stand **vor** dem
Fehler-Banner, das dadurch unerreichbar war. Ein Kunde, der ein Projekt ohne
Zugriffsrecht öffnete, sah dauerhaft graue Platzhalter — keine Meldung, kein
Rückweg. Jetzt eigener Fehlerzweig mit Meldung und Link zurück.
Deep-Link seit jeher, das Portal nicht — `/portal/projects/x?tab=invoices`
landete still auf Updates. Angeglichen.
`htmlFor`/`id`; die `<label>` waren Geschwister der Inputs statt mit ihnen
verknüpft, für Screenreader also unbeschriftet. Verdrahtet.
Sammelersetzung — eine frühere Sammelersetzung hatte API-Pfade wie
`/api/auth/organization/invite-member` mitübersetzt. Projekt-Tabs laufen
jetzt über `data-testid="project-tab-<id>"` statt über Labels und überstehen
damit weitere Übersetzungsrunden. `no-translated-paths.e2e.ts` prüft jeden
`/api/<segment>` der Specs gegen die real registrierten `@Controller`-Präfixe.
Fixed — Production-Review 2026-07-25
19 Spalten der Migration `20260709212452_task02_de_invoice_fields`, obwohl
`_prisma_migrations` sie als angewandt führte (`prisma migrate resolve
--applied` auf eine echte Migration). Jede `systemSettings`- und
`invoice`-Query lief in einen Prisma-P2022 — damit waren in Production
Rechnungen, Settings **und der gesamte Mailversand** tot
(`getEffectiveEmailConfig` hängt an jedem `mail.send()`). Nachweis, Behebung
und Konsequenzen:
`docs/decisions/2026-07-25-schema-drift-migrate-resolve.md`.
`migrate deploy` ein `prisma migrate diff --exit-code` gegen die laufende DB;
`/api/status` meldet `schemaDrift` (`clean|drift|unknown`, fehlender Marker =
`unknown`, nie `clean`); `deploy.yml` bricht rot ab bei Commit-Mismatch,
gestopptem Container oder Drift.
sie nur per `@UseGuards` an einzelnen Controllern — Authentifizierung war
opt-in, ein vergessener Decorator machte eine Route stillschweigend öffentlich
(so geschehen in `7b50153`). `/api/health` hat dabei das fehlende `@Public()`
bekommen, das sonst jeden Deploy-Healthcheck auf 401 laufen ließe. Neuer
Architektur-Test `common/guards/public-routes.spec.ts` friert den Satz
öffentlicher Routen ein.
aus) mit automatischem Bootstrap-Fenster, solange keine Organisation
existiert — eine frische Instanz kann ihre erste Org weiter über die normale
UI anlegen, danach schließt sich das Fenster von selbst. UI blendet
Signup-Formular und -Links entsprechend aus.
`130,00 €` ausgegeben (hartkodiertes Dollarzeichen), Datum als
`December 31, 2026`, und der „Rechnung ansehen"-Button verlinkte auf
`/portal/invoices` — eine Route, die es nicht gab (404). Währung und Datum
laufen jetzt über den gemeinsamen Helper `common/utils/format-de.ts`, den
auch die PDF-Erzeugung nutzt.
Mailversand lösen den Empfänger beide über `ProjectClient` auf — eine
Rechnung ohne Projekt galt als „versendet", war für den Kunden aber weder
sichtbar noch wurde er benachrichtigt. Der Übergang `draft→sent` wird jetzt
mit klarer Meldung verweigert; ein Projekt ohne zugewiesene Kunden wird
geloggt statt verschluckt.
Organisation entfernte alle `Invoice`-Zeilen — entgegen § 147 AO (10 Jahre),
die der Löschpflicht aus Art. 17 DSGVO vorgeht (Art. 17 Abs. 3 lit. b).
Löschung wird bei ausgestellten Rechnungen abgelehnt; Entwürfe und Stornos
zählen nicht mit.
laufenden Betrieb dateiweise (zerrissener Snapshot). Neuer
`before_backup`-Hook `scripts/ops/deck-db-dump.sh` legt einen konsistenten
`pg_dump -Fc` ab; per Restore in eine Wegwerf-DB verifiziert (43 Tabellen).
Restore-Anleitung in `DEPLOY.md` → *Backup & Restore*.
Added — Production-Review 2026-07-25
`/agb` als öffentliche Routen, `LegalFooter` in Landing-, Auth- und
Portal-Layout. Inhalte sind ausgewiesene Platzhalter mit TODO-Markern für die
Pflichtangaben — bewusst kein erfundener Rechtstext. 8 e2e-Tests.
plus Nav-Eintrag. Löst zugleich den toten Link aus der Rechnungs-E-Mail.
4 e2e-Tests.
bereits konfiguriert, es gab nur keinen Einstieg — besonders relevant für
Kunden ohne gesetztes Passwort.
verschickt jetzt beim Übergang `sent→overdue` genau einmal eine neutral
formulierte Erinnerung (bewusst keine Mahnung mit Verzugsfolgen).
Einladungsmail — der allererste Kundenkontakt. Gemeinsames Layout
(`packages/email/src/components/shell.tsx`) statt in jedem Template
wiederholtem Markup.
Datumsformaten (vorher durchgehend `en-US`) — das ist die Beweisdokumentation
der E-Signatur.
Added
Architektur nach ADR Option D (Getrennte Hoheit): Stripe steuert Zyklus +
SEPA-Lastschrift-Zahlung + Dunning nativ (kein eigener Cron, kein
Single-Instance-Lock); das Portal erzeugt jedes rechtsgültige
Rechnungsdokument (§14 UStG) als Spiegelung des Stripe-Events.
- **Neues Modell `ClientSubscription`** (bewusst getrennt von Atriums
SaaS-`Subscription`, Namens-Clash vermieden) + Migration
`20260725024144_task04_client_subscription`: clientId/projectId,
stripeCustomerId/SubscriptionId/PriceId, status, amount/currency/interval,
currentPeriodStart/End, cancelAtPeriodEnd, mandateReference.
- **Neues Modell `StripeEvent`** für Webhook-Idempotenz: gleicher
Stripe-Event (`evt_…`) wird genau einmal verarbeitet — Webhook-Repeat
≠ Doppel-Rechnung.
- **`subscriptions.service`**: Admin-Checkout (Stripe subscription mode,
SEPA priorisiert), SEPA-Mandat-Setup (setup mode, an E-Sign-Vertrags-
abschluss gekoppelt), Customer-Portal-Deeplink, Webhook-Handler
(`invoice.paid`/`finalized`/`payment_failed`, `customer.subscription.
created/updated/deleted`) idempotent, Spiegelungsrechnung via
`invoices.service.createMirrorInvoice` (RE-JJJJ-NNNN-Nummernkreis,
DE-Ausstellerdaten aus SystemSettings).
- **Webhook-Dispatch**: `payments.service.handleWebhookEvent` delegiert
Recurring-Events an `SubscriptionsService`; einmalige Rechnungs-Checkouts
bleiben im PaymentService (Sauber getrennt via metadata).
- **Portal-Ansicht** read-only: Plan, Betrag, Status, nächste Abbuchung +
"Verwalten"-Button → Stripe Customer Portal (Zahlungsmethode ändern /
kündigen, Stripe-gehostet).
- **Admin-UI**: Subscriptions-Tab im Projekt-Detail (Dashboard), Anlegen-
Modal (Stripe-Price-ID + Client), Kündigen.
- **Kalender**: `subscription_renewal`-Event aus
`ClientSubscription.currentPeriodEnd` (Admin + Portal, scoped via
`assertProjectAccess`/ProjectClient).
- **Feature-Flag** `features.recurringBilling` (Standard: an).
- **17 Unit-Tests** (Idempotenz, Spiegelung, Portal-Isolation,
Subscription-Sync, SEPA-Setup, Customer-Portal, Cancel) + **5/7 e2e**
(2 skipped bei leerer Test-DB). 726 unit pass, build grün, bestehende
calendar/invoices-e2e ohne Regression. Siehe
[Task 04](docs/deck/tasks/04-recurring-billing.md). _(Implementiert
2026-07-25.)_
Der Tresor-Tab im Kundenportal ist ein reiner Verweis auf zwei externe
Dienste — es werden **keine Credentials in der Portal-DB gespeichert**
(Architektur-Regel: Zero-Knowledge-Krypto nicht selbst bauen; gespeicherte
Kunden-Zugänge = max. Haftung + Breach-Ziel).
- **Neue Felder `SystemSettings.vaultUrl` +
`SystemSettings.oneTimeSecretUrl`** (Plaintext-URL-Pointer, keine
Secrets, nicht verschlüsselt) + Migration
`20260725103009_task08_vault_url` (beide Spalten). NULL = jeweilige
Sektion im Portal-Tresor-Tab ausgeblendet.
- **`GET /api/settings/vault-url`** (alle authentifizierten User inkl.
Portal-Kunden, keine `@Roles`-Einschränkung — analog
`payment-instructions`). Liefert `{ vaultUrl, oneTimeSecretUrl }`.
`PUT /settings` akzeptiert beide (URL-validiert mit `require_protocol`,
null = clear).
- **Portal-Layout:** Tresor-Nav-Link erscheint **nur**, wenn mind. eine
URL konfiguriert ist (Vaultwarden ODER OneTimeSecret).
- **`/portal/tresor`:** zwei Karten:
1. **Vaultwarden** — dauerhafter geteilter Tresor (Org-pro-Kunde):
"Tresor öffnen"-Link (`target=_blank`) + Anleitung (eigener
Org-Schlüssel, E2E-Verschlüsselung, 2FA).
2. **OneTimeSecret** — ephmere einmalige Credential-Übergabe (Initial-
Passwort, ad-hoc-Token): Link zum OneTimeSecret-Service +
Anleitung (Secret nach einmaligem Lesen permanent vernichtet).
Beide mit Privacy-Hinweis ("Portal speichert keine Zugangsdaten") +
je "Noch nicht eingerichtet"-State wenn URL leer.
- **Admin-Workspace-Settings:** Tresor-URL-Konfig-Sektion
(`vault-section.tsx`) mit beiden URL-Feldern in einem Formular.
- **i18n:** `t.nav.tresor`, `t.portal.tresor.*` (inkl.
`oneTimeSecret*`), `t.dashboard.settings.vault.*` (inkl.
`oneTimeSecret*`).
- **16 Unit-Tests** (8 service: getVaultUrls null/configured/cleared für
beide URLs, plaintext storage; 8 dto: URL-Validierung für beide Felder)
+ **19/19 e2e** (`task08-tresor.e2e.ts`: beide URLs, Tab-Sichtbarkeit
iff mind. eine gesetzt, Karten + Links, Not-configured-States pro
Dienst, Anleitungen). Build grün, bestehende
ballast-ausblenden-e2e ohne Regression.
- **Doku:** [vaultwarden-org-pro-kunde.md](docs/deck/vaultwarden-org-pro-kunde.md)
— Org-pro-Kunde-Prinzip, Onboarding-Runbook, **kompletter
Kunden-Workflow** (Onboarding via OneTimeSecret → Dauergebrauch via
Vaultwarden → Ad-hoc-Secrets), OneTimeSecret (Open Source,
self-hostbar, EU-Region), Self-Host-vs-Hosted-Vergleich,
Härtungs-Checkliste (Instanz, Account/2FA, Backup, Monitoring,
Netzwerk). Infra-Umsetzung an Felix delegiert (vor erstem Kunden mit
Tresor). Siehe
[Task 08](docs/deck/tasks/08-secret-sharing.md). _(Implementiert
2026-07-25.)_
Fixed
gegen Production gefahren.** Der automatische `Deploy`-Workflow
(`deploy.yml`) schlug nach Merge von Task 03 (`c021b0d`) und Task 07
(`e6a0f13`) fehl, sodass die Migrationen `20260724083729_task03_calendar_entry`
und `20260724180303_task07_file_pinned_category` nicht auf Production
angewandt wurden (Production lief weiter auf `c517791`, vor Task 03). Die
CI-Phase `Build & unit test` (inkl. `db:migrate:deploy` gegen die CI-Postgres)
war in beiden Läufen grün — der Fehler lag im `Deploy to Hetzner`-Job.
Zwei server-seitige Ursachen behoben: (1) 44 Dateien unter `/opt/deck-src`
gehörten `root:root` (darunter `packages/database/prisma/migrations/`),
sodass der `deploy`-User beim `git checkout` keine neuen
Migrations-Verzeichnisse anlegen konnte — `chown -R deploy:deploy`
durchgeführt. (2) Der lokale `main`-Branch auf vps2 hinkte hinter
`origin/main` hinterher; `deploy.sh` checkte den stale Branch aus —
`deploy.sh` gepatcht, bei `REF=main` auf `origin/main` zu fast-forwarden
+ Guard gegen stale Deploys. Post-Mortem:
`docs/decisions/2026-07-24-deploy-incident-migrations-not-applied.md`;
Runbook-Ergänzung in `DEPLOY.md` (Troubleshooting). Beide Fixes sind
server-seitig (nicht im Repo). Verifikation: alle 4 Migrationen in
`_prisma_migrations` applied, `/api/status` reportet `e6a0f13`, Auto-Deploy
re-run grün.
Added
- Schema: `File.pinned Boolean @default(false)` + `File.category String?`
(Migration `20260724180303_task07_file_pinned_category`).
- API: `PATCH /files/:id/pin` Pin-Toggle-Endpoint (beidseitig — Admin und
Kunde über `assertProjectAccess`, kein `@Roles`-Lock); `category`-Filter
in `GET /files/project/:projectId?category=...` (leere String = "ohne
Kategorie"); `category` in `CreateFileLinkDto` und `UpdateFileDto`;
`findByProject` ordnet gepinnte zuerst, dann neueste.
- UI: geteilte `MaterialTiles`-Komponente (`components/material-tiles.tsx`)
— quadratisches Kachel-Grid, Bild-Thumbnail via `mimeType`, Link-Icon,
Pin-Badge, Kategorie-Filter-Chips, Hover-Actions. Admin und Portal nutzen
dieselbe Komponente (Portal ohne Delete/Edit, aber mit Pin + Upload).
- i18n: `pin`, `unpin`, `pinned`, `category`, `categoryPlaceholder`,
`allCategories` zur `dashboard.files`-Sektion hinzugefügt.
- Tests: 709 Unit-Tests grün (togglePin, category CRUD, findByProject-Filter,
beidseitiger Pin-Access); 6/6 Task07-e2e grün; files-links.e2e unverändert
grün. Pre-existing i18n-Failures in files.e2e.ts (DE-Rebrand, nicht Task 7).
- Prisma-Modell `CalendarEntry` (type/title/date/time/location/link/notes,
scoped zu Project + Organization, onDelete Cascade).
- Shared-Types: `CALENDAR_EVENT_TYPES` um `contract_deadline` + `entry`
erweitert; `CalendarEvent`-Union mit entsprechenden Varianten.
- Backend `calendar.service`: refactored in shared `aggregate()` mit
`projectIds`-Scoping; `contract_deadline` aus `Document.expiresAt`;
`invoice_due` inkl. `paidAt` im Select (Status zeigt "bezahlt");
`CalendarEntry`-CRUD (create/update/delete, org-scoped).
- Backend `calendar.controller`: `/calendar/mine` Portal-Endpoint (member,
`assertProjectAccess` via `ProjectClient`); `/calendar/entries` CRUD
(owner/admin). Klassen-`@Roles` entfernt zugunsten Method-level.
- Web: `calendar-utils` extrahiert nach `lib/` für Wiederverwendung;
`event-chip` mit `contract_deadline` + `entry` + portal-aware href;
Admin-Kalender mit "Termin hinzufügen"-Modal (`entry-modal.tsx`);
Portal-Kalender `/portal/calendar` (Monatsgrid + Agenda, ohne task-Events).
- i18n: `nav.calendar`, neue Event-Typ-Labels, CRUD-Labels, Portal-Strings.
- Tests: 22 Unit-Tests (CRUD, contract_deadline, Portal-Scoping, Isolation);
7/9 e2e-Tests grün (2 UI-Render-Tests flaky, API-Level alle grün).
- `DEV_BYPASS_AUTH=false` in `.env` (vorbestehend: Preview-Mode blockierte
Signup bei e2e-Tests).
Endpoint zur schnellen Verifikation, was gerade in Produktion laeuft.
Auth via `X-Status-Token`-Header gegen `STATUS_CHECK_TOKEN`-Env (fehlend/falsch → 401).
Antwort: `commitSha` (via Docker build-arg `GIT_SHA` baked), `deployedAt`
(Commit-Timestamp via `GIT_COMMIT_DATE` build-arg, formatiert in
`Europe/Berlin` mit IANA-Timezone-Handling — auto-adjusts CET/CEST, kein
hardcoded Offset), `containerStatus` (`running`/`unhealthy`, reuses die
selbe DB-Ping-Logik wie `/api/health`, nicht reimplementiert). Dev-Fallback
liest `git rev-parse HEAD`/`git show -s --format=%cI HEAD` live. `/api/health`
und der CI deploy-gate unberuehrt. Siehe `apps/api/src/status.controller.ts`.
Changed
Added
Fixed
Added
Fixed
Fixed
Fixed
Added
Added
Changed
Database
Added
Changed
Fixed
Database
Added
Fixed
Added
Security
Added
Changed
Fixed
Upgrade Notes
If you set `BETTER_AUTH_URL` in your `.env`, rename it to `API_URL`. The old name still works as a fallback but will be removed in a future release.
Added
Changed
Fixed
Added
Added
Security
Upgrade Notes
New database tables: `comment`, `label`, `project_label`, `task_label`, `file_label`, `member_label`, `notification`, `push_subscription`. New relation columns on `project`, `task`, `file`, and `member`. Docker handles this automatically via `prisma db push` in the entrypoint; bare-metal deployments must run `bun run db:push` after updating.
Added
Mobile-Responsive UI
Testing
Fixed
Security
Breaking Changes
Added
Document Lifecycle
E-Signature Enhancements
Expiration & Reminders
Direct Signing Links
Completion Certificate
Client Choices
UI Improvements
Document Versioning
Direct Signing Links
Email Templates
Testing
Fixed
Upgrade Notes
Schema changes require `bun run db:push`. **Data migration needed**: run `bun run packages/database/scripts/migrate-document-sent-at.ts` to backfill `sentAt` on existing pending documents. New tables: `document_audit_event`, `document_access_token`. New columns on `document` and `signature_field` models.
Added
Fixed
Upgrade Notes
Additive schema changes only (new tables + nullable columns). No data migration needed. Docker entrypoint handles it automatically; manual deployments run `bun run db:push`.
Added
Fixed
Added
Account Deletion
Supabase Row Level Security
Docker
Unraid
Invitations
UX
Security
Changed
Fixed
Added
Billing & Subscriptions
Performance
Fixed
Security
Database
Security
Fixed
Changed
Added
Tasks
Invoicing
Internal Notes
Client Profiles
Email Verification
Notifications
System Settings
Setup Wizard
1. Organization profile (name, logo, colors)
2. Email configuration (None / Resend / SMTP with test send)
3. Create first project
4. Invite first client
5. Completion summary