Vydané zdroje
Stáhnout zdroje ZIP
CH-J Tools
Procházejte adresáře a soubory konkrétního vydání aplikace.
CH-J / Server Manager
1704 souborů
0.1.2-beta
Přidáno: · Sestaveno:
1
"use strict";3
const api = window.chjPlugin;4
const i18n = window.chjPluginI18n?.createI18n("cs");5
const t = (key, params = {}) => i18n ? i18n.t(key, params) : String(key).replace(/\{(\w+)\}/g, (all, name) => String(params[name] ?? all));6
const byId = (id) => document.getElementById(id);7
let sessions = [], sessionId = "", endedSessionId = "", sessionsLoading = false;8
let generation = 0, revision = 0, planRequest = 0, inspectRequest = 0, diagnosticRequest = 0;9
let inventory = null, selected = null, plan = null, transaction = null;10
let busy = false, planning = false, diagnosticBusy = false;12
function node(tag, className = "", text) {13
const element = document.createElement(tag);14
if (className) element.className = className;15
if (text !== undefined) element.textContent = String(text);16
return element;17
}18
function badge(text, tone = "") { return node("span", `badge ${tone}`, text); }19
function row(title, detail = "", badges = [], onClick) {20
const element = node(onClick ? "button" : "div", "site-row");21
if (onClick) { element.type = "button"; element.addEventListener("click", onClick); }22
const main = node("span", "site-main");23
main.append(node("strong", "", title), node("small", "", detail));24
const holder = node("span", "badges"); holder.append(...badges);25
element.append(main, holder);26
return element;27
}28
function fill(id, rows, empty = t("Žádné záznamy.")) {29
byId(id).replaceChildren(...(rows.length ? rows : [node("p", "empty", inventory ? empty : t("Údaje zatím nejsou načtené."))]));30
}31
function notice(id, text) { byId(id).textContent = text; byId(id).hidden = !text; }32
function setStatus(text, error = false) { byId("status").textContent = text; byId("status").classList.toggle("error", error); }33
function errorText(error) { return String(error?.message || error).replace(/^Error invoking remote method '[^']*': Error: /, ""); }34
function takeSecret(id) { const value = byId(id).value; byId(id).value = ""; return value; }35
function auth() { return { sudoPassword: takeSecret("sudoPassword") }; }36
function credentials() { return { password: takeSecret("verifyPassword"), passphrase: takeSecret("verifyPassphrase") }; }37
function connected(id = sessionId) { return Boolean(id) && sessions.some((s) => s.sessionId === id && s.state === "connected"); }38
function selectedSession() { return sessions.find((s) => s.sessionId === sessionId); }39
function terminal(tx = transaction) { return tx?.state === "confirmed" || tx?.state === "rolled-back" || tx?.state === "failed"; }40
function changing() { return Boolean(transaction && !terminal()); }41
function syncControls() {42
const ready = connected() && Boolean(inventory) && !busy && !planning && !changing();43
byId("loadButton").disabled = !connected() || busy;44
byId("refreshButton").disabled = sessionsLoading;45
byId("planInterface").disabled = !ready || !selected;46
for (const id of ["planRule", "planPolicy", "planProfile"]) byId(id).disabled = !ready || inventory?.firewall?.readable !== true || !editableBackend();47
for (const id of ["runDiagnostics", "checkPort"]) byId(id).disabled = !connected() || !inventory || diagnosticBusy;48
byId("applyPlan").disabled = !canApply();49
for (const button of document.querySelectorAll("[data-remove-rule]")) button.disabled = !ready;50
}51
function resetSession(id) {52
generation += 1; inspectRequest += 1; diagnosticRequest += 1;53
sessionId = id; inventory = null; selected = null; busy = false; planning = false; diagnosticBusy = false;54
invalidatePlan();55
takeSecret("sudoPassword");56
byId("interfaceEditor").hidden = true;57
byId("diagnosticResults").replaceChildren();58
for (const id of ["pendingNotice", "sudoNotice"]) notice(id, "");59
renderAll();60
}61
async function loadSessions() {62
if (sessionsLoading) return;63
sessionsLoading = true;64
try {65
sessions = (await api.sessions.list()).filter((session) => session.state === "connected");66
// Reconcile against the selection NOW, not the selection when list() started.67
const next = api.sessions.reconcile(endedSessionId || sessionId, sessions);68
const select = byId("sessionSelect");69
select.replaceChildren(new Option(sessions.length ? t("Vyberte relaci…") : t("Žádná připojená relace"), ""));70
for (const session of sessions) select.add(new Option(api.sessions.displayName(session), session.sessionId));71
select.value = next.id;72
if (next.reason === "choose") api.sessions.offerChoice(select, sessions, t("Vyberte relaci…"));73
if (next.reason === "gone") endedSessionId ||= sessionId;74
if (next.id !== sessionId) { resetSession(next.id); if (next.id) void inspect(); }75
if (next.reason === "gone") setStatus(t("Relace skončila. Vyberte server."), true);76
} catch (error) { setStatus(errorText(error), true); }77
finally { sessionsLoading = false; syncControls(); }78
}79
async function inspect() {80
if (!connected()) return;81
const own = generation, request = ++inspectRequest, id = sessionId;82
busy = true; invalidatePlan(); const editorRevision = revision; syncControls(); setStatus(t("Načítám síť a firewall…"));83
const results = await Promise.allSettled([84
api.network.inspect({ sessionId: id, ...auth() }),85
api.network.pending({ sessionId: id })86
]);87
if (own !== generation || request !== inspectRequest) return;88
busy = false;89
const [reading, pending] = results;90
if (reading.status === "fulfilled") {91
inventory = reading.value;92
selected = inventory.interfaces?.find((item) => item.name === selected?.name) || null;93
renderAll();94
if (selected && editorRevision === revision) openInterface(selected);95
if (!selected) byId("interfaceEditor").hidden = true;96
notice("sudoNotice", inventory.sudoError || inventory.firewall?.readable === false ? t("Firewall nelze přečíst. Zadejte heslo pro sudo a znovu klikněte na Načíst.") : "");97
setStatus(t("Síť a firewall načteny."));98
} else {99
inventory = null; selected = null; byId("interfaceEditor").hidden = true; renderAll();100
setStatus(errorText(reading.reason), true);101
}102
if (pending.status === "fulfilled") {103
const item = pending.value;104
notice("pendingNotice", item?.id ? t("Na serveru čeká změna {id}. Stav: {state}.", { id: item.id, state: stateText(item.state) }) : "");105
if (item?.id && (!changing() || transaction?.untracked) && transaction?.id !== item.id) startTransaction({ id: item.id, state: item.state, sessionId: id, server: api.sessions.displayName(selectedSession()), resumed: true });106
if (!item?.id && transaction?.untracked && transaction.sessionId === id) {107
transaction.state = "failed"; transaction.untracked = false;108
transaction.message = t("Server nehlásí čekající změnu. Ověřte aktuální stav v načteném přehledu a původní připojení.");109
renderTransaction();110
}111
} else notice("pendingNotice", t("Čekající změnu se nepodařilo zjistit: {message}", { message: errorText(pending.reason) }));112
syncControls();113
}114
function kindText(kind) {115
switch (kind) {116
case "loopback": return t("Smyčka"); case "ethernet": return t("Ethernet"); case "bridge": return t("Most");117
case "bond": return t("Sdružené rozhraní"); case "vlan": return t("VLAN"); case "wireguard": return t("WireGuard");118
case "tun": return t("Tunel"); case "veth": return t("Virtuální pár"); case "dummy": return t("Virtuální rozhraní");119
case "wifi": return t("Wi-Fi"); default: return t("Jiné rozhraní");120
}121
}122
function stateText(state) {123
switch (state) {124
case "confirmed": return t("Potvrzeno"); case "rolled-back": return t("Vráceno"); case "applying": return t("Provádění změny");125
case "verified": return t("Spojení ověřeno"); case "pending": case "waiting": case "applied": return t("Čeká na potvrzení");126
case "rolling-back": return t("Obnovování původního stavu"); default: return t("Stav: {state}", { state: state || t("Nezjištěno") });127
}128
}129
function actionText(action) {130
switch (action) { case "accept": return t("Povolit"); case "drop": return t("Zahodit"); case "reject": return t("Odmítnout"); case "log": return t("Zaznamenat"); default: return t("Nezjištěno"); }131
}132
function directionText(direction) { return direction === "in" ? t("Příchozí") : direction === "out" ? t("Odchozí") : t("Předávaný"); }133
function addressText(a) { return `${a.address}${a.prefix == null ? "" : `/${a.prefix}`}`; }134
function renderInterfaces() {135
const list = inventory?.interfaces || [];136
byId("statCount").textContent = inventory ? list.length : "—";137
byId("statActive").textContent = inventory ? list.filter((i) => i.state === "up").length : "—";138
byId("statIPv6").textContent = inventory ? list.filter((i) => i.addresses?.some((a) => a.family === 6)).length : "—";139
byId("statManager").textContent = inventory?.config?.manager || "—";140
fill("interfaceList", list.map((item) => {141
const marks = [badge(item.state === "up" ? t("Aktivní") : item.state === "down" ? t("Neaktivní") : t("Nezjištěno"), item.state === "up" ? "good" : "")];142
if (item.ssh || inventory.ssh?.interface === item.name) marks.push(badge(t("Aktuální SSH – rizikové"), "warn"));143
const carrier = item.carrier === true ? t("Linka připojena") : item.carrier === false ? t("Bez linky") : t("Linka nezjištěna");144
const speed = item.speedMbps == null ? "" : t("{speed} Mb/s", { speed: item.speedMbps });145
const element = row(item.name, [kindText(item.kind), speed, carrier, ...(item.addresses || []).map(addressText)].filter(Boolean).join(" · "), marks, () => openInterface(item));146
element.classList.toggle("active", item.name === selected?.name);147
return element;148
}));149
}150
function openInterface(item) {151
invalidatePlan(); selected = item;152
byId("interfaceEditor").hidden = false;153
byId("interfaceMeta").textContent = t("{name} · spravuje {manager} · {file}", { name: item.name, manager: item.managedBy || inventory?.config?.manager || t("Nezjištěno"), file: item.configuredIn || item.methodSource || t("Soubor nezjištěn") });154
byId("sshNotice").hidden = !(item.ssh || inventory?.ssh?.interface === item.name);155
for (const family of [4, 6]) {156
const known = item[`ipv${family}Method`];157
// Only link-local IPv6 says nothing about the method; the kernel's default behaves like SLAAC. The diff shows it.158
byId(`ipv${family}Method`).value = known && known !== "unknown" ? known : family === 6 ? "slaac" : "unknown";159
byId(`ipv${family}Addresses`).value = (item.addresses || []).filter((a) => a.family === family && a.scope !== "link" && a.scope !== "host").map(addressText).join("\n");160
byId(`gateway${family}`).value = item[`gateway${family}`] || "";161
}162
// What DHCP hands out must not become a fixed setting by being shown here.163
byId("interfaceDns").value = byId("ipv4Method").value === "dhcp" ? "" : (item.dns || []).join("\n");164
byId("interfaceSearch").value = byId("ipv4Method").value === "dhcp" ? "" : (inventory?.dns?.search || []).join("\n");165
byId("interfaceMtu").value = item.mtu && item.mtu !== 1500 ? item.mtu : "";166
syncMethods(); renderInterfaces(); syncControls();167
}168
function syncMethods() {169
for (const family of [4, 6]) {170
const method = byId(`ipv${family}Method`).value;171
byId(`ipv${family}Addresses`).disabled = method !== "static";172
byId(`gateway${family}`).disabled = method !== "static" || method === "unknown";173
}174
}175
function renderRoutes() {176
fill("routeList", (inventory?.routes || []).map((r) => row(r.destination || (r.default ? "default" : "—"), t("IPv{family} · přes {gateway} · {device} · metrika {metric}", { family: r.family, gateway: r.gateway || "—", device: r.device || "—", metric: r.metric ?? "—" }), r.default ? [badge(t("Výchozí"), "good")] : [])));177
fill("dnsList", inventory ? [row(t("DNS servery"), (inventory.dns?.servers || []).join(", ") || "—"), row(t("Vyhledávací domény"), (inventory.dns?.search || []).join(", ") || "—"), row(t("Zdroj DNS"), inventory.dns?.source || "—")] : []);178
fill("listenerList", listenerRows());179
}180
function editableBackend() {181
const active = inventory?.firewall?.active;182
return active && active !== "none" && active !== "unknown" ? active : byId("firewallBackend").value;183
}184
// One row for a service, not one for each of its addresses and IP versions.185
function listenerRows() {186
const grouped = new Map();187
for (const item of inventory?.listening || []) {188
const protocol = String(item.protocol).replace(/6$/, ""), processes = item.processes || [];189
const key = `${protocol}:${item.port}:${processes.join(",")}`;190
if (!grouped.has(key)) grouped.set(key, { protocol, port: item.port, processes, addresses: [] });191
const entry = grouped.get(key);192
if (!entry.addresses.includes(item.address)) entry.addresses.push(item.address);193
}194
const rows = [...grouped.values()].sort((a, b) => a.port - b.port || a.protocol.localeCompare(b.protocol))195
.map((item) => row([item.protocol, item.port, ...(item.processes.length ? [item.processes.join(", ")] : [])].join(" · "), item.addresses.join(", ")));196
if (rows.length && inventory?.sudoError) rows.unshift(row(t("Bez sudo nejde zjistit, který program port používá."), ""));197
return rows;198
}199
function removable(rule) {200
// Only the tool's own list. Its internal chains and rules of other tools stay as they are.201
if (rule.managed !== true || !rule.id || rule.backend !== editableBackend()) return false;202
if (rule.direction === "forward") return false;203
if (rule.backend === "ufw") return Number(rule.number) > 0;204
if (rule.backend === "firewalld") return ["port", "service", "rich"].includes(rule.kind);205
return rule.backend === "nftables" || rule.backend === "iptables";206
}207
function visibleRules(fw) {208
const zones = new Set((fw?.zones || []).filter((zone) => zone.active).map((zone) => zone.name));209
return (fw?.rules || []).filter((rule) => byId("showPlumbing").checked || (!rule.plumbing && (!rule.zone || !zones.size || zones.has(rule.zone))));210
}212
function populateSelect(id, items, label) {213
const select = byId(id), value = select.value;214
select.replaceChildren(new Option(label, ""), ...items.map((s) => new Option(s, s)));215
select.value = items.includes(value) ? value : "";216
}217
function renderFirewall() {218
const fw = inventory?.firewall;219
const active = (fw?.backends || []).filter((b) => b.active);220
const candidates = fw?.active && !["none", "unknown"].includes(fw.active) ? [fw.active] : (fw?.backends || []).filter((b) => b.installed).map((b) => b.name);221
const backendSelect = byId("firewallBackend"), previous = backendSelect.value;222
backendSelect.replaceChildren(new Option(t("Vyberte nástroj…"), ""), ...candidates.map((name) => new Option(name, name)));223
backendSelect.value = candidates.includes(previous) ? previous : candidates.length === 1 ? candidates[0] : "";224
backendSelect.disabled = active.length > 0;225
byId("firewallMeta").textContent = fw ? t("Spravuje: {tool}. Pravidla: {readable}. Vstup: {input}, výstup: {output}, předávání: {forward}.", { tool: fw.active === "none" ? t("Žádný firewall") : fw.active || t("Nezjištěno"), readable: fw.readable ? t("Čitelná") : t("Nedostupná"), input: actionText(fw.defaultPolicy?.input), output: actionText(fw.defaultPolicy?.output), forward: actionText(fw.defaultPolicy?.forward) }) : "—";226
fill("backendList", (fw?.backends || []).filter((b) => b.installed || b.active).map((b) => row(b.name, b.detail || "", [badge(b.active ? t("Aktivní") : t("Neaktivní"), b.active ? "good" : ""), badge(b.enabled === true ? t("Spouští se při startu") : b.enabled === false ? t("Start nezapnut") : t("Spouštění nezjištěno"))])));227
notice("firewallWarning", active.length > 1 || (inventory?.findings || []).some((f) => f.code === "several-firewalls") ? t("Běží několik firewallů. Nezapínejte další; ověřte, který má pravidla spravovat.") : "");228
notice("runtimeNotice", fw?.differs || fw?.runtimeDiffers || (fw?.findings || []).some((f) => f.code === "firewall-runtime-differs") ? t("Běžící pravidla se liší od trvalé konfigurace. Zkontrolujte zóny a návrh před uložením.") : "");229
fill("zoneList", (fw?.zones || []).map((z) => row(z.name, [z.target, ...(z.interfaces || []), ...(z.sources || []), ...(z.services || []), ...(z.ports || [])].filter(Boolean).join(" · "), z.active ? [badge(t("Aktivní"), "good")] : [])));230
populateSelect("ruleInterface", (inventory?.interfaces || []).map((i) => i.name), t("Všechna rozhraní"));231
populateSelect("ruleZone", (fw?.zones || []).map((z) => z.name), t("Výchozí zóna"));232
byId("ruleZone").disabled = !["firewalld", "fw4"].includes(fw?.active);233
const hidden = (fw?.rules || []).length - visibleRules(fw).length;234
byId("hiddenRules").textContent = hidden ? t("Skryto: {count} interních pravidel nástroje a pravidel neaktivních zón. Zůstávají beze změny.", { count: hidden }) : "";235
fill("ruleList", visibleRules(fw).map((rule) => {236
const opaque = rule.understood === false;237
const element = row(opaque ? t("Nerozpoznané pravidlo – zachováno") : `${directionText(rule.direction)} · ${actionText(rule.action)} · ${rule.protocol || "—"} ${(rule.ports || []).join(", ")}`, [rule.backend, t("Rodina: {family}", { family: rule.family }), rule.source, rule.destination, rule.interface, rule.zone, rule.comment].filter(Boolean).join(" · "), [badge(directionText(rule.direction))]);238
if (rule.direction === "forward") element.lastChild?.append(badge(t("Pouze pro čtení")));239
if (opaque) element.firstChild.append(node("pre", "command rule-raw", rule.raw || "—"));240
if (removable(rule)) {241
const button = node("button", "mini", t("Navrhnout odstranění")); button.type = "button"; button.dataset.removeRule = rule.id;242
button.addEventListener("click", () => void requestPlan("firewall", { remove: [rule.id], backend: rule.backend, zone: rule.zone || undefined })); element.append(button);243
}244
return element;245
}));246
fill("natList", (fw?.nat || []).map((item) => {247
const entry = row(t("NAT / předávání"), "", [badge(t("Pouze pro čtení"))]);248
entry.firstChild.append(node("pre", "command", typeof item === "string" ? item : JSON.stringify(item, null, 2)));249
return entry;250
}));251
}253
function findingText(item) {254
const p = item.params || {};255
switch (item.code) {256
case "several-network-managers": return [t("Síť spravuje více nástrojů."), t("Ověřte správce každého rozhraní. Souběžná instalace sama o sobě není chyba.")];257
case "no-default-route": return [t("Chybí výchozí trasa IPv4."), t("Pokud server potřebuje cestu mimo místní síť, nastavte bránu.")];258
case "no-default-route-v6": return [t("Globální IPv6 adresa nemá výchozí trasu."), t("Ověřte oznámení směrovače a bránu IPv6.")];259
case "interface-no-carrier": return [t("Rozhraní {interface} nemá linku.", { interface: p.interface || p.name || "—" }), t("Zkontrolujte kabel, přepínač nebo virtuální síť. Odpojené rozhraní může být záměr.")];260
case "needs-root": return [t("Firewall vyžaduje oprávnění root."), t("Zadejte heslo pro sudo a znovu načtěte údaje.")];261
case "several-firewalls": return [t("Pravidla spravuje více aktivních firewallů."), t("Určete jeden nástroj pro správu. Další firewall nezapínejte.")];262
case "firewall-runtime-differs": return [t("Běžící a trvalá pravidla se liší."), t("Porovnejte zóny firewalld před ukládáním a restartem.")];263
case "ssh-may-be-blocked": return [t("Firewall může blokovat SSH na portu {port}.", p), t("Povolte port této relace ze správcovské sítě a ověřte nové spojení.")];264
case "icmpv6-blocked": return [t("ICMPv6 může být blokované. Nefunguje pak hledání sousedů ani zjišťování MTU."), t("Povolte potřebné ICMPv6 zprávy podle pravidel vaší sítě.")];265
case "no-firewall": return [t("Není aktivní firewall."), t("Zvažte ochranu podle role serveru a pravidel nadřazené sítě.")];266
case "ufw-inactive": return [t("UFW je neaktivní."), t("Ověřte, zda provoz chrání jiný nástroj. UFW nezapínejte souběžně s ním.")];267
default: return [t("Neznámý nález: {code}", { code: item.code }), t("Prověřte podrobnosti na serveru před změnou konfigurace.")];268
}269
}270
function levelText(level) { return level === "error" ? t("Chyba") : level === "warning" ? t("Varování") : t("Doporučení"); }271
function renderFindings() {272
const findings = [...(inventory?.findings || [])];273
for (const item of inventory?.firewall?.findings || []) if (!findings.some((f) => f.code === item.code && JSON.stringify(f.params) === JSON.stringify(item.params))) findings.push(item);274
byId("findingCounts").replaceChildren(...["error", "warning", "recommendation"].map((level) => badge(`${levelText(level)}: ${findings.filter((f) => f.level === level).length}`, level === "error" ? "bad" : level === "warning" ? "warn" : "")));275
fill("findingList", findings.map((item) => {276
const [reason, fix] = findingText(item);277
const element = row(reason, item.code, [badge(levelText(item.level), item.level === "error" ? "bad" : item.level === "warning" ? "warn" : "")]);278
element.classList.add("finding"); element.firstChild.append(node("span", "fix", fix)); return element;279
}), t("Bez nalezených problémů."));280
}281
function renderAll() { renderInterfaces(); renderRoutes(); renderFirewall(); renderFindings(); syncControls(); }282
function verdictText(verdict) {283
switch (verdict) {284
case "nothing-listens": return t("Na portu nic nenaslouchá. Spusťte službu nebo ověřte její port.");285
case "only-local": return t("Služba naslouchá jen místně. Změňte její naslouchající adresu, má-li být dostupná ze sítě.");286
case "firewall-blocks": return t("Firewall port blokuje. Navrhněte povolení ze správného zdroje.");287
case "firewall-unknown": return t("Pravidla firewallu nelze spolehlivě vyhodnotit. Zkontrolujte je s oprávněním root.");288
case "open-on-server": return t("Služba naslouchá a server port povoluje. Pokud spojení nefunguje, ověřte cestu k serveru.");289
default: return t("Dostupnost portu se nepodařilo určit.");290
}291
}292
function firewallAnswer(answer) {293
switch (answer) {294
case "no-firewall": return t("Bez firewallu"); case "blocked": return t("Blokováno pravidlem"); case "allowed": return t("Povoleno pravidlem");295
case "default-blocks": return t("Blokováno výchozí zásadou"); case "default-allows": return t("Povoleno výchozí zásadou"); default: return t("Firewall nezjištěn");296
}297
}298
function renderDiagnosis(result) {299
const rows = [];300
const okBadge = (ok) => badge(ok === true ? t("V pořádku") : ok === false ? t("Selhalo") : t("Nezjištěno"), ok === true ? "good" : "warn");301
for (const g of result.gateway || []) rows.push(row(t("Brána IPv{family}", g), `${g.gateway || "—"} · ${g.device || "—"}`, [okBadge(g.ok)]));302
for (const d of result.dns || []) rows.push(row(d.name, t("Odpověď: {answer} · {seconds} s", { answer: Array.isArray(d.answer) ? d.answer.join(", ") : d.answer || "—", seconds: d.seconds ?? "—" }), [okBadge(d.ok)]));303
for (const item of result.internet || []) rows.push(row(t("Internet IPv{family}", item), typeof item.result === "object" ? JSON.stringify(item.result) : String(item.result ?? "—")));304
if (result.port) {305
const p = result.port;306
rows.push(row(t("Port {port}/{protocol}", p), verdictText(p.verdict)));307
rows.push(row(t("Vyhodnocení firewallu"), firewallAnswer(p.firewall)));308
rows.push(row(t("Naslouchání a pravidla"), JSON.stringify({ listeners: p.listeners, rules: p.rules, fromHere: p.fromHere }, null, 2)));309
rows.push(row(t("Cesta z internetu"), t("Ze serveru nelze ověřit celou cestu z internetu: router, NAT, cloudový firewall ani nadřazená ACL.")));310
}311
fill("diagnosticResults", rows);312
}313
function splitList(value) { return value.split(/[\s,]+/).map((s) => s.trim()).filter(Boolean); }314
async function diagnose(portCheck = false) {315
if (!connected() || !inventory) return;316
const own = generation, request = ++diagnosticRequest;317
const port = Number(byId("diagnosticPort").value);318
if (portCheck && (!Number.isInteger(port) || port < 1 || port > 65535)) { setStatus(t("Zadejte port od 1 do 65535."), true); return; }319
diagnosticBusy = true; syncControls(); setStatus(t("Kontroluji spojení…"));320
try {321
const result = await api.network.diagnose({ sessionId, ...auth(), names: splitList(byId("diagnosticNames").value), ...(portCheck ? { port, protocol: byId("diagnosticProtocol").value } : {}) });322
if (own !== generation || request !== diagnosticRequest) return;323
renderDiagnosis(result); setStatus(t("Kontrola dokončena."));324
} catch (error) { if (own === generation && request === diagnosticRequest) setStatus(errorText(error), true); }325
finally { if (own === generation && request === diagnosticRequest) { diagnosticBusy = false; syncControls(); } }326
}328
function invalidatePlan() {329
revision += 1; planRequest += 1; plan = null; planning = false;330
byId("planPanel").hidden = true; byId("applyPlan").disabled = true;331
}332
function interfacePayload() {333
if (!selected) throw new Error(t("Vyberte rozhraní."));334
const data = { interface: selected.name, dns: splitList(byId("interfaceDns").value), search: splitList(byId("interfaceSearch").value), mtu: byId("interfaceMtu").value === "" ? null : Number(byId("interfaceMtu").value) };335
if (data.mtu !== null && (!Number.isInteger(data.mtu) || data.mtu < 68 || data.mtu > 65535)) throw new Error(t("MTU musí být celé číslo od 68 do 65535."));336
for (const family of [4, 6]) {337
const method = byId(`ipv${family}Method`).value;338
if (method === "unknown" || !method) throw new Error(t("Vyberte metodu pro IPv4 i IPv6. Nezjištěnou metodu nelze bezpečně přepsat."));339
const addresses = method === "static" ? splitList(byId(`ipv${family}Addresses`).value) : [];340
if (method === "static" && !addresses.length) throw new Error(t("Pro statickou konfiguraci zadejte alespoň jednu adresu s prefixem."));341
data[`ipv${family}`] = { method, addresses, gateway: method === "static" ? byId(`gateway${family}`).value.trim() : "" };342
}343
return data;344
}345
function rulePayload() {346
const protocol = byId("ruleProtocol").value;347
const ports = splitList(byId("rulePorts").value);348
if (ports.length && !["tcp", "udp"].includes(protocol)) throw new Error(t("Porty lze zadat jen pro TCP nebo UDP."));349
for (const value of ports) {350
const parts = value.split(":").map(Number);351
if (!/^\d+(?::\d+)?$/.test(value) || parts.some((p) => p < 1 || p > 65535) || (parts.length === 2 && parts[0] > parts[1])) throw new Error(t("Zadejte porty 1–65535 nebo rozsah ve tvaru 1000:2000."));352
}353
const family = byId("ruleFamily").value;354
if ((protocol === "icmp" && family === "6") || (protocol === "ipv6-icmp" && family === "4")) throw new Error(t("Protokol ICMP musí odpovídat rodině adres."));355
const zone = byId("ruleZone").value;356
return { backend: editableBackend(), zone: zone || undefined, add: [{ direction: byId("ruleDirection").value, action: byId("ruleAction").value, family, protocol, ports, source: byId("ruleSource").value.trim(), destination: byId("ruleDestination").value.trim(), interface: byId("ruleInterface").value, comment: byId("ruleComment").value.trim(), zone }] };357
}358
function policyPayload() {359
const policy = {};360
for (const [key, id] of [["input", "policyInput"], ["output", "policyOutput"], ["forward", "policyForward"]]) if (byId(id).value) policy[key] = byId(id).value;361
const enable = byId("firewallEnable").value;362
if (enable === "true" && (inventory?.firewall?.backends || []).filter((b) => b.active).length > 1) throw new Error(t("Nejprve vyřešte souběh více firewallů."));363
if (!Object.keys(policy).length && !enable) throw new Error(t("Vyberte změnu zásady nebo stavu firewallu."));364
return { backend: editableBackend(), policy, ...(enable ? { enable: enable === "true" } : {}), zone: byId("ruleZone").value || undefined };365
}366
function profilePayload() {367
const kind = byId("profileKind").value, source = byId("profileSource").value.trim();368
const wanted = { web: [80, 443], proxy: [80, 443], database: [3306, 5432, 6379, 27017, 1433, 1521], router: [53, 67, 68, 546, 547, 179], proxmox: [8006, 3128, 5900, 5901, 5902, 5405, 5406, 5407] }[kind] || [];369
if (kind === "database" && !source) throw new Error(t("Pro databázový profil vyplňte povolený zdroj (adresu nebo síť)."));370
const sshPort = Number(inventory?.ssh?.serverPort);371
if (!Number.isInteger(sshPort) || sshPort < 1 || sshPort > 65535) throw new Error(t("SSH port této relace není známý. Profil nelze bezpečně navrhnout."));372
const add = [], seen = new Set(), zone = byId("ruleZone").value;373
const comment = kind === "web" ? t("Profil: Webserver") : kind === "proxy" ? t("Profil: Reverse proxy") : kind === "database" ? t("Profil: Databázový server") : kind === "router" ? t("Profil: Router") : t("Profil: Proxmox host");374
const append = (port, protocol, allowedSource) => {375
const key = `${port}/${protocol}/${allowedSource}`;376
if (seen.has(key)) return;377
seen.add(key); add.push({ direction: "in", action: "accept", family: "both", protocol, ports: [String(port)], source: allowedSource, destination: "", interface: "", comment, zone });378
};379
append(sshPort, "tcp", inventory?.ssh?.clientAddress || "");380
for (const item of inventory?.listening || []) {381
const port = Number(item.port), protocol = String(item.protocol).startsWith("tcp") ? "tcp" : String(item.protocol).startsWith("udp") ? "udp" : "";382
// Loopback-only databases must not be exposed merely by choosing a profile.383
const local = /^(?:127\.|::1$|\[::1\]$)/.test(String(item.address));384
if (wanted.includes(port) && protocol && !local) append(port, protocol, source);385
}386
return { backend: editableBackend(), zone: zone || undefined, add, remove: [] };387
}388
async function requestPlan(kind, supplied) {389
if (!connected() || !inventory || busy || changing()) return;390
if (kind === "firewall" && (!editableBackend() || inventory.firewall?.readable !== true)) return;391
invalidatePlan();392
const own = generation, startedRevision = revision, request = ++planRequest;393
try {394
const payload = supplied || (kind === "interface" ? interfacePayload() : rulePayload());395
planning = true; syncControls(); setStatus(t("Připravuji návrh…"));396
const result = kind === "interface" ? await api.network.planInterface({ sessionId, ...payload, ...auth() }) : await api.network.planFirewall({ sessionId, add: [], remove: [], ...payload, ...auth() });397
if (own !== generation || startedRevision !== revision || request !== planRequest) return;398
if (!result?.planId) throw new Error(t("Server nevrátil platný návrh."));399
plan = { ...result, sessionId, revision, generation, server: api.sessions.displayName(selectedSession()) };400
showPlan(); setStatus(t("Návrh je připraven. Zkontrolujte rozdíly a rizika."));401
} catch (error) { if (own === generation && startedRevision === revision && request === planRequest) setStatus(errorText(error), true); }402
finally { if (own === generation && request === planRequest) { planning = false; syncControls(); } }403
}404
function requestFirewallPayload(make) {405
try { void requestPlan("firewall", make()); } catch (error) { invalidatePlan(); syncControls(); setStatus(errorText(error), true); }406
}408
// Small LCS, with common edges trimmed and a bounded fallback for very large files.409
function lineDiff(before, after) {410
const a = String(before ?? "").split("\n"), b = String(after ?? "").split("\n");411
let start = 0, endA = a.length, endB = b.length;412
while (start < endA && start < endB && a[start] === b[start]) start += 1;413
while (endA > start && endB > start && a[endA - 1] === b[endB - 1]) { endA -= 1; endB -= 1; }414
const lines = a.slice(Math.max(0, start - 3), start).map((line) => ["", line]);415
const x = a.slice(start, endA), y = b.slice(start, endB);416
if (x.length * y.length > 4_000_000) {417
x.forEach((line) => lines.push(["del", line])); y.forEach((line) => lines.push(["add", line]));418
} else {419
const table = Array.from({ length: x.length + 1 }, () => new Uint32Array(y.length + 1));420
for (let i = x.length - 1; i >= 0; i -= 1) for (let j = y.length - 1; j >= 0; j -= 1) table[i][j] = x[i] === y[j] ? table[i + 1][j + 1] + 1 : Math.max(table[i + 1][j], table[i][j + 1]);421
let i = 0, j = 0;422
while (i < x.length || j < y.length) {423
if (i < x.length && j < y.length && x[i] === y[j]) { lines.push(["", x[i]]); i += 1; j += 1; }424
else if (i < x.length && (j === y.length || table[i + 1][j] >= table[i][j + 1])) { lines.push(["del", x[i]]); i += 1; }425
else { lines.push(["add", y[j]]); j += 1; }426
}427
}428
return [...lines, ...a.slice(endA, endA + 3).map((line) => ["", line])];429
}430
function riskText(risk) {431
const p = { ...(risk.params || {}), ...risk };432
switch (risk.code) {433
case "ssh-interface": return t("Měníte rozhraní aktuálního SSH spojení. Spojení může vypadnout.");434
case "ssh-address-changes": return t("SSH adresa se změní z {from} na {to}. Nové spojení ověříme na nové adrese.", p);435
case "ssh-address-removed": return t("SSH adresa {from} bude odebrána. Připravte si jinou cestu ke správě.", p);436
case "bridge-carries-guests": return t("Most přenáší síť virtuálních strojů nebo kontejnerů. Změna může odpojit i je.");437
case "ipv6-depends-on-router": return t("IPv6 závisí na oznámeních směrovače. DHCPv6 samo výchozí bránu nezajistí.");438
case "several-firewalls": return t("Běží více firewallů. Jejich pravidla se mohou ovlivňovat.");439
case "rule-cuts-ssh": return t("Nové pravidlo může přerušit SSH na portu {port}.", p);440
case "removes-ssh-rule": return t("Odstraňujete pravidlo pro SSH na portu {port}.", p);441
case "policy-cuts-ssh": return t("Výchozí zásada může zablokovat SSH na portu {port}.", p);442
case "nft-file-rewritten": return t("Soubor {path} bude přepsán. Zkontrolujte celý rozdíl i vlastní pravidla.", p);443
case "default-route-options-lost": return t("Výchozí trasa má vlastní volby (metrika, tabulka). Tento návrh je nezachová.");444
case "runtime-only": return t("Změna platí jen za běhu. Po restartu se použije trvalá konfigurace.");445
default: return t("Neznámé riziko {code}. Před použitím ověřte jeho význam.", { code: risk.code });446
}447
}448
function showPlan() {449
byId("planPanel").hidden = false;450
byId("planMeta").textContent = t("Server: {server} · síť: {manager} · firewall: {firewall}", { server: plan.server, manager: plan.manager || "—", firewall: plan.firewall || "—" });451
byId("planFiles").replaceChildren(...(plan.files || []).map((file) => {452
const block = node("section", "sub-block"); block.append(node("h3", "", file.path));453
const diff = node("pre", "code diff"), lines = lineDiff(file.before, file.after);454
if (!lines.some(([kind]) => kind)) diff.textContent = t("Bez rozdílů.");455
else diff.append(...lines.map(([kind, line]) => node("span", kind, `${kind === "add" ? "+" : kind === "del" ? "-" : " "} ${line}`)));456
block.append(diff); return block;457
}));458
byId("planCommands").textContent = (plan.commands || []).map((c) => typeof c === "string" ? c : JSON.stringify(c)).join("\n") || t("Žádné příkazy.");459
byId("planRisks").replaceChildren(...(plan.risks || []).map((risk, index) => {460
const label = node("label", "check");461
const check = node("input"); check.type = "checkbox"; check.required = true; check.dataset.riskCode = risk.code; check.id = `risk-${index}`;462
check.addEventListener("change", syncControls); label.append(check, node("span", "", riskText(risk))); return label;463
}));464
byId("planSeconds").value = Math.min(3600, Math.max(30, Number(plan.defaultSeconds) || 120));465
syncControls();466
byId("planPanel").scrollIntoView({ block: "nearest" });467
}468
function riskChecks() { return [...byId("planRisks").querySelectorAll("input[type=checkbox]")]; }469
function canApply() {470
const seconds = Number(byId("planSeconds").value);471
return Boolean(plan && plan.generation === generation && plan.revision === revision && plan.sessionId === sessionId && connected() && !busy && !planning && !changing() && Number.isInteger(seconds) && seconds >= 30 && seconds <= 3600 && riskChecks().length === (plan.risks || []).length && riskChecks().every((check) => check.checked));472
}474
function plainAddress(value) { return String(value || "").trim().split("/")[0].replace(/^\[|\]$/g, ""); }475
async function applyPlan() {476
if (!canApply()) return;477
const current = plan;478
const addressRisk = (current.risks || []).find((r) => r.code === "ssh-address-changes");479
const address = plainAddress(addressRisk?.to || addressRisk?.params?.to);480
const tx = {481
id: null, sessionId: current.sessionId, server: current.server, state: "applying", verified: false,482
address, oldAddress: inventory?.ssh?.serverAddress || "", deadline: null, operation: 0, action: true,483
message: t("Používám změnu. Záznam čekající změny se ukládá na serveru."), checking: false484
};485
const payload = { planId: current.planId, confirm: true, acceptRisks: [...new Set(riskChecks().map((check) => check.dataset.riskCode))], seconds: Number(byId("planSeconds").value), ...auth(), ...(address ? { expectAddress: address } : {}) };486
takeSecret("verifyPassword"); takeSecret("verifyPassphrase");487
byId("transactionLog").textContent = ""; byId("transactionLog").hidden = true;488
transaction = tx; invalidatePlan(); renderTransaction(); syncControls();489
try {490
const result = await api.network.apply(payload);491
if (!result?.id) throw new Error(t("Server nevrátil ID změny. Znovu načtěte čekající změnu."));492
tx.id = result.id; tx.state = "pending"; tx.deadline = Date.now() + (Number(result.seconds) || payload.seconds) * 1000;493
tx.message = t("Ověřuji nové SSH spojení…");494
} catch (error) {495
tx.message = t("Výsledek použití není potvrzen: {message}", { message: errorText(error) });496
// A dropped SSH session can lose the reply after the server has already armed rollback.497
// Query pending once, only if the original session is still live. Never guess another server.498
if (connected(tx.sessionId)) {499
try {500
const pending = await api.network.pending({ sessionId: tx.sessionId });501
if (pending?.id) { tx.id = pending.id; tx.state = pending.state || "pending"; }502
} catch { /* The automatic server rollback remains independent of this client. */ }503
}504
if (!tx.id) { tx.state = "unknown"; tx.message += ` ${t("Obnovte SSH relaci a načtěte čekající změnu. Server používá vlastní časovač návratu.")}`; }505
} finally {506
tx.action = false;507
if (!tx.id) tx.untracked = true;508
renderTransaction(); syncControls();509
if (tx.id && !terminal(tx)) void verifyTransaction();510
}511
}512
function startTransaction(details) {513
transaction = { ...details, verified: false, checking: false, action: false, operation: 0, address: details.address || "", oldAddress: inventory?.ssh?.serverAddress || "", deadline: null, message: t("Obnovuji sledování čekající změny. Zbývající čas určuje server.") };514
byId("transactionLog").textContent = ""; byId("transactionLog").hidden = true;515
renderTransaction(); syncControls();516
if (terminal()) void finishTransaction(transaction, transaction.state);517
else void verifyTransaction();518
}519
function transactionActive(tx = transaction) { return Boolean(tx && !terminal(tx) && !tx.untracked); }520
function renderTransaction() {521
const tx = transaction;522
byId("transactionPanel").hidden = !tx;523
if (!tx) return;524
byId("transactionIdentity").textContent = t("Server: {server} · změna: {id}", { server: tx.server || "—", id: tx.id || "—" });525
notice("transactionState", tx.message || stateText(tx.state));526
const remaining = tx.deadline ? Math.max(0, Math.ceil((tx.deadline - Date.now()) / 1000)) : null;527
byId("countdown").textContent = terminal(tx) ? "" : remaining === null ? t("Časový limit spravuje server.") : t("Zbývá {seconds} s", { seconds: remaining });528
byId("rollbackAdvice").hidden = terminal(tx);529
byId("confirmChange").hidden = !(tx.verified === true && transactionActive(tx) && (remaining === null || remaining > 0));530
byId("confirmChange").disabled = tx.action || tx.checking;531
byId("rollbackChange").hidden = terminal(tx);532
byId("extendChange").hidden = terminal(tx);533
for (const id of ["rollbackChange", "extendChange"]) byId(id).disabled = !tx.id || tx.action || !transactionActive(tx);534
byId("verifyNow").disabled = !tx.id || tx.action || tx.checking;535
byId("showTransactionLog").disabled = !tx.id;536
byId("loginFields").hidden = !tx.needsAuth || terminal(tx);537
notice("reconnectAdvice", tx.state === "rolled-back" ? t("Server obnovil původní stav. Připojte se znovu na původní adresu {address} a načtěte údaje. Záznam změny je níže.", { address: tx.oldAddress || t("z původního SSH profilu") }) : remaining === 0 && !terminal(tx) ? t("Čas na potvrzení vypršel. Server automaticky obnovuje původní stav; čekáme na ověření výsledku. Připojte se na původní adresu {address} a zkontrolujte záznam.", { address: tx.oldAddress || t("z původního SSH profilu") }) : "");538
}539
function needLogin(result, tx, preserveVerification = false) {540
const message = String(result?.message || result?.error || "");541
if (result?.code === "PASSWORD_REQUIRED" || /PASSWORD_REQUIRED|passphrase|password required|requires.*password/i.test(message)) {542
tx.needsAuth = true; if (!preserveVerification) tx.verified = false;543
tx.message = /passphrase/i.test(message) ? t("Pro nové SSH spojení zadejte heslo k SSH klíči.") : t("Pro nové SSH spojení zadejte přihlašovací heslo.");544
return true;545
}546
return false;547
}548
async function verifyTransaction(manual = false) {549
const tx = transaction;550
if (!transactionActive(tx) || !tx.id || tx.checking || tx.action || (tx.verified && !manual)) return;551
const operation = tx.operation;552
tx.checking = true; renderTransaction();553
try {554
const result = await api.network.verify({ id: tx.id, address: tx.address || undefined, ...(manual ? credentials() : {}) });555
if (transaction !== tx || operation !== tx.operation || terminal(tx)) return;556
if (result.state === "rolled-back" || result.state === "confirmed") { await finishTransaction(tx, result.state); return; }557
if (result.state) tx.state = result.state;558
if (needLogin(result, tx)) return;559
tx.verified = result.verified === true && (!tx.deadline || tx.deadline > Date.now());560
if (tx.verified) {561
tx.needsAuth = false; tx.message = t("Nové SSH spojení je ověřené. Nyní můžete změnu potvrdit.");562
} else tx.message = result.error || result.message ? t("Spojení zatím není ověřené: {message}", { message: result.message || result.error }) : result.reachable ? t("Server odpovídá, nové SSH spojení zatím není ověřené.") : t("Server zatím není dostupný. Ověření se opakuje každé 3 sekundy.");563
} catch (error) {564
if (transaction !== tx || operation !== tx.operation || terminal(tx)) return;565
tx.verified = false;566
if (!needLogin({ message: errorText(error) }, tx)) tx.message = t("Ověření spojení zatím selhává: {message}", { message: errorText(error) });567
} finally { if (transaction === tx && operation === tx.operation) { tx.checking = false; renderTransaction(); } }568
}569
async function finishTransaction(tx, state) {570
if (transaction !== tx) return;571
tx.state = state; tx.verified = false; tx.needsAuth = false; tx.operation += 1; tx.checking = false;572
takeSecret("verifyPassword"); takeSecret("verifyPassphrase");573
tx.message = state === "confirmed" ? t("Změna je potvrzená. Automatický návrat byl zrušen.") : t("Server obnovil předchozí konfiguraci.");574
renderTransaction(); syncControls();575
await loadTransactionLog(tx);576
if (state === "confirmed" && transaction === tx && sessionId === tx.sessionId && connected()) await inspect();577
}578
async function confirmChange() {579
const tx = transaction;580
if (!tx || tx.verified !== true || !tx.id || !transactionActive(tx) || tx.action || tx.checking || (tx.deadline && tx.deadline <= Date.now())) return;581
tx.action = true; tx.operation += 1; renderTransaction();582
try {583
const result = await api.network.confirm({ id: tx.id, ...credentials() });584
if (transaction !== tx) return;585
if (needLogin(result, tx, true)) return;586
if (result?.state === "rolled-back") { await finishTransaction(tx, "rolled-back"); return; }587
if (result?.error || (result?.state && result.state !== "confirmed")) {588
tx.verified = false; tx.message = result.message || result.error || stateText(result.state); return;589
}590
await finishTransaction(tx, "confirmed");591
} catch (error) {592
if (transaction !== tx) return;593
if (!needLogin({ message: errorText(error) }, tx, true)) {594
tx.verified = false; tx.message = t("Potvrzení se nezdařilo: {message}", { message: errorText(error) });595
}596
} finally { tx.action = false; renderTransaction(); syncControls(); }597
}598
async function rollbackChange() {599
const tx = transaction;600
if (!transactionActive(tx) || !tx.id || tx.action) return;601
tx.action = true; tx.operation += 1; tx.checking = false; tx.verified = false; renderTransaction();602
try {603
const result = await api.network.rollback({ id: tx.id });604
if (transaction !== tx) return;605
if (result?.error) throw new Error(result.message || result.error);606
if (!result?.state || result.state === "rolled-back" || result.state === "confirmed") await finishTransaction(tx, result?.state || "rolled-back");607
else { tx.state = result.state; tx.message = stateText(result.state); }608
} catch (error) { if (transaction === tx) tx.message = t("Vrácení zatím není potvrzené: {message}. Automatický časovač na serveru zůstává v platnosti.", { message: errorText(error) }); }609
finally { tx.action = false; renderTransaction(); syncControls(); }610
}611
async function extendChange() {612
const tx = transaction;613
if (!transactionActive(tx) || !tx.id || tx.action) return;614
tx.action = true; tx.operation += 1; tx.checking = false; renderTransaction();615
try {616
const result = await api.network.extend({ id: tx.id, seconds: 120 });617
if (transaction !== tx) return;618
if (result?.error) throw new Error(result.message || result.error);619
if (result?.state === "rolled-back" || result?.state === "confirmed") { await finishTransaction(tx, result.state); return; }620
// A resumed pending transaction has no remaining-time field in the API.621
// In that case keep the server-owned deadline unknown rather than invent one.622
if (Number(result?.seconds) > 0) tx.deadline = Date.now() + Number(result.seconds) * 1000;623
else if (tx.deadline) tx.deadline = Math.max(Date.now(), tx.deadline) + 120000;624
tx.expired = false; tx.message = t("Čas na potvrzení byl prodloužen o 2 minuty.");625
} catch (error) { if (transaction === tx) tx.message = t("Prodloužení se nezdařilo: {message}", { message: errorText(error) }); }626
finally { tx.action = false; renderTransaction(); }627
}628
async function loadTransactionLog(tx = transaction) {629
if (!tx?.id) return;630
const id = tx.id;631
try {632
const result = await api.network.transactionLog({ id });633
if (transaction !== tx || tx.id !== id) return;634
byId("transactionLog").textContent = typeof result === "string" ? result : JSON.stringify(result, null, 2);635
} catch (error) { if (transaction === tx) byId("transactionLog").textContent = t("Záznam nelze načíst: {message}", { message: errorText(error) }); }636
if (transaction === tx) byId("transactionLog").hidden = false;637
}638
function tickTransaction() {639
const tx = transaction;640
if (!tx) return;641
if (transactionActive(tx) && tx.deadline && tx.deadline <= Date.now() && !tx.expired) {642
tx.expired = true; tx.verified = false; tx.needsAuth = false;643
void loadTransactionLog(tx); void verifyTransaction();644
}645
renderTransaction();646
}648
function switchTab(name, focus = false) {649
for (const tab of document.querySelectorAll(".tab")) {650
const active = tab.dataset.tab === name;651
tab.classList.toggle("active", active); tab.setAttribute("aria-selected", String(active)); tab.tabIndex = active ? 0 : -1;652
if (active && focus) tab.focus();653
}654
for (const panel of document.querySelectorAll(".tab-panel")) panel.hidden = panel.dataset.panel !== name;655
}656
function applyLabels() {657
if (i18n) { i18n.apply(document); return; }658
for (const element of document.querySelectorAll("[data-i18n]")) element.textContent = t(element.dataset.i18n);659
for (const element of document.querySelectorAll("[data-i18n-placeholder]")) element.setAttribute("placeholder", t(element.dataset.i18nPlaceholder));660
}661
function bindEvents() {662
byId("showPlumbing").addEventListener("change", renderFirewall);663
byId("closeButton").addEventListener("click", () => api?.close?.());664
byId("sessionSelect").addEventListener("change", () => { endedSessionId = ""; resetSession(byId("sessionSelect").value); if (sessionId) void inspect(); });665
byId("refreshButton").addEventListener("click", () => void loadSessions());666
byId("loadButton").addEventListener("click", () => void inspect());667
const tabs = [...document.querySelectorAll(".tab")];668
tabs.forEach((tab, index) => {669
tab.addEventListener("click", () => switchTab(tab.dataset.tab));670
tab.addEventListener("keydown", (event) => {671
const next = event.key === "ArrowRight" ? (index + 1) % tabs.length : event.key === "ArrowLeft" ? (index + tabs.length - 1) % tabs.length : event.key === "Home" ? 0 : event.key === "End" ? tabs.length - 1 : -1;672
if (next >= 0) { event.preventDefault(); switchTab(tabs[next].dataset.tab, true); }673
});674
});675
for (const id of ["interfaceForm", "firewallForm"]) {676
byId(id).addEventListener("submit", (event) => event.preventDefault());677
for (const type of ["input", "change"]) byId(id).addEventListener(type, () => { invalidatePlan(); syncMethods(); syncControls(); });678
}679
for (const id of ["profileKind", "profileSource", "firewallBackend"]) {680
for (const type of ["input", "change"]) byId(id).addEventListener(type, () => { invalidatePlan(); syncControls(); });681
}682
byId("planInterface").addEventListener("click", () => void requestPlan("interface"));683
byId("planRule").addEventListener("click", () => void requestPlan("firewall"));684
byId("planPolicy").addEventListener("click", () => requestFirewallPayload(policyPayload));685
byId("planProfile").addEventListener("click", () => requestFirewallPayload(profilePayload));686
byId("cancelPlan").addEventListener("click", () => { invalidatePlan(); syncControls(); });687
byId("planSeconds").addEventListener("input", syncControls);688
byId("applyPlan").addEventListener("click", () => void applyPlan());689
byId("verifyNow").addEventListener("click", () => void verifyTransaction(true));690
byId("confirmChange").addEventListener("click", () => void confirmChange());691
byId("rollbackChange").addEventListener("click", () => void rollbackChange());692
byId("extendChange").addEventListener("click", () => void extendChange());693
byId("showTransactionLog").addEventListener("click", () => void loadTransactionLog());694
byId("diagnosticForm").addEventListener("submit", (event) => event.preventDefault());695
for (const type of ["input", "change"]) byId("diagnosticForm").addEventListener(type, () => { diagnosticRequest += 1; diagnosticBusy = false; byId("diagnosticResults").replaceChildren(); syncControls(); });696
byId("runDiagnostics").addEventListener("click", () => void diagnose());697
byId("checkPort").addEventListener("click", () => void diagnose(true));698
}699
async function init() {700
bindEvents(); applyLabels(); renderAll();701
if (!api) { setStatus(t("Rozhraní CH-J není dostupné. Otevřete modul v aplikaci."), true); return; }702
try { if (i18n && api.ui?.getLanguage) { i18n.setLanguage(await api.ui.getLanguage()); applyLabels(); renderAll(); } } catch { /* Czech is always available. */ }703
api.ui?.onLanguageChanged?.((language) => { i18n?.setLanguage(language); applyLabels(); renderAll(); if (plan) showPlan(); renderTransaction(); });704
try { const info = await api.getInfo?.(); byId("pluginVersion").textContent = info ? `${info.name} v${info.version} · ${info.publisher}` : t("Správa sítě a firewallu"); } catch { byId("pluginVersion").textContent = t("Správa sítě a firewallu"); }705
await loadSessions();706
setInterval(() => { if (!document.hidden) void loadSessions(); }, 3000);707
setInterval(() => { void verifyTransaction(); }, 3000);708
setInterval(tickTransaction, 1000);709
}710
void init().catch((error) => setStatus(errorText(error), true));SHA-256
3de2e8997a59bec2ade19e8b22b8427d547bbe76e666fec6fca34bb4e0e1c02aCH-J Proprietary Software License 1.14 · Zdroje jsou zveřejněny pod CH-J Proprietary Software License 1.14. Jejich dostupnost nemění licenční podmínky ani neposkytuje další oprávnění.
SHA-256 archivu
ac8fe790f4ba60fc098f8c985fc2656cfcc6cdc21fb34b30e308a2b54e62258f