cf7c29639c
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.
111 lines
4.0 KiB
Vue
111 lines
4.0 KiB
Vue
<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>
|