diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..08d5934d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,42 @@ +.git +.github +.codex-tmp +.gocache +.tdesktop-e2e +.vscode +.idea + +# Local builds, caches, and runtime state. +bin +dist +logs +tmp +coverage.* +*.exe +*.test +*.out +**/node_modules +**/__pycache__ +**/*.pyc + +# Deployment credentials stay out of the build context. The explicitly +# published test RSA fixture below is the only private-key exception. +.env +.env.* +**/.env +**/.env.* +codex.local +secrets +**/secrets +*.pem +*.key + +# Runtime data is excluded except for the tracked language-pack seed. +data/* +!data/langpack/ +!data/langpack/** + +# Deployment-local state and overrides. +deploy/docker/.env +deploy/docker/backups +deploy/docker/overrides diff --git a/.env.example b/.env.example index c2d07bff..352fa895 100644 --- a/.env.example +++ b/.env.example @@ -40,6 +40,24 @@ TELESRV_PHONE_CODE_LENGTH=5 TELESRV_AUTH_CODE_TTL=5m # How many wrong guesses are allowed before a code is rejected outright. TELESRV_AUTH_CODE_MAX_ATTEMPTS=5 +# The message sent from the official system account (777000) into a user's +# own chat on every completed sign-in -- a lightweight security notice, not +# the login code itself. {{server_name}} is replaced with the server's +# configured identity name (or product name if unset). Leave empty to use +# the built-in English copy. The admin panel's Server Settings page can +# override these live, without a restart; these env vars are only the +# fallback for when it hasn't been touched. +TELESRV_WELCOME_MESSAGE_PHONE_TEMPLATE= +TELESRV_WELCOME_MESSAGE_EMAIL_TEMPLATE= +# The message sent from the official system account (777000) that carries the +# actual login code -- one template for every delivery channel (SMS or +# email). Must contain the {{code}} placeholder exactly once (that's where +# the real code is inserted and bolded); {{server_name}} is optional and may +# appear any number of times. Leave empty to use the built-in English copy. +# The admin panel's Server Settings page can override this live, without a +# restart (rejecting a save that doesn't contain {{code}} exactly once); this +# env var is only the fallback for when it hasn't been touched. +TELESRV_LOGIN_CODE_MESSAGE_TEMPLATE= # Where webhook-delivered codes are POSTed, and the shared secret used to # sign that request (see docs/otp-delivery.md for the exact payload). TELESRV_OTP_WEBHOOK_URL= @@ -82,17 +100,24 @@ TELESRV_SMTP_TIMEOUT=10s ## Public Links & Branding -- What clients show/open for links, and your product's name. # Public web address for links this server generates (invite links, sticker -# packs, etc). Use your real domain once you have one, e.g. https://example.com. -TELESRV_PUBLIC_BASE_URL=http://127.0.0.1 +# packs, etc). :2401 matches TELESRV_PUBLIC_LINK_WEB_ADDR below (the Public +# Web Listener that actually serves those preview pages) so a link opened +# right after setup shows a real preview card instead of a dead connection. +# Use your real domain once you have one, e.g. https://example.com -- and +# put nginx (or similar) in front of the listener rather than pointing this +# at the raw port. +TELESRV_PUBLIC_BASE_URL=http://127.0.0.1:2401 # Custom URL scheme (like "owpg://") that public pages use to open your # patched client. Must match what your client builds were compiled with. TELESRV_PUBLIC_APP_SCHEME=owpg # Optional: use "scheme://yourdomain.com/..." links instead of plain # "scheme://...". Leave empty unless you specifically need this. TELESRV_PUBLIC_APP_LINK_BASE= -# Address of your web client (if you have one) and the product name shown -# on public landing pages. -TELESRV_PUBLIC_WEB_BASE_URL=https://web.telesrv.net +# Address of your web client, if you have one -- shows an "Open in Web" +# button on public profile/invite pages. Leave empty (the default) to hide +# that button; there's no web client running unless you've deployed one. +TELESRV_PUBLIC_WEB_BASE_URL= +# Product name shown on public landing pages. TELESRV_PUBLIC_APP_NAME=OwpenGram # Where the "Download" button on public pages links to. TELESRV_PUBLIC_DOWNLOAD_URL=https://owpengram.org @@ -100,6 +125,11 @@ TELESRV_PUBLIC_DOWNLOAD_URL=https://owpengram.org # scam/fake. Leave empty to use the built-in English text. TELESRV_SCAM_WARNING= TELESRV_FAKE_WARNING= +# Usernames a user/channel can never self-service claim (account.updateUsername, +# channels.updateUsername) -- brand-adjacent or staff-sounding words, plus +# your own real handle if you want it protected too. Comma-separated, not +# case-sensitive. The admin panel can still assign any of these on purpose. +TELESRV_RESERVED_USERNAMES=owpengram,admin,administrator,support,staff,moderator,official,root,owner ## Admin Panel -- Login and access for the web-based admin dashboard. @@ -117,8 +147,13 @@ TELESRV_ADMIN_SESSION_KEY= # the admin panel entirely; set to a loopback address (127.0.0.1:...) to # enable it without exposing it outside this machine. TELESRV_ADMIN_API_ADDR= -# Address the admin panel's own web UI listens on. -TELESRV_ADMIN_UI_ADDR=127.0.0.1:2600 +# Address the admin panel's own web UI listens on. 0.0.0.0 so a fresh +# install is reachable right away from wherever you're setting it up from -- +# a VPS you're provisioning from across the world included -- without an SSH +# tunnel just to see the first-run wizard. Narrow it to a loopback or LAN +# address once you're done if you'd rather it not be open to the internet; +# the login itself still needs the password (or token) below either way. +TELESRV_ADMIN_UI_ADDR=0.0.0.0:2600 # Permissions granted to an Admin UI session that logged in with # TELESRV_ADMIN_UI_PASSWORD / _TOKEN. Comma-separated; "*" means every # permission and is the default, so enabling RBAC never locks an operator out of @@ -269,14 +304,63 @@ TELESRV_S3_USE_SSL=false # MinIO needs this on (bucket in the URL path); AWS S3 does not. TELESRV_S3_PATH_STYLE=true # Reject new uploads once storage is nearly full, instead of letting the disk -# fill up. Thresholds live in the Advanced section below. +# fill up. Thresholds are the three fields right below. TELESRV_STORAGE_LOW_SPACE_GUARD_ENABLE=true -# Automatically delete old media once it's no longer referenced by any -# message, profile photo, or sticker set (never deletes media still visible -# in a conversation). Off by default -- storage usage is tracked and shown -# in the admin panel either way; enable this once you're comfortable with -# what it will reclaim. Retention age lives in the Advanced section below. -TELESRV_STORAGE_RETENTION_ENABLE=false +# localfs: reject new uploads once real free disk bytes fall below this; <=0 disables. +TELESRV_STORAGE_MIN_FREE_BYTES=1073741824 +# Reject new uploads once total tracked blob bytes would exceed this. The only +# meaningful "low space" signal on the s3 backend (no OS free-space concept); +# optional soft cap on localfs too. <=0 disables. +TELESRV_STORAGE_MAX_TOTAL_BYTES=0 +# Reject a single upload once its total assembled size (sum of all its parts) +# would exceed this. <=0 disables this check (the protocol's own part-count +# ceiling of ~4GB still applies). Must not exceed that ceiling. +TELESRV_STORAGE_MAX_UPLOAD_FILE_BYTES=0 +# Storage retention sweep mode: "off" (default, nothing auto-deleted -- +# storage usage is still tracked and shown in the admin panel either way), +# "orphan" (safe: deletes a document/photo's blob only once it's no longer +# referenced by any message/profile photo/sticker set), or "hard" +# (aggressive: deletes a document/photo's blob once it's old enough, +# REGARDLESS of whether it's still referenced -- old media in active +# conversations will show as unavailable). +TELESRV_STORAGE_RETENTION_MODE=off +# How long a document/photo must have had zero references before the sweep +# deletes it in "orphan" mode above -- or how old the media itself is before +# "hard" mode deletes its bytes regardless of references. Ignored when the +# mode above is "off". The sweep itself runs alongside every other retention +# check on the shared TELESRV_RETENTION_INTERVAL/TELESRV_RETENTION_BATCH +# cadence (Advanced section below). +TELESRV_STORAGE_RETENTION_MAX_AGE=720h +# Optional per-category overrides of the shared age above (Photo/Video/Round +# Video/Gif/Music/Voice/File/Avatar) -- each empty/unset value falls back to +# TELESRV_STORAGE_RETENTION_MAX_AGE. The mode switch above still applies to +# all of them; these only let one category expire sooner or later than the +# rest (e.g. purge voice notes after a week but keep files for a year). +TELESRV_STORAGE_RETENTION_MAX_AGE_PHOTO= +TELESRV_STORAGE_RETENTION_MAX_AGE_VIDEO= +TELESRV_STORAGE_RETENTION_MAX_AGE_ROUND_VIDEO= +TELESRV_STORAGE_RETENTION_MAX_AGE_GIF= +TELESRV_STORAGE_RETENTION_MAX_AGE_MUSIC= +TELESRV_STORAGE_RETENTION_MAX_AGE_VOICE= +TELESRV_STORAGE_RETENTION_MAX_AGE_FILE= +TELESRV_STORAGE_RETENTION_MAX_AGE_AVATAR= +# Once enabled, actively reclaims space once total physical storage exceeds +# TELESRV_STORAGE_MAX_TOTAL_BYTES above: the oldest files (regardless of +# category/age) are purged the same way "hard" retention mode purges blob +# bytes, until back under budget. Independent of the retention mode switch +# above -- can run even when that's "off". Default false: TELESRV_STORAGE_MAX_ +# TOTAL_BYTES otherwise only ever blocks new uploads, never reclaims from +# existing ones. +TELESRV_STORAGE_EVICTION_ENABLE=false +# Secret-chat encrypted files (photos/documents sent in a secret chat) are +# opaque ciphertext the server can't inspect -- none of the retention/ +# eviction settings above ever touch them, so they otherwise accumulate +# forever. Secret chats are single-device on both ends (no multi-device +# sync), so once the one recipient device that will ever ask for a file has +# downloaded it in full, the server has no further reason to keep it. +# Default true: set to false if you'd rather keep the ciphertext around +# regardless (it still can't be read without the secret chat's own key). +TELESRV_SECRET_CHAT_DELETE_FILE_AFTER_DOWNLOAD=true # ============================================================================== @@ -312,7 +396,7 @@ TELESRV_MTPROTO_RPC_MAX_INFLIGHT=32 TELESRV_MTPROTO_RPC_QUEUE_SIZE=64 TELESRV_MTPROTO_RPC_TIMEOUT=30s TELESRV_MTPROTO_RPC_GLOBAL_WORKERS=256 -TELESRV_MTPROTO_RPC_GLOBAL_MAX_TASKS=8192 +TELESRV_MTPROTO_RPC_GLOBAL_MAX_TASKS=32768 TELESRV_MTPROTO_RPC_GLOBAL_MAX_BYTES=536870912 # Metadata-only rpc_result receipt budgets: global >= auth >= session. ACK deletes immediately; # 331s is only the no-ACK horizon. Payloads live solely in the logical-session outbound budget. @@ -410,23 +494,14 @@ TELESRV_DEFAULT_STICKER_SET_ID=0 # imported once (matched by filename); renaming a file re-imports it as a new # entry. Missing directory is skipped, not an error. TELESRV_GIF_SEED_DIR=data/gifs +# Admin-editable server name/description/icon (Server Settings in the admin +# panel), served over /owpengram/server-info + /owpengram/server-icon and +# read fresh on every request -- editing them takes effect with no restart. +TELESRV_IDENTITY_DIR=data/identity -# Storage low-space guard thresholds (master toggle is TELESRV_STORAGE_LOW_SPACE_GUARD_ENABLE above). -# localfs: reject new uploads once real free disk bytes fall below this; <=0 disables. -TELESRV_STORAGE_MIN_FREE_BYTES=1073741824 -# Reject new uploads once total tracked blob bytes would exceed this. The only -# meaningful "low space" signal on the s3 backend (no OS free-space concept); -# optional soft cap on localfs too. <=0 disables. -TELESRV_STORAGE_MAX_TOTAL_BYTES=0 -# How often the cached free-space/usage gauge behind the guard above refreshes. +# How often the cached free-space/usage gauge behind the low-space guard +# (thresholds now live in the Storage & Media section above) refreshes. TELESRV_STORAGE_USAGE_REFRESH_INTERVAL=1m -# Storage retention sweep tuning (master toggle is TELESRV_STORAGE_RETENTION_ENABLE above). -# How long a document/photo must have had zero references before the sweep -# deletes it -- not how old the media itself is, and it never touches media -# still referenced by a live message/profile-photo/sticker-set. The sweep -# itself runs alongside every other retention check on the shared -# TELESRV_RETENTION_INTERVAL/TELESRV_RETENTION_BATCH cadence above. -TELESRV_STORAGE_RETENTION_MAX_AGE=720h # New-account perks: free Telegram Premium months. TELESRV_PREMIUM_GRANT_MONTHS=3 @@ -465,6 +540,10 @@ TELESRV_VERIFICATION_BOT_RATE_WINDOW=1m # rows. Interval must be positive; batch must be 1..500. TELESRV_VERIFICATION_NOTIFY_INTERVAL=15s TELESRV_VERIFICATION_NOTIFY_BATCH=50 +TELESRV_BROADCAST_WORKER_INTERVAL=3s +TELESRV_BROADCAST_WORKER_LEASE=30s +TELESRV_BROADCAST_MATERIALIZE_BATCH=200 +TELESRV_BROADCAST_DELIVERY_BATCH=50 # Applications one applicant may keep open at once; 0 disables the cap, maximum # is 50. TELESRV_VERIFICATION_MAX_ACTIVE_PER_USER=3 diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..8d9b02e2 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,15 @@ +# These are supported funding model platforms + +github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +patreon: owpengram +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry +polar: # Replace with a single Polar username +buy_me_a_coffee: # Replace with a single Buy Me a Coffee username +thanks_dev: # Replace with a single thanks.dev username +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..ef2c0c1e --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,130 @@ +name: Build and Release + +on: + workflow_dispatch: + push: + tags: + - 'v*' + +permissions: + contents: read + +concurrency: + group: build-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + +jobs: + ci: + name: CI + uses: ./.github/workflows/ci.yml + + build: + name: Build ${{ matrix.goos }}/${{ matrix.goarch }} + runs-on: ubuntu-24.04 + timeout-minutes: 30 + + strategy: + fail-fast: false + matrix: + include: + - goos: linux + goarch: amd64 + - goos: linux + goarch: arm64 + - goos: windows + goarch: amd64 + - goos: windows + goarch: arm64 + + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Set up Go + uses: actions/setup-go@v7 + with: + go-version-file: go.mod + cache-dependency-path: go.sum + + - name: Set up Node + uses: actions/setup-node@v6 + with: + node-version: '22' + cache: npm + cache-dependency-path: cmd/telesrv-admin/web/package-lock.json + + - name: Build admin web assets + working-directory: cmd/telesrv-admin/web + run: | + npm ci + npm run build + + - name: Download Go modules + run: go mod download + + - name: Build binaries + env: + CGO_ENABLED: '0' + GOOS: ${{ matrix.goos }} + GOARCH: ${{ matrix.goarch }} + run: | + mkdir -p dist + + suffix="" + if [ "${GOOS}" = "windows" ]; then + suffix=".exe" + fi + + go build \ + -trimpath \ + -ldflags="-s -w" \ + -o "dist/gramsrv-${GOOS}-${GOARCH}${suffix}" \ + ./cmd/telesrv + + go build \ + -trimpath \ + -ldflags="-s -w" \ + -o "dist/gramsrv-admin-${GOOS}-${GOARCH}${suffix}" \ + ./cmd/telesrv-admin + + - name: Upload build artifact + uses: actions/upload-artifact@v4 + with: + name: gramsrv-${{ matrix.goos }}-${{ matrix.goarch }} + path: dist/* + if-no-files-found: error + retention-days: 7 + + release: + name: Publish GitHub Release + needs: + - ci + - build + if: startsWith(github.ref, 'refs/tags/v') + runs-on: ubuntu-24.04 + timeout-minutes: 15 + + permissions: + contents: write + + steps: + - name: Download build artifacts + uses: actions/download-artifact@v4 + with: + pattern: gramsrv-* + path: dist + merge-multiple: true + + - name: Generate combined checksums + working-directory: dist + run: | + sha256sum gramsrv-* > SHA256SUMS + + - name: Publish GitHub Release + env: + GH_TOKEN: ${{ github.token }} + run: | + gh release create "${{ github.ref_name }}" \ + dist/* \ + --title "${{ github.ref_name }}" \ + --generate-notes diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..235c2dbb --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,180 @@ +name: CI + +on: + push: + branches: + - main + pull_request: + workflow_dispatch: + workflow_call: + +permissions: + contents: read + +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + go-test: + name: Go tests + runs-on: ubuntu-24.04 + timeout-minutes: 45 + + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Set up Go + uses: actions/setup-go@v7 + with: + go-version-file: go.mod + cache-dependency-path: go.sum + + - name: Download Go modules + run: go mod download + + - name: Test + run: go test ./... -count=1 + + admin-web: + name: Admin web build + runs-on: ubuntu-24.04 + timeout-minutes: 15 + + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Set up Node + uses: actions/setup-node@v6 + with: + node-version: '22' + cache: npm + cache-dependency-path: cmd/telesrv-admin/web/package-lock.json + + - name: Install dependencies + working-directory: cmd/telesrv-admin/web + run: npm ci + + - name: Build + working-directory: cmd/telesrv-admin/web + run: npm run build + + grammystore: + name: Grammy store bot + runs-on: ubuntu-24.04 + timeout-minutes: 15 + + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Set up Node + uses: actions/setup-node@v6 + with: + node-version: '22' + cache: npm + cache-dependency-path: cmd/bots/grammystore/package-lock.json + + - name: Install dependencies + working-directory: cmd/bots/grammystore + run: npm ci + + - name: Check syntax + working-directory: cmd/bots/grammystore + run: npm run check + + - name: Test + working-directory: cmd/bots/grammystore + run: npm test + + docker-smoke: + name: Docker main topology smoke + runs-on: ubuntu-24.04 + timeout-minutes: 30 + + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Generate isolated environment + run: ./scripts/new-docker-env.sh --advertise-ip 127.0.0.1 + + - name: Validate deployment inputs + run: | + docker compose version + sh -n scripts/new-docker-env.sh + bash -n scripts/start-docker.sh + sh -n deploy/docker/docker-entrypoint.sh + compose=(docker compose -p telesrv-main-ci --project-directory deploy/docker --env-file deploy/docker/.env -f deploy/docker/compose.yaml) + bridge=("${compose[@]}" -f deploy/docker/compose.bridge-network.yaml) + "${compose[@]}" config --quiet + "${compose[@]}" config --format json | python3 -c 'import json,sys; s=json.load(sys.stdin)["services"]; assert s["server"].get("network_mode") == "host"; assert s["admin"].get("network_mode") == "host"; assert len(s["server"].get("ports", [])) == 0; assert len(s["admin"].get("ports", [])) == 0; assert str(s["server"]["environment"]["TELESRV_TURN_RELAY_MAX_PORT"]) == "12999"' + "${bridge[@]}" config --quiet + "${bridge[@]}" config --format json | python3 -c 'import json,sys; c=json.load(sys.stdin); s=c["services"]; assert s["server"].get("network_mode") != "host"; assert s["admin"].get("network_mode") != "host"; assert len(s["server"].get("ports", [])) == 69; assert len(s["admin"].get("ports", [])) == 1; assert "admin_host_access" in s["admin"]["networks"]; assert not c["networks"]["admin_host_access"].get("internal", False); assert str(s["server"]["environment"]["TELESRV_TURN_RELAY_MAX_PORT"]) == "12563"' + + - name: Validate PowerShell launchers + shell: pwsh + run: | + $tokens = $null + $errors = $null + [void][System.Management.Automation.Language.Parser]::ParseFile("scripts/new-docker-env.ps1", [ref]$tokens, [ref]$errors) + if ($errors.Count -gt 0) { $errors | ForEach-Object { Write-Error $_ }; exit 1 } + $tokens = $null + $errors = $null + [void][System.Management.Automation.Language.Parser]::ParseFile("scripts/start-docker.ps1", [ref]$tokens, [ref]$errors) + if ($errors.Count -gt 0) { $errors | ForEach-Object { Write-Error $_ }; exit 1 } + + - name: Build application images + run: | + compose=(docker compose -p telesrv-main-ci --project-directory deploy/docker --env-file deploy/docker/.env -f deploy/docker/compose.yaml) + "${compose[@]}" build --pull server admin + + - name: Start and wait for readiness + run: docker compose -p telesrv-main-ci --project-directory deploy/docker --env-file deploy/docker/.env -f deploy/docker/compose.yaml up -d --no-build --wait --wait-timeout 600 + + - name: Verify runtime and media listeners + run: | + compose=(docker compose -p telesrv-main-ci --project-directory deploy/docker --env-file deploy/docker/.env -f deploy/docker/compose.yaml) + for service in server admin; do + container_id="$("${compose[@]}" ps --quiet "$service")" + test -n "$container_id" + test "$(docker inspect --format '{{.Config.User}}' "$container_id")" = "10001:10001" + test "$(docker inspect --format '{{.HostConfig.ReadonlyRootfs}}' "$container_id")" = "true" + test "$(docker inspect --format '{{json .HostConfig.CapDrop}}' "$container_id")" = '["ALL"]' + test "$(docker inspect --format '{{.HostConfig.PidsLimit}}' "$container_id")" = "1024" + test "$(docker inspect --format '{{json .HostConfig.SecurityOpt}}' "$container_id")" = '["no-new-privileges:true"]' + test "$(docker inspect --format '{{.HostConfig.NetworkMode}}' "$container_id")" = "host" + done + curl --fail --silent --show-error http://127.0.0.1:2401/healthz | grep -qx ok + curl --fail --silent --show-error http://127.0.0.1:2600/ >/dev/null + timeout 5 bash -c 'exec 3<>/dev/tcp/127.0.0.1/2400' + python3 - <<'PY' + import os + import socket + import struct + + transaction_id = os.urandom(12) + request = struct.pack("!HHI12s", 0x0001, 0, 0x2112A442, transaction_id) + with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as client: + client.settimeout(5) + client.sendto(request, ("127.0.0.1", 12400)) + response, _ = client.recvfrom(2048) + message_type, _, cookie = struct.unpack("!HHI", response[:8]) + assert message_type == 0x0101, hex(message_type) + assert cookie == 0x2112A442, hex(cookie) + assert response[8:20] == transaction_id + PY + server_logs="$("${compose[@]}" logs --no-color server)" + case "$server_logs" in *"sfu listening"*) ;; *) echo "Embedded SFU did not become ready" >&2; exit 1 ;; esac + case "$server_logs" in *"turn listening"*) ;; *) echo "Embedded TURN did not become ready" >&2; exit 1 ;; esac + case "$server_logs" in *"live stream rtmp ingest listening"*) ;; *) echo "RTMP listener did not become ready" >&2; exit 1 ;; esac + + - name: Show logs on failure + if: failure() + run: docker compose -p telesrv-main-ci --project-directory deploy/docker --env-file deploy/docker/.env -f deploy/docker/compose.yaml logs --no-color --tail 200 + + - name: Remove isolated stack + if: always() + run: docker compose -p telesrv-main-ci --project-directory deploy/docker --env-file deploy/docker/.env -f deploy/docker/compose.yaml down --volumes --remove-orphans diff --git a/.github/workflows/container-images.yml b/.github/workflows/container-images.yml new file mode 100644 index 00000000..748c97b3 --- /dev/null +++ b/.github/workflows/container-images.yml @@ -0,0 +1,76 @@ +name: Publish main container images (manual) + +on: + workflow_dispatch: + +permissions: + contents: read + packages: write + +concurrency: + group: containers-main-${{ github.ref }} + cancel-in-progress: true + +jobs: + publish: + name: Publish ${{ matrix.role }} + if: github.ref == 'refs/heads/main' + runs-on: ubuntu-24.04 + timeout-minutes: 45 + strategy: + fail-fast: false + matrix: + include: + - role: server + target: server-test + - role: admin + target: admin + + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Log in to GHCR + uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Generate image metadata + id: meta + uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7 + with: + images: ghcr.io/${{ github.repository }}/${{ matrix.role }} + tags: | + type=raw,value=main + type=sha,prefix=sha- + + - name: Set build date + id: build + shell: bash + run: echo "date=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> "$GITHUB_OUTPUT" + + - name: Build and publish + uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4 + with: + context: . + file: Dockerfile + target: ${{ matrix.target }} + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + build-args: | + VCS_REF=${{ github.sha }} + VCS_BRANCH=${{ github.ref_name }} + VCS_TREE_STATE=clean + BUILD_DATE=${{ steps.build.outputs.date }} + cache-from: type=gha,scope=main-${{ matrix.role }} + cache-to: type=gha,mode=max,scope=main-${{ matrix.role }} diff --git a/.gitignore b/.gitignore index bb719bb3..811658e3 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,9 @@ node_modules/ __pycache__/ *.py[cod] *$py.class +# install-prereqs.sh / owpengram-server.sh create this for the panel's packages. +# Untracked and unignored, it was one `git clean -fd` away from being deleted. +/.venv/ # 本地环境 / 密钥(server RSA private key 必须持久化,但禁止入库) .env @@ -35,6 +38,10 @@ tmp/ .db_naming .server_panel.json +# Local-only destructive dev tool (deletes .env, data/, Docker volumes) -- +# never belongs in the repo. +/wipe-server.bat + # IDE .idea/ .vscode/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..62012f90 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,91 @@ +# syntax=docker/dockerfile:1.7 + +ARG GO_IMAGE=golang:1.25-alpine@sha256:1ae0735f00daffa3aaf1363a5184c0d2dc55c78e3db4ec70241cdac97bf84b59 +ARG ALPINE_IMAGE=alpine:3.22@sha256:14358309a308569c32bdc37e2e0e9694be33a9d99e68afb0f5ff33cc1f695dce + +FROM --platform=$BUILDPLATFORM ${GO_IMAGE} AS build-base + +ARG TARGETOS +ARG TARGETARCH + +RUN apk add --no-cache ca-certificates git +WORKDIR /src + +COPY go.mod go.sum ./ +RUN --mount=type=cache,target=/go/pkg/mod go mod download + +COPY cmd/ ./cmd/ +COPY deploy/ ./deploy/ +COPY internal/ ./internal/ + +ENV CGO_ENABLED=0 + +FROM build-base AS build-server +ARG VCS_REF=unknown +ARG VCS_BRANCH=unknown +ARG VCS_TREE_STATE=unknown +ARG BUILD_DATE=unknown +RUN --mount=type=cache,target=/go/pkg/mod \ + --mount=type=cache,target=/root/.cache/go-build \ + GOOS=${TARGETOS} GOARCH=${TARGETARCH} \ + go build -trimpath \ + -ldflags="-s -w -X main.gitCommit=${VCS_REF} -X main.gitBranch=${VCS_BRANCH} -X main.gitTreeState=${VCS_TREE_STATE} -X main.buildTime=${BUILD_DATE}" \ + -o /out/telesrv ./cmd/telesrv + +FROM build-base AS build-admin +RUN apk add --no-cache nodejs npm +WORKDIR /src/cmd/telesrv-admin/web +RUN --mount=type=cache,target=/root/.npm npm ci && npm run build +WORKDIR /src +RUN --mount=type=cache,target=/go/pkg/mod \ + --mount=type=cache,target=/root/.cache/go-build \ + GOOS=${TARGETOS} GOARCH=${TARGETARCH} \ + go build -trimpath -ldflags="-s -w" -o /out/telesrv-admin ./cmd/telesrv-admin + +FROM ${ALPINE_IMAGE} AS runtime-base + +ARG VCS_REF=unknown +ARG BUILD_DATE=unknown + +LABEL org.opencontainers.image.title="gramsrv" \ + org.opencontainers.image.description="Telegram-like MTProto server" \ + org.opencontainers.image.source="https://github.com/iamxvbaba/gramsrv" \ + org.opencontainers.image.revision="${VCS_REF}" \ + org.opencontainers.image.created="${BUILD_DATE}" + +RUN apk add --no-cache ca-certificates tzdata \ + && addgroup -S -g 10001 telesrv \ + && adduser -S -D -H -u 10001 -G telesrv telesrv \ + && install -d -o telesrv -g telesrv -m 0750 /app /var/lib/telesrv + +COPY --chmod=0555 deploy/docker/docker-entrypoint.sh /usr/local/bin/telesrv-container-entrypoint + +WORKDIR /app +USER 10001:10001 +ENTRYPOINT ["/usr/local/bin/telesrv-container-entrypoint"] + +FROM runtime-base AS server +USER root +RUN apk add --no-cache ffmpeg openssl \ + && install -d -o telesrv -g telesrv -m 0750 \ + /var/lib/telesrv/blobs \ + /var/lib/telesrv/blob-staging \ + /var/lib/telesrv/maptiles \ + /var/lib/telesrv/livestream +COPY --from=build-server /out/telesrv /usr/local/bin/telesrv +COPY --chown=telesrv:telesrv data/langpack/ /usr/share/telesrv/langpack/ +USER 10001:10001 +EXPOSE 2398 2400 2401 2599 12399/udp 12400/udp +CMD ["telesrv"] + +FROM server AS server-test +USER root +RUN install -d -o telesrv -g telesrv -m 0755 /usr/share/telesrv/keys +COPY --chown=telesrv:telesrv --chmod=0444 deploy/docker/assets/test-server-rsa.pub /usr/share/telesrv/keys/test-server-rsa.pub +COPY --chown=telesrv:telesrv --chmod=0444 deploy/docker/assets/test-server-rsa.pem.b64 /usr/share/telesrv/keys/test-server-rsa.pem.b64 +USER 10001:10001 + +FROM runtime-base AS admin +COPY --from=build-admin /out/telesrv-admin /usr/local/bin/telesrv-admin +EXPOSE 2600 +CMD ["telesrv-admin"] diff --git a/README.md b/README.md index 8c1a60a9..75865d86 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,8 @@ The protocol stack is built on the published [`github.com/iamxvbaba/td`](https://github.com/iamxvbaba/td) module -(`v1.1.0`), using a canonical Layer 228 schema with sparse `tlprofile` -exact Layer 225-228 compatibility profiles. +(`v1.3.2`), using a canonical Layer 229 schema with sparse `tlprofile` +exact Layer 225-229 compatibility profiles. If you are looking for a **Telegram server**, **MTProto server**, **Telegram backend**, **Telegram clone server**, or **self-hosted @@ -21,7 +21,10 @@ in Go. Run it on your own network for a private, closed setup, or on a VPS to be reachable anywhere in the world. Your data, your keys, your rules — no cloud, no lock-in, no censorship. -> 🔗 Implements **MTProto API layer 228**. +> 🔗 Implements **MTProto API layers 225-229** — a client is admitted on the +> exact layer it announces, so older builds keep working after the server moves +> forward. The running server reports its version, the layers it accepts, and +> its build in the admin panel sidebar. `OwpenGram Server` is independent and unofficial. It is not affiliated with, endorsed by, or sponsored by Telegram or the official Telegram team. @@ -36,6 +39,9 @@ or sponsored by Telegram or the official Telegram team. - 🛡️ **Censorship-resistant** — no central authority can shut you down. - ⚙️ **Single binary** — one Go program prepares keys, runs migrations, serves MTProto, and dispatches updates and background workers. +- 📦 **One command to install** — the launcher installs the prerequisites + it needs (Go, Python, Docker, OpenSSL), brings the stack up, and hands you + a browser setup wizard. - 🆓 **Free & open source** — Apache-2.0, audit and extend it freely. ## 🎯 What works today @@ -50,6 +56,15 @@ or sponsored by Telegram or the official Telegram team. - 🔑 Self-hosted "Log in with Telegram" (OpenID Connect) and passkey sign-in - 🌐 Message translation and AI-assisted compose - 📇 Contacts, dialogs sync, chat folders, public link landing pages +- 🔎 **Self-configuring clients** — "Add Server" needs only `host:port`; the + server publishes its DC id, RSA key and identity over a well-known HTTP path +- 👥 **Multi-operator admin panel** — named operator accounts with scoped + permissions, instead of one shared password +- 🗄️ **Storage management** — usage breakdown, retention rules, and guarded + purge of orphaned or expired media +- 👋 Welcome messages and login-code templates you can edit from the panel +- 🧙 **First-run web setup wizard** — server identity, public address, Bot API + and your operator account, then a restart, all from the browser - 🖥️ Admin API and web UI for operations, plus a TUI server panel to run it all
@@ -57,7 +72,7 @@ or sponsored by Telegram or the official Telegram team. | Status | Feature | What works today | |---|---|---| -| ✅ | MTProto server edge | TCP transport, RSA key exchange, auth keys, encrypted sessions, salts, ack/resend, bad messages, RPC dispatch, canonical Layer 228, and sparse exact Layer 225-228 compatibility profiles. | +| ✅ | MTProto server edge | TCP transport, RSA key exchange, auth keys, encrypted sessions, salts, ack/resend, bad messages, RPC dispatch, canonical Layer 229, sparse exact Layer 225-229 compatibility profiles, and a same-port HTTP side that serves WebSocket transport plus the `/owpengram/server-info` and `/owpengram/server-icon` self-configuration endpoints. | | ✅ | Login and accounts | Development login code, configurable external code delivery (SMS webhook or SMTP), login email as a second factor, email-as-identity sign-up (no phone number needed), sign-in, sign-up, log-out, authorizations, account settings, SRP/password state, WebAuthn passkey sign-in, and a self-hosted Telegram Login (OpenID Connect) provider for third-party sites. | | ✅ | Users and contacts | User profiles, usernames, profile photos, contact import/search, blocked/privacy state, presence, and last-seen style status. | | ✅ | Dialogs and sync | Dialog list, pinned dialogs, manual unread, folders/filters, drafts, read boundaries, durable updates, online fan-out, and offline difference recovery. | @@ -72,21 +87,24 @@ or sponsored by Telegram or the official Telegram team. | ✅ | Collectible usernames and verification | Fragment-style NFT/collectible usernames (mint, transfer, activate/deactivate), the official platform-checkmark flow (`@verifybot`), and a third-party bot-verification mark mechanism (`@marksbot`, icon + description before a name) — the latter is experimental and hidden by default. | | ✅ | Bots and mini apps | Bot service foundations, callbacks, inline helpers, webview/mini-app paths, a minimal Bot API gateway for libraries such as `python-telegram-bot`, persistent `getUpdates` delivery, and demo tools. | | ✅ | Calls and live streams | Private call signaling foundations, group call state, RTMP live streaming, scheduled video chats, channel `join_as`, SFU/TURN building blocks, liveness, and expiry workers. | -| ✅ | Admin and operations | Admin API/UI backend, per-account freeze (admin-set read-only restriction, advertised to the client via appConfig), broadcast messaging (announce from the official account to every user or a picked list), shared-device detection across accounts, RBAC-scoped admin API tokens, PostgreSQL migrations, Redis volatile state, retention workers, pprof/debug hooks, load-test helpers, and a bundled TUI server panel (setup wizard, start/stop/restart, one-click update via `git pull` + rebuild, live logs, `.env` editor) as an alternative to manual builds. | +| ✅ | Admin and operations | Admin API/UI backend, a first-run web setup wizard (server identity, public network fields, optional Bot API gateway, first operator account), named operator accounts with per-section permissions and a wildcard "full access" grant, per-account freeze (admin-set read-only restriction, advertised to the client via appConfig), broadcast messaging (announce from the official account to every user or a picked list), editable welcome and login-code message templates, storage management (usage breakdown, retention rules, guarded purge), shared-device detection across accounts, RBAC-scoped admin API tokens, PostgreSQL migrations, Redis volatile state, retention workers, pprof/debug hooks, load-test helpers, one-click update with a dry run before it applies, and a bundled TUI server panel as an alternative to the web UI. | | ✅ | Desktop, Android, iOS, and Web focus | Telegram Desktop is the primary target, with Android, iOS, and Web compatibility paths actively covered by the same server. | Some items are compatibility-first or experimental, but they are real open server code, not hidden product-only features.
+## 🚀 Want to see it first? + +You do not have to run a server to try OwpenGram. We keep a **public server +live**, and it ships inside both clients as a ready-made entry — install a +client, pick **OwpenGram** on the server-selection screen, sign in. Nothing to +configure. + +Come back here when you want that server to be yours. + ## ⚡ Quick Start -Requirements: - -- **Go 1.25+** -- **Docker** (or Docker Desktop), for PostgreSQL and Redis -- OpenSSL, to export the server's RSA public key for the client's "Add Server" dialog - **1. Clone the repository** ```bash @@ -94,13 +112,56 @@ git clone https://github.com/owpengram/owpengram-server.git cd owpengram-server ``` -**2. Start the infrastructure** (PostgreSQL + Redis) +**2. Run the launcher** + +```bash +./owpengram-server.sh # Linux +``` +```powershell +.\owpengram-server.bat # Windows +``` + +The launcher checks what the server needs — Go 1.25+, Python 3, Docker, +OpenSSL — and **installs whatever is missing** instead of handing you a +shopping list: `scripts/install-prereqs.sh` on Arch and Ubuntu/Debian (asks for +root once, then works unattended) or `scripts/install-prereqs.ps1` on Windows +via winget. Run either directly with `--dry-run` to see what it would install +without touching anything. + +> Docker on Windows is the one thing the script will not install for you: its +> containers are Linux images, so the daemon needs Docker Desktop's WSL2 +> backend. The launcher reports it with a link instead of starting it. + +**3. Answer the first-run form** + +With the prerequisites in place the launcher opens the server panel. On a fresh +clone it shows a short form instead of the menu — only the values that need a +human decision, with `.env.example` defaults for everything else; the admin API +token and session key are generated for you. Confirm it and the panel writes +`.env`, starts PostgreSQL/Redis, builds both binaries, runs them, and shows the +admin panel address and password ready to copy. + +**4. Finish setup in the browser** + +Open that address. On a fresh install the panel opens a **web setup wizard** +that walks through the server name, description and icon, the public address +clients will connect to, the optional Bot API gateway, and your own operator +account — then restarts the server so it all takes effect. Nothing has to be +hand-edited to get going. + +
+🔧 Prefer to do it manually? (click to expand) + +Requirements: **Go 1.25+**, **Docker** (or Docker Desktop) for PostgreSQL and +Redis, and OpenSSL. + +**Start the infrastructure** (PostgreSQL + Redis) ```powershell docker compose -f deploy/docker-compose.yml up -d ``` -**3. Build and run the server** +**Build and run the server** Windows (PowerShell): @@ -123,42 +184,86 @@ workers in the same process. > **Default local login code:** `12345` — change it before any real use! -> 💡 **Prefer a menu over the command line?** Steps 2 and 3 above (Docker -> infrastructure, build, run) can be done through the bundled **TUI server -> panel** instead — see "🖥️ Server Panel" right below. +Without the web wizard you also have to fill in `.env` yourself; see +"⚙️ Configuration" below and [`.env.example`](.env.example). -### 🖥️ Server Panel (optional) +
-A cross-platform interactive TUI wraps the steps above — Docker naming -migration, `docker compose up`, `go build`, and launching both -`owpengram-server` and `owpengram-admin-panel` — behind a menu, so you don't -re-run commands from scratch every time. +### 🖥️ Server Panel -```bash -./owpengram-server.sh # Linux/macOS -``` -```powershell -.\owpengram-server.bat # Windows -``` - -Both launchers check prerequisites first (Go, Python 3, and the panel's own -dependencies via `tui-panel/requirements-panel.txt`), then start the panel. +The launcher from step 2 is also a cross-platform interactive TUI: once the +prerequisites are in place it drops into a menu that wraps everything above — +Docker naming migration, `docker compose up`, `go build`, and launching both +`owpengram-server` and `owpengram-admin-panel` — so you don't re-run commands +from scratch every time. What it does: -- 🧙 **First-run setup wizard** — walks through the required `.env` values - before the first start. +- 🧙 **First-run setup** — the short `.env` form from step 3. It is the + only thing the panel offers on a fresh clone; once `.env` exists the menu + below replaces it, and later changes go through the `.env` editor. - ▶️ **Start / Stop / Restart** — launches `owpengram-server` and `owpengram-admin-panel` as detached background processes; closing the panel does **not** stop them, only "Stop" does. Reopening the panel later picks the same processes back up and reports live status. - ⬆️ **Update** — `git pull --ff-only`, rebuilds both binaries, restarts them, and re-execs the panel itself so it also picks up any change to its own - code — one menu action instead of a manual pull/build/restart sequence. + code — one menu action instead of a manual pull/build/restart sequence. The + web panel has the same action, with a dry run that reports what an update + would do before anything is applied. - 📜 **Live log viewer** — tail either binary's log, or both in a split view. - ⚙️ **`.env` editor** — edit configuration from inside the panel, grouped by feature, without hand-editing the file. +### 🏷️ Version and build + +The admin panel's sidebar footer identifies exactly what is running: + +```text +Version: O7 +API layers: 225-229 +Build: 7ad68c3 +``` + +- **Version** — the OwpenGram server release line (`O7`). +- **API layers** — every MTProto TL schema layer this binary can talk, read + straight from the compatibility profiles rather than hardcoded. +- **Build** — the short commit the binary was built from, stamped + automatically by Go's VCS info (no special build flags needed); a trailing + `+` means it was built from a working tree with uncommitted changes. Hover + it for the full hash. + +Quote the `Version` / `Build` pair in bug reports — it pins the exact code, +which a release tag alone does not. + +### 👥 Operators and permissions + +The panel is no longer one shared password. Its **Operators** page creates +named accounts, each with its own login and an explicit set of permissions, +granted per section and per level — for example `accounts.read` +vs `accounts.manage`, `storage.read` vs `storage.manage`, `broadcasts.send`, +`moderation.review`, `server.manage`, `admins.manage`. An operator only sees +the sections they hold a permission for; reaching anything else lands on a +clear 403 screen instead of an empty page. + +A **Full access** checkbox at the top of the permission grid grants the `*` +wildcard — everything, including permissions added by future releases. It is +deliberately separate from the grid (which is disabled while it is on), so +"this person is a full admin" and "this person may do these six things" never +get confused. + +The one thing the panel will not let you do is lock yourself out: removing +`admins.manage` from the last enabled operator who holds it — by editing, +disabling, or deleting them — is refused, whether the grant is explicit or via +the wildcard. There is always someone left who can manage operators. + +Your first operator is created by the web setup wizard. Before it exists, the +panel lets you in with a password it generates for that one purpose — and stops +accepting that generated password the moment the wizard finishes. A password you +set yourself (in Server Settings, or `TELESRV_ADMIN_UI_PASSWORD` in `.env`) +keeps working as a full-access break-glass login alongside the operator +accounts, so leave it unset or treat it like a root password. + ### ⚙️ Configuration [`.env.example`](.env.example) is the complete configuration reference — every @@ -256,6 +361,17 @@ Related toggles (defaults in `.env.example`'s Advanced section): a low-space guard that rejects new uploads once storage nears full, and automatic cleanup of media no longer referenced by any message. +**Storage management in the panel.** The admin panel's Storage page puts the +rest of this behind a UI: a usage breakdown per media type, an upload size cap +(`TELESRV_STORAGE_MAX_UPLOAD_FILE_BYTES`, validated against the protocol's own +upload ceiling), retention rules that can expire media globally or per type +(`TELESRV_STORAGE_RETENTION_MODE` and the `TELESRV_STORAGE_RETENTION_MAX_AGE*` +family), and a "danger zone" for manual purges by media category and age. + +Destructive panel actions — purge, update, restart — all go through the same +three-step flow: type a reason, run a **dry run** that reports exactly what +would happen, then confirm. Nothing irreversible fires on a single click. + ## 🔌 Ports to open When deploying on a public server, open the following according to the @@ -317,37 +433,47 @@ the public routes to it with HTTPS. ## 📱 Connect a client Use the OwpenGram clients, which have a built-in **Add Server** option on the -server-selection screen at login — no source patching or custom build needed: +server-selection screen at login — no source patching or custom build needed. +They also ship with our public server already in that list, so a client is +useful the moment it is installed, with or without a server of your own. +Both are forks of the official apps, kept on the same TL layer as the server and +rebased on the upstream release that introduced it: -- 🤖 [Android client](https://github.com/owpengram/owpengram-android-client) -- 💻 [Desktop client](https://github.com/owpengram/owpengram-desktop-client) +| Client | Upstream base | Upstream commit | TL layer | +|---|---|---|---| +| 💻 [Desktop](https://github.com/owpengram/owpengram-desktop-client) | Telegram Desktop `v7.2.2` | `7b4481b6941212bb9dbf08e533adea97947b0f44` | 229 | +| 🤖 [Android](https://github.com/owpengram/owpengram-android-client) | Telegram for Android `v12.10.1` | `62b56a07ca7e30e39f7fd00a6728d6bbd716ca1c` | 229 | A stock Telegram client will not connect, since it only trusts Telegram's own -DC list and RSA keys. +DC list and RSA keys. The server's canonical layer is 229, with exact +compatibility profiles for layers 225-229 — so an older fork build keeps +working after the server moves forward. Locally that server is +`127.0.0.1:2398`, DC id `2`. -- Telegram Desktop commit: `9caf32dffc90ddd9bb08ad5777b865f729fa167b` -- Canonical TL layer: 228 -- Exact compatibility profiles: Layer 225-228 -- Local DC: `127.0.0.1:2398`, DC id `2` +**All you need is `host:port`.** On the login screen open server selection → +**Add Server** and type the address (e.g. `chat.example.com:2398`, or +`192.168.1.50:2398`). The client fetches `/owpengram/server-info` from the same +port and fills in the rest by itself — RSA public key, DC id, and the server's +name, description and icon as you set them in the setup wizard. No +`openssl`, no PEM copy-paste. -**1. Export your server's public key** +The RSA key and DC id are still there under **Advanced** if you want to check +or override them. To get the key by hand — for an air-gapped machine, or to +verify what the client fetched — export it from the server's private key: -After the server generates `data/server_rsa.pem`, export the matching public -key as PEM: - -```powershell +```bash openssl rsa -in data/server_rsa.pem -RSAPublicKey_out -out data/server_rsa.pub ``` -**2. Add the server in the client** +or just read the JSON the client reads: -On the login screen, open server selection → **Add Server**, and fill in: +```bash +curl http://your-server:2398/owpengram/server-info +``` -- **Host** — your server's address (e.g. `192.168.1.50` or `chat.example.com`) -- **Port** — `2398` by default -- **Main data center** — the DC id from `TELESRV_DC` (`2` by default) -- **RSA Public Key** — paste the full contents of `data/server_rsa.pub` - (the `-----BEGIN RSA PUBLIC KEY-----...` PEM block) into the key field +> Self-configuration rides the same-port HTTP side that also serves the +> WebSocket transport, so it needs `TELESRV_WEBSOCKET_ENABLE=true` (the +> default) and opens no extra port. With it off, fill in **Advanced** manually. ## 🧪 Development: multi-device smoke test @@ -374,16 +500,21 @@ you changed `TELESRV_DEV_AUTH_CODE`. Recommended checks: ## 📂 Repository layout ```text +owpengram-server.sh/.bat one-command launcher (installs prerequisites, then the panel) +scripts/install-prereqs.* unattended prerequisite installers (Arch/Ubuntu, Windows) cmd/telesrv/ server entrypoint -cmd/telesrv-admin/ admin backend and web UI +cmd/telesrv-admin/ admin backend and embedded React web UI (incl. the setup wizard) +cmd/telesrv-update/ one-click update helper used by the panels tui-panel/ interactive TUI server panel (setup, start/stop, update, logs, .env editor) deploy/ docker-compose (incl. MinIO), migrations, deploy helpers data/ bundled language packs and optional seed data -internal/mtprotoedge/ MTProto transport, auth key, session, ack/resend +internal/mtprotoedge/ MTProto transport, auth key, session, ack/resend, server-info endpoints internal/rpc/ TL router and client compatibility handlers internal/app/ domain services internal/domain/ protocol-independent domain models internal/store/ memory/postgres/redis storage backends +internal/identity/ admin-editable server name, description, and icon +internal/botapi/ minimal HTTP Bot API gateway internal/seed/ bundled seed catalog loaders internal/sfu/ real-time SFU experiments internal/turnsrv/ TURN/STUN building blocks diff --git a/cmd/telesrv-admin/addserverlink.go b/cmd/telesrv-admin/addserverlink.go new file mode 100644 index 00000000..5ef3694f --- /dev/null +++ b/cmd/telesrv-admin/addserverlink.go @@ -0,0 +1,32 @@ +package main + +import ( + "net/http" + "net/url" + "strconv" +) + +// handleAddServerLinkAPI builds an owpg://addserver link (see the desktop +// and Android clients' handling of that scheme) carrying only this server's +// host and port, so an operator can hand it out as a ready-made "add my +// server" button/QR code. +// +// Deliberately carries nothing else -- no name, description, key, or DC. +// Anyone who can get a link in front of a user (a forum post, a chat +// message, an intercepted share) controls whatever it contains; if it also +// carried the RSA key, a link with a forged key pointed at an attacker's own +// host would be indistinguishable from a real one, and the client would +// trust it outright as "this server's identity" -- a real MITM vector, not +// a hypothetical one. host+port alone can't misrepresent anything: the +// client always fetches name/description/key/DC itself, straight from +// whatever actually answers at that address (ServerInfoPath), the same way +// it already does for a hand-typed address in the "Add Server" form. +func (s *server) handleAddServerLinkAPI(w http.ResponseWriter, r *http.Request) { + q := url.Values{} + q.Set("host", s.cfg.AdvertiseHost) + q.Set("port", strconv.Itoa(s.cfg.ServerPort)) + + writeJSON(w, http.StatusOK, map[string]any{ + "link": "owpg://addserver?" + q.Encode(), + }) +} diff --git a/cmd/telesrv-admin/adminauth.go b/cmd/telesrv-admin/adminauth.go new file mode 100644 index 00000000..5d4f04e2 --- /dev/null +++ b/cmd/telesrv-admin/adminauth.go @@ -0,0 +1,154 @@ +package main + +import ( + "context" + "errors" + "strings" + "unicode" + + "golang.org/x/crypto/bcrypt" +) + +// bcryptCost is deliberately above bcrypt.DefaultCost (10). A panel login is a +// once-per-shift operation, so the extra time is invisible to an operator and +// meaningful to anyone working through a stolen dump of the table. +const bcryptCost = 12 + +// dummyBcryptHash is compared against when no account matched, so a login +// attempt costs the same whether or not the username exists. Without it the +// response time alone answers "is there an operator called X" -- the exact +// question the uniform error message refuses to answer. +// +// Value is bcrypt of a random string at bcryptCost; nothing authenticates +// against it. +const dummyBcryptHash = "$2a$12$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy" + +// breakGlassUsername is the name of the built-in operator backed by +// TELESRV_ADMIN_UI_PASSWORD / _TOKEN rather than by a database row. +// +// It is a real name rather than "no name" so audit lines read as an operator +// instead of as a blank, and so signing in as it is an explicit act: a blank +// username authenticates nothing. +// +// A database account may not take this name: authenticateLogin resolves it to +// the environment credential before ever consulting the table, so a row called +// "owpengram" would be shadowed -- and a name that silently does nothing is a +// trap. createAdminConsoleUser rejects it outright. +const breakGlassUsername = "owpengram" + +// loginIdentity is who a successful login turns out to be. +type loginIdentity struct { + actor string + userID int64 + epoch int32 + permissions []string +} + +// authenticateLogin resolves a login request to an identity, or reports +// failure. It never distinguishes its failure modes to the caller: every one +// of them is a plain false, so the handler cannot accidentally leak which. +func (s *server) authenticateLogin(ctx context.Context, req loginRequest) (loginIdentity, bool) { + username := strings.TrimSpace(req.Username) + + // A username is always required. An empty one used to resolve to the + // break-glass operator, which made a blank field an unnamed second route to + // the highest-privilege login -- the sort of thing that does not belong in + // an admin panel. The operator must now be asked for by name. + if username == "" { + return loginIdentity{}, false + } + + // The break-glass operator. Intentionally not backed by the database so it + // still works when the database does not. + if strings.EqualFold(username, breakGlassUsername) { + if !s.validSecret(req.Secret) { + return loginIdentity{}, false + } + return loginIdentity{actor: breakGlassUsername, permissions: s.cfg.Permissions}, true + } + + if s.read == nil { + return loginIdentity{}, false + } + cred, err := s.read.AdminConsoleCredentialByUsername(ctx, username) + if err != nil { + if !errors.Is(err, errAdminUserNotFound) { + return loginIdentity{}, false + } + // Burn the same work an existing account would have cost before + // answering, so "no such user" and "wrong password" take equal time. + _ = bcrypt.CompareHashAndPassword([]byte(dummyBcryptHash), []byte(req.Secret)) + return loginIdentity{}, false + } + if bcrypt.CompareHashAndPassword([]byte(cred.PasswordHash), []byte(req.Secret)) != nil { + return loginIdentity{}, false + } + // Checked after the hash comparison on purpose: answering "disabled" + // faster than "wrong password" would confirm the account exists to someone + // who does not know its password. + if !cred.Enabled { + return loginIdentity{}, false + } + return loginIdentity{ + actor: cred.Username, + userID: cred.ID, + epoch: cred.TokenEpoch, + permissions: cred.Permissions, + }, true +} + +// hashAdminPassword validates a new password and returns its bcrypt hash. +func hashAdminPassword(password string) (string, error) { + if err := validateAdminPassword(password); err != nil { + return "", err + } + hash, err := bcrypt.GenerateFromPassword([]byte(password), bcryptCost) + if err != nil { + return "", err + } + return string(hash), nil +} + +// validateAdminPassword deliberately imposes no length floor and no +// composition rule: the operator picks the password. +// +// The two checks that remain are not policy. A blank password is not a weak +// password, it is no password -- anyone who learns the username is in. And +// bcrypt silently ignores everything past 72 bytes, so a longer one is refused +// rather than quietly truncated to something the operator did not choose and +// cannot reproduce. +func validateAdminPassword(password string) error { + if strings.TrimSpace(password) == "" { + return errPasswordBlank + } + if len([]byte(password)) > 72 { + return errPasswordTooLong + } + return nil +} + +var ( + errPasswordTooLong = errors.New("password must be at most 72 bytes") + errPasswordBlank = errors.New("password must not be blank") + errUsernameInvalid = errors.New("username must be 3-64 characters: letters, digits, dot, dash or underscore") +) + +// validateAdminUsername keeps usernames to a shape that reads the same +// everywhere it is displayed. Anything outside it -- spaces, control +// characters, look-alike unicode -- is refused rather than normalised, since a +// username that renders differently from what is stored is a way to be +// mistaken for another operator. +func validateAdminUsername(username string) error { + if n := len([]rune(username)); n < 3 || n > 64 { + return errUsernameInvalid + } + for _, r := range username { + switch { + case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', unicode.IsDigit(r): + case r == '.', r == '-', r == '_': + default: + return errUsernameInvalid + } + } + return nil +} diff --git a/cmd/telesrv-admin/adminauth_test.go b/cmd/telesrv-admin/adminauth_test.go new file mode 100644 index 00000000..cd8829d1 --- /dev/null +++ b/cmd/telesrv-admin/adminauth_test.go @@ -0,0 +1,292 @@ +package main + +import ( + "errors" + "os" + "path/filepath" + "strings" + "testing" + + "golang.org/x/crypto/bcrypt" + + "telesrv/internal/identity" +) + +func TestValidateAdminPassword(t *testing.T) { + cases := []struct { + name string + password string + want error + }{ + {"ok", "correct horse battery", nil}, + // No length floor: the operator picks the password, however short. + {"a single character", "x", nil}, + {"blank", " ", errPasswordBlank}, + {"empty", "", errPasswordBlank}, + // bcrypt truncates silently past 72 bytes, so anything longer must be + // refused rather than accepted as a password the operator did not set. + {"past bcrypt's input limit", strings.Repeat("a", 73), errPasswordTooLong}, + {"73 bytes of multibyte runes", strings.Repeat("é", 37), errPasswordTooLong}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + err := validateAdminPassword(tc.password) + if !errors.Is(err, tc.want) { + t.Fatalf("validateAdminPassword(%q) = %v, want %v", tc.password, err, tc.want) + } + }) + } +} + +func TestValidateAdminUsername(t *testing.T) { + valid := []string{"admin", "ops.lead", "on-call_2", strings.Repeat("a", 64)} + for _, username := range valid { + if err := validateAdminUsername(username); err != nil { + t.Errorf("validateAdminUsername(%q) = %v, want nil", username, err) + } + } + invalid := []string{ + "", + "ab", // under the floor + strings.Repeat("a", 65), // over the ceiling + "has space", + "with\ttab", + "with\nnewline", + "аdmin", // Cyrillic 'а': renders like "admin" but is a different operator + "admin*", // permission wildcard has no business in a name + "a@b", + } + for _, username := range invalid { + if err := validateAdminUsername(username); !errors.Is(err, errUsernameInvalid) { + t.Errorf("validateAdminUsername(%q) = %v, want errUsernameInvalid", username, err) + } + } +} + +func TestHashAdminPasswordRoundTrips(t *testing.T) { + const password = "a sufficiently long password" + hash, err := hashAdminPassword(password) + if err != nil { + t.Fatalf("hashAdminPassword: %v", err) + } + if strings.Contains(hash, password) { + t.Fatal("hash contains the plaintext") + } + if err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)); err != nil { + t.Fatalf("hash does not verify against its own password: %v", err) + } + if err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password+"x")); err == nil { + t.Fatal("hash verified against the wrong password") + } + if cost, err := bcrypt.Cost([]byte(hash)); err != nil || cost != bcryptCost { + t.Fatalf("cost = %d (err %v), want %d", cost, err, bcryptCost) + } +} + +func TestHashAdminPasswordRejectsInvalid(t *testing.T) { + // A short password is fine; an absent one is not. + if _, err := hashAdminPassword("x"); err != nil { + t.Fatalf("a one-character password was refused: %v", err) + } + if _, err := hashAdminPassword(" "); !errors.Is(err, errPasswordBlank) { + t.Fatalf("err = %v, want errPasswordBlank", err) + } + if _, err := hashAdminPassword(strings.Repeat("a", 73)); !errors.Is(err, errPasswordTooLong) { + t.Fatalf("err = %v, want errPasswordTooLong", err) + } +} + +// The dummy hash exists so a login against an unknown username costs the same +// bcrypt work as a real one. If it were malformed, CompareHashAndPassword would +// return early and hand back the timing signal it is there to remove. +func TestDummyBcryptHashIsWellFormedAndUnusable(t *testing.T) { + cost, err := bcrypt.Cost([]byte(dummyBcryptHash)) + if err != nil { + t.Fatalf("dummy hash is not a valid bcrypt hash: %v", err) + } + if cost != bcryptCost { + t.Fatalf("dummy hash cost = %d, want %d -- it must cost the same as a real one", cost, bcryptCost) + } + for _, guess := range []string{"", "password", "admin", dummyBcryptHash} { + if err := bcrypt.CompareHashAndPassword([]byte(dummyBcryptHash), []byte(guess)); err == nil { + t.Fatalf("dummy hash authenticated %q", guess) + } + } +} + +func TestNormalisePermissions(t *testing.T) { + cases := []struct { + name string + in []string + want []string + }{ + {"trims and drops empties", []string{" a ", "", " ", "b"}, []string{"a", "b"}}, + {"de-duplicates", []string{"a", "a", "b", "a"}, []string{"a", "b"}}, + // A stored list that both names the wildcard and lists rights would read + // narrower than it actually is wherever it is displayed. + {"wildcard collapses everything", []string{"a", "*", "b"}, []string{permissionAll}}, + {"wildcard alone", []string{"*"}, []string{permissionAll}}, + {"empty stays empty", []string{}, []string{}}, + {"only blanks", []string{"", " "}, []string{}}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got := normalisePermissions(tc.in) + if len(got) != len(tc.want) { + t.Fatalf("normalisePermissions(%v) = %v, want %v", tc.in, got, tc.want) + } + for i := range got { + if got[i] != tc.want[i] { + t.Fatalf("normalisePermissions(%v) = %v, want %v", tc.in, got, tc.want) + } + } + }) + } +} + +// assignablePermissions drives the account editor. Offering "*" there would let +// a click hand out every right including admins.manage, which is exactly what +// the per-permission list exists to make deliberate. +func TestAssignablePermissionsExcludesWildcard(t *testing.T) { + for _, p := range assignablePermissions() { + if p == permissionAll { + t.Fatal("assignablePermissions offers the wildcard") + } + } + var sawAdminsManage bool + for _, p := range assignablePermissions() { + if p == permissionAdminsManage { + sawAdminsManage = true + } + } + if !sawAdminsManage { + t.Fatal("assignablePermissions omits admins.manage, so it could never be granted") + } +} + +// A blank username must never authenticate, even with the correct break-glass +// secret. It briefly did, which made an empty field an unnamed second route to +// the highest-privilege login; the operator has to be asked for by name. +func TestBlankUsernameNeverAuthenticates(t *testing.T) { + s := &server{cfg: uiConfig{Password: "letmein", Permissions: []string{permissionAll}}} + for _, username := range []string{"", " ", "\t"} { + if _, ok := s.authenticateLogin(t.Context(), loginRequest{Username: username, Secret: "letmein"}); ok { + t.Fatalf("blank username %q authenticated", username) + } + } + // The same secret under the operator's actual name still works, so the + // check above is refusing the blank name rather than the credential. + identity, ok := s.authenticateLogin(t.Context(), loginRequest{Username: breakGlassUsername, Secret: "letmein"}) + if !ok { + t.Fatal("the break-glass operator could not sign in by name") + } + if identity.actor != breakGlassUsername { + t.Fatalf("actor = %q, want %q", identity.actor, breakGlassUsername) + } + if identity.userID != 0 { + t.Fatalf("userID = %d, want 0 -- the break-glass operator has no database row", identity.userID) + } +} + +// Case is not a way to get a different operator: the name resolves to the +// break-glass login however it is typed, matching the case-insensitive unique +// index that named accounts live under. +func TestBreakGlassUsernameIsCaseInsensitive(t *testing.T) { + s := &server{cfg: uiConfig{Password: "letmein", Permissions: []string{permissionAll}}} + for _, username := range []string{"owpengram", "OwpenGram", "OWPENGRAM", " owpengram "} { + identity, ok := s.authenticateLogin(t.Context(), loginRequest{Username: username, Secret: "letmein"}) + if !ok { + t.Fatalf("%q did not resolve to the break-glass operator", username) + } + if identity.actor != breakGlassUsername { + t.Fatalf("%q signed in as %q", username, identity.actor) + } + } + if _, ok := s.authenticateLogin(t.Context(), loginRequest{Username: breakGlassUsername, Secret: "wrong"}); ok { + t.Fatal("the break-glass operator authenticated with the wrong secret") + } +} + +// The break-glass password quickstart generates for the very first login +// must stop authenticating once the first-run wizard is done, but a +// password an operator actually chose -- even one that happens to still be +// sitting in .env from before the wizard finished -- must never be +// affected by that. This is the actual integration point between +// validSecret and identity.Store; the package's own tests cover +// SetupPending/TemporaryPasswordMatches in isolation. +func TestValidSecretRetiresOnlyTheGeneratedPassword(t *testing.T) { + dir := t.TempDir() + store := identity.NewStore(dir) + s := &server{cfg: uiConfig{Password: "generated-once", Permissions: []string{permissionAll}}, identity: store} + + // No marker written at all yet (identity.Store's zero state) -- the + // password behaves like an ordinary one an operator set. + if !s.validSecret("generated-once") { + t.Fatal("password should authenticate before any wizard marker exists") + } + + // Bootstrap-style: the setup-pending marker plus the matching + // temporary-password marker, exactly as tui-panel/server-panel.py's + // bootstrap_env() writes them on a fresh install. + if err := os.MkdirAll(dir, 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, ".setup_pending"), nil, 0o644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, ".admin_password_temporary"), []byte("generated-once"), 0o644); err != nil { + t.Fatal(err) + } + if !s.validSecret("generated-once") { + t.Fatal("the generated password must keep working while the wizard is still pending") + } + + // Wizard finishes: MarkSetupComplete removes both markers. + if err := store.MarkSetupComplete(); err != nil { + t.Fatal(err) + } + if s.validSecret("generated-once") { + t.Fatal("the generated password must stop authenticating once setup is complete") + } + + // An operator-chosen password behaves normally regardless: setting a + // new .env value (this test's stand-in for that) authenticates whether + // or not a wizard ever ran, because it never matches either marker. + s.cfg.Password = "an-operator-actually-chose-this" + if !s.validSecret("an-operator-actually-chose-this") { + t.Fatal("an operator-chosen password must authenticate after setup completion, same as always") + } +} + +// A session for a named account must not be trusted on the strength of its +// signature alone: the account's rights are re-read per request, and a nil read +// store has to fail closed rather than fall back to the claims. +func TestCurrentSessionPermissionsFailsClosedWithoutStore(t *testing.T) { + s := &server{} + if _, ok := s.currentSessionPermissions(t.Context(), sessionClaims{ + UserID: 7, + Epoch: 1, + Permissions: []string{permissionAll}, + }); ok { + t.Fatal("a named-account session was accepted with no store to verify it against") + } +} + +// The break-glass operator has no row to re-read, so it keeps the configured +// rights -- that login is the way back in when the database is unreachable. +func TestCurrentSessionPermissionsAllowsBreakGlass(t *testing.T) { + s := &server{} + perms, ok := s.currentSessionPermissions(t.Context(), sessionClaims{ + UserID: 0, + Permissions: []string{permissionServerManage}, + }) + if !ok { + t.Fatal("break-glass session rejected") + } + if !perms.Has(permissionServerManage) { + t.Fatal("break-glass session lost its configured permission") + } + if perms.Has(permissionAdminsManage) { + t.Fatal("break-glass session gained a permission it was not configured with") + } +} diff --git a/cmd/telesrv-admin/adminusers.go b/cmd/telesrv-admin/adminusers.go new file mode 100644 index 00000000..5148deaa --- /dev/null +++ b/cmd/telesrv-admin/adminusers.go @@ -0,0 +1,125 @@ +package main + +import ( + "context" + "errors" + "fmt" + "strings" + "time" + + "github.com/jackc/pgx/v5" +) + +// AdminConsoleUser is one named panel operator. It deliberately never carries +// the password hash outside authentication: everything that renders or returns +// a user uses this shape, so a hash cannot leak into an API response by +// someone adding a field to a JSON struct. +type AdminConsoleUser struct { + ID int64 `json:"id"` + Username string `json:"username"` + Permissions []string `json:"permissions"` + Enabled bool `json:"enabled"` + TokenEpoch int32 `json:"token_epoch"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + LastLoginAt *time.Time `json:"last_login_at,omitempty"` +} + +// adminConsoleCredential is the authentication-only view: the hash plus the +// few fields a login decision needs. Kept unexported and separate from +// AdminConsoleUser so the hash has exactly one reason to be read. +type adminConsoleCredential struct { + ID int64 + Username string + PasswordHash string + Permissions []string + Enabled bool + TokenEpoch int32 +} + +// errAdminUserNotFound is returned instead of pgx.ErrNoRows so callers can +// treat "no such operator" without importing pgx. +var errAdminUserNotFound = errors.New("admin console user not found") + +const adminConsoleUserColumns = `id, username, permissions, enabled, token_epoch, created_at, updated_at, last_login_at` + +// AdminConsoleCredentialByUsername loads the authentication view for a login +// attempt. The lookup is case-insensitive to match the unique index, so an +// operator cannot be shadowed by a differently-cased duplicate. +func (s *readStore) AdminConsoleCredentialByUsername(ctx context.Context, username string) (adminConsoleCredential, error) { + var out adminConsoleCredential + err := s.pool.QueryRow(ctx, ` +SELECT id, username, password_hash, permissions, enabled, token_epoch +FROM admin_console_users +WHERE lower(username) = lower($1)`, strings.TrimSpace(username)).Scan( + &out.ID, &out.Username, &out.PasswordHash, &out.Permissions, &out.Enabled, &out.TokenEpoch) + if errors.Is(err, pgx.ErrNoRows) { + return adminConsoleCredential{}, errAdminUserNotFound + } + if err != nil { + return adminConsoleCredential{}, fmt.Errorf("load admin console credential: %w", err) + } + return out, nil +} + +// AdminConsoleSessionState re-reads the two things a live session depends on. +// requireAuthAPI calls it per request so that disabling an operator, editing +// their rights or changing their password takes effect immediately rather than +// whenever their signed cookie happens to expire. +func (s *readStore) AdminConsoleSessionState(ctx context.Context, id int64) (enabled bool, epoch int32, permissions []string, err error) { + err = s.pool.QueryRow(ctx, ` +SELECT enabled, token_epoch, permissions FROM admin_console_users WHERE id = $1`, id). + Scan(&enabled, &epoch, &permissions) + if errors.Is(err, pgx.ErrNoRows) { + return false, 0, nil, errAdminUserNotFound + } + if err != nil { + return false, 0, nil, fmt.Errorf("load admin console session state: %w", err) + } + return enabled, epoch, permissions, nil +} + +// ListAdminConsoleUsers returns every operator, newest last so the list reads +// like the order they were added. +func (s *readStore) ListAdminConsoleUsers(ctx context.Context) ([]AdminConsoleUser, error) { + rows, err := s.pool.Query(ctx, ` +SELECT `+adminConsoleUserColumns+` FROM admin_console_users ORDER BY id`) + if err != nil { + return nil, fmt.Errorf("list admin console users: %w", err) + } + defer rows.Close() + + out := []AdminConsoleUser{} + for rows.Next() { + var u AdminConsoleUser + if err := rows.Scan(&u.ID, &u.Username, &u.Permissions, &u.Enabled, + &u.TokenEpoch, &u.CreatedAt, &u.UpdatedAt, &u.LastLoginAt); err != nil { + return nil, fmt.Errorf("scan admin console user: %w", err) + } + if u.Permissions == nil { + u.Permissions = []string{} + } + out = append(out, u) + } + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("iterate admin console users: %w", err) + } + return out, nil +} + +// CountEnabledAdminConsoleUsersWith reports how many enabled operators hold a +// given permission, counting the '*' wildcard as holding everything. It exists +// for the last-administrator guard: the panel refuses the edit that would +// leave nobody able to manage operators. +func (s *readStore) CountEnabledAdminConsoleUsersWith(ctx context.Context, permission string, excludeID int64) (int, error) { + var n int + if err := s.pool.QueryRow(ctx, ` +SELECT count(*)::int FROM admin_console_users +WHERE enabled + AND id <> $2 + AND (permissions @> ARRAY[$1]::text[] OR permissions @> ARRAY['*']::text[])`, + permission, excludeID).Scan(&n); err != nil { + return 0, fmt.Errorf("count admin console users with permission: %w", err) + } + return n, nil +} diff --git a/cmd/telesrv-admin/adminusers_api.go b/cmd/telesrv-admin/adminusers_api.go new file mode 100644 index 00000000..95297eb0 --- /dev/null +++ b/cmd/telesrv-admin/adminusers_api.go @@ -0,0 +1,400 @@ +package main + +import ( + "context" + "errors" + "fmt" + "net/http" + "strings" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" + + "telesrv/internal/admin" +) + +// Operator accounts are written here rather than through callAdminAPI like the +// domain mutations are, on purpose. They are not a Telegram entity: they are +// the console's own authentication, and routing them through the domain API +// would mean the console cannot fix its own locked-out operators whenever that +// service is unreachable -- exactly when you need to. Reads already go straight +// to Postgres for the same reason, so this keeps one owner for one table. +// +// They do follow the panel's command convention: every mutation is a +// /api/actions/* route that takes a reason, runs as a dry run first and returns +// an admin.CommandResult. Granting somebody the run of the console deserves the +// same "here is what this will do, confirm it" step as freezing an account. + +// requireAdminsManage is the single gate for every operator-account route, so +// none of them can be registered without it by accident. +func (s *server) requireAdminsManage(next http.Handler) http.Handler { + return s.scopedRoute(permissionAdminsManage, next) +} + +// errAdminUsernameTaken maps the unique-index violation to something the panel +// can show, without leaking the constraint name. +var errAdminUsernameTaken = errors.New("username is already taken") + +// errLastManagerStanding guards against an edit that would leave nobody able to +// administer operators. The break-glass credential could still recover it, but +// that is a recovery path, not a thing to walk into by accident. +var errLastManagerStanding = errors.New("this would leave no enabled account able to manage operators") + +// errUsernameReserved guards the break-glass name, which authentication +// resolves before the table is consulted. +var errUsernameReserved = errors.New("this username is reserved for the built-in operator") + +// createAdminConsoleUser inserts a new operator. token_epoch starts at 1; there +// are no sessions to invalidate yet. +func (s *server) createAdminConsoleUser(ctx context.Context, username, password string, permissions []string, enabled bool) (AdminConsoleUser, error) { + if err := validateAdminUsername(username); err != nil { + return AdminConsoleUser{}, err + } + // authenticateLogin resolves this name to the environment credential before + // it ever reaches the table, so a row by this name could never be logged + // into. Refuse it rather than storing an account that silently does nothing. + if strings.EqualFold(strings.TrimSpace(username), breakGlassUsername) { + return AdminConsoleUser{}, errUsernameReserved + } + hash, err := hashAdminPassword(password) + if err != nil { + return AdminConsoleUser{}, err + } + permissions = normalisePermissions(permissions) + + var u AdminConsoleUser + err = s.read.pool.QueryRow(ctx, ` +INSERT INTO admin_console_users (username, password_hash, permissions, enabled) +VALUES ($1, $2, $3, $4) +RETURNING `+adminConsoleUserColumns, + strings.TrimSpace(username), hash, permissions, enabled). + Scan(&u.ID, &u.Username, &u.Permissions, &u.Enabled, &u.TokenEpoch, + &u.CreatedAt, &u.UpdatedAt, &u.LastLoginAt) + var pgErr *pgconn.PgError + if errors.As(err, &pgErr) && pgErr.Code == "23505" { + return AdminConsoleUser{}, errAdminUsernameTaken + } + if err != nil { + return AdminConsoleUser{}, fmt.Errorf("create admin console user: %w", err) + } + if u.Permissions == nil { + u.Permissions = []string{} + } + return u, nil +} + +// updateAdminConsoleUser changes permissions and/or enabled state. +// +// It deliberately does NOT move token_epoch. currentSessionPermissions re-reads +// this row on every request, so a narrowed permission set applies from the +// operator's next request and a disabled account is refused outright -- both +// without ending a session. Bumping the epoch here would only sign someone out +// mid-task to achieve what the re-read already achieves. +// +// A password change is different and does bump it: the password is not +// re-checked per request, so nothing else would retire the old sessions. +func (s *server) updateAdminConsoleUser(ctx context.Context, id int64, permissions []string, enabled bool) (AdminConsoleUser, error) { + permissions = normalisePermissions(permissions) + + var u AdminConsoleUser + err := s.read.pool.QueryRow(ctx, ` +UPDATE admin_console_users +SET permissions = $2, + enabled = $3, + updated_at = now() +WHERE id = $1 +RETURNING `+adminConsoleUserColumns, + id, permissions, enabled). + Scan(&u.ID, &u.Username, &u.Permissions, &u.Enabled, &u.TokenEpoch, + &u.CreatedAt, &u.UpdatedAt, &u.LastLoginAt) + if errors.Is(err, pgx.ErrNoRows) { + return AdminConsoleUser{}, errAdminUserNotFound + } + if err != nil { + return AdminConsoleUser{}, fmt.Errorf("update admin console user: %w", err) + } + if u.Permissions == nil { + u.Permissions = []string{} + } + return u, nil +} + +// setAdminConsoleUserPassword replaces the hash and bumps the epoch, so a +// password change signs out whoever was using the old one -- which is the +// point of changing it after a suspected compromise. +func (s *server) setAdminConsoleUserPassword(ctx context.Context, id int64, password string) error { + hash, err := hashAdminPassword(password) + if err != nil { + return err + } + tag, err := s.read.pool.Exec(ctx, ` +UPDATE admin_console_users +SET password_hash = $2, token_epoch = token_epoch + 1, updated_at = now() +WHERE id = $1`, id, hash) + if err != nil { + return fmt.Errorf("set admin console user password: %w", err) + } + if tag.RowsAffected() == 0 { + return errAdminUserNotFound + } + return nil +} + +// normalisePermissions trims, de-duplicates and collapses to the wildcard when +// it is present, so "*" plus a list cannot be stored as something that reads +// narrower than it is. +func normalisePermissions(in []string) []string { + seen := make(map[string]struct{}, len(in)) + out := make([]string, 0, len(in)) + for _, p := range in { + p = strings.TrimSpace(p) + if p == "" { + continue + } + if p == permissionAll { + return []string{permissionAll} + } + if _, dup := seen[p]; dup { + continue + } + seen[p] = struct{}{} + out = append(out, p) + } + return out +} + +// --- HTTP surface ----------------------------------------------------------- + +// adminUserActionRequest carries the panel's usual command envelope alongside +// the operator fields. ID is absent when creating. +type adminUserActionRequest struct { + CommandID string `json:"command_id"` + Reason string `json:"reason"` + Confirm bool `json:"confirm"` + ID int64 `json:"id"` + Username string `json:"username"` + Password string `json:"password"` + Permissions []string `json:"permissions"` + Enabled *bool `json:"enabled"` +} + +func (s *server) handleListAdminUsersAPI(w http.ResponseWriter, r *http.Request) { + if s.read == nil { + writeAPIError(w, http.StatusServiceUnavailable, "read store is not configured") + return + } + users, err := s.read.ListAdminConsoleUsers(r.Context()) + if err != nil { + writeAPIError(w, http.StatusInternalServerError, err.Error()) + return + } + writeJSON(w, http.StatusOK, map[string]any{ + // The built-in operator has no database row, so it would otherwise be + // invisible here -- a list of who can sign in that omits the account + // with the most rights is worse than no list. It is reported first and + // flagged as system; the panel renders it read-only, and every mutation + // below refuses it anyway. + "system": map[string]any{ + "username": breakGlassUsername, + "permissions": newPanelPermissions(s.cfg.Permissions).List(), + "enabled": true, + "system": true, + }, + "rows": users, + // The vocabulary the panel offers when editing an account, so the list + // of assignable rights lives in one place instead of being duplicated + // in the frontend and drifting from what the routes actually check. + "available_permissions": assignablePermissions(), + }) +} + +// handleCreateAdminUserAPI runs as a dry run unless confirmed. +func (s *server) handleCreateAdminUserAPI(w http.ResponseWriter, r *http.Request) { + var body adminUserActionRequest + if !s.decodeAdminUserAction(w, r, &body) { + return + } + meta := s.commandMetaFromAPI(r, body.CommandID, body.Reason, body.Confirm, "admin-operator-create") + enabled := body.Enabled == nil || *body.Enabled + permissions := normalisePermissions(body.Permissions) + + // Validate on the dry run too, so "this will fail" is discovered before the + // operator is asked to confirm rather than after. + if err := validateAdminUsername(strings.TrimSpace(body.Username)); err != nil { + writeCommandResultAPI(w, admin.CommandResult{CommandID: meta.CommandID, Action: "create-admin-operator"}, err) + return + } + if strings.EqualFold(strings.TrimSpace(body.Username), breakGlassUsername) { + writeCommandResultAPI(w, admin.CommandResult{CommandID: meta.CommandID, Action: "create-admin-operator"}, errUsernameReserved) + return + } + if err := validateAdminPassword(body.Password); err != nil { + writeCommandResultAPI(w, admin.CommandResult{CommandID: meta.CommandID, Action: "create-admin-operator"}, err) + return + } + + if meta.DryRun { + writeJSON(w, http.StatusOK, admin.CommandResult{ + CommandID: meta.CommandID, + Action: "create-admin-operator", + Status: "ok", + DryRun: true, + Message: fmt.Sprintf("Would create operator %q with %d permission(s), %s.", + strings.TrimSpace(body.Username), len(permissions), enabledWord(enabled)), + Details: map[string]any{ + "username": strings.TrimSpace(body.Username), + "permissions": permissions, + "enabled": enabled, + }, + }) + return + } + + user, err := s.createAdminConsoleUser(r.Context(), body.Username, body.Password, permissions, enabled) + if err != nil { + writeCommandResultAPI(w, admin.CommandResult{CommandID: meta.CommandID, Action: "create-admin-operator"}, err) + return + } + writeJSON(w, http.StatusOK, admin.CommandResult{ + CommandID: meta.CommandID, + Action: "create-admin-operator", + Status: "ok", + Message: fmt.Sprintf("Created operator %q.", user.Username), + Details: map[string]any{"id": user.ID, "username": user.Username, "permissions": user.Permissions, "enabled": user.Enabled}, + }) +} + +// handleUpdateAdminUserAPI changes rights and/or enabled state, dry run first. +func (s *server) handleUpdateAdminUserAPI(w http.ResponseWriter, r *http.Request) { + var body adminUserActionRequest + if !s.decodeAdminUserAction(w, r, &body) { + return + } + meta := s.commandMetaFromAPI(r, body.CommandID, body.Reason, body.Confirm, "admin-operator-access") + const action = "set-admin-operator-access" + enabled := body.Enabled == nil || *body.Enabled + permissions := normalisePermissions(body.Permissions) + + if body.ID <= 0 { + writeCommandResultAPI(w, admin.CommandResult{CommandID: meta.CommandID, Action: action}, errAdminUserNotFound) + return + } + if err := s.guardManagerRemoval(r.Context(), body.ID, permissions, enabled); err != nil { + writeCommandResultAPI(w, admin.CommandResult{CommandID: meta.CommandID, Action: action}, err) + return + } + + if meta.DryRun { + writeJSON(w, http.StatusOK, admin.CommandResult{ + CommandID: meta.CommandID, + Action: action, + Status: "ok", + DryRun: true, + Message: fmt.Sprintf("Would set operator #%d to %d permission(s), %s. Takes effect on their next request.", + body.ID, len(permissions), enabledWord(enabled)), + Details: map[string]any{"id": body.ID, "permissions": permissions, "enabled": enabled}, + }) + return + } + + user, err := s.updateAdminConsoleUser(r.Context(), body.ID, permissions, enabled) + if err != nil { + writeCommandResultAPI(w, admin.CommandResult{CommandID: meta.CommandID, Action: action}, err) + return + } + writeJSON(w, http.StatusOK, admin.CommandResult{ + CommandID: meta.CommandID, + Action: action, + Status: "ok", + Message: fmt.Sprintf("Updated %q. The new access applies from their next request.", user.Username), + Details: map[string]any{"id": user.ID, "username": user.Username, "permissions": user.Permissions, "enabled": user.Enabled}, + }) +} + +// handleSetAdminUserPasswordAPI resets a password, dry run first. +func (s *server) handleSetAdminUserPasswordAPI(w http.ResponseWriter, r *http.Request) { + var body adminUserActionRequest + if !s.decodeAdminUserAction(w, r, &body) { + return + } + meta := s.commandMetaFromAPI(r, body.CommandID, body.Reason, body.Confirm, "admin-operator-password") + const action = "set-admin-operator-password" + + if body.ID <= 0 { + writeCommandResultAPI(w, admin.CommandResult{CommandID: meta.CommandID, Action: action}, errAdminUserNotFound) + return + } + if err := validateAdminPassword(body.Password); err != nil { + writeCommandResultAPI(w, admin.CommandResult{CommandID: meta.CommandID, Action: action}, err) + return + } + + if meta.DryRun { + writeJSON(w, http.StatusOK, admin.CommandResult{ + CommandID: meta.CommandID, + Action: action, + Status: "ok", + DryRun: true, + Message: fmt.Sprintf("Would set a new password for operator #%d. Their existing sessions would be signed out.", body.ID), + // The password itself is never echoed, not even back to the + // operator who just typed it. + Details: map[string]any{"id": body.ID}, + }) + return + } + + if err := s.setAdminConsoleUserPassword(r.Context(), body.ID, body.Password); err != nil { + writeCommandResultAPI(w, admin.CommandResult{CommandID: meta.CommandID, Action: action}, err) + return + } + writeJSON(w, http.StatusOK, admin.CommandResult{ + CommandID: meta.CommandID, + Action: action, + Status: "ok", + Message: fmt.Sprintf("Password changed for operator #%d. Their existing sessions are signed out.", body.ID), + Details: map[string]any{"id": body.ID}, + }) +} + +// decodeAdminUserAction shares the store check, body decode and reason +// requirement across the three mutations. +func (s *server) decodeAdminUserAction(w http.ResponseWriter, r *http.Request, body *adminUserActionRequest) bool { + if s.read == nil { + writeAPIError(w, http.StatusServiceUnavailable, "read store is not configured") + return false + } + if err := decodeJSON(r, body); err != nil { + writeAPIError(w, http.StatusBadRequest, err.Error()) + return false + } + if strings.TrimSpace(body.Reason) == "" { + writeAPIError(w, http.StatusBadRequest, "a reason is required") + return false + } + return true +} + +func enabledWord(enabled bool) string { + if enabled { + return "enabled" + } + return "disabled" +} + +// guardManagerRemoval refuses an edit that would leave nobody able to manage +// operators. Counted over the other accounts, so demoting or disabling the +// only remaining manager is what trips it. +func (s *server) guardManagerRemoval(ctx context.Context, id int64, permissions []string, enabled bool) error { + stillManages := enabled && newPanelPermissions(permissions).Has(permissionAdminsManage) + if stillManages { + return nil + } + others, err := s.read.CountEnabledAdminConsoleUsersWith(ctx, permissionAdminsManage, id) + if err != nil { + return err + } + if others == 0 { + return errLastManagerStanding + } + return nil +} diff --git a/cmd/telesrv-admin/adminusers_guard_integration_test.go b/cmd/telesrv-admin/adminusers_guard_integration_test.go new file mode 100644 index 00000000..11e7fcc9 --- /dev/null +++ b/cmd/telesrv-admin/adminusers_guard_integration_test.go @@ -0,0 +1,101 @@ +package main + +import ( + "context" + "errors" + "strconv" + "testing" + "time" +) + +// guardManagerRemoval is the only thing between an operator and a console +// nobody can administer, and it decides from a COUNT over admin_console_users +// whose predicate treats '*' as holding every right. Neither that wildcard +// matching nor the enabled filter can be proven anywhere but against the real +// table, so this is an integration test, gated on TELESRV_TEST_POSTGRES_DSN +// like the rest of the package. +// +// The count spans every enabled manager except the row being edited, so unlike +// the other integration tests here this one cannot keep to its own fixtures +// with a unique suffix -- a manager left behind by an earlier run would make +// the "nobody else" cases silently pass for the wrong reason. It empties +// admin_console_users instead; verificationReadStore refuses a DSN whose +// database name does not contain "test", which is what makes that safe, and no +// other test in the repo touches this table. +func TestGuardManagerRemovalIntegration(t *testing.T) { + store, pool := verificationReadStore(t) + srv := &server{read: store} + ctx := context.Background() + + resetTable := func() { + t.Helper() + if _, err := pool.Exec(ctx, `TRUNCATE admin_console_users RESTART IDENTITY`); err != nil { + t.Fatalf("truncate admin_console_users: %v", err) + } + } + resetTable() + t.Cleanup(resetTable) + + unique := time.Now().UnixNano() & 0x7fffffff + insertOperator := func(permissions []string, enabled bool) int64 { + t.Helper() + unique++ + var id int64 + if err := pool.QueryRow(ctx, ` +INSERT INTO admin_console_users (username, password_hash, permissions, enabled) +VALUES ($1, 'not-a-real-hash', $2, $3) +RETURNING id`, "guardop"+strconv.FormatInt(unique, 10), permissions, enabled).Scan(&id); err != nil { + t.Fatalf("insert operator: %v", err) + } + return id + } + + t.Run("sole wildcard holder cannot drop the wildcard", func(t *testing.T) { + resetTable() + id := insertOperator([]string{permissionAll}, true) + err := srv.guardManagerRemoval(ctx, id, []string{permissionAccountsRead}, true) + if !errors.Is(err, errLastManagerStanding) { + t.Fatalf("err = %v, want errLastManagerStanding", err) + } + }) + + t.Run("sole manager cannot disable itself", func(t *testing.T) { + resetTable() + id := insertOperator([]string{permissionAll}, true) + err := srv.guardManagerRemoval(ctx, id, []string{permissionAll}, false) + if !errors.Is(err, errLastManagerStanding) { + t.Fatalf("err = %v, want errLastManagerStanding", err) + } + }) + + // Narrowing the wildcard down to the managing right itself is the supported + // way out of full access, so it must not trip the guard. + t.Run("sole manager may trade the wildcard for admins.manage", func(t *testing.T) { + resetTable() + id := insertOperator([]string{permissionAll}, true) + if err := srv.guardManagerRemoval(ctx, id, []string{permissionAdminsManage}, true); err != nil { + t.Fatalf("err = %v, want nil", err) + } + }) + + // The case the SQL's '*' arm exists for: the remaining manager holds the + // wildcard rather than a literal admins.manage, and must still be counted. + t.Run("another enabled wildcard holder counts as a manager", func(t *testing.T) { + resetTable() + id := insertOperator([]string{permissionAll}, true) + insertOperator([]string{permissionAll}, true) + if err := srv.guardManagerRemoval(ctx, id, []string{permissionAccountsRead}, true); err != nil { + t.Fatalf("err = %v, want nil", err) + } + }) + + t.Run("a disabled second manager does not count", func(t *testing.T) { + resetTable() + id := insertOperator([]string{permissionAll}, true) + insertOperator([]string{permissionAdminsManage}, false) + err := srv.guardManagerRemoval(ctx, id, []string{permissionAccountsRead}, true) + if !errors.Is(err, errLastManagerStanding) { + t.Fatalf("err = %v, want errLastManagerStanding", err) + } + }) +} diff --git a/cmd/telesrv-admin/buildinfo.go b/cmd/telesrv-admin/buildinfo.go new file mode 100644 index 00000000..fa110a59 --- /dev/null +++ b/cmd/telesrv-admin/buildinfo.go @@ -0,0 +1,45 @@ +package main + +import "runtime/debug" + +// gitCommit/buildTime can be set via -ldflags "-X main.gitCommit=... -X +// main.buildTime=...", mirroring cmd/telesrv/buildinfo.go -- but in +// practice neither procctl's goBuild (used by the admin panel's own +// Restart/Update) nor a plain `go build` sets them, so this normally falls +// back to Go's automatic VCS stamping (debug.ReadBuildInfo's vcs.revision), +// which needs nothing extra to work from a git checkout. +var ( + gitCommit = "" + buildTime = "" +) + +type buildMetadata struct { + Commit string + Dirty bool + BuildTime string +} + +// shortCommit is what the sidebar footer shows next to "Version: O7" -- the +// full hash is one click away in git log, the footer just needs enough to +// tell two builds apart at a glance. +func (m buildMetadata) shortCommit() string { + if len(m.Commit) > 7 { + return m.Commit[:7] + } + return m.Commit +} + +func currentBuildMetadata() buildMetadata { + meta := buildMetadata{Commit: gitCommit, BuildTime: buildTime} + if info, ok := debug.ReadBuildInfo(); ok { + settings := map[string]string{} + for _, setting := range info.Settings { + settings[setting.Key] = setting.Value + } + if meta.Commit == "" { + meta.Commit = settings["vcs.revision"] + } + meta.Dirty = settings["vcs.modified"] == "true" + } + return meta +} diff --git a/cmd/telesrv-admin/main.go b/cmd/telesrv-admin/main.go index 7c87778d..cba28576 100644 --- a/cmd/telesrv-admin/main.go +++ b/cmd/telesrv-admin/main.go @@ -7,9 +7,11 @@ import ( "encoding/hex" "fmt" "log" + "net" "net/http" "os" "os/signal" + "strconv" "strings" "syscall" "time" @@ -27,6 +29,23 @@ const hostStatsPollInterval = 5 * time.Second const defaultAdminAPIAddr = "127.0.0.1:2599" +// bootID is a random value generated once per process start, exposed via +// GET /api/session -- see that handler's doc comment for why (the +// Restart/Update polling flow's way of detecting a genuinely new admin +// process, not just a slow-to-respond old one). +var bootID = newBootID() + +func newBootID() string { + buf := make([]byte, 16) + if _, err := rand.Read(buf); err != nil { + // crypto/rand failing is effectively unheard of on any real target + // this binary runs on; falling back to the wall clock still gives a + // value that changes across restarts, which is all this is for. + return fmt.Sprintf("t%d", time.Now().UnixNano()) + } + return hex.EncodeToString(buf) +} + func main() { if err := run(); err != nil { log.Fatal(err) @@ -47,7 +66,7 @@ func run() error { } defer pool.Close() - hs := hoststats.NewPoller(cfg.BlobDir) + hs := hoststats.NewPoller(cfg.DiskStatsPath) go hs.Run(ctx, hostStatsPollInterval) srv, err := newServer(cfg, newReadStore(pool), hs) @@ -80,10 +99,10 @@ type uiConfig struct { Password string Token string SessionKey []byte - // BlobDir is the local blob-storage root, reused only to pick which - // filesystem the dashboard's disk-free reading statfs's -- irrelevant when - // TELESRV_BLOB_BACKEND=s3, where disk space isn't the storage constraint. - BlobDir string + // DiskStatsPath points the dashboard host-disk sampler at the local path + // that matters for the selected blob backend: permanent localfs storage or + // the S3 upload spool. + DiskStatsPath string // Permissions is the right set a panel session is issued with, from // TELESRV_ADMIN_UI_PERMISSIONS. The shipped default is the single wildcard // entry, so introducing the permission model never locks an operator out of a @@ -95,6 +114,41 @@ type uiConfig struct { // permissions, and the session/login response tells the frontend to hide // the "Third-party marks" nav entry and its routes. HideThirdPartyVerification bool + // IdentityDir mirrors config.IdentityDir -- must point at the same + // directory owpengram-server reads, so an identity edit here is visible + // over /owpengram/server-info immediately (see internal/identity). + IdentityDir string + // WelcomeMessagePhoneDefault/WelcomeMessageEmailDefault mirror + // config.WelcomeMessage{Phone,Email}Template -- the env-var-resolved + // fallback text (TELESRV_WELCOME_MESSAGE_*_TEMPLATE, itself defaulting + // to the compiled-in copy) the running owpengram-server process falls + // back to whenever the identity panel override is unset. Surfaced as + // "the effective default" in the Server Settings login-notifications + // panel, assuming both binaries share the same .env. + WelcomeMessagePhoneDefault string + WelcomeMessageEmailDefault string + // LoginCodeMessageDefault mirrors config.LoginCodeMessageTemplate -- the + // env-var-resolved fallback text (TELESRV_LOGIN_CODE_MESSAGE_TEMPLATE, + // itself defaulting to the compiled-in copy) the running owpengram-server + // process falls back to whenever the identity panel override is unset. + // Same "effective default" contract as WelcomeMessage{Phone,Email}Default. + LoginCodeMessageDefault string + // RepoRoot is where Server Settings' Restart/Update/.env-editing (see + // internal/procctl) operate: bin/, logs/, .env, .env.example and + // .server_panel.json are all expected directly under it, exactly as + // tui-panel/server-panel.py expects. Defaults to the process's current + // working directory, which is correct whenever this binary is launched + // from (or by something that cd'd into) the repo root -- true both for a + // manual run and for how the TUI itself launches it. + RepoRoot string + // AdvertiseHost/ServerPort mirror config.AdvertiseIP and the port half + // of config.ListenAddr -- what the "Add Server" sidebar button needs to + // build an owpg://addserver link for this exact server (host+port only, + // see handleAddServerLinkAPI's doc comment for why nothing else belongs + // in that link), assuming both binaries share the same .env (same + // convention as the WelcomeMessage/LoginCode defaults above). + AdvertiseHost string + ServerPort int } // loadConfig 通过 internal/config.Load() 加载 .env 配置文件与环境变量, @@ -121,6 +175,20 @@ func loadConfig() (uiConfig, error) { } sum := sha256.Sum256([]byte(appCfg.AdminSessionKey)) + repoRoot, err := os.Getwd() + if err != nil { + return uiConfig{}, fmt.Errorf("resolve repo root: %w", err) + } + + _, serverPortStr, err := net.SplitHostPort(appCfg.ListenAddr) + if err != nil { + return uiConfig{}, fmt.Errorf("parse TELESRV_LISTEN %q: %w", appCfg.ListenAddr, err) + } + serverPort, err := strconv.Atoi(serverPortStr) + if err != nil { + return uiConfig{}, fmt.Errorf("parse TELESRV_LISTEN port %q: %w", serverPortStr, err) + } + return uiConfig{ Addr: appCfg.AdminUIAddr, PostgresDSN: appCfg.PostgresDSN, @@ -129,12 +197,26 @@ func loadConfig() (uiConfig, error) { Password: appCfg.AdminUIPassword, Token: appCfg.AdminUIToken, SessionKey: sum[:], + DiskStatsPath: dashboardDiskPath(appCfg), Permissions: appCfg.AdminUIPermissions, HideThirdPartyVerification: appCfg.HideThirdPartyVerification, - BlobDir: appCfg.BlobDir, + IdentityDir: appCfg.IdentityDir, + WelcomeMessagePhoneDefault: appCfg.WelcomeMessagePhoneTemplate, + WelcomeMessageEmailDefault: appCfg.WelcomeMessageEmailTemplate, + LoginCodeMessageDefault: appCfg.LoginCodeMessageTemplate, + RepoRoot: repoRoot, + AdvertiseHost: appCfg.AdvertiseIP, + ServerPort: serverPort, }, nil } +func dashboardDiskPath(cfg config.Config) string { + if strings.EqualFold(strings.TrimSpace(cfg.BlobBackendKind), "s3") && strings.TrimSpace(cfg.BlobStagingDir) != "" { + return cfg.BlobStagingDir + } + return cfg.BlobDir +} + func adminAPIURL(addr string) string { addr = strings.TrimSpace(addr) if addr == "" { diff --git a/cmd/telesrv-admin/publicbranding.go b/cmd/telesrv-admin/publicbranding.go new file mode 100644 index 00000000..2a23aa6b --- /dev/null +++ b/cmd/telesrv-admin/publicbranding.go @@ -0,0 +1,64 @@ +package main + +import ( + "net/http" +) + +// The login screen shows which server it belongs to, so the two fields it needs +// are served without a session. +// +// This discloses nothing new. owpengram-server already publishes the same name +// and icon to every client that asks, over its own open endpoints +// (/owpengram/server-info and /owpengram/server-icon) -- that is how a client +// fills in the "Add Server" form and how the panel's own RefreshServersInfo +// works. Branding is public by design; the point of a server name is to be +// read before you are anybody. +// +// It stays deliberately narrow all the same: name and whether an icon exists, +// and nothing else off identity.Info, which also carries the description and +// the welcome/login-code message templates. Those are operator-facing settings +// and stay behind server.manage. + +type publicBrandingResponse struct { + Name string `json:"name"` + HasIcon bool `json:"has_icon"` +} + +func (s *server) handlePublicBrandingAPI(w http.ResponseWriter, _ *http.Request) { + out := publicBrandingResponse{} + if s.identity != nil { + if info, err := s.identity.Get(); err == nil { + out.Name = info.Name + } + _, _, ok := s.identity.Icon() + out.HasIcon = ok + } + // A server that has not been named yet answers with an empty name rather + // than an error: the login page falls back to its own branding, and a + // failed request there would only produce a console error for nothing. + writeJSON(w, http.StatusOK, out) +} + +// handlePublicIconAPI serves the same bytes as handleServerIconAPI, without the +// session. Same file, same rationale as above. +func (s *server) handlePublicIconAPI(w http.ResponseWriter, _ *http.Request) { + if s.identity == nil { + http.NotFound(w, nil) + return + } + data, ext, ok := s.identity.Icon() + if !ok { + http.Error(w, "no icon configured", http.StatusNotFound) + return + } + contentType := map[string]string{ + ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", + ".webp": "image/webp", ".gif": "image/gif", + }[ext] + if contentType == "" { + contentType = "application/octet-stream" + } + w.Header().Set("Content-Type", contentType) + w.Header().Set("Cache-Control", "no-store") + _, _ = w.Write(data) +} diff --git a/cmd/telesrv-admin/readstore.go b/cmd/telesrv-admin/readstore.go index 4c78b3b6..711ec610 100644 --- a/cmd/telesrv-admin/readstore.go +++ b/cmd/telesrv-admin/readstore.go @@ -12,6 +12,7 @@ import ( "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" + "golang.org/x/sync/errgroup" "telesrv/internal/domain" ) @@ -386,6 +387,7 @@ LEFT JOIN peer_usernames p ON p.peer_type = 'user' AND p.peer_id = u.id AND p.ed LEFT JOIN auth a ON a.user_id = u.id LEFT JOIN account_passwords ap ON ap.user_id = u.id WHERE NOT u.is_bot + AND NOT (u.id = ANY($8::bigint[])) AND ( u.id = $1 OR u.phone LIKE $2 @@ -396,7 +398,7 @@ WHERE NOT u.is_bot ) AND ($5::bigint = 0 OR (COALESCE(a.last_active_at, '0001-01-01 00:00:00+00'::timestamptz), u.id) < (to_timestamp(($5::double precision) / 1000000.0), $6::bigint)) ORDER BY COALESCE(a.last_active_at, '0001-01-01 00:00:00+00'::timestamptz) DESC, u.id DESC -LIMIT $7`, id, phonePrefix, substring, term, beforeActiveUS, beforeID, limit+1) +LIMIT $7`, id, phonePrefix, substring, term, beforeActiveUS, beforeID, limit+1, domain.SystemUserIDs()) if err != nil { return nil, false, fmt.Errorf("search accounts: %w", err) } @@ -508,44 +510,77 @@ type DashboardCounts struct { PendingVerifications int64 } +// DashboardCounts gathers every headline number on the admin overview. +// +// The queries are independent, so they run concurrently: this used to be eight +// round trips in series and the page waited for their sum. errgroup cancels the +// rest as soon as one fails, and each goroutine writes to its own field of +// `out`, so no locking is needed. func (s *readStore) DashboardCounts(ctx context.Context) (DashboardCounts, error) { var out DashboardCounts - var err error - if out.Users, err = s.CountAccounts(ctx); err != nil { - return out, err - } - if out.OnlineUsers, err = s.CountOnlineAccounts(ctx); err != nil { - return out, err - } - if err := s.pool.QueryRow(ctx, ` + g, gctx := errgroup.WithContext(ctx) + + g.Go(func() error { + v, err := s.CountAccounts(gctx) + out.Users = v + return err + }) + g.Go(func() error { + v, err := s.CountOnlineAccounts(gctx) + out.OnlineUsers = v + return err + }) + g.Go(func() error { + if err := s.pool.QueryRow(gctx, ` SELECT count(*) FROM users WHERE is_bot AND deleted_at IS NULL`).Scan(&out.Bots); err != nil { - return out, fmt.Errorf("count bots: %w", err) - } - if err := s.pool.QueryRow(ctx, ` + return fmt.Errorf("count bots: %w", err) + } + return nil + }) + g.Go(func() error { + if err := s.pool.QueryRow(gctx, ` SELECT count(*) FILTER (WHERE broadcast), count(*) FILTER (WHERE megagroup) FROM channels WHERE NOT deleted AND NOT monoforum`).Scan(&out.BroadcastChannels, &out.Supergroups); err != nil { - return out, fmt.Errorf("count channels: %w", err) - } - if err := s.pool.QueryRow(ctx, ` + return fmt.Errorf("count channels: %w", err) + } + return nil + }) + g.Go(func() error { + if err := s.pool.QueryRow(gctx, ` SELECT count(*) FILTER (WHERE set_kind = 'stickers'), count(*) FILTER (WHERE set_kind = 'emoji') FROM sticker_sets WHERE deleted = false`).Scan(&out.StickerSets, &out.EmojiSets); err != nil { - return out, fmt.Errorf("count sticker sets: %w", err) - } - // There's no global GIF catalog -- a GIF is just a document a user saved to - // their personal collection (messages.saveGif). This counts distinct - // documents saved by anyone, the closest thing to "how many GIFs does this - // server know about." - if err := s.pool.QueryRow(ctx, ` + return fmt.Errorf("count sticker sets: %w", err) + } + return nil + }) + g.Go(func() error { + // There's no global GIF catalog -- a GIF is just a document a user saved + // to their personal collection (messages.saveGif). This counts distinct + // documents saved by anyone, the closest thing to "how many GIFs does + // this server know about." + if err := s.pool.QueryRow(gctx, ` SELECT count(DISTINCT document_id) FROM user_sticker_collections WHERE kind = 'gif'`).Scan(&out.Gifs); err != nil { - return out, fmt.Errorf("count gifs: %w", err) - } - if err := s.pool.QueryRow(ctx, ` + return fmt.Errorf("count gifs: %w", err) + } + return nil + }) + g.Go(func() error { + if err := s.pool.QueryRow(gctx, ` SELECT count(*) FROM moderation_cases WHERE status NOT IN ('resolved', 'dismissed')`).Scan(&out.PendingReports); err != nil { - return out, fmt.Errorf("count pending moderation cases: %w", err) - } - if err := s.pool.QueryRow(ctx, ` + return fmt.Errorf("count pending moderation cases: %w", err) + } + return nil + }) + g.Go(func() error { + if err := s.pool.QueryRow(gctx, ` SELECT count(*) FROM verification_applications WHERE status IN ('submitted', 'in_review')`).Scan(&out.PendingVerifications); err != nil { - return out, fmt.Errorf("count pending verification applications: %w", err) + return fmt.Errorf("count pending verification applications: %w", err) + } + return nil + }) + + if err := g.Wait(); err != nil { + return DashboardCounts{}, err } return out, nil } @@ -850,9 +885,10 @@ LEFT JOIN account_restrictions r ON r.user_id = u.id LEFT JOIN peer_usernames p ON p.peer_type = 'user' AND p.peer_id = u.id AND p.editable LEFT JOIN account_passwords ap ON ap.user_id = u.id WHERE NOT u.is_bot + AND NOT (u.id = ANY($4::bigint[])) AND ($1::bigint = 0 OR (COALESCE(auth.last_active_at, '0001-01-01 00:00:00+00'::timestamptz), u.id) < (to_timestamp(($1::double precision) / 1000000.0), $2::bigint)) ORDER BY COALESCE(auth.last_active_at, '0001-01-01 00:00:00+00'::timestamptz) DESC, u.id DESC -LIMIT $3`, beforeActiveUS, beforeID, limit+1) +LIMIT $3`, beforeActiveUS, beforeID, limit+1, domain.SystemUserIDs()) if err != nil { return nil, false, fmt.Errorf("list accounts: %w", err) } @@ -957,27 +993,31 @@ ORDER BY g.last_active_at DESC, g.device_model, g.system_version, g.platform, g. return groups, hasMore, nil } -// BroadcastRow is one system-broadcast campaign, with sent/failed counts -// derived live from broadcast_recipients (never stored, so they can't drift). +// BroadcastRow is one system-broadcast campaign. SentCount/FailedCount/ +// MaterializedCount are maintained incrementally by the delivery worker as +// it closes out each recipient row (see internal/app/broadcast); for an +// "all"-mode campaign still enumerating, TargetCount grows until +// EnumerationDone. type BroadcastRow struct { - ID int64 - Message string - TargetMode string - TotalCount int - SentCount int - FailedCount int - CreatedBy string - CreatedAt time.Time + ID int64 + Message string + TargetMode string + TargetCount int64 + MaterializedCount int64 + SentCount int64 + FailedCount int64 + EnumerationDone bool + CreatedBy string + CreatedAt time.Time } const broadcastRowColumns = ` - b.id, b.message, b.target_mode, b.total_count, b.created_by, b.created_at, - count(*) FILTER (WHERE r.status = 'sent')::int AS sent_count, - count(*) FILTER (WHERE r.status = 'failed')::int AS failed_count` + b.id, b.message, b.target_mode, b.target_count, b.materialized_count, + b.sent_count, b.failed_count, b.enumeration_done, b.created_by, b.created_at` func scanBroadcastRow(row interface{ Scan(...any) error }, item *BroadcastRow) error { - return row.Scan(&item.ID, &item.Message, &item.TargetMode, &item.TotalCount, &item.CreatedBy, &item.CreatedAt, - &item.SentCount, &item.FailedCount) + return row.Scan(&item.ID, &item.Message, &item.TargetMode, &item.TargetCount, &item.MaterializedCount, + &item.SentCount, &item.FailedCount, &item.EnumerationDone, &item.CreatedBy, &item.CreatedAt) } // ListBroadcasts pages campaigns newest-first. @@ -988,9 +1028,7 @@ func (s *readStore) ListBroadcasts(ctx context.Context, beforeID int64, limit in rows, err := s.pool.Query(ctx, ` SELECT `+broadcastRowColumns+` FROM broadcasts b -LEFT JOIN broadcast_recipients r ON r.broadcast_id = b.id WHERE $1::bigint = 0 OR b.id < $1 -GROUP BY b.id ORDER BY b.id DESC LIMIT $2`, beforeID, limit+1) if err != nil { @@ -2538,62 +2576,181 @@ const ( ) // perOwnerMediaSizeSQL is shared between StorageStats and -// ListAccountStorageUsage: each document row's size is its stored column; -// each photo row has no single size column (JSONB sizes holds one entry per -// rendition), so its "attributed size" is the largest rendition -- the -// dominant cost, thumbnails are comparatively tiny. This is an -// approximation (not the exact sum of every rendition's blob bytes, which -// would require joining file_blobs by location_key prefix) chosen for admin -// visibility, not billing precision. +// ListAccountStorageUsage: attributes each document/photo's REAL remaining +// bytes (the sum of every file_blobs row it still owns -- main body plus +// thumbnail/rendition variants) to its owner, one row per document/photo so +// an outer COUNT(*)/SUM(size) aggregate still gets an accurate file count +// alongside the byte total. This used to read documents.size / the largest +// photo rendition size directly -- a static value on the metadata row that +// survives a hard-retention purge unchanged, so it kept counting bytes for +// files whose blobs were long gone. Joining through file_blobs instead means +// a purged item correctly contributes 0: there is nothing left to attribute. const perOwnerMediaSizeSQL = ` -SELECT owner_user_id, size FROM documents +SELECT d.owner_user_id, COALESCE(( + SELECT SUM(fb.size) FROM file_blobs fb + WHERE fb.location_key = 'doc:' || d.id::text + OR fb.location_key LIKE 'doc:' || d.id::text || ':%' +), 0) AS size +FROM documents d UNION ALL SELECT p.owner_user_id, COALESCE(( - SELECT MAX((elem->>'size')::bigint) FROM jsonb_array_elements(p.sizes) elem + SELECT SUM(fb.size) FROM file_blobs fb + WHERE fb.location_key = 'photo:' || p.id::text + OR fb.location_key LIKE 'photo:' || p.id::text || ':%' ), 0) AS size FROM photos p ` // StorageStatsRow is the admin panel's storage overview: physical bytes // (from file_blobs, backend-dedup-aware -- what's actually consuming disk -// or S3) versus logical bytes (sum of the same approximate per-row -// attribution the per-account breakdown uses, which can legitimately be -// higher than physical when identical content is shared by more than one -// document/photo). +// or S3, deduplicated exactly once across the whole system) versus logical +// bytes (the SAME real, still-existing file_blobs bytes as physical, just +// summed per-owner via perOwnerMediaSizeSQL without deduplicating content +// shared across accounts/documents -- so logical can legitimately be higher +// than physical when the same blob is attributed to more than one +// document/photo, but a purged file with no file_blobs rows left correctly +// contributes 0 to both, never a stale non-zero "ghost" size). +// +// Every field here deliberately EXCLUDES system/bundled content (owner_user_id +// = 0 on the documents/photos row -- the built-in sticker packs, emoji sets, +// default wallpapers, GIF catalog, and system-bot avatars this server seeds +// at every boot; see internal/app/files's Seed* functions). None of that is +// something an operator manages through storage retention/purge -- it isn't +// one of the Photo/Video/GIF/Music/Voice/File/Avatar categories those +// controls target, it's permanent server furniture -- so counting it here +// alongside real user uploads made every number on this page mean "user +// storage plus an unpredictable pile of bundled assets" instead of just +// answering "how much space are my users actually using". SystemBytes below +// is the one exception: it reports that excluded total separately, purely +// for an operator's own curiosity/disk-accounting, never folded into the +// other totals. type StorageStatsRow struct { - PhysicalBytes int64 `json:"PhysicalBytes,string"` - LogicalBytes int64 `json:"LogicalBytes,string"` - UnattributedBytes int64 `json:"UnattributedBytes,string"` - DocumentCount int64 `json:"DocumentCount,string"` - PhotoCount int64 `json:"PhotoCount,string"` - AccountCount int64 `json:"AccountCount,string"` - BackendKind string + PhysicalBytes int64 `json:"PhysicalBytes,string"` + LogicalBytes int64 `json:"LogicalBytes,string"` + // SystemBytes is the physical size of excluded system/bundled content + // (owner_user_id = 0) -- shown separately so the gap between this and + // what `docker exec ... mc du` or the MinIO console reports isn't a + // mystery, but never added into PhysicalBytes/LogicalBytes/DocumentCount/ + // PhotoCount/AccountCount above. + SystemBytes int64 `json:"SystemBytes,string"` + // DocumentCount, PhotoCount and AccountCount all count only items that + // still own real file_blobs bytes -- documents/photos rows are + // deliberately kept forever after a hard-retention purge (so a message + // can still render "here was a file"), so counting rows instead of live + // bytes would keep growing even as the actual content becomes physically + // empty, diverging further and further from PhysicalBytes above. + DocumentCount int64 `json:"DocumentCount,string"` + PhotoCount int64 `json:"PhotoCount,string"` + AccountCount int64 `json:"AccountCount,string"` + BackendKind string } // StorageStats returns the admin panel's storage overview. +// StorageStats runs six aggregates over file_blobs/documents/photos. They are +// independent and each is expensive, so they run concurrently rather than +// summing their latencies -- see file_blobs_location_key_pattern_idx for why +// they were slow in the first place. func (s *readStore) StorageStats(ctx context.Context) (StorageStatsRow, error) { var stats StorageStatsRow - if err := s.pool.QueryRow(ctx, `SELECT COALESCE(SUM(size), 0)::bigint FROM file_blobs`).Scan(&stats.PhysicalBytes); err != nil { - return StorageStatsRow{}, fmt.Errorf("sum physical blob bytes: %w", err) - } - if err := s.pool.QueryRow(ctx, ` -SELECT COALESCE(SUM(size), 0)::bigint FROM (`+perOwnerMediaSizeSQL+`) x`).Scan(&stats.LogicalBytes); err != nil { - return StorageStatsRow{}, fmt.Errorf("sum logical media bytes: %w", err) - } - if err := s.pool.QueryRow(ctx, ` -SELECT COALESCE(SUM(size), 0)::bigint FROM (`+perOwnerMediaSizeSQL+`) x WHERE owner_user_id = 0`).Scan(&stats.UnattributedBytes); err != nil { - return StorageStatsRow{}, fmt.Errorf("sum unattributed media bytes: %w", err) - } - if err := s.pool.QueryRow(ctx, `SELECT count(*)::bigint FROM documents`).Scan(&stats.DocumentCount); err != nil { - return StorageStatsRow{}, fmt.Errorf("count documents: %w", err) - } - if err := s.pool.QueryRow(ctx, `SELECT count(*)::bigint FROM photos`).Scan(&stats.PhotoCount); err != nil { - return StorageStatsRow{}, fmt.Errorf("count photos: %w", err) - } - if err := s.pool.QueryRow(ctx, ` -SELECT count(DISTINCT owner_user_id)::bigint FROM (`+perOwnerMediaSizeSQL+`) x WHERE owner_user_id <> 0`).Scan(&stats.AccountCount); err != nil { - return StorageStatsRow{}, fmt.Errorf("count storage accounts: %w", err) + g, gctx := errgroup.WithContext(ctx) + + // Physical usage dedups by (backend, object_key) like the unfiltered + // version used to, but only counts an object if at least one real user's + // (owner_user_id <> 0) document/photo still references it -- content + // shared between a system asset and a real upload (content-addressed + // storage, so only possible via a byte-for-byte coincidental duplicate) + // still counts, since a real user genuinely has that data stored. + g.Go(func() error { + // Reads the owner out of the location_key ("doc:", "doc::", + // "photo::") and looks it up by primary key, instead of asking + // "is there any document whose id, glued into a string, equals this key". + // + // That original phrasing was the one query file_blobs_location_key_pattern_idx + // could not help: the pattern is built from d.id and matched against + // fb.location_key, so no index on location_key applies and every blob had + // to scan documents (then photos) end to end. This direction is a plain + // PK probe per blob. + // + // The CASE keeps the cast total -- a key whose second field isn't numeric + // yields NULL rather than raising, and NULL matches no id, exactly like + // the string form matched no row. Keys of other kinds ("enc:") are + // excluded by the kind check, as before. + if err := s.pool.QueryRow(gctx, ` +SELECT COALESCE(SUM(size), 0)::bigint FROM ( + SELECT DISTINCT ON (fb.backend, fb.object_key) fb.backend, fb.object_key, fb.size + FROM file_blobs fb + CROSS JOIN LATERAL ( + SELECT split_part(fb.location_key, ':', 1) AS kind, + CASE WHEN split_part(fb.location_key, ':', 2) ~ '^[0-9]+$' + THEN split_part(fb.location_key, ':', 2)::bigint + END AS owner_id + ) k + WHERE (k.kind = 'doc' AND EXISTS ( + SELECT 1 FROM documents d WHERE d.id = k.owner_id AND d.owner_user_id <> 0 + )) OR (k.kind = 'photo' AND EXISTS ( + SELECT 1 FROM photos p WHERE p.id = k.owner_id AND p.owner_user_id <> 0 + )) +) x`).Scan(&stats.PhysicalBytes); err != nil { + return fmt.Errorf("sum physical blob bytes: %w", err) + } + return nil + }) + g.Go(func() error { + if err := s.pool.QueryRow(gctx, ` +SELECT COALESCE(SUM(size), 0)::bigint FROM (`+perOwnerMediaSizeSQL+`) x WHERE owner_user_id <> 0`).Scan(&stats.LogicalBytes); err != nil { + return fmt.Errorf("sum logical media bytes: %w", err) + } + return nil + }) + g.Go(func() error { + if err := s.pool.QueryRow(gctx, ` +SELECT COALESCE(SUM(size), 0)::bigint FROM (`+perOwnerMediaSizeSQL+`) x WHERE owner_user_id = 0`).Scan(&stats.SystemBytes); err != nil { + return fmt.Errorf("sum system media bytes: %w", err) + } + return nil + }) + // Documents/Photos/AccountCount all count only items that still own real + // file_blobs bytes -- documents/photos rows are deliberately kept forever + // after a hard-retention purge (so a message can still render "here was + // a file"), so a plain count(*) would keep growing even as everything it + // counts becomes physically empty, wildly diverging from PhysicalBytes + // above and making the overview page look broken/confusing rather than + // informative. owner_user_id <> 0 excludes system/bundled content -- see + // StorageStatsRow's doc comment. + g.Go(func() error { + if err := s.pool.QueryRow(gctx, ` +SELECT count(*)::bigint FROM documents d WHERE d.owner_user_id <> 0 AND EXISTS ( + SELECT 1 FROM file_blobs fb + WHERE fb.location_key = 'doc:' || d.id::text + OR fb.location_key LIKE 'doc:' || d.id::text || ':%' +)`).Scan(&stats.DocumentCount); err != nil { + return fmt.Errorf("count documents: %w", err) + } + return nil + }) + g.Go(func() error { + if err := s.pool.QueryRow(gctx, ` +SELECT count(*)::bigint FROM photos p WHERE p.owner_user_id <> 0 AND EXISTS ( + SELECT 1 FROM file_blobs fb + WHERE fb.location_key = 'photo:' || p.id::text + OR fb.location_key LIKE 'photo:' || p.id::text || ':%' +)`).Scan(&stats.PhotoCount); err != nil { + return fmt.Errorf("count photos: %w", err) + } + return nil + }) + g.Go(func() error { + if err := s.pool.QueryRow(gctx, ` +SELECT count(DISTINCT owner_user_id)::bigint FROM (`+perOwnerMediaSizeSQL+`) x WHERE owner_user_id <> 0 AND size > 0`).Scan(&stats.AccountCount); err != nil { + return fmt.Errorf("count storage accounts: %w", err) + } + return nil + }) + + if err := g.Wait(); err != nil { + return StorageStatsRow{}, err } + stats.BackendKind = strings.ToLower(strings.TrimSpace(os.Getenv("TELESRV_BLOB_BACKEND"))) if stats.BackendKind == "" { stats.BackendKind = "s3" @@ -2611,11 +2768,25 @@ type AccountStorageRow struct { FileCount int64 `json:"FileCount,string"` } -// ListAccountStorageUsage pages the per-account storage breakdown, largest -// user first. Offset-based (not keyset): storage administration on a -// self-hosted deployment doesn't need to support arbitrarily deep pages -// efficiently the way an infinite-scroll feed does. -func (s *readStore) ListAccountStorageUsage(ctx context.Context, offset, limit int) ([]AccountStorageRow, bool, error) { +// storageUsageSortColumns whitelists the columns ListAccountStorageUsage's +// sortBy may map to -- never interpolate the caller's sort key directly into +// SQL, since unlike a plain value it can't go through a query parameter. +var storageUsageSortColumns = map[string]string{ + "bytes": "t.bytes", + "files": "t.file_count", + "user_id": "t.owner_user_id", + "username": "lower(COALESCE(u.username, ''))", + "first_name": "lower(COALESCE(u.first_name, ''))", +} + +// ListAccountStorageUsage pages the per-account storage breakdown. sortBy +// selects one of storageUsageSortColumns (falls back to "bytes" for an +// unknown/empty key); sortDesc reverses it. q, if non-empty, filters to +// accounts whose id/username/first name match it. Offset-based (not +// keyset): storage administration on a self-hosted deployment doesn't need +// to support arbitrarily deep pages efficiently the way an infinite-scroll +// feed does. +func (s *readStore) ListAccountStorageUsage(ctx context.Context, q string, sortBy string, sortDesc bool, offset, limit int) ([]AccountStorageRow, bool, error) { if limit <= 0 { limit = storageUsageListDefaultLimit } @@ -2625,19 +2796,51 @@ func (s *readStore) ListAccountStorageUsage(ctx context.Context, offset, limit i if offset < 0 { offset = 0 } + column, ok := storageUsageSortColumns[sortBy] + if !ok { + column = storageUsageSortColumns["bytes"] + } + direction := "ASC" + if sortDesc { + direction = "DESC" + } + q = strings.TrimSpace(q) + var whereClause string + args := []any{} + argN := 1 + if q != "" { + id := int64(-1) + if n, err := strconv.ParseInt(q, 10, 64); err == nil { + id = n + } + whereClause = fmt.Sprintf(`WHERE t.owner_user_id = $%d + OR lower(COALESCE(u.username, '')) LIKE $%d + OR lower(COALESCE(u.first_name, '')) LIKE $%d`, argN, argN+1, argN+2) + args = append(args, id, "%"+strings.ToLower(q)+"%", "%"+strings.ToLower(q)+"%") + argN += 3 + } + offsetArg := argN + limitArg := argN + 1 + args = append(args, offset, limit+1) rows, err := s.pool.Query(ctx, ` WITH totals AS ( + -- size > 0 excludes documents/photos whose file_blobs bytes have already + -- been purged -- their row is kept forever (see perOwnerMediaSizeSQL's + -- doc comment) so counting every row here would keep FileCount growing + -- long after Bytes has settled at (or near) 0, same mismatch this query + -- used to have before it was joined through file_blobs at all. SELECT owner_user_id, SUM(size)::bigint AS bytes, COUNT(*)::bigint AS file_count FROM (`+perOwnerMediaSizeSQL+`) x - WHERE owner_user_id <> 0 + WHERE owner_user_id <> 0 AND size > 0 GROUP BY owner_user_id ) SELECT t.owner_user_id, COALESCE(u.username, ''), COALESCE(u.first_name, ''), t.bytes, t.file_count FROM totals t LEFT JOIN users u ON u.id = t.owner_user_id -ORDER BY t.bytes DESC, t.owner_user_id -OFFSET $1 -LIMIT $2`, offset, limit+1) +`+whereClause+` +ORDER BY `+column+` `+direction+`, t.owner_user_id +OFFSET $`+strconv.Itoa(offsetArg)+` +LIMIT $`+strconv.Itoa(limitArg), args...) if err != nil { return nil, false, fmt.Errorf("list account storage usage: %w", err) } diff --git a/cmd/telesrv-admin/routescope_test.go b/cmd/telesrv-admin/routescope_test.go new file mode 100644 index 00000000..e1a5c356 --- /dev/null +++ b/cmd/telesrv-admin/routescope_test.go @@ -0,0 +1,134 @@ +package main + +import ( + "os" + "path/filepath" + "regexp" + "strings" + "testing" +) + +// The panel is deny-by-default: an API route must say which right it belongs +// to. Registering one with a bare requireAuthAPI would make it answer to every +// signed-in operator regardless of what they were granted -- which is how a +// scoped account quietly gets the run of the place. +// +// This reads the source rather than the routing table because that is where the +// mistake is made: it fails on the line someone is about to add, and names it. +func TestEveryAPIRouteDeclaresAScope(t *testing.T) { + // Every /api route must be registered through a wrapper that names a + // permission. Whitelisting the wrappers rather than blacklisting the bare + // one is what makes this hold for helpers added later: a new wrapper is + // unknown here until someone adds it deliberately, so it fails closed. + allowed := []string{ + "s.scopedRoute(", + "s.scopedRouteAll(", + "s.requirePermission(", + "s.requireAdminsManage(", + "s.serverManage(", + "s.verificationRead(", + "s.botVerificationRead(", + "s.botVerificationManage(", + } + // Routes that are reachable before a session exists, each for a stated + // reason: /api/login is the way in (it carries its own credential), and the + // two branding routes feed the login screen with the server name and icon + // that owpengram-server already publishes to every client. + exempt := map[string]bool{ + "POST /api/login": true, + "GET /api/public/branding": true, + "GET /api/public/icon": true, + } + + route := regexp.MustCompile(`mux\.Handle(Func)?\("([A-Z]+ /api/[^"]*)"`) + + entries, err := os.ReadDir(".") + if err != nil { + t.Fatalf("read package directory: %v", err) + } + var offenders []string + for _, entry := range entries { + name := entry.Name() + if entry.IsDir() || !strings.HasSuffix(name, ".go") || strings.HasSuffix(name, "_test.go") { + continue + } + source, err := os.ReadFile(filepath.Clean(name)) + if err != nil { + t.Fatalf("read %s: %v", name, err) + } + for _, line := range strings.Split(string(source), "\n") { + m := route.FindStringSubmatch(line) + if m == nil || exempt[m[2]] { + continue + } + guarded := false + for _, wrapper := range allowed { + if strings.Contains(line, wrapper) { + guarded = true + break + } + } + if !guarded { + offenders = append(offenders, name+": "+strings.TrimSpace(line)) + } + } + } + if len(offenders) > 0 { + t.Fatalf("these routes are registered without a permission -- wrap them in s.scopedRoute(permission, ...):\n %s", + strings.Join(offenders, "\n ")) + } +} + +// Every right the account editor offers must be one the routes actually check, +// and vice versa -- a name in one list and not the other is either a right +// nobody can be granted or a checkbox that grants nothing. +func TestAssignablePermissionsMatchWhatRoutesEnforce(t *testing.T) { + assignable := make(map[string]bool, len(assignablePermissions())) + for _, p := range assignablePermissions() { + if p == permissionSessionOnly { + t.Fatal("permissionSessionOnly is not a grantable right and must not be offered") + } + if assignable[p] { + t.Fatalf("permission %q is offered twice", p) + } + assignable[p] = true + } + if len(assignable) == 0 { + t.Fatal("no assignable permissions") + } + // Spot-check the pairs the sections are built around, so a rename that + // misses one half is caught here rather than by an operator who suddenly + // cannot open a page. + for _, required := range []string{ + permissionAccountsRead, permissionAccountsManage, + permissionChannelsRead, permissionChannelsManage, + permissionBotsRead, permissionBotsManage, + permissionMessagesRead, permissionMessagesManage, + permissionContentRead, permissionContentManage, + permissionUsernamesRead, permissionUsernamesManage, + permissionStorageRead, permissionStorageManage, + permissionBroadcastsRead, permissionBroadcastsSend, + permissionModerationReview, permissionDashboardRead, + permissionAdminsManage, permissionServerManage, + } { + if !assignable[required] { + t.Errorf("permission %q is enforced somewhere but cannot be granted", required) + } + } +} + +// permissionSessionOnly must stay the empty string: scopedRoute distinguishes +// "a session is enough" from a real right by that emptiness, and panelPermissions +// drops empty entries, so it can never be smuggled into an account's list. +func TestSessionOnlyIsNotGrantable(t *testing.T) { + if permissionSessionOnly != "" { + t.Fatalf("permissionSessionOnly = %q, want the empty string", permissionSessionOnly) + } + perms := newPanelPermissions([]string{permissionSessionOnly, permissionAccountsRead}) + if perms.Has(permissionSessionOnly) { + t.Fatal("an empty permission was treated as granted") + } + if !perms.Has(permissionAccountsRead) { + t.Fatal("a real permission alongside it was lost") + } +} diff --git a/cmd/telesrv-admin/security.go b/cmd/telesrv-admin/security.go index cf180864..38f786a7 100644 --- a/cmd/telesrv-admin/security.go +++ b/cmd/telesrv-admin/security.go @@ -36,6 +36,8 @@ import ( // TELESRV_ADMIN_UI_PERMISSIONS and the ones the admin API enforces. const ( permissionAll = "*" + permissionPremiumManage = "premium.manage" + permissionBotTokenRead = "bots.token.read" permissionVerificationReview = "verification.review" permissionVerificationRevoke = "verification.revoke" // Third-party bot verification. Deliberately not implied by the official @@ -45,8 +47,122 @@ const ( // curates the icon catalogue and strips granted marks. permissionBotVerificationReview = "botverification.review" permissionBotVerificationManage = "botverification.manage" + // permissionServerManage gates the whole Server Settings panel: identity + // (name/description/icon), .env editing, and Restart/Update -- all of it + // meaningfully more sensitive than any domain-data action above (.env + // editing exposes every secret the deployment holds; Restart/Update runs + // git/go and bounces the live MTProto process), so it is one right, not + // split into review/manage like the sections above. + permissionServerManage = "server.manage" + // permissionAdminsManage gates the operator accounts themselves: creating + // them, editing their rights, disabling them, resetting their passwords. + // + // It is the one right that can grant every other right, so it is never + // implied by anything else and is worth handing out to far fewer people + // than server.manage. guardManagerRemoval additionally refuses the edit + // that would leave nobody holding it. + permissionAdminsManage = "admins.manage" + + // Section rights, in read/manage pairs that follow the sidebar. Reading a + // section and changing it are separate grants because most of the people + // who need to look at this data never need to alter it. + permissionAccountsRead = "accounts.read" + permissionAccountsManage = "accounts.manage" + permissionChannelsRead = "channels.read" + permissionChannelsManage = "channels.manage" + permissionBotsRead = "bots.read" + permissionBotsManage = "bots.manage" + permissionMessagesRead = "messages.read" + permissionMessagesManage = "messages.manage" + permissionModerationReview = "moderation.review" + permissionBroadcastsRead = "broadcasts.read" + permissionBroadcastsSend = "broadcasts.send" + permissionStorageRead = "storage.read" + permissionStorageManage = "storage.manage" + // Sticker packs, emoji packs and the GIF catalogue: one section as far as + // the panel is concerned, so one pair of rights. + permissionContentRead = "content.read" + permissionContentManage = "content.manage" + permissionUsernamesRead = "usernames.read" + permissionUsernamesManage = "usernames.manage" + permissionDashboardRead = "dashboard.read" + + // permissionSessionOnly marks the handful of routes that need a session but + // no right: reading who you are, and signing out. It is not a grantable + // name -- scopedRoute treats it as "authenticated is enough" -- so it can + // never be typed into an account's permission list by mistake. + permissionSessionOnly = "" ) +// assignablePermissions is the vocabulary the operator-accounts screen offers. +// +// The wildcard is deliberately absent: it is meaningful in +// TELESRV_ADMIN_UI_PERMISSIONS for the break-glass login, but handing "*" to a +// named account through a UI is how least privilege quietly stops being a +// thing. An operator who genuinely needs everything gets every entry ticked, +// which at least leaves a legible record of what was granted. +func assignablePermissions() []string { + return []string{ + permissionAccountsRead, + permissionAccountsManage, + permissionChannelsRead, + permissionChannelsManage, + permissionBotsRead, + permissionBotsManage, + permissionMessagesRead, + permissionMessagesManage, + permissionModerationReview, + permissionBroadcastsRead, + permissionBroadcastsSend, + permissionContentRead, + permissionContentManage, + permissionUsernamesRead, + permissionUsernamesManage, + permissionStorageRead, + permissionStorageManage, + permissionDashboardRead, + permissionPremiumManage, + permissionBotTokenRead, + permissionVerificationReview, + permissionVerificationRevoke, + permissionBotVerificationReview, + permissionBotVerificationManage, + permissionServerManage, + permissionAdminsManage, + } +} + +// scopedRoute is the only way an API route should be registered. Requiring the +// permission as an argument is what makes the panel deny-by-default: a route +// cannot be added without someone stating which right it belongs to, so the +// failure mode of forgetting is a compile error rather than an endpoint that +// quietly answers to everyone. +// +// permissionSessionOnly is the deliberate exception, spelled out at each use. +func (s *server) scopedRoute(permission string, handler http.Handler) http.Handler { + if permission == permissionSessionOnly { + return s.requireAuthAPI(handler) + } + return s.requireAuthAPI(s.requirePermission(permission, handler)) +} + +// scopedRouteAll is scopedRoute for a route that needs more than one right at +// once -- taking a granted verification badge away needs both the right to work +// the queue and the separate right to revoke. Every permission must be held; +// they are requirements, not alternatives. +func (s *server) scopedRouteAll(permissions []string, handler http.Handler) http.Handler { + if len(permissions) == 0 { + // Refusing outright beats silently degrading to "any session": an empty + // list here is a mistake at the call site, not a way to open a route. + panic("scopedRouteAll: no permissions given") + } + wrapped := handler + for i := len(permissions) - 1; i >= 0; i-- { + wrapped = s.requirePermission(permissions[i], wrapped) + } + return s.requireAuthAPI(wrapped) +} + type permissionsKey struct{} // requireAuthAPI is the gate on every authenticated API route: a valid session, @@ -67,12 +183,46 @@ func (s *server) requireAuthAPI(next http.Handler) http.Handler { if !checkMutationSafety(w, r, claims) { return } + // Rights inside the cookie are a 12-hour snapshot; the account they + // belong to may have been disabled, demoted or had its password changed + // since. Re-read it and use what the database says now, so revocation + // takes effect on the next request rather than at session expiry. + permissions, ok := s.currentSessionPermissions(r.Context(), claims) + if !ok { + clearSessionCookie(w) + writeAPIError(w, http.StatusUnauthorized, "session is no longer valid") + return + } ctx := context.WithValue(r.Context(), actorKey{}, claims.Actor) - ctx = context.WithValue(ctx, permissionsKey{}, newPanelPermissions(claims.Permissions)) + ctx = context.WithValue(ctx, permissionsKey{}, permissions) next.ServeHTTP(w, r.WithContext(ctx)) }) } +// currentSessionPermissions resolves the rights this request actually gets. +// +// The break-glass operator (UserID 0) has no database row and keeps the +// configured set -- that login exists precisely for when the database cannot +// be consulted, so it must not depend on one. +// +// A named account is re-read every request. Anything that moved its token +// epoch invalidates the session; anything that narrowed its permissions +// narrows this request. A read failure is treated as a refusal rather than as +// permission, so a database outage cannot silently widen access. +func (s *server) currentSessionPermissions(ctx context.Context, claims sessionClaims) (panelPermissions, bool) { + if claims.UserID == 0 { + return newPanelPermissions(claims.Permissions), true + } + if s.read == nil { + return panelPermissions{}, false + } + enabled, epoch, permissions, err := s.read.AdminConsoleSessionState(ctx, claims.UserID) + if err != nil || !enabled || epoch != claims.Epoch { + return panelPermissions{}, false + } + return newPanelPermissions(permissions), true +} + // requirePermission refuses a session that was not granted the right, before the // request ever reaches the admin API. The panel is the only caller that can be // driven by a browser, so the check belongs here as well as upstream: a 403 from diff --git a/cmd/telesrv-admin/server.go b/cmd/telesrv-admin/server.go index f61f3626..bb212b57 100644 --- a/cmd/telesrv-admin/server.go +++ b/cmd/telesrv-admin/server.go @@ -18,9 +18,15 @@ import ( "strings" "time" + "github.com/iamxvbaba/td/tg" + "github.com/iamxvbaba/td/tlprofile" + "golang.org/x/sync/errgroup" + "telesrv/internal/admin" "telesrv/internal/domain" "telesrv/internal/hoststats" + "telesrv/internal/identity" + "telesrv/internal/procctl" ) //go:embed web/dist @@ -32,6 +38,8 @@ type server struct { hostStats *hoststats.Poller web fs.FS webServer http.Handler + identity *identity.Store + serverCtl *procctl.Manager } func newServer(cfg uiConfig, read *readStore, hostStats *hoststats.Poller) (*server, error) { @@ -45,6 +53,8 @@ func newServer(cfg uiConfig, read *readStore, hostStats *hoststats.Poller) (*ser hostStats: hostStats, web: web, webServer: http.FileServer(http.FS(web)), + identity: identity.NewStore(cfg.IdentityDir), + serverCtl: procctl.NewManager(cfg.RepoRoot), }, nil } @@ -54,88 +64,106 @@ func (s *server) routes() http.Handler { // Logout goes through the same gate as every other mutating route: a forced // logout is a state change, and an invalid session is cleared by the gate // itself, so nothing is stranded by protecting it. - mux.Handle("POST /api/logout", s.requireAuthAPI(http.HandlerFunc(s.handleAPILogout))) - mux.Handle("GET /api/session", s.requireAuthAPI(http.HandlerFunc(s.handleSession))) - mux.Handle("GET /api/dashboard", s.requireAuthAPI(http.HandlerFunc(s.handleDashboardAPI))) - mux.Handle("GET /api/accounts", s.requireAuthAPI(http.HandlerFunc(s.handleAccountsAPI))) - mux.Handle("GET /api/accounts/stats", s.requireAuthAPI(http.HandlerFunc(s.handleAccountsStatsAPI))) - mux.Handle("GET /api/accounts/shared-devices", s.requireAuthAPI(http.HandlerFunc(s.handleSharedDeviceGroupsAPI))) - mux.Handle("GET /api/broadcasts", s.requireAuthAPI(http.HandlerFunc(s.handleBroadcastsAPI))) - mux.Handle("GET /api/accounts/{id}", s.requireAuthAPI(http.HandlerFunc(s.handleAccountDetailAPI))) - mux.Handle("GET /api/accounts/{id}/avatar", s.requireAuthAPI(http.HandlerFunc(s.handleAccountAvatarAPI))) - mux.Handle("GET /api/channels", s.requireAuthAPI(http.HandlerFunc(s.handleChannelsAPI))) - mux.Handle("GET /api/channels/{id}", s.requireAuthAPI(http.HandlerFunc(s.handleChannelDetailAPI))) - mux.Handle("GET /api/channels/{id}/avatar", s.requireAuthAPI(http.HandlerFunc(s.handleChannelAvatarAPI))) - mux.Handle("GET /api/bots", s.requireAuthAPI(http.HandlerFunc(s.handleBotsAPI))) - mux.Handle("GET /api/bots/{id}", s.requireAuthAPI(http.HandlerFunc(s.handleBotDetailAPI))) - mux.Handle("GET /api/emoji", s.requireAuthAPI(http.HandlerFunc(s.handleEmojiAPI))) - mux.Handle("GET /api/emoji/{id}/animation", s.requireAuthAPI(http.HandlerFunc(s.handleEmojiAnimationAPI))) - mux.Handle("GET /api/messages", s.requireAuthAPI(http.HandlerFunc(s.handleMessagesAPI))) - mux.Handle("GET /api/messages/detail", s.requireAuthAPI(http.HandlerFunc(s.handleMessageDetailAPI))) - mux.Handle("GET /api/messages/groups", s.requireAuthAPI(http.HandlerFunc(s.handleGroupMessagesAPI))) - mux.Handle("GET /api/messages/groups/detail", s.requireAuthAPI(http.HandlerFunc(s.handleGroupMessageDetailAPI))) - mux.Handle("GET /api/collectible-usernames", s.requireAuthAPI(http.HandlerFunc(s.handleCollectibleUsernamesAPI))) - mux.Handle("GET /api/reserved-usernames", s.requireAuthAPI(http.HandlerFunc(s.handleReservedUsernamesAPI))) - mux.Handle("GET /api/collectible-usernames/{id}", s.requireAuthAPI(http.HandlerFunc(s.handleCollectibleUsernameDetailAPI))) - mux.Handle("GET /api/storage/stats", s.requireAuthAPI(http.HandlerFunc(s.handleStorageStatsAPI))) - mux.Handle("GET /api/storage/accounts", s.requireAuthAPI(http.HandlerFunc(s.handleStorageAccountsAPI))) - mux.Handle("GET /api/moderation/cases", s.requireAuthAPI(http.HandlerFunc(s.handleModerationCasesAPI))) - mux.Handle("GET /api/moderation/cases/{id}", s.requireAuthAPI(http.HandlerFunc(s.handleModerationCaseAPI))) - mux.Handle("GET /api/moderation/reports/{id}", s.requireAuthAPI(http.HandlerFunc(s.handleModerationReportAPI))) - mux.Handle("POST /api/moderation/cases/{id}/claim", s.requireAuthAPI(http.HandlerFunc(s.handleClaimModerationCaseAPI))) - mux.Handle("POST /api/moderation/cases/{id}/decide", s.requireAuthAPI(http.HandlerFunc(s.handleDecideModerationCaseAPI))) - mux.Handle("POST /api/moderation/cases/{id}/appeals/{appeal_id}/review", s.requireAuthAPI(http.HandlerFunc(s.handleReviewModerationAppealAPI))) - mux.Handle("POST /api/actions/set-frozen", s.requireAuthAPI(http.HandlerFunc(s.handleSetAccountFrozenAPI))) - mux.Handle("POST /api/actions/grant-premium", s.requireAuthAPI(http.HandlerFunc(s.handleGrantPremiumAPI))) - mux.Handle("POST /api/actions/set-verified", s.requireAuthAPI(http.HandlerFunc(s.handleSetVerifiedAPI))) - mux.Handle("POST /api/actions/set-account-flags", s.requireAuthAPI(http.HandlerFunc(s.handleSetUserFlagsAPI))) - mux.Handle("POST /api/actions/set-channel-flags", s.requireAuthAPI(http.HandlerFunc(s.handleSetChannelFlagsAPI))) - mux.Handle("POST /api/actions/set-support", s.requireAuthAPI(http.HandlerFunc(s.handleSetSupportAPI))) - mux.Handle("POST /api/actions/set-account-username", s.requireAuthAPI(http.HandlerFunc(s.handleSetUsernameAPI))) - mux.Handle("POST /api/actions/set-account-profile", s.requireAuthAPI(http.HandlerFunc(s.handleSetProfileAPI))) - mux.Handle("POST /api/actions/set-account-phone", s.requireAuthAPI(http.HandlerFunc(s.handleSetPhoneAPI))) - mux.Handle("POST /api/actions/set-account-avatar", s.requireAuthAPI(http.HandlerFunc(s.handleSetAccountAvatarAPI))) - mux.Handle("POST /api/actions/set-account-login-email", s.requireAuthAPI(http.HandlerFunc(s.handleSetLoginEmailAPI))) - mux.Handle("POST /api/actions/set-account-color", s.requireAuthAPI(http.HandlerFunc(s.handleSetUserColorAPI))) - mux.Handle("POST /api/actions/set-account-emoji-status", s.requireAuthAPI(http.HandlerFunc(s.handleSetUserEmojiStatusAPI))) - mux.Handle("POST /api/actions/set-channel-avatar", s.requireAuthAPI(http.HandlerFunc(s.handleSetChannelAvatarAPI))) - mux.Handle("POST /api/actions/set-channel-settings", s.requireAuthAPI(http.HandlerFunc(s.handleSetChannelSettingsAPI))) - mux.Handle("POST /api/actions/set-channel-username", s.requireAuthAPI(http.HandlerFunc(s.handleSetChannelUsernameAPI))) - mux.Handle("POST /api/actions/set-channel-color", s.requireAuthAPI(http.HandlerFunc(s.handleSetChannelColorAPI))) - mux.Handle("POST /api/actions/set-channel-emoji-status", s.requireAuthAPI(http.HandlerFunc(s.handleSetChannelEmojiStatusAPI))) - mux.Handle("POST /api/actions/create-bot", s.requireAuthAPI(http.HandlerFunc(s.handleCreateBotAPI))) - mux.Handle("POST /api/actions/create-broadcast", s.requireAuthAPI(http.HandlerFunc(s.handleCreateBroadcastAPI))) - mux.Handle("POST /api/actions/delete-bot", s.requireAuthAPI(http.HandlerFunc(s.handleDeleteBotAPI))) - mux.Handle("POST /api/actions/export-bot-token", s.requireAuthAPI(http.HandlerFunc(s.handleExportBotTokenAPI))) - mux.Handle("POST /api/actions/set-channel-verified", s.requireAuthAPI(http.HandlerFunc(s.handleSetChannelVerifiedAPI))) - mux.Handle("POST /api/actions/revoke-sessions", s.requireAuthAPI(http.HandlerFunc(s.handleRevokeSessionsAPI))) - mux.Handle("POST /api/actions/delete-messages", s.requireAuthAPI(http.HandlerFunc(s.handleDeleteMessagesAPI))) - mux.Handle("POST /api/actions/delete-history", s.requireAuthAPI(http.HandlerFunc(s.handleDeleteHistoryAPI))) - mux.Handle("GET /api/stickers", s.requireAuthAPI(http.HandlerFunc(s.handleStickerSetsAPI))) - mux.Handle("GET /api/stickers/{id}/documents", s.requireAuthAPI(http.HandlerFunc(s.handleStickerSetDocumentsAPI))) - mux.Handle("GET /api/stickers/documents/{id}/animation", s.requireAuthAPI(http.HandlerFunc(s.handleStickerDocumentAnimationAPI))) - mux.Handle("GET /api/gif-catalog/documents/{id}/preview", s.requireAuthAPI(http.HandlerFunc(s.handleGifCatalogDocumentPreviewAPI))) - mux.Handle("POST /api/actions/set-sticker-set-archived", s.requireAuthAPI(http.HandlerFunc(s.handleSetStickerSetArchivedAPI))) - mux.Handle("POST /api/actions/set-sticker-set-sort-order", s.requireAuthAPI(http.HandlerFunc(s.handleSetStickerSetSortOrderAPI))) - mux.Handle("POST /api/actions/rename-sticker-set", s.requireAuthAPI(http.HandlerFunc(s.handleRenameStickerSetAPI))) - mux.Handle("POST /api/actions/delete-sticker-set", s.requireAuthAPI(http.HandlerFunc(s.handleDeleteStickerSetAPI))) - mux.Handle("POST /api/actions/create-sticker-set", s.requireAuthAPI(http.HandlerFunc(s.handleCreateStickerSetAPI))) - mux.Handle("POST /api/actions/add-sticker-to-set", s.requireAuthAPI(http.HandlerFunc(s.handleAddStickerToSetAPI))) - mux.Handle("POST /api/actions/remove-sticker-from-set", s.requireAuthAPI(http.HandlerFunc(s.handleRemoveStickerFromSetAPI))) - mux.Handle("GET /api/gif-catalog", s.requireAuthAPI(http.HandlerFunc(s.handleGifCatalogAPI))) - mux.Handle("POST /api/actions/create-gif-catalog-entry", s.requireAuthAPI(http.HandlerFunc(s.handleCreateGifCatalogEntryAPI))) - mux.Handle("POST /api/actions/set-gif-catalog-enabled", s.requireAuthAPI(http.HandlerFunc(s.handleSetGifCatalogEnabledAPI))) - mux.Handle("POST /api/actions/set-gif-catalog-sort-order", s.requireAuthAPI(http.HandlerFunc(s.handleSetGifCatalogSortOrderAPI))) - mux.Handle("POST /api/actions/set-gif-catalog-category", s.requireAuthAPI(http.HandlerFunc(s.handleSetGifCatalogCategoryAPI))) - mux.Handle("POST /api/actions/auto-categorize-gif-catalog", s.requireAuthAPI(http.HandlerFunc(s.handleAutoCategorizeGifCatalogAPI))) - mux.Handle("POST /api/actions/delete-uncategorized-gifs", s.requireAuthAPI(http.HandlerFunc(s.handleDeleteUncategorizedGifsAPI))) - mux.Handle("POST /api/actions/delete-gif-catalog-entry", s.requireAuthAPI(http.HandlerFunc(s.handleDeleteGifCatalogEntryAPI))) - mux.Handle("POST /api/actions/reserve-username", s.requireAuthAPI(http.HandlerFunc(s.handleReserveUsernameAPI))) - mux.Handle("POST /api/actions/unreserve-username", s.requireAuthAPI(http.HandlerFunc(s.handleUnreserveUsernameAPI))) - mux.Handle("POST /api/actions/mint-collectible-username", s.requireAuthAPI(http.HandlerFunc(s.handleMintCollectibleUsernameAPI))) - mux.Handle("POST /api/actions/transfer-collectible-username", s.requireAuthAPI(http.HandlerFunc(s.handleTransferCollectibleUsernameAPI))) - mux.Handle("POST /api/actions/revoke-collectible-username", s.requireAuthAPI(http.HandlerFunc(s.handleRevokeCollectibleUsernameAPI))) - mux.Handle("POST /api/actions/delete-collectible-username", s.requireAuthAPI(http.HandlerFunc(s.handleDeleteCollectibleUsernameAPI))) + mux.Handle("POST /api/logout", s.scopedRoute(permissionSessionOnly, http.HandlerFunc(s.handleAPILogout))) + // Unauthenticated on purpose: the login screen shows which server it is, + // and the name/icon are already public from owpengram-server's own client + // endpoints. See publicbranding.go. + mux.HandleFunc("GET /api/public/branding", s.handlePublicBrandingAPI) + mux.HandleFunc("GET /api/public/icon", s.handlePublicIconAPI) + + mux.Handle("GET /api/session", s.scopedRoute(permissionSessionOnly, http.HandlerFunc(s.handleSession))) + + // Operator accounts. Every one of these is gated on admins.manage -- the + // right that can hand out every other right -- so they are registered + // together rather than scattered among the domain routes. + mux.Handle("GET /api/admin-users", s.requireAdminsManage(http.HandlerFunc(s.handleListAdminUsersAPI))) + // Mutations live under /api/actions/* like every other command in the + // panel, so they get the same reason + dry-run + confirm flow. + mux.Handle("POST /api/actions/create-admin-operator", s.requireAdminsManage(http.HandlerFunc(s.handleCreateAdminUserAPI))) + mux.Handle("POST /api/actions/set-admin-operator-access", s.requireAdminsManage(http.HandlerFunc(s.handleUpdateAdminUserAPI))) + mux.Handle("POST /api/actions/set-admin-operator-password", s.requireAdminsManage(http.HandlerFunc(s.handleSetAdminUserPasswordAPI))) + mux.Handle("GET /api/dashboard", s.scopedRoute(permissionDashboardRead, http.HandlerFunc(s.handleDashboardAPI))) + mux.Handle("GET /api/accounts", s.scopedRoute(permissionAccountsRead, http.HandlerFunc(s.handleAccountsAPI))) + mux.Handle("GET /api/accounts/stats", s.scopedRoute(permissionAccountsRead, http.HandlerFunc(s.handleAccountsStatsAPI))) + mux.Handle("GET /api/accounts/shared-devices", s.scopedRoute(permissionAccountsRead, http.HandlerFunc(s.handleSharedDeviceGroupsAPI))) + mux.Handle("GET /api/broadcasts", s.scopedRoute(permissionBroadcastsRead, http.HandlerFunc(s.handleBroadcastsAPI))) + mux.Handle("GET /api/accounts/{id}", s.scopedRoute(permissionAccountsRead, http.HandlerFunc(s.handleAccountDetailAPI))) + mux.Handle("GET /api/accounts/{id}/avatar", s.scopedRoute(permissionAccountsRead, http.HandlerFunc(s.handleAccountAvatarAPI))) + mux.Handle("GET /api/channels", s.scopedRoute(permissionChannelsRead, http.HandlerFunc(s.handleChannelsAPI))) + mux.Handle("GET /api/channels/{id}", s.scopedRoute(permissionChannelsRead, http.HandlerFunc(s.handleChannelDetailAPI))) + mux.Handle("GET /api/channels/{id}/avatar", s.scopedRoute(permissionChannelsRead, http.HandlerFunc(s.handleChannelAvatarAPI))) + mux.Handle("GET /api/bots", s.scopedRoute(permissionBotsRead, http.HandlerFunc(s.handleBotsAPI))) + mux.Handle("GET /api/bots/{id}", s.scopedRoute(permissionBotsRead, http.HandlerFunc(s.handleBotDetailAPI))) + mux.Handle("GET /api/emoji", s.scopedRoute(permissionContentRead, http.HandlerFunc(s.handleEmojiAPI))) + mux.Handle("GET /api/emoji/{id}/animation", s.scopedRoute(permissionContentRead, http.HandlerFunc(s.handleEmojiAnimationAPI))) + mux.Handle("GET /api/messages", s.scopedRoute(permissionMessagesRead, http.HandlerFunc(s.handleMessagesAPI))) + mux.Handle("GET /api/messages/detail", s.scopedRoute(permissionMessagesRead, http.HandlerFunc(s.handleMessageDetailAPI))) + mux.Handle("GET /api/messages/groups", s.scopedRoute(permissionMessagesRead, http.HandlerFunc(s.handleGroupMessagesAPI))) + mux.Handle("GET /api/messages/groups/detail", s.scopedRoute(permissionMessagesRead, http.HandlerFunc(s.handleGroupMessageDetailAPI))) + mux.Handle("GET /api/collectible-usernames", s.scopedRoute(permissionUsernamesRead, http.HandlerFunc(s.handleCollectibleUsernamesAPI))) + mux.Handle("GET /api/reserved-usernames", s.scopedRoute(permissionUsernamesRead, http.HandlerFunc(s.handleReservedUsernamesAPI))) + mux.Handle("GET /api/collectible-usernames/{id}", s.scopedRoute(permissionUsernamesRead, http.HandlerFunc(s.handleCollectibleUsernameDetailAPI))) + mux.Handle("GET /api/storage/stats", s.scopedRoute(permissionStorageRead, http.HandlerFunc(s.handleStorageStatsAPI))) + mux.Handle("GET /api/storage/accounts", s.scopedRoute(permissionStorageRead, http.HandlerFunc(s.handleStorageAccountsAPI))) + mux.Handle("GET /api/moderation/cases", s.scopedRoute(permissionModerationReview, http.HandlerFunc(s.handleModerationCasesAPI))) + mux.Handle("GET /api/moderation/cases/{id}", s.scopedRoute(permissionModerationReview, http.HandlerFunc(s.handleModerationCaseAPI))) + mux.Handle("GET /api/moderation/reports/{id}", s.scopedRoute(permissionModerationReview, http.HandlerFunc(s.handleModerationReportAPI))) + mux.Handle("POST /api/moderation/cases/{id}/claim", s.scopedRoute(permissionModerationReview, http.HandlerFunc(s.handleClaimModerationCaseAPI))) + mux.Handle("POST /api/moderation/cases/{id}/decide", s.scopedRoute(permissionModerationReview, http.HandlerFunc(s.handleDecideModerationCaseAPI))) + mux.Handle("POST /api/moderation/cases/{id}/appeals/{appeal_id}/review", s.scopedRoute(permissionModerationReview, http.HandlerFunc(s.handleReviewModerationAppealAPI))) + mux.Handle("POST /api/actions/set-frozen", s.scopedRoute(permissionAccountsManage, http.HandlerFunc(s.handleSetAccountFrozenAPI))) + mux.Handle("POST /api/actions/grant-premium", s.scopedRoute(permissionPremiumManage, http.HandlerFunc(s.handleGrantPremiumAPI))) + mux.Handle("POST /api/actions/set-verified", s.scopedRoute(permissionVerificationReview, http.HandlerFunc(s.handleSetVerifiedAPI))) + mux.Handle("POST /api/actions/set-account-flags", s.scopedRoute(permissionAccountsManage, http.HandlerFunc(s.handleSetUserFlagsAPI))) + mux.Handle("POST /api/actions/set-channel-flags", s.scopedRoute(permissionChannelsManage, http.HandlerFunc(s.handleSetChannelFlagsAPI))) + mux.Handle("POST /api/actions/set-support", s.scopedRoute(permissionAccountsManage, http.HandlerFunc(s.handleSetSupportAPI))) + mux.Handle("POST /api/actions/set-account-username", s.scopedRoute(permissionAccountsManage, http.HandlerFunc(s.handleSetUsernameAPI))) + mux.Handle("POST /api/actions/set-account-profile", s.scopedRoute(permissionAccountsManage, http.HandlerFunc(s.handleSetProfileAPI))) + mux.Handle("POST /api/actions/set-account-phone", s.scopedRoute(permissionAccountsManage, http.HandlerFunc(s.handleSetPhoneAPI))) + mux.Handle("POST /api/actions/set-account-avatar", s.scopedRoute(permissionAccountsManage, http.HandlerFunc(s.handleSetAccountAvatarAPI))) + mux.Handle("POST /api/actions/set-account-avatar-video", s.scopedRoute(permissionAccountsManage, http.HandlerFunc(s.handleSetAccountAvatarVideoAPI))) + mux.Handle("POST /api/actions/set-account-login-email", s.scopedRoute(permissionAccountsManage, http.HandlerFunc(s.handleSetLoginEmailAPI))) + mux.Handle("POST /api/actions/set-account-color", s.scopedRoute(permissionAccountsManage, http.HandlerFunc(s.handleSetUserColorAPI))) + mux.Handle("POST /api/actions/set-account-emoji-status", s.scopedRoute(permissionAccountsManage, http.HandlerFunc(s.handleSetUserEmojiStatusAPI))) + mux.Handle("POST /api/actions/set-channel-avatar", s.scopedRoute(permissionChannelsManage, http.HandlerFunc(s.handleSetChannelAvatarAPI))) + mux.Handle("POST /api/actions/set-channel-settings", s.scopedRoute(permissionChannelsManage, http.HandlerFunc(s.handleSetChannelSettingsAPI))) + mux.Handle("POST /api/actions/set-channel-username", s.scopedRoute(permissionChannelsManage, http.HandlerFunc(s.handleSetChannelUsernameAPI))) + mux.Handle("POST /api/actions/set-channel-color", s.scopedRoute(permissionChannelsManage, http.HandlerFunc(s.handleSetChannelColorAPI))) + mux.Handle("POST /api/actions/set-channel-emoji-status", s.scopedRoute(permissionChannelsManage, http.HandlerFunc(s.handleSetChannelEmojiStatusAPI))) + mux.Handle("POST /api/actions/create-bot", s.scopedRoute(permissionBotsManage, http.HandlerFunc(s.handleCreateBotAPI))) + mux.Handle("POST /api/actions/create-broadcast", s.scopedRoute(permissionBroadcastsSend, http.HandlerFunc(s.handleCreateBroadcastAPI))) + mux.Handle("POST /api/actions/delete-bot", s.scopedRoute(permissionBotsManage, http.HandlerFunc(s.handleDeleteBotAPI))) + mux.Handle("POST /api/actions/export-bot-token", s.scopedRoute(permissionBotTokenRead, http.HandlerFunc(s.handleExportBotTokenAPI))) + mux.Handle("POST /api/actions/set-channel-verified", s.scopedRoute(permissionVerificationReview, http.HandlerFunc(s.handleSetChannelVerifiedAPI))) + mux.Handle("POST /api/actions/revoke-sessions", s.scopedRoute(permissionAccountsManage, http.HandlerFunc(s.handleRevokeSessionsAPI))) + mux.Handle("POST /api/actions/delete-messages", s.scopedRoute(permissionMessagesManage, http.HandlerFunc(s.handleDeleteMessagesAPI))) + mux.Handle("POST /api/actions/delete-history", s.scopedRoute(permissionMessagesManage, http.HandlerFunc(s.handleDeleteHistoryAPI))) + mux.Handle("GET /api/stickers", s.scopedRoute(permissionContentRead, http.HandlerFunc(s.handleStickerSetsAPI))) + mux.Handle("GET /api/stickers/{id}/documents", s.scopedRoute(permissionContentRead, http.HandlerFunc(s.handleStickerSetDocumentsAPI))) + mux.Handle("GET /api/stickers/documents/{id}/animation", s.scopedRoute(permissionContentRead, http.HandlerFunc(s.handleStickerDocumentAnimationAPI))) + mux.Handle("GET /api/gif-catalog/documents/{id}/preview", s.scopedRoute(permissionContentRead, http.HandlerFunc(s.handleGifCatalogDocumentPreviewAPI))) + mux.Handle("POST /api/actions/set-sticker-set-archived", s.scopedRoute(permissionContentManage, http.HandlerFunc(s.handleSetStickerSetArchivedAPI))) + mux.Handle("POST /api/actions/set-sticker-set-sort-order", s.scopedRoute(permissionContentManage, http.HandlerFunc(s.handleSetStickerSetSortOrderAPI))) + mux.Handle("POST /api/actions/rename-sticker-set", s.scopedRoute(permissionContentManage, http.HandlerFunc(s.handleRenameStickerSetAPI))) + mux.Handle("POST /api/actions/delete-sticker-set", s.scopedRoute(permissionContentManage, http.HandlerFunc(s.handleDeleteStickerSetAPI))) + mux.Handle("POST /api/actions/create-sticker-set", s.scopedRoute(permissionContentManage, http.HandlerFunc(s.handleCreateStickerSetAPI))) + mux.Handle("POST /api/actions/add-sticker-to-set", s.scopedRoute(permissionContentManage, http.HandlerFunc(s.handleAddStickerToSetAPI))) + mux.Handle("POST /api/actions/remove-sticker-from-set", s.scopedRoute(permissionContentManage, http.HandlerFunc(s.handleRemoveStickerFromSetAPI))) + mux.Handle("GET /api/gif-catalog", s.scopedRoute(permissionContentRead, http.HandlerFunc(s.handleGifCatalogAPI))) + mux.Handle("POST /api/actions/create-gif-catalog-entry", s.scopedRoute(permissionContentManage, http.HandlerFunc(s.handleCreateGifCatalogEntryAPI))) + mux.Handle("POST /api/actions/set-gif-catalog-enabled", s.scopedRoute(permissionContentManage, http.HandlerFunc(s.handleSetGifCatalogEnabledAPI))) + mux.Handle("POST /api/actions/set-gif-catalog-sort-order", s.scopedRoute(permissionContentManage, http.HandlerFunc(s.handleSetGifCatalogSortOrderAPI))) + mux.Handle("POST /api/actions/set-gif-catalog-category", s.scopedRoute(permissionContentManage, http.HandlerFunc(s.handleSetGifCatalogCategoryAPI))) + mux.Handle("POST /api/actions/auto-categorize-gif-catalog", s.scopedRoute(permissionContentManage, http.HandlerFunc(s.handleAutoCategorizeGifCatalogAPI))) + mux.Handle("POST /api/actions/delete-uncategorized-gifs", s.scopedRoute(permissionContentManage, http.HandlerFunc(s.handleDeleteUncategorizedGifsAPI))) + mux.Handle("POST /api/actions/storage-manual-purge", s.scopedRoute(permissionStorageManage, http.HandlerFunc(s.handleStorageManualPurgeAPI))) + mux.Handle("POST /api/actions/delete-gif-catalog-entry", s.scopedRoute(permissionContentManage, http.HandlerFunc(s.handleDeleteGifCatalogEntryAPI))) + mux.Handle("POST /api/actions/reserve-username", s.scopedRoute(permissionUsernamesManage, http.HandlerFunc(s.handleReserveUsernameAPI))) + mux.Handle("POST /api/actions/unreserve-username", s.scopedRoute(permissionUsernamesManage, http.HandlerFunc(s.handleUnreserveUsernameAPI))) + mux.Handle("POST /api/actions/mint-collectible-username", s.scopedRoute(permissionUsernamesManage, http.HandlerFunc(s.handleMintCollectibleUsernameAPI))) + mux.Handle("POST /api/actions/transfer-collectible-username", s.scopedRoute(permissionUsernamesManage, http.HandlerFunc(s.handleTransferCollectibleUsernameAPI))) + mux.Handle("POST /api/actions/revoke-collectible-username", s.scopedRoute(permissionUsernamesManage, http.HandlerFunc(s.handleRevokeCollectibleUsernameAPI))) + mux.Handle("POST /api/actions/delete-collectible-username", s.scopedRoute(permissionUsernamesManage, http.HandlerFunc(s.handleDeleteCollectibleUsernameAPI))) // Official platform verification. Every route needs verification.review; // clearing an existing badge needs verification.revoke on top of it. mux.Handle("GET /api/verification/applications", s.verificationRead(s.handleVerificationApplicationsAPI)) @@ -144,9 +172,9 @@ func (s *server) routes() http.Handler { mux.Handle("POST /api/verification/applications/{id}/claim", s.verificationRead(s.handleClaimVerificationAPI)) mux.Handle("POST /api/verification/applications/{id}/approve", s.verificationRead(s.handleApproveVerificationAPI)) mux.Handle("POST /api/verification/applications/{id}/reject", s.verificationRead(s.handleRejectVerificationAPI)) - mux.Handle("POST /api/actions/revoke-verification", s.requireAuthAPI( - s.requirePermission(permissionVerificationReview, - s.requirePermission(permissionVerificationRevoke, http.HandlerFunc(s.handleRevokeVerificationAPI))))) + mux.Handle("POST /api/actions/revoke-verification", s.scopedRouteAll( + []string{permissionVerificationReview, permissionVerificationRevoke}, + http.HandlerFunc(s.handleRevokeVerificationAPI))) // Third-party bot verification. A separate section from the official // verification block above -- separate tables, separate rights, separate routes. // Reads and queue decisions need botverification.review; appointing verifiers, @@ -167,6 +195,26 @@ func (s *server) routes() http.Handler { mux.Handle("POST /api/actions/upsert-verification-icon", s.botVerificationManage(s.handleUpsertVerificationIconAPI)) mux.Handle("POST /api/actions/set-verification-icon-active", s.botVerificationManage(s.handleSetVerificationIconActiveAPI)) mux.Handle("POST /api/actions/revoke-custom-verification", s.botVerificationManage(s.handleRevokeCustomVerificationAPI)) + // Server Settings -- see serversettings.go. Everything here operates + // directly on local files/processes (no RPC hop to owpengram-server's + // in-process admin API), so it works even for actions (Restart/Update) + // that owpengram-server could never safely perform on itself. + mux.Handle("GET /api/server/identity", s.serverManage(s.handleServerIdentityAPI)) + mux.Handle("GET /api/server/add-server-link", s.serverManage(s.handleAddServerLinkAPI)) + mux.Handle("GET /api/server/icon", s.serverManage(s.handleServerIconAPI)) + mux.Handle("POST /api/actions/set-server-identity", s.serverManage(s.handleSetServerIdentityAPI)) + mux.Handle("POST /api/actions/set-welcome-message-templates", s.serverManage(s.handleSetWelcomeMessageTemplatesAPI)) + mux.Handle("POST /api/actions/set-login-code-message-template", s.serverManage(s.handleSetLoginCodeMessageTemplateAPI)) + mux.Handle("POST /api/actions/upload-server-icon", s.serverManage(s.handleUploadServerIconAPI)) + mux.Handle("POST /api/actions/remove-server-icon", s.serverManage(s.handleRemoveServerIconAPI)) + mux.Handle("POST /api/actions/complete-setup", s.serverManage(s.handleCompleteSetupAPI)) + mux.Handle("GET /api/server/env", s.serverManage(s.handleServerEnvAPI)) + mux.Handle("POST /api/actions/update-server-env", s.serverManage(s.handleUpdateServerEnvAPI)) + mux.Handle("GET /api/server/status", s.serverManage(s.handleServerStatusAPI)) + mux.Handle("GET /api/server/docker-status", s.serverManage(s.handleDockerStatusAPI)) + mux.Handle("GET /api/server/check-updates", s.serverManage(s.handleCheckServerUpdatesAPI)) + mux.Handle("POST /api/actions/restart-server", s.serverManage(s.handleRestartServerAPI)) + mux.Handle("POST /api/actions/update-server", s.serverManage(s.handleUpdateServerAPI)) mux.HandleFunc("/api/", func(w http.ResponseWriter, _ *http.Request) { writeAPIError(w, http.StatusNotFound, "api route not found") }) @@ -180,7 +228,11 @@ func actorFromContext(ctx context.Context) string { if actor, ok := ctx.Value(actorKey{}).(string); ok && actor != "" { return actor } - return "admin" + // requireAuthAPI always puts the actor in the context, so this is + // unreachable in practice. It returns a name that is obviously not a real + // operator rather than a plausible one: an audit line reading "admin" would + // silently attribute the action to somebody. + return "unknown" } func (s *server) handleApp(w http.ResponseWriter, r *http.Request) { @@ -197,7 +249,12 @@ func (s *server) handleApp(w http.ResponseWriter, r *http.Request) { } type loginRequest struct { - Secret string `json:"secret"` + // Username selects a named account in admin_console_users. Left empty, the + // credential is checked against TELESRV_ADMIN_UI_PASSWORD / _TOKEN instead, + // which keeps the pre-accounts login working and doubles as the way back in + // if the database is unreachable or every named account is locked out. + Username string `json:"username"` + Secret string `json:"secret"` } // sessionTTL bounds a signed panel session and the CSRF cookie that goes with it, @@ -217,7 +274,11 @@ func (s *server) handleAPILogin(w http.ResponseWriter, r *http.Request) { writeAPIError(w, http.StatusBadRequest, err.Error()) return } - if !s.validSecret(req.Secret) { + identity, ok := s.authenticateLogin(r.Context(), req) + if !ok { + // One message and one status for every failure mode -- unknown account, + // wrong password, disabled account. Saying which would let anyone with + // the login form enumerate operators. writeAPIError(w, http.StatusUnauthorized, "invalid credential") return } @@ -226,9 +287,11 @@ func (s *server) handleAPILogin(w http.ResponseWriter, r *http.Request) { writeAPIError(w, http.StatusInternalServerError, err.Error()) return } - permissions := newPanelPermissions(s.cfg.Permissions) + permissions := newPanelPermissions(identity.permissions) value, err := signSession(s.cfg.SessionKey, sessionClaims{ - Actor: "admin", + Actor: identity.actor, + UserID: identity.userID, + Epoch: identity.epoch, Exp: time.Now().Add(sessionTTL).Unix(), Nonce: newCommandID("sess"), Permissions: permissions.List(), @@ -248,7 +311,7 @@ func (s *server) handleAPILogin(w http.ResponseWriter, r *http.Request) { }) setCSRFCookie(w, csrfToken, sessionTTL) writeJSON(w, http.StatusOK, map[string]any{ - "actor": "admin", + "actor": identity.actor, "permissions": permissions.List(), "csrf_token": csrfToken, "hide_third_party_verification": s.cfg.HideThirdPartyVerification, @@ -257,6 +320,16 @@ func (s *server) handleAPILogin(w http.ResponseWriter, r *http.Request) { func (s *server) validSecret(secret string) bool { if s.cfg.Password != "" && subtle.ConstantTimeCompare([]byte(secret), []byte(s.cfg.Password)) == 1 { + // The password quickstart auto-generates for the very first login + // (see identity.Store.TemporaryPasswordMatches) is only good until + // the first-run wizard finishes -- one login's worth of "how do I + // even get in", not a credential anyone actually chose to keep + // around. A password an operator set on purpose, whether by saving + // one from Server Settings or editing .env by hand, never matches + // the stored generated value, so this never touches it. + if s.identity.TemporaryPasswordMatches(s.cfg.Password) && !s.identity.SetupPending() { + return false + } return true } if s.cfg.Token != "" && subtle.ConstantTimeCompare([]byte(secret), []byte(s.cfg.Token)) == 1 { @@ -270,14 +343,56 @@ func (s *server) handleAPILogout(w http.ResponseWriter, _ *http.Request) { writeJSON(w, http.StatusOK, map[string]any{"ok": true}) } +// supportedTLLayers returns every MTProto TL schema layer this server binary +// can admit and encode for, oldest first. Probes tlprofile.ResolveProfile +// (the vendored td package's registry) rather than hardcoding a range here, +// so this stays correct as new profiles get added upstream without a +// second place to remember to update. tg.Layer is always the newest one and +// is included by construction, since ResolveProfile(tg.Layer) must succeed. +func supportedTLLayers() []int { + var layers []int + for n := 1; n <= tg.Layer; n++ { + if _, ok := tlprofile.ResolveProfile(n); ok { + layers = append(layers, n) + } + } + return layers +} + // handleSession is what the panel asks on load. It reports the permissions the // session carries, so the UI can hide a section the operator may not use rather // than letting them walk into a 403. func (s *server) handleSession(w http.ResponseWriter, r *http.Request) { + build := currentBuildMetadata() writeJSON(w, http.StatusOK, map[string]any{ "actor": actorFromContext(r.Context()), "permissions": permissionsFromContext(r.Context()).List(), "hide_third_party_verification": s.cfg.HideThirdPartyVerification, + // setup_completed gates the first-run wizard -- see + // identity.Store.SetupPending's doc comment for why this reads a + // sentinel file rather than anything in identity.json itself. + "setup_completed": !s.identity.SetupPending(), + // boot_id is random per process start (see main.go) -- Server + // Settings' Restart/Update flow polls this after triggering an + // action and reloads the page once it changes, which is how it + // tells "the old admin process died and a new one answered" apart + // from "the old one is just slow to respond". + "boot_id": bootID, + // api_layers is every MTProto TL schema layer this server binary can + // actually admit and encode for (see tlprofile.ResolveProfile in the + // vendored td package) -- the server is multi-layer (a client on an + // older supported layer still works, not just the newest one), so + // the sidebar shows the whole supported set, not just tg.Layer. + "api_layers": supportedTLLayers(), + // build is this admin binary's own commit -- shown under "Version" + // in the sidebar footer so an operator can tell at a glance which + // build is actually running, independent of the app version string. + "build": map[string]any{ + "commit": build.Commit, + "short_commit": build.shortCommit(), + "dirty": build.Dirty, + "build_time": build.BuildTime, + }, }) } @@ -289,13 +404,26 @@ func (s *server) handleDashboardAPI(w http.ResponseWriter, r *http.Request) { writeAPIError(w, http.StatusServiceUnavailable, "read store is not configured") return } - counts, err := s.read.DashboardCounts(r.Context()) - if err != nil { - writeAPIError(w, http.StatusInternalServerError, err.Error()) - return - } - storage, err := s.read.StorageStats(r.Context()) - if err != nil { + // The two halves hit different tables and neither feeds the other, so the + // page waited for their sum for no reason. Storage in particular is the + // expensive one; running it alongside the counts means the response costs + // whichever is slower rather than both. + var ( + counts DashboardCounts + storage StorageStatsRow + ) + g, gctx := errgroup.WithContext(r.Context()) + g.Go(func() error { + var err error + counts, err = s.read.DashboardCounts(gctx) + return err + }) + g.Go(func() error { + var err error + storage, err = s.read.StorageStats(gctx) + return err + }) + if err := g.Wait(); err != nil { writeAPIError(w, http.StatusInternalServerError, err.Error()) return } @@ -902,10 +1030,13 @@ type createBroadcastAPIRequest struct { UserIDs []int64 `json:"user_ids,omitempty"` } -// handleCreateBroadcastAPI resolves "all users" into an explicit id list -// before forwarding to the admin API: the admin service always receives an -// already-resolved recipient list, never "every user" as a live concept it -// would have to know how to enumerate itself. +// handleCreateBroadcastAPI forwards a broadcast create straight to the admin +// API. "all" mode is no longer pre-resolved into an explicit id list here: +// the admin service snapshots the current eligible user set itself and the +// broadcast worker enumerates it incrementally, so "every user" never has +// to cross this boundary (or the one after it) as a potentially huge id +// slice. Only "selected" mode carries UserIDs, already an operator-picked +// list bounded by domain.MaxBroadcastSelectedRecipients. func (s *server) handleCreateBroadcastAPI(w http.ResponseWriter, r *http.Request) { var body createBroadcastAPIRequest if !decodeAction(w, r, &body) { @@ -913,16 +1044,7 @@ func (s *server) handleCreateBroadcastAPI(w http.ResponseWriter, r *http.Request } userIDs := body.UserIDs if body.TargetMode == "all" { - if s.read == nil { - writeAPIError(w, http.StatusServiceUnavailable, "read store is not configured") - return - } - all, err := s.read.ListAllAccountIDs(r.Context()) - if err != nil { - writeAPIError(w, http.StatusInternalServerError, err.Error()) - return - } - userIDs = all + userIDs = nil } req := admin.CreateBroadcastRequest{ CommandMeta: s.commandMetaFromAPI(r, body.CommandID, body.Reason, body.Confirm, "create-broadcast"), @@ -1410,6 +1532,52 @@ func (s *server) handleSetAccountAvatarAPI(w http.ResponseWriter, r *http.Reques writeCommandResultAPI(w, result, err) } +type setAccountAvatarVideoAPIRequest struct { + CommandID string `json:"command_id"` + Reason string `json:"reason"` + Confirm bool `json:"confirm"` + UserID int64 `json:"user_id"` + VideoStartTs float64 `json:"video_start_ts"` +} + +func (s *server) handleSetAccountAvatarVideoAPI(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + r.Body = http.MaxBytesReader(w, r.Body, admin.MaxAccountAvatarVideoBytes+(1<<20)) + if err := r.ParseMultipartForm(1 << 20); err != nil { + writeAPIError(w, http.StatusBadRequest, "invalid multipart form: "+err.Error()) + return + } + if r.MultipartForm != nil { + defer r.MultipartForm.RemoveAll() + } + var body setAccountAvatarVideoAPIRequest + dec := json.NewDecoder(strings.NewReader(r.FormValue("metadata"))) + dec.DisallowUnknownFields() + if err := dec.Decode(&body); err != nil { + writeAPIError(w, http.StatusBadRequest, "invalid metadata: "+err.Error()) + return + } + file, header, err := r.FormFile("file") + if err != nil { + writeAPIError(w, http.StatusBadRequest, "avatar video file is required") + return + } + defer file.Close() + data, err := io.ReadAll(io.LimitReader(file, admin.MaxAccountAvatarVideoBytes+1)) + if err != nil || len(data) == 0 || int64(len(data)) > admin.MaxAccountAvatarVideoBytes { + writeAPIError(w, http.StatusBadRequest, "avatar video file is empty or too large") + return + } + req := admin.SetAccountAvatarVideoRequest{ + CommandMeta: s.commandMetaFromAPI(r, body.CommandID, body.Reason, body.Confirm, "set-avatar-video"), + UserID: body.UserID, + FileName: header.Filename, + VideoStartTs: body.VideoStartTs, + } + result, err := s.callAdminMultipart(r.Context(), "/v1/accounts/set-avatar-video", req, header.Filename, data) + writeCommandResultAPI(w, result, err) +} + type setUserColorAPIRequest struct { CommandID string `json:"command_id"` Reason string `json:"reason"` @@ -1984,6 +2152,37 @@ func (s *server) handleDeleteUncategorizedGifsAPI(w http.ResponseWriter, r *http writeCommandResultAPI(w, result, err) } +// storageManualPurgeAPIRequest mirrors ManualPurgeStorageRequest's frontend +// payload (see StoragePage.tsx's manual purge modal): categories/include_avatars +// go through as native JSON, created_before is an ISO/RFC3339 date string +// (produced by `new Date(x).toISOString()` on the frontend, same convention +// as freeze_until -- see setAccountFrozenAPIRequest.Until) that encoding/json +// parses straight into *time.Time; the field left undefined by the frontend +// (no date entered) decodes to nil, meaning no age filter at all. +type storageManualPurgeAPIRequest struct { + CommandID string `json:"command_id"` + Reason string `json:"reason"` + Confirm bool `json:"confirm"` + Categories []string `json:"categories"` + IncludeAvatars bool `json:"include_avatars"` + CreatedBefore *time.Time `json:"created_before"` +} + +func (s *server) handleStorageManualPurgeAPI(w http.ResponseWriter, r *http.Request) { + var body storageManualPurgeAPIRequest + if !decodeAction(w, r, &body) { + return + } + req := admin.ManualPurgeStorageRequest{ + CommandMeta: s.commandMetaFromAPI(r, body.CommandID, body.Reason, body.Confirm, "storage-manual-purge"), + Categories: body.Categories, + IncludeAvatars: body.IncludeAvatars, + CreatedBefore: body.CreatedBefore, + } + result, err := s.callAdminAPI(r.Context(), "/v1/storage/manual-purge", req) + writeCommandResultAPI(w, result, err) +} + type deleteGifCatalogEntryAPIRequest struct { CommandID string `json:"command_id"` Reason string `json:"reason"` @@ -2505,7 +2704,14 @@ func (s *server) handleStorageAccountsAPI(w http.ResponseWriter, r *http.Request writeAPIError(w, http.StatusBadRequest, "invalid limit") return } - rows, hasMore, err := s.read.ListAccountStorageUsage(r.Context(), offset, limit) + sortDesc := query.Get("order") != "asc" + rows, hasMore, err := s.read.ListAccountStorageUsage( + r.Context(), + query.Get("q"), + query.Get("sort"), + sortDesc, + offset, + limit) if err != nil { writeAPIError(w, http.StatusInternalServerError, err.Error()) return diff --git a/cmd/telesrv-admin/serversettings.go b/cmd/telesrv-admin/serversettings.go new file mode 100644 index 00000000..92f78f64 --- /dev/null +++ b/cmd/telesrv-admin/serversettings.go @@ -0,0 +1,425 @@ +package main + +import ( + "encoding/json" + "io" + "net/http" + "path/filepath" + "strings" + + "telesrv/internal/admin" + "telesrv/internal/domain" + "telesrv/internal/identity" +) + +// serverManage gates the whole Server Settings surface -- see +// permissionServerManage's doc comment in security.go for why this is one +// right rather than split review/manage like other sections. +func (s *server) serverManage(handler http.HandlerFunc) http.Handler { + return s.requireAuthAPI(s.requirePermission(permissionServerManage, handler)) +} + +// serverCommandResult builds the same admin.CommandResult shape every other +// action returns, without going through internal/admin's runCommand + +// Postgres audit log: everything in this file operates on local files/ +// processes directly (see routes() in server.go for why), so there is no +// owpengram-server-side admin_commands row to write. The actor/reason are +// still in meta for structured logging if that's ever added; today they are +// simply not persisted anywhere. +func serverCommandResult(meta admin.CommandMeta, action string, err error, message string, details map[string]any) admin.CommandResult { + status := "completed" + errText := "" + if err != nil { + status = "failed" + errText = err.Error() + if message == "" { + message = "command failed" + } + } + return admin.CommandResult{ + CommandID: meta.CommandID, + Action: action, + Status: status, + DryRun: meta.DryRun, + Message: message, + Details: details, + Error: errText, + } +} + +// --- identity (name/description/icon) --------------------------------- + +// serverIdentityAPIResponse extends identity.Info's raw fields with the two +// *effective* fallback welcome-message templates -- s.cfg's +// WelcomeMessage{Phone,Email}Default, i.e. this admin process's own reading +// of TELESRV_WELCOME_MESSAGE_*_TEMPLATE (env var, itself defaulting to the +// compiled-in copy), which matches what owpengram-server falls back to +// whenever the panel override is unset, as long as both processes share the +// same .env (see uiConfig.WelcomeMessagePhoneDefault's doc comment). The +// panel needs both: the raw override (possibly empty) to know whether a +// field is "explicitly set", and the default text to show as "(using +// default: ...)" / to restore on Reset. +type serverIdentityAPIResponse struct { + identity.Info + DefaultWelcomeMessagePhoneTemplate string `json:"default_welcome_message_phone_template"` + DefaultWelcomeMessageEmailTemplate string `json:"default_welcome_message_email_template"` + // DefaultLoginCodeMessageTemplate is the effective fallback text for + // the login-code delivery message (s.cfg.LoginCodeMessageDefault) -- + // same "raw override + effective default" contract as the two fields + // above, see their doc comment. + DefaultLoginCodeMessageTemplate string `json:"default_login_code_message_template"` +} + +func (s *server) handleServerIdentityAPI(w http.ResponseWriter, r *http.Request) { + info, err := s.identity.Get() + if err != nil { + writeAPIError(w, http.StatusInternalServerError, err.Error()) + return + } + writeJSON(w, http.StatusOK, serverIdentityAPIResponse{ + Info: info, + DefaultWelcomeMessagePhoneTemplate: s.cfg.WelcomeMessagePhoneDefault, + DefaultWelcomeMessageEmailTemplate: s.cfg.WelcomeMessageEmailDefault, + DefaultLoginCodeMessageTemplate: s.cfg.LoginCodeMessageDefault, + }) +} + +// handleServerIconAPI serves the icon's raw bytes for the panel's own +// preview -- separate from owpengram-server's public /owpengram/server-icon +// (same underlying file, different process/auth: this one is behind the +// admin session, not open to clients). +func (s *server) handleServerIconAPI(w http.ResponseWriter, r *http.Request) { + data, ext, ok := s.identity.Icon() + if !ok { + writeAPIError(w, http.StatusNotFound, "no icon configured") + return + } + contentType := map[string]string{ + ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", + ".webp": "image/webp", ".gif": "image/gif", + }[ext] + if contentType == "" { + contentType = "application/octet-stream" + } + w.Header().Set("Content-Type", contentType) + w.Header().Set("Cache-Control", "no-store") + _, _ = w.Write(data) +} + +type setServerIdentityAPIRequest struct { + CommandID string `json:"command_id"` + Reason string `json:"reason"` + Confirm bool `json:"confirm"` + Name string `json:"name"` + Description string `json:"description"` +} + +func (s *server) handleSetServerIdentityAPI(w http.ResponseWriter, r *http.Request) { + var body setServerIdentityAPIRequest + if !decodeAction(w, r, &body) { + return + } + meta := s.commandMetaFromAPI(r, body.CommandID, body.Reason, body.Confirm, "set-server-identity") + details := map[string]any{"name": body.Name, "description": body.Description} + if meta.DryRun { + writeJSON(w, http.StatusOK, serverCommandResult(meta, "server.set_identity", nil, "server identity validated", details)) + return + } + err := s.identity.SetText(body.Name, body.Description) + writeJSON(w, http.StatusOK, serverCommandResult(meta, "server.set_identity", err, "server identity updated", details)) +} + +// --- login-notification templates --------------------------------------- + +type setWelcomeMessageTemplatesAPIRequest struct { + CommandID string `json:"command_id"` + Reason string `json:"reason"` + Confirm bool `json:"confirm"` + PhoneTemplate string `json:"phone_template"` + EmailTemplate string `json:"email_template"` +} + +// handleSetWelcomeMessageTemplatesAPI sets (or, with an empty string, +// clears) the admin-panel override for the 777000 login-notification +// message's phone/email template -- see identity.Store.SetWelcomeMessageTemplates +// and domain.ResolveWelcomeMessageTemplate. Deliberately a separate endpoint +// from set-server-identity: brand identity (name/description/icon) and +// login-notification copy are different concerns that happen to share the +// same on-disk identity.json, and keeping them as separate actions/buttons +// means editing one never risks silently blanking the other. +func (s *server) handleSetWelcomeMessageTemplatesAPI(w http.ResponseWriter, r *http.Request) { + var body setWelcomeMessageTemplatesAPIRequest + if !decodeAction(w, r, &body) { + return + } + meta := s.commandMetaFromAPI(r, body.CommandID, body.Reason, body.Confirm, "set-welcome-message-templates") + details := map[string]any{ + "phone_template_set": strings.TrimSpace(body.PhoneTemplate) != "", + "email_template_set": strings.TrimSpace(body.EmailTemplate) != "", + } + if meta.DryRun { + writeJSON(w, http.StatusOK, serverCommandResult(meta, "server.set_welcome_message_templates", nil, "login-notification templates validated", details)) + return + } + err := s.identity.SetWelcomeMessageTemplates(body.PhoneTemplate, body.EmailTemplate) + writeJSON(w, http.StatusOK, serverCommandResult(meta, "server.set_welcome_message_templates", err, "login-notification templates updated", details)) +} + +// --- login-code delivery message template -------------------------------- + +type setLoginCodeMessageTemplateAPIRequest struct { + CommandID string `json:"command_id"` + Reason string `json:"reason"` + Confirm bool `json:"confirm"` + Template string `json:"template"` +} + +// handleSetLoginCodeMessageTemplateAPI sets (or, with an empty string, +// clears) the admin-panel override for the 777000 login-code delivery +// message -- see identity.Store.SetLoginCodeMessageTemplate and +// domain.ResolveLoginCodeMessageTemplate. A dedicated endpoint (not folded +// into set-welcome-message-templates): this message embeds the actual OTP +// code via the {{code}} placeholder, so a save here carries an extra, +// security-relevant validation the login-notification templates don't +// need -- a template missing {{code}} (or containing it more than once) +// would either silently drop the code from the message or leave it +// ambiguous which occurrence carries it, so it is rejected outright with a +// 422 rather than saved. Clearing the override (empty string) is exempt -- +// it always resolves to a valid built-in/env default. +func (s *server) handleSetLoginCodeMessageTemplateAPI(w http.ResponseWriter, r *http.Request) { + var body setLoginCodeMessageTemplateAPIRequest + if !decodeAction(w, r, &body) { + return + } + if t := strings.TrimSpace(body.Template); t != "" { + if err := domain.ValidateLoginCodeMessageTemplate(t); err != nil { + writeAPIError(w, http.StatusUnprocessableEntity, err.Error()) + return + } + } + meta := s.commandMetaFromAPI(r, body.CommandID, body.Reason, body.Confirm, "set-login-code-message-template") + details := map[string]any{"template_set": strings.TrimSpace(body.Template) != ""} + if meta.DryRun { + writeJSON(w, http.StatusOK, serverCommandResult(meta, "server.set_login_code_message_template", nil, "login-code message template validated", details)) + return + } + err := s.identity.SetLoginCodeMessageTemplate(body.Template) + writeJSON(w, http.StatusOK, serverCommandResult(meta, "server.set_login_code_message_template", err, "login-code message template updated", details)) +} + +var allowedServerIconExts = map[string]bool{ + ".png": true, ".jpg": true, ".jpeg": true, ".webp": true, ".gif": true, +} + +const maxServerIconBytes = 2 << 20 // 2 MiB + +type uploadServerIconAPIRequest struct { + CommandID string `json:"command_id"` + Reason string `json:"reason"` + Confirm bool `json:"confirm"` +} + +// handleUploadServerIconAPI takes multipart/form-data (a "metadata" JSON +// field + a "file" field), the same shape handleSetAccountAvatarAPI uses -- +// deliberately not JSON+base64 like the other Server Settings actions: +// base64 inflates a file ~33%, and decodeAction's plain io.LimitReader caps +// the request body at 1MiB regardless of maxServerIconBytes, so a real +// multi-hundred-KB icon would fail decoding ("unexpected EOF" from the +// truncated body) before this handler ever saw it. Multipart sidesteps that +// entirely -- the size cap below is enforced on the actual file bytes. +func (s *server) handleUploadServerIconAPI(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + r.Body = http.MaxBytesReader(w, r.Body, maxServerIconBytes+(1<<20)) + if err := r.ParseMultipartForm(1 << 20); err != nil { + writeAPIError(w, http.StatusBadRequest, "invalid multipart form: "+err.Error()) + return + } + if r.MultipartForm != nil { + defer r.MultipartForm.RemoveAll() + } + var body uploadServerIconAPIRequest + dec := json.NewDecoder(strings.NewReader(r.FormValue("metadata"))) + dec.DisallowUnknownFields() + if err := dec.Decode(&body); err != nil { + writeAPIError(w, http.StatusBadRequest, "invalid metadata: "+err.Error()) + return + } + file, header, err := r.FormFile("file") + if err != nil { + writeAPIError(w, http.StatusBadRequest, "icon file is required") + return + } + defer file.Close() + ext := strings.ToLower(filepath.Ext(header.Filename)) + if !allowedServerIconExts[ext] { + writeAPIError(w, http.StatusBadRequest, "unsupported icon extension") + return + } + data, err := io.ReadAll(io.LimitReader(file, maxServerIconBytes+1)) + if err != nil || len(data) == 0 || len(data) > maxServerIconBytes { + writeAPIError(w, http.StatusBadRequest, "icon file is empty or too large (max 2MiB)") + return + } + meta := s.commandMetaFromAPI(r, body.CommandID, body.Reason, body.Confirm, "upload-server-icon") + details := map[string]any{"bytes": len(data), "ext": ext} + if meta.DryRun { + writeJSON(w, http.StatusOK, serverCommandResult(meta, "server.upload_icon", nil, "server icon validated", details)) + return + } + setErr := s.identity.SetIcon(data, ext) + writeJSON(w, http.StatusOK, serverCommandResult(meta, "server.upload_icon", setErr, "server icon updated", details)) +} + +type removeServerIconAPIRequest struct { + CommandID string `json:"command_id"` + Reason string `json:"reason"` + Confirm bool `json:"confirm"` +} + +func (s *server) handleRemoveServerIconAPI(w http.ResponseWriter, r *http.Request) { + var body removeServerIconAPIRequest + if !decodeAction(w, r, &body) { + return + } + meta := s.commandMetaFromAPI(r, body.CommandID, body.Reason, body.Confirm, "remove-server-icon") + if meta.DryRun { + writeJSON(w, http.StatusOK, serverCommandResult(meta, "server.remove_icon", nil, "server icon removal validated", nil)) + return + } + err := s.identity.RemoveIcon() + writeJSON(w, http.StatusOK, serverCommandResult(meta, "server.remove_icon", err, "server icon removed", nil)) +} + +// --- first-run setup wizard ---------------------------------------------- + +type completeSetupAPIRequest struct { + CommandID string `json:"command_id"` + Reason string `json:"reason"` + Confirm bool `json:"confirm"` +} + +// handleCompleteSetupAPI is the wizard's last step: removes the +// identity.Store setup-pending marker so /api/session stops telling the +// frontend to show it. Everything the wizard actually configures (identity, +// .env, the operator account) is already saved as the operator moves +// through it via the same actions Server Settings/Operators use outside the +// wizard -- this action only marks that the walkthrough happened, so it +// never fails partway through something worth retrying. +func (s *server) handleCompleteSetupAPI(w http.ResponseWriter, r *http.Request) { + var body completeSetupAPIRequest + if !decodeAction(w, r, &body) { + return + } + meta := s.commandMetaFromAPI(r, body.CommandID, body.Reason, body.Confirm, "complete-setup") + if meta.DryRun { + writeJSON(w, http.StatusOK, serverCommandResult(meta, "server.complete_setup", nil, "setup completion validated", nil)) + return + } + err := s.identity.MarkSetupComplete() + writeJSON(w, http.StatusOK, serverCommandResult(meta, "server.complete_setup", err, "setup marked complete", nil)) +} + +// --- .env editing -------------------------------------------------------- + +func (s *server) handleServerEnvAPI(w http.ResponseWriter, r *http.Request) { + groups, err := s.serverCtl.ReadEnvGroups() + if err != nil { + writeAPIError(w, http.StatusInternalServerError, err.Error()) + return + } + writeJSON(w, http.StatusOK, groups) +} + +type updateServerEnvAPIRequest struct { + CommandID string `json:"command_id"` + Reason string `json:"reason"` + Confirm bool `json:"confirm"` + Values map[string]string `json:"values"` +} + +func (s *server) handleUpdateServerEnvAPI(w http.ResponseWriter, r *http.Request) { + var body updateServerEnvAPIRequest + if !decodeAction(w, r, &body) { + return + } + meta := s.commandMetaFromAPI(r, body.CommandID, body.Reason, body.Confirm, "update-server-env") + details := map[string]any{"keys_changed": len(body.Values)} + if meta.DryRun { + writeJSON(w, http.StatusOK, serverCommandResult(meta, "server.update_env", nil, "would update .env -- takes effect on next Restart/Update", details)) + return + } + err := s.serverCtl.WriteEnvValues(body.Values) + writeJSON(w, http.StatusOK, serverCommandResult(meta, "server.update_env", err, ".env updated -- restart the server for changes to take effect", details)) +} + +// --- status / restart / update ------------------------------------------- + +func (s *server) handleServerStatusAPI(w http.ResponseWriter, r *http.Request) { + writeJSON(w, http.StatusOK, s.serverCtl.Status()) +} + +// handleDockerStatusAPI backs the Services tab's live container list +// (postgres/redis/minio) -- see procctl.Manager.DockerStatus. A "docker +// compose ps" failure (daemon not running, compose file missing) is +// reported as an API error rather than an empty list, so the frontend can +// tell "no services" apart from "couldn't ask Docker". +// handleCheckServerUpdatesAPI backs the Update button's "Check updates" +// state -- a plain git fetch + rev-list count, no pull/build/restart. See +// procctl.Manager.CheckUpdates. +func (s *server) handleCheckServerUpdatesAPI(w http.ResponseWriter, r *http.Request) { + behind, err := s.serverCtl.CheckUpdates(r.Context()) + if err != nil { + writeAPIError(w, http.StatusInternalServerError, err.Error()) + return + } + writeJSON(w, http.StatusOK, map[string]any{"commits_behind": behind}) +} + +func (s *server) handleDockerStatusAPI(w http.ResponseWriter, r *http.Request) { + services, err := s.serverCtl.DockerStatus(r.Context()) + if err != nil { + writeAPIError(w, http.StatusInternalServerError, err.Error()) + return + } + writeJSON(w, http.StatusOK, services) +} + +type restartServerAPIRequest struct { + CommandID string `json:"command_id"` + Reason string `json:"reason"` + Confirm bool `json:"confirm"` +} + +func (s *server) handleRestartServerAPI(w http.ResponseWriter, r *http.Request) { + var body restartServerAPIRequest + if !decodeAction(w, r, &body) { + return + } + meta := s.commandMetaFromAPI(r, body.CommandID, body.Reason, body.Confirm, "restart-server") + if meta.DryRun { + writeJSON(w, http.StatusOK, serverCommandResult(meta, "server.restart", nil, "restart validated -- rebuilds both bin/owpengram-server and bin/owpengram-admin-panel, relaunches owpengram-server", nil)) + return + } + log, err := s.serverCtl.Restart(r.Context()) + writeJSON(w, http.StatusOK, serverCommandResult(meta, "server.restart", err, "server restarted", map[string]any{"log": log})) +} + +type updateServerAPIRequest struct { + CommandID string `json:"command_id"` + Reason string `json:"reason"` + Confirm bool `json:"confirm"` +} + +func (s *server) handleUpdateServerAPI(w http.ResponseWriter, r *http.Request) { + var body updateServerAPIRequest + if !decodeAction(w, r, &body) { + return + } + meta := s.commandMetaFromAPI(r, body.CommandID, body.Reason, body.Confirm, "update-server") + if meta.DryRun { + writeJSON(w, http.StatusOK, serverCommandResult(meta, "server.update", nil, "update validated -- git pull, rebuild both binaries, relaunch bin/owpengram-server (admin panel binary is rebuilt but not self-restarted)", nil)) + return + } + log, err := s.serverCtl.Update(r.Context()) + writeJSON(w, http.StatusOK, serverCommandResult(meta, "server.update", err, "server updated", map[string]any{"log": log})) +} diff --git a/cmd/telesrv-admin/serversettings_test.go b/cmd/telesrv-admin/serversettings_test.go new file mode 100644 index 00000000..ba43521e --- /dev/null +++ b/cmd/telesrv-admin/serversettings_test.go @@ -0,0 +1,87 @@ +package main + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "telesrv/internal/identity" +) + +func TestServerIdentityAPIReportsOverridesAndDefaultsSeparately(t *testing.T) { + store := identity.NewStore(t.TempDir()) + srv := &server{ + cfg: uiConfig{ + WelcomeMessagePhoneDefault: "env phone default", + WelcomeMessageEmailDefault: "env email default", + }, + identity: store, + } + + // Before any override: GET must report empty raw fields (so the UI can + // tell "unset" apart from "explicitly set to the same text as the + // default"), alongside the effective default text. + req := httptest.NewRequest(http.MethodGet, "/api/server/identity", nil) + rec := httptest.NewRecorder() + srv.handleServerIdentityAPI(rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("GET status = %d, body = %s", rec.Code, rec.Body.String()) + } + var resp serverIdentityAPIResponse + if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil { + t.Fatalf("decode: %v", err) + } + if resp.WelcomeMessagePhoneTemplate != "" || resp.WelcomeMessageEmailTemplate != "" { + t.Fatalf("expected empty overrides before any Set, got %+v", resp) + } + if resp.DefaultWelcomeMessagePhoneTemplate != "env phone default" || resp.DefaultWelcomeMessageEmailTemplate != "env email default" { + t.Fatalf("expected effective defaults from cfg, got %+v", resp) + } + + // Set an override for phone only. + setReq := httptest.NewRequest(http.MethodPost, "/api/actions/set-welcome-message-templates", strings.NewReader(`{ + "reason": "test", + "confirm": true, + "phone_template": "custom phone template" + }`)) + setRec := httptest.NewRecorder() + srv.handleSetWelcomeMessageTemplatesAPI(setRec, setReq) + if setRec.Code != http.StatusOK { + t.Fatalf("SET status = %d, body = %s", setRec.Code, setRec.Body.String()) + } + + req2 := httptest.NewRequest(http.MethodGet, "/api/server/identity", nil) + rec2 := httptest.NewRecorder() + srv.handleServerIdentityAPI(rec2, req2) + var resp2 serverIdentityAPIResponse + if err := json.Unmarshal(rec2.Body.Bytes(), &resp2); err != nil { + t.Fatalf("decode: %v", err) + } + if resp2.WelcomeMessagePhoneTemplate != "custom phone template" { + t.Fatalf("expected phone override to be set, got %+v", resp2) + } + if resp2.WelcomeMessageEmailTemplate != "" { + t.Fatalf("expected email override to stay unset, got %+v", resp2) + } + + // Reset (empty string) clears the override back to "unset". + resetReq := httptest.NewRequest(http.MethodPost, "/api/actions/set-welcome-message-templates", strings.NewReader(`{ + "reason": "test", + "confirm": true, + "phone_template": "" + }`)) + resetRec := httptest.NewRecorder() + srv.handleSetWelcomeMessageTemplatesAPI(resetRec, resetReq) + if resetRec.Code != http.StatusOK { + t.Fatalf("reset status = %d, body = %s", resetRec.Code, resetRec.Body.String()) + } + info, err := store.Get() + if err != nil { + t.Fatal(err) + } + if info.WelcomeMessagePhoneTemplate != "" { + t.Fatalf("expected phone override cleared after reset, got %+v", info) + } +} diff --git a/cmd/telesrv-admin/session.go b/cmd/telesrv-admin/session.go index 86ac8888..e5584417 100644 --- a/cmd/telesrv-admin/session.go +++ b/cmd/telesrv-admin/session.go @@ -26,10 +26,28 @@ type sessionClaims struct { Actor string `json:"actor"` Exp int64 `json:"exp"` Nonce string `json:"nonce"` - // Permissions is the right set granted to this session, taken from - // TELESRV_ADMIN_UI_PERMISSIONS at login. It travels inside the signed cookie - // rather than being re-read per request, so a session keeps the rights it was - // issued with, and it cannot be edited by the browser: the HMAC covers it. + // UserID identifies the admin_console_users row this session belongs to. + // + // Zero means the break-glass operator: whoever logged in with + // TELESRV_ADMIN_UI_PASSWORD / _TOKEN rather than a named account. That + // login has no database row, so it is deliberately exempt from the + // per-request revocation check below -- it is the way back in when the + // database is unreachable or every named account has been locked out. + UserID int64 `json:"uid,omitempty"` + // Epoch is the account's token_epoch at the moment this session was minted. + // + // Permissions travel inside the signed cookie, which is fast but means a + // 12-hour session would otherwise keep whatever rights it was issued with + // long after they were taken away. Every request re-reads the account's + // current epoch and refuses the session if it has moved, so disabling an + // operator, editing their rights or changing their password logs them out + // on their very next request. + Epoch int32 `json:"epoch,omitempty"` + // Permissions is the right set granted to this session. For a named account + // it is a snapshot of that row's permissions; for the break-glass operator + // it comes from TELESRV_ADMIN_UI_PERMISSIONS. It cannot be edited by the + // browser: the HMAC covers it. It is still re-read per request for named + // accounts (see Epoch) so an edit narrows access immediately. Permissions []string `json:"permissions,omitempty"` // CSRF is the double-submit token bound to this session. Binding it into the // signed claims is what makes the cookie/header pair unforgeable by a sibling diff --git a/cmd/telesrv-admin/session_test.go b/cmd/telesrv-admin/session_test.go index 1ab68c69..872eebb7 100644 --- a/cmd/telesrv-admin/session_test.go +++ b/cmd/telesrv-admin/session_test.go @@ -144,6 +144,53 @@ func TestModerationReadAPIDisablesBrowserCaching(t *testing.T) { } } +func TestAuthorizationRowJSONPreservesInt64AsDecimalStrings(t *testing.T) { + const maxInt64 = int64(9223372036854775807) + raw, err := json.Marshal(AuthorizationRow{AuthKeyID: maxInt64, Hash: maxInt64}) + if err != nil { + t.Fatalf("marshal authorization row: %v", err) + } + var got map[string]any + if err := json.Unmarshal(raw, &got); err != nil { + t.Fatalf("unmarshal authorization row: %v", err) + } + for _, field := range []string{"AuthKeyID", "Hash"} { + if got[field] != "9223372036854775807" { + t.Fatalf("authorization %s = %#v, want exact decimal string", field, got[field]) + } + } +} + +func TestRevokeSessionsBFFForwardsExactAuthorizationHash(t *testing.T) { + const authorizationHash = int64(2361577175213625973) + var got admin.RevokeSessionsRequest + upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/v1/accounts/revoke-sessions" || r.Header.Get("Authorization") != "Bearer secret" { + t.Fatalf("upstream request path=%q authorization=%q", r.URL.Path, r.Header.Get("Authorization")) + } + if err := json.NewDecoder(r.Body).Decode(&got); err != nil { + t.Fatal(err) + } + _ = json.NewEncoder(w).Encode(admin.CommandResult{CommandID: got.CommandID, Status: "completed", DryRun: got.DryRun}) + })) + defer upstream.Close() + + srv := &server{cfg: uiConfig{AdminAPIURL: upstream.URL, AdminAPIToken: "secret"}} + req := httptest.NewRequest(http.MethodPost, "/api/actions/revoke-sessions", strings.NewReader(`{ + "reason":"precision regression","confirm":false,"user_id":1001, + "hash":"2361577175213625973" + }`)) + req = req.WithContext(context.WithValue(req.Context(), actorKey{}, "operator")) + rec := httptest.NewRecorder() + srv.handleRevokeSessionsAPI(rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String()) + } + if got.Hash != authorizationHash || got.Actor != "operator" || !got.DryRun { + t.Fatalf("forwarded revoke request = %+v", got) + } +} + func TestMintCollectibleUsernameBFFForwardsActorAndTolerantScalars(t *testing.T) { const maxInt64 = int64(9223372036854775807) var got admin.MintCollectibleUsernameRequest diff --git a/cmd/telesrv-admin/verification_test.go b/cmd/telesrv-admin/verification_test.go index 603400ef..3ec84e06 100644 --- a/cmd/telesrv-admin/verification_test.go +++ b/cmd/telesrv-admin/verification_test.go @@ -35,7 +35,7 @@ func panelServer(t *testing.T, permissions ...string) *server { func signIn(t *testing.T, srv *server) ([]*http.Cookie, string) { t.Helper() rec := httptest.NewRecorder() - req := httptest.NewRequest(http.MethodPost, "/api/login", strings.NewReader(`{"secret":"letmein"}`)) + req := httptest.NewRequest(http.MethodPost, "/api/login", strings.NewReader(`{"username":"owpengram","secret":"letmein"}`)) srv.routes().ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("login status=%d body=%s", rec.Code, rec.Body.String()) @@ -94,7 +94,7 @@ func TestPanelSessionReportsPermissions(t *testing.T) { if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { t.Fatalf("decode session: %v", err) } - if body.Actor != "admin" || len(body.Permissions) != 1 || body.Permissions[0] != permissionVerificationReview { + if body.Actor != breakGlassUsername || len(body.Permissions) != 1 || body.Permissions[0] != permissionVerificationReview { t.Fatalf("session=%+v, want the granted permissions reported to the panel", body) } } @@ -245,7 +245,7 @@ func originRequest(origin, host string) *http.Request { func TestLoginRefusesAForeignOrigin(t *testing.T) { srv := panelServer(t, permissionAll) - req := httptest.NewRequest(http.MethodPost, "/api/login", strings.NewReader(`{"secret":"letmein"}`)) + req := httptest.NewRequest(http.MethodPost, "/api/login", strings.NewReader(`{"username":"owpengram","secret":"letmein"}`)) req.Header.Set("Origin", "https://evil.example") rec := httptest.NewRecorder() srv.routes().ServeHTTP(rec, req) diff --git a/cmd/telesrv-admin/web/dist/assets/index-CQKJMNpu.css b/cmd/telesrv-admin/web/dist/assets/index-CQKJMNpu.css deleted file mode 100644 index 74e62e61..00000000 --- a/cmd/telesrv-admin/web/dist/assets/index-CQKJMNpu.css +++ /dev/null @@ -1 +0,0 @@ -@font-face{font-family:Plus Jakarta Sans;font-style:normal;font-weight:400;font-display:swap;src:url(/fonts/plus-jakarta-sans-400.woff2)format("woff2")}@font-face{font-family:Plus Jakarta Sans;font-style:normal;font-weight:500;font-display:swap;src:url(/fonts/plus-jakarta-sans-500.woff2)format("woff2")}@font-face{font-family:Plus Jakarta Sans;font-style:normal;font-weight:600;font-display:swap;src:url(/fonts/plus-jakarta-sans-600.woff2)format("woff2")}@font-face{font-family:Plus Jakarta Sans;font-style:normal;font-weight:700;font-display:swap;src:url(/fonts/plus-jakarta-sans-700.woff2)format("woff2")}@font-face{font-family:Plus Jakarta Sans;font-style:normal;font-weight:800;font-display:swap;src:url(/fonts/plus-jakarta-sans-800.woff2)format("woff2")}:root{--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light;--bg:#f7f9fc;--bg-accent:#eef1f5;--panel:#fff;--panel-subtle:#f7f9fc;--panel-strong:#f1f5f9;--surface-soft:#f2f7fd;--overlay:#18222f6b;--topbar-bg:#fffffff0;--line:#e2e8f0;--line-strong:#cbd5e1;--heading:#101828;--text:#0f1720;--text-soft:#344054;--muted:#64748b;--muted-2:#94a3b8;--brand:#2563eb;--brand-strong:#1d4ed8;--brand-2:#38bdf8;--grad:linear-gradient(135deg, #38bdf8 0%, #2563eb 55%, #1e40af 100%);--brand-tint:#eaf2fd;--brand-tint-border:#c7dcf9;--brand-tint-text:#1e3a8a;--good:#167447;--good-tint:#eaf6ef;--good-border:#c1e1cf;--warn:#a15c07;--warn-tint:#fcf4e4;--warn-border:#e7d09e;--danger:#b42318;--danger-tint:#fcefec;--danger-border:#eecac3;--danger-text:#8f2f27;--purple:#6a4fa3;--purple-tint:#f4effb;--purple-border:#dcd0f0;--purple-text:#5a4590;--input-bg:#fff;--btn-bg:#fff;--btn-text:#29323d;--btn-hover:#f4f7fa;--switch-track:#c8d0d6;--code-bg:#1b2733;--code-text:#d6e3ef;--code-border:#2b3a49;--sidebar:#08080e;--sidebar-soft:#12121a;--sidebar-line:#222228;--sidebar-row:#17171f;--sidebar-text:#c6d0dc;--sidebar-muted:#8fa0b4;--sidebar-faint:#8492a6;--sidebar-heading:#fff;--focus:#2563eb29;--shadow:0 28px 70px -36px #05050859;--shadow-sm:0 2px 10px #1827380d;--shadow-brand:0 8px 22px #2563eb38;--hero-glow:#2563eb24;--hero-grid:#0505080a;--radius-xs:8px;--radius-sm:9px;--radius:11px;--radius-lg:14px}[data-theme=dark]{--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark;--bg:#0f141a;--bg-accent:#131a22;--panel:#171f28;--panel-subtle:#1c2530;--panel-strong:#212c38;--surface-soft:#1a232d;--overlay:#05080c9e;--topbar-bg:#151c24db;--line:#29333f;--line-strong:#38434f;--heading:#eef3f8;--text:#d5dde6;--text-soft:#c2ccd6;--muted:#98a4b1;--muted-2:#6d7885;--brand:#5b9dff;--brand-strong:#7db4ff;--brand-2:#7cd1fb;--brand-tint:#142a4a;--brand-tint-border:#24466e;--brand-tint-text:#9dc3f5;--good:#47c281;--good-tint:#12301f;--good-border:#245639;--warn:#e0aa4d;--warn-tint:#322810;--warn-border:#574413;--danger:#e6695c;--danger-tint:#35201d;--danger-border:#5c332d;--danger-text:#f0a49b;--purple:#ac90e2;--purple-tint:#221b31;--purple-border:#3d3357;--purple-text:#c9b6ef;--input-bg:#131a22;--btn-bg:#1e2731;--btn-text:#dbe2ea;--btn-hover:#26313d;--switch-track:#3a454f;--code-bg:#0c1218;--code-text:#cdd9e5;--code-border:#232f3b;--sidebar:#10151b;--sidebar-soft:#1c242f;--sidebar-line:#262f3a;--sidebar-row:#161d25;--sidebar-text:#cbd4de;--sidebar-muted:#7c8794;--sidebar-faint:#6f7b88;--sidebar-heading:#f0f4f8;--focus:#5b9dff3d;--shadow:0 16px 40px #00000075;--shadow-sm:0 2px 12px #00000061;--shadow-brand:0 8px 22px #5b9dff42;--hero-glow:#5b9dff40;--hero-grid:#ffffff0a}*{box-sizing:border-box}html,body,#root{min-height:100%}body{color:var(--text);background:var(--bg);-webkit-font-smoothing:antialiased;text-rendering:optimizelegibility;margin:0;font:13px/1.45 Plus Jakarta Sans,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;transition:background-color .2s,color .2s}button,input,select,textarea{font:inherit}a{color:inherit;text-decoration:none}.shell{grid-template-columns:232px minmax(0,1fr);min-height:100vh;display:grid}.sidebar{height:100vh;color:var(--sidebar-text);background:var(--sidebar);border-right:1px solid var(--sidebar-line);flex-direction:column;gap:16px;padding:18px 12px;display:flex;position:sticky;top:0;overflow-y:auto}.brand{align-items:center;gap:10px;min-height:42px;padding:0 4px;display:flex}.brand.compact{justify-content:center}.brand-mark{place-items:center;width:34px;height:34px;display:grid}.brand-mark img{object-fit:contain;width:100%;height:100%;display:block}.brand strong{font-size:14px;line-height:1.1;display:block}.brand small{color:var(--sidebar-muted);margin-top:3px;font-size:11px;display:block}.sidebar-label{color:var(--sidebar-faint);text-transform:uppercase;letter-spacing:.04em;padding:0 8px;font-size:11px;font-weight:700}.nav-list,.nav-section{gap:4px;display:grid}.nav-section-toggle{width:100%;min-height:38px;color:var(--sidebar-muted);border-radius:var(--radius-sm);cursor:pointer;text-align:left;background:0 0;border:1px solid #0000;grid-template-columns:18px minmax(0,1fr) 16px;align-items:center;gap:9px;padding:0 10px;font-size:12px;font-weight:800;transition:color .14s,background-color .14s,border-color .14s;display:grid}.nav-section-toggle:hover,.nav-section.active .nav-section-toggle{color:var(--sidebar-heading);background:var(--sidebar-soft);border-color:#34404d}.nav-section-chevron{color:var(--sidebar-muted);justify-self:end;transition:transform .14s}.nav-section.open .nav-section-chevron{transform:rotate(180deg)}.nav-children{gap:4px;padding:2px 0 2px 18px;display:grid}.nav-item{min-height:38px;color:var(--sidebar-text);border-radius:var(--radius-sm);border:1px solid #0000;grid-template-columns:18px minmax(0,1fr);align-items:center;gap:9px;padding:0 10px;transition:color .14s,background-color .14s,border-color .14s;display:grid}.nav-dot{background:var(--sidebar-faint);border-radius:999px;justify-self:center;width:6px;height:6px}.nav-item:hover,.nav-item.active{color:var(--sidebar-heading);background:var(--sidebar-soft);border-color:#34404d}.nav-item.active .nav-dot{background:var(--brand)}.sidebar-status{gap:7px;margin-top:auto;display:grid}.runtime-row{min-height:32px;color:var(--sidebar-text);background:var(--sidebar-row);border-radius:var(--radius-sm);border:1px solid #27313c;grid-template-columns:18px minmax(0,1fr) auto;align-items:center;gap:7px;padding:0 8px;display:grid}.runtime-row strong{color:var(--sidebar-heading);font-size:11px}.workspace{min-width:0}.topbar{z-index:20;background:var(--topbar-bg);border-bottom:1px solid var(--line);-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);justify-content:space-between;align-items:center;gap:18px;min-height:66px;padding:12px 24px;display:flex;position:sticky;top:0}.topbar h1{color:var(--heading);margin:2px 0 0;font-size:20px;line-height:1.2}.topbar-actions,.page-actions,.section-action,.entity-badges,.row-actions,.modal-actions{flex-wrap:wrap;align-items:center;gap:8px;display:flex}.theme-toggle{width:34px;height:34px;color:var(--muted);background:var(--panel-subtle);border:1px solid var(--line);cursor:pointer;border-radius:999px;place-items:center;transition:color .16s,background-color .16s,border-color .16s;display:inline-grid}.theme-toggle:hover{color:var(--brand);border-color:var(--brand-tint-border);background:var(--brand-tint)}.theme-toggle:focus-visible{outline:2px solid var(--brand);outline-offset:2px}.actor-pill{min-height:30px;color:var(--text-soft);background:var(--panel-subtle);border:1px solid var(--line);border-radius:999px;align-items:center;padding:0 10px;display:inline-flex}.content{gap:16px;padding:18px 24px 30px;display:grid}.eyebrow{color:var(--muted);text-transform:uppercase;letter-spacing:.04em;font-size:11px;font-weight:800}.dashboard-layout,.stacked-sections{gap:14px;display:grid}.dashboard-section{gap:10px;display:grid}.dashboard-section-title{color:var(--heading);text-transform:uppercase;letter-spacing:.04em;align-items:baseline;gap:8px;font-size:13px;font-weight:800;display:flex}.dashboard-section-title span{color:var(--muted);text-transform:none;letter-spacing:normal;font-size:11px;font-weight:600}.dashboard-grid{grid-template-columns:repeat(auto-fill,minmax(190px,1fr));gap:10px;display:grid}.stat-tile{text-align:left;background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);min-width:0;box-shadow:var(--shadow-sm);gap:8px;padding:14px;display:grid}button.stat-tile{cursor:pointer;font:inherit;color:inherit}a.stat-tile.clickable,button.stat-tile.clickable{transition:border-color .16s,box-shadow .16s,transform .16s}.stat-tile.clickable:hover{border-color:var(--brand-tint-border);box-shadow:var(--shadow);transform:translateY(-1px)}.stat-tile-head{justify-content:space-between;align-items:center;gap:8px;display:flex}.stat-tile-icon{width:30px;height:30px;color:var(--brand);background:var(--brand-tint);border:1px solid var(--brand-tint-border);border-radius:var(--radius-sm);flex:none;place-items:center;display:grid}.stat-tile.warn .stat-tile-icon{color:var(--warn);background:var(--warn-tint);border-color:var(--warn-border)}.stat-tile.danger .stat-tile-icon{color:var(--danger);background:var(--danger-tint);border-color:var(--danger-border)}.stat-tile.good .stat-tile-icon{color:var(--good);background:var(--good-tint);border-color:var(--good-border)}.stat-tile-open{color:var(--muted)}.stat-tile-value{color:var(--heading);font-size:24px;font-weight:800;line-height:1.1}.stat-tile.warn .stat-tile-value{color:var(--warn)}.stat-tile.danger .stat-tile-value{color:var(--danger)}.stat-tile-label{color:var(--text-soft);font-size:12px;font-weight:700}.stat-tile-sub{color:var(--muted);font-size:11px}.stat-tile-bar{background:var(--panel-subtle);border:1px solid var(--line);border-radius:999px;width:100%;height:5px;overflow:hidden}.stat-tile-bar>span{background:var(--brand-2);height:100%;display:block}.stat-tile.warn .stat-tile-bar>span{background:var(--warn)}.stat-tile.danger .stat-tile-bar>span{background:var(--danger)}.overview-band,.page-frame{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);min-width:0;box-shadow:var(--shadow-sm)}.overview-band{grid-template-columns:minmax(220px,1fr) minmax(420px,.9fr);align-items:center;gap:16px;padding:16px;display:grid}.overview-band h2,.page-title-row h2,.section-head h2,.modal h2{color:var(--heading);margin:0;font-size:18px;line-height:1.25}.overview-metrics,.metric-row{grid-template-columns:repeat(4,minmax(120px,1fr));gap:8px;display:grid}.overview-metrics{grid-template-columns:repeat(3,minmax(120px,1fr))}.status-item,.metric,.summary-item{background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius-sm);min-width:0;padding:10px}.status-item span,.metric span,.summary-item span{color:var(--muted);margin-bottom:6px;font-size:11px;display:block}.status-item strong,.metric strong,.summary-item strong{overflow-wrap:anywhere;color:var(--text);font-weight:800;display:block}.status-item.good,.metric.good{border-color:var(--good-border)}.status-item.warn,.metric.warn{border-color:var(--warn-border)}.metric.danger{border-color:var(--danger-border)}.command-grid{grid-template-columns:repeat(3,minmax(220px,1fr));gap:12px;display:grid}.launcher{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);min-height:94px;box-shadow:var(--shadow-sm);grid-template-columns:38px minmax(0,1fr) 18px;align-items:center;gap:12px;padding:14px;transition:border-color .16s,box-shadow .16s,transform .16s;display:grid}.launcher:hover{border-color:var(--brand-tint-border);box-shadow:var(--shadow);transform:translateY(-1px)}.launcher-icon{width:38px;height:38px;color:var(--brand);background:var(--brand-tint);border:1px solid var(--brand-tint-border);border-radius:var(--radius-sm);place-items:center;display:grid}.launcher-copy{gap:4px;display:grid}.launcher-copy strong{color:var(--heading);font-size:15px}.launcher-copy span{color:var(--muted)}.work-strip{grid-template-columns:repeat(4,minmax(160px,1fr));gap:8px;display:grid}.strip-item{min-height:38px;color:var(--text-soft);background:var(--panel);border:1px solid var(--line);border-radius:var(--radius-sm);align-items:center;gap:8px;padding:0 10px;display:flex}.page-frame{gap:14px;padding:14px;display:grid}.page-title-row{border-bottom:1px solid var(--line);justify-content:space-between;align-items:flex-start;gap:14px;padding-bottom:12px;display:flex}.query-panel{background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius-sm);padding:10px}.toolbar{flex-wrap:wrap;align-items:center;gap:8px;display:flex}.message-query input{width:150px}.message-selector-grid{grid-template-columns:repeat(2,minmax(280px,1fr));gap:10px;margin-bottom:10px;display:grid}.message-selector-grid.single{grid-template-columns:minmax(320px,620px)}.entity-picker{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius-sm);gap:8px;min-width:0;padding:10px;display:grid}.picker-head{min-height:24px;color:var(--text-soft);justify-content:space-between;align-items:center;gap:8px;font-weight:800;display:flex}.selected-entity{min-height:40px;color:var(--brand-tint-text);background:var(--brand-tint);border:1px solid var(--brand-tint-border);border-radius:var(--radius-sm);grid-template-columns:18px minmax(0,1fr) auto;align-items:center;gap:8px;padding:7px 9px;display:grid}.selected-entity strong,.selected-entity span{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.selected-entity div{gap:2px;min-width:0;display:grid}.selected-entity div span{color:var(--brand-tint-text);opacity:.85;font-size:11px}.picker-search{background:var(--panel-subtle);border:1px solid var(--line-strong);border-radius:var(--radius-sm);grid-template-columns:18px minmax(0,1fr) auto;align-items:center;gap:7px;height:34px;padding:0 6px 0 9px;display:grid}.picker-search input{width:100%;height:30px;box-shadow:none;background:0 0;border:0;padding:0}.picker-results{border:1px solid var(--line);border-radius:var(--radius-sm);max-height:236px;display:grid;overflow:auto}.picker-row{min-height:36px;color:var(--text);background:var(--panel);border:0;border-bottom:1px solid var(--line);cursor:pointer;text-align:left;grid-template-columns:96px minmax(120px,1fr) minmax(120px,1fr) auto;align-items:center;gap:8px;padding:6px 8px;display:grid}.picker-row:last-child{border-bottom:0}.picker-row:hover,.picker-row.selected{background:var(--surface-soft)}.picker-row strong,.picker-row span{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.picker-empty,.picker-error{color:var(--muted);text-align:center;padding:9px}.picker-chip-list{flex-wrap:wrap;gap:6px;display:flex}.picker-chip{color:var(--brand-tint-text);background:var(--brand-tint);border:1px solid var(--brand-tint-border);border-radius:999px;align-items:center;gap:6px;padding:5px 8px;font-size:11px;font-weight:700;display:inline-flex}.picker-chip button{color:inherit;cursor:pointer;opacity:.75;background:0 0;border:0;align-items:center;padding:0;display:inline-flex}.picker-chip button:hover{opacity:1}.emoji-picker-row{grid-template-columns:36px minmax(140px,1fr) minmax(100px,1fr)}.emoji-picker-glyph{text-align:center;font-size:22px;line-height:1}.emoji-picker-anim{width:28px;height:28px}.emoji-picker-anim canvas{width:100%!important;height:100%!important}.picker-error{color:var(--danger);background:var(--danger-tint);border:1px solid var(--danger-border);border-radius:var(--radius-sm)}input,select,textarea{color:var(--text);background:var(--input-bg);border:1px solid var(--line-strong);border-radius:var(--radius-sm);outline:none;transition:border-color .14s,box-shadow .14s}input::placeholder,textarea::placeholder{color:var(--muted-2)}input,select{width:190px;height:34px;padding:0 10px}select{min-width:220px;height:34px;font:inherit;appearance:none;cursor:pointer;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%239aa4b2' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");background-position:right 10px center;background-repeat:no-repeat;padding:0 30px 0 10px;font-weight:600}select:disabled{color:var(--muted-2);cursor:not-allowed}textarea{resize:vertical;width:100%;padding:9px 10px}input:focus,select:focus,textarea:focus{border-color:var(--brand);box-shadow:0 0 0 3px var(--focus)}.small-input{width:88px}.sort-order-editor{align-items:center;gap:6px;display:flex}.sort-order-editor .small-input{width:64px;height:32px}.sort-order-editor .title-input{width:160px}.field-inline{color:var(--muted);align-items:center;gap:6px;display:inline-flex}.field-inline span{font-size:11px;font-weight:700}.searchbox{width:min(380px,100%);height:34px;color:var(--text);background:var(--input-bg);border:1px solid var(--line-strong);border-radius:var(--radius-sm);align-items:center;gap:8px;padding:0 10px;display:inline-flex}.searchbox input{width:100%;height:30px;box-shadow:none;border:0;padding:0}.btn{min-height:34px;color:var(--btn-text);background:var(--btn-bg);border:1px solid var(--line-strong);border-radius:var(--radius-sm);cursor:pointer;white-space:nowrap;justify-content:center;align-items:center;gap:6px;padding:0 12px;transition:background-color .14s,border-color .14s,color .14s,box-shadow .14s;display:inline-flex}.btn:hover:not(:disabled){background:var(--btn-hover)}.btn:disabled{color:var(--muted-2);cursor:not-allowed}.btn.primary{color:#fff;background:var(--brand);border-color:var(--brand)}.btn.primary:hover:not(:disabled){background:var(--brand-strong);border-color:var(--brand-strong)}.btn.ghost{background:var(--panel-subtle)}.btn.danger{color:var(--danger);background:var(--danger-tint);border-color:var(--danger-border)}.btn.danger:hover:not(:disabled){background:var(--danger-tint);border-color:var(--danger)}.btn.warn{color:var(--warn);background:var(--warn-tint);border-color:var(--warn-border)}.btn.warn:hover:not(:disabled){background:var(--warn-tint);border-color:var(--warn)}.btn:disabled,.btn.primary:disabled,.btn.warn:disabled,.btn.danger:disabled{color:var(--muted-2);background:var(--panel-strong);border-color:var(--line);cursor:not-allowed}.btn.full{width:100%}.icon-text{gap:7px}.compact-btn{min-height:28px;padding:0 8px;font-size:12px}.row-link,.link-button{color:var(--brand-2);cursor:pointer;background:0 0;border:0;align-items:center;gap:4px;padding:0;display:inline-flex}.avatar-link{cursor:pointer;background:0 0;border:0;border-radius:999px;padding:0;line-height:0;display:block}.avatar-link:hover,.avatar-link:focus-visible{outline:2px solid var(--focus);outline-offset:2px}.table-wrap{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);width:100%;overflow-x:auto}.data-table{border-collapse:collapse;width:100%;font-size:12.5px}.data-table th,.data-table td{border-bottom:1px solid var(--line);text-align:left;vertical-align:middle;white-space:nowrap;height:38px;padding:7px 9px}.data-table th{z-index:0;color:var(--muted);background:var(--panel-strong);font-weight:800;position:sticky;top:0}.data-table tbody tr:hover{background:var(--panel-subtle)}.data-table tr:last-child td{border-bottom:0}.mono{font-family:SFMono-Regular,Consolas,Liberation Mono,monospace}.truncate{text-overflow:ellipsis;max-width:380px;overflow:hidden}.badge{min-height:22px;color:var(--muted);background:var(--panel-strong);border:1px solid var(--line-strong);white-space:nowrap;border-radius:999px;align-items:center;padding:1px 8px;display:inline-flex}.badge.good{color:var(--good);background:var(--good-tint);border-color:var(--good-border)}.badge.danger{color:var(--danger);background:var(--danger-tint);border-color:var(--danger-border)}.badge.warn{color:var(--warn);background:var(--warn-tint);border-color:var(--warn-border)}.empty-cell{color:var(--muted);text-align:center}.bot-create-fields{grid-template-columns:repeat(3,minmax(0,1fr));gap:12px;display:grid}.bot-create-fields .duration-field input{width:100%}.bot-create-actions{border-top:1px solid var(--line);justify-content:space-between;align-items:center;gap:14px;margin-top:14px;padding-top:14px;display:flex}.bot-create-note{color:var(--muted);font-size:12px;line-height:1.4}@media (width<=760px){.bot-create-fields{grid-template-columns:1fr}.bot-create-actions{flex-direction:column;align-items:stretch}}.split-layout{grid-template-columns:minmax(0,1fr) 330px;align-items:start;gap:14px;display:grid}.split-main,.split-side{min-width:0}.entity-head{background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius);justify-content:space-between;align-items:flex-start;gap:14px;padding:14px;display:flex}.entity-head-main{align-items:center;gap:14px;min-width:0;display:flex}.entity-head-main .avatar-photo-img,.entity-head-main .avatar-fallback{flex-shrink:0}.avatar-edit-slot{flex-shrink:0;position:relative}.avatar-edit-btn{width:24px;height:24px;color:var(--brand);background:var(--panel);border:1px solid var(--line-strong);border-radius:999px;padding:0;position:absolute;bottom:-4px;right:-4px;box-shadow:0 1px 3px #0003}.avatar-edit-btn:hover{background:var(--brand-tint);border-color:var(--brand)}.entity-title{color:var(--heading);font-size:20px;font-weight:800;line-height:1.25}.entity-subtitle{color:var(--muted);margin-top:4px}.summary-grid{grid-template-columns:repeat(4,minmax(150px,1fr));gap:8px;display:grid}.about-text{color:var(--text-soft);background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius);margin:0;padding:10px}.action-groups{grid-template-columns:repeat(3,minmax(0,1fr));gap:14px;display:grid}.action-groups>.section-block{flex-direction:column;display:flex}.action-groups>.section-block>.section-head{flex-shrink:0}.action-groups>.section-block>.card-body{flex-direction:column;flex:1;justify-content:center;gap:10px;display:flex}.section-block,.action-dock,.surface{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);min-width:0;box-shadow:var(--shadow-sm);padding:12px}.section-head{justify-content:space-between;align-items:flex-start;gap:12px;margin-bottom:10px;display:flex}.section-head p{color:var(--muted);margin:5px 0 0}.action-dock{gap:10px;display:grid;position:sticky;top:82px}.dock-title{color:var(--text-soft);border-bottom:1px solid var(--line);padding-bottom:4px;font-weight:800}.action-dock>.btn,.action-dock .action-stack .btn{justify-content:center;width:100%}.duration-field{gap:4px;display:grid}.duration-field span{color:var(--muted);font-size:11px;font-weight:800}.duration-field input,.duration-field select{width:100%}.action-stack{gap:10px;display:grid}.action-stack .btn,.action-dock>.btn{min-height:42px}.danger-zone{border-top:1px solid var(--line);flex-wrap:wrap;gap:8px;margin-top:10px;padding-top:10px;display:flex}.dock-title+.danger-zone{border-top:0;margin-top:0;padding-top:0}.authorization-block{gap:10px;display:grid}.authorization-table{table-layout:fixed;min-width:720px}.authorization-table th,.authorization-table td{height:46px}.device-text{text-overflow:ellipsis;max-width:260px;overflow:hidden}.device-actions-head{width:250px}.device-actions-cell{width:250px;min-width:250px}.device-actions{white-space:normal;grid-template-columns:repeat(2,minmax(110px,1fr));gap:6px;min-width:226px;display:grid}.device-actions .btn{justify-content:center;width:100%}.operation-row{grid-template-columns:repeat(2,minmax(280px,1fr));gap:10px;display:grid}.operation-box{background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius);flex-wrap:wrap;align-items:center;gap:8px;padding:10px;display:flex}.operation-title{width:100%;color:var(--heading);align-items:center;gap:6px;font-weight:800;display:flex}.checkline{color:var(--muted);align-items:center;gap:6px;display:inline-flex}.checkline input{width:auto;height:auto}.alert{color:var(--danger-text);background:var(--danger-tint);border:1px solid var(--danger-border);border-radius:var(--radius);align-items:flex-start;gap:8px;padding:9px 10px;display:flex}.json-block{max-height:520px;color:var(--code-text);background:var(--code-bg);border:1px solid var(--code-border);border-radius:var(--radius);margin:0;padding:12px;font-size:12px;overflow:auto}.raw-grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;display:grid}.loading-line{min-height:80px;color:var(--muted);place-items:center;display:grid}.empty-panel{min-height:92px;color:var(--muted);background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius);place-items:center;display:grid}.gift-metrics .metric{background:var(--panel-subtle);min-height:68px;padding:12px}.gift-metrics .metric strong{font-size:17px}.gift-file-icon{color:var(--brand);background:var(--brand-tint);border:1px solid var(--brand-tint-border);flex:none;place-items:center;display:grid}.gift-format-chips{flex-wrap:wrap;flex:none;justify-content:flex-end;gap:6px;display:flex}.gift-format-chips span{color:var(--brand-tint-text);background:var(--brand-tint);border:1px solid var(--brand-tint-border);letter-spacing:.02em;border-radius:999px;padding:4px 8px;font-size:10px;font-weight:800}.gift-list-summary{color:var(--muted);margin-left:auto;font-size:11px;font-weight:700}.gift-import-modal{width:min(860px,100%)}.gift-bulk-import-modal{width:min(480px,100%)}.gift-bulk-import-modal .command-body{gap:14px;padding:16px 18px;display:grid}.gift-import-modal-body{gap:14px}.gift-source-tabs{gap:8px;display:flex}.give-gift-summary{background:var(--panel-subtle);border:1px solid var(--line-strong);color:var(--text-soft);border-radius:12px;align-items:center;gap:11px;padding:11px 13px;display:flex}.give-gift-summary>svg{color:var(--brand);flex:none}.give-gift-summary strong{color:var(--text);font-size:13px;display:block}.give-gift-summary .mono{color:var(--muted);font-size:11px}.give-gift-tabs{background:var(--panel-subtle);border:1px solid var(--line-strong);border-radius:12px;gap:4px;width:100%;padding:4px;display:flex}.give-gift-tabs .btn{min-height:36px;box-shadow:none;color:var(--text-soft);background:0 0;border:1px solid #0000;border-radius:9px;flex:1 1 0;justify-content:center;transition:color .15s,background .15s,border-color .15s,box-shadow .15s}.give-gift-tabs .btn:not(.primary):hover{color:var(--brand);background:var(--brand-tint)}.give-gift-tabs .btn.primary{color:#fff;background:var(--brand);border-color:var(--brand);box-shadow:var(--shadow-brand)}.give-gift-upgrade-note{background:var(--brand-tint);border:1px solid var(--brand-tint-border);color:var(--text-soft);border-radius:10px;margin:0;padding:9px 12px;font-size:11px;font-weight:650;line-height:1.45}.give-gift-attrs{grid-template-columns:repeat(3,minmax(0,1fr));align-items:end}.give-gift-attrs select,.give-gift-attrs input{width:100%;min-width:0;height:38px;color:var(--text);background-color:var(--input-bg);border:1px solid var(--line);border-radius:var(--radius-sm);font:inherit;appearance:none;cursor:pointer;padding:0 32px 0 10px;font-size:12px;font-weight:600}.give-gift-attrs input{cursor:text;text-overflow:ellipsis;padding-right:10px}.give-gift-attrs select{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%239aa4b2' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");background-position:right 11px center;background-repeat:no-repeat}.give-gift-attrs select:focus,.give-gift-attrs input:focus{border-color:var(--brand);box-shadow:0 0 0 3px var(--focus);outline:none}.give-gift-layout{grid-template-columns:minmax(220px,280px) minmax(0,1fr);align-items:start;gap:16px;display:grid}.give-gift-picker{align-content:start;gap:10px;display:grid}.give-gift-picker-head{align-items:center;gap:12px;display:flex}.give-gift-picker-head .searchbox{flex:auto}.give-gift-picker-list{background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius-lg);gap:8px;max-height:640px;padding:8px;display:grid;overflow-y:auto}.give-gift-option{text-align:left;min-width:0;color:var(--text);background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);cursor:pointer;box-shadow:var(--shadow-sm);grid-template-columns:46px minmax(0,1fr) auto;align-items:center;gap:11px;padding:9px 11px;transition:border-color .15s,box-shadow .15s,transform .15s;display:grid}.give-gift-option:hover{border-color:var(--brand-tint-border);box-shadow:var(--shadow);transform:translateY(-1px)}.give-gift-option.selected{border-color:var(--brand);box-shadow:0 0 0 2px var(--focus), var(--shadow)}.give-gift-thumb{place-items:center;width:46px;height:46px;display:grid}.give-gift-thumb canvas{width:100%!important;height:100%!important}.give-gift-option-info{gap:3px;min-width:0;display:grid}.give-gift-option-info strong{text-overflow:ellipsis;white-space:nowrap;font-size:12px;overflow:hidden}.give-gift-option-info .mono{color:var(--muted);font-size:10px}.give-gift-option-price{white-space:nowrap;justify-self:end}.give-gift-panel{background:var(--panel);border:1px solid var(--line-strong);border-radius:var(--radius-lg);gap:12px;min-width:0;padding:16px;display:grid}.give-gift-form{gap:12px;min-width:0;display:grid}.give-gift-form-actions{flex-wrap:wrap;justify-content:flex-end;gap:10px;padding-top:4px;display:flex}.give-gift-empty-panel{color:var(--muted);text-align:center;place-items:center;gap:10px;padding:48px 20px;display:grid}.give-gift-empty-panel svg{color:var(--brand);opacity:.8}.official-gift-picker{gap:12px;min-width:0;display:grid}.official-gift-bulk-import{flex-wrap:wrap;align-items:center;gap:10px;display:flex}.gift-bulk-import-progress{align-items:center;gap:8px;min-width:180px;display:flex}.gift-bulk-import-progress-bar{background:#e3e8ef;border-radius:999px;flex:auto;width:120px;height:6px;overflow:hidden}.gift-bulk-import-progress-bar>div{background:var(--brand);border-radius:999px;height:100%;transition:width .2s}.gift-bulk-import-progress span{color:var(--muted);white-space:nowrap;font-size:11px;font-weight:700}.official-gift-tools{align-items:center;gap:12px;display:flex}.official-gift-tools .searchbox{width:100%}.official-gift-tools>span{color:var(--muted);flex:none;font-size:11px;font-weight:750}.official-gift-categories{flex-wrap:wrap;gap:7px;display:flex}.official-gift-categories button{min-height:32px;color:var(--text-soft);background:var(--panel-subtle);border:1px solid var(--line-strong);font:inherit;cursor:pointer;border-radius:999px;align-items:center;gap:7px;padding:5px 10px;font-size:11px;font-weight:800;transition:color .15s,background .15s,border-color .15s,box-shadow .15s;display:inline-flex}.official-gift-categories button:hover{color:var(--brand);border-color:var(--brand-tint-border)}.official-gift-categories button.active{color:#fff;background:var(--brand);border-color:var(--brand);box-shadow:var(--shadow-brand)}.official-gift-categories button span{min-width:20px;height:20px;color:inherit;background:#7d8c9b38;border-radius:999px;place-items:center;padding:0 5px;font-size:10px;display:grid}.official-gift-categories button.active span{color:var(--brand);background:#ffffffd9}.official-gift-list{border:1px solid var(--line);border-radius:var(--radius-lg);background:var(--panel-subtle);scrollbar-gutter:stable;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;min-height:126px;max-height:314px;padding:8px;display:grid;overflow:auto}.official-gift-option{text-align:left;min-width:0;color:var(--text);background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);cursor:pointer;box-shadow:var(--shadow-sm);gap:8px;padding:11px 12px;transition:border-color .15s,box-shadow .15s,transform .15s;display:grid}.official-gift-option:hover{border-color:var(--brand-tint-border);box-shadow:var(--shadow);transform:translateY(-1px)}.official-gift-option.selected{border-color:var(--brand);box-shadow:0 0 0 2px var(--focus), var(--shadow)}.official-gift-option-head{grid-template-columns:minmax(0,1fr) auto;align-items:baseline;gap:8px;display:grid}.official-gift-option-head strong{text-overflow:ellipsis;white-space:nowrap;font-size:12px;overflow:hidden}.official-gift-option-head .mono{color:var(--muted);font-size:9px}.official-gift-option-meta{color:var(--muted);flex-wrap:wrap;gap:10px;font-size:10px;font-weight:700;display:flex}.official-gift-capabilities{flex-wrap:wrap;gap:5px;display:flex}.official-gift-capabilities>span{letter-spacing:.01em;border:1px solid #0000;border-radius:999px;padding:3px 7px;font-size:9px;font-weight:850}.official-gift-capabilities>span.yes{color:var(--good);background:var(--good-tint);border-color:var(--good-border)}.official-gift-capabilities>span.craft{color:var(--purple);background:var(--purple-tint);border-color:var(--purple-border)}.official-gift-capabilities>span.no{color:var(--muted);background:var(--panel-strong);border-color:var(--line-strong)}.official-gift-empty{min-height:108px;color:var(--muted);text-align:center;grid-column:1/-1;place-items:center;padding:20px;font-size:12px;display:grid}.official-gift-selected{border:1px solid var(--line);border-radius:var(--radius-lg);background:var(--surface-soft);grid-template-columns:108px minmax(0,1fr);align-items:center;gap:14px;padding:12px;display:grid}.official-gift-selected .gift-animation-shell{border-radius:12px;width:96px;height:96px;min-height:96px;overflow:hidden}.official-gift-selected .gift-animation{width:96px;height:96px}.official-gift-selected>div:last-child{gap:5px;min-width:0;display:grid}.official-gift-selected small{color:var(--muted)}.gift-import-note{color:var(--muted);justify-content:space-between;align-items:center;gap:12px;line-height:1.45;display:flex}.gift-file-picker{min-height:78px;color:var(--text);background:var(--panel);border:1px dashed var(--line-strong);border-radius:var(--radius);cursor:pointer;grid-template-columns:42px minmax(0,1fr) auto;align-items:center;gap:12px;padding:12px 14px;transition:border-color .16s,background .16s,box-shadow .16s;display:grid;position:relative}.gift-file-picker:hover,.gift-file-picker.has-file{background:var(--brand-tint);border-color:var(--brand);box-shadow:0 0 0 2px var(--focus)}.gift-file-picker.compact{grid-template-columns:minmax(0,1fr);min-height:44px;padding:8px 12px}.gift-file-picker input{opacity:0;pointer-events:none;width:1px;height:1px;position:absolute}.gift-file-icon{border-radius:var(--radius-sm);width:40px;height:40px}.gift-file-copy{gap:2px;min-width:0;display:grid}.gift-field-label{color:var(--muted);text-transform:uppercase;letter-spacing:.04em;font-size:10px;font-weight:800}.gift-file-copy strong{color:var(--heading);text-overflow:ellipsis;white-space:nowrap;font-size:13px;overflow:hidden}.gift-file-copy small{color:var(--muted);font-size:11px;font-weight:500}.gift-file-action{color:var(--brand);background:var(--brand-tint);border:1px solid var(--brand-tint-border);border-radius:var(--radius-sm);padding:7px 10px;font-size:11px;font-weight:800}.gif-catalog-preview{background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius-sm);place-items:center;max-height:220px;display:grid;overflow:hidden}.gif-catalog-preview img,.gif-catalog-preview video{object-fit:contain;max-width:100%;max-height:220px}.gift-fields-grid{grid-template-columns:minmax(200px,1.5fr) repeat(3,minmax(120px,1fr));gap:10px;display:grid}.gift-fields-grid label,.gift-reason-field{color:var(--muted);gap:6px;font-size:11px;font-weight:700;display:grid}.gift-fields-grid input,.gift-reason-field input{width:100%;min-width:0;height:38px;color:var(--text);background:var(--input-bg);border:1px solid var(--line);border-radius:var(--radius-sm);padding:0 10px}.gift-fields-grid input:focus,.gift-reason-field input:focus{border-color:var(--brand);box-shadow:0 0 0 3px var(--focus);outline:none}.gift-switch{color:var(--text-soft);cursor:pointer;align-items:center;gap:9px;font-size:12px;font-weight:700;display:inline-flex}.gift-switch input{opacity:0;width:1px;height:1px;position:absolute}.gift-switch-track{background:var(--switch-track);border-radius:999px;align-items:center;width:34px;height:19px;padding:2px;transition:background .16s;display:flex}.gift-switch-track span{background:#fff;border-radius:50%;width:15px;height:15px;transition:transform .16s;box-shadow:0 1px 3px #10182838}.gift-switch input:checked+.gift-switch-track{background:var(--brand)}.gift-switch input:checked+.gift-switch-track span{transform:translate(15px)}.gift-switch input:focus-visible+.gift-switch-track{outline:3px solid var(--focus);outline-offset:2px}.gift-validation{color:var(--code-text);background:var(--code-bg);border:1px solid var(--code-border);border-radius:var(--radius-sm);overflow:hidden}.gift-validation-head{color:var(--code-text);background:#ffffff09;border-bottom:1px solid #ffffff17;align-items:center;gap:9px;padding:10px 12px;display:flex}.gift-validation-head div{gap:2px;display:grid}.gift-validation-head span{color:var(--brand);font-size:10px}.gift-validation pre{max-height:180px;color:var(--code-text);margin:0;padding:11px 12px;font-size:11px;overflow:auto}.sticker-preview-modal{width:min(760px,100%)}.sticker-doc-grid{grid-template-columns:repeat(auto-fill,minmax(84px,1fr));gap:8px;max-height:420px;padding:2px;display:grid;overflow:auto}.sticker-doc-cell{aspect-ratio:1;background:var(--panel-strong);border:1px solid var(--line);border-radius:10px;place-items:center;display:grid;position:relative;overflow:hidden}.sticker-doc-canvas{width:100%;height:100%}.sticker-doc-canvas canvas{width:100%!important;height:100%!important}.sticker-doc-image{object-fit:contain;width:100%;height:100%}.sticker-doc-cell.list-thumb{flex:0 0 40px;width:40px}.sticker-list-thumb-empty{background:var(--panel-strong);border:1px solid var(--line);width:40px;height:40px;color:var(--muted);border-radius:9px;place-items:center;display:grid}.gif-catalog-thumb{object-fit:cover;background:var(--panel-strong);border:1px solid var(--line);border-radius:9px;width:40px;height:40px}.sticker-doc-grid-cell{gap:4px;display:grid}.sticker-doc-grid-cell .btn{justify-content:center;width:100%}.sticker-add-form{background:var(--panel-strong);border:1px solid var(--line);border-radius:10px;flex-wrap:wrap;align-items:center;gap:8px;margin-bottom:14px;padding:10px;display:flex}.sticker-add-form .gift-file-picker.compact{flex:220px;min-width:180px}.sticker-add-form .small-input{flex:0 140px}.sticker-add-form-error{color:var(--danger);flex-basis:100%;font-size:12px}.sticker-doc-error{color:var(--danger);text-align:center;place-items:center;padding:4px;font-size:9px;display:grid;position:absolute;inset:0}.gift-animation-shell{background:var(--surface-soft);place-items:center;min-height:210px;display:grid;position:relative}.gift-animation{width:200px;height:200px}.gift-animation canvas{width:100%!important;height:100%!important}.gift-play{width:30px;height:30px;color:var(--text);background:var(--panel);border:1px solid var(--line);border-radius:50%;place-items:center;display:grid;position:absolute;bottom:8px;right:8px}.gift-table-wrap{background:var(--panel)}.gift-table{min-width:1080px}.gift-table th:nth-child(2){width:74px}.gift-table td{vertical-align:middle}.gift-select-col{text-align:center;width:34px}.gift-select-col input{width:15px;height:15px}.avatar-col{width:44px}.muted-cell{color:var(--muted)}.avatar-photo-img,.avatar-fallback{object-fit:cover;border-radius:50%;display:block}.avatar-fallback{color:#fff;letter-spacing:-.02em;place-items:center;font-weight:800;display:grid}.gift-bulk-toolbar{background:var(--panel-strong);border:1px solid var(--line);border-radius:9px;align-items:center;gap:10px;margin-bottom:10px;padding:9px 12px;display:flex}.gift-bulk-count{color:var(--text);white-space:nowrap;font-size:12px;font-weight:700}.gift-bulk-reason{flex:1;min-width:160px}.gift-bulk-reason input{height:34px}.gift-bulk-error{color:var(--danger);font-size:11px;font-weight:700}.gift-page-size{color:var(--muted);white-space:nowrap;align-items:center;gap:6px;font-size:11px;font-weight:700;display:inline-flex}.gift-page-size select{height:30px;color:var(--text);background:var(--input-bg);border:1px solid var(--line);border-radius:var(--radius-sm);font:inherit;padding:0 8px;font-weight:700}.gift-pager{flex-wrap:wrap;justify-content:space-between;align-items:center;gap:12px;margin-top:10px;display:flex}.gift-pager-range{color:var(--muted);font-size:11px;font-weight:700}.gift-pager-controls{align-items:center;gap:10px;display:flex}.gift-pager-page{color:var(--text);white-space:nowrap;font-size:12px;font-weight:700}.gift-animation-shell.compact{border:1px solid var(--line);border-radius:var(--radius-sm);width:56px;min-height:56px;overflow:hidden}.gift-animation-shell.compact .gift-animation{width:54px;height:54px}.gift-animation-shell.compact .gift-play{width:20px;height:20px;bottom:3px;right:3px}.gift-row-disabled{opacity:.68}.gift-table-title,.gift-sort-order,.gift-source-size,.gift-convert-price{display:block}.gift-table-title{text-overflow:ellipsis;white-space:nowrap;max-width:220px;overflow:hidden}.gift-sort-order,.gift-source-size,.gift-convert-price{color:var(--muted);margin-top:3px;font-size:10px}.gift-table-price{color:var(--warn)}.gift-table-actions{align-items:center;gap:6px;display:flex}.collectible-button{color:var(--purple);background:var(--purple-tint);border-color:var(--purple-border)}.collectible-button:hover{background:var(--purple-tint);border-color:var(--purple)}.collectible-modal{width:min(1180px,100%);max-height:min(92vh,980px)}.collectible-modal .modal-head p{color:var(--muted);margin:4px 0 0;font-size:11px}.collectible-modal-body{background:var(--bg);gap:16px;padding:16px 18px 22px;overflow:auto}.collectible-loading{min-height:90px;color:var(--muted);justify-content:center;align-items:center;gap:8px;display:flex}.collectible-empty{color:var(--purple-text);background:var(--purple-tint);border:1px dashed var(--purple-border);border-radius:var(--radius);align-items:center;gap:12px;padding:16px;display:flex}.collectible-empty div,.collectible-definition-head>div:first-child,.collectible-section-head>div:first-child{gap:3px;display:grid}.collectible-empty span,.collectible-definition-head span,.collectible-section-head span{color:var(--muted);font-size:10px;font-weight:500}.collectible-active{background:var(--panel);border:1px solid var(--purple-border);border-radius:var(--radius);box-shadow:var(--shadow-sm);overflow:hidden}.collectible-active-head{background:var(--purple-tint);border-bottom:1px solid var(--purple-border);justify-content:space-between;align-items:center;gap:12px;padding:12px 14px;display:flex}.collectible-active-head>div{color:var(--purple-text);align-items:center;gap:9px;display:flex}.collectible-active-head>div>div{gap:2px;display:grid}.collectible-active-head span{color:var(--muted);font-size:10px}.collectible-active-grid{background:var(--line);grid-template-columns:repeat(auto-fill,minmax(145px,1fr));gap:1px;display:grid}.collectible-active-grid article{background:var(--panel);align-items:center;gap:9px;min-width:0;padding:9px 11px;display:flex}.collectible-active-grid article>div:last-child{gap:2px;min-width:0;display:grid}.collectible-active-grid article strong{text-overflow:ellipsis;white-space:nowrap;font-size:11px;overflow:hidden}.collectible-active-grid article span{color:var(--muted);font-size:9px}.collectible-definition{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);box-shadow:var(--shadow-sm);overflow:hidden}.collectible-definition-head{background:var(--panel-subtle);border-bottom:1px solid var(--line);justify-content:space-between;align-items:center;gap:12px;padding:14px 16px;display:flex}.collectible-main-fields{background:var(--panel-subtle);border-bottom:1px solid var(--line);padding:14px 16px}.collectible-section{border-bottom:1px solid var(--line);padding:14px 16px}.collectible-section:last-child{border-bottom:0}.collectible-section-head{justify-content:space-between;align-items:center;gap:12px;margin-bottom:10px;display:flex}.collectible-section-tools{align-items:center;gap:7px;display:flex}.collectible-rows{gap:7px;display:grid}.collectible-row{background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius-sm);align-items:end;gap:7px;padding:9px 9px 9px 36px;display:grid;position:relative}.collectible-row:hover{background:var(--panel);border-color:var(--line-strong);box-shadow:var(--shadow-sm)}.collectible-row.animated{grid-template-columns:minmax(120px,1.2fr) 90px 78px minmax(160px,1.4fr) 48px 30px}.collectible-row.backdrop{grid-template-columns:minmax(110px,1.2fr) 70px 80px 70px repeat(4,52px) 48px 30px}.collectible-row-index{width:27px;color:var(--purple-text);background:var(--purple-tint);border-right:1px solid var(--purple-border);border-radius:var(--radius-xs) 0 0 var(--radius-xs);place-items:center;font-size:10px;font-weight:800;display:grid;position:absolute;top:0;bottom:0;left:0}.collectible-row label{gap:4px;min-width:0;display:grid}.collectible-row label>span{color:var(--muted);text-transform:uppercase;letter-spacing:.025em;font-size:9px;font-weight:800}.collectible-row input:not([type=file]){width:100%;min-width:0;height:32px;color:var(--text);background:var(--input-bg);border:1px solid var(--line-strong);border-radius:var(--radius-sm);font:inherit;padding:0 8px;font-size:11px}.collectible-row input:focus{border-color:var(--purple);box-shadow:0 0 0 3px var(--purple-tint);outline:none}.collectible-file input{opacity:0;pointer-events:none;width:1px;height:1px;position:absolute}.collectible-file em{min-width:0;height:32px;color:var(--purple-text);background:var(--purple-tint);border:1px dashed var(--purple-border);border-radius:var(--radius-sm);text-overflow:ellipsis;white-space:nowrap;cursor:pointer;align-items:center;gap:5px;padding:0 8px;font-size:10px;font-style:normal;font-weight:700;display:flex;overflow:hidden}.collectible-inline-preview{width:42px;height:42px;color:var(--purple);background:var(--purple-tint);border:1px solid var(--purple-border);border-radius:var(--radius-sm);place-items:center;display:grid;overflow:hidden}.collectible-animation{width:100%;height:100%;overflow:hidden}.collectible-animation.compact{background:var(--purple-tint);border:1px solid var(--purple-border);border-radius:var(--radius-sm);flex:0 0 42px;place-items:center;width:42px;height:42px;display:grid}.collectible-animation canvas{width:100%!important;height:100%!important}.collectible-animation.failed{color:var(--danger);background:var(--danger-tint)}.collectible-animation.loading{color:var(--purple-text)}.collectible-file-error{color:var(--danger);grid-column:1/-1;font-size:10px}.collectible-color input{cursor:pointer;height:32px!important;padding:3px!important}.collectible-backdrop-preview{border-radius:var(--radius-sm);border:1px solid #2a1f472e;flex:0 0 42px;place-items:center;width:42px;height:42px;font-size:11px;font-weight:900;display:grid;box-shadow:inset 0 0 0 1px #fff3}.collectible-row .icon-btn{align-self:center}.collectible-row .icon-btn:disabled{opacity:.28}@media (width<=900px){.gift-fields-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.give-gift-layout{grid-template-columns:1fr}.give-gift-picker-list{max-height:320px}.collectible-row.animated,.collectible-row.backdrop{grid-template-columns:repeat(2,minmax(0,1fr))}.collectible-inline-preview,.collectible-backdrop-preview,.collectible-row .icon-btn{place-self:center start}}@media (width<=620px){.gift-import-note{flex-direction:column;align-items:flex-start}.gift-format-chips{justify-content:flex-start}.gift-file-picker{grid-template-columns:40px minmax(0,1fr)}.gift-file-action{display:none}.gift-fields-grid{grid-template-columns:1fr}.gift-list-summary{width:100%;margin-left:0}.official-gift-tools{flex-direction:column;align-items:stretch}.official-gift-list{grid-template-columns:1fr;max-height:340px}.official-gift-selected{grid-template-columns:82px minmax(0,1fr)}.official-gift-selected .gift-animation-shell{width:72px;height:72px}.collectible-modal-body{padding:10px}.collectible-definition-head,.collectible-section-head{flex-direction:column;align-items:flex-start}.collectible-row.animated,.collectible-row.backdrop{grid-template-columns:1fr}.collectible-active-grid{grid-template-columns:1fr 1fr}}.attr-block{background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius-sm);gap:8px;padding:10px;display:grid}.attr-block+.attr-block{margin-top:10px}.attr-block .duration-field input,.duration-field select{width:100%}.attr-block .btn{justify-content:center;width:100%}.emoji-grid{grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:10px;display:grid}.emoji-card{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);box-shadow:var(--shadow-sm);gap:8px;padding:12px;display:grid}.emoji-preview{background:var(--surface-soft);border:1px solid var(--line);border-radius:var(--radius-sm);place-items:center;height:88px;display:grid}.emoji-anim{width:80px;height:80px}.emoji-anim canvas{width:100%!important;height:100%!important}.emoji-glyph{font-size:46px;line-height:1}.emoji-meta{gap:4px;min-width:0;display:grid}.emoji-alt{font-size:18px;line-height:1.2}.emoji-id{width:100%;min-width:0;color:var(--text);background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius-sm);cursor:pointer;justify-content:space-between;align-items:center;gap:6px;padding:4px 8px;font-size:11px;display:flex}.emoji-id .mono{text-overflow:ellipsis;white-space:nowrap;flex:auto;min-width:0;overflow:hidden}.emoji-id svg{flex:none}.emoji-id:hover{border-color:var(--brand-tint-border);color:var(--brand)}.emoji-sub{color:var(--muted);text-overflow:ellipsis;white-space:nowrap;font-size:11px;overflow:hidden}.username-branch{margin:2px 0 0;padding:0;list-style:none}.username-branch li{color:var(--text-soft);padding-left:14px;font-size:12px;line-height:1.7;position:relative}.username-branch li:before{border-left:1px solid var(--line-strong,var(--line));border-bottom:1px solid var(--line-strong,var(--line));content:"";width:6px;height:11px;position:absolute;top:0;left:3px}.username-branch li.inactive{color:var(--muted)}.username-branch li.inactive span{text-decoration:line-through}.username-branch li em{text-transform:uppercase;letter-spacing:.04em;margin-left:6px;font-size:10px;font-style:normal;font-weight:800}.modal-backdrop{z-index:10000;background:var(--overlay);-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);place-items:center;padding:24px;display:grid;position:fixed;inset:0}.modal{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius-lg);width:min(760px,100%);max-height:min(820px,100vh - 48px);box-shadow:var(--shadow);padding:0;overflow:hidden}.command-modal{flex-direction:column;display:flex}.command-modal>.modal-head,.command-modal>.modal-actions{flex:none}.modal-head{border-bottom:1px solid var(--line);justify-content:space-between;align-items:flex-start;gap:12px;padding:16px 18px 12px;display:flex}.icon-btn{width:30px;height:30px;color:var(--text-soft);background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius-sm);cursor:pointer;place-items:center;transition:background-color .14s,border-color .14s,color .14s;display:grid}.icon-btn:hover{background:var(--btn-hover);border-color:var(--line-strong)}.command-steps{grid-template-columns:repeat(3,minmax(0,1fr));gap:8px;display:grid}.command-body{grid-auto-rows:max-content;gap:12px;min-height:0;padding:14px 18px;display:grid;overflow:auto}.mint-field-group{background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius);gap:8px;padding:12px;display:grid}.mint-field-group-label{color:var(--text-soft);text-transform:uppercase;letter-spacing:.04em;font-size:12px;font-weight:800}.command-step{min-height:38px;color:var(--muted);background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius-sm);align-items:center;gap:8px;padding:0 10px;display:flex}.command-step span{background:var(--panel);border:1px solid var(--line);border-radius:999px;place-items:center;width:20px;height:20px;font-size:11px;font-weight:800;display:grid}.command-step.active{color:var(--brand);border-color:var(--brand-tint-border)}.command-step.done{color:var(--good);border-color:var(--good-border)}.form-field{gap:6px;display:grid}.form-field span,.form-stack span{color:var(--text-soft);font-weight:800}.form-field input:disabled,.form-field textarea:disabled{opacity:.6;cursor:not-allowed}.command-preview{gap:8px;display:grid}.command-preview .json-block{max-height:150px}.preview-head,.result-title{color:var(--text-soft);align-items:center;gap:7px;font-weight:800;display:flex}.result-box{background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius);gap:8px;padding:10px;display:grid}.result-line{grid-template-columns:92px minmax(0,1fr);gap:8px;display:grid}.result-line span{color:var(--muted)}.result-line strong{overflow-wrap:anywhere}.result-message{color:var(--text-soft)}.secret-reveal{background:var(--warn-tint);border:1px solid var(--warn-border);border-radius:var(--radius);gap:6px;padding:10px;display:grid}.secret-reveal-label{color:var(--warn);font-size:12px;font-weight:800}.secret-reveal-row{align-items:center;gap:10px;display:flex}.secret-reveal-value{color:var(--text-soft);letter-spacing:.12em;background:var(--panel);border:1px solid var(--line);border-radius:var(--radius-sm);text-overflow:ellipsis;white-space:nowrap;flex:auto;padding:6px 10px;font-size:13px;overflow:hidden}.modal-actions{background:var(--panel);border-top:1px solid var(--line);justify-content:flex-end;padding:12px 18px}.login-page{background:var(--bg);background-image:radial-gradient(900px 480px at 50% -8%, var(--hero-glow) 0%, #0000 70%), linear-gradient(var(--hero-grid) 1px, transparent 1px), linear-gradient(90deg, var(--hero-grid) 1px, transparent 1px);background-size:auto,44px 44px,44px 44px;place-items:center;min-height:100vh;padding:24px;display:grid;position:relative;overflow:hidden}.login-page .bg-orbs{z-index:0;pointer-events:none;position:absolute;inset:-60px}.login-page .bg-orb{filter:blur(100px);pointer-events:none;border-radius:50%;position:absolute}.login-page .bg-orb--1{background:color-mix(in srgb, var(--brand-2) 40%, transparent);width:700px;height:700px;animation:20s ease-in-out infinite loginOrbFloat1;top:-15%;left:-10%}.login-page .bg-orb--2{background:color-mix(in srgb, var(--brand) 38%, transparent);width:600px;height:600px;animation:24s ease-in-out infinite loginOrbFloat2;top:25%;right:-15%}.login-page .bg-orb--3{background:color-mix(in srgb, var(--brand-2) 30%, transparent);width:500px;height:500px;animation:28s ease-in-out infinite loginOrbFloat3;bottom:-15%;left:30%}@keyframes loginOrbFloat1{0%,to{transform:translate(0)scale(1)}33%{transform:translate(60px,-40px)scale(1.08)}66%{transform:translate(-30px,30px)scale(.92)}}@keyframes loginOrbFloat2{0%,to{transform:translate(0)scale(1)}33%{transform:translate(-50px,-35px)scale(.93)}66%{transform:translate(45px,25px)scale(1.07)}}@keyframes loginOrbFloat3{0%,to{transform:translate(0)scale(1)}33%{transform:translate(40px,45px)scale(1.06)}66%{transform:translate(-55px,-25px)scale(.94)}}@media (width<=720px){.login-page .bg-orb{filter:blur(60px)}.login-page .bg-orb--1{width:350px;height:350px}.login-page .bg-orb--2{width:300px;height:300px}.login-page .bg-orb--3{width:250px;height:250px}}@media (prefers-reduced-motion:reduce){.login-page .bg-orb{animation:none}}.login-page .login-panel{z-index:1;position:relative}.login-panel{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius-lg);width:min(420px,100%);box-shadow:var(--shadow);gap:18px;padding:22px;display:grid}.login-head{justify-content:space-between;align-items:center;gap:12px;display:flex}.login-head-actions{flex-wrap:wrap;justify-content:flex-end;align-items:center;gap:8px;display:flex}.login-chip{min-height:24px;color:var(--brand);background:var(--brand-tint);border:1px solid var(--brand-tint-border);border-radius:999px;align-items:center;padding:0 8px;font-size:12px;display:inline-flex}.login-copy h1{color:var(--heading);margin:0;font-size:22px}.login-copy p{color:var(--muted);margin:8px 0 0}.form-stack{gap:12px;display:grid}.form-stack label{gap:6px;display:grid}.form-stack input{width:100%}.boot-screen{background:var(--bg);align-content:center;place-items:center;gap:18px;min-height:100vh;display:grid}.loader-bar{background:var(--line-strong);border-radius:999px;width:180px;height:4px;overflow:hidden}.loader-bar:before{content:"";background:var(--brand);width:42%;height:100%;animation:1s ease-in-out infinite load;display:block}.spin{animation:.8s linear infinite spin}@keyframes load{0%{transform:translate(-120%)}to{transform:translate(260%)}}@keyframes spin{to{transform:rotate(360deg)}}@media (width<=1120px){.shell{grid-template-columns:1fr}.sidebar{height:auto;position:static}.nav-list{grid-template-columns:repeat(4,minmax(0,1fr))}.sidebar-status{display:none}.overview-band,.split-layout,.operation-row,.raw-grid,.message-selector-grid,.message-selector-grid.single{grid-template-columns:1fr}.action-dock{position:static}.action-groups{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (width<=760px){.content,.topbar{padding-left:14px;padding-right:14px}.command-grid,.work-strip,.overview-metrics,.metric-row,.summary-grid,.command-steps,.action-groups{grid-template-columns:1fr}.sidebar{gap:12px;padding:14px}.nav-list{grid-template-columns:repeat(2,minmax(0,1fr))}.topbar,.page-title-row,.entity-head{flex-direction:column;align-items:flex-start}input,.searchbox{width:100%}.toolbar{align-items:stretch}.picker-row,.selected-entity{grid-template-columns:1fr}} diff --git a/cmd/telesrv-admin/web/dist/assets/index-DTpNyCcP.js b/cmd/telesrv-admin/web/dist/assets/index-DTpNyCcP.js new file mode 100644 index 00000000..7bb8c607 --- /dev/null +++ b/cmd/telesrv-admin/web/dist/assets/index-DTpNyCcP.js @@ -0,0 +1,9 @@ +var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,t)=>()=>(t||(e((t={exports:{}}).exports,t),e=null),t.exports),s=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;li[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},c=(n,r,a)=>(a=n==null?{}:e(i(n)),s(r||!n||!n.__esModule?t(a,`default`,{value:n,enumerable:!0}):a,n));(function(){let e=document.createElement(`link`).relList;if(e&&e.supports&&e.supports(`modulepreload`))return;for(let e of document.querySelectorAll(`link[rel="modulepreload"]`))n(e);new MutationObserver(e=>{for(let t of e)if(t.type===`childList`)for(let e of t.addedNodes)e.tagName===`LINK`&&e.rel===`modulepreload`&&n(e)}).observe(document,{childList:!0,subtree:!0});function t(e){let t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),e.crossOrigin===`use-credentials`?t.credentials=`include`:e.crossOrigin===`anonymous`?t.credentials=`omit`:t.credentials=`same-origin`,t}function n(e){if(e.ep)return;e.ep=!0;let n=t(e);fetch(e.href,n)}})();var l=o((e=>{var t=Symbol.for(`react.element`),n=Symbol.for(`react.portal`),r=Symbol.for(`react.fragment`),i=Symbol.for(`react.strict_mode`),a=Symbol.for(`react.profiler`),o=Symbol.for(`react.provider`),s=Symbol.for(`react.context`),c=Symbol.for(`react.forward_ref`),l=Symbol.for(`react.suspense`),u=Symbol.for(`react.memo`),d=Symbol.for(`react.lazy`),f=Symbol.iterator;function p(e){return typeof e!=`object`||!e?null:(e=f&&e[f]||e[`@@iterator`],typeof e==`function`?e:null)}var m={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},h=Object.assign,g={};function _(e,t,n){this.props=e,this.context=t,this.refs=g,this.updater=n||m}_.prototype.isReactComponent={},_.prototype.setState=function(e,t){if(typeof e!=`object`&&typeof e!=`function`&&e!=null)throw Error(`setState(...): takes an object of state variables to update or a function which returns an object of state variables.`);this.updater.enqueueSetState(this,e,t,`setState`)},_.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,`forceUpdate`)};function v(){}v.prototype=_.prototype;function y(e,t,n){this.props=e,this.context=t,this.refs=g,this.updater=n||m}var b=y.prototype=new v;b.constructor=y,h(b,_.prototype),b.isPureReactComponent=!0;var x=Array.isArray,S=Object.prototype.hasOwnProperty,C={current:null},w={key:!0,ref:!0,__self:!0,__source:!0};function T(e,n,r){var i,a={},o=null,s=null;if(n!=null)for(i in n.ref!==void 0&&(s=n.ref),n.key!==void 0&&(o=``+n.key),n)S.call(n,i)&&!w.hasOwnProperty(i)&&(a[i]=n[i]);var c=arguments.length-2;if(c===1)a.children=r;else if(1{t.exports=l()})),d=o((e=>{function t(e,t){var n=e.length;e.push(t);a:for(;0>>1,a=e[r];if(0>>1;ri(c,n))li(u,c)?(e[r]=u,e[l]=n,r=l):(e[r]=c,e[s]=n,r=s);else if(li(u,n))e[r]=u,e[l]=n,r=l;else break a}}return t}function i(e,t){var n=e.sortIndex-t.sortIndex;return n===0?e.id-t.id:n}if(typeof performance==`object`&&typeof performance.now==`function`){var a=performance;e.unstable_now=function(){return a.now()}}else{var o=Date,s=o.now();e.unstable_now=function(){return o.now()-s}}var c=[],l=[],u=1,d=null,f=3,p=!1,m=!1,h=!1,g=typeof setTimeout==`function`?setTimeout:null,_=typeof clearTimeout==`function`?clearTimeout:null,v=typeof setImmediate<`u`?setImmediate:null;typeof navigator<`u`&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0&&navigator.scheduling.isInputPending.bind(navigator.scheduling);function y(e){for(var i=n(l);i!==null;){if(i.callback===null)r(l);else if(i.startTime<=e)r(l),i.sortIndex=i.expirationTime,t(c,i);else break;i=n(l)}}function b(e){if(h=!1,y(e),!m)if(n(c)!==null)m=!0,M(x);else{var t=n(l);t!==null&&N(b,t.startTime-e)}}function x(t,i){m=!1,h&&(h=!1,_(w),w=-1),p=!0;var a=f;try{for(y(i),d=n(c);d!==null&&(!(d.expirationTime>i)||t&&!D());){var o=d.callback;if(typeof o==`function`){d.callback=null,f=d.priorityLevel;var s=o(d.expirationTime<=i);i=e.unstable_now(),typeof s==`function`?d.callback=s:d===n(c)&&r(c),y(i)}else r(c);d=n(c)}if(d!==null)var u=!0;else{var g=n(l);g!==null&&N(b,g.startTime-i),u=!1}return u}finally{d=null,f=a,p=!1}}var S=!1,C=null,w=-1,T=5,E=-1;function D(){return!(e.unstable_now()-Ee||125o?(r.sortIndex=a,t(l,r),n(c)===null&&r===n(l)&&(h?(_(w),w=-1):h=!0,N(b,a-o))):(r.sortIndex=s,t(c,r),m||p||(m=!0,M(x))),r},e.unstable_shouldYield=D,e.unstable_wrapCallback=function(e){var t=f;return function(){var n=f;f=t;try{return e.apply(this,arguments)}finally{f=n}}}})),f=o(((e,t)=>{t.exports=d()})),p=o((e=>{var t=u(),n=f();function r(e){for(var t=`https://reactjs.org/docs/error-decoder.html?invariant=`+e,n=1;n`u`||window.document===void 0||window.document.createElement===void 0),l=Object.prototype.hasOwnProperty,d=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,p={},m={};function h(e){return l.call(m,e)?!0:l.call(p,e)?!1:d.test(e)?m[e]=!0:(p[e]=!0,!1)}function g(e,t,n,r){if(n!==null&&n.type===0)return!1;switch(typeof t){case`function`:case`symbol`:return!0;case`boolean`:return r?!1:n===null?(e=e.toLowerCase().slice(0,5),e!==`data-`&&e!==`aria-`):!n.acceptsBooleans;default:return!1}}function _(e,t,n,r){if(t==null||g(e,t,n,r))return!0;if(r)return!1;if(n!==null)switch(n.type){case 3:return!t;case 4:return!1===t;case 5:return isNaN(t);case 6:return isNaN(t)||1>t}return!1}function v(e,t,n,r,i,a,o){this.acceptsBooleans=t===2||t===3||t===4,this.attributeName=r,this.attributeNamespace=i,this.mustUseProperty=n,this.propertyName=e,this.type=t,this.sanitizeURL=a,this.removeEmptyString=o}var y={};`children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style`.split(` `).forEach(function(e){y[e]=new v(e,0,!1,e,null,!1,!1)}),[[`acceptCharset`,`accept-charset`],[`className`,`class`],[`htmlFor`,`for`],[`httpEquiv`,`http-equiv`]].forEach(function(e){var t=e[0];y[t]=new v(t,1,!1,e[1],null,!1,!1)}),[`contentEditable`,`draggable`,`spellCheck`,`value`].forEach(function(e){y[e]=new v(e,2,!1,e.toLowerCase(),null,!1,!1)}),[`autoReverse`,`externalResourcesRequired`,`focusable`,`preserveAlpha`].forEach(function(e){y[e]=new v(e,2,!1,e,null,!1,!1)}),`allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope`.split(` `).forEach(function(e){y[e]=new v(e,3,!1,e.toLowerCase(),null,!1,!1)}),[`checked`,`multiple`,`muted`,`selected`].forEach(function(e){y[e]=new v(e,3,!0,e,null,!1,!1)}),[`capture`,`download`].forEach(function(e){y[e]=new v(e,4,!1,e,null,!1,!1)}),[`cols`,`rows`,`size`,`span`].forEach(function(e){y[e]=new v(e,6,!1,e,null,!1,!1)}),[`rowSpan`,`start`].forEach(function(e){y[e]=new v(e,5,!1,e.toLowerCase(),null,!1,!1)});var b=/[\-:]([a-z])/g;function x(e){return e[1].toUpperCase()}`accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height`.split(` `).forEach(function(e){var t=e.replace(b,x);y[t]=new v(t,1,!1,e,null,!1,!1)}),`xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type`.split(` `).forEach(function(e){var t=e.replace(b,x);y[t]=new v(t,1,!1,e,`http://www.w3.org/1999/xlink`,!1,!1)}),[`xml:base`,`xml:lang`,`xml:space`].forEach(function(e){var t=e.replace(b,x);y[t]=new v(t,1,!1,e,`http://www.w3.org/XML/1998/namespace`,!1,!1)}),[`tabIndex`,`crossOrigin`].forEach(function(e){y[e]=new v(e,1,!1,e.toLowerCase(),null,!1,!1)}),y.xlinkHref=new v(`xlinkHref`,1,!1,`xlink:href`,`http://www.w3.org/1999/xlink`,!0,!1),[`src`,`href`,`action`,`formAction`].forEach(function(e){y[e]=new v(e,1,!1,e.toLowerCase(),null,!0,!0)});function S(e,t,n,r){var i=y.hasOwnProperty(t)?y[t]:null;(i===null?r||!(2s||i[o]!==a[s]){var c=` +`+i[o].replace(` at new `,` at `);return e.displayName&&c.includes(``)&&(c=c.replace(``,e.displayName)),c}while(1<=o&&0<=s);break}}}finally{ae=!1,Error.prepareStackTrace=n}return(e=e?e.displayName||e.name:``)?ie(e):``}function se(e){switch(e.tag){case 5:return ie(e.type);case 16:return ie(`Lazy`);case 13:return ie(`Suspense`);case 19:return ie(`SuspenseList`);case 0:case 2:case 15:return e=oe(e.type,!1),e;case 11:return e=oe(e.type.render,!1),e;case 1:return e=oe(e.type,!0),e;default:return``}}function ce(e){if(e==null)return null;if(typeof e==`function`)return e.displayName||e.name||null;if(typeof e==`string`)return e;switch(e){case E:return`Fragment`;case T:return`Portal`;case O:return`Profiler`;case D:return`StrictMode`;case M:return`Suspense`;case N:return`SuspenseList`}if(typeof e==`object`)switch(e.$$typeof){case A:return(e.displayName||`Context`)+`.Consumer`;case k:return(e._context.displayName||`Context`)+`.Provider`;case j:var t=e.render;return e=e.displayName,e||=(e=t.displayName||t.name||``,e===``?`ForwardRef`:`ForwardRef(`+e+`)`),e;case P:return t=e.displayName||null,t===null?ce(e.type)||`Memo`:t;case ee:t=e._payload,e=e._init;try{return ce(e(t))}catch{}}return null}function le(e){var t=e.type;switch(e.tag){case 24:return`Cache`;case 9:return(t.displayName||`Context`)+`.Consumer`;case 10:return(t._context.displayName||`Context`)+`.Provider`;case 18:return`DehydratedFragment`;case 11:return e=t.render,e=e.displayName||e.name||``,t.displayName||(e===``?`ForwardRef`:`ForwardRef(`+e+`)`);case 7:return`Fragment`;case 5:return t;case 4:return`Portal`;case 3:return`Root`;case 6:return`Text`;case 16:return ce(t);case 8:return t===D?`StrictMode`:`Mode`;case 22:return`Offscreen`;case 12:return`Profiler`;case 21:return`Scope`;case 13:return`Suspense`;case 19:return`SuspenseList`;case 25:return`TracingMarker`;case 1:case 0:case 17:case 2:case 14:case 15:if(typeof t==`function`)return t.displayName||t.name||null;if(typeof t==`string`)return t}return null}function ue(e){switch(typeof e){case`boolean`:case`number`:case`string`:case`undefined`:return e;case`object`:return e;default:return``}}function de(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()===`input`&&(t===`checkbox`||t===`radio`)}function fe(e){var t=de(e)?`checked`:`value`,n=Object.getOwnPropertyDescriptor(e.constructor.prototype,t),r=``+e[t];if(!e.hasOwnProperty(t)&&n!==void 0&&typeof n.get==`function`&&typeof n.set==`function`){var i=n.get,a=n.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return i.call(this)},set:function(e){r=``+e,a.call(this,e)}}),Object.defineProperty(e,t,{enumerable:n.enumerable}),{getValue:function(){return r},setValue:function(e){r=``+e},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}function L(e){e._valueTracker||=fe(e)}function pe(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var n=t.getValue(),r=``;return e&&(r=de(e)?e.checked?`true`:`false`:e.value),e=r,e===n?!1:(t.setValue(e),!0)}function me(e){if(e||=typeof document<`u`?document:void 0,e===void 0)return null;try{return e.activeElement||e.body}catch{return e.body}}function he(e,t){var n=t.checked;return re({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:n??e._wrapperState.initialChecked})}function ge(e,t){var n=t.defaultValue==null?``:t.defaultValue,r=t.checked==null?t.defaultChecked:t.checked;n=ue(t.value==null?n:t.value),e._wrapperState={initialChecked:r,initialValue:n,controlled:t.type===`checkbox`||t.type===`radio`?t.checked!=null:t.value!=null}}function _e(e,t){t=t.checked,t!=null&&S(e,`checked`,t,!1)}function ve(e,t){_e(e,t);var n=ue(t.value),r=t.type;if(n!=null)r===`number`?(n===0&&e.value===``||e.value!=n)&&(e.value=``+n):e.value!==``+n&&(e.value=``+n);else if(r===`submit`||r===`reset`){e.removeAttribute(`value`);return}t.hasOwnProperty(`value`)?be(e,t.type,n):t.hasOwnProperty(`defaultValue`)&&be(e,t.type,ue(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}function ye(e,t,n){if(t.hasOwnProperty(`value`)||t.hasOwnProperty(`defaultValue`)){var r=t.type;if(!(r!==`submit`&&r!==`reset`||t.value!==void 0&&t.value!==null))return;t=``+e._wrapperState.initialValue,n||t===e.value||(e.value=t),e.defaultValue=t}n=e.name,n!==``&&(e.name=``),e.defaultChecked=!!e._wrapperState.initialChecked,n!==``&&(e.name=n)}function be(e,t,n){(t!==`number`||me(e.ownerDocument)!==e)&&(n==null?e.defaultValue=``+e._wrapperState.initialValue:e.defaultValue!==``+n&&(e.defaultValue=``+n))}var xe=Array.isArray;function Se(e,t,n,r){if(e=e.options,t){t={};for(var i=0;i`+t.valueOf().toString()+``,t=De.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;t.firstChild;)e.appendChild(t.firstChild)}});function ke(e,t){if(t){var n=e.firstChild;if(n&&n===e.lastChild&&n.nodeType===3){n.nodeValue=t;return}}e.textContent=t}var Ae={animationIterationCount:!0,aspectRatio:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},je=[`Webkit`,`ms`,`Moz`,`O`];Object.keys(Ae).forEach(function(e){je.forEach(function(t){t=t+e.charAt(0).toUpperCase()+e.substring(1),Ae[t]=Ae[e]})});function Me(e,t,n){return t==null||typeof t==`boolean`||t===``?``:n||typeof t!=`number`||t===0||Ae.hasOwnProperty(e)&&Ae[e]?(``+t).trim():t+`px`}function Ne(e,t){for(var n in e=e.style,t)if(t.hasOwnProperty(n)){var r=n.indexOf(`--`)===0,i=Me(n,t[n],r);n===`float`&&(n=`cssFloat`),r?e.setProperty(n,i):e[n]=i}}var Pe=re({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0});function Fe(e,t){if(t){if(Pe[e]&&(t.children!=null||t.dangerouslySetInnerHTML!=null))throw Error(r(137,e));if(t.dangerouslySetInnerHTML!=null){if(t.children!=null)throw Error(r(60));if(typeof t.dangerouslySetInnerHTML!=`object`||!(`__html`in t.dangerouslySetInnerHTML))throw Error(r(61))}if(t.style!=null&&typeof t.style!=`object`)throw Error(r(62))}}function Ie(e,t){if(e.indexOf(`-`)===-1)return typeof t.is==`string`;switch(e){case`annotation-xml`:case`color-profile`:case`font-face`:case`font-face-src`:case`font-face-uri`:case`font-face-format`:case`font-face-name`:case`missing-glyph`:return!1;default:return!0}}var Le=null;function Re(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}var ze=null,Be=null,Ve=null;function He(e){if(e=Ai(e)){if(typeof ze!=`function`)throw Error(r(280));var t=e.stateNode;t&&(t=Mi(t),ze(e.stateNode,e.type,t))}}function Ue(e){Be?Ve?Ve.push(e):Ve=[e]:Be=e}function We(){if(Be){var e=Be,t=Ve;if(Ve=Be=null,He(e),t)for(e=0;e>>=0,e===0?32:31-(Tt(e)/Et|0)|0}var Ot=64,kt=4194304;function At(e){switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e&4194240;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return e&130023424;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return e}}function jt(e,t){var n=e.pendingLanes;if(n===0)return 0;var r=0,i=e.suspendedLanes,a=e.pingedLanes,o=n&268435455;if(o!==0){var s=o&~i;s===0?(a&=o,a!==0&&(r=At(a))):r=At(s)}else o=n&~i,o===0?a!==0&&(r=At(a)):r=At(o);if(r===0)return 0;if(t!==0&&t!==r&&(t&i)===0&&(i=r&-r,a=t&-t,i>=a||i===16&&a&4194240))return t;if(r&4&&(r|=n&16),t=e.entangledLanes,t!==0)for(e=e.entanglements,t&=r;0n;n++)t.push(e);return t}function Lt(e,t,n){e.pendingLanes|=t,t!==536870912&&(e.suspendedLanes=0,e.pingedLanes=0),e=e.eventTimes,t=31-wt(t),e[t]=n}function Rt(e,t){var n=e.pendingLanes&~t;e.pendingLanes=t,e.suspendedLanes=0,e.pingedLanes=0,e.expiredLanes&=t,e.mutableReadLanes&=t,e.entangledLanes&=t,t=e.entanglements;var r=e.eventTimes;for(e=e.expirationTimes;0=J),Jn=` `,Yn=!1;function Xn(e,t){switch(e){case`keyup`:return K.indexOf(t.keyCode)!==-1;case`keydown`:return t.keyCode!==229;case`keypress`:case`mousedown`:case`focusout`:return!0;default:return!1}}function Zn(e){return e=e.detail,typeof e==`object`&&`data`in e?e.data:null}var Qn=!1;function $n(e,t){switch(e){case`compositionend`:return Zn(t);case`keypress`:return t.which===32?(Yn=!0,Jn):null;case`textInput`:return e=t.data,e===Jn&&Yn?null:e;default:return null}}function er(e,t){if(Qn)return e===`compositionend`||!q&&Xn(e,t)?(e=xn(),bn=yn=vn=null,Qn=!1,e):null;switch(e){case`paste`:return null;case`keypress`:if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=r}a:{for(;n;){if(n.nextSibling){n=n.nextSibling;break a}n=n.parentNode}n=void 0}n=br(n)}}function Sr(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?Sr(e,t.parentNode):`contains`in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function Cr(){for(var e=window,t=me();t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href==`string`}catch{n=!1}if(n)e=t.contentWindow;else break;t=me(e.document)}return t}function wr(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t===`input`&&(e.type===`text`||e.type===`search`||e.type===`tel`||e.type===`url`||e.type===`password`)||t===`textarea`||e.contentEditable===`true`)}function Tr(e){var t=Cr(),n=e.focusedElem,r=e.selectionRange;if(t!==n&&n&&n.ownerDocument&&Sr(n.ownerDocument.documentElement,n)){if(r!==null&&wr(n)){if(t=r.start,e=r.end,e===void 0&&(e=t),`selectionStart`in n)n.selectionStart=t,n.selectionEnd=Math.min(e,n.value.length);else if(e=(t=n.ownerDocument||document)&&t.defaultView||window,e.getSelection){e=e.getSelection();var i=n.textContent.length,a=Math.min(r.start,i);r=r.end===void 0?a:Math.min(r.end,i),!e.extend&&a>r&&(i=r,r=a,a=i),i=xr(n,a);var o=xr(n,r);i&&o&&(e.rangeCount!==1||e.anchorNode!==i.node||e.anchorOffset!==i.offset||e.focusNode!==o.node||e.focusOffset!==o.offset)&&(t=t.createRange(),t.setStart(i.node,i.offset),e.removeAllRanges(),a>r?(e.addRange(t),e.extend(o.node,o.offset)):(t.setEnd(o.node,o.offset),e.addRange(t)))}}for(t=[],e=n;e=e.parentNode;)e.nodeType===1&&t.push({element:e,left:e.scrollLeft,top:e.scrollTop});for(typeof n.focus==`function`&&n.focus(),n=0;n=document.documentMode,Dr=null,Or=null,kr=null,Ar=!1;function jr(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;Ar||Dr==null||Dr!==me(r)||(r=Dr,`selectionStart`in r&&wr(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),kr&&yr(kr,r)||(kr=r,r=ri(Or,`onSelect`),0Pi||(e.current=Ni[Pi],Ni[Pi]=null,Pi--)}function Li(e,t){Pi++,Ni[Pi]=e.current,e.current=t}var Ri={},zi=Fi(Ri),Bi=Fi(!1),Vi=Ri;function Hi(e,t){var n=e.type.contextTypes;if(!n)return Ri;var r=e.stateNode;if(r&&r.__reactInternalMemoizedUnmaskedChildContext===t)return r.__reactInternalMemoizedMaskedChildContext;var i={},a;for(a in n)i[a]=t[a];return r&&(e=e.stateNode,e.__reactInternalMemoizedUnmaskedChildContext=t,e.__reactInternalMemoizedMaskedChildContext=i),i}function Ui(e){return e=e.childContextTypes,e!=null}function Wi(){Ii(Bi),Ii(zi)}function Gi(e,t,n){if(zi.current!==Ri)throw Error(r(168));Li(zi,t),Li(Bi,n)}function Ki(e,t,n){var i=e.stateNode;if(t=t.childContextTypes,typeof i.getChildContext!=`function`)return n;for(var a in i=i.getChildContext(),i)if(!(a in t))throw Error(r(108,le(e)||`Unknown`,a));return re({},n,i)}function qi(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||Ri,Vi=zi.current,Li(zi,e),Li(Bi,Bi.current),!0}function Ji(e,t,n){var i=e.stateNode;if(!i)throw Error(r(169));n?(e=Ki(e,t,Vi),i.__reactInternalMemoizedMergedChildContext=e,Ii(Bi),Ii(zi),Li(zi,e)):Ii(Bi),Li(Bi,n)}var Yi=null,Xi=!1,Zi=!1;function Qi(e){Yi===null?Yi=[e]:Yi.push(e)}function $i(e){Xi=!0,Qi(e)}function ea(){if(!Zi&&Yi!==null){Zi=!0;var e=0,t=H;try{var n=Yi;for(H=1;e>=o,i-=o,ca=1<<32-wt(t)+i|n<h?(g=d,d=null):g=d.sibling;var _=p(r,d,s[h],c);if(_===null){d===null&&(d=g);break}e&&d&&_.alternate===null&&t(r,d),a=o(_,a,h),u===null?l=_:u.sibling=_,u=_,d=g}if(h===s.length)return n(r,d),ga&&ua(r,h),l;if(d===null){for(;hg?(_=h,h=null):_=h.sibling;var y=p(a,h,v.value,l);if(y===null){h===null&&(h=_);break}e&&h&&y.alternate===null&&t(a,h),s=o(y,s,g),d===null?u=y:d.sibling=y,d=y,h=_}if(v.done)return n(a,h),ga&&ua(a,g),u;if(h===null){for(;!v.done;g++,v=c.next())v=f(a,v.value,l),v!==null&&(s=o(v,s,g),d===null?u=v:d.sibling=v,d=v);return ga&&ua(a,g),u}for(h=i(a,h);!v.done;g++,v=c.next())v=m(h,a,g,v.value,l),v!==null&&(e&&v.alternate!==null&&h.delete(v.key===null?g:v.key),s=o(v,s,g),d===null?u=v:d.sibling=v,d=v);return e&&h.forEach(function(e){return t(a,e)}),ga&&ua(a,g),u}function _(e,r,i,o){if(typeof i==`object`&&i&&i.type===E&&i.key===null&&(i=i.props.children),typeof i==`object`&&i){switch(i.$$typeof){case w:a:{for(var c=i.key,l=r;l!==null;){if(l.key===c){if(c=i.type,c===E){if(l.tag===7){n(e,l.sibling),r=a(l,i.props.children),r.return=e,e=r;break a}}else if(l.elementType===c||typeof c==`object`&&c&&c.$$typeof===ee&&Aa(c)===l.type){n(e,l.sibling),r=a(l,i.props),r.ref=Oa(e,l,i),r.return=e,e=r;break a}n(e,l);break}else t(e,l);l=l.sibling}i.type===E?(r=Zl(i.props.children,e.mode,o,i.key),r.return=e,e=r):(o=Xl(i.type,i.key,i.props,null,e.mode,o),o.ref=Oa(e,r,i),o.return=e,e=o)}return s(e);case T:a:{for(l=i.key;r!==null;){if(r.key===l)if(r.tag===4&&r.stateNode.containerInfo===i.containerInfo&&r.stateNode.implementation===i.implementation){n(e,r.sibling),r=a(r,i.children||[]),r.return=e,e=r;break a}else{n(e,r);break}else t(e,r);r=r.sibling}r=eu(i,e.mode,o),r.return=e,e=r}return s(e);case ee:return l=i._init,_(e,r,l(i._payload),o)}if(xe(i))return h(e,r,i,o);if(ne(i))return g(e,r,i,o);ka(e,i)}return typeof i==`string`&&i!==``||typeof i==`number`?(i=``+i,r!==null&&r.tag===6?(n(e,r.sibling),r=a(r,i),r.return=e,e=r):(n(e,r),r=$l(i,e.mode,o),r.return=e,e=r),s(e)):n(e,r)}return _}var Ma=ja(!0),Na=ja(!1),Pa=Fi(null),Fa=null,Ia=null,La=null;function Ra(){La=Ia=Fa=null}function za(e){var t=Pa.current;Ii(Pa),e._currentValue=t}function Ba(e,t,n){for(;e!==null;){var r=e.alternate;if((e.childLanes&t)===t?r!==null&&(r.childLanes&t)!==t&&(r.childLanes|=t):(e.childLanes|=t,r!==null&&(r.childLanes|=t)),e===n)break;e=e.return}}function Va(e,t){Fa=e,La=Ia=null,e=e.dependencies,e!==null&&e.firstContext!==null&&((e.lanes&t)!==0&&(js=!0),e.firstContext=null)}function Ha(e){var t=e._currentValue;if(La!==e)if(e={context:e,memoizedValue:t,next:null},Ia===null){if(Fa===null)throw Error(r(308));Ia=e,Fa.dependencies={lanes:0,firstContext:e}}else Ia=Ia.next=e;return t}var Ua=null;function Wa(e){Ua===null?Ua=[e]:Ua.push(e)}function Ga(e,t,n,r){var i=t.interleaved;return i===null?(n.next=n,Wa(t)):(n.next=i.next,i.next=n),t.interleaved=n,Ka(e,r)}function Ka(e,t){e.lanes|=t;var n=e.alternate;for(n!==null&&(n.lanes|=t),n=e,e=e.return;e!==null;)e.childLanes|=t,n=e.alternate,n!==null&&(n.childLanes|=t),n=e,e=e.return;return n.tag===3?n.stateNode:null}var qa=!1;function Ja(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,interleaved:null,lanes:0},effects:null}}function Ya(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,effects:e.effects})}function Xa(e,t){return{eventTime:e,lane:t,tag:0,payload:null,callback:null,next:null}}function Za(e,t,n){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,Bc&2){var i=r.pending;return i===null?t.next=t:(t.next=i.next,i.next=t),r.pending=t,Ka(e,n)}return i=r.interleaved,i===null?(t.next=t,Wa(r)):(t.next=i.next,i.next=t),r.interleaved=t,Ka(e,n)}function Qa(e,t,n){if(t=t.updateQueue,t!==null&&(t=t.shared,n&4194240)){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,zt(e,n)}}function $a(e,t){var n=e.updateQueue,r=e.alternate;if(r!==null&&(r=r.updateQueue,n===r)){var i=null,a=null;if(n=n.firstBaseUpdate,n!==null){do{var o={eventTime:n.eventTime,lane:n.lane,tag:n.tag,payload:n.payload,callback:n.callback,next:null};a===null?i=a=o:a=a.next=o,n=n.next}while(n!==null);a===null?i=a=t:a=a.next=t}else i=a=t;n={baseState:r.baseState,firstBaseUpdate:i,lastBaseUpdate:a,shared:r.shared,effects:r.effects},e.updateQueue=n;return}e=n.lastBaseUpdate,e===null?n.firstBaseUpdate=t:e.next=t,n.lastBaseUpdate=t}function eo(e,t,n,r){var i=e.updateQueue;qa=!1;var a=i.firstBaseUpdate,o=i.lastBaseUpdate,s=i.shared.pending;if(s!==null){i.shared.pending=null;var c=s,l=c.next;c.next=null,o===null?a=l:o.next=l,o=c;var u=e.alternate;u!==null&&(u=u.updateQueue,s=u.lastBaseUpdate,s!==o&&(s===null?u.firstBaseUpdate=l:s.next=l,u.lastBaseUpdate=c))}if(a!==null){var d=i.baseState;o=0,u=l=c=null,s=a;do{var f=s.lane,p=s.eventTime;if((r&f)===f){u!==null&&(u=u.next={eventTime:p,lane:0,tag:s.tag,payload:s.payload,callback:s.callback,next:null});a:{var m=e,h=s;switch(f=t,p=n,h.tag){case 1:if(m=h.payload,typeof m==`function`){d=m.call(p,d,f);break a}d=m;break a;case 3:m.flags=m.flags&-65537|128;case 0:if(m=h.payload,f=typeof m==`function`?m.call(p,d,f):m,f==null)break a;d=re({},d,f);break a;case 2:qa=!0}}s.callback!==null&&s.lane!==0&&(e.flags|=64,f=i.effects,f===null?i.effects=[s]:f.push(s))}else p={eventTime:p,lane:f,tag:s.tag,payload:s.payload,callback:s.callback,next:null},u===null?(l=u=p,c=d):u=u.next=p,o|=f;if(s=s.next,s===null){if(s=i.shared.pending,s===null)break;f=s,s=f.next,f.next=null,i.lastBaseUpdate=f,i.shared.pending=null}}while(1);if(u===null&&(c=d),i.baseState=c,i.firstBaseUpdate=l,i.lastBaseUpdate=u,t=i.shared.interleaved,t!==null){i=t;do o|=i.lane,i=i.next;while(i!==t)}else a===null&&(i.shared.lanes=0);Jc|=o,e.lanes=o,e.memoizedState=d}}function to(e,t,n){if(e=t.effects,t.effects=null,e!==null)for(t=0;tn?n:4,e(!0);var r=_o.transition;_o.transition={};try{e(!1),t()}finally{H=n,_o.transition=r}}function is(){return jo().memoizedState}function as(e,t,n){var r=pl(e);if(n={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null},ss(e))cs(t,n);else if(n=Ga(e,t,n,r),n!==null){var i=fl();ml(n,e,r,i),ls(n,t,r)}}function os(e,t,n){var r=pl(e),i={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null};if(ss(e))cs(t,i);else{var a=e.alternate;if(e.lanes===0&&(a===null||a.lanes===0)&&(a=t.lastRenderedReducer,a!==null))try{var o=t.lastRenderedState,s=a(o,n);if(i.hasEagerState=!0,i.eagerState=s,Q(s,o)){var c=t.interleaved;c===null?(i.next=i,Wa(t)):(i.next=c.next,c.next=i),t.interleaved=i;return}}catch{}n=Ga(e,t,i,r),n!==null&&(i=fl(),ml(n,e,r,i),ls(n,t,r))}}function ss(e){var t=e.alternate;return e===yo||t!==null&&t===yo}function cs(e,t){Co=So=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function ls(e,t,n){if(n&4194240){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,zt(e,n)}}var us={readContext:Ha,useCallback:Eo,useContext:Eo,useEffect:Eo,useImperativeHandle:Eo,useInsertionEffect:Eo,useLayoutEffect:Eo,useMemo:Eo,useReducer:Eo,useRef:Eo,useState:Eo,useDebugValue:Eo,useDeferredValue:Eo,useTransition:Eo,useMutableSource:Eo,useSyncExternalStore:Eo,useId:Eo,unstable_isNewReconciler:!1},ds={readContext:Ha,useCallback:function(e,t){return Ao().memoizedState=[e,t===void 0?null:t],e},useContext:Ha,useEffect:qo,useImperativeHandle:function(e,t,n){return n=n==null?null:n.concat([e]),Go(4194308,4,Zo.bind(null,t,e),n)},useLayoutEffect:function(e,t){return Go(4194308,4,e,t)},useInsertionEffect:function(e,t){return Go(4,2,e,t)},useMemo:function(e,t){var n=Ao();return t=t===void 0?null:t,e=e(),n.memoizedState=[e,t],e},useReducer:function(e,t,n){var r=Ao();return t=n===void 0?t:n(t),r.memoizedState=r.baseState=t,e={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:t},r.queue=e,e=e.dispatch=as.bind(null,yo,e),[r.memoizedState,e]},useRef:function(e){var t=Ao();return e={current:e},t.memoizedState=e},useState:Ho,useDebugValue:$o,useDeferredValue:function(e){return Ao().memoizedState=e},useTransition:function(){var e=Ho(!1),t=e[0];return e=rs.bind(null,e[1]),Ao().memoizedState=e,[t,e]},useMutableSource:function(){},useSyncExternalStore:function(e,t,n){var i=yo,a=Ao();if(ga){if(n===void 0)throw Error(r(407));n=n()}else{if(n=t(),Vc===null)throw Error(r(349));vo&30||Lo(i,t,n)}a.memoizedState=n;var o={value:n,getSnapshot:t};return a.queue=o,qo(zo.bind(null,i,o,e),[e]),i.flags|=2048,Uo(9,Ro.bind(null,i,o,n,t),void 0,null),n},useId:function(){var e=Ao(),t=Vc.identifierPrefix;if(ga){var n=la,r=ca;n=(r&~(1<<32-wt(r)-1)).toString(32)+n,t=`:`+t+`R`+n,n=wo++,0<\/script>`,e=e.removeChild(e.firstChild)):typeof i.is==`string`?e=c.createElement(n,{is:i.is}):(e=c.createElement(n),n===`select`&&(c=e,i.multiple?c.multiple=!0:i.size&&(c.size=i.size))):e=c.createElementNS(e,n),e[Ci]=t,e[wi]=i,tc(e,t,!1,!1),t.stateNode=e;a:{switch(c=Ie(n,i),n){case`dialog`:Xr(`cancel`,e),Xr(`close`,e),o=i;break;case`iframe`:case`object`:case`embed`:Xr(`load`,e),o=i;break;case`video`:case`audio`:for(o=0;oel&&(t.flags|=128,i=!0,ic(s,!1),t.lanes=4194304)}else{if(!i)if(e=po(c),e!==null){if(t.flags|=128,i=!0,n=e.updateQueue,n!==null&&(t.updateQueue=n,t.flags|=4),ic(s,!0),s.tail===null&&s.tailMode===`hidden`&&!c.alternate&&!ga)return ac(t),null}else 2*V()-s.renderingStartTime>el&&n!==1073741824&&(t.flags|=128,i=!0,ic(s,!1),t.lanes=4194304);s.isBackwards?(c.sibling=t.child,t.child=c):(n=s.last,n===null?t.child=c:n.sibling=c,s.last=c)}return s.tail===null?(ac(t),null):(t=s.tail,s.rendering=t,s.tail=t.sibling,s.renderingStartTime=V(),t.sibling=null,n=fo.current,Li(fo,i?n&1|2:n&1),t);case 22:case 23:return wl(),i=t.memoizedState!==null,e!==null&&e.memoizedState!==null!==i&&(t.flags|=8192),i&&t.mode&1?Wc&1073741824&&(ac(t),t.subtreeFlags&6&&(t.flags|=8192)):ac(t),null;case 24:return null;case 25:return null}throw Error(r(156,t.tag))}function sc(e,t){switch(pa(t),t.tag){case 1:return Ui(t.type)&&Wi(),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return co(),Ii(Bi),Ii(zi),ho(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 5:return uo(t),null;case 13:if(Ii(fo),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(r(340));Ta()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return Ii(fo),null;case 4:return co(),null;case 10:return za(t.type._context),null;case 22:case 23:return wl(),null;case 24:return null;default:return null}}var cc=!1,lc=!1,uc=typeof WeakSet==`function`?WeakSet:Set,$=null;function dc(e,t){var n=e.ref;if(n!==null)if(typeof n==`function`)try{n(null)}catch(n){Rl(e,t,n)}else n.current=null}function fc(e,t,n){try{n()}catch(n){Rl(e,t,n)}}var pc=!1;function mc(e,t){if(di=dn,e=Cr(),wr(e)){if(`selectionStart`in e)var n={start:e.selectionStart,end:e.selectionEnd};else a:{n=(n=e.ownerDocument)&&n.defaultView||window;var i=n.getSelection&&n.getSelection();if(i&&i.rangeCount!==0){n=i.anchorNode;var a=i.anchorOffset,o=i.focusNode;i=i.focusOffset;try{n.nodeType,o.nodeType}catch{n=null;break a}var s=0,c=-1,l=-1,u=0,d=0,f=e,p=null;b:for(;;){for(var m;f!==n||a!==0&&f.nodeType!==3||(c=s+a),f!==o||i!==0&&f.nodeType!==3||(l=s+i),f.nodeType===3&&(s+=f.nodeValue.length),(m=f.firstChild)!==null;)p=f,f=m;for(;;){if(f===e)break b;if(p===n&&++u===a&&(c=s),p===o&&++d===i&&(l=s),(m=f.nextSibling)!==null)break;f=p,p=f.parentNode}f=m}n=c===-1||l===-1?null:{start:c,end:l}}else n=null}n||={start:0,end:0}}else n=null;for(fi={focusedElem:e,selectionRange:n},dn=!1,$=t;$!==null;)if(t=$,e=t.child,t.subtreeFlags&1028&&e!==null)e.return=t,$=e;else for(;$!==null;){t=$;try{var h=t.alternate;if(t.flags&1024)switch(t.tag){case 0:case 11:case 15:break;case 1:if(h!==null){var g=h.memoizedProps,_=h.memoizedState,v=t.stateNode;v.__reactInternalSnapshotBeforeUpdate=v.getSnapshotBeforeUpdate(t.elementType===t.type?g:ms(t.type,g),_)}break;case 3:var y=t.stateNode.containerInfo;y.nodeType===1?y.textContent=``:y.nodeType===9&&y.documentElement&&y.removeChild(y.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(r(163))}}catch(e){Rl(t,t.return,e)}if(e=t.sibling,e!==null){e.return=t.return,$=e;break}$=t.return}return h=pc,pc=!1,h}function hc(e,t,n){var r=t.updateQueue;if(r=r===null?null:r.lastEffect,r!==null){var i=r=r.next;do{if((i.tag&e)===e){var a=i.destroy;i.destroy=void 0,a!==void 0&&fc(t,n,a)}i=i.next}while(i!==r)}}function gc(e,t){if(t=t.updateQueue,t=t===null?null:t.lastEffect,t!==null){var n=t=t.next;do{if((n.tag&e)===e){var r=n.create;n.destroy=r()}n=n.next}while(n!==t)}}function _c(e){var t=e.ref;if(t!==null){var n=e.stateNode;switch(e.tag){case 5:e=n;break;default:e=n}typeof t==`function`?t(e):t.current=e}}function vc(e){var t=e.alternate;t!==null&&(e.alternate=null,vc(t)),e.child=null,e.deletions=null,e.sibling=null,e.tag===5&&(t=e.stateNode,t!==null&&(delete t[Ci],delete t[wi],delete t[Ei],delete t[Di],delete t[Oi])),e.stateNode=null,e.return=null,e.dependencies=null,e.memoizedProps=null,e.memoizedState=null,e.pendingProps=null,e.stateNode=null,e.updateQueue=null}function yc(e){return e.tag===5||e.tag===3||e.tag===4}function bc(e){a:for(;;){for(;e.sibling===null;){if(e.return===null||yc(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.flags&2||e.child===null||e.tag===4)continue a;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function xc(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.nodeType===8?n.parentNode.insertBefore(e,t):n.insertBefore(e,t):(n.nodeType===8?(t=n.parentNode,t.insertBefore(e,n)):(t=n,t.appendChild(e)),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=ui));else if(r!==4&&(e=e.child,e!==null))for(xc(e,t,n),e=e.sibling;e!==null;)xc(e,t,n),e=e.sibling}function Sc(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(r!==4&&(e=e.child,e!==null))for(Sc(e,t,n),e=e.sibling;e!==null;)Sc(e,t,n),e=e.sibling}var Cc=null,wc=!1;function Tc(e,t,n){for(n=n.child;n!==null;)Ec(e,t,n),n=n.sibling}function Ec(e,t,n){if(St&&typeof St.onCommitFiberUnmount==`function`)try{St.onCommitFiberUnmount(xt,n)}catch{}switch(n.tag){case 5:lc||dc(n,t);case 6:var r=Cc,i=wc;Cc=null,Tc(e,t,n),Cc=r,wc=i,Cc!==null&&(wc?(e=Cc,n=n.stateNode,e.nodeType===8?e.parentNode.removeChild(n):e.removeChild(n)):Cc.removeChild(n.stateNode));break;case 18:Cc!==null&&(wc?(e=Cc,n=n.stateNode,e.nodeType===8?yi(e.parentNode,n):e.nodeType===1&&yi(e,n),ln(e)):yi(Cc,n.stateNode));break;case 4:r=Cc,i=wc,Cc=n.stateNode.containerInfo,wc=!0,Tc(e,t,n),Cc=r,wc=i;break;case 0:case 11:case 14:case 15:if(!lc&&(r=n.updateQueue,r!==null&&(r=r.lastEffect,r!==null))){i=r=r.next;do{var a=i,o=a.destroy;a=a.tag,o!==void 0&&(a&2||a&4)&&fc(n,t,o),i=i.next}while(i!==r)}Tc(e,t,n);break;case 1:if(!lc&&(dc(n,t),r=n.stateNode,typeof r.componentWillUnmount==`function`))try{r.props=n.memoizedProps,r.state=n.memoizedState,r.componentWillUnmount()}catch(e){Rl(n,t,e)}Tc(e,t,n);break;case 21:Tc(e,t,n);break;case 22:n.mode&1?(lc=(r=lc)||n.memoizedState!==null,Tc(e,t,n),lc=r):Tc(e,t,n);break;default:Tc(e,t,n)}}function Dc(e){var t=e.updateQueue;if(t!==null){e.updateQueue=null;var n=e.stateNode;n===null&&(n=e.stateNode=new uc),t.forEach(function(t){var r=Hl.bind(null,e,t);n.has(t)||(n.add(t),t.then(r,r))})}}function Oc(e,t){var n=t.deletions;if(n!==null)for(var i=0;ia&&(a=s),i&=~o}if(i=a,i=V()-i,i=(120>i?120:480>i?480:1080>i?1080:1920>i?1920:3e3>i?3e3:4320>i?4320:1960*Ic(i/1960))-i,10e?16:e,ol===null)var i=!1;else{if(e=ol,ol=null,sl=0,Bc&6)throw Error(r(331));var a=Bc;for(Bc|=4,$=e.current;$!==null;){var o=$,s=o.child;if($.flags&16){var c=o.deletions;if(c!==null){for(var l=0;lV()-$c?Tl(e,0):Xc|=n),hl(e,t)}function Bl(e,t){t===0&&(e.mode&1?(t=kt,kt<<=1,!(kt&130023424)&&(kt=4194304)):t=1);var n=fl();e=Ka(e,t),e!==null&&(Lt(e,t,n),hl(e,n))}function Vl(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),Bl(e,n)}function Hl(e,t){var n=0;switch(e.tag){case 13:var i=e.stateNode,a=e.memoizedState;a!==null&&(n=a.retryLane);break;case 19:i=e.stateNode;break;default:throw Error(r(314))}i!==null&&i.delete(t),Bl(e,n)}var Ul=function(e,t,n){if(e!==null)if(e.memoizedProps!==t.pendingProps||Bi.current)js=!0;else{if((e.lanes&n)===0&&!(t.flags&128))return js=!1,ec(e,t,n);js=!!(e.flags&131072)}else js=!1,ga&&t.flags&1048576&&da(t,ia,t.index);switch(t.lanes=0,t.tag){case 2:var i=t.type;Qs(e,t),e=t.pendingProps;var a=Hi(t,zi.current);Va(t,n),a=Oo(null,t,i,e,a,n);var o=ko();return t.flags|=1,typeof a==`object`&&a&&typeof a.render==`function`&&a.$$typeof===void 0?(t.tag=1,t.memoizedState=null,t.updateQueue=null,Ui(i)?(o=!0,qi(t)):o=!1,t.memoizedState=a.state!==null&&a.state!==void 0?a.state:null,Ja(t),a.updater=gs,t.stateNode=a,a._reactInternals=t,bs(t,i,e,n),t=Bs(null,t,i,!0,o,n)):(t.tag=0,ga&&o&&fa(t),Ms(null,t,a,n),t=t.child),t;case 16:i=t.elementType;a:{switch(Qs(e,t),e=t.pendingProps,a=i._init,i=a(i._payload),t.type=i,a=t.tag=Jl(i),e=ms(i,e),a){case 0:t=Rs(null,t,i,e,n);break a;case 1:t=zs(null,t,i,e,n);break a;case 11:t=Ns(null,t,i,e,n);break a;case 14:t=Ps(null,t,i,ms(i.type,e),n);break a}throw Error(r(306,i,``))}return t;case 0:return i=t.type,a=t.pendingProps,a=t.elementType===i?a:ms(i,a),Rs(e,t,i,a,n);case 1:return i=t.type,a=t.pendingProps,a=t.elementType===i?a:ms(i,a),zs(e,t,i,a,n);case 3:a:{if(Vs(t),e===null)throw Error(r(387));i=t.pendingProps,o=t.memoizedState,a=o.element,Ya(e,t),eo(t,i,null,n);var s=t.memoizedState;if(i=s.element,o.isDehydrated)if(o={element:i,isDehydrated:!1,cache:s.cache,pendingSuspenseBoundaries:s.pendingSuspenseBoundaries,transitions:s.transitions},t.updateQueue.baseState=o,t.memoizedState=o,t.flags&256){a=xs(Error(r(423)),t),t=Hs(e,t,i,n,a);break a}else if(i!==a){a=xs(Error(r(424)),t),t=Hs(e,t,i,n,a);break a}else for(ha=bi(t.stateNode.containerInfo.firstChild),ma=t,ga=!0,_a=null,n=Na(t,null,i,n),t.child=n;n;)n.flags=n.flags&-3|4096,n=n.sibling;else{if(Ta(),i===a){t=$s(e,t,n);break a}Ms(e,t,i,n)}t=t.child}return t;case 5:return lo(t),e===null&&xa(t),i=t.type,a=t.pendingProps,o=e===null?null:e.memoizedProps,s=a.children,pi(i,a)?s=null:o!==null&&pi(i,o)&&(t.flags|=32),Ls(e,t),Ms(e,t,s,n),t.child;case 6:return e===null&&xa(t),null;case 13:return Gs(e,t,n);case 4:return so(t,t.stateNode.containerInfo),i=t.pendingProps,e===null?t.child=Ma(t,null,i,n):Ms(e,t,i,n),t.child;case 11:return i=t.type,a=t.pendingProps,a=t.elementType===i?a:ms(i,a),Ns(e,t,i,a,n);case 7:return Ms(e,t,t.pendingProps,n),t.child;case 8:return Ms(e,t,t.pendingProps.children,n),t.child;case 12:return Ms(e,t,t.pendingProps.children,n),t.child;case 10:a:{if(i=t.type._context,a=t.pendingProps,o=t.memoizedProps,s=a.value,Li(Pa,i._currentValue),i._currentValue=s,o!==null)if(Q(o.value,s)){if(o.children===a.children&&!Bi.current){t=$s(e,t,n);break a}}else for(o=t.child,o!==null&&(o.return=t);o!==null;){var c=o.dependencies;if(c!==null){s=o.child;for(var l=c.firstContext;l!==null;){if(l.context===i){if(o.tag===1){l=Xa(-1,n&-n),l.tag=2;var u=o.updateQueue;if(u!==null){u=u.shared;var d=u.pending;d===null?l.next=l:(l.next=d.next,d.next=l),u.pending=l}}o.lanes|=n,l=o.alternate,l!==null&&(l.lanes|=n),Ba(o.return,n,t),c.lanes|=n;break}l=l.next}}else if(o.tag===10)s=o.type===t.type?null:o.child;else if(o.tag===18){if(s=o.return,s===null)throw Error(r(341));s.lanes|=n,c=s.alternate,c!==null&&(c.lanes|=n),Ba(s,n,t),s=o.sibling}else s=o.child;if(s!==null)s.return=o;else for(s=o;s!==null;){if(s===t){s=null;break}if(o=s.sibling,o!==null){o.return=s.return,s=o;break}s=s.return}o=s}Ms(e,t,a.children,n),t=t.child}return t;case 9:return a=t.type,i=t.pendingProps.children,Va(t,n),a=Ha(a),i=i(a),t.flags|=1,Ms(e,t,i,n),t.child;case 14:return i=t.type,a=ms(i,t.pendingProps),a=ms(i.type,a),Ps(e,t,i,a,n);case 15:return Fs(e,t,t.type,t.pendingProps,n);case 17:return i=t.type,a=t.pendingProps,a=t.elementType===i?a:ms(i,a),Qs(e,t),t.tag=1,Ui(i)?(e=!0,qi(t)):e=!1,Va(t,n),vs(t,i,a),bs(t,i,a,n),Bs(null,t,i,!0,e,n);case 19:return Zs(e,t,n);case 22:return Is(e,t,n)}throw Error(r(156,t.tag))};function Wl(e,t){return dt(e,t)}function Gl(e,t,n,r){this.tag=e,this.key=n,this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null,this.index=0,this.ref=null,this.pendingProps=t,this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null,this.mode=r,this.subtreeFlags=this.flags=0,this.deletions=null,this.childLanes=this.lanes=0,this.alternate=null}function Kl(e,t,n,r){return new Gl(e,t,n,r)}function ql(e){return e=e.prototype,!(!e||!e.isReactComponent)}function Jl(e){if(typeof e==`function`)return+!!ql(e);if(e!=null){if(e=e.$$typeof,e===j)return 11;if(e===P)return 14}return 2}function Yl(e,t){var n=e.alternate;return n===null?(n=Kl(e.tag,t,e.key,e.mode),n.elementType=e.elementType,n.type=e.type,n.stateNode=e.stateNode,n.alternate=e,e.alternate=n):(n.pendingProps=t,n.type=e.type,n.flags=0,n.subtreeFlags=0,n.deletions=null),n.flags=e.flags&14680064,n.childLanes=e.childLanes,n.lanes=e.lanes,n.child=e.child,n.memoizedProps=e.memoizedProps,n.memoizedState=e.memoizedState,n.updateQueue=e.updateQueue,t=e.dependencies,n.dependencies=t===null?null:{lanes:t.lanes,firstContext:t.firstContext},n.sibling=e.sibling,n.index=e.index,n.ref=e.ref,n}function Xl(e,t,n,i,a,o){var s=2;if(i=e,typeof e==`function`)ql(e)&&(s=1);else if(typeof e==`string`)s=5;else a:switch(e){case E:return Zl(n.children,a,o,t);case D:s=8,a|=8;break;case O:return e=Kl(12,n,t,a|2),e.elementType=O,e.lanes=o,e;case M:return e=Kl(13,n,t,a),e.elementType=M,e.lanes=o,e;case N:return e=Kl(19,n,t,a),e.elementType=N,e.lanes=o,e;case te:return Ql(n,a,o,t);default:if(typeof e==`object`&&e)switch(e.$$typeof){case k:s=10;break a;case A:s=9;break a;case j:s=11;break a;case P:s=14;break a;case ee:s=16,i=null;break a}throw Error(r(130,e==null?e:typeof e,``))}return t=Kl(s,n,t,a),t.elementType=e,t.type=i,t.lanes=o,t}function Zl(e,t,n,r){return e=Kl(7,e,r,t),e.lanes=n,e}function Ql(e,t,n,r){return e=Kl(22,e,r,t),e.elementType=te,e.lanes=n,e.stateNode={isHidden:!1},e}function $l(e,t,n){return e=Kl(6,e,null,t),e.lanes=n,e}function eu(e,t,n){return t=Kl(4,e.children===null?[]:e.children,e.key,t),t.lanes=n,t.stateNode={containerInfo:e.containerInfo,pendingChildren:null,implementation:e.implementation},t}function tu(e,t,n,r,i){this.tag=t,this.containerInfo=e,this.finishedWork=this.pingCache=this.current=this.pendingChildren=null,this.timeoutHandle=-1,this.callbackNode=this.pendingContext=this.context=null,this.callbackPriority=0,this.eventTimes=It(0),this.expirationTimes=It(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=It(0),this.identifierPrefix=r,this.onRecoverableError=i,this.mutableSourceEagerHydrationData=null}function nu(e,t,n,r,i,a,o,s,c){return e=new tu(e,t,n,s,c),t===1?(t=1,!0===a&&(t|=8)):t=0,a=Kl(3,null,null,t),e.current=a,a.stateNode=e,a.memoizedState={element:r,isDehydrated:n,cache:null,transitions:null,pendingSuspenseBoundaries:null},Ja(a),e}function ru(e,t,n){var r=3{function n(){if(!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__>`u`||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!=`function`))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(n)}catch(e){console.error(e)}}n(),t.exports=p()})),h=o((e=>{var t=m();e.createRoot=t.createRoot,e.hydrateRoot=t.hydrateRoot})),g=c(u()),_=c(h(),1),v=class extends Error{status;constructor(e,t){super(t),this.status=e}},y=`telesrv_admin_csrf`,b=`X-CSRF-Token`,x=``;function S(e){x=(e??``).trim()}function C(){if(typeof document>`u`)return``;for(let e of document.cookie.split(`;`)){let t=e.trim(),n=t.indexOf(`=`);if(!(n<=0||t.slice(0,n)!==y))try{return decodeURIComponent(t.slice(n+1))}catch{return t.slice(n+1)}}return``}function w(){return C()||x}function T(e){let t=(e??`GET`).toUpperCase();return t!==`GET`&&t!==`HEAD`&&t!==`OPTIONS`}function E(e){if(!e)return{};if(e instanceof Headers){let t={};return e.forEach((e,n)=>{t[n]=e}),t}return Array.isArray(e)?Object.fromEntries(e):{...e}}async function D(e,t={}){let n=typeof FormData<`u`&&t.body instanceof FormData?{}:{"Content-Type":`application/json`};if(Object.assign(n,E(t.headers)),T(t.method)){let e=w();e&&(n[b]=e)}let r=await fetch(e,{credentials:`same-origin`,...t,headers:n}),i=await r.text(),a=i?JSON.parse(i):null;if(!r.ok){let e=a?.error||a?.Error||a?.message||r.statusText;throw new v(r.status,e)}return a}function O(e){return e instanceof Error?e.message:String(e)}var k={session:()=>D(`/api/session`),publicBranding:()=>D(`/api/public/branding`),publicIconURL:()=>`/api/public/icon?t=${Date.now()}`,login:async(e,t=``)=>{let n=await D(`/api/login`,{method:`POST`,body:JSON.stringify({username:t,secret:e})});return S(n.csrf_token),n},logout:()=>D(`/api/logout`,{method:`POST`,body:`{}`}),adminUsers:()=>D(`/api/admin-users`),accounts:e=>D(`/api/accounts?${e.toString()}`),accountStats:()=>D(`/api/accounts/stats`),sharedDeviceGroups:e=>D(`/api/accounts/shared-devices?${e.toString()}`),account:e=>D(`/api/accounts/${e}`),channels:e=>D(`/api/channels?${e.toString()}`),channel:e=>D(`/api/channels/${e}`),bots:e=>D(`/api/bots?${e.toString()}`),broadcasts:e=>D(`/api/broadcasts?${e.toString()}`),bot:e=>D(`/api/bots/${e}`),collectibleUsernames:e=>D(`/api/collectible-usernames?${e.toString()}`),collectibleUsername:e=>D(`/api/collectible-usernames/${encodeURIComponent(e)}`),dashboard:()=>D(`/api/dashboard`),storageStats:()=>D(`/api/storage/stats`),storageAccounts:e=>D(`/api/storage/accounts?${e.toString()}`),verificationApplications:e=>D(`/api/verification/applications?${e.toString()}`),verificationApplication:e=>D(`/api/verification/applications/${encodeURIComponent(e)}`),verificationCounts:()=>D(`/api/verification/counts`),botVerifiers:e=>D(`/api/botverification/verifiers?${e.toString()}`),verificationIcons:e=>D(`/api/botverification/icons?${e.toString()}`),customVerifications:e=>D(`/api/botverification/marks?${e.toString()}`),customVerificationRequests:e=>D(`/api/botverification/requests?${e.toString()}`),customVerificationRequest:e=>D(`/api/botverification/requests/${encodeURIComponent(e)}`),botVerificationCounts:()=>D(`/api/botverification/counts`),emoji:e=>D(`/api/emoji?${e.toString()}`),emojiAnimation:e=>D(`/api/emoji/${encodeURIComponent(e)}/animation`),messages:e=>D(`/api/messages?${e.toString()}`),message:(e,t)=>D(`/api/messages/detail?${new URLSearchParams({owner_user_id:String(e),msg_id:String(t)}).toString()}`),groupMessages:e=>D(`/api/messages/groups?${e.toString()}`),groupMessage:(e,t)=>D(`/api/messages/groups/detail?${new URLSearchParams({channel_id:String(e),msg_id:String(t)}).toString()}`),moderationCases:e=>D(`/api/moderation/cases?${e.toString()}`),moderationCase:e=>D(`/api/moderation/cases/${e}`),moderationReport:e=>D(`/api/moderation/reports/${e}`),claimModerationCase:(e,t)=>D(`/api/moderation/cases/${e}/claim`,{method:`POST`,body:JSON.stringify({expected_version:t})}),decideModerationCase:(e,t)=>D(`/api/moderation/cases/${e}/decide`,{method:`POST`,body:JSON.stringify(t)}),reviewModerationAppeal:(e,t,n)=>D(`/api/moderation/cases/${e}/appeals/${t}/review`,{method:`POST`,body:JSON.stringify(n)}),stickerSets:e=>D(`/api/stickers?kind=${encodeURIComponent(e)}`),stickerSetDocuments:e=>D(`/api/stickers/${encodeURIComponent(e)}/documents`),stickerDocumentAnimationURL:e=>`/api/stickers/documents/${encodeURIComponent(e)}/animation`,gifCatalogDocumentPreviewURL:e=>`/api/gif-catalog/documents/${encodeURIComponent(e)}/preview`,createStickerSet:e=>D(`/api/actions/create-sticker-set`,{method:`POST`,body:e}),setAccountAvatar:e=>D(`/api/actions/set-account-avatar`,{method:`POST`,body:e}),setAccountAvatarVideo:e=>D(`/api/actions/set-account-avatar-video`,{method:`POST`,body:e}),setChannelAvatar:e=>D(`/api/actions/set-channel-avatar`,{method:`POST`,body:e}),addStickerToSet:e=>D(`/api/actions/add-sticker-to-set`,{method:`POST`,body:e}),gifCatalog:()=>D(`/api/gif-catalog`),createGifCatalogEntry:e=>D(`/api/actions/create-gif-catalog-entry`,{method:`POST`,body:e}),serverIdentity:()=>D(`/api/server/identity`),addServerLink:()=>D(`/api/server/add-server-link`),uploadServerIcon:e=>D(`/api/actions/upload-server-icon`,{method:`POST`,body:e}),serverIconURL:()=>`/api/server/icon?t=${Date.now()}`,serverEnv:()=>D(`/api/server/env`),serverStatus:()=>D(`/api/server/status`),dockerStatus:()=>D(`/api/server/docker-status`),checkServerUpdates:()=>D(`/api/server/check-updates`),action:(e,t)=>D(e,{method:`POST`,body:JSON.stringify(t)})},A=e=>e.replace(/([a-z0-9])([A-Z])/g,`$1-$2`).toLowerCase(),j=(...e)=>e.filter((e,t,n)=>!!e&&e.trim()!==``&&n.indexOf(e)===t).join(` `).trim(),M={xmlns:`http://www.w3.org/2000/svg`,width:24,height:24,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:2,strokeLinecap:`round`,strokeLinejoin:`round`},N=(0,g.forwardRef)(({color:e=`currentColor`,size:t=24,strokeWidth:n=2,absoluteStrokeWidth:r,className:i=``,children:a,iconNode:o,...s},c)=>(0,g.createElement)(`svg`,{ref:c,...M,width:t,height:t,stroke:e,strokeWidth:r?Number(n)*24/Number(t):n,className:j(`lucide`,i),...s},[...o.map(([e,t])=>(0,g.createElement)(e,t)),...Array.isArray(a)?a:[a]])),P=(e,t)=>{let n=(0,g.forwardRef)(({className:n,...r},i)=>(0,g.createElement)(N,{ref:i,iconNode:t,className:j(`lucide-${A(e)}`,n),...r}));return n.displayName=`${e}`,n},ee=P(`BadgeCheck`,[[`path`,{d:`M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z`,key:`3c2336`}],[`path`,{d:`m9 12 2 2 4-4`,key:`dzmm74`}]]),te=P(`CircleAlert`,[[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}],[`line`,{x1:`12`,x2:`12`,y1:`8`,y2:`12`,key:`1pkeuh`}],[`line`,{x1:`12`,x2:`12.01`,y1:`16`,y2:`16`,key:`4dfq90`}]]),F=P(`CircleCheck`,[[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}],[`path`,{d:`m9 12 2 2 4-4`,key:`dzmm74`}]]),ne=P(`CircleX`,[[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}],[`path`,{d:`m15 9-6 6`,key:`1uzhvr`}],[`path`,{d:`m9 9 6 6`,key:`z0biqf`}]]),re=P(`Layers`,[[`path`,{d:`M12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83z`,key:`zw3jo`}],[`path`,{d:`M2 12a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 12`,key:`1wduqc`}],[`path`,{d:`M2 17a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 17`,key:`kqbvx6`}]]),I=P(`LoaderCircle`,[[`path`,{d:`M21 12a9 9 0 1 1-6.219-8.56`,key:`13zald`}]]),ie=P(`PanelLeftClose`,[[`rect`,{width:`18`,height:`18`,x:`3`,y:`3`,rx:`2`,key:`afitv7`}],[`path`,{d:`M9 3v18`,key:`fh3hqa`}],[`path`,{d:`m16 15-3-3 3-3`,key:`14y99z`}]]),ae=P(`PanelLeftOpen`,[[`rect`,{width:`18`,height:`18`,x:`3`,y:`3`,rx:`2`,key:`afitv7`}],[`path`,{d:`M9 3v18`,key:`fh3hqa`}],[`path`,{d:`m14 9 3 3-3 3`,key:`8010ee`}]]),oe=P(`ShieldX`,[[`path`,{d:`M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z`,key:`oel41y`}],[`path`,{d:`m14.5 9.5-5 5`,key:`17q4r4`}],[`path`,{d:`m9.5 9.5 5 5`,key:`18nt4w`}]]),se=P(`Sparkles`,[[`path`,{d:`M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z`,key:`4pj2yx`}],[`path`,{d:`M20 3v4`,key:`1olli1`}],[`path`,{d:`M22 5h-4`,key:`1gvqau`}],[`path`,{d:`M4 17v2`,key:`vumght`}],[`path`,{d:`M5 18H3`,key:`zchphs`}]]),ce=P(`TriangleAlert`,[[`path`,{d:`m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3`,key:`wmoenq`}],[`path`,{d:`M12 9v4`,key:`juzpu7`}],[`path`,{d:`M12 17h.01`,key:`p32p05`}]]),le=P(`UserRound`,[[`circle`,{cx:`12`,cy:`8`,r:`5`,key:`1hypcn`}],[`path`,{d:`M20 21a8 8 0 0 0-16 0`,key:`rfgkzh`}]]),ue=P(`UsersRound`,[[`path`,{d:`M18 21a8 8 0 0 0-16 0`,key:`3ypg7q`}],[`circle`,{cx:`10`,cy:`8`,r:`5`,key:`o932ke`}],[`path`,{d:`M22 20c0-3.37-2-6.5-4-8a5 5 0 0 0-.45-8.3`,key:`10s06x`}]]),de=P(`Activity`,[[`path`,{d:`M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36a.25.25 0 0 1-.48 0L9.24 2.18a.25.25 0 0 0-.48 0l-2.35 8.36A2 2 0 0 1 4.49 12H2`,key:`169zse`}]]),fe=P(`ArrowDown`,[[`path`,{d:`M12 5v14`,key:`s699le`}],[`path`,{d:`m19 12-7 7-7-7`,key:`1idqje`}]]),L=P(`ArrowLeftRight`,[[`path`,{d:`M8 3 4 7l4 4`,key:`9rb6wj`}],[`path`,{d:`M4 7h16`,key:`6tx8e3`}],[`path`,{d:`m16 21 4-4-4-4`,key:`siv7j2`}],[`path`,{d:`M20 17H4`,key:`h6l3hr`}]]),pe=P(`ArrowLeft`,[[`path`,{d:`m12 19-7-7 7-7`,key:`1l729n`}],[`path`,{d:`M19 12H5`,key:`x3x0zl`}]]),me=P(`ArrowRight`,[[`path`,{d:`M5 12h14`,key:`1ays0h`}],[`path`,{d:`m12 5 7 7-7 7`,key:`xquz4c`}]]),he=P(`ArrowUpDown`,[[`path`,{d:`m21 16-4 4-4-4`,key:`f6ql7i`}],[`path`,{d:`M17 20V4`,key:`1ejh1v`}],[`path`,{d:`m3 8 4-4 4 4`,key:`11wl7u`}],[`path`,{d:`M7 4v16`,key:`1glfcx`}]]),ge=P(`ArrowUp`,[[`path`,{d:`m5 12 7-7 7 7`,key:`hav0vg`}],[`path`,{d:`M12 19V5`,key:`x0mq9r`}]]),_e=P(`AtSign`,[[`circle`,{cx:`12`,cy:`12`,r:`4`,key:`4exip2`}],[`path`,{d:`M16 8v5a3 3 0 0 0 6 0v-1a10 10 0 1 0-4 8`,key:`7n84p3`}]]),ve=P(`Ban`,[[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}],[`path`,{d:`m4.9 4.9 14.2 14.2`,key:`1m5liu`}]]),ye=P(`Bot`,[[`path`,{d:`M12 8V4H8`,key:`hb8ula`}],[`rect`,{width:`16`,height:`12`,x:`4`,y:`8`,rx:`2`,key:`enze0r`}],[`path`,{d:`M2 14h2`,key:`vft8re`}],[`path`,{d:`M20 14h2`,key:`4cs60a`}],[`path`,{d:`M15 13v2`,key:`1xurst`}],[`path`,{d:`M9 13v2`,key:`rq6x2g`}]]),be=P(`Building2`,[[`path`,{d:`M6 22V4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v18Z`,key:`1b4qmf`}],[`path`,{d:`M6 12H4a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h2`,key:`i71pzd`}],[`path`,{d:`M18 9h2a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2h-2`,key:`10jefs`}],[`path`,{d:`M10 6h4`,key:`1itunk`}],[`path`,{d:`M10 10h4`,key:`tcdvrf`}],[`path`,{d:`M10 14h4`,key:`kelpxr`}],[`path`,{d:`M10 18h4`,key:`1ulq68`}]]),xe=P(`Cable`,[[`path`,{d:`M17 21v-2a1 1 0 0 1-1-1v-1a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v1a1 1 0 0 1-1 1`,key:`10bnsj`}],[`path`,{d:`M19 15V6.5a1 1 0 0 0-7 0v11a1 1 0 0 1-7 0V9`,key:`1eqmu1`}],[`path`,{d:`M21 21v-2h-4`,key:`14zm7j`}],[`path`,{d:`M3 5h4V3`,key:`z442eg`}],[`path`,{d:`M7 5a1 1 0 0 1 1 1v1a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6a1 1 0 0 1 1-1V3`,key:`ebdjd7`}]]),Se=P(`Check`,[[`path`,{d:`M20 6 9 17l-5-5`,key:`1gmf2c`}]]),Ce=P(`ChevronDown`,[[`path`,{d:`m6 9 6 6 6-6`,key:`qrunsl`}]]),R=P(`ChevronLeft`,[[`path`,{d:`m15 18-6-6 6-6`,key:`1wnfg3`}]]),z=P(`ChevronRight`,[[`path`,{d:`m9 18 6-6-6-6`,key:`mthhwq`}]]),we=P(`CircleOff`,[[`path`,{d:`m2 2 20 20`,key:`1ooewy`}],[`path`,{d:`M8.35 2.69A10 10 0 0 1 21.3 15.65`,key:`1pfsoa`}],[`path`,{d:`M19.08 19.08A10 10 0 1 1 4.92 4.92`,key:`1ablyi`}]]),Te=P(`Contact`,[[`path`,{d:`M16 2v2`,key:`scm5qe`}],[`path`,{d:`M7 22v-2a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v2`,key:`1waht3`}],[`path`,{d:`M8 2v2`,key:`pbkmx`}],[`circle`,{cx:`12`,cy:`11`,r:`3`,key:`itu57m`}],[`rect`,{x:`3`,y:`4`,width:`18`,height:`18`,rx:`2`,key:`12vinp`}]]),Ee=P(`Copy`,[[`rect`,{width:`14`,height:`14`,x:`8`,y:`8`,rx:`2`,ry:`2`,key:`17jyea`}],[`path`,{d:`M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2`,key:`zix9uf`}]]),De=P(`Cpu`,[[`rect`,{width:`16`,height:`16`,x:`4`,y:`4`,rx:`2`,key:`14l7u7`}],[`rect`,{width:`6`,height:`6`,x:`9`,y:`9`,rx:`1`,key:`5aljv4`}],[`path`,{d:`M15 2v2`,key:`13l42r`}],[`path`,{d:`M15 20v2`,key:`15mkzm`}],[`path`,{d:`M2 15h2`,key:`1gxd5l`}],[`path`,{d:`M2 9h2`,key:`1bbxkp`}],[`path`,{d:`M20 15h2`,key:`19e6y8`}],[`path`,{d:`M20 9h2`,key:`19tzq7`}],[`path`,{d:`M9 2v2`,key:`165o2o`}],[`path`,{d:`M9 20v2`,key:`i2bqo8`}]]),Oe=P(`Database`,[[`ellipse`,{cx:`12`,cy:`5`,rx:`9`,ry:`3`,key:`msslwz`}],[`path`,{d:`M3 5V19A9 3 0 0 0 21 19V5`,key:`1wlel7`}],[`path`,{d:`M3 12A9 3 0 0 0 21 12`,key:`mv7ke4`}]]),ke=P(`Dice5`,[[`rect`,{width:`18`,height:`18`,x:`3`,y:`3`,rx:`2`,ry:`2`,key:`1m3agn`}],[`path`,{d:`M16 8h.01`,key:`cr5u4v`}],[`path`,{d:`M8 8h.01`,key:`1e4136`}],[`path`,{d:`M8 16h.01`,key:`18s6g9`}],[`path`,{d:`M16 16h.01`,key:`1f9h7w`}],[`path`,{d:`M12 12h.01`,key:`1mp3jc`}]]),Ae=P(`Download`,[[`path`,{d:`M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4`,key:`ih7n3h`}],[`polyline`,{points:`7 10 12 15 17 10`,key:`2ggqvy`}],[`line`,{x1:`12`,x2:`12`,y1:`15`,y2:`3`,key:`1vk2je`}]]),je=P(`ExternalLink`,[[`path`,{d:`M15 3h6v6`,key:`1q9fwt`}],[`path`,{d:`M10 14 21 3`,key:`gplh6r`}],[`path`,{d:`M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6`,key:`a6xqqp`}]]),Me=P(`EyeOff`,[[`path`,{d:`M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49`,key:`ct8e1f`}],[`path`,{d:`M14.084 14.158a3 3 0 0 1-4.242-4.242`,key:`151rxh`}],[`path`,{d:`M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143`,key:`13bj9a`}],[`path`,{d:`m2 2 20 20`,key:`1ooewy`}]]),Ne=P(`Eye`,[[`path`,{d:`M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0`,key:`1nclc0`}],[`circle`,{cx:`12`,cy:`12`,r:`3`,key:`1v7zrd`}]]),Pe=P(`FileJson`,[[`path`,{d:`M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z`,key:`1rqfz7`}],[`path`,{d:`M14 2v4a2 2 0 0 0 2 2h4`,key:`tnqrlb`}],[`path`,{d:`M10 12a1 1 0 0 0-1 1v1a1 1 0 0 1-1 1 1 1 0 0 1 1 1v1a1 1 0 0 0 1 1`,key:`1oajmo`}],[`path`,{d:`M14 18a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1 1 1 0 0 1-1-1v-1a1 1 0 0 0-1-1`,key:`mpwhp6`}]]),Fe=P(`FileText`,[[`path`,{d:`M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z`,key:`1rqfz7`}],[`path`,{d:`M14 2v4a2 2 0 0 0 2 2h4`,key:`tnqrlb`}],[`path`,{d:`M10 9H8`,key:`b1mrlr`}],[`path`,{d:`M16 13H8`,key:`t4e002`}],[`path`,{d:`M16 17H8`,key:`z1uh3a`}]]),Ie=P(`Film`,[[`rect`,{width:`18`,height:`18`,x:`3`,y:`3`,rx:`2`,key:`afitv7`}],[`path`,{d:`M7 3v18`,key:`bbkbws`}],[`path`,{d:`M3 7.5h4`,key:`zfgn84`}],[`path`,{d:`M3 12h18`,key:`1i2n21`}],[`path`,{d:`M3 16.5h4`,key:`1230mu`}],[`path`,{d:`M17 3v18`,key:`in4fa5`}],[`path`,{d:`M17 7.5h4`,key:`myr1c1`}],[`path`,{d:`M17 16.5h4`,key:`go4c1d`}]]),Le=P(`Flag`,[[`path`,{d:`M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z`,key:`i9b6wo`}],[`line`,{x1:`4`,x2:`4`,y1:`22`,y2:`15`,key:`1cm3nv`}]]),Re=P(`Flame`,[[`path`,{d:`M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z`,key:`96xj49`}]]),ze=P(`Gift`,[[`rect`,{x:`3`,y:`8`,width:`18`,height:`4`,rx:`1`,key:`bkv52`}],[`path`,{d:`M12 8v13`,key:`1c76mn`}],[`path`,{d:`M19 12v7a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2v-7`,key:`6wjy6b`}],[`path`,{d:`M7.5 8a2.5 2.5 0 0 1 0-5A4.8 8 0 0 1 12 8a4.8 8 0 0 1 4.5-5 2.5 2.5 0 0 1 0 5`,key:`1ihvrl`}]]),Be=P(`Handshake`,[[`path`,{d:`m11 17 2 2a1 1 0 1 0 3-3`,key:`efffak`}],[`path`,{d:`m14 14 2.5 2.5a1 1 0 1 0 3-3l-3.88-3.88a3 3 0 0 0-4.24 0l-.88.88a1 1 0 1 1-3-3l2.81-2.81a5.79 5.79 0 0 1 7.06-.87l.47.28a2 2 0 0 0 1.42.25L21 4`,key:`9pr0kb`}],[`path`,{d:`m21 3 1 11h-2`,key:`1tisrp`}],[`path`,{d:`M3 3 2 14l6.5 6.5a1 1 0 1 0 3-3`,key:`1uvwmv`}],[`path`,{d:`M3 4h8`,key:`1ep09j`}]]),Ve=P(`HardDrive`,[[`line`,{x1:`22`,x2:`2`,y1:`12`,y2:`12`,key:`1y58io`}],[`path`,{d:`M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z`,key:`oot6mr`}],[`line`,{x1:`6`,x2:`6.01`,y1:`16`,y2:`16`,key:`sgf278`}],[`line`,{x1:`10`,x2:`10.01`,y1:`16`,y2:`16`,key:`1l4acy`}]]),He=P(`History`,[[`path`,{d:`M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8`,key:`1357e3`}],[`path`,{d:`M3 3v5h5`,key:`1xhq8a`}],[`path`,{d:`M12 7v5l4 2`,key:`1fdv2h`}]]),Ue=P(`ImageOff`,[[`line`,{x1:`2`,x2:`22`,y1:`2`,y2:`22`,key:`a6p6uj`}],[`path`,{d:`M10.41 10.41a2 2 0 1 1-2.83-2.83`,key:`1bzlo9`}],[`line`,{x1:`13.5`,x2:`6`,y1:`13.5`,y2:`21`,key:`1q0aeu`}],[`line`,{x1:`18`,x2:`21`,y1:`12`,y2:`15`,key:`5mozeu`}],[`path`,{d:`M3.59 3.59A1.99 1.99 0 0 0 3 5v14a2 2 0 0 0 2 2h14c.55 0 1.052-.22 1.41-.59`,key:`mmje98`}],[`path`,{d:`M21 15V5a2 2 0 0 0-2-2H9`,key:`43el77`}]]),We=P(`ImagePlus`,[[`path`,{d:`M16 5h6`,key:`1vod17`}],[`path`,{d:`M19 2v6`,key:`4bpg5p`}],[`path`,{d:`M21 11.5V19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7.5`,key:`1ue2ih`}],[`path`,{d:`m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21`,key:`1xmnt7`}],[`circle`,{cx:`9`,cy:`9`,r:`2`,key:`af1f0g`}]]),B=P(`Image`,[[`rect`,{width:`18`,height:`18`,x:`3`,y:`3`,rx:`2`,ry:`2`,key:`1m3agn`}],[`circle`,{cx:`9`,cy:`9`,r:`2`,key:`af1f0g`}],[`path`,{d:`m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21`,key:`1xmnt7`}]]),Ge=P(`KeyRound`,[[`path`,{d:`M2.586 17.414A2 2 0 0 0 2 18.828V21a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h1a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h.172a2 2 0 0 0 1.414-.586l.814-.814a6.5 6.5 0 1 0-4-4z`,key:`1s6t7t`}],[`circle`,{cx:`16.5`,cy:`7.5`,r:`.5`,fill:`currentColor`,key:`w0ekpg`}]]),Ke=P(`LayoutDashboard`,[[`rect`,{width:`7`,height:`9`,x:`3`,y:`3`,rx:`1`,key:`10lvy0`}],[`rect`,{width:`7`,height:`5`,x:`14`,y:`3`,rx:`1`,key:`16une8`}],[`rect`,{width:`7`,height:`9`,x:`14`,y:`12`,rx:`1`,key:`1hutg5`}],[`rect`,{width:`7`,height:`5`,x:`3`,y:`16`,rx:`1`,key:`ldoo1y`}]]),qe=P(`LifeBuoy`,[[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}],[`path`,{d:`m4.93 4.93 4.24 4.24`,key:`1ymg45`}],[`path`,{d:`m14.83 9.17 4.24-4.24`,key:`1cb5xl`}],[`path`,{d:`m14.83 14.83 4.24 4.24`,key:`q42g0n`}],[`path`,{d:`m9.17 14.83-4.24 4.24`,key:`bqpfvv`}],[`circle`,{cx:`12`,cy:`12`,r:`4`,key:`4exip2`}]]),Je=P(`Link2`,[[`path`,{d:`M9 17H7A5 5 0 0 1 7 7h2`,key:`8i5ue5`}],[`path`,{d:`M15 7h2a5 5 0 1 1 0 10h-2`,key:`1b9ql8`}],[`line`,{x1:`8`,x2:`16`,y1:`12`,y2:`12`,key:`1jonct`}]]),Ye=P(`ListChecks`,[[`path`,{d:`m3 17 2 2 4-4`,key:`1jhpwq`}],[`path`,{d:`m3 7 2 2 4-4`,key:`1obspn`}],[`path`,{d:`M13 6h8`,key:`15sg57`}],[`path`,{d:`M13 12h8`,key:`h98zly`}],[`path`,{d:`M13 18h8`,key:`oe0vm4`}]]),Xe=P(`Lock`,[[`rect`,{width:`18`,height:`11`,x:`3`,y:`11`,rx:`2`,ry:`2`,key:`1w4ew1`}],[`path`,{d:`M7 11V7a5 5 0 0 1 10 0v4`,key:`fwvmzm`}]]),Ze=P(`LogOut`,[[`path`,{d:`M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4`,key:`1uf3rs`}],[`polyline`,{points:`16 17 21 12 16 7`,key:`1gabdz`}],[`line`,{x1:`21`,x2:`9`,y1:`12`,y2:`12`,key:`1uyos4`}]]),Qe=P(`Mail`,[[`rect`,{width:`20`,height:`16`,x:`2`,y:`4`,rx:`2`,key:`18n3k1`}],[`path`,{d:`m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7`,key:`1ocrg3`}]]),$e=P(`MapPin`,[[`path`,{d:`M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0`,key:`1r0f0z`}],[`circle`,{cx:`12`,cy:`10`,r:`3`,key:`ilqhr7`}]]),et=P(`Megaphone`,[[`path`,{d:`m3 11 18-5v12L3 14v-3z`,key:`n962bs`}],[`path`,{d:`M11.6 16.8a3 3 0 1 1-5.8-1.6`,key:`1yl0tm`}]]),tt=P(`MemoryStick`,[[`path`,{d:`M6 19v-3`,key:`1nvgqn`}],[`path`,{d:`M10 19v-3`,key:`iu8nkm`}],[`path`,{d:`M14 19v-3`,key:`kcehxu`}],[`path`,{d:`M18 19v-3`,key:`1vh91z`}],[`path`,{d:`M8 11V9`,key:`63erz4`}],[`path`,{d:`M16 11V9`,key:`fru6f3`}],[`path`,{d:`M12 11V9`,key:`ha00sb`}],[`path`,{d:`M2 15h20`,key:`16ne18`}],[`path`,{d:`M2 7a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v1.1a2 2 0 0 0 0 3.837V17a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-5.1a2 2 0 0 0 0-3.837Z`,key:`lhddv3`}]]),nt=P(`MessageSquareText`,[[`path`,{d:`M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z`,key:`1lielz`}],[`path`,{d:`M13 8H7`,key:`14i4kc`}],[`path`,{d:`M17 12H7`,key:`16if0g`}]]),rt=P(`MonitorSmartphone`,[[`path`,{d:`M18 8V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h8`,key:`10dyio`}],[`path`,{d:`M10 19v-3.96 3.15`,key:`1irgej`}],[`path`,{d:`M7 19h5`,key:`qswx4l`}],[`rect`,{width:`6`,height:`10`,x:`16`,y:`12`,rx:`2`,key:`1egngj`}]]),it=P(`Moon`,[[`path`,{d:`M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z`,key:`a7tn18`}]]),at=P(`Palette`,[[`circle`,{cx:`13.5`,cy:`6.5`,r:`.5`,fill:`currentColor`,key:`1okk4w`}],[`circle`,{cx:`17.5`,cy:`10.5`,r:`.5`,fill:`currentColor`,key:`f64h9f`}],[`circle`,{cx:`8.5`,cy:`7.5`,r:`.5`,fill:`currentColor`,key:`fotxhn`}],[`circle`,{cx:`6.5`,cy:`12.5`,r:`.5`,fill:`currentColor`,key:`qy21gx`}],[`path`,{d:`M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10c.926 0 1.648-.746 1.648-1.688 0-.437-.18-.835-.437-1.125-.29-.289-.438-.652-.438-1.125a1.64 1.64 0 0 1 1.668-1.668h1.996c3.051 0 5.555-2.503 5.555-5.554C21.965 6.012 17.461 2 12 2z`,key:`12rzf8`}]]),ot=P(`Phone`,[[`path`,{d:`M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z`,key:`foiqr5`}]]),st=P(`Play`,[[`polygon`,{points:`6 3 20 12 6 21 6 3`,key:`1oa8hb`}]]),ct=P(`Plus`,[[`path`,{d:`M5 12h14`,key:`1ays0h`}],[`path`,{d:`M12 5v14`,key:`s699le`}]]),lt=P(`PowerOff`,[[`path`,{d:`M18.36 6.64A9 9 0 0 1 20.77 15`,key:`dxknvb`}],[`path`,{d:`M6.16 6.16a9 9 0 1 0 12.68 12.68`,key:`1x7qb5`}],[`path`,{d:`M12 2v4`,key:`3427ic`}],[`path`,{d:`m2 2 20 20`,key:`1ooewy`}]]),ut=P(`Power`,[[`path`,{d:`M12 2v10`,key:`mnfbl`}],[`path`,{d:`M18.4 6.6a9 9 0 1 1-12.77.04`,key:`obofu9`}]]),dt=P(`Radio`,[[`path`,{d:`M4.9 19.1C1 15.2 1 8.8 4.9 4.9`,key:`1vaf9d`}],[`path`,{d:`M7.8 16.2c-2.3-2.3-2.3-6.1 0-8.5`,key:`u1ii0m`}],[`circle`,{cx:`12`,cy:`12`,r:`2`,key:`1c9p78`}],[`path`,{d:`M16.2 7.8c2.3 2.3 2.3 6.1 0 8.5`,key:`1j5fej`}],[`path`,{d:`M19.1 4.9C23 8.8 23 15.1 19.1 19`,key:`10b0cb`}]]),ft=P(`RefreshCw`,[[`path`,{d:`M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8`,key:`v9h5vc`}],[`path`,{d:`M21 3v5h-5`,key:`1q7to0`}],[`path`,{d:`M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16`,key:`3uifl3`}],[`path`,{d:`M8 16H3v5`,key:`1cv678`}]]),pt=P(`Rocket`,[[`path`,{d:`M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z`,key:`m3kijz`}],[`path`,{d:`m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z`,key:`1fmvmk`}],[`path`,{d:`M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0`,key:`1f8sc4`}],[`path`,{d:`M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5`,key:`qeys4`}]]),mt=P(`ScrollText`,[[`path`,{d:`M15 12h-5`,key:`r7krc0`}],[`path`,{d:`M15 8h-5`,key:`1khuty`}],[`path`,{d:`M19 17V5a2 2 0 0 0-2-2H4`,key:`zz82l3`}],[`path`,{d:`M8 21h12a2 2 0 0 0 2-2v-1a1 1 0 0 0-1-1H11a1 1 0 0 0-1 1v1a2 2 0 1 1-4 0V5a2 2 0 1 0-4 0v2a1 1 0 0 0 1 1h3`,key:`1ph1d7`}]]),V=P(`Search`,[[`circle`,{cx:`11`,cy:`11`,r:`8`,key:`4ej97u`}],[`path`,{d:`m21 21-4.3-4.3`,key:`1qie3q`}]]),ht=P(`Send`,[[`path`,{d:`M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z`,key:`1ffxy3`}],[`path`,{d:`m21.854 2.147-10.94 10.939`,key:`12cjpa`}]]),gt=P(`Server`,[[`rect`,{width:`20`,height:`8`,x:`2`,y:`2`,rx:`2`,ry:`2`,key:`ngkwjq`}],[`rect`,{width:`20`,height:`8`,x:`2`,y:`14`,rx:`2`,ry:`2`,key:`iecqi9`}],[`line`,{x1:`6`,x2:`6.01`,y1:`6`,y2:`6`,key:`16zg32`}],[`line`,{x1:`6`,x2:`6.01`,y1:`18`,y2:`18`,key:`nzw8ys`}]]),_t=P(`Settings2`,[[`path`,{d:`M20 7h-9`,key:`3s1dr2`}],[`path`,{d:`M14 17H5`,key:`gfn3mx`}],[`circle`,{cx:`17`,cy:`17`,r:`3`,key:`18b49y`}],[`circle`,{cx:`7`,cy:`7`,r:`3`,key:`dfmy0x`}]]),vt=P(`Settings`,[[`path`,{d:`M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z`,key:`1qme2f`}],[`circle`,{cx:`12`,cy:`12`,r:`3`,key:`1v7zrd`}]]),yt=P(`Share2`,[[`circle`,{cx:`18`,cy:`5`,r:`3`,key:`gq8acd`}],[`circle`,{cx:`6`,cy:`12`,r:`3`,key:`w7nqdw`}],[`circle`,{cx:`18`,cy:`19`,r:`3`,key:`1xt0gg`}],[`line`,{x1:`8.59`,x2:`15.42`,y1:`13.51`,y2:`17.49`,key:`47mynk`}],[`line`,{x1:`15.41`,x2:`8.59`,y1:`6.51`,y2:`10.49`,key:`1n3mei`}]]),bt=P(`ShieldAlert`,[[`path`,{d:`M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z`,key:`oel41y`}],[`path`,{d:`M12 8v4`,key:`1got3b`}],[`path`,{d:`M12 16h.01`,key:`1drbdi`}]]),xt=P(`ShieldCheck`,[[`path`,{d:`M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z`,key:`oel41y`}],[`path`,{d:`m9 12 2 2 4-4`,key:`dzmm74`}]]),St=P(`ShieldOff`,[[`path`,{d:`m2 2 20 20`,key:`1ooewy`}],[`path`,{d:`M5 5a1 1 0 0 0-1 1v7c0 5 3.5 7.5 7.67 8.94a1 1 0 0 0 .67.01c2.35-.82 4.48-1.97 5.9-3.71`,key:`1jlk70`}],[`path`,{d:`M9.309 3.652A12.252 12.252 0 0 0 11.24 2.28a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1v7a9.784 9.784 0 0 1-.08 1.264`,key:`18rp1v`}]]),Ct=P(`Smartphone`,[[`rect`,{width:`14`,height:`20`,x:`5`,y:`2`,rx:`2`,ry:`2`,key:`1yt0o3`}],[`path`,{d:`M12 18h.01`,key:`mhygvu`}]]),wt=P(`Smile`,[[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}],[`path`,{d:`M8 14s1.5 2 4 2 4-2 4-2`,key:`1y1vjs`}],[`line`,{x1:`9`,x2:`9.01`,y1:`9`,y2:`9`,key:`yxxnd0`}],[`line`,{x1:`15`,x2:`15.01`,y1:`9`,y2:`9`,key:`1p4y9e`}]]),Tt=P(`Stamp`,[[`path`,{d:`M5 22h14`,key:`ehvnwv`}],[`path`,{d:`M19.27 13.73A2.5 2.5 0 0 0 17.5 13h-11A2.5 2.5 0 0 0 4 15.5V17a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1v-1.5c0-.66-.26-1.3-.73-1.77Z`,key:`1sy9ra`}],[`path`,{d:`M14 13V8.5C14 7 15 7 15 5a3 3 0 0 0-3-3c-1.66 0-3 1-3 3s1 2 1 3.5V13`,key:`cnxgux`}]]),Et=P(`Sticker`,[[`path`,{d:`M15.5 3H5a2 2 0 0 0-2 2v14c0 1.1.9 2 2 2h14a2 2 0 0 0 2-2V8.5L15.5 3Z`,key:`1wis1t`}],[`path`,{d:`M14 3v4a2 2 0 0 0 2 2h4`,key:`36rjfy`}],[`path`,{d:`M8 13h.01`,key:`1sbv64`}],[`path`,{d:`M16 13h.01`,key:`wip0gl`}],[`path`,{d:`M10 16s.8 1 2 1c1.3 0 2-1 2-1`,key:`1vvgv3`}]]),Dt=P(`Sun`,[[`circle`,{cx:`12`,cy:`12`,r:`4`,key:`4exip2`}],[`path`,{d:`M12 2v2`,key:`tus03m`}],[`path`,{d:`M12 20v2`,key:`1lh1kg`}],[`path`,{d:`m4.93 4.93 1.41 1.41`,key:`149t6j`}],[`path`,{d:`m17.66 17.66 1.41 1.41`,key:`ptbguv`}],[`path`,{d:`M2 12h2`,key:`1t8f8n`}],[`path`,{d:`M20 12h2`,key:`1q8mjw`}],[`path`,{d:`m6.34 17.66-1.41 1.41`,key:`1m8zz5`}],[`path`,{d:`m19.07 4.93-1.41 1.41`,key:`1shlcs`}]]),Ot=P(`Trash2`,[[`path`,{d:`M3 6h18`,key:`d0wm0j`}],[`path`,{d:`M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6`,key:`4alrt4`}],[`path`,{d:`M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2`,key:`v07s0e`}],[`line`,{x1:`10`,x2:`10`,y1:`11`,y2:`17`,key:`1uufr5`}],[`line`,{x1:`14`,x2:`14`,y1:`11`,y2:`17`,key:`xtxkd`}]]),kt=P(`Undo2`,[[`path`,{d:`M9 14 4 9l5-5`,key:`102s5s`}],[`path`,{d:`M4 9h10.5a5.5 5.5 0 0 1 5.5 5.5a5.5 5.5 0 0 1-5.5 5.5H11`,key:`f3b9sd`}]]),At=P(`Upload`,[[`path`,{d:`M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4`,key:`ih7n3h`}],[`polyline`,{points:`17 8 12 3 7 8`,key:`t8dd8p`}],[`line`,{x1:`12`,x2:`12`,y1:`3`,y2:`15`,key:`widbto`}]]),jt=P(`UserCog`,[[`circle`,{cx:`18`,cy:`15`,r:`3`,key:`gjjjvw`}],[`circle`,{cx:`9`,cy:`7`,r:`4`,key:`nufk8`}],[`path`,{d:`M10 15H6a4 4 0 0 0-4 4v2`,key:`1nfge6`}],[`path`,{d:`m21.7 16.4-.9-.3`,key:`12j9ji`}],[`path`,{d:`m15.2 13.9-.9-.3`,key:`1fdjdi`}],[`path`,{d:`m16.6 18.7.3-.9`,key:`heedtr`}],[`path`,{d:`m19.1 12.2.3-.9`,key:`1af3ki`}],[`path`,{d:`m19.6 18.7-.4-1`,key:`1x9vze`}],[`path`,{d:`m16.8 12.3-.4-1`,key:`vqeiwj`}],[`path`,{d:`m14.3 16.6 1-.4`,key:`1qlj63`}],[`path`,{d:`m20.7 13.8 1-.4`,key:`1v5t8k`}]]),Mt=P(`UserPlus`,[[`path`,{d:`M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2`,key:`1yyitq`}],[`circle`,{cx:`9`,cy:`7`,r:`4`,key:`nufk8`}],[`line`,{x1:`19`,x2:`19`,y1:`8`,y2:`14`,key:`1bvyxn`}],[`line`,{x1:`22`,x2:`16`,y1:`11`,y2:`11`,key:`1shjgl`}]]),Nt=P(`User`,[[`path`,{d:`M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2`,key:`975kel`}],[`circle`,{cx:`12`,cy:`7`,r:`4`,key:`17ys0d`}]]),Pt=P(`Users`,[[`path`,{d:`M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2`,key:`1yyitq`}],[`circle`,{cx:`9`,cy:`7`,r:`4`,key:`nufk8`}],[`path`,{d:`M22 21v-2a4 4 0 0 0-3-3.87`,key:`kshegd`}],[`path`,{d:`M16 3.13a4 4 0 0 1 0 7.75`,key:`1da9ce`}]]),Ft=P(`Vault`,[[`rect`,{width:`18`,height:`18`,x:`3`,y:`3`,rx:`2`,key:`afitv7`}],[`circle`,{cx:`7.5`,cy:`7.5`,r:`.5`,fill:`currentColor`,key:`kqv944`}],[`path`,{d:`m7.9 7.9 2.7 2.7`,key:`hpeyl3`}],[`circle`,{cx:`16.5`,cy:`7.5`,r:`.5`,fill:`currentColor`,key:`w0ekpg`}],[`path`,{d:`m13.4 10.6 2.7-2.7`,key:`264c1n`}],[`circle`,{cx:`7.5`,cy:`16.5`,r:`.5`,fill:`currentColor`,key:`nkw3mc`}],[`path`,{d:`m7.9 16.1 2.7-2.7`,key:`p81g5e`}],[`circle`,{cx:`16.5`,cy:`16.5`,r:`.5`,fill:`currentColor`,key:`fubopw`}],[`path`,{d:`m13.4 13.4 2.7 2.7`,key:`abhel3`}],[`circle`,{cx:`12`,cy:`12`,r:`2`,key:`1c9p78`}]]),It=P(`X`,[[`path`,{d:`M18 6 6 18`,key:`1bl5f8`}],[`path`,{d:`m6 6 12 12`,key:`d8bk6v`}]]),Lt=P(`Zap`,[[`path`,{d:`M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z`,key:`1xq2db`}]]),Rt=new Map;function zt(e){return Rt.get(e)}function H(e,t){Rt.set(e,t)}function Bt(){Rt.clear()}var Vt={dashboard:`dashboard`,storageStats:`storage.stats`,storageAccounts:`storage.accounts`},Ht=o((e=>{var t=u(),n=Symbol.for(`react.element`),r=Symbol.for(`react.fragment`),i=Object.prototype.hasOwnProperty,a=t.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,o={key:!0,ref:!0,__self:!0,__source:!0};function s(e,t,r){var s,c={},l=null,u=null;for(s in r!==void 0&&(l=``+r),t.key!==void 0&&(l=``+t.key),t.ref!==void 0&&(u=t.ref),t)i.call(t,s)&&!o.hasOwnProperty(s)&&(c[s]=t[s]);if(e&&e.defaultProps)for(s in t=e.defaultProps,t)c[s]===void 0&&(c[s]=t[s]);return{$$typeof:n,type:e,key:l,ref:u,props:c,_owner:a.current}}e.Fragment=r,e.jsx=s,e.jsxs=s})),U=o(((e,t)=>{t.exports=Ht()}))();function Ut({code:e,icon:t,title:n,children:r,detail:i,navigate:a}){return(0,U.jsxs)(`section`,{className:`status-screen`,children:[(0,U.jsx)(`span`,{className:`status-screen-code`,"aria-hidden":`true`,children:e}),(0,U.jsxs)(`div`,{className:`status-screen-body`,children:[(0,U.jsx)(`span`,{className:`status-screen-icon`,children:(0,U.jsx)(t,{size:26})}),(0,U.jsx)(`h1`,{children:n}),(0,U.jsx)(`p`,{children:r}),i&&(0,U.jsx)(`code`,{className:`status-screen-detail`,children:i}),a&&(0,U.jsxs)(`button`,{className:`btn primary icon-text`,type:`button`,onClick:()=>a(`/`),children:[(0,U.jsx)(pe,{size:15}),` `,`Back to overview`]})]})]})}var Wt=`verification.review`,Gt=`botverification.review`,Kt=`botverification.manage`,qt=`server.manage`,Jt=`admins.manage`,Yt=`accounts.read`,Xt=`channels.read`,Zt=`bots.read`,Qt=`messages.read`,$t=`moderation.review`,en=`broadcasts.read`,tn=`storage.read`,nn=`content.read`,rn=`usernames.read`,an=`dashboard.read`,on=(0,g.createContext)({permissions:[],hideThirdPartyVerification:!0});function sn({permissions:e,hideThirdPartyVerification:t=!0,children:n}){let r=(0,g.useMemo)(()=>({permissions:e,hideThirdPartyVerification:t}),[e,t]);return(0,U.jsx)(on.Provider,{value:r,children:n})}function cn(){let{permissions:e}=(0,g.useContext)(on);return(0,g.useMemo)(()=>({permissions:e,can:t=>e.includes(`*`)||e.includes(t)}),[e])}function ln(e){return cn().can(e)}function un(){return(0,g.useContext)(on).hideThirdPartyVerification}function dn({permission:e,navigate:t,children:n}){let{can:r}=cn();return r(e)?(0,U.jsx)(U.Fragment,{children:n}):(0,U.jsx)(fn,{permission:e,navigate:t})}function fn({permission:e,navigate:t}){return(0,U.jsx)(Ut,{code:`403`,icon:St,title:`You do not have access to this section`,detail:e,navigate:t,children:`It needs the "${hn(e)}" permission. Ask an operator who can manage operators to add it, then sign in again.`})}function pn({navigate:e,children:t}){return un()?(0,U.jsx)(Ut,{code:`404`,icon:Me,title:`This section is switched off`,detail:`TELESRV_HIDE_THIRD_PARTY_VERIFICATION=false`,navigate:e,children:`Third-party bot verification is not finished and is hidden on this server. It is a server setting, not a permission -- no account can see it while it is off.`}):(0,U.jsx)(U.Fragment,{children:t})}var mn={"accounts.read":{title:`View accounts`,hint:`Browse users, their profiles and sessions`},"accounts.manage":{title:`Edit accounts`,hint:`Change profiles, usernames, freeze and revoke sessions`},"channels.read":{title:`View groups and channels`,hint:`Browse supergroups and channels`},"channels.manage":{title:`Edit groups and channels`,hint:`Change settings, usernames and avatars`},"bots.read":{title:`View bots`,hint:`Browse the bot list and their details`},"bots.manage":{title:`Create and delete bots`,hint:`Add new bots and remove existing ones`},"bots.token.read":{title:`Reveal bot tokens`,hint:`Export a bot's live credential`},"messages.read":{title:`View messages`,hint:`Read private and group message history`},"messages.manage":{title:`Delete messages`,hint:`Remove messages and clear history`},"moderation.review":{title:`Handle reports`,hint:`Work the moderation queue and decide cases`},"broadcasts.read":{title:`View broadcasts`,hint:`See past and scheduled broadcasts`},"broadcasts.send":{title:`Send broadcasts`,hint:`Deliver a message to many users at once`},"content.read":{title:`View stickers, emoji and GIFs`,hint:`Browse the packs and the GIF catalogue`},"content.manage":{title:`Edit stickers, emoji and GIFs`,hint:`Create, rename and remove packs and catalogue entries`},"usernames.read":{title:`View NFT usernames`,hint:`Browse collectible usernames`},"usernames.manage":{title:`Manage NFT usernames`,hint:`Mint, transfer and revoke collectible usernames`},"storage.read":{title:`View storage`,hint:`See media usage per account`},"storage.manage":{title:`Purge storage`,hint:`Manually delete stored media`},"dashboard.read":{title:`View the dashboard`,hint:`See the overview counters and server health`},"premium.manage":{title:`Manage Premium`,hint:`Grant, revoke and refund Premium`},"verification.review":{title:`Verify accounts`,hint:`Work the verification queue and grant badges`},"verification.revoke":{title:`Remove verification`,hint:`Take a granted badge away (needs the right above too)`},"botverification.review":{title:`Handle third-party marks`,hint:`Work the third-party verification queue`},"botverification.manage":{title:`Appoint verifiers`,hint:`Grant verifier status and curate mark icons`},"server.manage":{title:`Server settings`,hint:`Identity, .env editing, restart and update`},"admins.manage":{title:`Manage operators`,hint:`Create operators and decide what everyone can do`},"*":{title:`Full access`,hint:`Every right, including future ones`}};function hn(e){return mn[e]?.title??e}function gn(e){return mn[e]?.hint??``}var _n=[{title:`People and chats`,hint:`Users, groups and their message history`,permissions:[`accounts.read`,`accounts.manage`,`channels.read`,`channels.manage`,`messages.read`,`messages.manage`]},{title:`Moderation and verification`,hint:`Reports, badges and third-party marks`,permissions:[`moderation.review`,`verification.review`,`verification.revoke`,`botverification.review`,`botverification.manage`]},{title:`Content`,hint:`Sticker packs, emoji, GIFs and collectible usernames`,permissions:[`content.read`,`content.manage`,`usernames.read`,`usernames.manage`]},{title:`Bots`,hint:`The bot roster and its credentials`,permissions:[`bots.read`,`bots.manage`,`bots.token.read`]},{title:`Broadcasting`,hint:`Messages sent to many users at once`,permissions:[`broadcasts.read`,`broadcasts.send`]},{title:`Storage and overview`,hint:`Media usage and the dashboard`,permissions:[`storage.read`,`storage.manage`,`dashboard.read`]},{title:`Billing`,hint:`Premium grants and refunds`,permissions:[`premium.manage`]},{title:`The console itself`,hint:`The two rights that can change the deployment or hand out every other right`,permissions:[`server.manage`,`admins.manage`]}];function vn(e){let t=new Set(e),n=[];for(let e of _n){let r=e.permissions.filter(e=>t.has(e));r.forEach(e=>t.delete(e)),r.length>0&&n.push({title:e.title,hint:e.hint,permissions:r})}return t.size>0&&n.push({title:`Other`,hint:`Rights this console version does not have a group for`,permissions:[...t]}),n}function yn(){return{href:`${window.location.pathname}${window.location.search}`,path:window.location.pathname,search:new URLSearchParams(window.location.search)}}function bn(e){return e.startsWith(`/bot-verification`)?`Third-party verification`:e.startsWith(`/verification`)?`Official Verification`:e.startsWith(`/collectible-usernames`)?`Collectible Usernames`:e.startsWith(`/storage`)?`Storage`:e.startsWith(`/accounts/shared-devices`)?`Shared Devices`:e.startsWith(`/accounts`)?`Accounts`:e.startsWith(`/channels`)?`Supergroups and Channels`:e.startsWith(`/bots`)?`Bots`:e.startsWith(`/moderation`)?`Reports and Moderation`:e.startsWith(`/broadcasts`)?`Broadcasts`:e.startsWith(`/emoji`)?`Emoji`:e.startsWith(`/messages`)?`Message Audit`:e.startsWith(`/stickers`)?`Stickers`:e.startsWith(`/gif-catalog`)?`GIFs`:e.startsWith(`/server-settings`)?`Server Settings`:`Operations Console`}var xn=`telesrv.admin.theme`,Sn=(0,g.createContext)(null);function Cn(e){document.documentElement.setAttribute(`data-theme`,e),document.documentElement.style.colorScheme=e}function wn({children:e}){let[t,n]=(0,g.useState)(()=>Dn());(0,g.useEffect)(()=>{Cn(t);try{localStorage.setItem(xn,t)}catch{}},[t]),(0,g.useEffect)(()=>{if(!window.matchMedia)return;let e=window.matchMedia(`(prefers-color-scheme: dark)`),t=e=>{let t=null;try{t=localStorage.getItem(xn)}catch{t=null}t!==`light`&&t!==`dark`&&n(e.matches?`dark`:`light`)};return e.addEventListener(`change`,t),()=>e.removeEventListener(`change`,t)},[]);let r=(0,g.useCallback)(e=>n(e),[]),i=(0,g.useCallback)(()=>n(e=>e===`dark`?`light`:`dark`),[]),a=(0,g.useMemo)(()=>({theme:t,setTheme:r,toggleTheme:i}),[t,r,i]);return(0,U.jsx)(Sn.Provider,{value:a,children:e})}function Tn(){let e=(0,g.useContext)(Sn);if(!e)throw Error(`useTheme must be used inside ThemeProvider`);return e}function En(){let{theme:e,toggleTheme:t}=Tn(),n=e===`light`?`Switch to dark theme`:`Switch to light theme`;return(0,U.jsx)(`button`,{className:`theme-toggle`,type:`button`,onClick:t,"aria-label":n,title:n,children:e===`dark`?(0,U.jsx)(Dt,{size:16}):(0,U.jsx)(it,{size:16})})}function Dn(){try{let e=localStorage.getItem(xn);if(e===`light`||e===`dark`)return e}catch{}try{if(window.matchMedia&&window.matchMedia(`(prefers-color-scheme: dark)`).matches)return`dark`}catch{}return`light`}var On=m();async function kn(e){if(navigator.clipboard?.writeText){await navigator.clipboard.writeText(e);return}let t=document.createElement(`textarea`);t.value=e,t.style.position=`fixed`,t.style.top=`-1000px`,t.style.left=`-1000px`,document.body.appendChild(t),t.focus(),t.select();try{if(!document.execCommand(`copy`))throw Error(`Copy command was not successful`)}finally{document.body.removeChild(t)}}function An(e){let t=e.trim();return!t||t.startsWith(`+`)?t:/^\d+$/.test(t)?`+${t}`:t}function W(e){let t=e.trim();return t?t.startsWith(`@`)?t:`@${t}`:``}function jn(e){return`${e.FirstName||``} ${e.LastName||``}`.trim()||`-`}function Mn(e){return e.Broadcast&&!e.Megagroup?`Channel`:e.Megagroup&&e.Forum?`Supergroup / Forum`:e.Megagroup?`Supergroup`:`Channel / Group`}function G(e){if(!e||e.startsWith(`0001-`))return``;let t=new Date(e);return Number.isNaN(t.getTime())?``:t.toLocaleString()}function Nn(e){if(!e||e<=0)return``;let t=new Date(e*1e3);return Number.isNaN(t.getTime())?``:t.toLocaleString()}function Pn(e){let t=(e??``).trim();if(!/^https?:\/\//i.test(t))return``;try{let e=new URL(t);return e.protocol!==`http:`&&e.protocol!==`https:`?``:e.href}catch{return``}}function Fn(e){if(!e.trim())return 0;let t=Number.parseInt(e,10);return Number.isFinite(t)?t:0}function In(e){let t=(e??``).trim();if(!t)return`0`;let n=Number(t);return Number.isFinite(n)?n.toLocaleString():t}var Ln={XTR:0,TON:9,USD:2,EUR:2,RUB:2};function Rn(e){let t=(e??``).trim().toUpperCase();return t in Ln?Ln[t]:2}function zn(e,t){let n=(e??``).trim();if(!n)return`0`;if(!/^-?\d+$/.test(n))return n;let r=Rn(t),i=n.startsWith(`-`),a=(i?n.slice(1):n).replace(/^0+(?=\d)/,``).padStart(r+1,`0`),o=a.slice(0,a.length-r)||`0`,s=r>0?a.slice(a.length-r):``;r>2&&(s=s.replace(/0+$/,``));let c=i?`-`:``;return s?`${c}${Bn(o)}.${s}`:`${c}${Bn(o)}`}function Bn(e){return e.replace(/\B(?=(\d{3})+(?!\d))/g,` `)}function Vn(e,t){let n=(t??``).trim().toUpperCase(),r=zn(e,n);return n?`${r} ${n}`:r}function Hn(e,t){let n=(e??``).trim().replace(/\s+/g,``).replace(`,`,`.`);if(!n)return`0`;if(!/^\d*(\.\d*)?$/.test(n)||n===`.`)return null;let r=Rn(t),[i,a=``]=n.split(`.`);if(a.length>r)return null;let o=`${i||`0`}${a.padEnd(r,`0`)}`.replace(/^0+(?=\d)/,``);return o===``?`0`:o}function Un(e){let t=(e??``).trim();if(!t||!/^\d+$/.test(t))return`0 B`;let n=Number(t);if(!Number.isFinite(n))return`${t} B`;let r=[`B`,`KB`,`MB`,`GB`,`TB`,`PB`],i=n,a=0;for(;i>=1024&&ae.trim()).filter(Boolean).map(e=>Number.parseInt(e,10));if(n.length===0||n.some(e=>!Number.isFinite(e)||e<=0))throw Error(t);return n}function Gn({title:e,eyebrow:t,children:n,actions:r}){return(0,U.jsxs)(`div`,{className:`page-frame`,children:[(0,U.jsxs)(`div`,{className:`page-title-row`,children:[(0,U.jsxs)(`div`,{children:[t&&(0,U.jsx)(`div`,{className:`eyebrow`,children:t}),(0,U.jsx)(`h2`,{children:e})]}),r&&(0,U.jsx)(`div`,{className:`page-actions`,children:r})]}),n]})}function Kn({children:e}){return(0,U.jsx)(`div`,{className:`query-panel`,children:e})}function qn({main:e,side:t}){return(0,U.jsxs)(`div`,{className:`split-layout`,children:[(0,U.jsx)(`div`,{className:`split-main`,children:e}),(0,U.jsx)(`aside`,{className:`split-side`,children:t})]})}function K({title:e,text:t,action:n}){return(0,U.jsxs)(`div`,{className:`section-head`,children:[(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`h2`,{children:e}),t&&(0,U.jsx)(`p`,{children:t})]}),n&&(0,U.jsx)(`div`,{className:`section-action`,children:n})]})}function q({children:e}){return(0,U.jsxs)(`div`,{className:`alert`,children:[(0,U.jsx)(te,{size:16}),` `,(0,U.jsx)(`span`,{children:e})]})}function J({children:e,tone:t=`neutral`}){return(0,U.jsx)(`span`,{className:`badge ${t}`,children:e})}function Y({label:e,value:t,tone:n=`neutral`,mono:r=!1,loading:i=!1}){return(0,U.jsxs)(`div`,{className:`metric ${n}`,children:[(0,U.jsx)(`span`,{children:e}),(0,U.jsx)(`strong`,{className:r?`mono`:``,"aria-busy":i||void 0,children:i?(0,U.jsx)(`span`,{className:`skeleton skeleton-text`,"aria-label":`Loading`}):t})]})}function X({label:e,value:t,mono:n=!1}){return(0,U.jsxs)(`div`,{className:`summary-item`,children:[(0,U.jsx)(`span`,{children:e}),(0,U.jsx)(`strong`,{className:n?`mono`:``,children:t})]})}function Jn({rows:e}){return(0,U.jsx)(`div`,{className:`table-wrap`,children:(0,U.jsxs)(`table`,{className:`data-table`,children:[(0,U.jsx)(`thead`,{children:(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`th`,{children:`ID`}),(0,U.jsx)(`th`,{children:`Command ID`}),(0,U.jsx)(`th`,{children:`Action`}),(0,U.jsx)(`th`,{children:`Actor`}),(0,U.jsx)(`th`,{children:`Status`}),(0,U.jsx)(`th`,{children:`Dry-run`}),(0,U.jsx)(`th`,{children:`Reason`}),(0,U.jsx)(`th`,{children:`Time`})]})}),(0,U.jsxs)(`tbody`,{children:[e.map(e=>(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`td`,{children:e.ID}),(0,U.jsx)(`td`,{className:`mono`,children:e.CommandID}),(0,U.jsx)(`td`,{children:e.Action}),(0,U.jsx)(`td`,{children:e.Actor}),(0,U.jsx)(`td`,{children:e.Status}),(0,U.jsx)(`td`,{children:e.DryRun?`Yes`:`No`}),(0,U.jsx)(`td`,{className:`truncate`,children:e.Reason}),(0,U.jsx)(`td`,{children:G(e.CreatedAt)})]},e.ID)),e.length===0&&(0,U.jsx)(Yn,{colSpan:8})]})]})})}function Yn({colSpan:e}){return(0,U.jsx)(`tr`,{children:(0,U.jsx)(`td`,{colSpan:e,className:`empty-cell`,children:`No results`})})}function Xn({colSpan:e,rows:t=3}){return(0,U.jsx)(U.Fragment,{children:Array.from({length:t},(t,n)=>(0,U.jsx)(`tr`,{"aria-busy":`true`,children:Array.from({length:e},(e,t)=>(0,U.jsx)(`td`,{children:(0,U.jsx)(`span`,{className:`skeleton skeleton-text`,"aria-label":t===0?`Loading`:void 0})},t))},n))})}function Zn({label:e}){return(0,U.jsx)(`section`,{className:`surface`,children:(0,U.jsx)(`div`,{className:`loading-line`,children:e})})}function Qn({value:e}){return(0,U.jsx)(`pre`,{className:`json-block`,children:e||`{}`})}function $n({username:e,collectibles:t}){let n=W(e??``),r=t??[];return r.length===0?(0,U.jsx)(U.Fragment,{children:n||`-`}):(0,U.jsxs)(U.Fragment,{children:[n,(0,U.jsx)(`ul`,{className:`username-branch`,children:r.map(e=>(0,U.jsxs)(`li`,{className:e.Active?``:`inactive`,children:[(0,U.jsx)(`span`,{children:W(e.Username)}),!e.Active&&(0,U.jsx)(`em`,{children:`inactive`})]},e.Username))})]})}function er({onClose:e}){let[t,n]=(0,g.useState)(null),[r,i]=(0,g.useState)(``),[a,o]=(0,g.useState)(!1);(0,g.useEffect)(()=>{let e=!1;return k.addServerLink().then(t=>{e||n(t.link)}).catch(t=>{e||i(O(t))}),()=>{e=!0}},[]);async function s(){if(t)try{await kn(t),o(!0)}catch(e){i(O(e))}}return(0,On.createPortal)((0,U.jsx)(`div`,{className:`modal-backdrop`,role:`presentation`,children:(0,U.jsxs)(`section`,{className:`modal command-modal add-server-link-modal`,role:`dialog`,"aria-modal":`true`,"aria-label":`Share server`,children:[(0,U.jsxs)(`div`,{className:`modal-head`,children:[(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`div`,{className:`eyebrow`,children:`Server`}),(0,U.jsx)(`h2`,{children:`Share server`})]}),(0,U.jsx)(`button`,{className:`icon-btn`,type:`button`,onClick:e,"aria-label":`Close`,children:(0,U.jsx)(It,{size:15})})]}),(0,U.jsxs)(`div`,{className:`command-body`,children:[(0,U.jsx)(`p`,{children:`Share this link (a button, a QR code, a message) so anyone with the OwpenGram client can add this server in one tap. It only carries the address and port -- the client fetches the name, description, and key directly from the server itself, so the link can never be tampered with to point someone at a fake identity for this address.`}),r&&(0,U.jsx)(q,{children:r}),!t&&!r&&(0,U.jsx)(Zn,{label:`Building link...`}),t&&(0,U.jsxs)(`div`,{className:`add-server-link-field`,children:[(0,U.jsxs)(`label`,{className:`form-field`,children:[(0,U.jsx)(`span`,{children:`owpg:// link`}),(0,U.jsx)(`textarea`,{value:t,readOnly:!0,rows:2,onFocus:e=>e.currentTarget.select()})]}),(0,U.jsxs)(`button`,{className:`btn primary icon-text`,type:`button`,onClick:()=>void s(),children:[(0,U.jsx)(Ee,{size:15}),` `,a?`Copy again`:`Copy link`]}),a&&(0,U.jsx)(`div`,{className:`secret-reveal`,children:(0,U.jsxs)(`div`,{className:`secret-reveal-label`,children:[(0,U.jsx)(Se,{size:14}),` `,`Copied to clipboard.`]})})]})]}),(0,U.jsx)(`div`,{className:`modal-actions`,children:(0,U.jsx)(`button`,{className:`btn`,type:`button`,onClick:e,children:`Close`})})]})}),document.body)}var tr=[``,``,``,``,``,``,``,``,``,``,``,``,``,``,``,``,``,``,``,``,``,``,``,``,``],nr=28;function rr(){let e=(0,g.useRef)(null);return(0,g.useEffect)(()=>{let t=e.current;if(!t||window.matchMedia?.(`(prefers-reduced-motion: reduce)`).matches)return;let n=Array.from(t.querySelectorAll(`.bg-icon`)),r=[];function i(){let e=window.innerWidth,t=window.innerHeight;r=n.map(n=>{let r=22+Math.random()*30;return n.style.width=`${r}px`,n.style.height=`${r}px`,n.style.opacity=String(.18+Math.random()*.12),{x:60+Math.random()*Math.max(1,e-120),y:60+Math.random()*Math.max(1,t-120),vx:(Math.random()-.5)*.3,vy:(Math.random()-.5)*.3,size:r,rot:Math.random()*360,rotSpeed:(Math.random()-.5)*.08}})}let a=0;function o(){let e=window.innerWidth,t=window.innerHeight;for(let n=0;ne-a&&(i.x=e-a,i.vx*=-1),i.yt-a&&(i.y=t-a,i.vy*=-1);for(let e=n+1;e.01){let e=(s-o)/s*.02,r=n/o,c=a/o;i.vx-=r*e,i.vy-=c*e,t.vx+=r*e,t.vy+=c*e}}}for(let e=0;e{cancelAnimationFrame(a),window.removeEventListener(`resize`,i)}},[]),(0,U.jsx)(`div`,{className:`bg-icons`,"aria-hidden":`true`,ref:e,children:Array.from({length:nr},(e,t)=>(0,U.jsx)(`svg`,{className:`bg-icon`,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:`1.6`,strokeLinecap:`round`,strokeLinejoin:`round`,dangerouslySetInnerHTML:{__html:tr[t%tr.length]}},t))})}function ir({className:e=``}){let t=(0,g.useRef)(null);return(0,g.useEffect)(()=>{if(window.matchMedia?.(`(prefers-reduced-motion: reduce)`).matches)return;function e(e){let n=t.current;if(!n)return;let r=(e.clientX/window.innerWidth-.5)*30,i=(e.clientY/window.innerHeight-.5)*30;n.style.transform=`translate(${r}px, ${i}px)`}return window.addEventListener(`mousemove`,e),()=>window.removeEventListener(`mousemove`,e)},[]),(0,U.jsxs)(`div`,{className:`app-background ${e}`.trim(),"aria-hidden":`true`,children:[(0,U.jsxs)(`div`,{className:`bg-orbs`,ref:t,children:[(0,U.jsx)(`div`,{className:`bg-orb bg-orb--1`}),(0,U.jsx)(`div`,{className:`bg-orb bg-orb--2`}),(0,U.jsx)(`div`,{className:`bg-orb bg-orb--3`})]}),(0,U.jsx)(rr,{})]})}function ar({href:e,navigate:t,className:n,title:r,children:i}){return(0,U.jsx)(`a`,{className:n,href:e,title:r,onClick:n=>{n.preventDefault(),t(e)},children:i})}function or(e){let t=[...e].sort((e,t)=>e-t),n=[],r=t[0],i=t[0];for(let e=1;e<=t.length;e++){let a=t[e];if(a===i+1){i=a;continue}n.push(r===i?`${r}`:`${r}-${i}`),r=a,i=a}return n.join(`, `)}function sr(){return(0,U.jsxs)(`div`,{className:`boot-screen`,children:[(0,U.jsxs)(`div`,{className:`brand compact brand-elevated`,children:[(0,U.jsx)(`span`,{className:`brand-mark`,children:(0,U.jsx)(`img`,{src:`/logo.png`,alt:`OwpenGram`})}),(0,U.jsxs)(`span`,{children:[(0,U.jsx)(`strong`,{children:`OwpenGram`}),(0,U.jsx)(`small`,{children:`Admin Console`})]})]}),(0,U.jsx)(`div`,{className:`loader-bar`})]})}function cr({actor:e,apiLayers:t,build:n,route:r,navigate:i,onLogout:a,children:o}){let s=ln(Wt),c=ln(Jt),l=ln(Yt),u=ln(Xt),d=ln(Zt),f=ln(Qt),p=ln($t),m=ln(en),h=ln(tn),_=ln(nn),v=ln(rn),y=ln(Gt),b=ln(qt),[x,S]=(0,g.useState)(()=>{try{return localStorage.getItem(`owpengram.nav.collapsed`)===`1`}catch{return!1}});function C(){S(e=>{let t=!e;try{localStorage.setItem(`owpengram.nav.collapsed`,t?`1`:`0`)}catch{}return t})}let[w,T]=(0,g.useState)(!1),[E,D]=(0,g.useState)(!1),[A,j]=(0,g.useState)(``);async function M(){D(!0),j(``);try{let e=await k.addServerLink();window.location.href=e.link}catch(e){j(O(e))}finally{D(!1)}}let[N,P]=(0,g.useState)(null);(0,g.useEffect)(()=>{b&&k.serverIdentity().then(e=>P({name:e.name,iconExt:e.icon_ext})).catch(()=>void 0)},[b]);let[te,F]=(0,g.useState)(!1),ne=N?.name?.trim()||`OwpenGram`,re=N?.iconExt&&!te?k.serverIconURL():`/logo.png`;(0,g.useEffect)(()=>{document.title=`${ne} Admin`},[ne]),(0,g.useEffect)(()=>{let e=document.querySelector(`link[rel='icon']`);e||(e=document.createElement(`link`),e.rel=`icon`,document.head.appendChild(e));let t=e,n=N?.iconExt&&!te?k.serverIconURL():`/logo.png`,r=!1,i=new Image;return i.crossOrigin=`anonymous`,i.onload=()=>{if(r)return;let e=document.createElement(`canvas`);e.width=64,e.height=64;let a=e.getContext(`2d`);if(!a){t.href=n;return}a.save(),a.beginPath(),a.arc(64/2,64/2,64/2,0,Math.PI*2),a.closePath(),a.clip(),a.drawImage(i,0,0,64,64),a.restore(),t.href=e.toDataURL(`image/png`)},i.onerror=()=>{r||(t.href=n)},i.src=n,()=>{r=!0}},[N?.iconExt,te]);let I=un();async function oe(){await k.logout().catch(()=>void 0),Bt(),a()}return(0,U.jsxs)(`div`,{className:`shell ${x?`shell--nav-collapsed`:``}`.trim(),children:[(0,U.jsxs)(`aside`,{className:`sidebar`,children:[(0,U.jsxs)(ar,{className:`brand`,href:`/`,navigate:i,children:[(0,U.jsx)(`span`,{className:`brand-mark`,children:(0,U.jsx)(`img`,{src:re,alt:ne,onError:()=>F(!0)})}),(0,U.jsxs)(`span`,{children:[(0,U.jsx)(`strong`,{children:ne}),(0,U.jsx)(`small`,{children:`Admin Console`})]})]}),(0,U.jsx)(`div`,{className:`sidebar-label`,children:`Navigation`}),(0,U.jsxs)(`nav`,{className:`nav-list`,"aria-label":`Primary navigation`,children:[(0,U.jsx)(lr,{icon:(0,U.jsx)(Ke,{size:16}),href:`/`,route:r,navigate:i,children:`Overview`}),l&&(0,U.jsx)(lr,{icon:(0,U.jsx)(Pt,{size:16}),href:`/accounts`,route:r,navigate:i,children:`Accounts`}),u&&(0,U.jsx)(lr,{icon:(0,U.jsx)(xt,{size:16}),href:`/channels`,route:r,navigate:i,children:`Supergroups / Channels`}),d&&(0,U.jsx)(lr,{icon:(0,U.jsx)(ye,{size:16}),href:`/bots`,route:r,navigate:i,children:`Bots`}),p&&(0,U.jsx)(lr,{icon:(0,U.jsx)(bt,{size:16}),href:`/moderation`,route:r,navigate:i,children:`Reports / Moderation`}),m&&(0,U.jsx)(lr,{icon:(0,U.jsx)(et,{size:16}),href:`/broadcasts`,route:r,navigate:i,children:`Broadcasts`}),s&&(0,U.jsx)(lr,{icon:(0,U.jsx)(ee,{size:16}),href:`/verification`,route:r,navigate:i,children:`Verification`}),y&&!I&&(0,U.jsx)(lr,{icon:(0,U.jsx)(Tt,{size:16}),href:`/bot-verification`,route:r,navigate:i,children:`Third-party marks`}),v&&(0,U.jsx)(lr,{icon:(0,U.jsx)(_e,{size:16}),href:`/collectible-usernames`,route:r,navigate:i,children:`NFT Usernames`}),h&&(0,U.jsx)(lr,{icon:(0,U.jsx)(Oe,{size:16}),href:`/storage`,route:r,navigate:i,children:`Storage`}),_&&(0,U.jsx)(lr,{icon:(0,U.jsx)(Et,{size:16}),href:`/stickers`,route:r,navigate:i,children:`Stickers`}),_&&(0,U.jsx)(lr,{icon:(0,U.jsx)(wt,{size:16}),href:`/emoji`,route:r,navigate:i,children:`Emoji`}),_&&(0,U.jsx)(lr,{icon:(0,U.jsx)(Ie,{size:16}),href:`/gif-catalog`,route:r,navigate:i,children:`GIFs`}),f&&(0,U.jsx)(lr,{icon:(0,U.jsx)(nt,{size:16}),href:`/messages/private`,route:r,navigate:i,activeWhen:e=>e.startsWith(`/messages`),children:`Messages`}),c&&(0,U.jsx)(lr,{icon:(0,U.jsx)(jt,{size:16}),href:`/admin-users`,route:r,navigate:i,children:`Operators`}),b&&(0,U.jsx)(lr,{icon:(0,U.jsx)(vt,{size:16}),href:`/server-settings`,route:r,navigate:i,children:`Server Settings`})]}),(0,U.jsxs)(`div`,{className:`sidebar-status`,children:[(0,U.jsx)(`span`,{className:`sidebar-label`,children:`Version: O7`}),t&&t.length>0&&(0,U.jsx)(`span`,{className:`sidebar-label sidebar-api-layer`,title:`Layers: ${t.join(`, `)}`,children:`API layers: ${or(t)}`}),n?.short_commit&&(0,U.jsx)(`span`,{className:`sidebar-label sidebar-build`,title:n.commit+(n.dirty?` (uncommitted changes)`:``),children:`Build: ${n.short_commit}${n.dirty?`+`:``}`})]}),b&&(0,U.jsxs)(`div`,{className:`sidebar-server-actions`,children:[(0,U.jsxs)(`button`,{className:`btn ghost sidebar-server-action`,type:`button`,title:`Connect this browser's client to this server`,disabled:E,onClick:()=>void M(),children:[(0,U.jsx)(Lt,{size:15}),` `,`Connect`]}),(0,U.jsxs)(`button`,{className:`btn ghost sidebar-server-action`,type:`button`,title:`Share server (get an add-server link)`,onClick:()=>T(!0),children:[(0,U.jsx)(yt,{size:15}),` `,`Share`]})]}),A&&(0,U.jsx)(`div`,{className:`sidebar-server-action-error`,children:A}),w&&(0,U.jsx)(er,{onClose:()=>T(!1)})]}),(0,U.jsxs)(`div`,{className:`workspace`,children:[(0,U.jsxs)(`header`,{className:`topbar`,children:[(0,U.jsxs)(`div`,{className:`topbar-lead`,children:[(0,U.jsx)(`button`,{className:`icon-btn nav-toggle`,type:`button`,onClick:C,"aria-expanded":!x,"aria-label":x?`Expand navigation`:`Collapse navigation`,title:x?`Expand navigation`:`Collapse navigation`,children:x?(0,U.jsx)(ae,{size:16}):(0,U.jsx)(ie,{size:16})}),(0,U.jsx)(`h1`,{children:bn(r.path)})]}),(0,U.jsxs)(`div`,{className:`topbar-actions`,children:[(0,U.jsx)(En,{}),(0,U.jsxs)(`span`,{className:`actor-pill`,children:[(0,U.jsx)(le,{size:14}),` `,e]}),(0,U.jsxs)(`button`,{className:`btn ghost icon-text`,type:`button`,onClick:oe,title:`Log out`,children:[(0,U.jsx)(Ze,{size:16}),` `,`Log out`]})]})]}),(0,U.jsx)(`main`,{className:`content`,children:o})]}),(0,U.jsx)(ir,{className:`app-background--workspace`})]})}function lr({href:e,route:t,navigate:n,icon:r,children:i,activeWhen:a}){return(0,U.jsxs)(ar,{className:`nav-item ${(a?a(t.path):e===`/`?t.path===`/`:t.path.startsWith(e))?`active`:``}`,href:e,navigate:n,title:typeof i==`string`?i:void 0,children:[r??(0,U.jsx)(`span`,{"aria-hidden":`true`,className:`nav-dot`}),(0,U.jsx)(`span`,{className:`nav-item-label`,children:i})]})}function Z({label:e,path:t,payload:n,icon:r,compact:i=!1,tone:a=`danger`,disabled:o=!1,onDone:s,onError:c,secretField:l}){let[u,d]=(0,g.useState)(!1),[f,p]=(0,g.useState)(``),[m,h]=(0,g.useState)(null),[_,v]=(0,g.useState)(``),[y,b]=(0,g.useState)(!1),[x,S]=(0,g.useState)(!1);function C(){p(``),h(null),v(``),S(!1)}async function w(e){if(!f.trim()){v(`Please enter an operation reason`);return}b(!0),v(``);try{let r={...n(),reason:f,confirm:e};h(await k.action(t,r)),e&&s?.()}catch(e){v(c?.(e)||O(e))}finally{b(!1)}}let T=m?.dry_run&&!m.error,E=`btn ${a===`danger`?`danger`:a===`warn`?`warn`:a===`primary`?`primary`:``} ${i?`compact-btn`:``}`,D=(0,g.useMemo)(()=>{try{return n()}catch(e){return{payload_error:O(e)}}},[u,n]),A=l&&m?.details&&typeof m.details[l]==`string`?m.details[l]:``,j=A&&m?.details?Object.fromEntries(Object.entries(m.details).filter(([e])=>e!==l)):m?.details;async function M(){await kn(A),S(!0)}return(0,U.jsxs)(U.Fragment,{children:[(0,U.jsxs)(`button`,{className:E,type:`button`,disabled:o,onClick:()=>{C(),d(!0)},children:[r,e]}),u&&(0,On.createPortal)((0,U.jsx)(`div`,{className:`modal-backdrop`,role:`presentation`,children:(0,U.jsxs)(`section`,{className:`modal command-modal`,role:`dialog`,"aria-modal":`true`,"aria-label":e,children:[(0,U.jsxs)(`div`,{className:`modal-head`,children:[(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`div`,{className:`eyebrow`,children:`Action Flow`}),(0,U.jsx)(`h2`,{children:e})]}),(0,U.jsx)(`button`,{className:`icon-btn`,type:`button`,onClick:()=>d(!1),"aria-label":`Close`,children:(0,U.jsx)(It,{size:15})})]}),(0,U.jsxs)(`div`,{className:`command-body`,children:[(0,U.jsxs)(`div`,{className:`command-steps`,children:[(0,U.jsxs)(`div`,{className:`command-step ${f.trim()?`done`:`active`}`,children:[(0,U.jsx)(`span`,{children:`1`}),(0,U.jsx)(`strong`,{children:`Enter reason`})]}),(0,U.jsxs)(`div`,{className:`command-step ${m?.dry_run?`done`:f.trim()?`active`:``}`,children:[(0,U.jsx)(`span`,{children:`2`}),(0,U.jsx)(`strong`,{children:`Dry-run check`})]}),(0,U.jsxs)(`div`,{className:`command-step ${m&&!m.dry_run&&!m.error?`done`:T?`active`:``}`,children:[(0,U.jsx)(`span`,{children:`3`}),(0,U.jsx)(`strong`,{children:`Confirm execution`})]})]}),(0,U.jsxs)(`label`,{className:`form-field`,children:[(0,U.jsx)(`span`,{children:`Operation reason`}),(0,U.jsx)(`textarea`,{value:f,onChange:e=>p(e.target.value),rows:3,placeholder:`Describe why this operation is being performed`})]}),(0,U.jsxs)(`div`,{className:`command-preview`,children:[(0,U.jsxs)(`div`,{className:`preview-head`,children:[(0,U.jsx)(Pe,{size:14}),` `,`Request preview`]}),(0,U.jsx)(Qn,{value:JSON.stringify(D,null,2)})]}),_&&(0,U.jsx)(q,{children:_}),m&&(0,U.jsxs)(`div`,{className:`result-box`,children:[(0,U.jsxs)(`div`,{className:`result-title`,children:[m.error?(0,U.jsx)(te,{size:16}):(0,U.jsx)(F,{size:16}),(0,U.jsx)(`strong`,{children:m.message||m.error||`Action result`})]}),(0,U.jsxs)(`div`,{className:`result-line`,children:[(0,U.jsx)(`span`,{children:`Command ID`}),(0,U.jsx)(`strong`,{children:m.command_id})]}),(0,U.jsxs)(`div`,{className:`result-line`,children:[(0,U.jsx)(`span`,{children:`Status`}),(0,U.jsx)(`strong`,{children:m.status})]}),(0,U.jsxs)(`div`,{className:`result-line`,children:[(0,U.jsx)(`span`,{children:`Dry-run`}),(0,U.jsx)(`strong`,{children:m.dry_run?`Yes`:`No`})]}),(0,U.jsx)(`div`,{className:`result-message`,children:m.message||m.error}),A&&(0,U.jsxs)(`div`,{className:`secret-reveal`,children:[(0,U.jsx)(`div`,{className:`secret-reveal-label`,children:`One-time secret — copy it now, it won't be shown again`}),(0,U.jsxs)(`div`,{className:`secret-reveal-row`,children:[(0,U.jsx)(`code`,{className:`secret-reveal-value`,children:`•`.repeat(Math.min(A.length,40))}),(0,U.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>void M(),children:[x?(0,U.jsx)(Se,{size:15}):(0,U.jsx)(Ee,{size:15}),x?`Copied`:`Copy`]})]})]}),j&&Object.keys(j).length>0&&(0,U.jsx)(Qn,{value:JSON.stringify(j,null,2)})]})]}),(0,U.jsxs)(`div`,{className:`modal-actions`,children:[(0,U.jsx)(`button`,{className:`btn`,type:`button`,onClick:()=>d(!1),children:`Close`}),(0,U.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>w(!1),disabled:y,children:[y?(0,U.jsx)(I,{size:15,className:`spin`}):(0,U.jsx)(st,{size:15}),m?`Run dry-run again`:`Run dry-run first`]}),(0,U.jsxs)(`button`,{className:`btn danger icon-text`,type:`button`,onClick:()=>w(!0),disabled:y||!T,children:[(0,U.jsx)(F,{size:15}),`Confirm execution`]})]})]})}),document.body)]})}function ur(){let[e,t]=(0,g.useState)(`settings`);return(0,U.jsxs)(Gn,{title:`Server Settings`,eyebrow:`Identity, .env, and live process/service control`,children:[(0,U.jsxs)(`div`,{className:`tab-bar`,role:`tablist`,"aria-label":`Server Settings sections`,children:[(0,U.jsx)(`button`,{className:`tab-btn ${e===`settings`?`active`:``}`,type:`button`,role:`tab`,"aria-selected":e===`settings`,onClick:()=>t(`settings`),children:`Settings`}),(0,U.jsx)(`button`,{className:`tab-btn ${e===`services`?`active`:``}`,type:`button`,role:`tab`,"aria-selected":e===`services`,onClick:()=>t(`services`),children:`Services`})]}),e===`settings`?(0,U.jsxs)(`div`,{className:`stacked-sections`,children:[(0,U.jsx)(dr,{}),(0,U.jsx)(fr,{}),(0,U.jsx)(mr,{})]}):(0,U.jsx)(`div`,{className:`stacked-sections`,children:(0,U.jsx)(wr,{})})]})}function dr(){let[e,t]=(0,g.useState)(null),[n,r]=(0,g.useState)(``),[i,a]=(0,g.useState)(``),[o,s]=(0,g.useState)(!1),[c,l]=(0,g.useState)(0),[u,d]=(0,g.useState)(!1),[f,p]=(0,g.useState)(``);async function m(){p(``);try{let e=await k.serverIdentity();t(e),r(e.name),a(e.description),d(!1)}catch(e){p(O(e))}}return(0,g.useEffect)(()=>{m()},[]),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(K,{title:`Server identity`}),f&&(0,U.jsx)(q,{children:f}),e?(0,U.jsxs)(`div`,{className:`card-body identity-card`,children:[(0,U.jsxs)(`div`,{className:`identity-layout`,children:[(0,U.jsxs)(`div`,{className:`avatar-edit-slot`,children:[e.icon_ext&&!u?(0,U.jsx)(`img`,{className:`avatar-photo-img`,src:k.serverIconURL()+`&b=${c}`,alt:``,style:{width:88,height:88},onError:()=>d(!0)}):(0,U.jsx)(`div`,{className:`avatar-fallback server-icon-fallback`,style:{width:88,height:88},children:(0,U.jsx)(Ue,{size:26})}),(0,U.jsx)(`button`,{className:`icon-btn avatar-edit-btn`,type:`button`,"aria-label":`Change server icon`,title:`Change server icon`,onClick:()=>s(!0),children:(0,U.jsx)(We,{size:14})})]}),(0,U.jsxs)(`div`,{className:`server-identity-fields`,children:[(0,U.jsxs)(`label`,{className:`form-field`,children:[(0,U.jsx)(`span`,{children:`Name`}),(0,U.jsx)(`input`,{value:n,maxLength:128,onChange:e=>r(e.target.value)})]}),(0,U.jsxs)(`label`,{className:`form-field`,children:[(0,U.jsx)(`span`,{children:`Description`}),(0,U.jsx)(`textarea`,{rows:4,value:i,maxLength:512,onChange:e=>a(e.target.value)})]})]})]}),(0,U.jsx)(`div`,{className:`gift-table-actions identity-save-row`,children:(0,U.jsx)(Z,{tone:`neutral`,label:`Save identity`,path:`/api/actions/set-server-identity`,payload:()=>({name:n,description:i}),onDone:()=>void m()})})]}):(0,U.jsx)(Zn,{label:`Loading identity...`}),o&&(0,U.jsx)(pr,{hasIcon:!!e?.icon_ext,onClose:()=>s(!1),onDone:()=>{l(e=>e+1),d(!1),m()}})]})}function fr(){let[e,t]=(0,g.useState)(null),[n,r]=(0,g.useState)(``),[i,a]=(0,g.useState)(``),[o,s]=(0,g.useState)(``),[c,l]=(0,g.useState)(``);async function u(){l(``);try{let e=await k.serverIdentity();t(e),r(e.welcome_message_phone_template??``),a(e.welcome_message_email_template??``),s(e.login_code_message_template??``)}catch(e){l(O(e))}}(0,g.useEffect)(()=>{u()},[]);let d=n.trim()!==``,f=i.trim()!==``,p=o.trim()!==``,m=(o.match(/\{\{code\}\}/g)??[]).length,h=p&&m!==1;return(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(K,{title:`Login notifications`}),c&&(0,U.jsx)(q,{children:c}),e?(0,U.jsxs)(`div`,{className:`card-body`,children:[(0,U.jsxs)(`p`,{style:{color:`var(--muted)`,marginTop:0},children:[`Sent from the official system account on every completed sign-in. Use `,(0,U.jsx)(`code`,{children:`{{server_name}}`}),` to insert the server's configured name.`]}),(0,U.jsxs)(`label`,{className:`form-field`,children:[(0,U.jsxs)(`span`,{children:[`Phone sign-in template`,` `,d?(0,U.jsx)(`span`,{className:`badge good`,children:`custom`}):(0,U.jsx)(`span`,{className:`badge`,children:`default`})]}),(0,U.jsx)(`textarea`,{rows:4,value:n,onChange:e=>r(e.target.value),placeholder:e.default_welcome_message_phone_template})]}),(0,U.jsx)(`div`,{className:`gift-table-actions`,children:(0,U.jsx)(Z,{tone:`neutral`,compact:!0,label:`Reset to default`,path:`/api/actions/set-welcome-message-templates`,payload:()=>({phone_template:``,email_template:i}),disabled:!d,onDone:()=>{r(``),u()}})}),(0,U.jsxs)(`label`,{className:`form-field`,children:[(0,U.jsxs)(`span`,{children:[`Email sign-in template`,` `,f?(0,U.jsx)(`span`,{className:`badge good`,children:`custom`}):(0,U.jsx)(`span`,{className:`badge`,children:`default`})]}),(0,U.jsx)(`textarea`,{rows:4,value:i,onChange:e=>a(e.target.value),placeholder:e.default_welcome_message_email_template})]}),(0,U.jsx)(`div`,{className:`gift-table-actions`,children:(0,U.jsx)(Z,{tone:`neutral`,compact:!0,label:`Reset to default`,path:`/api/actions/set-welcome-message-templates`,payload:()=>({phone_template:n,email_template:``}),disabled:!f,onDone:()=>{a(``),u()}})}),(0,U.jsx)(`div`,{className:`gift-table-actions identity-save-row`,children:(0,U.jsx)(Z,{tone:`neutral`,label:`Save login notification templates`,path:`/api/actions/set-welcome-message-templates`,payload:()=>({phone_template:n,email_template:i}),onDone:()=>void u()})}),(0,U.jsxs)(`p`,{style:{color:`var(--muted)`,marginTop:`1.5em`,borderTop:`1px solid var(--line)`,paddingTop:`1em`},children:[`Sent from the official system account with every login code (SMS and email alike). Must contain `,(0,U.jsx)(`code`,{children:`{{code}}`}),` exactly once -- that's where the actual code is inserted and bolded. `,(0,U.jsx)(`code`,{children:`{{server_name}}`}),` is optional and may appear any number of times.`]}),(0,U.jsxs)(`label`,{className:`form-field`,children:[(0,U.jsxs)(`span`,{children:[`Login-code message template`,` `,p?(0,U.jsx)(`span`,{className:`badge good`,children:`custom`}):(0,U.jsx)(`span`,{className:`badge`,children:`default`})]}),(0,U.jsx)(`textarea`,{rows:5,value:o,onChange:e=>s(e.target.value),placeholder:e.default_login_code_message_template}),h&&(0,U.jsx)(`span`,{style:{color:`var(--danger-text)`,fontSize:`0.85em`},children:m===0?`Must contain {{code}} exactly once -- it is currently missing.`:`Must contain {{code}} exactly once -- it currently appears ${m} times.`})]}),(0,U.jsx)(`div`,{className:`gift-table-actions`,children:(0,U.jsx)(Z,{tone:`neutral`,compact:!0,label:`Reset to default`,path:`/api/actions/set-login-code-message-template`,payload:()=>({template:``}),disabled:!p,onDone:()=>{s(``),u()}})}),(0,U.jsx)(`div`,{className:`gift-table-actions identity-save-row`,children:(0,U.jsx)(Z,{tone:`neutral`,label:`Save login-code message template`,path:`/api/actions/set-login-code-message-template`,payload:()=>({template:o}),disabled:h,onDone:()=>void u()})})]}):(0,U.jsx)(Zn,{label:`Loading login notification templates...`})]})}function pr({hasIcon:e,onClose:t,onDone:n,autoReason:r}){let[i,a]=(0,g.useState)(null),[o,s]=(0,g.useState)(``),[c,l]=(0,g.useState)(``),u=r??c,[d,f]=(0,g.useState)(!1),[p,m]=(0,g.useState)(``);(0,g.useEffect)(()=>{if(!i){s(``);return}let e=URL.createObjectURL(i);return s(e),()=>URL.revokeObjectURL(e)},[i]);async function h(){if(!i){m(`Choose an image file first.`);return}if(!u.trim()){m(`Please enter an operation reason`);return}f(!0),m(``);try{let e=new FormData;e.set(`metadata`,JSON.stringify({command_id:``,reason:u.trim(),confirm:!0})),e.set(`file`,i,i.name);let r=await k.uploadServerIcon(e);if(r.error){m(r.error);return}n(),t()}catch(e){m(O(e))}finally{f(!1)}}async function _(){if(!u.trim()){m(`Please enter an operation reason`);return}f(!0),m(``);try{let e=await k.action(`/api/actions/remove-server-icon`,{command_id:``,reason:u.trim(),confirm:!0});if(e.error){m(e.error);return}n(),t()}catch(e){m(O(e))}finally{f(!1)}}return(0,On.createPortal)((0,U.jsx)(`div`,{className:`modal-backdrop`,role:`presentation`,children:(0,U.jsxs)(`section`,{className:`modal command-modal`,role:`dialog`,"aria-modal":`true`,"aria-label":`Change server icon`,children:[(0,U.jsxs)(`div`,{className:`modal-head`,children:[(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`div`,{className:`eyebrow`,children:`Server identity`}),(0,U.jsx)(`h2`,{children:`Change server icon`})]}),(0,U.jsx)(`button`,{className:`icon-btn`,type:`button`,onClick:t,disabled:d,"aria-label":`Close`,children:(0,U.jsx)(It,{size:15})})]}),(0,U.jsxs)(`div`,{className:`command-body`,children:[(0,U.jsxs)(`label`,{className:`gift-file-picker ${i?`has-file`:``}`,children:[(0,U.jsx)(`input`,{type:`file`,accept:`.png,.jpg,.jpeg,.webp,.gif,image/png,image/jpeg,image/webp,image/gif`,onChange:e=>a(e.target.files?.[0]??null)}),o?(0,U.jsx)(`img`,{className:`gift-file-icon`,src:o,alt:``,style:{objectFit:`cover`}}):(0,U.jsx)(We,{size:22}),(0,U.jsxs)(`span`,{className:`gift-file-copy`,children:[(0,U.jsx)(`span`,{className:`gift-field-label`,children:`New icon`}),(0,U.jsx)(`strong`,{children:i?i.name:`Choose a PNG, JPEG, WebP, or GIF image`})]}),(0,U.jsx)(`span`,{className:`gift-file-action`,children:i?`Change file`:`Choose file`})]}),r===void 0&&(0,U.jsxs)(`label`,{className:`gift-reason-field`,children:[(0,U.jsx)(`span`,{children:`Audit reason`}),(0,U.jsx)(`input`,{value:c,placeholder:`Briefly describe why the server icon is changing`,onChange:e=>l(e.target.value)})]}),p&&(0,U.jsx)(q,{children:p})]}),(0,U.jsxs)(`div`,{className:`modal-actions`,children:[(0,U.jsx)(`button`,{className:`btn`,type:`button`,onClick:t,disabled:d,children:`Close`}),e&&(0,U.jsxs)(`button`,{className:`btn danger icon-text`,type:`button`,onClick:()=>void _(),disabled:d,children:[d?(0,U.jsx)(I,{className:`spin`,size:15}):(0,U.jsx)(Ot,{size:15}),`Remove icon`]}),(0,U.jsxs)(`button`,{className:`btn primary icon-text`,type:`button`,onClick:()=>void h(),disabled:d,children:[d?(0,U.jsx)(I,{className:`spin`,size:15}):(0,U.jsx)(At,{size:15}),`Upload icon`]})]})]})}),document.body)}function mr(){let[e,t]=(0,g.useState)([]),[n,r]=(0,g.useState)({}),[i,a]=(0,g.useState)({}),[o,s]=(0,g.useState)(``);async function c(){s(``);try{let e=await k.serverEnv();t(e);let n={};for(let t of e)for(let e of t.fields)n[e.key]=e.value;r(n)}catch(e){s(O(e))}}return(0,g.useEffect)(()=>{c()},[]),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(K,{title:`Environment (.env)`,text:`${(0,g.useMemo)(()=>e.reduce((e,t)=>e+t.fields.length,0),[e])} setting(s) across ${e.length} group(s). Changes take effect on the next Restart/Update.`}),o&&(0,U.jsx)(q,{children:o}),(0,U.jsx)(`div`,{className:`env-groups`,children:e.map(e=>{let t=!!i[e.title];return(0,U.jsxs)(`div`,{className:`env-group ${t?`open`:``}`,children:[(0,U.jsxs)(`button`,{className:`env-group-toggle`,type:`button`,"aria-expanded":t,onClick:()=>a(t=>({...t,[e.title]:!t[e.title]})),children:[(0,U.jsxs)(`span`,{className:`env-group-toggle-text`,children:[(0,U.jsx)(`span`,{className:`env-group-toggle-title`,children:e.title}),(0,U.jsx)(`span`,{className:`env-group-toggle-count`,children:`${e.fields.length} field${e.fields.length===1?``:`s`}`})]}),(0,U.jsx)(Ce,{size:16,className:`env-group-chevron`})]}),t&&(0,U.jsxs)(`div`,{className:`env-group-body`,children:[e.description&&(0,U.jsx)(`p`,{className:`env-group-desc`,children:e.description}),e.fields.map(e=>(0,U.jsxs)(`label`,{className:`form-field env-field`,children:[(0,U.jsx)(`span`,{className:`mono`,children:e.key}),e.description&&(0,U.jsx)(`span`,{className:`env-field-desc`,children:e.description}),(0,U.jsx)(`input`,{type:e.sensitive?`password`:`text`,value:n[e.key]??``,placeholder:e.default_value,onChange:t=>r(n=>({...n,[e.key]:t.target.value}))})]},e.key))]})]},e.title)})}),(0,U.jsx)(`div`,{className:`gift-table-actions env-save-row`,children:(0,U.jsx)(Z,{tone:`warn`,label:`Save .env changes`,path:`/api/actions/update-server-env`,payload:()=>({values:n}),onDone:()=>void c()})})]})}function hr(e){return new Promise(t=>setTimeout(t,e))}function gr(){let[e,t]=(0,g.useState)(!1),[n,r]=(0,g.useState)(!1),i=(0,g.useRef)(!1);return{waiting:e,timedOut:n,watch:(0,g.useCallback)(async(e=15e4,n)=>{i.current=!1,r(!1),t(!0);let a=``;try{a=(await k.session()).boot_id??``}catch{}let o=Date.now()+e;for(;Date.now()void 0),window.location.reload();return}}catch{}}t(!1),r(!0)},[]),dismiss:(0,g.useCallback)(()=>{i.current=!0,t(!1),r(!1)},[])}}function _r({timedOut:e,onDismiss:t}){return(0,On.createPortal)((0,U.jsx)(`div`,{className:`modal-backdrop`,role:`presentation`,children:(0,U.jsx)(`section`,{className:`modal command-modal restart-overlay`,role:`dialog`,"aria-modal":`true`,"aria-label":e?`Restart is taking longer than expected`:`Restarting`,children:e?(0,U.jsxs)(`div`,{className:`command-body restart-overlay-body`,children:[(0,U.jsx)(`div`,{className:`restart-overlay-badge warn`,children:(0,U.jsx)(ft,{size:26})}),(0,U.jsx)(`h2`,{className:`restart-overlay-heading`,children:`Still restarting...`}),(0,U.jsx)(q,{children:`The admin panel did not come back within the expected time. It may still be building/restarting -- reload manually in a bit, or check the server logs.`}),(0,U.jsxs)(`div`,{className:`gift-table-actions restart-overlay-actions`,children:[(0,U.jsx)(`button`,{className:`btn`,type:`button`,onClick:t,children:`Dismiss`}),(0,U.jsx)(`button`,{className:`btn primary`,type:`button`,onClick:()=>window.location.reload(),children:`Reload now`})]})]}):(0,U.jsxs)(`div`,{className:`command-body restart-overlay-body`,children:[(0,U.jsx)(`div`,{className:`restart-overlay-badge`,children:(0,U.jsx)(ft,{size:26,className:`restart-overlay-spin`})}),(0,U.jsx)(`h2`,{className:`restart-overlay-heading`,children:`Restarting`}),(0,U.jsx)(`div`,{className:`loader-bar restart-overlay-progress`})]})})}),document.body)}function vr(e){switch(e){case`good`:return(0,U.jsx)(F,{size:15});case`warn`:return(0,U.jsx)(I,{className:`spin`,size:15});case`danger`:return(0,U.jsx)(ne,{size:15});default:return(0,U.jsx)(we,{size:15})}}function Q({icon:e,name:t,tone:n,statusLabel:r,detail:i}){return(0,U.jsxs)(`div`,{className:`service-card tone-${n}`,children:[(0,U.jsx)(`div`,{className:`service-card-icon`,children:e}),(0,U.jsxs)(`div`,{className:`service-card-body`,children:[(0,U.jsx)(`div`,{className:`service-card-name`,children:t}),(0,U.jsx)(`div`,{className:`service-card-detail`,children:i??`\xA0`})]}),(0,U.jsxs)(`div`,{className:`service-card-status`,children:[vr(n),(0,U.jsx)(`span`,{children:r})]})]})}var yr={postgres:(0,U.jsx)(Oe,{size:18}),redis:(0,U.jsx)(re,{size:18}),minio:(0,U.jsx)(Ve,{size:18})};function br(e){let t=e.state.toLowerCase(),n=e.health.toLowerCase();return t!==`running`||n===`unhealthy`?`danger`:n===`starting`?`warn`:`good`}function xr(e){return e.state.toLowerCase()===`running`?e.health?e.health:`running`:e.state||`stopped`}var Sr=4e3;function Cr({onUpdateStarted:e}){let[t,n]=(0,g.useState)(null),[r,i]=(0,g.useState)(``),[a,o]=(0,g.useState)(!1),[s,c]=(0,g.useState)(``),l=(0,g.useCallback)(async()=>{o(!0),c(``),i(``);try{let e=await k.checkServerUpdates();n(e.commits_behind),i(e.commits_behind>0?`${e.commits_behind} new commit${e.commits_behind===1?``:`s`} pulled from GitHub.`:`Already up to date.`)}catch(e){c(O(e))}finally{o(!1)}},[]);(0,g.useEffect)(()=>{l()},[l]);let u=t??0;return u>0?(0,U.jsx)(Z,{compact:!0,tone:`danger`,icon:(0,U.jsx)(Ae,{size:15}),label:`Update (${u})`,path:`/api/actions/update-server`,payload:()=>({}),onDone:e}):(0,U.jsxs)(`button`,{className:`btn compact-btn icon-text`,type:`button`,disabled:a,title:s||r||void 0,onClick:()=>void l(),children:[a?(0,U.jsx)(I,{className:`spin`,size:15}):(0,U.jsx)(ft,{size:15}),`Check updates`]})}function wr(){let[e,t]=(0,g.useState)(null),[n,r]=(0,g.useState)(``),[i,a]=(0,g.useState)(null),[o,s]=(0,g.useState)(``),c=gr(),l=(0,g.useRef)(!1);l.current=c.waiting;let u=(0,g.useCallback)(async()=>{if(!l.current){try{t(await k.serverStatus()),r(``)}catch(e){r(O(e))}try{a(await k.dockerStatus()),s(``)}catch(e){s(O(e))}}},[]);return(0,g.useEffect)(()=>{u();let e=window.setInterval(()=>void u(),Sr);return()=>window.clearInterval(e)},[u]),(0,U.jsxs)(U.Fragment,{children:[(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(K,{title:`Services`,action:(0,U.jsxs)(`div`,{className:`services-header-actions`,children:[(0,U.jsx)(Cr,{onUpdateStarted:()=>void c.watch()}),(0,U.jsx)(Z,{compact:!0,tone:`primary`,label:`Restart`,path:`/api/actions/restart-server`,payload:()=>({}),onDone:()=>void c.watch()})]})}),n&&(0,U.jsx)(q,{children:n}),o&&(0,U.jsx)(q,{children:o}),e===null&&i===null&&!n&&!o?(0,U.jsx)(Zn,{label:`Loading service status...`}):(0,U.jsxs)(`div`,{className:`service-grid`,children:[e&&(0,U.jsxs)(U.Fragment,{children:[(0,U.jsx)(Q,{icon:(0,U.jsx)(gt,{size:18}),name:`Server`,tone:e.ServerAlive?`good`:`danger`,statusLabel:e.ServerAlive?`running`:`stopped`,detail:e.ServerAlive?`pid ${e.ServerPID}`:void 0}),(0,U.jsx)(Q,{icon:(0,U.jsx)(xt,{size:18}),name:`admin panel`,tone:e.AdminAlive?`good`:`danger`,statusLabel:e.AdminAlive?`running`:`stopped`,detail:e.AdminAlive?`pid ${e.AdminPID}`:void 0})]}),i?.map(e=>(0,U.jsx)(Q,{icon:yr[e.name]??(0,U.jsx)(Oe,{size:18}),name:e.name,tone:br(e),statusLabel:xr(e),detail:e.state},e.name))]})]}),c.waiting&&(0,U.jsx)(_r,{timedOut:!1,onDismiss:c.dismiss}),c.timedOut&&(0,U.jsx)(_r,{timedOut:!0,onDismiss:c.dismiss})]})}var Tr=`Set from the first-run setup wizard`,Er=[`welcome`,`identity`,`network`,`botapi`,`account`,`done`],Dr={welcome:`Welcome`,identity:`Identity`,network:`Network`,botapi:`Bot API`,account:`Account`,done:`Done`};function Or(){let[e,t]=(0,g.useState)(`welcome`);function n(e){t(e)}return(0,U.jsxs)(`main`,{className:`login-page setup-wizard-page`,children:[(0,U.jsx)(ir,{}),(0,U.jsxs)(`section`,{className:`login-panel setup-wizard-panel`,children:[(0,U.jsxs)(`div`,{className:`login-head`,children:[(0,U.jsx)(`div`,{className:`brand brand-elevated`,children:(0,U.jsxs)(`span`,{children:[(0,U.jsx)(`strong`,{children:`Let's set up your server`}),(0,U.jsx)(`small`,{children:`First-run setup`})]})}),(0,U.jsx)(`div`,{className:`login-head-actions`,children:(0,U.jsx)(En,{})})]}),(0,U.jsx)(`div`,{className:`wizard-steps`,children:Er.map((t,n)=>{let r=Er.indexOf(e);return(0,U.jsxs)(`div`,{className:`command-step ${n===r?`active`:nn(`identity`)}),e===`identity`&&(0,U.jsx)(jr,{onNext:()=>n(`network`)}),e===`network`&&(0,U.jsx)(Nr,{onNext:()=>n(`botapi`)}),e===`botapi`&&(0,U.jsx)(Lr,{onNext:()=>n(`account`)}),e===`account`&&(0,U.jsx)(Rr,{onNext:()=>n(`done`)}),e===`done`&&(0,U.jsx)(zr,{})]})]})}function kr({children:e}){return(0,U.jsx)(`div`,{className:`wizard-actions`,children:e})}function Ar({onNext:e}){return(0,U.jsxs)(`div`,{className:`wizard-step-body`,children:[(0,U.jsx)(`p`,{className:`wizard-welcome-greeting`,children:`Hi!`}),(0,U.jsxs)(`p`,{children:[`Let's get your server set up -- a name, an address for clients, and an account of `,`your own. Takes about a minute, and everything here stays editable later.`]}),(0,U.jsx)(kr,{children:(0,U.jsxs)(`button`,{className:`btn primary icon-text`,type:`button`,onClick:e,children:[`Get started`,` `,(0,U.jsx)(me,{size:15})]})})]})}function jr({onNext:e}){let[t,n]=(0,g.useState)(``),[r,i]=(0,g.useState)(``),[a,o]=(0,g.useState)(void 0),[s,c]=(0,g.useState)(!1),[l,u]=(0,g.useState)(0),[d,f]=(0,g.useState)(!1),[p,m]=(0,g.useState)(!1),[h,_]=(0,g.useState)(``);(0,g.useEffect)(()=>{let e=!1;return k.serverIdentity().then(t=>{e||(n(t.name),i(t.description),o(t.icon_ext),f(!0))}).catch(t=>{e||(_(O(t)),f(!0))}),()=>{e=!0}},[]);async function v(){m(!0),_(``);try{let n=await k.action(`/api/actions/set-server-identity`,{command_id:``,reason:Tr,confirm:!0,name:t,description:r});if(n.error){_(n.error);return}e()}catch(e){_(O(e))}finally{m(!1)}}return(0,U.jsxs)(`div`,{className:`wizard-step-body`,children:[(0,U.jsx)(`p`,{className:`wizard-step-hint`,children:`Shown to clients when they add this server, and in the sidebar here.`}),h&&(0,U.jsx)(q,{children:h}),(0,U.jsxs)(`div`,{className:`wizard-identity-row`,children:[(0,U.jsxs)(`div`,{className:`avatar-edit-slot`,children:[a?(0,U.jsx)(`img`,{className:`avatar-photo-img`,src:k.serverIconURL()+`&b=${l}`,alt:``,style:{width:72,height:72}}):(0,U.jsx)(`div`,{className:`avatar-fallback server-icon-fallback`,style:{width:72,height:72},children:(0,U.jsx)(We,{size:22})}),(0,U.jsx)(`button`,{className:`icon-btn avatar-edit-btn`,type:`button`,"aria-label":`Add server icon`,onClick:()=>c(!0),children:(0,U.jsx)(We,{size:13})})]}),(0,U.jsxs)(`div`,{className:`server-identity-fields`,children:[(0,U.jsxs)(`label`,{className:`form-field`,children:[(0,U.jsx)(`span`,{children:`Name`}),(0,U.jsx)(`input`,{value:t,maxLength:128,placeholder:`OwpenGram`,disabled:!d,onChange:e=>n(e.target.value)})]}),(0,U.jsxs)(`label`,{className:`form-field`,children:[(0,U.jsx)(`span`,{children:`Description`}),(0,U.jsx)(`textarea`,{rows:2,value:r,maxLength:512,disabled:!d,onChange:e=>i(e.target.value)})]})]})]}),(0,U.jsx)(kr,{children:(0,U.jsxs)(`button`,{className:`btn primary icon-text`,type:`button`,disabled:p||!d,onClick:()=>void v(),children:[p?(0,U.jsx)(I,{className:`spin`,size:15}):(0,U.jsx)(me,{size:15}),`Continue`]})}),s&&(0,U.jsx)(pr,{hasIcon:!!a,autoReason:Tr,onClose:()=>c(!1),onDone:()=>{u(e=>e+1),k.serverIdentity().then(e=>o(e.icon_ext))}})]})}var Mr=[{key:`TELESRV_ADVERTISE_IP`,label:`Server public IP or hostname`,hint:`What clients connect to. Fine to leave as 127.0.0.1 for local testing.`,placeholder:`127.0.0.1`},{key:`TELESRV_PUBLIC_BASE_URL`,label:`Public base URL`,hint:`Used for links this server generates -- invites, sticker packs. e.g. https://example.com`,placeholder:`http://127.0.0.1:2401`},{key:`TELESRV_PUBLIC_APP_SCHEME`,label:`Custom app link scheme`,hint:`Must match what your client builds were compiled with.`,placeholder:`owpg`}];function Nr({onNext:e}){let[t,n]=(0,g.useState)({}),[r,i]=(0,g.useState)(!1),[a,o]=(0,g.useState)(!1),[s,c]=(0,g.useState)(``);(0,g.useEffect)(()=>{let e=!1;return k.serverEnv().then(t=>{if(e)return;let r={};for(let e of t)for(let t of e.fields)Mr.some(e=>e.key===t.key)&&(r[t.key]=t.value);n(r),i(!0)}).catch(t=>{e||(c(O(t)),i(!0))}),()=>{e=!0}},[]);async function l(){o(!0),c(``);try{let n=await k.action(`/api/actions/update-server-env`,{command_id:``,reason:Tr,confirm:!0,values:t});if(n.error){c(n.error);return}e()}catch(e){c(O(e))}finally{o(!1)}}return(0,U.jsxs)(`div`,{className:`wizard-step-body`,children:[(0,U.jsx)(`p`,{className:`wizard-step-hint`,children:`Takes effect once setup finishes below -- that last step restarts the server.`}),s&&(0,U.jsx)(q,{children:s}),Mr.map(e=>(0,U.jsxs)(`label`,{className:`form-field env-field`,children:[(0,U.jsx)(`span`,{children:e.label}),(0,U.jsx)(`span`,{className:`env-field-desc`,children:e.hint}),(0,U.jsx)(`input`,{value:t[e.key]??``,placeholder:e.placeholder,disabled:!r,onChange:t=>n(n=>({...n,[e.key]:t.target.value}))})]},e.key)),(0,U.jsx)(kr,{children:(0,U.jsxs)(`button`,{className:`btn primary icon-text`,type:`button`,disabled:a||!r,onClick:()=>void l(),children:[a?(0,U.jsx)(I,{className:`spin`,size:15}):(0,U.jsx)(me,{size:15}),`Continue`]})})]})}var Pr=`TELESRV_BOT_API_ADDR`,Fr=`127.0.0.1:2500`;function Ir(e){let t=e.trim();if(t===``)return`Enter an address like 127.0.0.1:2500.`;let n=t.lastIndexOf(`:`);if(n<0)return`Include a port, for example 127.0.0.1:2500.`;let r=Number(t.slice(n+1));return!Number.isInteger(r)||r<1||r>65535?`Port must be a whole number between 1 and 65535.`:``}function Lr({onNext:e}){let[t,n]=(0,g.useState)(!1),[r,i]=(0,g.useState)(``),[a,o]=(0,g.useState)(!1),[s,c]=(0,g.useState)(!1),[l,u]=(0,g.useState)(``);(0,g.useEffect)(()=>{let e=!1;return k.serverEnv().then(t=>{if(!e){for(let e of t)for(let t of e.fields)t.key===Pr&&(n(t.value.trim()!==``),i(t.value.trim()));o(!0)}}).catch(t=>{e||(u(O(t)),o(!0))}),()=>{e=!0}},[]);let d=t?Ir(r):``;async function f(){if(!d){c(!0),u(``);try{let n=await k.action(`/api/actions/update-server-env`,{command_id:``,reason:Tr,confirm:!0,values:{[Pr]:t?r.trim():``}});if(n.error){u(n.error);return}e()}catch(e){u(O(e))}finally{c(!1)}}}return(0,U.jsxs)(`div`,{className:`wizard-step-body`,children:[(0,U.jsxs)(`p`,{className:`wizard-step-hint`,children:[`An HTTP gateway that lets bot libraries -- python-telegram-bot, aiogram and friends -- `,`talk to this server. Leave it off if you are not running bots; you can turn it on later in Server Settings.`]}),l&&(0,U.jsx)(q,{children:l}),(0,U.jsxs)(`label`,{className:`checkline`,children:[(0,U.jsx)(`input`,{type:`checkbox`,checked:t,disabled:!a,onChange:e=>{let t=e.target.checked;n(t),t&&r.trim()===``&&i(Fr)}}),` Enable the Bot API gateway`]}),t&&(0,U.jsxs)(`label`,{className:`form-field env-field`,children:[(0,U.jsx)(`span`,{children:`Listen address`}),(0,U.jsxs)(`span`,{className:`env-field-desc`,children:[`Keep 127.0.0.1 to accept only local bots; use 0.0.0.0 to expose it. `,`The server will refuse to start if this port is already taken.`]}),(0,U.jsx)(`input`,{value:r,placeholder:Fr,disabled:!a,spellCheck:!1,autoCapitalize:`none`,onChange:e=>i(e.target.value)}),d&&(0,U.jsx)(`span`,{className:`env-field-desc`,children:d})]}),(0,U.jsx)(kr,{children:(0,U.jsxs)(`button`,{className:`btn primary icon-text`,type:`button`,disabled:s||!a||d!==``,onClick:()=>void f(),children:[s?(0,U.jsx)(I,{className:`spin`,size:15}):(0,U.jsx)(me,{size:15}),`Continue`]})})]})}function Rr({onNext:e}){let[t,n]=(0,g.useState)(``),[r,i]=(0,g.useState)(``),[a,o]=(0,g.useState)(!1),[s,c]=(0,g.useState)(``),l=t.trim().length<3||r.trim()===``;async function u(){if(!l){o(!0),c(``);try{let n=await k.action(`/api/actions/create-admin-operator`,{command_id:``,reason:Tr,confirm:!0,username:t.trim(),password:r,permissions:[`*`],enabled:!0});if(n.error){c(n.error);return}e()}catch(e){c(O(e))}finally{o(!1)}}}return(0,U.jsxs)(`div`,{className:`wizard-step-body`,children:[(0,U.jsx)(`p`,{className:`wizard-step-hint`,children:`Replace the generated password with a login of your own.`}),s&&(0,U.jsx)(q,{children:s}),(0,U.jsxs)(`label`,{className:`form-field`,children:[(0,U.jsx)(`span`,{children:`Username`}),(0,U.jsx)(`input`,{autoFocus:!0,value:t,spellCheck:!1,autoCapitalize:`none`,placeholder:`letters, digits, dot, dash or underscore`,onChange:e=>n(e.target.value)})]}),(0,U.jsxs)(`label`,{className:`form-field`,children:[(0,U.jsx)(`span`,{children:`Password`}),(0,U.jsx)(`input`,{type:`password`,value:r,autoComplete:`new-password`,onChange:e=>i(e.target.value)})]}),(0,U.jsxs)(kr,{children:[(0,U.jsx)(`button`,{className:`btn`,type:`button`,onClick:e,children:`Skip for now`}),(0,U.jsxs)(`button`,{className:`btn primary icon-text`,type:`button`,disabled:a||l,onClick:()=>void u(),children:[a?(0,U.jsx)(I,{className:`spin`,size:15}):(0,U.jsx)(Mt,{size:15}),`Create account & continue`]})]})]})}function zr(){let[e,t]=(0,g.useState)(!1),[n,r]=(0,g.useState)(``),i=gr();async function a(){t(!0),r(``);try{let e=await k.action(`/api/actions/complete-setup`,{command_id:``,reason:Tr,confirm:!0});if(e.error){r(e.error),t(!1);return}let n=await k.action(`/api/actions/restart-server`,{command_id:``,reason:Tr,confirm:!0});if(n.error){r(n.error),t(!1);return}i.watch(15e4,{beforeReload:async()=>{await k.logout()}})}catch(e){r(O(e)),t(!1)}}return(0,U.jsxs)(`div`,{className:`wizard-step-body`,children:[(0,U.jsxs)(`p`,{children:[`That's the essentials. Finishing restarts the server so the network and Bot API settings from `,`the earlier steps take effect. Everything here stays editable from Server Settings and Operators any time.`]}),n&&(0,U.jsx)(q,{children:n}),(0,U.jsx)(kr,{children:(0,U.jsxs)(`button`,{className:`btn primary icon-text`,type:`button`,disabled:e,onClick:()=>void a(),children:[e?(0,U.jsx)(I,{className:`spin`,size:15}):(0,U.jsx)(pt,{size:15}),`Finish setup & restart`]})}),i.waiting&&(0,U.jsx)(_r,{timedOut:!1,onDismiss:i.dismiss}),i.timedOut&&(0,U.jsx)(_r,{timedOut:!0,onDismiss:i.dismiss})]})}function Br({onLogin:e}){let[t,n]=(0,g.useState)(``),[r,i]=(0,g.useState)(``),[a,o]=(0,g.useState)(0),[s,c]=(0,g.useState)(``),[l,u]=(0,g.useState)(!1),d=(0,g.useRef)(null),f=(0,g.useRef)(null),[p,m]=(0,g.useState)(null),[h,_]=(0,g.useState)(!1);(0,g.useEffect)(()=>{k.publicBranding().then(m).catch(()=>void 0)},[]),(0,g.useEffect)(()=>{a===1?f.current?.focus({preventScroll:!0}):d.current?.focus({preventScroll:!0})},[a]);let v=p?.name?.trim()||`OwpenGram`,y=p?.has_icon&&!h?k.publicIconURL():`/logo.png`;function b(){t.trim()&&(c(``),o(1))}function x(){c(``),o(0)}async function S(n){if(n.preventDefault(),a===0){b();return}u(!0),c(``);try{let n=await k.login(r,t);try{e(await k.session())}catch{e({actor:n.actor,permissions:n.permissions??[]})}}catch(e){c(O(e))}finally{u(!1)}}return(0,U.jsxs)(`main`,{className:`login-page`,children:[(0,U.jsx)(ir,{}),(0,U.jsxs)(`section`,{className:`login-panel`,children:[(0,U.jsxs)(`div`,{className:`login-head`,children:[(0,U.jsxs)(`div`,{className:`brand brand-elevated`,children:[(0,U.jsx)(`span`,{className:`brand-mark`,children:(0,U.jsx)(`img`,{src:y,alt:v,onError:()=>_(!0)})}),(0,U.jsxs)(`span`,{children:[(0,U.jsx)(`strong`,{children:v}),(0,U.jsx)(`small`,{children:`Admin Console`})]})]}),(0,U.jsx)(`div`,{className:`login-head-actions`,children:(0,U.jsx)(En,{})})]}),s&&(0,U.jsx)(q,{children:s}),(0,U.jsxs)(`form`,{className:`form-stack`,onSubmit:S,children:[(0,U.jsx)(`div`,{className:`login-wizard`,children:(0,U.jsxs)(`div`,{className:`login-wizard-track`,style:{transform:`translateX(-${a*100}%)`},children:[(0,U.jsx)(`div`,{className:`login-wizard-step`,"aria-hidden":a!==0,children:(0,U.jsxs)(`label`,{children:[(0,U.jsx)(`span`,{children:`Username`}),(0,U.jsx)(`input`,{ref:d,type:`text`,value:t,autoComplete:`username`,spellCheck:!1,autoCapitalize:`none`,placeholder:`login`,tabIndex:a===0?void 0:-1,onChange:e=>n(e.target.value)})]})}),(0,U.jsx)(`div`,{className:`login-wizard-step`,"aria-hidden":a!==1,children:(0,U.jsxs)(`label`,{children:[(0,U.jsx)(`span`,{children:`Password`}),(0,U.jsx)(`input`,{ref:f,type:`password`,value:r,autoComplete:`current-password`,placeholder:`password`,tabIndex:a===1?void 0:-1,onChange:e=>i(e.target.value)})]})})]})}),a===0?(0,U.jsx)(`button`,{className:`btn primary full`,type:`submit`,disabled:!t.trim(),children:`Next`}):(0,U.jsxs)(`div`,{className:`login-wizard-actions`,children:[(0,U.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:x,children:[(0,U.jsx)(pe,{size:15}),`Back`]}),(0,U.jsxs)(`button`,{className:`btn primary`,type:`submit`,disabled:l,children:[l?(0,U.jsx)(I,{className:`spin`,size:15}):null,l?`Logging in`:`Log in`]})]})]})]})]})}function Vr({kind:e,id:t,onClose:n,onDone:r}){let[i,a]=(0,g.useState)(null),[o,s]=(0,g.useState)(``),[c,l]=(0,g.useState)(`0`),[u,d]=(0,g.useState)(``),[f,p]=(0,g.useState)(!1),[m,h]=(0,g.useState)(``),_=e===`user`&&!!i&&i.type.startsWith(`video/`);(0,g.useEffect)(()=>{if(!i){s(``);return}let e=URL.createObjectURL(i);return s(e),()=>URL.revokeObjectURL(e)},[i]);async function v(){if(!i){h(`Choose an image or video file first.`);return}if(!u.trim()){h(`Please enter an operation reason`);return}p(!0),h(``);try{let a=e===`channel`?`channel_id`:`user_id`,o=new FormData,s={command_id:``,reason:u.trim(),confirm:!0,[a]:t};_&&(s.video_start_ts=Number(c)||0),o.set(`metadata`,JSON.stringify(s)),o.set(`file`,i,i.name);let l=e===`channel`?await k.setChannelAvatar(o):_?await k.setAccountAvatarVideo(o):await k.setAccountAvatar(o);if(l.error){h(l.error);return}r(),n()}catch(e){h(O(e))}finally{p(!1)}}return(0,On.createPortal)((0,U.jsx)(`div`,{className:`modal-backdrop`,role:`presentation`,children:(0,U.jsxs)(`section`,{className:`modal command-modal`,role:`dialog`,"aria-modal":`true`,"aria-label":`Change avatar`,children:[(0,U.jsxs)(`div`,{className:`modal-head`,children:[(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`div`,{className:`eyebrow`,children:e===`channel`?`Channel`:`Account`}),(0,U.jsx)(`h2`,{children:`Change avatar`})]}),(0,U.jsx)(`button`,{className:`icon-btn`,type:`button`,onClick:n,disabled:f,"aria-label":`Close`,children:(0,U.jsx)(It,{size:15})})]}),(0,U.jsxs)(`div`,{className:`command-body`,children:[(0,U.jsxs)(`label`,{className:`gift-file-picker ${i?`has-file`:``}`,children:[(0,U.jsx)(`input`,{type:`file`,accept:e===`user`?`image/png,image/jpeg,image/webp,video/mp4`:`image/png,image/jpeg,image/webp`,onChange:e=>a(e.target.files?.[0]??null)}),o?_?(0,U.jsx)(`video`,{className:`gift-file-icon`,src:o,style:{objectFit:`cover`},muted:!0,loop:!0,autoPlay:!0}):(0,U.jsx)(`img`,{className:`gift-file-icon`,src:o,alt:``,style:{objectFit:`cover`}}):(0,U.jsx)(We,{size:22}),(0,U.jsxs)(`span`,{className:`gift-file-copy`,children:[(0,U.jsx)(`span`,{className:`gift-field-label`,children:`New avatar`}),(0,U.jsx)(`strong`,{children:i?i.name:e===`user`?`Choose a JPEG, PNG, WebP image, or MP4 video`:`Choose a JPEG, PNG, or WebP image`})]}),(0,U.jsx)(`span`,{className:`gift-file-action`,children:i?`Change file`:`Choose file`})]}),_&&(0,U.jsxs)(`label`,{className:`gift-reason-field`,children:[(0,U.jsx)(`span`,{children:`Video start (seconds)`}),(0,U.jsx)(`input`,{type:`number`,min:`0`,step:`0.1`,value:c,onChange:e=>l(e.target.value)})]}),(0,U.jsxs)(`label`,{className:`gift-reason-field`,children:[(0,U.jsx)(`span`,{children:`Audit reason`}),(0,U.jsx)(`input`,{value:u,placeholder:`Briefly describe why this avatar is being changed`,onChange:e=>d(e.target.value)})]}),m&&(0,U.jsx)(q,{children:m})]}),(0,U.jsxs)(`div`,{className:`modal-actions`,children:[(0,U.jsx)(`button`,{className:`btn`,type:`button`,onClick:n,disabled:f,children:`Close`}),(0,U.jsxs)(`button`,{className:`btn primary`,type:`button`,onClick:v,disabled:f,children:[f?(0,U.jsx)(I,{className:`spin`,size:15}):(0,U.jsx)(At,{size:15}),`Upload avatar`]})]})]})}),document.body)}var Hr=[[`#FF885E`,`#FF516A`],[`#FFCD6A`,`#FFA85C`],[`#82B1FF`,`#665FFF`],[`#A0DE7E`,`#54CB68`],[`#53EDD6`,`#28C9B7`],[`#72D5FD`,`#2A9EF1`],[`#E0A2F3`,`#D669ED`]];function Ur(e){return Hr[Math.abs(e)%Hr.length]}function Wr(e){let t=Array.from(e);return t.length>0?t[0]:``}function Gr(e,t,n){let r=`${e} ${t}`.trim().split(/\s+/).filter(Boolean),i=r.length>0?r:n?[n]:[];if(i.length===0)return`T`;let a=Wr(i[0]);return i.length>1&&(a+=Wr(i[i.length-1])),a.toUpperCase()}function Kr({id:e,kind:t=`user`,firstName:n=``,lastName:r=``,username:i=``,title:a=``,size:o=34,refreshKey:s}){let[c,l]=(0,g.useState)(!1);if((0,g.useEffect)(()=>{l(!1)},[e,t,s]),c){let[s,c]=Ur(e);return(0,U.jsx)(`div`,{className:`avatar-fallback`,style:{width:o,height:o,background:`linear-gradient(135deg, ${s}, ${c})`,fontSize:Math.round(o*.42)},children:t===`channel`?Gr(a,``,i):Gr(n,r,i)})}return(0,U.jsx)(`img`,{className:`avatar-photo-img`,src:`${t===`channel`?`/api/channels/${e}/avatar`:`/api/accounts/${e}/avatar`}${s===void 0?``:`?v=${encodeURIComponent(String(s))}`}`,alt:``,loading:`lazy`,style:{width:o,height:o},onError:()=>l(!0)})}function qr({rows:e,userID:t,onDone:n}){let[r,i]=(0,g.useState)(()=>new Set);(0,g.useEffect)(()=>{i(new Set)},[t]);let a=(0,g.useMemo)(()=>e.filter(e=>!r.has(e.Hash)),[e,r]);function o(e){i(t=>e(t)),n()}return(0,U.jsxs)(`div`,{className:`authorization-block`,children:[(0,U.jsx)(`div`,{className:`table-wrap`,children:(0,U.jsxs)(`table`,{className:`data-table authorization-table`,children:[(0,U.jsx)(`thead`,{children:(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`th`,{children:`Device`}),(0,U.jsx)(`th`,{children:`Platform`}),(0,U.jsx)(`th`,{children:`IP`}),(0,U.jsx)(`th`,{children:`Last active`}),(0,U.jsx)(`th`,{className:`device-actions-head`,children:`Actions`})]})}),(0,U.jsxs)(`tbody`,{children:[a.map(n=>(0,U.jsxs)(`tr`,{children:[(0,U.jsxs)(`td`,{className:`device-text`,children:[n.DeviceModel,` `,n.SystemVersion]}),(0,U.jsxs)(`td`,{className:`device-text`,children:[n.Platform,` `,n.AppVersion]}),(0,U.jsx)(`td`,{children:n.IP}),(0,U.jsx)(`td`,{children:G(n.ActiveAt)}),(0,U.jsx)(`td`,{className:`device-actions-cell`,children:(0,U.jsxs)(`div`,{className:`device-actions`,children:[(0,U.jsx)(Z,{label:`Revoke current`,icon:(0,U.jsx)(Ze,{size:13}),compact:!0,path:`/api/actions/revoke-sessions`,payload:()=>({user_id:t,hash:n.Hash}),onDone:()=>o(e=>new Set([...e,n.Hash]))}),(0,U.jsx)(Z,{label:`Keep current`,icon:(0,U.jsx)(xt,{size:13}),compact:!0,path:`/api/actions/revoke-sessions`,payload:()=>({user_id:t,keep_hash:n.Hash}),onDone:()=>o(()=>new Set(e.filter(e=>e.Hash!==n.Hash).map(e=>e.Hash)))})]})})]},n.Hash)),a.length===0&&(0,U.jsx)(Yn,{colSpan:5})]})]})}),(0,U.jsx)(`div`,{className:`danger-zone`,children:(0,U.jsx)(Z,{label:`Revoke all devices`,icon:(0,U.jsx)(xe,{size:15}),path:`/api/actions/revoke-sessions`,payload:()=>({user_id:t,revoke_all:!0}),onDone:()=>o(()=>new Set(e.map(e=>e.Hash)))})})]})}function Jr({scam:e,fake:t}){return!e&&!t?null:(0,U.jsxs)(U.Fragment,{children:[e&&(0,U.jsx)(J,{tone:`danger`,children:`SCAM`}),t&&(0,U.jsx)(J,{tone:`danger`,children:`FAKE`})]})}function Yr({idKey:e,id:t,path:n,scam:r,fake:i,onDone:a}){return(0,U.jsxs)(`div`,{className:`action-stack`,children:[(0,U.jsx)(Z,{label:r?`Clear SCAM`:`Mark as SCAM`,icon:(0,U.jsx)(bt,{size:15}),tone:`danger`,path:n,payload:()=>({[e]:t,scam:!r,fake:r?i:!1}),onDone:a}),(0,U.jsx)(Z,{label:i?`Clear FAKE`:`Mark as FAKE`,icon:(0,U.jsx)(oe,{size:15}),tone:`danger`,path:n,payload:()=>({[e]:t,fake:!i,scam:i?r:!1}),onDone:a})]})}function Xr({id:e,support:t,onDone:n}){return(0,U.jsx)(Z,{label:t?`Clear support`:`Mark as support`,icon:(0,U.jsx)(qe,{size:15}),tone:`neutral`,path:`/api/actions/set-support`,payload:()=>({user_id:e,support:!t}),onDone:n})}function Zr({idKey:e,id:t,path:n,current:r,onDone:i}){let[a,o]=(0,g.useState)(r.replace(/^@/,``));return(0,U.jsxs)(`div`,{className:`attr-block`,children:[(0,U.jsxs)(`label`,{className:`duration-field`,children:[(0,U.jsx)(`span`,{children:`Username`}),(0,U.jsx)(`input`,{value:a,onChange:e=>o(e.target.value),placeholder:`username`})]}),(0,U.jsx)(Z,{label:`Set username`,icon:(0,U.jsx)(_e,{size:15}),tone:`neutral`,path:n,payload:()=>({[e]:t,username:a.trim().replace(/^@/,``)}),onDone:i})]})}function Qr({id:e,path:t,currentFirstName:n,currentLastName:r,onDone:i}){let[a,o]=(0,g.useState)(n),[s,c]=(0,g.useState)(r);return(0,U.jsxs)(`div`,{className:`attr-block`,children:[(0,U.jsxs)(`label`,{className:`duration-field`,children:[(0,U.jsx)(`span`,{children:`First name`}),(0,U.jsx)(`input`,{value:a,onChange:e=>o(e.target.value),placeholder:`First name`})]}),(0,U.jsxs)(`label`,{className:`duration-field`,children:[(0,U.jsx)(`span`,{children:`Last name`}),(0,U.jsx)(`input`,{value:s,onChange:e=>c(e.target.value),placeholder:`Last name`})]}),(0,U.jsx)(Z,{label:`Set name`,icon:(0,U.jsx)(le,{size:15}),tone:`neutral`,path:t,payload:()=>({user_id:e,first_name:a.trim(),last_name:s.trim()}),onDone:i})]})}function $r({id:e,path:t,current:n,onDone:r}){let[i,a]=(0,g.useState)(n);return(0,U.jsxs)(`div`,{className:`attr-block`,children:[(0,U.jsxs)(`label`,{className:`duration-field`,children:[(0,U.jsx)(`span`,{children:`Phone number`}),(0,U.jsx)(`input`,{value:i,onChange:e=>a(e.target.value),placeholder:`15551234567`})]}),(0,U.jsx)(Z,{label:`Set phone`,icon:(0,U.jsx)(ot,{size:15}),tone:`warn`,path:t,payload:()=>({user_id:e,phone:i.trim()}),onDone:r})]})}function ei({id:e,path:t,current:n,onDone:r}){let[i,a]=(0,g.useState)(n);return(0,U.jsxs)(`div`,{className:`attr-block`,children:[(0,U.jsxs)(`label`,{className:`duration-field`,children:[(0,U.jsx)(`span`,{children:`Login email`}),(0,U.jsx)(`input`,{value:i,onChange:e=>a(e.target.value),placeholder:`name@example.com (empty clears it)`,type:`email`})]}),(0,U.jsx)(Z,{label:i.trim()?`Set login email`:`Clear login email`,icon:(0,U.jsx)(Qe,{size:15}),tone:`warn`,path:t,payload:()=>({user_id:e,email:i.trim()}),onDone:r})]})}function ti({idKey:e,id:t,path:n,onDone:r}){let[i,a]=(0,g.useState)(!1),[o,s]=(0,g.useState)(!0),[c,l]=(0,g.useState)(`0`),[u,d]=(0,g.useState)(``);return(0,U.jsxs)(`div`,{className:`attr-block`,children:[(0,U.jsxs)(`label`,{className:`checkline`,children:[(0,U.jsx)(`input`,{type:`checkbox`,checked:i,onChange:e=>a(e.target.checked)}),` `,`Profile color`]}),(0,U.jsxs)(`label`,{className:`checkline`,children:[(0,U.jsx)(`input`,{type:`checkbox`,checked:o,onChange:e=>s(e.target.checked)}),` `,`Enable color`]}),(0,U.jsxs)(`label`,{className:`duration-field`,children:[(0,U.jsx)(`span`,{children:`Color index`}),(0,U.jsx)(`input`,{type:`number`,min:`0`,max:`20`,value:c,onChange:e=>l(e.target.value)})]}),(0,U.jsxs)(`label`,{className:`duration-field`,children:[(0,U.jsx)(`span`,{children:`Background emoji ID`}),(0,U.jsx)(`input`,{value:u,onChange:e=>d(e.target.value),placeholder:`0`})]}),(0,U.jsx)(Z,{label:`Set color`,icon:(0,U.jsx)(at,{size:15}),tone:`neutral`,path:n,payload:()=>({[e]:t,for_profile:i,has_color:o,color:Fn(c),background_emoji_id:u.trim()||`0`}),onDone:r})]})}function ni({idKey:e,id:t,path:n,onDone:r}){let[i,a]=(0,g.useState)(``),[o,s]=(0,g.useState)(`0`);return(0,U.jsxs)(`div`,{className:`attr-block`,children:[(0,U.jsxs)(`label`,{className:`duration-field`,children:[(0,U.jsx)(`span`,{children:`Emoji document ID`}),(0,U.jsx)(`input`,{value:i,onChange:e=>a(e.target.value),placeholder:`0 = clear`})]}),(0,U.jsxs)(`label`,{className:`duration-field`,children:[(0,U.jsx)(`span`,{children:`Until (unix, 0 = permanent)`}),(0,U.jsx)(`input`,{type:`number`,min:`0`,value:o,onChange:e=>s(e.target.value)})]}),(0,U.jsx)(Z,{label:`Set emoji status`,icon:(0,U.jsx)(wt,{size:15}),tone:`neutral`,path:n,payload:()=>({[e]:t,document_id:i.trim()||`0`,until:Fn(o)}),onDone:r})]})}function ri({channel:e,onDone:t}){let[n,r]=(0,g.useState)(e.Gigagroup),[i,a]=(0,g.useState)(e.AntiSpam),[o,s]=(0,g.useState)(e.ParticipantsHidden),[c,l]=(0,g.useState)(e.NoForwards),[u,d]=(0,g.useState)(e.JoinToSend),[f,p]=(0,g.useState)(e.JoinRequest),[m,h]=(0,g.useState)(String(e.SlowmodeSeconds));(0,g.useEffect)(()=>{r(e.Gigagroup),a(e.AntiSpam),s(e.ParticipantsHidden),l(e.NoForwards),d(e.JoinToSend),p(e.JoinRequest),h(String(e.SlowmodeSeconds))},[e]);function _(){let t={channel_id:e.ID};return n!==e.Gigagroup&&(t.gigagroup=n),i!==e.AntiSpam&&(t.antispam=i),o!==e.ParticipantsHidden&&(t.participants_hidden=o),c!==e.NoForwards&&(t.noforwards=c),u!==e.JoinToSend&&(t.join_to_send=u),f!==e.JoinRequest&&(t.join_request=f),Fn(m)!==e.SlowmodeSeconds&&(t.slowmode_seconds=Fn(m)),t}return(0,U.jsxs)(`div`,{className:`attr-block`,children:[(0,U.jsxs)(`label`,{className:`checkline`,children:[(0,U.jsx)(`input`,{type:`checkbox`,checked:n,onChange:e=>r(e.target.checked)}),` `,`Gigagroup`]}),(0,U.jsxs)(`label`,{className:`checkline`,children:[(0,U.jsx)(`input`,{type:`checkbox`,checked:i,onChange:e=>a(e.target.checked)}),` `,`Aggressive anti-spam`]}),(0,U.jsxs)(`label`,{className:`checkline`,children:[(0,U.jsx)(`input`,{type:`checkbox`,checked:o,onChange:e=>s(e.target.checked)}),` `,`Hide members`]}),(0,U.jsxs)(`label`,{className:`checkline`,children:[(0,U.jsx)(`input`,{type:`checkbox`,checked:c,onChange:e=>l(e.target.checked)}),` `,`Restrict forwarding`]}),(0,U.jsxs)(`label`,{className:`checkline`,children:[(0,U.jsx)(`input`,{type:`checkbox`,checked:u,onChange:e=>d(e.target.checked)}),` `,`Join to send messages`]}),(0,U.jsxs)(`label`,{className:`checkline`,children:[(0,U.jsx)(`input`,{type:`checkbox`,checked:f,onChange:e=>p(e.target.checked)}),` `,`Join by request`]}),(0,U.jsxs)(`label`,{className:`duration-field`,children:[(0,U.jsx)(`span`,{children:`Slowmode (seconds)`}),(0,U.jsx)(`input`,{type:`number`,min:`0`,max:`86400`,value:m,onChange:e=>h(e.target.value)})]}),(0,U.jsx)(Z,{label:`Apply settings`,icon:(0,U.jsx)(_t,{size:15}),tone:`warn`,path:`/api/actions/set-channel-settings`,payload:_,onDone:t})]})}function ii({id:e,navigate:t}){let[n,r]=(0,g.useState)(null),[i,a]=(0,g.useState)(``),[o,s]=(0,g.useState)(!1),[c,l]=(0,g.useState)(`profile`),[u,d]=(0,g.useState)(`1`),[f,p]=(0,g.useState)(()=>ai(new Date(Date.now()+7*864e5))),[m,h]=(0,g.useState)(``),[_,v]=(0,g.useState)(!1),[y,b]=(0,g.useState)(0);async function x(){s(!0),a(``);try{let t=await k.account(e);r(t),t.Restriction.Frozen&&(t.Restriction.Until&&p(ai(new Date(t.Restriction.Until))),h(t.Restriction.AppealURL||``))}catch(e){a(O(e))}finally{s(!1)}}if((0,g.useEffect)(()=>{x(),l(`profile`)},[e]),i)return(0,U.jsx)(q,{children:i});if(!n)return(0,U.jsx)(Zn,{label:o?`Loading account detail`:`Waiting for data`});let S=n.Account,C=[{key:`profile`,label:`Profile & Status`,icon:(0,U.jsx)(le,{size:15})},{key:`devices`,label:`Authorized Devices`,icon:(0,U.jsx)(rt,{size:15})},{key:`actions`,label:`Actions & Management`,icon:(0,U.jsx)(_t,{size:15})}];return(0,U.jsxs)(Gn,{title:`Account #${S.ID}`,eyebrow:`Account Profile`,actions:(0,U.jsxs)(`button`,{className:`btn icon-text`,onClick:()=>t(`/accounts`),children:[(0,U.jsx)(pe,{size:15}),` `,`Back to list`]}),children:[(0,U.jsxs)(`section`,{className:`entity-head`,children:[(0,U.jsxs)(`div`,{className:`entity-head-main`,children:[(0,U.jsxs)(`div`,{className:`avatar-edit-slot`,children:[(0,U.jsx)(Kr,{id:S.ID,firstName:S.FirstName,lastName:S.LastName,username:S.Username,size:64,refreshKey:y||void 0}),(0,U.jsx)(`button`,{className:`icon-btn avatar-edit-btn`,type:`button`,"aria-label":`Change avatar`,title:`Change avatar`,onClick:()=>v(!0),children:(0,U.jsx)(We,{size:13})})]}),(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`div`,{className:`entity-title`,children:jn(S)}),(0,U.jsxs)(`div`,{className:`entity-subtitle`,children:[W(S.Username)||`No username`,` · `,An(S.Phone)||`No phone`]}),S.Collectibles?.length>0&&(0,U.jsx)(`div`,{className:`entity-subtitle`,children:(0,U.jsx)($n,{username:``,collectibles:S.Collectibles})})]})]}),(0,U.jsxs)(`div`,{className:`entity-badges`,children:[S.PremiumUntil>0?(0,U.jsx)(J,{tone:`good`,children:`Premium`}):(0,U.jsx)(J,{children:`Not premium`}),n.Verified?(0,U.jsx)(J,{tone:`good`,children:`Verified`}):(0,U.jsx)(J,{children:`Not verified`}),(0,U.jsx)(Jr,{scam:n.Scam,fake:n.Fake}),S.Frozen?(0,U.jsx)(J,{tone:`danger`,children:`Account frozen`}):(0,U.jsx)(J,{children:`Account active`})]})]}),(0,U.jsx)(`div`,{className:`toolbar`,role:`group`,"aria-label":`Account sections`,children:C.map(e=>(0,U.jsxs)(`button`,{className:`btn icon-text ${c===e.key?`primary`:``}`,type:`button`,"aria-pressed":c===e.key,onClick:()=>l(e.key),children:[e.icon,` `,e.label]},e.key))}),c===`profile`&&(0,U.jsxs)(`div`,{className:`stacked-sections`,children:[(0,U.jsxs)(`div`,{className:`summary-grid`,children:[(0,U.jsx)(X,{label:`User ID`,value:String(S.ID),mono:!0}),(0,U.jsx)(X,{label:`Last active`,value:Nn(n.LastSeenAt)||`-`}),(0,U.jsx)(X,{label:`Premium expires`,value:S.PremiumUntil>0?Nn(S.PremiumUntil):`None`}),(0,U.jsx)(X,{label:`Updated`,value:G(S.UpdatedAt)||`-`}),(0,U.jsx)(X,{label:`Authorized devices`,value:String(n.Authorizations.length)}),(0,U.jsx)(X,{label:`Account flags`,value:`support=${n.Support} bot=${n.Bot}`}),(0,U.jsx)(X,{label:`Restriction`,value:n.HasRestriction?n.Restriction.Reason||`Restricted`:`None`}),(0,U.jsx)(X,{label:`Frozen since`,value:n.Restriction.Since?G(n.Restriction.Since):`None`}),(0,U.jsx)(X,{label:`Appeal deadline`,value:n.Restriction.Until?G(n.Restriction.Until):`None`}),(0,U.jsx)(X,{label:`Appeal URL`,value:n.Restriction.AppealURL||`None`}),(0,U.jsx)(X,{label:`Created`,value:G(S.CreatedAt)||`-`})]}),n.About&&(0,U.jsx)(`p`,{className:`about-text`,children:n.About})]}),c===`devices`&&(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(K,{title:`Authorized Devices`,text:`${n.Authorizations.length} authorizations`}),(0,U.jsx)(qr,{rows:n.Authorizations,userID:S.ID,onDone:x})]}),c===`actions`&&(0,U.jsxs)(`div`,{className:`stacked-sections`,children:[(0,U.jsxs)(`div`,{className:`action-groups`,children:[(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(K,{title:`Freeze & Restriction`,text:`Blocks sign-in and marks the account for appeal review.`}),(0,U.jsx)(`div`,{className:`card-body`,children:(0,U.jsxs)(`div`,{className:`attr-block`,children:[(0,U.jsxs)(`label`,{className:`duration-field`,children:[(0,U.jsx)(`span`,{children:`Appeal deadline`}),(0,U.jsx)(`input`,{"aria-label":`Freeze appeal deadline`,value:f,onChange:e=>p(e.target.value),type:`datetime-local`})]}),(0,U.jsxs)(`label`,{className:`duration-field`,children:[(0,U.jsx)(`span`,{children:`Appeal URL`}),(0,U.jsx)(`input`,{"aria-label":`Freeze appeal URL`,value:m,onChange:e=>h(e.target.value),type:`url`,placeholder:`https://...`})]}),(0,U.jsx)(Z,{label:S.Frozen?`Update freeze`:`Freeze account`,icon:(0,U.jsx)(te,{size:15}),tone:`danger`,path:`/api/actions/set-frozen`,payload:()=>({user_id:S.ID,frozen:!0,freeze_until:new Date(f).toISOString(),freeze_appeal_url:m.trim()}),onDone:x}),S.Frozen&&(0,U.jsx)(Z,{label:`Unfreeze account`,icon:(0,U.jsx)(te,{size:15}),path:`/api/actions/set-frozen`,payload:()=>({user_id:S.ID,frozen:!1}),onDone:x})]})})]}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(K,{title:`Premium`}),(0,U.jsx)(`div`,{className:`card-body`,children:(0,U.jsxs)(`div`,{className:`attr-block`,children:[(0,U.jsxs)(`label`,{className:`duration-field`,children:[(0,U.jsx)(`span`,{children:`Premium duration (months)`}),(0,U.jsx)(`input`,{"aria-label":`Set premium duration in months`,value:u,onChange:e=>d(e.target.value),type:`number`,min:`1`,max:`120`})]}),(0,U.jsxs)(`div`,{className:`action-stack`,children:[(0,U.jsx)(Z,{label:`Set premium`,icon:(0,U.jsx)(se,{size:15}),tone:`warn`,path:`/api/actions/grant-premium`,payload:()=>({user_id:S.ID,months:Fn(u)}),onDone:x}),(0,U.jsx)(Z,{label:`Clear premium`,icon:(0,U.jsx)(se,{size:15}),tone:`warn`,path:`/api/actions/grant-premium`,payload:()=>({user_id:S.ID,months:0}),onDone:x})]})]})})]}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(K,{title:`Verification & Moderation Flags`}),(0,U.jsx)(`div`,{className:`card-body`,children:(0,U.jsxs)(`div`,{className:`action-stack`,children:[(0,U.jsx)(Z,{label:n.Verified?`Clear verified`:`Set verified`,icon:(0,U.jsx)(ee,{size:15}),tone:`warn`,path:`/api/actions/set-verified`,payload:()=>({user_id:S.ID,verified:!n.Verified}),onDone:x}),(0,U.jsx)(Yr,{idKey:`user_id`,id:S.ID,path:`/api/actions/set-account-flags`,scam:n.Scam,fake:n.Fake,onDone:x}),(0,U.jsx)(Xr,{id:S.ID,support:n.Support,onDone:x})]})})]}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(K,{title:`Username`}),(0,U.jsx)(`div`,{className:`card-body`,children:(0,U.jsx)(Zr,{idKey:`user_id`,id:S.ID,path:`/api/actions/set-account-username`,current:S.Username,onDone:x})})]}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(K,{title:`Name`}),(0,U.jsx)(`div`,{className:`card-body`,children:(0,U.jsx)(Qr,{id:S.ID,path:`/api/actions/set-account-profile`,currentFirstName:S.FirstName,currentLastName:S.LastName,onDone:x})})]}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(K,{title:`Phone Number`}),(0,U.jsx)(`div`,{className:`card-body`,children:(0,U.jsx)($r,{id:S.ID,path:`/api/actions/set-account-phone`,current:S.Phone,onDone:x})})]}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(K,{title:`Login Email`,text:`The email used for sign-in / password-recovery, not a contact address.`}),(0,U.jsx)(`div`,{className:`card-body`,children:(0,U.jsx)(ei,{id:S.ID,path:`/api/actions/set-account-login-email`,current:S.LoginEmail,onDone:x})})]}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(K,{title:`Profile Color`}),(0,U.jsx)(`div`,{className:`card-body`,children:(0,U.jsx)(ti,{idKey:`user_id`,id:S.ID,path:`/api/actions/set-account-color`,onDone:x})})]}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(K,{title:`Emoji Status`}),(0,U.jsx)(`div`,{className:`card-body`,children:(0,U.jsx)(ni,{idKey:`user_id`,id:S.ID,path:`/api/actions/set-account-emoji-status`,onDone:x})})]})]}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(K,{title:`Recent Admin Actions`,text:`Last 30 audit rows`,action:(0,U.jsx)(mt,{size:16})}),(0,U.jsx)(Jn,{rows:n.AuditLogs})]})]}),_&&(0,U.jsx)(Vr,{kind:`user`,id:S.ID,onClose:()=>v(!1),onDone:()=>{b(e=>e+1),x()}})]})}function ai(e){return new Date(e.getTime()-e.getTimezoneOffset()*6e4).toISOString().slice(0,16)}function oi(e){return e.reduce((e,t)=>(e.devices+=t.DeviceCount,e),{devices:0})}function si(e){return e.reduce((e,t)=>(t.Megagroup&&(e.megagroups+=1),t.Broadcast&&(e.broadcasts+=1),t.Verified&&(e.verified+=1),e),{megagroups:0,broadcasts:0,verified:0})}var ci={beforeID:0,beforeActiveUS:0};function li({navigate:e}){let[t,n]=(0,g.useState)(``),[r,i]=(0,g.useState)(50),[a,o]=(0,g.useState)(null),[s,c]=(0,g.useState)(null),[l,u]=(0,g.useState)([]),[d,f]=(0,g.useState)(ci),[p,m]=(0,g.useState)(!1),[h,_]=(0,g.useState)(``);async function v(e,t){m(!0),_(``);let n=new URLSearchParams({limit:String(r)});e.trim()&&n.set(`q`,e.trim()),(t.beforeID||t.beforeActiveUS)&&(n.set(`before_id`,String(t.beforeID)),n.set(`before_active_us`,String(t.beforeActiveUS)));try{let e=await k.accounts(n);return o(e),e}catch(e){return _(O(e)),null}finally{m(!1)}}async function y(){u([]),f(ci),await v(t,ci)}async function b(){if(!a?.has_more)return;let e={beforeID:a.next_before_id,beforeActiveUS:a.next_before_active_us};await v(t,e)&&(u(e=>[...e,d]),f(e))}async function x(){if(l.length===0)return;let e=l[l.length-1];await v(t,e)&&(u(e=>e.slice(0,-1)),f(e))}async function S(){try{c(await k.accountStats())}catch{}}(0,g.useEffect)(()=>{y(),S()},[]);let C=oi(a?.rows??[]),w=l.length>0&&!p,T=!!a?.has_more&&!p;return(0,U.jsxs)(Gn,{title:`Accounts`,eyebrow:a?.listing===!1?`Search results`:`Recently active accounts`,actions:(0,U.jsxs)(U.Fragment,{children:[(0,U.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>e(`/accounts/shared-devices`),children:[(0,U.jsx)(Ct,{size:15}),` `,`Shared devices`]}),(0,U.jsxs)(`button`,{className:`btn`,type:`button`,onClick:()=>{y(),S()},disabled:p,children:[(0,U.jsx)(ft,{size:15}),` `,`Refresh`]})]}),children:[h&&(0,U.jsx)(q,{children:h}),(0,U.jsxs)(`div`,{className:`metric-row`,children:[(0,U.jsx)(Y,{label:`Total users`,value:s?String(s.total):`…`}),(0,U.jsx)(Y,{label:`Online now`,value:s?String(s.online):`…`,tone:`good`}),(0,U.jsx)(Y,{label:`Online device records`,value:String(C.devices)})]}),(0,U.jsx)(Kn,{children:(0,U.jsxs)(`form`,{className:`toolbar`,onSubmit:e=>{e.preventDefault(),y()},children:[(0,U.jsxs)(`label`,{className:`searchbox`,children:[(0,U.jsx)(V,{size:15}),(0,U.jsx)(`input`,{value:t,onChange:e=>n(e.target.value),placeholder:`User ID / phone / username / email / name`})]}),(0,U.jsxs)(`label`,{className:`gift-page-size`,children:[(0,U.jsx)(`span`,{children:`Limit`}),(0,U.jsxs)(`select`,{value:String(r),onChange:e=>i(Number(e.target.value)),children:[(0,U.jsx)(`option`,{value:`10`,children:`10`}),(0,U.jsx)(`option`,{value:`20`,children:`20`}),(0,U.jsx)(`option`,{value:`50`,children:`50`}),(0,U.jsx)(`option`,{value:`100`,children:`100`})]})]}),(0,U.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,disabled:p,children:[p?(0,U.jsx)(I,{size:15,className:`spin`}):(0,U.jsx)(V,{size:15}),` `,`Search`]}),(0,U.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>void x(),disabled:!w,children:[(0,U.jsx)(R,{size:15}),` `,`Previous page`]}),(0,U.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>void b(),disabled:!T,children:[(0,U.jsx)(z,{size:15}),` `,`Next page`]})]})}),(0,U.jsx)(`div`,{className:`table-wrap`,children:(0,U.jsxs)(`table`,{className:`data-table`,children:[(0,U.jsx)(`thead`,{children:(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`th`,{className:`avatar-col`}),(0,U.jsx)(`th`,{children:`User ID`}),(0,U.jsx)(`th`,{children:`Phone`}),(0,U.jsx)(`th`,{children:`Username`}),(0,U.jsx)(`th`,{children:`Name`}),(0,U.jsx)(`th`,{children:`Login email`}),(0,U.jsx)(`th`,{children:`Device`}),(0,U.jsx)(`th`,{children:`Last active`}),(0,U.jsx)(`th`,{children:`Premium`}),(0,U.jsx)(`th`,{children:`Verified`}),(0,U.jsx)(`th`,{children:`Frozen`}),(0,U.jsx)(`th`,{children:`Updated`}),(0,U.jsx)(`th`,{})]})}),(0,U.jsxs)(`tbody`,{children:[a?.rows.map(t=>(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`td`,{className:`avatar-col`,children:(0,U.jsx)(`button`,{className:`avatar-link`,type:`button`,onClick:()=>e(`/accounts/${t.ID}`),"aria-label":`Open account ${t.ID}`,children:(0,U.jsx)(Kr,{id:t.ID,firstName:t.FirstName,lastName:t.LastName,username:t.Username})})}),(0,U.jsx)(`td`,{className:`mono`,children:t.ID}),(0,U.jsx)(`td`,{children:An(t.Phone)}),(0,U.jsx)(`td`,{children:(0,U.jsx)($n,{username:t.Username,collectibles:t.Collectibles})}),(0,U.jsx)(`td`,{children:jn(t)}),(0,U.jsx)(`td`,{children:t.LoginEmail||(0,U.jsx)(`span`,{className:`muted-cell`,children:`None`})}),(0,U.jsx)(`td`,{children:t.DeviceCount}),(0,U.jsx)(`td`,{children:G(t.LastActiveAt)}),(0,U.jsx)(`td`,{children:t.PremiumUntil>0?(0,U.jsxs)(J,{tone:`good`,children:[`Premium`,` `,Nn(t.PremiumUntil)]}):(0,U.jsx)(J,{children:`None`})}),(0,U.jsxs)(`td`,{children:[t.Verified?(0,U.jsx)(J,{tone:`good`,children:`Verified`}):(0,U.jsx)(J,{children:`Not verified`}),` `,(0,U.jsx)(Jr,{scam:t.Scam,fake:t.Fake})]}),(0,U.jsx)(`td`,{children:t.Frozen?(0,U.jsx)(J,{tone:`danger`,children:`Frozen`}):(0,U.jsx)(J,{children:`Normal`})}),(0,U.jsx)(`td`,{children:G(t.UpdatedAt)}),(0,U.jsx)(`td`,{children:(0,U.jsxs)(`button`,{className:`row-link`,onClick:()=>e(`/accounts/${t.ID}`),children:[`Details`,` `,(0,U.jsx)(z,{size:14})]})})]},t.ID)),(!a||a.rows.length===0)&&(0,U.jsx)(Yn,{colSpan:12})]})]})})]})}function ui({navigate:e}){let[t,n]=(0,g.useState)([]),[r,i]=(0,g.useState)(!1),[a,o]=(0,g.useState)(0),[s,c]=(0,g.useState)(!1),[l,u]=(0,g.useState)(``);async function d(e=!1){c(!0),u(``);let t=new URLSearchParams({limit:`20`,offset:String(e?a:0)});try{let r=await k.sharedDeviceGroups(t),a=r.rows??[];n(t=>e?[...t,...a]:a),o(r.next_offset),i(!!r.has_more)}catch(e){u(O(e))}finally{c(!1)}}(0,g.useEffect)(()=>{d(!1)},[]);let f=t.reduce((e,t)=>e+t.AccountCount,0);return(0,U.jsxs)(Gn,{title:`Shared Devices`,eyebrow:`Multi-account signal — device/IP overlap across different accounts`,actions:(0,U.jsxs)(U.Fragment,{children:[(0,U.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>e(`/accounts`),children:[(0,U.jsx)(pe,{size:15}),` `,`Back to accounts`]}),(0,U.jsxs)(`button`,{className:`btn`,type:`button`,onClick:()=>d(!1),disabled:s,children:[(0,U.jsx)(ft,{size:15,className:s?`spin`:``}),` `,`Refresh`]})]}),children:[l&&(0,U.jsx)(q,{children:l}),(0,U.jsxs)(`div`,{className:`metric-row`,children:[(0,U.jsx)(Y,{label:`Device groups on page`,value:String(t.length)}),(0,U.jsx)(Y,{label:`Accounts flagged on page`,value:String(f),tone:`warn`})]}),(0,U.jsxs)(`p`,{className:`about-text`,children:[`Each card below is a device fingerprint (device model + OS + platform + IP) that more than one account has authorized from. `,`device_model/system_version are self-reported by the client, and IP alone can collide innocently -- use this as a lead, not a verdict.`]}),(0,U.jsxs)(`div`,{className:`stacked-sections`,children:[t.map(t=>(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(K,{title:t.DeviceModel||`Unknown device`,text:`${t.Platform||`unknown platform`} ${t.SystemVersion} · ${t.IP} · last active ${G(t.LastActiveAt)}`,action:(0,U.jsxs)(J,{tone:`warn`,children:[(0,U.jsx)(Ct,{size:12}),` `,`${t.AccountCount} accounts`]})}),(0,U.jsx)(`div`,{className:`table-wrap`,children:(0,U.jsxs)(`table`,{className:`data-table`,children:[(0,U.jsx)(`thead`,{children:(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`th`,{className:`avatar-col`}),(0,U.jsx)(`th`,{children:`User ID`}),(0,U.jsx)(`th`,{children:`Phone`}),(0,U.jsx)(`th`,{children:`Username`}),(0,U.jsx)(`th`,{children:`Name`}),(0,U.jsx)(`th`,{children:`Active from this device`}),(0,U.jsx)(`th`,{})]})}),(0,U.jsx)(`tbody`,{children:t.Accounts.map(t=>(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`td`,{className:`avatar-col`,children:(0,U.jsx)(`button`,{className:`avatar-link`,type:`button`,onClick:()=>e(`/accounts/${t.UserID}`),"aria-label":`Open account ${t.UserID}`,children:(0,U.jsx)(Kr,{id:t.UserID,firstName:t.FirstName,lastName:t.LastName,username:t.Username})})}),(0,U.jsx)(`td`,{className:`mono`,children:t.UserID}),(0,U.jsx)(`td`,{children:An(t.Phone)}),(0,U.jsx)(`td`,{children:W(t.Username)||`-`}),(0,U.jsx)(`td`,{children:jn(t)||`-`}),(0,U.jsx)(`td`,{children:G(t.ActiveAt)}),(0,U.jsx)(`td`,{children:(0,U.jsxs)(`button`,{className:`row-link`,onClick:()=>e(`/accounts/${t.UserID}`),children:[`Details`,` `,(0,U.jsx)(z,{size:14})]})})]},t.UserID))})]})})]},`${t.DeviceModel}|${t.SystemVersion}|${t.Platform}|${t.IP}`)),t.length===0&&(0,U.jsx)(`div`,{className:`table-wrap`,children:(0,U.jsx)(`table`,{className:`data-table`,children:(0,U.jsx)(`tbody`,{children:(0,U.jsx)(Yn,{colSpan:7})})})})]}),r&&(0,U.jsx)(`div`,{className:`toolbar`,children:(0,U.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>d(!0),disabled:s,children:[s?(0,U.jsx)(I,{size:15,className:`spin`}):(0,U.jsx)(Ce,{size:15}),` `,`Load more`]})})]})}function di({label:e,value:t,onChange:n}){let[r,i]=(0,g.useState)(``),[a,o]=(0,g.useState)([]),[s,c]=(0,g.useState)(!1),[l,u]=(0,g.useState)(``);async function d(){c(!0),u(``);let e=new URLSearchParams({limit:`20`});r.trim()&&e.set(`q`,r.trim());try{o((await k.accounts(e)).rows)}catch(e){u(O(e))}finally{c(!1)}}return(0,g.useEffect)(()=>{d()},[]),(0,U.jsxs)(`div`,{className:`entity-picker`,children:[(0,U.jsxs)(`div`,{className:`picker-head`,children:[(0,U.jsx)(`span`,{children:e}),t?(0,U.jsxs)(`button`,{className:`link-button`,type:`button`,onClick:()=>n(null),children:[(0,U.jsx)(It,{size:13}),` `,`Clear`]}):null]}),t?(0,U.jsxs)(`div`,{className:`selected-entity`,children:[(0,U.jsx)(Se,{size:15}),(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`strong`,{children:jn(t)}),(0,U.jsx)(`span`,{className:`mono`,children:t.ID})]}),(0,U.jsx)(`span`,{children:W(t.Username)||An(t.Phone)||`-`})]}):null,(0,U.jsxs)(`div`,{className:`picker-search`,children:[(0,U.jsx)(V,{size:15}),(0,U.jsx)(`input`,{value:r,onChange:e=>i(e.target.value),onKeyDown:e=>{e.key===`Enter`&&(e.preventDefault(),d())},placeholder:`Search user_id / phone / username`}),(0,U.jsx)(`button`,{className:`btn compact-btn`,type:`button`,onClick:d,disabled:s,children:s?(0,U.jsx)(I,{size:14,className:`spin`}):`Search`})]}),l&&(0,U.jsx)(`div`,{className:`picker-error`,children:l}),(0,U.jsxs)(`div`,{className:`picker-results`,children:[a.map(e=>(0,U.jsxs)(`button`,{className:`picker-row ${t?.ID===e.ID?`selected`:``}`,type:`button`,onClick:()=>n(e),children:[(0,U.jsx)(`span`,{className:`mono`,children:e.ID}),(0,U.jsx)(`strong`,{children:jn(e)}),(0,U.jsx)(`span`,{children:W(e.Username)||An(e.Phone)||`-`}),e.Verified?(0,U.jsx)(J,{tone:`good`,children:`Verified`}):(0,U.jsx)(J,{children:`Regular`})]},e.ID)),a.length===0&&!s?(0,U.jsx)(`div`,{className:`picker-empty`,children:`No results`}):null]})]})}function fi({label:e,selected:t,onChange:n}){let[r,i]=(0,g.useState)(``),[a,o]=(0,g.useState)([]),[s,c]=(0,g.useState)(!1),[l,u]=(0,g.useState)(``);async function d(){c(!0),u(``);let e=new URLSearchParams({limit:`20`});r.trim()&&e.set(`q`,r.trim());try{o((await k.accounts(e)).rows)}catch(e){u(O(e))}finally{c(!1)}}(0,g.useEffect)(()=>{d()},[]);function f(e){t.some(t=>t.ID===e.ID)?n(t.filter(t=>t.ID!==e.ID)):n([...t,e])}function p(e){n(t.filter(t=>t.ID!==e))}return(0,U.jsxs)(`div`,{className:`entity-picker`,children:[(0,U.jsxs)(`div`,{className:`picker-head`,children:[(0,U.jsx)(`span`,{children:e}),t.length>0?(0,U.jsxs)(`button`,{className:`link-button`,type:`button`,onClick:()=>n([]),children:[(0,U.jsx)(It,{size:13}),` `,`Clear all`]}):null]}),t.length>0?(0,U.jsx)(`div`,{className:`picker-chip-list`,children:t.map(e=>(0,U.jsxs)(`span`,{className:`picker-chip`,children:[jn(e),` `,(0,U.jsx)(`span`,{className:`mono`,children:e.ID}),(0,U.jsx)(`button`,{type:`button`,onClick:()=>p(e.ID),"aria-label":`Remove ${e.ID}`,children:(0,U.jsx)(It,{size:12})})]},e.ID))}):null,(0,U.jsxs)(`div`,{className:`picker-search`,children:[(0,U.jsx)(V,{size:15}),(0,U.jsx)(`input`,{value:r,onChange:e=>i(e.target.value),onKeyDown:e=>{e.key===`Enter`&&(e.preventDefault(),d())},placeholder:`Search user_id / phone / username`}),(0,U.jsx)(`button`,{className:`btn compact-btn`,type:`button`,onClick:d,disabled:s,children:s?(0,U.jsx)(I,{size:14,className:`spin`}):`Search`})]}),l&&(0,U.jsx)(`div`,{className:`picker-error`,children:l}),(0,U.jsxs)(`div`,{className:`picker-results`,children:[a.map(e=>{let n=t.some(t=>t.ID===e.ID);return(0,U.jsxs)(`button`,{className:`picker-row ${n?`selected`:``}`,type:`button`,onClick:()=>f(e),children:[(0,U.jsx)(`span`,{className:`mono`,children:e.ID}),(0,U.jsx)(`strong`,{children:jn(e)}),(0,U.jsx)(`span`,{children:W(e.Username)||An(e.Phone)||`-`}),n?(0,U.jsx)(Se,{size:15}):null]},e.ID)}),a.length===0&&!s?(0,U.jsx)(`div`,{className:`picker-empty`,children:`No results`}):null]})]})}function pi({label:e,value:t,onChange:n}){let[r,i]=(0,g.useState)(``),[a,o]=(0,g.useState)([]),[s,c]=(0,g.useState)(!1),[l,u]=(0,g.useState)(``);async function d(){c(!0),u(``);let e=new URLSearchParams({limit:`20`});r.trim()&&e.set(`q`,r.trim().replace(/^@/,``));try{o((await k.bots(e)).rows??[])}catch(e){u(O(e))}finally{c(!1)}}return(0,g.useEffect)(()=>{d()},[]),(0,U.jsxs)(`div`,{className:`entity-picker`,children:[(0,U.jsxs)(`div`,{className:`picker-head`,children:[(0,U.jsx)(`span`,{children:e}),t?(0,U.jsxs)(`button`,{className:`link-button`,type:`button`,onClick:()=>n(null),children:[(0,U.jsx)(It,{size:13}),` `,`Clear`]}):null]}),t?(0,U.jsxs)(`div`,{className:`selected-entity`,children:[(0,U.jsx)(Se,{size:15}),(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`strong`,{children:t.FirstName||`-`}),(0,U.jsx)(`span`,{className:`mono`,children:t.ID})]}),(0,U.jsx)(`span`,{children:W(t.Username)||`-`})]}):null,(0,U.jsxs)(`div`,{className:`picker-search`,children:[(0,U.jsx)(V,{size:15}),(0,U.jsx)(`input`,{value:r,onChange:e=>i(e.target.value),onKeyDown:e=>{e.key===`Enter`&&(e.preventDefault(),d())},placeholder:`Bot username or id`}),(0,U.jsx)(`button`,{className:`btn compact-btn`,type:`button`,onClick:d,disabled:s,children:s?(0,U.jsx)(I,{size:14,className:`spin`}):`Search`})]}),l&&(0,U.jsx)(`div`,{className:`picker-error`,children:l}),(0,U.jsxs)(`div`,{className:`picker-results`,children:[a.map(e=>(0,U.jsxs)(`button`,{className:`picker-row ${t?.ID===e.ID?`selected`:``}`,type:`button`,onClick:()=>n(e),children:[(0,U.jsx)(`span`,{className:`mono`,children:e.ID}),(0,U.jsx)(`strong`,{children:e.FirstName||`-`}),(0,U.jsx)(`span`,{children:W(e.Username)||`-`}),e.System?(0,U.jsx)(J,{tone:`warn`,children:`System`}):(0,U.jsx)(J,{children:`Regular`})]},e.ID)),a.length===0&&!s?(0,U.jsx)(`div`,{className:`picker-empty`,children:`No results`}):null]})]})}function mi({label:e,value:t,onChange:n}){let[r,i]=(0,g.useState)(``),[a,o]=(0,g.useState)([]),[s,c]=(0,g.useState)(!1),[l,u]=(0,g.useState)(``);async function d(){c(!0),u(``);let e=new URLSearchParams({limit:`20`});r.trim()&&e.set(`q`,r.trim());try{o((await k.channels(e)).rows)}catch(e){u(O(e))}finally{c(!1)}}return(0,g.useEffect)(()=>{d()},[]),(0,U.jsxs)(`div`,{className:`entity-picker`,children:[(0,U.jsxs)(`div`,{className:`picker-head`,children:[(0,U.jsx)(`span`,{children:e}),t?(0,U.jsxs)(`button`,{className:`link-button`,type:`button`,onClick:()=>n(null),children:[(0,U.jsx)(It,{size:13}),` `,`Clear`]}):null]}),t?(0,U.jsxs)(`div`,{className:`selected-entity`,children:[(0,U.jsx)(Se,{size:15}),(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`strong`,{children:t.Title||`-`}),(0,U.jsx)(`span`,{className:`mono`,children:t.ID})]}),(0,U.jsx)(`span`,{children:W(t.Username)||Mn(t)})]}):null,(0,U.jsxs)(`div`,{className:`picker-search`,children:[(0,U.jsx)(V,{size:15}),(0,U.jsx)(`input`,{value:r,onChange:e=>i(e.target.value),onKeyDown:e=>{e.key===`Enter`&&(e.preventDefault(),d())},placeholder:`Search channel_id / username / title`}),(0,U.jsx)(`button`,{className:`btn compact-btn`,type:`button`,onClick:d,disabled:s,children:s?(0,U.jsx)(I,{size:14,className:`spin`}):`Search`})]}),l&&(0,U.jsx)(`div`,{className:`picker-error`,children:l}),(0,U.jsxs)(`div`,{className:`picker-results`,children:[a.map(e=>(0,U.jsxs)(`button`,{className:`picker-row ${t?.ID===e.ID?`selected`:``}`,type:`button`,onClick:()=>n(e),children:[(0,U.jsx)(`span`,{className:`mono`,children:e.ID}),(0,U.jsx)(`strong`,{children:e.Title||`-`}),(0,U.jsx)(`span`,{children:W(e.Username)||Mn(e)}),e.Verified?(0,U.jsx)(J,{tone:`good`,children:`Verified`}):(0,U.jsx)(J,{children:Mn(e)})]},e.ID)),a.length===0&&!s?(0,U.jsx)(`div`,{className:`picker-empty`,children:`No results`}):null]})]})}function hi({onClose:e,onMinted:t}){let[n,r]=(0,g.useState)(`vault`),[i,a]=(0,g.useState)(null),[o,s]=(0,g.useState)(null),[c,l]=(0,g.useState)(``),[u,d]=(0,g.useState)(`XTR`),[f,p]=(0,g.useState)(``),[m,h]=(0,g.useState)(!1),[_,v]=(0,g.useState)(`TON`),[y,b]=(0,g.useState)(``),[x,S]=(0,g.useState)(!1),[C,w]=(0,g.useState)(``),[T,E]=(0,g.useState)(``),[D,O]=(0,g.useState)(``),k=Hn(f,u),A=m?Hn(y,_):`0`,j=k===null,M=m&&A===null,N=c.trim()!==``&&f.trim()!==``&&!j&&!M&&(n===`vault`||(n===`user`?i!==null:o!==null));function P(){let e={username:c.trim().replace(/^@/,``),currency:u,amount:k??`0`};if(n===`user`&&i&&(e.owner_user_id=String(i.ID)),n===`channel`&&o&&(e.owner_channel_id=String(o.ID)),m&&(e.crypto_currency=_,e.crypto_amount=A??`0`),C.trim()&&(e.url=C.trim()),T){let t=Date.parse(`${T}T${D||`00:00`}:00Z`);Number.isFinite(t)&&(e.purchase_date=Math.floor(t/1e3))}return e}return(0,On.createPortal)((0,U.jsx)(`div`,{className:`modal-backdrop`,role:`presentation`,children:(0,U.jsxs)(`section`,{className:`modal command-modal`,role:`dialog`,"aria-modal":`true`,"aria-label":`Mint a collectible username`,children:[(0,U.jsxs)(`div`,{className:`modal-head`,children:[(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`div`,{className:`eyebrow`,children:`NFT usernames`}),(0,U.jsx)(`h2`,{children:`Mint a collectible username`})]}),(0,U.jsx)(`button`,{className:`icon-btn`,type:`button`,onClick:e,"aria-label":`Close`,children:(0,U.jsx)(It,{size:15})})]}),(0,U.jsxs)(`div`,{className:`command-body`,children:[(0,U.jsxs)(`div`,{className:`mint-field-group`,children:[(0,U.jsx)(`div`,{className:`mint-field-group-label`,children:`1. Username`}),(0,U.jsxs)(`label`,{className:`duration-field`,children:[(0,U.jsx)(`span`,{children:`Username`}),(0,U.jsx)(`input`,{value:c,onChange:e=>l(e.target.value),placeholder:`durov`})]})]}),(0,U.jsxs)(`div`,{className:`mint-field-group`,children:[(0,U.jsx)(`div`,{className:`mint-field-group-label`,children:`2. Owner`}),(0,U.jsxs)(`div`,{className:`toolbar`,role:`group`,"aria-label":`Owner type`,children:[(0,U.jsxs)(`button`,{type:`button`,className:`btn ${n===`vault`?`primary`:``}`,onClick:()=>r(`vault`),children:[(0,U.jsx)(Ft,{size:15}),` `,`Vault (no owner)`]}),(0,U.jsx)(`button`,{type:`button`,className:`btn ${n===`user`?`primary`:``}`,onClick:()=>r(`user`),children:`User owner`}),(0,U.jsx)(`button`,{type:`button`,className:`btn ${n===`channel`?`primary`:``}`,onClick:()=>r(`channel`),children:`Channel owner`})]}),n===`user`&&(0,U.jsx)(di,{label:`User owner`,value:i,onChange:a}),n===`channel`&&(0,U.jsx)(mi,{label:`Channel owner`,value:o,onChange:s}),n===`vault`&&(0,U.jsx)(`p`,{className:`bot-create-note`,children:`Mints the asset unassigned; issue it to someone later from the asset page.`})]}),(0,U.jsxs)(`div`,{className:`mint-field-group`,children:[(0,U.jsx)(`div`,{className:`mint-field-group-label`,children:`3. Price`}),(0,U.jsx)(`p`,{className:`bot-create-note`,children:`A record of what it was sold for -- minting doesn't charge anyone.`}),(0,U.jsxs)(`div`,{className:`bot-create-fields`,children:[(0,U.jsxs)(`label`,{className:`duration-field`,children:[(0,U.jsx)(`span`,{children:`Currency`}),(0,U.jsxs)(`select`,{value:u,onChange:e=>d(e.target.value),children:[(0,U.jsx)(`option`,{value:`XTR`,children:`XTR`}),(0,U.jsx)(`option`,{value:`TON`,children:`TON`}),(0,U.jsx)(`option`,{value:`USD`,children:`USD`})]})]}),(0,U.jsxs)(`label`,{className:`duration-field`,children:[(0,U.jsx)(`span`,{children:`Amount (${u})`}),(0,U.jsx)(`input`,{value:f,onChange:e=>p(e.target.value),inputMode:`decimal`,placeholder:`1000`})]})]}),f.trim()!==``&&!j&&(0,U.jsx)(`p`,{className:`bot-create-note`,children:`Clients will show: ${Vn(k??`0`,u)}.`}),j&&(0,U.jsx)(`p`,{className:`bot-create-note`,children:`Not a valid ${u} amount: digits only, at most ${String(Rn(u))} decimal places.`}),(0,U.jsxs)(`label`,{className:`checkline`,children:[(0,U.jsx)(`input`,{type:`checkbox`,checked:m,onChange:e=>h(e.target.checked)}),` Also record a TON price`]}),m&&(0,U.jsxs)(`div`,{className:`bot-create-fields`,children:[(0,U.jsxs)(`label`,{className:`duration-field`,children:[(0,U.jsx)(`span`,{children:`Crypto currency`}),(0,U.jsx)(`select`,{value:_,onChange:e=>v(e.target.value),children:(0,U.jsx)(`option`,{value:`TON`,children:`TON`})})]}),(0,U.jsxs)(`label`,{className:`duration-field`,children:[(0,U.jsx)(`span`,{children:`Crypto amount (${_})`}),(0,U.jsx)(`input`,{value:y,onChange:e=>b(e.target.value),inputMode:`decimal`,placeholder:`12.5`})]})]}),M&&(0,U.jsx)(`p`,{className:`bot-create-note`,children:`Not a valid ${_} amount: digits only, at most ${String(Rn(_))} decimal places.`})]}),(0,U.jsxs)(`div`,{className:`mint-field-group`,children:[(0,U.jsx)(`button`,{type:`button`,className:`link-button`,onClick:()=>S(e=>!e),children:x?`Hide marketplace record`:`+ Add marketplace record (optional)`}),x&&(0,U.jsxs)(`div`,{className:`bot-create-fields`,children:[(0,U.jsxs)(`label`,{className:`duration-field`,children:[(0,U.jsx)(`span`,{children:`Marketplace URL`}),(0,U.jsx)(`input`,{value:C,onChange:e=>w(e.target.value),placeholder:`https://fragment.com/username/durov`})]}),(0,U.jsxs)(`label`,{className:`duration-field`,children:[(0,U.jsx)(`span`,{children:`Purchase date (UTC)`}),(0,U.jsx)(`input`,{value:T,onChange:e=>E(e.target.value),type:`date`})]}),(0,U.jsxs)(`label`,{className:`duration-field`,children:[(0,U.jsx)(`span`,{children:`Purchase time (UTC)`}),(0,U.jsx)(`input`,{value:D,onChange:e=>O(e.target.value),type:`time`,step:60,disabled:!T})]})]})]})]}),(0,U.jsxs)(`div`,{className:`modal-actions`,children:[(0,U.jsx)(`button`,{className:`btn`,type:`button`,onClick:e,children:`Close`}),(0,U.jsx)(Z,{disabled:!N,label:`Mint username`,icon:(0,U.jsx)(ct,{size:15}),tone:`neutral`,path:`/api/actions/mint-collectible-username`,payload:P,onDone:t})]})]})}),document.body)}function gi({navigate:e}){let[t,n]=(0,g.useState)(`all`),[r,i]=(0,g.useState)(``),[a,o]=(0,g.useState)(`50`),[s,c]=(0,g.useState)([]),[l,u]=(0,g.useState)(!1),[d,f]=(0,g.useState)(``),[p,m]=(0,g.useState)(!1),[h,_]=(0,g.useState)(``),[v,y]=(0,g.useState)(!1);async function b(e=!1){m(!0),_(``);let n=new URLSearchParams({limit:a});t!==`all`&&n.set(`status`,t),r.trim()&&n.set(`q`,r.trim().replace(/^@/,``)),e&&d&&n.set(`before_id`,d);try{let t=await k.collectibleUsernames(n),r=t.rows??[];c(t=>e?[...t,...r]:r),f(t.next_before_id??``),u(!!t.has_more)}catch(e){_(O(e))}finally{m(!1)}}(0,g.useEffect)(()=>{b(!1)},[]);let x=s.filter(e=>e.Status===`vault`).length,S=s.filter(e=>e.Status===`owned`).length,C=s.filter(e=>e.Status===`burned`).length;return(0,U.jsxs)(Gn,{title:`Collectible usernames`,eyebrow:`NFT usernames / Registry`,actions:(0,U.jsxs)(U.Fragment,{children:[(0,U.jsxs)(`button`,{className:`btn primary icon-text`,type:`button`,onClick:()=>y(!0),children:[(0,U.jsx)(ct,{size:15}),` `,`Mint username`]}),(0,U.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>b(!1),disabled:p,children:[(0,U.jsx)(ft,{size:15,className:p?`spin`:``}),` `,`Refresh`]})]}),children:[h&&(0,U.jsx)(q,{children:h}),(0,U.jsxs)(`div`,{className:`metric-row`,children:[(0,U.jsx)(Y,{label:`Loaded rows`,value:String(s.length)}),(0,U.jsx)(Y,{label:`In vault`,value:String(x)}),(0,U.jsx)(Y,{label:`Held by owners`,value:String(S),tone:`good`}),(0,U.jsx)(Y,{label:`Burned`,value:String(C),tone:C?`danger`:`neutral`})]}),(0,U.jsx)(Kn,{children:(0,U.jsxs)(`form`,{className:`toolbar`,onSubmit:e=>{e.preventDefault(),b(!1)},children:[(0,U.jsxs)(`label`,{className:`searchbox`,children:[(0,U.jsx)(V,{size:15}),(0,U.jsx)(`input`,{value:r,onChange:e=>i(e.target.value),placeholder:`Search by username`})]}),(0,U.jsxs)(`label`,{className:`field-inline`,children:[(0,U.jsx)(`span`,{children:`Status`}),(0,U.jsxs)(`select`,{value:t,onChange:e=>n(e.target.value),children:[(0,U.jsx)(`option`,{value:`all`,children:`All statuses`}),(0,U.jsx)(`option`,{value:`vault`,children:`Vault`}),(0,U.jsx)(`option`,{value:`owned`,children:`Owned`}),(0,U.jsx)(`option`,{value:`burned`,children:`Burned`})]})]}),(0,U.jsxs)(`label`,{className:`field-inline`,children:[(0,U.jsx)(`span`,{children:`Limit`}),(0,U.jsx)(`input`,{className:`small-input`,value:a,onChange:e=>o(e.target.value),type:`number`,min:`1`,max:`200`})]}),(0,U.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,disabled:p,children:[p?(0,U.jsx)(I,{size:15,className:`spin`}):(0,U.jsx)(V,{size:15}),` `,`Search`]})]})}),(0,U.jsx)(`div`,{className:`table-wrap`,children:(0,U.jsxs)(`table`,{className:`data-table`,children:[(0,U.jsx)(`thead`,{children:(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`th`,{children:`Username`}),(0,U.jsx)(`th`,{children:`Status`}),(0,U.jsx)(`th`,{children:`Owner`}),(0,U.jsx)(`th`,{children:`Price`}),(0,U.jsx)(`th`,{children:`Purchase date (UTC)`}),(0,U.jsx)(`th`,{children:`Transfers`}),(0,U.jsx)(`th`,{children:`Updated`}),(0,U.jsx)(`th`,{})]})}),(0,U.jsxs)(`tbody`,{children:[s.map(t=>(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`td`,{children:(0,U.jsx)(`strong`,{children:W(t.Username)})}),(0,U.jsx)(`td`,{children:(0,U.jsx)(_i,{status:t.Status})}),(0,U.jsx)(`td`,{children:vi(t,`Vault`)}),(0,U.jsx)(`td`,{className:`mono`,children:yi(t)}),(0,U.jsx)(`td`,{children:G(t.PurchaseDate)||`-`}),(0,U.jsx)(`td`,{className:`mono`,children:t.TransferCount}),(0,U.jsx)(`td`,{children:G(t.UpdatedAt)||`-`}),(0,U.jsx)(`td`,{children:(0,U.jsxs)(`button`,{className:`row-link`,type:`button`,onClick:()=>e(`/collectible-usernames/${t.ID}`),children:[(0,U.jsx)(_e,{size:14}),` `,`Details`,` `,(0,U.jsx)(z,{size:14})]})})]},t.ID)),s.length===0&&(0,U.jsx)(Yn,{colSpan:8})]})]})}),l&&(0,U.jsx)(`div`,{className:`toolbar`,children:(0,U.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>b(!0),disabled:p,children:[p?(0,U.jsx)(I,{size:15,className:`spin`}):(0,U.jsx)(Ce,{size:15}),` `,`Load more`]})}),v&&(0,U.jsx)(hi,{onClose:()=>y(!1),onMinted:()=>void b(!1)})]})}function _i({status:e}){return e===`owned`?(0,U.jsx)(J,{tone:`good`,children:`Owned`}):e===`burned`?(0,U.jsxs)(J,{tone:`danger`,children:[(0,U.jsx)(Re,{size:12}),` `,`Burned`]}):(0,U.jsxs)(J,{children:[(0,U.jsx)(Ft,{size:12}),` `,`Vault`]})}function vi(e,t){return!e.OwnerPeerType||e.OwnerPeerID===``||e.OwnerPeerID===`0`?t:`${W(e.OwnerUsername)||e.OwnerName||e.OwnerPeerID} · ${e.OwnerPeerType}:${e.OwnerPeerID}`}function yi(e){let t=Vn(e.Amount,e.Currency);return e.CryptoCurrency&&e.CryptoAmount&&e.CryptoAmount!==`0`?`${Vn(e.CryptoAmount,e.CryptoCurrency)} (${t})`:t}function bi({id:e,navigate:t}){let[n,r]=(0,g.useState)(null),[i,a]=(0,g.useState)(``),[o,s]=(0,g.useState)(!1),[c,l]=(0,g.useState)(`profile`),[u,d]=(0,g.useState)(`user`),[f,p]=(0,g.useState)(null),[m,h]=(0,g.useState)(null);async function _(){s(!0),a(``);try{r(await k.collectibleUsername(e))}catch(e){a(O(e))}finally{s(!1)}}if((0,g.useEffect)(()=>{_(),l(`profile`)},[e]),i&&!n)return(0,U.jsx)(q,{children:i});if(!n)return(0,U.jsx)(Zn,{label:o?`Loading collectible username…`:`Waiting for data`});let v=n.asset,y=n.transfers??[],b=`Vault`,x=!!v.OwnerPeerType&&v.OwnerPeerID!==``&&v.OwnerPeerID!==`0`,S=v.Status===`burned`;function C(){x&&t(v.OwnerPeerType===`channel`?`/channels/${v.OwnerPeerID}`:`/accounts/${v.OwnerPeerID}`)}function w(){let e={username:v.Username};return u===`user`&&f&&(e.to_user_id=String(f.ID)),u===`channel`&&m&&(e.to_channel_id=String(m.ID)),e}let T=[{key:`profile`,label:`Profile & Status`,icon:(0,U.jsx)(le,{size:15})},{key:`actions`,label:`Actions & Management`,icon:(0,U.jsx)(_t,{size:15})}];return(0,U.jsxs)(Gn,{title:`Collectible ${W(v.Username)}`,eyebrow:`NFT usernames / Asset`,actions:(0,U.jsxs)(U.Fragment,{children:[(0,U.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>t(`/collectible-usernames`),children:[(0,U.jsx)(pe,{size:15}),` `,`Back to list`]}),(0,U.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:_,disabled:o,children:[(0,U.jsx)(ft,{size:15,className:o?`spin`:``}),` `,`Refresh`]})]}),children:[i&&(0,U.jsx)(q,{children:i}),(0,U.jsxs)(`section`,{className:`entity-head`,children:[(0,U.jsx)(`div`,{className:`entity-head-main`,children:(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`div`,{className:`entity-title`,children:W(v.Username)}),(0,U.jsx)(`div`,{className:`entity-subtitle`,children:`Asset #${v.ID}`})]})}),(0,U.jsxs)(`div`,{className:`entity-badges`,children:[(0,U.jsx)(_i,{status:v.Status}),(0,U.jsx)(J,{tone:v.TransferCount>0?`warn`:`neutral`,children:`${v.TransferCount} transfers`}),v.Status===`owned`&&(0,U.jsx)(J,{tone:v.RegistryActive?`good`:`warn`,children:v.RegistryActive?`Active in profile`:`Hidden in profile`})]})]}),(0,U.jsx)(`div`,{className:`toolbar`,role:`group`,"aria-label":`Asset sections`,children:T.map(e=>(0,U.jsxs)(`button`,{className:`btn icon-text ${c===e.key?`primary`:``}`,type:`button`,"aria-pressed":c===e.key,onClick:()=>l(e.key),children:[e.icon,` `,e.label]},e.key))}),c===`profile`&&(0,U.jsxs)(`div`,{className:`stacked-sections`,children:[(0,U.jsxs)(`div`,{className:`summary-grid`,children:[(0,U.jsx)(X,{label:`Owner`,value:vi(v,b)}),(0,U.jsx)(X,{label:`Price`,value:yi(v),mono:!0}),(0,U.jsx)(X,{label:`Purchase date (UTC)`,value:G(v.PurchaseDate)||`-`}),(0,U.jsx)(X,{label:`Original owner`,value:Ci(v.OriginalOwnerPeerType,v.OriginalOwnerPeerID,b,v.OriginalOwnerUsername)}),(0,U.jsx)(X,{label:`Transfers`,value:String(v.TransferCount),mono:!0}),(0,U.jsx)(X,{label:`Created`,value:G(v.CreatedAt)||`-`}),(0,U.jsx)(X,{label:`Updated`,value:G(v.UpdatedAt)||`-`})]}),(0,U.jsxs)(`div`,{className:`toolbar`,children:[x&&(0,U.jsx)(`button`,{className:`row-link`,type:`button`,onClick:C,children:v.OwnerPeerType===`channel`?`Open owner channel`:`Open owner account`}),v.URL&&(0,U.jsxs)(`a`,{className:`row-link`,href:v.URL,target:`_blank`,rel:`noreferrer noopener`,children:[(0,U.jsx)(je,{size:14}),` `,`Open marketplace page`]})]}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(K,{title:`Provenance history`,text:`Mint, transfer, revoke and burn events in chronological order.`,action:(0,U.jsx)(mt,{size:16})}),(0,U.jsx)(`div`,{className:`table-wrap`,children:(0,U.jsxs)(`table`,{className:`data-table`,children:[(0,U.jsx)(`thead`,{children:(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`th`,{children:`ID`}),(0,U.jsx)(`th`,{children:`Event`}),(0,U.jsx)(`th`,{children:`From`}),(0,U.jsx)(`th`,{children:`To`}),(0,U.jsx)(`th`,{children:`Price`}),(0,U.jsx)(`th`,{children:`Actor`}),(0,U.jsx)(`th`,{children:`Reason`}),(0,U.jsx)(`th`,{children:`Time`})]})}),(0,U.jsxs)(`tbody`,{children:[y.map(e=>(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`td`,{className:`mono`,children:e.ID}),(0,U.jsx)(`td`,{children:(0,U.jsx)(Si,{kind:e.Kind})}),(0,U.jsx)(`td`,{className:`mono`,children:Ci(e.FromPeerType,e.FromPeerID,b,e.FromUsername)}),(0,U.jsx)(`td`,{className:`mono`,children:Ci(e.ToPeerType,e.ToPeerID,b,e.ToUsername)}),(0,U.jsx)(`td`,{className:`mono`,children:e.Amount&&e.Amount!==`0`?Vn(e.Amount,e.Currency):`-`}),(0,U.jsx)(`td`,{children:e.Actor||`-`}),(0,U.jsx)(`td`,{className:`truncate`,children:e.Reason||`-`}),(0,U.jsx)(`td`,{children:G(e.CreatedAt)||`-`})]},e.ID)),y.length===0&&(0,U.jsx)(Yn,{colSpan:8})]})]})})]})]}),c===`actions`&&(0,U.jsx)(`div`,{className:`stacked-sections`,children:S?(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(K,{title:`Asset Operations`}),(0,U.jsx)(`div`,{className:`card-body`,children:(0,U.jsx)(`p`,{className:`bot-create-note`,children:`This username is burned — no further operations are possible.`})})]}):(0,U.jsxs)(`div`,{className:`action-groups`,children:[(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(K,{title:`Transfer Ownership`,text:`Sent immediately; appended to the provenance history.`}),(0,U.jsxs)(`div`,{className:`card-body`,children:[(0,U.jsxs)(`div`,{className:`toolbar`,role:`group`,"aria-label":`Recipient type`,children:[(0,U.jsx)(`button`,{type:`button`,className:`btn ${u===`user`?`primary`:``}`,onClick:()=>d(`user`),children:`To user`}),(0,U.jsx)(`button`,{type:`button`,className:`btn ${u===`channel`?`primary`:``}`,onClick:()=>d(`channel`),children:`To channel`})]}),u===`user`?(0,U.jsx)(di,{label:`To user`,value:f,onChange:p}):(0,U.jsx)(mi,{label:`To channel`,value:m,onChange:h}),(0,U.jsx)(Z,{label:`Transfer`,icon:(0,U.jsx)(L,{size:15}),tone:`warn`,path:`/api/actions/transfer-collectible-username`,payload:w,onDone:_})]})]}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(K,{title:`Revoke To Vault`,text:`Returns the username to the vault; it can be issued again later.`}),(0,U.jsx)(`div`,{className:`card-body`,children:(0,U.jsx)(`div`,{className:`action-stack`,children:(0,U.jsx)(Z,{label:`Revoke to vault`,icon:(0,U.jsx)(kt,{size:15}),tone:`warn`,path:`/api/actions/revoke-collectible-username`,payload:()=>({username:v.Username,burn:!1}),onDone:_})})})]}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(K,{title:`Danger Zone`}),(0,U.jsx)(`div`,{className:`card-body`,children:(0,U.jsxs)(`div`,{className:`danger-zone`,children:[(0,U.jsx)(Z,{label:`Burn permanently`,icon:(0,U.jsx)(Re,{size:15}),tone:`danger`,path:`/api/actions/revoke-collectible-username`,payload:()=>({username:v.Username,burn:!0}),onDone:_}),(0,U.jsx)(`p`,{className:`bot-create-note`,children:`Irreversible: the username is destroyed and can never be issued again.`}),(0,U.jsx)(Z,{label:`Delete record`,icon:(0,U.jsx)(Ot,{size:15}),tone:`danger`,path:`/api/actions/delete-collectible-username`,payload:()=>({username:v.Username}),onDone:()=>t(`/collectible-usernames`)}),(0,U.jsx)(`p`,{className:`bot-create-note`,children:`Erases the asset and its ownership history, and frees the username for a fresh issue. Use this for a username issued by mistake; a burn keeps the history instead.`})]})})]})]})})]})}var xi={mint:`Mint`,transfer:`Transfer`,burn:`Burn`,revoke:`Revoke`};function Si({kind:e}){return(0,U.jsx)(J,{tone:e===`burn`?`danger`:e===`revoke`?`warn`:e===`mint`?`good`:`neutral`,children:xi[e]})}function Ci(e,t,n,r=``){if(!e||t===``||t===`0`)return n;let i=W(r);return i?`${i} · ${e}:${t}`:`${e}:${t}`}function wi({id:e,navigate:t}){let[n,r]=(0,g.useState)(null),[i,a]=(0,g.useState)(``),[o,s]=(0,g.useState)(!1),[c,l]=(0,g.useState)(`profile`),[u,d]=(0,g.useState)(!1),[f,p]=(0,g.useState)(0);async function m(){s(!0),a(``);try{r(await k.channel(e))}catch(e){a(O(e))}finally{s(!1)}}if((0,g.useEffect)(()=>{m(),l(`profile`)},[e]),i)return(0,U.jsx)(q,{children:i});if(!n)return(0,U.jsx)(Zn,{label:o?`Loading channel detail`:`Waiting for data`});let h=n.Channel,_=[{key:`profile`,label:`Profile & Status`,icon:(0,U.jsx)(le,{size:15})},{key:`actions`,label:`Actions & Management`,icon:(0,U.jsx)(_t,{size:15})}];return(0,U.jsxs)(Gn,{title:`${Mn(h)} #${h.ID}`,eyebrow:`Channel Profile`,actions:(0,U.jsxs)(`button`,{className:`btn icon-text`,onClick:()=>t(`/channels`),children:[(0,U.jsx)(pe,{size:15}),` `,`Back to list`]}),children:[(0,U.jsxs)(`section`,{className:`entity-head`,children:[(0,U.jsxs)(`div`,{className:`entity-head-main`,children:[(0,U.jsxs)(`div`,{className:`avatar-edit-slot`,children:[(0,U.jsx)(Kr,{id:h.ID,kind:`channel`,title:h.Title,size:64,refreshKey:f||void 0}),(0,U.jsx)(`button`,{className:`icon-btn avatar-edit-btn`,type:`button`,"aria-label":`Change avatar`,title:`Change avatar`,onClick:()=>d(!0),children:(0,U.jsx)(We,{size:13})})]}),(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`div`,{className:`entity-title`,children:h.Title||`-`}),(0,U.jsxs)(`div`,{className:`entity-subtitle`,children:[W(h.Username)||`No username`,` · `,`Creator ${h.CreatorUserID}`]})]})]}),(0,U.jsxs)(`div`,{className:`entity-badges`,children:[(0,U.jsx)(J,{children:Mn(h)}),h.Verified?(0,U.jsx)(J,{tone:`good`,children:`Verified`}):(0,U.jsx)(J,{children:`Not verified`}),(0,U.jsx)(Jr,{scam:h.Scam,fake:h.Fake}),h.Deleted?(0,U.jsx)(J,{tone:`danger`,children:`Deleted`}):(0,U.jsx)(J,{children:`Valid`})]})]}),(0,U.jsx)(`div`,{className:`toolbar`,role:`group`,"aria-label":`Channel sections`,children:_.map(e=>(0,U.jsxs)(`button`,{className:`btn icon-text ${c===e.key?`primary`:``}`,type:`button`,"aria-pressed":c===e.key,onClick:()=>l(e.key),children:[e.icon,` `,e.label]},e.key))}),c===`profile`&&(0,U.jsxs)(`div`,{className:`stacked-sections`,children:[(0,U.jsxs)(`div`,{className:`summary-grid`,children:[(0,U.jsx)(X,{label:`Channel ID`,value:String(h.ID),mono:!0}),(0,U.jsx)(X,{label:`access_hash`,value:String(h.AccessHash),mono:!0}),(0,U.jsx)(X,{label:`Members`,value:`${h.ParticipantsCount} / Admins ${h.AdminsCount}`}),(0,U.jsx)(X,{label:`Moderation`,value:`Banned ${h.BannedCount} / Kicked ${h.KickedCount}`}),(0,U.jsx)(X,{label:`Channel flags`,value:`broadcast=${h.Broadcast} megagroup=${h.Megagroup} forum=${h.Forum}`}),(0,U.jsx)(X,{label:`top / pinned / PTS`,value:`${h.TopMessageID} / ${h.PinnedMessageID} / ${h.PTS}`}),(0,U.jsx)(X,{label:`Created`,value:Nn(h.Date)||`-`}),(0,U.jsx)(X,{label:`Updated`,value:G(h.UpdatedAt)||`-`})]}),h.About&&(0,U.jsx)(`p`,{className:`about-text`,children:h.About}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(K,{title:`Channel Raw Row`,text:`Database read-only snapshot`}),(0,U.jsx)(Qn,{value:n.ChannelJSON})]})]}),c===`actions`&&(0,U.jsxs)(`div`,{className:`stacked-sections`,children:[(0,U.jsxs)(`div`,{className:`action-groups`,children:[(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(K,{title:`Verification & Moderation Flags`}),(0,U.jsx)(`div`,{className:`card-body`,children:(0,U.jsxs)(`div`,{className:`action-stack`,children:[(0,U.jsx)(Z,{label:h.Verified?`Clear verified`:`Set verified`,icon:(0,U.jsx)(ee,{size:15}),tone:`warn`,path:`/api/actions/set-channel-verified`,payload:()=>({channel_id:h.ID,verified:!h.Verified}),onDone:m}),(0,U.jsx)(Yr,{idKey:`channel_id`,id:h.ID,path:`/api/actions/set-channel-flags`,scam:h.Scam,fake:h.Fake,onDone:m})]})})]}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(K,{title:`Settings`}),(0,U.jsx)(`div`,{className:`card-body`,children:(0,U.jsx)(ri,{channel:h,onDone:m})})]}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(K,{title:`Username`}),(0,U.jsx)(`div`,{className:`card-body`,children:(0,U.jsx)(Zr,{idKey:`channel_id`,id:h.ID,path:`/api/actions/set-channel-username`,current:h.Username,onDone:m})})]}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(K,{title:`Profile Color`}),(0,U.jsx)(`div`,{className:`card-body`,children:(0,U.jsx)(ti,{idKey:`channel_id`,id:h.ID,path:`/api/actions/set-channel-color`,onDone:m})})]}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(K,{title:`Emoji Status`}),(0,U.jsx)(`div`,{className:`card-body`,children:(0,U.jsx)(ni,{idKey:`channel_id`,id:h.ID,path:`/api/actions/set-channel-emoji-status`,onDone:m})})]})]}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(K,{title:`Recent Admin Actions`,text:`Last 30 audit rows`,action:(0,U.jsx)(mt,{size:16})}),(0,U.jsx)(Jn,{rows:n.AuditLogs})]})]}),u&&(0,U.jsx)(Vr,{kind:`channel`,id:h.ID,onClose:()=>d(!1),onDone:()=>{p(e=>e+1),m()}})]})}var Ti={beforeID:0,beforeUpdatedUS:0};function Ei({navigate:e}){let[t,n]=(0,g.useState)(``),[r,i]=(0,g.useState)(50),[a,o]=(0,g.useState)(null),[s,c]=(0,g.useState)([]),[l,u]=(0,g.useState)(Ti),[d,f]=(0,g.useState)(!1),[p,m]=(0,g.useState)(``);async function h(e,t){f(!0),m(``);let n=new URLSearchParams({limit:String(r)});e.trim()&&n.set(`q`,e.trim()),(t.beforeID||t.beforeUpdatedUS)&&(n.set(`before_id`,String(t.beforeID)),n.set(`before_updated_us`,String(t.beforeUpdatedUS)));try{let e=await k.channels(n);return o(e),e}catch(e){return m(O(e)),null}finally{f(!1)}}async function _(){c([]),u(Ti),await h(t,Ti)}async function v(){if(!a?.has_more)return;let e={beforeID:a.next_before_id,beforeUpdatedUS:a.next_before_updated_us};await h(t,e)&&(c(e=>[...e,l]),u(e))}async function y(){if(s.length===0)return;let e=s[s.length-1];await h(t,e)&&(c(e=>e.slice(0,-1)),u(e))}(0,g.useEffect)(()=>{_()},[]);let b=si(a?.rows??[]),x=s.length>0&&!d,S=!!a?.has_more&&!d;return(0,U.jsxs)(Gn,{title:`Supergroups and Channels`,eyebrow:a?.listing===!1?`Search results`:`Recently updated`,actions:(0,U.jsxs)(`button`,{className:`btn`,type:`button`,onClick:()=>void _(),disabled:d,children:[(0,U.jsx)(ft,{size:15}),` `,`Refresh`]}),children:[p&&(0,U.jsx)(q,{children:p}),(0,U.jsxs)(`div`,{className:`metric-row`,children:[(0,U.jsx)(Y,{label:`Entities on page`,value:String(a?.rows.length??0)}),(0,U.jsx)(Y,{label:`Supergroups`,value:String(b.megagroups)}),(0,U.jsx)(Y,{label:`Channels`,value:String(b.broadcasts)}),(0,U.jsx)(Y,{label:`Verified`,value:String(b.verified),tone:`good`})]}),(0,U.jsx)(Kn,{children:(0,U.jsxs)(`form`,{className:`toolbar`,onSubmit:e=>{e.preventDefault(),_()},children:[(0,U.jsxs)(`label`,{className:`searchbox`,children:[(0,U.jsx)(V,{size:15}),(0,U.jsx)(`input`,{value:t,onChange:e=>n(e.target.value),placeholder:`Channel ID / username / title`})]}),(0,U.jsxs)(`label`,{className:`gift-page-size`,children:[(0,U.jsx)(`span`,{children:`Limit`}),(0,U.jsxs)(`select`,{value:String(r),onChange:e=>i(Number(e.target.value)),children:[(0,U.jsx)(`option`,{value:`10`,children:`10`}),(0,U.jsx)(`option`,{value:`20`,children:`20`}),(0,U.jsx)(`option`,{value:`50`,children:`50`}),(0,U.jsx)(`option`,{value:`100`,children:`100`})]})]}),(0,U.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,disabled:d,children:[d?(0,U.jsx)(I,{size:15,className:`spin`}):(0,U.jsx)(V,{size:15}),` `,`Search`]}),(0,U.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>void y(),disabled:!x,children:[(0,U.jsx)(R,{size:15}),` `,`Previous page`]}),(0,U.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>void v(),disabled:!S,children:[(0,U.jsx)(z,{size:15}),` `,`Next page`]})]})}),(0,U.jsx)(`div`,{className:`table-wrap`,children:(0,U.jsxs)(`table`,{className:`data-table`,children:[(0,U.jsx)(`thead`,{children:(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`th`,{className:`avatar-col`}),(0,U.jsx)(`th`,{children:`Channel ID`}),(0,U.jsx)(`th`,{children:`Kind`}),(0,U.jsx)(`th`,{children:`Username`}),(0,U.jsx)(`th`,{children:`Title`}),(0,U.jsx)(`th`,{children:`Members`}),(0,U.jsx)(`th`,{children:`Admins`}),(0,U.jsx)(`th`,{children:`PTS`}),(0,U.jsx)(`th`,{children:`Verified`}),(0,U.jsx)(`th`,{children:`Updated`}),(0,U.jsx)(`th`,{})]})}),(0,U.jsxs)(`tbody`,{children:[a?.rows.map(t=>(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`td`,{className:`avatar-col`,children:(0,U.jsx)(`button`,{className:`avatar-link`,type:`button`,onClick:()=>e(`/channels/${t.ID}`),"aria-label":`Open channel ${t.ID}`,children:(0,U.jsx)(Kr,{id:t.ID,kind:`channel`,title:t.Title})})}),(0,U.jsx)(`td`,{className:`mono`,children:t.ID}),(0,U.jsx)(`td`,{children:Mn(t)}),(0,U.jsx)(`td`,{children:W(t.Username)}),(0,U.jsx)(`td`,{children:t.Title}),(0,U.jsx)(`td`,{children:t.ParticipantsCount}),(0,U.jsx)(`td`,{children:t.AdminsCount}),(0,U.jsx)(`td`,{children:t.PTS}),(0,U.jsxs)(`td`,{children:[t.Verified?(0,U.jsx)(J,{tone:`good`,children:`Verified`}):(0,U.jsx)(J,{children:`Not verified`}),` `,(0,U.jsx)(Jr,{scam:t.Scam,fake:t.Fake})]}),(0,U.jsx)(`td`,{children:G(t.UpdatedAt)}),(0,U.jsx)(`td`,{children:(0,U.jsxs)(`button`,{className:`row-link`,onClick:()=>e(`/channels/${t.ID}`),children:[`Details`,` `,(0,U.jsx)(z,{size:14})]})})]},t.ID)),(!a||a.rows.length===0)&&(0,U.jsx)(Yn,{colSpan:11})]})]})})]})}function Di({botID:e,onClose:t}){let[n,r]=(0,g.useState)(``),[i,a]=(0,g.useState)(!1),[o,s]=(0,g.useState)(``),[c,l]=(0,g.useState)(!1);async function u(){if(!n.trim()){s(`Please enter an operation reason`);return}a(!0),s(``),l(!1);try{let t=await k.action(`/api/actions/export-bot-token`,{command_id:``,reason:n.trim(),confirm:!0,bot_user_id:e}),r=t.details?.token;if(t.error||typeof r!=`string`||!r){s(t.error||`No token returned.`);return}await kn(r),l(!0)}catch(e){s(O(e))}finally{a(!1)}}return(0,On.createPortal)((0,U.jsx)(`div`,{className:`modal-backdrop`,role:`presentation`,children:(0,U.jsxs)(`section`,{className:`modal command-modal`,role:`dialog`,"aria-modal":`true`,"aria-label":`Copy bot token`,children:[(0,U.jsxs)(`div`,{className:`modal-head`,children:[(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`div`,{className:`eyebrow`,children:`Bot`}),(0,U.jsx)(`h2`,{children:`Copy bot token`})]}),(0,U.jsx)(`button`,{className:`icon-btn`,type:`button`,onClick:t,disabled:i,"aria-label":`Close`,children:(0,U.jsx)(It,{size:15})})]}),(0,U.jsxs)(`div`,{className:`command-body`,children:[(0,U.jsx)(`p`,{children:`The token is written straight to your clipboard and is never shown on screen. Paste it wherever it's needed right after copying.`}),(0,U.jsxs)(`label`,{className:`form-field`,children:[(0,U.jsx)(`span`,{children:`Operation reason`}),(0,U.jsx)(`textarea`,{value:n,onChange:e=>r(e.target.value),rows:3,placeholder:`Describe why this token is being retrieved`})]}),o&&(0,U.jsx)(q,{children:o}),c&&(0,U.jsx)(`div`,{className:`secret-reveal`,children:(0,U.jsxs)(`div`,{className:`secret-reveal-label`,children:[(0,U.jsx)(Se,{size:14}),` `,`Token copied to clipboard.`]})})]}),(0,U.jsxs)(`div`,{className:`modal-actions`,children:[(0,U.jsx)(`button`,{className:`btn`,type:`button`,onClick:t,disabled:i,children:`Close`}),(0,U.jsxs)(`button`,{className:`btn primary icon-text`,type:`button`,onClick:()=>void u(),disabled:i,children:[(0,U.jsx)(Ee,{size:15}),` `,c?`Copy again`:`Copy token`]})]})]})}),document.body)}function Oi({id:e,navigate:t}){let[n,r]=(0,g.useState)(null),[i,a]=(0,g.useState)(``),[o,s]=(0,g.useState)(!1),[c,l]=(0,g.useState)(`profile`),[u,d]=(0,g.useState)(!1),[f,p]=(0,g.useState)(0),[m,h]=(0,g.useState)(!1);async function _(){s(!0),a(``);try{r(await k.bot(e))}catch(e){a(O(e))}finally{s(!1)}}if((0,g.useEffect)(()=>{_(),l(`profile`)},[e]),i)return(0,U.jsx)(q,{children:i});if(!n)return(0,U.jsx)(Zn,{label:o?`Loading bot detail`:`Waiting for data`});let v=n.Bot,y=[{key:`profile`,label:`Profile & Status`,icon:(0,U.jsx)(le,{size:15})},{key:`actions`,label:`Actions & Management`,icon:(0,U.jsx)(_t,{size:15})}];return(0,U.jsxs)(Gn,{title:`Bot #${v.ID}`,eyebrow:`Bot Profile`,actions:(0,U.jsxs)(`button`,{className:`btn icon-text`,onClick:()=>t(`/bots`),children:[(0,U.jsx)(pe,{size:15}),` `,`Back to list`]}),children:[(0,U.jsxs)(`section`,{className:`entity-head`,children:[(0,U.jsxs)(`div`,{className:`entity-head-main`,children:[(0,U.jsxs)(`div`,{className:`avatar-edit-slot`,children:[(0,U.jsx)(Kr,{id:v.ID,firstName:v.FirstName,username:v.Username,size:64,refreshKey:f||void 0}),(0,U.jsx)(`button`,{className:`icon-btn avatar-edit-btn`,type:`button`,"aria-label":`Change avatar`,title:`Change avatar`,onClick:()=>d(!0),children:(0,U.jsx)(We,{size:13})})]}),(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`div`,{className:`entity-title`,children:v.FirstName||`Unnamed bot`}),(0,U.jsx)(`div`,{className:`entity-subtitle`,children:W(v.Username)||`No username`})]})]}),(0,U.jsxs)(`div`,{className:`entity-badges`,children:[(0,U.jsx)(J,{tone:v.System?`warn`:`neutral`,children:v.System?`System`:`User`}),v.Verified?(0,U.jsx)(J,{tone:`good`,children:`Verified`}):(0,U.jsx)(J,{children:`Not verified`}),(0,U.jsx)(Jr,{scam:v.Scam,fake:v.Fake})]})]}),(0,U.jsx)(`div`,{className:`toolbar`,role:`group`,"aria-label":`Bot sections`,children:y.map(e=>(0,U.jsxs)(`button`,{className:`btn icon-text ${c===e.key?`primary`:``}`,type:`button`,"aria-pressed":c===e.key,onClick:()=>l(e.key),children:[e.icon,` `,e.label]},e.key))}),c===`profile`&&(0,U.jsxs)(`div`,{className:`stacked-sections`,children:[(0,U.jsxs)(`div`,{className:`summary-grid`,children:[(0,U.jsx)(X,{label:`Bot ID`,value:String(v.ID),mono:!0}),(0,U.jsx)(X,{label:`Owner`,value:v.OwnerUserID>0?`${v.OwnerUserID} ${W(n.OwnerUsername)}`.trim():`None`}),(0,U.jsx)(X,{label:`Type`,value:v.System?`System`:`User`}),(0,U.jsx)(X,{label:`Updated`,value:G(v.UpdatedAt)||`-`}),(0,U.jsx)(X,{label:`Created`,value:G(v.CreatedAt)||`-`})]}),n.About&&(0,U.jsx)(`p`,{className:`about-text`,children:n.About}),n.Description&&n.Description.trim()!==n.About.trim()&&(0,U.jsx)(`p`,{className:`about-text`,children:n.Description})]}),c===`actions`&&(0,U.jsxs)(`div`,{className:`stacked-sections`,children:[(0,U.jsxs)(`div`,{className:`action-groups`,children:[(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(K,{title:`Verification & Moderation Flags`}),(0,U.jsx)(`div`,{className:`card-body`,children:(0,U.jsxs)(`div`,{className:`action-stack`,children:[(0,U.jsx)(Z,{label:v.Verified?`Clear verified`:`Set verified`,icon:(0,U.jsx)(ee,{size:15}),tone:`neutral`,path:`/api/actions/set-verified`,payload:()=>({user_id:v.ID,verified:!v.Verified}),onDone:_}),(0,U.jsx)(Yr,{idKey:`user_id`,id:v.ID,path:`/api/actions/set-account-flags`,scam:v.Scam,fake:v.Fake,onDone:_})]})})]}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(K,{title:`Username`}),(0,U.jsx)(`div`,{className:`card-body`,children:(0,U.jsx)(Zr,{idKey:`user_id`,id:v.ID,path:`/api/actions/set-account-username`,current:v.Username,onDone:_})})]}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(K,{title:`Profile Color`}),(0,U.jsx)(`div`,{className:`card-body`,children:(0,U.jsx)(ti,{idKey:`user_id`,id:v.ID,path:`/api/actions/set-account-color`,onDone:_})})]}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(K,{title:`Emoji Status`}),(0,U.jsx)(`div`,{className:`card-body`,children:(0,U.jsx)(ni,{idKey:`user_id`,id:v.ID,path:`/api/actions/set-account-emoji-status`,onDone:_})})]}),!v.System&&(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(K,{title:`Credentials`}),(0,U.jsxs)(`div`,{className:`card-body`,children:[(0,U.jsx)(`div`,{className:`action-stack`,children:(0,U.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>h(!0),children:[(0,U.jsx)(Ee,{size:15}),` `,`Copy token`]})}),(0,U.jsx)(`p`,{className:`bot-create-note`,children:`Copies straight to the clipboard through a dedicated confirmation step -- the token itself is never shown on this page.`})]})]}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(K,{title:`Danger Zone`}),(0,U.jsx)(`div`,{className:`card-body`,children:v.System?(0,U.jsx)(`p`,{className:`bot-create-note`,children:`System bots are built in and cannot be deleted.`}):(0,U.jsxs)(`div`,{className:`danger-zone`,children:[(0,U.jsx)(Z,{label:`Delete bot`,icon:(0,U.jsx)(Ot,{size:15}),tone:`danger`,path:`/api/actions/delete-bot`,payload:()=>({bot_user_id:v.ID}),onDone:()=>t(`/bots`)}),(0,U.jsx)(`p`,{className:`bot-create-note`,children:`Permanently deletes this user-created bot and invalidates its token. This cannot be undone.`})]})})]})]}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(K,{title:`Recent Admin Actions`,text:`Last 30 audit rows`,action:(0,U.jsx)(mt,{size:16})}),(0,U.jsx)(Jn,{rows:n.AuditLogs})]})]}),u&&(0,U.jsx)(Vr,{kind:`user`,id:v.ID,onClose:()=>d(!1),onDone:()=>{p(e=>e+1),_()}}),m&&(0,U.jsx)(Di,{botID:v.ID,onClose:()=>h(!1)})]})}function ki({onClose:e,onCreated:t}){let[n,r]=(0,g.useState)(``),[i,a]=(0,g.useState)(``),[o,s]=(0,g.useState)(``);return(0,On.createPortal)((0,U.jsx)(`div`,{className:`modal-backdrop`,role:`presentation`,children:(0,U.jsxs)(`section`,{className:`modal command-modal`,role:`dialog`,"aria-modal":`true`,"aria-label":`Create bot`,children:[(0,U.jsxs)(`div`,{className:`modal-head`,children:[(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`div`,{className:`eyebrow`,children:`Bots`}),(0,U.jsx)(`h2`,{children:`Create bot`})]}),(0,U.jsx)(`button`,{className:`icon-btn`,type:`button`,onClick:e,"aria-label":`Close`,children:(0,U.jsx)(It,{size:15})})]}),(0,U.jsxs)(`div`,{className:`command-body`,children:[(0,U.jsx)(`p`,{children:`Provision a bot account owned by the given user. The token is shown once after confirmation.`}),(0,U.jsxs)(`div`,{className:`bot-create-fields`,children:[(0,U.jsxs)(`label`,{className:`duration-field`,children:[(0,U.jsx)(`span`,{children:`Owner user ID`}),(0,U.jsx)(`input`,{value:n,onChange:e=>r(e.target.value),type:`number`,min:`1`,placeholder:`123456789`})]}),(0,U.jsxs)(`label`,{className:`duration-field`,children:[(0,U.jsx)(`span`,{children:`Display name`}),(0,U.jsx)(`input`,{value:i,onChange:e=>a(e.target.value),placeholder:`e.g. Service Bot`,maxLength:64})]}),(0,U.jsxs)(`label`,{className:`duration-field`,children:[(0,U.jsx)(`span`,{children:`Username`}),(0,U.jsx)(`input`,{value:o,onChange:e=>s(e.target.value),placeholder:`my_service_bot`})]})]}),(0,U.jsx)(`span`,{className:`bot-create-note`,children:`Username must be 5-32 characters and end with 'bot'.`})]}),(0,U.jsxs)(`div`,{className:`modal-actions`,children:[(0,U.jsx)(`button`,{className:`btn`,type:`button`,onClick:e,children:`Close`}),(0,U.jsx)(Z,{label:`Create bot`,icon:(0,U.jsx)(ct,{size:15}),tone:`neutral`,path:`/api/actions/create-bot`,payload:()=>({owner_user_id:Fn(n),name:i.trim(),username:o.trim().replace(/^@/,``)}),secretField:`token`,onDone:t})]})]})}),document.body)}var Ai={beforeID:0};function ji({navigate:e}){let[t,n]=(0,g.useState)(``),[r,i]=(0,g.useState)(50),[a,o]=(0,g.useState)(null),[s,c]=(0,g.useState)([]),[l,u]=(0,g.useState)(Ai),[d,f]=(0,g.useState)(!1),[p,m]=(0,g.useState)(``),[h,_]=(0,g.useState)(!1);async function v(e,t){f(!0),m(``);let n=new URLSearchParams({limit:String(r)});e.trim()&&n.set(`q`,e.trim()),t.beforeID&&n.set(`before_id`,String(t.beforeID));try{let e=await k.bots(n);return o(e),e}catch(e){return m(O(e)),null}finally{f(!1)}}async function y(){c([]),u(Ai),await v(t,Ai)}async function b(){if(!a?.has_more)return;let e={beforeID:a.next_before_id};await v(t,e)&&(c(e=>[...e,l]),u(e))}async function x(){if(s.length===0)return;let e=s[s.length-1];await v(t,e)&&(c(e=>e.slice(0,-1)),u(e))}(0,g.useEffect)(()=>{y()},[]);let S=a?.rows??[],C=S.filter(e=>e.Verified).length,w=S.filter(e=>e.System).length,T=s.length>0&&!d,E=!!a?.has_more&&!d;return(0,U.jsxs)(Gn,{title:`Bots`,eyebrow:a?.listing===!1?`Search results`:`Recently created bots`,actions:(0,U.jsxs)(U.Fragment,{children:[(0,U.jsxs)(`button`,{className:`btn primary icon-text`,type:`button`,onClick:()=>_(!0),children:[(0,U.jsx)(ct,{size:15}),` `,`Create bot`]}),(0,U.jsxs)(`button`,{className:`btn`,type:`button`,onClick:()=>void y(),disabled:d,children:[(0,U.jsx)(ft,{size:15}),` `,`Refresh`]})]}),children:[p&&(0,U.jsx)(q,{children:p}),(0,U.jsxs)(`div`,{className:`metric-row`,children:[(0,U.jsx)(Y,{label:`Bots on page`,value:String(S.length)}),(0,U.jsx)(Y,{label:`Verified`,value:String(C),tone:`good`}),(0,U.jsx)(Y,{label:`System`,value:String(w)})]}),(0,U.jsx)(Kn,{children:(0,U.jsxs)(`form`,{className:`toolbar`,onSubmit:e=>{e.preventDefault(),y()},children:[(0,U.jsxs)(`label`,{className:`searchbox`,children:[(0,U.jsx)(V,{size:15}),(0,U.jsx)(`input`,{value:t,onChange:e=>n(e.target.value),placeholder:`Bot ID / username`})]}),(0,U.jsxs)(`label`,{className:`gift-page-size`,children:[(0,U.jsx)(`span`,{children:`Limit`}),(0,U.jsxs)(`select`,{value:String(r),onChange:e=>i(Number(e.target.value)),children:[(0,U.jsx)(`option`,{value:`10`,children:`10`}),(0,U.jsx)(`option`,{value:`20`,children:`20`}),(0,U.jsx)(`option`,{value:`50`,children:`50`}),(0,U.jsx)(`option`,{value:`100`,children:`100`})]})]}),(0,U.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,disabled:d,children:[d?(0,U.jsx)(I,{size:15,className:`spin`}):(0,U.jsx)(V,{size:15}),` `,`Search`]}),(0,U.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>void x(),disabled:!T,children:[(0,U.jsx)(R,{size:15}),` `,`Previous page`]}),(0,U.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>void b(),disabled:!E,children:[(0,U.jsx)(z,{size:15}),` `,`Next page`]})]})}),(0,U.jsx)(`div`,{className:`table-wrap`,children:(0,U.jsxs)(`table`,{className:`data-table`,children:[(0,U.jsx)(`thead`,{children:(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`th`,{className:`avatar-col`}),(0,U.jsx)(`th`,{children:`Bot ID`}),(0,U.jsx)(`th`,{children:`Username`}),(0,U.jsx)(`th`,{children:`Name`}),(0,U.jsx)(`th`,{children:`Owner`}),(0,U.jsx)(`th`,{children:`Verified`}),(0,U.jsx)(`th`,{children:`Type`}),(0,U.jsx)(`th`,{children:`Created`}),(0,U.jsx)(`th`,{})]})}),(0,U.jsxs)(`tbody`,{children:[S.map(t=>(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`td`,{className:`avatar-col`,children:(0,U.jsx)(`button`,{className:`avatar-link`,type:`button`,onClick:()=>e(`/bots/${t.ID}`),"aria-label":`Open bot ${t.ID}`,children:(0,U.jsx)(Kr,{id:t.ID,firstName:t.FirstName,username:t.Username})})}),(0,U.jsx)(`td`,{className:`mono`,children:t.ID}),(0,U.jsx)(`td`,{children:W(t.Username)||`-`}),(0,U.jsx)(`td`,{children:t.FirstName||`-`}),(0,U.jsx)(`td`,{className:`mono`,children:t.OwnerUserID>0?t.OwnerUserID:`-`}),(0,U.jsxs)(`td`,{children:[t.Verified?(0,U.jsxs)(J,{tone:`good`,children:[(0,U.jsx)(ee,{size:12}),` `,`Verified`]}):(0,U.jsx)(J,{children:`Not verified`}),` `,(0,U.jsx)(Jr,{scam:t.Scam,fake:t.Fake})]}),(0,U.jsx)(`td`,{children:t.System?(0,U.jsx)(J,{tone:`warn`,children:`System`}):(0,U.jsx)(J,{children:`User`})}),(0,U.jsx)(`td`,{children:G(t.CreatedAt)}),(0,U.jsx)(`td`,{children:(0,U.jsxs)(`button`,{className:`row-link`,onClick:()=>e(`/bots/${t.ID}`),children:[(0,U.jsx)(ye,{size:14}),` `,`Details`,` `,(0,U.jsx)(z,{size:14})]})})]},t.ID)),S.length===0&&(0,U.jsx)(Yn,{colSpan:9})]})]})}),h&&(0,U.jsx)(ki,{onClose:()=>_(!1),onCreated:()=>void y()})]})}function Mi({onClose:e,onCreated:t}){let[n,r]=(0,g.useState)(``),[i,a]=(0,g.useState)(`all`),[o,s]=(0,g.useState)([]),c=(0,g.useMemo)(()=>!n.trim()||i===`selected`&&o.length===0,[n,i,o]);return(0,On.createPortal)((0,U.jsx)(`div`,{className:`modal-backdrop`,role:`presentation`,children:(0,U.jsxs)(`section`,{className:`modal command-modal`,role:`dialog`,"aria-modal":`true`,"aria-label":`Send broadcast`,children:[(0,U.jsxs)(`div`,{className:`modal-head`,children:[(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`div`,{className:`eyebrow`,children:`Broadcasts`}),(0,U.jsx)(`h2`,{children:`Send broadcast`})]}),(0,U.jsx)(`button`,{className:`icon-btn`,type:`button`,onClick:e,"aria-label":`Close`,children:(0,U.jsx)(It,{size:15})})]}),(0,U.jsxs)(`div`,{className:`command-body`,children:[(0,U.jsx)(`p`,{children:`Sends a message from the official system account (777000) to all users or to a chosen list. Delivery happens in the background and may take a few minutes for large audiences.`}),(0,U.jsxs)(`label`,{className:`form-field`,children:[(0,U.jsx)(`span`,{children:`Message`}),(0,U.jsx)(`textarea`,{value:n,onChange:e=>r(e.target.value),rows:5,maxLength:4096,placeholder:`What's new...`})]}),(0,U.jsx)(`div`,{className:`bot-create-fields`,children:(0,U.jsxs)(`label`,{className:`duration-field`,children:[(0,U.jsx)(`span`,{children:`Target`}),(0,U.jsxs)(`select`,{value:i,onChange:e=>a(e.target.value),children:[(0,U.jsx)(`option`,{value:`all`,children:`All users`}),(0,U.jsx)(`option`,{value:`selected`,children:`Selected users`})]})]})}),i===`selected`&&(0,U.jsx)(fi,{label:`Recipients`,selected:o,onChange:s})]}),(0,U.jsxs)(`div`,{className:`modal-actions`,children:[(0,U.jsx)(`button`,{className:`btn`,type:`button`,onClick:e,children:`Close`}),(0,U.jsx)(Z,{label:`Send broadcast`,icon:(0,U.jsx)(ht,{size:15}),tone:`neutral`,path:`/api/actions/create-broadcast`,disabled:c,payload:()=>({message:n.trim(),target_mode:i,user_ids:i===`selected`?o.map(e=>e.ID):void 0}),onDone:t})]})]})}),document.body)}var Ni={beforeID:0};function Pi(){let[e,t]=(0,g.useState)(null),[n,r]=(0,g.useState)([]),[i,a]=(0,g.useState)(Ni),[o,s]=(0,g.useState)(!1),[c,l]=(0,g.useState)(``),[u,d]=(0,g.useState)(!1);async function f(e){s(!0),l(``);let n=new URLSearchParams({limit:`50`});e.beforeID&&n.set(`before_id`,String(e.beforeID));try{let e=await k.broadcasts(n);return t(e),e}catch(e){return l(O(e)),null}finally{s(!1)}}async function p(){r([]),a(Ni),await f(Ni)}async function m(){if(!e?.has_more)return;let t={beforeID:e.next_before_id};await f(t)&&(r(e=>[...e,i]),a(t))}async function h(){if(n.length===0)return;let e=n[n.length-1];await f(e)&&(r(e=>e.slice(0,-1)),a(e))}(0,g.useEffect)(()=>{p()},[]);let _=e?.rows??[],v=_.filter(e=>!e.EnumerationDone||e.SentCount+e.FailedCount0&&!o,b=!!e?.has_more&&!o;return(0,U.jsxs)(Gn,{title:`Broadcasts`,eyebrow:`Announcements sent from the official system account`,actions:(0,U.jsxs)(U.Fragment,{children:[(0,U.jsxs)(`button`,{className:`btn primary icon-text`,type:`button`,onClick:()=>d(!0),children:[(0,U.jsx)(ht,{size:15}),` `,`Send broadcast`]}),(0,U.jsxs)(`button`,{className:`btn`,type:`button`,onClick:()=>void p(),disabled:o,children:[(0,U.jsx)(ft,{size:15}),` `,`Refresh`]})]}),children:[c&&(0,U.jsx)(q,{children:c}),(0,U.jsxs)(`div`,{className:`metric-row`,children:[(0,U.jsx)(Y,{label:`Campaigns on page`,value:String(_.length)}),(0,U.jsx)(Y,{label:`Still delivering`,value:String(v),tone:v>0?`warn`:`neutral`})]}),(0,U.jsx)(`div`,{className:`table-wrap`,children:(0,U.jsxs)(`table`,{className:`data-table`,children:[(0,U.jsx)(`thead`,{children:(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`th`,{children:`ID`}),(0,U.jsx)(`th`,{children:`Message`}),(0,U.jsx)(`th`,{children:`Target`}),(0,U.jsx)(`th`,{children:`Sent`}),(0,U.jsx)(`th`,{children:`Failed`}),(0,U.jsx)(`th`,{children:`Total`}),(0,U.jsx)(`th`,{children:`Created by`}),(0,U.jsx)(`th`,{children:`Created`})]})}),(0,U.jsxs)(`tbody`,{children:[_.map(e=>{let t=e.SentCount+e.FailedCount,n=e.EnumerationDone&&e.TargetCount>0&&t>=e.TargetCount;return(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`td`,{className:`mono`,children:e.ID}),(0,U.jsx)(`td`,{className:`truncate`,children:e.Message}),(0,U.jsx)(`td`,{children:e.TargetMode===`all`?(0,U.jsx)(J,{tone:`warn`,children:`All users`}):(0,U.jsx)(J,{children:`Selected`})}),(0,U.jsx)(`td`,{children:e.SentCount}),(0,U.jsx)(`td`,{children:e.FailedCount>0?(0,U.jsx)(J,{tone:`danger`,children:e.FailedCount}):e.FailedCount}),(0,U.jsx)(`td`,{children:e.TargetCount}),(0,U.jsx)(`td`,{children:e.CreatedBy||`-`}),(0,U.jsxs)(`td`,{children:[G(e.CreatedAt),!n&&(0,U.jsx)(J,{tone:`warn`,children:`Sending`})]})]},e.ID)}),_.length===0&&(0,U.jsx)(Yn,{colSpan:8})]})]})}),(0,U.jsxs)(`div`,{className:`toolbar`,children:[(0,U.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>void h(),disabled:!y,children:[(0,U.jsx)(R,{size:15}),` `,`Previous page`]}),(0,U.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>void m(),disabled:!b,children:[o?(0,U.jsx)(I,{size:15,className:`spin`}):(0,U.jsx)(z,{size:15}),` `,`Next page`]})]}),u&&(0,U.jsx)(Mi,{onClose:()=>d(!1),onCreated:()=>void p()})]})}function Fi({navigate:e}){let[t,n]=(0,g.useState)(()=>zt(Vt.dashboard)??null),[r,i]=(0,g.useState)(``);(0,g.useEffect)(()=>{let e=!1;async function t(){try{let t=await k.dashboard();H(Vt.dashboard,t),e||n(t)}catch(t){e||i(t instanceof Error?t.message:`Failed to load dashboard`)}}t();let r=window.setInterval(()=>void t(),15e3);return()=>{e=!0,window.clearInterval(r)}},[]);let a=t?.counts,o=t?.storage,s=t?.host;return(0,U.jsxs)(`div`,{className:`dashboard-layout`,children:[r&&(0,U.jsx)(q,{children:r}),(0,U.jsxs)(Ii,{title:`Needs attention`,children:[(0,U.jsx)(Li,{icon:(0,U.jsx)(Le,{}),label:`Pending reports`,value:a?In(String(a.PendingReports)):``,loading:!a&&!r,tone:a&&a.PendingReports>0?`warn`:`good`,href:`/moderation`,navigate:e}),(0,U.jsx)(Li,{icon:(0,U.jsx)(ee,{}),label:`Verification requests`,value:a?In(String(a.PendingVerifications)):``,loading:!a&&!r,tone:a&&a.PendingVerifications>0?`warn`:`good`,href:`/verification`,navigate:e})]}),(0,U.jsxs)(Ii,{title:`People & chats`,children:[(0,U.jsx)(Li,{icon:(0,U.jsx)(Pt,{}),label:`Users`,value:a?In(String(a.Users)):``,loading:!a&&!r,href:`/accounts`,navigate:e}),(0,U.jsx)(Li,{icon:(0,U.jsx)(de,{}),label:`Online now`,value:a?In(String(a.OnlineUsers)):``,loading:!a&&!r,sub:`last 5 min`,href:`/accounts`,navigate:e}),(0,U.jsx)(Li,{icon:(0,U.jsx)(ye,{}),label:`Bots`,value:a?In(String(a.Bots)):``,loading:!a&&!r,href:`/bots`,navigate:e}),(0,U.jsx)(Li,{icon:(0,U.jsx)(dt,{}),label:`Channels`,value:a?In(String(a.BroadcastChannels)):``,loading:!a&&!r,href:`/channels`,navigate:e}),(0,U.jsx)(Li,{icon:(0,U.jsx)(ue,{}),label:`Supergroups`,value:a?In(String(a.Supergroups)):``,loading:!a&&!r,href:`/channels`,navigate:e})]}),(0,U.jsxs)(Ii,{title:`Content`,children:[(0,U.jsx)(Li,{icon:(0,U.jsx)(Et,{}),label:`Sticker packs`,value:a?In(String(a.StickerSets)):``,loading:!a&&!r,href:`/stickers`,navigate:e}),(0,U.jsx)(Li,{icon:(0,U.jsx)(wt,{}),label:`Emoji packs`,value:a?In(String(a.EmojiSets)):``,loading:!a&&!r,href:`/emoji`,navigate:e}),(0,U.jsx)(Li,{icon:(0,U.jsx)(Ie,{}),label:`GIFs`,value:a?In(String(a.Gifs)):``,loading:!a&&!r,sub:`saved by users`,href:`/gif-catalog`,navigate:e}),(0,U.jsx)(Li,{icon:(0,U.jsx)(Oe,{}),label:`Media storage used`,value:o?Un(o.PhysicalBytes):``,loading:!o&&!r,sub:o?`${o.BackendKind} backend`:void 0,href:`/storage`,navigate:e})]}),(0,U.jsxs)(Ii,{title:`Server health`,hint:s?.Ready?void 0:`waiting for first sample…`,children:[(0,U.jsx)(Ri,{icon:(0,U.jsx)(De,{}),label:`CPU load`,percent:s?.Ready?s.CPUPercent:void 0,valueText:s?.Ready?`${s.CPUPercent.toFixed(0)}%`:``,loading:!s?.Ready&&!r}),(0,U.jsx)(Ri,{icon:(0,U.jsx)(tt,{}),label:`RAM used`,percent:s?.Ready&&s.MemTotalBytes>0?s.MemUsedBytes/s.MemTotalBytes*100:void 0,valueText:s?.Ready?Un(String(s.MemUsedBytes)):``,loading:!s?.Ready&&!r,sub:s?.Ready?`of ${Un(String(s.MemTotalBytes))}`:void 0}),(0,U.jsx)(Ri,{icon:(0,U.jsx)(Ve,{}),label:`Disk free`,percent:s?.Ready&&s.DiskReady&&s.DiskTotalBytes>0?(s.DiskTotalBytes-s.DiskFreeBytes)/s.DiskTotalBytes*100:void 0,valueText:s?.Ready&&s.DiskReady?Un(String(s.DiskFreeBytes)):`—`,loading:!s?.Ready&&!r,sub:s?.Ready&&s.DiskReady?`of ${Un(String(s.DiskTotalBytes))}`:`no reading yet`,warnAbove:85})]})]})}function Ii({title:e,hint:t,children:n}){return(0,U.jsxs)(`div`,{className:`dashboard-section`,children:[(0,U.jsxs)(`div`,{className:`dashboard-section-title`,children:[e,t&&(0,U.jsx)(`span`,{children:t})]}),(0,U.jsx)(`div`,{className:`dashboard-grid`,children:n})]})}function Li({icon:e,label:t,value:n,sub:r,tone:i=`neutral`,href:a,navigate:o,loading:s=!1}){let c=i===`neutral`?``:` ${i}`,l=(0,U.jsxs)(U.Fragment,{children:[(0,U.jsxs)(`div`,{className:`stat-tile-head`,children:[(0,U.jsx)(`span`,{className:`stat-tile-icon`,children:e}),i===`warn`&&(0,U.jsx)(ce,{size:15,className:`stat-tile-open`})]}),(0,U.jsx)(`div`,{className:`stat-tile-value`,"aria-busy":s||void 0,children:s?(0,U.jsx)(`span`,{className:`skeleton skeleton-value`,"aria-label":`Loading`}):n}),(0,U.jsx)(`div`,{className:`stat-tile-label`,children:t}),r&&(0,U.jsx)(`div`,{className:`stat-tile-sub`,children:r})]});return a&&o?(0,U.jsx)(`a`,{className:`stat-tile clickable${c}`,href:a,onClick:e=>{e.preventDefault(),o(a)},children:l}):(0,U.jsx)(`div`,{className:`stat-tile${c}`,children:l})}function Ri({icon:e,label:t,percent:n,valueText:r,sub:i,warnAbove:a=90,loading:o=!1}){let s=n===void 0?0:Math.max(0,Math.min(100,n)),c=n===void 0?`neutral`:n>=a?`danger`:n>=a-15?`warn`:`neutral`;return(0,U.jsxs)(`div`,{className:`stat-tile${c===`neutral`?``:` ${c}`}`,children:[(0,U.jsx)(`div`,{className:`stat-tile-head`,children:(0,U.jsx)(`span`,{className:`stat-tile-icon`,children:e})}),(0,U.jsx)(`div`,{className:`stat-tile-value`,"aria-busy":o||void 0,children:o?(0,U.jsx)(`span`,{className:`skeleton skeleton-value`,"aria-label":`Loading`}):r}),(0,U.jsx)(`div`,{className:`stat-tile-label`,children:t}),i&&(0,U.jsx)(`div`,{className:`stat-tile-sub`,children:i}),(0,U.jsx)(`div`,{className:`stat-tile-bar`,children:(0,U.jsx)(`span`,{style:{width:`${s}%`}})})]})}function zi(e){if(!e)return null;try{let t=JSON.parse(e);return!t||typeof t!=`object`||Object.keys(t).length===0?null:t}catch{return null}}function Bi(e){let t=e.kind??``;switch(t){case`photo`:return{icon:B,title:`Photo`,detail:e.photo?.id?`id ${e.photo.id}`:``};case`document`:{let t=e.document??{},n=[t.mime_type,t.size?Un(String(t.size)):``,t.duration?`${t.duration}s`:``].filter(Boolean);return{icon:Fe,title:e.voice?`Voice message`:e.round?`Round video`:e.video?`Video`:`File`,detail:[t.file_name,n.join(` · `)].filter(Boolean).join(` — `)}}case`contact`:{let t=e.contact??{};return{icon:Te,title:`Contact`,detail:[[t.first_name,t.last_name].filter(Boolean).join(` `),t.phone_number].filter(Boolean).join(` · `)}}case`geo`:return{icon:$e,title:`Location`,detail:e.geo?`${e.geo.lat}, ${e.geo.long}`:``};case`geo_live`:return{icon:$e,title:`Live location`,detail:e.geo_live?`${e.geo_live.lat}, ${e.geo_live.long}`:``};case`venue`:return{icon:$e,title:`Venue`,detail:[e.venue?.title,e.venue?.address].filter(Boolean).join(` — `)};case`poll`:return{icon:Ye,title:e.poll?.closed?`Poll (closed)`:`Poll`,detail:[e.poll?.question,e.poll?.answers?`${e.poll.answers.length} options`:``].filter(Boolean).join(` — `)};case`web_page`:return{icon:Je,title:`Link preview`,detail:[e.web_page?.title,e.web_page?.url].filter(Boolean).join(` — `)};case`story`:return{icon:se,title:`Story`,detail:e.story?.id?`id ${e.story.id}`:``};case`todo`:return{icon:Ye,title:`Checklist`,detail:e.todo?.title??``};case`dice`:return{icon:ke,title:`Dice`,detail:[e.dice?.emoticon,e.dice?.value].filter(Boolean).join(` `)};case`giveaway`:return{icon:ze,title:`Giveaway`,detail:``};case`service`:return{icon:_t,title:`Service action`,detail:e.service_action?.kind??e.service_action?.type??``};default:return{icon:dt,title:t?`Media (${t})`:`Media`,detail:``}}}function Vi({body:e,media:t,sender:n,meta:r,badges:i}){let a=zi(t),o=a?Bi(a):null,s=o?.icon,c=e?.trim()??``;return(0,U.jsxs)(`section`,{className:`message-view`,children:[(0,U.jsxs)(`div`,{className:`message-view-head`,children:[(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`strong`,{children:n}),(0,U.jsx)(`small`,{children:r})]}),i&&(0,U.jsx)(`div`,{className:`entity-badges`,children:i})]}),(0,U.jsxs)(`div`,{className:`message-bubble`,children:[c?(0,U.jsx)(`p`,{className:`message-text`,children:c}):(0,U.jsx)(`p`,{className:`message-text empty`,children:o?`No caption`:`No text`}),o&&s&&(0,U.jsxs)(`div`,{className:`message-attachment`,children:[(0,U.jsx)(`span`,{className:`message-attachment-icon`,children:(0,U.jsx)(s,{size:16})}),(0,U.jsxs)(`span`,{className:`message-attachment-copy`,children:[(0,U.jsx)(`strong`,{children:o.title}),o.detail&&(0,U.jsx)(`small`,{children:o.detail})]})]}),a&&(a.spoiler||a.ttl_seconds)&&(0,U.jsxs)(`div`,{className:`message-flags`,children:[a.spoiler&&(0,U.jsx)(`span`,{className:`chip`,children:`Spoiler`}),a.ttl_seconds?(0,U.jsx)(`span`,{className:`chip`,children:`Self-destructs after ${a.ttl_seconds}s`}):null]})]})]})}function Hi({channelID:e,msgID:t,navigate:n}){let[r,i]=(0,g.useState)(null),[a,o]=(0,g.useState)(``);async function s(){o(``);try{i(await k.groupMessage(e,t))}catch(e){o(O(e))}}if((0,g.useEffect)(()=>{s()},[e,t]),a)return(0,U.jsx)(q,{children:a});if(!r)return(0,U.jsx)(Zn,{label:`Loading`});let c=r.Message;return(0,U.jsx)(Gn,{title:`Group Message #${c.ID}`,eyebrow:`Message Detail`,actions:(0,U.jsxs)(`button`,{className:`btn icon-text`,onClick:()=>n(`/messages/groups`),children:[(0,U.jsx)(pe,{size:15}),` `,`Back to group messages`]}),children:(0,U.jsxs)(`div`,{className:`stacked-sections`,children:[(0,U.jsx)(Vi,{body:c.Body,media:c.Media,sender:c.Post?`Channel post in ${c.ChannelID}`:`From ${c.SenderUserID}`,meta:`${Nn(c.Date)}${c.EditDate?` · edited ${Nn(c.EditDate)}`:``}${c.ViewsCount?` · ${c.ViewsCount} views`:``}`,badges:(0,U.jsxs)(U.Fragment,{children:[c.Deleted?(0,U.jsx)(J,{tone:`danger`,children:`Deleted`}):(0,U.jsx)(J,{children:`Live`}),c.Pinned&&(0,U.jsx)(J,{tone:`warn`,children:`Pinned`}),c.Post&&(0,U.jsx)(J,{children:`Channel post`})]})}),(0,U.jsxs)(`div`,{className:`summary-grid`,children:[(0,U.jsx)(X,{label:`Message ID`,value:String(c.ID),mono:!0}),(0,U.jsx)(X,{label:`Channel / Group`,value:String(c.ChannelID),mono:!0}),(0,U.jsx)(X,{label:`From Peer`,value:`${c.FromPeerType}:${c.FromPeerID}`,mono:!0}),(0,U.jsx)(X,{label:`pts`,value:String(c.PTS),mono:!0})]}),(0,U.jsxs)(`details`,{className:`raw-details`,children:[(0,U.jsx)(`summary`,{children:`Stored rows (JSON)`}),(0,U.jsxs)(`div`,{className:`stacked-sections`,children:[(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(K,{title:`Channel Message Row`,text:`channel_messages read-only snapshot`}),(0,U.jsx)(Qn,{value:r.MessageJSON})]}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(K,{title:`Channel Row`,text:`channels read-only snapshot`}),(0,U.jsx)(Qn,{value:r.ChannelJSON})]})]})]}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(K,{title:`Channel Update Events`,text:`durable channel_update_events`}),(0,U.jsx)(`div`,{className:`table-wrap`,children:(0,U.jsxs)(`table`,{className:`data-table`,children:[(0,U.jsx)(`thead`,{children:(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`th`,{children:`PTS`}),(0,U.jsx)(`th`,{children:`Count`}),(0,U.jsx)(`th`,{children:`Type`}),(0,U.jsx)(`th`,{children:`Message ID`}),(0,U.jsx)(`th`,{children:`Sender`}),(0,U.jsx)(`th`,{children:`Time`})]})}),(0,U.jsxs)(`tbody`,{children:[r.UpdateEvents.map(e=>(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`td`,{children:e.PTS}),(0,U.jsx)(`td`,{children:e.PTSCount}),(0,U.jsx)(`td`,{children:e.Type}),(0,U.jsx)(`td`,{children:e.MessageID}),(0,U.jsx)(`td`,{children:e.SenderUserID}),(0,U.jsx)(`td`,{children:Nn(e.Date)})]},`${e.PTS}-${e.Type}-${e.MessageID}`)),r.UpdateEvents.length===0&&(0,U.jsx)(Yn,{colSpan:6})]})]})})]}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(K,{title:`Event JSON`}),(0,U.jsxs)(`div`,{className:`raw-grid`,children:[r.UpdateEvents.map(e=>(0,U.jsx)(Qn,{value:e.JSON},`${e.PTS}-${e.Type}-json`)),r.UpdateEvents.length===0&&(0,U.jsx)(`div`,{className:`empty-panel`,children:`No results`})]})]})]})})}function Ui({ownerUserID:e,msgID:t,navigate:n}){let[r,i]=(0,g.useState)(null),[a,o]=(0,g.useState)(``);async function s(){o(``);try{i(await k.message(e,t))}catch(e){o(O(e))}}if((0,g.useEffect)(()=>{s()},[e,t]),a)return(0,U.jsx)(q,{children:a});if(!r)return(0,U.jsx)(Zn,{label:`Loading`});let c=r.Message;return(0,U.jsx)(Gn,{title:`Message #${c.BoxID}`,eyebrow:`Message Detail`,actions:(0,U.jsxs)(`button`,{className:`btn icon-text`,onClick:()=>n(`/messages/private`),children:[(0,U.jsx)(pe,{size:15}),` `,`Back to private messages`]}),children:(0,U.jsx)(qn,{main:(0,U.jsxs)(`div`,{className:`stacked-sections`,children:[(0,U.jsx)(Vi,{body:c.Body,media:c.Media,sender:`From ${c.FromUserID}`,meta:`${c.Outgoing?`Sent to`:`Received from`} ${c.PeerID} · ${Nn(c.Date)}`,badges:(0,U.jsxs)(U.Fragment,{children:[c.Deleted?(0,U.jsx)(J,{tone:`danger`,children:`Deleted`}):(0,U.jsx)(J,{children:`Live`}),(0,U.jsx)(J,{children:c.Outgoing?`Outgoing`:`Incoming`})]})}),(0,U.jsxs)(`div`,{className:`summary-grid`,children:[(0,U.jsx)(X,{label:`Message box ID`,value:String(c.BoxID),mono:!0}),(0,U.jsx)(X,{label:`Private message ID`,value:String(c.PrivateMessageID),mono:!0}),(0,U.jsx)(X,{label:`Message sender`,value:String(c.MessageSenderID),mono:!0}),(0,U.jsx)(X,{label:`pts`,value:String(c.PTS),mono:!0})]}),(0,U.jsxs)(`details`,{className:`raw-details`,children:[(0,U.jsx)(`summary`,{children:`Stored rows (JSON)`}),(0,U.jsxs)(`div`,{className:`stacked-sections`,children:[(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(K,{title:`Message Box`,text:`message_boxes read-only snapshot`}),(0,U.jsx)(Qn,{value:r.MessageJSON})]}),(0,U.jsxs)(`div`,{className:`raw-grid`,children:[(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(K,{title:`Dialog Row`,text:`dialogs read-only snapshot`}),(0,U.jsx)(Qn,{value:r.DialogJSON})]}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(K,{title:`Private Message Row`,text:`private_messages read-only snapshot`}),(0,U.jsx)(Qn,{value:r.PrivateJSON})]})]})]})]}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(K,{title:`Update Events`,text:`durable user_update_events`}),(0,U.jsx)(`div`,{className:`table-wrap`,children:(0,U.jsxs)(`table`,{className:`data-table`,children:[(0,U.jsx)(`thead`,{children:(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`th`,{children:`PTS`}),(0,U.jsx)(`th`,{children:`Count`}),(0,U.jsx)(`th`,{children:`Type`}),(0,U.jsx)(`th`,{children:`Time`})]})}),(0,U.jsxs)(`tbody`,{children:[r.UpdateEvents.map(e=>(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`td`,{children:e.PTS}),(0,U.jsx)(`td`,{children:e.PTSCount}),(0,U.jsx)(`td`,{children:e.Type}),(0,U.jsx)(`td`,{children:Nn(e.Date)})]},`${e.PTS}-${e.Type}`)),r.UpdateEvents.length===0&&(0,U.jsx)(Yn,{colSpan:4})]})]})})]}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(K,{title:`Dispatch Queue`,text:`online/offline dispatch_outbox`}),(0,U.jsx)(`div`,{className:`table-wrap`,children:(0,U.jsxs)(`table`,{className:`data-table`,children:[(0,U.jsx)(`thead`,{children:(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`th`,{children:`ID`}),(0,U.jsx)(`th`,{children:`User ID`}),(0,U.jsx)(`th`,{children:`PTS`}),(0,U.jsx)(`th`,{children:`Type`}),(0,U.jsx)(`th`,{children:`Status`}),(0,U.jsx)(`th`,{children:`Attempts`}),(0,U.jsx)(`th`,{children:`Updated`})]})}),(0,U.jsxs)(`tbody`,{children:[r.Outbox.map(e=>(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`td`,{children:e.ID}),(0,U.jsx)(`td`,{children:e.TargetUserID}),(0,U.jsx)(`td`,{children:e.PTS}),(0,U.jsx)(`td`,{children:e.EventType}),(0,U.jsx)(`td`,{children:e.Status}),(0,U.jsx)(`td`,{children:e.Attempts}),(0,U.jsx)(`td`,{children:G(e.UpdatedAt)})]},e.ID)),r.Outbox.length===0&&(0,U.jsx)(Yn,{colSpan:7})]})]})})]})]}),side:(0,U.jsxs)(`section`,{className:`action-dock`,children:[(0,U.jsx)(`div`,{className:`dock-title`,children:`Operations`}),(0,U.jsx)(Z,{label:`Delete this message`,icon:(0,U.jsx)(Ot,{size:15}),path:`/api/actions/delete-messages`,payload:()=>({owner_user_id:c.OwnerUserID,peer_id:c.PeerID,ids:[c.BoxID],revoke:!0}),onDone:s})]})})})}function Wi({navigate:e}){let[t,n]=(0,g.useState)(null),[r,i]=(0,g.useState)(``),[a,o]=(0,g.useState)(``),[s,c]=(0,g.useState)(`100`),[l,u]=(0,g.useState)(null),[d,f]=(0,g.useState)(``);async function p(e=!1){if(f(``),!t){f(`Search and select a supergroup or channel first`);return}let n=new URLSearchParams({channel_id:String(t.ID),limit:s});if(e&&l?.rows.length){let e=l.rows[l.rows.length-1];n.set(`before_date`,String(e.Date)),n.set(`before_id`,String(e.ID)),i(String(e.Date)),o(String(e.ID))}else r&&n.set(`before_date`,r),a&&n.set(`before_id`,a);try{u(await k.groupMessages(n))}catch(e){f(O(e))}}function m(e){n(e),i(``),o(``),u(null)}let h=l?.rows??[];return(0,U.jsxs)(U.Fragment,{children:[d&&(0,U.jsx)(q,{children:d}),(0,U.jsxs)(Kn,{children:[(0,U.jsx)(`div`,{className:`message-selector-grid single`,children:(0,U.jsx)(mi,{label:`Channel / Group`,value:t,onChange:m})}),(0,U.jsxs)(`form`,{className:`toolbar message-query`,onSubmit:e=>{e.preventDefault(),p(!1)},children:[(0,U.jsx)(`input`,{value:r,onChange:e=>i(e.target.value),placeholder:`before_date cursor`}),(0,U.jsx)(`input`,{value:a,onChange:e=>o(e.target.value),placeholder:`before_msg_id cursor`}),(0,U.jsx)(`input`,{className:`small-input`,value:s,onChange:e=>c(e.target.value),placeholder:`limit <= 100`}),(0,U.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,children:[(0,U.jsx)(V,{size:15}),` `,`Search messages`]}),h.length?(0,U.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>p(!0),children:[(0,U.jsx)(z,{size:15}),` `,`Next page`]}):null]})]}),(0,U.jsxs)(`div`,{className:`metric-row`,children:[(0,U.jsx)(Y,{label:`Messages on page`,value:String(h.length)}),(0,U.jsx)(Y,{label:`With media`,value:String(h.filter(e=>e.Media&&e.Media!==`{}`).length)}),(0,U.jsx)(Y,{label:`Channel posts`,value:String(h.filter(e=>e.Post).length)}),(0,U.jsx)(Y,{label:`Channel / Group`,value:t?`${t.Title||Mn(t)} (${t.ID})`:`-`})]}),(0,U.jsx)(`div`,{className:`table-wrap`,children:(0,U.jsxs)(`table`,{className:`data-table`,children:[(0,U.jsx)(`thead`,{children:(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`th`,{children:`Message ID`}),(0,U.jsx)(`th`,{children:`Time`}),(0,U.jsx)(`th`,{children:`Sender`}),(0,U.jsx)(`th`,{children:`From Peer`}),(0,U.jsx)(`th`,{children:`PTS`}),(0,U.jsx)(`th`,{children:`Views`}),(0,U.jsx)(`th`,{children:`Status`}),(0,U.jsx)(`th`,{children:`Body`}),(0,U.jsx)(`th`,{})]})}),(0,U.jsxs)(`tbody`,{children:[h.map(t=>(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`td`,{className:`mono`,children:t.ID}),(0,U.jsx)(`td`,{children:Nn(t.Date)}),(0,U.jsx)(`td`,{className:`mono`,children:t.SenderUserID}),(0,U.jsxs)(`td`,{className:`mono`,children:[t.FromPeerType,`:`,t.FromPeerID]}),(0,U.jsx)(`td`,{children:t.PTS}),(0,U.jsx)(`td`,{children:t.ViewsCount}),(0,U.jsx)(`td`,{children:t.Deleted?(0,U.jsx)(J,{tone:`danger`,children:`Deleted`}):t.Pinned?(0,U.jsx)(J,{tone:`warn`,children:`Pinned`}):(0,U.jsx)(J,{children:`Live`})}),(0,U.jsx)(`td`,{className:`truncate`,children:t.Body}),(0,U.jsx)(`td`,{children:(0,U.jsxs)(`button`,{className:`row-link`,onClick:()=>e(`/messages/groups/detail?channel_id=${t.ChannelID}&msg_id=${t.ID}`),children:[`Details`,` `,(0,U.jsx)(z,{size:14})]})})]},`${t.ChannelID}-${t.ID}`)),h.length===0&&(0,U.jsx)(Yn,{colSpan:9})]})]})})]})}function Gi({navigate:e}){let[t,n]=(0,g.useState)(null),[r,i]=(0,g.useState)(null),[a,o]=(0,g.useState)(``),[s,c]=(0,g.useState)(``),[l,u]=(0,g.useState)(`100`),[d,f]=(0,g.useState)(``),[p,m]=(0,g.useState)(!0),[h,_]=(0,g.useState)(!1),[v,y]=(0,g.useState)(``),[b,x]=(0,g.useState)(`1`),[S,C]=(0,g.useState)(null),[w,T]=(0,g.useState)(``);async function E(e=!1){if(T(``),!t||!r){T(`Search and select the owner user and peer user first`);return}let n=new URLSearchParams({owner_user_id:String(t.ID),peer_id:String(r.ID),limit:l});if(e&&S?.rows.length){let e=S.rows[S.rows.length-1];n.set(`before_date`,String(e.Date)),n.set(`before_id`,String(e.BoxID)),o(String(e.Date)),c(String(e.BoxID))}else a&&n.set(`before_date`,a),s&&n.set(`before_id`,s);try{C(await k.messages(n))}catch(e){T(O(e))}}function D(e){n(e),o(``),c(``),C(null)}function A(e){i(e),o(``),c(``),C(null)}return(0,U.jsxs)(U.Fragment,{children:[w&&(0,U.jsx)(q,{children:w}),(0,U.jsxs)(Kn,{children:[(0,U.jsxs)(`div`,{className:`message-selector-grid`,children:[(0,U.jsx)(di,{label:`Owner user`,value:t,onChange:D}),(0,U.jsx)(di,{label:`Peer user`,value:r,onChange:A})]}),(0,U.jsxs)(`form`,{className:`toolbar message-query`,onSubmit:e=>{e.preventDefault(),E(!1)},children:[(0,U.jsx)(`input`,{value:a,onChange:e=>o(e.target.value),placeholder:`before_date cursor`}),(0,U.jsx)(`input`,{value:s,onChange:e=>c(e.target.value),placeholder:`before_msg_id cursor`}),(0,U.jsx)(`input`,{className:`small-input`,value:l,onChange:e=>u(e.target.value),placeholder:`limit <= 100`}),(0,U.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,children:[(0,U.jsx)(V,{size:15}),` `,`Search messages`]}),S?.rows.length?(0,U.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>E(!0),children:[(0,U.jsx)(z,{size:15}),` `,`Next page`]}):null]})]}),(0,U.jsxs)(`div`,{className:`metric-row`,children:[(0,U.jsx)(Y,{label:`Messages on page`,value:String(S?.rows.length??0)}),(0,U.jsx)(Y,{label:`Deleted`,value:String((S?.rows??[]).filter(e=>e.Deleted).length),tone:`danger`}),(0,U.jsx)(Y,{label:`Outgoing`,value:String((S?.rows??[]).filter(e=>e.Outgoing).length)}),(0,U.jsx)(Y,{label:`Owner / Peer`,value:t&&r?`${jn(t)} / ${jn(r)}`:`-`})]}),(0,U.jsxs)(`div`,{className:`operation-row`,children:[(0,U.jsxs)(`div`,{className:`operation-box`,children:[(0,U.jsxs)(`div`,{className:`operation-title`,children:[(0,U.jsx)(Ot,{size:15}),` `,`Delete selected messages`]}),(0,U.jsx)(`input`,{value:d,onChange:e=>f(e.target.value),placeholder:`Message IDs, comma separated`}),(0,U.jsxs)(`label`,{className:`checkline`,children:[(0,U.jsx)(`input`,{type:`checkbox`,checked:p,onChange:e=>m(e.target.checked)}),` `,`Revoke for both sides`]}),(0,U.jsx)(Z,{path:`/api/actions/delete-messages`,label:`Dry-run delete`,payload:()=>({owner_user_id:t?.ID??0,peer_id:r?.ID??0,ids:Wn(d,`Message IDs are invalid`),revoke:p})})]}),(0,U.jsxs)(`div`,{className:`operation-box`,children:[(0,U.jsxs)(`div`,{className:`operation-title`,children:[(0,U.jsx)(He,{size:15}),` `,`Clear private history`]}),(0,U.jsx)(`input`,{value:v,onChange:e=>y(e.target.value),placeholder:`max_id cutoff`}),(0,U.jsx)(`input`,{value:b,onChange:e=>x(e.target.value),placeholder:`max_batches`}),(0,U.jsxs)(`label`,{className:`checkline`,children:[(0,U.jsx)(`input`,{type:`checkbox`,checked:p,onChange:e=>m(e.target.checked)}),` `,`Revoke for both sides`]}),(0,U.jsxs)(`label`,{className:`checkline`,children:[(0,U.jsx)(`input`,{type:`checkbox`,checked:h,onChange:e=>_(e.target.checked)}),` `,`Clear only this side`]}),(0,U.jsx)(Z,{path:`/api/actions/delete-history`,label:`Dry-run clear history`,payload:()=>({owner_user_id:t?.ID??0,peer_id:r?.ID??0,max_id:Fn(v),max_batches:Fn(b),just_clear:h,revoke:p})})]})]}),(0,U.jsx)(`div`,{className:`table-wrap`,children:(0,U.jsxs)(`table`,{className:`data-table`,children:[(0,U.jsx)(`thead`,{children:(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`th`,{children:`Message ID`}),(0,U.jsx)(`th`,{children:`Time`}),(0,U.jsx)(`th`,{children:`Sender`}),(0,U.jsx)(`th`,{children:`Direction`}),(0,U.jsx)(`th`,{children:`PTS`}),(0,U.jsx)(`th`,{children:`Status`}),(0,U.jsx)(`th`,{children:`Body`}),(0,U.jsx)(`th`,{})]})}),(0,U.jsxs)(`tbody`,{children:[S?.rows.map(t=>(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`td`,{className:`mono`,children:t.BoxID}),(0,U.jsx)(`td`,{children:Nn(t.Date)}),(0,U.jsx)(`td`,{className:`mono`,children:t.FromUserID}),(0,U.jsx)(`td`,{children:t.Outgoing?`Outgoing`:`Incoming`}),(0,U.jsx)(`td`,{children:t.PTS}),(0,U.jsx)(`td`,{children:t.Deleted?(0,U.jsx)(J,{tone:`danger`,children:`Deleted`}):(0,U.jsx)(J,{children:`Live`})}),(0,U.jsx)(`td`,{className:`truncate`,children:t.Body}),(0,U.jsx)(`td`,{children:(0,U.jsxs)(`button`,{className:`row-link`,onClick:()=>e(`/messages/private/detail?owner_user_id=${t.OwnerUserID}&msg_id=${t.BoxID}`),children:[`Details`,` `,(0,U.jsx)(z,{size:14})]})})]},`${t.OwnerUserID}-${t.BoxID}`)),(!S||S.rows.length===0)&&(0,U.jsx)(Yn,{colSpan:8})]})]})})]})}function Ki({navigate:e,tab:t,onTab:n}){return(0,U.jsxs)(Gn,{title:`Messages`,eyebrow:`Message boxes and channel history`,children:[(0,U.jsxs)(`div`,{className:`tab-bar`,role:`tablist`,"aria-label":`Message sections`,children:[(0,U.jsx)(`button`,{className:`tab-btn ${t===`private`?`active`:``}`,type:`button`,role:`tab`,"aria-selected":t===`private`,onClick:()=>n(`private`),children:`Private`}),(0,U.jsx)(`button`,{className:`tab-btn ${t===`groups`?`active`:``}`,type:`button`,role:`tab`,"aria-selected":t===`groups`,onClick:()=>n(`groups`),children:`Groups and channels`})]}),t===`private`?(0,U.jsx)(Gi,{navigate:e}):(0,U.jsx)(Wi,{navigate:e})]})}var qi=c(o(((e,t)=>{typeof document<`u`&&typeof navigator<`u`&&(function(n,r){typeof e==`object`&&t!==void 0?t.exports=r():typeof define==`function`&&define.amd?define(r):(n=typeof globalThis<`u`?globalThis:n||self,n.lottie=r())})(e,(function(){var n=``,r=!1,i=-999999,a=function(e){r=!!e},o=function(){return r},s=function(e){n=e},c=function(){return n};function l(e){return document.createElement(e)}function u(e,t){var n,r=e.length,i;for(n=0;n1?n[1]=1:n[1]<=0&&(n[1]=0),F(n[0],n[1],n[2])}function I(e,t){var n=ne(e[0]*255,e[1]*255,e[2]*255);return n[2]+=t,n[2]>1?n[2]=1:n[2]<0&&(n[2]=0),F(n[0],n[1],n[2])}function ie(e,t){var n=ne(e[0]*255,e[1]*255,e[2]*255);return n[0]+=t/360,n[0]>1?--n[0]:n[0]<0&&(n[0]+=1),F(n[0],n[1],n[2])}(function(){var e=[],t,n;for(t=0;t<256;t+=1)n=t.toString(16),e[t]=n.length===1?`0`+n:n;return function(t,n,r){return t<0&&(t=0),n<0&&(n=0),r<0&&(r=0),`#`+e[t]+e[n]+e[r]}})();var ae=function(e){g=!!e},oe=function(){return g},se=function(e){_=e},ce=function(){return _},le=function(){return v},ue=function(e){E=e},de=function(){return E},fe=function(e){y=e};function L(e){return document.createElementNS(`http://www.w3.org/2000/svg`,e)}function pe(e){"@babel/helpers - typeof";return pe=typeof Symbol==`function`&&typeof Symbol.iterator==`symbol`?function(e){return typeof e}:function(e){return e&&typeof Symbol==`function`&&e.constructor===Symbol&&e!==Symbol.prototype?`symbol`:typeof e},pe(e)}var me=function(){var e=1,t=[],n,r,i={onmessage:function(){},postMessage:function(e){n({data:e})}},a={postMessage:function(e){i.onmessage({data:e})}};function s(e){if(window.Worker&&window.Blob&&o()){var t=new Blob([`var _workerSelf = self; self.onmessage = `,e.toString()],{type:`text/javascript`}),r=URL.createObjectURL(t);return new Worker(r)}return n=e,i}function c(){r||(r=s(function(e){function t(){function e(t,n){var o,s,c=t.length,l,u,d,f;for(s=0;s=0;--t)if(e[t].ty===`sh`)if(e[t].ks.k.i)a(e[t].ks.k);else for(o=e[t].ks.k.length,r=0;rn[0]?!0:n[0]>e[0]?!1:e[1]>n[1]?!0:n[1]>e[1]?!1:e[2]>n[2]?!0:n[2]>e[2]?!1:null}var s=function(){var e=[4,4,14];function t(e){var t=e.t.d;e.t.d={k:[{s:t,t:0}]}}function n(e){var n,r=e.length;for(n=0;n=0;--n)if(e[n].ty===`sh`)if(e[n].ks.k.i)e[n].ks.k.c=e[n].closed;else for(a=e[n].ks.k.length,i=0;i500)&&(this._imageLoaded(),clearInterval(n)),t+=1}.bind(this),50)}function a(t){var n=r(t,this.assetsPath,this.path),i=L(`image`);b?this.testImageLoaded(i):i.addEventListener(`load`,this._imageLoaded,!1),i.addEventListener(`error`,function(){a.img=e,this._imageLoaded()}.bind(this),!1),i.setAttributeNS(`http://www.w3.org/1999/xlink`,`href`,n),this._elementHelper.append?this._elementHelper.append(i):this._elementHelper.appendChild(i);var a={img:i,assetData:t};return a}function o(t){var n=r(t,this.assetsPath,this.path),i=l(`img`);i.crossOrigin=`anonymous`,i.addEventListener(`load`,this._imageLoaded,!1),i.addEventListener(`error`,function(){a.img=e,this._imageLoaded()}.bind(this),!1),i.src=n;var a={img:i,assetData:t};return a}function s(e){var t={assetData:e},n=r(e,this.assetsPath,this.path);return me.loadData(n,function(e){t.img=e,this._footageLoaded()}.bind(this),function(){t.img={},this._footageLoaded()}.bind(this)),t}function c(e,t){this.imagesLoadedCb=t;var n,r=e.length;for(n=0;nthis.animationData.op&&(this.animationData.op=e.op,this.totalFrames=Math.floor(e.op-this.animationData.ip));var t=this.animationData.layers,n,r=t.length,i=e.layers,a,o=i.length;for(a=0;athis.timeCompleted&&(this.currentFrame=this.timeCompleted),this.trigger(`enterFrame`),this.renderFrame(),this.trigger(`drawnFrame`)},R.prototype.renderFrame=function(){if(!(this.isLoaded===!1||!this.renderer))try{this.expressionsPlugin&&this.expressionsPlugin.resetFrame(),this.renderer.renderFrame(this.currentFrame+this.firstFrame)}catch(e){this.triggerRenderFrameError(e)}},R.prototype.play=function(e){e&&this.name!==e||this.isPaused===!0&&(this.isPaused=!1,this.trigger(`_play`),this.audioController.resume(),this._idle&&(this._idle=!1,this.trigger(`_active`)))},R.prototype.pause=function(e){e&&this.name!==e||this.isPaused===!1&&(this.isPaused=!0,this.trigger(`_pause`),this._idle=!0,this.trigger(`_idle`),this.audioController.pause())},R.prototype.togglePause=function(e){e&&this.name!==e||(this.isPaused===!0?this.play():this.pause())},R.prototype.stop=function(e){e&&this.name!==e||(this.pause(),this.playCount=0,this._completedLoop=!1,this.setCurrentRawFrameValue(0))},R.prototype.getMarkerData=function(e){for(var t,n=0;n=this.totalFrames-1&&this.frameModifier>0?!this.loop||this.playCount===this.loop?this.checkSegments(t>this.totalFrames?t%this.totalFrames:0)||(n=!0,t=this.totalFrames-1):t>=this.totalFrames?(this.playCount+=1,this.checkSegments(t%this.totalFrames)||(this.setCurrentRawFrameValue(t%this.totalFrames),this._completedLoop=!0,this.trigger(`loopComplete`))):this.setCurrentRawFrameValue(t):t<0?this.checkSegments(t%this.totalFrames)||(this.loop&&!(this.playCount--<=0&&this.loop!==!0)?(this.setCurrentRawFrameValue(this.totalFrames+t%this.totalFrames),this._completedLoop?this.trigger(`loopComplete`):this._completedLoop=!0):(n=!0,t=0)):this.setCurrentRawFrameValue(t),n&&(this.setCurrentRawFrameValue(t),this.pause(),this.trigger(`complete`))}},R.prototype.adjustSegment=function(e,t){this.playCount=0,e[1]0&&(this.playSpeed<0?this.setSpeed(-this.playSpeed):this.setDirection(-1)),this.totalFrames=e[0]-e[1],this.timeCompleted=this.totalFrames,this.firstFrame=e[1],this.setCurrentRawFrameValue(this.totalFrames-.001-t)):e[1]>e[0]&&(this.frameModifier<0&&(this.playSpeed<0?this.setSpeed(-this.playSpeed):this.setDirection(1)),this.totalFrames=e[1]-e[0],this.timeCompleted=this.totalFrames,this.firstFrame=e[0],this.setCurrentRawFrameValue(.001+t)),this.trigger(`segmentStart`)},R.prototype.setSegment=function(e,t){var n=-1;this.isPaused&&(this.currentRawFrame+this.firstFramet&&(n=t-e)),this.firstFrame=e,this.totalFrames=t-e,this.timeCompleted=this.totalFrames,n!==-1&&this.goToAndStop(n,!0)},R.prototype.playSegments=function(e,t){if(t&&(this.segments.length=0),Ce(e[0])===`object`){var n,r=e.length;for(n=0;n=0;--n)t[n].animation.destroy(e)}function T(e,t,n){var r=[].concat([].slice.call(document.getElementsByClassName(`lottie`)),[].slice.call(document.getElementsByClassName(`bodymovin`))),i,a=r.length;for(i=0;i0?n=c:t=c;while(Math.abs(s)>a&&++l=i?g(e,d,t,n):f===0?d:h(e,a,a+c,t,n)}},e}(),Te=function(){function e(e){return e.concat(m(e.length))}return{double:e}}(),Ee=function(){return function(e,t,n){var r=0,i=e,a=m(i),o={newElement:s,release:c};function s(){var e;return r?(--r,e=a[r]):e=t(),e}function c(e){r===i&&(a=Te.double(a),i*=2),n&&n(e),a[r]=e,r+=1}return o}}(),De=function(){function e(){return{addedLength:0,percents:p(`float32`,de()),lengths:p(`float32`,de())}}return Ee(8,e)}(),Oe=function(){function e(){return{lengths:[],totalLength:0}}function t(e){var t,n=e.lengths.length;for(t=0;t-.001&&o<.001}function n(n,r,i,a,o,s,c,l,u){if(i===0&&s===0&&u===0)return t(n,r,a,o,c,l);var d=e.sqrt(e.pow(a-n,2)+e.pow(o-r,2)+e.pow(s-i,2)),f=e.sqrt(e.pow(c-n,2)+e.pow(l-r,2)+e.pow(u-i,2)),p=e.sqrt(e.pow(c-a,2)+e.pow(l-o,2)+e.pow(u-s,2)),m=d>f?d>p?d-f-p:p-f-d:p>f?p-f-d:f-d-p;return m>-1e-4&&m<1e-4}var r=function(){return function(e,t,n,r){var i=de(),a,o,s,c,l,u=0,d,f=[],p=[],m=De.newElement();for(s=n.length,a=0;ao?-1:1,l=!0;l;)if(r[a]<=o&&r[a+1]>o?(s=(o-r[a])/(r[a+1]-r[a]),l=!1):a+=c,a<0||a>=i-1){if(a===i-1)return n[a];l=!1}return n[a]+(n[a+1]-n[a])*s}function l(t,n,r,i,a,o){var s=c(a,o),l=1-s;return[e.round((l*l*l*t[0]+(s*l*l+l*s*l+l*l*s)*r[0]+(s*s*l+l*s*s+s*l*s)*i[0]+s*s*s*n[0])*1e3)/1e3,e.round((l*l*l*t[1]+(s*l*l+l*s*l+l*l*s)*r[1]+(s*s*l+l*s*s+s*l*s)*i[1]+s*s*s*n[1])*1e3)/1e3]}var u=p(`float32`,8);function d(t,n,r,i,a,o,s){a<0?a=0:a>1&&(a=1);var l=c(a,s);o=o>1?1:o;var d=c(o,s),f,p=t.length,m=1-l,h=1-d,g=m*m*m,_=l*m*m*3,v=l*l*m*3,y=l*l*l,b=m*m*h,x=l*m*h+m*l*h+m*m*d,S=l*l*h+m*l*d+l*m*d,C=l*l*d,w=m*h*h,T=l*h*h+m*d*h+m*h*d,E=l*d*h+m*d*d+l*h*d,D=l*d*d,O=h*h*h,k=d*h*h+h*d*h+h*h*d,A=d*d*h+h*d*d+d*h*d,j=d*d*d;for(f=0;f=l.t-n){c.h&&(c=l),i=0;break}if(l.t-n>e){i=a;break}a=v||e=v?x.points.length-1:0;for(f=x.points[S].point.length,d=0;d=T&&C=v)r[0]=b[0],r[1]=b[1],r[2]=b[2];else if(e<=y)r[0]=c.s[0],r[1]=c.s[1],r[2]=c.s[2];else{var j=Ie(c.s),M=Ie(b),N=(e-y)/(v-y);Fe(r,Pe(j,M,N))}else for(a=0;a=v?m=1:e1e-6?(f=Math.acos(p),m=Math.sin(f),h=Math.sin((1-n)*f)/m,g=Math.sin(n*f)/m):(h=1-n,g=n),r[0]=h*i+g*c,r[1]=h*a+g*l,r[2]=h*o+g*u,r[3]=h*s+g*d,r}function Fe(e,t){var n=t[0],r=t[1],i=t[2],a=t[3],o=Math.atan2(2*r*a-2*n*i,1-2*r*r-2*i*i),s=Math.asin(2*n*r+2*i*a),c=Math.atan2(2*n*a-2*r*i,1-2*n*n-2*i*i);e[0]=o/D,e[1]=s/D,e[2]=c/D}function Ie(e){var t=e[0]*D,n=e[1]*D,r=e[2]*D,i=Math.cos(t/2),a=Math.cos(n/2),o=Math.cos(r/2),s=Math.sin(t/2),c=Math.sin(n/2),l=Math.sin(r/2),u=i*a*o-s*c*l;return[s*c*o+i*a*l,s*a*o+i*c*l,i*c*o-s*a*l,u]}function Le(){var e=this.comp.renderedFrame-this.offsetTime,t=this.keyframes[0].t-this.offsetTime,n=this.keyframes[this.keyframes.length-1].t-this.offsetTime;if(!(e===this._caching.lastFrame||this._caching.lastFrame!==je&&(this._caching.lastFrame>=n&&e>=n||this._caching.lastFrame=e&&(this._caching._lastKeyframeIndex=-1,this._caching.lastIndex=0);var r=this.interpolateValue(e,this._caching);this.pv=r}return this._caching.lastFrame=e,this.pv}function Re(e){var t;if(this.propType===`unidimensional`)t=e*this.mult,Me(this.v-t)>1e-5&&(this.v=t,this._mdf=!0);else for(var n=0,r=this.v.length;n1e-5&&(this.v[n]=t,this._mdf=!0),n+=1}function ze(){if(!(this.elem.globalData.frameId===this.frameId||!this.effectsSequence.length)){if(this.lock){this.setVValue(this.pv);return}this.lock=!0,this._mdf=this._isFirstFrame;var e,t=this.effectsSequence.length,n=this.kf?this.pv:this.data.k;for(e=0;e=this._maxLength&&this.doubleArrayLength(),n){case`v`:a=this.v;break;case`i`:a=this.i;break;case`o`:a=this.o;break;default:a=[];break}(!a[r]||a[r]&&!i)&&(a[r]=Ke.newElement()),a[r][0]=e,a[r][1]=t},qe.prototype.setTripleAt=function(e,t,n,r,i,a,o,s){this.setXYAt(e,t,`v`,o,s),this.setXYAt(n,r,`o`,o,s),this.setXYAt(i,a,`i`,o,s)},qe.prototype.reverse=function(){var e=new qe;e.setPathData(this.c,this._length);var t=this.v,n=this.o,r=this.i,i=0;this.c&&(e.setTripleAt(t[0][0],t[0][1],r[0][0],r[0][1],n[0][0],n[0][1],0,!1),i=1);var a=this._length-1,o=this._length,s;for(s=i;s=p[p.length-1].t-this.offsetTime)i=p[p.length-1].s?p[p.length-1].s[0]:p[p.length-2].e[0],o=!0;else{for(var m=r,h=p.length-1,g=!0,_,v,y;g&&(_=p[m],v=p[m+1],!(v.t-this.offsetTime>e));)m=v.t-this.offsetTime)d=1;else if(e<_.t-this.offsetTime)d=0;else{var b;y.__fnct?b=y.__fnct:(b=we.getBezierEasing(_.o.x,_.o.y,_.i.x,_.i.y).get,y.__fnct=b),d=b((e-(_.t-this.offsetTime))/(v.t-this.offsetTime-(_.t-this.offsetTime)))}a=v.s?v.s[0]:_.e[0]}i=_.s[0]}for(l=t._length,u=i.i[0].length,n.lastIndex=r,s=0;sr&&t>r)||(this._caching.lastIndex=i0||e>-1e-6&&e<0?r(e*t)/t:e}function P(){var e=this.props,t=N(e[0]),n=N(e[1]),r=N(e[4]),i=N(e[5]),a=N(e[12]),o=N(e[13]);return`matrix(`+t+`,`+n+`,`+r+`,`+i+`,`+a+`,`+o+`)`}return function(){this.reset=i,this.rotate=a,this.rotateX=o,this.rotateY=s,this.rotateZ=c,this.skew=u,this.skewFromAxis=d,this.shear=l,this.scale=f,this.setTransform=m,this.translate=h,this.transform=g,this.multiply=_,this.applyToPoint=S,this.applyToX=C,this.applyToY=w,this.applyToZ=T,this.applyToPointArray=A,this.applyToTriplePoints=k,this.applyToPointStringified=j,this.toCSS=M,this.to2dCSS=P,this.clone=b,this.cloneFromProps=x,this.equals=y,this.inversePoints=O,this.inversePoint=D,this.getInverseMatrix=E,this._t=this.transform,this.isIdentity=v,this._identity=!0,this._identityCalculated=!1,this.props=p(`float32`,16),this.reset()}}();function $e(e){"@babel/helpers - typeof";return $e=typeof Symbol==`function`&&typeof Symbol.iterator==`symbol`?function(e){return typeof e}:function(e){return e&&typeof Symbol==`function`&&e.constructor===Symbol&&e!==Symbol.prototype?`symbol`:typeof e},$e(e)}var et={},tt=`__[STANDALONE]__`,nt=`__[ANIMATIONDATA]__`,rt=``;function it(e){s(e)}function at(){tt===!0?z.searchAnimations(nt,tt,rt):z.searchAnimations()}function ot(e){ae(e)}function st(e){fe(e)}function ct(e){return tt===!0&&(e.animationData=JSON.parse(nt)),z.loadAnimation(e)}function lt(e){if(typeof e==`string`)switch(e){case`high`:ue(200);break;default:case`medium`:ue(50);break;case`low`:ue(10);break}else!isNaN(e)&&e>1&&ue(e)}function ut(){return typeof navigator<`u`}function dt(e,t){e===`expressions`&&se(t)}function ft(e){switch(e){case`propertyFactory`:return B;case`shapePropertyFactory`:return Ze;case`matrix`:return Qe;default:return null}}et.play=z.play,et.pause=z.pause,et.setLocationHref=it,et.togglePause=z.togglePause,et.setSpeed=z.setSpeed,et.setDirection=z.setDirection,et.stop=z.stop,et.searchAnimations=at,et.registerAnimation=z.registerAnimation,et.loadAnimation=ct,et.setSubframeRendering=ot,et.resize=z.resize,et.goToAndStop=z.goToAndStop,et.destroy=z.destroy,et.setQuality=lt,et.inBrowser=ut,et.installPlugin=dt,et.freeze=z.freeze,et.unfreeze=z.unfreeze,et.setVolume=z.setVolume,et.mute=z.mute,et.unmute=z.unmute,et.getRegisteredAnimations=z.getRegisteredAnimations,et.useWebWorker=a,et.setIDPrefix=st,et.__getFactory=ft,et.version=`5.13.0`;function pt(){document.readyState===`complete`&&(clearInterval(_t),at())}function mt(e){for(var t=V.split(`&`),n=0;n=1?a.push({s:e-1,e:t-1}):(a.push({s:e,e:1}),a.push({s:0,e:t-1}));var o=[],s,c=a.length,l;for(s=0;sr+n)){var u=l.s*i<=r?0:(l.s*i-r)/n,d=l.e*i>=r+n?1:(l.e*i-r)/n;o.push([u,d])}return o.length||o.push([0,0]),o},bt.prototype.releasePathsData=function(e){var t,n=e.length;for(t=0;t1?1+r:this.s.v<0?0+r:this.s.v+r,n=this.e.v>1?1+r:this.e.v<0?0+r:this.e.v+r,t>n){var i=t;t=n,n=i}t=Math.round(t*1e4)*1e-4,n=Math.round(n*1e4)*1e-4,this.sValue=t,this.eValue=n}else t=this.sValue,n=this.eValue;var a,o,s=this.shapes.length,c,l,u,d,f,p=0;if(n===t)for(o=0;o=0;--o)if(h=this.shapes[o],h.shape._mdf){for(g=h.localShapeCollection,g.releaseShapes(),this.m===2&&s>1?(b=this.calculateShapeEdges(t,n,h.totalShapeLength,y,p),y+=h.totalShapeLength):b=[[_,v]],l=b.length,c=0;c=1?m.push({s:h.totalShapeLength*(_-1),e:h.totalShapeLength*(v-1)}):(m.push({s:h.totalShapeLength*_,e:h.totalShapeLength}),m.push({s:0,e:h.totalShapeLength*(v-1)}));var x=this.addShapes(h,m[0]);if(m[0].s!==m[0].e){if(m.length>1)if(h.shape.paths.shapes[h.shape.paths._length-1].c){var S=x.pop();this.addPaths(x,g),x=this.addShapes(h,m[1],S)}else this.addPaths(x,g),x=this.addShapes(h,m[1]);this.addPaths(x,g)}}h.shape.paths=g}}else if(this._mdf)for(o=0;ot.e){n.c=!1;break}else t.s<=l&&t.e>=l+u.addedLength?(this.addSegment(i[a].v[s-1],i[a].o[s-1],i[a].i[s],i[a].v[s],n,d,g),g=!1):(p=Ae.getNewSegment(i[a].v[s-1],i[a].v[s],i[a].o[s-1],i[a].i[s],(t.s-l)/u.addedLength,(t.e-l)/u.addedLength,f[s-1]),this.addSegmentFromArray(p,n,d,g),g=!1,n.c=!1),l+=u.addedLength,d+=1;if(i[a].c&&f.length){if(u=f[s-1],l<=t.e){var _=f[s-1].addedLength;t.s<=l&&t.e>=l+_?(this.addSegment(i[a].v[s-1],i[a].o[s-1],i[a].i[0],i[a].v[0],n,d,g),g=!1):(p=Ae.getNewSegment(i[a].v[s-1],i[a].v[0],i[a].o[s-1],i[a].i[0],(t.s-l)/_,(t.e-l)/_,f[s-1]),this.addSegmentFromArray(p,n,d,g),g=!1,n.c=!1)}else n.c=!1;l+=u.addedLength,d+=1}if(n._length&&(n.setXYAt(n.v[h][0],n.v[h][1],`i`,h),n.setXYAt(n.v[n._length-1][0],n.v[n._length-1][1],`o`,n._length-1)),l>t.e)break;a=this.p.keyframes[this.p.keyframes.length-1].t?(r=this.p.getValueAtTime(this.p.keyframes[this.p.keyframes.length-1].t/n,0),i=this.p.getValueAtTime((this.p.keyframes[this.p.keyframes.length-1].t-.05)/n,0)):(r=this.p.pv,i=this.p.getValueAtTime((this.p._caching.lastFrame+this.p.offsetTime-.01)/n,this.p.offsetTime));else if(this.px&&this.px.keyframes&&this.py.keyframes&&this.px.getValueAtTime&&this.py.getValueAtTime){r=[],i=[];var a=this.px,o=this.py;a._caching.lastFrame+a.offsetTime<=a.keyframes[0].t?(r[0]=a.getValueAtTime((a.keyframes[0].t+.01)/n,0),r[1]=o.getValueAtTime((o.keyframes[0].t+.01)/n,0),i[0]=a.getValueAtTime(a.keyframes[0].t/n,0),i[1]=o.getValueAtTime(o.keyframes[0].t/n,0)):a._caching.lastFrame+a.offsetTime>=a.keyframes[a.keyframes.length-1].t?(r[0]=a.getValueAtTime(a.keyframes[a.keyframes.length-1].t/n,0),r[1]=o.getValueAtTime(o.keyframes[o.keyframes.length-1].t/n,0),i[0]=a.getValueAtTime((a.keyframes[a.keyframes.length-1].t-.01)/n,0),i[1]=o.getValueAtTime((o.keyframes[o.keyframes.length-1].t-.01)/n,0)):(r=[a.pv,o.pv],i[0]=a.getValueAtTime((a._caching.lastFrame+a.offsetTime-.01)/n,a.offsetTime),i[1]=o.getValueAtTime((o._caching.lastFrame+o.offsetTime-.01)/n,o.offsetTime))}else i=e,r=i;this.v.rotate(-Math.atan2(r[1]-i[1],r[0]-i[0]))}this.data.p&&this.data.p.s?this.data.p.z?this.v.translate(this.px.v,this.py.v,-this.pz.v):this.v.translate(this.px.v,this.py.v,0):this.v.translate(this.p.v[0],this.p.v[1],-this.p.v[2])}this.frameId=this.elem.globalData.frameId}}function r(){if(this.appliedTransformations=0,this.pre.reset(),!this.a.effectsSequence.length)this.pre.translate(-this.a.v[0],-this.a.v[1],this.a.v[2]),this.appliedTransformations=1;else return;if(!this.s.effectsSequence.length)this.pre.scale(this.s.v[0],this.s.v[1],this.s.v[2]),this.appliedTransformations=2;else return;if(this.sk)if(!this.sk.effectsSequence.length&&!this.sa.effectsSequence.length)this.pre.skewFromAxis(-this.sk.v,this.sa.v),this.appliedTransformations=3;else return;this.r?this.r.effectsSequence.length||(this.pre.rotate(-this.r.v),this.appliedTransformations=4):!this.rz.effectsSequence.length&&!this.ry.effectsSequence.length&&!this.rx.effectsSequence.length&&!this.or.effectsSequence.length&&(this.pre.rotateZ(-this.rz.v).rotateY(this.ry.v).rotateX(this.rx.v).rotateZ(-this.or.v[2]).rotateY(this.or.v[1]).rotateX(this.or.v[0]),this.appliedTransformations=4)}function i(){}function a(e){this._addDynamicProperty(e),this.elem.addDynamicProperty(e),this._isDirty=!0}function o(e,t,n){if(this.elem=e,this.frameId=-1,this.propType=`transform`,this.data=t,this.v=new Qe,this.pre=new Qe,this.appliedTransformations=0,this.initDynamicPropertyContainer(n||e),t.p&&t.p.s?(this.px=B.getProp(e,t.p.x,0,0,this),this.py=B.getProp(e,t.p.y,0,0,this),t.p.z&&(this.pz=B.getProp(e,t.p.z,0,0,this))):this.p=B.getProp(e,t.p||{k:[0,0,0]},1,0,this),t.rx){if(this.rx=B.getProp(e,t.rx,0,D,this),this.ry=B.getProp(e,t.ry,0,D,this),this.rz=B.getProp(e,t.rz,0,D,this),t.or.k[0].ti){var r,i=t.or.k.length;for(r=0;r0;)--n,this._elements.unshift(t[n]);this.dynamicProperties.length?this.k=!0:this.getValue(!0)},Ct.prototype.resetElements=function(e){var t,n=e.length;for(t=0;t0?Math.floor(f):Math.ceil(f),h=this.pMatrix.props,g=this.rMatrix.props,_=this.sMatrix.props;this.pMatrix.reset(),this.rMatrix.reset(),this.sMatrix.reset(),this.tMatrix.reset(),this.matrix.reset();var v=0;if(f>0){for(;vm;)this.applyTransforms(this.pMatrix,this.rMatrix,this.sMatrix,this.tr,1,!0),--v;p&&(this.applyTransforms(this.pMatrix,this.rMatrix,this.sMatrix,this.tr,-p,!0),v-=p)}r=this.data.m===1?0:this._currentCopies-1,i=this.data.m===1?1:-1,a=this._currentCopies;for(var y,b;a;){if(t=this.elemsData[r].it,n=t[t.length-1].transform.mProps.v.props,b=n.length,t[t.length-1].transform.mProps._mdf=!0,t[t.length-1].transform.op._mdf=!0,t[t.length-1].transform.op.v=this._currentCopies===1?this.so.v:this.so.v+(this.eo.v-this.so.v)*(r/(this._currentCopies-1)),v!==0){for((r!==0&&i===1||r!==this._currentCopies-1&&i===-1)&&this.applyTransforms(this.pMatrix,this.rMatrix,this.sMatrix,this.tr,1,!1),this.matrix.transform(g[0],g[1],g[2],g[3],g[4],g[5],g[6],g[7],g[8],g[9],g[10],g[11],g[12],g[13],g[14],g[15]),this.matrix.transform(_[0],_[1],_[2],_[3],_[4],_[5],_[6],_[7],_[8],_[9],_[10],_[11],_[12],_[13],_[14],_[15]),this.matrix.transform(h[0],h[1],h[2],h[3],h[4],h[5],h[6],h[7],h[8],h[9],h[10],h[11],h[12],h[13],h[14],h[15]),y=0;y0&&r<1?[t]:[]:[t-r,t+r].filter(function(e){return e>0&&e<1})},Mt.prototype.split=function(e){if(e<=0)return[jt(this.points[0]),this];if(e>=1)return[this,jt(this.points[this.points.length-1])];var t=Ot(this.points[0],this.points[1],e),n=Ot(this.points[1],this.points[2],e),r=Ot(this.points[2],this.points[3],e),i=Ot(t,n,e),a=Ot(n,r,e),o=Ot(i,a,e);return[new Mt(this.points[0],t,i,o,!0),new Mt(o,a,r,this.points[3],!0)]};function Nt(e,t){var n=e.points[0][t],r=e.points[e.points.length-1][t];if(n>r){var i=r;r=n,n=i}for(var a=kt(3*e.a[t],2*e.b[t],e.c[t]),o=0;o0&&a[o]<1){var s=e.point(a[o])[t];sr&&(r=s)}return{min:n,max:r}}Mt.prototype.bounds=function(){return{x:Nt(this,0),y:Nt(this,1)}},Mt.prototype.boundingBox=function(){var e=this.bounds();return{left:e.x.min,right:e.x.max,top:e.y.min,bottom:e.y.max,width:e.x.max-e.x.min,height:e.y.max-e.y.min,cx:(e.x.max+e.x.min)/2,cy:(e.y.max+e.y.min)/2}};function Pt(e,t,n){var r=e.boundingBox();return{cx:r.cx,cy:r.cy,width:r.width,height:r.height,bez:e,t:(t+n)/2,t1:t,t2:n}}function Ft(e){var t=e.bez.split(.5);return[Pt(t[0],e.t1,e.t),Pt(t[1],e.t,e.t2)]}function It(e,t){return Math.abs(e.cx-t.cx)*2=a||e.width<=r&&e.height<=r&&t.width<=r&&t.height<=r){i.push([e.t,t.t]);return}var o=Ft(e),s=Ft(t);Lt(o[0],s[0],n+1,r,i,a),Lt(o[0],s[1],n+1,r,i,a),Lt(o[1],s[0],n+1,r,i,a),Lt(o[1],s[1],n+1,r,i,a)}}Mt.prototype.intersections=function(e,t,n){t===void 0&&(t=2),n===void 0&&(n=7);var r=[];return Lt(Pt(this,0,1),Pt(e,0,1),0,t,r,n),r},Mt.shapeSegment=function(e,t){var n=(t+1)%e.length();return new Mt(e.v[t],e.o[t],e.i[n],e.v[n],!0)},Mt.shapeSegmentInverted=function(e,t){var n=(t+1)%e.length();return new Mt(e.v[n],e.i[n],e.o[t],e.v[t],!0)};function Rt(e,t){return[e[1]*t[2]-e[2]*t[1],e[2]*t[0]-e[0]*t[2],e[0]*t[1]-e[1]*t[0]]}function zt(e,t,n,r){var i=[e[0],e[1],1],a=[t[0],t[1],1],o=[n[0],n[1],1],s=[r[0],r[1],1],c=Rt(Rt(i,a),Rt(o,s));return Et(c[2])?null:[c[0]/c[2],c[1]/c[2]]}function H(e,t,n){return[e[0]+Math.cos(t)*n,e[1]-Math.sin(t)*n]}function Bt(e,t){return Math.hypot(e[0]-t[0],e[1]-t[1])}function Vt(e,t){return Tt(e[0],t[0])&&Tt(e[1],t[1])}function Ht(){}u([yt],Ht),Ht.prototype.initModifierProperties=function(e,t){this.getValue=this.processKeys,this.amplitude=B.getProp(e,t.s,0,null,this),this.frequency=B.getProp(e,t.r,0,null,this),this.pointsType=B.getProp(e,t.pt,0,null,this),this._isAnimated=this.amplitude.effectsSequence.length!==0||this.frequency.effectsSequence.length!==0||this.pointsType.effectsSequence.length!==0};function U(e,t,n,r,i,a,o){var s=n-Math.PI/2,c=n+Math.PI/2,l=t[0]+Math.cos(n)*r*i,u=t[1]-Math.sin(n)*r*i;e.setTripleAt(l,u,l+Math.cos(s)*a,u-Math.sin(s)*a,l+Math.cos(c)*o,u-Math.sin(c)*o,e.length())}function Ut(e,t){var n=[t[0]-e[0],t[1]-e[1]],r=-Math.PI*.5;return[Math.cos(r)*n[0]-Math.sin(r)*n[1],Math.sin(r)*n[0]+Math.cos(r)*n[1]]}function Wt(e,t){var n=t===0?e.length()-1:t-1,r=(t+1)%e.length(),i=e.v[n],a=e.v[r],o=Ut(i,a);return Math.atan2(0,1)-Math.atan2(o[1],o[0])}function Gt(e,t,n,r,i,a,o){var s=Wt(t,n),c=t.v[n%t._length],l=t.v[n===0?t._length-1:n-1],u=t.v[(n+1)%t._length],d=a===2?Math.sqrt((c[0]-l[0])**2+(c[1]-l[1])**2):0,f=a===2?Math.sqrt((c[0]-u[0])**2+(c[1]-u[1])**2):0;U(e,t.v[n%t._length],s,o,r,f/((i+1)*2),d/((i+1)*2),a)}function Kt(e,t,n,r,i,a){for(var o=0;o1&&t.length>1&&(i=Xt(e[0],t[t.length-1]),i)?[[e[0].split(i[0])[0]],[t[t.length-1].split(i[1])[1]]]:[n,r]}function Qt(e){for(var t,n=1;n1&&(t=Zt(e[e.length-1],e[0]),e[e.length-1]=t[0],e[0]=t[1]),e}function $t(e,t){var n=e.inflectionPoints(),r,i,a,o;if(n.length===0)return[Jt(e,t)];if(n.length===1||Tt(n[1],1))return a=e.split(n[0]),r=a[0],i=a[1],[Jt(r,t),Jt(i,t)];a=e.split(n[0]),r=a[0];var s=(n[1]-n[0])/(1-n[0]);return a=a[1].split(s),o=a[0],i=a[1],[Jt(r,t),Jt(o,t),Jt(i,t)]}function en(){}u([yt],en),en.prototype.initModifierProperties=function(e,t){this.getValue=this.processKeys,this.amount=B.getProp(e,t.a,0,null,this),this.miterLimit=B.getProp(e,t.ml,0,null,this),this.lineJoin=t.lj,this._isAnimated=this.amount.effectsSequence.length!==0},en.prototype.processPath=function(e,t,n,r){var i=Je.newElement();i.c=e.c;var a=e.length();e.c||--a;var o,s,c,l=[];for(o=0;o=0;--o)c=Mt.shapeSegmentInverted(e,o),l.push($t(c,t));l=Qt(l);var u=null,d=null;for(o=0;o0&&(o=!1),o){var u=l(`style`);u.setAttribute(`f-forigin`,n[r].fOrigin),u.setAttribute(`f-origin`,n[r].origin),u.setAttribute(`f-family`,n[r].fFamily),u.type=`text/css`,u.innerText=`@font-face {font-family: `+n[r].fFamily+`; font-style: normal; src: url('`+n[r].fPath+`');}`,t.appendChild(u)}}else if(n[r].fOrigin===`g`||n[r].origin===1){for(s=document.querySelectorAll(`link[f-forigin="g"], link[f-origin="1"]`),c=0;c=55296&&n<=56319){var r=e.charCodeAt(1);r>=56320&&r<=57343&&(t=(n-55296)*1024+r-56320+65536)}return t}function S(e,t){var n=e.toString(16)+t.toString(16);return d.indexOf(n)!==-1}function C(e){return e===s}function w(e){return e===o}function T(e){var t=x(e);return t>=c&&t<=u}function E(e){return T(e.substr(0,2))&&T(e.substr(2,2))}function D(e){return t.indexOf(e)!==-1}function O(e,t){var o=x(e.substr(t,2));if(o!==n)return!1;var s=0;for(t+=2;s<5;){if(o=x(e.substr(t,2)),oa)return!1;s+=1,t+=2}return x(e.substr(t,2))===r}function k(){this.isLoaded=!0}var A=function(){this.fonts=[],this.chars=null,this.typekitLoaded=0,this.isLoaded=!1,this._warned=!1,this.initTime=Date.now(),this.setIsLoadedBinded=this.setIsLoaded.bind(this),this.checkLoadedFontsBinded=this.checkLoadedFonts.bind(this)};return A.isModifier=S,A.isZeroWidthJoiner=C,A.isFlagEmoji=E,A.isRegionalCode=T,A.isCombinedCharacter=D,A.isRegionalFlag=O,A.isVariationSelector=w,A.BLACK_FLAG_CODE_POINT=n,A.prototype={addChars:_,addFonts:g,getCharData:v,getFontByName:b,measureText:y,checkLoadedFonts:m,setIsLoaded:k},A}();function rn(e){this.animationData=e}rn.prototype.getProp=function(e){return this.animationData.slots&&this.animationData.slots[e.sid]?Object.assign(e,this.animationData.slots[e.sid].p):e};function an(e){return new rn(e)}function on(){}on.prototype={initRenderable:function(){this.isInRange=!1,this.hidden=!1,this.isTransparent=!1,this.renderableComponents=[]},addRenderableComponent:function(e){this.renderableComponents.indexOf(e)===-1&&this.renderableComponents.push(e)},removeRenderableComponent:function(e){this.renderableComponents.indexOf(e)!==-1&&this.renderableComponents.splice(this.renderableComponents.indexOf(e),1)},prepareRenderableFrame:function(e){this.checkLayerLimits(e)},checkTransparency:function(){this.finalTransform.mProp.o.v<=0?!this.isTransparent&&this.globalData.renderConfig.hideOnTransparent&&(this.isTransparent=!0,this.hide()):this.isTransparent&&(this.isTransparent=!1,this.show())},checkLayerLimits:function(e){this.data.ip-this.data.st<=e&&this.data.op-this.data.st>e?this.isInRange!==!0&&(this.globalData._mdf=!0,this._mdf=!0,this.isInRange=!0,this.show()):this.isInRange!==!1&&(this.globalData._mdf=!0,this.isInRange=!1,this.hide())},renderRenderable:function(){var e,t=this.renderableComponents.length;for(e=0;e.1)&&this.audio.seek(this._currentTime/this.globalData.frameRate):(this.audio.play(),this.audio.seek(this._currentTime/this.globalData.frameRate),this._isPlaying=!0))},xn.prototype.show=function(){},xn.prototype.hide=function(){this.audio.pause(),this._isPlaying=!1},xn.prototype.pause=function(){this.audio.pause(),this._isPlaying=!1,this._canPlay=!1},xn.prototype.resume=function(){this._canPlay=!0},xn.prototype.setRate=function(e){this.audio.rate(e)},xn.prototype.volume=function(e){this._volumeMultiplier=e,this._previousVolume=e*this._volume,this.audio.volume(this._previousVolume)},xn.prototype.getBaseElement=function(){return null},xn.prototype.destroy=function(){},xn.prototype.sourceRectAtTime=function(){},xn.prototype.initExpressions=function(){};function Sn(){}Sn.prototype.checkLayers=function(e){var t,n=this.layers.length,r;for(this.completeLayers=!0,t=n-1;t>=0;--t)this.elements[t]||(r=this.layers[t],r.ip-r.st<=e-this.layers[t].st&&r.op-r.st>e-this.layers[t].st&&this.buildItem(t)),this.completeLayers=this.elements[t]?this.completeLayers:!1;this.checkPendingElements()},Sn.prototype.createItem=function(e){switch(e.ty){case 2:return this.createImage(e);case 0:return this.createComp(e);case 1:return this.createSolid(e);case 3:return this.createNull(e);case 4:return this.createShape(e);case 5:return this.createText(e);case 6:return this.createAudio(e);case 13:return this.createCamera(e);case 15:return this.createFootage(e);default:return this.createNull(e)}},Sn.prototype.createCamera=function(){throw Error(`You're using a 3d camera. Try the html renderer.`)},Sn.prototype.createAudio=function(e){return new xn(e,this.globalData,this)},Sn.prototype.createFootage=function(e){return new bn(e,this.globalData,this)},Sn.prototype.buildAllItems=function(){var e,t=this.layers.length;for(e=0;e0&&(this.maskElement.setAttribute(`id`,p),this.element.maskedElement.setAttribute(b,`url(`+c()+`#`+p+`)`),r.appendChild(this.maskElement)),this.viewData.length&&this.element.addRenderableComponent(this)}Tn.prototype.getMaskProperty=function(e){return this.viewData[e].prop},Tn.prototype.renderFrame=function(e){var t=this.element.finalTransform.mat,n,r=this.masksProperties.length;for(n=0;n1&&(r+=` C`+t.o[i-1][0]+`,`+t.o[i-1][1]+` `+t.i[0][0]+`,`+t.i[0][1]+` `+t.v[0][0]+`,`+t.v[0][1]),n.lastPath!==r){var o=``;n.elem&&(t.c&&(o=e.inv?this.solidPath+r:r),n.elem.setAttribute(`d`,o)),n.lastPath=r}},Tn.prototype.destroy=function(){this.element=null,this.globalData=null,this.maskElement=null,this.data=null,this.masksProperties=null};var En=function(){var e={};e.createFilter=t,e.createAlphaToLuminanceFilter=n;function t(e,t){var n=L(`filter`);return n.setAttribute(`id`,e),t!==!0&&(n.setAttribute(`filterUnits`,`objectBoundingBox`),n.setAttribute(`x`,`0%`),n.setAttribute(`y`,`0%`),n.setAttribute(`width`,`100%`),n.setAttribute(`height`,`100%`)),n}function n(){var e=L(`feColorMatrix`);return e.setAttribute(`type`,`matrix`),e.setAttribute(`color-interpolation-filters`,`sRGB`),e.setAttribute(`values`,`0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 1`),e}return e}(),Dn=function(){var e={maskType:!0,svgLumaHidden:!0,offscreenCanvas:typeof OffscreenCanvas<`u`};return(/MSIE 10/i.test(navigator.userAgent)||/MSIE 9/i.test(navigator.userAgent)||/rv:11.0/i.test(navigator.userAgent)||/Edge\/\d./i.test(navigator.userAgent))&&(e.maskType=!1),/firefox/i.test(navigator.userAgent)&&(e.svgLumaHidden=!1),e}(),On={},kn=`filter_result_`;function An(e){var t,n=`SourceGraphic`,r=e.data.ef?e.data.ef.length:0,i=te(),a=En.createFilter(i,!0),o=0;this.filters=[];var s;for(t=0;t=0&&(n=this.shapeModifiers[e].processShapes(this._isFirstFrame),!n);--e);}},searchProcessedElement:function(e){for(var t=this.processedElements,n=0,r=t.length;n.01)return!1;n+=1}return!0},Un.prototype.checkCollapsable=function(){if(this.o.length/2!=this.c.length/4)return!1;if(this.data.k.k[0].s)for(var e=0,t=this.data.k.k.length;e0;)c=r.transformers[g].mProps._mdf||c,--h,--g;if(c)for(h=f-r.styles[u].lvl,g=r.transformers.length-1;h>0;)m.multiply(r.transformers[g].mProps.v),--h,--g}else m=e;if(p=r.sh.paths,o=p._length,c){for(s=``,a=0;a=1?v=.99:v<=-1&&(v=-.99);var y=g*v,b=Math.cos(_+t.a.v)*y+a[0],x=Math.sin(_+t.a.v)*y+a[1];r.setAttribute(`fx`,b),r.setAttribute(`fy`,x),i&&!t.g._collapsable&&(t.of.setAttribute(`fx`,b),t.of.setAttribute(`fy`,x))}}}function u(e,t,n){var r=t.style,i=t.d;i&&(i._mdf||n)&&i.dashStr&&(r.pElem.setAttribute(`stroke-dasharray`,i.dashStr),r.pElem.setAttribute(`stroke-dashoffset`,i.dashoffset[0])),t.c&&(t.c._mdf||n)&&r.pElem.setAttribute(`stroke`,`rgb(`+C(t.c.v[0])+`,`+C(t.c.v[1])+`,`+C(t.c.v[2])+`)`),(t.o._mdf||n)&&r.pElem.setAttribute(`stroke-opacity`,t.o.v),(t.w._mdf||n)&&(r.pElem.setAttribute(`stroke-width`,t.w.v),r.msElem&&r.msElem.setAttribute(`stroke-width`,t.w.v))}return n}();function J(e,t,n){this.shapes=[],this.shapesData=e.shapes,this.stylesList=[],this.shapeModifiers=[],this.itemsData=[],this.processedElements=[],this.animatedContents=[],this.initElement(e,t,n),this.prevViewData=[]}u([vn,wn,W,Pn,jn,yn,Mn],J),J.prototype.initSecondaryElement=function(){},J.prototype.identityMatrix=new Qe,J.prototype.buildExpressionInterface=function(){},J.prototype.createContent=function(){this.searchShapes(this.shapesData,this.itemsData,this.prevViewData,this.layerElement,0,[],!0),this.filterUniqueShapes()},J.prototype.filterUniqueShapes=function(){var e,t=this.shapes.length,n,r,i=this.stylesList.length,a,o=[],s=!1;for(r=0;r1&&s&&this.setShapesAsAnimated(o)}},J.prototype.setShapesAsAnimated=function(e){var t,n=e.length;for(t=0;t=0;--c){if(g=this.searchProcessedElement(e[c]),g?t[c]=n[g-1]:e[c]._render=o,e[c].ty===`fl`||e[c].ty===`st`||e[c].ty===`gf`||e[c].ty===`gs`||e[c].ty===`no`)g?t[c].style.closed=e[c].hd:t[c]=this.createStyleElement(e[c],i),e[c]._render&&t[c].style.pElem.parentNode!==r&&r.appendChild(t[c].style.pElem),f.push(t[c].style);else if(e[c].ty===`gr`){if(!g)t[c]=this.createGroupElement(e[c]);else for(d=t[c].it.length,u=0;u1,this.kf&&this.addEffect(this.getKeyframeValue.bind(this)),this.kf},X.prototype.addEffect=function(e){this.effectsSequence.push(e),this.elem.addDynamicProperty(this)},X.prototype.getValue=function(e){if(!((this.elem.globalData.frameId===this.frameId||!this.effectsSequence.length)&&!e)){this.currentData.t=this.data.d.k[this.keysIndex].s.t;var t=this.currentData,n=this.keysIndex;if(this.lock){this.setCurrentData(this.currentData);return}this.lock=!0,this._mdf=!1;var r,i=this.effectsSequence.length,a=e||this.data.d.k[this.keysIndex].s;for(r=0;rt);)n+=1;return this.keysIndex!==n&&(this.keysIndex=n),this.data.d.k[this.keysIndex].s},X.prototype.buildFinalText=function(e){for(var t=[],n=0,r=e.length,i,a,o=!1,s=!1,c=``;n=55296&&i<=56319?nn.isRegionalFlag(e,n)?c=e.substr(n,14):(a=e.charCodeAt(n+1),a>=56320&&a<=57343&&(nn.isModifier(i,a)?(c=e.substr(n,2),o=!0):c=nn.isFlagEmoji(e.substr(n,4))?e.substr(n,4):e.substr(n,2))):i>56319?(a=e.charCodeAt(n+1),nn.isVariationSelector(i)&&(o=!0)):nn.isZeroWidthJoiner(i)&&(o=!0,s=!0),o?(t[t.length-1]+=c,o=!1):t.push(c),n+=c.length;return t},X.prototype.completeTextData=function(e){e.__complete=!0;var t=this.elem.globalData.fontManager,n=this.data,r=[],i,a,o,s=0,c,l=n.m.g,u=0,d=0,f=0,p=[],m=0,h=0,g,_,v=t.getFontByName(e.f),y,b=0,x=tn(v);e.fWeight=x.weight,e.fStyle=x.style,e.finalSize=e.s,e.finalText=this.buildFinalText(e.t),a=e.finalText.length,e.finalLineHeight=e.lh;var S=e.tr/1e3*e.finalSize,C;if(e.sz)for(var w=!0,T=e.sz[0],E=e.sz[1],D,O;w;){O=this.buildFinalText(e.t),D=0,m=0,a=O.length,S=e.tr/1e3*e.finalSize;var k=-1;for(i=0;iT&&O[i]!==` `?(k===-1?a+=1:i=k,D+=e.finalLineHeight||e.finalSize*1.2,O.splice(i,+(k===i),`\r`),k=-1,m=0):(m+=b,m+=S);D+=v.ascent*e.finalSize/100,this.canResize&&e.finalSize>this.minimumFontSize&&Eh?m:h,m=-2*S,c=``,o=!0,f+=1):c=j,t.chars?(y=t.getCharData(j,v.fStyle,t.getFontByName(e.f).fFamily),b=o?0:y.w*e.finalSize/100):b=t.measureText(c,e.f,e.finalSize),j===` `?A+=b+S:(m+=b+S+A,A=0),r.push({l:b,an:b,add:u,n:o,anIndexes:[],val:c,line:f,animatorJustifyOffset:0}),l==2){if(u+=b,c===``||c===` `||i===a-1){for((c===``||c===` `)&&(u-=b);d<=i;)r[d].an=u,r[d].ind=s,r[d].extra=b,d+=1;s+=1,u=0}}else if(l==3){if(u+=b,c===``||i===a-1){for(c===``&&(u-=b);d<=i;)r[d].an=u,r[d].ind=s,r[d].extra=b,d+=1;u=0,s+=1}}else r[s].ind=s,r[s].extra=0,s+=1;if(e.l=r,h=m>h?m:h,p.push(m),e.sz)e.boxWidth=e.sz[0],e.justifyOffset=0;else switch(e.boxWidth=h,e.j){case 1:e.justifyOffset=-e.boxWidth;break;case 2:e.justifyOffset=-e.boxWidth/2;break;default:e.justifyOffset=0}e.lineWidths=p;var M=n.a,N,P;_=M.length;var ee,te,F=[];for(g=0;g<_;g+=1){for(N=M[g],N.a.sc&&(e.strokeColorAnim=!0),N.a.sw&&(e.strokeWidthAnim=!0),(N.a.fc||N.a.fh||N.a.fs||N.a.fb)&&(e.fillColorAnim=!0),te=0,ee=N.s.b,i=0;i0?i=this.ne.v/100:a=-this.ne.v/100,this.xe.v>0?o=1-this.xe.v/100:s=1+this.xe.v/100;var c=we.getBezierEasing(i,a,o,s).get,l=0,u=this.finalS,d=this.finalE,f=this.data.sh;if(f===2)l=d===u?+(r>=d):e(0,t(.5/(d-u)+(r-u)/(d-u),1)),l=c(l);else if(f===3)l=d===u?r>=d?0:1:1-e(0,t(.5/(d-u)+(r-u)/(d-u),1)),l=c(l);else if(f===4)d===u?l=0:(l=e(0,t(.5/(d-u)+(r-u)/(d-u),1)),l<.5?l*=2:l=1-2*(l-.5)),l=c(l);else if(f===5){if(d===u)l=0;else{var p=d-u;r=t(e(0,r+.5-u),d-u);var m=-p/2+r,h=p/2;l=Math.sqrt(1-m*m/(h*h))}l=c(l)}else f===6?(d===u?l=0:(r=t(e(0,r+.5-u),d-u),l=(1+Math.cos(Math.PI+Math.PI*2*r/(d-u)))/2),l=c(l)):(r>=n(u)&&(l=r-u<0?e(0,t(t(d,1)-(u-r),1)):e(0,t(d-r,1))),l=c(l));if(this.sm.v!==100){var g=this.sm.v*.01;g===0&&(g=1e-8);var _=.5-g*.5;l<_?l=0:(l=(l-_)/g,l>1&&(l=1))}return l*this.a.v},getValue:function(e){this.iterateDynamicProperties(),this._mdf=e||this._mdf,this._currentTextLength=this.elem.textProperty.currentData.l.length||0,e&&this.data.r===2&&(this.e.v=this._currentTextLength);var t=this.data.r===2?1:100/this.data.totalChars,n=this.o.v/t,r=this.s.v/t+n,i=this.e.v/t+n;if(r>i){var a=r;r=i,i=a}this.finalS=r,this.finalE=i}},u([Ge],r);function i(e,t,n){return new r(e,t,n)}return{getTextSelectorProp:i}}();function Yn(e,t,n){var r={propType:!1},i=B.getProp,a=t.a;this.a={r:a.r?i(e,a.r,0,D,n):r,rx:a.rx?i(e,a.rx,0,D,n):r,ry:a.ry?i(e,a.ry,0,D,n):r,sk:a.sk?i(e,a.sk,0,D,n):r,sa:a.sa?i(e,a.sa,0,D,n):r,s:a.s?i(e,a.s,1,.01,n):r,a:a.a?i(e,a.a,1,0,n):r,o:a.o?i(e,a.o,0,.01,n):r,p:a.p?i(e,a.p,1,0,n):r,sw:a.sw?i(e,a.sw,0,0,n):r,sc:a.sc?i(e,a.sc,1,0,n):r,fc:a.fc?i(e,a.fc,1,0,n):r,fh:a.fh?i(e,a.fh,0,0,n):r,fs:a.fs?i(e,a.fs,0,.01,n):r,fb:a.fb?i(e,a.fb,0,.01,n):r,t:a.t?i(e,a.t,0,0,n):r},this.s=Jn.getTextSelectorProp(e,t.s,n),this.s.t=t.s.t}function Xn(e,t,n){this._isFirstFrame=!0,this._hasMaskedPath=!1,this._frameId=-1,this._textData=e,this._renderType=t,this._elem=n,this._animatorsData=m(this._textData.a.length),this._pathData={},this._moreOptions={alignment:{}},this.renderedLetters=[],this.lettersChangedFlag=!1,this.initDynamicPropertyContainer(n)}Xn.prototype.searchProperties=function(){var e,t=this._textData.a.length,n,r=B.getProp;for(e=0;e=m+Te||!x?(T=(m+Te-g)/h.partialLength,oe=b.point[0]+(h.point[0]-b.point[0])*T,se=b.point[1]+(h.point[1]-b.point[1])*T,a.translate(-n[0]*f[u].an*.005,-(n[1]*A)*.01),_=!1):x&&(g+=h.partialLength,v+=1,v>=x.length&&(v=0,y+=1,S[y]?x=S[y].points:D.v.c?(v=0,y=0,x=S[y].points):(g-=h.partialLength,x=null)),x&&(b=h,h=x[v],C=h.partialLength));ae=f[u].an/2-f[u].add,a.translate(-ae,0,0)}else ae=f[u].an/2-f[u].add,a.translate(-ae,0,0),a.translate(-n[0]*f[u].an*.005,-n[1]*A*.01,0);for(P=0;Pe?this.textSpans[e].span:L(s?`g`:`text`),b<=e){if(c.setAttribute(`stroke-linecap`,`butt`),c.setAttribute(`stroke-linejoin`,`round`),c.setAttribute(`stroke-miterlimit`,`4`),this.textSpans[e].span=c,s){var S=L(`g`);c.appendChild(S),this.textSpans[e].childSpan=S}this.textSpans[e].span=c,this.layerElement.appendChild(c)}c.style.display=`inherit`}if(l.reset(),d&&(o[e].n&&(f=-g,p+=n.yOffset,p+=+!!h,h=!1),this.applyTextPropertiesToMatrix(n,l,o[e].line,f,p),f+=o[e].l||0,f+=g),s){x=this.globalData.fontManager.getCharData(n.finalText[e],r.fStyle,this.globalData.fontManager.getFontByName(n.f).fFamily);var C;if(x.t===1)C=new ir(x.data,this.globalData,this);else{var w=Qn;x.data&&x.data.shapes&&(w=this.buildShapeData(x.data,n.finalSize)),C=new J(w,this.globalData,this)}if(this.textSpans[e].glyph){var T=this.textSpans[e].glyph;this.textSpans[e].childSpan.removeChild(T.layerElement),T.destroy()}this.textSpans[e].glyph=C,C._debug=!0,C.prepareFrame(0),C.renderFrame(),this.textSpans[e].childSpan.appendChild(C.layerElement),x.t===1&&this.textSpans[e].childSpan.setAttribute(`transform`,`scale(`+n.finalSize/100+`,`+n.finalSize/100+`)`)}else d&&c.setAttribute(`transform`,`translate(`+l.props[12]+`,`+l.props[13]+`)`),c.textContent=o[e].val,c.setAttributeNS(`http://www.w3.org/XML/1998/namespace`,`xml:space`,`preserve`)}d&&c&&c.setAttribute(`d`,u)}for(;e=0;--t)(this.completeLayers||this.elements[t])&&this.elements[t].prepareFrame(e-this.layers[t].st);if(this.globalData._mdf)for(t=0;t=0;--n)(this.completeLayers||this.elements[n])&&(this.elements[n].prepareFrame(this.renderedFrame-this.layers[n].st),this.elements[n]._mdf&&(this._mdf=!0))}},rr.prototype.renderInnerContent=function(){var e,t=this.layers.length;for(e=0;e=0;--n)e.finalTransform.multiply(e.transforms[n].transform.mProps.v);e._mdf=i},processSequences:function(e){var t,n=this.sequenceList.length;for(t=0;t=1){this.buffers=[];var e=this.globalData.canvasContext,t=lr.createCanvas(e.canvas.width,e.canvas.height);this.buffers.push(t);var n=lr.createCanvas(e.canvas.width,e.canvas.height);this.buffers.push(n),this.data.tt>=3&&!document._isProxy&&lr.loadLumaCanvas()}this.canvasContext=this.globalData.canvasContext,this.transformCanvas=this.globalData.transformCanvas,this.renderableEffectsManager=new ur(this),this.searchEffectTransforms()},createContent:function(){},setBlendMode:function(){var e=this.globalData;if(e.blendMode!==this.data.bm){e.blendMode=this.data.bm;var t=sn(this.data.bm);e.canvasContext.globalCompositeOperation=t}},createRenderableComponents:function(){this.maskManager=new dr(this.data,this),this.transformEffects=this.renderableEffectsManager.getEffects(Cn.TRANSFORM_EFFECT)},hideElement:function(){!this.hidden&&(!this.isInRange||this.isTransparent)&&(this.hidden=!0)},showElement:function(){this.isInRange&&!this.isTransparent&&(this.hidden=!1,this._isFirstFrame=!0,this.maskManager._isFirstFrame=!0)},clearCanvas:function(e){e.clearRect(this.transformCanvas.tx,this.transformCanvas.ty,this.transformCanvas.w*this.transformCanvas.sx,this.transformCanvas.h*this.transformCanvas.sy)},prepareLayer:function(){if(this.data.tt>=1){var e=this.buffers[0].getContext(`2d`);this.clearCanvas(e),e.drawImage(this.canvasContext.canvas,0,0),this.currentTransform=this.canvasContext.getTransform(),this.canvasContext.setTransform(1,0,0,1,0,0),this.clearCanvas(this.canvasContext),this.canvasContext.setTransform(this.currentTransform)}},exitLayer:function(){if(this.data.tt>=1){var e=this.buffers[1],t=e.getContext(`2d`);if(this.clearCanvas(t),t.drawImage(this.canvasContext.canvas,0,0),this.canvasContext.setTransform(1,0,0,1,0,0),this.clearCanvas(this.canvasContext),this.canvasContext.setTransform(this.currentTransform),this.comp.getElementById(`tp`in this.data?this.data.tp:this.data.ind-1).renderFrame(!0),this.canvasContext.setTransform(1,0,0,1,0,0),this.data.tt>=3&&!document._isProxy){var n=lr.getLumaCanvas(this.canvasContext.canvas);n.getContext(`2d`).drawImage(this.canvasContext.canvas,0,0),this.clearCanvas(this.canvasContext),this.canvasContext.drawImage(n,0,0)}this.canvasContext.globalCompositeOperation=pr[this.data.tt],this.canvasContext.drawImage(e,0,0),this.canvasContext.globalCompositeOperation=`destination-over`,this.canvasContext.drawImage(this.buffers[0],0,0),this.canvasContext.setTransform(this.currentTransform),this.canvasContext.globalCompositeOperation=`source-over`}},renderFrame:function(e){if(!(this.hidden||this.data.hd)&&!(this.data.td===1&&!e)){this.renderTransform(),this.renderRenderable(),this.renderLocalTransform(),this.setBlendMode();var t=this.data.ty===0;this.prepareLayer(),this.globalData.renderer.save(t),this.globalData.renderer.ctxTransform(this.finalTransform.localMat.props),this.globalData.renderer.ctxOpacity(this.finalTransform.localOpacity),this.renderInnerContent(),this.globalData.renderer.restore(t),this.exitLayer(),this.maskManager.hasMasks&&this.globalData.renderer.restore(!0),this._isFirstFrame&&=!1}},destroy:function(){this.canvasContext=null,this.data=null,this.globalData=null,this.maskManager.destroy()},mHelper:new Qe},fr.prototype.hide=fr.prototype.hideElement,fr.prototype.show=fr.prototype.showElement;function mr(e,t,n,r){this.styledShapes=[],this.tr=[0,0,0,0,0,0];var i=4;t.ty===`rc`?i=5:t.ty===`el`?i=6:t.ty===`sr`&&(i=7),this.sh=Ze.getShapeProp(e,t,i,e);var a,o=n.length,s;for(a=0;a=0;--a){if(d=this.searchProcessedElement(e[a]),d?t[a]=n[d-1]:e[a]._shouldRender=r,e[a].ty===`fl`||e[a].ty===`st`||e[a].ty===`gf`||e[a].ty===`gs`)d?t[a].style.closed=!1:t[a]=this.createStyleElement(e[a],m),l.push(t[a].style);else if(e[a].ty===`gr`){if(!d)t[a]=this.createGroupElement(e[a]);else for(c=t[a].it.length,s=0;s=0;--i)t[i].ty===`tr`?(o=n[i].transform,this.renderShapeTransform(e,o)):t[i].ty===`sh`||t[i].ty===`el`||t[i].ty===`rc`||t[i].ty===`sr`?this.renderPath(t[i],n[i]):t[i].ty===`fl`?this.renderFill(t[i],n[i],o):t[i].ty===`st`?this.renderStroke(t[i],n[i],o):t[i].ty===`gf`||t[i].ty===`gs`?this.renderGradientFill(t[i],n[i],o):t[i].ty===`gr`?this.renderShape(o,t[i].it,n[i].it):t[i].ty;r&&this.drawLayer()},hr.prototype.renderStyledShape=function(e,t){if(this._isFirstFrame||t._mdf||e.transforms._mdf){var n=e.trNodes,r=t.paths,i,a,o,s=r._length;n.length=0;var c=e.transforms.finalTransform;for(o=0;o=1?u=.99:u<=-1&&(u=-.99);var d=c*u,f=Math.cos(l+t.a.v)*d+o[0],p=Math.sin(l+t.a.v)*d+o[1];i=a.createRadialGradient(f,p,0,o[0],o[1],c)}var m,h=e.g.p,g=t.g.c,_=1;for(m=0;ma&&c===`xMidYMid slice`||ii&&s===`meet`||ai&&s===`slice`)?this.transformCanvas.tx=(n-this.transformCanvas.w*(r/this.transformCanvas.h))/2*this.renderConfig.dpr:l===`xMax`&&(ai&&s===`slice`)?this.transformCanvas.tx=(n-this.transformCanvas.w*(r/this.transformCanvas.h))*this.renderConfig.dpr:this.transformCanvas.tx=0,u===`YMid`&&(a>i&&s===`meet`||ai&&s===`meet`||a=0;--e)this.elements[e]&&this.elements[e].destroy&&this.elements[e].destroy();this.elements.length=0,this.globalData.canvasContext=null,this.animationItem.container=null,this.destroyed=!0},Q.prototype.renderFrame=function(e,t){if(!(this.renderedFrame===e&&this.renderConfig.clearCanvas===!0&&!t||this.destroyed||e===-1)){this.renderedFrame=e,this.globalData.frameNum=e-this.animationItem._isFirstFrame,this.globalData.frameId+=1,this.globalData._mdf=!this.renderConfig.clearCanvas||t,this.globalData.projectInterface.currentFrame=e;var n,r=this.layers.length;for(this.completeLayers||this.checkLayers(e),n=r-1;n>=0;--n)(this.completeLayers||this.elements[n])&&this.elements[n].prepareFrame(e-this.layers[n].st);if(this.globalData._mdf){for(this.renderConfig.clearCanvas===!0?this.canvasContext.clearRect(0,0,this.transformCanvas.w,this.transformCanvas.h):this.save(),n=r-1;n>=0;--n)(this.completeLayers||this.elements[n])&&this.elements[n].renderFrame();this.renderConfig.clearCanvas!==!0&&this.restore()}}},Q.prototype.buildItem=function(e){var t=this.elements;if(!(t[e]||this.layers[e].ty===99)){var n=this.createItem(this.layers[e],this,this.globalData);t[e]=n,n.initExpressions()}},Q.prototype.checkPendingElements=function(){for(;this.pendingElements.length;)this.pendingElements.pop().checkParenting()},Q.prototype.hide=function(){this.animationItem.container.style.display=`none`},Q.prototype.show=function(){this.animationItem.container.style.display=`block`};function yr(){this.opacity=-1,this.transform=p(`float32`,16),this.fillStyle=``,this.strokeStyle=``,this.lineWidth=``,this.lineCap=``,this.lineJoin=``,this.miterLimit=``,this.id=Math.random()}function br(){this.stack=[],this.cArrPos=0,this.cTr=new Qe;var e,t=15;for(e=0;e=0;--t)(this.completeLayers||this.elements[t])&&this.elements[t].renderFrame()},xr.prototype.destroy=function(){var e;for(e=this.layers.length-1;e>=0;--e)this.elements[e]&&this.elements[e].destroy();this.layers=null,this.elements=null},xr.prototype.createComp=function(e){return new xr(e,this.globalData,this)};function Sr(e,t){this.animationItem=e,this.renderConfig={clearCanvas:t&&t.clearCanvas!==void 0?t.clearCanvas:!0,context:t&&t.context||null,progressiveLoad:t&&t.progressiveLoad||!1,preserveAspectRatio:t&&t.preserveAspectRatio||`xMidYMid meet`,imagePreserveAspectRatio:t&&t.imagePreserveAspectRatio||`xMidYMid slice`,contentVisibility:t&&t.contentVisibility||`visible`,className:t&&t.className||``,id:t&&t.id||``,runExpressions:!t||t.runExpressions===void 0||t.runExpressions},this.renderConfig.dpr=t&&t.dpr||1,this.animationItem.wrapper&&(this.renderConfig.dpr=t&&t.dpr||window.devicePixelRatio||1),this.renderedFrame=-1,this.globalData={frameNum:-1,_mdf:!1,renderConfig:this.renderConfig,currentGlobalAlpha:-1},this.contextData=new br,this.elements=[],this.pendingElements=[],this.transformMat=new Qe,this.completeLayers=!1,this.rendererType=`canvas`,this.renderConfig.clearCanvas&&(this.ctxTransform=this.contextData.transform.bind(this.contextData),this.ctxOpacity=this.contextData.opacity.bind(this.contextData),this.ctxFillStyle=this.contextData.fillStyle.bind(this.contextData),this.ctxStrokeStyle=this.contextData.strokeStyle.bind(this.contextData),this.ctxLineWidth=this.contextData.lineWidth.bind(this.contextData),this.ctxLineCap=this.contextData.lineCap.bind(this.contextData),this.ctxLineJoin=this.contextData.lineJoin.bind(this.contextData),this.ctxMiterLimit=this.contextData.miterLimit.bind(this.contextData),this.ctxFill=this.contextData.fill.bind(this.contextData),this.ctxFillRect=this.contextData.fillRect.bind(this.contextData),this.ctxStroke=this.contextData.stroke.bind(this.contextData),this.save=this.contextData.save.bind(this.contextData))}return u([Q],Sr),Sr.prototype.createComp=function(e){return new xr(e,this.globalData,this)},be(`canvas`,Sr),vt.registerModifier(`tm`,bt),vt.registerModifier(`pb`,xt),vt.registerModifier(`rp`,Ct),vt.registerModifier(`rd`,wt),vt.registerModifier(`zz`,Ht),vt.registerModifier(`op`,en),et}))}))(),1);function Ji({documentID:e,className:t=``,showError:n=!0}){let r=(0,g.useRef)(null),i=(0,g.useRef)(null),[a,o]=(0,g.useState)(``),[s,c]=(0,g.useState)(null);return(0,g.useEffect)(()=>{let t=!1,n=null;return o(``),c(null),fetch(k.stickerDocumentAnimationURL(e),{credentials:`same-origin`}).then(async e=>{if(!e.ok){let t=await e.json().catch(()=>null);throw Error(t?.error||e.statusText)}if((e.headers.get(`content-type`)??``).includes(`json`)){let n=await e.json();if(t||!r.current)return;i.current?.destroy(),i.current=qi.default.loadAnimation({container:r.current,renderer:`canvas`,loop:!0,autoplay:!0,animationData:n});return}let a=await e.blob();t||(n=URL.createObjectURL(a),c(n))}).catch(e=>{t||o(O(e))}),()=>{t=!0,i.current?.destroy(),i.current=null,n&&URL.revokeObjectURL(n)}},[e]),(0,U.jsxs)(`div`,{className:`sticker-doc-cell ${t}`.trim(),children:[s?(0,U.jsx)(`img`,{className:`sticker-doc-image`,src:s,alt:``}):(0,U.jsx)(`div`,{className:`sticker-doc-canvas`,ref:r}),a&&n&&(0,U.jsx)(`span`,{className:`sticker-doc-error`,children:a})]})}function Yi({kind:e,onClose:t,onCreated:n}){let r=e===`emoji`?`emoji`:`sticker`,[i,a]=(0,g.useState)(``),[o,s]=(0,g.useState)(``),[c,l]=(0,g.useState)(``),[u,d]=(0,g.useState)(null),[f,p]=(0,g.useState)(``),[m,h]=(0,g.useState)(!1),[_,v]=(0,g.useState)(``);async function y(){if(!i.trim()||!o.trim()||!c.trim()||!u){v(`Title, short name, emoji and a first ${r} file are required.`);return}if(!f.trim()){v(`Please enter an operation reason`);return}h(!0),v(``);try{let r=new FormData;r.set(`metadata`,JSON.stringify({command_id:``,reason:f.trim(),confirm:!0,title:i.trim(),short_name:o.trim().toLowerCase(),kind:e,emoji:c.trim()})),r.set(`file`,u,u.name),await k.createStickerSet(r),n(),t()}catch(e){v(O(e))}finally{h(!1)}}return(0,On.createPortal)((0,U.jsx)(`div`,{className:`modal-backdrop`,role:`presentation`,children:(0,U.jsxs)(`section`,{className:`modal command-modal`,role:`dialog`,"aria-modal":`true`,"aria-label":`Create a new ${r} pack`,children:[(0,U.jsxs)(`div`,{className:`modal-head`,children:[(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`div`,{className:`eyebrow`,children:`New set`}),(0,U.jsx)(`h2`,{children:`Create a new ${r} pack`})]}),(0,U.jsx)(`button`,{className:`icon-btn`,type:`button`,onClick:t,disabled:m,"aria-label":`Close`,children:(0,U.jsx)(It,{size:15})})]}),(0,U.jsxs)(`div`,{className:`command-body`,children:[(0,U.jsxs)(`div`,{className:`gift-fields-grid`,children:[(0,U.jsxs)(`label`,{children:[(0,U.jsx)(`span`,{children:`Title`}),(0,U.jsx)(`input`,{value:i,maxLength:64,onChange:e=>a(e.target.value)})]}),(0,U.jsxs)(`label`,{children:[(0,U.jsx)(`span`,{children:`Short name`}),(0,U.jsx)(`input`,{value:o,maxLength:32,onChange:e=>s(e.target.value),placeholder:`lowercase_short_name`})]}),(0,U.jsxs)(`label`,{children:[(0,U.jsx)(`span`,{children:`Emoji`}),(0,U.jsx)(`input`,{value:c,onChange:e=>l(e.target.value),placeholder:`e.g. 😀`})]})]}),(0,U.jsxs)(`label`,{className:`gift-file-picker ${u?`has-file`:``}`,children:[(0,U.jsx)(`input`,{type:`file`,accept:`.tgs,.json,.webp,application/json,application/x-tgsticker,image/webp`,onChange:e=>d(e.target.files?.[0]??null)}),(0,U.jsxs)(`span`,{className:`gift-file-copy`,children:[(0,U.jsx)(`span`,{className:`gift-field-label`,children:`First ${r}`}),(0,U.jsx)(`strong`,{children:u?u.name:`Choose a TGS, Lottie JSON, or WebP file`})]}),(0,U.jsx)(`span`,{className:`gift-file-action`,children:u?`Change file`:`Choose file`})]}),(0,U.jsxs)(`label`,{className:`gift-reason-field`,children:[(0,U.jsx)(`span`,{children:`Audit reason`}),(0,U.jsx)(`input`,{value:f,placeholder:`Briefly describe why this gift is being imported`,onChange:e=>p(e.target.value)})]}),_&&(0,U.jsx)(q,{children:_})]}),(0,U.jsxs)(`div`,{className:`modal-actions`,children:[(0,U.jsx)(`button`,{className:`btn`,type:`button`,onClick:t,disabled:m,children:`Close`}),(0,U.jsxs)(`button`,{className:`btn primary`,type:`button`,onClick:y,disabled:m,children:[m?(0,U.jsx)(I,{className:`spin`,size:15}):(0,U.jsx)(At,{size:15}),`Create ${r} pack`]})]})]})}),document.body)}var Xi=24;function Zi({set:e,onClose:t}){let n=e.Kind===`emoji`?`emoji`:`sticker`,[r,i]=(0,g.useState)(null),[a,o]=(0,g.useState)(``),[s,c]=(0,g.useState)(1),l=(0,g.useCallback)(()=>{let t=!1;return o(``),k.stickerSetDocuments(e.ID).then(e=>{t||i(e.document_ids??[])}).catch(e=>{t||o(O(e))}),()=>{t=!0}},[e.ID]);(0,g.useEffect)(()=>(i(null),c(1),l()),[l]);let u=r?.length??0,d=Math.max(1,Math.ceil(u/Xi)),f=Math.min(s,d),p=(f-1)*Xi,m=r?.slice(p,p+Xi)??[],h=m.length===0?0:p+1,_=h===0?0:h+m.length-1;return(0,On.createPortal)((0,U.jsx)(`div`,{className:`modal-backdrop`,role:`presentation`,children:(0,U.jsxs)(`section`,{className:`modal command-modal sticker-preview-modal`,role:`dialog`,"aria-modal":`true`,"aria-label":e.Title||`#${e.ID}`,children:[(0,U.jsxs)(`div`,{className:`modal-head`,children:[(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`div`,{className:`eyebrow`,children:`Set contents`}),(0,U.jsx)(`h2`,{children:e.Title||`#${e.ID}`})]}),(0,U.jsx)(`button`,{className:`icon-btn`,type:`button`,onClick:t,"aria-label":`Close`,children:(0,U.jsx)(It,{size:15})})]}),(0,U.jsxs)(`div`,{className:`command-body`,children:[(0,U.jsx)(Qi,{setID:e.ID,noun:n,onAdded:l}),a&&(0,U.jsx)(q,{children:a}),!a&&r===null&&(0,U.jsxs)(`div`,{className:`loading-line`,children:[(0,U.jsx)(I,{className:`spin`,size:18}),` `,`Loading`]}),r!==null&&u===0&&!a&&(0,U.jsx)(`div`,{className:`empty-panel`,children:`This set has no documents.`}),m.length>0&&(0,U.jsx)(`div`,{className:`sticker-doc-grid`,children:m.map(t=>(0,U.jsxs)(`div`,{className:`sticker-doc-grid-cell`,children:[(0,U.jsx)(Ji,{documentID:t}),(0,U.jsx)(Z,{compact:!0,tone:`danger`,label:`Remove`,icon:(0,U.jsx)(Ot,{size:12}),path:`/api/actions/remove-sticker-from-set`,payload:()=>({set_id:e.ID,document_id:t}),onDone:l})]},t))},f),u>Xi&&(0,U.jsxs)(`div`,{className:`gift-pager`,children:[(0,U.jsx)(`span`,{className:`gift-pager-range`,children:`Showing ${h}-${_} of ${u}`}),(0,U.jsxs)(`div`,{className:`gift-pager-controls`,children:[(0,U.jsxs)(`button`,{className:`btn compact-btn`,type:`button`,onClick:()=>c(e=>Math.max(1,e-1)),disabled:f<=1,children:[(0,U.jsx)(R,{size:14}),` `,`Previous`]}),(0,U.jsx)(`span`,{className:`gift-pager-page`,children:`Page ${f} of ${d}`}),(0,U.jsxs)(`button`,{className:`btn compact-btn`,type:`button`,onClick:()=>c(e=>Math.min(d,e+1)),disabled:f>=d,children:[`Next`,` `,(0,U.jsx)(z,{size:14})]})]})]})]})]})}),document.body)}function Qi({setID:e,noun:t,onAdded:n}){let[r,i]=(0,g.useState)(null),[a,o]=(0,g.useState)(``),[s,c]=(0,g.useState)(``),[l,u]=(0,g.useState)(!1),[d,f]=(0,g.useState)(``);async function p(){if(!r){f(`Choose a ${t} file first`);return}if(!a.trim()){f(`An emoji is required.`);return}if(!s.trim()){f(`Please enter an operation reason`);return}u(!0),f(``);try{let t=new FormData;t.set(`metadata`,JSON.stringify({command_id:``,reason:s.trim(),confirm:!0,set_id:e,emoji:a.trim()})),t.set(`file`,r,r.name),await k.addStickerToSet(t),i(null),o(``),c(``),n()}catch(e){f(O(e))}finally{u(!1)}}return(0,U.jsxs)(`div`,{className:`sticker-add-form`,children:[(0,U.jsxs)(`label`,{className:`gift-file-picker compact ${r?`has-file`:``}`,children:[(0,U.jsx)(`input`,{type:`file`,accept:`.tgs,.json,.webp,application/json,application/x-tgsticker,image/webp`,onChange:e=>i(e.target.files?.[0]??null)}),(0,U.jsx)(`span`,{className:`gift-file-copy`,children:(0,U.jsx)(`strong`,{children:r?r.name:`Choose a TGS, Lottie JSON, or WebP file`})})]}),(0,U.jsx)(`input`,{className:`small-input`,value:a,onChange:e=>o(e.target.value),placeholder:`e.g. 😀`}),(0,U.jsx)(`input`,{className:`small-input`,value:s,onChange:e=>c(e.target.value),placeholder:`Describe why this operation is being performed`}),(0,U.jsxs)(`button`,{className:`btn primary compact-btn`,type:`button`,onClick:p,disabled:l,children:[l?(0,U.jsx)(I,{className:`spin`,size:14}):(0,U.jsx)(ct,{size:14}),` `,`Add ${t}`]}),d&&(0,U.jsx)(`span`,{className:`sticker-add-form-error`,children:d})]})}function $i({kind:e}){let[t,n]=(0,g.useState)([]),[r,i]=(0,g.useState)(``),[a,o]=(0,g.useState)(!1),[s,c]=(0,g.useState)(``),[l,u]=(0,g.useState)(10),[d,f]=(0,g.useState)(1),[p,m]=(0,g.useState)({}),[h,_]=(0,g.useState)({}),[v,y]=(0,g.useState)(null),[b,x]=(0,g.useState)(!1),S=e===`emoji`?`Emoji`:`Stickers`,C=e===`emoji`?`Custom-emoji packs — system packs aren't shown here, they're not hand-edited`:`Sticker packs — system packs (dice, animated emoji, gifts) aren't shown here, they're not hand-edited`,w=e===`emoji`?`emoji`:`sticker`;async function T(){o(!0),c(``);try{n((await k.stickerSets(e)).rows??[])}catch(e){c(O(e))}finally{o(!1)}}(0,g.useEffect)(()=>{T()},[e]);let E=(0,g.useMemo)(()=>{let e=r.trim().toLowerCase();return e?t.filter(t=>String(t.ID).includes(e)||t.ShortName.toLowerCase().includes(e)||t.Title.toLowerCase().includes(e)):t},[t,r]);(0,g.useEffect)(()=>{f(1)},[r,l,e]);let D=l===`all`?1:Math.max(1,Math.ceil(E.length/l)),A=Math.min(d,D),j=(0,g.useMemo)(()=>{if(l===`all`)return E;let e=(A-1)*l;return E.slice(e,e+l)},[E,A,l]),M=j.length===0?0:l===`all`?1:(A-1)*l+1,N=M===0?0:M+j.length-1,P=(0,g.useMemo)(()=>({total:t.length,official:t.filter(e=>e.Official).length,archived:t.filter(e=>e.Archived).length}),[t]);return(0,U.jsxs)(Gn,{title:S,eyebrow:C,actions:(0,U.jsxs)(U.Fragment,{children:[(0,U.jsxs)(`button`,{className:`btn`,type:`button`,onClick:()=>T(),disabled:a,children:[(0,U.jsx)(ft,{size:15}),` `,`Refresh`]}),(0,U.jsxs)(`button`,{className:`btn primary`,type:`button`,onClick:()=>x(!0),children:[(0,U.jsx)(ct,{size:15}),` `,`Create ${w} pack`]})]}),children:[s&&(0,U.jsx)(q,{children:s}),(0,U.jsxs)(`div`,{className:`metric-row`,children:[(0,U.jsx)(Y,{label:`Total sets`,value:String(P.total)}),(0,U.jsx)(Y,{label:`Official`,value:String(P.official),tone:`good`}),(0,U.jsx)(Y,{label:`Archived`,value:String(P.archived),tone:P.archived>0?`warn`:`neutral`})]}),(0,U.jsx)(Kn,{children:(0,U.jsxs)(`div`,{className:`toolbar`,children:[(0,U.jsxs)(`label`,{className:`searchbox`,children:[(0,U.jsx)(V,{size:15}),(0,U.jsx)(`input`,{value:r,onChange:e=>i(e.target.value),placeholder:`Search set ID, short name or title`})]}),(0,U.jsxs)(`label`,{className:`gift-page-size`,children:[(0,U.jsx)(`span`,{children:`Per page`}),(0,U.jsxs)(`select`,{value:String(l),onChange:e=>u(e.target.value===`all`?`all`:Number(e.target.value)),children:[(0,U.jsx)(`option`,{value:`10`,children:`10`}),(0,U.jsx)(`option`,{value:`20`,children:`20`}),(0,U.jsx)(`option`,{value:`50`,children:`50`}),(0,U.jsx)(`option`,{value:`100`,children:`100`}),(0,U.jsx)(`option`,{value:`all`,children:`All`})]})]}),(0,U.jsx)(`span`,{className:`gift-list-summary`,children:`Showing ${E.length} of ${t.length}`})]})}),(0,U.jsx)(`div`,{className:`table-wrap gift-table-wrap`,children:(0,U.jsxs)(`table`,{className:`data-table`,children:[(0,U.jsx)(`thead`,{children:(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`th`,{children:`Logo`}),(0,U.jsx)(`th`,{children:`ID`}),(0,U.jsx)(`th`,{children:`Short name`}),(0,U.jsx)(`th`,{children:`Title`}),(0,U.jsx)(`th`,{children:`Documents`}),(0,U.jsx)(`th`,{children:`Official`}),(0,U.jsx)(`th`,{children:`Status`}),(0,U.jsx)(`th`,{children:`Sort order`}),(0,U.jsx)(`th`,{children:`Actions`})]})}),(0,U.jsxs)(`tbody`,{children:[j.map(e=>(0,U.jsxs)(`tr`,{className:e.Archived?`gift-row-disabled`:``,children:[(0,U.jsx)(`td`,{children:e.CoverDocumentID?(0,U.jsx)(Ji,{documentID:e.CoverDocumentID,className:`list-thumb`,showError:!1}):(0,U.jsx)(`div`,{className:`sticker-list-thumb-empty`,children:(0,U.jsx)(Ue,{size:14})})}),(0,U.jsx)(`td`,{className:`mono`,children:e.ID}),(0,U.jsx)(`td`,{className:`mono`,children:e.ShortName||(0,U.jsx)(`span`,{className:`muted-cell`,children:`None`})}),(0,U.jsx)(`td`,{children:(0,U.jsxs)(`div`,{className:`sort-order-editor`,children:[(0,U.jsx)(`input`,{className:`small-input title-input`,value:h[e.ID]??e.Title,onChange:t=>_(n=>({...n,[e.ID]:t.target.value}))}),(0,U.jsx)(Z,{compact:!0,tone:`neutral`,label:`Save`,path:`/api/actions/rename-sticker-set`,payload:()=>({set_id:e.ID,title:(h[e.ID]??e.Title).trim()}),onDone:()=>void T()})]})}),(0,U.jsx)(`td`,{children:e.Count}),(0,U.jsx)(`td`,{children:e.Official?(0,U.jsx)(J,{tone:`good`,children:`Yes`}):(0,U.jsx)(J,{children:`No`})}),(0,U.jsx)(`td`,{children:e.Archived?(0,U.jsx)(J,{tone:`danger`,children:`Archived`}):(0,U.jsx)(J,{tone:`good`,children:`Enabled`})}),(0,U.jsx)(`td`,{children:(0,U.jsxs)(`div`,{className:`sort-order-editor`,children:[(0,U.jsx)(`input`,{type:`number`,className:`small-input`,value:p[e.ID]??String(e.SortOrder),onChange:t=>m(n=>({...n,[e.ID]:t.target.value}))}),(0,U.jsx)(Z,{compact:!0,tone:`neutral`,label:`Save`,path:`/api/actions/set-sticker-set-sort-order`,payload:()=>({set_id:e.ID,sort_order:Number(p[e.ID]??e.SortOrder)}),onDone:()=>void T()})]})}),(0,U.jsx)(`td`,{children:(0,U.jsxs)(`div`,{className:`gift-table-actions`,children:[(0,U.jsxs)(`button`,{className:`btn compact-btn`,type:`button`,onClick:()=>y(e),children:[(0,U.jsx)(Ne,{size:13}),` `,`View`]}),(0,U.jsx)(Z,{compact:!0,tone:`neutral`,label:e.Archived?`Unarchive`:`Archive`,path:`/api/actions/set-sticker-set-archived`,payload:()=>({set_id:e.ID,archived:!e.Archived}),onDone:()=>void T()}),(0,U.jsx)(Z,{compact:!0,tone:`danger`,label:`Delete`,path:`/api/actions/delete-sticker-set`,payload:()=>({set_id:e.ID}),onDone:()=>void T()})]})})]},e.ID)),j.length===0&&(0,U.jsx)(Yn,{colSpan:9})]})]})}),l!==`all`&&E.length>0&&(0,U.jsxs)(`div`,{className:`gift-pager`,children:[(0,U.jsx)(`span`,{className:`gift-pager-range`,children:`Showing ${M}-${N} of ${E.length}`}),(0,U.jsxs)(`div`,{className:`gift-pager-controls`,children:[(0,U.jsxs)(`button`,{className:`btn compact-btn`,type:`button`,onClick:()=>f(e=>Math.max(1,e-1)),disabled:A<=1,children:[(0,U.jsx)(R,{size:14}),` `,`Previous`]}),(0,U.jsx)(`span`,{className:`gift-pager-page`,children:`Page ${A} of ${D}`}),(0,U.jsxs)(`button`,{className:`btn compact-btn`,type:`button`,onClick:()=>f(e=>Math.min(D,e+1)),disabled:A>=D,children:[`Next`,` `,(0,U.jsx)(z,{size:14})]})]})]}),v&&(0,U.jsx)(Zi,{set:v,onClose:()=>y(null)}),b&&(0,U.jsx)(Yi,{kind:e,onClose:()=>x(!1),onCreated:()=>void T()})]})}var ea=[`Love`,`Approval`,`Disapproval`,`Cheers`,`Laughter`,`Astonishment`,`Sadness`,`Anger`,`Neutral`,`Doubt`,`Silly`];function ta({documentID:e}){let[t,n]=(0,g.useState)(!1);return t?(0,U.jsx)(`div`,{className:`sticker-list-thumb-empty`,children:(0,U.jsx)(Ue,{size:14})}):(0,U.jsx)(`video`,{className:`gif-catalog-thumb`,src:k.gifCatalogDocumentPreviewURL(e),muted:!0,loop:!0,autoPlay:!0,playsInline:!0,onError:()=>n(!0)})}function na(){let[e,t]=(0,g.useState)([]),[n,r]=(0,g.useState)(``),[i,a]=(0,g.useState)(!1),[o,s]=(0,g.useState)(``),[c,l]=(0,g.useState)(10),[u,d]=(0,g.useState)(1),[f,p]=(0,g.useState)({}),[m,h]=(0,g.useState)({}),[_,v]=(0,g.useState)(!1);async function y(){a(!0),s(``);try{t((await k.gifCatalog()).rows??[])}catch(e){s(O(e))}finally{a(!1)}}(0,g.useEffect)(()=>{y()},[]);let b=(0,g.useMemo)(()=>{let t=n.trim().toLowerCase();return t?e.filter(e=>e.ID.includes(t)||e.Title.toLowerCase().includes(t)):e},[e,n]);(0,g.useEffect)(()=>{d(1)},[n,c]);let x=c===`all`?1:Math.max(1,Math.ceil(b.length/c)),S=Math.min(u,x),C=(0,g.useMemo)(()=>{if(c===`all`)return b;let e=(S-1)*c;return b.slice(e,e+c)},[b,S,c]),w=C.length===0?0:c===`all`?1:(S-1)*c+1,T=w===0?0:w+C.length-1,E=(0,g.useMemo)(()=>({total:e.length,enabled:e.filter(e=>e.Enabled).length,uncategorized:e.filter(e=>!e.Category).length}),[e]);return(0,U.jsxs)(Gn,{title:`GIFs`,eyebrow:`Curated GIFs served by @gif in the client's GIF picker (trending + search)`,actions:(0,U.jsxs)(U.Fragment,{children:[(0,U.jsxs)(`button`,{className:`btn`,type:`button`,onClick:()=>y(),disabled:i,children:[(0,U.jsx)(ft,{size:15}),` `,`Refresh`]}),(0,U.jsx)(Z,{tone:`neutral`,label:`Auto-categorize`,path:`/api/actions/auto-categorize-gif-catalog`,payload:()=>({}),onDone:()=>void y()}),(0,U.jsx)(Z,{tone:`danger`,label:`Delete uncategorized`,path:`/api/actions/delete-uncategorized-gifs`,payload:()=>({}),onDone:()=>void y()}),(0,U.jsxs)(`button`,{className:`btn primary`,type:`button`,onClick:()=>v(!0),children:[(0,U.jsx)(ct,{size:15}),` `,`Add GIF`]})]}),children:[o&&(0,U.jsx)(q,{children:o}),(0,U.jsxs)(`div`,{className:`metric-row`,children:[(0,U.jsx)(Y,{label:`Total GIFs`,value:String(E.total)}),(0,U.jsx)(Y,{label:`Enabled`,value:String(E.enabled),tone:`good`}),(0,U.jsx)(Y,{label:`Uncategorized`,value:String(E.uncategorized),tone:E.uncategorized>0?`warn`:void 0})]}),(0,U.jsx)(Kn,{children:(0,U.jsxs)(`div`,{className:`toolbar`,children:[(0,U.jsxs)(`label`,{className:`searchbox`,children:[(0,U.jsx)(V,{size:15}),(0,U.jsx)(`input`,{value:n,onChange:e=>r(e.target.value),placeholder:`Search ID or title`})]}),(0,U.jsxs)(`label`,{className:`gift-page-size`,children:[(0,U.jsx)(`span`,{children:`Per page`}),(0,U.jsxs)(`select`,{value:String(c),onChange:e=>l(e.target.value===`all`?`all`:Number(e.target.value)),children:[(0,U.jsx)(`option`,{value:`10`,children:`10`}),(0,U.jsx)(`option`,{value:`20`,children:`20`}),(0,U.jsx)(`option`,{value:`50`,children:`50`}),(0,U.jsx)(`option`,{value:`100`,children:`100`}),(0,U.jsx)(`option`,{value:`all`,children:`All`})]})]}),(0,U.jsx)(`span`,{className:`gift-list-summary`,children:`Showing ${b.length} of ${e.length}`})]})}),(0,U.jsx)(`div`,{className:`table-wrap gift-table-wrap`,children:(0,U.jsxs)(`table`,{className:`data-table`,children:[(0,U.jsx)(`thead`,{children:(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`th`,{children:`Preview`}),(0,U.jsx)(`th`,{children:`ID`}),(0,U.jsx)(`th`,{children:`Title`}),(0,U.jsx)(`th`,{children:`Document ID`}),(0,U.jsx)(`th`,{children:`Added by`}),(0,U.jsx)(`th`,{children:`Status`}),(0,U.jsx)(`th`,{children:`Category`}),(0,U.jsx)(`th`,{children:`Sort order`}),(0,U.jsx)(`th`,{children:`Actions`})]})}),(0,U.jsxs)(`tbody`,{children:[C.map(e=>(0,U.jsxs)(`tr`,{className:e.Enabled?``:`gift-row-disabled`,children:[(0,U.jsx)(`td`,{children:(0,U.jsx)(ta,{documentID:e.DocumentID})}),(0,U.jsx)(`td`,{className:`mono`,children:e.ID}),(0,U.jsx)(`td`,{children:e.Title||(0,U.jsx)(`span`,{className:`muted-cell`,children:`Untitled`})}),(0,U.jsx)(`td`,{className:`mono`,children:e.DocumentID}),(0,U.jsx)(`td`,{children:e.CreatedBy||(0,U.jsx)(`span`,{className:`muted-cell`,children:`—`})}),(0,U.jsx)(`td`,{children:e.Enabled?(0,U.jsx)(J,{tone:`good`,children:`Enabled`}):(0,U.jsx)(J,{tone:`danger`,children:`Disabled`})}),(0,U.jsx)(`td`,{children:(0,U.jsxs)(`div`,{className:`sort-order-editor`,children:[(0,U.jsxs)(`select`,{className:`small-input`,value:m[e.ID]??e.Category,onChange:t=>h(n=>({...n,[e.ID]:t.target.value})),children:[(0,U.jsx)(`option`,{value:``,children:`Uncategorized`}),ea.map(e=>(0,U.jsx)(`option`,{value:e,children:e},e))]}),(0,U.jsx)(Z,{compact:!0,tone:`neutral`,label:`Save`,path:`/api/actions/set-gif-catalog-category`,payload:()=>({id:e.ID,category:m[e.ID]??e.Category}),onDone:()=>void y()})]})}),(0,U.jsx)(`td`,{children:(0,U.jsxs)(`div`,{className:`sort-order-editor`,children:[(0,U.jsx)(`input`,{type:`number`,className:`small-input`,value:f[e.ID]??String(e.SortOrder),onChange:t=>p(n=>({...n,[e.ID]:t.target.value}))}),(0,U.jsx)(Z,{compact:!0,tone:`neutral`,label:`Save`,path:`/api/actions/set-gif-catalog-sort-order`,payload:()=>({id:e.ID,sort_order:Number(f[e.ID]??e.SortOrder)}),onDone:()=>void y()})]})}),(0,U.jsx)(`td`,{children:(0,U.jsxs)(`div`,{className:`gift-table-actions`,children:[(0,U.jsx)(Z,{compact:!0,tone:`neutral`,label:e.Enabled?`Disable`:`Enable`,path:`/api/actions/set-gif-catalog-enabled`,payload:()=>({id:e.ID,enabled:!e.Enabled}),onDone:()=>void y()}),(0,U.jsx)(Z,{compact:!0,tone:`danger`,label:`Delete`,path:`/api/actions/delete-gif-catalog-entry`,payload:()=>({id:e.ID}),onDone:()=>void y()})]})})]},e.ID)),C.length===0&&(0,U.jsx)(Yn,{colSpan:9})]})]})}),c!==`all`&&b.length>0&&(0,U.jsxs)(`div`,{className:`gift-pager`,children:[(0,U.jsx)(`span`,{className:`gift-pager-range`,children:`Showing ${w}-${T} of ${b.length}`}),(0,U.jsxs)(`div`,{className:`gift-pager-controls`,children:[(0,U.jsxs)(`button`,{className:`btn compact-btn`,type:`button`,onClick:()=>d(e=>Math.max(1,e-1)),disabled:S<=1,children:[(0,U.jsx)(R,{size:14}),` `,`Previous`]}),(0,U.jsx)(`span`,{className:`gift-pager-page`,children:`Page ${S} of ${x}`}),(0,U.jsxs)(`button`,{className:`btn compact-btn`,type:`button`,onClick:()=>d(e=>Math.min(x,e+1)),disabled:S>=x,children:[`Next`,` `,(0,U.jsx)(z,{size:14})]})]})]}),_&&(0,U.jsx)(ra,{onClose:()=>v(!1),onCreated:()=>void y()})]})}function ra({onClose:e,onCreated:t}){let[n,r]=(0,g.useState)(``),[i,a]=(0,g.useState)(null),[o,s]=(0,g.useState)(null),[c,l]=(0,g.useState)(``),[u,d]=(0,g.useState)(!1),[f,p]=(0,g.useState)(``);function m(e){a(e),s(t=>(t&&URL.revokeObjectURL(t),e?URL.createObjectURL(e):null))}async function h(){if(!n.trim()||!i){p(`Title and a GIF/MP4 file are required.`);return}if(!c.trim()){p(`Please enter an operation reason`);return}d(!0),p(``);try{let r=new FormData;r.set(`metadata`,JSON.stringify({command_id:``,reason:c.trim(),confirm:!0,title:n.trim()})),r.set(`file`,i,i.name),await k.createGifCatalogEntry(r),t(),e()}catch(e){p(O(e))}finally{d(!1)}}return(0,On.createPortal)((0,U.jsx)(`div`,{className:`modal-backdrop`,role:`presentation`,children:(0,U.jsxs)(`section`,{className:`modal command-modal`,role:`dialog`,"aria-modal":`true`,"aria-label":`Add a GIF`,children:[(0,U.jsxs)(`div`,{className:`modal-head`,children:[(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`div`,{className:`eyebrow`,children:`New catalog entry`}),(0,U.jsx)(`h2`,{children:`Add a GIF`})]}),(0,U.jsx)(`button`,{className:`icon-btn`,type:`button`,onClick:e,disabled:u,"aria-label":`Close`,children:(0,U.jsx)(It,{size:15})})]}),(0,U.jsxs)(`div`,{className:`command-body`,children:[(0,U.jsx)(`div`,{className:`gift-fields-grid`,children:(0,U.jsxs)(`label`,{children:[(0,U.jsx)(`span`,{children:`Title`}),(0,U.jsx)(`input`,{value:n,maxLength:128,onChange:e=>r(e.target.value)})]})}),(0,U.jsxs)(`label`,{className:`gift-file-picker ${i?`has-file`:``}`,children:[(0,U.jsx)(`input`,{type:`file`,accept:`.gif,.mp4,image/gif,video/mp4`,onChange:e=>m(e.target.files?.[0]??null)}),(0,U.jsxs)(`span`,{className:`gift-file-copy`,children:[(0,U.jsx)(`span`,{className:`gift-field-label`,children:`File`}),(0,U.jsx)(`strong`,{children:i?i.name:`Choose a GIF or MP4 file`})]}),(0,U.jsx)(`span`,{className:`gift-file-action`,children:i?`Change file`:`Choose file`})]}),o&&(0,U.jsx)(`div`,{className:`gif-catalog-preview`,children:i?.type===`video/mp4`?(0,U.jsx)(`video`,{src:o,autoPlay:!0,loop:!0,muted:!0,playsInline:!0}):(0,U.jsx)(`img`,{src:o,alt:``})}),(0,U.jsxs)(`label`,{className:`gift-reason-field`,children:[(0,U.jsx)(`span`,{children:`Audit reason`}),(0,U.jsx)(`input`,{value:c,placeholder:`Briefly describe why this GIF is being added`,onChange:e=>l(e.target.value)})]}),f&&(0,U.jsx)(q,{children:f})]}),(0,U.jsxs)(`div`,{className:`modal-actions`,children:[(0,U.jsx)(`button`,{className:`btn`,type:`button`,onClick:e,disabled:u,children:`Close`}),(0,U.jsxs)(`button`,{className:`btn primary`,type:`button`,onClick:h,disabled:u,children:[u?(0,U.jsx)(I,{className:`spin`,size:15}):(0,U.jsx)(At,{size:15}),`Add GIF`]})]})]})}),document.body)}function ia(){let[e,t]=(0,g.useState)([]),[n,r]=(0,g.useState)(null),[i,a]=(0,g.useState)([]),[o,s]=(0,g.useState)(!1),[c,l]=(0,g.useState)(!1),[u,d]=(0,g.useState)(``),[f,p]=(0,g.useState)(null),[m,h]=(0,g.useState)(null),[_,v]=(0,g.useState)(!1);async function y(){s(!0),d(``);try{let e=await k.adminUsers();t(e.rows??[]),r(e.system??null),a(e.available_permissions??[])}catch(e){d(O(e))}finally{s(!1),l(!0)}}return(0,g.useEffect)(()=>{y()},[]),(0,U.jsxs)(Gn,{eyebrow:`ACCESS / OPERATORS`,title:`Admin operators`,children:[u&&(0,U.jsx)(q,{children:u}),(0,U.jsx)(Kn,{children:(0,U.jsxs)(`div`,{className:`toolbar`,children:[(0,U.jsxs)(`button`,{className:`btn primary icon-text`,type:`button`,onClick:()=>v(!0),children:[(0,U.jsx)(Mt,{size:15}),` `,`New operator`]}),(0,U.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>void y(),disabled:o,children:[(0,U.jsx)(ft,{size:15,className:o?`spin`:``}),` `,`Refresh`]})]})}),(0,U.jsx)(K,{title:`Operators`}),(0,U.jsx)(`div`,{className:`table-wrap`,children:(0,U.jsxs)(`table`,{className:`data-table`,children:[(0,U.jsx)(`thead`,{children:(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`th`,{children:`Username`}),(0,U.jsx)(`th`,{children:`Can do`}),(0,U.jsx)(`th`,{children:`Status`}),(0,U.jsx)(`th`,{children:`Last login`}),(0,U.jsx)(`th`,{})]})}),(0,U.jsxs)(`tbody`,{children:[n&&(0,U.jsxs)(`tr`,{children:[(0,U.jsxs)(`td`,{className:`mono`,children:[n.username,` `,(0,U.jsx)(`span`,{className:`pill`,children:`built-in`})]}),(0,U.jsx)(`td`,{children:(0,U.jsx)(aa,{permissions:n.permissions})}),(0,U.jsx)(`td`,{children:(0,U.jsx)(`span`,{className:`pill good`,children:`Enabled`})}),(0,U.jsx)(`td`,{className:`mono`,children:`—`}),(0,U.jsx)(`td`,{children:(0,U.jsxs)(`span`,{className:`muted icon-text`,children:[(0,U.jsx)(Xe,{size:13}),` `,`Set in the server environment`]})})]}),e.map(e=>(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`td`,{className:`mono`,children:e.username}),(0,U.jsx)(`td`,{children:(0,U.jsx)(aa,{permissions:e.permissions})}),(0,U.jsx)(`td`,{children:e.enabled?(0,U.jsx)(`span`,{className:`pill good`,children:`Enabled`}):(0,U.jsx)(`span`,{className:`pill`,children:`Disabled`})}),(0,U.jsx)(`td`,{className:`mono`,children:e.last_login_at?new Date(e.last_login_at).toLocaleString():`—`}),(0,U.jsxs)(`td`,{children:[(0,U.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>p(e),children:[(0,U.jsx)(xt,{size:14}),` `,`Access`]}),(0,U.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>h(e),children:[(0,U.jsx)(Ge,{size:14}),` `,`Password`]})]})]},e.id)),e.length===0&&!n&&(o||!c?(0,U.jsx)(Xn,{colSpan:5}):(0,U.jsx)(Yn,{colSpan:5}))]})]})}),_&&(0,U.jsx)(sa,{title:`New operator`,available:i,onClose:()=>v(!1),onDone:()=>{v(!1),y()}}),f&&(0,U.jsx)(sa,{title:`Access for ${f.username}`,available:i,existing:f,onClose:()=>p(null),onDone:()=>{p(null),y()}}),m&&(0,U.jsx)(ca,{operator:m,onClose:()=>h(null),onDone:()=>{h(null),y()}})]})}function aa({permissions:e}){return e.length===0?(0,U.jsx)(`span`,{className:`muted`,children:`nothing yet`}):(0,U.jsx)(`span`,{className:`chip-row`,children:e.map(e=>(0,U.jsx)(`span`,{className:`chip`,title:e,children:hn(e)},e))})}function oa({available:e,selected:t,onToggle:n,onToggleGroup:r,onToggleAll:i}){let a=t.includes(`*`);return(0,U.jsxs)(`div`,{className:`permission-groups`,children:[(0,U.jsx)(`section`,{className:`permission-group`,children:(0,U.jsx)(`div`,{className:`permission-grid`,children:(0,U.jsxs)(`label`,{className:`permission-item`,title:`*`,children:[(0,U.jsx)(`input`,{type:`checkbox`,checked:a,onChange:e=>i(e.target.checked)}),(0,U.jsxs)(`span`,{className:`permission-copy`,children:[(0,U.jsx)(`strong`,{children:`Full access`}),(0,U.jsx)(`small`,{children:`Every right below, including ones added in future updates. Turn off to pick rights individually.`})]})]})})}),vn(e).map(e=>{let i=e.permissions.every(e=>t.includes(e));return(0,U.jsxs)(`section`,{className:`permission-group`,children:[(0,U.jsxs)(`div`,{className:`permission-group-head`,children:[(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`strong`,{children:e.title}),(0,U.jsx)(`small`,{children:e.hint})]}),(0,U.jsx)(`button`,{className:`btn compact`,type:`button`,disabled:a,onClick:()=>r(e.permissions,!i),children:i?`Clear`:`Select all`})]}),(0,U.jsx)(`div`,{className:`permission-grid`,children:e.permissions.map(e=>(0,U.jsxs)(`label`,{className:`permission-item`,title:e,children:[(0,U.jsx)(`input`,{type:`checkbox`,checked:a||t.includes(e),disabled:a,onChange:t=>n(e,t.target.checked)}),(0,U.jsxs)(`span`,{className:`permission-copy`,children:[(0,U.jsx)(`strong`,{children:hn(e)}),(0,U.jsx)(`small`,{children:gn(e)})]})]},e))})]},e.title)})]})}function sa({title:e,available:t,existing:n,onClose:r,onDone:i}){let[a,o]=(0,g.useState)(n?.username??``),[s,c]=(0,g.useState)(``),[l,u]=(0,g.useState)(n?.permissions??[]),[d,f]=(0,g.useState)(n?.enabled??!0),p=!!n,m=p?!1:a.trim().length<3||s.trim()===``;return(0,On.createPortal)((0,U.jsx)(`div`,{className:`modal-backdrop`,role:`presentation`,children:(0,U.jsxs)(`section`,{className:`modal command-modal`,role:`dialog`,"aria-modal":`true`,"aria-label":e,children:[(0,U.jsxs)(`div`,{className:`modal-head`,children:[(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`div`,{className:`eyebrow`,children:`Operators`}),(0,U.jsx)(`h2`,{children:e})]}),(0,U.jsx)(`button`,{className:`icon-btn`,type:`button`,onClick:r,"aria-label":`Close`,children:(0,U.jsx)(It,{size:15})})]}),(0,U.jsxs)(`div`,{className:`command-body`,children:[!p&&(0,U.jsxs)(`div`,{className:`operator-identity`,children:[(0,U.jsxs)(`label`,{className:`duration-field`,children:[(0,U.jsx)(`span`,{children:`Username`}),(0,U.jsx)(`input`,{autoFocus:!0,value:a,spellCheck:!1,autoCapitalize:`none`,placeholder:`letters, digits, dot, dash or underscore`,onChange:e=>o(e.target.value)})]}),(0,U.jsxs)(`label`,{className:`duration-field`,children:[(0,U.jsx)(`span`,{children:`Password`}),(0,U.jsx)(`input`,{type:`password`,value:s,autoComplete:`new-password`,onChange:e=>c(e.target.value)})]})]}),(0,U.jsx)(oa,{available:t,selected:l,onToggle:(e,t)=>u(n=>t?[...n,e]:n.filter(t=>t!==e)),onToggleGroup:(e,t)=>u(n=>t?[...n,...e.filter(e=>!n.includes(e))]:n.filter(t=>!e.includes(t))),onToggleAll:e=>u(t=>e?[`*`]:t.filter(e=>e!==`*`))}),(0,U.jsxs)(`label`,{className:`permission-item standalone`,children:[(0,U.jsx)(`input`,{type:`checkbox`,checked:d,onChange:e=>f(e.target.checked)}),(0,U.jsxs)(`span`,{className:`permission-copy`,children:[(0,U.jsx)(`strong`,{children:`Account is enabled`}),(0,U.jsx)(`small`,{children:`A disabled operator cannot sign in`})]})]}),p&&(0,U.jsx)(q,{children:`The new access applies from this operator's next request. They stay signed in.`})]}),(0,U.jsxs)(`div`,{className:`modal-actions toolbar`,children:[(0,U.jsx)(`button`,{className:`btn`,type:`button`,onClick:r,children:`Cancel`}),(0,U.jsx)(Z,{label:p?`Save access`:`Create operator`,path:p?`/api/actions/set-admin-operator-access`:`/api/actions/create-admin-operator`,tone:`primary`,disabled:m,icon:p?(0,U.jsx)(xt,{size:15}):(0,U.jsx)(Mt,{size:15}),payload:()=>p?{id:n?.id,permissions:l,enabled:d}:{username:a.trim(),password:s,permissions:l,enabled:d},onDone:i})]})]})}),document.body)}function ca({operator:e,onClose:t,onDone:n}){let[r,i]=(0,g.useState)(``);return(0,On.createPortal)((0,U.jsx)(`div`,{className:`modal-backdrop`,role:`presentation`,children:(0,U.jsxs)(`section`,{className:`modal command-modal narrow`,role:`dialog`,"aria-modal":`true`,"aria-label":`Set password`,children:[(0,U.jsxs)(`div`,{className:`modal-head`,children:[(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`div`,{className:`eyebrow`,children:`Operators`}),(0,U.jsx)(`h2`,{children:`Password for ${e.username}`})]}),(0,U.jsx)(`button`,{className:`icon-btn`,type:`button`,onClick:t,"aria-label":`Close`,children:(0,U.jsx)(It,{size:15})})]}),(0,U.jsxs)(`div`,{className:`command-body`,children:[(0,U.jsxs)(`label`,{className:`duration-field`,children:[(0,U.jsx)(`span`,{children:`New password`}),(0,U.jsx)(`input`,{autoFocus:!0,type:`password`,value:r,autoComplete:`new-password`,onChange:e=>i(e.target.value)})]}),(0,U.jsx)(q,{children:`Changing the password signs this operator out of any session they already have.`})]}),(0,U.jsxs)(`div`,{className:`modal-actions toolbar`,children:[(0,U.jsx)(`button`,{className:`btn`,type:`button`,onClick:t,children:`Cancel`}),(0,U.jsx)(Z,{label:`Set password`,path:`/api/actions/set-admin-operator-password`,tone:`primary`,disabled:r.trim()===``,icon:(0,U.jsx)(Ge,{size:15}),payload:()=>({id:e.id,password:r}),onDone:n})]})]})}),document.body)}var la=`open,in_review,action_pending,action_failed,appeal_review`,ua=[{value:la,label:`Active queue`},{value:`open,in_review,action_pending,action_failed,resolved,dismissed,appeal_review`,label:`All statuses`},{value:`open`,label:`Open`},{value:`in_review`,label:`In review`},{value:`action_pending`,label:`Action pending`},{value:`action_failed`,label:`Action failed`},{value:`appeal_review`,label:`Appeal review`},{value:`resolved`,label:`Resolved`},{value:`dismissed`,label:`Dismissed`}];function da({navigate:e}){let[t,n]=(0,g.useState)(la),[r,i]=(0,g.useState)(``),[a,o]=(0,g.useState)([]),[s,c]=(0,g.useState)(!1),[l,u]=(0,g.useState)(``);async function d(){c(!0),u(``);try{let e=new URLSearchParams({statuses:t,limit:`100`});r.trim()&&e.set(`assigned_to`,r.trim()),o((await k.moderationCases(e)).cases)}catch(e){u(O(e))}finally{c(!1)}}(0,g.useEffect)(()=>{d()},[]);let f=a.filter(e=>e.Status===`action_pending`||e.Status===`action_failed`).length,p=a.filter(e=>e.Severity===4).length;return(0,U.jsxs)(Gn,{title:`Reports and Moderation`,eyebrow:`Moderation / Cases`,actions:(0,U.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:d,disabled:s,children:[(0,U.jsx)(ft,{size:15,className:s?`spin`:``}),` `,`Refresh`]}),children:[l&&(0,U.jsx)(q,{children:l}),(0,U.jsxs)(`div`,{className:`metric-row`,children:[(0,U.jsx)(Y,{label:`Current queue`,value:String(a.length)}),(0,U.jsx)(Y,{label:`Critical cases`,value:String(p),tone:p?`danger`:`neutral`}),(0,U.jsx)(Y,{label:`Pending / failed actions`,value:String(f),tone:f?`warn`:`good`})]}),(0,U.jsx)(Kn,{children:(0,U.jsxs)(`form`,{className:`toolbar`,onSubmit:e=>{e.preventDefault(),d()},children:[(0,U.jsxs)(`label`,{className:`field-inline`,children:[(0,U.jsx)(`span`,{children:`Status`}),(0,U.jsx)(`select`,{"aria-label":`Case status filter`,value:t,onChange:e=>n(e.target.value),children:ua.map(e=>(0,U.jsx)(`option`,{value:e.value,children:e.label},e.value))})]}),(0,U.jsxs)(`label`,{className:`field-inline`,children:[(0,U.jsx)(`span`,{children:`Reviewer`}),(0,U.jsx)(`input`,{value:r,onChange:e=>i(e.target.value),placeholder:`Leave blank for all`})]}),(0,U.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,disabled:s,children:[(0,U.jsx)(bt,{size:15}),` `,`Search`]})]})}),(0,U.jsx)(`div`,{className:`table-wrap`,children:(0,U.jsxs)(`table`,{className:`data-table`,children:[(0,U.jsx)(`thead`,{children:(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`th`,{children:`Case`}),(0,U.jsx)(`th`,{children:`Target`}),(0,U.jsx)(`th`,{children:`Status`}),(0,U.jsx)(`th`,{children:`Severity`}),(0,U.jsx)(`th`,{children:`Reports / Reporters`}),(0,U.jsx)(`th`,{children:`Reviewer`}),(0,U.jsx)(`th`,{children:`Latest report`}),(0,U.jsx)(`th`,{})]})}),(0,U.jsxs)(`tbody`,{children:[a.map(t=>(0,U.jsxs)(`tr`,{children:[(0,U.jsxs)(`td`,{className:`mono`,children:[`#`,t.ID]}),(0,U.jsx)(`td`,{className:`mono`,children:_a(t.Target.Type,t.Target.ID)}),(0,U.jsx)(`td`,{children:(0,U.jsx)(fa,{status:t.Status})}),(0,U.jsx)(`td`,{children:(0,U.jsx)(ma,{value:t.Severity})}),(0,U.jsxs)(`td`,{children:[t.ReportCount,` / `,t.DistinctReporterCount]}),(0,U.jsx)(`td`,{children:t.AssignedTo||`-`}),(0,U.jsx)(`td`,{children:G(t.LastReportAt)}),(0,U.jsx)(`td`,{children:(0,U.jsxs)(`button`,{className:`row-link`,onClick:()=>e(`/moderation/${t.ID}`),children:[`Review`,` `,(0,U.jsx)(z,{size:14})]})})]},t.ID)),a.length===0&&(0,U.jsx)(Yn,{colSpan:8})]})]})})]})}function fa({status:e}){return(0,U.jsx)(J,{tone:e===`resolved`||e===`dismissed`?`good`:e===`action_failed`?`danger`:e===`action_pending`?`warn`:`neutral`,children:ga(`status`,e)})}var pa={low:`Low`,medium:`Medium`,high:`High`,critical:`Critical`};function ma({value:e}){let t=[``,`low`,`medium`,`high`,`critical`][e];return(0,U.jsx)(J,{tone:e>=4?`danger`:e>=3?`warn`:`neutral`,children:t?pa[t]:e})}var ha={status:{open:`Open`,in_review:`In review`,action_pending:`Action pending`,action_failed:`Action failed`,appeal_review:`Appeal review`,resolved:`Resolved`,dismissed:`Dismissed`},targetType:{channel:`Channel`,chat:`Group`,user:`Account`},source:{account_peer:`Account / peer`,antispam_false_positive:`Anti-spam false positive`,channel_spam:`Channel spam`,encrypted_spam:`Encrypted-chat spam`,ephemeral:`Ephemeral media`,messages:`Messages`,messages_spam:`Message spam`,profile_photo:`Profile photo`,reaction:`Reaction`,sponsored:`Sponsored message`,story:`Story`},reason:{child_abuse:`Child abuse`,copyright:`Copyright`,fake:`Fake`,geo_irrelevant:`Location-irrelevant`,illegal_drugs:`Illegal drugs`,other:`Other`,personal_details:`Personal details`,pornography:`Pornography`,spam:`Spam`,violence:`Violence`}};function ga(e,t){return ha[e]?.[t]??t}function _a(e,t){return`${ga(`targetType`,e)} #${t}`}function va({id:e,navigate:t}){let[n,r]=(0,g.useState)(null),[i,a]=(0,g.useState)(null),[o,s]=(0,g.useState)(``),[c,l]=(0,g.useState)(`no_violation`),[u,d]=(0,g.useState)(``),[f,p]=(0,g.useState)(``),[m,h]=(0,g.useState)(!0),[_,v]=(0,g.useState)(!1),[y,b]=(0,g.useState)(``);function x(e){a(e),e&&(d(e.Items.filter(e=>e.Kind===`message`).map(e=>Number(e.ItemID)).filter(e=>Number.isSafeInteger(e)&&e>0).join(`, `)),p(String(e.ReporterUserID)))}async function S(){b(``);try{let t=await k.moderationCase(e);r(t);let n=t.ReportIDs[0];x(n?await k.moderationReport(n):null)}catch(e){b(O(e))}}(0,g.useEffect)(()=>{S()},[e]);let C=(0,g.useMemo)(()=>ya(c,n?.Case.Target.Type,ba(u),Number(f),m),[c,n?.Case.Target.Type,u,f,m]),w=(0,g.useMemo)(()=>n?xa(n):{actions:[],label:`None`,blocked:!1},[n]);async function T(){if(n){v(!0),b(``);try{await k.claimModerationCase(e,n.Case.Version),await S()}catch(e){b(O(e))}finally{v(!1)}}}async function E(){if(!n||!o.trim()){b(`A review reason is required.`);return}if(c===`delete_messages`&&C.length===0){b(n.Case.Target.Type===`user`?`Private-message deletion requires valid evidence message IDs and the reporter's owner_user_id.`:`Channel-message deletion requires at least one valid evidence message ID.`);return}if(window.confirm(`Submit the “${Sa(c)}” decision? The action will run through the durable action queue.`)){v(!0),b(``);try{r((await k.decideModerationCase(e,{expected_version:n.Case.Version,reason:o.trim(),kind:c===`no_violation`?`no_violation`:`violation`,actions:C})).case),s(``)}catch(e){b(O(e))}finally{v(!1)}}}async function D(t,i){if(!n||!o.trim()){b(`An appeal review reason is required.`);return}if(window.confirm(i?`Grant this appeal?`:`Deny this appeal?`)){v(!0);try{r((await k.reviewModerationAppeal(e,t,{expected_version:n.Case.Version,reason:o.trim(),granted:i,actions:i?w.actions:[]})).case),s(``)}catch(e){b(O(e))}finally{v(!1)}}}if(y&&!n)return(0,U.jsx)(q,{children:y});if(!n)return(0,U.jsx)(Zn,{label:`Loading moderation case…`});let A=n.Case,j=A.Status===`open`||A.Status===`in_review`||A.Status===`appeal_review`,M=(A.Status===`in_review`||A.Status===`action_failed`)&&!!A.AssignedTo,N=M&&(A.Status!==`action_failed`||c!==`no_violation`),P=n.Appeals.find(e=>e.Status===`pending`);return(0,U.jsxs)(Gn,{title:`Review case #${A.ID}`,eyebrow:`Moderation / Case detail`,actions:(0,U.jsxs)(U.Fragment,{children:[(0,U.jsxs)(`button`,{className:`btn icon-text`,onClick:()=>t(`/moderation`),children:[(0,U.jsx)(pe,{size:15}),` `,`Back to queue`]}),(0,U.jsxs)(`button`,{className:`btn icon-text`,onClick:S,children:[(0,U.jsx)(ft,{size:15}),` `,`Refresh`]})]}),children:[y&&(0,U.jsx)(q,{children:y}),(0,U.jsx)(qn,{main:(0,U.jsxs)(`div`,{className:`stacked-sections`,children:[(0,U.jsxs)(`section`,{className:`entity-head`,children:[(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`div`,{className:`entity-title`,children:_a(A.Target.Type,A.Target.ID)}),(0,U.jsx)(`div`,{className:`entity-subtitle`,children:`Version ${A.Version} · Updated ${G(A.UpdatedAt)}`})]}),(0,U.jsxs)(`div`,{className:`entity-badges`,children:[(0,U.jsx)(fa,{status:A.Status}),(0,U.jsx)(ma,{value:A.Severity})]})]}),(0,U.jsxs)(`div`,{className:`summary-grid`,children:[(0,U.jsx)(X,{label:`Target`,value:_a(A.Target.Type,A.Target.ID),mono:!0}),(0,U.jsx)(X,{label:`Reports`,value:`${A.ReportCount} reports from ${A.DistinctReporterCount} reporters`}),(0,U.jsx)(X,{label:`Reviewer`,value:A.AssignedTo||`-`}),(0,U.jsx)(X,{label:`First / latest report`,value:`${G(A.FirstReportAt)} / ${G(A.LastReportAt)}`})]}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(K,{title:`Report evidence`,text:`Shows up to the latest 100 reports; snapshots are frozen when reports are admitted.`}),(0,U.jsx)(`div`,{className:`toolbar`,children:n.ReportIDs.map(e=>(0,U.jsxs)(`button`,{className:`btn`,onClick:async()=>x(await k.moderationReport(e)),children:[`#`,e]},e))}),i&&(0,U.jsxs)(U.Fragment,{children:[(0,U.jsxs)(`div`,{className:`summary-grid`,children:[(0,U.jsx)(X,{label:`Source / Reason`,value:`${ga(`source`,i.Source)} / ${ga(`reason`,i.Reason)}`}),(0,U.jsx)(X,{label:`Reporter`,value:String(i.ReporterUserID),mono:!0}),(0,U.jsx)(X,{label:`Option`,value:i.Option,mono:!0}),(0,U.jsx)(X,{label:`Time`,value:G(i.CreatedAt)})]}),i.Comment&&(0,U.jsx)(`p`,{className:`about-text`,children:i.Comment}),(0,U.jsx)(Qn,{value:JSON.stringify(i,null,2)})]})]}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(K,{title:`Decision and action audit`,text:`Actions run idempotently through a lease worker; failures retain their error and attempt count.`}),(0,U.jsx)(Qn,{value:JSON.stringify({decisions:n.Decisions,actions:n.Actions},null,2)})]}),n.Appeals.length>0&&(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(K,{title:`Appeals`}),(0,U.jsx)(Qn,{value:JSON.stringify(n.Appeals,null,2)})]})]}),side:(0,U.jsxs)(`section`,{className:`action-dock`,children:[(0,U.jsx)(`div`,{className:`dock-title`,children:`Case actions`}),j&&(0,U.jsxs)(`button`,{className:`btn primary icon-text`,disabled:_,onClick:T,children:[(0,U.jsx)(xt,{size:15}),` `,A.AssignedTo?`Renew claim`:`Claim case`]}),(0,U.jsxs)(`label`,{className:`field`,children:[(0,U.jsx)(`span`,{children:`Review reason`}),(0,U.jsx)(`textarea`,{value:o,onChange:e=>s(e.target.value),rows:5})]}),(0,U.jsxs)(`label`,{className:`field`,children:[(0,U.jsx)(`span`,{children:`Decision template`}),(0,U.jsxs)(`select`,{value:c,onChange:e=>l(e.target.value),children:[(0,U.jsx)(`option`,{value:`no_violation`,children:`No violation (dismiss report)`}),(0,U.jsx)(`option`,{value:`scam`,children:`Mark as SCAM`}),(0,U.jsx)(`option`,{value:`fake`,children:`Mark as FAKE`}),(0,U.jsx)(`option`,{value:`freeze`,children:`Freeze account`}),(0,U.jsx)(`option`,{value:`scam_freeze`,children:`SCAM + freeze`}),(0,U.jsx)(`option`,{value:`fake_freeze`,children:`FAKE + freeze`}),(0,U.jsx)(`option`,{value:`delete_messages`,children:`Delete messages covered by evidence`}),(0,U.jsx)(`option`,{value:`delete_account`,children:`Delete account`})]})]}),c===`delete_messages`&&(0,U.jsxs)(U.Fragment,{children:[(0,U.jsxs)(`label`,{className:`field`,children:[(0,U.jsx)(`span`,{children:`Evidence message IDs (comma-separated)`}),(0,U.jsx)(`input`,{value:u,onChange:e=>d(e.target.value),placeholder:`101, 102`})]}),A.Target.Type===`user`&&(0,U.jsxs)(U.Fragment,{children:[(0,U.jsxs)(`label`,{className:`field`,children:[(0,U.jsx)(`span`,{children:`Private-chat owner_user_id`}),(0,U.jsx)(`input`,{value:f,onChange:e=>p(e.target.value),inputMode:`numeric`})]}),(0,U.jsxs)(`label`,{className:`field checkbox-field`,children:[(0,U.jsx)(`input`,{type:`checkbox`,checked:m,onChange:e=>h(e.target.checked)}),(0,U.jsx)(`span`,{children:`Revoke for both sides`})]})]}),(0,U.jsx)(q,{children:`The server will verify again that every message ID exists in this case's immutable report evidence.`})]}),A.Status===`action_failed`&&c===`no_violation`&&(0,U.jsx)(q,{children:`The action was partially executed and cannot be changed directly to no violation. Select a new action to retry while retaining the previous failure audit.`}),M&&(0,U.jsxs)(`button`,{className:`btn danger icon-text`,disabled:_||!N,onClick:E,children:[(0,U.jsx)(F,{size:15}),` `,A.Status===`action_failed`?`Retry action`:`Submit decision`]}),P&&A.AssignedTo&&(0,U.jsxs)(U.Fragment,{children:[(0,U.jsx)(`div`,{className:`dock-title`,children:`Appeal review #${P.ID}`}),(0,U.jsx)(X,{label:`Automatic remedy after approval`,value:w.label}),w.blocked&&(0,U.jsx)(q,{children:`The case contains a completed irreversible deletion. It cannot be marked as approved and restored; deny it or escalate for manual handling.`}),(0,U.jsx)(`button`,{className:`btn`,disabled:_,onClick:()=>D(P.ID,!1),children:`Deny appeal`}),(0,U.jsx)(`button`,{className:`btn primary`,disabled:_||w.blocked,onClick:()=>D(P.ID,!0),children:`Grant appeal`})]})]})})]})}function ya(e,t,n,r,i){switch(e){case`scam`:return[{kind:`mark_scam`,payload:{}}];case`fake`:return[{kind:`mark_fake`,payload:{}}];case`freeze`:return[{kind:`freeze_account`,payload:{}}];case`scam_freeze`:return[{kind:`mark_scam`,payload:{}},{kind:`freeze_account`,payload:{}}];case`fake_freeze`:return[{kind:`mark_fake`,payload:{}},{kind:`freeze_account`,payload:{}}];case`delete_messages`:return n.length===0?[]:t===`channel`?[{kind:`delete_channel_message`,payload:{ids:n}}]:t===`user`&&Number.isSafeInteger(r)&&r>0?[{kind:`delete_private_message`,payload:{owner_user_id:r,ids:n,revoke:i}}]:[];case`delete_account`:return[{kind:`delete_account`,payload:{}}];default:return[]}}function ba(e){let t=e.split(/[,\s]+/).filter(Boolean).map(Number);return t.length===0||t.some(e=>!Number.isSafeInteger(e)||e<=0)?[]:[...new Set(t)]}function xa(e){let t=!1,n=!1,r=!1;for(let i of[...e.Actions].sort((e,t)=>e.ID-t.ID))if(i.Status===`succeeded`)switch(i.Kind){case`mark_scam`:case`mark_fake`:t=!0;break;case`clear_peer_flags`:t=!1;break;case`freeze_account`:n=!0;break;case`unfreeze_account`:n=!1;break;case`delete_private_message`:case`delete_channel_message`:case`delete_account`:r=!0;break}let i=[],a=[];return t&&(i.push({kind:`clear_peer_flags`,payload:{}}),a.push(`Clear SCAM / FAKE`)),n&&(i.push({kind:`unfreeze_account`,payload:{}}),a.push(`Unfreeze account`)),{actions:i,label:a.join(` + `)||`No recovery action needed`,blocked:r}}function Sa(e){return{no_violation:`No violation (dismiss report)`,scam:`Mark as SCAM`,fake:`Mark as FAKE`,freeze:`Freeze account`,scam_freeze:`SCAM + freeze`,fake_freeze:`FAKE + freeze`,delete_messages:`Delete messages covered by evidence`,delete_account:`Delete account`}[e]}function Ca({label:e,sortKey:t,activeKey:n,desc:r,onSort:i}){return(0,U.jsx)(`th`,{children:(0,U.jsxs)(`button`,{type:`button`,className:`sort-header`,onClick:()=>i(t),children:[e,t===n?r?(0,U.jsx)(fe,{size:13}):(0,U.jsx)(ge,{size:13}):(0,U.jsx)(he,{size:13,className:`sort-header-idle`})]})})}function wa({navigate:e}){let[t,n]=(0,g.useState)(()=>zt(Vt.storageStats)??null),[r,i]=(0,g.useState)(!1),[a,o]=(0,g.useState)(()=>zt(Vt.storageAccounts)??[]),[s,c]=(0,g.useState)(!1),[l,u]=(0,g.useState)(0),[d,f]=(0,g.useState)(!1),[p,m]=(0,g.useState)(``),[h,_]=(0,g.useState)(``),[v,y]=(0,g.useState)(`bytes`),[b,x]=(0,g.useState)(!0);async function S(){try{let e=await k.storageStats();H(Vt.storageStats,e),n(e),i(!1)}catch{i(!0)}}async function C(e=!1){f(!0),m(``);let t=new URLSearchParams({limit:`50`,offset:String(e?l:0),sort:v,order:b?`desc`:`asc`});h.trim()&&t.set(`q`,h.trim());try{let n=await k.storageAccounts(t),r=n.rows??[];o(t=>{let n=e?[...t,...r]:r;return e||H(Vt.storageAccounts,n),n}),u(n.next_offset),c(!!n.has_more)}catch(e){m(O(e))}finally{f(!1)}}function w(){S(),C(!1)}(0,g.useEffect)(()=>{w()},[v,b]);function T(e){e===v?x(e=>!e):(y(e),x(!0))}let E=t?Math.max(0,Number(t.LogicalBytes)-Number(t.PhysicalBytes)):0;return(0,U.jsxs)(U.Fragment,{children:[p&&(0,U.jsx)(q,{children:p}),(0,U.jsxs)(`div`,{className:`metric-row`,children:[(0,U.jsx)(Y,{label:`Physical usage (on disk / S3)`,value:t?Un(t.PhysicalBytes):`-`,loading:!t&&!r}),(0,U.jsx)(Y,{label:`Logical usage (sum per account)`,value:t?Un(t.LogicalBytes):`-`,loading:!t&&!r}),(0,U.jsx)(Y,{label:`Saved by dedup`,value:Un(String(E)),loading:!t&&!r,tone:E>0?`good`:`neutral`}),(0,U.jsx)(Y,{label:`Backend`,value:t?.BackendKind??`-`,loading:!t&&!r})]}),(0,U.jsxs)(`div`,{className:`metric-row`,children:[(0,U.jsx)(Y,{label:`Documents`,value:t?In(t.DocumentCount):`-`,loading:!t&&!r}),(0,U.jsx)(Y,{label:`Photos`,value:t?In(t.PhotoCount):`-`,loading:!t&&!r}),(0,U.jsx)(Y,{label:`Accounts with media`,value:t?In(t.AccountCount):`-`,loading:!t&&!r}),(0,U.jsx)(Y,{label:`System/bundled content`,value:t?Un(t.SystemBytes):`-`,loading:!t&&!r})]}),(0,U.jsx)(Kn,{children:(0,U.jsxs)(`form`,{className:`toolbar`,onSubmit:e=>{e.preventDefault(),C(!1)},children:[(0,U.jsxs)(`label`,{className:`searchbox`,children:[(0,U.jsx)(V,{size:15}),(0,U.jsx)(`input`,{value:h,onChange:e=>_(e.target.value),placeholder:`User ID / username / name`})]}),(0,U.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,disabled:d,children:[d?(0,U.jsx)(I,{size:15,className:`spin`}):(0,U.jsx)(V,{size:15}),` `,`Search`]}),(0,U.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:w,disabled:d,children:[(0,U.jsx)(ft,{size:15,className:d?`spin`:``}),` `,`Refresh`]})]})}),(0,U.jsx)(`div`,{className:`table-wrap`,children:(0,U.jsxs)(`table`,{className:`data-table`,children:[(0,U.jsx)(`thead`,{children:(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(Ca,{label:`User ID`,sortKey:`user_id`,activeKey:v,desc:b,onSort:T}),(0,U.jsx)(Ca,{label:`Account`,sortKey:`username`,activeKey:v,desc:b,onSort:T}),(0,U.jsx)(Ca,{label:`Storage used`,sortKey:`bytes`,activeKey:v,desc:b,onSort:T}),(0,U.jsx)(Ca,{label:`Files`,sortKey:`files`,activeKey:v,desc:b,onSort:T}),(0,U.jsx)(`th`,{})]})}),(0,U.jsxs)(`tbody`,{children:[a.map(t=>(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`td`,{className:`mono`,children:t.UserID}),(0,U.jsx)(`td`,{children:W(t.Username)||t.FirstName||`-`}),(0,U.jsx)(`td`,{className:`mono`,children:Un(t.Bytes)}),(0,U.jsx)(`td`,{className:`mono`,children:In(t.FileCount)}),(0,U.jsx)(`td`,{children:(0,U.jsxs)(`button`,{className:`row-link`,type:`button`,onClick:()=>e(`/accounts/${t.UserID}`),children:[`Details`,` `,(0,U.jsx)(z,{size:14})]})})]},t.UserID)),a.length===0&&(d?(0,U.jsx)(Xn,{colSpan:5}):(0,U.jsx)(Yn,{colSpan:5}))]})]})}),s&&(0,U.jsx)(`div`,{className:`toolbar`,children:(0,U.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>C(!0),disabled:d,children:[d?(0,U.jsx)(I,{size:15,className:`spin`}):(0,U.jsx)(Ce,{size:15}),` `,`Load more`]})})]})}function Ta({navigate:e}){let[t,n]=(0,g.useState)(`overview`);return(0,U.jsxs)(Gn,{title:`Storage`,eyebrow:`Media / Storage usage`,children:[(0,U.jsxs)(`div`,{className:`tab-bar`,role:`tablist`,"aria-label":`Storage sections`,children:[(0,U.jsx)(`button`,{className:`tab-btn ${t===`overview`?`active`:``}`,type:`button`,role:`tab`,"aria-selected":t===`overview`,onClick:()=>n(`overview`),children:`Overview`}),(0,U.jsx)(`button`,{className:`tab-btn ${t===`limits`?`active`:``}`,type:`button`,role:`tab`,"aria-selected":t===`limits`,onClick:()=>n(`limits`),children:`Limits & Retention`})]}),t===`overview`?(0,U.jsx)(wa,{navigate:e}):(0,U.jsx)(Va,{})]})}var Ea={blobBackend:`TELESRV_BLOB_BACKEND`,maxTotal:`TELESRV_STORAGE_MAX_TOTAL_BYTES`,minFree:`TELESRV_STORAGE_MIN_FREE_BYTES`,maxUploadFile:`TELESRV_STORAGE_MAX_UPLOAD_FILE_BYTES`,retentionMode:`TELESRV_STORAGE_RETENTION_MODE`,retentionMaxAge:`TELESRV_STORAGE_RETENTION_MAX_AGE`,evictionEnable:`TELESRV_STORAGE_EVICTION_ENABLE`},Da=[{key:`photo`,envKey:`TELESRV_STORAGE_RETENTION_MAX_AGE_PHOTO`,label:`Photo`},{key:`video`,envKey:`TELESRV_STORAGE_RETENTION_MAX_AGE_VIDEO`,label:`Video`},{key:`round_video`,envKey:`TELESRV_STORAGE_RETENTION_MAX_AGE_ROUND_VIDEO`,label:`Round video (video message)`},{key:`gif`,envKey:`TELESRV_STORAGE_RETENTION_MAX_AGE_GIF`,label:`GIF`},{key:`music`,envKey:`TELESRV_STORAGE_RETENTION_MAX_AGE_MUSIC`,label:`Music`},{key:`voice`,envKey:`TELESRV_STORAGE_RETENTION_MAX_AGE_VOICE`,label:`Voice message`},{key:`file`,envKey:`TELESRV_STORAGE_RETENTION_MAX_AGE_FILE`,label:`File`},{key:`avatar`,envKey:`TELESRV_STORAGE_RETENTION_MAX_AGE_AVATAR`,label:`Avatar`}],Oa=new Set([`gif`,`avatar`]),ka=524288*8e3,Aa=[{label:`MB`,bytes:1024**2},{label:`GB`,bytes:1024**3},{label:`TB`,bytes:1024**4}],ja=Aa.filter(e=>e.label!==`TB`),Ma=4*1024**3;function Na(e,t=Aa){for(let n=t.length-1;n>=0;n--)if(e>=t[n].bytes)return t[n];return t[Math.min(1,t.length-1)]}function Pa(e){let t=0,n=/(\d+(?:\.\d+)?)\s*(h|m|s)/g,r;for(;(r=n.exec(e))!==null;){let e=parseFloat(r[1]),n=r[2];t+=n===`h`?e*3600:n===`m`?e*60:e}return t/60}var Fa=[{label:`Minutes`,minutes:1},{label:`Hours`,minutes:60},{label:`Days`,minutes:1440}];function Ia(e){for(let t=Fa.length-1;t>=0;t--)if(e>=Fa[t].minutes)return Fa[t];return Fa[0]}function La({label:e,help:t,minutes:n,disabled:r,onChange:i}){let a=Number(n||`0`),[o,s]=(0,g.useState)(()=>Ia(a).label),c=Fa.find(e=>e.label===o)??Fa[2],l=Number.isFinite(a)?a/c.minutes:NaN;function u(e){if(!e.trim()){i(`0`);return}let t=Number(e);if(Number.isNaN(t)||t<0){i(`0`);return}if(t===0){i(`0`);return}i(String(Math.max(1,Math.round(t*c.minutes))))}return(0,U.jsxs)(`label`,{className:`duration-field`,children:[(0,U.jsx)(`span`,{children:e}),(0,U.jsxs)(`div`,{style:{display:`flex`,gap:8},children:[(0,U.jsx)(`input`,{type:`number`,min:`0`,step:`any`,value:Number.isNaN(l)?``:l,placeholder:`0`,disabled:r,onChange:e=>u(e.target.value)}),(0,U.jsx)(`select`,{value:o,disabled:r,onChange:e=>s(e.target.value),style:{maxWidth:100},children:Fa.map(e=>(0,U.jsx)(`option`,{value:e.label,children:e.label},e.label))})]}),(0,U.jsx)(`span`,{className:`env-field-desc`,children:t})]})}function Ra({label:e,help:t,bytes:n,onChange:r,units:i=Aa,maxBytes:a}){let o=Number(n||`0`),[s,c]=(0,g.useState)(()=>Na(o,i).label),l=i.find(e=>e.label===s)??i[0],u=o>0?o/l.bytes:NaN;function d(e){let t=Number(e);if(!e.trim()||Number.isNaN(t)||t<=0){r(`0`);return}let n=Math.round(t*l.bytes);a&&n>a&&(n=a),r(String(n))}return(0,U.jsxs)(`label`,{className:`duration-field`,children:[(0,U.jsx)(`span`,{children:e}),(0,U.jsxs)(`div`,{style:{display:`flex`,gap:8},children:[(0,U.jsx)(`input`,{type:`number`,min:`0`,max:a?a/l.bytes:void 0,step:`any`,value:Number.isNaN(u)?``:u,placeholder:`Unlimited`,onChange:e=>d(e.target.value)}),(0,U.jsx)(`select`,{value:s,onChange:e=>c(e.target.value),style:{maxWidth:90},children:i.map(e=>(0,U.jsx)(`option`,{value:e.label,children:e.label},e.label))})]}),(0,U.jsx)(`span`,{className:`env-field-desc`,children:t})]})}function za({categoryAgeMinutes:e,onChange:t,disabled:n,onClose:r}){return(0,On.createPortal)((0,U.jsx)(`div`,{className:`modal-backdrop`,role:`presentation`,children:(0,U.jsxs)(`section`,{className:`modal command-modal`,role:`dialog`,"aria-modal":`true`,"aria-label":`Per-category retention overrides`,children:[(0,U.jsxs)(`div`,{className:`modal-head`,children:[(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`div`,{className:`eyebrow`,children:`Limits & Retention`}),(0,U.jsx)(`h2`,{children:`Per-category overrides`})]}),(0,U.jsx)(`button`,{className:`icon-btn`,type:`button`,onClick:r,"aria-label":`Close`,children:(0,U.jsx)(It,{size:15})})]}),(0,U.jsxs)(`div`,{className:`command-body`,children:[(0,U.jsx)(`p`,{className:`env-field-desc`,children:`Leave a category at 0 to inherit the shared Retention age. The mode switch still applies to all of them -- these only change how old that one category's media must be.`}),Da.filter(e=>!Oa.has(e.key)).map(r=>(0,U.jsx)(La,{label:r.label,help:`Inherits the shared Retention age when left at 0.`,minutes:e[r.key]||`0`,disabled:n,onChange:e=>t(r.key,e)},r.key)),(0,U.jsxs)(`div`,{className:`danger-zone-box`,children:[(0,U.jsx)(`div`,{className:`danger-zone-box-title`,children:`Danger zone`}),(0,U.jsxs)(`div`,{className:`danger-zone-box-body`,children:[(0,U.jsx)(`p`,{className:`env-field-desc`,children:`Also affects built-in system bot avatars and the bundled GIF catalog (@gif), not just user media.`}),Da.filter(e=>Oa.has(e.key)).map(r=>(0,U.jsx)(La,{label:r.label,help:`Inherits the shared Retention age when left at 0.`,minutes:e[r.key]||`0`,disabled:n,onChange:e=>t(r.key,e)},r.key))]})]})]}),(0,U.jsx)(`div`,{className:`modal-actions`,children:(0,U.jsx)(`button`,{className:`btn primary`,type:`button`,onClick:r,children:`Close`})})]})}),document.body)}function Ba({onClose:e}){let[t,n]=(0,g.useState)({}),[r,i]=(0,g.useState)(``),a=Da.filter(e=>e.key!==`avatar`),o=Da.every(e=>t[e.key]);function s(e){n(t=>({...t,[e]:!t[e]}))}function c(){let e=!o,t={};for(let n of Da)t[n.key]=e;n(t)}let l=a.filter(e=>t[e.key]).map(e=>e.key),u=!!t.avatar,d=l.length>0||u;return(0,On.createPortal)((0,U.jsx)(`div`,{className:`modal-backdrop`,role:`presentation`,children:(0,U.jsxs)(`section`,{className:`modal command-modal`,role:`dialog`,"aria-modal":`true`,"aria-label":`Manually purge storage`,children:[(0,U.jsxs)(`div`,{className:`modal-head`,children:[(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`div`,{className:`eyebrow`,children:`Limits & Retention`}),(0,U.jsx)(`h2`,{children:`Manually purge storage`})]}),(0,U.jsx)(`button`,{className:`icon-btn`,type:`button`,onClick:e,"aria-label":`Close`,children:(0,U.jsx)(It,{size:15})})]}),(0,U.jsxs)(`div`,{className:`command-body`,children:[(0,U.jsx)(`p`,{className:`env-field-desc`,children:`Deletes the file bytes of every document/photo matching the categories below, right now -- independent of the retention mode/age configured above. The message/profile-photo itself is never deleted, only its file; a purged item starts showing as unavailable. Leave "Created before" empty to purge everything in the selected categories, regardless of age.`}),(0,U.jsxs)(`div`,{className:`attr-block`,children:[(0,U.jsxs)(`label`,{className:`checkline`,children:[(0,U.jsx)(`input`,{type:`checkbox`,checked:o,onChange:c}),` `,`Select all`]}),Da.filter(e=>!Oa.has(e.key)).map(e=>(0,U.jsxs)(`label`,{className:`checkline`,children:[(0,U.jsx)(`input`,{type:`checkbox`,checked:!!t[e.key],onChange:()=>s(e.key)}),` `,e.label]},e.key))]}),(0,U.jsxs)(`div`,{className:`danger-zone-box`,children:[(0,U.jsx)(`div`,{className:`danger-zone-box-title`,children:`Danger zone`}),(0,U.jsxs)(`div`,{className:`danger-zone-box-body`,children:[(0,U.jsx)(`p`,{className:`env-field-desc`,children:`Avatar also purges system bot avatars (auto-restored on next restart). GIF also purges the bundled GIF catalog (@gif) -- permanently, unless its source file is still in TELESRV_GIF_SEED_DIR.`}),Da.filter(e=>Oa.has(e.key)).map(e=>(0,U.jsxs)(`label`,{className:`checkline`,children:[(0,U.jsx)(`input`,{type:`checkbox`,checked:!!t[e.key],onChange:()=>s(e.key)}),` `,e.label]},e.key))]})]}),(0,U.jsxs)(`label`,{className:`duration-field`,children:[(0,U.jsx)(`span`,{children:`Created before (optional)`}),(0,U.jsx)(`input`,{type:`date`,value:r,onChange:e=>i(e.target.value)}),(0,U.jsx)(`span`,{className:`env-field-desc`,children:`Empty = no age limit, purge everything matching the selected categories.`})]})]}),(0,U.jsxs)(`div`,{className:`modal-actions`,children:[(0,U.jsx)(`button`,{className:`btn`,type:`button`,onClick:e,children:`Close`}),(0,U.jsx)(Z,{disabled:!d,label:`Purge selected storage`,tone:`danger`,path:`/api/actions/storage-manual-purge`,payload:()=>({categories:l,include_avatars:u,created_before:r?new Date(r).toISOString():void 0})})]})]})}),document.body)}function Va(){let[e,t]=(0,g.useState)(!1),[n,r]=(0,g.useState)(``),[i,a]=(0,g.useState)({}),[o,s]=(0,g.useState)(`s3`),[c,l]=(0,g.useState)(`0`),[u,d]=(0,g.useState)(`0`),[f,p]=(0,g.useState)(`0`),[m,h]=(0,g.useState)(`off`),[_,v]=(0,g.useState)(`43200`),[y,b]=(0,g.useState)({}),[x,S]=(0,g.useState)(!1),[C,w]=(0,g.useState)(!1),[T,E]=(0,g.useState)(!1);async function D(){r(``);try{let e=await k.serverEnv(),n={};for(let t of e)for(let e of t.fields)n[e.key]=e.value||e.default_value||``;a(n),s((n[Ea.blobBackend]||`s3`).trim().toLowerCase()),l(n[Ea.maxTotal]||`0`),d(n[Ea.minFree]||`0`),p(n[Ea.maxUploadFile]||`0`);let r=(n[Ea.retentionMode]||`off`).trim().toLowerCase();h(r===`orphan`||r===`hard`?r:`off`);let i=n[Ea.retentionMaxAge],o=Pa(i||`720h`);v(String(Math.max(0,Math.round(o))));let c={};for(let e of Da){let t=Pa(n[e.envKey]||``);c[e.key]=t>0?String(Math.round(t)):`0`}b(c),w((n[Ea.evictionEnable]||`false`).trim().toLowerCase()===`true`),t(!0)}catch(e){r(O(e))}}(0,g.useEffect)(()=>{D()},[]);let A=(0,g.useMemo)(()=>{let e={[Ea.maxTotal]:c||`0`,[Ea.minFree]:u||`0`,[Ea.maxUploadFile]:f||`0`,[Ea.retentionMode]:m,[Ea.retentionMaxAge]:m===`off`?i[Ea.retentionMaxAge]||`720h`:`${Math.max(0,Math.round(Number(_||`0`)))}m`,[Ea.evictionEnable]:C?`true`:`false`};for(let t of Da){let n=Math.round(Number(y[t.key]||`0`));e[t.envKey]=n>0?`${n}m`:``}let t={};for(let[n,r]of Object.entries(e))(i[n]??``)!==r&&(t[n]=r);return t},[c,u,f,m,_,y,C,i]),j=Object.keys(A).length>0,M=Number(f||`0`)>ka;return(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(K,{title:`Limits & Retention`,text:`Server-wide storage budget, per-file upload cap, and automatic media cleanup. Saved to .env -- takes effect on the next Restart/Update.`}),n&&(0,U.jsx)(q,{children:n}),e?(0,U.jsxs)(`div`,{className:`card-body`,children:[(0,U.jsxs)(`div`,{className:`action-groups`,children:[(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(K,{title:`Size limits`}),(0,U.jsx)(`div`,{className:`card-body`,children:(0,U.jsxs)(`div`,{className:`attr-block`,children:[(0,U.jsx)(Ra,{label:`Max total storage budget`,help:`Reject new uploads once total tracked blob bytes would exceed this. Empty/0 = unlimited.`,bytes:c,onChange:l}),o===`localfs`&&(0,U.jsx)(Ra,{label:`Min free space guard`,help:`localfs backend only: reject new uploads once real free disk space falls below this. Empty/0 disables the check.`,bytes:u,onChange:d}),(0,U.jsx)(Ra,{label:`Max single file size`,help:`Reject a single upload once its total assembled size exceeds this. Empty/0 = unlimited, bounded only by the protocol's own ~4GB per-file ceiling.`,bytes:f,onChange:p,units:ja,maxBytes:Ma}),M&&(0,U.jsx)(q,{children:`This exceeds the protocol's own ~4GB upload ceiling and will be refused when the server restarts.`})]})})]}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(K,{title:`Retention`,action:(0,U.jsxs)(`button`,{className:`btn compact-btn icon-text`,type:`button`,onClick:()=>S(!0),children:[(0,U.jsx)(_t,{size:14}),` `,`Per-category...`]})}),(0,U.jsx)(`div`,{className:`card-body`,children:(0,U.jsxs)(`div`,{className:`attr-block`,children:[(0,U.jsxs)(`label`,{className:`duration-field`,children:[(0,U.jsx)(`span`,{children:`Retention mode`}),(0,U.jsxs)(`select`,{value:m,onChange:e=>h(e.target.value),children:[(0,U.jsx)(`option`,{value:`off`,children:`Off`}),(0,U.jsx)(`option`,{value:`orphan`,children:`Delete once no longer used (safe)`}),(0,U.jsx)(`option`,{value:`hard`,children:`Delete after a fixed time, even if still in use`})]})]}),(0,U.jsxs)(`p`,{className:`env-field-desc`,children:[m===`off`&&`No storage sweep runs; nothing is auto-deleted. Storage usage is still tracked and shown above either way.`,m===`orphan`&&`Safe: a document or photo's file is deleted only once it is no longer referenced by any message, profile photo, or sticker set. Media still visible in a conversation is never touched, regardless of age.`,m===`hard`&&`Irreversible and aggressive: a document or photo's file bytes are deleted once old enough, REGARDLESS of whether a message still references it. Old media in active conversations will start showing as unavailable once purged -- only the file is removed, the message itself keeps rendering its placeholder (name, size, thumbnail).`]}),(0,U.jsx)(La,{label:`Retention age`,help:(m===`hard`?`How old the media itself must be, counted from when it was uploaded, before its bytes are purged.`:`How long a document or photo must have had zero references before its file is deleted.`)+` Set to 0 to disable the sweep by default and only clean up categories you explicitly override via Per-category.`,minutes:_,disabled:m===`off`,onChange:v})]})})]}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(K,{title:`Reclaim space`}),(0,U.jsx)(`div`,{className:`card-body`,children:(0,U.jsxs)(`div`,{className:`attr-block`,children:[(0,U.jsxs)(`label`,{className:`checkline`,children:[(0,U.jsx)(`input`,{type:`checkbox`,checked:C,onChange:e=>w(e.target.checked)}),` `,`Actively reclaim space once over budget`]}),(0,U.jsx)(`p`,{className:`env-field-desc`,children:`Once total physical storage exceeds the Max total storage budget, actively delete the oldest files (regardless of category or age) until back under budget -- the same way "hard" retention mode purges files. Independent of the retention mode: this can run even when that's Off. Off by default, since this changes the storage budget from block-new-uploads-only to also reclaiming from existing files.`})]})})]}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(K,{title:`Manual purge`}),(0,U.jsx)(`div`,{className:`card-body`,children:(0,U.jsxs)(`div`,{className:`attr-block`,children:[(0,U.jsxs)(`button`,{className:`btn danger icon-text`,type:`button`,onClick:()=>E(!0),children:[(0,U.jsx)(_t,{size:15}),` `,`Manually purge storage...`]}),(0,U.jsx)(`span`,{className:`env-field-desc`,children:`Delete media file bytes right now by hand-picked category and an optional age cutoff, independent of the retention settings above.`})]})})]})]}),x&&(0,U.jsx)(za,{categoryAgeMinutes:y,onChange:(e,t)=>b(n=>({...n,[e]:t})),disabled:m===`off`,onClose:()=>S(!1)}),T&&(0,U.jsx)(Ba,{onClose:()=>E(!1)}),(0,U.jsx)(`div`,{className:`gift-table-actions env-save-row`,children:(0,U.jsx)(Z,{tone:`warn`,label:`Save limits & retention settings`,path:`/api/actions/update-server-env`,payload:()=>({values:A}),disabled:!j||M,onDone:()=>void D()})})]}):(0,U.jsx)(`p`,{style:{color:`var(--muted)`},children:`Loading current settings...`})]})}function Ha({loader:e,cacheKey:t,className:n,playOnHover:r=!0,onError:i}){let a=(0,g.useRef)(null),o=(0,g.useRef)(null);(0,g.useEffect)(()=>{let t=!1;return e().then(e=>{t||!a.current||(o.current?.destroy(),o.current=qi.default.loadAnimation({container:a.current,renderer:`canvas`,loop:!0,autoplay:!1,animationData:structuredClone(e)}),o.current.goToAndStop(0,!0))}).catch(()=>i?.()),()=>{t=!0,o.current?.destroy(),o.current=null}},[t]);function s(){r&&o.current?.play()}function c(){r&&o.current?.goToAndStop(0,!0)}return(0,U.jsx)(`div`,{className:n,ref:a,onMouseEnter:s,onMouseLeave:c})}function Ua(e){let t=e.toLowerCase();return t.includes(`tgsticker`)||t.includes(`lottie`)||t.includes(`json`)}function Wa({row:e}){let[t,n]=(0,g.useState)(!Ua(e.MimeType));return(0,g.useEffect)(()=>{n(!Ua(e.MimeType))},[e.DocumentID,e.MimeType]),t?(0,U.jsx)(`div`,{className:`emoji-picker-glyph`,children:e.Alt||`🙂`}):(0,U.jsx)(Ha,{className:`emoji-picker-anim`,cacheKey:e.DocumentID,loader:()=>k.emojiAnimation(e.DocumentID),onError:()=>n(!0)})}function Ga({label:e,value:t,onChange:n}){let[r,i]=(0,g.useState)(``),[a,o]=(0,g.useState)([]),[s,c]=(0,g.useState)(!1),[l,u]=(0,g.useState)(``),d=a.find(e=>e.DocumentID===t)??null;async function f(){c(!0),u(``);let e=new URLSearchParams({limit:`24`});r.trim()&&e.set(`q`,r.trim());try{o((await k.emoji(e)).rows??[])}catch(e){u(O(e))}finally{c(!1)}}return(0,g.useEffect)(()=>{f()},[]),(0,U.jsxs)(`div`,{className:`entity-picker`,children:[(0,U.jsxs)(`div`,{className:`picker-head`,children:[(0,U.jsx)(`span`,{children:e}),t?(0,U.jsxs)(`button`,{className:`link-button`,type:`button`,onClick:()=>n(``),children:[(0,U.jsx)(It,{size:13}),` `,`Clear`]}):null]}),t?(0,U.jsxs)(`div`,{className:`selected-entity`,children:[(0,U.jsx)(Se,{size:15}),(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`strong`,{children:d?.Alt||`—`}),(0,U.jsx)(`span`,{className:`mono`,children:t})]}),(0,U.jsx)(`span`,{children:d?.SetTitle||`-`})]}):null,(0,U.jsxs)(`div`,{className:`picker-search`,children:[(0,U.jsx)(V,{size:15}),(0,U.jsx)(`input`,{value:r,onChange:e=>i(e.target.value),onKeyDown:e=>{e.key===`Enter`&&(e.preventDefault(),f())},placeholder:`Search document ID or emoji`}),(0,U.jsx)(`button`,{className:`btn compact-btn`,type:`button`,onClick:f,disabled:s,children:s?(0,U.jsx)(I,{size:14,className:`spin`}):`Search`})]}),l&&(0,U.jsx)(`div`,{className:`picker-error`,children:l}),(0,U.jsxs)(`div`,{className:`picker-results emoji-picker-results`,children:[a.map(e=>(0,U.jsxs)(`button`,{className:`picker-row emoji-picker-row ${t===e.DocumentID?`selected`:``}`,type:`button`,onClick:()=>n(e.DocumentID),children:[(0,U.jsx)(Wa,{row:e}),(0,U.jsx)(`span`,{className:`mono`,children:e.DocumentID}),(0,U.jsx)(`span`,{children:e.SetTitle||`—`})]},e.DocumentID)),a.length===0&&!s?(0,U.jsx)(`div`,{className:`picker-empty`,children:`No results`}):null]})]})}var Ka=[`pending`,`approved`,`rejected`,`revoked`],qa=[`user`,`channel`],Ja={pending:`Pending`,approved:`Approved`,rejected:`Rejected`,revoked:`Mark revoked`},Ya={user:`Account`,channel:`Channel`};function Xa({navigate:e}){let{can:t}=cn(),n=t(Kt),r=t(Wt),[i,a]=(0,g.useState)(`requests`),[o,s]=(0,g.useState)([]),[c,l]=(0,g.useState)([]),[u,d]=(0,g.useState)(``),[f,p]=(0,g.useState)(!1);async function m(){d(``),p(!1);try{let[e,t]=await Promise.all([k.botVerifiers(new URLSearchParams({limit:`200`})),k.verificationIcons(new URLSearchParams({limit:`200`}))]);s(e.rows??[]),l(t.rows??[])}catch(e){if(e instanceof v&&e.status===403){s([]),l([]),p(!0);return}d(O(e))}}return(0,g.useEffect)(()=>{m()},[]),(0,U.jsxs)(Gn,{title:`Third-party verification`,eyebrow:`Third-party verification / Verifiers, icons, marks`,actions:r?(0,U.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>e(`/verification`),children:[(0,U.jsx)(je,{size:15}),` `,`Official verification`]}):void 0,children:[u&&(0,U.jsx)(q,{children:u}),f&&(0,U.jsx)(q,{children:`The server refused the verifier roster and the icon catalogue for this session (403), so both lists are empty here — applications can still be reviewed.`}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(K,{title:`A verifier company's icon — not the official checkmark`,text:`A third-party mark is a verifier bot's own icon, drawn right BEFORE the name of an account, a bot or a channel, plus one line of description in the profile. It says “this verifier vouches for this peer”, and nothing more.`}),(0,U.jsx)(`p`,{className:`bot-create-note`,children:`The icon is a custom emoji document. The client fetches it through messages.getCustomEmojiDocuments, so a document id that resolves to nothing renders as no badge at all — which is why marks are granted from the catalogue below rather than from a typed number.`}),(0,U.jsx)(`p`,{className:`bot-create-note`,children:`The official checkmark is a different mechanism, granted by the platform in the Verification section. The two are stored, shown and taken away separately, and neither one implies the other.`}),!n&&(0,U.jsx)(`p`,{className:`bot-create-note`,children:`This session can read the section and decide applications, but not change verifiers or the icon catalogue — that needs the botverification.manage permission.`})]}),(0,U.jsx)(`div`,{className:`toolbar`,role:`group`,"aria-label":`Third-party verification`,children:[{key:`requests`,label:`Applications`,icon:(0,U.jsx)(Tt,{size:15})},{key:`verifiers`,label:`Verifiers`,icon:(0,U.jsx)(be,{size:15})},{key:`icons`,label:`Icon catalogue`,icon:(0,U.jsx)(Et,{size:15})},{key:`marks`,label:`Granted marks`,icon:(0,U.jsx)(ee,{size:15})}].map(e=>(0,U.jsxs)(`button`,{className:`btn icon-text ${i===e.key?`primary`:``}`,type:`button`,"aria-pressed":i===e.key,onClick:()=>a(e.key),children:[e.icon,` `,e.label]},e.key))}),i===`requests`&&(0,U.jsx)(Za,{navigate:e,verifiers:o}),i===`verifiers`&&(0,U.jsx)(Qa,{verifiers:o,icons:c,canManage:n,onChanged:m,navigate:e}),i===`icons`&&(0,U.jsx)($a,{icons:c,verifiers:o,canManage:n,onChanged:m}),i===`marks`&&(0,U.jsx)(eo,{verifiers:o,canManage:n,navigate:e})]})}function Za({navigate:e,verifiers:t}){let[n,r]=(0,g.useState)(`pending`),[i,a]=(0,g.useState)(``),[o,s]=(0,g.useState)(`all`),[c,l]=(0,g.useState)(``),[u,d]=(0,g.useState)(`50`),[f,p]=(0,g.useState)([]),[m,h]=(0,g.useState)({}),[_,v]=(0,g.useState)(!1),[y,b]=(0,g.useState)(``),[x,S]=(0,g.useState)(!1),[C,w]=(0,g.useState)(``);async function T(e=!1){S(!0),w(``);let t=new URLSearchParams({limit:u});n!==`all`&&t.set(`status`,n),i&&t.set(`verifier_bot_id`,i),o!==`all`&&t.set(`peer_type`,o),c.trim()&&t.set(`q`,c.trim().replace(/^@/,``)),e&&y&&t.set(`before_id`,y);try{let n=await k.customVerificationRequests(t),r=n.rows??[];p(t=>e?[...t,...r]:r),b(n.next_before_id??``),v(!!n.has_more)}catch(e){w(O(e))}finally{S(!1)}}async function E(){try{h((await k.botVerificationCounts()).counts??{})}catch(e){w(O(e))}}(0,g.useEffect)(()=>{T(!1),E()},[]);function D(){T(!1),E()}return(0,U.jsxs)(U.Fragment,{children:[(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(K,{title:`Application queue`,text:`Applications filed with a verifier bot by the owner of the peer. The counters cover the whole queue, not the page below.`,action:(0,U.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:D,disabled:x,children:[(0,U.jsx)(ft,{size:15,className:x?`spin`:``}),` `,`Refresh`]})}),C&&(0,U.jsx)(q,{children:C}),(0,U.jsx)(`div`,{className:`metric-row`,children:Ka.map(e=>(0,U.jsx)(Y,{label:Ja[e],value:m[e]??`0`,mono:!0,tone:io(e,m[e]??`0`)},e))})]}),(0,U.jsx)(Kn,{children:(0,U.jsxs)(`form`,{className:`toolbar`,onSubmit:e=>{e.preventDefault(),T(!1)},children:[(0,U.jsxs)(`label`,{className:`searchbox`,children:[(0,U.jsx)(V,{size:15}),(0,U.jsx)(`input`,{value:c,onChange:e=>l(e.target.value),placeholder:`Application id, peer id, username or title`})]}),(0,U.jsxs)(`label`,{className:`field-inline`,children:[(0,U.jsx)(`span`,{children:`Status`}),(0,U.jsxs)(`select`,{value:n,onChange:e=>r(e.target.value),children:[(0,U.jsx)(`option`,{value:`all`,children:`All statuses`}),Ka.map(e=>(0,U.jsx)(`option`,{value:e,children:Ja[e]},e))]})]}),(0,U.jsxs)(`label`,{className:`field-inline`,children:[(0,U.jsx)(`span`,{children:`Verifier`}),(0,U.jsx)(to,{value:i,verifiers:t,onChange:a})]}),(0,U.jsxs)(`label`,{className:`field-inline`,children:[(0,U.jsx)(`span`,{children:`Peer type`}),(0,U.jsxs)(`select`,{value:o,onChange:e=>s(e.target.value),children:[(0,U.jsx)(`option`,{value:`all`,children:`All types`}),qa.map(e=>(0,U.jsx)(`option`,{value:e,children:Ya[e]},e))]})]}),(0,U.jsxs)(`label`,{className:`field-inline`,children:[(0,U.jsx)(`span`,{children:`Limit`}),(0,U.jsx)(`input`,{className:`small-input`,value:u,onChange:e=>d(e.target.value),type:`number`,min:`1`,max:`200`})]}),(0,U.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,disabled:x,children:[x?(0,U.jsx)(I,{size:15,className:`spin`}):(0,U.jsx)(V,{size:15}),` `,`Search`]})]})}),(0,U.jsx)(`div`,{className:`table-wrap`,children:(0,U.jsxs)(`table`,{className:`data-table`,children:[(0,U.jsx)(`thead`,{children:(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`th`,{children:`ID`}),(0,U.jsx)(`th`,{children:`Verifier`}),(0,U.jsx)(`th`,{children:`Peer`}),(0,U.jsx)(`th`,{children:`Applicant`}),(0,U.jsx)(`th`,{children:`Stated reason`}),(0,U.jsx)(`th`,{children:`Status`}),(0,U.jsx)(`th`,{children:`Filed`}),(0,U.jsx)(`th`,{})]})}),(0,U.jsxs)(`tbody`,{children:[f.map(t=>(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`td`,{className:`mono`,children:(0,U.jsxs)(`button`,{className:`row-link`,type:`button`,onClick:()=>e(`/bot-verification/${t.ID}`),children:[`#`,t.ID]})}),(0,U.jsxs)(`td`,{children:[(0,U.jsx)(`strong`,{children:W(t.VerifierBotUsername)||t.VerifierBotID}),(0,U.jsx)(`div`,{className:`entity-subtitle mono`,children:t.VerifierBotID})]}),(0,U.jsxs)(`td`,{children:[(0,U.jsx)(`strong`,{children:ao(t)}),(0,U.jsxs)(`div`,{className:`entity-subtitle mono`,children:[Ya[t.PeerType],` · `,t.PeerID]})]}),(0,U.jsxs)(`td`,{children:[W(t.ApplicantUsername)||`-`,(0,U.jsx)(`div`,{className:`entity-subtitle mono`,children:t.ApplicantUserID})]}),(0,U.jsx)(`td`,{className:`truncate`,children:t.Reason||`-`}),(0,U.jsx)(`td`,{children:(0,U.jsx)(no,{status:t.Status})}),(0,U.jsx)(`td`,{children:G(t.CreatedAt)||`-`}),(0,U.jsx)(`td`,{children:(0,U.jsxs)(`button`,{className:`row-link`,type:`button`,onClick:()=>e(`/bot-verification/${t.ID}`),children:[(0,U.jsx)(Tt,{size:14}),` `,`Details`,` `,(0,U.jsx)(z,{size:14})]})})]},t.ID)),f.length===0&&(0,U.jsx)(Yn,{colSpan:8})]})]})}),_&&(0,U.jsx)(`div`,{className:`toolbar`,children:(0,U.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>T(!0),disabled:x,children:[x?(0,U.jsx)(I,{size:15,className:`spin`}):(0,U.jsx)(Ce,{size:15}),` `,`Load more`]})})]})}function Qa({verifiers:e,icons:t,canManage:n,onChanged:r,navigate:i}){let[a,o]=(0,g.useState)(null),[s,c]=(0,g.useState)(null),[l,u]=(0,g.useState)(``),[d,f]=(0,g.useState)(``),[p,m]=(0,g.useState)(``),[h,_]=(0,g.useState)(!1),v=t.filter(e=>e.Active),y=v.map(e=>({value:e.DocumentID,label:`${e.Name} · ${e.DocumentID}`}));if(l&&!y.some(e=>e.value===l)){let e=t.find(e=>e.DocumentID===l);y.unshift({value:l,label:`${e?.Name??l} · ${l} (Retired)`})}function b(e){c(e),o(null),u(e.IconDocumentID),f(e.CompanyName),m(e.DefaultDescription),_(e.CanModifyCustomDescription)}function x(){c(null),o(null),u(``),f(``),m(``),_(!1)}function S(){return{bot_id:s?s.BotID:a?String(a.ID):`0`,icon_document_id:l||`0`,company_name:d.trim(),default_description:p.trim(),can_modify_custom_description:h,version:s?s.Version:`0`}}return(0,U.jsxs)(U.Fragment,{children:[n&&(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(K,{title:s?`Update verifier`:`Grant verifier status`,text:`The bot gets an icon from the catalogue and a company name to vouch under. The same call updates an existing verifier, which is why it carries a version.`,action:s?(0,U.jsx)(`button`,{className:`btn icon-text`,type:`button`,onClick:x,children:`Cancel update`}):void 0}),s?(0,U.jsx)(`p`,{className:`bot-create-note`,children:`Updating ${W(s.BotUsername)||s.BotID} — version ${s.Version} is sent as the optimistic lock, so a row somebody else changed meanwhile is refused instead of overwritten.`}):(0,U.jsx)(pi,{label:`Bot`,value:a,onChange:o}),(0,U.jsxs)(`div`,{className:`bot-create-fields`,children:[(0,U.jsxs)(`label`,{className:`duration-field`,children:[(0,U.jsx)(`span`,{children:`Icon from the catalogue`}),(0,U.jsxs)(`select`,{value:l,onChange:e=>u(e.target.value),children:[(0,U.jsx)(`option`,{value:``,children:`Pick an icon`}),y.map(e=>(0,U.jsx)(`option`,{value:e.value,children:e.label},e.value))]})]}),(0,U.jsxs)(`label`,{className:`duration-field`,children:[(0,U.jsx)(`span`,{children:`Company`}),(0,U.jsx)(`input`,{value:d,onChange:e=>f(e.target.value),placeholder:`Acme Verification Ltd`})]}),(0,U.jsxs)(`label`,{className:`duration-field`,children:[(0,U.jsx)(`span`,{children:`Default description`}),(0,U.jsx)(`input`,{value:p,onChange:e=>m(e.target.value),placeholder:`Verified by Acme`})]})]}),(0,U.jsxs)(`label`,{className:`checkline`,children:[(0,U.jsx)(`input`,{type:`checkbox`,checked:h,onChange:e=>_(e.target.checked)}),`The verifier may replace the description per peer`]}),(0,U.jsx)(`p`,{className:`bot-create-note`,children:`This is botVerifierSettings.can_modify_custom_description: with it off, every mark this verifier grants carries the default description above, whatever the applicant asked for.`}),v.length===0&&(0,U.jsx)(q,{children:`The catalogue has no active icon, so there is nothing to grant. Add one in the icon catalogue first.`}),(0,U.jsxs)(`div`,{className:`bot-create-actions`,children:[(0,U.jsx)(`span`,{className:`bot-create-note`,children:`The bot can mark peers as soon as the row exists and is enabled.`}),(0,U.jsx)(Z,{label:s?`Update verifier`:`Grant verifier status`,icon:(0,U.jsx)(ct,{size:15}),tone:`neutral`,path:`/api/actions/grant-bot-verifier`,payload:S,onDone:()=>{x(),r()}})]})]}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(K,{title:`Verifier bots`,text:`Bots allowed to hand out their own mark. Verifier status is granted per deployment, so every row here is a badge printer an operator switched on by hand.`,action:(0,U.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:r,children:[(0,U.jsx)(ft,{size:15}),` `,`Refresh`]})}),(0,U.jsx)(`div`,{className:`table-wrap`,children:(0,U.jsxs)(`table`,{className:`data-table`,children:[(0,U.jsx)(`thead`,{children:(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`th`,{children:`Bot`}),(0,U.jsx)(`th`,{children:`Company`}),(0,U.jsx)(`th`,{children:`Icon`}),(0,U.jsx)(`th`,{children:`Own description`}),(0,U.jsx)(`th`,{children:`Status`}),(0,U.jsx)(`th`,{children:`Marks`}),(0,U.jsx)(`th`,{children:`Granted by`}),(0,U.jsx)(`th`,{children:`Updated`}),n&&(0,U.jsx)(`th`,{})]})}),(0,U.jsxs)(`tbody`,{children:[e.map(e=>(0,U.jsxs)(`tr`,{children:[(0,U.jsxs)(`td`,{children:[(0,U.jsx)(`button`,{className:`row-link`,type:`button`,onClick:()=>i(`/bots/${e.BotID}`),children:(0,U.jsx)(`strong`,{children:W(e.BotUsername)||e.BotName||e.BotID})}),(0,U.jsx)(`div`,{className:`entity-subtitle mono`,children:e.BotID})]}),(0,U.jsxs)(`td`,{children:[(0,U.jsx)(`strong`,{children:e.CompanyName||`-`}),(0,U.jsx)(`div`,{className:`entity-subtitle truncate`,children:e.DefaultDescription||`Not set`})]}),(0,U.jsxs)(`td`,{children:[e.IconName||`-`,(0,U.jsx)(`div`,{className:`entity-subtitle mono`,children:e.IconDocumentID})]}),(0,U.jsx)(`td`,{children:e.CanModifyCustomDescription?`Yes`:`No`}),(0,U.jsx)(`td`,{children:e.Enabled?(0,U.jsx)(J,{tone:`good`,children:`Enabled`}):(0,U.jsx)(J,{tone:`warn`,children:`disabled`})}),(0,U.jsx)(`td`,{className:`mono`,children:String(e.MarkCount??`0`)}),(0,U.jsxs)(`td`,{children:[e.GrantedBy||`-`,(0,U.jsx)(`div`,{className:`entity-subtitle truncate`,children:e.GrantReason||`-`})]}),(0,U.jsx)(`td`,{children:G(e.UpdatedAt)||`-`}),n&&(0,U.jsx)(`td`,{children:(0,U.jsxs)(`div`,{className:`row-actions`,children:[(0,U.jsx)(`button`,{className:`btn compact-btn`,type:`button`,onClick:()=>b(e),children:`Edit`}),(0,U.jsx)(Z,{label:e.Enabled?`Disable`:`Enable`,icon:e.Enabled?(0,U.jsx)(lt,{size:14}):(0,U.jsx)(ut,{size:14}),tone:e.Enabled?`warn`:`neutral`,compact:!0,path:`/api/actions/set-bot-verifier-enabled`,payload:()=>({bot_id:e.BotID,enabled:!e.Enabled}),onDone:r}),(0,U.jsx)(Z,{label:`Revoke status`,icon:(0,U.jsx)(Ot,{size:14}),tone:`danger`,compact:!0,path:`/api/actions/revoke-bot-verifier`,payload:()=>({bot_id:e.BotID}),onDone:r})]})})]},e.BotID)),e.length===0&&(0,U.jsx)(Yn,{colSpan:n?9:8})]})]})}),(0,U.jsx)(`p`,{className:`bot-create-note`,children:`Disabling is the per-verifier kill switch: the marks already granted keep rendering, but the bot can no longer mark anything new and its settings stop being projected into botInfo.`}),(0,U.jsx)(`p`,{className:`bot-create-note`,children:`Revoking verifier status removes the row and every mark this verifier granted — the icon disappears from all of its peers at once.`})]})]})}function $a({icons:e,verifiers:t,canManage:n,onChanged:r}){let[i,a]=(0,g.useState)(``),[o,s]=(0,g.useState)(``),[c,l]=(0,g.useState)(``);function u(){let e={document_id:i.trim()||`0`,name:o.trim()};return c&&(e.owner_bot_id=c),e}return(0,U.jsxs)(U.Fragment,{children:[n&&(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(K,{title:`Add or rename an icon`,text:`Search and pick any custom-emoji document already on this deployment (including bundled/system ones). Adding an id that already exists renames it instead of duplicating it.`}),(0,U.jsx)(Ga,{label:`Document`,value:i,onChange:a}),(0,U.jsxs)(`div`,{className:`bot-create-fields`,children:[(0,U.jsxs)(`label`,{className:`duration-field`,children:[(0,U.jsx)(`span`,{children:`Name`}),(0,U.jsx)(`input`,{value:o,onChange:e=>s(e.target.value),placeholder:`Acme blue tick`})]}),(0,U.jsxs)(`label`,{className:`duration-field`,children:[(0,U.jsx)(`span`,{children:`Owner`}),(0,U.jsxs)(`select`,{value:c,onChange:e=>l(e.target.value),children:[(0,U.jsx)(`option`,{value:``,children:`Shared`}),t.map(e=>(0,U.jsx)(`option`,{value:e.BotID,children:`${e.CompanyName||e.BotID} · ${W(e.BotUsername)||e.BotID}`},e.BotID))]})]})]}),(0,U.jsx)(`p`,{className:`bot-create-note`,children:`A document id that resolves to nothing produces an invisible badge: the peer is marked in the database and the client draws nothing.`}),(0,U.jsx)(`p`,{className:`bot-create-note`,children:`A shared icon may be granted to any verifier; picking an owner reserves it for that one bot.`}),(0,U.jsxs)(`div`,{className:`bot-create-actions`,children:[(0,U.jsx)(`span`,{className:`bot-create-note`,children:`Adding an icon grants nothing by itself — it only makes the document available to grant.`}),(0,U.jsx)(Z,{label:`Save icon`,icon:(0,U.jsx)(ct,{size:15}),tone:`neutral`,path:`/api/actions/upsert-verification-icon`,payload:u,onDone:()=>{a(``),s(``),l(``),r()}})]})]}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(K,{title:`Icon catalogue`,text:`The custom emoji documents a verifier may mark with. Nothing else can be used as an icon, so the catalogue is where a wrong badge is prevented rather than fixed.`,action:(0,U.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:r,children:[(0,U.jsx)(ft,{size:15}),` `,`Refresh`]})}),(0,U.jsx)(`div`,{className:`table-wrap`,children:(0,U.jsxs)(`table`,{className:`data-table`,children:[(0,U.jsx)(`thead`,{children:(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`th`,{children:`Document ID`}),(0,U.jsx)(`th`,{children:`Name`}),(0,U.jsx)(`th`,{children:`Owner`}),(0,U.jsx)(`th`,{children:`Status`}),(0,U.jsx)(`th`,{children:`Verifiers using it`}),(0,U.jsx)(`th`,{children:`Filed`}),n&&(0,U.jsx)(`th`,{})]})}),(0,U.jsxs)(`tbody`,{children:[e.map(e=>(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`td`,{className:`mono`,children:e.DocumentID}),(0,U.jsx)(`td`,{children:(0,U.jsx)(`strong`,{children:e.Name||`-`})}),(0,U.jsx)(`td`,{children:e.OwnerBotID&&e.OwnerBotID!==`0`?(0,U.jsxs)(U.Fragment,{children:[W(e.OwnerBotUsername)||e.OwnerBotID,(0,U.jsx)(`div`,{className:`entity-subtitle mono`,children:e.OwnerBotID})]}):(0,U.jsx)(J,{children:`Shared`})}),(0,U.jsx)(`td`,{children:e.Active?(0,U.jsx)(J,{tone:`good`,children:`Active`}):(0,U.jsx)(J,{tone:`warn`,children:`Retired`})}),(0,U.jsx)(`td`,{className:`mono`,children:String(e.UsedByVerifiers??`0`)}),(0,U.jsx)(`td`,{children:G(e.CreatedAt)||`-`}),n&&(0,U.jsx)(`td`,{children:(0,U.jsx)(`div`,{className:`row-actions`,children:(0,U.jsx)(Z,{label:e.Active?`Retire`:`Activate`,icon:e.Active?(0,U.jsx)(lt,{size:14}):(0,U.jsx)(ut,{size:14}),tone:e.Active?`warn`:`neutral`,compact:!0,path:`/api/actions/set-verification-icon-active`,payload:()=>({icon_id:e.ID,active:!e.Active}),onDone:r})})})]},e.ID)),e.length===0&&(0,U.jsx)(Yn,{colSpan:n?7:6})]})]})}),(0,U.jsx)(`p`,{className:`bot-create-note`,children:`Retiring an icon stops it from being granted to anybody new. Marks already carrying it keep it: the icon is copied onto the mark when it is granted.`})]})]})}function eo({verifiers:e,canManage:t,navigate:n}){let[r,i]=(0,g.useState)(``),[a,o]=(0,g.useState)(`all`),[s,c]=(0,g.useState)(``),[l,u]=(0,g.useState)(`50`),[d,f]=(0,g.useState)([]),[p,m]=(0,g.useState)(!1),[h,_]=(0,g.useState)(``),[v,y]=(0,g.useState)(!1),[b,x]=(0,g.useState)(``);async function S(e=!1){y(!0),x(``);let t=new URLSearchParams({limit:l});r&&t.set(`verifier_bot_id`,r),a!==`all`&&t.set(`peer_type`,a),s.trim()&&t.set(`q`,s.trim().replace(/^@/,``)),e&&h&&t.set(`before_id`,h);try{let n=await k.customVerifications(t),r=n.rows??[];f(t=>e?[...t,...r]:r),_(n.next_before_id??``),m(!!n.has_more)}catch(e){x(O(e))}finally{y(!1)}}return(0,g.useEffect)(()=>{S(!1)},[]),(0,U.jsxs)(U.Fragment,{children:[(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(K,{title:`Granted marks`,text:`Every peer currently carrying a third-party mark, whoever granted it — an operator decision, the verifier bot itself, or the peer's owner through bots.setCustomVerification.`,action:(0,U.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>S(!1),disabled:v,children:[(0,U.jsx)(ft,{size:15,className:v?`spin`:``}),` `,`Refresh`]})}),b&&(0,U.jsx)(q,{children:b})]}),(0,U.jsx)(Kn,{children:(0,U.jsxs)(`form`,{className:`toolbar`,onSubmit:e=>{e.preventDefault(),S(!1)},children:[(0,U.jsxs)(`label`,{className:`searchbox`,children:[(0,U.jsx)(V,{size:15}),(0,U.jsx)(`input`,{value:s,onChange:e=>c(e.target.value),placeholder:`Peer id, username or title`})]}),(0,U.jsxs)(`label`,{className:`field-inline`,children:[(0,U.jsx)(`span`,{children:`Verifier`}),(0,U.jsx)(to,{value:r,verifiers:e,onChange:i})]}),(0,U.jsxs)(`label`,{className:`field-inline`,children:[(0,U.jsx)(`span`,{children:`Peer type`}),(0,U.jsxs)(`select`,{value:a,onChange:e=>o(e.target.value),children:[(0,U.jsx)(`option`,{value:`all`,children:`All types`}),qa.map(e=>(0,U.jsx)(`option`,{value:e,children:Ya[e]},e))]})]}),(0,U.jsxs)(`label`,{className:`field-inline`,children:[(0,U.jsx)(`span`,{children:`Limit`}),(0,U.jsx)(`input`,{className:`small-input`,value:l,onChange:e=>u(e.target.value),type:`number`,min:`1`,max:`200`})]}),(0,U.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,disabled:v,children:[v?(0,U.jsx)(I,{size:15,className:`spin`}):(0,U.jsx)(V,{size:15}),` `,`Search`]})]})}),(0,U.jsx)(`div`,{className:`table-wrap`,children:(0,U.jsxs)(`table`,{className:`data-table`,children:[(0,U.jsx)(`thead`,{children:(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`th`,{children:`ID`}),(0,U.jsx)(`th`,{children:`Verifier`}),(0,U.jsx)(`th`,{children:`Peer`}),(0,U.jsx)(`th`,{children:`Description`}),(0,U.jsx)(`th`,{children:`Icon`}),(0,U.jsx)(`th`,{children:`Filed`}),t&&(0,U.jsx)(`th`,{})]})}),(0,U.jsxs)(`tbody`,{children:[d.map(e=>(0,U.jsxs)(`tr`,{children:[(0,U.jsxs)(`td`,{className:`mono`,children:[`#`,e.ID]}),(0,U.jsxs)(`td`,{children:[(0,U.jsx)(`strong`,{children:e.CompanyName||W(e.VerifierBotUsername)||e.VerifierBotID}),(0,U.jsx)(`div`,{className:`entity-subtitle mono`,children:W(e.VerifierBotUsername)||e.VerifierBotID})]}),(0,U.jsxs)(`td`,{children:[(0,U.jsx)(`button`,{className:`row-link`,type:`button`,onClick:()=>n(oo(e.PeerType,e.PeerID)),children:(0,U.jsx)(`strong`,{children:ao(e)})}),(0,U.jsxs)(`div`,{className:`entity-subtitle mono`,children:[Ya[e.PeerType],` · `,e.PeerID]})]}),(0,U.jsx)(`td`,{className:`truncate`,children:e.Description||`Not set`}),(0,U.jsx)(`td`,{className:`mono`,children:e.IconDocumentID}),(0,U.jsx)(`td`,{children:G(e.CreatedAt)||`-`}),t&&(0,U.jsx)(`td`,{children:(0,U.jsx)(`div`,{className:`row-actions`,children:(0,U.jsx)(Z,{label:`Remove mark`,icon:(0,U.jsx)(ve,{size:14}),tone:`danger`,compact:!0,path:`/api/actions/revoke-custom-verification`,payload:()=>({verifier_bot_id:e.VerifierBotID,peer_type:e.PeerType,peer_id:e.PeerID}),onDone:()=>S(!1)})})})]},e.ID)),d.length===0&&(0,U.jsx)(Yn,{colSpan:t?7:6})]})]})}),(0,U.jsx)(`p`,{className:`bot-create-note`,children:`Removing a mark clears the icon and the description from the peer. The application it came from keeps its history.`}),p&&(0,U.jsx)(`div`,{className:`toolbar`,children:(0,U.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>S(!0),disabled:v,children:[v?(0,U.jsx)(I,{size:15,className:`spin`}):(0,U.jsx)(Ce,{size:15}),` `,`Load more`]})})]})}function to({value:e,verifiers:t,onChange:n}){return(0,U.jsxs)(`select`,{value:e,onChange:e=>n(e.target.value),children:[(0,U.jsx)(`option`,{value:``,children:`All verifiers`}),t.map(e=>(0,U.jsx)(`option`,{value:e.BotID,children:`${e.CompanyName||e.BotID} · ${W(e.BotUsername)||e.BotID}`+(e.Enabled?``:` (disabled)`)},e.BotID))]})}function no({status:e}){return(0,U.jsx)(J,{tone:ro(e),children:Ja[e]})}function ro(e){return e===`approved`?`good`:e===`pending`?`warn`:e===`rejected`?`danger`:`neutral`}function io(e,t){return e===`pending`?t!==`0`&&t!==``?`warn`:`neutral`:e===`approved`?`good`:`neutral`}function ao(e){return W(e.PeerUsername)||e.PeerTitle||`#${e.PeerID}`}function oo(e,t){return e===`channel`?`/channels/${t}`:`/accounts/${t}`}function so({id:e,navigate:t}){let[n,r]=(0,g.useState)(null),[i,a]=(0,g.useState)(``),[o,s]=(0,g.useState)(!1),[c,l]=(0,g.useState)(!1),[u,d]=(0,g.useState)(``);async function f(){l(!0),d(``);try{r(await k.customVerificationRequest(e))}catch(e){d(O(e))}finally{l(!1)}}function p(){s(!1),f()}(0,g.useEffect)(()=>{f()},[e]);function m(e){if(e instanceof v&&e.status===409)return s(!0),f(),`Another admin has already changed this application. The data has been reloaded — check the status before deciding again.`}if(u&&!n)return(0,U.jsx)(q,{children:u});if(!n)return(0,U.jsx)(Zn,{label:`Loading the application…`});let h=n.request,_=lo(n.verifier),y=n.mark_active,b=h.Status===`pending`,x=h.Status===`approved`,S=i.trim(),C=h.RequestedDescription.trim(),w=!!_?.CanModifyCustomDescription&&C!==``,T=w?C:(_?.DefaultDescription??``).trim();function E(){let e={version:h.Version};return S&&(e.internal_note=S),e}function D(){a(``),s(!1),f()}return(0,U.jsxs)(Gn,{title:`Application #${h.ID}`,eyebrow:`Third-party verification / Review`,actions:(0,U.jsxs)(U.Fragment,{children:[(0,U.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>t(`/bot-verification`),children:[(0,U.jsx)(pe,{size:15}),` `,`Back to list`]}),(0,U.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:p,disabled:c,children:[(0,U.jsx)(ft,{size:15,className:c?`spin`:``}),` `,`Refresh`]})]}),children:[u&&(0,U.jsx)(q,{children:u}),o&&(0,U.jsx)(q,{children:`Another admin has already changed this application. The data has been reloaded — check the status before deciding again.`}),(0,U.jsx)(qn,{main:(0,U.jsxs)(`div`,{className:`stacked-sections`,children:[(0,U.jsxs)(`section`,{className:`entity-head`,children:[(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`div`,{className:`entity-title`,children:ao(h)}),(0,U.jsxs)(`div`,{className:`entity-subtitle mono`,children:[`#`,h.ID,` · `,Ya[h.PeerType],`:`,h.PeerID,` · v`,h.Version]})]}),(0,U.jsxs)(`div`,{className:`entity-badges`,children:[(0,U.jsx)(no,{status:h.Status}),y?(0,U.jsxs)(J,{tone:`good`,children:[(0,U.jsx)(ee,{size:12}),` `,`Mark is live`]}):(0,U.jsx)(J,{tone:`neutral`,children:`No mark on the peer`})]})]}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(K,{title:`A verifier company's icon — not the official checkmark`,text:`A third-party mark is a verifier bot's own icon, drawn right BEFORE the name of an account, a bot or a channel, plus one line of description in the profile. It says “this verifier vouches for this peer”, and nothing more.`}),(0,U.jsx)(`p`,{className:`bot-create-note`,children:`The icon is a custom emoji document. The client fetches it through messages.getCustomEmojiDocuments, so a document id that resolves to nothing renders as no badge at all — which is why marks are granted from the catalogue below rather than from a typed number.`})]}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(K,{title:`Verifier`,text:`The company whose icon the peer would carry, as its row stands right now.`,action:(0,U.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>t(`/bots/${h.VerifierBotID}`),children:[(0,U.jsx)(be,{size:15}),` `,`Open verifier bot`]})}),(0,U.jsxs)(`div`,{className:`summary-grid`,children:[(0,U.jsx)(X,{label:`Company`,value:_?.CompanyName||`-`}),(0,U.jsx)(X,{label:`Bot`,value:W(h.VerifierBotUsername)||`-`}),(0,U.jsx)(X,{label:`Verifier bot ID`,value:h.VerifierBotID,mono:!0}),(0,U.jsx)(X,{label:`Document ID`,value:_?.IconDocumentID||`-`,mono:!0}),(0,U.jsx)(X,{label:`Name`,value:_?.IconName||`-`}),(0,U.jsx)(X,{label:`Own description`,value:_?.CanModifyCustomDescription?`Yes`:`No`})]}),(0,U.jsx)(co,{label:`Default description`,children:_?.DefaultDescription?(0,U.jsx)(`p`,{className:`about-text`,children:_.DefaultDescription}):(0,U.jsx)(`p`,{className:`bot-create-note`,children:`Not set`})}),!_&&(0,U.jsx)(q,{children:`The verifier row is gone: its status was revoked after this application was filed. There is no icon to grant, so the application can only be rejected.`}),_&&!_.Enabled&&(0,U.jsx)(q,{children:`This verifier is disabled. It cannot mark anything new until an operator enables it again.`})]}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(K,{title:`Peer`,text:`The account, bot or channel the icon would be attached to.`,action:(0,U.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>t(oo(h.PeerType,h.PeerID)),children:[(0,U.jsx)(je,{size:15}),` `,`Open peer`]})}),(0,U.jsxs)(`div`,{className:`summary-grid`,children:[(0,U.jsx)(X,{label:`Type`,value:Ya[h.PeerType]}),(0,U.jsx)(X,{label:`Username`,value:W(h.PeerUsername)||`-`}),(0,U.jsx)(X,{label:`Title`,value:h.PeerTitle||`-`}),(0,U.jsx)(X,{label:`Peer ID`,value:h.PeerID,mono:!0})]})]}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(K,{title:`Applicant`,text:`Who filed the application with the verifier bot.`,action:(0,U.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>t(`/accounts/${h.ApplicantUserID}`),children:[(0,U.jsx)(Nt,{size:15}),` `,`Open account`]})}),(0,U.jsxs)(`div`,{className:`summary-grid`,children:[(0,U.jsx)(X,{label:`Username`,value:W(h.ApplicantUsername)||`-`}),(0,U.jsx)(X,{label:`User ID`,value:h.ApplicantUserID,mono:!0}),(0,U.jsx)(X,{label:`Filed`,value:G(h.CreatedAt)||`-`}),(0,U.jsx)(X,{label:`Updated`,value:G(h.UpdatedAt)||`-`})]})]}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(K,{title:`Application`,text:`What the applicant wrote, rendered as plain text.`}),(0,U.jsxs)(`div`,{className:`stacked-sections`,children:[(0,U.jsxs)(`div`,{className:`summary-grid`,children:[(0,U.jsx)(X,{label:`Correlation ID`,value:h.CorrelationID||`-`,mono:!0}),(0,U.jsx)(X,{label:`Status`,value:Ja[h.Status]})]}),(0,U.jsx)(co,{label:`Stated reason`,children:h.Reason?(0,U.jsx)(`p`,{className:`about-text`,children:h.Reason}):(0,U.jsx)(`p`,{className:`bot-create-note`,children:`Not set`})}),(0,U.jsx)(co,{label:`Requested description`,children:C?(0,U.jsx)(`p`,{className:`about-text`,children:C}):(0,U.jsx)(`p`,{className:`bot-create-note`,children:`Not set`})}),(0,U.jsx)(co,{label:`Description the mark would carry`,children:T?(0,U.jsx)(`p`,{className:`about-text`,children:T}):(0,U.jsx)(`p`,{className:`bot-create-note`,children:`Not set`})}),(0,U.jsx)(`p`,{className:`bot-create-note`,children:`Resolved the same way the backend resolves it: the applicant's wording only when this verifier may set its own description, otherwise the verifier's default.`}),C!==``&&!w&&(0,U.jsx)(`p`,{className:`bot-create-note`,children:`This verifier may not set a per-peer description, so the requested wording is ignored and the default is applied.`})]})]}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(K,{title:`Decision`,text:`What was decided, by whom, and with which wording.`}),(0,U.jsxs)(`div`,{className:`stacked-sections`,children:[(0,U.jsxs)(`div`,{className:`summary-grid`,children:[(0,U.jsx)(X,{label:`Decided by`,value:h.DecidedBy||`-`}),(0,U.jsx)(X,{label:`Approved`,value:G(h.ApprovedAt)||`-`}),(0,U.jsx)(X,{label:`Rejected`,value:G(h.RejectedAt)||`-`}),(0,U.jsx)(X,{label:`Version (optimistic lock)`,value:h.Version,mono:!0})]}),(0,U.jsx)(co,{label:`Decision reason`,children:h.DecisionReason?(0,U.jsx)(`p`,{className:`about-text`,children:h.DecisionReason}):(0,U.jsx)(`p`,{className:`bot-create-note`,children:`No decision yet`})}),(0,U.jsx)(co,{label:`Internal note · admins only`,children:h.InternalNote?(0,U.jsx)(`p`,{className:`about-text`,children:h.InternalNote}):(0,U.jsx)(`p`,{className:`bot-create-note`,children:`Not set`})})]})]})]}),side:(0,U.jsxs)(`section`,{className:`action-dock`,children:[(0,U.jsxs)(`div`,{className:`dock-title`,children:[(0,U.jsx)(Tt,{size:14}),` `,`Decision`]}),!b&&!x&&(0,U.jsx)(`p`,{className:`bot-create-note`,children:`This status has no available actions.`}),(b||x)&&(0,U.jsxs)(U.Fragment,{children:[(0,U.jsxs)(`label`,{className:`duration-field`,children:[(0,U.jsx)(`span`,{children:`Internal note`}),(0,U.jsx)(`textarea`,{value:i,onChange:e=>a(e.target.value),rows:3,placeholder:`Handover note for other admins`})]}),(0,U.jsx)(`p`,{className:`bot-create-note`,children:`Optional. Stored with the decision and visible to admins only — never sent to the applicant.`})]}),b&&(0,U.jsxs)(U.Fragment,{children:[!_&&(0,U.jsx)(q,{children:`The verifier row is gone: its status was revoked after this application was filed. There is no icon to grant, so the application can only be rejected.`}),_&&!_.Enabled&&(0,U.jsx)(q,{children:`This verifier is disabled. It cannot mark anything new until an operator enables it again.`}),y&&(0,U.jsx)(`p`,{className:`bot-create-note`,children:`This peer already carries this verifier's mark; approving refreshes the description and records the decision.`}),(0,U.jsxs)(`div`,{className:`action-stack`,children:[(0,U.jsx)(Z,{label:`Approve`,icon:(0,U.jsx)(F,{size:15}),tone:`neutral`,path:`/api/botverification/requests/${h.ID}/approve`,payload:E,onDone:D,onError:m}),(0,U.jsx)(Z,{label:`Reject`,icon:(0,U.jsx)(ne,{size:15}),tone:`warn`,path:`/api/botverification/requests/${h.ID}/reject`,payload:E,onDone:D,onError:m})]}),(0,U.jsx)(`p`,{className:`bot-create-note`,children:`Puts the verifier's icon before the peer's name and its description in the profile, and messages the applicant.`}),(0,U.jsx)(`p`,{className:`bot-create-note`,children:`The reason is mandatory: it is the wording the applicant is told, so write what exactly was missing.`})]}),x&&(0,U.jsxs)(U.Fragment,{children:[(0,U.jsxs)(`div`,{className:`dock-title`,children:[(0,U.jsx)(St,{size:14}),` `,`Danger zone`]}),(0,U.jsxs)(`div`,{className:`danger-zone`,children:[(0,U.jsx)(Z,{label:`Revoke mark`,icon:(0,U.jsx)(ve,{size:15}),tone:`danger`,path:`/api/botverification/requests/${h.ID}/revoke`,payload:E,onDone:D,onError:m}),(0,U.jsx)(`p`,{className:`bot-create-note`,children:`Takes the icon and the description off the peer and closes the application as revoked. The official checkmark, if the peer has one, is untouched.`}),!y&&(0,U.jsx)(`p`,{className:`bot-create-note`,children:`The peer carries no mark right now — revoking only closes the application.`})]})]})]})})]})}function co({label:e,children:t}){return(0,U.jsxs)(`div`,{className:`duration-field`,children:[(0,U.jsx)(`span`,{children:e}),t]})}function lo(e){return!e||!e.BotID||e.BotID===`0`?null:e}var uo=[`draft`,`submitted`,`in_review`,`approved`,`rejected`,`cancelled`],fo=[`bot`,`channel`,`supergroup`,`user`],po={draft:`Draft`,submitted:`Submitted`,in_review:`In review`,approved:`Approved`,rejected:`Rejected`,cancelled:`Cancelled`},mo={bot:`Bot`,channel:`Channel`,supergroup:`Supergroup`,user:`User`};function ho({navigate:e}){let[t,n]=(0,g.useState)(`all`),[r,i]=(0,g.useState)(`all`),[a,o]=(0,g.useState)(``),[s,c]=(0,g.useState)(``),[l,u]=(0,g.useState)(`50`),[d,f]=(0,g.useState)([]),[p,m]=(0,g.useState)({}),[h,_]=(0,g.useState)(!1),[v,y]=(0,g.useState)(``),[b,x]=(0,g.useState)(!1),[S,C]=(0,g.useState)(``);async function w(e=!1){x(!0),C(``);let n=new URLSearchParams({limit:l});t!==`all`&&n.set(`status`,t),r!==`all`&&n.set(`target_type`,r),a.trim()&&n.set(`reviewer`,a.trim()),s.trim()&&n.set(`q`,s.trim().replace(/^@/,``)),e&&v&&n.set(`before_id`,v);try{let t=await k.verificationApplications(n),r=t.rows??[];f(t=>e?[...t,...r]:r),y(t.next_before_id??``),_(!!t.has_more)}catch(e){C(O(e))}finally{x(!1)}}async function T(){try{m((await k.verificationCounts()).counts??{})}catch(e){C(O(e))}}(0,g.useEffect)(()=>{w(!1),T()},[]);function E(){w(!1),T()}return(0,U.jsxs)(Gn,{title:`Verification queue`,eyebrow:`Verification / Queue`,actions:(0,U.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:E,disabled:b,children:[(0,U.jsx)(ft,{size:15,className:b?`spin`:``}),` `,`Refresh`]}),children:[S&&(0,U.jsx)(q,{children:S}),(0,U.jsx)(`div`,{className:`metric-row`,children:uo.map(e=>(0,U.jsx)(Y,{label:po[e],value:p[e]??`0`,mono:!0,tone:vo(e,p[e]??`0`)},e))}),(0,U.jsx)(Kn,{children:(0,U.jsxs)(`form`,{className:`toolbar`,onSubmit:e=>{e.preventDefault(),w(!1)},children:[(0,U.jsxs)(`label`,{className:`searchbox`,children:[(0,U.jsx)(V,{size:15}),(0,U.jsx)(`input`,{value:s,onChange:e=>c(e.target.value),placeholder:`Application id, peer id, username or title`})]}),(0,U.jsxs)(`label`,{className:`field-inline`,children:[(0,U.jsx)(`span`,{children:`Status`}),(0,U.jsxs)(`select`,{value:t,onChange:e=>n(e.target.value),children:[(0,U.jsx)(`option`,{value:`all`,children:`All statuses`}),uo.map(e=>(0,U.jsx)(`option`,{value:e,children:po[e]},e))]})]}),(0,U.jsxs)(`label`,{className:`field-inline`,children:[(0,U.jsx)(`span`,{children:`Target type`}),(0,U.jsxs)(`select`,{value:r,onChange:e=>i(e.target.value),children:[(0,U.jsx)(`option`,{value:`all`,children:`All types`}),fo.map(e=>(0,U.jsx)(`option`,{value:e,children:mo[e]},e))]})]}),(0,U.jsxs)(`label`,{className:`field-inline`,children:[(0,U.jsx)(`span`,{children:`Reviewer`}),(0,U.jsx)(`input`,{value:a,onChange:e=>o(e.target.value),placeholder:`Any reviewer`})]}),(0,U.jsxs)(`label`,{className:`field-inline`,children:[(0,U.jsx)(`span`,{children:`Limit`}),(0,U.jsx)(`input`,{className:`small-input`,value:l,onChange:e=>u(e.target.value),type:`number`,min:`1`,max:`200`})]}),(0,U.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,disabled:b,children:[b?(0,U.jsx)(I,{size:15,className:`spin`}):(0,U.jsx)(V,{size:15}),` `,`Search`]})]})}),(0,U.jsx)(`div`,{className:`table-wrap`,children:(0,U.jsxs)(`table`,{className:`data-table`,children:[(0,U.jsx)(`thead`,{children:(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`th`,{children:`ID`}),(0,U.jsx)(`th`,{children:`Target`}),(0,U.jsx)(`th`,{children:`Applicant`}),(0,U.jsx)(`th`,{children:`Category`}),(0,U.jsx)(`th`,{children:`Status`}),(0,U.jsx)(`th`,{children:`Submitted`}),(0,U.jsx)(`th`,{children:`Reviewer`}),(0,U.jsx)(`th`,{})]})}),(0,U.jsxs)(`tbody`,{children:[d.map(t=>(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`td`,{className:`mono`,children:(0,U.jsxs)(`button`,{className:`row-link`,type:`button`,onClick:()=>e(`/verification/${t.ID}`),children:[`#`,t.ID]})}),(0,U.jsxs)(`td`,{children:[(0,U.jsx)(`strong`,{children:yo(t)}),(0,U.jsxs)(`div`,{className:`entity-subtitle mono`,children:[mo[t.TargetType],` · `,t.TargetID]}),t.TargetVerified&&(0,U.jsxs)(J,{tone:`good`,children:[(0,U.jsx)(ee,{size:12}),` `,`Badge already on`]})]}),(0,U.jsxs)(`td`,{children:[W(t.ApplicantUsername)||t.ApplicantName||`-`,(0,U.jsx)(`div`,{className:`entity-subtitle mono`,children:t.ApplicantUserID})]}),(0,U.jsx)(`td`,{children:t.Category||`-`}),(0,U.jsx)(`td`,{children:(0,U.jsx)(go,{status:t.Status})}),(0,U.jsx)(`td`,{children:G(t.SubmittedAt)||`-`}),(0,U.jsx)(`td`,{children:t.ReviewerAdminID||`-`}),(0,U.jsx)(`td`,{children:(0,U.jsxs)(`button`,{className:`row-link`,type:`button`,onClick:()=>e(`/verification/${t.ID}`),children:[(0,U.jsx)(xt,{size:14}),` `,`Details`,` `,(0,U.jsx)(z,{size:14})]})})]},t.ID)),d.length===0&&(0,U.jsx)(Yn,{colSpan:8})]})]})}),h&&(0,U.jsx)(`div`,{className:`toolbar`,children:(0,U.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>w(!0),disabled:b,children:[b?(0,U.jsx)(I,{size:15,className:`spin`}):(0,U.jsx)(Ce,{size:15}),` `,`Load more`]})})]})}function go({status:e}){return(0,U.jsx)(J,{tone:_o(e),children:po[e]})}function _o(e){return e===`approved`?`good`:e===`submitted`||e===`in_review`?`warn`:e===`rejected`?`danger`:`neutral`}function vo(e,t){return e===`submitted`||e===`in_review`?t!==`0`&&t!==``?`warn`:`neutral`:e===`approved`?`good`:`neutral`}function yo(e){return W(e.TargetUsername)||e.TargetTitle||`#${e.TargetID}`}function bo(e){return e.TargetType===`bot`?`/bots/${e.TargetID}`:e.TargetType===`user`?`/accounts/${e.TargetID}`:`/channels/${e.TargetID}`}var xo={created:`Created`,updated:`Updated`,submitted:`Submitted`,claimed:`Claimed`,approved:`Approved`,rejected:`Rejected`,cancelled:`Cancelled`,revoked:`Badge revoked`,notified:`Applicant notified`};function So({id:e,navigate:t}){let{can:n}=cn(),[r,i]=(0,g.useState)(null),[a,o]=(0,g.useState)(``),[s,c]=(0,g.useState)(!1),[l,u]=(0,g.useState)(!1),[d,f]=(0,g.useState)(``);async function p(){u(!0),f(``);try{i(await k.verificationApplication(e))}catch(e){f(O(e))}finally{u(!1)}}function m(){c(!1),p()}(0,g.useEffect)(()=>{p()},[e]);function h(e){if(e instanceof v&&e.status===409)return c(!0),p(),`Another admin has already changed this application. The data has been reloaded — check the status before deciding again.`}if(d&&!r)return(0,U.jsx)(q,{children:d});if(!r)return(0,U.jsx)(Zn,{label:`Loading the application…`});let _=r.application,y=r.events??[],b=r.applicant_controls_target,x=r.target_verified,S=_.Status===`submitted`,C=_.Status===`submitted`||_.Status===`in_review`,w=_.Status===`approved`&&n(`verification.revoke`),T=a.trim();function E(){let e={version:_.Version};return T&&(e.internal_note=T),e}function D(){o(``),c(!1),p()}return(0,U.jsxs)(Gn,{title:`Application #${_.ID}`,eyebrow:`Verification / Review`,actions:(0,U.jsxs)(U.Fragment,{children:[(0,U.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>t(`/verification`),children:[(0,U.jsx)(pe,{size:15}),` `,`Back to list`]}),(0,U.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:m,disabled:l,children:[(0,U.jsx)(ft,{size:15,className:l?`spin`:``}),` `,`Refresh`]})]}),children:[d&&(0,U.jsx)(q,{children:d}),s&&(0,U.jsx)(q,{children:`Another admin has already changed this application. The data has been reloaded — check the status before deciding again.`}),(0,U.jsx)(qn,{main:(0,U.jsxs)(`div`,{className:`stacked-sections`,children:[(0,U.jsxs)(`section`,{className:`entity-head`,children:[(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`div`,{className:`entity-title`,children:yo(_)}),(0,U.jsxs)(`div`,{className:`entity-subtitle mono`,children:[`#`,_.ID,` · `,mo[_.TargetType],`:`,_.TargetID,` · v`,_.Version]})]}),(0,U.jsxs)(`div`,{className:`entity-badges`,children:[(0,U.jsx)(go,{status:_.Status}),x&&(0,U.jsxs)(J,{tone:`good`,children:[(0,U.jsx)(ee,{size:12}),` `,`Badge already on`]}),(0,U.jsx)(J,{tone:b?`good`:`danger`,children:b?`Control confirmed`:`No control over the target`})]})]}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(K,{title:`Target`,text:`The peer the badge would be attached to, as it exists right now.`,action:(0,U.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>t(bo(_)),children:[(0,U.jsx)(je,{size:15}),` `,`Open target`]})}),(0,U.jsxs)(`div`,{className:`summary-grid`,children:[(0,U.jsx)(X,{label:`Type`,value:mo[_.TargetType]}),(0,U.jsx)(X,{label:`Username`,value:W(_.TargetUsername)||`-`}),(0,U.jsx)(X,{label:`Title`,value:_.TargetTitle||`-`}),(0,U.jsx)(X,{label:`Peer ID`,value:_.TargetID,mono:!0})]})]}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(K,{title:`Applicant`,text:`Who filed the application and whether they still hold rights on the target.`,action:(0,U.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>t(`/accounts/${_.ApplicantUserID}`),children:[(0,U.jsx)(Nt,{size:15}),` `,`Open account`]})}),(0,U.jsxs)(`div`,{className:`summary-grid`,children:[(0,U.jsx)(X,{label:`Username`,value:W(_.ApplicantUsername)||`-`}),(0,U.jsx)(X,{label:`Name`,value:_.ApplicantName||`-`}),(0,U.jsx)(X,{label:`User ID`,value:_.ApplicantUserID,mono:!0}),(0,U.jsx)(X,{label:`Submitted`,value:G(_.SubmittedAt)||`-`})]}),b?(0,U.jsx)(`p`,{className:`bot-create-note`,children:`The applicant controls the target right now — checked against the live records, not against the submission snapshot.`}):(0,U.jsx)(q,{children:`The applicant no longer controls the target. Approving would hand the badge to someone who does not hold the peer — normally a reason to reject.`})]}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(K,{title:`Application`,text:`Everything the applicant submitted, rendered as plain text.`}),(0,U.jsxs)(`div`,{className:`stacked-sections`,children:[(0,U.jsxs)(`div`,{className:`summary-grid`,children:[(0,U.jsx)(X,{label:`Category`,value:_.Category||`-`}),(0,U.jsx)(X,{label:`Correlation ID`,value:_.CorrelationID||`-`,mono:!0}),(0,U.jsx)(X,{label:`Created`,value:G(_.CreatedAt)||`-`}),(0,U.jsx)(X,{label:`Updated`,value:G(_.UpdatedAt)||`-`})]}),(0,U.jsx)(Co,{label:`Description`,children:_.Description?(0,U.jsx)(`p`,{className:`about-text`,children:_.Description}):(0,U.jsx)(`p`,{className:`bot-create-note`,children:`Not provided`})}),(0,U.jsx)(Co,{label:`Official website`,children:_.OfficialWebsite?(0,U.jsx)(`div`,{className:`about-text`,children:(0,U.jsx)(wo,{value:_.OfficialWebsite})}):(0,U.jsx)(`p`,{className:`bot-create-note`,children:`Not provided`})}),(0,U.jsx)(Co,{label:`Social links`,children:(0,U.jsx)(To,{values:_.SocialLinks})}),(0,U.jsx)(Co,{label:`Press coverage`,children:(0,U.jsx)(To,{values:_.PressLinks})}),(0,U.jsx)(Co,{label:`Applicant comment`,children:_.AdditionalNote?(0,U.jsx)(`p`,{className:`about-text`,children:_.AdditionalNote}):(0,U.jsx)(`p`,{className:`bot-create-note`,children:`Not provided`})}),(0,U.jsx)(`p`,{className:`bot-create-note`,children:`Only http:// and https:// links are clickable and open in a new tab; anything else is shown as text.`})]})]}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(K,{title:`Decision`,text:`What was decided, by whom, and with which wording.`}),(0,U.jsxs)(`div`,{className:`stacked-sections`,children:[(0,U.jsxs)(`div`,{className:`summary-grid`,children:[(0,U.jsx)(X,{label:`Reviewer`,value:_.ReviewerAdminID||`-`}),(0,U.jsx)(X,{label:`Decided`,value:G(_.ReviewedAt)||`-`}),(0,U.jsx)(X,{label:`Status`,value:po[_.Status]}),(0,U.jsx)(X,{label:`Version (optimistic lock)`,value:_.Version,mono:!0})]}),(0,U.jsx)(Co,{label:`Decision reason`,children:_.DecisionReason?(0,U.jsx)(`p`,{className:`about-text`,children:_.DecisionReason}):(0,U.jsx)(`p`,{className:`bot-create-note`,children:`No decision yet`})}),(0,U.jsx)(Co,{label:`Internal note · admins only`,children:_.InternalNote?(0,U.jsx)(`p`,{className:`about-text`,children:_.InternalNote}):(0,U.jsx)(`p`,{className:`bot-create-note`,children:`Not provided`})})]})]}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(K,{title:`History`,text:`Immutable trail of every status transition, with actor and reason.`}),(0,U.jsx)(`div`,{className:`table-wrap`,children:(0,U.jsxs)(`table`,{className:`data-table`,children:[(0,U.jsx)(`thead`,{children:(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`th`,{children:`Event`}),(0,U.jsx)(`th`,{children:`From → to`}),(0,U.jsx)(`th`,{children:`Actor`}),(0,U.jsx)(`th`,{children:`Reason`}),(0,U.jsx)(`th`,{children:`Internal note`}),(0,U.jsx)(`th`,{children:`Time`})]})}),(0,U.jsxs)(`tbody`,{children:[y.map(e=>(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`td`,{children:(0,U.jsx)(Eo,{kind:e.Kind})}),(0,U.jsxs)(`td`,{className:`mono`,children:[e.FromStatus||`-`,` → `,e.ToStatus||`-`]}),(0,U.jsx)(`td`,{children:e.Actor||`-`}),(0,U.jsx)(`td`,{className:`truncate`,children:e.Reason||`-`}),(0,U.jsx)(`td`,{className:`truncate`,children:e.Note||`-`}),(0,U.jsx)(`td`,{children:G(e.CreatedAt)||`-`})]},e.ID)),y.length===0&&(0,U.jsx)(Yn,{colSpan:6})]})]})})]})]}),side:(0,U.jsxs)(`section`,{className:`action-dock`,children:[(0,U.jsx)(`div`,{className:`dock-title`,children:`Review actions`}),!S&&!C&&!w&&(0,U.jsx)(`p`,{className:`bot-create-note`,children:`This status has no available actions.`}),S&&(0,U.jsxs)(U.Fragment,{children:[(0,U.jsx)(`div`,{className:`action-stack`,children:(0,U.jsx)(Z,{label:`Take into review`,icon:(0,U.jsx)(Be,{size:15}),tone:`neutral`,path:`/api/verification/applications/${_.ID}/claim`,payload:()=>({version:_.Version}),onDone:D,onError:h})}),(0,U.jsx)(`p`,{className:`bot-create-note`,children:`Assigns the application to you and moves it to in review, so two reviewers never work on the same one.`})]}),(C||w)&&(0,U.jsxs)(U.Fragment,{children:[(0,U.jsxs)(`label`,{className:`duration-field`,children:[(0,U.jsx)(`span`,{children:`Internal note`}),(0,U.jsx)(`textarea`,{value:a,onChange:e=>o(e.target.value),rows:3,placeholder:`Handover note for other reviewers`})]}),(0,U.jsx)(`p`,{className:`bot-create-note`,children:`Optional. Stored with the decision and visible to admins only — never sent to the applicant.`})]}),C&&(0,U.jsxs)(U.Fragment,{children:[!b&&(0,U.jsx)(q,{children:`The applicant no longer controls the target. Approving would hand the badge to someone who does not hold the peer — normally a reason to reject.`}),x&&(0,U.jsx)(`p`,{className:`bot-create-note`,children:`The target already carries the badge; approving only records the decision.`}),(0,U.jsxs)(`div`,{className:`action-stack`,children:[(0,U.jsx)(Z,{label:`Approve`,icon:(0,U.jsx)(F,{size:15}),tone:`neutral`,path:`/api/verification/applications/${_.ID}/approve`,payload:E,onDone:D,onError:h}),(0,U.jsx)(Z,{label:`Reject`,icon:(0,U.jsx)(ne,{size:15}),tone:`warn`,path:`/api/verification/applications/${_.ID}/reject`,payload:E,onDone:D,onError:h})]}),(0,U.jsx)(`p`,{className:`bot-create-note`,children:`Grants the official badge to the target and closes the application.`}),(0,U.jsx)(`p`,{className:`bot-create-note`,children:`The reason is mandatory: it is the wording the applicant is told, so write what exactly was missing.`})]}),w&&(0,U.jsxs)(U.Fragment,{children:[(0,U.jsxs)(`div`,{className:`dock-title`,children:[(0,U.jsx)(St,{size:14}),` `,`Danger zone`]}),(0,U.jsxs)(`div`,{className:`danger-zone`,children:[(0,U.jsx)(Z,{label:`Revoke verification`,icon:(0,U.jsx)(ve,{size:15}),tone:`danger`,path:`/api/actions/revoke-verification`,payload:()=>{let e={target_type:_.TargetType,target_id:_.TargetID};return T&&(e.internal_note=T),e},onDone:D,onError:h}),(0,U.jsx)(`p`,{className:`bot-create-note`,children:`Clears the badge from the target. The approved application stays in history.`}),!x&&(0,U.jsx)(`p`,{className:`bot-create-note`,children:`The target carries no badge right now — there is nothing to revoke.`})]})]})]})})]})}function Co({label:e,children:t}){return(0,U.jsxs)(`div`,{className:`duration-field`,children:[(0,U.jsx)(`span`,{children:e}),t]})}function wo({value:e}){let t=Pn(e);return t?(0,U.jsxs)(`a`,{className:`row-link`,href:t,target:`_blank`,rel:`noopener noreferrer`,children:[e,` `,(0,U.jsx)(je,{size:13})]}):(0,U.jsx)(`span`,{className:`mono`,children:e})}function To({values:e}){let t=(e??[]).filter(e=>e.trim()!==``);return t.length===0?(0,U.jsx)(`p`,{className:`bot-create-note`,children:`Not provided`}):(0,U.jsx)(`div`,{className:`about-text`,children:t.map((e,t)=>(0,U.jsx)(`div`,{children:(0,U.jsx)(wo,{value:e})},`${t}-${e}`))})}function Eo({kind:e}){return(0,U.jsx)(J,{tone:e===`approved`?`good`:e===`rejected`||e===`revoked`||e===`cancelled`?`danger`:e===`submitted`||e===`claimed`?`warn`:`neutral`,children:xo[e]})}function Do({route:e,navigate:t}){let n=(e,n)=>(0,U.jsx)(dn,{navigate:t,permission:e,children:n}),r=e.path.match(/^\/accounts\/(\d+)$/)?.[1],i=e.path.match(/^\/channels\/(\d+)$/)?.[1],a=e.path.match(/^\/bots\/(\d+)$/)?.[1],o=e.path.match(/^\/moderation\/(\d+)$/)?.[1],s=e.path.match(/^\/collectible-usernames\/(\d+)$/)?.[1],c=e.path.match(/^\/verification\/(\d+)$/)?.[1],l=e.path.match(/^\/bot-verification\/(\d+)$/)?.[1];return l?(0,U.jsx)(pn,{navigate:t,children:(0,U.jsx)(dn,{navigate:t,permission:Gt,children:(0,U.jsx)(so,{id:l,navigate:t})})}):e.path===`/bot-verification`?(0,U.jsx)(pn,{navigate:t,children:(0,U.jsx)(dn,{navigate:t,permission:Gt,children:(0,U.jsx)(Xa,{navigate:t})})}):c?(0,U.jsx)(dn,{navigate:t,permission:Wt,children:(0,U.jsx)(So,{id:c,navigate:t})}):e.path===`/verification`?(0,U.jsx)(dn,{navigate:t,permission:Wt,children:(0,U.jsx)(ho,{navigate:t})}):s?n(rn,(0,U.jsx)(bi,{id:s,navigate:t})):e.path===`/collectible-usernames`?n(rn,(0,U.jsx)(gi,{navigate:t})):e.path===`/storage`?n(tn,(0,U.jsx)(Ta,{navigate:t})):r?n(Yt,(0,U.jsx)(ii,{id:Number(r),navigate:t})):i?n(Xt,(0,U.jsx)(wi,{id:Number(i),navigate:t})):a?n(Zt,(0,U.jsx)(Oi,{id:Number(a),navigate:t})):o?n($t,(0,U.jsx)(va,{id:Number(o),navigate:t})):e.path===`/accounts/shared-devices`?n(Yt,(0,U.jsx)(ui,{navigate:t})):e.path===`/accounts`?n(Yt,(0,U.jsx)(li,{navigate:t})):e.path===`/channels`?n(Xt,(0,U.jsx)(Ei,{navigate:t})):e.path===`/bots`?n(Zt,(0,U.jsx)(ji,{navigate:t})):e.path===`/moderation`?n($t,(0,U.jsx)(da,{navigate:t})):e.path===`/broadcasts`?n(en,(0,U.jsx)(Pi,{})):e.path===`/emoji`?n(nn,(0,U.jsx)($i,{kind:`emoji`})):e.path===`/stickers`?n(nn,(0,U.jsx)($i,{kind:`stickers`})):e.path===`/gif-catalog`?n(nn,(0,U.jsx)(na,{})):e.path===`/admin-users`?(0,U.jsx)(dn,{navigate:t,permission:Jt,children:(0,U.jsx)(ia,{})}):e.path===`/server-settings`?(0,U.jsx)(dn,{navigate:t,permission:qt,children:(0,U.jsx)(ur,{})}):e.path===`/messages/detail`||e.path===`/messages/private/detail`?n(Qt,(0,U.jsx)(Ui,{ownerUserID:Number(e.search.get(`owner_user_id`)||`0`),msgID:Number(e.search.get(`msg_id`)||`0`),navigate:t})):e.path===`/messages/groups/detail`?n(Qt,(0,U.jsx)(Hi,{channelID:Number(e.search.get(`channel_id`)||`0`),msgID:Number(e.search.get(`msg_id`)||`0`),navigate:t})):e.path===`/messages`||e.path===`/messages/private`||e.path===`/messages/groups`?n(Qt,(0,U.jsx)(Ki,{navigate:t,tab:e.path===`/messages/groups`?`groups`:`private`,onTab:e=>t(e===`groups`?`/messages/groups`:`/messages/private`)})):n(an,(0,U.jsx)(Fi,{navigate:t}))}function Oo(){let[e,t]=(0,g.useState)(void 0),[n,r]=(0,g.useState)(()=>yn());(0,g.useEffect)(()=>{let e=()=>r(yn());return window.addEventListener(`popstate`,e),()=>window.removeEventListener(`popstate`,e)},[]),(0,g.useEffect)(()=>{k.session().then(e=>t(e)).catch(()=>t(null))},[]);let i=e=>{window.history.pushState(null,``,e),r(yn())};if(e===void 0)return(0,U.jsx)(sr,{});if(e===null)return(0,U.jsx)(Br,{onLogin:e=>{window.history.replaceState(null,``,`/`),r(yn()),t(e)}});let a=(e.permissions??[]).some(e=>e===`*`||e===`server.manage`);return e.setup_completed===!1&&a?(0,U.jsx)(Or,{}):(0,U.jsx)(sn,{permissions:e.permissions??[],hideThirdPartyVerification:e.hide_third_party_verification??!0,children:(0,U.jsx)(cr,{actor:e.actor,apiLayers:e.api_layers,build:e.build,route:n,navigate:i,onLogout:()=>t(null),children:(0,U.jsx)(Do,{route:n,navigate:i})})})}_.createRoot(document.getElementById(`root`)).render((0,U.jsx)(g.StrictMode,{children:(0,U.jsx)(wn,{children:(0,U.jsx)(Oo,{})})})); \ No newline at end of file diff --git a/cmd/telesrv-admin/web/dist/assets/index-P_k7ini0.css b/cmd/telesrv-admin/web/dist/assets/index-P_k7ini0.css new file mode 100644 index 00000000..f2662594 --- /dev/null +++ b/cmd/telesrv-admin/web/dist/assets/index-P_k7ini0.css @@ -0,0 +1 @@ +@font-face{font-family:Plus Jakarta Sans;font-style:normal;font-weight:400;font-display:swap;src:url(/fonts/plus-jakarta-sans-400.woff2)format("woff2")}@font-face{font-family:Plus Jakarta Sans;font-style:normal;font-weight:500;font-display:swap;src:url(/fonts/plus-jakarta-sans-500.woff2)format("woff2")}@font-face{font-family:Plus Jakarta Sans;font-style:normal;font-weight:600;font-display:swap;src:url(/fonts/plus-jakarta-sans-600.woff2)format("woff2")}@font-face{font-family:Plus Jakarta Sans;font-style:normal;font-weight:700;font-display:swap;src:url(/fonts/plus-jakarta-sans-700.woff2)format("woff2")}@font-face{font-family:Plus Jakarta Sans;font-style:normal;font-weight:800;font-display:swap;src:url(/fonts/plus-jakarta-sans-800.woff2)format("woff2")}:root{--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light;--bg:#f7f9fc;--bg-accent:#eef1f5;--panel:#fff;--panel-subtle:#f7f9fc;--panel-strong:#f1f5f9;--skeleton-base:#e6ebf2;--skeleton-sheen:#f4f7fa;--surface-soft:#f2f7fd;--overlay:#18222f6b;--topbar-bg:#fffffff0;--line:#e2e8f0;--line-strong:#cbd5e1;--heading:#101828;--text:#0f1720;--text-soft:#344054;--muted:#64748b;--muted-2:#94a3b8;--brand:#2563eb;--brand-strong:#1d4ed8;--brand-2:#38bdf8;--grad:linear-gradient(135deg, #38bdf8 0%, #2563eb 55%, #1e40af 100%);--brand-tint:#eaf2fd;--brand-tint-border:#c7dcf9;--brand-tint-text:#1e3a8a;--good:#167447;--good-tint:#eaf6ef;--good-border:#c1e1cf;--warn:#a15c07;--warn-tint:#fcf4e4;--warn-border:#e7d09e;--danger:#b42318;--danger-tint:#fcefec;--danger-border:#eecac3;--danger-text:#8f2f27;--purple:#6a4fa3;--purple-tint:#f4effb;--purple-border:#dcd0f0;--purple-text:#5a4590;--input-bg:#fff;--btn-bg:#fff;--btn-text:#29323d;--btn-hover:#f4f7fa;--switch-track:#c8d0d6;--code-bg:#1b2733;--code-text:#d6e3ef;--code-border:#2b3a49;--sidebar:#08080e;--sidebar-soft:#12121a;--sidebar-line:#222228;--sidebar-row:#17171f;--sidebar-text:#c6d0dc;--sidebar-muted:#8fa0b4;--sidebar-faint:#8492a6;--sidebar-heading:#fff;--focus:#2563eb29;--shadow:0 28px 70px -36px #05050859;--shadow-sm:0 2px 10px #1827380d;--shadow-brand:0 8px 22px #2563eb38;--hero-glow:#2563eb24;--hero-grid:#0505080a;--radius-xs:8px;--radius-sm:9px;--radius:11px;--radius-lg:14px}[data-theme=dark]{--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark;--bg:#0f141a;--bg-accent:#131a22;--panel:#171f28;--panel-subtle:#1c2530;--panel-strong:#212c38;--skeleton-base:#212c38;--skeleton-sheen:#2e3d4c;--surface-soft:#1a232d;--overlay:#05080c9e;--topbar-bg:#151c24db;--line:#29333f;--line-strong:#38434f;--heading:#eef3f8;--text:#d5dde6;--text-soft:#c2ccd6;--muted:#98a4b1;--muted-2:#6d7885;--brand:#5b9dff;--brand-strong:#7db4ff;--brand-2:#7cd1fb;--brand-tint:#142a4a;--brand-tint-border:#24466e;--brand-tint-text:#9dc3f5;--good:#47c281;--good-tint:#12301f;--good-border:#245639;--warn:#e0aa4d;--warn-tint:#322810;--warn-border:#574413;--danger:#e6695c;--danger-tint:#35201d;--danger-border:#5c332d;--danger-text:#f0a49b;--purple:#ac90e2;--purple-tint:#221b31;--purple-border:#3d3357;--purple-text:#c9b6ef;--input-bg:#131a22;--btn-bg:#1e2731;--btn-text:#dbe2ea;--btn-hover:#26313d;--switch-track:#3a454f;--code-bg:#0c1218;--code-text:#cdd9e5;--code-border:#232f3b;--sidebar:#10151b;--sidebar-soft:#1c242f;--sidebar-line:#262f3a;--sidebar-row:#161d25;--sidebar-text:#cbd4de;--sidebar-muted:#7c8794;--sidebar-faint:#6f7b88;--sidebar-heading:#f0f4f8;--focus:#5b9dff3d;--shadow:0 16px 40px #00000075;--shadow-sm:0 2px 12px #00000061;--shadow-brand:0 8px 22px #5b9dff42;--hero-glow:#5b9dff40;--hero-grid:#ffffff0a}*{box-sizing:border-box}html,body,#root{min-height:100%}body{color:var(--text);background:var(--bg);-webkit-font-smoothing:antialiased;text-rendering:optimizelegibility;margin:0;font:13px/1.45 Plus Jakarta Sans,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;transition:background-color .2s,color .2s}button,input,select,textarea{font:inherit}a{color:inherit;text-decoration:none}.shell{grid-template-columns:232px minmax(0,1fr);min-height:100vh;display:grid}.sidebar{height:100vh;color:var(--sidebar-text);background:var(--sidebar);border-right:1px solid var(--sidebar-line);flex-direction:column;gap:16px;padding:18px 12px;display:flex;position:sticky;top:0;overflow-y:auto}.brand{align-items:center;gap:10px;min-height:42px;padding:0 4px;display:flex}.brand.compact{justify-content:center}.brand-mark{border-radius:50%;place-items:center;width:34px;height:34px;display:grid;overflow:hidden}.brand-mark img{object-fit:cover;width:100%;height:100%;display:block}.brand strong{font-size:14px;line-height:1.1;display:block}.brand small{color:var(--sidebar-muted);margin-top:3px;font-size:11px;display:block}.sidebar-server-actions{gap:8px;display:flex}.sidebar-server-action{min-width:0;color:var(--sidebar-text);border-color:var(--sidebar-line);background:0 0;flex:1 1 0;font-size:12.5px}.sidebar-server-action:hover:not(:disabled){background:var(--sidebar-line)}.sidebar-server-action:disabled{opacity:.5;cursor:not-allowed}.sidebar-server-action-error{color:var(--danger,#e5484d);padding:0 4px;font-size:11.5px}.sidebar-label{color:var(--sidebar-faint);text-transform:uppercase;letter-spacing:.04em;padding:0 8px;font-size:11px;font-weight:700}.sidebar-api-layer,.sidebar-build{text-transform:none;opacity:.7;font-family:SFMono-Regular,Consolas,Liberation Mono,monospace;font-weight:500}.nav-list,.nav-section{gap:4px;display:grid}.nav-section-toggle{width:100%;min-height:38px;color:var(--sidebar-muted);border-radius:var(--radius-sm);cursor:pointer;text-align:left;background:0 0;border:1px solid #0000;grid-template-columns:18px minmax(0,1fr) 16px;align-items:center;gap:9px;padding:0 10px;font-size:12px;font-weight:800;transition:color .14s,background-color .14s,border-color .14s;display:grid}.nav-section-toggle:hover,.nav-section.active .nav-section-toggle{color:var(--sidebar-heading);background:var(--sidebar-soft);border-color:#34404d}.nav-section-chevron{color:var(--sidebar-muted);justify-self:end;transition:transform .14s}.nav-section.open .nav-section-chevron{transform:rotate(180deg)}.nav-children{gap:4px;padding:2px 0 2px 18px;display:grid}.nav-item{min-height:38px;color:var(--sidebar-text);border-radius:var(--radius-sm);border:1px solid #0000;grid-template-columns:18px minmax(0,1fr);align-items:center;gap:9px;padding:0 10px;transition:color .14s,background-color .14s,border-color .14s;display:grid}.nav-dot{background:var(--sidebar-faint);border-radius:999px;justify-self:center;width:6px;height:6px}.nav-item:hover,.nav-item.active{color:var(--sidebar-heading);background:var(--sidebar-soft);border-color:#34404d}.nav-item.active .nav-dot{background:var(--brand)}.sidebar-status{gap:7px;margin-top:auto;display:grid}.runtime-row{min-height:32px;color:var(--sidebar-text);background:var(--sidebar-row);border-radius:var(--radius-sm);border:1px solid #27313c;grid-template-columns:18px minmax(0,1fr) auto;align-items:center;gap:7px;padding:0 8px;display:grid}.runtime-row strong{color:var(--sidebar-heading);font-size:11px}.workspace{min-width:0}.topbar{z-index:20;background:var(--topbar-bg);border-bottom:1px solid var(--line);-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);justify-content:space-between;align-items:center;gap:18px;min-height:66px;padding:12px 24px;display:flex;position:sticky;top:0}.topbar h1{color:var(--heading);margin:2px 0 0;font-size:20px;line-height:1.2}.topbar-actions,.page-actions,.section-action,.entity-badges,.row-actions,.modal-actions{flex-wrap:wrap;align-items:center;gap:8px;display:flex}.theme-toggle{width:34px;height:34px;color:var(--muted);background:var(--panel-subtle);border:1px solid var(--line);cursor:pointer;border-radius:999px;place-items:center;transition:color .16s,background-color .16s,border-color .16s;display:inline-grid}.theme-toggle:hover{color:var(--brand);border-color:var(--brand-tint-border);background:var(--brand-tint)}.theme-toggle:focus-visible{outline:2px solid var(--brand);outline-offset:2px}.actor-pill{min-height:30px;color:var(--text-soft);background:var(--panel-subtle);border:1px solid var(--line);border-radius:999px;align-items:center;gap:6px;padding:0 12px;font-weight:600;display:inline-flex}.content{gap:16px;padding:18px 24px 30px;display:grid}.eyebrow{color:var(--muted);text-transform:uppercase;letter-spacing:.04em;font-size:11px;font-weight:800}@media (width>=1121px){.shell--nav-collapsed{grid-template-columns:68px minmax(0,1fr)}.shell--nav-collapsed .brand>span:not(.brand-mark),.shell--nav-collapsed .sidebar-label,.shell--nav-collapsed .sidebar-status,.shell--nav-collapsed .sidebar-server-actions,.shell--nav-collapsed .nav-item-label{display:none}.shell--nav-collapsed .sidebar{align-items:center;padding:18px 10px}.shell--nav-collapsed .brand{justify-content:center;padding:0}.shell--nav-collapsed .nav-item{grid-template-columns:1fr;justify-items:center;width:44px;padding:0}.shell--nav-collapsed .nav-list{justify-items:center}}.topbar-lead{align-items:center;gap:12px;min-width:0;display:flex}.nav-toggle{flex:none}.dashboard-layout,.stacked-sections{gap:14px;display:grid}.dashboard-section{gap:10px;display:grid}.dashboard-section-title{color:var(--heading);text-transform:uppercase;letter-spacing:.04em;align-items:baseline;gap:8px;font-size:13px;font-weight:800;display:flex}.dashboard-section-title span{color:var(--muted);text-transform:none;letter-spacing:normal;font-size:11px;font-weight:600}.dashboard-grid{grid-template-columns:repeat(auto-fill,minmax(190px,1fr));gap:10px;display:grid}.stat-tile{text-align:left;background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);min-width:0;box-shadow:var(--shadow-sm);gap:8px;padding:14px;display:grid}button.stat-tile{cursor:pointer;font:inherit;color:inherit}a.stat-tile.clickable,button.stat-tile.clickable{transition:border-color .16s,box-shadow .16s,transform .16s}.stat-tile.clickable:hover{border-color:var(--brand-tint-border);box-shadow:var(--shadow);transform:translateY(-1px)}.stat-tile-head{justify-content:space-between;align-items:center;gap:8px;display:flex}.stat-tile-icon{width:30px;height:30px;color:var(--brand);background:var(--brand-tint);border:1px solid var(--brand-tint-border);border-radius:var(--radius-sm);flex:none;place-items:center;display:grid}.stat-tile.warn .stat-tile-icon{color:var(--warn);background:var(--warn-tint);border-color:var(--warn-border)}.stat-tile.danger .stat-tile-icon{color:var(--danger);background:var(--danger-tint);border-color:var(--danger-border)}.stat-tile.good .stat-tile-icon{color:var(--good);background:var(--good-tint);border-color:var(--good-border)}.stat-tile-open{color:var(--muted)}.stat-tile-value{color:var(--heading);font-size:24px;font-weight:800;line-height:1.1}.skeleton{background-image:linear-gradient(90deg, var(--skeleton-base) 25%, var(--skeleton-sheen) 37%, var(--skeleton-base) 63%);-webkit-user-select:none;user-select:none;background-size:400% 100%;border-radius:6px;animation:1.4s ease-in-out infinite skeletonShimmer;display:inline-block}.skeleton-value{vertical-align:middle;width:2.75ch;height:.85em}.skeleton-text{vertical-align:middle;width:6ch;height:.8em}@keyframes skeletonShimmer{0%{background-position:100%}to{background-position:0}}@media (prefers-reduced-motion:reduce){.skeleton{animation:none}}.stat-tile.warn .stat-tile-value{color:var(--warn)}.stat-tile.danger .stat-tile-value{color:var(--danger)}.stat-tile-label{color:var(--text-soft);font-size:12px;font-weight:700}.stat-tile-sub{color:var(--muted);font-size:11px}.stat-tile-bar{background:var(--panel-subtle);border:1px solid var(--line);border-radius:999px;width:100%;height:5px;overflow:hidden}.stat-tile-bar>span{background:var(--brand-2);height:100%;display:block}.stat-tile.warn .stat-tile-bar>span{background:var(--warn)}.stat-tile.danger .stat-tile-bar>span{background:var(--danger)}.overview-band,.page-frame{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);min-width:0;box-shadow:var(--shadow-sm)}.overview-band{grid-template-columns:minmax(220px,1fr) minmax(420px,.9fr);align-items:center;gap:16px;padding:16px;display:grid}.overview-band h2,.page-title-row h2,.section-head h2,.modal h2{color:var(--heading);margin:0;font-size:18px;line-height:1.25}.overview-metrics,.metric-row{grid-template-columns:repeat(4,minmax(120px,1fr));gap:8px;display:grid}.overview-metrics{grid-template-columns:repeat(3,minmax(120px,1fr))}.status-item,.metric,.summary-item{background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius-sm);min-width:0;padding:10px}.status-item span,.metric span,.summary-item span{color:var(--muted);margin-bottom:6px;font-size:11px;display:block}.status-item strong,.metric strong,.summary-item strong{overflow-wrap:anywhere;color:var(--text);font-weight:800;display:block}.status-item.good,.metric.good{border-color:var(--good-border)}.status-item.warn,.metric.warn{border-color:var(--warn-border)}.metric.danger{border-color:var(--danger-border)}.command-grid{grid-template-columns:repeat(3,minmax(220px,1fr));gap:12px;display:grid}.launcher{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);min-height:94px;box-shadow:var(--shadow-sm);grid-template-columns:38px minmax(0,1fr) 18px;align-items:center;gap:12px;padding:14px;transition:border-color .16s,box-shadow .16s,transform .16s;display:grid}.launcher:hover{border-color:var(--brand-tint-border);box-shadow:var(--shadow);transform:translateY(-1px)}.launcher-icon{width:38px;height:38px;color:var(--brand);background:var(--brand-tint);border:1px solid var(--brand-tint-border);border-radius:var(--radius-sm);place-items:center;display:grid}.launcher-copy{gap:4px;display:grid}.launcher-copy strong{color:var(--heading);font-size:15px}.launcher-copy span{color:var(--muted)}.work-strip{grid-template-columns:repeat(4,minmax(160px,1fr));gap:8px;display:grid}.strip-item{min-height:38px;color:var(--text-soft);background:var(--panel);border:1px solid var(--line);border-radius:var(--radius-sm);align-items:center;gap:8px;padding:0 10px;display:flex}.page-frame{gap:14px;padding:14px;display:grid}.page-title-row{border-bottom:1px solid var(--line);justify-content:space-between;align-items:flex-start;gap:14px;padding-bottom:12px;display:flex}.query-panel{background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius-sm);padding:10px}.toolbar{flex-wrap:wrap;align-items:center;gap:8px;display:flex}.message-query input{width:150px}.message-selector-grid{grid-template-columns:repeat(2,minmax(280px,1fr));gap:10px;margin-bottom:10px;display:grid}.message-selector-grid.single{grid-template-columns:minmax(320px,620px)}.entity-picker{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius-sm);gap:8px;min-width:0;padding:10px;display:grid}.picker-head{min-height:24px;color:var(--text-soft);justify-content:space-between;align-items:center;gap:8px;font-weight:800;display:flex}.selected-entity{min-height:40px;color:var(--brand-tint-text);background:var(--brand-tint);border:1px solid var(--brand-tint-border);border-radius:var(--radius-sm);grid-template-columns:18px minmax(0,1fr) auto;align-items:center;gap:8px;padding:7px 9px;display:grid}.selected-entity strong,.selected-entity span{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.selected-entity div{gap:2px;min-width:0;display:grid}.selected-entity div span{color:var(--brand-tint-text);opacity:.85;font-size:11px}.picker-search{background:var(--panel-subtle);border:1px solid var(--line-strong);border-radius:var(--radius-sm);grid-template-columns:18px minmax(0,1fr) auto;align-items:center;gap:7px;height:34px;padding:0 6px 0 9px;display:grid}.picker-search input{width:100%;height:30px;box-shadow:none;background:0 0;border:0;padding:0}.picker-results{border:1px solid var(--line);border-radius:var(--radius-sm);max-height:236px;display:grid;overflow:auto}.picker-row{min-height:36px;color:var(--text);background:var(--panel);border:0;border-bottom:1px solid var(--line);cursor:pointer;text-align:left;grid-template-columns:96px minmax(120px,1fr) minmax(120px,1fr) auto;align-items:center;gap:8px;padding:6px 8px;display:grid}.picker-row:last-child{border-bottom:0}.picker-row:hover,.picker-row.selected{background:var(--surface-soft)}.picker-row strong,.picker-row span{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.picker-empty,.picker-error{color:var(--muted);text-align:center;padding:9px}.picker-chip-list{flex-wrap:wrap;gap:6px;display:flex}.picker-chip{color:var(--brand-tint-text);background:var(--brand-tint);border:1px solid var(--brand-tint-border);border-radius:999px;align-items:center;gap:6px;padding:5px 8px;font-size:11px;font-weight:700;display:inline-flex}.picker-chip button{color:inherit;cursor:pointer;opacity:.75;background:0 0;border:0;align-items:center;padding:0;display:inline-flex}.picker-chip button:hover{opacity:1}.emoji-picker-row{grid-template-columns:36px minmax(140px,1fr) minmax(100px,1fr)}.emoji-picker-glyph{text-align:center;font-size:22px;line-height:1}.emoji-picker-anim{width:28px;height:28px}.emoji-picker-anim canvas{width:100%!important;height:100%!important}.picker-error{color:var(--danger);background:var(--danger-tint);border:1px solid var(--danger-border);border-radius:var(--radius-sm)}input,select,textarea{color:var(--text);background:var(--input-bg);border:1px solid var(--line-strong);border-radius:var(--radius-sm);outline:none;transition:border-color .14s,box-shadow .14s}input::placeholder,textarea::placeholder{color:var(--muted-2)}input,select{width:190px;height:34px;padding:0 10px}select{min-width:220px;height:34px;font:inherit;appearance:none;cursor:pointer;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%239aa4b2' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");background-position:right 10px center;background-repeat:no-repeat;padding:0 30px 0 10px;font-weight:600}select:disabled{color:var(--muted-2);cursor:not-allowed}textarea{resize:vertical;width:100%;padding:9px 10px}input:focus,select:focus,textarea:focus{border-color:var(--brand);box-shadow:0 0 0 3px var(--focus)}.small-input{width:88px}.sort-order-editor{align-items:center;gap:6px;display:flex}.sort-order-editor .small-input{width:64px;height:32px}.sort-order-editor .title-input{width:160px}.field-inline{color:var(--muted);align-items:center;gap:6px;display:inline-flex}.field-inline span{font-size:11px;font-weight:700}.searchbox{width:min(380px,100%);height:34px;color:var(--text);background:var(--input-bg);border:1px solid var(--line-strong);border-radius:var(--radius-sm);align-items:center;gap:8px;padding:0 10px;display:inline-flex}.searchbox input{width:100%;height:30px;box-shadow:none;border:0;padding:0}.btn{min-height:34px;color:var(--btn-text);background:var(--btn-bg);border:1px solid var(--line-strong);border-radius:var(--radius-sm);cursor:pointer;white-space:nowrap;justify-content:center;align-items:center;gap:6px;padding:0 12px;transition:background-color .14s,border-color .14s,color .14s,box-shadow .14s;display:inline-flex}.btn:hover:not(:disabled){background:var(--btn-hover)}.btn:disabled{color:var(--muted-2);cursor:not-allowed}.btn.primary{color:#fff;background:var(--brand);border-color:var(--brand)}.btn.primary:hover:not(:disabled){background:var(--brand-strong);border-color:var(--brand-strong)}.btn.ghost{background:var(--panel-subtle)}.btn.danger{color:var(--danger);background:var(--danger-tint);border-color:var(--danger-border)}.btn.danger:hover:not(:disabled){background:var(--danger-tint);border-color:var(--danger)}.btn.warn{color:var(--warn);background:var(--warn-tint);border-color:var(--warn-border)}.btn.warn:hover:not(:disabled){background:var(--warn-tint);border-color:var(--warn)}.btn:disabled,.btn.primary:disabled,.btn.warn:disabled,.btn.danger:disabled{color:var(--muted-2);background:var(--panel-strong);border-color:var(--line);cursor:not-allowed}.btn.full{width:100%}.icon-text{gap:7px}.compact-btn{min-height:28px;padding:0 8px;font-size:12px}.row-link,.link-button{color:var(--brand-2);cursor:pointer;background:0 0;border:0;align-items:center;gap:4px;padding:0;display:inline-flex}.avatar-link{cursor:pointer;background:0 0;border:0;border-radius:999px;padding:0;line-height:0;display:block}.avatar-link:hover,.avatar-link:focus-visible{outline:2px solid var(--focus);outline-offset:2px}.table-wrap{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);width:100%;overflow-x:auto}.data-table{border-collapse:collapse;width:100%;font-size:12.5px}.data-table th,.data-table td{border-bottom:1px solid var(--line);text-align:left;vertical-align:middle;white-space:nowrap;height:38px;padding:7px 9px}.data-table th{z-index:0;color:var(--muted);background:var(--panel-strong);font-weight:800;position:sticky;top:0}.sort-header{color:inherit;font:inherit;cursor:pointer;background:0 0;border:0;align-items:center;gap:5px;padding:0;font-weight:800;display:inline-flex}.sort-header:hover{color:var(--text)}.sort-header-idle{opacity:.45}.data-table tbody tr:hover{background:var(--panel-subtle)}.data-table tr:last-child td{border-bottom:0}.mono{font-family:SFMono-Regular,Consolas,Liberation Mono,monospace}.truncate{text-overflow:ellipsis;max-width:380px;overflow:hidden}.badge{min-height:22px;color:var(--muted);background:var(--panel-strong);border:1px solid var(--line-strong);white-space:nowrap;border-radius:999px;align-items:center;padding:1px 8px;display:inline-flex}.badge.good{color:var(--good);background:var(--good-tint);border-color:var(--good-border)}.badge.danger{color:var(--danger);background:var(--danger-tint);border-color:var(--danger-border)}.badge.warn{color:var(--warn);background:var(--warn-tint);border-color:var(--warn-border)}.empty-cell{color:var(--muted);text-align:center}.bot-create-fields{grid-template-columns:repeat(3,minmax(0,1fr));gap:12px;display:grid}.bot-create-fields .duration-field input{width:100%}.bot-create-actions{border-top:1px solid var(--line);justify-content:space-between;align-items:center;gap:14px;margin-top:14px;padding-top:14px;display:flex}.bot-create-note{color:var(--muted);font-size:12px;line-height:1.4}@media (width<=760px){.bot-create-fields{grid-template-columns:1fr}.bot-create-actions{flex-direction:column;align-items:stretch}}.permission-grid{grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:6px;margin:2px 0 8px;display:grid}.form-stack .permission-item,.permission-item{border:1px solid var(--line);border-radius:var(--radius-sm);background:var(--panel-subtle);cursor:pointer;align-items:center;gap:8px;width:auto;padding:7px 10px;font-size:12px;transition:border-color .14s;display:flex}.permission-item:hover{border-color:var(--brand-tint-border)}.form-stack .permission-item input[type=checkbox],.permission-item input[type=checkbox]{width:15px;height:15px;accent-color:var(--brand);cursor:pointer;border-radius:4px;flex:none;margin:0;padding:0}.permission-copy{gap:1px;min-width:0;display:grid}.permission-copy strong{color:var(--text);text-overflow:ellipsis;white-space:nowrap;font-size:12px;font-weight:700;overflow:hidden}.permission-copy small{color:var(--muted);text-overflow:ellipsis;white-space:nowrap;font-size:11px;overflow:hidden}.permission-item.standalone{justify-self:start;width:max-content}.chip-row{flex-wrap:wrap;gap:4px;display:flex}.chip{border:1px solid var(--line);background:var(--panel-strong);color:var(--text-soft);white-space:nowrap;border-radius:999px;padding:2px 7px;font-size:11px;font-weight:600;display:inline-block}.pill{border:1px solid var(--line);background:var(--panel-strong);color:var(--text-soft);border-radius:999px;padding:2px 9px;font-size:11px;font-weight:700;display:inline-block}.pill.good{border-color:var(--good-border);color:var(--good)}.permission-groups{gap:14px;display:grid}.permission-group{gap:8px;display:grid}.permission-group-head{border-bottom:1px solid var(--line);justify-content:space-between;align-items:flex-start;gap:12px;padding-bottom:6px;display:flex}.permission-group-head strong{color:var(--heading);font-size:13px;display:block}.permission-group-head small{color:var(--muted);margin-top:1px;font-size:11px;display:block}.operator-identity{grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:10px;display:grid}.modal.narrow{width:min(460px,100%)}.status-screen{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);min-height:min(560px,70vh);box-shadow:var(--shadow-sm);text-align:center;place-items:center;padding:32px 24px;display:grid;position:relative;overflow:hidden}.status-screen-code{color:var(--heading);letter-spacing:-.04em;opacity:.05;-webkit-user-select:none;user-select:none;pointer-events:none;font-size:clamp(140px,26vw,280px);font-weight:800;line-height:1;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)}.status-screen-body{justify-items:center;gap:12px;max-width:460px;display:grid;position:relative}.status-screen-icon{width:54px;height:54px;color:var(--brand);background:var(--brand-tint);border:1px solid var(--brand-tint-border);border-radius:50%;place-items:center;display:grid}.status-screen-body h1{color:var(--heading);margin:0;font-size:20px;line-height:1.25}.status-screen-body p{color:var(--text-soft);margin:0;font-size:13px;line-height:1.55}.status-screen-detail{color:var(--text-soft);background:var(--panel-strong);border:1px solid var(--line);overflow-wrap:anywhere;border-radius:999px;padding:5px 10px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11.5px}.status-screen-body .btn{margin-top:4px}.split-layout{grid-template-columns:minmax(0,1fr) 330px;align-items:start;gap:14px;display:grid}.split-main,.split-side{min-width:0}.entity-head{background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius);justify-content:space-between;align-items:flex-start;gap:14px;padding:14px;display:flex}.entity-head-main{align-items:center;gap:14px;min-width:0;display:flex}.entity-head-main .avatar-photo-img,.entity-head-main .avatar-fallback{flex-shrink:0}.avatar-edit-slot{flex-shrink:0;position:relative}.avatar-edit-btn{width:24px;height:24px;color:var(--brand);background:var(--panel);border:1px solid var(--line-strong);border-radius:999px;padding:0;position:absolute;bottom:-4px;right:-4px;box-shadow:0 1px 3px #0003}.avatar-edit-btn:hover{background:var(--brand-tint);border-color:var(--brand)}.entity-title{color:var(--heading);font-size:20px;font-weight:800;line-height:1.25}.entity-subtitle{color:var(--muted);margin-top:4px}.summary-grid{grid-template-columns:repeat(4,minmax(150px,1fr));gap:8px;display:grid}.about-text{color:var(--text-soft);background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius);margin:0;padding:10px}.action-groups{grid-template-columns:repeat(3,minmax(0,1fr));gap:14px;display:grid}.action-groups>.section-block{flex-direction:column;display:flex}.action-groups>.section-block>.section-head{flex-shrink:0}.action-groups>.section-block>.card-body{flex-direction:column;flex:1;justify-content:center;gap:10px;display:flex}.section-block,.action-dock,.surface{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);min-width:0;box-shadow:var(--shadow-sm);padding:12px}.section-head{justify-content:space-between;align-items:flex-start;gap:12px;margin-bottom:10px;display:flex}.section-head p{color:var(--muted);margin:5px 0 0}.action-dock{gap:10px;display:grid;position:sticky;top:82px}.dock-title{color:var(--text-soft);border-bottom:1px solid var(--line);padding-bottom:4px;font-weight:800}.action-dock>.btn,.action-dock .action-stack .btn{justify-content:center;width:100%}.duration-field{gap:4px;display:grid}.duration-field span{color:var(--muted);font-size:11px;font-weight:800}.duration-field input,.duration-field select{width:100%}.action-stack{gap:10px;display:grid}.action-stack .btn,.action-dock>.btn{min-height:42px}.danger-zone{border-top:1px solid var(--line);flex-wrap:wrap;gap:8px;margin-top:10px;padding-top:10px;display:flex}.dock-title+.danger-zone{border-top:0;margin-top:0;padding-top:0}.danger-zone-box{border:1px solid var(--danger-border);border-radius:var(--radius);margin:4px 0 14px;overflow:hidden}.danger-zone-box-title{letter-spacing:.02em;text-transform:uppercase;color:var(--danger);background:var(--danger-tint);border-bottom:1px solid var(--danger-border);padding:8px 12px;font-size:12px;font-weight:800}.danger-zone-box-body{gap:10px;padding:10px 12px;display:grid}.danger-zone-box-body .env-field-desc{color:var(--danger-text,var(--danger))}.authorization-block{gap:10px;display:grid}.authorization-table{table-layout:fixed;min-width:720px}.authorization-table th,.authorization-table td{height:46px}.device-text{text-overflow:ellipsis;max-width:260px;overflow:hidden}.device-actions-head{width:250px}.device-actions-cell{width:250px;min-width:250px}.device-actions{white-space:normal;grid-template-columns:repeat(2,minmax(110px,1fr));gap:6px;min-width:226px;display:grid}.device-actions .btn{justify-content:center;width:100%}.operation-row{grid-template-columns:repeat(2,minmax(280px,1fr));gap:10px;display:grid}.operation-box{background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius);flex-wrap:wrap;align-items:center;gap:8px;padding:10px;display:flex}.operation-title{width:100%;color:var(--heading);align-items:center;gap:6px;font-weight:800;display:flex}.checkline{color:var(--muted);align-items:center;gap:6px;display:inline-flex}.checkline input{width:auto;height:auto}.alert{color:var(--danger-text);background:var(--danger-tint);border:1px solid var(--danger-border);border-radius:var(--radius);align-items:flex-start;gap:8px;padding:9px 10px;display:flex}.json-block{max-height:520px;color:var(--code-text);background:var(--code-bg);border:1px solid var(--code-border);border-radius:var(--radius);margin:0;padding:12px;font-size:12px;overflow:auto}.raw-grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;display:grid}.loading-line{min-height:80px;color:var(--muted);place-items:center;display:grid}.empty-panel{min-height:92px;color:var(--muted);background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius);place-items:center;display:grid}.gift-metrics .metric{background:var(--panel-subtle);min-height:68px;padding:12px}.gift-metrics .metric strong{font-size:17px}.gift-file-icon{color:var(--brand);background:var(--brand-tint);border:1px solid var(--brand-tint-border);flex:none;place-items:center;display:grid}.gift-format-chips{flex-wrap:wrap;flex:none;justify-content:flex-end;gap:6px;display:flex}.gift-format-chips span{color:var(--brand-tint-text);background:var(--brand-tint);border:1px solid var(--brand-tint-border);letter-spacing:.02em;border-radius:999px;padding:4px 8px;font-size:10px;font-weight:800}.gift-list-summary{color:var(--muted);margin-left:auto;font-size:11px;font-weight:700}.gift-import-modal{width:min(860px,100%)}.gift-bulk-import-modal{width:min(480px,100%)}.gift-bulk-import-modal .command-body{gap:14px;padding:16px 18px;display:grid}.gift-import-modal-body{gap:14px}.gift-source-tabs{gap:8px;display:flex}.give-gift-summary{background:var(--panel-subtle);border:1px solid var(--line-strong);color:var(--text-soft);border-radius:12px;align-items:center;gap:11px;padding:11px 13px;display:flex}.give-gift-summary>svg{color:var(--brand);flex:none}.give-gift-summary strong{color:var(--text);font-size:13px;display:block}.give-gift-summary .mono{color:var(--muted);font-size:11px}.give-gift-tabs{background:var(--panel-subtle);border:1px solid var(--line-strong);border-radius:12px;gap:4px;width:100%;padding:4px;display:flex}.give-gift-tabs .btn{min-height:36px;box-shadow:none;color:var(--text-soft);background:0 0;border:1px solid #0000;border-radius:9px;flex:1 1 0;justify-content:center;transition:color .15s,background .15s,border-color .15s,box-shadow .15s}.give-gift-tabs .btn:not(.primary):hover{color:var(--brand);background:var(--brand-tint)}.give-gift-tabs .btn.primary{color:#fff;background:var(--brand);border-color:var(--brand);box-shadow:var(--shadow-brand)}.give-gift-upgrade-note{background:var(--brand-tint);border:1px solid var(--brand-tint-border);color:var(--text-soft);border-radius:10px;margin:0;padding:9px 12px;font-size:11px;font-weight:650;line-height:1.45}.give-gift-attrs{grid-template-columns:repeat(3,minmax(0,1fr));align-items:end}.give-gift-attrs select,.give-gift-attrs input{width:100%;min-width:0;height:38px;color:var(--text);background-color:var(--input-bg);border:1px solid var(--line);border-radius:var(--radius-sm);font:inherit;appearance:none;cursor:pointer;padding:0 32px 0 10px;font-size:12px;font-weight:600}.give-gift-attrs input{cursor:text;text-overflow:ellipsis;padding-right:10px}.give-gift-attrs select{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%239aa4b2' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");background-position:right 11px center;background-repeat:no-repeat}.give-gift-attrs select:focus,.give-gift-attrs input:focus{border-color:var(--brand);box-shadow:0 0 0 3px var(--focus);outline:none}.give-gift-layout{grid-template-columns:minmax(220px,280px) minmax(0,1fr);align-items:start;gap:16px;display:grid}.give-gift-picker{align-content:start;gap:10px;display:grid}.give-gift-picker-head{align-items:center;gap:12px;display:flex}.give-gift-picker-head .searchbox{flex:auto}.give-gift-picker-list{background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius-lg);gap:8px;max-height:640px;padding:8px;display:grid;overflow-y:auto}.give-gift-option{text-align:left;min-width:0;color:var(--text);background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);cursor:pointer;box-shadow:var(--shadow-sm);grid-template-columns:46px minmax(0,1fr) auto;align-items:center;gap:11px;padding:9px 11px;transition:border-color .15s,box-shadow .15s,transform .15s;display:grid}.give-gift-option:hover{border-color:var(--brand-tint-border);box-shadow:var(--shadow);transform:translateY(-1px)}.give-gift-option.selected{border-color:var(--brand);box-shadow:0 0 0 2px var(--focus), var(--shadow)}.give-gift-thumb{place-items:center;width:46px;height:46px;display:grid}.give-gift-thumb canvas{width:100%!important;height:100%!important}.give-gift-option-info{gap:3px;min-width:0;display:grid}.give-gift-option-info strong{text-overflow:ellipsis;white-space:nowrap;font-size:12px;overflow:hidden}.give-gift-option-info .mono{color:var(--muted);font-size:10px}.give-gift-option-price{white-space:nowrap;justify-self:end}.give-gift-panel{background:var(--panel);border:1px solid var(--line-strong);border-radius:var(--radius-lg);gap:12px;min-width:0;padding:16px;display:grid}.give-gift-form{gap:12px;min-width:0;display:grid}.give-gift-form-actions{flex-wrap:wrap;justify-content:flex-end;gap:10px;padding-top:4px;display:flex}.give-gift-empty-panel{color:var(--muted);text-align:center;place-items:center;gap:10px;padding:48px 20px;display:grid}.give-gift-empty-panel svg{color:var(--brand);opacity:.8}.official-gift-picker{gap:12px;min-width:0;display:grid}.official-gift-bulk-import{flex-wrap:wrap;align-items:center;gap:10px;display:flex}.gift-bulk-import-progress{align-items:center;gap:8px;min-width:180px;display:flex}.gift-bulk-import-progress-bar{background:#e3e8ef;border-radius:999px;flex:auto;width:120px;height:6px;overflow:hidden}.gift-bulk-import-progress-bar>div{background:var(--brand);border-radius:999px;height:100%;transition:width .2s}.gift-bulk-import-progress span{color:var(--muted);white-space:nowrap;font-size:11px;font-weight:700}.official-gift-tools{align-items:center;gap:12px;display:flex}.official-gift-tools .searchbox{width:100%}.official-gift-tools>span{color:var(--muted);flex:none;font-size:11px;font-weight:750}.official-gift-categories{flex-wrap:wrap;gap:7px;display:flex}.official-gift-categories button{min-height:32px;color:var(--text-soft);background:var(--panel-subtle);border:1px solid var(--line-strong);font:inherit;cursor:pointer;border-radius:999px;align-items:center;gap:7px;padding:5px 10px;font-size:11px;font-weight:800;transition:color .15s,background .15s,border-color .15s,box-shadow .15s;display:inline-flex}.official-gift-categories button:hover{color:var(--brand);border-color:var(--brand-tint-border)}.official-gift-categories button.active{color:#fff;background:var(--brand);border-color:var(--brand);box-shadow:var(--shadow-brand)}.official-gift-categories button span{min-width:20px;height:20px;color:inherit;background:#7d8c9b38;border-radius:999px;place-items:center;padding:0 5px;font-size:10px;display:grid}.official-gift-categories button.active span{color:var(--brand);background:#ffffffd9}.official-gift-list{border:1px solid var(--line);border-radius:var(--radius-lg);background:var(--panel-subtle);scrollbar-gutter:stable;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;min-height:126px;max-height:314px;padding:8px;display:grid;overflow:auto}.official-gift-option{text-align:left;min-width:0;color:var(--text);background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);cursor:pointer;box-shadow:var(--shadow-sm);gap:8px;padding:11px 12px;transition:border-color .15s,box-shadow .15s,transform .15s;display:grid}.official-gift-option:hover{border-color:var(--brand-tint-border);box-shadow:var(--shadow);transform:translateY(-1px)}.official-gift-option.selected{border-color:var(--brand);box-shadow:0 0 0 2px var(--focus), var(--shadow)}.official-gift-option-head{grid-template-columns:minmax(0,1fr) auto;align-items:baseline;gap:8px;display:grid}.official-gift-option-head strong{text-overflow:ellipsis;white-space:nowrap;font-size:12px;overflow:hidden}.official-gift-option-head .mono{color:var(--muted);font-size:9px}.official-gift-option-meta{color:var(--muted);flex-wrap:wrap;gap:10px;font-size:10px;font-weight:700;display:flex}.official-gift-capabilities{flex-wrap:wrap;gap:5px;display:flex}.official-gift-capabilities>span{letter-spacing:.01em;border:1px solid #0000;border-radius:999px;padding:3px 7px;font-size:9px;font-weight:850}.official-gift-capabilities>span.yes{color:var(--good);background:var(--good-tint);border-color:var(--good-border)}.official-gift-capabilities>span.craft{color:var(--purple);background:var(--purple-tint);border-color:var(--purple-border)}.official-gift-capabilities>span.no{color:var(--muted);background:var(--panel-strong);border-color:var(--line-strong)}.official-gift-empty{min-height:108px;color:var(--muted);text-align:center;grid-column:1/-1;place-items:center;padding:20px;font-size:12px;display:grid}.official-gift-selected{border:1px solid var(--line);border-radius:var(--radius-lg);background:var(--surface-soft);grid-template-columns:108px minmax(0,1fr);align-items:center;gap:14px;padding:12px;display:grid}.official-gift-selected .gift-animation-shell{border-radius:12px;width:96px;height:96px;min-height:96px;overflow:hidden}.official-gift-selected .gift-animation{width:96px;height:96px}.official-gift-selected>div:last-child{gap:5px;min-width:0;display:grid}.official-gift-selected small{color:var(--muted)}.gift-import-note{color:var(--muted);justify-content:space-between;align-items:center;gap:12px;line-height:1.45;display:flex}.gift-file-picker{min-height:78px;color:var(--text);background:var(--panel);border:1px dashed var(--line-strong);border-radius:var(--radius);cursor:pointer;grid-template-columns:42px minmax(0,1fr) auto;align-items:center;gap:12px;padding:12px 14px;transition:border-color .16s,background .16s,box-shadow .16s;display:grid;position:relative}.gift-file-picker:hover,.gift-file-picker.has-file{background:var(--brand-tint);border-color:var(--brand);box-shadow:0 0 0 2px var(--focus)}.gift-file-picker.compact{grid-template-columns:minmax(0,1fr);min-height:44px;padding:8px 12px}.gift-file-picker input{opacity:0;pointer-events:none;width:1px;height:1px;position:absolute}.gift-file-icon{border-radius:var(--radius-sm);width:40px;height:40px}.gift-file-copy{gap:2px;min-width:0;display:grid}.gift-field-label{color:var(--muted);text-transform:uppercase;letter-spacing:.04em;font-size:10px;font-weight:800}.gift-file-copy strong{color:var(--heading);text-overflow:ellipsis;white-space:nowrap;font-size:13px;overflow:hidden}.gift-file-copy small{color:var(--muted);font-size:11px;font-weight:500}.gift-file-action{color:var(--brand);background:var(--brand-tint);border:1px solid var(--brand-tint-border);border-radius:var(--radius-sm);padding:7px 10px;font-size:11px;font-weight:800}.gif-catalog-preview{background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius-sm);place-items:center;max-height:220px;display:grid;overflow:hidden}.gif-catalog-preview img,.gif-catalog-preview video{object-fit:contain;max-width:100%;max-height:220px}.gift-fields-grid{grid-template-columns:minmax(200px,1.5fr) repeat(3,minmax(120px,1fr));gap:10px;display:grid}.gift-fields-grid label,.gift-reason-field{color:var(--muted);gap:6px;font-size:11px;font-weight:700;display:grid}.gift-fields-grid input,.gift-reason-field input{width:100%;min-width:0;height:38px;color:var(--text);background:var(--input-bg);border:1px solid var(--line);border-radius:var(--radius-sm);padding:0 10px}.gift-fields-grid input:focus,.gift-reason-field input:focus{border-color:var(--brand);box-shadow:0 0 0 3px var(--focus);outline:none}.gift-switch{color:var(--text-soft);cursor:pointer;align-items:center;gap:9px;font-size:12px;font-weight:700;display:inline-flex}.gift-switch input{opacity:0;width:1px;height:1px;position:absolute}.gift-switch-track{background:var(--switch-track);border-radius:999px;align-items:center;width:34px;height:19px;padding:2px;transition:background .16s;display:flex}.gift-switch-track span{background:#fff;border-radius:50%;width:15px;height:15px;transition:transform .16s;box-shadow:0 1px 3px #10182838}.gift-switch input:checked+.gift-switch-track{background:var(--brand)}.gift-switch input:checked+.gift-switch-track span{transform:translate(15px)}.gift-switch input:focus-visible+.gift-switch-track{outline:3px solid var(--focus);outline-offset:2px}.gift-validation{color:var(--code-text);background:var(--code-bg);border:1px solid var(--code-border);border-radius:var(--radius-sm);overflow:hidden}.gift-validation-head{color:var(--code-text);background:#ffffff09;border-bottom:1px solid #ffffff17;align-items:center;gap:9px;padding:10px 12px;display:flex}.gift-validation-head div{gap:2px;display:grid}.gift-validation-head span{color:var(--brand);font-size:10px}.gift-validation pre{max-height:180px;color:var(--code-text);margin:0;padding:11px 12px;font-size:11px;overflow:auto}.sticker-preview-modal{width:min(760px,100%)}.sticker-doc-grid{grid-template-columns:repeat(auto-fill,minmax(84px,1fr));gap:8px;max-height:420px;padding:2px;display:grid;overflow:auto}.sticker-doc-cell{aspect-ratio:1;background:var(--panel-strong);border:1px solid var(--line);border-radius:10px;place-items:center;display:grid;position:relative;overflow:hidden}.sticker-doc-canvas{width:100%;height:100%}.sticker-doc-canvas canvas{width:100%!important;height:100%!important}.sticker-doc-image{object-fit:contain;width:100%;height:100%}.sticker-doc-cell.list-thumb{flex:0 0 40px;width:40px}.sticker-list-thumb-empty{background:var(--panel-strong);border:1px solid var(--line);width:40px;height:40px;color:var(--muted);border-radius:9px;place-items:center;display:grid}.gif-catalog-thumb{object-fit:cover;background:var(--panel-strong);border:1px solid var(--line);border-radius:9px;width:40px;height:40px}.sticker-doc-grid-cell{gap:4px;display:grid}.sticker-doc-grid-cell .btn{justify-content:center;width:100%}.sticker-add-form{background:var(--panel-strong);border:1px solid var(--line);border-radius:10px;flex-wrap:wrap;align-items:center;gap:8px;margin-bottom:14px;padding:10px;display:flex}.sticker-add-form .gift-file-picker.compact{flex:220px;min-width:180px}.sticker-add-form .small-input{flex:0 140px}.sticker-add-form-error{color:var(--danger);flex-basis:100%;font-size:12px}.sticker-doc-error{color:var(--danger);text-align:center;place-items:center;padding:4px;font-size:9px;display:grid;position:absolute;inset:0}.gift-animation-shell{background:var(--surface-soft);place-items:center;min-height:210px;display:grid;position:relative}.gift-animation{width:200px;height:200px}.gift-animation canvas{width:100%!important;height:100%!important}.gift-play{width:30px;height:30px;color:var(--text);background:var(--panel);border:1px solid var(--line);border-radius:50%;place-items:center;display:grid;position:absolute;bottom:8px;right:8px}.gift-table-wrap{background:var(--panel)}.gift-table{min-width:1080px}.gift-table th:nth-child(2){width:74px}.gift-table td{vertical-align:middle}.gift-select-col{text-align:center;width:34px}.gift-select-col input{width:15px;height:15px}.avatar-col{width:44px}.muted-cell{color:var(--muted)}.avatar-photo-img,.avatar-fallback{object-fit:cover;border-radius:50%;display:block}.avatar-fallback{color:#fff;letter-spacing:-.02em;place-items:center;font-weight:800;display:grid}.gift-bulk-toolbar{background:var(--panel-strong);border:1px solid var(--line);border-radius:9px;align-items:center;gap:10px;margin-bottom:10px;padding:9px 12px;display:flex}.gift-bulk-count{color:var(--text);white-space:nowrap;font-size:12px;font-weight:700}.gift-bulk-reason{flex:1;min-width:160px}.gift-bulk-reason input{height:34px}.gift-bulk-error{color:var(--danger);font-size:11px;font-weight:700}.gift-page-size{color:var(--muted);white-space:nowrap;align-items:center;gap:6px;font-size:11px;font-weight:700;display:inline-flex}.gift-page-size select{height:30px;color:var(--text);background:var(--input-bg);border:1px solid var(--line);border-radius:var(--radius-sm);font:inherit;padding:0 8px;font-weight:700}.gift-pager{flex-wrap:wrap;justify-content:space-between;align-items:center;gap:12px;margin-top:10px;display:flex}.gift-pager-range{color:var(--muted);font-size:11px;font-weight:700}.gift-pager-controls{align-items:center;gap:10px;display:flex}.gift-pager-page{color:var(--text);white-space:nowrap;font-size:12px;font-weight:700}.gift-animation-shell.compact{border:1px solid var(--line);border-radius:var(--radius-sm);width:56px;min-height:56px;overflow:hidden}.gift-animation-shell.compact .gift-animation{width:54px;height:54px}.gift-animation-shell.compact .gift-play{width:20px;height:20px;bottom:3px;right:3px}.gift-row-disabled{opacity:.68}.gift-table-title,.gift-sort-order,.gift-source-size,.gift-convert-price{display:block}.gift-table-title{text-overflow:ellipsis;white-space:nowrap;max-width:220px;overflow:hidden}.gift-sort-order,.gift-source-size,.gift-convert-price{color:var(--muted);margin-top:3px;font-size:10px}.gift-table-price{color:var(--warn)}.gift-table-actions{align-items:center;gap:6px;display:flex}.collectible-button{color:var(--purple);background:var(--purple-tint);border-color:var(--purple-border)}.collectible-button:hover{background:var(--purple-tint);border-color:var(--purple)}.collectible-modal{width:min(1180px,100%);max-height:min(92vh,980px)}.collectible-modal .modal-head p{color:var(--muted);margin:4px 0 0;font-size:11px}.collectible-modal-body{background:var(--bg);gap:16px;padding:16px 18px 22px;overflow:auto}.collectible-loading{min-height:90px;color:var(--muted);justify-content:center;align-items:center;gap:8px;display:flex}.collectible-empty{color:var(--purple-text);background:var(--purple-tint);border:1px dashed var(--purple-border);border-radius:var(--radius);align-items:center;gap:12px;padding:16px;display:flex}.collectible-empty div,.collectible-definition-head>div:first-child,.collectible-section-head>div:first-child{gap:3px;display:grid}.collectible-empty span,.collectible-definition-head span,.collectible-section-head span{color:var(--muted);font-size:10px;font-weight:500}.collectible-active{background:var(--panel);border:1px solid var(--purple-border);border-radius:var(--radius);box-shadow:var(--shadow-sm);overflow:hidden}.collectible-active-head{background:var(--purple-tint);border-bottom:1px solid var(--purple-border);justify-content:space-between;align-items:center;gap:12px;padding:12px 14px;display:flex}.collectible-active-head>div{color:var(--purple-text);align-items:center;gap:9px;display:flex}.collectible-active-head>div>div{gap:2px;display:grid}.collectible-active-head span{color:var(--muted);font-size:10px}.collectible-active-grid{background:var(--line);grid-template-columns:repeat(auto-fill,minmax(145px,1fr));gap:1px;display:grid}.collectible-active-grid article{background:var(--panel);align-items:center;gap:9px;min-width:0;padding:9px 11px;display:flex}.collectible-active-grid article>div:last-child{gap:2px;min-width:0;display:grid}.collectible-active-grid article strong{text-overflow:ellipsis;white-space:nowrap;font-size:11px;overflow:hidden}.collectible-active-grid article span{color:var(--muted);font-size:9px}.collectible-definition{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);box-shadow:var(--shadow-sm);overflow:hidden}.collectible-definition-head{background:var(--panel-subtle);border-bottom:1px solid var(--line);justify-content:space-between;align-items:center;gap:12px;padding:14px 16px;display:flex}.collectible-main-fields{background:var(--panel-subtle);border-bottom:1px solid var(--line);padding:14px 16px}.collectible-section{border-bottom:1px solid var(--line);padding:14px 16px}.collectible-section:last-child{border-bottom:0}.collectible-section-head{justify-content:space-between;align-items:center;gap:12px;margin-bottom:10px;display:flex}.collectible-section-tools{align-items:center;gap:7px;display:flex}.collectible-rows{gap:7px;display:grid}.collectible-row{background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius-sm);align-items:end;gap:7px;padding:9px 9px 9px 36px;display:grid;position:relative}.collectible-row:hover{background:var(--panel);border-color:var(--line-strong);box-shadow:var(--shadow-sm)}.collectible-row.animated{grid-template-columns:minmax(120px,1.2fr) 90px 78px minmax(160px,1.4fr) 48px 30px}.collectible-row.backdrop{grid-template-columns:minmax(110px,1.2fr) 70px 80px 70px repeat(4,52px) 48px 30px}.collectible-row-index{width:27px;color:var(--purple-text);background:var(--purple-tint);border-right:1px solid var(--purple-border);border-radius:var(--radius-xs) 0 0 var(--radius-xs);place-items:center;font-size:10px;font-weight:800;display:grid;position:absolute;top:0;bottom:0;left:0}.collectible-row label{gap:4px;min-width:0;display:grid}.collectible-row label>span{color:var(--muted);text-transform:uppercase;letter-spacing:.025em;font-size:9px;font-weight:800}.collectible-row input:not([type=file]){width:100%;min-width:0;height:32px;color:var(--text);background:var(--input-bg);border:1px solid var(--line-strong);border-radius:var(--radius-sm);font:inherit;padding:0 8px;font-size:11px}.collectible-row input:focus{border-color:var(--purple);box-shadow:0 0 0 3px var(--purple-tint);outline:none}.collectible-file input{opacity:0;pointer-events:none;width:1px;height:1px;position:absolute}.collectible-file em{min-width:0;height:32px;color:var(--purple-text);background:var(--purple-tint);border:1px dashed var(--purple-border);border-radius:var(--radius-sm);text-overflow:ellipsis;white-space:nowrap;cursor:pointer;align-items:center;gap:5px;padding:0 8px;font-size:10px;font-style:normal;font-weight:700;display:flex;overflow:hidden}.collectible-inline-preview{width:42px;height:42px;color:var(--purple);background:var(--purple-tint);border:1px solid var(--purple-border);border-radius:var(--radius-sm);place-items:center;display:grid;overflow:hidden}.collectible-animation{width:100%;height:100%;overflow:hidden}.collectible-animation.compact{background:var(--purple-tint);border:1px solid var(--purple-border);border-radius:var(--radius-sm);flex:0 0 42px;place-items:center;width:42px;height:42px;display:grid}.collectible-animation canvas{width:100%!important;height:100%!important}.collectible-animation.failed{color:var(--danger);background:var(--danger-tint)}.collectible-animation.loading{color:var(--purple-text)}.collectible-file-error{color:var(--danger);grid-column:1/-1;font-size:10px}.collectible-color input{cursor:pointer;height:32px!important;padding:3px!important}.collectible-backdrop-preview{border-radius:var(--radius-sm);border:1px solid #2a1f472e;flex:0 0 42px;place-items:center;width:42px;height:42px;font-size:11px;font-weight:900;display:grid;box-shadow:inset 0 0 0 1px #fff3}.collectible-row .icon-btn{align-self:center}.collectible-row .icon-btn:disabled{opacity:.28}@media (width<=900px){.gift-fields-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.give-gift-layout{grid-template-columns:1fr}.give-gift-picker-list{max-height:320px}.collectible-row.animated,.collectible-row.backdrop{grid-template-columns:repeat(2,minmax(0,1fr))}.collectible-inline-preview,.collectible-backdrop-preview,.collectible-row .icon-btn{place-self:center start}}@media (width<=620px){.gift-import-note{flex-direction:column;align-items:flex-start}.gift-format-chips{justify-content:flex-start}.gift-file-picker{grid-template-columns:40px minmax(0,1fr)}.gift-file-action{display:none}.gift-fields-grid{grid-template-columns:1fr}.gift-list-summary{width:100%;margin-left:0}.official-gift-tools{flex-direction:column;align-items:stretch}.official-gift-list{grid-template-columns:1fr;max-height:340px}.official-gift-selected{grid-template-columns:82px minmax(0,1fr)}.official-gift-selected .gift-animation-shell{width:72px;height:72px}.collectible-modal-body{padding:10px}.collectible-definition-head,.collectible-section-head{flex-direction:column;align-items:flex-start}.collectible-row.animated,.collectible-row.backdrop{grid-template-columns:1fr}.collectible-active-grid{grid-template-columns:1fr 1fr}}.attr-block{background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius-sm);gap:8px;padding:10px;display:grid}.attr-block+.attr-block{margin-top:10px}.attr-block .duration-field input,.duration-field select{width:100%}.attr-block .btn{justify-content:center;width:100%}.emoji-grid{grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:10px;display:grid}.emoji-card{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);box-shadow:var(--shadow-sm);gap:8px;padding:12px;display:grid}.emoji-preview{background:var(--surface-soft);border:1px solid var(--line);border-radius:var(--radius-sm);place-items:center;height:88px;display:grid}.emoji-anim{width:80px;height:80px}.emoji-anim canvas{width:100%!important;height:100%!important}.emoji-glyph{font-size:46px;line-height:1}.emoji-meta{gap:4px;min-width:0;display:grid}.emoji-alt{font-size:18px;line-height:1.2}.emoji-id{width:100%;min-width:0;color:var(--text);background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius-sm);cursor:pointer;justify-content:space-between;align-items:center;gap:6px;padding:4px 8px;font-size:11px;display:flex}.emoji-id .mono{text-overflow:ellipsis;white-space:nowrap;flex:auto;min-width:0;overflow:hidden}.emoji-id svg{flex:none}.emoji-id:hover{border-color:var(--brand-tint-border);color:var(--brand)}.emoji-sub{color:var(--muted);text-overflow:ellipsis;white-space:nowrap;font-size:11px;overflow:hidden}.username-branch{margin:2px 0 0;padding:0;list-style:none}.username-branch li{color:var(--text-soft);padding-left:14px;font-size:12px;line-height:1.7;position:relative}.username-branch li:before{border-left:1px solid var(--line-strong,var(--line));border-bottom:1px solid var(--line-strong,var(--line));content:"";width:6px;height:11px;position:absolute;top:0;left:3px}.username-branch li.inactive{color:var(--muted)}.username-branch li.inactive span{text-decoration:line-through}.username-branch li em{text-transform:uppercase;letter-spacing:.04em;margin-left:6px;font-size:10px;font-style:normal;font-weight:800}.card-body{flex-direction:column;gap:12px;display:flex}.server-identity-fields{flex:auto;gap:8px;min-width:0;display:grid}.identity-card{width:100%}.identity-layout{align-items:flex-start;gap:24px;display:flex}.identity-layout .server-identity-fields{flex:auto;gap:12px}.identity-layout .form-field textarea{resize:vertical;min-height:92px}.identity-save-row .btn{justify-content:center;width:100%;min-height:40px}.server-icon-fallback{color:var(--muted);background:var(--panel-subtle);border:1px dashed var(--line-strong)}.env-groups{gap:8px;display:grid}.env-group{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);overflow:hidden}.env-group-toggle{background:var(--panel-subtle);cursor:pointer;text-align:left;border:none;justify-content:space-between;align-items:center;gap:10px;width:100%;padding:11px 14px;transition:background-color .14s;display:flex}.env-group-toggle:hover{background:var(--brand-tint)}.env-group-toggle-text{align-items:baseline;gap:8px;min-width:0;display:flex}.env-group-toggle-title{color:var(--heading);font-size:13px;font-weight:800}.env-group-toggle-count{color:var(--muted);flex-shrink:0;font-size:11px;font-weight:700}.env-group-chevron{color:var(--muted);flex-shrink:0;transition:transform .14s}.env-group.open .env-group-chevron{transform:rotate(180deg)}.env-group-body{border-top:1px solid var(--line);gap:12px;padding:14px;display:grid}.env-group-desc{color:var(--muted);margin:0;font-size:12px}.env-field .env-field-desc{color:var(--muted);text-transform:none;letter-spacing:normal;font-size:11px;font-weight:500}.env-save-row{margin-top:12px}.restart-overlay{background:var(--panel);background-image:radial-gradient(220px 140px at 50% 0%, var(--brand-tint) 0%, #0000 75%);width:min(340px,100%);overflow:hidden}.restart-overlay .restart-overlay-body{text-align:center;justify-items:center;gap:6px;padding:36px 24px 32px;display:grid}.restart-overlay-badge{width:56px;height:56px;color:var(--brand);background:var(--brand-tint);border:1px solid var(--brand-tint-border);box-shadow:0 0 0 8px var(--brand-tint);border-radius:999px;place-items:center;margin-bottom:10px;display:grid}.restart-overlay-badge.warn{color:var(--warn);background:var(--warn-tint);border-color:var(--warn-border);box-shadow:0 0 0 8px var(--warn-tint)}.restart-overlay-spin{animation:1.4s linear infinite spin}.restart-overlay-heading{color:var(--heading);margin:0;font-size:18px}.restart-overlay-body p{color:var(--muted);margin:0}.restart-overlay-progress{width:min(220px,100%);margin-top:18px}.restart-overlay-actions{justify-content:center}.tab-bar{background:var(--surface-soft);border:1px solid var(--line);border-radius:var(--radius);gap:4px;width:fit-content;margin-bottom:18px;padding:4px;display:flex}.tab-btn{appearance:none;color:var(--text-soft);border-radius:var(--radius-sm);cursor:pointer;background:0 0;border:none;padding:7px 16px;font-size:13px;font-weight:600;transition:background .15s,color .15s}.tab-btn:hover{color:var(--text)}.tab-btn.active{background:var(--panel);color:var(--text);box-shadow:0 1px 2px #00000014}.service-grid{grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:10px;display:grid}.service-card{border:1px solid var(--line);border-radius:var(--radius);background:var(--panel);align-items:center;gap:10px;padding:12px 14px;display:flex}.service-card-icon{border-radius:var(--radius-sm);background:var(--surface-soft);width:34px;height:34px;color:var(--text-soft);flex:none;justify-content:center;align-items:center;display:flex}.service-card-body{flex:1;min-width:0}.service-card-name{color:var(--text);text-transform:capitalize;font-size:13px;font-weight:700}.service-card-detail{color:var(--muted);margin-top:1px;font-family:SFMono-Regular,Consolas,Liberation Mono,monospace;font-size:11.5px}.service-card-status{text-transform:capitalize;border-radius:999px;flex:none;align-items:center;gap:5px;padding:4px 9px;font-size:12px;font-weight:700;display:flex}.service-card.tone-good .service-card-icon{color:var(--good)}.service-card.tone-good .service-card-status{color:var(--good);background:var(--good-tint);border:1px solid var(--good-border)}.service-card.tone-warn .service-card-icon{color:var(--warn)}.service-card.tone-warn .service-card-status{color:var(--warn);background:var(--warn-tint);border:1px solid var(--warn-border)}.service-card.tone-danger .service-card-icon{color:var(--danger)}.service-card.tone-danger .service-card-status{color:var(--danger);background:var(--danger-tint);border:1px solid var(--danger-border)}.service-card.tone-idle .service-card-status{color:var(--muted);background:var(--surface-soft);border:1px solid var(--line)}.services-header-actions{align-items:center;gap:8px;display:flex}.message-view{gap:10px;display:grid}.message-view-head{justify-content:space-between;align-items:flex-start;gap:12px;display:flex}.message-view-head strong{color:var(--heading);font-size:14px;display:block}.message-view-head small{color:var(--muted);margin-top:2px;font-size:12px;display:block}.message-bubble{background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius-lg);border-top-left-radius:var(--radius-xs);gap:10px;max-width:720px;padding:14px 16px;display:grid}.message-text{overflow-wrap:anywhere;color:var(--text);white-space:pre-wrap;margin:0;font-size:14px;line-height:1.5}.message-text.empty{color:var(--muted-2);font-style:italic}.message-attachment{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius-sm);align-items:center;gap:10px;padding:10px;display:flex}.message-attachment-icon{width:30px;height:30px;color:var(--brand);background:var(--brand-tint);border:1px solid var(--brand-tint-border);border-radius:var(--radius-sm);flex:none;place-items:center;display:grid}.message-attachment-copy{gap:1px;min-width:0;display:grid}.message-attachment-copy strong{color:var(--text);font-size:13px}.message-attachment-copy small{color:var(--muted);text-overflow:ellipsis;white-space:nowrap;font-size:11px;overflow:hidden}.message-flags{flex-wrap:wrap;gap:4px;display:flex}.raw-details>summary{color:var(--text-soft);background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius-sm);cursor:pointer;padding:8px 10px;font-size:12px;font-weight:700;list-style:none}.raw-details>summary::-webkit-details-marker{display:none}.raw-details>summary:before{content:"▸ ";color:var(--muted)}.raw-details[open]>summary:before{content:"▾ "}.raw-details[open]>summary{margin-bottom:12px}.modal-backdrop{z-index:10000;background:var(--overlay);-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);place-items:center;padding:24px;display:grid;position:fixed;inset:0}.modal{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius-lg);width:min(760px,100%);max-height:min(820px,100vh - 48px);box-shadow:var(--shadow);padding:0;overflow:hidden}.command-modal{flex-direction:column;display:flex}.command-modal>.modal-head,.command-modal>.modal-actions{flex:none}.modal-head{border-bottom:1px solid var(--line);justify-content:space-between;align-items:flex-start;gap:12px;padding:16px 18px 12px;display:flex}.icon-btn{width:30px;height:30px;color:var(--text-soft);background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius-sm);cursor:pointer;place-items:center;transition:background-color .14s,border-color .14s,color .14s;display:grid}.icon-btn:hover{background:var(--btn-hover);border-color:var(--line-strong)}.command-steps{grid-template-columns:repeat(3,minmax(0,1fr));gap:8px;display:grid}.add-server-link-modal{width:min(920px,100%);max-height:min(880px,100vh - 48px)}.add-server-link-field{background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius-md);gap:8px;padding:14px;display:grid}.add-server-link-field textarea{word-break:break-all;resize:vertical;font-family:SFMono-Regular,Consolas,Liberation Mono,monospace;font-size:12px}.add-server-link-hint{color:var(--muted-2);margin:0;font-size:12.5px}.command-body{grid-auto-rows:max-content;gap:12px;min-height:0;padding:14px 18px;display:grid;overflow:auto}.mint-field-group{background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius);gap:8px;padding:12px;display:grid}.mint-field-group-label{color:var(--text-soft);text-transform:uppercase;letter-spacing:.04em;font-size:12px;font-weight:800}.command-step{min-height:38px;color:var(--muted);background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius-sm);align-items:center;gap:8px;padding:0 10px;display:flex}.command-step span{background:var(--panel);border:1px solid var(--line);border-radius:999px;place-items:center;width:20px;height:20px;font-size:11px;font-weight:800;display:grid}.command-step.active{color:var(--brand);border-color:var(--brand-tint-border)}.command-step.done{color:var(--good);border-color:var(--good-border)}.form-field{gap:6px;display:grid}.form-field span,.form-stack span{color:var(--text-soft);font-weight:800}.form-field input:disabled,.form-field textarea:disabled{opacity:.6;cursor:not-allowed}.command-preview{gap:8px;display:grid}.command-preview .json-block{max-height:150px}.preview-head,.result-title{color:var(--text-soft);align-items:center;gap:7px;font-weight:800;display:flex}.result-box{background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius);gap:8px;padding:10px;display:grid}.result-line{grid-template-columns:92px minmax(0,1fr);gap:8px;display:grid}.result-line span{color:var(--muted)}.result-line strong{overflow-wrap:anywhere}.result-message{color:var(--text-soft)}.secret-reveal-label{color:var(--warn);font-size:12px;font-weight:800}.secret-reveal-value{color:var(--text-soft);letter-spacing:.12em;background:var(--panel);border:1px solid var(--line);border-radius:var(--radius-sm);text-overflow:ellipsis;white-space:nowrap;flex:auto;padding:6px 10px;font-size:13px;overflow:hidden}.modal-actions{background:var(--panel);border-top:1px solid var(--line);justify-content:flex-end;padding:12px 18px}.login-page{background:var(--bg);background-image:radial-gradient(900px 480px at 50% -8%, var(--hero-glow) 0%, #0000 70%), linear-gradient(var(--hero-grid) 1px, transparent 1px), linear-gradient(90deg, var(--hero-grid) 1px, transparent 1px);background-size:auto,44px 44px,44px 44px;place-items:center;min-height:100vh;padding:24px;display:grid;position:relative;overflow:hidden}.bg-orbs{z-index:0;pointer-events:none;will-change:transform;transition:transform .3s ease-out;position:absolute;inset:-60px}.bg-orb{filter:blur(100px);pointer-events:none;border-radius:50%;position:absolute}.bg-orb--1{background:color-mix(in srgb, var(--brand-2) 40%, transparent);width:700px;height:700px;animation:20s ease-in-out infinite loginOrbFloat1;top:-15%;left:-10%}.bg-orb--2{background:color-mix(in srgb, var(--brand) 38%, transparent);width:600px;height:600px;animation:24s ease-in-out infinite loginOrbFloat2;top:25%;right:-15%}.bg-orb--3{background:color-mix(in srgb, var(--brand-2) 30%, transparent);width:500px;height:500px;animation:28s ease-in-out infinite loginOrbFloat3;bottom:-15%;left:30%}@keyframes loginOrbFloat1{0%,to{transform:translate(0)scale(1)}33%{transform:translate(60px,-40px)scale(1.08)}66%{transform:translate(-30px,30px)scale(.92)}}@keyframes loginOrbFloat2{0%,to{transform:translate(0)scale(1)}33%{transform:translate(-50px,-35px)scale(.93)}66%{transform:translate(45px,25px)scale(1.07)}}@keyframes loginOrbFloat3{0%,to{transform:translate(0)scale(1)}33%{transform:translate(40px,45px)scale(1.06)}66%{transform:translate(-55px,-25px)scale(.94)}}@media (width<=720px){.bg-orb{filter:blur(60px)}.bg-orb--1{width:350px;height:350px}.bg-orb--2{width:300px;height:300px}.bg-orb--3{width:250px;height:250px}}@media (prefers-reduced-motion:reduce){.bg-orb{animation:none}.bg-orbs{transition:none}}.login-page .login-panel{z-index:1;position:relative}.login-panel{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius-lg);width:min(480px,100%);box-shadow:var(--shadow);gap:22px;padding:30px;font-size:15px;display:grid}.login-head{justify-content:space-between;align-items:center;gap:12px;display:flex}.login-head-actions{flex-wrap:wrap;justify-content:flex-end;align-items:center;gap:8px;display:flex}.login-head-actions .icon-btn{width:36px;height:36px}.login-panel .brand-mark{width:44px;height:44px}.login-panel .brand strong{font-size:18px}.login-panel .brand small{margin-top:4px;font-size:13px}.login-chip{min-height:24px;color:var(--brand);background:var(--brand-tint);border:1px solid var(--brand-tint-border);border-radius:999px;align-items:center;padding:0 8px;font-size:12px;display:inline-flex}.form-stack{gap:12px;display:grid}.form-stack label{gap:6px;display:grid}.form-stack input{width:100%}.login-panel .form-stack input{height:46px;padding:0 14px}.login-panel .btn{min-height:44px;font-size:15px}.login-wizard{overflow:hidden}.login-wizard-track{transition:transform .22s;display:flex}.login-wizard-step{flex:0 0 100%;min-width:0}.login-wizard-actions{gap:10px;display:flex}.login-wizard-actions .btn.primary{flex:auto}@media (prefers-reduced-motion:reduce){.login-wizard-track{transition:none}}.setup-wizard-panel{width:min(640px,100%);max-height:min(760px,100vh - 48px);overflow-y:auto}.wizard-steps{flex-wrap:wrap;gap:6px;display:flex}.wizard-steps .command-step{flex:1 1 0;justify-content:center;min-width:0;padding:0 6px;font-size:12.5px}.wizard-steps .command-step strong{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.wizard-step-body{gap:14px;display:grid}.wizard-step-body>p{color:var(--text-soft);margin:0}.wizard-step-body>p.wizard-step-hint{color:var(--muted);font-size:13px}.wizard-step-body>p.wizard-welcome-greeting{color:var(--heading);font-size:24px;font-weight:800}.wizard-identity-row{align-items:flex-start;gap:16px;display:flex}.wizard-identity-row .server-identity-fields{flex:auto;gap:10px;display:grid}.wizard-actions{justify-content:flex-end;gap:10px;display:flex}@media (width<=560px){.wizard-steps .command-step strong{display:none}.wizard-identity-row{flex-direction:column}}.boot-screen{background:var(--bg);align-content:center;place-items:center;gap:18px;min-height:100vh;display:grid}.loader-bar{background:var(--line-strong);border-radius:999px;width:180px;height:4px;overflow:hidden}.loader-bar:before{content:"";background:var(--brand);width:42%;height:100%;animation:1s ease-in-out infinite load;display:block}.spin{animation:.8s linear infinite spin}@keyframes load{0%{transform:translate(-120%)}to{transform:translate(260%)}}@keyframes spin{to{transform:rotate(360deg)}}.secret-reveal{background:var(--warn-tint);border:1px solid var(--warn-border);border-radius:var(--radius);gap:6px;padding:10px;display:grid}.secret-reveal-label{color:var(--warn);align-items:center;gap:6px;font-size:12px;font-weight:800;display:flex}.secret-reveal-row{align-items:center;gap:10px;display:flex}.secret-reveal-value{color:var(--text-soft);letter-spacing:.12em;background:var(--panel);border:1px solid var(--line);border-radius:var(--radius-sm);text-overflow:ellipsis;white-space:nowrap;flex:auto;padding:6px 10px;overflow:hidden}.bg-icons{z-index:0;color:var(--brand-2);pointer-events:none;position:absolute;inset:0;overflow:hidden}.bg-icon{will-change:transform;position:absolute;top:0;left:0}.app-background{z-index:0;pointer-events:none;position:absolute;inset:0;overflow:hidden}.app-background--workspace{position:fixed}.app-background--workspace .bg-icons{opacity:.55}.app-background--workspace .bg-orb{opacity:.6}.shell>.sidebar{z-index:2}.workspace>.content{z-index:1;position:relative}@media (width<=1120px){.shell{grid-template-columns:1fr}.sidebar{height:auto;position:static}.nav-list{grid-template-columns:repeat(4,minmax(0,1fr))}.sidebar-status{display:none}.overview-band,.split-layout,.operation-row,.raw-grid,.message-selector-grid,.message-selector-grid.single{grid-template-columns:1fr}.action-dock{position:static}.action-groups{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (width<=760px){.content,.topbar{padding-left:14px;padding-right:14px}.command-grid,.work-strip,.overview-metrics,.metric-row,.summary-grid,.command-steps,.action-groups{grid-template-columns:1fr}.sidebar{gap:12px;padding:14px}.nav-list{grid-template-columns:repeat(2,minmax(0,1fr))}.topbar,.page-title-row,.entity-head{flex-direction:column;align-items:flex-start}input,.searchbox{width:100%}.toolbar{align-items:stretch}.picker-row,.selected-entity{grid-template-columns:1fr}} diff --git a/cmd/telesrv-admin/web/dist/index.html b/cmd/telesrv-admin/web/dist/index.html index 9132c692..1cc997de 100644 --- a/cmd/telesrv-admin/web/dist/index.html +++ b/cmd/telesrv-admin/web/dist/index.html @@ -1,32 +1,32 @@ - - - - - - - OwpenGram Admin - - - - - - -
- - + + + + + + + OwpenGram Admin + + + + + + +
+ + diff --git a/cmd/telesrv-admin/web/package-lock.json b/cmd/telesrv-admin/web/package-lock.json index 18e4c2de..a74b0b78 100644 --- a/cmd/telesrv-admin/web/package-lock.json +++ b/cmd/telesrv-admin/web/package-lock.json @@ -758,9 +758,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.16", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", - "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", "dev": true, "funding": [ { diff --git a/cmd/telesrv-admin/web/src/App.tsx b/cmd/telesrv-admin/web/src/App.tsx index b3f76990..3d799981 100644 --- a/cmd/telesrv-admin/web/src/App.tsx +++ b/cmd/telesrv-admin/web/src/App.tsx @@ -1,8 +1,9 @@ import { useEffect, useState } from "react"; import { api } from "./api"; import { BootScreen, Shell } from "./components/Layout"; +import { SetupWizard } from "./components/SetupWizard"; import { LoginPage } from "./pages/LoginPage"; -import { PermissionsProvider } from "./permissions"; +import { permissionAll, permissionServerManage, PermissionsProvider } from "./permissions"; import { Routes } from "./pages/Routes"; import { currentRoute, type RouteState } from "./routing"; import type { AdminSession } from "./types"; @@ -37,12 +38,39 @@ export function App() { } if (session === null) { - return ; + return ( + { + // A stale/expired session can be caught on any deep link (a + // bookmark, a page refresh mid-review), landing whoever it belongs + // to on the login form without them having navigated there -- + // replaceState rather than a plain navigate() so signing back in + // doesn't leave a "login" entry in browser history to land back on + // via Back. Every login opens on the dashboard, not wherever the + // expired session happened to be. + window.history.replaceState(null, "", "/"); + setRoute(currentRoute()); + setSession(next); + }} + /> + ); + } + + // The wizard only ever shows to whoever can actually act on it -- a + // limited operator signing in before setup is finished just sees the + // normal (mostly empty) shell instead of a wizard whose every step would + // 403. setup_completed undefined (an admin binary old enough to predate + // the field) reads as "done", same convention as the type's doc comment. + const canRunSetupWizard = (session.permissions ?? []).some( + (permission) => permission === permissionAll || permission === permissionServerManage + ); + if (session.setup_completed === false && canRunSetupWizard) { + return ; } return ( - setSession(null)}> + setSession(null)}> diff --git a/cmd/telesrv-admin/web/src/api.ts b/cmd/telesrv-admin/web/src/api.ts index c0cfc777..cd6181a7 100644 --- a/cmd/telesrv-admin/web/src/api.ts +++ b/cmd/telesrv-admin/web/src/api.ts @@ -1,4 +1,6 @@ import type { + PublicBranding, + AdminConsoleUserList, AccountDetail, AccountListResponse, AccountStatsResponse, @@ -24,6 +26,10 @@ import type { CollectibleUsernameListResponse, ReservedUsernameListResponse, CommandResult, + DockerService, + EnvGroup, + ServerIdentity, + ServerStatus, GroupMessageDetail, GroupMessageListResponse, MessageDetail, @@ -141,16 +147,24 @@ export function errorMessage(error: unknown): string { export const api = { session: () => request("/api/session"), - login: async (secret: string) => { + // Reachable before login: the sign-in screen says which server it belongs to. + publicBranding: () => request("/api/public/branding"), + publicIconURL: () => `/api/public/icon?t=${Date.now()}`, + // The built-in operator is named "owpengram" and is checked against the + // configured TELESRV_ADMIN_UI_PASSWORD / _TOKEN -- the break-glass login + // that still works when the database is unreachable. A blank username is + // rejected: there is no anonymous way in. + login: async (secret: string, username = "") => { const result = await request("/api/login", { method: "POST", - body: JSON.stringify({ secret }) + body: JSON.stringify({ username, secret }) }); // Stashed here rather than in the caller so no login path can forget it. rememberCSRFToken(result.csrf_token); return result; }, logout: () => request<{ ok: boolean }>("/api/logout", { method: "POST", body: "{}" }), + adminUsers: () => request("/api/admin-users"), accounts: (params: URLSearchParams) => request(`/api/accounts?${params.toString()}`), accountStats: () => request("/api/accounts/stats"), sharedDeviceGroups: (params: URLSearchParams) => request(`/api/accounts/shared-devices?${params.toString()}`), @@ -231,10 +245,19 @@ export const api = { gifCatalogDocumentPreviewURL: (documentID: string) => `/api/gif-catalog/documents/${encodeURIComponent(documentID)}/preview`, createStickerSet: (form: FormData) => request("/api/actions/create-sticker-set", { method: "POST", body: form }), setAccountAvatar: (form: FormData) => request("/api/actions/set-account-avatar", { method: "POST", body: form }), + setAccountAvatarVideo: (form: FormData) => request("/api/actions/set-account-avatar-video", { method: "POST", body: form }), setChannelAvatar: (form: FormData) => request("/api/actions/set-channel-avatar", { method: "POST", body: form }), addStickerToSet: (form: FormData) => request("/api/actions/add-sticker-to-set", { method: "POST", body: form }), gifCatalog: () => request("/api/gif-catalog"), createGifCatalogEntry: (form: FormData) => request("/api/actions/create-gif-catalog-entry", { method: "POST", body: form }), + serverIdentity: () => request("/api/server/identity"), + addServerLink: () => request<{ link: string }>("/api/server/add-server-link"), + uploadServerIcon: (form: FormData) => request("/api/actions/upload-server-icon", { method: "POST", body: form }), + serverIconURL: () => `/api/server/icon?t=${Date.now()}`, + serverEnv: () => request("/api/server/env"), + serverStatus: () => request("/api/server/status"), + dockerStatus: () => request("/api/server/docker-status"), + checkServerUpdates: () => request<{ commits_behind: number }>("/api/server/check-updates"), action: (path: string, payload: Record) => request(path, { method: "POST", body: JSON.stringify(payload) diff --git a/cmd/telesrv-admin/web/src/clipboard.ts b/cmd/telesrv-admin/web/src/clipboard.ts new file mode 100644 index 00000000..e218861f --- /dev/null +++ b/cmd/telesrv-admin/web/src/clipboard.ts @@ -0,0 +1,28 @@ +// navigator.clipboard only exists in a secure context (HTTPS, or localhost). +// This admin panel is frequently reached over a plain http:// LAN address +// (e.g. a self-hosted server's own IP), where navigator.clipboard is simply +// undefined -- calling .writeText on it throws "Cannot read properties of +// undefined". Fall back to the old execCommand('copy') path via a hidden, +// off-screen textarea, which still works in that case. +export async function copyToClipboard(text: string): Promise { + if (navigator.clipboard?.writeText) { + await navigator.clipboard.writeText(text); + return; + } + const textarea = document.createElement("textarea"); + textarea.value = text; + textarea.style.position = "fixed"; + textarea.style.top = "-1000px"; + textarea.style.left = "-1000px"; + document.body.appendChild(textarea); + textarea.focus(); + textarea.select(); + try { + const ok = document.execCommand("copy"); + if (!ok) { + throw new Error("Copy command was not successful"); + } + } finally { + document.body.removeChild(textarea); + } +} diff --git a/cmd/telesrv-admin/web/src/components/ActionButton.tsx b/cmd/telesrv-admin/web/src/components/ActionButton.tsx index f3a9ffe2..9bfb42e3 100644 --- a/cmd/telesrv-admin/web/src/components/ActionButton.tsx +++ b/cmd/telesrv-admin/web/src/components/ActionButton.tsx @@ -3,10 +3,11 @@ import type { ReactNode } from "react"; import { useMemo, useState } from "react"; import { createPortal } from "react-dom"; import { api, errorMessage } from "../api"; +import { copyToClipboard } from "../clipboard"; import type { CommandResult } from "../types"; import { Alert, JsonBlock } from "./ui"; -type ActionTone = "neutral" | "warn" | "danger"; +type ActionTone = "neutral" | "warn" | "danger" | "primary"; export function ActionButton({ label, @@ -77,7 +78,7 @@ export function ActionButton({ } const canConfirm = result?.dry_run && !result.error; - const triggerClass = `btn ${tone === "danger" ? "danger" : tone === "warn" ? "warn" : ""} ${compact ? "compact-btn" : ""}`; + const triggerClass = `btn ${tone === "danger" ? "danger" : tone === "warn" ? "warn" : tone === "primary" ? "primary" : ""} ${compact ? "compact-btn" : ""}`; const previewPayload = useMemo(() => { try { return payload(); @@ -94,7 +95,7 @@ export function ActionButton({ : result?.details; async function copySecret() { - await navigator.clipboard.writeText(secretValue); + await copyToClipboard(secretValue); setSecretCopied(true); } diff --git a/cmd/telesrv-admin/web/src/components/AddServerLinkModal.tsx b/cmd/telesrv-admin/web/src/components/AddServerLinkModal.tsx new file mode 100644 index 00000000..178ecba9 --- /dev/null +++ b/cmd/telesrv-admin/web/src/components/AddServerLinkModal.tsx @@ -0,0 +1,81 @@ +import { Check, Copy, X } from "lucide-react"; +import { useEffect, useState } from "react"; +import { createPortal } from "react-dom"; +import { api, errorMessage } from "../api"; +import { copyToClipboard } from "../clipboard"; +import { Alert, LoadingSurface } from "./ui"; + +// AddServerLinkModal shows a ready-made owpg://addserver link for this +// exact server (host+port only -- see the Go handler's doc comment for why +// name/description/key/DC are deliberately never embedded in it) -- an +// operator hands this out (a website button, a QR code, a message +// elsewhere) and the desktop/Android client's "Add Server" form opens +// pre-filled from it, fetching the rest straight from this server itself. +export function AddServerLinkModal({ onClose }: { onClose: () => void }) { + const [link, setLink] = useState(null); + const [error, setError] = useState(""); + const [copied, setCopied] = useState(false); + + useEffect(() => { + let cancelled = false; + api.addServerLink() + .then((result) => { + if (!cancelled) setLink(result.link); + }) + .catch((err) => { + if (!cancelled) setError(errorMessage(err)); + }); + return () => { + cancelled = true; + }; + }, []); + + async function copy() { + if (!link) return; + try { + await copyToClipboard(link); + setCopied(true); + } catch (err) { + setError(errorMessage(err)); + } + } + + return createPortal( +
+
+
+
+
{"Server"}
+

{"Share server"}

+
+ +
+
+

{"Share this link (a button, a QR code, a message) so anyone with the OwpenGram client can add this server in one tap. It only carries the address and port -- the client fetches the name, description, and key directly from the server itself, so the link can never be tampered with to point someone at a fake identity for this address."}

+ {error && {error}} + {!link && !error && } + {link && ( +
+