Files
RAPPORT/ARCHITECTURE.md
T
karim c71feddf63 Doku & Aufräumen: CLAUDE.md/ARCHITECTURE.md, Tag-Schema, Legacy-Views weg
CLAUDE.md (Kurzform: was zu tun/lassen ist) und ARCHITECTURE.md
(vollständige Repo-Karte mit Verzeichnis, Datenfluss, View-Inventar,
Updater-Pipeline, Schwachstellen) als neue Onboarding-Dokumente.

Tag-Schema in Doku und Skript-Kommentar an die tatsächliche Konvention
angeglichen: Gitea-Tag ohne v-Prefix (latest.json-URL nutzt
/releases/download/<VERSION>/). Betrifft scripts/release.sh, README.md
und ARCHITECTURE.md §9+§10.

Legacy-Views Contacts.jsx und Clients.jsx entfernt — durch Persons.jsx
ersetzt, in NAV_ITEMS nicht mehr verlinkt, kein Import mehr im Code.
ARCHITECTURE.md §5/§12/§14 entsprechend aktualisiert.

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

22 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.]
│   ├── 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
│
├── 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)
                              localStorage.setItem("studio_data_v1", JSON.stringify(newData))
                                     │
                                     ▼
                              React re-rendert die Hierarchie

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