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>
27 KiB
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 PromisesSupabaseAdapter(supabase-adapter.js) — Cloud via Postgres + REST + Realtime; prodata.*-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ßendata-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
localStoragekorrupt 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
selectedProjectIdetc. (zweiter State nebenview)
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,verifyPasswordin 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-Formelp = Z1 + Z2/∛BcalcManualHours(phases, roles)— Stundenansatz × Stunden pro RollederiveQuoteBudget(linkedQuotes, allQuotes, roles)— Aggregiert Offerten zu Projekt-BudgetcalcLohn(emp, monat, spesen, bonus)— AHV/ALV/BVG/NBU/KTG/Quellensteuer
Crypto / Auth:
hashPassword(password, saltHex)— PBKDF2-SHA-256, 100k Iter, via Web CryptoverifyPassword(password, user)— Constant-TimewithHashedPassword(user, password)— Upgrade legacy → hashedstripCredentials(user)— Entferntpassword,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-RundungformatHours(minutes)→"2h 30m"
Schweizer QR-Rechnung:
isQRIban(iban)— IID-Range 30000–31999formatIban(iban)→ 4er-BlöckegenerateQRReference(invoiceNumber)→ 27-stellige Referenz mit Mod10-Prüfziffermod10(input)— Schweizer Modulo-10-Algorithmus
Templates / Nummerngenerierung:
applyProjectNumberFormat,applyProtoNumberFormat— Template-Syntax wie{YYYY}/{NN}parseSeqFromNumber,nextProtoSeqbuildReminderLetter(inv, nr, …)— Mahnungstexte (1./2./3. Mahnung)buildPdfName(format, content, settings)— Sanitierter Dateiname
Sonstiges:
exportBuchhaltungCSV(data, year)— Voller JahresexportmigrateDashboardLayout(val)— Alte Widget-IDs → Row-basiertes LayoutgetFeiertageForYear,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:
- System-Tray mit 5 Nav-Items (
nav:dashboard,nav:time,nav:projects,nav:buchhaltung,nav:settings) +show+quit(lib.rs:47-60) - Tray-Click → Fenster anzeigen + fokussieren (lib.rs:81-90)
- Tray-Nav-Click →
emit("rapport:navigate", "<view>")ans Frontend (lib.rs:77) - Window-Close (X) → Hide statt Quit, gesteuert durch
Arc<AtomicBool> is_quitting(lib.rs:25-35) - 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:defaultcore:webview:allow-print(fürwindow.print())updater:defaultprocess:allow-restart(für Relaunch nach Update)- Nichts für
fs:*,shell:*,http:*,dialog:*,clipboard:*
Tauri-Plugins (Cargo.toml):
tauri-plugin-updaterv2tauri-plugin-processv2tauri-plugin-logv2serde1.0,serde_json1.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:
- src/utils/updater.js (49 Z.) — Wrapper, kapselt Skip-Logik in
localStorage - src/components/UpdateNotifier.jsx (163 Z.) — Auto-Check beim Start, Modal mit Progress-Bar
- 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:
- package.json →
"version" - src-tauri/tauri.conf.json →
"version" - 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.jsx → rapport_changelog_seen-Vergleichswert (im Changelog-Modal-Close-Handler)
⚠️
release.shprü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.shnicht 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 viadata-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
- Vier "God Components" über 1200 Zeilen (Projects, Time, Invoices, Employees) — Refactoring riskant ohne Tests, Sub-Komponenten sind intern definiert statt extrahiert.
- 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.
- Inline-Styles ohne Konvention — Spacing/Farben sind über das Projekt verstreut, kein Design-Token-System.
- Modal-State chaotisch — manche Views haben
{type,id}, andere mehrereuseState. Kein zentraler Manager. - Keine Tests. Nichts. Kein Vitest, kein Cypress, kein Rust-Test. Kalkulationen in
utils.jswären leicht testbar. - Kein TypeScript. Bei 18k Zeilen JSX ohne Types ist jedes Schema-Refactor Risiko.
- Kein Error-Boundary — wenn eine lazy-geladene View crasht, weißer Screen.
localStorageohne Schema-Validierung — korrupte Daten crashen im Render.- Keine CI, keine Pre-Commit-Hooks. Linting muss man sich selbst merken.
- Updater nur für Apple Silicon — wenn User x86_64-Mac/Windows/Linux hat, kein Update.
- README-Release-Sektion veraltet — erwähnt
scripts/release.shnicht. release.shprüft Cargo.toml-Version nicht — Inkonsistenz bleibt unbemerkt..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ächstlocalStorageunkontrolliert. - Wie groß darf
datawerden, bevorlocalStorage(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[]indata, aber nur ein Browser-localStorage → keine echte Mehrfachnutzung.