The shop screen no longer shows camera video

The Live screen led with a camera tile beside the arrivals. Nobody at a
counter is watching CCTV; they are looking up at a customer and need the
name. The tile also cost CPU the recognition pipeline needs and pulled a
stream relay into the app for a picture that was decoration. Arrivals now
take the whole screen. The camera picture stays on the Cameras screen,
where it is a setup tool and not a feed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGcjxF1cNLcuwc3DAPcnfj
This commit is contained in:
2026-09-18 10:53:32 +05:30
parent 3d3775c8be
commit 979aa77cda
5 changed files with 38 additions and 91 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -4,8 +4,8 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Behavision</title>
<script type="module" crossorigin src="./assets/index-DmoSrGcS.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-CGxw2EVn.css">
<script type="module" crossorigin src="./assets/index-Dxm9O68-.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-Bml5ogS2.css">
</head>
<body>
<div id="root"></div>

View File

@@ -469,3 +469,8 @@ tbody tr[role="button"]:hover, tbody tr.clickable:hover { background: var(--s2);
.live-split { grid-template-columns: minmax(0, 1fr); }
.live-split > .panel { max-height: none; }
}
/* Arrivals alone on the Live screen: one column, capped so a long day scrolls
inside the panel rather than pushing the metrics off the bottom. */
.arrivals-panel { max-height: 64vh; margin-bottom: var(--sp-4); }
.arrivals-panel .arrivals { display: grid; grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); gap: var(--sp-2); }

View File

@@ -11,13 +11,14 @@ import * as Icon from '../ui/icons.jsx'
// actionable content taking the most space, and the product's whole reason for
// existing rendered as a log.
//
// Now: a status strip that answers "is this working" in one line, cameras as
// pictures, and arrivals as cards big enough to recognise a customer from
// while looking up at them.
// Now: a status strip that answers "is this working" in one line, and arrivals
// as cards big enough to recognise a customer from while looking up at them.
// No camera picture here - the person at the counter is not watching CCTV,
// and a live video tile costs CPU the recognition pipeline needs. The picture
// lives on the Cameras screen, where it is a setup tool.
export default function Live() {
const { data, error } = usePolled(() => api.live(), 3000)
const { data: pipe } = usePolled(() => api.pipelineStatus(), 5000)
const cams = useCameraFeeds()
const cameras = data?.stats?.cameras ?? []
const gallery = data?.stats?.gallery ?? {}
@@ -39,65 +40,30 @@ export default function Live() {
<div className="page">
<header>
<h2>Live</h2>
<p>Who is in the shop, what the cameras see, and whether any of it is reaching head office.</p>
<p>Who is in the shop, and whether it is reaching head office.</p>
</header>
{error && <div className="err"><Icon.Warning size={15} />{error}</div>}
<PipelineStrip pipe={pipe} cameras={cameras} up={up} />
<div className="live-split">
<div className="panel">
<div className="panelhead">
<h3>Cameras</h3>
{cameras.length > 0 && (
<span className="pill">{up} of {cameras.length} live</span>
)}
</div>
<div className="panelbody flush">
{cameras.length === 0
? <div className="empty">
<Icon.NoCamera size={34} />
<b>No cameras yet</b>
<p>Add one on the Cameras screen. You will need its address on the shop network and its password.</p>
</div>
: <div className="feeds">
{cameras.map(c => (
<figure className="feed" key={c.camera_id}>
{cams[c.camera_id]
? <img src={cams[c.camera_id]} alt={`Live view from ${c.camera_id}`} />
: <div className="placeholder"><Icon.Camera size={26} /></div>}
<figcaption className="cap">
<span>{c.camera_id}</span>
<span className={`pill ${c.connected ? 'ok' : 'bad'}`}>
<i className={`dot ${c.connected ? 'live' : 'bad'}`} />
{c.connected ? 'live' : 'offline'}
</span>
</figcaption>
</figure>
))}
</div>}
</div>
<div className="panel arrivals-panel">
<div className="panelhead">
<h3>Who just walked in</h3>
{arrivals.length > 0 && <span className="note">{arrivals.length} today</span>}
</div>
<div className="panel">
<div className="panelhead">
<h3>Who just walked in</h3>
{arrivals.length > 0 && <span className="note">{arrivals.length} today</span>}
</div>
<div className="panelbody flush">
{arrivals.length === 0
? <div className="empty">
<Icon.NoFaces size={34} />
<b>Nobody yet</b>
<p>Customers appear here the moment a camera recognises a face.</p>
</div>
: <div className="arrivals">
{arrivals.slice(0, 12).map((e, i) => (
<Arrival key={`${e.ts}-${i}`} e={e} fresh={i === 0 && freshest} />
))}
</div>}
</div>
<div className="panelbody flush">
{arrivals.length === 0
? <div className="empty">
<Icon.NoFaces size={34} />
<b>Nobody yet</b>
<p>Customers appear here the moment a camera recognises a face.</p>
</div>
: <div className="arrivals">
{arrivals.slice(0, 30).map((e, i) => (
<Arrival key={`${e.ts}-${i}`} e={e} fresh={i === 0 && freshest} />
))}
</div>}
</div>
</div>
@@ -224,27 +190,3 @@ function useFreshest(top) {
}, [top?.ts])
return fresh
}
// Stream URLs are fetched once per camera and then left alone: reassigning an
// MJPEG <img> src restarts the stream, so rebuilding them on every poll would
// make every feed flicker permanently.
function useCameraFeeds() {
const [urls, setUrls] = useState({})
const { data } = usePolled(() => api.cameras(), 10000)
useEffect(() => {
let cancelled = false
;(async () => {
const next = {}
for (const cam of data ?? []) {
if (urls[cam.id]) { next[cam.id] = urls[cam.id]; continue }
try { next[cam.id] = await api.streamURL(cam.id) } catch { /* engine down */ }
}
const changed = Object.keys(next).length !== Object.keys(urls).length ||
Object.keys(next).some(k => next[k] !== urls[k])
if (!cancelled && changed) setUrls(next)
})()
return () => { cancelled = true }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data])
return urls
}