Hub-and-Spoke Umbau: Multi-Tenant Zentrale + Satellite-Agent
- Backend: Customer/Satellite Models, customer_id auf Server/Job/Audit - Satellite-API: heartbeat, poll (atomares Claiming), logs, result, scan-result, health-report - Auth via X-Api-Key (SHA-256 gehasht) - Job-Queue: pending/claimed/running/success/failed + Stale-Janitor - Batch-Trigger: ein Job pro Server, Satellite arbeitet sequenziell ab - Credentials bleiben lokal: nur symbolische credential_ref zentral - Neues Paket satellite/: Pull-Loop, WinRM/SSH/CAU/Scanner, PyInstaller-tauglich - Frontend: Kunden-Switcher, Satelliten-View, Polling statt WebSocket - Entfernt: WebSocket/Socket.io, Redis, zentrale Credentials, JobRunner - Docs: README/AGENTS/PROMPT auf neue Architektur aktualisiert
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { useCustomersStore } from '@/stores/customers'
|
||||
import { useSatellitesStore } from '@/stores/satellites'
|
||||
import type { SatelliteCreated } from '@/types'
|
||||
|
||||
const customersStore = useCustomersStore()
|
||||
const store = useSatellitesStore()
|
||||
|
||||
const newName = ref('')
|
||||
const newKey = ref<SatelliteCreated | null>(null)
|
||||
const copied = ref(false)
|
||||
|
||||
const customerId = computed(() => customersStore.selectedId)
|
||||
|
||||
onMounted(async () => {
|
||||
await customersStore.fetchCustomers()
|
||||
if (customerId.value) await store.fetchSatellites(customerId.value)
|
||||
})
|
||||
|
||||
watch(customerId, async (id) => {
|
||||
if (id) await store.fetchSatellites(id)
|
||||
})
|
||||
|
||||
async function create(): Promise<void> {
|
||||
if (!customerId.value || !newName.value) return
|
||||
newKey.value = await store.createSatellite(customerId.value, newName.value)
|
||||
newName.value = ''
|
||||
}
|
||||
|
||||
async function rotate(id: number): Promise<void> {
|
||||
if (confirm('Neuen API-Key generieren? Der alte wird sofort ungültig.')) {
|
||||
newKey.value = await store.rotateKey(id)
|
||||
}
|
||||
}
|
||||
|
||||
async function remove(id: number): Promise<void> {
|
||||
if (confirm('Satellite wirklich löschen?')) {
|
||||
await store.deleteSatellite(id)
|
||||
}
|
||||
}
|
||||
|
||||
async function copyKey(): Promise<void> {
|
||||
if (!newKey.value) return
|
||||
await navigator.clipboard.writeText(newKey.value.api_key)
|
||||
copied.value = true
|
||||
setTimeout(() => (copied.value = false), 2000)
|
||||
}
|
||||
|
||||
function isOnline(lastSeen: string | null): boolean {
|
||||
if (!lastSeen) return false
|
||||
return Date.now() - new Date(lastSeen).getTime() < 3 * 60 * 1000
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h1 class="mb-6 text-2xl font-bold">Satelliten</h1>
|
||||
|
||||
<div v-if="newKey" class="mb-6 rounded-lg border-2 border-amber-400 bg-amber-50 p-5">
|
||||
<p class="mb-2 font-semibold text-amber-800">
|
||||
API-Key für "{{ newKey.name }}" - wird nur einmal angezeigt!
|
||||
</p>
|
||||
<div class="flex items-center gap-2">
|
||||
<code class="flex-1 overflow-x-auto rounded bg-white px-3 py-2 font-mono text-xs">{{ newKey.api_key }}</code>
|
||||
<button class="rounded bg-slate-800 px-3 py-2 text-xs text-white" @click="copyKey">
|
||||
{{ copied ? 'Kopiert!' : 'Kopieren' }}
|
||||
</button>
|
||||
<button class="rounded bg-slate-300 px-3 py-2 text-xs" @click="newKey = null">Schließen</button>
|
||||
</div>
|
||||
<p class="mt-2 text-xs text-amber-700">
|
||||
In config.yaml auf dem Satellite eintragen: <code>api_key: "..."</code>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-6 flex items-end gap-3 rounded-lg bg-white p-5 shadow">
|
||||
<div class="flex-1">
|
||||
<label class="mb-1 block text-sm font-medium">Neuer Satellite (Kunde: im Header wählen)</label>
|
||||
<input v-model="newName" placeholder="z.B. sat-hauptstandort" class="w-full rounded border-slate-300" />
|
||||
</div>
|
||||
<button
|
||||
:disabled="!newName || !customerId"
|
||||
class="rounded bg-green-700 px-4 py-2 text-sm font-semibold text-white hover:bg-green-600 disabled:opacity-50"
|
||||
@click="create"
|
||||
>
|
||||
Anlegen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="overflow-hidden rounded-lg bg-white shadow">
|
||||
<table class="min-w-full divide-y divide-slate-200 text-sm">
|
||||
<thead class="bg-slate-50">
|
||||
<tr>
|
||||
<th class="px-4 py-2 text-left font-medium">Name</th>
|
||||
<th class="px-4 py-2 text-left font-medium">Status</th>
|
||||
<th class="px-4 py-2 text-left font-medium">Version</th>
|
||||
<th class="px-4 py-2 text-left font-medium">Hostname</th>
|
||||
<th class="px-4 py-2 text-left font-medium">Key-Prefix</th>
|
||||
<th class="px-4 py-2 text-right font-medium">Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
<tr v-for="sat in store.satellites" :key="sat.id">
|
||||
<td class="px-4 py-2 font-medium">{{ sat.name }}</td>
|
||||
<td class="px-4 py-2">
|
||||
<span v-if="isOnline(sat.last_seen_at)" class="text-green-600">Online</span>
|
||||
<span v-else class="text-slate-400">Offline</span>
|
||||
</td>
|
||||
<td class="px-4 py-2">{{ sat.version || '-' }}</td>
|
||||
<td class="px-4 py-2">{{ sat.hostname || '-' }}</td>
|
||||
<td class="px-4 py-2 font-mono text-xs">{{ sat.api_key_prefix }}...</td>
|
||||
<td class="px-4 py-2 text-right">
|
||||
<button
|
||||
class="mr-2 rounded bg-blue-600 px-2 py-1 text-xs text-white hover:bg-blue-500"
|
||||
@click="rotate(sat.id)"
|
||||
>
|
||||
Key rotieren
|
||||
</button>
|
||||
<button
|
||||
class="rounded bg-red-600 px-2 py-1 text-xs text-white hover:bg-red-500"
|
||||
@click="remove(sat.id)"
|
||||
>
|
||||
Löschen
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="store.satellites.length === 0">
|
||||
<td colspan="6" class="px-4 py-6 text-center text-slate-500">
|
||||
Keine Satelliten für diesen Kunden.
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user