Initial scaffold: FastAPI backend + Vue 3 frontend + Docker setup
Backend: config/db/security/logging core, SQLAlchemy models (Server, Credential, UpdateJob, UpdateLog, AuditLog, User), services (winrm, ssh, cau, audit, job_runner), REST API (auth, servers, updates, audit), Socket.io WebSocket layer. Frontend: Vue 3 + TS + Pinia + Tailwind, Views (Dashboard, Servers, Updates, Audit, Login), axios + socket.io-client, nginx prod config.
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useServersStore } from '@/stores/servers'
|
||||
import { useUpdatesStore } from '@/stores/updates'
|
||||
import type { JobType } from '@/types'
|
||||
|
||||
const serversStore = useServersStore()
|
||||
const updatesStore = useUpdatesStore()
|
||||
|
||||
const selectedServerId = ref<number | null>(null)
|
||||
const selectedJobId = ref<number | null>(null)
|
||||
|
||||
const jobTypeForServer = computed<JobType>(() => {
|
||||
const server = serversStore.servers.find((s) => s.id === selectedServerId.value)
|
||||
if (!server) return 'windows_update'
|
||||
if (server.type === 'linux') return 'linux_update'
|
||||
if (server.type === 'cau_cluster') return 'cau_run'
|
||||
return 'windows_update'
|
||||
})
|
||||
|
||||
const selectedLogs = computed(() =>
|
||||
selectedJobId.value ? updatesStore.liveLogs[selectedJobId.value] || [] : [],
|
||||
)
|
||||
|
||||
onMounted(async () => {
|
||||
await Promise.all([serversStore.fetchServers(), updatesStore.fetchJobs()])
|
||||
})
|
||||
|
||||
async function trigger(): Promise<void> {
|
||||
if (!selectedServerId.value) return
|
||||
const job = await updatesStore.triggerUpdate(selectedServerId.value, jobTypeForServer.value)
|
||||
watchJob(job.id)
|
||||
}
|
||||
|
||||
async function watchJob(jobId: number): Promise<void> {
|
||||
selectedJobId.value = jobId
|
||||
await updatesStore.fetchLogs(jobId)
|
||||
updatesStore.subscribeJob(jobId)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h1 class="mb-6 text-2xl font-bold">Updates</h1>
|
||||
|
||||
<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">Server auswählen</label>
|
||||
<select v-model="selectedServerId" class="w-full rounded border-slate-300">
|
||||
<option :value="null" disabled>— bitte wählen —</option>
|
||||
<option v-for="server in serversStore.servers" :key="server.id" :value="server.id">
|
||||
{{ server.name }} ({{ server.hostname }})
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<button
|
||||
:disabled="!selectedServerId"
|
||||
class="rounded bg-green-700 px-4 py-2 text-sm font-semibold text-white hover:bg-green-600 disabled:opacity-50"
|
||||
@click="trigger"
|
||||
>
|
||||
Update starten
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
||||
<div class="overflow-hidden rounded-lg bg-white shadow">
|
||||
<h2 class="border-b bg-slate-50 px-4 py-2 font-semibold">Jobs</h2>
|
||||
<table class="min-w-full divide-y divide-slate-200 text-sm">
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
<tr
|
||||
v-for="job in updatesStore.jobs"
|
||||
:key="job.id"
|
||||
class="cursor-pointer hover:bg-slate-50"
|
||||
:class="{ 'bg-blue-50': job.id === selectedJobId }"
|
||||
@click="watchJob(job.id)"
|
||||
>
|
||||
<td class="px-4 py-2">#{{ job.id }}</td>
|
||||
<td class="px-4 py-2">{{ job.type }}</td>
|
||||
<td class="px-4 py-2">{{ job.status }}</td>
|
||||
<td class="px-4 py-2 text-right">
|
||||
<button
|
||||
v-if="job.status === 'running' || job.status === 'pending'"
|
||||
class="rounded bg-red-600 px-2 py-1 text-xs text-white hover:bg-red-500"
|
||||
@click.stop="updatesStore.cancelJob(job.id)"
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="updatesStore.jobs.length === 0">
|
||||
<td colspan="4" class="px-4 py-6 text-center text-slate-500">Keine Jobs.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg bg-slate-900 p-4 font-mono text-xs text-green-300 shadow">
|
||||
<h2 class="mb-2 font-sans text-sm font-semibold text-slate-300">
|
||||
Live-Log {{ selectedJobId ? `(Job #${selectedJobId})` : '' }}
|
||||
</h2>
|
||||
<div class="max-h-96 overflow-y-auto whitespace-pre-wrap">
|
||||
<p v-if="selectedLogs.length === 0" class="text-slate-500">
|
||||
Kein Job ausgewählt — klicke links einen Job an.
|
||||
</p>
|
||||
<p v-for="log in selectedLogs" :key="log.id">{{ log.line }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user