Version 2.8.22: Alt+F4, Sitzung im Fenster, bewaehrtes Verfahren zuerst

This commit is contained in:
alientim committed 2026-09-13 14:30:39 +02:00
1 parent 4c9ed20da4
commit 1c90251b0f
3 files changed
+206 -112

No files matched your search

+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "tesm-core" name = "tesm-core"
version = "2.8.11" version = "2.8.12"
description = "Gemeinsames Anwendungsgeruest (Auth, RBAC, Navigation, Design-System, Migrationen) fuer TESM und TESM-Lizenzserver" description = "Gemeinsames Anwendungsgeruest (Auth, RBAC, Navigation, Design-System, Migrationen) fuer TESM und TESM-Lizenzserver"
requires-python = ">=3.11" requires-python = ">=3.11"
dependencies = [ dependencies = [
@@ -1295,6 +1295,9 @@
sprengen; noVNC skaliert das Bild hinein. */ sprengen; noVNC skaliert das Bild hinein. */
height: min(72vh, 760px); height: min(72vh, 760px);
} }
/* Im Fenster ist weniger Platz -- und das Fenster soll nicht ueber den
Bildschirmrand hinauswachsen, nur weil das Geraet hochaufloesend ist. */
dialog.modal .screen__canvas { height: min(62vh, 620px); }
.screen__status { .screen__status {
font-size: var(--fs-sm); font-size: var(--fs-sm);
color: var(--text-faint); color: var(--text-faint);
@@ -8,6 +8,11 @@
// Die Meldungen sind der eigentliche Wert. Eine Sitzung, die schwarz bleibt, // Die Meldungen sind der eigentliche Wert. Eine Sitzung, die schwarz bleibt,
// laesst offen, ob es an der Berechtigung, am Netz, am Server oder am // laesst offen, ob es an der Berechtigung, am Netz, am Server oder am
// Passwort liegt -- und wer das nicht sieht, sucht den Fehler in TESM. // Passwort liegt -- und wer das nicht sieht, sucht den Fehler in TESM.
//
// **Ein Bauteil, zwei Orte.** Dieselbe Sitzung laeuft auf der Bildschirmseite
// und im Fenster der Statusuebersicht. Sie holt alles, was sie braucht, aus
// einer JSON-Auskunft des Servers -- damit gibt es keine zweite Fassung, die
// man beim naechsten Fund vergisst nachzuziehen.
import RFB from "../vendor/novnc/core/rfb.js"; import RFB from "../vendor/novnc/core/rfb.js";
@@ -18,40 +23,19 @@ import RFB from "../vendor/novnc/core/rfb.js";
// //
// Zwei davon holen nur Auskunft (_fail, _socketClose). Der dritte greift in // Zwei davon holen nur Auskunft (_fail, _socketClose). Der dritte greift in
// die Wahl des Sicherheitsverfahrens ein -- aber erst, **nachdem** eines // die Wahl des Sicherheitsverfahrens ein -- aber erst, **nachdem** eines
// nachweislich nicht getragen hat. // nachweislich nicht getragen hat, oder weil eines schon einmal getragen hat.
// //
// Die Vorgeschichte gehoert dazu, damit niemand den Schritt zurueckgeht: hier // Die Vorgeschichte gehoert dazu, damit niemand den Schritt zurueckgeht: hier
// stand einmal eine Vorauswahl, die ohne hinterlegte Zugangsdaten ein // stand einmal eine Vorauswahl, die ohne hinterlegte Zugangsdaten ein
// Verfahren ohne Anmeldung erzwang, weil RealVNC RA2ne zuerst nennt. Das war // Verfahren ohne Anmeldung erzwang, weil RealVNC RA2ne zuerst nennt. Das war
// geraten und falsch. Was jetzt hier steht, ist das Gegenteil einer // geraten und falsch. Was jetzt hier steht, ist das Gegenteil einer Vermutung:
// Vermutung: der Server nennt seine Reihenfolge, die gilt -- und wenn seine // ein Befund aus einer vorherigen Sitzung.
// erste Wahl die Verbindung abreissen laesst, ist das ein Befund, auf den man
// antworten darf. Gemessen am Geraet: RealVNC bietet 6 (RA2ne) an, waehlt man
// es, legt es auf; daneben liegt 1 (ohne Authentifizierung), und darueber
// laeuft auch der Viewer.
const istUnterstuetzt = RFB.prototype._isSupportedSecurityType; const istUnterstuetzt = RFB.prototype._isSupportedSecurityType;
//: Verfahren, die in dieser Sitzung schon abgerissen sind. //: Den *Grund* eines Fehlschlags kennt nur ``_fail`` -- das Ereignis
const gescheitert = new Set(); //: "disconnect" traegt ihn nicht, und der Weg ueber die Protokollausgabe
//: funktioniert nicht: die Bibliothek bindet ``console.error`` beim Laden,
//: Hoechstens ein zweiter Anlauf. Jeder Versuch ist eine Verbindung, und ein //: also bevor eine Seite etwas daran aendern koennte.
//: RealVNC Server zaehlt abgebrochene Versuche mit -- wer dreimal anklopft,
//: sperrt sich aus.
const HOECHSTENS = 2;
//: So lange darf "wird aufgebaut" stehen, bevor die Seite selbst nachsieht.
//:
//: Der Server hat eigene Grenzen und meldet sich, wenn das Geraet schweigt --
//: aber nur, wenn die Leitung dorthin ueberhaupt steht. Kommt die
//: WebSocket-Verbindung gar nicht erst zustande, wartet noVNC ohne jedes
//: Ereignis, und die Seite stuende stumm bis zum Seitenwechsel. Genau so
//: gemeldet: "Verbindung wird aufgebaut ..." und sonst nichts.
const AUFBAU_GRENZE = 12000;
// Den *Grund* eines Fehlschlags kennt nur ``_fail`` -- das Ereignis
// "disconnect" traegt ihn nicht, und der Weg ueber die Protokollausgabe
// funktioniert nicht: die Bibliothek bindet ``console.error`` beim Laden, also
// bevor eine Seite etwas daran aendern koennte. (Genau daran ist der erste
// Versuch gescheitert -- die Meldung blieb leer.)
let letzterGrund = ""; let letzterGrund = "";
const originalFail = RFB.prototype._fail; const originalFail = RFB.prototype._fail;
RFB.prototype._fail = function (details) { RFB.prototype._fail = function (details) {
@@ -59,9 +43,9 @@ RFB.prototype._fail = function (details) {
return originalFail.call(this, details); return originalFail.call(this, details);
}; };
// Und der Abschluss der Leitung selbst: der Schliesscode sagt, ob TESM //: Und der Abschluss der Leitung selbst: der Schliesscode sagt, ob TESM
// abgewiesen hat (1008), ob es ein Serverfehler war (1011) -- oder ob die //: abgewiesen hat (1008), ob es ein Serverfehler war (1011) -- oder ob die
// Verbindung nie zustande kam (1006). //: Verbindung nie zustande kam (1006).
let letzterCode = 0; let letzterCode = 0;
const originalSocketClose = RFB.prototype._socketClose; const originalSocketClose = RFB.prototype._socketClose;
RFB.prototype._socketClose = function (ereignis) { RFB.prototype._socketClose = function (ereignis) {
@@ -72,40 +56,61 @@ RFB.prototype._socketClose = function (ereignis) {
return originalSocketClose.call(this, ereignis); return originalSocketClose.call(this, ereignis);
}; };
const wurzel = document.getElementById("screen"); //: Hoechstens ein zweiter Anlauf. Jeder Versuch ist eine Verbindung, und ein
if (wurzel) { //: RealVNC Server zaehlt abgebrochene Versuche mit -- wer dreimal anklopft,
starten(wurzel); //: sperrt sich aus.
} const HOECHSTENS = 2;
function starten(wurzel) { //: So lange darf "wird aufgebaut" stehen, bevor die Seite selbst nachsieht.
const leinwand = document.getElementById("screen-canvas"); //: Kommt die WebSocket-Verbindung gar nicht erst zustande, wartet noVNC ohne
const zeile = document.getElementById("screen-status"); //: jedes Ereignis, und die Seite stuende stumm bis zum Seitenwechsel.
const abfrage = document.getElementById("screen-credentials"); const AUFBAU_GRENZE = 12000;
const feldBenutzer = document.getElementById("screen-username");
const feldPasswort = document.getElementById("screen-password"); //: Tastenkombinationen, die der Arbeitsplatz selbst abfaengt und die deshalb
const knopfSenden = document.getElementById("screen-send"); //: einen Knopf brauchen. X11-Keysyms.
const knopfTrennen = document.getElementById("screen-disconnect"); const ALT_LINKS = 0xffe9;
const knopfStrg = document.getElementById("screen-cad"); const F4 = 0xffc1;
const pfad = wurzel.dataset.socket;
const schema = window.location.protocol === "https:" ? "wss:" : "ws:"; const schema = window.location.protocol === "https:" ? "wss:" : "ws:";
const adresse = `${schema}//${window.location.host}${pfad}`;
const vorgabe = JSON.parse( /**
document.getElementById("screen-credentials-data")?.textContent || "{}", * Baut eine Sitzung in diesem Behaelter auf.
); *
* ``quelle`` ist die Adresse der JSON-Auskunft; alles Weitere steht dort.
* Rueckgabe: eine Funktion, die die Sitzung wieder abbaut.
*/
export function starteSitzung(wurzel, quelle) {
const teil = (rolle) => wurzel.querySelector(`[data-rolle="${rolle}"]`);
const leinwand = teil("leinwand");
const zeile = teil("status");
const abfrage = teil("zugangsdaten");
const feldBenutzer = teil("benutzer");
const feldPasswort = teil("passwort");
const knopfSenden = teil("anmelden");
const knopfTrennen = teil("trennen");
const knoepfeSitzung = [...wurzel.querySelectorAll("[data-rolle^='taste-']"), knopfTrennen];
const gescheitert = new Set();
let rfb = null; let rfb = null;
let aufbauUhr = 0; let aufbauUhr = 0;
let versuche = 0; let versuche = 0;
let stand = false; let stand = false;
let daten = null;
let letzterZugang = {}; let letzterZugang = {};
let beendet = false;
function melden(text, art = "") { function melden(text, art = "") {
if (!zeile) return;
zeile.textContent = text; zeile.textContent = text;
zeile.className = `screen__status${art ? " screen__status--" + art : ""}`; zeile.className = `screen__status${art ? " screen__status--" + art : ""}`;
} }
function sitzungsknoepfe(sichtbar) {
knoepfeSitzung.forEach((knopf) => {
if (knopf) knopf.hidden = !sichtbar;
});
}
function grund() { function grund() {
const teile = []; const teile = [];
if (letzterGrund) { if (letzterGrund) {
@@ -120,29 +125,6 @@ function starten(wurzel) {
return teile.join(" · "); return teile.join(" · ");
} }
// Hat das gewählte Verfahren die Verbindung abreissen lassen? Dann ist das
// ein Befund und keine Vermutung -- der naechste Anlauf laesst es aus.
//
// Nur solange nie ein Bild stand: bricht eine **laufende** Sitzung ab, liegt
// es nicht am Anmeldeverfahren, und ein stiller Neuaufbau wuerde die Ursache
// verdecken.
function naechsterVersuch() {
const verfahren = rfb && rfb._rfbAuthScheme > 0 ? rfb._rfbAuthScheme : 0;
if (!verfahren || versuche >= HOECHSTENS || gescheitert.has(verfahren)) {
return false;
}
gescheitert.add(verfahren);
melden(
`Verfahren ${verfahren} hat die Verbindung nicht getragen – nächster Anlauf ` +
"mit dem nächsten, das der Server anbietet …",
"warnung",
);
const zugang = letzterZugang;
rfb = null;
window.setTimeout(() => verbinden(zugang), 400);
return true;
}
// Steht die WebSocket-Strecke zu TESM überhaupt? Zwei sehr verschiedene // Steht die WebSocket-Strecke zu TESM überhaupt? Zwei sehr verschiedene
// Ursachen sehen von aussen gleich aus: der VNC-Server mag nicht, oder die // Ursachen sehen von aussen gleich aus: der VNC-Server mag nicht, oder die
// Verbindung kommt nie zustande, weil ein Proxy zwischen Browser und TESM // Verbindung kommt nie zustande, weil ein Proxy zwischen Browser und TESM
@@ -168,7 +150,45 @@ function starten(wurzel) {
}); });
} }
// Hat das gewählte Verfahren die Verbindung abreissen lassen? Dann ist das
// ein Befund und keine Vermutung -- der naechste Anlauf laesst es aus. Nur
// solange nie ein Bild stand: bricht eine **laufende** Sitzung ab, liegt es
// nicht am Anmeldeverfahren.
function naechsterVersuch() {
const verfahren = rfb && rfb._rfbAuthScheme > 0 ? rfb._rfbAuthScheme : 0;
if (!verfahren || versuche >= HOECHSTENS || gescheitert.has(verfahren)) {
return false;
}
gescheitert.add(verfahren);
melden(
`Verfahren ${verfahren} hat die Verbindung nicht getragen – nächster Anlauf ` +
"mit dem nächsten, das der Server anbietet …",
"warnung",
);
const zugang = letzterZugang;
rfb = null;
window.setTimeout(() => verbinden(zugang), 400);
return true;
}
function verfahrenMelden(gewaehlt) {
if (!gewaehlt || gewaehlt === daten.bewaehrt || !daten.meldeweg) return;
fetch(daten.meldeweg, {
method: "POST",
credentials: "same-origin",
headers: {
"Content-Type": "application/json",
"X-Requested-With": "fetch",
"X-CSRF-Token": document.body?.dataset.csrf || "",
},
body: JSON.stringify({ verfahren: gewaehlt }),
}).catch(() => {
/* Eine Merkhilfe, kein Teil der Sitzung. */
});
}
function verbinden(zugang) { function verbinden(zugang) {
if (beendet) return;
melden(versuche ? "Nächster Versuch …" : "Verbindung wird aufgebaut …"); melden(versuche ? "Nächster Versuch …" : "Verbindung wird aufgebaut …");
letzterGrund = ""; letzterGrund = "";
letzterCode = 0; letzterCode = 0;
@@ -176,8 +196,6 @@ function starten(wurzel) {
versuche += 1; versuche += 1;
window.clearTimeout(aufbauUhr); window.clearTimeout(aufbauUhr);
aufbauUhr = window.setTimeout(() => { aufbauUhr = window.setTimeout(() => {
// Weder verbunden noch abgebrochen noch nach Zugangsdaten gefragt --
// dann haengt es an der Leitung, nicht am VNC-Server.
melden("Es kommt keine Antwort. Wird geprüft, woran es liegt …", "warnung"); melden("Es kommt keine Antwort. Wird geprüft, woran es liegt …", "warnung");
streckeTesten().then((steht) => { streckeTesten().then((steht) => {
melden( melden(
@@ -194,15 +212,22 @@ function starten(wurzel) {
}); });
}, AUFBAU_GRENZE); }, AUFBAU_GRENZE);
// Was abgerissen ist, wird nicht noch einmal gewaehlt. Sonst faende noVNC // Zwei Regeln, beide aus Erfahrung und keine aus Vermutung:
// dasselbe Verfahren wieder zuerst -- und der zweite Anlauf waere eine //
// Wiederholung des ersten. // 1. Was schon einmal getragen hat, kommt zuerst -- aber nur beim ersten
// Anlauf. Ohne das faengt jede Sitzung wieder mit der ersten Wahl des
// Servers an, die hier schon abgerissen ist, und ein sperrender
// RealVNC Server laesst sich mit der Absage Minuten Zeit.
// 2. Was abgerissen ist, wird nicht noch einmal gewaehlt.
const bewaehrt = daten.bewaehrt || 0;
const zuerst = versuche === 1 && bewaehrt > 0 && !gescheitert.has(bewaehrt);
RFB.prototype._isSupportedSecurityType = function (typ) { RFB.prototype._isSupportedSecurityType = function (typ) {
if (gescheitert.has(typ)) return false; if (gescheitert.has(typ)) return false;
if (zuerst) return typ === bewaehrt;
return istUnterstuetzt.call(this, typ); return istUnterstuetzt.call(this, typ);
}; };
rfb = new RFB(leinwand, adresse, { rfb = new RFB(leinwand, `${schema}//${window.location.host}${daten.socket}`, {
credentials: zugang, credentials: zugang,
// Eine Bildschirmsitzung soll nicht nebenbei die Zwischenablage des // Eine Bildschirmsitzung soll nicht nebenbei die Zwischenablage des
// Arbeitsplatzes mit dem Geraet teilen -- das ist eine eigene // Arbeitsplatzes mit dem Geraet teilen -- das ist eine eigene
@@ -216,22 +241,20 @@ function starten(wurzel) {
rfb.addEventListener("connect", () => { rfb.addEventListener("connect", () => {
window.clearTimeout(aufbauUhr); window.clearTimeout(aufbauUhr);
stand = true; stand = true;
melden(`Verbunden mit ${wurzel.dataset.ziel}`, "ok"); verfahrenMelden(rfb && rfb._rfbAuthScheme > 0 ? rfb._rfbAuthScheme : 0);
abfrage.hidden = true; melden(`Verbunden mit ${daten.ziel}`, "ok");
if (abfrage) abfrage.hidden = true;
leinwand.hidden = false; leinwand.hidden = false;
knopfTrennen.hidden = false; sitzungsknoepfe(true);
knopfStrg.hidden = false;
}); });
rfb.addEventListener("disconnect", (ereignis) => { rfb.addEventListener("disconnect", (ereignis) => {
window.clearTimeout(aufbauUhr); window.clearTimeout(aufbauUhr);
leinwand.hidden = true; leinwand.hidden = true;
knopfTrennen.hidden = true; sitzungsknoepfe(false);
knopfStrg.hidden = true;
if (ereignis.detail && ereignis.detail.clean) { if (ereignis.detail && ereignis.detail.clean) {
// Beim sauberen Schluss steht der Grund im Schliessrahmen -- etwa, // Beim sauberen Schluss steht der Grund im Schliessrahmen -- etwa,
// dass TESM wegen Untätigkeit getrennt hat. Ohne ihn stuende da // dass TESM wegen Untätigkeit getrennt hat.
// "beendet", und niemand wuesste, warum das Bild weg ist.
melden(letzterGrund ? `Sitzung beendet – ${letzterGrund}` : "Sitzung beendet."); melden(letzterGrund ? `Sitzung beendet – ${letzterGrund}` : "Sitzung beendet.");
} else if (!stand && naechsterVersuch()) { } else if (!stand && naechsterVersuch()) {
return; return;
@@ -243,9 +266,6 @@ function starten(wurzel) {
: "Verbindung abgebrochen. Wird geprüft, woran es liegt …", : "Verbindung abgebrochen. Wird geprüft, woran es liegt …",
"fehler", "fehler",
); );
// 1006 heisst: die Leitung ging zu, ohne dass die Gegenseite einen
// Grund geschickt hat -- typisch, wenn die WebSocket-Verbindung gar
// nicht erst zustande kam. Dann sagt die Probe, wer schweigt.
if (letzterCode === 1006 || !erklaerung) { if (letzterCode === 1006 || !erklaerung) {
streckeTesten().then((steht) => { streckeTesten().then((steht) => {
melden( melden(
@@ -269,10 +289,10 @@ function starten(wurzel) {
rfb.addEventListener("credentialsrequired", (ereignis) => { rfb.addEventListener("credentialsrequired", (ereignis) => {
window.clearTimeout(aufbauUhr); window.clearTimeout(aufbauUhr);
const gefragt = ereignis.detail.types || ["password"]; const gefragt = ereignis.detail.types || ["password"];
abfrage.hidden = false; if (abfrage) abfrage.hidden = false;
feldBenutzer.closest("label").hidden = !gefragt.includes("username"); if (feldBenutzer) feldBenutzer.closest("label").hidden = !gefragt.includes("username");
melden("Der VNC-Server verlangt Zugangsdaten.", "warnung"); melden("Der VNC-Server verlangt Zugangsdaten.", "warnung");
(gefragt.includes("username") ? feldBenutzer : feldPasswort).focus(); (gefragt.includes("username") ? feldBenutzer : feldPasswort)?.focus();
}); });
rfb.addEventListener("securityfailure", (ereignis) => { rfb.addEventListener("securityfailure", (ereignis) => {
@@ -281,44 +301,115 @@ function starten(wurzel) {
`Anmeldung abgelehnt: ${ereignis.detail.reason || "kein Grund genannt"}`, `Anmeldung abgelehnt: ${ereignis.detail.reason || "kein Grund genannt"}`,
"fehler", "fehler",
); );
abfrage.hidden = false; if (abfrage) abfrage.hidden = false;
});
rfb.addEventListener("desktopname", (ereignis) => {
wurzel.dataset.desktop = ereignis.detail.name;
}); });
} }
knopfSenden?.addEventListener("click", () => { knopfSenden?.addEventListener("click", () => {
const zugang = {}; const zugang = {};
if (feldBenutzer.value) zugang.username = feldBenutzer.value; if (feldBenutzer?.value) zugang.username = feldBenutzer.value;
if (feldPasswort.value) zugang.password = feldPasswort.value; if (feldPasswort?.value) zugang.password = feldPasswort.value;
if (rfb) { if (rfb) rfb.sendCredentials(zugang);
rfb.sendCredentials(zugang); else verbinden(zugang);
} else {
verbinden(zugang);
}
}); });
[feldBenutzer, feldPasswort].forEach((feld) => [feldBenutzer, feldPasswort].forEach((feld) =>
feld?.addEventListener("keydown", (ereignis) => { feld?.addEventListener("keydown", (ereignis) => {
if (ereignis.key === "Enter") { if (ereignis.key === "Enter") {
ereignis.preventDefault(); ereignis.preventDefault();
knopfSenden.click(); knopfSenden?.click();
} }
}), }),
); );
knopfTrennen?.addEventListener("click", () => rfb && rfb.disconnect()); knopfTrennen?.addEventListener("click", () => rfb && rfb.disconnect());
knopfStrg?.addEventListener("click", () => rfb && rfb.sendCtrlAltDel()); teil("taste-cad")?.addEventListener("click", () => rfb && rfb.sendCtrlAltDel());
// Alt+F4 kennt noVNC nicht als fertigen Griff: der Arbeitsplatz faengt die
// Kombination selbst ab, bevor sie in der Leinwand ankommt. Also einzeln --
// Alt halten, F4 tippen, Alt loslassen -- wie es auf der Tastatur auch
// geschieht.
teil("taste-altf4")?.addEventListener("click", () => {
if (!rfb) return;
rfb.sendKey(ALT_LINKS, "AltLeft", true);
rfb.sendKey(F4, "F4", true);
rfb.sendKey(F4, "F4", false);
rfb.sendKey(ALT_LINKS, "AltLeft", false);
});
if (wurzel.dataset.blockiert === "1") { sitzungsknoepfe(false);
melden(wurzel.dataset.grund || "Diese Sitzung kann nicht aufgebaut werden.", "fehler"); melden("Wird vorbereitet …");
fetch(quelle, { credentials: "same-origin", headers: { "X-Requested-With": "fetch" } })
.then((antwort) => antwort.json().then((inhalt) => ({ status: antwort.status, inhalt })))
.then(({ status, inhalt }) => {
if (beendet) return;
if (status === 409 && inhalt.reauth) {
melden(
"Für den Bildschirm ist eine frische Passwortbestätigung nötig – bitte über " +
"die Geräteseite öffnen.",
"warnung",
);
return; return;
} }
if (status !== 200) {
melden(inhalt.fehler || "Diese Sitzung lässt sich nicht öffnen.", "fehler");
return;
}
daten = inhalt;
if (daten.hinderungsgrund) {
melden(daten.hinderungsgrund, "fehler");
return;
}
const zugang = {}; const zugang = {};
if (vorgabe.username) zugang.username = vorgabe.username; if (daten.vorgaben?.username) zugang.username = daten.vorgaben.username;
if (vorgabe.password) zugang.password = vorgabe.password; if (daten.vorgaben?.password) zugang.password = daten.vorgaben.password;
verbinden(zugang); verbinden(zugang);
})
.catch(() => melden("Die Sitzungsdaten sind nicht abrufbar.", "fehler"));
return function beenden() {
beendet = true;
window.clearTimeout(aufbauUhr);
try {
rfb?.disconnect();
} catch (fehler) {
/* schon zu */
}
rfb = null;
};
}
// -- Die beiden Orte -------------------------------------------------------- #
// 1. Die Bildschirmseite: ein Behaelter, der seine Quelle selbst mitbringt.
const seite = document.getElementById("screen");
if (seite?.dataset.sitzung) {
starteSitzung(seite, seite.dataset.sitzung);
}
// 2. Die Statusuebersicht: ein Fenster, das jede Kachel oeffnen kann. Ein
// Dialog fuer alle -- er bekommt beim Oeffnen, welches Geraet gemeint ist.
const fenster = document.getElementById("vnc-fenster");
if (fenster) {
let beenden = null;
const behaelter = fenster.querySelector("[data-rolle='behaelter']");
const titel = fenster.querySelector("[data-rolle='titel']");
document.addEventListener("click", (ereignis) => {
const knopf = ereignis.target.closest("[data-vnc-oeffnen]");
if (!knopf) return;
ereignis.preventDefault();
beenden?.();
titel.textContent = knopf.dataset.vncName || "Bildschirm";
if (typeof fenster.showModal === "function") fenster.showModal();
else fenster.setAttribute("open", "");
beenden = starteSitzung(behaelter, knopf.dataset.vncOeffnen);
});
// Zu heisst zu: eine Sitzung, die im Hintergrund weiterlaeuft, haelt die
// Verbindung zum Geraet und bei RealVNC den einen Sitzungsplatz.
fenster.addEventListener("close", () => {
beenden?.();
beenden = null;
});
} }