Files
3d.lehrstun.de/templates/base.html
Hans Puettmann 4658d4931a Initial commit: Konzept v2 + Etappe 1 (Blueprints + Login + Profile)
Schwester-Projekt zu arduino.lehrstun.de fuer die 3D-Druck-AG am
Lessing-Gymnasium. Offene jahrelange Nachmittags-AG (kein Trimester),
8-10 Einzel-SuS mit optionalen 2er-Paarungen, 4 Bambu-Lab-Drucker
(3 geschlossen + 1 A1 mini).

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

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

Nicht im Repo (.gitignore): .env, *.db, venv/, .obsidian/workspace.json
2026-05-24 18:56:59 +02:00

223 lines
7.6 KiB
HTML

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