Version 2.8.11: die Kachel oeffnet den Bildschirm

This commit is contained in:
alientim committed 2026-09-13 10:09:54 +02:00
1 parent 4275c84a6c
commit 0ac651a1b9
9 files changed
+131 -41

No files matched your search

+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "tesm"
version = "2.8.10"
version = "2.8.11"
description = "TESM -- Verwaltung und Ueberwachung von PoE-Endgeraeten und Aruba-Switchen"
requires-python = ">=3.11"
dependencies = [
+27 -6
View File
@@ -39,11 +39,21 @@ def _tiles(*, authenticated: bool) -> dict[str, Any]:
from ..services import inventory, restart
from ..services import vnc as vnc_dienst
conn = db()
counts = monitor.dashboard_counts(conn)
rows = inventory.list_devices(conn, only_active=not authenticated)
detailed = authenticated and current_user.has("devices.read")
# Einmal je Seitenaufbau, nicht je Kachel: der Port ist fuer alle derselbe,
# und ohne das Recht wird gar nicht erst nachgesehen.
vnc_erlaubt = detailed and current_user.has("device_vnc.execute")
vnc_port = (
int(core().settings.get(conn, "vnc_port") or vnc_dienst.STANDARD_PORT)
if vnc_erlaubt and core().settings.get(conn, "vnc_enabled")
else 0
)
tiles: list[dict[str, Any]] = []
for row in rows:
entry: dict[str, Any] = {
@@ -74,12 +84,23 @@ def _tiles(*, authenticated: bool) -> dict[str, Any]:
maintenance_active=_maintenance_active(),
darf=current_user.has,
),
# Aus dem letzten Durchlauf, nicht frisch gefragt: die
# Uebersicht zeigt viele Kacheln und laedt sich selbst
# nach. Ein Verbindungsversuch je Kachel und Aufbau waere
# genau die Art Hintergrundlast, die niemand bestellt hat.
"vnc": bool(row["vnc_open"])
and current_user.has("device_vnc.execute"),
# Die fertige Adresse, nicht nur ein Schalter: der Knopf
# auf der Kachel soll die Verbindung oeffnen und nicht
# erst eine Datei ablegen, die jemand anklicken muss.
#
# Beides -- ob dort ein Server antwortet und wohin gefragt
# wurde -- steht aus dem letzten Durchlauf in der
# Datenbank. Die Uebersicht zeigt viele Kacheln und laedt
# sich selbst nach; ein Verbindungsversuch oder eine
# Namensaufloesung je Kachel und Aufbau waere genau die
# Art Hintergrundlast, die niemand bestellt hat.
"vnc": (
vnc_dienst.Zugang(
ziel=str(row["vnc_target"]), port=vnc_port, erreichbar=True
).adresse
if vnc_port and row["vnc_open"] and row["vnc_target"]
else ""
),
}
)
tiles.append(entry)
+1 -1
View File
@@ -392,7 +392,7 @@ def _vnc_zugang(conn: Any, device: dict[str, Any]) -> vnc.Zugang | None:
zugang = vnc.zugang(device, port=port, pruefen=online)
if online:
with transaction():
monitor.merke_vnc(conn, int(device["id"]), zugang.erreichbar)
monitor.merke_vnc(conn, int(device["id"]), zugang.erreichbar, zugang.ziel)
return zugang
+13
View File
@@ -503,6 +503,18 @@ ALTER TABLE device_status ADD COLUMN vnc_checked_at TEXT;
"""
# Version 11 ----------------------------------------------------------------
#
# Nicht nur *ob* dort ein VNC-Server antwortet, sondern *wohin* gefragt wurde.
# Die Kacheln der Statusuebersicht verlinken die Verbindung unmittelbar
# (vnc://hap02); das Ziel dafuer aus dem Namen neu herzuleiten hiesse, bei
# jedem Seitenaufbau je Kachel eine Namensaufloesung zu versuchen. Der
# Durchlauf hat sie ohnehin schon gemacht -- also schreibt er sie hin.
V11 = """
ALTER TABLE device_status ADD COLUMN vnc_target TEXT NOT NULL DEFAULT '';
"""
def uebergang_standardgruppen(conn) -> None: # type: ignore[no-untyped-def]
"""Aus sieben Standardgruppen werden drei -- ohne jemandem etwas wegzunehmen.
@@ -546,4 +558,5 @@ TESM_MIGRATIONS: tuple[Migration, ...] = (
Migration(version=8, name="funktionsgruppe-fuer-admins", fn=uebergang_funktionsgruppe),
Migration(version=9, name="standardgruppen-aufraeumen", fn=uebergang_standardgruppen),
Migration(version=10, name="vnc-erreichbarkeit", sql=V10),
Migration(version=11, name="vnc-ziel", sql=V11),
)
+2 -1
View File
@@ -542,7 +542,8 @@ def list_devices(
WHERE k.host = d.ip AND k.port = d.ssh_port) AS host_key_known,
COALESCE(st.state,'unknown') AS state, st.latency_ms, st.last_check_at,
st.last_ok_at, st.last_change_at, st.last_restart_at, st.restart_count,
st.fail_count, COALESCE(st.vnc_open, 0) AS vnc_open, st.vnc_checked_at
st.fail_count, COALESCE(st.vnc_open, 0) AS vnc_open, st.vnc_checked_at,
COALESCE(st.vnc_target, '') AS vnc_target
FROM devices d
LEFT JOIN switches s ON s.id = d.switch_id AND s.deleted_at IS NULL
LEFT JOIN credentials c ON c.id = d.credential_id AND c.deleted_at IS NULL
+19 -7
View File
@@ -507,11 +507,20 @@ class SweepReport:
duration_ms: int = 0
def merke_vnc(conn: sqlite3.Connection, device_id: int, offen: bool) -> None:
"""Haelt fest, ob dort gerade ein VNC-Server antwortet."""
def merke_vnc(
conn: sqlite3.Connection, device_id: int, offen: bool, ziel: str = ""
) -> None:
"""Haelt fest, ob dort ein VNC-Server antwortet -- und unter welchem Ziel.
Das Ziel gehoert dazu, weil die Statusuebersicht die Verbindung unmittelbar
verlinkt. Es aus dem Namen neu herzuleiten hiesse, je Kachel und
Seitenaufbau eine Namensaufloesung zu versuchen; hier steht es einmal
geschrieben, von dem, der ohnehin gefragt hat.
"""
conn.execute(
"UPDATE device_status SET vnc_open=?, vnc_checked_at=? WHERE device_id=?",
(1 if offen else 0, _now(), device_id),
"UPDATE device_status SET vnc_open=?, vnc_checked_at=?, vnc_target=? "
"WHERE device_id=?",
(1 if offen else 0, _now(), ziel, device_id),
)
@@ -547,14 +556,17 @@ def run_sweep(
report.duration_ms = int((time.monotonic() - started) * 1000)
return report
def pruefen(device: dict[str, Any]) -> tuple[dict[str, Any], PingResult, bool | None]:
def pruefen(
device: dict[str, Any],
) -> tuple[dict[str, Any], PingResult, tuple[bool, str] | None]:
ergebnis = ping(str(device["ip"]))
if not vnc_port or not ergebnis.ok:
# Kein Ergebnis heisst "nicht gefragt" -- und laesst den zuletzt
# bekannten Stand stehen. Ein Geraet, das gerade neu startet, soll
# seinen Knopf nicht verlieren.
return device, ergebnis, None
return device, ergebnis, vnc_dienst.erreichbar(vnc_dienst.ziel(device), vnc_port)
ziel = vnc_dienst.ziel(device)
return device, ergebnis, (vnc_dienst.erreichbar(ziel, vnc_port), ziel)
with ThreadPoolExecutor(max_workers=min(MAX_PARALLEL_CHECKS, len(devices))) as pool:
results = list(pool.map(pruefen, devices))
@@ -566,7 +578,7 @@ def run_sweep(
report.checked += 1
previous, current = record_status(conn, int(device["id"]), result)
if vnc_offen is not None:
merke_vnc(conn, int(device["id"]), vnc_offen)
merke_vnc(conn, int(device["id"]), vnc_offen[0], vnc_offen[1])
if result.ok:
report.online += 1
else:
@@ -84,27 +84,6 @@
<a class="btn btn--sm btn--ghost" href="{{ url_for('devices.detail', device_id=tile.id) }}">
Details
</a>
{% if tile.restart_methods %}
{% set method = tile.restart_methods[0] %}
<form method="post" data-no-guard
action="{{ url_for('devices.restart_device', device_id=tile.id) }}">
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
<input type="hidden" name="method" value="{{ method.key }}">
<button class="btn btn--sm" type="submit" title="{{ method.hint }}"
data-busy-label="Startet ..."
data-confirm="{{ tile.name }} über {{ method.label }} neu starten? {{ method.hint }}"
data-confirm-title="Neustart" data-confirm-ok="Neu starten"
data-confirm-style="primary">
{{ icon('power', size=14) }} {{ method.label }}
</button>
</form>
{% endif %}
{% if tile.vnc %}
<a class="btn btn--sm" title="Verbindungsdatei für den Bildschirm"
href="{{ url_for('devices.vnc_connection', device_id=tile.id) }}">
{{ icon('monitor', size=14) }} VNC
</a>
{% endif %}
{% if current_user.has('devices.write') and tile.state == 'disabled' %}
<form method="post" action="{{ url_for('devices.toggle_active', device_id=tile.id) }}"
data-no-guard>
@@ -114,6 +93,42 @@
</form>
{% endif %}
</div>
{# Die beiden Eingriffe stehen unten und immer an derselben
Stelle: links der Bildschirm, rechts der Neustart. Wer
eine Reihe Kacheln abarbeitet, trifft denselben Knopf,
ohne hinzusehen -- und verwechselt ihn nicht mit dem
anderen. #}
{% if tile.vnc or tile.restart_methods %}
<div class="tile__actions">
{% if tile.vnc %}
{# Unmittelbar die Verbindung, keine Datei: auf der
Uebersicht will man hin, nicht etwas herunterladen.
Die Verbindungsdatei fuer Viewer ohne registriertes
Schema steht auf der Detailseite. #}
<a class="btn btn--sm" href="{{ tile.vnc }}"
title="Bildschirm öffnen ({{ tile.vnc }})">
{{ icon('monitor', size=14) }} VNC
</a>
{% else %}
<span class="tile__spacer" aria-hidden="true"></span>
{% endif %}
{% if tile.restart_methods %}
{% set method = tile.restart_methods[0] %}
<form method="post" data-no-guard
action="{{ url_for('devices.restart_device', device_id=tile.id) }}">
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
<input type="hidden" name="method" value="{{ method.key }}">
<button class="btn btn--sm" type="submit" title="{{ method.hint }}"
data-busy-label="Startet ..."
data-confirm="{{ tile.name }} über {{ method.label }} neu starten? {{ method.hint }}"
data-confirm-title="Neustart" data-confirm-ok="Neu starten"
data-confirm-style="primary">
{{ icon('power', size=14) }} {{ method.label }}
</button>
</form>
{% endif %}
</div>
{% endif %}
{% endif %}
</article>
{% endfor %}
@@ -17,8 +17,11 @@
</a>
{% endif %}
{% if vnc and vnc.erreichbar %}
<a class="btn btn--sm" href="{{ url_for('devices.vnc_connection', device_id=device.id) }}"
title="Verbindungsdatei für {{ vnc.ziel }}:{{ vnc.port }} – öffnet den VNC-Viewer">
{# Wie auf der Statusübersicht: der Knopf öffnet die Verbindung. Die
Verbindungsdatei steht eine Zeile tiefer in den Stammdaten -- für
Arbeitsplätze, an denen kein Viewer das Schema übernimmt. #}
<a class="btn btn--sm" href="{{ vnc.adresse }}"
title="Bildschirm öffnen ({{ vnc.adresse }})">
{{ icon('monitor', size=15) }} VNC
</a>
{% endif %}
@@ -237,9 +240,10 @@
{% if vnc.erreichbar %}
<a class="mono" href="{{ vnc.adresse }}">{{ vnc.adresse }}</a>
<div class="fs-xs faint">
Öffnet den Viewer unmittelbar, sofern er <code>vnc://</code> übernimmt.
Sonst die
<a href="{{ url_for('devices.vnc_connection', device_id=device.id) }}">
Verbindungsdatei herunterladen</a> – enthält nur Ziel und Namen.
Der Link daneben braucht einen Viewer, der <code>vnc://</code> übernimmt.
Verbindungsdatei</a> – sie enthält nur Ziel und Namen.
</div>
{% else %}
<span class="faint">
@@ -1208,6 +1208,12 @@
--tile-accent: var(--text-faint);
position: relative;
overflow: hidden;
/* Spalte, damit die Fusszeile unten haengen kann (siehe .tile__actions).
Die Kacheln eines Rasters sind gleich hoch, ihr Inhalt aber nicht: mal
fehlt der Switch, mal der Standort. Ohne diesen Halt stuenden die Knoepfe
in jeder Kachel auf einer anderen Hoehe -- und das Auge sucht sie. */
display: flex;
flex-direction: column;
padding: var(--sp-4);
padding-left: calc(var(--sp-4) + 3px);
border: 1px solid var(--border-subtle);
@@ -1269,6 +1275,24 @@
color: var(--text-faint);
}
/* Fusszeile der Kachel: links der Bildschirm, rechts der Neustart.
``margin-top: auto`` schiebt sie an den unteren Rand -- unabhaengig
davon, wie viel darueber steht. Die beiden Eingriffe stehen damit in
jeder Kachel an derselben Stelle, und der eine liegt nicht dort, wo eben
noch der andere war. */
.tile__actions {
display: flex;
align-items: center;
gap: var(--sp-2);
margin-top: auto;
padding-top: var(--sp-3);
}
.tile__actions > :last-child { margin-left: auto; }
/* Steht nur ein Knopf da, bleibt er links -- sonst spraenge der Neustart bei
einem Geraet ohne Bildschirmzugang an die Stelle, an der nebenan VNC
steht. Das Fuellstueck haelt den Platz. */
.tile__spacer { flex: 1 1 auto; }
/* Marke und Karte stehen uebereinander im normalen Fluss. Vorher lag die
Marke absolut positioniert hinter der Karte -- auf einem flachen Fenster
schnitt die Karte mitten durch den Schriftzug und die Funktionszeile. Ein