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:
112
frontend/src/views/admin/ChangelogView.vue
Normal file
112
frontend/src/views/admin/ChangelogView.vue
Normal 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>
|
||||
Reference in New Issue
Block a user