SW deckAnmelden

Änderungsprotokoll

Was ist neu in SW deck

vUnreleased

Fixed — e2e-Suite erstmals grün (2026-07-25)

Englische Backend-Meldungen in der deutschen Oberfläche.Das Muster

`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.

  • Sichtbar wurde das erst, **weil** der Portal-Fehlerzweig aus derselben Runde
  • 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

    7 × abgeschaltete Features.`time-tracking` und der

    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.

    7 × zurückgebliebene englische Selektoren.`/^clients/i` → `/^kunden/i`,

    `/^post$/i` → `/^senden$/i`, `title="View as customer"` →

    `"Als Kunde ansehen"`, Such-Placeholder u. a.

    1 × falscher Test-ID-Präfix.`MaterialTiles` rendert

    `${testIdPrefix}-${file.id}`, das Dashboard übergibt `"file"` — der Test

    suchte `file-row-`, einen Präfix, den es nie gab.

    1 × unerfüllbare Assertion.Der Edit-Test prüfte die Beschreibung auf

    der Kachel; `MaterialTiles` rendert `description` gar nicht. Die

    API-Assertion zwei Zeilen darunter prüfte dasselbe bereits korrekt.

    1 × falsches Portal-Projekt.Der subscriptions-Test zog die ID aus der

    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:

    `GET /api/files/project/:projectId?category=` lieferte immer 400.Die

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

    Kundenportal zeigte bei fehlgeschlagenem Projekt-Load ein Endlos-Skelett.

    `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.

    Portal ignorierte `?tab=`.Das Dashboard-Projekt unterstützt den

    Deep-Link seit jeher, das Portal nicht — `/portal/projects/x?tab=invoices`

    landete still auf Updates. Angeglichen.

    Kalender-Eintrags-Modal war nicht barrierefrei.Acht Formularfelder ohne

    `htmlFor`/`id`; die `<label>` waren Geschwister der Inputs statt mit ihnen

    verknüpft, für Screenreader also unbeschriftet. Verdrahtet.

    e2e-Assertions an die deutsche UI angeglichen(Task-01-Nachzug), ohne

    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

    KRITISCH: Schema-Drift in Production behoben.In der Prod-DB fehlten alle

    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`.

    Schema-Drift ist jetzt erkennbar.Der Entrypoint fährt nach

    `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.

    `AuthGuard`/`RolesGuard` global als `APP_GUARD` registriert.Vorher hingen

    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.

    Öffentliche Registrierung geschlossen.`PUBLIC_SIGNUP_ENABLED` (Default:

    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.

    Rechnungs-E-Mail an Kunden repariert.Betrag wurde als `$130.00` statt

    `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.

    Stille Zustellungslücke bei Rechnungen ohne Projekt.`findMine` und der

    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.

    Kontolöschung respektiert die Aufbewahrungspflicht.Das Löschen einer

    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.

    Backups sind restore-fähig.borgmatic sicherte das PostgreSQL-Datadir im

    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

    Rechts-Footer + Legal-Seiten (Task 09).`/impressum`, `/datenschutz`,

    `/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.

    `/portal/invoices`.Kundenseitige Rechnungsübersicht über alle Projekte,

    plus Nav-Eintrag. Löst zugleich den toten Link aus der Rechnungs-E-Mail.

    4 e2e-Tests.

    Magic-Link-Login im UI.Das Backend hatte den Flow samt deutscher Mail

    bereits konfiguriert, es gab nur keinen Einstieg — besonders relevant für

    Kunden ohne gesetztes Passwort.

    Zahlungserinnerung.Der Overdue-Cron setzte bisher nur den Status; er

    verschickt jetzt beim Übergang `sent→overdue` genau einmal eine neutral

    formulierte Erinnerung (bewusst keine Mahnung mit Verzugsfolgen).

    Alle E-Mail-Templates auf Deutsch.12 von 14 waren englisch, darunter die

    Einladungsmail — der allererste Kundenkontakt. Gemeinsames Layout

    (`packages/email/src/components/shell.tsx`) statt in jedem Template

    wiederholtem Markup.

    Vertrags-PDF und Abschlusszertifikat auf Deutsch, inklusive

    Datumsformaten (vorher durchgehend `en-US`) — das ist die Beweisdokumentation

    der E-Signatur.

    Added

    Recurring Billing (Task 04) — Dauerschuldverhältnis via Stripe.

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

    Secret Sharing via Vaultwarden + OneTimeSecret / Tresor-Tab (Task 08).

    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

  • **Production-Deploy-Incident 2026-07-24 — Task-03/07-Migrationen nicht
  • 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

    Materialien-Ausbau: Pinning + Kategorie + Kachel-Grid (Task 07).

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

    Kalender: manuelle Termine + Portal-Kalender-Ansicht (Task 03).

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

    Deploy-Status-Endpoint `GET /api/status`Shared-secret-geschützter

    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

    Rechnungs-Editor + DE-Rechtskonformität (Task 02)Rechnungsnummern neu `RE-JJJJ-NNNN` mit **global durchlaufendem Zähler** (nie Reset pro Jahr, rechtlich relevant). DE-Ausstellerdaten (Name/Anschrift, Steuernr./USt-IdNr., §19-UStG-Hinweis, IBAN/BIC, Gläubiger-ID) einmal in den Einstellungen gepflegt und beim Anlegen auf jede generierte Rechnung **gesnapshottet** — eine spätere Settings-Änderung verändert versendete Rechnungen nicht mehr rückwirkend. **Draft-Locking server-seitig**: einmal versendet sind Inhalte (Positionen/Notizen/Fälligkeit) unveränderlich (Korrektur = Storno + Neu). PDF zeigt Aussteller-/§19-Block und einen **paymentMethod-bedingen Zahlungsblock** (Überweisung → IBAN/BIC + Verwendungszweck = Rechnungsnummer; SEPA-Lastschrift → Gläubiger-ID + Mandatsreferenz). **SEPA-Lastschrift nur aktivierbar mit Gläubiger-ID** (Hard-Gate, leer bis zur Bundesbank-Zuteilung). „Aus Zeiten erstellen" entfernt. Siehe [Task 02](docs/deck/tasks/02-rechnungs-editor.md). _(Verifiziert 2026-07-18: frischer Postgres + `migrate deploy`, 678 unit pass/0 fail, 10/10 Task02-e2e gruen. Document-artiges Editor-Layout bleibt als Design-Polish-Folgetask offen.)_
    SW-deck-Branding: Rebrand von Atrium → SW deckAlle User-sichtbaren „Atrium"-Texte durch „SW deck" ersetzt: Root-Layout metadata (`title`, `lang="de"`, DE-Beschreibung), i18n-Dictionary (`de.ts`, 7 Stellen), Header-Fallbacks (Dashboard/Portal/Mobile-Nav), Landing-Page, Changelog-Header, Login-Title, Telemetry-Banner (DE), Color-Patch-Grid, PWA-sw.js. Mail-Templates (Magic-Link, Reset-Password) auf Deutsch + „SW deck"-Fallback. API Test-E-Mail auf DE. Docker/Infra (`docker-compose.yml`, `docker-compose.dev.yml`, `unified-entrypoint.sh`, `scripts/dev.ts`) auf `deck:local`/`swdeck`-Defaults. `.env.example` auf SW deck + R2/DE-Hinweise. Docs (`configuration.md`, `docker.md`, `security.md`, `stripe.md`) auf SW deck. Siehe [Task 6](docs/deck/tasks/06-konfig-infra.md).
    SW-deck-Assets`apps/web/public/icon.png` = SW-deck-Favicon (Browser-Tab + PWA), `apps/web/public/logo.png` = SW-deck-Wordmark (Header-Logo-Fallback, Aspect 3.18:1). Header-Logos mit `w-auto` statt quadratisch.
    Download URLs are now provider-aware (Signed-URL-Refactor)`FilesService.getDownloadUrl` delegates to `storage.getSignedUrl` instead of always returning the API proxy path. S3-compatible providers (R2, S3, MinIO) return a time-limited direct object-store URL, offloading bandwidth from the API (R2 zero-egress benefits actually take effect). LocalStorage keeps the access-controlled proxy path keyed by file id — no regression. Access check (`assertProjectAccess`) still runs before any URL is issued. See [Task 11](docs/deck/tasks/11-signed-url-refactor.md).
    Storage strategy: Cloudflare R2 recommended for production`local` remains the dev/trial default; `r2` is now the documented primary object store (zero egress, zero ops, full S3 API). `minio` kept as self-hosted alternative. AWS S3 and Hetzner Storage Box S3 evaluated and dropped. See [ADR: Storage-Strategie R2](docs/decisions/2026-07-08-storage-strategie-r2.md).
    v1.8.02026-05-19

    Added

    Time tracking, built for billable workStart and stop timers from the project's Time tab, log manual entries, and turn un-invoiced time into a polished invoice in one click. Invoices default to month-to-date and group by hourly rate. If entries are missing a rate, the current project or member rate is applied automatically.
    Calendar viewSee tasks, project milestones, and invoices on a month grid or agenda list, with project and type filters. Events respect your local timezone.
    Invoice preview in-appA new View button opens any invoice PDF in a modal directly inside Atrium (dashboard and client portal), so you can check an invoice without leaving the page or downloading it.
    Reports sectionDedicated Reports area in the sidebar with a Time report showing totals, per-project and per-user breakdowns, and CSV export. Designed to grow as more reports are added.
    Streamlined invoice creationA single "New Invoice" button now opens a clean menu for creating from scratch, generating from tracked time, or uploading a PDF — replacing the old triple-button row.
    Reorganized SettingsSettings is now a tabbed workspace (Account, Workspace, Billing, Payments) so each area has room to breathe and you always know where to find what you need.

    Fixed

    Merged invoice totals could drift by a cent or twoWhen generating an invoice with merged line items, the displayed `Xh @ $Y/hr` math now matches the stored total exactly.
    Possible duplicate running timersStarting a timer from two tabs simultaneously could create two "running" entries. Only one running timer per user is now enforced at the database level.
    CSV export silently capped at 200 rowsThe time-entries CSV export was truncating large requests without warning; the cap has been raised to 50,000 rows and the export uses a dedicated path that doesn't share the paginated limit.
    Version footer was hard-codedThe version number at the bottom-right now reads from the changelog automatically, so it always reflects the deployed release.
    Rate visibility tightenedHourly rate values are no longer included in time-entry list and report responses for members (owner/admin only).
    Cross-project task assignment guardEditing a time entry now rejects task IDs that belong to a different project.
    Friendlier error statesTime tab and manual-entry modal now surface load failures with a retry option instead of staying stuck on "Loading…". Settings nav no longer over-highlights sibling routes.
    v1.7.02026-04-30

    Added

    View as customerSee your client portal exactly the way your client sees it. Owners and admins can launch a preview from the Clients list or from any project's client chips, with a clear banner indicating you're in preview mode. Mutations are safely blocked end-to-end (server-side and in the UI), so you can explore freely without changing any real data.
    v1.6.32026-04-28

    Fixed

    Saved branding colors were ignored on the dashboard and setup wizardthe org's primary/accent colors persisted correctly and applied in the client portal, but the `(dashboard)` and `(setup)` route group layouts never set the `--primary`/`--accent` CSS variables, so buttons and accents fell back to the default theme. Both layouts now fetch branding server-side and apply the saved colors via inline CSS variables, matching the portal layout. Reported in #43.
    v1.6.22026-04-28

    Fixed

    Client invite signup landed on the org setup wizardwhen an invitee already belonged to another org (e.g. their own agency), the post-invite redirect picked an arbitrary `orgs[0]` as the active org and routed by *that* org's role, sending the client to `/dashboard` (or `/setup` if that org's wizard was incomplete) instead of the inviting org's portal. The invited org id from `acceptInvitation` is now pinned as active before redirecting.
    v1.6.12026-04-27

    Fixed

    Password reset emails were never sentthe forgot-password form posted to `/api/auth/forget-password`, an endpoint Better Auth renamed to `/api/auth/request-password-reset` in v1.4. Requests 404'd silently, so no email was generated. Reported in #40.
    Auth emails ignored per-org email configreset, verification, magic-link, and invitation emails routed through the default sender instead of the System Settings SMTP/Resend config. Each callback now resolves the user's primary org and threads `organizationId` into `MailService.send` so per-org configs are honored.

    Added

    Admin "Send password reset link" actionowners and admins can generate a reset URL for any team or client member from `/dashboard/clients`. The link is delivered by email when configured and also surfaced in-app for out-of-band sharing on self-hosted setups where email is unreliable. Owners can reset other owners; admins cannot reset owners; nobody can reset themselves. New `POST /clients/:id/reset-password` endpoint.
    v1.6.02026-04-26

    Added

    Link-type file entriesThe Files section now supports adding external links (Nextcloud, Canva, Notion, Google Drive, etc.) alongside hard-uploaded files. A single "+ Add" menu on the project files header replaces the previous upload button with two clearly labeled options (Upload file / Add link). Link entries open in a new tab and do not count against the plan storage quota. Visible read-only in the client portal.
    oEmbed discovery for project updatesProject update URLs are now resolved via a server-side oEmbed registry covering Canva, Spotify, SoundCloud, CodePen, and Vimeo. Existing regex providers (YouTube, Loom, Figma, Google Docs) remain a zero-network fast path. Provider HTML is sanitized to an iframe-only allowlist with per-provider host pinning; failed resolutions silently degrade to plain links. Embed cards are capped to `max-w-xl` so they don't dominate the timeline. Twitter/X is intentionally deferred — its oEmbed response is a `<blockquote>` + widgets.js script, which our iframe-only sanitizer rejects.

    Changed

  • `POST /files/link` endpoint added (owner/admin only). `download`/`remove` paths branch on file `type` so link entries are never fetched from storage. CSP `frame-src` extended for the new providers' embed hosts.
  • Database

  • `File.type` (`UPLOAD | LINK`, default `UPLOAD`), `File.url`, `File.description` added. `storageKey`/`mimeType`/`sizeBytes` are now nullable to accommodate link entries; all existing readers filter to `type = UPLOAD` or tolerate nulls.
  • v1.4.42026-04-21

    Added

    Client task requestsClients can submit task requests directly from the project portal via a "New Request" button. Requests appear in the agency's task list with a "Client Request" badge and requester attribution. Clients can cancel their own pending requests; agency members can assign, update status, and resolve them.
    Task status workflowTasks now have an explicit status (`todo`, `in_progress`, `in_review`, `done`, `cancelled`) instead of a boolean `completed` flag. Dashboard task list supports server-side status filtering. Status changes trigger in-app notifications for the assignee and requester.
    Task assignee pickerAgency members can assign tasks to any org member from the dashboard task row. Assignment changes send a notification to the new assignee.
    Ticket-style task detail modalClicking a task opens a detail modal with description, status, assignee, requester, and an inline label picker. Deep-linking via `?task=<id>` auto-opens the modal and switches to the Tasks tab on both dashboard and portal.
    Task notificationsNew notification types for client requests, task assignments, and status changes (deduplicated to avoid spamming on rapid edits).

    Changed

  • `POST /tasks/mine` endpoint added for client request creation; `PATCH /tasks/:id/cancel` added for client self-cancellation. Existing `POST /tasks` strips `requestedById` from agency-side creates and validates `assigneeId` org membership.
  • CSV export of tasks now includes status, assignee, and requester columns.
  • Dashboard task list status filter moved to the server; the page now resets to page 1 when the filter changes.
  • Fixed

  • Task creation modal label picker was clipped by modal overflow — replaced the popover with an inline picker.
  • Dashboard task list no longer issues N+1 member lookups when rendering assignees and requesters.
  • Playwright `NEXT_PUBLIC_API_URL` was not inlined into the dev web bundle because Next.js only auto-loads envs from its own package directory; the e2e config now pre-loads the monorepo root `.env` before spawning servers.
  • Database

  • Replaced `task.completed` boolean with `task.status` enum.
  • Added `task.requestedById` and `task.assigneeId` with indexes.
  • Added composite index on `task(projectId, status)` for filtered list queries.
  • v1.4.32026-04-07

    Added

    Plan limit upsell promptsFree-plan users now see contextual upgrade prompts at the point of friction: a usage counter and upgrade card on the Projects page, member/client limit banners on the People page, and a locked input with inline `PRO` badge on the Custom Domain setting.
    Billing page redesignPlans section is now the primary focus (moved above current plan details). Pro card is visually highlighted with a teal border and "Most Popular" badge. Lifetime card includes a founding member spots meter and a "Become a Founding Member" CTA. Usage meters are compact (2-column, small text).
    Contextual upgrade bannerArriving at the billing page via an upsell prompt now shows a banner explaining why you're there (projects limit, clients limit, or custom domain).
    Founder Discord accessAdded as a feature bullet on the Lifetime plan.

    Fixed

  • Invoice delete button was hidden for `overdue` and `cancelled` invoices — now any non-`paid` invoice without a Stripe payment intent can be deleted.
  • Upsell components were reading `sub.plan` directly but the billing API wraps it as `sub.subscription.plan`, causing plan limits to never apply.
  • Billing tab not opening when navigating to `/dashboard/settings/account?tab=billing` — `billingEnabled` wasn't in the sync effect's dependency array, so the tab stayed on Profile until a second interaction.
  • Upgrade links redirected through `/dashboard/settings/billing` (which itself redirected) — all links now point directly to `?tab=billing`.
  • v1.4.22026-04-05

    Added

    Optional telemetrySelf-hosters are prompted on first login to opt in to anonymous crash reporting via Sentry. Owners can change their preference at any time in Settings → General → Error Reporting. No personal data, client data, or identifiable information is ever included. See [docs/telemetry.md](docs/telemetry.md) for a full breakdown of what is and isn't collected.

    Security

    Sentry `beforeSend` scrubberAll Sentry configs (browser, Next.js server, edge, API) strip cookies, `Authorization` headers, and any user identity fields before events are transmitted, making the "no PII" guarantee enforceable at the code level.
    v1.4.12026-04-05

    Added

    Custom domainsPoint your own domain (e.g. `portal.yourcompany.com`) to the client portal. Caddy provisions SSL automatically on first visit. Includes DNS setup instructions for Cloudflare, Route 53, GoDaddy, Namecheap, and more. On hosted plans this is a paid feature; self-hosters get it for free.
    Branded login pageThe `/login` page now automatically shows your logo and brand colors on single-org (self-hosted) instances. No configuration needed — upload a logo in Settings and it appears. Hosted users can find their `/login/[slug]` URL with a copy button in Branding settings.
    Dynamic payment methodsThe Payments settings section now fetches your Stripe account's active capabilities and shows only the payment methods available to you (Card, ACH, SEPA, iDEAL, Klarna, Affirm, Afterpay, and more). Inactive BNPL methods link directly to the Stripe dashboard to enable them. Selected methods are persisted and used when generating Stripe Checkout sessions.

    Changed

    Settings page redesignedSystem Settings reorganised into three tabs (Branding, General, Payments) instead of a single long scroll. Save buttons are scoped to each section.
    Runtime billing config`BILLING_ENABLED` is now read at runtime via `GET /api/health/config` instead of being baked into the Next.js build. Changing billing state no longer requires a rebuild.
    Env var cleanup`BETTER_AUTH_URL` consolidated into `API_URL`. `NEXT_PUBLIC_DOMAIN` and `MAIN_DOMAIN` removed — the app now derives the hostname from `WEB_URL`. Existing deployments using `BETTER_AUTH_URL` continue to work via a backwards-compatible fallback.
  • Better Auth sessions now persist for 30 days instead of expiring at the end of the browser session.
  • Fixed

  • Settings page crashing with `Cannot read properties of undefined (reading 'slug')` when billing subscription response was missing the `plan` field.
  • Billing plan selection not loading plans on the signup page when `billingEnabled` resolved asynchronously.
  • 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.

    v1.4.02026-04-04

    Added

    Client invoice payments via StripeClients can now pay invoices directly from the portal with a "Pay Now" button. Supports Direct Keys (paste your Stripe secret key) or Stripe Connect OAuth for a white-labeled experience. Invoices are automatically marked as paid when the Stripe webhook fires.
    Global searchFull-text search across projects, files, clients, and updates. Open with `Cmd+K` from anywhere in the dashboard.

    Changed

    Invoice draft/send UXInvoice creation modal now has separate "Save as Draft" and "Send to Client" buttons, replacing the previous two-step flow. Draft invoices are hidden from the client portal.
  • Docker deployment guide updated with Stripe Connect environment variables (`STRIPE_CONNECT_CLIENT_ID`, `STRIPE_CONNECT_WEBHOOK_SECRET`, `STRIPE_CURRENCY`).
  • New `docs/stripe.md` covering both Direct Keys and Connect modes with step-by-step setup instructions.
  • Fixed

  • Duplicate search dialog appearing when both GlobalSearch instances received `Cmd+K` simultaneously.
  • Hardcoded `X-Forwarded-Proto: https` in Caddyfile that caused issues behind certain reverse proxies.
  • `DIRECT_URL` fallback in Docker entrypoint to prevent Prisma validation errors on startup.
  • v1.3.42026-04-01

    Added

    Global searchPress `⌘K` (or `Ctrl+K`) anywhere in the dashboard to open a command palette that searches across projects, tasks, files, and people in real time. Results are grouped by type with keyboard navigation (arrow keys + Enter). A search icon in the top-right sidebar header provides a mouse-accessible trigger.
    Content embeddingPaste a YouTube, Loom, Figma, or Google Docs/Sheets/Slides link into any project update and an inline preview will automatically appear below the text. Up to 3 embeds per update. A fallback "Open in …" link is shown for providers that require login.
    v1.3.32026-03-21

    Added

    CommentsReply to project updates and tasks from the dashboard or portal.
    Client updatesClients can now post updates from the portal.
    Tags & LabelsCreate org-wide labels in System Settings and assign them to projects, tasks, files, and clients. Filter projects by label on the projects list page. Colored badges display throughout the dashboard.
    In-app notificationsReal-time notification bell in the dashboard and portal with unread count, mark-as-read, and mark-all-read.
    Push notificationsBrowser push notifications for project updates, task assignments, and comments via Web Push (VAPID). Service worker included.
    CSV data exportDownload projects, invoices, people, and tasks as CSV files from their respective pages.

    Security

    kyselyoverride `0.28.11` → `0.28.14` — fixes two SQL injection vulnerabilities (CVE-2026-32763, CVE-2026-33468)
    fast-xml-parseroverride `5.4.1` → `5.5.7+` — fixes XML entity expansion (CVE-2026-33036) and input validation (CVE-2026-33349)

    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.

    v1.3.22026-03-21

    Added

    Mobile-Responsive UI

    Collapsible sidebarSlide-out hamburger menu on mobile with backdrop, auto-close on navigation
    Adaptive project detailCollapsible `<details>` card for project metadata on mobile so tabs are immediately accessible
    Responsive layoutsPadding, tab bars, and flex layouts adapt across all dashboard and portal views
    Responsive button labels"Upload Invoice" shortens to "Upload" on small screens

    Testing

  • Mobile navigation e2e tests (hamburger visibility, drawer open/close, route-change auto-close, desktop sidebar)
  • Fixed

  • Email errors in password reset and verification now propagate to the user instead of being silently swallowed
  • Removed PII (email addresses) from error logs in onboarding controller
  • Signup error logging now captures only `message` and `code` instead of full error object
  • Security

  • Next.js updated to 15.5.14 (patches CVE HTTP request smuggling + disk cache exhaustion)
  • v1.3.12026-03-18

    Breaking Changes

    Documents start as draftsUploaded documents are no longer immediately visible to clients. Admins must click "Send to Client" (or use "Upload & Send") to make them visible. Existing pending documents are unaffected.
    New version resets responsesWhen an admin uploads a new version of a sent/signed document, all client responses and signatures are cleared and the document returns to "pending" for re-review. This enables scope change tracking but means clients must re-sign.
    Unified upload flowThe separate "Upload File" button is removed. All uploads go through the document modal (title, type, optional signature/approval). For quick file shares, use type "Other" and hit "Upload & Send".

    Added

    Document Lifecycle

    Draft statusDocuments start as drafts, invisible to clients until explicitly sent
    Send workflow`Send to Client` transitions draft → pending and notifies clients via email
    Void documentsCancel sent documents with optional reason; clients see read-only "Voided" badge
    Audit trailEvery document action (created, sent, viewed, signed, voided, expired) logged with timestamp, user, IP, and user agent. Viewable from the dashboard

    E-Signature Enhancements

    Field typesSignature, date (auto-fill), initials, text input, and select (radio options) fields
    Signing orderSequential signing enforcement; locked fields show "Waiting..." until prior signers complete
    Signer assignmentAssign specific fields to specific project clients
    Admin signingAdmins/owners can sign documents directly from the dashboard
    Type-to-sign defaultSignature pad now defaults to type mode (draw still available)

    Expiration & Reminders

    Document expirationSet expiry (7/14/30/60/90 days) when sending; auto-expired by hourly cron
    Automatic remindersEmail reminders to unresponsive clients at configurable intervals (1-7 days)

    Direct Signing Links

    Access tokensGenerate secure signing links for clients (SHA-256 hashed, time-limited)
    Public signing page`/portal/sign/[token]` renders signing UI without portal login
    Token revocationAdmins can revoke individual signing links
    Token cleanupDaily cron deletes expired tokens older than 30 days

    Completion Certificate

    PDF certificateAuto-generated completion certificate with document info, signer table, and full audit trail
    Downloadfrom both dashboard and portal when document is fully signed

    Client Choices

    Question with optionsAttach a question with radio button choices to any document; client must select one

    UI Improvements

    Unified Files tabDocuments and files merged into single "Files" tab with one Upload button
    Simplified upload modalProgressive disclosure: title + type + file upfront, signature checkbox for PDFs, advanced options collapsed
    Upload & SendPrimary CTA sends immediately; Save Draft for preparation
    Auto-populate titlefrom filename on file selection
    Three-tier action barPrimary action (solid button), secondary actions (icon group), destructive actions (right-aligned, danger on hover)
    Continuous PDF scrollAll pages render in a scrollable view with lazy loading via IntersectionObserver
    Voided/expired statesin portal — Read-only badges, no action buttons

    Document Versioning

    Version historyUpload new file versions to any document (draft or sent). Previous versions preserved with uploader name and timestamp
    Restore versionsRestore any previous version with one click; creates a new version entry for traceability
    Scope change trackingUploading a new version on a sent document resets to "pending" so clients re-review the changes
    Version badgeDocuments with multiple versions show a "v2", "v3" etc. badge

    Direct Signing Links

    Generate signing linksCreate secure, time-limited signing URLs for specific clients from the dashboard
    Manage linksView active links, copy URLs, and revoke links per document
    Token securitySHA-256 hashed tokens, rate-limited public endpoints, auto-cleanup of expired tokens

    Email Templates

  • `DocumentReminderEmail` — Reminder for unresponsive clients with optional expiry date
  • `DocumentSigningTurnEmail` — "Your turn to sign" notification for sequential signing
  • Testing

  • 82 unit tests for `DocumentsService` covering all business logic paths
  • 13 new E2E API tests (send, void, audit trail, field locking, token generation, expiry validation)
  • Fixed

  • Certificate page overflow — audit trail now properly spans multiple pages
  • Token endpoint response filtered — no internal IDs exposed to unauthenticated users
  • Signature fields locked on sent documents — prevents editing after clients have signed
  • `requiresSignature` enforced as PDF-only on the API
  • Send blocked for signature documents with zero fields placed
  • `expiresInDays` validated on send endpoint (1-365 range)
  • Signing audit events logged synchronously for compliance
  • Notification type safety — removed unsafe `as any` cast
  • `fetchSigningInfo` stale closure fixed in signing viewer
  • 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.

    v1.3.02026-03-17

    Added

    DocumentsUpload quotes, contracts, and NDAs to projects. Clients review inline and accept/decline with optional reason. Admin sees status, decline reason, and can reset to re-request.
    Decision tasksNew task type where clients vote on options. Vote counts hidden until all clients have voted.
    Activity feedDocument responses and decision votes appear in the project updates timeline.
    Email notificationsClients notified on document uploads, invoice uploads, and decision results. Admins notified on client responses.
    Invoice uploadsUpload PDF/image invoices as an alternative to itemized invoices.
    Payment settingsConfigure payment instructions and method (bank transfer, PayPal, Stripe, other) with encrypted storage.
    Portal UXPending actions banner, badge counts on Files tab, confirmation dialog on accept/decline, inline document viewer modal.
    Responsive layoutDashboard and portal stack vertically on small screens; tabs scroll, forms wrap.
  • Configurable rate limiting via `THROTTLE_LIMIT` and `SIGNUP_THROTTLE_LIMIT` env vars.
  • Fixed

  • Portal invoice pagination now filters server-side by project (was client-side, broke page counts)
  • Activity logging errors now logged instead of silently swallowed
  • Tracker script injection hardened: `NEXT_PUBLIC_TRACKERS` validated against attribute whitelist
  • React hydration mismatch (#418) suppressed on root elements
  • 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`.

    v1.2.12026-03-12

    Added

  • Default Atrium logo on landing page and sidebar (falls back when no custom branding is set)
  • "Hide logo" toggle in branding settings for orgs without a company logo
  • Sidebar and portal header automatically reflect branding changes after save (no refresh needed)
  • Fixed

  • README image paths not rendering on GitHub (`public/` → `./public/`)
  • v1.2.02026-03-11

    Added

    Account Deletion

  • Owners can delete their account and cascade-delete their organization (projects, files, invoices, clients)
  • Password re-authentication required before deletion
  • Type-to-confirm dialog requiring `DELETE <org name>`
  • `GET /api/account/deletion-info` preflight endpoint returns org ownership context
  • Clients (non-owners) can delete their own account from portal settings
  • E2E tests for deletion flow, credential invalidation, and non-owner visibility
  • Supabase Row Level Security

  • `enable-rls.sql` enables RLS on all 21 tables and revokes `anon`/`authenticated` access
  • `bun run db:rls` command to apply manually
  • Docker entrypoints apply RLS automatically when `SUPABASE=true`
  • Safe for plain Postgres — gated behind env var, skipped by default
  • Docker

  • Built-in PostgreSQL 16 bundled in the unified Docker image — no separate database container needed
  • `USE_BUILT_IN_DB` toggle: set to `false` with a `DATABASE_URL` to use an external database
  • Graceful shutdown of built-in PostgreSQL on container stop
  • Docker Hub overview with quick start, Compose examples, and env var reference
  • `scripts/update-dockerhub-readme.sh` to push Docker Hub description from `docker/DOCKERHUB.md`
  • Docker deployment documentation (`docs/docker.md`)
  • Unraid

  • Unraid Community Applications template with single-container setup
  • Template repo at `Vibra-Labs/unraid-templates` linked as git submodule
  • PR submitted to `selfhosters/unRAID-CA-templates` for CA listing
  • Invitations

  • Accept-invite auto-login when a user signs up with an already-existing account
  • Accept-invite sets active organization before redirect
  • UX

  • Portal `/portal` redirects to `/portal/projects`
  • Danger zone section visible to all dashboard users (owners and non-owners)
  • Security

  • Comprehensive security audit with findings documented in `SECURITY_AUDIT.md`
  • DTO validation added for project and task inputs (`@IsDateString`, `@MaxLength`)
  • Path traversal protection in local file storage (reject `..` in keys)
  • Branding logo upload restricted to image MIME types
  • Update attachment size validated against system settings before storage
  • Caddyfile hardened with `X-Content-Type-Options`, `X-Frame-Options`, `Referrer-Policy` headers
  • PostgreSQL port no longer exposed to host in production `docker-compose.yml`
  • Generic password verification error message to prevent user enumeration
  • `DELETED_USER_SENTINEL` constant in shared package for anonymized user references
  • Changed

  • Deploy workflow now only pushes to Docker Hub (removed Google Cloud Run and Firebase Hosting steps)
  • Shared `setActiveOrgAndRedirect` helper replaces duplicated org-switch-and-redirect logic
  • Organization deletion now purges file blobs from storage provider (not just DB records)
  • Fixed

  • Dockerfile missing `ca-certificates` package broke PostgreSQL apt repo setup
  • v1.1.02026-03-09

    Added

    Billing & Subscriptions

  • Stripe integration with test/live mode toggle via `STRIPE_MODE`
  • Subscription plans (Free, Pro, Lifetime) with DB-seeded configuration
  • Checkout flow via Stripe Checkout Sessions
  • Stripe Customer Portal for managing payment methods
  • Webhook handler for checkout, subscription updates, invoice events
  • Usage meters (projects, storage, team members, clients) on billing page
  • Plan limit enforcement via `PlanGuard` and `@PlanLimit()` decorator
  • Lifetime plan with seat cap tracking
  • Lazy free plan initialization for orgs created before billing was enabled
  • Performance

  • In-memory session cache (30s TTL) — reduces DB round trips from 8+ to 2 per page load
  • Auth routes bypass and invalidate cache to prevent stale org context
  • Fixed

  • Auth controller uses `BETTER_AUTH_URL` for request origin instead of `WEB_URL`
  • Session cache invalidation on auth mutations prevents 401 "Organization context required" errors
  • Invoice update/delete mutations now scope Prisma queries to `organizationId` (prevents potential cross-org TOCTOU race)
  • Browser autofill no longer overrides dark mode input backgrounds
  • Sign-in button shows spinner during login
  • Security

  • Removed scripts containing hardcoded credentials
  • Sanitized infrastructure docs (removed project IDs and service refs)
  • GitHub Actions deploy workflow uses variables instead of hardcoded URLs
  • Added `.firebase/`, `.firebaserc`, `firebase.json`, `*.pem`, `*.key` to `.gitignore`
  • Database

  • New models: `SubscriptionPlan`, `Subscription`
  • Subscription linked to Organization (1:1) with Stripe customer/subscription IDs
  • Plan features stored as string array, limits as integers (-1 = unlimited)
  • v1.0.22026-03-02

    Security

  • Fix IDOR in invoice creation and project client assignment
  • Remove SVG uploads, sanitize Content-Disposition headers
  • Add access control to update attachment and file list endpoints
  • Fixed

  • Invoice status transition validation, PDF page breaks, dueDate clearing
  • Linkify regex `/g` flag bug, SMTP cache scoping and TTL eviction
  • Notification emails now receive organizationId for SMTP routing
  • Changed

  • Deduplicate `assertProjectAccess`, `contentDisposition`, and `linkify` into shared helpers
  • Replace all `any` types with proper type definitions
  • v1.0.11

    Added

    Tasks

  • Create, reorder, and track tasks per project
  • Inline task creation with due date picker in dashboard
  • Clients see read-only task lists in the portal
  • Client notification emails on task creation
  • Invoicing

  • Full invoice lifecycle with auto-numbered invoices (INV-0001)
  • Line items with quantity, unit price, and calculated totals
  • Status workflow: draft → sent → paid / overdue
  • Invoice stats dashboard (total, outstanding, paid amounts)
  • Client-facing invoice list and detail views in portal
  • Client notification emails when invoices are sent
  • Internal Notes

  • Team-only notes on projects (create, list, delete)
  • Collapsible "Internal Notes (Team Only)" section in project detail
  • Fully isolated from client portal
  • Client Profiles

  • Self-service profile editing (company, phone, address, website, description)
  • Admin profile viewing in client list
  • Profile form in portal settings
  • Email Verification

  • Verification email sent on signup via Better Auth
  • `/verify-email` page with verified/unverified states
  • Non-blocking dashboard banner with "Resend verification email" button
  • Email verification is optional — self-hosted users without email can still log in
  • Notifications

  • Email notifications for project updates (sent to all assigned clients)
  • Email notifications for new tasks
  • Email notifications when invoices are marked as sent
  • Fire-and-forget delivery — notification failures never block API responses
  • Parallel email delivery via `Promise.allSettled`
  • System Settings

  • `SystemSettings` Prisma model with per-organization config
  • Admin settings UI at `/dashboard/settings/system`
  • Email provider configuration (Resend or SMTP) from the UI
  • Sensitive fields encrypted at rest (AES-256-GCM with HKDF-derived key)
  • Dynamic file upload size limits (configurable per org, 1-500 MB)
  • "Send Test Email" button to verify email config
  • DB settings with env-var fallbacks: `DB setting → env var → default`
  • Setup Wizard

  • 5-step first-run wizard at `/setup` for new organizations:
  • 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

  • Automatic redirect from dashboard for owners who haven't completed setup
  • Steps 2-4 are skippable
  • Security

  • CSRF protection via double-submit cookie pattern
  • Auth secret validation — refuses to start in production with default secret
  • File download authorization — members must be assigned to the project
  • CSRF guard skips unauthenticated requests (no session = no CSRF risk)
  • CSRF token auto-retry on first mutating request from the frontend
  • Fixed

  • Invitation email grammar: "You have been invited you" → "You have been invited"
  • Welcome email template wired up (was dead code)
  • Invoice number race condition — serializable transaction with P2002 retry
  • `@IsEmail()` validator no longer rejects `null` when clearing email settings
  • File size validation returns HTTP 413 (`PayloadTooLargeException`) instead of 400
  • Invoice stats computed via DB aggregation instead of loading all records into memory
  • Welcome email failures now logged instead of silently swallowed
  • SMTP transporter cached and reused instead of created per email
  • Multer hard limit lowered from 500 MB to 200 MB
  • `sanitizeFilename` deduplicated into shared utility
  • Update attachments now appear in Files tab immediately (file list refreshes after posting/deleting updates)
  • Changed

  • Signup redirects to `/setup` wizard instead of directly to `/dashboard`
  • Invoice numbers use 4-digit padding (INV-0001 instead of INV-001)
  • Encryption key derived via HKDF instead of using auth secret directly
  • Frontend settings page uses boolean flags instead of fragile mask string comparison
  • Database

  • New models: `Task`, `Invoice`, `InvoiceLineItem`, `ProjectNote`, `ClientProfile`, `SystemSettings`
  • New relations on `Project`: `tasks`, `invoices`, `notes`
  • Added `setupCompleted` field to `Organization` model
  • Added indexes on `Member` table (`organizationId`, `userId`)
  • Tests

  • 165 unit tests across 16 test files (0 failures)
  • New test suites: settings service, settings DTO, invoices service, notifications service, mail service, setup controller, sanitize utility
  • Updated: CSRF guard (19 tests), files service (13 tests)
  • E2E tests for all new features: tasks, invoicing, notes, client profiles, email verification, notifications, system settings, setup wizard, portal isolation
  • vUnreleased