Refactor config loading and update SurrealDB setup documentation
- Refactored the `load` function in `config.rs` to use a new `locate_config_path` function for improved clarity and maintainability. - Updated SurrealDB setup instructions in `SURREALDB_SETUP.md` and `surrealdb-setup.md` to reflect changes in the installation process and removed outdated script references. - Added new dependencies for Tauri in `package.json` and `package-lock.json`, including `@tauri-apps/cli` and `@tauri-apps/plugin-dialog`. - Updated the package-lock to include the latest versions of Tauri dependencies.
@ -1,6 +1,7 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"arma/server/extension",
|
||||
"bin/host/src-tauri",
|
||||
"bin/icom",
|
||||
"lib/models",
|
||||
"lib/repositories",
|
||||
|
||||
26
README.md
@ -28,6 +28,7 @@ arma/
|
||||
client/ Client-side addons and browser UIs
|
||||
server/ Server-side addons and extension crate
|
||||
bin/
|
||||
host/ Tauri host control panel for SurrealDB, ICOM, and Arma server
|
||||
icom/ Interprocess communication helper
|
||||
lib/
|
||||
models/ Shared domain models
|
||||
@ -42,6 +43,7 @@ tools/ Web UI build tooling
|
||||
```powershell
|
||||
cargo test
|
||||
npm run build:webui
|
||||
npm run host:dev
|
||||
.\build-arma.ps1
|
||||
```
|
||||
|
||||
@ -64,3 +66,27 @@ clients, and `@forge_server` on the server only.
|
||||
```
|
||||
|
||||
Both commands report the persistence connection state.
|
||||
|
||||
## Host Control Panel
|
||||
|
||||
`bin/host` contains a Tauri control app for local Forge server hosting. It can
|
||||
start and stop SurrealDB, `forge-icom.exe`, and an Arma 3 dedicated server,
|
||||
edit their launch commands, and show basic TCP health plus captured logs.
|
||||
|
||||
```powershell
|
||||
# Build ICOM first if the host config points at target/release/forge-icom.exe
|
||||
cargo build --release -p forge-icom
|
||||
|
||||
# Run the host UI during development
|
||||
npm run host:dev
|
||||
|
||||
# Build the packaged desktop app
|
||||
npm run host:build
|
||||
```
|
||||
|
||||
The app reads the shared `config.toml` from the repo root during development, or
|
||||
from the current/executable directory in packaged use. If no shared config exists,
|
||||
it falls back to `bin/host/host.example.toml`; saving from the Settings view writes
|
||||
the active shared `config.toml`. The shared file includes the host process sections
|
||||
plus the `[server]` section used by ICOM and the `[surreal]` section used by the
|
||||
Arma extension.
|
||||
|
||||
@ -41,14 +41,7 @@ impl Default for SurrealConfig {
|
||||
pub fn load() -> Config {
|
||||
CONFIG_CACHE
|
||||
.get_or_init(|| {
|
||||
let config_path = std::env::current_exe()
|
||||
.ok()
|
||||
.and_then(|exe| {
|
||||
exe.parent()
|
||||
.map(|dir| dir.join("@forge_server").join("config.toml"))
|
||||
})
|
||||
.filter(|path| path.exists())
|
||||
.unwrap_or_else(|| PathBuf::from("@forge_server/config.toml"));
|
||||
let config_path = locate_config_path();
|
||||
|
||||
match fs::read_to_string(&config_path) {
|
||||
Ok(contents) => {
|
||||
@ -77,3 +70,24 @@ pub fn load() -> Config {
|
||||
})
|
||||
.clone()
|
||||
}
|
||||
|
||||
fn locate_config_path() -> PathBuf {
|
||||
let mut candidates = Vec::new();
|
||||
|
||||
if let Ok(cwd) = std::env::current_dir() {
|
||||
candidates.push(cwd.join("@forge_server").join("config.toml"));
|
||||
candidates.push(cwd.join("config.toml"));
|
||||
}
|
||||
|
||||
if let Ok(exe) = std::env::current_exe() {
|
||||
if let Some(dir) = exe.parent() {
|
||||
candidates.push(dir.join("@forge_server").join("config.toml"));
|
||||
candidates.push(dir.join("config.toml"));
|
||||
}
|
||||
}
|
||||
|
||||
candidates
|
||||
.into_iter()
|
||||
.find(|path| path.exists())
|
||||
.unwrap_or_else(|| PathBuf::from("@forge_server/config.toml"))
|
||||
}
|
||||
|
||||
@ -1,8 +0,0 @@
|
||||
@echo off
|
||||
setlocal EnableExtensions
|
||||
set "FORGE_SURREALDB_VERSION=%~1"
|
||||
if not defined FORGE_SURREALDB_VERSION set "FORGE_SURREALDB_VERSION=3"
|
||||
|
||||
call "%~dp0UpdateMe.bat" "%FORGE_SURREALDB_VERSION%"
|
||||
if errorlevel 1 exit /b %errorlevel%
|
||||
call "%~dp0RunMe.bat"
|
||||
@ -1,100 +1,23 @@
|
||||
# Forge SurrealDB
|
||||
|
||||
Forge uses SurrealDB as the durable database for the server extension. These
|
||||
helpers install the SurrealDB CLI and start a local RocksDB-backed Forge
|
||||
database from this directory.
|
||||
Forge uses SurrealDB as the durable database for the server extension. The
|
||||
Forge Host app installs, updates, starts, and stops the local SurrealDB process.
|
||||
|
||||
These scripts are for local development and single-host Forge servers. For a
|
||||
public or shared production host, change the root password and review bind,
|
||||
firewall, TLS, backup, and upgrade policy before exposing the database.
|
||||
Use the SurrealDB view in Forge Host to:
|
||||
|
||||
## Windows
|
||||
- Install or update the SurrealDB CLI.
|
||||
- Start the local RocksDB-backed database.
|
||||
- Configure the database bind address, root credentials, and database path.
|
||||
|
||||
Install or update SurrealDB to the newest compatible SurrealDB 3.x release:
|
||||
|
||||
```bat
|
||||
UpdateMe.bat
|
||||
```
|
||||
|
||||
Install a specific SurrealDB release:
|
||||
|
||||
```bat
|
||||
UpdateMe.bat v3.1.2
|
||||
```
|
||||
|
||||
Install the latest stable SurrealDB release, including newer major versions:
|
||||
|
||||
```bat
|
||||
UpdateMe.bat latest
|
||||
```
|
||||
|
||||
`latest` requires confirmation because a newer SurrealDB major version can
|
||||
require rebuilding the Forge server extension from source with a compatible
|
||||
`surrealdb` Rust crate.
|
||||
|
||||
The PowerShell entry point exposes the same behavior:
|
||||
|
||||
```powershell
|
||||
.\UpdateSurrealDB.ps1
|
||||
.\UpdateSurrealDB.ps1 -Version v3.1.2
|
||||
.\UpdateSurrealDB.ps1 -Version latest
|
||||
```
|
||||
|
||||
If this is the first install and the terminal cannot find `surreal` after the
|
||||
script finishes, open a new terminal so Windows reloads `PATH`.
|
||||
|
||||
Start Forge's local database:
|
||||
|
||||
```bat
|
||||
RunMe.bat
|
||||
```
|
||||
|
||||
Or start it directly with PowerShell:
|
||||
|
||||
```powershell
|
||||
.\RunSurrealDB.ps1
|
||||
```
|
||||
|
||||
Install and start in one step:
|
||||
|
||||
```bat
|
||||
AllInOne.bat
|
||||
```
|
||||
|
||||
`AllInOne.bat` also defaults to the newest compatible SurrealDB 3.x release.
|
||||
Pass the same version argument as `UpdateMe.bat` to override it.
|
||||
|
||||
## Linux or macOS
|
||||
|
||||
Install SurrealDB:
|
||||
|
||||
```bash
|
||||
./setup.sh
|
||||
```
|
||||
|
||||
Start Forge's local database:
|
||||
|
||||
```bash
|
||||
./run.sh
|
||||
```
|
||||
|
||||
Update SurrealDB:
|
||||
|
||||
```bash
|
||||
./update.sh
|
||||
```
|
||||
|
||||
## Manual Command
|
||||
|
||||
The run scripts execute:
|
||||
The default local launch arguments are:
|
||||
|
||||
```bash
|
||||
surreal start --user root --pass root --bind 127.0.0.1:8000 rocksdb://forge.db
|
||||
```
|
||||
|
||||
The database files are created under `arma/server/surrealdb/forge.db`.
|
||||
The default database files are created under this directory as `forge.db`.
|
||||
|
||||
Forge's extension config should match the local SurrealDB server:
|
||||
Forge's shared `config.toml` should match the local SurrealDB server:
|
||||
|
||||
```toml
|
||||
[surreal]
|
||||
@ -105,3 +28,6 @@ username = "root"
|
||||
password = "root"
|
||||
connect_timeout_ms = 5000
|
||||
```
|
||||
|
||||
`root`/`root` is only the local development default. For a public or shared
|
||||
server, set a real password and keep `config.toml` aligned.
|
||||
|
||||
@ -1,2 +0,0 @@
|
||||
@echo off
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0RunSurrealDB.ps1"
|
||||
@ -1,16 +0,0 @@
|
||||
param(
|
||||
[string]$User = "root",
|
||||
[string]$Pass = "root",
|
||||
[string]$Bind = "127.0.0.1:8000",
|
||||
[string]$DatabasePath = "forge.db"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
Set-Location $PSScriptRoot
|
||||
|
||||
if (-not (Get-Command surreal -ErrorAction SilentlyContinue)) {
|
||||
throw "The 'surreal' command was not found. Run UpdateSurrealDB.ps1 first, then open a new terminal if PATH was updated."
|
||||
}
|
||||
|
||||
surreal start --user $User --pass $Pass --bind $Bind "rocksdb://$DatabasePath"
|
||||
@ -1,10 +0,0 @@
|
||||
@echo off
|
||||
setlocal EnableExtensions
|
||||
set "DEFAULT_SURREALDB_VERSION=3"
|
||||
set "TARGET_SURREALDB_VERSION=%~1"
|
||||
|
||||
if not defined TARGET_SURREALDB_VERSION set "TARGET_SURREALDB_VERSION=%FORGE_SURREALDB_VERSION%"
|
||||
if not defined TARGET_SURREALDB_VERSION set "TARGET_SURREALDB_VERSION=%DEFAULT_SURREALDB_VERSION%"
|
||||
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0UpdateSurrealDB.ps1" -Version "%TARGET_SURREALDB_VERSION%"
|
||||
exit /b %errorlevel%
|
||||
@ -1,120 +0,0 @@
|
||||
param(
|
||||
[string]$Version = "3",
|
||||
[switch]$Force
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$ProgressPreference = "SilentlyContinue"
|
||||
|
||||
$VersionUrl = "https://version.surrealdb.com"
|
||||
$DownloadBaseUrl = "https://download.surrealdb.com"
|
||||
$Architecture = "windows-amd64"
|
||||
|
||||
function Normalize-Version {
|
||||
param([string]$Value)
|
||||
|
||||
$trimmed = $Value.Trim()
|
||||
if ($trimmed -match "(?i)^latest$") {
|
||||
return "latest"
|
||||
}
|
||||
if ($trimmed -match "^v?\d+$") {
|
||||
return $trimmed.TrimStart("v")
|
||||
}
|
||||
if ($trimmed -match "^v?\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$") {
|
||||
return "v$($trimmed.TrimStart("v"))"
|
||||
}
|
||||
|
||||
throw "Unsupported SurrealDB version '$Value'. Use a major version like '3', an exact version like 'v3.1.2', or 'latest'."
|
||||
}
|
||||
|
||||
function Get-Latest-Version {
|
||||
return (Invoke-WebRequest $VersionUrl -UseBasicParsing).Content.Trim()
|
||||
}
|
||||
|
||||
function Resolve-Version {
|
||||
param([string]$Target)
|
||||
|
||||
$normalized = Normalize-Version $Target
|
||||
if ($normalized -eq "latest") {
|
||||
return Get-Latest-Version
|
||||
}
|
||||
|
||||
if ($normalized -match "^\d+$") {
|
||||
$latest = Get-Latest-Version
|
||||
if ($latest -notmatch "^v?$normalized\.") {
|
||||
throw "Latest SurrealDB is $latest, not $normalized.x. Pass an exact $normalized.x version or use 'latest' after confirming Forge compatibility."
|
||||
}
|
||||
|
||||
return $latest
|
||||
}
|
||||
|
||||
return $normalized
|
||||
}
|
||||
|
||||
function Confirm-Latest {
|
||||
if ($Force) {
|
||||
return
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "WARNING: This will install the latest stable SurrealDB release, even if it is newer"
|
||||
Write-Host "than the Forge server extension was compiled and tested against."
|
||||
Write-Host ""
|
||||
Write-Host "The Forge server extension currently targets SurrealDB 3.x. A newer major"
|
||||
Write-Host "SurrealDB release can require rebuilding Forge from source with a compatible"
|
||||
Write-Host "surrealdb Rust crate before the extension works correctly."
|
||||
Write-Host ""
|
||||
|
||||
$answer = Read-Host "Install latest SurrealDB anyway? [Y/N]"
|
||||
if ($answer -notmatch "^(?i)y(es)?$") {
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
function Get-Install-Path {
|
||||
$existing = Get-Command surreal -ErrorAction SilentlyContinue | Select-Object -First 1
|
||||
if ($null -ne $existing -and $existing.Source -and (Split-Path -Leaf $existing.Source) -ieq "surreal.exe") {
|
||||
return $existing.Source
|
||||
}
|
||||
|
||||
$installDirectory = Join-Path $env:LOCALAPPDATA "SurrealDB"
|
||||
New-Item -ItemType Directory -Force -Path $installDirectory | Out-Null
|
||||
return Join-Path $installDirectory "surreal.exe"
|
||||
}
|
||||
|
||||
function Ensure-User-Path {
|
||||
param([string]$Directory)
|
||||
|
||||
$pathParts = $env:Path -split ";" | Where-Object { $_ }
|
||||
if ($pathParts -notcontains $Directory) {
|
||||
$env:Path = "$Directory;$env:Path"
|
||||
}
|
||||
|
||||
$userPath = [Environment]::GetEnvironmentVariable("Path", "User")
|
||||
$userPathParts = $userPath -split ";" | Where-Object { $_ }
|
||||
if ($userPathParts -notcontains $Directory) {
|
||||
$newUserPath = if ([string]::IsNullOrWhiteSpace($userPath)) { $Directory } else { "$Directory;$userPath" }
|
||||
[Environment]::SetEnvironmentVariable("Path", $newUserPath, "User")
|
||||
Write-Host "Added $Directory to the user PATH. Open a new terminal if 'surreal' is not found later."
|
||||
}
|
||||
}
|
||||
|
||||
$normalizedTarget = Normalize-Version $Version
|
||||
if ($normalizedTarget -eq "latest") {
|
||||
Confirm-Latest
|
||||
}
|
||||
|
||||
$resolvedVersion = Resolve-Version $Version
|
||||
$installPath = Get-Install-Path
|
||||
$installDirectory = Split-Path -Parent $installPath
|
||||
New-Item -ItemType Directory -Force -Path $installDirectory | Out-Null
|
||||
|
||||
$downloadUrl = "$DownloadBaseUrl/$resolvedVersion/surreal-$resolvedVersion.$Architecture.exe"
|
||||
$tempPath = Join-Path ([System.IO.Path]::GetTempPath()) "surreal-$resolvedVersion.$Architecture.exe"
|
||||
|
||||
Write-Host "Installing SurrealDB $resolvedVersion from $downloadUrl"
|
||||
Invoke-WebRequest $downloadUrl -OutFile $tempPath -UseBasicParsing
|
||||
Move-Item -Force -Path $tempPath -Destination $installPath
|
||||
Ensure-User-Path $installDirectory
|
||||
|
||||
& $installPath version
|
||||
@ -1,6 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
surreal start --user root --pass root --bind 127.0.0.1:8000 rocksdb://forge.db
|
||||
@ -1,15 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if command -v surreal >/dev/null 2>&1; then
|
||||
surreal version
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if command -v brew >/dev/null 2>&1; then
|
||||
brew install surrealdb/tap/surreal
|
||||
else
|
||||
curl -sSf https://install.surrealdb.com | sh
|
||||
fi
|
||||
|
||||
surreal version
|
||||
@ -1,10 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if command -v brew >/dev/null 2>&1; then
|
||||
brew upgrade surrealdb/tap/surreal || brew install surrealdb/tap/surreal
|
||||
else
|
||||
curl -sSf https://install.surrealdb.com | sh
|
||||
fi
|
||||
|
||||
surreal version
|
||||
17
bin/host/basic.example.cfg
Normal file
@ -0,0 +1,17 @@
|
||||
// Basic Network Configuration
|
||||
|
||||
language = "English";
|
||||
adapter = -1;
|
||||
3D_Performance = 1;
|
||||
Resolution_W = 0;
|
||||
Resolution_H = 0;
|
||||
Resolution_Bpp = 32;
|
||||
MinBandwidth = 131072;
|
||||
MaxBandwidth = 10000000000;
|
||||
MaxMsgSend = 128;
|
||||
MaxSizeGuaranteed = 512;
|
||||
MaxSizeNonguaranteed = 256;
|
||||
MinErrorToSend = 0.001;
|
||||
MinErrorToSendNear = 0.01;
|
||||
MaxCustomFileSize = 0;
|
||||
Windowed = 0;
|
||||
35
bin/host/host.example.toml
Normal file
@ -0,0 +1,35 @@
|
||||
[server]
|
||||
host = "0.0.0.0"
|
||||
port = 9090
|
||||
|
||||
[surreal]
|
||||
endpoint = "127.0.0.1:8000"
|
||||
namespace = "forge"
|
||||
database = "main"
|
||||
username = "root"
|
||||
password = "root"
|
||||
connect_timeout_ms = 5000
|
||||
|
||||
[surrealdb]
|
||||
enabled = true
|
||||
command = "surreal"
|
||||
args = ["start", "--user", "root", "--pass", "root", "--bind", "127.0.0.1:8000", "rocksdb://forge.db"]
|
||||
working_dir = "../../arma/server/surrealdb"
|
||||
health_host = "127.0.0.1"
|
||||
health_port = 8000
|
||||
|
||||
[icom]
|
||||
enabled = true
|
||||
command = "target/release/forge-icom.exe"
|
||||
args = []
|
||||
working_dir = "../.."
|
||||
health_host = "127.0.0.1"
|
||||
health_port = 9090
|
||||
|
||||
[arma]
|
||||
enabled = false
|
||||
command = "arma3server_x64.exe"
|
||||
args = ["-port=2302", "-profiles=serverprofiles", "-name=server", "-noBattlEye"]
|
||||
working_dir = ""
|
||||
health_host = "127.0.0.1"
|
||||
health_port = 2302
|
||||
81
bin/host/server.example.cfg
Normal file
@ -0,0 +1,81 @@
|
||||
// server.cfg
|
||||
|
||||
// GLOBAL SETTINGS
|
||||
hostname = "Fun and Test Server"; // The name of the server that shall be displayed in the public server list
|
||||
password = ""; // Password for joining, eg connecting to the server
|
||||
passwordAdmin = "xyz"; // Password to become server admin. When in Arma MP and connected to the server, type '#login xyz'
|
||||
serverCommandPassword = "xyzxyz"; // Password required by alternate syntax of [[serverCommand]] server-side scripting.
|
||||
|
||||
logFile = "server_console.log"; // Where the logfile should go and what it should be called
|
||||
|
||||
// WELCOME MESSAGE ("Message Of The Day")
|
||||
// It can be several lines, separated by comma
|
||||
// Empty messages "" will not be displayed and are only here to add delay
|
||||
motd[] =
|
||||
{
|
||||
"", "",
|
||||
"Two empty lines above to increase the time interval",
|
||||
"Welcome to our server",
|
||||
"", "",
|
||||
"We are looking for fun - Join us Now!",
|
||||
"http://www.example.com",
|
||||
"One more empty line below to increase interval",
|
||||
""
|
||||
};
|
||||
motdInterval = 2.5; // Time interval (in seconds) between each message
|
||||
|
||||
// JOINING RULES
|
||||
maxPlayers = 64; // Maximum amount of players. Civilians and watchers, beholder, bystanders and so on also count as player.
|
||||
kickDuplicate = 1; // Each ArmA version has its own ID. If kickDuplicate is set to 1, a player will be kicked when he joins a server where another player with the same ID is playing.
|
||||
verifySignatures = 2; // Verifies .pbos against .bisign files. Valid values 0 (disabled), 1 (prefer v2 sigs but accept v1 too) and 2 (only v2 sigs are allowed).
|
||||
equalModRequired = 0; // Outdated. If set to 1, player has to use exactly the same -mod= startup parameter as the server.
|
||||
allowedFilePatching = 0; // Allow or prevent client using -filePatching to join the server. 0, is disallow, 1 is allow HC, 2 is allow all clients (since Arma 3 v1.50)
|
||||
filePatchingExceptions[] = { "123456789", "987654321" }; // Whitelisted Steam IDs allowed to join with -filePatching enabled
|
||||
// requiredBuild = 12345; // Require clients joining to have at least build 12345 of game, preventing obsolete clients to connect
|
||||
|
||||
// VOTING
|
||||
voteMissionPlayers = 1; // Tells the server how many people must connect so that it displays the mission selection screen.
|
||||
voteThreshold = 0.33; // 33% or more players need to vote for something, for example an admin or a new map, to become effective
|
||||
|
||||
// INGAME SETTINGS
|
||||
disableVoN = 0; // If set to 1, Voice over Net will not be available
|
||||
vonCodec = 1; // If set to 1 then it uses IETF standard OPUS codec, if to 0 then it uses SPEEX codec (since Arma 3 v1.58)
|
||||
vonCodecQuality = 30; // 0..10 = 8kHz, 11..20 = 16kHz, 21..30 = 32kHz (48kHz)
|
||||
persistent = 1; // If 1, missions still run on even after the last player disconnected.
|
||||
timeStampFormat = "short"; // Set the timestamp format used on each report line in server-side RPT file. Possible values are "none" (default), "short", "full".
|
||||
BattlEye = 1; // Server to use BattlEye system
|
||||
allowedLoadFileExtensions[] = { "hpp", "sqs", "sqf", "fsm", "cpp", "paa", "txt", "xml", "inc", "ext", "sqm", "ods", "fxy", "lip", "csv", "kb", "bik", "bikb", "html", "htm", "biedi" }; // only allow files with those extensions to be loaded via loadFile command (since Arma 3 build 1.19.124216)
|
||||
allowedPreprocessFileExtensions[] = { "hpp", "sqs", "sqf", "fsm", "cpp", "paa", "txt", "xml", "inc", "ext", "sqm", "ods", "fxy", "lip", "csv", "kb", "bik", "bikb", "html", "htm", "biedi" }; // only allow files with those extensions to be loaded via preprocessFile/preprocessFileLineNumber commands (since Arma 3 build 1.19.124323)
|
||||
allowedHTMLLoadExtensions[] = { "htm", "html", "xml", "txt" }; // only allow files with those extensions to be loaded via HTMLLoad command (since Arma 3 build 1.27.126715)
|
||||
// allowedHTMLLoadURIs[] = {}; // Leave commented to let missions/campaigns/addons decide what URIs are supported. Uncomment to define server-level restrictions for URIs
|
||||
|
||||
// TIMEOUTS
|
||||
disconnectTimeout = 5; // Time to wait before disconnecting a user which temporarly lost connection. Range is 5 to 90 seconds.
|
||||
maxDesync = 150; // Max desync value until server kick the user
|
||||
maxPing= 200; // Max ping value until server kick the user
|
||||
maxPacketLoss = 50; // Max packetloss value until server kick the user
|
||||
kickClientsOnSlowNetwork[] = { 0, 0, 0, 0 }; // Defines if {<MaxPing>, <MaxPacketLoss>, <MaxDesync>, <DisconnectTimeout>} will be logged (0) or kicked (1)
|
||||
kickTimeout[] = { { 0, -1 }, { 1, 180 }, { 2, 180 }, { 3, 180 } };
|
||||
votingTimeOut[] = { 60, 90 }; // Kicks users from server if they spend too much time in mission voting
|
||||
roleTimeOut[] = { 90, 120 }; // Kicks users from server if they spend too much time in role selection
|
||||
briefingTimeOut[] = { 60, 90 }; // Kicks users from server if they spend too much time in briefing (map) screen
|
||||
debriefingTimeOut[] = { 45, 60 }; // Kicks users from server if they spend too much time in debriefing screen
|
||||
lobbyIdleTimeout = 300; // The amount of time the server will wait before force-starting a mission without a logged-in Admin.
|
||||
|
||||
// SCRIPTING ISSUES
|
||||
onUserConnected = "";
|
||||
onUserDisconnected = "";
|
||||
doubleIdDetected = "";
|
||||
|
||||
// SIGNATURE VERIFICATION
|
||||
onUnsignedData = "kick (_this select 0)"; // unsigned data detected
|
||||
onHackedData = "kick (_this select 0)"; // tampering of the signature detected
|
||||
onDifferentData = ""; // data with a valid signature, but different version than the one present on server detected
|
||||
|
||||
// MISSIONS CYCLE (see below)
|
||||
randomMissionOrder = true; // Randomly iterate through Missions list
|
||||
autoSelectMission = true; // Server auto selects next mission in cycle
|
||||
|
||||
class Missions {}; // An empty Missions class means there will be no mission rotation
|
||||
|
||||
missionWhitelist[] = {}; // An empty whitelist means there is no restriction on what missions' available
|
||||
24
bin/host/src-tauri/Cargo.toml
Normal file
@ -0,0 +1,24 @@
|
||||
[package]
|
||||
name = "forge-host"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
build = "build.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "forge-host"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
reqwest = { version = "0.12.20", default-features = false, features = ["rustls-tls"] }
|
||||
tauri = { version = "2", features = [] }
|
||||
tauri-plugin-dialog = "2"
|
||||
toml = "0.9.8"
|
||||
|
||||
[target.'cfg(not(windows))'.dependencies]
|
||||
flate2 = "1.1.2"
|
||||
tar = "0.4.44"
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
3
bin/host/src-tauri/build.rs
Normal file
@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
tauri_build::build();
|
||||
}
|
||||
7
bin/host/src-tauri/capabilities/default.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "Default permissions for Forge Host",
|
||||
"windows": ["main"],
|
||||
"permissions": ["core:default", "dialog:default"]
|
||||
}
|
||||
1
bin/host/src-tauri/gen/schemas/acl-manifests.json
Normal file
1
bin/host/src-tauri/gen/schemas/capabilities.json
Normal file
@ -0,0 +1 @@
|
||||
{"default":{"identifier":"default","description":"Default permissions for Forge Host","local":true,"windows":["main"],"permissions":["core:default","dialog:default"]}}
|
||||
2358
bin/host/src-tauri/gen/schemas/desktop-schema.json
Normal file
2358
bin/host/src-tauri/gen/schemas/linux-schema.json
Normal file
2358
bin/host/src-tauri/gen/schemas/windows-schema.json
Normal file
BIN
bin/host/src-tauri/icons/128x128.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
bin/host/src-tauri/icons/128x128@2x.png
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
bin/host/src-tauri/icons/32x32.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
bin/host/src-tauri/icons/64x64.png
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
BIN
bin/host/src-tauri/icons/Square107x107Logo.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
bin/host/src-tauri/icons/Square142x142Logo.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
bin/host/src-tauri/icons/Square150x150Logo.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
bin/host/src-tauri/icons/Square284x284Logo.png
Normal file
|
After Width: | Height: | Size: 87 KiB |
BIN
bin/host/src-tauri/icons/Square30x30Logo.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
bin/host/src-tauri/icons/Square310x310Logo.png
Normal file
|
After Width: | Height: | Size: 103 KiB |
BIN
bin/host/src-tauri/icons/Square44x44Logo.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
bin/host/src-tauri/icons/Square71x71Logo.png
Normal file
|
After Width: | Height: | Size: 8.7 KiB |
BIN
bin/host/src-tauri/icons/Square89x89Logo.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
bin/host/src-tauri/icons/StoreLogo.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
</adaptive-icon>
|
||||
BIN
bin/host/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
BIN
bin/host/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
BIN
bin/host/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 14 KiB |
BIN
bin/host/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 111 KiB |
|
After Width: | Height: | Size: 26 KiB |
BIN
bin/host/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 193 KiB |
|
After Width: | Height: | Size: 41 KiB |
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#fff</color>
|
||||
</resources>
|
||||
BIN
bin/host/src-tauri/icons/icon.icns
Normal file
BIN
bin/host/src-tauri/icons/icon.ico
Normal file
|
After Width: | Height: | Size: 91 KiB |
BIN
bin/host/src-tauri/icons/icon.png
Normal file
|
After Width: | Height: | Size: 272 KiB |
BIN
bin/host/src-tauri/icons/ios/AppIcon-20x20@1x.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
bin/host/src-tauri/icons/ios/AppIcon-20x20@2x-1.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
bin/host/src-tauri/icons/ios/AppIcon-20x20@2x.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
bin/host/src-tauri/icons/ios/AppIcon-20x20@3x.png
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
BIN
bin/host/src-tauri/icons/ios/AppIcon-29x29@1x.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
bin/host/src-tauri/icons/ios/AppIcon-29x29@2x-1.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
bin/host/src-tauri/icons/ios/AppIcon-29x29@2x.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
bin/host/src-tauri/icons/ios/AppIcon-29x29@3x.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
bin/host/src-tauri/icons/ios/AppIcon-40x40@1x.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
bin/host/src-tauri/icons/ios/AppIcon-40x40@2x-1.png
Normal file
|
After Width: | Height: | Size: 9.5 KiB |
BIN
bin/host/src-tauri/icons/ios/AppIcon-40x40@2x.png
Normal file
|
After Width: | Height: | Size: 9.5 KiB |
BIN
bin/host/src-tauri/icons/ios/AppIcon-40x40@3x.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
bin/host/src-tauri/icons/ios/AppIcon-512@2x.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
bin/host/src-tauri/icons/ios/AppIcon-60x60@2x.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
bin/host/src-tauri/icons/ios/AppIcon-60x60@3x.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
bin/host/src-tauri/icons/ios/AppIcon-76x76@1x.png
Normal file
|
After Width: | Height: | Size: 8.7 KiB |
BIN
bin/host/src-tauri/icons/ios/AppIcon-76x76@2x.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
bin/host/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
1005
bin/host/src-tauri/src/main.rs
Normal file
35
bin/host/src-tauri/tauri.conf.json
Normal file
@ -0,0 +1,35 @@
|
||||
{
|
||||
"$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
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
]
|
||||
}
|
||||
}
|
||||
946
bin/host/src/app.js
Normal file
@ -0,0 +1,946 @@
|
||||
const serviceNames = {
|
||||
surrealdb: "SurrealDB",
|
||||
icom: "ICOM",
|
||||
arma: "Arma 3 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 `
|
||||
<section class="settings-panel active">
|
||||
<div class="settings-header">
|
||||
<h2>${serviceNames[name] || name}</h2>
|
||||
<label class="toggle-row">
|
||||
<input type="checkbox" data-service="${name}" data-field="enabled" ${config.enabled ? "checked" : ""} />
|
||||
<span class="switch-track" aria-hidden="true"><span class="switch-thumb"></span></span>
|
||||
<span>Enabled</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="field-grid">
|
||||
${renderSurrealInstallControls()}
|
||||
<div class="field full">
|
||||
<label>Working Directory</label>
|
||||
<input data-service="${name}" data-field="working_dir" value="${escapeAttr(config.working_dir)}" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Health Host</label>
|
||||
<input data-service="${name}" data-field="health_host" value="${escapeAttr(config.health_host)}" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Health Port</label>
|
||||
<input data-service="${name}" data-field="health_port" type="number" min="1" max="65535" value="${config.health_port}" />
|
||||
</div>
|
||||
<div class="field full">
|
||||
<label>Arguments</label>
|
||||
<div class="args-list" data-service="${name}" data-field="args">
|
||||
${renderArgumentFields(name, config.args)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
|
||||
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 `
|
||||
<div class="field full surreal-install">
|
||||
<div class="install-summary">
|
||||
<div>
|
||||
<label>SurrealDB Install</label>
|
||||
<strong>${escapeHtml(version)}</strong>
|
||||
<span>${escapeHtml(path)}</span>
|
||||
</div>
|
||||
<div>
|
||||
<label>Latest</label>
|
||||
<strong>${escapeHtml(latest)}</strong>
|
||||
<span>${installed ? "Choose a target version when updating." : "Install before starting SurrealDB."}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="install-action">
|
||||
<label class="install-version">
|
||||
<span>Target</span>
|
||||
<select data-surreal-version-mode>
|
||||
<option value="3" selected>3.x</option>
|
||||
<option value="2">2.x</option>
|
||||
<option value="latest">Latest</option>
|
||||
<option value="custom">Custom</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="install-custom" hidden>
|
||||
<span>Custom Version</span>
|
||||
<input data-surreal-custom-version placeholder="v3.1.3" />
|
||||
</label>
|
||||
<button type="button" data-surreal-install>${installed ? "Update" : "Install"}</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
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 `
|
||||
<section class="settings-panel active">
|
||||
<div class="settings-header">
|
||||
<h2>ICOM</h2>
|
||||
<label class="toggle-row">
|
||||
<input type="checkbox" data-service="icom" data-field="enabled" ${config.enabled ? "checked" : ""} />
|
||||
<span class="switch-track" aria-hidden="true"><span class="switch-thumb"></span></span>
|
||||
<span>Enabled</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="field-grid">
|
||||
<div class="field with-button full">
|
||||
<label>ICOM Executable</label>
|
||||
<div class="input-action action-one">
|
||||
<input data-service="icom" data-field="command" value="${escapeAttr(config.command)}" placeholder="forge-icom.exe" />
|
||||
<button class="icon-button" type="button" data-picker="file" data-service-target="icom" data-target='[data-service="icom"][data-field="command"]' title="Select executable" aria-label="Select executable"><span class="svg-icon icon-browse"></span></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field with-button full">
|
||||
<label>Working Directory</label>
|
||||
<div class="input-action action-one">
|
||||
<input data-service="icom" data-field="working_dir" value="${escapeAttr(config.working_dir)}" />
|
||||
<button class="icon-button" type="button" data-picker="directory" data-service-target="icom" data-target='[data-service="icom"][data-field="working_dir"]' title="Select folder" aria-label="Select folder"><span class="svg-icon icon-browse"></span></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Health Host</label>
|
||||
<input data-service="icom" data-field="health_host" value="${escapeAttr(config.health_host)}" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Health Port</label>
|
||||
<input data-service="icom" data-field="health_port" type="number" min="1" max="65535" value="${config.health_port}" />
|
||||
</div>
|
||||
<div class="field full">
|
||||
<label>Arguments</label>
|
||||
<div class="args-list" data-service="icom" data-field="args">
|
||||
${renderArgumentFields("icom", config.args)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderArmaSettings(config) {
|
||||
const parsed = parseArmaArgs(config.args);
|
||||
return `
|
||||
<section class="settings-panel active">
|
||||
<div class="settings-header">
|
||||
<h2>Arma 3 Server</h2>
|
||||
<label class="toggle-row">
|
||||
<input type="checkbox" data-service="arma" data-field="enabled" ${config.enabled ? "checked" : ""} />
|
||||
<span class="switch-track" aria-hidden="true"><span class="switch-thumb"></span></span>
|
||||
<span>Enabled</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="field-grid arma-grid">
|
||||
<div class="field with-button full">
|
||||
<label>Server Executable</label>
|
||||
<div class="input-action action-one">
|
||||
<input data-service="arma" data-field="command" value="${escapeAttr(config.command)}" placeholder="arma3server_x64.exe" />
|
||||
<button class="icon-button" type="button" data-picker="file" data-service-target="arma" data-target='[data-service="arma"][data-field="command"]' title="Select executable" aria-label="Select executable"><span class="svg-icon icon-browse"></span></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field with-button full">
|
||||
<label>Server Directory</label>
|
||||
<div class="input-action action-one">
|
||||
<input data-service="arma" data-field="working_dir" value="${escapeAttr(config.working_dir)}" />
|
||||
<button class="icon-button" type="button" data-picker="directory" data-service-target="arma" data-target='[data-service="arma"][data-field="working_dir"]' title="Select folder" aria-label="Select folder"><span class="svg-icon icon-browse"></span></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Health Host</label>
|
||||
<input data-service="arma" data-field="health_host" value="${escapeAttr(config.health_host)}" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Health Port</label>
|
||||
<input data-service="arma" data-field="health_port" type="number" min="1" max="65535" value="${config.health_port}" />
|
||||
</div>
|
||||
<div class="field full">
|
||||
<label>Launch Options</label>
|
||||
<div class="args-list arma-args" data-service="arma" data-field="args">
|
||||
${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")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
|
||||
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 `
|
||||
<div class="arg-field ${className}">
|
||||
<label>${label}</label>
|
||||
<input class="arg-input" data-arg-key="${key}" value="${escapeAttr(value)}" />
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function serverConfigInput(value) {
|
||||
return `
|
||||
<div class="arg-field full">
|
||||
<label>Server Config</label>
|
||||
<div class="input-action action-three">
|
||||
<input class="arg-input" data-arg-key="config" value="${escapeAttr(value)}" placeholder="server.cfg" />
|
||||
<button class="icon-button" type="button" data-picker="config" data-service-target="arma" data-target='[data-arg-key="config"]' title="Select config" aria-label="Select config"><span class="svg-icon icon-browse"></span></button>
|
||||
<button class="icon-button" type="button" data-create-server-config title="Create default config" aria-label="Create default config"><span class="svg-icon icon-new"></span></button>
|
||||
<button class="icon-button" type="button" data-edit-server-config title="Edit config" aria-label="Edit config"><span class="svg-icon icon-edit"></span></button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function serverConfigTemplateInput() {
|
||||
return `
|
||||
<div class="arg-field">
|
||||
<label>Config Template</label>
|
||||
<select data-config-template>
|
||||
<option value="server" ${selectedAttr(selectedArmaConfigTemplate === "server")}>Server</option>
|
||||
<option value="basic" ${selectedAttr(selectedArmaConfigTemplate === "basic")}>Basic Network</option>
|
||||
</select>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function toggleArgument(label, key, value) {
|
||||
return `
|
||||
<label class="arg-toggle">
|
||||
<input type="checkbox" class="arg-input" data-arg-key="${key}" ${value ? "checked" : ""} />
|
||||
${label}
|
||||
</label>
|
||||
`;
|
||||
}
|
||||
|
||||
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();
|
||||
149
bin/host/src/index.html
Normal file
@ -0,0 +1,149 @@
|
||||
<!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>
|
||||
|
||||
<body>
|
||||
<main class="shell">
|
||||
<aside class="rail">
|
||||
<div class="brand">
|
||||
<span class="brand-mark">F</span>
|
||||
<span>FORGE Host</span>
|
||||
</div>
|
||||
<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>
|
||||
</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="saveButton" class="icon-button" type="button" aria-label="Save" title="Save">
|
||||
<span class="svg-icon icon-save"></span>
|
||||
</button>
|
||||
</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>
|
||||
</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>
|
||||
</div>
|
||||
<p class="command-line"></p>
|
||||
</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>
|
||||
|
||||
</html>
|
||||
4
bin/host/src/public/browse.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 1H6L9 4H16V14H0V1Z" fill="#000000"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 276 B |
2
bin/host/src/public/close.svg
Normal file
@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path fill="#000000" d="M195.2 195.2a64 64 0 0 1 90.496 0L512 421.504 738.304 195.2a64 64 0 0 1 90.496 90.496L602.496 512 828.8 738.304a64 64 0 0 1-90.496 90.496L512 602.496 285.696 828.8a64 64 0 0 1-90.496-90.496L421.504 512 195.2 285.696a64 64 0 0 1 0-90.496z"/></svg>
|
||||
|
After Width: | Height: | Size: 482 B |
2
bin/host/src/public/dashboard.svg
Normal file
@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg fill="#000000" width="800px" height="800px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" id="dashboard" class="icon glyph"><rect x="2" y="2" width="9" height="11" rx="2"></rect><rect x="13" y="2" width="9" height="7" rx="2"></rect><rect x="2" y="15" width="9" height="7" rx="2"></rect><rect x="13" y="11" width="9" height="11" rx="2"></rect></svg>
|
||||
|
After Width: | Height: | Size: 481 B |
68
bin/host/src/public/database.svg
Normal file
@ -0,0 +1,68 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg
|
||||
viewBox="0 0 600 600"
|
||||
version="1.1"
|
||||
id="svg9724"
|
||||
sodipodi:docname="database.svg"
|
||||
inkscape:version="1.2.2 (1:1.2.2+202212051550+b0a8486541)"
|
||||
width="600"
|
||||
height="600"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs9728" />
|
||||
<sodipodi:namedview
|
||||
id="namedview9726"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
showgrid="true"
|
||||
inkscape:zoom="0.42059316"
|
||||
inkscape:cx="148.59966"
|
||||
inkscape:cy="296.01052"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1009"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="1080"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg9724"
|
||||
showguides="true">
|
||||
<inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid9972"
|
||||
originx="0"
|
||||
originy="0" />
|
||||
<sodipodi:guide
|
||||
position="300,-90"
|
||||
orientation="1,0"
|
||||
id="guide385"
|
||||
inkscape:locked="false" />
|
||||
<sodipodi:guide
|
||||
position="140,100"
|
||||
orientation="0,-1"
|
||||
id="guide1388"
|
||||
inkscape:locked="false" />
|
||||
<sodipodi:guide
|
||||
position="140,100"
|
||||
orientation="0,-1"
|
||||
id="guide2256"
|
||||
inkscape:locked="false" />
|
||||
<sodipodi:guide
|
||||
position="0,475"
|
||||
orientation="0,-1"
|
||||
id="guide1920"
|
||||
inkscape:locked="false" />
|
||||
</sodipodi:namedview>
|
||||
|
||||
<path
|
||||
id="path3428"
|
||||
style="color:#000000;fill:#010101;stroke-linejoin:round;-inkscape-stroke:none;paint-order:stroke fill markers"
|
||||
d="M 300 0 C 221.30245 0 150.09841 8.0113158 97.068359 21.535156 C 70.553346 28.297076 48.605538 36.277916 31.677734 46.484375 C 16.579982 55.587421 3.2445893 67.928721 0.53125 85 L 0 85 L 0 90 C 0 95.160045 3.6392602 102.94345 17.03125 112.83789 C 30.423241 122.73233 52.11942 133.00486 79.691406 141.62109 C 134.83535 158.85361 213.32376 170 300 170 C 386.67624 170 465.16467 158.85361 520.30859 141.62109 C 547.8806 133.00486 569.57675 122.73233 582.96875 112.83789 C 596.36075 102.94345 600 95.160045 600 90 L 599.87305 90 C 599.19452 70.318664 584.84711 56.447884 568.32227 46.484375 C 551.39442 36.277916 529.44664 28.297076 502.93164 21.535156 C 449.90159 8.0113158 378.69755 0 300 0 z M 0 149.67969 L 0 234.10742 C 0.70499641 239.21983 4.6599347 246.30446 16.722656 255.2168 C 30.11466 265.11125 51.810798 275.38376 79.382812 284 C 134.52681 301.23251 213.01506 312.37891 299.69141 312.37891 C 386.36774 312.37891 464.85602 301.23251 520 284 C 547.57201 275.38376 569.26815 265.11125 582.66016 255.2168 C 596.05215 245.32235 599.69141 237.53895 599.69141 232.37891 L 600 232.37891 L 600 149.67969 C 581.93283 161.57337 559.1282 171.3983 532.24023 179.80078 C 471.56758 198.761 390.05399 210 300 210 C 209.94601 210 128.43244 198.761 67.759766 179.80078 C 40.871811 171.3983 18.067172 161.57337 0 149.67969 z M 600 291.79688 C 590.25148 298.2521 579.18165 304.12941 566.75 309.46875 C 556.06951 314.05598 544.44003 318.27081 531.93164 322.17969 C 471.2589 341.13992 389.74549 352.37891 299.69141 352.37891 C 209.63733 352.37891 128.12391 341.13993 67.451172 322.17969 C 40.720883 313.82647 18.016718 304.0712 0 292.27148 L 0 380 C 0 385.16005 3.6392334 392.94343 17.03125 402.83789 C 30.423267 412.73235 52.119364 423.00484 79.691406 431.62109 C 134.83545 448.85363 213.32358 460 300 460 C 386.67642 460 465.16455 448.85363 520.30859 431.62109 C 547.88068 423.00484 569.57666 412.73235 582.96875 402.83789 C 596.36074 392.94343 600 385.16005 600 380 L 600 291.79688 z M 0 439.67969 L 0 508.59375 L 0 515 L 0.53125 515 C 3.2445947 532.0713 16.579952 544.41257 31.677734 553.51562 C 48.605572 563.7221 70.553292 571.70292 97.068359 578.46484 C 150.09851 591.98873 221.30229 600 300 600 C 378.69771 600 449.90149 591.98873 502.93164 578.46484 C 529.4467 571.70292 551.3944 563.7221 568.32227 553.51562 C 583.42003 544.41257 596.7554 532.0713 599.46875 515 L 600 515 L 600 508.59375 L 600 439.67969 C 581.93278 451.57339 559.1283 461.39828 532.24023 469.80078 C 471.56747 488.76104 390.05417 500 300 500 C 209.94583 500 128.43256 488.76104 67.759766 469.80078 C 40.871757 461.39828 18.067208 451.57339 0 439.67969 z " />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.5 KiB |
2
bin/host/src/public/edit.svg
Normal file
@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="m3.99 16.854-1.314 3.504a.75.75 0 0 0 .966.965l3.503-1.314a3 3 0 0 0 1.068-.687L18.36 9.175s-.354-1.061-1.414-2.122c-1.06-1.06-2.122-1.414-2.122-1.414L4.677 15.786a3 3 0 0 0-.687 1.068zm12.249-12.63 1.383-1.383c.248-.248.579-.406.925-.348.487.08 1.232.322 1.934 1.025.703.703.945 1.447 1.025 1.934.058.346-.1.677-.348.925L19.774 7.76s-.353-1.06-1.414-2.12c-1.06-1.062-2.121-1.415-2.121-1.415z" fill="#000000"/></svg>
|
||||
|
After Width: | Height: | Size: 685 B |
BIN
bin/host/src/public/forge.png
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
6
bin/host/src/public/icom.svg
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg fill="#000000" width="800px" height="800px" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>arrows-repeat</title>
|
||||
<path d="M3.25 14.008c0.002-0.234 0.113-5.754 6.75-5.758h14.981l-2.866 2.866c-0.226 0.226-0.366 0.539-0.366 0.884 0 0.691 0.56 1.251 1.251 1.251 0.346 0 0.658-0.14 0.885-0.367v0l5-5c0.012-0.012 0.016-0.029 0.027-0.041 0.099-0.103 0.18-0.223 0.239-0.356l0.003-0.008 0-0.003c0.051-0.13 0.080-0.28 0.080-0.438 0-0.071-0.006-0.14-0.017-0.208l0.001 0.007c-0.008-0.059-0.018-0.11-0.031-0.16l0.002 0.009c-0.052-0.223-0.158-0.416-0.305-0.571l0.001 0.001-5-5c-0.226-0.227-0.539-0.367-0.885-0.367-0.691 0-1.251 0.56-1.251 1.251 0 0.345 0.14 0.658 0.366 0.884v0l2.867 2.866h-14.983c-7.274 0.005-9.23 5.394-9.249 8.242-0 0.002-0 0.005-0 0.008 0 0.688 0.555 1.246 1.242 1.25h0.008c0.687-0 1.245-0.555 1.25-1.242v-0zM30.014 16.753c-0.676 0.027-1.22 0.559-1.263 1.229l-0 0.004c-0.010 0.589-0.283 5.761-6.75 5.765h-14.981l2.865-2.865c0.227-0.226 0.367-0.539 0.367-0.885 0-0.691-0.56-1.251-1.251-1.251-0.345 0-0.658 0.14-0.884 0.366v0l-5 5c-0.012 0.012-0.016 0.029-0.027 0.041-0.1 0.103-0.182 0.224-0.241 0.357l-0.003 0.008-0 0.003c-0.038 0.093-0.065 0.2-0.073 0.312l-0 0.004c-0.003 0.033-0.004 0.072-0.004 0.11 0 0.075 0.005 0.149 0.016 0.221l-0.001-0.008c0.039 0.28 0.159 0.527 0.335 0.722l-0.001-0.001 5 5c0.226 0.226 0.539 0.366 0.884 0.366 0.691 0 1.251-0.56 1.251-1.251 0-0.346-0.14-0.658-0.367-0.885l-2.866-2.865h14.982c7.275-0.006 9.232-5.395 9.25-8.242 0-0.002 0-0.005 0-0.007 0-0.685-0.552-1.241-1.236-1.248h-0.001z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
13
bin/host/src/public/logs.svg
Normal file
@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 12 12" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
|
||||
<title>Combined Shape Copy 9</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<g id="Changes,-Tests,-Widgets" stroke="none" stroke-width="1" fill-rule="evenodd">
|
||||
<g id="Tests-History" transform="translate(-1156.000000, -1289.000000)">
|
||||
<path d="M1168,1289 L1156,1289 L1156,1301 L1168,1301 L1168,1289 Z M1165,1292 L1159,1292 L1159,1293 L1165,1293 L1165,1292 Z M1159,1294 L1165,1294 L1165,1295 L1159,1295 L1159,1294 Z M1165,1296 L1159,1296 L1159,1297 L1165,1297 L1165,1296 Z" id="Combined-Shape-Copy-9">
|
||||
</path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 845 B |
24
bin/host/src/public/new.svg
Normal file
@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" ?>
|
||||
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
|
||||
<title/>
|
||||
|
||||
<g id="Complete">
|
||||
|
||||
<g data-name="add" id="add-2">
|
||||
|
||||
<g>
|
||||
|
||||
<line fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" x1="12" x2="12" y1="19" y2="5"/>
|
||||
|
||||
<line fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" x1="5" x2="19" y1="12" y2="12"/>
|
||||
|
||||
</g>
|
||||
|
||||
</g>
|
||||
|
||||
</g>
|
||||
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 552 B |
6
bin/host/src/public/play.svg
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg fill="#000000" width="800px" height="800px" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>play</title>
|
||||
<path d="M5.92 24.096q0 1.088 0.928 1.728 0.512 0.288 1.088 0.288 0.448 0 0.896-0.224l16.16-8.064q0.48-0.256 0.8-0.736t0.288-1.088-0.288-1.056-0.8-0.736l-16.16-8.064q-0.448-0.224-0.896-0.224-0.544 0-1.088 0.288-0.928 0.608-0.928 1.728v16.16z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 516 B |
4
bin/host/src/public/refresh.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M21 3V8M21 8H16M21 8L18 5.29168C16.4077 3.86656 14.3051 3 12 3C7.02944 3 3 7.02944 3 12C3 16.9706 7.02944 21 12 21C16.2832 21 19.8675 18.008 20.777 14" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 471 B |
4
bin/host/src/public/save.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8 20H6C4.89543 20 4 19.1046 4 18V6C4 4.89543 4.89543 4 6 4H9M8 20V14C8 13.4477 8.44772 13 9 13H15C15.5523 13 16 13.4477 16 14V20M8 20H16M16 20H18C19.1046 20 20 19.1046 20 18V8.82843C20 8.29799 19.7893 7.78929 19.4142 7.41421L16.5858 4.58579C16.2107 4.21071 15.702 4 15.1716 4H15M15 4V7C15 7.55228 14.5523 8 14 8H10C9.44772 8 9 7.55228 9 7V4M15 4H9" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 669 B |
34
bin/host/src/public/server.svg
Normal file
@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg fill="#000000" height="800px" width="800px" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="0 0 60 60" xml:space="preserve">
|
||||
<g>
|
||||
<path d="M0,24.108v11.783c0,0.974,0.314,1.868,0.835,2.608h58.329C59.686,37.76,60,36.865,60,35.892V24.108
|
||||
c0-0.974-0.314-1.868-0.835-2.608H0.835C0.314,22.24,0,23.135,0,24.108z M52,30.5c0.552,0,1,0.448,1,1s-0.448,1-1,1s-1-0.448-1-1
|
||||
S51.448,30.5,52,30.5z M50,27.5c0.552,0,1,0.448,1,1s-0.448,1-1,1s-1-0.448-1-1S49.448,27.5,50,27.5z M48,30.5c0.552,0,1,0.448,1,1
|
||||
s-0.448,1-1,1s-1-0.448-1-1S47.448,30.5,48,30.5z M46,27.5c0.552,0,1,0.448,1,1s-0.448,1-1,1s-1-0.448-1-1S45.448,27.5,46,27.5z
|
||||
M44,30.5c0.552,0,1,0.448,1,1s-0.448,1-1,1s-1-0.448-1-1S43.448,30.5,44,30.5z M42,27.5c0.552,0,1,0.448,1,1s-0.448,1-1,1
|
||||
s-1-0.448-1-1S41.448,27.5,42,27.5z M40,30.5c0.552,0,1,0.448,1,1s-0.448,1-1,1s-1-0.448-1-1S39.448,30.5,40,30.5z M38,27.5
|
||||
c0.552,0,1,0.448,1,1s-0.448,1-1,1s-1-0.448-1-1S37.448,27.5,38,27.5z M36,30.5c0.552,0,1,0.448,1,1s-0.448,1-1,1s-1-0.448-1-1
|
||||
S35.448,30.5,36,30.5z M34,27.5c0.552,0,1,0.448,1,1s-0.448,1-1,1s-1-0.448-1-1S33.448,27.5,34,27.5z M10.5,25.5
|
||||
c2.481,0,4.5,2.019,4.5,4.5s-2.019,4.5-4.5,4.5S6,32.481,6,30S8.019,25.5,10.5,25.5z"/>
|
||||
<path d="M59.165,19.5C59.686,18.76,60,17.865,60,16.892V5.108C60,2.567,57.933,0.5,55.392,0.5H4.608C2.067,0.5,0,2.567,0,5.108
|
||||
v11.783c0,0.974,0.314,1.868,0.835,2.608H59.165z M52,11.5c0.552,0,1,0.448,1,1s-0.448,1-1,1s-1-0.448-1-1S51.448,11.5,52,11.5z
|
||||
M50,8.5c0.552,0,1,0.448,1,1s-0.448,1-1,1s-1-0.448-1-1S49.448,8.5,50,8.5z M48,11.5c0.552,0,1,0.448,1,1s-0.448,1-1,1
|
||||
s-1-0.448-1-1S47.448,11.5,48,11.5z M46,8.5c0.552,0,1,0.448,1,1s-0.448,1-1,1s-1-0.448-1-1S45.448,8.5,46,8.5z M44,11.5
|
||||
c0.552,0,1,0.448,1,1s-0.448,1-1,1s-1-0.448-1-1S43.448,11.5,44,11.5z M42,8.5c0.552,0,1,0.448,1,1s-0.448,1-1,1s-1-0.448-1-1
|
||||
S41.448,8.5,42,8.5z M40,11.5c0.552,0,1,0.448,1,1s-0.448,1-1,1s-1-0.448-1-1S39.448,11.5,40,11.5z M38,8.5c0.552,0,1,0.448,1,1
|
||||
s-0.448,1-1,1s-1-0.448-1-1S37.448,8.5,38,8.5z M36,11.5c0.552,0,1,0.448,1,1s-0.448,1-1,1s-1-0.448-1-1S35.448,11.5,36,11.5z
|
||||
M34,8.5c0.552,0,1,0.448,1,1s-0.448,1-1,1s-1-0.448-1-1S33.448,8.5,34,8.5z M10.5,6.5c2.481,0,4.5,2.019,4.5,4.5
|
||||
s-2.019,4.5-4.5,4.5S6,13.481,6,11S8.019,6.5,10.5,6.5z"/>
|
||||
<path d="M0.835,40.5C0.314,41.24,0,42.135,0,43.108v11.783C0,57.433,2.067,59.5,4.608,59.5h50.783c2.541,0,4.608-2.067,4.608-4.608
|
||||
V43.108c0-0.974-0.314-1.868-0.835-2.608H0.835z M10.5,53.5C8.019,53.5,6,51.481,6,49s2.019-4.5,4.5-4.5S15,46.519,15,49
|
||||
S12.981,53.5,10.5,53.5z M34,48.5c-0.552,0-1-0.448-1-1s0.448-1,1-1s1,0.448,1,1S34.552,48.5,34,48.5z M36,51.5
|
||||
c-0.552,0-1-0.448-1-1s0.448-1,1-1s1,0.448,1,1S36.552,51.5,36,51.5z M38,48.5c-0.552,0-1-0.448-1-1s0.448-1,1-1s1,0.448,1,1
|
||||
S38.552,48.5,38,48.5z M40,51.5c-0.552,0-1-0.448-1-1s0.448-1,1-1s1,0.448,1,1S40.552,51.5,40,51.5z M42,48.5c-0.552,0-1-0.448-1-1
|
||||
s0.448-1,1-1s1,0.448,1,1S42.552,48.5,42,48.5z M44,51.5c-0.552,0-1-0.448-1-1s0.448-1,1-1s1,0.448,1,1S44.552,51.5,44,51.5z
|
||||
M46,48.5c-0.552,0-1-0.448-1-1s0.448-1,1-1s1,0.448,1,1S46.552,48.5,46,48.5z M48,51.5c-0.552,0-1-0.448-1-1s0.448-1,1-1
|
||||
s1,0.448,1,1S48.552,51.5,48,51.5z M50,48.5c-0.552,0-1-0.448-1-1s0.448-1,1-1s1,0.448,1,1S50.552,48.5,50,48.5z M52,51.5
|
||||
c-0.552,0-1-0.448-1-1s0.448-1,1-1s1,0.448,1,1S52.552,51.5,52,51.5z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.4 KiB |
6
bin/host/src/public/stop.svg
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg fill="#000000" width="800px" height="800px" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>stop</title>
|
||||
<path d="M5.92 24.096q0 0.832 0.576 1.408t1.44 0.608h16.128q0.832 0 1.44-0.608t0.576-1.408v-16.16q0-0.832-0.576-1.44t-1.44-0.576h-16.128q-0.832 0-1.44 0.576t-0.576 1.44v16.16z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 450 B |
857
bin/host/src/styles.css
Normal file
@ -0,0 +1,857 @@
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
--bg: #0b0d10;
|
||||
--panel: #15181e;
|
||||
--panel-soft: #1c2028;
|
||||
--line: #343a45;
|
||||
--text: #f3f0e8;
|
||||
--muted: #aba79f;
|
||||
--gold: #ffe681;
|
||||
--green: #21d07a;
|
||||
--red: #ff5b66;
|
||||
--blue: #69a7ff;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-width: 940px;
|
||||
min-height: 620px;
|
||||
overflow: hidden;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-family: Inter, "Segoe UI", Arial, sans-serif;
|
||||
}
|
||||
|
||||
button,
|
||||
input,
|
||||
textarea,
|
||||
select {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
button {
|
||||
border: 1px solid rgba(255, 230, 129, 0.42);
|
||||
background: rgba(255, 230, 129, 0.08);
|
||||
color: var(--gold);
|
||||
height: 38px;
|
||||
padding: 0 14px;
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
border-color: var(--gold);
|
||||
background: rgba(255, 230, 129, 0.14);
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
cursor: default;
|
||||
opacity: 0.42;
|
||||
}
|
||||
|
||||
button.primary {
|
||||
background: var(--gold);
|
||||
color: #101114;
|
||||
border-color: var(--gold);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
button.primary:hover:not(:disabled) {
|
||||
background: #fff0a8;
|
||||
border-color: #fff0a8;
|
||||
box-shadow: 0 0 0 3px rgba(255, 230, 129, 0.14);
|
||||
}
|
||||
|
||||
button.icon-button {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 42px;
|
||||
min-width: 42px;
|
||||
height: 42px;
|
||||
padding: 0;
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.shell {
|
||||
display: grid;
|
||||
grid-template-columns: 260px 1fr;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.rail {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 28px;
|
||||
padding: 24px 18px;
|
||||
background: #101216;
|
||||
border-right: 1px solid #252a33;
|
||||
}
|
||||
|
||||
.brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
color: var(--gold);
|
||||
}
|
||||
|
||||
.brand-mark {
|
||||
display: grid;
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
place-items: center;
|
||||
background: var(--gold);
|
||||
color: #111216;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.nav {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.nav-button {
|
||||
display: grid;
|
||||
grid-template-columns: 28px 1fr auto;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
text-align: left;
|
||||
color: var(--muted);
|
||||
border-color: #3a3d2c;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.nav-button.active {
|
||||
color: #111216;
|
||||
border-color: var(--gold);
|
||||
background: var(--gold);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.nav-button[data-view="settings"]::after {
|
||||
content: "";
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background: var(--muted);
|
||||
transition: background-color 180ms ease, box-shadow 180ms ease;
|
||||
}
|
||||
|
||||
.nav-button.service-enabled::after {
|
||||
background: var(--green);
|
||||
animation: gumball-ping-green 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
}
|
||||
|
||||
.nav-button.service-stopped::after {
|
||||
background: var(--red);
|
||||
animation: gumball-ping-red 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
}
|
||||
|
||||
.nav-button.service-disabled::after {
|
||||
background: #1a1d23;
|
||||
border: 1px solid rgba(255, 230, 129, 0.38);
|
||||
animation: gumball-ping-gold 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
}
|
||||
|
||||
.svg-icon {
|
||||
display: block;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
flex: 0 0 auto;
|
||||
background: currentColor;
|
||||
-webkit-mask: var(--icon-url) center / contain no-repeat;
|
||||
mask: var(--icon-url) center / contain no-repeat;
|
||||
}
|
||||
|
||||
.icon-browse {
|
||||
--icon-url: url("./public/browse.svg");
|
||||
}
|
||||
|
||||
.icon-close {
|
||||
--icon-url: url("./public/close.svg");
|
||||
}
|
||||
|
||||
.icon-dashboard {
|
||||
--icon-url: url("./public/dashboard.svg");
|
||||
}
|
||||
|
||||
.icon-database {
|
||||
--icon-url: url("./public/database.svg");
|
||||
}
|
||||
|
||||
.icon-edit {
|
||||
--icon-url: url("./public/edit.svg");
|
||||
}
|
||||
|
||||
.icon-logs {
|
||||
--icon-url: url("./public/logs.svg");
|
||||
}
|
||||
|
||||
.icon-icom {
|
||||
--icon-url: url("./public/icom.svg");
|
||||
}
|
||||
|
||||
.icon-new {
|
||||
--icon-url: url("./public/new.svg");
|
||||
}
|
||||
|
||||
.icon-refresh {
|
||||
--icon-url: url("./public/refresh.svg");
|
||||
}
|
||||
|
||||
.icon-save {
|
||||
--icon-url: url("./public/save.svg");
|
||||
}
|
||||
|
||||
.icon-server {
|
||||
--icon-url: url("./public/server.svg");
|
||||
}
|
||||
|
||||
.icon-play {
|
||||
--icon-url: url("./public/play.svg");
|
||||
}
|
||||
|
||||
.icon-stop {
|
||||
--icon-url: url("./public/stop.svg");
|
||||
}
|
||||
|
||||
.config-path {
|
||||
margin-top: auto;
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.config-path strong {
|
||||
overflow-wrap: anywhere;
|
||||
color: var(--text);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.workspace {
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
background:
|
||||
linear-gradient(rgba(11, 13, 16, 0.88), rgba(11, 13, 16, 0.94)),
|
||||
url("./public/forge.png") center / 512px no-repeat;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 18px;
|
||||
padding: 28px 32px 20px;
|
||||
border-bottom: 1px solid #252a33;
|
||||
}
|
||||
|
||||
.topbar h1 {
|
||||
margin: 0;
|
||||
font-size: 28px;
|
||||
line-height: 1.1;
|
||||
color: var(--gold);
|
||||
}
|
||||
|
||||
.topbar p {
|
||||
margin: 8px 0 0;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.top-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.bulk-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.top-actions .icon-button {
|
||||
width: 42px;
|
||||
min-width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 8px;
|
||||
transition: border-color 140ms ease, background-color 140ms ease, color 140ms ease, box-shadow 140ms ease;
|
||||
}
|
||||
|
||||
.card-actions .icon-button {
|
||||
width: 36px;
|
||||
min-width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 10px;
|
||||
transition: border-color 140ms ease, background-color 140ms ease, color 140ms ease;
|
||||
}
|
||||
|
||||
.view {
|
||||
display: none;
|
||||
min-height: 0;
|
||||
max-height: 100%;
|
||||
overflow: auto;
|
||||
padding: 28px 32px 36px;
|
||||
}
|
||||
|
||||
.view.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.service-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(220px, 1fr));
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.service-card,
|
||||
.settings-panel {
|
||||
border: 1px solid rgba(255, 230, 129, 0.22);
|
||||
background: rgba(15, 17, 21, 0.86);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.service-card {
|
||||
display: grid;
|
||||
gap: 22px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.service-card header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.service-card h2,
|
||||
.settings-panel h2 {
|
||||
margin: 0;
|
||||
font-size: 19px;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.service-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.ping-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 22px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(114, 119, 128, 0.55);
|
||||
background: rgba(114, 119, 128, 0.14);
|
||||
color: #c1c6cf;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
.ping-pill.good {
|
||||
border-color: rgba(33, 208, 122, 0.45);
|
||||
background: rgba(33, 208, 122, 0.14);
|
||||
color: #92f7c6;
|
||||
}
|
||||
|
||||
.ping-pill.warn {
|
||||
border-color: rgba(255, 230, 129, 0.5);
|
||||
background: rgba(255, 230, 129, 0.14);
|
||||
color: #ffe9a8;
|
||||
}
|
||||
|
||||
.ping-pill.bad {
|
||||
border-color: rgba(255, 91, 102, 0.5);
|
||||
background: rgba(255, 91, 102, 0.14);
|
||||
color: #ffb2b8;
|
||||
}
|
||||
|
||||
.ping-pill.unavailable {
|
||||
border-color: rgba(114, 119, 128, 0.55);
|
||||
background: rgba(114, 119, 128, 0.14);
|
||||
color: #c1c6cf;
|
||||
}
|
||||
|
||||
.command-line {
|
||||
margin: 8px 0 0;
|
||||
color: var(--muted);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.status-pill {
|
||||
align-self: start;
|
||||
min-width: 82px;
|
||||
padding: 6px 8px;
|
||||
border-radius: 999px;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
color: #101114;
|
||||
background: var(--muted);
|
||||
}
|
||||
|
||||
.status-pill.running {
|
||||
background: var(--green);
|
||||
}
|
||||
|
||||
.status-pill.stopped {
|
||||
background: var(--red);
|
||||
}
|
||||
|
||||
.metrics {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.metrics div {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
padding-bottom: 9px;
|
||||
}
|
||||
|
||||
.metrics dt {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.metrics dd {
|
||||
margin: 0;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 12px;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.bulk-actions .start,
|
||||
.card-actions .start {
|
||||
color: #ffe48a;
|
||||
border-color: rgba(255, 230, 129, 0.42);
|
||||
background: rgba(255, 230, 129, 0.08);
|
||||
}
|
||||
|
||||
.bulk-actions .start:hover:not(:disabled),
|
||||
.card-actions .start:hover:not(:disabled) {
|
||||
border-color: #ffe48a;
|
||||
background: rgba(255, 230, 129, 0.16);
|
||||
box-shadow: 0 0 0 3px rgba(255, 230, 129, 0.1);
|
||||
}
|
||||
|
||||
.bulk-actions .stop,
|
||||
.card-actions .stop {
|
||||
color: #ff9ca3;
|
||||
border-color: rgba(255, 91, 102, 0.45);
|
||||
background: rgba(255, 91, 102, 0.08);
|
||||
}
|
||||
|
||||
.bulk-actions .stop:hover:not(:disabled),
|
||||
.card-actions .stop:hover:not(:disabled) {
|
||||
border-color: #ff9ca3;
|
||||
background: rgba(255, 91, 102, 0.16);
|
||||
box-shadow: 0 0 0 3px rgba(255, 91, 102, 0.1);
|
||||
}
|
||||
|
||||
.bulk-actions .icon-button:disabled,
|
||||
.card-actions .icon-button:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 1;
|
||||
color: #727780;
|
||||
border-color: rgba(114, 119, 128, 0.5);
|
||||
background: rgba(114, 119, 128, 0.12);
|
||||
}
|
||||
|
||||
.settings-form {
|
||||
display: block;
|
||||
padding-bottom: 44px;
|
||||
}
|
||||
|
||||
.settings-panel {
|
||||
padding: 20px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.settings-panel.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.settings-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.toggle-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
color: var(--muted);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.toggle-row input {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.switch-track {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
width: 48px;
|
||||
height: 26px;
|
||||
padding: 3px;
|
||||
border: 1px solid rgba(114, 119, 128, 0.55);
|
||||
border-radius: 999px;
|
||||
background: rgba(114, 119, 128, 0.22);
|
||||
transition: border-color 140ms ease, background-color 140ms ease, box-shadow 140ms ease;
|
||||
}
|
||||
|
||||
.switch-thumb {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 999px;
|
||||
background: #d4d0c6;
|
||||
box-shadow: 0 2px 7px rgba(0, 0, 0, 0.35);
|
||||
transform: translateX(0);
|
||||
transition: transform 140ms ease, background-color 140ms ease;
|
||||
}
|
||||
|
||||
.toggle-row:hover .switch-track {
|
||||
border-color: rgba(255, 230, 129, 0.6);
|
||||
}
|
||||
|
||||
.toggle-row input:focus-visible + .switch-track {
|
||||
box-shadow: 0 0 0 3px rgba(255, 230, 129, 0.18);
|
||||
}
|
||||
|
||||
.toggle-row input:checked + .switch-track {
|
||||
border-color: var(--gold);
|
||||
background: var(--gold);
|
||||
}
|
||||
|
||||
.toggle-row input:checked + .switch-track .switch-thumb {
|
||||
background: #101114;
|
||||
transform: translateX(22px);
|
||||
}
|
||||
|
||||
.field-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr 1fr;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.field {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.field.full {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.field.with-button {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.input-action {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 42px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.input-action.action-one {
|
||||
grid-template-columns: minmax(0, 1fr) 42px;
|
||||
}
|
||||
|
||||
.input-action.action-three {
|
||||
grid-template-columns: minmax(0, 1fr) repeat(3, 42px);
|
||||
}
|
||||
|
||||
.input-action button {
|
||||
height: 42px;
|
||||
}
|
||||
|
||||
.field label,
|
||||
.arg-field label {
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.field input,
|
||||
.field textarea,
|
||||
.field select,
|
||||
.arg-field input,
|
||||
.arg-field select {
|
||||
width: 100%;
|
||||
border: 1px solid var(--line);
|
||||
background: rgba(0, 0, 0, 0.48);
|
||||
color: var(--text);
|
||||
border-radius: 6px;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.field textarea {
|
||||
min-height: 72px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.args-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(180px, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.arg-field {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.arg-field.full {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.arg-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-height: 42px;
|
||||
margin-top: 21px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.arg-toggle input {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
accent-color: var(--gold);
|
||||
}
|
||||
|
||||
.surreal-install {
|
||||
padding: 14px 16px;
|
||||
border: 1px solid rgba(255, 230, 129, 0.18);
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 230, 129, 0.04);
|
||||
}
|
||||
|
||||
.install-summary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.install-summary div {
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.install-summary strong {
|
||||
color: var(--text);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.install-summary span {
|
||||
color: var(--muted);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.install-action {
|
||||
display: flex;
|
||||
align-items: end;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.install-version,
|
||||
.install-custom {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.install-version {
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
.install-custom {
|
||||
width: 180px;
|
||||
}
|
||||
|
||||
.install-version span,
|
||||
.install-custom span {
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.install-action button {
|
||||
height: 42px;
|
||||
}
|
||||
|
||||
.log-output {
|
||||
min-height: 100%;
|
||||
margin: 0;
|
||||
padding: 18px;
|
||||
border: 1px solid #252a33;
|
||||
background: rgba(0, 0, 0, 0.68);
|
||||
color: #d7f7df;
|
||||
border-radius: 8px;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
font-family: "Cascadia Mono", Consolas, monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.editor-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 28px;
|
||||
background: rgba(0, 0, 0, 0.68);
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.editor-overlay[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.editor-modal {
|
||||
display: grid;
|
||||
grid-template-rows: auto minmax(0, 1fr) auto;
|
||||
width: min(1180px, 92vw);
|
||||
height: min(760px, 86vh);
|
||||
border: 1px solid rgba(255, 230, 129, 0.3);
|
||||
background: #0e1116;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.editor-header,
|
||||
.editor-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 14px;
|
||||
padding: 16px 18px;
|
||||
border-bottom: 1px solid #252a33;
|
||||
}
|
||||
|
||||
.editor-header {
|
||||
position: relative;
|
||||
padding-right: 64px;
|
||||
}
|
||||
|
||||
.editor-header #editorCloseButton {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 16px;
|
||||
width: 34px;
|
||||
min-width: 34px;
|
||||
height: 34px;
|
||||
color: #ff9ca3;
|
||||
border-color: rgba(255, 91, 102, 0.45);
|
||||
background: rgba(255, 91, 102, 0.08);
|
||||
}
|
||||
|
||||
.editor-header #editorCloseButton .svg-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.editor-header #editorCloseButton:hover {
|
||||
border-color: #ff9ca3;
|
||||
background: rgba(255, 91, 102, 0.15);
|
||||
}
|
||||
|
||||
.editor-actions {
|
||||
justify-content: flex-end;
|
||||
border-top: 1px solid #252a33;
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.editor-header h2 {
|
||||
margin: 0;
|
||||
color: var(--gold);
|
||||
}
|
||||
|
||||
.editor-header p {
|
||||
margin: 6px 0 0;
|
||||
color: var(--muted);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
#configEditor {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
resize: none;
|
||||
padding: 18px;
|
||||
background: #050609;
|
||||
color: #e7f7df;
|
||||
font-family: "Cascadia Mono", Consolas, monospace;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
@keyframes gumball-ping-green {
|
||||
0%, 100% {
|
||||
box-shadow: 0 0 0px rgba(33, 208, 122, 0.2);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 12px rgba(33, 208, 122, 0.6);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes gumball-ping-red {
|
||||
0%, 100% {
|
||||
box-shadow: 0 0 0px rgba(255, 91, 102, 0.2);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 12px rgba(255, 91, 102, 0.62);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes gumball-ping-gold {
|
||||
0%, 100% {
|
||||
box-shadow: 0 0 0px rgba(255, 230, 129, 0.16);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 12px rgba(255, 230, 129, 0.55);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1050px) {
|
||||
.service-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.field-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.args-list {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 1.9 MiB |
@ -54,19 +54,7 @@ fn default_port() -> u16 {
|
||||
///
|
||||
/// If no config file is found, uses default values.
|
||||
pub fn load() -> Config {
|
||||
// Try current directory first
|
||||
let config_path = PathBuf::from("config.toml");
|
||||
|
||||
let config_path = if config_path.exists() {
|
||||
config_path
|
||||
} else {
|
||||
// Try executable directory
|
||||
std::env::current_exe()
|
||||
.ok()
|
||||
.and_then(|exe| exe.parent().map(|dir| dir.join("config.toml")))
|
||||
.filter(|p| p.exists())
|
||||
.unwrap_or_else(|| PathBuf::from("config.toml"))
|
||||
};
|
||||
let config_path = locate_config_path();
|
||||
|
||||
match fs::read_to_string(&config_path) {
|
||||
Ok(contents) => {
|
||||
@ -89,3 +77,27 @@ pub fn load() -> Config {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn locate_config_path() -> PathBuf {
|
||||
let mut candidates = Vec::new();
|
||||
|
||||
if let Ok(cwd) = std::env::current_dir() {
|
||||
candidates.push(cwd.join("config.toml"));
|
||||
candidates.push(cwd.join("@forge_server").join("config.toml"));
|
||||
}
|
||||
|
||||
if let Ok(exe) = std::env::current_exe() {
|
||||
if let Some(dir) = exe.parent() {
|
||||
candidates.push(dir.join("config.toml"));
|
||||
if let Some(parent) = dir.parent() {
|
||||
candidates.push(parent.join("config.toml"));
|
||||
candidates.push(parent.join("@forge_server").join("config.toml"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
candidates
|
||||
.into_iter()
|
||||
.find(|path| path.exists())
|
||||
.unwrap_or_else(|| PathBuf::from("config.toml"))
|
||||
}
|
||||
|
||||
@ -36,45 +36,12 @@ Official SurrealDB resources:
|
||||
- [SurrealDB install page](https://surrealdb.com/install)
|
||||
- [SurrealDB CLI `start` reference](https://surrealdb.com/docs/surrealdb/cli/start)
|
||||
|
||||
Forge also includes helper scripts under `arma/server/surrealdb`:
|
||||
|
||||
```powershell
|
||||
cd arma/server/surrealdb
|
||||
.\UpdateMe.bat
|
||||
.\RunMe.bat
|
||||
```
|
||||
|
||||
On Windows, `UpdateMe.bat` is a wrapper around `UpdateSurrealDB.ps1`. By
|
||||
default it installs or updates to the newest compatible SurrealDB 3.x release
|
||||
reported by SurrealDB's official version endpoint. You can also pin an exact
|
||||
release:
|
||||
|
||||
```powershell
|
||||
.\UpdateMe.bat v3.1.2
|
||||
.\UpdateSurrealDB.ps1 -Version v3.1.2
|
||||
```
|
||||
|
||||
To intentionally install the latest stable SurrealDB release regardless of
|
||||
major version, run:
|
||||
|
||||
```powershell
|
||||
.\UpdateMe.bat latest
|
||||
```
|
||||
|
||||
The `latest` option prompts for confirmation because a newer SurrealDB major
|
||||
version can require rebuilding the Forge server extension from source with a
|
||||
compatible `surrealdb` Rust crate.
|
||||
|
||||
`RunMe.bat` is a wrapper around `RunSurrealDB.ps1`, which starts the local
|
||||
Forge database with the same defaults shown below.
|
||||
|
||||
On Linux or macOS:
|
||||
|
||||
```bash
|
||||
cd arma/server/surrealdb
|
||||
./setup.sh
|
||||
./run.sh
|
||||
```
|
||||
Forge Host includes the recommended local setup path. Open the SurrealDB view
|
||||
to install or update SurrealDB, configure the bind address and database path,
|
||||
and start or stop the local database. Enter `3` to install the latest compatible
|
||||
SurrealDB 3.x release, enter an exact version such as `v3.1.2` to pin a
|
||||
release, or enter `latest` only after confirming compatibility with the Forge
|
||||
server extension.
|
||||
|
||||
Install SurrealDB with the official method for your platform:
|
||||
|
||||
|
||||
@ -36,45 +36,12 @@ Official SurrealDB resources:
|
||||
- [SurrealDB install page](https://surrealdb.com/install)
|
||||
- [SurrealDB CLI `start` reference](https://surrealdb.com/docs/surrealdb/cli/start)
|
||||
|
||||
Forge also includes helper scripts under `arma/server/surrealdb`:
|
||||
|
||||
```powershell
|
||||
cd arma/server/surrealdb
|
||||
.\UpdateMe.bat
|
||||
.\RunMe.bat
|
||||
```
|
||||
|
||||
On Windows, `UpdateMe.bat` is a wrapper around `UpdateSurrealDB.ps1`. By
|
||||
default it installs or updates to the newest compatible SurrealDB 3.x release
|
||||
reported by SurrealDB's official version endpoint. You can also pin an exact
|
||||
release:
|
||||
|
||||
```powershell
|
||||
.\UpdateMe.bat v3.1.2
|
||||
.\UpdateSurrealDB.ps1 -Version v3.1.2
|
||||
```
|
||||
|
||||
To intentionally install the latest stable SurrealDB release regardless of
|
||||
major version, run:
|
||||
|
||||
```powershell
|
||||
.\UpdateMe.bat latest
|
||||
```
|
||||
|
||||
The `latest` option prompts for confirmation because a newer SurrealDB major
|
||||
version can require rebuilding the Forge server extension from source with a
|
||||
compatible `surrealdb` Rust crate.
|
||||
|
||||
`RunMe.bat` is a wrapper around `RunSurrealDB.ps1`, which starts the local
|
||||
Forge database with the same defaults shown below.
|
||||
|
||||
On Linux or macOS:
|
||||
|
||||
```bash
|
||||
cd arma/server/surrealdb
|
||||
./setup.sh
|
||||
./run.sh
|
||||
```
|
||||
Forge Host includes the recommended local setup path. Open the SurrealDB view
|
||||
to install or update SurrealDB, configure the bind address and database path,
|
||||
and start or stop the local database. Enter `3` to install the latest compatible
|
||||
SurrealDB 3.x release, enter an exact version such as `v3.1.2` to pin a
|
||||
release, or enter `latest` only after confirming compatibility with the Forge
|
||||
server extension.
|
||||
|
||||
Install SurrealDB with the official method for your platform:
|
||||
|
||||
|
||||
255
package-lock.json
generated
@ -5,7 +5,11 @@
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "forge-webui",
|
||||
"dependencies": {
|
||||
"@tauri-apps/plugin-dialog": "^2.7.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2.0.0",
|
||||
"html-minifier-terser": "^7.2.0",
|
||||
"lightningcss": "^1.29.3",
|
||||
"postcss": "^8.5.6",
|
||||
@ -64,6 +68,257 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/api": {
|
||||
"version": "2.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.11.0.tgz",
|
||||
"integrity": "sha512-7CinYODhky9lmO23xHnUFv0Xt43fbtWMyxZcLcRBlFkcgXKuEirBvHpmtJ89YMhyeGcq20Wuc47Fa4XjyniywA==",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/tauri"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli": {
|
||||
"version": "2.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.11.2.tgz",
|
||||
"integrity": "sha512-bk3HemqvGRoy+5D/dVMUQHKMYLglD0jVnMm/0iGMH6ufZ+p8r14m6BpIixwij3PBvZdvORUp1YifTD8QxVZ1Nw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"bin": {
|
||||
"tauri": "tauri.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/tauri"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@tauri-apps/cli-darwin-arm64": "2.11.2",
|
||||
"@tauri-apps/cli-darwin-x64": "2.11.2",
|
||||
"@tauri-apps/cli-linux-arm-gnueabihf": "2.11.2",
|
||||
"@tauri-apps/cli-linux-arm64-gnu": "2.11.2",
|
||||
"@tauri-apps/cli-linux-arm64-musl": "2.11.2",
|
||||
"@tauri-apps/cli-linux-riscv64-gnu": "2.11.2",
|
||||
"@tauri-apps/cli-linux-x64-gnu": "2.11.2",
|
||||
"@tauri-apps/cli-linux-x64-musl": "2.11.2",
|
||||
"@tauri-apps/cli-win32-arm64-msvc": "2.11.2",
|
||||
"@tauri-apps/cli-win32-ia32-msvc": "2.11.2",
|
||||
"@tauri-apps/cli-win32-x64-msvc": "2.11.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-darwin-arm64": {
|
||||
"version": "2.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.11.2.tgz",
|
||||
"integrity": "sha512-+4UZzLt+eOAEQCwgd+TqKgyUJMrvx+BgdXLLaqJYmPqzP+nE6YZr/hY6CWLYGQb8jFn99jEkmC6uA3tNvamA1w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-darwin-x64": {
|
||||
"version": "2.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.11.2.tgz",
|
||||
"integrity": "sha512-VjYYtZUPqDMLutSfJEyxFE3Bz+DPi7c8wC3imckgvciLDZLq4qwKJxBicg0BXGhXjJsl8vKWgWRFNMPELQ+Xyg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-arm-gnueabihf": {
|
||||
"version": "2.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.11.2.tgz",
|
||||
"integrity": "sha512-yMemD6f4i95AQriS8EazyOFzbE34yjnP16i3IOzpHGQvBoy2DjypFMFBq0NtPuITURv/cOGguRtHR5d79/9CSA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-arm64-gnu": {
|
||||
"version": "2.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.11.2.tgz",
|
||||
"integrity": "sha512-cgI91D2wL8GSgoWwZXDqt+DwnuZCP2/bz03QAE4TrhgAKIsrB4hX26W/H1EONPUUNkqrsgeCD0wU6pcNjV/5kw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-arm64-musl": {
|
||||
"version": "2.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.11.2.tgz",
|
||||
"integrity": "sha512-X1rm0BERqAAggtYTESSgXrS3sz4Sb/OiPiz54UqISlXW+GkR3vNIGnsy/lejNmoXGVqri3Q53BCfQiclOIyRPw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-riscv64-gnu": {
|
||||
"version": "2.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.11.2.tgz",
|
||||
"integrity": "sha512-usbMLJbT3KtkOrBMDVeGYNM35aTHXx38SJSzTMSqqjeUIOQ+iVPjb2yAGNAE+KqmBbAx4FOFIyMeKXx2M/JKGQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-x64-gnu": {
|
||||
"version": "2.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.11.2.tgz",
|
||||
"integrity": "sha512-Ru4gwJKPG0ctVGchRGpRup4Y4lW2SSfFnrbQcyHhCliKy4g8Qz97TrUgCur4CbWyAgKxvGh3SjrkA0LDYzDGiw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-x64-musl": {
|
||||
"version": "2.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.11.2.tgz",
|
||||
"integrity": "sha512-eUm7T6clN1MMmNSRQ9gaWsQdyehQx2Gmn5hht/QUlqZQI/qcP2OJK5dnaxqwFzCr2HdsEo9ydxaqcS1oJzMvUw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-win32-arm64-msvc": {
|
||||
"version": "2.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.11.2.tgz",
|
||||
"integrity": "sha512-HeeZW80jU+gVTOEX4X/hC6NVSAdDVXajwP5fxIZ/3z9WvUC7qrudX2GMTilYq6Dg0e0sk0XgsAJD1hZ5wPBXUA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-win32-ia32-msvc": {
|
||||
"version": "2.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.11.2.tgz",
|
||||
"integrity": "sha512-YhjQNZcXfbkCLyazSv1nPnJ9iRFE1wm6kc51FDbU10/Dk09io+6PAGMLjkxnX2GdM0qMnDmTjstY8mTDVvtKeA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-win32-x64-msvc": {
|
||||
"version": "2.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.11.2.tgz",
|
||||
"integrity": "sha512-d2JchlFIpZevZVReyqhQOekJmb1UH3rhZ5VX6sH3ty9ETE0TKQavpihvoScUXfKKpW6HZC0MrFGRU0ZtD+w3gA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/plugin-dialog": {
|
||||
"version": "2.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.7.1.tgz",
|
||||
"integrity": "sha512-OK1UBXYt+ojcmxMktzzuyonYIFta8CmAASpX+CA+DTGK24KlHjhYI6x2iOJ/TjZF4N7/ACK1oFmEOjIY9IhzOQ==",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.11.0"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.16.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
"name": "forge-webui",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2.0.0",
|
||||
"html-minifier-terser": "^7.2.0",
|
||||
"lightningcss": "^1.29.3",
|
||||
"postcss": "^8.5.6",
|
||||
@ -14,6 +15,11 @@
|
||||
"docs:sync": "node tools/sync-docus-docs.mjs",
|
||||
"docs:dev": "npm --prefix docus run dev",
|
||||
"docs:build": "npm --prefix docus run build",
|
||||
"host:dev": "cd bin/host/src-tauri && tauri dev",
|
||||
"host:build": "cd bin/host/src-tauri && tauri build",
|
||||
"workflow": "node tools/git-workflow.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tauri-apps/plugin-dialog": "^2.7.1"
|
||||
}
|
||||
}
|
||||
|
||||