Files
toollist/documentation.md
Oliver Walter 5f43c19878 initial
2026-06-06 15:37:42 +02:00

358 lines
12 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.
# 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<String>` | Mehrere Verwendungszwecke möglich |
| `tags` | `Vec<String>` | 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<String>` | Freitext für alles was sonst nirgends passt |
| `verantwortlich` | `Option<String>` | 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<u32>, // 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<f64> },
Kaufen { wo: String, preis: Option<f64> },
}
```
**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<String>, // nur für Leihen
wo: Option<String>, // nur für Mieten/Kaufen
preis: Option<f64>, // 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<Utc>` 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 `<Suspense>`-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
<Suspense fallback=|| view! { <p>"Lädt…"</p> }>
{move || {
tools_resource.get().map(|result| { // ← Zugriff korrekt innerhalb
let tools = result.unwrap_or_default();
// Tags und gefilterte Liste hier berechnen
view! { <FilterBar ... /> /* Tabelle */ }
})
}}
</Suspense>
```
**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
└── <Suspense>
└── FilterBar (bekommt filter: RwSignal + alle_tags: Vec<String>)
└── <table> (iteriert über gefilterte Tools)
```
**Zustand-Management in `ToolList`:**
| Signal | Typ | Zweck |
|---|---|---|
| `version` | `RwSignal<u32>` | Löst Resource-Reload aus |
| `bearbeitungs_modus` | `RwSignal<Option<FormModus>>` | Steuert Formular-Sichtbarkeit |
| `filter` | `RwSignal<FilterZustand>` | Aktueller Filterzustand |
| `csv_fehler/erfolg` | `RwSignal<Option<String>>` | 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 |