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

441 lines
27 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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](src/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](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](src/storage/supabase-adapter.js)) — Cloud via Postgres + REST + Realtime; pro `data.*`-Array eine eigene Tabelle (Mappers in [supabase-mappers.js](src/storage/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](src/App.jsx)) 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](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](src/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](src/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](src/views/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](src/utils.js)
- Lockout: 5 Fehlversuche → 60s Sperre ([Login.jsx](src/views/Login.jsx))
- Auto-Upgrade: legacy plaintext-Passwörter werden beim ersten erfolgreichen Login zu PBKDF2 migriert ([App.jsx:143-161](src/App.jsx#L143-L161))
**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/](src/views/), werden in [App.jsx](src/App.jsx) per `React.lazy()` geladen und über Props `{ data, update, saveAll, modal, setModal, currentUser, … }` versorgt.
| View | Zeilen | Zweck | Komplexität |
|---|---:|---|---|
| [Projects.jsx](src/views/Projects.jsx) | 1781 | List + Detail, SIA-Phasen, Quoten-Zuordnung, Nachträge | **Sehr hoch** |
| [Time.jsx](src/views/Time.jsx) | 1538 | Wochen-Grid mit Drag&Drop, Tag/Woche/Monat, Absenzen | **Sehr hoch** |
| [Invoices.jsx](src/views/Invoices.jsx) | 1467 | Rechnungen, Akonto, QR-Bill, Mahnungen, Planer | **Sehr hoch** |
| [Employees.jsx](src/views/Employees.jsx) | 1298 | Multi-Tab: Stammdaten, Absenzen, Ferien, Lohnabschluss | **Sehr hoch** |
| [Quotes.jsx](src/views/Quotes.jsx) | 980 | Offerten: SIA / manuell / frei, Übernahme als Projekt | Hoch |
| [Protocols.jsx](src/views/Protocols.jsx) | 978 | Sitzungsprotokolle (Beschluss/Info/Aufgabe), Mahnung-Modul | Hoch |
| [Expenses.jsx](src/views/Expenses.jsx) | 914 | Mitarbeiter-Spesen + interne Ausgaben, Bild-Upload | Hoch |
| [Settings.jsx](src/views/Settings.jsx) | 869 | 7 Tabs: Studio, Dokumente, Team, Kalender, System, Support, Profil | **Sehr hoch** |
| [StudioBudget.jsx](src/views/StudioBudget.jsx) | 847 | Revenue-Sparklines, Aggregation Rechnungen/Quoten | Hoch |
| [Dashboard.jsx](src/views/Dashboard.jsx) | 762 | Drag&Drop Widget-Layout, Template-System | Hoch |
| [Persons.jsx](src/views/Persons.jsx) | 682 | Kunden + Partner (seit v0.5 vereint) | Mittel |
| [Setup.jsx](src/views/Setup.jsx) | 423 | 7-Step-Wizard für Neuinstallation | Mittel |
| [Pinboard.jsx](src/views/Pinboard.jsx) | 417 | Blog-artige Notizen, kategorisiert | Mittel |
| [Accounting.jsx](src/views/Accounting.jsx) | 374 | CSV-Export, Jahreszahlen, MwSt-Berechnung | Mittel |
| [Payroll.jsx](src/views/Payroll.jsx) | 344 | Monats-Lohnzettel, BVG-Sätze, Abzüge | Mittel |
| [DeliveryNotes.jsx](src/views/DeliveryNotes.jsx) | 294 | Lieferscheine | Niedrig |
| [Login.jsx](src/views/Login.jsx) | 197 | Login + Brute-Force-Lockout | Niedrig |
| [Documents.jsx](src/views/Documents.jsx) | 194 | Router zu Protokolle/Lieferscheine/Briefe | Niedrig |
| [MigrationScreen.jsx](src/views/MigrationScreen.jsx) | 141 | v0.5-Migration-Wizard (wenn alte Daten erkannt) | Niedrig |
| [Letters.jsx](src/views/Letters.jsx) | 114 | Brieftemplates mit Placeholdern (`{{client}}`, …) | Niedrig |
---
## 6. utils.js — Business-Logik-Bibliothek
[utils.js](src/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](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](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](src-tauri/src/lib.rs#L47-L60))
2. **Tray-Click** → Fenster anzeigen + fokussieren ([lib.rs:81-90](src-tauri/src/lib.rs#L81-L90))
3. **Tray-Nav-Click**`emit("rapport:navigate", "<view>")` ans Frontend ([lib.rs:77](src-tauri/src/lib.rs#L77))
4. **Window-Close (X)** → Hide statt Quit, gesteuert durch `Arc<AtomicBool> is_quitting` ([lib.rs:25-35](src-tauri/src/lib.rs#L25-L35))
5. **Plugins registrieren:** `updater`, `process` (für Relaunch nach Update), `log` (nur Debug)
**Frontend lauscht** in [App.jsx:191](src/App.jsx#L191):
```js
listen("rapport:navigate", (event) => setView(event.payload))
```
**Capabilities** ([src-tauri/capabilities/default.json](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](src-tauri/src/lib.rs#L64))
- 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:**
- [src/utils/updater.js](src/utils/updater.js) (49 Z.) — Wrapper, kapselt Skip-Logik in `localStorage`
- [src/components/UpdateNotifier.jsx](src/components/UpdateNotifier.jsx) (163 Z.) — Auto-Check beim Start, Modal mit Progress-Bar
- [src/components/UpdatesSupport.jsx](src/components/UpdatesSupport.jsx) (197 Z.) — Settings-Tab "Updates & Support", manueller Check, ignoriert Skip
- Custom DOM-Event: `window.dispatchEvent(new CustomEvent("rapport:check-update"))` — UpdatesSupport triggert manuell
**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.json``plugins.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](package.json) → `"version"`
2. [src-tauri/tauri.conf.json](src-tauri/tauri.conf.json) → `"version"`
3. [src-tauri/Cargo.toml](src-tauri/Cargo.toml) → `[package] version`
**Zusätzlich für jeden Release:**
4. [src/App.jsx](src/App.jsx) → Changelog-Entry in `CHANGELOGS`-Array (hardcoded in JSX)
5. [src/App.jsx](src/App.jsx) → `rapport_changelog_seen`-Vergleichswert (im Changelog-Modal-Close-Handler)
> ⚠️ `release.sh` prüft nur 1+2. **Cargo.toml-Mismatch bleibt unbemerkt.**
**Dev-Workflow:**
```bash
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:**
```bash
# 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](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](src/views/Invoices.jsx) allein)
- Globale Klassen: `.btn`, `.card`, `.pill`, `.filter-bar`, `.modal` — definiert im `<style>`-Block in [App.jsx](src/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](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](src/views/Projects.jsx), [Time](src/views/Time.jsx), [Invoices](src/views/Invoices.jsx), [Employees](src/views/Employees.jsx)) — 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](src-tauri/src/lib.rs#L64) — 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.