Files
Behavision/desktop/frontend/src/mock.js
Suriyakumarvijayanayagam 1607f4ce74 Find the camera on the network instead of asking for its address
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
2026-09-21 12:27:47 +05:30

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}}
}