Version 2.8.22: Alt+F4, Sitzung im Fenster, bewaehrtes Verfahren zuerst
This commit is contained in:
1 parent
a528e9f3b2
commit
af81c92695
14 files changed
+463
-188
No files matched your search
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "tesm"
|
||||
version = "2.8.21"
|
||||
version = "2.8.22"
|
||||
description = "TESM -- Verwaltung und Ueberwachung von PoE-Endgeraeten und Aruba-Switchen"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
|
||||
@@ -14,7 +14,7 @@ from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from flask import Blueprint, abort, render_template, request
|
||||
from flask import Blueprint, abort, render_template, request, url_for
|
||||
from tesm_core.security.sessions import fragment
|
||||
from tesm_core.extension import core, db
|
||||
|
||||
@@ -49,6 +49,13 @@ def _tiles(*, authenticated: bool) -> dict[str, Any]:
|
||||
# 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")
|
||||
# Das Fenster setzt eine frische Passwortbestaetigung voraus -- dieselbe
|
||||
# Schwelle wie die Geraeteseite. Fehlt sie, fuehrt der Knopf dorthin: dort
|
||||
# wird danach gefragt, und ein Fenster, das sich mit "bitte erst
|
||||
# bestaetigen" oeffnet, waere ein Klick fuer nichts.
|
||||
from tesm_core.auth.service import reauth_is_fresh
|
||||
|
||||
vnc_fenster = vnc_erlaubt and reauth_is_fresh()
|
||||
vnc_port = (
|
||||
int(core().settings.get(conn, "vnc_port") or vnc_dienst.STANDARD_PORT)
|
||||
if vnc_erlaubt and core().settings.get(conn, "vnc_enabled")
|
||||
@@ -95,9 +102,16 @@ def _tiles(*, authenticated: bool) -> dict[str, Any]:
|
||||
# Verbindungsversuch je Kachel und Aufbau waere genau die
|
||||
# Art Hintergrundlast, die niemand bestellt hat.
|
||||
"vnc": bool(vnc_port and row["vnc_open"]),
|
||||
"vnc_quelle": (
|
||||
url_for("screen.session_json", device_id=int(row["id"]))
|
||||
if vnc_port and row["vnc_open"]
|
||||
else ""
|
||||
),
|
||||
}
|
||||
)
|
||||
tiles.append(entry)
|
||||
# Merker fuer die Vorlage: nur mit frischer Bestaetigung gibt es das
|
||||
# Fenster; sonst zeigt die Kachel auf die Seite.
|
||||
|
||||
sections = [
|
||||
{
|
||||
@@ -142,6 +156,7 @@ def _tiles(*, authenticated: bool) -> dict[str, Any]:
|
||||
"counts": counts,
|
||||
"sections": sections,
|
||||
"detailed": detailed,
|
||||
"vnc_fenster": vnc_fenster,
|
||||
"interval": int(core().settings.get(conn, "monitor_interval_seconds") or 300),
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,15 @@ import threading
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
from flask import Blueprint, abort, flash, redirect, render_template, url_for
|
||||
from flask import (
|
||||
Blueprint,
|
||||
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, reauth_is_fresh
|
||||
@@ -92,42 +100,51 @@ def screen(device_id: int):
|
||||
abort(404)
|
||||
|
||||
zugang = vnc.zugang_gemerkt(device, port=port)
|
||||
benutzer, passwort = "", ""
|
||||
darf_geheimnis = bool(current_user.has("credentials.secrets"))
|
||||
if device.get("credential_id") and darf_geheimnis:
|
||||
try:
|
||||
benutzer, passwort = inventory.credential_secret(
|
||||
conn, core().keystore, int(device["credential_id"])
|
||||
)
|
||||
except inventory.ValidationError:
|
||||
benutzer, passwort = "", ""
|
||||
if passwort:
|
||||
with core().database.transaction(conn):
|
||||
audit.log(
|
||||
conn,
|
||||
actor=current_user.username,
|
||||
actor_id=current_user.id,
|
||||
action="device.vnc_credentials_used",
|
||||
target=str(device["name"]),
|
||||
detail=(
|
||||
f"Hinterlegte Zugangsdaten ({benutzer}) an die Bildschirmsitzung "
|
||||
"übergeben"
|
||||
),
|
||||
ip=client_ip(),
|
||||
severity="warning",
|
||||
)
|
||||
|
||||
return render_template(
|
||||
"tesm/screen.html",
|
||||
device=device,
|
||||
zugang=zugang,
|
||||
websocket_path=f"/ws/screen/{device_id}",
|
||||
vorgaben={"username": benutzer, "password": passwort},
|
||||
darf_geheimnis=darf_geheimnis,
|
||||
quelle=url_for("screen.session_json", device_id=device_id),
|
||||
darf_geheimnis=bool(current_user.has("credentials.secrets")),
|
||||
verfahren_name=vnc.verfahren_name,
|
||||
)
|
||||
|
||||
|
||||
def _zugangsdaten(conn: Any, device: dict[str, Any]) -> tuple[str, str]:
|
||||
"""Hinterlegte Zugangsdaten -- nur fuer den, der Geheimnisse sehen darf.
|
||||
|
||||
Weil der Browser die Anmeldung fuehrt, muessen sie in die Seite. Wer
|
||||
Geheimnisse nicht sehen darf, bekommt sie auch hier nicht: sonst waere die
|
||||
Bildschirmsitzung der Umweg, auf dem jeder das Passwort liest. Jede
|
||||
Herausgabe steht im Aenderungsprotokoll -- das ist kein Zusehen, sondern
|
||||
ein Zugriff auf etwas, das sonst niemand sieht.
|
||||
"""
|
||||
if not device.get("credential_id") or not current_user.has("credentials.secrets"):
|
||||
return "", ""
|
||||
try:
|
||||
benutzer, passwort = inventory.credential_secret(
|
||||
conn, core().keystore, int(device["credential_id"])
|
||||
)
|
||||
except inventory.ValidationError:
|
||||
return "", ""
|
||||
if passwort:
|
||||
with core().database.transaction(conn):
|
||||
audit.log(
|
||||
conn,
|
||||
actor=current_user.username,
|
||||
actor_id=current_user.id,
|
||||
action="device.vnc_credentials_used",
|
||||
target=str(device["name"]),
|
||||
detail=(
|
||||
f"Hinterlegte Zugangsdaten ({benutzer}) an die Bildschirmsitzung "
|
||||
"übergeben"
|
||||
),
|
||||
ip=client_ip(),
|
||||
severity="warning",
|
||||
)
|
||||
return benutzer, passwort
|
||||
|
||||
|
||||
@bp.post("/monitoring/clients/<int:device_id>/screen/probe")
|
||||
@permission_required("device_vnc.execute")
|
||||
@reauth_required
|
||||
@@ -165,6 +182,73 @@ def probe(device_id: int):
|
||||
return redirect(url_for("screen.screen", device_id=device_id))
|
||||
|
||||
|
||||
@bp.get("/monitoring/clients/<int:device_id>/screen/session")
|
||||
@permission_required("device_vnc.execute")
|
||||
def session_json(device_id: int):
|
||||
"""Alles, was der Client fuer eine Sitzung braucht -- als Auskunft.
|
||||
|
||||
Warum nicht in die Seite geschrieben: dieselbe Sitzung laeuft auch im
|
||||
Fenster der Statusuebersicht, und dort stuenden sonst die Zugangsdaten
|
||||
**jedes** Geraets im Quelltext der Seite. So holt sie sich nur, wer eine
|
||||
Sitzung wirklich oeffnet -- und nur fuer das eine Geraet.
|
||||
|
||||
Die frische Passwortbestaetigung wird hier von Hand geprueft statt ueber
|
||||
den Dekorator: der wuerde auf eine Seite umleiten, und eine Umleitung ist
|
||||
fuer einen Abruf im Hintergrund keine Auskunft. 409 mit einem Hinweis ist
|
||||
eine.
|
||||
"""
|
||||
conn = db()
|
||||
device = _geraet(conn, device_id)
|
||||
if device is None:
|
||||
abort(404)
|
||||
port = _port(conn)
|
||||
if not port:
|
||||
abort(404)
|
||||
if not reauth_is_fresh():
|
||||
return {"reauth": True}, 409
|
||||
|
||||
zugang = vnc.zugang_gemerkt(device, port=port)
|
||||
benutzer, passwort = _zugangsdaten(conn, device)
|
||||
return {
|
||||
"name": str(device["name"]),
|
||||
"socket": f"/ws/screen/{device_id}",
|
||||
"ziel": f"{zugang.ziel}:{zugang.port}",
|
||||
"verfahren": list(zugang.verfahren),
|
||||
"bewaehrt": zugang.bewaehrt,
|
||||
"hinderungsgrund": zugang.hinderungsgrund,
|
||||
"meldeweg": url_for("screen.merke_verfahren", device_id=device_id),
|
||||
"vorgaben": {"username": benutzer, "password": passwort},
|
||||
}
|
||||
|
||||
|
||||
@bp.post("/monitoring/clients/<int:device_id>/screen/method")
|
||||
@permission_required("device_vnc.execute")
|
||||
def merke_verfahren(device_id: int):
|
||||
"""Der Browser meldet, womit die Sitzung zustande kam.
|
||||
|
||||
Nur er weiss es: die Strecke reicht Bytes durch und liest das Protokoll
|
||||
nicht. Beim naechsten Mal faengt die Seite damit an, statt wieder bei der
|
||||
ersten Wahl des Servers -- die hier schon einmal abgerissen ist und deren
|
||||
Absage sich ein sperrender RealVNC Server Minuten Zeit laesst.
|
||||
"""
|
||||
conn = db()
|
||||
device = _geraet(conn, device_id)
|
||||
if device is None:
|
||||
abort(404)
|
||||
try:
|
||||
verfahren = int(request.json.get("verfahren", 0)) if request.json else 0
|
||||
except (TypeError, ValueError):
|
||||
verfahren = 0
|
||||
if not 0 < verfahren < 256:
|
||||
return {"ok": False}, 400
|
||||
|
||||
from ..services import monitor
|
||||
|
||||
with core().database.transaction(conn):
|
||||
monitor.merke_verfahren_das_traegt(conn, int(device["id"]), verfahren)
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
def init_app(app) -> None: # type: ignore[no-untyped-def]
|
||||
"""Meldet Seite und -- falls verfuegbar -- die WebSocket-Strecke an."""
|
||||
app.register_blueprint(bp)
|
||||
@@ -246,6 +330,10 @@ def _strecke(ws, device_id: int) -> None: # type: ignore[no-untyped-def]
|
||||
# Stattdessen ein kurzer Takt: das Lesen laeuft immer wieder in ein
|
||||
# Zeitlimit, und *das* ist kein Abbruch, sondern der Anlass nachzusehen,
|
||||
# ob die Sitzung ueberhaupt noch etwas tut.
|
||||
# Ohne das sammelt der Kern kleine Pakete, bis genug beisammen ist
|
||||
# (Nagle) -- bei einem Bildschirm sind Tastendruck und Mausbewegung genau
|
||||
# solche kleinen Pakete, und das Warten darauf faellt als Ruckeln auf.
|
||||
gegenstelle.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
||||
gegenstelle.settimeout(TAKT)
|
||||
leerlauf = int(extension.settings.get(conn, "vnc_idle_timeout") or 0)
|
||||
|
||||
|
||||
@@ -539,6 +539,19 @@ ALTER TABLE device_status ADD COLUMN vnc_types_at TEXT;
|
||||
"""
|
||||
|
||||
|
||||
# Version 14 ----------------------------------------------------------------
|
||||
#
|
||||
# Welches Anmeldeverfahren zuletzt wirklich getragen hat. Der Server nennt
|
||||
# seine Reihenfolge, und die gilt -- aber wenn seine erste Wahl schon einmal
|
||||
# abgerissen ist und eine andere zum Bild gefuehrt hat, ist das ein Befund.
|
||||
# Ihn zu vergessen kostet bei jedem Oeffnen einen Fehlversuch, und RealVNC
|
||||
# laesst sich mit der Absage Zeit: gemessen zwei bis drei Minuten, bis das
|
||||
# Bild stand.
|
||||
V14 = """
|
||||
ALTER TABLE device_status ADD COLUMN vnc_method INTEGER NOT NULL DEFAULT 0;
|
||||
"""
|
||||
|
||||
|
||||
def uebergang_standardgruppen(conn) -> None: # type: ignore[no-untyped-def]
|
||||
"""Aus sieben Standardgruppen werden drei -- ohne jemandem etwas wegzunehmen.
|
||||
|
||||
@@ -585,4 +598,5 @@ TESM_MIGRATIONS: tuple[Migration, ...] = (
|
||||
Migration(version=11, name="vnc-ziel", sql=V11),
|
||||
Migration(version=12, name="vnc-verfahren", sql=V12),
|
||||
Migration(version=13, name="vnc-absage", sql=V13),
|
||||
Migration(version=14, name="vnc-verfahren-das-traegt", sql=V14),
|
||||
)
|
||||
@@ -545,7 +545,8 @@ def list_devices(
|
||||
st.fail_count, COALESCE(st.vnc_open, 0) AS vnc_open, st.vnc_checked_at,
|
||||
COALESCE(st.vnc_target, '') AS vnc_target,
|
||||
COALESCE(st.vnc_types, '') AS vnc_types,
|
||||
COALESCE(st.vnc_reason, '') AS vnc_reason, st.vnc_types_at
|
||||
COALESCE(st.vnc_reason, '') AS vnc_reason, st.vnc_types_at,
|
||||
COALESCE(st.vnc_method, 0) AS vnc_method
|
||||
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
|
||||
|
||||
@@ -540,6 +540,15 @@ def merke_verfahren(
|
||||
)
|
||||
|
||||
|
||||
def merke_verfahren_das_traegt(
|
||||
conn: sqlite3.Connection, device_id: int, verfahren: int
|
||||
) -> None:
|
||||
"""Haelt fest, womit die letzte Sitzung wirklich zustande kam."""
|
||||
conn.execute(
|
||||
"UPDATE device_status SET vnc_method=? WHERE device_id=?", (int(verfahren), device_id)
|
||||
)
|
||||
|
||||
|
||||
def run_sweep(
|
||||
*,
|
||||
database: Any,
|
||||
|
||||
@@ -64,6 +64,9 @@ class Zugang:
|
||||
#: Der Text, mit dem der Server die Verbindung abgelehnt hat -- er nennt
|
||||
#: dann kein Verfahren, sondern eine 0 und einen Grund.
|
||||
absage: str = ""
|
||||
#: Das Verfahren, mit dem zuletzt wirklich eine Sitzung zustande kam
|
||||
#: (0: noch keine). Damit faengt der naechste Versuch an.
|
||||
bewaehrt: int = 0
|
||||
|
||||
@property
|
||||
def browsertauglich(self) -> bool:
|
||||
@@ -350,6 +353,7 @@ def zugang_gemerkt(device: Mapping[str, Any], *, port: int = STANDARD_PORT) -> Z
|
||||
int(teil) for teil in gespeichert.split(",") if teil.strip().isdigit()
|
||||
),
|
||||
absage=str(device.get("vnc_reason") or ""),
|
||||
bewaehrt=int(device.get("vnc_method") or 0),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -102,11 +102,25 @@
|
||||
<div class="tile__actions">
|
||||
{% if tile.vnc %}
|
||||
{# Die Sitzung laeuft in TESM selbst: ein Klick, kein
|
||||
Viewer auf dem Arbeitsplatz, keine Datei. #}
|
||||
<a class="btn btn--sm" href="{{ url_for('screen.screen', device_id=tile.id) }}"
|
||||
title="Bildschirm im Browser öffnen">
|
||||
{{ icon('monitor', size=14) }} VNC
|
||||
</a>
|
||||
Viewer auf dem Arbeitsplatz, keine Datei.
|
||||
|
||||
Mit frischer Passwortbestätigung öffnet sie sich hier
|
||||
im Fenster -- man bleibt bei der Übersicht und sieht
|
||||
die anderen Geräte weiter. Ohne sie führt der Knopf
|
||||
auf die Geräteseite, wo danach gefragt wird. #}
|
||||
{% if vnc_fenster %}
|
||||
<button class="btn btn--sm" type="button"
|
||||
data-vnc-oeffnen="{{ tile.vnc_quelle }}"
|
||||
data-vnc-name="{{ tile.name }}"
|
||||
title="Bildschirm im Fenster öffnen">
|
||||
{{ icon('monitor', size=14) }} VNC
|
||||
</button>
|
||||
{% else %}
|
||||
<a class="btn btn--sm" href="{{ url_for('screen.screen', device_id=tile.id) }}"
|
||||
title="Bildschirm im Browser öffnen">
|
||||
{{ icon('monitor', size=14) }} VNC
|
||||
</a>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="tile__spacer" aria-hidden="true"></span>
|
||||
{% endif %}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
{# Die Bildschirmfläche -- einmal geschrieben, an zwei Orten benutzt: auf der
|
||||
Geräteseite und im Fenster der Statusübersicht.
|
||||
|
||||
Die Teile finden sich über `data-rolle`, nicht über Kennungen: im Fenster
|
||||
gäbe es sie sonst zweimal auf derselben Seite, und die zweite wäre die, die
|
||||
niemand findet. #}
|
||||
{% macro vnc_flaeche(quelle='') %}
|
||||
<div class="screen" {% if quelle %}data-sitzung="{{ quelle }}"{% endif %}>
|
||||
<div data-rolle="leinwand" class="screen__canvas" hidden></div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro vnc_status() %}
|
||||
<div data-rolle="status" class="screen__status">Wird vorbereitet …</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro vnc_zugangsdaten() %}
|
||||
<form data-rolle="zugangsdaten" class="row row--tight" hidden onsubmit="return false">
|
||||
<label class="field">
|
||||
<span class="field__label">Benutzer</span>
|
||||
<input class="input" type="text" data-rolle="benutzer" autocomplete="off">
|
||||
</label>
|
||||
<label class="field">
|
||||
<span class="field__label">Passwort</span>
|
||||
<input class="input" type="password" data-rolle="passwort" autocomplete="off">
|
||||
</label>
|
||||
<button class="btn btn--primary" type="button" data-rolle="anmelden">Anmelden</button>
|
||||
</form>
|
||||
{% endmacro %}
|
||||
|
||||
{# Tasten, die der Arbeitsplatz selbst abfängt und die deshalb einen Knopf
|
||||
brauchen: Strg+Alt+Entf schaltet der Browser gar nicht erst durch, Alt+F4
|
||||
schlösse das Fenster hier statt das Fenster dort. #}
|
||||
{% macro vnc_tasten() %}
|
||||
<button class="btn btn--sm" type="button" data-rolle="taste-cad" hidden
|
||||
title="Strg+Alt+Entf an das Gerät senden">Strg+Alt+Entf</button>
|
||||
<button class="btn btn--sm" type="button" data-rolle="taste-altf4" hidden
|
||||
title="Alt+F4 an das Gerät senden – schließt dort das oberste Fenster">Alt+F4</button>
|
||||
<button class="btn btn--sm" type="button" data-rolle="trennen" hidden>Trennen</button>
|
||||
{% endmacro %}
|
||||
@@ -1,5 +1,6 @@
|
||||
{% extends "tesm_core/base.html" %}
|
||||
{% from "tesm_core/macros.html" import search_box %}
|
||||
{% from "tesm_core/macros.html" import search_box, modal %}
|
||||
{% from "tesm/_vnc_flaeche.html" import vnc_flaeche, vnc_status, vnc_zugangsdaten, vnc_tasten %}
|
||||
|
||||
{% block page_title %}Statusübersicht{% endblock %}
|
||||
{% block page_sub %}
|
||||
@@ -19,4 +20,27 @@
|
||||
data-refresh-interval="20000">
|
||||
{% include "tesm/_dashboard_tiles.html" %}
|
||||
</div>
|
||||
|
||||
{% if vnc_fenster %}
|
||||
{# Ein Fenster für alle Kacheln: welches Gerät gemeint ist, bringt der Knopf
|
||||
beim Öffnen mit. Ein Dialog je Kachel wäre dieselbe Sache dutzendfach im
|
||||
Quelltext -- und dutzendfach eine Sitzung, die niemand aufräumt. #}
|
||||
{% call modal('vnc-fenster', 'Bildschirm', 'Tastatur und Maus gehen an das Gerät. Die Zwischenablage bleibt getrennt.', 'wide') %}
|
||||
<div class="modal__body stack" data-rolle="behaelter">
|
||||
<div class="row row--tight">
|
||||
<strong data-rolle="titel" class="flex-1"></strong>
|
||||
{{ vnc_tasten() }}
|
||||
</div>
|
||||
{{ vnc_status() }}
|
||||
{{ vnc_zugangsdaten() }}
|
||||
{{ vnc_flaeche() }}
|
||||
</div>
|
||||
{% endcall %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
{% if vnc_fenster %}
|
||||
<script type="module" src="{{ asset_url('js/screen.js') }}" nonce="{{ csp_nonce }}"></script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -1,5 +1,6 @@
|
||||
{% extends "tesm_core/base.html" %}
|
||||
{% from "tesm_core/macros.html" import alert, pill %}
|
||||
{% from "tesm/_vnc_flaeche.html" import vnc_flaeche, vnc_status, vnc_zugangsdaten, vnc_tasten %}
|
||||
|
||||
{% block page_title %}Bildschirm: {{ device.name }}{% endblock %}
|
||||
{% block page_sub %}{{ zugang.ziel }}:{{ zugang.port }}{% endblock %}
|
||||
@@ -13,13 +14,6 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{# Die Zugangsdaten stehen in einem eigenen JSON-Block statt in einem
|
||||
data-Attribut: ein Attribut steht im Quelltext der Seite zwischen der
|
||||
Beschriftung und wird beim Kopieren einer Zeile mitgenommen. Beides ist
|
||||
derselbe Inhalt -- der Unterschied ist, wie leicht er einem versehentlich
|
||||
unter die Augen kommt. #}
|
||||
<script type="application/json" id="screen-credentials-data">{{ vorgaben | tojson }}</script>
|
||||
|
||||
<div class="stack">
|
||||
{% if zugang.hinderungsgrund %}
|
||||
{{ alert(zugang.hinderungsgrund, 'warning') }}
|
||||
@@ -49,9 +43,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="row row--tight">
|
||||
<button class="btn btn--sm" type="button" id="screen-cad" hidden
|
||||
title="Strg+Alt+Entf an das Gerät senden">Strg+Alt+Entf</button>
|
||||
<button class="btn btn--sm" type="button" id="screen-disconnect" hidden>Trennen</button>
|
||||
{{ vnc_tasten() }}
|
||||
{# Ausdrücklich ein Knopf und keine Nebenwirkung des Seitenaufbaus: die
|
||||
Prüfung bricht eine Protokollverhandlung ab, und RealVNC zählt das.
|
||||
Wer sucht, drückt ihn; wer nur arbeiten will, merkt nichts davon. #}
|
||||
@@ -70,30 +62,10 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card__body stack">
|
||||
<div id="screen-status" class="screen__status">Wird vorbereitet …</div>
|
||||
|
||||
<form id="screen-credentials" class="row row--tight" hidden onsubmit="return false">
|
||||
<label class="field">
|
||||
<span class="field__label">Benutzer</span>
|
||||
<input class="input" type="text" id="screen-username" autocomplete="off"
|
||||
value="{{ vorgaben.username }}">
|
||||
</label>
|
||||
<label class="field">
|
||||
<span class="field__label">Passwort</span>
|
||||
<input class="input" type="password" id="screen-password" autocomplete="off">
|
||||
</label>
|
||||
<button class="btn btn--primary" type="button" id="screen-send">Anmelden</button>
|
||||
</form>
|
||||
|
||||
<div id="screen" class="screen"
|
||||
data-socket="{{ websocket_path }}"
|
||||
data-ziel="{{ zugang.ziel }}:{{ zugang.port }}"
|
||||
data-verfahren="{{ zugang.verfahren | join(',') }}"
|
||||
data-blockiert="{{ '1' if zugang.hinderungsgrund else '0' }}"
|
||||
data-grund="{{ zugang.hinderungsgrund }}">
|
||||
<div id="screen-canvas" class="screen__canvas" hidden></div>
|
||||
</div>
|
||||
<div class="card__body stack" id="screen" data-sitzung="{{ quelle }}">
|
||||
{{ vnc_status() }}
|
||||
{{ vnc_zugangsdaten() }}
|
||||
{{ vnc_flaeche() }}
|
||||
|
||||
{% if not darf_geheimnis and device.credential_id %}
|
||||
<p class="fs-xs faint">
|
||||
|
||||
Reference in new issue
Block a user