Files
yolo/frontend/index.html
Kunpeng 25fa0c5825 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
2026-08-13 16:50:23 +08:00

212 lines
21 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#0b1117" />
<title>燧安 · 火灾预防管理平台</title>
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='12' fill='%23153a39'/%3E%3Cpath d='M34 8c3 12-8 15-3 25 2-5 7-8 10-13 8 9 12 18 8 27-4 9-14 13-23 10C12 53 9 40 15 30c4-7 10-11 12-22 4 4 6 8 7 12z' fill='%2341d6c3'/%3E%3C/svg%3E" />
<link rel="stylesheet" href="./styles.css" />
</head>
<body>
<div class="platform-shell">
<aside class="sidebar" id="sidebar">
<div class="brand">
<div class="brand-mark" aria-hidden="true"></div>
<div>
<strong>燧安</strong>
<span>火灾预防管理平台</span>
</div>
</div>
<nav class="main-nav" aria-label="主导航">
<button class="nav-item is-active" type="button" data-view="overview"><span class="nav-icon"></span><span>系统总览</span></button>
<button class="nav-item" type="button" data-view="inspection"><span class="nav-icon"></span><span>视频巡检</span></button>
<button class="nav-item" type="button" data-view="events"><span class="nav-icon">!</span><span>告警中心</span><span class="nav-count" id="pendingNavCount">0</span></button>
<button class="nav-item" type="button" data-view="robots"><span class="nav-icon"></span><span>消息机器人</span><span class="nav-count nav-count-cyan" id="robotNavCount">0</span></button>
<button class="nav-item" type="button" data-view="risks"><span class="nav-icon"></span><span>风险台账</span></button>
<button class="nav-item" type="button" data-view="settings"><span class="nav-icon"></span><span>系统设置</span></button>
</nav>
<div class="sidebar-footer">
<div class="system-indicator"><span id="sidebarStatusDot" class="status-dot"></span><div><strong id="sidebarStatus">连接中</strong><span>智能视觉预警服务</span></div></div>
<span class="version">YOLO11S · v0.1.0</span>
</div>
</aside>
<main class="main-area">
<header class="topbar">
<div class="topbar-left">
<button id="menuButton" class="icon-button menu-button" type="button" aria-label="展开导航"></button>
<div>
<p class="breadcrumb">消防安全 / <span id="viewBreadcrumb">系统总览</span></p>
<h1 id="viewTitle">系统总览</h1>
</div>
</div>
<div class="topbar-actions">
<div class="clock-block"><span>当前时间</span><strong id="currentClock">--:--:--</strong></div>
<span id="globalStatus" class="status-chip status-checking">服务检查中</span>
<button class="icon-button" type="button" data-view-jump="events" aria-label="查看告警"><span id="notificationDot" class="notification-dot" hidden></span></button>
</div>
</header>
<div class="content-area">
<section class="view is-active" id="view-overview" data-view-panel="overview">
<div class="hero-strip command-hero">
<div>
<p class="section-kicker">FIRE PREVENTION COMMAND CENTER</p>
<h2>全域消防安全态势</h2>
<p id="overviewMessage">正在汇总模型、告警与通知通道状态。</p>
</div>
<div class="hero-actions"><span class="live-pill"><i></i>实时监测中</span><button class="primary-button" type="button" data-view-jump="inspection">开始视频巡检</button></div>
</div>
<div class="stat-grid">
<article class="stat-card"><div class="stat-head"><span>今日告警</span><span class="stat-symbol warning">!</span></div><strong id="todayEventCount">0</strong><small>系统产生的有效告警事件</small></article>
<article class="stat-card"><div class="stat-head"><span>待处置事件</span><span class="stat-symbol danger"></span></div><strong id="pendingEventCount">0</strong><small>需要值班人员确认处理</small></article>
<article class="stat-card"><div class="stat-head"><span>模型状态</span><span class="stat-symbol success"></span></div><strong id="modelStatusValue" class="text-value">检查中</strong><small id="modelStatusDetail">正在读取权重状态</small></article>
<article class="stat-card"><div class="stat-head"><span>消息机器人</span><span class="stat-symbol info"></span></div><strong id="onlineRobotCount">0</strong><small id="robotSummary">正在读取通知通道</small></article>
</div>
<div class="dashboard-command-grid">
<article class="panel safety-score-panel">
<div class="panel-header"><div><p class="section-kicker">SAFETY INDEX</p><h3>综合安全指数</h3></div></div>
<div class="safety-score-body"><div id="safetyGauge" class="safety-gauge" style="--score: 86"><div><strong id="safetyScore">86</strong><span>安全</span></div></div><div class="score-breakdown"><div><span>机器人可用率</span><strong id="deviceOnlineRate">--</strong></div><div><span>事件闭环率</span><strong id="eventClosureRate">--</strong></div><div><span>通知覆盖率</span><strong id="notificationCoverage">--</strong></div></div></div>
</article>
<article class="panel message-flow-panel">
<div class="panel-header"><div><p class="section-kicker">MESSAGE DELIVERY</p><h3>告警消息触达链路</h3></div><button class="text-button" type="button" data-view-jump="robots">管理机器人 →</button></div>
<div class="message-flow" aria-label="告警消息触达流程">
<div class="flow-node source"><span>01</span><strong>烟火识别</strong><small>YOLO 实时推理</small></div>
<i class="flow-connector"></i>
<div class="flow-node policy"><span>02</span><strong>告警确认</strong><small id="overviewPolicyText">连续帧策略</small></div>
<i class="flow-connector split"></i>
<div class="flow-destinations"><div class="flow-node channel"><span class="wechat-dot"></span><strong>企业微信</strong><small id="overviewWechatState">检查中</small></div><div class="flow-node channel"><span class="feishu-dot"></span><strong>飞书</strong><small id="overviewFeishuState">检查中</small></div></div>
</div>
<div class="flow-foot"><span>检测结果只在达到确认阈值后发送</span><strong id="overviewDeliveryCount">累计成功 0 次</strong></div>
</article>
<article class="panel risk-donut-panel">
<div class="panel-header"><div><p class="section-kicker">RISK MIX</p><h3>风险类型分布</h3></div><span class="micro-copy">近 7 日</span></div>
<div class="donut-layout"><div id="riskDonut" class="risk-donut"><div><strong id="riskTotal">0</strong><span>事件</span></div></div><div class="donut-legend"><div><span><i class="legend-fire"></i>火焰</span><strong id="fireRiskRatio">0%</strong></div><div><span><i class="legend-smoke"></i>烟雾</span><strong id="smokeRiskRatio">0%</strong></div></div></div>
</article>
</div>
<div class="overview-grid overview-grid-lower">
<article class="panel trend-panel">
<div class="panel-header"><div><p class="section-kicker">ALERT TREND</p><h3>近期告警趋势</h3></div><div class="legend"><span><i class="legend-fire"></i>火焰</span><span><i class="legend-smoke"></i>烟雾</span></div></div>
<div id="trendChart" class="trend-chart" role="img" aria-label="近期火焰和烟雾告警趋势"></div>
</article>
<article class="panel readiness-panel">
<div class="panel-header"><div><p class="section-kicker">SYSTEM READINESS</p><h3>系统就绪度</h3></div></div>
<div class="readiness-list">
<div class="readiness-item"><span>模型权重</span><strong id="readyWeights">检查中</strong></div>
<div class="readiness-item"><span>检测服务</span><strong id="readyApi">检查中</strong></div>
<div class="readiness-item"><span>企业微信</span><strong id="readyWechat">未配置</strong></div>
<div class="readiness-item"><span>飞书机器人</span><strong id="readyFeishu">未配置</strong></div>
</div>
</article>
</div>
<article class="panel recent-panel">
<div class="panel-header"><div><p class="section-kicker">RECENT EVENTS</p><h3>最近告警事件</h3></div><button class="text-button" type="button" data-view-jump="events">查看全部 →</button></div>
<div id="recentEventList" class="event-list empty-list"><p>暂无告警事件。开始视频巡检后,满足连续帧条件的告警会显示在这里。</p></div>
</article>
</section>
<section class="view" id="view-inspection" data-view-panel="inspection">
<div class="page-intro"><div><p class="section-kicker">VIDEO INSPECTION</p><h2>智能视频巡检</h2><p>选择本地监控视频,系统将按设定间隔抽帧并进行烟雾、火焰识别。</p></div><span id="inspectionStatus" class="status-chip status-idle">等待视频</span></div>
<div class="inspection-grid">
<article class="panel video-panel">
<div class="video-toolbar"><div><strong id="sourceLabel">未选择视频</strong><span id="videoMeta">支持 MP4、WebM 等浏览器可播放格式</span></div><span id="videoClock">00:00 / 00:00</span></div>
<div class="media-stage">
<video id="videoPreview" controls muted playsinline hidden></video>
<div id="emptyState" class="empty-state"><div class="upload-emblem"></div><strong>导入监控视频</strong><span>视频在浏览器本地播放,仅将抽取帧发送至检测服务</span></div>
<canvas id="overlayCanvas" aria-hidden="true"></canvas>
<div id="loadingState" class="loading-state" hidden><span class="spinner"></span>正在分析视频帧</div>
</div>
<div class="video-actions">
<label class="file-button"><input id="videoInput" type="file" accept="video/*" /><span>选择视频</span></label>
<button id="runDetectionButton" class="primary-button" type="button" disabled>开始连续检测</button>
<button id="clearButton" class="secondary-button" type="button">清除</button>
</div>
</article>
<div class="inspection-side">
<article class="panel detection-summary">
<div class="panel-header"><div><p class="section-kicker">LIVE RESULT</p><h3>实时检测摘要</h3></div><span id="lastUpdated" class="micro-copy">尚未检测</span></div>
<div class="detection-metrics">
<div><span>烟雾目标</span><strong id="smokeCount">0</strong></div>
<div><span>火焰目标</span><strong id="fireCount">0</strong></div>
<div><span>最高置信度</span><strong id="maxConfidence">--</strong></div>
<div><span>风险等级</span><strong id="riskStatus">待机</strong></div>
</div>
<div id="alertStatus" class="alert-status">机器人告警状态:检查中</div>
<div id="detectionList" class="detection-list"><p class="empty-copy">暂无检测结果</p></div>
</article>
<article class="panel inspection-settings">
<div class="panel-header"><div><p class="section-kicker">STRATEGY</p><h3>巡检策略</h3></div></div>
<label class="field-row"><span>检测间隔</span><select id="intervalSelect"><option value="500">0.5 秒</option><option value="1000" selected>1 秒</option><option value="2000">2 秒</option></select></label>
<div class="strategy-row"><span>连续确认帧</span><strong id="confirmFramesValue">3 帧</strong></div>
<div class="strategy-row"><span>告警冷却</span><strong id="cooldownValue">60 秒</strong></div>
<div class="strategy-note">连续检测到目标后才会生成告警事件,并按火焰、烟雾分别执行冷却。</div>
</article>
</div>
</div>
</section>
<section class="view" id="view-events" data-view-panel="events">
<div class="page-intro"><div><p class="section-kicker">ALERT CENTER</p><h2>告警事件管理</h2><p>集中查看、确认和关闭视频巡检产生的火灾风险事件。</p></div><button id="refreshEventsButton" class="secondary-button" type="button">刷新事件</button></div>
<div class="event-stat-row"><div><span>全部事件</span><strong id="allEventsCount">0</strong></div><div><span>待处置</span><strong id="pendingEventsCount">0</strong></div><div><span>已确认</span><strong id="ackEventsCount">0</strong></div><div><span>已解决</span><strong id="resolvedEventsCount">0</strong></div></div>
<article class="panel event-table-panel">
<div class="table-toolbar"><div class="filter-group" role="group" aria-label="事件状态筛选"><button type="button" class="filter-button is-active" data-event-filter="">全部</button><button type="button" class="filter-button" data-event-filter="pending">待处置</button><button type="button" class="filter-button" data-event-filter="acknowledged">已确认</button><button type="button" class="filter-button" data-event-filter="resolved">已解决</button></div><span id="eventTableMeta">0 条记录</span></div>
<div class="table-wrap"><table class="event-table"><thead><tr><th>事件编号</th><th>时间</th><th>类型</th><th>置信度</th><th>通知通道</th><th>状态</th><th>操作</th></tr></thead><tbody id="eventTableBody"><tr><td colspan="7" class="empty-cell">暂无告警事件</td></tr></tbody></table></div>
</article>
</section>
<section class="view" id="view-robots" data-view-panel="robots">
<div class="page-intro robot-page-intro"><div><p class="section-kicker">MESSAGING ROBOT OPERATIONS</p><h2>消息机器人管理</h2><p>管理企业微信与飞书群机器人,监控告警投递状态并执行连接测试。</p></div><button id="refreshRobotsButton" class="secondary-button" type="button">刷新状态</button></div>
<div class="fleet-stat-grid">
<article class="fleet-stat"><span>支持平台</span><strong id="robotTotalCount">0</strong><small>企业微信与飞书</small></article>
<article class="fleet-stat"><span>已配置</span><strong id="robotConfiguredCount">0</strong><small>可接收告警消息</small></article>
<article class="fleet-stat"><span>成功投递</span><strong id="robotDeliveredCount">0</strong><small>累计(已持久化)</small></article>
<article class="fleet-stat"><span>发送失败</span><strong id="robotFailedCount">0</strong><small>需要检查网络或凭据</small></article>
</div>
<div class="robot-management-grid message-robot-layout">
<div id="robotCardGrid" class="message-robot-grid"><article class="panel robot-empty">正在读取机器人状态</article></div>
<aside class="panel robot-policy-panel">
<div class="panel-header"><div><p class="section-kicker">DELIVERY POLICY</p><h3>告警发送策略</h3></div><span class="status-chip status-online">自动执行</span></div>
<div class="policy-chain"><div><span>1</span><p><strong>连续帧确认</strong><small id="robotPolicyConfirm">--</small></p></div><div><span>2</span><p><strong>生成告警内容</strong><small>目标类型、置信度与检测时间</small></p></div><div><span>3</span><p><strong>并行发送</strong><small>向所有已配置机器人投递</small></p></div><div><span>4</span><p><strong>分类冷却</strong><small id="robotPolicyCooldown">--</small></p></div></div>
<div class="credential-note"><strong>凭据安全</strong><p>Webhook 与签名密钥只保存在服务端 <code>.env</code>,页面不会读取或展示敏感值。</p></div>
</aside>
</div>
<article class="panel robot-config-panel"><div class="panel-header"><div><p class="section-kicker">CONFIGURATION</p><h3>机器人接入</h3></div></div><div class="config-steps"><div><span>01</span><p><strong>创建群机器人</strong><small>在企业微信群或飞书群中添加自定义机器人。</small></p></div><div><span>02</span><p><strong>写入服务端配置</strong><small><code>WECHAT_WEBHOOK_URL</code> / <code>FEISHU_WEBHOOK_URL</code></small></p></div><div><span>03</span><p><strong>重启并测试</strong><small>重启服务后,在机器人卡片中发送测试消息。</small></p></div></div></article>
</section>
<section class="view" id="view-risks" data-view-panel="risks">
<div class="page-intro"><div><p class="section-kicker">RISK REGISTER</p><h2>风险台账</h2><p>从模型阈值、告警策略和处置状态评估当前风险治理情况。</p></div></div>
<div class="risk-grid">
<article class="panel risk-card"><span class="risk-level high">高风险</span><h3>未处置告警事件</h3><strong id="riskPendingValue">0</strong><p>待值班人员确认的火焰或烟雾事件。</p><button class="text-button" type="button" data-view-jump="events">进入告警中心 →</button></article>
<article class="panel risk-card"><span class="risk-level medium">策略风险</span><h3>告警通知覆盖</h3><strong id="riskChannelValue">0 / 2</strong><p>建议至少启用一个机器人通知通道。</p><button class="text-button" type="button" data-view-jump="robots">管理消息机器人 →</button></article>
<article class="panel risk-card"><span class="risk-level low">模型策略</span><h3>检测置信度阈值</h3><strong id="riskConfidenceValue">--</strong><p>阈值过低会增加误报,过高可能遗漏早期烟雾。</p><button class="text-button" type="button" data-view-jump="settings">查看系统参数 →</button></article>
</div>
<article class="panel checklist-panel"><div class="panel-header"><div><p class="section-kicker">PREVENTION CHECKLIST</p><h3>火灾预防工作清单</h3></div></div><div class="checklist"><label><input type="checkbox" />视频监控覆盖重点区域</label><label><input type="checkbox" />告警机器人可正常接收消息</label><label><input type="checkbox" />值班人员明确事件处置流程</label><label><input type="checkbox" />模型权重与阈值定期复核</label><label><input type="checkbox" />消防通道和设备状态已巡查</label><label><input type="checkbox" />告警事件完成闭环记录</label></div></article>
</section>
<section class="view" id="view-settings" data-view-panel="settings">
<div class="page-intro"><div><p class="section-kicker">SYSTEM SETTINGS</p><h2>系统设置</h2><p>查看当前生效的模型和告警参数。配置修改后需重启服务。</p></div></div>
<div class="settings-grid">
<article class="panel settings-card"><div class="panel-header"><div><p class="section-kicker">MODEL</p><h3>模型配置</h3></div></div><dl><div><dt>权重文件</dt><dd id="settingWeights">--</dd></div><div><dt>输入尺寸</dt><dd id="settingImageSize">--</dd></div><div><dt>置信度阈值</dt><dd id="settingConfidence">--</dd></div><div><dt>IOU 阈值</dt><dd id="settingIou">--</dd></div></dl></article>
<article class="panel settings-card"><div class="panel-header"><div><p class="section-kicker">ALERT POLICY</p><h3>告警策略</h3></div></div><dl><div><dt>连续确认</dt><dd id="settingConfirmFrames">--</dd></div><div><dt>分类冷却</dt><dd id="settingCooldown">--</dd></div><div><dt>事件存储</dt><dd>SQLite 持久化 · 最近 500 条</dd></div><div><dt>视频处理</dt><dd>浏览器本地抽帧</dd></div></dl></article>
</div>
<article class="panel env-panel"><div class="panel-header"><div><p class="section-kicker">ENVIRONMENT</p><h3>环境变量参考</h3></div></div><pre><code>YOLO_WEIGHTS=...
YOLO_DEVICE=0
YOLO_IMGSZ=768
YOLO_CONF=0.40
YOLO_IOU=0.45
ALERT_CONFIRM_FRAMES=3
ALERT_COOLDOWN_SECONDS=60</code></pre></article>
</section>
</div>
</main>
</div>
<div id="toastRegion" class="toast-region" aria-live="polite"></div>
<script src="./main.js" type="module"></script>
</body>
</html>