diff --git a/frontend/src/views/admin/ops/OpsDashboard.vue b/frontend/src/views/admin/ops/OpsDashboard.vue index e8fedc5a..15f269c2 100644 --- a/frontend/src/views/admin/ops/OpsDashboard.vue +++ b/frontend/src/views/admin/ops/OpsDashboard.vue @@ -24,6 +24,7 @@ :query-mode="queryMode" :loading="loading" :last-updated="lastUpdated" + :thresholds="metricThresholds" @update:time-range="onTimeRangeChange" @update:platform="onPlatformChange" @update:group="onGroupChange" @@ -75,7 +76,7 @@ - + @@ -121,7 +122,8 @@ import { type OpsErrorDistributionResponse, type OpsErrorTrendResponse, type OpsLatencyHistogramResponse, - type OpsThroughputTrendResponse + type OpsThroughputTrendResponse, + type OpsMetricThresholds } from '@/api/admin/ops' import { useAdminSettingsStore, useAppStore } from '@/stores' import OpsDashboardHeader from './components/OpsDashboardHeader.vue' @@ -314,6 +316,7 @@ const syncQueryToRoute = useDebounceFn(async () => { }, 250) const overview = ref(null) +const metricThresholds = ref(null) const throughputTrend = ref(null) const loadingTrend = ref(false) @@ -376,6 +379,11 @@ function onTimeRangeChange(v: string | number | boolean | null) { timeRange.value = v as TimeRange } +function onSettingsSaved() { + loadThresholds() + fetchData() +} + function onPlatformChange(v: string | number | boolean | null) { platform.value = typeof v === 'string' ? v : '' } @@ -615,6 +623,9 @@ onMounted(async () => { return } + // Load thresholds configuration + loadThresholds() + if (adminSettingsStore.opsRealtimeMonitoringEnabled) { startQPSSubscription() } else { @@ -626,6 +637,16 @@ onMounted(async () => { } }) +async function loadThresholds() { + try { + const settings = await opsAPI.getAlertRuntimeSettings() + metricThresholds.value = settings.thresholds || null + } catch (err) { + console.warn('[OpsDashboard] Failed to load thresholds', err) + metricThresholds.value = null + } +} + onUnmounted(() => { stopQPSSubscription() abortDashboardFetch() diff --git a/frontend/src/views/admin/ops/components/OpsDashboardHeader.vue b/frontend/src/views/admin/ops/components/OpsDashboardHeader.vue index ccb5dac7..d6f0025a 100644 --- a/frontend/src/views/admin/ops/components/OpsDashboardHeader.vue +++ b/frontend/src/views/admin/ops/components/OpsDashboardHeader.vue @@ -4,8 +4,9 @@ import { useI18n } from 'vue-i18n' import Select from '@/components/common/Select.vue' import HelpTooltip from '@/components/common/HelpTooltip.vue' import BaseDialog from '@/components/common/BaseDialog.vue' +import Icon from '@/components/icons/Icon.vue' import { adminAPI } from '@/api' -import type { OpsDashboardOverview, OpsWSStatus } from '@/api/admin/ops' +import type { OpsDashboardOverview, OpsWSStatus, OpsMetricThresholds } from '@/api/admin/ops' import type { OpsRequestDetailsPreset } from './OpsRequestDetailsModal.vue' import { formatNumber } from '@/utils/format' @@ -24,6 +25,7 @@ interface Props { queryMode: string loading: boolean lastUpdated: Date | null + thresholds?: OpsMetricThresholds | null // 阈值配置 } interface Emits { @@ -143,6 +145,42 @@ function getLatencyColor(ms: number | null | undefined): string { return 'text-red-600 dark:text-red-400' } +// --- Threshold checking helpers --- +function isSLABelowThreshold(slaPercent: number | null): boolean { + if (slaPercent == null) return false + const threshold = props.thresholds?.sla_percent_min + if (threshold == null) return false + return slaPercent < threshold +} + +function isLatencyAboveThreshold(latencyP99Ms: number | null): boolean { + if (latencyP99Ms == null) return false + const threshold = props.thresholds?.latency_p99_ms_max + if (threshold == null) return false + return latencyP99Ms > threshold +} + +function isTTFTAboveThreshold(ttftP99Ms: number | null): boolean { + if (ttftP99Ms == null) return false + const threshold = props.thresholds?.ttft_p99_ms_max + if (threshold == null) return false + return ttftP99Ms > threshold +} + +function isRequestErrorRateAboveThreshold(errorRatePercent: number | null): boolean { + if (errorRatePercent == null) return false + const threshold = props.thresholds?.request_error_rate_percent_max + if (threshold == null) return false + return errorRatePercent > threshold +} + +function isUpstreamErrorRateAboveThreshold(upstreamErrorRatePercent: number | null): boolean { + if (upstreamErrorRatePercent == null) return false + const threshold = props.thresholds?.upstream_error_rate_percent_max + if (threshold == null) return false + return upstreamErrorRatePercent > threshold +} + // --- Realtime / Overview labels --- const totalRequestsLabel = computed(() => formatNumber(overview.value?.request_count_total ?? 0)) @@ -818,8 +856,9 @@ function openJobsDetails() { class="pointer-events-none absolute left-1/2 top-full z-50 mt-2 w-72 -translate-x-1/2 opacity-0 transition-opacity duration-200 group-hover:pointer-events-auto group-hover:opacity-100 md:left-full md:top-0 md:ml-2 md:mt-0 md:translate-x-0" >
-

- 🧠 {{ t('admin.ops.diagnosis.title') }} +

+ + {{ t('admin.ops.diagnosis.title') }}

@@ -850,8 +889,9 @@ function openJobsDetails() {
{{ item.message }}
{{ item.impact }}
-
- 💡 {{ item.action }} +
+ + {{ item.action }}
@@ -1061,7 +1101,7 @@ function openJobsDetails() {
SLA - +
-
+
{{ slaPercent == null ? '-' : `${slaPercent.toFixed(3)}%` }}
-
+
@@ -1101,7 +1141,7 @@ function openJobsDetails() {
-
+
{{ durationP99Ms ?? '-' }}
ms (P99) @@ -1151,7 +1191,7 @@ function openJobsDetails() {
-
+
{{ ttftP99Ms ?? '-' }}
ms (P99) @@ -1196,7 +1236,7 @@ function openJobsDetails() { {{ t('admin.ops.requestDetails.details') }}
-
+
{{ errorRatePercent == null ? '-' : `${errorRatePercent.toFixed(2)}%` }}
@@ -1222,7 +1262,7 @@ function openJobsDetails() { {{ t('admin.ops.requestDetails.details') }}
-
+
{{ upstreamErrorRatePercent == null ? '-' : `${upstreamErrorRatePercent.toFixed(2)}%` }}