Objavljen vir
Prenos vir ZIP
CH-J Tools
Brskajte po imenikih in datotekah za določeno izdajo programa.
CH-J / Server Manager
1704 datoteke
0.1.2-beta
Dodano: · Zgrajena:
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.4
const api = window.chjPlugin;5
const i18n = window.chjPluginI18n.createI18n("cs");6
const t = i18n.t;7
let endedSessionId = "";8
let sessionsLoading = false;10
async function sessionResult(promise, token) {11
const value = await promise;12
if (token !== generation) throw new Error("CHJ_SESSION_CHANGED");13
return value;14
}16
const elements = Object.fromEntries(["pluginVersion", "closeButton", "sessionSelect", "refreshButton", "sudoPassword", "rememberSudo", "loadButton", "reloadButton", "status", "apacheVersion", "serviceState", "mainConfig", "mpmName", "notice", "commandOutput"].map((id) => [id, document.querySelector(`#${id}`)]));17
const byId = (id) => document.getElementById(id);19
let sessions = [];20
let inspection = null;21
let inventory = null;22
let selected = null;23
let backends = null;24
let busy = false;25
let generation = 0;26
let sessionId = "";27
let logText = "";29
const TYPE_LABELS = { static: "Statický web", php: "PHP", proxy: "Reverse proxy", redirect: "Přesměrování", other: "Jiná konfigurace" };30
const LEVEL_LABELS = { error: "Chyba", warning: "Varování", recommendation: "Doporučení" };31
// What each finding means and what to do about it.32
const 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
};60
function selectedSession() { return sessions.find((session) => session.sessionId === elements.sessionSelect.value) || null; }61
function setStatus(message, error = false) { elements.status.textContent = message; elements.status.classList.toggle("error", error); }62
function errorText(error) { return String(error?.message || error).replace(/^Error invoking remote method '[^']*': Error: /, ""); }63
function needsPassword(error) { return /requires the sudo password/i.test(errorText(error)); }64
function auth() { return { sudoPassword: elements.sudoPassword.value, rememberSudo: elements.rememberSudo.checked }; }65
function setBusy(value) { busy = value; syncControls(); }66
function 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();75
}77
function 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;82
}83
function badge(text, tone = "") { return node("span", `badge${tone ? ` ${tone}` : ""}`, text); }84
function fill(container, items, emptyText) {85
if (items.length) container.replaceChildren(...items);86
else container.replaceChildren(node("p", "empty", emptyText));87
}88
function 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;97
}98
const place = (file, line) => (line ? `${file}:${line}` : file || "");99
const waitingText = () => (inventory ? "" : t("Konfigurace zatím není načtená."));101
function 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 || "—";108
}110
function 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);116
}118
async 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; }135
}137
async 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); } }149
}151
async 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); } }167
}169
function showNotice(text) { elements.notice.textContent = text; elements.notice.hidden = !text; }170
function listedSites() { return (inventory?.sites || []).filter((site) => site.role === "site" || site.role === "redirect"); }172
function 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(", ");176
}178
function 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);182
}184
function 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));190
}192
function 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);206
}208
function openDetail(site) { selected = site; byId("wizard").hidden = true; fillDetailForm(site); renderDetail(); renderWrite(); byId("siteDetail").scrollIntoView({ block: "nearest" }); }210
function 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), "");239
}241
function 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ů."));248
}250
function 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."));254
}256
function 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."));268
}270
function 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í."));274
}276
function 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 }))];279
}281
function 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."));290
}292
function 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."));294
}296
function 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;301
}303
function 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;310
}312
async 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); } }323
}325
function 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."));330
}332
function 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."));342
}344
function 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;353
}355
function 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."));368
}370
async 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); } }380
}382
function 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();386
}388
function 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;391
}393
elements.closeButton.addEventListener("click", () => api.close());394
elements.refreshButton.addEventListener("click", async () => { await loadSessions(); await inspect(); });395
elements.loadButton.addEventListener("click", () => void loadInventory());396
elements.sessionSelect.addEventListener("change", async () => { endedSessionId = ""; resetSession(elements.sessionSelect.value); await inspect(); });397
for (const tab of document.querySelectorAll(".tab")) tab.addEventListener("click", () => switchTab(tab.dataset.tab));398
byId("siteFilter").addEventListener("input", renderSites);399
byId("moduleFilter").addEventListener("input", renderModules);400
byId("accessFilter").addEventListener("input", renderRules);401
byId("fileFilter").addEventListener("input", renderFiles);402
byId("detailClose").addEventListener("click", () => { selected = null; byId("diffPanel").hidden = true; renderDetail(); });403
byId("detailOpen").addEventListener("click", () => { if (selected) void openFile(selected.editPath || selected.file, selected.line); });404
elements.reloadButton.addEventListener("click", () => void reloadApache());405
initWrite();406
byId("logRead").addEventListener("click", () => void readLog());407
byId("logSearch").addEventListener("input", renderLogText);408
byId("logStatus").addEventListener("change", renderLogText);409
byId("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));420
setInterval(() => { if (!document.hidden) void loadSessions().catch(() => {}); }, 3000);SHA-256
1dabad03fbf2c0d7be9c16dae82968745629ae193791abd826190bf46050d769CH-J Proprietary Software License 1.14 · Vir je na voljo pod CH-J Proprietary Software License 1.14. Njena razpoložljivost ne spreminja pogojev licence ali dodeljuje dodatnih pravic.
Arhiv SHA-256
ac8fe790f4ba60fc098f8c985fc2656cfcc6cdc21fb34b30e308a2b54e62258f