CH-J Toolsservera pārvaldība virs SSH
Izvēlne
Publicētais avots

CH-J Tools

Pārlūkot mapes un failus konkrētai lietojumprogrammas izdošanai.

Lejupielādēt avota ZIP
CH-J / Server Manager 1704 faili
0.1.2-beta Pievienots: · Būvēts:
index.js Pievienots: Datums nav reģistrēts 28,9 KB · 420 rindas
Kods Lejupielādēt failu
1"use strict";
3// Apache Manager: the overview. The changing side is in write.js. All text from the server is inserted as plain text.
4const api = window.chjPlugin;
5const i18n = window.chjPluginI18n.createI18n("cs");
6const t = i18n.t;
7let endedSessionId = "";
8let sessionsLoading = false;
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(["pluginVersion", "closeButton", "sessionSelect", "refreshButton", "sudoPassword", "rememberSudo", "loadButton", "reloadButton", "status", "apacheVersion", "serviceState", "mainConfig", "mpmName", "notice", "commandOutput"].map((id) => [id, document.querySelector(`#${id}`)]));
17const byId = (id) => document.getElementById(id);
19let sessions = [];
20let inspection = null;
21let inventory = null;
22let selected = null;
23let backends = null;
24let busy = false;
25let generation = 0;
26let sessionId = "";
27let logText = "";
29const TYPE_LABELS = { static: "Statický web", php: "PHP", proxy: "Reverse proxy", redirect: "Přesměrování", other: "Jiná konfigurace" };
30const LEVEL_LABELS = { error: "Chyba", warning: "Varování", recommendation: "Doporučení" };
31// What each finding means and what to do about it.
32const FINDINGS = {
33 "config-test-failed": ["Apache neprošel testem konfigurace: {output}", "Opravte chybu ve výpisu. Do té doby by restart služby selhal."],
34 "not-parsed": ["Soubor se nepodařilo rozebrat: {message}", "Přehled tento soubor nezahrnuje. Zkontrolujte ho ručně."],
35 "include-missing": ["Include odkazuje na neexistující {pattern}.", "Doplňte soubor, nebo použijte IncludeOptional, pokud je volitelný."],
36 "macro-unknown": ["Použité makro {name} není definované.", "Zkontrolujte, zda se načítá soubor s definicí makra."],
37 "broken-link": ["Symlink vede na neexistující {target}.", "Odstraňte symlink, nebo obnovte jeho cíl."],
38 "parser-missed-file": ["Apache tento soubor načítá, ale aplikace k němu nedošla.", "Přehled může být neúplný. Prosím nahlaste tuto konfiguraci."],
39 "parser-missed-vhost": ["Apache hlásí VirtualHost {name}, který aplikace nenašla.", "Přehled může být neúplný. Prosím nahlaste tuto konfiguraci."],
40 "duplicate-vhost": ["{name} je na adrese {address} definovaný vícekrát ({places}). Apache použije první.", "Určete, který VirtualHost má doménu obsluhovat, a ten druhý upravte."],
41 "no-listen": ["VirtualHost {site} je na portu {port}, na kterém Apache nenaslouchá.", "Přidejte Listen {port}, nebo změňte port VirtualHostu."],
42 "open-proxy": ["ProxyRequests On dělá ze serveru otevřenou proxy, kterou může zneužít kdokoli.", "Pro reverse proxy nastavte ProxyRequests Off."],
43 "docroot-missing": ["DocumentRoot {path} webu {site} neexistuje.", "Vytvořte adresář, nebo opravte cestu."],
44 "certificate-missing": ["Certifikát {path} webu {site} neexistuje.", "Doplňte soubor nebo opravte cestu; bez něj Apache nenastartuje."],
45 "certificate-not-set": ["Web {site} má zapnuté TLS, ale vlastní certifikát nemá.", "Ověřte, zda se dědí správný certifikát z globální konfigurace."],
46 "tls-without-name": ["VirtualHost s TLS nemá ServerName.", "Doplňte ServerName, aby odpovídal certifikátu."],
47 "old-tls": ["Web {site} povoluje zastaralý protokol ({value}).", "Povolte jen TLS 1.2 a novější."],
48 "php-socket-missing": ["PHP socket {path} webu {site} neexistuje.", "Spusťte PHP-FPM, nebo opravte cestu k socketu."],
49 "legacy-access": ["{path} používá starý zápis Order/Allow/Deny z Apache 2.2.", "Přepište ho na Require, až to bude vhodné."],
50 "directory-listing": ["{path} dovoluje výpis obsahu adresáře (Indexes).", "Pokud to není záměr, použijte Options -Indexes."],
51 "http-only": ["Web {site} běží jen přes nešifrované HTTP.", "Zvažte certifikát a HTTPS."],
52 "certificate-expired": ["Certifikát vypršel před {days} dny.", "Obnovte certifikát."],
53 "certificate-expiring": ["Certifikát vyprší za {days} dní.", "Ověřte automatickou obnovu, nebo ho obnovte ručně."],
54 "htaccess-ignored": ["Tento .htaccess Apache nepoužívá, protože AllowOverride je None.", "Přesuňte pravidla do hlavní konfigurace. AllowOverride zapínejte jen tam, kde je to potřeba."],
55 "twin-differs": ["Soubor se liší od načteného {loaded}. Apache používá {loaded}.", "Ověřte, která verze je správná, než jednu z nich změníte."],
56 "module-missing": ["Modul {module} není načtený, ale potřebují ho weby: {sites}. Test konfigurace to neodhalí, weby ale budou vracet chybu.", "Načtěte modul {module}, nebo upravte weby, které ho používají."],
57 "no-server-name": ["Globální ServerName není nastavený, Apache proto hlásí AH00558.", "Doplňte ServerName do hlavní konfigurace."]
58};
60function selectedSession() { return sessions.find((session) => session.sessionId === elements.sessionSelect.value) || null; }
61function setStatus(message, error = false) { elements.status.textContent = message; elements.status.classList.toggle("error", error); }
62function errorText(error) { return String(error?.message || error).replace(/^Error invoking remote method '[^']*': Error: /, ""); }
63function needsPassword(error) { return /requires the sudo password/i.test(errorText(error)); }
64function auth() { return { sudoPassword: elements.sudoPassword.value, rememberSudo: elements.rememberSudo.checked }; }
65function setBusy(value) { busy = value; syncControls(); }
66function syncControls() {
67 const ready = Boolean(elements.sessionSelect.value) && Boolean(inspection?.installed);
68 elements.sessionSelect.disabled = busy;
69 elements.refreshButton.disabled = busy;
70 elements.loadButton.disabled = busy || !ready;
71 byId("probeButton").disabled = busy || !inventory;
72 byId("logRead").disabled = busy || !inventory;
73 elements.reloadButton.disabled = busy || !ready;
74 syncWriteControls();
77function node(tag, className, text) {
78 const element = document.createElement(tag);
79 if (className) element.className = className;
80 if (text !== undefined) element.textContent = text;
81 return element;
83function badge(text, tone = "") { return node("span", `badge${tone ? ` ${tone}` : ""}`, text); }
84function fill(container, items, emptyText) {
85 if (items.length) container.replaceChildren(...items);
86 else container.replaceChildren(node("p", "empty", emptyText));
88function row(title, detail, badges = [], onClick = null) {
89 const element = node(onClick ? "button" : "div", "site-row");
90 if (onClick) { element.type = "button"; element.addEventListener("click", onClick); }
91 const main = node("span", "site-main");
92 main.append(node("strong", "", title), node("small", "", detail));
93 const holder = node("span", "badges");
94 holder.append(...badges);
95 element.append(main, holder);
96 return element;
98const place = (file, line) => (line ? `${file}:${line}` : file || "");
99const waitingText = () => (inventory ? "" : t("Konfigurace zatím není načtená."));
101function renderIdentity() {
102 elements.apacheVersion.textContent = inspection?.installed ? (inspection.version || t("nainstalován")) : t("nenalezen");
103 elements.serviceState.textContent = inspection?.serviceState || "—";
104 elements.serviceState.className = inspection?.serviceState === "active" ? "state-active" : "state-inactive";
105 elements.mainConfig.textContent = inventory?.mainConfig || "—";
106 elements.mainConfig.title = inventory?.mainConfig || "";
107 elements.mpmName.textContent = inspection?.mpm || "—";
110function resetSession(id) {
111 generation += 1; sessionId = id;
112 inspection = null; inventory = null; selected = null; backends = null; logText = "";
113 resetWrite();
114 elements.sudoPassword.value = ""; elements.rememberSudo.checked = false;
115 renderIdentity(); renderAll(); setBusy(false);
118async function loadSessions() {
119 if (sessionsLoading) return;
120 sessionsLoading = true;
121 try {
122 sessions = (await api.sessions.list()).filter((session) => session.state === "connected");
123 const next = api.sessions.reconcile(endedSessionId || sessionId, sessions);
124 elements.sessionSelect.replaceChildren(new Option(sessions.length ? t("Vyberte relaci…") : t("Žádná připojená relace"), ""));
125 for (const session of sessions) elements.sessionSelect.add(new Option(api.sessions.displayName(session), session.sessionId));
126 elements.sessionSelect.value = next.id;
127 if (next.reason === "choose") api.sessions.offerChoice(elements.sessionSelect, sessions, t("Vyberte relaci…"));
128 if (next.reason === "gone") endedSessionId ||= sessionId;
129 if (next.id !== sessionId) {
130 resetSession(next.id);
131 if (next.id) void inspect();
132 }
133 if (next.reason === "gone") setStatus(t("Relace skončila. Vyberte server."), true);
134 } finally { sessionsLoading = false; }
137async function inspect() {
138 const requestGeneration = generation;
139 if (!elements.sessionSelect.value) { inspection = null; renderIdentity(); syncControls(); return; }
140 setBusy(true); setStatus(t("Zjišťuji instalaci Apache…"));
141 try {
142 inspection = await sessionResult(api.apache.inspect(elements.sessionSelect.value), requestGeneration);
143 renderIdentity();
144 setStatus(inspection.installed ? t("Apache {version} je dostupný.", { version: inspection.version }) : t("Apache nebyl na serveru nalezen."), !inspection.installed);
145 } catch (error) {
146 if (requestGeneration !== generation) return;
147 inspection = null; renderIdentity(); setStatus(errorText(error), true);
148 } finally { if (requestGeneration === generation) { setBusy(false); if (inspection?.installed) void loadInventory(true); } }
151async function loadInventory(automatic = false) {
152 const requestGeneration = generation;
153 setBusy(true); setStatus(t("Čtu konfiguraci Apache…"));
154 try {
155 inventory = await sessionResult(api.apache.inventory(elements.sessionSelect.value, auth()), requestGeneration);
156 if (selected) selected = inventory.sites.find((site) => site.file === selected.file && site.line === selected.line) || null;
157 elements.commandOutput.textContent = inventory.testOutput || "—";
158 elements.commandOutput.classList.toggle("error", !inventory.testOk);
159 showNotice("");
160 renderIdentity(); renderAll();
161 setStatus(t("Načteno: {sites} webů, {files} souborů.", { sites: listedSites().length, files: inventory.files.length }));
162 } catch (error) {
163 if (requestGeneration !== generation || error?.message === "CHJ_SESSION_CHANGED") return;
164 if (needsPassword(error) && automatic) { setStatus(t("Konfiguraci čte Apache s oprávněním root. Zadejte heslo pro sudo a klikněte na Načíst konfiguraci.")); return; }
165 setStatus(errorText(error), true);
166 } finally { if (requestGeneration === generation) { elements.sudoPassword.value = ""; setBusy(false); } }
169function showNotice(text) { elements.notice.textContent = text; elements.notice.hidden = !text; }
170function listedSites() { return (inventory?.sites || []).filter((site) => site.role === "site" || site.role === "redirect"); }
172function siteTitle(site) {
173 const names = [site.domain, ...(site.aliases || [])].filter(Boolean);
174 if (!names.length) return t("Bez ServerName · {address}", { address: (site.addresses || []).join(" ") });
175 return names.length > 3 ? `${names.slice(0, 3).join(", ")} ${t("+{count} dalších", { count: names.length - 3 })}` : names.join(", ");
178function siteTarget(site) {
179 if (site.type === "redirect") return `→ ${site.redirectTarget || ""}`;
180 const parts = [site.documentRoot && !site.rootInherited ? site.documentRoot : "", site.phpHandler ? t("PHP: {target}", { target: site.phpHandler.target }) : "", ...(site.proxies || []).filter((rule) => !rule.excluded).map((rule) => `${rule.path} → ${rule.target}`)].filter(Boolean);
181 return parts.join(" · ") || place(site.file, site.line);
184function siteRow(site) {
185 const badges = [];
186 if (site.isDefault) badges.push(badge(t("Výchozí")));
187 if (site.redirectsHttp) badges.push(badge("HTTP → HTTPS"));
188 badges.push(site.sslEnabled ? badge("HTTPS", "good") : badge("HTTP", "warn"), site.enabled ? badge(t("Aktivní"), "good") : badge(t("Neaktivní")));
189 return row(siteTitle(site), `${t(TYPE_LABELS[site.type] || site.type)} ${siteTarget(site)}`, badges, () => openDetail(site));
192function renderSites() {
193 const query = byId("siteFilter").value.trim().toLowerCase();
194 const matches = (site) => !query || `${siteTitle(site)} ${siteTarget(site)} ${site.file}`.toLowerCase().includes(query);
195 fill(byId("siteList"), listedSites().filter(matches).map(siteRow), waitingText() || t("Filtru neodpovídá žádný web."));
196 const others = (inventory?.sites || []).filter((site) => site.role === "unnamed").filter(matches);
197 byId("otherBlock").hidden = !others.length;
198 fill(byId("otherList"), others.map(siteRow), "");
199 const known = Boolean(inventory);
200 const attention = (inventory?.findings || []).filter((item) => item.level !== "recommendation").length;
201 byId("statSites").textContent = known ? String(listedSites().length) : "—";
202 byId("statTls").textContent = known ? String(listedSites().filter((site) => site.sslEnabled).length) : "—";
203 byId("statModules").textContent = known ? String(inventory.modules.length) : "—";
204 byId("statFindings").textContent = known ? String(attention) : "—";
205 byId("statFindings").classList.toggle("warn", attention > 0);
208function openDetail(site) { selected = site; byId("wizard").hidden = true; fillDetailForm(site); renderDetail(); renderWrite(); byId("siteDetail").scrollIntoView({ block: "nearest" }); }
210function renderDetail() {
211 const site = selected;
212 byId("siteDetail").hidden = !site;
213 if (!site) return;
214 byId("detailTitle").textContent = site.domain || (site.addresses || []).join(" ");
215 const entries = [
216 ["Stav", site.enabled ? t("Aktivní") : t("Neaktivní, Apache soubor nenačítá")],
217 ["Potvrzeno Apache", site.enabled ? (site.confirmedByApache ? t("Ano, je ve výpisu -S") : t("Ne, není ve výpisu -S")) : ""],
218 ["Zdroj", place(site.file, site.line)],
219 ["Adresy", (site.addresses || []).join(" ")],
220 ["Domény", [site.domain, ...(site.aliases || [])].filter(Boolean).join(", ")],
221 ["Přesměrováno z", (site.redirectedFrom || []).join(", ")],
222 ["DocumentRoot", site.documentRoot ? `${site.documentRoot}${site.rootInherited ? ` (${t("zděděno z hlavní konfigurace")})` : ""}` : ""],
223 ["PHP", site.phpHandler ? `${site.phpHandler.kind}: ${site.phpHandler.target}${site.phpInherited ? ` (${t("zděděno z hlavní konfigurace")})` : ""}` : ""],
224 ["Certifikát", site.certificate || ""],
225 ["Protokoly TLS", site.sslProtocol || ""],
226 ["Proxy", (site.proxies || []).map((rule) => `${rule.path} → ${rule.target}`).join("\n")],
227 ["Přesměrování", (site.redirects || []).map((rule) => `${rule.from} → ${rule.to}`).join("\n")],
228 ["Přístupová pravidla", (site.access || []).map((block) => `<${block.section} ${block.path}> ${block.require.join(", ")}`).join("\n")],
229 ["Logy", [site.errorLog, ...(site.accessLog || [])].filter(Boolean).join(", ")],
230 ["Podmínky", (site.conditions || []).join(", ")],
231 ["Direktivy", String(site.directives)]
232 ].filter(([, value]) => value);
233 byId("detailGrid").replaceChildren(...entries.flatMap(([label, value]) => {
234 const text = node("dd", "", value); text.style.whiteSpace = "pre-wrap";
235 return [node("dt", "", t(label)), text];
236 }));
237 const related = (inventory?.findings || []).filter((item) => item.file === site.file && item.line === site.line);
238 fill(byId("detailFindings"), related.map(findingRow), "");
241function renderProxy() {
242 const rows = [];
243 for (const site of inventory?.sites || []) {
244 for (const rule of (site.proxies || []).filter((item) => !item.excluded)) rows.push(row(site.domain || place(site.file, site.line), `${rule.path} → ${rule.target}`, site.enabled ? [] : [badge(t("Neaktivní"))], () => openFile(rule.file, rule.line)));
245 }
246 fill(byId("proxyList"), rows, waitingText() || t("Žádný web nepředává požadavky na backend."));
247 fill(byId("balancerList"), (inventory?.server?.balancers || []).map((pool) => row(pool.name, pool.members.join(", "), [], () => openFile(pool.file, pool.line))), waitingText() || t("V konfiguraci není žádná skupina backendů."));
250function renderPhp() {
251 const rows = (inventory?.sites || []).filter((site) => site.phpHandler).map((site) => row(site.domain || place(site.file, site.line), site.phpHandler.target, [badge(site.phpHandler.kind === "php-fpm" ? "PHP-FPM" : "mod_php"), ...(site.phpInherited ? [badge(t("Zděděno"))] : [])], () => openFile(site.phpHandler.file, site.phpHandler.line)));
252 fill(byId("phpList"), rows, waitingText() || t("Žádný web nepoužívá PHP."));
253 fill(byId("socketList"), (inventory?.phpSockets || []).map((socket) => row(socket, "")), waitingText() || t("Na serveru nebyl nalezen žádný PHP-FPM socket."));
256function renderTls() {
257 const data = inventory?.certificates;
258 byId("certbotInstall").hidden = !(data && !data.certbotInstalled && data.certbotInstaller !== "none");
259 byId("tlsSummary").textContent = data ? `${data.certbotInstalled ? t("certbot je nainstalovaný") : t("certbot není nainstalovaný")} · ${data.renewal === "enabled" ? t("automatická obnova je zapnutá") : t("automatická obnova nebyla zjištěna")}` : "";
260 fill(byId("certList"), (data?.certificates || []).map((item) => {
261 const used = (inventory.sites || []).filter((site) => site.certificate === item.path).map((site) => site.domain).filter(Boolean).join(", ");
262 let state;
263 if (item.state !== "ok") state = badge(item.state === "missing" ? t("Soubor nenalezen") : t("Nelze přečíst"), "bad");
264 else if (item.daysLeft < 0) state = badge(t("Platnost vypršela"), "bad");
265 else state = badge(t("Zbývá dní: {count}", { count: item.daysLeft }), item.daysLeft < 21 ? "warn" : "good");
266 return row(item.path, [item.subject, item.issuer, used].filter(Boolean).join(" · "), [state]);
267 }), waitingText() || t("V konfiguraci ani v Let's Encrypt není žádný certifikát."));
270function renderModules() {
271 const query = byId("moduleFilter").value.trim().toLowerCase();
272 fill(byId("moduleList"), (inventory?.modules || []).filter((item) => item.name.includes(query)).map((item) => row(item.name, "", [badge(item.kind === "static" ? t("Vestavěný") : t("Sdílený")), ...moduleAction(item.name, item.kind, true)])), waitingText() || t("Filtru neodpovídá žádný modul."));
273 fill(byId("availableList"), (inventory?.availableModules || []).filter((item) => item.includes(query)).map((item) => row(item, "", moduleAction(item, "shared", false))), waitingText() || t("Tento server seznam dostupných modulů neuvádí."));
276function allOf(key) {
277 const own = (inventory?.sites || []).flatMap((site) => (site[key] || []).map((item) => ({ ...item, site })));
278 return [...own, ...(inventory?.server?.[key] || []).map((item) => ({ ...item, site: null }))];
281function renderRules() {
282 const redirects = allOf("redirects").filter((rule) => rule.kind.toLowerCase() !== "rewriterule" || rule.redirect);
283 fill(byId("redirectList"), redirects.map((rule) => row(`${rule.from} → ${rule.to}`, `${rule.kind} · ${rule.site?.domain || t("celý server")} · ${place(rule.file, rule.line)}`, [], () => openFile(rule.file, rule.line))), waitingText() || t("Konfigurace nemá žádné přesměrování."));
284 const query = byId("accessFilter").value.trim().toLowerCase();
285 const blocks = allOf("access").filter((block) => !query || `${block.path} ${block.file}`.toLowerCase().includes(query));
286 fill(byId("accessList"), blocks.slice(0, 300).map((block) => {
287 const details = [block.require.length ? `Require ${block.require.join(", ")}` : "", block.allowOverride ? `AllowOverride ${block.allowOverride}` : "", block.options ? `Options ${block.options}` : "", block.auth ? `AuthType ${block.auth}` : ""].filter(Boolean);
288 return row(`<${block.section} ${block.path}>`, `${details.join(" · ")}${details.length ? " · " : ""}${block.site?.domain || t("celý server")} · ${place(block.file, block.line)}`, block.legacy ? [badge("2.2", "warn")] : [], () => openFile(block.file, block.line));
289 }), waitingText() || t("Filtru neodpovídá žádné pravidlo."));
292function renderHtaccess() {
293 fill(byId("htaccessList"), (inventory?.htaccess || []).map((file) => row(file.path, t("{size} B", { size: file.size }), file.findingIgnored ? [badge(t("Apache ho ignoruje"), "warn")] : [], () => { byId("htaccessText").value = file.text; })), waitingText() || t("V kořenech webů není žádný .htaccess."));
296function renderLogs() {
297 const select = byId("logFile");
298 const previous = select.value;
299 select.replaceChildren(...(inventory?.logs || []).map((path) => { const option = node("option", "", path); option.value = path; return option; }));
300 if ((inventory?.logs || []).includes(previous)) select.value = previous;
303function renderLogText() {
304 const query = byId("logSearch").value.trim().toLowerCase();
305 const status = byId("logStatus").value;
306 const pattern = status ? new RegExp(`" ${status}\\d\\d `) : null;
307 const output = byId("logOutput");
308 output.textContent = logText.split("\n").filter((line) => (!query || line.toLowerCase().includes(query)) && (!pattern || pattern.test(line))).join("\n");
309 output.scrollTop = output.scrollHeight;
312async function readLog() {
313 const path = byId("logFile").value;
314 if (!path) return;
315 const requestGeneration = generation;
316 setBusy(true);
317 try {
318 const result = await sessionResult(api.apache.readLog(elements.sessionSelect.value, { path, lines: Number(byId("logLines").value), ...auth() }), requestGeneration);
319 logText = result.text; renderLogText(); setStatus(t("Log byl načten."));
320 } catch (error) {
321 if (requestGeneration === generation && error?.message !== "CHJ_SESSION_CHANGED") setStatus(errorText(error), true);
322 } finally { if (requestGeneration === generation) { elements.sudoPassword.value = ""; setBusy(false); } }
325function renderPerformance() {
326 const tuning = (inventory?.server?.tuning || []).map((item) => row(`${item.name} ${item.value}`, place(item.file, item.line), [], () => openFile(item.file, item.line)));
327 const head = inventory ? [row(`MPM ${inventory.mpm || "—"}`, t("Běžících procesů: {count}", { count: inventory.processes }))] : [];
328 fill(byId("tuningList"), [...head, ...tuning], waitingText());
329 fill(byId("listenList"), (inventory?.server?.listen || []).map((item) => row(item.value, place(item.file, item.line), [], () => openFile(item.file, item.line))), waitingText() || t("Konfigurace neuvádí žádné Listen."));
332function renderFiles() {
333 const query = byId("fileFilter").value.trim().toLowerCase();
334 const files = [...(inventory?.files || [])].filter((file) => file.path.toLowerCase().includes(query)).sort((a, b) => Number(b.loaded) - Number(a.loaded) || (a.order ?? 0) - (b.order ?? 0) || a.path.localeCompare(b.path));
335 const skipped = (inventory?.skippedFiles || []).filter((file) => file.path.toLowerCase().includes(query)).map((file) => row(file.path, t("Přeskočeno: {size} B, velký nebo binární soubor", { size: file.size })));
336 fill(byId("fileList"), [...files.slice(0, 500).map((file) => {
337 const badges = [file.loaded ? badge(t("Načtený"), "good") : badge(t("Nenačtený"))];
338 if (file.linkTarget) badges.push(badge("symlink"));
339 if (file.parseError) badges.push(badge(t("Nerozebráno"), "warn"));
340 return row(file.path, file.linkTarget ? `→ ${file.linkTarget}` : t("{size} B", { size: file.size }), badges, () => void openFile(file.linkTarget && (inventory.files || []).some((item) => item.path === file.linkTarget) ? file.linkTarget : file.path));
341 }), ...skipped], waitingText() || t("Filtru neodpovídá žádný soubor."));
344function findingRow(item) {
345 const [reason, fix] = FINDINGS[item.code] || [item.code, ""];
346 const values = Object.fromEntries(Object.entries(item.params || {}).map(([key, value]) => [key, Array.isArray(value) ? value.join(", ") : value ?? ""]));
347 const tone = item.level === "error" ? "bad" : item.level === "warning" ? "warn" : "";
348 const element = row(t(reason, values), place(item.file, item.line), [badge(t(LEVEL_LABELS[item.level]), tone)], item.file ? () => openFile(item.file, item.line) : null);
349 element.classList.add("finding");
350 if (fix) element.querySelector(".site-main").append(node("span", "fix", t(fix, values)));
351 addFindingFix(element, item);
352 return element;
355function renderDiagnostics() {
356 const list = inventory?.findings || [];
357 const count = (level) => list.filter((item) => item.level === level).length;
358 byId("countError").textContent = t("Chyby: {count}", { count: count("error") });
359 byId("countWarning").textContent = t("Varování: {count}", { count: count("warning") });
360 byId("countRecommendation").textContent = t("Doporučení: {count}", { count: count("recommendation") });
361 fill(byId("findingList"), list.map(findingRow), waitingText() || t("Bez nalezených problémů."));
362 const missed = list.filter((item) => item.code.startsWith("parser-missed")).length;
363 fill(byId("agreementList"), inventory ? [
364 row(t("VirtualHosty podle výpisu -S"), t("Apache hlásí {count}, všechny aplikace našla, pokud výše není nález.", { count: inventory.vhostsReported }), [badge(missed ? t("Neshoda") : t("Shoda"), missed ? "warn" : "good")]),
365 row(t("Načtené soubory"), inventory.includesReported ? t("Porovnáno s výpisem DUMP_INCLUDES.") : t("Tato instalace DUMP_INCLUDES nepodporuje, seznam vychází z vlastního rozboru."), [badge(inventory.includesReported ? t("Ověřeno") : t("Neověřeno"), inventory.includesReported ? "good" : "warn")])
366 ] : [], waitingText());
367 fill(byId("backendList"), (backends?.backends || []).map((item) => row(item.target, item.sites.join(", "), [badge(item.state === "open" ? t("Dostupný") : item.state === "closed" ? t("Nedostupný") : t("Nezjištěno"), item.state === "open" ? "good" : item.state === "closed" ? "bad" : "warn")])), backends ? t("Konfigurace neuvádí žádný backend.") : t("Spojení zatím nebylo vyzkoušeno."));
370async function probeBackends() {
371 const requestGeneration = generation;
372 setBusy(true); setStatus(t("Zkouším spojení s backendy…"));
373 try {
374 backends = await sessionResult(api.apache.probeBackends(elements.sessionSelect.value, auth()), requestGeneration);
375 renderDiagnostics();
376 setStatus(backends.omitted ? t("Hotovo. Vyzkoušeno prvních 32 backendů, {count} dalších ne.", { count: backends.omitted }) : t("Hotovo."));
377 } catch (error) {
378 if (requestGeneration === generation && error?.message !== "CHJ_SESSION_CHANGED") setStatus(errorText(error), true);
379 } finally { if (requestGeneration === generation) { elements.sudoPassword.value = ""; setBusy(false); } }
382function renderAll() {
383 renderSites(); renderDetail(); renderWrite(); renderProxy(); renderPhp(); renderTls(); renderModules(); renderRules(); renderHtaccess(); renderLogs(); renderPerformance(); renderFiles(); renderDiagnostics();
384 if (!inventory) { byId("htaccessText").value = ""; byId("logOutput").textContent = ""; elements.commandOutput.textContent = "—"; elements.commandOutput.classList.remove("error"); }
385 syncControls();
388function switchTab(name) {
389 for (const tab of document.querySelectorAll(".tab")) { tab.classList.toggle("active", tab.dataset.tab === name); tab.setAttribute("aria-selected", String(tab.dataset.tab === name)); }
390 for (const panel of document.querySelectorAll(".tab-panel")) panel.hidden = panel.dataset.panel !== name;
393elements.closeButton.addEventListener("click", () => api.close());
394elements.refreshButton.addEventListener("click", async () => { await loadSessions(); await inspect(); });
395elements.loadButton.addEventListener("click", () => void loadInventory());
396elements.sessionSelect.addEventListener("change", async () => { endedSessionId = ""; resetSession(elements.sessionSelect.value); await inspect(); });
397for (const tab of document.querySelectorAll(".tab")) tab.addEventListener("click", () => switchTab(tab.dataset.tab));
398byId("siteFilter").addEventListener("input", renderSites);
399byId("moduleFilter").addEventListener("input", renderModules);
400byId("accessFilter").addEventListener("input", renderRules);
401byId("fileFilter").addEventListener("input", renderFiles);
402byId("detailClose").addEventListener("click", () => { selected = null; byId("diffPanel").hidden = true; renderDetail(); });
403byId("detailOpen").addEventListener("click", () => { if (selected) void openFile(selected.editPath || selected.file, selected.line); });
404elements.reloadButton.addEventListener("click", () => void reloadApache());
405initWrite();
406byId("logRead").addEventListener("click", () => void readLog());
407byId("logSearch").addEventListener("input", renderLogText);
408byId("logStatus").addEventListener("change", renderLogText);
409byId("probeButton").addEventListener("click", () => void probeBackends());
411(async () => {
412 try { i18n.setLanguage(await api.ui.getLanguage()); } catch {}
413 i18n.apply(document);
414 api.ui.onLanguageChanged((language) => { i18n.setLanguage(language); i18n.apply(document); renderIdentity(); renderAll(); });
415 const info = await api.getInfo(); elements.pluginVersion.textContent = `${info.name} v${info.version} · ${info.publisher}`;
416 renderAll();
417 await loadSessions();
418})().catch((error) => setStatus(errorText(error), true));
420setInterval(() => { if (!document.hidden) void loadSessions().catch(() => {}); }, 3000);
SHA-2561dabad03fbf2c0d7be9c16dae82968745629ae193791abd826190bf46050d769

CH-J Proprietary Software License 1.14 · Avots ir sniegts saskaņā ar CH-J Proprietary Software License 1.14. Tā pieejamība nemaina licences noteikumus un nepiešķir papildu tiesības.

Arhīvs SHA-256ac8fe790f4ba60fc098f8c985fc2656cfcc6cdc21fb34b30e308a2b54e62258f
Faili