From e0ceb145893b01f7452720840e81afa7bba9e7ae Mon Sep 17 00:00:00 2001 From: Suriyakumarvijayanayagam Date: Fri, 4 Sep 2026 12:53:18 +0530 Subject: [PATCH] Camera pictures without an object-storage bucket Head office shows a camera's latest frame rather than live video, for a reason that has not changed: the engine serves MJPEG on 127.0.0.1 on a PC behind a shop's router with no inbound route, and relaying it needs WebRTC/TURN. Pointing a browser straight at the shop PC is not the escape either - the engine's API is Basic-authenticated with a credential it generates locally and never sends anywhere, and shipping that to the cloud so a web page could use it would put the key to the biometric API and the live face feed in the server's database. But that picture only worked if you had an S3 bucket. Without one, attachSnapshots reported "This system is not storing images" for every camera forever - on the two screens whose whole job is to show the camera. Making them picture-led turned a missing feature into a wall of empty tiles, on every local install and any self-hosted customer who does not want a bucket. migrations/009 adds camera_snapshots and the agent falls back to PUT /api/agent/cameras/{camera}/snapshot when the presigned route answers images_disabled - chosen by sentinel, never by matching the message, since it picks between two routes. One row per camera is what makes this safe in the database when face images are not: the key IS the camera, so storage is (cameras x ~100 KB) and does not grow with footfall. The read is session-authenticated rather than a signed link, which an cannot use - hence Shot.jsx and useAuthedImage, keyed on the URL string rather than the snapshot object so a poll does not re-fetch 90 KB per camera every few seconds, and revoking the object URL on cleanup. Verified against the real office camera with no bucket configured: 90,587 bytes stored in Postgres, served as image/jpeg to a signed-in user, 401 without a session, rendered on both the Cameras and Shops cards. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01HViLj9gYNRtSr7YVZmW5sn --- CLAUDE.md | 86 +++++++++++++ agent/pkg/cameras/cameras.go | 11 +- agent/pkg/cameras/cameras_test.go | 43 +++++++ agent/pkg/cameras/clients.go | 61 +++++++++ server/internal/api/api.go | 12 ++ server/internal/api/fake_test.go | 36 ++++++ server/internal/api/handlers_cameras.go | 17 ++- server/internal/api/handlers_snapshots.go | 118 ++++++++++++++++++ server/internal/api/snapshots_test.go | 102 +++++++++++++++ server/internal/store/api_snapshots.go | 84 +++++++++++++ .../web/dist/assets/index-B_gTsA07.js | 43 +++++++ .../web/dist/assets/index-C8M-zRAi.js | 43 ------- server/internal/web/dist/index.html | 2 +- server/migrations/009_camera_snapshots.sql | 45 +++++++ web/src/api.js | 34 +++++ web/src/hooks.js | 39 ++++++ web/src/views/Cameras.jsx | 3 +- web/src/views/Shot.jsx | 24 ++++ web/src/views/Sites.jsx | 3 +- 19 files changed, 757 insertions(+), 49 deletions(-) create mode 100644 server/internal/api/handlers_snapshots.go create mode 100644 server/internal/api/snapshots_test.go create mode 100644 server/internal/store/api_snapshots.go create mode 100644 server/internal/web/dist/assets/index-B_gTsA07.js delete mode 100644 server/internal/web/dist/assets/index-C8M-zRAi.js create mode 100644 server/migrations/009_camera_snapshots.sql create mode 100644 web/src/views/Shot.jsx diff --git a/CLAUDE.md b/CLAUDE.md index ea446ef..2f9493a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2171,3 +2171,89 @@ Two bugs, both found by running it after a reboot rather than by reading it. longer discarded, and the broker is waited for and reported on if it will not stay up. A failure there means the server cannot authenticate to its own broker, which is exactly what this script exists to surface early. + +## Where the live feed is, and why head office gets a picture instead + +Live video exists and always has — on the shop PC, where the camera is: + +- `GET /api/cameras/{id}/stream.mjpeg` on the engine's loopback API. Measured + on the office camera: 1280×720, ~850 KB/s. +- The desktop app's Live screen and the engine's own dashboard both render it. + +**Head office does not, and cannot cheaply.** The engine serves that stream on +`127.0.0.1` on a PC behind a shop's router with no inbound route. Putting live +video on `platform.loyaly.ai` needs a relay — WebRTC with TURN, or the agent +pushing a continuous stream up — which is infrastructure and bandwidth this does +not have. Nor can the browser be pointed straight at the shop PC even on one +LAN: the engine's API is Basic-authenticated with a credential it generates +locally and never sends anywhere, and shipping that credential to the cloud so a +web page could use it would put the key to the biometric API and the live face +feed in the server's database. That is a far worse trade than not having live +video at head office. + +So head office shows the camera's **latest frame**, refreshed every 60 s. The +agent already holds it in memory for its own stream, so it costs a memory copy +rather than a camera round trip. + +### The picture only worked if you had an S3 bucket + +Which meant that on any deployment without object storage — every local install, +and any self-hosted customer who does not want a bucket — `attachSnapshots` +returned *"This system is not storing images"* for every camera, **forever**, on +the two screens whose entire job is to show the camera. Making those screens +picture-led is what turned a missing feature into a wall of empty tiles. + +`migrations/009` adds `camera_snapshots`, and the agent falls back to +`PUT /api/agent/cameras/{camera}/snapshot` when the presigned route answers +`images_disabled`. What makes this safe in the database when face images are +not: + +- **One row per camera.** The primary key *is* the camera, so a snapshot + replaces its predecessor. Storage is (cameras × ~100 KB) and does not grow + with time or footfall. Face images grow with every visitor who ever walks in, + which is exactly why they stay in a bucket. +- It is a picture of a shop floor, not a face crop bound to an identity, and it + carries no template. +- `ON DELETE CASCADE` from the camera, so removing a camera removes its picture + with no second place to remember. + +Details that are not incidental: + +- **The bucket stays primary where one exists.** Both routes exist because they + are right for different deployments, not because one supersedes the other — + a presigned PUT never passes the bytes through the API at all, which is what + makes it the right route at estate scale. +- **The fallback is chosen by a sentinel (`bridge.ErrImagesOff`), never by + matching the message.** It decides which of two routes to take; getting it + wrong from prose somebody later rewords would silently stop every camera + picture in the estate. +- **The camera is resolved by (site_id, camera_id) inside the INSERT**, so an + agent cannot store a picture against another site's camera. The tenant and + site come from the agent's credential, never the request. +- **`snapshot_at` is written in the same transaction as the bytes.** It is what + tells the camera list a picture exists; set apart, a camera could advertise + one that is not there, which renders as a broken image on the one screen + meant to show it. +- **JPEG is verified from the magic bytes, not the Content-Type header**, and + the body is bounded by `MaxBytesReader` at 2 MB. This endpoint stores what it + is handed and serves it back to a browser, so the one thing it must not become + is a way to park arbitrary content under a URL this server will serve. +- **The read is session-authenticated, not a signed link.** There is no third + party to delegate to — the bytes are in our own database — and minting an + unauthenticated URL so that `` could use it would add a way to reach + a photograph of somebody's shop floor with no session at all. + +That last decision has a front-end consequence, and it is why `Shot.jsx` exists: +**an `` cannot send an Authorization header.** A presigned bucket URL is +absolute and carries its own signature, so a plain `src` loads it; a relative +URL served by this server has to be fetched with the session and handed over as +an object URL. `useAuthedImage` keys on the URL string rather than the +`snapshot` object — which is a fresh object on every poll, so an effect +depending on it would re-fetch ~90 KB per camera every few seconds — and revokes +the object URL on cleanup, or a screen left open all afternoon holds hundreds of +copies of the same photograph. + +Verified against the real office camera with no object storage configured: a +90,587-byte frame stored in Postgres, served as `image/jpeg` to a signed-in +user, **401 without a session**, and rendered on both the Cameras and Shops +cards. diff --git a/agent/pkg/cameras/cameras.go b/agent/pkg/cameras/cameras.go index 75c7d3b..d8585f0 100644 --- a/agent/pkg/cameras/cameras.go +++ b/agent/pkg/cameras/cameras.go @@ -29,7 +29,14 @@ type Engine interface { type Cloud interface { Desired(ctx context.Context) ([]Desired, error) Report(ctx context.Context, rep Report) error + // UploadSnapshot puts a JPEG in object storage and names it. Used for the + // placement check's proof picture, which is transient. UploadSnapshot(ctx context.Context, jpeg []byte) (key string, err error) + // PutSnapshot gets a camera's latest frame to head office by whichever + // route this deployment has - the bucket, or the server itself when there + // is none. An empty key means the server already stored it, so the state + // report has nothing to carry. + PutSnapshot(ctx context.Context, cameraID string, jpeg []byte) (key string, err error) } // Local is a camera as the engine holds it. @@ -252,7 +259,9 @@ func (s *Syncer) reportWith(ctx context.Context, adopt []Desired) { // camera is down matters far more than having a picture of it, // and the picture is the part most likely to fail. if jpeg, err := s.Engine.Snapshot(ctx, c.ID); err == nil && len(jpeg) > 0 { - if key, err := s.Cloud.UploadSnapshot(ctx, jpeg); err == nil { + // An empty key is not a failure: it means this deployment has + // no object storage and the server stored the picture itself. + if key, err := s.Cloud.PutSnapshot(ctx, c.ID, jpeg); err == nil { st.SnapshotKey = key } else { s.logf("camera %s: snapshot upload failed: %v", c.ID, err) diff --git a/agent/pkg/cameras/cameras_test.go b/agent/pkg/cameras/cameras_test.go index 860d785..91217a3 100644 --- a/agent/pkg/cameras/cameras_test.go +++ b/agent/pkg/cameras/cameras_test.go @@ -59,6 +59,10 @@ type fakeCloud struct { reports []Report uploads int uploadErr error + // direct records the cameras whose picture went to the server itself + // rather than to object storage. + direct []string + directOnly bool } func (f *fakeCloud) Desired(context.Context) ([]Desired, error) { @@ -68,6 +72,20 @@ func (f *fakeCloud) Report(_ context.Context, r Report) error { f.reports = append(f.reports, r) return nil } + +// PutSnapshot mirrors the real client: the bucket when there is one, the +// server itself when there is not. +func (f *fakeCloud) PutSnapshot(ctx context.Context, cameraID string, jpeg []byte) (string, error) { + if f.directOnly { + if f.uploadErr != nil { + return "", f.uploadErr + } + f.direct = append(f.direct, cameraID) + return "", nil + } + return f.UploadSnapshot(ctx, jpeg) +} + func (f *fakeCloud) UploadSnapshot(context.Context, []byte) (string, error) { if f.uploadErr != nil { return "", f.uploadErr @@ -239,3 +257,28 @@ func TestAnEngineThatIsNotRunningIsNotAnError(t *testing.T) { t.Fatal("reported state it could not have observed") } } + +// A deployment with no object storage must still get its picture to head +// office. Before this, the camera screen said "This system is not storing +// images" for every camera, forever - on the one screen whose entire job is to +// show the camera. +func TestASnapshotStillReachesHeadOfficeWithNoObjectStorage(t *testing.T) { + e := newEngine(Local{ID: "entrance", Connected: true}) + c := &fakeCloud{directOnly: true} + syncer(e, c).Once(context.Background()) + + if len(c.direct) != 1 || c.direct[0] != "entrance" { + t.Fatalf("the picture did not reach the server: %v", c.direct) + } + if len(c.reports) == 0 || len(c.reports[0].State) != 1 { + t.Fatalf("no state was reported: %+v", c.reports) + } + // Empty, and that is the point: there is no object to name. A key here + // would have head office try to presign a bucket it does not have. + if key := c.reports[0].State[0].SnapshotKey; key != "" { + t.Fatalf("the direct route reported an object key %q", key) + } + if !c.reports[0].State[0].Connected { + t.Error("connected state was lost") + } +} diff --git a/agent/pkg/cameras/clients.go b/agent/pkg/cameras/clients.go index 9770358..3a473e9 100644 --- a/agent/pkg/cameras/clients.go +++ b/agent/pkg/cameras/clients.go @@ -4,12 +4,15 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" "io" "net/http" "net/url" "strings" "time" + + "github.com/loyaly/behavision-agent/pkg/bridge" ) // EngineClient talks to the recognition engine on this PC's loopback. @@ -192,6 +195,64 @@ func (c *CloudClient) UploadSnapshot(ctx context.Context, jpeg []byte) (string, return c.Upload(ctx, jpeg) } +// PutSnapshot gets a camera's latest frame to head office by whichever route +// that deployment has. +// +// The bucket first: a presigned PUT goes straight to object storage and never +// passes through the API, which is what makes it the right route at estate +// scale. When there is no bucket the picture goes to the server itself, which +// stores one row per camera. Without this second route head office reported +// "This system is not storing images" for every camera forever, on the screen +// whose entire job is to show the camera. +// +// The returned key is empty for the direct route - there is no object to name - +// and the server records the picture as it stores it, so the state report has +// nothing to carry. +func (c *CloudClient) PutSnapshot(ctx context.Context, cameraID string, jpeg []byte) (string, error) { + if c.Upload != nil { + key, err := c.Upload(ctx, jpeg) + if err == nil { + return key, nil + } + // A bucket that is configured here but disabled at the server is the + // ordinary case on a self-hosted install: fall through rather than + // giving up, and let the direct route decide. + if !isImagesDisabled(err) { + return "", err + } + } + return "", c.putSnapshotDirect(ctx, cameraID, jpeg) +} + +func (c *CloudClient) putSnapshotDirect(ctx context.Context, cameraID string, jpeg []byte) error { + req, err := http.NewRequestWithContext(ctx, http.MethodPut, + c.Base+"/api/agent/cameras/"+url.PathEscape(cameraID)+"/snapshot", + bytes.NewReader(jpeg)) + if err != nil { + return err + } + req.Header.Set("Authorization", "Bearer "+c.Token) + req.Header.Set("Content-Type", "image/jpeg") + resp, err := c.Client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return fmt.Errorf("head office: %s", resp.Status) + } + return nil +} + +// isImagesDisabled recognises the server saying it has no object storage. +// +// A sentinel, not a string match on the message: this decides whether to take a +// completely different route, and getting it wrong from prose that somebody +// later rewords would silently stop every camera picture in the estate. +func isImagesDisabled(err error) bool { + return errors.Is(err, bridge.ErrImagesOff) +} + // ---------------------------------------------------------------- probing // Test opens the candidate stream once, without saving it. diff --git a/server/internal/api/api.go b/server/internal/api/api.go index 54161f9..536f6ab 100644 --- a/server/internal/api/api.go +++ b/server/internal/api/api.go @@ -65,6 +65,10 @@ type Store interface { // reachable only with that site's own agent token. AgentCameras(ctx context.Context, siteID string) ([]AgentCamera, error) ApplyAgentReport(ctx context.Context, clientID, siteID string, rep AgentCameraReport) error + // Camera pictures held by this server, for deployments with no object + // storage. Where a bucket is configured neither of these is called. + PutCameraSnapshot(ctx context.Context, clientID, siteID, cameraID string, jpeg []byte) error + CameraSnapshot(ctx context.Context, clientID, cameraID string) ([]byte, time.Time, error) // --- claiming a shop PC --- IssueEnrolmentCode(ctx context.Context, clientID, siteID, actorID, @@ -192,6 +196,7 @@ func (s *Server) Routes() *http.ServeMux { mux.HandleFunc("POST /api/sites/{site}/cameras", s.authed(s.handleCreateCamera)) mux.HandleFunc("PATCH /api/cameras/{id}", s.authed(s.handleUpdateCamera)) mux.HandleFunc("DELETE /api/cameras/{id}", s.authed(s.handleDeleteCamera)) + mux.HandleFunc("GET /api/cameras/{id}/snapshot.jpg", s.authed(s.handleGetSnapshot)) // Prove a camera works: "connection" asks whether the shop PC can open the // stream, "placement" asks whether somebody walking past produces a view // good enough to recognise. Two questions, because a camera passes the @@ -235,6 +240,8 @@ func (s *Server) Routes() *http.ServeMux { // What this shop PC should be running, and what it reports back. mux.HandleFunc("GET /api/agent/cameras", s.agentAuthed(s.handleAgentCameras)) mux.HandleFunc("POST /api/agent/cameras", s.agentAuthed(s.handleAgentCameraReport)) + mux.HandleFunc("PUT /api/agent/cameras/{camera}/snapshot", + s.agentAuthed(s.handlePutSnapshot)) mux.HandleFunc("GET /api/agent/checks", s.agentAuthed(s.handleAgentChecks)) mux.HandleFunc("POST /api/agent/checks", s.agentAuthed(s.handleAgentCheckResult)) @@ -408,6 +415,11 @@ func looksLikeUUID(s string) bool { // without importing the store package. var ErrNoSecrets = errors.New("this server has no encryption key, so camera passwords cannot be stored") +// ErrNoSnapshot means a camera has no stored picture. An ordinary state - a +// camera added a minute ago has none - so it is reported as absence, never as +// a failure. +var ErrNoSnapshot = errors.New("no snapshot for this camera") + // BlobStore is what the API needs from object storage. Declared here and // implemented by internal/blob, so the handlers can be tested without a bucket // and so a deployment with images switched off is a nil field rather than a diff --git a/server/internal/api/fake_test.go b/server/internal/api/fake_test.go index a0d1b0f..15517b4 100644 --- a/server/internal/api/fake_test.go +++ b/server/internal/api/fake_test.go @@ -19,6 +19,14 @@ import ( // live - which tenant, which message on failure, what is echoed back - and // those are exactly what a real database would make slow and awkward to test. type fakeStore struct { + // Camera pictures held by the server, for a deployment with no bucket. + // Keyed as written by PutCameraSnapshot (by camera_id) and as read by + // CameraSnapshot ("client/camera"), so a test has to say which it means. + snapshots map[string][]byte + snapshotRejects bool + lastSnapshotClient string + lastSnapshotSite string + mu sync.Mutex users map[string]UserRecord // by lower-cased email @@ -586,3 +594,31 @@ func (b *fakeBlob) Delete(_ context.Context, key string) error { b.deleted = append(b.deleted, key) return nil } + +// ------------------------------------------------------- camera snapshots -- + +func (f *fakeStore) PutCameraSnapshot(_ context.Context, + clientID, siteID, cameraID string, jpeg []byte) error { + f.mu.Lock() + defer f.mu.Unlock() + if f.snapshots == nil { + f.snapshots = map[string][]byte{} + } + if f.snapshotRejects { + return ErrNoSnapshot + } + f.lastSnapshotClient, f.lastSnapshotSite = clientID, siteID + f.snapshots[cameraID] = append([]byte(nil), jpeg...) + return nil +} + +func (f *fakeStore) CameraSnapshot(_ context.Context, clientID, cameraID string) ( + []byte, time.Time, error) { + f.mu.Lock() + defer f.mu.Unlock() + img, ok := f.snapshots[clientID+"/"+cameraID] + if !ok { + return nil, time.Time{}, ErrNoSnapshot + } + return img, time.Unix(1756900000, 0).UTC(), nil +} diff --git a/server/internal/api/handlers_cameras.go b/server/internal/api/handlers_cameras.go index 99dbe13..5f02ef7 100644 --- a/server/internal/api/handlers_cameras.go +++ b/server/internal/api/handlers_cameras.go @@ -49,10 +49,23 @@ func (s *Server) attachSnapshots(cams []Camera) { key := cams[i].Snapshot.Key cams[i].Snapshot.Key = "" switch { - case s.Blob == nil: - cams[i].Snapshot.Reason = "This system is not storing images." + case key == "" && cams[i].SnapshotAt != "": + // Held by this server, because the deployment has no object + // storage. Served from an endpoint rather than a signed link: + // there is no third party to delegate to, the bytes are in our own + // database, and an unauthenticated URL to somebody's shop floor + // would be a new way in for no gain. + cams[i].Snapshot = Image{ + Available: true, + URL: "/api/cameras/" + cams[i].ID + "/snapshot.jpg", + ExpiresIn: int(snapshotTTL.Seconds()), + } case key == "": cams[i].Snapshot.Reason = "No picture from this camera yet." + case s.Blob == nil: + // A key from a bucket this server can no longer reach. Distinct + // from "no picture yet": one is waiting, the other is misconfigured. + cams[i].Snapshot.Reason = "This system is not storing images." default: url, err := s.Blob.PresignGet(key, snapshotTTL) if err != nil { diff --git a/server/internal/api/handlers_snapshots.go b/server/internal/api/handlers_snapshots.go new file mode 100644 index 0000000..02cc23c --- /dev/null +++ b/server/internal/api/handlers_snapshots.go @@ -0,0 +1,118 @@ +package api + +import ( + "errors" + "fmt" + "io" + "net/http" + "strconv" + "time" +) + +// maxSnapshotBytes caps what a shop PC may store per camera. +// +// A camera frame downscaled to 1280 px is ~100 KB; 2 MB is generous for a +// 4K still and small enough that a misbehaving or compromised agent cannot use +// this endpoint as free storage. One row per camera means it cannot accumulate +// either - the cap is about a single request, the primary key about the total. +const maxSnapshotBytes = 2 << 20 + +// snapshotMaxAge is how long a browser may reuse a camera picture. The agent +// refreshes them every 60 s, so anything longer shows a stale shop floor and +// anything shorter re-fetches a picture that has not changed. +const snapshotMaxAge = 30 * time.Second + +// handlePutSnapshot stores the latest frame from one of this site's cameras. +// +// This is the path for a deployment with NO object storage. Where a bucket is +// configured the agent keeps using the presigned-URL route, which never puts a +// picture through this process at all; both exist because they are right for +// different deployments, not because one supersedes the other. +// +// The body is the JPEG itself rather than JSON with base64: it avoids a third +// of the bytes and a decode step, and there is exactly one thing being sent. +func (s *Server) handlePutSnapshot(w http.ResponseWriter, r *http.Request, ap AgentPrincipal) { + cameraID := r.PathValue("camera") + if cameraID == "" { + writeErr(w, http.StatusNotFound, "not_found", "No such camera.") + return + } + + // http.MaxBytesReader, not a Content-Length check: a length header is + // whatever the client says it is, and this has to bound what is actually + // read into memory. + body, err := io.ReadAll(http.MaxBytesReader(w, r.Body, maxSnapshotBytes+1)) + if err != nil { + writeErr(w, http.StatusRequestEntityTooLarge, "too_large", + fmt.Sprintf("A snapshot must be under %d KB.", maxSnapshotBytes/1024)) + return + } + if len(body) > maxSnapshotBytes { + writeErr(w, http.StatusRequestEntityTooLarge, "too_large", + fmt.Sprintf("A snapshot must be under %d KB.", maxSnapshotBytes/1024)) + return + } + // Checked against the bytes, not the Content-Type header. This endpoint + // stores whatever it is given and hands it back to a browser later, so the + // one thing it must not become is a way to park arbitrary content under a + // URL this server will serve. + if !isJPEG(body) { + badRequest(w, "a snapshot must be a JPEG") + return + } + + switch err := s.Store.PutCameraSnapshot(r.Context(), + ap.ClientID, ap.SiteID, cameraID, body); { + case err == nil: + w.WriteHeader(http.StatusNoContent) + case errors.Is(err, ErrNoSnapshot): + // Head office has not adopted this camera yet. Not the agent's fault + // and not worth retrying: the next sync adopts it. + writeErr(w, http.StatusNotFound, "not_found", + "That camera is not set up at head office yet.") + default: + s.serverError(w, "store snapshot", err) + } +} + +// handleGetSnapshot serves a camera's stored picture to a signed-in user. +// +// Deliberately NOT a signed link like the bucket path: there is no third party +// to delegate to here, the bytes are in this server's own database, and minting +// a URL that works without a session in order to serve them would be adding an +// unauthenticated path to reach a picture of somebody's shop floor for no gain. +func (s *Server) handleGetSnapshot(w http.ResponseWriter, r *http.Request) { + p := PrincipalFrom(r.Context()) + id := r.PathValue("id") + if !looksLikeUUID(id) { + writeErr(w, http.StatusNotFound, "not_found", "No such camera.") + return + } + img, at, err := s.Store.CameraSnapshot(r.Context(), p.ClientID, id) + if errors.Is(err, ErrNoSnapshot) { + writeErr(w, http.StatusNotFound, "no_image", "No picture from this camera yet.") + return + } + if err != nil { + s.serverError(w, "read snapshot", err) + return + } + w.Header().Set("Content-Type", "image/jpeg") + w.Header().Set("Content-Length", strconv.Itoa(len(img))) + w.Header().Set("Cache-Control", "private, max-age="+ + strconv.Itoa(int(snapshotMaxAge.Seconds()))) + w.Header().Set("Last-Modified", at.UTC().Format(http.TimeFormat)) + // A picture of a shop floor is not something to hand to another origin's + // script, and nothing here needs to. + w.Header().Set("X-Content-Type-Options", "nosniff") + w.WriteHeader(http.StatusOK) + _, _ = w.Write(img) +} + +// isJPEG checks the magic bytes: SOI marker at the front, EOI at the back. +func isJPEG(b []byte) bool { + if len(b) < 4 { + return false + } + return b[0] == 0xFF && b[1] == 0xD8 && b[2] == 0xFF +} diff --git a/server/internal/api/snapshots_test.go b/server/internal/api/snapshots_test.go new file mode 100644 index 0000000..33937d5 --- /dev/null +++ b/server/internal/api/snapshots_test.go @@ -0,0 +1,102 @@ +package api + +import ( + "bytes" + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +// A minimal but real JPEG header: SOI + APP0. The endpoint checks the bytes, +// not the Content-Type header, so a test that sends anything else is not +// testing the same path a shop PC uses. +func jpegBytes(padTo int) []byte { + b := []byte{0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 'J', 'F', 'I', 'F', 0} + for len(b) < padTo { + b = append(b, 0x00) + } + return append(b, 0xFF, 0xD9) +} + +func TestAnAgentCanStoreItsCameraPicture(t *testing.T) { + srv, fs := newServer(t) + fs.addAgent("agent-token", AgentPrincipal{ClientID: "client-1", SiteID: "site-1"}) + + rr := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPut, + "/api/agent/cameras/entrance/snapshot", bytes.NewReader(jpegBytes(64))) + req.Header.Set("Authorization", "Bearer agent-token") + srv.Routes().ServeHTTP(rr, req) + + if rr.Code != http.StatusNoContent { + t.Fatalf("status %d: %s", rr.Code, rr.Body) + } + if len(fs.snapshots["entrance"]) == 0 { + t.Fatal("nothing was stored") + } + // The tenant and site come from the AGENT's credential, never the request. + // A camera id a caller can set must not be able to choose whose camera it + // decorates. + if fs.lastSnapshotClient != "client-1" || fs.lastSnapshotSite != "site-1" { + t.Fatalf("stored against %s/%s", fs.lastSnapshotClient, fs.lastSnapshotSite) + } +} + +// This endpoint stores whatever it is handed and serves it back to a browser +// later, so the one thing it must not become is a way to park arbitrary content +// under a URL this server will serve. +func TestOnlyAJPEGIsAccepted(t *testing.T) { + srv, fs := newServer(t) + fs.addAgent("agent-token", AgentPrincipal{ClientID: "client-1", SiteID: "site-1"}) + + for _, body := range []string{ + "", + "GIF89a", + "", + } { + rr := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPut, + "/api/agent/cameras/entrance/snapshot", strings.NewReader(body)) + req.Header.Set("Authorization", "Bearer agent-token") + // Claiming to be a JPEG must not help: the check is on the bytes. + req.Header.Set("Content-Type", "image/jpeg") + srv.Routes().ServeHTTP(rr, req) + if rr.Code != http.StatusBadRequest { + t.Fatalf("body %q was accepted with status %d", body, rr.Code) + } + } +} + +// One row per camera is what makes this safe to keep in the database at all, +// but a single oversized request still has to be bounded - it is read into +// memory before anything else looks at it. +func TestAnOversizedSnapshotIsRefused(t *testing.T) { + srv, fs := newServer(t) + fs.addAgent("agent-token", AgentPrincipal{ClientID: "client-1", SiteID: "site-1"}) + + rr := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPut, + "/api/agent/cameras/entrance/snapshot", + bytes.NewReader(jpegBytes(maxSnapshotBytes+1024))) + req.Header.Set("Authorization", "Bearer agent-token") + srv.Routes().ServeHTTP(rr, req) + + if rr.Code != http.StatusRequestEntityTooLarge { + t.Fatalf("status %d", rr.Code) + } + if len(fs.snapshots) != 0 { + t.Fatal("an oversized snapshot was stored anyway") + } +} + +func TestAnUnauthenticatedAgentCannotStoreAPicture(t *testing.T) { + srv, _ := newServer(t) + rr := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPut, + "/api/agent/cameras/entrance/snapshot", bytes.NewReader(jpegBytes(64))) + srv.Routes().ServeHTTP(rr, req) + if rr.Code != http.StatusUnauthorized { + t.Fatalf("status %d", rr.Code) + } +} diff --git a/server/internal/store/api_snapshots.go b/server/internal/store/api_snapshots.go new file mode 100644 index 0000000..81eba96 --- /dev/null +++ b/server/internal/store/api_snapshots.go @@ -0,0 +1,84 @@ +package store + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/jackc/pgx/v5" +) + +// ErrNoSnapshot means this camera has no stored picture. It is an ordinary +// state - a camera added a minute ago has none - so callers report it as +// absence rather than as a failure. +var ErrNoSnapshot = errors.New("no snapshot for this camera") + +// PutCameraSnapshot stores the latest frame from one of a site's cameras. +// +// The camera is resolved by (site_id, camera_id) IN THE INSERT, so an agent +// physically cannot store a picture against another site's camera even if it +// sends one - the same rule as every other agent-authenticated write here. +// `camera_id` is what the ENGINE knows the camera by, because that is the only +// name the shop PC has. +func (s *Store) PutCameraSnapshot(ctx context.Context, + clientID, siteID, cameraID string, jpeg []byte) error { + + tx, err := s.pool.Begin(ctx) + if err != nil { + return err + } + defer func() { _ = tx.Rollback(context.WithoutCancel(ctx)) }() + + var id string + err = tx.QueryRow(ctx, ` + SELECT id::text FROM site_cameras + WHERE site_id = $1::uuid AND camera_id = $2 AND deleted_at IS NULL`, + siteID, cameraID).Scan(&id) + if errors.Is(err, pgx.ErrNoRows) { + // Head office has not been told about this camera yet, or it was + // removed. Neither is an error the agent can act on: the next sync + // adopts it and the snapshot after that lands. + return ErrNoSnapshot + } + if err != nil { + return err + } + + if _, err := tx.Exec(ctx, ` + INSERT INTO camera_snapshots (camera_id, client_id, site_id, image, bytes, captured_at) + VALUES ($1::uuid, $2::uuid, $3::uuid, $4, $5, now()) + ON CONFLICT (camera_id) DO UPDATE + SET image = EXCLUDED.image, bytes = EXCLUDED.bytes, + captured_at = EXCLUDED.captured_at`, + id, clientID, siteID, jpeg, len(jpeg)); err != nil { + return fmt.Errorf("store snapshot: %w", err) + } + + // snapshot_at is what tells the camera list a picture exists at all, and it + // is written in the SAME transaction as the bytes. Set apart, a camera + // could advertise a picture that is not there - which renders as a broken + // image on the one screen whose job is to show the camera. + if _, err := tx.Exec(ctx, ` + UPDATE site_cameras SET snapshot_at = now() WHERE id = $1::uuid`, + id); err != nil { + return err + } + return tx.Commit(ctx) +} + +// CameraSnapshot returns a camera's stored picture, scoped to the tenant. +func (s *Store) CameraSnapshot(ctx context.Context, clientID, cameraID string) ( + []byte, time.Time, error) { + + var img []byte + var at time.Time + err := s.pool.QueryRow(ctx, ` + SELECT image, captured_at FROM camera_snapshots + WHERE camera_id = $1::uuid AND client_id = $2::uuid`, + cameraID, clientID).Scan(&img, &at) + if errors.Is(err, pgx.ErrNoRows) { + return nil, time.Time{}, ErrNoSnapshot + } + return img, at, err +} diff --git a/server/internal/web/dist/assets/index-B_gTsA07.js b/server/internal/web/dist/assets/index-B_gTsA07.js new file mode 100644 index 0000000..838ad89 --- /dev/null +++ b/server/internal/web/dist/assets/index-B_gTsA07.js @@ -0,0 +1,43 @@ +(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const l of document.querySelectorAll('link[rel="modulepreload"]'))r(l);new MutationObserver(l=>{for(const i of l)if(i.type==="childList")for(const s of i.addedNodes)s.tagName==="LINK"&&s.rel==="modulepreload"&&r(s)}).observe(document,{childList:!0,subtree:!0});function n(l){const i={};return l.integrity&&(i.integrity=l.integrity),l.referrerPolicy&&(i.referrerPolicy=l.referrerPolicy),l.crossOrigin==="use-credentials"?i.credentials="include":l.crossOrigin==="anonymous"?i.credentials="omit":i.credentials="same-origin",i}function r(l){if(l.ep)return;l.ep=!0;const i=n(l);fetch(l.href,i)}})();var au={exports:{}},al={},cu={exports:{}},L={};/** + * @license React + * react.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var tr=Symbol.for("react.element"),Ec=Symbol.for("react.portal"),_c=Symbol.for("react.fragment"),Pc=Symbol.for("react.strict_mode"),Tc=Symbol.for("react.profiler"),zc=Symbol.for("react.provider"),Lc=Symbol.for("react.context"),Rc=Symbol.for("react.forward_ref"),Oc=Symbol.for("react.suspense"),Ic=Symbol.for("react.memo"),Fc=Symbol.for("react.lazy"),Zs=Symbol.iterator;function Dc(e){return e===null||typeof e!="object"?null:(e=Zs&&e[Zs]||e["@@iterator"],typeof e=="function"?e:null)}var du={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},fu=Object.assign,pu={};function an(e,t,n){this.props=e,this.context=t,this.refs=pu,this.updater=n||du}an.prototype.isReactComponent={};an.prototype.setState=function(e,t){if(typeof e!="object"&&typeof e!="function"&&e!=null)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,e,t,"setState")};an.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,"forceUpdate")};function hu(){}hu.prototype=an.prototype;function qi(e,t,n){this.props=e,this.context=t,this.refs=pu,this.updater=n||du}var bi=qi.prototype=new hu;bi.constructor=qi;fu(bi,an.prototype);bi.isPureReactComponent=!0;var Js=Array.isArray,mu=Object.prototype.hasOwnProperty,es={current:null},vu={key:!0,ref:!0,__self:!0,__source:!0};function yu(e,t,n){var r,l={},i=null,s=null;if(t!=null)for(r in t.ref!==void 0&&(s=t.ref),t.key!==void 0&&(i=""+t.key),t)mu.call(t,r)&&!vu.hasOwnProperty(r)&&(l[r]=t[r]);var u=arguments.length-2;if(u===1)l.children=n;else if(1>>1,q=C[Q];if(0>>1;Ql(zl,z))wtl(ur,zl)?(C[Q]=ur,C[wt]=z,Q=wt):(C[Q]=zl,C[gt]=z,Q=gt);else if(wtl(ur,z))C[Q]=ur,C[wt]=z,Q=wt;else break e}}return T}function l(C,T){var z=C.sortIndex-T.sortIndex;return z!==0?z:C.id-T.id}if(typeof performance=="object"&&typeof performance.now=="function"){var i=performance;e.unstable_now=function(){return i.now()}}else{var s=Date,u=s.now();e.unstable_now=function(){return s.now()-u}}var a=[],c=[],m=1,y=null,h=3,v=!1,w=!1,x=!1,R=typeof setTimeout=="function"?setTimeout:null,p=typeof clearTimeout=="function"?clearTimeout:null,d=typeof setImmediate<"u"?setImmediate:null;typeof navigator<"u"&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0&&navigator.scheduling.isInputPending.bind(navigator.scheduling);function f(C){for(var T=n(c);T!==null;){if(T.callback===null)r(c);else if(T.startTime<=C)r(c),T.sortIndex=T.expirationTime,t(a,T);else break;T=n(c)}}function g(C){if(x=!1,f(C),!w)if(n(a)!==null)w=!0,Pl(S);else{var T=n(c);T!==null&&Tl(g,T.startTime-C)}}function S(C,T){w=!1,x&&(x=!1,p(P),P=-1),v=!0;var z=h;try{for(f(T),y=n(a);y!==null&&(!(y.expirationTime>T)||C&&!Te());){var Q=y.callback;if(typeof Q=="function"){y.callback=null,h=y.priorityLevel;var q=Q(y.expirationTime<=T);T=e.unstable_now(),typeof q=="function"?y.callback=q:y===n(a)&&r(a),f(T)}else r(a);y=n(a)}if(y!==null)var or=!0;else{var gt=n(c);gt!==null&&Tl(g,gt.startTime-T),or=!1}return or}finally{y=null,h=z,v=!1}}var E=!1,_=null,P=-1,H=5,O=-1;function Te(){return!(e.unstable_now()-OC||125Q?(C.sortIndex=z,t(c,C),n(a)===null&&C===n(c)&&(x?(p(P),P=-1):x=!0,Tl(g,z-Q))):(C.sortIndex=q,t(a,C),w||v||(w=!0,Pl(S))),C},e.unstable_shouldYield=Te,e.unstable_wrapCallback=function(C){var T=h;return function(){var z=h;h=T;try{return C.apply(this,arguments)}finally{h=z}}}})(Su);xu.exports=Su;var Gc=xu.exports;/** + * @license React + * react-dom.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var Yc=N,ke=Gc;function k(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;n"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),li=Object.prototype.hasOwnProperty,Xc=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,bs={},eo={};function Zc(e){return li.call(eo,e)?!0:li.call(bs,e)?!1:Xc.test(e)?eo[e]=!0:(bs[e]=!0,!1)}function Jc(e,t,n,r){if(n!==null&&n.type===0)return!1;switch(typeof t){case"function":case"symbol":return!0;case"boolean":return r?!1:n!==null?!n.acceptsBooleans:(e=e.toLowerCase().slice(0,5),e!=="data-"&&e!=="aria-");default:return!1}}function qc(e,t,n,r){if(t===null||typeof t>"u"||Jc(e,t,n,r))return!0;if(r)return!1;if(n!==null)switch(n.type){case 3:return!t;case 4:return t===!1;case 5:return isNaN(t);case 6:return isNaN(t)||1>t}return!1}function de(e,t,n,r,l,i,s){this.acceptsBooleans=t===2||t===3||t===4,this.attributeName=r,this.attributeNamespace=l,this.mustUseProperty=n,this.propertyName=e,this.type=t,this.sanitizeURL=i,this.removeEmptyString=s}var re={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(e){re[e]=new de(e,0,!1,e,null,!1,!1)});[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(e){var t=e[0];re[t]=new de(t,1,!1,e[1],null,!1,!1)});["contentEditable","draggable","spellCheck","value"].forEach(function(e){re[e]=new de(e,2,!1,e.toLowerCase(),null,!1,!1)});["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(e){re[e]=new de(e,2,!1,e,null,!1,!1)});"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(e){re[e]=new de(e,3,!1,e.toLowerCase(),null,!1,!1)});["checked","multiple","muted","selected"].forEach(function(e){re[e]=new de(e,3,!0,e,null,!1,!1)});["capture","download"].forEach(function(e){re[e]=new de(e,4,!1,e,null,!1,!1)});["cols","rows","size","span"].forEach(function(e){re[e]=new de(e,6,!1,e,null,!1,!1)});["rowSpan","start"].forEach(function(e){re[e]=new de(e,5,!1,e.toLowerCase(),null,!1,!1)});var ns=/[\-:]([a-z])/g;function rs(e){return e[1].toUpperCase()}"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(e){var t=e.replace(ns,rs);re[t]=new de(t,1,!1,e,null,!1,!1)});"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(e){var t=e.replace(ns,rs);re[t]=new de(t,1,!1,e,"http://www.w3.org/1999/xlink",!1,!1)});["xml:base","xml:lang","xml:space"].forEach(function(e){var t=e.replace(ns,rs);re[t]=new de(t,1,!1,e,"http://www.w3.org/XML/1998/namespace",!1,!1)});["tabIndex","crossOrigin"].forEach(function(e){re[e]=new de(e,1,!1,e.toLowerCase(),null,!1,!1)});re.xlinkHref=new de("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1);["src","href","action","formAction"].forEach(function(e){re[e]=new de(e,1,!1,e.toLowerCase(),null,!0,!0)});function ls(e,t,n,r){var l=re.hasOwnProperty(t)?re[t]:null;(l!==null?l.type!==0:r||!(2u||l[s]!==i[u]){var a=` +`+l[s].replace(" at new "," at ");return e.displayName&&a.includes("")&&(a=a.replace("",e.displayName)),a}while(1<=s&&0<=u);break}}}finally{Ol=!1,Error.prepareStackTrace=n}return(e=e?e.displayName||e.name:"")?jn(e):""}function bc(e){switch(e.tag){case 5:return jn(e.type);case 16:return jn("Lazy");case 13:return jn("Suspense");case 19:return jn("SuspenseList");case 0:case 2:case 15:return e=Il(e.type,!1),e;case 11:return e=Il(e.type.render,!1),e;case 1:return e=Il(e.type,!0),e;default:return""}}function ui(e){if(e==null)return null;if(typeof e=="function")return e.displayName||e.name||null;if(typeof e=="string")return e;switch(e){case Mt:return"Fragment";case Dt:return"Portal";case ii:return"Profiler";case is:return"StrictMode";case si:return"Suspense";case oi:return"SuspenseList"}if(typeof e=="object")switch(e.$$typeof){case Nu:return(e.displayName||"Context")+".Consumer";case Cu:return(e._context.displayName||"Context")+".Provider";case ss:var t=e.render;return e=e.displayName,e||(e=t.displayName||t.name||"",e=e!==""?"ForwardRef("+e+")":"ForwardRef"),e;case os:return t=e.displayName||null,t!==null?t:ui(e.type)||"Memo";case be:t=e._payload,e=e._init;try{return ui(e(t))}catch{}}return null}function ed(e){var t=e.type;switch(e.tag){case 24:return"Cache";case 9:return(t.displayName||"Context")+".Consumer";case 10:return(t._context.displayName||"Context")+".Provider";case 18:return"DehydratedFragment";case 11:return e=t.render,e=e.displayName||e.name||"",t.displayName||(e!==""?"ForwardRef("+e+")":"ForwardRef");case 7:return"Fragment";case 5:return t;case 4:return"Portal";case 3:return"Root";case 6:return"Text";case 16:return ui(t);case 8:return t===is?"StrictMode":"Mode";case 22:return"Offscreen";case 12:return"Profiler";case 21:return"Scope";case 13:return"Suspense";case 19:return"SuspenseList";case 25:return"TracingMarker";case 1:case 0:case 17:case 2:case 14:case 15:if(typeof t=="function")return t.displayName||t.name||null;if(typeof t=="string")return t}return null}function pt(e){switch(typeof e){case"boolean":case"number":case"string":case"undefined":return e;case"object":return e;default:return""}}function _u(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(t==="checkbox"||t==="radio")}function td(e){var t=_u(e)?"checked":"value",n=Object.getOwnPropertyDescriptor(e.constructor.prototype,t),r=""+e[t];if(!e.hasOwnProperty(t)&&typeof n<"u"&&typeof n.get=="function"&&typeof n.set=="function"){var l=n.get,i=n.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return l.call(this)},set:function(s){r=""+s,i.call(this,s)}}),Object.defineProperty(e,t,{enumerable:n.enumerable}),{getValue:function(){return r},setValue:function(s){r=""+s},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}function dr(e){e._valueTracker||(e._valueTracker=td(e))}function Pu(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var n=t.getValue(),r="";return e&&(r=_u(e)?e.checked?"true":"false":e.value),e=r,e!==n?(t.setValue(e),!0):!1}function $r(e){if(e=e||(typeof document<"u"?document:void 0),typeof e>"u")return null;try{return e.activeElement||e.body}catch{return e.body}}function ai(e,t){var n=t.checked;return V({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:n??e._wrapperState.initialChecked})}function no(e,t){var n=t.defaultValue==null?"":t.defaultValue,r=t.checked!=null?t.checked:t.defaultChecked;n=pt(t.value!=null?t.value:n),e._wrapperState={initialChecked:r,initialValue:n,controlled:t.type==="checkbox"||t.type==="radio"?t.checked!=null:t.value!=null}}function Tu(e,t){t=t.checked,t!=null&&ls(e,"checked",t,!1)}function ci(e,t){Tu(e,t);var n=pt(t.value),r=t.type;if(n!=null)r==="number"?(n===0&&e.value===""||e.value!=n)&&(e.value=""+n):e.value!==""+n&&(e.value=""+n);else if(r==="submit"||r==="reset"){e.removeAttribute("value");return}t.hasOwnProperty("value")?di(e,t.type,n):t.hasOwnProperty("defaultValue")&&di(e,t.type,pt(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}function ro(e,t,n){if(t.hasOwnProperty("value")||t.hasOwnProperty("defaultValue")){var r=t.type;if(!(r!=="submit"&&r!=="reset"||t.value!==void 0&&t.value!==null))return;t=""+e._wrapperState.initialValue,n||t===e.value||(e.value=t),e.defaultValue=t}n=e.name,n!==""&&(e.name=""),e.defaultChecked=!!e._wrapperState.initialChecked,n!==""&&(e.name=n)}function di(e,t,n){(t!=="number"||$r(e.ownerDocument)!==e)&&(n==null?e.defaultValue=""+e._wrapperState.initialValue:e.defaultValue!==""+n&&(e.defaultValue=""+n))}var Cn=Array.isArray;function Yt(e,t,n,r){if(e=e.options,t){t={};for(var l=0;l"+t.valueOf().toString()+"",t=fr.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;t.firstChild;)e.appendChild(t.firstChild)}});function Mn(e,t){if(t){var n=e.firstChild;if(n&&n===e.lastChild&&n.nodeType===3){n.nodeValue=t;return}}e.textContent=t}var _n={animationIterationCount:!0,aspectRatio:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},nd=["Webkit","ms","Moz","O"];Object.keys(_n).forEach(function(e){nd.forEach(function(t){t=t+e.charAt(0).toUpperCase()+e.substring(1),_n[t]=_n[e]})});function Ou(e,t,n){return t==null||typeof t=="boolean"||t===""?"":n||typeof t!="number"||t===0||_n.hasOwnProperty(e)&&_n[e]?(""+t).trim():t+"px"}function Iu(e,t){e=e.style;for(var n in t)if(t.hasOwnProperty(n)){var r=n.indexOf("--")===0,l=Ou(n,t[n],r);n==="float"&&(n="cssFloat"),r?e.setProperty(n,l):e[n]=l}}var rd=V({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0});function hi(e,t){if(t){if(rd[e]&&(t.children!=null||t.dangerouslySetInnerHTML!=null))throw Error(k(137,e));if(t.dangerouslySetInnerHTML!=null){if(t.children!=null)throw Error(k(60));if(typeof t.dangerouslySetInnerHTML!="object"||!("__html"in t.dangerouslySetInnerHTML))throw Error(k(61))}if(t.style!=null&&typeof t.style!="object")throw Error(k(62))}}function mi(e,t){if(e.indexOf("-")===-1)return typeof t.is=="string";switch(e){case"annotation-xml":case"color-profile":case"font-face":case"font-face-src":case"font-face-uri":case"font-face-format":case"font-face-name":case"missing-glyph":return!1;default:return!0}}var vi=null;function us(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}var yi=null,Xt=null,Zt=null;function so(e){if(e=lr(e)){if(typeof yi!="function")throw Error(k(280));var t=e.stateNode;t&&(t=hl(t),yi(e.stateNode,e.type,t))}}function Fu(e){Xt?Zt?Zt.push(e):Zt=[e]:Xt=e}function Du(){if(Xt){var e=Xt,t=Zt;if(Zt=Xt=null,so(e),t)for(e=0;e>>=0,e===0?32:31-(hd(e)/md|0)|0}var pr=64,hr=4194304;function Nn(e){switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e&4194240;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return e&130023424;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return e}}function Wr(e,t){var n=e.pendingLanes;if(n===0)return 0;var r=0,l=e.suspendedLanes,i=e.pingedLanes,s=n&268435455;if(s!==0){var u=s&~l;u!==0?r=Nn(u):(i&=s,i!==0&&(r=Nn(i)))}else s=n&~l,s!==0?r=Nn(s):i!==0&&(r=Nn(i));if(r===0)return 0;if(t!==0&&t!==r&&!(t&l)&&(l=r&-r,i=t&-t,l>=i||l===16&&(i&4194240)!==0))return t;if(r&4&&(r|=n&16),t=e.entangledLanes,t!==0)for(e=e.entanglements,t&=r;0n;n++)t.push(e);return t}function nr(e,t,n){e.pendingLanes|=t,t!==536870912&&(e.suspendedLanes=0,e.pingedLanes=0),e=e.eventTimes,t=31-Ie(t),e[t]=n}function wd(e,t){var n=e.pendingLanes&~t;e.pendingLanes=t,e.suspendedLanes=0,e.pingedLanes=0,e.expiredLanes&=t,e.mutableReadLanes&=t,e.entangledLanes&=t,t=e.entanglements;var r=e.eventTimes;for(e=e.expirationTimes;0=Tn),vo=" ",yo=!1;function na(e,t){switch(e){case"keyup":return Gd.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function ra(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}var Ut=!1;function Xd(e,t){switch(e){case"compositionend":return ra(t);case"keypress":return t.which!==32?null:(yo=!0,vo);case"textInput":return e=t.data,e===vo&&yo?null:e;default:return null}}function Zd(e,t){if(Ut)return e==="compositionend"||!vs&&na(e,t)?(e=ea(),Tr=ps=rt=null,Ut=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=r}e:{for(;n;){if(n.nextSibling){n=n.nextSibling;break e}n=n.parentNode}n=void 0}n=xo(n)}}function oa(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?oa(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function ua(){for(var e=window,t=$r();t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href=="string"}catch{n=!1}if(n)e=t.contentWindow;else break;t=$r(e.document)}return t}function ys(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t==="input"&&(e.type==="text"||e.type==="search"||e.type==="tel"||e.type==="url"||e.type==="password")||t==="textarea"||e.contentEditable==="true")}function sf(e){var t=ua(),n=e.focusedElem,r=e.selectionRange;if(t!==n&&n&&n.ownerDocument&&oa(n.ownerDocument.documentElement,n)){if(r!==null&&ys(n)){if(t=r.start,e=r.end,e===void 0&&(e=t),"selectionStart"in n)n.selectionStart=t,n.selectionEnd=Math.min(e,n.value.length);else if(e=(t=n.ownerDocument||document)&&t.defaultView||window,e.getSelection){e=e.getSelection();var l=n.textContent.length,i=Math.min(r.start,l);r=r.end===void 0?i:Math.min(r.end,l),!e.extend&&i>r&&(l=r,r=i,i=l),l=So(n,i);var s=So(n,r);l&&s&&(e.rangeCount!==1||e.anchorNode!==l.node||e.anchorOffset!==l.offset||e.focusNode!==s.node||e.focusOffset!==s.offset)&&(t=t.createRange(),t.setStart(l.node,l.offset),e.removeAllRanges(),i>r?(e.addRange(t),e.extend(s.node,s.offset)):(t.setEnd(s.node,s.offset),e.addRange(t)))}}for(t=[],e=n;e=e.parentNode;)e.nodeType===1&&t.push({element:e,left:e.scrollLeft,top:e.scrollTop});for(typeof n.focus=="function"&&n.focus(),n=0;n=document.documentMode,$t=null,ji=null,Ln=null,Ci=!1;function jo(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;Ci||$t==null||$t!==$r(r)||(r=$t,"selectionStart"in r&&ys(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),Ln&&Wn(Ln,r)||(Ln=r,r=Kr(ji,"onSelect"),0Vt||(e.current=zi[Vt],zi[Vt]=null,Vt--)}function D(e,t){Vt++,zi[Vt]=e.current,e.current=t}var ht={},oe=vt(ht),he=vt(!1),_t=ht;function tn(e,t){var n=e.type.contextTypes;if(!n)return ht;var r=e.stateNode;if(r&&r.__reactInternalMemoizedUnmaskedChildContext===t)return r.__reactInternalMemoizedMaskedChildContext;var l={},i;for(i in n)l[i]=t[i];return r&&(e=e.stateNode,e.__reactInternalMemoizedUnmaskedChildContext=t,e.__reactInternalMemoizedMaskedChildContext=l),l}function me(e){return e=e.childContextTypes,e!=null}function Yr(){U(he),U(oe)}function zo(e,t,n){if(oe.current!==ht)throw Error(k(168));D(oe,t),D(he,n)}function ya(e,t,n){var r=e.stateNode;if(t=t.childContextTypes,typeof r.getChildContext!="function")return n;r=r.getChildContext();for(var l in r)if(!(l in t))throw Error(k(108,ed(e)||"Unknown",l));return V({},n,r)}function Xr(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||ht,_t=oe.current,D(oe,e),D(he,he.current),!0}function Lo(e,t,n){var r=e.stateNode;if(!r)throw Error(k(169));n?(e=ya(e,t,_t),r.__reactInternalMemoizedMergedChildContext=e,U(he),U(oe),D(oe,e)):U(he),D(he,n)}var We=null,ml=!1,Yl=!1;function ga(e){We===null?We=[e]:We.push(e)}function gf(e){ml=!0,ga(e)}function yt(){if(!Yl&&We!==null){Yl=!0;var e=0,t=F;try{var n=We;for(F=1;e>=s,l-=s,He=1<<32-Ie(t)+l|n<P?(H=_,_=null):H=_.sibling;var O=h(p,_,f[P],g);if(O===null){_===null&&(_=H);break}e&&_&&O.alternate===null&&t(p,_),d=i(O,d,P),E===null?S=O:E.sibling=O,E=O,_=H}if(P===f.length)return n(p,_),$&&kt(p,P),S;if(_===null){for(;PP?(H=_,_=null):H=_.sibling;var Te=h(p,_,O.value,g);if(Te===null){_===null&&(_=H);break}e&&_&&Te.alternate===null&&t(p,_),d=i(Te,d,P),E===null?S=Te:E.sibling=Te,E=Te,_=H}if(O.done)return n(p,_),$&&kt(p,P),S;if(_===null){for(;!O.done;P++,O=f.next())O=y(p,O.value,g),O!==null&&(d=i(O,d,P),E===null?S=O:E.sibling=O,E=O);return $&&kt(p,P),S}for(_=r(p,_);!O.done;P++,O=f.next())O=v(_,p,P,O.value,g),O!==null&&(e&&O.alternate!==null&&_.delete(O.key===null?P:O.key),d=i(O,d,P),E===null?S=O:E.sibling=O,E=O);return e&&_.forEach(function(fn){return t(p,fn)}),$&&kt(p,P),S}function R(p,d,f,g){if(typeof f=="object"&&f!==null&&f.type===Mt&&f.key===null&&(f=f.props.children),typeof f=="object"&&f!==null){switch(f.$$typeof){case cr:e:{for(var S=f.key,E=d;E!==null;){if(E.key===S){if(S=f.type,S===Mt){if(E.tag===7){n(p,E.sibling),d=l(E,f.props.children),d.return=p,p=d;break e}}else if(E.elementType===S||typeof S=="object"&&S!==null&&S.$$typeof===be&&Io(S)===E.type){n(p,E.sibling),d=l(E,f.props),d.ref=wn(p,E,f),d.return=p,p=d;break e}n(p,E);break}else t(p,E);E=E.sibling}f.type===Mt?(d=Et(f.props.children,p.mode,g,f.key),d.return=p,p=d):(g=Mr(f.type,f.key,f.props,null,p.mode,g),g.ref=wn(p,d,f),g.return=p,p=g)}return s(p);case Dt:e:{for(E=f.key;d!==null;){if(d.key===E)if(d.tag===4&&d.stateNode.containerInfo===f.containerInfo&&d.stateNode.implementation===f.implementation){n(p,d.sibling),d=l(d,f.children||[]),d.return=p,p=d;break e}else{n(p,d);break}else t(p,d);d=d.sibling}d=ni(f,p.mode,g),d.return=p,p=d}return s(p);case be:return E=f._init,R(p,d,E(f._payload),g)}if(Cn(f))return w(p,d,f,g);if(hn(f))return x(p,d,f,g);xr(p,f)}return typeof f=="string"&&f!==""||typeof f=="number"?(f=""+f,d!==null&&d.tag===6?(n(p,d.sibling),d=l(d,f),d.return=p,p=d):(n(p,d),d=ti(f,p.mode,g),d.return=p,p=d),s(p)):n(p,d)}return R}var rn=Sa(!0),ja=Sa(!1),qr=vt(null),br=null,Qt=null,xs=null;function Ss(){xs=Qt=br=null}function js(e){var t=qr.current;U(qr),e._currentValue=t}function Oi(e,t,n){for(;e!==null;){var r=e.alternate;if((e.childLanes&t)!==t?(e.childLanes|=t,r!==null&&(r.childLanes|=t)):r!==null&&(r.childLanes&t)!==t&&(r.childLanes|=t),e===n)break;e=e.return}}function qt(e,t){br=e,xs=Qt=null,e=e.dependencies,e!==null&&e.firstContext!==null&&(e.lanes&t&&(pe=!0),e.firstContext=null)}function _e(e){var t=e._currentValue;if(xs!==e)if(e={context:e,memoizedValue:t,next:null},Qt===null){if(br===null)throw Error(k(308));Qt=e,br.dependencies={lanes:0,firstContext:e}}else Qt=Qt.next=e;return t}var jt=null;function Cs(e){jt===null?jt=[e]:jt.push(e)}function Ca(e,t,n,r){var l=t.interleaved;return l===null?(n.next=n,Cs(t)):(n.next=l.next,l.next=n),t.interleaved=n,Xe(e,r)}function Xe(e,t){e.lanes|=t;var n=e.alternate;for(n!==null&&(n.lanes|=t),n=e,e=e.return;e!==null;)e.childLanes|=t,n=e.alternate,n!==null&&(n.childLanes|=t),n=e,e=e.return;return n.tag===3?n.stateNode:null}var et=!1;function Ns(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,interleaved:null,lanes:0},effects:null}}function Na(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,effects:e.effects})}function Ke(e,t){return{eventTime:e,lane:t,tag:0,payload:null,callback:null,next:null}}function at(e,t,n){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,I&2){var l=r.pending;return l===null?t.next=t:(t.next=l.next,l.next=t),r.pending=t,Xe(e,n)}return l=r.interleaved,l===null?(t.next=t,Cs(r)):(t.next=l.next,l.next=t),r.interleaved=t,Xe(e,n)}function Lr(e,t,n){if(t=t.updateQueue,t!==null&&(t=t.shared,(n&4194240)!==0)){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,cs(e,n)}}function Fo(e,t){var n=e.updateQueue,r=e.alternate;if(r!==null&&(r=r.updateQueue,n===r)){var l=null,i=null;if(n=n.firstBaseUpdate,n!==null){do{var s={eventTime:n.eventTime,lane:n.lane,tag:n.tag,payload:n.payload,callback:n.callback,next:null};i===null?l=i=s:i=i.next=s,n=n.next}while(n!==null);i===null?l=i=t:i=i.next=t}else l=i=t;n={baseState:r.baseState,firstBaseUpdate:l,lastBaseUpdate:i,shared:r.shared,effects:r.effects},e.updateQueue=n;return}e=n.lastBaseUpdate,e===null?n.firstBaseUpdate=t:e.next=t,n.lastBaseUpdate=t}function el(e,t,n,r){var l=e.updateQueue;et=!1;var i=l.firstBaseUpdate,s=l.lastBaseUpdate,u=l.shared.pending;if(u!==null){l.shared.pending=null;var a=u,c=a.next;a.next=null,s===null?i=c:s.next=c,s=a;var m=e.alternate;m!==null&&(m=m.updateQueue,u=m.lastBaseUpdate,u!==s&&(u===null?m.firstBaseUpdate=c:u.next=c,m.lastBaseUpdate=a))}if(i!==null){var y=l.baseState;s=0,m=c=a=null,u=i;do{var h=u.lane,v=u.eventTime;if((r&h)===h){m!==null&&(m=m.next={eventTime:v,lane:0,tag:u.tag,payload:u.payload,callback:u.callback,next:null});e:{var w=e,x=u;switch(h=t,v=n,x.tag){case 1:if(w=x.payload,typeof w=="function"){y=w.call(v,y,h);break e}y=w;break e;case 3:w.flags=w.flags&-65537|128;case 0:if(w=x.payload,h=typeof w=="function"?w.call(v,y,h):w,h==null)break e;y=V({},y,h);break e;case 2:et=!0}}u.callback!==null&&u.lane!==0&&(e.flags|=64,h=l.effects,h===null?l.effects=[u]:h.push(u))}else v={eventTime:v,lane:h,tag:u.tag,payload:u.payload,callback:u.callback,next:null},m===null?(c=m=v,a=y):m=m.next=v,s|=h;if(u=u.next,u===null){if(u=l.shared.pending,u===null)break;h=u,u=h.next,h.next=null,l.lastBaseUpdate=h,l.shared.pending=null}}while(!0);if(m===null&&(a=y),l.baseState=a,l.firstBaseUpdate=c,l.lastBaseUpdate=m,t=l.shared.interleaved,t!==null){l=t;do s|=l.lane,l=l.next;while(l!==t)}else i===null&&(l.shared.lanes=0);zt|=s,e.lanes=s,e.memoizedState=y}}function Do(e,t,n){if(e=t.effects,t.effects=null,e!==null)for(t=0;tn?n:4,e(!0);var r=Zl.transition;Zl.transition={};try{e(!1),t()}finally{F=n,Zl.transition=r}}function Va(){return Pe().memoizedState}function Sf(e,t,n){var r=dt(e);if(n={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null},Wa(e))Ha(t,n);else if(n=Ca(e,t,n,r),n!==null){var l=ae();Fe(n,e,r,l),Qa(n,t,r)}}function jf(e,t,n){var r=dt(e),l={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null};if(Wa(e))Ha(t,l);else{var i=e.alternate;if(e.lanes===0&&(i===null||i.lanes===0)&&(i=t.lastRenderedReducer,i!==null))try{var s=t.lastRenderedState,u=i(s,n);if(l.hasEagerState=!0,l.eagerState=u,De(u,s)){var a=t.interleaved;a===null?(l.next=l,Cs(t)):(l.next=a.next,a.next=l),t.interleaved=l;return}}catch{}finally{}n=Ca(e,t,l,r),n!==null&&(l=ae(),Fe(n,e,r,l),Qa(n,t,r))}}function Wa(e){var t=e.alternate;return e===B||t!==null&&t===B}function Ha(e,t){Rn=nl=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function Qa(e,t,n){if(n&4194240){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,cs(e,n)}}var rl={readContext:_e,useCallback:le,useContext:le,useEffect:le,useImperativeHandle:le,useInsertionEffect:le,useLayoutEffect:le,useMemo:le,useReducer:le,useRef:le,useState:le,useDebugValue:le,useDeferredValue:le,useTransition:le,useMutableSource:le,useSyncExternalStore:le,useId:le,unstable_isNewReconciler:!1},Cf={readContext:_e,useCallback:function(e,t){return Ue().memoizedState=[e,t===void 0?null:t],e},useContext:_e,useEffect:Uo,useImperativeHandle:function(e,t,n){return n=n!=null?n.concat([e]):null,Or(4194308,4,Ma.bind(null,t,e),n)},useLayoutEffect:function(e,t){return Or(4194308,4,e,t)},useInsertionEffect:function(e,t){return Or(4,2,e,t)},useMemo:function(e,t){var n=Ue();return t=t===void 0?null:t,e=e(),n.memoizedState=[e,t],e},useReducer:function(e,t,n){var r=Ue();return t=n!==void 0?n(t):t,r.memoizedState=r.baseState=t,e={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:t},r.queue=e,e=e.dispatch=Sf.bind(null,B,e),[r.memoizedState,e]},useRef:function(e){var t=Ue();return e={current:e},t.memoizedState=e},useState:Mo,useDebugValue:Os,useDeferredValue:function(e){return Ue().memoizedState=e},useTransition:function(){var e=Mo(!1),t=e[0];return e=xf.bind(null,e[1]),Ue().memoizedState=e,[t,e]},useMutableSource:function(){},useSyncExternalStore:function(e,t,n){var r=B,l=Ue();if($){if(n===void 0)throw Error(k(407));n=n()}else{if(n=t(),ee===null)throw Error(k(349));Tt&30||Ta(r,t,n)}l.memoizedState=n;var i={value:n,getSnapshot:t};return l.queue=i,Uo(La.bind(null,r,i,e),[e]),r.flags|=2048,Jn(9,za.bind(null,r,i,n,t),void 0,null),n},useId:function(){var e=Ue(),t=ee.identifierPrefix;if($){var n=Qe,r=He;n=(r&~(1<<32-Ie(r)-1)).toString(32)+n,t=":"+t+"R"+n,n=Xn++,0<\/script>",e=e.removeChild(e.firstChild)):typeof r.is=="string"?e=s.createElement(n,{is:r.is}):(e=s.createElement(n),n==="select"&&(s=e,r.multiple?s.multiple=!0:r.size&&(s.size=r.size))):e=s.createElementNS(e,n),e[$e]=t,e[Kn]=r,tc(e,t,!1,!1),t.stateNode=e;e:{switch(s=mi(n,r),n){case"dialog":M("cancel",e),M("close",e),l=r;break;case"iframe":case"object":case"embed":M("load",e),l=r;break;case"video":case"audio":for(l=0;lon&&(t.flags|=128,r=!0,kn(i,!1),t.lanes=4194304)}else{if(!r)if(e=tl(s),e!==null){if(t.flags|=128,r=!0,n=e.updateQueue,n!==null&&(t.updateQueue=n,t.flags|=4),kn(i,!0),i.tail===null&&i.tailMode==="hidden"&&!s.alternate&&!$)return ie(t),null}else 2*K()-i.renderingStartTime>on&&n!==1073741824&&(t.flags|=128,r=!0,kn(i,!1),t.lanes=4194304);i.isBackwards?(s.sibling=t.child,t.child=s):(n=i.last,n!==null?n.sibling=s:t.child=s,i.last=s)}return i.tail!==null?(t=i.tail,i.rendering=t,i.tail=t.sibling,i.renderingStartTime=K(),t.sibling=null,n=A.current,D(A,r?n&1|2:n&1),t):(ie(t),null);case 22:case 23:return $s(),r=t.memoizedState!==null,e!==null&&e.memoizedState!==null!==r&&(t.flags|=8192),r&&t.mode&1?ye&1073741824&&(ie(t),t.subtreeFlags&6&&(t.flags|=8192)):ie(t),null;case 24:return null;case 25:return null}throw Error(k(156,t.tag))}function Rf(e,t){switch(ws(t),t.tag){case 1:return me(t.type)&&Yr(),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return ln(),U(he),U(oe),Ps(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 5:return _s(t),null;case 13:if(U(A),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(k(340));nn()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return U(A),null;case 4:return ln(),null;case 10:return js(t.type._context),null;case 22:case 23:return $s(),null;case 24:return null;default:return null}}var jr=!1,se=!1,Of=typeof WeakSet=="function"?WeakSet:Set,j=null;function Kt(e,t){var n=e.ref;if(n!==null)if(typeof n=="function")try{n(null)}catch(r){W(e,t,r)}else n.current=null}function Vi(e,t,n){try{n()}catch(r){W(e,t,r)}}var Xo=!1;function If(e,t){if(Ni=Hr,e=ua(),ys(e)){if("selectionStart"in e)var n={start:e.selectionStart,end:e.selectionEnd};else e:{n=(n=e.ownerDocument)&&n.defaultView||window;var r=n.getSelection&&n.getSelection();if(r&&r.rangeCount!==0){n=r.anchorNode;var l=r.anchorOffset,i=r.focusNode;r=r.focusOffset;try{n.nodeType,i.nodeType}catch{n=null;break e}var s=0,u=-1,a=-1,c=0,m=0,y=e,h=null;t:for(;;){for(var v;y!==n||l!==0&&y.nodeType!==3||(u=s+l),y!==i||r!==0&&y.nodeType!==3||(a=s+r),y.nodeType===3&&(s+=y.nodeValue.length),(v=y.firstChild)!==null;)h=y,y=v;for(;;){if(y===e)break t;if(h===n&&++c===l&&(u=s),h===i&&++m===r&&(a=s),(v=y.nextSibling)!==null)break;y=h,h=y.parentNode}y=v}n=u===-1||a===-1?null:{start:u,end:a}}else n=null}n=n||{start:0,end:0}}else n=null;for(Ei={focusedElem:e,selectionRange:n},Hr=!1,j=t;j!==null;)if(t=j,e=t.child,(t.subtreeFlags&1028)!==0&&e!==null)e.return=t,j=e;else for(;j!==null;){t=j;try{var w=t.alternate;if(t.flags&1024)switch(t.tag){case 0:case 11:case 15:break;case 1:if(w!==null){var x=w.memoizedProps,R=w.memoizedState,p=t.stateNode,d=p.getSnapshotBeforeUpdate(t.elementType===t.type?x:Le(t.type,x),R);p.__reactInternalSnapshotBeforeUpdate=d}break;case 3:var f=t.stateNode.containerInfo;f.nodeType===1?f.textContent="":f.nodeType===9&&f.documentElement&&f.removeChild(f.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(k(163))}}catch(g){W(t,t.return,g)}if(e=t.sibling,e!==null){e.return=t.return,j=e;break}j=t.return}return w=Xo,Xo=!1,w}function On(e,t,n){var r=t.updateQueue;if(r=r!==null?r.lastEffect:null,r!==null){var l=r=r.next;do{if((l.tag&e)===e){var i=l.destroy;l.destroy=void 0,i!==void 0&&Vi(t,n,i)}l=l.next}while(l!==r)}}function gl(e,t){if(t=t.updateQueue,t=t!==null?t.lastEffect:null,t!==null){var n=t=t.next;do{if((n.tag&e)===e){var r=n.create;n.destroy=r()}n=n.next}while(n!==t)}}function Wi(e){var t=e.ref;if(t!==null){var n=e.stateNode;switch(e.tag){case 5:e=n;break;default:e=n}typeof t=="function"?t(e):t.current=e}}function lc(e){var t=e.alternate;t!==null&&(e.alternate=null,lc(t)),e.child=null,e.deletions=null,e.sibling=null,e.tag===5&&(t=e.stateNode,t!==null&&(delete t[$e],delete t[Kn],delete t[Ti],delete t[vf],delete t[yf])),e.stateNode=null,e.return=null,e.dependencies=null,e.memoizedProps=null,e.memoizedState=null,e.pendingProps=null,e.stateNode=null,e.updateQueue=null}function ic(e){return e.tag===5||e.tag===3||e.tag===4}function Zo(e){e:for(;;){for(;e.sibling===null;){if(e.return===null||ic(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.flags&2||e.child===null||e.tag===4)continue e;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function Hi(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.nodeType===8?n.parentNode.insertBefore(e,t):n.insertBefore(e,t):(n.nodeType===8?(t=n.parentNode,t.insertBefore(e,n)):(t=n,t.appendChild(e)),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=Gr));else if(r!==4&&(e=e.child,e!==null))for(Hi(e,t,n),e=e.sibling;e!==null;)Hi(e,t,n),e=e.sibling}function Qi(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(r!==4&&(e=e.child,e!==null))for(Qi(e,t,n),e=e.sibling;e!==null;)Qi(e,t,n),e=e.sibling}var te=null,Re=!1;function qe(e,t,n){for(n=n.child;n!==null;)sc(e,t,n),n=n.sibling}function sc(e,t,n){if(Ae&&typeof Ae.onCommitFiberUnmount=="function")try{Ae.onCommitFiberUnmount(cl,n)}catch{}switch(n.tag){case 5:se||Kt(n,t);case 6:var r=te,l=Re;te=null,qe(e,t,n),te=r,Re=l,te!==null&&(Re?(e=te,n=n.stateNode,e.nodeType===8?e.parentNode.removeChild(n):e.removeChild(n)):te.removeChild(n.stateNode));break;case 18:te!==null&&(Re?(e=te,n=n.stateNode,e.nodeType===8?Gl(e.parentNode,n):e.nodeType===1&&Gl(e,n),Bn(e)):Gl(te,n.stateNode));break;case 4:r=te,l=Re,te=n.stateNode.containerInfo,Re=!0,qe(e,t,n),te=r,Re=l;break;case 0:case 11:case 14:case 15:if(!se&&(r=n.updateQueue,r!==null&&(r=r.lastEffect,r!==null))){l=r=r.next;do{var i=l,s=i.destroy;i=i.tag,s!==void 0&&(i&2||i&4)&&Vi(n,t,s),l=l.next}while(l!==r)}qe(e,t,n);break;case 1:if(!se&&(Kt(n,t),r=n.stateNode,typeof r.componentWillUnmount=="function"))try{r.props=n.memoizedProps,r.state=n.memoizedState,r.componentWillUnmount()}catch(u){W(n,t,u)}qe(e,t,n);break;case 21:qe(e,t,n);break;case 22:n.mode&1?(se=(r=se)||n.memoizedState!==null,qe(e,t,n),se=r):qe(e,t,n);break;default:qe(e,t,n)}}function Jo(e){var t=e.updateQueue;if(t!==null){e.updateQueue=null;var n=e.stateNode;n===null&&(n=e.stateNode=new Of),t.forEach(function(r){var l=Wf.bind(null,e,r);n.has(r)||(n.add(r),r.then(l,l))})}}function ze(e,t){var n=t.deletions;if(n!==null)for(var r=0;rl&&(l=s),r&=~i}if(r=l,r=K()-r,r=(120>r?120:480>r?480:1080>r?1080:1920>r?1920:3e3>r?3e3:4320>r?4320:1960*Df(r/1960))-r,10e?16:e,lt===null)var r=!1;else{if(e=lt,lt=null,sl=0,I&6)throw Error(k(331));var l=I;for(I|=4,j=e.current;j!==null;){var i=j,s=i.child;if(j.flags&16){var u=i.deletions;if(u!==null){for(var a=0;aK()-Ms?Nt(e,0):Ds|=n),ve(e,t)}function hc(e,t){t===0&&(e.mode&1?(t=hr,hr<<=1,!(hr&130023424)&&(hr=4194304)):t=1);var n=ae();e=Xe(e,t),e!==null&&(nr(e,t,n),ve(e,n))}function Vf(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),hc(e,n)}function Wf(e,t){var n=0;switch(e.tag){case 13:var r=e.stateNode,l=e.memoizedState;l!==null&&(n=l.retryLane);break;case 19:r=e.stateNode;break;default:throw Error(k(314))}r!==null&&r.delete(t),hc(e,n)}var mc;mc=function(e,t,n){if(e!==null)if(e.memoizedProps!==t.pendingProps||he.current)pe=!0;else{if(!(e.lanes&n)&&!(t.flags&128))return pe=!1,zf(e,t,n);pe=!!(e.flags&131072)}else pe=!1,$&&t.flags&1048576&&wa(t,Jr,t.index);switch(t.lanes=0,t.tag){case 2:var r=t.type;Ir(e,t),e=t.pendingProps;var l=tn(t,oe.current);qt(t,n),l=zs(null,t,r,e,l,n);var i=Ls();return t.flags|=1,typeof l=="object"&&l!==null&&typeof l.render=="function"&&l.$$typeof===void 0?(t.tag=1,t.memoizedState=null,t.updateQueue=null,me(r)?(i=!0,Xr(t)):i=!1,t.memoizedState=l.state!==null&&l.state!==void 0?l.state:null,Ns(t),l.updater=yl,t.stateNode=l,l._reactInternals=t,Fi(t,r,e,n),t=Ui(null,t,r,!0,i,n)):(t.tag=0,$&&i&&gs(t),ue(null,t,l,n),t=t.child),t;case 16:r=t.elementType;e:{switch(Ir(e,t),e=t.pendingProps,l=r._init,r=l(r._payload),t.type=r,l=t.tag=Qf(r),e=Le(r,e),l){case 0:t=Mi(null,t,r,e,n);break e;case 1:t=Ko(null,t,r,e,n);break e;case 11:t=Ho(null,t,r,e,n);break e;case 14:t=Qo(null,t,r,Le(r.type,e),n);break e}throw Error(k(306,r,""))}return t;case 0:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Le(r,l),Mi(e,t,r,l,n);case 1:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Le(r,l),Ko(e,t,r,l,n);case 3:e:{if(qa(t),e===null)throw Error(k(387));r=t.pendingProps,i=t.memoizedState,l=i.element,Na(e,t),el(t,r,null,n);var s=t.memoizedState;if(r=s.element,i.isDehydrated)if(i={element:r,isDehydrated:!1,cache:s.cache,pendingSuspenseBoundaries:s.pendingSuspenseBoundaries,transitions:s.transitions},t.updateQueue.baseState=i,t.memoizedState=i,t.flags&256){l=sn(Error(k(423)),t),t=Go(e,t,r,n,l);break e}else if(r!==l){l=sn(Error(k(424)),t),t=Go(e,t,r,n,l);break e}else for(ge=ut(t.stateNode.containerInfo.firstChild),we=t,$=!0,Oe=null,n=ja(t,null,r,n),t.child=n;n;)n.flags=n.flags&-3|4096,n=n.sibling;else{if(nn(),r===l){t=Ze(e,t,n);break e}ue(e,t,r,n)}t=t.child}return t;case 5:return Ea(t),e===null&&Ri(t),r=t.type,l=t.pendingProps,i=e!==null?e.memoizedProps:null,s=l.children,_i(r,l)?s=null:i!==null&&_i(r,i)&&(t.flags|=32),Ja(e,t),ue(e,t,s,n),t.child;case 6:return e===null&&Ri(t),null;case 13:return ba(e,t,n);case 4:return Es(t,t.stateNode.containerInfo),r=t.pendingProps,e===null?t.child=rn(t,null,r,n):ue(e,t,r,n),t.child;case 11:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Le(r,l),Ho(e,t,r,l,n);case 7:return ue(e,t,t.pendingProps,n),t.child;case 8:return ue(e,t,t.pendingProps.children,n),t.child;case 12:return ue(e,t,t.pendingProps.children,n),t.child;case 10:e:{if(r=t.type._context,l=t.pendingProps,i=t.memoizedProps,s=l.value,D(qr,r._currentValue),r._currentValue=s,i!==null)if(De(i.value,s)){if(i.children===l.children&&!he.current){t=Ze(e,t,n);break e}}else for(i=t.child,i!==null&&(i.return=t);i!==null;){var u=i.dependencies;if(u!==null){s=i.child;for(var a=u.firstContext;a!==null;){if(a.context===r){if(i.tag===1){a=Ke(-1,n&-n),a.tag=2;var c=i.updateQueue;if(c!==null){c=c.shared;var m=c.pending;m===null?a.next=a:(a.next=m.next,m.next=a),c.pending=a}}i.lanes|=n,a=i.alternate,a!==null&&(a.lanes|=n),Oi(i.return,n,t),u.lanes|=n;break}a=a.next}}else if(i.tag===10)s=i.type===t.type?null:i.child;else if(i.tag===18){if(s=i.return,s===null)throw Error(k(341));s.lanes|=n,u=s.alternate,u!==null&&(u.lanes|=n),Oi(s,n,t),s=i.sibling}else s=i.child;if(s!==null)s.return=i;else for(s=i;s!==null;){if(s===t){s=null;break}if(i=s.sibling,i!==null){i.return=s.return,s=i;break}s=s.return}i=s}ue(e,t,l.children,n),t=t.child}return t;case 9:return l=t.type,r=t.pendingProps.children,qt(t,n),l=_e(l),r=r(l),t.flags|=1,ue(e,t,r,n),t.child;case 14:return r=t.type,l=Le(r,t.pendingProps),l=Le(r.type,l),Qo(e,t,r,l,n);case 15:return Xa(e,t,t.type,t.pendingProps,n);case 17:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Le(r,l),Ir(e,t),t.tag=1,me(r)?(e=!0,Xr(t)):e=!1,qt(t,n),Ka(t,r,l),Fi(t,r,l,n),Ui(null,t,r,!0,e,n);case 19:return ec(e,t,n);case 22:return Za(e,t,n)}throw Error(k(156,t.tag))};function vc(e,t){return Wu(e,t)}function Hf(e,t,n,r){this.tag=e,this.key=n,this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null,this.index=0,this.ref=null,this.pendingProps=t,this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null,this.mode=r,this.subtreeFlags=this.flags=0,this.deletions=null,this.childLanes=this.lanes=0,this.alternate=null}function Ne(e,t,n,r){return new Hf(e,t,n,r)}function Bs(e){return e=e.prototype,!(!e||!e.isReactComponent)}function Qf(e){if(typeof e=="function")return Bs(e)?1:0;if(e!=null){if(e=e.$$typeof,e===ss)return 11;if(e===os)return 14}return 2}function ft(e,t){var n=e.alternate;return n===null?(n=Ne(e.tag,t,e.key,e.mode),n.elementType=e.elementType,n.type=e.type,n.stateNode=e.stateNode,n.alternate=e,e.alternate=n):(n.pendingProps=t,n.type=e.type,n.flags=0,n.subtreeFlags=0,n.deletions=null),n.flags=e.flags&14680064,n.childLanes=e.childLanes,n.lanes=e.lanes,n.child=e.child,n.memoizedProps=e.memoizedProps,n.memoizedState=e.memoizedState,n.updateQueue=e.updateQueue,t=e.dependencies,n.dependencies=t===null?null:{lanes:t.lanes,firstContext:t.firstContext},n.sibling=e.sibling,n.index=e.index,n.ref=e.ref,n}function Mr(e,t,n,r,l,i){var s=2;if(r=e,typeof e=="function")Bs(e)&&(s=1);else if(typeof e=="string")s=5;else e:switch(e){case Mt:return Et(n.children,l,i,t);case is:s=8,l|=8;break;case ii:return e=Ne(12,n,t,l|2),e.elementType=ii,e.lanes=i,e;case si:return e=Ne(13,n,t,l),e.elementType=si,e.lanes=i,e;case oi:return e=Ne(19,n,t,l),e.elementType=oi,e.lanes=i,e;case Eu:return kl(n,l,i,t);default:if(typeof e=="object"&&e!==null)switch(e.$$typeof){case Cu:s=10;break e;case Nu:s=9;break e;case ss:s=11;break e;case os:s=14;break e;case be:s=16,r=null;break e}throw Error(k(130,e==null?e:typeof e,""))}return t=Ne(s,n,t,l),t.elementType=e,t.type=r,t.lanes=i,t}function Et(e,t,n,r){return e=Ne(7,e,r,t),e.lanes=n,e}function kl(e,t,n,r){return e=Ne(22,e,r,t),e.elementType=Eu,e.lanes=n,e.stateNode={isHidden:!1},e}function ti(e,t,n){return e=Ne(6,e,null,t),e.lanes=n,e}function ni(e,t,n){return t=Ne(4,e.children!==null?e.children:[],e.key,t),t.lanes=n,t.stateNode={containerInfo:e.containerInfo,pendingChildren:null,implementation:e.implementation},t}function Kf(e,t,n,r,l){this.tag=t,this.containerInfo=e,this.finishedWork=this.pingCache=this.current=this.pendingChildren=null,this.timeoutHandle=-1,this.callbackNode=this.pendingContext=this.context=null,this.callbackPriority=0,this.eventTimes=Dl(0),this.expirationTimes=Dl(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=Dl(0),this.identifierPrefix=r,this.onRecoverableError=l,this.mutableSourceEagerHydrationData=null}function Vs(e,t,n,r,l,i,s,u,a){return e=new Kf(e,t,n,u,a),t===1?(t=1,i===!0&&(t|=8)):t=0,i=Ne(3,null,null,t),e.current=i,i.stateNode=e,i.memoizedState={element:r,isDehydrated:n,cache:null,transitions:null,pendingSuspenseBoundaries:null},Ns(i),e}function Gf(e,t,n){var r=3"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(kc)}catch(e){console.error(e)}}kc(),ku.exports=xe;var qf=ku.exports,xc,iu=qf;xc=iu.createRoot,iu.hydrateRoot;const Sc="bv.access",Ks="bv.refresh";function Zi(e){try{return localStorage.getItem(e)||""}catch{return""}}function su(e,t){try{t?localStorage.setItem(e,t):localStorage.removeItem(e)}catch{}}function Nl(){return{access:Zi(Sc),refresh:Zi(Ks)}}function Gs(e,t){su(Sc,e),su(Ks,t)}function sr(){Gs("","")}function bf(){return!!Zi(Ks)}class bn extends Error{constructor(t,n,r){super(r),this.status=t,this.code=n}}let Sn=null;async function Ys(){return Sn||(Sn=(async()=>{const{refresh:e}=Nl();if(!e)throw new bn(401,"unauthorized","Signed out.");const t=await fetch("/api/auth/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refresh_token:e})});if(!t.ok)throw sr(),new bn(t.status,"unauthorized","Your session has ended. Sign in again.");const n=await t.json();return Gs(n.access_token,n.refresh_token),n})().finally(()=>{Sn=null}),Sn)}async function G(e,t,n,r=!0){const{access:l}=Nl(),i={};l&&(i.Authorization="Bearer "+l),n!==void 0&&(i["Content-Type"]="application/json");const s=await fetch(t,{method:e,headers:i,body:n===void 0?void 0:JSON.stringify(n)});if(s.status===204)return null;const u=await s.text();let a=null;try{a=u?JSON.parse(u):null}catch{}if(s.ok)return a;const c=(a==null?void 0:a.error)||"";if(c==="token_expired"&&r)return await Ys(),G(e,t,n,!1);throw s.status===401&&sr(),new bn(s.status,c,(a==null?void 0:a.message)||`Something went wrong (${s.status}).`)}async function jc(e,t=!0){const{access:n}=Nl(),r=await fetch(e,{headers:n?{Authorization:"Bearer "+n}:{}});if(r.ok)return URL.createObjectURL(await r.blob());let l=null;try{l=await r.json()}catch{}const i=(l==null?void 0:l.error)||"";if(i==="token_expired"&&t)return await Ys(),jc(e,!1);throw r.status===401&&sr(),new bn(r.status,i,(l==null?void 0:l.message)||`That picture could not be loaded (${r.status}).`)}const Ft=e=>{const t=new URLSearchParams;for(const[r,l]of Object.entries(e||{}))l!=null&&l!==""&&t.set(r,l);const n=t.toString();return n?"?"+n:""},J={async login(e,t){const n=await fetch("/api/auth/login",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({email:e,password:t})}),r=await n.json().catch(()=>null);if(!n.ok)throw new bn(n.status,(r==null?void 0:r.error)||"",(r==null?void 0:r.message)||"Could not sign in.");return Gs(r.access_token,r.refresh_token),r.user},async logout(){try{await G("POST","/api/auth/logout")}catch{}sr()},me:()=>G("GET","/api/auth/me"),sites:()=>G("GET","/api/sites"),arrivals:e=>G("GET","/api/visits"+Ft(e)),visitors:(e,t=50)=>G("GET","/api/visitors"+Ft({q:e,limit:t})),visitorHistory:(e,t=50)=>G("GET",`/api/visitors/${encodeURIComponent(e)}/history`+Ft({limit:t})),saveProfile:(e,t)=>G("PUT",`/api/visitors/${encodeURIComponent(e)}/profile`,t),footfall:e=>G("GET","/api/reports/footfall"+Ft(e)),conversion:e=>G("GET","/api/reports/conversion"+Ft(e)),cameras:()=>G("GET","/api/cameras"),cameraSnapshot:e=>jc(e),createCamera:(e,t)=>G("POST",`/api/sites/${encodeURIComponent(e)}/cameras`,t),updateCamera:(e,t)=>G("PATCH",`/api/cameras/${encodeURIComponent(e)}`,t),deleteCamera:e=>G("DELETE",`/api/cameras/${encodeURIComponent(e)}`),checkCamera:(e,t,n)=>G("POST",`/api/cameras/${encodeURIComponent(e)}/check`,{kind:t,...n?{seconds:n}:{}}),siteCheck:e=>G("GET",`/api/sites/${encodeURIComponent(e)}/check`),enrolmentCode:(e,t)=>G("POST",`/api/sites/${encodeURIComponent(e)}/enrolment-code`,t||{}),ask:e=>G("POST","/api/assistant",{history:e}),clients:()=>G("GET","/api/admin/clients"),createClient:e=>G("POST","/api/admin/clients",e)};function ep({cursor:e,siteId:t,onPage:n,onError:r,signal:l}){let i=!1,s=e||"";return(async()=>{for(;!i;){try{const{access:a}=Nl(),c=await fetch("/api/visits/stream"+Ft({cursor:s,site_id:t}),{headers:{Authorization:"Bearer "+a,Accept:"text/event-stream"},signal:l});if(c.status===401){await Ys();continue}if(!c.ok||!c.body)throw new Error("stream unavailable");const m=c.body.getReader(),y=new TextDecoder;let h="";for(;!i;){const{value:v,done:w}=await m.read();if(w)break;h+=y.decode(v,{stream:!0});let x;for(;(x=h.indexOf(` + +`))!==-1;){const R=h.slice(0,x);h=h.slice(x+2);for(const p of R.split(` +`))if(p.startsWith("id: "))s=p.slice(4).trim();else if(p.startsWith("data: "))try{n(JSON.parse(p.slice(6)))}catch{}}}}catch(a){if(i||l!=null&&l.aborted)return;r==null||r(a)}if(i)return;await new Promise(a=>setTimeout(a,3e3))}})(),()=>{i=!0}}function tp({onSignedIn:e}){const[t,n]=N.useState(""),[r,l]=N.useState(""),[i,s]=N.useState(""),[u,a]=N.useState(!1),c=async m=>{m.preventDefault(),a(!0),s("");try{await J.login(t.trim(),r),e(await J.me())}catch(y){s(y.message),a(!1)}};return o.jsx("div",{className:"signin",children:o.jsxs("form",{className:"card",onSubmit:c,children:[o.jsx("span",{className:"mark big","aria-hidden":"true"}),o.jsx("h1",{children:"Behavision"}),o.jsx("p",{className:"sub",children:"Sign in to your company account."}),o.jsxs("label",{children:["Email",o.jsx("input",{type:"email",value:t,autoComplete:"username",autoFocus:!0,required:!0,onChange:m=>n(m.target.value),placeholder:"you@company.com"})]}),o.jsxs("label",{children:["Password",o.jsx("input",{type:"password",value:r,autoComplete:"current-password",required:!0,onChange:m=>l(m.target.value)})]}),i&&o.jsx("p",{className:"error",role:"alert",children:i}),o.jsx("button",{className:"primary",disabled:u||!t||!r,children:u?"Signing in…":"Sign in"}),o.jsx("p",{className:"foot",children:"Accounts are created by Loyaly. Ask your account manager if you need one."})]})})}function er(e,t,n=[]){const[r,l]=N.useState(null),[i,s]=N.useState(null),[u,a]=N.useState(!0),c=N.useRef(!1),m=N.useRef(!0),y=N.useCallback(async()=>{if(!c.current){c.current=!0;try{const h=await e();if(!m.current)return;l(h),s(null)}catch(h){if(!m.current)return;s(h)}finally{c.current=!1,m.current&&a(!1)}}},n);return N.useEffect(()=>{if(m.current=!0,y(),!t)return()=>{m.current=!1};const h=setInterval(y,t);return()=>{m.current=!1,clearInterval(h)}},[y,t]),{data:r,error:i,loading:u,reload:y}}function np(e){const[t,n]=N.useState(null);return N.useEffect(()=>{if(!e){n(null);return}let r=!0,l=null;return J.cameraSnapshot(e).then(i=>{if(!r){URL.revokeObjectURL(i);return}l=i,n(i)}).catch(()=>{r&&n(null)}),()=>{r=!1,l&&URL.revokeObjectURL(l)}},[e]),t}function Cc({url:e,alt:t}){const n=typeof e=="string"&&e.startsWith("/"),r=np(n?e:null),l=n?r:e;return l?o.jsx("img",{src:l,alt:t,loading:"lazy"}):null}function rp({site:e,onClose:t}){const[n,r]=N.useState(null),[l,i]=N.useState(!1),[s,u]=N.useState(""),a=async()=>{i(!0),u(""),r(null);try{r(await J.siteCheck(e.site_id))}catch(c){u(c.message)}finally{i(!1)}};return o.jsx("div",{className:"overlay",onClick:t,children:o.jsxs("aside",{className:"drawer",onClick:c=>c.stopPropagation(),children:[o.jsxs("header",{className:"drawer-head",children:[o.jsxs("div",{children:[o.jsxs("h2",{children:["Is ",e.name," working?"]}),o.jsx("p",{className:"sub",children:"Checks the whole chain, from the shop’s PC to head office."})]}),o.jsx("button",{className:"ghost",onClick:t,children:"Close"})]}),o.jsxs("div",{className:"drawer-body",children:[!n&&!l&&o.jsxs(o.Fragment,{children:[o.jsx("p",{className:"sub",children:"Nothing is changed and no one is disturbed — this reads what the shop has already reported."}),o.jsx("button",{className:"primary",onClick:a,children:"Run the check"})]}),l&&o.jsxs("div",{className:"waiting",children:[o.jsx("span",{className:"spinner","aria-hidden":"true"}),o.jsx("b",{children:"Checking…"})]}),s&&o.jsx("p",{className:"error",role:"alert",children:s}),n&&o.jsxs(o.Fragment,{children:[o.jsx("div",{className:"banner "+(n.ok?"ok":"warn"),children:o.jsxs("div",{children:[o.jsx("b",{children:n.ok?"This shop is working.":"This shop needs attention."}),!n.ok&&o.jsx(o.Fragment,{children:" Work down the list — the first failure usually explains the rest."})]})}),o.jsx("ol",{className:"checklist",children:n.steps.map(c=>o.jsxs("li",{className:c.status,children:[o.jsx("span",{className:"mark","aria-hidden":"true",children:c.status==="pass"?"✓":c.status==="fail"?"✕":c.status==="warn"?"!":"–"}),o.jsxs("div",{children:[o.jsx("b",{children:c.name}),o.jsx("span",{className:"sub",children:c.detail}),c.advice&&o.jsx("span",{className:"advice-line",children:c.advice})]})]},c.name))}),o.jsx("button",{className:"ghost",onClick:a,children:"Check again"})]}),o.jsx(lp,{site:e})]})]})})}function lp({site:e}){const[t,n]=N.useState(null),[r,l]=N.useState(!1),[i,s]=N.useState(""),[u,a]=N.useState(""),c=async()=>{l(!0),s("");try{n(await J.enrolmentCode(e.site_id,{label:u.trim()}))}catch(m){s(m.message)}finally{l(!1)}};return o.jsxs("section",{className:"claim",children:[o.jsx("h3",{children:"Set up a shop PC"}),t?o.jsxs(o.Fragment,{children:[o.jsx("p",{className:"code","aria-live":"polite",children:t.code}),o.jsxs("p",{className:"sub",children:["Copy this now — it cannot be shown again. It works once, and stops working ",sp(t.expires_at),"."]}),o.jsxs("div",{className:"row",children:[o.jsx("button",{className:"ghost",onClick:()=>ip(t.code),children:"Copy"}),o.jsx("button",{className:"ghost",onClick:()=>n(null),children:"Done"})]})]}):o.jsxs(o.Fragment,{children:[o.jsx("p",{className:"sub",children:"Creates a one-time code to type into Behavision on the shop’s computer. Use one for a new shop, a replacement PC, or a reinstall."}),o.jsxs("label",{className:"field",children:[o.jsx("span",{children:"What is this PC? (optional)"}),o.jsx("input",{value:u,placeholder:"counter PC",onChange:m=>a(m.target.value)})]}),i&&o.jsx("p",{className:"error",role:"alert",children:i}),o.jsx("button",{className:"ghost",onClick:c,disabled:r,children:r?"Creating…":"Create an installation code"})]})]})}function ip(e){var t;try{(t=navigator.clipboard)==null||t.writeText(e)}catch{}}function sp(e){const t=new Date(e).getTime();if(Number.isNaN(t))return"shortly";const n=Math.round((t-Date.now())/864e5);return n<=0?"today":n===1?"tomorrow":`in ${n} days`}function op(){const{data:e,error:t,loading:n}=er(()=>J.sites(),2e4,[]),{data:r}=er(()=>J.cameras(),6e4,[]),[l,i]=N.useState(null),s=e||[];if(n&&!e)return o.jsx(El,{});if(t)return o.jsx(_l,{error:t});if(!s.length)return o.jsx(pp,{});const u=s.map(h=>{const v=(r||[]).filter(w=>w.site_id===h.site_id);return{site:h,cams:v,verdict:up(h,v)}}),a=h=>u.filter(v=>v.verdict.tone===h).length,c=a("bad"),m=a("warn"),y=a("idle");return o.jsxs(o.Fragment,{children:[o.jsxs("header",{className:"head",children:[o.jsx("h1",{children:"Shops"}),o.jsxs("p",{className:"sub",children:[s.length," ",s.length===1?"shop":"shops",c>0&&o.jsxs(o.Fragment,{children:[" · ",o.jsxs("b",{className:"bad",children:[c," not working"]})]}),m>0&&o.jsxs(o.Fragment,{children:[" · ",o.jsxs("b",{className:"warn",children:[m," needing attention"]})]}),y>0&&o.jsxs(o.Fragment,{children:[" · ",y," not set up yet"]}),!c&&!m&&!y&&o.jsxs(o.Fragment,{children:[" · ",o.jsx("b",{className:"ok",children:"all working"})]})]})]}),o.jsx("div",{className:"grid sites",children:u.map(h=>o.jsx(cp,{site:h.site,cams:h.cams,verdict:h.verdict,onCheck:()=>i(h.site)},h.site.site_id))}),l&&o.jsx(rp,{site:l,onClose:()=>i(null)})]})}function up(e,t){const n=e.fraction_below_gate,r=e.cameras_total||t.length;return e.online?e.dropped>0?{tone:"bad",mark:"✕",words:`${e.dropped} visits lost and unrecoverable`}:r===0?{tone:"idle",mark:"+",headline:"Not set up",words:"No cameras set up yet"}:e.cameras_up===0?{tone:"bad",mark:"✕",words:"No cameras connected"}:e.cameras_up.5?{tone:"bad",mark:"✕",words:`${Ji(n)} of faces too poor to recognise`}:n>.2?{tone:"warn",mark:"!",words:`${Ji(n)} of faces too poor to recognise`}:e.queued>0?{tone:"warn",mark:"!",words:`${e.queued} visits waiting to upload`}:{tone:"ok",mark:"✓",words:`Working — ${r} ${r===1?"camera":"cameras"} connected`}:{tone:"bad",mark:"✕",headline:"Offline",words:`Offline — last heard from ${un(e.last_heartbeat_at)}`}}const ap={ok:"Working",warn:"Needs attention",bad:"Not working",idle:"Not set up"};function cp({site:e,cams:t,verdict:n,onCheck:r}){const l=e.fraction_below_gate,i=n.tone,s=n.headline||ap[i],u=dp(t),a=e.cameras_total||t.length;return o.jsxs("article",{className:"card site state-"+i,onClick:r,role:"button",tabIndex:0,onKeyDown:c=>c.key==="Enter"&&r(),children:[o.jsxs("div",{className:"shot",children:[u.url?o.jsx(Cc,{url:u.url,alt:`View inside ${e.name}`}):o.jsxs("div",{className:"noshot",children:[o.jsx(fp,{}),u.reason&&o.jsx("span",{children:u.reason})]}),o.jsxs("div",{className:"shot-over",children:[o.jsxs("div",{className:"shot-name",children:[o.jsx("b",{children:e.name}),o.jsx("span",{children:a>0?`${a} ${a===1?"camera":"cameras"}`:"No cameras yet"})]}),o.jsxs("span",{className:"status "+i,children:[o.jsx("i",{"aria-hidden":"true"}),s]})]}),u.at&&o.jsx("span",{className:"shot-age",children:un(u.at)})]}),o.jsxs("div",{className:"metrics",children:[o.jsx(ri,{label:"Cameras",value:a?`${e.cameras_up}/${a}`:"—",tone:a?e.cameras_up0?Ji(1-l):"—",tone:l?l>.5?"bad":l>.2?"warn":"ok":"idle"}),o.jsx(ri,{label:"Last seen",value:un(e.last_heartbeat_at),tone:e.online?"ok":"bad"})]}),o.jsxs("div",{className:"verdict "+n.tone,children:[o.jsx("span",{className:"mark","aria-hidden":"true",children:n.mark}),o.jsx("span",{className:"words",children:n.words}),o.jsx("span",{className:"go","aria-hidden":"true",children:"→"})]})]})}function ri({label:e,value:t,tone:n}){return o.jsxs("div",{className:"metric",children:[o.jsx("b",{className:n,children:t}),o.jsx("span",{children:e})]})}function dp(e){var r;if(!e.length)return{};let t=null;for(const l of e)!((r=l.snapshot)!=null&&r.available)||!l.snapshot.url||(!t||(l.snapshot_at||"")>(t.snapshot_at||""))&&(t=l);return t?{url:t.snapshot.url,at:t.snapshot_at}:{reason:e.map(l=>{var i;return(i=l.snapshot)==null?void 0:i.reason}).find(Boolean)||"No picture from this shop yet."}}function fp(){return o.jsxs("svg",{className:"shopmark",viewBox:"0 0 40 32","aria-hidden":"true",children:[o.jsx("path",{d:"M4 12h32v18H4z"}),o.jsx("path",{d:"M2 12l4-8h28l4 8"}),o.jsx("path",{d:"M15 30v-9h10v9"})]})}function Ji(e){return`${Math.round(e*100)}%`}function un(e){if(!e)return"never";const t=new Date(e).getTime();if(Number.isNaN(t))return"—";const n=Math.max(0,(Date.now()-t)/1e3);if(n<90)return"just now";const r=Math.round(n/60);if(r<60)return`${r} min ago`;const l=Math.round(r/60);return l<48?`${l} h ago`:`${Math.round(l/24)} days ago`}function El(){return o.jsxs("div",{className:"state",children:[o.jsx("span",{className:"spinner","aria-hidden":"true"}),"Loading…"]})}function _l({error:e}){return o.jsx("div",{className:"state",children:o.jsx("p",{className:"error",role:"alert",children:e.message})})}function pp(){return o.jsxs("div",{className:"state",children:[o.jsx("h2",{children:"No shops yet"}),o.jsx("p",{className:"sub",children:"A shop appears here once its PC has been claimed with an enrolment code."})]})}const ou=60;function hp(){const[e,t]=N.useState([]),[n,r]=N.useState("connecting"),[l,i]=N.useState(null),s=N.useRef(new Set);return N.useEffect(()=>{const u=new AbortController;let a=()=>{};return(async()=>{try{const c=await J.arrivals({limit:30}),m=c.arrivals||[];m.forEach(y=>s.current.add(y.visit_id)),t(m.slice().reverse()),r("live"),a=ep({cursor:c.cursor,signal:u.signal,onPage:y=>{const h=(y.arrivals||[]).filter(v=>!s.current.has(v.visit_id));h.length&&(h.forEach(v=>s.current.add(v.visit_id)),r("live"),t(v=>[...h.reverse(),...v].slice(0,ou)))},onError:()=>r("reconnecting")})}catch(c){i(c)}})(),()=>{u.abort(),a()}},[]),l?o.jsx(_l,{error:l}):n==="connecting"&&!e.length?o.jsx(El,{}):o.jsxs(o.Fragment,{children:[o.jsxs("header",{className:"head",children:[o.jsx("h1",{children:"Live"}),o.jsxs("p",{className:"sub",children:[o.jsx("span",{className:"dot "+(n==="live"?"ok":"warn"),"aria-hidden":"true"}),n==="live"?"Connected":"Reconnecting…"," · ","last ",ou," arrivals"]})]}),e.length===0?o.jsxs("div",{className:"state",children:[o.jsx("h2",{children:"Nobody yet"}),o.jsx("p",{className:"sub",children:"Arrivals appear here the moment a camera recognises someone."})]}):o.jsx("ul",{className:"arrivals",children:e.map(u=>o.jsx(mp,{a:u},u.visit_id))})]})}function mp({a:e}){const t=e.name||e.label||"Unidentified";return o.jsxs("li",{className:"card arrival",children:[o.jsx(vp,{image:e.image,name:t}),o.jsxs("div",{className:"who-col",children:[o.jsx("strong",{children:t}),o.jsxs("span",{className:"sub",children:[e.site,e.camera_id?` · ${e.camera_id}`:""," · ",un(e.occurred_at)]}),e.attributes&&o.jsx(gp,{attrs:e.attributes})]}),e.is_new_visitor?o.jsx("span",{className:"pill new",children:"New"}):o.jsx("span",{className:"pill",children:"Returning"})]})}function vp({image:e,name:t}){return e!=null&&e.available?o.jsx("img",{className:"face",src:e.url,alt:"",loading:"lazy"}):o.jsx("span",{className:"face initials",title:(e==null?void 0:e.reason)||"","aria-hidden":"true",children:yp(t)})}function yp(e){const t=String(e).trim().split(/\s+/).filter(Boolean);return t.length?t.length===1?t[0].slice(0,2).toUpperCase():(t[0][0]+t[t.length-1][0]).toUpperCase():"?"}function gp({attrs:e}){const t=[];return e.gender&&t.push(e.gender),e.age&&t.push(`~${Math.round(e.age)}`),e.emotion&&t.push(e.emotion),t.length?o.jsx("span",{className:"attrs",children:t.join(" · ")}):null}const Ur=[{id:"hikvision",label:"Hikvision",path:"/Streaming/Channels/101",note:"Channel 1, main stream. Use /Streaming/Channels/102 for the lower-quality sub stream."},{id:"dahua",label:"Dahua",path:"/cam/realmonitor?channel=1&subtype=0",note:"Channel 1, main stream. subtype=1 is the sub stream."},{id:"cpplus",label:"CP Plus",path:"/cam/realmonitor?channel=1&subtype=0",note:"CP Plus cameras use the Dahua stream path."},{id:"uniview",label:"Uniview",path:"/media/video1",note:"Some older Uniview models use /video1 instead."},{id:"tplink",label:"TP-Link / Tapo",path:"/stream1",note:"Tapo cameras need a separate camera account created in the Tapo app — your Tapo login will not work."},{id:"reolink",label:"Reolink",path:"/h264Preview_01_main",note:"Use /h264Preview_01_sub for the lower-quality stream."},{id:"amcrest",label:"Amcrest",path:"/cam/realmonitor?channel=1&subtype=0",note:"Amcrest cameras use the Dahua stream path."},{id:"axis",label:"Axis",path:"/axis-media/media.amp",note:""},{id:"onvif",label:"Other (ONVIF)",path:"/onvif1",note:"Many generic cameras answer here. If it does not work, look for “RTSP” in the camera’s own app."},{id:"manual",label:"I know the path",path:"",note:""}],uu=e=>Ur.find(t=>t.id===e)||Ur[Ur.length-1],wp=["Camera","Connection","Test","Walk past"];function kp({sites:e,existing:t,onClose:n,onSaved:r}){var d;const l=!(t!=null&&t.id),[i,s]=N.useState(l?0:2),[u,a]=N.useState(t!=null&&t.id?t:null),[c,m]=N.useState({site_id:(t==null?void 0:t.site_id)||((d=e[0])==null?void 0:d.site_id)||"",make:"hikvision",label:(t==null?void 0:t.label)||"",host:(t==null?void 0:t.host)||"",port:(t==null?void 0:t.port)||554,path:(t==null?void 0:t.path)||"/Streaming/Channels/101",username:(t==null?void 0:t.username)||"",password:""}),[y,h]=N.useState(!1),[v,w]=N.useState(""),x=f=>g=>m(S=>({...S,[f]:g.target.value})),R=f=>{const g=uu(f.target.value);m(S=>({...S,make:g.id,path:g.path||S.path}))},p=async()=>{h(!0),w("");const f={};for(const[g,S]of Object.entries(c))g==="site_id"||g==="make"||S===""||S==null||(f[g]=g==="port"?Number(S):S);try{const g=u!=null&&u.id?await J.updateCamera(u.id,f):await J.createCamera(c.site_id,f);a(g),s(2),r==null||r(g,{keepOpen:!0})}catch(g){w(g.message)}finally{h(!1)}};return o.jsx("div",{className:"overlay",onClick:n,children:o.jsxs("aside",{className:"drawer wizard",onClick:f=>f.stopPropagation(),children:[o.jsxs("header",{className:"drawer-head",children:[o.jsxs("div",{children:[o.jsx("h2",{children:l?"Set up a camera":(u==null?void 0:u.label)||(t==null?void 0:t.label)}),o.jsx("p",{className:"sub",children:i<2?"The shop’s PC connects to the camera — nothing needs opening to the internet.":"Prove it works before you rely on it."})]}),o.jsx("button",{className:"ghost",onClick:n,children:"Close"})]}),o.jsx("ol",{className:"steps",children:wp.map((f,g)=>o.jsxs("li",{className:g===i?"now":go.jsx("option",{value:f.site_id,children:f.name},f.site_id))})]}),o.jsxs("label",{children:["What should staff call it?",o.jsx("input",{value:c.label,onChange:x("label"),placeholder:"Entrance",autoFocus:!0}),o.jsx("span",{className:"hint",children:"Visits are recorded against this name, so it cannot be changed later."})]}),o.jsxs("label",{children:["Make of camera",o.jsx("select",{value:c.make,onChange:R,children:Ur.map(f=>o.jsx("option",{value:f.id,children:f.label},f.id))}),o.jsx("span",{className:"hint",children:uu(c.make).note||"This only fills in the stream path for you. You can change it on the next step."})]}),o.jsx("button",{className:"primary",disabled:!c.label.trim(),onClick:()=>s(1),children:"Next"})]}),i===1&&o.jsxs("div",{className:"drawer-body",children:[o.jsxs("label",{children:["Camera’s address on the shop’s network",o.jsx("input",{value:c.host,onChange:x("host"),placeholder:"192.168.0.138",autoFocus:!0}),o.jsx("span",{className:"hint",children:"Not a website address. It usually starts 192.168. — look in the camera’s own app, on its label, or in your router’s device list."})]}),o.jsxs("div",{className:"pair",children:[o.jsxs("label",{children:["Port",o.jsx("input",{type:"number",value:c.port,onChange:x("port")})]}),o.jsxs("label",{children:["Stream path",o.jsx("input",{value:c.path,onChange:x("path")})]})]}),o.jsxs("label",{children:["Camera username",o.jsx("input",{value:c.username,onChange:x("username"),placeholder:"admin",name:"camera-account",autoComplete:"off",autoCorrect:"off",autoCapitalize:"none",spellCheck:"false"}),o.jsx("span",{className:"hint",children:"The camera’s own login, not your Behavision one."})]}),o.jsxs("label",{children:["Camera password",o.jsx("input",{type:"password",value:c.password,onChange:x("password"),name:"camera-secret",autoComplete:"new-password",placeholder:u!=null&&u.has_password?"(unchanged)":""})]}),v&&o.jsx("p",{className:"error",role:"alert",children:v}),o.jsxs("div",{className:"pair",children:[o.jsx("button",{className:"ghost",onClick:()=>s(0),children:"Back"}),o.jsx("button",{className:"primary",disabled:y||!c.host.trim(),onClick:p,children:y?"Saving…":"Save and test"})]})]}),i>=2&&u&&o.jsx(xp,{camera:u,step:i,onStep:s,onUpdated:f=>{a(f),r==null||r(f,{keepOpen:!0})},onEdit:()=>s(1),onDone:n})]})})}function xp({camera:e,step:t,onStep:n,onUpdated:r,onEdit:l,onDone:i}){const[s,u]=N.useState(e),[a,c]=N.useState(!1),[m,y]=N.useState(""),h=N.useRef(null);N.useEffect(()=>u(e),[e]);const v=s.check||{},w=v.state==="requested"||v.state==="running";N.useEffect(()=>{if(!w)return;let p=!0;const d=async()=>{try{const g=(await J.cameras()).find(S=>S.id===s.id);p&&g&&(u(g),r==null||r(g))}catch{}};return h.current=setInterval(d,4e3),()=>{p=!1,clearInterval(h.current)}},[w,s.id]);const x=async(p,d)=>{c(!0),y("");try{const f=await J.checkCamera(s.id,p,d);u(f),r==null||r(f),n(p==="placement"?3:2)}catch(f){y(f.message)}finally{c(!1)}},R=v.kind==="connection"&&v.state==="done"&&v.ok;return o.jsx(o.Fragment,{children:o.jsxs("div",{className:"drawer-body",children:[t===2?o.jsxs(o.Fragment,{children:[o.jsx("h3",{children:"Can the shop’s PC reach this camera?"}),o.jsx("p",{className:"sub",children:"The PC opens the stream once and takes a single picture. Nothing is recorded."})]}):o.jsxs(o.Fragment,{children:[o.jsx("h3",{children:"Can it actually recognise a face?"}),o.jsxs("p",{className:"sub",children:["Someone needs to ",o.jsx("b",{children:"walk through the camera’s view and out of it"}),", the way a customer would. Standing still measures nothing — the check scores the best view of each person as they leave the frame, which is what recognition really uses."]})]}),w&&o.jsxs("div",{className:"waiting",children:[o.jsx("span",{className:"spinner","aria-hidden":"true"}),o.jsxs("div",{children:[o.jsx("b",{children:v.state==="running"?"Running on the shop’s PC…":"Waiting for the shop’s PC…"}),o.jsx("span",{className:"sub",children:v.state==="running"?"This takes about "+(v.seconds||25)+" seconds.":"It picks up the request within a couple of minutes."})]})]}),v.state==="done"&&o.jsx(Sp,{check:v}),m&&o.jsx("p",{className:"error",role:"alert",children:m}),o.jsx("div",{className:"pair",children:t===2?o.jsxs(o.Fragment,{children:[o.jsx("button",{className:"ghost",onClick:l,children:"Change details"}),o.jsx("button",{className:"primary",disabled:a||w,onClick:()=>x("connection"),children:v.state==="done"?"Test again":"Test connection"})]}):o.jsxs(o.Fragment,{children:[o.jsx("button",{className:"ghost",onClick:()=>n(2),children:"Back"}),o.jsx("button",{className:"primary",disabled:a||w,onClick:()=>x("placement",25),children:v.state==="done"?"Check again":"Start walk-past check"})]})}),t===2&&R&&!w&&o.jsx("button",{className:"primary",onClick:()=>n(3),children:"Next: prove it can recognise faces"}),t===3&&v.state==="done"&&!w&&o.jsx("button",{className:"ghost",onClick:i,children:"Finish"})]})})}function Sp({check:e}){var n,r,l;const t=e.ok?"ok":e.verdict==="marginal"?"warn":"bad";return o.jsxs("div",{className:"outcome "+t,children:[o.jsxs("div",{className:"outcome-head",children:[o.jsx("span",{className:"pill "+t,children:e.ok?"Working":"Not ready"}),o.jsx("b",{children:e.headline||(e.ok?"Working":"Could not be verified")})]}),((n=e.image)==null?void 0:n.available)&&o.jsx("img",{className:"proof",src:e.image.url,alt:"The view from this camera",loading:"lazy"}),((r=e.advice)==null?void 0:r.length)>0&&o.jsx("ul",{className:"advice",children:e.advice.map((i,s)=>o.jsx("li",{children:i},s))}),((l=e.detail)==null?void 0:l.faces)!=null&&o.jsxs("p",{className:"sub",children:[e.detail.faces," ",e.detail.faces===1?"person":"people"," walked past during the check."]})]})}function jp(e){const t=e.check||{};return t.state==="requested"||t.state==="running"?{tone:"idle",mark:"…",words:"Checking now"}:t.state!=="done"?{tone:"idle",mark:"?",words:"Not checked yet"}:t.kind==="placement"?t.ok?{tone:"ok",mark:"✓",words:"Recognises faces here"}:{tone:"bad",mark:"✕",words:t.headline||"Cannot recognise faces here"}:t.ok?{tone:"warn",mark:"!",words:"Stream works — faces not checked yet"}:{tone:"bad",mark:"✕",words:t.headline||"Could not reach the camera"}}function Cp({user:e}){const{data:t,error:n,loading:r,reload:l}=er(()=>J.cameras(),2e4,[]),{data:i}=er(()=>J.sites(),0,[]),[s,u]=N.useState(null),a=["admin","owner","manager"].includes(e.role),c=t||[],m=c.filter(v=>v.connected).length,y=c.filter(v=>v.connected===!1).length,h=c.filter(v=>v.connected==null).length;return o.jsxs(o.Fragment,{children:[o.jsxs("header",{className:"head",children:[o.jsx("h1",{children:"Cameras"}),o.jsxs("p",{className:"sub",children:[c.length," ",c.length===1?"camera":"cameras",m>0&&o.jsxs(o.Fragment,{children:[" · ",o.jsxs("b",{className:"ok",children:[m," connected"]})]}),y>0&&o.jsxs(o.Fragment,{children:[" · ",o.jsxs("b",{className:"bad",children:[y," down"]})]}),h>0&&o.jsxs(o.Fragment,{children:[" · ",h," waiting for the shop PC"]})]}),a&&o.jsx("button",{className:"primary",onClick:()=>u({}),children:"Set up a camera"})]}),r&&!t?o.jsx(El,{}):n?o.jsx(_l,{error:n}):c.length===0?o.jsxs("div",{className:"state",children:[o.jsx("h2",{children:"No cameras yet"}),o.jsx("p",{className:"sub",children:"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."})]}):o.jsx("div",{className:"grid cams",children:c.map(v=>o.jsx(Np,{cam:v,canEdit:a,onEdit:()=>u(v)},v.id))}),s&&o.jsx(kp,{existing:s.id?s:null,sites:i||[],onClose:()=>{u(null),l()},onSaved:(v,w)=>{w!=null&&w.keepOpen||u(null),l()}})]})}function Np({cam:e,canEdit:t,onEdit:n}){var s,u;const r=e.connected==null?"idle":e.connected?"ok":"bad",l=e.connected==null?"Waiting for the shop PC":e.connected?"Connected":"Not connecting",i=jp(e);return o.jsxs("article",{className:"card cam state-"+r,onClick:t?n:void 0,role:t?"button":void 0,tabIndex:t?0:void 0,onKeyDown:a=>t&&a.key==="Enter"&&n(),children:[o.jsxs("div",{className:"shot",children:[(s=e.snapshot)!=null&&s.available?o.jsx(Cc,{url:e.snapshot.url,alt:`View from ${e.label}`}):o.jsxs("div",{className:"noshot",children:[o.jsx("span",{className:"lens","aria-hidden":"true"}),((u=e.snapshot)==null?void 0:u.reason)||"No picture yet."]}),o.jsxs("div",{className:"shot-over",children:[o.jsxs("div",{className:"shot-name",children:[o.jsx("b",{children:e.label||e.camera_id}),o.jsx("span",{children:e.site})]}),o.jsxs("span",{className:"status "+r,children:[o.jsx("i",{"aria-hidden":"true"}),l]})]}),e.snapshot_at&&o.jsx("span",{className:"shot-age",children:un(e.snapshot_at)})]}),o.jsxs("div",{className:"verdict "+i.tone,children:[o.jsx("span",{className:"mark","aria-hidden":"true",children:i.mark}),o.jsx("span",{className:"words",children:i.words}),t&&o.jsx("span",{className:"go","aria-hidden":"true",children:"→"})]})]})}const Ep=["Is everything working today?","Why is footfall low at Chennai?","How many people came in last week?","Which cameras still need checking?"];function _p({open:e,onClose:t}){const[n,r]=N.useState([]),[l,i]=N.useState(""),[s,u]=N.useState(!1),[a,c]=N.useState(!1),m=N.useRef(null),y=N.useRef(null);N.useEffect(()=>{var v;e&&((v=y.current)==null||v.focus())},[e]),N.useEffect(()=>{var v;(v=m.current)==null||v.scrollIntoView({behavior:"smooth",block:"end"})},[n,s]);const h=async v=>{const w=(v??l).trim();if(!w||s)return;const x=[...n,{role:"user",text:w}];r(x),i(""),u(!0);try{const R=await J.ask(x);r(p=>[...p,{role:"assistant",text:R.text,used:R.used}])}catch(R){R.code==="assistant_off"?c(!0):r(p=>[...p,{role:"assistant",text:R.message,failed:!0}])}finally{u(!1)}};return e?o.jsxs("aside",{className:"assistant",role:"complementary","aria-label":"Assistant",children:[o.jsxs("header",{className:"assistant-head",children:[o.jsxs("div",{children:[o.jsx("b",{children:"Ask Behavision"}),o.jsx("span",{className:"sub",children:"It reads your shops’ own data to answer."})]}),o.jsx("button",{className:"ghost",onClick:t,"aria-label":"Close assistant",children:"✕"})]}),o.jsxs("div",{className:"assistant-body",children:[a?o.jsx("p",{className:"sub pad",children:"The assistant is not switched on for this server."}):n.length===0?o.jsxs("div",{className:"suggest",children:[o.jsx("p",{className:"sub",children:"Try asking:"}),Ep.map(v=>o.jsx("button",{className:"chip",onClick:()=>h(v),children:v},v))]}):n.map((v,w)=>{var x;return o.jsxs("div",{className:"bubble "+v.role+(v.failed?" failed":""),children:[v.text,((x=v.used)==null?void 0:x.length)>0&&o.jsxs("span",{className:"used",children:["looked at: ",v.used.map(Pp).join(", ")]})]},w)}),s&&o.jsxs("div",{className:"bubble assistant-thinking",children:[o.jsx("span",{className:"spinner","aria-hidden":"true"})," Looking…"]}),o.jsx("div",{ref:m})]}),!a&&o.jsxs("form",{className:"assistant-ask",onSubmit:v=>{v.preventDefault(),h()},children:[o.jsx("input",{ref:y,value:l,onChange:v=>i(v.target.value),placeholder:"Ask about your shops…",disabled:s}),o.jsx("button",{className:"primary",disabled:s||!l.trim(),children:"Ask"})]})]}):null}function Pp(e){return{list_shops:"your shops",check_shop:"a shop check",list_cameras:"your cameras",check_camera:"a camera check",footfall:"footfall",sales:"sales",find_customer:"customer records"}[e]||e}function Tp(){const{data:e,error:t,loading:n,reload:r}=er(()=>J.clients(),0,[]),[l,i]=N.useState(!1),[s,u]=N.useState(null),a=e||[];return o.jsxs(o.Fragment,{children:[o.jsxs("header",{className:"head",children:[o.jsx("h1",{children:"Companies"}),o.jsx("button",{className:"primary",onClick:()=>{u(null),i(!0)},children:"New company"})]}),s&&o.jsx(Lp,{result:s,onDismiss:()=>u(null)}),n&&!e?o.jsx(El,{}):t?o.jsx(_l,{error:t}):a.length===0?o.jsxs("div",{className:"state",children:[o.jsx("h2",{children:"No companies yet"}),o.jsx("p",{className:"sub",children:"Create one, and its owner can sign in straight away."})]}):o.jsx("div",{className:"tablewrap",children:o.jsxs("table",{className:"rows",children:[o.jsx("thead",{children:o.jsxs("tr",{children:[o.jsx("th",{children:"Company"}),o.jsx("th",{children:"Short name"}),o.jsx("th",{children:"Sites"}),o.jsx("th",{children:"People"}),o.jsx("th",{children:"Created"})]})}),o.jsx("tbody",{children:a.map(c=>o.jsxs("tr",{children:[o.jsx("td",{children:o.jsx("strong",{children:c.name})}),o.jsx("td",{children:o.jsx("code",{children:c.slug})}),o.jsx("td",{className:"num",children:c.sites}),o.jsx("td",{className:"num",children:c.users}),o.jsx("td",{className:"sub",children:un(c.created_at)})]},c.id))})]})}),l&&o.jsx(zp,{onClose:()=>i(!1),onCreated:c=>{i(!1),u(c),r()}})]})}function zp({onClose:e,onCreated:t}){const[n,r]=N.useState({company_name:"",owner_name:"",owner_email:"",password:""}),[l,i]=N.useState(!1),[s,u]=N.useState(""),a=m=>y=>r({...n,[m]:y.target.value}),c=async m=>{m.preventDefault(),i(!0),u("");try{t(await J.createClient(n))}catch(y){u(y.message),i(!1)}};return o.jsx("div",{className:"overlay",onClick:e,children:o.jsxs("aside",{className:"drawer narrow",onClick:m=>m.stopPropagation(),children:[o.jsxs("header",{className:"drawer-head",children:[o.jsx("h2",{children:"New company"}),o.jsx("button",{className:"ghost",onClick:e,children:"Close"})]}),o.jsxs("form",{className:"drawer-body",onSubmit:c,children:[o.jsxs("label",{children:["Company name",o.jsx("input",{value:n.company_name,onChange:a("company_name"),required:!0,autoFocus:!0})]}),o.jsxs("label",{children:["Owner’s name",o.jsx("input",{value:n.owner_name,onChange:a("owner_name")})]}),o.jsxs("label",{children:["Owner’s email",o.jsx("input",{type:"email",value:n.owner_email,onChange:a("owner_email"),required:!0})]}),o.jsxs("label",{children:["Password",o.jsx("input",{type:"text",value:n.password,onChange:a("password"),placeholder:"Leave empty to generate one"}),o.jsx("span",{className:"hint",children:"Generated is better — a password you invent for someone else ends up weak and sent over chat."})]}),s&&o.jsx("p",{className:"error",role:"alert",children:s}),o.jsx("button",{className:"primary",disabled:l,children:l?"Creating…":"Create company"})]})]})})}function Lp({result:e,onDismiss:t}){return o.jsxs("div",{className:"banner ok credentials",children:[o.jsxs("div",{children:[o.jsxs("b",{children:[e.slug," created."]})," These sign-in details are shown once and cannot be recovered. Send them to the owner now.",o.jsxs("dl",{className:"creds",children:[o.jsxs("div",{children:[o.jsx("dt",{children:"Email"}),o.jsx("dd",{children:o.jsx("code",{children:e.owner_email})})]}),o.jsxs("div",{children:[o.jsx("dt",{children:"Password"}),o.jsx("dd",{children:o.jsx("code",{children:e.password})})]})]})]}),o.jsx("button",{className:"ghost",onClick:t,children:"Done"})]})}const Rp=[{id:"sites",label:"Shops",View:op},{id:"live",label:"Live",View:hp},{id:"cameras",label:"Cameras",View:Cp}],Op=[{id:"clients",label:"Companies",View:Tp}];function Ip(){const[e,t]=N.useState(null),[n,r]=N.useState(!0),[l,i]=N.useState("sites"),[s,u]=N.useState(!1);if(N.useEffect(()=>{(async()=>{if(bf())try{t(await J.me())}catch{sr()}r(!1)})()},[]),n)return o.jsxs("div",{className:"boot",children:[o.jsx("span",{className:"spinner","aria-hidden":"true"}),"Loading…"]});if(!e)return o.jsx(tp,{onSignedIn:t});const a=e.role==="admin"&&!e.client_id,c=a?Op:Rp,m=c.find(v=>v.id===l)||c[0],y=m.View,h=async()=>{await J.logout(),t(null)};return o.jsxs("div",{className:"app",children:[o.jsxs("header",{className:"topbar",children:[o.jsxs("div",{className:"brand",children:[o.jsx("span",{className:"mark","aria-hidden":"true"}),o.jsxs("div",{children:[o.jsx("strong",{children:"Behavision"}),o.jsx("span",{className:"org",children:e.client_name||"Loyaly platform"})]})]}),o.jsx("nav",{className:"tabs",children:c.map(v=>o.jsx("button",{onClick:()=>i(v.id),"aria-current":m.id===v.id?"page":void 0,children:v.label},v.id))}),o.jsxs("div",{className:"who",children:[o.jsxs("button",{className:"ask-btn"+(s?" on":""),onClick:()=>u(v=>!v),children:[o.jsx("span",{"aria-hidden":"true",children:"✳"})," Ask"]}),o.jsx("span",{className:"name",children:e.full_name||e.email}),o.jsx("span",{className:"role",children:a?"platform admin":e.role}),o.jsx("button",{className:"ghost",onClick:h,children:"Sign out"})]})]}),o.jsxs("div",{className:"with-assistant"+(s?" open":""),children:[o.jsx("main",{className:"page",children:o.jsx(y,{user:e})}),o.jsx(_p,{open:s,onClose:()=>u(!1)})]})]})}xc(document.getElementById("root")).render(o.jsx(Ip,{})); diff --git a/server/internal/web/dist/assets/index-C8M-zRAi.js b/server/internal/web/dist/assets/index-C8M-zRAi.js deleted file mode 100644 index 32ac65b..0000000 --- a/server/internal/web/dist/assets/index-C8M-zRAi.js +++ /dev/null @@ -1,43 +0,0 @@ -(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const l of document.querySelectorAll('link[rel="modulepreload"]'))r(l);new MutationObserver(l=>{for(const i of l)if(i.type==="childList")for(const s of i.addedNodes)s.tagName==="LINK"&&s.rel==="modulepreload"&&r(s)}).observe(document,{childList:!0,subtree:!0});function n(l){const i={};return l.integrity&&(i.integrity=l.integrity),l.referrerPolicy&&(i.referrerPolicy=l.referrerPolicy),l.crossOrigin==="use-credentials"?i.credentials="include":l.crossOrigin==="anonymous"?i.credentials="omit":i.credentials="same-origin",i}function r(l){if(l.ep)return;l.ep=!0;const i=n(l);fetch(l.href,i)}})();var uu={exports:{}},ul={},au={exports:{}},L={};/** - * @license React - * react.production.min.js - * - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */var er=Symbol.for("react.element"),Cc=Symbol.for("react.portal"),Nc=Symbol.for("react.fragment"),Ec=Symbol.for("react.strict_mode"),_c=Symbol.for("react.profiler"),Pc=Symbol.for("react.provider"),Tc=Symbol.for("react.context"),zc=Symbol.for("react.forward_ref"),Lc=Symbol.for("react.suspense"),Rc=Symbol.for("react.memo"),Oc=Symbol.for("react.lazy"),Xs=Symbol.iterator;function Ic(e){return e===null||typeof e!="object"?null:(e=Xs&&e[Xs]||e["@@iterator"],typeof e=="function"?e:null)}var cu={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},du=Object.assign,fu={};function an(e,t,n){this.props=e,this.context=t,this.refs=fu,this.updater=n||cu}an.prototype.isReactComponent={};an.prototype.setState=function(e,t){if(typeof e!="object"&&typeof e!="function"&&e!=null)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,e,t,"setState")};an.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,"forceUpdate")};function pu(){}pu.prototype=an.prototype;function Ji(e,t,n){this.props=e,this.context=t,this.refs=fu,this.updater=n||cu}var qi=Ji.prototype=new pu;qi.constructor=Ji;du(qi,an.prototype);qi.isPureReactComponent=!0;var Zs=Array.isArray,hu=Object.prototype.hasOwnProperty,bi={current:null},mu={key:!0,ref:!0,__self:!0,__source:!0};function vu(e,t,n){var r,l={},i=null,s=null;if(t!=null)for(r in t.ref!==void 0&&(s=t.ref),t.key!==void 0&&(i=""+t.key),t)hu.call(t,r)&&!mu.hasOwnProperty(r)&&(l[r]=t[r]);var u=arguments.length-2;if(u===1)l.children=n;else if(1>>1,J=C[Q];if(0>>1;Ql(Tl,z))wtl(sr,Tl)?(C[Q]=sr,C[wt]=z,Q=wt):(C[Q]=Tl,C[gt]=z,Q=gt);else if(wtl(sr,z))C[Q]=sr,C[wt]=z,Q=wt;else break e}}return T}function l(C,T){var z=C.sortIndex-T.sortIndex;return z!==0?z:C.id-T.id}if(typeof performance=="object"&&typeof performance.now=="function"){var i=performance;e.unstable_now=function(){return i.now()}}else{var s=Date,u=s.now();e.unstable_now=function(){return s.now()-u}}var a=[],c=[],m=1,y=null,h=3,v=!1,w=!1,x=!1,R=typeof setTimeout=="function"?setTimeout:null,p=typeof clearTimeout=="function"?clearTimeout:null,d=typeof setImmediate<"u"?setImmediate:null;typeof navigator<"u"&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0&&navigator.scheduling.isInputPending.bind(navigator.scheduling);function f(C){for(var T=n(c);T!==null;){if(T.callback===null)r(c);else if(T.startTime<=C)r(c),T.sortIndex=T.expirationTime,t(a,T);else break;T=n(c)}}function g(C){if(x=!1,f(C),!w)if(n(a)!==null)w=!0,_l(S);else{var T=n(c);T!==null&&Pl(g,T.startTime-C)}}function S(C,T){w=!1,x&&(x=!1,p(P),P=-1),v=!0;var z=h;try{for(f(T),y=n(a);y!==null&&(!(y.expirationTime>T)||C&&!Te());){var Q=y.callback;if(typeof Q=="function"){y.callback=null,h=y.priorityLevel;var J=Q(y.expirationTime<=T);T=e.unstable_now(),typeof J=="function"?y.callback=J:y===n(a)&&r(a),f(T)}else r(a);y=n(a)}if(y!==null)var ir=!0;else{var gt=n(c);gt!==null&&Pl(g,gt.startTime-T),ir=!1}return ir}finally{y=null,h=z,v=!1}}var N=!1,E=null,P=-1,H=5,O=-1;function Te(){return!(e.unstable_now()-OC||125Q?(C.sortIndex=z,t(c,C),n(a)===null&&C===n(c)&&(x?(p(P),P=-1):x=!0,Pl(g,z-Q))):(C.sortIndex=J,t(a,C),w||v||(w=!0,_l(S))),C},e.unstable_shouldYield=Te,e.unstable_wrapCallback=function(C){var T=h;return function(){var z=h;h=T;try{return C.apply(this,arguments)}finally{h=z}}}})(xu);ku.exports=xu;var Qc=ku.exports;/** - * @license React - * react-dom.production.min.js - * - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */var Kc=_,ke=Qc;function k(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;n"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),ri=Object.prototype.hasOwnProperty,Gc=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,qs={},bs={};function Yc(e){return ri.call(bs,e)?!0:ri.call(qs,e)?!1:Gc.test(e)?bs[e]=!0:(qs[e]=!0,!1)}function Xc(e,t,n,r){if(n!==null&&n.type===0)return!1;switch(typeof t){case"function":case"symbol":return!0;case"boolean":return r?!1:n!==null?!n.acceptsBooleans:(e=e.toLowerCase().slice(0,5),e!=="data-"&&e!=="aria-");default:return!1}}function Zc(e,t,n,r){if(t===null||typeof t>"u"||Xc(e,t,n,r))return!0;if(r)return!1;if(n!==null)switch(n.type){case 3:return!t;case 4:return t===!1;case 5:return isNaN(t);case 6:return isNaN(t)||1>t}return!1}function de(e,t,n,r,l,i,s){this.acceptsBooleans=t===2||t===3||t===4,this.attributeName=r,this.attributeNamespace=l,this.mustUseProperty=n,this.propertyName=e,this.type=t,this.sanitizeURL=i,this.removeEmptyString=s}var re={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(e){re[e]=new de(e,0,!1,e,null,!1,!1)});[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(e){var t=e[0];re[t]=new de(t,1,!1,e[1],null,!1,!1)});["contentEditable","draggable","spellCheck","value"].forEach(function(e){re[e]=new de(e,2,!1,e.toLowerCase(),null,!1,!1)});["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(e){re[e]=new de(e,2,!1,e,null,!1,!1)});"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(e){re[e]=new de(e,3,!1,e.toLowerCase(),null,!1,!1)});["checked","multiple","muted","selected"].forEach(function(e){re[e]=new de(e,3,!0,e,null,!1,!1)});["capture","download"].forEach(function(e){re[e]=new de(e,4,!1,e,null,!1,!1)});["cols","rows","size","span"].forEach(function(e){re[e]=new de(e,6,!1,e,null,!1,!1)});["rowSpan","start"].forEach(function(e){re[e]=new de(e,5,!1,e.toLowerCase(),null,!1,!1)});var ts=/[\-:]([a-z])/g;function ns(e){return e[1].toUpperCase()}"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(e){var t=e.replace(ts,ns);re[t]=new de(t,1,!1,e,null,!1,!1)});"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(e){var t=e.replace(ts,ns);re[t]=new de(t,1,!1,e,"http://www.w3.org/1999/xlink",!1,!1)});["xml:base","xml:lang","xml:space"].forEach(function(e){var t=e.replace(ts,ns);re[t]=new de(t,1,!1,e,"http://www.w3.org/XML/1998/namespace",!1,!1)});["tabIndex","crossOrigin"].forEach(function(e){re[e]=new de(e,1,!1,e.toLowerCase(),null,!1,!1)});re.xlinkHref=new de("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1);["src","href","action","formAction"].forEach(function(e){re[e]=new de(e,1,!1,e.toLowerCase(),null,!0,!0)});function rs(e,t,n,r){var l=re.hasOwnProperty(t)?re[t]:null;(l!==null?l.type!==0:r||!(2u||l[s]!==i[u]){var a=` -`+l[s].replace(" at new "," at ");return e.displayName&&a.includes("")&&(a=a.replace("",e.displayName)),a}while(1<=s&&0<=u);break}}}finally{Rl=!1,Error.prepareStackTrace=n}return(e=e?e.displayName||e.name:"")?jn(e):""}function Jc(e){switch(e.tag){case 5:return jn(e.type);case 16:return jn("Lazy");case 13:return jn("Suspense");case 19:return jn("SuspenseList");case 0:case 2:case 15:return e=Ol(e.type,!1),e;case 11:return e=Ol(e.type.render,!1),e;case 1:return e=Ol(e.type,!0),e;default:return""}}function oi(e){if(e==null)return null;if(typeof e=="function")return e.displayName||e.name||null;if(typeof e=="string")return e;switch(e){case Mt:return"Fragment";case Dt:return"Portal";case li:return"Profiler";case ls:return"StrictMode";case ii:return"Suspense";case si:return"SuspenseList"}if(typeof e=="object")switch(e.$$typeof){case Cu:return(e.displayName||"Context")+".Consumer";case ju:return(e._context.displayName||"Context")+".Provider";case is:var t=e.render;return e=e.displayName,e||(e=t.displayName||t.name||"",e=e!==""?"ForwardRef("+e+")":"ForwardRef"),e;case ss:return t=e.displayName||null,t!==null?t:oi(e.type)||"Memo";case be:t=e._payload,e=e._init;try{return oi(e(t))}catch{}}return null}function qc(e){var t=e.type;switch(e.tag){case 24:return"Cache";case 9:return(t.displayName||"Context")+".Consumer";case 10:return(t._context.displayName||"Context")+".Provider";case 18:return"DehydratedFragment";case 11:return e=t.render,e=e.displayName||e.name||"",t.displayName||(e!==""?"ForwardRef("+e+")":"ForwardRef");case 7:return"Fragment";case 5:return t;case 4:return"Portal";case 3:return"Root";case 6:return"Text";case 16:return oi(t);case 8:return t===ls?"StrictMode":"Mode";case 22:return"Offscreen";case 12:return"Profiler";case 21:return"Scope";case 13:return"Suspense";case 19:return"SuspenseList";case 25:return"TracingMarker";case 1:case 0:case 17:case 2:case 14:case 15:if(typeof t=="function")return t.displayName||t.name||null;if(typeof t=="string")return t}return null}function pt(e){switch(typeof e){case"boolean":case"number":case"string":case"undefined":return e;case"object":return e;default:return""}}function Eu(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(t==="checkbox"||t==="radio")}function bc(e){var t=Eu(e)?"checked":"value",n=Object.getOwnPropertyDescriptor(e.constructor.prototype,t),r=""+e[t];if(!e.hasOwnProperty(t)&&typeof n<"u"&&typeof n.get=="function"&&typeof n.set=="function"){var l=n.get,i=n.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return l.call(this)},set:function(s){r=""+s,i.call(this,s)}}),Object.defineProperty(e,t,{enumerable:n.enumerable}),{getValue:function(){return r},setValue:function(s){r=""+s},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}function ar(e){e._valueTracker||(e._valueTracker=bc(e))}function _u(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var n=t.getValue(),r="";return e&&(r=Eu(e)?e.checked?"true":"false":e.value),e=r,e!==n?(t.setValue(e),!0):!1}function Mr(e){if(e=e||(typeof document<"u"?document:void 0),typeof e>"u")return null;try{return e.activeElement||e.body}catch{return e.body}}function ui(e,t){var n=t.checked;return B({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:n??e._wrapperState.initialChecked})}function to(e,t){var n=t.defaultValue==null?"":t.defaultValue,r=t.checked!=null?t.checked:t.defaultChecked;n=pt(t.value!=null?t.value:n),e._wrapperState={initialChecked:r,initialValue:n,controlled:t.type==="checkbox"||t.type==="radio"?t.checked!=null:t.value!=null}}function Pu(e,t){t=t.checked,t!=null&&rs(e,"checked",t,!1)}function ai(e,t){Pu(e,t);var n=pt(t.value),r=t.type;if(n!=null)r==="number"?(n===0&&e.value===""||e.value!=n)&&(e.value=""+n):e.value!==""+n&&(e.value=""+n);else if(r==="submit"||r==="reset"){e.removeAttribute("value");return}t.hasOwnProperty("value")?ci(e,t.type,n):t.hasOwnProperty("defaultValue")&&ci(e,t.type,pt(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}function no(e,t,n){if(t.hasOwnProperty("value")||t.hasOwnProperty("defaultValue")){var r=t.type;if(!(r!=="submit"&&r!=="reset"||t.value!==void 0&&t.value!==null))return;t=""+e._wrapperState.initialValue,n||t===e.value||(e.value=t),e.defaultValue=t}n=e.name,n!==""&&(e.name=""),e.defaultChecked=!!e._wrapperState.initialChecked,n!==""&&(e.name=n)}function ci(e,t,n){(t!=="number"||Mr(e.ownerDocument)!==e)&&(n==null?e.defaultValue=""+e._wrapperState.initialValue:e.defaultValue!==""+n&&(e.defaultValue=""+n))}var Cn=Array.isArray;function Yt(e,t,n,r){if(e=e.options,t){t={};for(var l=0;l"+t.valueOf().toString()+"",t=cr.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;t.firstChild;)e.appendChild(t.firstChild)}});function Mn(e,t){if(t){var n=e.firstChild;if(n&&n===e.lastChild&&n.nodeType===3){n.nodeValue=t;return}}e.textContent=t}var _n={animationIterationCount:!0,aspectRatio:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},ed=["Webkit","ms","Moz","O"];Object.keys(_n).forEach(function(e){ed.forEach(function(t){t=t+e.charAt(0).toUpperCase()+e.substring(1),_n[t]=_n[e]})});function Ru(e,t,n){return t==null||typeof t=="boolean"||t===""?"":n||typeof t!="number"||t===0||_n.hasOwnProperty(e)&&_n[e]?(""+t).trim():t+"px"}function Ou(e,t){e=e.style;for(var n in t)if(t.hasOwnProperty(n)){var r=n.indexOf("--")===0,l=Ru(n,t[n],r);n==="float"&&(n="cssFloat"),r?e.setProperty(n,l):e[n]=l}}var td=B({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0});function pi(e,t){if(t){if(td[e]&&(t.children!=null||t.dangerouslySetInnerHTML!=null))throw Error(k(137,e));if(t.dangerouslySetInnerHTML!=null){if(t.children!=null)throw Error(k(60));if(typeof t.dangerouslySetInnerHTML!="object"||!("__html"in t.dangerouslySetInnerHTML))throw Error(k(61))}if(t.style!=null&&typeof t.style!="object")throw Error(k(62))}}function hi(e,t){if(e.indexOf("-")===-1)return typeof t.is=="string";switch(e){case"annotation-xml":case"color-profile":case"font-face":case"font-face-src":case"font-face-uri":case"font-face-format":case"font-face-name":case"missing-glyph":return!1;default:return!0}}var mi=null;function os(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}var vi=null,Xt=null,Zt=null;function io(e){if(e=rr(e)){if(typeof vi!="function")throw Error(k(280));var t=e.stateNode;t&&(t=pl(t),vi(e.stateNode,e.type,t))}}function Iu(e){Xt?Zt?Zt.push(e):Zt=[e]:Xt=e}function Fu(){if(Xt){var e=Xt,t=Zt;if(Zt=Xt=null,io(e),t)for(e=0;e>>=0,e===0?32:31-(fd(e)/pd|0)|0}var dr=64,fr=4194304;function Nn(e){switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e&4194240;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return e&130023424;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return e}}function Vr(e,t){var n=e.pendingLanes;if(n===0)return 0;var r=0,l=e.suspendedLanes,i=e.pingedLanes,s=n&268435455;if(s!==0){var u=s&~l;u!==0?r=Nn(u):(i&=s,i!==0&&(r=Nn(i)))}else s=n&~l,s!==0?r=Nn(s):i!==0&&(r=Nn(i));if(r===0)return 0;if(t!==0&&t!==r&&!(t&l)&&(l=r&-r,i=t&-t,l>=i||l===16&&(i&4194240)!==0))return t;if(r&4&&(r|=n&16),t=e.entangledLanes,t!==0)for(e=e.entanglements,t&=r;0n;n++)t.push(e);return t}function tr(e,t,n){e.pendingLanes|=t,t!==536870912&&(e.suspendedLanes=0,e.pingedLanes=0),e=e.eventTimes,t=31-Ie(t),e[t]=n}function yd(e,t){var n=e.pendingLanes&~t;e.pendingLanes=t,e.suspendedLanes=0,e.pingedLanes=0,e.expiredLanes&=t,e.mutableReadLanes&=t,e.entangledLanes&=t,t=e.entanglements;var r=e.eventTimes;for(e=e.expirationTimes;0=Tn),mo=" ",vo=!1;function ta(e,t){switch(e){case"keyup":return Qd.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function na(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}var $t=!1;function Gd(e,t){switch(e){case"compositionend":return na(t);case"keypress":return t.which!==32?null:(vo=!0,mo);case"textInput":return e=t.data,e===mo&&vo?null:e;default:return null}}function Yd(e,t){if($t)return e==="compositionend"||!ms&&ta(e,t)?(e=bu(),_r=fs=rt=null,$t=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=r}e:{for(;n;){if(n.nextSibling){n=n.nextSibling;break e}n=n.parentNode}n=void 0}n=ko(n)}}function sa(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?sa(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function oa(){for(var e=window,t=Mr();t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href=="string"}catch{n=!1}if(n)e=t.contentWindow;else break;t=Mr(e.document)}return t}function vs(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t==="input"&&(e.type==="text"||e.type==="search"||e.type==="tel"||e.type==="url"||e.type==="password")||t==="textarea"||e.contentEditable==="true")}function rf(e){var t=oa(),n=e.focusedElem,r=e.selectionRange;if(t!==n&&n&&n.ownerDocument&&sa(n.ownerDocument.documentElement,n)){if(r!==null&&vs(n)){if(t=r.start,e=r.end,e===void 0&&(e=t),"selectionStart"in n)n.selectionStart=t,n.selectionEnd=Math.min(e,n.value.length);else if(e=(t=n.ownerDocument||document)&&t.defaultView||window,e.getSelection){e=e.getSelection();var l=n.textContent.length,i=Math.min(r.start,l);r=r.end===void 0?i:Math.min(r.end,l),!e.extend&&i>r&&(l=r,r=i,i=l),l=xo(n,i);var s=xo(n,r);l&&s&&(e.rangeCount!==1||e.anchorNode!==l.node||e.anchorOffset!==l.offset||e.focusNode!==s.node||e.focusOffset!==s.offset)&&(t=t.createRange(),t.setStart(l.node,l.offset),e.removeAllRanges(),i>r?(e.addRange(t),e.extend(s.node,s.offset)):(t.setEnd(s.node,s.offset),e.addRange(t)))}}for(t=[],e=n;e=e.parentNode;)e.nodeType===1&&t.push({element:e,left:e.scrollLeft,top:e.scrollTop});for(typeof n.focus=="function"&&n.focus(),n=0;n=document.documentMode,Ut=null,Si=null,Ln=null,ji=!1;function So(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;ji||Ut==null||Ut!==Mr(r)||(r=Ut,"selectionStart"in r&&vs(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),Ln&&Wn(Ln,r)||(Ln=r,r=Hr(Si,"onSelect"),0Bt||(e.current=Ti[Bt],Ti[Bt]=null,Bt--)}function D(e,t){Bt++,Ti[Bt]=e.current,e.current=t}var ht={},oe=vt(ht),he=vt(!1),_t=ht;function tn(e,t){var n=e.type.contextTypes;if(!n)return ht;var r=e.stateNode;if(r&&r.__reactInternalMemoizedUnmaskedChildContext===t)return r.__reactInternalMemoizedMaskedChildContext;var l={},i;for(i in n)l[i]=t[i];return r&&(e=e.stateNode,e.__reactInternalMemoizedUnmaskedChildContext=t,e.__reactInternalMemoizedMaskedChildContext=l),l}function me(e){return e=e.childContextTypes,e!=null}function Kr(){$(he),$(oe)}function To(e,t,n){if(oe.current!==ht)throw Error(k(168));D(oe,t),D(he,n)}function va(e,t,n){var r=e.stateNode;if(t=t.childContextTypes,typeof r.getChildContext!="function")return n;r=r.getChildContext();for(var l in r)if(!(l in t))throw Error(k(108,qc(e)||"Unknown",l));return B({},n,r)}function Gr(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||ht,_t=oe.current,D(oe,e),D(he,he.current),!0}function zo(e,t,n){var r=e.stateNode;if(!r)throw Error(k(169));n?(e=va(e,t,_t),r.__reactInternalMemoizedMergedChildContext=e,$(he),$(oe),D(oe,e)):$(he),D(he,n)}var We=null,hl=!1,Gl=!1;function ya(e){We===null?We=[e]:We.push(e)}function vf(e){hl=!0,ya(e)}function yt(){if(!Gl&&We!==null){Gl=!0;var e=0,t=F;try{var n=We;for(F=1;e>=s,l-=s,He=1<<32-Ie(t)+l|n<P?(H=E,E=null):H=E.sibling;var O=h(p,E,f[P],g);if(O===null){E===null&&(E=H);break}e&&E&&O.alternate===null&&t(p,E),d=i(O,d,P),N===null?S=O:N.sibling=O,N=O,E=H}if(P===f.length)return n(p,E),U&&kt(p,P),S;if(E===null){for(;PP?(H=E,E=null):H=E.sibling;var Te=h(p,E,O.value,g);if(Te===null){E===null&&(E=H);break}e&&E&&Te.alternate===null&&t(p,E),d=i(Te,d,P),N===null?S=Te:N.sibling=Te,N=Te,E=H}if(O.done)return n(p,E),U&&kt(p,P),S;if(E===null){for(;!O.done;P++,O=f.next())O=y(p,O.value,g),O!==null&&(d=i(O,d,P),N===null?S=O:N.sibling=O,N=O);return U&&kt(p,P),S}for(E=r(p,E);!O.done;P++,O=f.next())O=v(E,p,P,O.value,g),O!==null&&(e&&O.alternate!==null&&E.delete(O.key===null?P:O.key),d=i(O,d,P),N===null?S=O:N.sibling=O,N=O);return e&&E.forEach(function(fn){return t(p,fn)}),U&&kt(p,P),S}function R(p,d,f,g){if(typeof f=="object"&&f!==null&&f.type===Mt&&f.key===null&&(f=f.props.children),typeof f=="object"&&f!==null){switch(f.$$typeof){case ur:e:{for(var S=f.key,N=d;N!==null;){if(N.key===S){if(S=f.type,S===Mt){if(N.tag===7){n(p,N.sibling),d=l(N,f.props.children),d.return=p,p=d;break e}}else if(N.elementType===S||typeof S=="object"&&S!==null&&S.$$typeof===be&&Oo(S)===N.type){n(p,N.sibling),d=l(N,f.props),d.ref=wn(p,N,f),d.return=p,p=d;break e}n(p,N);break}else t(p,N);N=N.sibling}f.type===Mt?(d=Et(f.props.children,p.mode,g,f.key),d.return=p,p=d):(g=Fr(f.type,f.key,f.props,null,p.mode,g),g.ref=wn(p,d,f),g.return=p,p=g)}return s(p);case Dt:e:{for(N=f.key;d!==null;){if(d.key===N)if(d.tag===4&&d.stateNode.containerInfo===f.containerInfo&&d.stateNode.implementation===f.implementation){n(p,d.sibling),d=l(d,f.children||[]),d.return=p,p=d;break e}else{n(p,d);break}else t(p,d);d=d.sibling}d=ti(f,p.mode,g),d.return=p,p=d}return s(p);case be:return N=f._init,R(p,d,N(f._payload),g)}if(Cn(f))return w(p,d,f,g);if(hn(f))return x(p,d,f,g);wr(p,f)}return typeof f=="string"&&f!==""||typeof f=="number"?(f=""+f,d!==null&&d.tag===6?(n(p,d.sibling),d=l(d,f),d.return=p,p=d):(n(p,d),d=ei(f,p.mode,g),d.return=p,p=d),s(p)):n(p,d)}return R}var rn=xa(!0),Sa=xa(!1),Zr=vt(null),Jr=null,Qt=null,ks=null;function xs(){ks=Qt=Jr=null}function Ss(e){var t=Zr.current;$(Zr),e._currentValue=t}function Ri(e,t,n){for(;e!==null;){var r=e.alternate;if((e.childLanes&t)!==t?(e.childLanes|=t,r!==null&&(r.childLanes|=t)):r!==null&&(r.childLanes&t)!==t&&(r.childLanes|=t),e===n)break;e=e.return}}function qt(e,t){Jr=e,ks=Qt=null,e=e.dependencies,e!==null&&e.firstContext!==null&&(e.lanes&t&&(pe=!0),e.firstContext=null)}function _e(e){var t=e._currentValue;if(ks!==e)if(e={context:e,memoizedValue:t,next:null},Qt===null){if(Jr===null)throw Error(k(308));Qt=e,Jr.dependencies={lanes:0,firstContext:e}}else Qt=Qt.next=e;return t}var jt=null;function js(e){jt===null?jt=[e]:jt.push(e)}function ja(e,t,n,r){var l=t.interleaved;return l===null?(n.next=n,js(t)):(n.next=l.next,l.next=n),t.interleaved=n,Xe(e,r)}function Xe(e,t){e.lanes|=t;var n=e.alternate;for(n!==null&&(n.lanes|=t),n=e,e=e.return;e!==null;)e.childLanes|=t,n=e.alternate,n!==null&&(n.childLanes|=t),n=e,e=e.return;return n.tag===3?n.stateNode:null}var et=!1;function Cs(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,interleaved:null,lanes:0},effects:null}}function Ca(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,effects:e.effects})}function Ke(e,t){return{eventTime:e,lane:t,tag:0,payload:null,callback:null,next:null}}function at(e,t,n){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,I&2){var l=r.pending;return l===null?t.next=t:(t.next=l.next,l.next=t),r.pending=t,Xe(e,n)}return l=r.interleaved,l===null?(t.next=t,js(r)):(t.next=l.next,l.next=t),r.interleaved=t,Xe(e,n)}function Tr(e,t,n){if(t=t.updateQueue,t!==null&&(t=t.shared,(n&4194240)!==0)){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,as(e,n)}}function Io(e,t){var n=e.updateQueue,r=e.alternate;if(r!==null&&(r=r.updateQueue,n===r)){var l=null,i=null;if(n=n.firstBaseUpdate,n!==null){do{var s={eventTime:n.eventTime,lane:n.lane,tag:n.tag,payload:n.payload,callback:n.callback,next:null};i===null?l=i=s:i=i.next=s,n=n.next}while(n!==null);i===null?l=i=t:i=i.next=t}else l=i=t;n={baseState:r.baseState,firstBaseUpdate:l,lastBaseUpdate:i,shared:r.shared,effects:r.effects},e.updateQueue=n;return}e=n.lastBaseUpdate,e===null?n.firstBaseUpdate=t:e.next=t,n.lastBaseUpdate=t}function qr(e,t,n,r){var l=e.updateQueue;et=!1;var i=l.firstBaseUpdate,s=l.lastBaseUpdate,u=l.shared.pending;if(u!==null){l.shared.pending=null;var a=u,c=a.next;a.next=null,s===null?i=c:s.next=c,s=a;var m=e.alternate;m!==null&&(m=m.updateQueue,u=m.lastBaseUpdate,u!==s&&(u===null?m.firstBaseUpdate=c:u.next=c,m.lastBaseUpdate=a))}if(i!==null){var y=l.baseState;s=0,m=c=a=null,u=i;do{var h=u.lane,v=u.eventTime;if((r&h)===h){m!==null&&(m=m.next={eventTime:v,lane:0,tag:u.tag,payload:u.payload,callback:u.callback,next:null});e:{var w=e,x=u;switch(h=t,v=n,x.tag){case 1:if(w=x.payload,typeof w=="function"){y=w.call(v,y,h);break e}y=w;break e;case 3:w.flags=w.flags&-65537|128;case 0:if(w=x.payload,h=typeof w=="function"?w.call(v,y,h):w,h==null)break e;y=B({},y,h);break e;case 2:et=!0}}u.callback!==null&&u.lane!==0&&(e.flags|=64,h=l.effects,h===null?l.effects=[u]:h.push(u))}else v={eventTime:v,lane:h,tag:u.tag,payload:u.payload,callback:u.callback,next:null},m===null?(c=m=v,a=y):m=m.next=v,s|=h;if(u=u.next,u===null){if(u=l.shared.pending,u===null)break;h=u,u=h.next,h.next=null,l.lastBaseUpdate=h,l.shared.pending=null}}while(!0);if(m===null&&(a=y),l.baseState=a,l.firstBaseUpdate=c,l.lastBaseUpdate=m,t=l.shared.interleaved,t!==null){l=t;do s|=l.lane,l=l.next;while(l!==t)}else i===null&&(l.shared.lanes=0);zt|=s,e.lanes=s,e.memoizedState=y}}function Fo(e,t,n){if(e=t.effects,t.effects=null,e!==null)for(t=0;tn?n:4,e(!0);var r=Xl.transition;Xl.transition={};try{e(!1),t()}finally{F=n,Xl.transition=r}}function Va(){return Pe().memoizedState}function kf(e,t,n){var r=dt(e);if(n={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null},Ba(e))Wa(t,n);else if(n=ja(e,t,n,r),n!==null){var l=ae();Fe(n,e,r,l),Ha(n,t,r)}}function xf(e,t,n){var r=dt(e),l={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null};if(Ba(e))Wa(t,l);else{var i=e.alternate;if(e.lanes===0&&(i===null||i.lanes===0)&&(i=t.lastRenderedReducer,i!==null))try{var s=t.lastRenderedState,u=i(s,n);if(l.hasEagerState=!0,l.eagerState=u,De(u,s)){var a=t.interleaved;a===null?(l.next=l,js(t)):(l.next=a.next,a.next=l),t.interleaved=l;return}}catch{}finally{}n=ja(e,t,l,r),n!==null&&(l=ae(),Fe(n,e,r,l),Ha(n,t,r))}}function Ba(e){var t=e.alternate;return e===V||t!==null&&t===V}function Wa(e,t){Rn=el=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function Ha(e,t,n){if(n&4194240){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,as(e,n)}}var tl={readContext:_e,useCallback:le,useContext:le,useEffect:le,useImperativeHandle:le,useInsertionEffect:le,useLayoutEffect:le,useMemo:le,useReducer:le,useRef:le,useState:le,useDebugValue:le,useDeferredValue:le,useTransition:le,useMutableSource:le,useSyncExternalStore:le,useId:le,unstable_isNewReconciler:!1},Sf={readContext:_e,useCallback:function(e,t){return $e().memoizedState=[e,t===void 0?null:t],e},useContext:_e,useEffect:Mo,useImperativeHandle:function(e,t,n){return n=n!=null?n.concat([e]):null,Lr(4194308,4,Da.bind(null,t,e),n)},useLayoutEffect:function(e,t){return Lr(4194308,4,e,t)},useInsertionEffect:function(e,t){return Lr(4,2,e,t)},useMemo:function(e,t){var n=$e();return t=t===void 0?null:t,e=e(),n.memoizedState=[e,t],e},useReducer:function(e,t,n){var r=$e();return t=n!==void 0?n(t):t,r.memoizedState=r.baseState=t,e={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:t},r.queue=e,e=e.dispatch=kf.bind(null,V,e),[r.memoizedState,e]},useRef:function(e){var t=$e();return e={current:e},t.memoizedState=e},useState:Do,useDebugValue:Rs,useDeferredValue:function(e){return $e().memoizedState=e},useTransition:function(){var e=Do(!1),t=e[0];return e=wf.bind(null,e[1]),$e().memoizedState=e,[t,e]},useMutableSource:function(){},useSyncExternalStore:function(e,t,n){var r=V,l=$e();if(U){if(n===void 0)throw Error(k(407));n=n()}else{if(n=t(),b===null)throw Error(k(349));Tt&30||Pa(r,t,n)}l.memoizedState=n;var i={value:n,getSnapshot:t};return l.queue=i,Mo(za.bind(null,r,i,e),[e]),r.flags|=2048,Jn(9,Ta.bind(null,r,i,n,t),void 0,null),n},useId:function(){var e=$e(),t=b.identifierPrefix;if(U){var n=Qe,r=He;n=(r&~(1<<32-Ie(r)-1)).toString(32)+n,t=":"+t+"R"+n,n=Xn++,0<\/script>",e=e.removeChild(e.firstChild)):typeof r.is=="string"?e=s.createElement(n,{is:r.is}):(e=s.createElement(n),n==="select"&&(s=e,r.multiple?s.multiple=!0:r.size&&(s.size=r.size))):e=s.createElementNS(e,n),e[Ue]=t,e[Kn]=r,ec(e,t,!1,!1),t.stateNode=e;e:{switch(s=hi(n,r),n){case"dialog":M("cancel",e),M("close",e),l=r;break;case"iframe":case"object":case"embed":M("load",e),l=r;break;case"video":case"audio":for(l=0;lon&&(t.flags|=128,r=!0,kn(i,!1),t.lanes=4194304)}else{if(!r)if(e=br(s),e!==null){if(t.flags|=128,r=!0,n=e.updateQueue,n!==null&&(t.updateQueue=n,t.flags|=4),kn(i,!0),i.tail===null&&i.tailMode==="hidden"&&!s.alternate&&!U)return ie(t),null}else 2*K()-i.renderingStartTime>on&&n!==1073741824&&(t.flags|=128,r=!0,kn(i,!1),t.lanes=4194304);i.isBackwards?(s.sibling=t.child,t.child=s):(n=i.last,n!==null?n.sibling=s:t.child=s,i.last=s)}return i.tail!==null?(t=i.tail,i.rendering=t,i.tail=t.sibling,i.renderingStartTime=K(),t.sibling=null,n=A.current,D(A,r?n&1|2:n&1),t):(ie(t),null);case 22:case 23:return $s(),r=t.memoizedState!==null,e!==null&&e.memoizedState!==null!==r&&(t.flags|=8192),r&&t.mode&1?ye&1073741824&&(ie(t),t.subtreeFlags&6&&(t.flags|=8192)):ie(t),null;case 24:return null;case 25:return null}throw Error(k(156,t.tag))}function zf(e,t){switch(gs(t),t.tag){case 1:return me(t.type)&&Kr(),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return ln(),$(he),$(oe),_s(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 5:return Es(t),null;case 13:if($(A),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(k(340));nn()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return $(A),null;case 4:return ln(),null;case 10:return Ss(t.type._context),null;case 22:case 23:return $s(),null;case 24:return null;default:return null}}var xr=!1,se=!1,Lf=typeof WeakSet=="function"?WeakSet:Set,j=null;function Kt(e,t){var n=e.ref;if(n!==null)if(typeof n=="function")try{n(null)}catch(r){W(e,t,r)}else n.current=null}function Vi(e,t,n){try{n()}catch(r){W(e,t,r)}}var Yo=!1;function Rf(e,t){if(Ci=Br,e=oa(),vs(e)){if("selectionStart"in e)var n={start:e.selectionStart,end:e.selectionEnd};else e:{n=(n=e.ownerDocument)&&n.defaultView||window;var r=n.getSelection&&n.getSelection();if(r&&r.rangeCount!==0){n=r.anchorNode;var l=r.anchorOffset,i=r.focusNode;r=r.focusOffset;try{n.nodeType,i.nodeType}catch{n=null;break e}var s=0,u=-1,a=-1,c=0,m=0,y=e,h=null;t:for(;;){for(var v;y!==n||l!==0&&y.nodeType!==3||(u=s+l),y!==i||r!==0&&y.nodeType!==3||(a=s+r),y.nodeType===3&&(s+=y.nodeValue.length),(v=y.firstChild)!==null;)h=y,y=v;for(;;){if(y===e)break t;if(h===n&&++c===l&&(u=s),h===i&&++m===r&&(a=s),(v=y.nextSibling)!==null)break;y=h,h=y.parentNode}y=v}n=u===-1||a===-1?null:{start:u,end:a}}else n=null}n=n||{start:0,end:0}}else n=null;for(Ni={focusedElem:e,selectionRange:n},Br=!1,j=t;j!==null;)if(t=j,e=t.child,(t.subtreeFlags&1028)!==0&&e!==null)e.return=t,j=e;else for(;j!==null;){t=j;try{var w=t.alternate;if(t.flags&1024)switch(t.tag){case 0:case 11:case 15:break;case 1:if(w!==null){var x=w.memoizedProps,R=w.memoizedState,p=t.stateNode,d=p.getSnapshotBeforeUpdate(t.elementType===t.type?x:Le(t.type,x),R);p.__reactInternalSnapshotBeforeUpdate=d}break;case 3:var f=t.stateNode.containerInfo;f.nodeType===1?f.textContent="":f.nodeType===9&&f.documentElement&&f.removeChild(f.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(k(163))}}catch(g){W(t,t.return,g)}if(e=t.sibling,e!==null){e.return=t.return,j=e;break}j=t.return}return w=Yo,Yo=!1,w}function On(e,t,n){var r=t.updateQueue;if(r=r!==null?r.lastEffect:null,r!==null){var l=r=r.next;do{if((l.tag&e)===e){var i=l.destroy;l.destroy=void 0,i!==void 0&&Vi(t,n,i)}l=l.next}while(l!==r)}}function yl(e,t){if(t=t.updateQueue,t=t!==null?t.lastEffect:null,t!==null){var n=t=t.next;do{if((n.tag&e)===e){var r=n.create;n.destroy=r()}n=n.next}while(n!==t)}}function Bi(e){var t=e.ref;if(t!==null){var n=e.stateNode;switch(e.tag){case 5:e=n;break;default:e=n}typeof t=="function"?t(e):t.current=e}}function rc(e){var t=e.alternate;t!==null&&(e.alternate=null,rc(t)),e.child=null,e.deletions=null,e.sibling=null,e.tag===5&&(t=e.stateNode,t!==null&&(delete t[Ue],delete t[Kn],delete t[Pi],delete t[hf],delete t[mf])),e.stateNode=null,e.return=null,e.dependencies=null,e.memoizedProps=null,e.memoizedState=null,e.pendingProps=null,e.stateNode=null,e.updateQueue=null}function lc(e){return e.tag===5||e.tag===3||e.tag===4}function Xo(e){e:for(;;){for(;e.sibling===null;){if(e.return===null||lc(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.flags&2||e.child===null||e.tag===4)continue e;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function Wi(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.nodeType===8?n.parentNode.insertBefore(e,t):n.insertBefore(e,t):(n.nodeType===8?(t=n.parentNode,t.insertBefore(e,n)):(t=n,t.appendChild(e)),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=Qr));else if(r!==4&&(e=e.child,e!==null))for(Wi(e,t,n),e=e.sibling;e!==null;)Wi(e,t,n),e=e.sibling}function Hi(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(r!==4&&(e=e.child,e!==null))for(Hi(e,t,n),e=e.sibling;e!==null;)Hi(e,t,n),e=e.sibling}var te=null,Re=!1;function qe(e,t,n){for(n=n.child;n!==null;)ic(e,t,n),n=n.sibling}function ic(e,t,n){if(Ae&&typeof Ae.onCommitFiberUnmount=="function")try{Ae.onCommitFiberUnmount(al,n)}catch{}switch(n.tag){case 5:se||Kt(n,t);case 6:var r=te,l=Re;te=null,qe(e,t,n),te=r,Re=l,te!==null&&(Re?(e=te,n=n.stateNode,e.nodeType===8?e.parentNode.removeChild(n):e.removeChild(n)):te.removeChild(n.stateNode));break;case 18:te!==null&&(Re?(e=te,n=n.stateNode,e.nodeType===8?Kl(e.parentNode,n):e.nodeType===1&&Kl(e,n),Vn(e)):Kl(te,n.stateNode));break;case 4:r=te,l=Re,te=n.stateNode.containerInfo,Re=!0,qe(e,t,n),te=r,Re=l;break;case 0:case 11:case 14:case 15:if(!se&&(r=n.updateQueue,r!==null&&(r=r.lastEffect,r!==null))){l=r=r.next;do{var i=l,s=i.destroy;i=i.tag,s!==void 0&&(i&2||i&4)&&Vi(n,t,s),l=l.next}while(l!==r)}qe(e,t,n);break;case 1:if(!se&&(Kt(n,t),r=n.stateNode,typeof r.componentWillUnmount=="function"))try{r.props=n.memoizedProps,r.state=n.memoizedState,r.componentWillUnmount()}catch(u){W(n,t,u)}qe(e,t,n);break;case 21:qe(e,t,n);break;case 22:n.mode&1?(se=(r=se)||n.memoizedState!==null,qe(e,t,n),se=r):qe(e,t,n);break;default:qe(e,t,n)}}function Zo(e){var t=e.updateQueue;if(t!==null){e.updateQueue=null;var n=e.stateNode;n===null&&(n=e.stateNode=new Lf),t.forEach(function(r){var l=Vf.bind(null,e,r);n.has(r)||(n.add(r),r.then(l,l))})}}function ze(e,t){var n=t.deletions;if(n!==null)for(var r=0;rl&&(l=s),r&=~i}if(r=l,r=K()-r,r=(120>r?120:480>r?480:1080>r?1080:1920>r?1920:3e3>r?3e3:4320>r?4320:1960*If(r/1960))-r,10e?16:e,lt===null)var r=!1;else{if(e=lt,lt=null,ll=0,I&6)throw Error(k(331));var l=I;for(I|=4,j=e.current;j!==null;){var i=j,s=i.child;if(j.flags&16){var u=i.deletions;if(u!==null){for(var a=0;aK()-Ds?Nt(e,0):Fs|=n),ve(e,t)}function pc(e,t){t===0&&(e.mode&1?(t=fr,fr<<=1,!(fr&130023424)&&(fr=4194304)):t=1);var n=ae();e=Xe(e,t),e!==null&&(tr(e,t,n),ve(e,n))}function Af(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),pc(e,n)}function Vf(e,t){var n=0;switch(e.tag){case 13:var r=e.stateNode,l=e.memoizedState;l!==null&&(n=l.retryLane);break;case 19:r=e.stateNode;break;default:throw Error(k(314))}r!==null&&r.delete(t),pc(e,n)}var hc;hc=function(e,t,n){if(e!==null)if(e.memoizedProps!==t.pendingProps||he.current)pe=!0;else{if(!(e.lanes&n)&&!(t.flags&128))return pe=!1,Pf(e,t,n);pe=!!(e.flags&131072)}else pe=!1,U&&t.flags&1048576&&ga(t,Xr,t.index);switch(t.lanes=0,t.tag){case 2:var r=t.type;Rr(e,t),e=t.pendingProps;var l=tn(t,oe.current);qt(t,n),l=Ts(null,t,r,e,l,n);var i=zs();return t.flags|=1,typeof l=="object"&&l!==null&&typeof l.render=="function"&&l.$$typeof===void 0?(t.tag=1,t.memoizedState=null,t.updateQueue=null,me(r)?(i=!0,Gr(t)):i=!1,t.memoizedState=l.state!==null&&l.state!==void 0?l.state:null,Cs(t),l.updater=vl,t.stateNode=l,l._reactInternals=t,Ii(t,r,e,n),t=Mi(null,t,r,!0,i,n)):(t.tag=0,U&&i&&ys(t),ue(null,t,l,n),t=t.child),t;case 16:r=t.elementType;e:{switch(Rr(e,t),e=t.pendingProps,l=r._init,r=l(r._payload),t.type=r,l=t.tag=Wf(r),e=Le(r,e),l){case 0:t=Di(null,t,r,e,n);break e;case 1:t=Qo(null,t,r,e,n);break e;case 11:t=Wo(null,t,r,e,n);break e;case 14:t=Ho(null,t,r,Le(r.type,e),n);break e}throw Error(k(306,r,""))}return t;case 0:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Le(r,l),Di(e,t,r,l,n);case 1:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Le(r,l),Qo(e,t,r,l,n);case 3:e:{if(Ja(t),e===null)throw Error(k(387));r=t.pendingProps,i=t.memoizedState,l=i.element,Ca(e,t),qr(t,r,null,n);var s=t.memoizedState;if(r=s.element,i.isDehydrated)if(i={element:r,isDehydrated:!1,cache:s.cache,pendingSuspenseBoundaries:s.pendingSuspenseBoundaries,transitions:s.transitions},t.updateQueue.baseState=i,t.memoizedState=i,t.flags&256){l=sn(Error(k(423)),t),t=Ko(e,t,r,n,l);break e}else if(r!==l){l=sn(Error(k(424)),t),t=Ko(e,t,r,n,l);break e}else for(ge=ut(t.stateNode.containerInfo.firstChild),we=t,U=!0,Oe=null,n=Sa(t,null,r,n),t.child=n;n;)n.flags=n.flags&-3|4096,n=n.sibling;else{if(nn(),r===l){t=Ze(e,t,n);break e}ue(e,t,r,n)}t=t.child}return t;case 5:return Na(t),e===null&&Li(t),r=t.type,l=t.pendingProps,i=e!==null?e.memoizedProps:null,s=l.children,Ei(r,l)?s=null:i!==null&&Ei(r,i)&&(t.flags|=32),Za(e,t),ue(e,t,s,n),t.child;case 6:return e===null&&Li(t),null;case 13:return qa(e,t,n);case 4:return Ns(t,t.stateNode.containerInfo),r=t.pendingProps,e===null?t.child=rn(t,null,r,n):ue(e,t,r,n),t.child;case 11:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Le(r,l),Wo(e,t,r,l,n);case 7:return ue(e,t,t.pendingProps,n),t.child;case 8:return ue(e,t,t.pendingProps.children,n),t.child;case 12:return ue(e,t,t.pendingProps.children,n),t.child;case 10:e:{if(r=t.type._context,l=t.pendingProps,i=t.memoizedProps,s=l.value,D(Zr,r._currentValue),r._currentValue=s,i!==null)if(De(i.value,s)){if(i.children===l.children&&!he.current){t=Ze(e,t,n);break e}}else for(i=t.child,i!==null&&(i.return=t);i!==null;){var u=i.dependencies;if(u!==null){s=i.child;for(var a=u.firstContext;a!==null;){if(a.context===r){if(i.tag===1){a=Ke(-1,n&-n),a.tag=2;var c=i.updateQueue;if(c!==null){c=c.shared;var m=c.pending;m===null?a.next=a:(a.next=m.next,m.next=a),c.pending=a}}i.lanes|=n,a=i.alternate,a!==null&&(a.lanes|=n),Ri(i.return,n,t),u.lanes|=n;break}a=a.next}}else if(i.tag===10)s=i.type===t.type?null:i.child;else if(i.tag===18){if(s=i.return,s===null)throw Error(k(341));s.lanes|=n,u=s.alternate,u!==null&&(u.lanes|=n),Ri(s,n,t),s=i.sibling}else s=i.child;if(s!==null)s.return=i;else for(s=i;s!==null;){if(s===t){s=null;break}if(i=s.sibling,i!==null){i.return=s.return,s=i;break}s=s.return}i=s}ue(e,t,l.children,n),t=t.child}return t;case 9:return l=t.type,r=t.pendingProps.children,qt(t,n),l=_e(l),r=r(l),t.flags|=1,ue(e,t,r,n),t.child;case 14:return r=t.type,l=Le(r,t.pendingProps),l=Le(r.type,l),Ho(e,t,r,l,n);case 15:return Ya(e,t,t.type,t.pendingProps,n);case 17:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Le(r,l),Rr(e,t),t.tag=1,me(r)?(e=!0,Gr(t)):e=!1,qt(t,n),Qa(t,r,l),Ii(t,r,l,n),Mi(null,t,r,!0,e,n);case 19:return ba(e,t,n);case 22:return Xa(e,t,n)}throw Error(k(156,t.tag))};function mc(e,t){return Bu(e,t)}function Bf(e,t,n,r){this.tag=e,this.key=n,this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null,this.index=0,this.ref=null,this.pendingProps=t,this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null,this.mode=r,this.subtreeFlags=this.flags=0,this.deletions=null,this.childLanes=this.lanes=0,this.alternate=null}function Ne(e,t,n,r){return new Bf(e,t,n,r)}function As(e){return e=e.prototype,!(!e||!e.isReactComponent)}function Wf(e){if(typeof e=="function")return As(e)?1:0;if(e!=null){if(e=e.$$typeof,e===is)return 11;if(e===ss)return 14}return 2}function ft(e,t){var n=e.alternate;return n===null?(n=Ne(e.tag,t,e.key,e.mode),n.elementType=e.elementType,n.type=e.type,n.stateNode=e.stateNode,n.alternate=e,e.alternate=n):(n.pendingProps=t,n.type=e.type,n.flags=0,n.subtreeFlags=0,n.deletions=null),n.flags=e.flags&14680064,n.childLanes=e.childLanes,n.lanes=e.lanes,n.child=e.child,n.memoizedProps=e.memoizedProps,n.memoizedState=e.memoizedState,n.updateQueue=e.updateQueue,t=e.dependencies,n.dependencies=t===null?null:{lanes:t.lanes,firstContext:t.firstContext},n.sibling=e.sibling,n.index=e.index,n.ref=e.ref,n}function Fr(e,t,n,r,l,i){var s=2;if(r=e,typeof e=="function")As(e)&&(s=1);else if(typeof e=="string")s=5;else e:switch(e){case Mt:return Et(n.children,l,i,t);case ls:s=8,l|=8;break;case li:return e=Ne(12,n,t,l|2),e.elementType=li,e.lanes=i,e;case ii:return e=Ne(13,n,t,l),e.elementType=ii,e.lanes=i,e;case si:return e=Ne(19,n,t,l),e.elementType=si,e.lanes=i,e;case Nu:return wl(n,l,i,t);default:if(typeof e=="object"&&e!==null)switch(e.$$typeof){case ju:s=10;break e;case Cu:s=9;break e;case is:s=11;break e;case ss:s=14;break e;case be:s=16,r=null;break e}throw Error(k(130,e==null?e:typeof e,""))}return t=Ne(s,n,t,l),t.elementType=e,t.type=r,t.lanes=i,t}function Et(e,t,n,r){return e=Ne(7,e,r,t),e.lanes=n,e}function wl(e,t,n,r){return e=Ne(22,e,r,t),e.elementType=Nu,e.lanes=n,e.stateNode={isHidden:!1},e}function ei(e,t,n){return e=Ne(6,e,null,t),e.lanes=n,e}function ti(e,t,n){return t=Ne(4,e.children!==null?e.children:[],e.key,t),t.lanes=n,t.stateNode={containerInfo:e.containerInfo,pendingChildren:null,implementation:e.implementation},t}function Hf(e,t,n,r,l){this.tag=t,this.containerInfo=e,this.finishedWork=this.pingCache=this.current=this.pendingChildren=null,this.timeoutHandle=-1,this.callbackNode=this.pendingContext=this.context=null,this.callbackPriority=0,this.eventTimes=Fl(0),this.expirationTimes=Fl(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=Fl(0),this.identifierPrefix=r,this.onRecoverableError=l,this.mutableSourceEagerHydrationData=null}function Vs(e,t,n,r,l,i,s,u,a){return e=new Hf(e,t,n,u,a),t===1?(t=1,i===!0&&(t|=8)):t=0,i=Ne(3,null,null,t),e.current=i,i.stateNode=e,i.memoizedState={element:r,isDehydrated:n,cache:null,transitions:null,pendingSuspenseBoundaries:null},Cs(i),e}function Qf(e,t,n){var r=3"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(wc)}catch(e){console.error(e)}}wc(),wu.exports=xe;var Zf=wu.exports,kc,lu=Zf;kc=lu.createRoot,lu.hydrateRoot;const xc="bv.access",Qs="bv.refresh";function Xi(e){try{return localStorage.getItem(e)||""}catch{return""}}function iu(e,t){try{t?localStorage.setItem(e,t):localStorage.removeItem(e)}catch{}}function Ks(){return{access:Xi(xc),refresh:Xi(Qs)}}function Gs(e,t){iu(xc,e),iu(Qs,t)}function Cl(){Gs("","")}function Jf(){return!!Xi(Qs)}class ol extends Error{constructor(t,n,r){super(r),this.status=t,this.code=n}}let Sn=null;async function Sc(){return Sn||(Sn=(async()=>{const{refresh:e}=Ks();if(!e)throw new ol(401,"unauthorized","Signed out.");const t=await fetch("/api/auth/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refresh_token:e})});if(!t.ok)throw Cl(),new ol(t.status,"unauthorized","Your session has ended. Sign in again.");const n=await t.json();return Gs(n.access_token,n.refresh_token),n})().finally(()=>{Sn=null}),Sn)}async function G(e,t,n,r=!0){const{access:l}=Ks(),i={};l&&(i.Authorization="Bearer "+l),n!==void 0&&(i["Content-Type"]="application/json");const s=await fetch(t,{method:e,headers:i,body:n===void 0?void 0:JSON.stringify(n)});if(s.status===204)return null;const u=await s.text();let a=null;try{a=u?JSON.parse(u):null}catch{}if(s.ok)return a;const c=(a==null?void 0:a.error)||"";if(c==="token_expired"&&r)return await Sc(),G(e,t,n,!1);throw s.status===401&&Cl(),new ol(s.status,c,(a==null?void 0:a.message)||`Something went wrong (${s.status}).`)}const Ft=e=>{const t=new URLSearchParams;for(const[r,l]of Object.entries(e||{}))l!=null&&l!==""&&t.set(r,l);const n=t.toString();return n?"?"+n:""},ee={async login(e,t){const n=await fetch("/api/auth/login",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({email:e,password:t})}),r=await n.json().catch(()=>null);if(!n.ok)throw new ol(n.status,(r==null?void 0:r.error)||"",(r==null?void 0:r.message)||"Could not sign in.");return Gs(r.access_token,r.refresh_token),r.user},async logout(){try{await G("POST","/api/auth/logout")}catch{}Cl()},me:()=>G("GET","/api/auth/me"),sites:()=>G("GET","/api/sites"),arrivals:e=>G("GET","/api/visits"+Ft(e)),visitors:(e,t=50)=>G("GET","/api/visitors"+Ft({q:e,limit:t})),visitorHistory:(e,t=50)=>G("GET",`/api/visitors/${encodeURIComponent(e)}/history`+Ft({limit:t})),saveProfile:(e,t)=>G("PUT",`/api/visitors/${encodeURIComponent(e)}/profile`,t),footfall:e=>G("GET","/api/reports/footfall"+Ft(e)),conversion:e=>G("GET","/api/reports/conversion"+Ft(e)),cameras:()=>G("GET","/api/cameras"),createCamera:(e,t)=>G("POST",`/api/sites/${encodeURIComponent(e)}/cameras`,t),updateCamera:(e,t)=>G("PATCH",`/api/cameras/${encodeURIComponent(e)}`,t),deleteCamera:e=>G("DELETE",`/api/cameras/${encodeURIComponent(e)}`),checkCamera:(e,t,n)=>G("POST",`/api/cameras/${encodeURIComponent(e)}/check`,{kind:t,...n?{seconds:n}:{}}),siteCheck:e=>G("GET",`/api/sites/${encodeURIComponent(e)}/check`),enrolmentCode:(e,t)=>G("POST",`/api/sites/${encodeURIComponent(e)}/enrolment-code`,t||{}),ask:e=>G("POST","/api/assistant",{history:e}),clients:()=>G("GET","/api/admin/clients"),createClient:e=>G("POST","/api/admin/clients",e)};function qf({cursor:e,siteId:t,onPage:n,onError:r,signal:l}){let i=!1,s=e||"";return(async()=>{for(;!i;){try{const{access:a}=Ks(),c=await fetch("/api/visits/stream"+Ft({cursor:s,site_id:t}),{headers:{Authorization:"Bearer "+a,Accept:"text/event-stream"},signal:l});if(c.status===401){await Sc();continue}if(!c.ok||!c.body)throw new Error("stream unavailable");const m=c.body.getReader(),y=new TextDecoder;let h="";for(;!i;){const{value:v,done:w}=await m.read();if(w)break;h+=y.decode(v,{stream:!0});let x;for(;(x=h.indexOf(` - -`))!==-1;){const R=h.slice(0,x);h=h.slice(x+2);for(const p of R.split(` -`))if(p.startsWith("id: "))s=p.slice(4).trim();else if(p.startsWith("data: "))try{n(JSON.parse(p.slice(6)))}catch{}}}}catch(a){if(i||l!=null&&l.aborted)return;r==null||r(a)}if(i)return;await new Promise(a=>setTimeout(a,3e3))}})(),()=>{i=!0}}function bf({onSignedIn:e}){const[t,n]=_.useState(""),[r,l]=_.useState(""),[i,s]=_.useState(""),[u,a]=_.useState(!1),c=async m=>{m.preventDefault(),a(!0),s("");try{await ee.login(t.trim(),r),e(await ee.me())}catch(y){s(y.message),a(!1)}};return o.jsx("div",{className:"signin",children:o.jsxs("form",{className:"card",onSubmit:c,children:[o.jsx("span",{className:"mark big","aria-hidden":"true"}),o.jsx("h1",{children:"Behavision"}),o.jsx("p",{className:"sub",children:"Sign in to your company account."}),o.jsxs("label",{children:["Email",o.jsx("input",{type:"email",value:t,autoComplete:"username",autoFocus:!0,required:!0,onChange:m=>n(m.target.value),placeholder:"you@company.com"})]}),o.jsxs("label",{children:["Password",o.jsx("input",{type:"password",value:r,autoComplete:"current-password",required:!0,onChange:m=>l(m.target.value)})]}),i&&o.jsx("p",{className:"error",role:"alert",children:i}),o.jsx("button",{className:"primary",disabled:u||!t||!r,children:u?"Signing in…":"Sign in"}),o.jsx("p",{className:"foot",children:"Accounts are created by Loyaly. Ask your account manager if you need one."})]})})}function bn(e,t,n=[]){const[r,l]=_.useState(null),[i,s]=_.useState(null),[u,a]=_.useState(!0),c=_.useRef(!1),m=_.useRef(!0),y=_.useCallback(async()=>{if(!c.current){c.current=!0;try{const h=await e();if(!m.current)return;l(h),s(null)}catch(h){if(!m.current)return;s(h)}finally{c.current=!1,m.current&&a(!1)}}},n);return _.useEffect(()=>{if(m.current=!0,y(),!t)return()=>{m.current=!1};const h=setInterval(y,t);return()=>{m.current=!1,clearInterval(h)}},[y,t]),{data:r,error:i,loading:u,reload:y}}function ep({site:e,onClose:t}){const[n,r]=_.useState(null),[l,i]=_.useState(!1),[s,u]=_.useState(""),a=async()=>{i(!0),u(""),r(null);try{r(await ee.siteCheck(e.site_id))}catch(c){u(c.message)}finally{i(!1)}};return o.jsx("div",{className:"overlay",onClick:t,children:o.jsxs("aside",{className:"drawer",onClick:c=>c.stopPropagation(),children:[o.jsxs("header",{className:"drawer-head",children:[o.jsxs("div",{children:[o.jsxs("h2",{children:["Is ",e.name," working?"]}),o.jsx("p",{className:"sub",children:"Checks the whole chain, from the shop’s PC to head office."})]}),o.jsx("button",{className:"ghost",onClick:t,children:"Close"})]}),o.jsxs("div",{className:"drawer-body",children:[!n&&!l&&o.jsxs(o.Fragment,{children:[o.jsx("p",{className:"sub",children:"Nothing is changed and no one is disturbed — this reads what the shop has already reported."}),o.jsx("button",{className:"primary",onClick:a,children:"Run the check"})]}),l&&o.jsxs("div",{className:"waiting",children:[o.jsx("span",{className:"spinner","aria-hidden":"true"}),o.jsx("b",{children:"Checking…"})]}),s&&o.jsx("p",{className:"error",role:"alert",children:s}),n&&o.jsxs(o.Fragment,{children:[o.jsx("div",{className:"banner "+(n.ok?"ok":"warn"),children:o.jsxs("div",{children:[o.jsx("b",{children:n.ok?"This shop is working.":"This shop needs attention."}),!n.ok&&o.jsx(o.Fragment,{children:" Work down the list — the first failure usually explains the rest."})]})}),o.jsx("ol",{className:"checklist",children:n.steps.map(c=>o.jsxs("li",{className:c.status,children:[o.jsx("span",{className:"mark","aria-hidden":"true",children:c.status==="pass"?"✓":c.status==="fail"?"✕":c.status==="warn"?"!":"–"}),o.jsxs("div",{children:[o.jsx("b",{children:c.name}),o.jsx("span",{className:"sub",children:c.detail}),c.advice&&o.jsx("span",{className:"advice-line",children:c.advice})]})]},c.name))}),o.jsx("button",{className:"ghost",onClick:a,children:"Check again"})]}),o.jsx(tp,{site:e})]})]})})}function tp({site:e}){const[t,n]=_.useState(null),[r,l]=_.useState(!1),[i,s]=_.useState(""),[u,a]=_.useState(""),c=async()=>{l(!0),s("");try{n(await ee.enrolmentCode(e.site_id,{label:u.trim()}))}catch(m){s(m.message)}finally{l(!1)}};return o.jsxs("section",{className:"claim",children:[o.jsx("h3",{children:"Set up a shop PC"}),t?o.jsxs(o.Fragment,{children:[o.jsx("p",{className:"code","aria-live":"polite",children:t.code}),o.jsxs("p",{className:"sub",children:["Copy this now — it cannot be shown again. It works once, and stops working ",rp(t.expires_at),"."]}),o.jsxs("div",{className:"row",children:[o.jsx("button",{className:"ghost",onClick:()=>np(t.code),children:"Copy"}),o.jsx("button",{className:"ghost",onClick:()=>n(null),children:"Done"})]})]}):o.jsxs(o.Fragment,{children:[o.jsx("p",{className:"sub",children:"Creates a one-time code to type into Behavision on the shop’s computer. Use one for a new shop, a replacement PC, or a reinstall."}),o.jsxs("label",{className:"field",children:[o.jsx("span",{children:"What is this PC? (optional)"}),o.jsx("input",{value:u,placeholder:"counter PC",onChange:m=>a(m.target.value)})]}),i&&o.jsx("p",{className:"error",role:"alert",children:i}),o.jsx("button",{className:"ghost",onClick:c,disabled:r,children:r?"Creating…":"Create an installation code"})]})]})}function np(e){var t;try{(t=navigator.clipboard)==null||t.writeText(e)}catch{}}function rp(e){const t=new Date(e).getTime();if(Number.isNaN(t))return"shortly";const n=Math.round((t-Date.now())/864e5);return n<=0?"today":n===1?"tomorrow":`in ${n} days`}function lp(){const{data:e,error:t,loading:n}=bn(()=>ee.sites(),2e4,[]),{data:r}=bn(()=>ee.cameras(),6e4,[]),[l,i]=_.useState(null),s=e||[];if(n&&!e)return o.jsx(Nl,{});if(t)return o.jsx(El,{error:t});if(!s.length)return o.jsx(cp,{});const u=s.map(h=>{const v=(r||[]).filter(w=>w.site_id===h.site_id);return{site:h,cams:v,verdict:ip(h,v)}}),a=h=>u.filter(v=>v.verdict.tone===h).length,c=a("bad"),m=a("warn"),y=a("idle");return o.jsxs(o.Fragment,{children:[o.jsxs("header",{className:"head",children:[o.jsx("h1",{children:"Shops"}),o.jsxs("p",{className:"sub",children:[s.length," ",s.length===1?"shop":"shops",c>0&&o.jsxs(o.Fragment,{children:[" · ",o.jsxs("b",{className:"bad",children:[c," not working"]})]}),m>0&&o.jsxs(o.Fragment,{children:[" · ",o.jsxs("b",{className:"warn",children:[m," needing attention"]})]}),y>0&&o.jsxs(o.Fragment,{children:[" · ",y," not set up yet"]}),!c&&!m&&!y&&o.jsxs(o.Fragment,{children:[" · ",o.jsx("b",{className:"ok",children:"all working"})]})]})]}),o.jsx("div",{className:"grid sites",children:u.map(h=>o.jsx(op,{site:h.site,cams:h.cams,verdict:h.verdict,onCheck:()=>i(h.site)},h.site.site_id))}),l&&o.jsx(ep,{site:l,onClose:()=>i(null)})]})}function ip(e,t){const n=e.fraction_below_gate,r=e.cameras_total||t.length;return e.online?e.dropped>0?{tone:"bad",mark:"✕",words:`${e.dropped} visits lost and unrecoverable`}:r===0?{tone:"idle",mark:"+",headline:"Not set up",words:"No cameras set up yet"}:e.cameras_up===0?{tone:"bad",mark:"✕",words:"No cameras connected"}:e.cameras_up.5?{tone:"bad",mark:"✕",words:`${Zi(n)} of faces too poor to recognise`}:n>.2?{tone:"warn",mark:"!",words:`${Zi(n)} of faces too poor to recognise`}:e.queued>0?{tone:"warn",mark:"!",words:`${e.queued} visits waiting to upload`}:{tone:"ok",mark:"✓",words:`Working — ${r} ${r===1?"camera":"cameras"} connected`}:{tone:"bad",mark:"✕",headline:"Offline",words:`Offline — last heard from ${un(e.last_heartbeat_at)}`}}const sp={ok:"Working",warn:"Needs attention",bad:"Not working",idle:"Not set up"};function op({site:e,cams:t,verdict:n,onCheck:r}){const l=e.fraction_below_gate,i=n.tone,s=n.headline||sp[i],u=up(t),a=e.cameras_total||t.length;return o.jsxs("article",{className:"card site state-"+i,onClick:r,role:"button",tabIndex:0,onKeyDown:c=>c.key==="Enter"&&r(),children:[o.jsxs("div",{className:"shot",children:[u.url?o.jsx("img",{src:u.url,alt:`View inside ${e.name}`,loading:"lazy"}):o.jsxs("div",{className:"noshot",children:[o.jsx(ap,{}),u.reason&&o.jsx("span",{children:u.reason})]}),o.jsxs("div",{className:"shot-over",children:[o.jsxs("div",{className:"shot-name",children:[o.jsx("b",{children:e.name}),o.jsx("span",{children:a>0?`${a} ${a===1?"camera":"cameras"}`:"No cameras yet"})]}),o.jsxs("span",{className:"status "+i,children:[o.jsx("i",{"aria-hidden":"true"}),s]})]}),u.at&&o.jsx("span",{className:"shot-age",children:un(u.at)})]}),o.jsxs("div",{className:"metrics",children:[o.jsx(ni,{label:"Cameras",value:a?`${e.cameras_up}/${a}`:"—",tone:a?e.cameras_up0?Zi(1-l):"—",tone:l?l>.5?"bad":l>.2?"warn":"ok":"idle"}),o.jsx(ni,{label:"Last seen",value:un(e.last_heartbeat_at),tone:e.online?"ok":"bad"})]}),o.jsxs("div",{className:"verdict "+n.tone,children:[o.jsx("span",{className:"mark","aria-hidden":"true",children:n.mark}),o.jsx("span",{className:"words",children:n.words}),o.jsx("span",{className:"go","aria-hidden":"true",children:"→"})]})]})}function ni({label:e,value:t,tone:n}){return o.jsxs("div",{className:"metric",children:[o.jsx("b",{className:n,children:t}),o.jsx("span",{children:e})]})}function up(e){var r;if(!e.length)return{};let t=null;for(const l of e)!((r=l.snapshot)!=null&&r.available)||!l.snapshot.url||(!t||(l.snapshot_at||"")>(t.snapshot_at||""))&&(t=l);return t?{url:t.snapshot.url,at:t.snapshot_at}:{reason:e.map(l=>{var i;return(i=l.snapshot)==null?void 0:i.reason}).find(Boolean)||"No picture from this shop yet."}}function ap(){return o.jsxs("svg",{className:"shopmark",viewBox:"0 0 40 32","aria-hidden":"true",children:[o.jsx("path",{d:"M4 12h32v18H4z"}),o.jsx("path",{d:"M2 12l4-8h28l4 8"}),o.jsx("path",{d:"M15 30v-9h10v9"})]})}function Zi(e){return`${Math.round(e*100)}%`}function un(e){if(!e)return"never";const t=new Date(e).getTime();if(Number.isNaN(t))return"—";const n=Math.max(0,(Date.now()-t)/1e3);if(n<90)return"just now";const r=Math.round(n/60);if(r<60)return`${r} min ago`;const l=Math.round(r/60);return l<48?`${l} h ago`:`${Math.round(l/24)} days ago`}function Nl(){return o.jsxs("div",{className:"state",children:[o.jsx("span",{className:"spinner","aria-hidden":"true"}),"Loading…"]})}function El({error:e}){return o.jsx("div",{className:"state",children:o.jsx("p",{className:"error",role:"alert",children:e.message})})}function cp(){return o.jsxs("div",{className:"state",children:[o.jsx("h2",{children:"No shops yet"}),o.jsx("p",{className:"sub",children:"A shop appears here once its PC has been claimed with an enrolment code."})]})}const su=60;function dp(){const[e,t]=_.useState([]),[n,r]=_.useState("connecting"),[l,i]=_.useState(null),s=_.useRef(new Set);return _.useEffect(()=>{const u=new AbortController;let a=()=>{};return(async()=>{try{const c=await ee.arrivals({limit:30}),m=c.arrivals||[];m.forEach(y=>s.current.add(y.visit_id)),t(m.slice().reverse()),r("live"),a=qf({cursor:c.cursor,signal:u.signal,onPage:y=>{const h=(y.arrivals||[]).filter(v=>!s.current.has(v.visit_id));h.length&&(h.forEach(v=>s.current.add(v.visit_id)),r("live"),t(v=>[...h.reverse(),...v].slice(0,su)))},onError:()=>r("reconnecting")})}catch(c){i(c)}})(),()=>{u.abort(),a()}},[]),l?o.jsx(El,{error:l}):n==="connecting"&&!e.length?o.jsx(Nl,{}):o.jsxs(o.Fragment,{children:[o.jsxs("header",{className:"head",children:[o.jsx("h1",{children:"Live"}),o.jsxs("p",{className:"sub",children:[o.jsx("span",{className:"dot "+(n==="live"?"ok":"warn"),"aria-hidden":"true"}),n==="live"?"Connected":"Reconnecting…"," · ","last ",su," arrivals"]})]}),e.length===0?o.jsxs("div",{className:"state",children:[o.jsx("h2",{children:"Nobody yet"}),o.jsx("p",{className:"sub",children:"Arrivals appear here the moment a camera recognises someone."})]}):o.jsx("ul",{className:"arrivals",children:e.map(u=>o.jsx(fp,{a:u},u.visit_id))})]})}function fp({a:e}){const t=e.name||e.label||"Unidentified";return o.jsxs("li",{className:"card arrival",children:[o.jsx(pp,{image:e.image,name:t}),o.jsxs("div",{className:"who-col",children:[o.jsx("strong",{children:t}),o.jsxs("span",{className:"sub",children:[e.site,e.camera_id?` · ${e.camera_id}`:""," · ",un(e.occurred_at)]}),e.attributes&&o.jsx(mp,{attrs:e.attributes})]}),e.is_new_visitor?o.jsx("span",{className:"pill new",children:"New"}):o.jsx("span",{className:"pill",children:"Returning"})]})}function pp({image:e,name:t}){return e!=null&&e.available?o.jsx("img",{className:"face",src:e.url,alt:"",loading:"lazy"}):o.jsx("span",{className:"face initials",title:(e==null?void 0:e.reason)||"","aria-hidden":"true",children:hp(t)})}function hp(e){const t=String(e).trim().split(/\s+/).filter(Boolean);return t.length?t.length===1?t[0].slice(0,2).toUpperCase():(t[0][0]+t[t.length-1][0]).toUpperCase():"?"}function mp({attrs:e}){const t=[];return e.gender&&t.push(e.gender),e.age&&t.push(`~${Math.round(e.age)}`),e.emotion&&t.push(e.emotion),t.length?o.jsx("span",{className:"attrs",children:t.join(" · ")}):null}const Dr=[{id:"hikvision",label:"Hikvision",path:"/Streaming/Channels/101",note:"Channel 1, main stream. Use /Streaming/Channels/102 for the lower-quality sub stream."},{id:"dahua",label:"Dahua",path:"/cam/realmonitor?channel=1&subtype=0",note:"Channel 1, main stream. subtype=1 is the sub stream."},{id:"cpplus",label:"CP Plus",path:"/cam/realmonitor?channel=1&subtype=0",note:"CP Plus cameras use the Dahua stream path."},{id:"uniview",label:"Uniview",path:"/media/video1",note:"Some older Uniview models use /video1 instead."},{id:"tplink",label:"TP-Link / Tapo",path:"/stream1",note:"Tapo cameras need a separate camera account created in the Tapo app — your Tapo login will not work."},{id:"reolink",label:"Reolink",path:"/h264Preview_01_main",note:"Use /h264Preview_01_sub for the lower-quality stream."},{id:"amcrest",label:"Amcrest",path:"/cam/realmonitor?channel=1&subtype=0",note:"Amcrest cameras use the Dahua stream path."},{id:"axis",label:"Axis",path:"/axis-media/media.amp",note:""},{id:"onvif",label:"Other (ONVIF)",path:"/onvif1",note:"Many generic cameras answer here. If it does not work, look for “RTSP” in the camera’s own app."},{id:"manual",label:"I know the path",path:"",note:""}],ou=e=>Dr.find(t=>t.id===e)||Dr[Dr.length-1],vp=["Camera","Connection","Test","Walk past"];function yp({sites:e,existing:t,onClose:n,onSaved:r}){var d;const l=!(t!=null&&t.id),[i,s]=_.useState(l?0:2),[u,a]=_.useState(t!=null&&t.id?t:null),[c,m]=_.useState({site_id:(t==null?void 0:t.site_id)||((d=e[0])==null?void 0:d.site_id)||"",make:"hikvision",label:(t==null?void 0:t.label)||"",host:(t==null?void 0:t.host)||"",port:(t==null?void 0:t.port)||554,path:(t==null?void 0:t.path)||"/Streaming/Channels/101",username:(t==null?void 0:t.username)||"",password:""}),[y,h]=_.useState(!1),[v,w]=_.useState(""),x=f=>g=>m(S=>({...S,[f]:g.target.value})),R=f=>{const g=ou(f.target.value);m(S=>({...S,make:g.id,path:g.path||S.path}))},p=async()=>{h(!0),w("");const f={};for(const[g,S]of Object.entries(c))g==="site_id"||g==="make"||S===""||S==null||(f[g]=g==="port"?Number(S):S);try{const g=u!=null&&u.id?await ee.updateCamera(u.id,f):await ee.createCamera(c.site_id,f);a(g),s(2),r==null||r(g,{keepOpen:!0})}catch(g){w(g.message)}finally{h(!1)}};return o.jsx("div",{className:"overlay",onClick:n,children:o.jsxs("aside",{className:"drawer wizard",onClick:f=>f.stopPropagation(),children:[o.jsxs("header",{className:"drawer-head",children:[o.jsxs("div",{children:[o.jsx("h2",{children:l?"Set up a camera":(u==null?void 0:u.label)||(t==null?void 0:t.label)}),o.jsx("p",{className:"sub",children:i<2?"The shop’s PC connects to the camera — nothing needs opening to the internet.":"Prove it works before you rely on it."})]}),o.jsx("button",{className:"ghost",onClick:n,children:"Close"})]}),o.jsx("ol",{className:"steps",children:vp.map((f,g)=>o.jsxs("li",{className:g===i?"now":go.jsx("option",{value:f.site_id,children:f.name},f.site_id))})]}),o.jsxs("label",{children:["What should staff call it?",o.jsx("input",{value:c.label,onChange:x("label"),placeholder:"Entrance",autoFocus:!0}),o.jsx("span",{className:"hint",children:"Visits are recorded against this name, so it cannot be changed later."})]}),o.jsxs("label",{children:["Make of camera",o.jsx("select",{value:c.make,onChange:R,children:Dr.map(f=>o.jsx("option",{value:f.id,children:f.label},f.id))}),o.jsx("span",{className:"hint",children:ou(c.make).note||"This only fills in the stream path for you. You can change it on the next step."})]}),o.jsx("button",{className:"primary",disabled:!c.label.trim(),onClick:()=>s(1),children:"Next"})]}),i===1&&o.jsxs("div",{className:"drawer-body",children:[o.jsxs("label",{children:["Camera’s address on the shop’s network",o.jsx("input",{value:c.host,onChange:x("host"),placeholder:"192.168.0.138",autoFocus:!0}),o.jsx("span",{className:"hint",children:"Not a website address. It usually starts 192.168. — look in the camera’s own app, on its label, or in your router’s device list."})]}),o.jsxs("div",{className:"pair",children:[o.jsxs("label",{children:["Port",o.jsx("input",{type:"number",value:c.port,onChange:x("port")})]}),o.jsxs("label",{children:["Stream path",o.jsx("input",{value:c.path,onChange:x("path")})]})]}),o.jsxs("label",{children:["Camera username",o.jsx("input",{value:c.username,onChange:x("username"),placeholder:"admin",name:"camera-account",autoComplete:"off",autoCorrect:"off",autoCapitalize:"none",spellCheck:"false"}),o.jsx("span",{className:"hint",children:"The camera’s own login, not your Behavision one."})]}),o.jsxs("label",{children:["Camera password",o.jsx("input",{type:"password",value:c.password,onChange:x("password"),name:"camera-secret",autoComplete:"new-password",placeholder:u!=null&&u.has_password?"(unchanged)":""})]}),v&&o.jsx("p",{className:"error",role:"alert",children:v}),o.jsxs("div",{className:"pair",children:[o.jsx("button",{className:"ghost",onClick:()=>s(0),children:"Back"}),o.jsx("button",{className:"primary",disabled:y||!c.host.trim(),onClick:p,children:y?"Saving…":"Save and test"})]})]}),i>=2&&u&&o.jsx(gp,{camera:u,step:i,onStep:s,onUpdated:f=>{a(f),r==null||r(f,{keepOpen:!0})},onEdit:()=>s(1),onDone:n})]})})}function gp({camera:e,step:t,onStep:n,onUpdated:r,onEdit:l,onDone:i}){const[s,u]=_.useState(e),[a,c]=_.useState(!1),[m,y]=_.useState(""),h=_.useRef(null);_.useEffect(()=>u(e),[e]);const v=s.check||{},w=v.state==="requested"||v.state==="running";_.useEffect(()=>{if(!w)return;let p=!0;const d=async()=>{try{const g=(await ee.cameras()).find(S=>S.id===s.id);p&&g&&(u(g),r==null||r(g))}catch{}};return h.current=setInterval(d,4e3),()=>{p=!1,clearInterval(h.current)}},[w,s.id]);const x=async(p,d)=>{c(!0),y("");try{const f=await ee.checkCamera(s.id,p,d);u(f),r==null||r(f),n(p==="placement"?3:2)}catch(f){y(f.message)}finally{c(!1)}},R=v.kind==="connection"&&v.state==="done"&&v.ok;return o.jsx(o.Fragment,{children:o.jsxs("div",{className:"drawer-body",children:[t===2?o.jsxs(o.Fragment,{children:[o.jsx("h3",{children:"Can the shop’s PC reach this camera?"}),o.jsx("p",{className:"sub",children:"The PC opens the stream once and takes a single picture. Nothing is recorded."})]}):o.jsxs(o.Fragment,{children:[o.jsx("h3",{children:"Can it actually recognise a face?"}),o.jsxs("p",{className:"sub",children:["Someone needs to ",o.jsx("b",{children:"walk through the camera’s view and out of it"}),", the way a customer would. Standing still measures nothing — the check scores the best view of each person as they leave the frame, which is what recognition really uses."]})]}),w&&o.jsxs("div",{className:"waiting",children:[o.jsx("span",{className:"spinner","aria-hidden":"true"}),o.jsxs("div",{children:[o.jsx("b",{children:v.state==="running"?"Running on the shop’s PC…":"Waiting for the shop’s PC…"}),o.jsx("span",{className:"sub",children:v.state==="running"?"This takes about "+(v.seconds||25)+" seconds.":"It picks up the request within a couple of minutes."})]})]}),v.state==="done"&&o.jsx(wp,{check:v}),m&&o.jsx("p",{className:"error",role:"alert",children:m}),o.jsx("div",{className:"pair",children:t===2?o.jsxs(o.Fragment,{children:[o.jsx("button",{className:"ghost",onClick:l,children:"Change details"}),o.jsx("button",{className:"primary",disabled:a||w,onClick:()=>x("connection"),children:v.state==="done"?"Test again":"Test connection"})]}):o.jsxs(o.Fragment,{children:[o.jsx("button",{className:"ghost",onClick:()=>n(2),children:"Back"}),o.jsx("button",{className:"primary",disabled:a||w,onClick:()=>x("placement",25),children:v.state==="done"?"Check again":"Start walk-past check"})]})}),t===2&&R&&!w&&o.jsx("button",{className:"primary",onClick:()=>n(3),children:"Next: prove it can recognise faces"}),t===3&&v.state==="done"&&!w&&o.jsx("button",{className:"ghost",onClick:i,children:"Finish"})]})})}function wp({check:e}){var n,r,l;const t=e.ok?"ok":e.verdict==="marginal"?"warn":"bad";return o.jsxs("div",{className:"outcome "+t,children:[o.jsxs("div",{className:"outcome-head",children:[o.jsx("span",{className:"pill "+t,children:e.ok?"Working":"Not ready"}),o.jsx("b",{children:e.headline||(e.ok?"Working":"Could not be verified")})]}),((n=e.image)==null?void 0:n.available)&&o.jsx("img",{className:"proof",src:e.image.url,alt:"The view from this camera",loading:"lazy"}),((r=e.advice)==null?void 0:r.length)>0&&o.jsx("ul",{className:"advice",children:e.advice.map((i,s)=>o.jsx("li",{children:i},s))}),((l=e.detail)==null?void 0:l.faces)!=null&&o.jsxs("p",{className:"sub",children:[e.detail.faces," ",e.detail.faces===1?"person":"people"," walked past during the check."]})]})}function kp(e){const t=e.check||{};return t.state==="requested"||t.state==="running"?{tone:"idle",mark:"…",words:"Checking now"}:t.state!=="done"?{tone:"idle",mark:"?",words:"Not checked yet"}:t.kind==="placement"?t.ok?{tone:"ok",mark:"✓",words:"Recognises faces here"}:{tone:"bad",mark:"✕",words:t.headline||"Cannot recognise faces here"}:t.ok?{tone:"warn",mark:"!",words:"Stream works — faces not checked yet"}:{tone:"bad",mark:"✕",words:t.headline||"Could not reach the camera"}}function xp({user:e}){const{data:t,error:n,loading:r,reload:l}=bn(()=>ee.cameras(),2e4,[]),{data:i}=bn(()=>ee.sites(),0,[]),[s,u]=_.useState(null),a=["admin","owner","manager"].includes(e.role),c=t||[],m=c.filter(v=>v.connected).length,y=c.filter(v=>v.connected===!1).length,h=c.filter(v=>v.connected==null).length;return o.jsxs(o.Fragment,{children:[o.jsxs("header",{className:"head",children:[o.jsx("h1",{children:"Cameras"}),o.jsxs("p",{className:"sub",children:[c.length," ",c.length===1?"camera":"cameras",m>0&&o.jsxs(o.Fragment,{children:[" · ",o.jsxs("b",{className:"ok",children:[m," connected"]})]}),y>0&&o.jsxs(o.Fragment,{children:[" · ",o.jsxs("b",{className:"bad",children:[y," down"]})]}),h>0&&o.jsxs(o.Fragment,{children:[" · ",h," waiting for the shop PC"]})]}),a&&o.jsx("button",{className:"primary",onClick:()=>u({}),children:"Set up a camera"})]}),r&&!t?o.jsx(Nl,{}):n?o.jsx(El,{error:n}):c.length===0?o.jsxs("div",{className:"state",children:[o.jsx("h2",{children:"No cameras yet"}),o.jsx("p",{className:"sub",children:"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."})]}):o.jsx("div",{className:"grid cams",children:c.map(v=>o.jsx(Sp,{cam:v,canEdit:a,onEdit:()=>u(v)},v.id))}),s&&o.jsx(yp,{existing:s.id?s:null,sites:i||[],onClose:()=>{u(null),l()},onSaved:(v,w)=>{w!=null&&w.keepOpen||u(null),l()}})]})}function Sp({cam:e,canEdit:t,onEdit:n}){var s,u;const r=e.connected==null?"idle":e.connected?"ok":"bad",l=e.connected==null?"Waiting for the shop PC":e.connected?"Connected":"Not connecting",i=kp(e);return o.jsxs("article",{className:"card cam state-"+r,onClick:t?n:void 0,role:t?"button":void 0,tabIndex:t?0:void 0,onKeyDown:a=>t&&a.key==="Enter"&&n(),children:[o.jsxs("div",{className:"shot",children:[(s=e.snapshot)!=null&&s.available?o.jsx("img",{src:e.snapshot.url,alt:`View from ${e.label}`,loading:"lazy"}):o.jsxs("div",{className:"noshot",children:[o.jsx("span",{className:"lens","aria-hidden":"true"}),((u=e.snapshot)==null?void 0:u.reason)||"No picture yet."]}),o.jsxs("div",{className:"shot-over",children:[o.jsxs("div",{className:"shot-name",children:[o.jsx("b",{children:e.label||e.camera_id}),o.jsx("span",{children:e.site})]}),o.jsxs("span",{className:"status "+r,children:[o.jsx("i",{"aria-hidden":"true"}),l]})]}),e.snapshot_at&&o.jsx("span",{className:"shot-age",children:un(e.snapshot_at)})]}),o.jsxs("div",{className:"verdict "+i.tone,children:[o.jsx("span",{className:"mark","aria-hidden":"true",children:i.mark}),o.jsx("span",{className:"words",children:i.words}),t&&o.jsx("span",{className:"go","aria-hidden":"true",children:"→"})]})]})}const jp=["Is everything working today?","Why is footfall low at Chennai?","How many people came in last week?","Which cameras still need checking?"];function Cp({open:e,onClose:t}){const[n,r]=_.useState([]),[l,i]=_.useState(""),[s,u]=_.useState(!1),[a,c]=_.useState(!1),m=_.useRef(null),y=_.useRef(null);_.useEffect(()=>{var v;e&&((v=y.current)==null||v.focus())},[e]),_.useEffect(()=>{var v;(v=m.current)==null||v.scrollIntoView({behavior:"smooth",block:"end"})},[n,s]);const h=async v=>{const w=(v??l).trim();if(!w||s)return;const x=[...n,{role:"user",text:w}];r(x),i(""),u(!0);try{const R=await ee.ask(x);r(p=>[...p,{role:"assistant",text:R.text,used:R.used}])}catch(R){R.code==="assistant_off"?c(!0):r(p=>[...p,{role:"assistant",text:R.message,failed:!0}])}finally{u(!1)}};return e?o.jsxs("aside",{className:"assistant",role:"complementary","aria-label":"Assistant",children:[o.jsxs("header",{className:"assistant-head",children:[o.jsxs("div",{children:[o.jsx("b",{children:"Ask Behavision"}),o.jsx("span",{className:"sub",children:"It reads your shops’ own data to answer."})]}),o.jsx("button",{className:"ghost",onClick:t,"aria-label":"Close assistant",children:"✕"})]}),o.jsxs("div",{className:"assistant-body",children:[a?o.jsx("p",{className:"sub pad",children:"The assistant is not switched on for this server."}):n.length===0?o.jsxs("div",{className:"suggest",children:[o.jsx("p",{className:"sub",children:"Try asking:"}),jp.map(v=>o.jsx("button",{className:"chip",onClick:()=>h(v),children:v},v))]}):n.map((v,w)=>{var x;return o.jsxs("div",{className:"bubble "+v.role+(v.failed?" failed":""),children:[v.text,((x=v.used)==null?void 0:x.length)>0&&o.jsxs("span",{className:"used",children:["looked at: ",v.used.map(Np).join(", ")]})]},w)}),s&&o.jsxs("div",{className:"bubble assistant-thinking",children:[o.jsx("span",{className:"spinner","aria-hidden":"true"})," Looking…"]}),o.jsx("div",{ref:m})]}),!a&&o.jsxs("form",{className:"assistant-ask",onSubmit:v=>{v.preventDefault(),h()},children:[o.jsx("input",{ref:y,value:l,onChange:v=>i(v.target.value),placeholder:"Ask about your shops…",disabled:s}),o.jsx("button",{className:"primary",disabled:s||!l.trim(),children:"Ask"})]})]}):null}function Np(e){return{list_shops:"your shops",check_shop:"a shop check",list_cameras:"your cameras",check_camera:"a camera check",footfall:"footfall",sales:"sales",find_customer:"customer records"}[e]||e}function Ep(){const{data:e,error:t,loading:n,reload:r}=bn(()=>ee.clients(),0,[]),[l,i]=_.useState(!1),[s,u]=_.useState(null),a=e||[];return o.jsxs(o.Fragment,{children:[o.jsxs("header",{className:"head",children:[o.jsx("h1",{children:"Companies"}),o.jsx("button",{className:"primary",onClick:()=>{u(null),i(!0)},children:"New company"})]}),s&&o.jsx(Pp,{result:s,onDismiss:()=>u(null)}),n&&!e?o.jsx(Nl,{}):t?o.jsx(El,{error:t}):a.length===0?o.jsxs("div",{className:"state",children:[o.jsx("h2",{children:"No companies yet"}),o.jsx("p",{className:"sub",children:"Create one, and its owner can sign in straight away."})]}):o.jsx("div",{className:"tablewrap",children:o.jsxs("table",{className:"rows",children:[o.jsx("thead",{children:o.jsxs("tr",{children:[o.jsx("th",{children:"Company"}),o.jsx("th",{children:"Short name"}),o.jsx("th",{children:"Sites"}),o.jsx("th",{children:"People"}),o.jsx("th",{children:"Created"})]})}),o.jsx("tbody",{children:a.map(c=>o.jsxs("tr",{children:[o.jsx("td",{children:o.jsx("strong",{children:c.name})}),o.jsx("td",{children:o.jsx("code",{children:c.slug})}),o.jsx("td",{className:"num",children:c.sites}),o.jsx("td",{className:"num",children:c.users}),o.jsx("td",{className:"sub",children:un(c.created_at)})]},c.id))})]})}),l&&o.jsx(_p,{onClose:()=>i(!1),onCreated:c=>{i(!1),u(c),r()}})]})}function _p({onClose:e,onCreated:t}){const[n,r]=_.useState({company_name:"",owner_name:"",owner_email:"",password:""}),[l,i]=_.useState(!1),[s,u]=_.useState(""),a=m=>y=>r({...n,[m]:y.target.value}),c=async m=>{m.preventDefault(),i(!0),u("");try{t(await ee.createClient(n))}catch(y){u(y.message),i(!1)}};return o.jsx("div",{className:"overlay",onClick:e,children:o.jsxs("aside",{className:"drawer narrow",onClick:m=>m.stopPropagation(),children:[o.jsxs("header",{className:"drawer-head",children:[o.jsx("h2",{children:"New company"}),o.jsx("button",{className:"ghost",onClick:e,children:"Close"})]}),o.jsxs("form",{className:"drawer-body",onSubmit:c,children:[o.jsxs("label",{children:["Company name",o.jsx("input",{value:n.company_name,onChange:a("company_name"),required:!0,autoFocus:!0})]}),o.jsxs("label",{children:["Owner’s name",o.jsx("input",{value:n.owner_name,onChange:a("owner_name")})]}),o.jsxs("label",{children:["Owner’s email",o.jsx("input",{type:"email",value:n.owner_email,onChange:a("owner_email"),required:!0})]}),o.jsxs("label",{children:["Password",o.jsx("input",{type:"text",value:n.password,onChange:a("password"),placeholder:"Leave empty to generate one"}),o.jsx("span",{className:"hint",children:"Generated is better — a password you invent for someone else ends up weak and sent over chat."})]}),s&&o.jsx("p",{className:"error",role:"alert",children:s}),o.jsx("button",{className:"primary",disabled:l,children:l?"Creating…":"Create company"})]})]})})}function Pp({result:e,onDismiss:t}){return o.jsxs("div",{className:"banner ok credentials",children:[o.jsxs("div",{children:[o.jsxs("b",{children:[e.slug," created."]})," These sign-in details are shown once and cannot be recovered. Send them to the owner now.",o.jsxs("dl",{className:"creds",children:[o.jsxs("div",{children:[o.jsx("dt",{children:"Email"}),o.jsx("dd",{children:o.jsx("code",{children:e.owner_email})})]}),o.jsxs("div",{children:[o.jsx("dt",{children:"Password"}),o.jsx("dd",{children:o.jsx("code",{children:e.password})})]})]})]}),o.jsx("button",{className:"ghost",onClick:t,children:"Done"})]})}const Tp=[{id:"sites",label:"Shops",View:lp},{id:"live",label:"Live",View:dp},{id:"cameras",label:"Cameras",View:xp}],zp=[{id:"clients",label:"Companies",View:Ep}];function Lp(){const[e,t]=_.useState(null),[n,r]=_.useState(!0),[l,i]=_.useState("sites"),[s,u]=_.useState(!1);if(_.useEffect(()=>{(async()=>{if(Jf())try{t(await ee.me())}catch{Cl()}r(!1)})()},[]),n)return o.jsxs("div",{className:"boot",children:[o.jsx("span",{className:"spinner","aria-hidden":"true"}),"Loading…"]});if(!e)return o.jsx(bf,{onSignedIn:t});const a=e.role==="admin"&&!e.client_id,c=a?zp:Tp,m=c.find(v=>v.id===l)||c[0],y=m.View,h=async()=>{await ee.logout(),t(null)};return o.jsxs("div",{className:"app",children:[o.jsxs("header",{className:"topbar",children:[o.jsxs("div",{className:"brand",children:[o.jsx("span",{className:"mark","aria-hidden":"true"}),o.jsxs("div",{children:[o.jsx("strong",{children:"Behavision"}),o.jsx("span",{className:"org",children:e.client_name||"Loyaly platform"})]})]}),o.jsx("nav",{className:"tabs",children:c.map(v=>o.jsx("button",{onClick:()=>i(v.id),"aria-current":m.id===v.id?"page":void 0,children:v.label},v.id))}),o.jsxs("div",{className:"who",children:[o.jsxs("button",{className:"ask-btn"+(s?" on":""),onClick:()=>u(v=>!v),children:[o.jsx("span",{"aria-hidden":"true",children:"✳"})," Ask"]}),o.jsx("span",{className:"name",children:e.full_name||e.email}),o.jsx("span",{className:"role",children:a?"platform admin":e.role}),o.jsx("button",{className:"ghost",onClick:h,children:"Sign out"})]})]}),o.jsxs("div",{className:"with-assistant"+(s?" open":""),children:[o.jsx("main",{className:"page",children:o.jsx(y,{user:e})}),o.jsx(Cp,{open:s,onClose:()=>u(!1)})]})]})}kc(document.getElementById("root")).render(o.jsx(Lp,{})); diff --git a/server/internal/web/dist/index.html b/server/internal/web/dist/index.html index 109ee0e..0d40333 100644 --- a/server/internal/web/dist/index.html +++ b/server/internal/web/dist/index.html @@ -5,7 +5,7 @@ Behavision - + diff --git a/server/migrations/009_camera_snapshots.sql b/server/migrations/009_camera_snapshots.sql new file mode 100644 index 0000000..357c7fc --- /dev/null +++ b/server/migrations/009_camera_snapshots.sql @@ -0,0 +1,45 @@ +-- The latest still frame from each camera, held by the server itself. +-- +-- Camera snapshots already worked, through the same presigned-URL path face +-- images use: the agent asks for a URL, PUTs the JPEG to object storage, and +-- the server presigns a short-lived link when somebody looks. That is the right +-- design for face images - one per visit, unbounded, and they must never touch +-- a shop PC's disk or the server's. +-- +-- It is the wrong design for the ONE case where a deployment has no object +-- storage at all. Head office then reports "This system is not storing images" +-- for every camera, forever, on a screen whose whole point is to SHOW the +-- camera. A self-hosted customer who does not want an S3 bucket, and every +-- local install, got a wall of empty tiles. +-- +-- What makes this safe to put in the database, when face images are not: +-- +-- * ONE ROW PER CAMERA. The primary key is the camera, so a snapshot +-- replaces its predecessor. An estate's storage is (cameras x ~100 KB) +-- and does not grow with time or with footfall. Face images grow with +-- every visitor who ever walks in, which is why they stay in a bucket. +-- * It is a picture of a shop floor, not a face crop attached to an +-- identity. It carries no template and is not tied to a person. +-- * ON DELETE CASCADE from the camera. Removing a camera removes its +-- picture, with no second place to remember to clean up. +-- +-- When object storage IS configured nothing changes: the bucket path stays +-- primary and this table is not written. + +BEGIN; + +CREATE TABLE IF NOT EXISTS camera_snapshots ( + camera_id uuid PRIMARY KEY REFERENCES site_cameras(id) ON DELETE CASCADE, + -- Denormalised deliberately, like every other table here: a cross-tenant + -- read should require a wrong WHERE clause rather than a forgotten join. + client_id uuid NOT NULL REFERENCES clients(id) ON DELETE CASCADE, + site_id uuid NOT NULL REFERENCES sites(id) ON DELETE CASCADE, + image bytea NOT NULL, + bytes integer NOT NULL, + captured_at timestamptz NOT NULL DEFAULT now() +); + +CREATE INDEX IF NOT EXISTS camera_snapshots_client_idx + ON camera_snapshots (client_id); + +COMMIT; diff --git a/web/src/api.js b/web/src/api.js index 2b093c4..8914a10 100644 --- a/web/src/api.js +++ b/web/src/api.js @@ -91,6 +91,37 @@ async function send(method, path, body, retry = true) { parsed?.message || `Something went wrong (${res.status}).`) } +// fetchImage loads a picture this server holds itself, with the session's +// bearer token, and returns an object URL an can use. +// +// It exists because an cannot carry an Authorization header. The +// object-storage path returns a presigned absolute URL that needs no auth, +// which is why it worked with a plain src; a picture served from our own +// database has no such link, and minting an unauthenticated one so that +// could use it would add a way to reach a photograph of somebody's shop floor +// without a session - the opposite of what this path is for. +// +// The caller MUST revoke the returned URL when it is finished with it, or the +// browser keeps every blob it has ever loaded for the life of the page. +async function fetchImage(path, retry = true) { + const { access } = tokens() + const res = await fetch(path, { + headers: access ? { Authorization: 'Bearer ' + access } : {}, + }) + if (res.ok) return URL.createObjectURL(await res.blob()) + + let parsed = null + try { parsed = await res.json() } catch { /* an image endpoint may not answer json */ } + const code = parsed?.error || '' + if (code === 'token_expired' && retry) { + await refresh() + return fetchImage(path, false) + } + if (res.status === 401) clearTokens() + throw new ApiError(res.status, code, + parsed?.message || `That picture could not be loaded (${res.status}).`) +} + const qs = (params) => { const p = new URLSearchParams() for (const [k, v] of Object.entries(params || {})) { @@ -138,6 +169,9 @@ export const api = { conversion: (params) => send('GET', '/api/reports/conversion' + qs(params)), cameras: () => send('GET', '/api/cameras'), + // A camera picture this server holds itself. Returns an object URL the + // caller must revoke; see fetchImage. + cameraSnapshot: (url) => fetchImage(url), createCamera: (siteID, cam) => send('POST', `/api/sites/${encodeURIComponent(siteID)}/cameras`, cam), updateCamera: (id, cam) => send('PATCH', `/api/cameras/${encodeURIComponent(id)}`, cam), diff --git a/web/src/hooks.js b/web/src/hooks.js index 5bd73b2..8c9d00d 100644 --- a/web/src/hooks.js +++ b/web/src/hooks.js @@ -1,5 +1,7 @@ import { useCallback, useEffect, useRef, useState } from 'react' +import { api } from './api.js' + // usePolled runs `fn` now and every `everyMs`, and is careful about the two // things every screen would otherwise get wrong on its own: overlapping // requests when the server is slower than the interval, and setting state @@ -40,3 +42,40 @@ export function usePolled(fn, everyMs, deps = []) { return { data, error, loading, reload: run } } + + +// useAuthedImage loads a picture that needs the session's bearer token and +// hands back a URL an can use. +// +// Two things it has to get right, and both were bugs the first time something +// like it was written elsewhere in this app: +// +// * REVOKE. An object URL pins the blob in memory until it is revoked, and +// this component re-renders on every poll. Without the cleanup a camera +// screen left open for an afternoon holds hundreds of copies of the same +// photograph. +// * Key on the URL, not on the object. `snapshot` is a fresh object on every +// poll, so an effect depending on it would re-fetch 90 KB per camera every +// few seconds; the URL only changes when the picture actually does. +export function useAuthedImage(url) { + const [src, setSrc] = useState(null) + + useEffect(() => { + if (!url) { setSrc(null); return } + let alive = true + let objectURL = null + api.cameraSnapshot(url) + .then(u => { + if (!alive) { URL.revokeObjectURL(u); return } + objectURL = u + setSrc(u) + }) + .catch(() => { if (alive) setSrc(null) }) + return () => { + alive = false + if (objectURL) URL.revokeObjectURL(objectURL) + } + }, [url]) + + return src +} diff --git a/web/src/views/Cameras.jsx b/web/src/views/Cameras.jsx index 5b8868e..df8bb6a 100644 --- a/web/src/views/Cameras.jsx +++ b/web/src/views/Cameras.jsx @@ -1,6 +1,7 @@ import { useState } from 'react' import { api } from '../api.js' import { usePolled } from '../hooks.js' +import Shot from './Shot.jsx' import { ago, Loading, Problem } from './Sites.jsx' import CameraSetup from './CameraSetup.jsx' @@ -114,7 +115,7 @@ function CameraCard({ cam, canEdit, onEdit }) { onKeyDown={e => canEdit && e.key === 'Enter' && onEdit()}>
{cam.snapshot?.available - ? {`View + ? :