feat: zentrale selfhost-dienste bereitstellen
This commit is contained in:
@@ -6,7 +6,8 @@ const { data, pending, refresh, error } = await useAsyncData("summary", () => pu
|
||||
instances: number;
|
||||
devices: number;
|
||||
jobs: number;
|
||||
failedJobs: number;
|
||||
failedJobs: number;
|
||||
serviceUnits: number;
|
||||
}>("/admin/summary"), { immediate: false });
|
||||
|
||||
onMounted(async () => {
|
||||
@@ -26,12 +27,12 @@ watch(error, (nextError) => {
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-gray-950">Übersicht</h2>
|
||||
<p class="text-sm text-gray-500">Technischer Zustand des zentralen Push-Gateways.</p>
|
||||
<p class="text-sm text-gray-500">Technischer Zustand des zentralen FEDEO-Service-Gateways.</p>
|
||||
</div>
|
||||
<UButton icon="i-lucide-refresh-cw" variant="soft" :loading="pending" @click="refresh()">Aktualisieren</UButton>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 md:grid-cols-4">
|
||||
<div class="grid gap-4 md:grid-cols-5">
|
||||
<UCard>
|
||||
<p class="text-sm text-gray-500">Instanzen</p>
|
||||
<p class="mt-2 text-3xl font-bold">{{ data?.instances ?? 0 }}</p>
|
||||
@@ -48,6 +49,10 @@ watch(error, (nextError) => {
|
||||
<p class="text-sm text-gray-500">Fehler</p>
|
||||
<p class="mt-2 text-3xl font-bold text-red-600">{{ data?.failedJobs ?? 0 }}</p>
|
||||
</UCard>
|
||||
<UCard>
|
||||
<p class="text-sm text-gray-500">Service-Einheiten</p>
|
||||
<p class="mt-2 text-3xl font-bold">{{ data?.serviceUnits ?? 0 }}</p>
|
||||
</UCard>
|
||||
</div>
|
||||
</div>
|
||||
</TokenGate>
|
||||
|
||||
@@ -4,6 +4,7 @@ const pushApi = usePushApi();
|
||||
const toast = useToast();
|
||||
const id = route.params.id as string;
|
||||
const testSubmitting = ref(false);
|
||||
const serviceSaving = ref("");
|
||||
const selectedDeviceIds = ref<string[]>([]);
|
||||
const testForm = reactive({
|
||||
title: "FEDEO Push-Test",
|
||||
@@ -13,6 +14,12 @@ const testForm = reactive({
|
||||
const { data: instance, refresh: refreshInstance } = await useAsyncData(`instance-${id}`, () => pushApi.request<Record<string, any>>(`/admin/instances/${id}`), { immediate: false });
|
||||
const { data: devices, refresh: refreshDevices } = await useAsyncData(`devices-${id}`, () => pushApi.request<Record<string, any>[]>(`/admin/instances/${id}/devices`), { default: () => [], immediate: false });
|
||||
const { data: jobs, refresh: refreshJobs } = await useAsyncData(`jobs-${id}`, () => pushApi.request<Record<string, any>[]>(`/admin/instances/${id}/jobs`), { default: () => [], immediate: false });
|
||||
const { data: entitlements, refresh: refreshEntitlements } = await useAsyncData(`services-${id}`, () => pushApi.request<Record<string, any>[]>(`/admin/instances/${id}/services`), { default: () => [], immediate: false });
|
||||
const { data: usage, refresh: refreshUsage } = await useAsyncData(`usage-${id}`, () => pushApi.request<Record<string, any>[]>(`/admin/instances/${id}/usage?limit=100`), { default: () => [], immediate: false });
|
||||
const serviceForms = reactive({
|
||||
ai: { enabled: false, monthlyLimit: null as number | null, unitPriceMicros: 0 },
|
||||
banking: { enabled: false, monthlyLimit: null as number | null, unitPriceMicros: 0 },
|
||||
});
|
||||
const activeDevices = computed(() => (devices.value || []).filter((device) => device.status === "active"));
|
||||
|
||||
onMounted(async () => {
|
||||
@@ -23,7 +30,30 @@ onMounted(async () => {
|
||||
});
|
||||
|
||||
async function refreshAll() {
|
||||
await Promise.all([refreshInstance(), refreshDevices(), refreshJobs()]);
|
||||
await Promise.all([refreshInstance(), refreshDevices(), refreshJobs(), refreshEntitlements(), refreshUsage()]);
|
||||
for (const entitlement of entitlements.value || []) {
|
||||
const form = serviceForms[entitlement.service as "ai" | "banking"];
|
||||
if (form) Object.assign(form, { enabled: entitlement.enabled, monthlyLimit: entitlement.monthlyLimit, unitPriceMicros: entitlement.unitPriceMicros });
|
||||
}
|
||||
}
|
||||
|
||||
async function saveService(service: "ai" | "banking") {
|
||||
serviceSaving.value = service;
|
||||
try {
|
||||
const form = serviceForms[service];
|
||||
await pushApi.request(`/admin/instances/${id}/services/${service}`, {
|
||||
method: "PUT",
|
||||
body: {
|
||||
enabled: form.enabled,
|
||||
monthlyLimit: Number(form.monthlyLimit) > 0 ? Number(form.monthlyLimit) : null,
|
||||
unitPriceMicros: Math.max(0, Number(form.unitPriceMicros) || 0),
|
||||
},
|
||||
});
|
||||
await Promise.all([refreshEntitlements(), refreshUsage()]);
|
||||
toast.add({ title: `${service === "ai" ? "KI" : "Banking"}-Dienst gespeichert`, color: "success" });
|
||||
} finally {
|
||||
serviceSaving.value = "";
|
||||
}
|
||||
}
|
||||
|
||||
function toggleDevice(centralDeviceId: string, checked: boolean) {
|
||||
@@ -96,6 +126,39 @@ async function sendTestPush() {
|
||||
</UCard>
|
||||
</div>
|
||||
|
||||
<UCard>
|
||||
<template #header><h3 class="font-bold">Zentrale Dienste und Abrechnung</h3></template>
|
||||
<div class="grid gap-4 lg:grid-cols-2">
|
||||
<div v-for="service in (['ai', 'banking'] as const)" :key="service" class="space-y-3 rounded-lg border border-gray-200 p-4">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<p class="font-semibold">{{ service === 'ai' ? 'KI' : 'Banking' }}</p>
|
||||
<p class="text-xs text-gray-500">{{ service === 'ai' ? 'Einheit: Provider-Token' : 'Einheit: Provider-Aufruf' }}</p>
|
||||
</div>
|
||||
<USwitch v-model="serviceForms[service].enabled" />
|
||||
</div>
|
||||
<UFormField label="Monatslimit (leer = unbegrenzt)">
|
||||
<UInput v-model.number="serviceForms[service].monthlyLimit" type="number" min="1" />
|
||||
</UFormField>
|
||||
<UFormField label="Preis pro Einheit in Mikro-Euro">
|
||||
<UInput v-model.number="serviceForms[service].unitPriceMicros" type="number" min="0" />
|
||||
</UFormField>
|
||||
<UButton :loading="serviceSaving === service" @click="saveService(service)">Speichern</UButton>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-6 overflow-x-auto">
|
||||
<table class="w-full text-left text-sm">
|
||||
<thead class="text-gray-500"><tr><th class="py-2">Zeit</th><th>Dienst</th><th>Vorgang</th><th>Status</th><th>Einheiten</th><th>Kosten</th></tr></thead>
|
||||
<tbody>
|
||||
<tr v-for="event in usage" :key="event.id" class="border-t border-gray-100">
|
||||
<td class="py-2">{{ new Date(event.createdAt).toLocaleString() }}</td><td>{{ event.service }}</td><td>{{ event.operation }}</td><td>{{ event.status }}</td><td>{{ event.units }}</td><td>{{ (Number(event.costMicros || 0) / 1_000_000).toLocaleString('de-DE', { style: 'currency', currency: 'EUR' }) }}</td>
|
||||
</tr>
|
||||
<tr v-if="!usage?.length"><td colspan="6" class="py-4 text-gray-500">Noch keine Nutzung erfasst.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</UCard>
|
||||
|
||||
<UCard>
|
||||
<template #header>
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
|
||||
Reference in New Issue
Block a user