175 lines
6.8 KiB
Vue
175 lines
6.8 KiB
Vue
<template>
|
|
<div class="min-h-screen bg-slate-950 p-6">
|
|
<div class="max-w-7xl mx-auto">
|
|
<div class="flex items-center justify-between mb-6">
|
|
<h1 class="text-2xl font-bold text-slate-100">XFlow Admin Panel</h1>
|
|
<div class="flex items-center gap-2 text-sm text-slate-400">
|
|
<span class="w-2 h-2 rounded-full bg-emerald-500"></span>
|
|
Online
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-6">
|
|
<div class="bg-slate-900 border border-slate-700 rounded-xl p-4">
|
|
<div class="text-3xl font-bold text-blue-400">{{ stats.users }}</div>
|
|
<div class="text-sm text-slate-400 mt-1">Registered Users</div>
|
|
</div>
|
|
<div class="bg-slate-900 border border-slate-700 rounded-xl p-4">
|
|
<div class="text-3xl font-bold text-emerald-400">{{ stats.canvases }}</div>
|
|
<div class="text-sm text-slate-400 mt-1">Canvases</div>
|
|
</div>
|
|
<div class="bg-slate-900 border border-slate-700 rounded-xl p-4">
|
|
<div class="text-3xl font-bold text-yellow-400">{{ stats.nodes }}</div>
|
|
<div class="text-sm text-slate-400 mt-1">Total Nodes</div>
|
|
</div>
|
|
<div class="bg-slate-900 border border-slate-700 rounded-xl p-4">
|
|
<div class="text-3xl font-bold text-purple-400">{{ stats.pushes }}</div>
|
|
<div class="text-sm text-slate-400 mt-1">Push Subscribers</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
<!-- Users -->
|
|
<div class="lg:col-span-2 bg-slate-900 border border-slate-700 rounded-xl overflow-hidden">
|
|
<div class="p-4 border-b border-slate-700 flex items-center justify-between">
|
|
<h2 class="text-lg font-semibold text-slate-200">Users</h2>
|
|
<button @click="refreshData" class="text-sm text-blue-400 hover:text-blue-300">Refresh</button>
|
|
</div>
|
|
<div class="overflow-x-auto">
|
|
<table class="w-full text-sm">
|
|
<thead class="bg-slate-800 text-slate-400">
|
|
<tr>
|
|
<th class="text-left p-3">Email</th>
|
|
<th class="text-left p-3">Username</th>
|
|
<th class="text-left p-3">Role</th>
|
|
<th class="text-left p-3">ID</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="text-slate-300">
|
|
<tr v-for="user in users" :key="user.id" class="border-t border-slate-800">
|
|
<td class="p-3">{{ user.email }}</td>
|
|
<td class="p-3">{{ user.username }}</td>
|
|
<td class="p-3">
|
|
<span :class="roleBadge(user.role)">{{ user.role }}</span>
|
|
</td>
|
|
<td class="p-3 text-xs text-slate-500 font-mono">{{ user.id }}</td>
|
|
</tr>
|
|
<tr v-if="users.length === 0">
|
|
<td colspan="4" class="p-6 text-center text-slate-500">No users found</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Canvases -->
|
|
<div class="bg-slate-900 border border-slate-700 rounded-xl overflow-hidden">
|
|
<div class="p-4 border-b border-slate-700">
|
|
<h2 class="text-lg font-semibold text-slate-200">Canvases</h2>
|
|
</div>
|
|
<div class="divide-y divide-slate-800">
|
|
<div v-for="canvas in canvases" :key="canvas.id" class="p-3">
|
|
<div class="flex items-center justify-between">
|
|
<NuxtLink :to="`/canvas/${canvas.id}`" class="text-blue-400 hover:text-blue-300 font-medium">
|
|
{{ canvas.name }}
|
|
</NuxtLink>
|
|
<span class="text-xs text-slate-500">{{ canvas.nodeCount }} nodes</span>
|
|
</div>
|
|
<div class="text-xs text-slate-500 mt-1">{{ formatDate(canvas.updatedAt) }}</div>
|
|
</div>
|
|
<div v-if="canvases.length === 0" class="p-6 text-center text-slate-500">
|
|
No canvases yet
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Script Runner -->
|
|
<div class="mt-6 bg-slate-900 border border-slate-700 rounded-xl p-6">
|
|
<h2 class="text-lg font-semibold text-slate-200 mb-4">Script Runner</h2>
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
<div
|
|
v-for="script in scripts"
|
|
:key="script.id"
|
|
class="bg-slate-800 rounded-lg p-4"
|
|
>
|
|
<div class="flex items-center justify-between mb-2">
|
|
<span class="font-medium text-slate-200">{{ script.name }}</span>
|
|
<span class="text-xs px-2 py-1 rounded-full" :class="statusClass(script.status)">{{ script.status }}</span>
|
|
</div>
|
|
<button
|
|
@click="runScript(script.id)"
|
|
class="w-full py-2 bg-emerald-600 hover:bg-emerald-500 text-white rounded text-sm transition"
|
|
:disabled="script.status === 'running'"
|
|
>
|
|
Run
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
definePageMeta({
|
|
middleware: ['auth', 'admin']
|
|
})
|
|
|
|
const users = ref([])
|
|
const canvases = ref([])
|
|
const stats = reactive({ users: 0, canvases: 0, nodes: 0, pushes: 0 })
|
|
|
|
const scripts = ref([
|
|
{ id: 'sync-git', name: 'Sync Git Server', status: 'idle' },
|
|
{ id: 'cleanup-nodes', name: 'Cleanup Orphan Nodes', status: 'idle' },
|
|
{ id: 'export-all', name: 'Export All Canvases', status: 'idle' }
|
|
])
|
|
|
|
const refreshData = async () => {
|
|
try {
|
|
const data = await $fetch('/api/admin/stats')
|
|
users.value = data.users || []
|
|
canvases.value = data.canvases || []
|
|
stats.users = data.stats?.users || 0
|
|
stats.canvases = data.stats?.canvases || 0
|
|
stats.nodes = data.stats?.nodes || 0
|
|
stats.pushes = data.stats?.pushes || 0
|
|
} catch (e) {
|
|
console.error('Failed to load admin data', e)
|
|
}
|
|
}
|
|
|
|
const roleBadge = (role) => {
|
|
return role === 'admin'
|
|
? 'px-2 py-0.5 bg-red-900/50 text-red-300 rounded text-xs'
|
|
: 'px-2 py-0.5 bg-slate-700 text-slate-300 rounded text-xs'
|
|
}
|
|
|
|
const statusClass = (status) => {
|
|
if (status === 'running') return 'bg-yellow-900/50 text-yellow-300'
|
|
if (status === 'completed') return 'bg-emerald-900/50 text-emerald-300'
|
|
return 'bg-slate-700 text-slate-400'
|
|
}
|
|
|
|
const formatDate = (date) => {
|
|
if (!date) return 'N/A'
|
|
return new Date(date).toLocaleDateString('ru-RU')
|
|
}
|
|
|
|
const runScript = async (scriptId) => {
|
|
const script = scripts.value.find(s => s.id === scriptId)
|
|
if (!script || script.status === 'running') return
|
|
script.status = 'running'
|
|
try {
|
|
await $fetch(`/api/admin/scripts/${scriptId}`, { method: 'POST' })
|
|
script.status = 'completed'
|
|
} catch (e) {
|
|
script.status = 'failed'
|
|
}
|
|
setTimeout(() => { script.status = 'idle' }, 3000)
|
|
}
|
|
|
|
onMounted(refreshData)
|
|
</script>
|