|
|
|
|
@ -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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|