import { runtimeStatus } from "/assets/js/ui-semantics.js"; import { state as appState } from "/assets/js/state.js"; import { addStorageSettings } from "./storage.js"; import { renderDevice } from "./device.js"; import { renderHosts } from "/assets/js/devices.js"; import { bulkSections, field, labels, options, sectionMeta, sectionOrder, } from "./schema.js"; import { tr } from "/assets/js/i18n.js"; import { loadPlugins, pluginText, renderPlugins } from "./hardware.js"; import { $, configurableHosts, esc, hostLabel } from "/assets/js/shared.js"; import { get, rows } from "/assets/js/navigation.js"; import { loadServices, renderServiceLists, resetMonitorForm, saveMonitor, tokenHeaders, } from "./services.js"; import { applyBulkConfig, deleteHost, saveHost, } from "/assets/js/device-actions.js"; export function renderGeneral(s, v) { if (s === "device") return renderDevice(v); const [t, d] = sectionMeta[s] || [s, ""]; return `

${tr(t)}

${tr(d)}
${ Object.entries(v) .map(([k, x]) => field(s, k, x)) .join("") || '
暂无配置项
' }
`; } export function renderServices() { return `

当前系统服务

点击“加入监控”会自动填写下方表单;也可以手动输入尚未安装的未来服务。
正在读取 systemd 服务…

已监控服务

“暂停监控”只停止状态检查,不影响服务运行;“停止服务”才会操作 systemd。

新建服务监控

`; } export function renderProcesses() { return `

${pluginText("已监控进程", "Monitored processes")}

${pluginText("用于没有 systemd 服务的程序,通过 pgrep -f 匹配完整命令行。", "For programs without a systemd service. Matching uses pgrep -f against the full command line.")}
${pluginText("正在读取进程监控…", "Loading process monitors…")}

${pluginText("新建进程监控", "Add process monitor")}

`; } export async function loadProcesses() { if (!$("processList")) return; try { const d = await get( `/api/v1/processes?host=${encodeURIComponent(appState.currentConfigHost)}`, ); appState.processCache = d.processes; renderProcessList(); } catch (e) { $("processList").innerHTML = `
${esc(e.message)}
`; } } export function renderProcessList() { rows( "processList", appState.processCache.map( (p) => `
${esc(p.name)} ${runtimeStatus(p.up, appState.LANG)}
${esc(p.target)}
`, ), pluginText("尚未监控任何普通进程", "No process monitors configured"), ); document .querySelectorAll("[data-edit-process]") .forEach((b) => (b.onclick = () => selectProcess(b.dataset.editProcess))); document .querySelectorAll("[data-delete-process]") .forEach((b) => (b.onclick = () => deleteProcess(b.dataset.deleteProcess))); } export function selectProcess(name) { const p = appState.processCache.find((x) => x.name === name); if (!p) return; appState.editingProcessName = p.name; $("processName").value = p.name; $("processTarget").value = p.target; $("processFormTitle").textContent = pluginText( "编辑进程监控", "Edit process monitor", ); } export function resetProcessForm() { appState.editingProcessName = ""; $("processName").value = ""; $("processTarget").value = ""; $("processFormTitle").textContent = pluginText( "新建进程监控", "Add process monitor", ); $("processMessage").textContent = ""; } export async function saveProcess() { const state = $("processMessage"); try { state.textContent = pluginText("正在保存…", "Saving…"); await get("/api/v1/processes", { method: "PUT", headers: tokenHeaders(), body: JSON.stringify({ host: appState.currentConfigHost, operation: "save", name: $("processName").value.trim(), target: $("processTarget").value.trim(), previous_name: appState.editingProcessName, }), }); resetProcessForm(); await loadProcesses(); state.textContent = pluginText("保存成功", "Saved"); state.className = "success"; } catch (e) { state.textContent = e.message; state.className = "error"; } } export async function deleteProcess(name) { if ( !confirm( pluginText( `确定删除 ${name} 的进程监控吗?`, `Remove process monitor ${name}?`, ), ) ) return; try { await get("/api/v1/processes", { method: "PUT", headers: tokenHeaders(), body: JSON.stringify({ host: appState.currentConfigHost, operation: "delete", name, }), }); await loadProcesses(); } catch (e) { alert(e.message); } } export function renderBulkTargets() { return `

${tr("目标机器")}

${tr("机器标识和机器列表不会参与同步。")}
${ configurableHosts() .filter( (h) => h.id !== appState.bulkSource && h.config_contract === appState.fleetHosts.find( (source) => source.id === appState.bulkSource, )?.config_contract, ) .map( (h) => ``, ) .join("") || pluginText("没有其他设备", "No other devices") }
`; } export function bulkValue(section, key, value) { const item = options[`${section}.${key}`]?.find(([v]) => v === String(value)); return item ? tr(item[1]) : value; } export function renderBulkSync() { const groups = bulkSections .filter( (s) => appState.bulkOriginal[s] && Object.keys(appState.bulkOriginal[s]).length, ) .map( (s) => `
${Object.entries( appState.bulkOriginal[s], ) .map( ([k, v]) => ``, ) .join("")}
`, ) .join(""); return `

${tr("来源机器")}

${esc(hostLabel(appState.fleetHosts.find((h) => h.id === appState.bulkSource) || { id: appState.bulkSource }))}
${renderBulkTargets()}

${tr("配置项")}

${groups}
`; } export function updateBulkSelection() { document.querySelectorAll("[data-bulk-section]").forEach((parent) => { const items = [ ...document.querySelectorAll( `[data-bulk-item^="${parent.dataset.bulkSection}."]`, ), ]; parent.checked = items.length > 0 && items.every((x) => x.checked); parent.indeterminate = items.some((x) => x.checked) && !parent.checked; }); const n = document.querySelectorAll("[data-bulk-item]:checked").length; if ($("bulkCount")) $("bulkCount").textContent = appState.LANG === "zh" ? `已选择 ${n} 项` : `${n} settings selected`; } export function bindBulkSync() { if (!$("bulkApply")) return; document.querySelectorAll("[data-bulk-section]").forEach( (x) => (x.onchange = () => { document .querySelectorAll(`[data-bulk-item^="${x.dataset.bulkSection}."]`) .forEach((i) => (i.checked = x.checked)); updateBulkSelection(); }), ); document .querySelectorAll("[data-bulk-item]") .forEach((x) => (x.onchange = updateBulkSelection)); $("bulkAll").onclick = () => { document .querySelectorAll("[data-bulk-item]") .forEach((x) => (x.checked = true)); updateBulkSelection(); }; $("bulkNone").onclick = () => { document .querySelectorAll("[data-bulk-item]") .forEach((x) => (x.checked = false)); updateBulkSelection(); }; $("bulkInvert").onclick = () => { document .querySelectorAll("[data-bulk-item]") .forEach((x) => (x.checked = !x.checked)); updateBulkSelection(); }; $("bulkApply").onclick = applyBulkConfig; updateBulkSelection(); } export function updateConditional() { const alertMode = document.querySelector('[data-key="alert_mode"]')?.value || "breathe"; document .querySelectorAll(".alert-flash") .forEach((e) => e.classList.toggle("hidden", alertMode !== "flash")); document .querySelectorAll(".alert-breathe") .forEach((e) => e.classList.toggle("hidden", alertMode !== "breathe")); const mode = document.querySelector('[data-section="led"][data-key="normal_mode"]') ?.value || "off"; document .querySelectorAll(".normal-field") .forEach((x) => x.classList.toggle("hidden", !x.dataset.modes.split(" ").includes(mode)), ); updateSwatches(); } export function updateSwatches() { document.querySelectorAll("[data-swatch]").forEach((s) => { const p = s.dataset.swatch, read = (c) => Math.max( 0, Math.min( 255, Number( document.querySelector( `[data-section="led"][data-key="${p}_${c}"]`, )?.value || 0, ), ), ), hex = "#" + ["r", "g", "b"] .map((c) => Math.round(read(c)).toString(16).padStart(2, "0")) .join(""); s.style.background = hex; const picker = document.querySelector(`[data-color-prefix="${p}"]`); if (picker) picker.value = hex; }); } export function renderConfig() { const ss = sectionOrder.filter( (s) => appState.original[s] || s === "plugins", ); $("configMenu").innerHTML = ss .map( (s, i) => ``, ) .join(""); $("configRows").innerHTML = ss .map( (s, i) => `

${tr(sectionMeta[s]?.[0] || s)}

${tr(sectionMeta[s]?.[1] || "")}

${s === "bulk_sync" ? renderBulkSync() : s === "plugins" ? renderPlugins() : s === "services" ? renderServices() : s === "processes" ? renderProcesses() : s === "hosts" ? renderHosts(appState.original[s]) : renderGeneral(s, appState.original[s])}
`, ) .join(""); $("configMenu").onclick = (e) => { const b = e.target.closest("button"); if (!b) return; document .querySelectorAll("[data-config-section]") .forEach((x) => x.classList.toggle("active", x === b)); document .querySelectorAll("[data-config-page]") .forEach((x) => x.classList.toggle( "active", x.dataset.configPage === b.dataset.configSection, ), ); $("mainSavebar").classList.toggle( "hidden", ["bulk_sync", "plugins"].includes(b.dataset.configSection), ); if (b.dataset.configSection === "services") loadServices(); if (b.dataset.configSection === "processes") loadProcesses(); if (b.dataset.configSection === "plugins") loadPlugins(); }; $("configRows").oninput = (e) => { if (e.target.matches("[data-color-prefix]")) { const p = e.target.dataset.colorPrefix, v = e.target.value; ["r", "g", "b"].forEach((c, i) => { document.querySelector( `[data-section="led"][data-key="${p}_${c}"]`, ).value = parseInt(v.slice(1 + i * 2, 3 + i * 2), 16); }); } if (e.target.matches('[data-section="led"],[data-color-prefix]')) updateConditional(); if (e.target.matches("[data-section]")) { if (e.target.dataset.section === "device") e.target.dataset.deviceAuto = "false"; markChanged(); } }; $("refreshServices")?.addEventListener("click", loadServices); $("serviceSearch")?.addEventListener("input", renderServiceLists); $("saveMonitor")?.addEventListener("click", saveMonitor); $("resetMonitor")?.addEventListener("click", resetMonitorForm); $("saveProcess")?.addEventListener("click", saveProcess); $("resetProcess")?.addEventListener("click", resetProcessForm); $("saveHost")?.addEventListener("click", saveHost); document .querySelectorAll("[data-delete-host]") .forEach((b) => (b.onclick = () => deleteHost(b.dataset.deleteHost))); bindBulkSync(); updateConditional(); addStorageSettings(); } export function markChanged() { let n = 0; document.querySelectorAll("#configRows [data-section]").forEach((i) => { if ( (i.dataset.deviceAuto === "true" ? "auto" : i.value) !== appState.original[i.dataset.section][i.dataset.key] ) n++; }); $("saveState").textContent = n ? appState.LANG === "zh" ? `有 ${n} 项尚未保存` : `${n} unsaved changes` : tr("尚未修改"); $("saveState").className = n ? "" : "muted"; }