# Toollist – Design-Dokumentation Dieses Dokument hält die Hintergründe und Abwägungen fest, die während der Planungsphase getroffen wurden. Es dient als Nachschlagewerk, wenn zukünftige Erweiterungen Fragen über den ursprünglichen Kontext aufwerfen. --- ## 1. Kontext & Ziel Das Tool entstand aus einer einfachen Textdatei (`toollist_notes.txt`), in der Werkzeuge für eine Haus-Renovierung grob notiert waren – ohne einheitliche Struktur, ohne Status-Tracking, ohne Möglichkeit für mehrere Personen gleichzeitig darauf zuzugreifen. **Kernziel:** Eine einfache, im Heimnetz zugängliche Web-App, mit der mehrere Personen gemeinsam verwalten können, welche Werkzeuge für die Renovierung benötigt werden, wie sie beschafft werden und ob sie bereits organisiert sind. --- ## 2. Technologie-Entscheidungen ### 2.1 Warum Full-Stack Rust mit Leptos? Das Projekt startete als Rust-Crate (`Cargo.toml` war bereits vorhanden). Für ein Web-Tool mit mehreren Nutzern wurden drei Optionen verglichen: | Option | Beschreibung | Verworfen weil | |---|---|---| | **CLI** | Befehle im Terminal | Kein Mehrbenutzerzugriff, keine Übersicht | | **TUI** | Interaktive Terminal-UI | Kein Netzwerkzugriff ohne zusätzlichen Server | | **Web** | Browser-basiert | ✅ Gewählt | Innerhalb der Web-Option: | Option | Beschreibung | Entscheidung | |---|---|---| | Axum + Vanilla JS | Rust-Backend, JS-Frontend | Zwei Sprachen, mehr Aufwand | | Axum + React/Svelte | Beste UX, zwei Codebasen | Zu aufwändig für ein internes Tool | | **Leptos SSR** | Full-Stack Rust, WASM | ✅ Gewählt – alles in einer Sprache | **Leptos SSR + Hydration** wurde gewählt, weil: - Alles bleibt in Rust (Typsicherheit zwischen Server und Client) - Server-seitiges Rendering für den initialen Load - Die Reaktivität (Signale, Ressourcen) ersetzt ein separates State-Management ### 2.2 Warum TOML als Datenspeicher? Es wurden drei Optionen diskutiert: | Option | Vorteile | Nachteile | |---|---|---| | **TOML-Datei** | Menschenlesbar, versionierbar mit Git, kein DB-Server | Kein Query-Layer, Concurrent Writes brauchen Locking | | JSON-Datei | Besser für komplexe Typen, weit verbreitet | Weniger lesbar als TOML | | SQLite | Besser für Abfragen, bewährt für gleichzeitigen Zugriff | Overhead für ein kleines internes Tool | **TOML wurde gewählt**, da die Datei direkt lesbar und editierbar sein soll, das Projekt nur für wenige Nutzer im Heimnetz ausgelegt ist (kein Hochlast-Concurrent-Access), und die Werkzeug-Liste manuell wartbar bleiben soll. > **Bekannte Einschränkung:** Bei gleichzeitigen Schreibzugriffen mehrerer > Nutzer gibt es kein Locking. Für den Heimnetz-Einsatz mit wenigen Personen > ist das akzeptabel. Bei Bedarf kann ein `tokio::sync::Mutex` in den Axum-State > eingebaut werden. ### 2.3 Warum kein Login / keine Auth? Bewusste Entscheidung: Die App ist ausschließlich im lokalen Heimnetz erreichbar und wird von einer kleinen, bekannten Personengruppe genutzt. Eine Nutzerverwaltung würde den Aufwand signifikant erhöhen, ohne einen echten Mehrwert für diesen Kontext zu liefern. Das Feld `verantwortlich` (wer kümmert sich um ein Werkzeug) ist ein Freitextfeld – kein Verweis auf einen eingeloggten Nutzer. --- ## 3. Datenmodell – Designentscheidungen ### 3.1 Werkzeug-Felder **Kernfelder** (aus der ursprünglichen Anforderung): | Feld | Entscheidung | Begründung | |---|---|---| | `name` | `String` | Freitext, keine Normalisierung nötig | | `anzahl` | `Anzahl`-Struct | Entweder konkrete Zahl oder „Mehrere" (z.B. bei Eimern, Handschuhen) | | `beschaffung` | Flache `Beschaffung`-Struct | Kontextabhängige Zusatzfelder je nach Art (s.u.) | | `verwendung` | `Vec` | Mehrere Verwendungszwecke möglich | | `tags` | `Vec` | Flexible Kategorisierung ohne fixe Taxonomie | **Zusatzfelder** (im Design-Gespräch ergänzt): | Feld | Entscheidung | Begründung | |---|---|---| | `status` | Enum `Offen/Organisiert/VorOrt` | Wichtigster Fortschrittsindikator auf einen Blick | | `prioritaet` | Enum `Hoch/Mittel/Niedrig` | Fokussierung auf kritische Werkzeuge | | `notizen` | `Option` | Freitext für alles was sonst nirgends passt | | `verantwortlich` | `Option` | Optional – wer beschafft das Werkzeug | | `erstellt_am` | `String` (ISO-8601) | Nützlich für Sortierung und Nachvollziehbarkeit | **Bewusst nicht aufgenommen (beim MVP):** - Bauabschnitt als eigenes Feld → wird über Tags abgebildet (z.B. `tag: estrich`, `tag: elektro`) - Vollständiges Changelog / Audit-Log → zu viel Overhead für diesen Kontext ### 3.2 Warum `Anzahl` als Struct statt Enum? Ursprünglich war ein Enum geplant: ```rust // Erstentwurf (verworfen) enum Anzahl { Zahl(u32), Mehrere, } ``` **Problem:** TOML serialisiert gemischte Enum-Varianten (eine als Zahl, eine als String) je nach Variante unterschiedlich, was zu Serialisierungsfehlern führen kann – insbesondere innerhalb von `[[tools]]`-Arrays, wo alle Felder als Inline-Tables dargestellt werden. **Lösung:** Eine flache Struct mit optionalem Zahlenfeld: ```rust struct Anzahl { zahl: Option, // None = "Mehrere" } ``` TOML-Darstellung ist damit eindeutig: ```toml # Konkrete Zahl: [tools.anzahl] zahl = 2 # "Mehrere": [tools.anzahl] # (kein zahl-Feld → None) ``` ### 3.3 Warum `Beschaffung` als flache Struct statt Enum? Ursprünglicher Plan: ```rust // Erstentwurf (verworfen) enum Beschaffung { Unbekannt, InBesitz, Leihen { von: String }, Mieten { wo: String, preis: Option }, Kaufen { wo: String, preis: Option }, } ``` **Problem:** Rust-Enums mit Struct-Varianten und TOML-Serialisierung können sich je nach verwendetem Serde-Tag-Modus (`untagged`, `tag`, extern) unterschiedlich verhalten und sind fehleranfällig, besonders mit `Option`-Feldern in Varianten. **Lösung:** Flache Struct mit einem `BeschaffungsArt`-Enum für den Typ und separaten optionalen Feldern: ```rust struct Beschaffung { art: BeschaffungsArt, // "Unbekannt" | "InBesitz" | "Leihen" | "Mieten" | "Kaufen" von: Option, // nur für Leihen wo: Option, // nur für Mieten/Kaufen preis: Option, // nur für Mieten/Kaufen } ``` **Vorteil:** Einfache, TOML-kompatible Serialisierung. Im Formular werden die Felder `von`, `wo`, `preis` je nach gewählter `art` dynamisch ein-/ausgeblendet. **Bekannter Trade-off:** Nicht alle Felddkombinationen sind sinnvoll (z.B. `von` bei `Kaufen`). Die Validierung liegt beim Formular, nicht im Typ selbst. ### 3.4 Warum `String` für `id` und `erstellt_am`? Für `id` wäre `uuid::Uuid` typsicherer. Für `erstellt_am` wäre `chrono::DateTime` korrekter. Beide Typen haben jedoch **WASM-Kompatibilitätsprobleme**: - `uuid::Uuid` mit dem `v4`-Feature benötigt `getrandom`, das für WASM die `js`-Feature-Flag braucht. - `chrono::Utc::now()` benötigt für WASM die `wasmbind`-Feature-Flag. Da beide Werte **ausschließlich serverseitig generiert** werden (in Server Functions) und der Client sie nur als opaque Strings liest, ist `String` die pragmatische Wahl: ```rust // In Tool::neu() – nur SSR #[cfg(feature = "ssr")] impl Tool { pub fn neu(input: NewTool) -> Self { Tool { id: uuid::Uuid::new_v4().to_string(), erstellt_am: chrono::Utc::now().to_rfc3339(), // ... } } } ``` `uuid` und `chrono` sind daher als **optionale, SSR-only** Dependencies deklariert. --- ## 4. Architektur-Entscheidungen (Leptos) ### 4.1 SSR-Rendering-Modus Leptos unterstützt verschiedene Rendering-Modi: | Modus | Beschreibung | Entscheidung | |---|---|---| | CSR | Nur WASM, kein Server-Rendering | Kein Dateisystemzugriff möglich | | **SSR + Hydration** | Server rendert HTML, WASM übernimmt | ✅ Gewählt | | Islands | Nur bestimmte Komponenten hydratisieren | Komplexer, kein Vorteil hier | SSR ist notwendig, weil der Server die TOML-Datei lesen muss. Mit CSR müsste ein separater API-Server betrieben werden. ### 4.2 Server Functions statt REST-API Leptos `#[server]`-Funktionen generieren automatisch: - Serverseitig: die echte Implementierung - Clientseitig: einen HTTP-Stub, der den Server aufruft Das erspart eine manuelle REST-API mit separaten Routen, Serialisierung und Client-Code. Alle Typen werden geteilt – wenn sich das Datenmodell ändert, bricht der Compiler sowohl Server- als auch Client-Code. ### 4.3 Suspense-Grenze für Resource-Zugriffe **Problem:** In Leptos 0.8 dürfen `Resource`-Zugriffe in Hydration-Mode ausschließlich innerhalb einer ``-Komponente erfolgen. Anderenfalls drohen Hydration-Mismatches und eine Warnung im Log. **Falsch (ursprünglicher Ansatz):** ```rust // Außerhalb Suspense → Warnung let alle_tags = Signal::derive(move || { tools_resource.get() // ← Resource-Zugriff außerhalb Suspense! .and_then(|r| r.ok()) // ... }); ``` **Richtig (finaler Ansatz):** ```rust "Lädt…"

}> {move || { tools_resource.get().map(|result| { // ← Zugriff korrekt innerhalb let tools = result.unwrap_or_default(); // Tags und gefilterte Liste hier berechnen view! { /* Tabelle */ } }) }}
``` **Konsequenz:** `FilterBar` und Tabelle liegen beide innerhalb der Suspense-Grenze. Bei Filteränderungen (reaktive `filter: RwSignal`) re-rendert der Suspense-Block, ohne den Ladeindikator zu zeigen (Resource ist bereits geladen). ### 4.4 Reaktivität: Wie Updates die Liste neu laden Nach jeder Mutation (erstellen, aktualisieren, löschen) muss die Liste neu geladen werden. Das geschieht über einen einfachen **Versions-Zähler**: ```rust let version = RwSignal::new(0u32); let neu_laden = move || version.update(|v| *v += 1); // Resource hängt von version ab let tools_resource = Resource::new( move || version.get(), // ← neu_laden() erhöht version → Resource re-fetcht |_| tools_laden(), ); ``` Das ist bewusst einfach gehalten. Eine Alternative wäre `ServerAction` mit automatischem `resource.refetch()`, was aber mehr Boilerplate erfordert. --- ## 5. Komponentenstruktur ``` ToolList (Hauptkomponente – hält den gesamten Zustand) │ ├── ToolForm (Formular – erscheint wenn bearbeitungs_modus != None) │ └── Callback on_fertig / on_abbrechen → zurück zur Liste │ └── └── FilterBar (bekommt filter: RwSignal + alle_tags: Vec) └── (iteriert über gefilterte Tools) ``` **Zustand-Management in `ToolList`:** | Signal | Typ | Zweck | |---|---|---| | `version` | `RwSignal` | Löst Resource-Reload aus | | `bearbeitungs_modus` | `RwSignal>` | Steuert Formular-Sichtbarkeit | | `filter` | `RwSignal` | Aktueller Filterzustand | | `csv_fehler/erfolg` | `RwSignal>` | Import-Feedback | **Warum kein globales State-Management (z.B. Context)?** Für eine Single-Page-App mit einer einzigen Hauptansicht ist lokaler Komponentenzustand ausreichend. Ein Context würde nur Sinn ergeben, wenn mehrere unabhängige Seiten/Routen den gleichen Zustand teilen müssten. --- ## 6. CSV-Import Design ### Format Bewusst minimal gehalten – nur `name` und `anzahl`: ```csv name,anzahl Schubkarren,2 Nadelrolle,mehrere Besen, ``` **Begründung:** Die bestehende Notizliste (`toollist_notes.txt`) ist nicht gut strukturiert genug für einen vollständigen Import. Ein minimales Format erlaubt das schnelle Anlegen vieler Einträge, die dann manuell nachbearbeitet werden können. ### Fehlertoleranz Der Import ist absichtlich tolerant: - Fehlende `anzahl` → `Anzahl::zahl(1)` - `"mehrere"` (Groß-/Kleinschreibung egal) → `Anzahl::mehrere()` - Nicht-numerische Anzahl → `Anzahl::mehrere()` - Leerzeilen und Kommentarzeilen (`#`) → übersprungen - Kopfzeile (`name,...`) → automatisch erkannt und übersprungen Alle importierten Einträge erhalten Standardwerte: `Status = Offen`, `Priorität = Mittel`, `Beschaffung = Unbekannt`. --- ## 7. Bekannte Einschränkungen & offene Punkte | Thema | Aktueller Stand | Mögliche Lösung | |---|---|---| | Concurrent Writes | Kein Locking auf TOML-Datei | `tokio::sync::Mutex` in Axum-State | | Verwendungsliste | Textarea (zeilenweise) | Dynamische Liste mit +/- Buttons | | Bauabschnitt | Nur via Tags | Eigenes Pflichtfeld für Bauphase | | Detailansicht | Alles in Tabellenzeile gequetscht | Aufklappbare Zeile oder Modal | | Sortierung | Keine | Klickbare Spaltenköpfe | | Export | Nicht vorhanden | Druckansicht, CSV-Export | | Deployment | Nur Dev-Server (`cargo leptos watch`) | Release-Build + Systemd-Service |