feat(selfhost): show sanitized update logs in admin UI
All checks were successful
Build and Release FEDEO / verify (push) Successful in 48s
Build and Release FEDEO / build-and-release (push) Successful in 5m45s

This commit is contained in:
2026-09-25 18:42:26 +02:00
parent 8f06d06037
commit 29155f916a
9 changed files with 398 additions and 9 deletions

View File

@@ -7,9 +7,11 @@ export type UpdateJob = UpdateState & {
plan?: { version: string; revision: string; previousVersion?: string; updateAvailable: boolean;
requiresComposeAdoption: boolean; estimatedBackupBytes?: number; retiredServices: string[] };
}
export type UpdateLogs = { requestId: string; logs: { source: 'service' | 'events' | 'commands'; content: string; truncated: boolean }[] }
type Overview = {
acceptingRequests: boolean; engine: UpdateState; request?: UpdateJob;
installed: { version?: string }; backend: { version: string; revision: string };
capabilities?: { requestLogs: boolean };
}
type Submission = { command: 'check' | 'apply'; body: { requestId: string; version?: string; checkRequestId?: string } }
@@ -24,6 +26,12 @@ export const useSelfhostUpdates = () => {
const refreshing = ref(false)
const message = ref('')
const accessDenied = ref(false)
const logs = ref<UpdateLogs | null>(null)
const logsOpen = ref(false)
const logsLoading = ref(false)
const logsMessage = ref('')
const logEntries = computed(() => logs.value?.requestId === job.value?.requestId ? logs.value?.logs || [] : [])
let openedFailureId: string | undefined
let stopped = false
let generation = 0
let timer: ReturnType<typeof setTimeout> | undefined
@@ -55,6 +63,28 @@ export const useSelfhostUpdates = () => {
message.value = error?.data?.error?.message || 'Der Update-Dienst ist nicht erreichbar. Dienst und Socket-Einbindung auf dem Host prüfen.'
}
}
const refreshLogs = async () => {
const id = job.value?.requestId
if (!logsOpen.value || !id || stopped || accessDenied.value || logsLoading.value) return
if (logs.value?.requestId !== id) { logs.value = null; logsMessage.value = '' }
if (!overview.value?.capabilities?.requestLogs) {
logsMessage.value = 'Dieser Host-Update-Dienst unterstützt die Protokollansicht noch nicht. Die zusammengehörigen Host-Update-Dateien aktualisieren und den Dienstinstaller erneut ausführen. Bis dahin liegen die Protokolle auf dem Host unter .updates/.'
return
}
const currentGeneration = generation
logsLoading.value = true
try {
const result = await api<UpdateLogs>(`requests/${encodeURIComponent(id)}/logs`)
if (stopped || currentGeneration !== generation || job.value?.requestId !== id) return
logs.value = result
logsMessage.value = ''
} catch (error: any) {
if (stopped || currentGeneration !== generation || job.value?.requestId !== id) return
logsMessage.value = error?.data?.error?.message || 'Das Update-Protokoll konnte nicht geladen werden. Die Abfrage wird automatisch wiederholt.'
if ([401, 403].includes(errorStatus(error))) { logs.value = null; report(error) }
} finally { logsLoading.value = false }
}
const toggleLogs = async () => { logsOpen.value = !logsOpen.value; await refreshLogs() }
const refresh = async () => {
if (refreshing.value || stopped || accessDenied.value) return
refreshing.value = true
@@ -81,6 +111,11 @@ export const useSelfhostUpdates = () => {
connected.value = true
message.value = pending.value ? 'Start noch nicht bestätigt. Du kannst denselben Auftrag erneut senden.' : ''
persist()
if (['failed', 'interrupted'].includes(job.value?.status || '') && openedFailureId !== job.value?.requestId) {
openedFailureId = job.value?.requestId
logsOpen.value = true
}
await refreshLogs()
} catch (error) {
if (currentGeneration !== generation || stopped) return
connected.value = false
@@ -90,6 +125,8 @@ export const useSelfhostUpdates = () => {
const sendPending = async () => {
if (!pending.value || submitting.value || accessDenied.value) return
generation++
logs.value = null
logsMessage.value = ''
submitting.value = true
const submission = pending.value
try {
@@ -109,6 +146,7 @@ export const useSelfhostUpdates = () => {
report(error)
}
} finally { submitting.value = false; persist() }
await refreshLogs()
}
const check = async () => {
if (!available.value) return
@@ -145,5 +183,5 @@ export const useSelfhostUpdates = () => {
})
onBeforeUnmount(() => { stopped = true; clearTimeout(timer) })
return { overview, job, pending, connected, submitting, refreshing, message, accessDenied, recovery, busy, available, canApply,
check, apply, refresh, sendPending }
logsOpen, logsLoading, logsMessage, logEntries, refreshLogs, toggleLogs, check, apply, refresh, sendPending }
}