Fonte publicada
Transferir fonte ZIP
CH-J Server Manager
Navegue por diretórios e arquivos para uma versão específica do aplicativo.
CH-J / Server Manager
377 arquivos
core-20261019T111616Z-160a00bfcddc
Adicionado: · Construído:
1
"use strict";3
const api = window.chjPlugin;4
const el = new Proxy({}, { get: (_target, id) => document.getElementById(id) });5
const HISTORY = 60;6
const 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: false16
};18
// ---------- formatting (server data is always rendered as text) ----------19
const known = (value) => value !== null && value !== undefined && Number.isFinite(Number(value));20
function 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]}`;25
}26
const pct = (used, total) => known(used) && known(total) && total > 0 ? Math.max(0, Math.min(100, used / total * 100)) : null;27
const pctText = (value) => known(value) ? `${Number(value).toLocaleString("cs-CZ", { maximumFractionDigits: 1 })} %` : "nedostupné";28
function 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`;32
}33
function setStatus(message, error = false) { el.status.textContent = message; el.status.classList.toggle("error", error); }34
function 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;47
}48
function 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: )?/, "") };52
}53
const 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
};83
function describe(error) {84
const { code, detail } = parseError(error);85
const base = ERROR_TEXT[code];86
return base ? (base.endsWith(":") ? `${base} ${detail}` : base) : detail || code;87
}89
// ---------- charts ----------90
function 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);110
}111
function pushHistory(list, value) { list.push(known(value) ? value : null); while (list.length > HISTORY) list.shift(); }113
// ---------- sessions and metrics ----------114
async 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.");125
}127
function 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;131
}133
async 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
}150
}152
// Each card renders independently: one missing metric never breaks the others.153
function safely(render) { try { render(); } catch (error) { console.error("Metric render failed", error); } }155
function 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));214
}216
function 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;246
}248
// ---------- memory and virtual memory section ----------249
const COLORS = { used: "#42a5ff", buffers: "#9b8cff", cached: "#43d6a0", reclaimable: "#2fb39a", free: "#2b3d52", inUse: "#42a5ff", modified: "#f2c463", standby: "#43d6a0" };251
function 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);285
}287
function 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 === 0292
? [["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;335
}337
// ---------- dialogs ----------338
function 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
});357
}358
function 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 = ""; });363
}364
function 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
});374
}375
function 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
});386
}387
// Core is called without a password first; the dialog appears only when Core reports that sudo needs one.388
async 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 = ""; }405
}407
// ---------- virtual memory management (never part of the monitoring loop) ----------408
async 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);415
}417
async 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
}429
}431
function 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
]);436
}437
function 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;440
}442
// Returns { ok: true, result } or { ok: false, error } so callers can offer a safer alternative.443
async 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.restartRequired456
? `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
}471
}473
function renderVirtualMemoryDialog(sessionId, vm) {474
const children = vm.platform === "windows" ? windowsDialog(sessionId, vm) : linuxDialog(sessionId, vm);475
el.vmContent.replaceChildren(...children);476
}478
function 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;502
}504
function 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;529
}531
function 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])]);546
}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.550
function priorityLabel(value) { return value === null || value === undefined ? "—" : value < 0 ? `${value} (automatická)` : String(value); }551
function savedLabel(area) { return area.persistent ? (area.persistentPriority === null ? "automatická" : String(area.persistentPriority)) : "neuloženo (není v /etc/fstab)"; }552
function 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;557
}559
function 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
});589
}591
function 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();596
}598
async 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
}610
}612
async 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."]);620
}622
async 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."]);634
}636
async 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."]);648
}650
async 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í" : "");658
}660
// ---------- events ----------661
el.closeButton.addEventListener("click", () => api.close());662
el.refreshSessionsButton.addEventListener("click", () => { void loadSessions(); });663
el.refreshMetricsButton.addEventListener("click", () => { void loadMetrics(); });664
el.sessionSelect.addEventListener("change", () => { resetSession(); el.refreshMetricsButton.disabled = !el.sessionSelect.value; void loadMetrics(); });665
el.manageVmButton.addEventListener("click", () => { void openVirtualMemory(); });666
// Monitoring loop: metrics only, never configuration changes or administrative checks.667
setInterval(() => { 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-256
25b23ad9dd8876b04168907fb306659d53797b05641c33eebfc6ad4618fb8cc2CH-J Proprietary Software License 1.14 · A fonte é fornecida sob o CH-J Proprietary Software License 1.14. A sua disponibilidade não altera os termos licença nem concede direitos adicionais.
Ficheiro SHA-256
a9605c0870a223b6a8542bb0705edd51d8b04f3069582653417a8397fabcc09a