"use strict"; const api = window.chjPlugin; const i18n = window.chjPluginI18n.createI18n("cs"); const t = i18n.t; let endedSessionId = ""; let sessionsLoading = false; // Guard every continuation, including sudo retries and dialog answers, against a server switch. async function sessionResult(promise, token) { const value = await promise; if (token !== state.generation) throw new Error("CHJ_SESSION_CHANGED"); return value; } // Marks a catalog key that is translated with t() only when it is shown. const k = (text) => text; const el = new Proxy({}, { get: (_target, id) => document.getElementById(id) }); const HISTORY = 60; const state = { generation: 0, busy: false, previousNetwork: new Map(), previousPaging: null, ramHistory: [], swapHistory: [], metrics: null, vm: null, vmBusy: false }; // ---------- CPU packages and GPUs: a missing value stays "unavailable", never 0 ---------- function node(tag, className, text) { const element = document.createElement(tag); if (className) element.className = className; if (text !== undefined) element.textContent = text; return element; } function usageRow(label, value, valueText) { const row = node("div", "usage-row"); const head = node("div", "usage-head"); head.append(node("span", "", label), node("b", "", valueText)); const bar = node("div", "bar"), fill = node("i"); fill.style.width = `${known(value) ? Math.min(100, Number(value)) : 0}%`; bar.append(fill); row.append(head, bar); return row; } const unit = (value, suffix) => known(value) ? `${Number(value).toLocaleString(i18n.locale(), { maximumFractionDigits: 1 })} ${suffix}` : null; function renderCpuPackages(packages) { const list = Array.isArray(packages) ? packages : []; el.cpuPackages.replaceChildren(...list.map((cpu, index) => { const card = node("div", "package"); const facts = [t("{logical} logických · {physical} fyzických", { logical: known(cpu.logicalCores) ? cpu.logicalCores : "?", physical: known(cpu.physicalCores) ? cpu.physicalCores : "?" }), unit(cpu.frequencyMhz, "MHz"), unit(cpu.temperatureC, "°C")].filter(Boolean).join(" · "); card.append(usageRow(`${t("Procesor {id}", { id: index + 1 })}${cpu.model ? ` — ${cpu.model}` : ""}`, cpu.usagePercent, pctText(cpu.usagePercent)), node("small", "", facts)); return card; })); } function gpuHint(gpu) { if (gpu.driver === "vfio-pci") return t("Zařízení je předané do virtuálního stroje; metriky jsou dostupné jen uvnitř něj."); if (gpu.source !== "none") return known(gpu.usagePercent) ? "" : t("Tento ovladač využití GPU nehlásí."); if (gpu.driver === "nvidia") return t("Nástroj nvidia-smi není dostupný, proto jsou vidět jen základní údaje."); return t("Pro toto zařízení nejsou metriky dostupné."); } function renderGpus(gpus) { const list = Array.isArray(gpus) ? gpus : []; el.gpuSummary.textContent = String(list.length); if (!list.length) { el.gpuList.replaceChildren(node("p", "empty", t("Server nehlásí žádnou GPU."))); return; } el.gpuList.replaceChildren(...list.map((gpu) => { const card = node("article", "gpu"); const ids = [gpu.vendorId, gpu.deviceId].filter(Boolean).join(":"); card.append(node("strong", "model", gpu.name || (ids ? `PCI ${ids}` : gpu.id))); card.append(usageRow(t("Využití GPU"), gpu.usagePercent, pctText(gpu.usagePercent))); const memory = pct(gpu.memoryUsed, gpu.memoryTotal); card.append(usageRow(t("Video paměť"), memory, known(gpu.memoryUsed) && known(gpu.memoryTotal) ? `${bytes(gpu.memoryUsed)} / ${bytes(gpu.memoryTotal)}` : known(gpu.memoryUsed) ? bytes(gpu.memoryUsed) : known(gpu.memoryTotal) ? bytes(gpu.memoryTotal) : t("nedostupné"))); const facts = node("dl", "facts compact"); const power = known(gpu.powerW) ? `${unit(gpu.powerW, "W")}${known(gpu.powerLimitW) ? ` / ${unit(gpu.powerLimitW, "W")}` : ""}` : null; for (const [label, value] of [[t("Teplota"), unit(gpu.temperatureC, "°C")], [t("Příkon"), power], [t("Takt"), unit(gpu.clockMhz, "MHz")], [t("Ventilátor"), unit(gpu.fanPercent, "%")], [t("Ovladač"), [gpu.driver, gpu.driverVersion].filter(Boolean).join(" ") || null], [t("Adresa"), [gpu.id, ids ? `(${ids})` : ""].filter(Boolean).join(" ")]]) { if (!value) continue; const row = node("div"); row.append(node("dt", "", label), node("dd", "", value)); facts.append(row); } card.append(facts); const hint = gpuHint(gpu); if (hint) card.append(node("small", "gpu-hint", hint)); return card; })); } // ---------- formatting (server data is always rendered as text) ---------- const known = (value) => value !== null && value !== undefined && Number.isFinite(Number(value)); function bytes(value) { if (!known(value)) return t("nedostupné"); const units = ["B", "KiB", "MiB", "GiB", "TiB"]; let amount = Number(value); let unit = 0; while (Math.abs(amount) >= 1024 && unit < units.length - 1) { amount /= 1024; unit += 1; } return `${amount.toLocaleString(i18n.locale(), { maximumFractionDigits: unit >= 3 ? 2 : 1 })} ${units[unit]}`; } const pct = (used, total) => known(used) && known(total) && total > 0 ? Math.max(0, Math.min(100, used / total * 100)) : null; const pctText = (value) => known(value) ? `${Number(value).toLocaleString(i18n.locale(), { maximumFractionDigits: 1 })} %` : t("nedostupné"); function duration(seconds) { if (!known(seconds)) return t("nedostupné"); let remaining = Math.max(0, Number(seconds)); const days = Math.floor(remaining / 86400); remaining %= 86400; return `${days ? `${days} d ` : ""}${Math.floor(remaining / 3600)} h ${Math.floor((remaining % 3600) / 60)} min`; } function setStatus(message, error = false) { el.status.textContent = message; el.status.classList.toggle("error", error); } function h(tag, props = {}, children = []) { const node = document.createElement(tag); for (const [key, value] of Object.entries(props)) { if (value === undefined || value === null || value === false) continue; if (key === "className") node.className = value; else if (key === "text") node.textContent = String(value); else if (key === "style") Object.assign(node.style, value); else if (key.startsWith("on") && typeof value === "function") node.addEventListener(key.slice(2), value); else if (typeof value === "boolean") node[key] = value; else node.setAttribute(key, String(value)); } for (const child of [].concat(children)) if (child !== null && child !== undefined && child !== false) node.append(child instanceof Node ? child : String(child)); return node; } function parseError(error) { const raw = String(error?.message || error || ""); const match = raw.match(/\b([A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+): ([\s\S]*)$/); return match ? { code: match[1], detail: match[2].trim() } : { code: "UNKNOWN", detail: raw.replace(/^Error invoking remote method '[^']+': (?:\w*Error: )?/, "") }; } const ERROR_TEXT = { CHJ_CANCELLED: k("Operace byla zrušena."), SUDO_NOT_PERMITTED: k("Účet SSH relace nemá oprávnění sudo; zadání hesla to nezmění."), SUDO_PASSWORD_INCORRECT: k("Heslo pro sudo bylo odmítnuto."), SUDO_TTY_REQUIRED: k("Politika sudo vyžaduje terminál (requiretty); CH-J ji neobchází."), SESSION_NOT_CONNECTED: k("SSH relace byla odpojena; stav operace na serveru ověřte po novém připojení."), REMOTE_COMMAND_TIMEOUT: k("Server neodpověděl včas."), VM_BUSY: k("Na této relaci právě probíhá jiná změna virtuální paměti."), VM_SWAP_BUSY: k("Na serveru právě probíhá jiná změna swapu."), VM_SWAPOFF_UNSAFE: k("Deaktivace by mohla vyčerpat paměť, operace byla odmítnuta:"), VM_SWAP_DISK_FULL: k("Nedostatek místa na disku:"), VM_SWAP_FS_UNSUPPORTED: k("Souborový systém nepodporuje bezpečný swapfile:"), VM_SWAP_SIZE_INVALID: k("Neplatná velikost (64 MiB – 64 GiB, celé MiB)."), VM_SWAP_PATH_INVALID: k("Neplatná cesta swapfile:"), VM_SWAP_MANUAL_ONLY: k("Tuto operaci je nutné provést ručně:"), VM_SWAP_EXISTS: k("Soubor již existuje."), VM_SWAP_PRIORITY_INVALID: k("Neplatná priorita: zvolte automatickou nebo celé číslo 0–32767."), VM_FSTAB_AMBIGUOUS: k("V /etc/fstab je pro tento swap více záznamů; nejprve je ručně sjednoťte:"), VM_FSTAB_BACKUP_FAILED: k("Zálohu /etc/fstab nelze vytvořit, nic nebylo změněno:"), VM_FSTAB_WRITE_FAILED: k("/etc/fstab nelze zapsat, původní obsah zůstal:"), VM_SWAPON_FAILED: k("Aktivace swapu selhala:"), VM_PRIVILEGE_REQUIRED: k("Změna vyžaduje oprávnění root/sudo."), VM_WINDOWS_ELEVATION_REQUIRED: k("Změna pagefile vyžaduje relaci správce se zvýšeným tokenem (CH-J neobchází UAC)."), VM_PAGEFILE_LAST: k("Jde o poslední pagefile – vyžaduje výslovné potvrzení."), VM_PAGEFILE_CRASHDUMP: k("Systémový pagefile slouží pro výpis paměti při pádu – vyžaduje výslovné potvrzení."), VM_PAGEFILE_SIZE_INVALID: k("Neplatná velikost pagefile:"), VM_PAGEFILE_DISK_FULL: k("Nedostatek místa na svazku:"), VM_PAGEFILE_AUTOMATIC: k("Nejprve přepněte na ruční správu."), METRICS_PLATFORM_UNSUPPORTED: k("Operační systém serveru nelze rozpoznat:") }; function describe(error) { const { code, detail } = parseError(error); const base = ERROR_TEXT[code] && t(ERROR_TEXT[code]); return base ? (base.endsWith(":") ? `${base} ${detail}` : base) : detail || code; } // ---------- charts ---------- function drawChart(canvas, values, color) { const context = canvas.getContext("2d"); const ratio = window.devicePixelRatio || 1; const width = canvas.clientWidth || 560; const height = canvas.clientHeight || 120; canvas.width = width * ratio; canvas.height = height * ratio; context.setTransform(ratio, 0, 0, ratio, 0, 0); context.clearRect(0, 0, width, height); context.strokeStyle = "rgba(151,176,205,.15)"; context.lineWidth = 1; for (const level of [25, 50, 75]) { const y = height - (level / 100) * height; context.beginPath(); context.moveTo(0, y); context.lineTo(width, y); context.stroke(); } context.strokeStyle = color; context.lineWidth = 2; context.beginPath(); let started = false; values.forEach((value, index) => { if (!known(value)) { started = false; return; } const x = index * (width / (HISTORY - 1)); const y = height - (value / 100) * (height - 4) - 2; if (!started) { context.moveTo(x, y); started = true; } else context.lineTo(x, y); }); context.stroke(); context.fillStyle = "#8ea5bf"; context.font = "10px sans-serif"; const last = values.at(-1); context.fillText(known(last) ? `${last.toFixed(1)} %` : "—", 6, 13); } function pushHistory(list, value) { list.push(known(value) ? value : null); while (list.length > HISTORY) list.shift(); } // ---------- sessions and metrics ---------- async function loadSessions() { if (sessionsLoading) return; sessionsLoading = true; let sessions = []; try { sessions = (await api.sessions.list()).filter((session) => session.state === "connected"); } catch (error) { sessionsLoading = false; setStatus(t("Relace nelze načíst: {error}", { error: describe(error) }), true); return; } sessionsLoading = false; const selected = el.sessionSelect.value; const next = api.sessions.reconcile(endedSessionId || selected, sessions); if (next.reason === "gone") endedSessionId ||= selected; el.sessionSelect.replaceChildren(new Option(sessions.length ? t("Vyberte relaci…") : t("Žádná připojená relace"), "")); for (const session of sessions) el.sessionSelect.add(new Option(api.sessions.displayName(session), session.sessionId)); el.sessionSelect.value = next.id; if (el.sessionSelect.value !== selected) resetSession(); if (next.reason === "choose") api.sessions.offerChoice(el.sessionSelect, sessions, t("Vyberte relaci…")); el.refreshMetricsButton.disabled = !el.sessionSelect.value; if (next.reason === "gone") setStatus(t("Relace skončila. Vyberte server."), true); else if (!sessions.length) setStatus(t("Nejprve se v Core připojte k serveru přes SSH.")); } function resetSession() { state.generation += 1; Object.assign(state, { previousNetwork: new Map(), previousPaging: null, ramHistory: [], swapHistory: [], metrics: null, vm: null }); state.busy = false; state.vmBusy = false; for (const dialog of document.querySelectorAll("dialog[open]")) dialog.close(); el.sudoPassword.value = ""; el.vmContent.replaceChildren(); el.vmStatus.textContent = ""; for (const id of ["hostname", "platform", "kernel", "uptime", "cpu", "cpuDetail", "cpuArchitecture", "cpuModel", "cpuTopology", "load", "loadDetail", "memory", "memoryDetail", "swap", "swapDetail", "disk", "diskDetail", "networkSummary", "gpuSummary", "memorySummary", "ramExplain", "swapNote", "vmPrivilege"]) el[id].textContent = "—"; for (const id of ["cpuPackages", "gpuList", "cpuTypes", "networkInterfaces", "ramStack", "ramLegend", "ramFacts", "swapFacts", "swapHead", "swapRows"]) el[id].replaceChildren(); for (const id of ["cpuBar", "memoryBar", "swapBar", "diskBar"]) el[id].style.width = "0%"; drawChart(el.ramChart, [], "#6ad"); drawChart(el.swapChart, [], "#6ad"); el.manageVmButton.disabled = true; } async function loadMetrics() { const requestGeneration = state.generation; const sessionId = el.sessionSelect.value; if (!sessionId || state.busy) return; const token = state.generation; state.busy = true; el.refreshMetricsButton.disabled = true; try { const metrics = await sessionResult(api.system.readMetrics(sessionId), requestGeneration); if (token !== state.generation) return; state.metrics = metrics; renderMetrics(metrics); setStatus(`${t("Aktualizováno {time}", { time: new Date(metrics.collectedAt).toLocaleTimeString(i18n.locale()) })}${metrics.unavailable?.length ? ` · ${t("nedostupné zdroje: {count}", { count: metrics.unavailable.length })}` : ""}.`); } catch (error) { if (requestGeneration !== state.generation) return; if (token === state.generation) setStatus(describe(error), true); if (parseError(error).code === "SESSION_NOT_CONNECTED") void loadSessions(); } finally { if (requestGeneration === state.generation) { state.busy = false; el.refreshMetricsButton.disabled = !el.sessionSelect.value; } } } // Each card renders independently: one missing metric never breaks the others. function safely(render) { try { render(); } catch (error) { console.error("Metric render failed", error); } } function renderMetrics(metrics) { const windows = metrics.os === "windows"; safely(() => { el.hostname.textContent = metrics.hostname || t("nedostupné"); el.platform.textContent = windows ? `${metrics.osName || "Windows"}${metrics.installationType ? ` (${metrics.installationType})` : ""}` : metrics.platform || t("nedostupné"); el.kernelLabel.textContent = windows ? t("Sestavení") : t("Jádro"); el.kernel.textContent = metrics.kernel || t("nedostupné"); el.uptime.textContent = duration(metrics.uptimeSeconds); }); safely(() => { const cpu = metrics.cpu || {}; const usage = known(cpu.usagePercent) ? Number(cpu.usagePercent) : null; el.cpu.textContent = usage !== null ? pctText(usage) : cpu.usagePending ? t("měřím…") : t("nedostupné"); el.cpuBar.style.width = `${usage ?? 0}%`; el.cpuDetail.textContent = known(cpu.logicalCores) ? t("{count} logických procesorů", { count: cpu.logicalCores }) : t("počet procesorů nedostupný"); el.cpuArchitecture.textContent = cpu.architecture || t("neznámá architektura"); el.cpuModel.textContent = cpu.model || t("Údaj o modelu CPU není dostupný."); el.cpuTopology.textContent = t("{logical} logických · {physical} fyzických · {sockets} socketů", { logical: known(cpu.logicalCores) ? cpu.logicalCores : "?", physical: known(cpu.physicalCores) ? cpu.physicalCores : t("nedostupné"), sockets: known(cpu.sockets) ? cpu.sockets : t("nedostupné") }); renderCpuPackages(cpu.packages); renderGpus(metrics.gpus); el.cpuTypes.textContent = Array.isArray(cpu.coreTypes) && cpu.coreTypes.length ? cpu.coreTypes.map((type) => `${type.name} ${type.count}`).join(" · ") : t("Server nerozlišuje typy jader."); }); safely(() => { if (metrics.loadAverageSupported === false) { el.loadLabel.textContent = t("Zatížení (load average)"); el.load.textContent = t("nedostupné"); el.loadDetail.textContent = `${t("Windows load average neposkytuje")}${known(metrics.processorQueueLength) ? ` · ${t("fronta procesoru {count}", { count: metrics.processorQueueLength })}` : ""}`; } else { el.loadLabel.textContent = t("Zatížení 1 min"); el.load.textContent = known(metrics.load1) ? Number(metrics.load1).toLocaleString(i18n.locale(), { maximumFractionDigits: 2 }) : t("nedostupné"); el.loadDetail.textContent = t("průměrná zátěž"); } }); safely(() => { const total = metrics.memory?.total; const available = metrics.memory?.available; const used = known(total) && known(available) ? Math.max(0, total - available) : null; const usedPercent = pct(used, total); el.memory.textContent = pctText(usedPercent); el.memoryBar.style.width = `${usedPercent ?? 0}%`; el.memoryDetail.textContent = `${bytes(used)} / ${bytes(total)}`; pushHistory(state.ramHistory, usedPercent); }); safely(() => { const swap = metrics.swap || {}; el.swapLabel.textContent = windows ? "Pagefile" : "Swap"; const swapPercent = pct(swap.used, swap.total); el.swap.textContent = known(swap.total) ? (swap.total > 0 ? pctText(swapPercent) : t("neaktivní")) : t("nedostupné"); el.swapBar.style.width = `${swapPercent ?? 0}%`; el.swapDetail.textContent = known(swap.total) ? `${bytes(swap.used)} / ${bytes(swap.total)}` : t("údaj není dostupný"); pushHistory(state.swapHistory, known(swap.total) ? (swap.total > 0 ? swapPercent : 0) : null); }); safely(() => { const disk = metrics.disk || {}; el.diskLabel.textContent = t("Disk {mount}", { mount: disk.mount || "/" }); const diskPercent = pct(disk.used, disk.total); el.disk.textContent = pctText(diskPercent); el.diskBar.style.width = `${diskPercent ?? 0}%`; el.diskDetail.textContent = `${bytes(disk.used)} / ${bytes(disk.total)}${Array.isArray(metrics.disks) && metrics.disks.length > 1 ? ` · ${t("{count} svazků", { count: metrics.disks.length })}` : ""}`; }); safely(() => renderNetwork(metrics.network, metrics.collectedAt)); safely(() => renderMemorySection(metrics)); } function renderNetwork(network, collectedAt) { const interfaces = Array.isArray(network?.interfaces) ? network.interfaces : []; el.networkInterfaces.replaceChildren(); const collectedMs = Date.parse(collectedAt) || Date.now(); const next = new Map(); const active = interfaces.filter((item) => item.state === "up" || item.state === "active").length; el.networkSummary.textContent = interfaces.length ? `${t("{active}/{total} aktivní", { active, total: interfaces.length })}${network.defaultInterface ? ` · ${t("výchozí {name}", { name: network.defaultInterface })}` : ""}` : t("žádná rozhraní"); if (!interfaces.length) el.networkInterfaces.append(h("p", { className: "empty", text: t("Nebyla nalezena žádná síťová rozhraní mimo loopback.") })); for (const item of interfaces) { const isUp = item.state === "up" || item.state === "active"; const previous = state.previousNetwork.get(item.name); const seconds = previous ? Math.max(0, (collectedMs - previous.collectedMs) / 1000) : 0; const rate = (current, before) => (previous && known(current) && known(before) && seconds > 0 && current >= before ? (current - before) / seconds : null); const rxRate = rate(item.rxBytes, previous?.rxBytes); const txRate = rate(item.txBytes, previous?.txBytes); const traffic = known(item.rxBytes) || known(item.txBytes) ? [h("span", { className: "rx", text: `↓ ${bytes(item.rxBytes)}${rxRate !== null ? ` · ${bytes(rxRate)}/s` : ""}` }), h("span", { className: "tx", text: `↑ ${bytes(item.txBytes)}${txRate !== null ? ` · ${bytes(txRate)}/s` : ""}` })] : [h("span", { className: "rx", text: `↓ ${known(item.rxPackets) ? t("{count} paketů", { count: item.rxPackets.toLocaleString(i18n.locale()) }) : t("nedostupné")}` }), h("span", { className: "tx", text: `↑ ${known(item.txPackets) ? t("{count} paketů", { count: item.txPackets.toLocaleString(i18n.locale()) }) : t("nedostupné")}` }), item.bytesUnavailableReason ? h("span", { className: "mac", text: t("Ovladač síťové karty nehlásí přenesené bajty.") }) : null]; el.networkInterfaces.append(h("article", { className: "network-interface" }, [ h("div", { className: "interface-heading" }, [h("strong", { className: "interface-name" }, [item.name || "unknown", item.isDefault ? h("span", { className: "default-mark", text: t("výchozí") }) : null]), h("span", { className: `state ${isUp ? "up" : "down"}`, text: isUp ? t("aktivní") : (item.state || t("neznámý")) })]), h("p", { className: "addresses", text: Array.isArray(item.addresses) && item.addresses.length ? item.addresses.join(" · ") : t("bez globální IP adresy") }), item.mac ? h("p", { className: "mac", text: `MAC ${item.mac}` }) : null, h("div", { className: "traffic" }, traffic) ])); next.set(item.name, { rxBytes: item.rxBytes, txBytes: item.txBytes, collectedMs }); } state.previousNetwork = next; } // ---------- memory and virtual memory section ---------- const COLORS = { used: "#42a5ff", buffers: "#9b8cff", cached: "#43d6a0", reclaimable: "#2fb39a", free: "#2b3d52", inUse: "#42a5ff", modified: "#f2c463", standby: "#43d6a0" }; function renderMemorySection(metrics) { const windows = metrics.os === "windows"; const detail = metrics.memoryDetail; const total = windows ? metrics.memory?.total : detail?.total ?? metrics.memory?.total; el.memorySummary.textContent = `RAM ${pctText(state.ramHistory.at(-1))} · ${windows ? "pagefile" : "swap"} ${metrics.swap && known(metrics.swap.total) && metrics.swap.total > 0 ? pctText(pct(metrics.swap.used, metrics.swap.total)) : t("neaktivní")}`; drawChart(el.ramChart, state.ramHistory, "#42a5ff"); drawChart(el.swapChart, state.swapHistory, "#f2c463"); let slices = []; let facts = []; let explain = ""; const rate = (value) => (known(value) ? value.toFixed(1) : "—"); if (windows && detail) { slices = [["inUse", t("Používaná"), detail.slices.inUse], ["modified", t("Upravená (Modified)"), detail.slices.modified], ["standby", "Standby cache", detail.slices.standby], ["free", t("Volná a vynulovaná"), detail.slices.free]]; facts = [[t("Fyzická RAM celkem"), bytes(total)], [t("Skutečně využitá"), `${bytes(detail.slices.inUse)} (${pctText(pct(detail.slices.inUse, total))})`], [t("Dostupná"), bytes(detail.available)], [t("Volná"), bytes(detail.slices.free)], ["Standby cache", bytes(detail.slices.standby)], ["Modified page list", bytes(detail.slices.modified)], [t("System Cache (rezidentní)"), bytes(detail.systemCache)], [t("Paměť jádra"), `${bytes((detail.kernel.pagedPool || 0) + (detail.kernel.nonpagedPool || 0))} (${t("stránkovaný pool {paged}, nestránkovaný {nonpaged}", { paged: bytes(detail.kernel.pagedPool), nonpaged: bytes(detail.kernel.nonpagedPool) })})`], ["Commit (Committed / Limit)", `${bytes(detail.commit.committed)} / ${bytes(detail.commit.limit)} (${pctText(pct(detail.commit.committed, detail.commit.limit))})`], [t("Stránkování in / out"), t("{in} / {out} stránek/s", { in: rate(detail.paging.pagesInPerSecond), out: rate(detail.paging.pagesOutPerSecond) })]]; explain = t("Používaná = RAM obsazená procesy a jádrem. Dostupná = Standby cache + volná paměť, kterou Windows uvolní okamžitě. Modified obsahuje data, která se musí nejdřív zapsat. System Cache se s ostatními kategoriemi překrývá, proto není v pruhu. Commit Limit = RAM + pagefile, nejde o velikost pagefile."); } else if (detail) { slices = [["used", t("Využitá (aplikace a jádro)"), detail.used], ["buffers", "Buffers", detail.buffers], ["cached", "Cache (Cached)", detail.cached], ["reclaimable", t("Uvolnitelná slab"), detail.slabReclaimable], ["free", t("Volná"), detail.free]]; const kernel = (detail.slab || 0) + (detail.kernelStack || 0) + (detail.pageTables || 0); facts = [[t("Fyzická RAM celkem"), bytes(detail.total)], [t("Skutečně využitá"), `${bytes(detail.used)} (${pctText(pct(detail.used, detail.total))})`], [t("Dostupná (MemAvailable)"), bytes(detail.available)], [t("Volná (MemFree)"), bytes(detail.free)], ["Cache / Buffers", `${bytes(detail.cached)} / ${bytes(detail.buffers)}`], ["Reclaimable (SReclaimable)", bytes(detail.slabReclaimable)], [t("Sdílená (Shmem, součást Cached)"), bytes(detail.shmem)], [t("Paměť jádra"), `${bytes(kernel)} (slab ${bytes(detail.slab)}, stack ${bytes(detail.kernelStack)}, page tables ${bytes(detail.pageTables)})`], ["Dirty / Writeback", `${bytes(detail.dirty)} / ${bytes(detail.writeback)}`], ["Commit (Committed_AS / CommitLimit)", `${bytes(detail.commit.committed)} / ${bytes(detail.commit.limit)}`]]; explain = t("Využitá = MemTotal − MemFree − Buffers − Cached − SReclaimable (paměť, kterou nelze rychle uvolnit). Dostupná je odhad jádra, kolik lze použít bez swapování (zahrnuje volnou paměť a uvolnitelnou cache). Shmem je součástí Cached a není uvolnitelný. Kategorie v pruhu se nepřekrývají."); } else { facts = [[t("Fyzická RAM celkem"), bytes(total)], [t("Dostupná"), bytes(metrics.memory?.available)]]; explain = t("Podrobný rozpis paměti není z tohoto serveru dostupný (např. chybí oprávnění ke čítačům)."); } el.ramStack.replaceChildren(...slices.map(([key, label, value]) => h("i", { title: `${label}: ${bytes(value)}`, style: { width: `${pct(value, total) ?? 0}%`, background: COLORS[key] } }))); el.ramLegend.replaceChildren(...slices.map(([key, label, value]) => h("li", {}, [h("b", { style: { background: COLORS[key] } }), `${label} ${bytes(value)}`]))); el.ramFacts.replaceChildren(...facts.map(([label, value]) => h("div", {}, [h("dt", { text: label }), h("dd", { text: value })]))); el.ramExplain.textContent = explain; renderVirtual(metrics); } function renderVirtual(metrics) { const windows = metrics.os === "windows"; el.virtualTitle.textContent = windows ? t("Pagefile (virtuální paměť)") : "Swap"; const swap = metrics.swap || {}; const facts = known(swap.total) && swap.total === 0 ? [[t("Stav"), windows ? t("žádný aktivní pagefile") : t("swap není aktivní")]] : [[t("Celková velikost"), bytes(swap.total)], [t("Využito"), `${bytes(swap.used)} (${pctText(pct(swap.used, swap.total))})`], [t("K dispozici"), bytes(swap.free)]]; const paging = metrics.memoryDetail?.paging; if (!windows && paging && known(paging.swapInPages)) { const pageSize = metrics.pageSize || 4096; const now = Date.parse(metrics.collectedAt) || Date.now(); const previous = state.previousPaging; let rates = t("měřím…"); if (previous && now > previous.time) { const seconds = (now - previous.time) / 1000; rates = `${bytes(((paging.swapInPages - previous.in) * pageSize) / seconds)}/s in · ${bytes(((paging.swapOutPages - previous.out) * pageSize) / seconds)}/s out`; } state.previousPaging = { in: paging.swapInPages, out: paging.swapOutPages, time: now }; facts.push(["Swap-in / swap-out", rates]); } if (windows && paging) facts.push([t("Stránkování in / out"), t("{in} / {out} stránek/s", { in: known(paging.pagesInPerSecond) ? paging.pagesInPerSecond.toFixed(1) : "—", out: known(paging.pagesOutPerSecond) ? paging.pagesOutPerSecond.toFixed(1) : "—" })]); if (!windows && metrics.memoryDetail?.zswap) facts.push([t("zswap (jen čtení)"), metrics.memoryDetail.zswap.enabled ? t("zapnut ({compressor}), pool {pool}", { compressor: metrics.memoryDetail.zswap.compressor || "?", pool: bytes(metrics.memoryDetail.zswapPool) }) : t("vypnut")]); el.swapFacts.replaceChildren(...facts.map(([label, value]) => h("div", {}, [h("dt", { text: label }), h("dd", { text: value })]))); const config = state.vm; if (windows) { el.swapHead.replaceChildren(h("tr", {}, ["Pagefile", t("Alokováno"), t("Využito"), t("Maximum využití"), t("Nastaveno (min–max)")].map((text) => h("th", { text })))); const rows = (metrics.pagefiles || []).map((file) => { const setting = config?.pagefiles?.find((entry) => String(entry.name).toLowerCase() === file.name.toLowerCase()); return h("tr", {}, [h("td", { text: file.name }), h("td", { text: bytes(file.allocated) }), h("td", { text: bytes(file.used) }), h("td", { text: bytes(file.peak) }), h("td", { text: config ? (config.automatic ? t("automaticky (Windows)") : setting?.configured ? (setting.systemManagedSize ? t("velikost spravuje systém") : `${bytes(setting.initial)} – ${bytes(setting.maximum)}`) : t("nenastaveno")) : t("načte se ve správě") })]); }); el.swapRows.replaceChildren(...(rows.length ? rows : [h("tr", {}, h("td", { colspan: 5, className: "empty", text: metrics.pagefiles ? t("Žádný aktivní pagefile.") : t("Údaje o pagefile nejsou dostupné (WMI/CIM odepřeno).") }))])); el.swapNote.textContent = config ? `${config.automatic ? t("Velikost pagefile spravuje Windows automaticky.") : t("Ruční správa pagefile.")}${config.restartPending ? ` ${t("Změny čekají na restart Windows.")}` : ""}` : t("Konfigurace pagefile se načte po otevření správy."); el.vmPrivilege.textContent = config ? (config.canManage ? t("Relace má zvýšený token správce.") : t("Jen pro čtení: chybí zvýšený token správce.")) : ""; } else { el.swapHead.replaceChildren(h("tr", {}, [t("Swap prostor"), t("Typ"), t("Velikost"), t("Využito"), t("Priorita"), t("Po restartu")].map((text) => h("th", { text })))); const rows = (metrics.swapAreas || []).map((area) => { const configured = config?.swapAreas?.find((entry) => entry.path === area.name); return h("tr", {}, [h("td", { text: area.name }), h("td", { text: area.type === "zram" ? "zram" : area.type === "partition" ? t("oddíl") : t("soubor") }), h("td", { text: bytes(area.size) }), h("td", { text: `${bytes(area.used)} (${pctText(pct(area.used, area.size))})` }), h("td", {}, priorityCell(configured || { active: true, priority: area.priority, effectivePriority: area.priority })), h("td", { text: config ? (configured?.persistent ? t("ano (fstab)") : t("ne")) : "—" })]); }); el.swapRows.replaceChildren(...(rows.length ? rows : [h("tr", {}, h("td", { colspan: 6, className: "empty", text: t("Swap není aktivní.") }))])); const zram = (metrics.memoryDetail?.zram || []).map((device) => `${t("{device}: logicky {size}, data {data} → komprimováno {compressed}, RAM {ram}", { device: device.device, size: bytes(device.diskSize), data: bytes(device.originalData), compressed: bytes(device.compressedData), ram: bytes(device.memoryUsed) })}${device.algorithm ? `, ${device.algorithm}` : ""}`); el.swapNote.textContent = [zram.length ? t("zram (jen pro čtení): {list}", { list: zram.join("; ") }) : "", config ? "" : t("Trvalost a souborové systémy se načtou po otevření správy.")].filter(Boolean).join(" "); const mode = config?.privilege?.mode; el.vmPrivilege.textContent = config ? (mode === "none" ? t("Jen pro čtení: účet nemá sudo.") : mode === "sudo-password" ? t("Změny si vyžádají heslo pro sudo.") : t("Změny lze provést (root/sudo bez hesla).")) : ""; } el.manageVmButton.disabled = !el.sessionSelect.value; } // ---------- dialogs ---------- function runDialog(dialog, { init, submit } = {}) { return new Promise((resolve) => { const form = dialog.querySelector("form"); let finished = false; const finish = (value) => { if (finished) return; finished = true; form?.removeEventListener("submit", onSubmit); dialog.removeEventListener("close", onClose); for (const button of dialog.querySelectorAll("[data-close]")) button.removeEventListener("click", onClose); if (dialog.open) dialog.close(); resolve(value); }; const onSubmit = async (event) => { event.preventDefault(); const value = await submit(); if (value !== undefined) finish(value); }; const onClose = () => finish(null); form?.addEventListener("submit", onSubmit); dialog.addEventListener("close", onClose); for (const button of dialog.querySelectorAll("[data-close]")) button.addEventListener("click", onClose); init?.(); if (!dialog.open) dialog.showModal(); }); } function askSudo(label, error) { return runDialog(el.sudoDialog, { init: () => { el.sudoMessage.textContent = t("Operace „{label}“ vyžaduje oprávnění správce.", { label }); el.sudoPassword.value = ""; el.sudoError.textContent = error || ""; el.sudoError.hidden = !error; setTimeout(() => el.sudoPassword.focus(), 0); }, submit: () => { const value = el.sudoPassword.value; el.sudoPassword.value = ""; return value || undefined; } }).finally(() => { el.sudoPassword.value = ""; }); } function confirmAction({ title, details, confirmLabel = t("Potvrdit"), option = "" }) { return runDialog(el.confirmDialog, { init: () => { el.confirmTitle.textContent = title; el.confirmDetails.replaceChildren(...details.map((item) => h("li", { text: item }))); el.confirmOptionLabel.hidden = !option; el.confirmOptionText.textContent = option; el.confirmOption.checked = false; el.confirmSubmit.textContent = confirmLabel; }, submit: () => ({ option: el.confirmOption.checked }) }); } function askNumber(title, label, value) { return runDialog(el.confirmDialog, { init: () => { el.confirmTitle.textContent = title; const input = h("input", { id: "numberInput", type: "number", value: String(value), "aria-label": label }); el.confirmDetails.replaceChildren(h("li", {}, [h("label", {}, [label, input])])); el.confirmOptionLabel.hidden = true; el.confirmSubmit.textContent = t("Pokračovat"); setTimeout(() => input.focus(), 0); }, submit: () => Number(document.getElementById("numberInput").value) }); } // Core is called without a password first; the dialog appears only when Core reports that sudo needs one. async function withSudo(label, invoke) { const requestGeneration = state.generation; try { let password = ""; let attempts = 0; try { for (;;) { try { return await sessionResult(invoke(password ? { sudoPassword: password } : {}), requestGeneration); } catch (error) { if (requestGeneration !== state.generation) return; const { code } = parseError(error); if ((code === "SUDO_PASSWORD_REQUIRED" || code === "SUDO_PASSWORD_INCORRECT") && attempts < 3) { attempts += 1; password = await sessionResult(askSudo(label, code === "SUDO_PASSWORD_INCORRECT" && password ? t("Heslo bylo odmítnuto.") : ""), requestGeneration); if (!password) throw new Error(`CHJ_CANCELLED: ${t("Operace byla zrušena.")}`); continue; } throw error; } } } finally { password = ""; } } catch (error) { if (requestGeneration !== state.generation) return; throw error; } } // ---------- virtual memory management (never part of the monitoring loop) ---------- async function openVirtualMemory() { const requestGeneration = state.generation; try { const sessionId = el.sessionSelect.value; if (!sessionId) return; el.vmContent.replaceChildren(h("p", { className: "hint", text: t("Načítám konfiguraci virtuální paměti…") })); el.vmStatus.textContent = ""; void runDialog(el.vmDialog); await sessionResult(reloadVirtualMemory(sessionId, false), requestGeneration); } catch (error) { if (requestGeneration !== state.generation) return; throw error; } } async function reloadVirtualMemory(sessionId, elevated) { const requestGeneration = state.generation; if (sessionId !== el.sessionSelect.value) return; const token = state.generation; try { const run = (auth) => api.system.virtualMemory(sessionId, { ...auth, privileged: elevated }); const vm = elevated ? await sessionResult(withSudo(t("Načtení konfigurace swapu"), run), requestGeneration) : await sessionResult(run({}), requestGeneration); if (token !== state.generation) return; state.vm = vm; renderVirtualMemoryDialog(sessionId, vm); if (state.metrics) renderVirtual(state.metrics); } catch (error) { if (requestGeneration !== state.generation) return; if (token === state.generation) el.vmStatus.textContent = describe(error); } } function sizeInput(id, value, unit) { return h("span", { className: "inline-actions" }, [ h("input", { id: `${id}Value`, type: "number", min: "1", step: "1", value: String(value), "aria-label": t("Velikost") }), h("select", { id: `${id}Unit`, "aria-label": t("Jednotka") }, [h("option", { value: "MiB", text: "MiB", selected: unit === "MiB" }), h("option", { value: "GiB", text: "GiB", selected: unit === "GiB" })]) ]); } function readSize(id) { const value = Number(document.getElementById(`${id}Value`).value); return Number.isFinite(value) ? Math.round(value * (document.getElementById(`${id}Unit`).value === "GiB" ? 1024 : 1)) : NaN; } // Returns { ok: true, result } or { ok: false, error } so callers can offer a safer alternative. async function runVmChange(sessionId, label, payload, details, option = "", { confirm = true, describeResult } = {}) { const requestGeneration = state.generation; try { if (state.vmBusy || sessionId !== el.sessionSelect.value) return { ok: false }; if (confirm) { const answer = await sessionResult(confirmAction({ title: label, details, confirmLabel: t("Provést"), option }), requestGeneration); if (!answer || (option && !answer.option)) { el.vmStatus.textContent = t("Požadavek nebyl odeslán – neuložená změna byla zrušena."); return { ok: false }; } } state.vmBusy = true; el.vmStatus.textContent = t("{label}… (změna běží na serveru)", { label }); for (const button of el.vmContent.querySelectorAll("button")) button.disabled = true; try { const result = await sessionResult(withSudo(label, (auth) => api.system.manageVirtualMemory(sessionId, { ...payload, ...auth })), requestGeneration); const steps = (result.steps || []).map((step) => `${step.step}: ${step.status}${step.message ? ` (${step.message})` : ""}`).join(", "); el.vmStatus.textContent = describeResult ? describeResult(result) : result.restartRequired ? `${t("Uloženo – čeká na restart Windows (CH-J restart neprovádí).")}${result.backup ? ` ${t("Záloha: {path}", { path: result.backup })}` : ""}` : `${t("Provedeno a ověřeno podle skutečného stavu serveru.")}${steps ? ` ${t("Kroky: {steps}.", { steps })}` : ""}`; if (result.state) { state.vm = { ...state.vm, ...result.state }; renderVirtualMemoryDialog(sessionId, state.vm); if (state.metrics) renderVirtual(state.metrics); } else await sessionResult(reloadVirtualMemory(sessionId, false), requestGeneration); void loadMetrics(); return { ok: true, result }; } catch (error) { if (requestGeneration !== state.generation) return; el.vmStatus.textContent = t("Nebylo provedeno: {error}", { error: describe(error) }); // Show the server's real state again after a failed change. void reloadVirtualMemory(sessionId, false); return { ok: false, error: parseError(error) }; } finally { if (requestGeneration === state.generation) { state.vmBusy = false; for (const button of el.vmContent.querySelectorAll("button")) button.disabled = false; } } } catch (error) { if (requestGeneration !== state.generation) return; throw error; } } function renderVirtualMemoryDialog(sessionId, vm) { const children = vm.platform === "windows" ? windowsDialog(sessionId, vm) : linuxDialog(sessionId, vm); el.vmContent.replaceChildren(...children); } function windowsDialog(sessionId, vm) { const children = [h("p", { className: "hint", text: t("Správa: {management} · Commit Limit {limit} (RAM + pagefile, nejde o velikost pagefile) · výpis paměti při pádu: {dump}", { management: vm.automatic === null ? t("nelze zjistit") : vm.automatic ? t("automatická (Windows)") : t("ruční"), limit: bytes(vm.commit.limit), dump: vm.crashDumpEnabled ? t("zapnut") : t("vypnut") }) })]; if (vm.restartPending) children.push(h("p", {}, h("span", { className: "pill warn", text: t("Čeká na restart: {changes}", { changes: vm.pendingChanges.map((change) => change.description).join("; ") || t("konfigurace se liší od aktivní") }) }))); if (!vm.canManage) children.push(h("p", { className: "error", text: vm.readOnlyReason })); children.push(h("div", { className: "table-wrap" }, h("table", {}, [ h("thead", {}, h("tr", {}, ["Pagefile", t("Aktivní"), t("Alokováno"), t("Využito / maximum"), t("Nastaveno"), t("Volné místo"), ""].map((text) => h("th", { text })))), h("tbody", {}, vm.pagefiles.map((file) => h("tr", {}, [h("td", { text: file.name }), h("td", { text: file.active ? t("ano") : t("ne (po restartu)") }), h("td", { text: bytes(file.allocated) }), h("td", { text: `${bytes(file.used)} / ${bytes(file.peak)}` }), h("td", { text: file.configured ? (file.systemManagedSize ? t("systémová velikost") : `${bytes(file.initial)} – ${bytes(file.maximum)}`) : vm.automatic ? t("automaticky") : "—" }), h("td", { text: bytes(file.volumeFree) }), h("td", {}, vm.canManage && file.configured && vm.automatic === false ? h("button", { type: "button", className: "mini danger", text: t("Odebrat…"), onclick: () => removePagefile(sessionId, vm, file) }) : null)])))]))); if (vm.canManage) { if (vm.automatic === false) children.push(h("div", { className: "inline-actions" }, h("button", { type: "button", className: "button", text: t("Vrátit automatickou správu Windows"), onclick: () => runVmChange(sessionId, t("Automatická správa pagefile"), { action: "automatic" }, [t("Windows bude velikost pagefile spravovat sám."), t("Změna se projeví po restartu Windows; CH-J restart neprovede.")]) }))); const ntfs = vm.volumes.filter((volume) => ["NTFS", "ReFS"].includes(volume.format)); children.push(h("div", { className: "vm-form" }, [h("h4", { text: vm.automatic ? t("Přepnout na ruční správu a nastavit pagefile") : t("Nastavit nebo přidat pagefile") }), h("div", { className: "form-grid" }, [ h("label", {}, [t("Svazek"), h("select", { id: "pfDrive" }, ntfs.map((volume) => h("option", { value: volume.drive, text: t("{drive} ({format}, volno {free})", { drive: volume.drive, format: volume.format, free: bytes(volume.free) }), selected: volume.drive === vm.systemDrive })))]), h("label", {}, [t("Počáteční velikost"), sizeInput("pfInitial", 1024, "MiB")]), h("label", {}, [t("Maximální velikost"), sizeInput("pfMaximum", 4096, "MiB")]), h("label", { className: "check" }, [h("input", { id: "pfSystem", type: "checkbox" }), t("velikost určí systém (0/0)")]) ]), h("p", { id: "pfError", className: "error", hidden: true }), h("div", { className: "inline-actions" }, h("button", { type: "button", className: "button primary", text: t("Uložit (čeká na restart)"), onclick: () => setPagefile(sessionId, vm) }))])); } children.push(h("p", { className: "hint", text: t("Změny pagefile se uloží do konfigurace a projeví se až po restartu Windows; CH-J restart nikdy neprovádí. Velikost pagefile ovlivňuje Commit Limit a možnost uložit výpis paměti při pádu.") })); return children; } function linuxDialog(sessionId, vm) { const mode = vm.privilege?.mode; const children = [h("p", { className: "hint", text: t("{os} · oprávnění: {privilege} · RAM dostupná {ram} · swap volný {swap}", { os: vm.os || "Linux", privilege: mode === "root" ? "root" : mode === "sudo-nopasswd" ? t("sudo bez hesla") : mode === "sudo-password" ? t("sudo s heslem (vyžádá se při změně)") : t("bez sudo – jen čtení"), ram: bytes(vm.memory.available), swap: bytes(vm.memory.swapFree) }) })]; if (!vm.canManage) children.push(h("p", { className: "error", text: vm.readOnlyReason })); children.push(h("div", { className: "table-wrap" }, h("table", {}, [ h("thead", {}, h("tr", {}, [t("Swap prostor"), t("Typ / FS"), t("Velikost"), t("Využito"), t("Priorita"), t("Stav"), t("Po restartu"), t("Akce")].map((text) => h("th", { text })))), h("tbody", {}, vm.swapAreas.length ? vm.swapAreas.map((area) => swapRow(sessionId, vm, area)) : [h("tr", {}, h("td", { colspan: 8, className: "empty", text: t("Žádný swap není nakonfigurován.") }))])]))); if (vm.zram.length) children.push(h("p", { className: "hint", text: t("zram zařízení jsou v této verzi jen pro čtení.") })); if (vm.canManage) { const supported = new Set(["ext4", "ext2/ext3", "xfs", "btrfs", "f2fs"]); children.push(h("div", { className: "vm-form" }, [h("h4", { text: t("Nový swapfile") }), h("div", { className: "form-grid" }, [ h("label", {}, [t("Umístění"), h("select", { id: "swapDir" }, vm.locations.map((location) => h("option", { value: location.dir, disabled: !supported.has(location.fsType), text: `${location.dir} (${location.fsType}${supported.has(location.fsType) ? "" : ` – ${t("nepodporováno")}`}, ${t("volno {free}", { free: bytes(location.free) })})` })))]), h("label", {}, [t("Název souboru"), h("input", { id: "swapName", value: "swapfile-chj", maxlength: "48" })]), h("label", {}, [t("Velikost"), sizeInput("swapSize", 1, "GiB")]), h("label", {}, [t("Priorita (prázdné = automatická, jinak 0–32767)"), h("input", { id: "swapPriority", type: "number", min: "0", max: "32767", value: "", placeholder: t("automatická") })]), h("label", { className: "check" }, [h("input", { id: "swapPersist", type: "checkbox", checked: true }), t("aktivovat i po restartu (/etc/fstab se zálohou)")]) ]), h("p", { id: "swapError", className: "error", hidden: true }), h("p", { className: "hint", text: t("Swapfile se vytvoří zápisem všech bloků (dd) a mkswap, na Btrfs jako NOCOW (btrfs mkswapfile). Vyžaduje volné místo + 1 GiB rezervu. Pokud deaktivace stávajícího swapu není bezpečná, je lepší přidat nový swapfile.") }), h("div", { className: "inline-actions" }, h("button", { type: "button", className: "button primary", text: t("Vytvořit a aktivovat"), onclick: () => createSwap(sessionId) }))])); } if (!vm.root && mode === "sudo-password") children.push(h("div", { className: "inline-actions" }, h("button", { type: "button", className: "button", text: t("Načíst úplnou konfiguraci (sudo)"), onclick: () => reloadVirtualMemory(sessionId, true) }))); children.push(h("p", { className: "hint", text: t("Deaktivace je odmítnuta, pokud by obsah swapu nemohl bezpečně přejít do RAM (rezerva 10 % RAM, min. 512 MiB, nejvýše čtvrtina RAM; nepoužitý swap lze vypnout vždy) nebo do jiného swapu. Oddíly CH-J nemaže ani nepřerozděluje; zram a zswap jsou jen pro čtení. Úpravy /etc/fstab zachovávají ostatní řádky a vytvářejí zálohu.") })); return children; } function swapRow(sessionId, vm, area) { const allowed = vm.canManage ? new Set(area.allowedActions || []) : new Set(); const actions = []; if (allowed.has("activate") && !area.active) actions.push(h("button", { type: "button", className: "mini", text: t("Aktivovat"), onclick: () => runVmChange(sessionId, t("Aktivace {path}", { path: area.path }), { action: "activate", path: area.path, priority: "keep" }, [t("swapon {path} s prioritou {priority}", { path: area.path, priority: area.persistentPriority ?? t("automatickou") })]) })); if (allowed.has("deactivate") && area.active) actions.push(h("button", { type: "button", className: "mini", text: t("Deaktivovat"), onclick: () => runVmChange(sessionId, t("Deaktivace {path}", { path: area.path }), { action: "deactivate", path: area.path }, [t("swapoff {path}: obsah ({used}) se přesune do RAM nebo jiného swapu.", { path: area.path, used: bytes(area.used) }), t("Před provedením se ověří dostupná RAM; při riziku se operace odmítne."), t("Záznam v /etc/fstab zůstane beze změny.")]) })); if (allowed.has("priority")) actions.push(h("button", { type: "button", className: "mini", text: t("Priorita…"), onclick: () => changePriority(sessionId, area) })); if (allowed.has("persist")) actions.push(h("button", { type: "button", className: "mini", text: area.persistent ? t("Nepoužít po restartu") : t("Použít po restartu"), onclick: () => runVmChange(sessionId, area.persistent ? t("Odebrání z /etc/fstab") : t("Přidání do /etc/fstab"), { action: "persist", path: area.path, persist: !area.persistent, priority: "keep" }, [t("/etc/fstab bude zazálohován; ostatní řádky zůstanou beze změny.")]) })); if (allowed.has("resize")) actions.push(h("button", { type: "button", className: "mini", text: t("Změnit velikost…"), onclick: () => resizeSwap(sessionId, area) })); if (allowed.has("delete")) actions.push(h("button", { type: "button", className: "mini danger", text: t("Odstranit…"), onclick: () => runVmChange(sessionId, t("Odstranění {path}", { path: area.path }), { action: "delete", path: area.path }, [area.active ? t("Soubor {path} bude deaktivován (s kontrolou paměti) a smazán.", { path: area.path }) : t("Soubor {path} bude smazán.", { path: area.path }), area.persistent ? t("Jeho záznam bude odebrán z /etc/fstab (se zálohou).") : t("Není v /etc/fstab.")]) })); const management = area.management === "partition" ? t("oddíl – velikost a odstranění jen ručně") : area.management === "read-only" ? t("jen pro čtení") : area.management === "foreign-file" ? t("soubor mimo spravované cesty – bez změny velikosti a mazání") : ""; return h("tr", {}, [h("td", { text: area.path }), h("td", { text: `${area.type === "zram" ? "zram" : area.type === "partition" ? t("oddíl") : t("soubor")}${area.fsType ? ` / ${area.fsType}` : ""}` }), h("td", { text: bytes(area.size) }), h("td", { text: area.active ? bytes(area.used) : "—" }), h("td", {}, priorityCell(area)), h("td", {}, h("span", { className: `pill ${area.active ? "ok" : ""}`, text: area.active ? t("aktivní") : t("neaktivní") })), h("td", { text: area.persistent ? `${t("ano")}${area.persistentByChj ? " (CH-J)" : ""}${area.systemdUnit ? ` · ${area.systemdUnit.unit}` : ""}` : t("ne") }), h("td", {}, [h("div", { className: "inline-actions" }, actions), management ? h("p", { className: "hint", text: management }) : null])]); } // Effective priority comes from /proc/swaps; the saved one from pri= in /etc/fstab (null = automatic). // Negative values are kernel-assigned automatic priorities and are shown, never offered as input. function priorityLabel(value) { return value === null || value === undefined ? "—" : value < 0 ? t("{value} (automatická)", { value }) : String(value); } function savedLabel(area) { return area.persistent ? (area.persistentPriority === null ? t("automatická") : String(area.persistentPriority)) : t("neuloženo (není v /etc/fstab)"); } function priorityCell(area) { const parts = [h("span", { text: area.active ? priorityLabel(area.effectivePriority ?? area.priority) : t("neaktivní · uloženo {saved}", { saved: savedLabel(area) }) })]; if (area.active && area.precedence) parts.push(h("span", { className: "prio-note", text: area.sharedPriority ? t("{order}. pořadí · stejná priorita – střídání", { order: area.precedence }) : area.precedence === 1 ? t("1. pořadí – používá se přednostně") : t("{order}. pořadí – záloha", { order: area.precedence }) })); if (area.active && area.persistent !== undefined && (area.priorityPending || !area.persistent)) parts.push(h("span", { className: "prio-note", text: area.priorityPending ? t("uloženo {saved} – projeví se po opětovné aktivaci/restartu", { saved: savedLabel(area) }) : t("jen do restartu (není v /etc/fstab)") })); return parts; } function askPriority(area) { const custom = area.priorityMode === "custom"; const initial = area.persistentPriority ?? (area.active && area.priority >= 0 ? area.priority : 100); return runDialog(el.priorityDialog, { init: () => { el.priorityFacts.replaceChildren(...[ t("Zařízení: {path}", { path: area.path }), t("Velikost: {size}", { size: bytes(area.size) }), t("Aktuálně aktivní priorita: {priority}", { priority: area.active ? priorityLabel(area.effectivePriority ?? area.priority) : t("swap není aktivní") }), t("Uložená priorita: {priority}", { priority: savedLabel(area) }) ].map((text) => h("li", { text }))); el.priorityAuto.checked = !custom; el.priorityCustom.checked = custom; el.priorityValue.value = String(initial); el.priorityValue.disabled = !custom; el.priorityNow.checked = true; el.priorityLater.checked = false; el.priorityApplyBox.hidden = !area.active; el.priorityPersistLabel.hidden = area.persistent; el.priorityPersist.checked = !area.persistent; el.priorityError.hidden = true; el.priorityError.textContent = ""; const sync = () => { el.priorityValue.disabled = !el.priorityCustom.checked; }; el.priorityAuto.onchange = sync; el.priorityCustom.onchange = sync; setTimeout(() => (custom ? el.priorityValue : el.priorityAuto).focus(), 0); }, submit: () => { const mode = el.priorityCustom.checked ? "custom" : "auto"; const raw = el.priorityValue.value.trim(); if (mode === "custom" && !/^\d{1,5}$/.test(raw) || mode === "custom" && Number(raw) > 32767) { el.priorityError.textContent = t("Vlastní priorita musí být celé číslo 0–32767. Záporné hodnoty přiděluje jen Linux v automatickém režimu."); el.priorityError.hidden = false; el.priorityValue.focus(); return undefined; } return { priority: mode === "custom" ? Number(raw) : "auto", apply: area.active && el.priorityLater.checked ? "later" : "now", persist: !area.persistent && el.priorityPersist.checked }; } }); } function describePriorityResult(result) { const saved = result.steps.find((step) => step.step === "priority-saved"), runtime = result.steps.find((step) => step.step === "priority-runtime"); const savedText = saved?.status === "ok" ? t("Uloženo do /etc/fstab (se zálohou).") : t("Do /etc/fstab neuloženo – platí jen do restartu."); const runtimeText = !runtime ? "" : runtime.status === "ok" ? t("Aktivní priorita je nyní {priority}.", { priority: runtime.message.replace(/^already /, "") }) : runtime.status === "pending" ? t("Na běžícím systému se projeví až při příští aktivaci nebo restartu.") : t("Swap není aktivní; priorita se použije při aktivaci."); return `${savedText} ${runtimeText}`.trim(); } async function changePriority(sessionId, area) { const requestGeneration = state.generation; try { if (state.vmBusy) return; const choice = await sessionResult(askPriority(area), requestGeneration); if (!choice) return; const payload = { action: "priority", path: area.path, priority: choice.priority, apply: choice.apply, ...(choice.persist ? { persist: true } : {}) }; const label = t("Priorita {path}", { path: area.path }); const outcome = await sessionResult(runVmChange(sessionId, label, payload, [], "", { confirm: false, describeResult: describePriorityResult }), requestGeneration); if (!outcome.ok && outcome.error?.code === "VM_SWAPOFF_UNSAFE" && area.persistent || !outcome.ok && outcome.error?.code === "VM_SWAPOFF_UNSAFE" && choice.persist) { const answer = await sessionResult(confirmAction({ title: t("Okamžitá změna není bezpečná"), confirmLabel: t("Uložit pro restart"), details: [ t("Swap nyní nelze bezpečně deaktivovat – jeho obsah by se nevešel do dostupné RAM s rezervou."), t("Priorita se může uložit do /etc/fstab a projeví se při příští aktivaci nebo restartu serveru (CH-J restart neprovádí)."), t("Běžící swap zůstane beze změny.")] }), requestGeneration); if (answer) await sessionResult(runVmChange(sessionId, label, { ...payload, apply: "later" }, [], "", { confirm: false, describeResult: describePriorityResult }), requestGeneration); } } catch (error) { if (requestGeneration !== state.generation) return; throw error; } } async function resizeSwap(sessionId, area) { const requestGeneration = state.generation; try { const value = await sessionResult(askNumber(t("Nová velikost {path}", { path: area.path }), t("Velikost v MiB (64 – 65536)"), Math.round((area.size || 1073741824) / 1048576)), requestGeneration); if (value === null) return; if (!Number.isInteger(value) || value < 64 || value > 65536) { el.vmStatus.textContent = t("Neplatná velikost (64 – 65536 MiB)."); return; } await sessionResult(runVmChange(sessionId, t("Změna velikosti {path}", { path: area.path }), { action: "resize", path: area.path, sizeMiB: value, priority: "keep" }, [ area.active ? t("Swap se deaktivuje (jen pokud {used} bezpečně přejde do RAM nebo jiného swapu), znovu vytvoří a aktivuje.", { used: bytes(area.used) }) : t("Soubor se znovu vytvoří v nové velikosti."), t("Nový soubor se nejprve sestaví vedle starého; při chybě se původní soubor znovu aktivuje."), t("Pokud deaktivace není bezpečná, přidejte raději nový swapfile.")]), requestGeneration); } catch (error) { if (requestGeneration !== state.generation) return; throw error; } } async function createSwap(sessionId) { const requestGeneration = state.generation; try { const dir = el.swapDir.value; const name = el.swapName.value.trim(); const size = readSize("swapSize"); const priorityRaw = el.swapPriority.value.trim(); const priority = priorityRaw === "" ? -1 : Number(priorityRaw); const target = dir === "/" ? `/${name}` : `${dir}/${name}`; let problem = ""; if (!/^[A-Za-z0-9._-]{1,48}$/.test(name)) problem = t("Název smí obsahovat jen písmena, číslice, tečku, _ a -."); else if (dir === "/" && !/^swapfile/.test(name)) problem = t("V kořenovém adresáři musí název začínat „swapfile“."); else if (!Number.isInteger(size) || size < 64 || size > 65536) problem = t("Velikost musí být 64 MiB až 64 GiB."); else if (!Number.isInteger(priority) || priority < -1 || priority > 32767) problem = t("Priorita musí být prázdná (automatická) nebo celé číslo 0–32767."); el.swapError.textContent = problem; el.swapError.hidden = !problem; if (problem) return; await sessionResult(runVmChange(sessionId, t("Vytvoření {path}", { path: target }), { action: "create", path: target, sizeMiB: size, priority, persist: el.swapPersist.checked }, [ t("Soubor {path}, {size}, priorita {priority}.", { path: target, size: bytes(size * 1048576), priority: priority < 0 ? t("automatická") : priority }), el.swapPersist.checked ? t("Přidá se do /etc/fstab (se zálohou).") : t("Nebude aktivní po restartu."), t("Zápis velkého souboru může trvat několik minut.")]), requestGeneration); } catch (error) { if (requestGeneration !== state.generation) return; throw error; } } async function setPagefile(sessionId, vm) { const requestGeneration = state.generation; try { const system = el.pfSystem.checked; const initial = system ? 0 : readSize("pfInitial"); const maximum = system ? 0 : readSize("pfMaximum"); const drive = el.pfDrive.value; let problem = ""; if (!drive) problem = t("Vyberte svazek NTFS."); else if (!system && (!Number.isInteger(initial) || !Number.isInteger(maximum) || initial < 16 || maximum < initial)) problem = t("Počáteční velikost musí být alespoň 16 MiB a maximální alespoň počáteční."); el.pfError.textContent = problem; el.pfError.hidden = !problem; if (problem) return; await sessionResult(runVmChange(sessionId, `Pagefile ${drive}`, { action: "set", drive, initialMb: initial, maximumMb: maximum }, [ vm.automatic ? t("Automatická správa Windows bude vypnuta (ruční správa).") : t("Ruční správa zůstane zapnutá."), system ? t("Na {drive} bude pagefile se systémem spravovanou velikostí.", { drive }) : `${drive}\\pagefile.sys: ${bytes(initial * 1048576)} – ${bytes(maximum * 1048576)}.`, t("Konfigurace se zazálohuje a projeví po restartu Windows; CH-J restart neprovede."), t("Změní se Commit Limit; příliš malý pagefile může znemožnit výpis paměti při pádu.")]), requestGeneration); } catch (error) { if (requestGeneration !== state.generation) return; throw error; } } async function removePagefile(sessionId, vm, file) { const requestGeneration = state.generation; try { const last = vm.pagefiles.filter((entry) => entry.configured).length <= 1; const crash = file.drive === vm.systemDrive && Boolean(vm.crashDumpEnabled); await sessionResult(runVmChange(sessionId, t("Odebrání {name}", { name: file.name }), { action: "remove", drive: file.drive, confirmLast: last, confirmCrashDump: crash }, [ t("Konfigurace se zazálohuje; změna se projeví po restartu Windows."), last ? t("Jde o POSLEDNÍ pagefile: Commit Limit klesne na velikost RAM a hrozí chyby nedostatku paměti.") : "", crash ? t("Je na systémovém svazku a výpis paměti při pádu je zapnut – bez něj se výpis nemusí uložit.") : ""].filter(Boolean), last || crash ? t("Rozumím rizikům a výslovně potvrzuji odebrání") : ""), requestGeneration); } catch (error) { if (requestGeneration !== state.generation) return; throw error; } } // ---------- events ---------- el.closeButton.addEventListener("click", () => api.close()); el.refreshSessionsButton.addEventListener("click", () => { void loadSessions(); }); el.refreshMetricsButton.addEventListener("click", () => { void loadMetrics(); }); el.sessionSelect.addEventListener("change", () => { endedSessionId = ""; resetSession(); el.refreshMetricsButton.disabled = !el.sessionSelect.value; void loadMetrics(); }); el.manageVmButton.addEventListener("click", () => { void openVirtualMemory(); }); // Monitoring loop: metrics only, never configuration changes or administrative checks. setInterval(() => { if (!document.hidden && el.autoRefresh.checked) void loadMetrics(); }, 5000); (async () => { try { i18n.setLanguage(await api.ui.getLanguage()); } catch {} i18n.apply(document); api.ui.onLanguageChanged((language) => { i18n.setLanguage(language); i18n.apply(document); if (state.metrics) renderMetrics(state.metrics); void loadSessions(); }); try { const info = await api.getInfo(); el.pluginVersion.textContent = `${info.name} v${info.version} · ${info.publisher}`; } catch {} await loadSessions(); await loadMetrics(); })().catch((error) => setStatus(describe(error), true)); setInterval(() => { if (!document.hidden) void loadSessions().catch(() => {}); }, 3000);