From 4f4310bfe47d2bc1823a4f690d4229e2488154de Mon Sep 17 00:00:00 2001 From: Tim Eertmoed Date: Sun, 13 Sep 2026 09:49:27 +0200 Subject: [PATCH] Version 2.8.9: VNC-Zugang, wo ein VNC-Server antwortet --- apps/tesm/pyproject.toml | 2 +- apps/tesm/src/tesm/blueprints/dashboard.py | 6 + apps/tesm/src/tesm/blueprints/devices.py | 76 +++++++++- apps/tesm/src/tesm/blueprints/settings.py | 7 +- apps/tesm/src/tesm/cli.py | 3 +- apps/tesm/src/tesm/permissions.py | 21 +++ apps/tesm/src/tesm/schema.py | 14 ++ apps/tesm/src/tesm/services/inventory.py | 2 +- apps/tesm/src/tesm/services/monitor.py | 31 +++- apps/tesm/src/tesm/services/vnc.py | 133 ++++++++++++++++++ apps/tesm/src/tesm/settings_spec.py | 21 +++ .../tesm/templates/tesm/_dashboard_tiles.html | 6 + .../tesm/templates/tesm/device_detail.html | 24 ++++ .../tesm/templates/tesm/settings_system.html | 7 + apps/tesm/src/tesm/worker.py | 3 +- 15 files changed, 347 insertions(+), 9 deletions(-) create mode 100644 apps/tesm/src/tesm/services/vnc.py diff --git a/apps/tesm/pyproject.toml b/apps/tesm/pyproject.toml index 16e92f3..6276915 100644 --- a/apps/tesm/pyproject.toml +++ b/apps/tesm/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "tesm" -version = "2.8.8" +version = "2.8.9" description = "TESM -- Verwaltung und Ueberwachung von PoE-Endgeraeten und Aruba-Switchen" requires-python = ">=3.11" dependencies = [ diff --git a/apps/tesm/src/tesm/blueprints/dashboard.py b/apps/tesm/src/tesm/blueprints/dashboard.py index 98ffe0c..28efccd 100644 --- a/apps/tesm/src/tesm/blueprints/dashboard.py +++ b/apps/tesm/src/tesm/blueprints/dashboard.py @@ -74,6 +74,12 @@ 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"), } ) tiles.append(entry) diff --git a/apps/tesm/src/tesm/blueprints/devices.py b/apps/tesm/src/tesm/blueprints/devices.py index 756c134..8b0f692 100644 --- a/apps/tesm/src/tesm/blueprints/devices.py +++ b/apps/tesm/src/tesm/blueprints/devices.py @@ -11,14 +11,23 @@ from __future__ import annotations from typing import Any -from flask import Blueprint, abort, flash, redirect, render_template, request, url_for +from flask import ( + Blueprint, + Response, + abort, + flash, + redirect, + render_template, + request, + url_for, +) from tesm_core import audit from tesm_core.auth.models import current_user, permission_required, reauth_required from tesm_core.auth.service import client_ip from tesm_core.db import Database from tesm_core.extension import core, db, transaction -from ..services import inventory, monitor, restart, ssh +from ..services import inventory, monitor, restart, ssh, vnc bp = Blueprint("devices", __name__, url_prefix="/monitoring/clients") @@ -360,6 +369,69 @@ def detail(device_id: int): host_keys=host_keys, method_labels=monitor.METHOD_LABELS, terminal_enabled=bool(extension.settings.get(conn, "terminal_clients_enabled")), + vnc=_vnc_zugang(conn, device), + ) + + +def _vnc_zugang(conn: Any, device: dict[str, Any]) -> vnc.Zugang | None: + """Der Bildschirmzugriff dieses Clients -- frisch gefragt, nicht geraten. + + Die Kacheln der Statusuebersicht nehmen den Stand aus dem letzten + Ueberwachungsdurchlauf; hier geht es um **ein** Geraet, also fragt die Seite + selbst nach (ein halbe Sekunde Zeitlimit, und nur wenn der Client gerade + erreichbar ist). Das Ergebnis wird gleich festgehalten -- dann stimmt auch + die Kachel, ohne auf den naechsten Durchlauf zu warten. + """ + extension = core() + if not current_user.has("device_vnc.execute"): + return None + if not extension.settings.get(conn, "vnc_enabled"): + return None + port = int(extension.settings.get(conn, "vnc_port") or vnc.STANDARD_PORT) + online = str(device.get("state") or "") == "online" and not device.get("deleted_at") + zugang = vnc.zugang(device, port=port, pruefen=online) + if online: + with transaction(): + monitor.merke_vnc(conn, int(device["id"]), zugang.erreichbar) + return zugang + + +@bp.get("//vnc") +@permission_required("device_vnc.execute") +def vnc_connection(device_id: int): + """Die Verbindungsdatei -- drei Zeilen, sonst nichts. + + Kein Passwort, kein Benutzername, keine Verschluesselungsvorgabe: was beim + Verbinden verlangt wird, entscheidet der VNC-Server auf dem Geraet. Die + Datei traegt nur, wohin es geht und wie die Verbindung im Viewer heisst. + """ + conn = db() + device = inventory.get_device(conn, device_id) + if device is None: + abort(404) + extension = core() + if not extension.settings.get(conn, "vnc_enabled"): + abort(404) + port = int(extension.settings.get(conn, "vnc_port") or vnc.STANDARD_PORT) + ziel = vnc.ziel(device) + if not ziel: + abort(404) + with transaction(): + _audit( + conn, + "device.vnc_opened", + str(device["name"]), + f"Verbindungsdatei für {ziel}:{port}", + ) + return Response( + vnc.verbindungsdatei(device, port=port), + mimetype="application/octet-stream", + headers={ + "Content-Disposition": f'attachment; filename="{vnc.dateiname(device)}"', + # Eine Verbindungsdatei ist kein Dokument, das ein Proxy aufheben + # soll -- und sie ist billig genug, sie jedes Mal neu zu erzeugen. + "Cache-Control": "no-store", + }, ) diff --git a/apps/tesm/src/tesm/blueprints/settings.py b/apps/tesm/src/tesm/blueprints/settings.py index c76afac..b678021 100644 --- a/apps/tesm/src/tesm/blueprints/settings.py +++ b/apps/tesm/src/tesm/blueprints/settings.py @@ -103,6 +103,8 @@ def system(): "monitor_failures_before_restart", "monitor_auto_restart", "dashboard_public", + "vnc_enabled", + "vnc_port", "terminal_switches_enabled", "terminal_clients_enabled", "terminal_idle_timeout", @@ -143,6 +145,8 @@ def system_save(): "monitor_failures_before_restart", "monitor_auto_restart", "dashboard_public", + "vnc_enabled", + "vnc_port", "terminal_switches_enabled", "terminal_clients_enabled", "terminal_idle_timeout", @@ -305,7 +309,7 @@ def run_check_now(): extension = core() def worker() -> None: - from ..services import inventory, monitor + from ..services import inventory, monitor, vnc allowed = extension.license is None or extension.license.feature_allowed( "auto_poe_restart" @@ -315,6 +319,7 @@ def run_check_now(): live=extension.registry["live_log"], keystore=extension.keystore, license_allows_auto_restart=allowed, + vnc_port=vnc.port_aus_einstellungen(extension), ) threading.Thread(target=worker, name="manual-sweep", daemon=True).start() diff --git a/apps/tesm/src/tesm/cli.py b/apps/tesm/src/tesm/cli.py index d5f156d..489562d 100644 --- a/apps/tesm/src/tesm/cli.py +++ b/apps/tesm/src/tesm/cli.py @@ -219,7 +219,7 @@ def cmd_restore_signatures(_args: argparse.Namespace) -> int: def cmd_check_now(_args: argparse.Namespace) -> int: app, extension = _app_and_core() - from .services import inventory, monitor + from .services import inventory, monitor, vnc with app.app_context(): allowed = extension.license is None or extension.license.feature_allowed( @@ -230,6 +230,7 @@ def cmd_check_now(_args: argparse.Namespace) -> int: live=extension.registry["live_log"], keystore=extension.keystore, license_allows_auto_restart=allowed, + vnc_port=vnc.port_aus_einstellungen(extension), ) print( f"{report.checked} geprüft, {report.online} online, {report.offline} offline, " diff --git a/apps/tesm/src/tesm/permissions.py b/apps/tesm/src/tesm/permissions.py index ff75ed9..683d354 100644 --- a/apps/tesm/src/tesm/permissions.py +++ b/apps/tesm/src/tesm/permissions.py @@ -78,6 +78,26 @@ DEVICE_RESTART_RPC = Resource( action_hints={A.EXECUTE: "Neustartauftrag anlegen (wie 'shutdown /m')."}, ) +#: Bildschirmzugriff ist **nicht** dasselbe wie die Konsole, auch wenn beides +#: "auf das Geraet" fuehrt: hier sieht jemand den Bildschirm, wie er ist -- +#: samt dem, was ein Mensch dort gerade offen hat. Das ist eine eigene +#: Entscheidung, also eine eigene Zeile. Sie wird **niemandem** mitgegeben: +#: keine Migration setzt sie, auch nicht bei denen, die schon eine Konsole +#: oeffnen duerfen. Wer sie haben soll, bekommt sie in der Matrix -- einmal, +#: sichtbar, mit Zeile im Aenderungsprotokoll. +DEVICE_VNC = Resource( + key="device_vnc", + parent="devices", + label="Clients: Bildschirm (VNC)", + actions=(A.READ, A.EXECUTE), + hint="Verbindungsdatei bzw. vnc://-Adresse für den Bildschirm des Clients", + action_hints={ + A.READ: "Den Knopf sehen, wo ein VNC-Server antwortet.", + A.EXECUTE: "Verbindung öffnen. Die Datei enthält nur Ziel und Namen – " + "was beim Verbinden verlangt wird, entscheidet der VNC-Server auf dem Gerät.", + }, +) + #: Die Client-Konsole lief unter ``maintenance.execute`` mit -- demselben Recht #: wie die Paketaktualisierung. Eine Sitzung auf einem Geraet ist aber etwas #: anderes als ein Auftrag, den der Server abarbeitet: hier tippt ein Mensch. @@ -316,6 +336,7 @@ MONITORING_AREA = Area( DEVICE_RESTART_SSH, DEVICE_RESTART_RPC, DEVICE_CONSOLE, + DEVICE_VNC, SWITCHES, SWITCH_CONSOLE, HOSTKEYS, diff --git a/apps/tesm/src/tesm/schema.py b/apps/tesm/src/tesm/schema.py index e94a7e6..b2c315a 100644 --- a/apps/tesm/src/tesm/schema.py +++ b/apps/tesm/src/tesm/schema.py @@ -490,6 +490,19 @@ def eingefrorene_vorlagen( return vorlagen +# Version 10 ---------------------------------------------------------------- +# +# Wo antwortet ein VNC-Server? Die Frage beantwortet der Ueberwachungs- +# durchlauf mit, der ohnehin jedes Geraet anfasst -- einmal je Intervall statt +# einmal je Seitenaufruf. Die Kacheln der Statusuebersicht koennen den Knopf +# damit zeigen, ohne dass das Rendern der Seite ein Dutzend Verbindungen +# aufbaut. +V10 = """ +ALTER TABLE device_status ADD COLUMN vnc_open INTEGER NOT NULL DEFAULT 0; +ALTER TABLE device_status ADD COLUMN vnc_checked_at TEXT; +""" + + def uebergang_standardgruppen(conn) -> None: # type: ignore[no-untyped-def] """Aus sieben Standardgruppen werden drei -- ohne jemandem etwas wegzunehmen. @@ -532,4 +545,5 @@ TESM_MIGRATIONS: tuple[Migration, ...] = ( Migration(version=7, name="signaturverzeichnis", sql=V7, fn=uebernehme_signaturen), 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), ) diff --git a/apps/tesm/src/tesm/services/inventory.py b/apps/tesm/src/tesm/services/inventory.py index 539d51b..1311df1 100644 --- a/apps/tesm/src/tesm/services/inventory.py +++ b/apps/tesm/src/tesm/services/inventory.py @@ -542,7 +542,7 @@ 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 + st.fail_count, COALESCE(st.vnc_open, 0) AS vnc_open, st.vnc_checked_at 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 diff --git a/apps/tesm/src/tesm/services/monitor.py b/apps/tesm/src/tesm/services/monitor.py index e08bb5e..1727bda 100644 --- a/apps/tesm/src/tesm/services/monitor.py +++ b/apps/tesm/src/tesm/services/monitor.py @@ -507,6 +507,14 @@ 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.""" + conn.execute( + "UPDATE device_status SET vnc_open=?, vnc_checked_at=? WHERE device_id=?", + (1 if offen else 0, _now(), device_id), + ) + + def run_sweep( *, database: Any, @@ -514,13 +522,21 @@ def run_sweep( keystore: Any, license_allows_auto_restart: bool, failures_before_restart: int = DEFAULT_FAILURES_BEFORE_RESTART, + vnc_port: int = 0, ) -> SweepReport: """Ein vollstaendiger Ueberwachungsdurchlauf. Die Pruefungen laufen parallel (I/O-gebunden), die Datenbankschreibvorgaenge danach in **einer** Transaktion -- so gibt es keinen Schreib-Wettlauf und keine halb geschriebenen Durchlaeufe. + + ``vnc_port`` (0 = aus) haengt eine zweite, sehr kurze Frage an jedes + erreichbare Geraet: nimmt dort ein VNC-Server Verbindungen an? Sie gehoert + hierher und nicht in den Seitenaufbau -- einmal je Intervall statt einmal + je Aufruf, und nur fuer Geraete, die ohnehin gerade antworten. """ + from . import vnc as vnc_dienst # spaet: vnc benutzt tcp_reachable von hier + report = SweepReport() started = time.monotonic() @@ -531,15 +547,26 @@ 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]: + 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) + with ThreadPoolExecutor(max_workers=min(MAX_PARALLEL_CHECKS, len(devices))) as pool: - results = list(pool.map(lambda device: (device, ping(str(device["ip"]))), devices)) + results = list(pool.map(pruefen, devices)) restart_candidates: list[dict[str, Any]] = [] with database.session() as conn: with database.transaction(conn): - for device, result in results: + for device, result, vnc_offen in results: 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) if result.ok: report.online += 1 else: diff --git a/apps/tesm/src/tesm/services/vnc.py b/apps/tesm/src/tesm/services/vnc.py new file mode 100644 index 0000000..e385118 --- /dev/null +++ b/apps/tesm/src/tesm/services/vnc.py @@ -0,0 +1,133 @@ +"""Bildschirmzugriff per VNC: Ziel bestimmen, Erreichbarkeit pruefen, Datei bauen. + +Auf den HAPs (Raspberry Pis) laeuft ein VNC-Server. TESM kennt von jedem +Client Namen und Adresse -- daraus laesst sich die Verbindung erzeugen, statt +sie in jedem Viewer von Hand zu pflegen. + +Zwei Wege, beide aus denselben zwei Angaben: + +* eine **Verbindungsdatei** (``.vnc``), wie der RealVNC Viewer sie schreibt. + Sie enthaelt genau drei Zeilen -- ``ConnMethod``, ``FriendlyName``, ``Host`` + -- und **nichts weiter**. Jede zusaetzliche Angabe ist eine Gelegenheit, + etwas zu verlangen, das hier niemand verlangen soll: keine + Verschluesselungsvorgabe, keine Identitaetspruefung, kein Benutzername, kein + Passwortfeld. Was am Ende gefragt wird, entscheidet ohnehin der VNC-Server + auf dem Geraet; die Datei fragt von sich aus nichts. +* eine **Adresse** (``vnc://host``) fuer den Fall, dass der Viewer als + Handler registriert ist -- ein Klick statt Herunterladen und Oeffnen. + +Der Zugang erscheint nur dort, wo wirklich ein VNC-Server antwortet +(:func:`erreichbar`). Das ist der Unterschied zu einem Schalter am Geraet: +niemand muss ihn pflegen, und er zeigt nie ins Leere. +""" + +from __future__ import annotations + +import re +from dataclasses import dataclass +from typing import Any, Mapping + +from .monitor import tcp_reachable + +#: Vorgabeport von VNC (Anzeige :0). Ueberschreibbar in den Einstellungen. +STANDARD_PORT = 5900 + +#: So kurz, dass die Detailseite es nicht merkt, und so lang, dass ein Geraet +#: im selben Netz antworten kann. Gefragt wird ohnehin nur, wenn der Client +#: gerade als erreichbar gilt. +ZEITLIMIT = 0.6 + +#: Ein Name, den ein Rechner aufloesen kann: Buchstaben, Ziffern, Bindestrich, +#: Punkt als Trenner. "HAP 02" oder "Kamera Eingang" ist keiner -- dann bleibt +#: die IP-Adresse, die im Datensatz ohnehin steht. +HOSTNAME_RE = re.compile(r"^(?!-)[A-Za-z0-9-]{1,63}(? str: + """``vnc://host`` -- den Standardport schreibt niemand dazu.""" + if self.port == STANDARD_PORT: + return f"vnc://{self.ziel}" + return f"vnc://{self.ziel}:{self.port}" + + +def ziel(device: Mapping[str, Any]) -> str: + """Der Name des Clients, sonst seine IP-Adresse. + + Der Name ist die bessere Wahl, wo er aufloest: er ueberlebt einen + Adresswechsel. Taugt er nicht als Hostname -- weil er ein Leerzeichen + enthaelt oder ein Umlaut darin steht --, waere ein Link darauf sicher + falsch; dann ist die Adresse richtig. + """ + name = str(device.get("name") or "").strip() + if name and HOSTNAME_RE.match(name): + return name.lower() + return str(device.get("ip") or "").strip() + + +def erreichbar(host: str, port: int = STANDARD_PORT) -> bool: + """Nimmt auf diesem Host ein VNC-Server Verbindungen an?""" + if not host: + return False + return tcp_reachable(host, port, timeout=ZEITLIMIT) + + +def zugang(device: Mapping[str, Any], *, port: int = STANDARD_PORT, pruefen: bool = True) -> Zugang: + """Ziel und Erreichbarkeit in einem Stueck -- so nimmt es die Vorlage.""" + host = ziel(device) + return Zugang(ziel=host, port=port, erreichbar=erreichbar(host, port) if pruefen else False) + + +def verbindungsdatei(device: Mapping[str, Any], *, port: int = STANDARD_PORT) -> str: + """Der Inhalt der ``.vnc``-Datei -- drei Zeilen, keine vierte. + + ``ConnMethod=tcp`` ist die direkte Verbindung (nicht ueber den Cloud-Dienst + von RealVNC), ``FriendlyName`` der Name, den der Viewer anzeigt, ``Host`` + das Ziel. Ein abweichender Port gehoert in dieser Datei an den Host, mit + Doppelpunkt -- einen eigenen Schluessel dafuer gibt es nicht. + """ + host = ziel(device) + if port != STANDARD_PORT: + host = f"{host}:{port}" + zeilen = [ + "ConnMethod=tcp", + f"FriendlyName={str(device.get('name') or host).strip()}", + f"Host={host}", + ] + return ZEILE.join(zeilen) + ZEILE + + +def dateiname(device: Mapping[str, Any]) -> str: + """``HAP02.vnc`` -- aus dem Namen, auf Unbedenkliches beschraenkt. + + Auch der Punkt faellt weg, nicht nur der Schraegstrich: ein Geraetename ist + Freitext, und ein Dateiname, in dem ".." vorkommen kann, ist eine Frage, + die man sich gar nicht erst stellen muss. + """ + name = re.sub(r"[^A-Za-z0-9_-]+", "-", str(device.get("name") or "client")).strip("-") + return f"{name or 'client'}.vnc" + + +def port_aus_einstellungen(extension: Any) -> int: + """Der eingestellte Port -- oder 0, wenn der Zugang abgeschaltet ist. + + Nimmt die Erweiterung statt der Verbindung, weil der Ueberwachungsprozess + ausserhalb eines Anfragekontextes laeuft und seine eigene Sitzung oeffnet. + """ + with extension.database.session() as conn: + if not extension.settings.get(conn, "vnc_enabled"): + return 0 + return int(extension.settings.get(conn, "vnc_port") or STANDARD_PORT) diff --git a/apps/tesm/src/tesm/settings_spec.py b/apps/tesm/src/tesm/settings_spec.py index acd260c..3676f9d 100644 --- a/apps/tesm/src/tesm/settings_spec.py +++ b/apps/tesm/src/tesm/settings_spec.py @@ -45,6 +45,27 @@ TESM_SETTINGS: tuple[SettingSpec, ...] = ( "IP-Adressen, keine Switch-Zuordnung, keine Verwaltungsfunktionen.", section="monitoring", ), + # -- Bildschirmzugriff (VNC) -------------------------------------------- # + SettingSpec( + "vnc_enabled", + True, + "bool", + "Bildschirmzugriff (VNC) anbieten", + hint="Der Überwachungsdurchlauf fragt bei jedem erreichbaren Client kurz, ob " + "auf diesem Port ein VNC-Server antwortet; nur dort erscheint der Knopf. " + "Sehen darf ihn trotzdem nur, wer das Recht dafür hat.", + section="monitoring", + ), + SettingSpec( + "vnc_port", + 5900, + "int", + "VNC-Port", + hint="5900 ist Anzeige :0 – die Vorgabe eines Raspberry Pi.", + section="monitoring", + minimum=1, + maximum=65535, + ), # -- DHCP --------------------------------------------------------------- # SettingSpec("dhcp_domain", "local", "str", "Domainname", section="dhcp"), SettingSpec( diff --git a/apps/tesm/src/tesm/templates/tesm/_dashboard_tiles.html b/apps/tesm/src/tesm/templates/tesm/_dashboard_tiles.html index 7207793..5382c02 100644 --- a/apps/tesm/src/tesm/templates/tesm/_dashboard_tiles.html +++ b/apps/tesm/src/tesm/templates/tesm/_dashboard_tiles.html @@ -99,6 +99,12 @@ {% endif %} + {% if tile.vnc %} + + {{ icon('monitor', size=14) }} VNC + + {% endif %} {% if current_user.has('devices.write') and tile.state == 'disabled' %}
diff --git a/apps/tesm/src/tesm/templates/tesm/device_detail.html b/apps/tesm/src/tesm/templates/tesm/device_detail.html index 25ef36b..c287126 100644 --- a/apps/tesm/src/tesm/templates/tesm/device_detail.html +++ b/apps/tesm/src/tesm/templates/tesm/device_detail.html @@ -16,6 +16,12 @@ {{ icon('terminal', size=15) }} Konsole {% endif %} + {% if vnc and vnc.erreichbar %} + + {{ icon('monitor', size=15) }} VNC + + {% endif %} {% if restart_methods %} {% for method in restart_methods %} Kategorie
{{ device.category or '–' | safe }}
Standort
{{ device.location or '–' | safe }}
Zugangsdaten
{{ device.credential_name or 'keine' }}
+ {% if vnc %} +
Bildschirm (VNC)
+
+ {% if vnc.erreichbar %} + {{ vnc.adresse }} +
+ + Verbindungsdatei herunterladen – enthält nur Ziel und Namen. + Der Link daneben braucht einen Viewer, der vnc:// übernimmt. +
+ {% else %} + + kein VNC-Server auf {{ vnc.ziel }}:{{ vnc.port }} + {%- if device.state != 'online' %} (Client antwortet gerade nicht){% endif %} + + {% endif %} +
+ {% endif %}
Automatischer Neustart
{{ device.auto_restart | yes_no }}
Angelegt
{{ device.created_at | dt }}
Geändert
diff --git a/apps/tesm/src/tesm/templates/tesm/settings_system.html b/apps/tesm/src/tesm/templates/tesm/settings_system.html index 19c5f10..c5b9f6a 100644 --- a/apps/tesm/src/tesm/templates/tesm/settings_system.html +++ b/apps/tesm/src/tesm/templates/tesm/settings_system.html @@ -46,6 +46,13 @@ {{ checkbox('dashboard_public', 'Statusübersicht ohne Anmeldung erreichbar', checked=values.dashboard_public, hint_text=specs['dashboard_public'].hint) }} + {{ checkbox('vnc_enabled', 'Bildschirmzugriff (VNC) anbieten', + checked=values.vnc_enabled, + hint_text=specs['vnc_enabled'].hint) }} +
+ {{ field('vnc_port', 'VNC-Port', value=values.vnc_port, type='number', + hint_text=specs['vnc_port'].hint) }} +
diff --git a/apps/tesm/src/tesm/worker.py b/apps/tesm/src/tesm/worker.py index d546735..52cd481 100644 --- a/apps/tesm/src/tesm/worker.py +++ b/apps/tesm/src/tesm/worker.py @@ -22,7 +22,7 @@ from types import FrameType from tesm_core.extension import core from . import create_app -from .services import inventory, monitor +from .services import inventory, monitor, vnc logger = logging.getLogger("tesm.monitor") @@ -82,6 +82,7 @@ class MonitorDaemon: keystore=self.extension.keystore, license_allows_auto_restart=self._auto_restart_allowed(), failures_before_restart=self._failures_before_restart(), + vnc_port=vnc.port_aus_einstellungen(self.extension), ) logger.info( "Durchlauf: %s geprüft, %s online, %s offline, %s Neustarts (%s erfolgreich), "