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

11 KiB

type, projekt, stand, tags
type projekt stand tags
konzept-modul 3d.lehrstun.de 2026-05-24
konzept
datenmodell
sql

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

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