umbau 1.0
This commit is contained in:
532
static/js/workspace.js
Normal file
532
static/js/workspace.js
Normal file
@ -0,0 +1,532 @@
|
||||
/*
|
||||
* Arbeitsflaeche (Umsetzungsauftrag_Sonnet5.md Teil F.3): Seitenleiste +
|
||||
* Hauptbereich. Nutzt dieselben Bausteine wie /terminal/{id} und /rdp/{id}
|
||||
* (SshTerminalSession/RdpSession aus terminal.js/rdp.js, Stufe F1) -- keine
|
||||
* zweite Implementierung (F.2).
|
||||
*
|
||||
* Stufe F4 (Umsetzungsauftrag_Sonnet5.md Teil F.3.2-F.3.4): mehrere
|
||||
* gleichzeitige Sitzungen. Jede geoeffnete Sitzung bleibt als eigene
|
||||
* SshTerminalSession/RdpSession-Instanz im DOM bestehen (this.element wird
|
||||
* NICHT entfernt, wenn man wegschaltet) -- nur ueber die bestehende
|
||||
* `.hidden`-Klasse (app.css, CSP-konformer Sichtbarkeits-Toggle wie an
|
||||
* anderer Stelle im Projekt) ein-/ausgeblendet. Genau eine ist gleichzeitig
|
||||
* sichtbar/aktiv (setActive(true)); alle anderen bleiben im Hintergrund
|
||||
* voll verbunden (Betreiberentscheidung dieser Session, siehe
|
||||
* FORTSETZUNG_Teil_F.md Abschnitt 0 Punkt 2 -- kein Pausenzustand).
|
||||
*
|
||||
* Bekannter Rest-Umfang dieser Stufe (siehe FORTSETZUNG_Teil_F.md):
|
||||
* - Server-Sitzungs-ID-Zuordnung ist "best effort" (claimServerSessionId
|
||||
* unten), weil terminal_ws.py/ws_tunnel.py dem Client seine eigene
|
||||
* session_id an keiner Stelle mitteilen (siehe F3-Limitierung). Loest
|
||||
* das Duplikat-/Verwechslungsrisiko fuer den Normalfall zuverlaessig,
|
||||
* mit einem sehr kleinen, dokumentierten Restrisiko bei echten
|
||||
* Wettlaeufen (siehe unten).
|
||||
* - Keine Live-Aktualisierung der Seitenleiste fuer Sitzungen, die in
|
||||
* ANDEREN Tabs geoeffnet/beendet werden, waehrend diese Seite laeuft --
|
||||
* nur bei Laden/Sitzungsende dieser Seite neu abgefragt (kein Polling).
|
||||
*
|
||||
* Stufe F5 (Umsetzungsauftrag_Sonnet5.md Teil F.3.8 + Feinschliff):
|
||||
* - Obergrenzen sichtbar: GET /catalog/session-limits liefert die
|
||||
* konfigurierten Werte (app/config.py) plus den aktuellen Stand: dieselbe
|
||||
* Logik wie die serverseitige Durchsetzung in terminal_ws.py/
|
||||
* ws_tunnel.py (E.4), NICHT neu erfunden -- nur zum Anzeigen VOR dem
|
||||
* Verbindungsversuch. "+ Neue Sitzung" und jeder "Verbinden"-Knopf im
|
||||
* Katalog werden deaktiviert, sobald eine der beiden Grenzen erreicht
|
||||
* ist. Die serverseitige Durchsetzung (WS-Code 4429) bleibt die
|
||||
* verbindliche Kontrolle -- diese Anzeige ist rein informativ und kann
|
||||
* durch die inhaerente Race zwischen Anzeige-Refresh und tatsaechlichem
|
||||
* Verbindungsaufbau leicht veralten (siehe unten, wie bei
|
||||
* claimServerSessionId: gleiche Klasse von Eventual-Consistency).
|
||||
* - Sammelzustand-Kennzeichnung: eine Hintergrundsitzung mit Fehler oder
|
||||
* Trennung wird auf ihrer Kachel UND in einer Sammel-Zeile ueber der
|
||||
* Kachelliste markiert, damit man das Problem nicht erst durch
|
||||
* Umschalten auf jede einzelne Sitzung entdeckt.
|
||||
*/
|
||||
(() => {
|
||||
"use strict";
|
||||
|
||||
const tilesEl = document.getElementById("workspace-tiles");
|
||||
const catalogViewEl = document.getElementById("workspace-catalog-view");
|
||||
const sessionsAreaEl = document.getElementById("workspace-sessions-area");
|
||||
const newBtn = document.getElementById("workspace-new-btn");
|
||||
const whoamiEl = document.getElementById("whoami");
|
||||
const limitHintEl = document.getElementById("workspace-limit-hint");
|
||||
const attentionSummaryEl = document.getElementById("workspace-attention-summary");
|
||||
|
||||
let cachedHosts = [];
|
||||
let remoteSessions = [];
|
||||
const claimedSessionIds = new Set();
|
||||
const openSessions = []; // { uid, instance, hostId, hostname, protocol, startedAtMs, statusText, isError, elapsedSpan, tileEl, serverId }
|
||||
let activeUid = null; // uid der gerade sichtbaren Sitzung, oder null (Katalog sichtbar)
|
||||
let nextUid = 1;
|
||||
let elapsedTimer = null;
|
||||
let sessionLimits = null; // { max_per_user, current_user_count, user_limit_applies, at_user_limit, max_global, current_global_count, at_global_limit }
|
||||
|
||||
async function getJson(url, opts) {
|
||||
const res = await fetch(url, { credentials: "same-origin", ...(opts || {}) });
|
||||
if (res.status === 401) {
|
||||
window.location.href = "/";
|
||||
throw new Error("nicht angemeldet");
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
|
||||
function findOpenSession(uid) {
|
||||
return openSessions.find((s) => s.uid === uid) || null;
|
||||
}
|
||||
|
||||
// --- Server-Sitzungs-ID best effort zuordnen -------------------------
|
||||
|
||||
async function claimServerSessionId(record) {
|
||||
for (let attempt = 0; attempt < 5; attempt++) {
|
||||
try {
|
||||
const rows = await getJson("/catalog/sessions?active_only=true");
|
||||
const candidates = rows
|
||||
.filter((r) => r.host_id === record.hostId && !claimedSessionIds.has(r.id))
|
||||
.sort((a, b) => new Date(b.started_at) - new Date(a.started_at));
|
||||
if (candidates.length > 0) {
|
||||
record.serverId = candidates[0].id;
|
||||
claimedSessionIds.add(candidates[0].id);
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Sitzungs-ID konnte nicht abgefragt werden:", err);
|
||||
}
|
||||
await new Promise((resolve) => window.setTimeout(resolve, 250));
|
||||
}
|
||||
console.warn(
|
||||
`Server-Sitzungs-ID fuer ${record.hostname} konnte nicht ermittelt werden (best effort -- ` +
|
||||
"siehe FORTSETZUNG_Teil_F.md, Stufe F4, bekannte Limitierung)."
|
||||
);
|
||||
}
|
||||
|
||||
// --- Obergrenzen (F5, F.3.8) -------------------------------------------
|
||||
//
|
||||
// Nur eine Anzeige der bereits bestehenden serverseitigen Durchsetzung
|
||||
// (E.4, terminal_ws.py/ws_tunnel.py) -- siehe Datei-Kopfkommentar.
|
||||
|
||||
async function refreshSessionLimits() {
|
||||
try {
|
||||
sessionLimits = await getJson("/catalog/session-limits");
|
||||
} catch (err) {
|
||||
console.error("Obergrenzen konnten nicht geladen werden:", err);
|
||||
sessionLimits = null;
|
||||
}
|
||||
}
|
||||
|
||||
function atAnyLimit() {
|
||||
return !!sessionLimits && (sessionLimits.at_user_limit || sessionLimits.at_global_limit);
|
||||
}
|
||||
|
||||
function renderLimitHint() {
|
||||
if (!limitHintEl) return;
|
||||
if (!sessionLimits) {
|
||||
limitHintEl.textContent = "";
|
||||
limitHintEl.classList.remove("workspace-limit-hint--at-limit");
|
||||
} else {
|
||||
const parts = [];
|
||||
if (sessionLimits.user_limit_applies) {
|
||||
parts.push(`Eigene Sitzungen: ${sessionLimits.current_user_count}/${sessionLimits.max_per_user}`);
|
||||
}
|
||||
parts.push(`Server gesamt: ${sessionLimits.current_global_count}/${sessionLimits.max_global}`);
|
||||
limitHintEl.textContent = parts.join(" · ");
|
||||
limitHintEl.classList.toggle("workspace-limit-hint--at-limit", atAnyLimit());
|
||||
}
|
||||
const atLimit = atAnyLimit();
|
||||
newBtn.disabled = atLimit;
|
||||
newBtn.title = atLimit
|
||||
? "Obergrenze erreicht (F.3.8) -- erst eine bestehende Sitzung schliessen."
|
||||
: "";
|
||||
}
|
||||
|
||||
// --- Sidebar ----------------------------------------------------------
|
||||
|
||||
function statusClass(text, isError) {
|
||||
if (isError) return "error";
|
||||
if (text === "Verbunden") return "connected";
|
||||
if (text === "Getrennt" || text.startsWith("Verbindung beendet")) return "disconnected";
|
||||
return "connecting";
|
||||
}
|
||||
|
||||
// F5: eine Hintergrundsitzung mit Fehler oder Trennung braucht
|
||||
// Aufmerksamkeit, ohne dass man erst umschaltet, um es zu sehen.
|
||||
function hasAttention(record) {
|
||||
const cls = statusClass(record.statusText, record.isError);
|
||||
return cls === "error" || cls === "disconnected";
|
||||
}
|
||||
|
||||
function formatElapsed(startedAtMs) {
|
||||
const secs = Math.max(0, Math.floor((Date.now() - startedAtMs) / 1000));
|
||||
const m = Math.floor(secs / 60);
|
||||
const s = secs % 60;
|
||||
return `${m}:${String(s).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
function buildLocalTile(record) {
|
||||
const isActive = record.uid === activeUid;
|
||||
const tile = document.createElement("div");
|
||||
tile.className = "workspace-tile " + (isActive ? "workspace-tile-active" : "workspace-tile-background");
|
||||
tile.tabIndex = 0;
|
||||
|
||||
const top = document.createElement("div");
|
||||
top.className = "workspace-tile-top";
|
||||
const dot = document.createElement("span");
|
||||
dot.className = "workspace-status-dot " + statusClass(record.statusText, record.isError);
|
||||
top.appendChild(dot);
|
||||
const name = document.createElement("span");
|
||||
name.className = "workspace-tile-hostname";
|
||||
name.textContent = record.hostname;
|
||||
top.appendChild(name);
|
||||
const proto = document.createElement("span");
|
||||
proto.className = "badge";
|
||||
proto.textContent = record.protocol.toUpperCase();
|
||||
top.appendChild(proto);
|
||||
tile.appendChild(top);
|
||||
|
||||
const meta = document.createElement("div");
|
||||
meta.className = "workspace-tile-meta";
|
||||
const elapsedSpan = document.createElement("span");
|
||||
elapsedSpan.className = "workspace-tile-elapsed";
|
||||
elapsedSpan.textContent = formatElapsed(record.startedAtMs);
|
||||
meta.appendChild(elapsedSpan);
|
||||
const statusSpan = document.createElement("span");
|
||||
let bgLabel = "im Hintergrund verbunden";
|
||||
if (!isActive && hasAttention(record)) {
|
||||
tile.classList.add("workspace-tile-attention");
|
||||
bgLabel = statusClass(record.statusText, record.isError) === "error"
|
||||
? "im Hintergrund: Fehler"
|
||||
: "im Hintergrund: getrennt";
|
||||
}
|
||||
statusSpan.textContent = isActive ? record.statusText : bgLabel;
|
||||
meta.appendChild(statusSpan);
|
||||
tile.appendChild(meta);
|
||||
|
||||
const closeBtn = document.createElement("button");
|
||||
closeBtn.type = "button";
|
||||
closeBtn.className = "btn-secondary btn-small workspace-tile-close-btn";
|
||||
closeBtn.textContent = "Schliessen";
|
||||
closeBtn.addEventListener("click", (ev) => {
|
||||
ev.stopPropagation();
|
||||
closeLocalSession(record.uid);
|
||||
});
|
||||
tile.appendChild(closeBtn);
|
||||
|
||||
if (!isActive) {
|
||||
tile.addEventListener("click", () => showSession(record.uid));
|
||||
tile.addEventListener("keydown", (ev) => {
|
||||
if (ev.key === "Enter" || ev.key === " ") showSession(record.uid);
|
||||
});
|
||||
}
|
||||
|
||||
record.elapsedSpan = elapsedSpan;
|
||||
return tile;
|
||||
}
|
||||
|
||||
function buildRemoteTile(session) {
|
||||
const tile = document.createElement("div");
|
||||
tile.className = "workspace-tile workspace-tile-remote";
|
||||
|
||||
const top = document.createElement("div");
|
||||
top.className = "workspace-tile-top";
|
||||
const dot = document.createElement("span");
|
||||
dot.className = "workspace-status-dot connected";
|
||||
top.appendChild(dot);
|
||||
const name = document.createElement("span");
|
||||
name.className = "workspace-tile-hostname";
|
||||
name.textContent = session.hostname;
|
||||
top.appendChild(name);
|
||||
const proto = document.createElement("span");
|
||||
proto.className = "badge";
|
||||
proto.textContent = session.protocol.toUpperCase();
|
||||
top.appendChild(proto);
|
||||
tile.appendChild(top);
|
||||
|
||||
const meta = document.createElement("div");
|
||||
meta.className = "workspace-tile-meta";
|
||||
meta.textContent = "laeuft in einem anderen Tab/Fenster";
|
||||
tile.appendChild(meta);
|
||||
|
||||
const closeBtn = document.createElement("button");
|
||||
closeBtn.type = "button";
|
||||
closeBtn.className = "btn-secondary btn-small workspace-tile-close-btn";
|
||||
closeBtn.textContent = "Beenden";
|
||||
closeBtn.addEventListener("click", async () => {
|
||||
closeBtn.disabled = true;
|
||||
closeBtn.textContent = "Wird beendet ...";
|
||||
try {
|
||||
const res = await fetch(`/catalog/sessions/${session.id}/terminate`, {
|
||||
method: "POST", credentials: "same-origin",
|
||||
});
|
||||
if (!res.ok && res.status !== 409) throw new Error(`HTTP ${res.status}`);
|
||||
} catch (err) {
|
||||
console.error("Sitzung konnte nicht beendet werden:", err);
|
||||
}
|
||||
await Promise.all([refreshRemoteSessions(), refreshSessionLimits()]);
|
||||
renderSidebar();
|
||||
});
|
||||
tile.appendChild(closeBtn);
|
||||
|
||||
return tile;
|
||||
}
|
||||
|
||||
function renderSidebar() {
|
||||
renderLimitHint();
|
||||
|
||||
// F5: Sammelzustand -- Hintergrundsitzungen mit Fehler/Trennung ueber
|
||||
// der Kachelliste zusammenfassen (die aktive Sitzung zaehlt nicht mit,
|
||||
// deren Zustand sieht man ohnehin direkt).
|
||||
if (attentionSummaryEl) {
|
||||
const attentionCount = openSessions.filter((r) => r.uid !== activeUid && hasAttention(r)).length;
|
||||
if (attentionCount > 0) {
|
||||
attentionSummaryEl.textContent = attentionCount === 1
|
||||
? "⚠ 1 Sitzung im Hintergrund mit Problem (Fehler/getrennt)."
|
||||
: `⚠ ${attentionCount} Sitzungen im Hintergrund mit Problem (Fehler/getrennt).`;
|
||||
attentionSummaryEl.classList.remove("hidden");
|
||||
} else {
|
||||
attentionSummaryEl.textContent = "";
|
||||
attentionSummaryEl.classList.add("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
tilesEl.innerHTML = "";
|
||||
for (const record of openSessions) {
|
||||
tilesEl.appendChild(buildLocalTile(record));
|
||||
}
|
||||
for (const s of remoteSessions) {
|
||||
tilesEl.appendChild(buildRemoteTile(s));
|
||||
}
|
||||
if (openSessions.length === 0 && remoteSessions.length === 0) {
|
||||
const empty = document.createElement("div");
|
||||
empty.className = "hint workspace-empty-hint";
|
||||
empty.textContent = "Keine offenen Sitzungen.";
|
||||
tilesEl.appendChild(empty);
|
||||
}
|
||||
|
||||
if (elapsedTimer) {
|
||||
window.clearInterval(elapsedTimer);
|
||||
elapsedTimer = null;
|
||||
}
|
||||
if (openSessions.length > 0) {
|
||||
elapsedTimer = window.setInterval(() => {
|
||||
for (const record of openSessions) {
|
||||
if (record.elapsedSpan) record.elapsedSpan.textContent = formatElapsed(record.startedAtMs);
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshRemoteSessions() {
|
||||
try {
|
||||
const rows = await getJson("/catalog/sessions?active_only=true");
|
||||
// Best-effort-Filter: alle bereits als lokal geoeffnet zugeordneten
|
||||
// Server-Sitzungen (claimServerSessionId) duerfen NICHT zusaetzlich
|
||||
// als "laeuft anderswo" auftauchen -- sonst erschiene jede eigene
|
||||
// Sitzung dieser Seite doppelt.
|
||||
remoteSessions = rows.filter((r) => !claimedSessionIds.has(r.id));
|
||||
} catch (err) {
|
||||
console.error("Eigene Sitzungen konnten nicht geladen werden:", err);
|
||||
remoteSessions = [];
|
||||
}
|
||||
}
|
||||
|
||||
// --- Hauptbereich: Hostkatalog ---------------------------------------
|
||||
|
||||
function hostCard(host) {
|
||||
const div = document.createElement("div");
|
||||
div.className = "host-card";
|
||||
|
||||
const name = document.createElement("div");
|
||||
name.className = "hostname";
|
||||
name.textContent = host.hostname;
|
||||
div.appendChild(name);
|
||||
|
||||
const meta = document.createElement("div");
|
||||
meta.className = "meta";
|
||||
meta.textContent = `${host.protocol.toUpperCase()} · ${host.os_type} · ${host.address}`;
|
||||
div.appendChild(meta);
|
||||
|
||||
const actions = document.createElement("div");
|
||||
actions.className = "actions";
|
||||
const connectBtn = document.createElement("button");
|
||||
connectBtn.type = "button";
|
||||
connectBtn.textContent = "Verbinden";
|
||||
if (atAnyLimit()) {
|
||||
// F5 (F.3.8): Obergrenze bereits VOR dem Verbindungsversuch sichtbar
|
||||
// machen, statt den Nutzer erst beim WS-Handshake mit Code 4429
|
||||
// abzuweisen (serverseitige Durchsetzung bleibt unveraendert, siehe
|
||||
// Datei-Kopfkommentar).
|
||||
connectBtn.disabled = true;
|
||||
connectBtn.title = "Obergrenze erreicht (F.3.8) -- erst eine bestehende Sitzung schliessen.";
|
||||
}
|
||||
connectBtn.addEventListener("click", () => startSession(host));
|
||||
actions.appendChild(connectBtn);
|
||||
div.appendChild(actions);
|
||||
|
||||
return div;
|
||||
}
|
||||
|
||||
function renderCatalog() {
|
||||
catalogViewEl.innerHTML = "";
|
||||
const wrap = document.createElement("div");
|
||||
wrap.className = "container wide workspace-catalog";
|
||||
|
||||
const heading = document.createElement("h2");
|
||||
heading.textContent = "Host waehlen";
|
||||
wrap.appendChild(heading);
|
||||
|
||||
if (cachedHosts.length === 0) {
|
||||
const hint = document.createElement("p");
|
||||
hint.className = "hint";
|
||||
hint.textContent = "Keine Hosts zugewiesen. Bitte an einen Administrator wenden.";
|
||||
wrap.appendChild(hint);
|
||||
} else {
|
||||
const list = document.createElement("div");
|
||||
list.className = "host-list";
|
||||
for (const host of cachedHosts) list.appendChild(hostCard(host));
|
||||
wrap.appendChild(list);
|
||||
}
|
||||
catalogViewEl.appendChild(wrap);
|
||||
}
|
||||
|
||||
// --- Umschalten zwischen Katalog und Sitzungen -------------------------
|
||||
|
||||
function showCatalog() {
|
||||
activeUid = null;
|
||||
for (const record of openSessions) {
|
||||
record.instance.element.classList.add("hidden");
|
||||
record.instance.setActive(false);
|
||||
}
|
||||
sessionsAreaEl.classList.add("hidden");
|
||||
catalogViewEl.classList.remove("hidden");
|
||||
renderCatalog();
|
||||
renderSidebar();
|
||||
}
|
||||
|
||||
function showSession(uid) {
|
||||
const target = findOpenSession(uid);
|
||||
if (!target) return;
|
||||
activeUid = uid;
|
||||
for (const record of openSessions) {
|
||||
const visible = record.uid === uid;
|
||||
record.instance.element.classList.toggle("hidden", !visible);
|
||||
// F4 (F.3.2/F.3.3): setActive() steuert sowohl den Tastatur-/Paste-
|
||||
// Filter (RdpSession) als auch den Refit beim Sichtbarwerden (beide
|
||||
// Klassen) -- siehe terminal.js/rdp.js.
|
||||
record.instance.setActive(visible);
|
||||
}
|
||||
catalogViewEl.classList.add("hidden");
|
||||
sessionsAreaEl.classList.remove("hidden");
|
||||
renderSidebar();
|
||||
}
|
||||
|
||||
// --- Sitzung starten/beenden -------------------------------------------
|
||||
|
||||
function startSession(host) {
|
||||
const uid = nextUid++;
|
||||
const options = {
|
||||
onStatusChange: (text, isError) => {
|
||||
const record = findOpenSession(uid);
|
||||
if (!record) return;
|
||||
record.statusText = text;
|
||||
record.isError = isError;
|
||||
renderSidebar();
|
||||
},
|
||||
onExitRequested: () => {
|
||||
// Der interne Exit-Knopf der Sitzung selbst wurde geklickt -- dessen
|
||||
// dispose() lief bereits, hier nur noch den Arbeitsflaechen-Zustand
|
||||
// nachziehen (kein zweiter dispose()-Aufruf).
|
||||
handleSessionClosed(uid, /* alreadyDisposed */ true);
|
||||
},
|
||||
};
|
||||
const instance = host.protocol === "rdp" ? new RdpSession(host.id, options) : new SshTerminalSession(host.id, options);
|
||||
const record = {
|
||||
uid, instance, hostId: host.id, hostname: host.hostname, protocol: host.protocol,
|
||||
startedAtMs: Date.now(), statusText: "Verbinde ...", isError: false, serverId: null,
|
||||
};
|
||||
openSessions.push(record);
|
||||
instance.element.classList.add("hidden");
|
||||
sessionsAreaEl.appendChild(instance.element);
|
||||
instance.connect();
|
||||
claimServerSessionId(record);
|
||||
showSession(uid);
|
||||
|
||||
// F5: Obergrenzen-Anzeige nachziehen -- der Server registriert die neue
|
||||
// Sitzung (app/security/active_sessions.py::register) erst, nachdem der
|
||||
// WS-Handshake durchgelaufen ist, daher dieselbe kleine Verzoegerung wie
|
||||
// bei handleSessionClosed() unten statt eines sofortigen (noch
|
||||
// veralteten) Refreshs.
|
||||
window.setTimeout(() => {
|
||||
refreshSessionLimits().then(() => renderSidebar());
|
||||
}, 300);
|
||||
}
|
||||
|
||||
function closeLocalSession(uid) {
|
||||
const record = findOpenSession(uid);
|
||||
if (!record) return;
|
||||
record.instance.dispose(); // idempotent
|
||||
handleSessionClosed(uid, /* alreadyDisposed */ true);
|
||||
}
|
||||
|
||||
function handleSessionClosed(uid, alreadyDisposed) {
|
||||
const idx = openSessions.findIndex((s) => s.uid === uid);
|
||||
if (idx === -1) return;
|
||||
const [record] = openSessions.splice(idx, 1);
|
||||
if (!alreadyDisposed) record.instance.dispose();
|
||||
if (record.serverId !== null) claimedSessionIds.delete(record.serverId);
|
||||
|
||||
if (activeUid === uid) {
|
||||
const next = openSessions[openSessions.length - 1];
|
||||
if (next) {
|
||||
showSession(next.uid);
|
||||
} else {
|
||||
showCatalog();
|
||||
}
|
||||
} else {
|
||||
renderSidebar();
|
||||
}
|
||||
|
||||
// Kleine Verzoegerung: der serverseitige finally-Block (WS-Handler)
|
||||
// setzt ended_at erst, nachdem der Socket-Close tatsaechlich verarbeitet
|
||||
// wurde -- ohne Verzoegerung koennte die soeben beendete Sitzung noch
|
||||
// kurz als aktiv gelistet werden (unkritisch, naechster Refresh raeumt
|
||||
// es auf, siehe FORTSETZUNG_Teil_F.md).
|
||||
window.setTimeout(() => {
|
||||
Promise.all([refreshRemoteSessions(), refreshSessionLimits()]).then(() => renderSidebar());
|
||||
}, 300);
|
||||
}
|
||||
|
||||
newBtn.addEventListener("click", () => showCatalog());
|
||||
|
||||
// --- Start ------------------------------------------------------------
|
||||
|
||||
document.getElementById("logout-btn").addEventListener("click", async () => {
|
||||
await fetch("/auth/logout", { method: "POST", credentials: "same-origin" });
|
||||
window.location.href = "/";
|
||||
});
|
||||
|
||||
async function main() {
|
||||
const me = await getJson("/auth/me");
|
||||
whoamiEl.textContent = `${me.username}${me.is_admin ? " (Admin)" : ""}`;
|
||||
|
||||
cachedHosts = await getJson("/catalog/hosts");
|
||||
await refreshRemoteSessions();
|
||||
await refreshSessionLimits();
|
||||
renderCatalog();
|
||||
renderSidebar();
|
||||
|
||||
// ?host=<id> (von dashboard.js "Verbinden" gesetzt): direkt verbinden
|
||||
// statt erst den Katalog zu zeigen.
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const hostParam = params.get("host");
|
||||
if (hostParam) {
|
||||
const host = cachedHosts.find((h) => String(h.id) === hostParam);
|
||||
if (host) {
|
||||
startSession(host);
|
||||
return;
|
||||
}
|
||||
}
|
||||
showCatalog();
|
||||
}
|
||||
|
||||
main().catch((err) => console.error(err));
|
||||
})();
|
||||
Reference in New Issue
Block a user