Publicerad källa
Ladda ner källa ZIP
CH-J Server Manager
Bläddra i kataloger och filer för en specifik programutgåva.
CH-J / Server Manager
339 filer
core-20261014T184258Z-3fbed0b93fdf
Tillagd: Datum inte noterat · Byggd:
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 hashToolButton = $("#hashToolButton");25
const coreNavigationButtons = [...$("#mainNavigation").querySelectorAll("[data-view]")];26
const selectedExportIds = new Set();27
let importPreview = null;29
const elements = {30
viewTitle: $("#viewTitle"), runtimeText: $("#runtimeText"), sidebarVersion: $("#sidebarVersion"),31
coreApiValue: $("#coreApiValue"), dataSchemaValue: $("#dataSchemaValue"), pluginCountValue: $("#pluginCountValue"),32
currentVersion: $("#currentVersion"), updateChannel: $("#updateChannel"), updateHeadline: $("#updateHeadline"),33
updateMessage: $("#updateMessage"), updateHash: $("#updateHash"), checkUpdateButton: $("#checkUpdateButton"),34
downloadUpdateButton: $("#downloadUpdateButton"), installUpdateButton: $("#installUpdateButton"), revealUpdateButton: $("#revealUpdateButton"),35
updateReleaseRows: $("#updateReleaseRows"), updateReleaseCount: $("#updateReleaseCount"), pluginRows: $("#pluginRows"),36
pluginCatalogRows: $("#pluginCatalogRows"), pluginCatalogSource: $("#pluginCatalogSource"), checkPluginCatalogButton: $("#checkPluginCatalogButton"),37
pluginTaskbar: $("#pluginTaskbar"), pluginTaskbarItems: $("#pluginTaskbarItems"),38
updateViewChannelSelect: $("#updateViewChannelSelect"), pluginViewChannelSelect: $("#pluginViewChannelSelect"),39
settingsForm: $("#settingsForm"), languageSelect: $("#languageSelect"), channelSelect: $("#channelSelect"), pluginChannelSelect: $("#pluginChannelSelect"), autoCheckInput: $("#autoCheckInput"),40
baseUrlsText: $("#baseUrlsText"), settingsStatus: $("#settingsStatus"), lockVaultButton: $("#lockVaultButton"),41
vaultOverlay: $("#vaultOverlay"), vaultForm: $("#vaultForm"), vaultLanguageSelect: $("#vaultLanguageSelect"), vaultModeText: $("#vaultModeText"), vaultTitle: $("#vaultTitle"),42
vaultDescription: $("#vaultDescription"), vaultPassword: $("#vaultPassword"), vaultConfirmLabel: $("#vaultConfirmLabel"),43
vaultPasswordConfirm: $("#vaultPasswordConfirm"), vaultMessage: $("#vaultMessage"), vaultSubmit: $("#vaultSubmit"),44
showVaultResetButton: $("#showVaultResetButton"), vaultResetPanel: $("#vaultResetPanel"),45
vaultResetConfirmation: $("#vaultResetConfirmation"), cancelVaultResetButton: $("#cancelVaultResetButton"),46
confirmVaultResetButton: $("#confirmVaultResetButton"),47
profileRows: $("#profileRows"), profileForm: $("#profileForm"), profileId: $("#profileId"), profileLabel: $("#profileLabel"),48
profileHost: $("#profileHost"), profilePort: $("#profilePort"), profileUsername: $("#profileUsername"),49
profileAuthMethod: $("#profileAuthMethod"), privateKeyField: $("#privateKeyField"), passwordStorageField: $("#passwordStorageField"),50
profileStorePassword: $("#profileStorePassword"), profileStoredPassword: $("#profileStoredPassword"),51
profilePrivateKeyPath: $("#profilePrivateKeyPath"), selectPrivateKeyButton: $("#selectPrivateKeyButton"),52
profileMessage: $("#profileMessage"), deleteProfileButton: $("#deleteProfileButton"), newProfileButton: $("#newProfileButton"),53
terminalProfileSelect: $("#terminalProfileSelect"), credentialLabelText: $("#credentialLabelText"), terminalCredential: $("#terminalCredential"),54
connectButton: $("#connectButton"), disconnectButton: $("#disconnectButton"), terminalState: $("#terminalState"),55
terminalHost: $("#terminalHost"), terminalElement: $("#terminal")56
};58
const titles = {59
overview: "nav.overview", terminal: "terminal.title", profiles: "nav.servers",60
updates: "nav.updates", plugins: "nav.plugins", diagnostics: "nav.diagnostics", settings: "nav.settings"61
};63
let diagnosticsView = null;64
let terminal = null;65
let fitAddon = null;67
function errorText(error) {68
const code = error?.code || String(error?.message || "").match(/BIOMETRIC_[A-Z_]+/)?.[0];69
if (code?.startsWith("BIOMETRIC_")) return t(`biometrics.${code}`);70
if (["SSH_LOCAL_NETWORK_DENIED", "SSH_MAC_NETWORK_UNREACHABLE"].includes(code)) return `${t(`ssh.${code}`)}\n${error.message || ""}`;71
return error?.message || String(error || t("errors.unknown"));72
}74
let biometricStatus = null;75
function renderBiometrics() {76
const status = biometricStatus;77
$("#biometricUnlockButton").hidden = !status?.enabled || !status.available || state.vault?.unlocked || !state.vault?.initialized;78
$("#biometricUnlockButton").textContent = t(`biometrics.${status?.provider || "touch-id"}`);79
$("#biometricStatus").textContent = status ? `${status.provider}: ${t(`biometrics.${status.code}`)} · ${t(status.enabled ? "security.enabled" : "security.disabled")}` : "";80
$("#biometricProtection").textContent = status ? t(status.protection === "biometry-current-set" ? "security.strong" : "security.convenience") : "";81
$("#enableBiometricsButton").disabled = !state.vault?.unlocked || !status?.available || status.enabled;82
$("#disableBiometricsButton").disabled = !state.vault?.unlocked || !status?.enrolled;83
$("#testBiometricsButton").disabled = !state.vault?.unlocked || !status?.available;84
}85
async function refreshBiometrics() {86
try { biometricStatus = await api.getBiometricStatus(); renderBiometrics(); }87
catch (error) { $("#securityMessage").textContent = errorText(error); }88
}90
function showView(name) {91
document.querySelectorAll("[data-view]").forEach((button) => button.classList.toggle("active", button.dataset.view === name));92
document.querySelectorAll("[data-view-panel]").forEach((panel) => panel.classList.toggle("active", panel.dataset.viewPanel === name));93
elements.viewTitle.textContent = titles[name] ? t(titles[name]) : "CH-J Server Manager";94
if (name === "terminal") requestAnimationFrame(fitTerminal);95
}97
function pluginDisplay(id) {98
return state.config?.plugins.display?.[id] || (id === HASH_TOOL_ID ? "navigation" : "visible");99
}101
let preferenceSave = Promise.resolve();102
function saveUiPreferences(patch) {103
preferenceSave = preferenceSave.then(async () => {104
state.config = await api.updateConfig(patch());105
renderPlugins();106
}).catch((error) => { window.alert(errorText(error)); renderPlugins(); });107
return preferenceSave;108
}110
function moveNavigation(key, targetKey, after = false) {111
if (!$("#navigationEditToggle").checked) return;112
const keys = [...$("#mainNavigation").children].map((row) => row.dataset.navKey);113
const from = keys.indexOf(key), to = keys.indexOf(targetKey);114
if (from < 0 || to < 0 || from === to) return;115
keys.splice(from, 1); keys.splice(keys.indexOf(targetKey) + Number(after), 0, key);116
saveUiPreferences(() => ({ ui: { navigationOrder: [...keys, ...state.config.ui.navigationOrder.filter((id) => !keys.includes(id))] } }))117
.then(() => [...$("#mainNavigation").children].find((row) => row.dataset.navKey === key)?.querySelector(".nav-button").focus());118
}120
function renderNavigation() {121
const navigation = $("#mainNavigation");122
const buttons = new Map(coreNavigationButtons.map((button) => [button.dataset.view, button]));123
if (pluginDisplay(HASH_TOOL_ID) === "navigation") buttons.set(`plugin:${HASH_TOOL_ID}`, hashToolButton);124
for (const plugin of state.plugins) {125
if (plugin.id === HASH_TOOL_ID || pluginDisplay(plugin.id) !== "navigation") continue;126
const button = document.createElement("button"); button.type = "button"; button.className = "nav-button";127
const icon = document.createElement("span"); icon.textContent = "◇";128
const label = document.createElement("span"); label.className = "nav-label"; label.textContent = plugin.name;129
button.append(icon, label);130
button.addEventListener("click", async () => { try { await api.openPlugin(plugin.id); } catch (error) { window.alert(errorText(error)); } });131
buttons.set(`plugin:${plugin.id}`, button);132
}133
hashToolButton.querySelector(".nav-label").textContent = t("nav.hash");134
const defaults = [...coreNavigationButtons.map((button) => button.dataset.view)];135
defaults.splice(defaults.indexOf("settings"), 0, `plugin:${HASH_TOOL_ID}`);136
const order = [...new Set([...(state.config?.ui.navigationOrder || []), ...defaults, ...buttons.keys()])].filter((key) => buttons.has(key));137
navigation.replaceChildren();138
const editing = $("#navigationEditToggle").checked;139
navigation.classList.toggle("navigation-editing", editing);140
order.forEach((key) => {141
const row = document.createElement("div"); row.className = "navigation-row"; row.dataset.navKey = key;142
const button = buttons.get(key);143
button.title = editing ? t("nav.drag") : "";144
row.draggable = editing;145
const handle = document.createElement("span"); handle.className = "navigation-drag"; handle.textContent = "⠿"; handle.setAttribute("aria-hidden", "true");146
row.addEventListener("dragstart", (event) => {147
if (!$("#navigationEditToggle").checked) { event.preventDefault(); return; }148
event.dataTransfer.setData("text/chj-navigation", key); event.dataTransfer.effectAllowed = "move";149
row.classList.add("navigation-dragging");150
});151
row.addEventListener("dragend", () => navigation.querySelectorAll(".navigation-row").forEach((item) => item.classList.remove("navigation-dragging", "navigation-drop-before", "navigation-drop-after")));152
row.addEventListener("dragover", (event) => {153
if (!$("#navigationEditToggle").checked || ![...event.dataTransfer.types].includes("text/chj-navigation")) return;154
event.preventDefault(); event.dataTransfer.dropEffect = "move";155
const after = event.clientY >= row.getBoundingClientRect().top + row.offsetHeight / 2;156
row.classList.toggle("navigation-drop-before", !after); row.classList.toggle("navigation-drop-after", after);157
});158
row.addEventListener("dragleave", () => row.classList.remove("navigation-drop-before", "navigation-drop-after"));159
row.addEventListener("drop", (event) => {160
if (!$("#navigationEditToggle").checked) return;161
event.preventDefault(); row.classList.remove("navigation-drop-before", "navigation-drop-after");162
moveNavigation(event.dataTransfer.getData("text/chj-navigation"), key, event.clientY >= row.getBoundingClientRect().top + row.offsetHeight / 2);163
});164
row.append(button, handle); navigation.append(row);165
});166
}168
$("#navigationEditToggle").addEventListener("change", renderNavigation);169
$("#mainNavigation").addEventListener("click", (event) => {170
if ($("#navigationEditToggle").checked) { event.preventDefault(); event.stopPropagation(); }171
}, true);173
function cancelProfileImport() {174
importPreview = null;175
$("#profileImportDialog").close();176
$("#profileImportRows").replaceChildren();177
void api.cancelProfileImport().catch(() => {});178
}180
$("#importProfilesButton").addEventListener("click", async (event) => {181
const button = event.currentTarget; button.disabled = true;182
$("#profileTransferMessage").textContent = "";183
try {184
const preview = await api.previewProfileImport();185
if (preview.canceled) return;186
importPreview = preview;187
$("#profileImportRows").replaceChildren();188
for (const server of preview.servers) {189
const row = document.createElement("tr");190
for (const value of [server.name, server.host, server.username, server.port, server.duplicate ? t("transfer.yes") : "—"]) {191
const cell = document.createElement("td"); cell.textContent = value; row.append(cell);192
}193
$("#profileImportRows").append(row);194
}195
$("#profileImportSummary").textContent = t("transfer.summary", { count: preview.servers.length, duplicates: preview.servers.filter((server) => server.duplicate).length });196
$("#profileDuplicatePolicy").value = "skip";197
$("#profileImportMessage").textContent = "";198
$("#profileImportDialog").showModal();199
} catch (error) { $("#profileTransferMessage").textContent = errorText(error); }200
finally { button.disabled = false; }201
});202
$("#cancelProfileImport").addEventListener("click", cancelProfileImport);203
$("#profileImportDialog").addEventListener("cancel", (event) => { event.preventDefault(); cancelProfileImport(); });204
$("#confirmProfileImport").addEventListener("click", async (event) => {205
if (!importPreview) return;206
const button = event.currentTarget; button.disabled = true;207
try {208
const result = await api.commitProfileImport(importPreview.token, $("#profileDuplicatePolicy").value);209
cancelProfileImport();210
await loadProfiles();211
$("#profileTransferMessage").textContent = t("transfer.imported", result);212
} catch (error) { $("#profileImportMessage").textContent = errorText(error); }213
finally { button.disabled = false; }214
});215
$("#exportProfilesButton").addEventListener("click", async (event) => {216
const button = event.currentTarget; button.disabled = true;217
try {218
const result = await api.exportProfiles($("#profileExportFormat").value, $("#exportSelectedProfiles").checked ? [...selectedExportIds] : undefined);219
if (!result.canceled) $("#profileTransferMessage").textContent = t("transfer.exported", result);220
} catch (error) { $("#profileTransferMessage").textContent = errorText(error); }221
finally { button.disabled = false; }222
});224
function setBusy(button, busy, label) {225
button.disabled = busy;226
if (label) button.textContent = label;227
}229
function formatBytes(bytes) {230
const units = ["B", "KB", "MB", "GB"];231
let value = Number(bytes || 0);232
let index = 0;233
while (value >= 1024 && index < units.length - 1) { value /= 1024; index += 1; }234
return `${i18n.formatNumber(value, { maximumFractionDigits: 1 })} ${units[index]}`;235
}237
function applyLanguage(language) {238
const selected = i18n.setLanguage(language);239
document.documentElement.lang = selected;240
i18n.apply(document);241
elements.languageSelect.value = selected;242
elements.vaultLanguageSelect.value = selected;243
const activeView = document.querySelector("[data-view].active")?.dataset.view || "overview";244
showView(activeView);245
if (state.update) renderUpdate(state.update);246
if (state.config) elements.updateChannel.textContent = t("updates.channel", { channel: state.config.updates.channel });247
renderPlugins();248
renderProfiles();249
diagnosticsView?.refreshLanguage();250
renderTerminalProfileSelect();251
if (state.vault) configureVault(state.vault);252
renderBiometrics();253
const terminalStateKeys = { disconnected: "terminal.disconnected", connecting: "terminal.connecting", connected: "terminal.connected", error: "terminal.connectionError" };254
setTerminalState(state.terminalStateKind, t(terminalStateKeys[state.terminalStateKind] || "terminal.disconnected"));255
return selected;256
}258
function renderUpdate(updateState) {259
state.update = updateState;260
elements.currentVersion.textContent = `v${updateState.currentVersion || state.info?.version || "0.0.1"}`;261
const release = updateState.release;262
const currentReleaseSelected = updateState.releaseRelation === "sameVersionCurrentRelease";263
elements.downloadUpdateButton.hidden = currentReleaseSelected;264
elements.downloadUpdateButton.disabled = !release || currentReleaseSelected;265
renderReleaseCatalog(updateState);266
if (updateState.downloaded) {267
elements.updateHeadline.textContent = t("updates.downloaded", { version: release?.version || "?" });268
elements.updateMessage.textContent = `${updateState.downloaded.filename} · ${formatBytes(updateState.downloaded.size)} · ${t("updates.signatureVerified")}`;269
elements.updateHash.hidden = false;270
elements.updateHash.textContent = `SHA-512 ${updateState.downloaded.sha512}`;271
elements.revealUpdateButton.hidden = false;272
elements.installUpdateButton.hidden = false;273
} else if (release) {274
const relationKey = {275
newerVersion: "updates.newerVersion",276
sameVersionNewerRelease: "updates.sameVersionNewerRelease",277
sameVersionCurrentRelease: "updates.sameVersionCurrentRelease",278
sameVersionOlderRelease: "updates.sameVersionOlderRelease",279
olderVersion: "updates.olderVersion"280
}[updateState.releaseRelation] || "updates.selected";281
elements.updateHeadline.textContent = t(relationKey, { version: release.version });282
const published = t("updates.publishedAt", { date: formatReleaseDate(release.publishedAt) });283
elements.updateMessage.textContent = release.notes ? `${published} · ${release.notes}` : published;284
elements.updateHash.hidden = false;285
elements.updateHash.textContent = `SHA-512 ${release.sha512}`;286
elements.revealUpdateButton.hidden = true;287
elements.installUpdateButton.hidden = true;288
} else {289
elements.updateHeadline.textContent = t("updates.noneAvailable");290
elements.updateMessage.textContent = t("updates.noCompatible");291
elements.updateHash.hidden = true;292
elements.revealUpdateButton.hidden = true;293
elements.installUpdateButton.hidden = true;294
}295
}297
function formatReleaseDate(value) {298
const date = new Date(value);299
if (Number.isNaN(date.getTime())) return String(value || "—");300
return new Intl.DateTimeFormat(i18n.getLocale(), { dateStyle: "medium", timeStyle: "medium" }).format(date);301
}303
function renderReleaseCatalog(updateState) {304
const releases = Array.isArray(updateState.releases) ? updateState.releases : [];305
elements.updateReleaseCount.textContent = String(releases.length);306
elements.updateReleaseRows.replaceChildren();307
if (!releases.length) {308
const empty = document.createElement("div");309
empty.className = "update-release-empty";310
empty.textContent = t(updateState.status === "idle" ? "updates.catalogBeforeCheck" : "updates.catalogEmpty");311
elements.updateReleaseRows.append(empty);312
return;313
}314
for (const release of releases) {315
const row = document.createElement("button");316
row.type = "button";317
row.className = `update-release-row${release.id === updateState.selectedReleaseId ? " selected" : ""}`;318
const version = document.createElement("strong"); version.textContent = `v${release.version}`;319
const published = document.createElement("span"); published.textContent = formatReleaseDate(release.publishedAt);320
const file = document.createElement("span"); file.textContent = `${release.filename} · ${formatBytes(release.size)}`;321
const action = document.createElement("span"); action.className = "release-select-label"; action.textContent = t(release.id === updateState.selectedReleaseId ? "updates.selectedLabel" : "updates.selectLabel");322
row.append(version, published, file, action);323
row.addEventListener("click", async () => {324
try { renderUpdate(await api.selectUpdateRelease(release.id)); }325
catch (error) { elements.updateMessage.textContent = errorText(error); }326
});327
elements.updateReleaseRows.append(row);328
}329
}331
function renderPlugins() {332
const plugins = state.plugins;333
renderNavigation();334
elements.pluginCountValue.textContent = String(plugins.length);335
elements.pluginRows.replaceChildren();336
if (plugins.length === 0) {337
const empty = document.createElement("div");338
empty.className = "empty-state";339
const strong = document.createElement("strong"); strong.textContent = t("plugins.none");340
const span = document.createElement("span"); span.textContent = t("plugins.first");341
empty.append(strong, span); elements.pluginRows.append(empty); renderPluginCatalog(); renderPluginTaskbar(); return;342
}343
for (const plugin of plugins) {344
const row = document.createElement("div"); row.className = "plugin-row installed-plugin-row"; row.dataset.pluginId = plugin.id;345
const name = document.createElement("div"); name.className = "installed-plugin-name";346
const title = document.createElement("strong"); title.textContent = plugin.name; name.append(title);347
const details = document.createElement("details"); details.className = "installed-plugin-details";348
const summary = document.createElement("summary"); summary.textContent = t("plugins.details");349
const description = document.createElement("p"); description.textContent = plugin.description;350
const permissions = document.createElement("p"); permissions.textContent = `${t("plugins.permissions")}: ${plugin.permissions.join(", ") || t("plugins.noPermissions")}`;351
details.append(summary, description, permissions); name.append(details);352
const version = document.createElement("span"); version.textContent = plugin.version;353
const actions = document.createElement("div"); actions.className = "plugin-actions";354
const open = document.createElement("button"); open.type = "button"; open.className = "button"; open.textContent = t("plugins.open");355
open.addEventListener("click", async () => { try { await api.openPlugin(plugin.id); } catch (error) { window.alert(errorText(error)); } });356
const mode = pluginDisplay(plugin.id);357
row.classList.toggle("plugin-hidden", mode === "hidden");358
open.disabled = mode === "hidden";359
const visible = document.createElement("input"); visible.type = "checkbox"; visible.className = "plugin-visibility"; visible.checked = mode !== "hidden";360
visible.setAttribute("aria-label", `${plugin.name}: ${t("plugins.show")}`);361
const pinned = document.createElement("input"); pinned.type = "checkbox"; pinned.className = "plugin-pin"; pinned.checked = mode === "navigation"; pinned.disabled = mode === "hidden";362
pinned.setAttribute("aria-label", `${plugin.name}: ${t("plugins.inMenu")}`);363
const saveDisplay = (value) => {364
visible.disabled = true; pinned.disabled = true;365
saveUiPreferences(() => ({ plugins: { display: { ...state.config.plugins.display, [plugin.id]: value } } }));366
};367
visible.addEventListener("change", () => saveDisplay(visible.checked ? "visible" : "hidden"));368
pinned.addEventListener("change", () => saveDisplay(pinned.checked ? "navigation" : "visible"));369
actions.append(open);370
if (plugin.bundled) {371
version.title = t("plugins.bundled");372
} else {373
const remove = document.createElement("button"); remove.type = "button"; remove.className = "button danger"; remove.textContent = t("plugins.remove");374
remove.addEventListener("click", async () => {375
if (!window.confirm(t("plugins.removeConfirm", { name: plugin.name }))) return;376
remove.disabled = true;377
try {378
state.pluginState = await api.uninstallPlugin(plugin.id);379
state.plugins = state.pluginState.installed;380
renderPlugins();381
} catch (error) { window.alert(errorText(error)); remove.disabled = false; }382
});383
actions.append(remove);384
}385
row.append(name, version, visible, pinned, actions); elements.pluginRows.append(row);386
}387
renderPluginCatalog();388
renderPluginTaskbar();389
}391
function renderPluginTaskbar(windows = state.pluginWindows) {392
state.pluginWindows = Array.isArray(windows) ? windows : [];393
const minimized = state.pluginWindows.filter((item) => item.minimized);394
elements.pluginTaskbar.hidden = minimized.length === 0;395
elements.pluginTaskbarItems.replaceChildren();396
for (const plugin of minimized) {397
const button = document.createElement("button");398
button.type = "button";399
button.className = "plugin-taskbar-item";400
button.title = t("plugins.restore", { name: plugin.name });401
const icon = document.createElement("span");402
icon.className = "plugin-taskbar-icon";403
icon.textContent = String(plugin.name || plugin.pluginId).split(/\s+/).map((part) => part[0]).join("").slice(0, 2).toUpperCase();404
const name = document.createElement("strong");405
name.textContent = plugin.name;406
const action = document.createElement("small");407
action.textContent = t("plugins.restoreAction");408
button.append(icon, name, action);409
button.addEventListener("click", async () => {410
try { await api.openPlugin(plugin.pluginId); }411
catch (error) { window.alert(errorText(error)); }412
});413
elements.pluginTaskbarItems.append(button);414
}415
}417
function renderPluginCatalog() {418
if (!elements.pluginCatalogRows) return;419
const catalog = (state.pluginState?.catalog || []).filter((plugin) => plugin.id !== HASH_TOOL_ID);420
elements.pluginCatalogSource.textContent = state.pluginState?.sourceBaseUrl || "";421
elements.pluginCatalogRows.replaceChildren();422
if (!catalog.length) {423
const empty = document.createElement("div"); empty.className = "plugin-catalog-empty";424
empty.textContent = t(state.pluginState?.sourceBaseUrl ? "plugins.catalogEmpty" : "plugins.catalogBeforeCheck");425
elements.pluginCatalogRows.append(empty); return;426
}427
for (const plugin of catalog) {428
const row = document.createElement("div"); row.className = "plugin-row plugin-table-row";429
const name = document.createElement("div");430
const strong = document.createElement("strong"); strong.textContent = plugin.name;431
const notes = document.createElement("small"); notes.textContent = plugin.notes || plugin.id;432
name.append(strong, notes);433
const version = document.createElement("span"); version.textContent = `v${plugin.version} · ${plugin.channel}`;434
const permissions = document.createElement("span"); permissions.textContent = plugin.permissions.join(", ") || t("plugins.noPermissions");435
const install = document.createElement("button"); install.type = "button"; install.className = `button${plugin.installed ? "" : " primary"}`;436
install.disabled = plugin.installed;437
install.textContent = t(plugin.installed ? "plugins.installed" : plugin.updateAvailable ? "plugins.update" : "plugins.install");438
install.addEventListener("click", async () => {439
if (!window.confirm(t("plugins.installConfirm", { name: plugin.name, version: plugin.version, permissions: plugin.permissions.join(", ") || t("plugins.noPermissions") }))) return;440
install.disabled = true; install.textContent = t("plugins.installing");441
try {442
state.pluginState = await api.installPlugin(plugin.releaseId);443
state.plugins = state.pluginState.installed;444
renderPlugins();445
} catch (error) { window.alert(errorText(error)); install.disabled = false; install.textContent = t("plugins.install"); }446
});447
row.append(name, version, permissions, install); elements.pluginCatalogRows.append(row);448
}449
}451
function configureVault(status) {452
state.vault = status;453
renderBiometrics();454
$("#securityMasterPassword").value = "";455
elements.vaultOverlay.hidden = Boolean(status.unlocked);456
elements.vaultMessage.className = "form-message";457
elements.vaultMessage.textContent = "";458
elements.vaultPassword.value = "";459
elements.vaultPasswordConfirm.value = "";460
elements.vaultResetConfirmation.value = "";461
elements.vaultResetPanel.hidden = true;462
if (status.unlocked) return;463
if (status.damaged) {464
elements.vaultForm.dataset.mode = "damaged";465
elements.vaultModeText.textContent = t("vault.damagedMode");466
elements.vaultTitle.textContent = t("vault.damagedTitle");467
elements.vaultDescription.textContent = t("vault.damagedDescription");468
elements.vaultSubmit.disabled = true;469
elements.vaultConfirmLabel.hidden = true;470
elements.showVaultResetButton.hidden = false;471
return;472
}473
const setup = status.needsSetup;474
elements.vaultForm.dataset.mode = setup ? "setup" : "unlock";475
elements.vaultModeText.textContent = t(setup ? "vault.setupMode" : "vault.lockedMode");476
elements.vaultSubmit.disabled = false;477
elements.vaultConfirmLabel.hidden = !setup;478
elements.showVaultResetButton.hidden = setup;479
elements.vaultPassword.autocomplete = setup ? "new-password" : "current-password";480
elements.vaultTitle.textContent = t(setup ? "vault.setupTitle" : "vault.unlockTitle");481
elements.vaultDescription.textContent = t(setup ? "vault.setupDescription" : "vault.lockedDescription");482
elements.vaultSubmit.textContent = t(setup ? "vault.create" : "vault.unlock");483
setTimeout(() => elements.vaultPassword.focus(), 20);484
}486
function clearProtectedUi() {487
$("#navigationEditToggle").checked = false;488
renderNavigation();489
cancelProfileImport();490
selectedExportIds.clear();491
state.profiles = [];492
state.selectedProfileId = "";493
state.terminalConnected = false;494
renderProfiles();495
diagnosticsView?.refreshLanguage();496
renderTerminalProfileSelect();497
setTerminalState("disconnected", t("terminal.disconnected"));498
terminal?.reset();499
}501
async function loadProfiles(selectId = state.selectedProfileId) {502
state.profiles = await api.listProfiles();503
if (!state.profiles.some((profile) => profile.id === selectId)) selectId = state.profiles[0]?.id || "";504
state.selectedProfileId = selectId;505
renderProfiles();506
diagnosticsView?.refreshLanguage();507
renderTerminalProfileSelect();508
if (selectId) fillProfileForm(state.profiles.find((profile) => profile.id === selectId));509
else resetProfileForm();510
}512
function renderProfiles() {513
for (const id of selectedExportIds) if (!state.profiles.some((profile) => profile.id === id)) selectedExportIds.delete(id);514
elements.profileRows.replaceChildren();515
if (!state.profiles.length) {516
const empty = document.createElement("div"); empty.className = "profile-empty";517
empty.textContent = t("profiles.none"); elements.profileRows.append(empty); return;518
}519
for (const profile of state.profiles) {520
const row = document.createElement("button"); row.type = "button";521
row.className = `profile-row${profile.id === state.selectedProfileId ? " active" : ""}`;522
const name = document.createElement("strong"); name.textContent = profile.label;523
const address = document.createElement("span"); address.textContent = `${profile.username}@${profile.host}:${profile.port}`;524
const auth = document.createElement("span"); auth.className = "profile-auth"; auth.textContent = t(profile.authMethod === "privateKey" ? "profiles.key" : "profiles.password");525
row.append(name, address, auth);526
row.addEventListener("click", () => { state.selectedProfileId = profile.id; renderProfiles(); fillProfileForm(profile); });527
const wrapper = document.createElement("div"); wrapper.className = "profile-select-row";528
const select = document.createElement("input"); select.type = "checkbox"; select.checked = selectedExportIds.has(profile.id);529
select.setAttribute("aria-label", t("transfer.select", { name: profile.label }));530
select.addEventListener("change", () => { if (select.checked) selectedExportIds.add(profile.id); else selectedExportIds.delete(profile.id); });531
wrapper.append(select, row); elements.profileRows.append(wrapper);532
}533
}535
function renderTerminalProfileSelect() {536
const previous = elements.terminalProfileSelect.value || state.selectedProfileId;537
elements.terminalProfileSelect.replaceChildren(new Option(t("terminal.selectServer"), ""));538
for (const profile of state.profiles) elements.terminalProfileSelect.add(new Option(`${profile.label} — ${profile.host}`, profile.id));539
elements.terminalProfileSelect.value = state.profiles.some((profile) => profile.id === previous) ? previous : (state.profiles[0]?.id || "");540
updateTerminalProfile();541
}543
function resetProfileForm() {544
elements.profileForm.reset();545
elements.profileId.value = "";546
elements.profilePort.value = "22";547
elements.profileAuthMethod.value = "password";548
elements.profilePrivateKeyPath.value = "";549
elements.profileStorePassword.checked = false;550
elements.profileStoredPassword.value = "";551
elements.profileStoredPassword.placeholder = t("profiles.storePasswordPlaceholder");552
elements.deleteProfileButton.hidden = true;553
elements.profileMessage.textContent = "";554
updatePrivateKeyField();555
}557
function fillProfileForm(profile) {558
if (!profile) { resetProfileForm(); return; }559
elements.profileId.value = profile.id;560
elements.profileLabel.value = profile.label;561
elements.profileHost.value = profile.host;562
elements.profilePort.value = String(profile.port);563
elements.profileUsername.value = profile.username;564
elements.profileAuthMethod.value = profile.authMethod;565
elements.profilePrivateKeyPath.value = profile.privateKeyPath || "";566
elements.profileStorePassword.checked = Boolean(profile.hasStoredPassword);567
elements.profileStoredPassword.value = "";568
elements.profileStoredPassword.placeholder = t(profile.hasStoredPassword ? "profiles.passwordStoredPlaceholder" : "profiles.storePasswordPlaceholder");569
elements.deleteProfileButton.hidden = false;570
elements.profileMessage.textContent = "";571
updatePrivateKeyField();572
}574
function updatePrivateKeyField() {575
const privateKey = elements.profileAuthMethod.value === "privateKey";576
elements.privateKeyField.hidden = !privateKey;577
elements.passwordStorageField.hidden = privateKey;578
elements.profileStoredPassword.disabled = privateKey || !elements.profileStorePassword.checked;579
}581
function updateTerminalProfile() {582
const profile = state.profiles.find((item) => item.id === elements.terminalProfileSelect.value);583
if (!profile) {584
elements.terminalHost.textContent = t("terminal.noProfile");585
elements.credentialLabelText.textContent = t("terminal.password");586
elements.terminalCredential.placeholder = t("terminal.sshPassword");587
return;588
}589
elements.terminalHost.textContent = `${profile.username}@${profile.host}:${profile.port} · ${profile.authMethod === "privateKey" ? profile.privateKeyPath : t("terminal.passwordAuth")}`;590
elements.credentialLabelText.textContent = t(profile.authMethod === "privateKey" ? "terminal.passphrase" : "terminal.password");591
elements.terminalCredential.placeholder = t(profile.authMethod === "privateKey" ? "terminal.keyPassphrase" : profile.hasStoredPassword ? "terminal.savedPasswordHint" : "terminal.sshPassword");592
}594
function initTerminal() {595
if (!window.Terminal || !window.FitAddon?.FitAddon) {596
elements.terminalHost.textContent = t("terminal.libraryMissing");597
return;598
}599
terminal = new window.Terminal({600
cursorBlink: true,601
fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",602
fontSize: 13,603
scrollback: 8000,604
theme: { background: "#050b12", foreground: "#d8e7f5", cursor: "#43d6a0", selectionBackground: "#264c6d" }605
});606
fitAddon = new window.FitAddon.FitAddon();607
terminal.loadAddon(fitAddon);608
terminal.open(elements.terminalElement);609
terminal.writeln("\x1b[36mCH-J Server Manager Core\x1b[0m");610
terminal.writeln(`${t("terminal.welcome")}\r\n`);611
terminal.onData((data) => { if (state.terminalConnected) api.sendTerminalInput(TERMINAL_SESSION_ID, data); });612
new ResizeObserver(() => fitTerminal()).observe(elements.terminalElement);613
fitTerminal();614
}616
function fitTerminal() {617
if (!terminal || !fitAddon || elements.terminalElement.offsetParent === null) return;618
try {619
fitAddon.fit();620
if (state.terminalConnected) void api.resizeTerminal(TERMINAL_SESSION_ID, terminal.cols, terminal.rows);621
} catch {}622
}624
function setTerminalState(kind, text) {625
state.terminalStateKind = kind;626
elements.terminalState.className = `connection-badge ${kind}`;627
elements.terminalState.textContent = text;628
if (kind !== "connected") state.terminalLatency = null;629
renderTerminalLatency();630
elements.connectButton.disabled = kind === "connecting" || kind === "connected";631
elements.disconnectButton.disabled = kind !== "connected" && kind !== "connecting";632
elements.terminalProfileSelect.disabled = kind === "connecting" || kind === "connected";633
}635
function renderTerminalLatency() {636
const element = $("#terminalLatency");637
element.hidden = state.terminalStateKind !== "connected";638
const format = (value) => typeof value === "number" && Number.isFinite(value) && value > 0639
? (value < 0.1 ? "<0.1 ms" : `${value.toFixed(1)} ms`) : `— (${t("terminal.latencyUnavailable")})`;640
element.textContent = `${t("terminal.ping")}: ${format(state.terminalLatency?.pingMs)} · ${t("terminal.sshRtt")}: ${format(state.terminalLatency?.sshRttMs)}`;641
element.title = t("terminal.latencyMeaning");642
}644
async function connectTerminal(retry = false) {645
const profile = state.profiles.find((item) => item.id === elements.terminalProfileSelect.value);646
if (!profile) { showView("profiles"); elements.profileMessage.textContent = t("terminal.noProfile"); return; }647
fitTerminal();648
if (!retry) {649
terminal?.clear();650
terminal?.writeln(`\x1b[33m${t("terminal.connectingTo", { target: `${profile.username}@${profile.host}:${profile.port}` })}\x1b[0m`);651
}652
setTerminalState("connecting", t("terminal.connecting"));653
const credential = elements.terminalCredential.value;654
const result = await api.connectSsh({655
sessionId: TERMINAL_SESSION_ID,656
profileId: profile.id,657
password: profile.authMethod === "password" ? credential : undefined,658
passphrase: profile.authMethod === "privateKey" ? credential : undefined,659
cols: terminal?.cols || 100,660
rows: terminal?.rows || 30661
});662
if (result.ok) {663
state.terminalConnected = true;664
elements.terminalCredential.value = "";665
setTerminalState("connected", t("terminal.connected"));666
terminal?.focus();667
return;668
}669
const issue = result.error || { code: "SSH_CONNECTION_FAILED", message: t("ssh.failed") };670
if (issue.code === "HOST_KEY_UNKNOWN") {671
const accepted = window.confirm(t("ssh.unknownHost", issue));672
if (accepted) {673
const trusted = await api.trustSshHostKey({ sessionId: TERMINAL_SESSION_ID, profileId: profile.id, fingerprint: issue.fingerprint });674
if (trusted.ok) { await connectTerminal(true); return; }675
terminal?.writeln(`\r\n\x1b[31m${trusted.error?.message || t("ssh.trustFailed")}\x1b[0m`);676
}677
} else if (issue.code === "HOST_KEY_MISMATCH") {678
terminal?.writeln(`\r\n\x1b[31m${t("ssh.keyChanged")}\x1b[0m`);679
terminal?.writeln(`${t("ssh.knownKey")}: ${issue.knownFingerprint}\r\n${t("ssh.newKey")}: ${issue.fingerprint}`);680
const accepted = window.confirm(t("ssh.changedHostConfirm", issue));681
if (accepted) {682
const trusted = await api.trustSshHostKey({683
sessionId: TERMINAL_SESSION_ID,684
profileId: profile.id,685
fingerprint: issue.fingerprint,686
knownFingerprint: issue.knownFingerprint,687
replaceKnown: true688
});689
if (trusted.ok) { await connectTerminal(true); return; }690
terminal?.writeln(`\r\n\x1b[31m${trusted.error?.message || t("ssh.trustFailed")}\x1b[0m`);691
}692
} else {693
terminal?.writeln(`\r\n\x1b[31m${errorText(issue)}\x1b[0m`);694
}695
state.terminalConnected = false;696
setTerminalState("error", t("terminal.connectionError"));697
}699
async function checkUpdates() {700
setBusy(elements.checkUpdateButton, true, t("updates.checking"));701
elements.updateHeadline.textContent = t("updates.checkingServer");702
elements.updateMessage.textContent = t("updates.tryingEndpoints");703
try { renderUpdate(await api.checkForUpdates()); }704
catch (error) { elements.updateHeadline.textContent = t("updates.checkFailed"); elements.updateMessage.textContent = errorText(error); }705
finally { setBusy(elements.checkUpdateButton, false, t("updates.check")); }706
}708
async function initialize() {709
if (!api || !i18n) throw new Error("Core preload or i18n API is unavailable.");710
[state.info, state.config, state.pluginState, state.update, state.vault] = await Promise.all([711
api.getInfo(), api.getConfig(), api.getPluginState(), api.getUpdateState(), api.getVaultStatus()712
]);713
state.plugins = state.pluginState.installed;714
state.pluginWindows = state.pluginState.windows || [];715
i18n.setLanguage(state.config.ui.language);716
document.documentElement.lang = i18n.getLanguage();717
i18n.apply(document);718
elements.runtimeText.textContent = `${state.info.platform} / ${state.info.arch}`;719
elements.sidebarVersion.textContent = `v${state.info.version}`;720
elements.coreApiValue.textContent = state.info.coreApi;721
elements.dataSchemaValue.textContent = String(state.info.dataSchema);722
elements.currentVersion.textContent = `v${state.info.version}`;723
elements.languageSelect.value = state.config.ui.language;724
elements.vaultLanguageSelect.value = state.config.ui.language;725
elements.vaultLanguageSelect.disabled = false;726
elements.channelSelect.value = state.config.updates.channel;727
elements.updateViewChannelSelect.value = state.config.updates.channel;728
elements.pluginChannelSelect.value = state.config.plugins.channel;729
elements.pluginViewChannelSelect.value = state.config.plugins.channel;730
elements.autoCheckInput.checked = state.config.updates.autoCheck;731
elements.baseUrlsText.textContent = state.config.updates.baseUrls.join("\n");732
elements.updateChannel.textContent = t("updates.channel", { channel: state.config.updates.channel });733
const security = state.config.security;734
$("#autoLockEnabled").checked = security.autoLockMinutes > 0;735
$("#autoLockMinutes").value = String(security.autoLockMinutes || 15);736
$("#autoLockMinutes").disabled = !security.autoLockMinutes;737
$("#lockOnBlur").checked = security.lockOnBlur;738
$("#requireSystemAuthentication").checked = security.requireSystemAuthentication;739
diagnosticsView = window.CHJ_DIAGNOSTICS.mount($("#diagnosticsView"), {740
api: api.diagnostics, language: () => i18n.getLanguage(), getTheme: () => state.config.ui.theme,741
setTheme: async (theme) => { state.config = await api.updateConfig({ ui: { theme } }); }742
});743
renderPlugins(); renderUpdate(state.update); initTerminal(); configureVault(state.vault);744
void refreshBiometrics();745
if (state.vault.unlocked) await loadProfiles();746
}748
document.querySelectorAll("[data-view]").forEach((button) => button.addEventListener("click", () => showView(button.dataset.view)));749
hashToolButton.addEventListener("click", async () => {750
try { await api.openPlugin(HASH_TOOL_ID); }751
catch (error) { window.alert(errorText(error)); }752
});753
elements.vaultForm.addEventListener("submit", async (event) => {754
event.preventDefault();755
const password = elements.vaultPassword.value;756
const passwordLength = Array.from(password).length;757
if (passwordLength < 4 || passwordLength > 64) { elements.vaultMessage.textContent = t("vault.passwordLength"); return; }758
if (state.vault.needsSetup && password !== elements.vaultPasswordConfirm.value) { elements.vaultMessage.textContent = t("vault.passwordMismatch"); return; }759
setBusy(elements.vaultSubmit, true, t(state.vault.needsSetup ? "vault.creating" : "vault.unlocking"));760
try {761
const status = state.vault.needsSetup ? await api.createVault(password) : await api.unlockVault(password);762
configureVault(status);763
await loadProfiles();764
} catch (error) { elements.vaultMessage.className = "form-message"; elements.vaultMessage.textContent = errorText(error); }765
finally { if (!state.vault.unlocked) setBusy(elements.vaultSubmit, false, t(state.vault.needsSetup ? "vault.create" : "vault.unlock")); }766
});767
elements.lockVaultButton.addEventListener("click", async () => {768
configureVault(await api.lockVault());769
clearProtectedUi();770
terminal?.writeln(`${t("terminal.vaultLocked")}\r\n`);771
});772
$("#biometricUnlockButton").addEventListener("click", async event => {773
event.currentTarget.disabled = true;774
try { configureVault(await api.unlockVaultWithBiometrics()); await loadProfiles(); }775
catch (error) { elements.vaultMessage.textContent = errorText(error); }776
finally { $("#biometricUnlockButton").disabled = false; void refreshBiometrics(); }777
});778
for (const [id, action, message] of [779
["enableBiometricsButton", () => api.enableBiometrics(), "security.enabled"],780
["disableBiometricsButton", () => api.disableBiometrics(), "security.disabled"],781
["testBiometricsButton", () => api.authenticateSensitiveAction(), "security.testPassed"]782
]) {783
$("#" + id).addEventListener("click", async event => {784
event.currentTarget.disabled = true;785
try { await action(); $("#securityMessage").textContent = t(message); }786
catch (error) { $("#securityMessage").textContent = errorText(error); }787
finally { await refreshBiometrics(); }788
});789
}790
$("#autoLockEnabled").addEventListener("change", event => {791
$("#autoLockMinutes").disabled = !event.target.checked;792
if (event.target.checked && $("#autoLockMinutes").value === "0") $("#autoLockMinutes").value = "15";793
});794
$("#securityForm").addEventListener("submit", async event => {795
event.preventDefault();796
const password = $("#securityMasterPassword").value; $("#securityMasterPassword").value = "";797
try {798
state.config = await api.updateConfig({ masterPassword: password, security: {799
autoLockMinutes: $("#autoLockEnabled").checked ? Number($("#autoLockMinutes").value) : 0,800
lockOnBlur: $("#lockOnBlur").checked, requireSystemAuthentication: $("#requireSystemAuthentication").checked801
} });802
$("#securityMessage").textContent = t("settings.saved");803
} catch (error) { $("#securityMessage").textContent = errorText(error); }804
});805
elements.showVaultResetButton.addEventListener("click", () => {806
elements.vaultResetPanel.hidden = false;807
elements.showVaultResetButton.hidden = true;808
elements.vaultResetConfirmation.focus();809
});810
elements.cancelVaultResetButton.addEventListener("click", () => {811
elements.vaultResetPanel.hidden = true;812
elements.showVaultResetButton.hidden = false;813
elements.vaultResetConfirmation.value = "";814
});815
elements.confirmVaultResetButton.addEventListener("click", async () => {816
if (elements.vaultResetConfirmation.value !== "SMAZAT") {817
elements.vaultMessage.textContent = t("vault.typeDelete");818
return;819
}820
if (!window.confirm(t("vault.finalResetConfirm"))) return;821
setBusy(elements.confirmVaultResetButton, true, t("vault.deleting"));822
try {823
const status = await api.resetVault("SMAZAT");824
clearProtectedUi();825
configureVault(status);826
elements.vaultMessage.className = "form-message success";827
elements.vaultMessage.textContent = t("vault.resetDone");828
} catch (error) {829
elements.vaultMessage.className = "form-message";830
elements.vaultMessage.textContent = errorText(error);831
} finally {832
setBusy(elements.confirmVaultResetButton, false, t("vault.resetAction"));833
}834
});835
elements.newProfileButton.addEventListener("click", () => { state.selectedProfileId = ""; renderProfiles(); resetProfileForm(); elements.profileLabel.focus(); });836
elements.profileAuthMethod.addEventListener("change", updatePrivateKeyField);837
elements.profileStorePassword.addEventListener("change", updatePrivateKeyField);838
elements.selectPrivateKeyButton.addEventListener("click", async () => {839
const result = await api.selectPrivateKey();840
if (!result.canceled) elements.profilePrivateKeyPath.value = result.path;841
});842
elements.profileForm.addEventListener("submit", async (event) => {843
event.preventDefault(); elements.profileMessage.className = "form-message"; elements.profileMessage.textContent = "";844
try {845
const saved = await api.saveProfile({846
id: elements.profileId.value || undefined, label: elements.profileLabel.value, host: elements.profileHost.value,847
port: Number(elements.profilePort.value), username: elements.profileUsername.value,848
authMethod: elements.profileAuthMethod.value, privateKeyPath: elements.profilePrivateKeyPath.value,849
storePassword: elements.profileAuthMethod.value === "password" && elements.profileStorePassword.checked,850
password: elements.profileStoredPassword.value851
});852
await loadProfiles(saved.id); elements.profileMessage.className = "form-message success"; elements.profileMessage.textContent = t("profiles.saved");853
} catch (error) { elements.profileMessage.textContent = errorText(error); }854
});855
elements.deleteProfileButton.addEventListener("click", async () => {856
const id = elements.profileId.value;857
if (!id || !window.confirm(t("profiles.deleteConfirm"))) return;858
await api.deleteProfile(id); state.selectedProfileId = ""; await loadProfiles("");859
});860
elements.terminalProfileSelect.addEventListener("change", () => { elements.terminalCredential.value = ""; updateTerminalProfile(); });861
elements.connectButton.addEventListener("click", () => connectTerminal().catch((error) => { setTerminalState("error", t("terminal.error")); terminal?.writeln(`\r\n\x1b[31m${errorText(error)}\x1b[0m`); }));862
elements.disconnectButton.addEventListener("click", async () => { await api.disconnectSsh(TERMINAL_SESSION_ID); });863
elements.checkUpdateButton.addEventListener("click", checkUpdates);864
elements.downloadUpdateButton.addEventListener("click", async () => {865
setBusy(elements.downloadUpdateButton, true, t("updates.downloadingAndVerifying"));866
elements.updateHeadline.textContent = t("updates.verifyingSignature");867
try { renderUpdate(await api.downloadUpdate()); }868
catch (_error) {869
elements.updateHeadline.textContent = t("updates.downloadRejected");870
elements.updateMessage.textContent = t("updates.signatureFailure");871
elements.installUpdateButton.hidden = true;872
elements.revealUpdateButton.hidden = true;873
}874
finally { setBusy(elements.downloadUpdateButton, false, t("updates.download")); }875
});876
elements.installUpdateButton.addEventListener("click", async () => {877
const release = state.update?.release;878
if (!release || !window.confirm(t("updates.installConfirm", { version: release.version, date: formatReleaseDate(release.publishedAt) }))) return;879
setBusy(elements.installUpdateButton, true, t("updates.verifyingSignature"));880
try { await api.installUpdate(); }881
catch (_error) {882
try { renderUpdate(await api.getUpdateState()); } catch {}883
elements.updateHeadline.textContent = t("updates.installFailed");884
elements.updateMessage.textContent = t("updates.installFailure");885
elements.installUpdateButton.hidden = true;886
} finally { setBusy(elements.installUpdateButton, false, t("updates.install")); }887
});888
elements.revealUpdateButton.addEventListener("click", () => api.revealUpdate());889
elements.updateViewChannelSelect.addEventListener("change", async () => {890
state.config = await api.updateConfig({ updates: { channel: elements.updateViewChannelSelect.value } });891
elements.channelSelect.value = state.config.updates.channel;892
elements.updateChannel.textContent = t("updates.channel", { channel: state.config.updates.channel });893
await checkUpdates();894
});895
elements.pluginViewChannelSelect.addEventListener("change", async () => {896
state.config = await api.updateConfig({ plugins: { channel: elements.pluginViewChannelSelect.value } });897
elements.pluginChannelSelect.value = state.config.plugins.channel;898
elements.checkPluginCatalogButton.click();899
});900
elements.checkPluginCatalogButton.addEventListener("click", async () => {901
setBusy(elements.checkPluginCatalogButton, true, t("plugins.checking"));902
try { state.pluginState = await api.checkPluginCatalog(); state.plugins = state.pluginState.installed; renderPlugins(); }903
catch (error) { window.alert(errorText(error)); }904
finally { setBusy(elements.checkPluginCatalogButton, false, t("plugins.checkCatalog")); }905
});906
elements.settingsForm.addEventListener("submit", async (event) => {907
event.preventDefault();908
state.config = await api.updateConfig({909
ui: { language: elements.languageSelect.value },910
updates: { channel: elements.channelSelect.value, autoCheck: elements.autoCheckInput.checked },911
plugins: { channel: elements.pluginChannelSelect.value }912
});913
elements.updateViewChannelSelect.value = state.config.updates.channel;914
elements.pluginViewChannelSelect.value = state.config.plugins.channel;915
applyLanguage(state.config.ui.language);916
elements.updateChannel.textContent = t("updates.channel", { channel: state.config.updates.channel });917
elements.settingsStatus.textContent = t("settings.saved"); setTimeout(() => { elements.settingsStatus.textContent = ""; }, 1800);918
});919
elements.languageSelect.addEventListener("change", () => applyLanguage(elements.languageSelect.value));920
document.querySelectorAll("[data-legal-document]").forEach((button) => {921
button.addEventListener("click", async () => {922
try { await api.openLegalDocument(button.dataset.legalDocument); }923
catch (error) { window.alert(errorText(error)); }924
});925
});926
elements.vaultLanguageSelect.addEventListener("change", async () => {927
const language = applyLanguage(elements.vaultLanguageSelect.value);928
state.config = await api.updateConfig({ ui: { language } });929
});931
api?.onUpdateState((payload) => renderUpdate(payload));932
api?.onPluginWindowState((windows) => renderPluginTaskbar(windows));933
api?.onSshData((payload) => { if (payload.sessionId === TERMINAL_SESSION_ID) terminal?.write(payload.data); });934
api?.onSshState((payload) => {935
if (payload.sessionId !== TERMINAL_SESSION_ID) return;936
state.terminalConnected = payload.state === "connected";937
state.terminalLatency = payload.latency;938
if (payload.state === "connecting") setTerminalState("connecting", t("terminal.connecting"));939
else if (payload.state === "connected") setTerminalState("connected", t("terminal.connected"));940
else setTerminalState("disconnected", t("terminal.disconnected"));941
});942
api?.onSshError((payload) => { if (payload.sessionId === TERMINAL_SESSION_ID) terminal?.writeln(`\r\n\x1b[31m${errorText(payload)}\x1b[0m`); });943
api?.onVaultLocked(status => {944
configureVault(status); clearProtectedUi(); void refreshBiometrics();945
terminal?.writeln(`${t("terminal.vaultLocked")}\r\n`);946
});948
initialize().catch((error) => {949
elements.runtimeText.textContent = t("errors.initialization");950
elements.updateMessage.textContent = errorText(error);951
});SHA-256
3544b7e468b1171fdbbda5f6ea35eea4853787d3a3e4c9e6d6919887ce49f9e9CH-J Proprietary Software License 1.14 · Källan finns under CH-J Proprietary Software License 1.14. Dess tillgänglighet ändrar inte licensvillkoren eller ger ytterligare rättigheter.
Arkiv SHA-256
3df08cf5e899262a6ce92d7ec2caf5c42b683f1f2b57055662b845ff7dde67a8