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:
37
frontend/src/components/ErrorBoundary.vue
Normal file
37
frontend/src/components/ErrorBoundary.vue
Normal file
@@ -0,0 +1,37 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onErrorCaptured } from 'vue'
|
||||
import { AlertTriangle } from 'lucide-vue-next'
|
||||
|
||||
const hasError = ref(false)
|
||||
const errorMessage = ref('')
|
||||
|
||||
onErrorCaptured((err) => {
|
||||
hasError.value = true
|
||||
errorMessage.value = err.message || 'An unexpected error occurred'
|
||||
console.error('ErrorBoundary caught:', err)
|
||||
return false
|
||||
})
|
||||
|
||||
function retry() {
|
||||
hasError.value = false
|
||||
errorMessage.value = ''
|
||||
window.location.reload()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="hasError" class="min-h-screen bg-neutral-950 flex items-center justify-center p-6">
|
||||
<div class="bg-neutral-900 border border-neutral-800 rounded-lg p-8 max-w-md w-full text-center">
|
||||
<AlertTriangle class="w-12 h-12 text-red-500 mx-auto mb-4" />
|
||||
<h1 class="text-xl font-bold text-neutral-100 mb-2">Something went wrong</h1>
|
||||
<p class="text-sm text-neutral-400 mb-6">{{ errorMessage }}</p>
|
||||
<button
|
||||
@click="retry"
|
||||
class="px-4 py-2 bg-oxide-500 hover:bg-oxide-600 text-white font-medium rounded-lg transition-colors"
|
||||
>
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<slot v-else />
|
||||
</template>
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { RouterView, RouterLink, useRoute, useRouter } from 'vue-router'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useServerStore } from '@/stores/server'
|
||||
@@ -22,28 +23,37 @@ import {
|
||||
Key,
|
||||
CreditCard,
|
||||
Network,
|
||||
Clock,
|
||||
AlertTriangle,
|
||||
FileText,
|
||||
Menu,
|
||||
X,
|
||||
} from 'lucide-vue-next'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const auth = useAuthStore()
|
||||
const server = useServerStore()
|
||||
const sidebarOpen = ref(false)
|
||||
|
||||
const navItems = [
|
||||
{ name: 'Dashboard', path: '/', icon: LayoutDashboard },
|
||||
{ name: 'Server', path: '/server', icon: Server },
|
||||
{ name: 'Console', path: '/console', icon: Terminal },
|
||||
{ name: 'Players', path: '/players', icon: Users },
|
||||
{ name: 'Plugins', path: '/plugins', icon: Puzzle },
|
||||
{ name: 'Auto-Wiper', path: '/wipes', icon: RefreshCw },
|
||||
{ name: 'Maps', path: '/maps', icon: Map },
|
||||
{ name: 'Chat Log', path: '/chat', icon: MessageSquare },
|
||||
{ name: 'Analytics', path: '/analytics', icon: BarChart3 },
|
||||
{ name: 'Notifications', path: '/notifications', icon: Bell },
|
||||
{ name: 'Team', path: '/team', icon: UserPlus },
|
||||
{ name: 'Store', path: '/store/manage', icon: ShoppingBag },
|
||||
{ name: 'Modules', path: '/modules', icon: Package },
|
||||
{ name: 'Settings', path: '/settings', icon: Settings },
|
||||
{ name: 'Dashboard', path: '/', icon: LayoutDashboard, permission: null },
|
||||
{ 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: 'Auto-Wiper', path: '/wipes', icon: RefreshCw, permission: 'wipes.view' },
|
||||
{ name: 'Maps', path: '/maps', icon: Map, permission: 'maps.view' },
|
||||
{ name: 'Chat Log', path: '/chat', icon: MessageSquare, permission: 'chat.view' },
|
||||
{ name: 'Analytics', path: '/analytics', icon: BarChart3, permission: 'analytics.view' },
|
||||
{ name: 'Schedules', path: '/schedules', icon: Clock, permission: 'schedules.view' },
|
||||
{ name: 'Alerts', path: '/alerts', icon: AlertTriangle, permission: 'alerts.view' },
|
||||
{ name: 'Notifications', path: '/notifications', icon: Bell, permission: 'notifications.view' },
|
||||
{ name: 'Team', path: '/team', icon: UserPlus, permission: null },
|
||||
{ name: 'Store', path: '/store/config', icon: ShoppingBag, permission: 'store.view' },
|
||||
{ name: 'Modules', path: '/modules', icon: Package, permission: 'modules.view' },
|
||||
{ name: 'Changelog', path: '/changelog', icon: FileText, permission: 'changelog.view' },
|
||||
{ name: 'Settings', path: '/settings', icon: Settings, permission: 'settings.view' },
|
||||
]
|
||||
|
||||
const adminNavItems = [
|
||||
@@ -63,20 +73,55 @@ function handleLogout() {
|
||||
auth.logout()
|
||||
router.push('/login')
|
||||
}
|
||||
|
||||
function closeSidebar() {
|
||||
sidebarOpen.value = false
|
||||
}
|
||||
|
||||
function canShowNavItem(item: typeof navItems[0]): boolean {
|
||||
if (!item.permission) return true
|
||||
return auth.hasPermission(item.permission)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-screen bg-neutral-950">
|
||||
<!-- Mobile Hamburger -->
|
||||
<button
|
||||
@click="sidebarOpen = true"
|
||||
class="md:hidden fixed top-4 left-4 z-40 p-2 bg-neutral-900 border border-neutral-800 rounded-lg text-neutral-300 hover:text-oxide-400 transition-colors"
|
||||
>
|
||||
<Menu class="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
<!-- Sidebar Overlay (Mobile) -->
|
||||
<div
|
||||
v-if="sidebarOpen"
|
||||
@click="closeSidebar"
|
||||
class="md:hidden fixed inset-0 bg-black/50 z-40"
|
||||
/>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<aside class="w-64 bg-neutral-900 border-r border-neutral-800 flex flex-col">
|
||||
<aside
|
||||
class="w-64 bg-neutral-900 border-r border-neutral-800 flex flex-col fixed md:static inset-y-0 left-0 z-50 transform transition-transform"
|
||||
:class="sidebarOpen ? 'translate-x-0' : '-translate-x-full md:translate-x-0'"
|
||||
>
|
||||
<!-- Logo -->
|
||||
<div class="p-4 border-b border-neutral-800">
|
||||
<div class="flex items-center gap-3">
|
||||
<img src="/logo.png" alt="Corrosion" class="h-8 w-8" />
|
||||
<div>
|
||||
<h1 class="text-sm font-bold text-oxide-500 tracking-wider">CORROSION</h1>
|
||||
<p class="text-xs text-neutral-500">{{ auth.license?.server_name || 'Server Management' }}</p>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<img src="/logo.png" alt="Corrosion" class="h-8 w-8" />
|
||||
<div>
|
||||
<h1 class="text-sm font-bold text-oxide-500 tracking-wider">CORROSION</h1>
|
||||
<p class="text-xs text-neutral-500">{{ auth.license?.server_name || 'Server Management' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
@click="closeSidebar"
|
||||
class="md:hidden text-neutral-400 hover:text-neutral-200 transition-colors"
|
||||
>
|
||||
<X class="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -101,8 +146,10 @@ function handleLogout() {
|
||||
<nav class="flex-1 overflow-y-auto py-2">
|
||||
<RouterLink
|
||||
v-for="item in navItems"
|
||||
v-show="canShowNavItem(item)"
|
||||
:key="item.path"
|
||||
:to="item.path"
|
||||
@click="closeSidebar"
|
||||
class="flex items-center gap-3 px-4 py-2 mx-2 rounded-lg text-sm transition-colors"
|
||||
:class="isActive(item.path)
|
||||
? 'bg-oxide-500/10 text-oxide-400'
|
||||
@@ -125,6 +172,7 @@ function handleLogout() {
|
||||
v-for="item in adminNavItems"
|
||||
:key="item.path"
|
||||
:to="item.path"
|
||||
@click="closeSidebar"
|
||||
class="flex items-center gap-3 px-4 py-2 mx-2 rounded-lg text-sm transition-colors"
|
||||
:class="isActive(item.path)
|
||||
? 'bg-oxide-500/10 text-oxide-400'
|
||||
@@ -154,7 +202,7 @@ function handleLogout() {
|
||||
</aside>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="flex-1 overflow-y-auto">
|
||||
<main class="flex-1 overflow-y-auto md:ml-0">
|
||||
<RouterView />
|
||||
</main>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user