diff --git a/frontend/src/api/admin/ops.ts b/frontend/src/api/admin/ops.ts index 5b6d202c..4ec560a4 100644 --- a/frontend/src/api/admin/ops.ts +++ b/frontend/src/api/admin/ops.ts @@ -54,6 +54,7 @@ export type OpsUpstreamErrorEvent = { account_name?: string upstream_status_code?: number upstream_request_id?: string + upstream_request_body?: string kind?: string message?: string detail?: string @@ -944,7 +945,9 @@ export async function getErrorDistribution( return data } -export async function listErrorLogs(params: { +export type OpsErrorListView = 'errors' | 'excluded' | 'all' + +export type OpsErrorListQueryParams = { page?: number page_size?: number time_range?: string @@ -958,10 +961,14 @@ export async function listErrorLogs(params: { error_owner?: string error_source?: string resolved?: string + view?: OpsErrorListView q?: string status_codes?: string -}): Promise { +} + +// Legacy unified endpoints +export async function listErrorLogs(params: OpsErrorListQueryParams): Promise { const { data } = await apiClient.get('/admin/ops/errors', { params }) return data } @@ -985,6 +992,50 @@ export async function updateErrorResolved(errorId: number, resolved: boolean): P await apiClient.put(`/admin/ops/errors/${errorId}/resolve`, { resolved }) } +// New split endpoints +export async function listRequestErrors(params: OpsErrorListQueryParams): Promise { + const { data } = await apiClient.get('/admin/ops/request-errors', { params }) + return data +} + +export async function listUpstreamErrors(params: OpsErrorListQueryParams): Promise { + const { data } = await apiClient.get('/admin/ops/upstream-errors', { params }) + return data +} + +export async function getRequestErrorDetail(id: number): Promise { + const { data } = await apiClient.get(`/admin/ops/request-errors/${id}`) + return data +} + +export async function getUpstreamErrorDetail(id: number): Promise { + const { data } = await apiClient.get(`/admin/ops/upstream-errors/${id}`) + return data +} + +export async function retryRequestErrorClient(id: number): Promise { + const { data } = await apiClient.post(`/admin/ops/request-errors/${id}/retry-client`, {}) + return data +} + +export async function retryRequestErrorUpstreamEvent(id: number, idx: number): Promise { + const { data } = await apiClient.post(`/admin/ops/request-errors/${id}/upstream-errors/${idx}/retry`, {}) + return data +} + +export async function retryUpstreamError(id: number): Promise { + const { data } = await apiClient.post(`/admin/ops/upstream-errors/${id}/retry`, {}) + return data +} + +export async function updateRequestErrorResolved(errorId: number, resolved: boolean): Promise { + await apiClient.put(`/admin/ops/request-errors/${errorId}/resolve`, { resolved }) +} + +export async function updateUpstreamErrorResolved(errorId: number, resolved: boolean): Promise { + await apiClient.put(`/admin/ops/upstream-errors/${errorId}/resolve`, { resolved }) +} + export async function listRequestDetails(params: OpsRequestDetailsParams): Promise { const { data } = await apiClient.get('/admin/ops/requests', { params }) return data @@ -1103,11 +1154,25 @@ export const opsAPI = { getAccountAvailabilityStats, getRealtimeTrafficSummary, subscribeQPS, + + // Legacy unified endpoints listErrorLogs, getErrorLogDetail, retryErrorRequest, listRetryAttempts, updateErrorResolved, + + // New split endpoints + listRequestErrors, + listUpstreamErrors, + getRequestErrorDetail, + getUpstreamErrorDetail, + retryRequestErrorClient, + retryRequestErrorUpstreamEvent, + retryUpstreamError, + updateRequestErrorResolved, + updateUpstreamErrorResolved, + listRequestDetails, listAlertRules, createAlertRule, diff --git a/frontend/src/views/admin/ops/OpsDashboard.vue b/frontend/src/views/admin/ops/OpsDashboard.vue index d059059d..f6f18f3d 100644 --- a/frontend/src/views/admin/ops/OpsDashboard.vue +++ b/frontend/src/views/admin/ops/OpsDashboard.vue @@ -94,7 +94,7 @@ @openErrorDetail="openError" /> - + @@ -263,12 +263,12 @@ -
+
- +
- {{ t('admin.ops.errorDetail.retryNote2') }} + pinned to original account_id
@@ -327,8 +327,20 @@
#{{ idx + 1 }} {{ ev.kind }}
-
- {{ ev.at_unix_ms ? formatDateTime(new Date(ev.at_unix_ms)) : '' }} +
+ +
+ {{ ev.at_unix_ms ? formatDateTime(new Date(ev.at_unix_ms)) : '' }} +
@@ -526,13 +538,14 @@ import { useI18n } from 'vue-i18n' import BaseDialog from '@/components/common/BaseDialog.vue' import ConfirmDialog from '@/components/common/ConfirmDialog.vue' import { useAppStore } from '@/stores' -import { opsAPI, type OpsErrorDetail, type OpsRetryMode, type OpsRetryAttempt } from '@/api/admin/ops' +import { opsAPI, type OpsErrorDetail, type OpsRetryAttempt } from '@/api/admin/ops' import { formatDateTime } from '@/utils/format' import { getSeverityClass } from '../utils/opsFormatters' interface Props { show: boolean errorId: number | null + errorType?: 'request' | 'upstream' } interface Emits { @@ -552,7 +565,7 @@ const activeTab = ref<'overview' | 'retries' | 'request' | 'response'>('overview const retrying = ref(false) const showRetryConfirm = ref(false) -const pendingRetryMode = ref('client') +const pendingRetryMode = ref<'client' | 'upstream' | 'upstream_event'>('client') const forceRetryAck = ref(false) const retryHistory = ref([]) @@ -563,12 +576,6 @@ const compareA = ref(null) const compareB = ref(null) const pinnedAccountIdInput = ref('') -const pinnedAccountId = computed(() => { - const raw = String(pinnedAccountIdInput.value || '').trim() - if (!raw) return null - const n = Number.parseInt(raw, 10) - return Number.isFinite(n) && n > 0 ? n : null -}) const title = computed(() => { if (!props.errorId) return 'Error Detail' @@ -584,6 +591,7 @@ type UpstreamErrorEvent = { account_name?: string upstream_status_code?: number upstream_request_id?: string + upstream_request_body?: string kind?: string message?: string detail?: string @@ -641,15 +649,12 @@ const handlingSuggestion = computed(() => { async function fetchDetail(id: number) { loading.value = true try { - const d = await opsAPI.getErrorLogDetail(id) + const kind = props.errorType || (detail.value?.phase === 'upstream' ? 'upstream' : 'request') + const d = kind === 'upstream' ? await opsAPI.getUpstreamErrorDetail(id) : await opsAPI.getRequestErrorDetail(id) detail.value = d - // Default pinned account from error log if present. - if (d.account_id && d.account_id > 0) { - pinnedAccountIdInput.value = String(d.account_id) - } else { - pinnedAccountIdInput.value = '' - } + // Keep showing original account_id (read-only hint for upstream retries). + pinnedAccountIdInput.value = d.account_id && d.account_id > 0 ? String(d.account_id) : '' } catch (err: any) { detail.value = null appStore.showError(err?.message || t('admin.ops.failedToLoadErrorDetail')) @@ -679,7 +684,7 @@ watch( { immediate: true } ) -function openRetryConfirm(mode: OpsRetryMode) { +function openRetryConfirm(mode: 'client' | 'upstream' | 'upstream_event') { pendingRetryMode.value = mode // Force-ack required only when backend says not retryable. forceRetryAck.value = false @@ -733,7 +738,12 @@ const responseTabHint = computed(() => { async function markResolved(resolved: boolean) { if (!props.errorId) return try { - await opsAPI.updateErrorResolved(props.errorId, resolved) + const kind = props.errorType || (detail.value?.phase === 'upstream' ? 'upstream' : 'request') + if (kind === 'upstream') { + await opsAPI.updateUpstreamErrorResolved(props.errorId, resolved) + } else { + await opsAPI.updateRequestErrorResolved(props.errorId, resolved) + } await fetchDetail(props.errorId) appStore.showSuccess(resolved ? (t('admin.ops.errorDetails.resolved') || 'Resolved') : (t('admin.ops.errorDetails.unresolved') || 'Unresolved')) } catch (err: any) { @@ -779,12 +789,20 @@ async function runConfirmedRetry() { retrying.value = true try { - const req = - mode === 'upstream' - ? { mode, pinned_account_id: pinnedAccountId.value ?? undefined, force: !retryable ? true : undefined } - : { mode, force: !retryable ? true : undefined } + const kind = props.errorType || (detail.value?.phase === 'upstream' ? 'upstream' : 'request') + + let res + if (kind === 'upstream') { + // Upstream error retries always pin the original account_id. + res = await opsAPI.retryUpstreamError(props.errorId) + } else { + if (mode === 'client') { + res = await opsAPI.retryRequestErrorClient(props.errorId) + } else { + throw new Error('Unsupported retry mode') + } + } - const res = await opsAPI.retryErrorRequest(props.errorId, req) const summary = res.status === 'succeeded' ? t('admin.ops.errorDetail.retrySuccess') : t('admin.ops.errorDetail.retryFailed') appStore.showSuccess(summary) @@ -798,6 +816,22 @@ async function runConfirmedRetry() { } } +async function retryUpstreamEvent(idx: number) { + if (!props.errorId) return + try { + retrying.value = true + const res = await opsAPI.retryRequestErrorUpstreamEvent(props.errorId, idx) + const summary = res.status === 'succeeded' ? t('admin.ops.errorDetail.retrySuccess') : t('admin.ops.errorDetail.retryFailed') + appStore.showSuccess(summary) + await fetchDetail(props.errorId) + await loadRetryHistory() + } catch (err: any) { + appStore.showError(err?.message || t('admin.ops.retryFailed')) + } finally { + retrying.value = false + } +} + function cancelRetry() { showRetryConfirm.value = false } diff --git a/frontend/src/views/admin/ops/components/OpsErrorDetailsModal.vue b/frontend/src/views/admin/ops/components/OpsErrorDetailsModal.vue index 0abe183a..4ff2ec0f 100644 --- a/frontend/src/views/admin/ops/components/OpsErrorDetailsModal.vue +++ b/frontend/src/views/admin/ops/components/OpsErrorDetailsModal.vue @@ -32,7 +32,9 @@ const q = ref('') const statusCode = ref(null) const phase = ref('') const errorOwner = ref('') -const resolvedStatus = ref('unresolved') + const resolvedStatus = ref('unresolved') + const viewMode = ref<'errors' | 'excluded' | 'all'>('errors') + const modalTitle = computed(() => { return props.errorType === 'upstream' ? t('admin.ops.errorDetails.upstreamErrors') : t('admin.ops.errorDetails.requestErrors') @@ -63,6 +65,14 @@ const resolvedSelectOptions = computed(() => { ] }) +const viewModeSelectOptions = computed(() => { + return [ + { value: 'errors', label: t('admin.ops.errorDetails.viewErrors') || 'errors' }, + { value: 'excluded', label: t('admin.ops.errorDetails.viewExcluded') || 'excluded' }, + { value: 'all', label: t('common.all') } + ] +}) + const phaseSelectOptions = computed(() => { const options = [ { value: '', label: t('common.all') }, @@ -88,7 +98,8 @@ async function fetchErrorLogs() { const params: Record = { page: page.value, page_size: pageSize.value, - time_range: props.timeRange + time_range: props.timeRange, + view: viewMode.value } const platform = String(props.platform || '').trim() @@ -109,7 +120,9 @@ async function fetchErrorLogs() { else if (resolvedVal === 'unresolved') params.resolved = 'false' // 'all' -> omit - const res = await opsAPI.listErrorLogs(params) + const res = props.errorType === 'upstream' + ? await opsAPI.listUpstreamErrors(params) + : await opsAPI.listRequestErrors(params) rows.value = res.items || [] total.value = res.total || 0 } catch (err) { @@ -121,15 +134,17 @@ async function fetchErrorLogs() { } } -function resetFilters() { - q.value = '' - statusCode.value = null - phase.value = props.errorType === 'upstream' ? 'upstream' : '' - errorOwner.value = '' - resolvedStatus.value = 'unresolved' - page.value = 1 - fetchErrorLogs() -} + function resetFilters() { + q.value = '' + statusCode.value = null + phase.value = props.errorType === 'upstream' ? 'upstream' : '' + errorOwner.value = '' + resolvedStatus.value = 'unresolved' + viewMode.value = 'errors' + page.value = 1 + fetchErrorLogs() + } + watch( () => props.show, @@ -172,7 +187,7 @@ watch( ) watch( - () => [statusCode.value, phase.value, errorOwner.value, resolvedStatus.value] as const, + () => [statusCode.value, phase.value, errorOwner.value, resolvedStatus.value, viewMode.value] as const, () => { if (!props.show) return page.value = 1 @@ -186,7 +201,7 @@ watch(
-
+
@@ -224,6 +239,10 @@ watch( +
+