more stuff
This commit is contained in:
@ -520,10 +520,13 @@
|
||||
if (named.length) {
|
||||
return `Anmeldung als ${named.map((k) => `"${k.username}" (Key ${k.label})`).join(", ")}.`;
|
||||
}
|
||||
if ((host.ssh_keys || []).length === 0 && host.ssh_password_credentials_set) {
|
||||
return `Anmeldung als "${host.ssh_password_credentials_username}" (SSH-Passwort, kein Schluessel zugeordnet).`;
|
||||
}
|
||||
if (host.ssh_username) {
|
||||
return `Noch kein Benutzername am SSH-Key -- es wird der alte Wert am Host verwendet ("${host.ssh_username}"). Bitte im Reiter "Zugangsdaten" am Schluessel eintragen.`;
|
||||
}
|
||||
return 'Kein SSH-Benutzername hinterlegt -- im Reiter "Zugangsdaten" am zugeordneten Schluessel eintragen, sonst kommt keine Verbindung zustande.';
|
||||
return 'Kein SSH-Benutzername hinterlegt -- im Reiter "Zugangsdaten" am zugeordneten Schluessel oder als SSH-Passwort eintragen, sonst kommt keine Verbindung zustande.';
|
||||
}
|
||||
|
||||
async function showHostDetail(hostId) {
|
||||
@ -581,6 +584,11 @@
|
||||
const keySelect = document.getElementById("hkm-key-select");
|
||||
keySelect.replaceChildren(...optionsFromList(cachedSshKeys, "id", (k) => `${k.label} (#${k.id})`));
|
||||
|
||||
document.getElementById("ssh-pw-cred-status").textContent = host.ssh_password_credentials_set
|
||||
? `SSH-Passwort gesetzt fuer "${host.ssh_password_credentials_username}" (zuletzt aktualisiert: ${host.ssh_password_credentials_updated_at}).`
|
||||
: "Noch kein SSH-Passwort gesetzt.";
|
||||
document.getElementById("hspw-username").value = host.ssh_password_credentials_username || "";
|
||||
|
||||
document.getElementById("rdp-cred-status").textContent = host.rdp_credentials_set
|
||||
? `RDP-Zugangsdaten gesetzt fuer "${host.rdp_credentials_username || "(kein Benutzername)"}" (zuletzt aktualisiert: ${host.rdp_credentials_updated_at}).`
|
||||
: "Noch keine RDP-Zugangsdaten gesetzt.";
|
||||
@ -704,6 +712,40 @@
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById("hrc-delete-btn").addEventListener("click", async () => {
|
||||
try {
|
||||
await apiFetch(`/admin/hosts/${activeHostId}/rdp-credentials`, { method: "DELETE" });
|
||||
showBanner("RDP-Zugangsdaten geloescht.", "ok");
|
||||
await showHostDetail(activeHostId);
|
||||
} catch (err) {
|
||||
setDetailBanner(err.message, "error");
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById("host-ssh-pw-form").addEventListener("submit", async (ev) => {
|
||||
ev.preventDefault();
|
||||
try {
|
||||
const username = document.getElementById("hspw-username").value.trim();
|
||||
const password = document.getElementById("hspw-password").value;
|
||||
await sendJson(`/admin/hosts/${activeHostId}/ssh-password`, "PUT", { username, password });
|
||||
showBanner("SSH-Passwort gespeichert.", "ok");
|
||||
document.getElementById("hspw-password").value = "";
|
||||
await showHostDetail(activeHostId);
|
||||
} catch (err) {
|
||||
setDetailBanner(err.message, "error");
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById("hspw-delete-btn").addEventListener("click", async () => {
|
||||
try {
|
||||
await apiFetch(`/admin/hosts/${activeHostId}/ssh-password`, { method: "DELETE" });
|
||||
showBanner("SSH-Passwort geloescht.", "ok");
|
||||
await showHostDetail(activeHostId);
|
||||
} catch (err) {
|
||||
setDetailBanner(err.message, "error");
|
||||
}
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Zugangsdaten (SSH-Keys + RDP-Passwoerter)
|
||||
// ---------------------------------------------------------------------
|
||||
@ -727,7 +769,6 @@
|
||||
el("td", { textContent: k.label }),
|
||||
el("td", { textContent: k.username || "-" }),
|
||||
el("td", { textContent: k.key_type }),
|
||||
el("td", { textContent: k.owner_user_id === null ? "-" : String(k.owner_user_id) }),
|
||||
el("td", { textContent: k.tenant_name }),
|
||||
el("td", { textContent: k.has_passphrase ? "hinterlegt" : "-" }),
|
||||
el("td", { textContent: k.created_at }),
|
||||
@ -754,7 +795,6 @@
|
||||
document.getElementById("ssh-key-edit-name").textContent = `${k.label} (#${k.id})`;
|
||||
document.getElementById("ske-label").value = k.label;
|
||||
document.getElementById("ske-username").value = k.username || "";
|
||||
document.getElementById("ske-owner").value = k.owner_user_id === null ? "" : String(k.owner_user_id);
|
||||
document.getElementById("skr-type").value = k.key_type;
|
||||
document.getElementById("skp-passphrase").value = "";
|
||||
document.getElementById("ske-passphrase-state").textContent = k.has_passphrase
|
||||
@ -799,13 +839,11 @@
|
||||
document.getElementById("ssh-key-edit-form").addEventListener("submit", async (ev) => {
|
||||
ev.preventDefault();
|
||||
try {
|
||||
const ownerRaw = document.getElementById("ske-owner").value.trim();
|
||||
await sendJson(`/admin/ssh-keys/${editingSshKeyId}`, "PUT", {
|
||||
label: document.getElementById("ske-label").value.trim() || null,
|
||||
// Leeres Feld -> null entfernt den Benutzernamen bewusst (die
|
||||
// Server-Semantik unterscheidet zwischen "Feld fehlt" und "null").
|
||||
username: document.getElementById("ske-username").value.trim() || null,
|
||||
owner_user_id: ownerRaw ? Number(ownerRaw) : null,
|
||||
});
|
||||
showBanner("SSH-Key aktualisiert.", "ok");
|
||||
await refreshSshKeys();
|
||||
@ -823,7 +861,7 @@
|
||||
public_key: document.getElementById("skr-public").value,
|
||||
passphrase: document.getElementById("skr-passphrase").value || null,
|
||||
});
|
||||
showBanner("SSH-Key rotiert.", "ok");
|
||||
showBanner("Neues Schluesselmaterial gespeichert.", "ok");
|
||||
ev.target.reset();
|
||||
await refreshSshKeys();
|
||||
} catch (err) {
|
||||
@ -831,15 +869,29 @@
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById("skc-generate-btn").addEventListener("click", async () => {
|
||||
try {
|
||||
const keyType = document.getElementById("skc-type").value;
|
||||
if (keyType === "ca-cert") {
|
||||
showBanner("Automatische Erzeugung ist fuer ca-cert nicht moeglich -- bitte Schluessel/Zertifikat der eigenen CA einfuegen.", "error");
|
||||
return;
|
||||
}
|
||||
const result = await sendJson("/admin/ssh-keys/generate", "POST", { key_type: keyType });
|
||||
document.getElementById("skc-private").value = result.private_key_pem;
|
||||
document.getElementById("skc-public").value = result.public_key;
|
||||
showBanner("Schluesselpaar erzeugt -- bitte Label (und ggf. Benutzername) ausfuellen und 'Anlegen' druecken, um es zu speichern.", "ok");
|
||||
} catch (err) {
|
||||
showBanner(err.message, "error");
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById("ssh-key-create-form").addEventListener("submit", async (ev) => {
|
||||
ev.preventDefault();
|
||||
try {
|
||||
const ownerRaw = document.getElementById("skc-owner").value.trim();
|
||||
const tenantSelect = document.getElementById("skc-tenant");
|
||||
const payload = {
|
||||
label: document.getElementById("skc-label").value.trim(),
|
||||
username: document.getElementById("skc-username").value.trim() || null,
|
||||
owner_user_id: ownerRaw ? Number(ownerRaw) : null,
|
||||
key_type: document.getElementById("skc-type").value,
|
||||
private_key_pem: document.getElementById("skc-private").value,
|
||||
public_key: document.getElementById("skc-public").value,
|
||||
@ -1238,7 +1290,7 @@
|
||||
}
|
||||
if (s.has_recording) {
|
||||
actions.appendChild(
|
||||
actionButton("Aufzeichnung", "btn-secondary", () => showSessionRecording(s.id))
|
||||
actionButton("Wiedergabe", "btn-secondary", () => showSessionRecording(s))
|
||||
);
|
||||
}
|
||||
return el("tr", {}, [
|
||||
@ -1260,17 +1312,265 @@
|
||||
);
|
||||
}
|
||||
|
||||
async function showSessionRecording(sessionId) {
|
||||
// --- Wiedergabe (SSH: Terminal-Replay ueber xterm.js; RDP: volle
|
||||
// grafische Wiedergabe ueber Guacamole.SessionRecording) --------------
|
||||
//
|
||||
// Die vom Recorder aufgezeichneten "output"-Instruktionen einer RDP-
|
||||
// Sitzung sind bereits ein gueltiger .guac-Datenstrom (siehe
|
||||
// app/recordings/recorder.py + app/rdp_proxy/guacd_client.py::
|
||||
// encode_instruction): guacd sendet als Teil des normalen Protokolls
|
||||
// periodische "sync"-Instruktionen mit Zeitstempel, genau das nutzt
|
||||
// Guacamole.SessionRecording zur Framebildung. Aneinandergehaengt ergeben
|
||||
// die "output"-Eintraege deshalb exakt das Format, das die Bibliothek
|
||||
// erwartet -- kein eigener Player noetig.
|
||||
|
||||
function recB64decode(b64) {
|
||||
return decodeURIComponent(escape(atob(b64)));
|
||||
}
|
||||
|
||||
function formatMs(ms) {
|
||||
const totalSec = Math.max(0, Math.round(ms / 1000));
|
||||
const m = Math.floor(totalSec / 60);
|
||||
const s = totalSec % 60;
|
||||
return `${m}:${String(s).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
let activePlayer = null;
|
||||
|
||||
function closeRecordingOverlay() {
|
||||
document.getElementById("rec-overlay").classList.add("hidden");
|
||||
if (activePlayer) {
|
||||
activePlayer.destroy();
|
||||
activePlayer = null;
|
||||
}
|
||||
document.getElementById("rec-stage").replaceChildren();
|
||||
}
|
||||
|
||||
document.getElementById("rec-close-btn").addEventListener("click", closeRecordingOverlay);
|
||||
|
||||
function createSshPlayer(entries) {
|
||||
const outputEntries = entries.filter((e) => e.dir === "output");
|
||||
const stage = document.getElementById("rec-stage");
|
||||
const container = el("div", { className: "rec-terminal" });
|
||||
stage.appendChild(container);
|
||||
|
||||
const term = new Terminal({
|
||||
cursorBlink: false, disableStdin: true, convertEol: false,
|
||||
fontFamily: "Menlo, Consolas, monospace", fontSize: 13,
|
||||
theme: { background: "#000000" },
|
||||
});
|
||||
const fitAddon = new FitAddon.FitAddon();
|
||||
term.loadAddon(fitAddon);
|
||||
term.open(container);
|
||||
fitAddon.fit();
|
||||
|
||||
const playBtn = document.getElementById("rec-playpause-btn");
|
||||
const seek = document.getElementById("rec-seek");
|
||||
const timeLabel = document.getElementById("rec-time");
|
||||
const speedSelect = document.getElementById("rec-speed");
|
||||
speedSelect.classList.remove("hidden");
|
||||
|
||||
const durationMs = outputEntries.length ? outputEntries[outputEntries.length - 1].t * 1000 : 0;
|
||||
seek.min = "0";
|
||||
seek.max = String(Math.round(durationMs));
|
||||
seek.value = "0";
|
||||
|
||||
let idx = 0;
|
||||
let playing = false;
|
||||
let timer = null;
|
||||
let speed = Number(speedSelect.value) || 1;
|
||||
let realStart = 0;
|
||||
let baseMs = 0;
|
||||
|
||||
function updateTimeUi(posMs) {
|
||||
seek.value = String(Math.round(posMs));
|
||||
timeLabel.textContent = `${formatMs(posMs)} / ${formatMs(durationMs)}`;
|
||||
}
|
||||
|
||||
function scheduleNext() {
|
||||
if (!playing) return;
|
||||
if (idx >= outputEntries.length) {
|
||||
playing = false;
|
||||
playBtn.textContent = "Abspielen";
|
||||
return;
|
||||
}
|
||||
const entry = outputEntries[idx];
|
||||
const targetMs = entry.t * 1000;
|
||||
const elapsedMs = baseMs + (Date.now() - realStart) * speed;
|
||||
const delay = Math.max(0, (targetMs - elapsedMs) / speed);
|
||||
timer = window.setTimeout(() => {
|
||||
term.write(recB64decode(entry.data));
|
||||
idx += 1;
|
||||
updateTimeUi(targetMs);
|
||||
scheduleNext();
|
||||
}, delay);
|
||||
}
|
||||
|
||||
function play() {
|
||||
if (playing || idx >= outputEntries.length) return;
|
||||
playing = true;
|
||||
playBtn.textContent = "Pause";
|
||||
realStart = Date.now();
|
||||
baseMs = idx > 0 ? outputEntries[idx - 1].t * 1000 : 0;
|
||||
scheduleNext();
|
||||
}
|
||||
|
||||
function pause() {
|
||||
playing = false;
|
||||
playBtn.textContent = "Abspielen";
|
||||
if (timer) { window.clearTimeout(timer); timer = null; }
|
||||
}
|
||||
|
||||
function seekTo(targetMs) {
|
||||
pause();
|
||||
term.reset();
|
||||
idx = 0;
|
||||
const targetSec = targetMs / 1000;
|
||||
while (idx < outputEntries.length && outputEntries[idx].t <= targetSec) {
|
||||
term.write(recB64decode(outputEntries[idx].data));
|
||||
idx += 1;
|
||||
}
|
||||
updateTimeUi(targetMs);
|
||||
}
|
||||
|
||||
const onPlayPause = () => { if (playing) pause(); else play(); };
|
||||
const onSeekChange = () => seekTo(Number(seek.value));
|
||||
const onSpeedChange = () => {
|
||||
speed = Number(speedSelect.value) || 1;
|
||||
if (playing) { pause(); play(); }
|
||||
};
|
||||
|
||||
playBtn.textContent = "Abspielen";
|
||||
playBtn.addEventListener("click", onPlayPause);
|
||||
seek.addEventListener("change", onSeekChange);
|
||||
speedSelect.addEventListener("change", onSpeedChange);
|
||||
updateTimeUi(0);
|
||||
|
||||
return {
|
||||
destroy() {
|
||||
pause();
|
||||
playBtn.removeEventListener("click", onPlayPause);
|
||||
seek.removeEventListener("change", onSeekChange);
|
||||
speedSelect.removeEventListener("change", onSpeedChange);
|
||||
speedSelect.classList.add("hidden");
|
||||
term.dispose();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createRdpPlayer(entries) {
|
||||
const stage = document.getElementById("rec-stage");
|
||||
const container = el("div", {});
|
||||
stage.appendChild(container);
|
||||
|
||||
const guacStream = entries.filter((e) => e.dir === "output").map((e) => e.data).join("");
|
||||
const blob = new Blob([guacStream], { type: "application/octet-stream" });
|
||||
const recording = new Guacamole.SessionRecording(blob);
|
||||
container.appendChild(recording.getDisplay().getElement());
|
||||
|
||||
const playBtn = document.getElementById("rec-playpause-btn");
|
||||
const seek = document.getElementById("rec-seek");
|
||||
const timeLabel = document.getElementById("rec-time");
|
||||
|
||||
let posTimer = null;
|
||||
let seeking = false;
|
||||
|
||||
function updateTimeUi() {
|
||||
const duration = recording.getDuration();
|
||||
const durationRounded = Math.round(duration);
|
||||
if (Number(seek.max) !== durationRounded) seek.max = String(durationRounded);
|
||||
if (!seeking) seek.value = String(Math.round(recording.getPosition()));
|
||||
timeLabel.textContent = `${formatMs(recording.getPosition())} / ${formatMs(duration)}`;
|
||||
}
|
||||
|
||||
recording.onload = updateTimeUi;
|
||||
recording.onprogress = updateTimeUi;
|
||||
recording.onplay = () => {
|
||||
playBtn.textContent = "Pause";
|
||||
if (posTimer) window.clearInterval(posTimer);
|
||||
posTimer = window.setInterval(updateTimeUi, 200);
|
||||
};
|
||||
recording.onpause = () => {
|
||||
playBtn.textContent = "Abspielen";
|
||||
if (posTimer) { window.clearInterval(posTimer); posTimer = null; }
|
||||
updateTimeUi();
|
||||
};
|
||||
recording.onerror = (message) => {
|
||||
showBanner("Fehler bei der RDP-Wiedergabe: " + message, "error");
|
||||
};
|
||||
|
||||
const onPlayPause = () => { if (recording.isPlaying()) recording.pause(); else recording.play(); };
|
||||
const onSeekStart = () => { seeking = true; };
|
||||
const onSeekInput = () => {
|
||||
timeLabel.textContent = `${formatMs(Number(seek.value))} / ${formatMs(recording.getDuration())}`;
|
||||
};
|
||||
const onSeekCommit = () => {
|
||||
recording.seek(Number(seek.value), () => { seeking = false; updateTimeUi(); });
|
||||
};
|
||||
|
||||
playBtn.textContent = "Abspielen";
|
||||
playBtn.addEventListener("click", onPlayPause);
|
||||
seek.addEventListener("mousedown", onSeekStart);
|
||||
seek.addEventListener("touchstart", onSeekStart);
|
||||
seek.addEventListener("input", onSeekInput);
|
||||
seek.addEventListener("change", onSeekCommit);
|
||||
updateTimeUi();
|
||||
|
||||
return {
|
||||
destroy() {
|
||||
recording.pause();
|
||||
recording.abort();
|
||||
if (posTimer) window.clearInterval(posTimer);
|
||||
playBtn.removeEventListener("click", onPlayPause);
|
||||
seek.removeEventListener("mousedown", onSeekStart);
|
||||
seek.removeEventListener("touchstart", onSeekStart);
|
||||
seek.removeEventListener("input", onSeekInput);
|
||||
seek.removeEventListener("change", onSeekCommit);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function showSessionRecording(session) {
|
||||
const overlay = document.getElementById("rec-overlay");
|
||||
const title = document.getElementById("rec-title");
|
||||
const loading = document.getElementById("rec-loading");
|
||||
const stage = document.getElementById("rec-stage");
|
||||
const controls = document.getElementById("rec-controls");
|
||||
const verifyBanner = document.getElementById("rec-verify-banner");
|
||||
|
||||
closeRecordingOverlay();
|
||||
overlay.classList.remove("hidden");
|
||||
title.textContent = `Wiedergabe -- Sitzung #${session.id} (${session.username} @ ${session.hostname})`;
|
||||
loading.classList.remove("hidden");
|
||||
loading.textContent = "Aufzeichnung wird geladen ...";
|
||||
stage.classList.add("hidden");
|
||||
controls.classList.add("hidden");
|
||||
verifyBanner.classList.add("hidden");
|
||||
|
||||
try {
|
||||
const result = await getJson(`/admin/sessions/${sessionId}/recording`);
|
||||
const box = document.getElementById("session-recording-box");
|
||||
box.classList.remove("hidden");
|
||||
box.className = `banner ${result.verified ? "ok" : "error"}`;
|
||||
box.textContent = result.verified
|
||||
? `Aufzeichnung #${sessionId}: Integritaet OK, ${result.entry_count} Eintraege.`
|
||||
: `Aufzeichnung #${sessionId}: WARNUNG -- Hash-Kette gebrochen, moeglicherweise manipuliert!`;
|
||||
const result = await getJson(`/admin/sessions/${session.id}/recording/entries`);
|
||||
loading.classList.add("hidden");
|
||||
stage.classList.remove("hidden");
|
||||
controls.classList.remove("hidden");
|
||||
|
||||
if (!result.verified) {
|
||||
verifyBanner.className = "banner error";
|
||||
verifyBanner.textContent = "WARNUNG: Hash-Kette dieser Aufzeichnung ist gebrochen -- moeglicherweise nachtraeglich veraendert oder beschaedigt. Wiedergabe ist trotzdem moeglich.";
|
||||
verifyBanner.classList.remove("hidden");
|
||||
}
|
||||
|
||||
if (result.entries.length === 0) {
|
||||
loading.classList.remove("hidden");
|
||||
loading.textContent = "Diese Aufzeichnung enthaelt keine Eintraege.";
|
||||
stage.classList.add("hidden");
|
||||
controls.classList.add("hidden");
|
||||
return;
|
||||
}
|
||||
|
||||
activePlayer = result.protocol === "rdp" ? createRdpPlayer(result.entries) : createSshPlayer(result.entries);
|
||||
} catch (err) {
|
||||
showBanner(err.message, "error");
|
||||
loading.classList.remove("hidden");
|
||||
loading.textContent = "Fehler beim Laden der Aufzeichnung: " + err.message;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -46,6 +46,10 @@
|
||||
// reicht ihn als Guacamole.Status.message hierher durch.
|
||||
statusEl.textContent = "Fehler: " + ((err && err.message) || "unbekannt");
|
||||
statusEl.classList.add("error");
|
||||
// Troubleshooting: Statuscode + Rohobjekt zusaetzlich in die
|
||||
// Browser-Konsole, fuer den Fall, dass die Klartextmeldung allein nicht
|
||||
// reicht (z.B. guacd/FreeRDP-interne Codes).
|
||||
console.error("RDP-Sitzung beendet/Fehler:", err);
|
||||
};
|
||||
client.onerror = showError;
|
||||
tunnel.onerror = showError;
|
||||
@ -91,4 +95,16 @@
|
||||
document.exitFullscreen();
|
||||
}
|
||||
});
|
||||
|
||||
// Strg+Alt+Entf: der Browser faengt diese Kombination selbst ab (Windows
|
||||
// reserviert sie systemweit), sie kommt also nie als normales Tastatur-
|
||||
// Event beim RDP-Ziel an. Abhilfe wie im offiziellen Guacamole-Client:
|
||||
// die drei Tasten einzeln als synthetische Key-Events senden (X11-
|
||||
// Keysyms: Ctrl=0xFFE3, Alt=0xFFE9, Delete=0xFFFF), erst alle drei
|
||||
// herunter- dann in umgekehrter Reihenfolge wieder hochdruecken.
|
||||
document.getElementById("ctrlaltdel-btn").addEventListener("click", () => {
|
||||
const keys = [0xffe3, 0xffe9, 0xffff];
|
||||
for (const keysym of keys) client.sendKeyEvent(1, keysym);
|
||||
for (const keysym of keys.slice().reverse()) client.sendKeyEvent(0, keysym);
|
||||
});
|
||||
})();
|
||||
|
||||
@ -37,6 +37,14 @@
|
||||
return decodeURIComponent(escape(atob(b64)));
|
||||
}
|
||||
|
||||
// Troubleshooting: der Server liefert bei einem Fehlschlag entweder eine
|
||||
// {"type":"error",...}-Nachricht ODER (wenn die Sitzung nie so weit kam,
|
||||
// dass eine Nachricht gesendet werden konnte -- z.B. fehlende
|
||||
// Berechtigung) eine WebSocket-Close-Reason (siehe
|
||||
// app/ssh_proxy/terminal_ws.py::_reject). Beides wird angezeigt statt
|
||||
// eines generischen "Verbindung beendet" ohne jeden Grund.
|
||||
let lastErrorMessage = null;
|
||||
|
||||
const proto = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||
const ws = new WebSocket(`${proto}//${window.location.host}/ws/ssh/${hostId}`);
|
||||
|
||||
@ -44,15 +52,35 @@
|
||||
statusEl.textContent = "Verbunden";
|
||||
sendResize();
|
||||
});
|
||||
ws.addEventListener("close", () => { statusEl.textContent = "Verbindung beendet"; });
|
||||
ws.addEventListener("error", () => { statusEl.textContent = "Verbindungsfehler"; });
|
||||
ws.addEventListener("close", (ev) => {
|
||||
let text;
|
||||
if (lastErrorMessage) {
|
||||
text = "Verbindung beendet: " + lastErrorMessage;
|
||||
} else if (ev.reason) {
|
||||
text = `Verbindung beendet (Code ${ev.code}): ${ev.reason}`;
|
||||
} else if (ev.code && ev.code !== 1000) {
|
||||
text = `Verbindung beendet (Code ${ev.code})`;
|
||||
} else {
|
||||
text = "Verbindung beendet";
|
||||
}
|
||||
statusEl.textContent = text;
|
||||
statusEl.classList.add("error");
|
||||
console.error("SSH-Sitzung beendet:", { code: ev.code, reason: ev.reason, lastErrorMessage });
|
||||
});
|
||||
ws.addEventListener("error", (ev) => {
|
||||
statusEl.textContent = "Verbindungsfehler (WebSocket) -- Details siehe Browser-Konsole";
|
||||
statusEl.classList.add("error");
|
||||
console.error("SSH-WebSocket-Fehler:", ev);
|
||||
});
|
||||
|
||||
ws.addEventListener("message", (ev) => {
|
||||
const msg = JSON.parse(ev.data);
|
||||
if (msg.type === "output") {
|
||||
term.write(b64decode(msg.data));
|
||||
} else if (msg.type === "error") {
|
||||
lastErrorMessage = msg.message;
|
||||
statusEl.textContent = "Fehler: " + msg.message;
|
||||
statusEl.classList.add("error");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user