3 Commits

Author SHA1 Message Date
Vantz Stockwell
180631989a fix(panel): real auto-updating version + remove fake agent footer; rename companion -> Corrosion host agent
All checks were successful
Build Host Agent / build (push) Successful in 28s
Test Asgard Runner / test (push) Successful in 3s
Version badge: was hardcoded '1.0.8' — now single-sourced from frontend/package.json (1.0.0) via Vite define __APP_VERSION__, so it auto-updates on release. Sidebar agent footer: removed the FABRICATED 'asgard-01' host name and the fake 'Agent v1.0.8' line — now shows real server.connection data, or an honest 'No host agent connected' empty state when nothing is deployed (the operator's actual state). Renamed 'Companion agent' -> 'Corrosion host agent' across the UI (ServerView/SetupWizard/Dashboard/Plugins), the binary names (corrosion-host-agent-<os>-<arch>) + CDN path (/host-agent/), the Go Makefile build output, and the Gitea CI workflow — frontend download links and CI output now match. Marketing hero mock host names neutralized (asgard-01 -> rust-host/dune-host/conan-host). DB column names (companion_last_seen) left intact. Build green; zero 'asgard'/'1.0.8' remain in frontend/src.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 09:03:37 -04:00
Vantz Stockwell
23decd9b08 feat(panel): per-game UI adaptation — sidebar, Server view, and dashboard transform by selected game
All checks were successful
Test Asgard Runner / test (push) Successful in 3s
Drives the panel off the active game (GameSwitcher selection) + the GameProfile registry, so each game visibly differs (not just accent color). Sidebar nav: Rust = full (uMod plugins + plugin configs); Conan/Soulmask/Dune drop uMod + plugin-configs and relabel reset (Wipe World / World Reset / Deep Desert), Dune relabels Console->Broadcast (no RCON) and is Docker-managed. ServerView: management-model badge + game-appropriate panels (Rust deploy + Oxide; Dune Docker/BattleGroup-Sietches; Conan clans/thralls/avatars/purge; Soulmask main-client cluster) with HONEST EmptyStates where no backend data exists yet. Dashboard: per-game reset terminology + stat labels. No invented routes (all map to existing router entries); no fabricated data. Build green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 08:37:03 -04:00
Vantz Stockwell
8b84bba165 fix(docker): auto-build schema on a fresh DB via docker-entrypoint-initdb.d
All checks were successful
Test Asgard Runner / test (push) Successful in 3s
Root cause of 'data lost on every rebuild': nothing created the Postgres schema. TypeORM is synchronize:false, the API container runs no migration step, and there was no init mount — so a fresh pg_data volume came up with ZERO tables (empty/broken DB; the schema had only ever been loaded manually). Mount backend/migrations/*.sql into /docker-entrypoint-initdb.d so Postgres auto-applies the full schema (001..021, plain SQL) ON FIRST INIT ONLY. Existing volumes are untouched (initdb scripts run only on an empty data dir); a fresh volume now self-heals the schema. NOTE: actual row DATA still persists only while the pg_data named volume persists — 'docker compose down' keeps it across 'build --no-cache'; 'down -v' / volume prune is the only thing that wipes it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 08:34:18 -04:00
13 changed files with 456 additions and 157 deletions

View File

@@ -1,4 +1,4 @@
name: Build Companion Agent
name: Build Host Agent
on:
push:
@@ -26,19 +26,19 @@ jobs:
run: |
cd companion-agent
mkdir -p bin
GOOS=linux GOARCH=amd64 go build -ldflags "-s -w -X main.version=${{ steps.version.outputs.VERSION }}" -o bin/corrosion-companion-linux-amd64 ./cmd/agent
chmod +x bin/corrosion-companion-linux-amd64
GOOS=linux GOARCH=amd64 go build -ldflags "-s -w -X main.version=${{ steps.version.outputs.VERSION }}" -o bin/corrosion-host-agent-linux-amd64 ./cmd/agent
chmod +x bin/corrosion-host-agent-linux-amd64
- name: Build Windows AMD64
run: |
cd companion-agent
GOOS=windows GOARCH=amd64 go build -ldflags "-s -w -X main.version=${{ steps.version.outputs.VERSION }}" -o bin/corrosion-companion-windows-amd64.exe ./cmd/agent
GOOS=windows GOARCH=amd64 go build -ldflags "-s -w -X main.version=${{ steps.version.outputs.VERSION }}" -o bin/corrosion-host-agent-windows-amd64.exe ./cmd/agent
- name: Generate checksums
run: |
cd companion-agent/bin
sha256sum corrosion-companion-linux-amd64 > checksums.txt
sha256sum corrosion-companion-windows-amd64.exe >> checksums.txt
sha256sum corrosion-host-agent-linux-amd64 > checksums.txt
sha256sum corrosion-host-agent-windows-amd64.exe >> checksums.txt
cat checksums.txt
- name: Create Release
@@ -53,7 +53,7 @@ jobs:
RESPONSE=$(curl -s -X POST \
-H "Authorization: token ${RELEASE_TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"tag_name\": \"${VERSION}\", \"name\": \"Companion Agent ${VERSION}\", \"body\": \"Companion Agent release ${VERSION}\", \"draft\": false, \"prerelease\": false}" \
-d "{\"tag_name\": \"${VERSION}\", \"name\": \"Corrosion Host Agent ${VERSION}\", \"body\": \"Corrosion Host Agent release ${VERSION}\", \"draft\": false, \"prerelease\": false}" \
"${API_URL}/repos/${REPO}/releases")
RELEASE_ID=$(echo "$RESPONSE" | grep -o '"id":[0-9]*' | head -1 | grep -o '[0-9]*')
@@ -68,15 +68,15 @@ jobs:
curl -s -X POST \
-H "Authorization: token ${RELEASE_TOKEN}" \
-H "Content-Type: application/octet-stream" \
--data-binary @companion-agent/bin/corrosion-companion-linux-amd64 \
"${API_URL}/repos/${REPO}/releases/${RELEASE_ID}/assets?name=corrosion-companion-linux-amd64"
--data-binary @companion-agent/bin/corrosion-host-agent-linux-amd64 \
"${API_URL}/repos/${REPO}/releases/${RELEASE_ID}/assets?name=corrosion-host-agent-linux-amd64"
# Upload Windows binary
curl -s -X POST \
-H "Authorization: token ${RELEASE_TOKEN}" \
-H "Content-Type: application/octet-stream" \
--data-binary @companion-agent/bin/corrosion-companion-windows-amd64.exe \
"${API_URL}/repos/${REPO}/releases/${RELEASE_ID}/assets?name=corrosion-companion-windows-amd64.exe"
--data-binary @companion-agent/bin/corrosion-host-agent-windows-amd64.exe \
"${API_URL}/repos/${REPO}/releases/${RELEASE_ID}/assets?name=corrosion-host-agent-windows-amd64.exe"
# Upload checksums
curl -s -X POST \
@@ -89,43 +89,43 @@ jobs:
run: |
CDN_URL="https://cdn.corrosionmgmt.com"
# Upload Linux binary to /companion/latest/
# Upload Linux binary to /host-agent/latest/
curl -s -X POST \
-F "file=@companion-agent/bin/corrosion-companion-linux-amd64" \
"${CDN_URL}/companion/latest/corrosion-companion-linux-amd64"
-F "file=@companion-agent/bin/corrosion-host-agent-linux-amd64" \
"${CDN_URL}/host-agent/latest/corrosion-host-agent-linux-amd64"
# Upload Windows binary to /companion/latest/
# Upload Windows binary to /host-agent/latest/
curl -s -X POST \
-F "file=@companion-agent/bin/corrosion-companion-windows-amd64.exe" \
"${CDN_URL}/companion/latest/corrosion-companion-windows-amd64.exe"
-F "file=@companion-agent/bin/corrosion-host-agent-windows-amd64.exe" \
"${CDN_URL}/host-agent/latest/corrosion-host-agent-windows-amd64.exe"
# Upload checksums
curl -s -X POST \
-F "file=@companion-agent/bin/checksums.txt" \
"${CDN_URL}/companion/latest/checksums.txt"
"${CDN_URL}/host-agent/latest/checksums.txt"
# Also upload versioned copies
VERSION=${{ steps.version.outputs.VERSION }}
curl -s -X POST \
-F "file=@companion-agent/bin/corrosion-companion-linux-amd64" \
"${CDN_URL}/companion/${VERSION}/corrosion-companion-linux-amd64"
-F "file=@companion-agent/bin/corrosion-host-agent-linux-amd64" \
"${CDN_URL}/host-agent/${VERSION}/corrosion-host-agent-linux-amd64"
curl -s -X POST \
-F "file=@companion-agent/bin/corrosion-companion-windows-amd64.exe" \
"${CDN_URL}/companion/${VERSION}/corrosion-companion-windows-amd64.exe"
-F "file=@companion-agent/bin/corrosion-host-agent-windows-amd64.exe" \
"${CDN_URL}/host-agent/${VERSION}/corrosion-host-agent-windows-amd64.exe"
curl -s -X POST \
-F "file=@companion-agent/bin/checksums.txt" \
"${CDN_URL}/companion/${VERSION}/checksums.txt"
"${CDN_URL}/host-agent/${VERSION}/checksums.txt"
echo "CDN upload complete: ${CDN_URL}/companion/latest/"
echo "CDN upload complete: ${CDN_URL}/host-agent/latest/"
- name: Build Summary
run: |
echo "## Companion Agent Build Complete" >> $GITHUB_STEP_SUMMARY
echo "## Corrosion Host Agent Build Complete" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Version:** ${{ steps.version.outputs.VERSION }}" >> $GITHUB_STEP_SUMMARY
echo "**Commit:** ${GITHUB_SHA:0:7}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Built Artifacts:" >> $GITHUB_STEP_SUMMARY
echo "- Linux AMD64 ($(stat -c%s companion-agent/bin/corrosion-companion-linux-amd64) bytes)" >> $GITHUB_STEP_SUMMARY
echo "- Windows AMD64 ($(stat -c%s companion-agent/bin/corrosion-companion-windows-amd64.exe) bytes)" >> $GITHUB_STEP_SUMMARY
echo "- Linux AMD64 ($(stat -c%s companion-agent/bin/corrosion-host-agent-linux-amd64) bytes)" >> $GITHUB_STEP_SUMMARY
echo "- Windows AMD64 ($(stat -c%s companion-agent/bin/corrosion-host-agent-windows-amd64.exe) bytes)" >> $GITHUB_STEP_SUMMARY
echo "- SHA256 checksums" >> $GITHUB_STEP_SUMMARY

View File

@@ -1,7 +1,7 @@
.PHONY: all build build-linux build-windows clean test run
# Binary names
BINARY_NAME=corrosion-companion
BINARY_NAME=corrosion-host-agent
BINARY_LINUX=$(BINARY_NAME)-linux-amd64
BINARY_WINDOWS=$(BINARY_NAME)-windows-amd64.exe
@@ -66,10 +66,10 @@ run: build-local
install-service:
@echo "Installing systemd service..."
@sudo cp $(BUILD_DIR)/$(BINARY_LINUX) /usr/local/bin/$(BINARY_NAME)
@sudo cp deployment/corrosion-companion.service /etc/systemd/system/
@sudo cp deployment/corrosion-host-agent.service /etc/systemd/system/
@sudo systemctl daemon-reload
@sudo systemctl enable corrosion-companion
@echo "Service installed. Configure /etc/corrosion-companion/.env then start with: sudo systemctl start corrosion-companion"
@sudo systemctl enable corrosion-host-agent
@echo "Service installed. Configure /etc/corrosion-host-agent/.env then start with: sudo systemctl start corrosion-host-agent"
# Development helpers
dev: build-local

View File

@@ -8,6 +8,13 @@ services:
POSTGRES_PASSWORD: ${DB_PASSWORD:-corrosion_dev}
volumes:
- pg_data:/var/lib/postgresql/data
# Auto-build the schema on a FRESH database. Postgres runs these ONLY when
# the data dir is empty (first boot or after a volume reset), so it never
# touches an existing volume — it just makes a fresh DB self-heal: the full
# schema is applied in order from the sqlx migrations (001..NNN), then the
# API's bootstrap seeds the admin. Rebuilds (with the volume kept) are a
# no-op here; the data persists. Only `down -v` / volume prune loses data.
- ../backend/migrations:/docker-entrypoint-initdb.d:ro
ports:
- "8101:5432"
healthcheck:

View File

@@ -1,7 +1,7 @@
{
"name": "frontend",
"private": true,
"version": "0.0.0",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",

1
frontend/src/app-version.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
declare const __APP_VERSION__: string

View File

@@ -1,15 +1,19 @@
<script setup lang="ts">
/**
* DashboardLayout — game-aware app shell (Phase C redesign).
* Replaces the old Tailwind-only sidebar with the DS component set.
* Preserves: navSections, permission gating, super-admin section, logout, RouterView.
* Adds: GameSwitcher, Logo, DS NavItem, agent-health footer, topbar w/ search + theme toggle.
* Nav is driven by GAME_PROFILES[activeGame].nav — switching the GameSwitcher
* visibly changes nav items, labels, and sections per game.
* Preserves: permission gating, super-admin section, logout, mobile sidebar,
* GameSwitcher, agent-health footer, topbar.
*/
import { ref } from 'vue'
import { ref, computed } from 'vue'
import { RouterView, useRoute, useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import { useServerStore } from '@/stores/server'
import { useThemeGame } from '@/composables/useThemeGame'
import { useGameProfile } from '@/config/gameProfiles'
import type { NavSection, NavItemDef } from '@/config/gameProfiles'
import { safeDate } from '@/utils/formatters'
import Logo from '@/components/ds/brand/Logo.vue'
import Badge from '@/components/ds/core/Badge.vue'
import StatusDot from '@/components/ds/core/StatusDot.vue'
@@ -33,7 +37,7 @@ const sidebarOpen = ref(false)
function closeSidebar() { sidebarOpen.value = false }
// ---- App version ----
const APP_VERSION = '1.0.8'
const APP_VERSION = __APP_VERSION__
// ---- Game switcher ----
const GAME_OPTIONS: GameOption[] = [
@@ -53,61 +57,15 @@ function onActiveGame(val: string) {
setActiveGame(val as ActiveGame)
}
// ---- Navigation ----
type NavItemDef = { name: string; path: string; icon: string; permission: string | null }
type NavSection = { label: string; items: NavItemDef[] }
const navSections: NavSection[] = [
{
label: '',
items: [
{ name: 'Dashboard', path: '/', icon: 'layout-dashboard', permission: null },
],
},
{
label: 'Server',
items: [
{ name: 'Server', path: '/server', icon: 'server', permission: 'server.view' },
{ name: 'Console', path: '/console', icon: 'terminal', permission: 'console.view' },
{ name: 'Players', path: '/players', icon: 'users', permission: 'players.view' },
{ name: 'Plugins', path: '/plugins', icon: 'puzzle', permission: 'plugins.view' },
{ name: 'File manager', path: '/files', icon: 'folder-open', permission: 'files.view' },
],
},
{
label: 'Plugin configs',
items: [
{ name: 'Plugin configs', path: '/plugin-configs', icon: 'puzzle', permission: null },
],
},
{
label: 'Operations',
items: [
{ name: 'Wipe manager', path: '/wipes', icon: 'trash-2', permission: 'wipes.view' },
{ name: 'Maps', path: '/maps', icon: 'map', permission: 'maps.view' },
{ name: 'Schedules', path: '/schedules', icon: 'calendar-clock', permission: 'schedules.view' },
],
},
{
label: 'Monitoring',
items: [
{ name: 'Chat log', path: '/chat', icon: 'message-square', permission: 'chat.view' },
{ name: 'Analytics', path: '/analytics', icon: 'bar-chart-3', permission: 'analytics.view' },
{ name: 'Alerts', path: '/alerts', icon: 'triangle-alert', permission: 'alerts.view' },
{ name: 'Notifications', path: '/notifications', icon: 'bell', permission: 'notifications.view' },
],
},
{
label: 'Management',
items: [
{ name: 'Team', path: '/team', icon: 'users', permission: null },
{ name: 'Store', path: '/store/config', icon: 'shopping-cart', permission: 'store.view' },
{ name: 'Modules', path: '/modules', icon: 'layers', permission: 'modules.view' },
{ name: 'Changelog', path: '/changelog', icon: 'file-text', permission: 'changelog.view' },
{ name: 'Settings', path: '/settings', icon: 'settings', permission: 'settings.view' },
],
},
]
// ---- Navigation — driven by the game profile registry ----
/**
* For 'all', fall back to rust (superset nav). For a specific game, look up
* its profile. noUncheckedIndexedAccess-safe: always ?? GAME_PROFILES.rust.
*/
const activeNavSections = computed<NavSection[]>(() => {
const game = activeGame.value === 'all' ? 'rust' : activeGame.value
return (useGameProfile(game)).nav
})
const adminNavItems = [
{ name: 'Admin home', path: '/admin', icon: 'shield' },
@@ -137,6 +95,8 @@ function hasVisibleItems(section: NavSection): boolean {
}
// ---- Agent health ----
const hasAgent = computed(() => server.connection !== null)
const agentTone = computed(() => {
const cs = server.connection?.connection_status
if (cs === 'connected') return 'online' as const
@@ -149,18 +109,23 @@ const agentLabel = computed(() => {
if (cs === 'degraded') return 'Degraded'
return 'Offline'
})
const agentName = computed(() => {
const ip = server.connection?.server_ip
return ip ?? 'asgard-01'
const agentName = computed(() => server.connection?.server_ip ?? 'Host agent')
const agentMetaLine = computed(() => {
const cs = server.connection?.connection_status
let line = cs === 'connected' ? 'Connected' : server.connection?.companion_last_seen
? `Last seen ${safeDate(server.connection.companion_last_seen)}`
: 'Awaiting first heartbeat'
if (server.stats) {
line += ` · ${server.stats.player_count}/${server.stats.max_players} players`
}
return line
})
// ---- Topbar ----
const serverName = computed(() => auth.license?.server_name ?? 'Your servers')
const userName = computed(() => auth.user?.username ?? '')
const themeIcon = computed(() => theme.value === 'dark' ? 'sun' : 'moon')
// ---- Import computed from vue (missed above) ----
import { computed } from 'vue'
</script>
<template>
@@ -197,20 +162,20 @@ import { computed } from 'vue'
/>
</div>
<!-- Navigation -->
<!-- Navigation sections driven by GAME_PROFILES[activeGame].nav -->
<nav class="side__nav">
<template v-for="section in navSections" :key="section.label">
<template v-for="section in activeNavSections" :key="section.label">
<template v-if="hasVisibleItems(section)">
<div class="side__sec">
<div v-if="section.label" class="t-eyebrow side__lbl">{{ section.label }}</div>
<NavItem
v-for="item in section.items"
v-show="canShowNavItem(item)"
:key="item.path"
:key="item.route"
:icon="item.icon"
:label="item.name"
:active="isActive(item.path)"
@click="navigate(item.path)"
:label="item.label"
:active="isActive(item.route)"
@click="navigate(item.route)"
/>
</div>
</template>
@@ -230,18 +195,24 @@ import { computed } from 'vue'
</div>
</nav>
<!-- Agent health footer -->
<!-- Host agent footer -->
<div class="side__foot">
<div class="agent">
<!-- Connected: real IP + status badge + meta line -->
<div v-if="hasAgent" class="agent">
<div class="agent__row">
<StatusDot :tone="agentTone" :pulse="agentTone === 'online'" />
<span class="agent__name">{{ agentName }}</span>
<Badge :tone="agentTone" size="md">{{ agentLabel }}</Badge>
</div>
<div class="agent__meta">
Agent v{{ APP_VERSION }}
<template v-if="server.stats"> · {{ server.stats.player_count }}/{{ server.stats.max_players }} players</template>
<div class="agent__meta">{{ agentMetaLine }}</div>
</div>
<!-- Not connected: honest empty state -->
<div v-else class="agent agent--empty">
<div class="agent__row">
<StatusDot tone="offline" />
<span class="agent__name agent__name--muted">No host agent connected</span>
</div>
<div class="agent__meta">Install the Corrosion host agent from the Server page</div>
</div>
<!-- User / logout row -->
<div class="side__user">
@@ -419,6 +390,13 @@ body { margin: 0; overflow: hidden; }
padding-left: 16px;
}
.agent--empty { opacity: 0.7; }
.agent__name--muted {
color: var(--text-tertiary);
font-style: italic;
}
.side__user {
display: flex;
align-items: center;

View File

@@ -2,9 +2,9 @@
* gameProfiles.ts — Source of truth for per-game UI adaptation.
*
* Every game-specific label, terminology, Steam app ID, management model,
* and stat field list lives here. The dashboard, server cards, wipe manager,
* and any future multi-game surface should key off this registry — never
* hard-code game-specific strings in components.
* stat field list, AND sidebar nav lives here. The dashboard, server cards,
* wipe manager, sidebar, and any future multi-game surface should key off this
* registry — never hard-code game-specific strings in components.
*
* Backend status: the backend has NO game field on licenses yet. Today every
* license is implicitly Rust. This registry is ready: when the backend adds a
@@ -15,6 +15,26 @@
* GAME_PROFILES. Nothing else changes.
*/
// ---------------------------------------------------------------------------
// Nav structure — drives the per-game sidebar
// ---------------------------------------------------------------------------
/** A single sidebar nav item. route must be an existing panel route path. */
export interface NavItemDef {
label: string
route: string
icon: string
/** Permission key required to show this item (e.g. 'plugins.view'). Null = always visible. */
permission: string | null
}
/** A labelled section grouping nav items in the sidebar. */
export interface NavSection {
/** Section heading (eyebrow text). Empty string = no heading. */
label: string
items: NavItemDef[]
}
// ---------------------------------------------------------------------------
// Union types — exhaustive, never widen to string
// ---------------------------------------------------------------------------
@@ -87,12 +107,67 @@ export interface GameProfile {
* First entry is always Players; subsequent entries are game-specific.
*/
statFields: [string, string, string]
/**
* Per-game sidebar navigation. Ordered list of sections, each with items.
* Items MUST use only existing panel routes (see router/index.ts).
* The sidebar renders exactly these sections for the active game.
*/
nav: NavSection[]
}
// ---------------------------------------------------------------------------
// Registry
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// Shared nav building blocks — reused across game nav definitions
// ---------------------------------------------------------------------------
const NAV_DASHBOARD: NavItemDef = { label: 'Dashboard', route: '/', icon: 'layout-dashboard', permission: null }
const NAV_SERVER: NavItemDef = { label: 'Server', route: '/server', icon: 'server', permission: 'server.view' }
const NAV_CONSOLE: NavItemDef = { label: 'Console', route: '/console', icon: 'terminal', permission: 'console.view' }
const NAV_PLAYERS: NavItemDef = { label: 'Players', route: '/players', icon: 'users', permission: 'players.view' }
const NAV_PLUGINS: NavItemDef = { label: 'Plugins (uMod)', route: '/plugins', icon: 'puzzle', permission: 'plugins.view' }
const NAV_FILES: NavItemDef = { label: 'File manager', route: '/files', icon: 'folder-open', permission: 'files.view' }
const NAV_PLUGIN_CONFIGS: NavItemDef = { label: 'Plugin configs', route: '/plugin-configs', icon: 'sliders', permission: null }
const NAV_SCHEDULES: NavItemDef = { label: 'Schedules', route: '/schedules', icon: 'calendar-clock', permission: 'schedules.view' }
const NAV_CHAT: NavItemDef = { label: 'Chat log', route: '/chat', icon: 'message-square', permission: 'chat.view' }
const NAV_ANALYTICS: NavItemDef = { label: 'Analytics', route: '/analytics', icon: 'bar-chart-3', permission: 'analytics.view' }
const NAV_ALERTS: NavItemDef = { label: 'Alerts', route: '/alerts', icon: 'triangle-alert', permission: 'alerts.view' }
const NAV_NOTIFICATIONS: NavItemDef = { label: 'Notifications', route: '/notifications', icon: 'bell', permission: 'notifications.view' }
const NAV_TEAM: NavItemDef = { label: 'Team', route: '/team', icon: 'users', permission: null }
const NAV_STORE: NavItemDef = { label: 'Store', route: '/store/config', icon: 'shopping-cart', permission: 'store.view' }
const NAV_MODULES: NavItemDef = { label: 'Modules', route: '/modules', icon: 'layers', permission: 'modules.view' }
const NAV_CHANGELOG: NavItemDef = { label: 'Changelog', route: '/changelog', icon: 'file-text', permission: 'changelog.view' }
const NAV_SETTINGS: NavItemDef = { label: 'Settings', route: '/settings', icon: 'settings', permission: 'settings.view' }
const NAV_MAPS: NavItemDef = { label: 'Maps', route: '/maps', icon: 'map', permission: 'maps.view' }
/** Full Rust / 'all' nav — superset used as fallback. */
const RUST_NAV: NavSection[] = [
{ label: '', items: [NAV_DASHBOARD] },
{
label: 'Server',
items: [NAV_SERVER, NAV_CONSOLE, NAV_PLAYERS, NAV_PLUGINS, NAV_FILES],
},
{ label: 'Plugin configs', items: [NAV_PLUGIN_CONFIGS] },
{
label: 'Operations',
items: [
{ label: 'Wipe', route: '/wipes', icon: 'trash-2', permission: 'wipes.view' },
NAV_MAPS,
NAV_SCHEDULES,
],
},
{
label: 'Monitoring',
items: [NAV_CHAT, NAV_ANALYTICS, NAV_ALERTS, NAV_NOTIFICATIONS],
},
{
label: 'Management',
items: [NAV_TEAM, NAV_STORE, NAV_MODULES, NAV_CHANGELOG, NAV_SETTINGS],
},
]
export const GAME_PROFILES: Record<GameId, GameProfile> = {
rust: {
label: 'Rust',
@@ -109,6 +184,7 @@ export const GAME_PROFILES: Record<GameId, GameProfile> = {
group: 'Team',
},
statFields: ['Players', 'uMod', 'Wipe'],
nav: RUST_NAV,
},
conan: {
@@ -130,6 +206,30 @@ export const GAME_PROFILES: Record<GameId, GameProfile> = {
},
special: ['Clans', 'Thralls', 'Avatars', 'Purge', 'PvP windows'],
statFields: ['Players', 'Clans', 'Purge'],
nav: [
{ label: '', items: [NAV_DASHBOARD] },
{
label: 'Server',
// Conan: no uMod/Oxide; has RCON console, maps, players, files
items: [NAV_SERVER, NAV_CONSOLE, NAV_PLAYERS, NAV_FILES],
},
{
label: 'Operations',
items: [
{ label: 'Wipe World', route: '/wipes', icon: 'trash-2', permission: 'wipes.view' },
NAV_MAPS,
NAV_SCHEDULES,
],
},
{
label: 'Monitoring',
items: [NAV_CHAT, NAV_ANALYTICS, NAV_ALERTS, NAV_NOTIFICATIONS],
},
{
label: 'Management',
items: [NAV_TEAM, NAV_STORE, NAV_MODULES, NAV_CHANGELOG, NAV_SETTINGS],
},
],
},
soulmask: {
@@ -151,6 +251,29 @@ export const GAME_PROFILES: Record<GameId, GameProfile> = {
},
special: ['Cluster', 'Tribes'],
statFields: ['Players', 'Tribe', 'Mask'],
nav: [
{ label: '', items: [NAV_DASHBOARD] },
{
label: 'Server',
// Soulmask: no uMod/Oxide; has RCON+GM console, players, files
items: [NAV_SERVER, NAV_CONSOLE, NAV_PLAYERS, NAV_FILES],
},
{
label: 'Operations',
items: [
{ label: 'World Reset', route: '/wipes', icon: 'trash-2', permission: 'wipes.view' },
NAV_SCHEDULES,
],
},
{
label: 'Monitoring',
items: [NAV_CHAT, NAV_ANALYTICS, NAV_ALERTS],
},
{
label: 'Management',
items: [NAV_TEAM, NAV_STORE, NAV_MODULES, NAV_CHANGELOG, NAV_SETTINGS],
},
],
},
dune: {
@@ -170,6 +293,34 @@ export const GAME_PROFILES: Record<GameId, GameProfile> = {
},
special: ['Sietches', 'Deep Desert', 'Bases', 'Landsraad'],
statFields: ['Players', 'Sietches', 'Control'],
nav: [
{ label: '', items: [NAV_DASHBOARD] },
{
label: 'Server',
// Dune: no RCON (uses RabbitMQ); label console "Broadcast"; no maps route; no plugins
items: [
NAV_SERVER,
{ label: 'Broadcast', route: '/console', icon: 'radio', permission: 'console.view' },
NAV_PLAYERS,
NAV_FILES,
],
},
{
label: 'Operations',
items: [
{ label: 'Deep Desert', route: '/wipes', icon: 'wind', permission: 'wipes.view' },
NAV_SCHEDULES,
],
},
{
label: 'Monitoring',
items: [NAV_ANALYTICS, NAV_ALERTS],
},
{
label: 'Management',
items: [NAV_TEAM, NAV_STORE, NAV_CHANGELOG, NAV_SETTINGS],
},
],
},
} as const

View File

@@ -25,6 +25,7 @@ import { useWipeStore } from '@/stores/wipe'
import { useApi } from '@/composables/useApi'
import { useWebSocket, type WebSocketMessage } from '@/composables/useWebSocket'
import { useGameProfile } from '@/config/gameProfiles'
import { useThemeGame } from '@/composables/useThemeGame'
import Panel from '@/components/ds/data/Panel.vue'
import StatCard from '@/components/ds/data/StatCard.vue'
import ConsoleLineDS from '@/components/ds/data/ConsoleLine.vue'
@@ -44,10 +45,14 @@ const server = useServerStore()
const wipeStore = useWipeStore()
const router = useRouter()
const api = useApi()
const { activeGame } = useThemeGame()
// Today every license is Rust. When the backend adds a `game` field to the
// license or server_config, pass it here: useGameProfile(server.config?.game ?? 'rust')
const profile = computed(() => useGameProfile('rust'))
// Profile follows the GameSwitcher selection. 'all' falls back to rust (neutral house skin).
// When the backend adds a `game` field on licenses, swap activeGame for server.config?.game.
const profile = computed(() => {
const game = activeGame.value === 'all' ? 'rust' : activeGame.value
return useGameProfile(game)
})
// ---------------------------------------------------------------------------
// Derived server state — all real, no fallbacks to fabricated values
@@ -254,7 +259,7 @@ function navServer() { router.push('/server') }
<EmptyState
icon="server"
title="No server connected"
description="Install the companion agent on your host machine to begin managing your server from Corrosion."
description="Install the Corrosion host agent on your host machine to begin managing your server from Corrosion."
>
<template #action>
<Button icon="server" @click="navServer">Set up server</Button>
@@ -298,7 +303,7 @@ function navServer() { router.push('/server') }
<div class="dash__kpis">
<StatCard
icon="users"
:label="profile.statFields[0] + ' online'"
:label="(profile.statFields[0] ?? 'Players') + ' online'"
:value="soloPlayers !== null ? String(soloPlayers) : '—'"
:unit="soloMaxPlayers !== null ? '/' + soloMaxPlayers : ''"
note="live via agent"
@@ -399,7 +404,7 @@ function navServer() { router.push('/server') }
<div class="dash__col dash__col--side">
<!-- Resources real stats from agent; null = '—' -->
<Panel title="Resources" subtitle="Companion agent telemetry">
<Panel title="Resources" subtitle="Host agent telemetry">
<div class="solo-meters">
<ResourceMeter
label="CPU"
@@ -413,15 +418,15 @@ function navServer() { router.push('/server') }
/>
</div>
<div v-if="soloCpu === null && soloRamMb === null" class="meters-note">
Resource metrics arrive via the companion agent heartbeat.
Resource metrics arrive via the host agent heartbeat.
<Button size="sm" variant="ghost" icon="server" class="meters-cta" @click="navServer">
Agent setup
</Button>
</div>
</Panel>
<!-- Next wipe real schedule from wipeStore -->
<Panel title="Next wipe">
<!-- Next wipe/reset title follows game terminology -->
<Panel :title="'Next ' + profile.terminology.reset.toLowerCase()">
<div v-if="nextWipe" class="solo-wipe">
<div>
<div class="solo-wipe__type">{{ nextWipeType }}</div>
@@ -433,8 +438,8 @@ function navServer() { router.push('/server') }
<EmptyState
v-else
icon="calendar"
title="No wipe scheduled"
description="Configure automatic wipes in the wipe manager."
:title="'No ' + profile.terminology.reset.toLowerCase() + ' scheduled'"
:description="'Configure automatic ' + profile.terminology.reset.toLowerCase() + 's in the wipe manager.'"
>
<template #action>
<Button size="sm" variant="outline" icon="calendar-clock" @click="navWipes">

View File

@@ -485,7 +485,7 @@ onMounted(() => {
</Panel>
<Alert tone="info">
The plugin will be registered in your plugin list immediately. Your companion agent must be connected
The plugin will be registered in your plugin list immediately. Your host agent must be connected
for the file to be delivered to the game server. If the agent is offline, re-upload once it reconnects.
</Alert>
</div>

View File

@@ -3,6 +3,8 @@ import { ref, computed, onMounted } from 'vue'
import { useServerStore } from '@/stores/server'
import { useAuthStore } from '@/stores/auth'
import { useToastStore } from '@/stores/toast'
import { useThemeGame } from '@/composables/useThemeGame'
import { useGameProfile } from '@/config/gameProfiles'
import type { DeploymentConfig, DeploymentStatus } from '@/types'
import { useWebSocket } from '@/composables/useWebSocket'
import Panel from '@/components/ds/data/Panel.vue'
@@ -11,6 +13,7 @@ import Badge from '@/components/ds/core/Badge.vue'
import StatusDot from '@/components/ds/core/StatusDot.vue'
import Icon from '@/components/ds/core/Icon.vue'
import Alert from '@/components/ds/feedback/Alert.vue'
import EmptyState from '@/components/ds/feedback/EmptyState.vue'
import Input from '@/components/ds/forms/Input.vue'
import Switch from '@/components/ds/forms/Switch.vue'
import Tabs from '@/components/ds/navigation/Tabs.vue'
@@ -18,6 +21,39 @@ import Tabs from '@/components/ds/navigation/Tabs.vue'
const server = useServerStore()
const auth = useAuthStore()
const toast = useToastStore()
const { activeGame } = useThemeGame()
// Profile follows the GameSwitcher. 'all' defaults to rust (neutral house skin).
const profile = computed(() => {
const game = activeGame.value === 'all' ? 'rust' : activeGame.value
return useGameProfile(game)
})
// Game-specific derived flags
const isRust = computed(() => profile.value.mods === 'umod')
const hasPluginSystem = computed(() => profile.value.mods === 'umod')
const isDockerManaged = computed(() => profile.value.managementModel === 'docker-compose')
// Management model human label for the identity badge
const managementModelLabel = computed(() => {
const m = profile.value.managementModel
const c = profile.value.console
if (m === 'docker-compose') {
return profile.value.clustering === 'battlegroup' ? 'Docker · BattleGroup' : 'Docker · Compose'
}
if (c === 'rcon+ingame') return 'Process · RCON + In-game'
if (c === 'rcon+gm') return 'Process · RCON + GM'
return 'Process · RCON'
})
// Clustering section label per game
const clusterLabel = computed(() => {
const cl = profile.value.clustering
if (cl === 'battlegroup') return 'BattleGroups & Sietches'
if (cl === 'main-client') return 'Cluster'
if (cl === 'character-transfer') return 'Clans & Character Transfer'
return ''
})
const editMode = ref(false)
const saving = ref(false)
@@ -64,22 +100,22 @@ const agentLastSeenLabel = computed(() => {
const licenseKey = computed(() => auth.license?.license_key || 'YOUR-LICENSE-KEY')
const linuxCommands = computed(() => `# Download the agent
curl -LO https://cdn.corrosionmgmt.com/companion/latest/corrosion-companion-linux-amd64
chmod +x corrosion-companion-linux-amd64
curl -LO https://cdn.corrosionmgmt.com/host-agent/latest/corrosion-host-agent-linux-amd64
chmod +x corrosion-host-agent-linux-amd64
# Start with your license key
export LICENSE_ID="${licenseKey.value}"
export NATS_URL="nats://nats.corrosionmgmt.com:4222"
./corrosion-companion-linux-amd64`)
./corrosion-host-agent-linux-amd64`)
const windowsCommands = computed(() => `# Requires PowerShell (not Command Prompt)
# Download the agent
Invoke-WebRequest -Uri "https://cdn.corrosionmgmt.com/companion/latest/corrosion-companion-windows-amd64.exe" -OutFile "corrosion-companion-windows-amd64.exe"
Invoke-WebRequest -Uri "https://cdn.corrosionmgmt.com/host-agent/latest/corrosion-host-agent-windows-amd64.exe" -OutFile "corrosion-host-agent-windows-amd64.exe"
# Start with your license key
$env:LICENSE_ID="${licenseKey.value}"
$env:NATS_URL="nats://nats.corrosionmgmt.com:4222"
.\\corrosion-companion-windows-amd64.exe`)
.\\corrosion-host-agent-windows-amd64.exe`)
async function copySetupCommands() {
try {
@@ -278,17 +314,18 @@ onMounted(async () => {
<template>
<div class="sv">
<!-- Page head -->
<!-- Page head game-aware identity -->
<div class="sv__head">
<div class="sv__head-id">
<div class="sv__head-chip">
<Icon name="server" :size="20" :stroke-width="2" />
</div>
<div>
<div class="t-eyebrow">Server management</div>
<div class="t-eyebrow">{{ profile.label }} · Server management</div>
<h1 class="sv__title">Server</h1>
</div>
</div>
<Badge tone="neutral" :mono="true" class="sv__model-badge">{{ managementModelLabel }}</Badge>
</div>
<!-- Connection -->
@@ -350,8 +387,8 @@ onMounted(async () => {
</div>
</Panel>
<!-- Companion agent -->
<Panel title="Companion agent" subtitle="Bare-metal server management binary">
<!-- Host agent -->
<Panel title="Host agent" subtitle="Bare-metal server management binary">
<template #actions>
<Badge :tone="isAgentConnected ? 'online' : 'offline'" :dot="true" :pulse="isAgentConnected">
{{ isAgentConnected ? 'Active' : 'Inactive' }}
@@ -380,20 +417,20 @@ onMounted(async () => {
<!-- Download -->
<div class="sv__section-head">
<Icon name="download" :size="14" />
<span>Download companion agent</span>
<span>Download host agent</span>
</div>
<div class="sv__downloads sv__mb">
<a
href="https://cdn.corrosionmgmt.com/companion/latest/corrosion-companion-linux-amd64"
download="corrosion-companion-linux-amd64"
href="https://cdn.corrosionmgmt.com/host-agent/latest/corrosion-host-agent-linux-amd64"
download="corrosion-host-agent-linux-amd64"
class="sv__dl-link"
>
<Icon name="download" :size="15" />
Linux (amd64)
</a>
<a
href="https://cdn.corrosionmgmt.com/companion/latest/corrosion-companion-windows-amd64.exe"
download="corrosion-companion-windows-amd64.exe"
href="https://cdn.corrosionmgmt.com/host-agent/latest/corrosion-host-agent-windows-amd64.exe"
download="corrosion-host-agent-windows-amd64.exe"
class="sv__dl-link"
>
<Icon name="download" :size="15" />
@@ -424,28 +461,28 @@ onMounted(async () => {
<!-- Linux commands -->
<div v-if="setupTab === 'linux'" class="sv__codeblock">
<p class="sv__cmt"># Download the agent</p>
<p>curl -LO https://cdn.corrosionmgmt.com/companion/latest/corrosion-companion-linux-amd64</p>
<p>chmod +x corrosion-companion-linux-amd64</p>
<p>curl -LO https://cdn.corrosionmgmt.com/host-agent/latest/corrosion-host-agent-linux-amd64</p>
<p>chmod +x corrosion-host-agent-linux-amd64</p>
<p class="sv__cmt sv__mt">&#x23; Start with your license key</p>
<p>export LICENSE_ID=<span class="sv__accent">"{{ licenseKey }}"</span></p>
<p>export NATS_URL=<span class="sv__accent">"nats://nats.corrosionmgmt.com:4222"</span></p>
<p>./corrosion-companion-linux-amd64</p>
<p>./corrosion-host-agent-linux-amd64</p>
</div>
<!-- Windows commands -->
<div v-if="setupTab === 'windows'" class="sv__codeblock">
<p class="sv__cmt"># Requires PowerShell (not Command Prompt)</p>
<p class="sv__cmt"># Download the agent</p>
<p>Invoke-WebRequest -Uri <span class="sv__accent">"https://cdn.corrosionmgmt.com/companion/latest/corrosion-companion-windows-amd64.exe"</span> -OutFile <span class="sv__accent">"corrosion-companion-windows-amd64.exe"</span></p>
<p>Invoke-WebRequest -Uri <span class="sv__accent">"https://cdn.corrosionmgmt.com/host-agent/latest/corrosion-host-agent-windows-amd64.exe"</span> -OutFile <span class="sv__accent">"corrosion-host-agent-windows-amd64.exe"</span></p>
<p class="sv__cmt sv__mt">&#x23; Start with your license key</p>
<p>$env:LICENSE_ID=<span class="sv__accent">"{{ licenseKey }}"</span></p>
<p>$env:NATS_URL=<span class="sv__accent">"nats://nats.corrosionmgmt.com:4222"</span></p>
<p>.\corrosion-companion-windows-amd64.exe</p>
<p>.\corrosion-host-agent-windows-amd64.exe</p>
</div>
</Panel>
<!-- Deploy Rust Server -->
<Panel title="Deploy Rust server" subtitle="One-click: SteamCMD, download, configure, start">
<!-- Deploy Server Rust only (SteamCMD path). Other games use docker-compose or external tooling. -->
<Panel v-if="isRust" title="Deploy Rust server" subtitle="One-click: SteamCMD, download, configure, start">
<template #title-append>
<Icon name="rocket" :size="15" />
</template>
@@ -560,8 +597,28 @@ onMounted(async () => {
</div>
</Panel>
<!-- Install Oxide / uMod -->
<Panel title="Install Oxide / uMod" subtitle="Required for all plugins including CorrosionCompanion">
<!-- Non-Rust: Docker-managed server note -->
<Panel
v-if="isDockerManaged"
:title="profile.label + ' server deployment'"
subtitle="Managed via Docker Compose"
>
<template #title-append>
<Icon name="box" :size="15" />
</template>
<EmptyState
icon="box"
title="Docker-managed deployment"
:description="profile.label + ' servers are managed via Docker Compose. Connect the host agent on your Docker host to enable lifecycle management.'"
>
<template #action>
<Badge tone="info">Docker · Compose</Badge>
</template>
</EmptyState>
</Panel>
<!-- Install Oxide / uMod — Rust only -->
<Panel v-if="hasPluginSystem" title="Install Oxide / uMod" subtitle="Required for all plugins including CorrosionCompanion">
<template #title-append>
<Icon name="puzzle" :size="15" />
</template>
@@ -611,6 +668,79 @@ onMounted(async () => {
</div>
</Panel>
<!-- Workshop Mods info — Conan / Soulmask (Steam Workshop, no install step needed) -->
<Panel
v-else-if="profile.mods === 'workshop'"
:title="(profile.terminology.mods ?? 'Workshop Mods')"
:subtitle="profile.label + ' uses Steam Workshop — no manual install step required'"
>
<template #title-append>
<Icon name="layers" :size="15" />
</template>
<EmptyState
icon="layers"
:title="profile.label + ' mod management'"
:description="profile.label + ' loads mods directly from Steam Workshop. Manage your mod list in server config — no Corrosion install step needed.'"
/>
</Panel>
<!-- Conan Exiles special concepts (Clans / Thralls / Purge) -->
<Panel
v-if="profile.accent === 'conan'"
title="Conan Exiles concepts"
subtitle="Key admin mechanics for Conan Exiles servers"
>
<div class="sv__concept-grid">
<div class="sv__concept">
<Icon name="users" :size="16" />
<div>
<div class="sv__concept-label">Clans</div>
<div class="sv__concept-desc">Player factions. Clan management via in-game admin panel or RCON.</div>
</div>
</div>
<div class="sv__concept">
<Icon name="zap" :size="16" />
<div>
<div class="sv__concept-label">Thralls &amp; Avatars</div>
<div class="sv__concept-desc">Server-controlled NPCs and deity summons. Purge cycle managed via server settings.</div>
</div>
</div>
<div class="sv__concept">
<Icon name="shield" :size="16" />
<div>
<div class="sv__concept-label">Purge</div>
<div class="sv__concept-desc">NPC raid events targeting player bases. Enable / tune via server config.</div>
</div>
</div>
</div>
</Panel>
<!-- Soulmask clustering section -->
<Panel
v-if="profile.clustering === 'main-client'"
:title="clusterLabel"
subtitle="Main-client cluster topology for Soulmask"
>
<EmptyState
icon="network"
title="Cluster management coming soon"
:description="'Connect a ' + profile.label + ' host to manage the main-client cluster from this panel. Cluster configuration requires the host agent.'"
/>
</Panel>
<!-- Dune BattleGroup / Sietches section -->
<Panel
v-if="profile.clustering === 'battlegroup'"
title="BattleGroups &amp; Sietches"
subtitle="Dune: Awakening server cluster topology"
>
<EmptyState
icon="map"
title="Sietch management requires a connected Dune host"
description="Connect the host agent on your Dune: Awakening Docker host to manage BattleGroups and Sietches from this panel."
/>
</Panel>
<!-- Configuration -->
<Panel title="Configuration">
<template #actions>
@@ -708,8 +838,13 @@ onMounted(async () => {
</div>
<div class="sv__toggle-row">
<div class="sv__toggle-body">
<div class="sv__toggle-label">Auto-update on force wipe</div>
<div class="sv__toggle-sub">Update when Facepunch pushes</div>
<div class="sv__toggle-label">
<!-- Rust: "force wipe" is a Facepunch concept. Others: plain "auto-update" -->
{{ isRust ? 'Auto-update on force wipe' : 'Auto-update on patch' }}
</div>
<div class="sv__toggle-sub">
{{ isRust ? 'Update when Facepunch pushes' : 'Update when the developer pushes a patch' }}
</div>
</div>
<Switch
:model-value="server.config?.auto_update_on_force_wipe ?? false"
@@ -717,7 +852,8 @@ onMounted(async () => {
@update:model-value="toggleAutomation('auto_update_on_force_wipe')"
/>
</div>
<div class="sv__toggle-row">
<!-- Rust-only: force wipe eligibility is a Facepunch concept -->
<div v-if="isRust" class="sv__toggle-row">
<div class="sv__toggle-body">
<div class="sv__toggle-label">Force wipe eligible</div>
<div class="sv__toggle-sub">Server participates in force wipes</div>
@@ -848,4 +984,19 @@ onMounted(async () => {
.sv__toggle-row:first-child { padding-top: 0; }
.sv__toggle-label { font-size: var(--text-sm); font-weight: 500; color: var(--text-primary); }
.sv__toggle-sub { font-size: var(--text-xs); color: var(--text-tertiary); margin-top: 2px; }
/* Management model badge in page head */
.sv__model-badge { align-self: center; }
/* Game concept cards (Conan Exiles special features) */
.sv__concept-grid { display: flex; flex-direction: column; gap: 14px; }
.sv__concept {
display: flex; align-items: flex-start; gap: 12px;
padding: 12px 14px;
background: var(--surface-raised); border-radius: var(--radius-md);
box-shadow: var(--ring-default);
color: var(--accent);
}
.sv__concept-label { font-size: var(--text-sm); font-weight: 600; color: var(--text-primary); margin-bottom: 2px; }
.sv__concept-desc { font-size: var(--text-xs); color: var(--text-tertiary); line-height: 1.5; }
</style>

View File

@@ -35,7 +35,7 @@ function syncPorts() {
}
const connectionTypes = [
{ value: 'bare_metal', label: 'Bare metal / VPS', desc: 'Direct connection via Companion Agent' },
{ value: 'bare_metal', label: 'Bare metal / VPS', desc: 'Direct connection via Corrosion host agent' },
{ value: 'amp', label: 'AMP (CubeCoders)', desc: 'Connect through AMP panel API' },
{ value: 'pterodactyl', label: 'Pterodactyl', desc: 'Connect through Pterodactyl panel API' },
]
@@ -183,7 +183,7 @@ async function completeSetup() {
</form>
</div>
<!-- Step 2: Companion agent install -->
<!-- Step 2: Corrosion host agent install -->
<div v-if="step === 2" class="setup-card">
<div class="setup-card__head setup-card__head--center">
<div class="setup-icon">
@@ -191,12 +191,12 @@ async function completeSetup() {
<path d="M5 12.55a11 11 0 0 1 14.08 0M1.42 9a16 16 0 0 1 21.16 0M8.53 16.11a6 6 0 0 1 6.95 0M12 20h.01" />
</svg>
</div>
<h1 class="setup-card__title">Install the Companion Agent</h1>
<h1 class="setup-card__title">Install the Corrosion host agent</h1>
<p class="setup-card__sub">The agent runs on your server and connects to Corrosion — no inbound ports required.</p>
</div>
<div class="setup-code">
<p class="setup-code__comment"># Download and install the Companion Agent</p>
<p class="setup-code__comment"># Download and install the Corrosion host agent</p>
<p class="setup-code__cmd">curl -sSL https://get.corrosionmgmt.com | sh</p>
<p class="setup-code__comment setup-code__comment--mt"># Start the agent with your license key</p>
<p class="setup-code__cmd">corrosion-agent start --key {{ auth.license?.license_key ?? 'YOUR-LICENSE-KEY' }}</p>

View File

@@ -185,7 +185,7 @@ const mockActiveGame = activeGame
<span class="g"><Icon name="box" :size="13" /></span>
<span class="nm">
Main · 2x Vanilla
<small>asgard-01 · rust</small>
<small>rust-host · rust</small>
</span>
<span class="st"><b />online</span>
</div>
@@ -193,7 +193,7 @@ const mockActiveGame = activeGame
<span class="g"><Icon name="sun" :size="13" /></span>
<span class="nm">
Arrakis · Hardcore
<small>asgard-01 · dune</small>
<small>dune-host · dune</small>
</span>
<span class="st"><b />online</span>
</div>
@@ -201,7 +201,7 @@ const mockActiveGame = activeGame
<span class="g"><Icon name="swords" :size="13" /></span>
<span class="nm">
Exiled Lands · PvP-C
<small>asgard-02 · conan</small>
<small>conan-host · conan</small>
</span>
<span class="st"><b />online</span>
</div>

View File

@@ -2,12 +2,18 @@ import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import tailwindcss from '@tailwindcss/vite'
import { fileURLToPath, URL } from 'node:url'
import { readFileSync } from 'node:fs'
const pkg = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf-8'))
export default defineConfig({
plugins: [
vue(),
tailwindcss(),
],
define: {
__APP_VERSION__: JSON.stringify(pkg.version),
},
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),