feat: SQLite persistence, non-blocking inference, modular frontend
Backend:
- Persist alert events and robot delivery counters in SQLite (data/app.db,
YOLO_DB_PATH override); EventStore keeps its interface, DeliveryStatsStore
uses read/write-through UPSERT; in-memory fallback remains for standalone
AlertManager use
- Serve /api/detect from a sync endpoint and serialize YOLO inference with a
lock, so concurrent requests no longer block the event loop
- Unify single-image and session detection: requests without session_id
share the fixed single-image session and run full frame confirmation
- Store UTC Z-suffixed timestamps for sortable string comparison; close
executor and database on shutdown via FastAPI lifespan
Frontend:
- Split app.js into ES modules (main.js + modules/{dom,api,ui,charts,store}
+ views/{dashboard,inspection,events,robots,robotCards}), no build step
- Escape all server data interpolated into innerHTML; guard missing
event.classes; replace lazy element ID list with memoized qs()
- Remove hardcoded fake stats (trend badge, device donut segment)
Docs:
- Rewrite README: accurate weight policy (only trained best.pt committed,
no Git LFS), SQLite persistence, single-worker note, data asset inventory
- Align models/pretrained/README.md with actual files; add YOLO_DB_PATH to
.env.example; add persistence unit tests; ruff clean
This commit is contained in:
77
frontend/views/events.js
Normal file
77
frontend/views/events.js
Normal file
@@ -0,0 +1,77 @@
|
||||
import { qs, escapeHtml, STATUS_LABELS, classLabel, formatDate, enabledChannelNames } from "../modules/dom.js";
|
||||
import { apiFetch } from "../modules/api.js";
|
||||
import { showToast, setServiceStatus } from "../modules/ui.js";
|
||||
import { renderTrend } from "../modules/charts.js";
|
||||
import { store } from "../modules/store.js";
|
||||
import { loadDashboard } from "./dashboard.js";
|
||||
|
||||
let eventFilter = "";
|
||||
|
||||
export async function loadEvents() {
|
||||
try {
|
||||
const query = eventFilter ? `?status=${eventFilter}` : "";
|
||||
const result = await apiFetch(`/api/events${query}`);
|
||||
store.eventData = result.events || [];
|
||||
renderEventTable(store.eventData);
|
||||
updateEventSummary(result.summary);
|
||||
renderTrend(store.eventData);
|
||||
setServiceStatus(true);
|
||||
} catch (error) {
|
||||
setServiceStatus(false);
|
||||
showToast(`无法读取告警事件:${error.message}`, "error");
|
||||
}
|
||||
}
|
||||
|
||||
function updateEventSummary(summary) {
|
||||
qs("#allEventsCount").textContent = String(summary.total);
|
||||
qs("#pendingEventsCount").textContent = String(summary.pending);
|
||||
qs("#ackEventsCount").textContent = String(summary.acknowledged);
|
||||
qs("#resolvedEventsCount").textContent = String(summary.resolved);
|
||||
qs("#pendingNavCount").textContent = String(summary.pending);
|
||||
qs("#notificationDot").hidden = summary.pending === 0;
|
||||
}
|
||||
|
||||
function renderEventTable(events) {
|
||||
qs("#eventTableMeta").textContent = `${events.length} 条记录`;
|
||||
if (!events.length) {
|
||||
qs("#eventTableBody").innerHTML = '<tr><td colspan="7" class="empty-cell">当前筛选条件下暂无告警事件</td></tr>';
|
||||
return;
|
||||
}
|
||||
qs("#eventTableBody").innerHTML = events.map((event) => {
|
||||
const classes = (event.classes || [])
|
||||
.map((name) => `<span class="type-tag type-${escapeHtml(name)}">${classLabel(name)}</span>`)
|
||||
.join(" ");
|
||||
const channels = enabledChannelNames(event.notification_channels).join(" + ") || "未发送";
|
||||
const statusLabel = STATUS_LABELS[event.status] || escapeHtml(event.status);
|
||||
return `<tr><td>${escapeHtml(event.id)}</td><td>${formatDate(event.created_at)}</td><td>${classes}</td><td>${Math.round(event.max_confidence * 100)}%</td><td>${channels}</td><td><span class="event-status status-${escapeHtml(event.status)}">${statusLabel}</span></td><td><div class="table-actions">${event.status === "pending" ? `<button class="table-action" data-event-id="${escapeHtml(event.id)}" data-event-status="acknowledged">确认</button>` : ""}${event.status !== "resolved" ? `<button class="table-action" data-event-id="${escapeHtml(event.id)}" data-event-status="resolved">解决</button>` : ""}</div></td></tr>`;
|
||||
}).join("");
|
||||
}
|
||||
|
||||
export async function updateEventStatus(eventId, status) {
|
||||
try {
|
||||
await apiFetch(`/api/events/${encodeURIComponent(eventId)}`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ status }),
|
||||
});
|
||||
showToast(`事件 ${eventId} 已更新为${STATUS_LABELS[status]}`, "success");
|
||||
await Promise.all([loadEvents(), loadDashboard()]);
|
||||
} catch (error) {
|
||||
showToast(`事件更新失败:${error.message}`, "error");
|
||||
}
|
||||
}
|
||||
|
||||
export function initEvents() {
|
||||
qs("#refreshEventsButton").addEventListener("click", loadEvents);
|
||||
qs("#eventTableBody").addEventListener("click", (event) => {
|
||||
const button = event.target.closest("[data-event-id]");
|
||||
if (button) updateEventStatus(button.dataset.eventId, button.dataset.eventStatus);
|
||||
});
|
||||
document.querySelectorAll("[data-event-filter]").forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
eventFilter = button.dataset.eventFilter;
|
||||
document.querySelectorAll("[data-event-filter]").forEach((item) => item.classList.toggle("is-active", item === button));
|
||||
loadEvents();
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user