Refactor store dwell alert management API and dwell engine
- Updated argument parsing in manage_api.py to include new threshold parameters. - Enhanced _config_payload to include thresholds and webhook configurations. - Modified _build_summary to track queue metrics and adjust alert reporting. - Refactored DwellEngine to utilize queue thresholds for alerting and reporting. - Added queue metrics calculations and status change tracking in dwell_engine.py. - Updated notifier.py to support posting JSON events to webhooks. - Adjusted example configuration to reflect new threshold parameters. - Enhanced Docker entrypoint script for better process management. - Updated tests to cover new queue metrics and thresholds. - Improved ManagedServiceDetail and ManagedServices Vue components to display queue metrics.
This commit is contained in:
@@ -5,15 +5,13 @@
|
||||
<el-button link @click="router.push('/managed-services')">
|
||||
返回列表
|
||||
</el-button>
|
||||
<h3 class="page-title">{{ service?.display_name || route.params.id }}</h3>
|
||||
<h3 class="page-title">
|
||||
{{ service?.display_name || route.params.id }}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="toolbar-actions">
|
||||
<el-button @click="loadDetail" :loading="loading">刷新</el-button>
|
||||
<el-button
|
||||
type="warning"
|
||||
@click="handleRestart"
|
||||
:loading="restarting"
|
||||
>
|
||||
<el-button type="warning" @click="handleRestart" :loading="restarting">
|
||||
重启容器
|
||||
</el-button>
|
||||
<el-button type="primary" @click="openEdit">编辑 RTSP</el-button>
|
||||
@@ -96,6 +94,36 @@
|
||||
|
||||
<div v-if="service?.summary">
|
||||
<p class="summary-headline">{{ service.summary.headline }}</p>
|
||||
<div v-if="hasQueueMetrics" class="queue-summary-panel">
|
||||
<div class="queue-summary-card queue-summary-card--level">
|
||||
<span class="queue-summary-label">排队等级</span>
|
||||
<div class="queue-summary-main">
|
||||
<el-tag
|
||||
:type="queueLevelType(queueSummaryMetrics.queue_level)"
|
||||
>
|
||||
{{ queueLevelText(queueSummaryMetrics.queue_level) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
<div class="queue-summary-card">
|
||||
<span class="queue-summary-label">超 5 分钟人数</span>
|
||||
<span class="queue-summary-value">
|
||||
{{ queueSummaryMetrics.over_threshold_count ?? 0 }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="queue-summary-card">
|
||||
<span class="queue-summary-label">低于 5 分钟人数</span>
|
||||
<span class="queue-summary-value">
|
||||
{{ queueSummaryMetrics.under_threshold_count ?? 0 }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="queue-summary-card">
|
||||
<span class="queue-summary-label">状态变化</span>
|
||||
<span class="queue-summary-value queue-summary-value--text">
|
||||
{{ queueChangeText(queueSummaryMetrics.status_change) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<el-descriptions :column="1" border>
|
||||
<el-descriptions-item
|
||||
v-for="item in summaryEntries"
|
||||
@@ -150,6 +178,32 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="排队等级" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="queueLevelType(row.queue_level)">
|
||||
{{ queueLevelText(row.queue_level) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column
|
||||
prop="over_threshold_count"
|
||||
label="超 5 分钟人数"
|
||||
width="130"
|
||||
/>
|
||||
|
||||
<el-table-column
|
||||
prop="under_threshold_count"
|
||||
label="低于 5 分钟人数"
|
||||
width="130"
|
||||
/>
|
||||
|
||||
<el-table-column label="状态变化" min-width="120">
|
||||
<template #default="{ row }">
|
||||
{{ queueChangeText(row.status_change) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<template v-if="isStoreDwell">
|
||||
<el-table-column
|
||||
prop="active_customer_count"
|
||||
@@ -181,7 +235,9 @@
|
||||
<el-table-column prop="total_people" label="人流总数" width="110" />
|
||||
<el-table-column label="年龄分布" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<span class="break-all">{{ formatCounts(row.age_counts) }}</span>
|
||||
<span class="break-all">{{
|
||||
formatCounts(row.age_counts)
|
||||
}}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="男女人数" min-width="160">
|
||||
@@ -394,7 +450,11 @@ const previewFile = async (file) => {
|
||||
previewLines.value = [];
|
||||
|
||||
try {
|
||||
const res = await getManagedServicePreview(serviceId.value, file.path, 2000);
|
||||
const res = await getManagedServicePreview(
|
||||
serviceId.value,
|
||||
file.path,
|
||||
2000,
|
||||
);
|
||||
previewLines.value = res.lines || [];
|
||||
} catch (error) {
|
||||
ElMessage.error(error.message || "加载文件预览失败");
|
||||
@@ -404,14 +464,23 @@ const previewFile = async (file) => {
|
||||
};
|
||||
|
||||
const downloadFile = (file) => {
|
||||
window.open(getManagedServiceDownloadUrl(serviceId.value, file.path), "_blank");
|
||||
window.open(
|
||||
getManagedServiceDownloadUrl(serviceId.value, file.path),
|
||||
"_blank",
|
||||
);
|
||||
};
|
||||
|
||||
const summaryEntries = computed(() => {
|
||||
const metrics = service.value?.summary?.metrics || {};
|
||||
return Object.entries(metrics)
|
||||
.filter(
|
||||
([key]) => key !== "recent_window_stats" && key !== "all_window_stats",
|
||||
([key]) =>
|
||||
key !== "recent_window_stats" &&
|
||||
key !== "all_window_stats" &&
|
||||
key !== "queue_level" &&
|
||||
key !== "over_threshold_count" &&
|
||||
key !== "under_threshold_count" &&
|
||||
key !== "status_change",
|
||||
)
|
||||
.map(([key, value]) => ({
|
||||
key,
|
||||
@@ -419,6 +488,20 @@ const summaryEntries = computed(() => {
|
||||
}));
|
||||
});
|
||||
|
||||
const queueSummaryMetrics = computed(
|
||||
() => service.value?.summary?.metrics || {},
|
||||
);
|
||||
|
||||
const hasQueueMetrics = computed(() => {
|
||||
const metrics = queueSummaryMetrics.value;
|
||||
if (!metrics || typeof metrics !== "object") {
|
||||
return false;
|
||||
}
|
||||
return ["queue_level", "over_threshold_count", "under_threshold_count"].some(
|
||||
(key) => key in metrics,
|
||||
);
|
||||
});
|
||||
|
||||
const recentWindowStats = computed(() => {
|
||||
const stats = service.value?.summary?.metrics?.recent_window_stats;
|
||||
return Array.isArray(stats) ? stats : [];
|
||||
@@ -483,6 +566,35 @@ const formatCounts = (value) => {
|
||||
return entries.map(([key, count]) => `${key}: ${count}`).join(", ");
|
||||
};
|
||||
|
||||
const queueLevelText = (value) => {
|
||||
const map = {
|
||||
crowded: "人多",
|
||||
normal: "正常",
|
||||
few: "人少",
|
||||
};
|
||||
return map[value] || value || "-";
|
||||
};
|
||||
|
||||
const queueLevelType = (value) => {
|
||||
const map = {
|
||||
crowded: "danger",
|
||||
normal: "warning",
|
||||
few: "success",
|
||||
};
|
||||
return map[value] || "info";
|
||||
};
|
||||
|
||||
const queueChangeText = (value) => {
|
||||
const map = {
|
||||
initial: "初始窗口",
|
||||
unchanged: "无变化",
|
||||
queue_increased: "人数变多",
|
||||
queue_decreased: "人数变少",
|
||||
queue_normalized: "人数变正常",
|
||||
};
|
||||
return map[value] || value || "-";
|
||||
};
|
||||
|
||||
const formatTime = (value) => {
|
||||
if (!value) {
|
||||
return "-";
|
||||
@@ -578,6 +690,50 @@ watch(serviceId, loadDetail, { immediate: true });
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.queue-summary-panel {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.queue-summary-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 14px 16px;
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(180deg, #fbfbf5 0%, #f6f7f1 100%);
|
||||
}
|
||||
|
||||
.queue-summary-card--level {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.queue-summary-label {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.queue-summary-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
.queue-summary-value {
|
||||
color: var(--el-text-color-primary);
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.queue-summary-value--text {
|
||||
font-size: 15px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.error-tags {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -617,4 +773,16 @@ watch(serviceId, loadDetail, { immediate: true });
|
||||
.break-all {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.queue-summary-panel {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.queue-summary-panel {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -46,13 +46,47 @@
|
||||
<span class="section-label">摘要</span>
|
||||
<div class="summary-text">
|
||||
{{
|
||||
service.summary?.headline ||
|
||||
service.result_error ||
|
||||
"暂无摘要"
|
||||
service.summary?.headline || service.result_error || "暂无摘要"
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="hasQueueMetrics(service.summary?.metrics)"
|
||||
class="service-section queue-section"
|
||||
>
|
||||
<span class="section-label">排队概览</span>
|
||||
<div class="queue-metrics-grid">
|
||||
<div class="queue-metric-item queue-metric-item--highlight">
|
||||
<span class="queue-metric-label">排队等级</span>
|
||||
<el-tag
|
||||
size="small"
|
||||
:type="queueLevelType(service.summary.metrics.queue_level)"
|
||||
>
|
||||
{{ queueLevelText(service.summary.metrics.queue_level) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="queue-metric-item">
|
||||
<span class="queue-metric-label">超 5 分钟</span>
|
||||
<span class="queue-metric-value">
|
||||
{{ service.summary.metrics.over_threshold_count ?? 0 }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="queue-metric-item">
|
||||
<span class="queue-metric-label">低于 5 分钟</span>
|
||||
<span class="queue-metric-value">
|
||||
{{ service.summary.metrics.under_threshold_count ?? 0 }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="queue-metric-item">
|
||||
<span class="queue-metric-label">状态变化</span>
|
||||
<span class="queue-metric-value queue-metric-value--text">
|
||||
{{ queueChangeText(service.summary.metrics.status_change) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="
|
||||
service.config_error ||
|
||||
@@ -236,6 +270,44 @@ const formatTime = (value) => {
|
||||
return date.toLocaleString();
|
||||
};
|
||||
|
||||
const hasQueueMetrics = (metrics) => {
|
||||
if (!metrics || typeof metrics !== "object") {
|
||||
return false;
|
||||
}
|
||||
return ["queue_level", "over_threshold_count", "under_threshold_count"].some(
|
||||
(key) => key in metrics,
|
||||
);
|
||||
};
|
||||
|
||||
const queueLevelText = (value) => {
|
||||
const map = {
|
||||
crowded: "人多",
|
||||
normal: "正常",
|
||||
few: "人少",
|
||||
};
|
||||
return map[value] || value || "-";
|
||||
};
|
||||
|
||||
const queueLevelType = (value) => {
|
||||
const map = {
|
||||
crowded: "danger",
|
||||
normal: "warning",
|
||||
few: "success",
|
||||
};
|
||||
return map[value] || "info";
|
||||
};
|
||||
|
||||
const queueChangeText = (value) => {
|
||||
const map = {
|
||||
initial: "初始窗口",
|
||||
unchanged: "无变化",
|
||||
queue_increased: "人数变多",
|
||||
queue_decreased: "人数变少",
|
||||
queue_normalized: "人数变正常",
|
||||
};
|
||||
return map[value] || value || "-";
|
||||
};
|
||||
|
||||
onMounted(loadServices);
|
||||
</script>
|
||||
|
||||
@@ -297,6 +369,49 @@ onMounted(loadServices);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.queue-section {
|
||||
padding: 12px;
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 10px;
|
||||
background: linear-gradient(180deg, #fcfcf7 0%, #f7f9f4 100%);
|
||||
}
|
||||
|
||||
.queue-metrics-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.queue-metric-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, 0.78);
|
||||
}
|
||||
|
||||
.queue-metric-item--highlight {
|
||||
border: 1px solid rgba(140, 120, 56, 0.16);
|
||||
}
|
||||
|
||||
.queue-metric-label {
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.queue-metric-value {
|
||||
color: #303133;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.queue-metric-value--text {
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.service-errors {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
@@ -309,4 +424,10 @@ onMounted(loadServices);
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.queue-metrics-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user