CH-J ToolsSerververwaltung über SSH
Menü
Veröffentlichte Quellen

CH-J Tools

Durchsuchen Sie Verzeichnisse und Dateien einer bestimmten Anwendungsausgabe.

Quellen als ZIP herunterladen
CH-J / Server Manager 1704 Dateien
0.1.2-beta Hinzugefügt: · Erstellt:
index.js Hinzugefügt: Datum nicht erfasst 62,4 KB · 797 Zeilen
Code Datei herunterladen
1"use strict";
3const api = window.chjPlugin;
4const i18n = window.chjPluginI18n.createI18n("cs");
5const t = i18n.t;
6let endedSessionId = "";
7let sessionsLoading = false;
9// Guard every continuation, including sudo retries and dialog answers, against a server switch.
10async function sessionResult(promise, token) {
11 const value = await promise;
12 if (token !== state.generation) throw new Error("CHJ_SESSION_CHANGED");
13 return value;
16// Marks a catalog key that is translated with t() only when it is shown.
17const k = (text) => text;
18const el = new Proxy({}, { get: (_target, id) => document.getElementById(id) });
19const HISTORY = 60;
20const state = {
21 generation: 0,
22 busy: false,
23 previousNetwork: new Map(),
24 previousPaging: null,
25 ramHistory: [],
26 swapHistory: [],
27 metrics: null,
28 vm: null,
29 vmBusy: false
30};
32// ---------- CPU packages and GPUs: a missing value stays "unavailable", never 0 ----------
33function node(tag, className, text) {
34 const element = document.createElement(tag);
35 if (className) element.className = className;
36 if (text !== undefined) element.textContent = text;
37 return element;
39function usageRow(label, value, valueText) {
40 const row = node("div", "usage-row");
41 const head = node("div", "usage-head"); head.append(node("span", "", label), node("b", "", valueText));
42 const bar = node("div", "bar"), fill = node("i"); fill.style.width = `${known(value) ? Math.min(100, Number(value)) : 0}%`; bar.append(fill);
43 row.append(head, bar);
44 return row;
46const unit = (value, suffix) => known(value) ? `${Number(value).toLocaleString(i18n.locale(), { maximumFractionDigits: 1 })} ${suffix}` : null;
47function renderCpuPackages(packages) {
48 const list = Array.isArray(packages) ? packages : [];
49 el.cpuPackages.replaceChildren(...list.map((cpu, index) => {
50 const card = node("div", "package");
51 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(" · ");
52 card.append(usageRow(`${t("Procesor {id}", { id: index + 1 })}${cpu.model ? ` — ${cpu.model}` : ""}`, cpu.usagePercent, pctText(cpu.usagePercent)), node("small", "", facts));
53 return card;
54 }));
56function gpuHint(gpu) {
57 if (gpu.driver === "vfio-pci") return t("Zařízení je předané do virtuálního stroje; metriky jsou dostupné jen uvnitř něj.");
58 if (gpu.source !== "none") return known(gpu.usagePercent) ? "" : t("Tento ovladač využití GPU nehlásí.");
59 if (gpu.driver === "nvidia") return t("Nástroj nvidia-smi není dostupný, proto jsou vidět jen základní údaje.");
60 return t("Pro toto zařízení nejsou metriky dostupné.");
62function renderGpus(gpus) {
63 const list = Array.isArray(gpus) ? gpus : [];
64 el.gpuSummary.textContent = String(list.length);
65 if (!list.length) { el.gpuList.replaceChildren(node("p", "empty", t("Server nehlásí žádnou GPU."))); return; }
66 el.gpuList.replaceChildren(...list.map((gpu) => {
67 const card = node("article", "gpu");
68 const ids = [gpu.vendorId, gpu.deviceId].filter(Boolean).join(":");
69 card.append(node("strong", "model", gpu.name || (ids ? `PCI ${ids}` : gpu.id)));
70 card.append(usageRow(t("Využití GPU"), gpu.usagePercent, pctText(gpu.usagePercent)));
71 const memory = pct(gpu.memoryUsed, gpu.memoryTotal);
72 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é")));
73 const facts = node("dl", "facts compact");
74 const power = known(gpu.powerW) ? `${unit(gpu.powerW, "W")}${known(gpu.powerLimitW) ? ` / ${unit(gpu.powerLimitW, "W")}` : ""}` : null;
75 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(" ")]]) {
76 if (!value) continue;
77 const row = node("div"); row.append(node("dt", "", label), node("dd", "", value)); facts.append(row);
78 }
79 card.append(facts);
80 const hint = gpuHint(gpu);
81 if (hint) card.append(node("small", "gpu-hint", hint));
82 return card;
83 }));
86// ---------- formatting (server data is always rendered as text) ----------
87const known = (value) => value !== null && value !== undefined && Number.isFinite(Number(value));
88function bytes(value) {
89 if (!known(value)) return t("nedostupné");
90 const units = ["B", "KiB", "MiB", "GiB", "TiB"]; let amount = Number(value); let unit = 0;
91 while (Math.abs(amount) >= 1024 && unit < units.length - 1) { amount /= 1024; unit += 1; }
92 return `${amount.toLocaleString(i18n.locale(), { maximumFractionDigits: unit >= 3 ? 2 : 1 })} ${units[unit]}`;
94const pct = (used, total) => known(used) && known(total) && total > 0 ? Math.max(0, Math.min(100, used / total * 100)) : null;
95const pctText = (value) => known(value) ? `${Number(value).toLocaleString(i18n.locale(), { maximumFractionDigits: 1 })} %` : t("nedostupné");
96function duration(seconds) {
97 if (!known(seconds)) return t("nedostupné");
98 let remaining = Math.max(0, Number(seconds)); const days = Math.floor(remaining / 86400); remaining %= 86400;
99 return `${days ? `${days} d ` : ""}${Math.floor(remaining / 3600)} h ${Math.floor((remaining % 3600) / 60)} min`;
101function setStatus(message, error = false) { el.status.textContent = message; el.status.classList.toggle("error", error); }
102function h(tag, props = {}, children = []) {
103 const node = document.createElement(tag);
104 for (const [key, value] of Object.entries(props)) {
105 if (value === undefined || value === null || value === false) continue;
106 if (key === "className") node.className = value;
107 else if (key === "text") node.textContent = String(value);
108 else if (key === "style") Object.assign(node.style, value);
109 else if (key.startsWith("on") && typeof value === "function") node.addEventListener(key.slice(2), value);
110 else if (typeof value === "boolean") node[key] = value;
111 else node.setAttribute(key, String(value));
112 }
113 for (const child of [].concat(children)) if (child !== null && child !== undefined && child !== false) node.append(child instanceof Node ? child : String(child));
114 return node;
116function parseError(error) {
117 const raw = String(error?.message || error || "");
118 const match = raw.match(/\b([A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+): ([\s\S]*)$/);
119 return match ? { code: match[1], detail: match[2].trim() } : { code: "UNKNOWN", detail: raw.replace(/^Error invoking remote method '[^']+': (?:\w*Error: )?/, "") };
121const ERROR_TEXT = {
122 CHJ_CANCELLED: k("Operace byla zrušena."),
123 SUDO_NOT_PERMITTED: k("Účet SSH relace nemá oprávnění sudo; zadání hesla to nezmění."),
124 SUDO_PASSWORD_INCORRECT: k("Heslo pro sudo bylo odmítnuto."),
125 SUDO_TTY_REQUIRED: k("Politika sudo vyžaduje terminál (requiretty); CH-J ji neobchází."),
126 SESSION_NOT_CONNECTED: k("SSH relace byla odpojena; stav operace na serveru ověřte po novém připojení."),
127 REMOTE_COMMAND_TIMEOUT: k("Server neodpověděl včas."),
128 VM_BUSY: k("Na této relaci právě probíhá jiná změna virtuální paměti."),
129 VM_SWAP_BUSY: k("Na serveru právě probíhá jiná změna swapu."),
130 VM_SWAPOFF_UNSAFE: k("Deaktivace by mohla vyčerpat paměť, operace byla odmítnuta:"),
131 VM_SWAP_DISK_FULL: k("Nedostatek místa na disku:"),
132 VM_SWAP_FS_UNSUPPORTED: k("Souborový systém nepodporuje bezpečný swapfile:"),
133 VM_SWAP_SIZE_INVALID: k("Neplatná velikost (64 MiB – 64 GiB, celé MiB)."),
134 VM_SWAP_PATH_INVALID: k("Neplatná cesta swapfile:"),
135 VM_SWAP_MANUAL_ONLY: k("Tuto operaci je nutné provést ručně:"),
136 VM_SWAP_EXISTS: k("Soubor již existuje."),
137 VM_SWAP_PRIORITY_INVALID: k("Neplatná priorita: zvolte automatickou nebo celé číslo 0–32767."),
138 VM_FSTAB_AMBIGUOUS: k("V /etc/fstab je pro tento swap více záznamů; nejprve je ručně sjednoťte:"),
139 VM_FSTAB_BACKUP_FAILED: k("Zálohu /etc/fstab nelze vytvořit, nic nebylo změněno:"),
140 VM_FSTAB_WRITE_FAILED: k("/etc/fstab nelze zapsat, původní obsah zůstal:"),
141 VM_SWAPON_FAILED: k("Aktivace swapu selhala:"),
142 VM_PRIVILEGE_REQUIRED: k("Změna vyžaduje oprávnění root/sudo."),
143 VM_WINDOWS_ELEVATION_REQUIRED: k("Změna pagefile vyžaduje relaci správce se zvýšeným tokenem (CH-J neobchází UAC)."),
144 VM_PAGEFILE_LAST: k("Jde o poslední pagefile – vyžaduje výslovné potvrzení."),
145 VM_PAGEFILE_CRASHDUMP: k("Systémový pagefile slouží pro výpis paměti při pádu – vyžaduje výslovné potvrzení."),
146 VM_PAGEFILE_SIZE_INVALID: k("Neplatná velikost pagefile:"),
147 VM_PAGEFILE_DISK_FULL: k("Nedostatek místa na svazku:"),
148 VM_PAGEFILE_AUTOMATIC: k("Nejprve přepněte na ruční správu."),
149 METRICS_PLATFORM_UNSUPPORTED: k("Operační systém serveru nelze rozpoznat:")
150};
151function describe(error) {
152 const { code, detail } = parseError(error);
153 const base = ERROR_TEXT[code] && t(ERROR_TEXT[code]);
154 return base ? (base.endsWith(":") ? `${base} ${detail}` : base) : detail || code;
157// ---------- charts ----------
158function drawChart(canvas, values, color) {
159 const context = canvas.getContext("2d");
160 const ratio = window.devicePixelRatio || 1;
161 const width = canvas.clientWidth || 560; const height = canvas.clientHeight || 120;
162 canvas.width = width * ratio; canvas.height = height * ratio;
163 context.setTransform(ratio, 0, 0, ratio, 0, 0);
164 context.clearRect(0, 0, width, height);
165 context.strokeStyle = "rgba(151,176,205,.15)"; context.lineWidth = 1;
166 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(); }
167 context.strokeStyle = color; context.lineWidth = 2; context.beginPath();
168 let started = false;
169 values.forEach((value, index) => {
170 if (!known(value)) { started = false; return; }
171 const x = index * (width / (HISTORY - 1)); const y = height - (value / 100) * (height - 4) - 2;
172 if (!started) { context.moveTo(x, y); started = true; } else context.lineTo(x, y);
173 });
174 context.stroke();
175 context.fillStyle = "#8ea5bf"; context.font = "10px sans-serif";
176 const last = values.at(-1);
177 context.fillText(known(last) ? `${last.toFixed(1)} %` : "—", 6, 13);
179function pushHistory(list, value) { list.push(known(value) ? value : null); while (list.length > HISTORY) list.shift(); }
181// ---------- sessions and metrics ----------
182async function loadSessions() {
183 if (sessionsLoading) return;
184 sessionsLoading = true;
185 let sessions = [];
186 try { sessions = (await api.sessions.list()).filter((session) => session.state === "connected"); }
187 catch (error) { sessionsLoading = false; setStatus(t("Relace nelze načíst: {error}", { error: describe(error) }), true); return; }
188 sessionsLoading = false;
189 const selected = el.sessionSelect.value;
190 const next = api.sessions.reconcile(endedSessionId || selected, sessions);
191 if (next.reason === "gone") endedSessionId ||= selected;
192 el.sessionSelect.replaceChildren(new Option(sessions.length ? t("Vyberte relaci…") : t("Žádná připojená relace"), ""));
193 for (const session of sessions) el.sessionSelect.add(new Option(api.sessions.displayName(session), session.sessionId));
194 el.sessionSelect.value = next.id;
195 if (el.sessionSelect.value !== selected) resetSession();
196 if (next.reason === "choose") api.sessions.offerChoice(el.sessionSelect, sessions, t("Vyberte relaci…"));
197 el.refreshMetricsButton.disabled = !el.sessionSelect.value;
198 if (next.reason === "gone") setStatus(t("Relace skončila. Vyberte server."), true);
199 else if (!sessions.length) setStatus(t("Nejprve se v Core připojte k serveru přes SSH."));
202function resetSession() {
203 state.generation += 1;
204 Object.assign(state, { previousNetwork: new Map(), previousPaging: null, ramHistory: [], swapHistory: [], metrics: null, vm: null });
205 state.busy = false; state.vmBusy = false;
206 for (const dialog of document.querySelectorAll("dialog[open]")) dialog.close();
207 el.sudoPassword.value = ""; el.vmContent.replaceChildren(); el.vmStatus.textContent = "";
208 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 = "—";
209 for (const id of ["cpuPackages", "gpuList", "cpuTypes", "networkInterfaces", "ramStack", "ramLegend", "ramFacts", "swapFacts", "swapHead", "swapRows"]) el[id].replaceChildren();
210 for (const id of ["cpuBar", "memoryBar", "swapBar", "diskBar"]) el[id].style.width = "0%";
211 drawChart(el.ramChart, [], "#6ad"); drawChart(el.swapChart, [], "#6ad");
212 el.manageVmButton.disabled = true;
215async function loadMetrics() {
216 const requestGeneration = state.generation;
217 const sessionId = el.sessionSelect.value;
218 if (!sessionId || state.busy) return;
219 const token = state.generation;
220 state.busy = true; el.refreshMetricsButton.disabled = true;
221 try {
222 const metrics = await sessionResult(api.system.readMetrics(sessionId), requestGeneration);
223 if (token !== state.generation) return;
224 state.metrics = metrics;
225 renderMetrics(metrics);
226 setStatus(`${t("Aktualizováno {time}", { time: new Date(metrics.collectedAt).toLocaleTimeString(i18n.locale()) })}${metrics.unavailable?.length ? ` · ${t("nedostupné zdroje: {count}", { count: metrics.unavailable.length })}` : ""}.`);
227 } catch (error) { if (requestGeneration !== state.generation) return;
228 if (token === state.generation) setStatus(describe(error), true);
229 if (parseError(error).code === "SESSION_NOT_CONNECTED") void loadSessions();
230 } finally { if (requestGeneration === state.generation) {
231 state.busy = false; el.refreshMetricsButton.disabled = !el.sessionSelect.value;
232 } }
235// Each card renders independently: one missing metric never breaks the others.
236function safely(render) { try { render(); } catch (error) { console.error("Metric render failed", error); } }
238function renderMetrics(metrics) {
239 const windows = metrics.os === "windows";
240 safely(() => {
241 el.hostname.textContent = metrics.hostname || t("nedostupné");
242 el.platform.textContent = windows ? `${metrics.osName || "Windows"}${metrics.installationType ? ` (${metrics.installationType})` : ""}` : metrics.platform || t("nedostupné");
243 el.kernelLabel.textContent = windows ? t("Sestavení") : t("Jádro");
244 el.kernel.textContent = metrics.kernel || t("nedostupné");
245 el.uptime.textContent = duration(metrics.uptimeSeconds);
246 });
247 safely(() => {
248 const cpu = metrics.cpu || {};
249 const usage = known(cpu.usagePercent) ? Number(cpu.usagePercent) : null;
250 el.cpu.textContent = usage !== null ? pctText(usage) : cpu.usagePending ? t("měřím…") : t("nedostupné");
251 el.cpuBar.style.width = `${usage ?? 0}%`;
252 el.cpuDetail.textContent = known(cpu.logicalCores) ? t("{count} logických procesorů", { count: cpu.logicalCores }) : t("počet procesorů nedostupný");
253 el.cpuArchitecture.textContent = cpu.architecture || t("neznámá architektura");
254 el.cpuModel.textContent = cpu.model || t("Údaj o modelu CPU není dostupný.");
255 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é") });
256 renderCpuPackages(cpu.packages); renderGpus(metrics.gpus);
257 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.");
258 });
259 safely(() => {
260 if (metrics.loadAverageSupported === false) {
261 el.loadLabel.textContent = t("Zatížení (load average)");
262 el.load.textContent = t("nedostupné");
263 el.loadDetail.textContent = `${t("Windows load average neposkytuje")}${known(metrics.processorQueueLength) ? ` · ${t("fronta procesoru {count}", { count: metrics.processorQueueLength })}` : ""}`;
264 } else {
265 el.loadLabel.textContent = t("Zatížení 1 min");
266 el.load.textContent = known(metrics.load1) ? Number(metrics.load1).toLocaleString(i18n.locale(), { maximumFractionDigits: 2 }) : t("nedostupné");
267 el.loadDetail.textContent = t("průměrná zátěž");
268 }
269 });
270 safely(() => {
271 const total = metrics.memory?.total; const available = metrics.memory?.available;
272 const used = known(total) && known(available) ? Math.max(0, total - available) : null;
273 const usedPercent = pct(used, total);
274 el.memory.textContent = pctText(usedPercent);
275 el.memoryBar.style.width = `${usedPercent ?? 0}%`;
276 el.memoryDetail.textContent = `${bytes(used)} / ${bytes(total)}`;
277 pushHistory(state.ramHistory, usedPercent);
278 });
279 safely(() => {
280 const swap = metrics.swap || {};
281 el.swapLabel.textContent = windows ? "Pagefile" : "Swap";
282 const swapPercent = pct(swap.used, swap.total);
283 el.swap.textContent = known(swap.total) ? (swap.total > 0 ? pctText(swapPercent) : t("neaktivní")) : t("nedostupné");
284 el.swapBar.style.width = `${swapPercent ?? 0}%`;
285 el.swapDetail.textContent = known(swap.total) ? `${bytes(swap.used)} / ${bytes(swap.total)}` : t("údaj není dostupný");
286 pushHistory(state.swapHistory, known(swap.total) ? (swap.total > 0 ? swapPercent : 0) : null);
287 });
288 safely(() => {
289 const disk = metrics.disk || {};
290 el.diskLabel.textContent = t("Disk {mount}", { mount: disk.mount || "/" });
291 const diskPercent = pct(disk.used, disk.total);
292 el.disk.textContent = pctText(diskPercent);
293 el.diskBar.style.width = `${diskPercent ?? 0}%`;
294 el.diskDetail.textContent = `${bytes(disk.used)} / ${bytes(disk.total)}${Array.isArray(metrics.disks) && metrics.disks.length > 1 ? ` · ${t("{count} svazků", { count: metrics.disks.length })}` : ""}`;
295 });
296 safely(() => renderNetwork(metrics.network, metrics.collectedAt));
297 safely(() => renderMemorySection(metrics));
300function renderNetwork(network, collectedAt) {
301 const interfaces = Array.isArray(network?.interfaces) ? network.interfaces : [];
302 el.networkInterfaces.replaceChildren();
303 const collectedMs = Date.parse(collectedAt) || Date.now();
304 const next = new Map();
305 const active = interfaces.filter((item) => item.state === "up" || item.state === "active").length;
306 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í");
307 if (!interfaces.length) el.networkInterfaces.append(h("p", { className: "empty", text: t("Nebyla nalezena žádná síťová rozhraní mimo loopback.") }));
308 for (const item of interfaces) {
309 const isUp = item.state === "up" || item.state === "active";
310 const previous = state.previousNetwork.get(item.name);
311 const seconds = previous ? Math.max(0, (collectedMs - previous.collectedMs) / 1000) : 0;
312 const rate = (current, before) => (previous && known(current) && known(before) && seconds > 0 && current >= before ? (current - before) / seconds : null);
313 const rxRate = rate(item.rxBytes, previous?.rxBytes); const txRate = rate(item.txBytes, previous?.txBytes);
314 const traffic = known(item.rxBytes) || known(item.txBytes)
315 ? [h("span", { className: "rx", text: `↓ ${bytes(item.rxBytes)}${rxRate !== null ? ` · ${bytes(rxRate)}/s` : ""}` }),
316 h("span", { className: "tx", text: `↑ ${bytes(item.txBytes)}${txRate !== null ? ` · ${bytes(txRate)}/s` : ""}` })]
317 : [h("span", { className: "rx", text: `↓ ${known(item.rxPackets) ? t("{count} paketů", { count: item.rxPackets.toLocaleString(i18n.locale()) }) : t("nedostupné")}` }),
318 h("span", { className: "tx", text: `↑ ${known(item.txPackets) ? t("{count} paketů", { count: item.txPackets.toLocaleString(i18n.locale()) }) : t("nedostupné")}` }),
319 item.bytesUnavailableReason ? h("span", { className: "mac", text: t("Ovladač síťové karty nehlásí přenesené bajty.") }) : null];
320 el.networkInterfaces.append(h("article", { className: "network-interface" }, [
321 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]),
322 h("span", { className: `state ${isUp ? "up" : "down"}`, text: isUp ? t("aktivní") : (item.state || t("neznámý")) })]),
323 h("p", { className: "addresses", text: Array.isArray(item.addresses) && item.addresses.length ? item.addresses.join(" · ") : t("bez globální IP adresy") }),
324 item.mac ? h("p", { className: "mac", text: `MAC ${item.mac}` }) : null,
325 h("div", { className: "traffic" }, traffic)
326 ]));
327 next.set(item.name, { rxBytes: item.rxBytes, txBytes: item.txBytes, collectedMs });
328 }
329 state.previousNetwork = next;
332// ---------- memory and virtual memory section ----------
333const COLORS = { used: "#42a5ff", buffers: "#9b8cff", cached: "#43d6a0", reclaimable: "#2fb39a", free: "#2b3d52", inUse: "#42a5ff", modified: "#f2c463", standby: "#43d6a0" };
335function renderMemorySection(metrics) {
336 const windows = metrics.os === "windows";
337 const detail = metrics.memoryDetail;
338 const total = windows ? metrics.memory?.total : detail?.total ?? metrics.memory?.total;
339 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í")}`;
340 drawChart(el.ramChart, state.ramHistory, "#42a5ff");
341 drawChart(el.swapChart, state.swapHistory, "#f2c463");
342 let slices = []; let facts = []; let explain = "";
343 const rate = (value) => (known(value) ? value.toFixed(1) : "—");
344 if (windows && detail) {
345 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]];
346 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)],
347 ["Standby cache", bytes(detail.slices.standby)], ["Modified page list", bytes(detail.slices.modified)], [t("System Cache (rezidentní)"), bytes(detail.systemCache)],
348 [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) })})`],
349 ["Commit (Committed / Limit)", `${bytes(detail.commit.committed)} / ${bytes(detail.commit.limit)} (${pctText(pct(detail.commit.committed, detail.commit.limit))})`],
350 [t("Stránkování in / out"), t("{in} / {out} stránek/s", { in: rate(detail.paging.pagesInPerSecond), out: rate(detail.paging.pagesOutPerSecond) })]];
351 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.");
352 } else if (detail) {
353 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]];
354 const kernel = (detail.slab || 0) + (detail.kernelStack || 0) + (detail.pageTables || 0);
355 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)],
356 ["Cache / Buffers", `${bytes(detail.cached)} / ${bytes(detail.buffers)}`], ["Reclaimable (SReclaimable)", bytes(detail.slabReclaimable)], [t("Sdílená (Shmem, součást Cached)"), bytes(detail.shmem)],
357 [t("Paměť jádra"), `${bytes(kernel)} (slab ${bytes(detail.slab)}, stack ${bytes(detail.kernelStack)}, page tables ${bytes(detail.pageTables)})`],
358 ["Dirty / Writeback", `${bytes(detail.dirty)} / ${bytes(detail.writeback)}`], ["Commit (Committed_AS / CommitLimit)", `${bytes(detail.commit.committed)} / ${bytes(detail.commit.limit)}`]];
359 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í.");
360 } else {
361 facts = [[t("Fyzická RAM celkem"), bytes(total)], [t("Dostupná"), bytes(metrics.memory?.available)]];
362 explain = t("Podrobný rozpis paměti není z tohoto serveru dostupný (např. chybí oprávnění ke čítačům).");
363 }
364 el.ramStack.replaceChildren(...slices.map(([key, label, value]) => h("i", { title: `${label}: ${bytes(value)}`, style: { width: `${pct(value, total) ?? 0}%`, background: COLORS[key] } })));
365 el.ramLegend.replaceChildren(...slices.map(([key, label, value]) => h("li", {}, [h("b", { style: { background: COLORS[key] } }), `${label} ${bytes(value)}`])));
366 el.ramFacts.replaceChildren(...facts.map(([label, value]) => h("div", {}, [h("dt", { text: label }), h("dd", { text: value })])));
367 el.ramExplain.textContent = explain;
368 renderVirtual(metrics);
371function renderVirtual(metrics) {
372 const windows = metrics.os === "windows";
373 el.virtualTitle.textContent = windows ? t("Pagefile (virtuální paměť)") : "Swap";
374 const swap = metrics.swap || {};
375 const facts = known(swap.total) && swap.total === 0
376 ? [[t("Stav"), windows ? t("žádný aktivní pagefile") : t("swap není aktivní")]]
377 : [[t("Celková velikost"), bytes(swap.total)], [t("Využito"), `${bytes(swap.used)} (${pctText(pct(swap.used, swap.total))})`], [t("K dispozici"), bytes(swap.free)]];
378 const paging = metrics.memoryDetail?.paging;
379 if (!windows && paging && known(paging.swapInPages)) {
380 const pageSize = metrics.pageSize || 4096;
381 const now = Date.parse(metrics.collectedAt) || Date.now();
382 const previous = state.previousPaging;
383 let rates = t("měřím…");
384 if (previous && now > previous.time) {
385 const seconds = (now - previous.time) / 1000;
386 rates = `${bytes(((paging.swapInPages - previous.in) * pageSize) / seconds)}/s in · ${bytes(((paging.swapOutPages - previous.out) * pageSize) / seconds)}/s out`;
387 }
388 state.previousPaging = { in: paging.swapInPages, out: paging.swapOutPages, time: now };
389 facts.push(["Swap-in / swap-out", rates]);
390 }
391 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) : "—" })]);
392 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")]);
393 el.swapFacts.replaceChildren(...facts.map(([label, value]) => h("div", {}, [h("dt", { text: label }), h("dd", { text: value })])));
394 const config = state.vm;
395 if (windows) {
396 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 }))));
397 const rows = (metrics.pagefiles || []).map((file) => {
398 const setting = config?.pagefiles?.find((entry) => String(entry.name).toLowerCase() === file.name.toLowerCase());
399 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) }),
400 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ě") })]);
401 });
402 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).") }))]));
403 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.");
404 el.vmPrivilege.textContent = config ? (config.canManage ? t("Relace má zvýšený token správce.") : t("Jen pro čtení: chybí zvýšený token správce.")) : "";
405 } else {
406 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 }))));
407 const rows = (metrics.swapAreas || []).map((area) => {
408 const configured = config?.swapAreas?.find((entry) => entry.path === area.name);
409 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) }),
410 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")) : "—" })]);
411 });
412 el.swapRows.replaceChildren(...(rows.length ? rows : [h("tr", {}, h("td", { colspan: 6, className: "empty", text: t("Swap není aktivní.") }))]));
413 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}` : ""}`);
414 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(" ");
415 const mode = config?.privilege?.mode;
416 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).")) : "";
417 }
418 el.manageVmButton.disabled = !el.sessionSelect.value;
421// ---------- dialogs ----------
422function runDialog(dialog, { init, submit } = {}) {
423 return new Promise((resolve) => {
424 const form = dialog.querySelector("form");
425 let finished = false;
426 const finish = (value) => {
427 if (finished) return; finished = true;
428 form?.removeEventListener("submit", onSubmit); dialog.removeEventListener("close", onClose);
429 for (const button of dialog.querySelectorAll("[data-close]")) button.removeEventListener("click", onClose);
430 if (dialog.open) dialog.close();
431 resolve(value);
432 };
433 const onSubmit = async (event) => { event.preventDefault(); const value = await submit(); if (value !== undefined) finish(value); };
434 const onClose = () => finish(null);
435 form?.addEventListener("submit", onSubmit);
436 dialog.addEventListener("close", onClose);
437 for (const button of dialog.querySelectorAll("[data-close]")) button.addEventListener("click", onClose);
438 init?.();
439 if (!dialog.open) dialog.showModal();
440 });
442function askSudo(label, error) {
443 return runDialog(el.sudoDialog, {
444 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); },
445 submit: () => { const value = el.sudoPassword.value; el.sudoPassword.value = ""; return value || undefined; }
446 }).finally(() => { el.sudoPassword.value = ""; });
448function confirmAction({ title, details, confirmLabel = t("Potvrdit"), option = "" }) {
449 return runDialog(el.confirmDialog, {
450 init: () => {
451 el.confirmTitle.textContent = title;
452 el.confirmDetails.replaceChildren(...details.map((item) => h("li", { text: item })));
453 el.confirmOptionLabel.hidden = !option; el.confirmOptionText.textContent = option; el.confirmOption.checked = false;
454 el.confirmSubmit.textContent = confirmLabel;
455 },
456 submit: () => ({ option: el.confirmOption.checked })
457 });
459function askNumber(title, label, value) {
460 return runDialog(el.confirmDialog, {
461 init: () => {
462 el.confirmTitle.textContent = title;
463 const input = h("input", { id: "numberInput", type: "number", value: String(value), "aria-label": label });
464 el.confirmDetails.replaceChildren(h("li", {}, [h("label", {}, [label, input])]));
465 el.confirmOptionLabel.hidden = true; el.confirmSubmit.textContent = t("Pokračovat");
466 setTimeout(() => input.focus(), 0);
467 },
468 submit: () => Number(document.getElementById("numberInput").value)
469 });
471// Core is called without a password first; the dialog appears only when Core reports that sudo needs one.
472async function withSudo(label, invoke) {
473 const requestGeneration = state.generation;
474 try {
475 let password = ""; let attempts = 0;
476 try {
477 for (;;) {
478 try { return await sessionResult(invoke(password ? { sudoPassword: password } : {}), requestGeneration); }
479 catch (error) { if (requestGeneration !== state.generation) return;
480 const { code } = parseError(error);
481 if ((code === "SUDO_PASSWORD_REQUIRED" || code === "SUDO_PASSWORD_INCORRECT") && attempts < 3) {
482 attempts += 1;
483 password = await sessionResult(askSudo(label, code === "SUDO_PASSWORD_INCORRECT" && password ? t("Heslo bylo odmítnuto.") : ""), requestGeneration);
484 if (!password) throw new Error(`CHJ_CANCELLED: ${t("Operace byla zrušena.")}`);
485 continue;
486 }
487 throw error;
488 }
489 }
490 } finally { password = ""; }
491 } catch (error) { if (requestGeneration !== state.generation) return; throw error; }
494// ---------- virtual memory management (never part of the monitoring loop) ----------
495async function openVirtualMemory() {
496 const requestGeneration = state.generation;
497 try {
498 const sessionId = el.sessionSelect.value;
499 if (!sessionId) return;
500 el.vmContent.replaceChildren(h("p", { className: "hint", text: t("Načítám konfiguraci virtuální paměti…") }));
501 el.vmStatus.textContent = "";
502 void runDialog(el.vmDialog);
503 await sessionResult(reloadVirtualMemory(sessionId, false), requestGeneration);
504 } catch (error) { if (requestGeneration !== state.generation) return; throw error; }
507async function reloadVirtualMemory(sessionId, elevated) {
508 const requestGeneration = state.generation;
509 if (sessionId !== el.sessionSelect.value) return;
510 const token = state.generation;
511 try {
512 const run = (auth) => api.system.virtualMemory(sessionId, { ...auth, privileged: elevated });
513 const vm = elevated ? await sessionResult(withSudo(t("Načtení konfigurace swapu"), run), requestGeneration) : await sessionResult(run({}), requestGeneration);
514 if (token !== state.generation) return;
515 state.vm = vm;
516 renderVirtualMemoryDialog(sessionId, vm);
517 if (state.metrics) renderVirtual(state.metrics);
518 } catch (error) { if (requestGeneration !== state.generation) return;
519 if (token === state.generation) el.vmStatus.textContent = describe(error);
520 }
523function sizeInput(id, value, unit) {
524 return h("span", { className: "inline-actions" }, [
525 h("input", { id: `${id}Value`, type: "number", min: "1", step: "1", value: String(value), "aria-label": t("Velikost") }),
526 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" })])
527 ]);
529function readSize(id) {
530 const value = Number(document.getElementById(`${id}Value`).value);
531 return Number.isFinite(value) ? Math.round(value * (document.getElementById(`${id}Unit`).value === "GiB" ? 1024 : 1)) : NaN;
534// Returns { ok: true, result } or { ok: false, error } so callers can offer a safer alternative.
535async function runVmChange(sessionId, label, payload, details, option = "", { confirm = true, describeResult } = {}) {
536 const requestGeneration = state.generation;
537 try {
538 if (state.vmBusy || sessionId !== el.sessionSelect.value) return { ok: false };
539 if (confirm) {
540 const answer = await sessionResult(confirmAction({ title: label, details, confirmLabel: t("Provést"), option }), requestGeneration);
541 if (!answer || (option && !answer.option)) { el.vmStatus.textContent = t("Požadavek nebyl odeslán – neuložená změna byla zrušena."); return { ok: false }; }
542 }
543 state.vmBusy = true;
544 el.vmStatus.textContent = t("{label}… (změna běží na serveru)", { label });
545 for (const button of el.vmContent.querySelectorAll("button")) button.disabled = true;
546 try {
547 const result = await sessionResult(withSudo(label, (auth) => api.system.manageVirtualMemory(sessionId, { ...payload, ...auth })), requestGeneration);
548 const steps = (result.steps || []).map((step) => `${step.step}: ${step.status}${step.message ? ` (${step.message})` : ""}`).join(", ");
549 el.vmStatus.textContent = describeResult ? describeResult(result) : result.restartRequired
550 ? `${t("Uloženo – čeká na restart Windows (CH-J restart neprovádí).")}${result.backup ? ` ${t("Záloha: {path}", { path: result.backup })}` : ""}`
551 : `${t("Provedeno a ověřeno podle skutečného stavu serveru.")}${steps ? ` ${t("Kroky: {steps}.", { steps })}` : ""}`;
552 if (result.state) { state.vm = { ...state.vm, ...result.state }; renderVirtualMemoryDialog(sessionId, state.vm); if (state.metrics) renderVirtual(state.metrics); }
553 else await sessionResult(reloadVirtualMemory(sessionId, false), requestGeneration);
554 void loadMetrics();
555 return { ok: true, result };
556 } catch (error) { if (requestGeneration !== state.generation) return;
557 el.vmStatus.textContent = t("Nebylo provedeno: {error}", { error: describe(error) });
558 // Show the server's real state again after a failed change.
559 void reloadVirtualMemory(sessionId, false);
560 return { ok: false, error: parseError(error) };
561 } finally { if (requestGeneration === state.generation) {
562 state.vmBusy = false;
563 for (const button of el.vmContent.querySelectorAll("button")) button.disabled = false;
564 } }
565 } catch (error) { if (requestGeneration !== state.generation) return; throw error; }
568function renderVirtualMemoryDialog(sessionId, vm) {
569 const children = vm.platform === "windows" ? windowsDialog(sessionId, vm) : linuxDialog(sessionId, vm);
570 el.vmContent.replaceChildren(...children);
573function windowsDialog(sessionId, vm) {
574 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") }) })];
575 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í") }) })));
576 if (!vm.canManage) children.push(h("p", { className: "error", text: vm.readOnlyReason }));
577 children.push(h("div", { className: "table-wrap" }, h("table", {}, [
578 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 })))),
579 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)}` }),
580 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) }),
581 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)])))])));
582 if (vm.canManage) {
583 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.")]) })));
584 const ntfs = vm.volumes.filter((volume) => ["NTFS", "ReFS"].includes(volume.format));
585 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") }),
586 h("div", { className: "form-grid" }, [
587 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 })))]),
588 h("label", {}, [t("Počáteční velikost"), sizeInput("pfInitial", 1024, "MiB")]),
589 h("label", {}, [t("Maximální velikost"), sizeInput("pfMaximum", 4096, "MiB")]),
590 h("label", { className: "check" }, [h("input", { id: "pfSystem", type: "checkbox" }), t("velikost určí systém (0/0)")])
591 ]),
592 h("p", { id: "pfError", className: "error", hidden: true }),
593 h("div", { className: "inline-actions" }, h("button", { type: "button", className: "button primary", text: t("Uložit (čeká na restart)"), onclick: () => setPagefile(sessionId, vm) }))]));
594 }
595 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.") }));
596 return children;
599function linuxDialog(sessionId, vm) {
600 const mode = vm.privilege?.mode;
601 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) }) })];
602 if (!vm.canManage) children.push(h("p", { className: "error", text: vm.readOnlyReason }));
603 children.push(h("div", { className: "table-wrap" }, h("table", {}, [
604 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 })))),
605 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.") }))])])));
606 if (vm.zram.length) children.push(h("p", { className: "hint", text: t("zram zařízení jsou v této verzi jen pro čtení.") }));
607 if (vm.canManage) {
608 const supported = new Set(["ext4", "ext2/ext3", "xfs", "btrfs", "f2fs"]);
609 children.push(h("div", { className: "vm-form" }, [h("h4", { text: t("Nový swapfile") }),
610 h("div", { className: "form-grid" }, [
611 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) })})` })))]),
612 h("label", {}, [t("Název souboru"), h("input", { id: "swapName", value: nextSwapName(vm), maxlength: "48" })]),
613 h("label", {}, [t("Velikost"), sizeInput("swapSize", 1, "GiB")]),
614 h("label", {}, [t("Priorita (prázdné = automatická, jinak 0–32767)"), h("input", { id: "swapPriority", type: "number", min: "0", max: "32767", value: "", placeholder: t("automatická") })]),
615 h("label", { className: "check" }, [h("input", { id: "swapPersist", type: "checkbox", checked: true }), t("aktivovat i po restartu (/etc/fstab se zálohou)")])
616 ]),
617 h("p", { id: "swapError", className: "error", hidden: true }),
618 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.") }),
619 h("div", { className: "inline-actions" }, h("button", { type: "button", className: "button primary", text: t("Vytvořit a aktivovat"), onclick: () => createSwap(sessionId) }))]));
620 }
621 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) })));
622 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.") }));
623 return children;
626// Each new swap file offers a name nothing has yet, so a second and third one go through as they are.
627function nextSwapName(vm) {
628 const taken = new Set(vm.swapAreas.map((area) => String(area.path).split("/").pop()));
629 if (!taken.has("swapfile-chj")) return "swapfile-chj";
630 let number = 2;
631 while (taken.has(`swapfile-chj-${number}`)) number += 1;
632 return `swapfile-chj-${number}`;
634function swapRow(sessionId, vm, area) {
635 const allowed = vm.canManage ? new Set(area.allowedActions || []) : new Set();
636 const actions = [];
637 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") })]) }));
638 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.")]) }));
639 if (allowed.has("priority")) actions.push(h("button", { type: "button", className: "mini", text: t("Priorita…"), onclick: () => changePriority(sessionId, area) }));
640 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.")]) }));
641 if (allowed.has("resize")) actions.push(h("button", { type: "button", className: "mini", text: t("Změnit velikost…"), onclick: () => resizeSwap(sessionId, area) }));
642 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.")]) }));
643 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í") : "";
644 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) }),
645 h("td", { text: area.active ? bytes(area.used) : "—" }), h("td", {}, priorityCell(area)),
646 h("td", {}, h("span", { className: `pill ${area.active ? "ok" : ""}`, text: area.active ? t("aktivní") : t("neaktivní") })),
647 h("td", { text: area.persistent ? `${t("ano")}${area.persistentByChj ? " (CH-J)" : ""}${area.systemdUnit ? ` · ${area.systemdUnit.unit}` : ""}` : t("ne") }),
648 h("td", {}, [h("div", { className: "inline-actions" }, actions), management ? h("p", { className: "hint", text: management }) : null])]);
651// Effective priority comes from /proc/swaps; the saved one from pri= in /etc/fstab (null = automatic).
652// Negative values are kernel-assigned automatic priorities and are shown, never offered as input.
653function priorityLabel(value) { return value === null || value === undefined ? "—" : value < 0 ? t("{value} (automatická)", { value }) : String(value); }
654function savedLabel(area) { return area.persistent ? (area.persistentPriority === null ? t("automatická") : String(area.persistentPriority)) : t("neuloženo (není v /etc/fstab)"); }
655function priorityCell(area) {
656 const parts = [h("span", { text: area.active ? priorityLabel(area.effectivePriority ?? area.priority) : t("neaktivní · uloženo {saved}", { saved: savedLabel(area) }) })];
657 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 }) }));
658 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)") }));
659 return parts;
662function askPriority(area) {
663 const custom = area.priorityMode === "custom";
664 const initial = area.persistentPriority ?? (area.active && area.priority >= 0 ? area.priority : 100);
665 return runDialog(el.priorityDialog, {
666 init: () => {
667 el.priorityFacts.replaceChildren(...[
668 t("Zařízení: {path}", { path: area.path }), t("Velikost: {size}", { size: bytes(area.size) }),
669 t("Aktuálně aktivní priorita: {priority}", { priority: area.active ? priorityLabel(area.effectivePriority ?? area.priority) : t("swap není aktivní") }),
670 t("Uložená priorita: {priority}", { priority: savedLabel(area) })
671 ].map((text) => h("li", { text })));
672 el.priorityAuto.checked = !custom; el.priorityCustom.checked = custom;
673 el.priorityValue.value = String(initial); el.priorityValue.disabled = !custom;
674 el.priorityNow.checked = true; el.priorityLater.checked = false;
675 el.priorityApplyBox.hidden = !area.active;
676 el.priorityPersistLabel.hidden = area.persistent; el.priorityPersist.checked = !area.persistent;
677 el.priorityError.hidden = true; el.priorityError.textContent = "";
678 const sync = () => { el.priorityValue.disabled = !el.priorityCustom.checked; };
679 el.priorityAuto.onchange = sync; el.priorityCustom.onchange = sync;
680 setTimeout(() => (custom ? el.priorityValue : el.priorityAuto).focus(), 0);
681 },
682 submit: () => {
683 const mode = el.priorityCustom.checked ? "custom" : "auto";
684 const raw = el.priorityValue.value.trim();
685 if (mode === "custom" && !/^\d{1,5}$/.test(raw) || mode === "custom" && Number(raw) > 32767) {
686 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.");
687 el.priorityError.hidden = false; el.priorityValue.focus(); return undefined;
688 }
689 return { priority: mode === "custom" ? Number(raw) : "auto", apply: area.active && el.priorityLater.checked ? "later" : "now", persist: !area.persistent && el.priorityPersist.checked };
690 }
691 });
694function describePriorityResult(result) {
695 const saved = result.steps.find((step) => step.step === "priority-saved"), runtime = result.steps.find((step) => step.step === "priority-runtime");
696 const savedText = saved?.status === "ok" ? t("Uloženo do /etc/fstab (se zálohou).") : t("Do /etc/fstab neuloženo – platí jen do restartu.");
697 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.");
698 return `${savedText} ${runtimeText}`.trim();
701async function changePriority(sessionId, area) {
702 const requestGeneration = state.generation;
703 try {
704 if (state.vmBusy) return;
705 const choice = await sessionResult(askPriority(area), requestGeneration);
706 if (!choice) return;
707 const payload = { action: "priority", path: area.path, priority: choice.priority, apply: choice.apply, ...(choice.persist ? { persist: true } : {}) };
708 const label = t("Priorita {path}", { path: area.path });
709 const outcome = await sessionResult(runVmChange(sessionId, label, payload, [], "", { confirm: false, describeResult: describePriorityResult }), requestGeneration);
710 if (!outcome.ok && outcome.error?.code === "VM_SWAPOFF_UNSAFE" && area.persistent || !outcome.ok && outcome.error?.code === "VM_SWAPOFF_UNSAFE" && choice.persist) {
711 const answer = await sessionResult(confirmAction({ title: t("Okamžitá změna není bezpečná"), confirmLabel: t("Uložit pro restart"), details: [
712 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);
713 if (answer) await sessionResult(runVmChange(sessionId, label, { ...payload, apply: "later" }, [], "", { confirm: false, describeResult: describePriorityResult }), requestGeneration);
714 }
715 } catch (error) { if (requestGeneration !== state.generation) return; throw error; }
718async function resizeSwap(sessionId, area) {
719 const requestGeneration = state.generation;
720 try {
721 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);
722 if (value === null) return;
723 if (!Number.isInteger(value) || value < 64 || value > 65536) { el.vmStatus.textContent = t("Neplatná velikost (64 – 65536 MiB)."); return; }
724 await sessionResult(runVmChange(sessionId, t("Změna velikosti {path}", { path: area.path }), { action: "resize", path: area.path, sizeMiB: value, priority: "keep" }, [
725 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."),
726 t("Nový soubor se nejprve sestaví vedle starého; při chybě se původní soubor znovu aktivuje."),
727 t("Pokud deaktivace není bezpečná, přidejte raději nový swapfile.")]), requestGeneration);
728 } catch (error) { if (requestGeneration !== state.generation) return; throw error; }
731async function createSwap(sessionId) {
732 const requestGeneration = state.generation;
733 try {
734 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);
735 const target = dir === "/" ? `/${name}` : `${dir}/${name}`;
736 let problem = "";
737 if (!/^[A-Za-z0-9._-]{1,48}$/.test(name)) problem = t("Název smí obsahovat jen písmena, číslice, tečku, _ a -.");
738 else if (state.vm?.swapAreas?.some((area) => area.path === target)) problem = t("Soubor již existuje.");
739 else if (dir === "/" && !/^swapfile/.test(name)) problem = t("V kořenovém adresáři musí název začínat „swapfile“.");
740 else if (!Number.isInteger(size) || size < 64 || size > 65536) problem = t("Velikost musí být 64 MiB až 64 GiB.");
741 else if (!Number.isInteger(priority) || priority < -1 || priority > 32767) problem = t("Priorita musí být prázdná (automatická) nebo celé číslo 0–32767.");
742 el.swapError.textContent = problem; el.swapError.hidden = !problem;
743 if (problem) return;
744 await sessionResult(runVmChange(sessionId, t("Vytvoření {path}", { path: target }), { action: "create", path: target, sizeMiB: size, priority, persist: el.swapPersist.checked }, [
745 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);
746 } catch (error) { if (requestGeneration !== state.generation) return; throw error; }
749async function setPagefile(sessionId, vm) {
750 const requestGeneration = state.generation;
751 try {
752 const system = el.pfSystem.checked;
753 const initial = system ? 0 : readSize("pfInitial"); const maximum = system ? 0 : readSize("pfMaximum"); const drive = el.pfDrive.value;
754 let problem = "";
755 if (!drive) problem = t("Vyberte svazek NTFS.");
756 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í.");
757 el.pfError.textContent = problem; el.pfError.hidden = !problem;
758 if (problem) return;
759 await sessionResult(runVmChange(sessionId, `Pagefile ${drive}`, { action: "set", drive, initialMb: initial, maximumMb: maximum }, [
760 vm.automatic ? t("Automatická správa Windows bude vypnuta (ruční správa).") : t("Ruční správa zůstane zapnutá."),
761 system ? t("Na {drive} bude pagefile se systémem spravovanou velikostí.", { drive }) : `${drive}\\pagefile.sys: ${bytes(initial * 1048576)} – ${bytes(maximum * 1048576)}.`,
762 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);
763 } catch (error) { if (requestGeneration !== state.generation) return; throw error; }
766async function removePagefile(sessionId, vm, file) {
767 const requestGeneration = state.generation;
768 try {
769 const last = vm.pagefiles.filter((entry) => entry.configured).length <= 1;
770 const crash = file.drive === vm.systemDrive && Boolean(vm.crashDumpEnabled);
771 await sessionResult(runVmChange(sessionId, t("Odebrání {name}", { name: file.name }), { action: "remove", drive: file.drive, confirmLast: last, confirmCrashDump: crash }, [
772 t("Konfigurace se zazálohuje; změna se projeví po restartu Windows."),
773 last ? t("Jde o POSLEDNÍ pagefile: Commit Limit klesne na velikost RAM a hrozí chyby nedostatku paměti.") : "",
774 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),
775 last || crash ? t("Rozumím rizikům a výslovně potvrzuji odebrání") : ""), requestGeneration);
776 } catch (error) { if (requestGeneration !== state.generation) return; throw error; }
779// ---------- events ----------
780el.closeButton.addEventListener("click", () => api.close());
781el.refreshSessionsButton.addEventListener("click", () => { void loadSessions(); });
782el.refreshMetricsButton.addEventListener("click", () => { void loadMetrics(); });
783el.sessionSelect.addEventListener("change", () => { endedSessionId = ""; resetSession(); el.refreshMetricsButton.disabled = !el.sessionSelect.value; void loadMetrics(); });
784el.manageVmButton.addEventListener("click", () => { void openVirtualMemory(); });
785// Monitoring loop: metrics only, never configuration changes or administrative checks.
786setInterval(() => { if (!document.hidden && el.autoRefresh.checked) void loadMetrics(); }, 5000);
788(async () => {
789 try { i18n.setLanguage(await api.ui.getLanguage()); } catch {}
790 i18n.apply(document);
791 api.ui.onLanguageChanged((language) => { i18n.setLanguage(language); i18n.apply(document); if (state.metrics) renderMetrics(state.metrics); void loadSessions(); });
792 try { const info = await api.getInfo(); el.pluginVersion.textContent = `${info.name} v${info.version} · ${info.publisher}`; } catch {}
793 await loadSessions();
794 await loadMetrics();
795})().catch((error) => setStatus(describe(error), true));
797setInterval(() => { if (!document.hidden) void loadSessions().catch(() => {}); }, 3000);
SHA-25648dc39c1d63dd8404a89a9c7104d3fe9a90da4de01dcd681a1a6606caf4d7ac0

CH-J Proprietary Software License 1.14 · Die Quellen werden unter der CH-J Proprietary Software License 1.14 bereitgestellt. Ihre Verfügbarkeit ändert die Lizenzbedingungen nicht und gewährt keine zusätzlichen Rechte.

SHA-256 des Archivsac8fe790f4ba60fc098f8c985fc2656cfcc6cdc21fb34b30e308a2b54e62258f
Dateien