Published source
Download source ZIP
CH-J Server Manager
Browse directories and files for a specific application release.
Source is provided under the CH-J Proprietary Software License 1.14. Its availability does not change the license terms or grant additional rights.
1
"use strict";3
const api = window.chjCore;4
const i18n = window.chjI18n;5
const t = (key, values) => i18n.t(key, values);6
const TERMINAL_SESSION_ID = "terminal-main";7
const HASH_TOOL_ID = "chj.hash-checksum";8
if (api?.platform) document.body.classList.add(`platform-${api.platform}`);9
const state = {10
info: null,11
config: null,12
plugins: [],13
pluginState: null,14
pluginWindows: [],15
update: null,16
vault: null,17
profiles: [],18
selectedProfileId: "",19
terminalConnected: false,20
terminalStateKind: "disconnected"21
};23
const $ = (selector) => document.querySelector(selector);24
const elements = {25
viewTitle: $("#viewTitle"), runtimeText: $("#runtimeText"), sidebarVersion: $("#sidebarVersion"),26
coreApiValue: $("#coreApiValue"), dataSchemaValue: $("#dataSchemaValue"), pluginCountValue: $("#pluginCountValue"),27
currentVersion: $("#currentVersion"), updateChannel: $("#updateChannel"), updateHeadline: $("#updateHeadline"),28
updateMessage: $("#updateMessage"), updateHash: $("#updateHash"), checkUpdateButton: $("#checkUpdateButton"),29
downloadUpdateButton: $("#downloadUpdateButton"), installUpdateButton: $("#installUpdateButton"), revealUpdateButton: $("#revealUpdateButton"),30
updateReleaseRows: $("#updateReleaseRows"), updateReleaseCount: $("#updateReleaseCount"), pluginRows: $("#pluginRows"),31
pluginCatalogRows: $("#pluginCatalogRows"), pluginCatalogSource: $("#pluginCatalogSource"), checkPluginCatalogButton: $("#checkPluginCatalogButton"),32
pluginTaskbar: $("#pluginTaskbar"), pluginTaskbarItems: $("#pluginTaskbarItems"),33
updateViewChannelSelect: $("#updateViewChannelSelect"), pluginViewChannelSelect: $("#pluginViewChannelSelect"),34
settingsForm: $("#settingsForm"), languageSelect: $("#languageSelect"), channelSelect: $("#channelSelect"), pluginChannelSelect: $("#pluginChannelSelect"), autoCheckInput: $("#autoCheckInput"),35
baseUrlsText: $("#baseUrlsText"), settingsStatus: $("#settingsStatus"), lockVaultButton: $("#lockVaultButton"),36
vaultOverlay: $("#vaultOverlay"), vaultForm: $("#vaultForm"), vaultLanguageSelect: $("#vaultLanguageSelect"), vaultModeText: $("#vaultModeText"), vaultTitle: $("#vaultTitle"),37
vaultDescription: $("#vaultDescription"), vaultPassword: $("#vaultPassword"), vaultConfirmLabel: $("#vaultConfirmLabel"),38
vaultPasswordConfirm: $("#vaultPasswordConfirm"), vaultMessage: $("#vaultMessage"), vaultSubmit: $("#vaultSubmit"),39
showVaultResetButton: $("#showVaultResetButton"), vaultResetPanel: $("#vaultResetPanel"),40
vaultResetConfirmation: $("#vaultResetConfirmation"), cancelVaultResetButton: $("#cancelVaultResetButton"),41
confirmVaultResetButton: $("#confirmVaultResetButton"),42
profileRows: $("#profileRows"), profileForm: $("#profileForm"), profileId: $("#profileId"), profileLabel: $("#profileLabel"),43
profileHost: $("#profileHost"), profilePort: $("#profilePort"), profileUsername: $("#profileUsername"),44
profileAuthMethod: $("#profileAuthMethod"), privateKeyField: $("#privateKeyField"), passwordStorageField: $("#passwordStorageField"),45
profileStorePassword: $("#profileStorePassword"), profileStoredPassword: $("#profileStoredPassword"),46
profilePrivateKeyPath: $("#profilePrivateKeyPath"), selectPrivateKeyButton: $("#selectPrivateKeyButton"),47
profileMessage: $("#profileMessage"), deleteProfileButton: $("#deleteProfileButton"), newProfileButton: $("#newProfileButton"),48
terminalProfileSelect: $("#terminalProfileSelect"), credentialLabelText: $("#credentialLabelText"), terminalCredential: $("#terminalCredential"),49
connectButton: $("#connectButton"), disconnectButton: $("#disconnectButton"), terminalState: $("#terminalState"),50
terminalHost: $("#terminalHost"), terminalElement: $("#terminal")51
};53
const titles = {54
overview: "nav.overview", terminal: "terminal.title", profiles: "nav.servers",55
updates: "nav.updates", plugins: "nav.plugins", diagnostics: "nav.diagnostics", settings: "nav.settings"56
};58
let diagnosticsView = null;59
let terminal = null;60
let fitAddon = null;62
function errorText(error) {63
const code = error?.code || String(error?.message || "").match(/BIOMETRIC_[A-Z_]+/)?.[0];64
if (code?.startsWith("BIOMETRIC_")) return t(`biometrics.${code}`);65
if (["SSH_LOCAL_NETWORK_DENIED", "SSH_MAC_NETWORK_UNREACHABLE"].includes(code)) return `${t(`ssh.${code}`)}\n${error.message || ""}`;66
return error?.message || String(error || t("errors.unknown"));67
}69
let biometricStatus = null;70
function renderBiometrics() {71
const status = biometricStatus;72
$("#biometricUnlockButton").hidden = !status?.enabled || !status.available || state.vault?.unlocked || !state.vault?.initialized;73
$("#biometricUnlockButton").textContent = t(`biometrics.${status?.provider || "touch-id"}`);74
$("#biometricStatus").textContent = status ? `${status.provider}: ${t(`biometrics.${status.code}`)} · ${t(status.enabled ? "security.enabled" : "security.disabled")}` : "";75
$("#biometricProtection").textContent = status ? t(status.protection === "biometry-current-set" ? "security.strong" : "security.convenience") : "";76
$("#enableBiometricsButton").disabled = !state.vault?.unlocked || !status?.available || status.enabled;77
$("#disableBiometricsButton").disabled = !state.vault?.unlocked || !status?.enrolled;78
$("#testBiometricsButton").disabled = !state.vault?.unlocked || !status?.available;79
}80
async function refreshBiometrics() {81
try { biometricStatus = await api.getBiometricStatus(); renderBiometrics(); }82
catch (error) { $("#securityMessage").textContent = errorText(error); }83
}85
function showView(name) {86
document.querySelectorAll("[data-view]").forEach((button) => button.classList.toggle("active", button.dataset.view === name));87
document.querySelectorAll("[data-view-panel]").forEach((panel) => panel.classList.toggle("active", panel.dataset.viewPanel === name));88
elements.viewTitle.textContent = titles[name] ? t(titles[name]) : "CH-J Server Manager";89
if (name === "terminal") requestAnimationFrame(fitTerminal);90
}92
function setBusy(button, busy, label) {93
button.disabled = busy;94
if (label) button.textContent = label;95
}97
function formatBytes(bytes) {98
const units = ["B", "KB", "MB", "GB"];99
let value = Number(bytes || 0);100
let index = 0;101
while (value >= 1024 && index < units.length - 1) { value /= 1024; index += 1; }102
return `${i18n.formatNumber(value, { maximumFractionDigits: 1 })} ${units[index]}`;103
}105
function applyLanguage(language) {106
const selected = i18n.setLanguage(language);107
document.documentElement.lang = selected;108
i18n.apply(document);109
elements.languageSelect.value = selected;110
elements.vaultLanguageSelect.value = selected;111
const activeView = document.querySelector("[data-view].active")?.dataset.view || "overview";112
showView(activeView);113
if (state.update) renderUpdate(state.update);114
if (state.config) elements.updateChannel.textContent = t("updates.channel", { channel: state.config.updates.channel });115
renderPlugins();116
renderProfiles();117
diagnosticsView?.refreshLanguage();118
renderTerminalProfileSelect();119
if (state.vault) configureVault(state.vault);120
renderBiometrics();121
const terminalStateKeys = { disconnected: "terminal.disconnected", connecting: "terminal.connecting", connected: "terminal.connected", error: "terminal.connectionError" };122
setTerminalState(state.terminalStateKind, t(terminalStateKeys[state.terminalStateKind] || "terminal.disconnected"));123
return selected;124
}126
function renderUpdate(updateState) {127
state.update = updateState;128
elements.currentVersion.textContent = `v${updateState.currentVersion || state.info?.version || "0.0.1"}`;129
const release = updateState.release;130
const currentReleaseSelected = updateState.releaseRelation === "sameVersionCurrentRelease";131
elements.downloadUpdateButton.hidden = currentReleaseSelected;132
elements.downloadUpdateButton.disabled = !release || currentReleaseSelected;133
renderReleaseCatalog(updateState);134
if (updateState.downloaded) {135
elements.updateHeadline.textContent = t("updates.downloaded", { version: release?.version || "?" });136
elements.updateMessage.textContent = `${updateState.downloaded.filename} · ${formatBytes(updateState.downloaded.size)} · ${t("updates.signatureVerified")}`;137
elements.updateHash.hidden = false;138
elements.updateHash.textContent = `SHA-512 ${updateState.downloaded.sha512}`;139
elements.revealUpdateButton.hidden = false;140
elements.installUpdateButton.hidden = false;141
} else if (release) {142
const relationKey = {143
newerVersion: "updates.newerVersion",144
sameVersionNewerRelease: "updates.sameVersionNewerRelease",145
sameVersionCurrentRelease: "updates.sameVersionCurrentRelease",146
sameVersionOlderRelease: "updates.sameVersionOlderRelease",147
olderVersion: "updates.olderVersion"148
}[updateState.releaseRelation] || "updates.selected";149
elements.updateHeadline.textContent = t(relationKey, { version: release.version });150
const published = t("updates.publishedAt", { date: formatReleaseDate(release.publishedAt) });151
elements.updateMessage.textContent = release.notes ? `${published} · ${release.notes}` : published;152
elements.updateHash.hidden = false;153
elements.updateHash.textContent = `SHA-512 ${release.sha512}`;154
elements.revealUpdateButton.hidden = true;155
elements.installUpdateButton.hidden = true;156
} else {157
elements.updateHeadline.textContent = t("updates.noneAvailable");158
elements.updateMessage.textContent = t("updates.noCompatible");159
elements.updateHash.hidden = true;160
elements.revealUpdateButton.hidden = true;161
elements.installUpdateButton.hidden = true;162
}163
}165
function formatReleaseDate(value) {166
const date = new Date(value);167
if (Number.isNaN(date.getTime())) return String(value || "—");168
return new Intl.DateTimeFormat(i18n.getLocale(), { dateStyle: "medium", timeStyle: "medium" }).format(date);169
}171
function renderReleaseCatalog(updateState) {172
const releases = Array.isArray(updateState.releases) ? updateState.releases : [];173
elements.updateReleaseCount.textContent = String(releases.length);174
elements.updateReleaseRows.replaceChildren();175
if (!releases.length) {176
const empty = document.createElement("div");177
empty.className = "update-release-empty";178
empty.textContent = t(updateState.status === "idle" ? "updates.catalogBeforeCheck" : "updates.catalogEmpty");179
elements.updateReleaseRows.append(empty);180
return;181
}182
for (const release of releases) {183
const row = document.createElement("button");184
row.type = "button";185
row.className = `update-release-row${release.id === updateState.selectedReleaseId ? " selected" : ""}`;186
const version = document.createElement("strong"); version.textContent = `v${release.version}`;187
const published = document.createElement("span"); published.textContent = formatReleaseDate(release.publishedAt);188
const file = document.createElement("span"); file.textContent = `${release.filename} · ${formatBytes(release.size)}`;189
const action = document.createElement("span"); action.className = "release-select-label"; action.textContent = t(release.id === updateState.selectedReleaseId ? "updates.selectedLabel" : "updates.selectLabel");190
row.append(version, published, file, action);191
row.addEventListener("click", async () => {192
try { renderUpdate(await api.selectUpdateRelease(release.id)); }193
catch (error) { elements.updateMessage.textContent = errorText(error); }194
});195
elements.updateReleaseRows.append(row);196
}197
}199
function renderPlugins() {200
const plugins = state.plugins.filter((plugin) => plugin.id !== HASH_TOOL_ID);201
elements.pluginCountValue.textContent = String(plugins.length);202
elements.pluginRows.replaceChildren();203
if (plugins.length === 0) {204
const empty = document.createElement("div");205
empty.className = "empty-state";206
const strong = document.createElement("strong"); strong.textContent = t("plugins.none");207
const span = document.createElement("span"); span.textContent = t("plugins.first");208
empty.append(strong, span); elements.pluginRows.append(empty); renderPluginCatalog(); renderPluginTaskbar(); return;209
}210
for (const plugin of plugins) {211
const row = document.createElement("div"); row.className = "plugin-row plugin-table-row";212
const name = document.createElement("strong"); name.textContent = plugin.name;213
const version = document.createElement("span"); version.textContent = plugin.version;214
const permissions = document.createElement("span"); permissions.textContent = plugin.permissions.join(", ") || t("plugins.noPermissions");215
const actions = document.createElement("div"); actions.className = "plugin-actions";216
const open = document.createElement("button"); open.type = "button"; open.className = "button"; open.textContent = t("plugins.open");217
open.addEventListener("click", async () => { try { await api.openPlugin(plugin.id); } catch (error) { window.alert(errorText(error)); } });218
actions.append(open);219
if (plugin.bundled) {220
const bundled = document.createElement("span"); bundled.className = "release-select-label"; bundled.textContent = t("plugins.bundled"); actions.append(bundled);221
} else {222
const remove = document.createElement("button"); remove.type = "button"; remove.className = "button danger"; remove.textContent = t("plugins.remove");223
remove.addEventListener("click", async () => {224
if (!window.confirm(t("plugins.removeConfirm", { name: plugin.name }))) return;225
remove.disabled = true;226
try {227
state.pluginState = await api.uninstallPlugin(plugin.id);228
state.plugins = state.pluginState.installed;229
renderPlugins();230
} catch (error) { window.alert(errorText(error)); remove.disabled = false; }231
});232
actions.append(remove);233
}234
row.append(name, version, permissions, actions); elements.pluginRows.append(row);235
}236
renderPluginCatalog();237
renderPluginTaskbar();238
}240
function renderPluginTaskbar(windows = state.pluginWindows) {241
state.pluginWindows = Array.isArray(windows) ? windows : [];242
const minimized = state.pluginWindows.filter((item) => item.minimized);243
elements.pluginTaskbar.hidden = minimized.length === 0;244
elements.pluginTaskbarItems.replaceChildren();245
for (const plugin of minimized) {246
const button = document.createElement("button");247
button.type = "button";248
button.className = "plugin-taskbar-item";249
button.title = t("plugins.restore", { name: plugin.name });250
const icon = document.createElement("span");251
icon.className = "plugin-taskbar-icon";252
icon.textContent = String(plugin.name || plugin.pluginId).split(/\s+/).map((part) => part[0]).join("").slice(0, 2).toUpperCase();253
const name = document.createElement("strong");254
name.textContent = plugin.name;255
const action = document.createElement("small");256
action.textContent = t("plugins.restoreAction");257
button.append(icon, name, action);258
button.addEventListener("click", async () => {259
try { await api.openPlugin(plugin.pluginId); }260
catch (error) { window.alert(errorText(error)); }261
});262
elements.pluginTaskbarItems.append(button);263
}264
}266
function renderPluginCatalog() {267
if (!elements.pluginCatalogRows) return;268
const catalog = (state.pluginState?.catalog || []).filter((plugin) => plugin.id !== HASH_TOOL_ID);269
elements.pluginCatalogSource.textContent = state.pluginState?.sourceBaseUrl || "";270
elements.pluginCatalogRows.replaceChildren();271
if (!catalog.length) {272
const empty = document.createElement("div"); empty.className = "plugin-catalog-empty";273
empty.textContent = t(state.pluginState?.sourceBaseUrl ? "plugins.catalogEmpty" : "plugins.catalogBeforeCheck");274
elements.pluginCatalogRows.append(empty); return;275
}276
for (const plugin of catalog) {277
const row = document.createElement("div"); row.className = "plugin-row plugin-table-row";278
const name = document.createElement("div");279
const strong = document.createElement("strong"); strong.textContent = plugin.name;280
const notes = document.createElement("small"); notes.textContent = plugin.notes || plugin.id;281
name.append(strong, notes);282
const version = document.createElement("span"); version.textContent = `v${plugin.version} · ${plugin.channel}`;283
const permissions = document.createElement("span"); permissions.textContent = plugin.permissions.join(", ") || t("plugins.noPermissions");284
const install = document.createElement("button"); install.type = "button"; install.className = `button${plugin.installed ? "" : " primary"}`;285
install.disabled = plugin.installed;286
install.textContent = t(plugin.installed ? "plugins.installed" : plugin.updateAvailable ? "plugins.update" : "plugins.install");287
install.addEventListener("click", async () => {288
if (!window.confirm(t("plugins.installConfirm", { name: plugin.name, version: plugin.version, permissions: plugin.permissions.join(", ") || t("plugins.noPermissions") }))) return;289
install.disabled = true; install.textContent = t("plugins.installing");290
try {291
state.pluginState = await api.installPlugin(plugin.releaseId);292
state.plugins = state.pluginState.installed;293
renderPlugins();294
} catch (error) { window.alert(errorText(error)); install.disabled = false; install.textContent = t("plugins.install"); }295
});296
row.append(name, version, permissions, install); elements.pluginCatalogRows.append(row);297
}298
}300
function configureVault(status) {301
state.vault = status;302
renderBiometrics();303
$("#securityMasterPassword").value = "";304
elements.vaultOverlay.hidden = Boolean(status.unlocked);305
elements.vaultMessage.className = "form-message";306
elements.vaultMessage.textContent = "";307
elements.vaultPassword.value = "";308
elements.vaultPasswordConfirm.value = "";309
elements.vaultResetConfirmation.value = "";310
elements.vaultResetPanel.hidden = true;311
if (status.unlocked) return;312
if (status.damaged) {313
elements.vaultForm.dataset.mode = "damaged";314
elements.vaultModeText.textContent = t("vault.damagedMode");315
elements.vaultTitle.textContent = t("vault.damagedTitle");316
elements.vaultDescription.textContent = t("vault.damagedDescription");317
elements.vaultSubmit.disabled = true;318
elements.vaultConfirmLabel.hidden = true;319
elements.showVaultResetButton.hidden = false;320
return;321
}322
const setup = status.needsSetup;323
elements.vaultForm.dataset.mode = setup ? "setup" : "unlock";324
elements.vaultModeText.textContent = t(setup ? "vault.setupMode" : "vault.lockedMode");325
elements.vaultSubmit.disabled = false;326
elements.vaultConfirmLabel.hidden = !setup;327
elements.showVaultResetButton.hidden = setup;328
elements.vaultPassword.autocomplete = setup ? "new-password" : "current-password";329
elements.vaultTitle.textContent = t(setup ? "vault.setupTitle" : "vault.unlockTitle");330
elements.vaultDescription.textContent = t(setup ? "vault.setupDescription" : "vault.lockedDescription");331
elements.vaultSubmit.textContent = t(setup ? "vault.create" : "vault.unlock");332
setTimeout(() => elements.vaultPassword.focus(), 20);333
}335
function clearProtectedUi() {336
state.profiles = [];337
state.selectedProfileId = "";338
state.terminalConnected = false;339
renderProfiles();340
diagnosticsView?.refreshLanguage();341
renderTerminalProfileSelect();342
setTerminalState("disconnected", t("terminal.disconnected"));343
terminal?.reset();344
}346
async function loadProfiles(selectId = state.selectedProfileId) {347
state.profiles = await api.listProfiles();348
if (!state.profiles.some((profile) => profile.id === selectId)) selectId = state.profiles[0]?.id || "";349
state.selectedProfileId = selectId;350
renderProfiles();351
diagnosticsView?.refreshLanguage();352
renderTerminalProfileSelect();353
if (selectId) fillProfileForm(state.profiles.find((profile) => profile.id === selectId));354
else resetProfileForm();355
}357
function renderProfiles() {358
elements.profileRows.replaceChildren();359
if (!state.profiles.length) {360
const empty = document.createElement("div"); empty.className = "profile-empty";361
empty.textContent = t("profiles.none"); elements.profileRows.append(empty); return;362
}363
for (const profile of state.profiles) {364
const row = document.createElement("button"); row.type = "button";365
row.className = `profile-row${profile.id === state.selectedProfileId ? " active" : ""}`;366
const name = document.createElement("strong"); name.textContent = profile.label;367
const address = document.createElement("span"); address.textContent = `${profile.username}@${profile.host}:${profile.port}`;368
const auth = document.createElement("span"); auth.className = "profile-auth"; auth.textContent = t(profile.authMethod === "privateKey" ? "profiles.key" : "profiles.password");369
row.append(name, address, auth);370
row.addEventListener("click", () => { state.selectedProfileId = profile.id; renderProfiles(); fillProfileForm(profile); });371
elements.profileRows.append(row);372
}373
}375
function renderTerminalProfileSelect() {376
const previous = elements.terminalProfileSelect.value || state.selectedProfileId;377
elements.terminalProfileSelect.replaceChildren(new Option(t("terminal.selectServer"), ""));378
for (const profile of state.profiles) elements.terminalProfileSelect.add(new Option(`${profile.label} — ${profile.host}`, profile.id));379
elements.terminalProfileSelect.value = state.profiles.some((profile) => profile.id === previous) ? previous : (state.profiles[0]?.id || "");380
updateTerminalProfile();381
}383
function resetProfileForm() {384
elements.profileForm.reset();385
elements.profileId.value = "";386
elements.profilePort.value = "22";387
elements.profileAuthMethod.value = "password";388
elements.profilePrivateKeyPath.value = "";389
elements.profileStorePassword.checked = false;390
elements.profileStoredPassword.value = "";391
elements.profileStoredPassword.placeholder = t("profiles.storePasswordPlaceholder");392
elements.deleteProfileButton.hidden = true;393
elements.profileMessage.textContent = "";394
updatePrivateKeyField();395
}397
function fillProfileForm(profile) {398
if (!profile) { resetProfileForm(); return; }399
elements.profileId.value = profile.id;400
elements.profileLabel.value = profile.label;401
elements.profileHost.value = profile.host;402
elements.profilePort.value = String(profile.port);403
elements.profileUsername.value = profile.username;404
elements.profileAuthMethod.value = profile.authMethod;405
elements.profilePrivateKeyPath.value = profile.privateKeyPath || "";406
elements.profileStorePassword.checked = Boolean(profile.hasStoredPassword);407
elements.profileStoredPassword.value = "";408
elements.profileStoredPassword.placeholder = t(profile.hasStoredPassword ? "profiles.passwordStoredPlaceholder" : "profiles.storePasswordPlaceholder");409
elements.deleteProfileButton.hidden = false;410
elements.profileMessage.textContent = "";411
updatePrivateKeyField();412
}414
function updatePrivateKeyField() {415
const privateKey = elements.profileAuthMethod.value === "privateKey";416
elements.privateKeyField.hidden = !privateKey;417
elements.passwordStorageField.hidden = privateKey;418
elements.profileStoredPassword.disabled = privateKey || !elements.profileStorePassword.checked;419
}421
function updateTerminalProfile() {422
const profile = state.profiles.find((item) => item.id === elements.terminalProfileSelect.value);423
if (!profile) {424
elements.terminalHost.textContent = t("terminal.noProfile");425
elements.credentialLabelText.textContent = t("terminal.password");426
elements.terminalCredential.placeholder = t("terminal.sshPassword");427
return;428
}429
elements.terminalHost.textContent = `${profile.username}@${profile.host}:${profile.port} · ${profile.authMethod === "privateKey" ? profile.privateKeyPath : t("terminal.passwordAuth")}`;430
elements.credentialLabelText.textContent = t(profile.authMethod === "privateKey" ? "terminal.passphrase" : "terminal.password");431
elements.terminalCredential.placeholder = t(profile.authMethod === "privateKey" ? "terminal.keyPassphrase" : profile.hasStoredPassword ? "terminal.savedPasswordHint" : "terminal.sshPassword");432
}434
function initTerminal() {435
if (!window.Terminal || !window.FitAddon?.FitAddon) {436
elements.terminalHost.textContent = t("terminal.libraryMissing");437
return;438
}439
terminal = new window.Terminal({440
cursorBlink: true,441
fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",442
fontSize: 13,443
scrollback: 8000,444
theme: { background: "#050b12", foreground: "#d8e7f5", cursor: "#43d6a0", selectionBackground: "#264c6d" }445
});446
fitAddon = new window.FitAddon.FitAddon();447
terminal.loadAddon(fitAddon);448
terminal.open(elements.terminalElement);449
terminal.writeln("\x1b[36mCH-J Server Manager Core\x1b[0m");450
terminal.writeln(`${t("terminal.welcome")}\r\n`);451
terminal.onData((data) => { if (state.terminalConnected) api.sendTerminalInput(TERMINAL_SESSION_ID, data); });452
new ResizeObserver(() => fitTerminal()).observe(elements.terminalElement);453
fitTerminal();454
}456
function fitTerminal() {457
if (!terminal || !fitAddon || elements.terminalElement.offsetParent === null) return;458
try {459
fitAddon.fit();460
if (state.terminalConnected) void api.resizeTerminal(TERMINAL_SESSION_ID, terminal.cols, terminal.rows);461
} catch {}462
}464
function setTerminalState(kind, text) {465
state.terminalStateKind = kind;466
elements.terminalState.className = `connection-badge ${kind}`;467
elements.terminalState.textContent = text;468
if (kind !== "connected") state.terminalLatency = null;469
renderTerminalLatency();470
elements.connectButton.disabled = kind === "connecting" || kind === "connected";471
elements.disconnectButton.disabled = kind !== "connected" && kind !== "connecting";472
elements.terminalProfileSelect.disabled = kind === "connecting" || kind === "connected";473
}475
function renderTerminalLatency() {476
const element = $("#terminalLatency");477
element.hidden = state.terminalStateKind !== "connected";478
const format = (value) => typeof value === "number" && Number.isFinite(value) && value > 0479
? (value < 0.1 ? "<0.1 ms" : `${value.toFixed(1)} ms`) : `— (${t("terminal.latencyUnavailable")})`;480
element.textContent = `${t("terminal.ping")}: ${format(state.terminalLatency?.pingMs)} · ${t("terminal.sshRtt")}: ${format(state.terminalLatency?.sshRttMs)}`;481
element.title = t("terminal.latencyMeaning");482
}484
async function connectTerminal(retry = false) {485
const profile = state.profiles.find((item) => item.id === elements.terminalProfileSelect.value);486
if (!profile) { showView("profiles"); elements.profileMessage.textContent = t("terminal.noProfile"); return; }487
fitTerminal();488
if (!retry) {489
terminal?.clear();490
terminal?.writeln(`\x1b[33m${t("terminal.connectingTo", { target: `${profile.username}@${profile.host}:${profile.port}` })}\x1b[0m`);491
}492
setTerminalState("connecting", t("terminal.connecting"));493
const credential = elements.terminalCredential.value;494
const result = await api.connectSsh({495
sessionId: TERMINAL_SESSION_ID,496
profileId: profile.id,497
password: profile.authMethod === "password" ? credential : undefined,498
passphrase: profile.authMethod === "privateKey" ? credential : undefined,499
cols: terminal?.cols || 100,500
rows: terminal?.rows || 30501
});502
if (result.ok) {503
state.terminalConnected = true;504
elements.terminalCredential.value = "";505
setTerminalState("connected", t("terminal.connected"));506
terminal?.focus();507
return;508
}509
const issue = result.error || { code: "SSH_CONNECTION_FAILED", message: t("ssh.failed") };510
if (issue.code === "HOST_KEY_UNKNOWN") {511
const accepted = window.confirm(t("ssh.unknownHost", issue));512
if (accepted) {513
const trusted = await api.trustSshHostKey({ sessionId: TERMINAL_SESSION_ID, profileId: profile.id, fingerprint: issue.fingerprint });514
if (trusted.ok) { await connectTerminal(true); return; }515
terminal?.writeln(`\r\n\x1b[31m${trusted.error?.message || t("ssh.trustFailed")}\x1b[0m`);516
}517
} else if (issue.code === "HOST_KEY_MISMATCH") {518
terminal?.writeln(`\r\n\x1b[31m${t("ssh.keyChanged")}\x1b[0m`);519
terminal?.writeln(`${t("ssh.knownKey")}: ${issue.knownFingerprint}\r\n${t("ssh.newKey")}: ${issue.fingerprint}`);520
const accepted = window.confirm(t("ssh.changedHostConfirm", issue));521
if (accepted) {522
const trusted = await api.trustSshHostKey({523
sessionId: TERMINAL_SESSION_ID,524
profileId: profile.id,525
fingerprint: issue.fingerprint,526
knownFingerprint: issue.knownFingerprint,527
replaceKnown: true528
});529
if (trusted.ok) { await connectTerminal(true); return; }530
terminal?.writeln(`\r\n\x1b[31m${trusted.error?.message || t("ssh.trustFailed")}\x1b[0m`);531
}532
} else {533
terminal?.writeln(`\r\n\x1b[31m${errorText(issue)}\x1b[0m`);534
}535
state.terminalConnected = false;536
setTerminalState("error", t("terminal.connectionError"));537
}539
async function checkUpdates() {540
setBusy(elements.checkUpdateButton, true, t("updates.checking"));541
elements.updateHeadline.textContent = t("updates.checkingServer");542
elements.updateMessage.textContent = t("updates.tryingEndpoints");543
try { renderUpdate(await api.checkForUpdates()); }544
catch (error) { elements.updateHeadline.textContent = t("updates.checkFailed"); elements.updateMessage.textContent = errorText(error); }545
finally { setBusy(elements.checkUpdateButton, false, t("updates.check")); }546
}548
async function initialize() {549
if (!api || !i18n) throw new Error("Core preload or i18n API is unavailable.");550
[state.info, state.config, state.pluginState, state.update, state.vault] = await Promise.all([551
api.getInfo(), api.getConfig(), api.getPluginState(), api.getUpdateState(), api.getVaultStatus()552
]);553
state.plugins = state.pluginState.installed;554
state.pluginWindows = state.pluginState.windows || [];555
i18n.setLanguage(state.config.ui.language);556
document.documentElement.lang = i18n.getLanguage();557
i18n.apply(document);558
elements.runtimeText.textContent = `${state.info.platform} / ${state.info.arch}`;559
elements.sidebarVersion.textContent = `v${state.info.version}`;560
elements.coreApiValue.textContent = state.info.coreApi;561
elements.dataSchemaValue.textContent = String(state.info.dataSchema);562
elements.currentVersion.textContent = `v${state.info.version}`;563
elements.languageSelect.value = state.config.ui.language;564
elements.vaultLanguageSelect.value = state.config.ui.language;565
elements.vaultLanguageSelect.disabled = false;566
elements.channelSelect.value = state.config.updates.channel;567
elements.updateViewChannelSelect.value = state.config.updates.channel;568
elements.pluginChannelSelect.value = state.config.plugins.channel;569
elements.pluginViewChannelSelect.value = state.config.plugins.channel;570
elements.autoCheckInput.checked = state.config.updates.autoCheck;571
elements.baseUrlsText.textContent = state.config.updates.baseUrls.join("\n");572
elements.updateChannel.textContent = t("updates.channel", { channel: state.config.updates.channel });573
const security = state.config.security;574
$("#autoLockEnabled").checked = security.autoLockMinutes > 0;575
$("#autoLockMinutes").value = String(security.autoLockMinutes || 15);576
$("#autoLockMinutes").disabled = !security.autoLockMinutes;577
$("#lockOnBlur").checked = security.lockOnBlur;578
$("#requireSystemAuthentication").checked = security.requireSystemAuthentication;579
diagnosticsView = window.CHJ_DIAGNOSTICS.mount($("#diagnosticsView"), {580
api: api.diagnostics, language: () => i18n.getLanguage(), getTheme: () => state.config.ui.theme,581
setTheme: async (theme) => { state.config = await api.updateConfig({ ui: { theme } }); }582
});583
renderPlugins(); renderUpdate(state.update); initTerminal(); configureVault(state.vault);584
void refreshBiometrics();585
if (state.vault.unlocked) await loadProfiles();586
}588
document.querySelectorAll("[data-view]").forEach((button) => button.addEventListener("click", () => showView(button.dataset.view)));589
$("#hashToolButton").addEventListener("click", async () => {590
try { await api.openPlugin(HASH_TOOL_ID); }591
catch (error) { window.alert(errorText(error)); }592
});593
elements.vaultForm.addEventListener("submit", async (event) => {594
event.preventDefault();595
const password = elements.vaultPassword.value;596
const passwordLength = Array.from(password).length;597
if (passwordLength < 4 || passwordLength > 64) { elements.vaultMessage.textContent = t("vault.passwordLength"); return; }598
if (state.vault.needsSetup && password !== elements.vaultPasswordConfirm.value) { elements.vaultMessage.textContent = t("vault.passwordMismatch"); return; }599
setBusy(elements.vaultSubmit, true, t(state.vault.needsSetup ? "vault.creating" : "vault.unlocking"));600
try {601
const status = state.vault.needsSetup ? await api.createVault(password) : await api.unlockVault(password);602
configureVault(status);603
await loadProfiles();604
} catch (error) { elements.vaultMessage.className = "form-message"; elements.vaultMessage.textContent = errorText(error); }605
finally { if (!state.vault.unlocked) setBusy(elements.vaultSubmit, false, t(state.vault.needsSetup ? "vault.create" : "vault.unlock")); }606
});607
elements.lockVaultButton.addEventListener("click", async () => {608
configureVault(await api.lockVault());609
clearProtectedUi();610
terminal?.writeln(`${t("terminal.vaultLocked")}\r\n`);611
});612
$("#biometricUnlockButton").addEventListener("click", async event => {613
event.currentTarget.disabled = true;614
try { configureVault(await api.unlockVaultWithBiometrics()); await loadProfiles(); }615
catch (error) { elements.vaultMessage.textContent = errorText(error); }616
finally { $("#biometricUnlockButton").disabled = false; void refreshBiometrics(); }617
});618
for (const [id, action, message] of [619
["enableBiometricsButton", () => api.enableBiometrics(), "security.enabled"],620
["disableBiometricsButton", () => api.disableBiometrics(), "security.disabled"],621
["testBiometricsButton", () => api.authenticateSensitiveAction(), "security.testPassed"]622
]) {623
$("#" + id).addEventListener("click", async event => {624
event.currentTarget.disabled = true;625
try { await action(); $("#securityMessage").textContent = t(message); }626
catch (error) { $("#securityMessage").textContent = errorText(error); }627
finally { await refreshBiometrics(); }628
});629
}630
$("#autoLockEnabled").addEventListener("change", event => {631
$("#autoLockMinutes").disabled = !event.target.checked;632
if (event.target.checked && $("#autoLockMinutes").value === "0") $("#autoLockMinutes").value = "15";633
});634
$("#securityForm").addEventListener("submit", async event => {635
event.preventDefault();636
const password = $("#securityMasterPassword").value; $("#securityMasterPassword").value = "";637
try {638
state.config = await api.updateConfig({ masterPassword: password, security: {639
autoLockMinutes: $("#autoLockEnabled").checked ? Number($("#autoLockMinutes").value) : 0,640
lockOnBlur: $("#lockOnBlur").checked, requireSystemAuthentication: $("#requireSystemAuthentication").checked641
} });642
$("#securityMessage").textContent = t("settings.saved");643
} catch (error) { $("#securityMessage").textContent = errorText(error); }644
});645
elements.showVaultResetButton.addEventListener("click", () => {646
elements.vaultResetPanel.hidden = false;647
elements.showVaultResetButton.hidden = true;648
elements.vaultResetConfirmation.focus();649
});650
elements.cancelVaultResetButton.addEventListener("click", () => {651
elements.vaultResetPanel.hidden = true;652
elements.showVaultResetButton.hidden = false;653
elements.vaultResetConfirmation.value = "";654
});655
elements.confirmVaultResetButton.addEventListener("click", async () => {656
if (elements.vaultResetConfirmation.value !== "SMAZAT") {657
elements.vaultMessage.textContent = t("vault.typeDelete");658
return;659
}660
if (!window.confirm(t("vault.finalResetConfirm"))) return;661
setBusy(elements.confirmVaultResetButton, true, t("vault.deleting"));662
try {663
const status = await api.resetVault("SMAZAT");664
clearProtectedUi();665
configureVault(status);666
elements.vaultMessage.className = "form-message success";667
elements.vaultMessage.textContent = t("vault.resetDone");668
} catch (error) {669
elements.vaultMessage.className = "form-message";670
elements.vaultMessage.textContent = errorText(error);671
} finally {672
setBusy(elements.confirmVaultResetButton, false, t("vault.resetAction"));673
}674
});675
elements.newProfileButton.addEventListener("click", () => { state.selectedProfileId = ""; renderProfiles(); resetProfileForm(); elements.profileLabel.focus(); });676
elements.profileAuthMethod.addEventListener("change", updatePrivateKeyField);677
elements.profileStorePassword.addEventListener("change", updatePrivateKeyField);678
elements.selectPrivateKeyButton.addEventListener("click", async () => {679
const result = await api.selectPrivateKey();680
if (!result.canceled) elements.profilePrivateKeyPath.value = result.path;681
});682
elements.profileForm.addEventListener("submit", async (event) => {683
event.preventDefault(); elements.profileMessage.className = "form-message"; elements.profileMessage.textContent = "";684
try {685
const saved = await api.saveProfile({686
id: elements.profileId.value || undefined, label: elements.profileLabel.value, host: elements.profileHost.value,687
port: Number(elements.profilePort.value), username: elements.profileUsername.value,688
authMethod: elements.profileAuthMethod.value, privateKeyPath: elements.profilePrivateKeyPath.value,689
storePassword: elements.profileAuthMethod.value === "password" && elements.profileStorePassword.checked,690
password: elements.profileStoredPassword.value691
});692
await loadProfiles(saved.id); elements.profileMessage.className = "form-message success"; elements.profileMessage.textContent = t("profiles.saved");693
} catch (error) { elements.profileMessage.textContent = errorText(error); }694
});695
elements.deleteProfileButton.addEventListener("click", async () => {696
const id = elements.profileId.value;697
if (!id || !window.confirm(t("profiles.deleteConfirm"))) return;698
await api.deleteProfile(id); state.selectedProfileId = ""; await loadProfiles("");699
});700
elements.terminalProfileSelect.addEventListener("change", () => { elements.terminalCredential.value = ""; updateTerminalProfile(); });701
elements.connectButton.addEventListener("click", () => connectTerminal().catch((error) => { setTerminalState("error", t("terminal.error")); terminal?.writeln(`\r\n\x1b[31m${errorText(error)}\x1b[0m`); }));702
elements.disconnectButton.addEventListener("click", async () => { await api.disconnectSsh(TERMINAL_SESSION_ID); });703
elements.checkUpdateButton.addEventListener("click", checkUpdates);704
elements.downloadUpdateButton.addEventListener("click", async () => {705
setBusy(elements.downloadUpdateButton, true, t("updates.downloadingAndVerifying"));706
elements.updateHeadline.textContent = t("updates.verifyingSignature");707
try { renderUpdate(await api.downloadUpdate()); }708
catch (_error) {709
elements.updateHeadline.textContent = t("updates.downloadRejected");710
elements.updateMessage.textContent = t("updates.signatureFailure");711
elements.installUpdateButton.hidden = true;712
elements.revealUpdateButton.hidden = true;713
}714
finally { setBusy(elements.downloadUpdateButton, false, t("updates.download")); }715
});716
elements.installUpdateButton.addEventListener("click", async () => {717
const release = state.update?.release;718
if (!release || !window.confirm(t("updates.installConfirm", { version: release.version, date: formatReleaseDate(release.publishedAt) }))) return;719
setBusy(elements.installUpdateButton, true, t("updates.verifyingSignature"));720
try { await api.installUpdate(); }721
catch (_error) {722
try { renderUpdate(await api.getUpdateState()); } catch {}723
elements.updateHeadline.textContent = t("updates.installFailed");724
elements.updateMessage.textContent = t("updates.installFailure");725
elements.installUpdateButton.hidden = true;726
} finally { setBusy(elements.installUpdateButton, false, t("updates.install")); }727
});728
elements.revealUpdateButton.addEventListener("click", () => api.revealUpdate());729
elements.updateViewChannelSelect.addEventListener("change", async () => {730
state.config = await api.updateConfig({ updates: { channel: elements.updateViewChannelSelect.value } });731
elements.channelSelect.value = state.config.updates.channel;732
elements.updateChannel.textContent = t("updates.channel", { channel: state.config.updates.channel });733
await checkUpdates();734
});735
elements.pluginViewChannelSelect.addEventListener("change", async () => {736
state.config = await api.updateConfig({ plugins: { channel: elements.pluginViewChannelSelect.value } });737
elements.pluginChannelSelect.value = state.config.plugins.channel;738
elements.checkPluginCatalogButton.click();739
});740
elements.checkPluginCatalogButton.addEventListener("click", async () => {741
setBusy(elements.checkPluginCatalogButton, true, t("plugins.checking"));742
try { state.pluginState = await api.checkPluginCatalog(); state.plugins = state.pluginState.installed; renderPlugins(); }743
catch (error) { window.alert(errorText(error)); }744
finally { setBusy(elements.checkPluginCatalogButton, false, t("plugins.checkCatalog")); }745
});746
elements.settingsForm.addEventListener("submit", async (event) => {747
event.preventDefault();748
state.config = await api.updateConfig({749
ui: { language: elements.languageSelect.value },750
updates: { channel: elements.channelSelect.value, autoCheck: elements.autoCheckInput.checked },751
plugins: { channel: elements.pluginChannelSelect.value }752
});753
elements.updateViewChannelSelect.value = state.config.updates.channel;754
elements.pluginViewChannelSelect.value = state.config.plugins.channel;755
applyLanguage(state.config.ui.language);756
elements.updateChannel.textContent = t("updates.channel", { channel: state.config.updates.channel });757
elements.settingsStatus.textContent = t("settings.saved"); setTimeout(() => { elements.settingsStatus.textContent = ""; }, 1800);758
});759
elements.languageSelect.addEventListener("change", () => applyLanguage(elements.languageSelect.value));760
document.querySelectorAll("[data-legal-document]").forEach((button) => {761
button.addEventListener("click", async () => {762
try { await api.openLegalDocument(button.dataset.legalDocument); }763
catch (error) { window.alert(errorText(error)); }764
});765
});766
elements.vaultLanguageSelect.addEventListener("change", async () => {767
const language = applyLanguage(elements.vaultLanguageSelect.value);768
state.config = await api.updateConfig({ ui: { language } });769
});771
api?.onUpdateState((payload) => renderUpdate(payload));772
api?.onPluginWindowState((windows) => renderPluginTaskbar(windows));773
api?.onSshData((payload) => { if (payload.sessionId === TERMINAL_SESSION_ID) terminal?.write(payload.data); });774
api?.onSshState((payload) => {775
if (payload.sessionId !== TERMINAL_SESSION_ID) return;776
state.terminalConnected = payload.state === "connected";777
state.terminalLatency = payload.latency;778
if (payload.state === "connecting") setTerminalState("connecting", t("terminal.connecting"));779
else if (payload.state === "connected") setTerminalState("connected", t("terminal.connected"));780
else setTerminalState("disconnected", t("terminal.disconnected"));781
});782
api?.onSshError((payload) => { if (payload.sessionId === TERMINAL_SESSION_ID) terminal?.writeln(`\r\n\x1b[31m${errorText(payload)}\x1b[0m`); });783
api?.onVaultLocked(status => {784
configureVault(status); clearProtectedUi(); void refreshBiometrics();785
terminal?.writeln(`${t("terminal.vaultLocked")}\r\n`);786
});788
initialize().catch((error) => {789
elements.runtimeText.textContent = t("errors.initialization");790
elements.updateMessage.textContent = errorText(error);791
});SHA-256: af20c37a4ae10ff76c9bf8a6fc23c1ddee2f4f2a5f40ee66c2a985b9dda46ad7
Archive SHA-256: 5ac91caf4fa32a6fdb114f2430deed486fbe7489d5eea343d1f034169fafb5e0