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