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}

${renderSurrealInstallControls()}
${renderArgumentFields(name, config.args)}
`; } function renderSurrealInstallControls() { const installed = surrealInstallInfo?.installed; const version = surrealInstallInfo?.version || "Not installed"; const latest = surrealInstallInfo?.latest || "Checking..."; const path = surrealInstallInfo?.path || "SurrealDB will be installed to the user-local default path."; return `
${escapeHtml(version)} ${escapeHtml(path)}
${escapeHtml(latest)} ${installed ? "Choose a target version when updating." : "Install before starting SurrealDB."}
`; } function selectedSurrealInstallVersion() { const mode = document.querySelector("[data-surreal-version-mode]")?.value || "3"; if (mode !== "custom") { return mode; } return ( document.querySelector("[data-surreal-custom-version]")?.value.trim() || "3" ); } function syncSurrealCustomVersion() { const custom = document.querySelector(".install-custom"); if (!custom) return; const isCustom = document.querySelector("[data-surreal-version-mode]")?.value === "custom"; custom.hidden = !isCustom; if (isCustom) { custom.querySelector("input")?.focus(); } } async function loadSurrealInstallInfo() { try { surrealInstallInfo = await tauriInvoke("get_surrealdb_install_info"); if ( activeSettingsService === "surrealdb" && isSettingsViewActive() && !configDirty && !isSurrealInstallFocused() ) { renderSettings(true); } await loadLatestSurrealVersion(); } catch (error) { surrealInstallInfo = { installed: false, version: "Unable to check", path: String(error), latest: "Unknown", }; } } async function loadLatestSurrealVersion() { try { const latest = await tauriInvoke("get_latest_surrealdb_version"); surrealInstallInfo = { ...(surrealInstallInfo || {}), latest }; if ( activeSettingsService === "surrealdb" && isSettingsViewActive() && !configDirty && !isSurrealInstallFocused() ) { renderSettings(true); } } catch (error) { surrealInstallInfo = { ...(surrealInstallInfo || {}), latest: "Unavailable", }; if ( activeSettingsService === "surrealdb" && isSettingsViewActive() && !configDirty && !isSurrealInstallFocused() ) { renderSettings(true); } } } function isSurrealInstallFocused() { return Boolean(document.activeElement?.closest?.(".surreal-install")); } async function installSurrealDb(version) { surrealInstallInfo = await tauriInvoke("install_surrealdb", { version }); await refresh(); if (activeSettingsService === "surrealdb" && isSettingsViewActive()) { renderSettings(true); } } function renderIcomSettings(config) { return `

ICOM

${renderArgumentFields("icom", config.args)}
`; } function renderArmaSettings(config) { const parsed = parseArmaArgs(config.args); return `

Arma Server

${serverConfigInput(parsed.config)} ${serverConfigTemplateInput()} ${argumentInput("Port", "port", parsed.port)} ${argumentInput("Profiles", "profiles", parsed.profiles)} ${argumentInput("Name", "name", parsed.name)} ${argumentInput("Mods", "mods", parsed.mods)} ${argumentInput("Server Mods", "serverMods", parsed.serverMods)} ${toggleArgument("AutoInit", "autoInit", parsed.autoInit)} ${toggleArgument("ReportNonNetworkObject", "reportNonNetworkObject", parsed.reportNonNetworkObject)} ${toggleArgument("BattlEye", "battleye", parsed.battleye)} ${argumentInput("Misc Arguments", "misc", parsed.misc.join(" "), "full")}
`; } function persistActiveSettingsForm() { const form = document.getElementById("configForm"); if (!snapshot || !form.children.length) return; snapshot.config = readFormConfig(); } function renderArgumentFields(service, args) { if (service === "surrealdb") { const parsed = parseSurrealArgs(args); return ` ${argumentInput("Subcommand", "subcommand", parsed.subcommand)} ${argumentInput("Root User", "user", parsed.user)} ${argumentInput("Root Password", "pass", parsed.pass)} ${argumentInput("Bind Address", "bind", parsed.bind)} ${argumentInput("Database Path", "database", parsed.database)} ${argumentInput("Misc Arguments", "misc", parsed.misc.join(" "), "full")} `; } const label = service === "arma" ? "Startup Parameters" : "Misc Arguments"; return argumentInput(label, "misc", args.join(" "), "full"); } function argumentInput(label, key, value, className = "") { return `
`; } function serverConfigInput(value) { return `
`; } function serverConfigTemplateInput() { return `
`; } function toggleArgument(label, key, value) { return ` `; } function readArgumentFields(service, container) { const value = (key) => container.querySelector(`[data-arg-key="${key}"]`)?.value.trim() || ""; const checked = (key) => Boolean(container.querySelector(`[data-arg-key="${key}"]`)?.checked); if (service === "surrealdb") { const args = []; if (value("subcommand")) args.push(value("subcommand")); if (value("user")) args.push("--user", value("user")); if (value("pass")) args.push("--pass", value("pass")); if (value("bind")) args.push("--bind", value("bind")); args.push(...splitMiscArgs(value("misc"))); if (value("database")) args.push(value("database")); return args; } if (service === "arma") { const args = []; if (value("config")) args.push(`-config=${value("config")}`); if (value("port")) args.push(`-port=${value("port")}`); if (value("profiles")) args.push(`-profiles=${value("profiles")}`); if (value("name")) args.push(`-name=${value("name")}`); if (value("mods")) args.push(`-mod=${value("mods")}`); if (value("serverMods")) args.push(`-serverMod=${value("serverMods")}`); checked("autoInit") && args.push("-autoInit"); checked("reportNonNetworkObject") && args.push("-reportNonNetworkObject"); args.push(checked("battleye") ? "-battleye" : "-noBattlEye"); args.push(...splitMiscArgs(value("misc"))); return args; } return splitMiscArgs(value("misc")); } function parseSurrealArgs(args) { const parsed = { subcommand: "", user: "", pass: "", bind: "", database: "", misc: [], }; const remaining = [...args]; if (remaining[0] && !remaining[0].startsWith("-")) { parsed.subcommand = remaining.shift(); } for (let index = 0; index < remaining.length; index += 1) { const arg = remaining[index]; if (arg === "--user") { parsed.user = remaining[++index] || ""; } else if (arg === "--pass") { parsed.pass = remaining[++index] || ""; } else if (arg === "--bind") { parsed.bind = remaining[++index] || ""; } else if (!arg.startsWith("-")) { parsed.database = arg; } else { parsed.misc.push(arg); } } return parsed; } function parseArmaArgs(args) { const parsed = { config: "", port: "", profiles: "", name: "", mods: "", serverMods: "", autoInit: false, reportNonNetworkObject: false, battleye: true, misc: [], }; for (const arg of args) { if (!arg) continue; const lowerArg = arg.toLowerCase(); if (lowerArg.startsWith("-config=")) { parsed.config = arg.slice("-config=".length); } else if (lowerArg.startsWith("-port=")) { parsed.port = arg.slice("-port=".length); } else if (lowerArg.startsWith("-profiles=")) { parsed.profiles = arg.slice("-profiles=".length); } else if (lowerArg.startsWith("-name=")) { parsed.name = arg.slice("-name=".length); } else if (lowerArg.startsWith("-mod=")) { parsed.mods = arg.slice("-mod=".length); } else if (lowerArg.startsWith("-servermod=")) { parsed.serverMods = arg.slice("-serverMod=".length); } else if (lowerArg === "-nobattleye") { parsed.battleye = false; } else if (lowerArg === "-battleye") { parsed.battleye = true; } else if (lowerArg === "-autoinit") { parsed.autoInit = true; } else if (lowerArg === "-reportnonnetworkobject") { parsed.reportNonNetworkObject = true; } else { parsed.misc.push(arg); } } return parsed; } function splitMiscArgs(value) { return value .split(/\s+/) .map((arg) => arg.trim()) .filter(Boolean); } async function pickPath(kind, targetSelector, service) { const open = window.__TAURI__?.dialog?.open; if (!open) { throw new Error("Tauri dialog API is not available in this window."); } const selected = await open({ multiple: false, directory: kind === "directory", filters: pickerFilters(kind), }); if (!selected) return; const input = document.querySelector(targetSelector); if (!input) return; const finalPath = normalizePickedExecutable(service, selected); input.value = finalPath; if (kind === "file" && targetSelector.includes('data-field="command"')) { const workingDir = document.querySelector( `[data-service="${service}"][data-field="working_dir"]`, ); if (workingDir) { workingDir.value = finalPath.replace(/[\\/][^\\/]+$/, ""); } } configDirty = true; persistActiveSettingsForm(); if (kind === "config") { await openConfigEditor(resolveArmaConfigPath(finalPath)); } } function pickerFilters(kind) { if (kind === "file") { return [{ name: "Executable", extensions: ["exe"] }]; } if (kind === "config") { return [{ name: "Arma Server Config", extensions: ["cfg"] }]; } return undefined; } async function createArmaServerConfig(template) { const save = window.__TAURI__?.dialog?.save; if (!save) { throw new Error( "Tauri save dialog API is not available in this window.", ); } const workingDir = document .querySelector('[data-service="arma"][data-field="working_dir"]') ?.value.trim(); const defaultPath = workingDir ? `${workingDir}\\server.cfg` : "server.cfg"; const selected = await save({ defaultPath, filters: [{ name: "Arma Server Config", extensions: ["cfg"] }], }); if (!selected) return; await tauriInvoke("create_arma_server_config", { path: selected, template, }); const input = document.querySelector('[data-arg-key="config"]'); if (input) { input.value = selected; configDirty = true; persistActiveSettingsForm(); } await openConfigEditor(selected); } function resolveArmaConfigPath(path) { if (/^[a-zA-Z]:[\\/]/.test(path) || path.startsWith("\\\\")) { return path; } const workingDir = document .querySelector('[data-service="arma"][data-field="working_dir"]') ?.value.trim(); return workingDir ? `${workingDir}\\${path}` : path; } async function openConfigEditor(path) { if (!path) return; activeEditorPath = path; const content = await tauriInvoke("read_text_file", { path }); document.getElementById("editorPath").textContent = path; document.getElementById("configEditor").value = content; document.getElementById("editorOverlay").hidden = false; document.getElementById("configEditor").focus(); } function closeConfigEditor() { document.getElementById("editorOverlay").hidden = true; } async function saveConfigEditor() { if (!activeEditorPath) return; const content = document.getElementById("configEditor").value; await tauriInvoke("write_text_file", { path: activeEditorPath, content }); } function normalizePickedExecutable(service, path) { if (service !== "arma") { return path; } const fileName = path.split(/[\\/]/).pop()?.toLowerCase() || ""; if (isArmaServerExecutable(fileName)) { return path; } if (fileName === "arma3_x64.exe" || fileName === "arma3.exe") { alert( "That is the Arma 3 client executable. Forge Host will use arma3server_x64.exe from the same directory instead.", ); return path.replace(/[\\/][^\\/]+$/, "\\arma3server_x64.exe"); } alert( "Select arma3server_x64.exe for dedicated server hosting. The app will block client executables at launch.", ); return path; } function isArmaServerExecutable(fileName) { return fileName.startsWith("arma3server"); } async function runAction(action) { try { await action(); } catch (error) { alert(String(error)); await refresh(); } } function escapeHtml(value) { return String(value) .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">"); } function escapeAttr(value) { return escapeHtml(value).replaceAll('"', """); } function selectedAttr(selected) { return selected ? "selected" : ""; } refresh(); syncRefreshTimer(); loadAppVersion();