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
This commit is contained in:
249
docs/datenmodell.md
Normal file
249
docs/datenmodell.md
Normal file
@@ -0,0 +1,249 @@
|
||||
---
|
||||
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]]).
|
||||
Reference in New Issue
Block a user