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 import psycopg
from fastapi import Depends, FastAPI, Form, HTTPException, Request from fastapi import Depends, FastAPI, Form, HTTPException, Request
from fastapi.responses import RedirectResponse from fastapi.responses import RedirectResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
from starlette.middleware.sessions import SessionMiddleware from starlette.middleware.sessions import SessionMiddleware
@ -26,6 +27,7 @@ OPTIONEN_FELDER = {
app = FastAPI(title="Tuxflotte Kundenplattform") app = FastAPI(title="Tuxflotte Kundenplattform")
app.add_middleware(SessionMiddleware, secret_key=SESSION_SECRET, session_cookie="kundenplattform_session") app.add_middleware(SessionMiddleware, secret_key=SESSION_SECRET, session_cookie="kundenplattform_session")
app.mount("/static", StaticFiles(directory="static"), name="static")
templates = Jinja2Templates(directory="templates") 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> <h2>Auftragskatalog: {{ device.hostname or device.device_fingerprint }}</h2>
{% if fehler %} {% if fehler %}
<p class="fehler">{{ fehler }}</p> <p><mark>{{ fehler }}</mark></p>
{% else %} {% else %}
{% for kategorie_name, eintraege in kategorien.items() %} {% for kategorie_name, eintraege in kategorien.items() %}
<div class="kategorie"> <section>
<h2>{{ kategorie_name }}</h2> <h3>{{ kategorie_name }}</h3>
<figure>
<table> <table>
<tbody> <tbody>
{% for eintrag in eintraege %} {% for eintrag in eintraege %}
@ -23,15 +24,18 @@
</td> </td>
<td> <td>
{% if eintrag.state == "present" %} {% if eintrag.state == "present" %}
<form class="inline" method="post" action="/geraete/{{ device.id }}/auftragskatalog/{{ eintrag.merkmal }}/deselect"> <form method="post" action="/geraete/{{ device.id }}/auftragskatalog/{{ eintrag.merkmal }}/deselect">
<button type="submit">Abwählen</button> <button type="submit" class="secondary outline">Abwählen</button>
</form> </form>
{% else %} {% 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) %} {% if optionen_felder.get(eintrag.merkmal) %}
<div class="optionen"> <div class="optionen">
{% for feld in optionen_felder[eintrag.merkmal] %} {% 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 %} {% endfor %}
</div> </div>
{% endif %} {% endif %}
@ -43,7 +47,8 @@
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div> </figure>
</section>
{% endfor %} {% endfor %}
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View File

@ -2,31 +2,27 @@
<html lang="de"> <html lang="de">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}Tuxflotte{% endblock %}</title> <title>{% block title %}Tuxflotte{% endblock %}</title>
<link rel="stylesheet" href="/static/pico.min.css">
<style> <style>
body { font-family: system-ui, sans-serif; max-width: 60rem; margin: 2rem auto; padding: 0 1rem; color: #1a1a1a; } .state-present { color: var(--pico-ins-color); font-weight: 600; }
header { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 2rem; border-bottom: 1px solid #ddd; padding-bottom: 0.5rem; } .state-absent { color: var(--pico-muted-color); }
header a { color: inherit; text-decoration: none; } .optionen label { font-weight: normal; }
h1 { font-size: 1.2rem; margin: 0; } td form { margin-bottom: 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; }
</style> </style>
</head> </head>
<body> <body>
<header> <header class="container">
<h1><a href="/geraete">Tuxflotte Kundenplattform</a></h1> <nav>
{% if user %} <ul><li><strong><a href="/geraete">Tuxflotte Kundenplattform</a></strong></li></ul>
<span>{{ user.email }} — <a href="/logout">Abmelden</a></span> {% if user %}
{% endif %} <ul><li>{{ user.email }}</li><li><a href="/logout" role="button" class="secondary outline">Abmelden</a></li></ul>
{% endif %}
</nav>
</header> </header>
{% block content %}{% endblock %} <main class="container">
{% block content %}{% endblock %}
</main>
</body> </body>
</html> </html>

View File

@ -5,6 +5,7 @@
{% if not devices %} {% if not devices %}
<p>Keine Geräte gefunden.</p> <p>Keine Geräte gefunden.</p>
{% else %} {% else %}
<figure>
<table> <table>
<thead> <thead>
<tr><th>Hostname</th><th>Kennung</th><th>Letzter Check-in</th><th></th></tr> <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>{{ device.hostname or "—" }}</td>
<td><code>{{ device.device_fingerprint }}</code></td> <td><code>{{ device.device_fingerprint }}</code></td>
<td>{{ device.agent_last_checkin or "noch nie" }}</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> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</figure>
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View File

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