From 4c750cb2acc4e138ebf9a41361241061ce815a8f Mon Sep 17 00:00:00 2001 From: Suriyakumarvijayanayagam Date: Sat, 19 Sep 2026 11:55:26 +0530 Subject: [PATCH] Opening a shop is an API call; the broker learns of it in the same request The last step of onboarding that needed a shell: provision site printed a broker password and a person typed it into Mosquitto's passwd file on the host - mounted read-only in the container, so the first attempt failed silently and the password was re-rolled. No tenant could open a second branch without us. The server now drives Mosquitto's dynamic-security plugin over its own broker login: POST /api/sites (owner) writes the row and the sealed password, registers the login and a per-site role with literal topics (the 2.0 plugin does not substitute %u - measured), and removes the row again if the broker refuses, so a shop cannot exist in the database and not on the broker. provision site goes through the same path. The head-office Shops screen gets 'Open a new shop'. broker-init converts the existing passwd file into the plugin's store with every hash intact - PBKDF2-SHA512 both sides - so the cutover re-claims no shop PC. Rehearsed locally: old logins keep working, isolation holds, the health probe works, and a PC claiming a shop opened through the API connects as that shop. run-local.sh now brings the broker up the same way. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01KGcjxF1cNLcuwc3DAPcnfj --- API.md | 29 +- CLAUDE.md | 45 ++- run-local.sh | 49 +-- server/cmd/behavision-server/brokerinit.go | 69 ++++ server/cmd/behavision-server/main.go | 17 +- server/cmd/behavision-server/provision.go | 15 +- server/internal/api/api.go | 14 + server/internal/api/fake_test.go | 27 ++ server/internal/api/handlers_sites.go | 107 ++++++ server/internal/api/sites_test.go | 110 ++++++ server/internal/api/types.go | 24 ++ server/internal/broker/dynsec.go | 325 ++++++++++++++++++ server/internal/broker/dynsec_live_test.go | 96 ++++++ server/internal/broker/migrate.go | 132 +++++++ server/internal/broker/migrate_test.go | 73 ++++ server/internal/provision/provision.go | 23 +- server/internal/store/api_sites.go | 115 +++++++ .../{index-BI5JLIeo.js => index-xQ5C-n-U.js} | 16 +- server/internal/web/dist/index.html | 2 +- web/src/api.js | 1 + web/src/views/Sites.jsx | 75 +++- 21 files changed, 1310 insertions(+), 54 deletions(-) create mode 100644 server/cmd/behavision-server/brokerinit.go create mode 100644 server/internal/api/handlers_sites.go create mode 100644 server/internal/api/sites_test.go create mode 100644 server/internal/broker/dynsec.go create mode 100644 server/internal/broker/dynsec_live_test.go create mode 100644 server/internal/broker/migrate.go create mode 100644 server/internal/broker/migrate_test.go create mode 100644 server/internal/store/api_sites.go rename server/internal/web/dist/assets/{index-BI5JLIeo.js => index-xQ5C-n-U.js} (70%) diff --git a/API.md b/API.md index 3e5f53e..a083d35 100644 --- a/API.md +++ b/API.md @@ -49,6 +49,7 @@ user; the tenant is always taken from the session and never from the request. | `DELETE /api/visitors/{id}` — erasure | manager | | `GET /api/sites` · `GET /api/sites/{site}/check` · `GET /api/cameras` · `GET /api/cameras/{id}/snapshot.jpg` · `GET /api/cameras/{id}/live` | authed | | `POST /api/sites/{site}/cameras` · `PATCH` / `DELETE /api/cameras/{id}` · `POST /api/cameras/{id}/check` | manager | +| `POST /api/sites` — open a shop | owner | | `POST /api/sites/{site}/enrolment-code` | manager | | `GET /api/team` | authed (tenant users only) | | `POST /api/team/members` · `POST /api/team/{id}/password` · `PATCH /api/team/{id}` · `/api/team/invitations*` | manager | @@ -164,9 +165,9 @@ PATCH /api/team/{id} { "role": "manager" } → promote PATCH /api/team/{id} { "active": false } → they have left; signs them out now ``` -The owner also sets the shop up from the same login — `POST -/api/sites/{site}/enrolment-code` for the shop PC, `POST -/api/sites/{site}/cameras` for cameras — see §8. +The owner also opens shops and sets them up from the same login — `POST +/api/sites` to open one, `POST /api/sites/{site}/enrolment-code` for its +shop PC, `POST /api/sites/{site}/cameras` for cameras — see §8. ### Tier 3 — the salesperson gets their mobile login @@ -718,6 +719,28 @@ unplugged PC. footfall lost because that queue overflowed. Non-zero `dropped` is a report that is wrong in a way the report itself cannot show. +### `POST /api/sites` — open a shop (owner) + +``` +{ "name": "TeNext Bengaluru", "slug": "bengaluru", "timezone": "Asia/Kolkata" } +→ 201 { "site_id": "…", "slug": "bengaluru", "name": "TeNext Bengaluru", + "timezone": "Asia/Kolkata", "broker_username": "tenext-retail.bengaluru" } +``` + +`slug` and `timezone` are optional: the slug is made from the name (lower-case, +digits and dashes, 3–32 characters) and the timezone defaults to Asia/Kolkata. +The slug is the shop PC's identity and an MQTT topic segment; it **cannot be +changed afterwards**. The server registers the shop's broker login with +Mosquitto in the same request, so the next step is simply +`POST /api/sites/{slug}/enrolment-code` for the PC. + +| status | code | meaning | +|---|---|---| +| 403 | `forbidden` | not the owner | +| 409 | `conflict` | a shop with that slug exists | +| 502 | `broker_unavailable` | the broker did not accept the login; **nothing was created** — try again | +| 503 | `broker_unavailable` / `no_encryption_key` | this server cannot create shops; contact support | + ### `GET /api/sites/{site}/check` Five ordered steps that answer *is this shop working*, assembled from what head diff --git a/CLAUDE.md b/CLAUDE.md index 29cd8fa..43628cc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1696,20 +1696,41 @@ The enrol response gained `client_slug` and `topic_prefix`, both **derived from the broker username** rather than looked up separately, so the agent's topic prefix and the broker's ACL are equal by construction. -### Still a command: creating the shop itself +### Opening a shop is an API call, and the broker learns of it in the same request -`provision site` prints a broker password that a human then has to add to -Mosquitto. So a tenant cannot open their second shop without us, and that is the -one remaining hole in self-service onboarding. Closing it needs a decision, not -code: +`POST /api/sites` (owner), and `provision site` behind the same code. This was +the last piece of onboarding that needed a shell: `provision site` printed a +broker password and a person typed it into Mosquitto's passwd file on the host +— which turned out to be mounted read-only in the container, so the first +attempt failed silently and the password had to be re-rolled. No tenant could +open a second branch without us. -- **the server manages Mosquitto's `passwd`/`acl` and reloads it** — possible - because they are co-located, and it couples the API to the broker's - filesystem; or -- **one broker user per CLIENT rather than per site** — then adding a shop needs - no broker change at all. Cross-tenant isolation is unchanged; what is given up - is that one of a customer's own PCs could publish as another of their sites. - Every deployed site would need re-provisioning. +Neither option recorded here before was taken. The server does not write the +broker's files, and there is still one broker user per site. Mosquitto 2.0's +**dynamic-security plugin** takes the same operations as commands on +`$CONTROL/dynamic-security/v1`, from a client holding the `admin` role; +`server/internal/broker` drives it over the server's own broker login. + +- **A role per site, with literal topics.** The 2.0 plugin does **not** + substitute `%u` in ACL topics (measured: the publish was denied), so + `site..` is created with the client and deleted with it. +- **Idempotent.** Re-running `EnsureSite` on an existing login sets the password + to the one the database holds and confirms the role. `addClientRole` on a + client that already has the role answers "Internal error", so the role is + checked with `getClient` rather than inferred from prose. +- **The row and the login are created together, or not at all.** If the broker + refuses, the just-created row is removed and the caller gets 502. A shop that + exists in the database and not on the broker is one whose PC enrols fine and + never delivers a visit — the silent-failure class this whole endpoint ends. +- **Its own connection**, not the ingest client's: that one has + `SetOrderMatters` and blocking handlers, and a provisioning call must neither + wait behind a slow visit nor delay one. +- **Cutover keeps every password.** `behavision-server broker-init` converts the + passwd file into the plugin's store: `$7$` lines are PBKDF2-SHA512 with a + salt and iteration count, which is exactly what the plugin stores, so no shop + PC re-claims and no credential changes hands. Rehearsed locally against a + file `mosquitto_passwd` wrote; `run-local.sh` now brings the broker up the + same way as production. ## Running it against the real office camera: four dead wires diff --git a/run-local.sh b/run-local.sh index 7e04e1b..fda916b 100755 --- a/run-local.sh +++ b/run-local.sh @@ -72,15 +72,34 @@ step "3b. Schema" "./$STATE/bv-server" migrate step "4. Mosquitto" -if [ ! -f "$STATE/mosquitto/mosquitto.conf" ]; then +# Dynamic security, not a passwd file - the same shape as production. The +# server registers each site's broker login itself over the control topic, so +# there is no per-site password to type here and nothing to restart. The store +# is seeded once with the server's own login as the plugin admin; after that +# the plugin owns the file. +mkdir -p "$STATE/mosquitto/data" +# Rewritten when it is the pre-plugin shape, so a checkout that ran the old +# script comes up in the new one rather than half of each. +if ! grep -q mosquitto_dynamic_security "$STATE/mosquitto/mosquitto.conf" 2>/dev/null; then + rm -f "$STATE/mosquitto/passwd" "$STATE/mosquitto/acl" + docker rm -f bv-mqtt >/dev/null 2>&1 || true cat > "$STATE/mosquitto/mosquitto.conf" < "$STATE/mosquitto/acl" - : > "$STATE/mosquitto/passwd" +fi +if [ ! -f "$STATE/mosquitto/data/dynamic-security.json" ]; then + : > "$STATE/mosquitto/passwd.seed" + docker run --rm -v "$PWD/$STATE/mosquitto:/m" eclipse-mosquitto:2 \ + mosquitto_passwd -b /m/passwd.seed behavision-server "$MQTT_PASSWORD" 2>/dev/null + "./$STATE/bv-server" broker-init -passwd "$STATE/mosquitto/passwd.seed" \ + -out "$STATE/mosquitto/data/dynamic-security.json" -backend-user behavision-server >/dev/null + rm -f "$STATE/mosquitto/passwd.seed" + # The plugin rewrites this file, so the broker's user (1883) must own it. + chmod 666 "$STATE/mosquitto/data/dynamic-security.json" fi # A container is reused only if its config mount still points HERE. The bind # source is baked in when the container is created, so one made while the @@ -98,6 +117,7 @@ if docker inspect bv-mqtt >/dev/null 2>&1; then fi docker inspect bv-mqtt >/dev/null 2>&1 || docker run -d --name bv-mqtt \ -p "${MQTT_PORT}:1883" -v "$MQTT_CONF:/mosquitto/config" \ + -v "$MQTT_CONF/data:/mosquitto/data" \ eclipse-mosquitto:2 >/dev/null docker start bv-mqtt >/dev/null 2>&1 || true @@ -114,13 +134,7 @@ if ! docker exec bv-mqtt sh -c 'exit 0' >/dev/null 2>&1; then docker logs --tail 5 bv-mqtt >&2 exit 1 fi -# stderr is NOT discarded here. A failure means the server cannot authenticate -# to its own broker, and the whole point of this script is that you find that -# out now rather than from an empty arrivals feed. -docker exec bv-mqtt mosquitto_passwd -b /mosquitto/config/passwd \ - behavision-server "$MQTT_PASSWORD" >/dev/null -docker restart bv-mqtt >/dev/null -echo " broker on ${MQTT_PORT}" +echo " broker on ${MQTT_PORT} (dynamic security)" step "5. First accounts" # Idempotent throughout: every provision subcommand upserts, so re-running this @@ -140,14 +154,9 @@ step "5. First accounts" # never readable again - so it is pushed into Mosquitto here in the same breath. # A shop PC enrolled on an earlier run therefore has to be claimed again, which # is the right trade locally and is why this is not how production works. -SITE_OUT=$("./$STATE/bv-server" provision site -client tenext-retail -slug chennai \ - -name "TeNext Chennai" -tz Asia/Kolkata) -BUSER=$(printf '%s' "$SITE_OUT" | sed -n "s/.*passwd \([^ ]*\) .*/\1/p") -BPASS=$(printf '%s' "$SITE_OUT" | sed -n "s/.*passwd [^ ]* '\(.*\)'.*/\1/p") -docker exec bv-mqtt mosquitto_passwd -b /mosquitto/config/passwd "$BUSER" "$BPASS" >/dev/null -grep -q "^user $BUSER$" "$STATE/mosquitto/acl" || \ - printf '\nuser %s\ntopic write bv/%s/#\n' "$BUSER" "$BUSER" >> "$STATE/mosquitto/acl" -docker restart bv-mqtt >/dev/null +MQTT_URL="tcp://127.0.0.1:${MQTT_PORT}" MQTT_USERNAME=behavision-server MQTT_PASSWORD="$MQTT_PASSWORD" \ + "./$STATE/bv-server" provision site -client tenext-retail -slug chennai \ + -name "TeNext Chennai" -tz Asia/Kolkata | sed 's/^/ /' printf ' platform admin admin@loyaly.ai / loyaly-platform-2026 (Companies only)\n' printf ' TeNext owner suriya@tenext.in / tenext-2026 (Shops, Live, Cameras, Customers, Reports)\n' diff --git a/server/cmd/behavision-server/brokerinit.go b/server/cmd/behavision-server/brokerinit.go new file mode 100644 index 0000000..a970690 --- /dev/null +++ b/server/cmd/behavision-server/brokerinit.go @@ -0,0 +1,69 @@ +package main + +import ( + "errors" + "flag" + "fmt" + "os" + + "github.com/loyaly/behavision-server/internal/broker" +) + +// broker-init converts Mosquitto's passwd file into the dynamic-security +// plugin's store, once, at cutover. After it the broker is driven over MQTT +// and the passwd and acl files are no longer read. +func runBrokerInit(args []string) error { + fs := flag.NewFlagSet("broker-init", flag.ExitOnError) + passwd := fs.String("passwd", "", "path to the mosquitto passwd file to convert") + out := fs.String("out", "", "where to write dynamic-security.json (must be writable by the broker)") + backend := fs.String("backend-user", "behavision-backend", "the server's own broker username; becomes the plugin admin") + health := fs.String("health-user", "health", "the healthcheck username") + fs.Usage = func() { + fmt.Fprintf(os.Stderr, `usage: behavision-server broker-init -passwd FILE -out FILE + +Converts a mosquitto_passwd file into the dynamic-security plugin's store, +keeping every password hash exactly as it is, so no shop PC has to be +re-claimed. Then in mosquitto.conf replace password_file/acl_file with: + + per_listener_settings false + plugin /usr/lib/mosquitto_dynamic_security.so + plugin_opt_config_file /mosquitto/data/dynamic-security.json + +and restart the broker. From then on 'provision site' and POST /api/sites +register a shop's login themselves. +`) + fs.PrintDefaults() + } + if err := fs.Parse(args); err != nil { + return err + } + if *passwd == "" || *out == "" { + fs.Usage() + return errors.New("-passwd and -out are required") + } + f, err := os.Open(*passwd) + if err != nil { + return err + } + defer f.Close() + st, err := broker.FromPasswd(f, *backend, *health) + if err != nil { + return err + } + if _, err := os.Stat(*out); err == nil { + return fmt.Errorf("%s already exists - refusing to overwrite a live store", *out) + } + w, err := os.OpenFile(*out, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o600) + if err != nil { + return err + } + defer w.Close() + if err := st.Encode(w); err != nil { + return err + } + fmt.Printf("wrote %s: %d clients, %d roles\n", *out, len(st.Clients), len(st.Roles)) + for _, c := range st.Clients { + fmt.Printf(" %-28s %s\n", c.Username, c.Roles[0].Rolename) + } + return nil +} diff --git a/server/cmd/behavision-server/main.go b/server/cmd/behavision-server/main.go index ee02f20..dde0bc0 100644 --- a/server/cmd/behavision-server/main.go +++ b/server/cmd/behavision-server/main.go @@ -25,11 +25,12 @@ import ( "github.com/loyaly/behavision-server/internal/api" "github.com/loyaly/behavision-server/internal/assistant" "github.com/loyaly/behavision-server/internal/blob" + "github.com/loyaly/behavision-server/internal/broker" "github.com/loyaly/behavision-server/internal/ingest" "github.com/loyaly/behavision-server/internal/migrate" - "github.com/loyaly/behavision-server/internal/web" "github.com/loyaly/behavision-server/internal/secret" "github.com/loyaly/behavision-server/internal/store" + "github.com/loyaly/behavision-server/internal/web" "github.com/loyaly/behavision-server/migrations" ) @@ -46,6 +47,13 @@ func main() { } return } + if len(os.Args) > 1 && os.Args[1] == "broker-init" { + if err := runBrokerInit(os.Args[2:]); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + return + } if len(os.Args) > 1 && os.Args[1] == "migrate" { if err := runMigrate(os.Args[2:]); err != nil { fmt.Fprintln(os.Stderr, err) @@ -194,8 +202,11 @@ func run() error { apiSrv := &api.Server{ Store: st, Log: logger, - Blob: objectStore(ctx, logger), - Hub: hub, + // Opening a shop registers its broker login at the same moment, over the + // same broker credential the ingest side already holds. + Broker: broker.New(brokerURL, brokerUser, brokerPass, logger), + Blob: objectStore(ctx, logger), + Hub: hub, Bootstrap: api.BootstrapConfig{ // What an enrolling PC is told to connect to. From the server's own // environment, never from the request: an agent asking where to diff --git a/server/cmd/behavision-server/provision.go b/server/cmd/behavision-server/provision.go index 255fd8e..a69ed24 100644 --- a/server/cmd/behavision-server/provision.go +++ b/server/cmd/behavision-server/provision.go @@ -11,6 +11,7 @@ import ( "github.com/jackc/pgx/v5/pgxpool" + "github.com/loyaly/behavision-server/internal/broker" "github.com/loyaly/behavision-server/internal/provision" "github.com/loyaly/behavision-server/internal/secret" ) @@ -43,6 +44,14 @@ func runProvision(args []string) error { box, boxErr := secret.FromEnv("BEHAVISION_SECRET_KEY") p := &provision.Provisioner{Pool: pool, Secrets: box} + // The broker, so a new site's login is registered here and now instead of + // printed for somebody to type into a password file. Same variables the + // server itself connects with. + if u := os.Getenv("MQTT_URL"); u != "" && os.Getenv("MQTT_USERNAME") != "" { + dyn := broker.New(u, os.Getenv("MQTT_USERNAME"), os.Getenv("MQTT_PASSWORD"), nil) + defer dyn.Close() + p.Broker = dyn + } switch args[0] { case "client": @@ -82,12 +91,16 @@ func runProvision(args []string) error { return err } fmt.Printf("site created: %s\n", res.SiteID) + if res.BrokerRegistered { + fmt.Printf("broker login %s registered - this site can publish now.\n", res.Username) + return nil + } fmt.Printf("\nAdd this broker user to Mosquitto, then this site can publish:\n\n") fmt.Printf(" mosquitto_passwd -b /mosquitto/config/passwd %s '%s'\n\n", res.Username, res.Password) // The broker keeps a hash; we keep it sealed. Neither side can show it // again, which is why it is printed here in full. - fmt.Printf("The password is stored encrypted and handed out only at "+ + fmt.Printf("The password is stored encrypted and handed out only at " + "enrolment.\nIt is not recoverable from the logs. Copy it now.\n") return nil diff --git a/server/internal/api/api.go b/server/internal/api/api.go index 96d3ca9..d40d42b 100644 --- a/server/internal/api/api.go +++ b/server/internal/api/api.go @@ -36,6 +36,15 @@ import ( type Store interface { // --- identity --- UserByEmail(ctx context.Context, email string) (UserRecord, error) + + // --- shops --- + // CreateSite writes the shop and its sealed broker password in one + // transaction and returns the plaintext once, for the broker registration + // that must follow. DeleteNewSite is the compensation when that + // registration fails: a shop whose PC can enrol but never publish is the + // silent failure this whole endpoint exists to end. + CreateSite(ctx context.Context, clientID, slug, name, tz string) (NewSite, error) + DeleteNewSite(ctx context.Context, clientID, siteID string) error TouchUserLogin(ctx context.Context, userID string) error CreateSession(ctx context.Context, s NewSession) error SessionByAccess(ctx context.Context, hash []byte) (auth.Principal, time.Time, error) @@ -168,6 +177,10 @@ type Server struct { // business questions the screens ask. Nil means this deployment has no // API key, which is supported: the UI hides the panel. Assistant Assistant + // Broker registers a shop's login with Mosquitto at the moment the shop is + // created. Nil means this deployment cannot create shops through the API + // and says so, rather than creating one that can never publish. + Broker SiteBroker // Live relays camera frames from a shop PC to whoever is watching, on // demand. Created on first use. Live *LiveHub @@ -264,6 +277,7 @@ func (s *Server) Routes() *http.ServeMux { mux.HandleFunc("GET /api/reports/footfall", s.authed(s.handleFootfall)) mux.HandleFunc("GET /api/reports/conversion", s.authed(s.handleConversion)) mux.HandleFunc("GET /api/sites", s.authed(s.handleSites)) + mux.HandleFunc("POST /api/sites", s.authed(s.handleCreateSite)) // Cameras, onboarded from head office. The shop PC still does the // connecting - it is the only thing on the camera's network - so these diff --git a/server/internal/api/fake_test.go b/server/internal/api/fake_test.go index e219528..93455e9 100644 --- a/server/internal/api/fake_test.go +++ b/server/internal/api/fake_test.go @@ -6,6 +6,7 @@ import ( "encoding/hex" "errors" "fmt" + "github.com/jackc/pgx/v5/pgconn" "net/http" "strings" "sync" @@ -261,6 +262,32 @@ func (f *fakeStore) Conversion(_ context.Context, q ReportQuery) (SalesReport, e return f.sales, nil } +func (f *fakeStore) CreateSite(_ context.Context, clientID, slug, name, tz string) (NewSite, error) { + f.mu.Lock() + defer f.mu.Unlock() + for _, s := range f.sites { + if s.Slug == slug { + return NewSite{}, &pgconn.PgError{Code: "23505"} + } + } + id := "site-" + slug + f.sites = append(f.sites, SiteHealth{SiteID: id, Slug: slug, Name: name, Timezone: tz}) + return NewSite{SiteID: id, Slug: slug, Name: name, Timezone: tz, Username: "acme." + slug, Password: "pw-" + slug}, nil +} + +func (f *fakeStore) DeleteNewSite(_ context.Context, _ string, siteID string) error { + f.mu.Lock() + defer f.mu.Unlock() + kept := f.sites[:0] + for _, s := range f.sites { + if s.SiteID != siteID { + kept = append(kept, s) + } + } + f.sites = kept + return nil +} + func (f *fakeStore) SiteHealth(_ context.Context, _ string) ([]SiteHealth, error) { return f.sites, nil } diff --git a/server/internal/api/handlers_sites.go b/server/internal/api/handlers_sites.go new file mode 100644 index 0000000..1551d52 --- /dev/null +++ b/server/internal/api/handlers_sites.go @@ -0,0 +1,107 @@ +package api + +import ( + "context" + "errors" + "net/http" + "regexp" + "strings" + "time" + + "github.com/jackc/pgx/v5/pgconn" +) + +// SiteBroker is the broker-side half of creating a shop. It is the +// internal/broker package's interface, redeclared here so this package does +// not import a paho dependency for the sake of one method. +type SiteBroker interface { + EnsureSite(ctx context.Context, username, password string) error + DeleteSite(ctx context.Context, username string) error +} + +// Same rule the database enforces (sites_slug_format), checked here so the +// caller gets a sentence instead of a constraint name. +var slugRe = regexp.MustCompile(`^[a-z0-9][a-z0-9-]{1,30}[a-z0-9]$`) + +// POST /api/sites - an owner opens a shop. +// +// Until this existed a shop was `provision site` on the server's command line +// followed by a hand edit of the broker's password file. That made every new +// branch a support ticket, and it was the last piece of onboarding that could +// not be done from the product. The row and the broker login are created +// together here; if the broker will not take the login, the row is removed +// again and the caller is told, because a shop that exists in the database and +// not on the broker is one whose PC enrols fine and never delivers a visit. +func (s *Server) handleCreateSite(w http.ResponseWriter, r *http.Request) { + p := PrincipalFrom(r.Context()) + // Owner, not manager: a shop is a billing and tenancy object, not a + // setting. Managers can set up the PC and cameras once it exists. + if p.Role != "owner" || p.ClientID == "" { + writeErr(w, http.StatusForbidden, "forbidden", "Only the owner can open a new shop.") + return + } + if s.Broker == nil { + writeErr(w, http.StatusServiceUnavailable, "broker_unavailable", + "This server is not connected to a broker that can register shops. Contact support.") + return + } + var in NewSiteInput + if err := decode(w, r, &in); err != nil { + badRequest(w, err.Error()) + return + } + in.Name = clip(trim(in.Name), 120) + if in.Name == "" { + badRequest(w, "Give the shop a name.") + return + } + in.Slug = slugify(in.Slug) + if in.Slug == "" { + in.Slug = slugify(in.Name) + } + if !slugRe.MatchString(in.Slug) { + badRequest(w, "The short name must be 3-32 characters: lower-case letters, digits and dashes.") + return + } + tz := strings.TrimSpace(in.Timezone) + if tz == "" { + tz = "Asia/Kolkata" + } + if _, err := time.LoadLocation(tz); err != nil { + badRequest(w, "Unknown timezone. Use an IANA name such as Asia/Kolkata.") + return + } + + site, err := s.Store.CreateSite(r.Context(), p.ClientID, in.Slug, in.Name, tz) + if err != nil { + var pgErr *pgconn.PgError + if errors.As(err, &pgErr) && pgErr.Code == "23505" { + writeErr(w, http.StatusConflict, "conflict", "A shop with that short name already exists.") + return + } + if errors.Is(err, ErrNoSecrets) { + writeErr(w, http.StatusServiceUnavailable, "no_encryption_key", + "This server has no encryption key, so a shop's broker password cannot be stored. Contact support.") + return + } + s.serverError(w, "create site", err) + return + } + + if err := s.Broker.EnsureSite(r.Context(), site.Username, site.Password); err != nil { + s.logf("create site %s: broker registration failed, removing the row: %v", site.Slug, err) + if derr := s.Store.DeleteNewSite(r.Context(), p.ClientID, site.SiteID); derr != nil { + s.logf("create site %s: could not remove the row after broker failure: %v", site.Slug, derr) + } + writeErr(w, http.StatusBadGateway, "broker_unavailable", + "The broker did not accept the new shop, so it was not created. Try again in a moment; if it keeps failing, contact support.") + return + } + + s.Store.Audit(r.Context(), AuditEntry{ + ClientID: p.ClientID, ActorID: p.UserID, ActorKind: "user", + Action: "site.created", Entity: "site", EntityID: site.SiteID, + Detail: map[string]any{"slug": site.Slug, "name": site.Name, "timezone": site.Timezone}, + }) + writeJSON(w, http.StatusCreated, site) +} diff --git a/server/internal/api/sites_test.go b/server/internal/api/sites_test.go new file mode 100644 index 0000000..fbcd77c --- /dev/null +++ b/server/internal/api/sites_test.go @@ -0,0 +1,110 @@ +package api + +import ( + "context" + "encoding/json" + "errors" + "net/http" + "testing" +) + +// fakeBroker records what the server asked the broker to do. +type fakeBroker struct { + ensured map[string]string + deleted []string + fail error +} + +func (b *fakeBroker) EnsureSite(_ context.Context, user, pass string) error { + if b.fail != nil { + return b.fail + } + if b.ensured == nil { + b.ensured = map[string]string{} + } + b.ensured[user] = pass + return nil +} +func (b *fakeBroker) DeleteSite(_ context.Context, user string) error { + b.deleted = append(b.deleted, user) + return nil +} + +func ownerSession(t *testing.T, s *Server, fs *fakeStore) Session { + t.Helper() + fs.addUser("owner@acme.com", "correct horse battery", UserRecord{ + ID: "u-owner", ClientID: "client-acme", Role: "owner", Active: true, + }) + return login(t, s, "owner@acme.com", "correct horse battery") +} + +func TestAnOwnerOpensAShopAndTheBrokerLearnsOfIt(t *testing.T) { + s, fs := newServer(t) + b := &fakeBroker{} + s.Broker = b + sess := ownerSession(t, s, fs) + + rec := do(t, s, "POST", "/api/sites", sess.Token, map[string]any{"name": "Acme Bengaluru!"}) + if rec.Code != http.StatusCreated { + t.Fatalf("got %d: %s", rec.Code, rec.Body.String()) + } + var out map[string]any + _ = json.Unmarshal(rec.Body.Bytes(), &out) + if out["slug"] != "acme-bengaluru" { + t.Errorf("slug not derived from the name: %v", out["slug"]) + } + if _, leaked := out["password"]; leaked { + t.Fatal("the broker password was serialised") + } + if b.ensured["acme.acme-bengaluru"] == "" { + t.Fatalf("broker was not told about the shop: %+v", b.ensured) + } + if len(fs.sites) != 1 { + t.Fatalf("expected one site, have %d", len(fs.sites)) + } +} + +func TestABrokerFailureLeavesNoHalfMadeShop(t *testing.T) { + s, fs := newServer(t) + s.Broker = &fakeBroker{fail: errors.New("no answer on the control topic")} + sess := ownerSession(t, s, fs) + + rec := do(t, s, "POST", "/api/sites", sess.Token, map[string]any{"name": "Ghost"}) + if rec.Code != http.StatusBadGateway { + t.Fatalf("got %d: %s", rec.Code, rec.Body.String()) + } + if len(fs.sites) != 0 { + t.Fatalf("a shop the broker never accepted was kept: %+v", fs.sites) + } +} + +func TestAManagerCannotOpenAShop(t *testing.T) { + s, fs := newServer(t) + s.Broker = &fakeBroker{} + seedUser(fs) + sess := login(t, s, "manager@acme.com", "correct horse battery") + rec := do(t, s, "POST", "/api/sites", sess.Token, map[string]any{"name": "Nope"}) + if rec.Code != http.StatusForbidden { + t.Fatalf("manager opened a shop: %d", rec.Code) + } +} + +func TestADuplicateShortNameIsAConflict(t *testing.T) { + s, fs := newServer(t) + s.Broker = &fakeBroker{} + seedSite(fs) + sess := ownerSession(t, s, fs) + rec := do(t, s, "POST", "/api/sites", sess.Token, map[string]any{"name": "Again", "slug": "chennai"}) + if rec.Code != http.StatusConflict { + t.Fatalf("got %d: %s", rec.Code, rec.Body.String()) + } +} + +func TestNoBrokerConfiguredSaysSo(t *testing.T) { + s, fs := newServer(t) + sess := ownerSession(t, s, fs) + rec := do(t, s, "POST", "/api/sites", sess.Token, map[string]any{"name": "Shop"}) + if rec.Code != http.StatusServiceUnavailable { + t.Fatalf("got %d: %s", rec.Code, rec.Body.String()) + } +} diff --git a/server/internal/api/types.go b/server/internal/api/types.go index c5ed30e..ab63f4c 100644 --- a/server/internal/api/types.go +++ b/server/internal/api/types.go @@ -160,6 +160,30 @@ type PurchaseInput struct { // SiteHealth is what the dashboard needs to distinguish "no customers" from // "this shop's PC has been unplugged for a week" - two identical rows of zeroes // with completely different responses. +// NewSiteInput is what an owner types to open a shop. The slug is derived +// from the name when absent, because it becomes the shop PC's identity and +// an MQTT topic segment, and a person asked to invent one invents a bad one. +type NewSiteInput struct { + Name string `json:"name"` + Slug string `json:"slug,omitempty"` + Timezone string `json:"timezone,omitempty"` +} + +// NewSite is the created shop plus the broker login the server registered for +// it. The password is sealed in the database and handed to a shop PC at +// enrolment; it is NOT in the API response, because nobody needs to see it - +// the enrolment code is the credential a person handles. +type NewSite struct { + SiteID string `json:"site_id"` + Slug string `json:"slug"` + Name string `json:"name"` + Timezone string `json:"timezone"` + // Username is the broker login; Password is returned to the caller in + // Go only, for the broker registration, and never serialised. + Username string `json:"broker_username"` + Password string `json:"-"` +} + type SiteHealth struct { SiteID string `json:"site_id"` Slug string `json:"slug"` diff --git a/server/internal/broker/dynsec.go b/server/internal/broker/dynsec.go new file mode 100644 index 0000000..69e4c98 --- /dev/null +++ b/server/internal/broker/dynsec.go @@ -0,0 +1,325 @@ +// Package broker creates and removes a site's broker login at runtime. +// +// Until this existed a shop was created in two places by two mechanisms: +// `provision site` wrote the row and printed a password, and a person then +// typed that password into Mosquitto's passwd file on the host and reloaded +// the broker. Nothing else in the product needed a shell, so this one step +// was what stopped a tenant opening a second branch on their own - and it was +// fragile even for us: the file was mounted read-only in the container, the +// first attempt failed, and the password had to be re-rolled. +// +// Mosquitto 2.0's dynamic-security plugin takes the same operations as +// commands on a control topic, from a client that holds the `admin` role. +// The server already holds a broker login; this gives it that role and uses +// it. No file, no reload, no docker socket, and the per-site credential +// model is unchanged - one username per shop, topics only under its own +// prefix. +// +// Isolation is a ROLE PER SITE with literal topics, not one role with `%u`: +// the 2.0 plugin does not substitute `%u` in ACL topics (measured - the +// publish was denied). The role is created and deleted with the client, so +// there is still exactly one thing to get right and it is done in one place. +package broker + +import ( + "context" + "crypto/rand" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "log" + "strings" + "sync" + "time" + + paho "github.com/eclipse/paho.mqtt.golang" +) + +const ( + controlTopic = "$CONTROL/dynamic-security/v1" + responseTopic = "$CONTROL/dynamic-security/v1/response" + // A control round trip on a healthy broker is milliseconds; this is a + // bound on a broker that is up but not answering control commands, which + // is what a broker WITHOUT the plugin looks like. + commandTimeout = 8 * time.Second + connectTimeout = 10 * time.Second +) + +// ErrUnavailable is returned when the broker cannot be reached or does not +// answer control commands. Callers turn it into a 502/503, never a 500: the +// operator's next step is to look at the broker, not at the server. +var ErrUnavailable = errors.New("broker: dynamic security not available") + +// SiteBroker is what the API and the provisioner depend on. A fake satisfies +// it in tests; Dynsec satisfies it in production. +type SiteBroker interface { + // EnsureSite creates the broker login for one site, or resets its password + // if it already exists. Idempotent: safe to run again after any failure. + EnsureSite(ctx context.Context, username, password string) error + // DeleteSite removes the login and its role. Missing is not an error. + DeleteSite(ctx context.Context, username string) error +} + +// Dynsec drives the plugin over its own connection - not the ingest client's. +// That one has SetOrderMatters and blocking handlers, and a provisioning call +// must neither wait behind a slow visit nor delay one. +type Dynsec struct { + url, user, pass string + log *log.Logger + + mu sync.Mutex + client paho.Client + pending map[string]chan []response // keyed by batch id +} + +func New(url, user, pass string, logger *log.Logger) *Dynsec { + if logger == nil { + logger = log.Default() + } + return &Dynsec{url: url, user: user, pass: pass, log: logger, pending: map[string]chan []response{}} +} + +type response struct { + Command string `json:"command"` + Error string `json:"error,omitempty"` + CorrelationData string `json:"correlationData,omitempty"` + Data json.RawMessage `json:"data,omitempty"` +} + +// SiteTopics are exactly what a shop PC may do: publish its own visits, +// heartbeats and status, and read its own commands. This mirrors the acl +// file the broker used to be configured with, rule for rule. +func siteACLs(username string) []map[string]any { + prefix := "bv/" + username + acl := func(kind, topic string) map[string]any { + return map[string]any{"acltype": kind, "topic": topic, "allow": true, "priority": 0} + } + return []map[string]any{ + acl("publishClientSend", prefix+"/visit"), + acl("publishClientSend", prefix+"/heartbeat"), + acl("publishClientSend", prefix+"/status"), + acl("publishClientReceive", prefix+"/cmd/#"), + acl("subscribePattern", prefix+"/cmd/#"), + } +} + +// RoleName is the per-site role. Exported so the migration can name it the +// same way. +func RoleName(username string) string { return "site." + username } + +func (d *Dynsec) EnsureSite(ctx context.Context, username, password string) error { + if strings.TrimSpace(username) == "" || password == "" { + return errors.New("broker: username and password are required") + } + role := RoleName(username) + + cmds := []map[string]any{{"command": "createRole", "rolename": role}} + for _, a := range siteACLs(username) { + c := map[string]any{"command": "addRoleACL", "rolename": role} + for k, v := range a { + c[k] = v + } + cmds = append(cmds, c) + } + cmds = append(cmds, map[string]any{ + "command": "createClient", "username": username, "password": password, + "roles": []map[string]any{{"rolename": role}}, + }) + res, err := d.run(ctx, cmds) + if err != nil { + return err + } + + clientExisted := false + for _, r := range res { + switch { + case r.Error == "": + case strings.Contains(r.Error, "already exists") && r.Command != "createClient": + // A re-run after a partial failure. Fine. + case r.Command == "createClient" && strings.Contains(r.Error, "already exists"): + clientExisted = true + default: + return fmt.Errorf("broker: %s: %s", r.Command, r.Error) + } + } + if !clientExisted { + return nil + } + + // The login exists from an earlier run: make its password THIS one - the + // database holds this one and the enrolment hands it out - and make sure + // it carries the role. addClientRole on a client that already has it + // answers "Internal error", so check first rather than guess from prose. + res, err = d.run(ctx, []map[string]any{ + {"command": "setClientPassword", "username": username, "password": password}, + {"command": "getClient", "username": username}, + }) + if err != nil { + return err + } + hasRole := false + for _, r := range res { + if r.Error != "" { + return fmt.Errorf("broker: %s: %s", r.Command, r.Error) + } + if r.Command == "getClient" { + var got struct { + Client struct { + Roles []struct{ Rolename string } `json:"roles"` + } `json:"client"` + } + _ = json.Unmarshal(r.Data, &got) + for _, rr := range got.Client.Roles { + if rr.Rolename == role { + hasRole = true + } + } + } + } + if hasRole { + return nil + } + res, err = d.run(ctx, []map[string]any{{"command": "addClientRole", "username": username, "rolename": role}}) + if err != nil { + return err + } + if res[0].Error != "" { + return fmt.Errorf("broker: addClientRole: %s", res[0].Error) + } + return nil +} + +func (d *Dynsec) DeleteSite(ctx context.Context, username string) error { + res, err := d.run(ctx, []map[string]any{ + {"command": "deleteClient", "username": username}, + {"command": "deleteRole", "rolename": RoleName(username)}, + }) + if err != nil { + return err + } + for _, r := range res { + if r.Error != "" && !strings.Contains(r.Error, "not found") { + return fmt.Errorf("broker: %s: %s", r.Command, r.Error) + } + } + return nil +} + +// Close drops the control connection. Safe when never connected. +func (d *Dynsec) Close() { + d.mu.Lock() + c := d.client + d.client = nil + d.mu.Unlock() + if c != nil && c.IsConnected() { + c.Disconnect(250) + } +} + +// run sends one batch and waits for its one response message. Every command +// carries the batch id as correlationData, and the plugin echoes it, so a +// response for somebody else's batch - two servers, or a retry - is never +// mistaken for ours. +func (d *Dynsec) run(ctx context.Context, cmds []map[string]any) ([]response, error) { + if err := d.connect(ctx); err != nil { + return nil, err + } + id := newID() + for _, c := range cmds { + c["correlationData"] = id + } + body, err := json.Marshal(map[string]any{"commands": cmds}) + if err != nil { + return nil, err + } + ch := make(chan []response, 1) + d.mu.Lock() + d.pending[id] = ch + client := d.client + d.mu.Unlock() + defer func() { + d.mu.Lock() + delete(d.pending, id) + d.mu.Unlock() + }() + + tok := client.Publish(controlTopic, 1, false, body) + if !tok.WaitTimeout(commandTimeout) { + return nil, fmt.Errorf("%w: publish timed out", ErrUnavailable) + } + if tok.Error() != nil { + return nil, fmt.Errorf("%w: %v", ErrUnavailable, tok.Error()) + } + select { + case res := <-ch: + return res, nil + case <-time.After(commandTimeout): + return nil, fmt.Errorf("%w: no answer on %s - is the dynamic-security plugin enabled and does %q hold the admin role?", ErrUnavailable, responseTopic, d.user) + case <-ctx.Done(): + return nil, ctx.Err() + } +} + +func (d *Dynsec) onResponse(_ paho.Client, m paho.Message) { + var env struct { + Responses []response `json:"responses"` + } + if err := json.Unmarshal(m.Payload(), &env); err != nil || len(env.Responses) == 0 { + return + } + id := env.Responses[0].CorrelationData + d.mu.Lock() + ch, ok := d.pending[id] + d.mu.Unlock() + if ok { + select { + case ch <- env.Responses: + default: + } + } +} + +func (d *Dynsec) connect(ctx context.Context) error { + d.mu.Lock() + defer d.mu.Unlock() + if d.client != nil && d.client.IsConnectionOpen() { + return nil + } + opts := paho.NewClientOptions(). + AddBroker(d.url). + SetClientID(fmt.Sprintf("behavision-dynsec-%s", newID()[:8])). + SetUsername(d.user). + SetPassword(d.pass). + SetAutoReconnect(true). + SetCleanSession(true). + SetKeepAlive(30 * time.Second). + SetConnectTimeout(connectTimeout) + opts.OnConnect = func(c paho.Client) { + // Re-subscribed on every (re)connect: clean session keeps nothing. + c.Subscribe(responseTopic, 1, d.onResponse) + } + c := paho.NewClient(opts) + tok := c.Connect() + if !tok.WaitTimeout(connectTimeout) { + return fmt.Errorf("%w: connect to %s timed out", ErrUnavailable, d.url) + } + if tok.Error() != nil { + return fmt.Errorf("%w: %v", ErrUnavailable, tok.Error()) + } + // The subscription must be in place before the first command is sent, or + // its answer is published to nobody. + st := c.Subscribe(responseTopic, 1, d.onResponse) + if !st.WaitTimeout(connectTimeout) || st.Error() != nil { + c.Disconnect(100) + return fmt.Errorf("%w: cannot subscribe to %s (does %q hold the admin role?)", ErrUnavailable, responseTopic, d.user) + } + d.client = c + return nil +} + +func newID() string { + var b [12]byte + _, _ = rand.Read(b[:]) + return hex.EncodeToString(b[:]) +} diff --git a/server/internal/broker/dynsec_live_test.go b/server/internal/broker/dynsec_live_test.go new file mode 100644 index 0000000..fb54147 --- /dev/null +++ b/server/internal/broker/dynsec_live_test.go @@ -0,0 +1,96 @@ +package broker + +import ( + "context" + "os" + "testing" + "time" + + paho "github.com/eclipse/paho.mqtt.golang" +) + +// Runs against a real Mosquitto with the dynamic-security plugin, because a +// fake broker would only prove the JSON matches what I believe the plugin +// wants. Gated on the environment like the store's live tests: +// +// DYNSEC_TEST_URL=tcp://127.0.0.1:51884 DYNSEC_TEST_USER=behavision-backend \ +// DYNSEC_TEST_PASS=pw-backend go test ./internal/broker -run Live -v +func TestLiveEnsureAndDeleteSite(t *testing.T) { + url, user, pass := os.Getenv("DYNSEC_TEST_URL"), os.Getenv("DYNSEC_TEST_USER"), os.Getenv("DYNSEC_TEST_PASS") + if url == "" { + t.Skip("DYNSEC_TEST_URL not set") + } + d := New(url, user, pass, nil) + defer d.Close() + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + site := "livetest.shop1" + if err := d.EnsureSite(ctx, site, "first-pw"); err != nil { + t.Fatalf("EnsureSite: %v", err) + } + // Idempotent, and the password becomes the one given THIS time. + if err := d.EnsureSite(ctx, site, "second-pw"); err != nil { + t.Fatalf("EnsureSite again: %v", err) + } + if err := canConnect(url, site, "first-pw"); err == nil { + t.Fatalf("old password still accepted after re-ensure") + } + if err := canConnect(url, site, "second-pw"); err != nil { + t.Fatalf("new password refused: %v", err) + } + // Own topic allowed, another site's refused. A denied publish at QoS 1 + // still gets a PUBACK, so this is observed through the backend's inbox. + got := make(chan string, 4) + backend := connect(t, url, user, pass) + defer backend.Disconnect(100) + backend.Subscribe("bv/#", 1, func(_ paho.Client, m paho.Message) { got <- m.Topic() }).Wait() + shop := connect(t, url, site, "second-pw") + defer shop.Disconnect(100) + shop.Publish("bv/other.shop/visit", 1, false, "leak").Wait() + shop.Publish("bv/"+site+"/visit", 1, false, "ok").Wait() + select { + case topic := <-got: + if topic != "bv/"+site+"/visit" { + t.Fatalf("first delivered topic was %s", topic) + } + case <-time.After(5 * time.Second): + t.Fatal("own-topic publish never arrived") + } + select { + case topic := <-got: + t.Fatalf("unexpected second delivery: %s", topic) + case <-time.After(1500 * time.Millisecond): + } + + if err := d.DeleteSite(ctx, site); err != nil { + t.Fatalf("DeleteSite: %v", err) + } + if err := d.DeleteSite(ctx, site); err != nil { + t.Fatalf("DeleteSite twice: %v", err) + } + if err := canConnect(url, site, "second-pw"); err == nil { + t.Fatal("deleted site can still connect") + } +} + +func connect(t *testing.T, url, user, pass string) paho.Client { + t.Helper() + c := paho.NewClient(paho.NewClientOptions().AddBroker(url).SetUsername(user).SetPassword(pass).SetConnectTimeout(5 * time.Second)) + tok := c.Connect() + tok.Wait() + if tok.Error() != nil { + t.Fatalf("connect as %s: %v", user, tok.Error()) + } + return c +} + +func canConnect(url, user, pass string) error { + c := paho.NewClient(paho.NewClientOptions().AddBroker(url).SetUsername(user).SetPassword(pass).SetConnectTimeout(5 * time.Second)) + tok := c.Connect() + tok.Wait() + if tok.Error() == nil { + c.Disconnect(50) + } + return tok.Error() +} diff --git a/server/internal/broker/migrate.go b/server/internal/broker/migrate.go new file mode 100644 index 0000000..03582d1 --- /dev/null +++ b/server/internal/broker/migrate.go @@ -0,0 +1,132 @@ +package broker + +import ( + "bufio" + "encoding/json" + "fmt" + "io" + "strconv" + "strings" +) + +// Store is the plugin's on-disk shape - the part of it this code writes. +type Store struct { + Clients []Client `json:"clients"` + Roles []Role `json:"roles"` + DefaultACLAccess map[string]bool `json:"defaultACLAccess"` +} + +type Client struct { + Username string `json:"username"` + TextName string `json:"textName,omitempty"` + Password string `json:"password"` + Salt string `json:"salt"` + Iterations int `json:"iterations"` + Roles []RoleRef `json:"roles"` +} + +type RoleRef struct { + Rolename string `json:"rolename"` +} + +type Role struct { + Rolename string `json:"rolename"` + ACLs []ACL `json:"acls"` +} + +type ACL struct { + ACLType string `json:"acltype"` + Topic string `json:"topic"` + Allow bool `json:"allow"` + Priority int `json:"priority"` +} + +// FromPasswd converts a mosquitto_passwd file into the plugin's store, +// keeping every password exactly as it is. +// +// This is the cutover for a broker that already has sites: the hashes in the +// passwd file are PBKDF2-SHA512 ($7$$$, base64), +// which is the same thing the plugin stores as password/salt/iterations - so +// no shop PC has to be re-claimed and no credential changes hands. The roles +// reproduce the acl file rule for rule: the backend reads everything and +// drives the plugin, the health probe reads uptime, and every other user is a +// site that may write under its own prefix and read its own commands. +func FromPasswd(r io.Reader, backendUser, healthUser string) (*Store, error) { + st := &Store{ + DefaultACLAccess: map[string]bool{ + "publishClientSend": false, "publishClientReceive": false, + "subscribe": false, "unsubscribe": true, + }, + } + st.Roles = append(st.Roles, + Role{Rolename: "admin", ACLs: []ACL{ + {"publishClientSend", "$CONTROL/dynamic-security/#", true, 0}, + {"publishClientReceive", "$CONTROL/dynamic-security/#", true, 0}, + {"subscribePattern", "$CONTROL/dynamic-security/#", true, 0}, + }}, + Role{Rolename: "backend", ACLs: []ACL{ + {"publishClientSend", "bv/#", true, 0}, + {"publishClientReceive", "bv/#", true, 0}, + {"subscribePattern", "bv/#", true, 0}, + {"publishClientReceive", "$SYS/#", true, 0}, + {"subscribePattern", "$SYS/#", true, 0}, + }}, + Role{Rolename: "health", ACLs: []ACL{ + {"publishClientReceive", "$SYS/broker/uptime", true, 0}, + {"subscribePattern", "$SYS/broker/uptime", true, 0}, + }}, + ) + + sc := bufio.NewScanner(r) + line := 0 + for sc.Scan() { + line++ + text := strings.TrimSpace(sc.Text()) + if text == "" || strings.HasPrefix(text, "#") { + continue + } + user, hash, ok := strings.Cut(text, ":") + if !ok { + return nil, fmt.Errorf("passwd line %d: no ':'", line) + } + parts := strings.Split(hash, "$") + // "", "7", iterations, salt, digest + if len(parts) != 5 || parts[1] != "7" { + return nil, fmt.Errorf("passwd line %d (%s): not a $7$ PBKDF2 hash; re-set that password with mosquitto_passwd first", line, user) + } + iters, err := strconv.Atoi(parts[2]) + if err != nil { + return nil, fmt.Errorf("passwd line %d (%s): iterations %q", line, user, parts[2]) + } + c := Client{Username: user, Password: parts[4], Salt: parts[3], Iterations: iters} + switch user { + case backendUser: + c.TextName = "Behavision server" + c.Roles = []RoleRef{{"admin"}, {"backend"}} + case healthUser: + c.TextName = "health probe" + c.Roles = []RoleRef{{"health"}} + default: + role := RoleName(user) + var acls []ACL + for _, a := range siteACLs(user) { + acls = append(acls, ACL{a["acltype"].(string), a["topic"].(string), true, 0}) + } + st.Roles = append(st.Roles, Role{Rolename: role, ACLs: acls}) + c.TextName = "site " + user + c.Roles = []RoleRef{{role}} + } + st.Clients = append(st.Clients, c) + } + if err := sc.Err(); err != nil { + return nil, err + } + return st, nil +} + +// Encode writes the store as the plugin reads it. +func (s *Store) Encode(w io.Writer) error { + enc := json.NewEncoder(w) + enc.SetIndent("", "\t") + return enc.Encode(s) +} diff --git a/server/internal/broker/migrate_test.go b/server/internal/broker/migrate_test.go new file mode 100644 index 0000000..7c3984a --- /dev/null +++ b/server/internal/broker/migrate_test.go @@ -0,0 +1,73 @@ +package broker + +import ( + "bytes" + "encoding/json" + "strings" + "testing" +) + +const passwd = `behavision-backend:$7$101$c2FsdA==$ZGlnZXN0 +health:$7$101$aGVhbHRo$aGFzaA== +acme.store1:$7$101$c2l0ZQ==$c2l0ZWhhc2g= +` + +func TestPasswdBecomesTheStoreWithHashesIntact(t *testing.T) { + st, err := FromPasswd(strings.NewReader(passwd), "behavision-backend", "health") + if err != nil { + t.Fatal(err) + } + if len(st.Clients) != 3 { + t.Fatalf("clients: %d", len(st.Clients)) + } + site := st.Clients[2] + if site.Password != "c2l0ZWhhc2g=" || site.Salt != "c2l0ZQ==" || site.Iterations != 101 { + t.Fatalf("hash not carried over intact: %+v", site) + } + if site.Roles[0].Rolename != "site.acme.store1" { + t.Fatalf("site role: %+v", site.Roles) + } + var siteRole *Role + for i := range st.Roles { + if st.Roles[i].Rolename == "site.acme.store1" { + siteRole = &st.Roles[i] + } + } + if siteRole == nil { + t.Fatal("no role for the site") + } + topics := map[string]bool{} + for _, a := range siteRole.ACLs { + topics[a.ACLType+" "+a.Topic] = true + } + for _, want := range []string{ + "publishClientSend bv/acme.store1/visit", + "publishClientSend bv/acme.store1/heartbeat", + "publishClientSend bv/acme.store1/status", + "subscribePattern bv/acme.store1/cmd/#", + } { + if !topics[want] { + t.Errorf("missing %q in %v", want, topics) + } + } + if st.Clients[0].Roles[0].Rolename != "admin" { + t.Fatalf("backend is not an admin: %+v", st.Clients[0].Roles) + } + if st.DefaultACLAccess["publishClientSend"] || st.DefaultACLAccess["subscribe"] { + t.Fatal("default access must be deny") + } + var buf bytes.Buffer + if err := st.Encode(&buf); err != nil { + t.Fatal(err) + } + if !json.Valid(buf.Bytes()) { + t.Fatal("encoded store is not valid JSON") + } +} + +func TestANonPBKDF2LineIsRefusedByName(t *testing.T) { + _, err := FromPasswd(strings.NewReader("old:$6$abc$def\n"), "b", "h") + if err == nil || !strings.Contains(err.Error(), "old") { + t.Fatalf("expected a named refusal, got %v", err) + } +} diff --git a/server/internal/provision/provision.go b/server/internal/provision/provision.go index a93bd51..a0b9eae 100644 --- a/server/internal/provision/provision.go +++ b/server/internal/provision/provision.go @@ -28,6 +28,15 @@ import ( type Provisioner struct { Pool *pgxpool.Pool Secrets *secret.Box + // Broker registers a site's login with Mosquitto when set. Without it the + // command prints the password for a hand edit of the passwd file, which + // is the pre-dynamic-security fallback and nothing more. + Broker SiteBroker +} + +// SiteBroker mirrors internal/broker's interface without importing it. +type SiteBroker interface { + EnsureSite(ctx context.Context, username, password string) error } func (p *Provisioner) CreateClient(ctx context.Context, slug, name string) (string, error) { @@ -48,6 +57,9 @@ type SiteResult struct { AgentID string Username string Password string + // BrokerRegistered is true when the login was pushed to Mosquitto here, + // so nothing remains for a person to type. + BrokerRegistered bool } // CreateSite makes a site, its agent row, and the broker password. @@ -120,7 +132,16 @@ func (p *Provisioner) CreateSite(ctx context.Context, clientSlug, siteSlug, name out.AgentID, sealed); err != nil { return out, err } - return out, tx.Commit(ctx) + if err := tx.Commit(ctx); err != nil { + return out, err + } + if p.Broker != nil { + if err := p.Broker.EnsureSite(ctx, out.Username, out.Password); err != nil { + return out, fmt.Errorf("site %s created, but the broker did not accept its login: %w", out.Username, err) + } + out.BrokerRegistered = true + } + return out, nil } func (p *Provisioner) CreateUser(ctx context.Context, clientSlug, email, role, diff --git a/server/internal/store/api_sites.go b/server/internal/store/api_sites.go new file mode 100644 index 0000000..6c34af4 --- /dev/null +++ b/server/internal/store/api_sites.go @@ -0,0 +1,115 @@ +package store + +import ( + "context" + "crypto/rand" + "encoding/base32" + "fmt" + "strings" + "time" + + "github.com/loyaly/behavision-server/internal/api" +) + +// CreateSite is the row half of opening a shop. The broker half follows it in +// the handler and the provisioner, and both use this so there is one +// definition of what a site is. +func (s *Store) CreateSite(ctx context.Context, clientID, slug, name, tz string) (api.NewSite, error) { + var out api.NewSite + if s.secrets == nil { + return out, ErrNoSecrets + } + slug = strings.ToLower(strings.TrimSpace(slug)) + if tz == "" { + tz = "UTC" + } + if _, err := time.LoadLocation(tz); err != nil { + return out, fmt.Errorf("unknown timezone %q", tz) + } + + var clientSlug string + if err := s.pool.QueryRow(ctx, `SELECT slug FROM clients WHERE id = $1::uuid`, clientID).Scan(&clientSlug); err != nil { + return out, fmt.Errorf("client %s: %w", clientID, err) + } + + tx, err := s.pool.Begin(ctx) + if err != nil { + return out, err + } + defer tx.Rollback(ctx) //nolint:errcheck + + // A plain INSERT, not an upsert: the API must not let an owner silently + // rename an existing shop by re-posting its slug. A duplicate surfaces as + // 23505 and the handler turns it into 409. + if err := tx.QueryRow(ctx, ` + INSERT INTO sites (client_id, slug, name, timezone) + VALUES ($1::uuid, $2, $3, $4) + RETURNING id::text`, clientID, slug, name, tz).Scan(&out.SiteID); err != nil { + return out, err + } + out.Slug, out.Name, out.Timezone = slug, name, tz + // The broker username IS the topic namespace: .. The ACL is + // written against it, so it is derived, never chosen. + out.Username = clientSlug + "." + slug + + var agentID string + if err := tx.QueryRow(ctx, ` + INSERT INTO agents (client_id, site_id, mqtt_username) + VALUES ($1::uuid, $2::uuid, $3) + RETURNING id::text`, clientID, out.SiteID, out.Username).Scan(&agentID); err != nil { + return out, fmt.Errorf("create agent: %w", err) + } + + out.Password, err = randomSecret(24) + if err != nil { + return out, err + } + // Sealed with the agent id as aad, so a row copied between agents does not + // decrypt into a working credential. + sealed, err := s.secrets.SealString(out.Password, agentID) + if err != nil { + return out, err + } + if _, err := tx.Exec(ctx, `UPDATE agents SET mqtt_password_enc = $2 WHERE id = $1::uuid`, agentID, sealed); err != nil { + return out, err + } + return out, tx.Commit(ctx) +} + +// DeleteNewSite removes a shop that was created moments ago and could not be +// registered with the broker. Scoped to the tenant and refused once the shop +// has anything under it: this is compensation for a failed create, not a +// delete-shop feature. +func (s *Store) DeleteNewSite(ctx context.Context, clientID, siteID string) error { + tx, err := s.pool.Begin(ctx) + if err != nil { + return err + } + defer tx.Rollback(ctx) //nolint:errcheck + var used bool + if err := tx.QueryRow(ctx, ` + SELECT EXISTS (SELECT 1 FROM visits WHERE site_id = $1::uuid) + OR EXISTS (SELECT 1 FROM site_cameras WHERE site_id = $1::uuid)`, siteID).Scan(&used); err != nil { + return err + } + if used { + return fmt.Errorf("site %s is in use", siteID) + } + if _, err := tx.Exec(ctx, `DELETE FROM agents WHERE site_id = $1::uuid AND client_id = $2::uuid`, siteID, clientID); err != nil { + return err + } + if _, err := tx.Exec(ctx, `DELETE FROM sites WHERE id = $1::uuid AND client_id = $2::uuid`, siteID, clientID); err != nil { + return err + } + return tx.Commit(ctx) +} + +func randomSecret(n int) (string, error) { + b := make([]byte, n) + if _, err := rand.Read(b); err != nil { + return "", err + } + // base32 without padding: this gets typed, pasted into config files and + // read down a phone line, and base64's + / = survive none of that. + return strings.ToLower(base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(b)), nil +} diff --git a/server/internal/web/dist/assets/index-BI5JLIeo.js b/server/internal/web/dist/assets/index-xQ5C-n-U.js similarity index 70% rename from server/internal/web/dist/assets/index-BI5JLIeo.js rename to server/internal/web/dist/assets/index-xQ5C-n-U.js index 4e3e7a1..7da7b91 100644 --- a/server/internal/web/dist/assets/index-BI5JLIeo.js +++ b/server/internal/web/dist/assets/index-xQ5C-n-U.js @@ -22,7 +22,7 @@ * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. - */(function(e){function t(C,z){var L=C.length;C.push(z);e:for(;0>>1,q=C[G];if(0>>1;Gl(Rl,L))wtl(dr,Rl)?(C[G]=dr,C[wt]=L,G=wt):(C[G]=Rl,C[gt]=L,G=gt);else if(wtl(dr,L))C[G]=dr,C[wt]=L,G=wt;else break e}}return z}function l(C,z){var L=C.sortIndex-z.sortIndex;return L!==0?L:C.id-z.id}if(typeof performance=="object"&&typeof performance.now=="function"){var s=performance;e.unstable_now=function(){return s.now()}}else{var i=Date,a=i.now();e.unstable_now=function(){return i.now()-a}}var u=[],c=[],v=1,m=null,h=3,y=!1,x=!1,g=!1,T=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 z=n(c);z!==null;){if(z.callback===null)r(c);else if(z.startTime<=C)r(c),z.sortIndex=z.expirationTime,t(u,z);else break;z=n(c)}}function w(C){if(g=!1,f(C),!x)if(n(u)!==null)x=!0,zl(S);else{var z=n(c);z!==null&&Ll(w,z.startTime-C)}}function S(C,z){x=!1,g&&(g=!1,p(P),P=-1),y=!0;var L=h;try{for(f(z),m=n(u);m!==null&&(!(m.expirationTime>z)||C&&!Te());){var G=m.callback;if(typeof G=="function"){m.callback=null,h=m.priorityLevel;var q=G(m.expirationTime<=z);z=e.unstable_now(),typeof q=="function"?m.callback=q:m===n(u)&&r(u),f(z)}else r(u);m=n(u)}if(m!==null)var cr=!0;else{var gt=n(c);gt!==null&&Ll(w,gt.startTime-z),cr=!1}return cr}finally{m=null,h=L,y=!1}}var E=!1,_=null,P=-1,K=5,O=-1;function Te(){return!(e.unstable_now()-OC||125G?(C.sortIndex=L,t(c,C),n(u)===null&&C===n(c)&&(g?(p(P),P=-1):g=!0,Ll(w,L-G))):(C.sortIndex=q,t(u,C),x||y||(x=!0,zl(S))),C},e.unstable_shouldYield=Te,e.unstable_wrapCallback=function(C){var z=h;return function(){var L=h;h=z;try{return C.apply(this,arguments)}finally{h=L}}}})(Na);Sa.exports=Na;var Yc=Sa.exports;/** + */(function(e){function t(C,z){var L=C.length;C.push(z);e:for(;0>>1,q=C[G];if(0>>1;Gl(Rl,L))wtl(dr,Rl)?(C[G]=dr,C[wt]=L,G=wt):(C[G]=Rl,C[gt]=L,G=gt);else if(wtl(dr,L))C[G]=dr,C[wt]=L,G=wt;else break e}}return z}function l(C,z){var L=C.sortIndex-z.sortIndex;return L!==0?L:C.id-z.id}if(typeof performance=="object"&&typeof performance.now=="function"){var s=performance;e.unstable_now=function(){return s.now()}}else{var i=Date,a=i.now();e.unstable_now=function(){return i.now()-a}}var u=[],c=[],v=1,h=null,m=3,y=!1,x=!1,g=!1,P=typeof setTimeout=="function"?setTimeout:null,f=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 p(C){for(var z=n(c);z!==null;){if(z.callback===null)r(c);else if(z.startTime<=C)r(c),z.sortIndex=z.expirationTime,t(u,z);else break;z=n(c)}}function w(C){if(g=!1,p(C),!x)if(n(u)!==null)x=!0,zl(S);else{var z=n(c);z!==null&&Ll(w,z.startTime-C)}}function S(C,z){x=!1,g&&(g=!1,f(T),T=-1),y=!0;var L=m;try{for(p(z),h=n(u);h!==null&&(!(h.expirationTime>z)||C&&!Te());){var G=h.callback;if(typeof G=="function"){h.callback=null,m=h.priorityLevel;var q=G(h.expirationTime<=z);z=e.unstable_now(),typeof q=="function"?h.callback=q:h===n(u)&&r(u),p(z)}else r(u);h=n(u)}if(h!==null)var cr=!0;else{var gt=n(c);gt!==null&&Ll(w,gt.startTime-z),cr=!1}return cr}finally{h=null,m=L,y=!1}}var E=!1,_=null,T=-1,K=5,O=-1;function Te(){return!(e.unstable_now()-OC||125G?(C.sortIndex=L,t(c,C),n(u)===null&&C===n(c)&&(g?(f(T),T=-1):g=!0,Ll(w,L-G))):(C.sortIndex=q,t(u,C),x||y||(x=!0,zl(S))),C},e.unstable_shouldYield=Te,e.unstable_wrapCallback=function(C){var z=m;return function(){var L=m;m=z;try{return C.apply(this,arguments)}finally{m=L}}}})(Na);Sa.exports=Na;var Yc=Sa.exports;/** * @license React * react-dom.production.min.js * @@ -34,13 +34,13 @@ `+Il+e}var Fl=!1;function Dl(e,t){if(!e||Fl)return"";Fl=!0;var n=Error.prepareStackTrace;Error.prepareStackTrace=void 0;try{if(t)if(t=function(){throw Error()},Object.defineProperty(t.prototype,"props",{set:function(){throw Error()}}),typeof Reflect=="object"&&Reflect.construct){try{Reflect.construct(t,[])}catch(c){var r=c}Reflect.construct(e,[],t)}else{try{t.call()}catch(c){r=c}e.call(t.prototype)}else{try{throw Error()}catch(c){r=c}e()}}catch(c){if(c&&r&&typeof c.stack=="string"){for(var l=c.stack.split(` `),s=r.stack.split(` `),i=l.length-1,a=s.length-1;1<=i&&0<=a&&l[i]!==s[a];)a--;for(;1<=i&&0<=a;i--,a--)if(l[i]!==s[a]){if(i!==1||a!==1)do if(i--,a--,0>a||l[i]!==s[a]){var u=` -`+l[i].replace(" at new "," at ");return e.displayName&&u.includes("")&&(u=u.replace("",e.displayName)),u}while(1<=i&&0<=a);break}}}finally{Fl=!1,Error.prepareStackTrace=n}return(e=e?e.displayName||e.name:"")?Cn(e):""}function ed(e){switch(e.tag){case 5:return Cn(e.type);case 16:return Cn("Lazy");case 13:return Cn("Suspense");case 19:return Cn("SuspenseList");case 0:case 2:case 15:return e=Dl(e.type,!1),e;case 11:return e=Dl(e.type.render,!1),e;case 1:return e=Dl(e.type,!0),e;default:return""}}function cs(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 At:return"Fragment";case Ut:return"Portal";case os:return"Profiler";case oi:return"StrictMode";case as:return"Suspense";case us:return"SuspenseList"}if(typeof e=="object")switch(e.$$typeof){case _a:return(e.displayName||"Context")+".Consumer";case Ea:return(e._context.displayName||"Context")+".Provider";case ai:var t=e.render;return e=e.displayName,e||(e=t.displayName||t.name||"",e=e!==""?"ForwardRef("+e+")":"ForwardRef"),e;case ui:return t=e.displayName||null,t!==null?t:cs(e.type)||"Memo";case be:t=e._payload,e=e._init;try{return cs(e(t))}catch{}}return null}function td(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 cs(t);case 8:return t===oi?"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 Ta(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(t==="checkbox"||t==="radio")}function nd(e){var t=Ta(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,s=n.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return l.call(this)},set:function(i){r=""+i,s.call(this,i)}}),Object.defineProperty(e,t,{enumerable:n.enumerable}),{getValue:function(){return r},setValue:function(i){r=""+i},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}function hr(e){e._valueTracker||(e._valueTracker=nd(e))}function za(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var n=t.getValue(),r="";return e&&(r=Ta(e)?e.checked?"true":"false":e.value),e=r,e!==n?(t.setValue(e),!0):!1}function Vr(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 ds(e,t){var n=t.checked;return H({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:n??e._wrapperState.initialChecked})}function ro(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 La(e,t){t=t.checked,t!=null&&ii(e,"checked",t,!1)}function fs(e,t){La(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")?ps(e,t.type,n):t.hasOwnProperty("defaultValue")&&ps(e,t.type,pt(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}function lo(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 ps(e,t,n){(t!=="number"||Vr(e.ownerDocument)!==e)&&(n==null?e.defaultValue=""+e._wrapperState.initialValue:e.defaultValue!==""+n&&(e.defaultValue=""+n))}var En=Array.isArray;function Zt(e,t,n,r){if(e=e.options,t){t={};for(var l=0;l"+t.valueOf().toString()+"",t=mr.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;t.firstChild;)e.appendChild(t.firstChild)}});function Un(e,t){if(t){var n=e.firstChild;if(n&&n===e.lastChild&&n.nodeType===3){n.nodeValue=t;return}}e.textContent=t}var Tn={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},rd=["Webkit","ms","Moz","O"];Object.keys(Tn).forEach(function(e){rd.forEach(function(t){t=t+e.charAt(0).toUpperCase()+e.substring(1),Tn[t]=Tn[e]})});function Fa(e,t,n){return t==null||typeof t=="boolean"||t===""?"":n||typeof t!="number"||t===0||Tn.hasOwnProperty(e)&&Tn[e]?(""+t).trim():t+"px"}function Da(e,t){e=e.style;for(var n in t)if(t.hasOwnProperty(n)){var r=n.indexOf("--")===0,l=Fa(n,t[n],r);n==="float"&&(n="cssFloat"),r?e.setProperty(n,l):e[n]=l}}var ld=H({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 vs(e,t){if(t){if(ld[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 ys(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 gs=null;function ci(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}var ws=null,qt=null,bt=null;function oo(e){if(e=lr(e)){if(typeof ws!="function")throw Error(k(280));var t=e.stateNode;t&&(t=gl(t),ws(e.stateNode,e.type,t))}}function Ma(e){qt?bt?bt.push(e):bt=[e]:qt=e}function $a(){if(qt){var e=qt,t=bt;if(bt=qt=null,oo(e),t)for(e=0;e>>=0,e===0?32:31-(md(e)/vd|0)|0}var vr=64,yr=4194304;function _n(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 Kr(e,t){var n=e.pendingLanes;if(n===0)return 0;var r=0,l=e.suspendedLanes,s=e.pingedLanes,i=n&268435455;if(i!==0){var a=i&~l;a!==0?r=_n(a):(s&=i,s!==0&&(r=_n(s)))}else i=n&~l,i!==0?r=_n(i):s!==0&&(r=_n(s));if(r===0)return 0;if(t!==0&&t!==r&&!(t&l)&&(l=r&-r,s=t&-t,l>=s||l===16&&(s&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 xd(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=Ln),yo=" ",go=!1;function lu(e,t){switch(e){case"keyup":return Yd.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function su(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}var Bt=!1;function Jd(e,t){switch(e){case"compositionend":return su(t);case"keypress":return t.which!==32?null:(go=!0,yo);case"textInput":return e=t.data,e===yo&&go?null:e;default:return null}}function Zd(e,t){if(Bt)return e==="compositionend"||!gi&&lu(e,t)?(e=nu(),Rr=mi=rt=null,Bt=!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=jo(n)}}function uu(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?uu(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function cu(){for(var e=window,t=Vr();t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href=="string"}catch{n=!1}if(n)e=t.contentWindow;else break;t=Vr(e.document)}return t}function wi(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 of(e){var t=cu(),n=e.focusedElem,r=e.selectionRange;if(t!==n&&n&&n.ownerDocument&&uu(n.ownerDocument.documentElement,n)){if(r!==null&&wi(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,s=Math.min(r.start,l);r=r.end===void 0?s:Math.min(r.end,l),!e.extend&&s>r&&(l=r,r=s,s=l),l=So(n,s);var i=So(n,r);l&&i&&(e.rangeCount!==1||e.anchorNode!==l.node||e.anchorOffset!==l.offset||e.focusNode!==i.node||e.focusOffset!==i.offset)&&(t=t.createRange(),t.setStart(l.node,l.offset),e.removeAllRanges(),s>r?(e.addRange(t),e.extend(i.node,i.offset)):(t.setEnd(i.node,i.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,Vt=null,Cs=null,On=null,Es=!1;function No(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;Es||Vt==null||Vt!==Vr(r)||(r=Vt,"selectionStart"in r&&wi(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}),On&&Qn(On,r)||(On=r,r=Xr(Cs,"onSelect"),0Qt||(e.current=Rs[Qt],Rs[Qt]=null,Qt--)}function M(e,t){Qt++,Rs[Qt]=e.current,e.current=t}var ht={},oe=vt(ht),he=vt(!1),Tt=ht;function ln(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={},s;for(s in n)l[s]=t[s];return r&&(e=e.stateNode,e.__reactInternalMemoizedUnmaskedChildContext=t,e.__reactInternalMemoizedMaskedChildContext=l),l}function me(e){return e=e.childContextTypes,e!=null}function Zr(){A(he),A(oe)}function Lo(e,t,n){if(oe.current!==ht)throw Error(k(168));M(oe,t),M(he,n)}function wu(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,td(e)||"Unknown",l));return H({},n,r)}function qr(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||ht,Tt=oe.current,M(oe,e),M(he,he.current),!0}function Ro(e,t,n){var r=e.stateNode;if(!r)throw Error(k(169));n?(e=wu(e,t,Tt),r.__reactInternalMemoizedMergedChildContext=e,A(he),A(oe),M(oe,e)):A(he),M(he,n)}var We=null,wl=!1,Jl=!1;function xu(e){We===null?We=[e]:We.push(e)}function wf(e){wl=!0,xu(e)}function yt(){if(!Jl&&We!==null){Jl=!0;var e=0,t=F;try{var n=We;for(F=1;e>=i,l-=i,He=1<<32-Ie(t)+l|n<P?(K=_,_=null):K=_.sibling;var O=h(p,_,f[P],w);if(O===null){_===null&&(_=K);break}e&&_&&O.alternate===null&&t(p,_),d=s(O,d,P),E===null?S=O:E.sibling=O,E=O,_=K}if(P===f.length)return n(p,_),B&&xt(p,P),S;if(_===null){for(;PP?(K=_,_=null):K=_.sibling;var Te=h(p,_,O.value,w);if(Te===null){_===null&&(_=K);break}e&&_&&Te.alternate===null&&t(p,_),d=s(Te,d,P),E===null?S=Te:E.sibling=Te,E=Te,_=K}if(O.done)return n(p,_),B&&xt(p,P),S;if(_===null){for(;!O.done;P++,O=f.next())O=m(p,O.value,w),O!==null&&(d=s(O,d,P),E===null?S=O:E.sibling=O,E=O);return B&&xt(p,P),S}for(_=r(p,_);!O.done;P++,O=f.next())O=y(_,p,P,O.value,w),O!==null&&(e&&O.alternate!==null&&_.delete(O.key===null?P:O.key),d=s(O,d,P),E===null?S=O:E.sibling=O,E=O);return e&&_.forEach(function(hn){return t(p,hn)}),B&&xt(p,P),S}function T(p,d,f,w){if(typeof f=="object"&&f!==null&&f.type===At&&f.key===null&&(f=f.props.children),typeof f=="object"&&f!==null){switch(f.$$typeof){case pr:e:{for(var S=f.key,E=d;E!==null;){if(E.key===S){if(S=f.type,S===At){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&&Fo(S)===E.type){n(p,E.sibling),d=l(E,f.props),d.ref=kn(p,E,f),d.return=p,p=d;break e}n(p,E);break}else t(p,E);E=E.sibling}f.type===At?(d=_t(f.props.children,p.mode,w,f.key),d.return=p,p=d):(w=Ar(f.type,f.key,f.props,null,p.mode,w),w.ref=kn(p,d,f),w.return=p,p=w)}return i(p);case Ut: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=ls(f,p.mode,w),d.return=p,p=d}return i(p);case be:return E=f._init,T(p,d,E(f._payload),w)}if(En(f))return x(p,d,f,w);if(vn(f))return g(p,d,f,w);Nr(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=rs(f,p.mode,w),d.return=p,p=d),i(p)):n(p,d)}return T}var on=Nu(!0),Cu=Nu(!1),tl=vt(null),nl=null,Yt=null,Si=null;function Ni(){Si=Yt=nl=null}function Ci(e){var t=tl.current;A(tl),e._currentValue=t}function Fs(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 tn(e,t){nl=e,Si=Yt=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(Si!==e)if(e={context:e,memoizedValue:t,next:null},Yt===null){if(nl===null)throw Error(k(308));Yt=e,nl.dependencies={lanes:0,firstContext:e}}else Yt=Yt.next=e;return t}var Nt=null;function Ei(e){Nt===null?Nt=[e]:Nt.push(e)}function Eu(e,t,n,r){var l=t.interleaved;return l===null?(n.next=n,Ei(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 _i(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,interleaved:null,lanes:0},effects:null}}function _u(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 ut(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,Ei(r)):(t.next=l.next,l.next=t),r.interleaved=t,Xe(e,n)}function Ir(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,fi(e,n)}}function Do(e,t){var n=e.updateQueue,r=e.alternate;if(r!==null&&(r=r.updateQueue,n===r)){var l=null,s=null;if(n=n.firstBaseUpdate,n!==null){do{var i={eventTime:n.eventTime,lane:n.lane,tag:n.tag,payload:n.payload,callback:n.callback,next:null};s===null?l=s=i:s=s.next=i,n=n.next}while(n!==null);s===null?l=s=t:s=s.next=t}else l=s=t;n={baseState:r.baseState,firstBaseUpdate:l,lastBaseUpdate:s,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 rl(e,t,n,r){var l=e.updateQueue;et=!1;var s=l.firstBaseUpdate,i=l.lastBaseUpdate,a=l.shared.pending;if(a!==null){l.shared.pending=null;var u=a,c=u.next;u.next=null,i===null?s=c:i.next=c,i=u;var v=e.alternate;v!==null&&(v=v.updateQueue,a=v.lastBaseUpdate,a!==i&&(a===null?v.firstBaseUpdate=c:a.next=c,v.lastBaseUpdate=u))}if(s!==null){var m=l.baseState;i=0,v=c=u=null,a=s;do{var h=a.lane,y=a.eventTime;if((r&h)===h){v!==null&&(v=v.next={eventTime:y,lane:0,tag:a.tag,payload:a.payload,callback:a.callback,next:null});e:{var x=e,g=a;switch(h=t,y=n,g.tag){case 1:if(x=g.payload,typeof x=="function"){m=x.call(y,m,h);break e}m=x;break e;case 3:x.flags=x.flags&-65537|128;case 0:if(x=g.payload,h=typeof x=="function"?x.call(y,m,h):x,h==null)break e;m=H({},m,h);break e;case 2:et=!0}}a.callback!==null&&a.lane!==0&&(e.flags|=64,h=l.effects,h===null?l.effects=[a]:h.push(a))}else y={eventTime:y,lane:h,tag:a.tag,payload:a.payload,callback:a.callback,next:null},v===null?(c=v=y,u=m):v=v.next=y,i|=h;if(a=a.next,a===null){if(a=l.shared.pending,a===null)break;h=a,a=h.next,h.next=null,l.lastBaseUpdate=h,l.shared.pending=null}}while(!0);if(v===null&&(u=m),l.baseState=u,l.firstBaseUpdate=c,l.lastBaseUpdate=v,t=l.shared.interleaved,t!==null){l=t;do i|=l.lane,l=l.next;while(l!==t)}else s===null&&(l.shared.lanes=0);Rt|=i,e.lanes=i,e.memoizedState=m}}function Mo(e,t,n){if(e=t.effects,t.effects=null,e!==null)for(t=0;tn?n:4,e(!0);var r=ql.transition;ql.transition={};try{e(!1),t()}finally{F=n,ql.transition=r}}function Hu(){return Pe().memoizedState}function Sf(e,t,n){var r=dt(e);if(n={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null},Qu(e))Ku(t,n);else if(n=Eu(e,t,n,r),n!==null){var l=ue();Fe(n,e,r,l),Gu(n,t,r)}}function Nf(e,t,n){var r=dt(e),l={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null};if(Qu(e))Ku(t,l);else{var s=e.alternate;if(e.lanes===0&&(s===null||s.lanes===0)&&(s=t.lastRenderedReducer,s!==null))try{var i=t.lastRenderedState,a=s(i,n);if(l.hasEagerState=!0,l.eagerState=a,De(a,i)){var u=t.interleaved;u===null?(l.next=l,Ei(t)):(l.next=u.next,u.next=l),t.interleaved=l;return}}catch{}finally{}n=Eu(e,t,l,r),n!==null&&(l=ue(),Fe(n,e,r,l),Gu(n,t,r))}}function Qu(e){var t=e.alternate;return e===W||t!==null&&t===W}function Ku(e,t){In=sl=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function Gu(e,t,n){if(n&4194240){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,fi(e,n)}}var il={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 $e().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,Dr(4194308,4,Uu.bind(null,t,e),n)},useLayoutEffect:function(e,t){return Dr(4194308,4,e,t)},useInsertionEffect:function(e,t){return Dr(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=Sf.bind(null,W,e),[r.memoizedState,e]},useRef:function(e){var t=$e();return e={current:e},t.memoizedState=e},useState:$o,useDebugValue:Fi,useDeferredValue:function(e){return $e().memoizedState=e},useTransition:function(){var e=$o(!1),t=e[0];return e=jf.bind(null,e[1]),$e().memoizedState=e,[t,e]},useMutableSource:function(){},useSyncExternalStore:function(e,t,n){var r=W,l=$e();if(B){if(n===void 0)throw Error(k(407));n=n()}else{if(n=t(),ee===null)throw Error(k(349));Lt&30||Lu(r,t,n)}l.memoizedState=n;var s={value:n,getSnapshot:t};return l.queue=s,Uo(Ou.bind(null,r,s,e),[e]),r.flags|=2048,bn(9,Ru.bind(null,r,s,n,t),void 0,null),n},useId:function(){var e=$e(),t=ee.identifierPrefix;if(B){var n=Qe,r=He;n=(r&~(1<<32-Ie(r)-1)).toString(32)+n,t=":"+t+"R"+n,n=Zn++,0")&&(u=u.replace("",e.displayName)),u}while(1<=i&&0<=a);break}}}finally{Fl=!1,Error.prepareStackTrace=n}return(e=e?e.displayName||e.name:"")?Cn(e):""}function ed(e){switch(e.tag){case 5:return Cn(e.type);case 16:return Cn("Lazy");case 13:return Cn("Suspense");case 19:return Cn("SuspenseList");case 0:case 2:case 15:return e=Dl(e.type,!1),e;case 11:return e=Dl(e.type.render,!1),e;case 1:return e=Dl(e.type,!0),e;default:return""}}function cs(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 At:return"Fragment";case Ut:return"Portal";case os:return"Profiler";case oi:return"StrictMode";case as:return"Suspense";case us:return"SuspenseList"}if(typeof e=="object")switch(e.$$typeof){case _a:return(e.displayName||"Context")+".Consumer";case Ea:return(e._context.displayName||"Context")+".Provider";case ai:var t=e.render;return e=e.displayName,e||(e=t.displayName||t.name||"",e=e!==""?"ForwardRef("+e+")":"ForwardRef"),e;case ui:return t=e.displayName||null,t!==null?t:cs(e.type)||"Memo";case be:t=e._payload,e=e._init;try{return cs(e(t))}catch{}}return null}function td(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 cs(t);case 8:return t===oi?"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 Ta(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(t==="checkbox"||t==="radio")}function nd(e){var t=Ta(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,s=n.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return l.call(this)},set:function(i){r=""+i,s.call(this,i)}}),Object.defineProperty(e,t,{enumerable:n.enumerable}),{getValue:function(){return r},setValue:function(i){r=""+i},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}function hr(e){e._valueTracker||(e._valueTracker=nd(e))}function za(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var n=t.getValue(),r="";return e&&(r=Ta(e)?e.checked?"true":"false":e.value),e=r,e!==n?(t.setValue(e),!0):!1}function Vr(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 ds(e,t){var n=t.checked;return H({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:n??e._wrapperState.initialChecked})}function ro(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 La(e,t){t=t.checked,t!=null&&ii(e,"checked",t,!1)}function fs(e,t){La(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")?ps(e,t.type,n):t.hasOwnProperty("defaultValue")&&ps(e,t.type,pt(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}function lo(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 ps(e,t,n){(t!=="number"||Vr(e.ownerDocument)!==e)&&(n==null?e.defaultValue=""+e._wrapperState.initialValue:e.defaultValue!==""+n&&(e.defaultValue=""+n))}var En=Array.isArray;function Zt(e,t,n,r){if(e=e.options,t){t={};for(var l=0;l"+t.valueOf().toString()+"",t=mr.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;t.firstChild;)e.appendChild(t.firstChild)}});function Un(e,t){if(t){var n=e.firstChild;if(n&&n===e.lastChild&&n.nodeType===3){n.nodeValue=t;return}}e.textContent=t}var Tn={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},rd=["Webkit","ms","Moz","O"];Object.keys(Tn).forEach(function(e){rd.forEach(function(t){t=t+e.charAt(0).toUpperCase()+e.substring(1),Tn[t]=Tn[e]})});function Fa(e,t,n){return t==null||typeof t=="boolean"||t===""?"":n||typeof t!="number"||t===0||Tn.hasOwnProperty(e)&&Tn[e]?(""+t).trim():t+"px"}function Da(e,t){e=e.style;for(var n in t)if(t.hasOwnProperty(n)){var r=n.indexOf("--")===0,l=Fa(n,t[n],r);n==="float"&&(n="cssFloat"),r?e.setProperty(n,l):e[n]=l}}var ld=H({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 vs(e,t){if(t){if(ld[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 ys(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 gs=null;function ci(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}var ws=null,qt=null,bt=null;function oo(e){if(e=lr(e)){if(typeof ws!="function")throw Error(k(280));var t=e.stateNode;t&&(t=gl(t),ws(e.stateNode,e.type,t))}}function Ma(e){qt?bt?bt.push(e):bt=[e]:qt=e}function $a(){if(qt){var e=qt,t=bt;if(bt=qt=null,oo(e),t)for(e=0;e>>=0,e===0?32:31-(md(e)/vd|0)|0}var vr=64,yr=4194304;function _n(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 Kr(e,t){var n=e.pendingLanes;if(n===0)return 0;var r=0,l=e.suspendedLanes,s=e.pingedLanes,i=n&268435455;if(i!==0){var a=i&~l;a!==0?r=_n(a):(s&=i,s!==0&&(r=_n(s)))}else i=n&~l,i!==0?r=_n(i):s!==0&&(r=_n(s));if(r===0)return 0;if(t!==0&&t!==r&&!(t&l)&&(l=r&-r,s=t&-t,l>=s||l===16&&(s&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 xd(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=Ln),yo=" ",go=!1;function lu(e,t){switch(e){case"keyup":return Yd.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function su(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}var Bt=!1;function Jd(e,t){switch(e){case"compositionend":return su(t);case"keypress":return t.which!==32?null:(go=!0,yo);case"textInput":return e=t.data,e===yo&&go?null:e;default:return null}}function Zd(e,t){if(Bt)return e==="compositionend"||!gi&&lu(e,t)?(e=nu(),Rr=mi=rt=null,Bt=!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=jo(n)}}function uu(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?uu(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function cu(){for(var e=window,t=Vr();t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href=="string"}catch{n=!1}if(n)e=t.contentWindow;else break;t=Vr(e.document)}return t}function wi(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 of(e){var t=cu(),n=e.focusedElem,r=e.selectionRange;if(t!==n&&n&&n.ownerDocument&&uu(n.ownerDocument.documentElement,n)){if(r!==null&&wi(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,s=Math.min(r.start,l);r=r.end===void 0?s:Math.min(r.end,l),!e.extend&&s>r&&(l=r,r=s,s=l),l=So(n,s);var i=So(n,r);l&&i&&(e.rangeCount!==1||e.anchorNode!==l.node||e.anchorOffset!==l.offset||e.focusNode!==i.node||e.focusOffset!==i.offset)&&(t=t.createRange(),t.setStart(l.node,l.offset),e.removeAllRanges(),s>r?(e.addRange(t),e.extend(i.node,i.offset)):(t.setEnd(i.node,i.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,Vt=null,Cs=null,On=null,Es=!1;function No(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;Es||Vt==null||Vt!==Vr(r)||(r=Vt,"selectionStart"in r&&wi(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}),On&&Qn(On,r)||(On=r,r=Xr(Cs,"onSelect"),0Qt||(e.current=Rs[Qt],Rs[Qt]=null,Qt--)}function $(e,t){Qt++,Rs[Qt]=e.current,e.current=t}var ht={},oe=vt(ht),he=vt(!1),Tt=ht;function ln(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={},s;for(s in n)l[s]=t[s];return r&&(e=e.stateNode,e.__reactInternalMemoizedUnmaskedChildContext=t,e.__reactInternalMemoizedMaskedChildContext=l),l}function me(e){return e=e.childContextTypes,e!=null}function Zr(){A(he),A(oe)}function Lo(e,t,n){if(oe.current!==ht)throw Error(k(168));$(oe,t),$(he,n)}function wu(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,td(e)||"Unknown",l));return H({},n,r)}function qr(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||ht,Tt=oe.current,$(oe,e),$(he,he.current),!0}function Ro(e,t,n){var r=e.stateNode;if(!r)throw Error(k(169));n?(e=wu(e,t,Tt),r.__reactInternalMemoizedMergedChildContext=e,A(he),A(oe),$(oe,e)):A(he),$(he,n)}var We=null,wl=!1,Jl=!1;function xu(e){We===null?We=[e]:We.push(e)}function wf(e){wl=!0,xu(e)}function yt(){if(!Jl&&We!==null){Jl=!0;var e=0,t=D;try{var n=We;for(D=1;e>=i,l-=i,He=1<<32-Ie(t)+l|n<T?(K=_,_=null):K=_.sibling;var O=m(f,_,p[T],w);if(O===null){_===null&&(_=K);break}e&&_&&O.alternate===null&&t(f,_),d=s(O,d,T),E===null?S=O:E.sibling=O,E=O,_=K}if(T===p.length)return n(f,_),B&&xt(f,T),S;if(_===null){for(;TT?(K=_,_=null):K=_.sibling;var Te=m(f,_,O.value,w);if(Te===null){_===null&&(_=K);break}e&&_&&Te.alternate===null&&t(f,_),d=s(Te,d,T),E===null?S=Te:E.sibling=Te,E=Te,_=K}if(O.done)return n(f,_),B&&xt(f,T),S;if(_===null){for(;!O.done;T++,O=p.next())O=h(f,O.value,w),O!==null&&(d=s(O,d,T),E===null?S=O:E.sibling=O,E=O);return B&&xt(f,T),S}for(_=r(f,_);!O.done;T++,O=p.next())O=y(_,f,T,O.value,w),O!==null&&(e&&O.alternate!==null&&_.delete(O.key===null?T:O.key),d=s(O,d,T),E===null?S=O:E.sibling=O,E=O);return e&&_.forEach(function(hn){return t(f,hn)}),B&&xt(f,T),S}function P(f,d,p,w){if(typeof p=="object"&&p!==null&&p.type===At&&p.key===null&&(p=p.props.children),typeof p=="object"&&p!==null){switch(p.$$typeof){case pr:e:{for(var S=p.key,E=d;E!==null;){if(E.key===S){if(S=p.type,S===At){if(E.tag===7){n(f,E.sibling),d=l(E,p.props.children),d.return=f,f=d;break e}}else if(E.elementType===S||typeof S=="object"&&S!==null&&S.$$typeof===be&&Fo(S)===E.type){n(f,E.sibling),d=l(E,p.props),d.ref=kn(f,E,p),d.return=f,f=d;break e}n(f,E);break}else t(f,E);E=E.sibling}p.type===At?(d=_t(p.props.children,f.mode,w,p.key),d.return=f,f=d):(w=Ar(p.type,p.key,p.props,null,f.mode,w),w.ref=kn(f,d,p),w.return=f,f=w)}return i(f);case Ut:e:{for(E=p.key;d!==null;){if(d.key===E)if(d.tag===4&&d.stateNode.containerInfo===p.containerInfo&&d.stateNode.implementation===p.implementation){n(f,d.sibling),d=l(d,p.children||[]),d.return=f,f=d;break e}else{n(f,d);break}else t(f,d);d=d.sibling}d=ls(p,f.mode,w),d.return=f,f=d}return i(f);case be:return E=p._init,P(f,d,E(p._payload),w)}if(En(p))return x(f,d,p,w);if(vn(p))return g(f,d,p,w);Nr(f,p)}return typeof p=="string"&&p!==""||typeof p=="number"?(p=""+p,d!==null&&d.tag===6?(n(f,d.sibling),d=l(d,p),d.return=f,f=d):(n(f,d),d=rs(p,f.mode,w),d.return=f,f=d),i(f)):n(f,d)}return P}var on=Nu(!0),Cu=Nu(!1),tl=vt(null),nl=null,Yt=null,Si=null;function Ni(){Si=Yt=nl=null}function Ci(e){var t=tl.current;A(tl),e._currentValue=t}function Fs(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 tn(e,t){nl=e,Si=Yt=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(Si!==e)if(e={context:e,memoizedValue:t,next:null},Yt===null){if(nl===null)throw Error(k(308));Yt=e,nl.dependencies={lanes:0,firstContext:e}}else Yt=Yt.next=e;return t}var Nt=null;function Ei(e){Nt===null?Nt=[e]:Nt.push(e)}function Eu(e,t,n,r){var l=t.interleaved;return l===null?(n.next=n,Ei(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 _i(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,interleaved:null,lanes:0},effects:null}}function _u(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 ut(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,Ei(r)):(t.next=l.next,l.next=t),r.interleaved=t,Xe(e,n)}function Ir(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,fi(e,n)}}function Do(e,t){var n=e.updateQueue,r=e.alternate;if(r!==null&&(r=r.updateQueue,n===r)){var l=null,s=null;if(n=n.firstBaseUpdate,n!==null){do{var i={eventTime:n.eventTime,lane:n.lane,tag:n.tag,payload:n.payload,callback:n.callback,next:null};s===null?l=s=i:s=s.next=i,n=n.next}while(n!==null);s===null?l=s=t:s=s.next=t}else l=s=t;n={baseState:r.baseState,firstBaseUpdate:l,lastBaseUpdate:s,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 rl(e,t,n,r){var l=e.updateQueue;et=!1;var s=l.firstBaseUpdate,i=l.lastBaseUpdate,a=l.shared.pending;if(a!==null){l.shared.pending=null;var u=a,c=u.next;u.next=null,i===null?s=c:i.next=c,i=u;var v=e.alternate;v!==null&&(v=v.updateQueue,a=v.lastBaseUpdate,a!==i&&(a===null?v.firstBaseUpdate=c:a.next=c,v.lastBaseUpdate=u))}if(s!==null){var h=l.baseState;i=0,v=c=u=null,a=s;do{var m=a.lane,y=a.eventTime;if((r&m)===m){v!==null&&(v=v.next={eventTime:y,lane:0,tag:a.tag,payload:a.payload,callback:a.callback,next:null});e:{var x=e,g=a;switch(m=t,y=n,g.tag){case 1:if(x=g.payload,typeof x=="function"){h=x.call(y,h,m);break e}h=x;break e;case 3:x.flags=x.flags&-65537|128;case 0:if(x=g.payload,m=typeof x=="function"?x.call(y,h,m):x,m==null)break e;h=H({},h,m);break e;case 2:et=!0}}a.callback!==null&&a.lane!==0&&(e.flags|=64,m=l.effects,m===null?l.effects=[a]:m.push(a))}else y={eventTime:y,lane:m,tag:a.tag,payload:a.payload,callback:a.callback,next:null},v===null?(c=v=y,u=h):v=v.next=y,i|=m;if(a=a.next,a===null){if(a=l.shared.pending,a===null)break;m=a,a=m.next,m.next=null,l.lastBaseUpdate=m,l.shared.pending=null}}while(!0);if(v===null&&(u=h),l.baseState=u,l.firstBaseUpdate=c,l.lastBaseUpdate=v,t=l.shared.interleaved,t!==null){l=t;do i|=l.lane,l=l.next;while(l!==t)}else s===null&&(l.shared.lanes=0);Rt|=i,e.lanes=i,e.memoizedState=h}}function Mo(e,t,n){if(e=t.effects,t.effects=null,e!==null)for(t=0;tn?n:4,e(!0);var r=ql.transition;ql.transition={};try{e(!1),t()}finally{D=n,ql.transition=r}}function Hu(){return Pe().memoizedState}function Sf(e,t,n){var r=dt(e);if(n={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null},Qu(e))Ku(t,n);else if(n=Eu(e,t,n,r),n!==null){var l=ue();Fe(n,e,r,l),Gu(n,t,r)}}function Nf(e,t,n){var r=dt(e),l={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null};if(Qu(e))Ku(t,l);else{var s=e.alternate;if(e.lanes===0&&(s===null||s.lanes===0)&&(s=t.lastRenderedReducer,s!==null))try{var i=t.lastRenderedState,a=s(i,n);if(l.hasEagerState=!0,l.eagerState=a,De(a,i)){var u=t.interleaved;u===null?(l.next=l,Ei(t)):(l.next=u.next,u.next=l),t.interleaved=l;return}}catch{}finally{}n=Eu(e,t,l,r),n!==null&&(l=ue(),Fe(n,e,r,l),Gu(n,t,r))}}function Qu(e){var t=e.alternate;return e===W||t!==null&&t===W}function Ku(e,t){In=sl=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function Gu(e,t,n){if(n&4194240){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,fi(e,n)}}var il={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 $e().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,Dr(4194308,4,Uu.bind(null,t,e),n)},useLayoutEffect:function(e,t){return Dr(4194308,4,e,t)},useInsertionEffect:function(e,t){return Dr(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=Sf.bind(null,W,e),[r.memoizedState,e]},useRef:function(e){var t=$e();return e={current:e},t.memoizedState=e},useState:$o,useDebugValue:Fi,useDeferredValue:function(e){return $e().memoizedState=e},useTransition:function(){var e=$o(!1),t=e[0];return e=jf.bind(null,e[1]),$e().memoizedState=e,[t,e]},useMutableSource:function(){},useSyncExternalStore:function(e,t,n){var r=W,l=$e();if(B){if(n===void 0)throw Error(k(407));n=n()}else{if(n=t(),ee===null)throw Error(k(349));Lt&30||Lu(r,t,n)}l.memoizedState=n;var s={value:n,getSnapshot:t};return l.queue=s,Uo(Ou.bind(null,r,s,e),[e]),r.flags|=2048,bn(9,Ru.bind(null,r,s,n,t),void 0,null),n},useId:function(){var e=$e(),t=ee.identifierPrefix;if(B){var n=Qe,r=He;n=(r&~(1<<32-Ie(r)-1)).toString(32)+n,t=":"+t+"R"+n,n=Zn++,0<\/script>",e=e.removeChild(e.firstChild)):typeof r.is=="string"?e=i.createElement(n,{is:r.is}):(e=i.createElement(n),n==="select"&&(i=e,r.multiple?i.multiple=!0:r.size&&(i.size=r.size))):e=i.createElementNS(e,n),e[Ue]=t,e[Yn]=r,rc(e,t,!1,!1),t.stateNode=e;e:{switch(i=ys(n,r),n){case"dialog":U("cancel",e),U("close",e),l=r;break;case"iframe":case"object":case"embed":U("load",e),l=r;break;case"video":case"audio":for(l=0;lcn&&(t.flags|=128,r=!0,jn(s,!1),t.lanes=4194304)}else{if(!r)if(e=ll(i),e!==null){if(t.flags|=128,r=!0,n=e.updateQueue,n!==null&&(t.updateQueue=n,t.flags|=4),jn(s,!0),s.tail===null&&s.tailMode==="hidden"&&!i.alternate&&!B)return se(t),null}else 2*Y()-s.renderingStartTime>cn&&n!==1073741824&&(t.flags|=128,r=!0,jn(s,!1),t.lanes=4194304);s.isBackwards?(i.sibling=t.child,t.child=i):(n=s.last,n!==null?n.sibling=i:t.child=i,s.last=i)}return s.tail!==null?(t=s.tail,s.rendering=t,s.tail=t.sibling,s.renderingStartTime=Y(),t.sibling=null,n=V.current,M(V,r?n&1|2:n&1),t):(se(t),null);case 22:case 23:return Bi(),r=t.memoizedState!==null,e!==null&&e.memoizedState!==null!==r&&(t.flags|=8192),r&&t.mode&1?ye&1073741824&&(se(t),t.subtreeFlags&6&&(t.flags|=8192)):se(t),null;case 24:return null;case 25:return null}throw Error(k(156,t.tag))}function Of(e,t){switch(ki(t),t.tag){case 1:return me(t.type)&&Zr(),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return an(),A(he),A(oe),zi(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 5:return Ti(t),null;case 13:if(A(V),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(k(340));sn()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return A(V),null;case 4:return an(),null;case 10:return Ci(t.type._context),null;case 22:case 23:return Bi(),null;case 24:return null;default:return null}}var Er=!1,ie=!1,If=typeof WeakSet=="function"?WeakSet:Set,N=null;function Xt(e,t){var n=e.ref;if(n!==null)if(typeof n=="function")try{n(null)}catch(r){Q(e,t,r)}else n.current=null}function Hs(e,t,n){try{n()}catch(r){Q(e,t,r)}}var Jo=!1;function Ff(e,t){if(_s=Gr,e=cu(),wi(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,s=r.focusNode;r=r.focusOffset;try{n.nodeType,s.nodeType}catch{n=null;break e}var i=0,a=-1,u=-1,c=0,v=0,m=e,h=null;t:for(;;){for(var y;m!==n||l!==0&&m.nodeType!==3||(a=i+l),m!==s||r!==0&&m.nodeType!==3||(u=i+r),m.nodeType===3&&(i+=m.nodeValue.length),(y=m.firstChild)!==null;)h=m,m=y;for(;;){if(m===e)break t;if(h===n&&++c===l&&(a=i),h===s&&++v===r&&(u=i),(y=m.nextSibling)!==null)break;m=h,h=m.parentNode}m=y}n=a===-1||u===-1?null:{start:a,end:u}}else n=null}n=n||{start:0,end:0}}else n=null;for(Ps={focusedElem:e,selectionRange:n},Gr=!1,N=t;N!==null;)if(t=N,e=t.child,(t.subtreeFlags&1028)!==0&&e!==null)e.return=t,N=e;else for(;N!==null;){t=N;try{var x=t.alternate;if(t.flags&1024)switch(t.tag){case 0:case 11:case 15:break;case 1:if(x!==null){var g=x.memoizedProps,T=x.memoizedState,p=t.stateNode,d=p.getSnapshotBeforeUpdate(t.elementType===t.type?g:Le(t.type,g),T);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(w){Q(t,t.return,w)}if(e=t.sibling,e!==null){e.return=t.return,N=e;break}N=t.return}return x=Jo,Jo=!1,x}function Fn(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 s=l.destroy;l.destroy=void 0,s!==void 0&&Hs(t,n,s)}l=l.next}while(l!==r)}}function jl(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 Qs(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 ic(e){var t=e.alternate;t!==null&&(e.alternate=null,ic(t)),e.child=null,e.deletions=null,e.sibling=null,e.tag===5&&(t=e.stateNode,t!==null&&(delete t[Ue],delete t[Yn],delete t[Ls],delete t[yf],delete t[gf])),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 oc(e){return e.tag===5||e.tag===3||e.tag===4}function Zo(e){e:for(;;){for(;e.sibling===null;){if(e.return===null||oc(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 Ks(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=Jr));else if(r!==4&&(e=e.child,e!==null))for(Ks(e,t,n),e=e.sibling;e!==null;)Ks(e,t,n),e=e.sibling}function Gs(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(Gs(e,t,n),e=e.sibling;e!==null;)Gs(e,t,n),e=e.sibling}var te=null,Re=!1;function qe(e,t,n){for(n=n.child;n!==null;)ac(e,t,n),n=n.sibling}function ac(e,t,n){if(Ae&&typeof Ae.onCommitFiberUnmount=="function")try{Ae.onCommitFiberUnmount(hl,n)}catch{}switch(n.tag){case 5:ie||Xt(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?Xl(e.parentNode,n):e.nodeType===1&&Xl(e,n),Wn(e)):Xl(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(!ie&&(r=n.updateQueue,r!==null&&(r=r.lastEffect,r!==null))){l=r=r.next;do{var s=l,i=s.destroy;s=s.tag,i!==void 0&&(s&2||s&4)&&Hs(n,t,i),l=l.next}while(l!==r)}qe(e,t,n);break;case 1:if(!ie&&(Xt(n,t),r=n.stateNode,typeof r.componentWillUnmount=="function"))try{r.props=n.memoizedProps,r.state=n.memoizedState,r.componentWillUnmount()}catch(a){Q(n,t,a)}qe(e,t,n);break;case 21:qe(e,t,n);break;case 22:n.mode&1?(ie=(r=ie)||n.memoizedState!==null,qe(e,t,n),ie=r):qe(e,t,n);break;default:qe(e,t,n)}}function qo(e){var t=e.updateQueue;if(t!==null){e.updateQueue=null;var n=e.stateNode;n===null&&(n=e.stateNode=new If),t.forEach(function(r){var l=Hf.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=i),r&=~s}if(r=l,r=Y()-r,r=(120>r?120:480>r?480:1080>r?1080:1920>r?1920:3e3>r?3e3:4320>r?4320:1960*Mf(r/1960))-r,10e?16:e,lt===null)var r=!1;else{if(e=lt,lt=null,ul=0,I&6)throw Error(k(331));var l=I;for(I|=4,N=e.current;N!==null;){var s=N,i=s.child;if(N.flags&16){var a=s.deletions;if(a!==null){for(var u=0;uY()-Ui?Et(e,0):$i|=n),ve(e,t)}function vc(e,t){t===0&&(e.mode&1?(t=yr,yr<<=1,!(yr&130023424)&&(yr=4194304)):t=1);var n=ue();e=Xe(e,t),e!==null&&(nr(e,t,n),ve(e,n))}function Wf(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),vc(e,n)}function Hf(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),vc(e,n)}var yc;yc=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,Lf(e,t,n);pe=!!(e.flags&131072)}else pe=!1,B&&t.flags&1048576&&ku(t,el,t.index);switch(t.lanes=0,t.tag){case 2:var r=t.type;Mr(e,t),e=t.pendingProps;var l=ln(t,oe.current);tn(t,n),l=Ri(null,t,r,e,l,n);var s=Oi();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)?(s=!0,qr(t)):s=!1,t.memoizedState=l.state!==null&&l.state!==void 0?l.state:null,_i(t),l.updater=kl,t.stateNode=l,l._reactInternals=t,Ms(t,r,e,n),t=As(null,t,r,!0,s,n)):(t.tag=0,B&&s&&xi(t),ae(null,t,l,n),t=t.child),t;case 16:r=t.elementType;e:{switch(Mr(e,t),e=t.pendingProps,l=r._init,r=l(r._payload),t.type=r,l=t.tag=Kf(r),e=Le(r,e),l){case 0:t=Us(null,t,r,e,n);break e;case 1:t=Go(null,t,r,e,n);break e;case 11:t=Qo(null,t,r,e,n);break e;case 14:t=Ko(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),Us(e,t,r,l,n);case 1:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Le(r,l),Go(e,t,r,l,n);case 3:e:{if(ec(t),e===null)throw Error(k(387));r=t.pendingProps,s=t.memoizedState,l=s.element,_u(e,t),rl(t,r,null,n);var i=t.memoizedState;if(r=i.element,s.isDehydrated)if(s={element:r,isDehydrated:!1,cache:i.cache,pendingSuspenseBoundaries:i.pendingSuspenseBoundaries,transitions:i.transitions},t.updateQueue.baseState=s,t.memoizedState=s,t.flags&256){l=un(Error(k(423)),t),t=Yo(e,t,r,n,l);break e}else if(r!==l){l=un(Error(k(424)),t),t=Yo(e,t,r,n,l);break e}else for(ge=at(t.stateNode.containerInfo.firstChild),we=t,B=!0,Oe=null,n=Cu(t,null,r,n),t.child=n;n;)n.flags=n.flags&-3|4096,n=n.sibling;else{if(sn(),r===l){t=Je(e,t,n);break e}ae(e,t,r,n)}t=t.child}return t;case 5:return Pu(t),e===null&&Is(t),r=t.type,l=t.pendingProps,s=e!==null?e.memoizedProps:null,i=l.children,Ts(r,l)?i=null:s!==null&&Ts(r,s)&&(t.flags|=32),bu(e,t),ae(e,t,i,n),t.child;case 6:return e===null&&Is(t),null;case 13:return tc(e,t,n);case 4:return Pi(t,t.stateNode.containerInfo),r=t.pendingProps,e===null?t.child=on(t,null,r,n):ae(e,t,r,n),t.child;case 11:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Le(r,l),Qo(e,t,r,l,n);case 7:return ae(e,t,t.pendingProps,n),t.child;case 8:return ae(e,t,t.pendingProps.children,n),t.child;case 12:return ae(e,t,t.pendingProps.children,n),t.child;case 10:e:{if(r=t.type._context,l=t.pendingProps,s=t.memoizedProps,i=l.value,M(tl,r._currentValue),r._currentValue=i,s!==null)if(De(s.value,i)){if(s.children===l.children&&!he.current){t=Je(e,t,n);break e}}else for(s=t.child,s!==null&&(s.return=t);s!==null;){var a=s.dependencies;if(a!==null){i=s.child;for(var u=a.firstContext;u!==null;){if(u.context===r){if(s.tag===1){u=Ke(-1,n&-n),u.tag=2;var c=s.updateQueue;if(c!==null){c=c.shared;var v=c.pending;v===null?u.next=u:(u.next=v.next,v.next=u),c.pending=u}}s.lanes|=n,u=s.alternate,u!==null&&(u.lanes|=n),Fs(s.return,n,t),a.lanes|=n;break}u=u.next}}else if(s.tag===10)i=s.type===t.type?null:s.child;else if(s.tag===18){if(i=s.return,i===null)throw Error(k(341));i.lanes|=n,a=i.alternate,a!==null&&(a.lanes|=n),Fs(i,n,t),i=s.sibling}else i=s.child;if(i!==null)i.return=s;else for(i=s;i!==null;){if(i===t){i=null;break}if(s=i.sibling,s!==null){s.return=i.return,i=s;break}i=i.return}s=i}ae(e,t,l.children,n),t=t.child}return t;case 9:return l=t.type,r=t.pendingProps.children,tn(t,n),l=_e(l),r=r(l),t.flags|=1,ae(e,t,r,n),t.child;case 14:return r=t.type,l=Le(r,t.pendingProps),l=Le(r.type,l),Ko(e,t,r,l,n);case 15:return Zu(e,t,t.type,t.pendingProps,n);case 17:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Le(r,l),Mr(e,t),t.tag=1,me(r)?(e=!0,qr(t)):e=!1,tn(t,n),Yu(t,r,l),Ms(t,r,l,n),As(null,t,r,!0,e,n);case 19:return nc(e,t,n);case 22:return qu(e,t,n)}throw Error(k(156,t.tag))};function gc(e,t){return Qa(e,t)}function Qf(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 Ce(e,t,n,r){return new Qf(e,t,n,r)}function Wi(e){return e=e.prototype,!(!e||!e.isReactComponent)}function Kf(e){if(typeof e=="function")return Wi(e)?1:0;if(e!=null){if(e=e.$$typeof,e===ai)return 11;if(e===ui)return 14}return 2}function ft(e,t){var n=e.alternate;return n===null?(n=Ce(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 Ar(e,t,n,r,l,s){var i=2;if(r=e,typeof e=="function")Wi(e)&&(i=1);else if(typeof e=="string")i=5;else e:switch(e){case At:return _t(n.children,l,s,t);case oi:i=8,l|=8;break;case os:return e=Ce(12,n,t,l|2),e.elementType=os,e.lanes=s,e;case as:return e=Ce(13,n,t,l),e.elementType=as,e.lanes=s,e;case us:return e=Ce(19,n,t,l),e.elementType=us,e.lanes=s,e;case Pa:return Nl(n,l,s,t);default:if(typeof e=="object"&&e!==null)switch(e.$$typeof){case Ea:i=10;break e;case _a:i=9;break e;case ai:i=11;break e;case ui:i=14;break e;case be:i=16,r=null;break e}throw Error(k(130,e==null?e:typeof e,""))}return t=Ce(i,n,t,l),t.elementType=e,t.type=r,t.lanes=s,t}function _t(e,t,n,r){return e=Ce(7,e,r,t),e.lanes=n,e}function Nl(e,t,n,r){return e=Ce(22,e,r,t),e.elementType=Pa,e.lanes=n,e.stateNode={isHidden:!1},e}function rs(e,t,n){return e=Ce(6,e,null,t),e.lanes=n,e}function ls(e,t,n){return t=Ce(4,e.children!==null?e.children:[],e.key,t),t.lanes=n,t.stateNode={containerInfo:e.containerInfo,pendingChildren:null,implementation:e.implementation},t}function Gf(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=$l(0),this.expirationTimes=$l(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=$l(0),this.identifierPrefix=r,this.onRecoverableError=l,this.mutableSourceEagerHydrationData=null}function Hi(e,t,n,r,l,s,i,a,u){return e=new Gf(e,t,n,a,u),t===1?(t=1,s===!0&&(t|=8)):t=0,s=Ce(3,null,null,t),e.current=s,s.stateNode=e,s.memoizedState={element:r,isDehydrated:n,cache:null,transitions:null,pendingSuspenseBoundaries:null},_i(s),e}function Yf(e,t,n){var r=3"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(jc)}catch(e){console.error(e)}}jc(),ja.exports=ke;var bf=ja.exports,Sc,ia=bf;Sc=ia.createRoot,ia.hydrateRoot;const Nc="bv.access",Yi="bv.refresh";function qs(e){try{return localStorage.getItem(e)||""}catch{return""}}function oa(e,t){try{t?localStorage.setItem(e,t):localStorage.removeItem(e)}catch{}}function ir(){return{access:qs(Nc),refresh:qs(Yi)}}function fl(e,t){oa(Nc,e),oa(Yi,t)}function or(){fl("","")}function ep(){return!!qs(Yi)}class Pt extends Error{constructor(t,n,r){super(r),this.status=t,this.code=n}}let Nn=null;async function Tl(){return Nn||(Nn=(async()=>{const{refresh:e}=ir();if(!e)throw new Pt(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 or(),new Pt(t.status,"unauthorized","Your session has ended. Sign in again.");const n=await t.json();return fl(n.access_token,n.refresh_token),n})().finally(()=>{Nn=null}),Nn)}async function D(e,t,n,r=!0){const{access:l}=ir(),s={};l&&(s.Authorization="Bearer "+l),n!==void 0&&(s["Content-Type"]="application/json");const i=await fetch(t,{method:e,headers:s,body:n===void 0?void 0:JSON.stringify(n)});if(i.status===204)return null;const a=await i.text();let u=null;try{u=a?JSON.parse(a):null}catch{}if(i.ok)return u;const c=(u==null?void 0:u.error)||"";if(c==="token_expired"&&r)return await Tl(),D(e,t,n,!1);throw i.status===401&&or(),new Pt(i.status,c,(u==null?void 0:u.message)||`Something went wrong (${i.status}).`)}async function Cc(e,t=!0){const{access:n}=ir(),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 s=(l==null?void 0:l.error)||"";if(s==="token_expired"&&t)return await Tl(),Cc(e,!1);throw r.status===401&&or(),new Pt(r.status,s,(l==null?void 0:l.message)||`That picture could not be loaded (${r.status}).`)}function aa(){const e=navigator.userAgent||"",t=/Windows/.test(e)?"Windows":/Mac OS X|Macintosh/.test(e)?"Mac":/Android/.test(e)?"Android":/iPhone|iPad/.test(e)?"iOS":"Unknown";return`${/Edg\//.test(e)?"Edge":/Chrome\//.test(e)?"Chrome":/Safari\//.test(e)?"Safari":/Firefox\//.test(e)?"Firefox":"browser"} on ${t}`}const jt=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:""},$={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,device:aa()})}),r=await n.json().catch(()=>null);if(!n.ok)throw new Pt(n.status,(r==null?void 0:r.error)||"",(r==null?void 0:r.message)||"Could not sign in.");return fl(r.access_token,r.refresh_token),r.user},async previewInvitation(e){const t=await fetch("/api/auth/invitation"+jt({code:e})),n=await t.json().catch(()=>null);if(!t.ok)throw new Pt(t.status,(n==null?void 0:n.error)||"",(n==null?void 0:n.message)||"That invitation code is not valid.");return n},async register({code:e,full_name:t,password:n}){const r=await fetch("/api/auth/register",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({code:e,full_name:t,password:n,device:aa()})}),l=await r.json().catch(()=>null);if(!r.ok)throw new Pt(r.status,(l==null?void 0:l.error)||"",(l==null?void 0:l.message)||"Could not create the account.");return fl(l.access_token,l.refresh_token),l.user},async logout(){try{await D("POST","/api/auth/logout")}catch{}or()},me:()=>D("GET","/api/auth/me"),sites:()=>D("GET","/api/sites"),arrivals:e=>D("GET","/api/visits"+jt(e)),visitors:(e,t=50)=>D("GET","/api/visitors"+jt({q:e,limit:t})),visitorHistory:(e,t=50)=>D("GET",`/api/visitors/${encodeURIComponent(e)}/history`+jt({limit:t})),saveProfile:(e,t)=>D("PUT",`/api/visitors/${encodeURIComponent(e)}/profile`,t),footfall:e=>D("GET","/api/reports/footfall"+jt(e)),conversion:e=>D("GET","/api/reports/conversion"+jt(e)),cameras:()=>D("GET","/api/cameras"),cameraSnapshot:e=>Cc(e),createCamera:(e,t)=>D("POST",`/api/sites/${encodeURIComponent(e)}/cameras`,t),updateCamera:(e,t)=>D("PATCH",`/api/cameras/${encodeURIComponent(e)}`,t),deleteCamera:e=>D("DELETE",`/api/cameras/${encodeURIComponent(e)}`),checkCamera:(e,t,n)=>D("POST",`/api/cameras/${encodeURIComponent(e)}/check`,{kind:t,...n?{seconds:n}:{}}),siteCheck:e=>D("GET",`/api/sites/${encodeURIComponent(e)}/check`),enrolmentCode:(e,t)=>D("POST",`/api/sites/${encodeURIComponent(e)}/enrolment-code`,t||{}),ask:e=>D("POST","/api/assistant",{history:e}),clients:()=>D("GET","/api/admin/clients"),createClient:e=>D("POST","/api/admin/clients",e),team:()=>D("GET","/api/team"),updateMember:(e,t)=>D("PATCH",`/api/team/${encodeURIComponent(e)}`,t),invitations:()=>D("GET","/api/team/invitations"),invite:e=>D("POST","/api/team/invitations",e),revokeInvitation:e=>D("DELETE",`/api/team/invitations/${encodeURIComponent(e)}`),sessions:()=>D("GET","/api/auth/sessions"),revokeSession:e=>D("DELETE",`/api/auth/sessions/${encodeURIComponent(e)}`),signOutOthers:()=>D("POST","/api/auth/sessions/revoke-others")};function tp({cursor:e,siteId:t,onPage:n,onError:r,signal:l}){let s=!1,i=e||"";return(async()=>{for(;!s;){try{const{access:u}=ir(),c=await fetch("/api/visits/stream"+jt({cursor:i,site_id:t}),{headers:{Authorization:"Bearer "+u,Accept:"text/event-stream"},signal:l});if(c.status===401){await Tl();continue}if(!c.ok||!c.body)throw new Error("stream unavailable");const v=c.body.getReader(),m=new TextDecoder;let h="";for(;!s;){const{value:y,done:x}=await v.read();if(x)break;h+=m.decode(y,{stream:!0});let g;for(;(g=h.indexOf(` +`+s.stack}return{value:e,source:t,stack:l,digest:null}}function ts(e,t,n){return{value:e,source:null,stack:n??null,digest:t??null}}function $s(e,t){try{console.error(t.value)}catch(n){setTimeout(function(){throw n})}}var Pf=typeof WeakMap=="function"?WeakMap:Map;function Xu(e,t,n){n=Ke(-1,n),n.tag=3,n.payload={element:null};var r=t.value;return n.callback=function(){al||(al=!0,Ys=r),$s(e,t)},n}function Ju(e,t,n){n=Ke(-1,n),n.tag=3;var r=e.type.getDerivedStateFromError;if(typeof r=="function"){var l=t.value;n.payload=function(){return r(l)},n.callback=function(){$s(e,t)}}var s=e.stateNode;return s!==null&&typeof s.componentDidCatch=="function"&&(n.callback=function(){$s(e,t),typeof r!="function"&&(ct===null?ct=new Set([this]):ct.add(this));var i=t.stack;this.componentDidCatch(t.value,{componentStack:i!==null?i:""})}),n}function Vo(e,t,n){var r=e.pingCache;if(r===null){r=e.pingCache=new Pf;var l=new Set;r.set(t,l)}else l=r.get(t),l===void 0&&(l=new Set,r.set(t,l));l.has(n)||(l.add(n),e=Vf.bind(null,e,t,n),t.then(e,e))}function Wo(e){do{var t;if((t=e.tag===13)&&(t=e.memoizedState,t=t!==null?t.dehydrated!==null:!0),t)return e;e=e.return}while(e!==null);return null}function Ho(e,t,n,r,l){return e.mode&1?(e.flags|=65536,e.lanes=l,e):(e===t?e.flags|=65536:(e.flags|=128,n.flags|=131072,n.flags&=-52805,n.tag===1&&(n.alternate===null?n.tag=17:(t=Ke(-1,1),t.tag=2,ut(n,t,1))),n.lanes|=1),e)}var Tf=Ze.ReactCurrentOwner,pe=!1;function ae(e,t,n,r){t.child=e===null?Cu(t,null,n,r):on(t,e.child,n,r)}function Qo(e,t,n,r,l){n=n.render;var s=t.ref;return tn(t,l),r=Ri(e,t,n,r,s,l),n=Oi(),e!==null&&!pe?(t.updateQueue=e.updateQueue,t.flags&=-2053,e.lanes&=~l,Je(e,t,l)):(B&&n&&xi(t),t.flags|=1,ae(e,t,r,l),t.child)}function Ko(e,t,n,r,l){if(e===null){var s=n.type;return typeof s=="function"&&!Wi(s)&&s.defaultProps===void 0&&n.compare===null&&n.defaultProps===void 0?(t.tag=15,t.type=s,Zu(e,t,s,r,l)):(e=Ar(n.type,null,r,t,t.mode,l),e.ref=t.ref,e.return=t,t.child=e)}if(s=e.child,!(e.lanes&l)){var i=s.memoizedProps;if(n=n.compare,n=n!==null?n:Qn,n(i,r)&&e.ref===t.ref)return Je(e,t,l)}return t.flags|=1,e=ft(s,r),e.ref=t.ref,e.return=t,t.child=e}function Zu(e,t,n,r,l){if(e!==null){var s=e.memoizedProps;if(Qn(s,r)&&e.ref===t.ref)if(pe=!1,t.pendingProps=r=s,(e.lanes&l)!==0)e.flags&131072&&(pe=!0);else return t.lanes=e.lanes,Je(e,t,l)}return Us(e,t,n,r,l)}function qu(e,t,n){var r=t.pendingProps,l=r.children,s=e!==null?e.memoizedState:null;if(r.mode==="hidden")if(!(t.mode&1))t.memoizedState={baseLanes:0,cachePool:null,transitions:null},$(Jt,ye),ye|=n;else{if(!(n&1073741824))return e=s!==null?s.baseLanes|n:n,t.lanes=t.childLanes=1073741824,t.memoizedState={baseLanes:e,cachePool:null,transitions:null},t.updateQueue=null,$(Jt,ye),ye|=e,null;t.memoizedState={baseLanes:0,cachePool:null,transitions:null},r=s!==null?s.baseLanes:n,$(Jt,ye),ye|=r}else s!==null?(r=s.baseLanes|n,t.memoizedState=null):r=n,$(Jt,ye),ye|=r;return ae(e,t,l,n),t.child}function bu(e,t){var n=t.ref;(e===null&&n!==null||e!==null&&e.ref!==n)&&(t.flags|=512,t.flags|=2097152)}function Us(e,t,n,r,l){var s=me(n)?Tt:oe.current;return s=ln(t,s),tn(t,l),n=Ri(e,t,n,r,s,l),r=Oi(),e!==null&&!pe?(t.updateQueue=e.updateQueue,t.flags&=-2053,e.lanes&=~l,Je(e,t,l)):(B&&r&&xi(t),t.flags|=1,ae(e,t,n,l),t.child)}function Go(e,t,n,r,l){if(me(n)){var s=!0;qr(t)}else s=!1;if(tn(t,l),t.stateNode===null)Mr(e,t),Yu(t,n,r),Ms(t,n,r,l),r=!0;else if(e===null){var i=t.stateNode,a=t.memoizedProps;i.props=a;var u=i.context,c=n.contextType;typeof c=="object"&&c!==null?c=_e(c):(c=me(n)?Tt:oe.current,c=ln(t,c));var v=n.getDerivedStateFromProps,h=typeof v=="function"||typeof i.getSnapshotBeforeUpdate=="function";h||typeof i.UNSAFE_componentWillReceiveProps!="function"&&typeof i.componentWillReceiveProps!="function"||(a!==r||u!==c)&&Bo(t,i,r,c),et=!1;var m=t.memoizedState;i.state=m,rl(t,r,i,l),u=t.memoizedState,a!==r||m!==u||he.current||et?(typeof v=="function"&&(Ds(t,n,v,r),u=t.memoizedState),(a=et||Ao(t,n,a,r,m,u,c))?(h||typeof i.UNSAFE_componentWillMount!="function"&&typeof i.componentWillMount!="function"||(typeof i.componentWillMount=="function"&&i.componentWillMount(),typeof i.UNSAFE_componentWillMount=="function"&&i.UNSAFE_componentWillMount()),typeof i.componentDidMount=="function"&&(t.flags|=4194308)):(typeof i.componentDidMount=="function"&&(t.flags|=4194308),t.memoizedProps=r,t.memoizedState=u),i.props=r,i.state=u,i.context=c,r=a):(typeof i.componentDidMount=="function"&&(t.flags|=4194308),r=!1)}else{i=t.stateNode,_u(e,t),a=t.memoizedProps,c=t.type===t.elementType?a:Le(t.type,a),i.props=c,h=t.pendingProps,m=i.context,u=n.contextType,typeof u=="object"&&u!==null?u=_e(u):(u=me(n)?Tt:oe.current,u=ln(t,u));var y=n.getDerivedStateFromProps;(v=typeof y=="function"||typeof i.getSnapshotBeforeUpdate=="function")||typeof i.UNSAFE_componentWillReceiveProps!="function"&&typeof i.componentWillReceiveProps!="function"||(a!==h||m!==u)&&Bo(t,i,r,u),et=!1,m=t.memoizedState,i.state=m,rl(t,r,i,l);var x=t.memoizedState;a!==h||m!==x||he.current||et?(typeof y=="function"&&(Ds(t,n,y,r),x=t.memoizedState),(c=et||Ao(t,n,c,r,m,x,u)||!1)?(v||typeof i.UNSAFE_componentWillUpdate!="function"&&typeof i.componentWillUpdate!="function"||(typeof i.componentWillUpdate=="function"&&i.componentWillUpdate(r,x,u),typeof i.UNSAFE_componentWillUpdate=="function"&&i.UNSAFE_componentWillUpdate(r,x,u)),typeof i.componentDidUpdate=="function"&&(t.flags|=4),typeof i.getSnapshotBeforeUpdate=="function"&&(t.flags|=1024)):(typeof i.componentDidUpdate!="function"||a===e.memoizedProps&&m===e.memoizedState||(t.flags|=4),typeof i.getSnapshotBeforeUpdate!="function"||a===e.memoizedProps&&m===e.memoizedState||(t.flags|=1024),t.memoizedProps=r,t.memoizedState=x),i.props=r,i.state=x,i.context=u,r=c):(typeof i.componentDidUpdate!="function"||a===e.memoizedProps&&m===e.memoizedState||(t.flags|=4),typeof i.getSnapshotBeforeUpdate!="function"||a===e.memoizedProps&&m===e.memoizedState||(t.flags|=1024),r=!1)}return As(e,t,n,r,s,l)}function As(e,t,n,r,l,s){bu(e,t);var i=(t.flags&128)!==0;if(!r&&!i)return l&&Ro(t,n,!1),Je(e,t,s);r=t.stateNode,Tf.current=t;var a=i&&typeof n.getDerivedStateFromError!="function"?null:r.render();return t.flags|=1,e!==null&&i?(t.child=on(t,e.child,null,s),t.child=on(t,null,a,s)):ae(e,t,a,s),t.memoizedState=r.state,l&&Ro(t,n,!0),t.child}function ec(e){var t=e.stateNode;t.pendingContext?Lo(e,t.pendingContext,t.pendingContext!==t.context):t.context&&Lo(e,t.context,!1),Pi(e,t.containerInfo)}function Yo(e,t,n,r,l){return sn(),ji(l),t.flags|=256,ae(e,t,n,r),t.child}var Bs={dehydrated:null,treeContext:null,retryLane:0};function Vs(e){return{baseLanes:e,cachePool:null,transitions:null}}function tc(e,t,n){var r=t.pendingProps,l=V.current,s=!1,i=(t.flags&128)!==0,a;if((a=i)||(a=e!==null&&e.memoizedState===null?!1:(l&2)!==0),a?(s=!0,t.flags&=-129):(e===null||e.memoizedState!==null)&&(l|=1),$(V,l&1),e===null)return Is(t),e=t.memoizedState,e!==null&&(e=e.dehydrated,e!==null)?(t.mode&1?e.data==="$!"?t.lanes=8:t.lanes=1073741824:t.lanes=1,null):(i=r.children,e=r.fallback,s?(r=t.mode,s=t.child,i={mode:"hidden",children:i},!(r&1)&&s!==null?(s.childLanes=0,s.pendingProps=i):s=Nl(i,r,0,null),e=_t(e,r,n,null),s.return=t,e.return=t,s.sibling=e,t.child=s,t.child.memoizedState=Vs(n),t.memoizedState=Bs,e):Di(t,i));if(l=e.memoizedState,l!==null&&(a=l.dehydrated,a!==null))return zf(e,t,i,r,a,l,n);if(s){s=r.fallback,i=t.mode,l=e.child,a=l.sibling;var u={mode:"hidden",children:r.children};return!(i&1)&&t.child!==l?(r=t.child,r.childLanes=0,r.pendingProps=u,t.deletions=null):(r=ft(l,u),r.subtreeFlags=l.subtreeFlags&14680064),a!==null?s=ft(a,s):(s=_t(s,i,n,null),s.flags|=2),s.return=t,r.return=t,r.sibling=s,t.child=r,r=s,s=t.child,i=e.child.memoizedState,i=i===null?Vs(n):{baseLanes:i.baseLanes|n,cachePool:null,transitions:i.transitions},s.memoizedState=i,s.childLanes=e.childLanes&~n,t.memoizedState=Bs,r}return s=e.child,e=s.sibling,r=ft(s,{mode:"visible",children:r.children}),!(t.mode&1)&&(r.lanes=n),r.return=t,r.sibling=null,e!==null&&(n=t.deletions,n===null?(t.deletions=[e],t.flags|=16):n.push(e)),t.child=r,t.memoizedState=null,r}function Di(e,t){return t=Nl({mode:"visible",children:t},e.mode,0,null),t.return=e,e.child=t}function Cr(e,t,n,r){return r!==null&&ji(r),on(t,e.child,null,n),e=Di(t,t.pendingProps.children),e.flags|=2,t.memoizedState=null,e}function zf(e,t,n,r,l,s,i){if(n)return t.flags&256?(t.flags&=-257,r=ts(Error(k(422))),Cr(e,t,i,r)):t.memoizedState!==null?(t.child=e.child,t.flags|=128,null):(s=r.fallback,l=t.mode,r=Nl({mode:"visible",children:r.children},l,0,null),s=_t(s,l,i,null),s.flags|=2,r.return=t,s.return=t,r.sibling=s,t.child=r,t.mode&1&&on(t,e.child,null,i),t.child.memoizedState=Vs(i),t.memoizedState=Bs,s);if(!(t.mode&1))return Cr(e,t,i,null);if(l.data==="$!"){if(r=l.nextSibling&&l.nextSibling.dataset,r)var a=r.dgst;return r=a,s=Error(k(419)),r=ts(s,r,void 0),Cr(e,t,i,r)}if(a=(i&e.childLanes)!==0,pe||a){if(r=ee,r!==null){switch(i&-i){case 4:l=2;break;case 16:l=8;break;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:case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:l=32;break;case 536870912:l=268435456;break;default:l=0}l=l&(r.suspendedLanes|i)?0:l,l!==0&&l!==s.retryLane&&(s.retryLane=l,Xe(e,l),Fe(r,e,l,-1))}return Vi(),r=ts(Error(k(421))),Cr(e,t,i,r)}return l.data==="$?"?(t.flags|=128,t.child=e.child,t=Wf.bind(null,e),l._reactRetry=t,null):(e=s.treeContext,ge=at(l.nextSibling),we=t,B=!0,Oe=null,e!==null&&(Se[Ne++]=He,Se[Ne++]=Qe,Se[Ne++]=zt,He=e.id,Qe=e.overflow,zt=t),t=Di(t,r.children),t.flags|=4096,t)}function Xo(e,t,n){e.lanes|=t;var r=e.alternate;r!==null&&(r.lanes|=t),Fs(e.return,t,n)}function ns(e,t,n,r,l){var s=e.memoizedState;s===null?e.memoizedState={isBackwards:t,rendering:null,renderingStartTime:0,last:r,tail:n,tailMode:l}:(s.isBackwards=t,s.rendering=null,s.renderingStartTime=0,s.last=r,s.tail=n,s.tailMode=l)}function nc(e,t,n){var r=t.pendingProps,l=r.revealOrder,s=r.tail;if(ae(e,t,r.children,n),r=V.current,r&2)r=r&1|2,t.flags|=128;else{if(e!==null&&e.flags&128)e:for(e=t.child;e!==null;){if(e.tag===13)e.memoizedState!==null&&Xo(e,n,t);else if(e.tag===19)Xo(e,n,t);else if(e.child!==null){e.child.return=e,e=e.child;continue}if(e===t)break e;for(;e.sibling===null;){if(e.return===null||e.return===t)break e;e=e.return}e.sibling.return=e.return,e=e.sibling}r&=1}if($(V,r),!(t.mode&1))t.memoizedState=null;else switch(l){case"forwards":for(n=t.child,l=null;n!==null;)e=n.alternate,e!==null&&ll(e)===null&&(l=n),n=n.sibling;n=l,n===null?(l=t.child,t.child=null):(l=n.sibling,n.sibling=null),ns(t,!1,l,n,s);break;case"backwards":for(n=null,l=t.child,t.child=null;l!==null;){if(e=l.alternate,e!==null&&ll(e)===null){t.child=l;break}e=l.sibling,l.sibling=n,n=l,l=e}ns(t,!0,n,null,s);break;case"together":ns(t,!1,null,null,void 0);break;default:t.memoizedState=null}return t.child}function Mr(e,t){!(t.mode&1)&&e!==null&&(e.alternate=null,t.alternate=null,t.flags|=2)}function Je(e,t,n){if(e!==null&&(t.dependencies=e.dependencies),Rt|=t.lanes,!(n&t.childLanes))return null;if(e!==null&&t.child!==e.child)throw Error(k(153));if(t.child!==null){for(e=t.child,n=ft(e,e.pendingProps),t.child=n,n.return=t;e.sibling!==null;)e=e.sibling,n=n.sibling=ft(e,e.pendingProps),n.return=t;n.sibling=null}return t.child}function Lf(e,t,n){switch(t.tag){case 3:ec(t),sn();break;case 5:Pu(t);break;case 1:me(t.type)&&qr(t);break;case 4:Pi(t,t.stateNode.containerInfo);break;case 10:var r=t.type._context,l=t.memoizedProps.value;$(tl,r._currentValue),r._currentValue=l;break;case 13:if(r=t.memoizedState,r!==null)return r.dehydrated!==null?($(V,V.current&1),t.flags|=128,null):n&t.child.childLanes?tc(e,t,n):($(V,V.current&1),e=Je(e,t,n),e!==null?e.sibling:null);$(V,V.current&1);break;case 19:if(r=(n&t.childLanes)!==0,e.flags&128){if(r)return nc(e,t,n);t.flags|=128}if(l=t.memoizedState,l!==null&&(l.rendering=null,l.tail=null,l.lastEffect=null),$(V,V.current),r)break;return null;case 22:case 23:return t.lanes=0,qu(e,t,n)}return Je(e,t,n)}var rc,Ws,lc,sc;rc=function(e,t){for(var n=t.child;n!==null;){if(n.tag===5||n.tag===6)e.appendChild(n.stateNode);else if(n.tag!==4&&n.child!==null){n.child.return=n,n=n.child;continue}if(n===t)break;for(;n.sibling===null;){if(n.return===null||n.return===t)return;n=n.return}n.sibling.return=n.return,n=n.sibling}};Ws=function(){};lc=function(e,t,n,r){var l=e.memoizedProps;if(l!==r){e=t.stateNode,Ct(Be.current);var s=null;switch(n){case"input":l=ds(e,l),r=ds(e,r),s=[];break;case"select":l=H({},l,{value:void 0}),r=H({},r,{value:void 0}),s=[];break;case"textarea":l=hs(e,l),r=hs(e,r),s=[];break;default:typeof l.onClick!="function"&&typeof r.onClick=="function"&&(e.onclick=Jr)}vs(n,r);var i;n=null;for(c in l)if(!r.hasOwnProperty(c)&&l.hasOwnProperty(c)&&l[c]!=null)if(c==="style"){var a=l[c];for(i in a)a.hasOwnProperty(i)&&(n||(n={}),n[i]="")}else c!=="dangerouslySetInnerHTML"&&c!=="children"&&c!=="suppressContentEditableWarning"&&c!=="suppressHydrationWarning"&&c!=="autoFocus"&&($n.hasOwnProperty(c)?s||(s=[]):(s=s||[]).push(c,null));for(c in r){var u=r[c];if(a=l!=null?l[c]:void 0,r.hasOwnProperty(c)&&u!==a&&(u!=null||a!=null))if(c==="style")if(a){for(i in a)!a.hasOwnProperty(i)||u&&u.hasOwnProperty(i)||(n||(n={}),n[i]="");for(i in u)u.hasOwnProperty(i)&&a[i]!==u[i]&&(n||(n={}),n[i]=u[i])}else n||(s||(s=[]),s.push(c,n)),n=u;else c==="dangerouslySetInnerHTML"?(u=u?u.__html:void 0,a=a?a.__html:void 0,u!=null&&a!==u&&(s=s||[]).push(c,u)):c==="children"?typeof u!="string"&&typeof u!="number"||(s=s||[]).push(c,""+u):c!=="suppressContentEditableWarning"&&c!=="suppressHydrationWarning"&&($n.hasOwnProperty(c)?(u!=null&&c==="onScroll"&&U("scroll",e),s||a===u||(s=[])):(s=s||[]).push(c,u))}n&&(s=s||[]).push("style",n);var c=s;(t.updateQueue=c)&&(t.flags|=4)}};sc=function(e,t,n,r){n!==r&&(t.flags|=4)};function jn(e,t){if(!B)switch(e.tailMode){case"hidden":t=e.tail;for(var n=null;t!==null;)t.alternate!==null&&(n=t),t=t.sibling;n===null?e.tail=null:n.sibling=null;break;case"collapsed":n=e.tail;for(var r=null;n!==null;)n.alternate!==null&&(r=n),n=n.sibling;r===null?t||e.tail===null?e.tail=null:e.tail.sibling=null:r.sibling=null}}function se(e){var t=e.alternate!==null&&e.alternate.child===e.child,n=0,r=0;if(t)for(var l=e.child;l!==null;)n|=l.lanes|l.childLanes,r|=l.subtreeFlags&14680064,r|=l.flags&14680064,l.return=e,l=l.sibling;else for(l=e.child;l!==null;)n|=l.lanes|l.childLanes,r|=l.subtreeFlags,r|=l.flags,l.return=e,l=l.sibling;return e.subtreeFlags|=r,e.childLanes=n,t}function Rf(e,t,n){var r=t.pendingProps;switch(ki(t),t.tag){case 2:case 16:case 15:case 0:case 11:case 7:case 8:case 12:case 9:case 14:return se(t),null;case 1:return me(t.type)&&Zr(),se(t),null;case 3:return r=t.stateNode,an(),A(he),A(oe),zi(),r.pendingContext&&(r.context=r.pendingContext,r.pendingContext=null),(e===null||e.child===null)&&(Sr(t)?t.flags|=4:e===null||e.memoizedState.isDehydrated&&!(t.flags&256)||(t.flags|=1024,Oe!==null&&(Zs(Oe),Oe=null))),Ws(e,t),se(t),null;case 5:Ti(t);var l=Ct(Jn.current);if(n=t.type,e!==null&&t.stateNode!=null)lc(e,t,n,r,l),e.ref!==t.ref&&(t.flags|=512,t.flags|=2097152);else{if(!r){if(t.stateNode===null)throw Error(k(166));return se(t),null}if(e=Ct(Be.current),Sr(t)){r=t.stateNode,n=t.type;var s=t.memoizedProps;switch(r[Ue]=t,r[Yn]=s,e=(t.mode&1)!==0,n){case"dialog":U("cancel",r),U("close",r);break;case"iframe":case"object":case"embed":U("load",r);break;case"video":case"audio":for(l=0;l<\/script>",e=e.removeChild(e.firstChild)):typeof r.is=="string"?e=i.createElement(n,{is:r.is}):(e=i.createElement(n),n==="select"&&(i=e,r.multiple?i.multiple=!0:r.size&&(i.size=r.size))):e=i.createElementNS(e,n),e[Ue]=t,e[Yn]=r,rc(e,t,!1,!1),t.stateNode=e;e:{switch(i=ys(n,r),n){case"dialog":U("cancel",e),U("close",e),l=r;break;case"iframe":case"object":case"embed":U("load",e),l=r;break;case"video":case"audio":for(l=0;lcn&&(t.flags|=128,r=!0,jn(s,!1),t.lanes=4194304)}else{if(!r)if(e=ll(i),e!==null){if(t.flags|=128,r=!0,n=e.updateQueue,n!==null&&(t.updateQueue=n,t.flags|=4),jn(s,!0),s.tail===null&&s.tailMode==="hidden"&&!i.alternate&&!B)return se(t),null}else 2*Y()-s.renderingStartTime>cn&&n!==1073741824&&(t.flags|=128,r=!0,jn(s,!1),t.lanes=4194304);s.isBackwards?(i.sibling=t.child,t.child=i):(n=s.last,n!==null?n.sibling=i:t.child=i,s.last=i)}return s.tail!==null?(t=s.tail,s.rendering=t,s.tail=t.sibling,s.renderingStartTime=Y(),t.sibling=null,n=V.current,$(V,r?n&1|2:n&1),t):(se(t),null);case 22:case 23:return Bi(),r=t.memoizedState!==null,e!==null&&e.memoizedState!==null!==r&&(t.flags|=8192),r&&t.mode&1?ye&1073741824&&(se(t),t.subtreeFlags&6&&(t.flags|=8192)):se(t),null;case 24:return null;case 25:return null}throw Error(k(156,t.tag))}function Of(e,t){switch(ki(t),t.tag){case 1:return me(t.type)&&Zr(),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return an(),A(he),A(oe),zi(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 5:return Ti(t),null;case 13:if(A(V),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(k(340));sn()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return A(V),null;case 4:return an(),null;case 10:return Ci(t.type._context),null;case 22:case 23:return Bi(),null;case 24:return null;default:return null}}var Er=!1,ie=!1,If=typeof WeakSet=="function"?WeakSet:Set,N=null;function Xt(e,t){var n=e.ref;if(n!==null)if(typeof n=="function")try{n(null)}catch(r){Q(e,t,r)}else n.current=null}function Hs(e,t,n){try{n()}catch(r){Q(e,t,r)}}var Jo=!1;function Ff(e,t){if(_s=Gr,e=cu(),wi(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,s=r.focusNode;r=r.focusOffset;try{n.nodeType,s.nodeType}catch{n=null;break e}var i=0,a=-1,u=-1,c=0,v=0,h=e,m=null;t:for(;;){for(var y;h!==n||l!==0&&h.nodeType!==3||(a=i+l),h!==s||r!==0&&h.nodeType!==3||(u=i+r),h.nodeType===3&&(i+=h.nodeValue.length),(y=h.firstChild)!==null;)m=h,h=y;for(;;){if(h===e)break t;if(m===n&&++c===l&&(a=i),m===s&&++v===r&&(u=i),(y=h.nextSibling)!==null)break;h=m,m=h.parentNode}h=y}n=a===-1||u===-1?null:{start:a,end:u}}else n=null}n=n||{start:0,end:0}}else n=null;for(Ps={focusedElem:e,selectionRange:n},Gr=!1,N=t;N!==null;)if(t=N,e=t.child,(t.subtreeFlags&1028)!==0&&e!==null)e.return=t,N=e;else for(;N!==null;){t=N;try{var x=t.alternate;if(t.flags&1024)switch(t.tag){case 0:case 11:case 15:break;case 1:if(x!==null){var g=x.memoizedProps,P=x.memoizedState,f=t.stateNode,d=f.getSnapshotBeforeUpdate(t.elementType===t.type?g:Le(t.type,g),P);f.__reactInternalSnapshotBeforeUpdate=d}break;case 3:var p=t.stateNode.containerInfo;p.nodeType===1?p.textContent="":p.nodeType===9&&p.documentElement&&p.removeChild(p.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(k(163))}}catch(w){Q(t,t.return,w)}if(e=t.sibling,e!==null){e.return=t.return,N=e;break}N=t.return}return x=Jo,Jo=!1,x}function Fn(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 s=l.destroy;l.destroy=void 0,s!==void 0&&Hs(t,n,s)}l=l.next}while(l!==r)}}function jl(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 Qs(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 ic(e){var t=e.alternate;t!==null&&(e.alternate=null,ic(t)),e.child=null,e.deletions=null,e.sibling=null,e.tag===5&&(t=e.stateNode,t!==null&&(delete t[Ue],delete t[Yn],delete t[Ls],delete t[yf],delete t[gf])),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 oc(e){return e.tag===5||e.tag===3||e.tag===4}function Zo(e){e:for(;;){for(;e.sibling===null;){if(e.return===null||oc(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 Ks(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=Jr));else if(r!==4&&(e=e.child,e!==null))for(Ks(e,t,n),e=e.sibling;e!==null;)Ks(e,t,n),e=e.sibling}function Gs(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(Gs(e,t,n),e=e.sibling;e!==null;)Gs(e,t,n),e=e.sibling}var te=null,Re=!1;function qe(e,t,n){for(n=n.child;n!==null;)ac(e,t,n),n=n.sibling}function ac(e,t,n){if(Ae&&typeof Ae.onCommitFiberUnmount=="function")try{Ae.onCommitFiberUnmount(hl,n)}catch{}switch(n.tag){case 5:ie||Xt(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?Xl(e.parentNode,n):e.nodeType===1&&Xl(e,n),Wn(e)):Xl(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(!ie&&(r=n.updateQueue,r!==null&&(r=r.lastEffect,r!==null))){l=r=r.next;do{var s=l,i=s.destroy;s=s.tag,i!==void 0&&(s&2||s&4)&&Hs(n,t,i),l=l.next}while(l!==r)}qe(e,t,n);break;case 1:if(!ie&&(Xt(n,t),r=n.stateNode,typeof r.componentWillUnmount=="function"))try{r.props=n.memoizedProps,r.state=n.memoizedState,r.componentWillUnmount()}catch(a){Q(n,t,a)}qe(e,t,n);break;case 21:qe(e,t,n);break;case 22:n.mode&1?(ie=(r=ie)||n.memoizedState!==null,qe(e,t,n),ie=r):qe(e,t,n);break;default:qe(e,t,n)}}function qo(e){var t=e.updateQueue;if(t!==null){e.updateQueue=null;var n=e.stateNode;n===null&&(n=e.stateNode=new If),t.forEach(function(r){var l=Hf.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=i),r&=~s}if(r=l,r=Y()-r,r=(120>r?120:480>r?480:1080>r?1080:1920>r?1920:3e3>r?3e3:4320>r?4320:1960*Mf(r/1960))-r,10e?16:e,lt===null)var r=!1;else{if(e=lt,lt=null,ul=0,I&6)throw Error(k(331));var l=I;for(I|=4,N=e.current;N!==null;){var s=N,i=s.child;if(N.flags&16){var a=s.deletions;if(a!==null){for(var u=0;uY()-Ui?Et(e,0):$i|=n),ve(e,t)}function vc(e,t){t===0&&(e.mode&1?(t=yr,yr<<=1,!(yr&130023424)&&(yr=4194304)):t=1);var n=ue();e=Xe(e,t),e!==null&&(nr(e,t,n),ve(e,n))}function Wf(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),vc(e,n)}function Hf(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),vc(e,n)}var yc;yc=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,Lf(e,t,n);pe=!!(e.flags&131072)}else pe=!1,B&&t.flags&1048576&&ku(t,el,t.index);switch(t.lanes=0,t.tag){case 2:var r=t.type;Mr(e,t),e=t.pendingProps;var l=ln(t,oe.current);tn(t,n),l=Ri(null,t,r,e,l,n);var s=Oi();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)?(s=!0,qr(t)):s=!1,t.memoizedState=l.state!==null&&l.state!==void 0?l.state:null,_i(t),l.updater=kl,t.stateNode=l,l._reactInternals=t,Ms(t,r,e,n),t=As(null,t,r,!0,s,n)):(t.tag=0,B&&s&&xi(t),ae(null,t,l,n),t=t.child),t;case 16:r=t.elementType;e:{switch(Mr(e,t),e=t.pendingProps,l=r._init,r=l(r._payload),t.type=r,l=t.tag=Kf(r),e=Le(r,e),l){case 0:t=Us(null,t,r,e,n);break e;case 1:t=Go(null,t,r,e,n);break e;case 11:t=Qo(null,t,r,e,n);break e;case 14:t=Ko(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),Us(e,t,r,l,n);case 1:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Le(r,l),Go(e,t,r,l,n);case 3:e:{if(ec(t),e===null)throw Error(k(387));r=t.pendingProps,s=t.memoizedState,l=s.element,_u(e,t),rl(t,r,null,n);var i=t.memoizedState;if(r=i.element,s.isDehydrated)if(s={element:r,isDehydrated:!1,cache:i.cache,pendingSuspenseBoundaries:i.pendingSuspenseBoundaries,transitions:i.transitions},t.updateQueue.baseState=s,t.memoizedState=s,t.flags&256){l=un(Error(k(423)),t),t=Yo(e,t,r,n,l);break e}else if(r!==l){l=un(Error(k(424)),t),t=Yo(e,t,r,n,l);break e}else for(ge=at(t.stateNode.containerInfo.firstChild),we=t,B=!0,Oe=null,n=Cu(t,null,r,n),t.child=n;n;)n.flags=n.flags&-3|4096,n=n.sibling;else{if(sn(),r===l){t=Je(e,t,n);break e}ae(e,t,r,n)}t=t.child}return t;case 5:return Pu(t),e===null&&Is(t),r=t.type,l=t.pendingProps,s=e!==null?e.memoizedProps:null,i=l.children,Ts(r,l)?i=null:s!==null&&Ts(r,s)&&(t.flags|=32),bu(e,t),ae(e,t,i,n),t.child;case 6:return e===null&&Is(t),null;case 13:return tc(e,t,n);case 4:return Pi(t,t.stateNode.containerInfo),r=t.pendingProps,e===null?t.child=on(t,null,r,n):ae(e,t,r,n),t.child;case 11:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Le(r,l),Qo(e,t,r,l,n);case 7:return ae(e,t,t.pendingProps,n),t.child;case 8:return ae(e,t,t.pendingProps.children,n),t.child;case 12:return ae(e,t,t.pendingProps.children,n),t.child;case 10:e:{if(r=t.type._context,l=t.pendingProps,s=t.memoizedProps,i=l.value,$(tl,r._currentValue),r._currentValue=i,s!==null)if(De(s.value,i)){if(s.children===l.children&&!he.current){t=Je(e,t,n);break e}}else for(s=t.child,s!==null&&(s.return=t);s!==null;){var a=s.dependencies;if(a!==null){i=s.child;for(var u=a.firstContext;u!==null;){if(u.context===r){if(s.tag===1){u=Ke(-1,n&-n),u.tag=2;var c=s.updateQueue;if(c!==null){c=c.shared;var v=c.pending;v===null?u.next=u:(u.next=v.next,v.next=u),c.pending=u}}s.lanes|=n,u=s.alternate,u!==null&&(u.lanes|=n),Fs(s.return,n,t),a.lanes|=n;break}u=u.next}}else if(s.tag===10)i=s.type===t.type?null:s.child;else if(s.tag===18){if(i=s.return,i===null)throw Error(k(341));i.lanes|=n,a=i.alternate,a!==null&&(a.lanes|=n),Fs(i,n,t),i=s.sibling}else i=s.child;if(i!==null)i.return=s;else for(i=s;i!==null;){if(i===t){i=null;break}if(s=i.sibling,s!==null){s.return=i.return,i=s;break}i=i.return}s=i}ae(e,t,l.children,n),t=t.child}return t;case 9:return l=t.type,r=t.pendingProps.children,tn(t,n),l=_e(l),r=r(l),t.flags|=1,ae(e,t,r,n),t.child;case 14:return r=t.type,l=Le(r,t.pendingProps),l=Le(r.type,l),Ko(e,t,r,l,n);case 15:return Zu(e,t,t.type,t.pendingProps,n);case 17:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Le(r,l),Mr(e,t),t.tag=1,me(r)?(e=!0,qr(t)):e=!1,tn(t,n),Yu(t,r,l),Ms(t,r,l,n),As(null,t,r,!0,e,n);case 19:return nc(e,t,n);case 22:return qu(e,t,n)}throw Error(k(156,t.tag))};function gc(e,t){return Qa(e,t)}function Qf(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 Ce(e,t,n,r){return new Qf(e,t,n,r)}function Wi(e){return e=e.prototype,!(!e||!e.isReactComponent)}function Kf(e){if(typeof e=="function")return Wi(e)?1:0;if(e!=null){if(e=e.$$typeof,e===ai)return 11;if(e===ui)return 14}return 2}function ft(e,t){var n=e.alternate;return n===null?(n=Ce(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 Ar(e,t,n,r,l,s){var i=2;if(r=e,typeof e=="function")Wi(e)&&(i=1);else if(typeof e=="string")i=5;else e:switch(e){case At:return _t(n.children,l,s,t);case oi:i=8,l|=8;break;case os:return e=Ce(12,n,t,l|2),e.elementType=os,e.lanes=s,e;case as:return e=Ce(13,n,t,l),e.elementType=as,e.lanes=s,e;case us:return e=Ce(19,n,t,l),e.elementType=us,e.lanes=s,e;case Pa:return Nl(n,l,s,t);default:if(typeof e=="object"&&e!==null)switch(e.$$typeof){case Ea:i=10;break e;case _a:i=9;break e;case ai:i=11;break e;case ui:i=14;break e;case be:i=16,r=null;break e}throw Error(k(130,e==null?e:typeof e,""))}return t=Ce(i,n,t,l),t.elementType=e,t.type=r,t.lanes=s,t}function _t(e,t,n,r){return e=Ce(7,e,r,t),e.lanes=n,e}function Nl(e,t,n,r){return e=Ce(22,e,r,t),e.elementType=Pa,e.lanes=n,e.stateNode={isHidden:!1},e}function rs(e,t,n){return e=Ce(6,e,null,t),e.lanes=n,e}function ls(e,t,n){return t=Ce(4,e.children!==null?e.children:[],e.key,t),t.lanes=n,t.stateNode={containerInfo:e.containerInfo,pendingChildren:null,implementation:e.implementation},t}function Gf(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=$l(0),this.expirationTimes=$l(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=$l(0),this.identifierPrefix=r,this.onRecoverableError=l,this.mutableSourceEagerHydrationData=null}function Hi(e,t,n,r,l,s,i,a,u){return e=new Gf(e,t,n,a,u),t===1?(t=1,s===!0&&(t|=8)):t=0,s=Ce(3,null,null,t),e.current=s,s.stateNode=e,s.memoizedState={element:r,isDehydrated:n,cache:null,transitions:null,pendingSuspenseBoundaries:null},_i(s),e}function Yf(e,t,n){var r=3"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(jc)}catch(e){console.error(e)}}jc(),ja.exports=ke;var bf=ja.exports,Sc,ia=bf;Sc=ia.createRoot,ia.hydrateRoot;const Nc="bv.access",Yi="bv.refresh";function qs(e){try{return localStorage.getItem(e)||""}catch{return""}}function oa(e,t){try{t?localStorage.setItem(e,t):localStorage.removeItem(e)}catch{}}function ir(){return{access:qs(Nc),refresh:qs(Yi)}}function fl(e,t){oa(Nc,e),oa(Yi,t)}function or(){fl("","")}function ep(){return!!qs(Yi)}class Pt extends Error{constructor(t,n,r){super(r),this.status=t,this.code=n}}let Nn=null;async function Tl(){return Nn||(Nn=(async()=>{const{refresh:e}=ir();if(!e)throw new Pt(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 or(),new Pt(t.status,"unauthorized","Your session has ended. Sign in again.");const n=await t.json();return fl(n.access_token,n.refresh_token),n})().finally(()=>{Nn=null}),Nn)}async function F(e,t,n,r=!0){const{access:l}=ir(),s={};l&&(s.Authorization="Bearer "+l),n!==void 0&&(s["Content-Type"]="application/json");const i=await fetch(t,{method:e,headers:s,body:n===void 0?void 0:JSON.stringify(n)});if(i.status===204)return null;const a=await i.text();let u=null;try{u=a?JSON.parse(a):null}catch{}if(i.ok)return u;const c=(u==null?void 0:u.error)||"";if(c==="token_expired"&&r)return await Tl(),F(e,t,n,!1);throw i.status===401&&or(),new Pt(i.status,c,(u==null?void 0:u.message)||`Something went wrong (${i.status}).`)}async function Cc(e,t=!0){const{access:n}=ir(),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 s=(l==null?void 0:l.error)||"";if(s==="token_expired"&&t)return await Tl(),Cc(e,!1);throw r.status===401&&or(),new Pt(r.status,s,(l==null?void 0:l.message)||`That picture could not be loaded (${r.status}).`)}function aa(){const e=navigator.userAgent||"",t=/Windows/.test(e)?"Windows":/Mac OS X|Macintosh/.test(e)?"Mac":/Android/.test(e)?"Android":/iPhone|iPad/.test(e)?"iOS":"Unknown";return`${/Edg\//.test(e)?"Edge":/Chrome\//.test(e)?"Chrome":/Safari\//.test(e)?"Safari":/Firefox\//.test(e)?"Firefox":"browser"} on ${t}`}const jt=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:""},M={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,device:aa()})}),r=await n.json().catch(()=>null);if(!n.ok)throw new Pt(n.status,(r==null?void 0:r.error)||"",(r==null?void 0:r.message)||"Could not sign in.");return fl(r.access_token,r.refresh_token),r.user},async previewInvitation(e){const t=await fetch("/api/auth/invitation"+jt({code:e})),n=await t.json().catch(()=>null);if(!t.ok)throw new Pt(t.status,(n==null?void 0:n.error)||"",(n==null?void 0:n.message)||"That invitation code is not valid.");return n},async register({code:e,full_name:t,password:n}){const r=await fetch("/api/auth/register",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({code:e,full_name:t,password:n,device:aa()})}),l=await r.json().catch(()=>null);if(!r.ok)throw new Pt(r.status,(l==null?void 0:l.error)||"",(l==null?void 0:l.message)||"Could not create the account.");return fl(l.access_token,l.refresh_token),l.user},async logout(){try{await F("POST","/api/auth/logout")}catch{}or()},me:()=>F("GET","/api/auth/me"),sites:()=>F("GET","/api/sites"),createSite:e=>F("POST","/api/sites",e),arrivals:e=>F("GET","/api/visits"+jt(e)),visitors:(e,t=50)=>F("GET","/api/visitors"+jt({q:e,limit:t})),visitorHistory:(e,t=50)=>F("GET",`/api/visitors/${encodeURIComponent(e)}/history`+jt({limit:t})),saveProfile:(e,t)=>F("PUT",`/api/visitors/${encodeURIComponent(e)}/profile`,t),footfall:e=>F("GET","/api/reports/footfall"+jt(e)),conversion:e=>F("GET","/api/reports/conversion"+jt(e)),cameras:()=>F("GET","/api/cameras"),cameraSnapshot:e=>Cc(e),createCamera:(e,t)=>F("POST",`/api/sites/${encodeURIComponent(e)}/cameras`,t),updateCamera:(e,t)=>F("PATCH",`/api/cameras/${encodeURIComponent(e)}`,t),deleteCamera:e=>F("DELETE",`/api/cameras/${encodeURIComponent(e)}`),checkCamera:(e,t,n)=>F("POST",`/api/cameras/${encodeURIComponent(e)}/check`,{kind:t,...n?{seconds:n}:{}}),siteCheck:e=>F("GET",`/api/sites/${encodeURIComponent(e)}/check`),enrolmentCode:(e,t)=>F("POST",`/api/sites/${encodeURIComponent(e)}/enrolment-code`,t||{}),ask:e=>F("POST","/api/assistant",{history:e}),clients:()=>F("GET","/api/admin/clients"),createClient:e=>F("POST","/api/admin/clients",e),team:()=>F("GET","/api/team"),updateMember:(e,t)=>F("PATCH",`/api/team/${encodeURIComponent(e)}`,t),invitations:()=>F("GET","/api/team/invitations"),invite:e=>F("POST","/api/team/invitations",e),revokeInvitation:e=>F("DELETE",`/api/team/invitations/${encodeURIComponent(e)}`),sessions:()=>F("GET","/api/auth/sessions"),revokeSession:e=>F("DELETE",`/api/auth/sessions/${encodeURIComponent(e)}`),signOutOthers:()=>F("POST","/api/auth/sessions/revoke-others")};function tp({cursor:e,siteId:t,onPage:n,onError:r,signal:l}){let s=!1,i=e||"";return(async()=>{for(;!s;){try{const{access:u}=ir(),c=await fetch("/api/visits/stream"+jt({cursor:i,site_id:t}),{headers:{Authorization:"Bearer "+u,Accept:"text/event-stream"},signal:l});if(c.status===401){await Tl();continue}if(!c.ok||!c.body)throw new Error("stream unavailable");const v=c.body.getReader(),h=new TextDecoder;let m="";for(;!s;){const{value:y,done:x}=await v.read();if(x)break;m+=h.decode(y,{stream:!0});let g;for(;(g=m.indexOf(` -`))!==-1;){const T=h.slice(0,g);h=h.slice(g+2);for(const p of T.split(` -`))if(p.startsWith("id: "))i=p.slice(4).trim();else if(p.startsWith("data: "))try{n(JSON.parse(p.slice(6)))}catch{}}}}catch(u){if(s||l!=null&&l.aborted)return;r==null||r(u)}if(s)return;await new Promise(u=>setTimeout(u,3e3))}})(),()=>{s=!0}}function np({cameraId:e,img:t,onState:n,signal:r}){let l=!1;return(async()=>{for(;!l;){try{const{access:i}=ir(),a=await fetch(`/api/cameras/${e}/live`,{headers:{Authorization:"Bearer "+i,Accept:"text/event-stream"},signal:r});if(a.status===401){await Tl();continue}if(!a.ok||!a.body)throw new Error("live view unavailable");const u=a.body.getReader(),c=new TextDecoder;let v="";for(;!l;){const{value:m,done:h}=await u.read();if(h)break;v+=c.decode(m,{stream:!0});let y;for(;(y=v.indexOf(` +`))!==-1;){const P=m.slice(0,g);m=m.slice(g+2);for(const f of P.split(` +`))if(f.startsWith("id: "))i=f.slice(4).trim();else if(f.startsWith("data: "))try{n(JSON.parse(f.slice(6)))}catch{}}}}catch(u){if(s||l!=null&&l.aborted)return;r==null||r(u)}if(s)return;await new Promise(u=>setTimeout(u,3e3))}})(),()=>{s=!0}}function np({cameraId:e,img:t,onState:n,signal:r}){let l=!1;return(async()=>{for(;!l;){try{const{access:i}=ir(),a=await fetch(`/api/cameras/${e}/live`,{headers:{Authorization:"Bearer "+i,Accept:"text/event-stream"},signal:r});if(a.status===401){await Tl();continue}if(!a.ok||!a.body)throw new Error("live view unavailable");const u=a.body.getReader(),c=new TextDecoder;let v="";for(;!l;){const{value:h,done:m}=await u.read();if(m)break;v+=c.decode(h,{stream:!0});let y;for(;(y=v.indexOf(` -`))!==-1;){const x=v.slice(0,y);v=v.slice(y+2);let g="message",T="";for(const p of x.split(` -`))p.startsWith("event: ")?g=p.slice(7).trim():p.startsWith("data: ")&&(T=p.slice(6));g==="frame"&&T?(t.current&&(t.current.src="data:image/jpeg;base64,"+T),n==null||n("live")):g==="waiting"&&(n==null||n("waiting"))}}}catch{if(l||r!=null&&r.aborted)return;n==null||n("reconnecting")}if(l)return;await new Promise(i=>setTimeout(i,1500))}})(),()=>{l=!0}}function rp({onSignedIn:e}){const[t,n]=j.useState(!1);return t?o.jsx(sp,{onSignedIn:e,onCancel:()=>n(!1)}):o.jsx(lp,{onSignedIn:e,onJoin:()=>n(!0)})}function lp({onSignedIn:e,onJoin:t}){const[n,r]=j.useState(""),[l,s]=j.useState(""),[i,a]=j.useState(""),[u,c]=j.useState(!1),v=async m=>{m.preventDefault(),c(!0),a("");try{await $.login(n.trim(),l),e(await $.me())}catch(h){a(h.message),c(!1)}};return o.jsx("div",{className:"signin",children:o.jsxs("form",{className:"card",onSubmit:v,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:n,autoComplete:"username",autoFocus:!0,required:!0,onChange:m=>r(m.target.value),placeholder:"you@company.com"})]}),o.jsxs("label",{children:["Password",o.jsx("input",{type:"password",value:l,autoComplete:"current-password",required:!0,onChange:m=>s(m.target.value)})]}),i&&o.jsx("p",{className:"error",role:"alert",children:i}),o.jsx("button",{className:"primary",disabled:u||!n||!l,children:u?"Signing in…":"Sign in"}),o.jsxs("p",{className:"foot",children:["Been invited? ",o.jsx("button",{type:"button",className:"linkish",onClick:t,children:"Use your invitation code"})]})]})})}function sp({onSignedIn:e,onCancel:t}){const[n,r]=j.useState(""),[l,s]=j.useState(null),[i,a]=j.useState(""),[u,c]=j.useState(""),[v,m]=j.useState(""),[h,y]=j.useState(""),[x,g]=j.useState(!1),T=async d=>{d.preventDefault(),g(!0),y("");try{const f=await $.previewInvitation(n.trim());s(f),a(f.full_name||"")}catch(f){y(f.message)}finally{g(!1)}},p=async d=>{if(d.preventDefault(),u!==v){y("Those two passwords are not the same.");return}g(!0),y("");try{const f=await $.register({code:n.trim(),full_name:i,password:u});e(f)}catch(f){y(f.message),g(!1)}};return o.jsx("div",{className:"signin",children:o.jsxs("form",{className:"card",onSubmit:l?p:T,children:[o.jsx("span",{className:"mark big","aria-hidden":"true"}),o.jsx("h1",{children:l?`Join ${l.client_name}`:"Behavision"}),l?o.jsxs(o.Fragment,{children:[o.jsxs("p",{className:"sub",children:["You are joining as ",o.jsx("b",{children:l.role}),", signing in with"," ",o.jsx("code",{children:l.email}),". Choose a password only you know."]}),o.jsxs("label",{children:["Your name",o.jsx("input",{value:i,onChange:d=>a(d.target.value),autoFocus:!0,autoComplete:"name"})]}),o.jsxs("label",{children:["Password",o.jsx("input",{type:"password",value:u,required:!0,minLength:8,autoComplete:"new-password",onChange:d=>c(d.target.value)}),o.jsx("span",{className:"hint",children:"At least 8 characters. Longer is the only thing that helps."})]}),o.jsxs("label",{children:["Password again",o.jsx("input",{type:"password",value:v,required:!0,autoComplete:"new-password",onChange:d=>m(d.target.value)})]}),h&&o.jsx("p",{className:"error",role:"alert",children:h}),o.jsx("button",{className:"primary",disabled:x||!u||!v,children:x?"Creating your account…":"Create account and sign in"})]}):o.jsxs(o.Fragment,{children:[o.jsx("p",{className:"sub",children:"Enter the invitation code you were given."}),o.jsxs("label",{children:["Invitation code",o.jsx("input",{value:n,onChange:d=>r(d.target.value),autoFocus:!0,required:!0,autoComplete:"off",spellCheck:"false",placeholder:"ABCDEF-123456-GHIJKL-789012",className:"codefield"}),o.jsx("span",{className:"hint",children:"Dashes and capitals do not matter."})]}),h&&o.jsx("p",{className:"error",role:"alert",children:h}),o.jsx("button",{className:"primary",disabled:x||!n.trim(),children:x?"Checking…":"Continue"})]}),o.jsx("p",{className:"foot",children:o.jsx("button",{type:"button",className:"linkish",onClick:t,children:"Back to sign in"})})]})})}function It(e,t,n=[]){const[r,l]=j.useState(null),[s,i]=j.useState(null),[a,u]=j.useState(!0),c=j.useRef(!1),v=j.useRef(!0),m=j.useCallback(async()=>{if(!c.current){c.current=!0;try{const h=await e();if(!v.current)return;l(h),i(null)}catch(h){if(!v.current)return;i(h)}finally{c.current=!1,v.current&&u(!1)}}},n);return j.useEffect(()=>{if(v.current=!0,m(),!t)return()=>{v.current=!1};const h=setInterval(m,t);return()=>{v.current=!1,clearInterval(h)}},[m,t]),{data:r,error:s,loading:a,reload:m}}function ip(e){const[t,n]=j.useState(null);return j.useEffect(()=>{if(!e){n(null);return}let r=!0,l=null;return $.cameraSnapshot(e).then(s=>{if(!r){URL.revokeObjectURL(s);return}l=s,n(s)}).catch(()=>{r&&n(null)}),()=>{r=!1,l&&URL.revokeObjectURL(l)}},[e]),t}function Xi({image:e,url:t,alt:n}){const r=e?e.url:t,l=e&&!!e.auth||typeof r=="string"&&r.startsWith("/"),s=ip(l?r:null),i=l?s:r;return i?o.jsx("img",{src:i,alt:n,loading:"lazy"}):null}function op({site:e,onClose:t}){const[n,r]=j.useState(null),[l,s]=j.useState(!1),[i,a]=j.useState(""),u=async()=>{s(!0),a(""),r(null);try{r(await $.siteCheck(e.site_id))}catch(c){a(c.message)}finally{s(!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:u,children:"Run the check"})]}),l&&o.jsxs("div",{className:"waiting",children:[o.jsx("span",{className:"spinner","aria-hidden":"true"}),o.jsx("b",{children:"Checking…"})]}),i&&o.jsx("p",{className:"error",role:"alert",children:i}),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:u,children:"Check again"})]}),o.jsx(ap,{site:e})]})]})})}function ap({site:e}){const[t,n]=j.useState(null),[r,l]=j.useState(!1),[s,i]=j.useState(""),[a,u]=j.useState(""),c=async()=>{l(!0),i("");try{n(await $.enrolmentCode(e.site_id,{label:a.trim()}))}catch(v){i(v.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 ",cp(t.expires_at),"."]}),o.jsxs("div",{className:"row",children:[o.jsx("button",{className:"ghost",onClick:()=>up(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:a,placeholder:"counter PC",onChange:v=>u(v.target.value)})]}),s&&o.jsx("p",{className:"error",role:"alert",children:s}),o.jsx("button",{className:"ghost",onClick:c,disabled:r,children:r?"Creating…":"Create an installation code"})]})]})}function up(e){var t;try{(t=navigator.clipboard)==null||t.writeText(e)}catch{}}function cp(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 dp(){const{data:e,error:t,loading:n}=It(()=>$.sites(),2e4,[]),{data:r}=It(()=>$.cameras(),6e4,[]),[l,s]=j.useState(null),i=e||[];if(n&&!e)return o.jsx(ar,{});if(t)return o.jsx(ur,{error:t});if(!i.length)return o.jsx(gp,{});const a=i.map(h=>{const y=(r||[]).filter(x=>x.site_id===h.site_id);return{site:h,cams:y,verdict:fp(h,y)}}),u=h=>a.filter(y=>y.verdict.tone===h).length,c=u("bad"),v=u("warn"),m=u("idle");return o.jsxs(o.Fragment,{children:[o.jsxs("header",{className:"head",children:[o.jsx("h1",{children:"Shops"}),o.jsxs("p",{className:"sub",children:[i.length," ",i.length===1?"shop":"shops",c>0&&o.jsxs(o.Fragment,{children:[" · ",o.jsxs("b",{className:"bad",children:[c," not working"]})]}),v>0&&o.jsxs(o.Fragment,{children:[" · ",o.jsxs("b",{className:"warn",children:[v," needing attention"]})]}),m>0&&o.jsxs(o.Fragment,{children:[" · ",m," not set up yet"]}),!c&&!v&&!m&&o.jsxs(o.Fragment,{children:[" · ",o.jsx("b",{className:"ok",children:"all working"})]})]})]}),o.jsx("div",{className:"grid sites",children:a.map(h=>o.jsx(hp,{site:h.site,cams:h.cams,verdict:h.verdict,onCheck:()=>s(h.site)},h.site.site_id))}),l&&o.jsx(op,{site:l,onClose:()=>s(null)})]})}function fp(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:`${bs(n)} of faces too poor to recognise`}:n>.2?{tone:"warn",mark:"!",words:`${bs(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 ${Ft(e.last_heartbeat_at)}`}}const pp={ok:"Working",warn:"Needs attention",bad:"Not working",idle:"Not set up"};function hp({site:e,cams:t,verdict:n,onCheck:r}){const l=e.fraction_below_gate,s=n.tone,i=n.headline||pp[s],a=mp(t),u=e.cameras_total||t.length;return o.jsxs("article",{className:"card site state-"+s,onClick:r,role:"button",tabIndex:0,onKeyDown:c=>c.key==="Enter"&&r(),children:[o.jsxs("div",{className:"shot",children:[a.url?o.jsx(Xi,{image:a,alt:`View inside ${e.name}`}):o.jsxs("div",{className:"noshot",children:[o.jsx(vp,{}),a.reason&&o.jsx("span",{children:a.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:u>0?`${u} ${u===1?"camera":"cameras"}`:"No cameras yet"})]}),o.jsxs("span",{className:"status "+s,children:[o.jsx("i",{"aria-hidden":"true"}),i]})]}),a.at&&o.jsx("span",{className:"shot-age",children:Ft(a.at)})]}),o.jsxs("div",{className:"metrics",children:[o.jsx(ss,{label:"Cameras",value:u?`${e.cameras_up}/${u}`:"—",tone:u?e.cameras_up0?bs(1-l):"—",tone:l?l>.5?"bad":l>.2?"warn":"ok":"idle"}),o.jsx(ss,{label:"Last seen",value:Ft(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 ss({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 mp(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?{...t.snapshot,at:t.snapshot_at}:{reason:e.map(l=>{var s;return(s=l.snapshot)==null?void 0:s.reason}).find(Boolean)||"No picture from this shop yet."}}function vp(){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 bs(e){return`${Math.round(e*100)}%`}function Ft(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 yp(e){if(!e)return"never";const t=new Date(e).getTime();if(Number.isNaN(t))return"—";const n=(t-Date.now())/1e3;if(n<=0)return"expired";const r=Math.round(n/60);if(r<60)return`in ${r} min`;const l=Math.round(r/60);return l<48?`in ${l} h`:`in ${Math.round(l/24)} days`}function ar(){return o.jsxs("div",{className:"state",children:[o.jsx("span",{className:"spinner","aria-hidden":"true"}),"Loading…"]})}function ur({error:e}){return o.jsx("div",{className:"state",children:o.jsx("p",{className:"error",role:"alert",children:e.message})})}function gp(){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 ua=60;function wp(){const[e,t]=j.useState([]),[n,r]=j.useState("connecting"),[l,s]=j.useState(null),i=j.useRef(new Set);return j.useEffect(()=>{const a=new AbortController;let u=()=>{};return(async()=>{try{const c=await $.arrivals({limit:30}),v=c.arrivals||[];v.forEach(m=>i.current.add(m.visit_id)),t(v.slice().reverse()),r("live"),u=tp({cursor:c.cursor,signal:a.signal,onPage:m=>{const h=(m.arrivals||[]).filter(y=>!i.current.has(y.visit_id));h.length&&(h.forEach(y=>i.current.add(y.visit_id)),r("live"),t(y=>[...h.reverse(),...y].slice(0,ua)))},onError:()=>r("reconnecting")})}catch(c){s(c)}})(),()=>{a.abort(),u()}},[]),l?o.jsx(ur,{error:l}):n==="connecting"&&!e.length?o.jsx(ar,{}):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 ",ua," 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(a=>o.jsx(xp,{a},a.visit_id))})]})}function xp({a:e}){const t=e.name||e.label||"Unidentified";return o.jsxs("li",{className:"card arrival",children:[o.jsx(kp,{image:e.image,name:t,customerRef:e.visitor_ref}),o.jsxs("div",{className:"who-col",children:[o.jsx("strong",{children:t}),o.jsxs("span",{className:"sub",children:[e.name&&e.visitor_ref?`${e.visitor_ref} · `:"",e.site,e.camera_id?` · ${e.camera_id}`:""," · ",Ft(e.occurred_at)]}),e.attributes&&o.jsx(Np,{attrs:e.attributes})]}),e.is_new_visitor?o.jsx("span",{className:"pill new",children:"New"}):o.jsx("span",{className:"pill",children:"Returning"})]})}function kp({image:e,name:t,customerRef:n}){return e!=null&&e.available?o.jsx("span",{className:"face",children:o.jsx(Xi,{image:e,alt:""})}):o.jsx("span",{className:"face initials",title:(e==null?void 0:e.reason)||"","aria-hidden":"true",children:jp(t,n)})}function jp(e,t){const n=/^Visitor (\d+)$/.exec(String(e).trim());if(n)return n[1];if(t){const r=/^V-(\d+)$/.exec(t);if(r)return r[1]}return Sp(e)}function Sp(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 Np({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}function Cp({camera:e,onClose:t}){const n=j.useRef(null),[r,l]=j.useState("waiting"),[s,i]=j.useState(!1);j.useEffect(()=>{const c=new AbortController,v=np({cameraId:e.id,img:n,onState:h=>{l(h),h==="live"&&i(!1)},signal:c.signal}),m=setTimeout(()=>i(!0),12e3);return()=>{v(),c.abort(),clearTimeout(m)}},[e.id]);const a=e.connected===!1?"This camera was not connecting when the shop PC last reported. Check it is powered on and reachable on the shop’s network.":e.connected==null?"The shop PC has not reported on this camera yet. It may still be starting up.":"The shop PC is not sending frames. It may be offline, or its Behavision app may not be running.",u=s&&r!=="live"?a:{waiting:"Asking the shop PC…",live:"Live",reconnecting:"Reconnecting…"}[r];return o.jsx("div",{className:"overlay",onClick:t,children:o.jsxs("aside",{className:"drawer live",onClick:c=>c.stopPropagation(),children:[o.jsxs("header",{className:"drawer-head",children:[o.jsxs("div",{children:[o.jsx("h2",{children:e.label}),o.jsx("p",{className:"sub",children:e.site})]}),o.jsx("button",{className:"ghost",onClick:t,children:"Close"})]}),o.jsxs("div",{className:"drawer-body",children:[o.jsxs("div",{className:"liveshot",children:[o.jsx("img",{ref:n,alt:`Live view from ${e.label}`}),r!=="live"&&o.jsx("div",{className:"livewait",children:o.jsx("span",{children:s?"No picture yet":u})})]}),o.jsx("p",{className:"hint",style:{marginTop:10},children:r==="live"?"Live. The shop only uploads while this view is open.":u})]})]})})}const Br=[{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:""}],ca=e=>Br.find(t=>t.id===e)||Br[Br.length-1],Ep=["Camera","Connection","Test","Walk past"];function _p({sites:e,existing:t,onClose:n,onSaved:r}){var d;const l=!(t!=null&&t.id),[s,i]=j.useState(l?0:2),[a,u]=j.useState(t!=null&&t.id?t:null),[c,v]=j.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:""}),[m,h]=j.useState(!1),[y,x]=j.useState(""),g=f=>w=>v(S=>({...S,[f]:w.target.value})),T=f=>{const w=ca(f.target.value);v(S=>({...S,make:w.id,path:w.path||S.path}))},p=async()=>{h(!0),x("");const f={};for(const[w,S]of Object.entries(c))w==="site_id"||w==="make"||S===""||S==null||(f[w]=w==="port"?Number(S):S);try{const w=a!=null&&a.id?await $.updateCamera(a.id,f):await $.createCamera(c.site_id,f);u(w),i(2),r==null||r(w,{keepOpen:!0})}catch(w){x(w.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":(a==null?void 0:a.label)||(t==null?void 0:t.label)}),o.jsx("p",{className:"sub",children:s<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:Ep.map((f,w)=>o.jsxs("li",{className:w===s?"now":wo.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:g("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:T,children:Br.map(f=>o.jsx("option",{value:f.id,children:f.label},f.id))}),o.jsx("span",{className:"hint",children:ca(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:()=>i(1),children:"Next"})]}),s===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:g("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:g("port")})]}),o.jsxs("label",{children:["Stream path",o.jsx("input",{value:c.path,onChange:g("path")})]})]}),o.jsxs("label",{children:["Camera username",o.jsx("input",{value:c.username,onChange:g("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:g("password"),name:"camera-secret",autoComplete:"new-password",placeholder:a!=null&&a.has_password?"(unchanged)":""})]}),y&&o.jsx("p",{className:"error",role:"alert",children:y}),o.jsxs("div",{className:"pair",children:[o.jsx("button",{className:"ghost",onClick:()=>i(0),children:"Back"}),o.jsx("button",{className:"primary",disabled:m||!c.host.trim(),onClick:p,children:m?"Saving…":"Save and test"})]})]}),s>=2&&a&&o.jsx(Pp,{camera:a,step:s,onStep:i,onUpdated:f=>{u(f),r==null||r(f,{keepOpen:!0})},onEdit:()=>i(1),onDone:n})]})})}function Pp({camera:e,step:t,onStep:n,onUpdated:r,onEdit:l,onDone:s}){const[i,a]=j.useState(e),[u,c]=j.useState(!1),[v,m]=j.useState(""),h=j.useRef(null);j.useEffect(()=>a(e),[e]);const y=i.check||{},x=y.state==="requested"||y.state==="running";j.useEffect(()=>{if(!x)return;let p=!0;const d=async()=>{try{const w=(await $.cameras()).find(S=>S.id===i.id);p&&w&&(a(w),r==null||r(w))}catch{}};return h.current=setInterval(d,4e3),()=>{p=!1,clearInterval(h.current)}},[x,i.id]);const g=async(p,d)=>{c(!0),m("");try{const f=await $.checkCamera(i.id,p,d);a(f),r==null||r(f),n(p==="placement"?3:2)}catch(f){m(f.message)}finally{c(!1)}},T=y.kind==="connection"&&y.state==="done"&&y.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."]})]}),x&&o.jsxs("div",{className:"waiting",children:[o.jsx("span",{className:"spinner","aria-hidden":"true"}),o.jsxs("div",{children:[o.jsx("b",{children:y.state==="running"?"Running on the shop’s PC…":"Waiting for the shop’s PC…"}),o.jsx("span",{className:"sub",children:y.state==="running"?"This takes about "+(y.seconds||25)+" seconds.":"It picks up the request within a couple of minutes."})]})]}),y.state==="done"&&o.jsx(Tp,{check:y}),v&&o.jsx("p",{className:"error",role:"alert",children:v}),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:u||x,onClick:()=>g("connection"),children:y.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:u||x,onClick:()=>g("placement",25),children:y.state==="done"?"Check again":"Start walk-past check"})]})}),t===2&&T&&!x&&o.jsx("button",{className:"primary",onClick:()=>n(3),children:"Next: prove it can recognise faces"}),t===3&&y.state==="done"&&!x&&o.jsx("button",{className:"ghost",onClick:s,children:"Finish"})]})})}function Tp({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((s,i)=>o.jsx("li",{children:s},i))}),((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 zp(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 Lp({user:e}){const{data:t,error:n,loading:r,reload:l}=It(()=>$.cameras(),2e4,[]),{data:s}=It(()=>$.sites(),0,[]),[i,a]=j.useState(null),[u,c]=j.useState(null),v=["admin","owner","manager"].includes(e.role),m=t||[],h=m.filter(g=>g.connected).length,y=m.filter(g=>g.connected===!1).length,x=m.filter(g=>g.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:[m.length," ",m.length===1?"camera":"cameras",h>0&&o.jsxs(o.Fragment,{children:[" · ",o.jsxs("b",{className:"ok",children:[h," connected"]})]}),y>0&&o.jsxs(o.Fragment,{children:[" · ",o.jsxs("b",{className:"bad",children:[y," down"]})]}),x>0&&o.jsxs(o.Fragment,{children:[" · ",x," waiting for the shop PC"]})]}),v&&o.jsx("button",{className:"primary",onClick:()=>a({}),children:"Set up a camera"})]}),r&&!t?o.jsx(ar,{}):n?o.jsx(ur,{error:n}):m.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:m.map(g=>o.jsx(Rp,{cam:g,canEdit:v,onEdit:()=>a(g),onWatch:()=>c(g)},g.id))}),i&&o.jsx(_p,{existing:i.id?i:null,sites:s||[],onClose:()=>{a(null),l()},onSaved:(g,T)=>{T!=null&&T.keepOpen||a(null),l()}}),u&&o.jsx(Cp,{camera:u,onClose:()=>c(null)})]})}function Rp({cam:e,canEdit:t,onEdit:n,onWatch:r}){var a,u;const l=e.connected==null?"idle":e.connected?"ok":"bad",s=e.connected==null?"Waiting for the shop PC":e.connected?"Connected":"Not connecting",i=zp(e);return o.jsxs("article",{className:"card cam state-"+l,onClick:t?n:void 0,role:t?"button":void 0,tabIndex:t?0:void 0,onKeyDown:c=>t&&c.key==="Enter"&&n(),children:[o.jsxs("div",{className:"shot",children:[(a=e.snapshot)!=null&&a.available?o.jsx(Xi,{image:e.snapshot,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 "+l,children:[o.jsx("i",{"aria-hidden":"true"}),s]})]}),e.snapshot_at&&o.jsx("span",{className:"shot-age",children:Ft(e.snapshot_at)}),o.jsxs("button",{className:"watch",title:"Watch this camera now",onClick:c=>{c.stopPropagation(),r()},children:[o.jsx("i",{"aria-hidden":"true"}),"Live"]})]}),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 Op=["Is everything working today?","Why is footfall low at Chennai?","How many people came in last week?","Which cameras still need checking?"];function Ip({open:e,onClose:t}){const[n,r]=j.useState([]),[l,s]=j.useState(""),[i,a]=j.useState(!1),[u,c]=j.useState(!1),v=j.useRef(null),m=j.useRef(null);j.useEffect(()=>{var y;e&&((y=m.current)==null||y.focus())},[e]),j.useEffect(()=>{var y;(y=v.current)==null||y.scrollIntoView({behavior:"smooth",block:"end"})},[n,i]);const h=async y=>{const x=(y??l).trim();if(!x||i)return;const g=[...n,{role:"user",text:x}];r(g),s(""),a(!0);try{const T=await $.ask(g);r(p=>[...p,{role:"assistant",text:T.text,used:T.used}])}catch(T){T.code==="assistant_off"?c(!0):r(p=>[...p,{role:"assistant",text:T.message,failed:!0}])}finally{a(!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:[u?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:"}),Op.map(y=>o.jsx("button",{className:"chip",onClick:()=>h(y),children:y},y))]}):n.map((y,x)=>{var g;return o.jsxs("div",{className:"bubble "+y.role+(y.failed?" failed":""),children:[y.text,((g=y.used)==null?void 0:g.length)>0&&o.jsxs("span",{className:"used",children:["looked at: ",y.used.map(Fp).join(", ")]})]},x)}),i&&o.jsxs("div",{className:"bubble assistant-thinking",children:[o.jsx("span",{className:"spinner","aria-hidden":"true"})," Looking…"]}),o.jsx("div",{ref:v})]}),!u&&o.jsxs("form",{className:"assistant-ask",onSubmit:y=>{y.preventDefault(),h()},children:[o.jsx("input",{ref:m,value:l,onChange:y=>s(y.target.value),placeholder:"Ask about your shops…",disabled:i}),o.jsx("button",{className:"primary",disabled:i||!l.trim(),children:"Ask"})]})]}):null}function Fp(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 Dp(){const{data:e,error:t,loading:n,reload:r}=It(()=>$.clients(),0,[]),[l,s]=j.useState(!1),[i,a]=j.useState(null),u=e||[];return o.jsxs(o.Fragment,{children:[o.jsxs("header",{className:"head",children:[o.jsx("h1",{children:"Companies"}),o.jsx("button",{className:"primary",onClick:()=>{a(null),s(!0)},children:"New company"})]}),i&&o.jsx($p,{result:i,onDismiss:()=>a(null)}),n&&!e?o.jsx(ar,{}):t?o.jsx(ur,{error:t}):u.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:u.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:Ft(c.created_at)})]},c.id))})]})}),l&&o.jsx(Mp,{onClose:()=>s(!1),onCreated:c=>{s(!1),a(c),r()}})]})}function Mp({onClose:e,onCreated:t}){const[n,r]=j.useState({company_name:"",owner_name:"",owner_email:"",password:""}),[l,s]=j.useState(!1),[i,a]=j.useState(""),u=v=>m=>r({...n,[v]:m.target.value}),c=async v=>{v.preventDefault(),s(!0),a("");try{t(await $.createClient(n))}catch(m){a(m.message),s(!1)}};return o.jsx("div",{className:"overlay",onClick:e,children:o.jsxs("aside",{className:"drawer narrow",onClick:v=>v.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:u("company_name"),required:!0,autoFocus:!0})]}),o.jsxs("label",{children:["Owner’s name",o.jsx("input",{value:n.owner_name,onChange:u("owner_name")})]}),o.jsxs("label",{children:["Owner’s email",o.jsx("input",{type:"email",value:n.owner_email,onChange:u("owner_email"),required:!0})]}),o.jsxs("label",{children:["Password",o.jsx("input",{type:"text",value:n.password,onChange:u("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."})]}),i&&o.jsx("p",{className:"error",role:"alert",children:i}),o.jsx("button",{className:"primary",disabled:l,children:l?"Creating…":"Create company"})]})]})})}function $p({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"})]})}function Up({user:e}){const t=It(()=>$.team(),0,[]),n=It(()=>$.invitations(),0,[]),[r,l]=j.useState(!1),[s,i]=j.useState(null),[a,u]=j.useState(""),[c,v]=j.useState(""),m=e.role==="owner"||e.role==="manager",h=t.data||[],y=n.data||[],x=async(g,T)=>{u(g),v("");try{await $.updateMember(g,T),t.reload()}catch(p){v(p.message)}finally{u("")}};return o.jsxs(o.Fragment,{children:[o.jsxs("header",{className:"head",children:[o.jsx("h1",{children:"Team"}),m&&o.jsx("button",{className:"primary",onClick:()=>{i(null),l(!0)},children:"Invite someone"})]}),s&&o.jsx(Bp,{invite:s,onDismiss:()=>i(null)}),c&&o.jsx("p",{className:"error",role:"alert",children:c}),t.loading&&!t.data?o.jsx(ar,{}):t.error?o.jsx(ur,{error:t.error}):o.jsx("div",{className:"tablewrap",children:o.jsxs("table",{className:"rows",children:[o.jsx("thead",{children:o.jsxs("tr",{children:[o.jsx("th",{children:"Name"}),o.jsx("th",{children:"Email"}),o.jsx("th",{children:"Role"}),o.jsx("th",{children:"Last signed in"}),m&&o.jsx("th",{})]})}),o.jsx("tbody",{children:h.map(g=>o.jsxs("tr",{className:g.active?"":"inactive",children:[o.jsxs("td",{children:[o.jsx("strong",{children:g.full_name||"—"}),!g.active&&o.jsx("span",{className:"pill muted",children:"No access"})]}),o.jsx("td",{children:o.jsx("code",{children:g.email})}),o.jsx("td",{children:m&&g.id!==e.id?o.jsxs("select",{value:g.role,disabled:a===g.id,onChange:T=>x(g.id,{role:T.target.value}),children:[o.jsx("option",{value:"staff",children:"Staff"}),o.jsx("option",{value:"manager",children:"Manager"}),e.role==="owner"&&o.jsx("option",{value:"owner",children:"Owner"})]}):o.jsx("span",{className:"role",children:g.role})}),o.jsx("td",{className:"sub",children:g.last_login_at?Ft(g.last_login_at):"Never"}),m&&o.jsx("td",{className:"right",children:g.id===e.id?null:g.active?o.jsx("button",{className:"ghost danger",disabled:a===g.id,onClick:()=>x(g.id,{active:!1}),children:"Remove access"}):o.jsx("button",{className:"ghost",disabled:a===g.id,onClick:()=>x(g.id,{active:!0}),children:"Restore"})})]},g.id))})]})}),m&&y.length>0&&o.jsxs("section",{className:"pending",children:[o.jsx("h2",{children:"Waiting to join"}),o.jsx("ul",{className:"invites",children:y.map(g=>o.jsxs("li",{className:"card invite",children:[o.jsxs("div",{children:[o.jsx("strong",{children:g.email}),o.jsxs("span",{className:"sub",children:["invited as ",g.role,g.invited_by?` by ${g.invited_by}`:""," · expires ",yp(g.expires_at)]})]}),o.jsx("button",{className:"ghost danger",onClick:async()=>{await $.revokeInvitation(g.id),n.reload()},children:"Withdraw"})]},g.id))})]}),r&&o.jsx(Ap,{canMintOwner:e.role==="owner",onClose:()=>l(!1),onDone:g=>{l(!1),i(g),n.reload()}})]})}function Ap({canMintOwner:e,onClose:t,onDone:n}){const[r,l]=j.useState({email:"",full_name:"",role:"staff"}),[s,i]=j.useState(!1),[a,u]=j.useState(""),c=m=>h=>l({...r,[m]:h.target.value}),v=async m=>{m.preventDefault(),i(!0),u("");try{n(await $.invite(r))}catch(h){u(h.message),i(!1)}};return o.jsx("div",{className:"overlay",onClick:t,children:o.jsxs("aside",{className:"drawer narrow",onClick:m=>m.stopPropagation(),children:[o.jsxs("header",{className:"drawer-head",children:[o.jsx("h2",{children:"Invite someone"}),o.jsx("button",{className:"ghost",onClick:t,children:"Close"})]}),o.jsxs("form",{className:"drawer-body",onSubmit:v,children:[o.jsxs("label",{children:["Their email",o.jsx("input",{type:"email",value:r.email,onChange:c("email"),required:!0,autoFocus:!0,autoComplete:"off",name:"invitee"}),o.jsx("span",{className:"hint",children:"This is the address they will sign in with, and it is fixed by the invitation — passing the code on cannot make it somebody else’s account."})]}),o.jsxs("label",{children:["Their name",o.jsx("input",{value:r.full_name,onChange:c("full_name"),autoComplete:"off",name:"invitee-name"})]}),o.jsxs("label",{children:["Role",o.jsxs("select",{value:r.role,onChange:c("role"),children:[o.jsx("option",{value:"staff",children:"Staff — see customers and shops"}),o.jsx("option",{value:"manager",children:"Manager — also set up cameras and invite people"}),e&&o.jsx("option",{value:"owner",children:"Owner — full control"})]})]}),a&&o.jsx("p",{className:"error",role:"alert",children:a}),o.jsx("button",{className:"primary",disabled:s,children:s?"Creating…":"Create invitation"})]})]})})}function Bp({invite:e,onDismiss:t}){return o.jsxs("div",{className:"banner ok credentials",children:[o.jsxs("div",{children:[o.jsxs("b",{children:["Invitation for ",e.email,"."]})," Give them this code. It is shown once, works once, and cannot be recovered.",o.jsxs("dl",{className:"creds",children:[o.jsxs("div",{children:[o.jsx("dt",{children:"Code"}),o.jsx("dd",{children:o.jsx("code",{className:"big",children:e.code})})]}),o.jsxs("div",{children:[o.jsx("dt",{children:"Role"}),o.jsx("dd",{children:e.role})]})]}),o.jsx("p",{className:"sub",children:"They open the app, choose “I have an invitation code”, and pick their own password. Nobody else ever sees it."})]}),o.jsx("button",{className:"ghost",onClick:t,children:"Done"})]})}const Vp=[{id:"sites",label:"Shops",View:dp},{id:"live",label:"Live",View:wp},{id:"cameras",label:"Cameras",View:Lp},{id:"team",label:"Team",View:Up}],Wp=[{id:"clients",label:"Companies",View:Dp}];function Hp(){const[e,t]=j.useState(null),[n,r]=j.useState(!0),[l,s]=j.useState("sites"),[i,a]=j.useState(!1);if(j.useEffect(()=>{(async()=>{if(ep())try{t(await $.me())}catch{or()}r(!1)})()},[]),n)return o.jsxs("div",{className:"boot",children:[o.jsx("span",{className:"spinner","aria-hidden":"true"}),"Loading…"]});if(!e)return o.jsx(rp,{onSignedIn:t});const u=e.role==="admin"&&!e.client_id,c=u?Wp:Vp,v=c.find(y=>y.id===l)||c[0],m=v.View,h=async()=>{await $.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(y=>o.jsx("button",{onClick:()=>s(y.id),"aria-current":v.id===y.id?"page":void 0,children:y.label},y.id))}),o.jsxs("div",{className:"who",children:[o.jsxs("button",{className:"ask-btn"+(i?" on":""),onClick:()=>a(y=>!y),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:u?"platform admin":e.role}),o.jsx("button",{className:"ghost",onClick:h,children:"Sign out"})]})]}),o.jsxs("div",{className:"with-assistant"+(i?" open":""),children:[o.jsx("main",{className:"page",children:o.jsx(m,{user:e})}),o.jsx(Ip,{open:i,onClose:()=>a(!1)})]})]})}Sc(document.getElementById("root")).render(o.jsx(Hp,{})); +`))!==-1;){const x=v.slice(0,y);v=v.slice(y+2);let g="message",P="";for(const f of x.split(` +`))f.startsWith("event: ")?g=f.slice(7).trim():f.startsWith("data: ")&&(P=f.slice(6));g==="frame"&&P?(t.current&&(t.current.src="data:image/jpeg;base64,"+P),n==null||n("live")):g==="waiting"&&(n==null||n("waiting"))}}}catch{if(l||r!=null&&r.aborted)return;n==null||n("reconnecting")}if(l)return;await new Promise(i=>setTimeout(i,1500))}})(),()=>{l=!0}}function rp({onSignedIn:e}){const[t,n]=j.useState(!1);return t?o.jsx(sp,{onSignedIn:e,onCancel:()=>n(!1)}):o.jsx(lp,{onSignedIn:e,onJoin:()=>n(!0)})}function lp({onSignedIn:e,onJoin:t}){const[n,r]=j.useState(""),[l,s]=j.useState(""),[i,a]=j.useState(""),[u,c]=j.useState(!1),v=async h=>{h.preventDefault(),c(!0),a("");try{await M.login(n.trim(),l),e(await M.me())}catch(m){a(m.message),c(!1)}};return o.jsx("div",{className:"signin",children:o.jsxs("form",{className:"card",onSubmit:v,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:n,autoComplete:"username",autoFocus:!0,required:!0,onChange:h=>r(h.target.value),placeholder:"you@company.com"})]}),o.jsxs("label",{children:["Password",o.jsx("input",{type:"password",value:l,autoComplete:"current-password",required:!0,onChange:h=>s(h.target.value)})]}),i&&o.jsx("p",{className:"error",role:"alert",children:i}),o.jsx("button",{className:"primary",disabled:u||!n||!l,children:u?"Signing in…":"Sign in"}),o.jsxs("p",{className:"foot",children:["Been invited? ",o.jsx("button",{type:"button",className:"linkish",onClick:t,children:"Use your invitation code"})]})]})})}function sp({onSignedIn:e,onCancel:t}){const[n,r]=j.useState(""),[l,s]=j.useState(null),[i,a]=j.useState(""),[u,c]=j.useState(""),[v,h]=j.useState(""),[m,y]=j.useState(""),[x,g]=j.useState(!1),P=async d=>{d.preventDefault(),g(!0),y("");try{const p=await M.previewInvitation(n.trim());s(p),a(p.full_name||"")}catch(p){y(p.message)}finally{g(!1)}},f=async d=>{if(d.preventDefault(),u!==v){y("Those two passwords are not the same.");return}g(!0),y("");try{const p=await M.register({code:n.trim(),full_name:i,password:u});e(p)}catch(p){y(p.message),g(!1)}};return o.jsx("div",{className:"signin",children:o.jsxs("form",{className:"card",onSubmit:l?f:P,children:[o.jsx("span",{className:"mark big","aria-hidden":"true"}),o.jsx("h1",{children:l?`Join ${l.client_name}`:"Behavision"}),l?o.jsxs(o.Fragment,{children:[o.jsxs("p",{className:"sub",children:["You are joining as ",o.jsx("b",{children:l.role}),", signing in with"," ",o.jsx("code",{children:l.email}),". Choose a password only you know."]}),o.jsxs("label",{children:["Your name",o.jsx("input",{value:i,onChange:d=>a(d.target.value),autoFocus:!0,autoComplete:"name"})]}),o.jsxs("label",{children:["Password",o.jsx("input",{type:"password",value:u,required:!0,minLength:8,autoComplete:"new-password",onChange:d=>c(d.target.value)}),o.jsx("span",{className:"hint",children:"At least 8 characters. Longer is the only thing that helps."})]}),o.jsxs("label",{children:["Password again",o.jsx("input",{type:"password",value:v,required:!0,autoComplete:"new-password",onChange:d=>h(d.target.value)})]}),m&&o.jsx("p",{className:"error",role:"alert",children:m}),o.jsx("button",{className:"primary",disabled:x||!u||!v,children:x?"Creating your account…":"Create account and sign in"})]}):o.jsxs(o.Fragment,{children:[o.jsx("p",{className:"sub",children:"Enter the invitation code you were given."}),o.jsxs("label",{children:["Invitation code",o.jsx("input",{value:n,onChange:d=>r(d.target.value),autoFocus:!0,required:!0,autoComplete:"off",spellCheck:"false",placeholder:"ABCDEF-123456-GHIJKL-789012",className:"codefield"}),o.jsx("span",{className:"hint",children:"Dashes and capitals do not matter."})]}),m&&o.jsx("p",{className:"error",role:"alert",children:m}),o.jsx("button",{className:"primary",disabled:x||!n.trim(),children:x?"Checking…":"Continue"})]}),o.jsx("p",{className:"foot",children:o.jsx("button",{type:"button",className:"linkish",onClick:t,children:"Back to sign in"})})]})})}function It(e,t,n=[]){const[r,l]=j.useState(null),[s,i]=j.useState(null),[a,u]=j.useState(!0),c=j.useRef(!1),v=j.useRef(!0),h=j.useCallback(async()=>{if(!c.current){c.current=!0;try{const m=await e();if(!v.current)return;l(m),i(null)}catch(m){if(!v.current)return;i(m)}finally{c.current=!1,v.current&&u(!1)}}},n);return j.useEffect(()=>{if(v.current=!0,h(),!t)return()=>{v.current=!1};const m=setInterval(h,t);return()=>{v.current=!1,clearInterval(m)}},[h,t]),{data:r,error:s,loading:a,reload:h}}function ip(e){const[t,n]=j.useState(null);return j.useEffect(()=>{if(!e){n(null);return}let r=!0,l=null;return M.cameraSnapshot(e).then(s=>{if(!r){URL.revokeObjectURL(s);return}l=s,n(s)}).catch(()=>{r&&n(null)}),()=>{r=!1,l&&URL.revokeObjectURL(l)}},[e]),t}function Xi({image:e,url:t,alt:n}){const r=e?e.url:t,l=e&&!!e.auth||typeof r=="string"&&r.startsWith("/"),s=ip(l?r:null),i=l?s:r;return i?o.jsx("img",{src:i,alt:n,loading:"lazy"}):null}function op({site:e,onClose:t}){const[n,r]=j.useState(null),[l,s]=j.useState(!1),[i,a]=j.useState(""),u=async()=>{s(!0),a(""),r(null);try{r(await M.siteCheck(e.site_id))}catch(c){a(c.message)}finally{s(!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:u,children:"Run the check"})]}),l&&o.jsxs("div",{className:"waiting",children:[o.jsx("span",{className:"spinner","aria-hidden":"true"}),o.jsx("b",{children:"Checking…"})]}),i&&o.jsx("p",{className:"error",role:"alert",children:i}),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:u,children:"Check again"})]}),o.jsx(ap,{site:e})]})]})})}function ap({site:e}){const[t,n]=j.useState(null),[r,l]=j.useState(!1),[s,i]=j.useState(""),[a,u]=j.useState(""),c=async()=>{l(!0),i("");try{n(await M.enrolmentCode(e.site_id,{label:a.trim()}))}catch(v){i(v.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 ",cp(t.expires_at),"."]}),o.jsxs("div",{className:"row",children:[o.jsx("button",{className:"ghost",onClick:()=>up(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:a,placeholder:"counter PC",onChange:v=>u(v.target.value)})]}),s&&o.jsx("p",{className:"error",role:"alert",children:s}),o.jsx("button",{className:"ghost",onClick:c,disabled:r,children:r?"Creating…":"Create an installation code"})]})]})}function up(e){var t;try{(t=navigator.clipboard)==null||t.writeText(e)}catch{}}function cp(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 dp({user:e}){const{data:t,error:n,loading:r,reload:l}=It(()=>M.sites(),2e4,[]),{data:s}=It(()=>M.cameras(),6e4,[]),[i,a]=j.useState(null),[u,c]=j.useState(!1),v=t||[],h=(e==null?void 0:e.role)==="owner";if(r&&!t)return o.jsx(ar,{});if(n)return o.jsx(ur,{error:n});if(!v.length&&!u)return o.jsx(wp,{canOpen:h,onOpen:()=>c(!0)});const m=v.map(f=>{const d=(s||[]).filter(p=>p.site_id===f.site_id);return{site:f,cams:d,verdict:pp(f,d)}}),y=f=>m.filter(d=>d.verdict.tone===f).length,x=y("bad"),g=y("warn"),P=y("idle");return o.jsxs(o.Fragment,{children:[o.jsxs("header",{className:"head",children:[o.jsx("h1",{children:"Shops"}),o.jsxs("p",{className:"sub",children:[v.length," ",v.length===1?"shop":"shops",x>0&&o.jsxs(o.Fragment,{children:[" · ",o.jsxs("b",{className:"bad",children:[x," not working"]})]}),g>0&&o.jsxs(o.Fragment,{children:[" · ",o.jsxs("b",{className:"warn",children:[g," needing attention"]})]}),P>0&&o.jsxs(o.Fragment,{children:[" · ",P," not set up yet"]}),!x&&!g&&!P&&o.jsxs(o.Fragment,{children:[" · ",o.jsx("b",{className:"ok",children:"all working"})]})]}),h&&o.jsx("button",{className:"primary",onClick:()=>c(!0),children:"Open a new shop"})]}),o.jsx("div",{className:"grid sites",children:m.map(f=>o.jsx(mp,{site:f.site,cams:f.cams,verdict:f.verdict,onCheck:()=>a(f.site)},f.site.site_id))}),i&&o.jsx(op,{site:i,onClose:()=>a(null)}),u&&o.jsx(fp,{onClose:()=>c(!1),onCreated:()=>{c(!1),l()}})]})}function fp({onClose:e,onCreated:t}){const[n,r]=j.useState({name:"",slug:"",timezone:"Asia/Kolkata"}),[l,s]=j.useState(!1),[i,a]=j.useState(""),u=v=>h=>r({...n,[v]:h.target.value}),c=async v=>{v.preventDefault(),s(!0),a("");try{t(await M.createSite({name:n.name,slug:n.slug||void 0,timezone:n.timezone}))}catch(h){a(h.message),s(!1)}};return o.jsx("div",{className:"overlay",onClick:e,children:o.jsxs("aside",{className:"drawer narrow",onClick:v=>v.stopPropagation(),children:[o.jsxs("header",{className:"drawer-head",children:[o.jsx("h2",{children:"Open a new shop"}),o.jsx("button",{className:"ghost",onClick:e,children:"Close"})]}),o.jsxs("form",{className:"drawer-body",onSubmit:c,children:[o.jsxs("label",{children:["Shop name",o.jsx("input",{value:n.name,onChange:u("name"),required:!0,autoFocus:!0,placeholder:"TeNext Bengaluru"})]}),o.jsxs("label",{children:["Short name",o.jsx("input",{value:n.slug,onChange:u("slug"),placeholder:"made from the name if left empty"}),o.jsx("span",{className:"hint",children:"Lower-case letters, digits and dashes. It becomes the shop PC’s identity and cannot be changed afterwards."})]}),o.jsxs("label",{children:["Timezone",o.jsx("input",{value:n.timezone,onChange:u("timezone"),required:!0}),o.jsx("span",{className:"hint",children:"Footfall is bucketed by the shop’s own clock, e.g. Asia/Kolkata."})]}),i&&o.jsx("p",{className:"error",role:"alert",children:i}),o.jsx("button",{className:"primary",disabled:l||!n.name.trim(),children:l?"Opening…":"Open shop"})]})]})})}function pp(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:`${bs(n)} of faces too poor to recognise`}:n>.2?{tone:"warn",mark:"!",words:`${bs(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 ${Ft(e.last_heartbeat_at)}`}}const hp={ok:"Working",warn:"Needs attention",bad:"Not working",idle:"Not set up"};function mp({site:e,cams:t,verdict:n,onCheck:r}){const l=e.fraction_below_gate,s=n.tone,i=n.headline||hp[s],a=vp(t),u=e.cameras_total||t.length;return o.jsxs("article",{className:"card site state-"+s,onClick:r,role:"button",tabIndex:0,onKeyDown:c=>c.key==="Enter"&&r(),children:[o.jsxs("div",{className:"shot",children:[a.url?o.jsx(Xi,{image:a,alt:`View inside ${e.name}`}):o.jsxs("div",{className:"noshot",children:[o.jsx(yp,{}),a.reason&&o.jsx("span",{children:a.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:u>0?`${u} ${u===1?"camera":"cameras"}`:"No cameras yet"})]}),o.jsxs("span",{className:"status "+s,children:[o.jsx("i",{"aria-hidden":"true"}),i]})]}),a.at&&o.jsx("span",{className:"shot-age",children:Ft(a.at)})]}),o.jsxs("div",{className:"metrics",children:[o.jsx(ss,{label:"Cameras",value:u?`${e.cameras_up}/${u}`:"—",tone:u?e.cameras_up0?bs(1-l):"—",tone:l?l>.5?"bad":l>.2?"warn":"ok":"idle"}),o.jsx(ss,{label:"Last seen",value:Ft(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 ss({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 vp(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?{...t.snapshot,at:t.snapshot_at}:{reason:e.map(l=>{var s;return(s=l.snapshot)==null?void 0:s.reason}).find(Boolean)||"No picture from this shop yet."}}function yp(){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 bs(e){return`${Math.round(e*100)}%`}function Ft(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 gp(e){if(!e)return"never";const t=new Date(e).getTime();if(Number.isNaN(t))return"—";const n=(t-Date.now())/1e3;if(n<=0)return"expired";const r=Math.round(n/60);if(r<60)return`in ${r} min`;const l=Math.round(r/60);return l<48?`in ${l} h`:`in ${Math.round(l/24)} days`}function ar(){return o.jsxs("div",{className:"state",children:[o.jsx("span",{className:"spinner","aria-hidden":"true"}),"Loading…"]})}function ur({error:e}){return o.jsx("div",{className:"state",children:o.jsx("p",{className:"error",role:"alert",children:e.message})})}function wp({canOpen:e,onOpen:t}){return o.jsxs("div",{className:"state",children:[o.jsx("h2",{children:"No shops yet"}),o.jsx("p",{className:"sub",children:e?"Open your first shop, then set up its PC and cameras from its card.":"The owner opens shops; they appear here once created."}),e&&o.jsx("button",{className:"primary",onClick:t,children:"Open a new shop"})]})}const ua=60;function xp(){const[e,t]=j.useState([]),[n,r]=j.useState("connecting"),[l,s]=j.useState(null),i=j.useRef(new Set);return j.useEffect(()=>{const a=new AbortController;let u=()=>{};return(async()=>{try{const c=await M.arrivals({limit:30}),v=c.arrivals||[];v.forEach(h=>i.current.add(h.visit_id)),t(v.slice().reverse()),r("live"),u=tp({cursor:c.cursor,signal:a.signal,onPage:h=>{const m=(h.arrivals||[]).filter(y=>!i.current.has(y.visit_id));m.length&&(m.forEach(y=>i.current.add(y.visit_id)),r("live"),t(y=>[...m.reverse(),...y].slice(0,ua)))},onError:()=>r("reconnecting")})}catch(c){s(c)}})(),()=>{a.abort(),u()}},[]),l?o.jsx(ur,{error:l}):n==="connecting"&&!e.length?o.jsx(ar,{}):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 ",ua," 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(a=>o.jsx(kp,{a},a.visit_id))})]})}function kp({a:e}){const t=e.name||e.label||"Unidentified";return o.jsxs("li",{className:"card arrival",children:[o.jsx(jp,{image:e.image,name:t,customerRef:e.visitor_ref}),o.jsxs("div",{className:"who-col",children:[o.jsx("strong",{children:t}),o.jsxs("span",{className:"sub",children:[e.name&&e.visitor_ref?`${e.visitor_ref} · `:"",e.site,e.camera_id?` · ${e.camera_id}`:""," · ",Ft(e.occurred_at)]}),e.attributes&&o.jsx(Cp,{attrs:e.attributes})]}),e.is_new_visitor?o.jsx("span",{className:"pill new",children:"New"}):o.jsx("span",{className:"pill",children:"Returning"})]})}function jp({image:e,name:t,customerRef:n}){return e!=null&&e.available?o.jsx("span",{className:"face",children:o.jsx(Xi,{image:e,alt:""})}):o.jsx("span",{className:"face initials",title:(e==null?void 0:e.reason)||"","aria-hidden":"true",children:Sp(t,n)})}function Sp(e,t){const n=/^Visitor (\d+)$/.exec(String(e).trim());if(n)return n[1];if(t){const r=/^V-(\d+)$/.exec(t);if(r)return r[1]}return Np(e)}function Np(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 Cp({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}function Ep({camera:e,onClose:t}){const n=j.useRef(null),[r,l]=j.useState("waiting"),[s,i]=j.useState(!1);j.useEffect(()=>{const c=new AbortController,v=np({cameraId:e.id,img:n,onState:m=>{l(m),m==="live"&&i(!1)},signal:c.signal}),h=setTimeout(()=>i(!0),12e3);return()=>{v(),c.abort(),clearTimeout(h)}},[e.id]);const a=e.connected===!1?"This camera was not connecting when the shop PC last reported. Check it is powered on and reachable on the shop’s network.":e.connected==null?"The shop PC has not reported on this camera yet. It may still be starting up.":"The shop PC is not sending frames. It may be offline, or its Behavision app may not be running.",u=s&&r!=="live"?a:{waiting:"Asking the shop PC…",live:"Live",reconnecting:"Reconnecting…"}[r];return o.jsx("div",{className:"overlay",onClick:t,children:o.jsxs("aside",{className:"drawer live",onClick:c=>c.stopPropagation(),children:[o.jsxs("header",{className:"drawer-head",children:[o.jsxs("div",{children:[o.jsx("h2",{children:e.label}),o.jsx("p",{className:"sub",children:e.site})]}),o.jsx("button",{className:"ghost",onClick:t,children:"Close"})]}),o.jsxs("div",{className:"drawer-body",children:[o.jsxs("div",{className:"liveshot",children:[o.jsx("img",{ref:n,alt:`Live view from ${e.label}`}),r!=="live"&&o.jsx("div",{className:"livewait",children:o.jsx("span",{children:s?"No picture yet":u})})]}),o.jsx("p",{className:"hint",style:{marginTop:10},children:r==="live"?"Live. The shop only uploads while this view is open.":u})]})]})})}const Br=[{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:""}],ca=e=>Br.find(t=>t.id===e)||Br[Br.length-1],_p=["Camera","Connection","Test","Walk past"];function Pp({sites:e,existing:t,onClose:n,onSaved:r}){var d;const l=!(t!=null&&t.id),[s,i]=j.useState(l?0:2),[a,u]=j.useState(t!=null&&t.id?t:null),[c,v]=j.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:""}),[h,m]=j.useState(!1),[y,x]=j.useState(""),g=p=>w=>v(S=>({...S,[p]:w.target.value})),P=p=>{const w=ca(p.target.value);v(S=>({...S,make:w.id,path:w.path||S.path}))},f=async()=>{m(!0),x("");const p={};for(const[w,S]of Object.entries(c))w==="site_id"||w==="make"||S===""||S==null||(p[w]=w==="port"?Number(S):S);try{const w=a!=null&&a.id?await M.updateCamera(a.id,p):await M.createCamera(c.site_id,p);u(w),i(2),r==null||r(w,{keepOpen:!0})}catch(w){x(w.message)}finally{m(!1)}};return o.jsx("div",{className:"overlay",onClick:n,children:o.jsxs("aside",{className:"drawer wizard",onClick:p=>p.stopPropagation(),children:[o.jsxs("header",{className:"drawer-head",children:[o.jsxs("div",{children:[o.jsx("h2",{children:l?"Set up a camera":(a==null?void 0:a.label)||(t==null?void 0:t.label)}),o.jsx("p",{className:"sub",children:s<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:_p.map((p,w)=>o.jsxs("li",{className:w===s?"now":wo.jsx("option",{value:p.site_id,children:p.name},p.site_id))})]}),o.jsxs("label",{children:["What should staff call it?",o.jsx("input",{value:c.label,onChange:g("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:P,children:Br.map(p=>o.jsx("option",{value:p.id,children:p.label},p.id))}),o.jsx("span",{className:"hint",children:ca(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:()=>i(1),children:"Next"})]}),s===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:g("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:g("port")})]}),o.jsxs("label",{children:["Stream path",o.jsx("input",{value:c.path,onChange:g("path")})]})]}),o.jsxs("label",{children:["Camera username",o.jsx("input",{value:c.username,onChange:g("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:g("password"),name:"camera-secret",autoComplete:"new-password",placeholder:a!=null&&a.has_password?"(unchanged)":""})]}),y&&o.jsx("p",{className:"error",role:"alert",children:y}),o.jsxs("div",{className:"pair",children:[o.jsx("button",{className:"ghost",onClick:()=>i(0),children:"Back"}),o.jsx("button",{className:"primary",disabled:h||!c.host.trim(),onClick:f,children:h?"Saving…":"Save and test"})]})]}),s>=2&&a&&o.jsx(Tp,{camera:a,step:s,onStep:i,onUpdated:p=>{u(p),r==null||r(p,{keepOpen:!0})},onEdit:()=>i(1),onDone:n})]})})}function Tp({camera:e,step:t,onStep:n,onUpdated:r,onEdit:l,onDone:s}){const[i,a]=j.useState(e),[u,c]=j.useState(!1),[v,h]=j.useState(""),m=j.useRef(null);j.useEffect(()=>a(e),[e]);const y=i.check||{},x=y.state==="requested"||y.state==="running";j.useEffect(()=>{if(!x)return;let f=!0;const d=async()=>{try{const w=(await M.cameras()).find(S=>S.id===i.id);f&&w&&(a(w),r==null||r(w))}catch{}};return m.current=setInterval(d,4e3),()=>{f=!1,clearInterval(m.current)}},[x,i.id]);const g=async(f,d)=>{c(!0),h("");try{const p=await M.checkCamera(i.id,f,d);a(p),r==null||r(p),n(f==="placement"?3:2)}catch(p){h(p.message)}finally{c(!1)}},P=y.kind==="connection"&&y.state==="done"&&y.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."]})]}),x&&o.jsxs("div",{className:"waiting",children:[o.jsx("span",{className:"spinner","aria-hidden":"true"}),o.jsxs("div",{children:[o.jsx("b",{children:y.state==="running"?"Running on the shop’s PC…":"Waiting for the shop’s PC…"}),o.jsx("span",{className:"sub",children:y.state==="running"?"This takes about "+(y.seconds||25)+" seconds.":"It picks up the request within a couple of minutes."})]})]}),y.state==="done"&&o.jsx(zp,{check:y}),v&&o.jsx("p",{className:"error",role:"alert",children:v}),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:u||x,onClick:()=>g("connection"),children:y.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:u||x,onClick:()=>g("placement",25),children:y.state==="done"?"Check again":"Start walk-past check"})]})}),t===2&&P&&!x&&o.jsx("button",{className:"primary",onClick:()=>n(3),children:"Next: prove it can recognise faces"}),t===3&&y.state==="done"&&!x&&o.jsx("button",{className:"ghost",onClick:s,children:"Finish"})]})})}function zp({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((s,i)=>o.jsx("li",{children:s},i))}),((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 Lp(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 Rp({user:e}){const{data:t,error:n,loading:r,reload:l}=It(()=>M.cameras(),2e4,[]),{data:s}=It(()=>M.sites(),0,[]),[i,a]=j.useState(null),[u,c]=j.useState(null),v=["admin","owner","manager"].includes(e.role),h=t||[],m=h.filter(g=>g.connected).length,y=h.filter(g=>g.connected===!1).length,x=h.filter(g=>g.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:[h.length," ",h.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"]})]}),x>0&&o.jsxs(o.Fragment,{children:[" · ",x," waiting for the shop PC"]})]}),v&&o.jsx("button",{className:"primary",onClick:()=>a({}),children:"Set up a camera"})]}),r&&!t?o.jsx(ar,{}):n?o.jsx(ur,{error:n}):h.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:h.map(g=>o.jsx(Op,{cam:g,canEdit:v,onEdit:()=>a(g),onWatch:()=>c(g)},g.id))}),i&&o.jsx(Pp,{existing:i.id?i:null,sites:s||[],onClose:()=>{a(null),l()},onSaved:(g,P)=>{P!=null&&P.keepOpen||a(null),l()}}),u&&o.jsx(Ep,{camera:u,onClose:()=>c(null)})]})}function Op({cam:e,canEdit:t,onEdit:n,onWatch:r}){var a,u;const l=e.connected==null?"idle":e.connected?"ok":"bad",s=e.connected==null?"Waiting for the shop PC":e.connected?"Connected":"Not connecting",i=Lp(e);return o.jsxs("article",{className:"card cam state-"+l,onClick:t?n:void 0,role:t?"button":void 0,tabIndex:t?0:void 0,onKeyDown:c=>t&&c.key==="Enter"&&n(),children:[o.jsxs("div",{className:"shot",children:[(a=e.snapshot)!=null&&a.available?o.jsx(Xi,{image:e.snapshot,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 "+l,children:[o.jsx("i",{"aria-hidden":"true"}),s]})]}),e.snapshot_at&&o.jsx("span",{className:"shot-age",children:Ft(e.snapshot_at)}),o.jsxs("button",{className:"watch",title:"Watch this camera now",onClick:c=>{c.stopPropagation(),r()},children:[o.jsx("i",{"aria-hidden":"true"}),"Live"]})]}),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 Ip=["Is everything working today?","Why is footfall low at Chennai?","How many people came in last week?","Which cameras still need checking?"];function Fp({open:e,onClose:t}){const[n,r]=j.useState([]),[l,s]=j.useState(""),[i,a]=j.useState(!1),[u,c]=j.useState(!1),v=j.useRef(null),h=j.useRef(null);j.useEffect(()=>{var y;e&&((y=h.current)==null||y.focus())},[e]),j.useEffect(()=>{var y;(y=v.current)==null||y.scrollIntoView({behavior:"smooth",block:"end"})},[n,i]);const m=async y=>{const x=(y??l).trim();if(!x||i)return;const g=[...n,{role:"user",text:x}];r(g),s(""),a(!0);try{const P=await M.ask(g);r(f=>[...f,{role:"assistant",text:P.text,used:P.used}])}catch(P){P.code==="assistant_off"?c(!0):r(f=>[...f,{role:"assistant",text:P.message,failed:!0}])}finally{a(!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:[u?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:"}),Ip.map(y=>o.jsx("button",{className:"chip",onClick:()=>m(y),children:y},y))]}):n.map((y,x)=>{var g;return o.jsxs("div",{className:"bubble "+y.role+(y.failed?" failed":""),children:[y.text,((g=y.used)==null?void 0:g.length)>0&&o.jsxs("span",{className:"used",children:["looked at: ",y.used.map(Dp).join(", ")]})]},x)}),i&&o.jsxs("div",{className:"bubble assistant-thinking",children:[o.jsx("span",{className:"spinner","aria-hidden":"true"})," Looking…"]}),o.jsx("div",{ref:v})]}),!u&&o.jsxs("form",{className:"assistant-ask",onSubmit:y=>{y.preventDefault(),m()},children:[o.jsx("input",{ref:h,value:l,onChange:y=>s(y.target.value),placeholder:"Ask about your shops…",disabled:i}),o.jsx("button",{className:"primary",disabled:i||!l.trim(),children:"Ask"})]})]}):null}function Dp(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 Mp(){const{data:e,error:t,loading:n,reload:r}=It(()=>M.clients(),0,[]),[l,s]=j.useState(!1),[i,a]=j.useState(null),u=e||[];return o.jsxs(o.Fragment,{children:[o.jsxs("header",{className:"head",children:[o.jsx("h1",{children:"Companies"}),o.jsx("button",{className:"primary",onClick:()=>{a(null),s(!0)},children:"New company"})]}),i&&o.jsx(Up,{result:i,onDismiss:()=>a(null)}),n&&!e?o.jsx(ar,{}):t?o.jsx(ur,{error:t}):u.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:u.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:Ft(c.created_at)})]},c.id))})]})}),l&&o.jsx($p,{onClose:()=>s(!1),onCreated:c=>{s(!1),a(c),r()}})]})}function $p({onClose:e,onCreated:t}){const[n,r]=j.useState({company_name:"",owner_name:"",owner_email:"",password:""}),[l,s]=j.useState(!1),[i,a]=j.useState(""),u=v=>h=>r({...n,[v]:h.target.value}),c=async v=>{v.preventDefault(),s(!0),a("");try{t(await M.createClient(n))}catch(h){a(h.message),s(!1)}};return o.jsx("div",{className:"overlay",onClick:e,children:o.jsxs("aside",{className:"drawer narrow",onClick:v=>v.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:u("company_name"),required:!0,autoFocus:!0})]}),o.jsxs("label",{children:["Owner’s name",o.jsx("input",{value:n.owner_name,onChange:u("owner_name")})]}),o.jsxs("label",{children:["Owner’s email",o.jsx("input",{type:"email",value:n.owner_email,onChange:u("owner_email"),required:!0})]}),o.jsxs("label",{children:["Password",o.jsx("input",{type:"text",value:n.password,onChange:u("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."})]}),i&&o.jsx("p",{className:"error",role:"alert",children:i}),o.jsx("button",{className:"primary",disabled:l,children:l?"Creating…":"Create company"})]})]})})}function Up({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"})]})}function Ap({user:e}){const t=It(()=>M.team(),0,[]),n=It(()=>M.invitations(),0,[]),[r,l]=j.useState(!1),[s,i]=j.useState(null),[a,u]=j.useState(""),[c,v]=j.useState(""),h=e.role==="owner"||e.role==="manager",m=t.data||[],y=n.data||[],x=async(g,P)=>{u(g),v("");try{await M.updateMember(g,P),t.reload()}catch(f){v(f.message)}finally{u("")}};return o.jsxs(o.Fragment,{children:[o.jsxs("header",{className:"head",children:[o.jsx("h1",{children:"Team"}),h&&o.jsx("button",{className:"primary",onClick:()=>{i(null),l(!0)},children:"Invite someone"})]}),s&&o.jsx(Vp,{invite:s,onDismiss:()=>i(null)}),c&&o.jsx("p",{className:"error",role:"alert",children:c}),t.loading&&!t.data?o.jsx(ar,{}):t.error?o.jsx(ur,{error:t.error}):o.jsx("div",{className:"tablewrap",children:o.jsxs("table",{className:"rows",children:[o.jsx("thead",{children:o.jsxs("tr",{children:[o.jsx("th",{children:"Name"}),o.jsx("th",{children:"Email"}),o.jsx("th",{children:"Role"}),o.jsx("th",{children:"Last signed in"}),h&&o.jsx("th",{})]})}),o.jsx("tbody",{children:m.map(g=>o.jsxs("tr",{className:g.active?"":"inactive",children:[o.jsxs("td",{children:[o.jsx("strong",{children:g.full_name||"—"}),!g.active&&o.jsx("span",{className:"pill muted",children:"No access"})]}),o.jsx("td",{children:o.jsx("code",{children:g.email})}),o.jsx("td",{children:h&&g.id!==e.id?o.jsxs("select",{value:g.role,disabled:a===g.id,onChange:P=>x(g.id,{role:P.target.value}),children:[o.jsx("option",{value:"staff",children:"Staff"}),o.jsx("option",{value:"manager",children:"Manager"}),e.role==="owner"&&o.jsx("option",{value:"owner",children:"Owner"})]}):o.jsx("span",{className:"role",children:g.role})}),o.jsx("td",{className:"sub",children:g.last_login_at?Ft(g.last_login_at):"Never"}),h&&o.jsx("td",{className:"right",children:g.id===e.id?null:g.active?o.jsx("button",{className:"ghost danger",disabled:a===g.id,onClick:()=>x(g.id,{active:!1}),children:"Remove access"}):o.jsx("button",{className:"ghost",disabled:a===g.id,onClick:()=>x(g.id,{active:!0}),children:"Restore"})})]},g.id))})]})}),h&&y.length>0&&o.jsxs("section",{className:"pending",children:[o.jsx("h2",{children:"Waiting to join"}),o.jsx("ul",{className:"invites",children:y.map(g=>o.jsxs("li",{className:"card invite",children:[o.jsxs("div",{children:[o.jsx("strong",{children:g.email}),o.jsxs("span",{className:"sub",children:["invited as ",g.role,g.invited_by?` by ${g.invited_by}`:""," · expires ",gp(g.expires_at)]})]}),o.jsx("button",{className:"ghost danger",onClick:async()=>{await M.revokeInvitation(g.id),n.reload()},children:"Withdraw"})]},g.id))})]}),r&&o.jsx(Bp,{canMintOwner:e.role==="owner",onClose:()=>l(!1),onDone:g=>{l(!1),i(g),n.reload()}})]})}function Bp({canMintOwner:e,onClose:t,onDone:n}){const[r,l]=j.useState({email:"",full_name:"",role:"staff"}),[s,i]=j.useState(!1),[a,u]=j.useState(""),c=h=>m=>l({...r,[h]:m.target.value}),v=async h=>{h.preventDefault(),i(!0),u("");try{n(await M.invite(r))}catch(m){u(m.message),i(!1)}};return o.jsx("div",{className:"overlay",onClick:t,children:o.jsxs("aside",{className:"drawer narrow",onClick:h=>h.stopPropagation(),children:[o.jsxs("header",{className:"drawer-head",children:[o.jsx("h2",{children:"Invite someone"}),o.jsx("button",{className:"ghost",onClick:t,children:"Close"})]}),o.jsxs("form",{className:"drawer-body",onSubmit:v,children:[o.jsxs("label",{children:["Their email",o.jsx("input",{type:"email",value:r.email,onChange:c("email"),required:!0,autoFocus:!0,autoComplete:"off",name:"invitee"}),o.jsx("span",{className:"hint",children:"This is the address they will sign in with, and it is fixed by the invitation — passing the code on cannot make it somebody else’s account."})]}),o.jsxs("label",{children:["Their name",o.jsx("input",{value:r.full_name,onChange:c("full_name"),autoComplete:"off",name:"invitee-name"})]}),o.jsxs("label",{children:["Role",o.jsxs("select",{value:r.role,onChange:c("role"),children:[o.jsx("option",{value:"staff",children:"Staff — see customers and shops"}),o.jsx("option",{value:"manager",children:"Manager — also set up cameras and invite people"}),e&&o.jsx("option",{value:"owner",children:"Owner — full control"})]})]}),a&&o.jsx("p",{className:"error",role:"alert",children:a}),o.jsx("button",{className:"primary",disabled:s,children:s?"Creating…":"Create invitation"})]})]})})}function Vp({invite:e,onDismiss:t}){return o.jsxs("div",{className:"banner ok credentials",children:[o.jsxs("div",{children:[o.jsxs("b",{children:["Invitation for ",e.email,"."]})," Give them this code. It is shown once, works once, and cannot be recovered.",o.jsxs("dl",{className:"creds",children:[o.jsxs("div",{children:[o.jsx("dt",{children:"Code"}),o.jsx("dd",{children:o.jsx("code",{className:"big",children:e.code})})]}),o.jsxs("div",{children:[o.jsx("dt",{children:"Role"}),o.jsx("dd",{children:e.role})]})]}),o.jsx("p",{className:"sub",children:"They open the app, choose “I have an invitation code”, and pick their own password. Nobody else ever sees it."})]}),o.jsx("button",{className:"ghost",onClick:t,children:"Done"})]})}const Wp=[{id:"sites",label:"Shops",View:dp},{id:"live",label:"Live",View:xp},{id:"cameras",label:"Cameras",View:Rp},{id:"team",label:"Team",View:Ap}],Hp=[{id:"clients",label:"Companies",View:Mp}];function Qp(){const[e,t]=j.useState(null),[n,r]=j.useState(!0),[l,s]=j.useState("sites"),[i,a]=j.useState(!1);if(j.useEffect(()=>{(async()=>{if(ep())try{t(await M.me())}catch{or()}r(!1)})()},[]),n)return o.jsxs("div",{className:"boot",children:[o.jsx("span",{className:"spinner","aria-hidden":"true"}),"Loading…"]});if(!e)return o.jsx(rp,{onSignedIn:t});const u=e.role==="admin"&&!e.client_id,c=u?Hp:Wp,v=c.find(y=>y.id===l)||c[0],h=v.View,m=async()=>{await M.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(y=>o.jsx("button",{onClick:()=>s(y.id),"aria-current":v.id===y.id?"page":void 0,children:y.label},y.id))}),o.jsxs("div",{className:"who",children:[o.jsxs("button",{className:"ask-btn"+(i?" on":""),onClick:()=>a(y=>!y),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:u?"platform admin":e.role}),o.jsx("button",{className:"ghost",onClick:m,children:"Sign out"})]})]}),o.jsxs("div",{className:"with-assistant"+(i?" open":""),children:[o.jsx("main",{className:"page",children:o.jsx(h,{user:e})}),o.jsx(Fp,{open:i,onClose:()=>a(!1)})]})]})}Sc(document.getElementById("root")).render(o.jsx(Qp,{})); diff --git a/server/internal/web/dist/index.html b/server/internal/web/dist/index.html index 5a391c2..39ac988 100644 --- a/server/internal/web/dist/index.html +++ b/server/internal/web/dist/index.html @@ -5,7 +5,7 @@ Behavision - + diff --git a/web/src/api.js b/web/src/api.js index 8c7953d..13d07b8 100644 --- a/web/src/api.js +++ b/web/src/api.js @@ -205,6 +205,7 @@ export const api = { me: () => send('GET', '/api/auth/me'), sites: () => send('GET', '/api/sites'), + createSite: (input) => send('POST', '/api/sites', input), // The live arrivals feed. `cursor` is opaque and must be echoed back. arrivals: (params) => send('GET', '/api/visits' + qs(params)), diff --git a/web/src/views/Sites.jsx b/web/src/views/Sites.jsx index d3df971..394b8bb 100644 --- a/web/src/views/Sites.jsx +++ b/web/src/views/Sites.jsx @@ -17,18 +17,22 @@ import SiteCheck from './SiteCheck.jsx' // shops. So the shop's own camera view is the card, the numbers sit under it, // and one line says what to do — with the technical detail one click away // rather than on the surface. -export default function Sites() { - const { data, error, loading } = usePolled(() => api.sites(), 20000, []) +export default function Sites({ user }) { + const { data, error, loading, reload } = usePolled(() => api.sites(), 20000, []) // Cameras come from a second call and are joined here rather than server-side: // the picture is decoration on this screen, so it must never be able to make // the health list fail. If this errors the cards simply have no photograph. const { data: cams } = usePolled(() => api.cameras(), 60000, []) const [checking, setChecking] = useState(null) + const [opening, setOpening] = useState(false) const sites = data || [] + // Opening a shop is the owner's: it is a billing and tenancy object, not a + // setting. Managers set up the PC and cameras once it exists. + const canOpen = user?.role === 'owner' if (loading && !data) return if (error) return - if (!sites.length) return + if (!sites.length && !opening) return setOpening(true)} /> // Counted from the same verdicts the cards show. Summarising with a second, // simpler rule up here is how a header ends up reading "all working" over a @@ -51,6 +55,9 @@ export default function Sites() { {fresh > 0 && <> · {fresh} not set up yet} {!broken && !watch && !fresh && <> · all working}

+ {canOpen && ( + + )}
@@ -64,10 +71,66 @@ export default function Sites() { button on the camera screen that always checked sites[0], so with two shops the second could not be checked at all. */} {checking && setChecking(null)} />} + {opening && setOpening(false)} + onCreated={() => { setOpening(false); reload() }} />} ) } +// Until this existed a shop was a command on the server plus a hand edit of +// the broker's password file - every new branch a support ticket, and the one +// piece of onboarding that could not be done from the product. The server now +// registers the shop's broker login as it creates the row, so the next step +// really is just "Set up a shop PC" on the new card. +function NewShop({ onClose, onCreated }) { + const [form, setForm] = useState({ name: '', slug: '', timezone: 'Asia/Kolkata' }) + const [busy, setBusy] = useState(false) + const [error, setError] = useState('') + const set = (k) => (e) => setForm({ ...form, [k]: e.target.value }) + + const submit = async (e) => { + e.preventDefault() + setBusy(true); setError('') + try { + onCreated(await api.createSite({ name: form.name, slug: form.slug || undefined, timezone: form.timezone })) + } catch (err) { + setError(err.message) + setBusy(false) + } + } + + return ( +
+ +
+ ) +} + // The one line the card leads with, in severity order. Only the first is shown: // a shop that is offline AND has a bad camera needs its PC turned on first, and // listing both invites someone to start with the wrong one. @@ -272,13 +335,15 @@ export function Problem({ error }) { ) } -function Empty() { +function Empty({ canOpen, onOpen }) { return (

No shops yet

- A shop appears here once its PC has been claimed with an enrolment code. + {canOpen ? 'Open your first shop, then set up its PC and cameras from its card.' + : 'The owner opens shops; they appear here once created.'}

+ {canOpen && }
) }