feat: restructure into XFlow monorepo with Nuxt 4, Modular Onion Monolith, Android client, and Desktop CLI

This commit is contained in:
XFlow Deploy
2026-09-28 08:57:32 +05:00
parent edcd30c829
commit 0837b3f612
204 changed files with 24616 additions and 39 deletions
+153
View File
@@ -0,0 +1,153 @@
<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>
<!-- Quick Actions -->
<div class="flex gap-4 mb-6">
<NuxtLink
to="/admin/groups"
class="px-4 py-2 bg-blue-600 hover:bg-blue-500 text-white rounded-lg font-medium transition flex items-center gap-2"
>
👥 Manage Groups & Teams
</NuxtLink>
<button
@click="refreshData"
class="px-4 py-2 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded-lg font-medium transition"
>
🔄 Refresh Data
</button>
</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>
</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 auth = useAuth()
const refreshData = async () => {
if (!auth.token.value) return
try {
const data = await $fetch('/api/admin/stats', {
headers: { Authorization: `Bearer ${auth.token.value}` }
})
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 formatDate = (date) => {
if (!date) return 'N/A'
return new Date(date).toLocaleDateString('ru-RU')
}
onMounted(async () => {
// Ждем инициализации пользователя если нужно
if (!auth.user.value) {
await auth.initAuth()
}
refreshData()
})
</script>