const serviceNames = {
surrealdb: "SurrealDB",
icom: "ICOM",
arma: "Arma Server",
};
const subtitles = {
overview: "Start, stop, and check local Forge hosting services.",
settings: "Edit process command, arguments, working directory, and health port.",
logs: "Recent stdout, stderr, and host supervisor events.",
};
let snapshot = null;
let refreshTimer = null;
let activeSettingsService = "surrealdb";
let configDirty = false;
let activeEditorPath = "";
let surrealInstallInfo = null;
let selectedArmaConfigTemplate = "server";
const views = {
overview: document.getElementById("overviewView"),
settings: document.getElementById("settingsView"),
logs: document.getElementById("logsView"),
};
document.querySelectorAll(".nav-button").forEach((button) => {
button.addEventListener("click", () => {
const view = button.dataset.view;
if (button.dataset.service) {
if (configDirty && button.dataset.service !== activeSettingsService) {
persistActiveSettingsForm();
}
activeSettingsService = button.dataset.service;
} else if (view === "settings") {
if (configDirty && activeSettingsService !== "surrealdb") {
persistActiveSettingsForm();
}
activeSettingsService = "surrealdb";
}
document.querySelectorAll(".nav-button").forEach((item) => item.classList.remove("active"));
button.classList.add("active");
Object.entries(views).forEach(([key, element]) => element.classList.toggle("active", key === view));
setHeader(view);
renderSettings(true);
syncBulkActionsVisibility();
syncRefreshTimer();
});
});
document.getElementById("refreshButton").addEventListener("click", refresh);
document.getElementById("saveButton").addEventListener("click", saveConfig);
document.getElementById("startAllButton").addEventListener("click", startAll);
document.getElementById("stopAllButton").addEventListener("click", stopAll);
document.getElementById("editorCloseButton").addEventListener("click", closeConfigEditor);
document.getElementById("editorReloadButton").addEventListener("click", () => runAction(() => openConfigEditor(activeEditorPath)));
document.getElementById("editorSaveButton").addEventListener("click", () => runAction(saveConfigEditor));
async function refresh() {
if (isSettingsViewActive() && configDirty) {
return;
}
try {
const nextSnapshot = await tauriInvoke("get_snapshot");
if (configDirty && snapshot) {
persistActiveSettingsForm();
nextSnapshot.config = snapshot.config;
}
snapshot = nextSnapshot;
render();
} catch (error) {
renderError(error);
}
}
async function startService(name) {
snapshot = await tauriInvoke("start_service", { name });
render();
}
async function stopService(name) {
snapshot = await tauriInvoke("stop_service", { name });
render();
}
async function startAll() {
if (!snapshot) return;
const serviceNames = Object.keys(snapshot.config).filter(
(name) =>
snapshot.config[name].enabled && (name === "surrealdb" || snapshot.config[name].command.trim() !== "")
);
for (const name of serviceNames) {
const status = snapshot.statuses.find((s) => s.name === name);
if (status && !status.running) {
await startService(name);
if (name === "surrealdb") {
await waitForServiceHealth("surrealdb", 30000);
}
}
}
}
async function stopAll() {
if (!snapshot) return;
const serviceNames = Object.keys(snapshot.config).filter((name) => snapshot.config[name].enabled);
for (const name of serviceNames) {
const status = snapshot.statuses.find((s) => s.name === name);
if (status && status.running) {
await stopService(name);
}
}
}
async function waitForServiceHealth(serviceName, timeoutMs = 30000) {
const startTime = Date.now();
while (Date.now() - startTime < timeoutMs) {
await refresh();
const status = snapshot?.statuses.find((s) => s.name === serviceName);
if (status?.healthy) {
return true;
}
await new Promise((resolve) => setTimeout(resolve, 500));
}
return false;
}
async function saveConfig() {
if (!snapshot) {
await refresh();
}
const config = readFormConfig();
snapshot = await tauriInvoke("save_config", { config });
configDirty = false;
render();
}
function tauriInvoke(command, payload) {
const invoke = window.__TAURI__?.core?.invoke;
if (!invoke) {
return Promise.reject("Tauri invoke API is not available in this window.");
}
return invoke(command, payload);
}
function render() {
if (!snapshot) return;
document.getElementById("configPath").textContent = snapshot.config_path;
syncSidebarStatus();
renderServices();
renderSettings();
renderLogs();
syncBulkActionsVisibility();
}
function setHeader(view) {
const title = view === "settings" ? serviceNames[activeSettingsService] : activeNavLabel();
document.getElementById("viewTitle").textContent = title;
document.getElementById("viewSubtitle").textContent = subtitles[view];
}
function activeNavLabel() {
return document.querySelector(".nav-button.active span:last-child")?.textContent.trim() || "Overview";
}
function syncSidebarStatus() {
document.querySelectorAll(".nav-button[data-service], .nav-button[data-view='settings']:not([data-service])").forEach((button) => {
const service = button.dataset.service || "surrealdb";
const config = snapshot.config[service];
const status = snapshot.statuses.find((s) => s.name === service);
const enabled = config?.enabled;
const running = status?.running;
const healthy = status?.healthy;
const isDisabled = !enabled;
const isOnline = enabled && running && healthy;
const isStopped = enabled && !isOnline;
button.classList.toggle("service-enabled", Boolean(isOnline));
button.classList.toggle("service-disabled", Boolean(isDisabled));
button.classList.toggle("service-stopped", Boolean(isStopped));
});
}
function renderServices() {
const grid = document.getElementById("serviceGrid");
const template = document.getElementById("serviceCardTemplate");
grid.replaceChildren();
snapshot.statuses.forEach((status) => {
const node = template.content.firstElementChild.cloneNode(true);
node.querySelector("h2").textContent = serviceNames[status.name] || status.name;
const pingPill = node.querySelector(".ping-pill");
const pingDisplay = resolvePingDisplay(status);
pingPill.textContent = pingDisplay.label;
pingPill.classList.remove("good", "warn", "bad", "unavailable");
pingPill.classList.add(pingDisplay.className);
node.querySelector(".command-line").textContent = status.command || "No command configured";
const pill = node.querySelector(".status-pill");
pill.textContent = status.running ? "Running" : "Stopped";
pill.classList.add(status.running ? "running" : "stopped");
node.querySelector(".pid").textContent = status.pid ? String(status.pid) : "-";
node.querySelector(".health").textContent = status.health;
node.querySelector(".enabled").textContent = status.enabled ? "Yes" : "No";
const start = node.querySelector(".start");
const stop = node.querySelector(".stop");
if (!start || !stop) {
throw new Error("Service card template is missing start or stop controls.");
}
start.disabled = status.running || !status.enabled || !status.configured;
stop.disabled = !status.running;
start.addEventListener("click", () => runAction(() => startService(status.name)));
stop.addEventListener("click", () => runAction(() => stopService(status.name)));
grid.appendChild(node);
});
}
function resolvePingDisplay(status) {
if (!status.healthy || typeof status.ping_ms !== "number") {
return { label: "N/A", className: "unavailable" };
}
if (status.ping_ms <= 60) {
return { label: `${status.ping_ms} ms`, className: "good" };
}
if (status.ping_ms <= 120) {
return { label: `${status.ping_ms} ms`, className: "warn" };
}
return { label: `${status.ping_ms} ms`, className: "bad" };
}
function renderSettings(force = false) {
if (!snapshot) return;
if (!isSettingsViewActive()) return;
const form = document.getElementById("configForm");
if (!force && configDirty && isSettingsViewActive()) return;
if (!snapshot.config[activeSettingsService]) {
activeSettingsService = Object.keys(snapshot.config)[0];
}
const name = activeSettingsService;
const config = snapshot.config[name];
form.replaceChildren();
form.className = "settings-form";
form.innerHTML = renderSettingsPanel(name, config);
form.querySelectorAll("input[data-service], input[data-arg-key], select[data-arg-key]").forEach((input) => {
input.addEventListener("input", () => {
configDirty = true;
});
input.addEventListener("change", () => {
configDirty = true;
});
});
form.querySelectorAll("[data-picker]").forEach((button) => {
button.addEventListener("click", () => {
runAction(() => pickPath(button.dataset.picker, button.dataset.target, button.dataset.serviceTarget));
});
});
form.querySelectorAll("[data-create-server-config]").forEach((button) => {
button.addEventListener("click", () => {
const template = document.querySelector("[data-config-template]")?.value || "server";
runAction(() => createArmaServerConfig(template));
});
});
form.querySelectorAll("[data-edit-server-config]").forEach((button) => {
button.addEventListener("click", () => {
const path = document.querySelector('[data-arg-key="config"]')?.value.trim();
if (!path) {
alert("Select or create a server config first.");
return;
}
runAction(() => openConfigEditor(resolveArmaConfigPath(path)));
});
});
form.querySelectorAll("[data-surreal-install]").forEach((button) => {
button.addEventListener("click", () => {
const version = selectedSurrealInstallVersion();
runAction(() => installSurrealDb(version));
});
});
form.querySelectorAll("[data-surreal-version-mode]").forEach((select) => {
select.addEventListener("change", () => {
syncSurrealCustomVersion();
});
});
form.querySelectorAll("[data-config-template]").forEach((select) => {
select.addEventListener("change", () => {
selectedArmaConfigTemplate = select.value;
});
});
if (name === "surrealdb" && !surrealInstallInfo) {
loadSurrealInstallInfo();
} else if (name === "surrealdb" && surrealInstallInfo && !surrealInstallInfo.latest) {
loadLatestSurrealVersion();
}
}
function isSettingsViewActive() {
return views.settings.classList.contains("active");
}
function syncRefreshTimer() {
if (refreshTimer) {
clearInterval(refreshTimer);
refreshTimer = null;
}
if (!isSettingsViewActive()) {
refreshTimer = setInterval(refresh, 2500);
}
}
function syncBulkActionsVisibility() {
const bulkActions = document.getElementById("bulkActions");
bulkActions.style.display = views.overview.classList.contains("active") ? "flex" : "none";
const stopAllButton = document.getElementById("stopAllButton");
const hasRunningService = Boolean(snapshot?.statuses.some((status) => status.running));
stopAllButton.disabled = !hasRunningService;
}
function renderLogs() {
const output = document.getElementById("logOutput");
output.textContent = snapshot.logs.length ? snapshot.logs.join("\n") : "No host logs yet.";
output.scrollTop = output.scrollHeight;
}
function renderError(error) {
const message = String(error);
document.getElementById("configPath").textContent = "Unable to load";
document.getElementById("serviceGrid").replaceChildren();
document.getElementById("configForm").replaceChildren();
document.getElementById("logOutput").textContent = message;
}
function readFormConfig() {
const config = structuredClone(snapshot.config);
document.querySelectorAll("[data-service][data-field]").forEach((input) => {
const service = input.dataset.service;
const field = input.dataset.field;
if (field === "enabled") {
config[service][field] = input.checked;
} else if (field === "health_port") {
config[service][field] = Number(input.value);
} else if (field === "args") {
config[service][field] = readArgumentFields(service, input);
} else {
config[service][field] = input.value.trim();
}
});
return config;
}
function renderSettingsPanel(name, config) {
if (name === "arma") {
return renderArmaSettings(config);
}
if (name === "icom") {
return renderIcomSettings(config);
}
return `
${serviceNames[name] || name}