Files
RAPPORT/ARCHITECTURE.md
T
karim 27b1057cd4 Release 0.8.0: Cloud-Variante (Supabase, Multi-Studio, Realtime, Web-Deploy)
Rapport ist jetzt dual: lokal (wie bisher) ODER Cloud auf eigenem Supabase-Server.
Beide Modi haben dieselben Funktionen, Cloud zusätzlich Multi-User + Live-Sync.

Storage-Architektur
- src/storage/adapter.js: einheitliche Promise-API, LocalStorage- und SupabaseAdapter
- src/storage/migrations.js: applyMigrations als reine Funktion, für beide Backends
- Konfig-driven: VITE_SUPABASE_URL im Production-Build → automatisch Cloud-Modus

Postgres-Schema (supabase/migrations/0001–0010)
- 29 Tabellen, multi-tenant via studio_id + Row-Level-Security
- Audit-Spalten (created_by/updated_by/at) + Trigger
- Seed-Trigger pro neuem Studio (Rollen, Templates, Absenz-Typen)
- Realtime-Publication für Live-Sync
- RPCs: ensure_profile, create_studio_with_admin (mit Personen-Sharing),
  list_studios, load_persons_for_studio, attach_user_to_studio

Cloud-Features (App)
- BackendChoice.jsx als Erst-Screen «Lokal oder Cloud»
- CloudSetup.jsx: 3-Schritt-Wizard für Erst-Einrichtung
- Login.jsx: Modus-Switcher + Server-URL + Studio-Dropdown + Passwort-Vergessen
- ResetPassword.jsx: empfängt Mail-Link-Klick via PASSWORD_RECOVERY-Event
- Realtime: Änderungen zwischen Browsern ohne Reload sichtbar
- Settings → System: Cloud-Verbindung, Studio-Switcher, weiteres Studio anlegen
- Settings → Team: Mitarbeiter via Email einladen (Admin-Aktion)
- Personen-Sharing: bei neuem Studio Personen aus anderen Studios übernehmen
- Reload-Resume: studio_id in sessionStorage, kein erneuter Login nötig

Web-Deploy
- deploy/docker-compose.yml + nginx.conf: dist/ via nginx-Container, Port 8080
- .env.production.example: Build-time Cloud-URL
- DEPLOY.md: Anleitung für LAN-only und extern via Nginx Proxy Manager

Doku
- README.md: Cloud-Variante prominent erklärt
- ARCHITECTURE.md: Storage-Adapter, Migrations, neue Views in Risiko-Tabelle
- DEPLOY.md: Schritt-für-Schritt für Mac Mini + NPM

Version-Bump auf 0.8.0 in package.json, src-tauri/tauri.conf.json, Cargo.toml.
Changelog-Entry im App.jsx-Modal (Karim sieht ihn beim ersten Start).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 19:08:00 +02:00

27 KiB
Raw Blame History

RAPPORT — Architektur-Übersicht

Studio-Management für Architekturbüros. Tauri 2.x Desktop-App, React 19 Frontend, Rust-Backend (minimal), localStorage-only Persistierung. Solo-Dev: Karim. Dieses Dokument ist die Karte der Codebase. Es ersetzt nicht das Lesen einzelner Dateien, soll aber verhindern, dass jede Session bei Null anfangen muss.


1. Mentales Modell in einem Absatz

RAPPORT ist eine monolithische SPA: ein React-Root in App.jsx hält den gesamten App-State in einem useState({...}), persistiert ihn synchron in localStorage unter dem Key studio_data_v1, und übergibt ihn als Props an lazy-geladene Views. Es gibt kein Routing-Framework (View-Wechsel via String-State), kein State-Library (kein Redux/Context/Zustand), kein TypeScript, kein CSS-Framework (alles inline style={{}} oder ein 700-Zeilen <style>-Block in App.jsx). Der Rust-Teil ist 109 Zeilen und macht nur drei Dinge: System-Tray, Window-Hide-on-Close, Plugin-Registrierung (Updater, Process, Log). Es gibt keine #[tauri::command] — Frontend ↔ Backend kommuniziert nur über das Event rapport:navigate (Tray → Frontend). Alle Daten sind im WebView-localStorage, nichts wird in Rust gespeichert.

Konsequenz: Wenn etwas mit Daten passiert, ist es JS. Wenn etwas mit dem Fenster/Tray/Update passiert, ist es Rust. Es gibt keine dritte Stelle.


2. Verzeichnis-Karte

APP/
├── src/                          React-Frontend (kein TS, nur .jsx)
│   ├── main.jsx                  Entry: createRoot().render(<App />)
│   ├── App.jsx                   Root: State, Navigation, Auth, Migrationen, Sidebar, Modals  [823 Z.]
│   ├── constants.js              STORAGE_KEY, NAV_ITEMS, defaultData, SIA-Phasen, Statusfarben  [252 Z.]
│   ├── utils.js                  Business-Logik: Kalkulation, Format, QR-Bill, Hash, CSV, Lohn  [678 Z.]
│   ├── storage/adapter.js        Storage-Adapter (Promise-API): LocalStorageAdapter + SupabaseAdapter, Modus-Auswahl
│   ├── storage/supabase-adapter.js  Cloud-Implementation: load/save/realtime/signIn/signUp/invite/reset
│   ├── storage/supabase-mappers.js  fromDB / toDB für ~22 Entities (camelCase ↔ snake_case, JSONB-Spread)
│   ├── storage/migrations.js     `applyMigrations(parsed, defaultData)` — Schema-Migrations als reine Funktion
│   ├── views/                    31 Top-Level-Screens, lazy-geladen
│   ├── components/               UI.jsx (StatusBadge, Modal, FormField, …), UpdateNotifier, UpdatesSupport
│   ├── print/                    PrintComponents.jsx — alle Druckansichten (Rechnung, QR, Brief, …)
│   ├── utils/updater.js          Wrapper um @tauri-apps/plugin-updater + plugin-process
│   ├── assets/                   hero.png
│   ├── App.css, index.css        Globale CSS-Reset + Variablen
│   └── index.jsx
│
├── supabase/                     Cloud-Schema (Postgres + Supabase Auth + Storage + Realtime)
│   ├── config.toml               Lokale Supabase-Konfiguration (site_url, redirects)
│   └── migrations/               10 SQL-Migrations:
│                                   0001 initial schema (29 Tabellen, RLS, Audit-Trigger)
│                                   0002 storage buckets (receipts, logos)
│                                   0003 seed defaults pro neuem Studio
│                                   0004 realtime publication
│                                   0005 signup RPCs (ensure_profile, create_studio_with_admin)
│                                   0006 list_studios RPC (für Login-Dropdown)
│                                   0007 persons sharing across studios
│                                   0008 load_persons RPC (lokale + geteilte)
│                                   0009 attach_user_to_studio (Mitarbeiter einladen)
│                                   0010 studio name + setup_completed in settings
│
├── deploy/                       Static-Hosting der Web-App via nginx-Container
│   ├── docker-compose.yml        nginx:alpine, mountet ../dist
│   └── nginx.conf                SPA-Routing (try_files $uri /index.html)
│
├── .env.production.example       Template für VITE_SUPABASE_URL / ANON_KEY
├── DEPLOY.md                     Deploy-Anleitung (LAN-only + extern via NPM)
│
├── src-tauri/                    Rust-Backend (Tauri 2.10.3)
│   ├── src/main.rs               6 Zeilen — delegiert zu app_lib::run()
│   ├── src/lib.rs                103 Zeilen — Tray, Window-Events, Plugins
│   ├── Cargo.toml                tauri, plugin-updater, plugin-process, plugin-log, serde
│   ├── tauri.conf.json           Updater-URL, Public-Key, CSP, Bundle-Targets
│   ├── capabilities/default.json core:default, core:webview:allow-print, updater:default, process:allow-restart
│   ├── icons/                    .icns, .ico, mehrere PNGs (icon.png = Quelle)
│   ├── gen/                      AUTO-GENERIERT — nie editieren
│   ├── target/                   Cargo-Build-Output (~2 GB) — nie editieren
│   └── build.rs                  Ruft tauri_build::build()
│
├── scripts/release.sh            Build + Sign + latest.json
├── latest.json                   Updater-Manifest (im Repo, weil über main.HTTP abgerufen)
├── package.json                  version, scripts (dev, build, lint), Deps
├── eslint.config.js              Flat-Config, sehr minimal
├── vite.config.js                Nur @vitejs/plugin-react
├── index.html                    Vite-Entry
├── public/                       favicon.svg, icons.svg
├── README.md                     Setup-Doku (Release-Sektion veraltet, siehe §6)
└── .claude/                      Lokale Claude-Code-Einstellungen (gitignored)

3. Datenfluss — Wie Updates wirklich passieren

User-Interaktion in View
     │
     ▼  onChange / onClick
View ruft eine der zwei Props auf:
     │
     ├── update(key, value)      → save({ ...data, [key]: value })  // Top-Level-Field
     └── saveAll(newData)         → save(newData)                    // Atomar
                                     │
                                     ▼
                              setData(newData)
                              storage.save(newData)   ← Storage-Adapter
                                     │
                                     ▼
                              React re-rendert die Hierarchie

Storage-Adapter (src/storage/adapter.js) ist die einzige Stelle, die studio_data_v1 schreibt. Zwei Implementationen mit identischem Promise-Interface:

  • LocalStorageAdapter — Browser-localStorage, sync wrapped in Promises
  • SupabaseAdapter (supabase-adapter.js) — Cloud via Postgres + REST + Realtime; pro data.*-Array eine eigene Tabelle (Mappers in supabase-mappers.js)

Auswahl-Logik beim Modul-Load: localStorage.rapport_backend entscheidet ("local" | "cloud"). Bei Production-Build mit gesetztem VITE_SUPABASE_URL wird automatisch Cloud (für Web-Deploy). Schnittstelle: hasExistingData(), load(), save(data), clear(). SupabaseAdapter zusätzlich: signIn/signUp/signOut, setStudioId, myStudios/listStudios, createStudio, inviteMember, requestPasswordReset, subscribeToChanges. App.jsx zeigt Boot-Spinner (ViewFallback) bis Initial-Load durch ist.

Per-Device-UI-State (Dark Mode, Zoom, Sidebar-Collapse, Changelog-Seen, rapport_backend/rapport_cloud_url) bleibt außerhalb des Adapters in direktem localStorage — kommt nicht in die Cloud.

Schema-Migrations (src/storage/migrations.js) sind als reine Funktion applyMigrations(parsed, defaultData) extrahiert, damit derselbe Code auf Local- und Cloud-geladenen Daten läuft. Beim Initial-Load in App.jsx wird das Ergebnis vom Adapter durch diese Funktion gepiped, bevor es in useState landet.

Wichtig:

  • save() schreibt synchron bei jedem Update — kein Debouncing. Bei großen data-Objekten kann das spürbar werden.
  • Es gibt kein Backup, kein Conflict-Resolution. Wenn der User zwei App-Fenster hat (passiert kaum, weil Single-Window), überschreibt das letzte gewinnt.
  • Kein Schema-Validator beim Laden — wenn localStorage korrupt ist, crasht es im Render.

data Top-Level-Struktur (definiert in constants.js):

{
  settings,           // Studio-Daten: Name, IBAN, MWST, Stundensätze, Rollen, …
  persons[],          // Kunden + Partner (vereint seit v0.5)
  projects[],         // Projekte mit SIA-Phasen / Budget / Billing-Type
  timeEntries[],      // Stundeneinträge
  invoices[], quotes[],
  expenses[], internalExpenses[],
  employees[], absences[], ferienEntries[], lohnEntries[],
  protocols[], deliveryNotes[], letterTemplates[],
  dashboardTemplates[], appRoles[], users[]
}

4. Navigation & State-Verwaltung

Navigation ist eine String-State-Machine in App.jsx:

  • const [view, setView] = useState("dashboard")
  • Jede View ist ein String-Identifier ("dashboard", "projects", "time", …)
  • navigate(newView) schiebt auf einen Ref-basierten History-Stack (Browser-ähnliches Vor/Zurück)
  • Drill-down via selectedProjectId etc. (zweiter State neben view)

Modals: Inkonsistent — manche Views nutzen modal: { type, id }, andere haben mehrere separate useState (siehe Invoices.jsx: setModal, setNewInvModal, setAkontoModal). Es gibt keinen zentralen Modal-Manager.

Sessions/Auth:

  • sessionStorage.rapport_user — angemeldeter User (gestrippt von Credentials)
  • Login per PBKDF2-SHA-256 (100k Iter, 16-Byte Salt) — siehe hashPassword, verifyPassword in utils.js
  • Lockout: 5 Fehlversuche → 60s Sperre (Login.jsx)
  • Auto-Upgrade: legacy plaintext-Passwörter werden beim ersten erfolgreichen Login zu PBKDF2 migriert (App.jsx:143-161)

Weitere localStorage-Keys (alle rapport_*-prefixed):

Key Zweck
studio_data_v1 Die Hauptdaten (alles außer Settings unten)
rapport_dark Dark Mode ("1"/"0")
rapport_sidebar_collapsed Sidebar collapsed ("1"/"0")
rapport_zoom UI-Zoom-Faktor
rapport_changelog_seen Zuletzt gesehene Changelog-Version
rapport_v0_5_migrated Marker für abgeschlossene Migration
rapport_update_skipped_version Vom User übersprungene Update-Version
rapport_update_last_check ISO-Timestamp letzter Update-Check
sessionStorage.rapport_user Aktive Session

5. Views — Inventar

Alle Views liegen in src/views/, werden in App.jsx per React.lazy() geladen und über Props { data, update, saveAll, modal, setModal, currentUser, … } versorgt.

View Zeilen Zweck Komplexität
Projects.jsx 1781 List + Detail, SIA-Phasen, Quoten-Zuordnung, Nachträge Sehr hoch
Time.jsx 1538 Wochen-Grid mit Drag&Drop, Tag/Woche/Monat, Absenzen Sehr hoch
Invoices.jsx 1467 Rechnungen, Akonto, QR-Bill, Mahnungen, Planer Sehr hoch
Employees.jsx 1298 Multi-Tab: Stammdaten, Absenzen, Ferien, Lohnabschluss Sehr hoch
Quotes.jsx 980 Offerten: SIA / manuell / frei, Übernahme als Projekt Hoch
Protocols.jsx 978 Sitzungsprotokolle (Beschluss/Info/Aufgabe), Mahnung-Modul Hoch
Expenses.jsx 914 Mitarbeiter-Spesen + interne Ausgaben, Bild-Upload Hoch
Settings.jsx 869 7 Tabs: Studio, Dokumente, Team, Kalender, System, Support, Profil Sehr hoch
StudioBudget.jsx 847 Revenue-Sparklines, Aggregation Rechnungen/Quoten Hoch
Dashboard.jsx 762 Drag&Drop Widget-Layout, Template-System Hoch
Persons.jsx 682 Kunden + Partner (seit v0.5 vereint) Mittel
Setup.jsx 423 7-Step-Wizard für Neuinstallation Mittel
Pinboard.jsx 417 Blog-artige Notizen, kategorisiert Mittel
Accounting.jsx 374 CSV-Export, Jahreszahlen, MwSt-Berechnung Mittel
Payroll.jsx 344 Monats-Lohnzettel, BVG-Sätze, Abzüge Mittel
DeliveryNotes.jsx 294 Lieferscheine Niedrig
Login.jsx 197 Login + Brute-Force-Lockout Niedrig
Documents.jsx 194 Router zu Protokolle/Lieferscheine/Briefe Niedrig
MigrationScreen.jsx 141 v0.5-Migration-Wizard (wenn alte Daten erkannt) Niedrig
Letters.jsx 114 Brieftemplates mit Placeholdern ({{client}}, …) Niedrig

6. utils.js — Business-Logik-Bibliothek

utils.js ist isoliert und gut testbar. Die wichtigsten Gruppen:

Kalkulation:

  • calcSIAHours(baukosten, schwierigkeit, phasen) — SIA-102-Formel p = Z1 + Z2/∛B
  • calcManualHours(phases, roles) — Stundenansatz × Stunden pro Rolle
  • deriveQuoteBudget(linkedQuotes, allQuotes, roles) — Aggregiert Offerten zu Projekt-Budget
  • calcLohn(emp, monat, spesen, bonus) — AHV/ALV/BVG/NBU/KTG/Quellensteuer

Crypto / Auth:

  • hashPassword(password, saltHex) — PBKDF2-SHA-256, 100k Iter, via Web Crypto
  • verifyPassword(password, user) — Constant-Time
  • withHashedPassword(user, password) — Upgrade legacy → hashed
  • stripCredentials(user) — Entfernt password, passwordHash, passwordSalt

Sicherheit:

  • sanitizeHtml(html) — Allowlist (<p>, <br>, <b>, <a>, …), blockiert <script>, javascript:, on*-Handler

Formatierung (de-CH):

  • formatCHF(amount)"CHF 1'234.50"
  • formatDate(iso)"14. Mai 2025"
  • roundCHF(amount) → 5-Rappen-Rundung
  • formatHours(minutes)"2h 30m"

Schweizer QR-Rechnung:

  • isQRIban(iban) — IID-Range 3000031999
  • formatIban(iban) → 4er-Blöcke
  • generateQRReference(invoiceNumber) → 27-stellige Referenz mit Mod10-Prüfziffer
  • mod10(input) — Schweizer Modulo-10-Algorithmus

Templates / Nummerngenerierung:

  • applyProjectNumberFormat, applyProtoNumberFormat — Template-Syntax wie {YYYY}/{NN}
  • parseSeqFromNumber, nextProtoSeq
  • buildReminderLetter(inv, nr, …) — Mahnungstexte (1./2./3. Mahnung)
  • buildPdfName(format, content, settings) — Sanitierter Dateiname

Sonstiges:

  • exportBuchhaltungCSV(data, year) — Voller Jahresexport
  • migrateDashboardLayout(val) — Alte Widget-IDs → Row-basiertes Layout
  • getFeiertageForYear, getWorkdaysInMonth, getSollStunden

7. Print-Modul

src/print/PrintComponents.jsx (~1200 Zeilen) exportiert <PrintView>, das via setPrintContent(...) aus App.jsx getriggert wird.

Content-Typen: invoice, invoice+qr, qrbill, quote, letter, lieferschein, protokoll, lohn, studioBudget, buchhaltung, projectDetail, projectsOverview, mitarbeiterOverview, timeReport.

Druck-Trigger: getCurrentWebviewWindow().print() (Tauri WebView) oder Fallback window.print() (Browser).

Schweizer QR-Rechnung: Lib swissqrbill (lokal installiert), erzeugt SVG für 100% akkuraten Druck. Format: 105mm × 210mm, separate @page-Regel.

Styles: Inline + @page, print-color-adjust: exact. Margins konfigurierbar über settings.pageMargin{Top,Bottom,Left,Right}.


8. Rust-Backend (src-tauri/src/lib.rs)

Alles in 103 Zeilen. Keine #[tauri::command]. Kein Filesystem, kein HTTP, keine DB.

Was er macht:

  1. System-Tray mit 5 Nav-Items (nav:dashboard, nav:time, nav:projects, nav:buchhaltung, nav:settings) + show + quit (lib.rs:47-60)
  2. Tray-Click → Fenster anzeigen + fokussieren (lib.rs:81-90)
  3. Tray-Nav-Clickemit("rapport:navigate", "<view>") ans Frontend (lib.rs:77)
  4. Window-Close (X) → Hide statt Quit, gesteuert durch Arc<AtomicBool> is_quitting (lib.rs:25-35)
  5. Plugins registrieren: updater, process (für Relaunch nach Update), log (nur Debug)

Frontend lauscht in App.jsx:191:

listen("rapport:navigate", (event) => setView(event.payload))

Capabilities (src-tauri/capabilities/default.json) — bewusst minimal:

  • core:default
  • core:webview:allow-print (für window.print())
  • updater:default
  • process:allow-restart (für Relaunch nach Update)
  • Nichts für fs:*, shell:*, http:*, dialog:*, clipboard:*

Tauri-Plugins (Cargo.toml):

  • tauri-plugin-updater v2
  • tauri-plugin-process v2
  • tauri-plugin-log v2
  • serde 1.0, serde_json 1.0

Bekannte Fragilitäten:

  • app.default_window_icon().unwrap() — panicked, wenn Icon fehlt (lib.rs:64)
  • Hardcoded "main"-Label für Window, Hardcoded "nav:"-Prefix — wenn Frontend Konventionen ändert, bricht Tray
  • Keine Tests in Rust

9. Updater-Pipeline End-to-End

release.sh (lokal, manuell aufgerufen)
  ├─ liest VERSION aus tauri.conf.json + package.json (Mismatch → Exit)
  ├─ ⚠️ Cargo.toml wird NICHT geprüft
  ├─ Lädt Private Key aus ~/.tauri/rapport_updater.key (kein Passwort)
  ├─ npx tauri build   (mit TAURI_SIGNING_PRIVATE_KEY env)
  ├─ findet .app.tar.gz + .sig in src-tauri/target/release/bundle/macos/
  └─ schreibt latest.json (Repo-Root) mit version, signature, url, pub_date
     └─ url zeigt auf Gitea Release Asset (manuell hochzuladen)

User (manuell):
  ├─ Gitea-Webinterface: Release mit Tag <VERSION> (ohne v-Prefix) erstellen
  ├─ .app.tar.gz (+ optional .dmg) hochladen
  └─ git add latest.json && git commit && git push origin main

App-Start (in jeder installierten Version):
  ├─ UpdateNotifier.jsx: setTimeout 1.5s → checkForAppUpdate({ silent: true })
  ├─ Tauri-Plugin GET https://git.kgva.ch/karim/RAPPORT/raw/branch/main/latest.json
  ├─ Verifiziert Signature gegen pubkey aus tauri.conf.json (Minisign)
  ├─ Vergleicht latest.json.version mit getVersion()
  └─ Wenn neuer → Modal mit "Installieren / Später / Diese Version überspringen"

Installation:
  ├─ update.downloadAndInstall(onProgress)  // lädt von url in latest.json
  └─ relaunch() (via plugin-process)

Updater-Komponenten im Frontend:

Aktueller latest.json: nur darwin-aarch64 (Apple Silicon). Kein Intel-Build, kein Windows-Build, kein Linux-Build. Wer auf x86_64-Mac oder anderem OS installiert, bekommt keine Updates.

Signatur-Setup (Minisign):

  • Private Key: ~/.tauri/rapport_updater.key (User-Home, niemals im Repo, gitignored via *.key)
  • Public Key: base64 in tauri.conf.jsonplugins.updater.pubkey
  • Kein Passwort (TAURI_SIGNING_PRIVATE_KEY_PASSWORD="")

10. Build & Release-Workflow

Versions-Bump betrifft drei Dateien — alle drei müssen synchron sein:

  1. package.json"version"
  2. src-tauri/tauri.conf.json"version"
  3. src-tauri/Cargo.toml[package] version

Zusätzlich für jeden Release: 4. src/App.jsx → Changelog-Entry in CHANGELOGS-Array (hardcoded in JSX) 5. src/App.jsxrapport_changelog_seen-Vergleichswert (im Changelog-Modal-Close-Handler)

⚠️ release.sh prüft nur 1+2. Cargo.toml-Mismatch bleibt unbemerkt.

Dev-Workflow:

npm run dev          # Vite-Server auf http://localhost:3000
npx tauri dev        # Native Window + HMR
npm run lint         # ESLint (manuell — kein Pre-Commit-Hook)

Release-Workflow:

# 1. Versionen in package.json, tauri.conf.json, Cargo.toml + Changelog-Entry hochziehen
# 2. Commit
# 3. Release-Script:
./scripts/release.sh
# 4. In Gitea-UI: Release <VERSION> erstellen (Tag OHNE v-Prefix — latest.json-URL nutzt /<VERSION>/), .app.tar.gz hochladen
# 5. git add latest.json && git commit -m "Release X.Y.Z" && git push origin main
# 6. git tag -a <VERSION> -m "..." && git push origin <VERSION>

Die README.md-Release-Sektion erwähnt scripts/release.sh nicht und ist veraltet.


11. Konventionen

Sprache:

  • UI-Strings: Deutsch ("Zeiterfassung", "Buchhaltung", "Beenden")
  • Code-Identifier: Englisch (isQuitting, setView, currentUser)
  • Wenig Inline-Kommentare — wenn vorhanden, meist Deutsch

Naming:

  • Komponenten/Views: PascalCase, eine Datei = ein Default-Export (ggf. mit Named-Exports für Sub-Views)
  • Utils: camelCase
  • Dateien: PascalCase für Components/Views, lowercase für constants/utils

Styling:

  • Inline-Styles dominieren (über 200 in Invoices.jsx allein)
  • Globale Klassen: .btn, .card, .pill, .filter-bar, .modal — definiert im <style>-Block in App.jsx
  • CSS-Variablen für Theming: --bg, --text, --border, … (Dark Mode via data-theme-Attribut)
  • Kein Tailwind, kein CSS-Module, kein styled-components

ESLint (eslint.config.js): Flat-Config mit js.configs.recommended, reactHooks.configs.flat.recommended, reactRefresh.configs.vite. Kein Prettier, kein Husky, kein lint-staged.

Imports: Stdlib oben (React), dann Constants/Utils, dann lokale Components. Keine Pfad-Aliase (~/, @/ werden nicht verwendet — relative Pfade ../foo).


12. Wo es weh tut — Realistische Schwachstellen

  1. Vier "God Components" über 1200 Zeilen (Projects, Time, Invoices, Employees) — Refactoring riskant ohne Tests, Sub-Komponenten sind intern definiert statt extrahiert.
  2. App.jsx ist 823 Zeilen und macht: Auth, State, Migration, Sidebar, Modals, Changelog, About, Print-Routing, Hotkeys, Navigation-History, Theme. Jede Änderung an App.jsx ist hochriskant — sie betrifft alles.
  3. Inline-Styles ohne Konvention — Spacing/Farben sind über das Projekt verstreut, kein Design-Token-System.
  4. Modal-State chaotisch — manche Views haben {type,id}, andere mehrere useState. Kein zentraler Manager.
  5. Keine Tests. Nichts. Kein Vitest, kein Cypress, kein Rust-Test. Kalkulationen in utils.js wären leicht testbar.
  6. Kein TypeScript. Bei 18k Zeilen JSX ohne Types ist jedes Schema-Refactor Risiko.
  7. Kein Error-Boundary — wenn eine lazy-geladene View crasht, weißer Screen.
  8. localStorage ohne Schema-Validierung — korrupte Daten crashen im Render.
  9. Keine CI, keine Pre-Commit-Hooks. Linting muss man sich selbst merken.
  10. Updater nur für Apple Silicon — wenn User x86_64-Mac/Windows/Linux hat, kein Update.
  11. README-Release-Sektion veraltet — erwähnt scripts/release.sh nicht.
  12. release.sh prüft Cargo.toml-Version nicht — Inkonsistenz bleibt unbemerkt.
  13. .unwrap() im Tray-Icon-Load in lib.rs:64 — Startup-Panic möglich, wenn Icon fehlt.

13. Wenn-du-anfasst-Hinweise

Bereich Risiko Notiz
App.jsx State/Auth/Migration Sehr hoch Touch nur mit klarem Auftrag, betrifft alles
constants.js defaultData Shape Hoch Schema-Änderung erfordert Migration (siehe Beispiele in App.jsx:56-122)
utils.js Kalkulationen Hoch Ohne Tests — Änderung an calcSIAHours, calcLohn, generateQRReference, mod10 → manuell durchrechnen
print/PrintComponents.jsx Hoch SwissQR-Bill ist Pixel-genau — Layout-Bugs sichtbar erst im Druck
Views (Invoices/Projects/Time) Hoch Lange Files mit Edge-Cases (Mahnung, Akonto, Drag&Drop)
Settings.jsx Permissions Hoch Tangiert Rollen/Berechtigungen, Dashboard-Templates
Login.jsx Hash-Logik Hoch PBKDF2 + Migration, sicherheitsrelevant
storage/adapter.js Hoch Einzige Schreibstelle für studio_data_v1. API ist async (Promise) — Direkt-Zugriffe auf localStorage[STORAGE_KEY] in Views sind verboten
storage/migrations.js Hoch Schema-Migrations. Jede Änderung am defaultData-Shape erfordert hier eine Migration-Step + Test mit alten Snapshots
storage/supabase-adapter.js Hoch Cloud-Read/Write, Auth, Realtime. Bei neuer Tabelle: load() + save() + Mapper anpassen + Migration anlegen + Realtime-Publication ergänzen
storage/supabase-mappers.js Hoch fromDB ↔ toDB pro Entity. Snake/Camel-Konvention; JSONB-Spread bei settings; isShared-Flag für geteilte Personen
supabase/migrations/ Hoch Cloud-Schema (29 Tabellen + RPCs). Via supabase db reset lokal anwendbar. Schema-Änderungen brauchen neue Migration-Datei, kein Bearbeiten bestehender
views/CloudSetup.jsx Mittel Erst-Einrichtung der Cloud (3 Schritte). Ruft cloudInit in App.jsx
views/BackendChoice.jsx Niedrig Modus-Wahl Lokal/Cloud bei frischer Installation
views/ResetPassword.jsx Mittel Passwort-Reset nach Mail-Link-Klick. Hängt am PASSWORD_RECOVERY-Event
deploy/ + DEPLOY.md Niedrig Nginx-Container + Anleitung. Reine Doku/Hosting-Files
lib.rs Tray/Window Mittel Wenn Nav-IDs geändert werden, müssen Frontend + Rust synchron bleiben
tauri.conf.json Updater Sehr hoch Public Key ändern bricht alle bestehenden Installationen
release.sh Sehr hoch Falsche Änderung → defekte Updates beim User
Neue Util / neue View Niedrig Isoliert, safe — kopiere bestehende, entferne was du nicht brauchst

14. Offene Fragen / Nicht-Validiertes

  • Wo werden Bild-Uploads (Receipts in Expenses, Logo in Settings) gespeichert? Vermutlich Base64 in data → wächst localStorage unkontrolliert.
  • Wie groß darf data werden, bevor localStorage (510 MB Limit) bricht? Aktuell ohne Monitoring.
  • PDF-Export: aktuell nur window.print() → User-PDF-Dialog. Kein direkter File-Save.
  • Multi-User-Workflow: users[] in data, aber nur ein Browser-localStorage → keine echte Mehrfachnutzung.