CH-J Toolsserverbeheer over SSH
Menu
Gepubliceerde bron

CH-J Tools

Blader door mappen en bestanden voor een specifieke toepassing release.

Download bron ZIP
CH-J / Server Manager 1704 bestanden
0.1.2-beta Toegevoegd: · Gebouwd:
index.js Toegevoegd: Datum niet geregistreerd 16,5 KB · 245 regels
Rubriek Bestand downloaden
1"use strict";
3const api = window.chjPlugin;
4const i18n = window.chjPluginI18n.createI18n("cs");
5const t = i18n.t;
6let endedSessionId = "";
7let sessionsLoading = false;
9// Guard every continuation, including sudo retries and dialog answers, against a server switch.
10async function sessionResult(promise, token) {
11 const value = await promise;
12 if (token !== generation) throw new Error("CHJ_SESSION_CHANGED");
13 return value;
16const elements = Object.fromEntries([
17 "pluginVersion", "closeButton", "sessionSelect", "refreshButton", "sudoPassword", "testButton", "reloadButton", "status",
18 "nginxVersion", "serviceState", "mainConfig", "configCount", "dumpButton", "configFilter", "configList", "editorMode",
19 "editorTitle", "editorMeta", "revertButton", "saveButton", "configEditor", "commandOutput", "clearOutputButton",
20 "rememberSudo", "linkTargetButton", "historyButton"
21].map((id) => [id, document.querySelector(`#${id}`)]));
23let sessions = [];
24let inspection = null;
25let selectedPath = "";
26let selectedKind = "";
27let originalText = "";
28let originalSha256 = "";
29let busy = false;
30let generation = 0;
31let sessionId = "";
32let editorSessionId = "";
33let disconnectedDraft = false;
35// Rebuilds the line under the editor title after a language change.
36let editorMetaText = null;
37function selectedSession() { return sessions.find((session) => session.sessionId === elements.sessionSelect.value) || null; }
38function sudoPassword() { return elements.sudoPassword.value; }
39function rememberSudo() { return elements.rememberSudo.checked; }
40function setStatus(message, error = false) { elements.status.textContent = message; elements.status.classList.toggle("error", error); }
41function setOutput(text, error = false) { elements.commandOutput.textContent = String(text || "—"); elements.commandOutput.classList.toggle("error", error); }
42function isDirty() { return disconnectedDraft || (Boolean(selectedPath) && elements.configEditor.value !== originalText); }
43function selectedWritable() { return selectedKind === "file" && Boolean(selectedPath); }
45function syncControls() {
46 const connected = Boolean(elements.sessionSelect.value);
47 elements.sessionSelect.disabled = busy;
48 elements.refreshButton.disabled = busy;
49 elements.testButton.disabled = busy || !connected || !inspection?.installed;
50 elements.reloadButton.disabled = busy || !connected || !inspection?.installed;
51 elements.dumpButton.disabled = busy || !connected || !inspection?.installed;
52 elements.revertButton.disabled = busy || !selectedPath;
53 elements.historyButton.disabled = busy || !selectedWritable();
54 elements.saveButton.disabled = busy || !selectedWritable() || !isDirty();
55 elements.configEditor.readOnly = busy || !selectedWritable();
58function setBusy(value) { busy = value; syncControls(); }
60function renderIdentity() {
61 elements.nginxVersion.textContent = inspection?.installed ? (inspection.version || t("nainstalován")) : t("nenalezen");
62 elements.serviceState.textContent = inspection?.serviceState || "—";
63 elements.serviceState.className = inspection?.serviceState === "active" ? "state-active" : "state-inactive";
64 elements.mainConfig.textContent = inspection?.configPath || "—";
65 elements.mainConfig.title = inspection?.configPath || "";
66 elements.configCount.textContent = String(inspection?.configs?.length || 0);
69function renderConfigList() {
70 const query = elements.configFilter.value.trim().toLowerCase();
71 const files = configEntries().filter((item) => item.path.toLowerCase().includes(query));
72 const fragment = document.createDocumentFragment();
73 for (const file of files) {
74 const button = document.createElement("button");
75 button.type = "button";
76 button.className = `config-item${file.path === selectedPath ? " active" : ""}`;
77 button.dataset.path = file.path;
78 button.dataset.kind = file.kind;
79 const path = document.createElement("span"); path.textContent = file.path;
80 const type = document.createElement("small");
81 const kindText = file.kind === "external" ? t("mimo spravované adresáře · pouze čtení") : file.kind === "symlink" ? t("symlink · pouze čtení") : t("soubor");
82 type.textContent = file.known ? `${kindText} · ${file.loaded ? t("NGINX ho načítá") : t("NGINX ho nenačítá")}` : kindText;
83 if (file.loaded) type.className = "loaded";
84 button.append(path, type); fragment.append(button);
85 }
86 elements.configList.replaceChildren(fragment);
87 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); }
90function resetSession(id) {
91 generation += 1; sessionId = id; editorSessionId = ""; disconnectedDraft = false;
92 inspection = null; selectedPath = ""; selectedKind = ""; originalText = ""; originalSha256 = "";
93 elements.rememberSudo.checked = false; elements.linkTargetButton.hidden = true; resetSites();
94 elements.configEditor.value = ""; elements.sudoPassword.value = "";
95 elements.editorTitle.textContent = "—"; elements.editorMeta.textContent = ""; editorMetaText = null;
96 elements.configFilter.value = ""; setOutput("");
97 renderIdentity(); renderConfigList(); setBusy(false);
100async function loadSessions() {
101 if (sessionsLoading) return;
102 sessionsLoading = true;
103 try {
104 sessions = (await api.sessions.list()).filter((session) => session.state === "connected");
105 const next = api.sessions.reconcile(endedSessionId || sessionId, sessions);
106 elements.sessionSelect.replaceChildren(new Option(sessions.length ? t("Vyberte relaci…") : t("Žádná připojená relace"), ""));
107 for (const session of sessions) elements.sessionSelect.add(new Option(api.sessions.displayName(session), session.sessionId));
108 elements.sessionSelect.value = next.id;
109 if (next.reason === "choose") api.sessions.offerChoice(elements.sessionSelect, sessions, t("Vyberte relaci…"));
110 if (next.reason === "gone") endedSessionId ||= sessionId;
111 if (next.id !== sessionId) {
112 // A disconnected editor stays available for copying unsaved text, but cannot save.
113 const draft = isDirty() ? elements.configEditor.value : null;
114 resetSession(next.id);
115 if (draft !== null) { elements.configEditor.value = draft; disconnectedDraft = true; }
116 if (next.id) void inspect();
117 }
118 if (next.reason === "gone") setStatus(t("Relace skončila. Vyberte server."), true);
119 } finally { sessionsLoading = false; }
122async function inspect() {
123 const requestGeneration = generation;
124 if (!elements.sessionSelect.value) { inspection = null; renderIdentity(); renderConfigList(); syncControls(); return; }
125 setBusy(true); setStatus(t("Zjišťuji instalaci, službu a konfigurační soubory NGINX…"));
126 try {
127 inspection = await sessionResult(api.nginx.inspect(elements.sessionSelect.value), requestGeneration);
128 renderIdentity(); renderConfigList();
129 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);
130 } catch (error) { if (requestGeneration !== generation) return; inspection = null; renderIdentity(); renderConfigList(); setStatus(error?.message || String(error), true); }
131 finally { if (requestGeneration === generation) { setBusy(false); if (inspection?.installed) void loadSites(true); } }
134// Where to edit a symlink follows from its real target, which may be outside the managed directories.
135function symlinkHint(path) {
136 const file = (inspection?.configs || []).find((item) => item.path === path);
137 if (file?.editablePath) return t("upravte cíl symlinku: {path}", { path: file.editablePath });
138 if (file?.target) return t("cíl {path} je mimo spravované adresáře, pouze čtení", { path: file.target });
139 return t("pouze čtení");
142async function readConfig(path, kind) {
143 const requestGeneration = generation;
144 if (isDirty() && !window.confirm(t("Zahodit neuložené změny aktuální konfigurace?"))) return;
145 if (kind === "external") {
146 // Loaded by NGINX from outside its directory: the text comes from nginx -T and cannot be saved here.
147 selectedPath = path; selectedKind = kind; originalText = externalConfigText(path) ?? ""; originalSha256 = "";
148 elements.configEditor.value = originalText; elements.linkTargetButton.hidden = true;
149 elements.editorMode.textContent = t("EFEKTIVNÍ KONFIGURACE"); elements.editorTitle.textContent = path;
150 editorMetaText = () => t("Kombinovaný výstup je pouze pro čtení."); elements.editorMeta.textContent = editorMetaText();
151 renderConfigList(); syncControls(); return;
152 }
153 setBusy(true); setStatus(t("Načítám {path}…", { path }));
154 try {
155 const result = await sessionResult(api.nginx.readConfig(elements.sessionSelect.value, { path, sudoPassword: sudoPassword(), rememberSudo: rememberSudo() }), requestGeneration);
156 editorSessionId = sessionId; selectedPath = result.path; selectedKind = kind; originalText = result.text; originalSha256 = result.sha256 || "";
157 elements.configEditor.value = result.text;
158 elements.editorMode.textContent = kind === "symlink" ? t("NÁHLED SYMLINKU") : t("EDITOR");
159 elements.editorTitle.textContent = result.path;
160 editorMetaText = () => `${result.size.toLocaleString(i18n.locale())} B · ${kind === "symlink" ? symlinkHint(result.path) : t("změny se ukládají se zálohou")}`;
161 elements.editorMeta.textContent = editorMetaText();
162 elements.linkTargetButton.hidden = !(kind === "symlink" && (inspection?.configs || []).find((item) => item.path === result.path)?.editablePath);
163 renderConfigList(); setStatus(t("Konfigurace byla načtena."));
164 } catch (error) { if (requestGeneration !== generation) return; setStatus(error?.message || String(error), true); }
165 finally { if (requestGeneration === generation) { elements.sudoPassword.value = ""; setBusy(false);} }
168async function runTest() {
169 const requestGeneration = generation;
170 setBusy(true); setStatus(t("Spouštím nginx -t…"));
171 try {
172 const result = await sessionResult(api.nginx.testConfig(elements.sessionSelect.value, sudoPassword(), rememberSudo()), requestGeneration);
173 setOutput(result.output, !result.ok); setStatus(result.ok ? t("Konfigurace NGINX je platná.") : t("Test konfigurace selhal."), !result.ok);
174 } catch (error) { if (requestGeneration !== generation) return; setOutput(error?.message || String(error), true); setStatus(error?.message || String(error), true); }
175 finally { if (requestGeneration === generation) { elements.sudoPassword.value = ""; setBusy(false);} }
178async function dumpEffectiveConfig() {
179 const requestGeneration = generation;
180 if (isDirty() && !window.confirm(t("Zahodit neuložené změny a zobrazit efektivní konfiguraci?"))) return;
181 setBusy(true); setStatus(t("Načítám efektivní konfiguraci pomocí nginx -T…"));
182 try {
183 const result = await sessionResult(api.nginx.dumpConfig(elements.sessionSelect.value, sudoPassword(), rememberSudo()), requestGeneration);
184 selectedPath = ""; selectedKind = ""; originalText = result.text; originalSha256 = ""; elements.linkTargetButton.hidden = true; elements.configEditor.value = result.text;
185 elements.editorMode.textContent = t("EFEKTIVNÍ KONFIGURACE"); elements.editorTitle.textContent = "nginx -T";
186 editorMetaText = () => t("Kombinovaný výstup je pouze pro čtení."); elements.editorMeta.textContent = editorMetaText(); renderConfigList(); setStatus(t("Efektivní konfigurace byla načtena."));
187 } catch (error) { if (requestGeneration !== generation) return; setOutput(error?.message || String(error), true); setStatus(error?.message || String(error), true); }
188 finally { if (requestGeneration === generation) { elements.sudoPassword.value = ""; setBusy(false);} }
191async function saveConfig() {
192 const requestGeneration = generation;
193 if (!selectedWritable() || !isDirty()) return;
194 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;
195 setBusy(true); setStatus(t("Ukládám zálohu a ověřuji novou konfiguraci…"));
196 try {
197 const result = await sessionResult(api.nginx.saveConfig(editorSessionId, { path: selectedPath, text: elements.configEditor.value, expectedSha256: originalSha256, sudoPassword: sudoPassword(), rememberSudo: rememberSudo() }), requestGeneration);
198 originalText = elements.configEditor.value; originalSha256 = result.sha256 || ""; 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."));
199 } catch (error) { if (requestGeneration !== generation) return; setOutput(error?.message || String(error), true); setStatus(error?.message || String(error), true); }
200 finally { if (requestGeneration === generation) { elements.sudoPassword.value = ""; setBusy(false);} }
203async function reloadNginx() {
204 const requestGeneration = generation;
205 if (isDirty()) { setStatus(t("Nejprve uložte nebo zahoďte neuložené změny."), true); return; }
206 if (!window.confirm(`${api.sessions.displayName(selectedSession())}\n\n${t("Provést graceful reload NGINX? Core před reloadem znovu spustí nginx -t.")}`)) return;
207 setBusy(true); setStatus(t("Ověřuji konfiguraci a provádím graceful reload…"));
208 try {
209 const result = await sessionResult(api.nginx.reload(elements.sessionSelect.value, sudoPassword(), true, rememberSudo()), requestGeneration);
210 setOutput(result.output); await sessionResult(inspect(), requestGeneration);
211 if (result.missingPorts?.length) setStatus(t("Reload proběhl, ale NGINX nenaslouchá na portech {ports}. Nové nastavení se nepoužilo; příčinou bývá SELinux nebo obsazený port. Podívejte se do error logu.", { ports: result.missingPorts.join(", ") }), true);
212 else setStatus(t("NGINX byl bezpečně reloadován."));
213 } catch (error) { if (requestGeneration !== generation) return; setOutput(error?.message || String(error), true); setStatus(error?.message || String(error), true); }
214 finally { if (requestGeneration === generation) { elements.sudoPassword.value = ""; setBusy(false);} }
217elements.closeButton.addEventListener("click", () => api.close());
218elements.refreshButton.addEventListener("click", async () => { await loadSessions(); await inspect(); });
219elements.sessionSelect.addEventListener("change", async () => {
220 if ((isDirty() || (!sessionId && elements.configEditor.value)) && !window.confirm(t("Zahodit neuložené změny aktuální konfigurace?"))) { elements.sessionSelect.value = sessionId; return; }
221 endedSessionId = ""; resetSession(elements.sessionSelect.value); await inspect();
222});
223elements.configFilter.addEventListener("input", renderConfigList);
224elements.configList.addEventListener("click", (event) => { const item = event.target.closest("button[data-path]"); if (item) void readConfig(item.dataset.path, item.dataset.kind); });
225elements.configEditor.addEventListener("input", syncControls);
226elements.revertButton.addEventListener("click", () => { if (selectedPath) void readConfig(selectedPath, selectedKind); });
227elements.saveButton.addEventListener("click", saveConfig);
228elements.testButton.addEventListener("click", runTest);
229elements.dumpButton.addEventListener("click", dumpEffectiveConfig);
230elements.reloadButton.addEventListener("click", reloadNginx);
231elements.clearOutputButton.addEventListener("click", () => setOutput(""));
232elements.linkTargetButton.addEventListener("click", () => { const target = (inspection?.configs || []).find((item) => item.path === selectedPath)?.editablePath; if (target) void readConfig(target, "file"); });
233elements.historyButton.addEventListener("click", () => void openHistory(selectedPath));
234initSites();
235window.addEventListener("beforeunload", (event) => { if (isDirty()) { event.preventDefault(); event.returnValue = ""; } });
237(async () => {
238 try { i18n.setLanguage(await api.ui.getLanguage()); } catch {}
239 i18n.apply(document);
240 api.ui.onLanguageChanged((language) => { i18n.setLanguage(language); i18n.apply(document); renderIdentity(); renderConfigList(); renderSites(); if (editorMetaText) { elements.editorMeta.textContent = editorMetaText(); elements.editorMode.textContent = selectedKind === "symlink" ? t("NÁHLED SYMLINKU") : t("EDITOR"); } });
241 const info = await api.getInfo(); elements.pluginVersion.textContent = `${info.name} v${info.version} · ${info.publisher}`;
242 await loadSessions(); await inspect();
243})().catch((error) => setStatus(error?.message || String(error), true));
245setInterval(() => { if (!document.hidden) void loadSessions().catch(() => {}); }, 3000);
SHA-256d7a0b77ab29c0c602fc093ef222954dddf4c9bf919caa2b38a29d3abd6927ccf

CH-J Proprietary Software License 1.14 · Bron wordt geleverd onder de CH-J Proprietary Software License 1.14. De beschikbaarheid verandert de licentievoorwaarden niet of verleent geen extra rechten.

Archief SHA-256ac8fe790f4ba60fc098f8c985fc2656cfcc6cdc21fb34b30e308a2b54e62258f
Bestanden