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:
15
.env.beispiel
Normal file
15
.env.beispiel
Normal 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
31
.gitignore
vendored
Normal 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
8
.obsidian/app.json
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"showLineNumber": true,
|
||||||
|
"newFileLocation": "current",
|
||||||
|
"attachmentFolderPath": "anhaenge",
|
||||||
|
"alwaysUpdateLinks": true,
|
||||||
|
"useMarkdownLinks": false,
|
||||||
|
"newLinkFormat": "shortest"
|
||||||
|
}
|
||||||
1
.obsidian/appearance.json
vendored
Normal file
1
.obsidian/appearance.json
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
1
.obsidian/community-plugins.json
vendored
Normal file
1
.obsidian/community-plugins.json
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
[]
|
||||||
34
.obsidian/core-plugins.json
vendored
Normal file
34
.obsidian/core-plugins.json
vendored
Normal 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
27
.obsidian/graph.json
vendored
Normal 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
38
CHANGELOG.md
Normal 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
83
CLAUDE.md
Normal 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
142
COCKPIT.md
Normal 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
56
README.md
Normal 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
90
app.py
Normal 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
301
database.py
Normal 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
111
docs/architektur.md
Normal 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
249
docs/datenmodell.md
Normal 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
166
docs/decisions.md
Normal 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
96
docs/druckerapi.md
Normal 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
195
docs/etappen.md
Normal 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
66
docs/glossar.md
Normal 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
308
docs/kurskonzept.md
Normal 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
147
docs/lehrer-dashboard.md
Normal 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]]
|
||||||
56
docs/lektion-L1-drucker-kennenlernen.md
Normal file
56
docs/lektion-L1-drucker-kennenlernen.md
Normal 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]]
|
||||||
52
docs/lektion-L2-modell-drucken.md
Normal file
52
docs/lektion-L2-modell-drucken.md
Normal 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]]
|
||||||
53
docs/lektion-L3-customizer.md
Normal file
53
docs/lektion-L3-customizer.md
Normal 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
99
docs/offene-fragen.md
Normal 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
126
docs/routen.md
Normal 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
176
docs/selbstlern-loop.md
Normal 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
58
docs/sicherheit.md
Normal 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
100
docs/slotsystem.md
Normal 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]]
|
||||||
39
docs/software-bambu-handy.md
Normal file
39
docs/software-bambu-handy.md
Normal 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)
|
||||||
49
docs/software-bambu-studio.md
Normal file
49
docs/software-bambu-studio.md
Normal 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]]
|
||||||
46
docs/software-makerworld.md
Normal file
46
docs/software-makerworld.md
Normal 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
61
docs/software-openscad.md
Normal 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]]
|
||||||
57
docs/software-printables.md
Normal file
57
docs/software-printables.md
Normal 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]]
|
||||||
57
docs/software-tinkercad.md
Normal file
57
docs/software-tinkercad.md
Normal 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
21
migrations/README.md
Normal 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
6
requirements.txt
Normal 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
10
routes/__init__.py
Normal 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
259
routes/admin.py
Normal 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
27
routes/api.py
Normal 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
56
routes/oeffentlich.py
Normal 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
131
routes/profil.py
Normal 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
86
seed.py
Normal 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
9
services/__init__.py
Normal 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
156
services/auth.py
Normal 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
20
services/bambu.py
Normal 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
47
services/datei.py
Normal 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
9
templates/404.html
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Nicht gefunden — 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
9
templates/500.html
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Fehler — 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
13
templates/501.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Noch nicht da — 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 %}
|
||||||
22
templates/admin_login.html
Normal file
22
templates/admin_login.html
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Admin-Login — 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 & 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 %}
|
||||||
89
templates/admin_profile.html
Normal file
89
templates/admin_profile.html
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Profile — 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 %}
|
||||||
57
templates/admin_profile_karten.html
Normal file
57
templates/admin_profile_karten.html
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}PIN-Karten — Druckbogen{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="no-print" style="margin-bottom: 1rem;">
|
||||||
|
<a href="{{ url_for('admin.profile_liste') }}" class="btn btn-secondary">← 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
222
templates/base.html
Normal 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>
|
||||||
24
templates/cockpit_stub.html
Normal file
24
templates/cockpit_stub.html
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Cockpit — {{ 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 — 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
61
templates/index.html
Normal 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 — 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 — 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;">🔒</div>
|
||||||
|
<h2 style="color: var(--primary);">Login</h2>
|
||||||
|
<p style="margin: 0.8rem 0;">Vorname · Tier-Icon · 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;">📚</div>
|
||||||
|
<h2 style="color: var(--success);">Lektionen</h2>
|
||||||
|
<p style="margin: 0.8rem 0;">L1 Drucker-Kennenlernen · L2 Modell drucken · L3 Customizer · 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;">🖨️</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 — 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
44
templates/login.html
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Login — 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
42
templates/login_pin.html
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}PIN-Eingabe — {{ 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 — 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
39
tiere.py
Normal 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
|
||||||
Reference in New Issue
Block a user