move_detect/server/src/View/events.php
유창욱 47ab1143e1 feat(fp): operator false-positive tag + per-camera FP-rate report
The feedback loop, per product decision (option a): tags feed STATISTICS only.
A human reads the rate and re-tunes a camera deliberately - the tag never
adjusts edge sensitivity on its own (no silent behavior change in a safety
system).

- POST /events/{id}/false-positive: same RBAC ('acknowledge') + CSRF + site
  scope as ack. Tagging also acknowledges (an FP is a handled event - leaving
  it unacknowledged would keep it alarming). COALESCE keeps the FIRST tagger:
  a second tag never rewrites who made the original audit-relevant call.
- move_events.false_positive_by/at (migration included), audit-logged.
- GET /api/fp-stats?days=N: per-(camera, event_type) totals + FP counts,
  FP>0 groups only, fp-desc, fail-closed site scoping.
- Console: each open row gets a visually-secondary 오탐 button beside 확인
  (can't be fat-fingered as the default); resolved rows show a mono verdict
  ("오탐 (user)" struck through vs "확인됨 (user)"). New rail panel "오탐율 ·
  30일" lists top offenders - 30%+ highlighted as re-tuning candidates -
  refreshed on a slow 60s cadence plus immediately after a tag. Detail view
  shows the tag.

Verified: PHPUnit 135 pass / pytest unchanged + Chrome real-render (tag ->
row flips to struck-through verdict + drops out of unacked KPI/tree counts ->
FP panel updates immediately; no console errors).
2026-08-06 20:38:36 +09:00

59 lines
2.6 KiB
PHP

<?php
require __DIR__ . '/layout.php';
render_header('이벤트');
?>
<div class="console">
<aside class="rail">
<section class="rail-block">
<div class="rail-head">
<h2 class="rail-title">장치 · 카메라</h2>
<button type="button" id="camera-reset" class="rail-reset" hidden>전체 보기</button>
</div>
<!-- The tree itself is NOT a live region: it is rebuilt on a 5s poll and would be re-read
in full every time. This one-line summary carries the announcement instead. -->
<p id="rail-summary" class="rail-sum" aria-live="polite">장치 상태 확인 중…</p>
<div id="devices-root" class="device-tree" aria-label="장치·카메라 트리">불러오는 중…</div>
</section>
<section class="rail-block">
<h2 class="rail-title">필터</h2>
<div class="rail-filters">
<label class="field">심각도
<select id="severity-filter" aria-label="심각도 필터">
<option value="">전체 심각도</option>
<option value="critical">위급</option>
<option value="high">높음</option>
<option value="warning">경고</option>
<option value="info">정보</option>
</select>
</label>
<label class="filter-check"><input type="checkbox" id="unacked-only"> 미확인만 보기</label>
</div>
</section>
<section class="rail-block">
<h2 class="rail-title">오탐율 · 30일</h2>
<!-- Read-only report: the 오탐 tags end here, as numbers a human reads before re-tuning
a camera — the tag never adjusts edge sensitivity on its own. -->
<div id="fp-stats-root" class="fp-stats" aria-label="카메라별 오탐율">집계 중…</div>
</section>
</aside>
<section class="stream">
<header class="stream-head">
<div class="stream-title">
<h1>감지 스트림</h1>
<span class="watch" id="watch-status" aria-live="polite">실시간 감시 중</span>
</div>
<div class="kpi-wrap">
<span class="kpi-cap">미확인</span>
<div id="events-status" class="kpi-strip" aria-label="미확인 이벤트 현황"></div>
</div>
</header>
<div id="events-root" class="event-list" aria-live="polite" aria-busy="true">
<div class="stream-empty">감지 이벤트를 불러오는 중…</div>
</div>
<div class="list-more"><button type="button" id="load-more" hidden>더 보기</button></div>
</section>
</div>
<script src="<?= view_url('/assets/device-tree.js') ?>"></script>
<script src="<?= view_url('/assets/app.js') ?>"></script>
<?php
render_footer();