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

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 Archivs476ed8aef0f1e96a5acdf223127b6692acdf4492eeaf5d0816b6e2a634fbbda6
Dateien