Files
3d.lehrstun.de/docs/datenmodell.md
Hans Puettmann 4658d4931a Initial commit: Konzept v2 + Etappe 1 (Blueprints + Login + Profile)
Schwester-Projekt zu arduino.lehrstun.de fuer die 3D-Druck-AG am
Lessing-Gymnasium. Offene jahrelange Nachmittags-AG (kein Trimester),
8-10 Einzel-SuS mit optionalen 2er-Paarungen, 4 Bambu-Lab-Drucker
(3 geschlossen + 1 A1 mini).

Konzept v2 (Reality-Check-Iteration 2026-05-24):
- Onboarding-Pfad statt Pflicht-Lektionen (L1-L3 selbst-zertifiziert)
- Selbstlern-Loop als Kern: SuS-Projekte wachsen die interne Bibliothek
- Drill-Down-Lehrer-Dashboard (Wer ist hier / Wartet Review / Haengt fest)
- Cockpit progressiv: zeigt nur was zum Status passt
- Bambu-Cloud-API fuer Drucker-Live-Status (fragil + Fallback manuell)
- Three.js-Vorschau fuer Cover-aus-3D-Ansicht (Etappe 6)
- Vollstaendige Doku als Obsidian-Vault (24 Markdown-Dateien)
- Entscheidungen E-001 bis E-022 in docs/decisions.md

Etappe 1 lauffaehig (~1800 Zeilen Code):
- Blueprint-Struktur (V8): routes/oeffentlich+profil+admin+api,
  services/auth+bambu+datei
- Komplettes Schema in database.py (14 Tabellen, idempotent)
- Login mit bcrypt + persistentem Lockout in DB (V7, verbessert ggue
  Arduino-Kurs der In-Memory-Dict nutzt)
- Admin-Login + Profile-CRUD + PIN-Reset + PIN-Karten-Druckbogen
- Inline-Edit-Endpunkt mit Whitelist + Audit-Log
- Seeds: AG lessing-3d-ag + 4 Drucker + Default-Einstellungen
- Smoke-Test bestanden (Login, Profil-Anlage, Lockout-Logging)

Nicht im Repo (.gitignore): .env, *.db, venv/, .obsidian/workspace.json
2026-05-24 18:56:59 +02:00

250 lines
11 KiB
Markdown

---
type: konzept-modul
projekt: 3d.lehrstun.de
stand: 2026-05-24
tags: [konzept, datenmodell, sql]
---
# Datenmodell
> SQLite-Schema. Live-Schema im Code: [`database.py`](../database.py).
> Architektur-Übersicht: [[architektur]]. Routes: [[routen]]. Selbstlern-Loop: [[selbstlern-loop]].
## ERD (Entity-Relationship)
```mermaid
erDiagram
AG_MITGLIED ||--o{ PROFIL : "umfasst"
PROFIL ||--o{ PROJEKT : "besitzt (Einzel)"
PROFIL ||--o{ DRUCKAUFTRAG : "stellt ein"
PROFIL ||--o{ SLOT : "reserviert"
PROFIL ||--o{ CHECK_IN : "macht pro Besuch"
PROFIL ||--o{ PROFIL_LEKTION_STATUS : "Fortschritt"
PROFIL ||--o{ LOGIN_LOCKOUT : "Sperrhistorie"
PROFIL ||--o{ PAARUNG : "Profil A"
PROFIL ||--o{ PAARUNG : "Profil B"
PAARUNG ||--o{ PROJEKT : "besitzt (Gruppe)"
DRUCKER ||--|| DRUCKER_STATUS : "hat Live-Status"
DRUCKER ||--o{ SLOT : "wird reserviert"
DRUCKER ||--o{ DRUCKAUFTRAG : "druckt"
PROJEKT ||--o{ DRUCKAUFTRAG : "gehört zu"
PROJEKT ||--o{ PROJEKT_DATEI : "hat Files"
PROJEKT ||--o{ PROJEKT_TAG : "hat Tags"
PROJEKT ||--o{ PROJEKT_RUECKFRAGE : "Lehrer-Notizen"
DRUCKAUFTRAG ||--o{ SLOT : "passt in"
LEKTION ||--o{ PROFIL_LEKTION_STATUS : "Fortschritt pro SuS"
AG_MITGLIED { TEXT id_PK "z.B. lessing-3d-ag" }
PROFIL { INT id_PK TEXT vorname TEXT tier TEXT pin_hash TEXT ag_id }
PAARUNG { INT id_PK INT profil_a INT profil_b INT aktiv }
PROJEKT { INT id_PK TEXT titel TEXT status_lebenszyklus TEXT selbsteinschaetzung TEXT freigabe_status }
PROJEKT_DATEI { INT id_PK TEXT dateiname TEXT art BLOB inhalt }
PROJEKT_TAG { INT projekt_id_FK TEXT tag }
PROJEKT_RUECKFRAGE { INT id_PK INT projekt_id TEXT text DATETIME erstellt_am TEXT status }
CHECK_IN { INT id_PK DATE besuch_datum TEXT modus TEXT freitext }
PROFIL_LEKTION_STATUS { INT profil_id INT lektion_nummer TEXT status }
LOGIN_LOCKOUT { INT id_PK INT profil_id INT versuche_count DATETIME sperre_bis }
DRUCKER { INT id_PK TEXT name TEXT modell TEXT serial_nr }
DRUCKER_STATUS { INT drucker_id_PK TEXT status INT restzeit_min }
DRUCKAUFTRAG { INT id_PK TEXT titel TEXT status }
SLOT { INT id_PK DATETIME start_zeit DATETIME ende_zeit INT prioritaet }
LEKTION { INT id_PK INT nummer TEXT titel INT onboarding }
```
## Tabellen-Übersicht
| Tabelle | Zweck | Querverweis |
|---|---|---|
| `ag_mitgliedschaft` | AG als dauerhafte Entität (statt Trimester-Kurs) | [[../decisions#E-012]] |
| `profil` | Einzel-SuS mit Vorname + Tier + PIN | [[../decisions#E-003]] |
| `paarung` | Optionale 2er-Zusammenschlüsse | [[../decisions#E-002]] |
| `profil_lektion_status` | Onboarding-Fortschritt pro SuS (offen/läuft/fertig) | [[../decisions#E-013]], [[../decisions#E-019]] |
| `login_lockout` | Persistenter Login-Lockout (V7, überlebt Restart) | [[../decisions#E-020]] |
| `projekt` | Eigene Projekte mit vollem Lebenszyklus | [[selbstlern-loop]] |
| `projekt_datei` | STL / 3MF / Fotos / Sonstiges pro Projekt | [[../decisions#E-016]] |
| `projekt_tag` | Tags für Such- und Vernetzungs-Logik | [[selbstlern-loop]] |
| `projekt_rueckfrage` | Lehrer-Rückfragen + Antworten | [[../decisions#E-017]] |
| `check_in` | „Was machst du heute?"-Abfrage pro Besuchstag | [[../decisions#E-021]] |
| `drucker` | 4 Bambu-Drucker als Entitäten | [[druckerapi]] |
| `drucker_status` | Live-Status, vom Poll-Worker aktualisiert | [[druckerapi]] |
| `druckauftrag` | Was die SuS drucken wollen | [[slotsystem]] |
| `slot` | Reservierungen auf Druckern | [[slotsystem]] |
| `lektion` | Onboarding-Lektionen + freie Module | [[lektion-L1-drucker-kennenlernen]] |
| `einstellung` | Konfigurierbare Werte (Inline-Edit) | — |
| `aenderung_log` | Audit-Trail für Inline-Edit + Lehrer-Aktionen | — |
## Wichtige neue Tabellen (gegenüber Skelett-Stand)
### `ag_mitgliedschaft` — ersetzt `kurs`
Die AG ist dauerhaft, nicht trimester-basiert. Eine einzelne Zeile (`id='lessing-3d-ag'`) reicht. Tabelle bleibt, weil später evtl. mehrere Schulen oder ein Reset möglich.
```sql
CREATE TABLE ag_mitgliedschaft (
id TEXT PRIMARY KEY, -- 'lessing-3d-ag'
name TEXT NOT NULL, -- '3D-Druck-AG Lessing-Gymnasium'
start_datum DATE,
status TEXT DEFAULT 'aktiv' -- 'aktiv'|'archiviert'
);
```
### `profil_lektion_status` — Onboarding-Fortschritt
Analog zu `gruppe_lektion_status` im Arduino-Kurs (`app.py:305-309`).
```sql
CREATE TABLE profil_lektion_status (
profil_id INTEGER NOT NULL,
lektion_nummer INTEGER NOT NULL,
status TEXT DEFAULT 'offen', -- 'offen'|'laeuft'|'fertig'
geaendert_am DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (profil_id, lektion_nummer),
FOREIGN KEY (profil_id) REFERENCES profil(id) ON DELETE CASCADE
);
```
### `login_lockout` — V7, persistenter Lockout
Verbesserung gegenüber Arduino-Kurs (In-Memory-Dict, bei Restart verloren).
```sql
CREATE TABLE login_lockout (
id INTEGER PRIMARY KEY AUTOINCREMENT,
profil_id INTEGER NOT NULL,
versuche_count INTEGER DEFAULT 1,
sperre_bis DATETIME, -- NULL = nicht gesperrt, nur gezählt
letzter_versuch_am DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (profil_id) REFERENCES profil(id) ON DELETE CASCADE
);
CREATE INDEX idx_lockout_profil ON login_lockout(profil_id);
```
### `projekt` — erweitert um Lebenszyklus
```sql
CREATE TABLE projekt (
id INTEGER PRIMARY KEY AUTOINCREMENT,
profil_id INTEGER,
paarung_id INTEGER,
titel TEXT NOT NULL,
beschreibung_md TEXT,
-- Lebenszyklus
status_lebenszyklus TEXT DEFAULT 'in_arbeit', -- 'in_arbeit'|'abgeschlossen'
abgeschlossen_am DATETIME,
selbsteinschaetzung TEXT, -- 'zufrieden'|'mittel'|'gelernt' (E-015)
was_gelernt TEXT, -- optionaler Freitext
-- Freigabe-Workflow
freigabe_status TEXT DEFAULT 'in_arbeit', -- 'in_arbeit'|'wartet_auf_freigabe'|'freigegeben'|'rueckfrage'
freigegeben_am DATETIME,
freigegeben_von INTEGER, -- Lehrer-Profil-ID, NULL=Admin direkt
-- Sichtbarkeit
sichtbar_in_ag INTEGER DEFAULT 1, -- SuS kann das ausschalten
-- Referenz auf Vorlage (wenn aus Katalog gewählt)
vorlage_projekt_id INTEGER, -- "ich mache sowas wie Mia"
geaendert_am DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (profil_id) REFERENCES profil(id) ON DELETE CASCADE,
FOREIGN KEY (paarung_id) REFERENCES paarung(id) ON DELETE CASCADE,
FOREIGN KEY (vorlage_projekt_id) REFERENCES projekt(id) ON DELETE SET NULL,
CHECK ((profil_id IS NOT NULL) <> (paarung_id IS NOT NULL))
);
CREATE INDEX idx_projekt_freigabe ON projekt(freigabe_status);
CREATE INDEX idx_projekt_sichtbar ON projekt(sichtbar_in_ag, freigabe_status);
```
### `projekt_datei` — STL / 3MF / Fotos
```sql
CREATE TABLE projekt_datei (
id INTEGER PRIMARY KEY AUTOINCREMENT,
projekt_id INTEGER NOT NULL,
dateiname TEXT NOT NULL,
mime_type TEXT,
groesse_bytes INTEGER,
inhalt BLOB,
art TEXT NOT NULL, -- 'cover_foto'|'stl'|'3mf'|'foto'|'sonstiges'
hochgeladen_am DATETIME DEFAULT CURRENT_TIMESTAMP,
hochgeladen_von INTEGER, -- profil_id (SuS oder Lehrer)
FOREIGN KEY (projekt_id) REFERENCES projekt(id) ON DELETE CASCADE,
FOREIGN KEY (hochgeladen_von) REFERENCES profil(id) ON DELETE SET NULL
);
CREATE INDEX idx_datei_projekt ON projekt_datei(projekt_id);
```
**Limit:** 30 MB pro Datei (App-Logik, nicht DB-Constraint).
**Abstraktion:** Zugriff über `services/datei.py` (siehe [[../decisions#E-016]]).
### `projekt_tag` — für Suche und Vernetzung
```sql
CREATE TABLE projekt_tag (
projekt_id INTEGER NOT NULL,
tag TEXT NOT NULL,
PRIMARY KEY (projekt_id, tag),
FOREIGN KEY (projekt_id) REFERENCES projekt(id) ON DELETE CASCADE
);
CREATE INDEX idx_tag ON projekt_tag(tag);
```
Tag-Pool ist offen (SuS gibt eigene Tags ein), aber Admin kuratiert kanonische Liste via `einstellung.tag_vorschlaege`. Beispiele: `schluesselanhaenger`, `organizer`, `werkzeug`, `spiel`, `dekoration`, `ersatzteil`, `geschenk`, `figur`.
### `projekt_rueckfrage` — Lehrer-Rückfrage-Notizen
```sql
CREATE TABLE projekt_rueckfrage (
id INTEGER PRIMARY KEY AUTOINCREMENT,
projekt_id INTEGER NOT NULL,
von_profil_id INTEGER NOT NULL, -- Lehrer
text TEXT NOT NULL,
erstellt_am DATETIME DEFAULT CURRENT_TIMESTAMP,
status TEXT DEFAULT 'offen', -- 'offen'|'erledigt'
erledigt_am DATETIME,
FOREIGN KEY (projekt_id) REFERENCES projekt(id) ON DELETE CASCADE,
FOREIGN KEY (von_profil_id) REFERENCES profil(id) ON DELETE SET NULL
);
```
Wird im Cockpit als gelbes Banner angezeigt (V6).
### `check_in` — angepasst an V3 und E-021
```sql
CREATE TABLE check_in (
id INTEGER PRIMARY KEY AUTOINCREMENT,
profil_id INTEGER NOT NULL,
besuch_datum DATE NOT NULL,
modus TEXT NOT NULL, -- 'weiter_wie_bisher'|'neues_projekt'|'onboarding'|'drucken'|'helfen'|'sonstiges'
freitext TEXT, -- optional
erstellt_am DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE(profil_id, besuch_datum),
FOREIGN KEY (profil_id) REFERENCES profil(id) ON DELETE CASCADE
);
```
**Wichtig:** „weiter_wie_bisher" ist die Default-Option und kann mit einem Klick gewählt werden (kein Freitext nötig). Erst bei „neues_projekt" oder „sonstiges" wird der Freitext sinnvoll.
## Wichtige Constraints
- `profil` UNIQUE(`ag_id`, `vorname`, `tier`) — zwei SuS mit demselben Vornamen müssen unterschiedliche Tiere wählen.
- `paarung` CHECK(`profil_a < profil_b`) + UNIQUE-Index für aktive Paarungen.
- `projekt` CHECK — entweder `profil_id` ODER `paarung_id`, nicht beides.
- `check_in` UNIQUE(`profil_id`, `besuch_datum`) — eine Abfrage pro SuS pro Tag.
- `slot` — überlappungsfreie Reservierungen werden in der App-Logik geprüft (Konflikt-Query siehe [[slotsystem]]).
- `projekt_datei` — 30-MB-Limit als App-Logik (kein DB-Constraint).
## Migrations-Strategie
- Initiales Schema in [`database.py`](../database.py) via `init_db()` (idempotent — `CREATE TABLE IF NOT EXISTS`).
- Spätere Änderungen als datierte Skripte in `migrations/YYYY-MM-DD_*.py`
Konvention wie bei `arduino.lehrstun.de`.
- Vor Migrationen auf der Live-Site immer Service stoppen (DB-Lock-Vermeidung).
## Performance-Hinweise
- Indexes auf häufig gefilterte Spalten: `idx_projekt_freigabe`, `idx_projekt_sichtbar`, `idx_tag`, `idx_lockout_profil`, `idx_datei_projekt`.
- Cockpit-Queries für „eigene Projekte / Aufträge / Slots" sind über `profil_id` indexiert (FK-Index ist in SQLite automatisch).
- BLOB-Größen bleiben durch 30-MB-Limit überschaubar; bei 5 GB+ DB ggf. Migration auf Object-Storage (siehe [[../decisions#E-016]]).