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 `
${
Object.entries(v)
.map(([k, x]) => field(s, k, x))
.join("") || '
暂无配置项
'
}
`;
}
export function renderServices() {
return `已监控服务 “暂停监控”只停止状态检查,不影响服务运行;“停止服务”才会操作 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…")}
`;
}
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)} ${tr("编辑")} ${tr("删除")}
`,
),
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) =>
`
${esc(hostLabel(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) =>
``,
)
.join("");
return `${tr("来源机器")} ${esc(hostLabel(appState.fleetHosts.find((h) => h.id === appState.bulkSource) || { id: appState.bulkSource }))}
${renderBulkTargets()}${tr("全部选择")} ${tr("全部取消")} ${tr("反选")}
${tr("配置项")} ${groups}${tr("开始批量同步")}
`;
}
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) =>
`${tr(sectionMeta[s]?.[0] || s)} `,
)
.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", "access"].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();
installAccessSettings();
}
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";
}
function installAccessSettings() {
const host = appState.fleetHosts.find(h => h.id === appState.currentConfigHost);
if (!host) return;
$("configMenu").insertAdjacentHTML("beforeend", `${pluginText("访问地址", "Access URL")} `);
$("configRows").insertAdjacentHTML("beforeend", `${pluginText("访问地址", "Access URL")} ${pluginText("设置 Gateway 管理页面地址,首页显示链接按钮;留空隐藏入口。", "Set the Gateway management URL to show a link on the overview. Leave blank to hide it.")}
${pluginText("访问地址", "Access URL")}
${pluginText("保存访问地址", "Save access URL")}
`);
$("saveGatewayAccess").onclick = async () => {
const button = $("saveGatewayAccess"), message = $("gatewayAccessMessage");
const value = $("gatewayAccessUrl").value.trim();
if (value) {
try {
const url = new URL(value);
if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.pathname !== "/" || url.search || url.hash) throw Error();
} catch {
message.textContent = pluginText("请使用不含路径或账号密码的 HTTP(S) 根地址", "Use an HTTP(S) origin without paths or credentials");
message.className = "error"; return;
}
}
button.disabled = true;
try {
const result = await get("/api/v1/hosts", {method: "PUT", headers: {"Content-Type": "application/json"}, body: JSON.stringify({operation: "save", id: host.id, adapter_id: host.adapter_id, connection: {management_url: value}})});
appState.fleetHosts = result.hosts;
message.textContent = pluginText("已保存", "Saved"); message.className = "success";
} catch (error) { message.textContent = error.message; message.className = "error"; }
finally { button.disabled = false; }
};
}