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:
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 %}
|
||||
Reference in New Issue
Block a user