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
58 lines
1.4 KiB
JavaScript
58 lines
1.4 KiB
JavaScript
const elementCache = new Map();
|
|
|
|
export function qs(selector) {
|
|
if (!elementCache.has(selector)) {
|
|
elementCache.set(selector, document.querySelector(selector));
|
|
}
|
|
return elementCache.get(selector);
|
|
}
|
|
|
|
export function escapeHtml(value) {
|
|
return String(value)
|
|
.replaceAll("&", "&")
|
|
.replaceAll("<", "<")
|
|
.replaceAll(">", ">")
|
|
.replaceAll('"', """)
|
|
.replaceAll("'", "'");
|
|
}
|
|
|
|
export const VIEW_LABELS = {
|
|
overview: "系统总览",
|
|
inspection: "视频巡检",
|
|
events: "告警中心",
|
|
robots: "消息机器人",
|
|
risks: "风险台账",
|
|
settings: "系统设置",
|
|
};
|
|
|
|
export const STATUS_LABELS = {
|
|
pending: "待处置",
|
|
acknowledged: "已确认",
|
|
resolved: "已解决",
|
|
};
|
|
|
|
const CLASS_LABELS = { fire: "火焰", smoke: "烟雾" };
|
|
|
|
export function classLabel(name) {
|
|
return CLASS_LABELS[name] || escapeHtml(String(name));
|
|
}
|
|
|
|
export function formatDate(value) {
|
|
if (!value) return "--";
|
|
return new Date(value).toLocaleString("zh-CN", { hour12: false });
|
|
}
|
|
|
|
export function formatTime(seconds) {
|
|
if (!Number.isFinite(seconds)) return "00:00";
|
|
const minutes = Math.floor(seconds / 60);
|
|
const remaining = Math.floor(seconds % 60);
|
|
return `${String(minutes).padStart(2, "0")}:${String(remaining).padStart(2, "0")}`;
|
|
}
|
|
|
|
export function enabledChannelNames(channels = {}) {
|
|
const names = [];
|
|
if (channels.wechat) names.push("企业微信");
|
|
if (channels.feishu) names.push("飞书");
|
|
return names;
|
|
}
|