feat(stability): unify readiness and recovery
CI - Build & Test / Backend (.NET) (push) Successful in 45s
CI - Build & Test / Backend integration (PostgreSQL/Toxiproxy) (push) Failing after 1m0s
CI - Build & Test / Frontend (Vue/TS) (push) Successful in 2m49s
CI - Build & Test / Security Check (push) Successful in 7s
CI - Build & Test / Deploy Nexus (push) Has been skipped

This commit is contained in:
AzuTear
2026-08-01 01:21:33 +02:00
parent 38282e4f7f
commit cd8c78d165
67 changed files with 2616 additions and 601 deletions
+155 -75
View File
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { onMounted, onUnmounted, ref, computed, watch } from 'vue'
import { onBeforeRouteLeave, useRoute, useRouter } from 'vue-router'
import { ArrowLeft, Bot, Loader2, AlertCircle, Activity, RefreshCw } from '@lucide/vue'
import { ArrowLeft, Bot, Activity, RefreshCw } from '@lucide/vue'
import { apiFetch } from '../services/api'
import {
applyAgentFileWrite,
@@ -22,6 +22,7 @@ import StandingOrdersEditor from '../components/config/StandingOrdersEditor.vue'
import { subscribeDomainEventState } from '../services/domainEvents'
import { createMutationRequestContext } from '../services/mutationContext'
import { useAuthStore } from '../stores/auth'
import AsyncStatePanel from '../components/mission-control/AsyncStatePanel.vue'
const route = useRoute()
const router = useRouter()
@@ -74,15 +75,6 @@ const configFiles = computed(() => filesQuery.data.value?.files ?? [])
const loading = computed(() => agentQuery.isPending.value)
const activityLoading = computed(() => activityQuery.isFetching.value)
const summaryLoading = computed(() => summaryQuery.isFetching.value)
const error = computed(() =>
agentQuery.error.value instanceof Error ? agentQuery.error.value.message : '',
)
const activityError = computed(() =>
activityQuery.error.value instanceof Error ? activityQuery.error.value.message : '',
)
const summaryError = computed(() =>
summaryQuery.error.value instanceof Error ? summaryQuery.error.value.message : '',
)
const currentFile = computed(() => {
if (!configFiles.value.length) return null
@@ -116,10 +108,7 @@ const configsError = computed(() => {
return cause instanceof Error ? cause.message : ''
})
const initLoading = computed(() =>
loading.value
|| activityQuery.isPending.value
|| summaryQuery.isPending.value
|| (canConfigure.value && filesQuery.isPending.value),
loading.value,
)
const fallbackName = computed(() => {
@@ -185,6 +174,23 @@ function scheduleActivityReload() {
}, 250)
}
function retryAgent() {
void agentQuery.refetch()
}
function retrySummary() {
void summaryQuery.refetch()
}
function retryActivity() {
void activityQuery.refetch()
}
function retryConfigs() {
void filesQuery.refetch()
if (activeTabFileName.value) void fileQuery.refetch()
}
function formatSummaryTimestamp(value?: string | null): string {
if (!value) return 'No timestamp'
const d = new Date(value)
@@ -316,12 +322,14 @@ async function saveFile() {
if (!response.ok) {
const err = await response.json().catch(() => ({}))
const problem = err as {
detail?: string
error?: string
message?: string
errors?: Record<string, string[]>
currentHash?: string
}
const detail = problem.message
const detail = problem.detail
|| problem.message
|| problem.error
|| Object.values(problem.errors ?? {}).flat().join(' ')
|| 'Failed to save file'
@@ -401,12 +409,30 @@ onUnmounted(() => {
Zurück zu Agents
</button>
<div v-if="initLoading" class="status-message">
<Loader2 :size="20" class="spin" />
Loading agent data...
</div>
<AsyncStatePanel
v-if="initLoading && !agent"
state="loading"
title="Agent wird geladen"
/>
<AsyncStatePanel
v-else-if="agentQuery.error.value && !agent"
state="error"
title="Agent nicht verfügbar"
:problem="agentQuery.error.value"
@action="retryAgent"
/>
<template v-else>
<AsyncStatePanel
v-if="agentQuery.error.value && agent"
state="stale"
title="Agentdaten möglicherweise veraltet"
:problem="agentQuery.error.value"
action-label="Aktualisieren"
compact
@action="retryAgent"
/>
<!-- Agent header -->
<div class="agent-header">
<div class="agent-avatar" :class="agentId">
@@ -426,12 +452,6 @@ onUnmounted(() => {
{{ formatLastSeen(agent.lastSeen) }}
</span>
</div>
<div v-if="error && !agent" class="agent-status-row">
<span class="status-label muted">
<AlertCircle :size="11" />
{{ error }}
</span>
</div>
</div>
</div>
@@ -455,15 +475,21 @@ onUnmounted(() => {
</button>
</header>
<div v-if="summaryLoading && !summary" class="status-message compact">
<Loader2 :size="16" class="spin" />
Loading summaries...
</div>
<AsyncStatePanel
v-if="summaryLoading && !summary"
state="loading"
title="Zusammenfassungen werden geladen"
compact
/>
<div v-else-if="summaryError && !summary" class="status-message compact error">
<AlertCircle :size="16" />
{{ summaryError }}
</div>
<AsyncStatePanel
v-else-if="summaryQuery.error.value && !summary"
state="error"
title="Zusammenfassungen nicht verfügbar"
:problem="summaryQuery.error.value"
compact
@action="retrySummary"
/>
<div v-else-if="summary" class="summary-row">
<div class="summary-card">
@@ -478,61 +504,115 @@ onUnmounted(() => {
</div>
</div>
<div v-else class="status-message compact">
No summary available.
</div>
<AsyncStatePanel
v-else
state="empty"
title="Keine Zusammenfassung"
message="OpenClaw hat für diesen Agenten noch keine bestätigte Zusammenfassung geliefert."
compact
/>
<div v-if="summaryError && summary" class="status-message compact error summary-inline-error">
<AlertCircle :size="14" />
{{ summaryError }}
</div>
<AsyncStatePanel
v-if="summaryQuery.error.value && summary"
state="stale"
title="Zusammenfassung möglicherweise veraltet"
:problem="summaryQuery.error.value"
action-label="Aktualisieren"
compact
@action="retrySummary"
/>
<div v-if="liveUnavailable" class="status-message compact">
Live stream reconnecting
</div>
<AsyncStatePanel
v-if="liveUnavailable"
state="stale"
title="Live-Stream wird neu verbunden"
message="Die letzte bestätigte Aktivität bleibt sichtbar."
action-label="Manuell aktualisieren"
compact
@action="refreshActivity"
/>
<div v-if="activityLoading && !activityItems.length" class="status-message compact">
<Loader2 :size="16" class="spin" />
Loading activity...
</div>
<AsyncStatePanel
v-if="activityLoading && !activityItems.length"
state="loading"
title="Aktivität wird geladen"
compact
/>
<div v-else-if="activityError" class="status-message compact error">
<AlertCircle :size="16" />
{{ activityError }}
</div>
<AsyncStatePanel
v-else-if="activityQuery.error.value && !activityItems.length"
state="error"
title="Aktivität nicht verfügbar"
:problem="activityQuery.error.value"
compact
@action="retryActivity"
/>
<div v-else-if="activityItems.length" class="thinking-list">
<article
v-for="item in activityItems"
:key="`${item.source}-${item.id ?? item.at}-${item.message}`"
class="thinking-item"
>
<div class="thinking-meta">
<span class="type-pill">{{ activityTypeLabel(item.type) }}</span>
<span>{{ formatActivityTime(item) }}</span>
</div>
<p>{{ item.message }}</p>
</article>
</div>
<template v-else-if="activityItems.length">
<AsyncStatePanel
v-if="activityQuery.error.value"
state="stale"
title="Aktivitätsliste möglicherweise veraltet"
:problem="activityQuery.error.value"
action-label="Aktualisieren"
compact
@action="retryActivity"
/>
<div class="thinking-list">
<article
v-for="item in activityItems"
:key="`${item.source}-${item.id ?? item.at}-${item.message}`"
class="thinking-item"
>
<div class="thinking-meta">
<span class="type-pill">{{ activityTypeLabel(item.type) }}</span>
<span>{{ formatActivityTime(item) }}</span>
</div>
<p>{{ item.message }}</p>
</article>
</div>
</template>
<div v-else class="status-message compact">
No recent activity.
</div>
<AsyncStatePanel
v-else
state="empty"
title="Keine aktuelle Aktivität"
message="Für diesen Agenten liegen noch keine bestätigten Events vor."
compact
/>
</section>
<!-- Config section -->
<div class="config-section">
<div v-if="configsLoading" class="status-message">
<Loader2 :size="16" class="spin" />
Loading config files...
</div>
<AsyncStatePanel
v-if="configsLoading && !configFiles.length"
state="loading"
title="Agent-Dateien werden geladen"
compact
/>
<div v-else-if="configsError" class="status-message error">
<AlertCircle :size="16" />
{{ configsError }}
</div>
<AsyncStatePanel
v-else-if="configsError && !configFiles.length"
state="error"
title="Agent-Dateien nicht verfügbar"
:message="configsError"
:problem="fileQuery.error.value ?? filesQuery.error.value"
:action-label="canConfigure ? 'Aktualisieren' : ''"
compact
@action="retryConfigs"
/>
<template v-else>
<AsyncStatePanel
v-if="configsError"
state="stale"
title="Agent-Dateiliste möglicherweise veraltet"
:message="configsError"
:problem="fileQuery.error.value ?? filesQuery.error.value"
action-label="Aktualisieren"
compact
@action="retryConfigs"
/>
<ConfigTabs
:tabs="configFiles.map(file => file.name)"
:active-tab="activeTab"