27b1057cd4
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>
441 lines
27 KiB
Markdown
441 lines
27 KiB
Markdown
# 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 30000–31999
|
||
- `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` (5–10 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.
|