feat: Pico.css für Dark/Light-Mode und Responsivität

Klassenloses CSS-Framework (self-hosted unter static/, kein CDN), passt
zum bestehenden Ansatz ohne JS-Build-Schritt. Bringt automatisches
Dark/Light-Mode (prefers-color-scheme) und Responsivität ohne
Zusatzaufwand. Templates auf Picos semantische Konventionen umgestellt
(label/input-Paare, article für die Login-Karte, figure um Tabellen für
horizontales Scrollen). Eigene CSS-Regeln auf die zwei Farb-Utility-
Klassen (state-present/state-absent) reduziert, Rest kommt von Pico.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Thomas Stallinger 2026-08-05 08:16:43 +02:00
parent 21c6210869
commit c35145ef8e
6 changed files with 49 additions and 36 deletions

2
app.py
View File

@ -6,6 +6,7 @@ import httpx
import psycopg
from fastapi import Depends, FastAPI, Form, HTTPException, Request
from fastapi.responses import RedirectResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from starlette.middleware.sessions import SessionMiddleware
@ -26,6 +27,7 @@ OPTIONEN_FELDER = {
app = FastAPI(title="Tuxflotte Kundenplattform")
app.add_middleware(SessionMiddleware, secret_key=SESSION_SECRET, session_cookie="kundenplattform_session")
app.mount("/static", StaticFiles(directory="static"), name="static")
templates = Jinja2Templates(directory="templates")

4
static/pico.min.css vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -5,11 +5,12 @@
<h2>Auftragskatalog: {{ device.hostname or device.device_fingerprint }}</h2>
{% if fehler %}
<p class="fehler">{{ fehler }}</p>
<p><mark>{{ fehler }}</mark></p>
{% else %}
{% for kategorie_name, eintraege in kategorien.items() %}
<div class="kategorie">
<h2>{{ kategorie_name }}</h2>
<section>
<h3>{{ kategorie_name }}</h3>
<figure>
<table>
<tbody>
{% for eintrag in eintraege %}
@ -23,15 +24,18 @@
</td>
<td>
{% if eintrag.state == "present" %}
<form class="inline" method="post" action="/geraete/{{ device.id }}/auftragskatalog/{{ eintrag.merkmal }}/deselect">
<button type="submit">Abwählen</button>
<form method="post" action="/geraete/{{ device.id }}/auftragskatalog/{{ eintrag.merkmal }}/deselect">
<button type="submit" class="secondary outline">Abwählen</button>
</form>
{% else %}
<form class="inline" method="post" action="/geraete/{{ device.id }}/auftragskatalog/{{ eintrag.merkmal }}/select">
<form method="post" action="/geraete/{{ device.id }}/auftragskatalog/{{ eintrag.merkmal }}/select">
{% if optionen_felder.get(eintrag.merkmal) %}
<div class="optionen">
{% for feld in optionen_felder[eintrag.merkmal] %}
<label><input type="checkbox" name="optionen.{{ feld.key }}"> {{ feld.label }}</label>
<label>
<input type="checkbox" name="optionen.{{ feld.key }}">
{{ feld.label }}
</label>
{% endfor %}
</div>
{% endif %}
@ -43,7 +47,8 @@
{% endfor %}
</tbody>
</table>
</div>
</figure>
</section>
{% endfor %}
{% endif %}
{% endblock %}

View File

@ -2,31 +2,27 @@
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}Tuxflotte{% endblock %}</title>
<link rel="stylesheet" href="/static/pico.min.css">
<style>
body { font-family: system-ui, sans-serif; max-width: 60rem; margin: 2rem auto; padding: 0 1rem; color: #1a1a1a; }
header { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 2rem; border-bottom: 1px solid #ddd; padding-bottom: 0.5rem; }
header a { color: inherit; text-decoration: none; }
h1 { font-size: 1.2rem; margin: 0; }
table { width: 100%; border-collapse: collapse; }
th, td { text-align: left; padding: 0.5rem; border-bottom: 1px solid #eee; }
.kategorie { margin-top: 1.5rem; }
.kategorie h2 { font-size: 1rem; color: #555; margin-bottom: 0.3rem; }
.state-present { color: #1a7f37; font-weight: 600; }
.state-absent { color: #999; }
form.inline { display: inline; }
button { cursor: pointer; padding: 0.3rem 0.8rem; }
.fehler { background: #fee; border: 1px solid #f99; padding: 0.6rem; border-radius: 4px; margin-bottom: 1rem; }
.optionen label { font-weight: normal; font-size: 0.9rem; color: #555; display: block; margin: 0.2rem 0; }
.state-present { color: var(--pico-ins-color); font-weight: 600; }
.state-absent { color: var(--pico-muted-color); }
.optionen label { font-weight: normal; }
td form { margin-bottom: 0; }
</style>
</head>
<body>
<header>
<h1><a href="/geraete">Tuxflotte Kundenplattform</a></h1>
<header class="container">
<nav>
<ul><li><strong><a href="/geraete">Tuxflotte Kundenplattform</a></strong></li></ul>
{% if user %}
<span>{{ user.email }} — <a href="/logout">Abmelden</a></span>
<ul><li>{{ user.email }}</li><li><a href="/logout" role="button" class="secondary outline">Abmelden</a></li></ul>
{% endif %}
</nav>
</header>
<main class="container">
{% block content %}{% endblock %}
</main>
</body>
</html>

View File

@ -5,6 +5,7 @@
{% if not devices %}
<p>Keine Geräte gefunden.</p>
{% else %}
<figure>
<table>
<thead>
<tr><th>Hostname</th><th>Kennung</th><th>Letzter Check-in</th><th></th></tr>
@ -15,10 +16,11 @@
<td>{{ device.hostname or "—" }}</td>
<td><code>{{ device.device_fingerprint }}</code></td>
<td>{{ device.agent_last_checkin or "noch nie" }}</td>
<td><a href="/geraete/{{ device.id }}">Auftragskatalog →</a></td>
<td><a href="/geraete/{{ device.id }}" role="button" class="outline">Auftragskatalog →</a></td>
</tr>
{% endfor %}
</tbody>
</table>
</figure>
{% endif %}
{% endblock %}

View File

@ -1,11 +1,15 @@
{% extends "base.html" %}
{% block title %}Anmelden — Tuxflotte{% endblock %}
{% block content %}
<article style="max-width: 24rem; margin: 3rem auto;">
<h2>Anmelden</h2>
{% if error %}<p class="fehler">{{ error }}</p>{% endif %}
{% if error %}<p><mark>{{ error }}</mark></p>{% endif %}
<form method="post" action="/login">
<p><label>E-Mail<br><input type="email" name="email" required autofocus></label></p>
<p><label>Passwort<br><input type="password" name="password" required></label></p>
<label for="email">E-Mail</label>
<input type="email" id="email" name="email" required autofocus>
<label for="password">Passwort</label>
<input type="password" id="password" name="password" required>
<button type="submit">Anmelden</button>
</form>
</article>
{% endblock %}