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
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:
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user