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

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

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

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

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

9
templates/404.html Normal file
View 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
View File

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

13
templates/501.html Normal file
View File

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

View File

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

View File

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

View File

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

222
templates/base.html Normal file
View File

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

View File

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

61
templates/index.html Normal file
View File

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

44
templates/login.html Normal file
View File

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

42
templates/login_pin.html Normal file
View File

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