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