CH-J Toolsquản lý máy chủ qua SSH
Menu
Mã nguồn đã công bố

CH-J Tools

Duyệt thư mục và tệp của một bản phát hành ứng dụng cụ thể.

Tải mã nguồn dạng ZIP
CH-J / Server Manager 1704 tệp
0.1.2-beta Ngày thêm: · Ngày dựng:
write.js Ngày thêm: Chưa ghi nhận ngày 36,4 KB · 585 dòng
Mã Tải tệp xuống
1"use strict";
3// Everything that changes the server. Each change is shown first, needs a confirmation, keeps a backup and passes the
4// configuration test, or the previous state comes back. A reload is always a step of its own.
5const writeState = { plan: null, pendingEdit: null, file: null, lastChange: [], seenDiffs: new Set(), missingModules: [] };
6const WIZARD_FIELDS = {
7 static: ["wizardRootField", "wizardCreateRootField"],
8 php: ["wizardRootField", "wizardCreateRootField", "wizardPhpField"],
9 wordpress: ["wizardRootField", "wizardCreateRootField", "wizardPhpField"],
10 proxy: ["wizardBackendField"],
11 websocket: ["wizardBackendField"],
12 redirect: ["wizardRedirectTargetField", "wizardRedirectCodeField"],
13 custom: ["wizardTemplateField", "wizardRootField", "wizardCreateRootField", "wizardBackendField", "wizardPhpField"]
14};
15const TEXT_FIXES = new Set(["open-proxy", "directory-listing", "no-listen"]);
17async function writeOperation(startMessage, work) {
18 const requestGeneration = generation;
19 setBusy(true); setStatus(startMessage);
20 try { return await work(requestGeneration); }
21 catch (error) {
22 if (requestGeneration !== generation || error?.message === "CHJ_SESSION_CHANGED") return undefined;
23 elements.commandOutput.textContent = errorText(error); elements.commandOutput.classList.add("error");
24 setStatus(errorText(error), true);
25 return undefined;
26 } finally { if (requestGeneration === generation) { elements.sudoPassword.value = ""; setBusy(false); } }
29function confirmOn(question) { return window.confirm(`${api.sessions.displayName(selectedSession())}\n\n${question}`); }
30function showOutput(text) { elements.commandOutput.textContent = text || "—"; elements.commandOutput.classList.remove("error"); }
31function readStored(key, fallback) { try { return JSON.parse(localStorage.getItem(key)) ?? fallback; } catch { return fallback; } }
32function writeStored(key, value) { try { localStorage.setItem(key, JSON.stringify(value)); return true; } catch { return false; } }
34// --- diff -----------------------------------------------------------------------------------------
36function lineDiff(before, after) {
37 const a = before.split("\n"), b = after.split("\n");
38 let start = 0;
39 while (start < a.length && start < b.length && a[start] === b[start]) start += 1;
40 let endA = a.length, endB = b.length;
41 while (endA > start && endB > start && a[endA - 1] === b[endB - 1]) { endA -= 1; endB -= 1; }
42 const lines = [];
43 for (let at = Math.max(0, start - 3); at < start; at += 1) lines.push(["", a[at]]);
44 const middleA = a.slice(start, endA), middleB = b.slice(start, endB);
45 if (middleA.length * middleB.length > 4_000_000) {
46 middleA.forEach((line) => lines.push(["del", line]));
47 middleB.forEach((line) => lines.push(["add", line]));
48 } else {
49 const table = Array.from({ length: middleA.length + 1 }, () => new Uint32Array(middleB.length + 1));
50 for (let i = middleA.length - 1; i >= 0; i -= 1) for (let j = middleB.length - 1; j >= 0; j -= 1) table[i][j] = middleA[i] === middleB[j] ? table[i + 1][j + 1] + 1 : Math.max(table[i + 1][j], table[i][j + 1]);
51 let i = 0, j = 0;
52 while (i < middleA.length || j < middleB.length) {
53 if (i < middleA.length && j < middleB.length && middleA[i] === middleB[j]) { lines.push(["", middleA[i]]); i += 1; j += 1; }
54 else if (i < middleA.length && (j === middleB.length || table[i + 1][j] >= table[i][j + 1])) { lines.push(["del", middleA[i]]); i += 1; }
55 else { lines.push(["add", middleB[j]]); j += 1; }
56 }
57 }
58 for (let at = endA; at < Math.min(a.length, endA + 3); at += 1) lines.push(["", a[at]]);
59 return lines;
62function renderDiff(target, before, after) {
63 const lines = lineDiff(before, after);
64 if (!lines.some(([kind]) => kind)) { target.textContent = t("Bez rozdílů."); return; }
65 target.replaceChildren(...lines.map(([kind, line]) => node("span", kind, `${kind === "add" ? "+" : kind === "del" ? "-" : " "} ${line}`)));
68function rememberChange(items) { writeState.lastChange = items; byId("undoButton").hidden = !items.length; }
70async function refreshAfterChange(requestGeneration, message) {
71 await sessionResult(loadInventory(true), requestGeneration);
72 setStatus(message);
75// --- wizard ---------------------------------------------------------------------------------------
77function syncWizard() {
78 const type = byId("wizardType").value;
79 for (const id of new Set(Object.values(WIZARD_FIELDS).flat())) byId(id).hidden = !WIZARD_FIELDS[type].includes(id);
80 byId("wizardTlsField").hidden = type === "custom";
81 byId("wizardHeadersField").hidden = type === "custom";
82 const tls = type === "custom" ? "none" : byId("wizardTls").value;
83 for (const id of ["wizardCertificateField", "wizardKeyField", "wizardRedirectHttpField"]) byId(id).hidden = tls !== "existing";
84 const root = byId("wizardRoot");
85 const domain = byId("wizardDomain").value.trim().toLowerCase().replace(/^\*\./, "");
86 if (!root.dataset.edited) root.value = domain ? `/var/www/${domain}` : "";
87 const sockets = inventory?.phpSockets || [];
88 byId("wizardPhpList").replaceChildren(...sockets.map((socket) => { const option = node("option"); option.value = socket; return option; }));
89 if (sockets.length && !byId("wizardPhp").value) byId("wizardPhp").value = sockets[0];
90 invalidatePlan();
93function invalidatePlan() {
94 writeState.plan = null; writeState.missingModules = [];
95 byId("wizardDeploy").disabled = true;
96 byId("wizardResult").hidden = true;
99function wizardRequest() {
100 const custom = byId("wizardType").value === "custom";
101 const https = !custom && byId("wizardTls").value === "existing";
102 return {
103 type: byId("wizardType").value, serverNames: [byId("wizardDomain").value.trim(), ...byId("wizardAliases").value.split(/[\s,]+/)].filter(Boolean), port: Number(byId("wizardPort").value) || 80,
104 root: byId("wizardRoot").value.trim(), createRoot: byId("wizardCreateRoot").checked, phpBackend: byId("wizardPhp").value.trim(), backend: byId("wizardBackend").value.trim(),
105 redirectTarget: byId("wizardRedirectTarget").value.trim(), redirectCode: Number(byId("wizardRedirectCode").value),
106 https, certificate: byId("wizardCertificate").value.trim(), certificateKey: byId("wizardKey").value.trim(), redirectHttp: https && byId("wizardRedirectHttp").checked,
107 securityHeaders: byId("wizardHeaders").checked, template: custom ? byId("wizardTemplateText").value : ""
108 };
111async function previewSite() {
112 await writeOperation(t("Sestavuji konfiguraci…"), async (requestGeneration) => {
113 const site = wizardRequest();
114 const plan = await sessionResult(api.apache.planSite(elements.sessionSelect.value, { site, ...auth() }), requestGeneration);
115 writeState.plan = site; writeState.missingModules = plan.missingModules || [];
116 byId("wizardPath").textContent = `${plan.path} (${plan.link ? t("vznikne vypnutý, zapnutí vytvoří {link}", { link: plan.link }) : t("vznikne vypnutý")})`;
117 byId("wizardText").textContent = plan.text;
118 const notes = plan.warnings.map((item) => t("Na portu {port} Apache nenaslouchá; web bude po zapnutí nedostupný, dokud nepřidáte Listen {port}.", { port: item.port }));
119 if (writeState.missingModules.length) notes.push(t("Web potřebuje moduly, které nejsou načtené: {modules}.", { modules: writeState.missingModules.join(", ") }));
120 byId("wizardWarnings").textContent = notes.join(" ");
121 byId("wizardWarnings").hidden = !notes.length;
122 byId("wizardEnableModules").hidden = !(writeState.missingModules.length && inventory?.canManageModules);
123 byId("wizardResult").hidden = false;
124 byId("wizardDeploy").disabled = writeState.missingModules.length > 0;
125 byId("wizardDeploy").scrollIntoView({ block: "start" });
126 setStatus(t("Náhled je připravený, na serveru se zatím nic nezměnilo."));
127 });
130async function enableMissingModules() {
131 const modules = [...writeState.missingModules];
132 if (!modules.length || !confirmOn(t("Zapnout moduly {modules} a spustit test konfigurace? Projeví se po plynulém načtení.", { modules: modules.join(", ") }))) return;
133 await writeOperation(t("Zapínám moduly…"), async (requestGeneration) => {
134 for (const name of modules) await sessionResult(api.apache.setModule(elements.sessionSelect.value, { name, enabled: true, confirm: true, ...auth() }), requestGeneration);
135 await sessionResult(api.apache.reload(elements.sessionSelect.value, { confirm: true, ...auth() }), requestGeneration);
136 await refreshAfterChange(requestGeneration, t("Moduly jsou zapnuté a načtené. Můžete znovu zobrazit generovanou konfiguraci."));
137 invalidatePlan();
138 });
141async function deploySite() {
142 const site = writeState.plan;
143 if (!site || !confirmOn(t("Vytvořit vypnutý web {name} a spustit test konfigurace? Při neúspěšném testu se nový soubor zase odstraní. Existující konfigurace se nepřepisuje.", { name: site.serverNames[0] }))) return;
144 await writeOperation(t("Vytvářím web a ověřuji konfiguraci…"), async (requestGeneration) => {
145 const result = await sessionResult(api.apache.createSite(elements.sessionSelect.value, { site, confirm: true, ...auth() }), requestGeneration);
146 showOutput(`${result.testOutput}\n\n${t("Soubor: {path}", { path: result.path })}`);
147 byId("wizard").hidden = true;
148 invalidatePlan();
149 await sessionResult(loadInventory(true), requestGeneration);
150 const created = inventory?.sites.find((item) => item.domain === site.serverNames[0].toLowerCase() && item.role !== "companion");
151 if (created) openDetail(created);
152 setStatus(t("Web byl vytvořen vypnutý a test konfigurace uspěl. Zapnete ho tlačítkem Aktivovat, projeví se po plynulém načtení konfigurace."));
153 });
156const TEMPLATES_KEY = "chj.apache.templates";
157function storedTemplates() { const value = readStored(TEMPLATES_KEY, {}); return value && typeof value === "object" ? value : {}; }
158function renderTemplates() {
159 const select = byId("wizardTemplateSelect");
160 const previous = select.value;
161 const names = Object.keys(storedTemplates()).sort();
162 const none = node("option", "", t("Nová předloha")); none.value = "";
163 select.replaceChildren(none, ...names.map((name) => { const option = node("option", "", name); option.value = name; return option; }));
164 if (names.includes(previous)) select.value = previous;
166function loadTemplate() {
167 const name = byId("wizardTemplateSelect").value;
168 byId("wizardTemplateName").value = name;
169 byId("wizardTemplateText").value = storedTemplates()[name] || "";
170 invalidatePlan();
172function saveTemplate() {
173 const name = byId("wizardTemplateName").value.trim();
174 const text = byId("wizardTemplateText").value;
175 if (!name || !text.trim()) { setStatus(t("Zadejte název a text předlohy."), true); return; }
176 if (!writeStored(TEMPLATES_KEY, { ...storedTemplates(), [name]: text })) { setStatus(t("Předlohu se nepodařilo uložit."), true); return; }
177 renderTemplates(); byId("wizardTemplateSelect").value = name;
178 setStatus(t("Předloha {name} byla uložena.", { name }));
180function deleteTemplate() {
181 const name = byId("wizardTemplateSelect").value;
182 if (!name || !window.confirm(t("Smazat předlohu {name}?", { name }))) return;
183 const remaining = storedTemplates(); delete remaining[name];
184 writeStored(TEMPLATES_KEY, remaining);
185 byId("wizardTemplateSelect").value = ""; renderTemplates(); loadTemplate();
188// --- site detail ----------------------------------------------------------------------------------
190function fillDetailForm(site) {
191 byId("detailName").value = site.domain || "";
192 byId("detailAliases").value = (site.aliases || []).join(" ");
193 byId("detailRoot").value = site.rootInherited ? "" : site.documentRoot || "";
194 byId("detailBackend").value = (site.proxies || []).find((rule) => rule.path === "/" && !rule.excluded)?.target || "";
195 byId("detailRedirect").value = site.redirectTarget || "";
196 byId("detailCertificate").value = ""; byId("detailKey").value = "";
197 byId("diffPanel").hidden = true;
200function renderWrite() {
201 const site = selected;
202 byId("detailEdit").hidden = !site;
203 const toggle = byId("detailToggle");
204 if (site) {
205 toggle.textContent = site.enabled ? t("Deaktivovat") : t("Aktivovat");
206 toggle.hidden = !site.canToggle;
207 const note = byId("detailEditNote");
208 note.textContent = site.editable ? "" : site.fromMacro ? t("Web vzniká z makra, upravte ho v konfiguračním editoru.") : t("Web je uvnitř podmínky nebo mimo upravitelné soubory, upravte ho v konfiguračním editoru.");
209 note.hidden = site.editable;
210 for (const id of ["detailName", "detailAliases", "detailRoot", "detailBackend", "detailRedirect", "detailCertificate", "detailKey"]) byId(id).disabled = !site.editable;
211 byId("detailPlan").disabled = !site.editable;
212 byId("detailBackendField").hidden = !(site.proxies || []).some((rule) => rule.path === "/" && !rule.excluded);
213 byId("detailRedirectField").hidden = site.type !== "redirect" || !/^https?:\/\/[^%$]+$/.test(site.redirectTarget || "");
214 byId("detailRootField").hidden = site.type === "redirect" || site.type === "proxy";
215 const hasTls = (inventory?.sites || []).some((other) => other.sslEnabled && other.enabled === site.enabled && other.domain === site.domain);
216 byId("detailHttps").hidden = !site.editable || site.sslEnabled || hasTls || !site.domain;
217 }
218 renderProposals(); renderAcmeSites();
221function detailChanges() {
222 const site = selected, changes = {};
223 const value = (id) => byId(id).value.trim();
224 if (value("detailName") !== (site.domain || "")) changes.serverName = value("detailName");
225 if (value("detailAliases") !== (site.aliases || []).join(" ")) changes.aliases = value("detailAliases");
226 if (!byId("detailRootField").hidden && value("detailRoot") && value("detailRoot") !== (site.rootInherited ? "" : site.documentRoot || "")) changes.documentRoot = value("detailRoot");
227 const backend = (site.proxies || []).find((rule) => rule.path === "/" && !rule.excluded)?.target || "";
228 if (!byId("detailBackendField").hidden && value("detailBackend") !== backend) changes.backend = value("detailBackend");
229 if (!byId("detailRedirectField").hidden && value("detailRedirect") !== (site.redirectTarget || "")) changes.redirect = value("detailRedirect");
230 if (!byId("detailHttps").hidden && value("detailCertificate")) changes.https = { certificate: value("detailCertificate"), certificateKey: value("detailKey"), redirectHttp: byId("detailRedirectHttp").checked };
231 return changes;
234function showPlan(plan) {
235 writeState.pendingEdit = plan;
236 byId("diffTitle").textContent = plan.path;
237 renderDiff(byId("diffOutput"), plan.before, plan.after);
238 byId("diffPanel").hidden = false;
239 switchTab("sites");
240 byId("diffPanel").scrollIntoView({ block: "nearest" });
241 setStatus(t("Náhled je připravený, na serveru se zatím nic nezměnilo."));
244async function planDetail() {
245 const site = selected;
246 const changes = detailChanges();
247 if (!Object.keys(changes).length) { setStatus(t("Nebyla provedena žádná změna.")); return; }
248 await writeOperation(t("Připravuji náhled změn…"), async (requestGeneration) => {
249 showPlan(await sessionResult(api.apache.planEdit(elements.sessionSelect.value, { path: site.editPath, line: site.line, changes, ...auth() }), requestGeneration));
250 });
253async function applyEdit() {
254 const plan = writeState.pendingEdit;
255 if (!plan || !confirmOn(t("Uložit {path}, vytvořit zálohu a spustit test konfigurace? Neplatná konfigurace se automaticky vrátí.", { path: plan.path }))) return;
256 await writeOperation(t("Ukládám zálohu a ověřuji novou konfiguraci…"), async (requestGeneration) => {
257 const result = await sessionResult(api.apache.saveFile(elements.sessionSelect.value, { path: plan.path, text: plan.after, expectedSha256: plan.sha256, confirm: true, ...auth() }), requestGeneration);
258 writeState.pendingEdit = null; byId("diffPanel").hidden = true;
259 rememberChange([{ path: plan.path, backup: result.backupPath, sha256: result.sha256 }]);
260 showOutput(`${result.testOutput}\n\n${t("Záloha: {path}", { path: result.backupPath })}`);
261 await refreshAfterChange(requestGeneration, t("Konfigurace byla uložena a test uspěl. Projeví se po plynulém načtení konfigurace."));
262 });
265async function toggleSite() {
266 const site = selected;
267 if (!site) return;
268 const enabled = !site.enabled;
269 const name = site.domain || site.file;
270 if (!confirmOn(enabled ? t("Aktivovat web {name} a spustit test konfigurace?", { name }) : t("Deaktivovat web {name} a spustit test konfigurace? Soubor webu zůstane na serveru.", { name }))) return;
271 await writeOperation(t("Měním stav webu a ověřuji konfiguraci…"), async (requestGeneration) => {
272 const result = await sessionResult(api.apache.setSiteEnabled(elements.sessionSelect.value, { path: site.linkPath ? site.editPath : site.file, linkPath: site.linkPath || "", enabled, confirm: true, ...auth() }), requestGeneration);
273 showOutput(result.testOutput);
274 selected = null;
275 await refreshAfterChange(requestGeneration, t("Stav webu byl změněn a test konfigurace uspěl. Projeví se po plynulém načtení konfigurace."));
276 });
279async function reloadApache() {
280 if (!confirmOn(t("Provést plynulé načtení konfigurace Apache? Před ním se znovu spustí test konfigurace."))) return;
281 await writeOperation(t("Ověřuji konfiguraci a provádím plynulé načtení…"), async (requestGeneration) => {
282 const result = await sessionResult(api.apache.reload(elements.sessionSelect.value, { confirm: true, ...auth() }), requestGeneration);
283 showOutput(result.output);
284 await sessionResult(loadInventory(true), requestGeneration);
285 if (result.missingPorts?.length) setStatus(t("Načtení proběhlo, ale Apache nenaslouchá na portech {ports}. Chybí Listen, nebo port obsadilo něco jiného.", { ports: result.missingPorts.join(", ") }), true);
286 else setStatus(t("Apache byl plynule načten."));
287 });
290async function undoLastChange() {
291 const items = writeState.lastChange;
292 if (!items.length || !confirmOn(t("Vrátit {files} do stavu před poslední změnou a spustit test konfigurace?", { files: items.map((item) => item.path).join(", ") }))) return;
293 await writeOperation(t("Obnovuji zálohu a ověřuji konfiguraci…"), async (requestGeneration) => {
294 for (const item of items) await sessionResult(api.apache.restoreBackup(elements.sessionSelect.value, { path: item.path, backup: item.backup, expectedSha256: item.sha256, confirm: true, ...auth() }), requestGeneration);
295 rememberChange([]);
296 await refreshAfterChange(requestGeneration, t("Změna byla vrácena a test konfigurace uspěl. Projeví se po plynulém načtení konfigurace."));
297 });
300// --- fixes ----------------------------------------------------------------------------------------
302function addFindingFix(element, item) {
303 const holder = element.querySelector(".badges");
304 const button = (label, handler) => { const control = node("button", "mini", label); control.type = "button"; control.addEventListener("click", (event) => { event.stopPropagation(); handler(); }); holder.append(control); };
305 if (TEXT_FIXES.has(item.code) && item.file) button(t("Navrhnout opravu"), () => void planFix(item));
306 if (item.code === "no-listen") {
307 holder.lastChild?.remove();
308 const anchor = (inventory?.server?.listen || []).at(-1);
309 if (anchor) button(t("Navrhnout opravu"), () => void planFix({ ...item, file: anchor.file, line: anchor.line }));
310 }
313async function planFix(item) {
314 await writeOperation(t("Připravuji náhled změn…"), async (requestGeneration) => {
315 const link = (inventory?.files || []).find((file) => file.path === item.file)?.linkTarget;
316 showPlan(await sessionResult(api.apache.planFix(elements.sessionSelect.value, { path: link || item.file, code: item.code, line: item.line, params: item.params, ...auth() }), requestGeneration));
317 });
320function proposalText(item) {
321 if (item.kind === "broken-link") return t("Symlink vede na neexistující soubor {target}. Oprava ho odstraní.", { target: item.target });
322 const older = !item.older ? "" : item.identical ? ` ${t("Stejná kopie v sites-available se použije.")}` : ` ${t("V sites-available je jiná verze. Nejdřív si prohlédněte rozdíl: převod by ji nahradil načteným souborem a starou uložil do zálohy.")}`;
323 return `${t("Skutečný soubor v sites-enabled funguje a lze ho vypínat i takto. Sjednocení by ho přesunulo do {path} a vytvořilo symlink.", { path: item.available })}${older}`;
325function proposalDiffers(item) { return Boolean(item.older && !item.identical); }
326function keptKey() { return `chj.apache.kept.${selectedSession()?.host || ""}`; }
327function visibleProposals() { const kept = readStored(keptKey(), []); return (inventory?.proposals || []).filter((item) => !kept.includes(item.path)); }
329function renderProposals() {
330 const items = visibleProposals();
331 byId("proposalsBlock").hidden = !items.length;
332 byId("proposalsList").replaceChildren(...items.map((item) => {
333 const element = row(item.path, proposalText(item));
334 const holder = element.querySelector(".badges");
335 const add = (label, handler, disabled = false, title = "") => { const control = node("button", "mini", label); control.type = "button"; control.disabled = disabled; control.title = title; control.addEventListener("click", handler); holder.append(control); };
336 if (proposalDiffers(item)) add(t("Rozdíl"), () => void showProposalDiff(item));
337 const locked = proposalDiffers(item) && !writeState.seenDiffs.has(item.path);
338 add(item.kind === "broken-link" ? t("Opravit") : t("Sjednotit"), () => void fixProposal(item), locked, locked ? t("Nejdřív si prohlédněte rozdíl.") : "");
339 if (item.kind !== "broken-link") add(t("Ponechat takto"), () => { writeStored(keptKey(), [...readStored(keptKey(), []), item.path]); renderProposals(); });
340 return element;
341 }));
344async function showProposalDiff(item) {
345 await writeOperation(t("Načítám oba soubory…"), async (requestGeneration) => {
346 const session = elements.sessionSelect.value;
347 const older = await sessionResult(api.apache.readFile(session, { path: item.older, ...auth() }), requestGeneration);
348 const loaded = await sessionResult(api.apache.readFile(session, { path: item.path, ...auth() }), requestGeneration);
349 renderDiff(byId("proposalDiff"), older.text, loaded.text);
350 byId("proposalDiff").hidden = false;
351 writeState.seenDiffs.add(item.path);
352 renderProposals();
353 setStatus(t("Rozdíl: {older} (−) a načtený {loaded} (+).", { older: item.older, loaded: item.path }));
354 });
357async function fixProposal(item) {
358 const question = item.kind === "broken-link" ? t("Odstranit symlink {path}? Jeho cíl {target} neexistuje, takže Apache tento web nenačítá.", { path: item.path, target: item.target }) : `${t("Přesunout {path} do sites-available a nahradit ho symlinkem? Apache načte stejnou konfiguraci jako dosud, oba soubory se zálohují.", { path: item.path })}${proposalDiffers(item) ? ` ${t("{path} je jiná verze. Nahradí ji načtený soubor, stará verze zůstane v záloze.", { path: item.older })}` : ""}`;
359 if (!confirmOn(question)) return;
360 await writeOperation(t("Opravuji a ověřuji konfiguraci…"), async (requestGeneration) => {
361 const session = elements.sessionSelect.value;
362 const result = await sessionResult(item.kind === "broken-link" ? api.apache.dropBrokenLink(session, { path: item.path, confirm: true, ...auth() }) : api.apache.adoptSite(session, { path: item.path, confirm: true, ...auth() }), requestGeneration);
363 showOutput(result.backupPath ? `${result.testOutput}\n\n${t("Záloha: {path}", { path: result.backupPath })}` : result.testOutput);
364 byId("proposalDiff").hidden = true;
365 await refreshAfterChange(requestGeneration, t("Změna proběhla a test konfigurace uspěl."));
366 });
369// --- modules and certificates ---------------------------------------------------------------------
371function moduleAction(name, kind, loaded) {
372 if (!inventory?.canManageModules || kind === "static") return [];
373 const control = node("button", "mini", loaded ? t("Vypnout") : t("Zapnout"));
374 control.type = "button";
375 control.addEventListener("click", () => void setModule(name, !loaded));
376 return [control];
379async function setModule(name, enabled) {
380 if (!confirmOn(enabled ? t("Zapnout modul {name} a spustit test konfigurace?", { name }) : t("Vypnout modul {name} a spustit test konfigurace? Modul, který potřebuje některý aktivní web, aplikace nevypne.", { name }))) return;
381 await writeOperation(t("Měním modul a ověřuji konfiguraci…"), async (requestGeneration) => {
382 const result = await sessionResult(api.apache.setModule(elements.sessionSelect.value, { name, enabled, confirm: true, ...auth() }), requestGeneration);
383 showOutput(result.testOutput);
384 setStatus(t("Modul byl změněn a test konfigurace uspěl. Projeví se po plynulém načtení konfigurace."));
385 });
388function acmeCandidates() { return (inventory?.sites || []).filter((site) => site.role === "site" && site.domain && site.documentRoot && !site.rootInherited && !site.domain.startsWith("*")); }
390function renderAcmeSites() {
391 const select = byId("acmeSite");
392 const previous = select.value;
393 select.replaceChildren(...acmeCandidates().map((site) => { const option = node("option", "", site.domain); option.value = `${site.file}:${site.line}`; return option; }));
394 if ([...select.options].some((option) => option.value === previous)) select.value = previous;
397async function issueCertificate() {
398 const site = acmeCandidates().find((item) => `${item.file}:${item.line}` === byId("acmeSite").value);
399 if (!site) { setStatus(t("Vyberte web."), true); return; }
400 if (!site.enabled) { setStatus(t("Web je vypnutý. Před vystavením certifikátu ho zapněte a proveďte plynulé načtení konfigurace."), true); return; }
401 if (!byId("acmeAgree").checked) { setStatus(t("Nejprve potvrďte souhlas s podmínkami Let's Encrypt."), true); return; }
402 const domains = [site.domain, ...(site.aliases || [])];
403 if (!confirmOn(t("Požádat Let's Encrypt o certifikát pro {names}?", { names: domains.join(", ") }))) return;
404 const fields = auth();
405 if (await requestCertificate(site, domains, fields) !== "missing") return;
406 if (!confirmOn(`${t("Certbot na tomto serveru chybí.")} ${certbotQuestion(false)}`)) { setStatus(t("Certbot na tomto serveru není nainstalovaný."), true); return; }
407 if (await installCertbot(fields, false, true)) await requestCertificate(site, domains, fields);
410async function requestCertificate(site, domains, fields) {
411 let missing = false;
412 await writeOperation(t("Certbot žádá o certifikát, může to trvat několik minut…"), async (requestGeneration) => {
413 let result;
414 try {
415 result = await sessionResult(api.apache.issueCertificate(elements.sessionSelect.value, { email: byId("acmeEmail").value.trim(), root: site.documentRoot, domains, agreeTos: true, confirm: true, ...fields }), requestGeneration);
416 } catch (error) {
417 if (!CERTBOT_MISSING.test(errorText(error))) throw error;
418 missing = true;
419 return;
420 }
421 showOutput(result.output);
422 switchTab("sites"); openDetail(site);
423 byId("detailCertificate").value = result.certificate; byId("detailKey").value = result.certificateKey;
424 setStatus(t("Certifikát byl vystaven. Teď ho zapněte v detailu webu."));
425 });
426 return missing ? "missing" : "done";
429function certbotQuestion(allowEpel) {
430 return allowEpel
431 ? t("Certbot není v zapnutých repozitářích tohoto serveru. Přidat repozitář EPEL (balíček epel-release) a nainstalovat certbot?")
432 : t("Nainstalovat certbot přes balíčkový správce serveru? Stáhne jeho balíčky a závislosti z repozitářů serveru a zapne automatickou obnovu certifikátů, pokud ji balíček obsahuje.");
434const CERTBOT_MISSING = /certbot is not installed/i;
435const CERTBOT_NEEDS_EPEL = /needs the EPEL repository/i;
437async function installCertbot(fields = auth(), allowEpel = false, asked = false) {
438 if (!asked && !confirmOn(certbotQuestion(allowEpel))) return false;
439 let needsEpel = false;
440 let installed = false;
441 await writeOperation(t("Instaluji certbot, může to trvat několik minut…"), async (requestGeneration) => {
442 let result;
443 try {
444 result = await sessionResult(api.apache.installCertbot(elements.sessionSelect.value, { confirm: true, allowEpel, ...fields }), requestGeneration);
445 } catch (error) {
446 if (!CERTBOT_NEEDS_EPEL.test(errorText(error))) throw error;
447 needsEpel = true;
448 return;
449 }
450 elements.sudoPassword.value = fields.sudoPassword;
451 await sessionResult(loadInventory(true), requestGeneration);
452 showOutput(result.output);
453 setStatus(result.renewal === "enabled" ? t("Certbot byl nainstalován a automatická obnova je zapnutá.") : t("Certbot byl nainstalován, ale automatická obnova nebyla zjištěna. Obnovu certifikátů nastavte na serveru ručně."));
454 installed = true;
455 });
456 return needsEpel ? installCertbot(fields, true) : installed;
459// --- configuration editor -------------------------------------------------------------------------
461function isDirty() { return Boolean(writeState.file) && byId("fileText").value !== writeState.file.text; }
462window.__CHJ_HAS_UNSAVED_CHANGES__ = isDirty;
464function syncWriteControls() {
465 byId("newSiteButton").disabled = busy || !inventory;
466 byId("fileSave").disabled = busy || !writeState.file || !isDirty();
467 byId("fileRevert").disabled = busy || !writeState.file;
468 byId("fileHistory").disabled = busy || !writeState.file;
469 byId("fileText").readOnly = !writeState.file;
472/// Shows a file. It can be edited once its exact bytes and hash came from the server, not from the overview.
473async function openFile(path, line) {
474 if (isDirty() && !window.confirm(t("Zahodit neuložené změny aktuální konfigurace?"))) return;
475 switchTab("config");
476 const area = byId("fileText");
477 const known = (inventory?.files || []).find((item) => item.path === path);
478 byId("historyBlock").hidden = true;
479 writeState.file = null;
480 area.value = known ? known.text : t("Soubor nebyl načten: {path}", { path });
481 byId("fileMeta").textContent = path;
482 const requestGeneration = generation;
483 setBusy(true); setStatus(t("Načítám {path}…", { path }));
484 try {
485 const read = await sessionResult(api.apache.readFile(elements.sessionSelect.value, { path, ...auth() }), requestGeneration);
486 writeState.file = { path: read.path, text: read.text, sha256: read.sha256 };
487 area.value = read.text;
488 setStatus(line ? `${path}:${line}` : path);
489 } catch (error) {
490 if (requestGeneration !== generation || error?.message === "CHJ_SESSION_CHANGED") return;
491 setStatus(needsPassword(error) ? t("Soubor je jen pro čtení. Pro úpravy zadejte heslo pro sudo a otevřete ho znovu.") : errorText(error), !needsPassword(error));
492 } finally { if (requestGeneration === generation) setBusy(false); }
493 if (line) {
494 const before = area.value.split("\n").slice(0, line - 1).join("\n").length + (line > 1 ? 1 : 0);
495 area.focus(); area.setSelectionRange(before, before + (area.value.slice(before).indexOf("\n") + 1 || 0));
496 area.scrollTop = Math.max(0, (line - 4) * 15);
497 } else area.scrollTop = 0;
500async function saveFile() {
501 const file = writeState.file;
502 if (!file || !isDirty() || !confirmOn(t("Uložit {path}, vytvořit zálohu a spustit test konfigurace? Neplatná konfigurace se automaticky vrátí.", { path: file.path }))) return;
503 const text = byId("fileText").value;
504 await writeOperation(t("Ukládám zálohu a ověřuji novou konfiguraci…"), async (requestGeneration) => {
505 const result = await sessionResult(api.apache.saveFile(elements.sessionSelect.value, { path: file.path, text, expectedSha256: file.sha256, confirm: true, ...auth() }), requestGeneration);
506 writeState.file = { ...file, text, sha256: result.sha256 };
507 rememberChange([{ path: file.path, backup: result.backupPath, sha256: result.sha256 }]);
508 showOutput(`${result.testOutput}\n\n${t("Záloha: {path}", { path: result.backupPath })}`);
509 await refreshAfterChange(requestGeneration, t("Konfigurace byla uložena a test uspěl. Projeví se po plynulém načtení konfigurace."));
510 });
513async function openHistory() {
514 const file = writeState.file;
515 if (!file) return;
516 await writeOperation(t("Načítám zálohy…"), async (requestGeneration) => {
517 const result = await sessionResult(api.apache.backups(elements.sessionSelect.value, { path: file.path, ...auth() }), requestGeneration);
518 byId("historyDiff").hidden = true;
519 fill(byId("historyList"), result.backups.map((backup) => {
520 const element = row(new Date(backup.modified * 1000).toLocaleString(i18n.locale()), t("{size} B", { size: backup.size }));
521 const holder = element.querySelector(".badges");
522 const show = node("button", "mini", t("Rozdíl")); show.type = "button"; show.addEventListener("click", () => void showBackup(file, backup.path));
523 const restore = node("button", "mini", t("Obnovit")); restore.type = "button"; restore.addEventListener("click", () => void restoreBackup(file, backup.path));
524 holder.append(show, restore);
525 return element;
526 }), t("Tento soubor zatím nemá žádnou zálohu."));
527 byId("historyBlock").hidden = false;
528 setStatus(t("Nalezeno záloh: {count}.", { count: result.backups.length }));
529 });
532async function showBackup(file, backup) {
533 await writeOperation(t("Načítám zálohu…"), async (requestGeneration) => {
534 const result = await sessionResult(api.apache.readBackup(elements.sessionSelect.value, { path: file.path, backup, ...auth() }), requestGeneration);
535 renderDiff(byId("historyDiff"), result.text, file.text);
536 byId("historyDiff").hidden = false;
537 setStatus(t("Rozdíl mezi zálohou (−) a načteným souborem (+)."));
538 });
541async function restoreBackup(file, backup) {
542 if (isDirty()) { setStatus(t("Nejprve uložte nebo zahoďte neuložené změny."), true); return; }
543 if (!confirmOn(t("Obnovit {path} ze zálohy a spustit test konfigurace? Současný stav se také zálohuje.", { path: file.path }))) return;
544 await writeOperation(t("Obnovuji zálohu a ověřuji konfiguraci…"), async (requestGeneration) => {
545 const result = await sessionResult(api.apache.restoreBackup(elements.sessionSelect.value, { path: file.path, backup, expectedSha256: file.sha256, confirm: true, ...auth() }), requestGeneration);
546 showOutput(`${result.testOutput}\n\n${t("Záloha: {path}", { path: result.backupPath })}`);
547 writeState.file = null;
548 await sessionResult(loadInventory(true), requestGeneration);
549 setStatus(t("Záloha byla obnovena a test konfigurace uspěl. Projeví se po plynulém načtení konfigurace."));
550 byId("historyBlock").hidden = true;
551 await openFile(file.path);
552 });
555function resetWrite() {
556 Object.assign(writeState, { plan: null, pendingEdit: null, file: null, missingModules: [], seenDiffs: new Set() });
557 rememberChange([]);
558 for (const id of ["wizard", "diffPanel", "proposalDiff", "historyBlock"]) byId(id).hidden = true;
559 byId("fileText").value = ""; byId("fileMeta").textContent = "";
562function initWrite() {
563 byId("newSiteButton").addEventListener("click", () => { selected = null; renderDetail(); renderWrite(); byId("wizard").hidden = false; syncWizard(); byId("wizard").scrollIntoView({ block: "start" }); byId("wizardDomain").focus(); });
564 byId("wizardClose").addEventListener("click", () => { byId("wizard").hidden = true; });
565 byId("wizard").addEventListener("input", (event) => { if (event.target.id === "wizardRoot") event.target.dataset.edited = "1"; syncWizard(); });
566 byId("wizardPreview").addEventListener("click", previewSite);
567 byId("wizardDeploy").addEventListener("click", deploySite);
568 byId("wizardEnableModules").addEventListener("click", enableMissingModules);
569 byId("wizardTemplateSelect").addEventListener("change", loadTemplate);
570 byId("wizardTemplateSave").addEventListener("click", saveTemplate);
571 byId("wizardTemplateDelete").addEventListener("click", deleteTemplate);
572 byId("detailToggle").addEventListener("click", toggleSite);
573 byId("detailPlan").addEventListener("click", planDetail);
574 byId("diffCancel").addEventListener("click", () => { writeState.pendingEdit = null; byId("diffPanel").hidden = true; });
575 byId("diffApply").addEventListener("click", applyEdit);
576 byId("undoButton").addEventListener("click", undoLastChange);
577 byId("acmeIssue").addEventListener("click", issueCertificate);
578 byId("certbotInstallButton").addEventListener("click", () => void installCertbot());
579 byId("fileText").addEventListener("input", syncWriteControls);
580 byId("fileSave").addEventListener("click", saveFile);
581 byId("fileRevert").addEventListener("click", () => { if (writeState.file) { const { path } = writeState.file; byId("fileText").value = writeState.file.text; void openFile(path); } });
582 byId("fileHistory").addEventListener("click", openHistory);
583 renderTemplates(); syncWizard();
584 window.addEventListener("beforeunload", (event) => { if (isDirty()) { event.preventDefault(); event.returnValue = ""; } });
SHA-256043c017f04adbb7df4fb5123cf0c7c0a6bbc5e14d6b2a987e534bf370e25b9b9

CH-J Proprietary Software License 1.14 · Mã nguồn được cung cấp theo CH-J Proprietary Software License 1.14. Việc công khai mã nguồn không thay đổi các điều khoản giấy phép và không cấp thêm quyền nào.

SHA-256 của tệp nénac8fe790f4ba60fc098f8c985fc2656cfcc6cdc21fb34b30e308a2b54e62258f
Tệp