"use strict"; const api = window.chjPlugin; const i18n = window.chjPluginI18n.createI18n("cs"); const t = i18n.t; let endedSessionId = ""; let sessionsLoading = false; // Guard every continuation, including sudo retries and dialog answers, against a server switch. async function sessionResult(promise, token) { const value = await promise; if (token !== generation) throw new Error("CHJ_SESSION_CHANGED"); return value; } const elements = Object.fromEntries([ "pluginVersion", "closeButton", "sessionSelect", "refreshButton", "sudoPassword", "testButton", "reloadButton", "status", "nginxVersion", "serviceState", "mainConfig", "configCount", "dumpButton", "configFilter", "configList", "editorMode", "editorTitle", "editorMeta", "revertButton", "saveButton", "configEditor", "commandOutput", "clearOutputButton" ].map((id) => [id, document.querySelector(`#${id}`)])); let sessions = []; let inspection = null; let selectedPath = ""; let selectedKind = ""; let originalText = ""; let busy = false; let generation = 0; let sessionId = ""; let editorSessionId = ""; let disconnectedDraft = false; // Rebuilds the line under the editor title after a language change. let editorMetaText = null; function selectedSession() { return sessions.find((session) => session.sessionId === elements.sessionSelect.value) || null; } function sudoPassword() { return elements.sudoPassword.value; } function setStatus(message, error = false) { elements.status.textContent = message; elements.status.classList.toggle("error", error); } function setOutput(text, error = false) { elements.commandOutput.textContent = String(text || "—"); elements.commandOutput.classList.toggle("error", error); } function isDirty() { return disconnectedDraft || (Boolean(selectedPath) && elements.configEditor.value !== originalText); } function selectedWritable() { return selectedKind === "file" && Boolean(selectedPath); } function syncControls() { const connected = Boolean(elements.sessionSelect.value); elements.sessionSelect.disabled = busy; elements.refreshButton.disabled = busy; elements.testButton.disabled = busy || !connected || !inspection?.installed; elements.reloadButton.disabled = busy || !connected || !inspection?.installed; elements.dumpButton.disabled = busy || !connected || !inspection?.installed; elements.revertButton.disabled = busy || !selectedPath; elements.saveButton.disabled = busy || !selectedWritable() || !isDirty(); elements.configEditor.readOnly = busy || !selectedWritable(); } function setBusy(value) { busy = value; syncControls(); } function renderIdentity() { elements.nginxVersion.textContent = inspection?.installed ? (inspection.version || t("nainstalován")) : t("nenalezen"); elements.serviceState.textContent = inspection?.serviceState || "—"; elements.serviceState.className = inspection?.serviceState === "active" ? "state-active" : "state-inactive"; elements.mainConfig.textContent = inspection?.configPath || "—"; elements.mainConfig.title = inspection?.configPath || ""; elements.configCount.textContent = String(inspection?.configs?.length || 0); } function renderConfigList() { const query = elements.configFilter.value.trim().toLowerCase(); const files = (inspection?.configs || []).filter((item) => item.path.toLowerCase().includes(query)); const fragment = document.createDocumentFragment(); for (const file of files) { const button = document.createElement("button"); button.type = "button"; button.className = `config-item${file.path === selectedPath ? " active" : ""}`; button.dataset.path = file.path; button.dataset.kind = file.kind; const path = document.createElement("span"); path.textContent = file.path; const type = document.createElement("small"); type.textContent = file.kind === "symlink" ? t("symlink · pouze čtení") : t("soubor"); button.append(path, type); fragment.append(button); } elements.configList.replaceChildren(fragment); if (!files.length) { const empty = document.createElement("p"); empty.className = "empty"; empty.textContent = t("Filtru neodpovídá žádný konfigurační soubor."); elements.configList.replaceChildren(empty); } } function resetSession(id) { generation += 1; sessionId = id; editorSessionId = ""; disconnectedDraft = false; inspection = null; selectedPath = ""; selectedKind = ""; originalText = ""; elements.configEditor.value = ""; elements.sudoPassword.value = ""; elements.editorTitle.textContent = "—"; elements.editorMeta.textContent = ""; editorMetaText = null; elements.configFilter.value = ""; setOutput(""); renderIdentity(); renderConfigList(); setBusy(false); } async function loadSessions() { if (sessionsLoading) return; sessionsLoading = true; try { sessions = (await api.sessions.list()).filter((session) => session.state === "connected"); const next = api.sessions.reconcile(endedSessionId || sessionId, sessions); elements.sessionSelect.replaceChildren(new Option(sessions.length ? t("Vyberte relaci…") : t("Žádná připojená relace"), "")); for (const session of sessions) elements.sessionSelect.add(new Option(api.sessions.displayName(session), session.sessionId)); elements.sessionSelect.value = next.id; if (next.reason === "choose") api.sessions.offerChoice(elements.sessionSelect, sessions, t("Vyberte relaci…")); if (next.reason === "gone") endedSessionId ||= sessionId; if (next.id !== sessionId) { // A disconnected editor stays available for copying unsaved text, but cannot save. const draft = isDirty() ? elements.configEditor.value : null; resetSession(next.id); if (draft !== null) { elements.configEditor.value = draft; disconnectedDraft = true; } if (next.id) void inspect(); } if (next.reason === "gone") setStatus(t("Relace skončila. Vyberte server."), true); } finally { sessionsLoading = false; } } async function inspect() { const requestGeneration = generation; if (!elements.sessionSelect.value) { inspection = null; renderIdentity(); renderConfigList(); syncControls(); return; } setBusy(true); setStatus(t("Zjišťuji instalaci, službu a konfigurační soubory NGINX…")); try { inspection = await sessionResult(api.nginx.inspect(elements.sessionSelect.value), requestGeneration); renderIdentity(); renderConfigList(); setStatus(inspection.installed ? t("NGINX {version} je dostupný; nalezeno {count} souborů.", { version: inspection.version || "", count: inspection.configs.length }) : t("NGINX nebyl na serveru nalezen."), !inspection.installed); } catch (error) { if (requestGeneration !== generation) return; inspection = null; renderIdentity(); renderConfigList(); setStatus(error?.message || String(error), true); } finally { if (requestGeneration === generation) { setBusy(false);} } } async function readConfig(path, kind) { const requestGeneration = generation; if (isDirty() && !window.confirm(t("Zahodit neuložené změny aktuální konfigurace?"))) return; setBusy(true); setStatus(t("Načítám {path}…", { path })); try { const result = await sessionResult(api.nginx.readConfig(elements.sessionSelect.value, { path, sudoPassword: sudoPassword() }), requestGeneration); editorSessionId = sessionId; selectedPath = result.path; selectedKind = kind; originalText = result.text; elements.configEditor.value = result.text; elements.editorMode.textContent = kind === "symlink" ? t("NÁHLED SYMLINKU") : t("EDITOR"); elements.editorTitle.textContent = result.path; editorMetaText = () => `${result.size.toLocaleString(i18n.locale())} B · ${kind === "symlink" ? t("upravte zdroj v sites-available") : t("změny se ukládají se zálohou")}`; elements.editorMeta.textContent = editorMetaText(); renderConfigList(); setStatus(t("Konfigurace byla načtena.")); } catch (error) { if (requestGeneration !== generation) return; setStatus(error?.message || String(error), true); } finally { if (requestGeneration === generation) { elements.sudoPassword.value = ""; setBusy(false);} } } async function runTest() { const requestGeneration = generation; setBusy(true); setStatus(t("Spouštím nginx -t…")); try { const result = await sessionResult(api.nginx.testConfig(elements.sessionSelect.value, sudoPassword()), requestGeneration); setOutput(result.output, !result.ok); setStatus(result.ok ? t("Konfigurace NGINX je platná.") : t("Test konfigurace selhal."), !result.ok); } catch (error) { if (requestGeneration !== generation) return; setOutput(error?.message || String(error), true); setStatus(error?.message || String(error), true); } finally { if (requestGeneration === generation) { elements.sudoPassword.value = ""; setBusy(false);} } } async function dumpEffectiveConfig() { const requestGeneration = generation; if (isDirty() && !window.confirm(t("Zahodit neuložené změny a zobrazit efektivní konfiguraci?"))) return; setBusy(true); setStatus(t("Načítám efektivní konfiguraci pomocí nginx -T…")); try { const result = await sessionResult(api.nginx.dumpConfig(elements.sessionSelect.value, sudoPassword()), requestGeneration); selectedPath = ""; selectedKind = ""; originalText = result.text; elements.configEditor.value = result.text; elements.editorMode.textContent = t("EFEKTIVNÍ KONFIGURACE"); elements.editorTitle.textContent = "nginx -T"; editorMetaText = () => t("Kombinovaný výstup je pouze pro čtení."); elements.editorMeta.textContent = editorMetaText(); renderConfigList(); setStatus(t("Efektivní konfigurace byla načtena.")); } catch (error) { if (requestGeneration !== generation) return; setOutput(error?.message || String(error), true); setStatus(error?.message || String(error), true); } finally { if (requestGeneration === generation) { elements.sudoPassword.value = ""; setBusy(false);} } } async function saveConfig() { const requestGeneration = generation; if (!selectedWritable() || !isDirty()) return; if (!window.confirm(`${api.sessions.displayName(selectedSession())}\n\n${t("Uložit {path}, vytvořit zálohu a spustit nginx -t? Neplatná konfigurace se automaticky vrátí.", { path: selectedPath })}`)) return; setBusy(true); setStatus(t("Ukládám zálohu a ověřuji novou konfiguraci…")); try { const result = await sessionResult(api.nginx.saveConfig(editorSessionId, { path: selectedPath, text: elements.configEditor.value, sudoPassword: sudoPassword() }), requestGeneration); originalText = elements.configEditor.value; setOutput(`${result.testOutput}\n\n${t("Záloha: {path}", { path: result.backupPath })}`); setStatus(t("Konfigurace byla uložena a nginx -t uspěl. Reload zatím nebyl proveden.")); } catch (error) { if (requestGeneration !== generation) return; setOutput(error?.message || String(error), true); setStatus(error?.message || String(error), true); } finally { if (requestGeneration === generation) { elements.sudoPassword.value = ""; setBusy(false);} } } async function reloadNginx() { const requestGeneration = generation; if (isDirty()) { setStatus(t("Nejprve uložte nebo zahoďte neuložené změny."), true); return; } if (!window.confirm(`${api.sessions.displayName(selectedSession())}\n\n${t("Provést graceful reload NGINX? Core před reloadem znovu spustí nginx -t.")}`)) return; setBusy(true); setStatus(t("Ověřuji konfiguraci a provádím graceful reload…")); try { const result = await sessionResult(api.nginx.reload(elements.sessionSelect.value, sudoPassword(), true), requestGeneration); setOutput(result.output); setStatus(t("NGINX byl bezpečně reloadován.")); await sessionResult(inspect(), requestGeneration); } catch (error) { if (requestGeneration !== generation) return; setOutput(error?.message || String(error), true); setStatus(error?.message || String(error), true); } finally { if (requestGeneration === generation) { elements.sudoPassword.value = ""; setBusy(false);} } } elements.closeButton.addEventListener("click", () => api.close()); elements.refreshButton.addEventListener("click", async () => { await loadSessions(); await inspect(); }); elements.sessionSelect.addEventListener("change", async () => { if ((isDirty() || (!sessionId && elements.configEditor.value)) && !window.confirm(t("Zahodit neuložené změny aktuální konfigurace?"))) { elements.sessionSelect.value = sessionId; return; } endedSessionId = ""; resetSession(elements.sessionSelect.value); await inspect(); }); elements.configFilter.addEventListener("input", renderConfigList); elements.configList.addEventListener("click", (event) => { const item = event.target.closest("button[data-path]"); if (item) void readConfig(item.dataset.path, item.dataset.kind); }); elements.configEditor.addEventListener("input", syncControls); elements.revertButton.addEventListener("click", () => { if (selectedPath) void readConfig(selectedPath, selectedKind); }); elements.saveButton.addEventListener("click", saveConfig); elements.testButton.addEventListener("click", runTest); elements.dumpButton.addEventListener("click", dumpEffectiveConfig); elements.reloadButton.addEventListener("click", reloadNginx); elements.clearOutputButton.addEventListener("click", () => setOutput("")); window.addEventListener("beforeunload", (event) => { if (isDirty()) { event.preventDefault(); event.returnValue = ""; } }); (async () => { try { i18n.setLanguage(await api.ui.getLanguage()); } catch {} i18n.apply(document); api.ui.onLanguageChanged((language) => { i18n.setLanguage(language); i18n.apply(document); renderIdentity(); renderConfigList(); if (editorMetaText) { elements.editorMeta.textContent = editorMetaText(); elements.editorMode.textContent = selectedKind === "symlink" ? t("NÁHLED SYMLINKU") : t("EDITOR"); } }); const info = await api.getInfo(); elements.pluginVersion.textContent = `${info.name} v${info.version} · ${info.publisher}`; await loadSessions(); await inspect(); })().catch((error) => setStatus(error?.message || String(error), true)); setInterval(() => { if (!document.hidden) void loadSessions().catch(() => {}); }, 3000);