Commit c5aa96a3 authored by Ethan0x0000's avatar Ethan0x0000
Browse files

feat(frontend): display error observability fields in ops admin panel

Show endpoint, model mapping, and request type in the ops error log
table and detail modal:
- Endpoint column with inbound/upstream tooltip
- Model column showing requested→upstream mapping with arrow
- Request type badge (sync/stream/ws) in status column
- New detail cards for inbound endpoint, upstream endpoint, request type
parent d927c0e4
...@@ -969,6 +969,10 @@ export interface OpsErrorLog { ...@@ -969,6 +969,10 @@ export interface OpsErrorLog {
client_ip?: string | null client_ip?: string | null
request_path?: string request_path?: string
stream?: boolean stream?: boolean
// Model mapping context for ops error observability
requested_model?: string
upstream_model?: string
} }
export interface OpsErrorDetail extends OpsErrorLog { export interface OpsErrorDetail extends OpsErrorLog {
......
...@@ -59,7 +59,14 @@ ...@@ -59,7 +59,14 @@
<div class="rounded-xl bg-gray-50 p-4 dark:bg-dark-900"> <div class="rounded-xl bg-gray-50 p-4 dark:bg-dark-900">
<div class="text-xs font-bold uppercase tracking-wider text-gray-400">{{ t('admin.ops.errorDetail.model') }}</div> <div class="text-xs font-bold uppercase tracking-wider text-gray-400">{{ t('admin.ops.errorDetail.model') }}</div>
<div class="mt-1 text-sm font-medium text-gray-900 dark:text-white"> <div class="mt-1 text-sm font-medium text-gray-900 dark:text-white">
{{ detail.model || '—' }} <template v-if="hasModelMapping(detail)">
<span class="font-mono">{{ detail.requested_model }}</span>
<span class="mx-1 text-gray-400"></span>
<span class="font-mono text-primary-600 dark:text-primary-400">{{ detail.upstream_model }}</span>
</template>
<template v-else>
{{ displayModel(detail) || '' }}
</template>
</div> </div>
</div> </div>
...@@ -213,6 +220,22 @@ function isUpstreamError(d: OpsErrorDetail | null): boolean { ...@@ -213,6 +220,22 @@ function isUpstreamError(d: OpsErrorDetail | null): boolean {
return phase === 'upstream' && owner === 'provider' return phase === 'upstream' && owner === 'provider'
} }
function hasModelMapping(d: OpsErrorDetail | null): boolean {
if (!d) return false
const requested = String(d.requested_model || '').trim()
const upstream = String(d.upstream_model || '').trim()
return !!requested && !!upstream && requested !== upstream
}
function displayModel(d: OpsErrorDetail | null): string {
if (!d) return ''
const upstream = String(d.upstream_model || '').trim()
if (upstream) return upstream
const requested = String(d.requested_model || '').trim()
if (requested) return requested
return String(d.model || '').trim()
}
const correlatedUpstream = ref<OpsErrorDetail[]>([]) const correlatedUpstream = ref<OpsErrorDetail[]>([])
const correlatedUpstreamLoading = ref(false) const correlatedUpstreamLoading = ref(false)
......
...@@ -83,11 +83,22 @@ ...@@ -83,11 +83,22 @@
<!-- Model --> <!-- Model -->
<td class="px-4 py-2"> <td class="px-4 py-2">
<div class="max-w-[120px] truncate" :title="log.model"> <div class="max-w-[180px]">
<span v-if="log.model" class="font-mono text-[11px] text-gray-700 dark:text-gray-300"> <template v-if="hasModelMapping(log)">
{{ log.model }} <el-tooltip :content="modelMappingTooltip(log)" placement="top" :show-after="500">
</span> <span class="flex items-center gap-1 truncate font-mono text-[11px] text-gray-700 dark:text-gray-300">
<span v-else class="text-xs text-gray-400">-</span> <span class="truncate">{{ log.requested_model }}</span>
<span class="flex-shrink-0 text-gray-400"></span>
<span class="truncate text-primary-600 dark:text-primary-400">{{ log.upstream_model }}</span>
</span>
</el-tooltip>
</template>
<template v-else>
<span v-if="displayModel(log)" class="truncate font-mono text-[11px] text-gray-700 dark:text-gray-300" :title="displayModel(log)">
{{ displayModel(log) }}
</span>
<span v-else class="text-xs text-gray-400">-</span>
</template>
</div> </div>
</td> </td>
...@@ -193,6 +204,28 @@ function isUpstreamRow(log: OpsErrorLog): boolean { ...@@ -193,6 +204,28 @@ function isUpstreamRow(log: OpsErrorLog): boolean {
return phase === 'upstream' && owner === 'provider' return phase === 'upstream' && owner === 'provider'
} }
function hasModelMapping(log: OpsErrorLog): boolean {
const requested = String(log.requested_model || '').trim()
const upstream = String(log.upstream_model || '').trim()
return !!requested && !!upstream && requested !== upstream
}
function modelMappingTooltip(log: OpsErrorLog): string {
const requested = String(log.requested_model || '').trim()
const upstream = String(log.upstream_model || '').trim()
if (!requested && !upstream) return ''
if (requested && upstream) return `${requested}${upstream}`
return upstream || requested
}
function displayModel(log: OpsErrorLog): string {
const upstream = String(log.upstream_model || '').trim()
if (upstream) return upstream
const requested = String(log.requested_model || '').trim()
if (requested) return requested
return String(log.model || '').trim()
}
function getTypeBadge(log: OpsErrorLog): { label: string; className: string } { function getTypeBadge(log: OpsErrorLog): { label: string; className: string } {
const phase = String(log.phase || '').toLowerCase() const phase = String(log.phase || '').toLowerCase()
const owner = String(log.error_owner || '').toLowerCase() const owner = String(log.error_owner || '').toLowerCase()
...@@ -263,4 +296,4 @@ function formatSmartMessage(msg: string): string { ...@@ -263,4 +296,4 @@ function formatSmartMessage(msg: string): string {
return msg.length > 200 ? msg.substring(0, 200) + '...' : msg return msg.length > 200 ? msg.substring(0, 200) + '...' : msg
} }
</script> </script>
\ No newline at end of file
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment