--- 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]]).