feat: Frontend gap closure — Schedules, Alerts, Migration, Changelog views

Implements missing frontend views and API integrations:

New Views:
- SchedulesView: CRUD for scheduled tasks (restart/announcement/command/plugin_reload)
- MigrationView: Export/import interface with file upload and history tracking
- ChangelogView: Paginated changelog feed with category badges
- ForgotPasswordView: Password reset flow with email submission
- AlertsView: Alert config dashboard with threshold settings and history

Component Updates:
- ErrorBoundary: Global error handler with retry functionality
- DashboardLayout: Mobile responsive sidebar, permission-based nav, new menu items
- ServerInfoView: Complete rewrite for public server info display

Infrastructure:
- useApi: Token refresh interceptor with 401 retry and infinite loop prevention
- plugins store: Implemented all stubbed methods with real API calls
- auth store: Added hasPermission() helper for RBAC UI visibility
- Router: Added new routes with catch-all fallback

Purpose: Closes frontend implementation gaps. Hardens auth flow, improves mobile UX,
enables server automation scheduling, alert configuration, and data migration tools.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Vantz Stockwell
2026-02-15 21:20:40 -05:00
parent 8cd792eb75
commit 4c648783a2
14 changed files with 1327 additions and 40 deletions

View File

@@ -1,10 +1,149 @@
<script setup lang="ts">
// TODO: Implement public-facing server information page
import { ref, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { Server, Users, Calendar, MessageCircle, Loader2, ExternalLink } from 'lucide-vue-next'
interface ServerInfo {
server_name: string
description: string | null
header_image: string | null
motd: string | null
wipe_schedule: string | null
discord_invite: string | null
player_count: number
max_players: number
mods: string[]
connect_url: string
}
const route = useRoute()
const subdomain = route.params.subdomain as string
const serverInfo = ref<ServerInfo | null>(null)
const isLoading = ref(false)
const error = ref('')
async function fetchServerInfo() {
isLoading.value = true
error.value = ''
try {
const response = await fetch(`/api/public/${subdomain}/info`)
if (!response.ok) {
throw new Error('Server not found')
}
serverInfo.value = await response.json()
} catch (err) {
error.value = err instanceof Error ? err.message : 'Failed to load server info'
} finally {
isLoading.value = false
}
}
function copyConnectUrl() {
if (serverInfo.value?.connect_url) {
navigator.clipboard.writeText(serverInfo.value.connect_url)
alert('Connect URL copied to clipboard')
}
}
onMounted(() => {
fetchServerInfo()
})
</script>
<template>
<div class="p-6">
<h1 class="text-2xl font-bold text-neutral-100 mb-4">Server Info</h1>
<p class="text-neutral-400">Public server information rules, description, and connection details.</p>
<div class="min-h-screen bg-neutral-950">
<!-- Loading State -->
<div v-if="isLoading" class="flex items-center justify-center min-h-screen">
<Loader2 class="w-8 h-8 text-oxide-500 animate-spin" />
</div>
<!-- Error State -->
<div v-else-if="error" class="flex items-center justify-center min-h-screen p-6">
<div class="bg-neutral-900 border border-neutral-800 rounded-lg p-8 max-w-md text-center">
<Server class="w-12 h-12 text-neutral-600 mx-auto mb-4" />
<h1 class="text-xl font-bold text-neutral-100 mb-2">Server Not Found</h1>
<p class="text-sm text-neutral-400">{{ error }}</p>
</div>
</div>
<!-- Server Info -->
<div v-else-if="serverInfo" class="max-w-4xl mx-auto p-6 space-y-6">
<!-- Header Image -->
<div v-if="serverInfo.header_image" class="rounded-lg overflow-hidden">
<img :src="serverInfo.header_image" :alt="serverInfo.server_name" class="w-full h-64 object-cover" />
</div>
<!-- Server Name & Stats -->
<div class="bg-neutral-900 border border-neutral-800 rounded-lg p-6">
<div class="flex items-start justify-between mb-4">
<div>
<h1 class="text-3xl font-bold text-neutral-100 mb-2">{{ serverInfo.server_name }}</h1>
<div class="flex items-center gap-2 text-neutral-400">
<Users class="w-4 h-4" />
<span class="text-sm">{{ serverInfo.player_count }}/{{ serverInfo.max_players }} players online</span>
</div>
</div>
<button
@click="copyConnectUrl"
class="flex items-center gap-2 px-4 py-2 bg-oxide-500 hover:bg-oxide-600 text-white font-medium rounded-lg transition-colors"
>
<ExternalLink class="w-4 h-4" />
Connect
</button>
</div>
<p v-if="serverInfo.description" class="text-neutral-300 leading-relaxed">
{{ serverInfo.description }}
</p>
</div>
<!-- MOTD -->
<div v-if="serverInfo.motd" class="bg-neutral-900 border border-neutral-800 rounded-lg p-6">
<h2 class="text-lg font-bold text-neutral-100 mb-3">Message of the Day</h2>
<p class="text-neutral-300 whitespace-pre-line">{{ serverInfo.motd }}</p>
</div>
<!-- Wipe Schedule -->
<div v-if="serverInfo.wipe_schedule" class="bg-neutral-900 border border-neutral-800 rounded-lg p-6">
<div class="flex items-center gap-2 mb-3">
<Calendar class="w-5 h-5 text-oxide-500" />
<h2 class="text-lg font-bold text-neutral-100">Wipe Schedule</h2>
</div>
<p class="text-neutral-300">{{ serverInfo.wipe_schedule }}</p>
</div>
<!-- Mods -->
<div v-if="serverInfo.mods.length > 0" class="bg-neutral-900 border border-neutral-800 rounded-lg p-6">
<h2 class="text-lg font-bold text-neutral-100 mb-3">Active Mods</h2>
<div class="flex flex-wrap gap-2">
<span
v-for="mod in serverInfo.mods"
:key="mod"
class="px-3 py-1 bg-neutral-800 border border-neutral-700 rounded-full text-sm text-neutral-300"
>
{{ mod }}
</span>
</div>
</div>
<!-- Discord -->
<div v-if="serverInfo.discord_invite" class="bg-neutral-900 border border-neutral-800 rounded-lg p-6">
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<MessageCircle class="w-5 h-5 text-oxide-500" />
<h2 class="text-lg font-bold text-neutral-100">Join our Discord</h2>
</div>
<a
:href="serverInfo.discord_invite"
target="_blank"
rel="noopener noreferrer"
class="flex items-center gap-2 px-4 py-2 bg-neutral-800 hover:bg-neutral-700 text-neutral-200 font-medium rounded-lg transition-colors"
>
<ExternalLink class="w-4 h-4" />
Join
</a>
</div>
</div>
</div>
</div>
</template>