initial
This commit is contained in:
@@ -0,0 +1,357 @@
|
||||
# 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 |
|
||||
Reference in New Issue
Block a user