The add-camera form asked for an IP address, and a shop owner does not know their camera's IP address - it is on a sticker under the camera or in a menu that differs by make. That field is where onboarding stopped for anyone who was not an installer. behavision/discover.py: one ONVIF WS-Discovery multicast (names the camera and often its make) merged with a TCP sweep of port 554 across the local /24 (misses nothing that streams). Stdlib only, ~4 s on the office network, both cameras found. The add-camera sheet leads with 'Find cameras on this network'; picking a row fills the address and, when the make is recognisable, the stream path. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KGcjxF1cNLcuwc3DAPcnfj
84 lines
6.6 KiB
JavaScript
84 lines
6.6 KiB
JavaScript
// A stand-in for the Go bindings, for looking at screens in a browser.
|
|
//
|
|
// The app only exists inside Wails, so until this there was no way to put a
|
|
// screen in front of somebody without a Windows build - which is how the
|
|
// first-run experience went unreviewed. Loaded ONLY by the dev server and only
|
|
// with ?mock=<scenario>; a production bundle never contains it.
|
|
//
|
|
// ?mock=fresh first launch after install: unclaimed, engine starting
|
|
// ?mock=standalone chose "run on this PC only", no cameras yet
|
|
// ?mock=claimed claimed, signed in, one camera, arrivals flowing
|
|
//
|
|
// State is in memory and advances as you click, so the flow can be walked.
|
|
const scenario = new URLSearchParams(location.search).get('mock')
|
|
|
|
if (scenario) {
|
|
const st = {
|
|
claimed: scenario === 'claimed',
|
|
standalone: scenario === 'standalone',
|
|
logged_in: scenario === 'claimed',
|
|
cameras: scenario === 'claimed' ? [{id: 'entrance', label: 'Entrance', host: '192.168.1.122', port: 554, path: '/ch0_1.264', username: 'admin', has_password: true, enabled: true}] : [],
|
|
engine: scenario === 'fresh' ? 'starting' : 'running',
|
|
startedAt: Date.now(),
|
|
}
|
|
const user = {id: 'u1', email: 'suriya@tenext.in', full_name: 'Suriya', role: 'owner', client_id: 'c1', client_name: 'TeNext Retail'}
|
|
const session = () => ({logged_in: st.logged_in, user: st.logged_in ? user : {}, site_name: st.claimed ? 'TeNext Chennai' : '', claimed: st.claimed, standalone: st.standalone && !st.claimed})
|
|
const now = () => new Date().toISOString()
|
|
const arrivals = () => st.cameras.length === 0 ? [] : [
|
|
{type: 'person.seen', ts: now(), camera_id: 'entrance', data: {label: 'Visitor 3', identity_id: 3, similarity: 0.61, gender: 'Male', age: 34, emotion: 'neutral'}},
|
|
{type: 'person.new', ts: new Date(Date.now() - 95e3).toISOString(), camera_id: 'entrance', data: {label: 'Visitor 7', identity_id: 7, gender: 'Female', age: 28}},
|
|
{type: 'person.seen', ts: new Date(Date.now() - 410e3).toISOString(), camera_id: 'entrance', data: {label: 'Priya', identity_id: 2, similarity: 0.72, gender: 'Female', age: 41}},
|
|
]
|
|
const delay = (v, ms = 120) => new Promise(r => setTimeout(() => r(v), ms))
|
|
const App = {
|
|
Session: () => delay(session()),
|
|
Login: (email) => { st.logged_in = true; user.email = email || user.email; return delay(session()) },
|
|
Logout: () => { st.logged_in = false; return delay(session()) },
|
|
Claim: (code) => code.replace(/[^A-Z0-9]/gi, '').length >= 20
|
|
? (st.claimed = true, st.standalone = false, delay(session(), 900))
|
|
: Promise.reject(new Error('That installation code is not valid. Ask for a new one.')),
|
|
RunStandalone: () => { st.standalone = true; return delay(session()) },
|
|
EngineStatus: () => {
|
|
// The engine takes a minute or two on first run (models download).
|
|
if (st.engine === 'starting' && Date.now() - st.startedAt > 20000) st.engine = 'running'
|
|
const cams = Object.fromEntries(st.cameras.map(c => [c.id, true]))
|
|
return delay({state: st.engine === 'starting' ? 'running' : 'running', reachable: st.engine !== 'starting', recognition_model: st.engine === 'starting' ? '' : 'w600k_r50', cameras: cams, restarts: 0})
|
|
},
|
|
StartEngine: () => delay({state: 'running', reachable: true}),
|
|
StopEngine: () => delay({state: 'stopped', reachable: false}),
|
|
Cameras: () => delay(st.cameras.map(c => ({...c, connected: true, frames: 1200, faces: 9}))),
|
|
DiscoverCameras: () => delay({networks: ['192.168.1.0/24'], cameras: [
|
|
{host: '192.168.1.122', rtsp: true, onvif: true, name: 'HIKVISION DS-2CD2043G2', make: 'hikvision'},
|
|
{host: '192.168.1.121', rtsp: true, onvif: true, name: 'IPC-model IPC', make: ''},
|
|
{host: '192.168.1.40', rtsp: true, onvif: false, name: '', make: ''},
|
|
]}, 2500),
|
|
TestCamera: (cam) => delay({ok: Boolean(cam.host), width: 800, height: 448, codec: 'hevc', error: cam.host ? '' : 'no host'}, 1500),
|
|
SaveCamera: (id, cam) => { const c = {id: id || cam.id || 'cam' + (st.cameras.length + 1), ...cam, has_password: Boolean(cam.password)}; delete c.password; st.cameras = [...st.cameras.filter(x => x.id !== c.id), c]; return delay(c) },
|
|
DeleteCamera: (id) => { st.cameras = st.cameras.filter(c => c.id !== id); return delay(null) },
|
|
StartPlacementCheck: () => delay({state: 'running'}),
|
|
PlacementResult: () => delay({state: 'finished', verdict: 'good', headline: 'faces recognised on a walk-past', advice: []}),
|
|
StreamURL: () => '',
|
|
Live: () => delay({stats: {cameras: st.cameras.map(c => ({camera_id: c.id, connected: true, pipeline: {best_quality: {n: 40, fraction_below_gate: 0.18}}})), gallery: {identities: st.cameras.length ? 7 : 0, sightings: st.cameras.length ? 44 : 0}}, events: arrivals()}),
|
|
PipelineStatus: () => delay({webhook_url: 'http://127.0.0.1:53658/events', queued: 0, dropped: 0, claimed: st.claimed, standalone: st.standalone && !st.claimed, broker_up: st.claimed, accepted: st.claimed ? 12 : 0}),
|
|
LocalIdentities: () => delay([]), LocalSightings: () => delay([]),
|
|
Footfall: () => delay({total: 0, buckets: []}), Sites: () => delay([]),
|
|
VisitorHistory: () => delay([]), VisitorPhoto: () => delay({available: false, reason: 'This system is not storing images.'}),
|
|
ForgetCustomer: () => delay(null), Sales: () => delay([]),
|
|
Customers: () => delay([{id: 'v1', label: 'Priya', number: 2, first_seen_at: now(), last_seen_at: now(), visits: 6}]),
|
|
SaveProfile: () => delay(null), RecordPurchase: () => delay(null),
|
|
Ask: (history) => {
|
|
const q = history[history.length - 1]?.text ?? ''
|
|
if (!st.claimed) return Promise.reject(new Error('The assistant is not switched on for this server.'))
|
|
const text = /camera/i.test(q)
|
|
? 'Go to Cameras and press Add camera. The address is on a sticker on the camera itself; pick the make and I fill in the stream path. Test it, save it, then walk past it once so I can tell you whether the placement works.'
|
|
: /code|install/i.test(q)
|
|
? 'Whoever runs head office makes one: open the shop there, press Set up a shop PC, and read the code out. It works once.'
|
|
: /who|morning|came/i.test(q)
|
|
? 'Three people so far: Priya at 13:12 (her sixth visit), a new face at 13:18 I have called Visitor 7, and Visitor 3 just now.'
|
|
: 'Chennai is online and the door camera is connected, but nobody has proved it yet. Walk past it once with Check placement running and I will tell you if it can actually see faces - until then a quiet screen might just be a badly aimed camera.'
|
|
return delay({text, used: /camera|code/i.test(q) ? [] : ['site_status', 'cameras']}, 1400)
|
|
},
|
|
}
|
|
window.go = {main: {App}}
|
|
}
|