feat: Implement server endpoints, store, and live dashboard

Backend: Server connection/config/admins DB queries, server API routes
with auth-gated endpoints (overview, config CRUD, admin management).
Frontend: Server store wired to API, dashboard fetches server data on
mount with live status indicators, uptime formatting, and server
config display. Logout now redirects to /login.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Vantz Stockwell
2026-02-14 21:51:49 -05:00
parent 5668675b6a
commit a53cb4d8a5
5 changed files with 387 additions and 85 deletions

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { RouterView, RouterLink, useRoute } from 'vue-router'
import { RouterView, RouterLink, useRoute, useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import { useServerStore } from '@/stores/server'
import {
@@ -21,6 +21,7 @@ import {
} from 'lucide-vue-next'
const route = useRoute()
const router = useRouter()
const auth = useAuthStore()
const server = useServerStore()
@@ -48,6 +49,7 @@ function isActive(path: string): boolean {
function handleLogout() {
auth.logout()
router.push('/login')
}
</script>

View File

@@ -1,6 +1,7 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import type { ServerConnection, ServerConfig, ServerStats } from '@/types'
import { useApi } from '@/composables/useApi'
export const useServerStore = defineStore('server', () => {
const connection = ref<ServerConnection | null>(null)
@@ -8,28 +9,45 @@ export const useServerStore = defineStore('server', () => {
const stats = ref<ServerStats | null>(null)
const isLoading = ref(false)
async function fetchServerStatus() {
// TODO: Fetch from API
const api = useApi()
async function fetchServer() {
isLoading.value = true
try {
const data = await api.get<{ connection: ServerConnection | null; config: ServerConfig | null }>('/servers')
connection.value = data.connection
config.value = data.config
} catch (e) {
console.error('Failed to fetch server:', e)
} finally {
isLoading.value = false
}
}
async function fetchServerConfig() {
// TODO: Fetch from API
}
async function startServer() {
// TODO: POST /api/servers/:id/start
}
async function stopServer() {
// TODO: POST /api/servers/:id/stop
}
async function restartServer() {
// TODO: POST /api/servers/:id/restart
async function updateConfig(updates: Partial<ServerConfig>) {
try {
await api.put('/servers/config', updates)
await fetchServer()
} catch (e) {
console.error('Failed to update config:', e)
throw e
}
}
async function sendCommand(command: string) {
// TODO: POST /api/servers/:id/command
return api.post('/servers/command', { command })
}
async function startServer() {
return api.post('/servers/start')
}
async function stopServer() {
return api.post('/servers/stop')
}
async function restartServer() {
return api.post('/servers/restart')
}
function updateStats(newStats: ServerStats) {
@@ -41,12 +59,12 @@ export const useServerStore = defineStore('server', () => {
config,
stats,
isLoading,
fetchServerStatus,
fetchServerConfig,
fetchServer,
updateConfig,
sendCommand,
startServer,
stopServer,
restartServer,
sendCommand,
updateStats,
}
})

View File

@@ -1,7 +1,38 @@
<script setup lang="ts">
import { onMounted } from 'vue'
import { useAuthStore } from '@/stores/auth'
import { useServerStore } from '@/stores/server'
const auth = useAuthStore()
const server = useServerStore()
onMounted(() => {
server.fetchServer()
})
function statusColor(status: string | undefined): string {
switch (status) {
case 'connected': return 'bg-green-500'
case 'degraded': return 'bg-yellow-500'
default: return 'bg-red-500'
}
}
function statusLabel(status: string | undefined): string {
switch (status) {
case 'connected': return 'Online'
case 'degraded': return 'Degraded'
default: return 'Offline'
}
}
function formatUptime(seconds: number | undefined): string {
if (!seconds) return '\u2014'
const h = Math.floor(seconds / 3600)
const m = Math.floor((seconds % 3600) / 60)
if (h > 0) return `${h}h ${m}m`
return `${m}m`
}
</script>
<template>
@@ -20,15 +51,17 @@ const auth = useAuthStore()
<div class="bg-neutral-900 border border-neutral-800 rounded-lg p-5">
<p class="text-sm text-neutral-400 mb-2">Server Status</p>
<div class="flex items-center gap-2">
<span class="h-2.5 w-2.5 rounded-full bg-red-500"></span>
<span class="text-2xl font-bold text-neutral-100">Offline</span>
<span class="h-2.5 w-2.5 rounded-full" :class="statusColor(server.connection?.connection_status)"></span>
<span class="text-2xl font-bold text-neutral-100">{{ statusLabel(server.connection?.connection_status) }}</span>
</div>
</div>
<!-- Players Online -->
<div class="bg-neutral-900 border border-neutral-800 rounded-lg p-5">
<p class="text-sm text-neutral-400 mb-2">Players Online</p>
<p class="text-2xl font-bold text-neutral-100">0/0</p>
<p class="text-2xl font-bold text-neutral-100">
{{ server.stats?.player_count ?? 0 }}/{{ server.stats?.max_players ?? server.config?.max_players ?? 0 }}
</p>
</div>
<!-- Next Wipe -->
@@ -40,7 +73,7 @@ const auth = useAuthStore()
<!-- Uptime -->
<div class="bg-neutral-900 border border-neutral-800 rounded-lg p-5">
<p class="text-sm text-neutral-400 mb-2">Uptime</p>
<p class="text-2xl font-bold text-neutral-100">&mdash;</p>
<p class="text-2xl font-bold text-neutral-100">{{ formatUptime(server.stats?.uptime_seconds) }}</p>
</div>
</div>
@@ -49,24 +82,46 @@ const auth = useAuthStore()
<h2 class="text-lg font-semibold text-neutral-200 mb-4">Quick Actions</h2>
<div class="flex flex-wrap gap-3">
<button
disabled
class="px-4 py-2.5 bg-neutral-800 hover:bg-neutral-700 disabled:opacity-50 disabled:cursor-not-allowed text-neutral-300 rounded-lg text-sm font-medium transition-colors"
:disabled="server.connection?.connection_status === 'connected'"
class="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"
>
Start Server
</button>
<button
disabled
class="px-4 py-2.5 bg-neutral-800 hover:bg-neutral-700 disabled:opacity-50 disabled:cursor-not-allowed text-neutral-300 rounded-lg text-sm font-medium transition-colors"
:disabled="server.connection?.connection_status !== 'connected'"
class="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"
>
Stop Server
</button>
<button
disabled
class="px-4 py-2.5 bg-neutral-800 hover:bg-neutral-700 disabled:opacity-50 disabled:cursor-not-allowed text-neutral-300 rounded-lg text-sm font-medium transition-colors"
class="px-4 py-2.5 bg-neutral-800 hover:bg-neutral-700 text-neutral-300 rounded-lg text-sm font-medium transition-colors"
>
Trigger Wipe
</button>
</div>
</div>
<!-- Server Info (if configured) -->
<div v-if="server.config" class="bg-neutral-900 border border-neutral-800 rounded-lg p-5">
<h2 class="text-lg font-semibold text-neutral-200 mb-3">Server Configuration</h2>
<div class="grid grid-cols-2 lg:grid-cols-4 gap-4 text-sm">
<div>
<span class="text-neutral-500">Server Name</span>
<p class="text-neutral-200 mt-0.5">{{ server.config.server_name || 'Not set' }}</p>
</div>
<div>
<span class="text-neutral-500">Max Players</span>
<p class="text-neutral-200 mt-0.5">{{ server.config.max_players ?? 'Not set' }}</p>
</div>
<div>
<span class="text-neutral-500">World Size</span>
<p class="text-neutral-200 mt-0.5">{{ server.config.world_size ?? 'Not set' }}</p>
</div>
<div>
<span class="text-neutral-500">Current Seed</span>
<p class="text-neutral-200 mt-0.5">{{ server.config.current_seed ?? 'Not set' }}</p>
</div>
</div>
</div>
</div>
</template>