commit 0e3c5f5e845268f8369d56f8732257b9e70210fa Author: Egutierrez Date: Mon May 25 01:05:43 2026 +0200 feat: scaffold matrix_admin_panel v0.1.0 (issue 0163) Wails + React + Mantine v7 admin panel for Matrix/Synapse. Replaces the removed synapse-admin container. MAS OIDC PKCE login (loopback :8766) + Synapse Admin API (users/rooms/sessions). - MAS client: XSFD2SWA394DXRVJFTREAMY6J6 (public PKCE, no auth method). - Backend: AdminService (Go) with Login/SetAdminToken/ListUsers/ DeactivateUser/ResetUserPassword/ListRooms/DeleteRoom/GetUserDevices. - Vendored helpers in internal/infra/ from registry: mas_oidc_loopback_go_infra, keyring_token_store_go_infra, synapse_admin_client_go_infra. - Frontend: AppShell + sidebar tabs (Users/Rooms/Sessions). Sessions placeholder pending MAS admin API. - Build verified: Linux + Windows. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a18c6f9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +# Wails build artifacts +build/bin/ +build/darwin/ +build/windows/ +build/linux/ + +# Frontend +frontend/dist/ +frontend/node_modules/ +frontend/wailsjs/go/ +frontend/wailsjs/runtime/ + +# Local state +*.db +*.db-shm +*.db-wal +local_files/ + +# IDE +.vscode/ +.idea/ + +# OS +.DS_Store +Thumbs.db diff --git a/README.md b/README.md new file mode 100644 index 0000000..7b23686 --- /dev/null +++ b/README.md @@ -0,0 +1,20 @@ +# matrix_admin_panel + +Panel admin Matrix propio. Sustituye `synapse-admin` (eliminado en issue 0162). + +Stack: Wails (Go) + React + Mantine v7. Login MAS OIDC PKCE (loopback :8766) + Synapse Admin API. + +## Dev + +```bash +wails dev +``` + +## Build + +```bash +wails build # linux/amd64 +wails build -platform windows/amd64 # windows +``` + +Binary en `build/bin/matrix_admin_panel(.exe)`. diff --git a/admin_service.go b/admin_service.go new file mode 100644 index 0000000..796c2c6 --- /dev/null +++ b/admin_service.go @@ -0,0 +1,418 @@ +package main + +import ( + "context" + "errors" + "fmt" + "net/http" + "strings" + "sync" + "time" + + "fn-registry/projects/element_agents/apps/matrix_admin_panel/internal/infra" +) + +// Constants — hardcoded for issue 0163 MVP. Operator-configurable later via settings UI. +const ( + homeserverURL = "https://matrix-af2f3d.organic-machine.com" + masIssuer = "https://auth-af2f3d.organic-machine.com/" + masClientID = "XSFD2SWA394DXRVJFTREAMY6J6" + loopbackPort = 8766 + keyringServiceName = "fn_registry.matrix_admin_panel" + oidcTimeoutSeconds = 300 + adminTokenPrefix = "admin_token:" +) + +var defaultScopes = []string{ + "openid", + "urn:matrix:org.matrix.msc2967.client:api:*", +} + +// AdminService is bound to the Wails frontend. +type AdminService struct { + ctx context.Context + mu sync.Mutex + store *infra.KeyringTokenStore + adminToken string + loggedUser string +} + +func NewAdminService() *AdminService { + return &AdminService{ + store: infra.NewKeyringTokenStore(keyringServiceName), + } +} + +func (s *AdminService) SetContext(ctx context.Context) { + s.ctx = ctx +} + +// SessionView is the safe-to-send JSON for the frontend (no tokens). +type SessionView struct { + UserID string `json:"user_id"` + HomeserverURL string `json:"homeserver_url"` + HasOIDCToken bool `json:"has_oidc_token"` + HasAdminToken bool `json:"has_admin_token"` + ExpiresAt string `json:"expires_at,omitempty"` +} + +// Login launches the OAuth2 PKCE flow against MAS. Blocks until completion or timeout. +// Returns the OIDC subject (user identifier as known by MAS) for session lookup. +func (s *AdminService) Login() (string, error) { + s.mu.Lock() + defer s.mu.Unlock() + + cfg := infra.MasOidcLoopbackConfig{ + Issuer: masIssuer, + ClientID: masClientID, + Scopes: defaultScopes, + LoopbackPort: loopbackPort, + OpenBrowser: true, + TimeoutSeconds: oidcTimeoutSeconds, + } + res, err := infra.MasOidcLoopback(cfg) + if err != nil { + return "", fmt.Errorf("oidc: %w", err) + } + + // Resolve user_id via /whoami so we have a stable handle. + userID, _, err := whoami(s.ctx, homeserverURL, res.AccessToken) + if err != nil { + return "", fmt.Errorf("whoami: %w", err) + } + + tok := infra.Token{ + AccessToken: res.AccessToken, + RefreshToken: res.RefreshToken, + UserID: userID, + HomeserverURL: homeserverURL, + Issuer: masIssuer, + ClientID: masClientID, + } + if res.ExpiresIn > 0 { + tok.ExpiresAt = time.Now().Add(time.Duration(res.ExpiresIn) * time.Second) + } + if err := s.store.Save(userID, tok); err != nil { + return "", fmt.Errorf("keyring save: %w", err) + } + s.loggedUser = userID + + // Try to load a previously saved admin token for this user. + if adminTok, err := s.store.Load(adminTokenPrefix + userID); err == nil { + s.adminToken = adminTok.AccessToken + } + + return userID, nil +} + +// GetSession returns the persisted session for the given user_id. +func (s *AdminService) GetSession(userID string) (*SessionView, error) { + if userID == "" { + return nil, errors.New("user_id required") + } + tok, err := s.store.Load(userID) + if err != nil { + if errors.Is(err, infra.ErrNotFound) { + return nil, nil + } + return nil, fmt.Errorf("keyring load: %w", err) + } + + // Re-attach admin token in memory on restart. + s.mu.Lock() + if s.adminToken == "" { + if adminTok, err := s.store.Load(adminTokenPrefix + userID); err == nil { + s.adminToken = adminTok.AccessToken + } + } + s.loggedUser = userID + hasAdmin := s.adminToken != "" + s.mu.Unlock() + + view := &SessionView{ + UserID: tok.UserID, + HomeserverURL: tok.HomeserverURL, + HasOIDCToken: tok.AccessToken != "", + HasAdminToken: hasAdmin, + } + if !tok.ExpiresAt.IsZero() { + view.ExpiresAt = tok.ExpiresAt.Format(time.RFC3339) + } + return view, nil +} + +// Logout deletes the persisted OIDC token + admin token for the given user_id. +func (s *AdminService) Logout(userID string) error { + s.mu.Lock() + defer s.mu.Unlock() + if userID == "" { + return errors.New("user_id required") + } + _ = s.store.Delete(adminTokenPrefix + userID) + s.adminToken = "" + s.loggedUser = "" + return s.store.Delete(userID) +} + +// SetAdminToken validates the provided Synapse admin access_token against the admin API +// and persists it on success. The token is NOT the MAS OIDC token — it must belong to a +// user with `admin: true` in Synapse. +func (s *AdminService) SetAdminToken(token string) error { + s.mu.Lock() + defer s.mu.Unlock() + + token = strings.TrimSpace(token) + if token == "" { + return errors.New("admin token is empty") + } + if s.loggedUser == "" { + return errors.New("must login first") + } + + // Probe the admin API: GET /_synapse/admin/v1/users/{self} requires admin:true. + // We use the OIDC-logged user as probe target — the lookup always works for admins. + client := infra.NewSynapseAdminClient(homeserverURL, token) + if _, err := client.GetUser(s.context(), s.loggedUser); err != nil { + return fmt.Errorf("admin token invalid: %w", err) + } + + s.adminToken = token + + // Persist (reuse Token struct, only AccessToken matters). + if err := s.store.Save(adminTokenPrefix+s.loggedUser, infra.Token{ + AccessToken: token, + UserID: s.loggedUser, + HomeserverURL: homeserverURL, + }); err != nil { + return fmt.Errorf("keyring save admin token: %w", err) + } + return nil +} + +// adminClient returns a configured SynapseAdminClient or an error if admin token not set. +func (s *AdminService) adminClient() (*infra.SynapseAdminClient, error) { + s.mu.Lock() + tok := s.adminToken + s.mu.Unlock() + if tok == "" { + return nil, errors.New("admin token not set; call SetAdminToken first") + } + return infra.NewSynapseAdminClient(homeserverURL, tok), nil +} + +func (s *AdminService) context() context.Context { + if s.ctx != nil { + return s.ctx + } + return context.Background() +} + +// --- Args/Results for Wails bindings (JSON-safe, no interfaces) --- + +type AdminUserArg struct { + UserID string `json:"user_id"` + DisplayName string `json:"display_name"` + AvatarURL string `json:"avatar_url"` + Admin bool `json:"admin"` + Deactivated bool `json:"deactivated"` + IsGuest bool `json:"is_guest"` + CreationTs int64 `json:"creation_ts"` + LastSeenTs int64 `json:"last_seen_ts"` +} + +type ListUsersFilterArg struct { + From int `json:"from"` + Limit int `json:"limit"` + SearchTerm string `json:"search_term"` + DeactivatedSet bool `json:"deactivated_set"` // true => apply deactivated filter + Deactivated bool `json:"deactivated"` + AdminsSet bool `json:"admins_set"` // true => apply admins filter + Admins bool `json:"admins"` +} + +type ListUsersResultArg struct { + Users []AdminUserArg `json:"users"` + TotalCount int `json:"total_count"` + NextToken int `json:"next_token"` // -1 if no more pages +} + +type AdminRoomArg struct { + RoomID string `json:"room_id"` + Name string `json:"name"` + CanonicalAlias string `json:"canonical_alias"` + JoinedMembers int `json:"joined_members"` + JoinedLocal int `json:"joined_local"` + Version string `json:"version"` + Encrypted bool `json:"encrypted"` + Federatable bool `json:"federatable"` + Public bool `json:"public"` +} + +type ListRoomsResultArg struct { + Rooms []AdminRoomArg `json:"rooms"` + TotalCount int `json:"total_count"` + NextToken int `json:"next_token"` // -1 if no more pages +} + +type AdminDeviceArg struct { + DeviceID string `json:"device_id"` + DisplayName string `json:"display_name"` + LastSeenIP string `json:"last_seen_ip"` + LastSeenTs int64 `json:"last_seen_ts"` +} + +func toUserArg(u infra.AdminUser) AdminUserArg { + return AdminUserArg{ + UserID: u.UserID, + DisplayName: u.DisplayName, + AvatarURL: u.AvatarURL, + Admin: u.Admin, + Deactivated: u.Deactivated, + IsGuest: u.IsGuest, + CreationTs: u.CreationTs, + LastSeenTs: u.LastSeenTs, + } +} + +func toRoomArg(r infra.AdminRoom) AdminRoomArg { + return AdminRoomArg{ + RoomID: r.RoomID, + Name: r.Name, + CanonicalAlias: r.CanonicalAlias, + JoinedMembers: r.JoinedMembers, + JoinedLocal: r.JoinedLocal, + Version: r.Version, + Encrypted: r.Encrypted, + Federatable: r.Federatable, + Public: r.Public, + } +} + +// --- Users --- + +func (s *AdminService) ListUsers(f ListUsersFilterArg) (*ListUsersResultArg, error) { + c, err := s.adminClient() + if err != nil { + return nil, err + } + + filter := infra.ListUsersFilter{ + From: f.From, + Limit: f.Limit, + SearchTerm: f.SearchTerm, + } + if f.DeactivatedSet { + v := f.Deactivated + filter.Deactivated = &v + } + if f.AdminsSet { + v := f.Admins + filter.Admins = &v + } + + res, err := c.ListUsers(s.context(), filter) + if err != nil { + return nil, err + } + + out := &ListUsersResultArg{ + TotalCount: res.TotalCount, + NextToken: -1, + Users: make([]AdminUserArg, 0, len(res.Users)), + } + if res.NextToken != nil { + out.NextToken = *res.NextToken + } + for _, u := range res.Users { + out.Users = append(out.Users, toUserArg(u)) + } + return out, nil +} + +func (s *AdminService) DeactivateUser(userID string, erase bool) error { + c, err := s.adminClient() + if err != nil { + return err + } + return c.DeactivateUser(s.context(), userID, erase) +} + +func (s *AdminService) ResetUserPassword(userID, newPassword string, logoutDevices bool) error { + if strings.TrimSpace(newPassword) == "" { + return errors.New("new_password is empty") + } + c, err := s.adminClient() + if err != nil { + return err + } + return c.ResetPassword(s.context(), userID, newPassword, logoutDevices) +} + +func (s *AdminService) GetUserDevices(userID string) ([]AdminDeviceArg, error) { + c, err := s.adminClient() + if err != nil { + return nil, err + } + devices, err := c.ListUserDevices(s.context(), userID) + if err != nil { + return nil, err + } + out := make([]AdminDeviceArg, 0, len(devices)) + for _, d := range devices { + out = append(out, AdminDeviceArg{ + DeviceID: d.DeviceID, + DisplayName: d.DisplayName, + LastSeenIP: d.LastSeenIP, + LastSeenTs: d.LastSeenTs, + }) + } + return out, nil +} + +// --- Rooms --- + +func (s *AdminService) ListRooms(from, limit int, search string) (*ListRoomsResultArg, error) { + c, err := s.adminClient() + if err != nil { + return nil, err + } + rooms, total, next, err := c.ListRooms(s.context(), from, limit, search) + if err != nil { + return nil, err + } + out := &ListRoomsResultArg{ + TotalCount: total, + NextToken: -1, + Rooms: make([]AdminRoomArg, 0, len(rooms)), + } + if next != nil { + out.NextToken = *next + } + for _, r := range rooms { + out.Rooms = append(out.Rooms, toRoomArg(r)) + } + return out, nil +} + +func (s *AdminService) DeleteRoom(roomID, reason string, purge, block bool) (string, error) { + c, err := s.adminClient() + if err != nil { + return "", err + } + return c.DeleteRoom(s.context(), roomID, reason, purge, block) +} + +// HTTPStatusCheck is a small helper exposed mostly for diagnostics: hits Synapse health. +func (s *AdminService) Ping() (int, error) { + req, err := http.NewRequestWithContext(s.context(), http.MethodGet, homeserverURL+"/_matrix/client/versions", nil) + if err != nil { + return 0, err + } + cl := &http.Client{Timeout: 5 * time.Second} + resp, err := cl.Do(req) + if err != nil { + return 0, err + } + defer resp.Body.Close() + return resp.StatusCode, nil +} diff --git a/app.md b/app.md new file mode 100644 index 0000000..474ee71 --- /dev/null +++ b/app.md @@ -0,0 +1,73 @@ +--- +name: matrix_admin_panel +lang: go +domain: infra +version: 0.1.0 +description: "Panel admin Matrix propio (Wails + React + Mantine). Sustituye synapse-admin. MAS OIDC login + Synapse Admin API." +tags: [matrix, admin, synapse, mas, wails, react, mantine, infra, matrix-mas, client] +uses_functions: + - mas_oidc_loopback_go_infra + - keyring_token_store_go_infra + - synapse_admin_client_go_infra +uses_types: [] +framework: "wails" +entry_point: "main.go" +dir_path: "projects/element_agents/apps/matrix_admin_panel" +repo_url: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/dataforge/matrix_admin_panel.git" +icon: + phosphor: "shield-check" + accent: "#dc2626" +--- + +## Goal + +Panel admin Matrix propio que sustituye el contenedor synapse-admin eliminado (issue 0162). Wails (Go) + React+Mantine. Login MAS OIDC PKCE (loopback puerto 8766) + Synapse Admin API. + +## Ejecutar + +```bash +cd projects/element_agents/apps/matrix_admin_panel +wails dev # hot-reload +wails build # binario Linux +wails build -platform windows/amd64 # binario Windows +``` + +## Flow + +1. Login MAS OIDC (PKCE public client, mismo issuer que matrix_client_pc, distinto client_id). +2. Tras login, modal `AdminTokenModal` pide el `access_token` Synapse de un user con `admin: true` (MAS no expone scope admin todavia). +3. Validacion: GET `/_synapse/admin/v2/users/{self}` con el token. 200 = OK, se persiste en keyring con prefijo `admin_token:`. +4. UI con AppShell.Navbar tabs: Users / Rooms / Sessions. +5. Acciones row: Deactivate user (purge opcional), Reset password, Delete room (purge + block opcionales). + +## Arquitectura + +``` +main.go entry: wails.Run + bind AdminService +admin_service.go bindings (Login/SetAdminToken/ListUsers/...) +helpers.go whoami helper +internal/infra/ vendored helpers del registry + mas_oidc_loopback.go + keyring_token_store.go + synapse_admin_client.go +frontend/ React+Vite+TS+Mantine v7 + src/ + main.tsx MantineProvider violet dark + App.tsx router (Login | Home) + LoginScreen.tsx boton "Sign in with MAS" + AdminTokenModal.tsx pide admin_token Synapse + HomeScreen.tsx AppShell + sidebar tabs + UsersTab.tsx tabla users + acciones + RoomsTab.tsx tabla rooms + acciones + SessionsTab.tsx placeholder TBD +``` + +## MAS client (registrado en production) + +- `client_id`: `XSFD2SWA394DXRVJFTREAMY6J6` +- `client_auth_method`: `none` (PKCE public) +- redirect URIs: `http://127.0.0.1:8766/callback`, `http://localhost:8766/callback`, `https://admin-mas.organic-machine.com/callback`, `http://localhost:8090/callback` + +## Capability growth log + +- v0.1.0 (2026-05-25) — baseline scaffold (issue 0163): Wails skeleton + login MAS OIDC + admin token modal + Users/Rooms/Sessions tabs (Sessions placeholder). diff --git a/appicon.ico b/appicon.ico new file mode 100644 index 0000000..04e3811 Binary files /dev/null and b/appicon.ico differ diff --git a/build/appicon.png b/build/appicon.png new file mode 100644 index 0000000..63617fe Binary files /dev/null and b/build/appicon.png differ diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..6b1417f --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,12 @@ + + + + + + matrix_admin_panel + + +
+ + + diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..c4487b8 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,26 @@ +{ + "name": "matrix_admin_panel-frontend", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@mantine/core": "^7.13.0", + "@mantine/hooks": "^7.13.0", + "@mantine/notifications": "^7.13.0", + "@tabler/icons-react": "^3.19.0", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/react": "^18.3.10", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.1", + "typescript": "^5.6.2", + "vite": "^5.4.8" + } +} diff --git a/frontend/package.json.md5 b/frontend/package.json.md5 new file mode 100755 index 0000000..93f1e22 --- /dev/null +++ b/frontend/package.json.md5 @@ -0,0 +1 @@ +935cdc8db32c31326419659d899b94e2 \ No newline at end of file diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml new file mode 100644 index 0000000..1c9bd2d --- /dev/null +++ b/frontend/pnpm-lock.yaml @@ -0,0 +1,1445 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@mantine/core': + specifier: ^7.13.0 + version: 7.17.8(@mantine/hooks@7.17.8(react@18.3.1))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@mantine/hooks': + specifier: ^7.13.0 + version: 7.17.8(react@18.3.1) + '@mantine/notifications': + specifier: ^7.13.0 + version: 7.17.8(@mantine/core@7.17.8(@mantine/hooks@7.17.8(react@18.3.1))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mantine/hooks@7.17.8(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@tabler/icons-react': + specifier: ^3.19.0 + version: 3.44.0(react@18.3.1) + react: + specifier: ^18.3.1 + version: 18.3.1 + react-dom: + specifier: ^18.3.1 + version: 18.3.1(react@18.3.1) + devDependencies: + '@types/react': + specifier: ^18.3.10 + version: 18.3.29 + '@types/react-dom': + specifier: ^18.3.0 + version: 18.3.7(@types/react@18.3.29) + '@vitejs/plugin-react': + specifier: ^4.3.1 + version: 4.7.0(vite@5.4.21) + typescript: + specifier: ^5.6.2 + version: 5.9.3 + vite: + specifier: ^5.4.8 + version: 5.4.21 + +packages: + + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.3': + resolution: {integrity: sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.0': + resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.28.6': + resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.2': + resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.3': + resolution: {integrity: sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-transform-react-jsx-self@7.27.1': + resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.27.1': + resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/runtime@7.29.2': + resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.0': + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@floating-ui/core@1.7.5': + resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} + + '@floating-ui/dom@1.7.6': + resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} + + '@floating-ui/react-dom@2.1.8': + resolution: {integrity: sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/react@0.26.28': + resolution: {integrity: sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/utils@0.2.11': + resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@mantine/core@7.17.8': + resolution: {integrity: sha512-42sfdLZSCpsCYmLCjSuntuPcDg3PLbakSmmYfz5Auea8gZYLr+8SS5k647doVu0BRAecqYOytkX2QC5/u/8VHw==} + peerDependencies: + '@mantine/hooks': 7.17.8 + react: ^18.x || ^19.x + react-dom: ^18.x || ^19.x + + '@mantine/hooks@7.17.8': + resolution: {integrity: sha512-96qygbkTjRhdkzd5HDU8fMziemN/h758/EwrFu7TlWrEP10Vw076u+Ap/sG6OT4RGPZYYoHrTlT+mkCZblWHuw==} + peerDependencies: + react: ^18.x || ^19.x + + '@mantine/notifications@7.17.8': + resolution: {integrity: sha512-/YK16IZ198W6ru/IVecCtHcVveL08u2c8TbQTu/2p26LSIM9AbJhUkrU6H+AO0dgVVvmdmNdvPxcJnfq3S9TMg==} + peerDependencies: + '@mantine/core': 7.17.8 + '@mantine/hooks': 7.17.8 + react: ^18.x || ^19.x + react-dom: ^18.x || ^19.x + + '@mantine/store@7.17.8': + resolution: {integrity: sha512-/FrB6PAVH4NEjQ1dsc9qOB+VvVlSuyjf4oOOlM9gscPuapDP/79Ryq7JkhHYfS55VWQ/YUlY24hDI2VV+VptXg==} + peerDependencies: + react: ^18.x || ^19.x + + '@rolldown/pluginutils@1.0.0-beta.27': + resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} + + '@rollup/rollup-android-arm-eabi@4.60.4': + resolution: {integrity: sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.60.4': + resolution: {integrity: sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.60.4': + resolution: {integrity: sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.60.4': + resolution: {integrity: sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.60.4': + resolution: {integrity: sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.60.4': + resolution: {integrity: sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.60.4': + resolution: {integrity: sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.60.4': + resolution: {integrity: sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.60.4': + resolution: {integrity: sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.60.4': + resolution: {integrity: sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.60.4': + resolution: {integrity: sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loong64-musl@4.60.4': + resolution: {integrity: sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.60.4': + resolution: {integrity: sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-ppc64-musl@4.60.4': + resolution: {integrity: sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.60.4': + resolution: {integrity: sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.60.4': + resolution: {integrity: sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.60.4': + resolution: {integrity: sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.60.4': + resolution: {integrity: sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.60.4': + resolution: {integrity: sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openbsd-x64@4.60.4': + resolution: {integrity: sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.60.4': + resolution: {integrity: sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.60.4': + resolution: {integrity: sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.60.4': + resolution: {integrity: sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.60.4': + resolution: {integrity: sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.60.4': + resolution: {integrity: sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==} + cpu: [x64] + os: [win32] + + '@tabler/icons-react@3.44.0': + resolution: {integrity: sha512-8+rvzBbVm/1Z3sG3x7GUNAaxIKxwgz8xaMhRs23nrCnMTKRFAhEC+82zAIFeAA0seXdrAGX5HFCkaLpGK2rVHg==} + peerDependencies: + react: '>= 16' + + '@tabler/icons@3.44.0': + resolution: {integrity: sha512-Wn0AOZG9sg0L+bjfMqq4eNhC6pQjIrk94LvvWYNYkY8KH8wC3YILRzQlrnVJc4FUeMxH/AK97QsYCX35H3LndA==} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/prop-types@15.7.15': + resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} + + '@types/react-dom@18.3.7': + resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==} + peerDependencies: + '@types/react': ^18.0.0 + + '@types/react@18.3.29': + resolution: {integrity: sha512-ch0qJdr2JY0r04NXSprbK6TXOgnaJ1Tz23fm5W+z0/CBah6BSBc3n96h7K9GOtwh0HrilNWHIBzE1Ko4Dcw/Wg==} + + '@vitejs/plugin-react@4.7.0': + resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + + baseline-browser-mapping@2.10.32: + resolution: {integrity: sha512-wbPvpyjJPC0zdfdKXxqEL3Ea+bOMD/87X4lftiJkkaBiuG6ALQy1SLmEd7BSmVCuwCQsBrCamgBoLyfFDD1EPg==} + engines: {node: '>=6.0.0'} + hasBin: true + + browserslist@4.28.2: + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + caniuse-lite@1.0.30001793: + resolution: {integrity: sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + detect-node-es@1.1.0: + resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + + dom-helpers@5.2.1: + resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} + + electron-to-chromium@1.5.361: + resolution: {integrity: sha512-Q6Hts7N9FnJc5LeGRINFvLhCI9xZmNtTDe5ZbcVezQz7cU4a8Aua3GH1b8J2XY8Al9PF+OCwYqhgsOOheMdvkA==} + + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-nonce@1.0.1: + resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} + engines: {node: '>=6'} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.12: + resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + node-releases@2.0.46: + resolution: {integrity: sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==} + engines: {node: '>=18'} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + postcss@8.5.15: + resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} + engines: {node: ^10 || ^12 || >=14} + + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + + react-dom@18.3.1: + resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} + peerDependencies: + react: ^18.3.1 + + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + react-number-format@5.4.5: + resolution: {integrity: sha512-y8O2yHHj3w0aE9XO8d2BCcUOOdQTRSVq+WIuMlLVucAm5XNjJAy+BoOJiuQMldVYVOKTMyvVNfnbl2Oqp+YxGw==} + peerDependencies: + react: ^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + react-refresh@0.17.0: + resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} + engines: {node: '>=0.10.0'} + + react-remove-scroll-bar@2.3.8: + resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + react-remove-scroll@2.7.2: + resolution: {integrity: sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react-style-singleton@2.2.3: + resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react-textarea-autosize@8.5.9: + resolution: {integrity: sha512-U1DGlIQN5AwgjTyOEnI1oCcMuEr1pv1qOtklB2l4nyMGbHzWrI0eFsYK0zos2YWqAolJyG0IWJaqWmWj5ETh0A==} + engines: {node: '>=10'} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + react-transition-group@4.4.5: + resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} + peerDependencies: + react: '>=16.6.0' + react-dom: '>=16.6.0' + + react@18.3.1: + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + engines: {node: '>=0.10.0'} + + rollup@4.60.4: + resolution: {integrity: sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + scheduler@0.23.2: + resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + tabbable@6.4.0: + resolution: {integrity: sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + type-fest@4.41.0: + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} + engines: {node: '>=16'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + use-callback-ref@1.3.3: + resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + use-composed-ref@1.4.0: + resolution: {integrity: sha512-djviaxuOOh7wkj0paeO1Q/4wMZ8Zrnag5H6yBvzN7AKKe8beOaED9SF5/ByLqsku8NP4zQqsvM2u3ew/tJK8/w==} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + use-isomorphic-layout-effect@1.2.1: + resolution: {integrity: sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA==} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + use-latest@1.3.0: + resolution: {integrity: sha512-mhg3xdm9NaM8q+gLT8KryJPnRFOz1/5XPBhmDEVZK1webPzDjrPk7f/mbpeLqTgB9msytYWANxgALOCJKnLvcQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + use-sidecar@1.1.3: + resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + vite@5.4.21: + resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + +snapshots: + + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.3': {} + + '@babel/core@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helpers': 7.29.2 + '@babel/parser': 7.29.3 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.1': + dependencies: + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.28.6': + dependencies: + '@babel/compat-data': 7.29.3 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.2 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-module-imports@7.28.6': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.28.6': {} + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.29.2': + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + + '@babel/parser@7.29.3': + dependencies: + '@babel/types': 7.29.0 + + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/runtime@7.29.2': {} + + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 + + '@babel/traverse@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.29.3 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@esbuild/aix-ppc64@0.21.5': + optional: true + + '@esbuild/android-arm64@0.21.5': + optional: true + + '@esbuild/android-arm@0.21.5': + optional: true + + '@esbuild/android-x64@0.21.5': + optional: true + + '@esbuild/darwin-arm64@0.21.5': + optional: true + + '@esbuild/darwin-x64@0.21.5': + optional: true + + '@esbuild/freebsd-arm64@0.21.5': + optional: true + + '@esbuild/freebsd-x64@0.21.5': + optional: true + + '@esbuild/linux-arm64@0.21.5': + optional: true + + '@esbuild/linux-arm@0.21.5': + optional: true + + '@esbuild/linux-ia32@0.21.5': + optional: true + + '@esbuild/linux-loong64@0.21.5': + optional: true + + '@esbuild/linux-mips64el@0.21.5': + optional: true + + '@esbuild/linux-ppc64@0.21.5': + optional: true + + '@esbuild/linux-riscv64@0.21.5': + optional: true + + '@esbuild/linux-s390x@0.21.5': + optional: true + + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/openbsd-x64@0.21.5': + optional: true + + '@esbuild/sunos-x64@0.21.5': + optional: true + + '@esbuild/win32-arm64@0.21.5': + optional: true + + '@esbuild/win32-ia32@0.21.5': + optional: true + + '@esbuild/win32-x64@0.21.5': + optional: true + + '@floating-ui/core@1.7.5': + dependencies: + '@floating-ui/utils': 0.2.11 + + '@floating-ui/dom@1.7.6': + dependencies: + '@floating-ui/core': 1.7.5 + '@floating-ui/utils': 0.2.11 + + '@floating-ui/react-dom@2.1.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@floating-ui/dom': 1.7.6 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@floating-ui/react@0.26.28(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@floating-ui/react-dom': 2.1.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@floating-ui/utils': 0.2.11 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + tabbable: 6.4.0 + + '@floating-ui/utils@0.2.11': {} + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@mantine/core@7.17.8(@mantine/hooks@7.17.8(react@18.3.1))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@floating-ui/react': 0.26.28(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@mantine/hooks': 7.17.8(react@18.3.1) + clsx: 2.1.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-number-format: 5.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-remove-scroll: 2.7.2(@types/react@18.3.29)(react@18.3.1) + react-textarea-autosize: 8.5.9(@types/react@18.3.29)(react@18.3.1) + type-fest: 4.41.0 + transitivePeerDependencies: + - '@types/react' + + '@mantine/hooks@7.17.8(react@18.3.1)': + dependencies: + react: 18.3.1 + + '@mantine/notifications@7.17.8(@mantine/core@7.17.8(@mantine/hooks@7.17.8(react@18.3.1))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mantine/hooks@7.17.8(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@mantine/core': 7.17.8(@mantine/hooks@7.17.8(react@18.3.1))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@mantine/hooks': 7.17.8(react@18.3.1) + '@mantine/store': 7.17.8(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-transition-group: 4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + + '@mantine/store@7.17.8(react@18.3.1)': + dependencies: + react: 18.3.1 + + '@rolldown/pluginutils@1.0.0-beta.27': {} + + '@rollup/rollup-android-arm-eabi@4.60.4': + optional: true + + '@rollup/rollup-android-arm64@4.60.4': + optional: true + + '@rollup/rollup-darwin-arm64@4.60.4': + optional: true + + '@rollup/rollup-darwin-x64@4.60.4': + optional: true + + '@rollup/rollup-freebsd-arm64@4.60.4': + optional: true + + '@rollup/rollup-freebsd-x64@4.60.4': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.60.4': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.60.4': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.60.4': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.60.4': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.60.4': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.60.4': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-x64-musl@4.60.4': + optional: true + + '@rollup/rollup-openbsd-x64@4.60.4': + optional: true + + '@rollup/rollup-openharmony-arm64@4.60.4': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.60.4': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.60.4': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.60.4': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.60.4': + optional: true + + '@tabler/icons-react@3.44.0(react@18.3.1)': + dependencies: + '@tabler/icons': 3.44.0 + react: 18.3.1 + + '@tabler/icons@3.44.0': {} + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.29.0 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.29.0 + + '@types/estree@1.0.8': {} + + '@types/prop-types@15.7.15': {} + + '@types/react-dom@18.3.7(@types/react@18.3.29)': + dependencies: + '@types/react': 18.3.29 + + '@types/react@18.3.29': + dependencies: + '@types/prop-types': 15.7.15 + csstype: 3.2.3 + + '@vitejs/plugin-react@4.7.0(vite@5.4.21)': + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.0) + '@rolldown/pluginutils': 1.0.0-beta.27 + '@types/babel__core': 7.20.5 + react-refresh: 0.17.0 + vite: 5.4.21 + transitivePeerDependencies: + - supports-color + + baseline-browser-mapping@2.10.32: {} + + browserslist@4.28.2: + dependencies: + baseline-browser-mapping: 2.10.32 + caniuse-lite: 1.0.30001793 + electron-to-chromium: 1.5.361 + node-releases: 2.0.46 + update-browserslist-db: 1.2.3(browserslist@4.28.2) + + caniuse-lite@1.0.30001793: {} + + clsx@2.1.1: {} + + convert-source-map@2.0.0: {} + + csstype@3.2.3: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + detect-node-es@1.1.0: {} + + dom-helpers@5.2.1: + dependencies: + '@babel/runtime': 7.29.2 + csstype: 3.2.3 + + electron-to-chromium@1.5.361: {} + + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + + escalade@3.2.0: {} + + fsevents@2.3.3: + optional: true + + gensync@1.0.0-beta.2: {} + + get-nonce@1.0.1: {} + + js-tokens@4.0.0: {} + + jsesc@3.1.0: {} + + json5@2.2.3: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + ms@2.1.3: {} + + nanoid@3.3.12: {} + + node-releases@2.0.46: {} + + object-assign@4.1.1: {} + + picocolors@1.1.1: {} + + postcss@8.5.15: + dependencies: + nanoid: 3.3.12 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + + react-dom@18.3.1(react@18.3.1): + dependencies: + loose-envify: 1.4.0 + react: 18.3.1 + scheduler: 0.23.2 + + react-is@16.13.1: {} + + react-number-format@5.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + react-refresh@0.17.0: {} + + react-remove-scroll-bar@2.3.8(@types/react@18.3.29)(react@18.3.1): + dependencies: + react: 18.3.1 + react-style-singleton: 2.2.3(@types/react@18.3.29)(react@18.3.1) + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.29 + + react-remove-scroll@2.7.2(@types/react@18.3.29)(react@18.3.1): + dependencies: + react: 18.3.1 + react-remove-scroll-bar: 2.3.8(@types/react@18.3.29)(react@18.3.1) + react-style-singleton: 2.2.3(@types/react@18.3.29)(react@18.3.1) + tslib: 2.8.1 + use-callback-ref: 1.3.3(@types/react@18.3.29)(react@18.3.1) + use-sidecar: 1.1.3(@types/react@18.3.29)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.29 + + react-style-singleton@2.2.3(@types/react@18.3.29)(react@18.3.1): + dependencies: + get-nonce: 1.0.1 + react: 18.3.1 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.29 + + react-textarea-autosize@8.5.9(@types/react@18.3.29)(react@18.3.1): + dependencies: + '@babel/runtime': 7.29.2 + react: 18.3.1 + use-composed-ref: 1.4.0(@types/react@18.3.29)(react@18.3.1) + use-latest: 1.3.0(@types/react@18.3.29)(react@18.3.1) + transitivePeerDependencies: + - '@types/react' + + react-transition-group@4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.29.2 + dom-helpers: 5.2.1 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + react@18.3.1: + dependencies: + loose-envify: 1.4.0 + + rollup@4.60.4: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.60.4 + '@rollup/rollup-android-arm64': 4.60.4 + '@rollup/rollup-darwin-arm64': 4.60.4 + '@rollup/rollup-darwin-x64': 4.60.4 + '@rollup/rollup-freebsd-arm64': 4.60.4 + '@rollup/rollup-freebsd-x64': 4.60.4 + '@rollup/rollup-linux-arm-gnueabihf': 4.60.4 + '@rollup/rollup-linux-arm-musleabihf': 4.60.4 + '@rollup/rollup-linux-arm64-gnu': 4.60.4 + '@rollup/rollup-linux-arm64-musl': 4.60.4 + '@rollup/rollup-linux-loong64-gnu': 4.60.4 + '@rollup/rollup-linux-loong64-musl': 4.60.4 + '@rollup/rollup-linux-ppc64-gnu': 4.60.4 + '@rollup/rollup-linux-ppc64-musl': 4.60.4 + '@rollup/rollup-linux-riscv64-gnu': 4.60.4 + '@rollup/rollup-linux-riscv64-musl': 4.60.4 + '@rollup/rollup-linux-s390x-gnu': 4.60.4 + '@rollup/rollup-linux-x64-gnu': 4.60.4 + '@rollup/rollup-linux-x64-musl': 4.60.4 + '@rollup/rollup-openbsd-x64': 4.60.4 + '@rollup/rollup-openharmony-arm64': 4.60.4 + '@rollup/rollup-win32-arm64-msvc': 4.60.4 + '@rollup/rollup-win32-ia32-msvc': 4.60.4 + '@rollup/rollup-win32-x64-gnu': 4.60.4 + '@rollup/rollup-win32-x64-msvc': 4.60.4 + fsevents: 2.3.3 + + scheduler@0.23.2: + dependencies: + loose-envify: 1.4.0 + + semver@6.3.1: {} + + source-map-js@1.2.1: {} + + tabbable@6.4.0: {} + + tslib@2.8.1: {} + + type-fest@4.41.0: {} + + typescript@5.9.3: {} + + update-browserslist-db@1.2.3(browserslist@4.28.2): + dependencies: + browserslist: 4.28.2 + escalade: 3.2.0 + picocolors: 1.1.1 + + use-callback-ref@1.3.3(@types/react@18.3.29)(react@18.3.1): + dependencies: + react: 18.3.1 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.29 + + use-composed-ref@1.4.0(@types/react@18.3.29)(react@18.3.1): + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.29 + + use-isomorphic-layout-effect@1.2.1(@types/react@18.3.29)(react@18.3.1): + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.29 + + use-latest@1.3.0(@types/react@18.3.29)(react@18.3.1): + dependencies: + react: 18.3.1 + use-isomorphic-layout-effect: 1.2.1(@types/react@18.3.29)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.29 + + use-sidecar@1.1.3(@types/react@18.3.29)(react@18.3.1): + dependencies: + detect-node-es: 1.1.0 + react: 18.3.1 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.29 + + vite@5.4.21: + dependencies: + esbuild: 0.21.5 + postcss: 8.5.15 + rollup: 4.60.4 + optionalDependencies: + fsevents: 2.3.3 + + yallist@3.1.1: {} diff --git a/frontend/src/AdminTokenModal.tsx b/frontend/src/AdminTokenModal.tsx new file mode 100644 index 0000000..a2157be --- /dev/null +++ b/frontend/src/AdminTokenModal.tsx @@ -0,0 +1,96 @@ +import { useState } from "react"; +import { + Alert, + Button, + Code, + Group, + Modal, + PasswordInput, + Stack, + Text, +} from "@mantine/core"; +import { IconAlertCircle, IconShieldCheck } from "@tabler/icons-react"; +import { notifications } from "@mantine/notifications"; +import { SetAdminToken } from "../wailsjs/go/main/AdminService"; + +interface Props { + opened: boolean; + onClose: () => void; + onSaved: () => void; +} + +export default function AdminTokenModal({ opened, onClose, onSaved }: Props) { + const [token, setToken] = useState(""); + const [busy, setBusy] = useState(false); + const [error, setError] = useState(null); + + async function handleSave() { + setBusy(true); + setError(null); + try { + await SetAdminToken(token); + notifications.show({ + title: "Admin token saved", + message: "Admin API validated successfully.", + color: "green", + }); + setToken(""); + onSaved(); + } catch (e: any) { + setError(String(e?.message ?? e)); + } finally { + setBusy(false); + } + } + + return ( + + + Synapse admin token + + } + centered + size="lg" + withCloseButton={false} + closeOnEscape={false} + closeOnClickOutside={false} + > + + + MAS aun no expone scope admin para la Synapse Admin API. Pega aqui el{" "} + access_token de un usuario con admin: true{" "} + (obtenible via Synapse legacy login o el .env del VPS). + + setToken(e.currentTarget.value)} + autoFocus + /> + {error && ( + } color="red" variant="light"> + {error} + + )} + + + + + + + ); +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..4cdd2bd --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,91 @@ +import { useEffect, useState } from "react"; +import { Box, LoadingOverlay } from "@mantine/core"; +import LoginScreen from "./LoginScreen"; +import HomeScreen from "./HomeScreen"; +import AdminTokenModal from "./AdminTokenModal"; +import { GetSession } from "../wailsjs/go/main/AdminService"; + +const LAST_USER_KEY = "matrix_admin_panel.last_user_id"; + +interface Session { + user_id: string; + homeserver_url: string; + has_oidc_token: boolean; + has_admin_token: boolean; + expires_at?: string; +} + +export default function App() { + const [userID, setUserID] = useState(null); + const [session, setSession] = useState(null); + const [loading, setLoading] = useState(true); + const [tokenModalOpen, setTokenModalOpen] = useState(false); + + useEffect(() => { + const last = localStorage.getItem(LAST_USER_KEY); + if (!last) { + setLoading(false); + return; + } + GetSession(last) + .then((s) => { + const sess = s as Session | null; + if (sess && sess.has_oidc_token) { + setUserID(sess.user_id); + setSession(sess); + if (!sess.has_admin_token) { + setTokenModalOpen(true); + } + } + }) + .finally(() => setLoading(false)); + }, []); + + async function refreshSession(uid: string) { + const s = (await GetSession(uid)) as Session | null; + if (s) setSession(s); + } + + const handleLogin = async (uid: string) => { + localStorage.setItem(LAST_USER_KEY, uid); + setUserID(uid); + await refreshSession(uid); + // After OIDC login, ALWAYS prompt for admin token unless already saved. + const s = (await GetSession(uid)) as Session | null; + if (s && !s.has_admin_token) setTokenModalOpen(true); + }; + + const handleLogout = () => { + localStorage.removeItem(LAST_USER_KEY); + setUserID(null); + setSession(null); + }; + + const handleTokenSaved = async () => { + setTokenModalOpen(false); + if (userID) await refreshSession(userID); + }; + + return ( + + + {userID ? ( + <> + setTokenModalOpen(true)} + /> + setTokenModalOpen(false)} + onSaved={handleTokenSaved} + /> + + ) : ( + + )} + + ); +} diff --git a/frontend/src/HomeScreen.tsx b/frontend/src/HomeScreen.tsx new file mode 100644 index 0000000..d225d45 --- /dev/null +++ b/frontend/src/HomeScreen.tsx @@ -0,0 +1,138 @@ +import { useState } from "react"; +import { + AppShell, + Badge, + Box, + Button, + Group, + NavLink, + Text, + Tooltip, +} from "@mantine/core"; +import { + IconLogout, + IconUsers, + IconBuildingCommunity, + IconDeviceMobile, + IconShieldCheck, + IconShieldOff, +} from "@tabler/icons-react"; +import { Logout } from "../wailsjs/go/main/AdminService"; +import UsersTab from "./UsersTab"; +import RoomsTab from "./RoomsTab"; +import SessionsTab from "./SessionsTab"; + +interface Session { + user_id: string; + homeserver_url: string; + has_oidc_token: boolean; + has_admin_token: boolean; + expires_at?: string; +} + +type Tab = "users" | "rooms" | "sessions"; + +interface Props { + userID: string; + session: Session | null; + onLogout: () => void; + onRequestAdminToken: () => void; +} + +export default function HomeScreen({ + userID, + session, + onLogout, + onRequestAdminToken, +}: Props) { + const [tab, setTab] = useState("users"); + + async function handleLogout() { + try { + await Logout(userID); + } finally { + onLogout(); + } + } + + const hasAdmin = !!session?.has_admin_token; + + return ( + + + + + + matrix_admin_panel + + v0.1.0 + + + + + + {userID} + + + {hasAdmin ? ( + }> + Admin + + ) : ( + + )} + + + + + + + } + active={tab === "users"} + onClick={() => setTab("users")} + /> + } + active={tab === "rooms"} + onClick={() => setTab("rooms")} + /> + } + active={tab === "sessions"} + onClick={() => setTab("sessions")} + /> + + + + + {tab === "users" && } + {tab === "rooms" && } + {tab === "sessions" && } + + + + ); +} diff --git a/frontend/src/LoginScreen.tsx b/frontend/src/LoginScreen.tsx new file mode 100644 index 0000000..3f1b39a --- /dev/null +++ b/frontend/src/LoginScreen.tsx @@ -0,0 +1,74 @@ +import { useState } from "react"; +import { + Button, + Card, + Center, + Stack, + Text, + Title, + Code, + Alert, +} from "@mantine/core"; +import { IconShieldCheck, IconAlertCircle } from "@tabler/icons-react"; +import { Login } from "../wailsjs/go/main/AdminService"; + +export default function LoginScreen({ onLogin }: { onLogin: (uid: string) => void }) { + const [busy, setBusy] = useState(false); + const [error, setError] = useState(null); + + async function handleClick() { + setBusy(true); + setError(null); + try { + const uid = await Login(); + onLogin(uid); + } catch (e: any) { + setError(String(e?.message ?? e)); + } finally { + setBusy(false); + } + } + + return ( +
+ + + + + matrix_admin_panel + + Synapse admin (Wails + React + Mantine) + + + + Inicia sesion en matrix-af2f3d.organic-machine.com via + Matrix Authentication Service. Tras login, se pedira un{" "} + admin_token Synapse adicional. + + {error && ( + } + color="red" + variant="light" + w="100%" + > + {error} + + )} + + + v0.1.0 (issue 0163) + + + +
+ ); +} diff --git a/frontend/src/RoomsTab.tsx b/frontend/src/RoomsTab.tsx new file mode 100644 index 0000000..d62756d --- /dev/null +++ b/frontend/src/RoomsTab.tsx @@ -0,0 +1,316 @@ +import { useEffect, useState } from "react"; +import { + ActionIcon, + Alert, + Badge, + Box, + Button, + Checkbox, + Code, + Group, + LoadingOverlay, + Menu, + Modal, + Stack, + Table, + Text, + Textarea, + TextInput, + Title, +} from "@mantine/core"; +import { + IconAlertCircle, + IconDots, + IconRefresh, + IconSearch, + IconTrash, +} from "@tabler/icons-react"; +import { notifications } from "@mantine/notifications"; +import { ListRooms, DeleteRoom } from "../wailsjs/go/main/AdminService"; + +interface AdminRoom { + room_id: string; + name: string; + canonical_alias: string; + joined_members: number; + joined_local: number; + version: string; + encrypted: boolean; + federatable: boolean; + public: boolean; +} + +interface ListResult { + rooms: AdminRoom[]; + total_count: number; + next_token: number; +} + +export default function RoomsTab({ hasAdminToken }: { hasAdminToken: boolean }) { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const [search, setSearch] = useState(""); + const [from, setFrom] = useState(0); + const limit = 50; + + // Delete modal + const [deleteRoom, setDeleteRoom] = useState(null); + const [reason, setReason] = useState(""); + const [purge, setPurge] = useState(true); + const [block, setBlock] = useState(false); + + async function load() { + if (!hasAdminToken) return; + setLoading(true); + setError(null); + try { + const res = (await ListRooms(from, limit, search)) as unknown as ListResult; + setData(res); + } catch (e: any) { + setError(String(e?.message ?? e)); + } finally { + setLoading(false); + } + } + + useEffect(() => { + if (hasAdminToken) load(); + + }, [hasAdminToken, from]); + + function applySearch() { + setFrom(0); + load(); + } + + async function handleDelete() { + if (!deleteRoom) return; + try { + const id = await DeleteRoom(deleteRoom.room_id, reason, purge, block); + notifications.show({ + title: "Room delete scheduled", + message: `delete_id: ${id}`, + color: "green", + }); + setDeleteRoom(null); + setReason(""); + setPurge(true); + setBlock(false); + load(); + } catch (e: any) { + notifications.show({ + title: "Delete failed", + message: String(e?.message ?? e), + color: "red", + }); + } + } + + if (!hasAdminToken) { + return ( + }> + Admin token not set. Click "Set admin token" on the top bar to enable + room management. + + ); + } + + return ( + + + + + Rooms + + + + + setSearch(e.currentTarget.value)} + onKeyDown={(e) => { + if (e.key === "Enter") applySearch(); + }} + leftSection={} + w={280} + /> + + + + {error && ( + }> + {error} + + )} + + + + + + Room ID + Name + Members + Encrypted + Public + + + + + {data?.rooms?.map((r) => ( + + + {r.room_id} + + {r.name || r.canonical_alias || "-"} + + + {r.joined_members}{" "} + + ({r.joined_local} local) + + + + + {r.encrypted ? ( + + E2EE + + ) : ( + + no + + )} + + + {r.public ? ( + + public + + ) : ( + + private + + )} + + + + + + + + + + } + onClick={() => setDeleteRoom(r)} + > + Delete room + + + + + + ))} + {!data?.rooms?.length && ( + + + + No rooms. + + + + )} + +
+
+ + + + Total: {data?.total_count ?? 0} · Showing from {from} + + + + + + +
+ + { + setDeleteRoom(null); + setReason(""); + }} + title="Delete room" + centered + > + + + About to delete {deleteRoom?.room_id}. This is + asynchronous. + +