Login-Seite überarbeitet, TESM-Logo als echter Hintergrund; SSH-MOTD-Banner mit Live-Daten

- Login-Seite: WiS-/TESM-Logo und "Melde dich an, um fortzufahren" aus der
  Box entfernt; die TESM-Wortmarke mit Funktionszeile ist jetzt der
  tatsächliche Seitenhintergrund (absolut positioniert, hinter allem), die
  Box liegt zentriert davor. Nebenbei zwei echte Bugs gefunden und behoben:
  fehlendes Cache-Busting auf login.html (asset_url() wie in base.html,
  sonst hält nginx' 7-Tage-Cache eine alte style.css/app.js fest) und ein
  durch den fehlenden align-items-Wert auf die volle Höhe gestrecktes
  Login-Formular.
- Neuer SSH-Login-Banner (dynamisches MOTD) mit Live-Flottenstatus statt der
  Standard-Ubuntu-Willkommensmeldung: srv/tesm/motd_banner.py liest
  Online/Offline- und DHCP-Status aus denselben Quellen wie das
  Web-Dashboard (get_device_status()/_dhcp_read_leases()), eingebunden über
  etc/update-motd.d/50-tesm. install.sh deaktiviert dabei die Standard-
  Ubuntu-MOTD-Skripte (chmod -x, reversibel) und installiert den neuen Hook.
This commit is contained in:
2026-08-13 09:13:18 +02:00
parent d1b10bd970
commit 3ab3407a18
6 changed files with 168 additions and 24 deletions
+38
View File
@@ -687,6 +687,44 @@ zeigten korrekt die neue Fehlermeldung, da die realen Switch-IPs
(192.168.200.x) vom Test-Host aus nicht erreichbar sind — vorher hätte das (192.168.200.x) vom Test-Host aus nicht erreichbar sind — vorher hätte das
fälschlich als 43× „PoE deaktiviert“/„PoE aktiviert“ geloggt. fälschlich als 43× „PoE deaktiviert“/„PoE aktiviert“ geloggt.
## SSH-Login-Banner (dynamisches MOTD)
Ein SSH-Login auf den TESM-Host selbst zeigt statt der Standard-Ubuntu-
Willkommensmeldung einen kompakten Live-Status der Flotte — Bernstein/Amber
als Akzentfarbe (passend zum Logo), fett hervorgehobene Zahlen:
```
TESM · TimEShepManager
überwachen · booten · anbinden (DHCP) · Betriebssystem geben (PXE)
3 Clients online · 45 down · DHCP: installiert, gestoppt
```
Umgesetzt als zwei Teile:
- **`srv/tesm/motd_banner.py`** — liest den Live-Status aus genau denselben
Quellen wie das Web-Dashboard, ohne selbst zu prüfen: `get_device_status()`
parst dieselbe Check-Log-Datei (`live.log`) wie das Dashboard,
`_detect_dhcp_status()`/`_dhcp_read_leases()` lesen Dienststatus bzw. die
Kea-Lease-CSV direkt. Läuft über `venv/bin/python3` (importiert `app.py`,
das Flask u.a. braucht, die nur dort installiert sind — dasselbe Muster
wie `generate_ips.py`). Ein Fehler beim Datenzugriff lässt die jeweilige
Zeile nur einen Platzhaltertext zeigen, statt den Login zu stören.
- **`etc/update-motd.d/50-tesm`** — der eigentliche, ausführbare
Ubuntu-„dynamic MOTD"-Hook (`run-parts /etc/update-motd.d`, von
`pam_motd.so` bei jedem SSH-Login neu ausgeführt und nach
`/run/motd.dynamic` geschrieben). `timeout 3` verhindert, dass ein
hängender DB-/Dateizugriff den Login blockiert.
**Bewusst kein PXE-Status** in der Live-Zeile: anders als Überwachen/
Booten/DHCP ist PXE bislang nur Teil der Tagline, keine echte, mit Daten
hinterlegte Funktion der App — ein erfundener Wert wäre keine Live-Daten.
Die Standard-Ubuntu-MOTD-Skripte (`00-header`, `10-help-text`,
`50-motd-news`, `60-unminimize`, `91-release-upgrade`,
`92-unattended-upgrades`) werden vom Installer per `chmod -x` deaktiviert
(nicht gelöscht — jederzeit reversibel mit `chmod +x`), damit nur noch der
TESM-Banner erscheint.
## Netzwerkeinstellungen des Hosts (Systemeinstellungen) ## Netzwerkeinstellungen des Hosts (Systemeinstellungen)
Eigene, klar getrennte Funktion von obigem DHCP-*Server*: hier geht es um Eigene, klar getrennte Funktion von obigem DHCP-*Server*: hier geht es um
+10
View File
@@ -0,0 +1,10 @@
#!/bin/sh
# TESM -- Live-Flotten-Status im SSH-Login-Banner (dynamisches MOTD, siehe
# /etc/pam.d/sshd: "session optional pam_motd.so motd=/run/motd.dynamic").
# Ruft motd_banner.py im TESM-Quellverzeichnis über dessen venv auf, da
# app.py (die eigentliche Datenquelle) Flask u.a. importiert, die nur dort
# installiert sind -- gleiches Muster wie generate_ips.py/poe.sh.
# "timeout" verhindert, dass ein hängender DB-/Dateizugriff den Login
# blockiert; "|| true" sorgt dafür, dass ein Fehler hier nie den SSH-Login
# selbst stört -- im schlimmsten Fall bleibt der Banner einfach leer.
timeout 3 /srv/tesm/venv/bin/python3 /srv/tesm/motd_banner.py 2>/dev/null || true
+14
View File
@@ -63,6 +63,20 @@ sudo cp "$REPO_DIR"/etc/systemd/system/*.service /etc/systemd/system/
sudo cp "$REPO_DIR"/etc/systemd/system/*.timer /etc/systemd/system/ sudo cp "$REPO_DIR"/etc/systemd/system/*.timer /etc/systemd/system/
print_status "Scripts and units copied" print_status "Scripts and units copied"
# ---- SSH-Login-Banner (MOTD) ----
# Live-Flotten-Status (Online/Offline, DHCP) statt der Standard-Ubuntu-
# Willkommensmeldung -- siehe srv/tesm/motd_banner.py für die Datenquellen.
# Die Standard-Skripte bleiben erhalten, werden aber deaktiviert (chmod -x,
# nicht gelöscht), damit ein Rückbau jederzeit per chmod +x möglich ist.
step "Setting up TESM SSH login banner"
sudo cp "$REPO_DIR/etc/update-motd.d/50-tesm" /etc/update-motd.d/50-tesm
sudo chmod 755 /etc/update-motd.d/50-tesm
sudo chmod -x /etc/update-motd.d/00-header /etc/update-motd.d/10-help-text \
/etc/update-motd.d/50-motd-news /etc/update-motd.d/60-unminimize \
/etc/update-motd.d/91-release-upgrade /etc/update-motd.d/92-unattended-upgrades \
2>/dev/null || true
print_status "SSH login banner configured"
# ---- nginx ---- # ---- nginx ----
step "Configuring nginx reverse proxy" step "Configuring nginx reverse proxy"
sudo cp "$REPO_DIR/etc/nginx/sites-available/tesm" /etc/nginx/sites-available/tesm sudo cp "$REPO_DIR/etc/nginx/sites-available/tesm" /etc/nginx/sites-available/tesm
+81
View File
@@ -0,0 +1,81 @@
#!/usr/bin/env python3
"""Live-Flotten-Banner für die SSH-Login-Message (MOTD).
Liest denselben Live-Status wie das Web-Dashboard, direkt aus den
gleichen Quellen (siehe app.py):
- Online/Offline: get_device_status() parst dieselbe Check-Log-Datei
(live.log) wie das Dashboard, statt selbst zu pingen -- ein Login
soll niemals eine eigene Prüfung anstoßen.
- DHCP: _detect_dhcp_status()/_dhcp_read_leases() lesen Dienststatus
bzw. Kea-Lease-CSV direkt, kein eigener Zustand.
Eingebunden über /etc/update-motd.d/50-tesm, das dieses Skript per
venv/bin/python3 aufruft -- app.py importiert Flask u.a., die nur in
der venv installiert sind (dasselbe Muster wie generate_ips.py, das
poe.sh auf demselben Weg konsumiert).
Bewusst NICHT enthalten: PXE-Status. Anders als überwachen/booten/
DHCP ist PXE (noch) keine echte, mit Daten hinterlegte Funktion der
App (bislang nur Teil der Tagline) -- ein erfundener "PXE: idle"-Wert
wäre keine Live-Daten, sondern eine Behauptung.
"""
from app import _detect_dhcp_status, _dhcp_read_leases, get_db_connection, get_device_status
AMBER = "\033[38;2;226;166;60m"
DIM = "\033[2m"
BOLD = "\033[1m"
RESET = "\033[0m"
def _fleet_line(amber, dim, bold, reset):
try:
conn = get_db_connection()
devices = conn.execute(
"SELECT mac, name, is_active FROM devices WHERE deleted_at IS NULL"
).fetchall()
conn.close()
status, _, _, _ = get_device_status(devices)
active = [d for d in devices if d["is_active"]]
online = sum(1 for d in active if status.get(d["mac"]) == "online")
down = len(active) - online
return f"{bold}{online}{reset} Clients online · {bold}{down}{reset} down"
except Exception:
return f"{dim}Geräte-Status nicht verfügbar{reset}"
def _dhcp_line():
try:
dhcp = _detect_dhcp_status()
if not dhcp["installed"]:
return "nicht installiert"
if not dhcp["active"]:
return "installiert, gestoppt"
leases = _dhcp_read_leases()
return f"{len(leases)} Leases aktiv" if leases is not None else "läuft, keine Lease-Datei"
except Exception:
return "Status nicht verfügbar"
def main():
# KEIN sys.stdout.isatty()-Check: dieses Skript schreibt beim Login
# nicht direkt auf ein Terminal, sondern über run-parts in
# /run/motd.dynamic (eine normale Datei -- isatty() wäre hier immer
# False). Der eigentliche Betrachter ist trotzdem ein echtes Terminal
# (die SSH-Session, der PAM den Inhalt danach ausgibt), daher immer
# Farbe ausgeben.
amber, dim, bold, reset = AMBER, DIM, BOLD, RESET
print(f"{bold}{amber}TESM{reset} · TimEShepManager")
print(f"{dim}überwachen · booten · anbinden (DHCP) · Betriebssystem geben (PXE){reset}")
print()
print(f"{_fleet_line(amber, dim, bold, reset)} · DHCP: {_dhcp_line()}")
print()
if __name__ == "__main__":
try:
main()
except Exception:
# Ein MOTD-Banner darf einen SSH-Login niemals stören -- lieber
# gar keine Ausgabe als ein Traceback vor dem Shell-Prompt.
pass
+22 -13
View File
@@ -999,28 +999,41 @@ select {
.login-page { .login-page {
min-height: 100vh; min-height: 100vh;
position: relative;
overflow: hidden;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 5vw; padding: 40px 20px;
background: background:
radial-gradient(circle at 15% 15%, rgba(255, 113, 0, 0.14), transparent 45%), radial-gradient(circle at 15% 15%, rgba(255, 113, 0, 0.14), transparent 45%),
radial-gradient(circle at 85% 85%, rgba(255, 113, 0, 0.08), transparent 40%), radial-gradient(circle at 85% 85%, rgba(255, 113, 0, 0.08), transparent 40%),
var(--bg); var(--bg);
padding: 20px;
} }
/* TESM-Wortmarke (mit Funktionszeile) als eigenes Panel neben der Login-Karte /* TESM-Wortmarke mit Funktionszeile als tatsächlicher Seitenhintergrund --
-- nebeneinander statt überlagert, damit beides gleichzeitig sichtbar absolut positioniert und hinter allem (z-index 0), kein eigenes Panel und
bleibt. Auf schmalen Bildschirmen ausgeblendet (Platz reicht sonst nicht). */ kein bloßes "Logo über der Box"-Stacking mehr. Die Login-Box (z-index 1)
.login-branding { display: none; pointer-events: none; } liegt bewusst weit oben auf der Seite, dadurch sichtbar VOR dem oberen
.login-branding img { width: min(34vw, 520px); height: auto; } Teil des Hintergrundmotivs statt klassisch vertikal zentriert. */
.login-bg {
position: absolute;
top: 4vh;
left: 50%;
transform: translateX(-50%);
width: min(72vw, 880px);
pointer-events: none;
z-index: 0;
}
.login-bg img { width: 100%; height: auto; display: block; }
@media (min-width: 860px) { @media (max-width: 640px) {
.login-branding { display: block; } .login-bg { width: min(90vw, 460px); }
} }
.login-card { .login-card {
position: relative;
z-index: 1;
width: 100%; width: 100%;
max-width: 380px; max-width: 380px;
background: var(--bg-card); background: var(--bg-card);
@@ -1030,11 +1043,7 @@ select {
padding: 34px 32px 28px; padding: 34px 32px 28px;
} }
.login-logo { display: flex; justify-content: center; margin-bottom: 22px; }
.login-logo img { height: 34px; }
.login-card h1 { font-size: 18px; text-align: center; margin-bottom: 4px; } .login-card h1 { font-size: 18px; text-align: center; margin-bottom: 4px; }
.login-card .login-sub { text-align: center; color: var(--text-faint); font-size: 12.5px; margin-bottom: 24px; }
/* ========================================================================== /* ==========================================================================
Logs page Logs page
+3 -11
View File
@@ -5,23 +5,15 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>Login · TESM</title> <title>Login · TESM</title>
<link rel="icon" type="image/svg+xml" href="{{ url_for('static', filename='images/icon-dark.svg') }}" id="app-favicon"> <link rel="icon" type="image/svg+xml" href="{{ url_for('static', filename='images/icon-dark.svg') }}" id="app-favicon">
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}"> <link rel="stylesheet" href="{{ asset_url('css/style.css') }}">
</head> </head>
<body> <body>
<div class="login-page"> <div class="login-page">
<div class="login-branding"> <div class="login-bg">
<img src="{{ url_for('static', filename='images/logo-dark-subline.svg') }}" alt="TESM" id="login-bg-logo"> <img src="{{ url_for('static', filename='images/logo-dark-subline.svg') }}" alt="TESM" id="login-bg-logo">
</div> </div>
<div class="login-card"> <div class="login-card">
<div class="login-logo">
<img src="{{ url_for('static', filename='images/logo.png') }}" alt="WiS">
</div>
<div style="display:flex; justify-content:center; margin-bottom:4px;">
<img id="sidebar-logo" src="{{ url_for('static', filename='images/logo-dark.svg') }}" alt="TESM" style="height:32px; width:auto;">
</div>
<p class="login-sub">Melde dich an, um fortzufahren</p>
{% with messages = get_flashed_messages() %} {% with messages = get_flashed_messages() %}
{% if messages %} {% if messages %}
<div class="field" style="margin-bottom:18px;"> <div class="field" style="margin-bottom:18px;">
@@ -46,6 +38,6 @@
</div> </div>
</div> </div>
<script src="{{ url_for('static', filename='js/app.js') }}"></script> <script src="{{ asset_url('js/app.js') }}"></script>
</body> </body>
</html> </html>