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 lastRenderedLogText = "";
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 loadAppVersion() {
const getVersion = window.__TAURI__?.app?.getVersion;
if (!getVersion) return;
try {
document.getElementById("appVersion").textContent =
`v${await getVersion()}`;
} catch (error) {
console.warn("Unable to load app version:", error);
}
}
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 isLogsViewActive() {
return views.logs.classList.contains("active");
}
function syncRefreshTimer() {
if (refreshTimer) {
clearInterval(refreshTimer);
refreshTimer = null;
}
if (!isSettingsViewActive()) {
refreshTimer = setInterval(refresh, isLogsViewActive() ? 30000 : 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");
const nextLogText = snapshot.logs.length
? snapshot.logs.join("\n")
: "No host logs yet.";
if (nextLogText === lastRenderedLogText) return;
lastRenderedLogText = nextLogText;
output.textContent = nextLogText;
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();
lastRenderedLogText = message;
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}