Refresh host UI and show app version
- Add version display to the host shell - Improve log rendering and refresh timing - Tidy layout, styling, and window config
This commit is contained in:
parent
9610e3512d
commit
c3531a5839
1
.gitignore
vendored
1
.gitignore
vendored
@ -27,6 +27,7 @@ docus/.nuxt/
|
||||
docus/.output/
|
||||
docus/.data/
|
||||
docus/.nitro/
|
||||
bin/launcher/
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
|
||||
@ -1,26 +1,27 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "FORGE Host",
|
||||
"version": "0.1.0",
|
||||
"identifier": "com.idsolutions.forge.host",
|
||||
"build": {
|
||||
"frontendDist": "../src"
|
||||
},
|
||||
"app": {
|
||||
"withGlobalTauri": true,
|
||||
"windows": [
|
||||
{
|
||||
"title": "FORGE Host",
|
||||
"width": 1280,
|
||||
"height": 720,
|
||||
"minWidth": 980,
|
||||
"minHeight": 640
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": null
|
||||
}
|
||||
},
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "FORGE Host",
|
||||
"version": "0.1.0",
|
||||
"identifier": "com.idsolutions.forge.host",
|
||||
"build": {
|
||||
"frontendDist": "../src"
|
||||
},
|
||||
"app": {
|
||||
"withGlobalTauri": true,
|
||||
"windows": [
|
||||
{
|
||||
"title": "FORGE Host",
|
||||
"width": 1280,
|
||||
"height": 720,
|
||||
"minWidth": 980,
|
||||
"minHeight": 640,
|
||||
"maximizable": false
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": null
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
|
||||
@ -6,12 +6,14 @@ const serviceNames = {
|
||||
|
||||
const subtitles = {
|
||||
overview: "Start, stop, and check local Forge hosting services.",
|
||||
settings: "Edit process command, arguments, working directory, and health port.",
|
||||
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 = "";
|
||||
@ -28,7 +30,10 @@ document.querySelectorAll(".nav-button").forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
const view = button.dataset.view;
|
||||
if (button.dataset.service) {
|
||||
if (configDirty && button.dataset.service !== activeSettingsService) {
|
||||
if (
|
||||
configDirty &&
|
||||
button.dataset.service !== activeSettingsService
|
||||
) {
|
||||
persistActiveSettingsForm();
|
||||
}
|
||||
activeSettingsService = button.dataset.service;
|
||||
@ -38,9 +43,13 @@ document.querySelectorAll(".nav-button").forEach((button) => {
|
||||
}
|
||||
activeSettingsService = "surrealdb";
|
||||
}
|
||||
document.querySelectorAll(".nav-button").forEach((item) => item.classList.remove("active"));
|
||||
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));
|
||||
Object.entries(views).forEach(([key, element]) =>
|
||||
element.classList.toggle("active", key === view),
|
||||
);
|
||||
setHeader(view);
|
||||
renderSettings(true);
|
||||
syncBulkActionsVisibility();
|
||||
@ -52,9 +61,29 @@ 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));
|
||||
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) {
|
||||
@ -89,7 +118,9 @@ async function startAll() {
|
||||
|
||||
const serviceNames = Object.keys(snapshot.config).filter(
|
||||
(name) =>
|
||||
snapshot.config[name].enabled && (name === "surrealdb" || snapshot.config[name].command.trim() !== "")
|
||||
snapshot.config[name].enabled &&
|
||||
(name === "surrealdb" ||
|
||||
snapshot.config[name].command.trim() !== ""),
|
||||
);
|
||||
|
||||
for (const name of serviceNames) {
|
||||
@ -106,7 +137,9 @@ async function startAll() {
|
||||
|
||||
async function stopAll() {
|
||||
if (!snapshot) return;
|
||||
const serviceNames = Object.keys(snapshot.config).filter((name) => snapshot.config[name].enabled);
|
||||
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) {
|
||||
@ -141,7 +174,9 @@ async function saveConfig() {
|
||||
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 Promise.reject(
|
||||
"Tauri invoke API is not available in this window.",
|
||||
);
|
||||
}
|
||||
return invoke(command, payload);
|
||||
}
|
||||
@ -157,32 +192,43 @@ function render() {
|
||||
}
|
||||
|
||||
function setHeader(view) {
|
||||
const title = view === "settings" ? serviceNames[activeSettingsService] : activeNavLabel();
|
||||
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";
|
||||
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;
|
||||
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;
|
||||
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));
|
||||
});
|
||||
button.classList.toggle("service-enabled", Boolean(isOnline));
|
||||
button.classList.toggle("service-disabled", Boolean(isDisabled));
|
||||
button.classList.toggle("service-stopped", Boolean(isStopped));
|
||||
});
|
||||
}
|
||||
|
||||
function renderServices() {
|
||||
@ -192,31 +238,44 @@ function renderServices() {
|
||||
|
||||
snapshot.statuses.forEach((status) => {
|
||||
const node = template.content.firstElementChild.cloneNode(true);
|
||||
node.querySelector("h2").textContent = serviceNames[status.name] || status.name;
|
||||
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";
|
||||
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(".pid").textContent = status.pid
|
||||
? String(status.pid)
|
||||
: "-";
|
||||
node.querySelector(".health").textContent = status.health;
|
||||
node.querySelector(".enabled").textContent = status.enabled ? "Yes" : "No";
|
||||
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.");
|
||||
throw new Error(
|
||||
"Service card template is missing start or stop controls.",
|
||||
);
|
||||
}
|
||||
start.disabled = status.running || !status.enabled || !status.configured;
|
||||
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)));
|
||||
start.addEventListener("click", () =>
|
||||
runAction(() => startService(status.name)),
|
||||
);
|
||||
stop.addEventListener("click", () =>
|
||||
runAction(() => stopService(status.name)),
|
||||
);
|
||||
|
||||
grid.appendChild(node);
|
||||
});
|
||||
@ -256,7 +315,9 @@ function renderSettings(force = false) {
|
||||
form.className = "settings-form";
|
||||
form.innerHTML = renderSettingsPanel(name, config);
|
||||
|
||||
form.querySelectorAll("input[data-service], input[data-arg-key], select[data-arg-key]").forEach((input) => {
|
||||
form.querySelectorAll(
|
||||
"input[data-service], input[data-arg-key], select[data-arg-key]",
|
||||
).forEach((input) => {
|
||||
input.addEventListener("input", () => {
|
||||
configDirty = true;
|
||||
});
|
||||
@ -267,20 +328,30 @@ function renderSettings(force = false) {
|
||||
|
||||
form.querySelectorAll("[data-picker]").forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
runAction(() => pickPath(button.dataset.picker, button.dataset.target, button.dataset.serviceTarget));
|
||||
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";
|
||||
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();
|
||||
const path = document
|
||||
.querySelector('[data-arg-key="config"]')
|
||||
?.value.trim();
|
||||
if (!path) {
|
||||
alert("Select or create a server config first.");
|
||||
return;
|
||||
@ -310,7 +381,11 @@ function renderSettings(force = false) {
|
||||
|
||||
if (name === "surrealdb" && !surrealInstallInfo) {
|
||||
loadSurrealInstallInfo();
|
||||
} else if (name === "surrealdb" && surrealInstallInfo && !surrealInstallInfo.latest) {
|
||||
} else if (
|
||||
name === "surrealdb" &&
|
||||
surrealInstallInfo &&
|
||||
!surrealInstallInfo.latest
|
||||
) {
|
||||
loadLatestSurrealVersion();
|
||||
}
|
||||
}
|
||||
@ -319,6 +394,10 @@ function isSettingsViewActive() {
|
||||
return views.settings.classList.contains("active");
|
||||
}
|
||||
|
||||
function isLogsViewActive() {
|
||||
return views.logs.classList.contains("active");
|
||||
}
|
||||
|
||||
function syncRefreshTimer() {
|
||||
if (refreshTimer) {
|
||||
clearInterval(refreshTimer);
|
||||
@ -326,22 +405,32 @@ function syncRefreshTimer() {
|
||||
}
|
||||
|
||||
if (!isSettingsViewActive()) {
|
||||
refreshTimer = setInterval(refresh, 2500);
|
||||
refreshTimer = setInterval(refresh, isLogsViewActive() ? 30000 : 2500);
|
||||
}
|
||||
}
|
||||
|
||||
function syncBulkActionsVisibility() {
|
||||
const bulkActions = document.getElementById("bulkActions");
|
||||
bulkActions.style.display = views.overview.classList.contains("active") ? "flex" : "none";
|
||||
bulkActions.style.display = views.overview.classList.contains("active")
|
||||
? "flex"
|
||||
: "none";
|
||||
|
||||
const stopAllButton = document.getElementById("stopAllButton");
|
||||
const hasRunningService = Boolean(snapshot?.statuses.some((status) => status.running));
|
||||
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.";
|
||||
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;
|
||||
}
|
||||
|
||||
@ -350,6 +439,7 @@ function renderError(error) {
|
||||
document.getElementById("configPath").textContent = "Unable to load";
|
||||
document.getElementById("serviceGrid").replaceChildren();
|
||||
document.getElementById("configForm").replaceChildren();
|
||||
lastRenderedLogText = message;
|
||||
document.getElementById("logOutput").textContent = message;
|
||||
}
|
||||
|
||||
@ -418,7 +508,9 @@ 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.";
|
||||
const path =
|
||||
surrealInstallInfo?.path ||
|
||||
"SurrealDB will be installed to the user-local default path.";
|
||||
|
||||
return `
|
||||
<div class="field full surreal-install">
|
||||
@ -455,17 +547,23 @@ function renderSurrealInstallControls() {
|
||||
}
|
||||
|
||||
function selectedSurrealInstallVersion() {
|
||||
const mode = document.querySelector("[data-surreal-version-mode]")?.value || "3";
|
||||
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";
|
||||
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";
|
||||
const isCustom =
|
||||
document.querySelector("[data-surreal-version-mode]")?.value ===
|
||||
"custom";
|
||||
custom.hidden = !isCustom;
|
||||
if (isCustom) {
|
||||
custom.querySelector("input")?.focus();
|
||||
@ -475,7 +573,12 @@ function syncSurrealCustomVersion() {
|
||||
async function loadSurrealInstallInfo() {
|
||||
try {
|
||||
surrealInstallInfo = await tauriInvoke("get_surrealdb_install_info");
|
||||
if (activeSettingsService === "surrealdb" && isSettingsViewActive() && !configDirty && !isSurrealInstallFocused()) {
|
||||
if (
|
||||
activeSettingsService === "surrealdb" &&
|
||||
isSettingsViewActive() &&
|
||||
!configDirty &&
|
||||
!isSurrealInstallFocused()
|
||||
) {
|
||||
renderSettings(true);
|
||||
}
|
||||
await loadLatestSurrealVersion();
|
||||
@ -493,12 +596,25 @@ async function loadLatestSurrealVersion() {
|
||||
try {
|
||||
const latest = await tauriInvoke("get_latest_surrealdb_version");
|
||||
surrealInstallInfo = { ...(surrealInstallInfo || {}), latest };
|
||||
if (activeSettingsService === "surrealdb" && isSettingsViewActive() && !configDirty && !isSurrealInstallFocused()) {
|
||||
if (
|
||||
activeSettingsService === "surrealdb" &&
|
||||
isSettingsViewActive() &&
|
||||
!configDirty &&
|
||||
!isSurrealInstallFocused()
|
||||
) {
|
||||
renderSettings(true);
|
||||
}
|
||||
} catch (error) {
|
||||
surrealInstallInfo = { ...(surrealInstallInfo || {}), latest: "Unavailable" };
|
||||
if (activeSettingsService === "surrealdb" && isSettingsViewActive() && !configDirty && !isSurrealInstallFocused()) {
|
||||
surrealInstallInfo = {
|
||||
...(surrealInstallInfo || {}),
|
||||
latest: "Unavailable",
|
||||
};
|
||||
if (
|
||||
activeSettingsService === "surrealdb" &&
|
||||
isSettingsViewActive() &&
|
||||
!configDirty &&
|
||||
!isSurrealInstallFocused()
|
||||
) {
|
||||
renderSettings(true);
|
||||
}
|
||||
}
|
||||
@ -685,8 +801,10 @@ function toggleArgument(label, key, value) {
|
||||
}
|
||||
|
||||
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);
|
||||
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"));
|
||||
@ -707,7 +825,8 @@ function readArgumentFields(service, container) {
|
||||
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");
|
||||
checked("reportNonNetworkObject") &&
|
||||
args.push("-reportNonNetworkObject");
|
||||
args.push(checked("battleye") ? "-battleye" : "-noBattlEye");
|
||||
args.push(...splitMiscArgs(value("misc")));
|
||||
return args;
|
||||
@ -796,7 +915,10 @@ function parseArmaArgs(args) {
|
||||
}
|
||||
|
||||
function splitMiscArgs(value) {
|
||||
return value.split(/\s+/).map((arg) => arg.trim()).filter(Boolean);
|
||||
return value
|
||||
.split(/\s+/)
|
||||
.map((arg) => arg.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
async function pickPath(kind, targetSelector, service) {
|
||||
@ -818,7 +940,9 @@ async function pickPath(kind, targetSelector, service) {
|
||||
input.value = finalPath;
|
||||
|
||||
if (kind === "file" && targetSelector.includes('data-field="command"')) {
|
||||
const workingDir = document.querySelector(`[data-service="${service}"][data-field="working_dir"]`);
|
||||
const workingDir = document.querySelector(
|
||||
`[data-service="${service}"][data-field="working_dir"]`,
|
||||
);
|
||||
if (workingDir) {
|
||||
workingDir.value = finalPath.replace(/[\\/][^\\/]+$/, "");
|
||||
}
|
||||
@ -845,10 +969,14 @@ function pickerFilters(kind) {
|
||||
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.");
|
||||
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 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,
|
||||
@ -856,7 +984,10 @@ async function createArmaServerConfig(template) {
|
||||
});
|
||||
if (!selected) return;
|
||||
|
||||
await tauriInvoke("create_arma_server_config", { path: selected, template });
|
||||
await tauriInvoke("create_arma_server_config", {
|
||||
path: selected,
|
||||
template,
|
||||
});
|
||||
const input = document.querySelector('[data-arg-key="config"]');
|
||||
if (input) {
|
||||
input.value = selected;
|
||||
@ -871,7 +1002,9 @@ function resolveArmaConfigPath(path) {
|
||||
return path;
|
||||
}
|
||||
|
||||
const workingDir = document.querySelector('[data-service="arma"][data-field="working_dir"]')?.value.trim();
|
||||
const workingDir = document
|
||||
.querySelector('[data-service="arma"][data-field="working_dir"]')
|
||||
?.value.trim();
|
||||
return workingDir ? `${workingDir}\\${path}` : path;
|
||||
}
|
||||
|
||||
@ -906,11 +1039,15 @@ function normalizePickedExecutable(service, 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.");
|
||||
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.");
|
||||
alert(
|
||||
"Select arma3server_x64.exe for dedicated server hosting. The app will block client executables at launch.",
|
||||
);
|
||||
return path;
|
||||
}
|
||||
|
||||
@ -944,3 +1081,4 @@ function selectedAttr(selected) {
|
||||
|
||||
refresh();
|
||||
syncRefreshTimer();
|
||||
loadAppVersion();
|
||||
|
||||
@ -1,145 +1,225 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>FORGE Host</title>
|
||||
<link rel="stylesheet" href="./styles.css" />
|
||||
</head>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>FORGE Host</title>
|
||||
<link rel="stylesheet" href="./styles.css" />
|
||||
</head>
|
||||
<body>
|
||||
<main class="shell">
|
||||
<aside class="rail">
|
||||
<nav class="nav">
|
||||
<button
|
||||
class="nav-button active"
|
||||
type="button"
|
||||
data-view="overview"
|
||||
title="Overview"
|
||||
>
|
||||
<span class="svg-icon icon-dashboard"></span>
|
||||
<span>Overview</span>
|
||||
</button>
|
||||
<button
|
||||
class="nav-button"
|
||||
type="button"
|
||||
data-view="settings"
|
||||
title="Settings"
|
||||
>
|
||||
<span class="svg-icon icon-database"></span>
|
||||
<span>SurrealDB</span>
|
||||
</button>
|
||||
<button
|
||||
class="nav-button"
|
||||
type="button"
|
||||
data-view="settings"
|
||||
data-service="icom"
|
||||
title="ICOM"
|
||||
>
|
||||
<span class="svg-icon icon-icom"></span>
|
||||
<span>ICOM</span>
|
||||
</button>
|
||||
<button
|
||||
class="nav-button"
|
||||
type="button"
|
||||
data-view="settings"
|
||||
data-service="arma"
|
||||
title="Arma 3 Server"
|
||||
>
|
||||
<span class="svg-icon icon-server"></span>
|
||||
<span>Arma Server</span>
|
||||
</button>
|
||||
<button
|
||||
class="nav-button"
|
||||
type="button"
|
||||
data-view="logs"
|
||||
title="Logs"
|
||||
>
|
||||
<span class="svg-icon icon-logs"></span>
|
||||
<span>Logs</span>
|
||||
</button>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<body>
|
||||
<main class="shell">
|
||||
<aside class="rail">
|
||||
<nav class="nav">
|
||||
<button class="nav-button active" type="button" data-view="overview" title="Overview">
|
||||
<span class="svg-icon icon-dashboard"></span>
|
||||
<span>Overview</span>
|
||||
</button>
|
||||
<button class="nav-button" type="button" data-view="settings" title="Settings">
|
||||
<span class="svg-icon icon-database"></span>
|
||||
<span>SurrealDB</span>
|
||||
</button>
|
||||
<button class="nav-button" type="button" data-view="settings" data-service="icom" title="ICOM">
|
||||
<span class="svg-icon icon-icom"></span>
|
||||
<span>ICOM</span>
|
||||
</button>
|
||||
<button class="nav-button" type="button" data-view="settings" data-service="arma" title="Arma 3 Server">
|
||||
<span class="svg-icon icon-server"></span>
|
||||
<span>Arma Server</span>
|
||||
</button>
|
||||
<button class="nav-button" type="button" data-view="logs" title="Logs">
|
||||
<span class="svg-icon icon-logs"></span>
|
||||
<span>Logs</span>
|
||||
</button>
|
||||
</nav>
|
||||
<div class="config-path">
|
||||
<span>Config</span>
|
||||
<strong id="configPath">Loading</strong>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<section class="workspace">
|
||||
<header class="topbar">
|
||||
<div>
|
||||
<h1 id="viewTitle">Overview</h1>
|
||||
<p id="viewSubtitle">Local service control for Forge server hosting.</p>
|
||||
</div>
|
||||
<div class="top-actions">
|
||||
<div id="bulkActions" class="bulk-actions" style="display: none;">
|
||||
<button id="startAllButton" class="start icon-button" type="button" aria-label="Start All"
|
||||
title="Start all services">
|
||||
<span class="svg-icon icon-play"></span>
|
||||
<section class="workspace">
|
||||
<header class="topbar">
|
||||
<div class="view-heading">
|
||||
<h1 id="viewTitle">Overview</h1>
|
||||
<p id="viewSubtitle">
|
||||
Local service control for Forge server hosting.
|
||||
</p>
|
||||
</div>
|
||||
<div class="top-actions">
|
||||
<div
|
||||
id="bulkActions"
|
||||
class="bulk-actions"
|
||||
style="display: none"
|
||||
>
|
||||
<button
|
||||
id="startAllButton"
|
||||
class="start icon-button"
|
||||
type="button"
|
||||
aria-label="Start All"
|
||||
title="Start all services"
|
||||
>
|
||||
<span class="svg-icon icon-play"></span>
|
||||
</button>
|
||||
<button
|
||||
id="stopAllButton"
|
||||
class="stop icon-button"
|
||||
type="button"
|
||||
aria-label="Stop All"
|
||||
title="Stop all services"
|
||||
>
|
||||
<span class="svg-icon icon-stop"></span>
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
id="refreshButton"
|
||||
class="primary icon-button"
|
||||
type="button"
|
||||
aria-label="Refresh"
|
||||
title="Refresh"
|
||||
>
|
||||
<span class="svg-icon icon-refresh"></span>
|
||||
</button>
|
||||
<button id="stopAllButton" class="stop icon-button" type="button" aria-label="Stop All"
|
||||
title="Stop all services">
|
||||
<span class="svg-icon icon-stop"></span>
|
||||
<button
|
||||
id="saveButton"
|
||||
class="icon-button"
|
||||
type="button"
|
||||
aria-label="Save"
|
||||
title="Save"
|
||||
>
|
||||
<span class="svg-icon icon-save"></span>
|
||||
</button>
|
||||
</div>
|
||||
<button id="refreshButton" class="primary icon-button" type="button" aria-label="Refresh" title="Refresh">
|
||||
<div class="config-path">
|
||||
<span>Config</span>
|
||||
<strong id="configPath">Loading</strong>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section id="overviewView" class="view active">
|
||||
<div id="serviceGrid" class="service-grid"></div>
|
||||
</section>
|
||||
|
||||
<section id="settingsView" class="view">
|
||||
<form id="configForm" class="settings-grid"></form>
|
||||
</section>
|
||||
|
||||
<section id="logsView" class="view">
|
||||
<div id="logOutput" class="log-output"></div>
|
||||
</section>
|
||||
</section>
|
||||
<div class="version-display" title="FORGE Host version">
|
||||
<span id="appVersion" class="version-text">v0.1.0</span>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div id="editorOverlay" class="editor-overlay" hidden>
|
||||
<section class="editor-modal">
|
||||
<header class="editor-header">
|
||||
<div>
|
||||
<h2>Server Config</h2>
|
||||
<p id="editorPath"></p>
|
||||
</div>
|
||||
<button
|
||||
id="editorCloseButton"
|
||||
class="icon-button"
|
||||
type="button"
|
||||
title="Close"
|
||||
aria-label="Close"
|
||||
>
|
||||
<span class="svg-icon icon-close"></span>
|
||||
</button>
|
||||
</header>
|
||||
<textarea id="configEditor" spellcheck="false"></textarea>
|
||||
<footer class="editor-actions">
|
||||
<button
|
||||
id="editorReloadButton"
|
||||
class="primary"
|
||||
type="button"
|
||||
aria-label="Refresh"
|
||||
>
|
||||
<span class="svg-icon icon-refresh"></span>
|
||||
</button>
|
||||
<button id="saveButton" class="icon-button" type="button" aria-label="Save" title="Save">
|
||||
<button
|
||||
id="editorSaveButton"
|
||||
class="icon-button"
|
||||
type="button"
|
||||
aria-label="Save"
|
||||
>
|
||||
<span class="svg-icon icon-save"></span>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section id="overviewView" class="view active">
|
||||
<div id="serviceGrid" class="service-grid"></div>
|
||||
</footer>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section id="settingsView" class="view">
|
||||
<form id="configForm" class="settings-grid"></form>
|
||||
</section>
|
||||
|
||||
<section id="logsView" class="view">
|
||||
<div id="logOutput" class="log-output"></div>
|
||||
</section>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<div id="editorOverlay" class="editor-overlay" hidden>
|
||||
<section class="editor-modal">
|
||||
<header class="editor-header">
|
||||
<div>
|
||||
<h2>Server Config</h2>
|
||||
<p id="editorPath"></p>
|
||||
</div>
|
||||
<button id="editorCloseButton" class="icon-button" type="button" title="Close" aria-label="Close">
|
||||
<span class="svg-icon icon-close"></span>
|
||||
</button>
|
||||
</header>
|
||||
<textarea id="configEditor" spellcheck="false"></textarea>
|
||||
<footer class="editor-actions">
|
||||
<button id="editorReloadButton" class="primary" type="button" aria-label="Refresh">
|
||||
<span class="svg-icon icon-refresh"></span>
|
||||
</button>
|
||||
<button id="editorSaveButton" class="icon-button" type="button" aria-label="Save">
|
||||
<span class="svg-icon icon-save"></span>
|
||||
</button>
|
||||
</footer>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<template id="serviceCardTemplate">
|
||||
<article class="service-card">
|
||||
<header>
|
||||
<div>
|
||||
<div class="service-title-row">
|
||||
<h2></h2>
|
||||
<span class="ping-pill"></span>
|
||||
<template id="serviceCardTemplate">
|
||||
<article class="service-card">
|
||||
<header>
|
||||
<div>
|
||||
<div class="service-title-row">
|
||||
<h2></h2>
|
||||
<span class="ping-pill"></span>
|
||||
</div>
|
||||
<p class="command-line"></p>
|
||||
</div>
|
||||
<p class="command-line"></p>
|
||||
<span class="status-pill"></span>
|
||||
</header>
|
||||
<dl class="metrics">
|
||||
<div>
|
||||
<dt>Process</dt>
|
||||
<dd class="pid"></dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Health</dt>
|
||||
<dd class="health"></dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Enabled</dt>
|
||||
<dd class="enabled"></dd>
|
||||
</div>
|
||||
</dl>
|
||||
<div class="card-actions">
|
||||
<button
|
||||
class="start icon-button"
|
||||
type="button"
|
||||
aria-label="Start"
|
||||
>
|
||||
<span class="svg-icon icon-play"></span>
|
||||
</button>
|
||||
<button
|
||||
class="stop icon-button"
|
||||
type="button"
|
||||
aria-label="Stop"
|
||||
>
|
||||
<span class="svg-icon icon-stop"></span>
|
||||
</button>
|
||||
</div>
|
||||
<span class="status-pill"></span>
|
||||
</header>
|
||||
<dl class="metrics">
|
||||
<div>
|
||||
<dt>Process</dt>
|
||||
<dd class="pid"></dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Health</dt>
|
||||
<dd class="health"></dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Enabled</dt>
|
||||
<dd class="enabled"></dd>
|
||||
</div>
|
||||
</dl>
|
||||
<div class="card-actions">
|
||||
<button class="start icon-button" type="button" aria-label="Start">
|
||||
<span class="svg-icon icon-play"></span>
|
||||
</button>
|
||||
<button class="stop icon-button" type="button" aria-label="Stop">
|
||||
<span class="svg-icon icon-stop"></span>
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<script src="./app.js"></script>
|
||||
</body>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<script src="./app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
BIN
bin/host/src/public/layer0.png
Normal file
BIN
bin/host/src/public/layer0.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.4 MiB |
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user