Files
corrosion-admin-panel/frontend/src/views/admin/ServerView.vue
Vantz Stockwell e0f9438dfa
All checks were successful
Test Asgard Runner / test (push) Successful in 3s
feat: Companion agent download in ServerView + Gitea CI pipeline fix
Frontend:
- Add Companion Agent card to ServerView (status, download links, setup instructions)
- Shows agent connection status, last heartbeat, license key for copy
- Download buttons for Linux/Windows amd64 from Gitea releases

CI/CD:
- Fix build-companion.yml: replace actions/github-script with Gitea API curl
- Inject version from git tag via ldflags (-X main.version)
- Add VERSION variable to Makefile with ldflags injection
- Change main.go version from const to var for ldflags compatibility

Deployment:
- Add systemd service file (deployment/corrosion-companion.service)
- Add .gitignore for bin/ (binaries should come from CI, not repo)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 13:47:09 -05:00

442 lines
18 KiB
Vue

<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useServerStore } from '@/stores/server'
import { useAuthStore } from '@/stores/auth'
import {
Server,
Wifi,
WifiOff,
Play,
Square,
RotateCcw,
Save,
Loader2,
Download,
Terminal,
Monitor,
} from 'lucide-vue-next'
const server = useServerStore()
const auth = useAuthStore()
const editMode = ref(false)
const saving = ref(false)
const actionLoading = ref<string | null>(null)
const copied = ref(false)
const isAgentConnected = computed(() =>
server.connection?.connection_type === 'bare_metal' &&
server.connection?.connection_status === 'connected'
)
const agentLastSeen = computed(() => {
const ts = server.connection?.companion_last_seen
if (!ts) return null
return new Date(ts)
})
const agentLastSeenLabel = computed(() => {
const d = agentLastSeen.value
if (!d) return 'Never'
const diff = Math.floor((Date.now() - d.getTime()) / 1000)
if (diff < 60) return `${diff}s ago`
if (diff < 3600) return `${Math.floor(diff / 60)}m ago`
if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`
return d.toLocaleDateString()
})
const licenseKey = computed(() => auth.license?.license_key || 'YOUR-LICENSE-KEY')
const linuxCommands = computed(() => `# Download the agent
curl -LO https://git.corrosionmgmt.com/vantzs/corrosion-admin-panel/releases/latest/download/corrosion-companion-linux-amd64
chmod +x corrosion-companion-linux-amd64
# Start with your license key
export LICENSE_ID="${licenseKey.value}"
export NATS_URL="nats://nats.corrosionmgmt.com:4222"
export NATS_TOKEN="<your-nats-token>"
export GAME_SERVER_PATH="/path/to/RustDedicated"
./corrosion-companion-linux-amd64`)
async function copyCommands() {
try {
await navigator.clipboard.writeText(linuxCommands.value)
copied.value = true
setTimeout(() => { copied.value = false }, 2000)
} catch {
// Clipboard API unavailable
}
}
const form = ref({
server_name: '',
max_players: 0,
world_size: 0,
current_seed: 0,
})
function loadFormFromConfig() {
if (server.config) {
form.value = {
server_name: server.config.server_name || '',
max_players: server.config.max_players ?? 100,
world_size: server.config.world_size ?? 4000,
current_seed: server.config.current_seed ?? 0,
}
}
}
async function saveConfig() {
saving.value = true
try {
await server.updateConfig(form.value)
editMode.value = false
} catch {
// Handle error
} finally {
saving.value = false
}
}
async function serverAction(action: 'start' | 'stop' | 'restart') {
actionLoading.value = action
try {
if (action === 'start') await server.startServer()
else if (action === 'stop') await server.stopServer()
else await server.restartServer()
await server.fetchServer()
} catch {
// Handle error
} finally {
actionLoading.value = null
}
}
onMounted(async () => {
await server.fetchServer()
loadFormFromConfig()
})
</script>
<template>
<div class="p-6 space-y-6">
<!-- Header -->
<div class="flex items-center gap-3">
<Server class="w-5 h-5 text-oxide-500" />
<h1 class="text-2xl font-bold text-neutral-100">Server Management</h1>
</div>
<!-- Connection Status Card -->
<div class="bg-neutral-900 border border-neutral-800 rounded-lg p-5">
<h2 class="text-sm font-medium text-neutral-400 uppercase tracking-wider mb-4">Connection</h2>
<div class="grid grid-cols-2 lg:grid-cols-4 gap-6">
<div>
<p class="text-xs text-neutral-500 mb-1">Status</p>
<div class="flex items-center gap-2">
<component
:is="server.connection?.connection_status === 'connected' ? Wifi : WifiOff"
class="w-4 h-4"
:class="server.connection?.connection_status === 'connected' ? 'text-green-400' : 'text-red-400'"
/>
<span class="text-sm font-medium text-neutral-100 capitalize">
{{ server.connection?.connection_status || 'Unknown' }}
</span>
</div>
</div>
<div>
<p class="text-xs text-neutral-500 mb-1">Connection Type</p>
<p class="text-sm font-medium text-neutral-100 uppercase">
{{ server.connection?.connection_type || '\u2014' }}
</p>
</div>
<div>
<p class="text-xs text-neutral-500 mb-1">Server IP</p>
<p class="text-sm font-mono text-neutral-300">
{{ server.connection?.server_ip || '\u2014' }}:{{ server.connection?.server_port || '' }}
</p>
</div>
<div>
<p class="text-xs text-neutral-500 mb-1">Game Port</p>
<p class="text-sm font-mono text-neutral-300">
{{ server.connection?.game_port || '\u2014' }}
</p>
</div>
</div>
</div>
<!-- Server Controls -->
<div class="bg-neutral-900 border border-neutral-800 rounded-lg p-5">
<h2 class="text-sm font-medium text-neutral-400 uppercase tracking-wider mb-4">Controls</h2>
<div class="flex flex-wrap gap-3">
<button
@click="serverAction('start')"
:disabled="server.connection?.connection_status === 'connected' || actionLoading !== null"
class="flex items-center gap-2 px-4 py-2.5 bg-green-600/20 hover:bg-green-600/30 disabled:opacity-30 disabled:cursor-not-allowed text-green-400 border border-green-600/30 rounded-lg text-sm font-medium transition-colors"
>
<Loader2 v-if="actionLoading === 'start'" class="w-4 h-4 animate-spin" />
<Play v-else class="w-4 h-4" />
Start Server
</button>
<button
@click="serverAction('stop')"
:disabled="server.connection?.connection_status !== 'connected' || actionLoading !== null"
class="flex items-center gap-2 px-4 py-2.5 bg-red-600/20 hover:bg-red-600/30 disabled:opacity-30 disabled:cursor-not-allowed text-red-400 border border-red-600/30 rounded-lg text-sm font-medium transition-colors"
>
<Loader2 v-if="actionLoading === 'stop'" class="w-4 h-4 animate-spin" />
<Square v-else class="w-4 h-4" />
Stop Server
</button>
<button
@click="serverAction('restart')"
:disabled="server.connection?.connection_status !== 'connected' || actionLoading !== null"
class="flex items-center gap-2 px-4 py-2.5 bg-oxide-600/20 hover:bg-oxide-600/30 disabled:opacity-30 disabled:cursor-not-allowed text-oxide-400 border border-oxide-600/30 rounded-lg text-sm font-medium transition-colors"
>
<Loader2 v-if="actionLoading === 'restart'" class="w-4 h-4 animate-spin" />
<RotateCcw v-else class="w-4 h-4" />
Restart Server
</button>
</div>
</div>
<!-- Companion Agent -->
<div class="bg-neutral-900 border border-neutral-800 rounded-lg p-5">
<div class="flex items-center gap-2 mb-5">
<Monitor class="w-4 h-4 text-oxide-400" />
<h2 class="text-sm font-medium text-neutral-400 uppercase tracking-wider">Companion Agent</h2>
</div>
<!-- Agent Status -->
<div class="grid grid-cols-2 lg:grid-cols-3 gap-6 mb-6">
<div>
<p class="text-xs text-neutral-500 mb-1">Agent Status</p>
<div class="flex items-center gap-2">
<span
class="w-2 h-2 rounded-full"
:class="isAgentConnected ? 'bg-green-400' : 'bg-neutral-600'"
/>
<span
class="text-sm font-medium"
:class="isAgentConnected ? 'text-green-400' : 'text-neutral-400'"
>
{{ isAgentConnected ? 'Active' : 'Inactive' }}
</span>
</div>
</div>
<div>
<p class="text-xs text-neutral-500 mb-1">Connection Type</p>
<p class="text-sm font-medium text-neutral-100 uppercase">
{{ server.connection?.connection_type || '\u2014' }}
</p>
</div>
<div>
<p class="text-xs text-neutral-500 mb-1">Last Heartbeat</p>
<p class="text-sm font-medium" :class="agentLastSeen ? 'text-neutral-200' : 'text-neutral-500'">
{{ agentLastSeenLabel }}
</p>
</div>
</div>
<!-- Download Section -->
<div class="mb-6">
<div class="flex items-center gap-2 mb-3">
<Download class="w-3.5 h-3.5 text-neutral-500" />
<p class="text-xs font-medium text-neutral-400 uppercase tracking-wider">Download Companion Agent</p>
</div>
<div class="flex flex-wrap gap-3">
<a
href="https://git.corrosionmgmt.com/vantzs/corrosion-admin-panel/releases/latest/download/corrosion-companion-linux-amd64"
download="corrosion-companion-linux-amd64"
class="flex items-center gap-2 px-4 py-2.5 bg-neutral-800 hover:bg-neutral-700 text-neutral-200 border border-neutral-700 hover:border-neutral-600 rounded-lg text-sm font-medium transition-colors"
>
<Download class="w-4 h-4 text-oxide-400" />
Linux (amd64)
</a>
<a
href="https://git.corrosionmgmt.com/vantzs/corrosion-admin-panel/releases/latest/download/corrosion-companion-windows-amd64.exe"
download="corrosion-companion-windows-amd64.exe"
class="flex items-center gap-2 px-4 py-2.5 bg-neutral-800 hover:bg-neutral-700 text-neutral-200 border border-neutral-700 hover:border-neutral-600 rounded-lg text-sm font-medium transition-colors"
>
<Download class="w-4 h-4 text-oxide-400" />
Windows (amd64)
</a>
</div>
</div>
<!-- Quick Setup Section -->
<div>
<div class="flex items-center justify-between mb-3">
<div class="flex items-center gap-2">
<Terminal class="w-3.5 h-3.5 text-neutral-500" />
<p class="text-xs font-medium text-neutral-400 uppercase tracking-wider">Quick Setup (Linux)</p>
</div>
<button
@click="copyCommands"
class="flex items-center gap-1.5 px-3 py-1 text-xs font-medium rounded-md transition-colors"
:class="copied
? 'bg-green-600/20 text-green-400 border border-green-600/30'
: 'bg-neutral-800 hover:bg-neutral-700 text-neutral-400 hover:text-neutral-200 border border-neutral-700'"
>
{{ copied ? 'Copied!' : 'Copy' }}
</button>
</div>
<div class="bg-black/50 border border-neutral-800 rounded-lg p-4 font-mono text-sm text-neutral-300 overflow-x-auto">
<p class="text-neutral-500"># Download the agent</p>
<p>curl -LO https://git.corrosionmgmt.com/vantzs/corrosion-admin-panel/releases/latest/download/corrosion-companion-linux-amd64</p>
<p>chmod +x corrosion-companion-linux-amd64</p>
<p class="mt-3 text-neutral-500"># Start with your license key</p>
<p>export LICENSE_ID=<span class="text-oxide-400">"{{ licenseKey }}"</span></p>
<p>export NATS_URL=<span class="text-oxide-400">"nats://nats.corrosionmgmt.com:4222"</span></p>
<p>export NATS_TOKEN=<span class="text-neutral-500">"&lt;your-nats-token&gt;"</span></p>
<p>export GAME_SERVER_PATH=<span class="text-neutral-500">"/path/to/RustDedicated"</span></p>
<p>./corrosion-companion-linux-amd64</p>
</div>
</div>
</div>
<!-- Configuration -->
<div class="bg-neutral-900 border border-neutral-800 rounded-lg p-5">
<div class="flex items-center justify-between mb-4">
<h2 class="text-sm font-medium text-neutral-400 uppercase tracking-wider">Configuration</h2>
<button
v-if="!editMode"
@click="editMode = true; loadFormFromConfig()"
class="text-sm text-oxide-400 hover:text-oxide-300 transition-colors"
>
Edit
</button>
</div>
<!-- Read mode -->
<div v-if="!editMode" class="grid grid-cols-2 lg:grid-cols-4 gap-6">
<div>
<p class="text-xs text-neutral-500 mb-1">Server Name</p>
<p class="text-sm text-neutral-200">{{ server.config?.server_name || 'Not configured' }}</p>
</div>
<div>
<p class="text-xs text-neutral-500 mb-1">Max Players</p>
<p class="text-sm text-neutral-200">{{ server.config?.max_players ?? '\u2014' }}</p>
</div>
<div>
<p class="text-xs text-neutral-500 mb-1">World Size</p>
<p class="text-sm text-neutral-200">{{ server.config?.world_size ?? '\u2014' }}</p>
</div>
<div>
<p class="text-xs text-neutral-500 mb-1">Current Seed</p>
<p class="text-sm text-neutral-200">{{ server.config?.current_seed ?? '\u2014' }}</p>
</div>
</div>
<!-- Edit mode -->
<form v-else @submit.prevent="saveConfig" class="space-y-4">
<div class="grid grid-cols-2 gap-4">
<div class="col-span-2">
<label class="block text-xs text-neutral-500 mb-1">Server Name</label>
<input
v-model="form.server_name"
type="text"
class="w-full px-3 py-2 bg-neutral-800 border border-neutral-700 rounded-lg text-sm text-neutral-100 focus:outline-none focus:ring-2 focus:ring-oxide-500/50 focus:border-oxide-500 transition-colors"
/>
</div>
<div>
<label class="block text-xs text-neutral-500 mb-1">Max Players</label>
<input
v-model.number="form.max_players"
type="number"
min="1"
max="500"
class="w-full px-3 py-2 bg-neutral-800 border border-neutral-700 rounded-lg text-sm text-neutral-100 focus:outline-none focus:ring-2 focus:ring-oxide-500/50 focus:border-oxide-500 transition-colors"
/>
</div>
<div>
<label class="block text-xs text-neutral-500 mb-1">World Size</label>
<input
v-model.number="form.world_size"
type="number"
min="1000"
max="8000"
class="w-full px-3 py-2 bg-neutral-800 border border-neutral-700 rounded-lg text-sm text-neutral-100 focus:outline-none focus:ring-2 focus:ring-oxide-500/50 focus:border-oxide-500 transition-colors"
/>
</div>
<div>
<label class="block text-xs text-neutral-500 mb-1">Current Seed</label>
<input
v-model.number="form.current_seed"
type="number"
class="w-full px-3 py-2 bg-neutral-800 border border-neutral-700 rounded-lg text-sm text-neutral-100 focus:outline-none focus:ring-2 focus:ring-oxide-500/50 focus:border-oxide-500 transition-colors"
/>
</div>
</div>
<div class="flex items-center gap-3 pt-2">
<button
type="submit"
:disabled="saving"
class="flex items-center gap-2 px-4 py-2 bg-oxide-600 hover:bg-oxide-700 disabled:opacity-50 text-white text-sm font-medium rounded-lg transition-colors"
>
<Save class="w-4 h-4" />
{{ saving ? 'Saving...' : 'Save Changes' }}
</button>
<button
type="button"
@click="editMode = false"
class="px-4 py-2 text-sm text-neutral-400 hover:text-neutral-200 transition-colors"
>
Cancel
</button>
</div>
</form>
</div>
<!-- Advanced Settings -->
<div class="bg-neutral-900 border border-neutral-800 rounded-lg p-5">
<h2 class="text-sm font-medium text-neutral-400 uppercase tracking-wider mb-4">Automation</h2>
<div class="grid grid-cols-2 lg:grid-cols-3 gap-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-neutral-200">Auto-Restart</p>
<p class="text-xs text-neutral-500">Restart on crash detection</p>
</div>
<div
class="w-9 h-5 rounded-full transition-colors cursor-pointer"
:class="server.config?.crash_recovery_enabled ? 'bg-oxide-500' : 'bg-neutral-700'"
>
<div
class="w-4 h-4 bg-white rounded-full shadow transition-transform mt-0.5"
:class="server.config?.crash_recovery_enabled ? 'translate-x-4.5' : 'translate-x-0.5'"
/>
</div>
</div>
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-neutral-200">Auto-Update on Force Wipe</p>
<p class="text-xs text-neutral-500">Update when Facepunch pushes</p>
</div>
<div
class="w-9 h-5 rounded-full transition-colors cursor-pointer"
:class="server.config?.auto_update_on_force_wipe ? 'bg-oxide-500' : 'bg-neutral-700'"
>
<div
class="w-4 h-4 bg-white rounded-full shadow transition-transform mt-0.5"
:class="server.config?.auto_update_on_force_wipe ? 'translate-x-4.5' : 'translate-x-0.5'"
/>
</div>
</div>
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-neutral-200">Force Wipe Eligible</p>
<p class="text-xs text-neutral-500">Server participates in force wipes</p>
</div>
<div
class="w-9 h-5 rounded-full transition-colors cursor-pointer"
:class="server.config?.force_wipe_eligible ? 'bg-oxide-500' : 'bg-neutral-700'"
>
<div
class="w-4 h-4 bg-white rounded-full shadow transition-transform mt-0.5"
:class="server.config?.force_wipe_eligible ? 'translate-x-4.5' : 'translate-x-0.5'"
/>
</div>
</div>
</div>
</div>
</div>
</template>