commit 4658d4931a21898661aac33cc99e8056dc7037ab Author: Hans Puettmann Date: Sun May 24 18:56:59 2026 +0200 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 diff --git a/.env.beispiel b/.env.beispiel new file mode 100644 index 0000000..46911c9 --- /dev/null +++ b/.env.beispiel @@ -0,0 +1,15 @@ +# Kopiere diese Datei zu .env und trage echte Werte ein. +# .env selbst NICHT committen (steht in .gitignore). + +# Flask +FLASK_SECRET=ersetze-mich-durch-einen-langen-zufallsstring +ADMIN_PASSWORT=ersetze-mich + +# Bambu Cloud-API (erst ab Etappe 4 nötig) +BAMBU_EMAIL= +BAMBU_PASSWORT= +BAMBU_REGION=eu + +# PummelHummel/Alph (optional, ab Integration) +ALPH_ADMIN_TOKEN= +# ALPH_DEAKTIVIERT=1 # Hart-Aus für das Feedback-Widget diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7cb51f5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,31 @@ +# Python +__pycache__/ +*.pyc +*.pyo +venv/ +.venv/ +*.egg-info/ + +# Daten + Secrets +.env +*.db +*.db-journal +backups/ + +# Editor +.vscode/ +.idea/ +*.swp +.DS_Store + +# Build / Logs +*.log +dist/ +build/ + +# Obsidian: user-spezifischer Zustand bleibt lokal, Vault-Struktur committen +.obsidian/workspace.json +.obsidian/workspace-mobile.json +.obsidian/cache +.obsidian/plugins/ +anhaenge/ diff --git a/.obsidian/app.json b/.obsidian/app.json new file mode 100644 index 0000000..d143f6c --- /dev/null +++ b/.obsidian/app.json @@ -0,0 +1,8 @@ +{ + "showLineNumber": true, + "newFileLocation": "current", + "attachmentFolderPath": "anhaenge", + "alwaysUpdateLinks": true, + "useMarkdownLinks": false, + "newLinkFormat": "shortest" +} \ No newline at end of file diff --git a/.obsidian/appearance.json b/.obsidian/appearance.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/.obsidian/appearance.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/.obsidian/community-plugins.json b/.obsidian/community-plugins.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/.obsidian/community-plugins.json @@ -0,0 +1 @@ +[] diff --git a/.obsidian/core-plugins.json b/.obsidian/core-plugins.json new file mode 100644 index 0000000..87cf716 --- /dev/null +++ b/.obsidian/core-plugins.json @@ -0,0 +1,34 @@ +{ + "file-explorer": true, + "global-search": true, + "switcher": true, + "graph": true, + "backlink": true, + "outgoing-link": true, + "tag-pane": true, + "page-preview": true, + "daily-notes": false, + "templates": true, + "note-composer": true, + "command-palette": true, + "editor-status": true, + "starred": false, + "markdown-importer": false, + "outline": true, + "word-count": true, + "file-recovery": true, + "publish": false, + "sync": false, + "bookmarks": true, + "properties": true, + "canvas": true, + "footnotes": false, + "slash-command": false, + "zk-prefixer": false, + "random-note": false, + "slides": false, + "audio-recorder": false, + "workspaces": false, + "bases": true, + "webviewer": false +} \ No newline at end of file diff --git a/.obsidian/graph.json b/.obsidian/graph.json new file mode 100644 index 0000000..fd2bfc9 --- /dev/null +++ b/.obsidian/graph.json @@ -0,0 +1,27 @@ +{ + "collapse-filter": false, + "search": "", + "showTags": true, + "showAttachments": false, + "hideUnresolved": false, + "showOrphans": true, + "collapse-color-groups": false, + "colorGroups": [ + { "query": "path:docs/lektion-", "color": { "a": 1, "rgb": 2774967 } }, + { "query": "path:docs/software-", "color": { "a": 1, "rgb": 11498495 } }, + { "query": "path:docs/drucker", "color": { "a": 1, "rgb": 15105570 } }, + { "query": "path:docs/architektur OR path:docs/datenmodell OR path:docs/routen", "color": { "a": 1, "rgb": 5419488 } } + ], + "collapse-display": false, + "showArrow": true, + "textFadeMultiplier": 0, + "nodeSizeMultiplier": 1, + "lineSizeMultiplier": 1, + "collapse-forces": false, + "centerStrength": 0.5, + "repelStrength": 10, + "linkStrength": 1, + "linkDistance": 200, + "scale": 1, + "close": false +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..3851bf5 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,38 @@ +--- +type: log +projekt: 3d.lehrstun.de +tags: [changelog] +--- + +# CHANGELOG — 3d.lehrstun.de + +Format: [Datum] — Kurz-Beschreibung. Details bei nicht-trivialen Änderungen. + +## 2026-05-24 — Etappe 1 lauffähig +- **Blueprint-Struktur etabliert** (V8): `routes/oeffentlich.py`, `routes/profil.py`, `routes/admin.py`, `routes/api.py`, `services/auth.py`, `services/bambu.py` (Stub), `services/datei.py` (Stub). +- **app.py zu Bootstrap umgebaut** (~100 Z.) — registriert Blueprints, definiert Markdown-Filter, Context-Processor, Error-Handler (404/500/501). +- **database.py mit allen Tabellen aus [[datenmodell]]** — idempotent via `CREATE TABLE IF NOT EXISTS`. Inkl. `profil_lektion_status`, `login_lockout`, `projekt_*`, `lehrer_notiz`, `ag_mitgliedschaft`, `check_in` (E-021), `aenderung_log`. +- **Auth-Layer** in `services/auth.py`: bcrypt-PIN-Hashing/-Pruefung, persistenter Lockout in DB (V7), Decorators `login_required` + `admin_required` + `onboarding_l1_fertig` (V2). +- **Profil-Login funktional**: 4-stellige PIN, Rate-Limit 5 Versuche / 5 Min → 15 Min DB-Sperre. +- **Admin-Login + Profile-Verwaltung**: Neue Profile mit Auto-PIN-Generation (einmalig sichtbar), PIN-Reset, aktiv-Toggle, Druckbogen für PIN-Karten (CSS-Print). +- **Inline-Edit-Infrastruktur** in `routes/admin.py`: `ERLAUBTE_INLINE_TABELLEN`-Whitelist, `/admin/inline-edit`-POST mit Audit-Log in `aenderung_log`. +- **seed.py** mit Initial-Daten: AG `lessing-3d-ag`, 4 Drucker (3 geschlossen + A1 mini), 3 Default-Einstellungen. +- **Templates portiert**: `login.html`, `login_pin.html`, `cockpit_stub.html`, `admin_login.html`, `admin_profile.html`, `admin_profile_karten.html`, `501.html`. +- **Smoke-Test bestanden**: Admin-Login, Profil-Anlage, SuS-Login mit PIN, Falsche-PIN-Lockout-Logging — alles funktional. + +## 2026-05-24 — Reality-Check + Selbstlern-Loop (Konzept v2) +- Reality-Check-Analyse aus vier Perspektiven (neuer SuS / erfahrener SuS / Lehrer / Technik) + Vergleich mit `arduino.lehrstun.de`. Plan-Datei: `~/.claude/plans/f-hre-noch-einmal-eina-cosmic-wind.md` (approved). +- **Wichtige Konzept-Korrektur:** AG ist offen, jahrelang, keine festen Termine. Pflicht-Lektionen werden zu Onboarding-Pfad für Neue. +- **Neue Module dokumentiert:** [[selbstlern-loop]] (Projekt-Lebenszyklus + interne Bibliothek), [[lehrer-dashboard]] (Drill-Down: Wer ist hier / Wartet auf Review / Wer hängt fest). +- **Entscheidungen E-012 bis E-022 dokumentiert** ([[decisions]]): u.a. Selbstlern-Loop, Vernetzung Variante 1, Feedback A-Variante, Three.js, Blueprint-Struktur, persistenter Login-Lockout, ZSL-Material-Strategie. +- **Reichhaltig überarbeitet:** [[kurskonzept]] auf v2, [[datenmodell]] (5 neue Tabellen), [[routen]] (Selbstlern-Loop-Routen + Drill-Down), [[etappen]] (mit Blueprint von Anfang an), [[offene-fragen]] (geklärtes raus). +- COCKPIT.md mit erweitertem Mermaid-Diagramm und neuen Wikilinks. + +## 2026-05-23 +- Projekt-Skelett angelegt: [[CLAUDE]], [[kurskonzept]], [[decisions]], [[README]], leeres Flask-Gerüst (`app.py`, `database.py`, `tiere.py`). +- Recherche zu Bambu Lab Education, Modell-Plattformen, didaktischen Vorbildern, Bambu Cloud-API durchgeführt. +- Entscheidungen E-001 bis E-011 dokumentiert ([[decisions]]). +- Vault-Struktur ergänzt: `.obsidian/`-Konfig, [[COCKPIT]] als Startseite, Konzept-Module + ([[architektur]], [[datenmodell]], [[routen]], [[druckerapi]], [[slotsystem]], [[sicherheit]], + [[etappen]], [[offene-fragen]], [[glossar]]), Lektions-Stubs L1/L2/L3, Software-Tool-Stubs + (Bambu Studio/Handy, Printables, MakerWorld, TinkerCAD, OpenSCAD). diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..f2d7236 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,83 @@ +--- +type: project +name: 3d.lehrstun.de +status: entwurf +domain: 3d.lehrstun.de +deployed: false +next_step: "Kurskonzept mit Markus durchgehen, offene Fragen klären" +tags: [schule, 3d-druck, web, ag] +--- + +# 3D-Druck-AG — 3d.lehrstun.de + +> Vault-Startseite: [[COCKPIT]]. Hauptkonzept: [[kurskonzept]]. Architektur: [[architektur]]. +> Offene Klärungen: [[offene-fragen]]. Entscheidungen: [[decisions]]. Etappen: [[etappen]]. + +**Status:** **Etappe 1 läuft lokal** (Stand 2026-05-24). Login + Admin + Inline-Edit-Infra + Blueprint-Struktur funktional. Smoke-Test bestanden (Admin-Login, Profil-Anlage, SuS-Login mit PIN, Lockout-Logging). +**Repo:** noch nicht angelegt (lokales Git folgt nach Markus-Review). +**Port intern:** 5057 (5055=quiz-collector, 5056=arduino-db). +**Brand:** 3D-Druck-AG +**Admin-Passwort lokal:** `lokal-test-123` (in `.env`) + +## Kontext (Kurzfassung) + +- Wahl-AG Klasse 8 MINT, **8-10 Einzel-SuS** (optional 2er-Paarungen). +- **4 Bambu-Lab-Drucker**: 3× geschlossen (P1S/X1C/P2S) + 1× A1 mini (offen). +- Eigener Druckraum, dauergelüftet. PLA + PLA+ Default, PETG/ABS unter Aufsicht. +- Drucker dürfen über Nacht / Freistunden laufen. +- Herb hat 3D-Druck **selbst noch nie unterrichtet** — Inhalte folgen mit Markus. +- Drucker-Status via **Bambu Cloud-API** (Polling, mit Fallback auf manuell). +- **Login:** Vorname + Tier-Icon + 4-stellige PIN. Sichtbarkeit: Lehrer voll, SuS nur Projekttitel. + +## Quickstart (lokal) +```bash +cd D:/Claude-Projekte/3d.lehrstun.de +# Einmalig: venv + Pakete +python -m venv venv +venv/Scripts/python.exe -m pip install -r requirements.txt +cp .env.beispiel .env # ADMIN_PASSWORT eintragen! +# Schema + Initial-Seeds +venv/Scripts/python.exe seed.py +# App starten +venv/Scripts/python.exe app.py # http://localhost:5057 +``` + +**Funktionierende Routen (Etappe 1):** +- `/` Startseite +- `/login` Profil-Kacheln (anfangs leer, im Admin anlegen) +- `/login/` PIN-Eingabe mit persistentem Lockout (V7) +- `/cockpit` Stub (Etappe 2 macht das progressiv) +- `/admin/login` Admin-Login mit Passwort aus `.env` +- `/admin/profile` SuS-Profile + Neuanlage + PIN-Reset + aktiv-Toggle +- `/admin/profile/karten` Druckbogen für PIN-Karten (CSS-Print) +- `/admin/inline-edit` Universal-Endpoint mit Whitelist + Audit-Log + +Alle anderen Routen geben **501** + Verweis auf die Etappe. + +## Update-Workflow auf Hetzner (vor erstem Deploy noch einzurichten) +```bash +ssh herb@46.225.114.15 +cd ~/3d.lehrstun.de +git pull origin master +sudo systemctl stop 3d-ag +python3 migrations/YYYY-MM-DD_xxx.py +sudo systemctl start 3d-ag +``` + +## Nächste Schritte +- [x] Skelett + Obsidian-Vault anlegen (2026-05-23) +- [x] Reality-Check + Konzept v2 (2026-05-24) +- [x] **Etappe 1: Blueprints + Schema + Login + Admin + Inline-Edit + Lockout** (2026-05-24) +- [ ] [[kurskonzept]] mit Markus durchgehen +- [ ] [[offene-fragen]] klären (Bambu-Account, A1-mini-Firmware) +- [ ] Lokales Git-Repo + Gitea-Push +- [ ] **Etappe 2:** Cockpit progressiv + Check-In + Paarung + [[lehrer-dashboard|Lehrer-Dashboard]] +- [ ] Etappe 3: Lektionen + Software-Sektion + Troubleshooting +- [ ] Etappe 4: Drucker-API-Spike → [[druckerapi]] +- [ ] DNS-A-Record `3d.lehrstun.de` → 46.225.114.15 (via Hetzner Cloud DNS-CLI, vor Etappe 9) + +## Querverbindungen +- Schwester-Projekt: `D:/Claude-Projekte/arduino.lehrstun.de/` (Stack + Design + Code-Quelle) +- PummelHummel/Alph: `D:/Claude-Projekte/PummelHummel/` (Integration in Etappe 8) +- Hetzner-DNS-CLI: `D:/Claude-Projekte/hetzner-dns-cli/` +- Detail-Doku: `docs/` (22 Markdown-Dateien als Obsidian-Vault verlinkt) diff --git a/COCKPIT.md b/COCKPIT.md new file mode 100644 index 0000000..f7863c4 --- /dev/null +++ b/COCKPIT.md @@ -0,0 +1,142 @@ +--- +type: cockpit +projekt: 3d.lehrstun.de +status: etappe-1-lauffaehig +stand: 2026-05-24 +tags: [3d-druck, ag, schule, cockpit] +--- + +# 3D-Druck-AG — Cockpit + +> **Vault-Startseite.** Wenn du diesen Ordner in Obsidian öffnest, landest du hier. +> Von hier verlinken Wikilinks (`[[Name]]`) auf alle Detail-Dateien des Konzepts. + +## Status auf einen Blick + +- **Stand:** Konzept v2 + **Etappe 1 lokal lauffaehig** (Login + Admin + Inline-Edit + Blueprint-Struktur, Smoke-Test bestanden). Code: ~1800 Zeilen Python+HTML. +- **Nichts deployed**, DNS noch nicht gesetzt. +- **Nächster Schritt:** Etappe 2 — progressives Cockpit, Check-In, Paarung, [[lehrer-dashboard|Lehrer-Dashboard]]. Oder vorher: Markus liest [[kurskonzept]] + [[selbstlern-loop]] + [[lehrer-dashboard]]. +- **Offene Klärungen:** [[offene-fragen]] (Bambu-Cloud-Account, A1-mini-Firmware, ZSL-Projekt-Auswahl) +- **Lokal starten:** `venv/Scripts/python.exe app.py` → http://localhost:5057 (Admin-PW lokal: `lokal-test-123`) + +## Architektur in einem Bild + +```mermaid +flowchart TB + subgraph Public["Öffentlich (kein Login)"] + L[Onboarding-Pfad
+ freie Module] + S[Software-Sektion] + P[Projekt-Katalog
SuS-Projekte + ZSL-Seed] + T[Troubleshooting] + D[Drucker-Live-Status] + end + + subgraph Profil["Profil (Vorname + Tier + PIN)"] + C[Cockpit progressiv] + CI[Check-In 'Was machst du heute?'] + EP[Eigene Projekte
im Lebenszyklus] + AB[Abschluss-Checkliste] + SL[Slot-Reservierungen
nach L1 fertig] + end + + subgraph Admin["Admin (Herb + Markus)"] + AD[Drill-Down-Dashboard] + RQ[Review-Queue
+ Three.js-Vorschau] + DAQ[Druck-Queue] + ADR[Drucker-Verwaltung] + ALE[Inline-Edit für Lektionen] + end + + subgraph Extern["Externe Systeme"] + BC[Bambu Cloud-API] + BS[Bambu Studio
auf Schul-PCs] + BH[Bambu Handy App] + PR[Printables /
MakerWorld] + TC[TinkerCAD] + OS[OpenSCAD] + ZSL[ZSL Baden-Württemberg
CC-BY-4.0 Material] + end + + Public --> Profil + Profil --> Admin + AB --> RQ + RQ --> P + AD -.Polling 30s.-> BC + BC -.Status.-> D + L --> PR + L --> TC + L --> OS + BS --> BC + BH --> BC + ZSL -.Initial-Seed.-> P + + classDef extern fill:#FEF9E7,stroke:#F5C842 + class BC,BS,BH,PR,TC,OS,ZSL extern +``` + +## Inhalt des Vaults + +### Wurzel +- [[CLAUDE]] — Projektstatus + Quickstart (für Claude/Herb) +- [[README]] — Vorstellung für Markus + Aussenstehende +- [[CHANGELOG]] — was wurde wann angelegt + +### Konzept +- [[kurskonzept]] — **Hauptdokument v2**, vollständiges Konzept zum Lesen (Markus, am Stück lesbar) +- [[selbstlern-loop]] — **Kern-Mechanismus** der AG: Wie Projekte wachsen + vernetzt werden +- [[lehrer-dashboard]] — **Drill-Down-Konzept** für Herb + Markus +- [[architektur]] — Komponentensicht mit Mermaid-Diagramm +- [[datenmodell]] — SQLite-Schema als ERD (mit allen neuen Tabellen) +- [[routen]] — Vollständige Routen-Liste mit Blueprint-Aufteilung +- [[druckerapi]] — Bambu Cloud-API-Anbindung, Fallback, Risiken +- [[slotsystem]] — Reservierung, Windhund, Priorisierung, Ausfall-Handling +- [[sicherheit]] — Druckraum, Material, A1 mini +- [[etappen]] — Implementierungs-Etappen 0-9 (mit Blueprint-Struktur von Anfang an) +- [[offene-fragen]] — was wir noch besprechen müssen +- [[glossar]] — 3D-Druck-Begriffe für SuS und Markus + +### Lektionen (Pflicht, Inhalte folgen) +- [[lektion-L1-drucker-kennenlernen]] +- [[lektion-L2-modell-drucken]] +- [[lektion-L3-customizer]] + +### Software-Tools (Stubs) +- [[software-bambu-studio]] +- [[software-bambu-handy]] +- [[software-printables]] +- [[software-makerworld]] +- [[software-tinkercad]] +- [[software-openscad]] + +### Entscheidungen +- [[decisions]] — Entscheidungs-Log, neueste oben + +## Wichtige externe Quellen + +- [Bambu Lab Academy](https://bambulab.com/en/support/academy) — Drucker-Kurse mit Zertifikat +- [PHBern IdeenSet 3D-Drucken](https://www.phbern.ch/dienstleistungen/unterrichtsmedien/ideenset-3d-drucken) +- [inf-schule.de 3D-Druck](https://inf-schule.de/informatiksysteme/3d_druck) +- [UKBW Sicherheit (PDF)](https://www.ukbw.de/fileadmin/user_upload/Sicherer-Umgang-mit-3D-Druckern-in-der-Schule.pdf) +- [bambulabs_api (GitHub)](https://github.com/BambuTools/bambulabs_api) +- [OpenBambuAPI (Protokoll-Doku)](https://github.com/Doridian/OpenBambuAPI) + +## Tags-Übersicht + +- `#konzept` — Architektur-/Konzept-Dateien +- `#kern` — die zentralen Module ([[selbstlern-loop]], [[lehrer-dashboard]]) +- `#lektion` — Lektions-Inhalte +- `#software` — Software-Tool-Beschreibungen +- `#offen` — noch zu klären +- `#entscheidung` — getroffene Entscheidung +- `#druckerapi` — alles zur Bambu-Anbindung + +## Hinweis für Markus + +Dieser Vault ist Markdown-only. Du kannst ihn mit Obsidian öffnen +(`File → Open vault → diesen Ordner wählen`) und kriegst dann: +- **Wikilinks** (`[[…]]`) zum Springen zwischen Dateien +- **Graph-View** zur visuellen Übersicht der Querverbindungen +- **Backlinks** rechts: wer verlinkt auf die aktuelle Datei + +Du kannst die Dateien aber auch mit jedem anderen Editor (VSCode, Notepad++, +sogar Notepad) lesen — sie sind reines Markdown. diff --git a/README.md b/README.md new file mode 100644 index 0000000..b697f14 --- /dev/null +++ b/README.md @@ -0,0 +1,56 @@ +# 3D-Druck-AG — Lernplattform + +Web-Plattform für eine 3D-Druck-AG (Klasse 8 MINT, ca. 8-10 SuS, 4 Bambu-Lab-Drucker). + +**Status:** Entwurf. Skelett angelegt, noch nicht lauffähig. Inhalte und Routen werden mit Markus + Herb sukzessive ergänzt. + +## Was die Plattform kann (geplant) + +- **Lektionen**: Pflicht-Block L1-L3 (Drucker-Kennenlernen, fremdes Modell drucken, parametrischer Customizer), danach freier Block (TinkerCAD, OpenSCAD, eigenes Projekt). +- **SuS-Cockpit**: eigenes Profil mit Vorname + Tier-Icon + PIN. Projektstand, Druckaufträge, Slot-Reservierungen. Optional Paarung zu 2er-Gruppen. +- **Drucker-Übersicht** mit Live-Status via Bambu Cloud-API (oder manuell). +- **Slot-Buchung** für die 4 Drucker, Windhund-Prinzip mit Lehrer-Override. +- **Stunden-Beginn-Abfrage** ("Was machst du heute?") aggregiert im Lehrer-Dashboard. +- **Admin-Inline-Edit** für Lektionen, Software-Seiten, Profile. + +## Für wen ist das? + +- **Primär:** Herb + Markus (Lehrkräfte), die SuS der AG. +- **Sekundär:** andere Lehrkräfte, die einen 3D-Druck-Kurs aufsetzen wollen — Code ist MIT-lizenziert (TODO), Setup-Anleitung folgt mit dem ersten Deploy. + +## Schnellstart (lokal, sobald lauffähig) + +```bash +git clone 3d.lehrstun.de +cd 3d.lehrstun.de +python -m venv venv +source venv/Scripts/activate # Git-Bash auf Windows +# auf Linux/Mac: source venv/bin/activate +pip install -r requirements.txt +cp .env.beispiel .env # Werte eintragen! +python -c "from database import init_db; init_db()" +python app.py # http://localhost:5057 +``` + +## Technik-Stack + +- Python 3.11+ +- Flask + SQLite (kein ORM, plain SQL — wie beim Schwesterprojekt arduino.lehrstun.de) +- Bambu Lab Cloud-API für Drucker-Status (`bambulabs_api` oder Code aus `pybambu`) +- Vanilla CSS + JS (keine Build-Tools, kein npm) + +## Schwesterprojekte + +- **arduino.lehrstun.de** — Arduino-Kurs Klasse 8. Design-Vorlage und Code-Quelle für Login, Inline-Edit, Lektionen-Modul. +- **PummelHummel/Alph** — Feedback-Widget, hier identisch eingebunden. + +## Dokumentation + +- `docs/kurskonzept.md` — vollständiges didaktisches Konzept (für Markus + Herb) +- `docs/decisions.md` — Entscheidungs-Log +- `CHANGELOG.md` +- `CLAUDE.md` — Projektstatus + Quickstart (für Claude/Herb) + +## Lizenz + +TODO (vermutlich MIT, sobald das Repo öffentlich wird). diff --git a/app.py b/app.py new file mode 100644 index 0000000..32b3903 --- /dev/null +++ b/app.py @@ -0,0 +1,90 @@ +"""3D-Druck-AG — Flask-Bootstrap. + +Nach V8 (siehe docs/decisions.md#E-020): Blueprint-Struktur. +Diese Datei laedt die Konfiguration, registriert Blueprints, definiert +Context-Processor + Error-Handler. + +Routen leben in routes/. Externe Services in services/. + +Quickstart (lokal): + python -c "from database import init_db; init_db()" + python seed.py + python app.py # http://localhost:5057 +""" +import os +from pathlib import Path + +from dotenv import load_dotenv +from flask import Flask, render_template, request + +from tiere import tier_emoji, tier_name + +# Routes-Blueprints +from routes.oeffentlich import bp as bp_oeffentlich +from routes.profil import bp as bp_profil +from routes.admin import bp as bp_admin +from routes.api import bp as bp_api + +load_dotenv() + +app = Flask(__name__) +app.secret_key = os.environ.get("FLASK_SECRET", "dev-key-bitte-aendern") + +# Blueprints registrieren +app.register_blueprint(bp_oeffentlich) +app.register_blueprint(bp_profil) +app.register_blueprint(bp_admin) +app.register_blueprint(bp_api) + + +# =========================================================================== +# Markdown-Filter fuer Templates (Pattern wie arduino.lehrstun.de) +# =========================================================================== +import markdown as _markdown + + +@app.template_filter("markdown") +def markdown_filter(text): + if not text: + return "" + return _markdown.markdown( + text, + extensions=["fenced_code", "tables", "nl2br"], + output_format="html5", + ) + + +# =========================================================================== +# Context-Processor: in jedem Template verfuegbar +# =========================================================================== + +@app.context_processor +def inject_globals(): + return { + "tier_emoji": tier_emoji, + "tier_name": tier_name, + } + + +# =========================================================================== +# Error-Handler +# =========================================================================== + +@app.errorhandler(404) +def err_404(e): + return render_template("404.html"), 404 + + +@app.errorhandler(500) +def err_500(e): + return render_template("500.html"), 500 + + +@app.errorhandler(501) +def err_501(e): + return render_template("501.html"), 501 + + +if __name__ == "__main__": + # Port 5057 (5055 = quiz-collector, 5056 = arduino-db) + app.run(host="0.0.0.0", port=5057, debug=True) diff --git a/database.py b/database.py new file mode 100644 index 0000000..ddbe0f2 --- /dev/null +++ b/database.py @@ -0,0 +1,301 @@ +"""SQLite-Schema und Init fuer 3d.lehrstun.de. + +Wird beim ersten Start aufgerufen via: + python -c "from database import init_db; init_db()" + +Migrationen kommen separat in migrations/, idempotent + datiert, +analog zu arduino.lehrstun.de. + +Vollstaendige Tabellen-Doku: docs/datenmodell.md +Entscheidungen E-002 (Profil primaer), E-013 (Onboarding-Status), +E-014 (Projekt-Lebenszyklus), E-016 (Files in BLOB), E-020 (V7 Lockout). +""" +import sqlite3 +from pathlib import Path + +DB_PFAD = Path(__file__).parent / "3dag.db" + + +def get_db(): + """Standard-Verbindung mit Row-Factory + Foreign-Keys an.""" + conn = sqlite3.connect(DB_PFAD) + conn.row_factory = sqlite3.Row + conn.execute("PRAGMA foreign_keys = ON") + return conn + + +SCHEMA = """ +-- =========================================================================== +-- AG-Mitgliedschaft (statt Trimester-Kurs — E-012, AG ist offen + jahrelang) +-- =========================================================================== +CREATE TABLE IF NOT EXISTS ag_mitgliedschaft ( + id TEXT PRIMARY KEY, -- z.B. 'lessing-3d-ag' + name TEXT NOT NULL, + start_datum DATE, + status TEXT DEFAULT 'aktiv' -- 'aktiv'|'archiviert' +); + +-- =========================================================================== +-- SuS-Profile (Primaer-Objekt — E-002) +-- =========================================================================== +CREATE TABLE IF NOT EXISTS profil ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + vorname TEXT NOT NULL, -- Klarname (E-003) + tier TEXT NOT NULL, -- Tier-Slug fuer Icon + pin_hash TEXT NOT NULL, -- bcrypt + ag_id TEXT NOT NULL, -- FK ag_mitgliedschaft + aktiv INTEGER DEFAULT 1, + erstellt_am DATETIME DEFAULT CURRENT_TIMESTAMP, + UNIQUE(ag_id, vorname, tier), + FOREIGN KEY (ag_id) REFERENCES ag_mitgliedschaft(id) +); + +-- Persistenter Login-Lockout (V7, verbessert ggue Arduino-Kurs) +CREATE TABLE IF NOT EXISTS login_lockout ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + profil_id INTEGER NOT NULL, + versuche_count INTEGER DEFAULT 1, + sperre_bis DATETIME, -- NULL = nur Zaehler, kein aktives Sperren + letzter_versuch_am DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (profil_id) REFERENCES profil(id) ON DELETE CASCADE +); +CREATE INDEX IF NOT EXISTS idx_lockout_profil ON login_lockout(profil_id); + +-- 2er-Paarungen (optional) +CREATE TABLE IF NOT EXISTS paarung ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + profil_a INTEGER NOT NULL, + profil_b INTEGER NOT NULL, + aktiv INTEGER DEFAULT 1, + gebildet_am DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (profil_a) REFERENCES profil(id) ON DELETE CASCADE, + FOREIGN KEY (profil_b) REFERENCES profil(id) ON DELETE CASCADE, + CHECK (profil_a < profil_b) +); +CREATE UNIQUE INDEX IF NOT EXISTS idx_paarung_aktiv + ON paarung(profil_a, profil_b) WHERE aktiv = 1; + +-- =========================================================================== +-- Onboarding-Pfad-Fortschritt (E-013, E-019 selbst-zertifiziert) +-- =========================================================================== +CREATE TABLE IF NOT EXISTS 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 +); + +-- =========================================================================== +-- Projekte (mit vollem Lebenszyklus — E-014) +-- =========================================================================== +CREATE TABLE IF NOT EXISTS projekt ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + profil_id INTEGER, -- entweder dies ODER paarung_id + 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 (E-017) + freigabe_status TEXT DEFAULT 'in_arbeit', -- 'in_arbeit'|'wartet_auf_freigabe'|'freigegeben'|'rueckfrage' + freigegeben_am DATETIME, + freigegeben_von INTEGER, -- Lehrer-Profil-ID + -- Sichtbarkeit + sichtbar_in_ag INTEGER DEFAULT 1, + -- Vernetzung + 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, + FOREIGN KEY (freigegeben_von) REFERENCES profil(id) ON DELETE SET NULL, + CHECK ((profil_id IS NOT NULL) <> (paarung_id IS NOT NULL)) +); +CREATE INDEX IF NOT EXISTS idx_projekt_freigabe ON projekt(freigabe_status); +CREATE INDEX IF NOT EXISTS idx_projekt_sichtbar ON projekt(sichtbar_in_ag, freigabe_status); + +-- Projekt-Dateien (E-016, 30 MB-Limit als App-Logik) +CREATE TABLE IF NOT EXISTS 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, + FOREIGN KEY (projekt_id) REFERENCES projekt(id) ON DELETE CASCADE, + FOREIGN KEY (hochgeladen_von) REFERENCES profil(id) ON DELETE SET NULL +); +CREATE INDEX IF NOT EXISTS idx_datei_projekt ON projekt_datei(projekt_id); + +-- Projekt-Tags (fuer Suche und Vernetzung) +CREATE TABLE IF NOT EXISTS 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 IF NOT EXISTS idx_tag ON projekt_tag(tag); + +-- Lehrer-Rueckfragen pro Projekt (V6) +CREATE TABLE IF NOT EXISTS projekt_rueckfrage ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + projekt_id INTEGER NOT NULL, + von_profil_id INTEGER NOT NULL, + 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 +); + +-- =========================================================================== +-- Lehrer-Notizen (allgemein, an ein SuS-Cockpit gerichtet — V5-Quick-Action) +-- =========================================================================== +CREATE TABLE IF NOT EXISTS lehrer_notiz ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + profil_id INTEGER NOT NULL, -- Adressat + von_profil_id INTEGER, -- NULL = System + text TEXT NOT NULL, + erstellt_am DATETIME DEFAULT CURRENT_TIMESTAMP, + gelesen_am DATETIME, -- NULL = ungelesen + typ TEXT DEFAULT 'info', -- 'info'|'rueckfrage'|'erinnerung' + FOREIGN KEY (profil_id) REFERENCES profil(id) ON DELETE CASCADE +); + +-- =========================================================================== +-- Stunden-Beginn-Abfrage (E-021 mit "weiter wie bisher"-Default) +-- =========================================================================== +CREATE TABLE IF NOT EXISTS check_in ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + profil_id INTEGER NOT NULL, + besuch_datum DATE NOT NULL, + modus TEXT NOT NULL, -- siehe docs/datenmodell.md + freitext TEXT, -- optional + erstellt_am DATETIME DEFAULT CURRENT_TIMESTAMP, + UNIQUE(profil_id, besuch_datum), + FOREIGN KEY (profil_id) REFERENCES profil(id) ON DELETE CASCADE +); + +-- =========================================================================== +-- Drucker (Stamm-Daten, vom Admin gepflegt) + Live-Status +-- =========================================================================== +CREATE TABLE IF NOT EXISTS drucker ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL UNIQUE, + modell TEXT NOT NULL, -- 'P1S'|'X1C'|'P2S'|'A1 mini' + serial_nr TEXT, -- fuer Cloud-API + geschlossenes_gehaeuse INTEGER DEFAULT 1, -- A1 mini = 0 + aktiv INTEGER DEFAULT 1, + notiz TEXT +); + +CREATE TABLE IF NOT EXISTS drucker_status ( + drucker_id INTEGER PRIMARY KEY, + status TEXT, -- 'idle'|'printing'|'paused'|'finished'|'failed'|'offline' + job_name TEXT, + progress_pct REAL, + restzeit_min INTEGER, + bett_temp REAL, + duese_temp REAL, + fehlermeldung TEXT, + gepollt_am DATETIME, + manuell_gesetzt INTEGER DEFAULT 0, -- 1 = Lehrer hat Override gesetzt + FOREIGN KEY (drucker_id) REFERENCES drucker(id) ON DELETE CASCADE +); + +-- =========================================================================== +-- Druck-Auftraege + Slots (Etappe 7) +-- =========================================================================== +CREATE TABLE IF NOT EXISTS druckauftrag ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + profil_id INTEGER NOT NULL, + projekt_id INTEGER, + titel TEXT NOT NULL, + geschaetzte_dauer_min INTEGER, + farbe TEXT, -- 'PLA rot', 'PLA+ schwarz' etc. + notiz TEXT, + status TEXT DEFAULT 'geplant', -- 'geplant'|'reserviert'|'druckt'|'fertig'|'abgeholt'|'fehlgeschlagen'|'abgebrochen' + drucker_id INTEGER, + erstellt_am DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (profil_id) REFERENCES profil(id) ON DELETE CASCADE, + FOREIGN KEY (projekt_id) REFERENCES projekt(id) ON DELETE SET NULL, + FOREIGN KEY (drucker_id) REFERENCES drucker(id) ON DELETE SET NULL +); + +CREATE TABLE IF NOT EXISTS slot ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + drucker_id INTEGER NOT NULL, + profil_id INTEGER NOT NULL, + druckauftrag_id INTEGER, + start_zeit DATETIME NOT NULL, + ende_zeit DATETIME NOT NULL, + prioritaet INTEGER DEFAULT 0, + status TEXT DEFAULT 'reserviert', -- 'reserviert'|'aktiv'|'beendet'|'storniert' + notiz TEXT, + erstellt_am DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (drucker_id) REFERENCES drucker(id) ON DELETE CASCADE, + FOREIGN KEY (profil_id) REFERENCES profil(id) ON DELETE CASCADE, + FOREIGN KEY (druckauftrag_id) REFERENCES druckauftrag(id) ON DELETE SET NULL +); + +-- =========================================================================== +-- Lektionen (Onboarding-Pfad + freie Module) +-- =========================================================================== +CREATE TABLE IF NOT EXISTS lektion ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + nummer INTEGER UNIQUE NOT NULL, + titel TEXT NOT NULL, + onboarding INTEGER DEFAULT 0, -- 1 = Pflicht fuer neue SuS (Onboarding-Pfad) + aufgabe_md TEXT NOT NULL, + tipp_md TEXT, + extern_link TEXT, + schaltplan_bild BLOB, + schaltplan_mime TEXT, + schwierigkeit TEXT DEFAULT 'leicht', + aktiv INTEGER DEFAULT 1 +); + +-- =========================================================================== +-- Einstellungen (Inline-Edit-Konfig) + Audit-Log +-- =========================================================================== +CREATE TABLE IF NOT EXISTS einstellung ( + schluessel TEXT PRIMARY KEY, + wert TEXT NOT NULL, + typ TEXT DEFAULT 'text', + beschreibung TEXT, + geaendert_am DATETIME DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS aenderung_log ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + tabelle TEXT NOT NULL, + datensatz_id INTEGER, + feld TEXT, + alter_wert TEXT, + neuer_wert TEXT, + durch_profil_id INTEGER, -- Lehrer (NULL = system/admin) + geaendert_am DATETIME DEFAULT CURRENT_TIMESTAMP +); +""" + + +def init_db(): + """Legt alle Tabellen an (idempotent).""" + conn = get_db() + conn.executescript(SCHEMA) + conn.commit() + conn.close() + print(f"Schema initialisiert in {DB_PFAD}") + + +if __name__ == "__main__": + init_db() diff --git a/docs/architektur.md b/docs/architektur.md new file mode 100644 index 0000000..f82cc6d --- /dev/null +++ b/docs/architektur.md @@ -0,0 +1,111 @@ +--- +type: konzept-modul +projekt: 3d.lehrstun.de +stand: 2026-05-23 +tags: [konzept, architektur] +--- + +# Architektur + +> Komponentensicht der 3D-Druck-AG-Plattform. Detail-Schema siehe [[datenmodell]], +> Bambu-Anbindung siehe [[druckerapi]], Reservierungen siehe [[slotsystem]]. + +## 1. Komponenten + +```mermaid +flowchart LR + subgraph Browser["Browser"] + SUS[SuS-Cockpit] + LEHRER[Lehrer-Dashboard] + OEFF[Öffentliche Seiten] + end + + subgraph Server["Hetzner: 3d.lehrstun.de:5057"] + FL[Flask-App] + DB[(SQLite)] + POLL[Drucker-Poll-Worker
alle ~30 s] + FL --- DB + POLL --- DB + end + + subgraph Schule["Schule"] + BS_PC[Bambu Studio
auf Schul-PCs] + DRU[4 Bambu-Drucker] + BS_PC -.WLAN.-> DRU + end + + subgraph Cloud["Bambu Cloud"] + BAMBU[api.bambulab.com
+ MQTT] + end + + Browser -->|HTTPS| FL + POLL -->|HTTPS| BAMBU + DRU -->|MQTT| BAMBU + BS_PC -->|HTTPS| BAMBU +``` + +## 2. Datenfluss bei einem Druckauftrag + +```mermaid +sequenceDiagram + actor SuS + participant Site as 3d.lehrstun.de + participant DB as SQLite + participant Studio as Bambu Studio + participant Drucker + participant Cloud as Bambu Cloud + + SuS->>Site: Druckauftrag eintragen (Titel, Dauer, Material) + Site->>DB: druckauftrag (status=geplant) + SuS->>Site: Slot reservieren (Drucker X, 14:00-16:00) + Site->>DB: slot (status=reserviert) + + Note over SuS,Studio: SuS slict ausserhalb der Site + + SuS->>Studio: Modell slicen + Studio->>Cloud: Job hochladen + Cloud->>Drucker: Job starten + Drucker->>Cloud: Status (printing, ETA, ...) + + loop alle 30 s + Site->>Cloud: GET Status aller Drucker + Cloud-->>Site: JSON + Site->>DB: drucker_status aktualisieren + end + + SuS->>Site: Cockpit aufrufen + Site-->>SuS: "Dein Job auf Drucker X läuft, noch 1h 20m" + + Drucker->>Cloud: Status (finished) + Site->>Cloud: GET (im nächsten Poll) + Site->>DB: druckauftrag.status = fertig + SuS->>Site: "Abgeholt" klicken + Site->>DB: druckauftrag.status = abgeholt +``` + +## 3. Fehlerfälle + +```mermaid +flowchart TB + A[Drucker meldet 'failed' an Cloud] --> B{Poll erkennt es} + B --> C[drucker_status.status = failed] + C --> D[Dashboard zeigt rote Markierung] + D --> E{Aktive Slots betroffen?} + E -- ja --> F[Vorschlag: Slot auf anderen Drucker verschieben] + E -- nein --> G[Nur Drucker als 'Wartung nötig' markieren] + F --> H[Herb/Markus klickt 'Verschieben'] + H --> I[Slot.drucker_id neu, Notiz im SuS-Cockpit] + + J[Cloud-API antwortet nicht] --> K{Letzter Poll-Erfolg > 5 Min her?} + K -- ja --> L[UI: 'Status veraltet'] + L --> M[Admin: Button 'Manueller Status'] + K -- nein --> N[Normal weiter] +``` + +## 4. Querverweise + +- Detail-Tabellen: [[datenmodell]] +- Welche Routes welche Komponenten füttern: [[routen]] +- Wie das Polling konkret läuft: [[druckerapi]] +- Wie Slots verteilt werden: [[slotsystem]] +- Welche Etappe was baut: [[etappen]] diff --git a/docs/datenmodell.md b/docs/datenmodell.md new file mode 100644 index 0000000..603df6d --- /dev/null +++ b/docs/datenmodell.md @@ -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]]). diff --git a/docs/decisions.md b/docs/decisions.md new file mode 100644 index 0000000..e838f8d --- /dev/null +++ b/docs/decisions.md @@ -0,0 +1,166 @@ +--- +type: decisions-log +projekt: 3d.lehrstun.de +tags: [entscheidungen] +--- + +# Entscheidungs- und Änderungslog — 3d.lehrstun.de + +> Anker-Format: `[[decisions#E-NNN]]` aus anderen Dateien verlinkt direkt +> auf den jeweiligen Entscheid. + +Format: chronologisch, neueste oben. Pro Entscheidung: Kontext, Optionen, Entscheidung, Begründung. + +--- + +## 2026-05-23 — Projekt-Skelett angelegt + +### E-001: Bestehende Arduino-Kurs-Architektur als Vorlage nehmen +**Kontext:** Neuer Kurs für 3D-Druck-AG. `arduino.lehrstun.de` läuft erprobt seit Mai 2026. +**Optionen:** (a) Neue Architektur, (b) Arduino-Kurs als Basis übernehmen + anpassen, (c) Bestehende Site erweitern. +**Entscheidung:** (b) — Arduino-Stack (Flask/SQLite/Jinja2 + Tier-PIN-Login + Inline-Edit + PummelHummel) als Basis, eigenständige Site. +**Begründung:** Herb will einheitliches Design. Tier-PIN-Login + Migrations-System + Admin-Inline-Edit haben sich im Schultest 2026-05-11 bewährt. Eigenständige Site, weil andere Domain (`3d.lehrstun.de`) und Inhalte komplett anders. + +### E-002: SuS-Profil als Primär-Objekt (statt Gruppe) +**Kontext:** AG hat 8-10 SuS, optional 2er-Gruppen. +**Optionen:** (a) Gruppe primär (wie Arduino), (b) Profil primär, Paarung sekundär. +**Entscheidung:** (b). +**Begründung:** Bei einer AG ändert sich die Sozialform — Einzelarbeit ist hier der Normalfall, Paarung ein Sonderfall. Das ist anders als beim Arduino-Pflichtkurs, wo 2er-Gruppen fest waren. Datenmodell muss das abbilden: `profil` + optional `paarung`. +**Wirkt sich aus auf:** [[datenmodell#profil]], [[datenmodell#paarung]] + +### E-003: Login = Vorname + Tier-Icon + 4-stellige PIN +**Kontext:** SuS-Identifikation auf der Site. +**Optionen:** (a) Tier-PIN (wie Arduino), (b) Vorname + PIN, (c) TinkerCAD-Style (Klassencode + Spitzname), (d) Hybrid Vorname+Tier+PIN. +**Entscheidung:** (d) — Vorname (Klarname) + Tier-Icon (visuell) + 4-stellige PIN. +**Begründung:** Herb-Wunsch. Vorname für persönliche Ansprache, Tier-Icon für Differenzierung bei Namens-Duplikaten und als spielerisches Element, PIN als Auth. + +### E-004: Sichtbarkeit zwischen SuS — nur Projekttitel +**Kontext:** SuS sollen sich gegenseitig inspirieren, aber kein Druck durch Stand-Vergleich. +**Optionen:** (a) Nur Lehrer sieht alles, (b) alle SuS sehen alles, (c) Lehrer voll + SuS sehen nur Projekttitel. +**Entscheidung:** (c). +**Begründung:** Inspiration ja (Projekttitel sind Anregung), Vergleich nein (Stand/Drucke/Probleme bleiben privat). + +### E-005: Drucker-Status via Bambu Cloud-API (kein Pi-Bridge) +**Kontext:** Drucker stehen im Schul-LAN, Site auf Hetzner. Live-Status gewünscht. +**Optionen:** (a) Raspberry Pi in der Schule als MQTT-Bridge, (b) Cloud-API direkt vom Hetzner pollen, (c) manuell, (d) gar nicht. +**Entscheidung:** (b). +**Begründung:** Herb: "Alle SuS in der AG haben über Handy-App Cloud-Zugang zu den Druckern, bisher keine schlechten Erfahrungen." Cloud-API ist akzeptabel, kein Pi-Hardware-Aufwand. Risiko bekannt: Bambu ändert die API ab und zu → Fallback "manueller Status" eingeplant. +**Wirkt sich aus auf:** [[druckerapi]] + +### E-006: Kein Druckjob-Upload von der Site zum Drucker +**Kontext:** Wäre nice-to-have, aber Bambu erlaubt das nur mit „Developer Mode" am Drucker. +**Optionen:** (a) Developer Mode aktivieren + Site darf Jobs senden, (b) SuS slicen weiterhin in Bambu Studio. +**Entscheidung:** (b). +**Begründung:** Developer Mode würde Bambu-Cloud-Konto-Funktionen am Drucker abschalten. Zu hoher Preis für einen Komfort-Gewinn. Site verwaltet nur Reservierungen + zeigt Status. +**Wirkt sich aus auf:** [[druckerapi#was-wir-nicht-machen]], [[slotsystem]] + +### E-007: Drucker laufen auch über Nacht / Freistunden +**Kontext:** Druckzeit ≫ Schulstunde. +**Entscheidung:** Drucker dürfen 24/7 laufen, Slot-System bildet das ab. +**Begründung:** Druckraum mit Dauerlüftung, Bambu-Drucker mit Notabschaltung. Erlaubt komplexere Projekte. + +### E-008: PLA + PLA+ als Default, PETG/ABS nur unter Aufsicht +**Entscheidung:** Material-Auswahl bei Druckauftrag standardmäßig auf PLA-Varianten beschränkt; PETG/ABS nur freischaltbar im Admin. +**Begründung:** Herb: "Für die grundlegenden Sachen erst mal alles mit PLA (weil ungefährlich)." +**Wirkt sich aus auf:** [[sicherheit]], [[datenmodell#druckauftrag]] + +### E-009: Site-Inhalt bei Kursstart = Skelett + L1-L3 fertig +**Entscheidung:** Lektionen L4+ und Software-Detailseiten kommen später inline durch Herb + Markus. +**Begründung:** Herb hat 3D-Druck selbst noch nie unterrichtet — Inhalt entsteht im Doing. Admin-Inline-Edit muss daher von Anfang an stabil sein. + +### E-010: Brand = "3D-Druck-AG" +**Begründung:** Herb-Entscheidung. Passt zum tatsächlichen Format (Wahl-AG, nicht Kurs). + +### E-011: Obsidian-tauglich, aber kein Plugin-Zwang +**Kontext:** `D:\Claude-Projekte\` ist bereits Obsidian-Vault. Markdown + Wikilinks sollten überall funktionieren. +**Entscheidung:** Doku-Dateien als reines Markdown mit normalen Links (keine `[[wikilinks]]`-Pflicht), damit sie auch in jedem anderen Editor lesbar bleiben. Wikilinks dürfen ergänzt werden, sind aber optional. +**Begründung:** Markus arbeitet vielleicht nicht mit Obsidian — Doku muss universell lesbar bleiben. + +--- + +## 2026-05-24 — Reality-Check + Selbstlern-Loop + +Nach Analyse aus vier Perspektiven (neuer SuS / erfahrener SuS / Lehrer / technische Machbarkeit) plus Vergleich mit `arduino.lehrstun.de`. Vollständige Analyse: `~/.claude/plans/f-hre-noch-einmal-eina-cosmic-wind.md` (approved). + +### E-012: AG ist offen, jahrelang, keine festen Termine +**Kontext:** Hatte ursprünglich „12 Termine à 60 Min" vom Arduino-Trimester unwissend übernommen. +**Entscheidung:** Die AG ist Nachmittags-Wahlangebot, läuft über Jahre, kein Lehrplan, freier Zugang. +**Begründung:** Herb-Korrektur. Bedeutet: Mitgliedschaften sind dauerhaft, Onboarding-Pfad für Neue, danach freie Bibliotheks-Nutzung. +**Wirkt sich aus auf:** [[kurskonzept]], [[etappen]] + +### E-013: Onboarding-Pfad statt Pflicht-Lektionen +**Kontext:** „L1-L3 Pflicht für alle" passt nicht zu einer freien AG. +**Entscheidung:** L1-L3 werden zum **Onboarding-Pfad** — Pflicht nur für Neue. Erledigt → unsichtbar im Cockpit. +**Begründung:** Bei kontinuierlicher AG-Mitgliedschaft kommt nicht jeder gleichzeitig dazu. Onboarding ist Einstiegs-Werkzeug, kein Curriculum. +**Wirkt sich aus auf:** [[kurskonzept]], [[selbstlern-loop]], [[datenmodell#profil_lektion_status]] + +### E-014: Selbstlern-Loop mit interner Projekt-Bibliothek +**Kontext:** Mentoring-Idee von Herb. Die Site wächst über die Jahre durch SuS-Projekte selbst. +**Entscheidung:** Vollständiger Projekt-Lebenszyklus: Stöbern → Auswählen → Arbeiten → Abschluss-Checkliste → Lehrer-Review → AG-öffentlich. Vernetzung über Stöbern (Variante 1), nicht Auto-Match. +**Begründung:** Macht die Site wertvoller als jeden externen Katalog. SuS sehen „bei uns hat das schonmal jemand gemacht". Lehrer-Review ist Qualitätsfilter. +**Wirkt sich aus auf:** [[selbstlern-loop]], [[datenmodell#projekt]], [[routen]] + +### E-015: Feedback nach Abschluss = A-Variante (sachlich) + optionaler Freitext +**Entscheidung:** Drei Stufen: 🟢 zufrieden / 🟡 würde Sachen anders machen / 🔴 nicht geklappt, aber gelernt. Optionales Freitext-Feld „Was würdest du anderen mitgeben?". +**Begründung:** Sachliche Sprache schlägt Maker-Slang oder Sterne-Bewertung. Freitext optional, weil 8.-Klässler Pflicht-Freitext oft nicht ausfüllen. +**Wirkt sich aus auf:** [[selbstlern-loop]], [[datenmodell#projekt]] + +### E-016: File-Storage als BLOB in SQLite, 30 MB pro Datei, abstrahiert +**Kontext:** Projekt-Dateien (STL, 3MF, Fotos) gehören zum Lebenszyklus. +**Entscheidung:** Files in SQLite-BLOBs, Limit 30 MB pro Datei. File-Handling über `services/datei.py` abstrahieren, damit später Migration auf Hetzner-Object-Storage / S3 möglich ist. +**Begründung:** SQLite hält 2-3 GB ohne Performance-Probleme; bei 8 SuS × 5 Projekte/Jahr × 5 Files × 10 MB → ~2 GB/Jahr. Reicht für die ersten 5 AG-Jahre. +**Wirkt sich aus auf:** [[datenmodell#projekt_datei]] + +### E-017: Lehrer-Review mit drei Aktionen (Variante 3) +**Entscheidung:** Lehrer kann beim Review (a) freigeben (mit oder ohne eigene Bearbeitung), (b) Rückfrage stellen (Status zurück + Notiz im Cockpit), (c) hart löschen (für Penismodelle, Copyright-Probleme). +**Begründung:** Flexibilität. Hartes Löschen mit Audit-Log-Eintrag (Datum, Lehrer, SuS, optional Grund — aber Dateien sind weg). +**Wirkt sich aus auf:** [[lehrer-dashboard]], [[selbstlern-loop]] + +### E-018: 3D-Vorschau mit Three.js — Cover-Foto aus 3D-Ansicht +**Kontext:** SuS vergisst manchmal Cover-Foto. Lehrer beim Review oder SuS selbst soll Screenshot aus STL-Vorschau machen können. +**Entscheidung:** Three.js + STLLoader im Browser einbinden (zwei JS-Dateien in `static/js/`, kein npm, kein Build). +**Begründung:** Herb-OK. Externe Frontend-Dependency, ca. 600 KB. Funktioniert offline auf jedem modernen Browser, inkl. iPad. Falls Browser-Test scheitert: Lehrer macht Screenshot in Bambu Studio manuell. +**Wirkt sich aus auf:** [[selbstlern-loop]], `static/js/three.min.js` + `STLLoader.js` + +### E-019: Onboarding-Pfad = Selbst-Zertifizierung +**Kontext:** Wer markiert eine Onboarding-Lektion als „fertig"? +**Optionen:** (a) SuS klickt selbst „fertig", (b) Lehrer nickt ab. +**Entscheidung:** (a) — selbst-zertifiziert, analog zu `gruppe_lektion_status` im Arduino-Kurs. +**Begründung:** Lehrer sieht im Drill-Down-Dashboard ohnehin Fortschritts-Stände — Abnahme-Workflow wäre Overhead. Vertrauen geht vor Kontrolle. +**Wirkt sich aus auf:** [[datenmodell#profil_lektion_status]], [[selbstlern-loop]] + +### E-020: 8 Empfehlungen aus Reality-Check angenommen (V1-V8) +- **V1 — Cockpit progressiv:** zeigt nur was zum Status passt (Onboarding nicht erledigt → nur Onboarding-Karte; nach Abschluss kommen Projekt-Karte, Slot-Buchung etc. dazu). +- **V2 — Slot-Buchung erst nach L1 (Sicherheits-Lektion).** Bis dahin Hinweis statt Button. +- **V3 — Stunden-Beginn-Abfrage radikal kurz.** Ein Radio, ein optionales Freitext-Feld. Default: „Ich arbeite an X weiter" als Ein-Klick-Option. +- **V4 — Abschluss-Checkliste auf 3 Pflicht-Felder runter** (Selbsteinschätzung + Cover-Foto **oder** STL + 1 Tag). Rest optional. +- **V5 — Drill-Down-Lehrer-Dashboard** mit „Wer ist heute hier / Wartet auf Review / Wer hängt fest". +- **V6 — Rückfrage-Mechanik via Cockpit-Banner**, keine E-Mail. +- **V7 — Persistenter Login-Lockout in DB-Tabelle** (statt In-Memory wie Arduino-Kurs). +- **V8 — Blueprint-Struktur von Anfang an** (`routes/oeffentlich.py`, `routes/profil.py`, `routes/admin.py`, `routes/api.py`, `services/bambu.py`). + +### E-021: Herb-Zusatz „Ich arbeite weiter"-Knopf +**Kontext:** Zu V1/V3 ergänzt: bei laufenden Projekten soll das Stunden-Beginn-Modal mit einem Klick übersprungen werden können. +**Entscheidung:** Modal hat als allererste Option „Ich arbeite an meinem laufenden Projekt weiter" — bei Klick: kein Freitext nötig, direkt ins Cockpit. Erkennt das System, dass ein aktives Projekt da ist, ist diese Option vorausgewählt. +**Begründung:** Reibungsfreie Routine für erfahrene SuS, die wochenlang am gleichen Projekt arbeiten. +**Wirkt sich aus auf:** [[selbstlern-loop]], [[datenmodell#check_in]] + +### E-022: ZSL-Material in 8-10 Projekten umarbeiten, Rest später +**Kontext:** ZSL Baden-Württemberg liefert 20+ CC-BY-4.0-Projekte. Zu viel auf einmal. +**Entscheidung:** Markus + Herb wählen 8-10 Projekte aus (gemischte Schwierigkeit), arbeiten Beschreibungen + Bilder in unser Design um (mit Urhebernachweis). Rest als Notiz „weitere verfügbar" — SuS-Projekte ergänzen die Bibliothek automatisch über die Zeit. +**Begründung:** Keep it small (Herb). Wachstum kommt aus SuS-Projekten + späterer Erweiterung durch Lehrer-Inline-Edit. +**Wirkt sich aus auf:** [[selbstlern-loop]], [[offene-fragen]] + +--- + +## Vorlage für neue Einträge + +``` +## YYYY-MM-DD — Titel + +### E-NNN: Kurz-Titel der Entscheidung +**Kontext:** +**Optionen:** +**Entscheidung:** +**Begründung:** +``` diff --git a/docs/druckerapi.md b/docs/druckerapi.md new file mode 100644 index 0000000..c6b7534 --- /dev/null +++ b/docs/druckerapi.md @@ -0,0 +1,96 @@ +--- +type: konzept-modul +projekt: 3d.lehrstun.de +stand: 2026-05-23 +tags: [konzept, druckerapi, bambu] +--- + +# Drucker-API-Anbindung (Bambu Cloud) + +> Architektur-Einbettung: [[architektur#3-Fehlerfälle]]. +> Entscheidung dazu: [[../decisions#E-005]] und [[../decisions#E-006]]. + +## Ziel + +Live-Status aller 4 Bambu-Lab-Drucker auf der Site anzeigen, mit Fallback auf manuellen Status bei API-Ausfall. + +## Variante (gewählt): Cloud-API-Polling vom Hetzner + +```mermaid +flowchart LR + PW[Poll-Worker
auf Hetzner] -->|GET alle 30s| BAMBU[api.bambulab.com] + BAMBU --> PW + PW --> DB[(drucker_status)] + FE[Frontend] -.alle 30s.-> API[/api/drucker/status/] + API --> DB +``` + +- **Auth:** Bambu-Cloud-Konto-Credentials in `.env` (`BAMBU_EMAIL`, `BAMBU_PASSWORT`, `BAMBU_REGION=eu`). +- **Lib:** [`bambulabs_api`](https://github.com/BambuTools/bambulabs_api) (Python, aktiv gepflegt). Alternativ Code aus [`pybambu`](https://github.com/greghesp/ha-bambulab) (Home-Assistant-Integration) extrahieren. +- **Frequenz:** ~30 s pro Drucker. Bei 4 Druckern unkritisch. +- **Polling-Worker:** entweder als Hintergrund-Thread in Flask oder eigener systemd-Service (saubere Trennung, wird in [[etappen#etappe-5]] entschieden). + +## Welche Felder lesen wir? + +| Feld | Beispiel | Pflicht? | +|---|---|---| +| `status` | `printing`, `idle`, `paused`, `finished`, `failed`, `offline` | ja | +| `job_name` | "schluesselanhaenger_mia.3mf" | ja | +| `progress_pct` | 67.5 | ja | +| `restzeit_min` | 42 | ja | +| `bett_temp` | 58.2 | nein | +| `duese_temp` | 210.0 | nein | +| `fehlermeldung` | "AMS error: filament jam" | nein | + +Die Bonus-Felder werden im UI nur angezeigt, wenn vorhanden. + +## Fallback-Strategie + +```mermaid +flowchart TB + A[Poll-Versuch] --> B{Erfolg?} + B -- ja --> C[drucker_status aktualisiert] + B -- nein --> D[Lasse alten Wert stehen] + D --> E{gepollt_am > 5 Min alt?} + E -- ja --> F[UI: 'Status veraltet'] + F --> G[Admin: Button 'Manueller Status'] + E -- nein --> H[Noch ignorierbar] +``` + +- **Admin-Override**: `/admin/drucker//manueller_status` — Herb/Markus setzen manuell, überschreibt nächsten Poll. +- **Notbetrieb ohne API**: Wenn die Cloud dauerhaft nicht erreichbar ist, kann der Slot-Betrieb komplett manuell laufen — SuS sehen Status nur über Bambu Handy App, Reservierungen funktionieren weiter, weil sie nicht von API abhängen. + +## Was wir **nicht** machen + +- **Keine Druckjob-Übertragung von der Site zum Drucker.** Würde "Developer Mode" am Drucker brauchen → Verlust der Bambu-Cloud-Funktionen. Siehe [[../decisions#E-006]]. +- **Keine Kamera-Streams** auf der Site. SuS nutzen die Bambu Handy App fürs Bild. DSGVO-Frage in der Schule wäre sonst aufwendig. + +## Spike vor Integration + +Bevor wir das in die Site einbauen, baue ich ein eigenständiges Script: + +``` +scripts/drucker_api_spike.py +``` + +- Liest Credentials aus `.env`. +- Fragt einmalig alle 4 Drucker ab. +- Gibt JSON auf STDOUT. +- Validiert: funktioniert die Auth? Werden alle 4 Drucker gefunden? Welche Felder liefert die API? + +Wenn der Spike läuft, übernehmen wir die Logik in einen sauberen Poll-Worker (Etappe 5). + +## Risiken (Stand 2026-05) + +- **Bambu hat 2024/25 die API mehrfach geändert** ("Authorization Control"-Update). Status-Lesen funktioniert weiterhin, aber kann jederzeit brechen. +- **Inoffizielle API**: Bambu supportet keine Drittsoftware. Bei Bruch helfen nur die GitHub-Communities (`bambulabs_api`, `pybambu`). +- **Cloud-Login-Flow** ändert sich gelegentlich (2FA, Captcha). Wenn das passiert, Spike-Script anpassen. + +→ Deshalb ist [[slotsystem]] so gebaut, dass es **ohne** Drucker-API funktioniert. Status-Anzeige ist Komfort, nicht Pflicht. + +## Querverweise + +- DB-Schema: [[datenmodell#drucker]] +- Route: `/api/drucker/status` siehe [[routen]] +- Architektur: [[architektur]] +- Entscheidungen: [[../decisions#E-005]], [[../decisions#E-006]] diff --git a/docs/etappen.md b/docs/etappen.md new file mode 100644 index 0000000..91d457e --- /dev/null +++ b/docs/etappen.md @@ -0,0 +1,195 @@ +--- +type: konzept-modul +projekt: 3d.lehrstun.de +stand: 2026-05-24 +tags: [konzept, plan, etappen] +--- + +# Implementierungs-Etappen + +> Grobe Reihenfolge zum Bauen. Detaillierte Inhalte: [[../kurskonzept]]. +> **V8 ([[../decisions#E-020]]): Blueprint-Struktur von Anfang an** — `app.py` +> ist Bootstrap, Routen in `routes/`, externe Services in `services/`. + +## Etappe 0 — Skelett (✓ am 2026-05-23) + +- [x] Projektordner + CLAUDE.md + README.md + CHANGELOG.md +- [x] [[architektur]] + [[datenmodell]] + [[routen]] + [[druckerapi]] + [[slotsystem]] + [[sicherheit]] + [[etappen]] + [[selbstlern-loop]] + [[lehrer-dashboard]] +- [x] Flask-Skelett (app.py mit Stubs, database.py, tiere.py) +- [x] base.html portiert vom Arduino-Kurs +- [x] [[../COCKPIT]] als Vault-Startseite + .obsidian/-Konfig +- [x] Reality-Check-Iteration mit Selbstlern-Loop, V1-V8 angenommen (2026-05-24) + +## Etappe 1 — Basis: Blueprints + Login + Profile + Inline-Edit (✓ am 2026-05-24) + +**Blueprint-Refactoring (V8):** +- [x] `routes/oeffentlich.py`, `routes/profil.py`, `routes/admin.py`, `routes/api.py` anlegen +- [x] `services/datei.py` anlegen (abstrahiertes File-Handling, Stubs) +- [x] `services/bambu.py` anlegen (Stub, Implementation folgt in Etappe 4) +- [x] `services/auth.py` anlegen (bcrypt + persistenter Lockout + Decorators) +- [x] `app.py` zur Bootstrap-Datei umbauen (90 Zeilen, Blueprint-Registrierung) + +**Schema:** +- [x] `init_db()` aktualisieren mit allen Tabellen aus [[datenmodell]] +- [x] `ag_mitgliedschaft` Initial-Seed („lessing-3d-ag") +- [x] 4 Drucker-Stammdaten als Seeds +- [x] Default-Einstellungen (`tag_vorschlaege`, `default_lockout_dauer_min`, `feedback_widget_aktiv`) + +**Login + Profile:** +- [x] Profil-Kacheln-Seite mit Vorname + Tier +- [x] PIN-Eingabe mit `bcrypt`-Check +- [x] **Persistenter Login-Lockout** in `login_lockout`-Tabelle (V7) statt In-Memory wie Arduino +- [x] `/logout` +- [x] Admin-Login mit Passwort aus `.env` + +**Profil-Verwaltung im Admin:** +- [x] `/admin/profile` Liste mit aktiv-Toggle, PIN-Reset +- [x] `/admin/profil/neu` mit PIN-Generation (einmalig sichtbar via Flash) +- [x] `/admin/profile/karten` Druckbogen (CSS-Print) + +**Inline-Edit-Infrastruktur:** +- [x] `ERLAUBTE_INLINE_TABELLEN`-Dict mit Whitelist (Backend) +- [x] `/admin/inline-edit`-POST mit Audit-Log in `aenderung_log` +- [ ] base.html-JS für Inline-Edit-UI (kommt in Etappe 2, wenn das erste editierbare Feld in der UI sichtbar wird) +- [ ] CSRF-Token (kommt in Etappe 2, sobald Forms wachsen) + +## Etappe 2 — Cockpit (progressiv) + Check-In + Paarung + Lehrer-Dashboard (1 Sitzung) + +**Cockpit (V1 — progressives Layout):** +- [ ] `/cockpit` rendert je nach Status: + - Onboarding nicht erledigt → nur Onboarding-Karte + Check-In-Trigger + - L1 fertig → zusätzlich: Drucker-Buchung freigeschaltet + - Hat aktive Projekte → zusätzlich: eigene Projekte, Druckaufträge, Slots +- [ ] Cockpit-Banner für offene Rückfragen (V6) +- [ ] Cockpit-Banner für ungelesene Lehrer-Notizen + +**Check-In (V3 + E-021):** +- [ ] Modal beim ersten Cockpit-Aufruf eines Besuchstages +- [ ] Default-Option „Ich arbeite an X weiter" wenn aktives Projekt vorhanden +- [ ] `/cockpit/check-in`-POST speichert in `check_in`-Tabelle + +**Paarung:** +- [ ] `/cockpit/paarung/anfragen/` + Bestätigungsmechanik +- [ ] `/cockpit/paarung/aufloesen` + +**Lehrer-Dashboard (V5, [[lehrer-dashboard]]):** +- [ ] `/admin/dashboard` mit 4 Sektionen (Wer hier / Wartet Review / Drucker-Status / Hängt fest) +- [ ] Badge im Header für „wartet auf Review"-Anzahl +- [ ] „Notiz hinterlassen"-Quick-Action via `lehrer_notiz`-Tabelle + +## Etappe 3 — Lektionen + Software-Sektion + Troubleshooting (1 Sitzung) + +- [ ] `/lektionen` mit Onboarding-Pfad-Markierung +- [ ] `/lektion/` mit Markdown-Rendering + Inline-Edit (für Lehrer) +- [ ] `/cockpit/lektion//status` (selbst-zertifizierend, [[../decisions#E-019]]) +- [ ] `/software` + `/software/` (CMS-artig, Inline-Edit) +- [ ] `/troubleshooting` mit Entscheidungsbaum +- [ ] L1-L3 Inhalte ausgearbeitet (mit Markus zusammen, ggf. asynchron) + +## Etappe 4 — Drucker-API-Spike (0.5 Sitzung) + +- [ ] `scripts/drucker_api_spike.py` — einmalige Abfrage aller 4 Drucker +- [ ] Bambu-Cloud-Credentials in `.env` +- [ ] Validierung: Auth funktioniert, alle 4 Drucker erreichbar, welche Felder liefern sie +- [ ] Entscheidung: `bambulabs_api` direkt nutzen oder Code aus `pybambu` extrahieren + +## Etappe 5 — Drucker-Integration + Live-Status (1 Sitzung) + +- [ ] `services/bambu.py` implementieren (Poll-Funktion) +- [ ] Poll-Worker als Hintergrund-Thread in Flask (oder eigener systemd-Service) +- [ ] `/drucker` Live-View +- [ ] `/api/drucker/status` JSON-Endpunkt für Polling +- [ ] `/admin/drucker` + manueller Status-Override +- [ ] UI: „Status veraltet"-Banner wenn Poll > 5 Min alt + +## Etappe 6 — Selbstlern-Loop: Projekt-Katalog + Lebenszyklus + Files + Review + Three.js (2 Sitzungen) + +**Datenmodell + Backend:** +- [ ] Alle Selbstlern-Loop-Tabellen aus [[datenmodell]] anlegen +- [ ] `services/datei.py` voll implementieren (Upload, Größen-Check, Download, Löschen) + +**Projekt-Workflow im Cockpit:** +- [ ] `/cockpit/projekt/neu` (Vorlage aus Katalog wählen oder eigene Idee) +- [ ] `/cockpit/projekt//edit` während `in_arbeit` +- [ ] Datei-Upload-UI (mit Drag&Drop, 30 MB-Limit) +- [ ] `/cockpit/projekt//abschluss` mit V4-Checkliste (3 Pflicht-Felder) + +**Projekt-Katalog:** +- [ ] `/projekte` mit Filter (Tag, Schwierigkeit, Erfolg, Macher) +- [ ] `/projekte/` Detail-Seite +- [ ] `/api/projekte/suche` für Live-Suche +- [ ] „Sowas mache ich auch"-Button → neues Projekt mit `vorlage_projekt_id` + +**Three.js-Vorschau (V3-Aktion + E-018):** +- [ ] `static/js/three.min.js` + `static/js/STLLoader.js` (zwei einzelne JS-Dateien, kein npm) +- [ ] STL-Viewer-Widget für Detail-Seite und Lehrer-Review +- [ ] „Aus 3D-Ansicht Cover speichern"-Funktion (Canvas-Screenshot) + +**Lehrer-Review (V3-Aktionen + E-017):** +- [ ] `/admin/projekt-freigabe` Queue +- [ ] `/admin/projekt-freigabe/` Detail mit Three.js + Inline-Edit +- [ ] Freigeben / Rückfrage / Hart-Löschen-Aktionen +- [ ] Audit-Log für Löschungen + +**Rückfrage-Mechanik (V6):** +- [ ] `/cockpit/projekt//rueckfrage//erledigt` als Reaktions-Endpunkt +- [ ] Banner-Rendering im Cockpit +- [ ] Lehrer-Sicht der Rückfragen-Antworten + +## Etappe 7 — Slot-System + Druckaufträge (1 Sitzung) + +**Slots:** +- [ ] `/cockpit/slots` Kalender-Ansicht +- [ ] `/cockpit/slot/reservieren` mit Konflikt-Check +- [ ] **V2-Gate**: Slot-Routen mit Decorator `@onboarding_l1_fertig` schützen +- [ ] `/admin/slot//prioritaet` Lehrer-Priorisierung +- [ ] `/admin/slot//verschieben` Drucker-Wechsel bei Ausfall + +**Druckaufträge:** +- [ ] `/cockpit/druckauftrag/neu` (verknüpft mit Projekt + Slot) +- [ ] `/cockpit/druckauftraege` Eigene Liste +- [ ] `/admin/druckauftraege` Queue mit Freigabe für lange Drucke + +## Etappe 8 — ZSL-Material-Import (1 Sitzung mit Markus) + +- [ ] Markus + Herb wählen 8-10 ZSL-Projekte aus +- [ ] Pro Projekt: Beschreibung in unser Format umarbeiten (CC-BY-4.0-Nachweis als Footer) +- [ ] Bilder + STLs in `projekt_datei` importieren (Initial-Seed via Admin-Upload) +- [ ] Tag `zsl-vorlage` + spezifische Tags pro Projekt setzen +- [ ] Sichtbarkeits-Test im Katalog + +## Etappe 9 — Deploy + Polish (0.5 Sitzung) + +- [ ] DNS-A-Record `3d.lehrstun.de` → 46.225.114.15 (via Hetzner Cloud DNS-CLI) +- [ ] Nginx vhost + Let's-Encrypt +- [ ] systemd-Service `3d-ag.service` +- [ ] PummelHummel/Alph-Integration aus `D:/Claude-Projekte/PummelHummel/` +- [ ] PIN-Karten-Druckbogen testen +- [ ] Bambu-Cloud-Credentials produktiv setzen +- [ ] Erste echte AG-Sitzung als Soft-Launch + +## Erwarteter Gesamtaufwand + +8-10 Coding-Sitzungen. Davon: +- Etappen 1-3: Grundlage (3 Sitzungen) +- Etappen 4-5: Drucker-API (1.5 Sitzungen) +- Etappen 6-7: Selbstlern-Loop + Slots (3 Sitzungen) +- Etappen 8-9: Inhalte + Deploy (1.5 Sitzungen) + +## Reihenfolge-Hinweise + +- **Etappen 1-3 zusammenhängend** bauen, sonst nichts funktioniert sinnvoll. +- **Etappe 4 (Spike)** früh, weil davon abhängt, ob die Drucker-Anbindung überhaupt geht. +- **Etappen 6 (Selbstlern-Loop) und 7 (Slots) sind unabhängig** — können in beliebiger Reihenfolge. +- **Etappe 8 (ZSL-Inhalte) kann parallel** zu 6 laufen (Markus pflegt Inhalte ein, während ich Code schreibe). +- **Etappe 9 (Deploy) erst, wenn 1-7 lokal stabil läuft.** + +## Querverweise + +- Konzept-Übersicht: [[../kurskonzept]] +- Datenmodell pro Etappe: [[datenmodell]] +- Routen pro Etappe: [[routen]] +- Selbstlern-Loop-Details: [[selbstlern-loop]] +- Lehrer-Dashboard: [[lehrer-dashboard]] +- Entscheidungen: [[decisions]] +- Offene Punkte: [[offene-fragen]] diff --git a/docs/glossar.md b/docs/glossar.md new file mode 100644 index 0000000..27ed45d --- /dev/null +++ b/docs/glossar.md @@ -0,0 +1,66 @@ +--- +type: konzept-modul +projekt: 3d.lehrstun.de +stand: 2026-05-23 +tags: [glossar, begriffe] +--- + +# Glossar — 3D-Druck-Begriffe + +> Begriffe, die in [[../kurskonzept]] und den Lektionen vorkommen. Für SuS und +> für Markus (falls noch nicht 3D-Druck-vertraut). + +## Drucker + Hardware + +- **AMS** (Automatic Material System) — Bambu-Wechslerbox für bis zu 4 Filamentrollen, ermöglicht Multi-Color-Druck oder automatischen Rollenwechsel. +- **Bauraum** — der Bereich, in dem ein Drucker drucken kann (z.B. P1S: 256×256×256 mm). +- **Bett / Druckbett** — die beheizte Platte, auf der die erste Schicht haftet. +- **Düse / Nozzle** — die heiße Spitze, aus der das geschmolzene Filament kommt. Standardgröße 0.4 mm. +- **Hotend** — der heizbare Teil des Druckkopfs (umfasst Düse + Heizpatrone + Sensor). +- **Schichthöhe** — Höhe einer einzelnen Druckschicht (typisch 0.16-0.28 mm). Niedriger = feiner = länger. +- **Support / Stützmaterial** — temporäre Streben, die Überhänge stützen. Werden nach dem Druck abgebrochen. + +## Material + +- **PLA** (Polylactid) — biologisch abbaubares Bio-Plastik, niedriger Schmelzpunkt (~190-220°C), Standard für Schule. Siehe [[sicherheit]]. +- **PLA+** — Variante mit Additiven für mehr Zähigkeit, sonst wie PLA. +- **PETG** — etwas zäher als PLA, höhere Drucktemperatur (~230°C), klebt fest am Bett. +- **ABS** — klassisches Plastik (Lego), schmilzt bei ~240°C, Geruch beim Druck, in der Schule selten verwendet. Siehe [[sicherheit]]. +- **Filament** — der Draht (1.75 mm), aus dem gedruckt wird. Wird auf Rollen geliefert. + +## Software & Dateien + +- **STL** — Standard-Format für 3D-Modelle (nur Geometrie, keine Druckeinstellungen). +- **3MF** — moderneres Format, enthält Geometrie + Druckeinstellungen + Stützen + Plattenposition. +- **G-Code** — die maschinen-lesbare Anweisungsliste, die der Drucker tatsächlich ausführt. Wird aus STL/3MF beim Slicen generiert. +- **Slicer** — die Software, die ein 3D-Modell in Schichten zerlegt und G-Code daraus macht. Bei uns: [[software-bambu-studio|Bambu Studio]]. +- **CAD** (Computer Aided Design) — Software, mit der 3D-Modelle entworfen werden. Bei uns: [[software-tinkercad|TinkerCAD]] und [[software-openscad|OpenSCAD]]. + +## Druckparameter + +- **Infill / Fülldichte** — wie dicht das Innere eines Drucks gefüllt wird (0% = hohl, 100% = massiv, typisch 15-30%). +- **Druckgeschwindigkeit** — mm/s, bei Bambu typisch 200-500 mm/s. +- **Adhäsion / Haftung** — wie gut die erste Schicht am Bett klebt. Wichtigster Faktor für erfolgreichen Druck. +- **Warping** — wenn ein Druck sich an den Ecken vom Bett löst und nach oben biegt. +- **Spaghetti** — Druckfehler, bei dem nicht-haftendes Filament zu einem Wurmnest wird. Drucker sofort stoppen. +- **Stringing / Fäden** — feine Fäden zwischen getrennten Druckbereichen, oft durch zu heißes Hotend. + +## Plattformen & Communities + +- **[[software-printables|Printables.com]]** — Modell-Plattform von Prusa, sauber, kein NSFW. +- **[[software-makerworld|MakerWorld]]** — Bambu-eigene Modell-Plattform, mit parametrischem Customizer. +- **Thingiverse** — älteste Modell-Plattform, aber bei uns nicht empfohlen (NSFW-Werbung). +- **Customizer** — Browser-Tool auf MakerWorld, mit dem ein fertiges parametrisches Modell (z.B. Schlüsselanhänger) angepasst werden kann, ohne CAD-Software. + +## Methoden + +- **FDM** (Fused Deposition Modeling) — was unsere Bambu-Drucker machen: heißes Filament schichtweise auftragen. Auch "FFF" genannt. +- **Resin / SLA** — andere Druckmethode mit flüssigem Kunstharz, bei uns **nicht** im Einsatz. +- **Slicen** — der Prozess, ein 3D-Modell in Druck-Schichten zu zerlegen. +- **Benchy** — kleines Test-Schiffchen (Bench-mark), Standard-Modell zum Drucker-Test/Kalibrierung. + +## Querverweise + +- Lektionen, in denen Begriffe vorkommen: [[lektion-L1-drucker-kennenlernen]], [[lektion-L2-modell-drucken]], [[lektion-L3-customizer]] +- Software-Tool-Übersicht: [[software-bambu-studio]], [[software-printables]] etc. +- Sicherheits-Aspekte: [[sicherheit]] diff --git a/docs/kurskonzept.md b/docs/kurskonzept.md new file mode 100644 index 0000000..19a8607 --- /dev/null +++ b/docs/kurskonzept.md @@ -0,0 +1,308 @@ +--- +type: konzept-hauptdokument +projekt: 3d.lehrstun.de +stand: 2026-05-24 +status: entwurf v2 +tags: [konzept, hauptdokument] +--- + +# 3D-Druck-AG — Kurskonzept (Entwurf v2) + +> **Lesehinweis Obsidian:** Das ist das Hauptdokument am Stück. Detail-Module +> liegen in eigenen Dateien — siehe [[COCKPIT]] für die Übersicht, oder direkt +> [[architektur]], [[datenmodell]], [[routen]], [[druckerapi]], [[slotsystem]], +> [[sicherheit]], [[selbstlern-loop]], [[lehrer-dashboard]], [[etappen]], +> [[offene-fragen]], [[glossar]]. + +**Stand:** 2026-05-24 (nach Reality-Check-Iteration mit Herb) +**Status:** Entwurf v2, zur Abstimmung mit Markus +**Rahmen:** Offene Nachmittags-AG am Lessing-Gymnasium, **läuft jahrelang**, +ca. 8-10 Einzel-SuS (optional 2er-Paarungen), eigener Druckraum mit +4 Bambu-Lab-Druckern (3 geschlossen + 1 A1 mini). **Unbenotet**, kein Lehrplan, +freier Zugang. Neue Mitglieder kommen über die Jahre dazu, alte gehen. + +> **Lesehinweis für Markus:** Dieses Dokument ist Entwurf v2. Es ersetzt v1 +> (2026-05-23) nach einer Reality-Check-Analyse aus vier Perspektiven (neuer +> SuS / erfahrener SuS / Lehrer / Technik). Wichtige Korrektur gegenüber v1: +> Wir haben fälschlich „12 Termine" angenommen — die AG ist tatsächlich offen +> und langfristig. Das ändert mehrere Architektur-Entscheidungen. Vollständige +> Entscheidungs-Historie: [[decisions]]. + +--- + +## 1. Didaktischer Kern + +### Onboarding-Pfad statt fester Lehrplan + +Die AG hat **keinen Curriculum**. Neue Mitglieder bekommen einen +**Onboarding-Pfad** mit 3 selbst-zertifizierten Lektionen — danach freie +Wahl, was sie machen wollen. + +| Lektion | Inhalt | Pflicht für | Nach Abschluss freigeschaltet | +|---|---|---|---| +| **L1 Drucker-Kennenlernen + Sicherheit** | Drucker-Übersicht, A1-mini-Hands-Off, Bambu Handy App einrichten, vorbereitetes Modell drucken | Neue | **Slot-Buchung** (V2 — siehe [[../decisions#E-020]]) | +| **L2 Fremdes Modell wählen + drucken** | Modell auf Printables/MakerWorld suchen, in Bambu Studio slicen, drucken | Neue | Eigene Projekte aus Katalog | +| **L3 Customizer / parametrisches Modell** | MakerWorld-Customizer: Schlüsselanhänger mit eigenem Namen | Neue | Eigenes-Projekt-Vollzugriff | +| **L4+ Eigene Modelle zeichnen** | TinkerCAD → OpenSCAD | Frei | — | +| **Eigene Projekte (Dauer-Modus)** | Idee → Modell → Druck → Iteration → Abschluss mit Reflexion → in Katalog | Frei | — | + +Nach Onboarding **verschwindet die Onboarding-Karte** aus dem Cockpit (V1 — progressives Cockpit, [[../decisions#E-020]]). Erfahrene SuS sehen nur noch ihre laufenden Projekte, Druckaufträge, Slots. + +### Warum diese Reihenfolge + +Quellen: PHBern IdeenSet, inf-schule.de, ZSL Baden-Württemberg. +- „Druck zuerst, Modellierung später" verhindert Frust. +- Customizer als Brücke zwischen „fertig laden" und „selbst zeichnen" — fehlt in den meisten Schulkonzepten, MakerWorld macht ihn so leicht zugänglich. +- L4+ ist optional und nach Interesse — TinkerCAD für klick-basierte, OpenSCAD für code-basierte SuS. + +### Selbstlern-Loop als Kern (Wachstums-Mechanismus) + +**Was die Site vom Arduino-Kurs grundsätzlich unterscheidet:** Jedes +abgeschlossene Projekt wird nach Lehrer-Freigabe in die interne Bibliothek +aufgenommen. Über die Jahre wächst eine **eigene Projekt-Sammlung** — wertvoller +als jeder externe Modell-Katalog, weil sie zeigt: „Das wurde bei uns +schonmal gemacht." + +Vollständiger Workflow: [[selbstlern-loop]]. + +Kurzfassung: +1. Neuer SuS **stöbert** im Projekt-Katalog (eigene + ZSL-Seed). +2. **Wählt** Vorlage oder eigene Idee. +3. **Arbeitet** im Cockpit (Files, Druckaufträge, Slots). +4. Beim Abschluss: **Pflicht-Checkliste** (Selbsteinschätzung 🟢🟡🔴 + Cover-Foto **oder** STL + 1 Tag — sonst alles optional). +5. **Lehrer-Review**: freigeben, Rückfrage, oder hartes Löschen (Penismodell-Fall). +6. Bei Freigabe: Projekt landet im Katalog, ist für andere SuS sichtbar. + +### Doku-Hürde + +8.-Klässler tun sich schwer mit ausführlicher Stand-Notiz. Lösung: +**Stunden-Beginn-Abfrage** (siehe §7) als „Was machst du heute?"-Mini-Modal, +das mit einem Klick beantwortbar ist („Ich arbeite an X weiter"). + +Kein KI-Lernhelfer in dieser Site. SuS nutzen Schul-Copilot. + +--- + +## 2. Architektur — eine Site, drei Zonen + +> 📎 Detail mit Mermaid-Diagrammen: [[architektur]]. Datenmodell: [[datenmodell]]. Routen: [[routen]]. +> Code-Organisation: **Blueprint-Struktur** von Anfang an (V8, [[../decisions#E-020]]). + +``` +┌─────────────────────── 3d.lehrstun.de ─────────────────────────┐ +│ │ +│ ÖFFENTLICH (kein Login) │ +│ ───────────────────────── │ +│ • Lektionen (Onboarding-Pfad + freie Module) │ +│ • Software-Sektion (Bambu Studio, Printables, ...) │ +│ • Projekt-Katalog (interner Wachstum + ZSL-Seed) │ +│ • Drucker-Status (Live, anonym) │ +│ • Troubleshooting │ +│ │ +│ ┌── Vorname + Tier-Icon + PIN-Login ─────────────────────┐ │ +│ │ │ │ +│ │ SuS-COCKPIT (PROGRESSIV, V1) │ │ +│ │ ─ Neu: nur Onboarding-Karte + Check-In │ │ +│ │ ─ Onboarding fertig: + eigene Projekte, Druck, Slots │ │ +│ │ ─ Erfahren: voll, ohne Onboarding-Karte │ │ +│ │ Bei Lehrer-Rückfrage: gelbes Banner ganz oben (V6) │ │ +│ └──────────────────────────────────────────────────────────┘ │ +│ │ +│ ADMIN (Passwort, Herb + Markus) │ +│ • Drill-Down-Dashboard (V5): Wer ist heute hier / │ +│ Wartet auf Review / Wer hängt fest / Drucker-Status │ +│ • Projekt-Freigabe-Queue (mit Three.js-Vorschau, V3-Aktion) │ +│ • SuS-Profile, PIN-Generierung, Druckbogen │ +│ • Slot-Priorisierung │ +│ • Drucker-Übersicht + manueller Status-Override │ +│ • Lektionen-/Software-Editor (inline, Whitelist) │ +└─────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 3. Datenmodell (Kurzfassung) + +> 📎 Detail mit ERD + Tabellen-Erklärung: [[datenmodell]]. Live im Code: [`database.py`](../database.py). + +**Kern-Tabellen (gegenüber v1 erweitert):** + +- `ag_mitgliedschaft` (ersetzt `kurs` — AG ist dauerhaft, nicht trimester-basiert) +- `profil` (Vorname + Tier + PIN, Pflicht-Login) +- `paarung` (optional, 2er-Zusammenschluss) +- `profil_lektion_status` ([[../decisions#E-019]] — Onboarding-Fortschritt, selbst-zertifiziert) +- `login_lockout` (V7 — persistenter Lockout in DB) +- `projekt` (vollständiger Lebenszyklus: `status_lebenszyklus`, `selbsteinschaetzung`, `freigabe_status`, `vorlage_projekt_id`) +- `projekt_datei` (BLOB, 30 MB Limit, abstrahiert) +- `projekt_tag` (für Suche und Filter) +- `projekt_rueckfrage` (Lehrer-Notizen pro Projekt) +- `lehrer_notiz` (allgemeine Notizen vom Lehrer ans SuS-Cockpit) +- `check_in` (V3-Format mit Modus + optionalem Freitext) +- `drucker`, `drucker_status`, `druckauftrag`, `slot`, `lektion`, `einstellung`, `aenderung_log` + +--- + +## 4. Routen (Kurzfassung) + +> 📎 Vollständige Routen-Tabelle: [[routen]]. Stubs im Code: [`app.py`](../app.py). + +Blueprint-Aufteilung (V8): +- `routes/oeffentlich.py` — öffentliche Seiten +- `routes/profil.py` — Login, Cockpit, eigene Projekte/Aufträge/Slots +- `routes/admin.py` — Dashboard, Review-Queue, Inline-Edit +- `routes/api.py` — JSON-Endpunkte (Drucker-Status, Slot-Polling) +- `services/bambu.py` — Cloud-API-Wrapper (isoliert) +- `services/datei.py` — File-BLOB-Handling (abstrahiert für spätere S3-Migration) + +--- + +## 5. Drucker-API-Anbindung + +> 📎 Detail: [[druckerapi]]. Entscheidungen: [[../decisions#E-005]], [[../decisions#E-006]]. + +- Bambu Cloud-API-Polling vom Hetzner aus, alle ~30 s (Lib: `bambulabs_api`). +- Inoffiziell, fragil → Fallback: manueller Status-Override durch Lehrer. +- Lesen: Status, Job, Restzeit. **Kein** Job-Upload von der Site (Developer-Mode-Pflicht wäre Bambu-Cloud-Verlust). + +--- + +## 6. Slot-System + +> 📎 Detail mit Sequenz-Diagrammen + UI-Skizze: [[slotsystem]]. + +- **Windhund-Prinzip** als Default, Lehrer kann priorisieren oder verschieben. +- **Slot-Buchung erst freigeschaltet nach L1 (Sicherheits-Lektion)** — V2, [[../decisions#E-020]]. +- Drucker dürfen über Nacht / Freistunden laufen. +- Drucker-Ausfall → Admin-Vorschlag „Slot auf anderen Drucker verschieben". + +--- + +## 7. Stunden-Beginn-Abfrage (V3 — radikal kurz) + +UI im Cockpit beim ersten Aufruf eines Besuchstages: + +``` +┌─ Hallo Mia! Was machst du heute? ──────────┐ +│ │ +│ ◉ Ich arbeite an meinem Projekt weiter: │ +│ „Schlüsselanhänger mit Logo" │ +│ │ +│ ○ Neues Projekt anfangen │ +│ ○ Onboarding-Lektion machen │ +│ ○ Drucken (warte auf Drucker) │ +│ ○ Anderen helfen │ +│ ○ Sonstiges │ +│ │ +│ [Optional] kurz, was genau: │ +│ ┌──────────────────────────────────────┐ │ +│ │ │ │ +│ └──────────────────────────────────────┘ │ +│ │ +│ [Los geht's] │ +└──────────────────────────────────────────────┘ +``` + +**Verhaltens-Details (V3 + E-021):** +- Default-Auswahl ist „Ich arbeite weiter" mit Titel des aktiven Projekts — **ein Klick reicht**. +- Freitext **ist immer optional**, nicht Pflicht. +- Nur einmal pro Besuchstag (UNIQUE-Constraint auf `check_in`). +- Lehrer-Dashboard zeigt alle Antworten des Tages aggregiert (siehe [[lehrer-dashboard]]). + +--- + +## 8. Software-Sektion + +| Software | Wofür | Lektion | +|---|---|---| +| **Bambu Studio** | Slicer (PC), Pflicht | L1 | +| **Bambu Handy** | Live-Kamera, Druck-Steuerung | L1 | +| **Bambu Academy** | Kostenloser Drucker-Kurs mit Zertifikat | L1 | +| **Printables.com** | Modell-Plattform (sauber, kein NSFW) | L2 | +| **MakerWorld.com** | Bambu-eigene Plattform + Customizer | L2 + L3 | +| **TinkerCAD** | Visuelles CAD im Browser, Klassenraum-Modus | L4 | +| **OpenSCAD** | Parametrisches CAD (Code) | L4+ | + +**Nicht empfohlen:** Thingiverse (NSFW-Ads), Cults3D (Bezahl-Push). + +--- + +## 9. Sicherheit + Organisation + +> 📎 Detail: [[sicherheit]]. Quellen: siehe [[COCKPIT#wichtige-externe-quellen]]. + +- **UKBW-Richtlinie** als Basis für L1. +- **PLA + PLA+ als Default**, PETG + ABS nur unter Aufsicht (Admin-Toggle). +- **A1 mini (offen):** explizite Hands-Off-Regel beim Hotend. +- **Druckraum mit Dauerlüftung** → entspannte Lage. + +--- + +## 10. ZSL-Material als Seed-Inhalt + +Bei Kursstart ist die Projekt-Bibliothek leer. Damit Neuzugänge nicht ins Nichts schauen: + +- **8-10 ZSL-Projekte** aus dem CC-BY-4.0-Material des Landes Baden-Württemberg werden in unser Design **umgearbeitet** (mit Urhebernachweis). +- Markus + Herb wählen aus den 20+ verfügbaren Projekten gemischte Schwierigkeit. +- Beschreibungen werden auf unsere Bambu-Drucker + 8-10-SuS-AG angepasst (keine 1:1-PDF-Einbettung). +- Bilder + STLs werden mit Quellen-Hinweis übernommen. +- Diese Projekte erscheinen als reguläre Katalog-Karten mit Badge „Vorlagen-Projekt". + +Über die Jahre **wächst die Bibliothek durch SuS-Projekte selbst** — siehe [[selbstlern-loop]]. + +Details: [[../decisions#E-022]]. + +--- + +## 11. Implementierungs-Etappen (Übersicht) + +> 📎 Lebende Detail-Version mit Checkboxen: [[etappen]]. + +Etappen sind seit V8 ([[../decisions#E-020]]) **mit Blueprint-Struktur von Anfang an** gedacht: + +``` +0 ✓ Skelett anlegen (2026-05-23) +1 Basis: Login + Profile + Onboarding-Status + Inline-Edit + Persistenter Lockout +2 Cockpit (progressiv, V1) + Check-In + Paarung + Lehrer-Dashboard (V5) +3 Lektionen + Software-Sektion + Troubleshooting +4 Drucker-API-Spike (Bambu-Cloud-Auth + Felder-Validierung) +5 Drucker-Integration + Live-Status + manueller Override +6 Selbstlern-Loop: Projekt-Katalog + Lebenszyklus + Files + Review + Three.js +7 Slot-System (gesperrt bis L1, V2) + Druckaufträge +8 ZSL-Material-Import (8-10 Projekte umgearbeitet) +9 Deploy + Polish (DNS, nginx, systemd, PummelHummel) +``` + +Erwarteter Gesamtaufwand: 8-10 Coding-Sitzungen. + +--- + +## 12. Offene Punkte (lebende Liste) + +> 📎 Aktuelle Liste: [[offene-fragen]]. Geklärte Punkte stehen in [[decisions]]. + +**Vor Implementation noch zu klären** (mit Markus oder im Doing): +- Welche 8-10 ZSL-Projekte konkret? (mit Markus) +- Material-Tracking als späteres Modul nötig? (siehe [[offene-fragen]]) +- Bambu-Cloud-Account-Inhaber (Schul-Account vs. Herb privat) +- A1-mini-Firmware-Version (für API-Kompatibilität) + +--- + +## 13. Quellen + +- **PHBern IdeenSet 3D-Drucken**: https://www.phbern.ch/dienstleistungen/unterrichtsmedien/ideenset-3d-drucken +- **inf-schule.de 3D-Druck**: https://inf-schule.de/informatiksysteme/3d_druck +- **ZSL Baden-Württemberg „3D-Druck erleben"**: CC-BY-4.0, im OneDrive-Bücher-Ordner +- **UKBW Sicherheit (PDF)**: https://www.ukbw.de/fileadmin/user_upload/Sicherer-Umgang-mit-3D-Druckern-in-der-Schule.pdf +- **Bambu Lab Academy**: https://bambulab.com/en/support/academy +- **Printables Education**: https://www.printables.com/education +- **bambulabs_api (Python)**: https://github.com/BambuTools/bambulabs_api + +--- + +## 14. Was als Nächstes passieren muss + +1. **Markus liest dieses Konzept** + die Detail-Module ([[selbstlern-loop]], [[lehrer-dashboard]], [[datenmodell]]). +2. Markus + Herb wählen die 8-10 ZSL-Projekte aus (siehe §10). +3. **DNS**: `3d.lehrstun.de` als A-Record auf 46.225.114.15 anlegen. +4. **Etappe 1 starten**: Blueprint-Struktur + Login + Profile + Onboarding-Status (siehe [[etappen]]). diff --git a/docs/lehrer-dashboard.md b/docs/lehrer-dashboard.md new file mode 100644 index 0000000..7304e19 --- /dev/null +++ b/docs/lehrer-dashboard.md @@ -0,0 +1,147 @@ +--- +type: konzept-modul +projekt: 3d.lehrstun.de +stand: 2026-05-24 +tags: [konzept, admin, dashboard] +--- + +# Lehrer-Dashboard mit Drill-Down + +> **V5 aus der Reality-Check-Analyse** ([[../decisions#E-020]]). Schließt eine +> bekannte Lücke des Arduino-Kurses, wo Herb manuell in jedes Gruppen-Cockpit +> schauen muss, um zu sehen, wer wo steht. + +Route: `/admin/dashboard`. Berechtigung: `@admin_required`. Vom Cockpit nicht zu verwechseln — das ist der **Lehrer-Arbeitsplatz**. + +## Layout-Skizze + +``` +┌─ Admin-Dashboard ──────────────────────────────────────────────┐ +│ │ +│ ┌─ Wer ist heute hier (basiert auf check_in von HEUTE) ───┐ │ +│ │ 🦊 Mia "Schlüsselanhänger weitermachen" → [→] │ │ +│ │ 🦦 Tom "neues Projekt: Wasserrakete" → [→] │ │ +│ │ 🐧 Lara "L2 fertig machen" → [→] │ │ +│ │ 3 SuS heute eingeloggt │ │ +│ └──────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─ Wartet auf Lehrer-Reaktion ─────────────────── [3] ─────┐ │ +│ │ 🟡 Mia — "Schlüsselanhänger" zur Freigabe (2 Tage) │ │ +│ │ 🟡 Tom — "Drohnenrahmen" zur Freigabe (heute) │ │ +│ │ 🔵 Lara — Rückfrage offen seit 5 Tagen │ │ +│ └──────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─ Drucker-Status (live) ─────────────────────────────────┐ │ +│ │ 🟢 P1S #1 drucken "Schlüsselanhänger Mia" 43% │ │ +│ │ 🟢 P1S #2 idle │ │ +│ │ 🟡 X1C drucken "Drohnenrahmen Tom" 89% │ │ +│ │ 🔴 A1 mini offline (Status veraltet >5min) │ │ +│ └──────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─ Wer hängt fest (>2 Wochen kein Fortschritt) ───────────┐ │ +│ │ 🐝 Anna — letzter Check-In vor 3 Wochen │ │ +│ │ → [Erinnerungs-Notiz im Cockpit hinterlassen] │ │ +│ └──────────────────────────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +## Sektion 1: Wer ist heute hier + +**Datenquelle:** `check_in` mit `besuch_datum = TODAY`. + +**Inhalt pro Zeile:** +- Tier-Icon + Vorname (Klick → Profil-Detail) +- Modus aus Check-In (z.B. „weiter wie bisher", „neues Projekt", „onboarding", „helfen") +- Optionaler Freitext aus Check-In (falls vorhanden, in kursiv) +- Link zum aktuellen Projekt (falls vorhanden) +- Quick-Action: „Notiz hinterlassen" → schreibt Lehrer-Notiz ins SuS-Cockpit + +**Zweck:** In den ersten 5 Minuten der AG-Stunde weiß der Lehrer ohne Rumfragen, was läuft. + +## Sektion 2: Wartet auf Lehrer-Reaktion + +**Datenquelle:** Zwei Untergruppen: +1. **Projekte mit `freigabe_status = 'wartet_auf_freigabe'`** — sortiert nach Wartezeit, älteste oben. +2. **Rückfragen** mit `status = 'offen'`, die schon länger als 3 Tage warten — der SuS hat möglicherweise vergessen, nachzubessern. + +**Badge im Header:** Anzahl der zu reviewen Projekte. Sichtbar auch in der Navigation (klein, rote Zahl neben „Admin"). + +**Quick-Action:** Klick auf Eintrag → `/admin/projekt-freigabe/` öffnet Detail-Review mit Three.js-Vorschau, allen Files, Edit-Möglichkeit, Freigeben-/Rückfrage-/Löschen-Buttons. + +**Zweck:** Review-Last sichtbar machen, damit nichts liegen bleibt. + +## Sektion 3: Drucker-Status (live) + +**Datenquelle:** `drucker_status` (vom Bambu-Cloud-Polling befüllt — siehe [[druckerapi]]). + +**Inhalt pro Drucker:** +- Status-Ampel (🟢 druckt/idle, 🟡 pausiert/Wartung, 🔴 offline/failed) +- Drucker-Name + Modell +- Aktueller Job-Name (wenn drucken) +- Fortschritt + Restzeit +- Quick-Action bei roter Ampel: „Manueller Status setzen" (Override) + +**Zweck:** Der Lehrer sieht beim Reinkommen in den Druckraum sofort, welche Drucker frei sind, welche fertig zur Abholung sind, welche Probleme haben. + +## Sektion 4: Wer hängt fest + +**Datenquelle:** SuS, die in den letzten 2 Wochen weder einen `check_in` gemacht haben noch ein Projekt abgeschlossen haben. + +**Zweck:** Stille Aussteiger erkennen. Wer 3 Wochen nicht da war, ist möglicherweise nicht mehr dabei — oder braucht eine kurze Nachricht. + +**Quick-Action:** „Erinnerungs-Notiz im Cockpit hinterlassen" — beim nächsten Login sieht der SuS ein grünes Banner („Hey, lange nicht gesehen — alles okay?"). + +## Performance-Hinweise + +Das Dashboard macht 4 Queries: +- `SELECT` aus `check_in WHERE besuch_datum = TODAY` + JOIN `profil` + LEFT JOIN aktives `projekt` +- `SELECT` aus `projekt WHERE freigabe_status IN ('wartet_auf_freigabe')` + JOIN `profil` +- `SELECT` aus `drucker_status` (4 Zeilen) +- Subquery für „hängt fest"-Liste + +Alle Indexes setzen: `idx_projekt_freigabe`, FK-Index auf `check_in.besuch_datum`. Bei 8-10 SuS und ~50 Projekten/Jahr ist Performance kein Thema. + +## Quick-Action-Mechanik: „Notiz im Cockpit hinterlassen" + +Tabelle `lehrer_notiz` (neu): + +```sql +CREATE TABLE lehrer_notiz ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + profil_id INTEGER NOT NULL, -- Adressat + von_profil_id INTEGER, -- Lehrer (NULL = System) + text TEXT NOT NULL, + erstellt_am DATETIME DEFAULT CURRENT_TIMESTAMP, + gelesen_am DATETIME, -- NULL = ungelesen + typ TEXT DEFAULT 'info', -- 'info'|'rueckfrage'|'erinnerung' + FOREIGN KEY (profil_id) REFERENCES profil(id) ON DELETE CASCADE +); +``` + +Wird im Cockpit als gelbes Banner gerendert (analog zur Rückfrage-Mechanik V6). SuS kann jede Notiz mit „verstanden" wegklicken → `gelesen_am` wird gesetzt. + +## Was das Dashboard NICHT zeigt + +- Keine Aggregate über die ganze AG-Historie (das wäre Statistik, gehört in `/admin/statistik` als optionaler späterer Bereich). +- Keine Daten anderer Lehrkräfte (es gibt nur Herb + Markus, gleiche Sicht). +- Kein Eingriff in laufende Druckjobs — das macht weiterhin die Bambu Handy App. + +## Inline-Edit-Berechtigungen für Lehrer + +Aus [[routen#inline-edit-whitelist]]: +- Lektionen: titel, aufgabe_md, tipp_md, extern_link — **immer**. +- Software-Seiten: titel, inhalt_md, url, quickstart_md — **immer**. +- Projekte: titel, beschreibung_md, tags — **nur während Review** (Status `wartet_auf_freigabe` oder `rueckfrage`). +- Einstellungen: wert — **immer**. + +Jede Inline-Edit-Aktion landet in `aenderung_log` mit Lehrer-Profil-ID. So bleibt nachvollziehbar, wer was wann geändert hat. + +## Querverweise + +- Routen: [[routen#admin-routesadminpy--herb--markus]] +- Selbstlern-Loop (das Dashboard zeigt dessen Status): [[selbstlern-loop]] +- Drucker-Anbindung: [[druckerapi]] +- Datenmodell: [[datenmodell]] +- Entscheidung V5: [[../decisions#E-020]] +- Rückfrage-Mechanik V6: [[../decisions#E-020]] diff --git a/docs/lektion-L1-drucker-kennenlernen.md b/docs/lektion-L1-drucker-kennenlernen.md new file mode 100644 index 0000000..8a540da --- /dev/null +++ b/docs/lektion-L1-drucker-kennenlernen.md @@ -0,0 +1,56 @@ +--- +type: lektion +nummer: 1 +pflicht: true +titel: "Drucker-Kennenlernen + Sicherheit" +stand: stub +tags: [lektion, pflicht, sicherheit] +--- + +# L1 — Drucker-Kennenlernen + Sicherheit + +> **Status: Stub.** Inhalte werden mit Markus erarbeitet. Struktur steht. +> Übergeordnetes Konzept: [[../kurskonzept#vier-stufen-progression]]. + +## Lernziel + +Die SuS können: +- die 4 Drucker im Raum unterscheiden ([[architektur|wer welcher Drucker]]). +- die [[sicherheit|Sicherheits-Regeln]] benennen und einhalten. +- ein vorbereitetes Modell in [[software-bambu-studio|Bambu Studio]] öffnen und auf einen Drucker schicken. +- die [[software-bambu-handy|Bambu Handy App]] einrichten und ihren Druck verfolgen. + +## Aufbau der Stunde + +1. **Empfang im Druckraum** (5 Min) — Drucker zeigen, Druckraum-Regeln. +2. **Sicherheits-Lektion** (15 Min) — A1 mini-Hands-Off, PLA-only-Default, was bei Fehlern. Quelle: [[sicherheit]]. +3. **Bambu Handy App einrichten** (10 Min) — App-Store, Account-Daten von Herb, Drucker hinzufügen. +4. **Vorbereitetes Modell drucken** (Rest der Stunde + läuft drüber hinaus) — bewährtes Test-Modell (Vorschlag: kleines Benchy oder Schlüsselanhänger-Rohling). SuS startet den Druck selbst, beobachtet die erste Schicht. +5. **Abholung in nächster Stunde**. + +## Was die Plattform dazu liefert + +- Diese Lektion als Seite mit Bambu Academy-Link, eingebettetem Sicherheits-Video, Quiz? +- Buchung des Test-Drucks als ersten [[slotsystem|Slot]] (mit Herb-Anleitung). +- Erst-Druckauftrag als ersten Eintrag in der eigenen [[datenmodell#druckauftrag|Auftrags-Liste]]. + +## TODO mit Markus + +- [ ] Welches **Test-Modell** als "garantierter Erfolg"? + - Optionen: 3DBenchy (Standard), kleiner Schlüsselanhänger-Rohling, Pinguin von Printables +- [ ] **Sicherheits-Video von Bambu** verlinken oder eigenes drehen? +- [ ] **Quiz am Ende** (3-5 Fragen zur Sicherheit)? +- [ ] Bambu Academy-Kurs als optionale Hausaufgabe? + +## Material-Bedarf + +- 1 Rolle PLA pro 2-3 Test-Drucke +- SuS-Smartphones mit Internet (für Bambu Handy) +- Vorbereitetes 3MF-File von Herb für jedes Druckermodell + +## Querverweise + +- Nächste Lektion: [[lektion-L2-modell-drucken]] +- Software dieser Lektion: [[software-bambu-studio]], [[software-bambu-handy]] +- Grundlagen: [[sicherheit]], [[glossar]] +- Drucker-Übersicht: [[architektur]] diff --git a/docs/lektion-L2-modell-drucken.md b/docs/lektion-L2-modell-drucken.md new file mode 100644 index 0000000..fb8b244 --- /dev/null +++ b/docs/lektion-L2-modell-drucken.md @@ -0,0 +1,52 @@ +--- +type: lektion +nummer: 2 +pflicht: true +titel: "Fremdes Modell wählen + drucken" +stand: stub +tags: [lektion, pflicht] +--- + +# L2 — Fremdes Modell wählen + drucken + +> **Status: Stub.** Voraussetzung: [[lektion-L1-drucker-kennenlernen]]. + +## Lernziel + +Die SuS können: +- ein passendes Modell auf [[software-printables|Printables]] oder [[software-makerworld|MakerWorld]] finden. +- die **Lizenz** des Modells prüfen (CC-BY, CC-BY-NC, …) und wissen, was sie damit dürfen. +- ein STL/3MF in [[software-bambu-studio|Bambu Studio]] importieren, ausrichten, slicen. +- die geschätzte Druckzeit und den Materialverbrauch lesen. +- den Druck planen und auf der Plattform als [[datenmodell#druckauftrag|Druckauftrag]] eintragen. + +## Aufbau der Stunde + +1. **Plattform-Rundgang** (10 Min) — [[software-printables|Printables]] zeigen, Suche, Filter, Lizenz-Hinweise. +2. **Eigene Wahl** (10 Min) — SuS sucht sich ein Modell. Tipp: erstmal klein (unter 1h Druckzeit). +3. **Slicen + Planen** (15 Min) — Modell in Bambu Studio öffnen, ausrichten (Druckbett-Orientierung!), slicen, Zeit + Material notieren. +4. **Eintragen + Buchen** (10 Min) — Druckauftrag auf der Plattform anlegen, [[slotsystem|Slot]] reservieren. +5. **Drucken** (asynchron, während der Stunde oder über Nacht). + +## Was die Plattform dazu liefert + +- Liste empfohlener Such-Stichwörter für [[software-printables|Printables]]. +- Quick-Reference für **Lizenz-Symbole** (CC-Familie kurz erklärt). +- Vorlage für den ersten eigenen [[datenmodell#druckauftrag|Druckauftrag]]. + +## TODO mit Markus + +- [ ] **Empfohlene Modell-Liste** auf Printables zusammenstellen? Oder freie Wahl? +- [ ] **Lizenz-Schulung**: wie tief erklären? Wir drucken nur, geben nichts weiter — Lizenz-Sorge ist gering. +- [ ] **Druckzeit-Grenze**: max. 2 h für L2? Oder offen? + +## Material-Bedarf + +- Wie L1, je nach SuS-Wahl 1 Rolle PLA pro 4-6 Drucke + +## Querverweise + +- Vorher: [[lektion-L1-drucker-kennenlernen]] +- Nachher: [[lektion-L3-customizer]] +- Software: [[software-bambu-studio]], [[software-printables]], [[software-makerworld]] +- DB: [[datenmodell#druckauftrag]] diff --git a/docs/lektion-L3-customizer.md b/docs/lektion-L3-customizer.md new file mode 100644 index 0000000..19f896f --- /dev/null +++ b/docs/lektion-L3-customizer.md @@ -0,0 +1,53 @@ +--- +type: lektion +nummer: 3 +pflicht: true +titel: "Customizer / parametrisches Modell" +stand: stub +tags: [lektion, pflicht] +--- + +# L3 — Customizer / parametrisches Modell + +> **Status: Stub.** Voraussetzung: [[lektion-L2-modell-drucken]]. +> **Pädagogische Brücke** zwischen "fremdes Modell drucken" und "selbst zeichnen". + +## Lernziel + +Die SuS können: +- den Begriff **parametrisches Modell** erklären (Maße/Texte/Optionen veränderlich). +- den [[software-makerworld#customizer|MakerWorld-Customizer]] bedienen. +- ein parametrisches Modell so anpassen, dass es ihre eigene Note bekommt (Name, Maße, Symbol). +- den Unterschied "fertig laden" vs. "selbst customizen" vs. "selbst zeichnen" erklären. + +## Aufbau der Stunde + +1. **Was ist parametrisch?** (5 Min) — Lehrer-Demo: Schlüsselanhänger-Generator, ein Mal mit Maximilian, ein Mal mit Lisa, ein Mal mit "Pizza". +2. **Eigene Wahl** (15 Min) — SuS sucht sich einen Customizer aus, vorzugsweise einen kleinen + schnellen. +3. **Anpassen + Slicen** (10 Min) — Werte verändern, Vorschau, dann STL/3MF exportieren und in [[software-bambu-studio|Bambu Studio]] slicen. +4. **Reflexion** (5 Min) — Was war einfach, was nicht? Wie wäre es, das **ganz von vorne** zu zeichnen? → leitet zu L4+ über. +5. **Drucken** (asynchron). + +## Empfohlene Customizer für L3 (TODO mit Markus prüfen) + +- [Parametric Customizable Name Keychain Creator](https://makerworld.com/en/models/1320280) — der Klassiker +- weitere folgen, sobald Markus + Herb durchgespielt haben + +## Was die Plattform dazu liefert + +- Anleitung "So funktioniert ein Customizer" mit Screenshots. +- Liste der empfohlenen Customizer. +- Verknüpfung zum nächsten Schritt: [[../kurskonzept#vier-stufen-progression|L4+ — eigenes Modell]]. + +## TODO mit Markus + +- [ ] **Customizer-Liste** kuratieren: 3-5 Stück, klein + schnell + cool. +- [ ] Wie tief auf **Lizenz** beim Customizer-Output eingehen? +- [ ] **Anschluss zu L4**: lassen wir die SuS ihre Reflexion schriftlich machen oder mündlich? + +## Querverweise + +- Vorher: [[lektion-L2-modell-drucken]] +- Anschluss (frei): TinkerCAD → [[software-tinkercad]], OpenSCAD → [[software-openscad]] +- Tool: [[software-makerworld]] +- Konzept-Begründung "warum diese Brücke?": [[../kurskonzept#warum-diese-reihenfolge]] diff --git a/docs/offene-fragen.md b/docs/offene-fragen.md new file mode 100644 index 0000000..554bd27 --- /dev/null +++ b/docs/offene-fragen.md @@ -0,0 +1,99 @@ +--- +type: konzept-modul +projekt: 3d.lehrstun.de +stand: 2026-05-24 +tags: [offen, klärungsbedarf] +--- + +# Offene Fragen + +> Wird laufend gepflegt. Wenn eine Frage entschieden ist, wandert sie nach +> [[decisions]] und wird hier gestrichen. +> +> **Geklärt am 2026-05-24:** Onboarding-Zertifizierung (selbst, E-019), +> Three.js-Dependency (ja, E-018), ZSL-Projekt-Menge (8-10, E-022), +> Slot-Granularität-Default (freie Zeitspannen, E-020 V8), Brand-Farbe +> (Teal wie Arduino für einheitliches Wirken, kein eigener Decision-Eintrag — +> ist implizit aus E-001). + +## Vor Etappe 1 zu klären + +### Bambu-Cloud-Account +- **Wer ist Account-Inhaber?** Schul-Account oder Herb privat? +- Hat der Account alle 4 Drucker in derselben Region (EU)? +- Brauchen wir 2FA-Bypass / App-Passwort? +→ siehe [[druckerapi#auth]] + +### A1 mini Genau-Bezeichnung +- Welche Firmware-Version aktuell? +- Sind alle 4 Drucker im selben WLAN? + +### Domain-Vorbereitung +- DNS-A-Record `3d.lehrstun.de → 46.225.114.15` anlegen (via [Hetzner Cloud DNS-CLI](file:///D:/Claude-Projekte/hetzner-dns-cli/)) +- Port 5057 in der CLAUDE.md-Konvention reservieren (ist bereits notiert) + +## Vor Etappe 6 (Selbstlern-Loop) zu klären + +### Material-Tracking — eigenes Modul? + +**Kontext (aus Reality-Check):** Aktuell führt Herb Material-Bestand (welche Filament-Rollen + Farben sind da, welche fast leer) im Kopf. Wenn Markus auch reviewt + ggf. nachbestellt, brauchen sie eine gemeinsame Sicht. + +**Vorschlag:** Eigene `material_rolle`-Tabelle (Hersteller, Farbe, Material-Typ, Restmenge geschätzt, Standort, Notiz). Im `druckauftrag`-Modell ein optionales `material_rolle_id`-Feld → SuS oder Lehrer kann beim Druckabschluss „X Gramm verbraucht" eintragen. Admin-Übersicht zeigt Bestand. + +→ **Optional, nicht Etappe-1-Thema.** Wenn entschieden, kommt es als Etappe 10 dazu. + +### Mentor-Badge / aktive Vernetzung + +**Kontext:** Variante 1 (SuS stöbert selbst) macht die Vernetzung passiv. Wenn Mias Projekt 3x als Vorlage gewählt wurde, könnte ein „Mentor"-Badge automatisch aufploppen — sichtbar im Profil und im Lehrer-Dashboard. + +→ **Spätere Iteration**, nicht initial. Erfordert: SuS müssen einverstanden sein (kein automatisches Outing). + +### Foto-Galerie pro Projekt (mehrere Fotos im Lifecycle) + +Aktuell sieht das Datenmodell beliebig viele `projekt_datei` mit `art='foto'` vor — aber UI-seitig wird nur das **erste** als Cover gezeigt. Sollen wir später eine richtige Galerie bauen (Anfang/Mitte/Ende-Bilder)? + +→ **Spätere Iteration**, kommt mit Bedarf. + +## Pädagogisch (mit Markus) + +### Welche 8-10 ZSL-Projekte zum Start? + +Aus den 20+ ZSL-Projekten auswählen (siehe Material-Übersicht im OneDrive). Vorschlag: 3 aus Level 1 (Einsteiger), 3-4 aus Level 2 (Mittel), 2-3 aus Level 3 (Fortgeschritten). + +**Heuristik für die Auswahl:** +- Druckzeit unter 4 Stunden auf P1S +- Funktioniert mit PLA +- Cover-Foto bereits vorhanden +- Keine SuS-Erfahrung von extern nötig + +**Spezifische Kandidaten** (Empfehlung von Claude, nicht final): +- Level 1: Schlüsselanhänger, Teigstempel, Spielfigur +- Level 2: Faschingsbrille, Stifteigel, Smartphonehalterung (Frosch) +- Level 3: Steckklammer, Handventilator + +→ **Markus + Herb müssen das gemeinsam durchsehen.** + +### Inhalte L1-L3 + +- **L1:** Welches Modell als „garantierter Erfolg"? Vorschlag: kleines Benchy ODER Schlüsselanhänger-Rohling (passt zu L3-Customizer-Anschluss). +- **L2:** Empfehlen wir eine konkrete Modell-Liste auf Printables, oder freie Wahl? +- **L3:** Welcher MakerWorld-Customizer als Erstgehversuch? Vorschlag: [Parametric Name Keychain Creator](https://makerworld.com/en/models/1320280). + +→ alle mit Markus besprechen, dann in [[lektion-L1-drucker-kennenlernen]] etc. einpflegen. + +### Bewertung / Dokumentation für Außenwirkung + +- AG ist unbenotet — aber sollen SuS-Projekte mit Bild in einer „Galerie" auf der Schul-Website oder im Jahresbericht erscheinen? Datenschutz: Eltern-OK pro Projekt nötig. +- Vorschlag: nicht initial; nach 1 Jahr AG mal mit Schulleitung quatschen, wenn etwas vorzeigbares da ist. + +## Späterer Optimierungs-Bedarf + +- **Speicher-Migration:** Wenn DB > 5 GB wird (~2-3 Jahre AG-Laufzeit), Migration der BLOB-Dateien auf Hetzner Object-Storage. Vorbereitet durch Abstraktion in `services/datei.py`. +- **Performance-Indexes:** Wenn Cockpit-Ladezeiten merklich werden, ggf. zusätzliche Indexes auf `check_in.besuch_datum` und `projekt.geaendert_am` setzen. +- **Bambu-API-Bruch-Plan:** Wenn die inoffizielle API länger als 1 Woche bricht, Variante (b) aus der API-Recherche aktivieren (Pi-Bridge in der Schule). + +## Querverweise + +- Entschiedene Punkte: [[decisions]] +- Konzept-Hauptdokument: [[../kurskonzept]] +- COCKPIT: [[../COCKPIT]] diff --git a/docs/routen.md b/docs/routen.md new file mode 100644 index 0000000..bc55e14 --- /dev/null +++ b/docs/routen.md @@ -0,0 +1,126 @@ +--- +type: konzept-modul +projekt: 3d.lehrstun.de +stand: 2026-05-24 +tags: [konzept, routen, api] +--- + +# Routen + +> Vollständige Routen-Liste. Live im Code: `app.py` + `routes/*.py` (V8: Blueprint-Struktur, siehe [[../decisions#E-020]]). +> Welche Etappe was baut: [[etappen]]. Selbstlern-Loop-Details: [[selbstlern-loop]]. Lehrer-Dashboard: [[lehrer-dashboard]]. + +## Blueprint-Aufteilung (V8) + +| Blueprint-Datei | URL-Präfix | Inhalt | +|---|---|---| +| `routes/oeffentlich.py` | `/` (kein Präfix) | Startseite, Lektionen, Software, Projekt-Katalog, Drucker, Troubleshooting | +| `routes/profil.py` | `/cockpit`, `/login` | Login, Cockpit, Projekt-Workflow, Slots, Druckaufträge | +| `routes/admin.py` | `/admin` | Dashboard, Profile, Lektionen, Projekt-Freigabe, Inline-Edit | +| `routes/api.py` | `/api` | JSON-Endpunkte für Frontend-Polling | +| `services/bambu.py` | (kein HTTP) | Bambu-Cloud-API-Wrapper, isoliert für späteren Austausch | +| `services/datei.py` | (kein HTTP) | File-BLOB-Handling, abstrahiert für spätere Object-Storage-Migration | + +## Öffentlich (`routes/oeffentlich.py`) + +| Methode | Pfad | Zweck | Etappe | +|---|---|---|---| +| GET | `/` | Startseite | ✓ skelett | +| GET | `/lektionen` | Onboarding-Pfad + freie Module | 3 | +| GET | `/lektion/` | Einzelne Lektion | 3 | +| GET | `/software` | Software-Tools | 3 | +| GET | `/software/` | Software-Detail | 3 | +| GET | `/projekte` | **Projekt-Katalog** (mit Filter: tag, level, erfolg, macher) | 6 | +| GET | `/projekte/` | Projekt-Detail (Cover, Beschreibung, Files, Bewertung, Macher) | 6 | +| GET | `/projekte/datei/` | Datei-Download (mit Auth-Check, sichtbar_in_ag prüfen) | 6 | +| GET | `/troubleshooting` | Entscheidungsbaum + Suchliste | 3 | +| GET | `/drucker` | 4 Drucker live | 5 | + +## Profil (`routes/profil.py` — eingeloggt) + +| Methode | Pfad | Zweck | Etappe | +|---|---|---|---| +| GET | `/login` | Profil-Kacheln (Vorname + Tier) | 1 | +| POST | `/login/` | PIN-Eingabe (Rate-Limit + persistenter Lockout) | 1 | +| GET | `/logout` | Abmelden | 1 | +| GET | `/cockpit` | **Progressiv** (V1): zeigt nur was zum Status passt | 2 | +| POST | `/cockpit/check-in` | Stunden-Beginn-Abfrage (V3: 1 Radio, 1 optionaler Freitext, "weiter wie bisher" als Default) | 2 | +| POST | `/cockpit/lektion//status` | Onboarding-Lektion als fertig/läuft markieren (V2-relevant) | 3 | +| GET | `/cockpit/projekt/neu` | Projekt anlegen (mit Vorlage-Auswahl aus Katalog) | 6 | +| POST | `/cockpit/projekt/neu` | Speichern | 6 | +| POST | `/cockpit/projekt//edit` | Projekt-Details anpassen (während `in_arbeit`) | 6 | +| GET | `/cockpit/projekt//abschluss` | **Abschluss-Checkliste** (V4: 3 Pflicht-Felder) | 6 | +| POST | `/cockpit/projekt//abschluss` | Abschluss absenden → Status `wartet_auf_freigabe` | 6 | +| POST | `/cockpit/projekt//datei` | Datei hochladen (30 MB Limit) | 6 | +| POST | `/cockpit/projekt//datei//loeschen` | Eigene Datei entfernen | 6 | +| POST | `/cockpit/projekt//rueckfrage//erledigt` | Rückfrage als erledigt markieren (V6) | 6 | +| GET | `/cockpit/druckauftraege` | Eigene Auftragsliste | 7 | +| POST | `/cockpit/druckauftrag/neu` | Auftrag eintragen | 7 | +| GET | `/cockpit/slots` | Slot-Kalender (eigene + freie) — **gesperrt bis L1 fertig** (V2) | 6 | +| POST | `/cockpit/slot/reservieren` | Neuen Slot buchen — gesperrt bis L1 fertig | 6 | +| POST | `/cockpit/slot//stornieren` | Slot zurückgeben | 6 | +| POST | `/cockpit/paarung/anfragen/` | Paar-Anfrage | 2 | +| POST | `/cockpit/paarung/aufloesen` | Paar trennen | 2 | + +## Admin (`routes/admin.py` — Herb + Markus) + +| Methode | Pfad | Zweck | Etappe | +|---|---|---|---| +| GET | `/admin/login` | Passwort-Eingabe | 1 | +| GET | `/admin/dashboard` | **Drill-Down** (V5): „Wer ist heute hier / Wartet auf Review / Wer hängt fest" | 2 | +| GET | `/admin/profile` | Liste + Edit | 1 | +| POST | `/admin/profil/neu` | Neues SuS-Profil + PIN-Generation | 1 | +| GET | `/admin/profile/karten` | Druckbogen PIN-Karten (CSS-Print) | 1 | +| GET | `/admin/projekt-freigabe` | **Review-Queue** mit Badge im Dashboard | 6 | +| GET | `/admin/projekt-freigabe/` | Detail-Review mit Three.js-Vorschau + Edit-Möglichkeit | 6 | +| POST | `/admin/projekt//freigeben` | Projekt freigeben (mit optionaler Eigenbearbeitung) | 6 | +| POST | `/admin/projekt//rueckfrage` | Rückfrage stellen → Status `rueckfrage` + Notiz im Cockpit | 6 | +| POST | `/admin/projekt//loeschen` | **Hartes Löschen** mit Bestätigung (Audit-Log behält Datum + Lehrer + SuS-Name) | 6 | +| POST | `/admin/projekt//datei-aus-3d` | Three.js-Screenshot als Cover speichern (von Detail-Review aus) | 6 | +| GET | `/admin/druckauftraege` | Queue + Freigabe-UI | 7 | +| POST | `/admin/druckauftrag//freigeben` | Lange Drucke explizit freigeben | 7 | +| POST | `/admin/slot//prioritaet` | Slot-Priorität setzen | 6 | +| POST | `/admin/slot//verschieben` | Slot bei Drucker-Ausfall auf anderen Drucker | 6 | +| GET | `/admin/drucker` | Drucker-Stammdaten + manueller Status | 5 | +| POST | `/admin/drucker//manueller_status` | Override bei API-Ausfall | 5 | +| GET/POST | `/admin/lektionen` | Lektionen-CRUD | 3 | +| GET | `/admin/einstellungen` | Konfig-Werte (auch Tag-Vorschläge) | 1 | +| POST | `/admin/inline-edit` | Universal-Endpoint für Inline-Edit (Whitelist!) | 1 | + +## API (`routes/api.py` — JSON für Frontend-Polling) + +| Methode | Pfad | Liefert | Frequenz | +|---|---|---|---| +| GET | `/api/drucker/status` | JSON aller 4 Drucker mit Status/Restzeit | alle 30 s im Frontend | +| GET | `/api/slots/heute` | JSON aller aktiven/anstehenden Slots heute | alle 60 s | +| GET | `/api/projekte/suche?q=...&tag=...&erfolg=...` | Projekt-Filter-Ergebnis als JSON (für Live-Suche im Katalog) | on-demand | +| GET | `/api/admin/dashboard-zahlen` | Badge-Zahlen für Admin-Dashboard | beim Page-Load | + +## Inline-Edit-Whitelist (`ERLAUBTE_INLINE_TABELLEN`) + +Pattern aus Arduino-Kurs (`app.py:~1530-1550`). Pro Tabelle und Feld definiert, was editiert werden darf. + +```python +ERLAUBTE_INLINE_TABELLEN = { + "lektion": {"titel", "aufgabe_md", "tipp_md", "extern_link"}, + "projekt": {"titel", "beschreibung_md", "tags"}, # bei Lehrer-Review zusätzlich + "einstellung": {"wert"}, + "software_seite": {"titel", "inhalt_md", "url", "quickstart_md"}, + "drucker": {"name", "notiz"}, +} + +# Zeitliche Begrenzung beim Projekt: Lehrer darf alle Felder editieren, +# aber nur wenn projekt.freigabe_status IN ('wartet_auf_freigabe', 'rueckfrage') +``` + +Jede Änderung loggt in `aenderung_log` (Tabelle, Datensatz-ID, Feld, alter Wert, neuer Wert, Lehrer-Profil-ID). + +## Sicherheit + +- Admin-Routen via `@admin_required`-Decorator (Passwort aus `.env`). +- Profil-Routen via `@login_required` (Session-basiert). +- Slot-Buchung zusätzlich via `@onboarding_l1_fertig` (V2). +- Inline-Edit-POST mit CSRF-Token. +- Login: persistenter Lockout über `login_lockout`-Tabelle (V7). Fenster: 5 Versuche / 5 Min, danach 15 Min Sperre. +- Datei-Download nur wenn `sichtbar_in_ag=1` und SuS eingeloggt; Lehrer sieht immer alles. +- Hartes Löschen (V3-Aktion) mit Audit-Log (Datum, Lehrer, SuS, Grund optional) — Dateien sind danach weg. diff --git a/docs/selbstlern-loop.md b/docs/selbstlern-loop.md new file mode 100644 index 0000000..cedc071 --- /dev/null +++ b/docs/selbstlern-loop.md @@ -0,0 +1,176 @@ +--- +type: konzept-modul +projekt: 3d.lehrstun.de +stand: 2026-05-24 +tags: [konzept, selbstlern-loop, projekte, kern] +--- + +# Selbstlern-Loop + +> **Der Kern der 3D-Druck-AG-Site.** Was uns vom Arduino-Kurs unterscheidet: +> Die Site **wächst durch die SuS selbst**. Jedes abgeschlossene Projekt landet +> nach Lehrer-Freigabe in der internen Bibliothek. Neue SuS sehen: "Bei uns +> hat das schonmal jemand gemacht — und so ging's aus." Das schlägt jeden +> externen Modell-Katalog. + +Architektur-Einbettung: [[architektur]]. Datenmodell: [[datenmodell#projekt]], +[[datenmodell#projekt_datei]], [[datenmodell#projekt_tag]]. Lehrer-Sicht: [[lehrer-dashboard]]. + +## Der Lebenszyklus eines Projekts + +```mermaid +stateDiagram-v2 + [*] --> Stoebern + Stoebern --> Ausgewaehlt: SuS wählt aus Katalog (oder eigene Idee) + Ausgewaehlt --> InArbeit: Projekt angelegt im Cockpit + InArbeit --> InArbeit: Dateien hochladen, slicen, drucken, iterieren + InArbeit --> AbschlussCheckliste: Knopf "Projekt beenden" + AbschlussCheckliste --> WartetAufFreigabe: Pflicht-Checkliste erfüllt + AbschlussCheckliste --> InArbeit: Zurück (Checkliste unvollständig) + + WartetAufFreigabe --> Freigegeben: Lehrer-OK + WartetAufFreigabe --> Rueckfrage: Lehrer hat Hinweis + Rueckfrage --> InArbeit: SuS bessert nach + WartetAufFreigabe --> [*]: Lehrer hart-löscht (Penismodell etc.) + + Freigegeben --> [*]: Projekt im Katalog sichtbar + Freigegeben --> Stoebern: Andere SuS wählt es als Vorlage +``` + +## Station 1: Stöbern + +**UI:** `/projekte` — Projekt-Katalog. + +**Inhalt:** +- Karten mit Cover-Foto, Titel, Macher (Vorname + Tier-Icon), Selbsteinschätzung (🟢🟡🔴), Tags. +- Filter: nach Tag, Schwierigkeitslevel (Anfänger/Mittel/Fortgeschritten), Erfolgsstatus, Macher. +- Auch ZSL-Projekte (umgearbeitet, mit CC-BY-4.0-Nachweis — siehe [[../decisions#E-022]]) erscheinen als Karten. +- Suchfeld für Titel-Volltext. + +**Ankerseite für die Vernetzung (Variante 1):** SuS sucht selbst, kein Auto-Match-Knopf. Wenn Mia das gleiche Projekt schon gemacht hat, sieht der neue SuS die Karte und kann sie in der AG-Stunde ansprechen. + +## Station 2: Auswählen + +**UI:** `/cockpit/projekt/neu`. + +Drei Optionen: +1. **Aus Katalog**: SuS klickt auf eine Karte → „Sowas mache ich auch" → neues Projekt-Datensatz mit `vorlage_projekt_id` befüllt. Titel + Tags vorausgefüllt, SuS kann anpassen. +2. **Komplett eigene Idee**: leerer Titel + Beschreibung. +3. **Onboarding-Pflicht-Projekt** (z.B. L2 „erstes Modell drucken"): die Lektion legt automatisch ein Projekt an. + +## Station 3: Arbeiten + +**UI:** Cockpit-Karte „Eigene Projekte" mit Liste der eigenen `status_lebenszyklus='in_arbeit'`-Projekte. + +**Was passiert hier:** +- Datei-Uploads (laufend oder am Ende, frei): STL, 3MF, Fotos, Sonstiges. Limit 30 MB pro Datei. +- Beschreibung pflegen (Markdown). +- Druckaufträge anlegen (`/cockpit/druckauftrag/neu`), Slots reservieren. +- Mit anderen SuS in der AG sprechen — die Site lenkt nicht aktiv. + +## Station 4: Abschluss-Checkliste + +**UI:** `/cockpit/projekt//abschluss`. + +**Pflicht-Felder (V4, radikal kurz):** +- **Selbsteinschätzung** (Pflicht, eine Wahl): + - 🟢 Bin zufrieden, würde es so wieder machen + - 🟡 Funktioniert, aber ich würde Sachen anders machen + - 🔴 Hat nicht geklappt, aber ich habe was gelernt +- **Cover-Foto ODER STL/3MF** (mindestens eines): ein Bild, das im Katalog erscheint. Wenn nur STL vorhanden: Lehrer kann beim Review per Three.js-Vorschau einen Screenshot machen. +- **Mindestens 1 Tag** (frei wählbar oder aus kuratierter Liste). + +**Optional, aber empfohlen:** +- Beschreibungs-Freitext (Markdown). +- Freitext „Was würdest du anderen mit auf den Weg geben?" — wird im Detail bei der Karte angezeigt, ist die wichtigste Lehrgeld-Quelle für nächste SuS. +- Weitere Dateien. + +**Knopf „Abschicken":** ist erst aktivierbar, wenn alle Pflicht-Felder erfüllt sind. + +**Nach Abschicken:** `freigabe_status = 'wartet_auf_freigabe'`, im Cockpit erscheint die Karte mit Hinweis „Wartet auf Lehrer-Freigabe (typisch 1-3 Tage)". + +## Station 5: Lehrer-Review + +**UI:** `/admin/projekt-freigabe` (Liste) → `/admin/projekt-freigabe/` (Detail). + +**Drei Aktionen (siehe [[../decisions#E-017]]):** + +### A) Freigeben +- Lehrer prüft Cover-Foto, Beschreibung, Tags, Dateien. +- Darf Inline-editieren: Titel, Beschreibung, Tags (siehe [[routen#inline-edit-whitelist]]). Audit-Log dokumentiert jede Änderung. +- Optional: Three.js-Screenshot als Cover speichern, wenn SuS keines hochgeladen hat. +- Klick „Freigeben" → `freigabe_status = 'freigegeben'`, sofort im Katalog sichtbar. + +### B) Rückfrage stellen +- Lehrer tippt Notiz (z.B. „Cover-Foto fehlt, mach bitte eines vor dem Drucker"). +- Klick „Rückfrage" → `freigabe_status = 'rueckfrage'`, Eintrag in `projekt_rueckfrage`. +- SuS sieht beim nächsten Login **gelbes Banner ganz oben im Cockpit** (V6): „Markus hat eine Rückfrage zu deinem Projekt X: …". +- SuS bessert nach, klickt im Cockpit „Rückfrage erledigt + erneut zur Freigabe" → zurück auf `wartet_auf_freigabe`. + +### C) Hart löschen +- Für Penismodelle, Copyright-Probleme, sonstige problematische Inhalte. +- Bestätigungsdialog: „Wirklich löschen? Dateien sind danach weg." +- Audit-Log behält: Datum, Lehrer-Profil-ID, SuS-Vorname + Tier, optional Grund. Dateien selbst werden physisch entfernt. + +## Station 6: Im Katalog + +**UI:** `/projekte` zeigt das Projekt für alle eingeloggten SuS. + +**Detail-Seite `/projekte/`:** +- Cover-Foto groß +- Titel + Macher (Vorname + Tier-Icon) +- Selbsteinschätzung als Badge (🟢🟡🔴) +- Tags als Chips (klickbar → Filter) +- Beschreibung +- „Was würdest du anderen mitgeben?" — als hervorgehobener Block +- Dateien-Liste mit Download-Buttons (Auth-Check: nur eingeloggte SuS) +- Three.js-Vorschau, wenn STL vorhanden +- **Knopf „Sowas mache ich auch"** → legt neues Projekt mit `vorlage_projekt_id` an + +**Wichtig:** SuS kann das eigene Projekt auch nachträglich verbergen (`sichtbar_in_ag = 0`) — z.B. wenn ihm das später peinlich ist. Lehrer sieht es weiterhin im Admin. + +## Vernetzung in der Praxis (Variante 1) + +Die Site **vermittelt nicht aktiv**. Aber sie macht sichtbar, wer was schon gemacht hat. Drei Realsituationen: + +1. **Neuer SuS sucht Projekt-Idee:** Klickt im Katalog herum, sieht „Schlüsselanhänger — Mia, 🟢". Geht in der nächsten AG zu Mia: „Wie hast du das gemacht?" Mia kann die Hilfe leisten, weil sie ihren Stand selbst dokumentiert hat. + +2. **SuS-Stuck-Situation:** Tom hängt mit dem Drohnenrahmen fest. Lehrer schaut im Drill-Down-Dashboard (siehe [[lehrer-dashboard]]), sieht „Drohne — Lara, 🟡". Lehrer vermittelt: „Lara hat sowas gemacht, frag sie." + +3. **Erfahrener SuS wird Mentor:** Wenn Mias Projekt 3x als Vorlage gewählt wurde (`vorlage_projekt_id` zeigt darauf), wäre denkbar, dass Mia einen „Mentor"-Badge bekommt. Aktuell nicht implementiert — als spätere Idee in [[offene-fragen]]. + +## ZSL-Material als Initial-Inhalt + +Bei Kursstart ist die Bibliothek leer. Damit neue SuS nicht ins Nichts schauen, kommen **8-10 ZSL-Projekte** als Seed-Inhalt (siehe [[../decisions#E-022]]): + +- Markus + Herb wählen Projekte gemischter Schwierigkeit aus den 20+ ZSL-Vorlagen. +- Beschreibungen werden in unser Design umgearbeitet (mit CC-BY-4.0-Nachweis pro Karte: „Vorlage: Tilo Bödigheimer, ZSL Baden-Württemberg, CC-BY-4.0"). +- Bilder + STLs übernehmen wir 1:1 mit Quelle. +- Diese Projekte erscheinen als reguläre Katalog-Karten, sind aber **markiert als „Vorlagen-Projekt"** (Tag `zsl-vorlage` oder Badge). +- SuS können sie als Vorlage wählen — dadurch entstehen eigene Projekte daraus, die wiederum in den Katalog landen. + +## Sichtbarkeit für SuS + +- **Andere SuS sehen:** Cover, Titel, Macher, Tags, Selbsteinschätzung, Dateien, Beschreibung, „Was würdest du anderen mitgeben?". +- **Andere SuS sehen NICHT:** Druckaufträge, Slots, Stand-Notizen, Stunden-Beginn-Abfragen anderer. +- **Macher selbst:** sieht alles + kann eigene Projekte editieren (während `in_arbeit`). +- **Lehrer:** sieht alles immer, kann immer editieren (im Review-Modus oder via Admin). + +Konsistent mit [[../decisions#E-004]] („Inspiration ja, Vergleich nein"). + +## Spätere Erweiterungen (nicht in Etappe 6) + +Diese Ideen sind im Konzept dokumentiert, aber nicht zur Initial-Implementation geplant: + +- **Mentor-Badge** für SuS, deren Projekte oft als Vorlage gewählt werden. +- **Kommentar-Funktion** unter Projekten („Mia, wie hast du den Support entfernt?"). +- **Foto-Galerie pro Projekt** (mehrere Fotos im Lifecycle: Anfang, Mitte, Druck, Fertig). +- **Externe Veröffentlichung einzelner Projekte** auf Printables/MakerWorld (mit SuS-Zustimmung + Eltern-OK). +- **Speicher-Optimierung:** Migration der BLOB-Dateien auf Hetzner Object-Storage, sobald DB > 5 GB (siehe [[../decisions#E-016]]). + +## Querverweise + +- DB-Schema: [[datenmodell#projekt]], [[datenmodell#projekt_datei]], [[datenmodell#projekt_tag]], [[datenmodell#projekt_rueckfrage]] +- Routen: [[routen]] +- Lehrer-Sicht: [[lehrer-dashboard]] +- Entscheidungen: [[../decisions#E-014]] (Loop), [[../decisions#E-015]] (Feedback), [[../decisions#E-016]] (Files), [[../decisions#E-017]] (Review), [[../decisions#E-018]] (Three.js) diff --git a/docs/sicherheit.md b/docs/sicherheit.md new file mode 100644 index 0000000..7fea0c9 --- /dev/null +++ b/docs/sicherheit.md @@ -0,0 +1,58 @@ +--- +type: konzept-modul +projekt: 3d.lehrstun.de +stand: 2026-05-23 +tags: [konzept, sicherheit, material] +--- + +# Sicherheit & Druckraum + +> Inhalt der [[lektion-L1-drucker-kennenlernen|Pflicht-Lektion L1]] basiert +> auf den Punkten hier. + +## Setup an unserer Schule + +- **Eigener Druckraum**, dauergelüftet → entspannte Ausgangslage. +- **4 Drucker**: 3× geschlossen (P1S/X1C/P2S), 1× **A1 mini (offen)**. +- **Materialien (Default):** PLA + PLA+. **Selten:** PETG, ABS (nur unter Aufsicht). +- **Andere Filamente** (Nylon, ASA, PC, TPU) werden nicht genutzt. + +## Pflicht-Regeln für SuS + +1. **Hände weg vom Hotend** — besonders beim A1 mini (offen). +2. **Nur freigegebene Filamente** — PLA/PLA+ jederzeit, andere nur mit Herb/Markus. +3. **Bei sichtbarem Fehler (Spaghetti, Warping, Rauchgeruch)** → Drucker per + Bambu Handy App **pausieren** + Herb/Markus rufen, nicht selbst dran drehen. +4. **Druckraum verlassen** ist OK, solange ein Drucker läuft (Drucker haben + Notabschaltung). Türen geschlossen halten wegen Lüftung. + +## Materialkunde-Kurzfassung + +| Material | Empfohlen? | Warum | +|---|---|---| +| **PLA** | ja, Default | Geringste UFP-Emissionen, niedrige Druck-Temp, bricht spröde (Splitter unkritisch) | +| **PLA+** | ja | Wie PLA, etwas zäher | +| **PETG** | mit Aufsicht | Höhere Temp, etwas mehr Emissionen, klebt am Bett — Lehrer hilft bei Einrichtung | +| **ABS** | mit Aufsicht | Höhere Temp, Geruchsbildung, **nur in geschlossenen Druckern** (nicht A1 mini), Lüftung wichtig | + +PLA-Sicherheit basiert auf: [Ultimaker-Whitepaper (DE)](https://yop3d.de/wp-content/uploads/2020/07/3D-printer-emissions-and-indoor-air-quality-de.pdf), [Fraunhofer WKI](https://www.iaqip.wki.fraunhofer.de/de/themen/emissionen-aus-3d-druckern.html). + +## Offizielle Schul-Richtlinie + +[UKBW — Sicherer Umgang mit 3D-Druckern in der Schule (PDF)](https://www.ukbw.de/fileadmin/user_upload/Sicherer-Umgang-mit-3D-Druckern-in-der-Schule.pdf) — Pflichtlektüre für Herb + Markus, und Grundlage für die SuS-Lektion L1. + +## Was die Plattform dazu beiträgt + +- **Material-Auswahl** bei [[datenmodell#druckauftrag|Druckauftrag]] standardmäßig + auf PLA-Varianten beschränkt; PETG/ABS nur freischaltbar im + [[routen#admin|Admin]]. +- **A1 mini-Drucker** im UI mit gelbem Hinweis-Banner: "offener Drucker, Hände weg". +- **L1-Lektion** als Pflicht-Lektion vor Drucker-Bedienung (Einleitung, + Sicherheits-Video von Bambu Academy, Quiz?). + +## Querverweise + +- Lektion: [[lektion-L1-drucker-kennenlernen]] +- A1 mini-Hinweise im UI: [[architektur]] +- Materialauswahl bei Druckauftrag: [[datenmodell#druckauftrag]] +- Entscheidung Material-Default: [[../decisions#E-008]] diff --git a/docs/slotsystem.md b/docs/slotsystem.md new file mode 100644 index 0000000..b7cc7b1 --- /dev/null +++ b/docs/slotsystem.md @@ -0,0 +1,100 @@ +--- +type: konzept-modul +projekt: 3d.lehrstun.de +stand: 2026-05-23 +tags: [konzept, slots, reservierung] +--- + +# Slot-System (Drucker-Reservierung) + +> DB-Tabelle: [[datenmodell#slot]]. Routes: [[routen]]. Drucker-Status: [[druckerapi]]. + +## Ziel + +Faire Verteilung der 4 Drucker auf 8-10 SuS, die unterschiedlich lange Drucke +fahren. Funktioniert auch bei Drucker-Ausfall ([[druckerapi]] kann brechen). + +## Default: Windhund-Prinzip + +- Wer zuerst kommt, bekommt den Slot. +- Jede:r SuS reserviert selbst über das Cockpit. +- Konflikt-Check: keine Überlappung mit aktiver/zukünftiger reservierter Reservierung + auf demselben Drucker. + +## Override: Lehrer-Priorität + +- Im [[routen#admin|Admin-Dashboard]] kann Herb/Markus pro Slot `prioritaet` + setzen (Default 0, höher = wichtiger). +- Sortierung im UI: `ORDER BY prioritaet DESC, erstellt_am ASC`. +- Bei Konflikt durch Priorisierung: Site zeigt eine Warnung, verschiebt + nichts automatisch — Herb entscheidet. + +## Slot-Granularität (Entscheidung **offen**) + +Drei Optionen, siehe [[offene-fragen#slot-granularität]]: + +| Option | Vorteil | Nachteil | +|---|---|---| +| **Freie Zeitspannen** (Start + Dauer) | flexibel, passt zu realer Druckzeit (15 Min - 8 h) | Konflikt-Check komplexer | +| **Feste Stunden-Slots** | einfach zu buchen, kalenderartig | unflexibel — 4-h-Druck blockiert 4 Slots | +| **Tages-Slots** (Vormittag/Nachmittag/Nacht) | passt zu langen Drucken | zu grob für kurze Drucke | + +Empfehlung aus Recherche: **freie Zeitspannen**, weil Druckzeit stark variiert. + +## Drucker-Ausfall-Handling + +Wenn [[druckerapi]] meldet `status = failed` oder `offline` für > 10 Min: + +```mermaid +flowchart TB + A[Drucker fällt aus] --> B{Aktive Slots betroffen?} + B -- ja --> C[Dashboard: rote Markierung] + C --> D[Vorschlag: Slot auf anderen Drucker verschieben] + D --> E[Admin: 1-Klick-Verschieben] + E --> F[Slot.drucker_id ändern] + F --> G[Cockpit-Notiz für betroffene SuS] + B -- nein --> H[Drucker als 'Wartung nötig' markieren] +``` + +## Konflikt-Erkennung + +```sql +-- Neue Reservierung würde mit bestehender überlappen? +SELECT COUNT(*) FROM slot +WHERE drucker_id = :drucker_id + AND status IN ('reserviert', 'aktiv') + AND start_zeit < :neue_ende + AND ende_zeit > :neue_start; +``` + +Falls > 0 Treffer: Reservierung abgelehnt mit Liste der Konflikte. + +## UI-Skizze (Cockpit-Slot-Buchung) + +``` +┌─ Drucker reservieren ──────────────────────────┐ +│ │ +│ Drucker: [ ▼ P1S #1 ] │ +│ Start: [ Heute, 14:30 ] │ +│ Dauer: [ 2 h 30 m ] (Schätzung aus 3MF) │ +│ │ +│ ┌─ Kalender ────────────────────────────────┐ │ +│ │ P1S #1 ▓▓░░░░░░░░░░░░▓▓▓▓▓░░░░░░░░░░░ │ │ +│ │ P1S #2 ░░░░░▓▓▓▓░░░░░░░░░░░░░░░▓▓▓░░░ │ │ +│ │ X1C ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │ │ +│ │ A1 mini ▓▓▓▓▓▓▓░░░░░░░░░░░▓▓░░░░░░░░░░ │ │ +│ │ 09 11 13 15 17 19 21 23 │ │ +│ └────────────────────────────────────────────┘ │ +│ │ +│ [ Reservieren ] │ +└─────────────────────────────────────────────────┘ +``` + +(Genaues Design folgt in [[etappen#etappe-6]].) + +## Querverweise + +- DB-Schema: [[datenmodell#slot]] +- Drucker-Live-Status (für Ausfall-Erkennung): [[druckerapi]] +- Routes: [[routen#profil]] + [[routen#admin]] +- Offene Frage zur Granularität: [[offene-fragen]] diff --git a/docs/software-bambu-handy.md b/docs/software-bambu-handy.md new file mode 100644 index 0000000..1484b4f --- /dev/null +++ b/docs/software-bambu-handy.md @@ -0,0 +1,39 @@ +--- +type: software +slug: bambu-handy +titel: Bambu Handy App +url: https://bambulab.com/de/download +plattform: [ios, android] +kosten: kostenlos +tags: [software, app, monitoring] +--- + +# Bambu Handy App + +> SuS-Smartphone-Begleiter für den Druck. Sehen den eigenen Druck live, können +> ihn pausieren, sehen die Drucker-Kamera. + +## Was es ist + +Bambus offizielle Smartphone-App. iOS + Android, kostenlos. Verbindet sich +mit dem Bambu-Account und zeigt alle Drucker, die dem Account zugeordnet sind. + +## Was die SuS damit machen + +- Druckfortschritt + Restzeit ansehen, ohne im Raum zu sein. +- **Drucker pausieren / abbrechen**, wenn etwas schief geht (siehe [[sicherheit#pflicht-regeln-für-sus|Pflicht-Regel #3]]). +- Drucker-Kamera live ansehen (jeder Bambu-Drucker hat eine). +- Push-Benachrichtigung "Dein Druck ist fertig". + +## Setup (in [[lektion-L1-drucker-kennenlernen|L1]] eingeführt) + +1. App im Store suchen ("Bambu Handy"), installieren. +2. Einloggen mit Schul-Bambu-Account (Herb hat die Daten). +3. Alle 4 Drucker erscheinen automatisch. + +## Querverweise + +- Pendant am PC: [[software-bambu-studio]] +- Sicherheits-Pflicht: [[sicherheit]] +- Eingeführt in: [[lektion-L1-drucker-kennenlernen]] +- **Warum die Plattform diese Funktion nicht selbst hat:** [[druckerapi#was-wir-nicht-machen]] (Kamera-Streams auf Site = DSGVO-Schmerz) diff --git a/docs/software-bambu-studio.md b/docs/software-bambu-studio.md new file mode 100644 index 0000000..bb216dc --- /dev/null +++ b/docs/software-bambu-studio.md @@ -0,0 +1,49 @@ +--- +type: software +slug: bambu-studio +titel: Bambu Studio +url: https://bambulab.com/de/download/studio +plattform: [windows, mac, linux] +kosten: kostenlos +tags: [software, slicer, pflicht] +--- + +# Bambu Studio + +> Der Pflicht-[[glossar#software-dateien|Slicer]] für unsere Drucker. Auf den +> Schul-PCs installieren, vor [[lektion-L1-drucker-kennenlernen|L1]] startklar. + +## Was es ist + +Bambu Labs eigene Slicer-Software, läuft lokal auf dem Schul-PC. Nimmt ein +STL- oder 3MF-Modell, zerlegt es in Schichten, generiert [[glossar#g-code|G-Code]] +und sendet ihn (über Bambu Cloud) an den ausgewählten Drucker. + +## Was die SuS damit machen + +- **L1:** vorbereitetes Modell öffnen, Drucker wählen, "Print" drücken. +- **L2:** eigenes Modell von [[software-printables]] importieren, slicen, drucken. +- **L3+:** eigene Modelle aus [[software-tinkercad]] / [[software-openscad]] importieren. + +## Quickstart (intern, kurze Version für die Site) + +1. Download von [bambulab.com/de/download/studio](https://bambulab.com/de/download/studio) +2. Installieren, beim Start "EU"-Region wählen. +3. Mit Schul-Bambu-Account einloggen (Herb hat die Zugangsdaten). +4. Drucker erscheinen automatisch in der Geräte-Liste. +5. Modell per Drag&Drop ins Fenster ziehen. +6. Drucker + Filament auswählen (PLA Default). +7. "Slice" → Druckzeit + Material werden gezeigt. +8. "Print" → Druckjob startet auf dem gewählten Drucker. + +## Offizielle Doku + +- [Quickstart EN (Bambu Wiki)](https://wiki.bambulab.com/en/software/bambu-studio/studio-quick-start) +- [Quickstart A1 DE (PDF)](https://cdn1.bambulab.com/documentation/quick-start-b7f4999004153/Quick%20start%20guide-A1-German%20Version.pdf) +- [Quickstart P1P DE (PDF)](https://cdn1.bambulab.com/documentation/quick-start-ae7a0e4825b43/de/QuickStart-P1P-DE.pdf) + +## Querverweise + +- Lektionen, die das nutzen: [[lektion-L1-drucker-kennenlernen]], [[lektion-L2-modell-drucken]], [[lektion-L3-customizer]] +- Verbunden mit: [[software-bambu-handy]] (Handy-Pendant) +- Bezug zu unserer Drucker-API: NICHTS direkt — wir greifen unabhängig auf die Cloud zu, siehe [[druckerapi]] diff --git a/docs/software-makerworld.md b/docs/software-makerworld.md new file mode 100644 index 0000000..4c1f126 --- /dev/null +++ b/docs/software-makerworld.md @@ -0,0 +1,46 @@ +--- +type: software +slug: makerworld +titel: MakerWorld +url: https://makerworld.com +kosten: kostenlos (Konto nötig) +tags: [software, modelle, plattform, customizer, pflicht-l3] +--- + +# MakerWorld + +> Bambu-eigene Modell-Plattform. **Stark im Customizer** — perfekt für [[lektion-L3-customizer|L3]]. +> Konto-Pflicht für Download, deshalb nicht erste Wahl für L2. + +## Wofür wir es nutzen + +- **L3**: Customizer-Modelle (Schlüsselanhänger mit eigenem Namen, Box mit Wunschmaßen, …) +- Optional in L2 als Alternative zu [[software-printables|Printables]] + +## Customizer + +Das ist das Killer-Feature: Browser-basierte parametrische Modelle. SuS +verändern Werte (Name, Größe, Farbe) → MakerWorld rendert das fertige Modell +live → STL-Download → in [[software-bambu-studio|Bambu Studio]] slicen. + +**Konkret für L3:** +- [Parametric Customizable Name Keychain Creator](https://makerworld.com/en/models/1320280) +- weitere Empfehlungen folgen aus dem L3-Stub: [[lektion-L3-customizer#empfohlene-customizer-für-l3-todo-mit-markus-prüfen]] + +## Was die SuS dafür brauchen + +- **Konto** — Herb klärt, ob jeder SuS ein eigenes anlegt oder ob alle einen + Schul-Account teilen. (→ in [[offene-fragen]] aufnehmen) + +## Lizenz + +MakerWorld hat eine eigene "MakerWorld License" plus CC-Optionen. Für unseren +Schulgebrauch (drucken für uns selbst) unkritisch. + +## Querverweise + +- Lektion: [[lektion-L3-customizer]] +- Slicer: [[software-bambu-studio]] +- Alternative Modell-Plattform: [[software-printables]] +- Bambu-Drucker-Anbindung: [[druckerapi]] (MakerWorld läuft technisch über + dieselbe Cloud, aber wir nutzen sie unabhängig) diff --git a/docs/software-openscad.md b/docs/software-openscad.md new file mode 100644 index 0000000..223a06d --- /dev/null +++ b/docs/software-openscad.md @@ -0,0 +1,61 @@ +--- +type: software +slug: openscad +titel: OpenSCAD +url: https://openscad.org +plattform: [windows, mac, linux] +kosten: kostenlos (Open Source, GPL) +tags: [software, cad, parametrisch, frei-l4] +--- + +# OpenSCAD + +> Parametrisches CAD — Modelle werden **als Code geschrieben**. Krönung der +> 4-Stufen-Progression. Brücke zwischen Mathe/Informatik und Werkstattproduktion. + +## Was es ist + +Statt mit der Maus zu klicken, schreiben SuS einen kurzen Code: + +```scad +// Schluesselanhaenger +breite = 30; +hoehe = 15; +dicke = 4; +loch_radius = 3; + +difference() { + cube([breite, hoehe, dicke]); + translate([5, hoehe/2, -1]) + cylinder(h = dicke + 2, r = loch_radius); +} +``` + +→ OpenSCAD rendert ein 3D-Modell live → STL-Export. + +**Magie:** Ändere eine Zahl, ändere das Modell. **Parametrisch im wahren Sinn.** + +## Wofür in unserer AG + +- L4+/L5+ als parametrischer Pfad, parallel zu [[software-tinkercad]]. +- Brücke zur Informatik: SuS schreiben (kleinen) Code, sehen sofort die Wirkung. + +## Empfohlene Tutorials / Quellen (DE) + +Aus der Recherche: +- [inf-schule.de — OpenSCAD im 3D-Druck](https://inf-schule.de/informatiksysteme/3d_druck/grundlage_cad/openscad) — **erste Wahl, schul-erprobt** +- [Hessen-Hub OpenSCAD-Tutorial (PDF)](https://www.hessenhub.de/wp-content/uploads/2022/09/OpenScad-Tutorial.pdf) +- [Wikibooks OpenSCAD Benutzerhandbuch (DE)](https://de.wikibooks.org/wiki/OpenSCAD_Benutzerhandbuch/Kapitel_1) +- [attempto.blog — Programmieren ist das neue Modellieren](https://attempto.blog/de/2024/programming-3d-models-openscad/) + +## Lehrer-Tipps + +- inf-schule liefert komplette Lektionssequenzen — fast turnkey, mit Markus durchsehen. +- Erste Projekte: Schlüsselanhänger, Stiftehalter, Einkaufswagenchip — alle in inf-schule fertig. + +## Querverweise + +- Visuelle Alternative: [[software-tinkercad]] +- Slicer: [[software-bambu-studio]] +- Konzept-Pfad: [[../kurskonzept#vier-stufen-progression]] +- Entscheidung "warum OpenSCAD statt Fusion 360": [[offene-fragen#cad-schwerpunkt-l4]] diff --git a/docs/software-printables.md b/docs/software-printables.md new file mode 100644 index 0000000..0c72fbd --- /dev/null +++ b/docs/software-printables.md @@ -0,0 +1,57 @@ +--- +type: software +slug: printables +titel: Printables.com +url: https://www.printables.com +kosten: kostenlos +tags: [software, modelle, plattform, pflicht-l2] +--- + +# Printables.com + +> Modell-Plattform von Prusa. **Empfohlene Erst-Adresse** für SuS — werbefrei, +> sauber moderiert, deutsche UI, eigene Education-Sektion. + +## Warum diese und nicht Thingiverse? + +Aus der [Recherche](../README.md) hervorgegangen: +- **Thingiverse**: NSFW-Werbeanzeigen für 14-Jährige berichtet → nicht empfehlen. +- **Cults3D**: starke Bezahl-Push, viele kostenpflichtige Modelle → nicht empfehlen. +- **Printables**: kein NSFW, Education-Sektion, deutsche UI, optionaler Login. +- **MakerWorld**: gut für [[software-makerworld#customizer|Customizer]], aber Konto-Pflicht. + +→ Default: Printables zuerst zeigen, MakerWorld speziell für [[lektion-L3-customizer]]. + +## Was die SuS damit machen + +- **L2**: ein passendes Modell suchen, herunterladen, drucken. +- Inspiration sammeln für eigene Projekte (L5+). + +## Quickstart + +1. [printables.com](https://www.printables.com) öffnen. +2. Suchbegriff eingeben (z.B. "keychain", "phone stand"). +3. Filter: "Print Time" auf "< 1h" stellen für schnelle Drucke. +4. Modell anklicken → "Download" (ohne Login bis 25 Modelle/Monat). +5. STL oder 3MF speichern, in [[software-bambu-studio|Bambu Studio]] importieren. + +## Lizenz-Hinweise (Kurz für SuS) + +Die meisten Modelle stehen unter **CC-BY** oder **CC-BY-NC**: +- **CC-BY** = darfst alles, musst Autor nennen. +- **CC-BY-NC** = wie oben, aber nicht kommerziell. +- **CC-BY-SA** = darfst alles, musst gleiche Lizenz weitergeben. +- **CC-BY-ND** = darfst nicht ändern. + +Wir drucken nur für uns selbst → Lizenz-Sorge minimal. + +## Education-Programm + +[printables.com/education](https://www.printables.com/education) — kuratierte +Sammlung speziell für Schulen. Mit Markus durchsehen für L2-Empfehlungen. + +## Querverweise + +- Lektion: [[lektion-L2-modell-drucken]] +- Slicer für die Drucke: [[software-bambu-studio]] +- Alternative für Customizer: [[software-makerworld]] diff --git a/docs/software-tinkercad.md b/docs/software-tinkercad.md new file mode 100644 index 0000000..f5a9269 --- /dev/null +++ b/docs/software-tinkercad.md @@ -0,0 +1,57 @@ +--- +type: software +slug: tinkercad +titel: TinkerCAD +url: https://www.tinkercad.com +plattform: [browser] +kosten: kostenlos +tags: [software, cad, frei-l4] +--- + +# TinkerCAD + +> Visuelles CAD im Browser von Autodesk. **Erster Schritt zu eigenem Modellieren** +> nach L3. + +## Was es ist + +Visuelles, klick-basiertes 3D-CAD. SuS ziehen Grundkörper (Quader, Zylinder, …) +auf die Arbeitsfläche, kombinieren sie, bohren Löcher mit "Hole"-Objekten, +exportieren STL. + +## Klassenraum-Modus (DSGVO-tauglich) + +- Lehrer (Herb) legt einen "Class"-Code an. +- SuS treten **ohne E-Mail-Adresse** bei, nur mit Klassencode + Spitzname. +- Keine personenbezogenen Daten an Autodesk. + +Quellen: +- [Autodesk Children's Privacy Statement (DE)](https://www.autodesk.com/company/legal-notices-trademarks/privacy-statement-de/childrens-privacy-statement) +- [Beispiel-Datenschutz Dillenberg-Schule (PDF)](https://www.dillenberg-schule.de/informationen-formulare.html?file=files/Dillenberg/Downloads/Datenschutz+-+Nutzungsordnung/Datenschutz_Tinkercad.pdf) + +## Wofür in unserer AG + +- L4-Einstieg ins eigene Modellieren. +- Für schnelle eigene Ideen, die nicht durch [[software-openscad|OpenSCAD]] gehen müssen. +- "Komfort-CAD" — visuell, sofort sichtbar, niedrige Frustschwelle. + +## Quickstart (intern, kurz) + +1. [tinkercad.com](https://www.tinkercad.com) +2. Klassencode von Herb eingeben + Spitzname wählen. +3. "New 3D Design" → arbeiten. +4. "Export" → STL → in [[software-bambu-studio|Bambu Studio]] importieren. + +## Empfohlen weil + +- Werbefrei im Klassen-Modus. +- DSGVO-tauglich (keine SuS-Mail). +- Deutsche UI vollständig. +- Tutorials in der App eingebaut. + +## Querverweise + +- Freie Phase ab: [[lektion-L3-customizer]] +- Parametrische Alternative: [[software-openscad]] +- Slicer: [[software-bambu-studio]] +- Konzept: [[../kurskonzept#vier-stufen-progression]] diff --git a/migrations/README.md b/migrations/README.md new file mode 100644 index 0000000..b04b87d --- /dev/null +++ b/migrations/README.md @@ -0,0 +1,21 @@ +# Migrationen + +Idempotente Schema-Migrationen, analog zu `arduino.lehrstun.de/migrations/`. + +## Namenskonvention + +`YYYY-MM-DD_kurzbeschreibung.py` + +## Regeln + +- Jede Migration muss **idempotent** sein — wenn sie zweimal laeuft, darf nichts + kaputt gehen. +- Bei Schema-Aenderungen auf der Live-Site: erst Service stoppen, dann Migration, + dann Service wieder starten (DB-Lock-Vermeidung). +- Reihenfolge ist alphabetisch nach Dateinamen. + +## Anwenden + +```bash +for m in migrations/2026-*.py; do python "$m"; done +``` diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..f6b7580 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +flask>=3.0 +markdown>=3.5 +bcrypt>=4.1 +python-dotenv>=1.0 +# Drucker-Anbindung — wird in Etappe 4/5 wirklich genutzt: +# bambulabs_api>=2.0 diff --git a/routes/__init__.py b/routes/__init__.py new file mode 100644 index 0000000..53b5f3c --- /dev/null +++ b/routes/__init__.py @@ -0,0 +1,10 @@ +"""Routes-Package fuer 3d.lehrstun.de. + +Blueprint-Struktur nach V8-Entscheidung (siehe docs/decisions.md#E-020). + +Module: +- oeffentlich: Startseite, Lektionen, Software, Projekt-Katalog, Drucker, Troubleshooting +- profil: Login, Cockpit, eigene Projekte/Auftraege/Slots +- admin: Dashboard, Profile-Verwaltung, Review-Queue, Inline-Edit +- api: JSON-Endpunkte fuer Frontend-Polling +""" diff --git a/routes/admin.py b/routes/admin.py new file mode 100644 index 0000000..8a86b2f --- /dev/null +++ b/routes/admin.py @@ -0,0 +1,259 @@ +"""Admin-Routen: Login, Profile-Verwaltung, Inline-Edit. + +Etappe 1: Admin-Login + Profil-Anlage/-Loeschung + PIN-Karten-Druckbogen + Inline-Edit. +Etappe 2: Drill-Down-Dashboard (V5). +Etappe 6: Projekt-Review-Queue. +Etappe 7: Druckauftrag-Freigabe + Slot-Priorisierung. +""" +import secrets + +from flask import Blueprint, render_template, request, redirect, url_for, session, flash, jsonify, abort + +from database import get_db +from services.auth import pin_hashen, admin_passwort_pruefen, admin_required +from tiere import TIERE, tier_emoji, tier_name + +bp = Blueprint("admin", __name__, url_prefix="/admin") + + +# =========================================================================== +# Whitelist fuer Inline-Edit (Pattern vom Arduino-Kurs) +# =========================================================================== + +ERLAUBTE_INLINE_TABELLEN = { + # Tabelle -> Set erlaubter Felder + "lektion": {"titel", "aufgabe_md", "tipp_md", "extern_link"}, + "einstellung": {"wert"}, + "drucker": {"name", "notiz"}, + # spaeter: + # "projekt": {"titel", "beschreibung_md"} -- mit Status-Check, Etappe 6 + # "software_seite": {"titel", "inhalt_md", "url", "quickstart_md"} -- Etappe 3 +} + + +# =========================================================================== +# Admin-Login +# =========================================================================== + +@bp.route("/login", methods=["GET", "POST"]) +def admin_login(): + """Passwort-Eingabe fuer Admin-Modus. Passwort aus .env (ADMIN_PASSWORT).""" + if request.method == "POST": + eingabe = (request.form.get("passwort") or "").strip() + if admin_passwort_pruefen(eingabe): + session["admin"] = True + flash("Admin-Modus aktiv.", "success") + return redirect(url_for("admin.profile_liste")) + flash("Falsches Passwort.", "error") + return render_template("admin_login.html") + + +@bp.route("/logout") +def admin_logout(): + session.pop("admin", None) + flash("Admin-Modus aus.", "success") + return redirect(url_for("oeffentlich.index")) + + +# =========================================================================== +# Profil-Verwaltung (Etappe 1) +# =========================================================================== + +@bp.route("/profile") +@admin_required +def profile_liste(): + conn = get_db() + profile = conn.execute( + "SELECT p.id, p.vorname, p.tier, p.aktiv, p.erstellt_am, " + " p.ag_id, ag.name AS ag_name " + "FROM profil p " + "LEFT JOIN ag_mitgliedschaft ag ON p.ag_id = ag.id " + "ORDER BY p.aktiv DESC, p.vorname COLLATE NOCASE" + ).fetchall() + ags = conn.execute("SELECT id, name FROM ag_mitgliedschaft WHERE status = 'aktiv'").fetchall() + conn.close() + return render_template( + "admin_profile.html", + profile=profile, + ags=ags, + tiere=TIERE, + ) + + +@bp.route("/profil/neu", methods=["POST"]) +@admin_required +def profil_neu(): + """Neues Profil anlegen. Generiert automatisch eine 4-stellige PIN, zeigt sie einmalig.""" + vorname = (request.form.get("vorname") or "").strip() + tier = (request.form.get("tier") or "").strip() + ag_id = (request.form.get("ag_id") or "").strip() + + if not (vorname and tier and ag_id): + flash("Bitte Vorname, Tier und AG angeben.", "error") + return redirect(url_for("admin.profile_liste")) + + # PIN generieren: 4-stellig, mit fuehrenden Nullen + pin = f"{secrets.randbelow(10000):04d}" + pin_hash = pin_hashen(pin) + + conn = get_db() + try: + cur = conn.execute( + "INSERT INTO profil (vorname, tier, pin_hash, ag_id, aktiv) " + "VALUES (?, ?, ?, ?, 1)", + (vorname, tier, pin_hash, ag_id) + ) + conn.commit() + neue_id = cur.lastrowid + except Exception as e: + conn.close() + flash(f"Fehler beim Anlegen: {e}", "error") + return redirect(url_for("admin.profile_liste")) + conn.close() + + # PIN als Flash zeigen — nur EINMAL sichtbar, danach nur Hash in DB + flash( + f"Profil '{vorname}' {tier_emoji(tier)} angelegt. " + f"PIN: {pin} — bitte aufschreiben, sie wird nicht erneut gezeigt.", + "success" + ) + return redirect(url_for("admin.profile_liste")) + + +@bp.route("/profil//aktiv", methods=["POST"]) +@admin_required +def profil_aktiv_toggle(profil_id): + """Profil aktiv/inaktiv schalten (statt loeschen, weil FK-Cascade die ganze Historie killen wuerde).""" + conn = get_db() + conn.execute( + "UPDATE profil SET aktiv = CASE aktiv WHEN 1 THEN 0 ELSE 1 END WHERE id = ?", + (profil_id,) + ) + conn.commit() + conn.close() + return redirect(url_for("admin.profile_liste")) + + +@bp.route("/profil//pin-reset", methods=["POST"]) +@admin_required +def profil_pin_reset(profil_id): + """Neue PIN generieren, einmalig zeigen.""" + conn = get_db() + profil = conn.execute( + "SELECT id, vorname, tier FROM profil WHERE id = ?", + (profil_id,) + ).fetchone() + if not profil: + conn.close() + flash("Profil nicht gefunden.", "error") + return redirect(url_for("admin.profile_liste")) + + pin = f"{secrets.randbelow(10000):04d}" + pin_hash = pin_hashen(pin) + conn.execute("UPDATE profil SET pin_hash = ? WHERE id = ?", (pin_hash, profil_id)) + # Lockout zuruecksetzen, falls einer aktiv war + conn.execute("DELETE FROM login_lockout WHERE profil_id = ?", (profil_id,)) + conn.commit() + conn.close() + + flash( + f"Neue PIN fuer '{profil['vorname']}' {tier_emoji(profil['tier'])}: {pin} " + f"— bitte sofort aufschreiben.", + "success" + ) + return redirect(url_for("admin.profile_liste")) + + +# =========================================================================== +# Inline-Edit (Etappe 1) +# =========================================================================== + +@bp.route("/inline-edit", methods=["POST"]) +@admin_required +def inline_edit(): + """Universal-Endpunkt fuer Inline-Edit. Prueft Whitelist + loggt Aenderung.""" + tabelle = (request.form.get("tabelle") or "").strip() + datensatz_id = request.form.get("id") + feld = (request.form.get("feld") or "").strip() + neuer_wert = request.form.get("wert", "") + + # Whitelist-Pruefung + if tabelle not in ERLAUBTE_INLINE_TABELLEN: + return jsonify(ok=False, error=f"Tabelle '{tabelle}' nicht erlaubt"), 400 + if feld not in ERLAUBTE_INLINE_TABELLEN[tabelle]: + return jsonify(ok=False, error=f"Feld '{feld}' nicht erlaubt"), 400 + try: + datensatz_id = int(datensatz_id) + except (TypeError, ValueError): + return jsonify(ok=False, error="Ungueltige ID"), 400 + + conn = get_db() + # Alten Wert lesen fuer Log + alt_row = conn.execute( + f"SELECT {feld} AS alt FROM {tabelle} WHERE id = ?", + (datensatz_id,) + ).fetchone() + if not alt_row: + conn.close() + return jsonify(ok=False, error="Datensatz nicht gefunden"), 404 + alter_wert = alt_row["alt"] + + # Update + conn.execute( + f"UPDATE {tabelle} SET {feld} = ? WHERE id = ?", + (neuer_wert, datensatz_id) + ) + # Audit-Log + conn.execute( + "INSERT INTO aenderung_log (tabelle, datensatz_id, feld, alter_wert, neuer_wert) " + "VALUES (?, ?, ?, ?, ?)", + (tabelle, datensatz_id, feld, str(alter_wert) if alter_wert is not None else "", neuer_wert) + ) + conn.commit() + conn.close() + return jsonify(ok=True, wert=neuer_wert) + + +# =========================================================================== +# Druckbogen fuer PIN-Karten (Etappe 1, CSS-Print) +# =========================================================================== + +@bp.route("/profile/karten") +@admin_required +def profile_karten(): + """Druckbogen mit Profil-Karten — Tier, Vorname, Platz fuer PIN handschriftlich.""" + conn = get_db() + profile = conn.execute( + "SELECT id, vorname, tier FROM profil WHERE aktiv = 1 " + "ORDER BY vorname COLLATE NOCASE" + ).fetchall() + conn.close() + return render_template("admin_profile_karten.html", profile=profile) + + +# =========================================================================== +# Stubs fuer spaetere Etappen +# =========================================================================== + +@bp.route("/dashboard") +@admin_required +def dashboard(): + abort(501) # Etappe 2 + + +@bp.route("/lektionen") +@admin_required +def admin_lektionen(): + abort(501) # Etappe 3 + + +@bp.route("/drucker") +@admin_required +def admin_drucker(): + abort(501) # Etappe 5 + + +@bp.route("/projekt-freigabe") +@admin_required +def projekt_freigabe(): + abort(501) # Etappe 6 diff --git a/routes/api.py b/routes/api.py new file mode 100644 index 0000000..cc91905 --- /dev/null +++ b/routes/api.py @@ -0,0 +1,27 @@ +"""JSON-API fuer Frontend-Polling. Voll in Etappen 5/6. + +Etappe 5: /api/drucker/status (Live-Status fuer /drucker) +Etappe 6: /api/projekte/suche (Filter-Live-Suche im Katalog) +Etappe 5: /api/slots/heute (Slot-Polling) +""" +from flask import Blueprint, jsonify, abort + +bp = Blueprint("api", __name__, url_prefix="/api") + + +@bp.route("/drucker/status") +def drucker_status(): + """Etappe 5: JSON aller 4 Drucker mit Status/Restzeit.""" + abort(501) + + +@bp.route("/slots/heute") +def slots_heute(): + """Etappe 6: JSON aller aktiven/anstehenden Slots heute.""" + abort(501) + + +@bp.route("/projekte/suche") +def projekte_suche(): + """Etappe 6: Filter-Live-Suche im Katalog.""" + abort(501) diff --git a/routes/oeffentlich.py b/routes/oeffentlich.py new file mode 100644 index 0000000..052d1bd --- /dev/null +++ b/routes/oeffentlich.py @@ -0,0 +1,56 @@ +"""Oeffentliche Routen (kein Login noetig). + +Etappe 1: nur Startseite + Error-Handler portiert. +Etappe 3: Lektionen, Software, Projekt-Katalog, Troubleshooting. +Etappe 5: Drucker-Live-View. +""" +from flask import Blueprint, render_template, abort + +bp = Blueprint("oeffentlich", __name__) + + +@bp.route("/") +def index(): + return render_template("index.html") + + +# Stubs fuer spaetere Etappen — geben 501 zurueck, damit Navigation nicht bricht. + +@bp.route("/lektionen") +def lektionen(): + abort(501) + + +@bp.route("/lektion/") +def lektion_detail(nummer): + abort(501) + + +@bp.route("/software") +def software(): + abort(501) + + +@bp.route("/software/") +def software_detail(slug): + abort(501) + + +@bp.route("/projekte") +def projekte(): + abort(501) + + +@bp.route("/projekte/") +def projekt_detail(projekt_id): + abort(501) + + +@bp.route("/troubleshooting") +def troubleshooting(): + abort(501) + + +@bp.route("/drucker") +def drucker(): + abort(501) diff --git a/routes/profil.py b/routes/profil.py new file mode 100644 index 0000000..0c131c0 --- /dev/null +++ b/routes/profil.py @@ -0,0 +1,131 @@ +"""SuS-Profil-Routen: Login, Cockpit (Skelett), Logout. + +Etappe 1: Login-Flow mit bcrypt + persistenter Lockout (E-020 V7). +Etappe 2: Cockpit (progressiv), Check-In, Paarung. +Etappe 6: Projekt-Workflow. +Etappe 7: Druckauftraege + Slots. +""" +from flask import Blueprint, render_template, request, redirect, url_for, session, flash, abort + +from database import get_db +from services.auth import ( + pin_pruefen, + ist_gesperrt, + fehlversuch_loggen, + lockout_zuruecksetzen, + login_required, +) +from tiere import tier_emoji, tier_name + +bp = Blueprint("profil", __name__) + + +# =========================================================================== +# Login-Flow (Etappe 1) +# =========================================================================== + +@bp.route("/login") +def login(): + """Kachel-Seite mit allen aktiven Profilen (Vorname + Tier-Icon).""" + conn = get_db() + profile = conn.execute( + "SELECT id, vorname, tier FROM profil WHERE aktiv = 1 " + "ORDER BY vorname COLLATE NOCASE, tier" + ).fetchall() + conn.close() + return render_template("login.html", profile=profile) + + +@bp.route("/login/", methods=["GET", "POST"]) +def login_pin(profil_id): + """PIN-Eingabe fuer ein gewaehltes Profil.""" + conn = get_db() + profil = conn.execute( + "SELECT id, vorname, tier, pin_hash, aktiv FROM profil WHERE id = ?", + (profil_id,) + ).fetchone() + conn.close() + + if not profil or not profil["aktiv"]: + flash("Profil nicht gefunden.", "error") + return redirect(url_for("profil.login")) + + # Lockout-Check (persistent in DB, V7) + gesperrt, sek_rest = ist_gesperrt(profil_id) + if gesperrt: + min_rest = sek_rest // 60 + 1 + return render_template( + "login_pin.html", + profil=profil, + gesperrt=True, + min_rest=min_rest + ) + + if request.method == "POST": + eingabe_pin = (request.form.get("pin") or "").strip() + if pin_pruefen(eingabe_pin, profil["pin_hash"]): + # Erfolg: Lockout zuruecksetzen, Session befuellen + lockout_zuruecksetzen(profil_id) + session.clear() + session["profil_id"] = profil["id"] + session["vorname"] = profil["vorname"] + session["tier"] = profil["tier"] + return redirect(url_for("profil.cockpit")) + else: + fehlversuch_loggen(profil_id) + flash("Falsche PIN. Versuch's nochmal.", "error") + + return render_template("login_pin.html", profil=profil, gesperrt=False) + + +@bp.route("/logout") +def logout(): + session.clear() + flash("Du bist ausgeloggt.", "success") + return redirect(url_for("oeffentlich.index")) + + +# =========================================================================== +# Cockpit (Etappe 1: Minimal-Stub, voll in Etappe 2 mit progressivem Layout) +# =========================================================================== + +@bp.route("/cockpit") +@login_required +def cockpit(): + """Minimal-Stub fuer Etappe 1. + + Etappe 2 macht das progressiv (V1): + - Onboarding nicht erledigt: nur Onboarding-Karte + Check-In-Trigger + - L1 fertig: zusaetzlich Drucker-Buchung + - Hat aktive Projekte: zusaetzlich Projekt-Karten, Druckauftraege, Slots + """ + return render_template( + "cockpit_stub.html", + vorname=session.get("vorname"), + tier=session.get("tier"), + ) + + +# Spaetere Etappen — Stubs damit Navigation nicht bricht +@bp.route("/cockpit/check-in", methods=["POST"]) +@login_required +def check_in(): + abort(501) + + +@bp.route("/cockpit/lektion//status", methods=["POST"]) +@login_required +def lektion_status(nummer): + abort(501) + + +@bp.route("/cockpit/slots") +@login_required +def slots(): + abort(501) + + +@bp.route("/cockpit/druckauftraege") +@login_required +def druckauftraege(): + abort(501) diff --git a/seed.py b/seed.py new file mode 100644 index 0000000..133f574 --- /dev/null +++ b/seed.py @@ -0,0 +1,86 @@ +"""Initial-Seeds fuer 3d.lehrstun.de. + +Idempotent — laeuft bei jeder Ausfuehrung ohne Schaden. + +Aktuell befuellt: +- ag_mitgliedschaft: 'lessing-3d-ag' (die eine, dauerhafte AG) +- drucker: 4 Bambu-Lab-Drucker (3 geschlossen + 1 A1 mini) + +Spaeter folgen Seeds fuer: +- lektion: L1-L3 Onboarding (Etappe 3, mit Markus inhaltlich) +- einstellung: Default-Werte fuer Tag-Vorschlaege etc. +""" +from database import get_db, init_db + + +def seed_ag(): + """Die eine dauerhafte AG.""" + conn = get_db() + conn.execute( + "INSERT OR IGNORE INTO ag_mitgliedschaft (id, name, start_datum, status) " + "VALUES (?, ?, ?, ?)", + ("lessing-3d-ag", "3D-Druck-AG Lessing-Gymnasium", "2026-05-24", "aktiv") + ) + conn.commit() + conn.close() + print("[seed] AG 'lessing-3d-ag' angelegt (oder bereits da).") + + +def seed_drucker(): + """4 Bambu-Lab-Drucker mit Default-Namen. + + Konkrete Serial-Numbers + Namen werden vom Admin per Inline-Edit gepflegt. + Hier nur Platzhalter, damit die Drucker-Verwaltung was zum Anzeigen hat. + """ + drucker = [ + ("P1S #1", "P1S", None, 1, "geschlossenes Gehäuse"), + ("P1S #2", "P1S", None, 1, "geschlossenes Gehäuse"), + ("X1C", "X1C", None, 1, "geschlossen + HEPA"), + ("A1 mini", "A1 mini", None, 0, "offen — Hände weg vom Hotend!"), + ] + conn = get_db() + for name, modell, serial, geschlossen, notiz in drucker: + conn.execute( + "INSERT OR IGNORE INTO drucker (name, modell, serial_nr, geschlossenes_gehaeuse, aktiv, notiz) " + "VALUES (?, ?, ?, ?, 1, ?)", + (name, modell, serial, geschlossen, notiz) + ) + conn.commit() + conn.close() + print(f"[seed] {len(drucker)} Drucker angelegt (oder bereits da).") + + +def seed_einstellungen(): + """Default-Einstellungen, die spaeter inline editiert werden koennen.""" + werte = [ + ("tag_vorschlaege", + "schluesselanhaenger,organizer,werkzeug,spiel,dekoration,ersatzteil,geschenk,figur", + "text", + "Komma-getrennte Liste der vorgeschlagenen Tags im Projekt-Katalog"), + ("default_lockout_dauer_min", "15", "integer", + "Minuten, die ein Profil bei zu vielen Fehlversuchen gesperrt wird"), + ("feedback_widget_aktiv", "0", "integer", + "Soft-Schalter fuer das Alph-PummelHummel-Widget (kommt in Etappe 9)"), + ] + conn = get_db() + for schluessel, wert, typ, beschreibung in werte: + conn.execute( + "INSERT OR IGNORE INTO einstellung (schluessel, wert, typ, beschreibung) " + "VALUES (?, ?, ?, ?)", + (schluessel, wert, typ, beschreibung) + ) + conn.commit() + conn.close() + print(f"[seed] {len(werte)} Default-Einstellungen angelegt.") + + +def main(): + init_db() + seed_ag() + seed_drucker() + seed_einstellungen() + print("[seed] Fertig.") + + +if __name__ == "__main__": + main() diff --git a/services/__init__.py b/services/__init__.py new file mode 100644 index 0000000..93e5a03 --- /dev/null +++ b/services/__init__.py @@ -0,0 +1,9 @@ +"""Services-Package fuer 3d.lehrstun.de. + +Externe Anbindungen und abstrahierte Helfer, getrennt von HTTP-Routen. + +Module: +- bambu: Bambu-Cloud-API-Wrapper (Etappe 4/5) +- datei: File-BLOB-Handling, abstrahiert fuer spaetere S3-Migration (Etappe 6) +- auth: Login-Helfer, persistenter Lockout, bcrypt-PIN-Check +""" diff --git a/services/auth.py b/services/auth.py new file mode 100644 index 0000000..1fca5fb --- /dev/null +++ b/services/auth.py @@ -0,0 +1,156 @@ +"""Login-/Auth-Helper. + +Konventionen: +- PIN-Hashing: bcrypt +- Persistenter Login-Lockout in DB-Tabelle login_lockout (V7, siehe decisions.md#E-020) +- Decorators: @login_required (Profil-Session), @admin_required (Admin-Session), + @onboarding_l1_fertig (V2) +""" +import os +from datetime import datetime, timedelta +from functools import wraps + +import bcrypt +from flask import session, redirect, url_for, flash, request + +from database import get_db + +# Lockout-Konfiguration +LOCKOUT_FENSTER_MINUTEN = 5 # Fehlversuche werden innerhalb dieses Fensters gezaehlt +LOCKOUT_MAX_VERSUCHE = 5 # Nach so vielen Fehlversuchen: Sperre +LOCKOUT_DAUER_MINUTEN = 15 # So lange wird gesperrt +ONBOARDING_L1 = 1 # Lektions-Nummer von L1 (Sicherheits-Lektion) + + +# =========================================================================== +# PIN-Handling +# =========================================================================== + +def pin_hashen(pin: str) -> str: + """Hasht eine PIN mit bcrypt. Default cost ist OK fuer 4-stellige PINs.""" + return bcrypt.hashpw(pin.encode("utf-8"), bcrypt.gensalt()).decode("utf-8") + + +def pin_pruefen(pin: str, hash_str: str) -> bool: + """Prueft eine PIN gegen den gespeicherten Hash.""" + try: + return bcrypt.checkpw(pin.encode("utf-8"), hash_str.encode("utf-8")) + except (ValueError, TypeError): + return False + + +# =========================================================================== +# Persistenter Login-Lockout (V7, verbessert ggue Arduino-Kurs) +# =========================================================================== + +def ist_gesperrt(profil_id: int) -> tuple[bool, int]: + """Liefert (gesperrt: bool, sek_rest: int).""" + conn = get_db() + row = conn.execute( + "SELECT sperre_bis FROM login_lockout WHERE profil_id = ? " + "ORDER BY id DESC LIMIT 1", + (profil_id,) + ).fetchone() + conn.close() + if not row or not row["sperre_bis"]: + return (False, 0) + try: + sperre_bis = datetime.fromisoformat(row["sperre_bis"]) + except (ValueError, TypeError): + return (False, 0) + jetzt = datetime.now() + if sperre_bis <= jetzt: + return (False, 0) + return (True, int((sperre_bis - jetzt).total_seconds())) + + +def fehlversuch_loggen(profil_id: int) -> None: + """Loggt einen Fehlversuch. Bei zu vielen innerhalb des Fensters: setzt Sperre.""" + conn = get_db() + jetzt = datetime.now() + fenster_start = (jetzt - timedelta(minutes=LOCKOUT_FENSTER_MINUTEN)).isoformat() + + # Zaehle Fehlversuche im Fenster (ungelaufene, also ohne aktive Sperre) + versuche_im_fenster = conn.execute( + "SELECT COUNT(*) AS n FROM login_lockout " + "WHERE profil_id = ? AND letzter_versuch_am > ?", + (profil_id, fenster_start) + ).fetchone()["n"] + + sperre_bis = None + if versuche_im_fenster + 1 >= LOCKOUT_MAX_VERSUCHE: + sperre_bis = (jetzt + timedelta(minutes=LOCKOUT_DAUER_MINUTEN)).isoformat() + + conn.execute( + "INSERT INTO login_lockout (profil_id, versuche_count, sperre_bis, letzter_versuch_am) " + "VALUES (?, ?, ?, ?)", + (profil_id, versuche_im_fenster + 1, sperre_bis, jetzt.isoformat()) + ) + conn.commit() + conn.close() + + +def lockout_zuruecksetzen(profil_id: int) -> None: + """Bei erfolgreichem Login: alte Lockout-Eintraege loeschen.""" + conn = get_db() + conn.execute("DELETE FROM login_lockout WHERE profil_id = ?", (profil_id,)) + conn.commit() + conn.close() + + +# =========================================================================== +# Decorators +# =========================================================================== + +def login_required(f): + """Profil muss eingeloggt sein, sonst Redirect zum Login.""" + @wraps(f) + def wrapper(*args, **kwargs): + if not session.get("profil_id"): + flash("Bitte erst einloggen.", "error") + return redirect(url_for("profil.login")) + return f(*args, **kwargs) + return wrapper + + +def admin_required(f): + """Admin-Session noetig.""" + @wraps(f) + def wrapper(*args, **kwargs): + if not session.get("admin"): + return redirect(url_for("admin.admin_login")) + return f(*args, **kwargs) + return wrapper + + +def onboarding_l1_fertig(f): + """V2: blockiert Slot-Routes, bis L1 (Sicherheits-Lektion) abgeschlossen ist.""" + @wraps(f) + def wrapper(*args, **kwargs): + if not session.get("profil_id"): + flash("Bitte erst einloggen.", "error") + return redirect(url_for("profil.login")) + conn = get_db() + row = conn.execute( + "SELECT status FROM profil_lektion_status " + "WHERE profil_id = ? AND lektion_nummer = ?", + (session["profil_id"], ONBOARDING_L1) + ).fetchone() + conn.close() + if not row or row["status"] != "fertig": + flash( + "Du musst erst die Onboarding-Lektion L1 (Sicherheit) abschliessen, " + "bevor du Drucker reservieren darfst.", + "error" + ) + return redirect(url_for("profil.cockpit")) + return f(*args, **kwargs) + return wrapper + + +def admin_passwort_pruefen(eingegeben: str) -> bool: + """Prueft Admin-Passwort aus .env.""" + erwartet = os.environ.get("ADMIN_PASSWORT", "") + if not erwartet: + return False + return eingegeben == erwartet diff --git a/services/bambu.py b/services/bambu.py new file mode 100644 index 0000000..685b95c --- /dev/null +++ b/services/bambu.py @@ -0,0 +1,20 @@ +"""Bambu-Cloud-API-Wrapper (Stub fuer Etappe 1). + +Voll implementiert in Etappe 4/5. Siehe docs/druckerapi.md. + +Architektur-Idee: +- get_drucker_status(serial_nr) -> dict mit status, job_name, progress_pct, restzeit_min +- poll_alle_drucker() -> wird vom Hintergrund-Worker oder einem Cron-Tick aufgerufen +- Bei API-Fehler: stille Rueckgabe von None, kein Crash — Aufrufer entscheidet, + ob alter Wert weiter angezeigt wird oder "Status veraltet"-Banner kommt. +""" + + +def get_drucker_status(serial_nr: str) -> dict | None: + """Stub: gibt None zurueck, bis Etappe 5 implementiert ist.""" + return None + + +def poll_alle_drucker() -> None: + """Stub: tut nichts, bis Etappe 5 implementiert ist.""" + return None diff --git a/services/datei.py b/services/datei.py new file mode 100644 index 0000000..8b552d4 --- /dev/null +++ b/services/datei.py @@ -0,0 +1,47 @@ +"""File-BLOB-Handling, abstrahiert fuer spaetere Object-Storage-Migration. + +Aktuell: speichert direkt in SQLite-BLOBs (projekt_datei.inhalt). +Spaeter (wenn DB > 5 GB): Migration auf Hetzner Object-Storage / S3, +ohne dass aufrufende Routes geaendert werden muessen. + +Limits siehe docs/decisions.md#E-016: +- 30 MB pro Datei (App-Logik, kein DB-Constraint) +""" +from typing import Optional + +# 30 MB als App-Logik-Limit +MAX_DATEI_GROESSE = 30 * 1024 * 1024 + + +def datei_speichern( + projekt_id: int, + dateiname: str, + mime_type: str, + inhalt: bytes, + art: str, + hochgeladen_von: int, +) -> tuple[bool, str]: + """Speichert eine Datei zum Projekt. Stub fuer Etappe 1, voll in Etappe 6. + + Returns: (erfolg, fehlertext_oder_id) + """ + # Etappe 6: Groessen-Check, INSERT in projekt_datei, return id + return (False, "Noch nicht implementiert (Etappe 6)") + + +def datei_laden(datei_id: int) -> Optional[dict]: + """Laedt eine Datei (inhalt + meta). Stub fuer Etappe 1, voll in Etappe 6.""" + return None + + +def datei_loeschen(datei_id: int, durch_profil_id: int) -> bool: + """Loescht eine Datei. Stub fuer Etappe 1, voll in Etappe 6.""" + return False + + +def groesse_pruefen(inhalt: bytes) -> tuple[bool, str]: + """Universal-Helfer: prueft 30-MB-Limit.""" + if len(inhalt) > MAX_DATEI_GROESSE: + mb = len(inhalt) / 1024 / 1024 + return (False, f"Datei zu gross ({mb:.1f} MB, max. 30 MB)") + return (True, "") diff --git a/templates/404.html b/templates/404.html new file mode 100644 index 0000000..cc991e5 --- /dev/null +++ b/templates/404.html @@ -0,0 +1,9 @@ +{% extends "base.html" %} +{% block title %}Nicht gefunden — 3D-Druck-AG{% endblock %} +{% block content %} +
+

Seite nicht gefunden

+

Diese Adresse gibt es nicht. Vielleicht ein Tippfehler?

+

Zur Startseite

+
+{% endblock %} diff --git a/templates/500.html b/templates/500.html new file mode 100644 index 0000000..61122c4 --- /dev/null +++ b/templates/500.html @@ -0,0 +1,9 @@ +{% extends "base.html" %} +{% block title %}Fehler — 3D-Druck-AG{% endblock %} +{% block content %} +
+

Etwas ist schiefgegangen

+

Der Server hat einen Fehler bemerkt. Wir gucken uns das an.

+

Zur Startseite

+
+{% endblock %} diff --git a/templates/501.html b/templates/501.html new file mode 100644 index 0000000..5f35c1c --- /dev/null +++ b/templates/501.html @@ -0,0 +1,13 @@ +{% extends "base.html" %} +{% block title %}Noch nicht da — 3D-Druck-AG{% endblock %} +{% block content %} +
+

Noch nicht implementiert

+

Diese Funktion ist im Konzept vorgesehen, aber noch nicht gebaut.

+

+ Stand: Etappe 1. Welche Etappe was baut, siehst du in + docs/etappen.md. +

+

Zur Startseite

+
+{% endblock %} diff --git a/templates/admin_login.html b/templates/admin_login.html new file mode 100644 index 0000000..11074e0 --- /dev/null +++ b/templates/admin_login.html @@ -0,0 +1,22 @@ +{% extends "base.html" %} +{% block title %}Admin-Login — 3D-Druck-AG{% endblock %} +{% block content %} +
+

Admin-Login

+

+ Nur fuer Herb & Markus. +

+
+ + +
+
+{% endblock %} diff --git a/templates/admin_profile.html b/templates/admin_profile.html new file mode 100644 index 0000000..ca9c926 --- /dev/null +++ b/templates/admin_profile.html @@ -0,0 +1,89 @@ +{% extends "base.html" %} +{% block title %}Profile — Admin{% endblock %} +{% block content %} +
+

SuS-Profile

+ +
+ + +
+

Neues Profil anlegen

+
+
+ + +
+
+ + +
+
+ + +
+ +
+

+ Eine 4-stellige PIN wird automatisch erzeugt und nach dem Speichern einmal angezeigt. +

+
+ + +
+

Alle Profile

+ {% if profile %} + + + + + + + + + + + + + + {% for p in profile %} + + + + + + + + + + {% endfor %} + +
VornameTierAGStatusAngelegtAktionen
{{ tier_emoji(p.tier) }}{{ p.vorname }}{{ tier_name(p.tier) }}{{ p.ag_name or p.ag_id }}{% if p.aktiv %}aktiv{% else %}inaktiv{% endif %}{{ p.erstellt_am[:10] if p.erstellt_am else '' }} +
+ +
+
+ +
+
+ {% else %} +

Noch keine Profile. Leg oben das erste an.

+ {% endif %} +
+{% endblock %} diff --git a/templates/admin_profile_karten.html b/templates/admin_profile_karten.html new file mode 100644 index 0000000..d84015a --- /dev/null +++ b/templates/admin_profile_karten.html @@ -0,0 +1,57 @@ +{% extends "base.html" %} +{% block title %}PIN-Karten — Druckbogen{% endblock %} +{% block content %} +
+ ← Zurueck + +

+ Hinweis: Die PINs musst du handschriftlich auf den Karten ergaenzen + (sie werden nicht in der DB im Klartext gespeichert). +

+
+ +
+ {% for p in profile %} +
+
{{ tier_emoji(p.tier) }}
+
{{ p.vorname }}
+
{{ tier_name(p.tier) }}
+
PIN: __ __ __ __
+
3d.lehrstun.de
+
+ {% endfor %} +
+{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..ac8b8dc --- /dev/null +++ b/templates/base.html @@ -0,0 +1,222 @@ + + + + + + {% block title %}3D-Druck-AG{% endblock %} + + + + + +
+ {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
{{ message }}
+ {% endfor %} + {% endif %} + {% endwith %} + + {% block content %}{% endblock %} +
+ + {% block scripts %}{% endblock %} + + {# Alph-PummelHummel-Feedback-Widget — wird in einer spaeteren Etappe integriert. #} + {{ alph_widget_snippet|default('')|safe }} + + diff --git a/templates/cockpit_stub.html b/templates/cockpit_stub.html new file mode 100644 index 0000000..e19f2ec --- /dev/null +++ b/templates/cockpit_stub.html @@ -0,0 +1,24 @@ +{% extends "base.html" %} +{% block title %}Cockpit — {{ vorname }}{% endblock %} +{% block content %} +
+

+ {{ tier_emoji(tier) }} + {{ vorname }} +

+ Abmelden +
+ +
+

Cockpit-Stub — Etappe 1

+

Hier kommt in Etappe 2 das progressive Cockpit (V1):

+
    +
  • Wenn du neu bist: nur Onboarding-Karte + Check-In-Trigger
  • +
  • Nach L1 fertig: zusaetzlich Drucker-Buchung
  • +
  • Mit aktiven Projekten: zusaetzlich Projekt-/Auftrags-/Slot-Karten
  • +
+

+ Details: docs/selbstlern-loop.md und docs/etappen.md. +

+
+{% endblock %} diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..b876d4f --- /dev/null +++ b/templates/index.html @@ -0,0 +1,61 @@ +{% extends "base.html" %} +{% block title %}3D-Druck-AG{% endblock %} + +{% block content %} +
+

3D-Druck-AG

+

+ Vom fertigen Modell zum eigenen Druck — Schritt fuer Schritt. +

+
+ + + +
+

Hinweis

+

+ Diese Seite ist im Aufbau. Stand: 2026-05-23, Skelett. Inhalte werden in + den naechsten Wochen schrittweise ergaenzt — siehe + docs/kurskonzept.md. +

+
+{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/templates/login.html b/templates/login.html new file mode 100644 index 0000000..57dd64a --- /dev/null +++ b/templates/login.html @@ -0,0 +1,44 @@ +{% extends "base.html" %} +{% block title %}Login — 3D-Druck-AG{% endblock %} + +{% block content %} +
+

Wer bist du?

+

Such dein Profil und klick drauf.

+
+ +{% if profile %} + +{% else %} +
+

Noch keine Profile angelegt

+

Der Lehrer muss erst Profile anlegen. Sag Herb oder Markus Bescheid.

+

Admin-Login

+
+{% endif %} +{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/templates/login_pin.html b/templates/login_pin.html new file mode 100644 index 0000000..e468da8 --- /dev/null +++ b/templates/login_pin.html @@ -0,0 +1,42 @@ +{% extends "base.html" %} +{% block title %}PIN-Eingabe — {{ profil.vorname }}{% endblock %} + +{% block content %} +
+
{{ tier_emoji(profil.tier) }}
+

{{ profil.vorname }}

+

{{ tier_name(profil.tier) }}

+ + {% if gesperrt %} +
+ Zu viele Fehlversuche — gesperrt fuer noch ca. {{ min_rest }} Minuten. + Frag den Lehrer, wenn du nicht warten kannst. +
+

+ Anderes Profil waehlen +

+ {% else %} +
+ + +
+ + Anderes Profil +
+ {% endif %} +
+{% endblock %} diff --git a/tiere.py b/tiere.py new file mode 100644 index 0000000..77cb138 --- /dev/null +++ b/tiere.py @@ -0,0 +1,39 @@ +"""Tier-Set für Profil-Icons. + +Anders als beim Arduino-Kurs (16 Tiere = 16 Gruppen) reichen hier 10-12 für +eine AG. Die Tiere sind nicht Gruppen-gebunden, sondern werden zusammen mit +dem Vornamen als visuelles Unterscheidungs-Merkmal verwendet. + +Falls zwei SuS denselben Vornamen haben, müssen sie unterschiedliche Tiere +wählen — der UNIQUE-Constraint auf (kurs, vorname, tier) erzwingt das. +""" + +TIERE = [ + {"slug": "fuchs", "emoji": "\U0001F98A", "name": "Fuchs"}, + {"slug": "otter", "emoji": "\U0001F9A6", "name": "Otter"}, + {"slug": "igel", "emoji": "\U0001F994", "name": "Igel"}, + {"slug": "eule", "emoji": "\U0001F989", "name": "Eule"}, + {"slug": "pinguin", "emoji": "\U0001F427", "name": "Pinguin"}, + {"slug": "krake", "emoji": "\U0001F419", "name": "Krake"}, + {"slug": "biene", "emoji": "\U0001F41D", "name": "Biene"}, + {"slug": "marienkaefer","emoji": "\U0001F41E", "name": "Marienkäfer"}, + {"slug": "waschbaer", "emoji": "\U0001F99D", "name": "Waschbär"}, + {"slug": "lama", "emoji": "\U0001F999", "name": "Lama"}, + {"slug": "flamingo", "emoji": "\U0001F9A9", "name": "Flamingo"}, + {"slug": "pfau", "emoji": "\U0001F9DA", "name": "Pfau"}, +] + + +def tier_emoji(slug: str) -> str: + """Findet das Emoji zum Tier-Slug, oder Fragezeichen falls unbekannt.""" + for t in TIERE: + if t["slug"] == slug: + return t["emoji"] + return "?" + + +def tier_name(slug: str) -> str: + for t in TIERE: + if t["slug"] == slug: + return t["name"] + return slug