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

@@ -0,0 +1,112 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useApi } from '@/composables/useApi'
import { FileText, Tag, Loader2 } from 'lucide-vue-next'
interface ChangelogEntry {
id: string
version: string
title: string
body: string
category: 'feature' | 'bugfix' | 'module' | 'security'
published_at: string
}
const api = useApi()
const entries = ref<ChangelogEntry[]>([])
const isLoading = ref(false)
const page = ref(1)
const hasMore = ref(true)
async function fetchChangelog() {
isLoading.value = true
try {
const result = await api.get<ChangelogEntry[]>(`/changelog?page=${page.value}&limit=20`)
if (result.length === 0) {
hasMore.value = false
} else {
entries.value.push(...result)
}
} finally {
isLoading.value = false
}
}
function loadMore() {
page.value++
fetchChangelog()
}
function getCategoryColor(category: string): string {
switch (category) {
case 'feature': return 'bg-green-500/10 text-green-400'
case 'bugfix': return 'bg-red-500/10 text-red-400'
case 'module': return 'bg-blue-500/10 text-blue-400'
case 'security': return 'bg-yellow-500/10 text-yellow-400'
default: return 'bg-neutral-700/50 text-neutral-400'
}
}
onMounted(() => {
fetchChangelog()
})
</script>
<template>
<div class="p-6 space-y-6">
<!-- Header -->
<div class="flex items-center gap-3">
<FileText class="w-5 h-5 text-oxide-500" />
<h1 class="text-2xl font-bold text-neutral-100">Changelog</h1>
</div>
<!-- Changelog Feed -->
<div class="space-y-4">
<div
v-for="entry in entries"
:key="entry.id"
class="bg-neutral-900 border border-neutral-800 rounded-lg p-5"
>
<div class="flex items-start justify-between mb-3">
<div class="flex items-center gap-3">
<div class="flex items-center gap-2 px-2 py-1 bg-oxide-500/10 border border-oxide-500/20 rounded-lg">
<Tag class="w-3 h-3 text-oxide-400" />
<span class="text-xs font-mono text-oxide-400">{{ entry.version }}</span>
</div>
<span
class="text-xs font-medium px-2 py-0.5 rounded-full capitalize"
:class="getCategoryColor(entry.category)"
>
{{ entry.category }}
</span>
</div>
<span class="text-xs text-neutral-500">{{ new Date(entry.published_at).toLocaleDateString() }}</span>
</div>
<h3 class="text-lg font-bold text-neutral-100 mb-2">{{ entry.title }}</h3>
<div class="text-sm text-neutral-300 whitespace-pre-line leading-relaxed">
{{ entry.body }}
</div>
</div>
<!-- Loading State -->
<div v-if="isLoading" class="flex justify-center py-6">
<Loader2 class="w-6 h-6 text-oxide-500 animate-spin" />
</div>
<!-- Load More -->
<div v-else-if="hasMore" class="flex justify-center">
<button
@click="loadMore"
class="px-4 py-2 text-sm font-medium text-neutral-300 bg-neutral-800 hover:bg-neutral-700 rounded-lg transition-colors"
>
Load More
</button>
</div>
<!-- End of List -->
<div v-else class="text-center py-6 text-sm text-neutral-500">
No more changelog entries
</div>
</div>
</div>
</template>