Files
FEDEO/frontend/pages/administration/scanners.vue

407 lines
14 KiB
Vue

<script setup lang="ts">
import type { InstanceAgent } from "~/composables/useAdmin"
const auth = useAuthStore()
const router = useRouter()
const toast = useToast()
const admin = useAdmin()
const loading = ref(true)
const saving = ref(false)
const creating = ref(false)
const agents = ref<InstanceAgent[]>([])
const selectedAgentId = ref<string | null>(null)
const generatedToken = ref("")
const newAgent = reactive({
name: "",
description: "",
})
const editState = reactive({
name: "",
description: "",
active: true,
preferredScannerName: "",
format: "pdf",
resolution: 300,
mode: "Color",
source: "",
})
const selectedAgent = computed(() =>
agents.value.find((agent) => agent.id === selectedAgentId.value) || null
)
const scannerOptions = computed(() => {
const names = selectedAgent.value?.scannerNames || []
return names.map((scanner) => ({
label: scanner,
value: scanner,
}))
})
const isOnline = (agent: InstanceAgent) => {
if (!agent.lastSeenAt) return false
return Date.now() - new Date(agent.lastSeenAt).getTime() < 2 * 60 * 1000
}
const formatDate = (value?: string | null) => {
if (!value) return "-"
return new Date(value).toLocaleString("de-DE")
}
const applyAgentToForm = (agent: InstanceAgent | null) => {
if (!agent) return
editState.name = agent.name || ""
editState.description = agent.description || ""
editState.active = Boolean(agent.active)
editState.preferredScannerName = agent.preferredScannerName || ""
editState.format = agent.scanDefaults?.format || "pdf"
editState.resolution = Number(agent.scanDefaults?.resolution || 300)
editState.mode = agent.scanDefaults?.mode || "Color"
editState.source = agent.scanDefaults?.source || ""
}
watch(selectedAgent, (agent) => applyAgentToForm(agent), { immediate: true })
const loadAgents = async () => {
loading.value = true
try {
const response = await admin.getInstanceAgents()
agents.value = response.agents
if (!selectedAgentId.value && agents.value[0]) {
selectedAgentId.value = agents.value[0].id
}
if (selectedAgent.value) applyAgentToForm(selectedAgent.value)
} catch (err: any) {
console.error("[administration/scanners]", err)
toast.add({
title: "Scanner konnten nicht geladen werden",
description: err?.data?.error || err?.message || "Unbekannter Fehler",
color: "error",
})
} finally {
loading.value = false
}
}
const createAgent = async () => {
if (!newAgent.name.trim()) return
creating.value = true
generatedToken.value = ""
try {
const response = await admin.createInstanceAgent({
name: newAgent.name.trim(),
description: newAgent.description.trim() || null,
})
generatedToken.value = response.token
newAgent.name = ""
newAgent.description = ""
selectedAgentId.value = response.agent.id
await loadAgents()
toast.add({
title: "Agent angelegt",
description: "Der Token wird nur jetzt vollständig angezeigt.",
color: "success",
})
} catch (err: any) {
toast.add({
title: "Agent konnte nicht angelegt werden",
description: err?.data?.error || err?.message || "Unbekannter Fehler",
color: "error",
})
} finally {
creating.value = false
}
}
const saveAgent = async () => {
if (!selectedAgent.value) return
saving.value = true
try {
await admin.updateInstanceAgent(selectedAgent.value.id, {
name: editState.name.trim(),
description: editState.description.trim() || null,
active: editState.active,
preferredScannerName: editState.preferredScannerName || null,
scanDefaults: {
format: editState.format,
resolution: Number(editState.resolution || 300),
mode: editState.mode,
source: editState.source || null,
},
})
await loadAgents()
toast.add({
title: "Scanner-Einstellungen gespeichert",
color: "success",
})
} catch (err: any) {
toast.add({
title: "Scanner-Einstellungen konnten nicht gespeichert werden",
description: err?.data?.error || err?.message || "Unbekannter Fehler",
color: "error",
})
} finally {
saving.value = false
}
}
onMounted(async () => {
if (!auth.user?.is_admin) {
await router.push("/")
return
}
await loadAgents()
})
</script>
<template>
<UDashboardNavbar title="Administration: Scanner">
<template #right>
<UButton
icon="i-heroicons-arrow-path"
color="neutral"
variant="outline"
:loading="loading"
@click="loadAgents"
>
Aktualisieren
</UButton>
</template>
</UDashboardNavbar>
<UDashboardPanelContent>
<div class="grid min-h-0 gap-6 xl:grid-cols-[360px_minmax(0,1fr)]">
<div class="space-y-4">
<UCard :ui="{ root: 'rounded-lg' }">
<template #header>
<div class="flex items-center gap-2">
<UIcon name="i-heroicons-plus-circle" class="size-5 text-primary" />
<h2 class="text-base font-semibold text-highlighted">Agent anlegen</h2>
</div>
</template>
<form class="space-y-3" @submit.prevent="createAgent">
<UFormField label="Name">
<UInput v-model="newAgent.name" placeholder="MacBook Büro" />
</UFormField>
<UFormField label="Beschreibung">
<UTextarea v-model="newAgent.description" :rows="2" placeholder="Arbeitsplatz oder Standort" />
</UFormField>
<UButton
type="submit"
icon="i-heroicons-plus"
:loading="creating"
:disabled="!newAgent.name.trim()"
>
Agent anlegen
</UButton>
</form>
</UCard>
<UAlert
v-if="generatedToken"
color="warning"
variant="soft"
icon="i-heroicons-key"
title="Agent-Token"
description="Diesen Token jetzt in die .env des lokalen Agenten übernehmen. Später wird er nicht erneut angezeigt."
>
<template #description>
<code class="mt-2 block break-all rounded bg-muted px-2 py-1 text-xs">{{ generatedToken }}</code>
</template>
</UAlert>
<UCard :ui="{ root: 'rounded-lg' }">
<template #header>
<div class="flex items-center justify-between gap-3">
<div class="flex items-center gap-2">
<UIcon name="i-heroicons-computer-desktop" class="size-5 text-primary" />
<h2 class="text-base font-semibold text-highlighted">Agenten</h2>
</div>
<UBadge variant="soft">{{ agents.length }}</UBadge>
</div>
</template>
<div class="space-y-2">
<button
v-for="agent in agents"
:key="agent.id"
type="button"
class="flex w-full items-center gap-3 rounded-lg border border-default px-3 py-2 text-left transition hover:bg-muted"
:class="selectedAgentId === agent.id ? 'border-primary bg-primary/5' : ''"
@click="selectedAgentId = agent.id"
>
<span
class="size-2 rounded-full"
:class="isOnline(agent) ? 'bg-success' : 'bg-muted'"
/>
<span class="min-w-0 flex-1">
<span class="block truncate text-sm font-medium text-highlighted">{{ agent.name }}</span>
<span class="block truncate text-xs text-muted">{{ agent.scannerNames?.length || 0 }} Scanner · {{ formatDate(agent.lastSeenAt) }}</span>
</span>
<UBadge :color="agent.active ? 'success' : 'neutral'" variant="soft" size="xs">
{{ agent.active ? "Aktiv" : "Inaktiv" }}
</UBadge>
</button>
</div>
</UCard>
</div>
<div v-if="selectedAgent" class="space-y-4">
<UCard :ui="{ root: 'rounded-lg' }">
<template #header>
<div class="flex flex-wrap items-center justify-between gap-3">
<div>
<h2 class="text-base font-semibold text-highlighted">{{ selectedAgent.name }}</h2>
<p class="text-xs text-muted">Token-Präfix {{ selectedAgent.tokenPrefix }} · zuletzt gesehen {{ formatDate(selectedAgent.lastSeenAt) }}</p>
</div>
<div class="flex items-center gap-2">
<UBadge :color="isOnline(selectedAgent) ? 'success' : 'neutral'" variant="soft">
{{ isOnline(selectedAgent) ? "Online" : "Offline" }}
</UBadge>
<UBadge :color="selectedAgent.active ? 'success' : 'neutral'" variant="soft">
{{ selectedAgent.active ? "Aktiv" : "Inaktiv" }}
</UBadge>
</div>
</div>
</template>
<div class="grid gap-6 lg:grid-cols-2">
<div class="space-y-4">
<UFormField label="Name">
<UInput v-model="editState.name" />
</UFormField>
<UFormField label="Beschreibung">
<UTextarea v-model="editState.description" :rows="3" />
</UFormField>
<UCheckbox v-model="editState.active" label="Agent aktiv" />
</div>
<div class="space-y-4">
<UFormField label="Standard-Scanner">
<USelectMenu
v-if="scannerOptions.length"
v-model="editState.preferredScannerName"
:items="scannerOptions"
value-key="value"
label-key="label"
placeholder="Scanner wählen"
/>
<UInput
v-else
v-model="editState.preferredScannerName"
placeholder="Scannername"
/>
</UFormField>
<div class="grid gap-3 sm:grid-cols-2">
<UFormField label="Format">
<USelectMenu
v-model="editState.format"
:items="[
{ label: 'PDF', value: 'pdf' },
{ label: 'PNG', value: 'png' },
{ label: 'TIFF', value: 'tiff' },
]"
value-key="value"
label-key="label"
/>
</UFormField>
<UFormField label="Auflösung">
<UInput v-model.number="editState.resolution" type="number" min="75" step="25" />
</UFormField>
</div>
<div class="grid gap-3 sm:grid-cols-2">
<UFormField label="Modus">
<UInput v-model="editState.mode" placeholder="Color" />
</UFormField>
<UFormField label="Quelle">
<UInput v-model="editState.source" placeholder="ADF Duplex" />
</UFormField>
</div>
</div>
</div>
<template #footer>
<div class="flex justify-end">
<UButton icon="i-heroicons-check" :loading="saving" @click="saveAgent">
Speichern
</UButton>
</div>
</template>
</UCard>
<div class="grid gap-4 lg:grid-cols-2">
<UCard :ui="{ root: 'rounded-lg' }">
<template #header>
<div class="flex items-center gap-2">
<UIcon name="i-heroicons-queue-list" class="size-5 text-primary" />
<h2 class="text-base font-semibold text-highlighted">Erkannte Scanner</h2>
</div>
</template>
<div v-if="selectedAgent.scannerNames?.length" class="space-y-2">
<div
v-for="scanner in selectedAgent.scannerNames"
:key="scanner"
class="rounded border border-default px-3 py-2 text-sm text-highlighted"
>
{{ scanner }}
</div>
</div>
<p v-else class="text-sm text-muted">Noch keine Scanner vom Agenten gemeldet.</p>
</UCard>
<UCard :ui="{ root: 'rounded-lg' }">
<template #header>
<div class="flex items-center gap-2">
<UIcon name="i-heroicons-information-circle" class="size-5 text-primary" />
<h2 class="text-base font-semibold text-highlighted">Agent-Info</h2>
</div>
</template>
<dl class="space-y-3 text-sm">
<div class="flex justify-between gap-3">
<dt class="text-muted">Plattform</dt>
<dd class="text-highlighted">{{ selectedAgent.capabilities?.platform || "-" }}</dd>
</div>
<div class="flex justify-between gap-3">
<dt class="text-muted">Scan</dt>
<dd class="text-highlighted">{{ selectedAgent.capabilities?.scan ? "Ja" : "Nein" }}</dd>
</div>
<div class="flex justify-between gap-3">
<dt class="text-muted">Druck</dt>
<dd class="text-highlighted">{{ selectedAgent.capabilities?.print ? "Ja" : "Nein" }}</dd>
</div>
<div class="flex justify-between gap-3">
<dt class="text-muted">Hostname</dt>
<dd class="truncate text-highlighted">{{ selectedAgent.lastDebugInfo?.hostname || "-" }}</dd>
</div>
</dl>
</UCard>
</div>
</div>
<UCard v-else :ui="{ root: 'rounded-lg' }">
<div class="py-10 text-center text-muted">
Noch kein Scanner-Agent angelegt.
</div>
</UCard>
</div>
</UDashboardPanelContent>
</template>