import { useState } from 'react' import { api } from '../api.js' import { usePolled } from '../hooks.js' import Shot from './Shot.jsx' import CameraLive from './CameraLive.jsx' import { ago, Loading, Problem } from './Sites.jsx' import CameraSetup from './CameraSetup.jsx' // Cameras, onboarded from head office. // // The shop PC is still the thing that CONNECTS — it is the only machine on the // camera's network — so everything here is desired state that its agent pulls // and applies. That is why a new camera reads "waiting for the shop PC" rather // than "connected": saying it is set up would be claiming something nobody has // verified yet. // verification is the second claim a camera card makes, and the one that // matters: connected says the stream opens, verified says a person walking past // can actually be recognised. Office1 was the first for weeks without ever // being the second. function verification(cam) { const c = cam.check || {} if (c.state === 'requested' || c.state === 'running') { return { tone: 'idle', mark: '…', words: 'Checking now' } } if (c.state !== 'done') { return { tone: 'idle', mark: '?', words: 'Not checked yet' } } if (c.kind === 'placement') { return c.ok ? { tone: 'ok', mark: '✓', words: 'Recognises faces here' } : { tone: 'bad', mark: '✕', words: c.headline || 'Cannot recognise faces here' } } return c.ok ? { tone: 'warn', mark: '!', words: 'Stream works — faces not checked yet' } : { tone: 'bad', mark: '✕', words: c.headline || 'Could not reach the camera' } } export default function Cameras({ user }) { const { data: cams, error, loading, reload } = usePolled(() => api.cameras(), 20000, []) const { data: sites } = usePolled(() => api.sites(), 0, []) const [editing, setEditing] = useState(null) // Only one camera streams at a time, on purpose. Every open view makes a // shop PC upload, so a grid that went live all at once would put an estate's // worth of cameras on the wire because somebody opened a page. const [watching, setWatching] = useState(null) const canEdit = ['admin', 'owner', 'manager'].includes(user.role) const list = cams || [] // Counted off `state`, the one field the server computes, never off // `connected`. Two places deciding the same fact is how a shop came out // labelled Working, in green, above "2 of 3 cameras not connecting". const up = list.filter(c => c.state === 'connected').length const down = list.filter(c => c.state === 'not_connecting').length const waiting = list.filter(c => c.state === 'waiting').length const stale = list.filter(c => c.state === 'stale').length return ( <>

Cameras

{list.length} {list.length === 1 ? 'camera' : 'cameras'} {up > 0 && <> · {up} connected} {stale > 0 && <> · {stale} not reporting} {down > 0 && <> · {down} down} {waiting > 0 && <> · {waiting} waiting for the shop PC}

{canEdit && ( )}
{loading && !cams ? : error ? : ( list.length === 0 ? (

No cameras yet

Add one here and the shop’s PC will pick it up within a couple of minutes. Cameras already set up on a shop PC appear here on their own.

) : (
{list.map(c => ( setEditing(c)} onWatch={() => setWatching(c)} /> ))}
) )} {editing && ( { setEditing(null); reload() }} onSaved={(_, opts) => { if (!opts?.keepOpen) setEditing(null); reload() }} /> )} {watching && ( setWatching(null)} /> )} ) } function CameraCard({ cam, canEdit, onEdit, onWatch }) { // Three states, not two. A camera nobody has tried yet is not a camera that // is down, and telling an operator to check the cabling on a camera the shop // PC has not even seen sends them to the wrong building. // Four states, and the fourth is the one that was missing: a camera whose // shop PC has stopped reporting it. The agent correctly says nothing when it // cannot reach the engine, so the last value it sent used to sit in the // database reading Connected - measured at 34 minutes on the live estate. const state = { connected: 'ok', not_connecting: 'bad', stale: 'warn' }[cam.state] || 'idle' const words = { connected: 'Connected', not_connecting: 'Not connecting', stale: 'Not reporting' }[cam.state] || 'Waiting for the shop PC' const verified = verification(cam) return ( // The picture IS the card. A camera is a thing you look at, and the // previous layout made it a black rectangle sitting above a table of // connection settings — which is the view a developer wants, not a shop // owner. The technical detail moves behind Edit, where it is needed only // when something is being changed.
canEdit && e.key === 'Enter' && onEdit()}>
{cam.snapshot?.available ? :
} {/* Over the picture, where a camera label belongs, rather than in a caption underneath it. */}
{cam.label || cam.camera_id} {cam.site}