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>
113 lines
3.3 KiB
Vue
113 lines
3.3 KiB
Vue
<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>
|