diff --git a/.gitignore b/.gitignore index a0368a0..f5ed2b3 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,9 @@ web/node_modules/ *.log .DS_Store +# Local planning notes +/PLAN.md + # NOTE: internal/webui/dist IS committed on purpose. It is the embedded web UI, # and keeping it in the tree means `go build` alone produces a working binary # without a Node toolchain. Regenerate it with `make ui`. diff --git a/README.md b/README.md index e8162d6..313aa3f 100644 --- a/README.md +++ b/README.md @@ -31,17 +31,17 @@ reach it — see [sources](#sources). ```bash git clone https://git.azuze.fr/kawa/DockMV.git dockmv && cd dockmv docker compose up -d -docker compose logs dockmv # prints the URL, including the access token +docker compose logs dockmv # prints the URL ``` -Open the printed URL. It binds to `127.0.0.1` only; reach it from your laptop with a tunnel: +Open the printed URL, create the first account (there is no default login), and you're in. It binds to +`127.0.0.1` only; reach it from your laptop with a tunnel: ```bash ssh -L 8080:127.0.0.1:8080 you@source-host ``` -Uses the published image `git.azuze.fr/kawa/dockmv:latest`. Pin a version with `VERSION=v1.2.0 docker compose up -d`, -and set a fixed token with `DOCKMV_TOKEN` in the compose file to keep the same URL across restarts. +Uses the published image `git.azuze.fr/kawa/dockmv:latest`. Pin a version with `VERSION=v1.2.0 docker compose up -d`. ### Prebuilt binary @@ -190,7 +190,12 @@ carries only genuine run-time overrides and keeps working when the image is upda The tool can stop containers and read every volume on the host, so it is treated as a privileged admin tool: -- Binds to **`127.0.0.1` by default**. Binding elsewhere auto-generates an access token and prints it. +- Binds to **`127.0.0.1` by default**. A fresh install has no account yet, and refuses to bind + anywhere else until one is created — finish setup over an SSH tunnel first (see above). +- **Local accounts, no roles yet.** The first visit creates an account; there is no default login. + Every account can manage every other account and every resource — the same single-workspace model + the rest of the tool already has. Scripted access uses a **personal API token** (Account tab), + sent as `X-Auth-Token`, instead of the old shared `--token`. - **SSH host keys are verified** like OpenSSH, for sources as well as targets. An unknown key is refused until you approve the fingerprint in the UI; a *changed* key is refused outright. Trusted keys go to `/known_hosts`. @@ -255,23 +260,38 @@ dockmv version ``` --addr string address to listen on (default "127.0.0.1:8080") ---token string require this token on every request; "auto" generates one ---data-dir string sources, connections and trusted host keys (default: OS config dir) +--data-dir string accounts, sources, connections and trusted host keys (default: OS config dir) --package-dir string where migration packages are written (default /packages) --docker-host string local source docker daemon (default: the DOCKER_HOST environment); given explicitly, it overrides the remembered source -v verbose logging ``` +Binding to anything but a loopback address refuses to start until an account exists — see the Safety +section above. Set `DOCKMV_TRUST_ADDR=1` to skip that check when something else already restricts +exposure, e.g. a container's own port mapping (the shipped `docker-compose.yml` does this). + `inspect` is handy for scripting and for reporting bugs: ```bash dockmv inspect --sizes | jq '.containers[] | {name, image, mounts}' ``` -Everything the UI does is available over HTTP. Pass the token as `X-Auth-Token` when one is set. +Everything the UI does is available over HTTP. The browser uses a session cookie, set by +`/api/login`; scripts use a personal API token from the Account tab, passed as `X-Auth-Token` or +`Authorization: Bearer …`. ``` +POST /api/setup create the first account (only while none exists) +POST /api/login +POST /api/logout +GET /api/me who's signed in, or whether setup is still needed +GET /api/users every account +POST /api/users create another account +DELETE /api/users/{id} +GET /api/tokens the caller's own personal API tokens +POST /api/tokens create one; the plaintext is only ever shown once +DELETE /api/tokens/{id} GET /api/health GET /api/source inventory + default selections GET /api/source/sizes volume sizes (slow) diff --git a/docker-compose.yml b/docker-compose.yml index da9dab9..8b6ef57 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,11 +1,12 @@ # dockmv, running on the SOURCE host. # # docker compose up -d -# docker compose logs dockmv # the URL and access token are printed here +# docker compose logs dockmv # the URL is printed here # # The container needs the Docker socket to read containers and stream their # data. That is equivalent to root on this host, so the UI is protected by a -# generated token and should not be published to an untrusted network. +# login (created on first visit) and should not be published to an untrusted +# network. services: dockmv: @@ -18,12 +19,15 @@ services: - "127.0.0.1:8080:8080" volumes: - /var/run/docker.sock:/var/run/docker.sock - # Connections, trusted SSH host keys and built packages. + # Accounts, connections, trusted SSH host keys and built packages. - migrate-data:/data environment: - # Set a fixed token to keep the same URL across restarts. - # DOCKMV_TOKEN: change-me TZ: ${TZ:-UTC} + # dockmv listens on 0.0.0.0 *inside* the container so the ports: mapping + # above can reach it; that mapping (127.0.0.1 on the host side) is the + # actual loopback restriction, not this bind address. This tells dockmv + # not to second-guess that and refuse to start before setup is done. + DOCKMV_TRUST_ADDR: "1" command: - serve - --addr=0.0.0.0:8080 diff --git a/internal/api/auth.go b/internal/api/auth.go new file mode 100644 index 0000000..80cc5cf --- /dev/null +++ b/internal/api/auth.go @@ -0,0 +1,227 @@ +package api + +import ( + "context" + "net" + "net/http" + "strings" + + "github.com/arescom/dockmv/internal/session" + "github.com/arescom/dockmv/internal/store" +) + +// Identity is the authenticated caller, attached to the request context by +// auth(). Every handler reached past auth() has one. +type Identity struct { + UserID string + Username string +} + +type identityKey struct{} + +func identityFrom(r *http.Request) Identity { + id, _ := r.Context().Value(identityKey{}).(Identity) + return id +} + +// publicPaths need no authentication: they are how a fresh install creates +// its first account, how a session is established or torn down, and the +// health check the UI's own bootstrap (and Docker's HEALTHCHECK) depend on. +var publicPaths = map[string]bool{ + "/api/setup": true, + "/api/login": true, + "/api/logout": true, + "/api/me": true, + "/api/health": true, +} + +// auth gates every /api/* request behind a session cookie or a personal API +// token. +// +// Static assets are served without it: see the comment on spaHandler. Nothing +// sensitive lives in the bundle; every piece of data is behind /api. +func (s *Server) auth(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if !strings.HasPrefix(r.URL.Path, "/api/") || publicPaths[r.URL.Path] { + next.ServeHTTP(w, r) + return + } + + if c, err := r.Cookie(session.CookieName); err == nil { + if sess, ok := s.sessions.Get(c.Value); ok { + s.serveAs(w, r, next, Identity{UserID: sess.UserID, Username: sess.Username}) + return + } + } + + if got := bearerToken(r); got != "" { + if tok, err := s.tokens.Authenticate(got); err == nil { + if usr, err := s.users.Get(tok.UserID); err == nil { + s.serveAs(w, r, next, Identity{UserID: usr.ID, Username: usr.Username}) + return + } + } + } + + writeError(w, http.StatusUnauthorized, "authentication required") + }) +} + +func (s *Server) serveAs(w http.ResponseWriter, r *http.Request, next http.Handler, id Identity) { + ctx := context.WithValue(r.Context(), identityKey{}, id) + next.ServeHTTP(w, r.WithContext(ctx)) +} + +func bearerToken(r *http.Request) string { + if got := r.Header.Get("X-Auth-Token"); got != "" { + return got + } + if h := r.Header.Get("Authorization"); strings.HasPrefix(h, "Bearer ") { + return strings.TrimPrefix(h, "Bearer ") + } + return "" +} + +// isHTTPS reports whether the request reached us over TLS, directly or +// through a reverse proxy that sets the standard forwarded-proto header. It +// decides the session cookie's Secure attribute. +func isHTTPS(r *http.Request) bool { + return r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https" +} + +func setSessionCookie(w http.ResponseWriter, r *http.Request, sess session.Session) { + http.SetCookie(w, &http.Cookie{ + Name: session.CookieName, + Value: sess.ID, + Path: "/", + HttpOnly: true, + Secure: isHTTPS(r), + SameSite: http.SameSiteLaxMode, + Expires: sess.ExpiresAt, + }) +} + +func clearSessionCookie(w http.ResponseWriter, r *http.Request) { + http.SetCookie(w, &http.Cookie{ + Name: session.CookieName, + Value: "", + Path: "/", + HttpOnly: true, + Secure: isHTTPS(r), + SameSite: http.SameSiteLaxMode, + MaxAge: -1, + }) +} + +// remoteIP strips the port from RemoteAddr, for the login rate limiter. +func remoteIP(r *http.Request) string { + host, _, err := net.SplitHostPort(r.RemoteAddr) + if err != nil { + return r.RemoteAddr + } + return host +} + +type setupRequest struct { + Username string `json:"username"` + Password string `json:"password"` +} + +// handleSetup creates the first account. It only succeeds while no account +// exists yet; main.go also refuses to bind non-loopback until then, so this +// endpoint being open to anyone is not a standing risk. +func (s *Server) handleSetup(w http.ResponseWriter, r *http.Request) { + if !s.NeedsSetup() { + writeError(w, http.StatusConflict, "setup has already been completed") + return + } + var req setupRequest + if err := decode(r, &req); err != nil { + writeError(w, http.StatusBadRequest, "%v", err) + return + } + usr, err := s.users.Create(req.Username, req.Password) + if err != nil { + writeError(w, http.StatusBadRequest, "%v", err) + return + } + s.completeLogin(w, r, usr) +} + +type loginRequest struct { + Username string `json:"username"` + Password string `json:"password"` +} + +func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) { + ip := remoteIP(r) + if !s.logins.Allow(ip) { + writeError(w, http.StatusTooManyRequests, "too many attempts; try again shortly") + return + } + var req loginRequest + if err := decode(r, &req); err != nil { + writeError(w, http.StatusBadRequest, "%v", err) + return + } + usr, err := s.users.Verify(req.Username, req.Password) + if err != nil { + s.logins.Fail(ip) + writeError(w, http.StatusUnauthorized, "invalid username or password") + return + } + s.logins.Reset(ip) + _ = s.users.TouchLastLogin(usr.ID) + s.completeLogin(w, r, usr) +} + +func (s *Server) completeLogin(w http.ResponseWriter, r *http.Request, usr store.User) { + sess, err := s.sessions.Create(usr.ID, usr.Username) + if err != nil { + writeError(w, http.StatusInternalServerError, "%v", err) + return + } + setSessionCookie(w, r, *sess) + writeJSON(w, http.StatusOK, meResponse{ID: usr.ID, Username: usr.Username, Authenticated: true}) +} + +func (s *Server) handleLogout(w http.ResponseWriter, r *http.Request) { + if c, err := r.Cookie(session.CookieName); err == nil { + s.sessions.Delete(c.Value) + } + clearSessionCookie(w, r) + w.WriteHeader(http.StatusNoContent) +} + +// meResponse is also what /api/setup and /api/login return on success. +type meResponse struct { + ID string `json:"id,omitempty"` + Username string `json:"username,omitempty"` + Authenticated bool `json:"authenticated"` + NeedsSetup bool `json:"needsSetup,omitempty"` +} + +// handleMe reports the caller's identity, or that setup is still needed. It +// is public so the UI can decide, on load, whether to show the setup screen, +// a login form, or the app itself. +func (s *Server) handleMe(w http.ResponseWriter, r *http.Request) { + if s.NeedsSetup() { + writeJSON(w, http.StatusOK, meResponse{NeedsSetup: true}) + return + } + if c, err := r.Cookie(session.CookieName); err == nil { + if sess, ok := s.sessions.Get(c.Value); ok { + writeJSON(w, http.StatusOK, meResponse{ID: sess.UserID, Username: sess.Username, Authenticated: true}) + return + } + } + if got := bearerToken(r); got != "" { + if tok, err := s.tokens.Authenticate(got); err == nil { + if usr, err := s.users.Get(tok.UserID); err == nil { + writeJSON(w, http.StatusOK, meResponse{ID: usr.ID, Username: usr.Username, Authenticated: true}) + return + } + } + } + writeJSON(w, http.StatusOK, meResponse{}) +} diff --git a/internal/api/handlers.go b/internal/api/handlers.go index cd273a2..ca043ec 100644 --- a/internal/api/handlers.go +++ b/internal/api/handlers.go @@ -25,11 +25,10 @@ func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) { defer cancel() body := map[string]any{ - "ok": true, - "packageDir": s.cfg.PackageDir, - "dataDir": s.cfg.DataDir, - "knownHosts": s.hosts.Path(), - "authRequired": s.cfg.Token != "", + "ok": true, + "packageDir": s.cfg.PackageDir, + "dataDir": s.cfg.DataDir, + "knownHosts": s.hosts.Path(), } // Health is also what connects to the selected source on a fresh start, so diff --git a/internal/api/server.go b/internal/api/server.go index 6e620a8..a2cbc70 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -2,7 +2,6 @@ package api import ( - "crypto/subtle" "encoding/json" "fmt" "io/fs" @@ -15,6 +14,7 @@ import ( "time" "github.com/arescom/dockmv/internal/job" + "github.com/arescom/dockmv/internal/session" "github.com/arescom/dockmv/internal/sshx" "github.com/arescom/dockmv/internal/store" ) @@ -23,9 +23,7 @@ import ( type Config struct { // Addr is the listen address, e.g. 127.0.0.1:8080. Addr string - // Token, when set, must be presented on every API request. - Token string - // DataDir holds connections and the known-hosts file. + // DataDir holds accounts, connections and the known-hosts file. DataDir string // PackageDir is where migration packages are written. PackageDir string @@ -42,13 +40,17 @@ type Config struct { // Server ties the source daemon, connection store and job manager to HTTP. type Server struct { - cfg Config - log *slog.Logger - sources *store.Sources - conns *store.Connections - hosts *sshx.KnownHosts - jobs *job.Manager - mux *http.ServeMux + cfg Config + log *slog.Logger + sources *store.Sources + conns *store.Connections + hosts *sshx.KnownHosts + jobs *job.Manager + users *store.Users + tokens *store.Tokens + sessions *session.Manager + logins *session.Limiter + mux *http.ServeMux // srcMu guards cur, the connection to the selected source. It is opened on // first use and replaced when the operator picks another source. @@ -92,14 +94,33 @@ func New(cfg Config) (*Server, error) { } } + users, err := store.NewUsers(filepath.Join(cfg.DataDir, "users.json")) + if err != nil { + return nil, err + } + tokens, err := store.NewTokens(filepath.Join(cfg.DataDir, "tokens.json")) + if err != nil { + return nil, err + } + s := &Server{ cfg: cfg, log: cfg.Logger, sources: sources, - conns: conns, hosts: hosts, jobs: job.NewManager(), mux: http.NewServeMux(), + conns: conns, hosts: hosts, jobs: job.NewManager(), + users: users, tokens: tokens, + sessions: session.NewManager(), logins: session.NewLimiter(), + mux: http.NewServeMux(), } s.routes() return s, nil } +// NeedsSetup reports whether no account exists yet. main.go refuses to bind +// non-loopback while this is true, and GET /api/me uses it to decide whether +// the UI should show the first-run setup screen instead of a login form. +func (s *Server) NeedsSetup() bool { + return s.users.Count() == 0 +} + // Close releases the connection to the current source. func (s *Server) Close() error { s.invalidateSource("") @@ -114,6 +135,19 @@ func (s *Server) Handler() http.Handler { func (s *Server) routes() { m := s.mux + m.HandleFunc("POST /api/setup", s.handleSetup) + m.HandleFunc("POST /api/login", s.handleLogin) + m.HandleFunc("POST /api/logout", s.handleLogout) + m.HandleFunc("GET /api/me", s.handleMe) + + m.HandleFunc("GET /api/users", s.handleListUsers) + m.HandleFunc("POST /api/users", s.handleCreateUser) + m.HandleFunc("DELETE /api/users/{id}", s.handleDeleteUser) + + m.HandleFunc("GET /api/tokens", s.handleListTokens) + m.HandleFunc("POST /api/tokens", s.handleCreateToken) + m.HandleFunc("DELETE /api/tokens/{id}", s.handleRevokeToken) + m.HandleFunc("GET /api/health", s.handleHealth) m.HandleFunc("GET /api/source", s.handleSource) m.HandleFunc("GET /api/source/sizes", s.handleSourceSizes) @@ -152,41 +186,6 @@ func (s *Server) routes() { } } -// auth enforces the shared token on the API. The token may also be passed as a -// query parameter, because EventSource cannot set headers and neither can a -// download link. -// -// Static assets are deliberately served without it. A browser opening -// /?token=… does not carry the query string over to /assets/app.js, so gating -// the shell would leave the UI unable to boot. Nothing sensitive lives in the -// bundle; every piece of data is behind /api. -func (s *Server) auth(next http.Handler) http.Handler { - if s.cfg.Token == "" { - return next - } - want := []byte(s.cfg.Token) - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if !strings.HasPrefix(r.URL.Path, "/api/") { - next.ServeHTTP(w, r) - return - } - got := r.Header.Get("X-Auth-Token") - if got == "" { - if h := r.Header.Get("Authorization"); strings.HasPrefix(h, "Bearer ") { - got = strings.TrimPrefix(h, "Bearer ") - } - } - if got == "" { - got = r.URL.Query().Get("token") - } - if subtle.ConstantTimeCompare([]byte(got), want) != 1 { - writeError(w, http.StatusUnauthorized, "invalid or missing token") - return - } - next.ServeHTTP(w, r) - }) -} - func (s *Server) logging(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() diff --git a/internal/api/tokens.go b/internal/api/tokens.go new file mode 100644 index 0000000..6bcec49 --- /dev/null +++ b/internal/api/tokens.go @@ -0,0 +1,50 @@ +package api + +import ( + "net/http" + + "github.com/arescom/dockmv/internal/store" +) + +// handleListTokens lists only the caller's own tokens; there is no way to see +// another account's. +func (s *Server) handleListTokens(w http.ResponseWriter, r *http.Request) { + writeJSON(w, http.StatusOK, s.tokens.List(identityFrom(r).UserID)) +} + +type createTokenRequest struct { + Name string `json:"name"` +} + +// createTokenResponse embeds the stored token plus the plaintext secret, +// which is only ever shown this once. +type createTokenResponse struct { + store.APIToken + Token string `json:"token"` +} + +func (s *Server) handleCreateToken(w http.ResponseWriter, r *http.Request) { + var req createTokenRequest + if err := decode(r, &req); err != nil { + writeError(w, http.StatusBadRequest, "%v", err) + return + } + if req.Name == "" { + writeError(w, http.StatusBadRequest, "name is required") + return + } + tok, plain, err := s.tokens.Create(identityFrom(r).UserID, req.Name) + if err != nil { + writeError(w, http.StatusInternalServerError, "%v", err) + return + } + writeJSON(w, http.StatusCreated, createTokenResponse{APIToken: tok, Token: plain}) +} + +func (s *Server) handleRevokeToken(w http.ResponseWriter, r *http.Request) { + if err := s.tokens.Revoke(identityFrom(r).UserID, r.PathValue("id")); err != nil { + writeError(w, http.StatusNotFound, "%v", err) + return + } + w.WriteHeader(http.StatusNoContent) +} diff --git a/internal/api/users.go b/internal/api/users.go new file mode 100644 index 0000000..1bef2fa --- /dev/null +++ b/internal/api/users.go @@ -0,0 +1,49 @@ +package api + +import ( + "net/http" + + "github.com/arescom/dockmv/internal/store" +) + +func (s *Server) handleListUsers(w http.ResponseWriter, r *http.Request) { + writeJSON(w, http.StatusOK, s.users.List()) +} + +type createUserRequest struct { + Username string `json:"username"` + Password string `json:"password"` +} + +// handleCreateUser adds another account. v1 has no roles: any authenticated +// account can create or remove any other. +func (s *Server) handleCreateUser(w http.ResponseWriter, r *http.Request) { + var req createUserRequest + if err := decode(r, &req); err != nil { + writeError(w, http.StatusBadRequest, "%v", err) + return + } + usr, err := s.users.Create(req.Username, req.Password) + if err != nil { + writeError(w, http.StatusBadRequest, "%v", err) + return + } + writeJSON(w, http.StatusCreated, usr) +} + +// handleDeleteUser removes an account, along with its sessions and personal +// API tokens so nothing left behind still authenticates as it. +func (s *Server) handleDeleteUser(w http.ResponseWriter, r *http.Request) { + id := r.PathValue("id") + if err := s.users.Delete(id); err != nil { + status := http.StatusNotFound + if err == store.ErrLastAccount { + status = http.StatusBadRequest + } + writeError(w, status, "%v", err) + return + } + s.sessions.DeleteAllForUser(id) + _ = s.tokens.RevokeAllForUser(id) + w.WriteHeader(http.StatusNoContent) +} diff --git a/internal/job/job.go b/internal/job/job.go index 136aee9..ad00fad 100644 --- a/internal/job/job.go +++ b/internal/job/job.go @@ -39,6 +39,7 @@ const ( KindSSH Kind = "ssh" KindPackage Kind = "package" KindRestore Kind = "restore" + KindBackup Kind = "backup" ) // Level classifies a log line. diff --git a/internal/session/limiter.go b/internal/session/limiter.go new file mode 100644 index 0000000..dfd7efd --- /dev/null +++ b/internal/session/limiter.go @@ -0,0 +1,76 @@ +package session + +import ( + "sync" + "time" +) + +// limiterBase and limiterCap bound the exponential backoff: 1s, 2s, 4s, ... +// capped at 30s. +const ( + limiterBase = 1 * time.Second + limiterCap = 30 * time.Second + // maxShift keeps 1< maxShift { + shift = maxShift + } + backoff := limiterBase * time.Duration(1< limiterCap { + backoff = limiterCap + } + st.blockedUntil = time.Now().Add(backoff) + return backoff +} + +// Reset clears key's failure history after a successful login. +func (l *Limiter) Reset(key string) { + l.mu.Lock() + delete(l.state, key) + l.mu.Unlock() +} diff --git a/internal/session/manager.go b/internal/session/manager.go new file mode 100644 index 0000000..8f450cd --- /dev/null +++ b/internal/session/manager.go @@ -0,0 +1,95 @@ +// Package session tracks logged-in browser sessions in memory. There is no +// persistence and no signing: state lives only for the life of the process, +// so a restart already invalidates every session, and signing would protect +// nothing that a restart doesn't already. +package session + +import ( + "crypto/rand" + "encoding/hex" + "sync" + "time" +) + +// CookieName is the session cookie set on a successful login. +const CookieName = "dockmv_session" + +// TTL is how long a session survives without activity. Every successful +// lookup slides the expiry forward by this much. +const TTL = 7 * 24 * time.Hour + +// Session is one logged-in browser tab's worth of state. +type Session struct { + ID string + UserID string + Username string + ExpiresAt time.Time +} + +// Manager holds every live session. +type Manager struct { + mu sync.Mutex + byID map[string]*Session +} + +// NewManager returns an empty session store. +func NewManager() *Manager { + return &Manager{byID: map[string]*Session{}} +} + +// Create starts a new session for a logged-in account. +func (m *Manager) Create(userID, username string) (*Session, error) { + id, err := newSessionID() + if err != nil { + return nil, err + } + s := &Session{ID: id, UserID: userID, Username: username, ExpiresAt: time.Now().Add(TTL)} + m.mu.Lock() + m.byID[id] = s + m.mu.Unlock() + return s, nil +} + +// Get returns the session for id, sliding its expiry forward. ok is false for +// an unknown or expired id. +func (m *Manager) Get(id string) (Session, bool) { + m.mu.Lock() + defer m.mu.Unlock() + s, ok := m.byID[id] + if !ok { + return Session{}, false + } + if time.Now().After(s.ExpiresAt) { + delete(m.byID, id) + return Session{}, false + } + s.ExpiresAt = time.Now().Add(TTL) + return *s, true +} + +// Delete ends one session, e.g. on logout. +func (m *Manager) Delete(id string) { + m.mu.Lock() + delete(m.byID, id) + m.mu.Unlock() +} + +// DeleteAllForUser ends every session belonging to an account, e.g. when the +// account itself is deleted. +func (m *Manager) DeleteAllForUser(userID string) { + m.mu.Lock() + for id, s := range m.byID { + if s.UserID == userID { + delete(m.byID, id) + } + } + m.mu.Unlock() +} + +func newSessionID() (string, error) { + b := make([]byte, 32) + if _, err := rand.Read(b); err != nil { + return "", err + } + return hex.EncodeToString(b), nil +} diff --git a/internal/store/tokens.go b/internal/store/tokens.go new file mode 100644 index 0000000..efab2ee --- /dev/null +++ b/internal/store/tokens.go @@ -0,0 +1,197 @@ +package store + +import ( + "crypto/rand" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + "sort" + "sync" + "time" +) + +// ErrTokenNotFound is returned for an unknown, revoked, or not-owned token. +var ErrTokenNotFound = errors.New("token not found") + +// APIToken is a personal access token, scoped to the account that created it. +// The plaintext is only ever returned once, by Create. +type APIToken struct { + ID string `json:"id"` + UserID string `json:"userId"` + Name string `json:"name"` + Hint string `json:"hint"` // last 4 characters, for telling tokens apart in a list + Hash string `json:"hash"` + CreatedAt time.Time `json:"createdAt"` + LastUsedAt *time.Time `json:"lastUsedAt,omitempty"` +} + +// Tokens is a JSON-backed collection of personal API tokens. +type Tokens struct { + path string + mu sync.RWMutex + // items holds every token by id. + items map[string]APIToken + // byHash maps a token's sha256 hex digest to its id, for authentication + // lookups without ever storing the plaintext. + byHash map[string]string +} + +// NewTokens loads (or creates) the tokens file at path. +func NewTokens(path string) (*Tokens, error) { + t := &Tokens{path: path, items: map[string]APIToken{}, byHash: map[string]string{}} + if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil { + return nil, fmt.Errorf("create data directory: %w", err) + } + b, err := os.ReadFile(path) + if errors.Is(err, os.ErrNotExist) { + return t, nil + } + if err != nil { + return nil, fmt.Errorf("read tokens: %w", err) + } + var list []APIToken + if err := json.Unmarshal(b, &list); err != nil { + return nil, fmt.Errorf("parse tokens file %s: %w", path, err) + } + for _, tok := range list { + t.items[tok.ID] = tok + t.byHash[tok.Hash] = tok.ID + } + return t, nil +} + +// List returns every token owned by userID, hash stripped, newest first. +func (t *Tokens) List(userID string) []APIToken { + t.mu.RLock() + defer t.mu.RUnlock() + out := make([]APIToken, 0, len(t.items)) + for _, tok := range t.items { + if tok.UserID == userID { + out = append(out, redactToken(tok)) + } + } + sort.Slice(out, func(i, j int) bool { return out[i].CreatedAt.After(out[j].CreatedAt) }) + return out +} + +// Create mints a new token for userID and returns it (hash stripped) plus the +// plaintext secret, which is never stored and never retrievable again. +func (t *Tokens) Create(userID, name string) (APIToken, string, error) { + plain, err := newTokenSecret() + if err != nil { + return APIToken{}, "", fmt.Errorf("generate token: %w", err) + } + hash := hashToken(plain) + + t.mu.Lock() + defer t.mu.Unlock() + + tok := APIToken{ + ID: newID(), + UserID: userID, + Name: name, + Hint: plain[len(plain)-4:], + Hash: hash, + CreatedAt: time.Now(), + } + t.items[tok.ID] = tok + t.byHash[hash] = tok.ID + + if err := t.flush(); err != nil { + return APIToken{}, "", err + } + return redactToken(tok), plain, nil +} + +// Authenticate looks up the token behind a plaintext secret and records it as +// used. It does not check ownership: any valid token authenticates as its +// owner, which the caller then treats as the request's identity. +func (t *Tokens) Authenticate(plain string) (APIToken, error) { + hash := hashToken(plain) + + t.mu.Lock() + defer t.mu.Unlock() + id, ok := t.byHash[hash] + if !ok { + return APIToken{}, ErrTokenNotFound + } + tok := t.items[id] + now := time.Now() + tok.LastUsedAt = &now + t.items[id] = tok + if err := t.flush(); err != nil { + return APIToken{}, err + } + return redactToken(tok), nil +} + +// Revoke deletes a token, refusing if it is not owned by userID. +func (t *Tokens) Revoke(userID, id string) error { + t.mu.Lock() + defer t.mu.Unlock() + tok, ok := t.items[id] + if !ok || tok.UserID != userID { + return ErrTokenNotFound + } + delete(t.items, id) + delete(t.byHash, tok.Hash) + return t.flush() +} + +// RevokeAllForUser deletes every token owned by userID, e.g. when the account +// itself is deleted. +func (t *Tokens) RevokeAllForUser(userID string) error { + t.mu.Lock() + defer t.mu.Unlock() + for id, tok := range t.items { + if tok.UserID == userID { + delete(t.items, id) + delete(t.byHash, tok.Hash) + } + } + return t.flush() +} + +// flush writes the file. The caller must hold the write lock. +func (t *Tokens) flush() error { + list := make([]APIToken, 0, len(t.items)) + for _, tok := range t.items { + list = append(list, tok) + } + sort.Slice(list, func(i, j int) bool { return list[i].ID < list[j].ID }) + b, err := json.MarshalIndent(list, "", " ") + if err != nil { + return err + } + tmp := t.path + ".tmp" + if err := os.WriteFile(tmp, b, 0o600); err != nil { + return fmt.Errorf("write tokens: %w", err) + } + if err := os.Rename(tmp, t.path); err != nil { + return fmt.Errorf("replace tokens file: %w", err) + } + return nil +} + +func redactToken(tok APIToken) APIToken { + tok.Hash = "" + return tok +} + +// newTokenSecret generates a token in the form dmv_<48 hex characters>. +func newTokenSecret() (string, error) { + b := make([]byte, 24) + if _, err := rand.Read(b); err != nil { + return "", err + } + return "dmv_" + hex.EncodeToString(b), nil +} + +func hashToken(plain string) string { + sum := sha256.Sum256([]byte(plain)) + return hex.EncodeToString(sum[:]) +} diff --git a/internal/store/tokens_test.go b/internal/store/tokens_test.go new file mode 100644 index 0000000..0d29747 --- /dev/null +++ b/internal/store/tokens_test.go @@ -0,0 +1,145 @@ +package store + +import ( + "path/filepath" + "strings" + "testing" +) + +func newTestTokens(t *testing.T) (*Tokens, string) { + t.Helper() + path := filepath.Join(t.TempDir(), "tokens.json") + tk, err := NewTokens(path) + if err != nil { + t.Fatalf("NewTokens: %v", err) + } + return tk, path +} + +func TestTokensCreateAndAuthenticate(t *testing.T) { + tk, _ := newTestTokens(t) + + tok, plain, err := tk.Create("user-1", "ci token") + if err != nil { + t.Fatalf("Create: %v", err) + } + if tok.Hash != "" { + t.Fatal("Create must return a redacted token") + } + if !strings.HasPrefix(plain, "dmv_") { + t.Fatalf("plaintext = %q, want dmv_ prefix", plain) + } + if tok.Hint != plain[len(plain)-4:] { + t.Fatalf("hint = %q, want last 4 chars of %q", tok.Hint, plain) + } + + got, err := tk.Authenticate(plain) + if err != nil { + t.Fatalf("Authenticate: %v", err) + } + if got.UserID != "user-1" { + t.Fatalf("UserID = %q, want user-1", got.UserID) + } + if got.Hash != "" { + t.Fatal("Authenticate must return a redacted token") + } + + if _, err := tk.Authenticate("dmv_deadbeef"); err != ErrTokenNotFound { + t.Fatalf("Authenticate with bogus token = %v, want ErrTokenNotFound", err) + } +} + +func TestTokensAuthenticateRecordsLastUsed(t *testing.T) { + tk, _ := newTestTokens(t) + + tok, plain, err := tk.Create("user-1", "ci token") + if err != nil { + t.Fatalf("Create: %v", err) + } + if tok.LastUsedAt != nil { + t.Fatal("a fresh token should have no last-used time") + } + got, err := tk.Authenticate(plain) + if err != nil { + t.Fatalf("Authenticate: %v", err) + } + if got.LastUsedAt == nil { + t.Fatal("Authenticate should record LastUsedAt") + } +} + +func TestTokensListScopedToOwner(t *testing.T) { + tk, _ := newTestTokens(t) + + if _, _, err := tk.Create("user-1", "a"); err != nil { + t.Fatalf("Create: %v", err) + } + if _, _, err := tk.Create("user-2", "b"); err != nil { + t.Fatalf("Create: %v", err) + } + + list := tk.List("user-1") + if len(list) != 1 || list[0].Name != "a" { + t.Fatalf("List(user-1) = %+v, want just token a", list) + } +} + +func TestTokensRevokeOwnershipChecked(t *testing.T) { + tk, _ := newTestTokens(t) + + tok, plain, err := tk.Create("user-1", "a") + if err != nil { + t.Fatalf("Create: %v", err) + } + + if err := tk.Revoke("user-2", tok.ID); err != ErrTokenNotFound { + t.Fatalf("Revoke by non-owner = %v, want ErrTokenNotFound", err) + } + if err := tk.Revoke("user-1", tok.ID); err != nil { + t.Fatalf("Revoke by owner: %v", err) + } + if _, err := tk.Authenticate(plain); err != ErrTokenNotFound { + t.Fatalf("Authenticate after revoke = %v, want ErrTokenNotFound", err) + } +} + +func TestTokensRevokeAllForUser(t *testing.T) { + tk, _ := newTestTokens(t) + + if _, _, err := tk.Create("user-1", "a"); err != nil { + t.Fatalf("Create: %v", err) + } + if _, _, err := tk.Create("user-1", "b"); err != nil { + t.Fatalf("Create: %v", err) + } + if _, _, err := tk.Create("user-2", "c"); err != nil { + t.Fatalf("Create: %v", err) + } + + if err := tk.RevokeAllForUser("user-1"); err != nil { + t.Fatalf("RevokeAllForUser: %v", err) + } + if len(tk.List("user-1")) != 0 { + t.Fatal("user-1 should have no tokens left") + } + if len(tk.List("user-2")) != 1 { + t.Fatal("user-2's token should be untouched") + } +} + +func TestTokensPersistAcrossReload(t *testing.T) { + tk, path := newTestTokens(t) + + _, plain, err := tk.Create("user-1", "a") + if err != nil { + t.Fatalf("Create: %v", err) + } + + reopened, err := NewTokens(path) + if err != nil { + t.Fatalf("NewTokens: %v", err) + } + if _, err := reopened.Authenticate(plain); err != nil { + t.Fatalf("Authenticate after reload: %v", err) + } +} diff --git a/internal/store/users.go b/internal/store/users.go new file mode 100644 index 0000000..2c33ff7 --- /dev/null +++ b/internal/store/users.go @@ -0,0 +1,231 @@ +package store + +import ( + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + "sort" + "strings" + "sync" + "time" + + "golang.org/x/crypto/bcrypt" +) + +// ErrUsernameTaken is returned by Create when the username is already in use. +var ErrUsernameTaken = errors.New("username already taken") + +// ErrInvalidCredentials is returned by Verify on an unknown username or a +// wrong password. The two cases are not distinguished, to avoid leaking which +// usernames exist. +var ErrInvalidCredentials = errors.New("invalid username or password") + +// ErrLastAccount is returned by Delete when it would remove the only account. +var ErrLastAccount = errors.New("cannot delete the last account") + +// ErrPasswordTooLong is returned when a password exceeds bcrypt's 72-byte +// limit. Passwords are rejected rather than silently truncated. +var ErrPasswordTooLong = errors.New("password must be 72 bytes or fewer") + +// User is one local account. +type User struct { + ID string `json:"id"` + Username string `json:"username"` + PasswordHash string `json:"passwordHash"` + CreatedAt time.Time `json:"createdAt"` + LastLoginAt *time.Time `json:"lastLoginAt,omitempty"` +} + +// Users is a JSON-backed collection of local accounts. +type Users struct { + path string + mu sync.RWMutex + // items holds every account by id. + items map[string]User + // byName maps a lowercased username to its account id, for case-insensitive + // uniqueness checks and login lookups. + byName map[string]string +} + +// dummyHash is compared against on a lookup-miss in Verify, so a login attempt +// against a username that doesn't exist takes the same time as one that does. +var dummyHash = mustHash("dockmv-dummy-password-for-timing-only") + +func mustHash(password string) []byte { + h, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + panic(err) + } + return h +} + +// NewUsers loads (or creates) the users file at path. +func NewUsers(path string) (*Users, error) { + u := &Users{path: path, items: map[string]User{}, byName: map[string]string{}} + if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil { + return nil, fmt.Errorf("create data directory: %w", err) + } + b, err := os.ReadFile(path) + if errors.Is(err, os.ErrNotExist) { + return u, nil + } + if err != nil { + return nil, fmt.Errorf("read users: %w", err) + } + var list []User + if err := json.Unmarshal(b, &list); err != nil { + return nil, fmt.Errorf("parse users file %s: %w", path, err) + } + for _, usr := range list { + u.items[usr.ID] = usr + u.byName[strings.ToLower(usr.Username)] = usr.ID + } + return u, nil +} + +// Count returns the number of accounts. +func (u *Users) Count() int { + u.mu.RLock() + defer u.mu.RUnlock() + return len(u.items) +} + +// List returns every account, password hashes stripped, username order. +func (u *Users) List() []User { + u.mu.RLock() + defer u.mu.RUnlock() + out := make([]User, 0, len(u.items)) + for _, usr := range u.items { + out = append(out, redactUser(usr)) + } + sort.Slice(out, func(i, j int) bool { return out[i].Username < out[j].Username }) + return out +} + +// Get returns one account, password hash stripped. +func (u *Users) Get(id string) (User, error) { + u.mu.RLock() + defer u.mu.RUnlock() + usr, ok := u.items[id] + if !ok { + return User{}, ErrNotFound + } + return redactUser(usr), nil +} + +// Create adds a new account. Usernames are unique case-insensitively. +func (u *Users) Create(username, password string) (User, error) { + username = strings.TrimSpace(username) + if username == "" { + return User{}, errors.New("username is required") + } + if password == "" { + return User{}, errors.New("password is required") + } + if len(password) > 72 { + return User{}, ErrPasswordTooLong + } + + u.mu.Lock() + defer u.mu.Unlock() + + key := strings.ToLower(username) + if _, ok := u.byName[key]; ok { + return User{}, ErrUsernameTaken + } + + hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + return User{}, fmt.Errorf("hash password: %w", err) + } + + usr := User{ID: newID(), Username: username, PasswordHash: string(hash), CreatedAt: time.Now()} + u.items[usr.ID] = usr + u.byName[key] = usr.ID + + if err := u.flush(); err != nil { + return User{}, err + } + return redactUser(usr), nil +} + +// Verify checks a username/password pair and returns the account on success. +// A lookup-miss still runs a bcrypt comparison against a dummy hash, so the +// two failure cases take the same time. +func (u *Users) Verify(username, password string) (User, error) { + u.mu.RLock() + id, ok := u.byName[strings.ToLower(strings.TrimSpace(username))] + var usr User + if ok { + usr = u.items[id] + } + u.mu.RUnlock() + + if !ok { + _ = bcrypt.CompareHashAndPassword(dummyHash, []byte(password)) + return User{}, ErrInvalidCredentials + } + if err := bcrypt.CompareHashAndPassword([]byte(usr.PasswordHash), []byte(password)); err != nil { + return User{}, ErrInvalidCredentials + } + return redactUser(usr), nil +} + +// TouchLastLogin records the current time as an account's last login. +func (u *Users) TouchLastLogin(id string) error { + u.mu.Lock() + defer u.mu.Unlock() + usr, ok := u.items[id] + if !ok { + return ErrNotFound + } + now := time.Now() + usr.LastLoginAt = &now + u.items[id] = usr + return u.flush() +} + +// Delete removes an account. The last remaining account cannot be deleted, so +// the instance never ends up with no way to log in. +func (u *Users) Delete(id string) error { + u.mu.Lock() + defer u.mu.Unlock() + usr, ok := u.items[id] + if !ok { + return ErrNotFound + } + if len(u.items) == 1 { + return ErrLastAccount + } + delete(u.items, id) + delete(u.byName, strings.ToLower(usr.Username)) + return u.flush() +} + +// flush writes the file. The caller must hold the write lock. +func (u *Users) flush() error { + list := make([]User, 0, len(u.items)) + for _, usr := range u.items { + list = append(list, usr) + } + sort.Slice(list, func(i, j int) bool { return list[i].ID < list[j].ID }) + b, err := json.MarshalIndent(list, "", " ") + if err != nil { + return err + } + tmp := u.path + ".tmp" + if err := os.WriteFile(tmp, b, 0o600); err != nil { + return fmt.Errorf("write users: %w", err) + } + if err := os.Rename(tmp, u.path); err != nil { + return fmt.Errorf("replace users file: %w", err) + } + return nil +} + +func redactUser(usr User) User { + usr.PasswordHash = "" + return usr +} diff --git a/internal/store/users_test.go b/internal/store/users_test.go new file mode 100644 index 0000000..1db1c57 --- /dev/null +++ b/internal/store/users_test.go @@ -0,0 +1,131 @@ +package store + +import ( + "path/filepath" + "strings" + "testing" +) + +func newTestUsers(t *testing.T) (*Users, string) { + t.Helper() + path := filepath.Join(t.TempDir(), "users.json") + u, err := NewUsers(path) + if err != nil { + t.Fatalf("NewUsers: %v", err) + } + return u, path +} + +func TestUsersCreateAndVerify(t *testing.T) { + u, _ := newTestUsers(t) + + usr, err := u.Create("Alice", "correct-password") + if err != nil { + t.Fatalf("Create: %v", err) + } + if usr.PasswordHash != "" { + t.Fatal("Create must return a redacted user") + } + if usr.ID == "" { + t.Fatal("an id should have been generated") + } + + if _, err := u.Verify("alice", "correct-password"); err != nil { + t.Fatalf("Verify with matching case-insensitive username: %v", err) + } + if _, err := u.Verify("Alice", "wrong-password"); err != ErrInvalidCredentials { + t.Fatalf("Verify with wrong password = %v, want ErrInvalidCredentials", err) + } + if _, err := u.Verify("nobody", "correct-password"); err != ErrInvalidCredentials { + t.Fatalf("Verify with unknown username = %v, want ErrInvalidCredentials", err) + } +} + +func TestUsersUsernameUniqueCaseInsensitive(t *testing.T) { + u, _ := newTestUsers(t) + + if _, err := u.Create("Bob", "password1"); err != nil { + t.Fatalf("Create: %v", err) + } + if _, err := u.Create("bob", "password2"); err != ErrUsernameTaken { + t.Fatalf("Create with case-different duplicate = %v, want ErrUsernameTaken", err) + } +} + +func TestUsersValidation(t *testing.T) { + u, _ := newTestUsers(t) + + if _, err := u.Create("", "password"); err == nil { + t.Fatal("empty username should be rejected") + } + if _, err := u.Create("carol", ""); err == nil { + t.Fatal("empty password should be rejected") + } + if _, err := u.Create("dave", strings.Repeat("x", 73)); err != ErrPasswordTooLong { + t.Fatalf("73-byte password = %v, want ErrPasswordTooLong", err) + } +} + +func TestUsersDeleteRefusesLastAccount(t *testing.T) { + u, _ := newTestUsers(t) + + a, err := u.Create("alice", "password1") + if err != nil { + t.Fatalf("Create: %v", err) + } + b, err := u.Create("bob", "password2") + if err != nil { + t.Fatalf("Create: %v", err) + } + + if err := u.Delete(a.ID); err != nil { + t.Fatalf("Delete first account: %v", err) + } + if err := u.Delete(b.ID); err != ErrLastAccount { + t.Fatalf("Delete last account = %v, want ErrLastAccount", err) + } + if u.Count() != 1 { + t.Fatalf("Count = %d, want 1", u.Count()) + } +} + +func TestUsersPersistAcrossReload(t *testing.T) { + u, path := newTestUsers(t) + + if _, err := u.Create("alice", "correct-password"); err != nil { + t.Fatalf("Create: %v", err) + } + + reopened, err := NewUsers(path) + if err != nil { + t.Fatalf("NewUsers: %v", err) + } + if reopened.Count() != 1 { + t.Fatalf("Count after reload = %d, want 1", reopened.Count()) + } + if _, err := reopened.Verify("alice", "correct-password"); err != nil { + t.Fatalf("Verify after reload: %v", err) + } +} + +func TestUsersTouchLastLogin(t *testing.T) { + u, _ := newTestUsers(t) + + usr, err := u.Create("alice", "correct-password") + if err != nil { + t.Fatalf("Create: %v", err) + } + if usr.LastLoginAt != nil { + t.Fatal("a fresh account should have no last login") + } + if err := u.TouchLastLogin(usr.ID); err != nil { + t.Fatalf("TouchLastLogin: %v", err) + } + got, err := u.Get(usr.ID) + if err != nil { + t.Fatalf("Get: %v", err) + } + if got.LastLoginAt == nil { + t.Fatal("LastLoginAt should be set after TouchLastLogin") + } +} diff --git a/internal/webui/dist/assets/index-4h69aJoO.js b/internal/webui/dist/assets/index-4h69aJoO.js new file mode 100644 index 0000000..69d4714 --- /dev/null +++ b/internal/webui/dist/assets/index-4h69aJoO.js @@ -0,0 +1,11 @@ +(function(){const g=document.createElement("link").relList;if(g&&g.supports&&g.supports("modulepreload"))return;for(const O of document.querySelectorAll('link[rel="modulepreload"]'))f(O);new MutationObserver(O=>{for(const D of O)if(D.type==="childList")for(const x of D.addedNodes)x.tagName==="LINK"&&x.rel==="modulepreload"&&f(x)}).observe(document,{childList:!0,subtree:!0});function j(O){const D={};return O.integrity&&(D.integrity=O.integrity),O.referrerPolicy&&(D.referrerPolicy=O.referrerPolicy),O.crossOrigin==="use-credentials"?D.credentials="include":O.crossOrigin==="anonymous"?D.credentials="omit":D.credentials="same-origin",D}function f(O){if(O.ep)return;O.ep=!0;const D=j(O);fetch(O.href,D)}})();var ys={exports:{}},Cn={};var zr;function oy(){if(zr)return Cn;zr=1;var o=Symbol.for("react.transitional.element"),g=Symbol.for("react.fragment");function j(f,O,D){var x=null;if(D!==void 0&&(x=""+D),O.key!==void 0&&(x=""+O.key),"key"in O){D={};for(var L in O)L!=="key"&&(D[L]=O[L])}else D=O;return O=D.ref,{$$typeof:o,type:f,key:x,ref:O!==void 0?O:null,props:D}}return Cn.Fragment=g,Cn.jsx=j,Cn.jsxs=j,Cn}var Nr;function dy(){return Nr||(Nr=1,ys.exports=oy()),ys.exports}var c=dy(),vs={exports:{}},F={};var Ar;function ry(){if(Ar)return F;Ar=1;var o=Symbol.for("react.transitional.element"),g=Symbol.for("react.portal"),j=Symbol.for("react.fragment"),f=Symbol.for("react.strict_mode"),O=Symbol.for("react.profiler"),D=Symbol.for("react.consumer"),x=Symbol.for("react.context"),L=Symbol.for("react.forward_ref"),U=Symbol.for("react.suspense"),p=Symbol.for("react.memo"),V=Symbol.for("react.lazy"),E=Symbol.for("react.activity"),C=Symbol.iterator;function I(h){return h===null||typeof h!="object"?null:(h=C&&h[C]||h["@@iterator"],typeof h=="function"?h:null)}var Q={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},ct=Object.assign,Dt={};function bt(h,A,B){this.props=h,this.context=A,this.refs=Dt,this.updater=B||Q}bt.prototype.isReactComponent={},bt.prototype.setState=function(h,A){if(typeof h!="object"&&typeof h!="function"&&h!=null)throw Error("takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,h,A,"setState")},bt.prototype.forceUpdate=function(h){this.updater.enqueueForceUpdate(this,h,"forceUpdate")};function _t(){}_t.prototype=bt.prototype;function ht(h,A,B){this.props=h,this.context=A,this.refs=Dt,this.updater=B||Q}var at=ht.prototype=new _t;at.constructor=ht,ct(at,bt.prototype),at.isPureReactComponent=!0;var St=Array.isArray;function At(){}var H={H:null,A:null,T:null,S:null},K=Object.prototype.hasOwnProperty;function Y(h,A,B){var G=B.ref;return{$$typeof:o,type:h,key:A,ref:G!==void 0?G:null,props:B}}function P(h,A){return Y(h.type,A,h.props)}function mt(h){return typeof h=="object"&&h!==null&&h.$$typeof===o}function st(h){var A={"=":"=0",":":"=2"};return"$"+h.replace(/[=:]/g,function(B){return A[B]})}var $t=/\/+/g;function il(h,A){return typeof h=="object"&&h!==null&&h.key!=null?st(""+h.key):A.toString(36)}function ll(h){switch(h.status){case"fulfilled":return h.value;case"rejected":throw h.reason;default:switch(typeof h.status=="string"?h.then(At,At):(h.status="pending",h.then(function(A){h.status==="pending"&&(h.status="fulfilled",h.value=A)},function(A){h.status==="pending"&&(h.status="rejected",h.reason=A)})),h.status){case"fulfilled":return h.value;case"rejected":throw h.reason}}throw h}function T(h,A,B,G,$){var tt=typeof h;(tt==="undefined"||tt==="boolean")&&(h=null);var yt=!1;if(h===null)yt=!0;else switch(tt){case"bigint":case"string":case"number":yt=!0;break;case"object":switch(h.$$typeof){case o:case g:yt=!0;break;case V:return yt=h._init,T(yt(h._payload),A,B,G,$)}}if(yt)return $=$(h),yt=G===""?"."+il(h,0):G,St($)?(B="",yt!=null&&(B=yt.replace($t,"$&/")+"/"),T($,A,B,"",function(se){return se})):$!=null&&(mt($)&&($=P($,B+($.key==null||h&&h.key===$.key?"":(""+$.key).replace($t,"$&/")+"/")+yt)),A.push($)),1;yt=0;var Kt=G===""?".":G+":";if(St(h))for(var Ut=0;Ut>>1,nt=T[J];if(0>>1;JO(B,M))GO($,B)?(T[J]=$,T[G]=M,J=G):(T[J]=B,T[A]=M,J=A);else if(GO($,M))T[J]=$,T[G]=M,J=G;else break t}}return R}function O(T,R){var M=T.sortIndex-R.sortIndex;return M!==0?M:T.id-R.id}if(o.unstable_now=void 0,typeof performance=="object"&&typeof performance.now=="function"){var D=performance;o.unstable_now=function(){return D.now()}}else{var x=Date,L=x.now();o.unstable_now=function(){return x.now()-L}}var U=[],p=[],V=1,E=null,C=3,I=!1,Q=!1,ct=!1,Dt=!1,bt=typeof setTimeout=="function"?setTimeout:null,_t=typeof clearTimeout=="function"?clearTimeout:null,ht=typeof setImmediate<"u"?setImmediate:null;function at(T){for(var R=j(p);R!==null;){if(R.callback===null)f(p);else if(R.startTime<=T)f(p),R.sortIndex=R.expirationTime,g(U,R);else break;R=j(p)}}function St(T){if(ct=!1,at(T),!Q)if(j(U)!==null)Q=!0,At||(At=!0,st());else{var R=j(p);R!==null&&ll(St,R.startTime-T)}}var At=!1,H=-1,K=5,Y=-1;function P(){return Dt?!0:!(o.unstable_now()-YT&&P());){var J=E.callback;if(typeof J=="function"){E.callback=null,C=E.priorityLevel;var nt=J(E.expirationTime<=T);if(T=o.unstable_now(),typeof nt=="function"){E.callback=nt,at(T),R=!0;break l}E===j(U)&&f(U),at(T)}else f(U);E=j(U)}if(E!==null)R=!0;else{var h=j(p);h!==null&&ll(St,h.startTime-T),R=!1}}break t}finally{E=null,C=M,I=!1}R=void 0}}finally{R?st():At=!1}}}var st;if(typeof ht=="function")st=function(){ht(mt)};else if(typeof MessageChannel<"u"){var $t=new MessageChannel,il=$t.port2;$t.port1.onmessage=mt,st=function(){il.postMessage(null)}}else st=function(){bt(mt,0)};function ll(T,R){H=bt(function(){T(o.unstable_now())},R)}o.unstable_IdlePriority=5,o.unstable_ImmediatePriority=1,o.unstable_LowPriority=4,o.unstable_NormalPriority=3,o.unstable_Profiling=null,o.unstable_UserBlockingPriority=2,o.unstable_cancelCallback=function(T){T.callback=null},o.unstable_forceFrameRate=function(T){0>T||125J?(T.sortIndex=M,g(p,T),j(U)===null&&T===j(p)&&(ct?(_t(H),H=-1):ct=!0,ll(St,M-J))):(T.sortIndex=nt,g(U,T),Q||I||(Q=!0,At||(At=!0,st()))),T},o.unstable_shouldYield=P,o.unstable_wrapCallback=function(T){var R=C;return function(){var M=C;C=R;try{return T.apply(this,arguments)}finally{C=M}}}})(ps)),ps}var Mr;function my(){return Mr||(Mr=1,bs.exports=hy()),bs.exports}var Ss={exports:{}},al={};var Dr;function yy(){if(Dr)return al;Dr=1;var o=js();function g(U){var p="https://react.dev/errors/"+U;if(1"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(o)}catch(g){console.error(g)}}return o(),Ss.exports=yy(),Ss.exports}var Ur;function gy(){if(Ur)return Un;Ur=1;var o=my(),g=js(),j=vy();function f(t){var l="https://react.dev/errors/"+t;if(1nt||(t.current=J[nt],J[nt]=null,nt--)}function B(t,l){nt++,J[nt]=t.current,t.current=l}var G=h(null),$=h(null),tt=h(null),yt=h(null);function Kt(t,l){switch(B(tt,l),B($,t),B(G,null),l.nodeType){case 9:case 11:t=(t=l.documentElement)&&(t=t.namespaceURI)?Jd(t):0;break;default:if(t=l.tagName,l=l.namespaceURI)l=Jd(l),t=$d(l,t);else switch(t){case"svg":t=1;break;case"math":t=2;break;default:t=0}}A(G),B(G,t)}function Ut(){A(G),A($),A(tt)}function se(t){t.memoizedState!==null&&B(yt,t);var l=G.current,e=$d(l,t.type);l!==e&&(B($,t),B(G,e))}function Pe(t){$.current===t&&(A(G),A($)),yt.current===t&&(A(yt),_n._currentValue=M)}var W,el;function zl(t){if(W===void 0)try{throw Error()}catch(e){var l=e.stack.trim().match(/\n( *(at )?)/);W=l&&l[1]||"",el=-1)":-1n||d[a]!==v[n]){var z=` +`+d[a].replace(" at new "," at ");return t.displayName&&z.includes("")&&(z=z.replace("",t.displayName)),z}while(1<=a&&0<=n);break}}}finally{He=!1,Error.prepareStackTrace=e}return(e=t?t.displayName||t.name:"")?zl(e):""}function Qr(t,l){switch(t.tag){case 26:case 27:case 5:return zl(t.type);case 16:return zl("Lazy");case 13:return t.child!==l&&l!==null?zl("Suspense Fallback"):zl("Suspense");case 19:return zl("SuspenseList");case 0:case 15:return Pu(t.type,!1);case 11:return Pu(t.type.render,!1);case 1:return Pu(t.type,!0);case 31:return zl("Activity");default:return""}}function Es(t){try{var l="",e=null;do l+=Qr(t,e),e=t,t=t.return;while(t);return l}catch(a){return` +Error generating stack: `+a.message+` +`+a.stack}}var ti=Object.prototype.hasOwnProperty,li=o.unstable_scheduleCallback,ei=o.unstable_cancelCallback,Zr=o.unstable_shouldYield,Lr=o.unstable_requestPaint,yl=o.unstable_now,Vr=o.unstable_getCurrentPriorityLevel,Ts=o.unstable_ImmediatePriority,zs=o.unstable_UserBlockingPriority,Rn=o.unstable_NormalPriority,Kr=o.unstable_LowPriority,Ns=o.unstable_IdlePriority,wr=o.log,kr=o.unstable_setDisableYieldValue,Ga=null,vl=null;function fe(t){if(typeof wr=="function"&&kr(t),vl&&typeof vl.setStrictMode=="function")try{vl.setStrictMode(Ga,t)}catch{}}var gl=Math.clz32?Math.clz32:Wr,Jr=Math.log,$r=Math.LN2;function Wr(t){return t>>>=0,t===0?32:31-(Jr(t)/$r|0)|0}var qn=256,Bn=262144,Yn=4194304;function Re(t){var l=t&42;if(l!==0)return l;switch(t&-t){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:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return t&261888;case 262144:case 524288:case 1048576:case 2097152:return t&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return t&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return t}}function Gn(t,l,e){var a=t.pendingLanes;if(a===0)return 0;var n=0,u=t.suspendedLanes,i=t.pingedLanes;t=t.warmLanes;var s=a&134217727;return s!==0?(a=s&~u,a!==0?n=Re(a):(i&=s,i!==0?n=Re(i):e||(e=s&~t,e!==0&&(n=Re(e))))):(s=a&~u,s!==0?n=Re(s):i!==0?n=Re(i):e||(e=a&~t,e!==0&&(n=Re(e)))),n===0?0:l!==0&&l!==n&&(l&u)===0&&(u=n&-n,e=l&-l,u>=e||u===32&&(e&4194048)!==0)?l:n}function Xa(t,l){return(t.pendingLanes&~(t.suspendedLanes&~t.pingedLanes)&l)===0}function Fr(t,l){switch(t){case 1:case 2:case 4:case 8:case 64:return l+250;case 16:case 32: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 l+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function As(){var t=Yn;return Yn<<=1,(Yn&62914560)===0&&(Yn=4194304),t}function ai(t){for(var l=[],e=0;31>e;e++)l.push(t);return l}function Qa(t,l){t.pendingLanes|=l,l!==268435456&&(t.suspendedLanes=0,t.pingedLanes=0,t.warmLanes=0)}function Ir(t,l,e,a,n,u){var i=t.pendingLanes;t.pendingLanes=e,t.suspendedLanes=0,t.pingedLanes=0,t.warmLanes=0,t.expiredLanes&=e,t.entangledLanes&=e,t.errorRecoveryDisabledLanes&=e,t.shellSuspendCounter=0;var s=t.entanglements,d=t.expirationTimes,v=t.hiddenUpdates;for(e=i&~e;0"u")return null;try{return t.activeElement||t.body}catch{return t.body}}var nh=/[\n"\\]/g;function Al(t){return t.replace(nh,function(l){return"\\"+l.charCodeAt(0).toString(16)+" "})}function fi(t,l,e,a,n,u,i,s){t.name="",i!=null&&typeof i!="function"&&typeof i!="symbol"&&typeof i!="boolean"?t.type=i:t.removeAttribute("type"),l!=null?i==="number"?(l===0&&t.value===""||t.value!=l)&&(t.value=""+Nl(l)):t.value!==""+Nl(l)&&(t.value=""+Nl(l)):i!=="submit"&&i!=="reset"||t.removeAttribute("value"),l!=null?oi(t,i,Nl(l)):e!=null?oi(t,i,Nl(e)):a!=null&&t.removeAttribute("value"),n==null&&u!=null&&(t.defaultChecked=!!u),n!=null&&(t.checked=n&&typeof n!="function"&&typeof n!="symbol"),s!=null&&typeof s!="function"&&typeof s!="symbol"&&typeof s!="boolean"?t.name=""+Nl(s):t.removeAttribute("name")}function Xs(t,l,e,a,n,u,i,s){if(u!=null&&typeof u!="function"&&typeof u!="symbol"&&typeof u!="boolean"&&(t.type=u),l!=null||e!=null){if(!(u!=="submit"&&u!=="reset"||l!=null)){si(t);return}e=e!=null?""+Nl(e):"",l=l!=null?""+Nl(l):e,s||l===t.value||(t.value=l),t.defaultValue=l}a=a??n,a=typeof a!="function"&&typeof a!="symbol"&&!!a,t.checked=s?t.checked:!!a,t.defaultChecked=!!a,i!=null&&typeof i!="function"&&typeof i!="symbol"&&typeof i!="boolean"&&(t.name=i),si(t)}function oi(t,l,e){l==="number"&&Zn(t.ownerDocument)===t||t.defaultValue===""+e||(t.defaultValue=""+e)}function ua(t,l,e,a){if(t=t.options,l){l={};for(var n=0;n"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),yi=!1;if(Kl)try{var Ka={};Object.defineProperty(Ka,"passive",{get:function(){yi=!0}}),window.addEventListener("test",Ka,Ka),window.removeEventListener("test",Ka,Ka)}catch{yi=!1}var de=null,vi=null,Vn=null;function ks(){if(Vn)return Vn;var t,l=vi,e=l.length,a,n="value"in de?de.value:de.textContent,u=n.length;for(t=0;t=Ja),Ps=" ",tf=!1;function lf(t,l){switch(t){case"keyup":return Ch.indexOf(l.keyCode)!==-1;case"keydown":return l.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function ef(t){return t=t.detail,typeof t=="object"&&"data"in t?t.data:null}var fa=!1;function Hh(t,l){switch(t){case"compositionend":return ef(l);case"keypress":return l.which!==32?null:(tf=!0,Ps);case"textInput":return t=l.data,t===Ps&&tf?null:t;default:return null}}function Rh(t,l){if(fa)return t==="compositionend"||!xi&&lf(t,l)?(t=ks(),Vn=vi=de=null,fa=!1,t):null;switch(t){case"paste":return null;case"keypress":if(!(l.ctrlKey||l.altKey||l.metaKey)||l.ctrlKey&&l.altKey){if(l.char&&1=l)return{node:e,offset:l-t};t=a}t:{for(;e;){if(e.nextSibling){e=e.nextSibling;break t}e=e.parentNode}e=void 0}e=df(e)}}function hf(t,l){return t&&l?t===l?!0:t&&t.nodeType===3?!1:l&&l.nodeType===3?hf(t,l.parentNode):"contains"in t?t.contains(l):t.compareDocumentPosition?!!(t.compareDocumentPosition(l)&16):!1:!1}function mf(t){t=t!=null&&t.ownerDocument!=null&&t.ownerDocument.defaultView!=null?t.ownerDocument.defaultView:window;for(var l=Zn(t.document);l instanceof t.HTMLIFrameElement;){try{var e=typeof l.contentWindow.location.href=="string"}catch{e=!1}if(e)t=l.contentWindow;else break;l=Zn(t.document)}return l}function Ti(t){var l=t&&t.nodeName&&t.nodeName.toLowerCase();return l&&(l==="input"&&(t.type==="text"||t.type==="search"||t.type==="tel"||t.type==="url"||t.type==="password")||l==="textarea"||t.contentEditable==="true")}var Lh=Kl&&"documentMode"in document&&11>=document.documentMode,oa=null,zi=null,Ia=null,Ni=!1;function yf(t,l,e){var a=e.window===e?e.document:e.nodeType===9?e:e.ownerDocument;Ni||oa==null||oa!==Zn(a)||(a=oa,"selectionStart"in a&&Ti(a)?a={start:a.selectionStart,end:a.selectionEnd}:(a=(a.ownerDocument&&a.ownerDocument.defaultView||window).getSelection(),a={anchorNode:a.anchorNode,anchorOffset:a.anchorOffset,focusNode:a.focusNode,focusOffset:a.focusOffset}),Ia&&Fa(Ia,a)||(Ia=a,a=Bu(zi,"onSelect"),0>=i,n-=i,Gl=1<<32-gl(l)+n|e<et?(ot=Z,Z=null):ot=Z.sibling;var gt=b(m,Z,y[et],N);if(gt===null){Z===null&&(Z=ot);break}t&&Z&>.alternate===null&&l(m,Z),r=u(gt,r,et),vt===null?w=gt:vt.sibling=gt,vt=gt,Z=ot}if(et===y.length)return e(m,Z),dt&&kl(m,et),w;if(Z===null){for(;etet?(ot=Z,Z=null):ot=Z.sibling;var Ue=b(m,Z,gt.value,N);if(Ue===null){Z===null&&(Z=ot);break}t&&Z&&Ue.alternate===null&&l(m,Z),r=u(Ue,r,et),vt===null?w=Ue:vt.sibling=Ue,vt=Ue,Z=ot}if(gt.done)return e(m,Z),dt&&kl(m,et),w;if(Z===null){for(;!gt.done;et++,gt=y.next())gt=_(m,gt.value,N),gt!==null&&(r=u(gt,r,et),vt===null?w=gt:vt.sibling=gt,vt=gt);return dt&&kl(m,et),w}for(Z=a(Z);!gt.done;et++,gt=y.next())gt=S(Z,m,et,gt.value,N),gt!==null&&(t&>.alternate!==null&&Z.delete(gt.key===null?et:gt.key),r=u(gt,r,et),vt===null?w=gt:vt.sibling=gt,vt=gt);return t&&Z.forEach(function(fy){return l(m,fy)}),dt&&kl(m,et),w}function zt(m,r,y,N){if(typeof y=="object"&&y!==null&&y.type===ct&&y.key===null&&(y=y.props.children),typeof y=="object"&&y!==null){switch(y.$$typeof){case I:t:{for(var w=y.key;r!==null;){if(r.key===w){if(w=y.type,w===ct){if(r.tag===7){e(m,r.sibling),N=n(r,y.props.children),N.return=m,m=N;break t}}else if(r.elementType===w||typeof w=="object"&&w!==null&&w.$$typeof===K&&we(w)===r.type){e(m,r.sibling),N=n(r,y.props),nn(N,y),N.return=m,m=N;break t}e(m,r);break}else l(m,r);r=r.sibling}y.type===ct?(N=Qe(y.props.children,m.mode,N,y.key),N.return=m,m=N):(N=tu(y.type,y.key,y.props,null,m.mode,N),nn(N,y),N.return=m,m=N)}return i(m);case Q:t:{for(w=y.key;r!==null;){if(r.key===w)if(r.tag===4&&r.stateNode.containerInfo===y.containerInfo&&r.stateNode.implementation===y.implementation){e(m,r.sibling),N=n(r,y.children||[]),N.return=m,m=N;break t}else{e(m,r);break}else l(m,r);r=r.sibling}N=Ui(y,m.mode,N),N.return=m,m=N}return i(m);case K:return y=we(y),zt(m,r,y,N)}if(ll(y))return X(m,r,y,N);if(st(y)){if(w=st(y),typeof w!="function")throw Error(f(150));return y=w.call(y),k(m,r,y,N)}if(typeof y.then=="function")return zt(m,r,cu(y),N);if(y.$$typeof===ht)return zt(m,r,au(m,y),N);su(m,y)}return typeof y=="string"&&y!==""||typeof y=="number"||typeof y=="bigint"?(y=""+y,r!==null&&r.tag===6?(e(m,r.sibling),N=n(r,y),N.return=m,m=N):(e(m,r),N=Ci(y,m.mode,N),N.return=m,m=N),i(m)):e(m,r)}return function(m,r,y,N){try{an=0;var w=zt(m,r,y,N);return xa=null,w}catch(Z){if(Z===Sa||Z===uu)throw Z;var vt=pl(29,Z,null,m.mode);return vt.lanes=N,vt.return=m,vt}}}var Je=Yf(!0),Gf=Yf(!1),ve=!1;function Ki(t){t.updateQueue={baseState:t.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function wi(t,l){t=t.updateQueue,l.updateQueue===t&&(l.updateQueue={baseState:t.baseState,firstBaseUpdate:t.firstBaseUpdate,lastBaseUpdate:t.lastBaseUpdate,shared:t.shared,callbacks:null})}function ge(t){return{lane:t,tag:0,payload:null,callback:null,next:null}}function be(t,l,e){var a=t.updateQueue;if(a===null)return null;if(a=a.shared,(pt&2)!==0){var n=a.pending;return n===null?l.next=l:(l.next=n.next,n.next=l),a.pending=l,l=Pn(t),jf(t,null,e),l}return In(t,a,l,e),Pn(t)}function un(t,l,e){if(l=l.updateQueue,l!==null&&(l=l.shared,(e&4194048)!==0)){var a=l.lanes;a&=t.pendingLanes,e|=a,l.lanes=e,Os(t,e)}}function ki(t,l){var e=t.updateQueue,a=t.alternate;if(a!==null&&(a=a.updateQueue,e===a)){var n=null,u=null;if(e=e.firstBaseUpdate,e!==null){do{var i={lane:e.lane,tag:e.tag,payload:e.payload,callback:null,next:null};u===null?n=u=i:u=u.next=i,e=e.next}while(e!==null);u===null?n=u=l:u=u.next=l}else n=u=l;e={baseState:a.baseState,firstBaseUpdate:n,lastBaseUpdate:u,shared:a.shared,callbacks:a.callbacks},t.updateQueue=e;return}t=e.lastBaseUpdate,t===null?e.firstBaseUpdate=l:t.next=l,e.lastBaseUpdate=l}var Ji=!1;function cn(){if(Ji){var t=pa;if(t!==null)throw t}}function sn(t,l,e,a){Ji=!1;var n=t.updateQueue;ve=!1;var u=n.firstBaseUpdate,i=n.lastBaseUpdate,s=n.shared.pending;if(s!==null){n.shared.pending=null;var d=s,v=d.next;d.next=null,i===null?u=v:i.next=v,i=d;var z=t.alternate;z!==null&&(z=z.updateQueue,s=z.lastBaseUpdate,s!==i&&(s===null?z.firstBaseUpdate=v:s.next=v,z.lastBaseUpdate=d))}if(u!==null){var _=n.baseState;i=0,z=v=d=null,s=u;do{var b=s.lane&-536870913,S=b!==s.lane;if(S?(ft&b)===b:(a&b)===b){b!==0&&b===ba&&(Ji=!0),z!==null&&(z=z.next={lane:0,tag:s.tag,payload:s.payload,callback:null,next:null});t:{var X=t,k=s;b=l;var zt=e;switch(k.tag){case 1:if(X=k.payload,typeof X=="function"){_=X.call(zt,_,b);break t}_=X;break t;case 3:X.flags=X.flags&-65537|128;case 0:if(X=k.payload,b=typeof X=="function"?X.call(zt,_,b):X,b==null)break t;_=E({},_,b);break t;case 2:ve=!0}}b=s.callback,b!==null&&(t.flags|=64,S&&(t.flags|=8192),S=n.callbacks,S===null?n.callbacks=[b]:S.push(b))}else S={lane:b,tag:s.tag,payload:s.payload,callback:s.callback,next:null},z===null?(v=z=S,d=_):z=z.next=S,i|=b;if(s=s.next,s===null){if(s=n.shared.pending,s===null)break;S=s,s=S.next,S.next=null,n.lastBaseUpdate=S,n.shared.pending=null}}while(!0);z===null&&(d=_),n.baseState=d,n.firstBaseUpdate=v,n.lastBaseUpdate=z,u===null&&(n.shared.lanes=0),Ee|=i,t.lanes=i,t.memoizedState=_}}function Xf(t,l){if(typeof t!="function")throw Error(f(191,t));t.call(l)}function Qf(t,l){var e=t.callbacks;if(e!==null)for(t.callbacks=null,t=0;tu?u:8;var i=T.T,s={};T.T=s,hc(t,!1,l,e);try{var d=n(),v=T.S;if(v!==null&&v(s,d),d!==null&&typeof d=="object"&&typeof d.then=="function"){var z=Ih(d,a);dn(t,l,z,Tl(t))}else dn(t,l,a,Tl(t))}catch(_){dn(t,l,{then:function(){},status:"rejected",reason:_},Tl())}finally{R.p=u,i!==null&&s.types!==null&&(i.types=s.types),T.T=i}}function nm(){}function dc(t,l,e,a){if(t.tag!==5)throw Error(f(476));var n=So(t).queue;po(t,n,l,M,e===null?nm:function(){return xo(t),e(a)})}function So(t){var l=t.memoizedState;if(l!==null)return l;l={memoizedState:M,baseState:M,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Fl,lastRenderedState:M},next:null};var e={};return l.next={memoizedState:e,baseState:e,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Fl,lastRenderedState:e},next:null},t.memoizedState=l,t=t.alternate,t!==null&&(t.memoizedState=l),l}function xo(t){var l=So(t);l.next===null&&(l=t.alternate.memoizedState),dn(t,l.next.queue,{},Tl())}function rc(){return It(_n)}function jo(){return Bt().memoizedState}function Eo(){return Bt().memoizedState}function um(t){for(var l=t.return;l!==null;){switch(l.tag){case 24:case 3:var e=Tl();t=ge(e);var a=be(l,t,e);a!==null&&(hl(a,l,e),un(a,l,e)),l={cache:Qi()},t.payload=l;return}l=l.return}}function im(t,l,e){var a=Tl();e={lane:a,revertLane:0,gesture:null,action:e,hasEagerState:!1,eagerState:null,next:null},bu(t)?zo(l,e):(e=Mi(t,l,e,a),e!==null&&(hl(e,t,a),No(e,l,a)))}function To(t,l,e){var a=Tl();dn(t,l,e,a)}function dn(t,l,e,a){var n={lane:a,revertLane:0,gesture:null,action:e,hasEagerState:!1,eagerState:null,next:null};if(bu(t))zo(l,n);else{var u=t.alternate;if(t.lanes===0&&(u===null||u.lanes===0)&&(u=l.lastRenderedReducer,u!==null))try{var i=l.lastRenderedState,s=u(i,e);if(n.hasEagerState=!0,n.eagerState=s,bl(s,i))return In(t,l,n,0),Nt===null&&Fn(),!1}catch{}if(e=Mi(t,l,n,a),e!==null)return hl(e,t,a),No(e,l,a),!0}return!1}function hc(t,l,e,a){if(a={lane:2,revertLane:Kc(),gesture:null,action:a,hasEagerState:!1,eagerState:null,next:null},bu(t)){if(l)throw Error(f(479))}else l=Mi(t,e,a,2),l!==null&&hl(l,t,2)}function bu(t){var l=t.alternate;return t===lt||l!==null&&l===lt}function zo(t,l){Ea=du=!0;var e=t.pending;e===null?l.next=l:(l.next=e.next,e.next=l),t.pending=l}function No(t,l,e){if((e&4194048)!==0){var a=l.lanes;a&=t.pendingLanes,e|=a,l.lanes=e,Os(t,e)}}var rn={readContext:It,use:mu,useCallback:Ht,useContext:Ht,useEffect:Ht,useImperativeHandle:Ht,useLayoutEffect:Ht,useInsertionEffect:Ht,useMemo:Ht,useReducer:Ht,useRef:Ht,useState:Ht,useDebugValue:Ht,useDeferredValue:Ht,useTransition:Ht,useSyncExternalStore:Ht,useId:Ht,useHostTransitionStatus:Ht,useFormState:Ht,useActionState:Ht,useOptimistic:Ht,useMemoCache:Ht,useCacheRefresh:Ht};rn.useEffectEvent=Ht;var Ao={readContext:It,use:mu,useCallback:function(t,l){return ul().memoizedState=[t,l===void 0?null:l],t},useContext:It,useEffect:fo,useImperativeHandle:function(t,l,e){e=e!=null?e.concat([t]):null,vu(4194308,4,mo.bind(null,l,t),e)},useLayoutEffect:function(t,l){return vu(4194308,4,t,l)},useInsertionEffect:function(t,l){vu(4,2,t,l)},useMemo:function(t,l){var e=ul();l=l===void 0?null:l;var a=t();if($e){fe(!0);try{t()}finally{fe(!1)}}return e.memoizedState=[a,l],a},useReducer:function(t,l,e){var a=ul();if(e!==void 0){var n=e(l);if($e){fe(!0);try{e(l)}finally{fe(!1)}}}else n=l;return a.memoizedState=a.baseState=n,t={pending:null,lanes:0,dispatch:null,lastRenderedReducer:t,lastRenderedState:n},a.queue=t,t=t.dispatch=im.bind(null,lt,t),[a.memoizedState,t]},useRef:function(t){var l=ul();return t={current:t},l.memoizedState=t},useState:function(t){t=ic(t);var l=t.queue,e=To.bind(null,lt,l);return l.dispatch=e,[t.memoizedState,e]},useDebugValue:fc,useDeferredValue:function(t,l){var e=ul();return oc(e,t,l)},useTransition:function(){var t=ic(!1);return t=po.bind(null,lt,t.queue,!0,!1),ul().memoizedState=t,[!1,t]},useSyncExternalStore:function(t,l,e){var a=lt,n=ul();if(dt){if(e===void 0)throw Error(f(407));e=e()}else{if(e=l(),Nt===null)throw Error(f(349));(ft&127)!==0||kf(a,l,e)}n.memoizedState=e;var u={value:e,getSnapshot:l};return n.queue=u,fo($f.bind(null,a,u,t),[t]),a.flags|=2048,za(9,{destroy:void 0},Jf.bind(null,a,u,e,l),null),e},useId:function(){var t=ul(),l=Nt.identifierPrefix;if(dt){var e=Xl,a=Gl;e=(a&~(1<<32-gl(a)-1)).toString(32)+e,l="_"+l+"R_"+e,e=ru++,0<\/script>",u=u.removeChild(u.firstChild);break;case"select":u=typeof a.is=="string"?i.createElement("select",{is:a.is}):i.createElement("select"),a.multiple?u.multiple=!0:a.size&&(u.size=a.size);break;default:u=typeof a.is=="string"?i.createElement(n,{is:a.is}):i.createElement(n)}}u[Wt]=l,u[cl]=a;t:for(i=l.child;i!==null;){if(i.tag===5||i.tag===6)u.appendChild(i.stateNode);else if(i.tag!==4&&i.tag!==27&&i.child!==null){i.child.return=i,i=i.child;continue}if(i===l)break t;for(;i.sibling===null;){if(i.return===null||i.return===l)break t;i=i.return}i.sibling.return=i.return,i=i.sibling}l.stateNode=u;t:switch(tl(u,n,a),n){case"button":case"input":case"select":case"textarea":a=!!a.autoFocus;break t;case"img":a=!0;break t;default:a=!1}a&&Pl(l)}}return Mt(l),Ac(l,l.type,t===null?null:t.memoizedProps,l.pendingProps,e),null;case 6:if(t&&l.stateNode!=null)t.memoizedProps!==a&&Pl(l);else{if(typeof a!="string"&&l.stateNode===null)throw Error(f(166));if(t=tt.current,va(l)){if(t=l.stateNode,e=l.memoizedProps,a=null,n=Ft,n!==null)switch(n.tag){case 27:case 5:a=n.memoizedProps}t[Wt]=l,t=!!(t.nodeValue===e||a!==null&&a.suppressHydrationWarning===!0||wd(t.nodeValue,e)),t||me(l,!0)}else t=Yu(t).createTextNode(a),t[Wt]=l,l.stateNode=t}return Mt(l),null;case 31:if(e=l.memoizedState,t===null||t.memoizedState!==null){if(a=va(l),e!==null){if(t===null){if(!a)throw Error(f(318));if(t=l.memoizedState,t=t!==null?t.dehydrated:null,!t)throw Error(f(557));t[Wt]=l}else Ze(),(l.flags&128)===0&&(l.memoizedState=null),l.flags|=4;Mt(l),t=!1}else e=Bi(),t!==null&&t.memoizedState!==null&&(t.memoizedState.hydrationErrors=e),t=!0;if(!t)return l.flags&256?(xl(l),l):(xl(l),null);if((l.flags&128)!==0)throw Error(f(558))}return Mt(l),null;case 13:if(a=l.memoizedState,t===null||t.memoizedState!==null&&t.memoizedState.dehydrated!==null){if(n=va(l),a!==null&&a.dehydrated!==null){if(t===null){if(!n)throw Error(f(318));if(n=l.memoizedState,n=n!==null?n.dehydrated:null,!n)throw Error(f(317));n[Wt]=l}else Ze(),(l.flags&128)===0&&(l.memoizedState=null),l.flags|=4;Mt(l),n=!1}else n=Bi(),t!==null&&t.memoizedState!==null&&(t.memoizedState.hydrationErrors=n),n=!0;if(!n)return l.flags&256?(xl(l),l):(xl(l),null)}return xl(l),(l.flags&128)!==0?(l.lanes=e,l):(e=a!==null,t=t!==null&&t.memoizedState!==null,e&&(a=l.child,n=null,a.alternate!==null&&a.alternate.memoizedState!==null&&a.alternate.memoizedState.cachePool!==null&&(n=a.alternate.memoizedState.cachePool.pool),u=null,a.memoizedState!==null&&a.memoizedState.cachePool!==null&&(u=a.memoizedState.cachePool.pool),u!==n&&(a.flags|=2048)),e!==t&&e&&(l.child.flags|=8192),Eu(l,l.updateQueue),Mt(l),null);case 4:return Ut(),t===null&&$c(l.stateNode.containerInfo),Mt(l),null;case 10:return $l(l.type),Mt(l),null;case 19:if(A(qt),a=l.memoizedState,a===null)return Mt(l),null;if(n=(l.flags&128)!==0,u=a.rendering,u===null)if(n)mn(a,!1);else{if(Rt!==0||t!==null&&(t.flags&128)!==0)for(t=l.child;t!==null;){if(u=ou(t),u!==null){for(l.flags|=128,mn(a,!1),t=u.updateQueue,l.updateQueue=t,Eu(l,t),l.subtreeFlags=0,t=e,e=l.child;e!==null;)Ef(e,t),e=e.sibling;return B(qt,qt.current&1|2),dt&&kl(l,a.treeForkCount),l.child}t=t.sibling}a.tail!==null&&yl()>_u&&(l.flags|=128,n=!0,mn(a,!1),l.lanes=4194304)}else{if(!n)if(t=ou(u),t!==null){if(l.flags|=128,n=!0,t=t.updateQueue,l.updateQueue=t,Eu(l,t),mn(a,!0),a.tail===null&&a.tailMode==="hidden"&&!u.alternate&&!dt)return Mt(l),null}else 2*yl()-a.renderingStartTime>_u&&e!==536870912&&(l.flags|=128,n=!0,mn(a,!1),l.lanes=4194304);a.isBackwards?(u.sibling=l.child,l.child=u):(t=a.last,t!==null?t.sibling=u:l.child=u,a.last=u)}return a.tail!==null?(t=a.tail,a.rendering=t,a.tail=t.sibling,a.renderingStartTime=yl(),t.sibling=null,e=qt.current,B(qt,n?e&1|2:e&1),dt&&kl(l,a.treeForkCount),t):(Mt(l),null);case 22:case 23:return xl(l),Wi(),a=l.memoizedState!==null,t!==null?t.memoizedState!==null!==a&&(l.flags|=8192):a&&(l.flags|=8192),a?(e&536870912)!==0&&(l.flags&128)===0&&(Mt(l),l.subtreeFlags&6&&(l.flags|=8192)):Mt(l),e=l.updateQueue,e!==null&&Eu(l,e.retryQueue),e=null,t!==null&&t.memoizedState!==null&&t.memoizedState.cachePool!==null&&(e=t.memoizedState.cachePool.pool),a=null,l.memoizedState!==null&&l.memoizedState.cachePool!==null&&(a=l.memoizedState.cachePool.pool),a!==e&&(l.flags|=2048),t!==null&&A(Ke),null;case 24:return e=null,t!==null&&(e=t.memoizedState.cache),l.memoizedState.cache!==e&&(l.flags|=2048),$l(Yt),Mt(l),null;case 25:return null;case 30:return null}throw Error(f(156,l.tag))}function dm(t,l){switch(Ri(l),l.tag){case 1:return t=l.flags,t&65536?(l.flags=t&-65537|128,l):null;case 3:return $l(Yt),Ut(),t=l.flags,(t&65536)!==0&&(t&128)===0?(l.flags=t&-65537|128,l):null;case 26:case 27:case 5:return Pe(l),null;case 31:if(l.memoizedState!==null){if(xl(l),l.alternate===null)throw Error(f(340));Ze()}return t=l.flags,t&65536?(l.flags=t&-65537|128,l):null;case 13:if(xl(l),t=l.memoizedState,t!==null&&t.dehydrated!==null){if(l.alternate===null)throw Error(f(340));Ze()}return t=l.flags,t&65536?(l.flags=t&-65537|128,l):null;case 19:return A(qt),null;case 4:return Ut(),null;case 10:return $l(l.type),null;case 22:case 23:return xl(l),Wi(),t!==null&&A(Ke),t=l.flags,t&65536?(l.flags=t&-65537|128,l):null;case 24:return $l(Yt),null;case 25:return null;default:return null}}function Fo(t,l){switch(Ri(l),l.tag){case 3:$l(Yt),Ut();break;case 26:case 27:case 5:Pe(l);break;case 4:Ut();break;case 31:l.memoizedState!==null&&xl(l);break;case 13:xl(l);break;case 19:A(qt);break;case 10:$l(l.type);break;case 22:case 23:xl(l),Wi(),t!==null&&A(Ke);break;case 24:$l(Yt)}}function yn(t,l){try{var e=l.updateQueue,a=e!==null?e.lastEffect:null;if(a!==null){var n=a.next;e=n;do{if((e.tag&t)===t){a=void 0;var u=e.create,i=e.inst;a=u(),i.destroy=a}e=e.next}while(e!==n)}}catch(s){jt(l,l.return,s)}}function xe(t,l,e){try{var a=l.updateQueue,n=a!==null?a.lastEffect:null;if(n!==null){var u=n.next;a=u;do{if((a.tag&t)===t){var i=a.inst,s=i.destroy;if(s!==void 0){i.destroy=void 0,n=l;var d=e,v=s;try{v()}catch(z){jt(n,d,z)}}}a=a.next}while(a!==u)}}catch(z){jt(l,l.return,z)}}function Io(t){var l=t.updateQueue;if(l!==null){var e=t.stateNode;try{Qf(l,e)}catch(a){jt(t,t.return,a)}}}function Po(t,l,e){e.props=We(t.type,t.memoizedProps),e.state=t.memoizedState;try{e.componentWillUnmount()}catch(a){jt(t,l,a)}}function vn(t,l){try{var e=t.ref;if(e!==null){switch(t.tag){case 26:case 27:case 5:var a=t.stateNode;break;case 30:a=t.stateNode;break;default:a=t.stateNode}typeof e=="function"?t.refCleanup=e(a):e.current=a}}catch(n){jt(t,l,n)}}function Ql(t,l){var e=t.ref,a=t.refCleanup;if(e!==null)if(typeof a=="function")try{a()}catch(n){jt(t,l,n)}finally{t.refCleanup=null,t=t.alternate,t!=null&&(t.refCleanup=null)}else if(typeof e=="function")try{e(null)}catch(n){jt(t,l,n)}else e.current=null}function td(t){var l=t.type,e=t.memoizedProps,a=t.stateNode;try{t:switch(l){case"button":case"input":case"select":case"textarea":e.autoFocus&&a.focus();break t;case"img":e.src?a.src=e.src:e.srcSet&&(a.srcset=e.srcSet)}}catch(n){jt(t,t.return,n)}}function _c(t,l,e){try{var a=t.stateNode;Um(a,t.type,e,l),a[cl]=l}catch(n){jt(t,t.return,n)}}function ld(t){return t.tag===5||t.tag===3||t.tag===26||t.tag===27&&_e(t.type)||t.tag===4}function Oc(t){t:for(;;){for(;t.sibling===null;){if(t.return===null||ld(t.return))return null;t=t.return}for(t.sibling.return=t.return,t=t.sibling;t.tag!==5&&t.tag!==6&&t.tag!==18;){if(t.tag===27&&_e(t.type)||t.flags&2||t.child===null||t.tag===4)continue t;t.child.return=t,t=t.child}if(!(t.flags&2))return t.stateNode}}function Mc(t,l,e){var a=t.tag;if(a===5||a===6)t=t.stateNode,l?(e.nodeType===9?e.body:e.nodeName==="HTML"?e.ownerDocument.body:e).insertBefore(t,l):(l=e.nodeType===9?e.body:e.nodeName==="HTML"?e.ownerDocument.body:e,l.appendChild(t),e=e._reactRootContainer,e!=null||l.onclick!==null||(l.onclick=Vl));else if(a!==4&&(a===27&&_e(t.type)&&(e=t.stateNode,l=null),t=t.child,t!==null))for(Mc(t,l,e),t=t.sibling;t!==null;)Mc(t,l,e),t=t.sibling}function Tu(t,l,e){var a=t.tag;if(a===5||a===6)t=t.stateNode,l?e.insertBefore(t,l):e.appendChild(t);else if(a!==4&&(a===27&&_e(t.type)&&(e=t.stateNode),t=t.child,t!==null))for(Tu(t,l,e),t=t.sibling;t!==null;)Tu(t,l,e),t=t.sibling}function ed(t){var l=t.stateNode,e=t.memoizedProps;try{for(var a=t.type,n=l.attributes;n.length;)l.removeAttributeNode(n[0]);tl(l,a,e),l[Wt]=t,l[cl]=e}catch(u){jt(t,t.return,u)}}var te=!1,Qt=!1,Dc=!1,ad=typeof WeakSet=="function"?WeakSet:Set,kt=null;function rm(t,l){if(t=t.containerInfo,Ic=Ku,t=mf(t),Ti(t)){if("selectionStart"in t)var e={start:t.selectionStart,end:t.selectionEnd};else t:{e=(e=t.ownerDocument)&&e.defaultView||window;var a=e.getSelection&&e.getSelection();if(a&&a.rangeCount!==0){e=a.anchorNode;var n=a.anchorOffset,u=a.focusNode;a=a.focusOffset;try{e.nodeType,u.nodeType}catch{e=null;break t}var i=0,s=-1,d=-1,v=0,z=0,_=t,b=null;l:for(;;){for(var S;_!==e||n!==0&&_.nodeType!==3||(s=i+n),_!==u||a!==0&&_.nodeType!==3||(d=i+a),_.nodeType===3&&(i+=_.nodeValue.length),(S=_.firstChild)!==null;)b=_,_=S;for(;;){if(_===t)break l;if(b===e&&++v===n&&(s=i),b===u&&++z===a&&(d=i),(S=_.nextSibling)!==null)break;_=b,b=_.parentNode}_=S}e=s===-1||d===-1?null:{start:s,end:d}}else e=null}e=e||{start:0,end:0}}else e=null;for(Pc={focusedElem:t,selectionRange:e},Ku=!1,kt=l;kt!==null;)if(l=kt,t=l.child,(l.subtreeFlags&1028)!==0&&t!==null)t.return=l,kt=t;else for(;kt!==null;){switch(l=kt,u=l.alternate,t=l.flags,l.tag){case 0:if((t&4)!==0&&(t=l.updateQueue,t=t!==null?t.events:null,t!==null))for(e=0;e title"))),tl(u,a,e),u[Wt]=t,wt(u),a=u;break t;case"link":var i=fr("link","href",n).get(a+(e.href||""));if(i){for(var s=0;szt&&(i=zt,zt=k,k=i);var m=rf(s,k),r=rf(s,zt);if(m&&r&&(S.rangeCount!==1||S.anchorNode!==m.node||S.anchorOffset!==m.offset||S.focusNode!==r.node||S.focusOffset!==r.offset)){var y=_.createRange();y.setStart(m.node,m.offset),S.removeAllRanges(),k>zt?(S.addRange(y),S.extend(r.node,r.offset)):(y.setEnd(r.node,r.offset),S.addRange(y))}}}}for(_=[],S=s;S=S.parentNode;)S.nodeType===1&&_.push({element:S,left:S.scrollLeft,top:S.scrollTop});for(typeof s.focus=="function"&&s.focus(),s=0;s<_.length;s++){var N=_[s];N.element.scrollLeft=N.left,N.element.scrollTop=N.top}}Ku=!!Ic,Pc=Ic=null}finally{pt=n,R.p=a,T.T=e}}t.current=l,Vt=2}}function Md(){if(Vt===2){Vt=0;var t=ze,l=Ma,e=(l.flags&8772)!==0;if((l.subtreeFlags&8772)!==0||e){e=T.T,T.T=null;var a=R.p;R.p=2;var n=pt;pt|=4;try{nd(t,l.alternate,l)}finally{pt=n,R.p=a,T.T=e}}Vt=3}}function Dd(){if(Vt===4||Vt===3){Vt=0,Lr();var t=ze,l=Ma,e=ue,a=gd;(l.subtreeFlags&10256)!==0||(l.flags&10256)!==0?Vt=5:(Vt=0,Ma=ze=null,Cd(t,t.pendingLanes));var n=t.pendingLanes;if(n===0&&(Te=null),ui(e),l=l.stateNode,vl&&typeof vl.onCommitFiberRoot=="function")try{vl.onCommitFiberRoot(Ga,l,void 0,(l.current.flags&128)===128)}catch{}if(a!==null){l=T.T,n=R.p,R.p=2,T.T=null;try{for(var u=t.onRecoverableError,i=0;ie?32:e,T.T=null,e=Yc,Yc=null;var u=ze,i=ue;if(Vt=0,Ma=ze=null,ue=0,(pt&6)!==0)throw Error(f(331));var s=pt;if(pt|=4,md(u.current),dd(u,u.current,i,e),pt=s,jn(0,!1),vl&&typeof vl.onPostCommitFiberRoot=="function")try{vl.onPostCommitFiberRoot(Ga,u)}catch{}return!0}finally{R.p=n,T.T=a,Cd(t,l)}}function Hd(t,l,e){l=Ol(e,l),l=gc(t.stateNode,l,2),t=be(t,l,2),t!==null&&(Qa(t,2),Zl(t))}function jt(t,l,e){if(t.tag===3)Hd(t,t,e);else for(;l!==null;){if(l.tag===3){Hd(l,t,e);break}else if(l.tag===1){var a=l.stateNode;if(typeof l.type.getDerivedStateFromError=="function"||typeof a.componentDidCatch=="function"&&(Te===null||!Te.has(a))){t=Ol(e,t),e=Ro(2),a=be(l,e,2),a!==null&&(qo(e,a,l,t),Qa(a,2),Zl(a));break}}l=l.return}}function Zc(t,l,e){var a=t.pingCache;if(a===null){a=t.pingCache=new ym;var n=new Set;a.set(l,n)}else n=a.get(l),n===void 0&&(n=new Set,a.set(l,n));n.has(e)||(Hc=!0,n.add(e),t=Sm.bind(null,t,l,e),l.then(t,t))}function Sm(t,l,e){var a=t.pingCache;a!==null&&a.delete(l),t.pingedLanes|=t.suspendedLanes&e,t.warmLanes&=~e,Nt===t&&(ft&e)===e&&(Rt===4||Rt===3&&(ft&62914560)===ft&&300>yl()-Au?(pt&2)===0&&Da(t,0):Rc|=e,Oa===ft&&(Oa=0)),Zl(t)}function Rd(t,l){l===0&&(l=As()),t=Xe(t,l),t!==null&&(Qa(t,l),Zl(t))}function xm(t){var l=t.memoizedState,e=0;l!==null&&(e=l.retryLane),Rd(t,e)}function jm(t,l){var e=0;switch(t.tag){case 31:case 13:var a=t.stateNode,n=t.memoizedState;n!==null&&(e=n.retryLane);break;case 19:a=t.stateNode;break;case 22:a=t.stateNode._retryCache;break;default:throw Error(f(314))}a!==null&&a.delete(l),Rd(t,e)}function Em(t,l){return li(t,l)}var Hu=null,Ua=null,Lc=!1,Ru=!1,Vc=!1,Ae=0;function Zl(t){t!==Ua&&t.next===null&&(Ua===null?Hu=Ua=t:Ua=Ua.next=t),Ru=!0,Lc||(Lc=!0,zm())}function jn(t,l){if(!Vc&&Ru){Vc=!0;do for(var e=!1,a=Hu;a!==null;){if(t!==0){var n=a.pendingLanes;if(n===0)var u=0;else{var i=a.suspendedLanes,s=a.pingedLanes;u=(1<<31-gl(42|t)+1)-1,u&=n&~(i&~s),u=u&201326741?u&201326741|1:u?u|2:0}u!==0&&(e=!0,Gd(a,u))}else u=ft,u=Gn(a,a===Nt?u:0,a.cancelPendingCommit!==null||a.timeoutHandle!==-1),(u&3)===0||Xa(a,u)||(e=!0,Gd(a,u));a=a.next}while(e);Vc=!1}}function Tm(){qd()}function qd(){Ru=Lc=!1;var t=0;Ae!==0&&Rm()&&(t=Ae);for(var l=yl(),e=null,a=Hu;a!==null;){var n=a.next,u=Bd(a,l);u===0?(a.next=null,e===null?Hu=n:e.next=n,n===null&&(Ua=e)):(e=a,(t!==0||(u&3)!==0)&&(Ru=!0)),a=n}Vt!==0&&Vt!==5||jn(t),Ae!==0&&(Ae=0)}function Bd(t,l){for(var e=t.suspendedLanes,a=t.pingedLanes,n=t.expirationTimes,u=t.pendingLanes&-62914561;0s)break;var z=d.transferSize,_=d.initiatorType;z&&kd(_)&&(d=d.responseEnd,i+=z*(d"u"?null:document;function ur(t,l,e){var a=Ha;if(a&&typeof l=="string"&&l){var n=Al(l);n='link[rel="'+t+'"][href="'+n+'"]',typeof e=="string"&&(n+='[crossorigin="'+e+'"]'),nr.has(n)||(nr.add(n),t={rel:t,crossOrigin:e,href:l},a.querySelector(n)===null&&(l=a.createElement("link"),tl(l,"link",t),wt(l),a.head.appendChild(l)))}}function Vm(t){ie.D(t),ur("dns-prefetch",t,null)}function Km(t,l){ie.C(t,l),ur("preconnect",t,l)}function wm(t,l,e){ie.L(t,l,e);var a=Ha;if(a&&t&&l){var n='link[rel="preload"][as="'+Al(l)+'"]';l==="image"&&e&&e.imageSrcSet?(n+='[imagesrcset="'+Al(e.imageSrcSet)+'"]',typeof e.imageSizes=="string"&&(n+='[imagesizes="'+Al(e.imageSizes)+'"]')):n+='[href="'+Al(t)+'"]';var u=n;switch(l){case"style":u=Ra(t);break;case"script":u=qa(t)}Rl.has(u)||(t=E({rel:"preload",href:l==="image"&&e&&e.imageSrcSet?void 0:t,as:l},e),Rl.set(u,t),a.querySelector(n)!==null||l==="style"&&a.querySelector(Nn(u))||l==="script"&&a.querySelector(An(u))||(l=a.createElement("link"),tl(l,"link",t),wt(l),a.head.appendChild(l)))}}function km(t,l){ie.m(t,l);var e=Ha;if(e&&t){var a=l&&typeof l.as=="string"?l.as:"script",n='link[rel="modulepreload"][as="'+Al(a)+'"][href="'+Al(t)+'"]',u=n;switch(a){case"audioworklet":case"paintworklet":case"serviceworker":case"sharedworker":case"worker":case"script":u=qa(t)}if(!Rl.has(u)&&(t=E({rel:"modulepreload",href:t},l),Rl.set(u,t),e.querySelector(n)===null)){switch(a){case"audioworklet":case"paintworklet":case"serviceworker":case"sharedworker":case"worker":case"script":if(e.querySelector(An(u)))return}a=e.createElement("link"),tl(a,"link",t),wt(a),e.head.appendChild(a)}}}function Jm(t,l,e){ie.S(t,l,e);var a=Ha;if(a&&t){var n=aa(a).hoistableStyles,u=Ra(t);l=l||"default";var i=n.get(u);if(!i){var s={loading:0,preload:null};if(i=a.querySelector(Nn(u)))s.loading=5;else{t=E({rel:"stylesheet",href:t,"data-precedence":l},e),(e=Rl.get(u))&&is(t,e);var d=i=a.createElement("link");wt(d),tl(d,"link",t),d._p=new Promise(function(v,z){d.onload=v,d.onerror=z}),d.addEventListener("load",function(){s.loading|=1}),d.addEventListener("error",function(){s.loading|=2}),s.loading|=4,Xu(i,l,a)}i={type:"stylesheet",instance:i,count:1,state:s},n.set(u,i)}}}function $m(t,l){ie.X(t,l);var e=Ha;if(e&&t){var a=aa(e).hoistableScripts,n=qa(t),u=a.get(n);u||(u=e.querySelector(An(n)),u||(t=E({src:t,async:!0},l),(l=Rl.get(n))&&cs(t,l),u=e.createElement("script"),wt(u),tl(u,"link",t),e.head.appendChild(u)),u={type:"script",instance:u,count:1,state:null},a.set(n,u))}}function Wm(t,l){ie.M(t,l);var e=Ha;if(e&&t){var a=aa(e).hoistableScripts,n=qa(t),u=a.get(n);u||(u=e.querySelector(An(n)),u||(t=E({src:t,async:!0,type:"module"},l),(l=Rl.get(n))&&cs(t,l),u=e.createElement("script"),wt(u),tl(u,"link",t),e.head.appendChild(u)),u={type:"script",instance:u,count:1,state:null},a.set(n,u))}}function ir(t,l,e,a){var n=(n=tt.current)?Gu(n):null;if(!n)throw Error(f(446));switch(t){case"meta":case"title":return null;case"style":return typeof e.precedence=="string"&&typeof e.href=="string"?(l=Ra(e.href),e=aa(n).hoistableStyles,a=e.get(l),a||(a={type:"style",instance:null,count:0,state:null},e.set(l,a)),a):{type:"void",instance:null,count:0,state:null};case"link":if(e.rel==="stylesheet"&&typeof e.href=="string"&&typeof e.precedence=="string"){t=Ra(e.href);var u=aa(n).hoistableStyles,i=u.get(t);if(i||(n=n.ownerDocument||n,i={type:"stylesheet",instance:null,count:0,state:{loading:0,preload:null}},u.set(t,i),(u=n.querySelector(Nn(t)))&&!u._p&&(i.instance=u,i.state.loading=5),Rl.has(t)||(e={rel:"preload",as:"style",href:e.href,crossOrigin:e.crossOrigin,integrity:e.integrity,media:e.media,hrefLang:e.hrefLang,referrerPolicy:e.referrerPolicy},Rl.set(t,e),u||Fm(n,t,e,i.state))),l&&a===null)throw Error(f(528,""));return i}if(l&&a!==null)throw Error(f(529,""));return null;case"script":return l=e.async,e=e.src,typeof e=="string"&&l&&typeof l!="function"&&typeof l!="symbol"?(l=qa(e),e=aa(n).hoistableScripts,a=e.get(l),a||(a={type:"script",instance:null,count:0,state:null},e.set(l,a)),a):{type:"void",instance:null,count:0,state:null};default:throw Error(f(444,t))}}function Ra(t){return'href="'+Al(t)+'"'}function Nn(t){return'link[rel="stylesheet"]['+t+"]"}function cr(t){return E({},t,{"data-precedence":t.precedence,precedence:null})}function Fm(t,l,e,a){t.querySelector('link[rel="preload"][as="style"]['+l+"]")?a.loading=1:(l=t.createElement("link"),a.preload=l,l.addEventListener("load",function(){return a.loading|=1}),l.addEventListener("error",function(){return a.loading|=2}),tl(l,"link",e),wt(l),t.head.appendChild(l))}function qa(t){return'[src="'+Al(t)+'"]'}function An(t){return"script[async]"+t}function sr(t,l,e){if(l.count++,l.instance===null)switch(l.type){case"style":var a=t.querySelector('style[data-href~="'+Al(e.href)+'"]');if(a)return l.instance=a,wt(a),a;var n=E({},e,{"data-href":e.href,"data-precedence":e.precedence,href:null,precedence:null});return a=(t.ownerDocument||t).createElement("style"),wt(a),tl(a,"style",n),Xu(a,e.precedence,t),l.instance=a;case"stylesheet":n=Ra(e.href);var u=t.querySelector(Nn(n));if(u)return l.state.loading|=4,l.instance=u,wt(u),u;a=cr(e),(n=Rl.get(n))&&is(a,n),u=(t.ownerDocument||t).createElement("link"),wt(u);var i=u;return i._p=new Promise(function(s,d){i.onload=s,i.onerror=d}),tl(u,"link",a),l.state.loading|=4,Xu(u,e.precedence,t),l.instance=u;case"script":return u=qa(e.src),(n=t.querySelector(An(u)))?(l.instance=n,wt(n),n):(a=e,(n=Rl.get(u))&&(a=E({},e),cs(a,n)),t=t.ownerDocument||t,n=t.createElement("script"),wt(n),tl(n,"link",a),t.head.appendChild(n),l.instance=n);case"void":return null;default:throw Error(f(443,l.type))}else l.type==="stylesheet"&&(l.state.loading&4)===0&&(a=l.instance,l.state.loading|=4,Xu(a,e.precedence,t));return l.instance}function Xu(t,l,e){for(var a=e.querySelectorAll('link[rel="stylesheet"][data-precedence],style[data-precedence]'),n=a.length?a[a.length-1]:null,u=n,i=0;i title"):null)}function Im(t,l,e){if(e===1||l.itemProp!=null)return!1;switch(t){case"meta":case"title":return!0;case"style":if(typeof l.precedence!="string"||typeof l.href!="string"||l.href==="")break;return!0;case"link":if(typeof l.rel!="string"||typeof l.href!="string"||l.href===""||l.onLoad||l.onError)break;return l.rel==="stylesheet"?(t=l.disabled,typeof l.precedence=="string"&&t==null):!0;case"script":if(l.async&&typeof l.async!="function"&&typeof l.async!="symbol"&&!l.onLoad&&!l.onError&&l.src&&typeof l.src=="string")return!0}return!1}function dr(t){return!(t.type==="stylesheet"&&(t.state.loading&3)===0)}function Pm(t,l,e,a){if(e.type==="stylesheet"&&(typeof a.media!="string"||matchMedia(a.media).matches!==!1)&&(e.state.loading&4)===0){if(e.instance===null){var n=Ra(a.href),u=l.querySelector(Nn(n));if(u){l=u._p,l!==null&&typeof l=="object"&&typeof l.then=="function"&&(t.count++,t=Zu.bind(t),l.then(t,t)),e.state.loading|=4,e.instance=u,wt(u);return}u=l.ownerDocument||l,a=cr(a),(n=Rl.get(n))&&is(a,n),u=u.createElement("link"),wt(u);var i=u;i._p=new Promise(function(s,d){i.onload=s,i.onerror=d}),tl(u,"link",a),e.instance=u}t.stylesheets===null&&(t.stylesheets=new Map),t.stylesheets.set(e,l),(l=e.state.preload)&&(e.state.loading&3)===0&&(t.count++,e=Zu.bind(t),l.addEventListener("load",e),l.addEventListener("error",e))}}var ss=0;function ty(t,l){return t.stylesheets&&t.count===0&&Vu(t,t.stylesheets),0ss?50:800)+l);return t.unsuspend=e,function(){t.unsuspend=null,clearTimeout(a),clearTimeout(n)}}:null}function Zu(){if(this.count--,this.count===0&&(this.imgCount===0||!this.waitingForImages)){if(this.stylesheets)Vu(this,this.stylesheets);else if(this.unsuspend){var t=this.unsuspend;this.unsuspend=null,t()}}}var Lu=null;function Vu(t,l){t.stylesheets=null,t.unsuspend!==null&&(t.count++,Lu=new Map,l.forEach(ly,t),Lu=null,Zu.call(t))}function ly(t,l){if(!(l.state.loading&4)){var e=Lu.get(t);if(e)var a=e.get(null);else{e=new Map,Lu.set(t,e);for(var n=t.querySelectorAll("link[data-precedence],style[data-precedence]"),u=0;u"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(o)}catch(g){console.error(g)}}return o(),gs.exports=gy(),gs.exports}var py=by();class Ya extends Error{status;body;constructor(g,j,f={}){super(j),this.status=g,this.body=f}get needsTrust(){return this.body.needsTrust===!0}}const Sy=new Set(["/api/me","/api/login","/api/setup"]);let qr=null;function Rr(o){qr=o}async function Zt(o,g){const j={...g?.headers};g?.body&&(j["Content-Type"]="application/json");const f=await fetch(o,{...g,headers:j,credentials:"same-origin"});if(f.status===401&&!Sy.has(o)&&qr?.(),f.status===204)return;const O=await f.text();let D={};if(O)try{D=JSON.parse(O)}catch{if(!f.ok)throw new Ya(f.status,O.slice(0,400))}if(!f.ok){const x=typeof D.error=="string"?D.error:`request failed (${f.status})`;throw new Ya(f.status,x,D)}return D}const nl=(o,g)=>Zt(o,{method:"POST",body:g===void 0?void 0:JSON.stringify(g)}),rt={health:()=>Zt("/api/health"),source:()=>Zt("/api/source"),volumeSizes:()=>Zt("/api/source/sizes"),me:()=>Zt("/api/me"),setup:(o,g)=>nl("/api/setup",{username:o,password:g}),login:(o,g)=>nl("/api/login",{username:o,password:g}),logout:()=>Zt("/api/logout",{method:"POST"}),users:()=>Zt("/api/users"),createUser:(o,g)=>nl("/api/users",{username:o,password:g}),deleteUser:o=>Zt(`/api/users/${o}`,{method:"DELETE"}),tokens:()=>Zt("/api/tokens"),createToken:o=>nl("/api/tokens",{name:o}),revokeToken:o=>Zt(`/api/tokens/${o}`,{method:"DELETE"}),sources:()=>Zt("/api/sources"),saveSource:o=>nl("/api/sources",o),deleteSource:o=>Zt(`/api/sources/${o}`,{method:"DELETE"}),selectSource:o=>nl(`/api/sources/${o}/select`),probeSource:o=>nl(`/api/sources/${o}/probe`),trustSource:(o,g)=>nl(`/api/sources/${o}/trust`,{fingerprint:g}),connections:()=>Zt("/api/connections"),saveConnection:o=>nl("/api/connections",o),deleteConnection:o=>Zt(`/api/connections/${o}`,{method:"DELETE"}),probe:o=>nl(`/api/connections/${o}/probe`),trust:(o,g)=>nl(`/api/connections/${o}/trust`,{fingerprint:g}),testConnection:o=>nl(`/api/connections/${o}/test`),targetInventory:o=>Zt(`/api/connections/${o}/inventory`),preview:o=>nl("/api/plan/preview",o),migrateSSH:(o,g)=>nl("/api/migrate/ssh",{connectionId:o,plan:g}),buildPackage:(o,g)=>nl("/api/migrate/package",{plan:o,format:g}),jobs:()=>Zt("/api/jobs"),job:o=>Zt(`/api/jobs/${o}`),cancelJob:o=>nl(`/api/jobs/${o}/cancel`),deleteJob:o=>Zt(`/api/jobs/${o}`,{method:"DELETE"}),packages:()=>Zt("/api/packages"),deletePackage:o=>Zt(`/api/packages/${encodeURIComponent(o)}`,{method:"DELETE"}),downloadUrl:o=>`/api/packages/${encodeURIComponent(o)}/download`,jobEvents:o=>new EventSource(`/api/jobs/${o}/events`)};function ce(o){if(o==null||o<0)return"–";if(o===0)return"0 B";const g=["B","KiB","MiB","GiB","TiB","PiB"];let j=o,f=0;for(;j>=1024&&fg(D.target.checked)}),c.jsx("span",{children:j})]})}function Lt({label:o,children:g}){return c.jsxs("label",{className:"field",children:[c.jsx("span",{children:o}),g]})}function Iu({title:o,onClose:g,children:j,footer:f,wide:O}){return q.useEffect(()=>{const D=x=>{x.key==="Escape"&&g()};return window.addEventListener("keydown",D),()=>window.removeEventListener("keydown",D)},[g]),c.jsx("div",{className:"modal-backdrop",onMouseDown:D=>D.target===D.currentTarget&&g(),children:c.jsxs("div",{className:"modal",style:O?{width:"min(1000px, 100%)"}:void 0,children:[c.jsxs("header",{children:[o,c.jsx("span",{className:"spacer"}),c.jsx("button",{className:"btn ghost tiny",onClick:g,children:"close"})]}),c.jsx("div",{className:"content",children:j}),f&&c.jsx("footer",{children:f})]})})}function Jt({kind:o,children:g}){return c.jsx("div",{className:`notice ${o==="info"?"":o}`,children:g})}function xs({done:o,total:g,state:j}){const f=g>0?Math.min(100,o/g*100):j==="succeeded"?100:0,O=j==="succeeded"?"done":j==="failed"?"failed":"";return c.jsx("div",{className:`progress ${O}`,children:c.jsx("div",{style:{width:`${f}%`}})})}function Yr(o,g){return o.kind==="tmpfs"?0:o.sizeBytes>=0?o.sizeBytes:o.name&&g[o.name]!==void 0?g[o.name]:-1}function xy({source:o,sel:g,setSel:j,targetInv:f,loading:O,sizes:D}){const[x,L]=q.useState(""),[U,p]=q.useState(new Set),[V,E]=q.useState(!1),C=o?.inventory.containers??[],I=q.useMemo(()=>new Set((f?.containers??[]).map(H=>H.name)),[f]),Q=q.useMemo(()=>{const H=x.trim().toLowerCase();return C.filter(K=>V&&K.state!=="running"?!1:H?K.name.toLowerCase().includes(H)||K.image.toLowerCase().includes(H)||(K.composeProject??"").toLowerCase().includes(H)||(K.mounts??[]).some(Y=>Y.destination.toLowerCase().includes(H)||(Y.name??"").toLowerCase().includes(H)):!0)},[C,x,V]),ct=q.useMemo(()=>{const H=new Map;for(const K of Q){const Y=K.composeProject||"",P=H.get(Y);P?P.push(K):H.set(Y,[K])}return[...H.entries()].sort((K,Y)=>K[0]===""?1:Y[0]===""?-1:K[0].localeCompare(Y[0]))},[Q]);function Dt(H,K){j(Y=>({...Y,[H]:{...Y[H],...K}}))}function bt(H,K){j(Y=>{const P={...Y};for(const mt of H)P[mt]&&(P[mt]={...P[mt],include:K});return P})}function _t(H){j(K=>{const Y={...K};for(const P of C){const mt=Y[P.id];mt?.include&&(Y[P.id]=H(mt,P))}return Y})}function ht(H,K){_t((Y,P)=>{const mt={...Y.mounts};for(const st of P.mounts??[])st.kind!=="tmpfs"&&K.includes(st.kind)&&(mt[st.destination]={...mt[st.destination],action:H});return{...Y,mounts:mt}})}const at=Q.map(H=>H.id),St=Q.filter(H=>g[H.id]?.include).length,At=Object.values(g).some(H=>H.include);return c.jsxs(c.Fragment,{children:[c.jsxs("div",{className:"toolbar",children:[c.jsx("input",{className:"search",type:"text",placeholder:"filter by name, image, mount…",value:x,onChange:H=>L(H.target.value)}),c.jsx("button",{className:"btn tiny",onClick:()=>bt(at,!0),children:"select all"}),c.jsx("button",{className:"btn tiny",onClick:()=>bt(at,!1),children:"clear"}),c.jsx("button",{className:"btn tiny",onClick:()=>bt(Q.filter(H=>H.state==="running").map(H=>H.id),!0),children:"select running"}),c.jsx(ml,{checked:V,onChange:E,label:c.jsx("span",{className:"small muted",children:"running only"})}),c.jsx("span",{className:"spacer"}),c.jsxs("span",{className:"small faint nowrap",children:["apply to ",St?`${St} selected`:"selection",":"]}),c.jsx("button",{className:"btn tiny",disabled:!At,onClick:()=>ht("copy",["volume","anonymous","bind"]),children:"copy all data"}),c.jsx("button",{className:"btn tiny",disabled:!At,onClick:()=>ht("skip",["bind"]),children:"skip binds"}),c.jsx("button",{className:"btn tiny",disabled:!At,onClick:()=>ht("structure",["volume","anonymous","bind"]),children:"structure only"}),c.jsxs("select",{className:"btn tiny",style:{width:"auto"},disabled:!At,value:"",onChange:H=>{const K=H.target.value;if(K){if(K==="start"&&_t(Y=>({...Y,startAfter:!0})),K==="nostart"&&_t(Y=>({...Y,startAfter:!1})),K==="live"&&_t(Y=>({...Y,stopSourceDuringCopy:!1})),K==="quiesce"&&_t(Y=>({...Y,stopSourceDuringCopy:!0})),K==="keepsource"&&_t(Y=>({...Y,stopSourceAfter:!1})),K==="stopsource"&&_t(Y=>({...Y,stopSourceAfter:!0})),K.startsWith("img:")){const Y=K.slice(4);_t(P=>({...P,migrateImage:Y!=="skip",imageMode:Y}))}H.target.value=""}},children:[c.jsx("option",{value:"",children:"more…"}),c.jsx("option",{value:"start",children:"start after migration"}),c.jsx("option",{value:"nostart",children:"leave stopped on target"}),c.jsx("option",{value:"quiesce",children:"stop source while copying"}),c.jsx("option",{value:"live",children:"copy while running (hot)"}),c.jsx("option",{value:"stopsource",children:"stop source after migration"}),c.jsx("option",{value:"keepsource",children:"leave source running"}),c.jsx("option",{value:"img:auto",children:"image: auto"}),c.jsx("option",{value:"img:pull",children:"image: pull on target"}),c.jsx("option",{value:"img:stream",children:"image: transfer layers"}),c.jsx("option",{value:"img:skip",children:"image: already on target"})]})]}),O&&C.length===0&&c.jsx("div",{className:"empty",children:"reading the source daemon…"}),!O&&C.length===0&&c.jsx("div",{className:"empty",children:"no containers on this host"}),!O&&C.length>0&&Q.length===0&&c.jsx("div",{className:"empty",children:"nothing matches the filter"}),c.jsx("div",{className:"clist",children:ct.map(([H,K])=>c.jsxs("div",{children:[ct.length>1&&c.jsxs("div",{className:"group-head",children:[c.jsx(ml,{checked:K.every(Y=>g[Y.id]?.include),onChange:Y=>bt(K.map(P=>P.id),Y),label:H?`compose: ${H}`:"standalone"}),c.jsx("span",{className:"line"}),c.jsx("span",{children:K.length})]}),K.map(Y=>c.jsx(jy,{c:Y,s:g[Y.id],onChange:P=>Dt(Y.id,P),expanded:U.has(Y.id),toggleExpanded:()=>p(P=>{const mt=new Set(P);return mt.has(Y.id)?mt.delete(Y.id):mt.add(Y.id),mt}),conflicts:I.has(g[Y.id]?.nameOverride||Y.name),sizes:D},Y.id))]},H||"__none"))})]})}function jy({c:o,s:g,onChange:j,expanded:f,toggleExpanded:O,conflicts:D,sizes:x}){if(!g)return null;const U=(o.mounts??[]).filter(E=>E.kind!=="tmpfs"),p=U.filter(E=>(g.mounts[E.destination]?.action??"copy")==="copy"),V=p.reduce((E,C)=>{const I=Yr(C,x);return E+(I>0?I:0)},0);return c.jsxs(c.Fragment,{children:[c.jsxs("div",{className:`crow${g.include?" selected":""}`,children:[c.jsx(ml,{checked:g.include,onChange:E=>j({include:E}),label:""}),c.jsx("button",{className:"expander",onClick:O,title:"per-item options",children:f?"▾":"▸"}),c.jsxs("div",{style:{minWidth:0},children:[c.jsx("div",{className:"name truncate",title:o.name,children:o.name}),c.jsxs("div",{className:"sub row",style:{gap:6},children:[c.jsx(Hn,{state:o.state}),o.composeService&&c.jsxs("span",{className:"faint",children:["· ",o.composeService]}),D&&c.jsx("span",{className:"badge",style:{borderColor:"#5c4520",color:"#e0b556"},children:"on target"})]})]}),c.jsx("div",{className:"image truncate",title:o.image,children:o.image}),c.jsxs("div",{className:"tags",children:[U.map(E=>c.jsx("span",{className:`badge ${E.kind==="bind"?"bind":E.kind==="anonymous"?"anon":"vol"}`,title:`${E.kind} → ${E.destination}${E.readOnly?" (read-only)":""}`,style:{opacity:(g.mounts[E.destination]?.action??"copy")==="skip"?.35:1},children:E.kind==="bind"?(E.source??"").split("/").pop()||"/":E.kind==="anonymous"?"anon":E.name},E.destination)),(o.endpoints??[]).filter(E=>!["bridge","host","none"].includes(E.network)).map(E=>c.jsx("span",{className:"badge net",title:`network ${E.network}`,children:E.network},E.network)),(o.ports??[]).slice(0,3).map((E,C)=>c.jsxs("span",{className:"badge port",children:[E.hostPort,":",E.containerPort.split("/")[0]]},C)),(o.ports??[]).length>3&&c.jsxs("span",{className:"badge port",children:["+",(o.ports??[]).length-3]})]}),c.jsxs("div",{className:"small faint nowrap",style:{textAlign:"right"},children:[p.length>0?`${p.length} to copy`:"no data",V>0&&c.jsxs(c.Fragment,{children:[" · ",ce(V)]})]})]}),f&&c.jsx(Ey,{c:o,s:g,onChange:j,sizes:x})]})}function Ey({c:o,s:g,onChange:j,sizes:f}){const O=o.mounts??[];function D(x,L){j({mounts:{...g.mounts,[x]:{...g.mounts[x],...L}}})}return c.jsxs("div",{className:"detail",children:[(o.warnings??[]).map((x,L)=>c.jsx("div",{className:"notice warn",children:x},L)),c.jsxs("div",{className:"grid2",children:[c.jsx(Lt,{label:"name on target",children:c.jsx("input",{type:"text",placeholder:o.name,value:g.nameOverride??"",onChange:x=>j({nameOverride:x.target.value})})}),c.jsx(Lt,{label:"image",children:c.jsxs("select",{value:g.migrateImage?g.imageMode:"skip",onChange:x=>{const L=x.target.value;j({migrateImage:L!=="skip",imageMode:L})},children:[c.jsx("option",{value:"auto",children:"auto — reuse, pull, or transfer"}),c.jsx("option",{value:"pull",children:"pull on the target"}),c.jsx("option",{value:"stream",children:"transfer the layers"}),c.jsx("option",{value:"skip",children:"already on the target"})]})}),c.jsxs("div",{className:"stack",style:{gap:6},children:[c.jsx(ml,{checked:g.migrateNetworks,onChange:x=>j({migrateNetworks:x}),label:"recreate networks and reattach"}),c.jsx(ml,{checked:g.keepStaticIps,onChange:x=>j({keepStaticIps:x}),disabled:!g.migrateNetworks,label:"keep static IP addresses",title:"Only works when the target networks use the same subnets"}),c.jsx(ml,{checked:g.migratePorts,onChange:x=>j({migratePorts:x}),label:"publish the same host ports"})]}),c.jsxs("div",{className:"stack",style:{gap:6},children:[c.jsx(ml,{checked:g.startAfter,onChange:x=>j({startAfter:x}),label:"start on the target"}),c.jsx(ml,{checked:g.stopSourceDuringCopy,onChange:x=>j({stopSourceDuringCopy:x}),label:"stop the source while copying",title:"Recommended: databases and other writers produce inconsistent copies while running"}),c.jsx(ml,{checked:g.stopSourceAfter,onChange:x=>j({stopSourceAfter:x}),label:"leave the source stopped afterwards"})]})]}),O.length===0?c.jsx("div",{className:"small faint",children:"this container has no mounts"}):c.jsxs("table",{className:"mount-table",children:[c.jsx("thead",{children:c.jsxs("tr",{children:[c.jsx("th",{style:{width:74},children:"kind"}),c.jsx("th",{children:"in the container"}),c.jsx("th",{children:"on the source"}),c.jsx("th",{style:{width:130},children:"action"}),c.jsx("th",{children:"on the target"}),c.jsx("th",{style:{width:70,textAlign:"right"},children:"size"})]})}),c.jsx("tbody",{children:O.map(x=>{const L=g.mounts[x.destination]??{action:"copy"},U=x.kind==="tmpfs";return c.jsxs("tr",{children:[c.jsx("td",{children:c.jsx("span",{className:`badge ${x.kind==="bind"?"bind":x.kind==="anonymous"?"anon":x.kind==="tmpfs"?"tmpfs":"vol"}`,children:x.kind})}),c.jsxs("td",{className:"mono truncate",title:x.destination,children:[x.destination,x.readOnly&&c.jsx("span",{className:"faint",children:" :ro"})]}),c.jsx("td",{className:"mono truncate faint",title:x.source||x.name,children:x.kind==="bind"?x.source:x.kind==="anonymous"?"(generated)":x.name}),c.jsx("td",{children:c.jsxs("select",{value:L.action,disabled:U,onChange:p=>D(x.destination,{action:p.target.value}),children:[c.jsx("option",{value:"copy",children:"copy data"}),c.jsx("option",{value:"structure",children:"create empty"}),c.jsx("option",{value:"skip",children:"do not mount"})]})}),c.jsxs("td",{children:[x.kind==="bind"&&L.action!=="skip"&&c.jsx("input",{type:"text",placeholder:x.source,value:L.targetSource??"",onChange:p=>D(x.destination,{targetSource:p.target.value})}),x.kind==="volume"&&L.action!=="skip"&&c.jsx("input",{type:"text",placeholder:x.name,value:L.targetName??"",onChange:p=>D(x.destination,{targetName:p.target.value})}),x.kind==="anonymous"&&c.jsx("span",{className:"small faint",children:"a fresh volume is created"}),U&&c.jsx("span",{className:"small faint",children:"in memory, nothing to copy"})]}),c.jsx("td",{className:"small faint nowrap",style:{textAlign:"right"},children:U?"–":ce(Yr(x,f))})]},x.destination)})})]})]})}function Gr({value:o,set:g,where:j}){return c.jsxs(c.Fragment,{children:[c.jsxs("div",{className:"row",style:{gap:12},children:[c.jsx(Lt,{label:"host",children:c.jsx("input",{type:"text",value:o.host??"",onChange:f=>g("host",f.target.value)})}),c.jsx("div",{style:{width:90},children:c.jsx(Lt,{label:"port",children:c.jsx("input",{type:"number",value:o.port??22,onChange:f=>g("port",Number(f.target.value))})})})]}),c.jsxs("div",{className:"row",style:{gap:12},children:[c.jsx(Lt,{label:"user",children:c.jsx("input",{type:"text",value:o.user??"",onChange:f=>g("user",f.target.value)})}),c.jsx(Lt,{label:"authentication",children:c.jsxs("select",{value:o.auth??"password",onChange:f=>g("auth",f.target.value),children:[c.jsx("option",{value:"password",children:"password"}),c.jsx("option",{value:"key",children:"private key"}),c.jsx("option",{value:"agent",children:"ssh agent"})]})})]}),o.auth==="password"&&c.jsx(Lt,{label:"password",children:c.jsx("input",{type:"password",value:o.password??"",onChange:f=>g("password",f.target.value)})}),o.auth==="key"&&c.jsxs(c.Fragment,{children:[c.jsx(Lt,{label:"private key path on this machine (leave empty to paste the key below)",children:c.jsx("input",{type:"text",placeholder:"/root/.ssh/id_ed25519",value:o.privateKeyPath??"",onChange:f=>g("privateKeyPath",f.target.value)})}),c.jsx(Lt,{label:"or paste the private key",children:c.jsx("textarea",{rows:5,value:o.privateKey??"",onChange:f=>g("privateKey",f.target.value),placeholder:"-----BEGIN OPENSSH PRIVATE KEY-----"})}),c.jsx(Lt,{label:"passphrase (if the key is encrypted)",children:c.jsx("input",{type:"password",value:o.passphrase??"",onChange:f=>g("passphrase",f.target.value)})})]}),o.auth==="agent"&&c.jsxs("div",{className:"small muted",children:["Uses the agent at ",c.jsx("span",{className:"mono",children:"$SSH_AUTH_SOCK"})," of the process running dockmv."]}),c.jsx(ml,{checked:o.sudo??!1,onChange:f=>g("sudo",f),label:`run docker through sudo -n on the ${j}`,title:"Needed when the login user is not in the docker group. sudo must not ask for a password."}),c.jsx(Lt,{label:`docker command on the ${j} (optional)`,children:c.jsx("input",{type:"text",placeholder:"docker",value:o.dockerCmd??"",onChange:f=>g("dockerCmd",f.target.value)})}),c.jsx(ml,{checked:o.saveSecrets??!1,onChange:f=>g("saveSecrets",f),label:"remember the password / key on disk"}),o.saveSecrets?c.jsx(Jt,{kind:"warn",children:"Credentials are stored in plain text in dockmv's data directory, readable only by this user. Leave this off to keep them in memory for this session only."}):c.jsx("div",{className:"small faint",children:"Credentials stay in memory and are lost when dockmv restarts."})]})}function Xr({info:o,onClose:g,onTrust:j}){return c.jsx(Iu,{title:"SSH host key",onClose:g,footer:c.jsxs(c.Fragment,{children:[c.jsx("button",{className:"btn",onClick:g,children:"cancel"}),c.jsx("button",{className:"btn primary",onClick:j,children:o.changed?"replace the stored key and trust":"trust this host"})]}),children:c.jsxs("div",{className:"stack",children:[o.changed&&c.jsxs(Jt,{kind:"err",children:["The key presented by this host is ",c.jsx("b",{children:"different"})," from the one recorded earlier. This happens after a reinstall — but it is also what a machine-in-the-middle looks like. Only continue if you know why it changed."]}),o.trusted&&!o.changed&&c.jsx(Jt,{kind:"ok",children:"This host key is already trusted."}),c.jsxs("div",{className:"small muted",children:["Compare this with the output of ",c.jsxs("span",{className:"mono",children:["ssh-keyscan -t ",o.keyType," ",o.host]})," ","run on the host itself, or with ",c.jsx("span",{className:"mono",children:"ssh-keygen -lf /etc/ssh/ssh_host_*_key.pub"}),"."]}),c.jsxs("div",{className:"fingerprint",children:[o.keyType,c.jsx("br",{}),o.fingerprint]})]})})}function Ty({sources:o,selected:g,status:j,selectSource:f,reload:O,onError:D}){const[x,L]=q.useState(null),[U,p]=q.useState(null),[V,E]=q.useState(""),[C,I]=q.useState(""),Q=o.find(at=>at.id===g),ct=!Q||Q.kind==="local",Dt=Q?.kind==="ssh";async function bt(at,St,At){E(at);try{await At()}catch(H){H instanceof Ya&&H.needsTrust?(I(St),await _t(St)):D(H instanceof Error?H.message:String(H))}finally{E("")}}async function _t(at){try{p(await rt.probeSource(at))}catch(St){D(St instanceof Error?St.message:String(St))}}async function ht(){const at=C||g;U&&await bt("trust",at,async()=>{await rt.trustSource(at,U.fingerprint),p(null),await f(at)})}return c.jsxs("div",{className:"section",children:[c.jsx("h3",{children:"source host"}),c.jsxs("div",{className:"stack",children:[c.jsxs("div",{className:"row",children:[c.jsx("select",{value:g,disabled:!!V,onChange:at=>{const St=at.target.value;bt("select",St,()=>f(St))},children:o.map(at=>c.jsx("option",{value:at.id,children:zy(at)},at.id))}),c.jsx("button",{className:"btn tiny",onClick:()=>L({kind:"ssh",ssh:{port:22,auth:"password",saveSecrets:!1,sudo:!1}}),children:"new"})]}),c.jsxs("div",{className:"row wrap",style:{gap:6},children:[c.jsx("button",{className:"btn tiny",disabled:!!V,onClick:()=>{bt("select",g,()=>f(g))},children:V==="select"?"connecting…":"reconnect"}),!ct&&c.jsx("button",{className:"btn tiny",onClick:()=>L(Q),children:"edit"}),Dt&&c.jsx("button",{className:"btn tiny",onClick:()=>{I(g),_t(g)},children:"host key"}),!ct&&c.jsx("button",{className:"btn tiny danger",onClick:()=>{!Q||!confirm(`Delete source "${Q.name}"?`)||bt("del",Q.id,async()=>{await rt.deleteSource(Q.id),await O(),g===Q.id&&await f("local")})},children:"delete"})]}),j?.error&&c.jsx(Jt,{kind:"err",children:j.error}),j&&!j.error&&c.jsxs("dl",{className:"kv",children:[c.jsx("dt",{children:"reached by"}),c.jsx("dd",{className:"mono",children:j.endpoint}),j.dockerVersion&&c.jsxs(c.Fragment,{children:[c.jsx("dt",{children:"docker"}),c.jsx("dd",{children:j.dockerVersion})]})]}),Dt&&c.jsx("div",{className:"small faint",children:"Data is streamed through dockmv: source → this host → target. A local source moves it in one hop."})]}),x&&c.jsx(Ny,{initial:x,onClose:()=>L(null),onSaved:async at=>{L(null),await O(),await bt("select",at.id,()=>f(at.id))},onError:D}),U&&c.jsx(Xr,{info:U,onClose:()=>p(null),onTrust:ht})]})}function zy(o){switch(o.kind){case"local":return`${o.name} (local docker)`;case"docker":return`${o.name} (${o.dockerHost})`;default:return`${o.name} (ssh ${o.ssh?.user}@${o.ssh?.host})`}}function Ny({initial:o,onClose:g,onSaved:j,onError:f}){const[O,D]=q.useState(o),[x,L]=q.useState(!1),U=O.kind??"ssh",p=O.ssh??{};function V(C,I){D(Q=>({...Q,ssh:{...Q.ssh,[C]:I}}))}const E=U==="ssh"?!!p.host&&!!p.user:!!O.dockerHost;return c.jsx(Iu,{title:o.id?`Edit ${o.name}`:"New source host",onClose:g,footer:c.jsxs(c.Fragment,{children:[c.jsx("button",{className:"btn",onClick:g,children:"cancel"}),c.jsx("button",{className:"btn primary",disabled:x||!E,onClick:async()=>{L(!0);try{await j(await rt.saveSource(Ay(O,U)))}catch(C){f(C instanceof Error?C.message:String(C))}finally{L(!1)}},children:x?"saving…":"save"})]}),children:c.jsxs("div",{className:"stack",style:{gap:12},children:[c.jsxs("div",{className:"row",style:{gap:12},children:[c.jsx(Lt,{label:"label",children:c.jsx("input",{type:"text",value:O.name??"",onChange:C=>D(I=>({...I,name:C.target.value}))})}),c.jsx(Lt,{label:"reached by",children:c.jsxs("select",{value:U,onChange:C=>D(I=>({...I,kind:C.target.value})),children:[c.jsx("option",{value:"ssh",children:"ssh — remote host, driven through its docker CLI"}),c.jsx("option",{value:"docker",children:"docker address — a daemon this host can reach"})]})})]}),U==="docker"?c.jsxs(c.Fragment,{children:[c.jsx(Lt,{label:"docker address",children:c.jsx("input",{type:"text",placeholder:"tcp://10.0.0.5:2375",value:O.dockerHost??"",onChange:C=>D(I=>({...I,dockerHost:C.target.value}))})}),c.jsxs("div",{className:"small muted",children:["Any address the docker CLI accepts: ",c.jsx("span",{className:"mono",children:"tcp://host:2375"}),", or another socket with"," ",c.jsx("span",{className:"mono",children:"unix:///path/docker.sock"}),". A TLS-protected daemon uses the certificates from"," ",c.jsx("span",{className:"mono",children:"DOCKER_CERT_PATH"})," in dockmv's own environment."]}),c.jsxs(Jt,{kind:"warn",children:["A plain ",c.jsx("span",{className:"mono",children:"tcp://"})," daemon is unauthenticated: anyone who can reach that port is root on that host. Prefer an ssh source unless the port is already protected."]})]}):c.jsxs(c.Fragment,{children:[c.jsx(Gr,{value:p,set:V,where:"source host"}),c.jsxs("div",{className:"small muted",children:["Needs ",c.jsx("span",{className:"mono",children:"sshd"})," and a docker CLI of 18.09 or newer on that host — the API is tunnelled through ",c.jsx("span",{className:"mono",children:"docker system dial-stdio"}),". Nothing is installed."]})]})]})})}function Ay(o,g){const j={id:o.id,name:o.name,kind:g};if(g==="docker")return j.dockerHost=o.dockerHost,j;const f=o.ssh??{};return j.ssh={host:f.host??"",port:f.port??22,user:f.user??"",auth:f.auth??"password",password:f.password,privateKey:f.privateKey,privateKeyPath:f.privateKeyPath,passphrase:f.passphrase,sudo:f.sudo??!1,dockerCmd:f.dockerCmd,saveSecrets:f.saveSecrets??!1},j}function _y({source:o,plan:g,includedCount:j,options:f,setOptions:O,connections:D,activeConn:x,setActiveConn:L,reloadConnections:U,targetInv:p,connectTarget:V,onJobStarted:E,onError:C}){const[I,Q]=q.useState(null),[ct,Dt]=q.useState(null),[bt,_t]=q.useState(null),[ht,at]=q.useState(""),[St,At]=q.useState(""),[H,K]=q.useState("tar"),Y=D.find(M=>M.id===x),P=p?.preflight,mt=!!P?.serverVersion;async function st(M,J){at(M);try{await J()}catch(nt){nt instanceof Ya&&nt.needsTrust?await il():C(nt instanceof Error?nt.message:String(nt))}finally{at("")}}const $t=q.useCallback(()=>{x&&st("test",()=>V(x))},[x]);q.useEffect(()=>{$t()},[$t]);async function il(){if(x)try{Dt(await rt.probe(x))}catch(M){C(M instanceof Error?M.message:String(M))}}async function ll(){!x||!ct||await st("trust",async()=>{await rt.trust(x,ct.fingerprint),Dt(null),await V(x)})}const T=j>0&&mt&&!ht,R=j>0&&!ht;return c.jsxs(c.Fragment,{children:[c.jsxs("div",{className:"section",children:[c.jsx("h3",{children:"target host"}),c.jsxs("div",{className:"stack",children:[c.jsxs("div",{className:"row",children:[c.jsxs("select",{value:x,onChange:M=>L(M.target.value),children:[c.jsx("option",{value:"",children:"— no target selected —"}),D.map(M=>c.jsxs("option",{value:M.id,children:[M.name," (",M.user,"@",M.host,")"]},M.id))]}),c.jsx("button",{className:"btn tiny",onClick:()=>Q({port:22,auth:"password",saveSecrets:!1,sudo:!1}),children:"new"})]}),Y&&c.jsxs("div",{className:"row wrap",style:{gap:6},children:[c.jsx("button",{className:"btn tiny",disabled:!!ht,onClick:$t,children:ht==="test"?"connecting…":"connect"}),c.jsx("button",{className:"btn tiny",onClick:()=>Q(Y),children:"edit"}),c.jsx("button",{className:"btn tiny",onClick:il,children:"host key"}),c.jsx("button",{className:"btn tiny danger",onClick:()=>{confirm(`Delete connection "${Y.name}"?`)&&st("del",async()=>{await rt.deleteConnection(Y.id),U()})},children:"delete"})]}),Y&&!p&&c.jsx("div",{className:"small faint",children:"not connected yet"}),P&&c.jsxs(c.Fragment,{children:[(P.problems??[]).map((M,J)=>c.jsx(Jt,{kind:"warn",children:M},J)),mt&&c.jsxs("dl",{className:"kv",children:[c.jsx("dt",{children:"host"}),c.jsx("dd",{children:p?.host||Y?.host}),c.jsx("dt",{children:"docker"}),c.jsxs("dd",{children:[P.serverVersion," · ",P.os,"/",P.arch]}),c.jsx("dt",{children:"free space"}),c.jsxs("dd",{children:[ce(P.diskFreeBytes)," on ",P.dockerRoot]}),c.jsx("dt",{children:"existing"}),c.jsxs("dd",{children:[(p?.containers??[]).length," containers ·"," ",(p?.volumes??[]).length," volumes"]}),c.jsx("dt",{children:"gzip"}),c.jsx("dd",{children:P.hasGzip?"yes":"missing"})]})]})]})]}),c.jsxs("div",{className:"section",children:[c.jsx("h3",{children:"options"}),c.jsxs("div",{className:"stack",children:[c.jsx(Lt,{label:"if the name already exists on the target",children:c.jsxs("select",{value:f.conflict,onChange:M=>O(J=>({...J,conflict:M.target.value})),children:[c.jsx("option",{value:"fail",children:"stop with an error"}),c.jsx("option",{value:"skip",children:"skip that container"}),c.jsx("option",{value:"rename",children:"create it under a new name"}),c.jsx("option",{value:"replace",children:"remove the target's container first"})]})}),f.conflict==="rename"&&c.jsx(Lt,{label:"suffix",children:c.jsx("input",{type:"text",value:f.renameSuffix??"",onChange:M=>O(J=>({...J,renameSuffix:M.target.value}))})}),f.conflict==="replace"&&c.jsx(Jt,{kind:"warn",children:"Existing containers and volumes with the same name are deleted on the target before the copy."}),c.jsx(ml,{checked:f.compress,onChange:M=>O(J=>({...J,compress:M})),label:"compress transfers (gzip)"}),c.jsx(ml,{checked:f.verifyAfter,onChange:M=>O(J=>({...J,verifyAfter:M})),label:"verify each container after migrating"}),c.jsx(ml,{checked:f.dryRun,onChange:M=>O(J=>({...J,dryRun:M})),label:"dry run — show every command, change nothing"}),c.jsx(Lt,{label:`containers at a time: ${f.parallelism}`,children:c.jsx("input",{type:"range",min:1,max:6,value:f.parallelism,onChange:M=>O(J=>({...J,parallelism:Number(M.target.value)})),style:{width:"100%"}})})]})]}),c.jsxs("div",{className:"section",children:[c.jsx("h3",{children:"migrate over ssh"}),c.jsxs("div",{className:"stack",children:[c.jsx("button",{className:"btn primary",disabled:!T,onClick:()=>st("ssh",async()=>{const M=await rt.migrateSSH(x,g);E(M)}),children:ht==="ssh"?"starting…":`migrate ${j} container${j===1?"":"s"} to target`}),c.jsx("button",{className:"btn",disabled:j===0||!!ht,onClick:()=>st("preview",async()=>{_t(await rt.preview(g))}),children:"preview the commands"}),j===0&&c.jsx("div",{className:"small faint",children:"select at least one container"}),j>0&&!mt&&c.jsx("div",{className:"small faint",children:"connect to a target first"})]})]}),c.jsxs("div",{className:"section",children:[c.jsx("h3",{children:"migration package"}),c.jsxs("div",{className:"stack",children:[c.jsxs("div",{className:"small muted",children:["Builds a self-contained folder with the data, the images and an ",c.jsx("span",{className:"mono",children:"install.sh"})," to run on the target. No network between the hosts required."]}),c.jsx(Lt,{label:"package name",children:c.jsx("input",{type:"text",placeholder:"auto (timestamped)",value:St,onChange:M=>At(M.target.value)})}),c.jsx(Lt,{label:"format",children:c.jsxs("select",{value:H,onChange:M=>K(M.target.value),children:[c.jsx("option",{value:"tar",children:"single .tar file (downloadable)"}),c.jsx("option",{value:"dir",children:"directory on this host"})]})}),c.jsx("button",{className:"btn",disabled:!R,onClick:()=>st("pkg",async()=>{const M=await rt.buildPackage({...g,packageName:St},H);E(M)}),children:ht==="pkg"?"starting…":"build package"})]})]}),o?.inventory.warnings?.length?c.jsxs("div",{className:"section",children:[c.jsx("h3",{children:"source warnings"}),c.jsx("div",{className:"stack",children:o.inventory.warnings.map((M,J)=>c.jsx(Jt,{kind:"warn",children:M},J))})]}):null,I&&c.jsx(Oy,{initial:I,onClose:()=>Q(null),onSaved:M=>{Q(null),U(),L(M.id)},onError:C}),ct&&c.jsx(Xr,{info:ct,onClose:()=>Dt(null),onTrust:ll}),bt&&c.jsx(My,{data:bt,onClose:()=>_t(null)})]})}function Oy({initial:o,onClose:g,onSaved:j,onError:f}){const[O,D]=q.useState(o),[x,L]=q.useState(!1);function U(p,V){D(E=>({...E,[p]:V}))}return c.jsx(Iu,{title:o.id?`Edit ${o.name}`:"New target host",onClose:g,footer:c.jsxs(c.Fragment,{children:[c.jsx("button",{className:"btn",onClick:g,children:"cancel"}),c.jsx("button",{className:"btn primary",disabled:x||!O.host||!O.user,onClick:async()=>{L(!0);try{j(await rt.saveConnection(O))}catch(p){f(p instanceof Error?p.message:String(p))}finally{L(!1)}},children:x?"saving…":"save"})]}),children:c.jsxs("div",{className:"stack",style:{gap:12},children:[c.jsx(Lt,{label:"label",children:c.jsx("input",{type:"text",value:O.name??"",onChange:p=>U("name",p.target.value)})}),c.jsx(Gr,{value:O,set:U,where:"target"})]})})}function My({data:o,onClose:g}){const j=o.items.reduce((f,O)=>f+O.totalBytes,0);return c.jsx(Iu,{title:"What this migration will run",wide:!0,onClose:g,footer:c.jsx("button",{className:"btn",onClick:g,children:"close"}),children:c.jsxs("div",{className:"stack",style:{gap:16},children:[c.jsxs("div",{className:"small muted",children:[o.items.length," container(s)",j>0&&c.jsxs(c.Fragment,{children:[" · about ",ce(j)," of known volume data"]}),". These are the commands that run on the target; data is streamed into ",c.jsx("span",{className:"mono",children:"docker cp"})," rather than written to a file."]}),(o.networkCommands??[]).length>0&&c.jsxs("div",{children:[c.jsx("h3",{style:{margin:"0 0 6px",fontSize:12},children:"shared networks"}),c.jsx("pre",{className:"cmdblock",children:(o.networkCommands??[]).join(` +`)})]}),o.items.map(f=>c.jsxs("div",{children:[c.jsxs("h3",{style:{margin:"0 0 6px",fontSize:12},children:[f.name,f.targetName!==f.name&&c.jsxs("span",{className:"faint",children:[" → ",f.targetName]})]}),(f.warnings??[]).map((O,D)=>c.jsx(Jt,{kind:"warn",children:O},`w${D}`)),(f.notes??[]).map((O,D)=>c.jsxs("div",{className:"small faint",children:["· ",O]},`n${D}`)),c.jsx("pre",{className:"cmdblock",children:(f.commands??[]).join(` +`)})]},f.containerId))]})})}function Dy({jobs:o,activeJob:g,setActiveJob:j,reload:f,reloadPackages:O}){const D=g||o[0]?.id||"";return o.length===0?c.jsx("div",{className:"empty",children:"no migrations yet — select containers and start one"}):c.jsxs("div",{style:{display:"flex",minHeight:0,height:"100%"},children:[c.jsx("div",{className:"joblist",style:{width:320,flex:"0 0 320px",overflow:"auto"},children:o.map(x=>c.jsxs("div",{className:`jobcard${x.id===D?" active":""}`,onClick:()=>j(x.id),children:[c.jsxs("div",{className:"row",children:[c.jsx(Hn,{state:x.state}),c.jsx("span",{className:"spacer"}),c.jsx("span",{className:"small faint",children:x.kind==="ssh"?"ssh":"package"})]}),c.jsx("div",{className:"truncate",style:{marginTop:2},children:x.title}),c.jsxs("div",{className:"small faint",children:[new Date(x.createdAt).toLocaleTimeString()," · ",Br(x.startedAt,x.endedAt),x.dryRun&&" · dry run"]}),c.jsx("div",{style:{marginTop:6},children:c.jsx(xs,{done:x.bytesDone,total:x.bytesTotal,state:x.state})})]},x.id))}),c.jsx("div",{style:{flex:1,minWidth:0,overflow:"auto",borderLeft:"1px solid var(--border)"},children:D&&c.jsx(Cy,{id:D,reload:f,reloadPackages:O})})]})}function Cy({id:o,reload:g,reloadPackages:j}){const[f,O]=q.useState(null),[D,x]=q.useState(!0),L=q.useRef(null),U=q.useRef(!0);q.useEffect(()=>{O(null);let E=!1;rt.job(o).then(I=>!E&&O(I)).catch(()=>{});const C=rt.jobEvents(o);return C.onmessage=I=>{try{O(JSON.parse(I.data))}catch{}},C.addEventListener("done",()=>{C.close(),g(),j()}),C.onerror=()=>C.close(),()=>{E=!0,C.close()}},[o,g,j]);const p=q.useMemo(()=>(f?.log??[]).filter(E=>D||E.level!=="cmd"),[f,D]);if(q.useEffect(()=>{const E=L.current;E&&U.current&&(E.scrollTop=E.scrollHeight)},[p]),!f)return c.jsx("div",{className:"empty",children:"loading…"});const V=f.state==="running"||f.state==="pending";return c.jsxs("div",{style:{padding:16,display:"flex",flexDirection:"column",gap:14},children:[c.jsxs("div",{className:"row",children:[c.jsx(Hn,{state:f.state}),c.jsx("b",{children:f.title}),f.dryRun&&c.jsx("span",{className:"badge",children:"dry run"}),c.jsx("span",{className:"spacer"}),c.jsxs("span",{className:"small faint",children:[ce(f.bytesDone),f.bytesTotal>0&&c.jsxs(c.Fragment,{children:[" of ",ce(f.bytesTotal)]})," · ",Br(f.startedAt,f.endedAt)]}),V?c.jsx("button",{className:"btn tiny danger",onClick:()=>rt.cancelJob(f.id).then(g),children:"cancel"}):c.jsx("button",{className:"btn tiny ghost",onClick:()=>rt.deleteJob(f.id).then(g),children:"remove"})]}),c.jsx(xs,{done:f.bytesDone,total:f.bytesTotal,state:f.state}),f.error&&c.jsx(Jt,{kind:"err",children:f.error}),f.state==="succeeded"&&f.artifact&&c.jsxs(Jt,{kind:"ok",children:["Package ready at ",c.jsx("span",{className:"mono",children:f.artifact})," (",ce(f.artifactBytes??0),")."," ","Open the Packages tab to download it."]}),f.items.map(E=>c.jsxs("div",{style:{border:"1px solid var(--border)",borderRadius:6,padding:"8px 10px"},children:[c.jsxs("div",{className:"row",children:[c.jsx(Hn,{state:E.state}),c.jsx("b",{children:E.name}),c.jsx("span",{className:"spacer"}),c.jsxs("span",{className:"small faint",children:[E.steps.filter(C=>C.state==="succeeded").length,"/",E.steps.length," steps"]})]}),E.error&&c.jsx("div",{className:"small",style:{color:"var(--err)"},children:E.error}),(E.warnings??[]).map((C,I)=>c.jsxs("div",{className:"small",style:{color:"var(--warn)"},children:["! ",C]},I)),c.jsx("div",{className:"steps",children:E.steps.map(C=>c.jsxs("div",{className:`step ${C.state}`,children:[c.jsx(Hn,{state:C.state,label:""}),c.jsx("span",{className:"label truncate",title:C.error||C.label,children:C.label}),c.jsx("span",{children:C.bytesTotal>0||C.bytesDone>0?c.jsx(xs,{done:C.bytesDone,total:C.bytesTotal,state:C.state}):null}),c.jsx("span",{className:"faint nowrap",style:{textAlign:"right"},children:C.bytesDone>0?ce(C.bytesDone):C.state==="skipped"?"skipped":""})]},C.id))})]},E.id)),c.jsxs("div",{className:"row",children:[c.jsx("h3",{style:{margin:0,fontSize:12},children:"log"}),c.jsx("span",{className:"spacer"}),c.jsxs("label",{className:"check small",children:[c.jsx("input",{type:"checkbox",checked:D,onChange:E=>x(E.target.checked)}),c.jsx("span",{children:"show commands"})]})]}),c.jsxs("div",{className:"log",ref:L,onScroll:E=>{const C=E.currentTarget;U.current=C.scrollHeight-C.scrollTop-C.clientHeight<24},children:[p.map(E=>c.jsxs("div",{className:`l-${E.level}`,children:[c.jsxs("span",{className:"ts",children:[new Date(E.at).toLocaleTimeString()," "]}),E.message]},E.seq)),p.length===0&&c.jsx("span",{className:"faint",children:"nothing logged yet"})]})]})}function Uy({packages:o,reload:g}){return o.length===0?c.jsx("div",{className:"empty",children:"no packages built yet"}):c.jsxs("div",{style:{padding:16,display:"flex",flexDirection:"column",gap:12},children:[c.jsxs(Jt,{kind:"info",children:["Copy a package to the target host, then run ",c.jsx("span",{className:"mono",children:"./install.sh --dry-run"})," to review it and"," ",c.jsx("span",{className:"mono",children:"./install.sh"})," to restore. The target needs only bash, gzip and docker."]}),c.jsxs("table",{className:"mount-table",children:[c.jsx("thead",{children:c.jsxs("tr",{children:[c.jsx("th",{children:"name"}),c.jsx("th",{style:{width:110},children:"kind"}),c.jsx("th",{style:{width:110,textAlign:"right"},children:"size"}),c.jsx("th",{style:{width:170},children:"built"}),c.jsx("th",{style:{width:190}})]})}),c.jsx("tbody",{children:o.map(j=>c.jsxs("tr",{children:[c.jsx("td",{className:"mono truncate",title:j.path,children:j.name}),c.jsx("td",{children:c.jsx("span",{className:"badge",children:j.isDir?"directory":"tar"})}),c.jsx("td",{className:"nowrap",style:{textAlign:"right"},children:ce(j.bytes)}),c.jsx("td",{className:"small faint",children:new Date(j.createdAt).toLocaleString()}),c.jsx("td",{children:c.jsxs("div",{className:"row",style:{justifyContent:"flex-end",gap:6},children:[j.isDir?c.jsx("span",{className:"small faint",title:j.path,children:"copy it from disk"}):c.jsx("a",{className:"btn tiny",href:rt.downloadUrl(j.name),download:!0,children:"download"}),c.jsx("button",{className:"btn tiny danger",onClick:()=>{confirm(`Delete package "${j.name}"? This cannot be undone.`)&&rt.deletePackage(j.name).then(g)},children:"delete"})]})})]},j.name))})]})]})}function Hy({me:o}){const[g,j]=q.useState([]),[f,O]=q.useState(""),[D,x]=q.useState(""),[L,U]=q.useState(!1),[p,V]=q.useState(null),E=q.useCallback(async()=>{try{j(await rt.tokens())}catch(Q){x(Q instanceof Error?Q.message:String(Q))}},[]);q.useEffect(()=>{E()},[E]);const C=async()=>{const Q=f.trim();if(Q){U(!0),x("");try{const ct=await rt.createToken(Q);V({name:ct.name,token:ct.token}),O(""),await E()}catch(ct){x(ct instanceof Ya?ct.message:String(ct))}finally{U(!1)}}},I=async Q=>{confirm("Revoke this token? Anything using it stops working immediately.")&&(await rt.revokeToken(Q),await E())};return c.jsxs("div",{style:{padding:16,display:"flex",flexDirection:"column",gap:12,maxWidth:720},children:[c.jsxs("div",{className:"section",style:{padding:0,border:"none"},children:[c.jsx("h3",{children:"signed in as"}),c.jsx("div",{className:"mono",children:o.username})]}),c.jsxs("div",{className:"section",style:{padding:0,border:"none"},children:[c.jsx("h3",{children:"personal API tokens"}),c.jsxs("div",{className:"small faint",children:["For scripted access: pass a token as ",c.jsx("span",{className:"mono",children:"X-Auth-Token"})," or"," ",c.jsx("span",{className:"mono",children:"Authorization: Bearer …"}),"."]})]}),D&&c.jsx(Jt,{kind:"err",children:D}),p&&c.jsxs(Jt,{kind:"warn",children:["Token ",c.jsx("b",{children:p.name})," — copy it now, it will not be shown again:",c.jsx("div",{className:"fingerprint",style:{marginTop:6},children:p.token}),c.jsx("button",{className:"btn tiny",style:{marginTop:6},onClick:()=>V(null),children:"done"})]}),c.jsxs("div",{className:"row",style:{gap:8},children:[c.jsx("input",{type:"text",placeholder:"token name, e.g. ci-backup",value:f,onChange:Q=>O(Q.target.value),onKeyDown:Q=>Q.key==="Enter"&&C()}),c.jsx("button",{className:"btn primary",onClick:C,disabled:L||!f.trim(),children:"create token"})]}),g.length===0?c.jsx("div",{className:"empty",children:"no personal tokens yet"}):c.jsxs("table",{className:"mount-table",children:[c.jsx("thead",{children:c.jsxs("tr",{children:[c.jsx("th",{children:"name"}),c.jsx("th",{style:{width:90},children:"ends in"}),c.jsx("th",{style:{width:170},children:"created"}),c.jsx("th",{style:{width:170},children:"last used"}),c.jsx("th",{style:{width:80}})]})}),c.jsx("tbody",{children:g.map(Q=>c.jsxs("tr",{children:[c.jsx("td",{children:Q.name}),c.jsxs("td",{className:"mono",children:["…",Q.hint]}),c.jsx("td",{className:"small faint",children:new Date(Q.createdAt).toLocaleString()}),c.jsx("td",{className:"small faint",children:Q.lastUsedAt?new Date(Q.lastUsedAt).toLocaleString():"never"}),c.jsx("td",{children:c.jsx("button",{className:"btn tiny danger",onClick:()=>I(Q.id),children:"revoke"})})]},Q.id))})]})]})}function Ry({me:o,logout:g}){const[j,f]=q.useState(null),[O,D]=q.useState(null),[x,L]=q.useState({}),[U,p]=q.useState({conflict:"fail",renameSuffix:"-migrated",compress:!0,compressLevel:1,dryRun:!1,parallelism:1,verifyAfter:!0}),[V,E]=q.useState({}),[C,I]=q.useState([]),[Q,ct]=q.useState("local"),[Dt,bt]=q.useState(null),[_t,ht]=q.useState([]),[at,St]=q.useState(""),[At,H]=q.useState(null),[K,Y]=q.useState([]),[P,mt]=q.useState([]),[st,$t]=q.useState("containers"),[il,ll]=q.useState(""),[T,R]=q.useState(""),[M,J]=q.useState(!0),nt=q.useCallback(async()=>{J(!0);try{const W=await rt.source();D(W),L(el=>{const zl={};for(const He of W.inventory.containers)zl[He.id]=el[He.id]??W.defaults[He.id];return zl}),R(""),rt.volumeSizes().then(el=>E(el.volumes??{})).catch(()=>{})}catch(W){R(W instanceof Error?W.message:String(W))}finally{J(!1)}},[]),h=q.useCallback(async()=>{try{const W=await rt.health();f(W),W.source&&bt(W.source)}catch{}},[]),A=q.useCallback(async()=>{try{const W=await rt.sources();I(W.sources),ct(W.selected),W.current&&bt(W.current)}catch(W){R(W instanceof Error?W.message:String(W))}},[]),B=q.useCallback(async W=>{const el=await rt.selectSource(W);ct(W),bt(el),L({}),E({}),R(""),await nt(),await h()},[nt,h]),G=q.useCallback(async()=>{try{const W=await rt.connections();ht(W),St(el=>el&&W.some(zl=>zl.id===el)?el:W[0]?.id??"")}catch(W){R(W instanceof Error?W.message:String(W))}},[]),$=q.useCallback(async()=>{try{Y(await rt.jobs())}catch{}},[]),tt=q.useCallback(async()=>{try{mt(await rt.packages())}catch{}},[]);q.useEffect(()=>{h(),A(),nt(),G(),$(),tt()},[h,A,nt,G,$,tt]),q.useEffect(()=>{const W=setInterval($,4e3);return()=>clearInterval(W)},[$]);const yt=q.useCallback(async W=>{if(H(null),!W)return;const el=await rt.targetInventory(W);H(el),R("")},[]),Kt=q.useMemo(()=>Object.values(x).filter(W=>W.include),[x]),Ut=q.useMemo(()=>({items:Object.values(x),options:U}),[x,U]),se=K.filter(W=>W.state==="running"||W.state==="pending").length,Pe=q.useCallback(W=>{Y(el=>[W,...el]),ll(W.id),$t("jobs")},[]);return c.jsxs("div",{className:"app",children:[c.jsxs("header",{className:"topbar",children:[c.jsxs("div",{className:"brand",children:[c.jsx("img",{src:"/logo-icon.png",alt:"",className:"brand-logo"}),"DockMV"]}),c.jsxs("nav",{className:"tabs",children:[c.jsxs("button",{className:`tab${st==="containers"?" active":""}`,onClick:()=>$t("containers"),children:["Containers",c.jsxs("span",{className:"count",children:[Kt.length,"/",O?.inventory.containers.length??0]})]}),c.jsxs("button",{className:`tab${st==="jobs"?" active":""}`,onClick:()=>$t("jobs"),children:["Jobs",se>0&&c.jsxs("span",{className:"count",children:[se," running"]})]}),c.jsxs("button",{className:`tab${st==="packages"?" active":""}`,onClick:()=>$t("packages"),children:["Packages",P.length>0&&c.jsx("span",{className:"count",children:P.length})]}),c.jsx("button",{className:`tab${st==="account"?" active":""}`,onClick:()=>$t("account"),children:"Account"})]}),c.jsxs("div",{className:"topbar-right",children:[j&&c.jsxs("span",{className:"hostinfo",children:["source ",c.jsx("b",{children:O?.inventory.host||Dt?.name||j.dockerHost}),Dt?.kind==="ssh"&&c.jsx(c.Fragment,{children:" · over ssh"}),Dt?.kind==="docker"&&c.jsxs(c.Fragment,{children:[" · ",Dt.endpoint]}),j.dockerVersion&&c.jsxs(c.Fragment,{children:[" · docker ",j.dockerVersion]})]}),c.jsx("button",{className:"btn tiny",onClick:nt,disabled:M,children:M?"loading…":"refresh"}),c.jsx("span",{className:"small faint",children:o.username}),c.jsx("button",{className:"btn tiny ghost",onClick:g,children:"sign out"})]})]}),T&&c.jsx("div",{style:{padding:"10px 16px"},children:c.jsxs(Jt,{kind:"err",children:[T,c.jsx("button",{className:"btn tiny ghost",style:{marginLeft:8},onClick:()=>R(""),children:"dismiss"})]})}),j&&!j.ok&&c.jsx("div",{style:{padding:"10px 16px"},children:c.jsxs(Jt,{kind:"err",children:["Cannot reach the source ",c.jsx("b",{children:j.source?.name??"docker daemon"}),j.dockerHost&&c.jsxs(c.Fragment,{children:[" at ",c.jsx("span",{className:"mono",children:j.dockerHost})]}),j.dockerError&&c.jsxs(c.Fragment,{children:[" — ",j.dockerError]}),c.jsx("div",{className:"small",children:"Pick another source in the panel on the right."})]})}),c.jsxs("div",{className:"body",children:[c.jsxs("main",{className:"main",children:[st==="containers"&&c.jsx(xy,{source:O,sel:x,setSel:L,targetInv:At,loading:M,sizes:V}),st==="jobs"&&c.jsx(Dy,{jobs:K,activeJob:il,setActiveJob:ll,reload:$,reloadPackages:tt}),st==="packages"&&c.jsx(Uy,{packages:P,reload:tt}),st==="account"&&c.jsx(Hy,{me:o})]}),st==="containers"&&c.jsxs("aside",{className:"sidebar",children:[c.jsx(Ty,{sources:C,selected:Q,status:Dt,selectSource:B,reload:A,onError:R}),c.jsx(_y,{source:O,plan:Ut,includedCount:Kt.length,options:U,setOptions:p,connections:_t,activeConn:at,setActiveConn:St,reloadConnections:G,targetInv:At,connectTarget:yt,onJobStarted:Pe,onError:R})]})]})]})}function qy({needsSetup:o,onDone:g}){const[j,f]=q.useState(""),[O,D]=q.useState(""),[x,L]=q.useState(""),[U,p]=q.useState(!1),V=async E=>{E.preventDefault(),p(!0),L("");try{o?await rt.setup(j,O):await rt.login(j,O),g()}catch(C){L(C instanceof Ya?C.message:String(C))}finally{p(!1)}};return c.jsx("div",{className:"login",children:c.jsxs("form",{className:"login-box",onSubmit:V,children:[c.jsxs("div",{className:"brand",children:[c.jsx("img",{src:"/logo-icon.png",alt:"",className:"brand-logo"}),"DockMV"]}),c.jsx("h1",{children:o?"Create your account":"Sign in"}),o&&c.jsx(Jt,{kind:"info",children:"No account exists yet. Create the first one to finish setup — every account can manage every other one, there are no separate roles yet."}),c.jsxs("label",{className:"field",children:[c.jsx("span",{children:"username"}),c.jsx("input",{type:"text",autoFocus:!0,autoComplete:"username",value:j,onChange:E=>f(E.target.value),required:!0})]}),c.jsxs("label",{className:"field",children:[c.jsx("span",{children:"password"}),c.jsx("input",{type:"password",autoComplete:o?"new-password":"current-password",value:O,onChange:E=>D(E.target.value),required:!0})]}),x&&c.jsx(Jt,{kind:"err",children:x}),c.jsx("button",{className:"btn primary",type:"submit",disabled:U,children:U?"please wait…":o?"create account":"sign in"})]})})}function By(){const[o,g]=q.useState(null),[j,f]=q.useState(""),O=q.useCallback(async()=>{try{g(await rt.me()),f("")}catch(x){f(x instanceof Error?x.message:String(x))}},[]);q.useEffect(()=>{O()},[O]),q.useEffect(()=>(Rr(()=>g(x=>x&&{...x,authenticated:!1})),()=>Rr(null)),[]);const D=q.useCallback(async()=>{await rt.logout().catch(()=>{}),await O()},[O]);return o?o.authenticated?c.jsx(Ry,{me:o,logout:D}):c.jsx(qy,{needsSetup:!!o.needsSetup,onDone:O}):c.jsx("div",{className:"empty",children:j||"loading…"})}py.createRoot(document.getElementById("root")).render(c.jsx(q.StrictMode,{children:c.jsx(By,{})})); diff --git a/internal/webui/dist/assets/index-CKzWD9Xt.css b/internal/webui/dist/assets/index-CjhDzpx2.css similarity index 95% rename from internal/webui/dist/assets/index-CKzWD9Xt.css rename to internal/webui/dist/assets/index-CjhDzpx2.css index 705d376..61f6c71 100644 --- a/internal/webui/dist/assets/index-CKzWD9Xt.css +++ b/internal/webui/dist/assets/index-CjhDzpx2.css @@ -1 +1 @@ -:root{--bg: #0e1116;--bg-raised: #161b22;--bg-sunken: #0a0d12;--bg-hover: #1c232d;--border: #262d38;--border-strong: #38414f;--text: #dfe6ef;--text-dim: #8b97a8;--text-faint: #5d6878;--accent: #4c9aff;--accent-dim: #1b3a63;--ok: #3fb950;--warn: #d29922;--err: #f85149;--run: #58a6ff;--mono: ui-monospace, "SF Mono", "JetBrains Mono", "Cascadia Mono", Menlo, Consolas, monospace;--sans: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;--radius: 6px;color-scheme:dark}*{box-sizing:border-box}html,body,#root{height:100%}body{margin:0;background:var(--bg);color:var(--text);font-family:var(--sans);font-size:13px;line-height:1.5;-webkit-font-smoothing:antialiased}button,input,select,textarea{font:inherit;color:inherit}.app{display:flex;flex-direction:column;height:100%}.topbar{display:flex;align-items:center;gap:16px;padding:0 16px;height:48px;flex:0 0 auto;background:var(--bg-raised);border-bottom:1px solid var(--border)}.brand{font-family:var(--mono);font-weight:600;letter-spacing:-.3px;display:flex;align-items:center;gap:8px}.brand-logo{height:22px;width:auto;display:block}.tabs{display:flex;gap:2px;margin-left:8px}.tab{background:none;border:0;padding:6px 12px;border-radius:var(--radius);color:var(--text-dim);cursor:pointer}.tab:hover{background:var(--bg-hover);color:var(--text)}.tab.active{background:var(--accent-dim);color:#cfe3ff}.tab .count{font-family:var(--mono);font-size:11px;margin-left:6px;color:var(--text-faint)}.topbar-right{margin-left:auto;display:flex;align-items:center;gap:12px}.hostinfo{font-family:var(--mono);font-size:11px;color:var(--text-dim)}.hostinfo b{color:var(--text);font-weight:600}.body{flex:1;display:flex;min-height:0}.main{flex:1;min-width:0;overflow:auto}.sidebar{width:340px;flex:0 0 340px;overflow:auto;background:var(--bg-raised);border-left:1px solid var(--border)}@media(max-width:1100px){.body{flex-direction:column;overflow:auto}.main{overflow:visible;flex:0 0 auto}.sidebar{width:auto;flex:0 0 auto;overflow:visible;border-left:0;border-top:1px solid var(--border)}}.section{padding:14px 16px;border-bottom:1px solid var(--border)}.section h3{margin:0 0 10px;font-size:11px;text-transform:uppercase;letter-spacing:.08em;color:var(--text-faint);font-weight:600}.row{display:flex;align-items:center;gap:8px}.row.wrap{flex-wrap:wrap}.spacer{flex:1}.stack{display:flex;flex-direction:column;gap:8px}.muted{color:var(--text-dim)}.faint{color:var(--text-faint)}.mono{font-family:var(--mono)}.small{font-size:11px}.nowrap{white-space:nowrap}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.btn{background:var(--bg-hover);border:1px solid var(--border-strong);border-radius:var(--radius);padding:6px 12px;cursor:pointer;color:var(--text);white-space:nowrap}.btn:hover:not(:disabled){background:#232c38;border-color:#4a5566}.btn:disabled{opacity:.45;cursor:not-allowed}.btn.primary{background:#1f6feb;border-color:#2f7ef5;color:#fff;font-weight:500}.btn.primary:hover:not(:disabled){background:#2b7bf3}.btn.danger{border-color:#6b2a28;color:#ff8a80}.btn.danger:hover:not(:disabled){background:#351c1b}.btn.tiny{padding:2px 7px;font-size:11px}.btn.ghost{background:none;border-color:transparent;color:var(--text-dim)}.btn.ghost:hover:not(:disabled){background:var(--bg-hover);color:var(--text)}input[type=text],input[type=number],input[type=password],select,textarea{background:var(--bg-sunken);border:1px solid var(--border-strong);border-radius:var(--radius);padding:5px 8px;width:100%}input:focus,select:focus,textarea:focus{outline:2px solid var(--accent-dim);border-color:var(--accent)}textarea{font-family:var(--mono);font-size:11px;resize:vertical}label.field{display:block}label.field>span{display:block;font-size:11px;color:var(--text-dim);margin-bottom:3px}.check{display:flex;align-items:center;gap:7px;cursor:pointer;-webkit-user-select:none;user-select:none}.check input{accent-color:var(--accent);width:14px;height:14px;margin:0;cursor:pointer}.check.disabled{opacity:.45;cursor:not-allowed}.badge{font-family:var(--mono);font-size:10px;padding:1px 5px;border-radius:3px;border:1px solid var(--border-strong);color:var(--text-dim);white-space:nowrap}.badge.vol{border-color:#2d4a6b;color:#83b8f0}.badge.bind{border-color:#5c4520;color:#e0b556}.badge.anon{border-color:#46375e;color:#b294e0}.badge.tmpfs{border-color:#33404d;color:#9aa7b6}.badge.net{border-color:#2b5040;color:#6cc79b}.badge.port{border-color:#3a3f52;color:#a5aec9}.state{display:inline-flex;align-items:center;gap:5px;font-size:11px}.state .dot{width:7px;height:7px;border-radius:50%;background:var(--text-faint);flex:0 0 auto}.state.running .dot{background:var(--ok)}.state.exited .dot,.state.dead .dot{background:var(--text-faint)}.state.paused .dot,.state.restarting .dot{background:var(--warn)}.state.succeeded .dot{background:var(--ok)}.state.failed .dot{background:var(--err)}.state.canceled .dot{background:var(--warn)}.state.pending .dot,.state.skipped .dot{background:var(--text-faint)}.state.running .dot{animation:pulse 1.4s ease-in-out infinite}@keyframes pulse{0%,to{opacity:1}50%{opacity:.35}}.notice{padding:8px 10px;border-radius:var(--radius);font-size:12px;border:1px solid var(--border-strong);background:var(--bg-sunken)}.notice.warn{border-color:#5c4520;background:#221a0c;color:#f0cd82}.notice.err{border-color:#6b2a28;background:#2a1413;color:#ffb3ad}.notice.ok{border-color:#23543a;background:#0f2318;color:#97e0ac}.toolbar{position:sticky;top:0;z-index:5;display:flex;align-items:center;gap:10px;flex-wrap:wrap;padding:10px 16px;background:var(--bg);border-bottom:1px solid var(--border)}.toolbar .search{width:220px}.group-head{display:flex;align-items:center;gap:8px;padding:8px 16px 4px;color:var(--text-faint);font-size:11px;text-transform:uppercase;letter-spacing:.06em}.group-head .line{flex:1;height:1px;background:var(--border)}.clist{display:flex;flex-direction:column}.crow{display:grid;grid-template-columns:26px 22px minmax(180px,1.4fr) minmax(140px,1.2fr) minmax(200px,2fr) auto;align-items:center;gap:10px;padding:7px 16px;border-bottom:1px solid var(--border);cursor:default}.crow:hover{background:var(--bg-hover)}.crow.selected{background:#11213a}.crow.selected:hover{background:#16294a}.crow .name{font-weight:500}.crow .sub{font-size:11px;color:var(--text-faint)}.crow .image{font-family:var(--mono);font-size:11px;color:var(--text-dim)}.crow .tags{display:flex;gap:4px;flex-wrap:wrap}.expander{background:none;border:0;color:var(--text-faint);cursor:pointer;padding:2px;line-height:1;border-radius:3px}.expander:hover{color:var(--text);background:var(--bg-hover)}.detail{padding:12px 16px 16px 52px;background:var(--bg-sunken);border-bottom:1px solid var(--border);display:grid;gap:14px}.detail .grid2{display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:12px}.mount-table{width:100%;border-collapse:collapse;font-size:12px}.mount-table th{text-align:left;font-weight:500;color:var(--text-faint);font-size:11px;padding:4px 8px 4px 0;border-bottom:1px solid var(--border)}.mount-table td{padding:5px 8px 5px 0;border-bottom:1px solid var(--border);vertical-align:middle}.mount-table tr:last-child td{border-bottom:0}.mount-table select{width:auto;min-width:110px}.mount-table input[type=text]{min-width:150px}.joblist{padding:12px 16px;display:flex;flex-direction:column;gap:8px}.jobcard{border:1px solid var(--border);border-radius:var(--radius);background:var(--bg-raised);padding:10px 12px;cursor:pointer}.jobcard:hover{border-color:var(--border-strong)}.jobcard.active{border-color:var(--accent)}.progress{height:4px;background:var(--bg-sunken);border-radius:2px;overflow:hidden}.progress>div{height:100%;background:var(--accent);transition:width .25s ease}.progress.done>div{background:var(--ok)}.progress.failed>div{background:var(--err)}.steps{display:flex;flex-direction:column;gap:3px;margin-top:6px}.step{display:grid;grid-template-columns:14px 1fr 130px 90px;align-items:center;gap:8px;font-size:11px}.step .label{color:var(--text-dim)}.step.failed .label{color:#ff9b95}.log{font-family:var(--mono);font-size:11px;line-height:1.55;background:var(--bg-sunken);border:1px solid var(--border);border-radius:var(--radius);padding:8px 10px;max-height:340px;overflow:auto;white-space:pre-wrap;word-break:break-word}.log .l-warn{color:var(--warn)}.log .l-error{color:var(--err)}.log .l-cmd{color:#7ee0b8}.log .l-info{color:var(--text-dim)}.log .ts{color:var(--text-faint)}.modal-backdrop{position:fixed;inset:0;background:#03060ab8;display:flex;align-items:center;justify-content:center;padding:24px;z-index:50}.modal{background:var(--bg-raised);border:1px solid var(--border-strong);border-radius:10px;width:min(760px,100%);max-height:100%;display:flex;flex-direction:column;overflow:hidden;box-shadow:0 18px 50px #0000008c}.modal header{padding:12px 16px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:10px;font-weight:600}.modal .content{padding:16px;overflow:auto}.modal footer{padding:12px 16px;border-top:1px solid var(--border);display:flex;gap:8px;justify-content:flex-end}.cmdblock{font-family:var(--mono);font-size:11px;background:var(--bg-sunken);border:1px solid var(--border);border-radius:var(--radius);padding:8px 10px;overflow-x:auto;white-space:pre;margin:0}.empty{padding:48px 16px;text-align:center;color:var(--text-faint)}.kv{display:grid;grid-template-columns:auto 1fr;gap:3px 12px;font-size:12px}.kv dt{color:var(--text-faint)}.kv dd{margin:0;font-family:var(--mono);font-size:11px}.fingerprint{font-family:var(--mono);font-size:12px;word-break:break-all;background:var(--bg-sunken);border:1px solid var(--border-strong);border-radius:var(--radius);padding:8px 10px} +:root{--bg: #0e1116;--bg-raised: #161b22;--bg-sunken: #0a0d12;--bg-hover: #1c232d;--border: #262d38;--border-strong: #38414f;--text: #dfe6ef;--text-dim: #8b97a8;--text-faint: #5d6878;--accent: #4c9aff;--accent-dim: #1b3a63;--ok: #3fb950;--warn: #d29922;--err: #f85149;--run: #58a6ff;--mono: ui-monospace, "SF Mono", "JetBrains Mono", "Cascadia Mono", Menlo, Consolas, monospace;--sans: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;--radius: 6px;color-scheme:dark}*{box-sizing:border-box}html,body,#root{height:100%}body{margin:0;background:var(--bg);color:var(--text);font-family:var(--sans);font-size:13px;line-height:1.5;-webkit-font-smoothing:antialiased}button,input,select,textarea{font:inherit;color:inherit}.app{display:flex;flex-direction:column;height:100%}.topbar{display:flex;align-items:center;gap:16px;padding:0 16px;height:48px;flex:0 0 auto;background:var(--bg-raised);border-bottom:1px solid var(--border)}.brand{font-family:var(--mono);font-weight:600;letter-spacing:-.3px;display:flex;align-items:center;gap:8px}.brand-logo{height:22px;width:auto;display:block}.tabs{display:flex;gap:2px;margin-left:8px}.tab{background:none;border:0;padding:6px 12px;border-radius:var(--radius);color:var(--text-dim);cursor:pointer}.tab:hover{background:var(--bg-hover);color:var(--text)}.tab.active{background:var(--accent-dim);color:#cfe3ff}.tab .count{font-family:var(--mono);font-size:11px;margin-left:6px;color:var(--text-faint)}.topbar-right{margin-left:auto;display:flex;align-items:center;gap:12px}.hostinfo{font-family:var(--mono);font-size:11px;color:var(--text-dim)}.hostinfo b{color:var(--text);font-weight:600}.body{flex:1;display:flex;min-height:0}.main{flex:1;min-width:0;overflow:auto}.sidebar{width:340px;flex:0 0 340px;overflow:auto;background:var(--bg-raised);border-left:1px solid var(--border)}@media(max-width:1100px){.body{flex-direction:column;overflow:auto}.main{overflow:visible;flex:0 0 auto}.sidebar{width:auto;flex:0 0 auto;overflow:visible;border-left:0;border-top:1px solid var(--border)}}.section{padding:14px 16px;border-bottom:1px solid var(--border)}.section h3{margin:0 0 10px;font-size:11px;text-transform:uppercase;letter-spacing:.08em;color:var(--text-faint);font-weight:600}.row{display:flex;align-items:center;gap:8px}.row.wrap{flex-wrap:wrap}.spacer{flex:1}.stack{display:flex;flex-direction:column;gap:8px}.muted{color:var(--text-dim)}.faint{color:var(--text-faint)}.mono{font-family:var(--mono)}.small{font-size:11px}.nowrap{white-space:nowrap}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.btn{background:var(--bg-hover);border:1px solid var(--border-strong);border-radius:var(--radius);padding:6px 12px;cursor:pointer;color:var(--text);white-space:nowrap}.btn:hover:not(:disabled){background:#232c38;border-color:#4a5566}.btn:disabled{opacity:.45;cursor:not-allowed}.btn.primary{background:#1f6feb;border-color:#2f7ef5;color:#fff;font-weight:500}.btn.primary:hover:not(:disabled){background:#2b7bf3}.btn.danger{border-color:#6b2a28;color:#ff8a80}.btn.danger:hover:not(:disabled){background:#351c1b}.btn.tiny{padding:2px 7px;font-size:11px}.btn.ghost{background:none;border-color:transparent;color:var(--text-dim)}.btn.ghost:hover:not(:disabled){background:var(--bg-hover);color:var(--text)}input[type=text],input[type=number],input[type=password],select,textarea{background:var(--bg-sunken);border:1px solid var(--border-strong);border-radius:var(--radius);padding:5px 8px;width:100%}input:focus,select:focus,textarea:focus{outline:2px solid var(--accent-dim);border-color:var(--accent)}textarea{font-family:var(--mono);font-size:11px;resize:vertical}label.field{display:block}label.field>span{display:block;font-size:11px;color:var(--text-dim);margin-bottom:3px}.check{display:flex;align-items:center;gap:7px;cursor:pointer;-webkit-user-select:none;user-select:none}.check input{accent-color:var(--accent);width:14px;height:14px;margin:0;cursor:pointer}.check.disabled{opacity:.45;cursor:not-allowed}.badge{font-family:var(--mono);font-size:10px;padding:1px 5px;border-radius:3px;border:1px solid var(--border-strong);color:var(--text-dim);white-space:nowrap}.badge.vol{border-color:#2d4a6b;color:#83b8f0}.badge.bind{border-color:#5c4520;color:#e0b556}.badge.anon{border-color:#46375e;color:#b294e0}.badge.tmpfs{border-color:#33404d;color:#9aa7b6}.badge.net{border-color:#2b5040;color:#6cc79b}.badge.port{border-color:#3a3f52;color:#a5aec9}.state{display:inline-flex;align-items:center;gap:5px;font-size:11px}.state .dot{width:7px;height:7px;border-radius:50%;background:var(--text-faint);flex:0 0 auto}.state.running .dot{background:var(--ok)}.state.exited .dot,.state.dead .dot{background:var(--text-faint)}.state.paused .dot,.state.restarting .dot{background:var(--warn)}.state.succeeded .dot{background:var(--ok)}.state.failed .dot{background:var(--err)}.state.canceled .dot{background:var(--warn)}.state.pending .dot,.state.skipped .dot{background:var(--text-faint)}.state.running .dot{animation:pulse 1.4s ease-in-out infinite}@keyframes pulse{0%,to{opacity:1}50%{opacity:.35}}.notice{padding:8px 10px;border-radius:var(--radius);font-size:12px;border:1px solid var(--border-strong);background:var(--bg-sunken)}.notice.warn{border-color:#5c4520;background:#221a0c;color:#f0cd82}.notice.err{border-color:#6b2a28;background:#2a1413;color:#ffb3ad}.notice.ok{border-color:#23543a;background:#0f2318;color:#97e0ac}.toolbar{position:sticky;top:0;z-index:5;display:flex;align-items:center;gap:10px;flex-wrap:wrap;padding:10px 16px;background:var(--bg);border-bottom:1px solid var(--border)}.toolbar .search{width:220px}.group-head{display:flex;align-items:center;gap:8px;padding:8px 16px 4px;color:var(--text-faint);font-size:11px;text-transform:uppercase;letter-spacing:.06em}.group-head .line{flex:1;height:1px;background:var(--border)}.clist{display:flex;flex-direction:column}.crow{display:grid;grid-template-columns:26px 22px minmax(180px,1.4fr) minmax(140px,1.2fr) minmax(200px,2fr) auto;align-items:center;gap:10px;padding:7px 16px;border-bottom:1px solid var(--border);cursor:default}.crow:hover{background:var(--bg-hover)}.crow.selected{background:#11213a}.crow.selected:hover{background:#16294a}.crow .name{font-weight:500}.crow .sub{font-size:11px;color:var(--text-faint)}.crow .image{font-family:var(--mono);font-size:11px;color:var(--text-dim)}.crow .tags{display:flex;gap:4px;flex-wrap:wrap}.expander{background:none;border:0;color:var(--text-faint);cursor:pointer;padding:2px;line-height:1;border-radius:3px}.expander:hover{color:var(--text);background:var(--bg-hover)}.detail{padding:12px 16px 16px 52px;background:var(--bg-sunken);border-bottom:1px solid var(--border);display:grid;gap:14px}.detail .grid2{display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:12px}.mount-table{width:100%;border-collapse:collapse;font-size:12px}.mount-table th{text-align:left;font-weight:500;color:var(--text-faint);font-size:11px;padding:4px 8px 4px 0;border-bottom:1px solid var(--border)}.mount-table td{padding:5px 8px 5px 0;border-bottom:1px solid var(--border);vertical-align:middle}.mount-table tr:last-child td{border-bottom:0}.mount-table select{width:auto;min-width:110px}.mount-table input[type=text]{min-width:150px}.joblist{padding:12px 16px;display:flex;flex-direction:column;gap:8px}.jobcard{border:1px solid var(--border);border-radius:var(--radius);background:var(--bg-raised);padding:10px 12px;cursor:pointer}.jobcard:hover{border-color:var(--border-strong)}.jobcard.active{border-color:var(--accent)}.progress{height:4px;background:var(--bg-sunken);border-radius:2px;overflow:hidden}.progress>div{height:100%;background:var(--accent);transition:width .25s ease}.progress.done>div{background:var(--ok)}.progress.failed>div{background:var(--err)}.steps{display:flex;flex-direction:column;gap:3px;margin-top:6px}.step{display:grid;grid-template-columns:14px 1fr 130px 90px;align-items:center;gap:8px;font-size:11px}.step .label{color:var(--text-dim)}.step.failed .label{color:#ff9b95}.log{font-family:var(--mono);font-size:11px;line-height:1.55;background:var(--bg-sunken);border:1px solid var(--border);border-radius:var(--radius);padding:8px 10px;max-height:340px;overflow:auto;white-space:pre-wrap;word-break:break-word}.log .l-warn{color:var(--warn)}.log .l-error{color:var(--err)}.log .l-cmd{color:#7ee0b8}.log .l-info{color:var(--text-dim)}.log .ts{color:var(--text-faint)}.modal-backdrop{position:fixed;inset:0;background:#03060ab8;display:flex;align-items:center;justify-content:center;padding:24px;z-index:50}.modal{background:var(--bg-raised);border:1px solid var(--border-strong);border-radius:10px;width:min(760px,100%);max-height:100%;display:flex;flex-direction:column;overflow:hidden;box-shadow:0 18px 50px #0000008c}.modal header{padding:12px 16px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:10px;font-weight:600}.modal .content{padding:16px;overflow:auto}.modal footer{padding:12px 16px;border-top:1px solid var(--border);display:flex;gap:8px;justify-content:flex-end}.cmdblock{font-family:var(--mono);font-size:11px;background:var(--bg-sunken);border:1px solid var(--border);border-radius:var(--radius);padding:8px 10px;overflow-x:auto;white-space:pre;margin:0}.empty{padding:48px 16px;text-align:center;color:var(--text-faint)}.kv{display:grid;grid-template-columns:auto 1fr;gap:3px 12px;font-size:12px}.kv dt{color:var(--text-faint)}.kv dd{margin:0;font-family:var(--mono);font-size:11px}.fingerprint{font-family:var(--mono);font-size:12px;word-break:break-all;background:var(--bg-sunken);border:1px solid var(--border-strong);border-radius:var(--radius);padding:8px 10px}.login{height:100%;display:flex;align-items:center;justify-content:center}.login-box{width:min(360px,90vw);display:flex;flex-direction:column;gap:14px;background:var(--bg-raised);border:1px solid var(--border);border-radius:var(--radius);padding:28px}.login-box .brand{display:flex;align-items:center;gap:8px;font-weight:600}.login-box h1{font-size:16px;font-weight:600;margin:0} diff --git a/internal/webui/dist/assets/index-qcSVszEj.js b/internal/webui/dist/assets/index-qcSVszEj.js deleted file mode 100644 index 13dfbc0..0000000 --- a/internal/webui/dist/assets/index-qcSVszEj.js +++ /dev/null @@ -1,11 +0,0 @@ -(function(){const S=document.createElement("link").relList;if(S&&S.supports&&S.supports("modulepreload"))return;for(const O of document.querySelectorAll('link[rel="modulepreload"]'))s(O);new MutationObserver(O=>{for(const D of O)if(D.type==="childList")for(const T of D.addedNodes)T.tagName==="LINK"&&T.rel==="modulepreload"&&s(T)}).observe(document,{childList:!0,subtree:!0});function z(O){const D={};return O.integrity&&(D.integrity=O.integrity),O.referrerPolicy&&(D.referrerPolicy=O.referrerPolicy),O.crossOrigin==="use-credentials"?D.credentials="include":O.crossOrigin==="anonymous"?D.credentials="omit":D.credentials="same-origin",D}function s(O){if(O.ep)return;O.ep=!0;const D=z(O);fetch(O.href,D)}})();var gf={exports:{}},Dn={};var Nr;function sy(){if(Nr)return Dn;Nr=1;var o=Symbol.for("react.transitional.element"),S=Symbol.for("react.fragment");function z(s,O,D){var T=null;if(D!==void 0&&(T=""+D),O.key!==void 0&&(T=""+O.key),"key"in O){D={};for(var w in O)w!=="key"&&(D[w]=O[w])}else D=O;return O=D.ref,{$$typeof:o,type:s,key:T,ref:O!==void 0?O:null,props:D}}return Dn.Fragment=S,Dn.jsx=z,Dn.jsxs=z,Dn}var Ar;function oy(){return Ar||(Ar=1,gf.exports=sy()),gf.exports}var c=oy(),Sf={exports:{}},I={};var _r;function dy(){if(_r)return I;_r=1;var o=Symbol.for("react.transitional.element"),S=Symbol.for("react.portal"),z=Symbol.for("react.fragment"),s=Symbol.for("react.strict_mode"),O=Symbol.for("react.profiler"),D=Symbol.for("react.consumer"),T=Symbol.for("react.context"),w=Symbol.for("react.forward_ref"),H=Symbol.for("react.suspense"),b=Symbol.for("react.memo"),V=Symbol.for("react.lazy"),E=Symbol.for("react.activity"),U=Symbol.iterator;function ll(h){return h===null||typeof h!="object"?null:(h=U&&h[U]||h["@@iterator"],typeof h=="function"?h:null)}var W={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},vl=Object.assign,Yl={};function gl(h,A,q){this.props=h,this.context=A,this.refs=Yl,this.updater=q||W}gl.prototype.isReactComponent={},gl.prototype.setState=function(h,A){if(typeof h!="object"&&typeof h!="function"&&h!=null)throw Error("takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,h,A,"setState")},gl.prototype.forceUpdate=function(h){this.updater.enqueueForceUpdate(this,h,"forceUpdate")};function _l(){}_l.prototype=gl.prototype;function dl(h,A,q){this.props=h,this.context=A,this.refs=Yl,this.updater=q||W}var el=dl.prototype=new _l;el.constructor=dl,vl(el,gl.prototype),el.isPureReactComponent=!0;var Sl=Array.isArray;function Nl(){}var C={H:null,A:null,T:null,S:null},Z=Object.prototype.hasOwnProperty;function B(h,A,q){var Y=q.ref;return{$$typeof:o,type:h,key:A,ref:Y!==void 0?Y:null,props:q}}function $(h,A){return B(h.type,A,h.props)}function sl(h){return typeof h=="object"&&h!==null&&h.$$typeof===o}function jl(h){var A={"=":"=0",":":"=2"};return"$"+h.replace(/[=:]/g,function(q){return A[q]})}var at=/\/+/g;function tt(h,A){return typeof h=="object"&&h!==null&&h.key!=null?jl(""+h.key):A.toString(36)}function Cl(h){switch(h.status){case"fulfilled":return h.value;case"rejected":throw h.reason;default:switch(typeof h.status=="string"?h.then(Nl,Nl):(h.status="pending",h.then(function(A){h.status==="pending"&&(h.status="fulfilled",h.value=A)},function(A){h.status==="pending"&&(h.status="rejected",h.reason=A)})),h.status){case"fulfilled":return h.value;case"rejected":throw h.reason}}throw h}function j(h,A,q,Y,F){var al=typeof h;(al==="undefined"||al==="boolean")&&(h=null);var rl=!1;if(h===null)rl=!0;else switch(al){case"bigint":case"string":case"number":rl=!0;break;case"object":switch(h.$$typeof){case o:case S:rl=!0;break;case V:return rl=h._init,j(rl(h._payload),A,q,Y,F)}}if(rl)return F=F(h),rl=Y===""?"."+tt(h,0):Y,Sl(F)?(q="",rl!=null&&(q=rl.replace(at,"$&/")+"/"),j(F,A,q,"",function(k){return k})):F!=null&&(sl(F)&&(F=$(F,q+(F.key==null||h&&h.key===F.key?"":(""+F.key).replace(at,"$&/")+"/")+rl)),A.push(F)),1;rl=0;var Kl=Y===""?".":Y+":";if(Sl(h))for(var Ul=0;Ul>>1,nl=j[K];if(0>>1;KO(q,M))YO(F,q)?(j[K]=F,j[Y]=M,K=Y):(j[K]=q,j[A]=M,K=A);else if(YO(F,M))j[K]=F,j[Y]=M,K=Y;else break l}}return R}function O(j,R){var M=j.sortIndex-R.sortIndex;return M!==0?M:j.id-R.id}if(o.unstable_now=void 0,typeof performance=="object"&&typeof performance.now=="function"){var D=performance;o.unstable_now=function(){return D.now()}}else{var T=Date,w=T.now();o.unstable_now=function(){return T.now()-w}}var H=[],b=[],V=1,E=null,U=3,ll=!1,W=!1,vl=!1,Yl=!1,gl=typeof setTimeout=="function"?setTimeout:null,_l=typeof clearTimeout=="function"?clearTimeout:null,dl=typeof setImmediate<"u"?setImmediate:null;function el(j){for(var R=z(b);R!==null;){if(R.callback===null)s(b);else if(R.startTime<=j)s(b),R.sortIndex=R.expirationTime,S(H,R);else break;R=z(b)}}function Sl(j){if(vl=!1,el(j),!W)if(z(H)!==null)W=!0,Nl||(Nl=!0,jl());else{var R=z(b);R!==null&&Cl(Sl,R.startTime-j)}}var Nl=!1,C=-1,Z=5,B=-1;function $(){return Yl?!0:!(o.unstable_now()-Bj&&$());){var K=E.callback;if(typeof K=="function"){E.callback=null,U=E.priorityLevel;var nl=K(E.expirationTime<=j);if(j=o.unstable_now(),typeof nl=="function"){E.callback=nl,el(j),R=!0;break t}E===z(H)&&s(H),el(j)}else s(H);E=z(H)}if(E!==null)R=!0;else{var h=z(b);h!==null&&Cl(Sl,h.startTime-j),R=!1}}break l}finally{E=null,U=M,ll=!1}R=void 0}}finally{R?jl():Nl=!1}}}var jl;if(typeof dl=="function")jl=function(){dl(sl)};else if(typeof MessageChannel<"u"){var at=new MessageChannel,tt=at.port2;at.port1.onmessage=sl,jl=function(){tt.postMessage(null)}}else jl=function(){gl(sl,0)};function Cl(j,R){C=gl(function(){j(o.unstable_now())},R)}o.unstable_IdlePriority=5,o.unstable_ImmediatePriority=1,o.unstable_LowPriority=4,o.unstable_NormalPriority=3,o.unstable_Profiling=null,o.unstable_UserBlockingPriority=2,o.unstable_cancelCallback=function(j){j.callback=null},o.unstable_forceFrameRate=function(j){0>j||125K?(j.sortIndex=M,S(b,j),z(H)===null&&j===z(b)&&(vl?(_l(C),C=-1):vl=!0,Cl(Sl,M-K))):(j.sortIndex=nl,S(H,j),W||ll||(W=!0,Nl||(Nl=!0,jl()))),j},o.unstable_shouldYield=$,o.unstable_wrapCallback=function(j){var R=U;return function(){var M=U;U=R;try{return j.apply(this,arguments)}finally{U=M}}}})(jf)),jf}var Dr;function hy(){return Dr||(Dr=1,pf.exports=ry()),pf.exports}var xf={exports:{}},lt={};var Cr;function my(){if(Cr)return lt;Cr=1;var o=Tf();function S(H){var b="https://react.dev/errors/"+H;if(1"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(o)}catch(S){console.error(S)}}return o(),xf.exports=my(),xf.exports}var Hr;function vy(){if(Hr)return Cn;Hr=1;var o=hy(),S=Tf(),z=yy();function s(l){var t="https://react.dev/errors/"+l;if(1nl||(l.current=K[nl],K[nl]=null,nl--)}function q(l,t){nl++,K[nl]=l.current,l.current=t}var Y=h(null),F=h(null),al=h(null),rl=h(null);function Kl(l,t){switch(q(al,t),q(F,l),q(Y,null),t.nodeType){case 9:case 11:l=(l=t.documentElement)&&(l=l.namespaceURI)?$d(l):0;break;default:if(l=t.tagName,t=t.namespaceURI)t=$d(t),l=Wd(t,l);else switch(l){case"svg":l=1;break;case"math":l=2;break;default:l=0}}A(Y),q(Y,l)}function Ul(){A(Y),A(F),A(al)}function k(l){l.memoizedState!==null&&q(rl,l);var t=Y.current,e=Wd(t,l.type);t!==e&&(q(F,l),q(Y,e))}function Jl(l){F.current===l&&(A(Y),A(F)),rl.current===l&&(A(rl),An._currentValue=M)}var ce,Ie;function Ue(l){if(ce===void 0)try{throw Error()}catch(e){var t=e.stack.trim().match(/\n( *(at )?)/);ce=t&&t[1]||"",Ie=-1)":-1n||d[a]!==v[n]){var x=` -`+d[a].replace(" at new "," at ");return l.displayName&&x.includes("")&&(x=x.replace("",l.displayName)),x}while(1<=a&&0<=n);break}}}finally{Pu=!1,Error.prepareStackTrace=e}return(e=l?l.displayName||l.name:"")?Ue(e):""}function Xr(l,t){switch(l.tag){case 26:case 27:case 5:return Ue(l.type);case 16:return Ue("Lazy");case 13:return l.child!==t&&t!==null?Ue("Suspense Fallback"):Ue("Suspense");case 19:return Ue("SuspenseList");case 0:case 15:return li(l.type,!1);case 11:return li(l.type.render,!1);case 1:return li(l.type,!0);case 31:return Ue("Activity");default:return""}}function Ef(l){try{var t="",e=null;do t+=Xr(l,e),e=l,l=l.return;while(l);return t}catch(a){return` -Error generating stack: `+a.message+` -`+a.stack}}var ti=Object.prototype.hasOwnProperty,ei=o.unstable_scheduleCallback,ai=o.unstable_cancelCallback,Qr=o.unstable_shouldYield,Zr=o.unstable_requestPaint,mt=o.unstable_now,Lr=o.unstable_getCurrentPriorityLevel,Nf=o.unstable_ImmediatePriority,Af=o.unstable_UserBlockingPriority,Hn=o.unstable_NormalPriority,Vr=o.unstable_LowPriority,_f=o.unstable_IdlePriority,Kr=o.log,Jr=o.unstable_setDisableYieldValue,Ya=null,yt=null;function fe(l){if(typeof Kr=="function"&&Jr(l),yt&&typeof yt.setStrictMode=="function")try{yt.setStrictMode(Ya,l)}catch{}}var vt=Math.clz32?Math.clz32:$r,wr=Math.log,kr=Math.LN2;function $r(l){return l>>>=0,l===0?32:31-(wr(l)/kr|0)|0}var Rn=256,qn=262144,Bn=4194304;function He(l){var t=l&42;if(t!==0)return t;switch(l&-l){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:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return l&261888;case 262144:case 524288:case 1048576:case 2097152:return l&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return l&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return l}}function Yn(l,t,e){var a=l.pendingLanes;if(a===0)return 0;var n=0,u=l.suspendedLanes,i=l.pingedLanes;l=l.warmLanes;var f=a&134217727;return f!==0?(a=f&~u,a!==0?n=He(a):(i&=f,i!==0?n=He(i):e||(e=f&~l,e!==0&&(n=He(e))))):(f=a&~u,f!==0?n=He(f):i!==0?n=He(i):e||(e=a&~l,e!==0&&(n=He(e)))),n===0?0:t!==0&&t!==n&&(t&u)===0&&(u=n&-n,e=t&-t,u>=e||u===32&&(e&4194048)!==0)?t:n}function Ga(l,t){return(l.pendingLanes&~(l.suspendedLanes&~l.pingedLanes)&t)===0}function Wr(l,t){switch(l){case 1:case 2:case 4:case 8:case 64:return t+250;case 16:case 32: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 t+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function Of(){var l=Bn;return Bn<<=1,(Bn&62914560)===0&&(Bn=4194304),l}function ni(l){for(var t=[],e=0;31>e;e++)t.push(l);return t}function Xa(l,t){l.pendingLanes|=t,t!==268435456&&(l.suspendedLanes=0,l.pingedLanes=0,l.warmLanes=0)}function Fr(l,t,e,a,n,u){var i=l.pendingLanes;l.pendingLanes=e,l.suspendedLanes=0,l.pingedLanes=0,l.warmLanes=0,l.expiredLanes&=e,l.entangledLanes&=e,l.errorRecoveryDisabledLanes&=e,l.shellSuspendCounter=0;var f=l.entanglements,d=l.expirationTimes,v=l.hiddenUpdates;for(e=i&~e;0"u")return null;try{return l.activeElement||l.body}catch{return l.body}}var ah=/[\n"\\]/g;function Et(l){return l.replace(ah,function(t){return"\\"+t.charCodeAt(0).toString(16)+" "})}function oi(l,t,e,a,n,u,i,f){l.name="",i!=null&&typeof i!="function"&&typeof i!="symbol"&&typeof i!="boolean"?l.type=i:l.removeAttribute("type"),t!=null?i==="number"?(t===0&&l.value===""||l.value!=t)&&(l.value=""+Tt(t)):l.value!==""+Tt(t)&&(l.value=""+Tt(t)):i!=="submit"&&i!=="reset"||l.removeAttribute("value"),t!=null?di(l,i,Tt(t)):e!=null?di(l,i,Tt(e)):a!=null&&l.removeAttribute("value"),n==null&&u!=null&&(l.defaultChecked=!!u),n!=null&&(l.checked=n&&typeof n!="function"&&typeof n!="symbol"),f!=null&&typeof f!="function"&&typeof f!="symbol"&&typeof f!="boolean"?l.name=""+Tt(f):l.removeAttribute("name")}function Zf(l,t,e,a,n,u,i,f){if(u!=null&&typeof u!="function"&&typeof u!="symbol"&&typeof u!="boolean"&&(l.type=u),t!=null||e!=null){if(!(u!=="submit"&&u!=="reset"||t!=null)){si(l);return}e=e!=null?""+Tt(e):"",t=t!=null?""+Tt(t):e,f||t===l.value||(l.value=t),l.defaultValue=t}a=a??n,a=typeof a!="function"&&typeof a!="symbol"&&!!a,l.checked=f?l.checked:!!a,l.defaultChecked=!!a,i!=null&&typeof i!="function"&&typeof i!="symbol"&&typeof i!="boolean"&&(l.name=i),si(l)}function di(l,t,e){t==="number"&&Qn(l.ownerDocument)===l||l.defaultValue===""+e||(l.defaultValue=""+e)}function na(l,t,e,a){if(l=l.options,t){t={};for(var n=0;n"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),vi=!1;if(Vt)try{var Va={};Object.defineProperty(Va,"passive",{get:function(){vi=!0}}),window.addEventListener("test",Va,Va),window.removeEventListener("test",Va,Va)}catch{vi=!1}var oe=null,gi=null,Ln=null;function $f(){if(Ln)return Ln;var l,t=gi,e=t.length,a,n="value"in oe?oe.value:oe.textContent,u=n.length;for(l=0;l=wa),ts=" ",es=!1;function as(l,t){switch(l){case"keyup":return Dh.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function ns(l){return l=l.detail,typeof l=="object"&&"data"in l?l.data:null}var fa=!1;function Uh(l,t){switch(l){case"compositionend":return ns(t);case"keypress":return t.which!==32?null:(es=!0,ts);case"textInput":return l=t.data,l===ts&&es?null:l;default:return null}}function Hh(l,t){if(fa)return l==="compositionend"||!xi&&as(l,t)?(l=$f(),Ln=gi=oe=null,fa=!1,l):null;switch(l){case"paste":return null;case"keypress":if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:e,offset:t-l};l=a}l:{for(;e;){if(e.nextSibling){e=e.nextSibling;break l}e=e.parentNode}e=void 0}e=rs(e)}}function ms(l,t){return l&&t?l===t?!0:l&&l.nodeType===3?!1:t&&t.nodeType===3?ms(l,t.parentNode):"contains"in l?l.contains(t):l.compareDocumentPosition?!!(l.compareDocumentPosition(t)&16):!1:!1}function ys(l){l=l!=null&&l.ownerDocument!=null&&l.ownerDocument.defaultView!=null?l.ownerDocument.defaultView:window;for(var t=Qn(l.document);t instanceof l.HTMLIFrameElement;){try{var e=typeof t.contentWindow.location.href=="string"}catch{e=!1}if(e)l=t.contentWindow;else break;t=Qn(l.document)}return t}function Ei(l){var t=l&&l.nodeName&&l.nodeName.toLowerCase();return t&&(t==="input"&&(l.type==="text"||l.type==="search"||l.type==="tel"||l.type==="url"||l.type==="password")||t==="textarea"||l.contentEditable==="true")}var Zh=Vt&&"documentMode"in document&&11>=document.documentMode,sa=null,Ni=null,Fa=null,Ai=!1;function vs(l,t,e){var a=e.window===e?e.document:e.nodeType===9?e:e.ownerDocument;Ai||sa==null||sa!==Qn(a)||(a=sa,"selectionStart"in a&&Ei(a)?a={start:a.selectionStart,end:a.selectionEnd}:(a=(a.ownerDocument&&a.ownerDocument.defaultView||window).getSelection(),a={anchorNode:a.anchorNode,anchorOffset:a.anchorOffset,focusNode:a.focusNode,focusOffset:a.focusOffset}),Fa&&Wa(Fa,a)||(Fa=a,a=qu(Ni,"onSelect"),0>=i,n-=i,Yt=1<<32-vt(t)+n|e<tl?(fl=X,X=null):fl=X.sibling;var ml=g(m,X,y[tl],N);if(ml===null){X===null&&(X=fl);break}l&&X&&ml.alternate===null&&t(m,X),r=u(ml,r,tl),hl===null?L=ml:hl.sibling=ml,hl=ml,X=fl}if(tl===y.length)return e(m,X),ol&&Jt(m,tl),L;if(X===null){for(;tltl?(fl=X,X=null):fl=X.sibling;var Ce=g(m,X,ml.value,N);if(Ce===null){X===null&&(X=fl);break}l&&X&&Ce.alternate===null&&t(m,X),r=u(Ce,r,tl),hl===null?L=Ce:hl.sibling=Ce,hl=Ce,X=fl}if(ml.done)return e(m,X),ol&&Jt(m,tl),L;if(X===null){for(;!ml.done;tl++,ml=y.next())ml=_(m,ml.value,N),ml!==null&&(r=u(ml,r,tl),hl===null?L=ml:hl.sibling=ml,hl=ml);return ol&&Jt(m,tl),L}for(X=a(X);!ml.done;tl++,ml=y.next())ml=p(X,m,tl,ml.value,N),ml!==null&&(l&&ml.alternate!==null&&X.delete(ml.key===null?tl:ml.key),r=u(ml,r,tl),hl===null?L=ml:hl.sibling=ml,hl=ml);return l&&X.forEach(function(fy){return t(m,fy)}),ol&&Jt(m,tl),L}function Tl(m,r,y,N){if(typeof y=="object"&&y!==null&&y.type===vl&&y.key===null&&(y=y.props.children),typeof y=="object"&&y!==null){switch(y.$$typeof){case ll:l:{for(var L=y.key;r!==null;){if(r.key===L){if(L=y.type,L===vl){if(r.tag===7){e(m,r.sibling),N=n(r,y.props.children),N.return=m,m=N;break l}}else if(r.elementType===L||typeof L=="object"&&L!==null&&L.$$typeof===Z&&Ke(L)===r.type){e(m,r.sibling),N=n(r,y.props),an(N,y),N.return=m,m=N;break l}e(m,r);break}else t(m,r);r=r.sibling}y.type===vl?(N=Xe(y.props.children,m.mode,N,y.key),N.return=m,m=N):(N=Pn(y.type,y.key,y.props,null,m.mode,N),an(N,y),N.return=m,m=N)}return i(m);case W:l:{for(L=y.key;r!==null;){if(r.key===L)if(r.tag===4&&r.stateNode.containerInfo===y.containerInfo&&r.stateNode.implementation===y.implementation){e(m,r.sibling),N=n(r,y.children||[]),N.return=m,m=N;break l}else{e(m,r);break}else t(m,r);r=r.sibling}N=Hi(y,m.mode,N),N.return=m,m=N}return i(m);case Z:return y=Ke(y),Tl(m,r,y,N)}if(Cl(y))return G(m,r,y,N);if(jl(y)){if(L=jl(y),typeof L!="function")throw Error(s(150));return y=L.call(y),J(m,r,y,N)}if(typeof y.then=="function")return Tl(m,r,iu(y),N);if(y.$$typeof===dl)return Tl(m,r,eu(m,y),N);cu(m,y)}return typeof y=="string"&&y!==""||typeof y=="number"||typeof y=="bigint"?(y=""+y,r!==null&&r.tag===6?(e(m,r.sibling),N=n(r,y),N.return=m,m=N):(e(m,r),N=Ui(y,m.mode,N),N.return=m,m=N),i(m)):e(m,r)}return function(m,r,y,N){try{en=0;var L=Tl(m,r,y,N);return pa=null,L}catch(X){if(X===ba||X===nu)throw X;var hl=St(29,X,null,m.mode);return hl.lanes=N,hl.return=m,hl}}}var we=Gs(!0),Xs=Gs(!1),ye=!1;function Ji(l){l.updateQueue={baseState:l.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function wi(l,t){l=l.updateQueue,t.updateQueue===l&&(t.updateQueue={baseState:l.baseState,firstBaseUpdate:l.firstBaseUpdate,lastBaseUpdate:l.lastBaseUpdate,shared:l.shared,callbacks:null})}function ve(l){return{lane:l,tag:0,payload:null,callback:null,next:null}}function ge(l,t,e){var a=l.updateQueue;if(a===null)return null;if(a=a.shared,(yl&2)!==0){var n=a.pending;return n===null?t.next=t:(t.next=n.next,n.next=t),a.pending=t,t=In(l),zs(l,null,e),t}return Fn(l,a,t,e),In(l)}function nn(l,t,e){if(t=t.updateQueue,t!==null&&(t=t.shared,(e&4194048)!==0)){var a=t.lanes;a&=l.pendingLanes,e|=a,t.lanes=e,Df(l,e)}}function ki(l,t){var e=l.updateQueue,a=l.alternate;if(a!==null&&(a=a.updateQueue,e===a)){var n=null,u=null;if(e=e.firstBaseUpdate,e!==null){do{var i={lane:e.lane,tag:e.tag,payload:e.payload,callback:null,next:null};u===null?n=u=i:u=u.next=i,e=e.next}while(e!==null);u===null?n=u=t:u=u.next=t}else n=u=t;e={baseState:a.baseState,firstBaseUpdate:n,lastBaseUpdate:u,shared:a.shared,callbacks:a.callbacks},l.updateQueue=e;return}l=e.lastBaseUpdate,l===null?e.firstBaseUpdate=t:l.next=t,e.lastBaseUpdate=t}var $i=!1;function un(){if($i){var l=Sa;if(l!==null)throw l}}function cn(l,t,e,a){$i=!1;var n=l.updateQueue;ye=!1;var u=n.firstBaseUpdate,i=n.lastBaseUpdate,f=n.shared.pending;if(f!==null){n.shared.pending=null;var d=f,v=d.next;d.next=null,i===null?u=v:i.next=v,i=d;var x=l.alternate;x!==null&&(x=x.updateQueue,f=x.lastBaseUpdate,f!==i&&(f===null?x.firstBaseUpdate=v:f.next=v,x.lastBaseUpdate=d))}if(u!==null){var _=n.baseState;i=0,x=v=d=null,f=u;do{var g=f.lane&-536870913,p=g!==f.lane;if(p?(cl&g)===g:(a&g)===g){g!==0&&g===ga&&($i=!0),x!==null&&(x=x.next={lane:0,tag:f.tag,payload:f.payload,callback:null,next:null});l:{var G=l,J=f;g=t;var Tl=e;switch(J.tag){case 1:if(G=J.payload,typeof G=="function"){_=G.call(Tl,_,g);break l}_=G;break l;case 3:G.flags=G.flags&-65537|128;case 0:if(G=J.payload,g=typeof G=="function"?G.call(Tl,_,g):G,g==null)break l;_=E({},_,g);break l;case 2:ye=!0}}g=f.callback,g!==null&&(l.flags|=64,p&&(l.flags|=8192),p=n.callbacks,p===null?n.callbacks=[g]:p.push(g))}else p={lane:g,tag:f.tag,payload:f.payload,callback:f.callback,next:null},x===null?(v=x=p,d=_):x=x.next=p,i|=g;if(f=f.next,f===null){if(f=n.shared.pending,f===null)break;p=f,f=p.next,p.next=null,n.lastBaseUpdate=p,n.shared.pending=null}}while(!0);x===null&&(d=_),n.baseState=d,n.firstBaseUpdate=v,n.lastBaseUpdate=x,u===null&&(n.shared.lanes=0),xe|=i,l.lanes=i,l.memoizedState=_}}function Qs(l,t){if(typeof l!="function")throw Error(s(191,l));l.call(t)}function Zs(l,t){var e=l.callbacks;if(e!==null)for(l.callbacks=null,l=0;lu?u:8;var i=j.T,f={};j.T=f,mc(l,!1,t,e);try{var d=n(),v=j.S;if(v!==null&&v(f,d),d!==null&&typeof d=="object"&&typeof d.then=="function"){var x=Fh(d,a);on(l,t,x,zt(l))}else on(l,t,a,zt(l))}catch(_){on(l,t,{then:function(){},status:"rejected",reason:_},zt())}finally{R.p=u,i!==null&&f.types!==null&&(i.types=f.types),j.T=i}}function am(){}function rc(l,t,e,a){if(l.tag!==5)throw Error(s(476));var n=jo(l).queue;po(l,n,t,M,e===null?am:function(){return xo(l),e(a)})}function jo(l){var t=l.memoizedState;if(t!==null)return t;t={memoizedState:M,baseState:M,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Wt,lastRenderedState:M},next:null};var e={};return t.next={memoizedState:e,baseState:e,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Wt,lastRenderedState:e},next:null},l.memoizedState=t,l=l.alternate,l!==null&&(l.memoizedState=t),t}function xo(l){var t=jo(l);t.next===null&&(t=l.alternate.memoizedState),on(l,t.next.queue,{},zt())}function hc(){return Fl(An)}function zo(){return Bl().memoizedState}function To(){return Bl().memoizedState}function nm(l){for(var t=l.return;t!==null;){switch(t.tag){case 24:case 3:var e=zt();l=ve(e);var a=ge(t,l,e);a!==null&&(ot(a,t,e),nn(a,t,e)),t={cache:Zi()},l.payload=t;return}t=t.return}}function um(l,t,e){var a=zt();e={lane:a,revertLane:0,gesture:null,action:e,hasEagerState:!1,eagerState:null,next:null},gu(l)?No(t,e):(e=Di(l,t,e,a),e!==null&&(ot(e,l,a),Ao(e,t,a)))}function Eo(l,t,e){var a=zt();on(l,t,e,a)}function on(l,t,e,a){var n={lane:a,revertLane:0,gesture:null,action:e,hasEagerState:!1,eagerState:null,next:null};if(gu(l))No(t,n);else{var u=l.alternate;if(l.lanes===0&&(u===null||u.lanes===0)&&(u=t.lastRenderedReducer,u!==null))try{var i=t.lastRenderedState,f=u(i,e);if(n.hasEagerState=!0,n.eagerState=f,gt(f,i))return Fn(l,t,n,0),El===null&&Wn(),!1}catch{}if(e=Di(l,t,n,a),e!==null)return ot(e,l,a),Ao(e,t,a),!0}return!1}function mc(l,t,e,a){if(a={lane:2,revertLane:Jc(),gesture:null,action:a,hasEagerState:!1,eagerState:null,next:null},gu(l)){if(t)throw Error(s(479))}else t=Di(l,e,a,2),t!==null&&ot(t,l,2)}function gu(l){var t=l.alternate;return l===P||t!==null&&t===P}function No(l,t){xa=ou=!0;var e=l.pending;e===null?t.next=t:(t.next=e.next,e.next=t),l.pending=t}function Ao(l,t,e){if((e&4194048)!==0){var a=t.lanes;a&=l.pendingLanes,e|=a,t.lanes=e,Df(l,e)}}var dn={readContext:Fl,use:hu,useCallback:Hl,useContext:Hl,useEffect:Hl,useImperativeHandle:Hl,useLayoutEffect:Hl,useInsertionEffect:Hl,useMemo:Hl,useReducer:Hl,useRef:Hl,useState:Hl,useDebugValue:Hl,useDeferredValue:Hl,useTransition:Hl,useSyncExternalStore:Hl,useId:Hl,useHostTransitionStatus:Hl,useFormState:Hl,useActionState:Hl,useOptimistic:Hl,useMemoCache:Hl,useCacheRefresh:Hl};dn.useEffectEvent=Hl;var _o={readContext:Fl,use:hu,useCallback:function(l,t){return et().memoizedState=[l,t===void 0?null:t],l},useContext:Fl,useEffect:oo,useImperativeHandle:function(l,t,e){e=e!=null?e.concat([l]):null,yu(4194308,4,yo.bind(null,t,l),e)},useLayoutEffect:function(l,t){return yu(4194308,4,l,t)},useInsertionEffect:function(l,t){yu(4,2,l,t)},useMemo:function(l,t){var e=et();t=t===void 0?null:t;var a=l();if(ke){fe(!0);try{l()}finally{fe(!1)}}return e.memoizedState=[a,t],a},useReducer:function(l,t,e){var a=et();if(e!==void 0){var n=e(t);if(ke){fe(!0);try{e(t)}finally{fe(!1)}}}else n=t;return a.memoizedState=a.baseState=n,l={pending:null,lanes:0,dispatch:null,lastRenderedReducer:l,lastRenderedState:n},a.queue=l,l=l.dispatch=um.bind(null,P,l),[a.memoizedState,l]},useRef:function(l){var t=et();return l={current:l},t.memoizedState=l},useState:function(l){l=cc(l);var t=l.queue,e=Eo.bind(null,P,t);return t.dispatch=e,[l.memoizedState,e]},useDebugValue:oc,useDeferredValue:function(l,t){var e=et();return dc(e,l,t)},useTransition:function(){var l=cc(!1);return l=po.bind(null,P,l.queue,!0,!1),et().memoizedState=l,[!1,l]},useSyncExternalStore:function(l,t,e){var a=P,n=et();if(ol){if(e===void 0)throw Error(s(407));e=e()}else{if(e=t(),El===null)throw Error(s(349));(cl&127)!==0||ks(a,t,e)}n.memoizedState=e;var u={value:e,getSnapshot:t};return n.queue=u,oo(Ws.bind(null,a,u,l),[l]),a.flags|=2048,Ta(9,{destroy:void 0},$s.bind(null,a,u,e,t),null),e},useId:function(){var l=et(),t=El.identifierPrefix;if(ol){var e=Gt,a=Yt;e=(a&~(1<<32-vt(a)-1)).toString(32)+e,t="_"+t+"R_"+e,e=du++,0<\/script>",u=u.removeChild(u.firstChild);break;case"select":u=typeof a.is=="string"?i.createElement("select",{is:a.is}):i.createElement("select"),a.multiple?u.multiple=!0:a.size&&(u.size=a.size);break;default:u=typeof a.is=="string"?i.createElement(n,{is:a.is}):i.createElement(n)}}u[$l]=t,u[nt]=a;l:for(i=t.child;i!==null;){if(i.tag===5||i.tag===6)u.appendChild(i.stateNode);else if(i.tag!==4&&i.tag!==27&&i.child!==null){i.child.return=i,i=i.child;continue}if(i===t)break l;for(;i.sibling===null;){if(i.return===null||i.return===t)break l;i=i.return}i.sibling.return=i.return,i=i.sibling}t.stateNode=u;l:switch(Pl(u,n,a),n){case"button":case"input":case"select":case"textarea":a=!!a.autoFocus;break l;case"img":a=!0;break l;default:a=!1}a&&It(t)}}return Ml(t),_c(t,t.type,l===null?null:l.memoizedProps,t.pendingProps,e),null;case 6:if(l&&t.stateNode!=null)l.memoizedProps!==a&&It(t);else{if(typeof a!="string"&&t.stateNode===null)throw Error(s(166));if(l=al.current,ya(t)){if(l=t.stateNode,e=t.memoizedProps,a=null,n=Wl,n!==null)switch(n.tag){case 27:case 5:a=n.memoizedProps}l[$l]=t,l=!!(l.nodeValue===e||a!==null&&a.suppressHydrationWarning===!0||wd(l.nodeValue,e)),l||he(t,!0)}else l=Bu(l).createTextNode(a),l[$l]=t,t.stateNode=l}return Ml(t),null;case 31:if(e=t.memoizedState,l===null||l.memoizedState!==null){if(a=ya(t),e!==null){if(l===null){if(!a)throw Error(s(318));if(l=t.memoizedState,l=l!==null?l.dehydrated:null,!l)throw Error(s(557));l[$l]=t}else Qe(),(t.flags&128)===0&&(t.memoizedState=null),t.flags|=4;Ml(t),l=!1}else e=Yi(),l!==null&&l.memoizedState!==null&&(l.memoizedState.hydrationErrors=e),l=!0;if(!l)return t.flags&256?(pt(t),t):(pt(t),null);if((t.flags&128)!==0)throw Error(s(558))}return Ml(t),null;case 13:if(a=t.memoizedState,l===null||l.memoizedState!==null&&l.memoizedState.dehydrated!==null){if(n=ya(t),a!==null&&a.dehydrated!==null){if(l===null){if(!n)throw Error(s(318));if(n=t.memoizedState,n=n!==null?n.dehydrated:null,!n)throw Error(s(317));n[$l]=t}else Qe(),(t.flags&128)===0&&(t.memoizedState=null),t.flags|=4;Ml(t),n=!1}else n=Yi(),l!==null&&l.memoizedState!==null&&(l.memoizedState.hydrationErrors=n),n=!0;if(!n)return t.flags&256?(pt(t),t):(pt(t),null)}return pt(t),(t.flags&128)!==0?(t.lanes=e,t):(e=a!==null,l=l!==null&&l.memoizedState!==null,e&&(a=t.child,n=null,a.alternate!==null&&a.alternate.memoizedState!==null&&a.alternate.memoizedState.cachePool!==null&&(n=a.alternate.memoizedState.cachePool.pool),u=null,a.memoizedState!==null&&a.memoizedState.cachePool!==null&&(u=a.memoizedState.cachePool.pool),u!==n&&(a.flags|=2048)),e!==l&&e&&(t.child.flags|=8192),xu(t,t.updateQueue),Ml(t),null);case 4:return Ul(),l===null&&Wc(t.stateNode.containerInfo),Ml(t),null;case 10:return kt(t.type),Ml(t),null;case 19:if(A(ql),a=t.memoizedState,a===null)return Ml(t),null;if(n=(t.flags&128)!==0,u=a.rendering,u===null)if(n)hn(a,!1);else{if(Rl!==0||l!==null&&(l.flags&128)!==0)for(l=t.child;l!==null;){if(u=su(l),u!==null){for(t.flags|=128,hn(a,!1),l=u.updateQueue,t.updateQueue=l,xu(t,l),t.subtreeFlags=0,l=e,e=t.child;e!==null;)Ts(e,l),e=e.sibling;return q(ql,ql.current&1|2),ol&&Jt(t,a.treeForkCount),t.child}l=l.sibling}a.tail!==null&&mt()>Au&&(t.flags|=128,n=!0,hn(a,!1),t.lanes=4194304)}else{if(!n)if(l=su(u),l!==null){if(t.flags|=128,n=!0,l=l.updateQueue,t.updateQueue=l,xu(t,l),hn(a,!0),a.tail===null&&a.tailMode==="hidden"&&!u.alternate&&!ol)return Ml(t),null}else 2*mt()-a.renderingStartTime>Au&&e!==536870912&&(t.flags|=128,n=!0,hn(a,!1),t.lanes=4194304);a.isBackwards?(u.sibling=t.child,t.child=u):(l=a.last,l!==null?l.sibling=u:t.child=u,a.last=u)}return a.tail!==null?(l=a.tail,a.rendering=l,a.tail=l.sibling,a.renderingStartTime=mt(),l.sibling=null,e=ql.current,q(ql,n?e&1|2:e&1),ol&&Jt(t,a.treeForkCount),l):(Ml(t),null);case 22:case 23:return pt(t),Fi(),a=t.memoizedState!==null,l!==null?l.memoizedState!==null!==a&&(t.flags|=8192):a&&(t.flags|=8192),a?(e&536870912)!==0&&(t.flags&128)===0&&(Ml(t),t.subtreeFlags&6&&(t.flags|=8192)):Ml(t),e=t.updateQueue,e!==null&&xu(t,e.retryQueue),e=null,l!==null&&l.memoizedState!==null&&l.memoizedState.cachePool!==null&&(e=l.memoizedState.cachePool.pool),a=null,t.memoizedState!==null&&t.memoizedState.cachePool!==null&&(a=t.memoizedState.cachePool.pool),a!==e&&(t.flags|=2048),l!==null&&A(Ve),null;case 24:return e=null,l!==null&&(e=l.memoizedState.cache),t.memoizedState.cache!==e&&(t.flags|=2048),kt(Gl),Ml(t),null;case 25:return null;case 30:return null}throw Error(s(156,t.tag))}function om(l,t){switch(qi(t),t.tag){case 1:return l=t.flags,l&65536?(t.flags=l&-65537|128,t):null;case 3:return kt(Gl),Ul(),l=t.flags,(l&65536)!==0&&(l&128)===0?(t.flags=l&-65537|128,t):null;case 26:case 27:case 5:return Jl(t),null;case 31:if(t.memoizedState!==null){if(pt(t),t.alternate===null)throw Error(s(340));Qe()}return l=t.flags,l&65536?(t.flags=l&-65537|128,t):null;case 13:if(pt(t),l=t.memoizedState,l!==null&&l.dehydrated!==null){if(t.alternate===null)throw Error(s(340));Qe()}return l=t.flags,l&65536?(t.flags=l&-65537|128,t):null;case 19:return A(ql),null;case 4:return Ul(),null;case 10:return kt(t.type),null;case 22:case 23:return pt(t),Fi(),l!==null&&A(Ve),l=t.flags,l&65536?(t.flags=l&-65537|128,t):null;case 24:return kt(Gl),null;case 25:return null;default:return null}}function Io(l,t){switch(qi(t),t.tag){case 3:kt(Gl),Ul();break;case 26:case 27:case 5:Jl(t);break;case 4:Ul();break;case 31:t.memoizedState!==null&&pt(t);break;case 13:pt(t);break;case 19:A(ql);break;case 10:kt(t.type);break;case 22:case 23:pt(t),Fi(),l!==null&&A(Ve);break;case 24:kt(Gl)}}function mn(l,t){try{var e=t.updateQueue,a=e!==null?e.lastEffect:null;if(a!==null){var n=a.next;e=n;do{if((e.tag&l)===l){a=void 0;var u=e.create,i=e.inst;a=u(),i.destroy=a}e=e.next}while(e!==n)}}catch(f){pl(t,t.return,f)}}function pe(l,t,e){try{var a=t.updateQueue,n=a!==null?a.lastEffect:null;if(n!==null){var u=n.next;a=u;do{if((a.tag&l)===l){var i=a.inst,f=i.destroy;if(f!==void 0){i.destroy=void 0,n=t;var d=e,v=f;try{v()}catch(x){pl(n,d,x)}}}a=a.next}while(a!==u)}}catch(x){pl(t,t.return,x)}}function Po(l){var t=l.updateQueue;if(t!==null){var e=l.stateNode;try{Zs(t,e)}catch(a){pl(l,l.return,a)}}}function ld(l,t,e){e.props=$e(l.type,l.memoizedProps),e.state=l.memoizedState;try{e.componentWillUnmount()}catch(a){pl(l,t,a)}}function yn(l,t){try{var e=l.ref;if(e!==null){switch(l.tag){case 26:case 27:case 5:var a=l.stateNode;break;case 30:a=l.stateNode;break;default:a=l.stateNode}typeof e=="function"?l.refCleanup=e(a):e.current=a}}catch(n){pl(l,t,n)}}function Xt(l,t){var e=l.ref,a=l.refCleanup;if(e!==null)if(typeof a=="function")try{a()}catch(n){pl(l,t,n)}finally{l.refCleanup=null,l=l.alternate,l!=null&&(l.refCleanup=null)}else if(typeof e=="function")try{e(null)}catch(n){pl(l,t,n)}else e.current=null}function td(l){var t=l.type,e=l.memoizedProps,a=l.stateNode;try{l:switch(t){case"button":case"input":case"select":case"textarea":e.autoFocus&&a.focus();break l;case"img":e.src?a.src=e.src:e.srcSet&&(a.srcset=e.srcSet)}}catch(n){pl(l,l.return,n)}}function Oc(l,t,e){try{var a=l.stateNode;Cm(a,l.type,e,t),a[nt]=t}catch(n){pl(l,l.return,n)}}function ed(l){return l.tag===5||l.tag===3||l.tag===26||l.tag===27&&Ae(l.type)||l.tag===4}function Mc(l){l:for(;;){for(;l.sibling===null;){if(l.return===null||ed(l.return))return null;l=l.return}for(l.sibling.return=l.return,l=l.sibling;l.tag!==5&&l.tag!==6&&l.tag!==18;){if(l.tag===27&&Ae(l.type)||l.flags&2||l.child===null||l.tag===4)continue l;l.child.return=l,l=l.child}if(!(l.flags&2))return l.stateNode}}function Dc(l,t,e){var a=l.tag;if(a===5||a===6)l=l.stateNode,t?(e.nodeType===9?e.body:e.nodeName==="HTML"?e.ownerDocument.body:e).insertBefore(l,t):(t=e.nodeType===9?e.body:e.nodeName==="HTML"?e.ownerDocument.body:e,t.appendChild(l),e=e._reactRootContainer,e!=null||t.onclick!==null||(t.onclick=Lt));else if(a!==4&&(a===27&&Ae(l.type)&&(e=l.stateNode,t=null),l=l.child,l!==null))for(Dc(l,t,e),l=l.sibling;l!==null;)Dc(l,t,e),l=l.sibling}function zu(l,t,e){var a=l.tag;if(a===5||a===6)l=l.stateNode,t?e.insertBefore(l,t):e.appendChild(l);else if(a!==4&&(a===27&&Ae(l.type)&&(e=l.stateNode),l=l.child,l!==null))for(zu(l,t,e),l=l.sibling;l!==null;)zu(l,t,e),l=l.sibling}function ad(l){var t=l.stateNode,e=l.memoizedProps;try{for(var a=l.type,n=t.attributes;n.length;)t.removeAttributeNode(n[0]);Pl(t,a,e),t[$l]=l,t[nt]=e}catch(u){pl(l,l.return,u)}}var Pt=!1,Zl=!1,Cc=!1,nd=typeof WeakSet=="function"?WeakSet:Set,kl=null;function dm(l,t){if(l=l.containerInfo,Pc=Vu,l=ys(l),Ei(l)){if("selectionStart"in l)var e={start:l.selectionStart,end:l.selectionEnd};else l:{e=(e=l.ownerDocument)&&e.defaultView||window;var a=e.getSelection&&e.getSelection();if(a&&a.rangeCount!==0){e=a.anchorNode;var n=a.anchorOffset,u=a.focusNode;a=a.focusOffset;try{e.nodeType,u.nodeType}catch{e=null;break l}var i=0,f=-1,d=-1,v=0,x=0,_=l,g=null;t:for(;;){for(var p;_!==e||n!==0&&_.nodeType!==3||(f=i+n),_!==u||a!==0&&_.nodeType!==3||(d=i+a),_.nodeType===3&&(i+=_.nodeValue.length),(p=_.firstChild)!==null;)g=_,_=p;for(;;){if(_===l)break t;if(g===e&&++v===n&&(f=i),g===u&&++x===a&&(d=i),(p=_.nextSibling)!==null)break;_=g,g=_.parentNode}_=p}e=f===-1||d===-1?null:{start:f,end:d}}else e=null}e=e||{start:0,end:0}}else e=null;for(lf={focusedElem:l,selectionRange:e},Vu=!1,kl=t;kl!==null;)if(t=kl,l=t.child,(t.subtreeFlags&1028)!==0&&l!==null)l.return=t,kl=l;else for(;kl!==null;){switch(t=kl,u=t.alternate,l=t.flags,t.tag){case 0:if((l&4)!==0&&(l=t.updateQueue,l=l!==null?l.events:null,l!==null))for(e=0;e title"))),Pl(u,a,e),u[$l]=l,wl(u),a=u;break l;case"link":var i=or("link","href",n).get(a+(e.href||""));if(i){for(var f=0;fTl&&(i=Tl,Tl=J,J=i);var m=hs(f,J),r=hs(f,Tl);if(m&&r&&(p.rangeCount!==1||p.anchorNode!==m.node||p.anchorOffset!==m.offset||p.focusNode!==r.node||p.focusOffset!==r.offset)){var y=_.createRange();y.setStart(m.node,m.offset),p.removeAllRanges(),J>Tl?(p.addRange(y),p.extend(r.node,r.offset)):(y.setEnd(r.node,r.offset),p.addRange(y))}}}}for(_=[],p=f;p=p.parentNode;)p.nodeType===1&&_.push({element:p,left:p.scrollLeft,top:p.scrollTop});for(typeof f.focus=="function"&&f.focus(),f=0;f<_.length;f++){var N=_[f];N.element.scrollLeft=N.left,N.element.scrollTop=N.top}}Vu=!!Pc,lf=Pc=null}finally{yl=n,R.p=a,j.T=e}}l.current=t,Vl=2}}function Dd(){if(Vl===2){Vl=0;var l=Te,t=Oa,e=(t.flags&8772)!==0;if((t.subtreeFlags&8772)!==0||e){e=j.T,j.T=null;var a=R.p;R.p=2;var n=yl;yl|=4;try{ud(l,t.alternate,t)}finally{yl=n,R.p=a,j.T=e}}Vl=3}}function Cd(){if(Vl===4||Vl===3){Vl=0,Zr();var l=Te,t=Oa,e=ne,a=Sd;(t.subtreeFlags&10256)!==0||(t.flags&10256)!==0?Vl=5:(Vl=0,Oa=Te=null,Ud(l,l.pendingLanes));var n=l.pendingLanes;if(n===0&&(ze=null),ii(e),t=t.stateNode,yt&&typeof yt.onCommitFiberRoot=="function")try{yt.onCommitFiberRoot(Ya,t,void 0,(t.current.flags&128)===128)}catch{}if(a!==null){t=j.T,n=R.p,R.p=2,j.T=null;try{for(var u=l.onRecoverableError,i=0;ie?32:e,j.T=null,e=Gc,Gc=null;var u=Te,i=ne;if(Vl=0,Oa=Te=null,ne=0,(yl&6)!==0)throw Error(s(331));var f=yl;if(yl|=4,yd(u.current),rd(u,u.current,i,e),yl=f,jn(0,!1),yt&&typeof yt.onPostCommitFiberRoot=="function")try{yt.onPostCommitFiberRoot(Ya,u)}catch{}return!0}finally{R.p=n,j.T=a,Ud(l,t)}}function Rd(l,t,e){t=At(e,t),t=Sc(l.stateNode,t,2),l=ge(l,t,2),l!==null&&(Xa(l,2),Qt(l))}function pl(l,t,e){if(l.tag===3)Rd(l,l,e);else for(;t!==null;){if(t.tag===3){Rd(t,l,e);break}else if(t.tag===1){var a=t.stateNode;if(typeof t.type.getDerivedStateFromError=="function"||typeof a.componentDidCatch=="function"&&(ze===null||!ze.has(a))){l=At(e,l),e=qo(2),a=ge(t,e,2),a!==null&&(Bo(e,a,t,l),Xa(a,2),Qt(a));break}}t=t.return}}function Lc(l,t,e){var a=l.pingCache;if(a===null){a=l.pingCache=new mm;var n=new Set;a.set(t,n)}else n=a.get(t),n===void 0&&(n=new Set,a.set(t,n));n.has(e)||(Rc=!0,n.add(e),l=bm.bind(null,l,t,e),t.then(l,l))}function bm(l,t,e){var a=l.pingCache;a!==null&&a.delete(t),l.pingedLanes|=l.suspendedLanes&e,l.warmLanes&=~e,El===l&&(cl&e)===e&&(Rl===4||Rl===3&&(cl&62914560)===cl&&300>mt()-Nu?(yl&2)===0&&Ma(l,0):qc|=e,_a===cl&&(_a=0)),Qt(l)}function qd(l,t){t===0&&(t=Of()),l=Ge(l,t),l!==null&&(Xa(l,t),Qt(l))}function pm(l){var t=l.memoizedState,e=0;t!==null&&(e=t.retryLane),qd(l,e)}function jm(l,t){var e=0;switch(l.tag){case 31:case 13:var a=l.stateNode,n=l.memoizedState;n!==null&&(e=n.retryLane);break;case 19:a=l.stateNode;break;case 22:a=l.stateNode._retryCache;break;default:throw Error(s(314))}a!==null&&a.delete(t),qd(l,e)}function xm(l,t){return ei(l,t)}var Uu=null,Ca=null,Vc=!1,Hu=!1,Kc=!1,Ne=0;function Qt(l){l!==Ca&&l.next===null&&(Ca===null?Uu=Ca=l:Ca=Ca.next=l),Hu=!0,Vc||(Vc=!0,Tm())}function jn(l,t){if(!Kc&&Hu){Kc=!0;do for(var e=!1,a=Uu;a!==null;){if(l!==0){var n=a.pendingLanes;if(n===0)var u=0;else{var i=a.suspendedLanes,f=a.pingedLanes;u=(1<<31-vt(42|l)+1)-1,u&=n&~(i&~f),u=u&201326741?u&201326741|1:u?u|2:0}u!==0&&(e=!0,Xd(a,u))}else u=cl,u=Yn(a,a===El?u:0,a.cancelPendingCommit!==null||a.timeoutHandle!==-1),(u&3)===0||Ga(a,u)||(e=!0,Xd(a,u));a=a.next}while(e);Kc=!1}}function zm(){Bd()}function Bd(){Hu=Vc=!1;var l=0;Ne!==0&&Hm()&&(l=Ne);for(var t=mt(),e=null,a=Uu;a!==null;){var n=a.next,u=Yd(a,t);u===0?(a.next=null,e===null?Uu=n:e.next=n,n===null&&(Ca=e)):(e=a,(l!==0||(u&3)!==0)&&(Hu=!0)),a=n}Vl!==0&&Vl!==5||jn(l),Ne!==0&&(Ne=0)}function Yd(l,t){for(var e=l.suspendedLanes,a=l.pingedLanes,n=l.expirationTimes,u=l.pendingLanes&-62914561;0f)break;var x=d.transferSize,_=d.initiatorType;x&&kd(_)&&(d=d.responseEnd,i+=x*(d"u"?null:document;function ir(l,t,e){var a=Ua;if(a&&typeof t=="string"&&t){var n=Et(t);n='link[rel="'+l+'"][href="'+n+'"]',typeof e=="string"&&(n+='[crossorigin="'+e+'"]'),ur.has(n)||(ur.add(n),l={rel:l,crossOrigin:e,href:t},a.querySelector(n)===null&&(t=a.createElement("link"),Pl(t,"link",l),wl(t),a.head.appendChild(t)))}}function Lm(l){ue.D(l),ir("dns-prefetch",l,null)}function Vm(l,t){ue.C(l,t),ir("preconnect",l,t)}function Km(l,t,e){ue.L(l,t,e);var a=Ua;if(a&&l&&t){var n='link[rel="preload"][as="'+Et(t)+'"]';t==="image"&&e&&e.imageSrcSet?(n+='[imagesrcset="'+Et(e.imageSrcSet)+'"]',typeof e.imageSizes=="string"&&(n+='[imagesizes="'+Et(e.imageSizes)+'"]')):n+='[href="'+Et(l)+'"]';var u=n;switch(t){case"style":u=Ha(l);break;case"script":u=Ra(l)}Ut.has(u)||(l=E({rel:"preload",href:t==="image"&&e&&e.imageSrcSet?void 0:l,as:t},e),Ut.set(u,l),a.querySelector(n)!==null||t==="style"&&a.querySelector(En(u))||t==="script"&&a.querySelector(Nn(u))||(t=a.createElement("link"),Pl(t,"link",l),wl(t),a.head.appendChild(t)))}}function Jm(l,t){ue.m(l,t);var e=Ua;if(e&&l){var a=t&&typeof t.as=="string"?t.as:"script",n='link[rel="modulepreload"][as="'+Et(a)+'"][href="'+Et(l)+'"]',u=n;switch(a){case"audioworklet":case"paintworklet":case"serviceworker":case"sharedworker":case"worker":case"script":u=Ra(l)}if(!Ut.has(u)&&(l=E({rel:"modulepreload",href:l},t),Ut.set(u,l),e.querySelector(n)===null)){switch(a){case"audioworklet":case"paintworklet":case"serviceworker":case"sharedworker":case"worker":case"script":if(e.querySelector(Nn(u)))return}a=e.createElement("link"),Pl(a,"link",l),wl(a),e.head.appendChild(a)}}}function wm(l,t,e){ue.S(l,t,e);var a=Ua;if(a&&l){var n=ea(a).hoistableStyles,u=Ha(l);t=t||"default";var i=n.get(u);if(!i){var f={loading:0,preload:null};if(i=a.querySelector(En(u)))f.loading=5;else{l=E({rel:"stylesheet",href:l,"data-precedence":t},e),(e=Ut.get(u))&&ff(l,e);var d=i=a.createElement("link");wl(d),Pl(d,"link",l),d._p=new Promise(function(v,x){d.onload=v,d.onerror=x}),d.addEventListener("load",function(){f.loading|=1}),d.addEventListener("error",function(){f.loading|=2}),f.loading|=4,Gu(i,t,a)}i={type:"stylesheet",instance:i,count:1,state:f},n.set(u,i)}}}function km(l,t){ue.X(l,t);var e=Ua;if(e&&l){var a=ea(e).hoistableScripts,n=Ra(l),u=a.get(n);u||(u=e.querySelector(Nn(n)),u||(l=E({src:l,async:!0},t),(t=Ut.get(n))&&sf(l,t),u=e.createElement("script"),wl(u),Pl(u,"link",l),e.head.appendChild(u)),u={type:"script",instance:u,count:1,state:null},a.set(n,u))}}function $m(l,t){ue.M(l,t);var e=Ua;if(e&&l){var a=ea(e).hoistableScripts,n=Ra(l),u=a.get(n);u||(u=e.querySelector(Nn(n)),u||(l=E({src:l,async:!0,type:"module"},t),(t=Ut.get(n))&&sf(l,t),u=e.createElement("script"),wl(u),Pl(u,"link",l),e.head.appendChild(u)),u={type:"script",instance:u,count:1,state:null},a.set(n,u))}}function cr(l,t,e,a){var n=(n=al.current)?Yu(n):null;if(!n)throw Error(s(446));switch(l){case"meta":case"title":return null;case"style":return typeof e.precedence=="string"&&typeof e.href=="string"?(t=Ha(e.href),e=ea(n).hoistableStyles,a=e.get(t),a||(a={type:"style",instance:null,count:0,state:null},e.set(t,a)),a):{type:"void",instance:null,count:0,state:null};case"link":if(e.rel==="stylesheet"&&typeof e.href=="string"&&typeof e.precedence=="string"){l=Ha(e.href);var u=ea(n).hoistableStyles,i=u.get(l);if(i||(n=n.ownerDocument||n,i={type:"stylesheet",instance:null,count:0,state:{loading:0,preload:null}},u.set(l,i),(u=n.querySelector(En(l)))&&!u._p&&(i.instance=u,i.state.loading=5),Ut.has(l)||(e={rel:"preload",as:"style",href:e.href,crossOrigin:e.crossOrigin,integrity:e.integrity,media:e.media,hrefLang:e.hrefLang,referrerPolicy:e.referrerPolicy},Ut.set(l,e),u||Wm(n,l,e,i.state))),t&&a===null)throw Error(s(528,""));return i}if(t&&a!==null)throw Error(s(529,""));return null;case"script":return t=e.async,e=e.src,typeof e=="string"&&t&&typeof t!="function"&&typeof t!="symbol"?(t=Ra(e),e=ea(n).hoistableScripts,a=e.get(t),a||(a={type:"script",instance:null,count:0,state:null},e.set(t,a)),a):{type:"void",instance:null,count:0,state:null};default:throw Error(s(444,l))}}function Ha(l){return'href="'+Et(l)+'"'}function En(l){return'link[rel="stylesheet"]['+l+"]"}function fr(l){return E({},l,{"data-precedence":l.precedence,precedence:null})}function Wm(l,t,e,a){l.querySelector('link[rel="preload"][as="style"]['+t+"]")?a.loading=1:(t=l.createElement("link"),a.preload=t,t.addEventListener("load",function(){return a.loading|=1}),t.addEventListener("error",function(){return a.loading|=2}),Pl(t,"link",e),wl(t),l.head.appendChild(t))}function Ra(l){return'[src="'+Et(l)+'"]'}function Nn(l){return"script[async]"+l}function sr(l,t,e){if(t.count++,t.instance===null)switch(t.type){case"style":var a=l.querySelector('style[data-href~="'+Et(e.href)+'"]');if(a)return t.instance=a,wl(a),a;var n=E({},e,{"data-href":e.href,"data-precedence":e.precedence,href:null,precedence:null});return a=(l.ownerDocument||l).createElement("style"),wl(a),Pl(a,"style",n),Gu(a,e.precedence,l),t.instance=a;case"stylesheet":n=Ha(e.href);var u=l.querySelector(En(n));if(u)return t.state.loading|=4,t.instance=u,wl(u),u;a=fr(e),(n=Ut.get(n))&&ff(a,n),u=(l.ownerDocument||l).createElement("link"),wl(u);var i=u;return i._p=new Promise(function(f,d){i.onload=f,i.onerror=d}),Pl(u,"link",a),t.state.loading|=4,Gu(u,e.precedence,l),t.instance=u;case"script":return u=Ra(e.src),(n=l.querySelector(Nn(u)))?(t.instance=n,wl(n),n):(a=e,(n=Ut.get(u))&&(a=E({},e),sf(a,n)),l=l.ownerDocument||l,n=l.createElement("script"),wl(n),Pl(n,"link",a),l.head.appendChild(n),t.instance=n);case"void":return null;default:throw Error(s(443,t.type))}else t.type==="stylesheet"&&(t.state.loading&4)===0&&(a=t.instance,t.state.loading|=4,Gu(a,e.precedence,l));return t.instance}function Gu(l,t,e){for(var a=e.querySelectorAll('link[rel="stylesheet"][data-precedence],style[data-precedence]'),n=a.length?a[a.length-1]:null,u=n,i=0;i title"):null)}function Fm(l,t,e){if(e===1||t.itemProp!=null)return!1;switch(l){case"meta":case"title":return!0;case"style":if(typeof t.precedence!="string"||typeof t.href!="string"||t.href==="")break;return!0;case"link":if(typeof t.rel!="string"||typeof t.href!="string"||t.href===""||t.onLoad||t.onError)break;return t.rel==="stylesheet"?(l=t.disabled,typeof t.precedence=="string"&&l==null):!0;case"script":if(t.async&&typeof t.async!="function"&&typeof t.async!="symbol"&&!t.onLoad&&!t.onError&&t.src&&typeof t.src=="string")return!0}return!1}function rr(l){return!(l.type==="stylesheet"&&(l.state.loading&3)===0)}function Im(l,t,e,a){if(e.type==="stylesheet"&&(typeof a.media!="string"||matchMedia(a.media).matches!==!1)&&(e.state.loading&4)===0){if(e.instance===null){var n=Ha(a.href),u=t.querySelector(En(n));if(u){t=u._p,t!==null&&typeof t=="object"&&typeof t.then=="function"&&(l.count++,l=Qu.bind(l),t.then(l,l)),e.state.loading|=4,e.instance=u,wl(u);return}u=t.ownerDocument||t,a=fr(a),(n=Ut.get(n))&&ff(a,n),u=u.createElement("link"),wl(u);var i=u;i._p=new Promise(function(f,d){i.onload=f,i.onerror=d}),Pl(u,"link",a),e.instance=u}l.stylesheets===null&&(l.stylesheets=new Map),l.stylesheets.set(e,t),(t=e.state.preload)&&(e.state.loading&3)===0&&(l.count++,e=Qu.bind(l),t.addEventListener("load",e),t.addEventListener("error",e))}}var of=0;function Pm(l,t){return l.stylesheets&&l.count===0&&Lu(l,l.stylesheets),0of?50:800)+t);return l.unsuspend=e,function(){l.unsuspend=null,clearTimeout(a),clearTimeout(n)}}:null}function Qu(){if(this.count--,this.count===0&&(this.imgCount===0||!this.waitingForImages)){if(this.stylesheets)Lu(this,this.stylesheets);else if(this.unsuspend){var l=this.unsuspend;this.unsuspend=null,l()}}}var Zu=null;function Lu(l,t){l.stylesheets=null,l.unsuspend!==null&&(l.count++,Zu=new Map,t.forEach(ly,l),Zu=null,Qu.call(l))}function ly(l,t){if(!(t.state.loading&4)){var e=Zu.get(l);if(e)var a=e.get(null);else{e=new Map,Zu.set(l,e);for(var n=l.querySelectorAll("link[data-precedence],style[data-precedence]"),u=0;u"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(o)}catch(S){console.error(S)}}return o(),bf.exports=vy(),bf.exports}var Sy=gy();function by(){const o=new URLSearchParams(location.search).get("token");if(o){sessionStorage.setItem("dm.token",o);const S=location.pathname+location.hash;return history.replaceState(null,"",S),o}return sessionStorage.getItem("dm.token")??""}const Ba=by();class Fu extends Error{status;body;constructor(S,z,s={}){super(z),this.status=S,this.body=s}get needsTrust(){return this.body.needsTrust===!0}}async function dt(o,S){const z={...S?.headers};Ba&&(z["X-Auth-Token"]=Ba),S?.body&&(z["Content-Type"]="application/json");const s=await fetch(o,{...S,headers:z});if(s.status===204)return;const O=await s.text();let D={};if(O)try{D=JSON.parse(O)}catch{if(!s.ok)throw new Fu(s.status,O.slice(0,400))}if(!s.ok){const T=typeof D.error=="string"?D.error:`request failed (${s.status})`;throw new Fu(s.status,T,D)}return D}const Ht=(o,S)=>dt(o,{method:"POST",body:S===void 0?void 0:JSON.stringify(S)}),Al={health:()=>dt("/api/health"),source:()=>dt("/api/source"),volumeSizes:()=>dt("/api/source/sizes"),sources:()=>dt("/api/sources"),saveSource:o=>Ht("/api/sources",o),deleteSource:o=>dt(`/api/sources/${o}`,{method:"DELETE"}),selectSource:o=>Ht(`/api/sources/${o}/select`),probeSource:o=>Ht(`/api/sources/${o}/probe`),trustSource:(o,S)=>Ht(`/api/sources/${o}/trust`,{fingerprint:S}),connections:()=>dt("/api/connections"),saveConnection:o=>Ht("/api/connections",o),deleteConnection:o=>dt(`/api/connections/${o}`,{method:"DELETE"}),probe:o=>Ht(`/api/connections/${o}/probe`),trust:(o,S)=>Ht(`/api/connections/${o}/trust`,{fingerprint:S}),testConnection:o=>Ht(`/api/connections/${o}/test`),targetInventory:o=>dt(`/api/connections/${o}/inventory`),preview:o=>Ht("/api/plan/preview",o),migrateSSH:(o,S)=>Ht("/api/migrate/ssh",{connectionId:o,plan:S}),buildPackage:(o,S)=>Ht("/api/migrate/package",{plan:o,format:S}),jobs:()=>dt("/api/jobs"),job:o=>dt(`/api/jobs/${o}`),cancelJob:o=>Ht(`/api/jobs/${o}/cancel`),deleteJob:o=>dt(`/api/jobs/${o}`,{method:"DELETE"}),packages:()=>dt("/api/packages"),deletePackage:o=>dt(`/api/packages/${encodeURIComponent(o)}`,{method:"DELETE"}),downloadUrl:o=>`/api/packages/${encodeURIComponent(o)}/download`+(Ba?`?token=${encodeURIComponent(Ba)}`:""),jobEvents:o=>new EventSource(`/api/jobs/${o}/events`+(Ba?`?token=${encodeURIComponent(Ba)}`:""))};function ie(o){if(o==null||o<0)return"–";if(o===0)return"0 B";const S=["B","KiB","MiB","GiB","TiB","PiB"];let z=o,s=0;for(;z>=1024&&sS(D.target.checked)}),c.jsx("span",{children:z})]})}function Ll({label:o,children:S}){return c.jsxs("label",{className:"field",children:[c.jsx("span",{children:o}),S]})}function Iu({title:o,onClose:S,children:z,footer:s,wide:O}){return Q.useEffect(()=>{const D=T=>{T.key==="Escape"&&S()};return window.addEventListener("keydown",D),()=>window.removeEventListener("keydown",D)},[S]),c.jsx("div",{className:"modal-backdrop",onMouseDown:D=>D.target===D.currentTarget&&S(),children:c.jsxs("div",{className:"modal",style:O?{width:"min(1000px, 100%)"}:void 0,children:[c.jsxs("header",{children:[o,c.jsx("span",{className:"spacer"}),c.jsx("button",{className:"btn ghost tiny",onClick:S,children:"close"})]}),c.jsx("div",{className:"content",children:z}),s&&c.jsx("footer",{children:s})]})})}function ht({kind:o,children:S}){return c.jsx("div",{className:`notice ${o==="info"?"":o}`,children:S})}function zf({done:o,total:S,state:z}){const s=S>0?Math.min(100,o/S*100):z==="succeeded"?100:0,O=z==="succeeded"?"done":z==="failed"?"failed":"";return c.jsx("div",{className:`progress ${O}`,children:c.jsx("div",{style:{width:`${s}%`}})})}function Br(o,S){return o.kind==="tmpfs"?0:o.sizeBytes>=0?o.sizeBytes:o.name&&S[o.name]!==void 0?S[o.name]:-1}function py({source:o,sel:S,setSel:z,targetInv:s,loading:O,sizes:D}){const[T,w]=Q.useState(""),[H,b]=Q.useState(new Set),[V,E]=Q.useState(!1),U=o?.inventory.containers??[],ll=Q.useMemo(()=>new Set((s?.containers??[]).map(C=>C.name)),[s]),W=Q.useMemo(()=>{const C=T.trim().toLowerCase();return U.filter(Z=>V&&Z.state!=="running"?!1:C?Z.name.toLowerCase().includes(C)||Z.image.toLowerCase().includes(C)||(Z.composeProject??"").toLowerCase().includes(C)||(Z.mounts??[]).some(B=>B.destination.toLowerCase().includes(C)||(B.name??"").toLowerCase().includes(C)):!0)},[U,T,V]),vl=Q.useMemo(()=>{const C=new Map;for(const Z of W){const B=Z.composeProject||"",$=C.get(B);$?$.push(Z):C.set(B,[Z])}return[...C.entries()].sort((Z,B)=>Z[0]===""?1:B[0]===""?-1:Z[0].localeCompare(B[0]))},[W]);function Yl(C,Z){z(B=>({...B,[C]:{...B[C],...Z}}))}function gl(C,Z){z(B=>{const $={...B};for(const sl of C)$[sl]&&($[sl]={...$[sl],include:Z});return $})}function _l(C){z(Z=>{const B={...Z};for(const $ of U){const sl=B[$.id];sl?.include&&(B[$.id]=C(sl,$))}return B})}function dl(C,Z){_l((B,$)=>{const sl={...B.mounts};for(const jl of $.mounts??[])jl.kind!=="tmpfs"&&Z.includes(jl.kind)&&(sl[jl.destination]={...sl[jl.destination],action:C});return{...B,mounts:sl}})}const el=W.map(C=>C.id),Sl=W.filter(C=>S[C.id]?.include).length,Nl=Object.values(S).some(C=>C.include);return c.jsxs(c.Fragment,{children:[c.jsxs("div",{className:"toolbar",children:[c.jsx("input",{className:"search",type:"text",placeholder:"filter by name, image, mount…",value:T,onChange:C=>w(C.target.value)}),c.jsx("button",{className:"btn tiny",onClick:()=>gl(el,!0),children:"select all"}),c.jsx("button",{className:"btn tiny",onClick:()=>gl(el,!1),children:"clear"}),c.jsx("button",{className:"btn tiny",onClick:()=>gl(W.filter(C=>C.state==="running").map(C=>C.id),!0),children:"select running"}),c.jsx(rt,{checked:V,onChange:E,label:c.jsx("span",{className:"small muted",children:"running only"})}),c.jsx("span",{className:"spacer"}),c.jsxs("span",{className:"small faint nowrap",children:["apply to ",Sl?`${Sl} selected`:"selection",":"]}),c.jsx("button",{className:"btn tiny",disabled:!Nl,onClick:()=>dl("copy",["volume","anonymous","bind"]),children:"copy all data"}),c.jsx("button",{className:"btn tiny",disabled:!Nl,onClick:()=>dl("skip",["bind"]),children:"skip binds"}),c.jsx("button",{className:"btn tiny",disabled:!Nl,onClick:()=>dl("structure",["volume","anonymous","bind"]),children:"structure only"}),c.jsxs("select",{className:"btn tiny",style:{width:"auto"},disabled:!Nl,value:"",onChange:C=>{const Z=C.target.value;if(Z){if(Z==="start"&&_l(B=>({...B,startAfter:!0})),Z==="nostart"&&_l(B=>({...B,startAfter:!1})),Z==="live"&&_l(B=>({...B,stopSourceDuringCopy:!1})),Z==="quiesce"&&_l(B=>({...B,stopSourceDuringCopy:!0})),Z==="keepsource"&&_l(B=>({...B,stopSourceAfter:!1})),Z==="stopsource"&&_l(B=>({...B,stopSourceAfter:!0})),Z.startsWith("img:")){const B=Z.slice(4);_l($=>({...$,migrateImage:B!=="skip",imageMode:B}))}C.target.value=""}},children:[c.jsx("option",{value:"",children:"more…"}),c.jsx("option",{value:"start",children:"start after migration"}),c.jsx("option",{value:"nostart",children:"leave stopped on target"}),c.jsx("option",{value:"quiesce",children:"stop source while copying"}),c.jsx("option",{value:"live",children:"copy while running (hot)"}),c.jsx("option",{value:"stopsource",children:"stop source after migration"}),c.jsx("option",{value:"keepsource",children:"leave source running"}),c.jsx("option",{value:"img:auto",children:"image: auto"}),c.jsx("option",{value:"img:pull",children:"image: pull on target"}),c.jsx("option",{value:"img:stream",children:"image: transfer layers"}),c.jsx("option",{value:"img:skip",children:"image: already on target"})]})]}),O&&U.length===0&&c.jsx("div",{className:"empty",children:"reading the source daemon…"}),!O&&U.length===0&&c.jsx("div",{className:"empty",children:"no containers on this host"}),!O&&U.length>0&&W.length===0&&c.jsx("div",{className:"empty",children:"nothing matches the filter"}),c.jsx("div",{className:"clist",children:vl.map(([C,Z])=>c.jsxs("div",{children:[vl.length>1&&c.jsxs("div",{className:"group-head",children:[c.jsx(rt,{checked:Z.every(B=>S[B.id]?.include),onChange:B=>gl(Z.map($=>$.id),B),label:C?`compose: ${C}`:"standalone"}),c.jsx("span",{className:"line"}),c.jsx("span",{children:Z.length})]}),Z.map(B=>c.jsx(jy,{c:B,s:S[B.id],onChange:$=>Yl(B.id,$),expanded:H.has(B.id),toggleExpanded:()=>b($=>{const sl=new Set($);return sl.has(B.id)?sl.delete(B.id):sl.add(B.id),sl}),conflicts:ll.has(S[B.id]?.nameOverride||B.name),sizes:D},B.id))]},C||"__none"))})]})}function jy({c:o,s:S,onChange:z,expanded:s,toggleExpanded:O,conflicts:D,sizes:T}){if(!S)return null;const H=(o.mounts??[]).filter(E=>E.kind!=="tmpfs"),b=H.filter(E=>(S.mounts[E.destination]?.action??"copy")==="copy"),V=b.reduce((E,U)=>{const ll=Br(U,T);return E+(ll>0?ll:0)},0);return c.jsxs(c.Fragment,{children:[c.jsxs("div",{className:`crow${S.include?" selected":""}`,children:[c.jsx(rt,{checked:S.include,onChange:E=>z({include:E}),label:""}),c.jsx("button",{className:"expander",onClick:O,title:"per-item options",children:s?"▾":"▸"}),c.jsxs("div",{style:{minWidth:0},children:[c.jsx("div",{className:"name truncate",title:o.name,children:o.name}),c.jsxs("div",{className:"sub row",style:{gap:6},children:[c.jsx(Un,{state:o.state}),o.composeService&&c.jsxs("span",{className:"faint",children:["· ",o.composeService]}),D&&c.jsx("span",{className:"badge",style:{borderColor:"#5c4520",color:"#e0b556"},children:"on target"})]})]}),c.jsx("div",{className:"image truncate",title:o.image,children:o.image}),c.jsxs("div",{className:"tags",children:[H.map(E=>c.jsx("span",{className:`badge ${E.kind==="bind"?"bind":E.kind==="anonymous"?"anon":"vol"}`,title:`${E.kind} → ${E.destination}${E.readOnly?" (read-only)":""}`,style:{opacity:(S.mounts[E.destination]?.action??"copy")==="skip"?.35:1},children:E.kind==="bind"?(E.source??"").split("/").pop()||"/":E.kind==="anonymous"?"anon":E.name},E.destination)),(o.endpoints??[]).filter(E=>!["bridge","host","none"].includes(E.network)).map(E=>c.jsx("span",{className:"badge net",title:`network ${E.network}`,children:E.network},E.network)),(o.ports??[]).slice(0,3).map((E,U)=>c.jsxs("span",{className:"badge port",children:[E.hostPort,":",E.containerPort.split("/")[0]]},U)),(o.ports??[]).length>3&&c.jsxs("span",{className:"badge port",children:["+",(o.ports??[]).length-3]})]}),c.jsxs("div",{className:"small faint nowrap",style:{textAlign:"right"},children:[b.length>0?`${b.length} to copy`:"no data",V>0&&c.jsxs(c.Fragment,{children:[" · ",ie(V)]})]})]}),s&&c.jsx(xy,{c:o,s:S,onChange:z,sizes:T})]})}function xy({c:o,s:S,onChange:z,sizes:s}){const O=o.mounts??[];function D(T,w){z({mounts:{...S.mounts,[T]:{...S.mounts[T],...w}}})}return c.jsxs("div",{className:"detail",children:[(o.warnings??[]).map((T,w)=>c.jsx("div",{className:"notice warn",children:T},w)),c.jsxs("div",{className:"grid2",children:[c.jsx(Ll,{label:"name on target",children:c.jsx("input",{type:"text",placeholder:o.name,value:S.nameOverride??"",onChange:T=>z({nameOverride:T.target.value})})}),c.jsx(Ll,{label:"image",children:c.jsxs("select",{value:S.migrateImage?S.imageMode:"skip",onChange:T=>{const w=T.target.value;z({migrateImage:w!=="skip",imageMode:w})},children:[c.jsx("option",{value:"auto",children:"auto — reuse, pull, or transfer"}),c.jsx("option",{value:"pull",children:"pull on the target"}),c.jsx("option",{value:"stream",children:"transfer the layers"}),c.jsx("option",{value:"skip",children:"already on the target"})]})}),c.jsxs("div",{className:"stack",style:{gap:6},children:[c.jsx(rt,{checked:S.migrateNetworks,onChange:T=>z({migrateNetworks:T}),label:"recreate networks and reattach"}),c.jsx(rt,{checked:S.keepStaticIps,onChange:T=>z({keepStaticIps:T}),disabled:!S.migrateNetworks,label:"keep static IP addresses",title:"Only works when the target networks use the same subnets"}),c.jsx(rt,{checked:S.migratePorts,onChange:T=>z({migratePorts:T}),label:"publish the same host ports"})]}),c.jsxs("div",{className:"stack",style:{gap:6},children:[c.jsx(rt,{checked:S.startAfter,onChange:T=>z({startAfter:T}),label:"start on the target"}),c.jsx(rt,{checked:S.stopSourceDuringCopy,onChange:T=>z({stopSourceDuringCopy:T}),label:"stop the source while copying",title:"Recommended: databases and other writers produce inconsistent copies while running"}),c.jsx(rt,{checked:S.stopSourceAfter,onChange:T=>z({stopSourceAfter:T}),label:"leave the source stopped afterwards"})]})]}),O.length===0?c.jsx("div",{className:"small faint",children:"this container has no mounts"}):c.jsxs("table",{className:"mount-table",children:[c.jsx("thead",{children:c.jsxs("tr",{children:[c.jsx("th",{style:{width:74},children:"kind"}),c.jsx("th",{children:"in the container"}),c.jsx("th",{children:"on the source"}),c.jsx("th",{style:{width:130},children:"action"}),c.jsx("th",{children:"on the target"}),c.jsx("th",{style:{width:70,textAlign:"right"},children:"size"})]})}),c.jsx("tbody",{children:O.map(T=>{const w=S.mounts[T.destination]??{action:"copy"},H=T.kind==="tmpfs";return c.jsxs("tr",{children:[c.jsx("td",{children:c.jsx("span",{className:`badge ${T.kind==="bind"?"bind":T.kind==="anonymous"?"anon":T.kind==="tmpfs"?"tmpfs":"vol"}`,children:T.kind})}),c.jsxs("td",{className:"mono truncate",title:T.destination,children:[T.destination,T.readOnly&&c.jsx("span",{className:"faint",children:" :ro"})]}),c.jsx("td",{className:"mono truncate faint",title:T.source||T.name,children:T.kind==="bind"?T.source:T.kind==="anonymous"?"(generated)":T.name}),c.jsx("td",{children:c.jsxs("select",{value:w.action,disabled:H,onChange:b=>D(T.destination,{action:b.target.value}),children:[c.jsx("option",{value:"copy",children:"copy data"}),c.jsx("option",{value:"structure",children:"create empty"}),c.jsx("option",{value:"skip",children:"do not mount"})]})}),c.jsxs("td",{children:[T.kind==="bind"&&w.action!=="skip"&&c.jsx("input",{type:"text",placeholder:T.source,value:w.targetSource??"",onChange:b=>D(T.destination,{targetSource:b.target.value})}),T.kind==="volume"&&w.action!=="skip"&&c.jsx("input",{type:"text",placeholder:T.name,value:w.targetName??"",onChange:b=>D(T.destination,{targetName:b.target.value})}),T.kind==="anonymous"&&c.jsx("span",{className:"small faint",children:"a fresh volume is created"}),H&&c.jsx("span",{className:"small faint",children:"in memory, nothing to copy"})]}),c.jsx("td",{className:"small faint nowrap",style:{textAlign:"right"},children:H?"–":ie(Br(T,s))})]},T.destination)})})]})]})}function Yr({value:o,set:S,where:z}){return c.jsxs(c.Fragment,{children:[c.jsxs("div",{className:"row",style:{gap:12},children:[c.jsx(Ll,{label:"host",children:c.jsx("input",{type:"text",value:o.host??"",onChange:s=>S("host",s.target.value)})}),c.jsx("div",{style:{width:90},children:c.jsx(Ll,{label:"port",children:c.jsx("input",{type:"number",value:o.port??22,onChange:s=>S("port",Number(s.target.value))})})})]}),c.jsxs("div",{className:"row",style:{gap:12},children:[c.jsx(Ll,{label:"user",children:c.jsx("input",{type:"text",value:o.user??"",onChange:s=>S("user",s.target.value)})}),c.jsx(Ll,{label:"authentication",children:c.jsxs("select",{value:o.auth??"password",onChange:s=>S("auth",s.target.value),children:[c.jsx("option",{value:"password",children:"password"}),c.jsx("option",{value:"key",children:"private key"}),c.jsx("option",{value:"agent",children:"ssh agent"})]})})]}),o.auth==="password"&&c.jsx(Ll,{label:"password",children:c.jsx("input",{type:"password",value:o.password??"",onChange:s=>S("password",s.target.value)})}),o.auth==="key"&&c.jsxs(c.Fragment,{children:[c.jsx(Ll,{label:"private key path on this machine (leave empty to paste the key below)",children:c.jsx("input",{type:"text",placeholder:"/root/.ssh/id_ed25519",value:o.privateKeyPath??"",onChange:s=>S("privateKeyPath",s.target.value)})}),c.jsx(Ll,{label:"or paste the private key",children:c.jsx("textarea",{rows:5,value:o.privateKey??"",onChange:s=>S("privateKey",s.target.value),placeholder:"-----BEGIN OPENSSH PRIVATE KEY-----"})}),c.jsx(Ll,{label:"passphrase (if the key is encrypted)",children:c.jsx("input",{type:"password",value:o.passphrase??"",onChange:s=>S("passphrase",s.target.value)})})]}),o.auth==="agent"&&c.jsxs("div",{className:"small muted",children:["Uses the agent at ",c.jsx("span",{className:"mono",children:"$SSH_AUTH_SOCK"})," of the process running dockmv."]}),c.jsx(rt,{checked:o.sudo??!1,onChange:s=>S("sudo",s),label:`run docker through sudo -n on the ${z}`,title:"Needed when the login user is not in the docker group. sudo must not ask for a password."}),c.jsx(Ll,{label:`docker command on the ${z} (optional)`,children:c.jsx("input",{type:"text",placeholder:"docker",value:o.dockerCmd??"",onChange:s=>S("dockerCmd",s.target.value)})}),c.jsx(rt,{checked:o.saveSecrets??!1,onChange:s=>S("saveSecrets",s),label:"remember the password / key on disk"}),o.saveSecrets?c.jsx(ht,{kind:"warn",children:"Credentials are stored in plain text in dockmv's data directory, readable only by this user. Leave this off to keep them in memory for this session only."}):c.jsx("div",{className:"small faint",children:"Credentials stay in memory and are lost when dockmv restarts."})]})}function Gr({info:o,onClose:S,onTrust:z}){return c.jsx(Iu,{title:"SSH host key",onClose:S,footer:c.jsxs(c.Fragment,{children:[c.jsx("button",{className:"btn",onClick:S,children:"cancel"}),c.jsx("button",{className:"btn primary",onClick:z,children:o.changed?"replace the stored key and trust":"trust this host"})]}),children:c.jsxs("div",{className:"stack",children:[o.changed&&c.jsxs(ht,{kind:"err",children:["The key presented by this host is ",c.jsx("b",{children:"different"})," from the one recorded earlier. This happens after a reinstall — but it is also what a machine-in-the-middle looks like. Only continue if you know why it changed."]}),o.trusted&&!o.changed&&c.jsx(ht,{kind:"ok",children:"This host key is already trusted."}),c.jsxs("div",{className:"small muted",children:["Compare this with the output of ",c.jsxs("span",{className:"mono",children:["ssh-keyscan -t ",o.keyType," ",o.host]})," ","run on the host itself, or with ",c.jsx("span",{className:"mono",children:"ssh-keygen -lf /etc/ssh/ssh_host_*_key.pub"}),"."]}),c.jsxs("div",{className:"fingerprint",children:[o.keyType,c.jsx("br",{}),o.fingerprint]})]})})}function zy({sources:o,selected:S,status:z,selectSource:s,reload:O,onError:D}){const[T,w]=Q.useState(null),[H,b]=Q.useState(null),[V,E]=Q.useState(""),[U,ll]=Q.useState(""),W=o.find(el=>el.id===S),vl=!W||W.kind==="local",Yl=W?.kind==="ssh";async function gl(el,Sl,Nl){E(el);try{await Nl()}catch(C){C instanceof Fu&&C.needsTrust?(ll(Sl),await _l(Sl)):D(C instanceof Error?C.message:String(C))}finally{E("")}}async function _l(el){try{b(await Al.probeSource(el))}catch(Sl){D(Sl instanceof Error?Sl.message:String(Sl))}}async function dl(){const el=U||S;H&&await gl("trust",el,async()=>{await Al.trustSource(el,H.fingerprint),b(null),await s(el)})}return c.jsxs("div",{className:"section",children:[c.jsx("h3",{children:"source host"}),c.jsxs("div",{className:"stack",children:[c.jsxs("div",{className:"row",children:[c.jsx("select",{value:S,disabled:!!V,onChange:el=>{const Sl=el.target.value;gl("select",Sl,()=>s(Sl))},children:o.map(el=>c.jsx("option",{value:el.id,children:Ty(el)},el.id))}),c.jsx("button",{className:"btn tiny",onClick:()=>w({kind:"ssh",ssh:{port:22,auth:"password",saveSecrets:!1,sudo:!1}}),children:"new"})]}),c.jsxs("div",{className:"row wrap",style:{gap:6},children:[c.jsx("button",{className:"btn tiny",disabled:!!V,onClick:()=>{gl("select",S,()=>s(S))},children:V==="select"?"connecting…":"reconnect"}),!vl&&c.jsx("button",{className:"btn tiny",onClick:()=>w(W),children:"edit"}),Yl&&c.jsx("button",{className:"btn tiny",onClick:()=>{ll(S),_l(S)},children:"host key"}),!vl&&c.jsx("button",{className:"btn tiny danger",onClick:()=>{!W||!confirm(`Delete source "${W.name}"?`)||gl("del",W.id,async()=>{await Al.deleteSource(W.id),await O(),S===W.id&&await s("local")})},children:"delete"})]}),z?.error&&c.jsx(ht,{kind:"err",children:z.error}),z&&!z.error&&c.jsxs("dl",{className:"kv",children:[c.jsx("dt",{children:"reached by"}),c.jsx("dd",{className:"mono",children:z.endpoint}),z.dockerVersion&&c.jsxs(c.Fragment,{children:[c.jsx("dt",{children:"docker"}),c.jsx("dd",{children:z.dockerVersion})]})]}),Yl&&c.jsx("div",{className:"small faint",children:"Data is streamed through dockmv: source → this host → target. A local source moves it in one hop."})]}),T&&c.jsx(Ey,{initial:T,onClose:()=>w(null),onSaved:async el=>{w(null),await O(),await gl("select",el.id,()=>s(el.id))},onError:D}),H&&c.jsx(Gr,{info:H,onClose:()=>b(null),onTrust:dl})]})}function Ty(o){switch(o.kind){case"local":return`${o.name} (local docker)`;case"docker":return`${o.name} (${o.dockerHost})`;default:return`${o.name} (ssh ${o.ssh?.user}@${o.ssh?.host})`}}function Ey({initial:o,onClose:S,onSaved:z,onError:s}){const[O,D]=Q.useState(o),[T,w]=Q.useState(!1),H=O.kind??"ssh",b=O.ssh??{};function V(U,ll){D(W=>({...W,ssh:{...W.ssh,[U]:ll}}))}const E=H==="ssh"?!!b.host&&!!b.user:!!O.dockerHost;return c.jsx(Iu,{title:o.id?`Edit ${o.name}`:"New source host",onClose:S,footer:c.jsxs(c.Fragment,{children:[c.jsx("button",{className:"btn",onClick:S,children:"cancel"}),c.jsx("button",{className:"btn primary",disabled:T||!E,onClick:async()=>{w(!0);try{await z(await Al.saveSource(Ny(O,H)))}catch(U){s(U instanceof Error?U.message:String(U))}finally{w(!1)}},children:T?"saving…":"save"})]}),children:c.jsxs("div",{className:"stack",style:{gap:12},children:[c.jsxs("div",{className:"row",style:{gap:12},children:[c.jsx(Ll,{label:"label",children:c.jsx("input",{type:"text",value:O.name??"",onChange:U=>D(ll=>({...ll,name:U.target.value}))})}),c.jsx(Ll,{label:"reached by",children:c.jsxs("select",{value:H,onChange:U=>D(ll=>({...ll,kind:U.target.value})),children:[c.jsx("option",{value:"ssh",children:"ssh — remote host, driven through its docker CLI"}),c.jsx("option",{value:"docker",children:"docker address — a daemon this host can reach"})]})})]}),H==="docker"?c.jsxs(c.Fragment,{children:[c.jsx(Ll,{label:"docker address",children:c.jsx("input",{type:"text",placeholder:"tcp://10.0.0.5:2375",value:O.dockerHost??"",onChange:U=>D(ll=>({...ll,dockerHost:U.target.value}))})}),c.jsxs("div",{className:"small muted",children:["Any address the docker CLI accepts: ",c.jsx("span",{className:"mono",children:"tcp://host:2375"}),", or another socket with"," ",c.jsx("span",{className:"mono",children:"unix:///path/docker.sock"}),". A TLS-protected daemon uses the certificates from"," ",c.jsx("span",{className:"mono",children:"DOCKER_CERT_PATH"})," in dockmv's own environment."]}),c.jsxs(ht,{kind:"warn",children:["A plain ",c.jsx("span",{className:"mono",children:"tcp://"})," daemon is unauthenticated: anyone who can reach that port is root on that host. Prefer an ssh source unless the port is already protected."]})]}):c.jsxs(c.Fragment,{children:[c.jsx(Yr,{value:b,set:V,where:"source host"}),c.jsxs("div",{className:"small muted",children:["Needs ",c.jsx("span",{className:"mono",children:"sshd"})," and a docker CLI of 18.09 or newer on that host — the API is tunnelled through ",c.jsx("span",{className:"mono",children:"docker system dial-stdio"}),". Nothing is installed."]})]})]})})}function Ny(o,S){const z={id:o.id,name:o.name,kind:S};if(S==="docker")return z.dockerHost=o.dockerHost,z;const s=o.ssh??{};return z.ssh={host:s.host??"",port:s.port??22,user:s.user??"",auth:s.auth??"password",password:s.password,privateKey:s.privateKey,privateKeyPath:s.privateKeyPath,passphrase:s.passphrase,sudo:s.sudo??!1,dockerCmd:s.dockerCmd,saveSecrets:s.saveSecrets??!1},z}function Ay({source:o,plan:S,includedCount:z,options:s,setOptions:O,connections:D,activeConn:T,setActiveConn:w,reloadConnections:H,targetInv:b,connectTarget:V,onJobStarted:E,onError:U}){const[ll,W]=Q.useState(null),[vl,Yl]=Q.useState(null),[gl,_l]=Q.useState(null),[dl,el]=Q.useState(""),[Sl,Nl]=Q.useState(""),[C,Z]=Q.useState("tar"),B=D.find(M=>M.id===T),$=b?.preflight,sl=!!$?.serverVersion;async function jl(M,K){el(M);try{await K()}catch(nl){nl instanceof Fu&&nl.needsTrust?await tt():U(nl instanceof Error?nl.message:String(nl))}finally{el("")}}const at=Q.useCallback(()=>{T&&jl("test",()=>V(T))},[T]);Q.useEffect(()=>{at()},[at]);async function tt(){if(T)try{Yl(await Al.probe(T))}catch(M){U(M instanceof Error?M.message:String(M))}}async function Cl(){!T||!vl||await jl("trust",async()=>{await Al.trust(T,vl.fingerprint),Yl(null),await V(T)})}const j=z>0&&sl&&!dl,R=z>0&&!dl;return c.jsxs(c.Fragment,{children:[c.jsxs("div",{className:"section",children:[c.jsx("h3",{children:"target host"}),c.jsxs("div",{className:"stack",children:[c.jsxs("div",{className:"row",children:[c.jsxs("select",{value:T,onChange:M=>w(M.target.value),children:[c.jsx("option",{value:"",children:"— no target selected —"}),D.map(M=>c.jsxs("option",{value:M.id,children:[M.name," (",M.user,"@",M.host,")"]},M.id))]}),c.jsx("button",{className:"btn tiny",onClick:()=>W({port:22,auth:"password",saveSecrets:!1,sudo:!1}),children:"new"})]}),B&&c.jsxs("div",{className:"row wrap",style:{gap:6},children:[c.jsx("button",{className:"btn tiny",disabled:!!dl,onClick:at,children:dl==="test"?"connecting…":"connect"}),c.jsx("button",{className:"btn tiny",onClick:()=>W(B),children:"edit"}),c.jsx("button",{className:"btn tiny",onClick:tt,children:"host key"}),c.jsx("button",{className:"btn tiny danger",onClick:()=>{confirm(`Delete connection "${B.name}"?`)&&jl("del",async()=>{await Al.deleteConnection(B.id),H()})},children:"delete"})]}),B&&!b&&c.jsx("div",{className:"small faint",children:"not connected yet"}),$&&c.jsxs(c.Fragment,{children:[($.problems??[]).map((M,K)=>c.jsx(ht,{kind:"warn",children:M},K)),sl&&c.jsxs("dl",{className:"kv",children:[c.jsx("dt",{children:"host"}),c.jsx("dd",{children:b?.host||B?.host}),c.jsx("dt",{children:"docker"}),c.jsxs("dd",{children:[$.serverVersion," · ",$.os,"/",$.arch]}),c.jsx("dt",{children:"free space"}),c.jsxs("dd",{children:[ie($.diskFreeBytes)," on ",$.dockerRoot]}),c.jsx("dt",{children:"existing"}),c.jsxs("dd",{children:[(b?.containers??[]).length," containers ·"," ",(b?.volumes??[]).length," volumes"]}),c.jsx("dt",{children:"gzip"}),c.jsx("dd",{children:$.hasGzip?"yes":"missing"})]})]})]})]}),c.jsxs("div",{className:"section",children:[c.jsx("h3",{children:"options"}),c.jsxs("div",{className:"stack",children:[c.jsx(Ll,{label:"if the name already exists on the target",children:c.jsxs("select",{value:s.conflict,onChange:M=>O(K=>({...K,conflict:M.target.value})),children:[c.jsx("option",{value:"fail",children:"stop with an error"}),c.jsx("option",{value:"skip",children:"skip that container"}),c.jsx("option",{value:"rename",children:"create it under a new name"}),c.jsx("option",{value:"replace",children:"remove the target's container first"})]})}),s.conflict==="rename"&&c.jsx(Ll,{label:"suffix",children:c.jsx("input",{type:"text",value:s.renameSuffix??"",onChange:M=>O(K=>({...K,renameSuffix:M.target.value}))})}),s.conflict==="replace"&&c.jsx(ht,{kind:"warn",children:"Existing containers and volumes with the same name are deleted on the target before the copy."}),c.jsx(rt,{checked:s.compress,onChange:M=>O(K=>({...K,compress:M})),label:"compress transfers (gzip)"}),c.jsx(rt,{checked:s.verifyAfter,onChange:M=>O(K=>({...K,verifyAfter:M})),label:"verify each container after migrating"}),c.jsx(rt,{checked:s.dryRun,onChange:M=>O(K=>({...K,dryRun:M})),label:"dry run — show every command, change nothing"}),c.jsx(Ll,{label:`containers at a time: ${s.parallelism}`,children:c.jsx("input",{type:"range",min:1,max:6,value:s.parallelism,onChange:M=>O(K=>({...K,parallelism:Number(M.target.value)})),style:{width:"100%"}})})]})]}),c.jsxs("div",{className:"section",children:[c.jsx("h3",{children:"migrate over ssh"}),c.jsxs("div",{className:"stack",children:[c.jsx("button",{className:"btn primary",disabled:!j,onClick:()=>jl("ssh",async()=>{const M=await Al.migrateSSH(T,S);E(M)}),children:dl==="ssh"?"starting…":`migrate ${z} container${z===1?"":"s"} to target`}),c.jsx("button",{className:"btn",disabled:z===0||!!dl,onClick:()=>jl("preview",async()=>{_l(await Al.preview(S))}),children:"preview the commands"}),z===0&&c.jsx("div",{className:"small faint",children:"select at least one container"}),z>0&&!sl&&c.jsx("div",{className:"small faint",children:"connect to a target first"})]})]}),c.jsxs("div",{className:"section",children:[c.jsx("h3",{children:"migration package"}),c.jsxs("div",{className:"stack",children:[c.jsxs("div",{className:"small muted",children:["Builds a self-contained folder with the data, the images and an ",c.jsx("span",{className:"mono",children:"install.sh"})," to run on the target. No network between the hosts required."]}),c.jsx(Ll,{label:"package name",children:c.jsx("input",{type:"text",placeholder:"auto (timestamped)",value:Sl,onChange:M=>Nl(M.target.value)})}),c.jsx(Ll,{label:"format",children:c.jsxs("select",{value:C,onChange:M=>Z(M.target.value),children:[c.jsx("option",{value:"tar",children:"single .tar file (downloadable)"}),c.jsx("option",{value:"dir",children:"directory on this host"})]})}),c.jsx("button",{className:"btn",disabled:!R,onClick:()=>jl("pkg",async()=>{const M=await Al.buildPackage({...S,packageName:Sl},C);E(M)}),children:dl==="pkg"?"starting…":"build package"})]})]}),o?.inventory.warnings?.length?c.jsxs("div",{className:"section",children:[c.jsx("h3",{children:"source warnings"}),c.jsx("div",{className:"stack",children:o.inventory.warnings.map((M,K)=>c.jsx(ht,{kind:"warn",children:M},K))})]}):null,ll&&c.jsx(_y,{initial:ll,onClose:()=>W(null),onSaved:M=>{W(null),H(),w(M.id)},onError:U}),vl&&c.jsx(Gr,{info:vl,onClose:()=>Yl(null),onTrust:Cl}),gl&&c.jsx(Oy,{data:gl,onClose:()=>_l(null)})]})}function _y({initial:o,onClose:S,onSaved:z,onError:s}){const[O,D]=Q.useState(o),[T,w]=Q.useState(!1);function H(b,V){D(E=>({...E,[b]:V}))}return c.jsx(Iu,{title:o.id?`Edit ${o.name}`:"New target host",onClose:S,footer:c.jsxs(c.Fragment,{children:[c.jsx("button",{className:"btn",onClick:S,children:"cancel"}),c.jsx("button",{className:"btn primary",disabled:T||!O.host||!O.user,onClick:async()=>{w(!0);try{z(await Al.saveConnection(O))}catch(b){s(b instanceof Error?b.message:String(b))}finally{w(!1)}},children:T?"saving…":"save"})]}),children:c.jsxs("div",{className:"stack",style:{gap:12},children:[c.jsx(Ll,{label:"label",children:c.jsx("input",{type:"text",value:O.name??"",onChange:b=>H("name",b.target.value)})}),c.jsx(Yr,{value:O,set:H,where:"target"})]})})}function Oy({data:o,onClose:S}){const z=o.items.reduce((s,O)=>s+O.totalBytes,0);return c.jsx(Iu,{title:"What this migration will run",wide:!0,onClose:S,footer:c.jsx("button",{className:"btn",onClick:S,children:"close"}),children:c.jsxs("div",{className:"stack",style:{gap:16},children:[c.jsxs("div",{className:"small muted",children:[o.items.length," container(s)",z>0&&c.jsxs(c.Fragment,{children:[" · about ",ie(z)," of known volume data"]}),". These are the commands that run on the target; data is streamed into ",c.jsx("span",{className:"mono",children:"docker cp"})," rather than written to a file."]}),(o.networkCommands??[]).length>0&&c.jsxs("div",{children:[c.jsx("h3",{style:{margin:"0 0 6px",fontSize:12},children:"shared networks"}),c.jsx("pre",{className:"cmdblock",children:(o.networkCommands??[]).join(` -`)})]}),o.items.map(s=>c.jsxs("div",{children:[c.jsxs("h3",{style:{margin:"0 0 6px",fontSize:12},children:[s.name,s.targetName!==s.name&&c.jsxs("span",{className:"faint",children:[" → ",s.targetName]})]}),(s.warnings??[]).map((O,D)=>c.jsx(ht,{kind:"warn",children:O},`w${D}`)),(s.notes??[]).map((O,D)=>c.jsxs("div",{className:"small faint",children:["· ",O]},`n${D}`)),c.jsx("pre",{className:"cmdblock",children:(s.commands??[]).join(` -`)})]},s.containerId))]})})}function My({jobs:o,activeJob:S,setActiveJob:z,reload:s,reloadPackages:O}){const D=S||o[0]?.id||"";return o.length===0?c.jsx("div",{className:"empty",children:"no migrations yet — select containers and start one"}):c.jsxs("div",{style:{display:"flex",minHeight:0,height:"100%"},children:[c.jsx("div",{className:"joblist",style:{width:320,flex:"0 0 320px",overflow:"auto"},children:o.map(T=>c.jsxs("div",{className:`jobcard${T.id===D?" active":""}`,onClick:()=>z(T.id),children:[c.jsxs("div",{className:"row",children:[c.jsx(Un,{state:T.state}),c.jsx("span",{className:"spacer"}),c.jsx("span",{className:"small faint",children:T.kind==="ssh"?"ssh":"package"})]}),c.jsx("div",{className:"truncate",style:{marginTop:2},children:T.title}),c.jsxs("div",{className:"small faint",children:[new Date(T.createdAt).toLocaleTimeString()," · ",qr(T.startedAt,T.endedAt),T.dryRun&&" · dry run"]}),c.jsx("div",{style:{marginTop:6},children:c.jsx(zf,{done:T.bytesDone,total:T.bytesTotal,state:T.state})})]},T.id))}),c.jsx("div",{style:{flex:1,minWidth:0,overflow:"auto",borderLeft:"1px solid var(--border)"},children:D&&c.jsx(Dy,{id:D,reload:s,reloadPackages:O})})]})}function Dy({id:o,reload:S,reloadPackages:z}){const[s,O]=Q.useState(null),[D,T]=Q.useState(!0),w=Q.useRef(null),H=Q.useRef(!0);Q.useEffect(()=>{O(null);let E=!1;Al.job(o).then(ll=>!E&&O(ll)).catch(()=>{});const U=Al.jobEvents(o);return U.onmessage=ll=>{try{O(JSON.parse(ll.data))}catch{}},U.addEventListener("done",()=>{U.close(),S(),z()}),U.onerror=()=>U.close(),()=>{E=!0,U.close()}},[o,S,z]);const b=Q.useMemo(()=>(s?.log??[]).filter(E=>D||E.level!=="cmd"),[s,D]);if(Q.useEffect(()=>{const E=w.current;E&&H.current&&(E.scrollTop=E.scrollHeight)},[b]),!s)return c.jsx("div",{className:"empty",children:"loading…"});const V=s.state==="running"||s.state==="pending";return c.jsxs("div",{style:{padding:16,display:"flex",flexDirection:"column",gap:14},children:[c.jsxs("div",{className:"row",children:[c.jsx(Un,{state:s.state}),c.jsx("b",{children:s.title}),s.dryRun&&c.jsx("span",{className:"badge",children:"dry run"}),c.jsx("span",{className:"spacer"}),c.jsxs("span",{className:"small faint",children:[ie(s.bytesDone),s.bytesTotal>0&&c.jsxs(c.Fragment,{children:[" of ",ie(s.bytesTotal)]})," · ",qr(s.startedAt,s.endedAt)]}),V?c.jsx("button",{className:"btn tiny danger",onClick:()=>Al.cancelJob(s.id).then(S),children:"cancel"}):c.jsx("button",{className:"btn tiny ghost",onClick:()=>Al.deleteJob(s.id).then(S),children:"remove"})]}),c.jsx(zf,{done:s.bytesDone,total:s.bytesTotal,state:s.state}),s.error&&c.jsx(ht,{kind:"err",children:s.error}),s.state==="succeeded"&&s.artifact&&c.jsxs(ht,{kind:"ok",children:["Package ready at ",c.jsx("span",{className:"mono",children:s.artifact})," (",ie(s.artifactBytes??0),")."," ","Open the Packages tab to download it."]}),s.items.map(E=>c.jsxs("div",{style:{border:"1px solid var(--border)",borderRadius:6,padding:"8px 10px"},children:[c.jsxs("div",{className:"row",children:[c.jsx(Un,{state:E.state}),c.jsx("b",{children:E.name}),c.jsx("span",{className:"spacer"}),c.jsxs("span",{className:"small faint",children:[E.steps.filter(U=>U.state==="succeeded").length,"/",E.steps.length," steps"]})]}),E.error&&c.jsx("div",{className:"small",style:{color:"var(--err)"},children:E.error}),(E.warnings??[]).map((U,ll)=>c.jsxs("div",{className:"small",style:{color:"var(--warn)"},children:["! ",U]},ll)),c.jsx("div",{className:"steps",children:E.steps.map(U=>c.jsxs("div",{className:`step ${U.state}`,children:[c.jsx(Un,{state:U.state,label:""}),c.jsx("span",{className:"label truncate",title:U.error||U.label,children:U.label}),c.jsx("span",{children:U.bytesTotal>0||U.bytesDone>0?c.jsx(zf,{done:U.bytesDone,total:U.bytesTotal,state:U.state}):null}),c.jsx("span",{className:"faint nowrap",style:{textAlign:"right"},children:U.bytesDone>0?ie(U.bytesDone):U.state==="skipped"?"skipped":""})]},U.id))})]},E.id)),c.jsxs("div",{className:"row",children:[c.jsx("h3",{style:{margin:0,fontSize:12},children:"log"}),c.jsx("span",{className:"spacer"}),c.jsxs("label",{className:"check small",children:[c.jsx("input",{type:"checkbox",checked:D,onChange:E=>T(E.target.checked)}),c.jsx("span",{children:"show commands"})]})]}),c.jsxs("div",{className:"log",ref:w,onScroll:E=>{const U=E.currentTarget;H.current=U.scrollHeight-U.scrollTop-U.clientHeight<24},children:[b.map(E=>c.jsxs("div",{className:`l-${E.level}`,children:[c.jsxs("span",{className:"ts",children:[new Date(E.at).toLocaleTimeString()," "]}),E.message]},E.seq)),b.length===0&&c.jsx("span",{className:"faint",children:"nothing logged yet"})]})]})}function Cy({packages:o,reload:S}){return o.length===0?c.jsx("div",{className:"empty",children:"no packages built yet"}):c.jsxs("div",{style:{padding:16,display:"flex",flexDirection:"column",gap:12},children:[c.jsxs(ht,{kind:"info",children:["Copy a package to the target host, then run ",c.jsx("span",{className:"mono",children:"./install.sh --dry-run"})," to review it and"," ",c.jsx("span",{className:"mono",children:"./install.sh"})," to restore. The target needs only bash, gzip and docker."]}),c.jsxs("table",{className:"mount-table",children:[c.jsx("thead",{children:c.jsxs("tr",{children:[c.jsx("th",{children:"name"}),c.jsx("th",{style:{width:110},children:"kind"}),c.jsx("th",{style:{width:110,textAlign:"right"},children:"size"}),c.jsx("th",{style:{width:170},children:"built"}),c.jsx("th",{style:{width:190}})]})}),c.jsx("tbody",{children:o.map(z=>c.jsxs("tr",{children:[c.jsx("td",{className:"mono truncate",title:z.path,children:z.name}),c.jsx("td",{children:c.jsx("span",{className:"badge",children:z.isDir?"directory":"tar"})}),c.jsx("td",{className:"nowrap",style:{textAlign:"right"},children:ie(z.bytes)}),c.jsx("td",{className:"small faint",children:new Date(z.createdAt).toLocaleString()}),c.jsx("td",{children:c.jsxs("div",{className:"row",style:{justifyContent:"flex-end",gap:6},children:[z.isDir?c.jsx("span",{className:"small faint",title:z.path,children:"copy it from disk"}):c.jsx("a",{className:"btn tiny",href:Al.downloadUrl(z.name),download:!0,children:"download"}),c.jsx("button",{className:"btn tiny danger",onClick:()=>{confirm(`Delete package "${z.name}"? This cannot be undone.`)&&Al.deletePackage(z.name).then(S)},children:"delete"})]})})]},z.name))})]})]})}function Uy(){const[o,S]=Q.useState(null),[z,s]=Q.useState(null),[O,D]=Q.useState({}),[T,w]=Q.useState({conflict:"fail",renameSuffix:"-migrated",compress:!0,compressLevel:1,dryRun:!1,parallelism:1,verifyAfter:!0}),[H,b]=Q.useState({}),[V,E]=Q.useState([]),[U,ll]=Q.useState("local"),[W,vl]=Q.useState(null),[Yl,gl]=Q.useState([]),[_l,dl]=Q.useState(""),[el,Sl]=Q.useState(null),[Nl,C]=Q.useState([]),[Z,B]=Q.useState([]),[$,sl]=Q.useState("containers"),[jl,at]=Q.useState(""),[tt,Cl]=Q.useState(""),[j,R]=Q.useState(!0),M=Q.useCallback(async()=>{R(!0);try{const k=await Al.source();s(k),D(Jl=>{const ce={};for(const Ie of k.inventory.containers)ce[Ie.id]=Jl[Ie.id]??k.defaults[Ie.id];return ce}),Cl(""),Al.volumeSizes().then(Jl=>b(Jl.volumes??{})).catch(()=>{})}catch(k){Cl(k instanceof Error?k.message:String(k))}finally{R(!1)}},[]),K=Q.useCallback(async()=>{try{const k=await Al.health();S(k),k.source&&vl(k.source)}catch{}},[]),nl=Q.useCallback(async()=>{try{const k=await Al.sources();E(k.sources),ll(k.selected),k.current&&vl(k.current)}catch(k){Cl(k instanceof Error?k.message:String(k))}},[]),h=Q.useCallback(async k=>{const Jl=await Al.selectSource(k);ll(k),vl(Jl),D({}),b({}),Cl(""),await M(),await K()},[M,K]),A=Q.useCallback(async()=>{try{const k=await Al.connections();gl(k),dl(Jl=>Jl&&k.some(ce=>ce.id===Jl)?Jl:k[0]?.id??"")}catch(k){Cl(k instanceof Error?k.message:String(k))}},[]),q=Q.useCallback(async()=>{try{C(await Al.jobs())}catch{}},[]),Y=Q.useCallback(async()=>{try{B(await Al.packages())}catch{}},[]);Q.useEffect(()=>{K(),nl(),M(),A(),q(),Y()},[K,nl,M,A,q,Y]),Q.useEffect(()=>{const k=setInterval(q,4e3);return()=>clearInterval(k)},[q]);const F=Q.useCallback(async k=>{if(Sl(null),!k)return;const Jl=await Al.targetInventory(k);Sl(Jl),Cl("")},[]),al=Q.useMemo(()=>Object.values(O).filter(k=>k.include),[O]),rl=Q.useMemo(()=>({items:Object.values(O),options:T}),[O,T]),Kl=Nl.filter(k=>k.state==="running"||k.state==="pending").length,Ul=Q.useCallback(k=>{C(Jl=>[k,...Jl]),at(k.id),sl("jobs")},[]);return c.jsxs("div",{className:"app",children:[c.jsxs("header",{className:"topbar",children:[c.jsxs("div",{className:"brand",children:[c.jsx("img",{src:"/logo-icon.png",alt:"",className:"brand-logo"}),"DockMV"]}),c.jsxs("nav",{className:"tabs",children:[c.jsxs("button",{className:`tab${$==="containers"?" active":""}`,onClick:()=>sl("containers"),children:["Containers",c.jsxs("span",{className:"count",children:[al.length,"/",z?.inventory.containers.length??0]})]}),c.jsxs("button",{className:`tab${$==="jobs"?" active":""}`,onClick:()=>sl("jobs"),children:["Jobs",Kl>0&&c.jsxs("span",{className:"count",children:[Kl," running"]})]}),c.jsxs("button",{className:`tab${$==="packages"?" active":""}`,onClick:()=>sl("packages"),children:["Packages",Z.length>0&&c.jsx("span",{className:"count",children:Z.length})]})]}),c.jsxs("div",{className:"topbar-right",children:[o&&c.jsxs("span",{className:"hostinfo",children:["source ",c.jsx("b",{children:z?.inventory.host||W?.name||o.dockerHost}),W?.kind==="ssh"&&c.jsx(c.Fragment,{children:" · over ssh"}),W?.kind==="docker"&&c.jsxs(c.Fragment,{children:[" · ",W.endpoint]}),o.dockerVersion&&c.jsxs(c.Fragment,{children:[" · docker ",o.dockerVersion]})]}),c.jsx("button",{className:"btn tiny",onClick:M,disabled:j,children:j?"loading…":"refresh"})]})]}),tt&&c.jsx("div",{style:{padding:"10px 16px"},children:c.jsxs(ht,{kind:"err",children:[tt,c.jsx("button",{className:"btn tiny ghost",style:{marginLeft:8},onClick:()=>Cl(""),children:"dismiss"})]})}),o&&!o.ok&&c.jsx("div",{style:{padding:"10px 16px"},children:c.jsxs(ht,{kind:"err",children:["Cannot reach the source ",c.jsx("b",{children:o.source?.name??"docker daemon"}),o.dockerHost&&c.jsxs(c.Fragment,{children:[" at ",c.jsx("span",{className:"mono",children:o.dockerHost})]}),o.dockerError&&c.jsxs(c.Fragment,{children:[" — ",o.dockerError]}),c.jsx("div",{className:"small",children:"Pick another source in the panel on the right."})]})}),c.jsxs("div",{className:"body",children:[c.jsxs("main",{className:"main",children:[$==="containers"&&c.jsx(py,{source:z,sel:O,setSel:D,targetInv:el,loading:j,sizes:H}),$==="jobs"&&c.jsx(My,{jobs:Nl,activeJob:jl,setActiveJob:at,reload:q,reloadPackages:Y}),$==="packages"&&c.jsx(Cy,{packages:Z,reload:Y})]}),$==="containers"&&c.jsxs("aside",{className:"sidebar",children:[c.jsx(zy,{sources:V,selected:U,status:W,selectSource:h,reload:nl,onError:Cl}),c.jsx(Ay,{source:z,plan:rl,includedCount:al.length,options:T,setOptions:w,connections:Yl,activeConn:_l,setActiveConn:dl,reloadConnections:A,targetInv:el,connectTarget:F,onJobStarted:Ul,onError:Cl})]})]})]})}Sy.createRoot(document.getElementById("root")).render(c.jsx(Q.StrictMode,{children:c.jsx(Uy,{})})); diff --git a/internal/webui/dist/index.html b/internal/webui/dist/index.html index 95445e5..1e36550 100644 --- a/internal/webui/dist/index.html +++ b/internal/webui/dist/index.html @@ -6,8 +6,8 @@ DockMV - - + +
diff --git a/main.go b/main.go index 0b1289e..2b8d2ab 100644 --- a/main.go +++ b/main.go @@ -9,8 +9,6 @@ package main import ( "context" - "crypto/rand" - "encoding/hex" "encoding/json" "errors" "flag" @@ -81,8 +79,7 @@ Run "dockmv serve -h" for the server flags. func serve(args []string) error { fs := flag.NewFlagSet("serve", flag.ContinueOnError) addr := fs.String("addr", "127.0.0.1:8080", "address to listen on; use 0.0.0.0:8080 to expose it on the network") - token := fs.String("token", os.Getenv("DOCKMV_TOKEN"), "require this token on every request; \"auto\" generates one (default: $DOCKMV_TOKEN)") - dataDir := fs.String("data-dir", defaultDataDir(), "directory for connections and trusted host keys") + dataDir := fs.String("data-dir", defaultDataDir(), "directory for accounts, connections and trusted host keys") pkgDir := fs.String("package-dir", "", "directory for migration packages (default /packages)") dockerHost := fs.String("docker-host", "", "local source docker daemon (default: the DOCKER_HOST environment)") verbose := fs.Bool("v", false, "verbose logging") @@ -108,21 +105,8 @@ func serve(args []string) error { } logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: level})) - authToken := *token - if authToken == "auto" { - authToken = randomToken() - } - // Anything but a loopback bind is reachable by other machines. This tool - // can stop containers and read every volume on the host, so it refuses to - // be exposed without a token. - if authToken == "" && !isLoopback(*addr) { - authToken = randomToken() - logger.Warn("listening on a non-loopback address; generated an access token") - } - cfg := api.Config{ Addr: *addr, - Token: authToken, DataDir: *dataDir, PackageDir: *pkgDir, DockerHost: *dockerHost, @@ -136,6 +120,19 @@ func serve(args []string) error { } defer srv.Close() + // This tool can stop containers and read every volume on the host, so a + // fresh install must complete account setup from loopback before it is + // reachable from anywhere else. DOCKMV_TRUST_ADDR skips this when some + // other layer already restricts exposure — e.g. the Docker image, which + // binds 0.0.0.0 internally so `ports: "127.0.0.1:8080:8080"` can do the + // actual restricting. + if srv.NeedsSetup() && !isLoopback(*addr) && os.Getenv("DOCKMV_TRUST_ADDR") == "" { + return fmt.Errorf("no account exists yet; bind to a loopback address first "+ + "(e.g. --addr 127.0.0.1:8080), reach it with an SSH tunnel if needed "+ + "(ssh -L 8080:127.0.0.1:8080 user@this-host), finish setup, then restart with %s; "+ + "or set DOCKMV_TRUST_ADDR=1 if exposure is already restricted elsewhere (e.g. a container's own port mapping)", *addr) + } + if !webui.Built() { logger.Warn("web UI is not embedded in this binary; only the HTTP API is available") } @@ -154,9 +151,6 @@ func serve(args []string) error { } url := "http://" + displayAddr(ln.Addr().String()) - if authToken != "" { - url += "/?token=" + authToken - } fmt.Fprintf(os.Stderr, "\n dockmv %s\n open %s\n data: %s\n\n", version, url, *dataDir) ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) @@ -227,14 +221,6 @@ func defaultDataDir() string { return ".dockmv" } -func randomToken() string { - b := make([]byte, 16) - if _, err := rand.Read(b); err != nil { - return fmt.Sprintf("t%d", time.Now().UnixNano()) - } - return hex.EncodeToString(b) -} - func isLoopback(addr string) bool { host, _, err := net.SplitHostPort(addr) if err != nil { diff --git a/web/src/Account.tsx b/web/src/Account.tsx new file mode 100644 index 0000000..66d1872 --- /dev/null +++ b/web/src/Account.tsx @@ -0,0 +1,115 @@ +import { useCallback, useEffect, useState } from 'react' +import { api, ApiError } from './api' +import type { APIToken, Me } from './types' +import { Notice } from './ui' + +export function Account({ me }: { me: Me }) { + const [tokens, setTokens] = useState([]) + const [name, setName] = useState('') + const [error, setError] = useState('') + const [busy, setBusy] = useState(false) + // Set only right after a create; the plaintext is never retrievable again. + const [revealed, setRevealed] = useState<{ name: string; token: string } | null>(null) + + const load = useCallback(async () => { + try { + setTokens(await api.tokens()) + } catch (e) { + setError(e instanceof Error ? e.message : String(e)) + } + }, []) + + useEffect(() => { + load() + }, [load]) + + const create = async () => { + const trimmed = name.trim() + if (!trimmed) return + setBusy(true) + setError('') + try { + const tok = await api.createToken(trimmed) + setRevealed({ name: tok.name, token: tok.token }) + setName('') + await load() + } catch (e) { + setError(e instanceof ApiError ? e.message : String(e)) + } finally { + setBusy(false) + } + } + + const revoke = async (id: string) => { + if (!confirm('Revoke this token? Anything using it stops working immediately.')) return + await api.revokeToken(id) + await load() + } + + return ( +
+
+

signed in as

+
{me.username}
+
+ +
+

personal API tokens

+
+ For scripted access: pass a token as X-Auth-Token or{' '} + Authorization: Bearer …. +
+
+ + {error && {error}} + + {revealed && ( + + Token {revealed.name} — copy it now, it will not be shown again: +
{revealed.token}
+ +
+ )} + +
+ setName(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && create()} + /> + +
+ + {tokens.length === 0 ? ( +
no personal tokens yet
+ ) : ( + + + + + + + + + + + + {tokens.map((t) => ( + + + + + + + + ))} + +
nameends increatedlast used
{t.name}…{t.hint}{new Date(t.createdAt).toLocaleString()}{t.lastUsedAt ? new Date(t.lastUsedAt).toLocaleString() : 'never'} + +
+ )} +
+ ) +} diff --git a/web/src/App.tsx b/web/src/App.tsx index bc7f573..653b177 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -1,7 +1,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react' import { api } from './api' import type { - Connection, Health, ItemSelection, JobSnapshot, Options, PackageInfo, Plan, Source, SourceResponse, + Connection, Health, ItemSelection, JobSnapshot, Me, Options, PackageInfo, Plan, Source, SourceResponse, SourceStatus, TargetInventory, } from './types' import { Notice } from './ui' @@ -10,10 +10,11 @@ import { SourcePanel } from './SourcePanel' import { Sidebar } from './Sidebar' import { Jobs } from './Jobs' import { Packages } from './Packages' +import { Account } from './Account' -type View = 'containers' | 'jobs' | 'packages' +type View = 'containers' | 'jobs' | 'packages' | 'account' -export default function App() { +export default function App({ me, logout }: { me: Me; logout: () => void }) { const [health, setHealth] = useState(null) const [source, setSource] = useState(null) const [sel, setSel] = useState>({}) @@ -174,6 +175,9 @@ export default function App() { Packages {packages.length > 0 && {packages.length}} +
{health && ( @@ -187,6 +191,8 @@ export default function App() { + {me.username} +
@@ -232,6 +238,7 @@ export default function App() { /> )} {view === 'packages' && } + {view === 'account' && } {view === 'containers' && ( diff --git a/web/src/AuthGate.tsx b/web/src/AuthGate.tsx new file mode 100644 index 0000000..c4f0867 --- /dev/null +++ b/web/src/AuthGate.tsx @@ -0,0 +1,49 @@ +import { useCallback, useEffect, useState } from 'react' +import App from './App' +import { api, setUnauthorizedHandler } from './api' +import { Login } from './Login' +import type { Me } from './types' + +/** + * AuthGate decides, on load and after every 401, whether to show the setup + * screen, a login form, or the app itself. It owns the one GET /api/me call + * the app needs before it can render anything real. + */ +export function AuthGate() { + const [me, setMe] = useState(null) + const [error, setError] = useState('') + + const load = useCallback(async () => { + try { + setMe(await api.me()) + setError('') + } catch (e) { + setError(e instanceof Error ? e.message : String(e)) + } + }, []) + + useEffect(() => { + load() + }, [load]) + + // A 401 elsewhere in the app (an expired or revoked session) means the + // server no longer considers us logged in; drop straight to the login form + // instead of waiting for the next unrelated re-render to notice. + useEffect(() => { + setUnauthorizedHandler(() => setMe((prev) => (prev ? { ...prev, authenticated: false } : prev))) + return () => setUnauthorizedHandler(null) + }, []) + + const logout = useCallback(async () => { + await api.logout().catch(() => undefined) + await load() + }, [load]) + + if (!me) { + return
{error || 'loading…'}
+ } + if (!me.authenticated) { + return + } + return +} diff --git a/web/src/Login.tsx b/web/src/Login.tsx new file mode 100644 index 0000000..deb3232 --- /dev/null +++ b/web/src/Login.tsx @@ -0,0 +1,70 @@ +import { useState, type FormEvent } from 'react' +import { api, ApiError } from './api' +import { Notice } from './ui' + +/** + * Login is the one form for both first-run setup and every login after that: + * the server itself has no separate "register" concept, just an account + * store that starts empty. + */ +export function Login({ needsSetup, onDone }: { needsSetup: boolean; onDone: () => void }) { + const [username, setUsername] = useState('') + const [password, setPassword] = useState('') + const [error, setError] = useState('') + const [busy, setBusy] = useState(false) + + const submit = async (e: FormEvent) => { + e.preventDefault() + setBusy(true) + setError('') + try { + if (needsSetup) { + await api.setup(username, password) + } else { + await api.login(username, password) + } + onDone() + } catch (err) { + setError(err instanceof ApiError ? err.message : String(err)) + } finally { + setBusy(false) + } + } + + return ( +
+
+
DockMV
+

{needsSetup ? 'Create your account' : 'Sign in'}

+ + {needsSetup && ( + + No account exists yet. Create the first one to finish setup — every account can manage every + other one, there are no separate roles yet. + + )} + + + + + {error && {error}} + + +
+
+ ) +} diff --git a/web/src/api.ts b/web/src/api.ts index 58038e1..857e0b4 100644 --- a/web/src/api.ts +++ b/web/src/api.ts @@ -1,23 +1,8 @@ import type { - Connection, Health, HostKeyInfo, JobSnapshot, PackageInfo, Plan, - Preflight, PreviewResponse, Source, SourceResponse, SourcesResponse, SourceStatus, TargetInventory, + APIToken, Connection, Health, HostKeyInfo, JobSnapshot, Me, PackageInfo, Plan, + Preflight, PreviewResponse, Source, SourceResponse, SourcesResponse, SourceStatus, TargetInventory, User, } from './types' -// The token, when the server requires one, arrives as a query parameter the -// first time and is kept for the tab afterwards. -function readToken(): string { - const fromUrl = new URLSearchParams(location.search).get('token') - if (fromUrl) { - sessionStorage.setItem('dm.token', fromUrl) - const clean = location.pathname + location.hash - history.replaceState(null, '', clean) - return fromUrl - } - return sessionStorage.getItem('dm.token') ?? '' -} - -const token = readToken() - /** ApiError carries the server's message plus anything it attached to it. */ export class ApiError extends Error { status: number @@ -33,12 +18,26 @@ export class ApiError extends Error { } } +// Paths whose own job is to report or resolve "not authenticated" — a 401 +// from one of these is the expected outcome of a bad login, not a session +// that dropped out from under the app, so it must not trigger the global +// handler below. +const authPaths = new Set(['/api/me', '/api/login', '/api/setup']) + +// Set by AuthGate so a 401 on any other endpoint (a session that expired or +// was revoked mid-use) drops the app back to the login screen, without every +// call site having to check for it. +let onUnauthorized: (() => void) | null = null +export function setUnauthorizedHandler(fn: (() => void) | null) { + onUnauthorized = fn +} + async function request(path: string, init?: RequestInit): Promise { const headers: Record = { ...(init?.headers as Record) } - if (token) headers['X-Auth-Token'] = token if (init?.body) headers['Content-Type'] = 'application/json' - const res = await fetch(path, { ...init, headers }) + const res = await fetch(path, { ...init, headers, credentials: 'same-origin' }) + if (res.status === 401 && !authPaths.has(path)) onUnauthorized?.() if (res.status === 204) return undefined as T const text = await res.text() @@ -65,6 +64,19 @@ export const api = { source: () => request('/api/source'), volumeSizes: () => request<{ volumes: Record }>('/api/source/sizes'), + me: () => request('/api/me'), + setup: (username: string, password: string) => post('/api/setup', { username, password }), + login: (username: string, password: string) => post('/api/login', { username, password }), + logout: () => request('/api/logout', { method: 'POST' }), + + users: () => request('/api/users'), + createUser: (username: string, password: string) => post('/api/users', { username, password }), + deleteUser: (id: string) => request(`/api/users/${id}`, { method: 'DELETE' }), + + tokens: () => request('/api/tokens'), + createToken: (name: string) => post('/api/tokens', { name }), + revokeToken: (id: string) => request(`/api/tokens/${id}`, { method: 'DELETE' }), + sources: () => request('/api/sources'), saveSource: (s: Partial) => post('/api/sources', s), deleteSource: (id: string) => request(`/api/sources/${id}`, { method: 'DELETE' }), @@ -92,10 +104,8 @@ export const api = { packages: () => request('/api/packages'), deletePackage: (name: string) => request(`/api/packages/${encodeURIComponent(name)}`, { method: 'DELETE' }), - downloadUrl: (name: string) => - `/api/packages/${encodeURIComponent(name)}/download` + (token ? `?token=${encodeURIComponent(token)}` : ''), + downloadUrl: (name: string) => `/api/packages/${encodeURIComponent(name)}/download`, - /** Opens the live progress stream for a job. */ - jobEvents: (id: string) => - new EventSource(`/api/jobs/${id}/events` + (token ? `?token=${encodeURIComponent(token)}` : '')), + /** Opens the live progress stream for a job. Cookies ride along automatically: it's a same-origin request. */ + jobEvents: (id: string) => new EventSource(`/api/jobs/${id}/events`), } diff --git a/web/src/main.tsx b/web/src/main.tsx index c31d4c6..afb2eb3 100644 --- a/web/src/main.tsx +++ b/web/src/main.tsx @@ -1,10 +1,10 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' -import App from './App' +import { AuthGate } from './AuthGate' import './styles.css' createRoot(document.getElementById('root')!).render( - + , ) diff --git a/web/src/styles.css b/web/src/styles.css index 6dc76f6..2aa9863 100644 --- a/web/src/styles.css +++ b/web/src/styles.css @@ -311,3 +311,14 @@ label.field > span { display: block; font-size: 11px; color: var(--text-dim); ma background: var(--bg-sunken); border: 1px solid var(--border-strong); border-radius: var(--radius); padding: 8px 10px; } + +/* ---------- login ---------- */ + +.login { height: 100%; display: flex; align-items: center; justify-content: center; } +.login-box { + width: min(360px, 90vw); display: flex; flex-direction: column; gap: 14px; + background: var(--bg-raised); border: 1px solid var(--border); + border-radius: var(--radius); padding: 28px; +} +.login-box .brand { display: flex; align-items: center; gap: 8px; font-weight: 600; } +.login-box h1 { font-size: 16px; font-weight: 600; margin: 0; } diff --git a/web/src/types.ts b/web/src/types.ts index 10688ff..587dfd3 100644 --- a/web/src/types.ts +++ b/web/src/types.ts @@ -286,5 +286,28 @@ export interface Health { packageDir: string dataDir: string knownHosts: string - authRequired: boolean +} + +export interface User { + id: string + username: string + createdAt: string + lastLoginAt?: string +} + +export interface APIToken { + id: string + userId: string + name: string + hint: string + createdAt: string + lastUsedAt?: string +} + +/** The response from GET /api/me, /api/setup and /api/login. */ +export interface Me { + id?: string + username?: string + authenticated: boolean + needsSetup?: boolean }