CH-J Server Manageradministrasi server melalui SSH
Menu
Sumber yang diterbitkan

CH-J Server Manager

Jelajahi direktori dan file untuk rilis aplikasi tertentu.

Unduh ZIP sumber
CH-J / Server Manager 377 file
core-20261019T111616Z-160a00bfcddc Ditambahkan: · Dibuat:
index.js Ditambahkan: Tanggal tidak tercatat 51,5 KB · 673 baris
Kode Unduh file
1"use strict";
3const api = window.chjPlugin;
4const el = new Proxy({}, { get: (_target, id) => document.getElementById(id) });
5const HISTORY = 60;
6const state = {
7 generation: 0,
8 busy: false,
9 previousNetwork: new Map(),
10 previousPaging: null,
11 ramHistory: [],
12 swapHistory: [],
13 metrics: null,
14 vm: null,
15 vmBusy: false
16};
18// ---------- formatting (server data is always rendered as text) ----------
19const known = (value) => value !== null && value !== undefined && Number.isFinite(Number(value));
20function bytes(value) {
21 if (!known(value)) return "nedostupné";
22 const units = ["B", "KiB", "MiB", "GiB", "TiB"]; let amount = Number(value); let unit = 0;
23 while (Math.abs(amount) >= 1024 && unit < units.length - 1) { amount /= 1024; unit += 1; }
24 return `${amount.toLocaleString("cs-CZ", { maximumFractionDigits: unit >= 3 ? 2 : 1 })} ${units[unit]}`;
26const pct = (used, total) => known(used) && known(total) && total > 0 ? Math.max(0, Math.min(100, used / total * 100)) : null;
27const pctText = (value) => known(value) ? `${Number(value).toLocaleString("cs-CZ", { maximumFractionDigits: 1 })} %` : "nedostupné";
28function duration(seconds) {
29 if (!known(seconds)) return "nedostupné";
30 let remaining = Math.max(0, Number(seconds)); const days = Math.floor(remaining / 86400); remaining %= 86400;
31 return `${days ? `${days} d ` : ""}${Math.floor(remaining / 3600)} h ${Math.floor((remaining % 3600) / 60)} min`;
33function setStatus(message, error = false) { el.status.textContent = message; el.status.classList.toggle("error", error); }
34function h(tag, props = {}, children = []) {
35 const node = document.createElement(tag);
36 for (const [key, value] of Object.entries(props)) {
37 if (value === undefined || value === null || value === false) continue;
38 if (key === "className") node.className = value;
39 else if (key === "text") node.textContent = String(value);
40 else if (key === "style") Object.assign(node.style, value);
41 else if (key.startsWith("on") && typeof value === "function") node.addEventListener(key.slice(2), value);
42 else if (typeof value === "boolean") node[key] = value;
43 else node.setAttribute(key, String(value));
44 }
45 for (const child of [].concat(children)) if (child !== null && child !== undefined && child !== false) node.append(child instanceof Node ? child : String(child));
46 return node;
48function parseError(error) {
49 const raw = String(error?.message || error || "");
50 const match = raw.match(/\b([A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+): ([\s\S]*)$/);
51 return match ? { code: match[1], detail: match[2].trim() } : { code: "UNKNOWN", detail: raw.replace(/^Error invoking remote method '[^']+': (?:\w*Error: )?/, "") };
53const ERROR_TEXT = {
54 CHJ_CANCELLED: "Operace byla zrušena.",
55 SUDO_NOT_PERMITTED: "Účet SSH relace nemá oprávnění sudo; zadání hesla to nezmění.",
56 SUDO_PASSWORD_INCORRECT: "Heslo pro sudo bylo odmítnuto.",
57 SUDO_TTY_REQUIRED: "Politika sudo vyžaduje terminál (requiretty); CH-J ji neobchází.",
58 SESSION_NOT_CONNECTED: "SSH relace byla odpojena; stav operace na serveru ověřte po novém připojení.",
59 REMOTE_COMMAND_TIMEOUT: "Server neodpověděl včas.",
60 VM_BUSY: "Na této relaci právě probíhá jiná změna virtuální paměti.",
61 VM_SWAP_BUSY: "Na serveru právě probíhá jiná změna swapu.",
62 VM_SWAPOFF_UNSAFE: "Deaktivace by mohla vyčerpat paměť, operace byla odmítnuta:",
63 VM_SWAP_DISK_FULL: "Nedostatek místa na disku:",
64 VM_SWAP_FS_UNSUPPORTED: "Souborový systém nepodporuje bezpečný swapfile:",
65 VM_SWAP_SIZE_INVALID: "Neplatná velikost (64 MiB – 64 GiB, celé MiB).",
66 VM_SWAP_PATH_INVALID: "Neplatná cesta swapfile:",
67 VM_SWAP_MANUAL_ONLY: "Tuto operaci je nutné provést ručně:",
68 VM_SWAP_EXISTS: "Soubor již existuje.",
69 VM_SWAP_PRIORITY_INVALID: "Neplatná priorita: zvolte automatickou nebo celé číslo 0–32767.",
70 VM_FSTAB_AMBIGUOUS: "V /etc/fstab je pro tento swap více záznamů; nejprve je ručně sjednoťte:",
71 VM_FSTAB_BACKUP_FAILED: "Zálohu /etc/fstab nelze vytvořit, nic nebylo změněno:",
72 VM_FSTAB_WRITE_FAILED: "/etc/fstab nelze zapsat, původní obsah zůstal:",
73 VM_SWAPON_FAILED: "Aktivace swapu selhala:",
74 VM_PRIVILEGE_REQUIRED: "Změna vyžaduje oprávnění root/sudo.",
75 VM_WINDOWS_ELEVATION_REQUIRED: "Změna pagefile vyžaduje relaci správce se zvýšeným tokenem (CH-J neobchází UAC).",
76 VM_PAGEFILE_LAST: "Jde o poslední pagefile – vyžaduje výslovné potvrzení.",
77 VM_PAGEFILE_CRASHDUMP: "Systémový pagefile slouží pro výpis paměti při pádu – vyžaduje výslovné potvrzení.",
78 VM_PAGEFILE_SIZE_INVALID: "Neplatná velikost pagefile:",
79 VM_PAGEFILE_DISK_FULL: "Nedostatek místa na svazku:",
80 VM_PAGEFILE_AUTOMATIC: "Nejprve přepněte na ruční správu.",
81 METRICS_PLATFORM_UNSUPPORTED: "Operační systém serveru nelze rozpoznat:"
82};
83function describe(error) {
84 const { code, detail } = parseError(error);
85 const base = ERROR_TEXT[code];
86 return base ? (base.endsWith(":") ? `${base} ${detail}` : base) : detail || code;
89// ---------- charts ----------
90function drawChart(canvas, values, color) {
91 const context = canvas.getContext("2d");
92 const ratio = window.devicePixelRatio || 1;
93 const width = canvas.clientWidth || 560; const height = canvas.clientHeight || 120;
94 canvas.width = width * ratio; canvas.height = height * ratio;
95 context.setTransform(ratio, 0, 0, ratio, 0, 0);
96 context.clearRect(0, 0, width, height);
97 context.strokeStyle = "rgba(151,176,205,.15)"; context.lineWidth = 1;
98 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(); }
99 context.strokeStyle = color; context.lineWidth = 2; context.beginPath();
100 let started = false;
101 values.forEach((value, index) => {
102 if (!known(value)) { started = false; return; }
103 const x = index * (width / (HISTORY - 1)); const y = height - (value / 100) * (height - 4) - 2;
104 if (!started) { context.moveTo(x, y); started = true; } else context.lineTo(x, y);
105 });
106 context.stroke();
107 context.fillStyle = "#8ea5bf"; context.font = "10px sans-serif";
108 const last = values.at(-1);
109 context.fillText(known(last) ? `${last.toFixed(1)} %` : "—", 6, 13);
111function pushHistory(list, value) { list.push(known(value) ? value : null); while (list.length > HISTORY) list.shift(); }
113// ---------- sessions and metrics ----------
114async function loadSessions() {
115 const selected = el.sessionSelect.value;
116 let sessions = [];
117 try { sessions = (await api.sessions.list()).filter((session) => session.state === "connected"); }
118 catch (error) { setStatus(`Relace nelze načíst: ${describe(error)}`, true); }
119 el.sessionSelect.replaceChildren(new Option(sessions.length ? "Vyberte relaci…" : "Žádná připojená relace", ""));
120 for (const session of sessions) el.sessionSelect.add(new Option(`${session.label} — ${session.username}@${session.host}`, session.sessionId));
121 el.sessionSelect.value = sessions.some((session) => session.sessionId === selected) ? selected : (sessions[0]?.sessionId || "");
122 if (el.sessionSelect.value !== selected) resetSession();
123 el.refreshMetricsButton.disabled = !el.sessionSelect.value;
124 if (!sessions.length) setStatus("Nejprve se v Core připojte k serveru přes SSH.");
127function resetSession() {
128 state.generation += 1;
129 Object.assign(state, { previousNetwork: new Map(), previousPaging: null, ramHistory: [], swapHistory: [], metrics: null, vm: null });
130 el.manageVmButton.disabled = true;
133async function loadMetrics() {
134 const sessionId = el.sessionSelect.value;
135 if (!sessionId || state.busy) return;
136 const token = state.generation;
137 state.busy = true; el.refreshMetricsButton.disabled = true;
138 try {
139 const metrics = await api.system.readMetrics(sessionId);
140 if (token !== state.generation) return;
141 state.metrics = metrics;
142 renderMetrics(metrics);
143 setStatus(`Aktualizováno ${new Date(metrics.collectedAt).toLocaleTimeString("cs-CZ")}${metrics.unavailable?.length ? ` · nedostupné zdroje: ${metrics.unavailable.length}` : ""}.`);
144 } catch (error) {
145 if (token === state.generation) setStatus(describe(error), true);
146 if (parseError(error).code === "SESSION_NOT_CONNECTED") void loadSessions();
147 } finally {
148 state.busy = false; el.refreshMetricsButton.disabled = !el.sessionSelect.value;
149 }
152// Each card renders independently: one missing metric never breaks the others.
153function safely(render) { try { render(); } catch (error) { console.error("Metric render failed", error); } }
155function renderMetrics(metrics) {
156 const windows = metrics.os === "windows";
157 safely(() => {
158 el.hostname.textContent = metrics.hostname || "nedostupné";
159 el.platform.textContent = windows ? `${metrics.osName || "Windows"}${metrics.installationType ? ` (${metrics.installationType})` : ""}` : metrics.platform || "nedostupné";
160 el.kernelLabel.textContent = windows ? "Sestavení" : "Kernel";
161 el.kernel.textContent = metrics.kernel || "nedostupné";
162 el.uptime.textContent = duration(metrics.uptimeSeconds);
163 });
164 safely(() => {
165 const cpu = metrics.cpu || {};
166 const usage = known(cpu.usagePercent) ? Number(cpu.usagePercent) : null;
167 el.cpu.textContent = usage !== null ? pctText(usage) : cpu.usagePending ? "měřím…" : "nedostupné";
168 el.cpuBar.style.width = `${usage ?? 0}%`;
169 el.cpuDetail.textContent = known(cpu.logicalCores) ? `${cpu.logicalCores} logických procesorů` : "počet procesorů nedostupný";
170 el.cpuArchitecture.textContent = cpu.architecture || "neznámá architektura";
171 el.cpuModel.textContent = cpu.model || "Údaj o modelu CPU není dostupný.";
172 el.cpuTopology.textContent = `${known(cpu.logicalCores) ? cpu.logicalCores : "?"} logických · ${known(cpu.physicalCores) ? cpu.physicalCores : "nedostupné"} fyzických · ${known(cpu.sockets) ? cpu.sockets : "nedostupné"} socketů`;
173 el.cpuTypes.textContent = Array.isArray(cpu.coreTypes) && cpu.coreTypes.length ? cpu.coreTypes.map((type) => `${type.name} ${type.count}`).join(" · ") : "Server nerozlišuje typy jader.";
174 });
175 safely(() => {
176 if (metrics.loadAverageSupported === false) {
177 el.loadLabel.textContent = "Zatížení (load average)";
178 el.load.textContent = "nedostupné";
179 el.loadDetail.textContent = `Windows load average neposkytuje${known(metrics.processorQueueLength) ? ` · fronta procesoru ${metrics.processorQueueLength}` : ""}`;
180 } else {
181 el.loadLabel.textContent = "Zatížení 1 min";
182 el.load.textContent = known(metrics.load1) ? Number(metrics.load1).toLocaleString("cs-CZ", { maximumFractionDigits: 2 }) : "nedostupné";
183 el.loadDetail.textContent = "load average";
184 }
185 });
186 safely(() => {
187 const total = metrics.memory?.total; const available = metrics.memory?.available;
188 const used = known(total) && known(available) ? Math.max(0, total - available) : null;
189 const usedPercent = pct(used, total);
190 el.memory.textContent = pctText(usedPercent);
191 el.memoryBar.style.width = `${usedPercent ?? 0}%`;
192 el.memoryDetail.textContent = `${bytes(used)} / ${bytes(total)}`;
193 pushHistory(state.ramHistory, usedPercent);
194 });
195 safely(() => {
196 const swap = metrics.swap || {};
197 el.swapLabel.textContent = windows ? "Pagefile" : "Swap";
198 const swapPercent = pct(swap.used, swap.total);
199 el.swap.textContent = known(swap.total) ? (swap.total > 0 ? pctText(swapPercent) : "neaktivní") : "nedostupné";
200 el.swapBar.style.width = `${swapPercent ?? 0}%`;
201 el.swapDetail.textContent = known(swap.total) ? `${bytes(swap.used)} / ${bytes(swap.total)}` : "údaj není dostupný";
202 pushHistory(state.swapHistory, known(swap.total) ? (swap.total > 0 ? swapPercent : 0) : null);
203 });
204 safely(() => {
205 const disk = metrics.disk || {};
206 el.diskLabel.textContent = `Disk ${disk.mount || "/"}`;
207 const diskPercent = pct(disk.used, disk.total);
208 el.disk.textContent = pctText(diskPercent);
209 el.diskBar.style.width = `${diskPercent ?? 0}%`;
210 el.diskDetail.textContent = `${bytes(disk.used)} / ${bytes(disk.total)}${Array.isArray(metrics.disks) && metrics.disks.length > 1 ? ` · ${metrics.disks.length} svazků` : ""}`;
211 });
212 safely(() => renderNetwork(metrics.network, metrics.collectedAt));
213 safely(() => renderMemorySection(metrics));
216function renderNetwork(network, collectedAt) {
217 const interfaces = Array.isArray(network?.interfaces) ? network.interfaces : [];
218 el.networkInterfaces.replaceChildren();
219 const collectedMs = Date.parse(collectedAt) || Date.now();
220 const next = new Map();
221 const active = interfaces.filter((item) => item.state === "up" || item.state === "active").length;
222 el.networkSummary.textContent = interfaces.length ? `${active}/${interfaces.length} aktivní${network.defaultInterface ? ` · výchozí ${network.defaultInterface}` : ""}` : "žádná rozhraní";
223 if (!interfaces.length) el.networkInterfaces.append(h("p", { className: "empty", text: "Nebyla nalezena žádná síťová rozhraní mimo loopback." }));
224 for (const item of interfaces) {
225 const isUp = item.state === "up" || item.state === "active";
226 const previous = state.previousNetwork.get(item.name);
227 const seconds = previous ? Math.max(0, (collectedMs - previous.collectedMs) / 1000) : 0;
228 const rate = (current, before) => (previous && known(current) && known(before) && seconds > 0 && current >= before ? (current - before) / seconds : null);
229 const rxRate = rate(item.rxBytes, previous?.rxBytes); const txRate = rate(item.txBytes, previous?.txBytes);
230 const traffic = known(item.rxBytes) || known(item.txBytes)
231 ? [h("span", { className: "rx", text: `↓ ${bytes(item.rxBytes)}${rxRate !== null ? ` · ${bytes(rxRate)}/s` : ""}` }),
232 h("span", { className: "tx", text: `↑ ${bytes(item.txBytes)}${txRate !== null ? ` · ${bytes(txRate)}/s` : ""}` })]
233 : [h("span", { className: "rx", text: `↓ ${known(item.rxPackets) ? `${item.rxPackets.toLocaleString("cs-CZ")} paketů` : "nedostupné"}` }),
234 h("span", { className: "tx", text: `↑ ${known(item.txPackets) ? `${item.txPackets.toLocaleString("cs-CZ")} paketů` : "nedostupné"}` }),
235 item.bytesUnavailableReason ? h("span", { className: "mac", text: "Ovladač síťové karty nehlásí přenesené bajty." }) : null];
236 el.networkInterfaces.append(h("article", { className: "network-interface" }, [
237 h("div", { className: "interface-heading" }, [h("strong", { className: "interface-name" }, [item.name || "unknown", item.isDefault ? h("span", { className: "default-mark", text: "výchozí" }) : null]),
238 h("span", { className: `state ${isUp ? "up" : "down"}`, text: isUp ? "aktivní" : (item.state || "neznámý") })]),
239 h("p", { className: "addresses", text: Array.isArray(item.addresses) && item.addresses.length ? item.addresses.join(" · ") : "bez globální IP adresy" }),
240 item.mac ? h("p", { className: "mac", text: `MAC ${item.mac}` }) : null,
241 h("div", { className: "traffic" }, traffic)
242 ]));
243 next.set(item.name, { rxBytes: item.rxBytes, txBytes: item.txBytes, collectedMs });
244 }
245 state.previousNetwork = next;
248// ---------- memory and virtual memory section ----------
249const COLORS = { used: "#42a5ff", buffers: "#9b8cff", cached: "#43d6a0", reclaimable: "#2fb39a", free: "#2b3d52", inUse: "#42a5ff", modified: "#f2c463", standby: "#43d6a0" };
251function renderMemorySection(metrics) {
252 const windows = metrics.os === "windows";
253 const detail = metrics.memoryDetail;
254 const total = windows ? metrics.memory?.total : detail?.total ?? metrics.memory?.total;
255 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)) : "neaktivní"}`;
256 drawChart(el.ramChart, state.ramHistory, "#42a5ff");
257 drawChart(el.swapChart, state.swapHistory, "#f2c463");
258 let slices = []; let facts = []; let explain = "";
259 const rate = (value) => (known(value) ? value.toFixed(1) : "—");
260 if (windows && detail) {
261 slices = [["inUse", "Používaná", detail.slices.inUse], ["modified", "Upravená (Modified)", detail.slices.modified], ["standby", "Standby cache", detail.slices.standby], ["free", "Volná a vynulovaná", detail.slices.free]];
262 facts = [["Fyzická RAM celkem", bytes(total)], ["Skutečně využitá", `${bytes(detail.slices.inUse)} (${pctText(pct(detail.slices.inUse, total))})`], ["Dostupná", bytes(detail.available)], ["Volná", bytes(detail.slices.free)],
263 ["Standby cache", bytes(detail.slices.standby)], ["Modified page list", bytes(detail.slices.modified)], ["System Cache (rezidentní)", bytes(detail.systemCache)],
264 ["Paměť jádra", `${bytes((detail.kernel.pagedPool || 0) + (detail.kernel.nonpagedPool || 0))} (stránkovaný pool ${bytes(detail.kernel.pagedPool)}, nestránkovaný ${bytes(detail.kernel.nonpagedPool)})`],
265 ["Commit (Committed / Limit)", `${bytes(detail.commit.committed)} / ${bytes(detail.commit.limit)} (${pctText(pct(detail.commit.committed, detail.commit.limit))})`],
266 ["Stránkování in / out", `${rate(detail.paging.pagesInPerSecond)} / ${rate(detail.paging.pagesOutPerSecond)} stránek/s`]];
267 explain = "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.";
268 } else if (detail) {
269 slices = [["used", "Využitá (aplikace a jádro)", detail.used], ["buffers", "Buffers", detail.buffers], ["cached", "Cache (Cached)", detail.cached], ["reclaimable", "Uvolnitelná slab", detail.slabReclaimable], ["free", "Volná", detail.free]];
270 const kernel = (detail.slab || 0) + (detail.kernelStack || 0) + (detail.pageTables || 0);
271 facts = [["Fyzická RAM celkem", bytes(detail.total)], ["Skutečně využitá", `${bytes(detail.used)} (${pctText(pct(detail.used, detail.total))})`], ["Dostupná (MemAvailable)", bytes(detail.available)], ["Volná (MemFree)", bytes(detail.free)],
272 ["Cache / Buffers", `${bytes(detail.cached)} / ${bytes(detail.buffers)}`], ["Reclaimable (SReclaimable)", bytes(detail.slabReclaimable)], ["Sdílená (Shmem, součást Cached)", bytes(detail.shmem)],
273 ["Paměť jádra", `${bytes(kernel)} (slab ${bytes(detail.slab)}, stack ${bytes(detail.kernelStack)}, page tables ${bytes(detail.pageTables)})`],
274 ["Dirty / Writeback", `${bytes(detail.dirty)} / ${bytes(detail.writeback)}`], ["Commit (Committed_AS / CommitLimit)", `${bytes(detail.commit.committed)} / ${bytes(detail.commit.limit)}`]];
275 explain = "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í.";
276 } else {
277 facts = [["Fyzická RAM celkem", bytes(total)], ["Dostupná", bytes(metrics.memory?.available)]];
278 explain = "Podrobný rozpis paměti není z tohoto serveru dostupný (např. chybí oprávnění ke čítačům).";
279 }
280 el.ramStack.replaceChildren(...slices.map(([key, label, value]) => h("i", { title: `${label}: ${bytes(value)}`, style: { width: `${pct(value, total) ?? 0}%`, background: COLORS[key] } })));
281 el.ramLegend.replaceChildren(...slices.map(([key, label, value]) => h("li", {}, [h("b", { style: { background: COLORS[key] } }), `${label} ${bytes(value)}`])));
282 el.ramFacts.replaceChildren(...facts.map(([label, value]) => h("div", {}, [h("dt", { text: label }), h("dd", { text: value })])));
283 el.ramExplain.textContent = explain;
284 renderVirtual(metrics);
287function renderVirtual(metrics) {
288 const windows = metrics.os === "windows";
289 el.virtualTitle.textContent = windows ? "Pagefile (virtuální paměť)" : "Swap";
290 const swap = metrics.swap || {};
291 const facts = known(swap.total) && swap.total === 0
292 ? [["Stav", windows ? "žádný aktivní pagefile" : "swap není aktivní"]]
293 : [["Celková velikost", bytes(swap.total)], ["Využito", `${bytes(swap.used)} (${pctText(pct(swap.used, swap.total))})`], ["K dispozici", bytes(swap.free)]];
294 const paging = metrics.memoryDetail?.paging;
295 if (!windows && paging && known(paging.swapInPages)) {
296 const pageSize = metrics.pageSize || 4096;
297 const now = Date.parse(metrics.collectedAt) || Date.now();
298 const previous = state.previousPaging;
299 let rates = "měřím…";
300 if (previous && now > previous.time) {
301 const seconds = (now - previous.time) / 1000;
302 rates = `${bytes(((paging.swapInPages - previous.in) * pageSize) / seconds)}/s in · ${bytes(((paging.swapOutPages - previous.out) * pageSize) / seconds)}/s out`;
303 }
304 state.previousPaging = { in: paging.swapInPages, out: paging.swapOutPages, time: now };
305 facts.push(["Swap-in / swap-out", rates]);
306 }
307 if (windows && paging) facts.push(["Stránkování in / out", `${known(paging.pagesInPerSecond) ? paging.pagesInPerSecond.toFixed(1) : "—"} / ${known(paging.pagesOutPerSecond) ? paging.pagesOutPerSecond.toFixed(1) : "—"} stránek/s`]);
308 if (!windows && metrics.memoryDetail?.zswap) facts.push(["zswap (jen čtení)", metrics.memoryDetail.zswap.enabled ? `zapnut (${metrics.memoryDetail.zswap.compressor || "?"}), pool ${bytes(metrics.memoryDetail.zswapPool)}` : "vypnut"]);
309 el.swapFacts.replaceChildren(...facts.map(([label, value]) => h("div", {}, [h("dt", { text: label }), h("dd", { text: value })])));
310 const config = state.vm;
311 if (windows) {
312 el.swapHead.replaceChildren(h("tr", {}, ["Pagefile", "Alokováno", "Využito", "Maximum využití", "Nastaveno (min–max)"].map((text) => h("th", { text }))));
313 const rows = (metrics.pagefiles || []).map((file) => {
314 const setting = config?.pagefiles?.find((entry) => String(entry.name).toLowerCase() === file.name.toLowerCase());
315 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) }),
316 h("td", { text: config ? (config.automatic ? "automaticky (Windows)" : setting?.configured ? (setting.systemManagedSize ? "velikost spravuje systém" : `${bytes(setting.initial)} – ${bytes(setting.maximum)}`) : "nenastaveno") : "načte se ve správě" })]);
317 });
318 el.swapRows.replaceChildren(...(rows.length ? rows : [h("tr", {}, h("td", { colspan: 5, className: "empty", text: metrics.pagefiles ? "Žádný aktivní pagefile." : "Údaje o pagefile nejsou dostupné (WMI/CIM odepřeno)." }))]));
319 el.swapNote.textContent = config ? `${config.automatic ? "Velikost pagefile spravuje Windows automaticky." : "Ruční správa pagefile."}${config.restartPending ? " Změny čekají na restart Windows." : ""}` : "Konfigurace pagefile se načte po otevření správy.";
320 el.vmPrivilege.textContent = config ? (config.canManage ? "Relace má zvýšený token správce." : "Jen pro čtení: chybí zvýšený token správce.") : "";
321 } else {
322 el.swapHead.replaceChildren(h("tr", {}, ["Swap prostor", "Typ", "Velikost", "Využito", "Priorita", "Po restartu"].map((text) => h("th", { text }))));
323 const rows = (metrics.swapAreas || []).map((area) => {
324 const configured = config?.swapAreas?.find((entry) => entry.path === area.name);
325 return h("tr", {}, [h("td", { text: area.name }), h("td", { text: area.type === "zram" ? "zram" : area.type === "partition" ? "oddíl" : "soubor" }), h("td", { text: bytes(area.size) }),
326 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 ? "ano (fstab)" : "ne") : "—" })]);
327 });
328 el.swapRows.replaceChildren(...(rows.length ? rows : [h("tr", {}, h("td", { colspan: 6, className: "empty", text: "Swap není aktivní." }))]));
329 const zram = (metrics.memoryDetail?.zram || []).map((device) => `${device.device}: logicky ${bytes(device.diskSize)}, data ${bytes(device.originalData)} → komprimováno ${bytes(device.compressedData)}, RAM ${bytes(device.memoryUsed)}${device.algorithm ? `, ${device.algorithm}` : ""}`);
330 el.swapNote.textContent = [zram.length ? `zram (jen pro čtení): ${zram.join("; ")}` : "", config ? "" : "Trvalost a souborové systémy se načtou po otevření správy."].filter(Boolean).join(" ");
331 const mode = config?.privilege?.mode;
332 el.vmPrivilege.textContent = config ? (mode === "none" ? "Jen pro čtení: účet nemá sudo." : mode === "sudo-password" ? "Změny si vyžádají heslo pro sudo." : "Změny lze provést (root/sudo bez hesla).") : "";
333 }
334 el.manageVmButton.disabled = !el.sessionSelect.value;
337// ---------- dialogs ----------
338function runDialog(dialog, { init, submit } = {}) {
339 return new Promise((resolve) => {
340 const form = dialog.querySelector("form");
341 let finished = false;
342 const finish = (value) => {
343 if (finished) return; finished = true;
344 form?.removeEventListener("submit", onSubmit); dialog.removeEventListener("close", onClose);
345 for (const button of dialog.querySelectorAll("[data-close]")) button.removeEventListener("click", onClose);
346 if (dialog.open) dialog.close();
347 resolve(value);
348 };
349 const onSubmit = async (event) => { event.preventDefault(); const value = await submit(); if (value !== undefined) finish(value); };
350 const onClose = () => finish(null);
351 form?.addEventListener("submit", onSubmit);
352 dialog.addEventListener("close", onClose);
353 for (const button of dialog.querySelectorAll("[data-close]")) button.addEventListener("click", onClose);
354 init?.();
355 if (!dialog.open) dialog.showModal();
356 });
358function askSudo(label, error) {
359 return runDialog(el.sudoDialog, {
360 init: () => { el.sudoMessage.textContent = `Operace „${label}“ vyžaduje oprávnění správce.`; el.sudoPassword.value = ""; el.sudoError.textContent = error || ""; el.sudoError.hidden = !error; setTimeout(() => el.sudoPassword.focus(), 0); },
361 submit: () => { const value = el.sudoPassword.value; el.sudoPassword.value = ""; return value || undefined; }
362 }).finally(() => { el.sudoPassword.value = ""; });
364function confirmAction({ title, details, confirmLabel = "Potvrdit", option = "" }) {
365 return runDialog(el.confirmDialog, {
366 init: () => {
367 el.confirmTitle.textContent = title;
368 el.confirmDetails.replaceChildren(...details.map((item) => h("li", { text: item })));
369 el.confirmOptionLabel.hidden = !option; el.confirmOptionText.textContent = option; el.confirmOption.checked = false;
370 el.confirmSubmit.textContent = confirmLabel;
371 },
372 submit: () => ({ option: el.confirmOption.checked })
373 });
375function askNumber(title, label, value) {
376 return runDialog(el.confirmDialog, {
377 init: () => {
378 el.confirmTitle.textContent = title;
379 const input = h("input", { id: "numberInput", type: "number", value: String(value), "aria-label": label });
380 el.confirmDetails.replaceChildren(h("li", {}, [h("label", {}, [label, input])]));
381 el.confirmOptionLabel.hidden = true; el.confirmSubmit.textContent = "Pokračovat";
382 setTimeout(() => input.focus(), 0);
383 },
384 submit: () => Number(document.getElementById("numberInput").value)
385 });
387// Core is called without a password first; the dialog appears only when Core reports that sudo needs one.
388async function withSudo(label, invoke) {
389 let password = ""; let attempts = 0;
390 try {
391 for (;;) {
392 try { return await invoke(password ? { sudoPassword: password } : {}); }
393 catch (error) {
394 const { code } = parseError(error);
395 if ((code === "SUDO_PASSWORD_REQUIRED" || code === "SUDO_PASSWORD_INCORRECT") && attempts < 3) {
396 attempts += 1;
397 password = await askSudo(label, code === "SUDO_PASSWORD_INCORRECT" && password ? "Heslo bylo odmítnuto." : "");
398 if (!password) throw new Error("CHJ_CANCELLED: Operace byla zrušena.");
399 continue;
400 }
401 throw error;
402 }
403 }
404 } finally { password = ""; }
407// ---------- virtual memory management (never part of the monitoring loop) ----------
408async function openVirtualMemory() {
409 const sessionId = el.sessionSelect.value;
410 if (!sessionId) return;
411 el.vmContent.replaceChildren(h("p", { className: "hint", text: "Načítám konfiguraci virtuální paměti…" }));
412 el.vmStatus.textContent = "";
413 void runDialog(el.vmDialog);
414 await reloadVirtualMemory(sessionId, false);
417async function reloadVirtualMemory(sessionId, elevated) {
418 const token = state.generation;
419 try {
420 const run = (auth) => api.system.virtualMemory(sessionId, { ...auth, privileged: elevated });
421 const vm = elevated ? await withSudo("Načtení konfigurace swapu", run) : await run({});
422 if (token !== state.generation) return;
423 state.vm = vm;
424 renderVirtualMemoryDialog(sessionId, vm);
425 if (state.metrics) renderVirtual(state.metrics);
426 } catch (error) {
427 if (token === state.generation) el.vmStatus.textContent = describe(error);
428 }
431function sizeInput(id, value, unit) {
432 return h("span", { className: "inline-actions" }, [
433 h("input", { id: `${id}Value`, type: "number", min: "1", step: "1", value: String(value), "aria-label": "Velikost" }),
434 h("select", { id: `${id}Unit`, "aria-label": "Jednotka" }, [h("option", { value: "MiB", text: "MiB", selected: unit === "MiB" }), h("option", { value: "GiB", text: "GiB", selected: unit === "GiB" })])
435 ]);
437function readSize(id) {
438 const value = Number(document.getElementById(`${id}Value`).value);
439 return Number.isFinite(value) ? Math.round(value * (document.getElementById(`${id}Unit`).value === "GiB" ? 1024 : 1)) : NaN;
442// Returns { ok: true, result } or { ok: false, error } so callers can offer a safer alternative.
443async function runVmChange(sessionId, label, payload, details, option = "", { confirm = true, describeResult } = {}) {
444 if (state.vmBusy) return { ok: false };
445 if (confirm) {
446 const answer = await confirmAction({ title: label, details, confirmLabel: "Provést", option });
447 if (!answer || (option && !answer.option)) { el.vmStatus.textContent = "Požadavek nebyl odeslán – neuložená změna byla zrušena."; return { ok: false }; }
448 }
449 state.vmBusy = true;
450 el.vmStatus.textContent = `${label}… (změna běží na serveru)`;
451 for (const button of el.vmContent.querySelectorAll("button")) button.disabled = true;
452 try {
453 const result = await withSudo(label, (auth) => api.system.manageVirtualMemory(sessionId, { ...payload, ...auth }));
454 const steps = (result.steps || []).map((step) => `${step.step}: ${step.status}${step.message ? ` (${step.message})` : ""}`).join(", ");
455 el.vmStatus.textContent = describeResult ? describeResult(result) : result.restartRequired
456 ? `Uloženo – čeká na restart Windows (CH-J restart neprovádí).${result.backup ? ` Záloha: ${result.backup}` : ""}`
457 : `Provedeno a ověřeno podle skutečného stavu serveru.${steps ? ` Kroky: ${steps}.` : ""}`;
458 if (result.state) { state.vm = { ...state.vm, ...result.state }; renderVirtualMemoryDialog(sessionId, state.vm); if (state.metrics) renderVirtual(state.metrics); }
459 else await reloadVirtualMemory(sessionId, false);
460 void loadMetrics();
461 return { ok: true, result };
462 } catch (error) {
463 el.vmStatus.textContent = `Nebylo provedeno: ${describe(error)}`;
464 // Show the server's real state again after a failed change.
465 void reloadVirtualMemory(sessionId, false);
466 return { ok: false, error: parseError(error) };
467 } finally {
468 state.vmBusy = false;
469 for (const button of el.vmContent.querySelectorAll("button")) button.disabled = false;
470 }
473function renderVirtualMemoryDialog(sessionId, vm) {
474 const children = vm.platform === "windows" ? windowsDialog(sessionId, vm) : linuxDialog(sessionId, vm);
475 el.vmContent.replaceChildren(...children);
478function windowsDialog(sessionId, vm) {
479 const children = [h("p", { className: "hint", text: `Správa: ${vm.automatic === null ? "nelze zjistit" : vm.automatic ? "automatická (Windows)" : "ruční"} · Commit Limit ${bytes(vm.commit.limit)} (RAM + pagefile, nejde o velikost pagefile) · výpis paměti při pádu: ${vm.crashDumpEnabled ? "zapnut" : "vypnut"}` })];
480 if (vm.restartPending) children.push(h("p", {}, h("span", { className: "pill warn", text: `Čeká na restart: ${vm.pendingChanges.map((change) => change.description).join("; ") || "konfigurace se liší od aktivní"}` })));
481 if (!vm.canManage) children.push(h("p", { className: "error", text: vm.readOnlyReason }));
482 children.push(h("div", { className: "table-wrap" }, h("table", {}, [
483 h("thead", {}, h("tr", {}, ["Pagefile", "Aktivní", "Alokováno", "Využito / maximum", "Nastaveno", "Volné místo", ""].map((text) => h("th", { text })))),
484 h("tbody", {}, vm.pagefiles.map((file) => h("tr", {}, [h("td", { text: file.name }), h("td", { text: file.active ? "ano" : "ne (po restartu)" }), h("td", { text: bytes(file.allocated) }), h("td", { text: `${bytes(file.used)} / ${bytes(file.peak)}` }),
485 h("td", { text: file.configured ? (file.systemManagedSize ? "systémová velikost" : `${bytes(file.initial)} – ${bytes(file.maximum)}`) : vm.automatic ? "automaticky" : "—" }), h("td", { text: bytes(file.volumeFree) }),
486 h("td", {}, vm.canManage && file.configured && vm.automatic === false ? h("button", { type: "button", className: "mini danger", text: "Odebrat…", onclick: () => removePagefile(sessionId, vm, file) }) : null)])))])));
487 if (vm.canManage) {
488 if (vm.automatic === false) children.push(h("div", { className: "inline-actions" }, h("button", { type: "button", className: "button", text: "Vrátit automatickou správu Windows", onclick: () => runVmChange(sessionId, "Automatická správa pagefile", { action: "automatic" }, ["Windows bude velikost pagefile spravovat sám.", "Změna se projeví po restartu Windows; CH-J restart neprovede."]) })));
489 const ntfs = vm.volumes.filter((volume) => ["NTFS", "ReFS"].includes(volume.format));
490 children.push(h("div", { className: "vm-form" }, [h("h4", { text: vm.automatic ? "Přepnout na ruční správu a nastavit pagefile" : "Nastavit nebo přidat pagefile" }),
491 h("div", { className: "form-grid" }, [
492 h("label", {}, ["Svazek", h("select", { id: "pfDrive" }, ntfs.map((volume) => h("option", { value: volume.drive, text: `${volume.drive} (${volume.format}, volno ${bytes(volume.free)})`, selected: volume.drive === vm.systemDrive })))]),
493 h("label", {}, ["Počáteční velikost", sizeInput("pfInitial", 1024, "MiB")]),
494 h("label", {}, ["Maximální velikost", sizeInput("pfMaximum", 4096, "MiB")]),
495 h("label", { className: "check" }, [h("input", { id: "pfSystem", type: "checkbox" }), "velikost určí systém (0/0)"])
496 ]),
497 h("p", { id: "pfError", className: "error", hidden: true }),
498 h("div", { className: "inline-actions" }, h("button", { type: "button", className: "button primary", text: "Uložit (čeká na restart)", onclick: () => setPagefile(sessionId, vm) }))]));
499 }
500 children.push(h("p", { className: "hint", text: "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." }));
501 return children;
504function linuxDialog(sessionId, vm) {
505 const mode = vm.privilege?.mode;
506 const children = [h("p", { className: "hint", text: `${vm.os || "Linux"} · oprávnění: ${mode === "root" ? "root" : mode === "sudo-nopasswd" ? "sudo bez hesla" : mode === "sudo-password" ? "sudo s heslem (vyžádá se při změně)" : "bez sudo – jen čtení"} · RAM dostupná ${bytes(vm.memory.available)} · swap volný ${bytes(vm.memory.swapFree)}` })];
507 if (!vm.canManage) children.push(h("p", { className: "error", text: vm.readOnlyReason }));
508 children.push(h("div", { className: "table-wrap" }, h("table", {}, [
509 h("thead", {}, h("tr", {}, ["Swap prostor", "Typ / FS", "Velikost", "Využito", "Priorita", "Stav", "Po restartu", "Akce"].map((text) => h("th", { text })))),
510 h("tbody", {}, vm.swapAreas.length ? vm.swapAreas.map((area) => swapRow(sessionId, vm, area)) : [h("tr", {}, h("td", { colspan: 8, className: "empty", text: "Žádný swap není nakonfigurován." }))])])));
511 if (vm.zram.length) children.push(h("p", { className: "hint", text: "zram zařízení jsou v této verzi jen pro čtení." }));
512 if (vm.canManage) {
513 const supported = new Set(["ext4", "ext2/ext3", "xfs", "btrfs", "f2fs"]);
514 children.push(h("div", { className: "vm-form" }, [h("h4", { text: "Nový swapfile" }),
515 h("div", { className: "form-grid" }, [
516 h("label", {}, ["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) ? "" : " – nepodporováno"}, volno ${bytes(location.free)})` })))]),
517 h("label", {}, ["Název souboru", h("input", { id: "swapName", value: "swapfile-chj", maxlength: "48" })]),
518 h("label", {}, ["Velikost", sizeInput("swapSize", 1, "GiB")]),
519 h("label", {}, ["Priorita (prázdné = automatická, jinak 0–32767)", h("input", { id: "swapPriority", type: "number", min: "0", max: "32767", value: "", placeholder: "automatická" })]),
520 h("label", { className: "check" }, [h("input", { id: "swapPersist", type: "checkbox", checked: true }), "aktivovat i po restartu (/etc/fstab se zálohou)"])
521 ]),
522 h("p", { id: "swapError", className: "error", hidden: true }),
523 h("p", { className: "hint", text: "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." }),
524 h("div", { className: "inline-actions" }, h("button", { type: "button", className: "button primary", text: "Vytvořit a aktivovat", onclick: () => createSwap(sessionId) }))]));
525 }
526 if (!vm.root && mode === "sudo-password") children.push(h("div", { className: "inline-actions" }, h("button", { type: "button", className: "button", text: "Načíst úplnou konfiguraci (sudo)", onclick: () => reloadVirtualMemory(sessionId, true) })));
527 children.push(h("p", { className: "hint", text: "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." }));
528 return children;
531function swapRow(sessionId, vm, area) {
532 const allowed = vm.canManage ? new Set(area.allowedActions || []) : new Set();
533 const actions = [];
534 if (allowed.has("activate") && !area.active) actions.push(h("button", { type: "button", className: "mini", text: "Aktivovat", onclick: () => runVmChange(sessionId, `Aktivace ${area.path}`, { action: "activate", path: area.path, priority: "keep" }, [`swapon ${area.path} s prioritou ${area.persistentPriority ?? "automatickou"}`]) }));
535 if (allowed.has("deactivate") && area.active) actions.push(h("button", { type: "button", className: "mini", text: "Deaktivovat", onclick: () => runVmChange(sessionId, `Deaktivace ${area.path}`, { action: "deactivate", path: area.path }, [`swapoff ${area.path}: obsah (${bytes(area.used)}) se přesune do RAM nebo jiného swapu.`, "Před provedením se ověří dostupná RAM; při riziku se operace odmítne.", "Záznam v /etc/fstab zůstane beze změny."]) }));
536 if (allowed.has("priority")) actions.push(h("button", { type: "button", className: "mini", text: "Priorita…", onclick: () => changePriority(sessionId, area) }));
537 if (allowed.has("persist")) actions.push(h("button", { type: "button", className: "mini", text: area.persistent ? "Nepoužít po restartu" : "Použít po restartu", onclick: () => runVmChange(sessionId, area.persistent ? "Odebrání z /etc/fstab" : "Přidání do /etc/fstab", { action: "persist", path: area.path, persist: !area.persistent, priority: "keep" }, ["/etc/fstab bude zazálohován; ostatní řádky zůstanou beze změny."]) }));
538 if (allowed.has("resize")) actions.push(h("button", { type: "button", className: "mini", text: "Změnit velikost…", onclick: () => resizeSwap(sessionId, area) }));
539 if (allowed.has("delete")) actions.push(h("button", { type: "button", className: "mini danger", text: "Odstranit…", onclick: () => runVmChange(sessionId, `Odstranění ${area.path}`, { action: "delete", path: area.path }, [`Soubor ${area.path} bude ${area.active ? "deaktivován (s kontrolou paměti) a " : ""}smazán.`, area.persistent ? "Jeho záznam bude odebrán z /etc/fstab (se zálohou)." : "Není v /etc/fstab."]) }));
540 const management = area.management === "partition" ? "oddíl – velikost a odstranění jen ručně" : area.management === "read-only" ? "jen pro čtení" : area.management === "foreign-file" ? "soubor mimo spravované cesty – bez změny velikosti a mazání" : "";
541 return h("tr", {}, [h("td", { text: area.path }), h("td", { text: `${area.type === "zram" ? "zram" : area.type === "partition" ? "oddíl" : "soubor"}${area.fsType ? ` / ${area.fsType}` : ""}` }), h("td", { text: bytes(area.size) }),
542 h("td", { text: area.active ? bytes(area.used) : "—" }), h("td", {}, priorityCell(area)),
543 h("td", {}, h("span", { className: `pill ${area.active ? "ok" : ""}`, text: area.active ? "aktivní" : "neaktivní" })),
544 h("td", { text: area.persistent ? `ano${area.persistentByChj ? " (CH-J)" : ""}${area.systemdUnit ? ` · ${area.systemdUnit.unit}` : ""}` : "ne" }),
545 h("td", {}, [h("div", { className: "inline-actions" }, actions), management ? h("p", { className: "hint", text: management }) : null])]);
548// Effective priority comes from /proc/swaps; the saved one from pri= in /etc/fstab (null = automatic).
549// Negative values are kernel-assigned automatic priorities and are shown, never offered as input.
550function priorityLabel(value) { return value === null || value === undefined ? "—" : value < 0 ? `${value} (automatická)` : String(value); }
551function savedLabel(area) { return area.persistent ? (area.persistentPriority === null ? "automatická" : String(area.persistentPriority)) : "neuloženo (není v /etc/fstab)"; }
552function priorityCell(area) {
553 const parts = [h("span", { text: area.active ? priorityLabel(area.effectivePriority ?? area.priority) : `neaktivní · uloženo ${savedLabel(area)}` })];
554 if (area.active && area.precedence) parts.push(h("span", { className: "prio-note", text: area.sharedPriority ? `${area.precedence}. pořadí · stejná priorita – střídání` : area.precedence === 1 ? "1. pořadí – používá se přednostně" : `${area.precedence}. pořadí – záloha` }));
555 if (area.active && area.persistent !== undefined && (area.priorityPending || !area.persistent)) parts.push(h("span", { className: "prio-note", text: area.priorityPending ? `uloženo ${savedLabel(area)} – projeví se po opětovné aktivaci/restartu` : "jen do restartu (není v /etc/fstab)" }));
556 return parts;
559function askPriority(area) {
560 const custom = area.priorityMode === "custom";
561 const initial = area.persistentPriority ?? (area.active && area.priority >= 0 ? area.priority : 100);
562 return runDialog(el.priorityDialog, {
563 init: () => {
564 el.priorityFacts.replaceChildren(...[
565 `Zařízení: ${area.path}`, `Velikost: ${bytes(area.size)}`,
566 `Aktuálně aktivní priorita: ${area.active ? priorityLabel(area.effectivePriority ?? area.priority) : "swap není aktivní"}`,
567 `Uložená priorita: ${savedLabel(area)}`
568 ].map((text) => h("li", { text })));
569 el.priorityAuto.checked = !custom; el.priorityCustom.checked = custom;
570 el.priorityValue.value = String(initial); el.priorityValue.disabled = !custom;
571 el.priorityNow.checked = true; el.priorityLater.checked = false;
572 el.priorityApplyBox.hidden = !area.active;
573 el.priorityPersistLabel.hidden = area.persistent; el.priorityPersist.checked = !area.persistent;
574 el.priorityError.hidden = true; el.priorityError.textContent = "";
575 const sync = () => { el.priorityValue.disabled = !el.priorityCustom.checked; };
576 el.priorityAuto.onchange = sync; el.priorityCustom.onchange = sync;
577 setTimeout(() => (custom ? el.priorityValue : el.priorityAuto).focus(), 0);
578 },
579 submit: () => {
580 const mode = el.priorityCustom.checked ? "custom" : "auto";
581 const raw = el.priorityValue.value.trim();
582 if (mode === "custom" && !/^\d{1,5}$/.test(raw) || mode === "custom" && Number(raw) > 32767) {
583 el.priorityError.textContent = "Vlastní priorita musí být celé číslo 0–32767. Záporné hodnoty přiděluje jen Linux v automatickém režimu.";
584 el.priorityError.hidden = false; el.priorityValue.focus(); return undefined;
585 }
586 return { priority: mode === "custom" ? Number(raw) : "auto", apply: area.active && el.priorityLater.checked ? "later" : "now", persist: !area.persistent && el.priorityPersist.checked };
587 }
588 });
591function describePriorityResult(result) {
592 const saved = result.steps.find((step) => step.step === "priority-saved"), runtime = result.steps.find((step) => step.step === "priority-runtime");
593 const savedText = saved?.status === "ok" ? "Uloženo do /etc/fstab (se zálohou)." : "Do /etc/fstab neuloženo – platí jen do restartu.";
594 const runtimeText = !runtime ? "" : runtime.status === "ok" ? `Aktivní priorita je nyní ${runtime.message.replace(/^already /, "")}.` : runtime.status === "pending" ? "Na běžícím systému se projeví až při příští aktivaci nebo restartu." : "Swap není aktivní; priorita se použije při aktivaci.";
595 return `${savedText} ${runtimeText}`.trim();
598async function changePriority(sessionId, area) {
599 if (state.vmBusy) return;
600 const choice = await askPriority(area);
601 if (!choice) return;
602 const payload = { action: "priority", path: area.path, priority: choice.priority, apply: choice.apply, ...(choice.persist ? { persist: true } : {}) };
603 const label = `Priorita ${area.path}`;
604 const outcome = await runVmChange(sessionId, label, payload, [], "", { confirm: false, describeResult: describePriorityResult });
605 if (!outcome.ok && outcome.error?.code === "VM_SWAPOFF_UNSAFE" && area.persistent || !outcome.ok && outcome.error?.code === "VM_SWAPOFF_UNSAFE" && choice.persist) {
606 const answer = await confirmAction({ title: "Okamžitá změna není bezpečná", confirmLabel: "Uložit pro restart", details: [
607 "Swap nyní nelze bezpečně deaktivovat – jeho obsah by se nevešel do dostupné RAM s rezervou.", "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í).", "Běžící swap zůstane beze změny."] });
608 if (answer) await runVmChange(sessionId, label, { ...payload, apply: "later" }, [], "", { confirm: false, describeResult: describePriorityResult });
609 }
612async function resizeSwap(sessionId, area) {
613 const value = await askNumber(`Nová velikost ${area.path}`, "Velikost v MiB (64 – 65536)", Math.round((area.size || 1073741824) / 1048576));
614 if (value === null) return;
615 if (!Number.isInteger(value) || value < 64 || value > 65536) { el.vmStatus.textContent = "Neplatná velikost (64 – 65536 MiB)."; return; }
616 await runVmChange(sessionId, `Změna velikosti ${area.path}`, { action: "resize", path: area.path, sizeMiB: value, priority: "keep" }, [
617 area.active ? `Swap se deaktivuje (jen pokud ${bytes(area.used)} bezpečně přejde do RAM nebo jiného swapu), znovu vytvoří a aktivuje.` : "Soubor se znovu vytvoří v nové velikosti.",
618 "Nový soubor se nejprve sestaví vedle starého; při chybě se původní soubor znovu aktivuje.",
619 "Pokud deaktivace není bezpečná, přidejte raději nový swapfile."]);
622async function createSwap(sessionId) {
623 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);
624 const target = dir === "/" ? `/${name}` : `${dir}/${name}`;
625 let problem = "";
626 if (!/^[A-Za-z0-9._-]{1,48}$/.test(name)) problem = "Název smí obsahovat jen písmena, číslice, tečku, _ a -.";
627 else if (dir === "/" && !/^swapfile/.test(name)) problem = "V kořenovém adresáři musí název začínat „swapfile“.";
628 else if (!Number.isInteger(size) || size < 64 || size > 65536) problem = "Velikost musí být 64 MiB až 64 GiB.";
629 else if (!Number.isInteger(priority) || priority < -1 || priority > 32767) problem = "Priorita musí být prázdná (automatická) nebo celé číslo 0–32767.";
630 el.swapError.textContent = problem; el.swapError.hidden = !problem;
631 if (problem) return;
632 await runVmChange(sessionId, `Vytvoření ${target}`, { action: "create", path: target, sizeMiB: size, priority, persist: el.swapPersist.checked }, [
633 `Soubor ${target}, ${bytes(size * 1048576)}, priorita ${priority < 0 ? "automatická" : priority}.`, el.swapPersist.checked ? "Přidá se do /etc/fstab (se zálohou)." : "Nebude aktivní po restartu.", "Zápis velkého souboru může trvat několik minut."]);
636async function setPagefile(sessionId, vm) {
637 const system = el.pfSystem.checked;
638 const initial = system ? 0 : readSize("pfInitial"); const maximum = system ? 0 : readSize("pfMaximum"); const drive = el.pfDrive.value;
639 let problem = "";
640 if (!drive) problem = "Vyberte svazek NTFS.";
641 else if (!system && (!Number.isInteger(initial) || !Number.isInteger(maximum) || initial < 16 || maximum < initial)) problem = "Počáteční velikost musí být alespoň 16 MiB a maximální alespoň počáteční.";
642 el.pfError.textContent = problem; el.pfError.hidden = !problem;
643 if (problem) return;
644 await runVmChange(sessionId, `Pagefile ${drive}`, { action: "set", drive, initialMb: initial, maximumMb: maximum }, [
645 vm.automatic ? "Automatická správa Windows bude vypnuta (ruční správa)." : "Ruční správa zůstane zapnutá.",
646 system ? `Na ${drive} bude pagefile se systémem spravovanou velikostí.` : `${drive}\\pagefile.sys: ${bytes(initial * 1048576)} – ${bytes(maximum * 1048576)}.`,
647 "Konfigurace se zazálohuje a projeví po restartu Windows; CH-J restart neprovede.", "Změní se Commit Limit; příliš malý pagefile může znemožnit výpis paměti při pádu."]);
650async function removePagefile(sessionId, vm, file) {
651 const last = vm.pagefiles.filter((entry) => entry.configured).length <= 1;
652 const crash = file.drive === vm.systemDrive && Boolean(vm.crashDumpEnabled);
653 await runVmChange(sessionId, `Odebrání ${file.name}`, { action: "remove", drive: file.drive, confirmLast: last, confirmCrashDump: crash }, [
654 "Konfigurace se zazálohuje; změna se projeví po restartu Windows.",
655 last ? "Jde o POSLEDNÍ pagefile: Commit Limit klesne na velikost RAM a hrozí chyby nedostatku paměti." : "",
656 crash ? "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),
657 last || crash ? "Rozumím rizikům a výslovně potvrzuji odebrání" : "");
660// ---------- events ----------
661el.closeButton.addEventListener("click", () => api.close());
662el.refreshSessionsButton.addEventListener("click", () => { void loadSessions(); });
663el.refreshMetricsButton.addEventListener("click", () => { void loadMetrics(); });
664el.sessionSelect.addEventListener("change", () => { resetSession(); el.refreshMetricsButton.disabled = !el.sessionSelect.value; void loadMetrics(); });
665el.manageVmButton.addEventListener("click", () => { void openVirtualMemory(); });
666// Monitoring loop: metrics only, never configuration changes or administrative checks.
667setInterval(() => { if (el.autoRefresh.checked) void loadMetrics(); }, 5000);
669(async () => {
670 try { const info = await api.getInfo(); el.pluginVersion.textContent = `${info.name} v${info.version} · ${info.publisher}`; } catch {}
671 await loadSessions();
672 await loadMetrics();
673})().catch((error) => setStatus(describe(error), true));
SHA-25625b23ad9dd8876b04168907fb306659d53797b05641c33eebfc6ad4618fb8cc2

CH-J Proprietary Software License 1.14 · Kode sumber disediakan berdasarkan CH-J Proprietary Software License 1.14. Ketersediaannya tidak mengubah ketentuan lisensi atau memberikan hak tambahan.

SHA-256 arsipa9605c0870a223b6a8542bb0705edd51d8b04f3069582653417a8397fabcc09a
File