CH-J Server Managerupravljanje strežnikov prek SSH
Meni
Objavljen vir

CH-J Server Manager

Brskajte po imenikih in datotekah za določeno izdajo programa.

Prenos vir ZIP
CH-J / Server Manager 339 datoteke
core-20261014T184258Z-3fbed0b93fdf Dodano: Datum, ki ni zabeležen · Zgrajena:
Datoteke
app.js Dodano: Datum, ki ni zabeležen 52,1 KB · 951 vrstice
Prenesi datoteko
1"use strict";
3const api = window.chjCore;
4const i18n = window.chjI18n;
5const t = (key, values) => i18n.t(key, values);
6const TERMINAL_SESSION_ID = "terminal-main";
7const HASH_TOOL_ID = "chj.hash-checksum";
8if (api?.platform) document.body.classList.add(`platform-${api.platform}`);
9const 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};
23const $ = (selector) => document.querySelector(selector);
24const hashToolButton = $("#hashToolButton");
25const coreNavigationButtons = [...$("#mainNavigation").querySelectorAll("[data-view]")];
26const selectedExportIds = new Set();
27let importPreview = null;
29const 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};
58const 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};
63let diagnosticsView = null;
64let terminal = null;
65let fitAddon = null;
67function 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"));
74let biometricStatus = null;
75function 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;
85async function refreshBiometrics() {
86 try { biometricStatus = await api.getBiometricStatus(); renderBiometrics(); }
87 catch (error) { $("#securityMessage").textContent = errorText(error); }
90function 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);
97function pluginDisplay(id) {
98 return state.config?.plugins.display?.[id] || (id === HASH_TOOL_ID ? "navigation" : "visible");
101let preferenceSave = Promise.resolve();
102function 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;
110function 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());
120function 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 });
168$("#navigationEditToggle").addEventListener("change", renderNavigation);
169$("#mainNavigation").addEventListener("click", (event) => {
170 if ($("#navigationEditToggle").checked) { event.preventDefault(); event.stopPropagation(); }
171}, true);
173function cancelProfileImport() {
174 importPreview = null;
175 $("#profileImportDialog").close();
176 $("#profileImportRows").replaceChildren();
177 void api.cancelProfileImport().catch(() => {});
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});
224function setBusy(button, busy, label) {
225 button.disabled = busy;
226 if (label) button.textContent = label;
229function 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]}`;
237function 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;
258function 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 }
297function 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);
303function 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 }
331function 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();
391function 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 }
417function 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 }
451function 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);
486function 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();
501async 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();
512function 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 }
535function 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();
543function 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();
557function 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();
574function 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;
581function 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");
594function 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();
616function 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 {}
624function 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";
635function renderTerminalLatency() {
636 const element = $("#terminalLatency");
637 element.hidden = state.terminalStateKind !== "connected";
638 const format = (value) => typeof value === "number" && Number.isFinite(value) && value > 0
639 ? (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");
644async 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 || 30
661 });
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: true
688 });
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"));
699async 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")); }
708async 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();
748document.querySelectorAll("[data-view]").forEach((button) => button.addEventListener("click", () => showView(button.dataset.view)));
749hashToolButton.addEventListener("click", async () => {
750 try { await api.openPlugin(HASH_TOOL_ID); }
751 catch (error) { window.alert(errorText(error)); }
752});
753elements.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});
767elements.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});
778for (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 });
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").checked
801 } });
802 $("#securityMessage").textContent = t("settings.saved");
803 } catch (error) { $("#securityMessage").textContent = errorText(error); }
804});
805elements.showVaultResetButton.addEventListener("click", () => {
806 elements.vaultResetPanel.hidden = false;
807 elements.showVaultResetButton.hidden = true;
808 elements.vaultResetConfirmation.focus();
809});
810elements.cancelVaultResetButton.addEventListener("click", () => {
811 elements.vaultResetPanel.hidden = true;
812 elements.showVaultResetButton.hidden = false;
813 elements.vaultResetConfirmation.value = "";
814});
815elements.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});
835elements.newProfileButton.addEventListener("click", () => { state.selectedProfileId = ""; renderProfiles(); resetProfileForm(); elements.profileLabel.focus(); });
836elements.profileAuthMethod.addEventListener("change", updatePrivateKeyField);
837elements.profileStorePassword.addEventListener("change", updatePrivateKeyField);
838elements.selectPrivateKeyButton.addEventListener("click", async () => {
839 const result = await api.selectPrivateKey();
840 if (!result.canceled) elements.profilePrivateKeyPath.value = result.path;
841});
842elements.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.value
851 });
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});
855elements.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});
860elements.terminalProfileSelect.addEventListener("change", () => { elements.terminalCredential.value = ""; updateTerminalProfile(); });
861elements.connectButton.addEventListener("click", () => connectTerminal().catch((error) => { setTerminalState("error", t("terminal.error")); terminal?.writeln(`\r\n\x1b[31m${errorText(error)}\x1b[0m`); }));
862elements.disconnectButton.addEventListener("click", async () => { await api.disconnectSsh(TERMINAL_SESSION_ID); });
863elements.checkUpdateButton.addEventListener("click", checkUpdates);
864elements.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});
876elements.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});
888elements.revealUpdateButton.addEventListener("click", () => api.revealUpdate());
889elements.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});
895elements.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});
900elements.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});
906elements.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});
919elements.languageSelect.addEventListener("change", () => applyLanguage(elements.languageSelect.value));
920document.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});
926elements.vaultLanguageSelect.addEventListener("change", async () => {
927 const language = applyLanguage(elements.vaultLanguageSelect.value);
928 state.config = await api.updateConfig({ ui: { language } });
929});
931api?.onUpdateState((payload) => renderUpdate(payload));
932api?.onPluginWindowState((windows) => renderPluginTaskbar(windows));
933api?.onSshData((payload) => { if (payload.sessionId === TERMINAL_SESSION_ID) terminal?.write(payload.data); });
934api?.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});
942api?.onSshError((payload) => { if (payload.sessionId === TERMINAL_SESSION_ID) terminal?.writeln(`\r\n\x1b[31m${errorText(payload)}\x1b[0m`); });
943api?.onVaultLocked(status => {
944 configureVault(status); clearProtectedUi(); void refreshBiometrics();
945 terminal?.writeln(`${t("terminal.vaultLocked")}\r\n`);
946});
948initialize().catch((error) => {
949 elements.runtimeText.textContent = t("errors.initialization");
950 elements.updateMessage.textContent = errorText(error);
951});
SHA-2563544b7e468b1171fdbbda5f6ea35eea4853787d3a3e4c9e6d6919887ce49f9e9

CH-J Proprietary Software License 1.14 · Vir je na voljo pod CH-J Proprietary Software License 1.14. Njena razpoložljivost ne spreminja pogojev licence ali dodeljuje dodatnih pravic.

Arhiv SHA-2563df08cf5e899262a6ce92d7ec2caf5c42b683f1f2b57055662b845ff7dde67a8