feat: implement T-019 reliable event ingress
Harness governance / validate (pull_request) Has been cancelled
Harness governance / validate (pull_request) Has been cancelled
This commit is contained in:
@@ -90,11 +90,23 @@
|
||||
.form-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
|
||||
.inline-state { min-height: 42px; padding: 9px 10px; border-left: 3px solid var(--cyan); background: rgba(56, 189, 248, .07); color: var(--muted); font-size: 12px; }
|
||||
.event-list { max-height: 330px; padding: 8px; display: grid; gap: 7px; overflow-y: auto; }
|
||||
.ingress-summary { padding: 11px 12px; display: grid; gap: 8px; border-bottom: 1px solid var(--line); background: rgba(56, 189, 248, .04); }
|
||||
.ingress-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
|
||||
.ingress-head h3 { margin: 0; font-size: 13px; }
|
||||
.ingress-state { min-height: 28px; padding: 3px 9px; display: inline-flex; align-items: center; gap: 7px; border: 1px solid var(--line); border-radius: 999px; color: var(--muted); font-size: 12px; font-weight: 750; }
|
||||
.status-dot.error { background: var(--danger); box-shadow: 0 0 0 4px rgba(251, 113, 133, .12); }
|
||||
.ingress-copy { margin: 0; color: var(--muted); font-size: 12px; overflow-wrap: anywhere; }
|
||||
.ingress-counts { margin: 0; display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 6px; }
|
||||
.ingress-counts[hidden] { display: none; }
|
||||
.ingress-counts div { min-width: 0; padding: 7px 8px; border: 1px solid var(--line); border-radius: 8px; background: var(--surface-2); }
|
||||
.ingress-counts dt { color: var(--muted); font-size: 11px; }
|
||||
.ingress-counts dd { margin: 2px 0 0; color: var(--text); font-size: 15px; font-weight: 800; font-variant-numeric: tabular-nums; }
|
||||
.event { padding: 11px; border: 1px solid var(--line); border-radius: 10px; background: var(--surface-2); }
|
||||
.event-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
||||
.event strong { font-size: 13px; }
|
||||
.event time, .event code { color: var(--muted); font-size: 12px; }
|
||||
.event code { display: block; margin-top: 6px; overflow-wrap: anywhere; }
|
||||
.event-delivery { margin-top: 5px; color: var(--muted); font-size: 12px; }
|
||||
.event-tag { padding: 2px 7px; border: 1px solid rgba(251, 191, 36, .35); border-radius: 999px; background: rgba(251, 191, 36, .08); color: #ffe18a; font-size: 11px; }
|
||||
.empty { min-height: 120px; padding: 26px 16px; display: grid; place-items: center; color: var(--muted); text-align: center; }
|
||||
.architecture-note { margin-top: 14px; padding: 13px 15px; display: grid; grid-template-columns: auto 1fr; gap: 11px; border: 1px solid rgba(139, 124, 246, .30); border-radius: 12px; background: rgba(139, 124, 246, .07); color: var(--muted); }
|
||||
@@ -127,6 +139,8 @@
|
||||
.metrics { grid-template-columns: 1fr 1fr; }
|
||||
.metric strong { font-size: 17px; }
|
||||
.form-actions { grid-template-columns: 1fr; }
|
||||
.ingress-head { align-items: flex-start; flex-direction: column; }
|
||||
.ingress-counts { grid-template-columns: 1fr 1fr; }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*, *::before, *::after { scroll-behavior: auto !important; transition-duration: .01ms !important; animation-duration: .01ms !important; animation-iteration-count: 1 !important; }
|
||||
@@ -155,7 +169,7 @@
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<h1>单路检测与区域判定</h1>
|
||||
<p>验证从画面、匿名人员框、track 到区域进入候选事实的工程闭环。平台事件 ID、持久投递和 Alert 由后续 Bell 链路负责。</p>
|
||||
<p>验证从画面、匿名人员框、track 到区域进入候选事实的工程闭环。Brain 先持久化到 Outbox,再由 Bell 分配平台事件 ID;Alert 仍由后续 Bell 链路负责。</p>
|
||||
</div>
|
||||
<div id="mode-badge" class="mode-badge">加载运行状态</div>
|
||||
</div>
|
||||
@@ -220,9 +234,22 @@
|
||||
|
||||
<section class="card" aria-labelledby="event-title">
|
||||
<div class="card-head">
|
||||
<div><h2 id="event-title">最近候选事实</h2><p>只产生 source_event_id;Bell 才生成平台 ULID</p></div>
|
||||
<div><h2 id="event-title">最近候选事实</h2><p>内存环仅用于观察;可靠性以 Outbox 状态为准</p></div>
|
||||
<span id="event-count" class="chip"><strong>0</strong> 项</span>
|
||||
</div>
|
||||
<section id="event-ingress" class="ingress-summary" aria-labelledby="event-ingress-title" role="status" aria-live="polite" aria-atomic="true">
|
||||
<div class="ingress-head">
|
||||
<h3 id="event-ingress-title">Bell 可靠投递</h3>
|
||||
<span class="ingress-state"><i id="ingress-dot" class="status-dot" aria-hidden="true"></i><span id="ingress-label">未启用</span></span>
|
||||
</div>
|
||||
<p id="ingress-copy" class="ingress-copy">可靠投递未启用;候选事实不会被标记为 Bell 已接收。</p>
|
||||
<dl id="ingress-counts" class="ingress-counts" aria-label="Outbox 投递计数" hidden>
|
||||
<div><dt>待投递</dt><dd id="ingress-queued">0</dd></div>
|
||||
<div><dt>投递中</dt><dd id="ingress-delivering">0</dd></div>
|
||||
<div><dt>已送达</dt><dd id="ingress-delivered">0</dd></div>
|
||||
<div><dt>死信</dt><dd id="ingress-dead-letter">0</dd></div>
|
||||
</dl>
|
||||
</section>
|
||||
<div id="event-list" class="event-list" aria-live="polite">
|
||||
<div class="empty">人员从区域外进入后,这里会出现一次候选事实。</div>
|
||||
</div>
|
||||
@@ -252,6 +279,7 @@
|
||||
let state = null;
|
||||
let offlineSequence = 0;
|
||||
let offlineTriggered = false;
|
||||
let ingressSignature = "";
|
||||
|
||||
const offlineState = () => {
|
||||
offlineSequence += 1;
|
||||
@@ -275,6 +303,7 @@
|
||||
zone: state && state.zone ? state.zone : {id: "zone-demo-01", name: "楼梯口危险区", version: 1, points: parsePoints(false)},
|
||||
detections: bbox[2] - bbox[0] > .01 ? [{track_id: "P-DEMO-001", class: "person", bbox, detector_score: null, inside_zone: inside}] : [],
|
||||
events,
|
||||
event_ingress: {enabled: false},
|
||||
last_error_code: null
|
||||
};
|
||||
};
|
||||
@@ -376,11 +405,48 @@
|
||||
const tag = document.createElement("span"); tag.className = "event-tag"; tag.textContent = event.fixture ? "夹具候选" : "检测候选";
|
||||
head.append(title, tag);
|
||||
const time = document.createElement("time"); time.dateTime = event.occurred_at; time.textContent = `${formatTime(event.occurred_at)} · ${event.kind}`;
|
||||
const delivery = document.createElement("div"); delivery.className = "event-delivery"; delivery.textContent = event.delivery_status === "outbox_persisted" ? "交接:Outbox 已持久化" : "交接:仅内存观察";
|
||||
const code = document.createElement("code"); code.textContent = event.source_event_id;
|
||||
article.append(head, time, code); return article;
|
||||
article.append(head, time, delivery, code); return article;
|
||||
}));
|
||||
}
|
||||
|
||||
function renderIngress(value = {enabled: false}) {
|
||||
const enabled = value.enabled === true;
|
||||
const queued = Number(value.queued) || 0;
|
||||
const delivering = Number(value.delivering) || 0;
|
||||
const delivered = Number(value.delivered) || 0;
|
||||
const deadLetter = Number(value.dead_letter) || 0;
|
||||
const errorCode = typeof value.last_error_code === "string" ? value.last_error_code : "";
|
||||
const nextSignature = JSON.stringify([enabled, queued, delivering, delivered, deadLetter, errorCode]);
|
||||
if (nextSignature === ingressSignature) return;
|
||||
ingressSignature = nextSignature;
|
||||
const dot = $("ingress-dot");
|
||||
const counts = $("ingress-counts");
|
||||
counts.hidden = !enabled;
|
||||
$("ingress-queued").textContent = String(queued);
|
||||
$("ingress-delivering").textContent = String(delivering);
|
||||
$("ingress-delivered").textContent = String(delivered);
|
||||
$("ingress-dead-letter").textContent = String(deadLetter);
|
||||
if (!enabled) {
|
||||
dot.className = "status-dot";
|
||||
$("ingress-label").textContent = "未启用";
|
||||
$("ingress-copy").textContent = "可靠投递未启用;候选事实不会被标记为 Bell 已接收。";
|
||||
} else if (deadLetter > 0) {
|
||||
dot.className = "status-dot error";
|
||||
$("ingress-label").textContent = "需要处理";
|
||||
$("ingress-copy").textContent = `存在死信${errorCode ? `(${errorCode})` : ""};请检查 Bell 绑定、鉴权或候选字段后再处理。`;
|
||||
} else if (errorCode || queued > 0 || delivering > 0) {
|
||||
dot.className = "status-dot warning";
|
||||
$("ingress-label").textContent = errorCode ? "正在重试" : "投递中";
|
||||
$("ingress-copy").textContent = errorCode ? `Outbox 已保留候选并自动重试(${errorCode})。` : "候选已持久化到本地 Outbox,正在等待 Bell 确认。";
|
||||
} else {
|
||||
dot.className = "status-dot online";
|
||||
$("ingress-label").textContent = "投递正常";
|
||||
$("ingress-copy").textContent = "Outbox 可用;Bell 返回 accepted 或 duplicate 后才计为已送达。";
|
||||
}
|
||||
}
|
||||
|
||||
function render(next) {
|
||||
state = next;
|
||||
$("connection-dot").className = `status-dot ${next.source.connected ? "online" : "warning"}`;
|
||||
@@ -398,6 +464,7 @@
|
||||
if (document.activeElement !== $("zone-name") && document.activeElement !== $("zone-points")) {
|
||||
$("zone-name").value = next.zone.name; setPoints(next.zone.points);
|
||||
}
|
||||
renderIngress(next.event_ingress);
|
||||
renderEvents(next.events);
|
||||
drawOverlay();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user