feat(booth): live lane busy/free barrier lights from camera vehicle detection
A Hikvision vehicle detection (eventType=VMD, targetType=vehicle) on a
camera bound to entry/exit now marks that lane "busy" and shows it as a
barrier light beside the scan input on the booth (green=free, red=busy).
Advisory only — it gates nothing (never blocks a ticket or opens a barrier).
- Parse eventState (active/inactive) from the Hik payload.
- LaneStatus tracker: a vehicle `active` event marks the camera's bound lane
busy + arms an auto-clear timer. This camera class sends no leave/`inactive`
signal, so "free" is timeout-driven (LANE_BUSY_TTL_MS, default 90s; the
camera re-fires `active` while a car sits there, refreshing the timer). A
"both"-direction camera marks both lanes.
- Push lane-status over the existing booth WS (+ in the hello snapshot);
live-store holds { entry, exit }; two BarrierLight icons render it.
- i18n booth.laneEntry/laneExit (sq + en).
Tests: lane-status.test.ts (7 — busy/free, TTL auto-clear, timer re-arm,
no re-emit while busy, both/exit direction, unknown device). server 120/120;
web + server build/lint green.
Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
This commit is contained in:
@@ -112,6 +112,44 @@ function TicketInput({ onSubmit }: { onSubmit: (identity: string) => void }) {
|
||||
);
|
||||
}
|
||||
|
||||
/** One barrier light — green = free, red = busy (a vehicle is at the lane vicinity,
|
||||
* from camera detection). Advisory only; it gates nothing. */
|
||||
function BarrierLight({ label, busy }: { label: string; busy: boolean }) {
|
||||
return (
|
||||
<div
|
||||
className={`flex items-center gap-2 rounded-term border px-3 py-2 ${
|
||||
busy ? "border-term-red bg-term-red/10" : "border-term-green bg-term-green/10"
|
||||
}`}
|
||||
title={label}
|
||||
>
|
||||
{/* Barrier glyph: a post + an arm. Colour carries the state. */}
|
||||
<svg viewBox="0 0 24 24" className={`h-5 w-5 ${busy ? "text-term-red" : "text-term-green"}`} fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
||||
<line x1="5" y1="21" x2="5" y2="9" />
|
||||
<line x1="5" y1="10" x2="21" y2="6" />
|
||||
<circle cx="5" cy="7" r="1.6" fill="currentColor" stroke="none" />
|
||||
</svg>
|
||||
<div className="leading-tight">
|
||||
<div className="text-[10px] uppercase tracking-wider text-term-muted">{label}</div>
|
||||
<div className={`text-xs font-bold ${busy ? "text-term-red" : "text-term-green"}`}>
|
||||
{busy ? "●" : "○"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** The two lane barrier lights (entry / exit) fed by the live lane-status. */
|
||||
function LaneIndicators() {
|
||||
const { t } = useTranslation();
|
||||
const lanes = useLiveStore((s) => s.lanes);
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<BarrierLight label={t("booth.laneEntry")} busy={lanes?.entry ?? false} />
|
||||
<BarrierLight label={t("booth.laneExit")} busy={lanes?.exit ?? false} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function BoothScreen() {
|
||||
const { t } = useTranslation();
|
||||
// The site-wide shift drives the log scope: the feed shows ONLY the open shift's
|
||||
@@ -198,10 +236,16 @@ export function BoothScreen() {
|
||||
|
||||
return (
|
||||
<div className="grid h-full grid-cols-1 gap-3 lg:grid-cols-[minmax(320px,1fr)_2fr] lg:grid-rows-[auto_1fr]">
|
||||
{/* Ticket input spans both columns at the top — the operator's primary action. */}
|
||||
{/* Ticket input spans both columns at the top — the operator's primary action.
|
||||
The lane barrier lights sit beside it (live vehicle-detection busy/free). */}
|
||||
<div className="lg:col-span-2">
|
||||
<Panel title={t("booth.processTicket")}>
|
||||
<TicketInput onSubmit={setActiveTicket} />
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<div className="min-w-[260px] flex-1">
|
||||
<TicketInput onSubmit={setActiveTicket} />
|
||||
</div>
|
||||
<LaneIndicators />
|
||||
</div>
|
||||
</Panel>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user