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
11 KiB
type, projekt, stand, tags
| type | projekt | stand | tags | |||
|---|---|---|---|---|---|---|
| konzept-modul | 3d.lehrstun.de | 2026-05-24 |
|
Datenmodell
SQLite-Schema. Live-Schema im Code:
database.py. Architektur-Übersicht: architektur. Routes: routen. Selbstlern-Loop: selbstlern-loop.
ERD (Entity-Relationship)
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.
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).
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).
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
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
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
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
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
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
profilUNIQUE(ag_id,vorname,tier) — zwei SuS mit demselben Vornamen müssen unterschiedliche Tiere wählen.paarungCHECK(profil_a < profil_b) + UNIQUE-Index für aktive Paarungen.projektCHECK — entwederprofil_idODERpaarung_id, nicht beides.check_inUNIQUE(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.pyviainit_db()(idempotent —CREATE TABLE IF NOT EXISTS). - Spätere Änderungen als datierte Skripte in
migrations/YYYY-MM-DD_*.py— Konvention wie beiarduino.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_idindexiert (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).