Initial commit: Konzept v2 + Etappe 1 (Blueprints + Login + Profile)

Schwester-Projekt zu arduino.lehrstun.de fuer die 3D-Druck-AG am
Lessing-Gymnasium. Offene jahrelange Nachmittags-AG (kein Trimester),
8-10 Einzel-SuS mit optionalen 2er-Paarungen, 4 Bambu-Lab-Drucker
(3 geschlossen + 1 A1 mini).

Konzept v2 (Reality-Check-Iteration 2026-05-24):
- Onboarding-Pfad statt Pflicht-Lektionen (L1-L3 selbst-zertifiziert)
- Selbstlern-Loop als Kern: SuS-Projekte wachsen die interne Bibliothek
- Drill-Down-Lehrer-Dashboard (Wer ist hier / Wartet Review / Haengt fest)
- Cockpit progressiv: zeigt nur was zum Status passt
- Bambu-Cloud-API fuer Drucker-Live-Status (fragil + Fallback manuell)
- Three.js-Vorschau fuer Cover-aus-3D-Ansicht (Etappe 6)
- Vollstaendige Doku als Obsidian-Vault (24 Markdown-Dateien)
- Entscheidungen E-001 bis E-022 in docs/decisions.md

Etappe 1 lauffaehig (~1800 Zeilen Code):
- Blueprint-Struktur (V8): routes/oeffentlich+profil+admin+api,
  services/auth+bambu+datei
- Komplettes Schema in database.py (14 Tabellen, idempotent)
- Login mit bcrypt + persistentem Lockout in DB (V7, verbessert ggue
  Arduino-Kurs der In-Memory-Dict nutzt)
- Admin-Login + Profile-CRUD + PIN-Reset + PIN-Karten-Druckbogen
- Inline-Edit-Endpunkt mit Whitelist + Audit-Log
- Seeds: AG lessing-3d-ag + 4 Drucker + Default-Einstellungen
- Smoke-Test bestanden (Login, Profil-Anlage, Lockout-Logging)

Nicht im Repo (.gitignore): .env, *.db, venv/, .obsidian/workspace.json
This commit is contained in:
2026-05-24 18:56:59 +02:00
commit 4658d4931a
59 changed files with 4653 additions and 0 deletions

15
.env.beispiel Normal file
View File

@@ -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

31
.gitignore vendored Normal file
View File

@@ -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/

8
.obsidian/app.json vendored Normal file
View File

@@ -0,0 +1,8 @@
{
"showLineNumber": true,
"newFileLocation": "current",
"attachmentFolderPath": "anhaenge",
"alwaysUpdateLinks": true,
"useMarkdownLinks": false,
"newLinkFormat": "shortest"
}

1
.obsidian/appearance.json vendored Normal file
View File

@@ -0,0 +1 @@
{}

1
.obsidian/community-plugins.json vendored Normal file
View File

@@ -0,0 +1 @@
[]

34
.obsidian/core-plugins.json vendored Normal file
View File

@@ -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
}

27
.obsidian/graph.json vendored Normal file
View File

@@ -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
}

38
CHANGELOG.md Normal file
View File

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

83
CLAUDE.md Normal file
View File

@@ -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/<id>` 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)

142
COCKPIT.md Normal file
View File

@@ -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<br/>+ freie Module]
S[Software-Sektion]
P[Projekt-Katalog<br/>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<br/>im Lebenszyklus]
AB[Abschluss-Checkliste]
SL[Slot-Reservierungen<br/>nach L1 fertig]
end
subgraph Admin["Admin (Herb + Markus)"]
AD[Drill-Down-Dashboard]
RQ[Review-Queue<br/>+ 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<br/>auf Schul-PCs]
BH[Bambu Handy App]
PR[Printables /<br/>MakerWorld]
TC[TinkerCAD]
OS[OpenSCAD]
ZSL[ZSL Baden-Württemberg<br/>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.

56
README.md Normal file
View File

@@ -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 <repo-url> 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).

90
app.py Normal file
View File

@@ -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)

301
database.py Normal file
View File

@@ -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()

111
docs/architektur.md Normal file
View File

@@ -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<br/>alle ~30 s]
FL --- DB
POLL --- DB
end
subgraph Schule["Schule"]
BS_PC[Bambu Studio<br/>auf Schul-PCs]
DRU[4 Bambu-Drucker]
BS_PC -.WLAN.-> DRU
end
subgraph Cloud["Bambu Cloud"]
BAMBU[api.bambulab.com<br/>+ 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]]

249
docs/datenmodell.md Normal file
View File

@@ -0,0 +1,249 @@
---
type: konzept-modul
projekt: 3d.lehrstun.de
stand: 2026-05-24
tags: [konzept, datenmodell, sql]
---
# Datenmodell
> SQLite-Schema. Live-Schema im Code: [`database.py`](../database.py).
> Architektur-Übersicht: [[architektur]]. Routes: [[routen]]. Selbstlern-Loop: [[selbstlern-loop]].
## ERD (Entity-Relationship)
```mermaid
erDiagram
AG_MITGLIED ||--o{ PROFIL : "umfasst"
PROFIL ||--o{ PROJEKT : "besitzt (Einzel)"
PROFIL ||--o{ DRUCKAUFTRAG : "stellt ein"
PROFIL ||--o{ SLOT : "reserviert"
PROFIL ||--o{ CHECK_IN : "macht pro Besuch"
PROFIL ||--o{ PROFIL_LEKTION_STATUS : "Fortschritt"
PROFIL ||--o{ LOGIN_LOCKOUT : "Sperrhistorie"
PROFIL ||--o{ PAARUNG : "Profil A"
PROFIL ||--o{ PAARUNG : "Profil B"
PAARUNG ||--o{ PROJEKT : "besitzt (Gruppe)"
DRUCKER ||--|| DRUCKER_STATUS : "hat Live-Status"
DRUCKER ||--o{ SLOT : "wird reserviert"
DRUCKER ||--o{ DRUCKAUFTRAG : "druckt"
PROJEKT ||--o{ DRUCKAUFTRAG : "gehört zu"
PROJEKT ||--o{ PROJEKT_DATEI : "hat Files"
PROJEKT ||--o{ PROJEKT_TAG : "hat Tags"
PROJEKT ||--o{ PROJEKT_RUECKFRAGE : "Lehrer-Notizen"
DRUCKAUFTRAG ||--o{ SLOT : "passt in"
LEKTION ||--o{ PROFIL_LEKTION_STATUS : "Fortschritt pro SuS"
AG_MITGLIED { TEXT id_PK "z.B. lessing-3d-ag" }
PROFIL { INT id_PK TEXT vorname TEXT tier TEXT pin_hash TEXT ag_id }
PAARUNG { INT id_PK INT profil_a INT profil_b INT aktiv }
PROJEKT { INT id_PK TEXT titel TEXT status_lebenszyklus TEXT selbsteinschaetzung TEXT freigabe_status }
PROJEKT_DATEI { INT id_PK TEXT dateiname TEXT art BLOB inhalt }
PROJEKT_TAG { INT projekt_id_FK TEXT tag }
PROJEKT_RUECKFRAGE { INT id_PK INT projekt_id TEXT text DATETIME erstellt_am TEXT status }
CHECK_IN { INT id_PK DATE besuch_datum TEXT modus TEXT freitext }
PROFIL_LEKTION_STATUS { INT profil_id INT lektion_nummer TEXT status }
LOGIN_LOCKOUT { INT id_PK INT profil_id INT versuche_count DATETIME sperre_bis }
DRUCKER { INT id_PK TEXT name TEXT modell TEXT serial_nr }
DRUCKER_STATUS { INT drucker_id_PK TEXT status INT restzeit_min }
DRUCKAUFTRAG { INT id_PK TEXT titel TEXT status }
SLOT { INT id_PK DATETIME start_zeit DATETIME ende_zeit INT prioritaet }
LEKTION { INT id_PK INT nummer TEXT titel INT onboarding }
```
## Tabellen-Übersicht
| Tabelle | Zweck | Querverweis |
|---|---|---|
| `ag_mitgliedschaft` | AG als dauerhafte Entität (statt Trimester-Kurs) | [[../decisions#E-012]] |
| `profil` | Einzel-SuS mit Vorname + Tier + PIN | [[../decisions#E-003]] |
| `paarung` | Optionale 2er-Zusammenschlüsse | [[../decisions#E-002]] |
| `profil_lektion_status` | Onboarding-Fortschritt pro SuS (offen/läuft/fertig) | [[../decisions#E-013]], [[../decisions#E-019]] |
| `login_lockout` | Persistenter Login-Lockout (V7, überlebt Restart) | [[../decisions#E-020]] |
| `projekt` | Eigene Projekte mit vollem Lebenszyklus | [[selbstlern-loop]] |
| `projekt_datei` | STL / 3MF / Fotos / Sonstiges pro Projekt | [[../decisions#E-016]] |
| `projekt_tag` | Tags für Such- und Vernetzungs-Logik | [[selbstlern-loop]] |
| `projekt_rueckfrage` | Lehrer-Rückfragen + Antworten | [[../decisions#E-017]] |
| `check_in` | „Was machst du heute?"-Abfrage pro Besuchstag | [[../decisions#E-021]] |
| `drucker` | 4 Bambu-Drucker als Entitäten | [[druckerapi]] |
| `drucker_status` | Live-Status, vom Poll-Worker aktualisiert | [[druckerapi]] |
| `druckauftrag` | Was die SuS drucken wollen | [[slotsystem]] |
| `slot` | Reservierungen auf Druckern | [[slotsystem]] |
| `lektion` | Onboarding-Lektionen + freie Module | [[lektion-L1-drucker-kennenlernen]] |
| `einstellung` | Konfigurierbare Werte (Inline-Edit) | — |
| `aenderung_log` | Audit-Trail für Inline-Edit + Lehrer-Aktionen | — |
## Wichtige neue Tabellen (gegenüber Skelett-Stand)
### `ag_mitgliedschaft` — ersetzt `kurs`
Die AG ist dauerhaft, nicht trimester-basiert. Eine einzelne Zeile (`id='lessing-3d-ag'`) reicht. Tabelle bleibt, weil später evtl. mehrere Schulen oder ein Reset möglich.
```sql
CREATE TABLE ag_mitgliedschaft (
id TEXT PRIMARY KEY, -- 'lessing-3d-ag'
name TEXT NOT NULL, -- '3D-Druck-AG Lessing-Gymnasium'
start_datum DATE,
status TEXT DEFAULT 'aktiv' -- 'aktiv'|'archiviert'
);
```
### `profil_lektion_status` — Onboarding-Fortschritt
Analog zu `gruppe_lektion_status` im Arduino-Kurs (`app.py:305-309`).
```sql
CREATE TABLE profil_lektion_status (
profil_id INTEGER NOT NULL,
lektion_nummer INTEGER NOT NULL,
status TEXT DEFAULT 'offen', -- 'offen'|'laeuft'|'fertig'
geaendert_am DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (profil_id, lektion_nummer),
FOREIGN KEY (profil_id) REFERENCES profil(id) ON DELETE CASCADE
);
```
### `login_lockout` — V7, persistenter Lockout
Verbesserung gegenüber Arduino-Kurs (In-Memory-Dict, bei Restart verloren).
```sql
CREATE TABLE login_lockout (
id INTEGER PRIMARY KEY AUTOINCREMENT,
profil_id INTEGER NOT NULL,
versuche_count INTEGER DEFAULT 1,
sperre_bis DATETIME, -- NULL = nicht gesperrt, nur gezählt
letzter_versuch_am DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (profil_id) REFERENCES profil(id) ON DELETE CASCADE
);
CREATE INDEX idx_lockout_profil ON login_lockout(profil_id);
```
### `projekt` — erweitert um Lebenszyklus
```sql
CREATE TABLE projekt (
id INTEGER PRIMARY KEY AUTOINCREMENT,
profil_id INTEGER,
paarung_id INTEGER,
titel TEXT NOT NULL,
beschreibung_md TEXT,
-- Lebenszyklus
status_lebenszyklus TEXT DEFAULT 'in_arbeit', -- 'in_arbeit'|'abgeschlossen'
abgeschlossen_am DATETIME,
selbsteinschaetzung TEXT, -- 'zufrieden'|'mittel'|'gelernt' (E-015)
was_gelernt TEXT, -- optionaler Freitext
-- Freigabe-Workflow
freigabe_status TEXT DEFAULT 'in_arbeit', -- 'in_arbeit'|'wartet_auf_freigabe'|'freigegeben'|'rueckfrage'
freigegeben_am DATETIME,
freigegeben_von INTEGER, -- Lehrer-Profil-ID, NULL=Admin direkt
-- Sichtbarkeit
sichtbar_in_ag INTEGER DEFAULT 1, -- SuS kann das ausschalten
-- Referenz auf Vorlage (wenn aus Katalog gewählt)
vorlage_projekt_id INTEGER, -- "ich mache sowas wie Mia"
geaendert_am DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (profil_id) REFERENCES profil(id) ON DELETE CASCADE,
FOREIGN KEY (paarung_id) REFERENCES paarung(id) ON DELETE CASCADE,
FOREIGN KEY (vorlage_projekt_id) REFERENCES projekt(id) ON DELETE SET NULL,
CHECK ((profil_id IS NOT NULL) <> (paarung_id IS NOT NULL))
);
CREATE INDEX idx_projekt_freigabe ON projekt(freigabe_status);
CREATE INDEX idx_projekt_sichtbar ON projekt(sichtbar_in_ag, freigabe_status);
```
### `projekt_datei` — STL / 3MF / Fotos
```sql
CREATE TABLE projekt_datei (
id INTEGER PRIMARY KEY AUTOINCREMENT,
projekt_id INTEGER NOT NULL,
dateiname TEXT NOT NULL,
mime_type TEXT,
groesse_bytes INTEGER,
inhalt BLOB,
art TEXT NOT NULL, -- 'cover_foto'|'stl'|'3mf'|'foto'|'sonstiges'
hochgeladen_am DATETIME DEFAULT CURRENT_TIMESTAMP,
hochgeladen_von INTEGER, -- profil_id (SuS oder Lehrer)
FOREIGN KEY (projekt_id) REFERENCES projekt(id) ON DELETE CASCADE,
FOREIGN KEY (hochgeladen_von) REFERENCES profil(id) ON DELETE SET NULL
);
CREATE INDEX idx_datei_projekt ON projekt_datei(projekt_id);
```
**Limit:** 30 MB pro Datei (App-Logik, nicht DB-Constraint).
**Abstraktion:** Zugriff über `services/datei.py` (siehe [[../decisions#E-016]]).
### `projekt_tag` — für Suche und Vernetzung
```sql
CREATE TABLE projekt_tag (
projekt_id INTEGER NOT NULL,
tag TEXT NOT NULL,
PRIMARY KEY (projekt_id, tag),
FOREIGN KEY (projekt_id) REFERENCES projekt(id) ON DELETE CASCADE
);
CREATE INDEX idx_tag ON projekt_tag(tag);
```
Tag-Pool ist offen (SuS gibt eigene Tags ein), aber Admin kuratiert kanonische Liste via `einstellung.tag_vorschlaege`. Beispiele: `schluesselanhaenger`, `organizer`, `werkzeug`, `spiel`, `dekoration`, `ersatzteil`, `geschenk`, `figur`.
### `projekt_rueckfrage` — Lehrer-Rückfrage-Notizen
```sql
CREATE TABLE projekt_rueckfrage (
id INTEGER PRIMARY KEY AUTOINCREMENT,
projekt_id INTEGER NOT NULL,
von_profil_id INTEGER NOT NULL, -- Lehrer
text TEXT NOT NULL,
erstellt_am DATETIME DEFAULT CURRENT_TIMESTAMP,
status TEXT DEFAULT 'offen', -- 'offen'|'erledigt'
erledigt_am DATETIME,
FOREIGN KEY (projekt_id) REFERENCES projekt(id) ON DELETE CASCADE,
FOREIGN KEY (von_profil_id) REFERENCES profil(id) ON DELETE SET NULL
);
```
Wird im Cockpit als gelbes Banner angezeigt (V6).
### `check_in` — angepasst an V3 und E-021
```sql
CREATE TABLE check_in (
id INTEGER PRIMARY KEY AUTOINCREMENT,
profil_id INTEGER NOT NULL,
besuch_datum DATE NOT NULL,
modus TEXT NOT NULL, -- 'weiter_wie_bisher'|'neues_projekt'|'onboarding'|'drucken'|'helfen'|'sonstiges'
freitext TEXT, -- optional
erstellt_am DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE(profil_id, besuch_datum),
FOREIGN KEY (profil_id) REFERENCES profil(id) ON DELETE CASCADE
);
```
**Wichtig:** „weiter_wie_bisher" ist die Default-Option und kann mit einem Klick gewählt werden (kein Freitext nötig). Erst bei „neues_projekt" oder „sonstiges" wird der Freitext sinnvoll.
## Wichtige Constraints
- `profil` UNIQUE(`ag_id`, `vorname`, `tier`) — zwei SuS mit demselben Vornamen müssen unterschiedliche Tiere wählen.
- `paarung` CHECK(`profil_a < profil_b`) + UNIQUE-Index für aktive Paarungen.
- `projekt` CHECK — entweder `profil_id` ODER `paarung_id`, nicht beides.
- `check_in` UNIQUE(`profil_id`, `besuch_datum`) — eine Abfrage pro SuS pro Tag.
- `slot` — überlappungsfreie Reservierungen werden in der App-Logik geprüft (Konflikt-Query siehe [[slotsystem]]).
- `projekt_datei` — 30-MB-Limit als App-Logik (kein DB-Constraint).
## Migrations-Strategie
- Initiales Schema in [`database.py`](../database.py) via `init_db()` (idempotent — `CREATE TABLE IF NOT EXISTS`).
- Spätere Änderungen als datierte Skripte in `migrations/YYYY-MM-DD_*.py`
Konvention wie bei `arduino.lehrstun.de`.
- Vor Migrationen auf der Live-Site immer Service stoppen (DB-Lock-Vermeidung).
## Performance-Hinweise
- Indexes auf häufig gefilterte Spalten: `idx_projekt_freigabe`, `idx_projekt_sichtbar`, `idx_tag`, `idx_lockout_profil`, `idx_datei_projekt`.
- Cockpit-Queries für „eigene Projekte / Aufträge / Slots" sind über `profil_id` indexiert (FK-Index ist in SQLite automatisch).
- BLOB-Größen bleiben durch 30-MB-Limit überschaubar; bei 5 GB+ DB ggf. Migration auf Object-Storage (siehe [[../decisions#E-016]]).

166
docs/decisions.md Normal file
View File

@@ -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:**
```

96
docs/druckerapi.md Normal file
View File

@@ -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<br/>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/<id>/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]]

195
docs/etappen.md Normal file
View File

@@ -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/<id>` + 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/<nr>` mit Markdown-Rendering + Inline-Edit (für Lehrer)
- [ ] `/cockpit/lektion/<nr>/status` (selbst-zertifizierend, [[../decisions#E-019]])
- [ ] `/software` + `/software/<slug>` (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/<id>/edit` während `in_arbeit`
- [ ] Datei-Upload-UI (mit Drag&Drop, 30 MB-Limit)
- [ ] `/cockpit/projekt/<id>/abschluss` mit V4-Checkliste (3 Pflicht-Felder)
**Projekt-Katalog:**
- [ ] `/projekte` mit Filter (Tag, Schwierigkeit, Erfolg, Macher)
- [ ] `/projekte/<id>` 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/<id>` 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/<id>/rueckfrage/<rid>/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/<id>/prioritaet` Lehrer-Priorisierung
- [ ] `/admin/slot/<id>/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]]

66
docs/glossar.md Normal file
View File

@@ -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]]

308
docs/kurskonzept.md Normal file
View File

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

147
docs/lehrer-dashboard.md Normal file
View File

@@ -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/<id>` ö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]]

View File

@@ -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]]

View File

@@ -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]]

View File

@@ -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]]

99
docs/offene-fragen.md Normal file
View File

@@ -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]]

126
docs/routen.md Normal file
View File

@@ -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/<nr>` | Einzelne Lektion | 3 |
| GET | `/software` | Software-Tools | 3 |
| GET | `/software/<slug>` | Software-Detail | 3 |
| GET | `/projekte` | **Projekt-Katalog** (mit Filter: tag, level, erfolg, macher) | 6 |
| GET | `/projekte/<id>` | Projekt-Detail (Cover, Beschreibung, Files, Bewertung, Macher) | 6 |
| GET | `/projekte/datei/<id>` | 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/<profil_id>` | 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/<nr>/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/<id>/edit` | Projekt-Details anpassen (während `in_arbeit`) | 6 |
| GET | `/cockpit/projekt/<id>/abschluss` | **Abschluss-Checkliste** (V4: 3 Pflicht-Felder) | 6 |
| POST | `/cockpit/projekt/<id>/abschluss` | Abschluss absenden → Status `wartet_auf_freigabe` | 6 |
| POST | `/cockpit/projekt/<id>/datei` | Datei hochladen (30 MB Limit) | 6 |
| POST | `/cockpit/projekt/<id>/datei/<dateiid>/loeschen` | Eigene Datei entfernen | 6 |
| POST | `/cockpit/projekt/<id>/rueckfrage/<rid>/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/<id>/stornieren` | Slot zurückgeben | 6 |
| POST | `/cockpit/paarung/anfragen/<andere_id>` | 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/<id>` | Detail-Review mit Three.js-Vorschau + Edit-Möglichkeit | 6 |
| POST | `/admin/projekt/<id>/freigeben` | Projekt freigeben (mit optionaler Eigenbearbeitung) | 6 |
| POST | `/admin/projekt/<id>/rueckfrage` | Rückfrage stellen → Status `rueckfrage` + Notiz im Cockpit | 6 |
| POST | `/admin/projekt/<id>/loeschen` | **Hartes Löschen** mit Bestätigung (Audit-Log behält Datum + Lehrer + SuS-Name) | 6 |
| POST | `/admin/projekt/<id>/datei-aus-3d` | Three.js-Screenshot als Cover speichern (von Detail-Review aus) | 6 |
| GET | `/admin/druckauftraege` | Queue + Freigabe-UI | 7 |
| POST | `/admin/druckauftrag/<id>/freigeben` | Lange Drucke explizit freigeben | 7 |
| POST | `/admin/slot/<id>/prioritaet` | Slot-Priorität setzen | 6 |
| POST | `/admin/slot/<id>/verschieben` | Slot bei Drucker-Ausfall auf anderen Drucker | 6 |
| GET | `/admin/drucker` | Drucker-Stammdaten + manueller Status | 5 |
| POST | `/admin/drucker/<id>/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.

176
docs/selbstlern-loop.md Normal file
View File

@@ -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/<id>/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/<id>` (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/<id>`:**
- 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)

58
docs/sicherheit.md Normal file
View File

@@ -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]]

100
docs/slotsystem.md Normal file
View File

@@ -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]]

View File

@@ -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)

View File

@@ -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]]

View File

@@ -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)

61
docs/software-openscad.md Normal file
View File

@@ -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]]

View File

@@ -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]]

View File

@@ -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]]

21
migrations/README.md Normal file
View File

@@ -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
```

6
requirements.txt Normal file
View File

@@ -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

10
routes/__init__.py Normal file
View File

@@ -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
"""

259
routes/admin.py Normal file
View File

@@ -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/<int:profil_id>/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/<int:profil_id>/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

27
routes/api.py Normal file
View File

@@ -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)

56
routes/oeffentlich.py Normal file
View File

@@ -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/<int:nummer>")
def lektion_detail(nummer):
abort(501)
@bp.route("/software")
def software():
abort(501)
@bp.route("/software/<slug>")
def software_detail(slug):
abort(501)
@bp.route("/projekte")
def projekte():
abort(501)
@bp.route("/projekte/<int:projekt_id>")
def projekt_detail(projekt_id):
abort(501)
@bp.route("/troubleshooting")
def troubleshooting():
abort(501)
@bp.route("/drucker")
def drucker():
abort(501)

131
routes/profil.py Normal file
View File

@@ -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/<int:profil_id>", 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/<int:nummer>/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)

86
seed.py Normal file
View File

@@ -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()

9
services/__init__.py Normal file
View File

@@ -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
"""

156
services/auth.py Normal file
View File

@@ -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

20
services/bambu.py Normal file
View File

@@ -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

47
services/datei.py Normal file
View File

@@ -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, "")

9
templates/404.html Normal file
View File

@@ -0,0 +1,9 @@
{% extends "base.html" %}
{% block title %}Nicht gefunden &mdash; 3D-Druck-AG{% endblock %}
{% block content %}
<div class="card" style="text-align: center; max-width: 600px; margin: 3rem auto;">
<h1>Seite nicht gefunden</h1>
<p>Diese Adresse gibt es nicht. Vielleicht ein Tippfehler?</p>
<p style="margin-top: 1rem;"><a href="/" class="btn btn-primary">Zur Startseite</a></p>
</div>
{% endblock %}

9
templates/500.html Normal file
View File

@@ -0,0 +1,9 @@
{% extends "base.html" %}
{% block title %}Fehler &mdash; 3D-Druck-AG{% endblock %}
{% block content %}
<div class="card" style="text-align: center; max-width: 600px; margin: 3rem auto;">
<h1>Etwas ist schiefgegangen</h1>
<p>Der Server hat einen Fehler bemerkt. Wir gucken uns das an.</p>
<p style="margin-top: 1rem;"><a href="/" class="btn btn-primary">Zur Startseite</a></p>
</div>
{% endblock %}

13
templates/501.html Normal file
View File

@@ -0,0 +1,13 @@
{% extends "base.html" %}
{% block title %}Noch nicht da &mdash; 3D-Druck-AG{% endblock %}
{% block content %}
<div class="card" style="text-align: center; max-width: 700px; margin: 3rem auto;">
<h1>Noch nicht implementiert</h1>
<p>Diese Funktion ist im Konzept vorgesehen, aber noch nicht gebaut.</p>
<p style="color: var(--text-light); font-size: 0.9rem; margin-top: 1rem;">
Stand: Etappe 1. Welche Etappe was baut, siehst du in
<code>docs/etappen.md</code>.
</p>
<p style="margin-top: 1rem;"><a href="/" class="btn btn-primary">Zur Startseite</a></p>
</div>
{% endblock %}

View File

@@ -0,0 +1,22 @@
{% extends "base.html" %}
{% block title %}Admin-Login &mdash; 3D-Druck-AG{% endblock %}
{% block content %}
<div class="card" style="max-width: 400px; margin: 3rem auto; text-align: center;">
<h1>Admin-Login</h1>
<p style="color: var(--text-light); margin-bottom: 1.5rem;">
Nur fuer Herb &amp; Markus.
</p>
<form method="post">
<input
type="password"
name="passwort"
placeholder="Passwort"
autofocus
style="font-size: 1rem; padding: 0.6rem; width: 100%;
border: 1px solid var(--border); border-radius: 6px;
margin-bottom: 1rem;"
>
<button type="submit" class="btn btn-primary" style="width: 100%;">Login</button>
</form>
</div>
{% endblock %}

View File

@@ -0,0 +1,89 @@
{% extends "base.html" %}
{% block title %}Profile &mdash; Admin{% endblock %}
{% block content %}
<div class="section-header">
<h1>SuS-Profile</h1>
<div>
<a href="{{ url_for('admin.profile_karten') }}" class="btn btn-secondary">PIN-Karten drucken</a>
<a href="{{ url_for('admin.admin_logout') }}" class="btn btn-secondary">Admin-Logout</a>
</div>
</div>
<!-- Neues Profil -->
<div class="card">
<h2>Neues Profil anlegen</h2>
<form method="post" action="{{ url_for('admin.profil_neu') }}"
style="display: grid; grid-template-columns: 1fr 1fr 1fr auto; gap: 0.6rem; align-items: end;">
<div>
<label style="display:block; font-size: 0.85rem; color: var(--text-light);">Vorname</label>
<input type="text" name="vorname" required style="padding: 0.5rem; width: 100%; border: 1px solid var(--border); border-radius: 4px;">
</div>
<div>
<label style="display:block; font-size: 0.85rem; color: var(--text-light);">Tier-Icon</label>
<select name="tier" required style="padding: 0.5rem; width: 100%; border: 1px solid var(--border); border-radius: 4px;">
<option value="">— waehlen —</option>
{% for t in tiere %}
<option value="{{ t.slug }}">{{ t.emoji }} {{ t.name }}</option>
{% endfor %}
</select>
</div>
<div>
<label style="display:block; font-size: 0.85rem; color: var(--text-light);">AG</label>
<select name="ag_id" required style="padding: 0.5rem; width: 100%; border: 1px solid var(--border); border-radius: 4px;">
{% for ag in ags %}
<option value="{{ ag.id }}">{{ ag.name }}</option>
{% endfor %}
</select>
</div>
<button class="btn btn-primary">Anlegen + PIN</button>
</form>
<p style="margin-top: 0.8rem; font-size: 0.85rem; color: var(--text-light);">
Eine 4-stellige PIN wird automatisch erzeugt und nach dem Speichern <strong>einmal</strong> angezeigt.
</p>
</div>
<!-- Liste -->
<div class="card">
<h2>Alle Profile</h2>
{% if profile %}
<table>
<thead>
<tr>
<th></th>
<th>Vorname</th>
<th>Tier</th>
<th>AG</th>
<th>Status</th>
<th>Angelegt</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
{% for p in profile %}
<tr style="{% if not p.aktiv %}opacity: 0.5;{% endif %}">
<td style="font-size: 1.5rem;">{{ tier_emoji(p.tier) }}</td>
<td><strong>{{ p.vorname }}</strong></td>
<td>{{ tier_name(p.tier) }}</td>
<td>{{ p.ag_name or p.ag_id }}</td>
<td>{% if p.aktiv %}<span class="badge badge-anfaenger">aktiv</span>{% else %}<span class="badge">inaktiv</span>{% endif %}</td>
<td style="font-size: 0.85rem; color: var(--text-light);">{{ p.erstellt_am[:10] if p.erstellt_am else '' }}</td>
<td>
<form method="post" action="{{ url_for('admin.profil_pin_reset', profil_id=p.id) }}" style="display:inline;"
onsubmit="return confirm('Neue PIN fuer {{ p.vorname }} generieren?')">
<button class="btn btn-secondary" style="padding: 0.3rem 0.6rem; font-size: 0.85rem;">PIN neu</button>
</form>
<form method="post" action="{{ url_for('admin.profil_aktiv_toggle', profil_id=p.id) }}" style="display:inline;">
<button class="btn btn-secondary" style="padding: 0.3rem 0.6rem; font-size: 0.85rem;">
{% if p.aktiv %}deaktivieren{% else %}aktivieren{% endif %}
</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p style="color: var(--text-light);">Noch keine Profile. Leg oben das erste an.</p>
{% endif %}
</div>
{% endblock %}

View File

@@ -0,0 +1,57 @@
{% extends "base.html" %}
{% block title %}PIN-Karten &mdash; Druckbogen{% endblock %}
{% block content %}
<div class="no-print" style="margin-bottom: 1rem;">
<a href="{{ url_for('admin.profile_liste') }}" class="btn btn-secondary">&larr; Zurueck</a>
<button class="btn btn-primary" onclick="window.print()">Drucken</button>
<p style="margin-top: 0.5rem; color: var(--text-light); font-size: 0.85rem;">
Hinweis: Die PINs musst du handschriftlich auf den Karten ergaenzen
(sie werden nicht in der DB im Klartext gespeichert).
</p>
</div>
<div class="karten-bogen">
{% for p in profile %}
<div class="pin-karte">
<div class="emoji">{{ tier_emoji(p.tier) }}</div>
<div class="vorname">{{ p.vorname }}</div>
<div class="tier-name">{{ tier_name(p.tier) }}</div>
<div class="pin-zeile">PIN: __ __ __ __</div>
<div class="url">3d.lehrstun.de</div>
</div>
{% endfor %}
</div>
{% endblock %}
{% block scripts %}
<style>
.karten-bogen {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.8rem;
max-width: 800px;
margin: 0 auto;
}
.pin-karte {
border: 2px dashed #999;
border-radius: 8px;
padding: 1rem;
text-align: center;
page-break-inside: avoid;
background: white;
}
.pin-karte .emoji { font-size: 3rem; }
.pin-karte .vorname { font-size: 1.3rem; font-weight: 700; margin-top: 0.2rem; }
.pin-karte .tier-name { font-size: 0.85rem; color: #666; margin-bottom: 0.8rem; }
.pin-karte .pin-zeile { font-family: monospace; font-size: 1.2rem; letter-spacing: 0.3rem; margin: 0.6rem 0; }
.pin-karte .url { font-size: 0.75rem; color: #888; margin-top: 0.5rem; }
@media print {
.no-print { display: none !important; }
nav, footer { display: none !important; }
main { padding: 0 !important; }
body { background: white !important; }
.pin-karte { break-inside: avoid; }
}
</style>
{% endblock %}

222
templates/base.html Normal file
View File

@@ -0,0 +1,222 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}3D-Druck-AG{% endblock %}</title>
<style>
:root {
--primary: #00979D; /* Teal — identisch zum Arduino-Kurs fuer einheitliches Wirken */
--primary-dark: #007A80;
--secondary: #E67E22;
--bg: #F8F9FA;
--card-bg: #FFFFFF;
--text: #2C3E50;
--text-light: #7F8C8D;
--border: #DEE2E6;
--code-bg: #2D2D2D;
--success: #27AE60;
--warning: #F39C12;
--danger: #E74C3C;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--bg);
color: var(--text);
line-height: 1.6;
}
/* Navigation */
nav {
background: var(--primary);
color: white;
padding: 0;
position: sticky;
top: 0;
z-index: 100;
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
}
nav .nav-inner {
max-width: 1200px;
margin: 0 auto;
display: flex;
align-items: center;
gap: 0;
flex-wrap: wrap;
}
nav .brand {
font-size: 1.2rem;
font-weight: 700;
padding: 0.8rem 1.2rem;
text-decoration: none;
color: white;
white-space: nowrap;
}
nav a {
color: rgba(255,255,255,0.85);
text-decoration: none;
padding: 0.8rem 1rem;
font-size: 0.9rem;
transition: background 0.2s;
}
nav a:hover, nav a.active {
background: rgba(255,255,255,0.15);
color: white;
}
/* Main */
main {
max-width: 1200px;
margin: 0 auto;
padding: 1.5rem;
}
/* Karten */
.card {
background: var(--card-bg);
border-radius: 8px;
padding: 1.5rem;
margin-bottom: 1rem;
border: 1px solid var(--border);
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
}
.card h2, .card h3 {
margin-bottom: 0.8rem;
color: var(--primary-dark);
}
/* Grid */
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1rem;
}
/* Badges */
.badge {
display: inline-block;
padding: 0.2rem 0.6rem;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
}
.badge-pflicht { background: #FDEBD0; color: #BA4A00; }
.badge-frei { background: #D5F5E3; color: #1E8449; }
.badge-anfaenger { background: #D5F5E3; color: #1E8449; }
.badge-mittel { background: #FEF9E7; color: #B7950B; }
.badge-fortgeschritten { background: #FADBD8; color: #943126; }
/* Buttons */
.btn {
display: inline-block;
padding: 0.5rem 1.2rem;
border-radius: 6px;
text-decoration: none;
font-size: 0.9rem;
font-weight: 500;
border: none;
cursor: pointer;
transition: background 0.2s;
}
.btn-primary { background: var(--primary); color: white; }
.btn-primary:hover { background: var(--primary-dark); }
.btn-secondary { background: #ECF0F1; color: var(--text); }
.btn-secondary:hover { background: #D5DBDB; }
/* Tabellen */
table { width: 100%; border-collapse: collapse; margin: 1rem 0; }
th, td { padding: 0.6rem 0.8rem; text-align: left; border-bottom: 1px solid var(--border); }
th { background: #F2F3F4; font-weight: 600; font-size: 0.85rem; }
/* Code */
pre {
background: var(--code-bg);
color: #F8F8F2;
padding: 1.2rem;
border-radius: 8px;
overflow-x: auto;
font-size: 0.85rem;
line-height: 1.5;
margin: 0.5rem 0;
}
code { font-family: 'Fira Code', 'Consolas', 'Monaco', monospace; }
/* Flash Messages */
.flash {
padding: 0.8rem 1.2rem;
border-radius: 6px;
margin-bottom: 1rem;
}
.flash-success { background: #D5F5E3; color: #1E8449; border: 1px solid #A9DFBF; }
.flash-error { background: #FADBD8; color: #943126; border: 1px solid #F5B7B1; }
h1 { font-size: 1.6rem; margin-bottom: 1rem; }
h2 { font-size: 1.3rem; }
/* Drucker-Status-spezifisch */
.drucker-kachel {
background: var(--card-bg);
border-radius: 8px;
padding: 1rem;
border: 1px solid var(--border);
border-left: 4px solid var(--text-light);
}
.drucker-kachel.drucker-printing { border-left-color: var(--success); }
.drucker-kachel.drucker-idle { border-left-color: var(--text-light); }
.drucker-kachel.drucker-failed { border-left-color: var(--danger); }
.drucker-kachel.drucker-offline { border-left-color: var(--warning); }
/* Responsive */
@media (max-width: 768px) {
.grid { grid-template-columns: 1fr; }
nav .nav-inner { flex-direction: column; align-items: stretch; }
}
</style>
</head>
<body>
<nav>
<div class="nav-inner">
<a href="/" class="brand">3D-Druck-AG</a>
<a href="/lektionen" {% if request.path.startswith('/lektion') %}class="active"{% endif %}>Lektionen</a>
<a href="/software" {% if request.path.startswith('/software') %}class="active"{% endif %} style="font-size:0.85rem;">Software</a>
<a href="/drucker" {% if request.path.startswith('/drucker') %}class="active"{% endif %} style="font-size:0.85rem;">Drucker</a>
<a href="/projekte" {% if request.path.startswith('/projekte') %}class="active"{% endif %} style="font-size:0.85rem;">Projekte</a>
<a href="/troubleshooting" {% if request.path.startswith('/troubleshooting') %}class="active"{% endif %} style="font-size:0.85rem;">Hilfe</a>
{% if session.profil_id %}
<a href="/cockpit" {% if request.path.startswith('/cockpit') %}class="active"{% endif %}
style="margin-left:auto;">{{ tier_emoji(session.get('tier','')) }} {{ session.get('vorname','') }}</a>
<a href="/logout" style="font-size:0.8rem; opacity:0.7;">Abmelden</a>
{% else %}
<a href="/login" {% if request.path.startswith('/login') %}class="active"{% endif %} style="margin-left:auto;">Login</a>
{% endif %}
<a href="/admin/dashboard" {% if request.path.startswith('/admin') %}class="active"{% endif %} style="font-size:0.8rem; opacity:0.7;">Admin</a>
</div>
</nav>
<main>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="flash flash-{{ category }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</main>
{% block scripts %}{% endblock %}
{# Alph-PummelHummel-Feedback-Widget — wird in einer spaeteren Etappe integriert. #}
{{ alph_widget_snippet|default('')|safe }}
</body>
</html>

View File

@@ -0,0 +1,24 @@
{% extends "base.html" %}
{% block title %}Cockpit &mdash; {{ vorname }}{% endblock %}
{% block content %}
<div class="section-header">
<h1 style="display:flex; align-items:center; gap:0.5rem;">
<span style="font-size: 2.2rem;">{{ tier_emoji(tier) }}</span>
{{ vorname }}
</h1>
<a href="{{ url_for('profil.logout') }}" class="btn btn-secondary">Abmelden</a>
</div>
<div class="card" style="background: #FEF9E7; border-color: #F5C842;">
<h3>Cockpit-Stub &mdash; Etappe 1</h3>
<p>Hier kommt in Etappe 2 das progressive Cockpit (V1):</p>
<ul style="margin-top: 0.5rem; padding-left: 1.5rem;">
<li>Wenn du <strong>neu</strong> bist: nur Onboarding-Karte + Check-In-Trigger</li>
<li>Nach <strong>L1 fertig</strong>: zusaetzlich Drucker-Buchung</li>
<li>Mit <strong>aktiven Projekten</strong>: zusaetzlich Projekt-/Auftrags-/Slot-Karten</li>
</ul>
<p style="margin-top: 0.8rem; color: var(--text-light); font-size: 0.9rem;">
Details: <code>docs/selbstlern-loop.md</code> und <code>docs/etappen.md</code>.
</p>
</div>
{% endblock %}

61
templates/index.html Normal file
View File

@@ -0,0 +1,61 @@
{% extends "base.html" %}
{% block title %}3D-Druck-AG{% endblock %}
{% block content %}
<div style="text-align: center; margin-bottom: 2rem;">
<h1 style="font-size: 2rem;">3D-Druck-AG</h1>
<p style="color: var(--text-light); font-size: 1.1rem;">
Vom fertigen Modell zum eigenen Druck &mdash; Schritt fuer Schritt.
</p>
</div>
<div class="grid" style="grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1.5rem; max-width: 1000px; margin: 0 auto;">
{% if session.profil_id %}
<a href="/cockpit" class="card bereich-card" style="text-decoration:none; color:inherit; border-top: 5px solid var(--primary); text-align:center; padding: 2.5rem 1.5rem;">
<div style="font-size: 3.5rem; margin-bottom: 0.5rem;">{{ tier_emoji(session.get('tier','')) }}</div>
<h2 style="color: var(--primary);">Dein Cockpit</h2>
<p style="margin: 0.8rem 0;">Projekt, Druckauftraege, Slots &mdash; alles auf einen Blick.</p>
<span class="btn btn-primary">Zum Cockpit</span>
</a>
{% else %}
<a href="/login" class="card bereich-card" style="text-decoration:none; color:inherit; border-top: 5px solid var(--primary); text-align:center; padding: 2.5rem 1.5rem;">
<div style="font-size: 3.5rem; margin-bottom: 0.5rem;">&#x1F512;</div>
<h2 style="color: var(--primary);">Login</h2>
<p style="margin: 0.8rem 0;">Vorname &middot; Tier-Icon &middot; PIN. Danach siehst du deinen Stand und kannst Drucker reservieren.</p>
<span class="btn btn-primary">Einloggen</span>
</a>
{% endif %}
<a href="/lektionen" class="card bereich-card" style="text-decoration:none; color:inherit; border-top: 5px solid var(--success); text-align:center; padding: 2.5rem 1.5rem;">
<div style="font-size: 3.5rem; margin-bottom: 0.5rem;">&#x1F4DA;</div>
<h2 style="color: var(--success);">Lektionen</h2>
<p style="margin: 0.8rem 0;">L1 Drucker-Kennenlernen &middot; L2 Modell drucken &middot; L3 Customizer &middot; danach frei.</p>
<span class="btn" style="background: var(--success); color: white;">Lektionen ansehen</span>
</a>
<a href="/drucker" class="card bereich-card" style="text-decoration:none; color:inherit; border-top: 5px solid var(--secondary); text-align:center; padding: 2.5rem 1.5rem;">
<div style="font-size: 3.5rem; margin-bottom: 0.5rem;">&#x1F5A8;&#xFE0F;</div>
<h2 style="color: var(--secondary);">Drucker</h2>
<p style="margin: 0.8rem 0;">4 Bambu-Lab-Drucker. Live-Status, freie Slots, Reservierungen.</p>
<span class="btn" style="background: var(--secondary); color: white;">Druckerstatus</span>
</a>
</div>
<div class="card" style="max-width: 800px; margin: 2rem auto 0; background: #FEF9E7; border-color: #F5C842;">
<h3 style="margin-bottom: 0.5rem;">Hinweis</h3>
<p style="margin: 0;">
Diese Seite ist im Aufbau. Stand: 2026-05-23, Skelett. Inhalte werden in
den naechsten Wochen schrittweise ergaenzt &mdash; siehe
<code>docs/kurskonzept.md</code>.
</p>
</div>
{% endblock %}
{% block scripts %}
<style>
.bereich-card { transition: transform 0.15s, box-shadow 0.15s; }
.bereich-card:hover { transform: translateY(-4px); box-shadow: 0 6px 20px rgba(0,0,0,0.12); }
</style>
{% endblock %}

44
templates/login.html Normal file
View File

@@ -0,0 +1,44 @@
{% extends "base.html" %}
{% block title %}Login &mdash; 3D-Druck-AG{% endblock %}
{% block content %}
<div style="text-align: center; margin-bottom: 1.5rem;">
<h1>Wer bist du?</h1>
<p style="color: var(--text-light);">Such dein Profil und klick drauf.</p>
</div>
{% if profile %}
<div class="grid" style="grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 1rem; max-width: 900px; margin: 0 auto;">
{% for p in profile %}
<a href="{{ url_for('profil.login_pin', profil_id=p.id) }}" class="card profil-kachel">
<div style="font-size: 3rem;">{{ tier_emoji(p.tier) }}</div>
<div style="font-weight: 600; margin-top: 0.3rem;">{{ p.vorname }}</div>
<div style="font-size: 0.8rem; color: var(--text-light);">{{ tier_name(p.tier) }}</div>
</a>
{% endfor %}
</div>
{% else %}
<div class="card" style="max-width: 600px; margin: 2rem auto; text-align: center;">
<h2>Noch keine Profile angelegt</h2>
<p>Der Lehrer muss erst Profile anlegen. Sag Herb oder Markus Bescheid.</p>
<p style="margin-top: 1rem;"><a href="{{ url_for('admin.admin_login') }}" class="btn btn-secondary">Admin-Login</a></p>
</div>
{% endif %}
{% endblock %}
{% block scripts %}
<style>
.profil-kachel {
text-decoration: none;
color: inherit;
text-align: center;
padding: 1.5rem 0.8rem;
transition: transform 0.15s, box-shadow 0.15s;
cursor: pointer;
}
.profil-kachel:hover {
transform: translateY(-3px);
box-shadow: 0 6px 20px rgba(0,0,0,0.12);
}
</style>
{% endblock %}

42
templates/login_pin.html Normal file
View File

@@ -0,0 +1,42 @@
{% extends "base.html" %}
{% block title %}PIN-Eingabe &mdash; {{ profil.vorname }}{% endblock %}
{% block content %}
<div class="card" style="max-width: 420px; margin: 2rem auto; text-align: center;">
<div style="font-size: 4rem; margin-bottom: 0.5rem;">{{ tier_emoji(profil.tier) }}</div>
<h1 style="font-size: 1.4rem; margin-bottom: 0.2rem;">{{ profil.vorname }}</h1>
<p style="color: var(--text-light); font-size: 0.9rem;">{{ tier_name(profil.tier) }}</p>
{% if gesperrt %}
<div class="flash flash-error" style="margin-top: 1.5rem;">
Zu viele Fehlversuche &mdash; gesperrt fuer noch ca. <strong>{{ min_rest }} Minuten</strong>.
Frag den Lehrer, wenn du nicht warten kannst.
</div>
<p style="margin-top: 1rem;">
<a href="{{ url_for('profil.login') }}" class="btn btn-secondary">Anderes Profil waehlen</a>
</p>
{% else %}
<form method="post" style="margin-top: 1.5rem;">
<label for="pin" style="display:block; font-size: 0.9rem; color: var(--text-light); margin-bottom: 0.5rem;">
Deine 4-stellige PIN:
</label>
<input
type="text"
inputmode="numeric"
pattern="[0-9]{4}"
maxlength="4"
id="pin"
name="pin"
autofocus
autocomplete="off"
style="font-size: 2rem; text-align: center; letter-spacing: 0.8rem;
width: 12rem; padding: 0.5rem; border: 2px solid var(--primary);
border-radius: 8px; margin-bottom: 1rem;"
>
<br>
<button type="submit" class="btn btn-primary">Einloggen</button>
<a href="{{ url_for('profil.login') }}" class="btn btn-secondary" style="margin-left: 0.5rem;">Anderes Profil</a>
</form>
{% endif %}
</div>
{% endblock %}

39
tiere.py Normal file
View File

@@ -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