commit c82866b58a2b9229f34e1932c9dedd360af681db Author: Manchik Date: Fri Apr 17 22:22:40 2026 +0300 v 3.0.3 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2f8f520 --- /dev/null +++ b/.gitignore @@ -0,0 +1,36 @@ +# Python +__pycache__/ +*.py[cod] +*.pyo +*.egg-info/ +*.egg +dist/ +build/ +venv/ +.venv/ +docs/ + +# Frontend +frontend/node_modules/ + +# app/static/assets/ +# app/static/index.html + +# User data — DB, mafiles, logs, settings +data/ + +# IDE / tools +.github/ +.vscode/ +.history/ +.serena/ +.idea/ +*.swp +*.swo +*~ +.DS_Store +Thumbs.db + +# Misc +example/ +*.log \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..6141e5f --- /dev/null +++ b/README.md @@ -0,0 +1,97 @@ +![SteamPanel](assets/steampanel_ru.png) + +# SteamPanel + +> [English version](assets/README_EN.md) | **Автор:** [@lolzdm](https://t.me/lolzdm) + +Self-hosted веб-панель для массового управления Steam-аккаунтами. Backend на FastAPI + SQLite, фронт на React. Все операции выполняются пачками, прокси поддерживаются, прогресс задач в реальном времени через SSE. + +Поддерживает **3 типа аккаунтов**: +- **Mafile** — полные аккаунты с секретами (shared_secret, identity_secret) +- **Log:Pass** — логин + пароль +- **Token** — access/refresh токены *(в разработке, не работает)* + +--- + +## Функционал + +**Аккаунты** +- Импорт из `.txt`: `login:pass`, `login:pass:email:email_pass`, с mafile — любой формат +- Bulk-операции на выбранных или всех аккаунтах +- Поиск по логину, Steam ID, заметкам, уровню (`lvl>10`) +- Настраиваемые колонки для каждой вкладки + +**Действия со Steam** +- Смена пароля (ручной / рандомный) +- Смена email +- Смена / удаление телефона +- Удаление Steam Guard +- Авто-подтверждение входов (accept logins) +- Открытие Steam в браузере через [NoDriver](https://github.com/ultrafunkamsterdam/nodriver) (Chrome автоматизация) + +**Валидация** +- Проверка логина, банов, парсинг профиля (никнейм, аватар, уровень, последний онлайн) +- Настройка кол-ва потоков + +**Прокси** +- HTTP / SOCKS5 +- Форматы: `login:pass@ip:port`, `ip:port:login:pass` и др. + кастомный конструктор +- Проверка (alive/dead), round-robin назначение, очистка + +**Mafile** +- Mafile'ы привязываются к аккаунтам через раздел **Аккаунты** +- Экспорт в ZIP: выбор конкретных полей mafile и Session-блока +- Форматы экспорта: **flat** (все файлы в корне), **per-folder** (папка на аккаунт), **single JSON** (один файл) +- Шаблоны имён файлов с переменными `{username}`, `{steamid}` +- Генерация `.txt`: глобальный `accounts.txt`, отдельный `.txt` в каждой папке, кастомный формат строки + - Доступные переменные: `{login}`, `{password}`, `{email}`, `{email_password}`, `{steam_id}`, `{proxy}`, `{mafile}` +- Опция «не создавать папку для каждого аккаунта» при включённом глобальном `.txt` + +**Инструменты** +- Генератор 2FA кодов по `shared_secret` — результат копируется по клику +- Настройки валидации: включение/отключение парсинга профиля (никнейм/аватар), проверки банов, количество потоков (1–50) +- Режим скрытия паролей (spoiler-режим) + +--- + +## Установка + +### Способ 1 — `run.bat` (Windows, рекомендуется) + +Необходимо скачать архив, распаковать его и запустить `run.bat`. + +Скрипт выполнит следующие действия автоматически: +1. Проверит наличие Python в системе +2. Создаст виртуальное окружение `venv` +3. Установит все необходимые зависимости +4. Запустит main.py + +**Требования:** Python 3.14 — [python.org](https://python.org) + +--- + +### Способ 2 — вручную + +```bash +git clone https://github.com/LOLZ-dev/SteamPanel.git + +cd SteamPanel + +python -m venv venv + +venv\scripts\activate + +pip install -r requirements.txt + +python main.py +``` + +После запуска панель будет доступна по адресу `http://127.0.0.1:8000`. + +> Chrome/Chromium требуется только для функции «Открыть в браузере». + +--- + +## Лицензия + +MIT — **by [@lolzdm](https://t.me/lolzdm)** diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/api/__init__.py b/app/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/api/accounts.py b/app/api/accounts.py new file mode 100644 index 0000000..51de4e6 --- /dev/null +++ b/app/api/accounts.py @@ -0,0 +1,348 @@ +import json +from pathlib import Path + +from fastapi import APIRouter, HTTPException, UploadFile, File +from loguru import logger + +from app.database import get_db +from app.models import ( + AccountCreate, + AccountOut, + AccountUpdate, + BulkImportResult, + MafileData, +) +from app.config import settings + +router = APIRouter(prefix="/api/accounts", tags=["accounts"]) + + +@router.get("", response_model=list[AccountOut]) +async def list_accounts(): + db = await get_db() + cursor = await db.execute("SELECT * FROM accounts ORDER BY id DESC") + rows = await cursor.fetchall() + return [dict(r) for r in rows] + + +@router.get("/{account_id}", response_model=AccountOut) +async def get_account(account_id: int): + db = await get_db() + cursor = await db.execute("SELECT * FROM accounts WHERE id = ?", (account_id,)) + row = await cursor.fetchone() + if not row: + raise HTTPException(status_code=404, detail="Account not found") + return dict(row) + + +@router.post("", response_model=AccountOut, status_code=201) +async def create_account(account: AccountCreate): + db = await get_db() + cursor = await db.execute( + """INSERT INTO accounts (login, password, steam_id, email, email_password, phone, proxy, notes) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)""", + ( + account.login, + account.password, + account.steam_id, + account.email, + account.email_password, + account.phone, + account.proxy, + account.notes, + ), + ) + await db.commit() + new_cursor = await db.execute( + "SELECT * FROM accounts WHERE id = ?", (cursor.lastrowid,) + ) + return dict(await new_cursor.fetchone()) + + +@router.put("/{account_id}", response_model=AccountOut) +async def update_account(account_id: int, account: AccountUpdate): + db = await get_db() + fields = {k: v for k, v in account.model_dump().items() if v is not None} + # Allow clearing fields by sending empty string → store as NULL + for k, v in account.model_dump().items(): + if v == "" and k not in fields: + fields[k] = None + if not fields: + raise HTTPException(status_code=400, detail="No fields to update") + + fields["updated_at"] = "datetime('now')" + set_clause = ", ".join( + f"{k} = datetime('now')" if k == "updated_at" else f"{k} = ?" + for k in fields + ) + values = [v for k, v in fields.items() if k != "updated_at"] + values.append(account_id) + + await db.execute( + f"UPDATE accounts SET {set_clause} WHERE id = ?", + values, + ) + await db.commit() + + cursor = await db.execute("SELECT * FROM accounts WHERE id = ?", (account_id,)) + row = await cursor.fetchone() + if not row: + raise HTTPException(status_code=404, detail="Account not found") + return dict(row) + + +@router.delete("/{account_id}", status_code=204) +async def delete_account(account_id: int): + db = await get_db() + cursor = await db.execute("SELECT mafile_path FROM accounts WHERE id = ?", (account_id,)) + row = await cursor.fetchone() + if not row: + raise HTTPException(status_code=404, detail="Account not found") + if row["mafile_path"]: + mafile = Path(row["mafile_path"]) + if mafile.exists(): + mafile.unlink() + await db.execute("DELETE FROM accounts WHERE id = ?", (account_id,)) + await db.commit() + +@router.post("/delete-bulk", status_code=200) +async def delete_accounts_bulk(request: dict): + """Delete multiple accounts by IDs list, or all if ids is empty.""" + db = await get_db() + ids = request.get("ids", []) + + if ids: + placeholders = ",".join("?" for _ in ids) + cursor = await db.execute( + f"SELECT id, mafile_path FROM accounts WHERE id IN ({placeholders})", ids + ) + else: + cursor = await db.execute("SELECT id, mafile_path FROM accounts") + + rows = await cursor.fetchall() + count = 0 + for row in rows: + if row["mafile_path"]: + mafile = Path(row["mafile_path"]) + if mafile.exists(): + mafile.unlink() + count += 1 + + if ids: + placeholders = ",".join("?" for _ in ids) + await db.execute(f"DELETE FROM accounts WHERE id IN ({placeholders})", ids) + else: + await db.execute("DELETE FROM accounts") + + await db.commit() + return {"deleted": count} + + +@router.post("/assign-proxies") +async def assign_proxies_round_robin(): + """Round-robin assign proxies to accounts that don't have one.""" + db = await get_db() + proxy_cursor = await db.execute("SELECT id, address, protocol FROM proxies ORDER BY id") + proxies = [dict(r) for r in await proxy_cursor.fetchall()] + if not proxies: + raise HTTPException(status_code=400, detail="No proxies available") + + acc_cursor = await db.execute("SELECT id FROM accounts WHERE proxy IS NULL OR proxy = '' ORDER BY id") + account_ids = [r["id"] for r in await acc_cursor.fetchall()] + if not account_ids: + return {"assigned": 0, "proxies_used": 0} + + for i, acc_id in enumerate(account_ids): + proxy = proxies[i % len(proxies)] + await db.execute( + "UPDATE accounts SET proxy = ?, updated_at = datetime('now') WHERE id = ?", + (proxy["address"], acc_id), + ) + await db.commit() + + from app.core.proxy_manager import proxy_manager + await proxy_manager.load() + logger.info(f"Assigned {len(proxies)} proxies to {len(account_ids)} accounts (round-robin)") + return {"assigned": len(account_ids), "proxies_used": len(proxies)} + + +@router.post("/reassign-proxies") +async def reassign_proxies_round_robin(): + """Round-robin reassign proxies to ALL accounts, overwriting existing.""" + db = await get_db() + proxy_cursor = await db.execute("SELECT id, address, protocol FROM proxies ORDER BY id") + proxies = [dict(r) for r in await proxy_cursor.fetchall()] + if not proxies: + raise HTTPException(status_code=400, detail="No proxies available") + + acc_cursor = await db.execute("SELECT id FROM accounts ORDER BY id") + account_ids = [r["id"] for r in await acc_cursor.fetchall()] + if not account_ids: + return {"assigned": 0, "proxies_used": 0} + + for i, acc_id in enumerate(account_ids): + proxy = proxies[i % len(proxies)] + await db.execute( + "UPDATE accounts SET proxy = ?, updated_at = datetime('now') WHERE id = ?", + (proxy["address"], acc_id), + ) + await db.commit() + + from app.core.proxy_manager import proxy_manager + await proxy_manager.load() + logger.info(f"Reassigned {len(proxies)} proxies to {len(account_ids)} accounts (round-robin)") + return {"assigned": len(account_ids), "proxies_used": len(proxies)} + + +@router.post("/clear-proxies") +async def clear_all_proxies(): + """Remove proxy assignment from all accounts.""" + db = await get_db() + cursor = await db.execute("UPDATE accounts SET proxy = NULL, updated_at = datetime('now') WHERE proxy IS NOT NULL AND proxy != ''") + await db.commit() + count = cursor.rowcount + + from app.core.proxy_manager import proxy_manager + await proxy_manager.load() + logger.info(f"Cleared proxies from {count} accounts") + return {"cleared": count} + + +@router.post("/import", response_model=BulkImportResult) +async def import_accounts(file: UploadFile = File(...)): + """Import accounts. + + Supported formats (: or | separator): + login|password|{mafile_json} + login|password|email|email_password|{mafile_json} + login|password|email|email_password + """ + content = (await file.read()).decode("utf-8", errors="ignore") + result = BulkImportResult() + db = await get_db() + + for raw_line in content.strip().splitlines(): + line = raw_line.strip() + if not line: + continue + + # Split off embedded mafile JSON (everything from first '{' onward) + json_start = line.find("{") + if json_start != -1: + mafile_str = line[json_start:] + prefix = line[:json_start].rstrip("|:") + else: + mafile_str = None + prefix = line + + sep = "|" if "|" in prefix else ":" + parts = prefix.split(sep) + + if mafile_str: + # Accepted: 2 fields (login|pass) or 4 fields (login|pass|email|email_pass) + if len(parts) == 2: + login, password = parts + email = email_password = None + elif len(parts) == 4: + login, password, email, email_password = parts + else: + result.errors.append(f"Invalid prefix (need 2 or 4 fields): {line[:50]}") + result.skipped += 1 + continue + + try: + mafile_data = json.loads(mafile_str) + except json.JSONDecodeError as exc: + result.errors.append(f"Bad mafile JSON on line starting '{prefix[:30]}': {exc}") + result.skipped += 1 + continue + + # Fall back to mail fields embedded in the mafile JSON itself + if not email: + email = mafile_data.get("mail") or mafile_data.get("email") or None + email_password = mafile_data.get("mail_password") or mafile_data.get("email_password") or None + else: + # No mafile — must be exactly 4 fields + if len(parts) != 4: + result.errors.append(f"Invalid line (need 4 fields): {line[:50]}") + result.skipped += 1 + continue + login, password, email, email_password = parts + mafile_data = None + + try: + # Check if account with this login already exists + existing = await db.execute( + "SELECT id FROM accounts WHERE login = ?", (login,) + ) + if await existing.fetchone(): + result.errors.append(f"{login}: уже существует") + result.skipped += 1 + continue + + await db.execute( + """INSERT INTO accounts (login, password, email, email_password) + VALUES (?, ?, ?, ?)""", + (login, password, email or None, email_password or None), + ) + result.imported += 1 + except Exception as exc: + result.errors.append(f"{login}: {exc}") + result.skipped += 1 + continue + + if mafile_data: + account_name = mafile_data.get("account_name") or login + shared_secret = mafile_data.get("shared_secret") or "" + identity_secret = mafile_data.get("identity_secret") or "" + steam_id = (mafile_data.get("Session") or {}).get("SteamID") or None + + settings.mafiles_dir.mkdir(parents=True, exist_ok=True) + dest = settings.mafiles_dir / f"{account_name}.mafile" + dest.write_text(json.dumps(mafile_data, ensure_ascii=False), encoding="utf-8") + + await db.execute( + """UPDATE accounts + SET mafile_path = ?, shared_secret = ?, identity_secret = ?, steam_id = ? + WHERE login = ? AND steam_id IS NULL""", + (str(dest), shared_secret, identity_secret, steam_id, login), + ) + + await db.commit() + logger.info(f"Imported {result.imported} accounts, skipped {result.skipped}") + return result + + +@router.post("/{account_id}/mafile") +async def upload_mafile(account_id: int, file: UploadFile = File(...)): + """Bind a .mafile to an account.""" + db = await get_db() + cursor = await db.execute("SELECT * FROM accounts WHERE id = ?", (account_id,)) + row = await cursor.fetchone() + if not row: + raise HTTPException(status_code=404, detail="Account not found") + + content = (await file.read()).decode("utf-8") + try: + mafile = MafileData.model_validate_json(content) + except Exception: + raise HTTPException(status_code=400, detail="Invalid mafile format") + + settings.mafiles_dir.mkdir(parents=True, exist_ok=True) + steam_id = mafile.Session.SteamID or row["steam_id"] or account_id + mafile_path = settings.mafiles_dir / f"{steam_id}.mafile" + mafile_path.write_text(content, encoding="utf-8") + + await db.execute( + """UPDATE accounts SET mafile_path = ?, shared_secret = ?, identity_secret = ?, + steam_id = COALESCE(steam_id, ?), updated_at = datetime('now') WHERE id = ?""", + ( + str(mafile_path), + mafile.shared_secret, + mafile.identity_secret, + str(mafile.Session.SteamID) if mafile.Session.SteamID else None, + account_id, + ), + ) + await db.commit() + return {"status": "ok", "mafile_path": str(mafile_path)} diff --git a/app/api/actions.py b/app/api/actions.py new file mode 100644 index 0000000..c3eaa63 --- /dev/null +++ b/app/api/actions.py @@ -0,0 +1,86 @@ +from fastapi import APIRouter, HTTPException +from pydantic import BaseModel +from loguru import logger + +from app.database import get_db +from app.models import ActionRequest +from app.core.task_manager import task_manager + +router = APIRouter(prefix="/api/actions", tags=["actions"]) + +VALID_ACTIONS = { + "change_password", + "random_password", + "change_email", + "change_phone", + "remove_guard", + "generate_2fa", + "validate", + "add_friend", + "accept_logins", + "change_language", +} + + +@router.post("") +async def execute_action(request: ActionRequest): + if request.action not in VALID_ACTIONS: + raise HTTPException( + status_code=400, + detail=f"Invalid action. Valid: {', '.join(sorted(VALID_ACTIONS))}", + ) + + db = await get_db() + placeholders = ",".join("?" for _ in request.account_ids) + cursor = await db.execute( + f"SELECT * FROM accounts WHERE id IN ({placeholders})", + request.account_ids, + ) + accounts = [dict(r) for r in await cursor.fetchall()] + + if not accounts: + raise HTTPException(status_code=404, detail="No accounts found") + + task_id = await task_manager.submit( + task_type=request.action, + accounts=accounts, + params=request.params or {}, + ) + + logger.info( + f"Action '{request.action}' submitted for {len(accounts)} accounts → task {task_id}" + ) + return {"task_id": task_id, "accounts_count": len(accounts)} + + +class Generate2FARequest(BaseModel): + shared_secret: str + + +class Generate2FAByAccountRequest(BaseModel): + account_id: int + + +@router.post("/generate-2fa") +async def generate_2fa_code(request: Generate2FARequest): + """Server-side 2FA code generation from shared_secret.""" + from app.services.steam_guard import generate_2fa_code as gen_code + + code = gen_code(request.shared_secret) + return {"code": code} + + +@router.post("/generate-2fa-by-account") +async def generate_2fa_by_account(request: Generate2FAByAccountRequest): + """Generate 2FA code for an account using its stored shared_secret.""" + from app.services.steam_guard import generate_2fa_code as gen_code + + db = await get_db() + cursor = await db.execute( + "SELECT shared_secret FROM accounts WHERE id = ?", + (request.account_id,), + ) + row = await cursor.fetchone() + if not row or not row["shared_secret"]: + raise HTTPException(status_code=404, detail="Account not found or has no shared_secret") + return {"code": gen_code(row["shared_secret"])} diff --git a/app/api/auto_accept.py b/app/api/auto_accept.py new file mode 100644 index 0000000..46679d9 --- /dev/null +++ b/app/api/auto_accept.py @@ -0,0 +1,49 @@ +from fastapi import APIRouter, HTTPException +from pydantic import BaseModel + +from app.database import get_db +from app.core.auto_accept import auto_accept_manager + +router = APIRouter(prefix="/api/auto-accept", tags=["auto-accept"]) + + +class AutoAcceptRequest(BaseModel): + account_ids: list[int] + + +@router.post("/start") +async def start_auto_accept(body: AutoAcceptRequest): + db = await get_db() + started: list[int] = [] + for aid in body.account_ids: + cursor = await db.execute("SELECT * FROM accounts WHERE id = ?", (aid,)) + row = await cursor.fetchone() + if not row: + raise HTTPException(404, f"Account {aid} not found") + account = dict(row) + if not account.get("shared_secret"): + raise HTTPException(400, f"Account {account['login']} has no shared_secret (mafile)") + await auto_accept_manager.start(account) + await db.execute("UPDATE accounts SET auto_accept = 1 WHERE id = ?", (aid,)) + started.append(aid) + await db.commit() + return {"started": started} + + +@router.post("/stop") +async def stop_auto_accept(body: AutoAcceptRequest): + db = await get_db() + stopped: list[int] = [] + for aid in body.account_ids: + auto_accept_manager.stop(aid) + await db.execute("UPDATE accounts SET auto_accept = 0 WHERE id = ?", (aid,)) + stopped.append(aid) + await db.commit() + return {"stopped": stopped} + + +@router.get("/status") +async def auto_accept_status(): + running = auto_accept_manager.running_ids() + errors = auto_accept_manager.pop_errors() + return {"running": list(running), "errors": errors} diff --git a/app/api/browser.py b/app/api/browser.py new file mode 100644 index 0000000..a7f36e1 --- /dev/null +++ b/app/api/browser.py @@ -0,0 +1,65 @@ +import asyncio + +from fastapi import APIRouter, HTTPException +from loguru import logger + +from app.database import get_db +from app.config import read_validation_settings +from app.core.task_manager import task_manager + +router = APIRouter(prefix="/api", tags=["browser"]) + + +@router.post("/accounts/{account_id}/browser") +async def open_browser(account_id: int): + db = await get_db() + cursor = await db.execute("SELECT * FROM accounts WHERE id = ?", (account_id,)) + row = await cursor.fetchone() + if not row: + raise HTTPException(404, "Account not found") + + account = dict(row) + + from app.services.steam_auth import check_cookies_alive, _resolve_proxy + from app.config import read_validation_settings + + # If account isn't validated or has no cookies — auto-revalidate if enabled + if account["status"] != "valid" or not account.get("session_cookies"): + val_settings = read_validation_settings() + if val_settings.get("auto_revalidate_browser"): + task_id = await task_manager.submit( + task_type="validate", + accounts=[account], + params={}, + ) + logger.info(f"Account {account['login']} not valid/no cookies, auto-revalidating → task {task_id}") + return {"status": "revalidating", "message": "Cookies expired. Re-validating...", "task_id": task_id} + if account["status"] != "valid": + raise HTTPException(400, "Account is not validated") + raise HTTPException(400, "No session cookies. Validate the account first.") + + proxy = await _resolve_proxy(account) + alive = await check_cookies_alive(account["session_cookies"], proxy) + if not alive: + val_settings = read_validation_settings() + if val_settings.get("auto_revalidate_browser"): + task_id = await task_manager.submit( + task_type="validate", + accounts=[account], + params={}, + ) + logger.info(f"Cookies dead for {account['login']}, auto-revalidating → task {task_id}") + return {"status": "revalidating", "message": "Cookies expired. Re-validating...", "task_id": task_id} + raise HTTPException(400, "Session cookies expired. Re-validate the account.") + + from app.services.browser_login import open_browser_with_cookies + + async def _run(): + try: + await open_browser_with_cookies(account) + except Exception as exc: + logger.error(f"Browser open failed for {account['login']}: {exc}") + + asyncio.create_task(_run()) + + return {"status": "ok", "message": "Browser opening..."} diff --git a/app/api/logpass.py b/app/api/logpass.py new file mode 100644 index 0000000..0717498 --- /dev/null +++ b/app/api/logpass.py @@ -0,0 +1,356 @@ +"""CRUD and validate endpoints for log:pass accounts.""" + +import csv +import ctypes +import io +import sys + +csv.field_size_limit(min(sys.maxsize, ctypes.c_ulong(-1).value // 2)) + +from fastapi import APIRouter, HTTPException +from loguru import logger + +from app.database import get_db +from app.models import ( + LogpassAccountCreate, + LogpassAccountOut, + LogpassAccountUpdate, +) +from app.core.task_manager import task_manager + +router = APIRouter(prefix="/api/logpass", tags=["logpass"]) + + +@router.get("", response_model=list[LogpassAccountOut]) +async def list_logpass(): + db = await get_db() + cursor = await db.execute("SELECT * FROM logpass_accounts ORDER BY id DESC") + rows = await cursor.fetchall() + return [dict(r) for r in rows] + + +@router.get("/{account_id}", response_model=LogpassAccountOut) +async def get_logpass(account_id: int): + db = await get_db() + cursor = await db.execute("SELECT * FROM logpass_accounts WHERE id = ?", (account_id,)) + row = await cursor.fetchone() + if not row: + raise HTTPException(status_code=404, detail="Account not found") + return dict(row) + + +@router.post("", response_model=LogpassAccountOut, status_code=201) +async def create_logpass(account: LogpassAccountCreate): + db = await get_db() + cursor = await db.execute( + "INSERT INTO logpass_accounts (login, password, steam_id, proxy, notes) VALUES (?, ?, ?, ?, ?)", + (account.login, account.password, account.steam_id, account.proxy, account.notes), + ) + await db.commit() + new_cursor = await db.execute( + "SELECT * FROM logpass_accounts WHERE id = ?", (cursor.lastrowid,) + ) + return dict(await new_cursor.fetchone()) + + +@router.put("/{account_id}", response_model=LogpassAccountOut) +async def update_logpass(account_id: int, account: LogpassAccountUpdate): + db = await get_db() + fields = {k: v for k, v in account.model_dump().items() if v is not None} + if not fields: + raise HTTPException(status_code=400, detail="No fields to update") + fields["updated_at"] = "datetime('now')" + set_clause = ", ".join( + f"{k} = datetime('now')" if k == "updated_at" else f"{k} = ?" + for k in fields + ) + values = [v for k, v in fields.items() if k != "updated_at"] + values.append(account_id) + await db.execute( + f"UPDATE logpass_accounts SET {set_clause} WHERE id = ?", values + ) + await db.commit() + cursor = await db.execute("SELECT * FROM logpass_accounts WHERE id = ?", (account_id,)) + row = await cursor.fetchone() + if not row: + raise HTTPException(status_code=404, detail="Account not found") + return dict(row) + + +@router.delete("/{account_id}", status_code=204) +async def delete_logpass(account_id: int): + db = await get_db() + await db.execute("DELETE FROM logpass_accounts WHERE id = ?", (account_id,)) + await db.commit() + + +@router.post("/delete-bulk") +async def delete_logpass_bulk(data: dict): + ids: list[int] = data.get("ids", []) + if not ids: + return {"deleted": 0} + db = await get_db() + placeholders = ",".join("?" for _ in ids) + await db.execute(f"DELETE FROM logpass_accounts WHERE id IN ({placeholders})", ids) + await db.commit() + return {"deleted": len(ids)} + + +@router.post("/import") +async def import_logpass(data: dict): + """Bulk import from plain text (login:pass / login|pass) or CSV with headers.""" + lines: list[str] = data.get("lines", []) + if not lines: + return {"imported": 0, "skipped": 0, "errors": []} + + imported = 0 + skipped = 0 + errors: list[str] = [] + + db = await get_db() + + async def upsert(login: str, fields: dict): + """Insert or update by login, works with or without UNIQUE index.""" + cursor = await db.execute( + "SELECT id FROM logpass_accounts WHERE login = ?", (login,) + ) + existing = await cursor.fetchone() + if existing: + sets = ", ".join(f"{k} = ?" for k in fields) + vals = list(fields.values()) + [existing["id"]] + await db.execute( + f"UPDATE logpass_accounts SET {sets}, updated_at = datetime('now') WHERE id = ?", + vals, + ) + else: + fields["login"] = login + cols = ", ".join(fields.keys()) + placeholders = ", ".join("?" for _ in fields) + await db.execute( + f"INSERT INTO logpass_accounts ({cols}) VALUES ({placeholders})", + list(fields.values()), + ) + + # Detect CSV: first line looks like a header row + first = lines[0].strip().lower() + is_csv = first.startswith("login,") or first.startswith("login;") + + try: + if is_csv: + text = "\n".join(lines) + reader = csv.DictReader(io.StringIO(text)) + for row in reader: + login = (row.get("login") or "").strip() + password = (row.get("password") or "").strip() + if not login or not password: + skipped += 1 + continue + fields = {"password": password} + _NA = {"n/a", "na", "none", "null", ""} + for csv_col, db_col in [ + ("steam_id", "steam_id"), ("ban", "ban_status"), + ("prime", "prime"), ("trophy", "trophy"), + ("behavior", "behavior"), ("license", "license"), + ]: + val = (row.get(csv_col) or "").strip() + if val: + if db_col in ("prime", "trophy", "behavior") and val.lower() in _NA: + val = "\u2014" + fields[db_col] = val + try: + await upsert(login, fields) + imported += 1 + except Exception as exc: + logger.error(f"Logpass CSV import error for '{login}': {exc}") + errors.append(f"{login}: {exc}") + else: + for raw in lines: + line = raw.strip() + if not line: + skipped += 1 + continue + # Skip JSON/mafile lines + if line.startswith("{") or line.startswith("["): + skipped += 1 + continue + if "|" in line: + parts = line.split("|", 1) + elif ":" in line: + parts = line.split(":", 1) + else: + skipped += 1 + continue + login, password = parts[0].strip(), parts[1].strip() + if not login or not password: + skipped += 1 + continue + # Reject if password looks like JSON (mafile embedded) or is too long + if password.startswith("{") or password.startswith("[") or len(password) > 128: + skipped += 1 + logger.warning(f"Logpass import: skipped '{login}' — password looks like mafile or is too long ({len(password)} chars)") + continue + try: + await upsert(login, {"password": password}) + imported += 1 + except Exception as exc: + logger.error(f"Logpass import error for '{login}': {exc}") + errors.append(f"{login}: {exc}") + + await db.commit() + except Exception as exc: + logger.exception(f"Logpass import failed: {exc}") + raise HTTPException(status_code=500, detail=str(exc)) + + return {"imported": imported, "skipped": skipped, "errors": errors} + + +@router.post("/validate") +async def validate_logpass(data: dict): + """Submit validation task for selected log:pass accounts.""" + ids: list[int] = data.get("account_ids", []) + if not ids: + raise HTTPException(status_code=400, detail="No account IDs provided") + + db = await get_db() + placeholders = ",".join("?" for _ in ids) + cursor = await db.execute( + f"SELECT * FROM logpass_accounts WHERE id IN ({placeholders})", ids + ) + accounts = [dict(r) for r in await cursor.fetchall()] + if not accounts: + raise HTTPException(status_code=404, detail="No accounts found") + + task_id = await task_manager.submit( + task_type="logpass_validate", + accounts=accounts, + params={}, + ) + logger.info(f"logpass_validate submitted for {len(accounts)} accounts → task {task_id}") + return {"task_id": task_id, "accounts_count": len(accounts)} + + +@router.post("/full-parse") +async def full_parse_logpass(data: dict): + """Submit full parse task (prime, trophy, behavior, licenses) for selected accounts.""" + ids: list[int] = data.get("account_ids", []) + if not ids: + raise HTTPException(status_code=400, detail="No account IDs provided") + + db = await get_db() + placeholders = ",".join("?" for _ in ids) + cursor = await db.execute( + f"SELECT * FROM logpass_accounts WHERE id IN ({placeholders})", ids + ) + accounts = [dict(r) for r in await cursor.fetchall()] + if not accounts: + raise HTTPException(status_code=404, detail="No accounts found") + + task_id = await task_manager.submit( + task_type="logpass_full_parse", + accounts=accounts, + params={}, + ) + logger.info(f"logpass_full_parse submitted for {len(accounts)} accounts → task {task_id}") + return {"task_id": task_id, "accounts_count": len(accounts)} + + +@router.post("/assign-proxies") +async def logpass_assign_proxies(): + """Round-robin assign proxies to logpass accounts that don't have one.""" + db = await get_db() + proxy_cursor = await db.execute("SELECT address FROM proxies ORDER BY id") + proxies = [r["address"] for r in await proxy_cursor.fetchall()] + if not proxies: + raise HTTPException(status_code=400, detail="No proxies available") + + acc_cursor = await db.execute( + "SELECT id FROM logpass_accounts WHERE proxy IS NULL OR proxy = '' ORDER BY id" + ) + account_ids = [r["id"] for r in await acc_cursor.fetchall()] + if not account_ids: + return {"assigned": 0, "proxies_used": 0} + + for i, acc_id in enumerate(account_ids): + await db.execute( + "UPDATE logpass_accounts SET proxy = ?, updated_at = datetime('now') WHERE id = ?", + (proxies[i % len(proxies)], acc_id), + ) + await db.commit() + return {"assigned": len(account_ids), "proxies_used": len(proxies)} + + +@router.post("/reassign-proxies") +async def logpass_reassign_proxies(): + """Round-robin reassign proxies to ALL logpass accounts.""" + db = await get_db() + proxy_cursor = await db.execute("SELECT address FROM proxies ORDER BY id") + proxies = [r["address"] for r in await proxy_cursor.fetchall()] + if not proxies: + raise HTTPException(status_code=400, detail="No proxies available") + + acc_cursor = await db.execute("SELECT id FROM logpass_accounts ORDER BY id") + account_ids = [r["id"] for r in await acc_cursor.fetchall()] + if not account_ids: + return {"assigned": 0, "proxies_used": 0} + + for i, acc_id in enumerate(account_ids): + await db.execute( + "UPDATE logpass_accounts SET proxy = ?, updated_at = datetime('now') WHERE id = ?", + (proxies[i % len(proxies)], acc_id), + ) + await db.commit() + return {"assigned": len(account_ids), "proxies_used": len(proxies)} + + +@router.post("/clear-proxies") +async def logpass_clear_proxies(): + """Remove proxy from all logpass accounts.""" + db = await get_db() + cursor = await db.execute( + "UPDATE logpass_accounts SET proxy = NULL, updated_at = datetime('now') WHERE proxy IS NOT NULL AND proxy != ''" + ) + await db.commit() + return {"cleared": cursor.rowcount} + + +@router.post("/{account_id}/browser") +async def open_logpass_browser(account_id: int): + """Open Chrome browser with saved session cookies for a logpass account.""" + import asyncio + db = await get_db() + cursor = await db.execute("SELECT * FROM logpass_accounts WHERE id = ?", (account_id,)) + row = await cursor.fetchone() + if not row: + raise HTTPException(status_code=404, detail="Account not found") + + account = dict(row) + if not account.get("session_cookies"): + raise HTTPException(status_code=400, detail="No session cookies. Validate the account first.") + + from app.services.steam_auth import check_cookies_alive, _resolve_proxy + from app.config import read_validation_settings + + proxy = await _resolve_proxy(account) + alive = await check_cookies_alive(account["session_cookies"], proxy) + if not alive: + val_settings = read_validation_settings() + if val_settings.get("auto_revalidate_browser"): + task_id = await task_manager.submit( + task_type="logpass_validate", + accounts=[account], + params={}, + ) + logger.info(f"Cookies dead for {account['login']}, auto-revalidating → task {task_id}") + return {"status": "revalidating", "message": "Cookies expired. Re-validating...", "task_id": task_id} + raise HTTPException(status_code=400, detail="Session cookies expired. Re-validate the account.") + + from app.services.browser_login import open_browser_with_cookies + + async def _run(): + try: + await open_browser_with_cookies(account) + except Exception as exc: + logger.error(f"Browser open failed for {account['login']}: {exc}") + + asyncio.create_task(_run()) + return {"status": "ok", "message": "Browser opening..."} diff --git a/app/api/logs.py b/app/api/logs.py new file mode 100644 index 0000000..1dc7452 --- /dev/null +++ b/app/api/logs.py @@ -0,0 +1,67 @@ +import asyncio +import json +from collections import deque +from typing import Any + +from fastapi import APIRouter +from fastapi.responses import StreamingResponse +from loguru import logger + +router = APIRouter(prefix="/api/logs", tags=["logs"]) + +_LOG_BUFFER: deque[dict[str, Any]] = deque(maxlen=500) +_SUBSCRIBERS: list[asyncio.Queue] = [] + +LEVEL_ICONS = { + "TRACE": "~", + "DEBUG": ".", + "INFO": "i", + "SUCCESS": "+", + "WARNING": "!", + "ERROR": "e", + "CRITICAL": "e", +} + + +def _log_sink(message: Any) -> None: + record = message.record + entry = { + "ts": record["time"].strftime("%H:%M:%S"), + "level": record["level"].name.lower(), + "icon": LEVEL_ICONS.get(record["level"].name, "i"), + "msg": record["message"], + } + _LOG_BUFFER.append(entry) + for q in _SUBSCRIBERS[:]: + try: + q.put_nowait(entry) + except asyncio.QueueFull: + pass + + +def install_log_sink() -> None: + logger.add(_log_sink, format="{message}", level="DEBUG", colorize=False) + + +@router.get("") +async def get_logs(): + return list(_LOG_BUFFER) + + +@router.get("/stream") +async def stream_logs(): + queue: asyncio.Queue = asyncio.Queue(maxsize=200) + _SUBSCRIBERS.append(queue) + + async def event_generator(): + try: + while True: + entry = await queue.get() + yield f"data: {json.dumps(entry, ensure_ascii=False)}\n\n" + except asyncio.CancelledError: + pass + finally: + if queue in _SUBSCRIBERS: + _SUBSCRIBERS.remove(queue) + + return StreamingResponse(event_generator(), media_type="text/event-stream") diff --git a/app/api/mafile_tools.py b/app/api/mafile_tools.py new file mode 100644 index 0000000..2b3b768 --- /dev/null +++ b/app/api/mafile_tools.py @@ -0,0 +1,275 @@ +import json +import io +import zipfile +from pathlib import Path + +from fastapi import APIRouter, HTTPException, UploadFile, File +from fastapi.responses import StreamingResponse +from loguru import logger +from pydantic import BaseModel + +from app.config import settings +from app.database import get_db +from app.models import MafileData + +router = APIRouter(prefix="/api/mafiles", tags=["mafile_tools"]) + + +@router.get("") +async def list_mafiles(): + """List all stored mafiles.""" + mafiles_dir = settings.mafiles_dir + if not mafiles_dir.exists(): + return [] + + result = [] + for f in sorted(mafiles_dir.glob("*.mafile")): + try: + data = json.loads(f.read_text(encoding="utf-8")) + result.append({ + "filename": f.name, + "account_name": data.get("account_name", ""), + "steam_id": data.get("Session", {}).get("SteamID", ""), + "has_shared_secret": bool(data.get("shared_secret")), + "has_identity_secret": bool(data.get("identity_secret")), + "fully_enrolled": data.get("fully_enrolled", False), + }) + except (json.JSONDecodeError, OSError): + result.append({"filename": f.name, "error": "Failed to parse"}) + return result + + +@router.post("/upload") +async def upload_mafiles(files: list[UploadFile] = File(...)): + """Upload multiple .mafile files.""" + settings.mafiles_dir.mkdir(parents=True, exist_ok=True) + uploaded = 0 + errors: list[str] = [] + + db = await get_db() + + for file in files: + content = (await file.read()).decode("utf-8", errors="ignore") + try: + mafile = MafileData.model_validate_json(content) + steam_id = mafile.Session.SteamID or mafile.account_name or file.filename + dest = settings.mafiles_dir / f"{steam_id}.mafile" + dest.write_text(content, encoding="utf-8") + uploaded += 1 + + if mafile.account_name: + await db.execute( + """UPDATE accounts + SET mafile_path = ?, shared_secret = ?, identity_secret = ? + WHERE login = ?""", + ( + str(dest), + mafile.shared_secret or "", + mafile.identity_secret or "", + mafile.account_name, + ), + ) + await db.commit() + except Exception as exc: + errors.append(f"{file.filename}: {exc}") + + logger.info(f"Uploaded {uploaded} mafiles, {len(errors)} errors") + return {"uploaded": uploaded, "errors": errors} + + +@router.get("/{filename}") +async def get_mafile(filename: str): + """Get parsed mafile data by filename.""" + safe_name = Path(filename).name + mafile_path = settings.mafiles_dir / safe_name + if not mafile_path.exists() or not mafile_path.suffix == ".mafile": + raise HTTPException(status_code=404, detail="Mafile not found") + + content = mafile_path.read_text(encoding="utf-8") + try: + return json.loads(content) + except json.JSONDecodeError: + raise HTTPException(status_code=500, detail="Failed to parse mafile") + + +@router.delete("/{filename}", status_code=204) +async def delete_mafile(filename: str): + safe_name = Path(filename).name + mafile_path = settings.mafiles_dir / safe_name + if not mafile_path.exists(): + raise HTTPException(status_code=404, detail="Mafile not found") + mafile_path.unlink() + + +@router.get("/export/all") +async def export_all_secrets(): + """Export account_name:shared_secret:identity_secret for all mafiles.""" + mafiles_dir = settings.mafiles_dir + if not mafiles_dir.exists(): + return [] + + result = [] + for f in sorted(mafiles_dir.glob("*.mafile")): + try: + data = json.loads(f.read_text(encoding="utf-8")) + result.append({ + "account_name": data.get("account_name", ""), + "shared_secret": data.get("shared_secret", ""), + "identity_secret": data.get("identity_secret", ""), + "steam_id": data.get("Session", {}).get("SteamID", ""), + }) + except (json.JSONDecodeError, OSError): + pass + return result + + +class ExportRequest(BaseModel): + fields: list[str] = [] + session_fields: list[str] = [] + format: str = "flat_mafiles" # flat_mafiles | per_account_folder | single_file + account_ids: list[int] = [] + # Naming variables: {username} and {steamid} are replaced at export time + folder_name_template: str = "{username}" + mafile_name_template: str = "{steamid}.mafile" + txt_name_template: str = "{username}.txt" + include_txt_per_folder: bool = False + include_global_txt: bool = False + skip_folders: bool = False + # Format for .txt lines: login:password:email:email_password + txt_format: str = "{login}:{password}:{email}:{email_password}" + + +def _filter_mafile(data: dict, fields: list[str], session_fields: list[str]) -> dict: + """Keep only selected top-level + Session fields from mafile JSON.""" + if not fields and not session_fields: + return data + + filtered = {} + for key in fields: + if key in data and key != "Session": + filtered[key] = data[key] + + if session_fields and "Session" in data: + filtered["Session"] = { + k: v for k, v in data["Session"].items() if k in session_fields + } + + return filtered + + +@router.post("/export/zip") +async def export_mafiles_zip(body: ExportRequest): + """Build a .zip with filtered mafile data, optionally limited to account IDs.""" + if not body.account_ids: + raise HTTPException(status_code=400, detail="No account IDs provided for export") + + mafiles_dir = settings.mafiles_dir + if not mafiles_dir.exists(): + raise HTTPException(status_code=404, detail="No mafiles directory") + + db = await get_db() + + # Load account info for naming and .txt generation + account_map: dict[str, dict] = {} # login -> account row + if body.account_ids: + placeholders = ",".join("?" * len(body.account_ids)) + cursor = await db.execute( + f"SELECT * FROM accounts WHERE id IN ({placeholders})", + body.account_ids, + ) + else: + cursor = await db.execute("SELECT * FROM accounts") + for row in await cursor.fetchall(): + account_map[row["login"]] = dict(row) + + account_logins = set(account_map.keys()) if body.account_ids else set() + + def _resolve_template(template: str, username: str, steam_id: str) -> str: + return template.replace("{username}", username).replace("{steamid}", steam_id) + + def _resolve_txt_line(template: str, acc: dict, mafile_data: dict | None = None) -> str: + has_mafile_var = "{mafile}" in template + if has_mafile_var: + before, _, after = template.partition("{mafile}") + else: + before = template + + line = ( + before + .replace("{login}", acc.get("login") or "") + .replace("{password}", acc.get("password") or "") + .replace("{email}", acc.get("email") or "") + .replace("{email_password}", acc.get("email_password") or "") + .replace("{steam_id}", acc.get("steam_id") or "") + .replace("{proxy}", acc.get("proxy") or "") + ) + line = line.rstrip(":") + if has_mafile_var and mafile_data is not None: + line += ":" + json.dumps(mafile_data, ensure_ascii=False, separators=(",", ":")) + return line + + buf = io.BytesIO() + all_data: list[str] = [] + global_txt_lines: list[str] = [] + folder_txt_map: dict[str, list[str]] = {} # folder_name -> lines + + with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf: + for f in sorted(mafiles_dir.glob("*.mafile")): + try: + raw = json.loads(f.read_text(encoding="utf-8")) + except (json.JSONDecodeError, OSError): + continue + + acct_name = raw.get("account_name", "") + steam_id = str(raw.get("Session", {}).get("SteamID", "")) + + if account_logins: + if acct_name not in account_logins and steam_id not in {str(a) for a in body.account_ids}: + continue + + filtered = _filter_mafile(raw, body.fields, body.session_fields) + + # Find matching account record for txt + acc = account_map.get(acct_name, {}) + + if body.format == "single_file": + if acc: + all_data.append(_resolve_txt_line(body.txt_format, acc, filtered)) + else: + all_data.append(json.dumps(filtered, ensure_ascii=False, separators=(",", ":"))) + elif body.format == "per_account_folder": + folder_name = _resolve_template(body.folder_name_template, acct_name, steam_id) + mafile_name = _resolve_template(body.mafile_name_template, acct_name, steam_id) + + if body.skip_folders: + zf.writestr(mafile_name, json.dumps(filtered, ensure_ascii=False, separators=(",", ":"))) + else: + zf.writestr(f"{folder_name}/{mafile_name}", json.dumps(filtered, ensure_ascii=False, separators=(",", ":"))) + + if body.include_txt_per_folder and acc: + txt_name = _resolve_template(body.txt_name_template, acct_name, steam_id) + line = _resolve_txt_line(body.txt_format, acc, filtered) + if folder_name not in folder_txt_map: + folder_txt_map[folder_name] = [] + folder_txt_map[folder_name].append(line) + zf.writestr(f"{folder_name}/{txt_name}", line + "\n") + else: + mafile_name = _resolve_template(body.mafile_name_template, acct_name, steam_id) + zf.writestr(mafile_name, json.dumps(filtered, ensure_ascii=False, separators=(",", ":"))) + + # Collect global txt line + if body.include_global_txt and acc: + global_txt_lines.append(_resolve_txt_line(body.txt_format, acc, filtered)) + + if body.format == "single_file" and all_data: + zf.writestr("accounts.txt", "\n".join(all_data) + "\n") + + if body.include_global_txt and global_txt_lines: + zf.writestr("accounts.txt", "\n".join(global_txt_lines) + "\n") + + buf.seek(0) + return StreamingResponse( + buf, + media_type="application/zip", + headers={"Content-Disposition": "attachment; filename=mafiles_export.zip"}, + ) diff --git a/app/api/proxies.py b/app/api/proxies.py new file mode 100644 index 0000000..abe1bc5 --- /dev/null +++ b/app/api/proxies.py @@ -0,0 +1,123 @@ +import asyncio + +from fastapi import APIRouter, HTTPException +from loguru import logger + +from app.database import get_db +from app.models import ProxyCreate, ProxyOut + +router = APIRouter(prefix="/api/proxies", tags=["proxies"]) + + +@router.get("", response_model=list[ProxyOut]) +async def list_proxies(): + db = await get_db() + cursor = await db.execute("SELECT * FROM proxies ORDER BY id") + rows = await cursor.fetchall() + return [dict(r) for r in rows] + + +@router.post("", response_model=ProxyOut, status_code=201) +async def add_proxy(proxy: ProxyCreate): + db = await get_db() + try: + cursor = await db.execute( + "INSERT INTO proxies (address, protocol) VALUES (?, ?)", + (proxy.address, proxy.protocol), + ) + await db.commit() + except Exception: + raise HTTPException(status_code=409, detail="Proxy already exists") + + new_cursor = await db.execute( + "SELECT * FROM proxies WHERE id = ?", (cursor.lastrowid,) + ) + return dict(await new_cursor.fetchone()) + + +@router.delete("/all", status_code=200) +async def delete_all_proxies(): + db = await get_db() + cursor = await db.execute("SELECT COUNT(*) as cnt FROM proxies") + row = await cursor.fetchone() + count = row["cnt"] + await db.execute("DELETE FROM proxies") + await db.commit() + from app.core.proxy_manager import proxy_manager + await proxy_manager.load() + logger.info(f"Deleted all {count} proxies") + return {"deleted": count} + + +@router.delete("/{proxy_id}", status_code=204) +async def delete_proxy(proxy_id: int): + db = await get_db() + result = await db.execute("DELETE FROM proxies WHERE id = ?", (proxy_id,)) + await db.commit() + if result.rowcount == 0: + raise HTTPException(status_code=404, detail="Proxy not found") + + +@router.post("/bulk") +async def bulk_add_proxies(proxies: list[ProxyCreate]): + db = await get_db() + added = 0 + for p in proxies: + try: + await db.execute( + "INSERT OR IGNORE INTO proxies (address, protocol) VALUES (?, ?)", + (p.address, p.protocol), + ) + added += 1 + except Exception: + pass + await db.commit() + logger.info(f"Bulk added {added} proxies") + return {"added": added} + + +async def _check_one_proxy(proxy_id: int, address: str, protocol: str) -> dict: + """Check proxy by making a real HTTP request through it.""" + import aiohttp + from aiohttp_socks import ProxyConnector + from app.core.proxy_manager import build_proxy_url + + try: + proxy_url = build_proxy_url(address, protocol) + connector = ProxyConnector.from_url(proxy_url, ssl=False) + + async with aiohttp.ClientSession(connector=connector) as session: + async with session.get( + "https://steamcommunity.com/robots.txt", + timeout=aiohttp.ClientTimeout(total=3), + ) as resp: + alive = resp.status == 200 + except Exception: + alive = False + + db = await get_db() + await db.execute( + "UPDATE proxies SET is_alive = ?, last_checked = datetime('now') WHERE id = ?", + (int(alive), proxy_id), + ) + await db.commit() + return {"id": proxy_id, "alive": alive} + + +@router.post("/check") +async def check_proxies(): + db = await get_db() + cursor = await db.execute("SELECT id, address, protocol FROM proxies ORDER BY id") + rows = await cursor.fetchall() + if not rows: + return {"total": 0, "alive": 0, "dead": 0} + + sem = asyncio.Semaphore(300) + + async def limited(row): + async with sem: + return await _check_one_proxy(row["id"], row["address"], row["protocol"]) + + results = await asyncio.gather(*(limited(r) for r in rows)) + alive_count = sum(1 for r in results if r["alive"]) + return {"total": len(results), "alive": alive_count, "dead": len(results) - alive_count} diff --git a/app/api/settings.py b/app/api/settings.py new file mode 100644 index 0000000..df5e792 --- /dev/null +++ b/app/api/settings.py @@ -0,0 +1,210 @@ +import json +from pathlib import Path + +from fastapi import APIRouter +from pydantic import BaseModel + +from app.config import settings + +router = APIRouter(prefix="/api/settings", tags=["settings"]) + +SETTINGS_FILE = settings.data_dir / "settings.json" + +DEFAULTS = { + "validation": { + "fetch_profile": True, + "check_ban": True, + "max_threads": 5, + "auto_revalidate_browser": True, + }, + "display": { + "hide_passwords": False, + }, + "columns": { + "browser": True, + "profile": True, + "steam_id": True, + "login": True, + "password": True, + "login_pass": True, + "email": True, + "phone": True, + "status": True, + "ban": True, + "twofa": True, + "mafile": True, + "proxy": True, + "actions": True, + }, + "logpass_columns": { + "browser": True, + "profile": True, + "steam_id": True, + "login": True, + "password": True, + "login_pass": True, + "status": True, + "ban": True, + "prime": True, + "trophy": True, + "behavior": True, + "license": True, + "proxy": True, + "notes": True, + "actions": True, + }, + "token_columns": { + "browser": True, + "profile": True, + "last_online": True, + "steam_id": True, + "login": True, + "token": True, + "status": True, + "proxy": True, + "notes": True, + "actions": True, + }, +} + + +def _read() -> dict: + if SETTINGS_FILE.exists(): + return json.loads(SETTINGS_FILE.read_text("utf-8")) + return DEFAULTS.copy() + + +def _write(data: dict) -> None: + SETTINGS_FILE.parent.mkdir(parents=True, exist_ok=True) + SETTINGS_FILE.write_text(json.dumps(data, indent=2, ensure_ascii=False), "utf-8") + + +class ValidationSettings(BaseModel): + fetch_profile: bool = True + check_ban: bool = True + max_threads: int = 5 + auto_revalidate_browser: bool = True + + +class DisplaySettings(BaseModel): + hide_passwords: bool = False + + +class ColumnSettings(BaseModel): + browser: bool = True + profile: bool = True + steam_id: bool = True + login: bool = True + password: bool = True + login_pass: bool = True + email: bool = True + phone: bool = True + status: bool = True + ban: bool = True + twofa: bool = True + mafile: bool = True + proxy: bool = True + actions: bool = True + last_online: bool = True + + +@router.get("/validation", response_model=ValidationSettings) +async def get_validation_settings(): + data = _read() + return data.get("validation", DEFAULTS["validation"]) + + +@router.put("/validation", response_model=ValidationSettings) +async def update_validation_settings(body: ValidationSettings): + data = _read() + data["validation"] = body.model_dump() + _write(data) + return data["validation"] + + +@router.get("/display", response_model=DisplaySettings) +async def get_display_settings(): + data = _read() + return data.get("display", DEFAULTS["display"]) + + +@router.put("/display", response_model=DisplaySettings) +async def update_display_settings(body: DisplaySettings): + data = _read() + data["display"] = body.model_dump() + _write(data) + return data["display"] + + +@router.get("/columns", response_model=ColumnSettings) +async def get_column_settings(): + data = _read() + return data.get("columns", DEFAULTS["columns"]) + + +@router.put("/columns", response_model=ColumnSettings) +async def update_column_settings(body: ColumnSettings): + data = _read() + data["columns"] = body.model_dump() + _write(data) + return data["columns"] + + +class LogpassColumnSettings(BaseModel): + browser: bool = True + profile: bool = True + steam_id: bool = True + login: bool = True + password: bool = True + login_pass: bool = True + status: bool = True + ban: bool = True + prime: bool = True + trophy: bool = True + behavior: bool = True + license: bool = True + proxy: bool = True + notes: bool = True + actions: bool = True + last_online: bool = True + + +@router.get("/logpass-columns", response_model=LogpassColumnSettings) +async def get_logpass_column_settings(): + data = _read() + return data.get("logpass_columns", DEFAULTS["logpass_columns"]) + + +@router.put("/logpass-columns", response_model=LogpassColumnSettings) +async def update_logpass_column_settings(body: LogpassColumnSettings): + data = _read() + data["logpass_columns"] = body.model_dump() + _write(data) + return data["logpass_columns"] + + +class TokenColumnSettings(BaseModel): + browser: bool = True + profile: bool = True + last_online: bool = True + steam_id: bool = True + login: bool = True + token: bool = True + status: bool = True + proxy: bool = True + notes: bool = True + actions: bool = True + + +@router.get("/token-columns", response_model=TokenColumnSettings) +async def get_token_column_settings(): + data = _read() + return data.get("token_columns", DEFAULTS["token_columns"]) + + +@router.put("/token-columns", response_model=TokenColumnSettings) +async def update_token_column_settings(body: TokenColumnSettings): + data = _read() + data["token_columns"] = body.model_dump() + _write(data) + return data["token_columns"] diff --git a/app/api/tasks.py b/app/api/tasks.py new file mode 100644 index 0000000..995aa46 --- /dev/null +++ b/app/api/tasks.py @@ -0,0 +1,101 @@ +import asyncio +import json + +from fastapi import APIRouter, HTTPException +from fastapi.responses import StreamingResponse +from loguru import logger + +from app.database import get_db +from app.models import TaskOut +from app.core.task_manager import task_manager + +router = APIRouter(prefix="/api/tasks", tags=["tasks"]) + + +@router.get("", response_model=list[TaskOut]) +async def list_tasks(): + db = await get_db() + cursor = await db.execute("SELECT * FROM tasks ORDER BY created_at DESC LIMIT 50") + rows = await cursor.fetchall() + return [dict(r) for r in rows] + + +@router.get("/{task_id}", response_model=TaskOut) +async def get_task(task_id: str): + db = await get_db() + cursor = await db.execute("SELECT * FROM tasks WHERE id = ?", (task_id,)) + row = await cursor.fetchone() + if not row: + raise HTTPException(status_code=404, detail="Task not found") + return dict(row) + + +@router.get("/{task_id}/stream") +async def stream_task(task_id: str): + """SSE endpoint for real-time task progress.""" + + async def event_generator(): + last_data = None + while True: + db = await get_db() + cursor = await db.execute("SELECT * FROM tasks WHERE id = ?", (task_id,)) + row = await cursor.fetchone() + if not row: + yield f"data: {json.dumps({'error': 'Task not found'})}\n\n" + break + + task = dict(row) + # Parse JSON string fields from DB into dicts for proper serialization + for json_field in ("account_ids", "account_results"): + if isinstance(task.get(json_field), str): + try: + task[json_field] = json.loads(task[json_field]) + except (json.JSONDecodeError, TypeError): + pass + prompt_info = task_manager.get_pending_prompt(task_id) + if prompt_info: + task["prompt"] = prompt_info["message"] + task["prompt_login"] = prompt_info["login"] + step_info = task_manager.get_step_info(task_id) + if step_info: + task["step"] = step_info["step"] + task["total_steps"] = step_info["total_steps"] + task["step_label"] = step_info["label"] + active = task_manager.get_active_count(task_id) + if active > 0: + task["active_count"] = active + acc_results = task_manager.get_account_results(task_id) + if acc_results: + task["account_results"] = {str(k): v for k, v in acc_results.items()} + acc_steps = task_manager.get_account_steps(task_id) + if acc_steps: + task["account_steps"] = {str(k): v for k, v in acc_steps.items()} + + data_str = json.dumps(task) + if data_str != last_data or task["status"] in ("completed", "failed", "cancelled"): + last_data = data_str + yield f"data: {data_str}\n\n" + + if task["status"] in ("completed", "failed", "cancelled"): + break + + await asyncio.sleep(0.5) + + return StreamingResponse(event_generator(), media_type="text/event-stream") + + +@router.post("/{task_id}/respond") +async def respond_to_prompt(task_id: str, body: dict): + """Provide user input for a pending task prompt.""" + value = body.get("value", "") + login = body.get("login", "") + if not task_manager.respond(task_id, value, login): + raise HTTPException(status_code=404, detail="No pending prompt for this task") + return {"status": "ok"} + + +@router.delete("/{task_id}", status_code=204) +async def cancel_task(task_id: str): + cancelled = task_manager.cancel(task_id) + if not cancelled: + raise HTTPException(status_code=404, detail="Task not found or already finished") diff --git a/app/api/token_accounts.py b/app/api/token_accounts.py new file mode 100644 index 0000000..1e5fa8e --- /dev/null +++ b/app/api/token_accounts.py @@ -0,0 +1,245 @@ +"""CRUD and validate endpoints for token accounts.""" + +import asyncio + +from fastapi import APIRouter, HTTPException +from loguru import logger + +from app.database import get_db +from app.core.task_manager import task_manager +from app.models import TokenAccountCreate, TokenAccountOut, TokenAccountUpdate + +router = APIRouter(prefix="/api/token-accounts", tags=["token-accounts"]) + + +@router.get("", response_model=list[TokenAccountOut]) +async def list_tokens(): + db = await get_db() + cursor = await db.execute("SELECT * FROM token_accounts ORDER BY id DESC") + rows = await cursor.fetchall() + return [dict(r) for r in rows] + + +@router.get("/{account_id}", response_model=TokenAccountOut) +async def get_token(account_id: int): + db = await get_db() + cursor = await db.execute("SELECT * FROM token_accounts WHERE id = ?", (account_id,)) + row = await cursor.fetchone() + if not row: + raise HTTPException(status_code=404, detail="Account not found") + return dict(row) + + +@router.post("", response_model=TokenAccountOut, status_code=201) +async def create_token(account: TokenAccountCreate): + db = await get_db() + cursor = await db.execute( + "INSERT INTO token_accounts (login, token, steam_id, proxy, notes) VALUES (?, ?, ?, ?, ?)", + (account.login, account.token, account.steam_id, account.proxy, account.notes), + ) + await db.commit() + new_cursor = await db.execute( + "SELECT * FROM token_accounts WHERE id = ?", (cursor.lastrowid,) + ) + return dict(await new_cursor.fetchone()) + + +@router.put("/{account_id}", response_model=TokenAccountOut) +async def update_token(account_id: int, account: TokenAccountUpdate): + db = await get_db() + fields = {k: v for k, v in account.model_dump().items() if v is not None} + if not fields: + raise HTTPException(status_code=400, detail="No fields to update") + fields["updated_at"] = "datetime('now')" + set_clause = ", ".join( + f"{k} = datetime('now')" if k == "updated_at" else f"{k} = ?" + for k in fields + ) + values = [v for k, v in fields.items() if k != "updated_at"] + values.append(account_id) + await db.execute(f"UPDATE token_accounts SET {set_clause} WHERE id = ?", values) + await db.commit() + cursor = await db.execute("SELECT * FROM token_accounts WHERE id = ?", (account_id,)) + row = await cursor.fetchone() + if not row: + raise HTTPException(status_code=404, detail="Account not found") + return dict(row) + + +@router.delete("/{account_id}", status_code=204) +async def delete_token(account_id: int): + db = await get_db() + await db.execute("DELETE FROM token_accounts WHERE id = ?", (account_id,)) + await db.commit() + + +@router.post("/delete-bulk") +async def delete_token_bulk(data: dict): + ids: list[int] = data.get("ids", []) + if not ids: + return {"deleted": 0} + db = await get_db() + placeholders = ",".join("?" for _ in ids) + await db.execute(f"DELETE FROM token_accounts WHERE id IN ({placeholders})", ids) + await db.commit() + return {"deleted": len(ids)} + + +@router.post("/import") +async def import_tokens(data: dict): + """Bulk import: one token per line, optionally 'login:token'.""" + lines: list[str] = data.get("lines", []) + imported = 0 + skipped = 0 + errors: list[str] = [] + + db = await get_db() + for raw in lines: + line = raw.strip() + if not line: + skipped += 1 + continue + login = None + token = line + if ":" in line: + parts = line.split(":", 1) + login, token = parts[0].strip(), parts[1].strip() + if not token: + skipped += 1 + continue + try: + await db.execute( + "INSERT OR IGNORE INTO token_accounts (login, token) VALUES (?, ?)", + (login, token), + ) + imported += 1 + except Exception as exc: + errors.append(f"{token[:20]}: {exc}") + await db.commit() + return {"imported": imported, "skipped": skipped, "errors": errors} + + +@router.post("/validate") +async def validate_tokens(data: dict): + """Submit validation task for selected token accounts.""" + ids: list[int] = data.get("account_ids", []) + if not ids: + raise HTTPException(status_code=400, detail="No account IDs provided") + + db = await get_db() + placeholders = ",".join("?" for _ in ids) + cursor = await db.execute( + f"SELECT * FROM token_accounts WHERE id IN ({placeholders})", ids + ) + accounts = [dict(r) for r in await cursor.fetchall()] + if not accounts: + raise HTTPException(status_code=404, detail="No accounts found") + + task_id = await task_manager.submit( + task_type="token_validate", + accounts=accounts, + params={}, + ) + logger.info(f"token_validate submitted for {len(accounts)} accounts → task {task_id}") + return {"task_id": task_id, "accounts_count": len(accounts)} + + +@router.post("/{account_id}/browser") +async def open_token_browser(account_id: int): + """Open Chrome browser with saved session cookies for a token account.""" + db = await get_db() + cursor = await db.execute("SELECT * FROM token_accounts WHERE id = ?", (account_id,)) + row = await cursor.fetchone() + if not row: + raise HTTPException(status_code=404, detail="Account not found") + + account = dict(row) + if not account.get("session_cookies"): + raise HTTPException(status_code=400, detail="No session cookies. Validate the account first.") + + from app.services.steam_auth import check_cookies_alive, _resolve_proxy + from app.config import read_validation_settings + + proxy = await _resolve_proxy(account) + alive = await check_cookies_alive(account["session_cookies"], proxy) + if not alive: + val_settings = read_validation_settings() + if val_settings.get("auto_revalidate_browser"): + task_id = await task_manager.submit( + task_type="token_validate", + accounts=[account], + params={}, + ) + logger.info(f"Cookies dead for token account {account_id}, auto-revalidating → task {task_id}") + return {"status": "revalidating", "message": "Cookies expired. Re-validating...", "task_id": task_id} + raise HTTPException(status_code=400, detail="Session cookies expired. Re-validate the account.") + + from app.services.browser_login import open_browser_with_cookies + + async def _run(): + try: + await open_browser_with_cookies(account) + except Exception as exc: + logger.error(f"Browser open failed for {account.get('login', account_id)}: {exc}") + + asyncio.create_task(_run()) + return {"status": "ok", "message": "Browser opening..."} + + +@router.post("/assign-proxies") +async def token_assign_proxies(): + """Round-robin assign proxies to token accounts that don't have one.""" + db = await get_db() + proxy_cursor = await db.execute("SELECT address FROM proxies ORDER BY id") + proxies = [r["address"] for r in await proxy_cursor.fetchall()] + if not proxies: + raise HTTPException(status_code=400, detail="No proxies available") + + acc_cursor = await db.execute( + "SELECT id FROM token_accounts WHERE proxy IS NULL OR proxy = '' ORDER BY id" + ) + account_ids = [r["id"] for r in await acc_cursor.fetchall()] + if not account_ids: + return {"assigned": 0, "proxies_used": 0} + + for i, acc_id in enumerate(account_ids): + await db.execute( + "UPDATE token_accounts SET proxy = ?, updated_at = datetime('now') WHERE id = ?", + (proxies[i % len(proxies)], acc_id), + ) + await db.commit() + return {"assigned": len(account_ids), "proxies_used": len(proxies)} + + +@router.post("/reassign-proxies") +async def token_reassign_proxies(): + """Round-robin reassign proxies to ALL token accounts.""" + db = await get_db() + proxy_cursor = await db.execute("SELECT address FROM proxies ORDER BY id") + proxies = [r["address"] for r in await proxy_cursor.fetchall()] + if not proxies: + raise HTTPException(status_code=400, detail="No proxies available") + + acc_cursor = await db.execute("SELECT id FROM token_accounts ORDER BY id") + account_ids = [r["id"] for r in await acc_cursor.fetchall()] + if not account_ids: + return {"assigned": 0, "proxies_used": 0} + + for i, acc_id in enumerate(account_ids): + await db.execute( + "UPDATE token_accounts SET proxy = ?, updated_at = datetime('now') WHERE id = ?", + (proxies[i % len(proxies)], acc_id), + ) + await db.commit() + return {"assigned": len(account_ids), "proxies_used": len(proxies)} + + +@router.post("/clear-proxies") +async def token_clear_proxies(): + """Remove proxy from all token accounts.""" + db = await get_db() + cursor = await db.execute( + "UPDATE token_accounts SET proxy = NULL, updated_at = datetime('now') WHERE proxy IS NOT NULL AND proxy != ''" + ) + await db.commit() + return {"cleared": cursor.rowcount} diff --git a/app/config.py b/app/config.py new file mode 100644 index 0000000..c73f9c4 --- /dev/null +++ b/app/config.py @@ -0,0 +1,50 @@ +from pathlib import Path +from pydantic_settings import BaseSettings + + +class Settings(BaseSettings): + app_name: str = "SteamPanel" + debug: bool = False + host: str = "127.0.0.1" + port: int = 8000 + + base_dir: Path = Path(__file__).resolve().parent.parent + data_dir: Path = base_dir / "data" + db_path: Path = data_dir / "accounts.db" + mafiles_dir: Path = data_dir / "mafiles" + logs_dir: Path = data_dir / "logs" + proxies_path: Path = data_dir / "proxies.json" + + max_concurrent_tasks: int = 5 + request_timeout: int = 30 + proxy_rotation_interval: int = 60 + + model_config = {"env_prefix": "STEAM_PANEL_"} + + +settings = Settings() + + +# Defaults for data/settings.json → "validation" section +_VALIDATION_DEFAULTS: dict = { + "fetch_profile": True, + "check_ban": True, + "max_threads": 10, + "account_timeout": 90, + "auto_revalidate_browser": True, +} + + +def read_validation_settings() -> dict: + """Read the 'validation' section from data/settings.json (sync, for small file).""" + import json as _json + + settings_path = settings.data_dir / "settings.json" + result = _VALIDATION_DEFAULTS.copy() + if settings_path.exists(): + try: + data = _json.loads(settings_path.read_text("utf-8")) + result.update(data.get("validation", {})) + except Exception: + pass + return result diff --git a/app/core/__init__.py b/app/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/core/auto_accept.py b/app/core/auto_accept.py new file mode 100644 index 0000000..b5cf221 --- /dev/null +++ b/app/core/auto_accept.py @@ -0,0 +1,120 @@ +"""AutoAcceptManager — manages per-account background loops for auto-accepting Steam logins.""" + +import asyncio + +import aiohttp +from loguru import logger + +from app.services.steam_auto_accept import mobile_login, get_pending_sessions, confirm_session +from app.database import get_db + +CHECK_INTERVAL = 15 # seconds + + +class AutoAcceptManager: + def __init__(self) -> None: + self._tasks: dict[int, asyncio.Task] = {} + self._tokens: dict[int, str] = {} + self._errors: dict[int, str] = {} + + async def start(self, account: dict) -> None: + account_id = account["id"] + if account_id in self._tasks and not self._tasks[account_id].done(): + return + + self._tasks[account_id] = asyncio.create_task(self._loop(account)) + logger.info(f"[auto-accept] Started monitoring for {account['login']} (id={account_id})") + + def stop(self, account_id: int) -> None: + task = self._tasks.pop(account_id, None) + self._tokens.pop(account_id, None) + self._errors.pop(account_id, None) + if task and not task.done(): + task.cancel() + logger.info(f"[auto-accept] Stopped monitoring for id={account_id}") + + def is_running(self, account_id: int) -> bool: + task = self._tasks.get(account_id) + return task is not None and not task.done() + + def running_ids(self) -> set[int]: + # Cleanup dead tasks + dead = [aid for aid, t in self._tasks.items() if t.done()] + for aid in dead: + self._tasks.pop(aid, None) + self._tokens.pop(aid, None) + return {aid for aid, t in self._tasks.items() if not t.done()} + + def get_errors(self) -> dict[int, str]: + """Return error messages for accounts that stopped unexpectedly.""" + return dict(self._errors) + + def pop_errors(self) -> dict[int, str]: + """Return and clear error messages.""" + errors = dict(self._errors) + self._errors.clear() + return errors + + async def _loop(self, account: dict) -> None: + account_id = account["id"] + login = account["login"] + + async with aiohttp.ClientSession() as session: + # Initial mobile login + token = await mobile_login(session, account) + if not token: + logger.error(f"[auto-accept] Initial login failed for {login}, stopping") + self._errors[account_id] = "Initial login failed" + await self._disable_in_db(account_id) + return + self._tokens[account_id] = token + + while True: + try: + await asyncio.sleep(CHECK_INTERVAL) + + client_ids = await get_pending_sessions(session, self._tokens[account_id]) + + # Token expired — re-login + if client_ids is None: + logger.warning(f"[auto-accept] Token expired for {login}, re-logging...") + new_token = await mobile_login(session, account) + if new_token: + self._tokens[account_id] = new_token + client_ids = await get_pending_sessions(session, new_token) + else: + logger.error(f"[auto-accept] Re-login failed for {login}, stopping") + self._errors[account_id] = "Re-login failed (token expired)" + await self._disable_in_db(account_id) + return + + if not client_ids: + continue + + for cid in client_ids: + await confirm_session(session, self._tokens[account_id], account, cid) + await asyncio.sleep(1) + + except asyncio.CancelledError: + logger.info(f"[auto-accept] Loop cancelled for {login}") + return + except Exception as exc: + logger.error(f"[auto-accept] Loop error for {login}: {exc}") + await asyncio.sleep(CHECK_INTERVAL) + + async def stop_all(self) -> None: + for aid in list(self._tasks): + self.stop(aid) + + async def _disable_in_db(self, account_id: int) -> None: + """Reset auto_accept flag in DB when task fails.""" + try: + db = await get_db() + await db.execute("UPDATE accounts SET auto_accept = 0 WHERE id = ?", (account_id,)) + await db.commit() + logger.info(f"[auto-accept] Disabled auto_accept in DB for id={account_id}") + except Exception as exc: + logger.error(f"[auto-accept] Failed to update DB for id={account_id}: {exc}") + + +auto_accept_manager = AutoAcceptManager() diff --git a/app/core/crypto.py b/app/core/crypto.py new file mode 100644 index 0000000..f109825 --- /dev/null +++ b/app/core/crypto.py @@ -0,0 +1,20 @@ +import base64 + +import rsa + + +def get_rsa_key(response_data: dict) -> tuple[str, str, int]: + """Extract RSA key components from Steam's getrsakey response.""" + mod = response_data["publickey_mod"] + exp = response_data["publickey_exp"] + timestamp = int(response_data["timestamp"]) + return mod, exp, timestamp + + +def encrypt_password(password: str, mod_hex: str, exp_hex: str) -> str: + """RSA-encrypt a password using Steam's public key, return base64.""" + mod = int(mod_hex, 16) + exp = int(exp_hex, 16) + public_key = rsa.PublicKey(mod, exp) + encrypted = rsa.encrypt(password.encode("ascii"), public_key) + return base64.b64encode(encrypted).decode("ascii") diff --git a/app/core/exceptions.py b/app/core/exceptions.py new file mode 100644 index 0000000..cae1f80 --- /dev/null +++ b/app/core/exceptions.py @@ -0,0 +1,35 @@ +class SteamPanelError(Exception): + def __init__(self, message: str, code: str = "unknown_error") -> None: + self.message = message + self.code = code + super().__init__(message) + + +class SteamAuthError(SteamPanelError): + def __init__(self, message: str = "Authentication failed") -> None: + super().__init__(message, code="auth_error") + + +class SteamGuardError(SteamPanelError): + def __init__(self, message: str = "Steam Guard operation failed") -> None: + super().__init__(message, code="guard_error") + + +class ProxyError(SteamPanelError): + def __init__(self, message: str = "Proxy error") -> None: + super().__init__(message, code="proxy_error") + + +class MafileError(SteamPanelError): + def __init__(self, message: str = "Invalid mafile") -> None: + super().__init__(message, code="mafile_error") + + +class TaskError(SteamPanelError): + def __init__(self, message: str = "Task execution failed") -> None: + super().__init__(message, code="task_error") + + +class EmailError(SteamPanelError): + def __init__(self, message: str = "Email operation failed") -> None: + super().__init__(message, code="email_error") diff --git a/app/core/logging.py b/app/core/logging.py new file mode 100644 index 0000000..0da0c0f --- /dev/null +++ b/app/core/logging.py @@ -0,0 +1,138 @@ +import logging +import re +import sys + +from loguru import logger + +from app.config import settings + + +LEVEL_ICONS = { + "TRACE": "[~]", + "DEBUG": "[.]", + "INFO": "[i]", + "SUCCESS": "[+]", + "WARNING": "[!]", + "ERROR": "[e]", + "CRITICAL":"[e]", +} + +LEVEL_COLORS = { + "TRACE": "\033[35m", + "DEBUG": "\033[37m", + "INFO": "\033[34m", + "SUCCESS": "\033[32m", + "WARNING": "\033[33m", + "ERROR": "\033[31m", + "CRITICAL":"\033[31m", +} +RESET = "\033[0m" + + +def _loguru_fmt(record: dict) -> str: + level = record["level"].name + icon = LEVEL_ICONS.get(level, "[i]") + color = LEVEL_COLORS.get(level, "") + ts = record["time"].strftime("%H:%M:%S") + msg = record["message"].replace("<", "\\<").replace("{", "{{").replace("}", "}}") + return f"{RESET}[{ts}] {color}{icon}{RESET} {msg}\n" + + +class _UvicornInterceptHandler(logging.Handler): + """Route all standard logging records (uvicorn, fastapi) into loguru.""" + + def emit(self, record: logging.LogRecord) -> None: + try: + level = logger.level(record.levelname).name + except ValueError: + level = record.levelno + + frame, depth = sys._getframe(6), 6 + while frame and frame.f_code.co_filename == logging.__file__: + frame = frame.f_back + depth += 1 + + logger.opt(depth=depth, exception=record.exc_info).log(level, record.getMessage()) + + +_logging_configured = False + + +def setup_logging() -> None: + global _logging_configured + if _logging_configured: + return + _logging_configured = True + + logger.remove() + + logger.add( + sys.stderr, + format=_loguru_fmt, + level="DEBUG" if settings.debug else "INFO", + colorize=False, + ) + + settings.logs_dir.mkdir(parents=True, exist_ok=True) + + def _file_fmt(record: dict) -> str: + level = record["level"].name + icon = LEVEL_ICONS.get(level, "[i]") + ts = record["time"].strftime("%Y-%m-%d %H:%M:%S") + msg = record["message"].replace("<", "\\<").replace("{", "{{").replace("}", "}}") + return f"[{ts}] {icon} {msg}\n" + + logger.add( + settings.logs_dir / "steampanel_{time:YYYY-MM-DD}.log", + format=_file_fmt, + level="DEBUG", + rotation="1 day", + retention="7 days", + compression="zip", + ) + + _install_intercept() + + +class _AioSQLiteFilter(logging.Filter): + """Reformats raw aiosqlite debug messages into readable DB operation lines.""" + + _OP_RE = re.compile(r"method (\w+) of sqlite3\.(\w+)") + _SQL_RE = re.compile(r',\s*"(.*?)"(?:,\s*(\(.*?\)))?\s*\)$', re.DOTALL) + + def filter(self, record: logging.LogRecord) -> bool: + msg = record.getMessage() + # Drop the redundant "operation X completed" lines + if " completed" in msg: + return False + if msg.startswith("executing"): + op_m = self._OP_RE.search(msg) + method = op_m.group(1).upper() if op_m else "OP" + sql_m = self._SQL_RE.search(msg) + if sql_m: + sql = sql_m.group(1).strip() + params = sql_m.group(2) or "" + record.msg = f"[DB] {sql}" + (f" {params}" if params else "") + else: + record.msg = f"[DB] {method}" + record.args = () + return True + + +def _install_intercept() -> None: + intercept = _UvicornInterceptHandler() + for name in ("", "uvicorn", "uvicorn.error", "uvicorn.access", "fastapi"): + log = logging.getLogger(name) + log.handlers = [intercept] + log.setLevel(logging.DEBUG) + log.propagate = False + + # Format aiosqlite SQL logs instead of silencing them + aiosqlite_log = logging.getLogger("aiosqlite") + aiosqlite_log.setLevel(logging.DEBUG) + aiosqlite_log.addFilter(_AioSQLiteFilter()) + + logging.getLogger("asyncio").setLevel(logging.WARNING) + + + diff --git a/app/core/proxy_manager.py b/app/core/proxy_manager.py new file mode 100644 index 0000000..d2d4f23 --- /dev/null +++ b/app/core/proxy_manager.py @@ -0,0 +1,78 @@ +import random + +import aiohttp +from aiohttp_socks import ProxyConnector +from loguru import logger + +from app.database import get_db + + +def build_proxy_url(address: str, protocol: str) -> str: + """Convert stored address to standard proxy URL (proto://login:pass@ip:port). + + The frontend always normalizes addresses to login:pass@ip:port (or plain ip:port), + so we just prepend the scheme. + """ + if "://" in address: + return address + scheme = protocol if protocol in ("socks5", "socks4") else "http" + return f"{scheme}://{address}" + + +class ProxyManager: + def __init__(self) -> None: + self._proxies: list[dict] = [] + self._index: int = 0 + + async def load(self) -> None: + db = await get_db() + cursor = await db.execute( + "SELECT * FROM proxies WHERE is_alive = 1 ORDER BY fail_count ASC" + ) + self._proxies = [dict(r) for r in await cursor.fetchall()] + logger.info(f"Loaded {len(self._proxies)} alive proxies") + + def get_next(self) -> dict | None: + if not self._proxies: + return None + proxy = self._proxies[self._index % len(self._proxies)] + self._index += 1 + return proxy + + def get_random(self) -> dict | None: + if not self._proxies: + return None + return random.choice(self._proxies) + + def get_connector(self, proxy: dict | None = None) -> aiohttp.TCPConnector | ProxyConnector: + if proxy is None: + return aiohttp.TCPConnector() + url = build_proxy_url(proxy["address"], proxy.get("protocol", "http")) + return ProxyConnector.from_url(url, ssl=False) + + async def mark_failed(self, proxy_id: int) -> None: + db = await get_db() + await db.execute( + "UPDATE proxies SET fail_count = fail_count + 1, last_checked = datetime('now') WHERE id = ?", + (proxy_id,), + ) + await db.execute( + "UPDATE proxies SET is_alive = 0 WHERE id = ? AND fail_count >= 5", + (proxy_id,), + ) + await db.commit() + + async def mark_alive(self, proxy_id: int) -> None: + db = await get_db() + await db.execute( + "UPDATE proxies SET fail_count = 0, is_alive = 1, last_checked = datetime('now') WHERE id = ?", + (proxy_id,), + ) + await db.commit() + + @property + def count(self) -> int: + return len(self._proxies) + + +proxy_manager = ProxyManager() diff --git a/app/core/task_manager.py b/app/core/task_manager.py new file mode 100644 index 0000000..0edfb6a --- /dev/null +++ b/app/core/task_manager.py @@ -0,0 +1,249 @@ +import asyncio +import json +import uuid +from pathlib import Path +from typing import Any, Protocol + +from loguru import logger + +from app.config import settings + +DEFAULT_MAX_PARALLEL = 10 +DEFAULT_ACCOUNT_TIMEOUT = 90 # fallback for actions not in the map below + +# Per-action timeouts (seconds). Fast read-only actions get 60s; +# write actions that involve SMS/email prompts get 120s. +TASK_TIMEOUTS: dict[str, int] = { + "validate": 60, + "remove_guard": 60, + "change_password": 60, + "random_password": 60, + "change_phone": 120, + "change_email": 120, +} + + +def _read_max_threads() -> int: + from app.config import read_validation_settings + return int(read_validation_settings().get("max_threads", DEFAULT_MAX_PARALLEL)) + + +def _read_account_timeout() -> int: + from app.config import read_validation_settings + return int(read_validation_settings().get("account_timeout", DEFAULT_ACCOUNT_TIMEOUT)) + + +class TaskHandler(Protocol): + async def __call__(self, account: dict, params: dict, *, task_id: str) -> None: ... + + +class TaskManager: + def __init__(self) -> None: + self._running: dict[str, asyncio.Task] = {} + self._handlers: dict[str, TaskHandler] = {} + self._prompts: dict[str, dict[str, Any]] = {} + self._steps: dict[str, dict[str, Any]] = {} + self._active_counts: dict[str, int] = {} + self._account_results: dict[str, dict[int, dict[str, str]]] = {} + self._account_steps: dict[str, dict[int, dict[str, int]]] = {} + + def register_handler(self, action: str, handler: TaskHandler) -> None: + self._handlers[action] = handler + + async def submit( + self, + task_type: str, + accounts: list[dict], + params: dict, + ) -> str: + task_id = uuid.uuid4().hex[:12] + account_ids = [acc.get("id", 0) for acc in accounts] + + from app.database import get_db + + db = await get_db() + await db.execute( + "INSERT INTO tasks (id, type, status, total, account_ids) VALUES (?, ?, 'running', ?, ?)", + (task_id, task_type, len(accounts), json.dumps(account_ids)), + ) + await db.commit() + + async_task = asyncio.create_task( + self._execute(task_id, task_type, accounts, params) + ) + self._running[task_id] = async_task + return task_id + + async def _execute( + self, + task_id: str, + task_type: str, + accounts: list[dict], + params: dict, + ) -> None: + from app.database import get_db + + handler = self._handlers.get(task_type) + if not handler: + db = await get_db() + await db.execute( + "UPDATE tasks SET status = 'failed', error = ? WHERE id = ?", + (f"No handler for action: {task_type}", task_id), + ) + await db.commit() + return + + completed = 0 + errors: list[str] = [] + lock = asyncio.Lock() + max_parallel = _read_max_threads() + account_timeout = TASK_TIMEOUTS.get(task_type, _read_account_timeout()) + sem = asyncio.Semaphore(max_parallel) + self._active_counts[task_id] = 0 + self._account_results[task_id] = {} + total = len(accounts) + + async def process_one(account: dict, idx: int) -> None: + nonlocal completed + login = account.get("login", "?") + acc_id = account.get("id", 0) + logger.debug(f"[{task_type}] ({idx}/{total}) processing {login}...") + + async with sem: + async with lock: + self._active_counts[task_id] = self._active_counts.get(task_id, 0) + 1 + try: + await asyncio.wait_for( + handler(account, params, task_id=task_id), + timeout=account_timeout, + ) + logger.success(f"[{task_type}] ({idx}/{total}) {login} — done") + async with lock: + self._account_results[task_id][acc_id] = {"status": "ok"} + except asyncio.TimeoutError: + async with lock: + errors.append(f"{login}: timed out after {account_timeout}s") + self._account_results[task_id][acc_id] = {"status": "error", "error": f"Timed out after {account_timeout}s"} + logger.error(f"[{task_type}] ({idx}/{total}) {login} — timed out after {account_timeout}s") + except Exception as exc: + err_str = str(exc) + async with lock: + errors.append(f"{login}: {exc}") + self._account_results[task_id][acc_id] = {"status": "error", "error": err_str} + logger.error(f"[{task_type}] ({idx}/{total}) {login} — error: {exc}") + finally: + async with lock: + self._active_counts[task_id] = max(0, self._active_counts.get(task_id, 0) - 1) + + async with lock: + completed += 1 + db = await get_db() + await db.execute( + "UPDATE tasks SET progress = ?, updated_at = datetime('now') WHERE id = ?", + (completed, task_id), + ) + await db.commit() + + try: + tasks = [process_one(acc, i) for i, acc in enumerate(accounts, 1)] + await asyncio.gather(*tasks) + + db = await get_db() + status = "completed" + result = f"Done: {completed - len(errors)} success, {len(errors)} errors" + error_text = "; ".join(errors[:10]) if errors else None + results_json = json.dumps( + {str(k): v for k, v in self._account_results.get(task_id, {}).items()} + ) + await db.execute( + "UPDATE tasks SET status = ?, result = ?, error = ?, account_results = ?, updated_at = datetime('now') WHERE id = ?", + (status, result, error_text, results_json, task_id), + ) + await db.commit() + + except asyncio.CancelledError: + db = await get_db() + results_json = json.dumps( + {str(k): v for k, v in self._account_results.get(task_id, {}).items()} + ) + await db.execute( + "UPDATE tasks SET status = 'cancelled', account_results = ?, updated_at = datetime('now') WHERE id = ?", + (results_json, task_id), + ) + await db.commit() + except Exception as exc: + logger.error(f"Task {task_id} fatal error: {exc}") + db = await get_db() + results_json = json.dumps( + {str(k): v for k, v in self._account_results.get(task_id, {}).items()} + ) + await db.execute( + "UPDATE tasks SET status = 'failed', error = ?, account_results = ?, updated_at = datetime('now') WHERE id = ?", + (str(exc)[:500], results_json, task_id), + ) + await db.commit() + finally: + self._running.pop(task_id, None) + self._steps.pop(task_id, None) + self._active_counts.pop(task_id, None) + self._account_results.pop(task_id, None) + self._account_steps.pop(task_id, None) + + def get_active_count(self, task_id: str) -> int: + return self._active_counts.get(task_id, 0) + + def get_account_results(self, task_id: str) -> dict[int, dict[str, str]]: + return self._account_results.get(task_id, {}) + + def clear_account_results(self, task_id: str) -> None: + self._account_results.pop(task_id, None) + + async def set_step(self, task_id: str, step: int, total_steps: int, label: str = "", acc_id: int = 0) -> None: + self._steps[task_id] = {"step": step, "total_steps": total_steps, "label": label} + if acc_id: + self._account_steps.setdefault(task_id, {})[acc_id] = {"step": step, "total": total_steps} + + def get_step_info(self, task_id: str) -> dict[str, Any] | None: + return self._steps.get(task_id) + + def get_account_steps(self, task_id: str) -> dict[int, dict[str, int]]: + return self._account_steps.get(task_id, {}) + + async def prompt_user(self, task_id: str, message: str, login: str = "") -> str: + """Pause task execution and ask the user for input via SSE prompt.""" + key = f"{task_id}:{login}" if login else task_id + event = asyncio.Event() + self._prompts[key] = {"message": message, "login": login, "event": event, "response": ""} + await event.wait() + response = self._prompts.pop(key, {}).get("response", "") + return response + + def respond(self, task_id: str, value: str, login: str = "") -> bool: + key = f"{task_id}:{login}" if login else task_id + prompt = self._prompts.get(key) + if not prompt: + return False + prompt["response"] = value + prompt["event"].set() + return True + + def get_pending_prompt(self, task_id: str) -> dict[str, str] | None: + """Return first pending prompt for this task (with login info).""" + prefix = f"{task_id}:" + for key, prompt in self._prompts.items(): + if key == task_id or key.startswith(prefix): + return {"message": prompt["message"], "login": prompt.get("login", "")} + return None + + def cancel(self, task_id: str) -> bool: + task = self._running.get(task_id) + if task and not task.done(): + task.cancel() + return True + return False + + def active_count(self) -> int: + return len(self._running) + + +task_manager = TaskManager() diff --git a/app/database.py b/app/database.py new file mode 100644 index 0000000..3dbeba7 --- /dev/null +++ b/app/database.py @@ -0,0 +1,200 @@ +import aiosqlite +from loguru import logger + +from app.config import settings + +import asyncio + +_db: aiosqlite.Connection | None = None +_db_lock = asyncio.Lock() + +SQL_CREATE_ACCOUNTS = """ +CREATE TABLE IF NOT EXISTS accounts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + login TEXT NOT NULL, + password TEXT NOT NULL, + steam_id TEXT UNIQUE, + email TEXT, + email_password TEXT, + phone TEXT, + mafile_path TEXT, + shared_secret TEXT, + identity_secret TEXT, + proxy TEXT, + status TEXT NOT NULL DEFAULT 'unknown', + notes TEXT, + nickname TEXT, + avatar_url TEXT, + steam_level INTEGER, + auto_accept INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')) +); +""" + +SQL_CREATE_PROXIES = """ +CREATE TABLE IF NOT EXISTS proxies ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + address TEXT NOT NULL UNIQUE, + protocol TEXT NOT NULL DEFAULT 'http', + is_alive INTEGER NOT NULL DEFAULT 1, + last_checked TEXT, + fail_count INTEGER NOT NULL DEFAULT 0 +); +""" + +SQL_CREATE_TASKS = """ +CREATE TABLE IF NOT EXISTS tasks ( + id TEXT PRIMARY KEY, + type TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'pending', + progress INTEGER NOT NULL DEFAULT 0, + total INTEGER NOT NULL DEFAULT 0, + result TEXT, + error TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')) +); +""" + +SQL_CREATE_LOGPASS_ACCOUNTS = """ +CREATE TABLE IF NOT EXISTS logpass_accounts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + login TEXT NOT NULL UNIQUE, + password TEXT NOT NULL, + steam_id TEXT, + proxy TEXT, + status TEXT NOT NULL DEFAULT 'unknown', + ban_status TEXT, + nickname TEXT, + steam_level INTEGER, + prime TEXT, + trophy TEXT, + behavior TEXT, + license TEXT, + notes TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')) +); +""" + +SQL_CREATE_TOKEN_ACCOUNTS = """ +CREATE TABLE IF NOT EXISTS token_accounts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + login TEXT, + token TEXT NOT NULL, + steam_id TEXT, + status TEXT NOT NULL DEFAULT 'unknown', + notes TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')) +); +""" + + +async def get_db() -> aiosqlite.Connection: + global _db + async with _db_lock: + if _db is None: + settings.data_dir.mkdir(parents=True, exist_ok=True) + _db = await aiosqlite.connect(str(settings.db_path)) + _db.row_factory = aiosqlite.Row + await _db.execute("PRAGMA journal_mode=WAL") + await _db.execute("PRAGMA foreign_keys=ON") + await _init_tables(_db) + logger.info(f"Database connected: {settings.db_path}") + return _db + + +async def _init_tables(db: aiosqlite.Connection) -> None: + await db.execute(SQL_CREATE_ACCOUNTS) + await db.execute(SQL_CREATE_PROXIES) + await db.execute(SQL_CREATE_TASKS) + await db.execute(SQL_CREATE_LOGPASS_ACCOUNTS) + await db.execute(SQL_CREATE_TOKEN_ACCOUNTS) + await _migrate(db) + await db.commit() + + +async def _migrate(db: aiosqlite.Connection) -> None: + """Add columns that may not exist in older DBs.""" + cursor = await db.execute("PRAGMA table_info(accounts)") + cols = {row[1] for row in await cursor.fetchall()} + migrations = [ + ("nickname", "ALTER TABLE accounts ADD COLUMN nickname TEXT"), + ("avatar_url", "ALTER TABLE accounts ADD COLUMN avatar_url TEXT"), + ("steam_level", "ALTER TABLE accounts ADD COLUMN steam_level INTEGER"), + ("auto_accept", "ALTER TABLE accounts ADD COLUMN auto_accept INTEGER NOT NULL DEFAULT 0"), + ("ban_status", "ALTER TABLE accounts ADD COLUMN ban_status TEXT"), + ("session_cookies", "ALTER TABLE accounts ADD COLUMN session_cookies TEXT"), + ("last_online", "ALTER TABLE accounts ADD COLUMN last_online TEXT"), + ] + for col, sql in migrations: + if col not in cols: + await db.execute(sql) + logger.info(f"Migration: added column '{col}' to accounts") + + cursor = await db.execute("PRAGMA table_info(tasks)") + task_cols = {row[1] for row in await cursor.fetchall()} + task_migrations = [ + ("account_ids", "ALTER TABLE tasks ADD COLUMN account_ids TEXT"), + ("account_results", "ALTER TABLE tasks ADD COLUMN account_results TEXT"), + ] + for col, sql in task_migrations: + if col not in task_cols: + await db.execute(sql) + logger.info(f"Migration: added column '{col}' to tasks") + + # Logpass accounts migrations + cursor = await db.execute("PRAGMA table_info(logpass_accounts)") + lp_cols = {row[1] for row in await cursor.fetchall()} + lp_migrations = [ + ("prime", "ALTER TABLE logpass_accounts ADD COLUMN prime TEXT"), + ("trophy", "ALTER TABLE logpass_accounts ADD COLUMN trophy TEXT"), + ("behavior", "ALTER TABLE logpass_accounts ADD COLUMN behavior TEXT"), + ("license", "ALTER TABLE logpass_accounts ADD COLUMN license TEXT"), + ("session_cookies", "ALTER TABLE logpass_accounts ADD COLUMN session_cookies TEXT"), + ("steam_level", "ALTER TABLE logpass_accounts ADD COLUMN steam_level INTEGER"), + ("avatar_url", "ALTER TABLE logpass_accounts ADD COLUMN avatar_url TEXT"), + ("last_online", "ALTER TABLE logpass_accounts ADD COLUMN last_online TEXT"), + ] + for col, sql in lp_migrations: + if col not in lp_cols: + await db.execute(sql) + logger.info(f"Migration: added column '{col}' to logpass_accounts") + + # Token accounts migrations + cursor = await db.execute("PRAGMA table_info(token_accounts)") + tk_cols = {row[1] for row in await cursor.fetchall()} + tk_migrations = [ + ("proxy", "ALTER TABLE token_accounts ADD COLUMN proxy TEXT"), + ("session_cookies", "ALTER TABLE token_accounts ADD COLUMN session_cookies TEXT"), + ("ban_status", "ALTER TABLE token_accounts ADD COLUMN ban_status TEXT"), + ("nickname", "ALTER TABLE token_accounts ADD COLUMN nickname TEXT"), + ("steam_level", "ALTER TABLE token_accounts ADD COLUMN steam_level INTEGER"), + ("avatar_url", "ALTER TABLE token_accounts ADD COLUMN avatar_url TEXT"), + ("last_online", "ALTER TABLE token_accounts ADD COLUMN last_online TEXT"), + ] + for col, sql in tk_migrations: + if col not in tk_cols: + await db.execute(sql) + logger.info(f"Migration: added column '{col}' to token_accounts") + + # Add UNIQUE index on login if missing + cursor = await db.execute("PRAGMA index_list(logpass_accounts)") + idx_rows = await cursor.fetchall() + idx_names = {row[1] for row in idx_rows} + if "uq_logpass_login" not in idx_names: + try: + await db.execute("CREATE UNIQUE INDEX uq_logpass_login ON logpass_accounts(login)") + logger.info("Migration: added unique index on logpass_accounts.login") + except Exception: + logger.warning("Could not create unique index on logpass_accounts.login (duplicates may exist)") + + +async def close_db() -> None: + global _db + if _db is not None: + await _db.close() + _db = None + logger.info("Database connection closed") diff --git a/app/models.py b/app/models.py new file mode 100644 index 0000000..e2a52ee --- /dev/null +++ b/app/models.py @@ -0,0 +1,246 @@ +import json +from datetime import datetime +from pathlib import Path + +from pydantic import BaseModel, Field, model_validator + + +class AccountCreate(BaseModel): + login: str + password: str + steam_id: str | None = None + email: str | None = None + email_password: str | None = None + phone: str | None = None + proxy: str | None = None + notes: str | None = None + + +class AccountUpdate(BaseModel): + login: str | None = None + password: str | None = None + steam_id: str | None = None + email: str | None = None + email_password: str | None = None + phone: str | None = None + proxy: str | None = None + status: str | None = None + notes: str | None = None + + +class AccountOut(BaseModel): + id: int + login: str + password: str + steam_id: str | None = None + email: str | None = None + email_password: str | None = None + phone: str | None = None + mafile_path: str | None = None + shared_secret: str | None = None + identity_secret: str | None = None + proxy: str | None = None + status: str + notes: str | None = None + nickname: str | None = None + avatar_url: str | None = None + steam_level: int | None = None + last_online: str | None = None + auto_accept: int = 0 + ban_status: str | None = None + has_cookies: bool = False + has_revocation_code: bool = False + created_at: str + updated_at: str + + @staticmethod + def _check_revocation_code(mafile_path: str | None) -> bool: + if not mafile_path: + return False + try: + p = Path(mafile_path) + if not p.exists(): + return False + data = json.loads(p.read_text(encoding="utf-8")) + return bool(data.get("revocation_code", "")) + except Exception: + return False + + @model_validator(mode="before") + @classmethod + def _compute_flags(cls, data): + if isinstance(data, dict): + data["has_cookies"] = bool(data.get("session_cookies")) + data["has_revocation_code"] = AccountOut._check_revocation_code(data.get("mafile_path")) + return data + + +class ProxyCreate(BaseModel): + address: str + protocol: str = "http" + + +class ProxyOut(BaseModel): + id: int + address: str + protocol: str + is_alive: bool + last_checked: str | None = None + fail_count: int + + +class MafileSession(BaseModel): + SessionID: str = "" + AccessToken: str = "" + RefreshToken: str = "" + SteamID: int = 0 + SteamLoginSecure: str = "" + + +class MafileData(BaseModel): + shared_secret: str + serial_number: str = "" + revocation_code: str = "" + uri: str = "" + account_name: str = "" + token_gid: str = "" + identity_secret: str = "" + secret_1: str = "" + device_id: str = "" + server_time: str = "" + fully_enrolled: bool = False + Session: MafileSession = Field(default_factory=MafileSession) + + +class TaskOut(BaseModel): + id: str + type: str + status: str + progress: int + total: int + result: str | None = None + error: str | None = None + account_ids: str | None = None + account_results: str | None = None + created_at: str + updated_at: str + + +class ActionRequest(BaseModel): + account_ids: list[int] + action: str + params: dict | None = None + + +class BulkImportResult(BaseModel): + imported: int = 0 + skipped: int = 0 + errors: list[str] = Field(default_factory=list) + + +# --------------------------------------------------------------------------- +# Log:pass accounts +# --------------------------------------------------------------------------- + +class LogpassAccountCreate(BaseModel): + login: str + password: str + steam_id: str | None = None + proxy: str | None = None + ban_status: str | None = None + prime: str | None = None + trophy: str | None = None + behavior: str | None = None + license: str | None = None + notes: str | None = None + + +class LogpassAccountUpdate(BaseModel): + login: str | None = None + password: str | None = None + steam_id: str | None = None + proxy: str | None = None + status: str | None = None + ban_status: str | None = None + nickname: str | None = None + steam_level: int | None = None + prime: str | None = None + trophy: str | None = None + behavior: str | None = None + license: str | None = None + notes: str | None = None + + +class LogpassAccountOut(BaseModel): + id: int + login: str + password: str + steam_id: str | None = None + proxy: str | None = None + status: str + ban_status: str | None = None + nickname: str | None = None + steam_level: int | None = None + prime: str | None = None + trophy: str | None = None + behavior: str | None = None + license: str | None = None + notes: str | None = None + avatar_url: str | None = None + last_online: str | None = None + has_cookies: bool = False + created_at: str + updated_at: str + + @model_validator(mode="before") + @classmethod + def _compute_has_cookies(cls, data): + if isinstance(data, dict): + data["has_cookies"] = bool(data.get("session_cookies")) + return data + + +# --------------------------------------------------------------------------- +# Token accounts +# --------------------------------------------------------------------------- + +class TokenAccountCreate(BaseModel): + login: str | None = None + token: str + steam_id: str | None = None + proxy: str | None = None + notes: str | None = None + + +class TokenAccountUpdate(BaseModel): + login: str | None = None + token: str | None = None + steam_id: str | None = None + proxy: str | None = None + status: str | None = None + notes: str | None = None + + +class TokenAccountOut(BaseModel): + id: int + login: str | None = None + token: str + steam_id: str | None = None + proxy: str | None = None + status: str + ban_status: str | None = None + nickname: str | None = None + steam_level: int | None = None + avatar_url: str | None = None + last_online: str | None = None + has_cookies: bool = False + notes: str | None = None + created_at: str + updated_at: str + + @model_validator(mode="before") + @classmethod + def _compute_has_cookies(cls, data): + if isinstance(data, dict): + data["has_cookies"] = bool(data.get("session_cookies")) + return data diff --git a/app/services/__init__.py b/app/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/services/browser_login.py b/app/services/browser_login.py new file mode 100644 index 0000000..b284c6d --- /dev/null +++ b/app/services/browser_login.py @@ -0,0 +1,96 @@ +import asyncio +import json + +from loguru import logger + +_active_browsers: list = [] + +STEAM_DOMAINS = [ + ".steamcommunity.com", + ".steampowered.com", + ".store.steampowered.com", + ".help.steampowered.com", + ".login.steampowered.com", +] + + +async def open_browser_with_cookies(account: dict) -> None: + """Open a Chrome browser with Steam session cookies using nodriver.""" + import nodriver as uc + + cookies_json = account.get("session_cookies") + if not cookies_json: + raise ValueError("No session cookies saved. Validate the account first.") + + cookies = json.loads(cookies_json) + + browser = await uc.start() + _active_browsers.append(browser) + + tab = await browser.get("about:blank") + + for c in cookies: + domain = c.get("domain") or ".steamcommunity.com" + domains_to_set = [domain] + if "steam" in domain: + domains_to_set = list(set([domain] + STEAM_DOMAINS)) + + for d in domains_to_set: + try: + await tab.send(uc.cdp.network.set_cookie( + name=c["name"], + value=c["value"], + domain=d, + path=c.get("path") or "/", + secure=c.get("secure", True), + http_only=c.get("httpOnly", False), + )) + except Exception: + pass + + steam_id = account.get("steam_id") or "" + if steam_id: + start_url = f"https://steamcommunity.com/profiles/{steam_id}/" + else: + start_url = "https://steamcommunity.com/" + await browser.get(start_url) + logger.info(f"Browser opened for {account['login']}") + + # Wait until browser is closed by user, suppress connection errors + try: + while browser: + await asyncio.sleep(1) + try: + if not browser.connection or browser.connection.closed: + break + except Exception: + break + except Exception: + pass + finally: + if browser in _active_browsers: + _active_browsers.remove(browser) + + # Temporarily suppress nodriver's internal task errors during cleanup + # (Browser.update_targets() raises ConnectionRefusedError after close) + loop = asyncio.get_running_loop() + _original_handler = loop.get_exception_handler() + + def _suppress_nodriver_close_errors(loop, context): + exc = context.get("exception") + if isinstance(exc, (ConnectionRefusedError, ConnectionResetError, OSError)): + return + if _original_handler: + _original_handler(loop, context) + else: + loop.default_exception_handler(context) + + loop.set_exception_handler(_suppress_nodriver_close_errors) + try: + browser.stop() + except Exception: + pass + await asyncio.sleep(0.5) + loop.set_exception_handler(_original_handler) + + logger.debug(f"Browser closed for {account['login']}") diff --git a/app/services/registry.py b/app/services/registry.py new file mode 100644 index 0000000..5a7258a --- /dev/null +++ b/app/services/registry.py @@ -0,0 +1,20 @@ +from app.core.task_manager import task_manager +from app.services.steam_guard import remove_guard, generate_2fa +from app.services.steam_password import change_password, random_password +from app.services.steam_email import change_email, validate_account +from app.services.steam_phone import change_phone +from app.services.steam_checker import check_logpass_account, full_parse_logpass_account +from app.services.steam_token_checker import check_token_account + + +def register_all_handlers() -> None: + task_manager.register_handler("change_password", change_password) + task_manager.register_handler("random_password", random_password) + task_manager.register_handler("change_email", change_email) + task_manager.register_handler("change_phone", change_phone) + task_manager.register_handler("remove_guard", remove_guard) + task_manager.register_handler("generate_2fa", generate_2fa) + task_manager.register_handler("validate", validate_account) + task_manager.register_handler("logpass_validate", check_logpass_account) + task_manager.register_handler("logpass_full_parse", full_parse_logpass_account) + task_manager.register_handler("token_validate", check_token_account) diff --git a/app/services/steam_auth.py b/app/services/steam_auth.py new file mode 100644 index 0000000..78c8a01 --- /dev/null +++ b/app/services/steam_auth.py @@ -0,0 +1,192 @@ +from __future__ import annotations + +import json +from pathlib import Path +from typing import TYPE_CHECKING + +import aiohttp +from aiohttp import ClientSession, ClientTimeout +from loguru import logger + +from app.core.exceptions import SteamAuthError +from app.core.proxy_manager import proxy_manager +from pysteamauth.base.request import BaseRequestStrategy, DEFAULT_REQUEST_TIMEOUT + + +class ProxyRequestStrategy(BaseRequestStrategy): + """Request strategy that routes traffic through a proxy connector.""" + + def __init__(self, connector: aiohttp.BaseConnector): + super().__init__() + self._proxy_connector = connector + + def _create_session(self) -> ClientSession: + return ClientSession( + connector=self._proxy_connector, + timeout=DEFAULT_REQUEST_TIMEOUT, + ) + + +def read_mafile_data(account: dict) -> dict | None: + """Read and parse the account's mafile JSON from disk.""" + mafile_path = account.get("mafile_path") + if not mafile_path: + return None + try: + return json.loads(Path(mafile_path).read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError, ValueError): + return None + + +async def _resolve_proxy(account: dict) -> dict | None: + """Determine proxy dict {address, protocol} for the account.""" + proxy_address = account.get("proxy") + if proxy_address: + from app.database import get_db + db = await get_db() + cursor = await db.execute( + "SELECT protocol FROM proxies WHERE address = ?", (proxy_address,) + ) + row = await cursor.fetchone() + protocol = row["protocol"] if row else "http" + return {"address": proxy_address, "protocol": protocol} + if proxy_manager.count > 0: + return proxy_manager.get_next() + return None + + +async def create_steam_session(account: dict): + """Create an authenticated Steam session for an account.""" + try: + from pysteamauth.auth import Steam + except ImportError: + raise SteamAuthError("pysteamauth not installed: pip install pysteamauth") + + proxy = await _resolve_proxy(account) + request_strategy = None + if proxy: + connector = proxy_manager.get_connector(proxy) + request_strategy = ProxyRequestStrategy(connector) + logger.debug(f"Using proxy {proxy['protocol']}://{proxy['address']} for {account['login']}") + + mafile = read_mafile_data(account) + + shared_secret = account.get("shared_secret", "") + identity_secret = account.get("identity_secret", "") + device_id = None + steamid = None + + if mafile: + shared_secret = mafile.get("shared_secret", shared_secret) + identity_secret = mafile.get("identity_secret", identity_secret) + device_id = mafile.get("device_id") + steamid = int(mafile.get("Session", {}).get("SteamID", 0)) or None + + steam = Steam( + login=account["login"], + password=account["password"], + shared_secret=shared_secret, + identity_secret=identity_secret, + device_id=device_id, + steamid=steamid, + request_strategy=request_strategy, + ) + + try: + await steam.login_to_steam() + except Exception as exc: + await close_steam(steam) + exc_str = str(exc) + # Pydantic ValidationError from pysteamauth = Steam rejected login + if "validation error" in exc_str.lower() and "FinalizeLoginStatus" in exc_str: + # Extract the raw response dict from the error + m = __import__("re").search(r"input_value=(\{[^}]+\})", exc_str) + raw = m.group(1) if m else "" + logger.error(f"Auth failed for {account['login']}: Steam rejected login: {raw}") + raise SteamAuthError(f"Login failed: Steam rejected credentials ({raw})") + logger.error(f"Auth failed for {account['login']}: {exc}") + raise SteamAuthError(f"Login failed: {exc}") + + return steam + + +async def close_steam(steam) -> None: + """Close the underlying aiohttp session and proxy connector of a pysteamauth Steam object.""" + requests = getattr(steam, "_requests", None) + if requests is None: + return + session = getattr(requests, "_session", None) + if session is not None and not getattr(session, "closed", True): + try: + await session.close() + except Exception: + pass + requests._session = None + + connector = getattr(requests, "_proxy_connector", None) + if connector is not None and not getattr(connector, "closed", True): + try: + await connector.close() + except Exception: + pass + + +def extract_session_cookies(steam) -> str | None: + """Extract session cookies from a pysteamauth Steam object as JSON string. + + Returns JSON string of cookies list, or None if extraction fails. + """ + try: + _req = getattr(steam, "_requests", None) + _sess = getattr(_req, "_session", None) if _req else None + if not _sess or not hasattr(_sess, "cookie_jar"): + return None + all_cookies = [] + for c in _sess.cookie_jar: + all_cookies.append({ + "name": c.key, + "value": c.value, + "domain": c.get("domain", ""), + "path": c.get("path", "/"), + "secure": str(c.get("secure", "")).lower() == "true" or c.get("secure") is True, + "httpOnly": str(c.get("httponly", "")).lower() == "true" or c.get("httponly") is True, + }) + return json.dumps(all_cookies) + except Exception: + return None + +async def check_cookies_alive(cookies_json: str, proxy: dict | None = None) -> bool: + """Check if Steam session cookies are still valid. + + Makes a lightweight request to Steam's clientjstoken endpoint. + Returns True if logged in, False otherwise. + """ + try: + cookies = json.loads(cookies_json) + jar = aiohttp.CookieJar(unsafe=True) + for c in cookies: + jar.update_cookies( + {c["name"]: c["value"]}, + response_url=__import__("yarl").URL(f"https://{c.get('domain', '.steamcommunity.com').lstrip('.')}"), + ) + connector = proxy_manager.get_connector(proxy) if proxy else None + async with aiohttp.ClientSession(cookie_jar=jar, connector=connector) as session: + async with session.get( + "https://steamcommunity.com/chat/clientjstoken", + timeout=aiohttp.ClientTimeout(total=10), + ) as resp: + if resp.status != 200: + return False + data = await resp.json(content_type=None) + return bool(data.get("logged_in")) + except Exception: + return False + + +async def raw_request(steam, url: str, method: str = "GET", **kwargs) -> aiohttp.ClientResponse: + """Low-level request returning raw aiohttp.ClientResponse (for redirects, etc.).""" + from yarl import URL + + host = URL(url).host or "" + cookies = await steam.cookies(host) + return await steam._requests.request(url=url, method=method, cookies=cookies, **kwargs) diff --git a/app/services/steam_auto_accept.py b/app/services/steam_auto_accept.py new file mode 100644 index 0000000..649d92a --- /dev/null +++ b/app/services/steam_auto_accept.py @@ -0,0 +1,220 @@ +"""Steam auto-accept login confirmations — ported from legacy/steam_auto_accept_logins.py.""" + +import asyncio +import base64 +import hashlib +import hmac +import struct + +import aiohttp +import rsa +from loguru import logger + +from pysteamauth.pb2.steammessages_auth.steamclient_pb2 import ( + CAuthentication_GetPasswordRSAPublicKey_Request, + CAuthentication_GetPasswordRSAPublicKey_Response, + CAuthentication_BeginAuthSessionViaCredentials_Request, + CAuthentication_BeginAuthSessionViaCredentials_Response, + CAuthentication_UpdateAuthSessionWithSteamGuardCode_Request, + CAuthentication_PollAuthSessionStatus_Request, + CAuthentication_PollAuthSessionStatus_Response, + CAuthentication_GetAuthSessionsForAccount_Response, + CAuthentication_GetAuthSessionInfo_Request, + CAuthentication_GetAuthSessionInfo_Response, + CAuthentication_UpdateAuthSessionWithMobileConfirmation_Request, + CAuthentication_UpdateAuthSessionWithMobileConfirmation_Response, + EAuthTokenPlatformType, + EAuthSessionGuardType, +) + +BASE_URL = "https://api.steampowered.com" +HEADERS = {"User-Agent": "okhttp/4.9.2", "Cookie": "Steam_Language=english"} + + +def _generate_2fa_code(shared_secret: str, server_time: int | None = None) -> str: + """Generate a Steam Guard TOTP code from shared_secret.""" + import time as _time + + if server_time is None: + server_time = int(_time.time()) + key = base64.b64decode(shared_secret) + msg = struct.pack(">Q", server_time // 30) + mac = hmac.new(key, msg, hashlib.sha1).digest() + offset = mac[-1] & 0x0F + code_int = struct.unpack(">I", mac[offset : offset + 4])[0] & 0x7FFFFFFF + chars = "23456789BCDFGHJKMNPQRTVWXY" + code = "" + for _ in range(5): + code += chars[code_int % len(chars)] + code_int //= len(chars) + return code + + +def _confirmation_signature(shared_secret: str, client_id: int, steamid: int) -> bytes: + """HMAC-SHA256 signature for login confirmation (version=1 + client_id + steamid).""" + key = base64.b64decode(shared_secret) + data = struct.pack(" str | None: + """Perform mobile login flow, return access_token or None.""" + login = account["login"] + password = account["password"] + shared_secret = account.get("shared_secret", "") + + try: + # GetPasswordRSAPublicKey + req = CAuthentication_GetPasswordRSAPublicKey_Request() + req.account_name = login + encoded = base64.b64encode(req.SerializeToString()).decode() + + async with session.get( + f"{BASE_URL}/IAuthenticationService/GetPasswordRSAPublicKey/v1", + params={"origin": "SteamMobile", "input_protobuf_encoded": encoded}, + headers=HEADERS, + ) as resp: + if resp.status != 200: + logger.error(f"[auto-accept] RSA key failed for {login}: HTTP {resp.status}") + return None + rsa_resp = CAuthentication_GetPasswordRSAPublicKey_Response.FromString(await resp.read()) + + pub_key = rsa.PublicKey(int(rsa_resp.publickey_mod, 16), int(rsa_resp.publickey_exp, 16)) + encrypted_pw = base64.b64encode(rsa.encrypt(password.encode(), pub_key)).decode() + + # BeginAuthSession + begin = CAuthentication_BeginAuthSessionViaCredentials_Request() + begin.account_name = login + begin.encrypted_password = encrypted_pw + begin.encryption_timestamp = rsa_resp.timestamp + begin.platform_type = EAuthTokenPlatformType.k_EAuthTokenPlatformType_MobileApp + begin.device_friendly_name = "Android Device" + begin.persistence = 1 + + data = aiohttp.FormData() + data.add_field("input_protobuf_encoded", base64.b64encode(begin.SerializeToString()).decode()) + + async with session.post( + f"{BASE_URL}/IAuthenticationService/BeginAuthSessionViaCredentials/v1", + data=data, + headers=HEADERS, + ) as resp: + if resp.status != 200: + logger.error(f"[auto-accept] BeginAuth failed for {login}: HTTP {resp.status}") + return None + begin_resp = CAuthentication_BeginAuthSessionViaCredentials_Response.FromString(await resp.read()) + + # Send 2FA code + if shared_secret: + code = _generate_2fa_code(shared_secret) + upd = CAuthentication_UpdateAuthSessionWithSteamGuardCode_Request() + upd.client_id = begin_resp.client_id + upd.steamid = begin_resp.steamid + upd.code = code + upd.code_type = EAuthSessionGuardType.k_EAuthSessionGuardType_DeviceCode + + data = aiohttp.FormData() + data.add_field("input_protobuf_encoded", base64.b64encode(upd.SerializeToString()).decode()) + + async with session.post( + f"{BASE_URL}/IAuthenticationService/UpdateAuthSessionWithSteamGuardCode/v1", + data=data, + headers=HEADERS, + ) as resp: + if resp.status != 200: + logger.error(f"[auto-accept] 2FA submit failed for {login}: HTTP {resp.status}") + return None + + # PollAuthSessionStatus + poll = CAuthentication_PollAuthSessionStatus_Request() + poll.client_id = begin_resp.client_id + poll.request_id = begin_resp.request_id + + data = aiohttp.FormData() + data.add_field("input_protobuf_encoded", base64.b64encode(poll.SerializeToString()).decode()) + + async with session.post( + f"{BASE_URL}/IAuthenticationService/PollAuthSessionStatus/v1", + data=data, + headers=HEADERS, + ) as resp: + if resp.status != 200: + logger.error(f"[auto-accept] Poll failed for {login}: HTTP {resp.status}") + return None + poll_resp = CAuthentication_PollAuthSessionStatus_Response.FromString(await resp.read()) + + if not poll_resp.access_token: + logger.error(f"[auto-accept] No access_token for {login}") + return None + + logger.success(f"[auto-accept] Mobile login OK for {login}") + return poll_resp.access_token + + except Exception as exc: + logger.error(f"[auto-accept] Login error for {login}: {exc}") + return None + + +async def get_pending_sessions(session: aiohttp.ClientSession, access_token: str) -> list[int] | None: + """Get pending auth session client_ids. Returns None on 401 (token expired).""" + try: + async with session.get( + f"{BASE_URL}/IAuthenticationService/GetAuthSessionsForAccount/v1", + params={ + "access_token": access_token, + "origin": "SteamMobile", + "input_protobuf_encoded": "", + }, + headers=HEADERS, + ) as resp: + if resp.status == 401: + return None + if resp.status != 200: + return [] + data = CAuthentication_GetAuthSessionsForAccount_Response.FromString(await resp.read()) + return list(data.client_ids) + except Exception as exc: + logger.warning(f"[auto-accept] get_pending_sessions error: {exc}") + return [] + + +async def confirm_session( + session: aiohttp.ClientSession, + access_token: str, + account: dict, + client_id: int, +) -> bool: + """Confirm a pending login session.""" + steam_id = int(account.get("steam_id", 0)) + shared_secret = account.get("shared_secret", "") + if not steam_id or not shared_secret: + return False + + try: + signature = _confirmation_signature(shared_secret, client_id, steam_id) + + msg = CAuthentication_UpdateAuthSessionWithMobileConfirmation_Request() + msg.version = 1 + msg.client_id = client_id + msg.steamid = steam_id + msg.signature = signature + msg.confirm = True + msg.persistence = 1 + + data = aiohttp.FormData() + data.add_field("input_protobuf_encoded", base64.b64encode(msg.SerializeToString()).decode()) + + async with session.post( + f"{BASE_URL}/IAuthenticationService/UpdateAuthSessionWithMobileConfirmation/v1", + params={"access_token": access_token}, + data=data, + headers=HEADERS, + ) as resp: + if resp.status == 200: + logger.success(f"[auto-accept] Confirmed login for {account['login']} (client_id={client_id})") + return True + logger.warning(f"[auto-accept] Confirm failed for {account['login']}: HTTP {resp.status}") + return False + except Exception as exc: + logger.error(f"[auto-accept] Confirm error for {account['login']}: {exc}") + return False diff --git a/app/services/steam_ban.py b/app/services/steam_ban.py new file mode 100644 index 0000000..8f2ff18 --- /dev/null +++ b/app/services/steam_ban.py @@ -0,0 +1,29 @@ +"""Steam account ban/alert checking via supportmessages page.""" + +from loguru import logger + + +async def check_ban(steam) -> str: + """Check if account has active bans/alerts. + + Returns: 'BANNED', 'NO BAN', or '' on error. + """ + try: + response_html = await steam.request( + "https://store.steampowered.com/supportmessages/", + method="GET", + ) + + if isinstance(response_html, bytes): + response_html = response_html.decode("utf-8", errors="replace") + + if "support_message_page" in response_html or "This account has been locked" in response_html: + return "BANNED" + elif "It doesn't appear that you have any active account alerts" in response_html: + return "NO BAN" + else: + return "NO BAN" + + except Exception as exc: + logger.warning(f"Ban check failed: {exc}") + return "" diff --git a/app/services/steam_checker.py b/app/services/steam_checker.py new file mode 100644 index 0000000..dc45ecc --- /dev/null +++ b/app/services/steam_checker.py @@ -0,0 +1,297 @@ +"""Validation service for log:pass accounts (no mafile).""" + +from pathlib import Path + +from bs4 import BeautifulSoup +from loguru import logger + +from app.services.steam_auth import _resolve_proxy, ProxyRequestStrategy, close_steam, extract_session_cookies +from app.services.steam_ban import check_ban +from app.services.steam_profile import fetch_profile +from app.core.proxy_manager import proxy_manager +from app.core.task_manager import task_manager + + +async def check_logpass_account(account: dict, params: dict, *, task_id: str) -> None: + """Login with login:pass only, get steamid/nickname, check ban, update DB row.""" + try: + from pysteamauth.auth import Steam + except ImportError: + raise RuntimeError("pysteamauth not installed") + + acc_id = account["id"] + proxy = await _resolve_proxy(account) + request_strategy = None + if proxy: + connector = proxy_manager.get_connector(proxy) + request_strategy = ProxyRequestStrategy(connector) + logger.debug(f"[checker] Using proxy for {account['login']}") + + steam = Steam( + login=account["login"], + password=account["password"], + request_strategy=request_strategy, + ) + + try: + await task_manager.set_step(task_id, 1, 3, "Авторизация", acc_id) + + # Pre-validate password before attempting RSA encryption (Steam RSA-2048 max 245 bytes) + password = account.get("password") or "" + if len(password.encode("utf-8")) > 245: + raise ValueError( + f"Password is too long ({len(password)} chars) — possible mafile data imported as password. " + "Re-import the account in login:password format." + ) + + await steam.login_to_steam() + + # Save session cookies right after login + try: + session_cookies_json = extract_session_cookies(steam) + if session_cookies_json: + from app.database import get_db as _get_db + _db = await _get_db() + await _db.execute( + "UPDATE logpass_accounts SET session_cookies = ? WHERE id = ?", + (session_cookies_json, acc_id), + ) + await _db.commit() + logger.debug(f"[checker] {account['login']}: saved session cookies") + except Exception as cookie_err: + logger.warning(f"[checker] {account['login']}: failed to save cookies — {cookie_err}") + + from app.config import read_validation_settings + val_settings = read_validation_settings() + + steam_id = str(steam.steamid) + ban_status = "" + if val_settings.get("check_ban"): + await task_manager.set_step(task_id, 2, 3, "Проверка бана", acc_id) + ban_status = await check_ban(steam) + + nickname = None + steam_level = None + avatar_url = None + last_online = None + if steam_id and steam_id != "0" and val_settings.get("fetch_profile"): + await task_manager.set_step(task_id, 3, 3, "Получение профиля", acc_id) + profile = await fetch_profile(steam_id, steam=steam) + if profile: + nickname = profile.get("nickname") + steam_level = profile.get("steam_level") + avatar_url = profile.get("avatar_url") + last_online = profile.get("last_online") + + from app.database import get_db + db = await get_db() + await db.execute( + """UPDATE logpass_accounts + SET steam_id = ?, nickname = ?, steam_level = ?, avatar_url = ?, last_online = ?, + ban_status = ?, status = 'valid', updated_at = datetime('now') + WHERE id = ?""", + (steam_id, nickname, steam_level, avatar_url, last_online, ban_status, acc_id), + ) + await db.commit() + logger.success(f"[checker] {account['login']} → valid, ban={ban_status}") + + except Exception as exc: + from app.database import get_db + db = await get_db() + await db.execute( + "UPDATE logpass_accounts SET status = 'invalid', updated_at = datetime('now') WHERE id = ?", + (acc_id,), + ) + await db.commit() + logger.error(f"[checker] {account['login']} → {exc}") + raise + + finally: + await close_steam(steam) + + +async def _check_cs2_prime(steam, steam_id: str) -> str: + """Check CS2 Prime status.""" + try: + url = f"https://steamcommunity.com/profiles/{steam_id}/gcpd/730/?tab=primeaccount" + resp = await steam.request(url, method="GET") + html = resp if isinstance(resp, str) else resp.decode("utf-8", errors="ignore") + if "no_personal_data_stored_message" in html: + return "Disabled" + if "personaldata_elements_container" in html and "Enabled" in html: + return "Enabled" + return "Disabled" + except Exception as e: + logger.debug(f"[full_parse] prime check error: {e}") + return "Disabled" + + +async def _check_dota_trophy(steam, steam_id: str) -> str | None: + """Check Dota 2 Trophy Score.""" + try: + url = f"https://steamcommunity.com/profiles/{steam_id}/gcpd/570/?category=Stats&tab=Trophy" + resp = await steam.request(url, method="GET") + html = resp if isinstance(resp, str) else resp.decode("utf-8", errors="ignore") + if "no_personal_data_stored_message" in html: + return None + if "personaldata_elements_container" in html: + soup = BeautifulSoup(html, "html.parser") + for row in soup.find_all("tr"): + cells = row.find_all("td") + if len(cells) >= 3: + try: + int(cells[0].get_text(strip=True)) + score = cells[1].get_text(strip=True) + if score.isdigit(): + return score + except (ValueError, AttributeError): + continue + return None + except Exception as e: + logger.debug(f"[full_parse] trophy check error: {e}") + return None + + +async def _check_dota_behavior(steam, steam_id: str) -> str | None: + """Check Dota 2 Behavior Score.""" + try: + url = f"https://steamcommunity.com/profiles/{steam_id}/gcpd/570/?category=Account&tab=MatchPlayerReportIncoming" + resp = await steam.request(url, method="GET") + html = resp if isinstance(resp, str) else resp.decode("utf-8", errors="ignore") + if "no_personal_data_stored_message" in html: + return None + if "personaldata_elements_container" in html: + soup = BeautifulSoup(html, "html.parser") + for row in soup.find_all("tr"): + cells = row.find_all("td") + if cells: + last_cell = cells[-1].get_text(strip=True) + if last_cell.isdigit(): + return last_cell + return None + except Exception as e: + logger.debug(f"[full_parse] behavior check error: {e}") + return None + + +async def _check_licenses(steam) -> str: + """Get list of licenses (games) from Steam account.""" + try: + resp = await steam.request("https://store.steampowered.com/account/licenses/", method="GET") + html = resp if isinstance(resp, str) else resp.decode("utf-8", errors="ignore") + soup = BeautifulSoup(html, "html.parser") + table = soup.find("table", class_="account_table") + if not table: + return "" + licenses = [] + rows = table.find_all("tr") + for row in rows[1:]: + cells = row.find_all("td") + if len(cells) >= 2: + game_cell = cells[1] + remove_div = game_cell.find("div", class_="free_license_remove_link") + if remove_div: + remove_div.decompose() + name = game_cell.get_text(strip=True) + if name: + licenses.append(name) + return ", ".join(licenses) + except Exception as e: + logger.debug(f"[full_parse] licenses check error: {e}") + return "" + + +async def full_parse_logpass_account(account: dict, params: dict, *, task_id: str) -> None: + """Full parse: login, ban, profile, prime, trophy, behavior, licenses.""" + try: + from pysteamauth.auth import Steam + except ImportError: + raise RuntimeError("pysteamauth not installed") + + acc_id = account["id"] + proxy = await _resolve_proxy(account) + request_strategy = None + if proxy: + connector = proxy_manager.get_connector(proxy) + request_strategy = ProxyRequestStrategy(connector) + + steam = Steam( + login=account["login"], + password=account["password"], + request_strategy=request_strategy, + ) + + try: + await task_manager.set_step(task_id, 1, 6, "Авторизация", acc_id) + await steam.login_to_steam() + + # Save session cookies + try: + session_cookies_json = extract_session_cookies(steam) + if session_cookies_json: + from app.database import get_db as _get_db + _db = await _get_db() + await _db.execute( + "UPDATE logpass_accounts SET session_cookies = ? WHERE id = ?", + (session_cookies_json, acc_id), + ) + await _db.commit() + except Exception as cookie_err: + logger.warning(f"[full_parse] {account['login']}: failed to save cookies — {cookie_err}") + + steam_id = str(steam.steamid) + + await task_manager.set_step(task_id, 2, 6, "Проверка бана", acc_id) + ban_status = await check_ban(steam) + + nickname = avatar_url = last_online = None + steam_level = None + if steam_id and steam_id != "0": + await task_manager.set_step(task_id, 3, 6, "Профиль", acc_id) + profile = await fetch_profile(steam_id, steam=steam) + if profile: + nickname = profile.get("nickname") + steam_level = profile.get("steam_level") + avatar_url = profile.get("avatar_url") + last_online = profile.get("last_online") + + prime = "Disabled" + trophy = None + behavior = None + if steam_id and steam_id != "0": + await task_manager.set_step(task_id, 4, 6, "Prime / Trophy", acc_id) + prime = await _check_cs2_prime(steam, steam_id) + trophy = await _check_dota_trophy(steam, steam_id) + behavior = await _check_dota_behavior(steam, steam_id) + + await task_manager.set_step(task_id, 5, 6, "Лицензии", acc_id) + license_str = await _check_licenses(steam) + + await task_manager.set_step(task_id, 6, 6, "Сохранение", acc_id) + from app.database import get_db + db = await get_db() + await db.execute( + """UPDATE logpass_accounts + SET steam_id = ?, nickname = ?, steam_level = ?, avatar_url = ?, last_online = ?, + ban_status = ?, prime = ?, trophy = ?, behavior = ?, license = ?, + status = 'valid', updated_at = datetime('now') + WHERE id = ?""", + (steam_id, nickname, steam_level, avatar_url, last_online, + ban_status, prime, trophy, behavior, license_str, acc_id), + ) + await db.commit() + logger.success(f"[full_parse] {account['login']} → valid, ban={ban_status}, prime={prime}, trophy={trophy}, behavior={behavior}, licenses={len(license_str.split(', ')) if license_str else 0}") + + except Exception as exc: + from app.database import get_db + db = await get_db() + await db.execute( + "UPDATE logpass_accounts SET status = 'invalid', updated_at = datetime('now') WHERE id = ?", + (acc_id,), + ) + await db.commit() + logger.error(f"[full_parse] {account['login']} → {exc}") + raise + + finally: + await close_steam(steam) diff --git a/app/services/steam_email.py b/app/services/steam_email.py new file mode 100644 index 0000000..a570d24 --- /dev/null +++ b/app/services/steam_email.py @@ -0,0 +1,201 @@ +import asyncio +import json + +from loguru import logger + +from app.services.steam_auth import create_steam_session, close_steam, extract_session_cookies +from app.services.steam_wizard import AJAX_HEADERS, run_common_wizard + + +def _get_fresh_sessionid(steam) -> str: + """Get sessionid from the live aiohttp cookie jar.""" + return steam._requests.cookies("help.steampowered.com").get("sessionid", "") + + +async def change_email(account: dict, params: dict, task_id: str = "") -> None: + """Change account email via Steam recovery wizard + interactive prompts.""" + from app.core.task_manager import task_manager + + login = account["login"] + new_email = params.get("new_email", "") + await task_manager.set_step(task_id, 1, 5, "Авторизация", acc_id=account["id"]) + logger.info(f"[change_email] {login}: logging in to Steam...") + steam = await create_steam_session(account) + try: + password = account["password"] + await task_manager.set_step(task_id, 2, 5, "Wizard", acc_id=account["id"]) + logger.info(f"[change_email] {login}: auth ok, starting email change wizard...") + + wizard = await run_common_wizard( + steam, + entry_url="https://help.steampowered.com/wizard/HelpChangeEmail?redir=store/account/", + login=login, + password=password, + ) + logger.info(f"[change_email] {login}: wizard done") + + if not new_email: + new_email = await task_manager.prompt_user(task_id, f"Введите новый email для {login}", login=login) + if not new_email: + raise ValueError("new_email is required") + + await task_manager.set_step(task_id, 3, 5, "Запрос смены", acc_id=account["id"]) + logger.info(f"[change_email] {login}: requesting email change -> {new_email}...") + + for attempt in range(2): + fresh_sid = _get_fresh_sessionid(steam) + text = await steam.request( + url="https://help.steampowered.com/en/wizard/AjaxAccountRecoveryChangeEmail/", + method="POST", + data={ + "s": wizard.s, + "account": wizard.account, + "sessionid": fresh_sid, + "wizard_ajax": 1, + "gamepad": 0, + "email": new_email, + }, + headers=AJAX_HEADERS, + ) + data = json.loads(text) if isinstance(text, str) else text + logger.debug(f"[change_email] {login}: change request attempt {attempt+1} response: {data}") + if data.get("errorMsg"): + error_code = data.get("success", 0) + if error_code == 24: + raise PermissionError(f"Insufficient privileges for email change: {data['errorMsg']}") + raise RuntimeError(f"Email change request failed: {data['errorMsg']}") + + if attempt == 0: + await asyncio.sleep(3) + + await task_manager.set_step(task_id, 4, 5, "Код подтверждения", acc_id=account["id"]) + email_code = await task_manager.prompt_user(task_id, f"Введите код подтверждения с email ({new_email})", login=login) + if not email_code: + raise ValueError("email_code is required") + email_code = email_code.strip() + + logger.info(f"[change_email] {login}: confirming email change...") + fresh_sid = _get_fresh_sessionid(steam) + confirm_text = await steam.request( + url="https://help.steampowered.com/en/wizard/AjaxAccountRecoveryConfirmChangeEmail/", + method="POST", + data={ + "s": wizard.s, + "account": wizard.account, + "sessionid": fresh_sid, + "wizard_ajax": 1, + "gamepad": 0, + "email": new_email, + "email_change_code": email_code, + }, + headers=AJAX_HEADERS, + ) + confirm_data = json.loads(confirm_text) if isinstance(confirm_text, str) else confirm_text + logger.debug(f"[change_email] {login}: confirm response: success={confirm_data.get('success')}") + + error_code = confirm_data.get("success", 0) + if confirm_data.get("errorMsg") and error_code != 29: + raise RuntimeError(f"Email confirmation failed: {confirm_data['errorMsg']}") + + await task_manager.set_step(task_id, 5, 5, "Сохранение", acc_id=account["id"]) + from app.database import get_db + db = await get_db() + await db.execute( + "UPDATE accounts SET email = ?, updated_at = datetime('now') WHERE id = ?", + (new_email, account["id"]), + ) + await db.commit() + + logger.success(f"[change_email] {login}: email changed successfully -> {new_email}") + finally: + await close_steam(steam) + + +async def validate_account(account: dict, params: dict, task_id: str = "") -> None: + """Validate account credentials by attempting login. Fetches profile and ban status on success.""" + from app.database import get_db + from app.services.steam_auth import close_steam + from app.services.steam_profile import fetch_profile + from app.services.steam_ban import check_ban + from app.core.task_manager import task_manager + from app.config import read_validation_settings + + steam = None + error_msg = None + ban_status = "" + + val_settings = read_validation_settings() + + login = account["login"] + profile = None + try: + await task_manager.set_step(task_id, 1, 4, "Авторизация", acc_id=account["id"]) + logger.info(f"[validate] {login}: logging in to Steam...") + steam = await create_steam_session(account) + status = "valid" + logger.info(f"[validate] {login}: auth ok") + + if status == "valid" and steam is not None: + if val_settings.get("check_ban"): + await task_manager.set_step(task_id, 2, 4, "Проверка бана", acc_id=account["id"]) + logger.info(f"[validate] {login}: checking ban status...") + ban_status = await check_ban(steam) + logger.info(f"[validate] {login}: ban = {ban_status or 'none'}") + if account.get("steam_id") and val_settings.get("fetch_profile"): + await task_manager.set_step(task_id, 3, 4, "Профиль", acc_id=account["id"]) + logger.info(f"[validate] {login}: fetching profile (id={account['steam_id']})...") + profile = await fetch_profile(account["steam_id"], steam=steam) + if profile: + logger.info(f"[validate] {login}: profile fetched — {profile.get('nickname')}, lvl={profile.get('steam_level')}") + else: + logger.warning(f"[validate] {login}: failed to fetch profile") + + await task_manager.set_step(task_id, 4, 4, "Сохранение", acc_id=account["id"]) + if steam is not None and status == "valid": + try: + session_cookies = extract_session_cookies(steam) + if session_cookies: + _db = await get_db() + await _db.execute( + "UPDATE accounts SET session_cookies = ?, updated_at = datetime('now') WHERE id = ?", + (session_cookies, account["id"]), + ) + await _db.commit() + logger.debug(f"[validate] {login}: saved session cookies") + except Exception as cookie_err: + logger.warning(f"[validate] {login}: failed to save cookies — {cookie_err}") + + except Exception as exc: + status = "invalid" + error_msg = str(exc) + logger.warning(f"[validate] {login}: error — {error_msg}") + finally: + if steam is not None: + await close_steam(steam) + + db = await get_db() + await db.execute( + "UPDATE accounts SET status = ?, ban_status = ?, updated_at = datetime('now') WHERE id = ?", + (status, ban_status or None, account["id"]), + ) + await db.commit() + + if profile: + sets = [] + vals = [] + for col, key in [("nickname", "nickname"), ("avatar_url", "avatar_url"), ("steam_level", "steam_level"), ("last_online", "last_online")]: + if profile.get(key) is not None: + sets.append(f"{col} = ?") + vals.append(profile[key]) + if sets: + sets.append("updated_at = datetime('now')") + vals.append(account["id"]) + await db.execute(f"UPDATE accounts SET {', '.join(sets)} WHERE id = ?", vals) + await db.commit() + logger.info(f"[validate] {login}: profile saved — {profile.get('nickname')}") + + if error_msg: + raise Exception(f"{error_msg}") + else: + ban_info = f" | ban={ban_status}" if ban_status else "" + logger.info(f"[validate] {login}: result — {status}{ban_info}") diff --git a/app/services/steam_guard.py b/app/services/steam_guard.py new file mode 100644 index 0000000..cb17092 --- /dev/null +++ b/app/services/steam_guard.py @@ -0,0 +1,83 @@ +import base64 +import hashlib +import hmac +import struct +import time + +from loguru import logger + + +def generate_2fa_code(shared_secret: str) -> str: + """Generate a Steam Guard 2FA code from shared_secret.""" + timestamp = int(time.time()) // 30 + msg = struct.pack(">Q", timestamp) + key = base64.b64decode(shared_secret) + auth = hmac.new(key, msg, hashlib.sha1).digest() + + offset = auth[19] & 0xF + code = struct.unpack(">I", auth[offset : offset + 4])[0] & 0x7FFFFFFF + + chars = "23456789BCDFGHJKMNPQRTVWXY" + result = [] + for _ in range(5): + result.append(chars[code % len(chars)]) + code //= len(chars) + + return "".join(result) + + +async def remove_guard(account: dict, params: dict, task_id: str = "") -> None: + """Remove Steam Guard 2FA via revocation code from mafile.""" + from app.services.steam_auth import create_steam_session, close_steam, read_mafile_data + from app.core.task_manager import task_manager + + mafile = read_mafile_data(account) + revocation_code = (mafile or {}).get("revocation_code", "") if mafile else "" + if not revocation_code: + raise ValueError("revocation_code not found in mafile") + + login = account["login"] + await task_manager.set_step(task_id, 1, 3, "Авторизация", acc_id=account["id"]) + logger.info(f"[remove_guard] {login}: logging in to Steam...") + steam = await create_steam_session(account) + logger.info(f"[remove_guard] {login}: auth ok") + try: + await task_manager.set_step(task_id, 2, 3, "Удаление Guard", acc_id=account["id"]) + logger.info(f"[remove_guard] {login}: sending Guard removal request...") + sessionid = await steam.sessionid("store.steampowered.com") + + await steam.request( + url="https://store.steampowered.com/twofactor/remove", + method="GET", + params={"step": "promptdevice"}, + ) + await steam.request( + url="https://store.steampowered.com/twofactor/remove", + method="GET", + params={"step": "promptrcode"}, + ) + await steam.request( + url="https://store.steampowered.com/twofactor/manage_action", + method="POST", + data={"action": "removercode", "sessionid": sessionid}, + ) + response = await steam.request( + url="https://store.steampowered.com/twofactor/manage_remove_revocation_code", + method="POST", + data={"revocation_code": revocation_code, "sessionid": sessionid}, + ) + + await task_manager.set_step(task_id, 3, 3, "Готово", acc_id=account["id"]) + logger.success(f"[remove_guard] {login}: Guard removed") + finally: + await close_steam(steam) + + +async def generate_2fa(account: dict, params: dict, task_id: str = "") -> None: + """Generate and log a 2FA code for an account.""" + shared_secret = account.get("shared_secret", "") + if not shared_secret: + raise ValueError("No shared_secret for this account") + + code = generate_2fa_code(shared_secret) + logger.debug(f"[generate_2fa] {account['login']}: code = {code}") diff --git a/app/services/steam_password.py b/app/services/steam_password.py new file mode 100644 index 0000000..5a2b57f --- /dev/null +++ b/app/services/steam_password.py @@ -0,0 +1,122 @@ +import json +import secrets +import string +from pathlib import Path + +from loguru import logger + +from app.core.crypto import encrypt_password +from app.services.steam_auth import create_steam_session, close_steam, read_mafile_data +from app.services.steam_wizard import ( + AJAX_HEADERS, + get_rsa_key, + run_common_wizard, +) + + +async def change_password(account: dict, params: dict, task_id: str = "") -> None: + """Change account password via Steam recovery wizard.""" + from app.core.task_manager import task_manager + + new_password = params.get("new_password", "") + if not new_password: + raise ValueError("new_password is required") + + login = account["login"] + + mafile_data = read_mafile_data(account) + if not mafile_data: + raise ValueError(f"No mafile found for {login} — cannot change password without 2FA") + + identity_secret = mafile_data.get("identity_secret", "") + device_id = mafile_data.get("device_id") + + if not identity_secret: + raise ValueError(f"No identity_secret in mafile for {login} — mobile confirmation impossible") + if not device_id: + raise ValueError(f"No device_id in mafile for {login} — mobile confirmation impossible") + + await task_manager.set_step(task_id, 1, 5, "Авторизация", acc_id=account["id"]) + logger.info(f"[change_password] {login}: logging in to Steam...") + steam = await create_steam_session(account) + logger.info(f"[change_password] {login}: auth ok") + + try: + await task_manager.set_step(task_id, 2, 5, "Wizard", acc_id=account["id"]) + logger.info(f"[change_password] {login}: starting password change wizard...") + wizard = await run_common_wizard( + steam, + entry_url="https://help.steampowered.com/wizard/HelpChangePassword?redir=store/account/", + login=login, + password=account["password"], + ) + + await task_manager.set_step(task_id, 3, 5, "RSA ключ", acc_id=account["id"]) + logger.info(f"[change_password] {login}: wizard done, getting RSA key...") + mod, exp, timestamp = await get_rsa_key(steam, login) + + logger.info(f"[change_password] {login}: checking new password availability...") + sessionid = await steam.sessionid("help.steampowered.com") + check_text = await steam.request( + url="https://help.steampowered.com/en/wizard/AjaxCheckPasswordAvailable/", + method="POST", + data={ + "sessionid": sessionid, + "wizard_ajax": 1, + "password": new_password, + }, + headers=AJAX_HEADERS, + ) + check_data = json.loads(check_text) if isinstance(check_text, str) else check_text + if not check_data.get("available"): + raise ValueError("New password is not available (too weak or reused)") + + encrypted_new = encrypt_password(new_password, mod, exp) + await task_manager.set_step(task_id, 4, 5, "Отправка запроса", acc_id=account["id"]) + logger.info(f"[change_password] {login}: password available, sending change request...") + result_text = await steam.request( + url="https://help.steampowered.com/en/wizard/AjaxAccountRecoveryChangePassword/", + method="POST", + data={ + "sessionid": sessionid, + "wizard_ajax": 1, + "s": wizard.s, + "account": wizard.account, + "password": encrypted_new, + "rsatimestamp": timestamp, + }, + headers=AJAX_HEADERS, + ) + + result_data = json.loads(result_text) if isinstance(result_text, str) else result_text + logger.debug(f"[change_password] {login}: change response: success={result_data.get('success')}, hasHash={bool(result_data.get('hash'))}") + + if result_data.get("errorMsg"): + raise RuntimeError(f"Password change rejected by Steam: {result_data['errorMsg']}") + if not result_data.get("hash"): + raise RuntimeError(f"Password change failed — no confirmation hash from Steam: {result_data}") + + await task_manager.set_step(task_id, 5, 5, "Сохранение", acc_id=account["id"]) + from app.database import get_db + db = await get_db() + await db.execute( + "UPDATE accounts SET password = ?, updated_at = datetime('now') WHERE id = ?", + (new_password, account["id"]), + ) + await db.commit() + + logger.success(f"[change_password] {login}: password changed") + finally: + await close_steam(steam) + + +def _generate_random_password(length: int = 24) -> str: + alphabet = string.ascii_letters + string.digits + "!" + return "".join(secrets.choice(alphabet) for _ in range(length)) + + +async def random_password(account: dict, params: dict, task_id: str = "") -> None: + """Change account password to a randomly generated one.""" + new_password = _generate_random_password() + params["new_password"] = new_password + await change_password(account, params) diff --git a/app/services/steam_phone.py b/app/services/steam_phone.py new file mode 100644 index 0000000..7ad8249 --- /dev/null +++ b/app/services/steam_phone.py @@ -0,0 +1,216 @@ +import json +import re + +from loguru import logger + +from app.services.steam_auth import create_steam_session, close_steam +from app.services.steam_wizard import AJAX_HEADERS, run_common_wizard + +_PHONE_RE = re.compile(r"^\+\d{7,15}$") + + +def _get_fresh_sessionid(steam) -> str: + """Get sessionid from the live aiohttp cookie jar (not from stale storage).""" + return steam._requests.cookies("help.steampowered.com").get("sessionid", "") + + +def _validate_phone(phone: str) -> str: + """Strip spaces/dashes and verify E.164-like format.""" + cleaned = re.sub(r"[\s\-()]", "", phone) + if cleaned and not cleaned.startswith("+"): + cleaned = "+" + cleaned + if not _PHONE_RE.match(cleaned): + raise ValueError( + f"Неверный формат номера: '{phone}'. " + "Ожидается международный формат, например +79123456789" + ) + return cleaned + + +async def change_phone(account: dict, params: dict, task_id: str = "") -> None: + """Change phone number on a Steam account via recovery wizard + interactive prompts.""" + from app.core.task_manager import task_manager + + login = account["login"] + new_phone = params.get("new_phone", "") + if new_phone: + new_phone = _validate_phone(new_phone) + await task_manager.set_step(task_id, 1, 5, "Авторизация", acc_id=account["id"]) + logger.info(f"[change_phone] {login}: logging in to Steam...") + steam = await create_steam_session(account) + try: + password = account["password"] + await task_manager.set_step(task_id, 2, 5, "Wizard", acc_id=account["id"]) + logger.info(f"[change_phone] {login}: auth ok, starting phone change wizard...") + + wizard = await run_common_wizard( + steam, + entry_url="https://help.steampowered.com/en/wizard/HelpRemovePhoneNumber?redir=store/account", + login=login, + password=password, + ) + logger.info(f"[change_phone] {login}: wizard done") + + if not new_phone: + new_phone = await task_manager.prompt_user(task_id, f"Введите новый номер телефона для {login}", login=login) + if not new_phone: + raise ValueError("new_phone is required") + new_phone = _validate_phone(new_phone) + + await task_manager.set_step(task_id, 3, 5, "Запрос смены", acc_id=account["id"]) + logger.info(f"[change_phone] {login}: requesting phone change -> {new_phone}...") + + fresh_sid = _get_fresh_sessionid(steam) + text = await steam.request( + url="https://help.steampowered.com/en/wizard/AjaxAccountRecoveryChangePhone/", + method="POST", + data={ + "s": wizard.s, + "account": wizard.account, + "sessionid": fresh_sid, + "wizard_ajax": 1, + "gamepad": 0, + "phone_number": new_phone, + }, + headers=AJAX_HEADERS, + ) + data = json.loads(text) if isinstance(text, str) else text + logger.info(f"[change_phone] {login}: change request response: {data}") + if data.get("errorMsg"): + err = data["errorMsg"] + hint = "" + if "112" in err: + hint = " (номер уже привязан к другому аккаунту, виртуальный/VoIP, или не поддерживается Steam)" + raise RuntimeError(f"Phone change request failed: {err}{hint}") + + await task_manager.set_step(task_id, 4, 5, "SMS код", acc_id=account["id"]) + sms_code = await task_manager.prompt_user(task_id, f"Введите SMS код с номера {new_phone}", login=login) + if not sms_code: + raise ValueError("sms_code is required") + sms_code = sms_code.strip() + + logger.info(f"[change_phone] {login}: confirming with SMS code...") + fresh_sid = _get_fresh_sessionid(steam) + confirm_text = await steam.request( + url="https://help.steampowered.com/en/wizard/AjaxAccountRecoveryConfirmChangePhone/", + method="POST", + data={ + "s": wizard.s, + "account": wizard.account, + "sessionid": fresh_sid, + "wizard_ajax": 1, + "gamepad": 0, + "phone_number": new_phone, + "phone_change_code": sms_code, + }, + headers=AJAX_HEADERS, + ) + confirm_data = json.loads(confirm_text) if isinstance(confirm_text, str) else confirm_text + logger.info(f"[change_phone] {login}: confirm response: {confirm_data}") + if confirm_data.get("errorMsg"): + raise RuntimeError(f"Phone confirmation failed: {confirm_data['errorMsg']}") + + await task_manager.set_step(task_id, 5, 5, "Сохранение", acc_id=account["id"]) + from app.database import get_db + db = await get_db() + await db.execute( + "UPDATE accounts SET phone = ?, updated_at = datetime('now') WHERE id = ?", + (new_phone, account["id"]), + ) + await db.commit() + + logger.success(f"[change_phone] {login}: phone changed -> {new_phone}") + finally: + await close_steam(steam) + + +async def remove_phone(account: dict, params: dict, task_id: str = "") -> None: + """Remove phone by changing it to a disposable number + SMS confirmation.""" + from app.core.task_manager import task_manager + + login = account["login"] + new_phone = params.get("new_phone", "") + if new_phone: + new_phone = _validate_phone(new_phone) + await task_manager.set_step(task_id, 1, 5, "Авторизация", acc_id=account["id"]) + logger.info(f"[remove_phone] {login}: logging in to Steam...") + steam = await create_steam_session(account) + try: + password = account["password"] + await task_manager.set_step(task_id, 2, 5, "Wizard", acc_id=account["id"]) + logger.info(f"[remove_phone] {login}: starting phone removal wizard...") + + wizard = await run_common_wizard( + steam, + entry_url="https://help.steampowered.com/en/wizard/HelpRemovePhoneNumber?redir=store/account", + login=login, + password=password, + ) + + if not new_phone: + new_phone = await task_manager.prompt_user(task_id, "Введите новый номер телефона (на который переставится старый)", login=login) + if not new_phone: + raise ValueError("new_phone is required for phone removal") + new_phone = _validate_phone(new_phone) + + await task_manager.set_step(task_id, 3, 5, "Запрос смены", acc_id=account["id"]) + logger.info(f"[remove_phone] {login}: requesting phone change -> {new_phone}...") + + fresh_sid = _get_fresh_sessionid(steam) + text = await steam.request( + url="https://help.steampowered.com/en/wizard/AjaxAccountRecoveryChangePhone/", + method="POST", + data={ + "s": wizard.s, + "account": wizard.account, + "sessionid": fresh_sid, + "wizard_ajax": 1, + "gamepad": 0, + "phone_number": new_phone, + }, + headers=AJAX_HEADERS, + ) + data = json.loads(text) if isinstance(text, str) else text + logger.info(f"[remove_phone] {login}: change request response: {data}") + if data.get("errorMsg"): + raise RuntimeError(f"Phone change request failed: {data['errorMsg']}") + + await task_manager.set_step(task_id, 4, 5, "SMS код", acc_id=account["id"]) + sms_code = await task_manager.prompt_user(task_id, f"Введите SMS код с номера {new_phone}", login=login) + if not sms_code: + raise ValueError("sms_code is required") + sms_code = sms_code.strip() + + logger.info(f"[remove_phone] {login}: confirming with SMS code...") + fresh_sid = _get_fresh_sessionid(steam) + confirm_text = await steam.request( + url="https://help.steampowered.com/en/wizard/AjaxAccountRecoveryConfirmChangePhone/", + method="POST", + data={ + "s": wizard.s, + "account": wizard.account, + "sessionid": fresh_sid, + "wizard_ajax": 1, + "gamepad": 0, + "phone_number": new_phone, + "phone_change_code": sms_code, + }, + headers=AJAX_HEADERS, + ) + confirm_data = json.loads(confirm_text) if isinstance(confirm_text, str) else confirm_text + logger.info(f"[remove_phone] {login}: confirm response: {confirm_data}") + if confirm_data.get("errorMsg"): + raise RuntimeError(f"Phone confirmation failed: {confirm_data['errorMsg']}") + + await task_manager.set_step(task_id, 5, 5, "Сохранение", acc_id=account["id"]) + from app.database import get_db + db = await get_db() + await db.execute( + "UPDATE accounts SET phone = NULL, updated_at = datetime('now') WHERE id = ?", + (account["id"],), + ) + await db.commit() + + logger.success(f"[remove_phone] {login}: phone removed (changed to {new_phone})") + finally: + await close_steam(steam) diff --git a/app/services/steam_profile.py b/app/services/steam_profile.py new file mode 100644 index 0000000..de4565e --- /dev/null +++ b/app/services/steam_profile.py @@ -0,0 +1,90 @@ +import re + +import aiohttp +from loguru import logger + + +async def fetch_profile(steam_id: str, steam=None) -> dict | None: + """Fetch persona name and avatar URL from Steam community profile page. + + If *steam* session is provided, uses authenticated cookies. + Returns {"nickname": str, "avatar_url": str, "steam_level": int|None} or None on failure. + """ + url = f"https://steamcommunity.com/profiles/{steam_id}/" + try: + if steam is not None: + from app.services.steam_auth import raw_request + resp = await raw_request(steam, url, method="GET", allow_redirects=True) + if resp.status != 200: + logger.warning(f"Profile fetch for {steam_id}: HTTP {resp.status}") + return None + html = await resp.text() + else: + headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"} + async with aiohttp.ClientSession(headers=headers) as session: + async with session.get(url, timeout=aiohttp.ClientTimeout(total=15)) as resp: + if resp.status != 200: + logger.warning(f"Profile fetch for {steam_id}: HTTP {resp.status}") + return None + html = await resp.text() + except Exception as exc: + logger.warning(f"Profile fetch error for {steam_id}: {exc}") + return None + + nickname = _parse_persona_name(html) + avatar_url = _parse_avatar(html) + steam_level = _parse_level(html) + last_online = _parse_last_online(html) + + if not nickname and not avatar_url: + return None + + return {"nickname": nickname, "avatar_url": avatar_url, "steam_level": steam_level, "last_online": last_online} + + +def _parse_persona_name(html: str) -> str | None: + # g_rgProfileData = {"personaname":"jeffreyoelze1999",...} + m = re.search(r'"personaname"\s*:\s*"([^"]*)"', html) + if m: + return m.group(1) + # Fallback: ... + m = re.search(r'([^<]+)', html) + return m.group(1) if m else None + + +def _parse_avatar(html: str) -> str | None: + # 1) g_rgProfileData JSON: {"avatarfull":"https://avatars.fastly.steamstatic.com/..."} + m = re.search(r'"avatarfull"\s*:\s*"(https://[^"]+)"', html) + if m: + return m.group(1).replace("_full.jpg", "_medium.jpg") + # 2) + m = re.search(r']+srcset="(https://[^"]+)"', html) + if m: + return m.group(1).replace("_full.jpg", "_medium.jpg") + return None + + +def _parse_level(html: str) -> int | None: + # 100 + m = re.search(r'(\d+)', html) + return int(m.group(1)) if m else None + + +def _parse_last_online(html: str) -> str | None: + # "Last Online X days ago" → "Xd", "X hrs ago" → "Xh", "X min ago" → "Xm" + m = re.search(r'Last\s+Online[^<]{0,150}?(\d+)\s+day', html, re.IGNORECASE) + if m: + return f"{m.group(1)}d" + m = re.search(r'Last\s+Online[^<]{0,150}?(\d+)\s+hr', html, re.IGNORECASE) + if m: + return f"{m.group(1)}h" + m = re.search(r'Last\s+Online[^<]{0,150}?(\d+)\s+min', html, re.IGNORECASE) + if m: + return f"{m.group(1)}m" + if re.search(r'Currently\s+(In-Game|Online)', html, re.IGNORECASE): + return "online" + return None diff --git a/app/services/steam_token_checker.py b/app/services/steam_token_checker.py new file mode 100644 index 0000000..2206cf5 --- /dev/null +++ b/app/services/steam_token_checker.py @@ -0,0 +1,176 @@ +"""Validation service for token accounts (refresh token → cookies).""" + +import base64 +import json as _json + +import aiohttp +from aiohttp import FormData +from loguru import logger + +from app.services.steam_auth import _resolve_proxy +from app.core.proxy_manager import proxy_manager +from app.core.task_manager import task_manager +from pysteamauth.auth.schemas import FinalizeLoginStatus + + +def _decode_jwt_payload(token: str) -> dict: + """Decode the payload section of a JWT without verifying the signature.""" + parts = token.split(".") + if len(parts) != 3: + raise ValueError("Token must have 3 parts (header.payload.signature)") + payload_b64 = parts[1] + # Pad base64 if needed + payload_b64 += "=" * (-len(payload_b64) % 4) + payload_bytes = base64.urlsafe_b64decode(payload_b64) + return _json.loads(payload_bytes) + + +async def check_token_account(account: dict, params: dict, *, task_id: str) -> None: + """Convert refresh_token → web cookies, fetch profile via cookies, update DB.""" + acc_id = account["id"] + token = account["token"] + + # Step 1: Decode JWT to get steam_id + await task_manager.set_step(task_id, 1, 4, "Декодирование токена", acc_id) + try: + jwt_payload = _decode_jwt_payload(token) + except Exception as exc: + logger.error(f"[token_checker] {account.get('login', acc_id)}: bad JWT — {exc}") + await _mark_invalid(acc_id) + raise + + steam_id = jwt_payload.get("sub") + + # Step 2: GET steamcommunity.com for sessionid cookie + await task_manager.set_step(task_id, 2, 4, "Получение sessionid", acc_id) + proxy = await _resolve_proxy(account) + connector = proxy_manager.get_connector(proxy) if proxy else aiohttp.TCPConnector() + jar = aiohttp.CookieJar(unsafe=True) + headers = { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", + "Origin": "https://steamcommunity.com", + } + timeout = aiohttp.ClientTimeout(total=30) + + try: + async with aiohttp.ClientSession( + connector=connector, cookie_jar=jar, headers=headers, timeout=timeout, + ) as session: + async with session.get("https://steamcommunity.com") as resp: + await resp.read() + + sessionid = None + for cookie in jar: + if cookie.key == "sessionid": + sessionid = cookie.value + break + if not sessionid: + raise RuntimeError("Failed to acquire sessionid cookie") + + # Step 3: POST /jwt/finalizelogin with refresh_token as nonce + await task_manager.set_step(task_id, 3, 4, "Получение куки", acc_id) + form = FormData(fields=[ + ("nonce", token), + ("sessionid", sessionid), + ("redir", "https://steamcommunity.com/login/home/?goto="), + ]) + async with session.post( + "https://login.steampowered.com/jwt/finalizelogin", + data=form, + ) as resp: + resp_text = await resp.text() + if resp.status != 200: + raise RuntimeError(f"finalizelogin HTTP {resp.status}: {resp_text[:200]}") + + resp_json = _json.loads(resp_text) + if not resp_json.get("steamID"): + error_msg = resp_json.get("message", resp_text[:200]) + raise RuntimeError(f"finalizelogin failed: {error_msg}") + + finalize = FinalizeLoginStatus.model_validate(resp_json) + steam_id = finalize.steamID or steam_id + + # Post to each transfer_info URL to set domain cookies + for ti in finalize.transfer_info: + form = FormData(fields=[ + ("nonce", ti.params.nonce), + ("auth", ti.params.auth), + ("steamID", str(steam_id)), + ]) + try: + async with session.post(ti.url, data=form) as _resp: + await _resp.read() + except Exception: + pass + + # Visit extra domains to collect all cookies + for url in ("https://store.steampowered.com", "https://help.steampowered.com"): + try: + async with session.get(url) as _resp: + await _resp.read() + except Exception: + pass + + # Collect cookies from jar + all_cookies = [] + for c in jar: + all_cookies.append({ + "name": c.key, + "value": c.value, + "domain": c.get("domain", ""), + "path": c.get("path", "/"), + "secure": "secure" in str(c).lower(), + "httpOnly": "httponly" in str(c).lower(), + }) + session_cookies_json = _json.dumps(all_cookies) + logger.debug(f"[token_checker] {account.get('login', acc_id)}: got {len(all_cookies)} cookies") + + # Step 4: Fetch profile using authenticated session (cookies set) + await task_manager.set_step(task_id, 4, 4, "Получение профиля", acc_id) + nickname = None + steam_level = None + avatar_url = None + last_online = None + if steam_id and steam_id != "0": + profile_url = f"https://steamcommunity.com/profiles/{steam_id}/" + try: + async with session.get(profile_url) as resp: + if resp.status == 200: + html = await resp.text() + from app.services.steam_profile import ( + _parse_persona_name, _parse_avatar, _parse_level, _parse_last_online, + ) + nickname = _parse_persona_name(html) + avatar_url = _parse_avatar(html) + steam_level = _parse_level(html) + last_online = _parse_last_online(html) + except Exception as exc: + logger.warning(f"[token_checker] profile fetch error: {exc}") + + # Update DB + from app.database import get_db + db = await get_db() + await db.execute( + """UPDATE token_accounts + SET steam_id = ?, nickname = ?, steam_level = ?, avatar_url = ?, last_online = ?, + session_cookies = ?, status = 'valid', updated_at = datetime('now') + WHERE id = ?""", + (steam_id, nickname, steam_level, avatar_url, last_online, session_cookies_json, acc_id), + ) + await db.commit() + logger.success(f"[token_checker] {account.get('login', acc_id)} → valid, steam_id={steam_id}") + + except Exception as exc: + await _mark_invalid(acc_id) + logger.error(f"[token_checker] {account.get('login', acc_id)} → {exc}") + raise + + +async def _mark_invalid(acc_id: int) -> None: + from app.database import get_db + db = await get_db() + await db.execute( + "UPDATE token_accounts SET status = 'invalid', updated_at = datetime('now') WHERE id = ?", + (acc_id,), + ) + await db.commit() diff --git a/app/services/steam_wizard.py b/app/services/steam_wizard.py new file mode 100644 index 0000000..af069f1 --- /dev/null +++ b/app/services/steam_wizard.py @@ -0,0 +1,397 @@ +"""Shared Steam account recovery wizard flow. + +Steps 1-9 are identical across password change, email change, and phone removal. +This module extracts that common logic. +""" + +import asyncio +import json +from dataclasses import dataclass +from urllib.parse import parse_qs, urlparse + +from loguru import logger + +from app.core.crypto import encrypt_password +from app.services.steam_auth import raw_request + +BROWSER_UA = ( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/" + "537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36" +) + +AJAX_HEADERS = { + "Accept": "*/*", + "X-Requested-With": "XMLHttpRequest", + "User-Agent": BROWSER_UA, + "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", + "Origin": "https://help.steampowered.com", +} + + +@dataclass +class WizardParams: + s: int + account: int + reset: int + issueid: int + lost: int = 0 + sessionid: str = "" + + +async def parse_wizard_redirect(steam, entry_url: str) -> WizardParams: + """Step 1-2: Navigate to wizard entry URL, follow redirects, parse params from final URL.""" + logger.debug(f"[Wizard] Step 1-2: GET {entry_url}") + response = await raw_request( + steam, + url=entry_url, + method="GET", + headers={"User-Agent": BROWSER_UA}, + allow_redirects=True, + ) + final_url = str(response.url) + logger.debug(f"[Wizard] Redirect → {final_url}") + query = parse_qs(urlparse(final_url).query) + + sessionid = await steam.sessionid("help.steampowered.com") + + params = WizardParams( + s=int(query.get("s", [0])[0]), + account=int(query.get("account", [0])[0]), + reset=int(query.get("reset", [0])[0]), + issueid=int(query.get("issueid", [0])[0]), + lost=int(query.get("lost", [0])[0]), + sessionid=sessionid, + ) + logger.debug(f"[Wizard] Parsed: s={params.s}, account={params.account}, reset={params.reset}, issueid={params.issueid}") + return params + + +async def login_info_enter_code(steam, params: WizardParams) -> dict: + """Step 3: Enter code mode.""" + logger.debug("[Wizard] Step 3: HelpWithLoginInfoEnterCode") + text = await steam.request( + url="https://help.steampowered.com/en/wizard/HelpWithLoginInfoEnterCode", + method="GET", + params={ + "s": params.s, + "account": params.account, + "reset": params.reset, + "lost": params.lost, + "issueid": params.issueid, + "sessionid": params.sessionid, + "wizard_ajax": 1, + "gamepad": 0, + }, + headers=AJAX_HEADERS, + ) + result = json.loads(text) if isinstance(text, str) else text + logger.debug(f"[Wizard] Step 3 response: {result}") + return result + + +async def send_recovery_code(steam, params: WizardParams) -> dict: + """Step 4: Send account recovery code (method=8 = mobile app).""" + logger.debug("[Wizard] Step 4: AjaxSendAccountRecoveryCode (method=8, mobile)") + text = await steam.request( + url="https://help.steampowered.com/en/wizard/AjaxSendAccountRecoveryCode", + method="POST", + data={ + "s": params.s, + "account": params.account, + "reset": params.reset, + "lost": params.lost, + "issueid": params.issueid, + "sessionid": params.sessionid, + "wizard_ajax": 1, + "gamepad": 0, + "method": 8, + "link": "", + "n": 1, + }, + headers=AJAX_HEADERS, + ) + result = json.loads(text) if isinstance(text, str) else text + logger.debug(f"[Wizard] Step 4 response: {result}") + if result.get("errorMsg"): + raise RuntimeError(f"SendRecoveryCode failed: {result['errorMsg']}") + return result + + +async def mobile_confirm(steam, params: WizardParams, retries: int = 5) -> None: + """Step 5: Confirm via Steam mobile confirmations API (getlist → find by creator_id → ajaxop allow).""" + logger.debug("[Wizard] Step 5: Mobile confirmation") + + try: + sid = steam.steamid + except (ValueError, AttributeError): + sid = None + if not sid: + raise RuntimeError("No steamid on Steam object — cannot perform mobile confirmation") + + try: + did = steam.device_id + except (ValueError, AttributeError): + did = None + if not did: + raise RuntimeError("No device_id on Steam object — cannot perform mobile confirmation") + + try: + _ = steam.identity_secret + except (ValueError, AttributeError): + raise RuntimeError("No identity_secret on Steam object — cannot perform mobile confirmation") + + logger.debug(f"[MobileConfirm] steamid={sid}, device_id={did[:20]}..., creator_id(s)={params.s}") + + # Steam needs time to create the confirmation after send_recovery_code + logger.debug("[MobileConfirm] Waiting 3s for Steam to create confirmation...") + await asyncio.sleep(3) + + for attempt in range(retries): + try: + server_time = await steam.get_server_time() + conf_hash = steam.get_confirmation_hash(server_time=server_time) + logger.debug(f"[MobileConfirm] Attempt {attempt + 1}/{retries}: getlist (server_time={server_time})") + + conf_text = await steam.request( + url="https://steamcommunity.com/mobileconf/getlist", + method="GET", + cookies={ + "mobileClient": "ios", + "mobileClientVersion": "2.0.20", + "steamid": str(sid), + "Steam_Language": "english", + }, + params={ + "p": did, + "a": str(sid), + "k": conf_hash, + "t": server_time, + "m": "react", + "tag": "conf", + }, + ) + conf_data = json.loads(conf_text) if isinstance(conf_text, str) else conf_text + + confs = conf_data.get("conf", []) + logger.debug(f"[MobileConfirm] getlist response: success={conf_data.get('success')}, {len(confs)} confirmation(s)") + for i, c in enumerate(confs): + logger.debug( + f"[MobileConfirm] [{i}] id={c.get('id')}, type={c.get('type')}, " + f"type_name={c.get('type_name', '?')}, creator_id={c.get('creator_id')}, " + f"headline={c.get('headline', '')}" + ) + + if not conf_data.get("success"): + logger.warning(f"[MobileConfirm] getlist failed: {conf_data}") + raise RuntimeError(f"getlist failed: {conf_data}") + + target = None + creator = str(params.s) + for c in confs: + if str(c.get("creator_id", "")) == creator: + target = c + break + + if not target: + ids = [str(c.get("creator_id", "")) for c in confs] + logger.warning(f"[MobileConfirm] No match for creator_id={creator}, available: {ids}") + raise RuntimeError(f"No confirmation found for creator_id={creator}") + + logger.debug( + f"[MobileConfirm] Found match! id={target['id']}, nonce={target.get('nonce')}, " + f"creator_id={target.get('creator_id')}" + ) + + allow_time = await steam.get_server_time() + allow_hash = steam.get_confirmation_hash(server_time=allow_time, tag="allow") + logger.debug(f"[MobileConfirm] Calling ajaxop allow (server_time={allow_time})") + + allow_text = await steam.request( + url="https://steamcommunity.com/mobileconf/ajaxop", + method="GET", + cookies={ + "mobileClient": "ios", + "mobileClientVersion": "2.0.20", + }, + params={ + "op": "allow", + "p": did, + "a": str(sid), + "k": allow_hash, + "t": allow_time, + "m": "react", + "tag": "allow", + "cid": target["id"], + "ck": target["nonce"], + }, + ) + allow_data = json.loads(allow_text) if isinstance(allow_text, str) else allow_text + logger.debug(f"[MobileConfirm] ajaxop response: {allow_data}") + + if allow_data.get("success"): + logger.debug(f"[MobileConfirm] ✓ Confirmation accepted (attempt {attempt + 1})") + return + raise RuntimeError(f"ajaxop returned success=false: {allow_data}") + + except Exception as exc: + logger.warning(f"[MobileConfirm] Attempt {attempt + 1}/{retries} failed: {exc}") + if attempt < retries - 1: + logger.debug(f"[MobileConfirm] Retrying in 3s...") + await asyncio.sleep(3) + + raise RuntimeError(f"Mobile confirmation failed after {retries} retries") + + +async def poll_recovery_confirmation(steam, params: WizardParams) -> dict: + """Step 6: Poll account recovery confirmation (single call, matches old chemail.py).""" + logger.debug("[Wizard] Step 6: AjaxPollAccountRecoveryConfirmation") + text = await steam.request( + url="https://help.steampowered.com/en/wizard/AjaxPollAccountRecoveryConfirmation", + method="POST", + data={ + "s": params.s, + "reset": params.reset, + "lost": params.lost, + "issueid": params.issueid, + "sessionid": params.sessionid, + "wizard_ajax": 1, + "gamepad": 0, + "method": 8, + }, + headers=AJAX_HEADERS, + ) + data = json.loads(text) if isinstance(text, str) else text + logger.debug(f"[Wizard] Step 6 response: {data}") + if data.get("errorMsg"): + raise RuntimeError(f"Poll error from Steam: {data['errorMsg']}") + return data + + +async def verify_recovery_code(steam, params: WizardParams) -> dict: + """Step 7: Verify recovery code (empty for mobile method).""" + logger.debug("[Wizard] Step 7: AjaxVerifyAccountRecoveryCode (code=empty, method=8)") + text = await steam.request( + url="https://help.steampowered.com/en/wizard/AjaxVerifyAccountRecoveryCode", + method="GET", + params={ + "code": "", + "s": params.s, + "reset": params.reset, + "lost": params.lost, + "method": 8, + "issueid": params.issueid, + "sessionid": params.sessionid, + "wizard_ajax": 1, + "gamepad": 0, + }, + headers=AJAX_HEADERS, + ) + result = json.loads(text) if isinstance(text, str) else text + logger.debug(f"[Wizard] Step 7 response: {result}") + if result.get("errorMsg"): + raise RuntimeError(f"Step 7 (VerifyRecoveryCode) failed: {result['errorMsg']}") + return result + + +async def get_next_step(steam, params: WizardParams) -> dict: + """Step 8: Get next wizard step (lost=2 hardcoded).""" + logger.debug("[Wizard] Step 8: AjaxAccountRecoveryGetNextStep (lost=2)") + text = await steam.request( + url="https://help.steampowered.com/en/wizard/AjaxAccountRecoveryGetNextStep", + method="POST", + data={ + "s": params.s, + "account": params.account, + "reset": params.reset, + "lost": 2, + "issueid": params.issueid, + "sessionid": params.sessionid, + "wizard_ajax": 1, + "gamepad": 0, + }, + headers=AJAX_HEADERS, + ) + result = json.loads(text) if isinstance(text, str) else text + logger.debug(f"[Wizard] Step 8 response: {result}") + if result.get("errorMsg"): + raise RuntimeError(f"Step 8 (GetNextStep) failed: {result['errorMsg']}") + return result + + +async def get_rsa_key(steam, login: str) -> tuple[str, str, int]: + """Step 9a: Get RSA public key from Steam.""" + logger.debug(f"[Wizard] Getting RSA key for {login}") + sessionid = await steam.sessionid("help.steampowered.com") + text = await steam.request( + url="https://help.steampowered.com/en/login/getrsakey/", + method="POST", + data={ + "sessionid": sessionid, + "username": login, + }, + headers=AJAX_HEADERS, + ) + data = json.loads(text) if isinstance(text, str) else text + logger.debug(f"[Wizard] RSA key received: success={data.get('success')}, timestamp={data.get('timestamp')}") + return data["publickey_mod"], data["publickey_exp"], int(data["timestamp"]) + + +async def verify_password(steam, params: WizardParams, login: str, password: str) -> dict: + """Step 9: Verify current password via RSA encryption.""" + logger.debug(f"[Wizard] Step 9: Verifying current password for {login}") + mod, exp, timestamp = await get_rsa_key(steam, login) + encrypted = encrypt_password(password, mod, exp) + sessionid = await steam.sessionid("help.steampowered.com") + + text = await steam.request( + url="https://help.steampowered.com/en/wizard/AjaxAccountRecoveryVerifyPassword/", + method="POST", + data={ + "sessionid": sessionid, + "s": params.s, + "lost": 2, + "reset": 1, + "password": encrypted, + "rsatimestamp": timestamp, + }, + headers=AJAX_HEADERS, + ) + result = json.loads(text) if isinstance(text, str) else text + logger.debug(f"[Wizard] Step 9 response: {result}") + if result.get("errorMsg"): + raise RuntimeError(f"Step 9 (VerifyPassword) failed: {result['errorMsg']}") + return result + + +async def run_common_wizard(steam, entry_url: str, login: str, password: str) -> WizardParams: + """Execute the full shared wizard flow (steps 1-9). + + Returns WizardParams ready for the service-specific final steps. + """ + logger.debug(f"[Wizard] ===== Starting wizard for {login} =====") + logger.debug(f"[Wizard] Entry URL: {entry_url}") + + params = await parse_wizard_redirect(steam, entry_url) + await asyncio.sleep(1) + + await login_info_enter_code(steam, params) + await asyncio.sleep(1) + + await send_recovery_code(steam, params) + await asyncio.sleep(1) + + await mobile_confirm(steam, params) + + await poll_recovery_confirmation(steam, params) + await asyncio.sleep(1) + + await verify_recovery_code(steam, params) + await asyncio.sleep(1) + + await get_next_step(steam, params) + await asyncio.sleep(1) + + await verify_password(steam, params, login, password) + + logger.debug(f"[Wizard] ===== Wizard complete for {login} =====") + return params diff --git a/app/static.zip b/app/static.zip new file mode 100644 index 0000000..6cc8b0d Binary files /dev/null and b/app/static.zip differ diff --git a/app/static/assets/index-Bxw--tTv.js b/app/static/assets/index-Bxw--tTv.js new file mode 100644 index 0000000..fe29013 --- /dev/null +++ b/app/static/assets/index-Bxw--tTv.js @@ -0,0 +1,14 @@ +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),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.transitional.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.consumer`),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.for(`react.activity`),p=Symbol.iterator;function m(e){return typeof e!=`object`||!e?null:(e=p&&e[p]||e[`@@iterator`],typeof e==`function`?e:null)}var h={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},g=Object.assign,_={};function v(e,t,n){this.props=e,this.context=t,this.refs=_,this.updater=n||h}v.prototype.isReactComponent={},v.prototype.setState=function(e,t){if(typeof e!=`object`&&typeof e!=`function`&&e!=null)throw Error(`takes an object of state variables to update or a function which returns an object of state variables.`);this.updater.enqueueSetState(this,e,t,`setState`)},v.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,`forceUpdate`)};function y(){}y.prototype=v.prototype;function b(e,t,n){this.props=e,this.context=t,this.refs=_,this.updater=n||h}var x=b.prototype=new y;x.constructor=b,g(x,v.prototype),x.isPureReactComponent=!0;var S=Array.isArray;function C(){}var w={H:null,A:null,T:null,S:null},T=Object.prototype.hasOwnProperty;function E(e,n,r){var i=r.ref;return{$$typeof:t,type:e,key:n,ref:i===void 0?null:i,props:r}}function D(e,t){return E(e.type,t,e.props)}function O(e){return typeof e==`object`&&!!e&&e.$$typeof===t}function ee(e){var t={"=":`=0`,":":`=2`};return`$`+e.replace(/[=:]/g,function(e){return t[e]})}var k=/\/+/g;function A(e,t){return typeof e==`object`&&e&&e.key!=null?ee(``+e.key):t.toString(36)}function j(e){switch(e.status){case`fulfilled`:return e.value;case`rejected`:throw e.reason;default:switch(typeof e.status==`string`?e.then(C,C):(e.status=`pending`,e.then(function(t){e.status===`pending`&&(e.status=`fulfilled`,e.value=t)},function(t){e.status===`pending`&&(e.status=`rejected`,e.reason=t)})),e.status){case`fulfilled`:return e.value;case`rejected`:throw e.reason}}throw e}function M(e,r,i,a,o){var s=typeof e;(s===`undefined`||s===`boolean`)&&(e=null);var c=!1;if(e===null)c=!0;else switch(s){case`bigint`:case`string`:case`number`:c=!0;break;case`object`:switch(e.$$typeof){case t:case n:c=!0;break;case d:return c=e._init,M(c(e._payload),r,i,a,o)}}if(c)return o=o(e),c=a===``?`.`+A(e,0):a,S(o)?(i=``,c!=null&&(i=c.replace(k,`$&/`)+`/`),M(o,r,i,``,function(e){return e})):o!=null&&(O(o)&&(o=D(o,i+(o.key==null||e&&e.key===o.key?``:(``+o.key).replace(k,`$&/`)+`/`)+c)),r.push(o)),1;c=0;var l=a===``?`.`:a+`:`;if(S(e))for(var u=0;u{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(e.unstable_now=void 0,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=!1,_=typeof setTimeout==`function`?setTimeout:null,v=typeof clearTimeout==`function`?clearTimeout:null,y=typeof setImmediate<`u`?setImmediate:null;function b(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 x(e){if(h=!1,b(e),!m)if(n(c)!==null)m=!0,S||(S=!0,O());else{var t=n(l);t!==null&&A(x,t.startTime-e)}}var S=!1,C=-1,w=5,T=-1;function E(){return g?!0:!(e.unstable_now()-Tt&&E());){var o=d.callback;if(typeof o==`function`){d.callback=null,f=d.priorityLevel;var s=o(d.expirationTime<=t);if(t=e.unstable_now(),typeof s==`function`){d.callback=s,b(t),i=!0;break b}d===n(c)&&r(c),b(t)}else r(c);d=n(c)}if(d!==null)i=!0;else{var u=n(l);u!==null&&A(x,u.startTime-t),i=!1}}break a}finally{d=null,f=a,p=!1}i=void 0}}finally{i?O():S=!1}}}var O;if(typeof y==`function`)O=function(){y(D)};else if(typeof MessageChannel<`u`){var ee=new MessageChannel,k=ee.port2;ee.port1.onmessage=D,O=function(){k.postMessage(null)}}else O=function(){_(D,0)};function A(t,n){C=_(function(){t(e.unstable_now())},n)}e.unstable_IdlePriority=5,e.unstable_ImmediatePriority=1,e.unstable_LowPriority=4,e.unstable_NormalPriority=3,e.unstable_Profiling=null,e.unstable_UserBlockingPriority=2,e.unstable_cancelCallback=function(e){e.callback=null},e.unstable_forceFrameRate=function(e){0>e||125o?(r.sortIndex=a,t(l,r),n(c)===null&&r===n(l)&&(h?(v(C),C=-1):h=!0,A(x,a-o))):(r.sortIndex=s,t(c,r),m||p||(m=!0,S||(S=!0,O()))),r},e.unstable_shouldYield=E,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();function n(e){var t=`https://react.dev/errors/`+e;if(1{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=f(),n=u(),r=m();function i(e){var t=`https://react.dev/errors/`+e;if(1ae||(e.current=ie[ae],ie[ae]=null,ae--)}function L(e,t){ae++,ie[ae]=e.current,e.current=t}var oe=F(null),R=F(null),se=F(null),ce=F(null);function le(e,t){switch(L(se,t),L(R,e),L(oe,null),t.nodeType){case 9:case 11:e=(e=t.documentElement)&&(e=e.namespaceURI)?Vd(e):0;break;default:if(e=t.tagName,t=t.namespaceURI)t=Vd(t),e=Hd(t,e);else switch(e){case`svg`:e=1;break;case`math`:e=2;break;default:e=0}}I(oe),L(oe,e)}function ue(){I(oe),I(R),I(se)}function de(e){e.memoizedState!==null&&L(ce,e);var t=oe.current,n=Hd(t,e.type);t!==n&&(L(R,e),L(oe,n))}function fe(e){R.current===e&&(I(oe),I(R)),ce.current===e&&(I(ce),Qf._currentValue=re)}var pe,me;function he(e){if(pe===void 0)try{throw Error()}catch(e){var t=e.stack.trim().match(/\n( *(at )?)/);pe=t&&t[1]||``,me=-1)`:-1i||c[r]!==l[i]){var u=` +`+c[r].replace(` at new `,` at `);return e.displayName&&u.includes(``)&&(u=u.replace(``,e.displayName)),u}while(1<=r&&0<=i);break}}}finally{ge=!1,Error.prepareStackTrace=n}return(n=e?e.displayName||e.name:``)?he(n):``}function ve(e,t){switch(e.tag){case 26:case 27:case 5:return he(e.type);case 16:return he(`Lazy`);case 13:return e.child!==t&&t!==null?he(`Suspense Fallback`):he(`Suspense`);case 19:return he(`SuspenseList`);case 0:case 15:return _e(e.type,!1);case 11:return _e(e.type.render,!1);case 1:return _e(e.type,!0);case 31:return he(`Activity`);default:return``}}function ye(e){try{var t=``,n=null;do t+=ve(e,n),n=e,e=e.return;while(e);return t}catch(e){return` +Error generating stack: `+e.message+` +`+e.stack}}var be=Object.prototype.hasOwnProperty,xe=t.unstable_scheduleCallback,Se=t.unstable_cancelCallback,Ce=t.unstable_shouldYield,we=t.unstable_requestPaint,Te=t.unstable_now,Ee=t.unstable_getCurrentPriorityLevel,De=t.unstable_ImmediatePriority,Oe=t.unstable_UserBlockingPriority,ke=t.unstable_NormalPriority,Ae=t.unstable_LowPriority,je=t.unstable_IdlePriority,Me=t.log,Ne=t.unstable_setDisableYieldValue,Pe=null,Fe=null;function Ie(e){if(typeof Me==`function`&&Ne(e),Fe&&typeof Fe.setStrictMode==`function`)try{Fe.setStrictMode(Pe,e)}catch{}}var Le=Math.clz32?Math.clz32:Be,Re=Math.log,ze=Math.LN2;function Be(e){return e>>>=0,e===0?32:31-(Re(e)/ze|0)|0}var Ve=256,He=262144,Ue=4194304;function We(e){var t=e&42;if(t!==0)return t;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:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return e&261888;case 262144:case 524288:case 1048576:case 2097152:return e&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return e&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return e}}function Ge(e,t,n){var r=e.pendingLanes;if(r===0)return 0;var i=0,a=e.suspendedLanes,o=e.pingedLanes;e=e.warmLanes;var s=r&134217727;return s===0?(s=r&~a,s===0?o===0?n||(n=r&~e,n!==0&&(i=We(n))):i=We(o):i=We(s)):(r=s&~a,r===0?(o&=s,o===0?n||(n=s&~e,n!==0&&(i=We(n))):i=We(o)):i=We(r)),i===0?0:t!==0&&t!==i&&(t&a)===0&&(a=i&-i,n=t&-t,a>=n||a===32&&n&4194048)?t:i}function Ke(e,t){return(e.pendingLanes&~(e.suspendedLanes&~e.pingedLanes)&t)===0}function qe(e,t){switch(e){case 1:case 2:case 4:case 8:case 64:return t+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return t+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function Je(){var e=Ue;return Ue<<=1,!(Ue&62914560)&&(Ue=4194304),e}function Ye(e){for(var t=[],n=0;31>n;n++)t.push(e);return t}function Xe(e,t){e.pendingLanes|=t,t!==268435456&&(e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0)}function Ze(e,t,n,r,i,a){var o=e.pendingLanes;e.pendingLanes=n,e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0,e.expiredLanes&=n,e.entangledLanes&=n,e.errorRecoveryDisabledLanes&=n,e.shellSuspendCounter=0;var s=e.entanglements,c=e.expirationTimes,l=e.hiddenUpdates;for(n=o&~n;0`u`||window.document===void 0||window.document.createElement===void 0),un=!1;if(ln)try{var dn={};Object.defineProperty(dn,`passive`,{get:function(){un=!0}}),window.addEventListener(`test`,dn,dn),window.removeEventListener(`test`,dn,dn)}catch{un=!1}var fn=null,pn=null,mn=null;function hn(){if(mn)return mn;var e,t=pn,n=t.length,r,i=`value`in fn?fn.value:fn.textContent,a=i.length;for(e=0;e=qn),Xn=` `,Zn=!1;function Qn(e,t){switch(e){case`keyup`:return Gn.indexOf(t.keyCode)!==-1;case`keydown`:return t.keyCode!==229;case`keypress`:case`mousedown`:case`focusout`:return!0;default:return!1}}function $n(e){return e=e.detail,typeof e==`object`&&`data`in e?e.data:null}var er=!1;function tr(e,t){switch(e){case`compositionend`:return $n(t);case`keypress`:return t.which===32?(Zn=!0,Xn):null;case`textInput`:return e=t.data,e===Xn&&Zn?null:e;default:return null}}function nr(e,t){if(er)return e===`compositionend`||!Kn&&Qn(e,t)?(e=hn(),mn=pn=fn=null,er=!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=wr(n)}}function Er(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?Er(e,t.parentNode):`contains`in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function Dr(e){e=e!=null&&e.ownerDocument!=null&&e.ownerDocument.defaultView!=null?e.ownerDocument.defaultView:window;for(var t=It(e.document);t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href==`string`}catch{n=!1}if(n)e=t.contentWindow;else break;t=It(e.document)}return t}function Or(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`)}var kr=ln&&`documentMode`in document&&11>=document.documentMode,Ar=null,jr=null,Mr=null,Nr=!1;function Pr(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;Nr||Ar==null||Ar!==It(r)||(r=Ar,`selectionStart`in r&&Or(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}),Mr&&Cr(Mr,r)||(Mr=r,r=Ed(jr,`onSelect`),0>=o,i-=o,Ei=1<<32-Le(t)+i|n<h?(g=d,d=null):g=d.sibling;var _=p(i,d,s[h],c);if(_===null){d===null&&(d=g);break}e&&d&&_.alternate===null&&t(i,d),a=o(_,a,h),u===null?l=_:u.sibling=_,u=_,d=g}if(h===s.length)return n(i,d),B&&Oi(i,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),B&&Oi(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 B&&Oi(a,g),u}for(h=r(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)}),B&&Oi(a,g),u}function b(e,r,o,c){if(typeof o==`object`&&o&&o.type===y&&o.key===null&&(o=o.props.children),typeof o==`object`&&o){switch(o.$$typeof){case _:a:{for(var l=o.key;r!==null;){if(r.key===l){if(l=o.type,l===y){if(r.tag===7){n(e,r.sibling),c=a(r,o.props.children),c.return=e,e=c;break a}}else if(r.elementType===l||typeof l==`object`&&l&&l.$$typeof===O&&Da(l)===r.type){n(e,r.sibling),c=a(r,o.props),Pa(c,o),c.return=e,e=c;break a}n(e,r);break}else t(e,r);r=r.sibling}o.type===y?(c=pi(o.props.children,e.mode,c,o.key),c.return=e,e=c):(c=fi(o.type,o.key,o.props,null,e.mode,c),Pa(c,o),c.return=e,e=c)}return s(e);case v:a:{for(l=o.key;r!==null;){if(r.key===l)if(r.tag===4&&r.stateNode.containerInfo===o.containerInfo&&r.stateNode.implementation===o.implementation){n(e,r.sibling),c=a(r,o.children||[]),c.return=e,e=c;break a}else{n(e,r);break}else t(e,r);r=r.sibling}c=gi(o,e.mode,c),c.return=e,e=c}return s(e);case O:return o=Da(o),b(e,r,o,c)}if(ne(o))return h(e,r,o,c);if(j(o)){if(l=j(o),typeof l!=`function`)throw Error(i(150));return o=l.call(o),g(e,r,o,c)}if(typeof o.then==`function`)return b(e,r,Na(o),c);if(o.$$typeof===C)return b(e,r,ta(e,o),c);Fa(e,o)}return typeof o==`string`&&o!==``||typeof o==`number`||typeof o==`bigint`?(o=``+o,r!==null&&r.tag===6?(n(e,r.sibling),c=a(r,o),c.return=e,e=c):(n(e,r),c=mi(o,e.mode,c),c.return=e,e=c),s(e)):n(e,r)}return function(e,t,n,r){try{Ma=0;var i=b(e,t,n,r);return ja=null,i}catch(t){if(t===xa||t===Ca)throw t;var a=ci(29,t,null,e.mode);return a.lanes=r,a.return=e,a}}}var La=Ia(!0),Ra=Ia(!1),za=!1;function Ba(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function Va(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,callbacks:null})}function Ha(e){return{lane:e,tag:0,payload:null,callback:null,next:null}}function Ua(e,t,n){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,G&2){var i=r.pending;return i===null?t.next=t:(t.next=i.next,i.next=t),r.pending=t,t=ai(e),ii(e,null,n),t}return ti(e,r,t,n),ai(e)}function Wa(e,t,n){if(t=t.updateQueue,t!==null&&(t=t.shared,n&4194048)){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,$e(e,n)}}function Ga(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={lane:n.lane,tag:n.tag,payload:n.payload,callback:null,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,callbacks:r.callbacks},e.updateQueue=n;return}e=n.lastBaseUpdate,e===null?n.firstBaseUpdate=t:e.next=t,n.lastBaseUpdate=t}var Ka=!1;function qa(){if(Ka){var e=fa;if(e!==null)throw e}}function Ja(e,t,n,r){Ka=!1;var i=e.updateQueue;za=!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&-536870913,p=f!==s.lane;if(p?(J&f)===f:(r&f)===f){f!==0&&f===da&&(Ka=!0),u!==null&&(u=u.next={lane:0,tag:s.tag,payload:s.payload,callback:null,next:null});a:{var m=e,g=s;f=t;var _=n;switch(g.tag){case 1:if(m=g.payload,typeof m==`function`){d=m.call(_,d,f);break a}d=m;break a;case 3:m.flags=m.flags&-65537|128;case 0:if(m=g.payload,f=typeof m==`function`?m.call(_,d,f):m,f==null)break a;d=h({},d,f);break a;case 2:za=!0}}f=s.callback,f!==null&&(e.flags|=64,p&&(e.flags|=8192),p=i.callbacks,p===null?i.callbacks=[f]:p.push(f))}else 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;p=s,s=p.next,p.next=null,i.lastBaseUpdate=p,i.shared.pending=null}}while(1);u===null&&(c=d),i.baseState=c,i.firstBaseUpdate=l,i.lastBaseUpdate=u,a===null&&(i.shared.lanes=0),Gl|=o,e.lanes=o,e.memoizedState=d}}function Ya(e,t){if(typeof e!=`function`)throw Error(i(191,e));e.call(t)}function Xa(e,t){var n=e.callbacks;if(n!==null)for(e.callbacks=null,e=0;ea?a:8;var o=N.T,s={};N.T=s,Fs(e,!1,t,n);try{var c=i(),l=N.S;l!==null&&l(s,c),typeof c==`object`&&c&&typeof c.then==`function`?Ps(e,t,ha(c,r),pu(e)):Ps(e,t,r,pu(e))}catch(n){Ps(e,t,{then:function(){},status:`rejected`,reason:n},pu())}finally{P.p=a,o!==null&&s.types!==null&&(o.types=s.types),N.T=o}}function ws(){}function Ts(e,t,n,r){if(e.tag!==5)throw Error(i(476));var a=Es(e).queue;Cs(e,a,t,re,n===null?ws:function(){return Ds(e),n(r)})}function Es(e){var t=e.memoizedState;if(t!==null)return t;t={memoizedState:re,baseState:re,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Io,lastRenderedState:re},next:null};var n={};return t.next={memoizedState:n,baseState:n,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Io,lastRenderedState:n},next:null},e.memoizedState=t,e=e.alternate,e!==null&&(e.memoizedState=t),t}function Ds(e){var t=Es(e);t.next===null&&(t=e.alternate.memoizedState),Ps(e,t.next.queue,{},pu())}function Os(){return ea(Qf)}function ks(){return jo().memoizedState}function As(){return jo().memoizedState}function js(e){for(var t=e.return;t!==null;){switch(t.tag){case 24:case 3:var n=pu();e=Ha(n);var r=Ua(t,e,n);r!==null&&(hu(r,t,n),Wa(r,t,n)),t={cache:sa()},e.payload=t;return}t=t.return}}function Ms(e,t,n){var r=pu();n={lane:r,revertLane:0,gesture:null,action:n,hasEagerState:!1,eagerState:null,next:null},Is(e)?Ls(t,n):(n=ni(e,t,n,r),n!==null&&(hu(n,e,r),Rs(n,t,r)))}function Ns(e,t,n){Ps(e,t,n,pu())}function Ps(e,t,n,r){var i={lane:r,revertLane:0,gesture:null,action:n,hasEagerState:!1,eagerState:null,next:null};if(Is(e))Ls(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,Sr(s,o))return ti(e,t,i,0),K===null&&ei(),!1}catch{}if(n=ni(e,t,i,r),n!==null)return hu(n,e,r),Rs(n,t,r),!0}return!1}function Fs(e,t,n,r){if(r={lane:2,revertLane:dd(),gesture:null,action:r,hasEagerState:!1,eagerState:null,next:null},Is(e)){if(t)throw Error(i(479))}else t=ni(e,n,r,2),t!==null&&hu(t,e,2)}function Is(e){var t=e.alternate;return e===V||t!==null&&t===V}function Ls(e,t){ho=mo=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function Rs(e,t,n){if(n&4194048){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,$e(e,n)}}var zs={readContext:ea,use:Po,useCallback:xo,useContext:xo,useEffect:xo,useImperativeHandle:xo,useLayoutEffect:xo,useInsertionEffect:xo,useMemo:xo,useReducer:xo,useRef:xo,useState:xo,useDebugValue:xo,useDeferredValue:xo,useTransition:xo,useSyncExternalStore:xo,useId:xo,useHostTransitionStatus:xo,useFormState:xo,useActionState:xo,useOptimistic:xo,useMemoCache:xo,useCacheRefresh:xo};zs.useEffectEvent=xo;var Bs={readContext:ea,use:Po,useCallback:function(e,t){return Ao().memoizedState=[e,t===void 0?null:t],e},useContext:ea,useEffect:us,useImperativeHandle:function(e,t,n){n=n==null?null:n.concat([e]),cs(4194308,4,gs.bind(null,t,e),n)},useLayoutEffect:function(e,t){return cs(4194308,4,e,t)},useInsertionEffect:function(e,t){cs(4,2,e,t)},useMemo:function(e,t){var n=Ao();t=t===void 0?null:t;var r=e();if(go){Ie(!0);try{e()}finally{Ie(!1)}}return n.memoizedState=[r,t],r},useReducer:function(e,t,n){var r=Ao();if(n!==void 0){var i=n(t);if(go){Ie(!0);try{n(t)}finally{Ie(!1)}}}else i=t;return r.memoizedState=r.baseState=i,e={pending:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:i},r.queue=e,e=e.dispatch=Ms.bind(null,V,e),[r.memoizedState,e]},useRef:function(e){var t=Ao();return e={current:e},t.memoizedState=e},useState:function(e){e=Ko(e);var t=e.queue,n=Ns.bind(null,V,t);return t.dispatch=n,[e.memoizedState,n]},useDebugValue:vs,useDeferredValue:function(e,t){return xs(Ao(),e,t)},useTransition:function(){var e=Ko(!1);return e=Cs.bind(null,V,e.queue,!0,!1),Ao().memoizedState=e,[!1,e]},useSyncExternalStore:function(e,t,n){var r=V,a=Ao();if(B){if(n===void 0)throw Error(i(407));n=n()}else{if(n=t(),K===null)throw Error(i(349));J&127||Vo(r,t,n)}a.memoizedState=n;var o={value:n,getSnapshot:t};return a.queue=o,us(Uo.bind(null,r,o,e),[e]),r.flags|=2048,os(9,{destroy:void 0},Ho.bind(null,r,o,n,t),null),n},useId:function(){var e=Ao(),t=K.identifierPrefix;if(B){var n=Di,r=Ei;n=(r&~(1<<32-Le(r)-1)).toString(32)+n,t=`_`+t+`R_`+n,n=_o++,0<\/script>`,o=o.removeChild(o.firstChild);break;case`select`:o=typeof r.is==`string`?s.createElement(`select`,{is:r.is}):s.createElement(`select`),r.multiple?o.multiple=!0:r.size&&(o.size=r.size);break;default:o=typeof r.is==`string`?s.createElement(a,{is:r.is}):s.createElement(a)}}o[ot]=t,o[st]=r;a:for(s=t.child;s!==null;){if(s.tag===5||s.tag===6)o.appendChild(s.stateNode);else if(s.tag!==4&&s.tag!==27&&s.child!==null){s.child.return=s,s=s.child;continue}if(s===t)break a;for(;s.sibling===null;){if(s.return===null||s.return===t)break a;s=s.return}s.sibling.return=s.return,s=s.sibling}t.stateNode=o;a:switch(Pd(o,a,r),a){case`button`:case`input`:case`select`:case`textarea`:r=!!r.autoFocus;break a;case`img`:r=!0;break a;default:r=!1}r&&Pc(t)}}return U(t),Fc(t,t.type,e===null?null:e.memoizedProps,t.pendingProps,n),null;case 6:if(e&&t.stateNode!=null)e.memoizedProps!==r&&Pc(t);else{if(typeof r!=`string`&&t.stateNode===null)throw Error(i(166));if(e=se.current,Bi(t)){if(e=t.stateNode,n=t.memoizedProps,r=null,a=Ni,a!==null)switch(a.tag){case 27:case 5:r=a.memoizedProps}e[ot]=t,e=!!(e.nodeValue===n||r!==null&&!0===r.suppressHydrationWarning||Md(e.nodeValue,n)),e||Li(t,!0)}else e=Bd(e).createTextNode(r),e[ot]=t,t.stateNode=e}return U(t),null;case 31:if(n=t.memoizedState,e===null||e.memoizedState!==null){if(r=Bi(t),n!==null){if(e===null){if(!r)throw Error(i(318));if(e=t.memoizedState,e=e===null?null:e.dehydrated,!e)throw Error(i(557));e[ot]=t}else Vi(),!(t.flags&128)&&(t.memoizedState=null),t.flags|=4;U(t),e=!1}else n=Hi(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=n),e=!0;if(!e)return t.flags&256?(co(t),t):(co(t),null);if(t.flags&128)throw Error(i(558))}return U(t),null;case 13:if(r=t.memoizedState,e===null||e.memoizedState!==null&&e.memoizedState.dehydrated!==null){if(a=Bi(t),r!==null&&r.dehydrated!==null){if(e===null){if(!a)throw Error(i(318));if(a=t.memoizedState,a=a===null?null:a.dehydrated,!a)throw Error(i(317));a[ot]=t}else Vi(),!(t.flags&128)&&(t.memoizedState=null),t.flags|=4;U(t),a=!1}else a=Hi(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=a),a=!0;if(!a)return t.flags&256?(co(t),t):(co(t),null)}return co(t),t.flags&128?(t.lanes=n,t):(n=r!==null,e=e!==null&&e.memoizedState!==null,n&&(r=t.child,a=null,r.alternate!==null&&r.alternate.memoizedState!==null&&r.alternate.memoizedState.cachePool!==null&&(a=r.alternate.memoizedState.cachePool.pool),o=null,r.memoizedState!==null&&r.memoizedState.cachePool!==null&&(o=r.memoizedState.cachePool.pool),o!==a&&(r.flags|=2048)),n!==e&&n&&(t.child.flags|=8192),Lc(t,t.updateQueue),U(t),null);case 4:return ue(),e===null&&Sd(t.stateNode.containerInfo),U(t),null;case 10:return Ji(t.type),U(t),null;case 19:if(I(lo),r=t.memoizedState,r===null)return U(t),null;if(a=(t.flags&128)!=0,o=r.rendering,o===null)if(a)Rc(r,!1);else{if(X!==0||e!==null&&e.flags&128)for(e=t.child;e!==null;){if(o=uo(e),o!==null){for(t.flags|=128,Rc(r,!1),e=o.updateQueue,t.updateQueue=e,Lc(t,e),t.subtreeFlags=0,e=n,n=t.child;n!==null;)di(n,e),n=n.sibling;return L(lo,lo.current&1|2),B&&Oi(t,r.treeForkCount),t.child}e=e.sibling}r.tail!==null&&Te()>tu&&(t.flags|=128,a=!0,Rc(r,!1),t.lanes=4194304)}else{if(!a)if(e=uo(o),e!==null){if(t.flags|=128,a=!0,e=e.updateQueue,t.updateQueue=e,Lc(t,e),Rc(r,!0),r.tail===null&&r.tailMode===`hidden`&&!o.alternate&&!B)return U(t),null}else 2*Te()-r.renderingStartTime>tu&&n!==536870912&&(t.flags|=128,a=!0,Rc(r,!1),t.lanes=4194304);r.isBackwards?(o.sibling=t.child,t.child=o):(e=r.last,e===null?t.child=o:e.sibling=o,r.last=o)}return r.tail===null?(U(t),null):(e=r.tail,r.rendering=e,r.tail=e.sibling,r.renderingStartTime=Te(),e.sibling=null,n=lo.current,L(lo,a?n&1|2:n&1),B&&Oi(t,r.treeForkCount),e);case 22:case 23:return co(t),to(),r=t.memoizedState!==null,e===null?r&&(t.flags|=8192):e.memoizedState!==null!==r&&(t.flags|=8192),r?n&536870912&&!(t.flags&128)&&(U(t),t.subtreeFlags&6&&(t.flags|=8192)):U(t),n=t.updateQueue,n!==null&&Lc(t,n.retryQueue),n=null,e!==null&&e.memoizedState!==null&&e.memoizedState.cachePool!==null&&(n=e.memoizedState.cachePool.pool),r=null,t.memoizedState!==null&&t.memoizedState.cachePool!==null&&(r=t.memoizedState.cachePool.pool),r!==n&&(t.flags|=2048),e!==null&&I(_a),null;case 24:return n=null,e!==null&&(n=e.memoizedState.cache),t.memoizedState.cache!==n&&(t.flags|=2048),Ji(oa),U(t),null;case 25:return null;case 30:return null}throw Error(i(156,t.tag))}function Bc(e,t){switch(ji(t),t.tag){case 1:return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return Ji(oa),ue(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 26:case 27:case 5:return fe(t),null;case 31:if(t.memoizedState!==null){if(co(t),t.alternate===null)throw Error(i(340));Vi()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 13:if(co(t),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(i(340));Vi()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return I(lo),null;case 4:return ue(),null;case 10:return Ji(t.type),null;case 22:case 23:return co(t),to(),e!==null&&I(_a),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 24:return Ji(oa),null;case 25:return null;default:return null}}function Vc(e,t){switch(ji(t),t.tag){case 3:Ji(oa),ue();break;case 26:case 27:case 5:fe(t);break;case 4:ue();break;case 31:t.memoizedState!==null&&co(t);break;case 13:co(t);break;case 19:I(lo);break;case 10:Ji(t.type);break;case 22:case 23:co(t),to(),e!==null&&I(_a);break;case 24:Ji(oa)}}function Hc(e,t){try{var n=t.updateQueue,r=n===null?null:n.lastEffect;if(r!==null){var i=r.next;n=i;do{if((n.tag&e)===e){r=void 0;var a=n.create,o=n.inst;r=a(),o.destroy=r}n=n.next}while(n!==i)}}catch(e){Z(t,t.return,e)}}function Uc(e,t,n){try{var r=t.updateQueue,i=r===null?null:r.lastEffect;if(i!==null){var a=i.next;r=a;do{if((r.tag&e)===e){var o=r.inst,s=o.destroy;if(s!==void 0){o.destroy=void 0,i=t;var c=n,l=s;try{l()}catch(e){Z(i,c,e)}}}r=r.next}while(r!==a)}}catch(e){Z(t,t.return,e)}}function Wc(e){var t=e.updateQueue;if(t!==null){var n=e.stateNode;try{Xa(t,n)}catch(t){Z(e,e.return,t)}}}function Gc(e,t,n){n.props=qs(e.type,e.memoizedProps),n.state=e.memoizedState;try{n.componentWillUnmount()}catch(n){Z(e,t,n)}}function Kc(e,t){try{var n=e.ref;if(n!==null){switch(e.tag){case 26:case 27:case 5:var r=e.stateNode;break;case 30:r=e.stateNode;break;default:r=e.stateNode}typeof n==`function`?e.refCleanup=n(r):n.current=r}}catch(n){Z(e,t,n)}}function qc(e,t){var n=e.ref,r=e.refCleanup;if(n!==null)if(typeof r==`function`)try{r()}catch(n){Z(e,t,n)}finally{e.refCleanup=null,e=e.alternate,e!=null&&(e.refCleanup=null)}else if(typeof n==`function`)try{n(null)}catch(n){Z(e,t,n)}else n.current=null}function Jc(e){var t=e.type,n=e.memoizedProps,r=e.stateNode;try{a:switch(t){case`button`:case`input`:case`select`:case`textarea`:n.autoFocus&&r.focus();break a;case`img`:n.src?r.src=n.src:n.srcSet&&(r.srcset=n.srcSet)}}catch(t){Z(e,e.return,t)}}function Yc(e,t,n){try{var r=e.stateNode;Fd(r,e.type,n,t),r[st]=t}catch(t){Z(e,e.return,t)}}function Xc(e){return e.tag===5||e.tag===3||e.tag===26||e.tag===27&&Zd(e.type)||e.tag===4}function Zc(e){a:for(;;){for(;e.sibling===null;){if(e.return===null||Xc(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.tag===27&&Zd(e.type)||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 Qc(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?(n.nodeType===9?n.body:n.nodeName===`HTML`?n.ownerDocument.body:n).insertBefore(e,t):(t=n.nodeType===9?n.body:n.nodeName===`HTML`?n.ownerDocument.body:n,t.appendChild(e),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=$t));else if(r!==4&&(r===27&&Zd(e.type)&&(n=e.stateNode,t=null),e=e.child,e!==null))for(Qc(e,t,n),e=e.sibling;e!==null;)Qc(e,t,n),e=e.sibling}function $c(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&&(r===27&&Zd(e.type)&&(n=e.stateNode),e=e.child,e!==null))for($c(e,t,n),e=e.sibling;e!==null;)$c(e,t,n),e=e.sibling}function el(e){var t=e.stateNode,n=e.memoizedProps;try{for(var r=e.type,i=t.attributes;i.length;)t.removeAttributeNode(i[0]);Pd(t,r,n),t[ot]=e,t[st]=n}catch(t){Z(e,e.return,t)}}var tl=!1,nl=!1,rl=!1,il=typeof WeakSet==`function`?WeakSet:Set,al=null;function ol(e,t){if(e=e.containerInfo,Rd=sp,e=Dr(e),Or(e)){if(`selectionStart`in e)var n={start:e.selectionStart,end:e.selectionEnd};else a:{n=(n=e.ownerDocument)&&n.defaultView||window;var r=n.getSelection&&n.getSelection();if(r&&r.rangeCount!==0){n=r.anchorNode;var a=r.anchorOffset,o=r.focusNode;r=r.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||r!==0&&f.nodeType!==3||(l=s+r),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===r&&(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(zd={focusedElem:e,selectionRange:n},sp=!1,al=t;al!==null;)if(t=al,e=t.child,t.subtreeFlags&1028&&e!==null)e.return=t,al=e;else for(;al!==null;){switch(t=al,o=t.alternate,e=t.flags,t.tag){case 0:if(e&4&&(e=t.updateQueue,e=e===null?null:e.events,e!==null))for(n=0;n title`))),Pd(o,r,n),o[ot]=e,yt(o),r=o;break a;case`link`:var s=Vf(`link`,`href`,a).get(r+(n.href||``));if(s){for(var c=0;cg&&(o=g,g=h,h=o);var _=Tr(s,h),v=Tr(s,g);if(_&&v&&(p.rangeCount!==1||p.anchorNode!==_.node||p.anchorOffset!==_.offset||p.focusNode!==v.node||p.focusOffset!==v.offset)){var y=d.createRange();y.setStart(_.node,_.offset),p.removeAllRanges(),h>g?(p.addRange(y),p.extend(v.node,v.offset)):(y.setEnd(v.node,v.offset),p.addRange(y))}}}}for(d=[],p=s;p=p.parentNode;)p.nodeType===1&&d.push({element:p,left:p.scrollLeft,top:p.scrollTop});for(typeof s.focus==`function`&&s.focus(),s=0;sn?32:n,N.T=null,n=lu,lu=null;var o=au,s=su;if(iu=0,ou=au=null,su=0,G&6)throw Error(i(331));var c=G;if(G|=4,Fl(o.current),Dl(o,o.current,s,n),G=c,id(0,!1),Fe&&typeof Fe.onPostCommitFiberRoot==`function`)try{Fe.onPostCommitFiberRoot(Pe,o)}catch{}return!0}finally{P.p=a,N.T=r,Vu(e,t)}}function Wu(e,t,n){t=vi(n,t),t=$s(e.stateNode,t,2),e=Ua(e,t,2),e!==null&&(Xe(e,2),rd(e))}function Z(e,t,n){if(e.tag===3)Wu(e,e,n);else for(;t!==null;){if(t.tag===3){Wu(t,e,n);break}else if(t.tag===1){var r=t.stateNode;if(typeof t.type.getDerivedStateFromError==`function`||typeof r.componentDidCatch==`function`&&(ru===null||!ru.has(r))){e=vi(n,e),n=ec(2),r=Ua(t,n,2),r!==null&&(tc(n,r,t,e),Xe(r,2),rd(r));break}}t=t.return}}function Gu(e,t,n){var r=e.pingCache;if(r===null){r=e.pingCache=new zl;var i=new Set;r.set(t,i)}else i=r.get(t),i===void 0&&(i=new Set,r.set(t,i));i.has(n)||(Ul=!0,i.add(n),e=Ku.bind(null,e,t,n),t.then(e,e))}function Ku(e,t,n){var r=e.pingCache;r!==null&&r.delete(t),e.pingedLanes|=e.suspendedLanes&n,e.warmLanes&=~n,K===e&&(J&n)===n&&(X===4||X===3&&(J&62914560)===J&&300>Te()-$l?!(G&2)&&Su(e,0):ql|=n,Yl===J&&(Yl=0)),rd(e)}function qu(e,t){t===0&&(t=Je()),e=ri(e,t),e!==null&&(Xe(e,t),rd(e))}function Ju(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),qu(e,n)}function Yu(e,t){var n=0;switch(e.tag){case 31:case 13:var r=e.stateNode,a=e.memoizedState;a!==null&&(n=a.retryLane);break;case 19:r=e.stateNode;break;case 22:r=e.stateNode._retryCache;break;default:throw Error(i(314))}r!==null&&r.delete(t),qu(e,n)}function Xu(e,t){return xe(e,t)}var Zu=null,Qu=null,$u=!1,ed=!1,td=!1,nd=0;function rd(e){e!==Qu&&e.next===null&&(Qu===null?Zu=Qu=e:Qu=Qu.next=e),ed=!0,$u||($u=!0,ud())}function id(e,t){if(!td&&ed){td=!0;do for(var n=!1,r=Zu;r!==null;){if(!t)if(e!==0){var i=r.pendingLanes;if(i===0)var a=0;else{var o=r.suspendedLanes,s=r.pingedLanes;a=(1<<31-Le(42|e)+1)-1,a&=i&~(o&~s),a=a&201326741?a&201326741|1:a?a|2:0}a!==0&&(n=!0,ld(r,a))}else a=J,a=Ge(r,r===K?a:0,r.cancelPendingCommit!==null||r.timeoutHandle!==-1),!(a&3)||Ke(r,a)||(n=!0,ld(r,a));r=r.next}while(n);td=!1}}function ad(){od()}function od(){ed=$u=!1;var e=0;nd!==0&&Gd()&&(e=nd);for(var t=Te(),n=null,r=Zu;r!==null;){var i=r.next,a=sd(r,t);a===0?(r.next=null,n===null?Zu=i:n.next=i,i===null&&(Qu=n)):(n=r,(e!==0||a&3)&&(ed=!0)),r=i}iu!==0&&iu!==5||id(e,!1),nd!==0&&(nd=0)}function sd(e,t){for(var n=e.suspendedLanes,r=e.pingedLanes,i=e.expirationTimes,a=e.pendingLanes&-62914561;0s)break;var u=c.transferSize,d=c.initiatorType;u&&Id(d)&&(c=c.responseEnd,o+=u*(c`u`?null:document;function xf(e,t,n){var r=bf;if(r&&typeof t==`string`&&t){var i=Rt(t);i=`link[rel="`+e+`"][href="`+i+`"]`,typeof n==`string`&&(i+=`[crossorigin="`+n+`"]`),hf.has(i)||(hf.add(i),e={rel:e,crossOrigin:n,href:t},r.querySelector(i)===null&&(t=r.createElement(`link`),Pd(t,`link`,e),yt(t),r.head.appendChild(t)))}}function Sf(e){_f.D(e),xf(`dns-prefetch`,e,null)}function Cf(e,t){_f.C(e,t),xf(`preconnect`,e,t)}function wf(e,t,n){_f.L(e,t,n);var r=bf;if(r&&e&&t){var i=`link[rel="preload"][as="`+Rt(t)+`"]`;t===`image`&&n&&n.imageSrcSet?(i+=`[imagesrcset="`+Rt(n.imageSrcSet)+`"]`,typeof n.imageSizes==`string`&&(i+=`[imagesizes="`+Rt(n.imageSizes)+`"]`)):i+=`[href="`+Rt(e)+`"]`;var a=i;switch(t){case`style`:a=Af(e);break;case`script`:a=Pf(e)}mf.has(a)||(e=h({rel:`preload`,href:t===`image`&&n&&n.imageSrcSet?void 0:e,as:t},n),mf.set(a,e),r.querySelector(i)!==null||t===`style`&&r.querySelector(jf(a))||t===`script`&&r.querySelector(Ff(a))||(t=r.createElement(`link`),Pd(t,`link`,e),yt(t),r.head.appendChild(t)))}}function Tf(e,t){_f.m(e,t);var n=bf;if(n&&e){var r=t&&typeof t.as==`string`?t.as:`script`,i=`link[rel="modulepreload"][as="`+Rt(r)+`"][href="`+Rt(e)+`"]`,a=i;switch(r){case`audioworklet`:case`paintworklet`:case`serviceworker`:case`sharedworker`:case`worker`:case`script`:a=Pf(e)}if(!mf.has(a)&&(e=h({rel:`modulepreload`,href:e},t),mf.set(a,e),n.querySelector(i)===null)){switch(r){case`audioworklet`:case`paintworklet`:case`serviceworker`:case`sharedworker`:case`worker`:case`script`:if(n.querySelector(Ff(a)))return}r=n.createElement(`link`),Pd(r,`link`,e),yt(r),n.head.appendChild(r)}}}function Ef(e,t,n){_f.S(e,t,n);var r=bf;if(r&&e){var i=vt(r).hoistableStyles,a=Af(e);t||=`default`;var o=i.get(a);if(!o){var s={loading:0,preload:null};if(o=r.querySelector(jf(a)))s.loading=5;else{e=h({rel:`stylesheet`,href:e,"data-precedence":t},n),(n=mf.get(a))&&Rf(e,n);var c=o=r.createElement(`link`);yt(c),Pd(c,`link`,e),c._p=new Promise(function(e,t){c.onload=e,c.onerror=t}),c.addEventListener(`load`,function(){s.loading|=1}),c.addEventListener(`error`,function(){s.loading|=2}),s.loading|=4,Lf(o,t,r)}o={type:`stylesheet`,instance:o,count:1,state:s},i.set(a,o)}}}function Df(e,t){_f.X(e,t);var n=bf;if(n&&e){var r=vt(n).hoistableScripts,i=Pf(e),a=r.get(i);a||(a=n.querySelector(Ff(i)),a||(e=h({src:e,async:!0},t),(t=mf.get(i))&&zf(e,t),a=n.createElement(`script`),yt(a),Pd(a,`link`,e),n.head.appendChild(a)),a={type:`script`,instance:a,count:1,state:null},r.set(i,a))}}function Of(e,t){_f.M(e,t);var n=bf;if(n&&e){var r=vt(n).hoistableScripts,i=Pf(e),a=r.get(i);a||(a=n.querySelector(Ff(i)),a||(e=h({src:e,async:!0,type:`module`},t),(t=mf.get(i))&&zf(e,t),a=n.createElement(`script`),yt(a),Pd(a,`link`,e),n.head.appendChild(a)),a={type:`script`,instance:a,count:1,state:null},r.set(i,a))}}function kf(e,t,n,r){var a=(a=se.current)?gf(a):null;if(!a)throw Error(i(446));switch(e){case`meta`:case`title`:return null;case`style`:return typeof n.precedence==`string`&&typeof n.href==`string`?(t=Af(n.href),n=vt(a).hoistableStyles,r=n.get(t),r||(r={type:`style`,instance:null,count:0,state:null},n.set(t,r)),r):{type:`void`,instance:null,count:0,state:null};case`link`:if(n.rel===`stylesheet`&&typeof n.href==`string`&&typeof n.precedence==`string`){e=Af(n.href);var o=vt(a).hoistableStyles,s=o.get(e);if(s||(a=a.ownerDocument||a,s={type:`stylesheet`,instance:null,count:0,state:{loading:0,preload:null}},o.set(e,s),(o=a.querySelector(jf(e)))&&!o._p&&(s.instance=o,s.state.loading=5),mf.has(e)||(n={rel:`preload`,as:`style`,href:n.href,crossOrigin:n.crossOrigin,integrity:n.integrity,media:n.media,hrefLang:n.hrefLang,referrerPolicy:n.referrerPolicy},mf.set(e,n),o||Nf(a,e,n,s.state))),t&&r===null)throw Error(i(528,``));return s}if(t&&r!==null)throw Error(i(529,``));return null;case`script`:return t=n.async,n=n.src,typeof n==`string`&&t&&typeof t!=`function`&&typeof t!=`symbol`?(t=Pf(n),n=vt(a).hoistableScripts,r=n.get(t),r||(r={type:`script`,instance:null,count:0,state:null},n.set(t,r)),r):{type:`void`,instance:null,count:0,state:null};default:throw Error(i(444,e))}}function Af(e){return`href="`+Rt(e)+`"`}function jf(e){return`link[rel="stylesheet"][`+e+`]`}function Mf(e){return h({},e,{"data-precedence":e.precedence,precedence:null})}function Nf(e,t,n,r){e.querySelector(`link[rel="preload"][as="style"][`+t+`]`)?r.loading=1:(t=e.createElement(`link`),r.preload=t,t.addEventListener(`load`,function(){return r.loading|=1}),t.addEventListener(`error`,function(){return r.loading|=2}),Pd(t,`link`,n),yt(t),e.head.appendChild(t))}function Pf(e){return`[src="`+Rt(e)+`"]`}function Ff(e){return`script[async]`+e}function If(e,t,n){if(t.count++,t.instance===null)switch(t.type){case`style`:var r=e.querySelector(`style[data-href~="`+Rt(n.href)+`"]`);if(r)return t.instance=r,yt(r),r;var a=h({},n,{"data-href":n.href,"data-precedence":n.precedence,href:null,precedence:null});return r=(e.ownerDocument||e).createElement(`style`),yt(r),Pd(r,`style`,a),Lf(r,n.precedence,e),t.instance=r;case`stylesheet`:a=Af(n.href);var o=e.querySelector(jf(a));if(o)return t.state.loading|=4,t.instance=o,yt(o),o;r=Mf(n),(a=mf.get(a))&&Rf(r,a),o=(e.ownerDocument||e).createElement(`link`),yt(o);var s=o;return s._p=new Promise(function(e,t){s.onload=e,s.onerror=t}),Pd(o,`link`,r),t.state.loading|=4,Lf(o,n.precedence,e),t.instance=o;case`script`:return o=Pf(n.src),(a=e.querySelector(Ff(o)))?(t.instance=a,yt(a),a):(r=n,(a=mf.get(o))&&(r=h({},n),zf(r,a)),e=e.ownerDocument||e,a=e.createElement(`script`),yt(a),Pd(a,`link`,r),e.head.appendChild(a),t.instance=a);case`void`:return null;default:throw Error(i(443,t.type))}else t.type===`stylesheet`&&!(t.state.loading&4)&&(r=t.instance,t.state.loading|=4,Lf(r,n.precedence,e));return t.instance}function Lf(e,t,n){for(var r=n.querySelectorAll(`link[rel="stylesheet"][data-precedence],style[data-precedence]`),i=r.length?r[r.length-1]:null,a=i,o=0;o title`):null)}function Uf(e,t,n){if(n===1||t.itemProp!=null)return!1;switch(e){case`meta`:case`title`:return!0;case`style`:if(typeof t.precedence!=`string`||typeof t.href!=`string`||t.href===``)break;return!0;case`link`:if(typeof t.rel!=`string`||typeof t.href!=`string`||t.href===``||t.onLoad||t.onError)break;switch(t.rel){case`stylesheet`:return e=t.disabled,typeof t.precedence==`string`&&e==null;default:return!0}case`script`:if(t.async&&typeof t.async!=`function`&&typeof t.async!=`symbol`&&!t.onLoad&&!t.onError&&t.src&&typeof t.src==`string`)return!0}return!1}function Wf(e){return!(e.type===`stylesheet`&&!(e.state.loading&3))}function Gf(e,t,n,r){if(n.type===`stylesheet`&&(typeof r.media!=`string`||!1!==matchMedia(r.media).matches)&&!(n.state.loading&4)){if(n.instance===null){var i=Af(r.href),a=t.querySelector(jf(i));if(a){t=a._p,typeof t==`object`&&t&&typeof t.then==`function`&&(e.count++,e=Jf.bind(e),t.then(e,e)),n.state.loading|=4,n.instance=a,yt(a);return}a=t.ownerDocument||t,r=Mf(r),(i=mf.get(i))&&Rf(r,i),a=a.createElement(`link`),yt(a);var o=a;o._p=new Promise(function(e,t){o.onload=e,o.onerror=t}),Pd(a,`link`,r),n.instance=a}e.stylesheets===null&&(e.stylesheets=new Map),e.stylesheets.set(n,t),(t=n.state.preload)&&!(n.state.loading&3)&&(e.count++,n=Jf.bind(e),t.addEventListener(`load`,n),t.addEventListener(`error`,n))}}var Kf=0;function qf(e,t){return e.stylesheets&&e.count===0&&Xf(e,e.stylesheets),0Kf?50:800)+t);return e.unsuspend=n,function(){e.unsuspend=null,clearTimeout(r),clearTimeout(i)}}:null}function Jf(){if(this.count--,this.count===0&&(this.imgCount===0||!this.waitingForImages)){if(this.stylesheets)Xf(this,this.stylesheets);else if(this.unsuspend){var e=this.unsuspend;this.unsuspend=null,e()}}}var Yf=null;function Xf(e,t){e.stylesheets=null,e.unsuspend!==null&&(e.count++,Yf=new Map,t.forEach(Zf,e),Yf=null,Jf.call(e))}function Zf(e,t){if(!(t.state.loading&4)){var n=Yf.get(e);if(n)var r=n.get(null);else{n=new Map,Yf.set(e,n);for(var i=e.querySelectorAll(`link[data-precedence],style[data-precedence]`),a=0;a{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=h()})),_=e=>{let t,n=new Set,r=(e,r)=>{let i=typeof e==`function`?e(t):e;if(!Object.is(i,t)){let e=t;t=r??(typeof i!=`object`||!i)?i:Object.assign({},t,i),n.forEach(n=>n(t,e))}},i=()=>t,a={setState:r,getState:i,getInitialState:()=>o,subscribe:e=>(n.add(e),()=>n.delete(e))},o=t=e(r,i,a);return a},v=(e=>e?_(e):_),y=c(u(),1),b=e=>e;function x(e,t=b){let n=y.useSyncExternalStore(e.subscribe,y.useCallback(()=>t(e.getState()),[e,t]),y.useCallback(()=>t(e.getInitialState()),[e,t]));return y.useDebugValue(n),n}var S=e=>{let t=v(e),n=e=>x(t,e);return Object.assign(n,t),n},C=(e=>e?S(e):S),w=g(),T=`/api`;async function E(e,t){let n=await fetch(`${T}${e}`,{...t,headers:{"Content-Type":`application/json`,...t?.headers}});if(!n.ok){let e=await n.json().catch(()=>({detail:n.statusText}));throw Error(e.detail??`HTTP ${n.status}`)}if(n.status!==204)return n.json()}var D={getVersion:()=>E(`/version`),getAccounts:()=>E(`/accounts`),getAccount:e=>E(`/accounts/${e}`),createAccount:e=>E(`/accounts`,{method:`POST`,body:JSON.stringify(e)}),updateAccount:(e,t)=>E(`/accounts/${e}`,{method:`PUT`,body:JSON.stringify(t)}),deleteAccount:e=>E(`/accounts/${e}`,{method:`DELETE`}),deleteBulk:e=>E(`/accounts/delete-bulk`,{method:`POST`,body:JSON.stringify({ids:e})}),assignProxies:()=>E(`/accounts/assign-proxies`,{method:`POST`}),reassignProxies:()=>E(`/accounts/reassign-proxies`,{method:`POST`}),clearProxies:()=>E(`/accounts/clear-proxies`,{method:`POST`}),importAccounts:e=>{let t=new FormData;return t.append(`file`,e),fetch(`${T}/accounts/import`,{method:`POST`,body:t}).then(async e=>{if(!e.ok)throw Error((await e.json().catch(()=>({}))).detail??e.statusText);return e.json()})},executeAction:e=>E(`/actions`,{method:`POST`,body:JSON.stringify(e)}),respondToPrompt:(e,t,n)=>E(`/tasks/${e}/respond`,{method:`POST`,body:JSON.stringify({value:t,login:n||``})}),generate2FA:e=>E(`/actions/generate-2fa`,{method:`POST`,body:JSON.stringify({shared_secret:e})}),generate2FAByAccount:e=>E(`/actions/generate-2fa-by-account`,{method:`POST`,body:JSON.stringify({account_id:e})}),getProxies:()=>E(`/proxies`),addProxy:(e,t=`http`)=>E(`/proxies`,{method:`POST`,body:JSON.stringify({address:e,protocol:t})}),deleteProxy:e=>E(`/proxies/${e}`,{method:`DELETE`}),deleteAllProxies:()=>E(`/proxies/all`,{method:`DELETE`}),bulkAddProxies:e=>E(`/proxies/bulk`,{method:`POST`,body:JSON.stringify(e)}),checkProxies:()=>E(`/proxies/check`,{method:`POST`}),getTasks:()=>E(`/tasks`),getTask:e=>E(`/tasks/${e}`),cancelTask:e=>E(`/tasks/${e}`,{method:`DELETE`}),getMafiles:()=>E(`/mafiles`),uploadMafiles:e=>{let t=new FormData;return e.forEach(e=>t.append(`files`,e)),fetch(`${T}/mafiles/upload`,{method:`POST`,body:t}).then(async e=>{if(!e.ok)throw Error((await e.json().catch(()=>({}))).detail??e.statusText);return e.json()})},deleteMafile:e=>E(`/mafiles/${encodeURIComponent(e)}`,{method:`DELETE`}),exportSecrets:()=>E(`/mafiles/export/all`),exportZip:e=>fetch(`${T}/mafiles/export/zip`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify(e)}).then(e=>{if(!e.ok)throw Error(`Export failed: ${e.status}`);return e.blob()}),getValidationSettings:()=>E(`/settings/validation`),updateValidationSettings:e=>E(`/settings/validation`,{method:`PUT`,body:JSON.stringify(e)}),getDisplaySettings:()=>E(`/settings/display`),updateDisplaySettings:e=>E(`/settings/display`,{method:`PUT`,body:JSON.stringify(e)}),getColumnSettings:()=>E(`/settings/columns`),updateColumnSettings:e=>E(`/settings/columns`,{method:`PUT`,body:JSON.stringify(e)}),getLogpassColumnSettings:()=>E(`/settings/logpass-columns`),updateLogpassColumnSettings:e=>E(`/settings/logpass-columns`,{method:`PUT`,body:JSON.stringify(e)}),getLogs:()=>E(`/logs`),startAutoAccept:e=>E(`/auto-accept/start`,{method:`POST`,body:JSON.stringify({account_ids:e})}),stopAutoAccept:e=>E(`/auto-accept/stop`,{method:`POST`,body:JSON.stringify({account_ids:e})}),getAutoAcceptStatus:()=>E(`/auto-accept/status`),openBrowser:e=>E(`/accounts/${e}/browser`,{method:`POST`}),getLogpassAccounts:()=>E(`/logpass`),createLogpassAccount:e=>E(`/logpass`,{method:`POST`,body:JSON.stringify(e)}),updateLogpassAccount:(e,t)=>E(`/logpass/${e}`,{method:`PUT`,body:JSON.stringify(t)}),deleteLogpassAccount:e=>E(`/logpass/${e}`,{method:`DELETE`}),deleteLogpassBulk:e=>E(`/logpass/delete-bulk`,{method:`POST`,body:JSON.stringify({ids:e})}),importLogpass:e=>E(`/logpass/import`,{method:`POST`,body:JSON.stringify({lines:e})}),validateLogpass:e=>E(`/logpass/validate`,{method:`POST`,body:JSON.stringify({account_ids:e})}),fullParseLogpass:e=>E(`/logpass/full-parse`,{method:`POST`,body:JSON.stringify({account_ids:e})}),assignLogpassProxies:()=>E(`/logpass/assign-proxies`,{method:`POST`}),reassignLogpassProxies:()=>E(`/logpass/reassign-proxies`,{method:`POST`}),clearLogpassProxies:()=>E(`/logpass/clear-proxies`,{method:`POST`}),openLogpassBrowser:e=>E(`/logpass/${e}/browser`,{method:`POST`}),getTokenAccounts:()=>E(`/token-accounts`),createTokenAccount:e=>E(`/token-accounts`,{method:`POST`,body:JSON.stringify(e)}),deleteTokenAccount:e=>E(`/token-accounts/${e}`,{method:`DELETE`}),deleteTokenBulk:e=>E(`/token-accounts/delete-bulk`,{method:`POST`,body:JSON.stringify({ids:e})}),importTokens:e=>E(`/token-accounts/import`,{method:`POST`,body:JSON.stringify({lines:e})}),validateTokens:e=>E(`/token-accounts/validate`,{method:`POST`,body:JSON.stringify({account_ids:e})}),openTokenBrowser:e=>E(`/token-accounts/${e}/browser`,{method:`POST`}),assignTokenProxies:()=>E(`/token-accounts/assign-proxies`,{method:`POST`}),reassignTokenProxies:()=>E(`/token-accounts/reassign-proxies`,{method:`POST`}),clearTokenProxies:()=>E(`/token-accounts/clear-proxies`,{method:`POST`}),getTokenColumnSettings:()=>E(`/settings/token-columns`),updateTokenColumnSettings:e=>E(`/settings/token-columns`,{method:`PUT`,body:JSON.stringify(e)})},O=0,ee={browser:!0,profile:!0,steam_id:!0,login:!0,password:!0,login_pass:!0,email:!0,email_pass:!1,email_login_pass:!1,phone:!0,status:!0,ban:!0,twofa:!0,mafile:!0,proxy:!0,notes:!0,actions:!0,last_online:!0},k={browser:!0,profile:!0,steam_id:!0,login:!0,password:!0,login_pass:!0,status:!0,ban:!0,prime:!0,trophy:!0,behavior:!0,license:!0,proxy:!0,notes:!0,actions:!0,last_online:!0},A={browser:!0,profile:!0,last_online:!0,steam_id:!0,login:!0,token:!0,status:!0,proxy:!0,notes:!0,actions:!0},j=C((e,t)=>({activeTab:`accounts`,setTab:t=>e({activeTab:t}),accountSection:`mafile`,setAccountSection:t=>e({accountSection:t}),toasts:[],addToast:(t,n)=>{let r=++O;e(e=>({toasts:[...e.toasts,{id:r,type:t,message:n}]})),setTimeout(()=>{e(e=>({toasts:e.toasts.filter(e=>e.id!==r)}))},4e3)},removeToast:t=>e(e=>({toasts:e.toasts.filter(e=>e.id!==t)})),hidePasswords:!1,setHidePasswords:t=>{e({hidePasswords:t}),D.updateDisplaySettings({hide_passwords:t}).catch(()=>{})},loadDisplaySettings:async()=>{try{e({hidePasswords:(await D.getDisplaySettings()).hide_passwords})}catch{}},columnVisibility:{...ee},setColumnVisibility:t=>e({columnVisibility:t}),toggleColumn:n=>{let r={...t().columnVisibility,[n]:!t().columnVisibility[n]};e({columnVisibility:r}),D.updateColumnSettings(r).catch(()=>{})},loadColumnSettings:async()=>{try{let t=await D.getColumnSettings();e({columnVisibility:{...ee,...t}})}catch{}},logpassColumnVisibility:{...k},toggleLogpassColumn:n=>{let r={...t().logpassColumnVisibility,[n]:!t().logpassColumnVisibility[n]};e({logpassColumnVisibility:r}),D.updateLogpassColumnSettings(r).catch(()=>{})},loadLogpassColumnSettings:async()=>{try{let t=await D.getLogpassColumnSettings();e({logpassColumnVisibility:{...k,...t}})}catch{}},tokenColumnVisibility:{...A},toggleTokenColumn:n=>{let r={...t().tokenColumnVisibility,[n]:!t().tokenColumnVisibility[n]};e({tokenColumnVisibility:r}),D.updateTokenColumnSettings(r).catch(()=>{})},loadTokenColumnSettings:async()=>{try{let t=await D.getTokenColumnSettings();e({tokenColumnVisibility:{...A,...t}})}catch{}}})),M=`steampanel_locale`,te=localStorage.getItem(M)||`ru`,ne=new Set;function N(){ne.forEach(e=>e())}function P(e){te=e,localStorage.setItem(M,e),N()}function re(){return[(0,y.useSyncExternalStore)(e=>(ne.add(e),()=>ne.delete(e)),()=>te),P]}var ie={ru:{"header.accounts":`Аккаунтов`,"tab.accounts":`Аккаунты`,"tab.mafiles":`Mafile Management`,"tab.tools":`Настройки`,"tab.logs":`Логи`,"section.dataType":`Тип данных`,"col.browser":`Войти`,"col.profile":`Профиль`,"col.lastOnline":`Отлега`,"col.steamId":`Steam ID`,"col.login":`Логин`,"col.password":`Пароль`,"col.loginPass":`Login:Pass`,"col.email":`Email`,"col.emailPass":`Email Pass`,"col.emailLoginPass":`Email:Pass`,"col.phone":`Телефон`,"col.status":`Статус`,"col.ban":`Бан`,"col.twofa":`2FA`,"col.mafile":`Mafile`,"col.proxy":`Прокси`,"col.notes":`Заметки`,"col.actions":`Действия`,"col.prime":`Prime`,"col.trophy":`Trophy`,"col.behavior":`Behavior`,"col.license":`Лицензии`,"col.token":`Токен`,"action.selectAction":`— Действие`,"action.validate":`Валидация`,"action.changePassword":`Сменить пароль`,"action.randomPassword":`Случайный пароль`,"action.changeEmail":`Сменить email`,"action.changePhone":`Сменить телефон`,"action.removePhone":`Удалить телефон`,"action.removeGuard":`Удалить Guard`,"action.noRevocationCode":`Нет revocation_code в mafile`,"action.enableAutoAccept":`Вкл. автопринятие`,"action.disableAutoAccept":`Выкл. авто-принятие входа`,"action.enableAutoAcceptLogin":`Вкл. авто-принятие входа`,"prompt.newPassword":`Введите новый пароль`,"prompt.newEmail":`Введите новый email`,"prompt.newPhone":`Введите новый номер телефона`,"prompt.removePhone":`Введите номер телефона (на который переставится старый)`,"prompt.enterValue":`Введите значение...`,"confirm.removeGuard":`Удалить Steam Guard для этого аккаунта?`,"confirm.deleteAccount":`Удалить аккаунт?`,"confirm.deleteSelected":`Удалить {count} выбранных аккаунтов?`,"confirm.deleteAll":`Удалить ВСЕ {count} аккаунтов? Это необратимо!`,"confirm.deleteLogpassSelected":`Удалить {count} аккаунт(ов)?`,"confirm.deleteLogpassAll":`Удалить все {count} аккаунтов?`,"confirm.deleteTokenSelected":`Удалить {count} токен(ов)?`,"confirm.deleteTokenAll":`Удалить все {count} токенов?`,"confirm.deleteToken":`Удалить токен {name}?`,"confirm.deleteLogpass":`Удалить {name}?`,"confirm.assignProxies":`Назначить прокси аккаунтам без прокси?`,"confirm.reassignProxies":`Переназначить прокси на все аккаунты?`,"confirm.reassignProxiesAll":`Переназначить прокси всем аккаунтам?`,"confirm.clearProxies":`Очистить прокси у всех аккаунтов?`,"confirm.executeBulk":`Выполнить «{action}» для {count} аккаунтов?`,"confirm.yes":`Да`,"btn.import":`Импорт`,"btn.add":`Добавить`,"btn.save":`Сохранить`,"btn.cancel":`Отмена`,"btn.execute":`Выполнить`,"btn.deleteSelected":`Удалить выбранные`,"btn.deleteAll":`Удалить все`,"btn.assignProxies":`Назначить прокси`,"btn.reassign":`Переназначить`,"btn.clearProxies":`Очистить прокси`,"btn.validate":`Валидировать`,"btn.fullParse":`Полный парс`,"btn.generate":`Сгенерировать`,"toast.copied":`Скопировано`,"toast.copyFailed":`Не удалось скопировать`,"toast.noteSaved":`Заметка сохранена`,"toast.noteSaveError":`Ошибка сохранения`,"toast.accountDeleted":`Аккаунт удален`,"toast.accountAdded":`Аккаунт добавлен`,"toast.saved":`Сохранено`,"toast.done":`Готово`,"toast.error":`Ошибка`,"toast.selectAction":`Выберите действие`,"toast.selectAccounts":`Выберите аккаунты`,"toast.noAccounts":`Нет аккаунтов`,"toast.autoAcceptEnabled":`Автопринятие включено для {count} акк.`,"toast.autoAcceptOn":`Авто-принятие входа включено`,"toast.autoAcceptOff":`Авто-принятие входа выключено`,"toast.taskCancelled":`Задача отменена`,"toast.browserOpening":`Браузер открывается...`,"toast.cookiesExpiredRevalidating":`Куки устарели. Запущена повторная валидация...`,"toast.cookiesExpired":`Куки устарели. Провалидируйте аккаунт заново.`,"toast.proxyCleared":`Прокси убран у {name}`,"toast.proxiesReassigned":`Переназначено {used} прокси на {count} акк.`,"toast.proxiesAssigned":`Назначено {used} прокси на {count} аккаунтов`,"toast.proxiesAssignedShort":`Назначено {count} аккаунтам`,"toast.proxiesReassignedShort":`Переназначено {count} аккаунтам`,"toast.proxiesClearedCount":`Очищено прокси у {count} аккаунтов`,"toast.proxiesClearedShort":`Очищено у {count} аккаунтов`,"toast.deleted":`Удалено: {count}`,"toast.sentToMafile":`{count} акк. отправлено в Mafile Management`,"toast.settingsSaved":`Настройки сохранены`,"toast.proxyFormatError":`Не удалось распознать прокси. Проверьте формат.`,"toast.proxiesAdded":`Добавлено {count} прокси`,"toast.proxiesDeleted":`Удалено {count} прокси`,"toast.exportDone":`Экспорт завершён`,"toast.uploaded":`Загружено: {count}`,"toast.uploadedWithErrors":`Загружено: {uploaded}, ошибок: {errors}`,"toast.importResult":`Импортировано: {imported}, Пропущено: {skipped}`,"toast.fileEmpty":`Файл пуст`,"task.validate":`Валидация`,"task.changePassword":`Смена пароля`,"task.randomPassword":`Случ. пароль`,"task.changeEmail":`Смена email`,"task.changePhone":`Смена телефона`,"task.removePhone":`Удал. телефона`,"task.removeGuard":`Удал. Guard`,"status.unknown":`Неизвестен`,"status.valid":`Валидный`,"status.invalid":`Невалидный`,"status.locked":`Заблокирован`,"status.limited":`Ограничен`,"status.banned":`БАН`,"status.ok":`ОК`,"tip.openBrowser":`Открыть Steam в браузере`,"tip.copyPassword":`Копировать пароль`,"tip.copyLoginPass":`Копировать login:pass`,"tip.copyEmailPass":`Копировать пароль email`,"tip.copyEmailLoginPass":`Копировать email:pass`,"tip.show":`Показать`,"tip.hide":`Скрыть`,"tip.gen2fa":`Сгенерировать и скопировать 2FA код`,"tip.code":`Код`,"tip.addNote":`Нажмите чтобы добавить заметку`,"tip.edit":`Редактировать`,"tip.delete":`Удалить`,"tip.autoAcceptActive":`Авто-принятие входа`,"tip.validate":`Валидировать`,"tip.columnSettings":`Настройки столбцов`,"tip.deleteProxy":`Удалить прокси`,"tip.clickToCopy":`Клик для копирования`,"tip.proxyError":`Ошибка прокси: {error}`,"ph.search":`Поиск: логин / Steam ID / заметка / lvl>10`,"ph.searchLogpass":`Поиск: логин / Steam ID / заметка / игра / lvl>10`,"ph.login":`Логин`,"ph.password":`Пароль`,"ph.proxy":`Прокси`,"ph.proxyOptional":`Прокси (необязательно)`,"ph.notes":`Заметки`,"ph.notesOptional":`Заметки (необязательно)`,"ph.loginOptional":`Логин (необязательно)`,"empty.accounts":`Нет аккаунтов. Импортируйте файл`,"empty.logpass":`Аккаунтов нет. Импортируйте файл.`,"empty.tokens":`Токенов нет. Импортируйте или добавьте токены.`,"empty.logs":`Нет логов`,"empty.tasks":`Нет задач`,"modal.addAccount":`Добавить аккаунт`,"modal.editAccount":`Редактировать аккаунт`,"modal.importAccounts":`Импорт аккаунтов`,"modal.addToken":`Добавить токен`,"modal.importTokens":`Импорт токенов`,"import.formats":`Форматы`,"import.formatsColon":`Форматы:`,"import.delimiter":`разделитель`,"import.or":`или`,"import.dragTxt":`Перетащите .txt файл или нажмите для выбора`,"import.dragTxtCsv":`Перетащите .txt / .csv файл или нажмите для выбора`,"import.dragMafile":`Перетащите .mafile файлы или нажмите для выбора`,"import.selected":`Выбран: {name}`,"import.uploading":`Загрузка...`,"import.importBtn":`Импортировать`,"import.tokenFormat":`refresh_token (по одному на строку)`,"paging.shown":`Показано {visible} из {total}…`,"proxy.label":`Прокси {num}`,"proxy.title":`Прокси`,"proxy.count":`{count} шт.`,"proxy.protocol":`Протокол:`,"proxy.format":`Формат:`,"proxy.customFormat":`Свой формат...`,"proxy.formatConstructor":`Конструктор формата:`,"proxy.preview":`Предпросмотр:`,"proxy.dragFile":`Перетащите .txt файл с прокси или нажмите для выбора`,"proxy.addBtn":`Добавить`,"proxy.checking":`Проверка...`,"proxy.checkAll":`Проверить все`,"proxy.deleteAllBtn":`Удалить все прокси`,"proxy.confirmDeleteAll":`Удалить все прокси? Это действие нельзя отменить.`,"proxy.checkResult":`Всего: {total} | Живых: {alive} | Мёртвых: {dead}`,"proxy.removeProxy":`Убрать прокси`,"proxy.reassignProxy":`Переназначить прокси`,"proxy.perLine":`Прокси по одному на строку ({format})`,"proxy.deleteProxy":`Удалить прокси`,"tools.2faGenerator":`Генератор 2FA кода`,"tools.validationSettings":`Настройки валидации`,"tools.loadProfile":`Загружать профиль (никнейм, аватар)`,"tools.checkBans":`Проверять баны`,"tools.maxThreads":`Макс. потоков:`,"tools.autoRevalidateBrowser":`Автовалидация при мёртвых куках (браузер)`,"tools.clickCopy":`Клик для копирования`,"tools.genErrorStr":`Ошибка`,"logs.title":`Лог`,"logs.clear":`Очистить`,"logs.activeTasks":`Активные задачи`,"mafile.upload":`Загрузка Mafile`,"mafile.exportSettings":`Настройки экспорта`,"mafile.exportFormat":`Формат:`,"mafile.flatFiles":`Плоские файлы (.mafile)`,"mafile.perAccountFolder":`Папка на аккаунт`,"mafile.singleFile":`Одним файлом`,"mafile.variables":`Переменные:`,"mafile.folderName":`Название папки:`,"mafile.mafileName":`Название .mafile:`,"mafile.txtSettings":`Настройки .txt`,"mafile.addGlobalTxt":`Добавить общий`,"mafile.withAllAccounts":`со всеми аккаунтами`,"mafile.addTxtPerFolder":`Добавить .txt в каждую папку`,"mafile.skipFolders":`Не создавать папку для каждого аккаунта`,"mafile.txtFolderName":`Название .txt в папке:`,"mafile.txtLineFormat":`Формат строки .txt:`,"mafile.mafileFields":`Поля Mafile`,"mafile.sessionFields":`Поля Session`,"mafile.export":`Экспорт`,"mafile.noAccounts":`нет аккаунтов`,"mafile.sentAccounts":`Отправленные аккаунты`,"mafile.clearBtn":`Очистить`,"mafile.insert":`Вставить {value}`,"settings.display":`Отображение`,"settings.hidePasswords":`Скрывать пароли (спойлер)`,"token.disclaimerTitle":`Вкладка в разработке`,"token.disclaimerBody1":`Функциональность вкладки Token не завершена и находится в стадии разработки.`,"token.disclaimerBody2":`⚠ Валидация токенов может привести к их инвалидации (убить токены). Используйте на свой страх и риск.`,"token.disclaimerBody3":`Импорт, редактирование и любые действия с токенами могут работать некорректно.`,"token.acceptRisk":`Я понимаю риски и хочу продолжить`,"twofa.copied":`2FA: {code} (скопировано)`,"twofa.error":`Ошибка 2FA: {error}`,"misc.task":`Задача {id}`,"misc.taskAction":`Задача {id} - {action} ({login})`,"misc.taskCount":`Задача {id} ({count} акк.)`,"misc.error":`Ошибка: {error}`,"misc.proxyErrorCheck":`Ошибка проверки: {error}`,"misc.exportError":`Ошибка экспорта: {error}`,"misc.genError":`Ошибка генерации: {error}`,"misc.errorStr":`Ошибка`,"misc.insert":`Вставить`},en:{"header.accounts":`Accounts`,"tab.accounts":`Accounts`,"tab.mafiles":`Mafile Management`,"tab.tools":`Settings`,"tab.logs":`Logs`,"section.dataType":`Data Type`,"col.browser":`Login`,"col.profile":`Profile`,"col.lastOnline":`Last Online`,"col.steamId":`Steam ID`,"col.login":`Login`,"col.password":`Password`,"col.loginPass":`Login:Pass`,"col.email":`Email`,"col.emailPass":`Email Pass`,"col.emailLoginPass":`Email:Pass`,"col.phone":`Phone`,"col.status":`Status`,"col.ban":`Ban`,"col.twofa":`2FA`,"col.mafile":`Mafile`,"col.proxy":`Proxy`,"col.notes":`Notes`,"col.actions":`Actions`,"col.prime":`Prime`,"col.trophy":`Trophy`,"col.behavior":`Behavior`,"col.license":`Licenses`,"col.token":`Token`,"action.selectAction":`— Action`,"action.validate":`Validate`,"action.changePassword":`Change password`,"action.randomPassword":`Random password`,"action.changeEmail":`Change email`,"action.changePhone":`Change phone`,"action.removePhone":`Remove phone`,"action.removeGuard":`Remove Guard`,"action.noRevocationCode":`No revocation_code in mafile`,"action.enableAutoAccept":`Enable auto-accept`,"action.disableAutoAccept":`Disable login auto-accept`,"action.enableAutoAcceptLogin":`Enable login auto-accept`,"prompt.newPassword":`Enter new password`,"prompt.newEmail":`Enter new email`,"prompt.newPhone":`Enter new phone number`,"prompt.removePhone":`Enter phone number (old number will be transferred to it)`,"prompt.enterValue":`Enter value...`,"confirm.removeGuard":`Remove Steam Guard for this account?`,"confirm.deleteAccount":`Delete account?`,"confirm.deleteSelected":`Delete {count} selected accounts?`,"confirm.deleteAll":`Delete ALL {count} accounts? This is irreversible!`,"confirm.deleteLogpassSelected":`Delete {count} account(s)?`,"confirm.deleteLogpassAll":`Delete all {count} accounts?`,"confirm.deleteTokenSelected":`Delete {count} token(s)?`,"confirm.deleteTokenAll":`Delete all {count} tokens?`,"confirm.deleteToken":`Delete token {name}?`,"confirm.deleteLogpass":`Delete {name}?`,"confirm.assignProxies":`Assign proxies to accounts without proxies?`,"confirm.reassignProxies":`Reassign proxies to all accounts?`,"confirm.reassignProxiesAll":`Reassign proxies to all accounts?`,"confirm.clearProxies":`Clear proxies from all accounts?`,"confirm.executeBulk":`Execute «{action}» for {count} accounts?`,"confirm.yes":`Yes`,"btn.import":`Import`,"btn.add":`Add`,"btn.save":`Save`,"btn.cancel":`Cancel`,"btn.execute":`Execute`,"btn.deleteSelected":`Delete selected`,"btn.deleteAll":`Delete all`,"btn.assignProxies":`Assign proxies`,"btn.reassign":`Reassign`,"btn.clearProxies":`Clear proxies`,"btn.validate":`Validate`,"btn.fullParse":`Full parse`,"btn.generate":`Generate`,"toast.copied":`Copied`,"toast.copyFailed":`Failed to copy`,"toast.noteSaved":`Note saved`,"toast.noteSaveError":`Error saving`,"toast.accountDeleted":`Account deleted`,"toast.accountAdded":`Account added`,"toast.saved":`Saved`,"toast.done":`Done`,"toast.error":`Error`,"toast.selectAction":`Select an action`,"toast.selectAccounts":`Select accounts`,"toast.noAccounts":`No accounts`,"toast.autoAcceptEnabled":`Auto-accept enabled for {count} accounts`,"toast.autoAcceptOn":`Login auto-accept enabled`,"toast.autoAcceptOff":`Login auto-accept disabled`,"toast.taskCancelled":`Task cancelled`,"toast.browserOpening":`Opening browser...`,"toast.cookiesExpiredRevalidating":`Cookies expired. Re-validating...`,"toast.cookiesExpired":`Cookies expired. Re-validate the account.`,"toast.proxyCleared":`Proxy removed from {name}`,"toast.proxiesReassigned":`Reassigned {used} proxies to {count} accounts`,"toast.proxiesAssigned":`Assigned {used} proxies to {count} accounts`,"toast.proxiesAssignedShort":`Assigned to {count} accounts`,"toast.proxiesReassignedShort":`Reassigned to {count} accounts`,"toast.proxiesClearedCount":`Cleared proxies from {count} accounts`,"toast.proxiesClearedShort":`Cleared from {count} accounts`,"toast.deleted":`Deleted: {count}`,"toast.sentToMafile":`{count} accounts sent to Mafile Management`,"toast.settingsSaved":`Settings saved`,"toast.proxyFormatError":`Failed to parse proxies. Check the format.`,"toast.proxiesAdded":`Added {count} proxies`,"toast.proxiesDeleted":`Deleted {count} proxies`,"toast.exportDone":`Export complete`,"toast.uploaded":`Uploaded: {count}`,"toast.uploadedWithErrors":`Uploaded: {uploaded}, errors: {errors}`,"toast.importResult":`Imported: {imported}, Skipped: {skipped}`,"toast.fileEmpty":`File is empty`,"task.validate":`Validation`,"task.changePassword":`Password change`,"task.randomPassword":`Random password`,"task.changeEmail":`Email change`,"task.changePhone":`Phone change`,"task.removePhone":`Phone removal`,"task.removeGuard":`Guard removal`,"status.unknown":`Unknown`,"status.valid":`Valid`,"status.invalid":`Invalid`,"status.locked":`Locked`,"status.limited":`Limited`,"status.banned":`BANNED`,"status.ok":`OK`,"tip.openBrowser":`Open Steam in browser`,"tip.copyPassword":`Copy password`,"tip.copyLoginPass":`Copy login:pass`,"tip.copyEmailPass":`Copy email password`,"tip.copyEmailLoginPass":`Copy email:pass`,"tip.show":`Show`,"tip.hide":`Hide`,"tip.gen2fa":`Generate and copy 2FA code`,"tip.code":`Code`,"tip.addNote":`Click to add a note`,"tip.edit":`Edit`,"tip.delete":`Delete`,"tip.autoAcceptActive":`Login auto-accept`,"tip.validate":`Validate`,"tip.columnSettings":`Column settings`,"tip.deleteProxy":`Delete proxy`,"tip.clickToCopy":`Click to copy`,"tip.proxyError":`Proxy error: {error}`,"ph.search":`Search: login / Steam ID / notes / lvl>10`,"ph.searchLogpass":`Search: login / Steam ID / notes / game / lvl>10`,"ph.login":`Login`,"ph.password":`Password`,"ph.proxy":`Proxy`,"ph.proxyOptional":`Proxy (optional)`,"ph.notes":`Notes`,"ph.notesOptional":`Notes (optional)`,"ph.loginOptional":`Login (optional)`,"empty.accounts":`No accounts. Import a file`,"empty.logpass":`No accounts. Import a file.`,"empty.tokens":`No tokens. Import or add tokens.`,"empty.logs":`No logs`,"empty.tasks":`No tasks`,"modal.addAccount":`Add account`,"modal.editAccount":`Edit account`,"modal.importAccounts":`Import accounts`,"modal.addToken":`Add token`,"modal.importTokens":`Import tokens`,"import.formats":`Formats`,"import.formatsColon":`Formats:`,"import.delimiter":`delimiter`,"import.or":`or`,"import.dragTxt":`Drag a .txt file or click to select`,"import.dragTxtCsv":`Drag a .txt / .csv file or click to select`,"import.dragMafile":`Drag .mafile files or click to select`,"import.selected":`Selected: {name}`,"import.uploading":`Uploading...`,"import.importBtn":`Import`,"import.tokenFormat":`refresh_token (one per line)`,"paging.shown":`Showing {visible} of {total}…`,"proxy.label":`Proxy {num}`,"proxy.title":`Proxies`,"proxy.count":`{count} total`,"proxy.protocol":`Protocol:`,"proxy.format":`Format:`,"proxy.customFormat":`Custom format...`,"proxy.formatConstructor":`Format constructor:`,"proxy.preview":`Preview:`,"proxy.dragFile":`Drag a .txt file with proxies or click to select`,"proxy.addBtn":`Add`,"proxy.checking":`Checking...`,"proxy.checkAll":`Check all`,"proxy.deleteAllBtn":`Delete all proxies`,"proxy.confirmDeleteAll":`Delete all proxies? This cannot be undone.`,"proxy.checkResult":`Total: {total} | Alive: {alive} | Dead: {dead}`,"proxy.removeProxy":`Remove proxy`,"proxy.reassignProxy":`Reassign proxy`,"proxy.perLine":`Proxies one per line ({format})`,"proxy.deleteProxy":`Delete proxy`,"tools.2faGenerator":`2FA Code Generator`,"tools.validationSettings":`Validation Settings`,"tools.loadProfile":`Load profile (nickname, avatar)`,"tools.checkBans":`Check bans`,"tools.maxThreads":`Max threads:`,"tools.autoRevalidateBrowser":`Auto-revalidate on dead cookies (browser)`,"tools.clickCopy":`Click to copy`,"tools.genErrorStr":`Error`,"logs.title":`Log`,"logs.clear":`Clear`,"logs.activeTasks":`Active Tasks`,"mafile.upload":`Upload Mafile`,"mafile.exportSettings":`Export Settings`,"mafile.exportFormat":`Format:`,"mafile.flatFiles":`Flat files (.mafile)`,"mafile.perAccountFolder":`Folder per account`,"mafile.singleFile":`Single file`,"mafile.variables":`Variables:`,"mafile.folderName":`Folder name:`,"mafile.mafileName":`.mafile name:`,"mafile.txtSettings":`.txt Settings`,"mafile.addGlobalTxt":`Add global`,"mafile.withAllAccounts":`with all accounts`,"mafile.addTxtPerFolder":`Add .txt to each folder`,"mafile.skipFolders":`Don't create folder for each account`,"mafile.txtFolderName":`.txt name in folder:`,"mafile.txtLineFormat":`.txt line format:`,"mafile.mafileFields":`Mafile Fields`,"mafile.sessionFields":`Session Fields`,"mafile.export":`Export`,"mafile.noAccounts":`no accounts`,"mafile.sentAccounts":`Sent Accounts`,"mafile.clearBtn":`Clear`,"mafile.insert":`Insert {value}`,"settings.display":`Display`,"settings.hidePasswords":`Hide passwords (spoiler)`,"token.disclaimerTitle":`Tab under development`,"token.disclaimerBody1":`Token tab functionality is not complete and is under development.`,"token.disclaimerBody2":`⚠ Token validation may invalidate (kill) tokens. Use at your own risk.`,"token.disclaimerBody3":`Import, editing and any actions with tokens may work incorrectly.`,"token.acceptRisk":`I understand the risks and want to continue`,"twofa.copied":`2FA: {code} (copied)`,"twofa.error":`2FA Error: {error}`,"misc.task":`Task {id}`,"misc.taskAction":`Task {id} - {action} ({login})`,"misc.taskCount":`Task {id} ({count} accounts)`,"misc.error":`Error: {error}`,"misc.proxyErrorCheck":`Check error: {error}`,"misc.exportError":`Export error: {error}`,"misc.genError":`Generation error: {error}`,"misc.errorStr":`Error`,"misc.insert":`Insert`}};function ae(e,t){let n=(ie[te]||ie.ru)[e]??ie.ru[e]??e;if(t)for(let[e,r]of Object.entries(t))n=n.replace(`{${e}}`,String(r));return n}function F(){return(0,y.useSyncExternalStore)(e=>(ne.add(e),()=>ne.delete(e)),()=>te),ae}var I=o((e=>{var t=Symbol.for(`react.transitional.element`),n=Symbol.for(`react.fragment`);function r(e,n,r){var i=null;if(r!==void 0&&(i=``+r),n.key!==void 0&&(i=``+n.key),`key`in n)for(var a in r={},n)a!==`key`&&(r[a]=n[a]);else r=n;return n=r.ref,{$$typeof:t,type:e,key:i,ref:n===void 0?null:n,props:r}}e.Fragment=n,e.jsx=r,e.jsxs=r})),L=o(((e,t)=>{t.exports=I()}))();function oe(){let[e,t]=re(),[n,r]=(0,y.useState)(``);return(0,y.useEffect)(()=>{D.getVersion().then(e=>r(e.version)).catch(()=>{})},[]),(0,L.jsxs)(`header`,{className:`bg-dark-800 border-b border-dark-600 px-4 py-3 flex items-center justify-between`,children:[(0,L.jsxs)(`div`,{className:`flex items-center gap-3`,children:[(0,L.jsxs)(`div`,{className:`flex items-center gap-1.5 cursor-pointer`,onClick:()=>window.location.reload(),children:[(0,L.jsx)(`svg`,{viewBox:`0 0 24 24`,xmlns:`http://www.w3.org/2000/svg`,className:`w-7 h-7 shrink-0 fill-white`,children:(0,L.jsx)(`path`,{d:`M11.979 0C5.678 0 0.511 4.86 0.022 11.037l6.432 2.658c0.545 -0.371 1.203 -0.59 1.912 -0.59 0.063 0 0.125 0.004 0.188 0.006l2.861 -4.142V8.91c0 -2.495 2.028 -4.524 4.524 -4.524 2.494 0 4.524 2.031 4.524 4.527s-2.03 4.525 -4.524 4.525h-0.105l-4.076 2.911c0 0.052 0.004 0.105 0.004 0.159 0 1.875 -1.515 3.396 -3.39 3.396 -1.635 0 -3.016 -1.173 -3.331 -2.727L0.436 15.27C1.862 20.307 6.486 24 11.979 24c6.627 0 11.999 -5.373 11.999 -12S18.605 0 11.979 0zM7.54 18.21l-1.473 -0.61c0.262 0.543 0.714 0.999 1.314 1.25 1.297 0.539 2.793 -0.076 3.332 -1.375 0.263 -0.63 0.264 -1.319 0.005 -1.949s-0.75 -1.121 -1.377 -1.383c-0.624 -0.26 -1.29 -0.249 -1.878 -0.03l1.523 0.63c0.956 0.4 1.409 1.5 1.009 2.455 -0.397 0.957 -1.497 1.41 -2.454 1.012H7.54zm11.415 -9.303c0 -1.662 -1.353 -3.015 -3.015 -3.015 -1.665 0 -3.015 1.353 -3.015 3.015 0 1.665 1.35 3.015 3.015 3.015 1.663 0 3.015 -1.35 3.015 -3.015zm-5.273 -0.005c0 -1.252 1.013 -2.266 2.265 -2.266 1.249 0 2.266 1.014 2.266 2.266 0 1.251 -1.017 2.265 -2.266 2.265 -1.253 0 -2.265 -1.014 -2.265 -2.265z`})}),(0,L.jsx)(`h1`,{className:`text-lg font-bold text-white tracking-wide`,children:`SteamPanel`})]}),(0,L.jsx)(`span`,{className:`text-xs text-gray-500`,children:n&&`v${n}`}),(0,L.jsxs)(`span`,{className:`text-xs text-gray-500`,children:[`by`,` `,(0,L.jsx)(`a`,{href:`https://t.me/lolzdm`,target:`_blank`,rel:`noopener noreferrer`,className:`hover:text-accent transition`,children:`@lolzdm`})]})]}),(0,L.jsx)(`div`,{className:`flex items-center gap-4`,children:(0,L.jsxs)(`button`,{onClick:()=>t(e===`ru`?`en`:`ru`),className:`flex items-center gap-1.5 text-xs text-gray-400 hover:text-gray-200 transition px-2 py-1 border border-dark-500 hover:border-gray-500 rounded`,children:[(0,L.jsxs)(`svg`,{xmlns:`http://www.w3.org/2000/svg`,width:`13`,height:`13`,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:`1.8`,strokeLinecap:`round`,strokeLinejoin:`round`,className:`opacity-80`,children:[(0,L.jsx)(`circle`,{cx:`12`,cy:`12`,r:`10`}),(0,L.jsx)(`path`,{d:`M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z`})]}),e===`ru`?`EN`:`RU`]})})]})}var R=(e=16)=>({width:e,height:e,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:2,strokeLinecap:`round`,strokeLinejoin:`round`}),se=({size:e=16,...t})=>(0,L.jsxs)(`svg`,{...R(e),...t,children:[(0,L.jsx)(`path`,{d:`M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4`}),(0,L.jsx)(`polyline`,{points:`7 10 12 15 17 10`}),(0,L.jsx)(`line`,{x1:`12`,y1:`15`,x2:`12`,y2:`3`})]}),ce=({size:e=16,...t})=>(0,L.jsxs)(`svg`,{...R(e),...t,children:[(0,L.jsx)(`path`,{d:`M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4`}),(0,L.jsx)(`polyline`,{points:`17 8 12 3 7 8`}),(0,L.jsx)(`line`,{x1:`12`,y1:`3`,x2:`12`,y2:`15`})]}),le=({size:e=16,...t})=>(0,L.jsxs)(`svg`,{...R(e),...t,children:[(0,L.jsx)(`line`,{x1:`12`,y1:`5`,x2:`12`,y2:`19`}),(0,L.jsx)(`line`,{x1:`5`,y1:`12`,x2:`19`,y2:`12`})]}),ue=({size:e=16,...t})=>(0,L.jsx)(`svg`,{...R(e),...t,children:(0,L.jsx)(`polyline`,{points:`20 6 9 17 4 12`})}),de=({size:e=16,...t})=>(0,L.jsxs)(`svg`,{...R(e),...t,children:[(0,L.jsx)(`path`,{d:`M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z`}),(0,L.jsx)(`line`,{x1:`12`,y1:`9`,x2:`12`,y2:`13`}),(0,L.jsx)(`line`,{x1:`12`,y1:`17`,x2:`12.01`,y2:`17`})]}),fe=({size:e=16,...t})=>(0,L.jsx)(`svg`,{...R(e),...t,children:(0,L.jsx)(`path`,{d:`m6 14 1.5-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.54 6a2 2 0 0 1-1.95 1.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H18a2 2 0 0 1 2 2v2`})}),pe=({size:e=16,...t})=>(0,L.jsxs)(`svg`,{...R(e),...t,children:[(0,L.jsx)(`circle`,{cx:`12`,cy:`12`,r:`10`}),(0,L.jsx)(`line`,{x1:`2`,y1:`12`,x2:`22`,y2:`12`}),(0,L.jsx)(`path`,{d:`M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z`})]}),me=({size:e=16,...t})=>(0,L.jsxs)(`svg`,{...R(e),...t,children:[(0,L.jsx)(`polyline`,{points:`23 4 23 10 17 10`}),(0,L.jsx)(`path`,{d:`M20.49 15a9 9 0 1 1-2.12-9.36L23 10`})]}),he=({size:e=16,...t})=>(0,L.jsxs)(`svg`,{...R(e),...t,children:[(0,L.jsx)(`circle`,{cx:`12`,cy:`12`,r:`10`}),(0,L.jsx)(`line`,{x1:`4.93`,y1:`4.93`,x2:`19.07`,y2:`19.07`})]}),ge=({size:e=16,...t})=>(0,L.jsxs)(`svg`,{...R(e),...t,children:[(0,L.jsx)(`path`,{d:`M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z`}),(0,L.jsx)(`circle`,{cx:`12`,cy:`12`,r:`3`})]}),_e=({size:e=16,...t})=>(0,L.jsxs)(`svg`,{...R(e),...t,children:[(0,L.jsx)(`path`,{d:`M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94`}),(0,L.jsx)(`path`,{d:`M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19`}),(0,L.jsx)(`line`,{x1:`1`,y1:`1`,x2:`23`,y2:`23`}),(0,L.jsx)(`path`,{d:`M14.12 14.12a3 3 0 1 1-4.24-4.24`})]}),ve=({size:e=16,...t})=>(0,L.jsx)(`svg`,{...R(e),...t,children:(0,L.jsx)(`path`,{d:`M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.78 7.78 5.5 5.5 0 0 1 7.78-7.78zm0 0L15.5 7.5m0 0 3 3L22 7l-3-3m-3.5 3.5L19 4`})}),ye=({size:e=16,...t})=>(0,L.jsx)(`svg`,{...R(e),...t,children:(0,L.jsx)(`polygon`,{points:`13 2 3 14 12 14 11 22 21 10 12 10 13 2`})}),be=({size:e=16,...t})=>(0,L.jsxs)(`svg`,{...R(e),...t,children:[(0,L.jsx)(`circle`,{cx:`12`,cy:`12`,r:`3`}),(0,L.jsx)(`path`,{d:`M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z`})]}),xe=({size:e=16,...t})=>(0,L.jsxs)(`svg`,{...R(e),...t,children:[(0,L.jsx)(`path`,{d:`M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7`}),(0,L.jsx)(`path`,{d:`M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z`})]}),Se=({size:e=16,...t})=>(0,L.jsxs)(`svg`,{...R(e),...t,children:[(0,L.jsx)(`polyline`,{points:`3 6 5 6 21 6`}),(0,L.jsx)(`path`,{d:`M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2`})]}),Ce=({size:e=16,...t})=>(0,L.jsxs)(`svg`,{...R(e),...t,children:[(0,L.jsx)(`rect`,{x:`3`,y:`11`,width:`18`,height:`11`,rx:`2`,ry:`2`}),(0,L.jsx)(`path`,{d:`M7 11V7a5 5 0 0 1 10 0v4`})]}),we=({size:e=16,...t})=>(0,L.jsxs)(`svg`,{...R(e),...t,children:[(0,L.jsx)(`line`,{x1:`16.5`,y1:`9.4`,x2:`7.5`,y2:`4.21`}),(0,L.jsx)(`path`,{d:`M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z`}),(0,L.jsx)(`polyline`,{points:`3.27 6.96 12 12.01 20.73 6.96`}),(0,L.jsx)(`line`,{x1:`12`,y1:`22.08`,x2:`12`,y2:`12`})]}),Te=({size:e=16,...t})=>(0,L.jsxs)(`svg`,{...R(e),...t,children:[(0,L.jsx)(`path`,{d:`M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z`}),(0,L.jsx)(`polyline`,{points:`14 2 14 8 20 8`}),(0,L.jsx)(`line`,{x1:`16`,y1:`13`,x2:`8`,y2:`13`}),(0,L.jsx)(`line`,{x1:`16`,y1:`17`,x2:`8`,y2:`17`}),(0,L.jsx)(`polyline`,{points:`10 9 9 9 8 9`})]}),Ee=({size:e=16,...t})=>(0,L.jsxs)(`svg`,{...R(e),...t,children:[(0,L.jsx)(`path`,{d:`M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2`}),(0,L.jsx)(`rect`,{x:`8`,y:`2`,width:`8`,height:`4`,rx:`1`,ry:`1`})]}),De=({size:e=16,...t})=>(0,L.jsx)(`svg`,{...R(e),...t,children:(0,L.jsx)(`polygon`,{points:`5 3 19 12 5 21 5 3`})}),Oe=({size:e=16,...t})=>(0,L.jsxs)(`svg`,{...R(e),...t,children:[(0,L.jsx)(`circle`,{cx:`12`,cy:`12`,r:`10`}),(0,L.jsx)(`line`,{x1:`15`,y1:`9`,x2:`9`,y2:`15`}),(0,L.jsx)(`line`,{x1:`9`,y1:`9`,x2:`15`,y2:`15`})]}),ke=({size:e=16,...t})=>(0,L.jsxs)(`svg`,{...R(e),...t,children:[(0,L.jsx)(`path`,{d:`M22 11.08V12a10 10 0 1 1-5.93-9.14`}),(0,L.jsx)(`polyline`,{points:`22 4 12 14.01 9 11.01`})]}),Ae=({size:e=16,...t})=>(0,L.jsxs)(`svg`,{...R(e),...t,children:[(0,L.jsx)(`path`,{d:`M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2`}),(0,L.jsx)(`circle`,{cx:`9`,cy:`7`,r:`4`}),(0,L.jsx)(`path`,{d:`M22 21v-2a4 4 0 0 0-3-3.87`}),(0,L.jsx)(`path`,{d:`M16 3.13a4 4 0 0 1 0 7.75`})]}),je=({size:e=16,...t})=>(0,L.jsx)(`svg`,{...R(e),...t,children:(0,L.jsx)(`path`,{d:`m6 14 1.5-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.54 6a2 2 0 0 1-1.95 1.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H18a2 2 0 0 1 2 2v2`})}),Me=({size:e=16,...t})=>(0,L.jsx)(`svg`,{...R(e),...t,children:(0,L.jsx)(`path`,{d:`M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z`})}),Ne=({size:e=16,...t})=>(0,L.jsxs)(`svg`,{...R(e),...t,children:[(0,L.jsx)(`path`,{d:`M15 12h-5`}),(0,L.jsx)(`path`,{d:`M15 8h-5`}),(0,L.jsx)(`path`,{d:`M19 17V5a2 2 0 0 0-2-2H4`}),(0,L.jsx)(`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 0v2`})]}),Pe=({size:e=16,...t})=>(0,L.jsx)(`svg`,{...R(e),...t,children:(0,L.jsx)(`polyline`,{points:`9 18 15 12 9 6`})}),Fe=[{id:`accounts`,labelKey:`tab.accounts`,icon:Ae},{id:`mafiles`,labelKey:`tab.mafiles`,icon:je},{id:`tools`,labelKey:`tab.tools`,icon:Me},{id:`logs`,labelKey:`tab.logs`,icon:Ne}];function Ie(){let e=j(e=>e.activeTab),t=j(e=>e.setTab),n=F();return(0,L.jsx)(`nav`,{className:`bg-dark-800 border-b border-dark-600 flex gap-1 px-4 overflow-x-auto`,children:Fe.map(({id:r,labelKey:i,icon:a})=>(0,L.jsxs)(`button`,{onClick:()=>t(r),className:`flex items-center gap-1.5 px-4 py-2.5 text-sm font-medium transition-colors border-b-2 -mb-px ${e===r?`border-accent text-accent`:`border-transparent text-gray-400 hover:text-gray-200`}`,children:[(0,L.jsx)(a,{size:14}),n(i)]},r))})}function Le(){let e=j(e=>e.toasts),t=j(e=>e.removeToast);return e.length===0?null:(0,L.jsx)(`div`,{className:`toast-container`,children:e.map(e=>(0,L.jsx)(`div`,{className:`toast toast-${e.type}`,onClick:()=>t(e.id),children:e.message},e.id))})}var Re=C(e=>({accounts:[],selectedIds:new Set,loading:!1,loadAccounts:async()=>{e({loading:!0});try{e({accounts:await D.getAccounts()})}finally{e({loading:!1})}},toggleSelect:t=>e(e=>{let n=new Set(e.selectedIds);return n.has(t)?n.delete(t):n.add(t),{selectedIds:n}}),selectAll:()=>e(e=>({selectedIds:new Set(e.accounts.map(e=>e.id))})),clearSelection:()=>e({selectedIds:new Set}),setSelectedIds:t=>e({selectedIds:t}),mafileManagerIds:new Set,sendToMafileManager:t=>e(e=>{let n=new Set(e.mafileManagerIds);return t.forEach(e=>n.add(e)),{mafileManagerIds:n}}),clearMafileManager:()=>e({mafileManagerIds:new Set})})),ze=C(e=>({tasks:[],hasRunning:!1,loadTasks:async()=>{let t=await D.getTasks();e({tasks:t,hasRunning:t.some(e=>e.status===`running`||e.status===`pending`)})}})),Be=C(e=>({accounts:[],selectedIds:new Set,loading:!1,loadAccounts:async()=>{e({loading:!0});try{e({accounts:await D.getLogpassAccounts()})}finally{e({loading:!1})}},toggleSelect:t=>e(e=>{let n=new Set(e.selectedIds);return n.has(t)?n.delete(t):n.add(t),{selectedIds:n}}),selectAll:()=>e(e=>({selectedIds:new Set(e.accounts.map(e=>e.id))})),clearSelection:()=>e({selectedIds:new Set})})),Ve=C(e=>({accounts:[],selectedIds:new Set,loading:!1,loadAccounts:async()=>{e({loading:!0});try{e({accounts:await D.getTokenAccounts()})}finally{e({loading:!1})}},toggleSelect:t=>e(e=>{let n=new Set(e.selectedIds);return n.has(t)?n.delete(t):n.add(t),{selectedIds:n}}),selectAll:()=>e(e=>({selectedIds:new Set(e.accounts.map(e=>e.id))})),clearSelection:()=>e({selectedIds:new Set})}));async function He(e){try{return await navigator.clipboard.writeText(e),!0}catch{return!1}}var Ue={unknown:[`badge-unknown`,`status.unknown`],valid:[`badge-ok`,`status.valid`],invalid:[`badge-error`,`status.invalid`],locked:[`badge-error`,`status.locked`],limited:[`badge-running`,`status.limited`]};function We({status:e}){let t=Ue[e],[n,r]=t?[t[0],ae(t[1])]:[`badge-unknown`,e];return(0,L.jsx)(`span`,{className:`badge ${n}`,children:r})}function Ge({status:e}){return e===`BANNED`?(0,L.jsx)(`span`,{className:`badge badge-error`,children:ae(`status.banned`)}):e===`NO BAN`?(0,L.jsx)(`span`,{className:`badge badge-ok`,children:ae(`status.ok`)}):(0,L.jsx)(`span`,{className:`badge badge-unknown`,children:`—`})}function Ke({hasMafile:e}){return e?(0,L.jsx)(`span`,{className:`badge badge-ok`,children:`✓`}):(0,L.jsx)(`span`,{className:`badge badge-unknown`,children:`—`})}var qe=m(),Je=[{action:`validate`,labelKey:`action.validate`},{action:`change_password`,labelKey:`action.changePassword`},{action:`random_password`,labelKey:`action.randomPassword`},{action:`change_email`,labelKey:`action.changeEmail`},{action:`change_phone`,labelKey:`action.changePhone`},{action:`remove_guard`,labelKey:`action.removeGuard`}],Ye={change_password:`prompt.newPassword`,change_email:`prompt.newEmail`,change_phone:`prompt.newPhone`},Xe={change_password:`new_password`,change_email:`new_email`,change_phone:`new_phone`},Ze={remove_guard:`confirm.removeGuard`};function Qe({account:e,onAction:t,onToggleAutoAccept:n}){let[r,i]=(0,y.useState)(!1),[a,o]=(0,y.useState)(null),[s,c]=(0,y.useState)(``),[l,u]=(0,y.useState)(null),[d,f]=(0,y.useState)(null),p=(0,y.useRef)(null),m=(0,y.useRef)(null),h=j(e=>e.addToast),g=Re(e=>e.loadAccounts),_=F(),v=(0,y.useCallback)(()=>{if(!m.current)return;let e=m.current.getBoundingClientRect();f({top:window.innerHeight-e.bottom<320?Math.max(4,e.top-320):e.bottom+4,left:Math.max(4,e.right-180)})},[]);(0,y.useEffect)(()=>{if(!r)return;v();function e(e){p.current&&!p.current.contains(e.target)&&m.current&&!m.current.contains(e.target)&&i(!1)}function t(){i(!1)}return document.addEventListener(`mousedown`,e),window.addEventListener(`scroll`,t,!0),()=>{document.removeEventListener(`mousedown`,e),window.removeEventListener(`scroll`,t,!0)}},[r,v]);let b=(n,r)=>{i(!1);let a=Ze[n];if(a){u({action:n,title:_(a)});return}let s=Ye[n];s?(o({action:n,title:_(s)}),c(``)):t(e.id,n,{})},x=()=>{if(!a||!s)return;let n=Xe[a.action]||`value`;t(e.id,a.action,{[n]:s}),o(null)},S=e.auto_accept?_(`action.disableAutoAccept`):_(`action.enableAutoAcceptLogin`);return(0,L.jsxs)(L.Fragment,{children:[(0,L.jsxs)(`div`,{className:`relative inline-block`,children:[(0,L.jsx)(`button`,{ref:m,onClick:()=>i(!r),className:`btn-secondary px-2 py-1 text-xs`,children:(0,L.jsx)(ye,{size:12})}),r&&d&&(0,qe.createPortal)((0,L.jsxs)(`div`,{ref:p,className:`action-menu-dropdown`,style:{position:`fixed`,top:d.top,left:d.left},children:[Je.map(({action:t,labelKey:n})=>t===`remove_guard`&&!e.has_revocation_code?(0,L.jsx)(`span`,{"data-tooltip":_(`action.noRevocationCode`),className:`block`,children:(0,L.jsx)(`button`,{className:`action-menu-item opacity-40 cursor-not-allowed`,disabled:!0,children:_(n)})},t):(0,L.jsx)(`button`,{onClick:()=>b(t,_(n)),className:`action-menu-item`,children:_(n)},t)),(0,L.jsx)(`hr`,{className:`border-dark-600 my-1`}),(0,L.jsx)(`button`,{onClick:async()=>{i(!1);try{await D.updateAccount(e.id,{proxy:``}),h(`success`,_(`toast.proxyCleared`,{name:e.login})),await g()}catch(e){h(`error`,_(`misc.error`,{error:e instanceof Error?e.message:String(e)}))}},className:`action-menu-item`,children:_(`proxy.removeProxy`)}),(0,L.jsx)(`button`,{onClick:async()=>{i(!1);try{let e=await D.reassignProxies();h(`success`,_(`toast.proxiesReassigned`,{used:e.proxies_used,count:e.assigned})),await g()}catch(e){h(`error`,_(`misc.error`,{error:e instanceof Error?e.message:String(e)}))}},className:`action-menu-item`,children:_(`proxy.reassignProxy`)}),(0,L.jsx)(`hr`,{className:`border-dark-600 my-1`}),(0,L.jsx)(`button`,{onClick:()=>{i(!1),n(e.id)},className:`action-menu-item`,children:S})]}),document.body)]}),a&&(0,L.jsx)(`div`,{className:`confirm-overlay`,onMouseDown:()=>o(null),children:(0,L.jsxs)(`div`,{className:`confirm-box`,onMouseDown:e=>e.stopPropagation(),children:[(0,L.jsx)(`p`,{className:`text-sm text-gray-300 mb-3`,children:a.title}),(0,L.jsx)(`input`,{autoFocus:!0,value:s,onChange:e=>c(e.target.value),onKeyDown:e=>e.key===`Enter`&&x(),className:`w-full bg-dark-700 border border-dark-600 rounded px-3 py-2 text-sm mb-3`,placeholder:a.action.includes(`phone`)?`+7 9123456789`:void 0}),(0,L.jsxs)(`div`,{className:`flex gap-2`,children:[(0,L.jsx)(`button`,{onClick:x,className:`btn-primary flex-1`,children:`OK`}),(0,L.jsx)(`button`,{onClick:()=>o(null),className:`btn-secondary flex-1`,children:_(`btn.cancel`)})]})]})}),l&&(0,L.jsx)(`div`,{className:`confirm-overlay`,onMouseDown:()=>u(null),children:(0,L.jsxs)(`div`,{className:`confirm-box`,onMouseDown:e=>e.stopPropagation(),children:[(0,L.jsx)(`p`,{className:`text-sm text-gray-300 mb-3`,children:l.title}),(0,L.jsxs)(`div`,{className:`flex gap-2`,children:[(0,L.jsx)(`button`,{autoFocus:!0,onClick:()=>{t(e.id,l.action,{}),u(null)},className:`btn-primary flex-1`,children:_(`confirm.yes`)}),(0,L.jsx)(`button`,{onClick:()=>u(null),className:`btn-secondary flex-1`,children:_(`btn.cancel`)})]})]})})]})}function $e({level:e}){return e>=100?(0,L.jsx)(`div`,{className:`steam-lvl l100p img-${Math.floor(e/100)*100}`,title:`Level ${e}`,children:(0,L.jsx)(`span`,{children:e})}):(0,L.jsx)(`div`,{className:`steam-lvl l${Math.floor(e/10)*10}`,title:`Level ${e}`,children:(0,L.jsx)(`span`,{children:e})})}function et({account:e,visibleColumns:t,proxyLabel:n,isProcessing:r,accountResult:i,accountStep:a,onEdit:o,onDelete:s,onAction:c,onToggleAutoAccept:l,onOpenBrowser:u}){let d=Re(e=>e.selectedIds),f=Re(e=>e.toggleSelect),p=j(e=>e.addToast),m=j(e=>e.hidePasswords),[h,g]=(0,y.useState)(!1),[_,v]=(0,y.useState)(!1),[b,x]=(0,y.useState)(e.notes||``),S=F(),C=async e=>{let t=await He(e);p(t?`success`:`error`,S(t?`toast.copied`:`toast.copyFailed`))},w=m&&!h,T=`••••••••`;return(0,L.jsxs)(`tr`,{className:`hover:bg-dark-700/50 transition`,children:[(0,L.jsx)(`td`,{className:`px-3 py-2`,children:(0,L.jsx)(`input`,{type:`checkbox`,checked:d.has(e.id),onChange:()=>f(e.id)})}),(0,L.jsx)(`td`,{className:`px-1 py-2 w-5`,children:r?(0,L.jsxs)(`div`,{className:`flex items-center gap-1`,children:[(0,L.jsx)(`div`,{className:`w-4 h-4 border-2 border-accent border-t-transparent rounded-full animate-spin shrink-0`}),a&&(0,L.jsxs)(`span`,{className:`text-[10px] text-gray-500 whitespace-nowrap`,children:[`[`,a.step,`/`,a.total,`]`]})]}):i?.status===`ok`?(0,L.jsx)(ue,{size:16,className:`text-gray-400`}):i?.status===`error`?(0,L.jsx)(`span`,{title:i.error?.toLowerCase().includes(`proxy`)||i.error?.toLowerCase().includes(`network`)||i.error?.includes(`WinError`)?S(`tip.proxyError`,{error:i.error}):i.error,children:(0,L.jsx)(de,{size:16,className:`text-red-400`})}):null}),t.browser&&(0,L.jsx)(`td`,{className:`px-3 py-2 whitespace-nowrap text-center`,children:e.has_cookies?(0,L.jsx)(`button`,{onClick:()=>u(e.id),className:`px-2 py-0.5 text-xs font-medium rounded bg-blue-600/20 text-blue-400 border border-blue-500/30 hover:bg-blue-600/40 hover:text-blue-300 active:scale-95 transition`,title:S(`tip.openBrowser`),children:S(`col.browser`)}):(0,L.jsx)(`span`,{className:`text-gray-600 text-xs`,children:`—`})}),t.profile&&(0,L.jsx)(`td`,{className:`px-3 py-2 whitespace-nowrap`,children:(0,L.jsxs)(`div`,{className:`flex items-center gap-1.5`,children:[e.avatar_url&&(0,L.jsx)(`img`,{src:e.avatar_url,className:`avatar-sm`,alt:``}),(0,L.jsx)(`span`,{className:`text-xs`,children:e.nickname||`—`}),e.steam_level!=null&&(0,L.jsx)($e,{level:e.steam_level})]})}),t.last_online&&(0,L.jsx)(`td`,{className:`px-3 py-2 text-xs text-gray-400 whitespace-nowrap`,children:e.last_online||`—`}),t.steam_id&&(0,L.jsx)(`td`,{className:`px-3 py-2 text-xs`,children:e.steam_id?(0,L.jsx)(`a`,{href:`https://steamcommunity.com/profiles/${encodeURIComponent(e.steam_id)}/`,target:`_blank`,rel:`noopener noreferrer`,className:`text-blue-400 hover:text-blue-300 hover:underline`,children:e.steam_id}):`—`}),t.login&&(0,L.jsx)(`td`,{className:`px-3 py-2 font-medium`,children:e.login}),t.password&&(0,L.jsxs)(`td`,{className:`px-3 py-2 text-gray-400 select-none whitespace-nowrap`,children:[(0,L.jsx)(`span`,{className:`cell-copy cursor-pointer`,onClick:()=>C(e.password),title:S(`tip.copyPassword`),children:w?T:e.password}),(0,L.jsx)(`button`,{onClick:e=>{e.stopPropagation(),g(!h)},className:`ml-1.5 text-gray-500 hover:text-gray-300 inline-flex align-middle`,title:S(w?`tip.show`:`tip.hide`),children:w?(0,L.jsx)(ge,{size:14}):(0,L.jsx)(_e,{size:14})})]}),t.login_pass&&(0,L.jsxs)(`td`,{className:`px-3 py-2 text-gray-400 text-xs select-none whitespace-nowrap`,children:[(0,L.jsx)(`span`,{className:`cell-copy cursor-pointer`,onClick:()=>C(`${e.login}:${e.password}`),title:S(`tip.copyLoginPass`),children:w?`${e.login}:${T}`:`${e.login}:${e.password}`}),(0,L.jsx)(`button`,{onClick:e=>{e.stopPropagation(),g(!h)},className:`ml-1.5 text-gray-500 hover:text-gray-300 inline-flex align-middle`,title:S(w?`tip.show`:`tip.hide`),children:w?(0,L.jsx)(ge,{size:14}):(0,L.jsx)(_e,{size:14})})]}),t.email&&(0,L.jsx)(`td`,{className:`px-3 py-2 text-gray-400 text-xs`,children:e.email||`—`}),t.email_pass&&(0,L.jsxs)(`td`,{className:`px-3 py-2 text-gray-400 text-xs select-none whitespace-nowrap`,children:[(0,L.jsx)(`span`,{className:`cell-copy cursor-pointer`,onClick:()=>{e.email_password&&C(e.email_password)},title:e.email_password?S(`tip.copyEmailPass`):``,children:e.email_password?w?T:e.email_password:`—`}),e.email_password&&(0,L.jsx)(`button`,{onClick:e=>{e.stopPropagation(),g(!h)},className:`ml-1.5 text-gray-500 hover:text-gray-300 inline-flex align-middle`,title:S(w?`tip.show`:`tip.hide`),children:w?(0,L.jsx)(ge,{size:14}):(0,L.jsx)(_e,{size:14})})]}),t.email_login_pass&&(0,L.jsxs)(`td`,{className:`px-3 py-2 text-gray-400 text-xs select-none whitespace-nowrap`,children:[(0,L.jsx)(`span`,{className:`cell-copy cursor-pointer`,onClick:()=>{e.email&&e.email_password&&C(`${e.email}:${e.email_password}`)},title:e.email&&e.email_password?S(`tip.copyEmailLoginPass`):``,children:e.email&&e.email_password?w?`${e.email}:${T}`:`${e.email}:${e.email_password}`:`—`}),e.email&&e.email_password&&(0,L.jsx)(`button`,{onClick:e=>{e.stopPropagation(),g(!h)},className:`ml-1.5 text-gray-500 hover:text-gray-300 inline-flex align-middle`,title:S(w?`tip.show`:`tip.hide`),children:w?(0,L.jsx)(ge,{size:14}):(0,L.jsx)(_e,{size:14})})]}),t.phone&&(0,L.jsx)(`td`,{className:`px-3 py-2 text-gray-400 text-xs`,children:e.phone||`—`}),t.status&&(0,L.jsx)(`td`,{className:`px-3 py-2`,children:(0,L.jsx)(We,{status:e.status})}),t.ban&&(0,L.jsx)(`td`,{className:`px-3 py-2`,children:(0,L.jsx)(Ge,{status:e.ban_status})}),t.twofa&&(0,L.jsx)(`td`,{className:`px-3 py-2 whitespace-nowrap`,children:e.shared_secret?(0,L.jsxs)(`button`,{onClick:async()=>{try{let{code:t}=await D.generate2FA(e.shared_secret);await He(t),p(`success`,S(`twofa.copied`,{code:t}))}catch(e){p(`error`,S(`twofa.error`,{error:e instanceof Error?e.message:String(e)}))}},className:`text-accent hover:underline text-xs`,title:S(`tip.gen2fa`),children:[(0,L.jsx)(ve,{size:12,className:`inline mr-0.5`}),` `,S(`tip.code`)]}):(0,L.jsx)(`span`,{className:`text-gray-600 text-xs`,children:`—`})}),t.mafile&&(0,L.jsx)(`td`,{className:`px-3 py-2`,children:(0,L.jsx)(Ke,{hasMafile:!!e.mafile_path})}),t.proxy&&(0,L.jsx)(`td`,{className:`px-3 py-2 text-xs`,children:e.proxy?(0,L.jsx)(`span`,{className:`text-blue-400 cursor-pointer hover:underline`,onClick:()=>C(e.proxy),title:e.proxy,children:n||e.proxy}):`—`}),t.notes&&(0,L.jsx)(`td`,{className:`px-3 py-2 text-xs max-w-[180px]`,children:_?(0,L.jsx)(`input`,{autoFocus:!0,className:`w-full bg-dark-600 border border-dark-500 rounded px-1.5 py-0.5 text-xs text-gray-200 outline-none focus:border-accent`,value:b,onChange:e=>x(e.target.value),onBlur:async()=>{if(v(!1),b!==(e.notes||``))try{await D.updateAccount(e.id,{notes:b}),p(`success`,S(`toast.noteSaved`)),Re.getState().loadAccounts()}catch{p(`error`,S(`toast.noteSaveError`))}},onKeyDown:t=>{t.key===`Enter`&&t.target.blur(),t.key===`Escape`&&(x(e.notes||``),v(!1))}}):(0,L.jsx)(`span`,{className:`cursor-pointer text-gray-400 hover:text-gray-200 truncate block`,onClick:()=>{x(e.notes||``),v(!0)},title:e.notes||S(`tip.addNote`),children:e.notes||`—`})}),t.actions&&(0,L.jsx)(`td`,{className:`px-3 py-2 whitespace-nowrap`,children:(0,L.jsxs)(`div`,{className:`flex items-center gap-1.5`,children:[(0,L.jsx)(Qe,{account:e,onAction:c,onToggleAutoAccept:l}),(0,L.jsx)(`button`,{onClick:()=>o(e.id),className:`text-gray-400 hover:text-accent`,title:S(`tip.edit`),children:(0,L.jsx)(xe,{size:14})}),(0,L.jsx)(`button`,{onClick:()=>s(e.id),className:`text-gray-400 hover:text-red-400`,title:S(`tip.delete`),children:(0,L.jsx)(Se,{size:14})}),e.auto_accept?(0,L.jsx)(`span`,{title:S(`tip.autoAcceptActive`),children:(0,L.jsx)(me,{size:14,className:`text-accent`})}):null]})})]})}var tt=[`browser`,`profile`,`last_online`,`steam_id`,`login`,`password`,`login_pass`,`email`,`email_pass`,`email_login_pass`,`phone`,`status`,`ban`,`twofa`,`mafile`,`proxy`,`notes`,`actions`],nt={browser:`col.browser`,profile:`col.profile`,last_online:`col.lastOnline`,steam_id:`col.steamId`,login:`col.login`,password:`col.password`,login_pass:`col.loginPass`,email:`col.email`,email_pass:`col.emailPass`,email_login_pass:`col.emailLoginPass`,phone:`col.phone`,status:`col.status`,ban:`col.ban`,twofa:`col.twofa`,mafile:`col.mafile`,proxy:`col.proxy`,notes:`col.notes`,actions:`col.actions`};function rt({accounts:e,processingIds:t,accountResults:n,accountSteps:r,onEdit:i,onDelete:a,onAction:o,onToggleAutoAccept:s,onOpenBrowser:c}){let l=Re(e=>e.selectAll),u=Re(e=>e.clearSelection),d=Re(e=>e.selectedIds),f=j(e=>e.columnVisibility),p=F(),m=e.length>0&&d.size===e.length,h=tt.filter(e=>f[e]),[g,_]=(0,y.useState)(100),v=(0,y.useRef)(null),[b,x]=(0,y.useState)(null),[S,C]=(0,y.useState)(`asc`),w=e=>{b===e?S===`asc`?C(`desc`):(x(null),C(`asc`)):(x(e),C(`asc`))},T=(0,y.useMemo)(()=>{if(!b)return e;let t=S===`asc`?1:-1,n=Symbol();return[...e].sort((e,r)=>{let i=e=>{if(!e||e===`—`)return n;if(e===`online`)return-1;let t=e.match(/^(\d+)([mhd])$/i);if(!t)return n;let r=parseInt(t[1],10);return t[2]===`m`?r:t[2]===`h`?r*60:r*1440},a=i(e.last_online),o=i(r.last_online);return a===n&&o===n?0:a===n?1:o===n?-1:a===o?0:(a{_(100)},[e,b,S]);let E=(0,y.useMemo)(()=>T.slice(0,g),[T,g]);(0,y.useEffect)(()=>{let e=v.current;if(!e)return;let t=new IntersectionObserver(e=>{e[0].isIntersecting&&_(e=>Math.min(e+100,T.length))},{rootMargin:`400px`});return t.observe(e),()=>t.disconnect()},[T.length,g]);let D=(0,y.useMemo)(()=>{let t=new Map,n=[];for(let t of e)t.proxy&&!n.includes(t.proxy)&&n.push(t.proxy);let r=new Map;n.forEach((e,t)=>r.set(e,t+1));for(let n of e)if(n.proxy){let e=r.get(n.proxy);t.set(n.id,e?p(`proxy.label`,{num:e}):n.proxy)}return t},[e]);return e.length?(0,L.jsx)(`div`,{className:`overflow-x-auto`,children:(0,L.jsxs)(`table`,{className:`w-full text-sm`,children:[(0,L.jsx)(`thead`,{className:`sticky top-0 bg-dark-800 z-10`,children:(0,L.jsxs)(`tr`,{className:`text-left text-xs text-gray-500 border-b border-dark-600`,children:[(0,L.jsx)(`th`,{className:`px-3 py-2`,children:(0,L.jsxs)(`div`,{className:`flex items-center gap-1.5`,children:[(0,L.jsx)(`input`,{type:`checkbox`,checked:m,onChange:()=>m?u():l()}),d.size>0&&(0,L.jsx)(`span`,{className:`text-accent font-medium`,children:d.size})]})}),(0,L.jsx)(`th`,{className:`w-5`}),h.map(e=>e===`last_online`?(0,L.jsxs)(`th`,{className:`px-3 py-2 cursor-pointer select-none hover:text-gray-300`,onClick:()=>w(`last_online`),children:[p(nt[e]),b===`last_online`?S===`asc`?` ▲`:` ▼`:``]},e):(0,L.jsx)(`th`,{className:`px-3 py-2${e===`browser`?` text-center`:``}`,children:p(nt[e])},e))]})}),(0,L.jsxs)(`tbody`,{children:[E.map(e=>(0,L.jsx)(et,{account:e,visibleColumns:f,proxyLabel:D.get(e.id)??null,isProcessing:t.has(e.id),accountResult:n[String(e.id)],accountStep:r[String(e.id)],onEdit:i,onDelete:a,onAction:o,onToggleAutoAccept:s,onOpenBrowser:c},e.id)),g{m.current?.focus()},[]),(0,L.jsx)(`div`,{className:`confirm-overlay`,onMouseDown:n,children:(0,L.jsxs)(`div`,{className:`confirm-box max-w-md`,onMouseDown:e=>e.stopPropagation(),children:[(0,L.jsx)(`h3`,{className:`text-lg font-semibold mb-4`,children:r(`modal.editAccount`)}),(0,L.jsxs)(`div`,{className:`space-y-3`,children:[(0,L.jsx)(`input`,{ref:m,value:i,onChange:e=>a(e.target.value),placeholder:r(`ph.login`),className:`w-full bg-dark-700 border border-dark-600 rounded px-3 py-2 text-sm`}),(0,L.jsx)(`input`,{value:o,onChange:e=>s(e.target.value),placeholder:r(`ph.password`),className:`w-full bg-dark-700 border border-dark-600 rounded px-3 py-2 text-sm`}),(0,L.jsx)(`input`,{value:c,onChange:e=>l(e.target.value),placeholder:`Email`,className:`w-full bg-dark-700 border border-dark-600 rounded px-3 py-2 text-sm`}),(0,L.jsx)(`input`,{value:u,onChange:e=>d(e.target.value),placeholder:r(`ph.proxy`),className:`w-full bg-dark-700 border border-dark-600 rounded px-3 py-2 text-sm`}),(0,L.jsx)(`input`,{value:f,onChange:e=>p(e.target.value),placeholder:r(`ph.notes`),className:`w-full bg-dark-700 border border-dark-600 rounded px-3 py-2 text-sm`}),(0,L.jsx)(`button`,{onClick:()=>{t(e.id,{login:i,password:o,email:c||void 0,proxy:u||void 0,notes:f||void 0})},className:`btn-primary w-full`,children:r(`btn.save`)})]})]})})}function at({onClose:e}){let t=F(),[n,r]=(0,y.useState)(null),[i,a]=(0,y.useState)(``),[o,s]=(0,y.useState)(!1),c=(0,y.useRef)(null),l=j(e=>e.addToast),u=Re(e=>e.loadAccounts);return(0,L.jsx)(`div`,{className:`confirm-overlay`,onMouseDown:e,children:(0,L.jsxs)(`div`,{className:`confirm-box max-w-lg w-full`,onMouseDown:e=>e.stopPropagation(),children:[(0,L.jsx)(`h3`,{className:`text-lg font-semibold mb-4`,children:t(`modal.importAccounts`)}),(0,L.jsxs)(`div`,{className:`text-sm text-gray-400 mb-3 space-y-1`,children:[(0,L.jsxs)(`p`,{className:`font-medium text-gray-300`,children:[t(`import.formats`),` `,(0,L.jsxs)(`span`,{className:`text-gray-500 font-normal`,children:[`(`,t(`import.delimiter`),` `,(0,L.jsx)(`code`,{className:`text-accent`,children:`:`}),` `,t(`import.or`),` `,(0,L.jsx)(`code`,{className:`text-accent`,children:`|`}),`)`]})]}),(0,L.jsxs)(`p`,{className:`font-mono text-xs`,children:[`login:pass:`,`{`,`mafile`,`}`]}),(0,L.jsxs)(`p`,{className:`font-mono text-xs`,children:[`login:pass:email:email_pass:`,`{`,`mafile`,`}`]}),(0,L.jsx)(`p`,{className:`font-mono text-xs`,children:`login:pass:email:email_pass`})]}),(0,L.jsxs)(`div`,{onDragOver:e=>e.preventDefault(),onDrop:e=>{e.preventDefault();let t=e.dataTransfer.files[0];t&&r(t)},onClick:()=>c.current?.click(),className:`border-2 border-dashed border-dark-500 rounded-lg p-6 mb-3 text-center cursor-pointer transition-colors hover:border-accent`,children:[(0,L.jsx)(ce,{size:32,className:`mx-auto mb-2 text-gray-500`}),(0,L.jsx)(`p`,{className:`text-sm text-gray-400`,children:n?t(`import.selected`,{name:n.name}):t(`import.dragTxt`)})]}),(0,L.jsx)(`input`,{ref:c,type:`file`,accept:`.txt`,className:`hidden`,onChange:e=>{let t=e.target.files?.[0];t&&r(t)}}),(0,L.jsx)(`button`,{onClick:async()=>{if(n){s(!0);try{let e=await D.importAccounts(n);a(t(`toast.importResult`,{imported:e.imported,skipped:e.skipped})),l(`success`,`${e.imported} ok, ${e.skipped} skip`),await u()}catch(e){a(e instanceof Error?e.message:t(`misc.errorStr`))}finally{s(!1)}}},disabled:!n||o,className:`btn-primary w-full`,children:t(o?`import.uploading`:`import.importBtn`)}),i&&(0,L.jsx)(`p`,{className:`mt-3 text-sm text-gray-300`,children:i})]})})}function ot({onSave:e,onClose:t}){let n=F(),[r,i]=(0,y.useState)(``),[a,o]=(0,y.useState)(``),[s,c]=(0,y.useState)(``),[l,u]=(0,y.useState)(``);return(0,L.jsx)(`div`,{className:`confirm-overlay`,onMouseDown:t,children:(0,L.jsxs)(`div`,{className:`confirm-box max-w-md`,onMouseDown:e=>e.stopPropagation(),children:[(0,L.jsx)(`h3`,{className:`text-lg font-semibold mb-4`,children:n(`modal.addAccount`)}),(0,L.jsxs)(`div`,{className:`space-y-3`,children:[(0,L.jsx)(`input`,{autoFocus:!0,value:r,onChange:e=>i(e.target.value),placeholder:n(`ph.login`),className:`w-full bg-dark-700 border border-dark-600 rounded px-3 py-2 text-sm`}),(0,L.jsx)(`input`,{value:a,onChange:e=>o(e.target.value),placeholder:n(`ph.password`),className:`w-full bg-dark-700 border border-dark-600 rounded px-3 py-2 text-sm`}),(0,L.jsx)(`input`,{value:s,onChange:e=>c(e.target.value),placeholder:`Email`,className:`w-full bg-dark-700 border border-dark-600 rounded px-3 py-2 text-sm`}),(0,L.jsx)(`input`,{value:l,onChange:e=>u(e.target.value),placeholder:n(`ph.proxy`),className:`w-full bg-dark-700 border border-dark-600 rounded px-3 py-2 text-sm`}),(0,L.jsx)(`button`,{onClick:()=>{!r||!a||e({login:r,password:a,email:s||void 0,proxy:l||void 0})},className:`btn-primary w-full`,children:n(`btn.add`)})]})]})})}function st({message:e,onConfirm:t,onCancel:n}){let r=F();return(0,L.jsx)(`div`,{className:`confirm-overlay`,onMouseDown:n,children:(0,L.jsxs)(`div`,{className:`confirm-box`,onMouseDown:e=>e.stopPropagation(),children:[(0,L.jsx)(`p`,{className:`text-sm text-gray-300 mb-4`,children:e}),(0,L.jsxs)(`div`,{className:`flex gap-2`,children:[(0,L.jsx)(`button`,{onClick:t,className:`btn-primary flex-1`,children:r(`confirm.yes`)}),(0,L.jsx)(`button`,{onClick:n,className:`btn-secondary flex-1`,children:r(`btn.cancel`)})]})]})})}var ct={browser:`col.browser`,profile:`col.profile`,last_online:`col.lastOnline`,steam_id:`col.steamId`,login:`col.login`,password:`col.password`,login_pass:`col.loginPass`,email:`col.email`,email_pass:`col.emailPass`,email_login_pass:`col.emailLoginPass`,phone:`col.phone`,status:`col.status`,ban:`col.ban`,twofa:`col.twofa`,mafile:`col.mafile`,proxy:`col.proxy`,notes:`col.notes`,actions:`col.actions`};function lt(){let[e,t]=(0,y.useState)(!1),n=j(e=>e.columnVisibility),r=j(e=>e.toggleColumn),i=(0,y.useRef)(null),a=F();return(0,y.useEffect)(()=>{let e=e=>{i.current&&!i.current.contains(e.target)&&t(!1)};return document.addEventListener(`mousedown`,e),()=>document.removeEventListener(`mousedown`,e)},[]),(0,L.jsxs)(`div`,{className:`relative`,ref:i,children:[(0,L.jsx)(`button`,{onClick:()=>t(!e),className:`btn-secondary text-sm px-2 py-2`,title:a(`tip.columnSettings`),children:(0,L.jsx)(be,{size:14})}),e&&(0,L.jsx)(`div`,{className:`absolute right-0 top-full mt-1 bg-dark-700 border border-dark-600 rounded-lg shadow-xl z-50 p-2 min-w-[160px]`,children:Object.keys(ct).map(e=>(0,L.jsxs)(`label`,{className:`flex items-center gap-2 px-2 py-1 hover:bg-dark-600 rounded cursor-pointer text-sm`,children:[(0,L.jsx)(`input`,{type:`checkbox`,checked:n[e],onChange:()=>r(e)}),a(ct[e])]},e))})]})}function ut({onClose:e}){let t=F(),[n,r]=(0,y.useState)(null),[i,a]=(0,y.useState)(``),[o,s]=(0,y.useState)(!1),c=(0,y.useRef)(null),l=j(e=>e.addToast),u=Be(e=>e.loadAccounts);return(0,L.jsx)(`div`,{className:`confirm-overlay`,onMouseDown:e,children:(0,L.jsxs)(`div`,{className:`confirm-box max-w-lg w-full`,onMouseDown:e=>e.stopPropagation(),children:[(0,L.jsx)(`h3`,{className:`text-lg font-semibold mb-4`,children:t(`modal.importAccounts`)}),(0,L.jsxs)(`div`,{className:`text-sm text-gray-400 mb-3 space-y-1`,children:[(0,L.jsx)(`p`,{className:`font-medium text-gray-300`,children:t(`import.formatsColon`)}),(0,L.jsx)(`p`,{className:`font-mono text-xs`,children:`login:pass`}),(0,L.jsx)(`p`,{className:`font-mono text-xs`,children:`login|pass`}),(0,L.jsx)(`p`,{className:`font-mono text-xs`,children:`CSV: login,password,steam_id,ban,prime,trophy,behavior,license`}),(0,L.jsx)(`p`,{className:`text-xs text-yellow-500/80 mt-1`,children:`⚠ Mafile-файлы и JSON не принимаются. Только log:pass / log|pass.`})]}),(0,L.jsxs)(`div`,{onDragOver:e=>e.preventDefault(),onDrop:e=>{e.preventDefault();let t=e.dataTransfer.files[0];t&&r(t)},onClick:()=>c.current?.click(),className:`border-2 border-dashed border-dark-500 rounded-lg p-6 mb-3 text-center cursor-pointer transition-colors hover:border-accent`,children:[(0,L.jsx)(ce,{size:32,className:`mx-auto mb-2 text-gray-500`}),(0,L.jsx)(`p`,{className:`text-sm text-gray-400`,children:n?t(`import.selected`,{name:n.name}):t(`import.dragTxtCsv`)})]}),(0,L.jsx)(`input`,{ref:c,type:`file`,accept:`.txt,.csv`,className:`hidden`,onChange:e=>{let t=e.target.files?.[0];t&&r(t)}}),(0,L.jsx)(`button`,{onClick:async()=>{if(n){s(!0);try{let e=(await n.text()).split(` +`).map(e=>e.trim()).filter(Boolean);if(!e.length){a(t(`toast.fileEmpty`)),s(!1);return}let r=await D.importLogpass(e);a(t(`toast.importResult`,{imported:r.imported,skipped:r.skipped})),l(`success`,`${r.imported} ok, ${r.skipped} skip`),await u()}catch(e){a(e instanceof Error?e.message:t(`misc.errorStr`))}finally{s(!1)}}},disabled:!n||o,className:`btn-primary w-full`,children:t(o?`import.uploading`:`import.importBtn`)}),i&&(0,L.jsx)(`p`,{className:`mt-3 text-sm text-gray-300`,children:i})]})})}function dt({onClose:e}){let t=F(),[n,r]=(0,y.useState)(``),[i,a]=(0,y.useState)(``),[o,s]=(0,y.useState)(``),c=Be(e=>e.loadAccounts);return(0,L.jsx)(`div`,{className:`confirm-overlay`,onMouseDown:e,children:(0,L.jsxs)(`div`,{className:`confirm-box max-w-md`,onMouseDown:e=>e.stopPropagation(),children:[(0,L.jsx)(`h3`,{className:`text-lg font-semibold mb-4`,children:t(`modal.addAccount`)}),(0,L.jsxs)(`div`,{className:`space-y-3`,children:[(0,L.jsx)(`input`,{autoFocus:!0,value:n,onChange:e=>r(e.target.value),placeholder:t(`ph.login`),className:`w-full bg-dark-700 border border-dark-600 rounded px-3 py-2 text-sm`}),(0,L.jsx)(`input`,{value:i,onChange:e=>a(e.target.value),placeholder:t(`ph.password`),className:`w-full bg-dark-700 border border-dark-600 rounded px-3 py-2 text-sm`}),(0,L.jsx)(`input`,{value:o,onChange:e=>s(e.target.value),placeholder:t(`ph.proxyOptional`),className:`w-full bg-dark-700 border border-dark-600 rounded px-3 py-2 text-sm`}),(0,L.jsx)(`button`,{onClick:async()=>{if(!n||!i)return;let t={login:n,password:i,proxy:o||void 0};await D.createLogpassAccount(t),await c(),e()},className:`btn-primary w-full`,children:t(`btn.add`)})]})]})})}function ft({account:e,onClose:t}){let n=F(),[r,i]=(0,y.useState)(e.login),[a,o]=(0,y.useState)(e.password),[s,c]=(0,y.useState)(e.proxy??``),[l,u]=(0,y.useState)(e.notes??``),d=Be(e=>e.loadAccounts);return(0,L.jsx)(`div`,{className:`confirm-overlay`,onMouseDown:t,children:(0,L.jsxs)(`div`,{className:`confirm-box max-w-md`,onMouseDown:e=>e.stopPropagation(),children:[(0,L.jsx)(`h3`,{className:`text-lg font-semibold mb-4`,children:n(`modal.editAccount`)}),(0,L.jsxs)(`div`,{className:`space-y-3`,children:[(0,L.jsx)(`input`,{autoFocus:!0,value:r,onChange:e=>i(e.target.value),placeholder:n(`ph.login`),className:`w-full bg-dark-700 border border-dark-600 rounded px-3 py-2 text-sm`}),(0,L.jsx)(`input`,{value:a,onChange:e=>o(e.target.value),placeholder:n(`ph.password`),className:`w-full bg-dark-700 border border-dark-600 rounded px-3 py-2 text-sm`}),(0,L.jsx)(`input`,{value:s,onChange:e=>c(e.target.value),placeholder:n(`ph.proxyOptional`),className:`w-full bg-dark-700 border border-dark-600 rounded px-3 py-2 text-sm`}),(0,L.jsx)(`input`,{value:l,onChange:e=>u(e.target.value),placeholder:n(`ph.notes`),className:`w-full bg-dark-700 border border-dark-600 rounded px-3 py-2 text-sm`}),(0,L.jsxs)(`div`,{className:`flex gap-2`,children:[(0,L.jsx)(`button`,{onClick:async()=>{!r||!a||(await D.updateLogpassAccount(e.id,{login:r,password:a,proxy:s||void 0,notes:l||void 0}),await d(),t())},className:`btn-primary flex-1`,children:n(`btn.save`)}),(0,L.jsx)(`button`,{onClick:t,className:`btn-secondary flex-1`,children:n(`btn.cancel`)})]})]})]})})}var pt={browser:`col.browser`,profile:`col.profile`,last_online:`col.lastOnline`,steam_id:`col.steamId`,login:`col.login`,password:`col.password`,login_pass:`col.loginPass`,status:`col.status`,ban:`col.ban`,prime:`col.prime`,trophy:`col.trophy`,behavior:`col.behavior`,license:`col.license`,proxy:`col.proxy`,notes:`col.notes`,actions:`col.actions`};function mt(){let[e,t]=(0,y.useState)(!1),n=j(e=>e.logpassColumnVisibility),r=j(e=>e.toggleLogpassColumn),i=(0,y.useRef)(null),a=F();return(0,y.useEffect)(()=>{let e=e=>{i.current&&!i.current.contains(e.target)&&t(!1)};return document.addEventListener(`mousedown`,e),()=>document.removeEventListener(`mousedown`,e)},[]),(0,L.jsxs)(`div`,{className:`relative`,ref:i,children:[(0,L.jsx)(`button`,{onClick:()=>t(!e),className:`btn-secondary text-sm px-2 py-2`,title:a(`tip.columnSettings`),children:(0,L.jsx)(be,{size:14})}),e&&(0,L.jsx)(`div`,{className:`absolute right-0 top-full mt-1 bg-dark-700 border border-dark-600 rounded-lg shadow-xl z-50 p-2 min-w-[160px]`,children:Object.keys(pt).map(e=>(0,L.jsxs)(`label`,{className:`flex items-center gap-2 px-2 py-1 hover:bg-dark-600 rounded cursor-pointer text-sm`,children:[(0,L.jsx)(`input`,{type:`checkbox`,checked:n[e],onChange:()=>r(e)}),a(pt[e])]},e))})]})}function ht(e){if(!e)return null;if(typeof e==`string`)try{return JSON.parse(e)}catch{return null}return e}function gt(){let e=Be(e=>e.accounts),t=Be(e=>e.selectedIds),n=Be(e=>e.loadAccounts),r=Be(e=>e.selectAll),i=Be(e=>e.clearSelection),a=j(e=>e.addToast),o=j(e=>e.logpassColumnVisibility),s=F(),[c,l]=(0,y.useState)(``),[u,d]=(0,y.useState)(!1),[f,p]=(0,y.useState)(!1),[m,h]=(0,y.useState)(null),[g,_]=(0,y.useState)({open:!1,message:``,onConfirm:()=>{}}),[v,b]=(0,y.useState)(new Set),[x,S]=(0,y.useState)({}),[C,w]=(0,y.useState)({}),[T,E]=(0,y.useState)(null),[O,ee]=(0,y.useState)(100),[k,A]=(0,y.useState)(null),[M,te]=(0,y.useState)(`asc`),ne=e=>{k===e?M===`asc`?te(`desc`):(A(null),te(`asc`)):(A(e),te(`asc`))},N=(0,y.useRef)(null);(0,y.useEffect)(()=>{n()},[n]);let P=(e,t)=>_({open:!0,message:e,onConfirm:t}),re=(0,y.useCallback)(e=>{let t=new EventSource(`/api/tasks/${e}/stream`);t.onmessage=e=>{try{let r=JSON.parse(e.data);if(E(r),r.account_results){let e=ht(r.account_results);e&&S(e)}r.account_steps&&w(r.account_steps),r.status!==`running`&&(t.close(),b(new Set),E(null),n())}catch{}},t.onerror=()=>t.close()},[n]),ie=(0,y.useCallback)(async()=>{let e=[...t];if(e.length){i(),b(new Set(e)),S({}),w({});try{let{task_id:t}=await D.validateLogpass(e);E({id:t,type:`logpass_validate`,status:`running`,progress:0,total:e.length,result:null,error:null,created_at:``,updated_at:``}),re(t)}catch(e){b(new Set),a(`error`,String(e))}}},[t,i,re,a]),ae=(0,y.useCallback)(async e=>{b(new Set([e])),S(t=>{let n={...t};return delete n[String(e)],n}),w(t=>{let n={...t};return delete n[String(e)],n});try{let{task_id:t}=await D.validateLogpass([e]);E({id:t,type:`logpass_validate`,status:`running`,progress:0,total:1,result:null,error:null,created_at:``,updated_at:``}),re(t)}catch(e){b(new Set),a(`error`,String(e))}},[re,a]),I=(0,y.useCallback)(async()=>{let e=[...t];if(e.length){i(),b(new Set(e)),S({}),w({});try{let{task_id:t}=await D.fullParseLogpass(e);E({id:t,type:`logpass_full_parse`,status:`running`,progress:0,total:e.length,result:null,error:null,created_at:``,updated_at:``}),re(t)}catch(e){b(new Set),a(`error`,String(e))}}},[t,i,re,a]),oe=()=>{let e=[...t];e.length&&P(s(`confirm.deleteLogpassSelected`,{count:e.length}),async()=>{await D.deleteLogpassBulk(e),i(),n()})},R=()=>{P(s(`confirm.deleteLogpassAll`,{count:e.length}),async()=>{await D.deleteLogpassBulk(e.map(e=>e.id)),i(),n()})},ce=()=>{P(s(`confirm.assignProxies`),async()=>{try{a(`success`,s(`toast.proxiesAssignedShort`,{count:(await D.assignLogpassProxies()).assigned})),n()}catch(e){a(`error`,e instanceof Error?e.message:String(e))}})},ue=()=>{P(s(`confirm.reassignProxiesAll`),async()=>{try{a(`success`,s(`toast.proxiesReassignedShort`,{count:(await D.reassignLogpassProxies()).assigned})),n()}catch(e){a(`error`,e instanceof Error?e.message:String(e))}})},de=()=>{P(s(`confirm.clearProxies`),async()=>{try{a(`success`,s(`toast.proxiesClearedShort`,{count:(await D.clearLogpassProxies()).cleared})),n()}catch(e){a(`error`,e instanceof Error?e.message:String(e))}})},fe=e.length>0&&t.size===e.length,ge=(0,y.useMemo)(()=>{if(!c.trim())return e;let t=c.trim(),n=t.match(/^lvl\s*(>=|<=|>|<|=)\s*(\d+)$/i);if(n){let t=n[1],r=parseInt(n[2],10);return e.filter(e=>{let n=e.steam_level;return n==null?!1:t===`>`?n>r:t===`>=`?n>=r:t===`<`?ne.login.toLowerCase().includes(r)||(e.steam_id??``).toLowerCase().includes(r)||(e.nickname??``).toLowerCase().includes(r)||(e.notes??``).toLowerCase().includes(r)||(e.license??``).toLowerCase().includes(r))},[e,c]),_e=(0,y.useMemo)(()=>{if(!k)return ge;let e=M===`asc`?1:-1,t=Symbol();return[...ge].sort((n,r)=>{let i=t,a=t;if(k===`last_online`){let e=e=>{if(!e||e===`—`)return t;if(e===`online`)return-1;let n=e.match(/^(\d+)([mhd])$/i);if(!n)return t;let r=parseInt(n[1],10);return n[2]===`m`?r:n[2]===`h`?r*60:r*1440};i=e(n.last_online),a=e(r.last_online)}else k===`prime`?(i=n.prime===`Enabled`?0:n.prime===`Disabled`?1:t,a=r.prime===`Enabled`?0:r.prime===`Disabled`?1:t):k===`trophy`?(i=n.trophy!=null&&n.trophy!==`—`?parseInt(n.trophy,10)||0:t,a=r.trophy!=null&&r.trophy!==`—`?parseInt(r.trophy,10)||0:t):k===`behavior`&&(i=n.behavior!=null&&n.behavior!==`—`?parseInt(n.behavior,10)||0:t,a=r.behavior!=null&&r.behavior!==`—`?parseInt(r.behavior,10)||0:t);return i===t&&a===t?0:i===t?1:a===t?-1:i===a?0:(i{ee(100)},[c,k,M]);let ve=(0,y.useMemo)(()=>_e.slice(0,O),[_e,O]);(0,y.useEffect)(()=>{let e=N.current;if(!e)return;let t=new IntersectionObserver(e=>{e[0].isIntersecting&&ee(e=>Math.min(e+100,_e.length))},{rootMargin:`400px`});return t.observe(e),()=>t.disconnect()},[_e.length,O]);let ye=(0,y.useMemo)(()=>{let t=new Map,n=[];for(let t of e)t.proxy&&!n.includes(t.proxy)&&n.push(t.proxy);let r=new Map;n.forEach((e,t)=>r.set(e,t+1));for(let n of e)n.proxy&&t.set(n.id,s(`proxy.label`,{num:r.get(n.proxy)??0}));return t},[e]),be=async e=>{try{let t=await D.openLogpassBrowser(e);t.status===`revalidating`?(a(`warn`,s(`toast.cookiesExpiredRevalidating`)),t.task_id&&(b(new Set([e])),E({id:t.task_id,type:`logpass_validate`,status:`running`,progress:0,total:1,result:null,error:null,created_at:``,updated_at:``}),re(t.task_id))):a(`success`,s(`toast.browserOpening`))}catch(e){a(`error`,e instanceof Error?e.message:String(e))}};return(0,L.jsxs)(`div`,{className:`flex flex-col gap-4 h-full min-h-0`,children:[(0,L.jsxs)(`div`,{className:`bg-dark-800 border border-dark-600 rounded-lg px-3 py-2 flex flex-wrap items-center gap-2 shrink-0`,children:[(0,L.jsxs)(`button`,{onClick:()=>d(!0),className:`btn-primary`,children:[(0,L.jsx)(se,{size:14,className:`inline mr-1`}),s(`btn.import`)]}),(0,L.jsxs)(`button`,{onClick:()=>p(!0),className:`btn-secondary`,children:[(0,L.jsx)(le,{size:14,className:`inline mr-1`}),s(`btn.add`)]}),T&&(0,L.jsxs)(L.Fragment,{children:[(0,L.jsx)(`div`,{className:`h-5 border-l border-dark-500`}),(0,L.jsxs)(`div`,{className:`flex items-center gap-2 min-w-0`,children:[(0,L.jsx)(`span`,{className:`text-xs text-gray-400 whitespace-nowrap`,children:s(`task.validate`)}),(0,L.jsx)(`div`,{className:`w-28 bg-dark-600 rounded-full h-2.5 shrink-0`,children:(0,L.jsx)(`div`,{className:`h-2.5 rounded-full transition-all duration-300 ${T.status===`completed`?`bg-green-500`:T.status===`failed`?`bg-red-500`:`bg-accent`}`,style:{width:`${T.total>1?T.total>0?Math.round(T.progress/T.total*100):0:T.total_steps?Math.round((T.step??0)/T.total_steps*100):0}%`}})}),(0,L.jsxs)(`span`,{className:`text-xs text-gray-500 whitespace-nowrap`,children:[T.total>1?`${T.progress}/${T.total}${T.active_count?` (${T.active_count})`:``}`:T.step_label||`${T.progress}/${T.total}`,T.total>1?` (${T.total>0?Math.round(T.progress/T.total*100):0}%)`:T.total_steps?` (${T.step}/${T.total_steps})`:``]}),T.status===`completed`&&(0,L.jsx)(ke,{size:14,className:`text-green-400`}),T.status===`failed`&&(0,L.jsx)(`span`,{title:T.error||``,children:(0,L.jsx)(Oe,{size:14,className:`text-red-400`})})]})]}),(0,L.jsxs)(`div`,{className:`ml-auto flex items-center gap-2`,children:[(0,L.jsx)(mt,{}),(0,L.jsx)(`div`,{className:`h-5 border-l border-dark-500`}),(0,L.jsx)(`button`,{onClick:oe,className:`btn-danger-outline text-xs`,children:s(`btn.deleteSelected`)}),(0,L.jsx)(`button`,{onClick:R,className:`btn-danger text-xs`,children:s(`btn.deleteAll`)})]})]}),(0,L.jsxs)(`div`,{className:`bg-dark-800 border border-dark-600 rounded-lg overflow-hidden flex-1 min-h-0 flex flex-col`,children:[(0,L.jsxs)(`div`,{className:`px-3 py-2 flex items-center gap-2 border-b border-dark-600 shrink-0 bg-dark-800`,children:[(0,L.jsxs)(`button`,{onClick:ie,disabled:t.size===0||v.size>0,className:`btn-accent text-sm disabled:opacity-40`,children:[(0,L.jsx)(De,{size:12,className:`inline mr-1`}),s(`btn.validate`)]}),(0,L.jsxs)(`button`,{onClick:I,disabled:t.size===0||v.size>0,className:`btn-secondary text-sm disabled:opacity-40`,children:[(0,L.jsx)(De,{size:12,className:`inline mr-1`}),s(`btn.fullParse`)]}),T&&T.status===`running`&&(0,L.jsxs)(`button`,{onClick:async()=>{try{await D.cancelTask(T.id)}catch{}},className:`btn-danger text-sm`,children:[(0,L.jsx)(Oe,{size:12,className:`inline mr-1`}),s(`btn.cancel`)]}),(0,L.jsx)(`div`,{className:`h-5 border-l border-dark-500`}),(0,L.jsxs)(`button`,{onClick:ce,className:`btn-secondary text-sm`,children:[(0,L.jsx)(pe,{size:14,className:`inline mr-1`}),s(`btn.assignProxies`)]}),(0,L.jsxs)(`button`,{onClick:ue,className:`btn-secondary text-sm`,children:[(0,L.jsx)(me,{size:14,className:`inline mr-1`}),s(`btn.reassign`)]}),(0,L.jsxs)(`button`,{onClick:de,className:`btn-danger-outline text-sm`,children:[(0,L.jsx)(he,{size:14,className:`inline mr-1`}),s(`btn.clearProxies`)]}),(0,L.jsx)(`div`,{className:`ml-auto`,children:(0,L.jsx)(`input`,{type:`text`,value:c,onChange:e=>l(e.target.value),placeholder:s(`ph.searchLogpass`),className:`bg-dark-700 border border-dark-600 rounded px-2 py-1.5 text-xs w-80 placeholder:text-gray-500 outline-none focus:border-accent`})})]}),(0,L.jsx)(`div`,{className:`overflow-auto flex-1 min-h-0`,children:(0,L.jsxs)(`table`,{className:`w-full text-sm`,children:[(0,L.jsx)(`thead`,{className:`sticky top-0 bg-dark-800 z-10`,children:(0,L.jsxs)(`tr`,{className:`text-left text-xs text-gray-500 border-b border-dark-600`,children:[(0,L.jsx)(`th`,{className:`px-3 py-2`,children:(0,L.jsxs)(`div`,{className:`flex items-center gap-1.5`,children:[(0,L.jsx)(`input`,{type:`checkbox`,checked:fe,onChange:()=>fe?i():r()}),t.size>0&&(0,L.jsx)(`span`,{className:`text-accent font-medium`,children:t.size})]})}),(0,L.jsx)(`th`,{className:`w-5`}),o.browser&&(0,L.jsx)(`th`,{className:`px-3 py-2`,children:s(`col.browser`)}),o.profile&&(0,L.jsx)(`th`,{className:`px-3 py-2`,children:s(`col.profile`)}),o.last_online&&(0,L.jsxs)(`th`,{className:`px-3 py-2 cursor-pointer select-none hover:text-gray-300`,onClick:()=>ne(`last_online`),children:[s(`col.lastOnline`),k===`last_online`?M===`asc`?` ▲`:` ▼`:``]}),o.steam_id&&(0,L.jsx)(`th`,{className:`px-3 py-2`,children:`Steam ID`}),o.login&&(0,L.jsx)(`th`,{className:`px-3 py-2`,children:s(`col.login`)}),o.password&&(0,L.jsx)(`th`,{className:`px-3 py-2`,children:s(`col.password`)}),o.login_pass&&(0,L.jsx)(`th`,{className:`px-3 py-2`,children:`Login:Pass`}),o.status&&(0,L.jsx)(`th`,{className:`px-3 py-2`,children:s(`col.status`)}),o.ban&&(0,L.jsx)(`th`,{className:`px-3 py-2`,children:s(`col.ban`)}),o.prime&&(0,L.jsxs)(`th`,{className:`px-3 py-2 cursor-pointer select-none hover:text-gray-300`,onClick:()=>ne(`prime`),children:[`Prime`,k===`prime`?M===`asc`?` ▲`:` ▼`:``]}),o.trophy&&(0,L.jsxs)(`th`,{className:`px-3 py-2 cursor-pointer select-none hover:text-gray-300`,onClick:()=>ne(`trophy`),children:[`Trophy`,k===`trophy`?M===`asc`?` ▲`:` ▼`:``]}),o.behavior&&(0,L.jsxs)(`th`,{className:`px-3 py-2 cursor-pointer select-none hover:text-gray-300`,onClick:()=>ne(`behavior`),children:[`Behavior`,k===`behavior`?M===`asc`?` ▲`:` ▼`:``]}),o.license&&(0,L.jsx)(`th`,{className:`px-3 py-2`,children:s(`col.license`)}),o.proxy&&(0,L.jsx)(`th`,{className:`px-3 py-2`,children:s(`col.proxy`)}),o.notes&&(0,L.jsx)(`th`,{className:`px-3 py-2`,children:s(`col.notes`)}),o.actions&&(0,L.jsx)(`th`,{className:`px-3 py-2`,children:s(`col.actions`)})]})}),(0,L.jsx)(`tbody`,{children:ge.length===0?(0,L.jsx)(`tr`,{children:(0,L.jsx)(`td`,{colSpan:16,className:`text-center py-12 text-gray-500`,children:s(`empty.logpass`)})}):(0,L.jsxs)(L.Fragment,{children:[ve.map(e=>(0,L.jsx)(_t,{account:e,cols:o,isProcessing:v.has(e.id),accountResult:x[String(e.id)],accountStep:C[String(e.id)],proxyLabel:ye.get(e.id)??null,onDelete:t=>P(s(`confirm.deleteLogpass`,{name:e.login}),async()=>{await D.deleteLogpassAccount(t),n()}),onOpenBrowser:be,onValidate:ae,onEdit:e=>h(e)},e.id)),O{let t=e.find(e=>e.id===m);return t?(0,L.jsx)(ft,{account:t,onClose:()=>h(null)}):null})(),u&&(0,L.jsx)(ut,{onClose:()=>d(!1)}),f&&(0,L.jsx)(dt,{onClose:()=>p(!1)}),g.open&&(0,L.jsx)(st,{message:g.message,onConfirm:()=>{g.onConfirm(),_(e=>({...e,open:!1}))},onCancel:()=>_(e=>({...e,open:!1}))})]})}function _t({account:e,cols:t,isProcessing:n,accountResult:r,accountStep:i,proxyLabel:a,onDelete:o,onOpenBrowser:s,onValidate:c,onEdit:l}){let u=Be(e=>e.selectedIds),d=Be(e=>e.toggleSelect),f=j(e=>e.addToast),p=j(e=>e.hidePasswords),m=Be(e=>e.loadAccounts),[h,g]=(0,y.useState)(!1),[_,v]=(0,y.useState)(!1),[b,x]=(0,y.useState)(e.notes||``),[S,C]=(0,y.useState)(!1),w=F(),T=p&&!h,E=`••••••••`,O=async e=>{let t=await He(e);f(t?`success`:`error`,w(t?`toast.copied`:`toast.copyFailed`))};return(0,L.jsxs)(`tr`,{className:`hover:bg-dark-700/50 transition`,children:[(0,L.jsx)(`td`,{className:`px-3 py-2`,children:(0,L.jsx)(`input`,{type:`checkbox`,checked:u.has(e.id),onChange:()=>d(e.id)})}),(0,L.jsx)(`td`,{className:`px-1 py-2 w-5`,children:n?(0,L.jsxs)(`div`,{className:`flex items-center gap-1`,children:[(0,L.jsx)(`div`,{className:`w-4 h-4 border-2 border-accent border-t-transparent rounded-full animate-spin shrink-0`}),i&&(0,L.jsxs)(`span`,{className:`text-[10px] text-gray-500 whitespace-nowrap`,children:[`[`,i.step,`/`,i.total,`]`]})]}):r?.status===`ok`?(0,L.jsx)(ue,{size:16,className:`text-gray-400`}):r?.status===`error`?(0,L.jsx)(`span`,{title:r.error,children:(0,L.jsx)(de,{size:16,className:`text-red-400`})}):null}),t.browser&&(0,L.jsx)(`td`,{className:`px-3 py-2 whitespace-nowrap text-center`,children:e.has_cookies?(0,L.jsx)(`button`,{onClick:()=>s(e.id),className:`px-2 py-0.5 text-xs font-medium rounded bg-blue-600/20 text-blue-400 border border-blue-500/30 hover:bg-blue-600/40 hover:text-blue-300 active:scale-95 transition`,title:w(`tip.openBrowser`),children:w(`col.browser`)}):(0,L.jsx)(`span`,{className:`text-gray-600 text-xs`,children:`—`})}),t.profile&&(0,L.jsx)(`td`,{className:`px-3 py-2 whitespace-nowrap`,children:(0,L.jsxs)(`div`,{className:`flex items-center gap-1.5`,children:[e.avatar_url&&(0,L.jsx)(`img`,{src:e.avatar_url,className:`avatar-sm`,alt:``}),(0,L.jsx)(`span`,{className:`text-xs`,children:e.nickname||`—`}),e.steam_level!=null&&(0,L.jsx)($e,{level:e.steam_level})]})}),t.last_online&&(0,L.jsx)(`td`,{className:`px-3 py-2 text-xs text-gray-400 whitespace-nowrap`,children:e.last_online||`—`}),t.steam_id&&(0,L.jsx)(`td`,{className:`px-3 py-2 text-xs`,children:e.steam_id?(0,L.jsx)(`a`,{href:`https://steamcommunity.com/profiles/${encodeURIComponent(e.steam_id)}/`,target:`_blank`,rel:`noopener noreferrer`,className:`text-blue-400 hover:text-blue-300 hover:underline`,children:e.steam_id}):`—`}),t.login&&(0,L.jsx)(`td`,{className:`px-3 py-2 font-medium`,children:e.login}),t.password&&(0,L.jsxs)(`td`,{className:`px-3 py-2 text-gray-400 select-none whitespace-nowrap`,children:[(0,L.jsx)(`span`,{className:`cursor-pointer`,onClick:()=>O(e.password),title:w(`tip.copyPassword`),children:T?E:e.password}),(0,L.jsx)(`button`,{onClick:e=>{e.stopPropagation(),g(!h)},className:`ml-1.5 text-gray-500 hover:text-gray-300 inline-flex align-middle`,title:w(T?`tip.show`:`tip.hide`),children:T?(0,L.jsx)(ge,{size:14}):(0,L.jsx)(_e,{size:14})})]}),t.login_pass&&(0,L.jsxs)(`td`,{className:`px-3 py-2 text-gray-400 text-xs select-none whitespace-nowrap`,children:[(0,L.jsx)(`span`,{className:`cursor-pointer`,onClick:()=>O(`${e.login}:${e.password}`),title:w(`tip.copyLoginPass`),children:T?`${e.login}:${E}`:`${e.login}:${e.password}`}),(0,L.jsx)(`button`,{onClick:e=>{e.stopPropagation(),g(!h)},className:`ml-1.5 text-gray-500 hover:text-gray-300 inline-flex align-middle`,title:w(T?`tip.show`:`tip.hide`),children:T?(0,L.jsx)(ge,{size:14}):(0,L.jsx)(_e,{size:14})})]}),t.status&&(0,L.jsx)(`td`,{className:`px-3 py-2`,children:(0,L.jsx)(We,{status:e.status})}),t.ban&&(0,L.jsx)(`td`,{className:`px-3 py-2`,children:(0,L.jsx)(Ge,{status:e.ban_status})}),t.prime&&(0,L.jsx)(`td`,{className:`px-3 py-2 text-xs text-gray-400`,children:e.prime??`—`}),t.trophy&&(0,L.jsx)(`td`,{className:`px-3 py-2 text-xs text-gray-400`,children:e.trophy??`—`}),t.behavior&&(0,L.jsx)(`td`,{className:`px-3 py-2 text-xs text-gray-400`,children:e.behavior??`—`}),t.license&&(0,L.jsx)(`td`,{className:`px-3 py-2 text-xs text-gray-400 cursor-pointer select-none ${S?`max-w-none whitespace-normal break-words`:`truncate max-w-[200px] whitespace-nowrap`}`,title:S?void 0:e.license??``,onClick:()=>C(e=>!e),children:e.license??`—`}),t.proxy&&(0,L.jsx)(`td`,{className:`px-3 py-2 text-xs`,children:e.proxy?(0,L.jsx)(`span`,{className:`text-blue-400 cursor-pointer hover:underline`,onClick:()=>O(e.proxy),title:e.proxy,children:a||e.proxy}):`—`}),t.notes&&(0,L.jsx)(`td`,{className:`px-3 py-2 text-xs max-w-[180px]`,children:_?(0,L.jsx)(`input`,{autoFocus:!0,className:`w-full bg-dark-600 border border-dark-500 rounded px-1.5 py-0.5 text-xs text-gray-200 outline-none focus:border-accent`,value:b,onChange:e=>x(e.target.value),onBlur:async()=>{if(v(!1),b!==(e.notes||``))try{await D.updateLogpassAccount(e.id,{notes:b}),f(`success`,w(`toast.noteSaved`)),m()}catch{f(`error`,w(`toast.noteSaveError`))}},onKeyDown:t=>{t.key===`Enter`&&t.target.blur(),t.key===`Escape`&&(x(e.notes||``),v(!1))}}):(0,L.jsx)(`span`,{className:`cursor-pointer text-gray-400 hover:text-gray-200 truncate block`,onClick:()=>{x(e.notes||``),v(!0)},title:e.notes||w(`tip.addNote`),children:e.notes||`—`})}),t.actions&&(0,L.jsx)(`td`,{className:`px-3 py-2 whitespace-nowrap`,children:(0,L.jsxs)(`div`,{className:`flex items-center gap-1.5`,children:[(0,L.jsx)(`button`,{onClick:()=>c(e.id),disabled:n,className:`text-gray-400 hover:text-accent disabled:opacity-40`,title:w(`tip.validate`),children:(0,L.jsx)(De,{size:14})}),(0,L.jsx)(`button`,{onClick:()=>l(e.id),className:`text-gray-400 hover:text-accent`,title:w(`tip.edit`),children:(0,L.jsx)(xe,{size:14})}),(0,L.jsx)(`button`,{onClick:()=>o(e.id),className:`text-gray-400 hover:text-red-400`,title:w(`tip.delete`),children:(0,L.jsx)(Se,{size:14})})]})})]})}function vt({onClose:e}){let t=F(),[n,r]=(0,y.useState)(null),[i,a]=(0,y.useState)(``),[o,s]=(0,y.useState)(!1),c=(0,y.useRef)(null),l=j(e=>e.addToast),u=Ve(e=>e.loadAccounts);return(0,L.jsx)(`div`,{className:`confirm-overlay`,onMouseDown:e,children:(0,L.jsxs)(`div`,{className:`confirm-box max-w-lg w-full`,onMouseDown:e=>e.stopPropagation(),children:[(0,L.jsx)(`h3`,{className:`text-lg font-semibold mb-4`,children:t(`modal.importTokens`)}),(0,L.jsxs)(`div`,{className:`text-sm text-gray-400 mb-3 space-y-1`,children:[(0,L.jsx)(`p`,{className:`font-medium text-gray-300`,children:t(`import.formatsColon`)}),(0,L.jsx)(`p`,{className:`font-mono text-xs`,children:t(`import.tokenFormat`)})]}),(0,L.jsxs)(`div`,{onDragOver:e=>e.preventDefault(),onDrop:e=>{e.preventDefault();let t=e.dataTransfer.files[0];t&&r(t)},onClick:()=>c.current?.click(),className:`border-2 border-dashed border-dark-500 rounded-lg p-6 mb-3 text-center cursor-pointer transition-colors hover:border-accent`,children:[(0,L.jsx)(ce,{size:32,className:`mx-auto mb-2 text-gray-500`}),(0,L.jsx)(`p`,{className:`text-sm text-gray-400`,children:n?t(`import.selected`,{name:n.name}):t(`import.dragTxt`)})]}),(0,L.jsx)(`input`,{ref:c,type:`file`,accept:`.txt`,className:`hidden`,onChange:e=>{let t=e.target.files?.[0];t&&r(t)}}),(0,L.jsx)(`button`,{onClick:async()=>{if(n){s(!0);try{let e=(await n.text()).split(` +`).map(e=>e.trim()).filter(Boolean);if(!e.length){a(t(`toast.fileEmpty`)),s(!1);return}let r=await D.importTokens(e);a(t(`toast.importResult`,{imported:r.imported,skipped:r.skipped})),l(`success`,`${r.imported} ok, ${r.skipped} skip`),await u()}catch(e){a(e instanceof Error?e.message:t(`misc.errorStr`))}finally{s(!1)}}},disabled:!n||o,className:`btn-primary w-full`,children:t(o?`import.uploading`:`import.importBtn`)}),i&&(0,L.jsx)(`p`,{className:`mt-3 text-sm text-gray-300`,children:i})]})})}function yt({onClose:e}){let t=F(),[n,r]=(0,y.useState)(``),[i,a]=(0,y.useState)(``),[o,s]=(0,y.useState)(``),[c,l]=(0,y.useState)(``),u=Ve(e=>e.loadAccounts);return(0,L.jsx)(`div`,{className:`confirm-overlay`,onMouseDown:e,children:(0,L.jsxs)(`div`,{className:`confirm-box max-w-md`,onMouseDown:e=>e.stopPropagation(),children:[(0,L.jsx)(`h3`,{className:`text-lg font-semibold mb-4`,children:t(`modal.addToken`)}),(0,L.jsxs)(`div`,{className:`space-y-3`,children:[(0,L.jsx)(`textarea`,{autoFocus:!0,value:n,onChange:e=>r(e.target.value),placeholder:`Refresh token`,rows:3,className:`w-full bg-dark-700 border border-dark-600 rounded px-3 py-2 text-sm font-mono resize-none`}),(0,L.jsx)(`input`,{value:i,onChange:e=>a(e.target.value),placeholder:t(`ph.loginOptional`),className:`w-full bg-dark-700 border border-dark-600 rounded px-3 py-2 text-sm`}),(0,L.jsx)(`input`,{value:o,onChange:e=>s(e.target.value),placeholder:t(`ph.proxyOptional`),className:`w-full bg-dark-700 border border-dark-600 rounded px-3 py-2 text-sm`}),(0,L.jsx)(`input`,{value:c,onChange:e=>l(e.target.value),placeholder:t(`ph.notesOptional`),className:`w-full bg-dark-700 border border-dark-600 rounded px-3 py-2 text-sm`}),(0,L.jsx)(`button`,{onClick:async()=>{if(!n.trim())return;let t={token:n.trim(),login:i.trim()||void 0,proxy:o.trim()||void 0,notes:c.trim()||void 0};await D.createTokenAccount(t),await u(),e()},disabled:!n.trim(),className:`btn-primary w-full disabled:opacity-40`,children:t(`btn.add`)})]})]})})}var bt={browser:`col.browser`,profile:`col.profile`,last_online:`col.lastOnline`,steam_id:`col.steamId`,login:`col.login`,token:`col.token`,status:`col.status`,proxy:`col.proxy`,notes:`col.notes`,actions:`col.actions`};function xt(){let[e,t]=(0,y.useState)(!1),n=j(e=>e.tokenColumnVisibility),r=j(e=>e.toggleTokenColumn),i=(0,y.useRef)(null),a=F();return(0,y.useEffect)(()=>{let e=e=>{i.current&&!i.current.contains(e.target)&&t(!1)};return document.addEventListener(`mousedown`,e),()=>document.removeEventListener(`mousedown`,e)},[]),(0,L.jsxs)(`div`,{className:`relative`,ref:i,children:[(0,L.jsx)(`button`,{onClick:()=>t(!e),className:`btn-secondary text-sm px-2 py-2`,title:a(`tip.columnSettings`),children:(0,L.jsx)(be,{size:14})}),e&&(0,L.jsx)(`div`,{className:`absolute right-0 top-full mt-1 bg-dark-700 border border-dark-600 rounded-lg shadow-xl z-50 p-2 min-w-[160px]`,children:Object.keys(bt).map(e=>(0,L.jsxs)(`label`,{className:`flex items-center gap-2 px-2 py-1 hover:bg-dark-600 rounded cursor-pointer text-sm`,children:[(0,L.jsx)(`input`,{type:`checkbox`,checked:n[e],onChange:()=>r(e)}),a(bt[e])]},e))})]})}function St(e){if(!e)return null;if(typeof e==`string`)try{return JSON.parse(e)}catch{return null}return e}function Ct(){let e=Ve(e=>e.accounts),t=Ve(e=>e.selectedIds),n=Ve(e=>e.loadAccounts),r=Ve(e=>e.toggleSelect),i=Ve(e=>e.selectAll),a=Ve(e=>e.clearSelection),o=j(e=>e.addToast),s=j(e=>e.tokenColumnVisibility),c=j(e=>e.loadTokenColumnSettings),l=F(),[u,d]=(0,y.useState)(!1),[f,p]=(0,y.useState)(``),[m,h]=(0,y.useState)(!1),[g,_]=(0,y.useState)(!1),[v,b]=(0,y.useState)({open:!1,message:``,onConfirm:()=>{}}),[x,S]=(0,y.useState)(new Set),[C,w]=(0,y.useState)({}),[T,E]=(0,y.useState)({}),[O,ee]=(0,y.useState)(null);(0,y.useEffect)(()=>{n(),c()},[n,c]);let k=(e,t)=>b({open:!0,message:e,onConfirm:t}),A=(0,y.useCallback)(e=>{let t=new EventSource(`/api/tasks/${e}/stream`);t.onmessage=e=>{try{let r=JSON.parse(e.data);if(ee(r),r.account_results){let e=St(r.account_results);e&&w(e)}r.account_steps&&E(r.account_steps),r.status!==`running`&&(t.close(),S(new Set),n())}catch{}},t.onerror=()=>t.close()},[n]),M=(0,y.useCallback)(async()=>{let e=[...t];if(e.length){a(),S(new Set(e)),w({}),E({});try{let{task_id:t}=await D.validateTokens(e);ee({id:t,type:`token_validate`,status:`running`,progress:0,total:e.length,result:null,error:null,created_at:``,updated_at:``}),A(t)}catch(e){S(new Set),o(`error`,String(e))}}},[t,a,A,o]),te=(0,y.useCallback)(async e=>{S(new Set([e])),w(t=>{let n={...t};return delete n[String(e)],n}),E(t=>{let n={...t};return delete n[String(e)],n});try{let{task_id:t}=await D.validateTokens([e]);ee({id:t,type:`token_validate`,status:`running`,progress:0,total:1,result:null,error:null,created_at:``,updated_at:``}),A(t)}catch(e){S(new Set),o(`error`,String(e))}},[A,o]),ne=()=>{let e=[...t];e.length&&k(l(`confirm.deleteTokenSelected`,{count:e.length}),async()=>{await D.deleteTokenBulk(e),a(),n()})},N=()=>{k(l(`confirm.deleteTokenAll`,{count:e.length}),async()=>{await D.deleteTokenBulk(e.map(e=>e.id)),a(),n()})},P=()=>{k(l(`confirm.assignProxies`),async()=>{try{o(`success`,l(`toast.proxiesAssignedShort`,{count:(await D.assignTokenProxies()).assigned})),n()}catch(e){o(`error`,e instanceof Error?e.message:String(e))}})},re=()=>{k(l(`confirm.reassignProxiesAll`),async()=>{try{o(`success`,l(`toast.proxiesReassignedShort`,{count:(await D.reassignTokenProxies()).assigned})),n()}catch(e){o(`error`,e instanceof Error?e.message:String(e))}})},ie=()=>{k(l(`confirm.clearProxies`),async()=>{try{o(`success`,l(`toast.proxiesClearedShort`,{count:(await D.clearTokenProxies()).cleared})),n()}catch(e){o(`error`,e instanceof Error?e.message:String(e))}})},ae=async e=>{try{let t=await D.openTokenBrowser(e);t.status===`revalidating`?(o(`warn`,l(`toast.cookiesExpiredRevalidating`)),t.task_id&&(S(new Set([e])),ee({id:t.task_id,type:`token_validate`,status:`running`,progress:0,total:1,result:null,error:null,created_at:``,updated_at:``}),A(t.task_id))):o(`success`,l(`toast.browserOpening`))}catch(e){o(`error`,e instanceof Error?e.message:String(e))}},I=e.length>0&&t.size===e.length,oe=(0,y.useMemo)(()=>{if(!f.trim())return e;let t=f.trim(),n=t.match(/^lvl\s*(>=|<=|>|<|=)\s*(\d+)$/i);if(n){let t=n[1],r=parseInt(n[2],10);return e.filter(e=>{let n=e.steam_level;return n==null?!1:t===`>`?n>r:t===`>=`?n>=r:t===`<`?n(e.login??``).toLowerCase().includes(r)||(e.steam_id??``).toLowerCase().includes(r)||(e.nickname??``).toLowerCase().includes(r)||(e.notes??``).toLowerCase().includes(r))},[e,f]),R=(0,y.useMemo)(()=>{let t=new Map,n=[];for(let t of e)t.proxy&&!n.includes(t.proxy)&&n.push(t.proxy);let r=new Map;n.forEach((e,t)=>r.set(e,t+1));for(let n of e)n.proxy&&t.set(n.id,l(`proxy.label`,{num:r.get(n.proxy)??0}));return t},[e,l]);return(0,L.jsxs)(`div`,{className:`relative flex flex-col gap-4 h-full min-h-0`,children:[!u&&(0,L.jsx)(`div`,{className:`absolute inset-0 z-50 bg-dark-900/80 backdrop-blur-sm flex items-center justify-center`,children:(0,L.jsxs)(`div`,{className:`bg-dark-800 border border-yellow-500/40 rounded-xl p-6 max-w-lg mx-4 shadow-2xl`,children:[(0,L.jsxs)(`div`,{className:`flex items-center gap-3 mb-4`,children:[(0,L.jsx)(de,{size:28,className:`text-yellow-400 shrink-0`}),(0,L.jsx)(`h3`,{className:`text-lg font-semibold text-yellow-400`,children:l(`token.disclaimerTitle`)})]}),(0,L.jsxs)(`div`,{className:`text-sm text-gray-300 space-y-2 mb-5`,children:[(0,L.jsx)(`p`,{dangerouslySetInnerHTML:{__html:l(`token.disclaimerBody1`)}}),(0,L.jsx)(`p`,{className:`text-red-400 font-medium`,dangerouslySetInnerHTML:{__html:l(`token.disclaimerBody2`)}}),(0,L.jsx)(`p`,{dangerouslySetInnerHTML:{__html:l(`token.disclaimerBody3`)}})]}),(0,L.jsxs)(`label`,{className:`flex items-center gap-2 cursor-pointer select-none group`,children:[(0,L.jsx)(`input`,{type:`checkbox`,checked:u,onChange:e=>d(e.target.checked),className:`w-4 h-4 rounded border-yellow-500/50 accent-yellow-500`}),(0,L.jsx)(`span`,{className:`text-sm text-gray-400 group-hover:text-gray-300 transition`,children:l(`token.acceptRisk`)})]})]})}),(0,L.jsxs)(`div`,{className:`bg-dark-800 border border-dark-600 rounded-lg px-3 py-2 flex flex-wrap items-center gap-2 shrink-0`,children:[(0,L.jsxs)(`button`,{onClick:()=>h(!0),className:`btn-primary`,children:[(0,L.jsx)(se,{size:14,className:`inline mr-1`}),l(`btn.import`)]}),(0,L.jsxs)(`button`,{onClick:()=>_(!0),className:`btn-secondary`,children:[(0,L.jsx)(le,{size:14,className:`inline mr-1`}),l(`btn.add`)]}),O&&(0,L.jsxs)(L.Fragment,{children:[(0,L.jsx)(`div`,{className:`h-5 border-l border-dark-500`}),(0,L.jsxs)(`div`,{className:`flex items-center gap-2 min-w-0`,children:[(0,L.jsx)(`span`,{className:`text-xs text-gray-400 whitespace-nowrap`,children:l(`task.validate`)}),(0,L.jsx)(`div`,{className:`w-28 bg-dark-600 rounded-full h-2.5 shrink-0`,children:(0,L.jsx)(`div`,{className:`h-2.5 rounded-full transition-all duration-300 ${O.status===`completed`?`bg-green-500`:O.status===`failed`?`bg-red-500`:`bg-accent`}`,style:{width:`${O.total>1?O.total>0?Math.round(O.progress/O.total*100):0:O.total_steps?Math.round((O.step??0)/O.total_steps*100):0}%`}})}),(0,L.jsxs)(`span`,{className:`text-xs text-gray-500 whitespace-nowrap`,children:[O.total>1?`${O.progress}/${O.total}${O.active_count?` (${O.active_count})`:``}`:O.step_label||`${O.progress}/${O.total}`,O.total>1?` (${O.total>0?Math.round(O.progress/O.total*100):0}%)`:O.total_steps?` (${O.step}/${O.total_steps})`:``]}),O.status===`completed`&&(0,L.jsx)(ke,{size:14,className:`text-green-400`}),O.status===`failed`&&(0,L.jsx)(`span`,{title:O.error||``,children:(0,L.jsx)(Oe,{size:14,className:`text-red-400`})})]})]}),(0,L.jsxs)(`div`,{className:`ml-auto flex items-center gap-2`,children:[(0,L.jsx)(xt,{}),(0,L.jsx)(`div`,{className:`h-5 border-l border-dark-500`}),(0,L.jsx)(`button`,{onClick:ne,className:`btn-danger-outline text-xs`,children:l(`btn.deleteSelected`)}),(0,L.jsx)(`button`,{onClick:N,className:`btn-danger text-xs`,children:l(`btn.deleteAll`)})]})]}),(0,L.jsxs)(`div`,{className:`bg-dark-800 border border-dark-600 rounded-lg overflow-hidden flex-1 min-h-0 flex flex-col`,children:[(0,L.jsxs)(`div`,{className:`px-3 py-2 flex items-center gap-2 border-b border-dark-600 shrink-0 bg-dark-800`,children:[(0,L.jsxs)(`button`,{onClick:M,disabled:t.size===0||x.size>0,className:`btn-accent text-sm disabled:opacity-40`,children:[(0,L.jsx)(De,{size:12,className:`inline mr-1`}),l(`btn.validate`)]}),(0,L.jsx)(`div`,{className:`h-5 border-l border-dark-500`}),(0,L.jsxs)(`button`,{onClick:P,className:`btn-secondary text-sm`,children:[(0,L.jsx)(pe,{size:14,className:`inline mr-1`}),l(`btn.assignProxies`)]}),(0,L.jsxs)(`button`,{onClick:re,className:`btn-secondary text-sm`,children:[(0,L.jsx)(me,{size:14,className:`inline mr-1`}),l(`btn.reassign`)]}),(0,L.jsxs)(`button`,{onClick:ie,className:`btn-danger-outline text-sm`,children:[(0,L.jsx)(he,{size:14,className:`inline mr-1`}),l(`btn.clearProxies`)]}),(0,L.jsx)(`div`,{className:`ml-auto`,children:(0,L.jsx)(`input`,{type:`text`,value:f,onChange:e=>p(e.target.value),placeholder:l(`ph.search`),className:`bg-dark-700 border border-dark-600 rounded px-2 py-1.5 text-xs w-80 placeholder:text-gray-500 outline-none focus:border-accent`})})]}),(0,L.jsx)(`div`,{className:`overflow-auto flex-1 min-h-0`,children:(0,L.jsxs)(`table`,{className:`w-full text-sm`,children:[(0,L.jsx)(`thead`,{className:`sticky top-0 bg-dark-800 z-10`,children:(0,L.jsxs)(`tr`,{className:`text-left text-xs text-gray-500 border-b border-dark-600`,children:[(0,L.jsx)(`th`,{className:`px-3 py-2`,children:(0,L.jsxs)(`div`,{className:`flex items-center gap-1.5`,children:[(0,L.jsx)(`input`,{type:`checkbox`,checked:I,onChange:()=>I?a():i()}),t.size>0&&(0,L.jsx)(`span`,{className:`text-accent font-medium`,children:t.size})]})}),(0,L.jsx)(`th`,{className:`w-5`}),s.browser&&(0,L.jsx)(`th`,{className:`px-3 py-2`,children:l(`col.browser`)}),s.profile&&(0,L.jsx)(`th`,{className:`px-3 py-2`,children:l(`col.profile`)}),s.last_online&&(0,L.jsx)(`th`,{className:`px-3 py-2`,children:l(`col.lastOnline`)}),s.steam_id&&(0,L.jsx)(`th`,{className:`px-3 py-2`,children:`Steam ID`}),s.login&&(0,L.jsx)(`th`,{className:`px-3 py-2`,children:l(`col.login`)}),s.token&&(0,L.jsx)(`th`,{className:`px-3 py-2`,children:l(`col.token`)}),s.status&&(0,L.jsx)(`th`,{className:`px-3 py-2`,children:l(`col.status`)}),s.proxy&&(0,L.jsx)(`th`,{className:`px-3 py-2`,children:l(`col.proxy`)}),s.notes&&(0,L.jsx)(`th`,{className:`px-3 py-2`,children:l(`col.notes`)}),s.actions&&(0,L.jsx)(`th`,{className:`px-3 py-2`,children:l(`col.actions`)})]})}),(0,L.jsx)(`tbody`,{children:oe.length===0?(0,L.jsx)(`tr`,{children:(0,L.jsx)(`td`,{colSpan:12,className:`text-center py-12 text-gray-500`,children:l(`empty.tokens`)})}):oe.map(e=>{let i=x.has(e.id),a=C[String(e.id)],o=T[String(e.id)];return(0,L.jsxs)(`tr`,{className:`hover:bg-dark-700/50 transition border-t border-dark-700`,children:[(0,L.jsx)(`td`,{className:`px-3 py-2`,children:(0,L.jsx)(`input`,{type:`checkbox`,checked:t.has(e.id),onChange:()=>r(e.id)})}),(0,L.jsx)(`td`,{className:`px-1 py-2 w-5`,children:i?(0,L.jsxs)(`div`,{className:`flex items-center gap-1`,children:[(0,L.jsx)(`div`,{className:`w-4 h-4 border-2 border-accent border-t-transparent rounded-full animate-spin shrink-0`}),o&&(0,L.jsxs)(`span`,{className:`text-[10px] text-gray-500 whitespace-nowrap`,children:[`[`,o.step,`/`,o.total,`]`]})]}):a?.status===`ok`?(0,L.jsx)(ue,{size:16,className:`text-gray-400`}):a?.status===`error`?(0,L.jsx)(`span`,{title:a.error,children:(0,L.jsx)(de,{size:16,className:`text-red-400`})}):null}),s.browser&&(0,L.jsx)(`td`,{className:`px-3 py-2 whitespace-nowrap text-center`,children:e.has_cookies?(0,L.jsx)(`button`,{onClick:()=>ae(e.id),className:`px-2 py-0.5 text-xs font-medium rounded bg-blue-600/20 text-blue-400 border border-blue-500/30 hover:bg-blue-600/40 hover:text-blue-300 active:scale-95 transition`,title:l(`tip.openBrowser`),children:l(`col.browser`)}):(0,L.jsx)(`span`,{className:`text-gray-600 text-xs`,children:`—`})}),s.profile&&(0,L.jsx)(`td`,{className:`px-3 py-2 whitespace-nowrap`,children:(0,L.jsxs)(`div`,{className:`flex items-center gap-1.5`,children:[e.avatar_url&&(0,L.jsx)(`img`,{src:e.avatar_url,className:`avatar-sm`,alt:``}),(0,L.jsx)(`span`,{className:`text-xs`,children:e.nickname||`—`}),e.steam_level!=null&&(0,L.jsx)($e,{level:e.steam_level})]})}),s.last_online&&(0,L.jsx)(`td`,{className:`px-3 py-2 text-xs text-gray-400 whitespace-nowrap`,children:e.last_online||`—`}),s.steam_id&&(0,L.jsx)(`td`,{className:`px-3 py-2 text-xs`,children:e.steam_id?(0,L.jsx)(`a`,{href:`https://steamcommunity.com/profiles/${encodeURIComponent(e.steam_id)}/`,target:`_blank`,rel:`noopener noreferrer`,className:`text-blue-400 hover:text-blue-300 hover:underline`,children:e.steam_id}):`—`}),s.login&&(0,L.jsx)(`td`,{className:`px-3 py-2 font-medium`,children:e.login??`—`}),s.token&&(0,L.jsxs)(`td`,{className:`px-3 py-2 font-mono text-gray-400 truncate max-w-[180px]`,title:e.token,children:[e.token.slice(0,24),`…`]}),s.status&&(0,L.jsx)(`td`,{className:`px-3 py-2`,children:(0,L.jsx)(We,{status:e.status})}),s.proxy&&(0,L.jsx)(`td`,{className:`px-3 py-2 text-xs text-gray-500 whitespace-nowrap`,children:R.get(e.id)??`—`}),s.notes&&(0,L.jsx)(`td`,{className:`px-3 py-2 text-xs text-gray-500`,children:e.notes??`—`}),s.actions&&(0,L.jsx)(`td`,{className:`px-3 py-2 whitespace-nowrap`,children:(0,L.jsxs)(`div`,{className:`flex items-center gap-1.5`,children:[(0,L.jsx)(`button`,{onClick:()=>te(e.id),disabled:i,className:`text-gray-400 hover:text-accent disabled:opacity-40 transition`,title:l(`tip.validate`),children:(0,L.jsx)(De,{size:14})}),(0,L.jsx)(`button`,{onClick:()=>k(l(`confirm.deleteToken`,{name:e.login??e.token.slice(0,16)}),async()=>{await D.deleteTokenAccount(e.id),n()}),className:`text-gray-400 hover:text-red-400 transition`,title:l(`tip.delete`),children:(0,L.jsx)(Se,{size:14})})]})})]},e.id)})})]})})]}),m&&(0,L.jsx)(vt,{onClose:()=>h(!1)}),g&&(0,L.jsx)(yt,{onClose:()=>_(!1)}),v.open&&(0,L.jsx)(st,{message:v.message,onConfirm:()=>{v.onConfirm(),b(e=>({...e,open:!1}))},onCancel:()=>b(e=>({...e,open:!1}))})]})}function wt(e){if(!e)return null;if(typeof e==`string`)try{return JSON.parse(e)}catch{return null}return e}function Tt(e){if(!e)return[];try{return JSON.parse(e)}catch{return[]}}var Et=[{value:``,labelKey:`action.selectAction`},{value:`validate`,labelKey:`action.validate`},{value:`change_password`,labelKey:`action.changePassword`},{value:`random_password`,labelKey:`action.randomPassword`},{value:`change_email`,labelKey:`action.changeEmail`},{value:`remove_guard`,labelKey:`action.removeGuard`},{value:`enable_auto_accept`,labelKey:`action.enableAutoAccept`}];function Dt({value:e,onChange:t,disabledActions:n}){let r=F(),[i,a]=(0,y.useState)(!1),o=(0,y.useRef)(null);return(0,y.useEffect)(()=>{if(!i)return;let e=e=>{o.current&&!o.current.contains(e.target)&&a(!1)};return document.addEventListener(`mousedown`,e),()=>document.removeEventListener(`mousedown`,e)},[i]),(0,L.jsxs)(`div`,{ref:o,className:`relative`,children:[(0,L.jsxs)(`button`,{type:`button`,onClick:()=>a(e=>!e),className:`bg-dark-700 border border-dark-600 rounded px-2 py-1.5 text-sm flex items-center gap-1 min-w-[170px] justify-between hover:border-dark-500 transition-colors`,children:[(0,L.jsx)(`span`,{className:`truncate`,children:r(Et.find(t=>t.value===e)?.labelKey||`action.selectAction`)}),(0,L.jsx)(`svg`,{width:`12`,height:`12`,viewBox:`0 0 12 12`,fill:`none`,className:`shrink-0 transition-transform ${i?`rotate-180`:``}`,children:(0,L.jsx)(`path`,{d:`M3 4.5L6 7.5L9 4.5`,stroke:`currentColor`,strokeWidth:`1.5`,strokeLinecap:`round`,strokeLinejoin:`round`})})]}),i&&(0,L.jsx)(`div`,{className:`absolute top-full left-0 mt-1 z-50 bg-dark-700 border border-dark-600 rounded shadow-lg min-w-[100px] py-1 max-h-[320px] overflow-y-auto`,children:Et.map(i=>{let o=!!(i.value&&n?.has(i.value));return(0,L.jsx)(`button`,{type:`button`,onClick:()=>{o||(t(i.value),a(!1))},disabled:o,title:o?r(`action.noRevocationCode`):void 0,className:`w-full text-left px-3 py-2 text-sm transition-colors ${o?`opacity-40 cursor-not-allowed`:`hover:bg-dark-600`} ${i.value===e?`text-accent bg-dark-600/50`:``} ${i.value?``:`text-gray-500`}`,children:r(i.labelKey)},i.value)})})]})}function Ot(){let e=j(e=>e.accountSection),t=Re(e=>e.accounts.length),n=Be(e=>e.accounts.length),r=Ve(e=>e.accounts.length),i=Be(e=>e.loadAccounts),a=Ve(e=>e.loadAccounts),[o,s]=(0,y.useState)(0),c=(0,y.useRef)(0),l=(0,y.useRef)(0),u=(0,y.useRef)(e);(0,y.useEffect)(()=>{e===`logpass`&&n===0&&i(),e===`token`&&r===0&&a()},[e]);let d=e===`logpass`?n:e===`token`?r:t;return(0,y.useEffect)(()=>{cancelAnimationFrame(c.current);let t=u.current!==e;if(u.current=e,t){l.current=d,s(d);return}let n=l.current,r=d-n;if(r===0)return;let i=Math.min(400,Math.max(150,Math.abs(r)*2)),a=performance.now(),o=e=>{let t=e-a,u=Math.min(t/i,1),d=1-(1-u)**3,f=Math.max(0,Math.round(n+r*d));l.current=f,s(f),u<1&&(c.current=requestAnimationFrame(o))};return c.current=requestAnimationFrame(o),()=>cancelAnimationFrame(c.current)},[d,e]),(0,L.jsxs)(`div`,{className:`mt-1 px-3 py-2 border-t border-dark-600 flex items-center gap-2`,children:[(0,L.jsxs)(`div`,{className:`relative shrink-0`,style:{width:14,height:14},children:[(0,L.jsxs)(`svg`,{xmlns:`http://www.w3.org/2000/svg`,width:`14`,height:`14`,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:`2`,strokeLinecap:`round`,strokeLinejoin:`round`,className:`text-gray-400`,children:[(0,L.jsx)(`path`,{d:`M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2`}),(0,L.jsx)(`circle`,{cx:`9`,cy:`7`,r:`4`}),(0,L.jsx)(`path`,{d:`M22 21v-2a4 4 0 0 0-3-3.87`}),(0,L.jsx)(`path`,{d:`M16 3.13a4 4 0 0 1 0 7.75`})]}),(0,L.jsxs)(`svg`,{xmlns:`http://www.w3.org/2000/svg`,width:`14`,height:`14`,viewBox:`0 0 24 24`,fill:`none`,strokeWidth:`2`,strokeLinecap:`round`,strokeLinejoin:`round`,className:`icon-shimmer absolute inset-0`,style:{pointerEvents:`none`},children:[(0,L.jsx)(`path`,{d:`M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2`}),(0,L.jsx)(`circle`,{cx:`9`,cy:`7`,r:`4`}),(0,L.jsx)(`path`,{d:`M22 21v-2a4 4 0 0 0-3-3.87`}),(0,L.jsx)(`path`,{d:`M16 3.13a4 4 0 0 1 0 7.75`})]})]}),(0,L.jsx)(`span`,{className:`text-sm text-gray-300 font-medium tabular-nums`,children:o})]})}function kt(){let e=Re(e=>e.accounts),t=Re(e=>e.selectedIds),n=Re(e=>e.loadAccounts),r=Re(e=>e.clearSelection),i=Re(e=>e.sendToMafileManager),a=j(e=>e.setTab),o=j(e=>e.addToast),s=ze(e=>e.loadTasks),c=F(),[l,u]=(0,y.useState)(null),[d,f]=(0,y.useState)(!1),[p,m]=(0,y.useState)(!1),[h,g]=(0,y.useState)(null),[_,v]=(0,y.useState)(null),[b,x]=(0,y.useState)(``),S=j(e=>e.accountSection),C=j(e=>e.setAccountSection),[w,T]=(0,y.useState)(null),[E,O]=(0,y.useState)(new Set),[ee,k]=(0,y.useState)(``),[A,M]=(0,y.useState)(null),[te,ne]=(0,y.useState)(``),[N,P]=(0,y.useState)({}),[re,ie]=(0,y.useState)({}),ae=(0,y.useMemo)(()=>{if(!ee.trim())return e;let t=ee.trim(),n=t.match(/^lvl\s*(>=|<=|>|<|=)\s*(\d+)$/i);if(n){let t=n[1],r=parseInt(n[2],10);return e.filter(e=>{let n=e.steam_level;return n==null?!1:t===`>`?n>r:t===`>=`?n>=r:t===`<`?n{if(e.login.toLowerCase().includes(r)||e.steam_id&&e.steam_id.toLowerCase().includes(r))return!0;if(e.notes){let t=e.notes.toLowerCase();return i.some(e=>t.includes(e))}return!1})},[e,ee]),I=(0,y.useMemo)(()=>{let n=new Set;return e.filter(e=>t.has(e.id)).some(e=>e.has_revocation_code)||n.add(`remove_guard`),n},[e,t]);(0,y.useEffect)(()=>{n()},[]);let oe=(e,t)=>{g(e),v(()=>t)},R=(0,y.useCallback)((e,t=[],r=!0)=>{r&&P(e=>{let n={...e};return t.forEach(e=>delete n[String(e)]),n});let i=new EventSource(`/api/tasks/${e}/stream`);i.onmessage=r=>{try{let a=JSON.parse(r.data);T(a);let l=wt(a.account_results);l&&P(e=>({...e,...l})),a.account_steps&&ie(e=>({...e,...a.account_steps})),a.prompt&&(M({taskId:e,message:a.prompt,login:a.prompt_login}),ne(``)),(a.status===`completed`||a.status===`failed`||a.status===`cancelled`)&&(a.status===`completed`?o(`success`,`${a.type}: ${a.result||c(`toast.done`)}`):a.status===`cancelled`?o(`info`,c(`toast.taskCancelled`)):o(`error`,`${a.type}: ${a.error||c(`toast.error`)}`),O(e=>{let n=new Set(e);return t.forEach(e=>n.delete(e)),n}),M(null),n(),s(),i.close(),setTimeout(()=>T(null),3e3))}catch{}},i.onerror=()=>i.close()},[o,n,s]);(0,y.useEffect)(()=>{let e=!1;return(async()=>{try{let t=await D.getTasks();if(e)return;let n=t.find(e=>e.status===`running`);if(n){let e=Tt(n.account_ids);T(n),O(new Set(e));let t=wt(n.account_results);t&&P(t),R(n.id,e,!1);return}let r=t.find(e=>e.status===`completed`||e.status===`failed`);if(r){let e=wt(r.account_results);e&&Object.keys(e).length>0&&P(e)}}catch{}})(),()=>{e=!0}},[R]);let ce=async(t,n,r)=>{let i=e.find(e=>e.id===t)?.login??`#${t}`;try{O(e=>new Set(e).add(t));let e=await D.executeAction({account_ids:[t],action:n,params:r});o(`info`,c(`misc.taskAction`,{id:e.task_id,action:n,login:i})),R(e.task_id,[t])}catch(e){O(e=>{let n=new Set(e);return n.delete(t),n}),o(`error`,c(`misc.error`,{error:e instanceof Error?e.message:String(e)}))}},ue=async()=>{if(!(!A||!te))try{await D.respondToPrompt(A.taskId,te,A.login),M(null)}catch(e){o(`error`,c(`misc.error`,{error:e instanceof Error?e.message:String(e)}))}},de=async()=>{if(A)try{await D.cancelTask(A.taskId)}catch{}M(null)},ge=()=>{if(!b)return o(`warn`,c(`toast.selectAction`));if(!t.size)return o(`warn`,c(`toast.selectAccounts`));if(b===`enable_auto_accept`){let e=[...t];r(),D.startAutoAccept(e).then(()=>{o(`success`,c(`toast.autoAcceptEnabled`,{count:e.length})),n()}).catch(e=>o(`error`,c(`misc.error`,{error:e instanceof Error?e.message:String(e)})));return}let e=[...t],i=b;oe(c(`confirm.executeBulk`,{action:c(Et.find(e=>e.value===i)?.labelKey||`action.selectAction`),count:e.length}),async()=>{try{let t=await D.executeAction({account_ids:e,action:i});r(),O(t=>{let n=new Set(t);return e.forEach(e=>n.add(e)),n}),o(`info`,c(`misc.taskCount`,{id:t.task_id,count:t.accounts_count})),R(t.task_id,e)}catch(e){o(`error`,c(`misc.error`,{error:e instanceof Error?e.message:String(e)}))}})},_e=async t=>{let r=e.find(e=>e.id===t);if(!r)return;let i=!r.auto_accept;try{i?await D.startAutoAccept([t]):await D.stopAutoAccept([t]),o(`info`,c(i?`toast.autoAcceptOn`:`toast.autoAcceptOff`)),await n()}catch(e){o(`error`,c(`misc.error`,{error:e instanceof Error?e.message:String(e)}))}},be=e=>u(e),xe=async(e,t)=>{try{await D.updateAccount(e,t),u(null),o(`success`,c(`toast.saved`)),await n()}catch(e){o(`error`,c(`misc.error`,{error:e instanceof Error?e.message:String(e)}))}},Se=e=>{oe(c(`confirm.deleteAccount`),async()=>{try{await D.deleteAccount(e),o(`success`,c(`toast.accountDeleted`)),await n()}catch(e){o(`error`,c(`misc.error`,{error:e instanceof Error?e.message:String(e)}))}})},we=()=>{if(!t.size)return o(`warn`,c(`toast.selectAccounts`));oe(c(`confirm.deleteSelected`,{count:t.size}),async()=>{try{let e=await D.deleteBulk([...t]);r(),o(`success`,c(`toast.deleted`,{count:e.deleted})),await n()}catch(e){o(`error`,c(`misc.error`,{error:e instanceof Error?e.message:String(e)}))}})},Te=()=>{if(!e.length)return o(`warn`,c(`toast.noAccounts`));oe(c(`confirm.deleteAll`,{count:e.length}),async()=>{try{let e=await D.deleteBulk([]);r(),o(`success`,c(`toast.deleted`,{count:e.deleted})),await n()}catch(e){o(`error`,c(`misc.error`,{error:e instanceof Error?e.message:String(e)}))}})},Ee=async e=>{try{await D.createAccount(e),m(!1),o(`success`,c(`toast.accountAdded`)),await n()}catch(e){o(`error`,c(`misc.error`,{error:e instanceof Error?e.message:String(e)}))}},Ae=()=>{if(!t.size)return o(`warn`,c(`toast.selectAccounts`));i([...t]),a(`mafiles`),o(`info`,c(`toast.sentToMafile`,{count:t.size}))},je=async e=>{try{let t=await D.openBrowser(e);t.status===`revalidating`?(o(`warn`,c(`toast.cookiesExpiredRevalidating`)),t.task_id&&(O(t=>new Set(t).add(e)),R(t.task_id,[e]))):o(`info`,c(`toast.browserOpening`))}catch(e){o(`error`,c(`misc.error`,{error:e instanceof Error?e.message:String(e)}))}},Me=async()=>{oe(c(`confirm.assignProxies`),async()=>{try{let e=await D.assignProxies();o(`success`,c(`toast.proxiesAssigned`,{used:e.proxies_used,count:e.assigned})),await n()}catch(e){o(`error`,c(`misc.error`,{error:e instanceof Error?e.message:String(e)}))}})},Ne=async()=>{oe(c(`confirm.reassignProxies`),async()=>{try{let e=await D.reassignProxies();o(`success`,c(`toast.proxiesReassigned`,{used:e.proxies_used,count:e.assigned})),await n()}catch(e){o(`error`,c(`misc.error`,{error:e instanceof Error?e.message:String(e)}))}})},Pe=async()=>{oe(c(`confirm.clearProxies`),async()=>{try{o(`success`,c(`toast.proxiesClearedCount`,{count:(await D.clearProxies()).cleared})),await n()}catch(e){o(`error`,c(`misc.error`,{error:e instanceof Error?e.message:String(e)}))}})},Fe=l?e.find(e=>e.id===l):null,Ie=[{id:`mafile`,label:`Mafile`,icon:(0,L.jsx)(Ce,{size:14})},{id:`logpass`,label:`Log:Pass`,icon:(0,L.jsx)(ve,{size:14})},{id:`token`,label:`Token`,icon:(0,L.jsx)(ye,{size:14})}];return(0,L.jsxs)(`div`,{className:`flex gap-3 h-full min-h-0 overflow-hidden`,children:[(0,L.jsxs)(`div`,{className:`w-36 shrink-0 flex flex-col gap-1 bg-dark-800 border border-dark-600 rounded-lg p-2 self-start`,children:[(0,L.jsx)(`p`,{className:`text-xs font-medium text-gray-500 px-2 py-1 uppercase tracking-wider`,children:c(`section.dataType`)}),Ie.map(e=>(0,L.jsxs)(`button`,{onClick:()=>C(e.id),className:`flex items-center gap-2 px-3 py-2 rounded text-sm text-left w-full transition-colors ${S===e.id?`bg-accent/20 text-accent`:`text-gray-400 hover:text-gray-200 hover:bg-dark-700`}`,children:[e.icon,e.label]},e.id)),(0,L.jsx)(Ot,{})]}),S===`logpass`&&(0,L.jsx)(`div`,{className:`flex-1 min-w-0 min-h-0`,children:(0,L.jsx)(gt,{})}),S===`token`&&(0,L.jsx)(`div`,{className:`flex-1 min-w-0 min-h-0`,children:(0,L.jsx)(Ct,{})}),S===`mafile`&&(0,L.jsxs)(`div`,{className:`flex flex-col gap-4 flex-1 min-w-0 min-h-0 overflow-hidden`,children:[(0,L.jsxs)(`div`,{className:`bg-dark-800 border border-dark-600 rounded-lg px-3 py-2 flex flex-wrap items-center gap-2 shrink-0`,children:[(0,L.jsxs)(`button`,{onClick:()=>f(!0),className:`btn-primary`,children:[(0,L.jsx)(se,{size:14,className:`inline mr-1`}),c(`btn.import`)]}),(0,L.jsxs)(`button`,{onClick:()=>m(!0),className:`btn-secondary`,children:[(0,L.jsx)(le,{size:14,className:`inline mr-1`}),c(`btn.add`)]}),w&&(0,L.jsxs)(L.Fragment,{children:[(0,L.jsx)(`div`,{className:`h-5 border-l border-dark-500`}),(0,L.jsxs)(`div`,{className:`flex items-center gap-2 min-w-0`,children:[(0,L.jsx)(`span`,{className:`text-xs text-gray-400 whitespace-nowrap`,children:w.type===`validate`?c(`task.validate`):w.type===`change_password`?c(`task.changePassword`):w.type===`random_password`?c(`task.randomPassword`):w.type===`change_email`?c(`task.changeEmail`):w.type===`change_phone`?c(`task.changePhone`):w.type===`remove_guard`?c(`task.removeGuard`):w.type}),(0,L.jsx)(`div`,{className:`w-28 bg-dark-600 rounded-full h-2.5 shrink-0`,children:(0,L.jsx)(`div`,{className:`h-2.5 rounded-full transition-all duration-300 ${w.status===`completed`?`bg-green-500`:w.status===`failed`?`bg-red-500`:`bg-accent`}`,style:{width:`${w.total>1?w.total>0?Math.round(w.progress/w.total*100):0:w.total_steps?Math.round((w.step??0)/w.total_steps*100):0}%`}})}),(0,L.jsxs)(`span`,{className:`text-xs text-gray-500 whitespace-nowrap`,children:[w.total>1?`${w.progress}/${w.total}${w.active_count?` (${w.active_count})`:``}`:w.step_label||`${w.progress}/${w.total}`,w.total>1?` (${w.total>0?Math.round(w.progress/w.total*100):0}%)`:w.total_steps?` (${w.step}/${w.total_steps})`:``]}),w.status===`completed`&&(0,L.jsx)(ke,{size:14,className:`text-green-400`}),w.status===`failed`&&(0,L.jsx)(`span`,{title:w.error||``,children:(0,L.jsx)(Oe,{size:14,className:`text-red-400`})})]})]}),(0,L.jsxs)(`div`,{className:`ml-auto flex items-center gap-2`,children:[(0,L.jsx)(lt,{}),(0,L.jsx)(`div`,{className:`h-5 border-l border-dark-500`}),(0,L.jsx)(`button`,{onClick:we,className:`btn-danger-outline text-xs`,children:c(`btn.deleteSelected`)}),(0,L.jsx)(`button`,{onClick:Te,className:`btn-danger text-xs`,children:c(`btn.deleteAll`)})]})]}),(0,L.jsxs)(`div`,{className:`bg-dark-800 border border-dark-600 rounded-lg overflow-hidden flex-1 min-h-0 flex flex-col`,children:[(0,L.jsxs)(`div`,{className:`px-3 py-2 flex items-center gap-2 border-b border-dark-600 shrink-0 bg-dark-800`,children:[(0,L.jsx)(Dt,{value:b,onChange:x,disabledActions:I}),(0,L.jsxs)(`button`,{onClick:ge,disabled:!!w||E.size>0,className:`btn-accent text-sm disabled:opacity-40 disabled:cursor-not-allowed`,children:[(0,L.jsx)(De,{size:12,className:`inline mr-1`}),c(`btn.execute`)]}),w&&w.status===`running`&&(0,L.jsxs)(`button`,{onClick:async()=>{try{await D.cancelTask(w.id)}catch{}},className:`btn-danger text-sm`,children:[(0,L.jsx)(Oe,{size:12,className:`inline mr-1`}),c(`btn.cancel`)]}),(0,L.jsx)(`div`,{className:`h-5 border-l border-dark-500`}),(0,L.jsxs)(`button`,{onClick:Ae,className:`btn-secondary text-sm`,children:[(0,L.jsx)(fe,{size:14,className:`inline mr-1`}),`В Mafile Manager`]}),(0,L.jsx)(`div`,{className:`h-5 border-l border-dark-500`}),(0,L.jsxs)(`button`,{onClick:Me,className:`btn-secondary text-sm`,children:[(0,L.jsx)(pe,{size:14,className:`inline mr-1`}),c(`btn.assignProxies`)]}),(0,L.jsxs)(`button`,{onClick:Ne,className:`btn-secondary text-sm`,children:[(0,L.jsx)(me,{size:14,className:`inline mr-1`}),c(`btn.reassign`)]}),(0,L.jsxs)(`button`,{onClick:Pe,className:`btn-danger-outline text-sm`,children:[(0,L.jsx)(he,{size:14,className:`inline mr-1`}),c(`btn.clearProxies`)]}),(0,L.jsx)(`div`,{className:`ml-auto`,children:(0,L.jsx)(`input`,{type:`text`,value:ee,onChange:e=>k(e.target.value),placeholder:c(`ph.search`),className:`bg-dark-700 border border-dark-600 rounded px-2 py-1.5 text-xs w-80 placeholder:text-gray-500 outline-none focus:border-accent`})})]}),(0,L.jsx)(`div`,{className:`overflow-auto flex-1 min-h-0`,children:(0,L.jsx)(rt,{accounts:ae,processingIds:E,accountResults:N,accountSteps:re,onEdit:be,onDelete:Se,onAction:ce,onToggleAutoAccept:_e,onOpenBrowser:je})})]})]}),Fe&&(0,L.jsx)(it,{account:Fe,onSave:xe,onClose:()=>u(null)}),d&&(0,L.jsx)(at,{onClose:()=>f(!1)}),p&&(0,L.jsx)(ot,{onSave:Ee,onClose:()=>m(!1)}),h&&_&&(0,L.jsx)(st,{message:h,onConfirm:()=>{_(),g(null),v(null)},onCancel:()=>{g(null),v(null)}}),A&&(0,L.jsx)(`div`,{className:`fixed inset-0 bg-black/60 flex items-center justify-center z-50`,onMouseDown:de,children:(0,L.jsxs)(`div`,{className:`bg-dark-800 border border-dark-600 rounded-lg p-5 w-96 shadow-xl`,onMouseDown:e=>e.stopPropagation(),children:[(0,L.jsx)(`p`,{className:`text-sm text-gray-300 mb-3`,children:A.message}),(0,L.jsx)(`input`,{autoFocus:!0,value:te,onChange:e=>ne(e.target.value),onKeyDown:e=>e.key===`Enter`&&ue(),className:`w-full bg-dark-700 border border-dark-600 rounded px-3 py-2 text-sm mb-3`,placeholder:c(`prompt.enterValue`)}),(0,L.jsxs)(`div`,{className:`flex gap-2`,children:[(0,L.jsx)(`button`,{onClick:ue,className:`btn-primary flex-1`,children:`OK`}),(0,L.jsx)(`button`,{onClick:de,className:`btn-secondary flex-1`,children:c(`btn.cancel`)})]})]})})]})}function At(){let e=F(),[t,n]=(0,y.useState)(`secret`),[r,i]=(0,y.useState)(``),[a,o]=(0,y.useState)(``),[s,c]=(0,y.useState)(``),[l,u]=(0,y.useState)(null),[d,f]=(0,y.useState)(!1),p=(0,y.useRef)(null),m=j(e=>e.addToast),h=Re(e=>e.accounts),g=Re(e=>e.loadAccounts);(0,y.useEffect)(()=>{t===`account`&&h.length===0&&g()},[t]),(0,y.useEffect)(()=>{let e=e=>{p.current&&!p.current.contains(e.target)&&f(!1)};return document.addEventListener(`mousedown`,e),()=>document.removeEventListener(`mousedown`,e)},[]);let _=h.filter(e=>{let t=s.toLowerCase();return!t||e.login.toLowerCase().includes(t)||(e.steam_id??``).includes(t)}).slice(0,12),v=async()=>{try{if(t===`secret`){if(!r.trim())return;o((await D.generate2FA(r.trim())).code)}else{if(!l)return;o((await D.generate2FAByAccount(l.id)).code)}}catch(t){o(e(`tools.genErrorStr`)),m(`error`,e(`misc.genError`,{error:t instanceof Error?t.message:String(t)}))}},b=async()=>{if(!a||a===e(`tools.genErrorStr`))return;let t=await He(a);m(t?`success`:`error`,e(t?`toast.copied`:`toast.copyFailed`))},x=e=>`flex-1 py-1.5 text-xs font-medium rounded transition-colors ${t===e?`bg-dark-600 text-gray-100`:`text-gray-500 hover:text-gray-300`}`;return(0,L.jsxs)(`div`,{className:`bg-dark-800 border border-dark-600 rounded-xl overflow-hidden flex flex-col`,children:[(0,L.jsxs)(`div`,{className:`px-4 py-3 border-b border-dark-600 flex items-center gap-2`,children:[(0,L.jsx)(ve,{size:14,className:`text-accent shrink-0`}),(0,L.jsx)(`span`,{className:`text-sm font-semibold text-gray-100`,children:e(`tools.2faGenerator`)})]}),(0,L.jsxs)(`div`,{className:`px-4 pt-3 pb-2`,children:[(0,L.jsxs)(`div`,{className:`flex gap-1 bg-dark-900 rounded p-0.5 mb-3`,children:[(0,L.jsx)(`button`,{type:`button`,className:x(`secret`),onClick:()=>n(`secret`),children:`Shared secret`}),(0,L.jsx)(`button`,{type:`button`,className:x(`account`),onClick:()=>n(`account`),children:`Из Mafile`})]}),t===`secret`?(0,L.jsxs)(`div`,{className:`flex gap-2`,children:[(0,L.jsx)(`input`,{value:r,onChange:e=>i(e.target.value),onKeyDown:e=>e.key===`Enter`&&v(),placeholder:`shared_secret`,className:`flex-1 bg-dark-700 border border-dark-600 rounded px-3 py-1.5 text-sm`}),(0,L.jsx)(`button`,{type:`button`,onClick:v,className:`btn-primary text-sm px-3 py-1.5`,children:e(`btn.generate`)})]}):(0,L.jsxs)(`div`,{className:`flex gap-2`,children:[(0,L.jsxs)(`div`,{className:`relative flex-1`,ref:p,children:[(0,L.jsx)(`input`,{value:l?`${l.login}${l.steam_id?` · `+l.steam_id:``}`:s,onChange:e=>{c(e.target.value),u(null),f(!0)},onFocus:()=>f(!0),placeholder:`Логин или SteamID...`,className:`w-full bg-dark-700 border border-dark-600 rounded px-3 py-1.5 text-sm`}),d&&_.length>0&&(0,L.jsx)(`div`,{className:`absolute top-full mt-1 left-0 right-0 bg-dark-800 border border-dark-600 rounded-lg shadow-xl z-50 max-h-48 overflow-y-auto`,children:_.map(e=>(0,L.jsxs)(`button`,{type:`button`,className:`w-full text-left px-3 py-1.5 text-sm hover:bg-dark-600 transition-colors flex items-center justify-between gap-2`,onMouseDown:()=>{u(e),c(``),f(!1)},children:[(0,L.jsx)(`span`,{className:`text-gray-200 truncate`,children:e.login}),e.steam_id&&(0,L.jsx)(`span`,{className:`text-gray-500 text-xs shrink-0`,children:e.steam_id})]},e.id))})]}),(0,L.jsx)(`button`,{type:`button`,onClick:v,disabled:!l,className:`btn-primary text-sm px-3 py-1.5 disabled:opacity-40 disabled:cursor-not-allowed`,children:e(`btn.generate`)})]}),a&&(0,L.jsx)(`div`,{className:`mt-3 text-2xl font-mono text-accent cursor-pointer tabular-nums tracking-widest`,onClick:b,title:e(`tools.clickCopy`),children:a})]})]})}var jt=[{value:`login:pass@ip:port`,label:`login:pass@ip:port`},{value:`login:pass:ip:port`,label:`login:pass:ip:port`},{value:`ip:port@login:pass`,label:`ip:port@login:pass`},{value:`ip:port:login:pass`,label:`ip:port:login:pass`}];function Mt(e,t,n,r){let i=e.trim();if(!i)return null;let a=n;for(let e of[`socks5://`,`socks4://`,`http://`,`https://`])if(i.toLowerCase().startsWith(e)){a=e.replace(`://`,``),i=i.slice(e.length);break}if(!i.includes(`@`)&&i.split(`:`).length===2)return{address:i,protocol:a};let o=``,s=``,c=``,l=``;if(t===`custom`&&r&&r.length>=4){let e=Nt(i,r);if(!e)return null;({ip:o,port:s,login:c,pass:l}=e)}else if(t===`login:pass@ip:port`){let e=i.indexOf(`@`);if(e===-1)return null;let t=i.slice(0,e),n=i.slice(e+1),r=t.split(`:`),a=n.split(`:`);if(r.length<2||a.length<2)return null;c=r[0],l=r.slice(1).join(`:`),o=a[0],s=a[1]}else if(t===`login:pass:ip:port`){let e=i.split(`:`);if(e.length<4)return null;c=e[0],l=e[1],o=e[2],s=e[3]}else if(t===`ip:port@login:pass`){let e=i.indexOf(`@`);if(e===-1)return null;let t=i.slice(0,e),n=i.slice(e+1),r=t.split(`:`),a=n.split(`:`);if(r.length<2||a.length<2)return null;o=r[0],s=r[1],c=a[0],l=a.slice(1).join(`:`)}else if(t===`ip:port:login:pass`){let e=i.split(`:`);if(e.length<4)return null;o=e[0],s=e[1],c=e[2],l=e[3]}return!o||!s?null:{address:c&&l?`${c}:${l}@${o}:${s}`:`${o}:${s}`,protocol:a}}function Nt(e,t){let n=[],r=[];for(let e=0;ee.addToast),x=F(),S=async()=>{try{t(await D.getProxies())}catch{}};(0,y.useEffect)(()=>{S()},[]);let C=async e=>{let t=e.split(` +`).filter(e=>e.trim());if(!t.length)return;let n=f?`custom`:c,i=[];for(let e of t){let t=Mt(e,n,u,f?m:void 0);t&&i.push(t)}if(!i.length){b(`error`,x(`toast.proxyFormatError`));return}try{b(`success`,x(`toast.proxiesAdded`,{count:(await D.bulkAddProxies(i)).added})),r(``),await S()}catch(e){b(`error`,x(`misc.error`,{error:e instanceof Error?e.message:String(e)}))}},w=()=>C(n),T=async e=>{e.preventDefault(),_(!1);let t=e.dataTransfer.files[0];t&&await C(await t.text())},E=async e=>{let t=e.target.files?.[0];t&&(await C(await t.text()),e.target.value=``)},O=async e=>{try{await D.deleteProxy(e),await S()}catch(e){b(`error`,x(`misc.error`,{error:e instanceof Error?e.message:String(e)}))}},ee=async()=>{if(e.length&&window.confirm(x(`proxy.confirmDeleteAll`)))try{b(`success`,x(`toast.proxiesDeleted`,{count:(await D.deleteAllProxies()).deleted})),s(``),await S()}catch(e){b(`error`,x(`misc.error`,{error:e instanceof Error?e.message:String(e)}))}},k=async()=>{a(!0),s(``);try{let e=await D.checkProxies();s(x(`proxy.checkResult`,{total:e.total,alive:e.alive,dead:e.dead})),await S()}catch(e){b(`error`,x(`misc.proxyErrorCheck`,{error:e instanceof Error?e.message:String(e)}))}finally{a(!1)}},A=(e,t)=>{let n=[...m];n[e]=t,h(n)},M=m.join(``);return(0,L.jsxs)(`div`,{className:`bg-dark-800 border border-dark-600 rounded-lg p-4`,children:[(0,L.jsxs)(`h3`,{className:`font-semibold mb-3`,children:[(0,L.jsx)(pe,{size:14,className:`inline mr-1`}),x(`proxy.title`),` `,(0,L.jsx)(`span`,{className:`text-xs text-gray-500 ml-2`,children:x(`proxy.count`,{count:e.length})})]}),(0,L.jsxs)(`div`,{className:`mb-3 space-y-2`,children:[(0,L.jsxs)(`div`,{className:`flex items-center gap-3 flex-wrap`,children:[(0,L.jsx)(`span`,{className:`text-xs text-gray-400`,children:x(`proxy.protocol`)}),(0,L.jsxs)(`select`,{value:u,onChange:e=>d(e.target.value),className:`bg-dark-700 border border-dark-600 rounded px-2 py-1 text-xs`,children:[(0,L.jsx)(`option`,{value:`http`,children:`HTTP`}),(0,L.jsx)(`option`,{value:`socks5`,children:`SOCKS5`})]}),(0,L.jsx)(`span`,{className:`text-xs text-gray-400`,children:x(`proxy.format`)}),(0,L.jsxs)(`select`,{value:f?`__custom__`:c,onChange:e=>{e.target.value===`__custom__`?p(!0):(p(!1),l(e.target.value))},className:`bg-dark-700 border border-dark-600 rounded px-2 py-1 text-xs`,children:[jt.map(e=>(0,L.jsx)(`option`,{value:e.value,children:e.label},e.value)),(0,L.jsx)(`option`,{value:`__custom__`,children:x(`proxy.customFormat`)})]})]}),f&&(0,L.jsxs)(`div`,{className:`bg-dark-700 border border-dark-600 rounded p-3 space-y-2`,children:[(0,L.jsx)(`p`,{className:`text-xs text-gray-400`,children:x(`proxy.formatConstructor`)}),(0,L.jsx)(`div`,{className:`flex items-center gap-1 flex-wrap`,children:m.map((e,t)=>t%2==0?(0,L.jsx)(`select`,{value:e,onChange:e=>A(t,e.target.value),className:`bg-dark-600 border border-dark-500 rounded px-2 py-1 text-xs text-accent`,children:Pt.map(e=>(0,L.jsx)(`option`,{value:e,children:e},e))},t):(0,L.jsx)(`select`,{value:e,onChange:e=>A(t,e.target.value),className:`bg-dark-600 border border-dark-500 rounded px-1 py-1 text-xs text-yellow-400 w-10 text-center`,children:Ft.map(e=>(0,L.jsx)(`option`,{value:e,children:e},e))},t))}),(0,L.jsxs)(`p`,{className:`text-xs text-gray-500`,children:[x(`proxy.preview`),` `,(0,L.jsx)(`span`,{className:`text-gray-300`,children:M})]})]})]}),(0,L.jsxs)(`div`,{onDragOver:e=>{e.preventDefault(),_(!0)},onDragLeave:()=>_(!1),onDrop:T,onClick:()=>v.current?.click(),className:`border-2 border-dashed rounded-lg p-4 mb-2 text-center cursor-pointer transition-colors ${g?`border-accent bg-accent/10`:`border-dark-500 hover:border-accent`}`,children:[(0,L.jsx)(ce,{size:24,className:`mx-auto mb-1 text-gray-500`}),(0,L.jsx)(`p`,{className:`text-xs text-gray-400`,children:x(`proxy.dragFile`)})]}),(0,L.jsx)(`input`,{ref:v,type:`file`,accept:`.txt`,className:`hidden`,onChange:E}),(0,L.jsx)(`textarea`,{value:n,onChange:e=>r(e.target.value),rows:4,placeholder:x(`proxy.perLine`,{format:f?M:c}),className:`w-full bg-dark-700 border border-dark-600 rounded px-3 py-2 text-sm mb-2 resize-y`}),(0,L.jsxs)(`div`,{className:`flex gap-2 mb-3 flex-wrap`,children:[(0,L.jsx)(`button`,{onClick:w,className:`btn-primary`,children:x(`proxy.addBtn`)}),(0,L.jsx)(`button`,{onClick:k,disabled:i,className:`btn-secondary`,children:x(i?`proxy.checking`:`proxy.checkAll`)}),e.length>0&&(0,L.jsx)(`button`,{onClick:ee,className:`btn-danger text-xs`,children:x(`proxy.deleteAllBtn`)})]}),o&&(0,L.jsx)(`p`,{className:`text-xs text-gray-300 mb-3`,children:o}),e.length>0&&(()=>{let t=e.filter(e=>e.is_alive).length,n=e.length-t;return(0,L.jsxs)(`p`,{className:`text-xs text-gray-400 mb-3`,children:[(0,L.jsxs)(`span`,{className:`text-green-400`,children:[`● `,t]}),(0,L.jsx)(`span`,{className:`mx-2`,children:`/`}),(0,L.jsxs)(`span`,{className:`text-red-400`,children:[`● `,n]}),(0,L.jsx)(`span`,{className:`mx-2`,children:`/`}),(0,L.jsx)(`span`,{children:e.length})]})})(),(0,L.jsx)(`div`,{className:`max-h-48 overflow-y-auto space-y-1`,children:e.map(e=>(0,L.jsxs)(`div`,{className:`flex items-center justify-between text-xs gap-2 group px-2 py-1 rounded hover:bg-dark-700`,children:[(0,L.jsxs)(`span`,{className:`${e.is_alive?`text-green-400`:`text-red-400`} min-w-0 truncate`,children:[e.protocol,`://`,e.address]}),(0,L.jsx)(`button`,{onClick:()=>O(e.id),className:`text-red-400 hover:bg-red-400/20 rounded px-2 py-0.5 text-sm font-medium shrink-0 transition`,title:x(`proxy.deleteProxy`),children:`✕`})]},e.id))})]})}function Lt({checked:e,onChange:t}){return(0,L.jsxs)(`label`,{className:`toggle-switch`,children:[(0,L.jsx)(`input`,{type:`checkbox`,checked:e,onChange:e=>t(e.target.checked)}),(0,L.jsx)(`span`,{className:`toggle-track`})]})}function Rt({label:e,desc:t,checked:n,onChange:r}){return(0,L.jsxs)(`div`,{className:`flex items-center justify-between gap-4 py-2.5`,children:[(0,L.jsxs)(`div`,{className:`min-w-0`,children:[(0,L.jsx)(`p`,{className:`text-sm text-gray-200 leading-tight`,children:e}),t&&(0,L.jsx)(`p`,{className:`text-xs text-gray-500 mt-0.5`,children:t})]}),(0,L.jsx)(Lt,{checked:n,onChange:r})]})}function zt({value:e,onChange:t}){let[n,r]=(0,y.useState)(!1),[i,a]=(0,y.useState)(``),o=(0,y.useRef)(null),s=e=>Math.max(1,e),c=()=>{t(s(parseInt(i)||e)),r(!1)},l=`h-6 px-1.5 rounded bg-dark-700 border border-dark-600 text-gray-300 hover:bg-dark-600 text-xs flex items-center justify-center select-none leading-none`;return(0,L.jsxs)(`div`,{className:`flex items-center gap-3 pt-3 mt-0.5`,children:[(0,L.jsx)(`span`,{className:`text-sm text-gray-400 flex-1`,title:`Двойной клик на числе для ручного ввода`,children:`Max threads`}),(0,L.jsxs)(`div`,{className:`flex items-center gap-1`,children:[(0,L.jsx)(`button`,{type:`button`,className:l,onClick:()=>t(s(e-10)),children:`-10`}),(0,L.jsx)(`button`,{type:`button`,className:l,onClick:()=>t(s(e-1)),children:`-1`}),n?(0,L.jsx)(`input`,{ref:o,autoFocus:!0,value:i,onChange:e=>a(e.target.value),onBlur:c,onKeyDown:e=>{e.key===`Enter`&&c(),e.key===`Escape`&&r(!1)},className:`w-10 text-center text-sm font-mono bg-dark-900 border border-accent rounded outline-none text-gray-100 py-0.5`}):(0,L.jsx)(`span`,{className:`w-10 text-center text-sm font-mono text-gray-100 cursor-pointer select-none`,onDoubleClick:()=>{a(String(e)),r(!0)},title:`Двойной клик для ручного ввода`,children:e}),(0,L.jsx)(`button`,{type:`button`,className:l,onClick:()=>t(s(e+1)),children:`+1`}),(0,L.jsx)(`button`,{type:`button`,className:l,onClick:()=>t(s(e+10)),children:`+10`})]})]})}function Bt(){let e=F(),[t,n]=(0,y.useState)({fetch_profile:!0,check_ban:!0,max_threads:5,auto_revalidate_browser:!0}),r=j(e=>e.hidePasswords),i=j(e=>e.setHidePasswords),a=j(e=>e.addToast);return(0,y.useEffect)(()=>{D.getValidationSettings().then(n).catch(()=>{})},[]),(0,L.jsxs)(`div`,{className:`bg-dark-800 border border-dark-600 rounded-xl overflow-hidden flex flex-col`,children:[(0,L.jsxs)(`div`,{className:`px-5 py-3.5 border-b border-dark-600 flex items-center gap-2.5`,children:[(0,L.jsx)(be,{size:15,className:`text-accent shrink-0`}),(0,L.jsx)(`span`,{className:`text-sm font-semibold text-gray-100`,children:`Settings`})]}),(0,L.jsxs)(`div`,{className:`px-5 pt-4 pb-3 border-b border-dark-600`,children:[(0,L.jsx)(`p`,{className:`text-[10px] font-semibold text-gray-500 uppercase tracking-widest mb-0.5`,children:e(`tools.validationSettings`)}),(0,L.jsxs)(`div`,{className:`divide-y divide-dark-600/60`,children:[(0,L.jsx)(Rt,{label:e(`tools.loadProfile`),checked:t.fetch_profile,onChange:e=>n({...t,fetch_profile:e})}),(0,L.jsx)(Rt,{label:e(`tools.checkBans`),checked:t.check_ban,onChange:e=>n({...t,check_ban:e})}),(0,L.jsx)(Rt,{label:e(`tools.autoRevalidateBrowser`),checked:t.auto_revalidate_browser,onChange:e=>n({...t,auto_revalidate_browser:e})})]}),(0,L.jsx)(zt,{value:t.max_threads,onChange:e=>n(t=>({...t,max_threads:e}))})]}),(0,L.jsxs)(`div`,{className:`px-5 pt-4 pb-3`,children:[(0,L.jsx)(`p`,{className:`text-[10px] font-semibold text-gray-500 uppercase tracking-widest mb-0.5`,children:e(`settings.display`)}),(0,L.jsx)(Rt,{label:e(`settings.hidePasswords`),checked:r,onChange:i})]}),(0,L.jsx)(`div`,{className:`px-5 py-3 border-t border-dark-600 flex justify-end`,children:(0,L.jsx)(`button`,{onClick:async()=>{try{await D.updateValidationSettings(t),a(`success`,e(`toast.settingsSaved`))}catch(t){a(`error`,e(`misc.error`,{error:t instanceof Error?t.message:String(t)}))}},className:`btn-primary`,children:e(`btn.save`)})})]})}function Vt(){return(0,L.jsxs)(`div`,{className:`max-w-5xl mx-auto flex flex-col gap-4`,children:[(0,L.jsxs)(`div`,{className:`grid grid-cols-1 md:grid-cols-2 gap-4`,children:[(0,L.jsx)(At,{}),(0,L.jsx)(Bt,{})]}),(0,L.jsx)(It,{})]})}var Ht=[`shared_secret`,`serial_number`,`revocation_code`,`uri`,`account_name`,`token_gid`,`identity_secret`,`secret_1`,`device_id`,`server_time`,`fully_enrolled`],Ut=[`SessionID`,`AccessToken`,`RefreshToken`,`SteamID`,`SteamLoginSecure`],Wt=[{value:`{username}`,label:`username`},{value:`{steamid}`,label:`steamid`}];function Gt({accountIds:e,onExport:t}){let n=F(),[r,i]=(0,y.useState)(new Set(Ht)),[a,o]=(0,y.useState)(new Set(Ut)),[s,c]=(0,y.useState)(`per_account_folder`),[l,u]=(0,y.useState)(`{username}`),[d,f]=(0,y.useState)(`{steamid}.mafile`),[p,m]=(0,y.useState)(`{username}.txt`),[h,g]=(0,y.useState)(!1),[_,v]=(0,y.useState)(!1),[b,x]=(0,y.useState)(!1),[S,C]=(0,y.useState)(`{login}:{password}:{email}:{email_password}`),w=(e,t,n)=>{let r=new Set(e);r.has(t)?r.delete(t):r.add(t),n(r)},T=(e,t)=>{e(e=>e+t)};return(0,L.jsxs)(`div`,{className:`bg-dark-800 border border-dark-600 rounded-lg p-4 space-y-4`,children:[(0,L.jsxs)(`h4`,{className:`font-semibold text-sm`,children:[(0,L.jsx)(we,{size:14,className:`inline mr-1`}),n(`mafile.exportSettings`)]}),(0,L.jsxs)(`div`,{children:[(0,L.jsx)(`p`,{className:`text-xs text-gray-400 mb-1`,children:n(`mafile.exportFormat`)}),(0,L.jsxs)(`select`,{value:s,onChange:e=>{let t=e.target.value;c(t),t===`single_file`&&(v(!1),x(!1))},className:`bg-dark-700 border border-dark-600 rounded px-2 py-1 text-sm`,children:[(0,L.jsx)(`option`,{value:`flat_mafiles`,children:n(`mafile.flatFiles`)}),(0,L.jsx)(`option`,{value:`per_account_folder`,children:n(`mafile.perAccountFolder`)}),(0,L.jsx)(`option`,{value:`single_file`,children:n(`mafile.singleFile`)})]})]}),s!==`single_file`&&(0,L.jsxs)(`div`,{className:`space-y-3 border border-dark-600 rounded-lg p-3`,children:[(0,L.jsxs)(`p`,{className:`text-xs text-gray-400 font-medium`,children:[n(`mafile.variables`),` `,(0,L.jsx)(`code`,{className:`text-accent`,children:`{username}`}),`, `,(0,L.jsx)(`code`,{className:`text-accent`,children:`{steamid}`})]}),s===`per_account_folder`&&(0,L.jsxs)(`div`,{children:[(0,L.jsx)(`label`,{className:`text-xs text-gray-400 block mb-1`,children:n(`mafile.folderName`)}),(0,L.jsxs)(`div`,{className:`flex gap-1`,children:[(0,L.jsx)(`input`,{value:l,onChange:e=>u(e.target.value),className:`flex-1 bg-dark-700 border border-dark-600 rounded px-2 py-1 text-sm font-mono`}),Wt.map(e=>(0,L.jsx)(`button`,{onClick:()=>T(u,e.value),className:`btn-secondary px-2 py-1 text-xs`,title:n(`mafile.insert`,{value:e.value}),children:e.label},e.value))]})]}),(0,L.jsxs)(`div`,{children:[(0,L.jsx)(`label`,{className:`text-xs text-gray-400 block mb-1`,children:n(`mafile.mafileName`)}),(0,L.jsxs)(`div`,{className:`flex gap-1`,children:[(0,L.jsx)(`input`,{value:d,onChange:e=>f(e.target.value),className:`flex-1 bg-dark-700 border border-dark-600 rounded px-2 py-1 text-sm font-mono`}),Wt.map(e=>(0,L.jsx)(`button`,{onClick:()=>T(f,e.value),className:`btn-secondary px-2 py-1 text-xs`,title:n(`mafile.insert`,{value:e.value}),children:e.label},e.value))]})]})]}),(0,L.jsxs)(`div`,{className:`space-y-2 border border-dark-600 rounded-lg p-3`,children:[(0,L.jsxs)(`p`,{className:`text-xs text-gray-400 font-medium`,children:[(0,L.jsx)(Te,{size:12,className:`inline mr-1`}),n(`mafile.txtSettings`)]}),s!==`single_file`&&(0,L.jsxs)(L.Fragment,{children:[(0,L.jsxs)(`label`,{className:`flex items-center gap-2 text-sm cursor-pointer`,children:[(0,L.jsx)(`input`,{type:`checkbox`,checked:_,onChange:e=>{v(e.target.checked),e.target.checked||x(!1)}}),n(`mafile.addGlobalTxt`),` `,(0,L.jsx)(`code`,{className:`text-accent text-xs`,children:`accounts.txt`}),` `,n(`mafile.withAllAccounts`)]}),s===`per_account_folder`&&(0,L.jsxs)(`label`,{className:`flex items-center gap-2 text-sm cursor-pointer`,children:[(0,L.jsx)(`input`,{type:`checkbox`,checked:h,onChange:e=>g(e.target.checked)}),n(`mafile.addTxtPerFolder`)]}),(0,L.jsxs)(`label`,{className:`flex items-center gap-2 text-sm ${_?`cursor-pointer`:`cursor-not-allowed opacity-40`}`,children:[(0,L.jsx)(`input`,{type:`checkbox`,checked:b,disabled:!_,onChange:e=>x(e.target.checked)}),n(`mafile.skipFolders`)]})]}),(_||h||s===`single_file`)&&(0,L.jsxs)(L.Fragment,{children:[s===`per_account_folder`&&h&&(0,L.jsxs)(`div`,{children:[(0,L.jsx)(`label`,{className:`text-xs text-gray-400 block mb-1`,children:n(`mafile.txtFolderName`)}),(0,L.jsxs)(`div`,{className:`flex gap-1`,children:[(0,L.jsx)(`input`,{value:p,onChange:e=>m(e.target.value),className:`flex-1 bg-dark-700 border border-dark-600 rounded px-2 py-1 text-sm font-mono`}),Wt.map(e=>(0,L.jsx)(`button`,{onClick:()=>T(m,e.value),className:`btn-secondary px-2 py-1 text-xs`,children:e.label},e.value))]})]}),(0,L.jsxs)(`div`,{children:[(0,L.jsx)(`label`,{className:`text-xs text-gray-400 block mb-1`,children:n(`mafile.txtLineFormat`)}),(0,L.jsx)(`input`,{value:S,onChange:e=>C(e.target.value),className:`w-full bg-dark-700 border border-dark-600 rounded px-2 py-1 text-sm font-mono`}),(0,L.jsxs)(`p`,{className:`text-xs text-gray-500 mt-1`,children:[n(`mafile.variables`),` `,(0,L.jsx)(`code`,{className:`text-accent`,children:`{login}`}),` `,(0,L.jsx)(`code`,{className:`text-accent`,children:`{password}`}),` `,(0,L.jsx)(`code`,{className:`text-accent`,children:`{email}`}),` `,(0,L.jsx)(`code`,{className:`text-accent`,children:`{email_password}`}),` `,(0,L.jsx)(`code`,{className:`text-accent`,children:`{steam_id}`}),` `,(0,L.jsx)(`code`,{className:`text-accent`,children:`{proxy}`}),` `,(0,L.jsx)(`code`,{className:`text-accent`,children:`{mafile}`})]})]})]})]}),(0,L.jsxs)(`details`,{className:`group`,children:[(0,L.jsxs)(`summary`,{className:`text-xs text-gray-400 cursor-pointer hover:text-gray-300 transition-colors flex items-center gap-1`,children:[(0,L.jsx)(Pe,{size:12,className:`transition-transform group-open:rotate-90`}),n(`mafile.mafileFields`),` (`,r.size,`/`,Ht.length,`)`]}),(0,L.jsx)(`div`,{className:`flex flex-wrap gap-2 mt-2`,children:Ht.map(e=>(0,L.jsxs)(`label`,{className:`flex items-center gap-1 text-xs cursor-pointer`,children:[(0,L.jsx)(`input`,{type:`checkbox`,checked:r.has(e),onChange:()=>w(r,e,i)}),e]},e))})]}),(0,L.jsxs)(`details`,{className:`group`,children:[(0,L.jsxs)(`summary`,{className:`text-xs text-gray-400 cursor-pointer hover:text-gray-300 transition-colors flex items-center gap-1`,children:[(0,L.jsx)(Pe,{size:12,className:`transition-transform group-open:rotate-90`}),n(`mafile.sessionFields`),` (`,a.size,`/`,Ut.length,`)`]}),(0,L.jsx)(`div`,{className:`flex flex-wrap gap-2 mt-2`,children:Ut.map(e=>(0,L.jsxs)(`label`,{className:`flex items-center gap-1 text-xs cursor-pointer`,children:[(0,L.jsx)(`input`,{type:`checkbox`,checked:a.has(e),onChange:()=>w(a,e,o)}),e]},e))})]}),(0,L.jsxs)(`button`,{onClick:()=>{t({fields:[...r],session_fields:[...a],format:s,account_ids:e,folder_name_template:l,mafile_name_template:d,txt_name_template:p,include_txt_per_folder:h,include_global_txt:_,skip_folders:b,txt_format:S})},disabled:e.length===0,className:e.length===0?`btn-primary opacity-50 cursor-not-allowed`:`btn-primary`,children:[(0,L.jsx)(se,{size:14,className:`inline mr-1`}),n(`mafile.export`),` (`,e.length>0?e.length:n(`mafile.noAccounts`),`)`]})]})}function Kt(){let e=F(),t=Re(e=>e.accounts),n=Re(e=>e.mafileManagerIds),r=Re(e=>e.clearMafileManager),i=j(e=>e.addToast),a=t.filter(e=>n.has(e.id)),o=async t=>{try{let n=await D.exportZip(t),r=URL.createObjectURL(n),a=document.createElement(`a`);a.href=r,a.download=`mafiles_export.zip`,a.click(),URL.revokeObjectURL(r),i(`success`,e(`toast.exportDone`))}catch(t){i(`error`,e(`misc.exportError`,{error:t instanceof Error?t.message:String(t)}))}};return(0,L.jsxs)(`div`,{className:`space-y-4 h-full overflow-y-auto pr-1`,children:[a.length>0&&(0,L.jsxs)(`div`,{className:`bg-dark-800 border border-dark-600 rounded-lg p-4`,children:[(0,L.jsxs)(`div`,{className:`flex items-center justify-between mb-3`,children:[(0,L.jsxs)(`h3`,{className:`font-semibold`,children:[(0,L.jsx)(Ee,{size:14,className:`inline mr-1`}),e(`mafile.sentAccounts`),` (`,a.length,`)`]}),(0,L.jsx)(`button`,{onClick:r,className:`btn-danger-outline text-xs`,children:e(`mafile.clearBtn`)})]}),(0,L.jsx)(`div`,{className:`max-h-48 overflow-y-auto space-y-1`,children:a.map(e=>(0,L.jsxs)(`div`,{className:`flex items-center gap-2 text-xs text-gray-300 min-w-0`,children:[e.avatar_url&&(0,L.jsx)(`img`,{src:e.avatar_url,className:`avatar-sm shrink-0`,alt:``}),(0,L.jsx)(`span`,{className:`truncate`,children:e.login}),(0,L.jsx)(`span`,{className:`text-gray-500 truncate`,children:e.steam_id||``}),(0,L.jsx)(`span`,{className:e.mafile_path?`text-green-400`:`text-gray-500`,children:e.mafile_path?`✓ mafile`:`—`})]},e.id))})]}),(0,L.jsx)(Gt,{accountIds:[...n],onExport:o})]})}function qt(e,t,n=!0){let r=(0,y.useRef)(t);r.current=t,(0,y.useEffect)(()=>{if(!n)return;let t=new EventSource(e);return t.onmessage=e=>{try{r.current(JSON.parse(e.data))}catch{}},t.onerror=()=>{t.close()},()=>t.close()},[e,n])}function Jt(){let[e,t]=(0,y.useState)([]),n=(0,y.useRef)(null),r=(0,y.useRef)(!0);(0,y.useEffect)(()=>{D.getLogs().then(t).catch(()=>{})},[]),qt(`/api/logs/stream`,e=>{t(t=>{let n=[...t,e];return n.length>500&&n.splice(0,n.length-500),n})}),(0,y.useEffect)(()=>{r.current&&n.current&&(n.current.scrollTop=n.current.scrollHeight)},[e]);let i=()=>{if(!n.current)return;let{scrollTop:e,scrollHeight:t,clientHeight:i}=n.current;r.current=t-e-i<40},a=e=>{switch(e){case`error`:case`critical`:return`text-red-400`;case`warning`:return`text-yellow-400`;case`success`:return`text-green-400`;case`debug`:return`text-gray-500`;default:return`text-gray-300`}};return(0,L.jsxs)(`div`,{className:`bg-dark-800 border border-dark-600 rounded-lg flex flex-col`,style:{maxHeight:`calc(100vh - 200px)`},children:[(0,L.jsxs)(`div`,{className:`px-3 py-2 border-b border-dark-600 flex items-center justify-between shrink-0`,children:[(0,L.jsx)(`h3`,{className:`font-semibold text-sm`,children:`📜 Лог`}),(0,L.jsx)(`button`,{onClick:()=>t([]),className:`text-xs text-gray-500 hover:text-gray-300 transition-colors`,children:`Очистить`})]}),(0,L.jsxs)(`div`,{ref:n,onScroll:i,className:`flex-1 overflow-y-auto min-h-0 p-2 font-mono text-xs space-y-px`,children:[e.map((e,t)=>(0,L.jsxs)(`div`,{className:`flex gap-2`,children:[(0,L.jsx)(`span`,{className:`text-gray-600 shrink-0`,children:e.ts}),(0,L.jsx)(`span`,{className:`shrink-0 w-3 text-center ${a(e.level)}`,children:e.icon}),(0,L.jsx)(`span`,{className:`${a(e.level)} break-all min-w-0`,children:e.msg})]},t)),e.length===0&&(0,L.jsx)(`p`,{className:`text-gray-600 text-center py-4`,children:`Нет логов`})]})]})}function Yt(){return(0,L.jsx)(Jt,{})}function Xt(){let e=j(e=>e.activeTab),t=j(e=>e.loadDisplaySettings),n=j(e=>e.loadColumnSettings),r=j(e=>e.loadLogpassColumnSettings);return(0,y.useEffect)(()=>{t(),n(),r()},[]),(0,L.jsxs)(`div`,{className:`h-dvh bg-dark-900 text-gray-300 flex flex-col overflow-hidden`,children:[(0,L.jsx)(oe,{}),(0,L.jsx)(Ie,{}),(0,L.jsxs)(`main`,{className:`flex-1 min-h-0 p-4 w-full overflow-y-auto`,children:[e===`accounts`&&(0,L.jsx)(kt,{}),e===`tools`&&(0,L.jsx)(Vt,{}),e===`mafiles`&&(0,L.jsx)(Kt,{}),e===`logs`&&(0,L.jsx)(Yt,{})]}),(0,L.jsx)(Le,{})]})}var Zt=class extends y.Component{state={error:null};static getDerivedStateFromError(e){return{error:e}}componentDidCatch(e,t){console.error(`React crash:`,e,t)}render(){let{error:e}=this.state;return e?(0,L.jsxs)(`div`,{style:{padding:32,fontFamily:`monospace`,background:`#0f0f0f`,color:`#f87171`,minHeight:`100vh`},children:[(0,L.jsx)(`h2`,{style:{fontSize:18,marginBottom:12},children:`⚠ SteamPanel failed to start`}),(0,L.jsxs)(`pre`,{style:{whiteSpace:`pre-wrap`,fontSize:13,color:`#fca5a5`},children:[e.message,` + +`,e.stack]}),(0,L.jsx)(`p`,{style:{marginTop:16,color:`#9ca3af`,fontSize:12},children:`Open browser console (F12) for more details, or report this error.`})]}):this.props.children}};(0,w.createRoot)(document.getElementById(`root`)).render((0,L.jsx)(y.StrictMode,{children:(0,L.jsx)(Zt,{children:(0,L.jsx)(Xt,{})})})); \ No newline at end of file diff --git a/app/static/assets/index-Cm89176E.css b/app/static/assets/index-Cm89176E.css new file mode 100644 index 0000000..5c84a32 --- /dev/null +++ b/app/static/assets/index-Cm89176E.css @@ -0,0 +1,2 @@ +/*! tailwindcss v4.2.1 | MIT License | https://tailwindcss.com */ +@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-space-y-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-400:oklch(70.4% .191 22.216);--color-red-500:oklch(63.7% .237 25.331);--color-red-600:oklch(57.7% .245 27.325);--color-yellow-400:oklch(85.2% .199 91.936);--color-yellow-500:oklch(79.5% .184 86.047);--color-green-400:oklch(79.2% .209 151.711);--color-green-500:oklch(72.3% .219 149.579);--color-blue-300:oklch(80.9% .105 251.813);--color-blue-400:oklch(70.7% .165 254.624);--color-blue-500:oklch(62.3% .214 259.815);--color-blue-600:oklch(54.6% .245 262.881);--color-gray-100:oklch(96.7% .003 264.542);--color-gray-200:oklch(92.8% .006 264.531);--color-gray-300:oklch(87.2% .01 258.338);--color-gray-400:oklch(70.7% .022 261.325);--color-gray-500:oklch(55.1% .027 264.364);--color-gray-600:oklch(44.6% .03 256.802);--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-md:28rem;--container-lg:32rem;--container-5xl:64rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-wide:.025em;--tracking-wider:.05em;--tracking-widest:.1em;--leading-tight:1.25;--radius-lg:.5rem;--radius-xl:.75rem;--animate-spin:spin 1s linear infinite;--blur-sm:8px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-dark-900:#0f0f0f;--color-dark-800:#1a1a1a;--color-dark-700:#252525;--color-dark-600:#333;--color-accent:#4f8cff;--color-accent-hover:#6ba0ff;--color-accent-dim:#3a6fd8}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.inset-0{inset:calc(var(--spacing) * 0)}.start{inset-inline-start:var(--spacing)}.top-0{top:calc(var(--spacing) * 0)}.top-full{top:100%}.right-0{right:calc(var(--spacing) * 0)}.left-0{left:calc(var(--spacing) * 0)}.z-10{z-index:10}.z-50{z-index:50}.mx-2{margin-inline:calc(var(--spacing) * 2)}.mx-4{margin-inline:calc(var(--spacing) * 4)}.mx-auto{margin-inline:auto}.my-1{margin-block:calc(var(--spacing) * 1)}.mt-0\.5{margin-top:calc(var(--spacing) * .5)}.mt-1{margin-top:calc(var(--spacing) * 1)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mr-0\.5{margin-right:calc(var(--spacing) * .5)}.mr-1{margin-right:calc(var(--spacing) * 1)}.-mb-px{margin-bottom:-1px}.mb-0\.5{margin-bottom:calc(var(--spacing) * .5)}.mb-1{margin-bottom:calc(var(--spacing) * 1)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-5{margin-bottom:calc(var(--spacing) * 5)}.ml-1\.5{margin-left:calc(var(--spacing) * 1.5)}.ml-2{margin-left:calc(var(--spacing) * 2)}.ml-auto{margin-left:auto}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.h-2\.5{height:calc(var(--spacing) * 2.5)}.h-4{height:calc(var(--spacing) * 4)}.h-5{height:calc(var(--spacing) * 5)}.h-6{height:calc(var(--spacing) * 6)}.h-7{height:calc(var(--spacing) * 7)}.h-dvh{height:100dvh}.h-full{height:100%}.max-h-48{max-height:calc(var(--spacing) * 48)}.max-h-\[320px\]{max-height:320px}.min-h-0{min-height:calc(var(--spacing) * 0)}.min-h-\[300px\]{min-height:300px}.w-3{width:calc(var(--spacing) * 3)}.w-4{width:calc(var(--spacing) * 4)}.w-5{width:calc(var(--spacing) * 5)}.w-7{width:calc(var(--spacing) * 7)}.w-10{width:calc(var(--spacing) * 10)}.w-28{width:calc(var(--spacing) * 28)}.w-36{width:calc(var(--spacing) * 36)}.w-80{width:calc(var(--spacing) * 80)}.w-96{width:calc(var(--spacing) * 96)}.w-full{width:100%}.max-w-5xl{max-width:var(--container-5xl)}.max-w-\[180px\]{max-width:180px}.max-w-\[200px\]{max-width:200px}.max-w-lg{max-width:var(--container-lg)}.max-w-md{max-width:var(--container-md)}.max-w-none{max-width:none}.min-w-0{min-width:calc(var(--spacing) * 0)}.min-w-\[100px\]{min-width:100px}.min-w-\[160px\]{min-width:160px}.min-w-\[170px\]{min-width:170px}.flex-1{flex:1}.shrink-0{flex-shrink:0}.rotate-180{rotate:180deg}.animate-spin{animation:var(--animate-spin)}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.resize-none{resize:none}.resize-y{resize:vertical}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-1{gap:calc(var(--spacing) * 1)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-2\.5{gap:calc(var(--spacing) * 2.5)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-px>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(1px * var(--tw-space-y-reverse));margin-block-end:calc(1px * calc(1 - var(--tw-space-y-reverse)))}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px * var(--tw-divide-y-reverse));border-bottom-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-dark-600\/60>:not(:last-child)){border-color:#3339}@supports (color:color-mix(in lab, red, red)){:where(.divide-dark-600\/60>:not(:last-child)){border-color:color-mix(in oklab, var(--color-dark-600) 60%, transparent)}}.self-start{align-self:flex-start}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-xl{border-radius:var(--radius-xl)}.border{border-style:var(--tw-border-style);border-width:1px}.border-2{border-style:var(--tw-border-style);border-width:2px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-b-2{border-bottom-style:var(--tw-border-style);border-bottom-width:2px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.border-accent{border-color:var(--color-accent)}.border-blue-500\/30{border-color:#3080ff4d}@supports (color:color-mix(in lab, red, red)){.border-blue-500\/30{border-color:color-mix(in oklab, var(--color-blue-500) 30%, transparent)}}.border-dark-600{border-color:var(--color-dark-600)}.border-dark-700{border-color:var(--color-dark-700)}.border-transparent{border-color:#0000}.border-yellow-500\/40{border-color:#edb20066}@supports (color:color-mix(in lab, red, red)){.border-yellow-500\/40{border-color:color-mix(in oklab, var(--color-yellow-500) 40%, transparent)}}.border-yellow-500\/50{border-color:#edb20080}@supports (color:color-mix(in lab, red, red)){.border-yellow-500\/50{border-color:color-mix(in oklab, var(--color-yellow-500) 50%, transparent)}}.border-t-transparent{border-top-color:#0000}.bg-accent{background-color:var(--color-accent)}.bg-accent\/10{background-color:#4f8cff1a}@supports (color:color-mix(in lab, red, red)){.bg-accent\/10{background-color:color-mix(in oklab, var(--color-accent) 10%, transparent)}}.bg-accent\/20{background-color:#4f8cff33}@supports (color:color-mix(in lab, red, red)){.bg-accent\/20{background-color:color-mix(in oklab, var(--color-accent) 20%, transparent)}}.bg-black\/60{background-color:#0009}@supports (color:color-mix(in lab, red, red)){.bg-black\/60{background-color:color-mix(in oklab, var(--color-black) 60%, transparent)}}.bg-blue-600\/20{background-color:#155dfc33}@supports (color:color-mix(in lab, red, red)){.bg-blue-600\/20{background-color:color-mix(in oklab, var(--color-blue-600) 20%, transparent)}}.bg-dark-600{background-color:var(--color-dark-600)}.bg-dark-600\/50{background-color:#33333380}@supports (color:color-mix(in lab, red, red)){.bg-dark-600\/50{background-color:color-mix(in oklab, var(--color-dark-600) 50%, transparent)}}.bg-dark-700{background-color:var(--color-dark-700)}.bg-dark-800{background-color:var(--color-dark-800)}.bg-dark-900{background-color:var(--color-dark-900)}.bg-dark-900\/80{background-color:#0f0f0fcc}@supports (color:color-mix(in lab, red, red)){.bg-dark-900\/80{background-color:color-mix(in oklab, var(--color-dark-900) 80%, transparent)}}.bg-green-500{background-color:var(--color-green-500)}.bg-red-500{background-color:var(--color-red-500)}.fill-white{fill:var(--color-white)}.p-0\.5{padding:calc(var(--spacing) * .5)}.p-2{padding:calc(var(--spacing) * 2)}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.p-5{padding:calc(var(--spacing) * 5)}.p-6{padding:calc(var(--spacing) * 6)}.px-1{padding-inline:calc(var(--spacing) * 1)}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-5{padding-inline:calc(var(--spacing) * 5)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:calc(var(--spacing) * 1)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-2\.5{padding-block:calc(var(--spacing) * 2.5)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-3\.5{padding-block:calc(var(--spacing) * 3.5)}.py-4{padding-block:calc(var(--spacing) * 4)}.py-8{padding-block:calc(var(--spacing) * 8)}.py-12{padding-block:calc(var(--spacing) * 12)}.pt-3{padding-top:calc(var(--spacing) * 3)}.pt-4{padding-top:calc(var(--spacing) * 4)}.pr-1{padding-right:calc(var(--spacing) * 1)}.pb-2{padding-bottom:calc(var(--spacing) * 2)}.pb-3{padding-bottom:calc(var(--spacing) * 3)}.text-center{text-align:center}.text-left{text-align:left}.align-middle{vertical-align:middle}.font-mono{font-family:var(--font-mono)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[10px\]{font-size:10px}.leading-none{--tw-leading:1;line-height:1}.leading-tight{--tw-leading:var(--leading-tight);line-height:var(--leading-tight)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.tracking-wider{--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider)}.tracking-widest{--tw-tracking:var(--tracking-widest);letter-spacing:var(--tracking-widest)}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.whitespace-normal{white-space:normal}.whitespace-nowrap{white-space:nowrap}.text-accent{color:var(--color-accent)}.text-blue-400{color:var(--color-blue-400)}.text-gray-100{color:var(--color-gray-100)}.text-gray-200{color:var(--color-gray-200)}.text-gray-300{color:var(--color-gray-300)}.text-gray-400{color:var(--color-gray-400)}.text-gray-500{color:var(--color-gray-500)}.text-gray-600{color:var(--color-gray-600)}.text-green-400{color:var(--color-green-400)}.text-red-400{color:var(--color-red-400)}.text-white{color:var(--color-white)}.text-yellow-400{color:var(--color-yellow-400)}.text-yellow-500\/80{color:#edb200cc}@supports (color:color-mix(in lab, red, red)){.text-yellow-500\/80{color:color-mix(in oklab, var(--color-yellow-500) 80%, transparent)}}.uppercase{text-transform:uppercase}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.accent-yellow-500{accent-color:var(--color-yellow-500)}.opacity-40{opacity:.4}.opacity-50{opacity:.5}.opacity-80{opacity:.8}.shadow-2xl{--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a), 0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.backdrop-blur-sm{--tw-backdrop-blur:blur(var(--blur-sm));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-300{--tw-duration:.3s;transition-duration:.3s}.outline-none{--tw-outline-style:none;outline-style:none}.select-none{-webkit-user-select:none;user-select:none}.group-open\:rotate-90:is(:where(.group):is([open],:popover-open,:open) *){rotate:90deg}@media (hover:hover){.group-hover\:text-gray-300:is(:where(.group):hover *){color:var(--color-gray-300)}}.placeholder\:text-gray-500::placeholder{color:var(--color-gray-500)}@media (hover:hover){.hover\:border-accent:hover{border-color:var(--color-accent)}.hover\:border-gray-500:hover{border-color:var(--color-gray-500)}.hover\:bg-blue-600\/40:hover{background-color:#155dfc66}@supports (color:color-mix(in lab, red, red)){.hover\:bg-blue-600\/40:hover{background-color:color-mix(in oklab, var(--color-blue-600) 40%, transparent)}}.hover\:bg-dark-600:hover{background-color:var(--color-dark-600)}.hover\:bg-dark-700:hover{background-color:var(--color-dark-700)}.hover\:bg-dark-700\/50:hover{background-color:#25252580}@supports (color:color-mix(in lab, red, red)){.hover\:bg-dark-700\/50:hover{background-color:color-mix(in oklab, var(--color-dark-700) 50%, transparent)}}.hover\:bg-red-400\/20:hover{background-color:#ff656833}@supports (color:color-mix(in lab, red, red)){.hover\:bg-red-400\/20:hover{background-color:color-mix(in oklab, var(--color-red-400) 20%, transparent)}}.hover\:text-accent:hover{color:var(--color-accent)}.hover\:text-blue-300:hover{color:var(--color-blue-300)}.hover\:text-gray-200:hover{color:var(--color-gray-200)}.hover\:text-gray-300:hover{color:var(--color-gray-300)}.hover\:text-red-400:hover{color:var(--color-red-400)}.hover\:underline:hover{text-decoration-line:underline}}.focus\:border-accent:focus{border-color:var(--color-accent)}.active\:scale-95:active{--tw-scale-x:95%;--tw-scale-y:95%;--tw-scale-z:95%;scale:var(--tw-scale-x) var(--tw-scale-y)}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-40:disabled{opacity:.4}@media (width>=48rem){.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}}.avatar-sm{object-fit:cover;border-radius:3px;flex-shrink:0;width:24px;height:24px}.steam-lvl{color:#e5e5e5;border:1.5px solid #9b9b9b;border-radius:10px;flex-shrink:0;justify-content:center;align-items:center;width:18px;height:18px;font-size:10px;font-weight:600;display:inline-flex}.steam-lvl.l10{border-color:#c02942}.steam-lvl.l20{border-color:#d95b43}.steam-lvl.l30{border-color:#fecc23}.steam-lvl.l40{border-color:#467a3c}.steam-lvl.l50{border-color:#4e8ddb}.steam-lvl.l60{border-color:#7652c9}.steam-lvl.l70{border-color:#c252c9}.steam-lvl.l80{border-color:#542437}.steam-lvl.l90{border-color:#997c52}.steam-lvl.l100p{text-shadow:1px 1px #1a1a1a;background-position:0 0;background-repeat:no-repeat;background-size:contain;border:none;border-radius:0;width:20px;height:20px;font-size:9px}.steam-lvl.l100p.img-100{background-image:url(https://community.fastly.steamstatic.com/public/shared/images/community/levels_hexagons.png)}.steam-lvl.l100p.img-200{background-image:url(https://community.fastly.steamstatic.com/public/shared/images/community/levels_shields.png)}.steam-lvl.l100p.img-300{background-image:url(https://community.fastly.steamstatic.com/public/shared/images/community/levels_books.png)}.steam-lvl.l100p.img-400{background-image:url(https://community.fastly.steamstatic.com/public/shared/images/community/levels_chevrons.png)}.steam-lvl.l100p.img-500{background-image:url(https://community.fastly.steamstatic.com/public/shared/images/community/levels_circle2.png)}.steam-lvl.l100p.img-600{background-image:url(https://community.fastly.steamstatic.com/public/shared/images/community/levels_angle.png)}.steam-lvl.l100p.img-700{background-image:url(https://community.fastly.steamstatic.com/public/shared/images/community/levels_flag.png)}.steam-lvl.l100p.img-800{background-image:url(https://community.fastly.steamstatic.com/public/shared/images/community/levels_wings.png)}.steam-lvl.l100p.img-900{background-image:url(https://community.fastly.steamstatic.com/public/shared/images/community/levels_arrows.png)}.btn-primary{cursor:pointer;background-color:var(--color-accent);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--color-white);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));border-radius:.25rem}@media (hover:hover){.btn-primary:hover{background-color:var(--color-accent-hover)}}.btn-secondary{cursor:pointer;border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-dark-600);background-color:var(--color-dark-700);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--color-gray-300);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));border-radius:.25rem}@media (hover:hover){.btn-secondary:hover{background-color:var(--color-dark-600)}}.btn-accent{cursor:pointer;background-color:var(--color-accent-dim);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--color-white);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));border-radius:.25rem}@media (hover:hover){.btn-accent:hover{background-color:var(--color-accent)}}.btn-danger{cursor:pointer;border-style:var(--tw-border-style);border-width:1px;border-color:#e400144d;border-radius:.25rem}@supports (color:color-mix(in lab, red, red)){.btn-danger{border-color:color-mix(in oklab, var(--color-red-600) 30%, transparent)}}.btn-danger{background-color:#e4001433}@supports (color:color-mix(in lab, red, red)){.btn-danger{background-color:color-mix(in oklab, var(--color-red-600) 20%, transparent)}}.btn-danger{padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--color-red-400);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}@media (hover:hover){.btn-danger:hover{background-color:#e400144d}@supports (color:color-mix(in lab, red, red)){.btn-danger:hover{background-color:color-mix(in oklab, var(--color-red-600) 30%, transparent)}}}.btn-danger-outline{cursor:pointer;border-style:var(--tw-border-style);border-width:1px;border-color:#e400144d;border-radius:.25rem}@supports (color:color-mix(in lab, red, red)){.btn-danger-outline{border-color:color-mix(in oklab, var(--color-red-600) 30%, transparent)}}.btn-danger-outline{padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--color-red-400);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));background-color:#0000}@media (hover:hover){.btn-danger-outline:hover{background-color:#e400141a}@supports (color:color-mix(in lab, red, red)){.btn-danger-outline:hover{background-color:color-mix(in oklab, var(--color-red-600) 10%, transparent)}}}.badge{padding-inline:calc(var(--spacing) * 2);padding-block:calc(var(--spacing) * .5);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);border-radius:.25rem;display:inline-block}.badge-ok{background-color:#00c75833}@supports (color:color-mix(in lab, red, red)){.badge-ok{background-color:color-mix(in oklab, var(--color-green-500) 20%, transparent)}}.badge-ok{color:var(--color-green-400)}.badge-error{background-color:#fb2c3633}@supports (color:color-mix(in lab, red, red)){.badge-error{background-color:color-mix(in oklab, var(--color-red-500) 20%, transparent)}}.badge-error{color:var(--color-red-400)}.badge-running{background-color:#3080ff33}@supports (color:color-mix(in lab, red, red)){.badge-running{background-color:color-mix(in oklab, var(--color-blue-500) 20%, transparent)}}.badge-running{color:var(--color-blue-400)}.badge-unknown{background-color:#6a728233}@supports (color:color-mix(in lab, red, red)){.badge-unknown{background-color:color-mix(in oklab, var(--color-gray-500) 20%, transparent)}}.badge-unknown{color:var(--color-gray-500)}.progress-bar{height:calc(var(--spacing) * 1.5);background-color:var(--color-dark-600);border-radius:3.40282e38px;width:100%;overflow:hidden}.progress-fill{background-color:var(--color-accent);height:100%;transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.3s;border-radius:3.40282e38px;transition-duration:.3s}.cell-copy{cursor:pointer;transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));-webkit-user-select:all;user-select:all}@media (hover:hover){.cell-copy:hover{color:var(--color-gray-200)}}.copy-tooltip{color:#fff;pointer-events:none;z-index:9999;background:#333;border-radius:4px;padding:2px 8px;font-size:11px;transition:opacity .2s;position:fixed;transform:translate(-50%,-100%)}[data-tooltip]{position:relative}button[disabled][data-tooltip]{pointer-events:auto}[data-tooltip]:after{content:attr(data-tooltip);color:#f0f0f8;white-space:nowrap;pointer-events:none;opacity:0;z-index:9999;background:#0f0f1a;border:1px solid #6b6b8a;border-radius:5px;padding:5px 10px;font-size:12px;font-weight:500;transition:opacity .15s;position:absolute;bottom:calc(100% + 6px);left:50%;transform:translate(-50%);box-shadow:0 4px 12px #0009}[data-tooltip]:hover:after{opacity:1}.popup-2fa{z-index:100;background:var(--color-dark-700);border:1px solid var(--color-dark-600);border-radius:8px;padding:8px 14px;position:fixed;box-shadow:0 4px 16px #0006}.popup-2fa-code{color:var(--color-accent);cursor:pointer;-webkit-user-select:all;user-select:all;font-family:monospace;font-size:1.5rem}.action-menu-dropdown{z-index:50;border-radius:var(--radius-lg);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-dark-600);background-color:var(--color-dark-700);min-width:180px;padding-block:calc(var(--spacing) * 1);--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a), 0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.action-menu-item{cursor:pointer;width:100%;padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 1.5);text-align:left;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-300);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));display:block}@media (hover:hover){.action-menu-item:hover{background-color:var(--color-dark-600);color:var(--color-white)}}.confirm-overlay{inset:calc(var(--spacing) * 0);z-index:50;background-color:#0009;justify-content:center;align-items:center;display:flex;position:fixed}@supports (color:color-mix(in lab, red, red)){.confirm-overlay{background-color:color-mix(in oklab, var(--color-black) 60%, transparent)}}.confirm-box{width:100%;max-width:var(--container-md);border-radius:var(--radius-lg);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-dark-600);background-color:var(--color-dark-800);padding:calc(var(--spacing) * 6);--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a), 0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);overflow-wrap:break-word;word-break:break-word}.toast-container{z-index:9999;pointer-events:none;flex-direction:column-reverse;gap:.5rem;display:flex;position:fixed;bottom:1rem;left:1rem}.toast{pointer-events:auto;color:#fff;border-radius:.375rem;padding:.5rem 1rem;font-size:.8rem;transition:opacity .3s;animation:.25s ease-out toastIn;box-shadow:0 2px 8px #0000004d}.toast-info{background:#2563eb}.toast-success{background:#16a34a}.toast-error{background:#dc2626}.toast-warn{background:#d97706}@keyframes toastIn{0%{opacity:0;transform:translate(-30px)}to{opacity:1;transform:translate(0)}}@keyframes shimmer{0%{stroke-dashoffset:80px}to{stroke-dashoffset:-80px}}.icon-shimmer{stroke:#ffffff73;stroke-dasharray:8 72;animation:2.5s linear infinite shimmer}.toggle-switch{cursor:pointer;flex-shrink:0;align-items:center;width:38px;height:22px;display:inline-flex;position:relative}.toggle-switch input{opacity:0;width:0;height:0;position:absolute}.toggle-track{background:#2a2a2a;border:1px solid #444;border-radius:999px;transition:background .2s,border-color .2s;position:absolute;inset:0}.toggle-track:before{content:"";background:#666;border-radius:50%;width:16px;height:16px;transition:transform .2s,background .2s;position:absolute;top:2px;left:2px}.toggle-switch input:checked~.toggle-track{background:#4f8cff;border-color:#4f8cff}.toggle-switch input:checked~.toggle-track:before{background:#fff;transform:translate(16px)}::-webkit-scrollbar{width:6px;height:6px}::-webkit-scrollbar-track{background:var(--color-dark-900)}::-webkit-scrollbar-thumb{background:var(--color-dark-600);border-radius:3px}::-webkit-scrollbar-thumb:hover{background:#555}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-scale-x{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-y{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-z{syntax:"*";inherits:false;initial-value:1}@keyframes spin{to{transform:rotate(360deg)}} diff --git a/app/static/favicon.svg b/app/static/favicon.svg new file mode 100644 index 0000000..d47ee23 --- /dev/null +++ b/app/static/favicon.svg @@ -0,0 +1,4 @@ + + Steam + + \ No newline at end of file diff --git a/app/static/index.html b/app/static/index.html new file mode 100644 index 0000000..af98d4e --- /dev/null +++ b/app/static/index.html @@ -0,0 +1,14 @@ + + + + + + + SteamPanel + + + + +
+ + diff --git a/assets/README_EN.md b/assets/README_EN.md new file mode 100644 index 0000000..17750a0 --- /dev/null +++ b/assets/README_EN.md @@ -0,0 +1,97 @@ +![SteamPanel](steampanel_en.png) + +# SteamPanel + +> [Русская версия](../README.md) | **Author:** [@lolzdm](https://t.me/lolzdm) + +Self-hosted web panel for bulk Steam account management. FastAPI + SQLite backend, React frontend. Operations run in parallel, proxy support included, real-time task progress via SSE. + +Supports **3 account types**: +- **Mafile** — full accounts with secrets (shared_secret, identity_secret) +- **Log:Pass** — login + password +- **Token** — access/refresh tokens *(work in progress, not functional)* + +--- + +## Features + +**Accounts** +- Import from `.txt`: `login:pass`, `login:pass:email:email_pass`, with mafile — any format +- Bulk operations on selected or all accounts +- Search by login, Steam ID, notes, level (`lvl>10`) +- Configurable columns per tab + +**Steam Actions** +- Change password (manual / random) +- Change email +- Change / remove phone +- Remove Steam Guard +- Auto-accept logins +- Open Steam in browser via [NoDriver](https://github.com/ultrafunkamsterdam/nodriver) (Chrome automation) + +**Validation** +- Login check, ban check, profile parsing (nickname, avatar, level, last online) +- Configurable thread count + +**Proxies** +- HTTP / SOCKS5 +- Formats: `login:pass@ip:port`, `ip:port:login:pass`, etc. + custom format builder +- Check (alive/dead), round-robin assignment, clear + +**Mafile** +- Mafiles are linked to accounts via the **Accounts** section (import or manual assignment) +- Export to ZIP: select specific mafile and Session block fields +- Export formats: **flat** (all files at root), **per-folder** (one folder per account), **single JSON** (one merged file) +- File name templates with `{username}`, `{steamid}` variables +- `.txt` generation: global `accounts.txt`, per-folder `.txt`, custom line format + - Available variables: `{login}`, `{password}`, `{email}`, `{email_password}`, `{steam_id}`, `{proxy}`, `{mafile}` +- “Don’t create folder per account” option when global `.txt` is enabled + +**Tools** +- 2FA code generator by `shared_secret` — click result to copy +- Validation settings: toggle profile parsing (nickname / avatar), ban check, thread count (1–50) +- Password hiding (spoiler mode) + +--- + +## Installation + +### Option 1 — `run.bat` (Windows, recommended) + +Download the archive, unpack, run `run.bat`. + +The script will: +1. Check for Python +2. Create a `venv` virtual environment +3. Install dependencies +4. Start main.py + +**Requirement:** Python 3.14 — [python.org](https://python.org) + +--- + +### Option 2 — manual + +```bash +git clone https://github.com/LOLZ-dev/SteamPanel.git + +cd SteamPanel + +python -m venv venv + +venv\scripts\activate + +pip install -r requirements.txt + +python main.py +``` + +Panel opens at `http://127.0.0.1:8000`. + +> Chrome/Chromium is only needed for the "Open in browser" feature. + +--- + +## License + +MIT — **by [@lolzdm](https://t.me/lolzdm)** diff --git a/assets/steampanel_en.png b/assets/steampanel_en.png new file mode 100644 index 0000000..51003d2 Binary files /dev/null and b/assets/steampanel_en.png differ diff --git a/assets/steampanel_ru.png b/assets/steampanel_ru.png new file mode 100644 index 0000000..bfae261 Binary files /dev/null and b/assets/steampanel_ru.png differ diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..602cbea --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +/logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..7dbf7eb --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,73 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs) +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + + // Remove tseslint.configs.recommended and replace with this + tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + tseslint.configs.stylisticTypeChecked, + + // Other configs... + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + // Enable lint rules for React + reactX.configs['recommended-typescript'], + // Enable lint rules for React DOM + reactDom.configs.recommended, + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js new file mode 100644 index 0000000..5e6b472 --- /dev/null +++ b/frontend/eslint.config.js @@ -0,0 +1,23 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + }, +]) diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..397b97b --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + SteamPanel + + +
+ + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..bc6f188 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,3619 @@ +{ + "name": "frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.0.0", + "dependencies": { + "lucide-react": "^0.577.0", + "react": "^19.2.4", + "react-dom": "^19.2.4", + "zustand": "^5.0.11" + }, + "devDependencies": { + "@eslint/js": "^9.39.4", + "@tailwindcss/postcss": "^4.2.1", + "@types/node": "^24.12.0", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.0", + "eslint": "^9.39.4", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.4.0", + "postcss": "^8.5.8", + "tailwindcss": "^4.2.1", + "typescript": "~5.9.3", + "typescript-eslint": "^8.56.1", + "vite": "^8.0.0" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", + "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", + "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.3.tgz", + "integrity": "sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.124.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.124.0.tgz", + "integrity": "sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.15.tgz", + "integrity": "sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.15.tgz", + "integrity": "sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.15.tgz", + "integrity": "sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.15.tgz", + "integrity": "sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.15.tgz", + "integrity": "sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.15.tgz", + "integrity": "sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.15.tgz", + "integrity": "sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.15.tgz", + "integrity": "sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.15.tgz", + "integrity": "sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.15.tgz", + "integrity": "sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.15.tgz", + "integrity": "sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.15.tgz", + "integrity": "sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.15.tgz", + "integrity": "sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.9.2", + "@emnapi/runtime": "1.9.2", + "@napi-rs/wasm-runtime": "^1.1.3" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.15.tgz", + "integrity": "sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.15.tgz", + "integrity": "sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.7", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz", + "integrity": "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tailwindcss/node": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.1.tgz", + "integrity": "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.19.0", + "jiti": "^2.6.1", + "lightningcss": "1.31.1", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.2.1" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz", + "integrity": "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.31.1", + "lightningcss-darwin-arm64": "1.31.1", + "lightningcss-darwin-x64": "1.31.1", + "lightningcss-freebsd-x64": "1.31.1", + "lightningcss-linux-arm-gnueabihf": "1.31.1", + "lightningcss-linux-arm64-gnu": "1.31.1", + "lightningcss-linux-arm64-musl": "1.31.1", + "lightningcss-linux-x64-gnu": "1.31.1", + "lightningcss-linux-x64-musl": "1.31.1", + "lightningcss-win32-arm64-msvc": "1.31.1", + "lightningcss-win32-x64-msvc": "1.31.1" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-android-arm64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.31.1.tgz", + "integrity": "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-darwin-arm64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.31.1.tgz", + "integrity": "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-darwin-x64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.31.1.tgz", + "integrity": "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-freebsd-x64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.31.1.tgz", + "integrity": "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.31.1.tgz", + "integrity": "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.31.1.tgz", + "integrity": "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-arm64-musl": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.31.1.tgz", + "integrity": "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-x64-gnu": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.31.1.tgz", + "integrity": "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-x64-musl": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.31.1.tgz", + "integrity": "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.31.1.tgz", + "integrity": "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-win32-x64-msvc": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.31.1.tgz", + "integrity": "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.1.tgz", + "integrity": "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.2.1", + "@tailwindcss/oxide-darwin-arm64": "4.2.1", + "@tailwindcss/oxide-darwin-x64": "4.2.1", + "@tailwindcss/oxide-freebsd-x64": "4.2.1", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1", + "@tailwindcss/oxide-linux-arm64-gnu": "4.2.1", + "@tailwindcss/oxide-linux-arm64-musl": "4.2.1", + "@tailwindcss/oxide-linux-x64-gnu": "4.2.1", + "@tailwindcss/oxide-linux-x64-musl": "4.2.1", + "@tailwindcss/oxide-wasm32-wasi": "4.2.1", + "@tailwindcss/oxide-win32-arm64-msvc": "4.2.1", + "@tailwindcss/oxide-win32-x64-msvc": "4.2.1" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.1.tgz", + "integrity": "sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.1.tgz", + "integrity": "sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.1.tgz", + "integrity": "sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.1.tgz", + "integrity": "sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.1.tgz", + "integrity": "sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.1.tgz", + "integrity": "sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.1.tgz", + "integrity": "sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.1.tgz", + "integrity": "sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.1.tgz", + "integrity": "sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.1.tgz", + "integrity": "sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.8.1", + "@emnapi/runtime": "^1.8.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.1", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.1.tgz", + "integrity": "sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.1.tgz", + "integrity": "sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.2.1.tgz", + "integrity": "sha512-OEwGIBnXnj7zJeonOh6ZG9woofIjGrd2BORfvE5p9USYKDCZoQmfqLcfNiRWoJlRWLdNPn2IgVZuWAOM4iTYMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.2.1", + "@tailwindcss/oxide": "4.2.1", + "postcss": "^8.5.6", + "tailwindcss": "4.2.1" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.12.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.0.tgz", + "integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.0.tgz", + "integrity": "sha512-qeu4rTHR3/IaFORbD16gmjq9+rEs9fGKdX0kF6BKSfi+gCuG3RCKLlSBYzn/bGsY9Tj7KE/DAQStbp8AHJGHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.57.0", + "@typescript-eslint/type-utils": "8.57.0", + "@typescript-eslint/utils": "8.57.0", + "@typescript-eslint/visitor-keys": "8.57.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.57.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.0.tgz", + "integrity": "sha512-XZzOmihLIr8AD1b9hL9ccNMzEMWt/dE2u7NyTY9jJG6YNiNthaD5XtUHVF2uCXZ15ng+z2hT3MVuxnUYhq6k1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.57.0", + "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/typescript-estree": "8.57.0", + "@typescript-eslint/visitor-keys": "8.57.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.0.tgz", + "integrity": "sha512-pR+dK0BlxCLxtWfaKQWtYr7MhKmzqZxuii+ZjuFlZlIGRZm22HnXFqa2eY+90MUz8/i80YJmzFGDUsi8dMOV5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.57.0", + "@typescript-eslint/types": "^8.57.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.0.tgz", + "integrity": "sha512-nvExQqAHF01lUM66MskSaZulpPL5pgy5hI5RfrxviLgzZVffB5yYzw27uK/ft8QnKXI2X0LBrHJFr1TaZtAibw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/visitor-keys": "8.57.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.0.tgz", + "integrity": "sha512-LtXRihc5ytjJIQEH+xqjB0+YgsV4/tW35XKX3GTZHpWtcC8SPkT/d4tqdf1cKtesryHm2bgp6l555NYcT2NLvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.0.tgz", + "integrity": "sha512-yjgh7gmDcJ1+TcEg8x3uWQmn8ifvSupnPfjP21twPKrDP/pTHlEQgmKcitzF/rzPSmv7QjJ90vRpN4U+zoUjwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/typescript-estree": "8.57.0", + "@typescript-eslint/utils": "8.57.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.0.tgz", + "integrity": "sha512-dTLI8PEXhjUC7B9Kre+u0XznO696BhXcTlOn0/6kf1fHaQW8+VjJAVHJ3eTI14ZapTxdkOmc80HblPQLaEeJdg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.0.tgz", + "integrity": "sha512-m7faHcyVg0BT3VdYTlX8GdJEM7COexXxS6KqGopxdtkQRvBanK377QDHr4W/vIPAR+ah9+B/RclSW5ldVniO1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.57.0", + "@typescript-eslint/tsconfig-utils": "8.57.0", + "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/visitor-keys": "8.57.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.0.tgz", + "integrity": "sha512-5iIHvpD3CZe06riAsbNxxreP+MuYgVUsV0n4bwLH//VJmgtt54sQeY2GszntJ4BjYCpMzrfVh2SBnUQTtys2lQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.57.0", + "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/typescript-estree": "8.57.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.0.tgz", + "integrity": "sha512-zm6xx8UT/Xy2oSr2ZXD0pZo7Jx2XsCoID2IUh9YSTFRu7z+WdwYTRk6LhUftm1crwqbuoF6I8zAFeCMw0YjwDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz", + "integrity": "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-rc.7" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.7", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.7.tgz", + "integrity": "sha512-1ghYO3HnxGec0TCGBXiDLVns4eCSx4zJpxnHrlqFQajmhfKMQBzUGDdkMK7fUW7PTHTeLf+j87aTuKuuwWzMGw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001778", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001778.tgz", + "integrity": "sha512-PN7uxFL+ExFJO61aVmP1aIEG4i9whQd4eoSCebav62UwDyp5OHh06zN4jqKSMePVgxHifCw1QJxdRkA1Pisekg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.313", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.313.tgz", + "integrity": "sha512-QBMrTWEf00GXZmJyx2lbYD45jpI3TUFnNIzJ5BBc8piGUDwMPa1GV6HJWTZVvY/eiN3fSopl7NRbgGp9sZ9LTA==", + "dev": true, + "license": "ISC" + }, + "node_modules/enhanced-resolve": { + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz", + "integrity": "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", + "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.2.tgz", + "integrity": "sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": "^9 || ^10" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.4.0.tgz", + "integrity": "sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.577.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.577.0.tgz", + "integrity": "sha512-4LjoFv2eEPwYDPg/CUdBJQSDfPyzXCRrVW1X7jrx/trgxnxkHFjnVZINbzvzxjN70dxychOfg+FTYwBiS3pQ5A==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rolldown": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.15.tgz", + "integrity": "sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.124.0", + "@rolldown/pluginutils": "1.0.0-rc.15" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.15", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.15", + "@rolldown/binding-darwin-x64": "1.0.0-rc.15", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.15", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.15", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.15", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.15", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.15", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.15", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.15", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.15", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.15", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.15", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.15", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.15" + } + }, + "node_modules/rolldown/node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.15.tgz", + "integrity": "sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==", + "dev": true, + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tailwindcss": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.1.tgz", + "integrity": "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/ts-api-utils": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.57.0.tgz", + "integrity": "sha512-W8GcigEMEeB07xEZol8oJ26rigm3+bfPHxHvwbYUlu1fUDsGuQ7Hiskx5xGW/xM4USc9Ephe3jtv7ZYPQntHeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.57.0", + "@typescript-eslint/parser": "8.57.0", + "@typescript-eslint/typescript-estree": "8.57.0", + "@typescript-eslint/utils": "8.57.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "8.0.8", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.8.tgz", + "integrity": "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.8", + "rolldown": "1.0.0-rc.15", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + }, + "node_modules/zustand": { + "version": "5.0.11", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.11.tgz", + "integrity": "sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..67c2d5e --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,35 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "lucide-react": "^0.577.0", + "react": "^19.2.4", + "react-dom": "^19.2.4", + "zustand": "^5.0.11" + }, + "devDependencies": { + "@eslint/js": "^9.39.4", + "@tailwindcss/postcss": "^4.2.1", + "@types/node": "^24.12.0", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.0", + "eslint": "^9.39.4", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.4.0", + "postcss": "^8.5.8", + "tailwindcss": "^4.2.1", + "typescript": "~5.9.3", + "typescript-eslint": "^8.56.1", + "vite": "^8.0.0" + } +} diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js new file mode 100644 index 0000000..a7f73a2 --- /dev/null +++ b/frontend/postcss.config.js @@ -0,0 +1,5 @@ +export default { + plugins: { + '@tailwindcss/postcss': {}, + }, +} diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg new file mode 100644 index 0000000..d47ee23 --- /dev/null +++ b/frontend/public/favicon.svg @@ -0,0 +1,4 @@ + + Steam + + \ No newline at end of file diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..23c770c --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,36 @@ +import { useEffect } from "react"; +import { useUiStore } from "@/stores/uiStore"; +import { Header } from "@/components/layout/Header"; +import { TabNav } from "@/components/layout/TabNav"; +import { ToastContainer } from "@/components/layout/Toast"; +import { AccountsTab } from "@/components/accounts/AccountsTab"; +import { ToolsTab } from "@/components/tools/ToolsTab"; +import { MafileTab } from "@/components/mafiles/MafileTab"; +import { LogsTab } from "@/components/logs/LogsTab"; + +export default function App() { + const activeTab = useUiStore((s) => s.activeTab); + const loadDisplaySettings = useUiStore((s) => s.loadDisplaySettings); + const loadColumnSettings = useUiStore((s) => s.loadColumnSettings); + const loadLogpassColumnSettings = useUiStore((s) => s.loadLogpassColumnSettings); + + useEffect(() => { + loadDisplaySettings(); + loadColumnSettings(); + loadLogpassColumnSettings(); + }, []); + + return ( +
+
+ +
+ {activeTab === "accounts" && } + {activeTab === "tools" && } + {activeTab === "mafiles" && } + {activeTab === "logs" && } +
+ +
+ ); +} diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts new file mode 100644 index 0000000..228152a --- /dev/null +++ b/frontend/src/api/client.ts @@ -0,0 +1,182 @@ +import type { + Account, AccountCreate, AccountUpdate, BulkImportResult, + Proxy, Task, MafileInfo, MafileExportRequest, + ActionRequest, ValidationSettings, DisplaySettings, ColumnSettings, LogpassColumnSettings, LogEntry, + LogpassAccount, LogpassAccountCreate, LogpassAccountUpdate, + TokenAccount, TokenAccountCreate, TokenColumnSettings, +} from "./types"; + +const BASE = "/api"; + +async function request(path: string, init?: RequestInit): Promise { + const res = await fetch(`${BASE}${path}`, { + ...init, + headers: { "Content-Type": "application/json", ...init?.headers }, + }); + if (!res.ok) { + const body = await res.json().catch(() => ({ detail: res.statusText })); + throw new Error(body.detail ?? `HTTP ${res.status}`); + } + if (res.status === 204) return undefined as T; + return res.json(); +} + +export const api = { + // Version + getVersion: () => request<{ version: string }>("/version"), + + // Accounts + getAccounts: () => request("/accounts"), + getAccount: (id: number) => request(`/accounts/${id}`), + createAccount: (data: AccountCreate) => + request("/accounts", { method: "POST", body: JSON.stringify(data) }), + updateAccount: (id: number, data: AccountUpdate) => + request(`/accounts/${id}`, { method: "PUT", body: JSON.stringify(data) }), + deleteAccount: (id: number) => + request(`/accounts/${id}`, { method: "DELETE" }), + deleteBulk: (ids: number[]) => + request<{ deleted: number }>("/accounts/delete-bulk", { method: "POST", body: JSON.stringify({ ids }) }), + assignProxies: () => + request<{ assigned: number; proxies_used: number }>("/accounts/assign-proxies", { method: "POST" }), + reassignProxies: () => + request<{ assigned: number; proxies_used: number }>("/accounts/reassign-proxies", { method: "POST" }), + clearProxies: () => + request<{ cleared: number }>("/accounts/clear-proxies", { method: "POST" }), + importAccounts: (file: File) => { + const form = new FormData(); + form.append("file", file); + return fetch(`${BASE}/accounts/import`, { method: "POST", body: form }).then(async (r) => { + if (!r.ok) throw new Error((await r.json().catch(() => ({}))).detail ?? r.statusText); + return r.json() as Promise; + }); + }, + + // Actions + executeAction: (data: ActionRequest) => + request<{ task_id: string; accounts_count: number }>("/actions", { method: "POST", body: JSON.stringify(data) }), + respondToPrompt: (taskId: string, value: string, login?: string) => + request<{ status: string }>(`/tasks/${taskId}/respond`, { method: "POST", body: JSON.stringify({ value, login: login || "" }) }), + generate2FA: (shared_secret: string) => + request<{ code: string }>("/actions/generate-2fa", { method: "POST", body: JSON.stringify({ shared_secret }) }), + generate2FAByAccount: (account_id: number) => + request<{ code: string }>("/actions/generate-2fa-by-account", { method: "POST", body: JSON.stringify({ account_id }) }), + + // Proxies + getProxies: () => request("/proxies"), + addProxy: (address: string, protocol = "http") => + request("/proxies", { method: "POST", body: JSON.stringify({ address, protocol }) }), + deleteProxy: (id: number) => + request(`/proxies/${id}`, { method: "DELETE" }), + deleteAllProxies: () => + request<{ deleted: number }>("/proxies/all", { method: "DELETE" }), + bulkAddProxies: (proxies: { address: string; protocol: string }[]) => + request<{ added: number }>("/proxies/bulk", { method: "POST", body: JSON.stringify(proxies) }), + checkProxies: () => + request<{ total: number; alive: number; dead: number }>("/proxies/check", { method: "POST" }), + + // Tasks + getTasks: () => request("/tasks"), + getTask: (id: string) => request(`/tasks/${id}`), + cancelTask: (id: string) => request(`/tasks/${id}`, { method: "DELETE" }), + + // Mafiles + getMafiles: () => request("/mafiles"), + uploadMafiles: (files: File[]) => { + const form = new FormData(); + files.forEach((f) => form.append("files", f)); + return fetch(`${BASE}/mafiles/upload`, { method: "POST", body: form }).then(async (r) => { + if (!r.ok) throw new Error((await r.json().catch(() => ({}))).detail ?? r.statusText); + return r.json() as Promise<{ uploaded: number; errors: string[] }>; + }); + }, + deleteMafile: (filename: string) => + request(`/mafiles/${encodeURIComponent(filename)}`, { method: "DELETE" }), + exportSecrets: () => request<{ account_name: string; shared_secret: string; identity_secret: string; steam_id: string }[]>("/mafiles/export/all"), + exportZip: (body: MafileExportRequest) => + fetch(`${BASE}/mafiles/export/zip`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }).then((r) => { + if (!r.ok) throw new Error(`Export failed: ${r.status}`); + return r.blob(); + }), + + // Settings + getValidationSettings: () => request("/settings/validation"), + updateValidationSettings: (data: ValidationSettings) => + request("/settings/validation", { method: "PUT", body: JSON.stringify(data) }), + getDisplaySettings: () => request("/settings/display"), + updateDisplaySettings: (data: DisplaySettings) => + request("/settings/display", { method: "PUT", body: JSON.stringify(data) }), + getColumnSettings: () => request("/settings/columns"), + updateColumnSettings: (data: ColumnSettings) => + request("/settings/columns", { method: "PUT", body: JSON.stringify(data) }), + getLogpassColumnSettings: () => request("/settings/logpass-columns"), + updateLogpassColumnSettings: (data: LogpassColumnSettings) => + request("/settings/logpass-columns", { method: "PUT", body: JSON.stringify(data) }), + + // Logs + getLogs: () => request("/logs"), + + // Auto-accept + startAutoAccept: (ids: number[]) => + request<{ started: number[] }>("/auto-accept/start", { method: "POST", body: JSON.stringify({ account_ids: ids }) }), + stopAutoAccept: (ids: number[]) => + request<{ stopped: number[] }>("/auto-accept/stop", { method: "POST", body: JSON.stringify({ account_ids: ids }) }), + getAutoAcceptStatus: () => + request<{ running: number[] }>("/auto-accept/status"), + + // Browser + openBrowser: (id: number) => + request<{ status: string; message: string; task_id?: string }>(`/accounts/${id}/browser`, { method: "POST" }), + + // Logpass accounts + getLogpassAccounts: () => request("/logpass"), + createLogpassAccount: (data: LogpassAccountCreate) => + request("/logpass", { method: "POST", body: JSON.stringify(data) }), + updateLogpassAccount: (id: number, data: LogpassAccountUpdate) => + request(`/logpass/${id}`, { method: "PUT", body: JSON.stringify(data) }), + deleteLogpassAccount: (id: number) => + request(`/logpass/${id}`, { method: "DELETE" }), + deleteLogpassBulk: (ids: number[]) => + request<{ deleted: number }>("/logpass/delete-bulk", { method: "POST", body: JSON.stringify({ ids }) }), + importLogpass: (lines: string[]) => + request("/logpass/import", { method: "POST", body: JSON.stringify({ lines }) }), + validateLogpass: (account_ids: number[]) => + request<{ task_id: string; accounts_count: number }>("/logpass/validate", { method: "POST", body: JSON.stringify({ account_ids }) }), + fullParseLogpass: (account_ids: number[]) => + request<{ task_id: string; accounts_count: number }>("/logpass/full-parse", { method: "POST", body: JSON.stringify({ account_ids }) }), + assignLogpassProxies: () => + request<{ assigned: number; proxies_used: number }>("/logpass/assign-proxies", { method: "POST" }), + reassignLogpassProxies: () => + request<{ assigned: number; proxies_used: number }>("/logpass/reassign-proxies", { method: "POST" }), + clearLogpassProxies: () => + request<{ cleared: number }>("/logpass/clear-proxies", { method: "POST" }), + openLogpassBrowser: (id: number) => + request<{ status: string; message: string; task_id?: string }>(`/logpass/${id}/browser`, { method: "POST" }), + + // Token accounts + getTokenAccounts: () => request("/token-accounts"), + createTokenAccount: (data: TokenAccountCreate) => + request("/token-accounts", { method: "POST", body: JSON.stringify(data) }), + deleteTokenAccount: (id: number) => + request(`/token-accounts/${id}`, { method: "DELETE" }), + deleteTokenBulk: (ids: number[]) => + request<{ deleted: number }>("/token-accounts/delete-bulk", { method: "POST", body: JSON.stringify({ ids }) }), + importTokens: (lines: string[]) => + request("/token-accounts/import", { method: "POST", body: JSON.stringify({ lines }) }), + validateTokens: (ids: number[]) => + request<{ task_id: string; accounts_count: number }>("/token-accounts/validate", { method: "POST", body: JSON.stringify({ account_ids: ids }) }), + openTokenBrowser: (id: number) => + request<{ status: string; message: string; task_id?: string }>(`/token-accounts/${id}/browser`, { method: "POST" }), + assignTokenProxies: () => + request<{ assigned: number; proxies_used: number }>("/token-accounts/assign-proxies", { method: "POST" }), + reassignTokenProxies: () => + request<{ assigned: number; proxies_used: number }>("/token-accounts/reassign-proxies", { method: "POST" }), + clearTokenProxies: () => + request<{ cleared: number }>("/token-accounts/clear-proxies", { method: "POST" }), + getTokenColumnSettings: () => request("/settings/token-columns"), + updateTokenColumnSettings: (data: TokenColumnSettings) => + request("/settings/token-columns", { method: "PUT", body: JSON.stringify(data) }), +}; diff --git a/frontend/src/api/types.ts b/frontend/src/api/types.ts new file mode 100644 index 0000000..3dcb9cd --- /dev/null +++ b/frontend/src/api/types.ts @@ -0,0 +1,260 @@ +export interface Account { + id: number; + login: string; + password: string; + steam_id: string | null; + email: string | null; + email_password: string | null; + phone: string | null; + mafile_path: string | null; + shared_secret: string | null; + identity_secret: string | null; + proxy: string | null; + status: string; + notes: string | null; + nickname: string | null; + avatar_url: string | null; + steam_level: number | null; + last_online: string | null; + auto_accept: number; + ban_status: string | null; + has_cookies: boolean; + has_revocation_code: boolean; + created_at: string; + updated_at: string; +} + +export interface AccountCreate { + login: string; + password: string; + steam_id?: string; + email?: string; + email_password?: string; + phone?: string; + proxy?: string; + notes?: string; +} + +export interface AccountUpdate { + login?: string; + password?: string; + steam_id?: string; + email?: string; + email_password?: string; + phone?: string; + proxy?: string; + status?: string; + notes?: string; +} + +export interface Proxy { + id: number; + address: string; + protocol: string; + is_alive: boolean; + last_checked: string | null; + fail_count: number; +} + +export interface Task { + id: string; + type: string; + status: string; + progress: number; + total: number; + result: string | null; + error: string | null; + prompt?: string; + prompt_login?: string; + step?: number; + total_steps?: number; + step_label?: string; + active_count?: number; + account_ids?: string | null; + account_results?: Record | string | null; + account_steps?: Record; + created_at: string; + updated_at: string; +} + +export interface MafileInfo { + filename: string; + account_name: string; + steam_id: string; + has_shared_secret: boolean; + has_identity_secret: boolean; + fully_enrolled: boolean; + error?: string; +} + +export interface MafileExportRequest { + fields: string[]; + session_fields: string[]; + format: "flat_mafiles" | "per_account_folder" | "single_file"; + account_ids: number[]; + folder_name_template: string; + mafile_name_template: string; + txt_name_template: string; + include_txt_per_folder: boolean; + include_global_txt: boolean; + skip_folders: boolean; + txt_format: string; +} + +export interface BulkImportResult { + imported: number; + skipped: number; + errors: string[]; +} + +export interface ActionRequest { + account_ids: number[]; + action: string; + params?: Record; +} + +export interface ValidationSettings { + fetch_profile: boolean; + check_ban: boolean; + max_threads: number; + auto_revalidate_browser: boolean; +} + +export interface DisplaySettings { + hide_passwords: boolean; +} + +export interface ColumnSettings { + browser: boolean; + profile: boolean; + steam_id: boolean; + login: boolean; + password: boolean; + login_pass: boolean; + email: boolean; + email_pass: boolean; + email_login_pass: boolean; + phone: boolean; + status: boolean; + ban: boolean; + twofa: boolean; + mafile: boolean; + proxy: boolean; + notes: boolean; + actions: boolean; + last_online: boolean; +} + +export interface LogpassColumnSettings { + browser: boolean; + profile: boolean; + steam_id: boolean; + login: boolean; + password: boolean; + login_pass: boolean; + status: boolean; + ban: boolean; + prime: boolean; + trophy: boolean; + behavior: boolean; + license: boolean; + proxy: boolean; + notes: boolean; + actions: boolean; + last_online: boolean; +} + +export interface TokenColumnSettings { + browser: boolean; + profile: boolean; + last_online: boolean; + steam_id: boolean; + login: boolean; + token: boolean; + status: boolean; + proxy: boolean; + notes: boolean; + actions: boolean; +} + +export interface LogEntry { + ts: string; + level: string; + icon: string; + msg: string; +} + +export interface LogpassAccount { + id: number; + login: string; + password: string; + steam_id: string | null; + proxy: string | null; + status: string; + ban_status: string | null; + nickname: string | null; + steam_level: number | null; + prime: string | null; + trophy: string | null; + behavior: string | null; + license: string | null; + notes: string | null; + avatar_url: string | null; + last_online: string | null; + has_cookies: boolean; + created_at: string; + updated_at: string; +} + +export interface LogpassAccountCreate { + login: string; + password: string; + steam_id?: string; + proxy?: string; + notes?: string; +} + +export interface LogpassAccountUpdate { + login?: string; + password?: string; + steam_id?: string; + proxy?: string; + status?: string; + notes?: string; +} + +export interface TokenAccount { + id: number; + login: string | null; + token: string; + steam_id: string | null; + proxy: string | null; + status: string; + ban_status: string | null; + nickname: string | null; + steam_level: number | null; + avatar_url: string | null; + last_online: string | null; + has_cookies: boolean; + notes: string | null; + created_at: string; + updated_at: string; +} + +export interface TokenAccountCreate { + login?: string; + token: string; + steam_id?: string; + proxy?: string; + notes?: string; +} + +export type TabId = "accounts" | "tools" | "mafiles" | "logs"; + +export type ToastType = "info" | "success" | "error" | "warn"; + +export interface Toast { + id: number; + type: ToastType; + message: string; +} diff --git a/frontend/src/components/accounts/AccountRow.tsx b/frontend/src/components/accounts/AccountRow.tsx new file mode 100644 index 0000000..8eef902 --- /dev/null +++ b/frontend/src/components/accounts/AccountRow.tsx @@ -0,0 +1,283 @@ +import { useState } from "react"; +import type { Account, ColumnSettings } from "@/api/types"; +import { useAccountStore } from "@/stores/accountStore"; +import { copyText } from "@/lib/clipboard"; +import { useUiStore } from "@/stores/uiStore"; +import { api } from "@/api/client"; +import { StatusBadge, BanBadge, MafileBadge } from "./Badges"; +import { ActionMenu } from "./ActionMenu"; +import { SteamLevelBadge } from "./SteamLevelBadge"; +import { IconCheck, IconAlertTriangle, IconEye, IconEyeOff, IconKey, IconRefresh, IconEdit, IconTrash } from "@/components/shared/Icons"; +import { useT } from "@/lib/i18n"; + +interface Props { + account: Account; + visibleColumns: ColumnSettings; + proxyLabel: string | null; + isProcessing: boolean; + accountResult?: { status: string; error?: string }; + accountStep?: { step: number; total: number }; + onEdit: (id: number) => void; + onDelete: (id: number) => void; + onAction: (id: number, action: string, params: Record) => void; + onToggleAutoAccept: (id: number) => void; + onOpenBrowser: (id: number) => void; +} + +export function AccountRow({ account: a, visibleColumns: v, proxyLabel, isProcessing, accountResult, accountStep, onEdit, onDelete, onAction, onToggleAutoAccept, onOpenBrowser }: Props) { + const selectedIds = useAccountStore((s) => s.selectedIds); + const toggleSelect = useAccountStore((s) => s.toggleSelect); + const addToast = useUiStore((s) => s.addToast); + const hidePasswords = useUiStore((s) => s.hidePasswords); + const [revealed, setRevealed] = useState(false); + const [editingNotes, setEditingNotes] = useState(false); + const [notesValue, setNotesValue] = useState(a.notes || ""); + const t = useT(); + + + const handleCopy = async (text: string) => { + const ok = await copyText(text); + addToast(ok ? "success" : "error", ok ? t("toast.copied") : t("toast.copyFailed")); + }; + + const masked = hidePasswords && !revealed; + const dots = "••••••••"; + + return ( + + + toggleSelect(a.id)} + /> + + + {isProcessing ? ( +
+
+ {accountStep && ( + [{accountStep.step}/{accountStep.total}] + )} +
+ ) : accountResult?.status === "ok" ? ( + + ) : accountResult?.status === "error" ? ( + + + + ) : null} + + {v.browser && ( + + {a.has_cookies ? ( + + ) : ( + + )} + + )} + {v.profile && ( + +
+ {a.avatar_url && } + {a.nickname || "—"} + {a.steam_level != null && } +
+ + )} + {v.last_online && ( + + {a.last_online || "—"} + + )} + {v.steam_id && ( + + {a.steam_id ? ( + + {a.steam_id} + + ) : "—"} + + )} + {v.login && {a.login}} + {v.password && ( + + handleCopy(a.password)} + title={t("tip.copyPassword")} + > + {masked ? dots : a.password} + + + + )} + {v.login_pass && ( + + handleCopy(`${a.login}:${a.password}`)} + title={t("tip.copyLoginPass")} + > + {masked ? `${a.login}:${dots}` : `${a.login}:${a.password}`} + + + + )} + {v.email && {a.email || "—"}} + {v.email_pass && ( + + { if (a.email_password) handleCopy(a.email_password); }} + title={a.email_password ? t("tip.copyEmailPass") : ""} + > + {a.email_password ? (masked ? dots : a.email_password) : "—"} + + {a.email_password && ( + + )} + + )} + {v.email_login_pass && ( + + { if (a.email && a.email_password) handleCopy(`${a.email}:${a.email_password}`); }} + title={a.email && a.email_password ? t("tip.copyEmailLoginPass") : ""} + > + {a.email && a.email_password ? (masked ? `${a.email}:${dots}` : `${a.email}:${a.email_password}`) : "—"} + + {a.email && a.email_password && ( + + )} + + )} + {v.phone && {a.phone || "—"}} + {v.status && } + {v.ban && } + {v.twofa && ( + + {a.shared_secret ? ( + + ) : ( + + )} + + )} + {v.mafile && ( + + + + )} + {v.proxy && ( + + {a.proxy ? ( + handleCopy(a.proxy!)} + title={a.proxy} + > + {proxyLabel || a.proxy} + + ) : "—"} + + )} + {v.notes && ( + + {editingNotes ? ( + setNotesValue(e.target.value)} + onBlur={async () => { + setEditingNotes(false); + if (notesValue !== (a.notes || "")) { + try { + await api.updateAccount(a.id, { notes: notesValue }); + addToast("success", t("toast.noteSaved")); + useAccountStore.getState().loadAccounts(); + } catch { + addToast("error", t("toast.noteSaveError")); + } + } + }} + onKeyDown={(e) => { if (e.key === "Enter") (e.target as HTMLInputElement).blur(); if (e.key === "Escape") { setNotesValue(a.notes || ""); setEditingNotes(false); } }} + /> + ) : ( + { setNotesValue(a.notes || ""); setEditingNotes(true); }} + title={a.notes || t("tip.addNote")} + > + {a.notes || "—"} + + )} + + )} + {v.actions && ( + +
+ + + + {a.auto_accept ? ( + + ) : null} +
+ + )} + + ); +} diff --git a/frontend/src/components/accounts/AccountTable.tsx b/frontend/src/components/accounts/AccountTable.tsx new file mode 100644 index 0000000..7d34762 --- /dev/null +++ b/frontend/src/components/accounts/AccountTable.tsx @@ -0,0 +1,183 @@ +import { useEffect, useMemo, useRef, useState } from "react"; +import type { Account, ColumnSettings } from "@/api/types"; +import { useAccountStore } from "@/stores/accountStore"; +import { useUiStore } from "@/stores/uiStore"; +import { AccountRow } from "./AccountRow"; +import { useT } from "@/lib/i18n"; + +const COLUMN_KEYS: (keyof ColumnSettings)[] = [ + "browser", "profile", "last_online", "steam_id", "login", "password", "login_pass", + "email", "email_pass", "email_login_pass", "phone", "status", "ban", "twofa", "mafile", "proxy", "notes", "actions", +]; + +const COL_LABEL_KEYS: Record = { + browser: "col.browser", profile: "col.profile", last_online: "col.lastOnline", + steam_id: "col.steamId", login: "col.login", password: "col.password", + login_pass: "col.loginPass", email: "col.email", email_pass: "col.emailPass", + email_login_pass: "col.emailLoginPass", phone: "col.phone", status: "col.status", + ban: "col.ban", twofa: "col.twofa", mafile: "col.mafile", proxy: "col.proxy", + notes: "col.notes", actions: "col.actions", +}; + +interface Props { + accounts: Account[]; + processingIds: Set; + accountResults: Record; + accountSteps: Record; + onEdit: (id: number) => void; + onDelete: (id: number) => void; + onAction: (id: number, action: string, params: Record) => void; + onToggleAutoAccept: (id: number) => void; + onOpenBrowser: (id: number) => void; +} + +export function AccountTable({ accounts, processingIds, accountResults, accountSteps, onEdit, onDelete, onAction, onToggleAutoAccept, onOpenBrowser }: Props) { + const selectAll = useAccountStore((s) => s.selectAll); + const clearSelection = useAccountStore((s) => s.clearSelection); + const selectedIds = useAccountStore((s) => s.selectedIds); + const cols = useUiStore((s) => s.columnVisibility); + const t = useT(); + + const allSelected = accounts.length > 0 && selectedIds.size === accounts.length; + const visibleKeys = COLUMN_KEYS.filter((k) => cols[k]); + + const BATCH = 100; + const [visibleCount, setVisibleCount] = useState(BATCH); + const sentinelRef = useRef(null); + + type SortField = "last_online" | null; + type SortDir = "asc" | "desc"; + const [sortField, setSortField] = useState(null); + const [sortDir, setSortDir] = useState("asc"); + const toggleSort = (field: SortField) => { + if (sortField === field) { + if (sortDir === "asc") setSortDir("desc"); + else { setSortField(null); setSortDir("asc"); } + } else { + setSortField(field); + setSortDir("asc"); + } + }; + + const sorted = useMemo(() => { + if (!sortField) return accounts; + const dir = sortDir === "asc" ? 1 : -1; + const EMPTY = Symbol(); + return [...accounts].sort((a, b) => { + const parseOnline = (v: string | null | undefined): number | typeof EMPTY => { + if (!v || v === "\u2014") return EMPTY; + if (v === "online") return -1; + const m = v.match(/^(\d+)([mhd])$/i); + if (!m) return EMPTY; + const n = parseInt(m[1], 10); + if (m[2] === "m") return n; + if (m[2] === "h") return n * 60; + return n * 1440; + }; + const va = parseOnline(a.last_online); + const vb = parseOnline(b.last_online); + if (va === EMPTY && vb === EMPTY) return 0; + if (va === EMPTY) return 1; + if (vb === EMPTY) return -1; + if (va === vb) return 0; + return (va < vb ? -1 : 1) * dir; + }); + }, [accounts, sortField, sortDir]); + + useEffect(() => { setVisibleCount(BATCH); }, [accounts, sortField, sortDir]); + + const visible = useMemo(() => sorted.slice(0, visibleCount), [sorted, visibleCount]); + + useEffect(() => { + const el = sentinelRef.current; + if (!el) return; + const obs = new IntersectionObserver((entries) => { + if (entries[0].isIntersecting) setVisibleCount((c) => Math.min(c + BATCH, sorted.length)); + }, { rootMargin: "400px" }); + obs.observe(el); + return () => obs.disconnect(); + }, [sorted.length, visibleCount]); + + // Build proxy labels: unique proxies get numbered "Прокси 1", "Прокси 2", etc. + const proxyLabels = useMemo(() => { + const map = new Map(); + const uniqueProxies: string[] = []; + for (const a of accounts) { + if (a.proxy && !uniqueProxies.includes(a.proxy)) { + uniqueProxies.push(a.proxy); + } + } + const proxyToNum = new Map(); + uniqueProxies.forEach((p, i) => proxyToNum.set(p, i + 1)); + for (const a of accounts) { + if (a.proxy) { + const num = proxyToNum.get(a.proxy); + map.set(a.id, num ? t("proxy.label", { num }) : a.proxy); + } + } + return map; + }, [accounts]); + + if (!accounts.length) { + return ( +
+ {t("empty.accounts")} +
+ ); + } + + return ( +
+ + + + + + {visibleKeys.map((k) => { + if (k === "last_online") { + return ( + + ); + } + return ; + })} + + + + {visible.map((a) => ( + + ))} + {visibleCount < accounts.length && ( + + )} + +
+
+ allSelected ? clearSelection() : selectAll()} + /> + {selectedIds.size > 0 && ( + {selectedIds.size} + )} +
+
toggleSort("last_online")}> + {t(COL_LABEL_KEYS[k])}{sortField === "last_online" ? (sortDir === "asc" ? " ▲" : " ▼") : ""} + {t(COL_LABEL_KEYS[k])}
{t("paging.shown", { visible: visibleCount, total: accounts.length })}
+
+ ); +} diff --git a/frontend/src/components/accounts/AccountsTab.tsx b/frontend/src/components/accounts/AccountsTab.tsx new file mode 100644 index 0000000..25dd4e4 --- /dev/null +++ b/frontend/src/components/accounts/AccountsTab.tsx @@ -0,0 +1,689 @@ +import { useEffect, useState, useCallback, useMemo, useRef } from "react"; +import { api } from "@/api/client"; +import { useAccountStore } from "@/stores/accountStore"; +import { useUiStore } from "@/stores/uiStore"; +import { useTaskStore } from "@/stores/taskStore"; +import { useLogpassStore } from "@/stores/logpassStore"; +import { useTokenStore } from "@/stores/tokenStore"; +import { AccountTable } from "./AccountTable"; +import { EditModal } from "./EditModal"; +import { ImportModal } from "./ImportModal"; +import { AddModal } from "./AddModal"; +import { ConfirmModal } from "@/components/shared/Modals"; +import { ColumnSettingsDropdown } from "./ColumnSettingsDropdown"; +import { + IconDownload, IconPlus, IconCheckCircle, IconXCircle, + IconFolder, IconGlobe, IconRefresh, IconBan, + IconPlay, IconLock, IconKey, IconZap, +} from "@/components/shared/Icons"; +import { LogpassTab } from "./LogpassTab"; +import { TokenTab } from "./TokenTab"; +import type { AccountCreate, AccountUpdate, Task } from "@/api/types"; +import { useT } from "@/lib/i18n"; + +type AccountSection = "mafile" | "logpass" | "token"; + +type AccResults = Record; + +function parseAccResults(raw: Task["account_results"]): AccResults | null { + if (!raw) return null; + if (typeof raw === "string") { + try { return JSON.parse(raw) as AccResults; } catch { return null; } + } + return raw; +} + +function parseIds(raw: Task["account_ids"]): number[] { + if (!raw) return []; + try { return JSON.parse(raw) as number[]; } catch { return []; } +} + +const BULK_ACTIONS = [ + { value: "", labelKey: "action.selectAction" }, + { value: "validate", labelKey: "action.validate" }, + { value: "change_password", labelKey: "action.changePassword" }, + { value: "random_password", labelKey: "action.randomPassword" }, + { value: "change_email", labelKey: "action.changeEmail" }, + { value: "remove_guard", labelKey: "action.removeGuard" }, + { value: "enable_auto_accept", labelKey: "action.enableAutoAccept" }, +] as const; + +function BulkActionDropdown({ value, onChange, disabledActions }: { value: string; onChange: (v: string) => void; disabledActions?: Set }) { + const t = useT(); + const [open, setOpen] = useState(false); + const ref = useRef(null); + + useEffect(() => { + if (!open) return; + const handler = (e: MouseEvent) => { + if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false); + }; + document.addEventListener("mousedown", handler); + return () => document.removeEventListener("mousedown", handler); + }, [open]); + + const label = t(BULK_ACTIONS.find((a) => a.value === value)?.labelKey || "action.selectAction"); + + return ( +
+ + {open && ( +
+ {BULK_ACTIONS.map((a) => { + const disabled = !!(a.value && disabledActions?.has(a.value)); + return ( + + ); + })} +
+ )} +
+ ); +} + +function SectionAccountCount() { + const section = useUiStore((s) => s.accountSection); + const mafileCount = useAccountStore((s) => s.accounts.length); + const logpassCount = useLogpassStore((s) => s.accounts.length); + const tokenCount = useTokenStore((s) => s.accounts.length); + const loadLogpass = useLogpassStore((s) => s.loadAccounts); + const loadTokens = useTokenStore((s) => s.loadAccounts); + const [displayCount, setDisplayCount] = useState(0); + const rafRef = useRef(0); + const displayRef = useRef(0); + const prevSection = useRef(section); + + useEffect(() => { + if (section === "logpass" && logpassCount === 0) loadLogpass(); + if (section === "token" && tokenCount === 0) loadTokens(); + }, [section]); + + const count = section === "logpass" ? logpassCount : section === "token" ? tokenCount : mafileCount; + + useEffect(() => { + cancelAnimationFrame(rafRef.current); + + const sectionChanged = prevSection.current !== section; + prevSection.current = section; + + if (sectionChanged) { + displayRef.current = count; + setDisplayCount(count); + return; + } + + const start = displayRef.current; + const diff = count - start; + if (diff === 0) return; + + const duration = Math.min(400, Math.max(150, Math.abs(diff) * 2)); + const startTime = performance.now(); + + const step = (now: number) => { + const elapsed = now - startTime; + const progress = Math.min(elapsed / duration, 1); + const eased = 1 - Math.pow(1 - progress, 3); + const value = Math.max(0, Math.round(start + diff * eased)); + displayRef.current = value; + setDisplayCount(value); + if (progress < 1) rafRef.current = requestAnimationFrame(step); + }; + + rafRef.current = requestAnimationFrame(step); + return () => cancelAnimationFrame(rafRef.current); + }, [count, section]); + + return ( +
+
+ + + + + + +
+ {displayCount} +
+ ); +} + +export function AccountsTab() { + const accounts = useAccountStore((s) => s.accounts); + const selectedIds = useAccountStore((s) => s.selectedIds); + const loadAccounts = useAccountStore((s) => s.loadAccounts); + const clearSelection = useAccountStore((s) => s.clearSelection); + const sendToMafileManager = useAccountStore((s) => s.sendToMafileManager); + const setTab = useUiStore((s) => s.setTab); + const addToast = useUiStore((s) => s.addToast); + const loadTasks = useTaskStore((s) => s.loadTasks); + const t = useT(); + + const [editId, setEditId] = useState(null); + const [showImport, setShowImport] = useState(false); + const [showAdd, setShowAdd] = useState(false); + const [confirmMsg, setConfirmMsg] = useState(null); + const [onConfirmAction, setOnConfirmAction] = useState<(() => void) | null>(null); + const [bulkAction, setBulkAction] = useState(""); + const section = useUiStore((s) => s.accountSection); + const setSection = useUiStore((s) => s.setAccountSection); + const [activeTask, setActiveTask] = useState(null); + const [processingIds, setProcessingIds] = useState>(new Set()); + const [searchQuery, setSearchQuery] = useState(""); + const [promptData, setPromptData] = useState<{ taskId: string; message: string; login?: string } | null>(null); + const [promptValue, setPromptValue] = useState(""); + const [accountResults, setAccountResults] = useState>({}); + const [accountSteps, setAccountSteps] = useState>({}); + + const filteredAccounts = useMemo(() => { + if (!searchQuery.trim()) return accounts; + const q = searchQuery.trim(); + + // lvl comparison syntax: lvl>10, lvl>=10, lvl<10, lvl<=10, lvl=10 + const lvlMatch = q.match(/^lvl\s*(>=|<=|>|<|=)\s*(\d+)$/i); + if (lvlMatch) { + const op = lvlMatch[1]; + const val = parseInt(lvlMatch[2], 10); + return accounts.filter((a) => { + const lvl = a.steam_level; + if (lvl == null) return false; + if (op === ">") return lvl > val; + if (op === ">=") return lvl >= val; + if (op === "<") return lvl < val; + if (op === "<=") return lvl <= val; + return lvl === val; + }); + } + + const ql = q.toLowerCase(); + const words = ql.split(/\s+/).filter(Boolean); + return accounts.filter((a) => { + if (a.login.toLowerCase().includes(ql)) return true; + if (a.steam_id && a.steam_id.toLowerCase().includes(ql)) return true; + if (a.notes) { + const notesLower = a.notes.toLowerCase(); + return words.some((w) => notesLower.includes(w)); + } + return false; + }); + }, [accounts, searchQuery]); + + const bulkDisabledActions = useMemo(() => { + const disabled = new Set(); + const selected = accounts.filter((a) => selectedIds.has(a.id)); + if (!selected.some((a) => a.has_revocation_code)) { + disabled.add("remove_guard"); + } + return disabled; + }, [accounts, selectedIds]); + + useEffect(() => { loadAccounts(); }, []); + + const confirm = (msg: string, action: () => void) => { + setConfirmMsg(msg); + setOnConfirmAction(() => action); + }; + + const watchTask = useCallback((taskId: string, accountIds: number[] = [], clearResults = true) => { + if (clearResults) { + setAccountResults((prev) => { + const next = { ...prev }; + accountIds.forEach((id) => delete next[String(id)]); + return next; + }); + } + const es = new EventSource(`/api/tasks/${taskId}/stream`); + es.onmessage = (e) => { + try { + const data = JSON.parse(e.data) as Task; + setActiveTask(data); + const parsed = parseAccResults(data.account_results); + if (parsed) { + setAccountResults((prev) => ({ ...prev, ...parsed })); + } + if (data.account_steps) { + setAccountSteps((prev) => ({ ...prev, ...data.account_steps })); + } + if (data.prompt) { + setPromptData({ taskId, message: data.prompt, login: data.prompt_login }); + setPromptValue(""); + } + if (data.status === "completed" || data.status === "failed" || data.status === "cancelled") { + if (data.status === "completed") { + addToast("success", `${data.type}: ${data.result || t("toast.done")}`); + } else if (data.status === "cancelled") { + addToast("info", t("toast.taskCancelled")); + } else { + addToast("error", `${data.type}: ${data.error || t("toast.error")}`); + } + setProcessingIds((prev) => { + const s = new Set(prev); + accountIds.forEach((id) => s.delete(id)); + return s; + }); + setPromptData(null); + loadAccounts(); + loadTasks(); + es.close(); + setTimeout(() => setActiveTask(null), 3000); + } + } catch { /* ignore */ } + }; + es.onerror = () => es.close(); + }, [addToast, loadAccounts, loadTasks]); + + // Restore running/completed task state on page load + useEffect(() => { + let cancelled = false; + (async () => { + try { + const tasks = await api.getTasks(); + if (cancelled) return; + + const running = tasks.find((t) => t.status === "running"); + if (running) { + const ids = parseIds(running.account_ids); + setActiveTask(running); + setProcessingIds(new Set(ids)); + const restored = parseAccResults(running.account_results); + if (restored) setAccountResults(restored); + watchTask(running.id, ids, false); + return; + } + + const recent = tasks.find((t) => t.status === "completed" || t.status === "failed"); + if (recent) { + const restored = parseAccResults(recent.account_results); + if (restored && Object.keys(restored).length > 0) { + setAccountResults(restored); + } + } + } catch { /* ignore */ } + })(); + return () => { cancelled = true; }; + }, [watchTask]); + + const handleAction = async (id: number, action: string, params: Record) => { + const acc = accounts.find((a) => a.id === id); + const login = acc?.login ?? `#${id}`; + try { + setProcessingIds((prev) => new Set(prev).add(id)); + const result = await api.executeAction({ account_ids: [id], action, params }); + addToast("info", t("misc.taskAction", { id: result.task_id, action, login })); + watchTask(result.task_id, [id]); + } catch (e: unknown) { + setProcessingIds((prev) => { const s = new Set(prev); s.delete(id); return s; }); + addToast("error", t("misc.error", { error: e instanceof Error ? e.message : String(e) })); + } + }; + + const handlePromptSubmit = async () => { + if (!promptData || !promptValue) return; + try { + await api.respondToPrompt(promptData.taskId, promptValue, promptData.login); + setPromptData(null); + } catch (e: unknown) { + addToast("error", t("misc.error", { error: e instanceof Error ? e.message : String(e) })); + } + }; + + const handlePromptCancel = async () => { + if (promptData) { + try { await api.cancelTask(promptData.taskId); } catch { /* task may already be done */ } + } + setPromptData(null); + }; + + const handleBulkAction = () => { + if (!bulkAction) return addToast("warn", t("toast.selectAction")); + if (!selectedIds.size) return addToast("warn", t("toast.selectAccounts")); + + if (bulkAction === "enable_auto_accept") { + const ids = [...selectedIds]; + clearSelection(); + api.startAutoAccept(ids) + .then(() => { addToast("success", t("toast.autoAcceptEnabled", { count: ids.length })); loadAccounts(); }) + .catch((e: unknown) => addToast("error", t("misc.error", { error: e instanceof Error ? e.message : String(e) }))); + return; + } + + const capturedIds = [...selectedIds]; + const capturedAction = bulkAction; + const actionLabel = t(BULK_ACTIONS.find((a) => a.value === capturedAction)?.labelKey || "action.selectAction"); + confirm( + t("confirm.executeBulk", { action: actionLabel, count: capturedIds.length }), + async () => { + try { + const result = await api.executeAction({ account_ids: capturedIds, action: capturedAction }); + clearSelection(); + setProcessingIds((prev) => { const s = new Set(prev); capturedIds.forEach((id) => s.add(id)); return s; }); + addToast("info", t("misc.taskCount", { id: result.task_id, count: result.accounts_count })); + watchTask(result.task_id, capturedIds); + } catch (e: unknown) { + addToast("error", t("misc.error", { error: e instanceof Error ? e.message : String(e) })); + } + }, + ); + }; + + const handleToggleAutoAccept = async (id: number) => { + const acc = accounts.find((a) => a.id === id); + if (!acc) return; + const enable = !acc.auto_accept; + try { + if (enable) await api.startAutoAccept([id]); + else await api.stopAutoAccept([id]); + addToast("info", enable ? t("toast.autoAcceptOn") : t("toast.autoAcceptOff")); + await loadAccounts(); + } catch (e: unknown) { + addToast("error", t("misc.error", { error: e instanceof Error ? e.message : String(e) })); + } + }; + + const handleEdit = (id: number) => setEditId(id); + + const handleSaveEdit = async (id: number, data: AccountUpdate) => { + try { + await api.updateAccount(id, data); + setEditId(null); + addToast("success", t("toast.saved")); + await loadAccounts(); + } catch (e: unknown) { + addToast("error", t("misc.error", { error: e instanceof Error ? e.message : String(e) })); + } + }; + + const handleDelete = (id: number) => { + confirm(t("confirm.deleteAccount"), async () => { + try { + await api.deleteAccount(id); + addToast("success", t("toast.accountDeleted")); + await loadAccounts(); + } catch (e: unknown) { + addToast("error", t("misc.error", { error: e instanceof Error ? e.message : String(e) })); + } + }); + }; + + const handleDeleteSelected = () => { + if (!selectedIds.size) return addToast("warn", t("toast.selectAccounts")); + confirm(t("confirm.deleteSelected", { count: selectedIds.size }), async () => { + try { + const result = await api.deleteBulk([...selectedIds]); + clearSelection(); + addToast("success", t("toast.deleted", { count: result.deleted })); + await loadAccounts(); + } catch (e: unknown) { + addToast("error", t("misc.error", { error: e instanceof Error ? e.message : String(e) })); + } + }); + }; + + const handleDeleteAll = () => { + if (!accounts.length) return addToast("warn", t("toast.noAccounts")); + confirm(t("confirm.deleteAll", { count: accounts.length }), async () => { + try { + const result = await api.deleteBulk([]); + clearSelection(); + addToast("success", t("toast.deleted", { count: result.deleted })); + await loadAccounts(); + } catch (e: unknown) { + addToast("error", t("misc.error", { error: e instanceof Error ? e.message : String(e) })); + } + }); + }; + + const handleAddAccount = async (data: AccountCreate) => { + try { + await api.createAccount(data); + setShowAdd(false); + addToast("success", t("toast.accountAdded")); + await loadAccounts(); + } catch (e: unknown) { + addToast("error", t("misc.error", { error: e instanceof Error ? e.message : String(e) })); + } + }; + + const handleSendToMafiles = () => { + if (!selectedIds.size) return addToast("warn", t("toast.selectAccounts")); + sendToMafileManager([...selectedIds]); + setTab("mafiles"); + addToast("info", t("toast.sentToMafile", { count: selectedIds.size })); + }; + + const handleOpenBrowser = async (id: number) => { + try { + const result = await api.openBrowser(id); + if (result.status === "revalidating") { + addToast("warn", t("toast.cookiesExpiredRevalidating")); + if (result.task_id) { + setProcessingIds((prev) => new Set(prev).add(id)); + watchTask(result.task_id, [id]); + } + } else { + addToast("info", t("toast.browserOpening")); + } + } catch (e: unknown) { + addToast("error", t("misc.error", { error: e instanceof Error ? e.message : String(e) })); + } + }; + + const handleAssignProxies = async () => { + confirm(t("confirm.assignProxies"), async () => { + try { + const r = await api.assignProxies(); + addToast("success", t("toast.proxiesAssigned", { used: r.proxies_used, count: r.assigned })); + await loadAccounts(); + } catch (e: unknown) { + addToast("error", t("misc.error", { error: e instanceof Error ? e.message : String(e) })); + } + }); + }; + + const handleReassignProxies = async () => { + confirm(t("confirm.reassignProxies"), async () => { + try { + const r = await api.reassignProxies(); + addToast("success", t("toast.proxiesReassigned", { used: r.proxies_used, count: r.assigned })); + await loadAccounts(); + } catch (e: unknown) { + addToast("error", t("misc.error", { error: e instanceof Error ? e.message : String(e) })); + } + }); + }; + + const handleClearProxies = async () => { + confirm(t("confirm.clearProxies"), async () => { + try { + const r = await api.clearProxies(); + addToast("success", t("toast.proxiesClearedCount", { count: r.cleared })); + await loadAccounts(); + } catch (e: unknown) { + addToast("error", t("misc.error", { error: e instanceof Error ? e.message : String(e) })); + } + }); + }; + + const editAccount = editId ? accounts.find((a) => a.id === editId) : null; + + const SECTIONS: { id: AccountSection; label: string; icon: React.ReactNode }[] = [ + { id: "mafile", label: "Mafile", icon: }, + { id: "logpass", label: "Log:Pass", icon: }, + { id: "token", label: "Token", icon: }, + ]; + + return ( +
+ {/* Section sidebar */} +
+

{t("section.dataType")}

+ {SECTIONS.map((s) => ( + + ))} + +
+ + {section === "logpass" &&
} + {section === "token" &&
} + {section === "mafile" &&
+ {/* Toolbar */} +
+ + + + {/* Inline progress indicator */} + {activeTask && ( + <> +
+
+ + {activeTask.type === "validate" ? t("task.validate") : + activeTask.type === "change_password" ? t("task.changePassword") : + activeTask.type === "random_password" ? t("task.randomPassword") : + activeTask.type === "change_email" ? t("task.changeEmail") : + activeTask.type === "change_phone" ? t("task.changePhone") : + activeTask.type === "remove_guard" ? t("task.removeGuard") : + activeTask.type} + +
+
1 + ? (activeTask.total > 0 ? Math.round((activeTask.progress / activeTask.total) * 100) : 0) + : (activeTask.total_steps ? Math.round(((activeTask.step ?? 0) / activeTask.total_steps) * 100) : 0)}%` }} + /> +
+ + {activeTask.total > 1 + ? `${activeTask.progress}/${activeTask.total}${activeTask.active_count ? ` (${activeTask.active_count})` : ""}` + : (activeTask.step_label || `${activeTask.progress}/${activeTask.total}`)} + {activeTask.total > 1 + ? ` (${activeTask.total > 0 ? Math.round((activeTask.progress / activeTask.total) * 100) : 0}%)` + : (activeTask.total_steps ? ` (${activeTask.step}/${activeTask.total_steps})` : "")} + + {activeTask.status === "completed" && } + {activeTask.status === "failed" && } +
+ + )} + +
+ +
+ + +
+
+ + {/* Table with action bar */} +
+ {/* Action bar (table header panel) */} +
+ + + {activeTask && activeTask.status === "running" && ( + + )} +
+ +
+ + + +
+ setSearchQuery(e.target.value)} + placeholder={t("ph.search")} + className="bg-dark-700 border border-dark-600 rounded px-2 py-1.5 text-xs w-80 placeholder:text-gray-500 outline-none focus:border-accent" + /> +
+
+ {/* Table */} +
+ +
+
+
} + + {/* Modals */} + {editAccount && ( + setEditId(null)} /> + )} + {showImport && setShowImport(false)} />} + {showAdd && setShowAdd(false)} />} + {confirmMsg && onConfirmAction && ( + { onConfirmAction(); setConfirmMsg(null); setOnConfirmAction(null); }} + onCancel={() => { setConfirmMsg(null); setOnConfirmAction(null); }} + /> + )} + {promptData && ( +
+
e.stopPropagation()}> +

{promptData.message}

+ setPromptValue(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && handlePromptSubmit()} + className="w-full bg-dark-700 border border-dark-600 rounded px-3 py-2 text-sm mb-3" + placeholder={t("prompt.enterValue")} + /> +
+ + +
+
+
+ )} +
+ ); +} diff --git a/frontend/src/components/accounts/ActionMenu.tsx b/frontend/src/components/accounts/ActionMenu.tsx new file mode 100644 index 0000000..0336c6f --- /dev/null +++ b/frontend/src/components/accounts/ActionMenu.tsx @@ -0,0 +1,219 @@ +import { useState, useRef, useEffect, useCallback } from "react"; +import { createPortal } from "react-dom"; +import type { Account } from "@/api/types"; +import { api } from "@/api/client"; +import { useAccountStore } from "@/stores/accountStore"; +import { useUiStore } from "@/stores/uiStore"; +import { IconZap } from "@/components/shared/Icons"; +import { useT } from "@/lib/i18n"; + +const ROW_ACTION_KEYS = [ + { action: "validate", labelKey: "action.validate" }, + { action: "change_password", labelKey: "action.changePassword" }, + { action: "random_password", labelKey: "action.randomPassword" }, + { action: "change_email", labelKey: "action.changeEmail" }, + { action: "change_phone", labelKey: "action.changePhone" }, + { action: "remove_guard", labelKey: "action.removeGuard" }, +] as const; + +const PARAM_ACTION_KEYS: Record = { + change_password: "prompt.newPassword", + change_email: "prompt.newEmail", + change_phone: "prompt.newPhone", +}; + +const PARAM_KEYS: Record = { + change_password: "new_password", + change_email: "new_email", + change_phone: "new_phone", +}; + +const CONFIRM_ACTION_KEYS: Record = { + remove_guard: "confirm.removeGuard", +}; + +interface Props { + account: Account; + onAction: (id: number, action: string, params: Record) => void; + onToggleAutoAccept: (id: number) => void; +} + +export function ActionMenu({ account, onAction, onToggleAutoAccept }: Props) { + const [open, setOpen] = useState(false); + const [paramPrompt, setParamPrompt] = useState<{ action: string; title: string } | null>(null); + const [paramValue, setParamValue] = useState(""); + const [confirmPrompt, setConfirmPrompt] = useState<{ action: string; title: string } | null>(null); + const [menuPos, setMenuPos] = useState<{ top: number; left: number } | null>(null); + const menuRef = useRef(null); + const btnRef = useRef(null); + const addToast = useUiStore((s) => s.addToast); + const loadAccounts = useAccountStore((s) => s.loadAccounts); + const t = useT(); + + const updatePosition = useCallback(() => { + if (!btnRef.current) return; + const rect = btnRef.current.getBoundingClientRect(); + const menuHeight = 320; + const spaceBelow = window.innerHeight - rect.bottom; + const top = spaceBelow < menuHeight + ? Math.max(4, rect.top - menuHeight) + : rect.bottom + 4; + const left = Math.max(4, rect.right - 180); + setMenuPos({ top, left }); + }, []); + + useEffect(() => { + if (!open) return; + updatePosition(); + function handleClick(e: MouseEvent) { + if ( + menuRef.current && !menuRef.current.contains(e.target as Node) && + btnRef.current && !btnRef.current.contains(e.target as Node) + ) { + setOpen(false); + } + } + function handleScroll() { setOpen(false); } + document.addEventListener("mousedown", handleClick); + window.addEventListener("scroll", handleScroll, true); + return () => { + document.removeEventListener("mousedown", handleClick); + window.removeEventListener("scroll", handleScroll, true); + }; + }, [open, updatePosition]); + + const handleAction = (action: string, _label: string) => { + setOpen(false); + const confirmKey = CONFIRM_ACTION_KEYS[action]; + if (confirmKey) { + setConfirmPrompt({ action, title: t(confirmKey) }); + return; + } + const promptKey = PARAM_ACTION_KEYS[action]; + if (promptKey) { + setParamPrompt({ action, title: t(promptKey) }); + setParamValue(""); + } else { + onAction(account.id, action, {}); + } + }; + + const submitParam = () => { + if (!paramPrompt || !paramValue) return; + const paramKey = PARAM_KEYS[paramPrompt.action] || "value"; + onAction(account.id, paramPrompt.action, { [paramKey]: paramValue }); + setParamPrompt(null); + }; + + const aaLabel = account.auto_accept ? t("action.disableAutoAccept") : t("action.enableAutoAcceptLogin"); + + const handleRemoveProxy = async () => { + setOpen(false); + try { + await api.updateAccount(account.id, { proxy: "" }); + addToast("success", t("toast.proxyCleared", { name: account.login })); + await loadAccounts(); + } catch (e: unknown) { + addToast("error", t("misc.error", { error: e instanceof Error ? e.message : String(e) })); + } + }; + + const handleReassignProxy = async () => { + setOpen(false); + try { + const r = await api.reassignProxies(); + addToast("success", t("toast.proxiesReassigned", { used: r.proxies_used, count: r.assigned })); + await loadAccounts(); + } catch (e: unknown) { + addToast("error", t("misc.error", { error: e instanceof Error ? e.message : String(e) })); + } + }; + + return ( + <> +
+ + {open && menuPos && createPortal( +
+ {ROW_ACTION_KEYS.map(({ action, labelKey }) => { + const disabled = action === "remove_guard" && !account.has_revocation_code; + if (disabled) { + return ( + + + + ); + } + return ( + + ); + })} +
+ + +
+ +
, + document.body, + )} +
+ + {paramPrompt && ( +
setParamPrompt(null)}> +
e.stopPropagation()}> +

{paramPrompt.title}

+ setParamValue(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && submitParam()} + className="w-full bg-dark-700 border border-dark-600 rounded px-3 py-2 text-sm mb-3" + placeholder={paramPrompt.action.includes("phone") ? "+7 9123456789" : undefined} + /> +
+ + +
+
+
+ )} + + {confirmPrompt && ( +
setConfirmPrompt(null)}> +
e.stopPropagation()}> +

{confirmPrompt.title}

+
+ + +
+
+
+ )} + + ); +} diff --git a/frontend/src/components/accounts/AddModal.tsx b/frontend/src/components/accounts/AddModal.tsx new file mode 100644 index 0000000..06722eb --- /dev/null +++ b/frontend/src/components/accounts/AddModal.tsx @@ -0,0 +1,41 @@ +import { useState } from "react"; +import type { AccountCreate } from "@/api/types"; +import { useT } from "@/lib/i18n"; + +interface Props { + onSave: (data: AccountCreate) => void; + onClose: () => void; +} + +export function AddModal({ onSave, onClose }: Props) { + const t = useT(); + const [login, setLogin] = useState(""); + const [password, setPassword] = useState(""); + const [email, setEmail] = useState(""); + const [proxy, setProxy] = useState(""); + + const handleSave = () => { + if (!login || !password) return; + onSave({ + login, + password, + email: email || undefined, + proxy: proxy || undefined, + }); + }; + + return ( +
+
e.stopPropagation()}> +

{t("modal.addAccount")}

+
+ setLogin(e.target.value)} placeholder={t("ph.login")} className="w-full bg-dark-700 border border-dark-600 rounded px-3 py-2 text-sm" /> + setPassword(e.target.value)} placeholder={t("ph.password")} className="w-full bg-dark-700 border border-dark-600 rounded px-3 py-2 text-sm" /> + setEmail(e.target.value)} placeholder="Email" className="w-full bg-dark-700 border border-dark-600 rounded px-3 py-2 text-sm" /> + setProxy(e.target.value)} placeholder={t("ph.proxy")} className="w-full bg-dark-700 border border-dark-600 rounded px-3 py-2 text-sm" /> + +
+
+
+ ); +} diff --git a/frontend/src/components/accounts/Badges.tsx b/frontend/src/components/accounts/Badges.tsx new file mode 100644 index 0000000..48fae53 --- /dev/null +++ b/frontend/src/components/accounts/Badges.tsx @@ -0,0 +1,37 @@ +import { IconLock, IconUnlock } from "@/components/shared/Icons"; +import { t } from "@/lib/i18n"; + +interface Props { + status: string; +} + +const STATUS_KEYS: Record = { + unknown: ["badge-unknown", "status.unknown"], + valid: ["badge-ok", "status.valid"], + invalid: ["badge-error", "status.invalid"], + locked: ["badge-error", "status.locked"], + limited: ["badge-running", "status.limited"], +}; + +export function StatusBadge({ status }: Props) { + const entry = STATUS_KEYS[status]; + const [cls, text] = entry ? [entry[0], t(entry[1])] : ["badge-unknown", status]; + return {text}; +} + +export function BanBadge({ status }: { status: string | null }) { + if (status === "BANNED") return {t("status.banned")}; + if (status === "NO BAN") return {t("status.ok")}; + return ; +} + +export function LockBadge({ status }: Props) { + if (status === "locked") return ; + return ; +} + +export function MafileBadge({ hasMafile }: { hasMafile: boolean }) { + return hasMafile + ? + : ; +} diff --git a/frontend/src/components/accounts/ColumnSettingsDropdown.tsx b/frontend/src/components/accounts/ColumnSettingsDropdown.tsx new file mode 100644 index 0000000..2ac994a --- /dev/null +++ b/frontend/src/components/accounts/ColumnSettingsDropdown.tsx @@ -0,0 +1,59 @@ +import { useState, useRef, useEffect } from "react"; +import { useUiStore } from "@/stores/uiStore"; +import type { ColumnSettings } from "@/api/types"; +import { IconSettings } from "@/components/shared/Icons"; +import { useT } from "@/lib/i18n"; + +const COL_LABEL_KEYS: Record = { + browser: "col.browser", profile: "col.profile", last_online: "col.lastOnline", + steam_id: "col.steamId", login: "col.login", password: "col.password", + login_pass: "col.loginPass", email: "col.email", email_pass: "col.emailPass", + email_login_pass: "col.emailLoginPass", phone: "col.phone", status: "col.status", + ban: "col.ban", twofa: "col.twofa", mafile: "col.mafile", proxy: "col.proxy", + notes: "col.notes", actions: "col.actions", +}; + +export function ColumnSettingsDropdown() { + const [open, setOpen] = useState(false); + const cols = useUiStore((s) => s.columnVisibility); + const toggleColumn = useUiStore((s) => s.toggleColumn); + const ref = useRef(null); + const t = useT(); + + useEffect(() => { + const handler = (e: MouseEvent) => { + if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false); + }; + document.addEventListener("mousedown", handler); + return () => document.removeEventListener("mousedown", handler); + }, []); + + return ( +
+ + {open && ( +
+ {(Object.keys(COL_LABEL_KEYS) as (keyof ColumnSettings)[]).map((key) => ( + + ))} +
+ )} +
+ ); +} diff --git a/frontend/src/components/accounts/EditModal.tsx b/frontend/src/components/accounts/EditModal.tsx new file mode 100644 index 0000000..6dea62c --- /dev/null +++ b/frontend/src/components/accounts/EditModal.tsx @@ -0,0 +1,47 @@ +import { useState, useRef, useEffect } from "react"; +import type { Account, AccountUpdate } from "@/api/types"; +import { useT } from "@/lib/i18n"; + +interface Props { + account: Account; + onSave: (id: number, data: AccountUpdate) => void; + onClose: () => void; +} + +export function EditModal({ account, onSave, onClose }: Props) { + const t = useT(); + const [login, setLogin] = useState(account.login); + const [password, setPassword] = useState(account.password); + const [email, setEmail] = useState(account.email ?? ""); + const [proxy, setProxy] = useState(account.proxy ?? ""); + const [notes, setNotes] = useState(account.notes ?? ""); + const ref = useRef(null); + + useEffect(() => { ref.current?.focus(); }, []); + + const handleSave = () => { + onSave(account.id, { + login, + password, + email: email || undefined, + proxy: proxy || undefined, + notes: notes || undefined, + }); + }; + + return ( +
+
e.stopPropagation()}> +

{t("modal.editAccount")}

+
+ setLogin(e.target.value)} placeholder={t("ph.login")} className="w-full bg-dark-700 border border-dark-600 rounded px-3 py-2 text-sm" /> + setPassword(e.target.value)} placeholder={t("ph.password")} className="w-full bg-dark-700 border border-dark-600 rounded px-3 py-2 text-sm" /> + setEmail(e.target.value)} placeholder="Email" className="w-full bg-dark-700 border border-dark-600 rounded px-3 py-2 text-sm" /> + setProxy(e.target.value)} placeholder={t("ph.proxy")} className="w-full bg-dark-700 border border-dark-600 rounded px-3 py-2 text-sm" /> + setNotes(e.target.value)} placeholder={t("ph.notes")} className="w-full bg-dark-700 border border-dark-600 rounded px-3 py-2 text-sm" /> + +
+
+
+ ); +} diff --git a/frontend/src/components/accounts/ImportModal.tsx b/frontend/src/components/accounts/ImportModal.tsx new file mode 100644 index 0000000..39fbf22 --- /dev/null +++ b/frontend/src/components/accounts/ImportModal.tsx @@ -0,0 +1,74 @@ +import { useState, useRef } from "react"; +import { api } from "@/api/client"; +import { useUiStore } from "@/stores/uiStore"; +import { useAccountStore } from "@/stores/accountStore"; +import { IconUpload } from "@/components/shared/Icons"; +import { useT } from "@/lib/i18n"; + +interface Props { + onClose: () => void; +} + +export function ImportModal({ onClose }: Props) { + const t = useT(); + const [file, setFile] = useState(null); + const [result, setResult] = useState(""); + const [uploading, setUploading] = useState(false); + const inputRef = useRef(null); + const addToast = useUiStore((s) => s.addToast); + const loadAccounts = useAccountStore((s) => s.loadAccounts); + + const handleDrop = (e: React.DragEvent) => { + e.preventDefault(); + const f = e.dataTransfer.files[0]; + if (f) setFile(f); + }; + + const doImport = async () => { + if (!file) return; + setUploading(true); + try { + const res = await api.importAccounts(file); + setResult(t("toast.importResult", { imported: res.imported, skipped: res.skipped })); + addToast("success", `${res.imported} ok, ${res.skipped} skip`); + await loadAccounts(); + } catch (e: unknown) { + setResult(e instanceof Error ? e.message : t("misc.errorStr")); + } finally { + setUploading(false); + } + }; + + return ( +
+
e.stopPropagation()}> +

{t("modal.importAccounts")}

+
+

{t("import.formats")} ({t("import.delimiter")} : {t("import.or")} |)

+

login:pass:{"{"}mafile{"}"}

+

login:pass:email:email_pass:{"{"}mafile{"}"}

+

login:pass:email:email_pass

+
+
e.preventDefault()} + onDrop={handleDrop} + onClick={() => inputRef.current?.click()} + className="border-2 border-dashed border-dark-500 rounded-lg p-6 mb-3 text-center cursor-pointer transition-colors hover:border-accent" + > + +

+ {file ? t("import.selected", { name: file.name }) : t("import.dragTxt")} +

+
+ { + const f = e.target.files?.[0]; + if (f) setFile(f); + }} /> + + {result &&

{result}

} +
+
+ ); +} diff --git a/frontend/src/components/accounts/LogpassAddModal.tsx b/frontend/src/components/accounts/LogpassAddModal.tsx new file mode 100644 index 0000000..3085605 --- /dev/null +++ b/frontend/src/components/accounts/LogpassAddModal.tsx @@ -0,0 +1,43 @@ +import { useState } from "react"; +import { api } from "@/api/client"; +import { useLogpassStore } from "@/stores/logpassStore"; +import type { LogpassAccountCreate } from "@/api/types"; +import { useT } from "@/lib/i18n"; + +interface Props { + onClose: () => void; +} + +export function LogpassAddModal({ onClose }: Props) { + const t = useT(); + const [login, setLogin] = useState(""); + const [password, setPassword] = useState(""); + const [proxy, setProxy] = useState(""); + const loadAccounts = useLogpassStore((s) => s.loadAccounts); + + const handleSave = async () => { + if (!login || !password) return; + const data: LogpassAccountCreate = { + login, + password, + proxy: proxy || undefined, + }; + await api.createLogpassAccount(data); + await loadAccounts(); + onClose(); + }; + + return ( +
+
e.stopPropagation()}> +

{t("modal.addAccount")}

+
+ setLogin(e.target.value)} placeholder={t("ph.login")} className="w-full bg-dark-700 border border-dark-600 rounded px-3 py-2 text-sm" /> + setPassword(e.target.value)} placeholder={t("ph.password")} className="w-full bg-dark-700 border border-dark-600 rounded px-3 py-2 text-sm" /> + setProxy(e.target.value)} placeholder={t("ph.proxyOptional")} className="w-full bg-dark-700 border border-dark-600 rounded px-3 py-2 text-sm" /> + +
+
+
+ ); +} diff --git a/frontend/src/components/accounts/LogpassColumnSettingsDropdown.tsx b/frontend/src/components/accounts/LogpassColumnSettingsDropdown.tsx new file mode 100644 index 0000000..a51187e --- /dev/null +++ b/frontend/src/components/accounts/LogpassColumnSettingsDropdown.tsx @@ -0,0 +1,58 @@ +import { useState, useRef, useEffect } from "react"; +import { useUiStore } from "@/stores/uiStore"; +import type { LogpassColumnSettings } from "@/api/types"; +import { IconSettings } from "@/components/shared/Icons"; +import { useT } from "@/lib/i18n"; + +const COL_LABEL_KEYS: Record = { + browser: "col.browser", profile: "col.profile", last_online: "col.lastOnline", + steam_id: "col.steamId", login: "col.login", password: "col.password", + login_pass: "col.loginPass", status: "col.status", ban: "col.ban", + prime: "col.prime", trophy: "col.trophy", behavior: "col.behavior", + license: "col.license", proxy: "col.proxy", notes: "col.notes", actions: "col.actions", +}; + +export function LogpassColumnSettingsDropdown() { + const [open, setOpen] = useState(false); + const cols = useUiStore((s) => s.logpassColumnVisibility); + const toggleColumn = useUiStore((s) => s.toggleLogpassColumn); + const ref = useRef(null); + const t = useT(); + + useEffect(() => { + const handler = (e: MouseEvent) => { + if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false); + }; + document.addEventListener("mousedown", handler); + return () => document.removeEventListener("mousedown", handler); + }, []); + + return ( +
+ + {open && ( +
+ {(Object.keys(COL_LABEL_KEYS) as (keyof LogpassColumnSettings)[]).map((key) => ( + + ))} +
+ )} +
+ ); +} diff --git a/frontend/src/components/accounts/LogpassEditModal.tsx b/frontend/src/components/accounts/LogpassEditModal.tsx new file mode 100644 index 0000000..302c22e --- /dev/null +++ b/frontend/src/components/accounts/LogpassEditModal.tsx @@ -0,0 +1,70 @@ +import { useState } from "react"; +import { api } from "@/api/client"; +import { useLogpassStore } from "@/stores/logpassStore"; +import type { LogpassAccount } from "@/api/types"; +import { useT } from "@/lib/i18n"; + +interface Props { + account: LogpassAccount; + onClose: () => void; +} + +export function LogpassEditModal({ account, onClose }: Props) { + const t = useT(); + const [login, setLogin] = useState(account.login); + const [password, setPassword] = useState(account.password); + const [proxy, setProxy] = useState(account.proxy ?? ""); + const [notes, setNotes] = useState(account.notes ?? ""); + const loadAccounts = useLogpassStore((s) => s.loadAccounts); + + const handleSave = async () => { + if (!login || !password) return; + await api.updateLogpassAccount(account.id, { + login, + password, + proxy: proxy || undefined, + notes: notes || undefined, + }); + await loadAccounts(); + onClose(); + }; + + return ( +
+
e.stopPropagation()}> +

{t("modal.editAccount")}

+
+ setLogin(e.target.value)} + placeholder={t("ph.login")} + className="w-full bg-dark-700 border border-dark-600 rounded px-3 py-2 text-sm" + /> + setPassword(e.target.value)} + placeholder={t("ph.password")} + className="w-full bg-dark-700 border border-dark-600 rounded px-3 py-2 text-sm" + /> + setProxy(e.target.value)} + placeholder={t("ph.proxyOptional")} + className="w-full bg-dark-700 border border-dark-600 rounded px-3 py-2 text-sm" + /> + setNotes(e.target.value)} + placeholder={t("ph.notes")} + className="w-full bg-dark-700 border border-dark-600 rounded px-3 py-2 text-sm" + /> +
+ + +
+
+
+
+ ); +} diff --git a/frontend/src/components/accounts/LogpassImportModal.tsx b/frontend/src/components/accounts/LogpassImportModal.tsx new file mode 100644 index 0000000..792e8c8 --- /dev/null +++ b/frontend/src/components/accounts/LogpassImportModal.tsx @@ -0,0 +1,78 @@ +import { useState, useRef } from "react"; +import { api } from "@/api/client"; +import { useLogpassStore } from "@/stores/logpassStore"; +import { useUiStore } from "@/stores/uiStore"; +import { IconUpload } from "@/components/shared/Icons"; +import { useT } from "@/lib/i18n"; + +interface Props { + onClose: () => void; +} + +export function LogpassImportModal({ onClose }: Props) { + const t = useT(); + const [file, setFile] = useState(null); + const [result, setResult] = useState(""); + const [uploading, setUploading] = useState(false); + const inputRef = useRef(null); + const addToast = useUiStore((s) => s.addToast); + const loadAccounts = useLogpassStore((s) => s.loadAccounts); + + const handleDrop = (e: React.DragEvent) => { + e.preventDefault(); + const f = e.dataTransfer.files[0]; + if (f) setFile(f); + }; + + const doImport = async () => { + if (!file) return; + setUploading(true); + try { + const text = await file.text(); + const lines = text.split("\n").map((l) => l.trim()).filter(Boolean); + if (!lines.length) { setResult(t("toast.fileEmpty")); setUploading(false); return; } + const res = await api.importLogpass(lines); + setResult(t("toast.importResult", { imported: res.imported, skipped: res.skipped })); + addToast("success", `${res.imported} ok, ${res.skipped} skip`); + await loadAccounts(); + } catch (e: unknown) { + setResult(e instanceof Error ? e.message : t("misc.errorStr")); + } finally { + setUploading(false); + } + }; + + return ( +
+
e.stopPropagation()}> +

{t("modal.importAccounts")}

+
+

{t("import.formatsColon")}

+

login:pass

+

login|pass

+

CSV: login,password,steam_id,ban,prime,trophy,behavior,license

+

⚠ Mafile-файлы и JSON не принимаются. Только log:pass / log|pass.

+
+
e.preventDefault()} + onDrop={handleDrop} + onClick={() => inputRef.current?.click()} + className="border-2 border-dashed border-dark-500 rounded-lg p-6 mb-3 text-center cursor-pointer transition-colors hover:border-accent" + > + +

+ {file ? t("import.selected", { name: file.name }) : t("import.dragTxtCsv")} +

+
+ { + const f = e.target.files?.[0]; + if (f) setFile(f); + }} /> + + {result &&

{result}

} +
+
+ ); +} diff --git a/frontend/src/components/accounts/LogpassTab.tsx b/frontend/src/components/accounts/LogpassTab.tsx new file mode 100644 index 0000000..cc0546a --- /dev/null +++ b/frontend/src/components/accounts/LogpassTab.tsx @@ -0,0 +1,658 @@ +import { useEffect, useState, useCallback, useMemo, useRef } from "react"; +import { api } from "@/api/client"; +import { useLogpassStore } from "@/stores/logpassStore"; +import { useUiStore } from "@/stores/uiStore"; +import { ConfirmModal } from "@/components/shared/Modals"; +import { LogpassImportModal } from "./LogpassImportModal"; +import { LogpassAddModal } from "./LogpassAddModal"; +import { LogpassEditModal } from "./LogpassEditModal"; +import { LogpassColumnSettingsDropdown } from "./LogpassColumnSettingsDropdown"; +import { StatusBadge, BanBadge } from "./Badges"; +import { SteamLevelBadge } from "./SteamLevelBadge"; +import { copyText } from "@/lib/clipboard"; +import { + IconDownload, IconPlus, IconPlay, IconTrash, IconCheckCircle, IconXCircle, + IconEye, IconEyeOff, IconGlobe, IconRefresh, IconBan, + IconCheck, IconAlertTriangle, IconEdit, +} from "@/components/shared/Icons"; +import type { Task, LogpassColumnSettings } from "@/api/types"; +import { useT } from "@/lib/i18n"; + +type AccResults = Record; +type AccSteps = Record; + +function parseAccResults(raw: Task["account_results"]): AccResults | null { + if (!raw) return null; + if (typeof raw === "string") { + try { return JSON.parse(raw) as AccResults; } catch { return null; } + } + return raw; +} + +export function LogpassTab() { + const accounts = useLogpassStore((s) => s.accounts); + const selectedIds = useLogpassStore((s) => s.selectedIds); + const loadAccounts = useLogpassStore((s) => s.loadAccounts); + const selectAll = useLogpassStore((s) => s.selectAll); + const clearSelection = useLogpassStore((s) => s.clearSelection); + const addToast = useUiStore((s) => s.addToast); + const cols = useUiStore((s) => s.logpassColumnVisibility); + const t = useT(); + + const [searchQuery, setSearchQuery] = useState(""); + const [showImport, setShowImport] = useState(false); + const [showAdd, setShowAdd] = useState(false); + const [editAccountId, setEditAccountId] = useState(null); + const [confirmModal, setConfirmModal] = useState<{ open: boolean; message: string; onConfirm: () => void }>({ open: false, message: "", onConfirm: () => {} }); + const [processingIds, setProcessingIds] = useState>(new Set()); + const [accountResults, setAccountResults] = useState({}); + const [accountSteps, setAccountSteps] = useState({}); + const [activeTask, setActiveTask] = useState(null); + const BATCH = 100; + const [visibleCount, setVisibleCount] = useState(BATCH); + + type SortField = "last_online" | "prime" | "trophy" | "behavior" | null; + type SortDir = "asc" | "desc"; + const [sortField, setSortField] = useState(null); + const [sortDir, setSortDir] = useState("asc"); + const toggleSort = (field: SortField) => { + if (sortField === field) { + if (sortDir === "asc") setSortDir("desc"); + else { setSortField(null); setSortDir("asc"); } + } else { + setSortField(field); + setSortDir("asc"); + } + }; + const sentinelRef = useRef(null); + + useEffect(() => { loadAccounts(); }, [loadAccounts]); + + const confirm = (msg: string, fn: () => void) => setConfirmModal({ open: true, message: msg, onConfirm: fn }); + + const watchTask = useCallback((taskId: string) => { + const es = new EventSource(`/api/tasks/${taskId}/stream`); + es.onmessage = (ev) => { + try { + const data = JSON.parse(ev.data) as Task; + setActiveTask(data); + if (data.account_results) { + const r = parseAccResults(data.account_results); + if (r) setAccountResults(r); + } + if (data.account_steps) setAccountSteps(data.account_steps as AccSteps); + if (data.status !== "running") { + es.close(); + setProcessingIds(new Set()); + setActiveTask(null); + loadAccounts(); + } + } catch { /* ignore */ } + }; + es.onerror = () => es.close(); + }, [loadAccounts]); + + const handleValidate = useCallback(async () => { + const ids = [...selectedIds]; + if (!ids.length) return; + clearSelection(); + setProcessingIds(new Set(ids)); + setAccountResults({}); + setAccountSteps({}); + try { + const { task_id } = await api.validateLogpass(ids); + setActiveTask({ id: task_id, type: "logpass_validate", status: "running", progress: 0, total: ids.length, result: null, error: null, created_at: "", updated_at: "" }); + watchTask(task_id); + } catch (err) { + setProcessingIds(new Set()); + addToast("error", String(err)); + } + }, [selectedIds, clearSelection, watchTask, addToast]); + + const handleValidateSingle = useCallback(async (id: number) => { + setProcessingIds(new Set([id])); + setAccountResults((r) => { const n = { ...r }; delete n[String(id)]; return n; }); + setAccountSteps((s) => { const n = { ...s }; delete n[String(id)]; return n; }); + try { + const { task_id } = await api.validateLogpass([id]); + setActiveTask({ id: task_id, type: "logpass_validate", status: "running", progress: 0, total: 1, result: null, error: null, created_at: "", updated_at: "" }); + watchTask(task_id); + } catch (err) { + setProcessingIds(new Set()); + addToast("error", String(err)); + } + }, [watchTask, addToast]); + + const handleFullParse = useCallback(async () => { + const ids = [...selectedIds]; + if (!ids.length) return; + clearSelection(); + setProcessingIds(new Set(ids)); + setAccountResults({}); + setAccountSteps({}); + try { + const { task_id } = await api.fullParseLogpass(ids); + setActiveTask({ id: task_id, type: "logpass_full_parse", status: "running", progress: 0, total: ids.length, result: null, error: null, created_at: "", updated_at: "" }); + watchTask(task_id); + } catch (err) { + setProcessingIds(new Set()); + addToast("error", String(err)); + } + }, [selectedIds, clearSelection, watchTask, addToast]); + + const handleDeleteSelected = () => { + const ids = [...selectedIds]; + if (!ids.length) return; + confirm(t("confirm.deleteLogpassSelected", { count: ids.length }), async () => { + await api.deleteLogpassBulk(ids); + clearSelection(); + loadAccounts(); + }); + }; + + const handleDeleteAll = () => { + confirm(t("confirm.deleteLogpassAll", { count: accounts.length }), async () => { + await api.deleteLogpassBulk(accounts.map((a) => a.id)); + clearSelection(); + loadAccounts(); + }); + }; + + const handleAssignProxies = () => { + confirm(t("confirm.assignProxies"), async () => { + try { + const r = await api.assignLogpassProxies(); + addToast("success", t("toast.proxiesAssignedShort", { count: r.assigned })); + loadAccounts(); + } catch (e: unknown) { addToast("error", e instanceof Error ? e.message : String(e)); } + }); + }; + + const handleReassignProxies = () => { + confirm(t("confirm.reassignProxiesAll"), async () => { + try { + const r = await api.reassignLogpassProxies(); + addToast("success", t("toast.proxiesReassignedShort", { count: r.assigned })); + loadAccounts(); + } catch (e: unknown) { addToast("error", e instanceof Error ? e.message : String(e)); } + }); + }; + + const handleClearProxies = () => { + confirm(t("confirm.clearProxies"), async () => { + try { + const r = await api.clearLogpassProxies(); + addToast("success", t("toast.proxiesClearedShort", { count: r.cleared })); + loadAccounts(); + } catch (e: unknown) { addToast("error", e instanceof Error ? e.message : String(e)); } + }); + }; + + const allSelected = accounts.length > 0 && selectedIds.size === accounts.length; + const filtered = useMemo(() => { + if (!searchQuery.trim()) return accounts; + const q = searchQuery.trim(); + + // lvl comparison syntax: lvl>10, lvl<10, lvl>=10, lvl<=10, lvl=10 + const lvlMatch = q.match(/^lvl\s*(>=|<=|>|<|=)\s*(\d+)$/i); + if (lvlMatch) { + const op = lvlMatch[1]; + const val = parseInt(lvlMatch[2], 10); + return accounts.filter((a) => { + const lvl = a.steam_level; + if (lvl == null) return false; + if (op === ">") return lvl > val; + if (op === ">=") return lvl >= val; + if (op === "<") return lvl < val; + if (op === "<=") return lvl <= val; + return lvl === val; + }); + } + + const ql = q.toLowerCase(); + return accounts.filter((a) => + a.login.toLowerCase().includes(ql) || + (a.steam_id ?? "").toLowerCase().includes(ql) || + (a.nickname ?? "").toLowerCase().includes(ql) || + (a.notes ?? "").toLowerCase().includes(ql) || + (a.license ?? "").toLowerCase().includes(ql) + ); + }, [accounts, searchQuery]); + + const sorted = useMemo(() => { + if (!sortField) return filtered; + const dir = sortDir === "asc" ? 1 : -1; + const EMPTY = Symbol(); + return [...filtered].sort((a, b) => { + let va: number | typeof EMPTY = EMPTY; + let vb: number | typeof EMPTY = EMPTY; + if (sortField === "last_online") { + const parseOnline = (v: string | null | undefined): number | typeof EMPTY => { + if (!v || v === "\u2014") return EMPTY; + if (v === "online") return -1; + const m = v.match(/^(\d+)([mhd])$/i); + if (!m) return EMPTY; + const n = parseInt(m[1], 10); + if (m[2] === "m") return n; + if (m[2] === "h") return n * 60; + return n * 1440; + }; + va = parseOnline(a.last_online); + vb = parseOnline(b.last_online); + } else if (sortField === "prime") { + va = a.prime === "Enabled" ? 0 : a.prime === "Disabled" ? 1 : EMPTY; + vb = b.prime === "Enabled" ? 0 : b.prime === "Disabled" ? 1 : EMPTY; + } else if (sortField === "trophy") { + va = a.trophy != null && a.trophy !== "\u2014" ? parseInt(a.trophy, 10) || 0 : EMPTY; + vb = b.trophy != null && b.trophy !== "\u2014" ? parseInt(b.trophy, 10) || 0 : EMPTY; + } else if (sortField === "behavior") { + va = a.behavior != null && a.behavior !== "\u2014" ? parseInt(a.behavior, 10) || 0 : EMPTY; + vb = b.behavior != null && b.behavior !== "\u2014" ? parseInt(b.behavior, 10) || 0 : EMPTY; + } + // Empty values always sink to the bottom + if (va === EMPTY && vb === EMPTY) return 0; + if (va === EMPTY) return 1; + if (vb === EMPTY) return -1; + if (va === vb) return 0; + return (va < vb ? -1 : 1) * dir; + }); + }, [filtered, sortField, sortDir]); + + useEffect(() => { setVisibleCount(BATCH); }, [searchQuery, sortField, sortDir]); + + const visible = useMemo(() => sorted.slice(0, visibleCount), [sorted, visibleCount]); + + useEffect(() => { + const el = sentinelRef.current; + if (!el) return; + const obs = new IntersectionObserver((entries) => { + if (entries[0].isIntersecting) setVisibleCount((c) => Math.min(c + BATCH, sorted.length)); + }, { rootMargin: "400px" }); + obs.observe(el); + return () => obs.disconnect(); + }, [sorted.length, visibleCount]); + + const proxyLabels = useMemo(() => { + const map = new Map(); + const unique: string[] = []; + for (const a of accounts) { + if (a.proxy && !unique.includes(a.proxy)) unique.push(a.proxy); + } + const proxyToNum = new Map(); + unique.forEach((p, i) => proxyToNum.set(p, i + 1)); + for (const a of accounts) { + if (a.proxy) map.set(a.id, t("proxy.label", { num: proxyToNum.get(a.proxy) ?? 0 })); + } + return map; + }, [accounts]); + + const handleOpenBrowser = async (id: number) => { + try { + const result = await api.openLogpassBrowser(id); + if (result.status === "revalidating") { + addToast("warn", t("toast.cookiesExpiredRevalidating")); + if (result.task_id) { + setProcessingIds(new Set([id])); + setActiveTask({ id: result.task_id, type: "logpass_validate", status: "running", progress: 0, total: 1, result: null, error: null, created_at: "", updated_at: "" }); + watchTask(result.task_id); + } + } else { + addToast("success", t("toast.browserOpening")); + } + } catch (e: unknown) { + addToast("error", e instanceof Error ? e.message : String(e)); + } + }; + + return ( +
+ {/* Toolbar */} +
+ + + + {/* Progress indicator — exact match to AccountsTab */} + {activeTask && ( + <> +
+
+ {t("task.validate")} +
+
1 + ? (activeTask.total > 0 ? Math.round((activeTask.progress / activeTask.total) * 100) : 0) + : (activeTask.total_steps ? Math.round(((activeTask.step ?? 0) / activeTask.total_steps) * 100) : 0)}%` }} + /> +
+ + {activeTask.total > 1 + ? `${activeTask.progress}/${activeTask.total}${activeTask.active_count ? ` (${activeTask.active_count})` : ""}` + : (activeTask.step_label || `${activeTask.progress}/${activeTask.total}`)} + {activeTask.total > 1 + ? ` (${activeTask.total > 0 ? Math.round((activeTask.progress / activeTask.total) * 100) : 0}%)` + : (activeTask.total_steps ? ` (${activeTask.step}/${activeTask.total_steps})` : "")} + + {activeTask.status === "completed" && } + {activeTask.status === "failed" && } +
+ + )} + +
+ +
+ + +
+
+ + {/* Table with action bar */} +
+ {/* Action bar */} +
+ + + {activeTask && activeTask.status === "running" && ( + + )} +
+ + + +
+ setSearchQuery(e.target.value)} + placeholder={t("ph.searchLogpass")} + className="bg-dark-700 border border-dark-600 rounded px-2 py-1.5 text-xs w-80 placeholder:text-gray-500 outline-none focus:border-accent" + /> +
+
+ + {/* Table */} +
+ + + + + } + {cols.profile && } + {cols.last_online && } + {cols.steam_id && } + {cols.login && } + {cols.password && } + {cols.login_pass && } + {cols.status && } + {cols.ban && } + {cols.prime && } + {cols.trophy && } + {cols.behavior && } + {cols.license && } + {cols.proxy && } + {cols.notes && } + {cols.actions && } + + + + {filtered.length === 0 ? ( + + ) : ( + <> + {visible.map((a) => ( + confirm(t("confirm.deleteLogpass", { name: a.login }), async () => { await api.deleteLogpassAccount(id); loadAccounts(); })} + onOpenBrowser={handleOpenBrowser} + onValidate={handleValidateSingle} + onEdit={(id) => setEditAccountId(id)} + /> + ))} + {visibleCount < filtered.length && ( + + )} + + )} + +
+
+ allSelected ? clearSelection() : selectAll()} /> + {selectedIds.size > 0 && {selectedIds.size}} +
+
+ {cols.browser && {t("col.browser")}{t("col.profile")} toggleSort("last_online")}>{t("col.lastOnline")}{sortField === "last_online" ? (sortDir === "asc" ? " ▲" : " ▼") : ""}Steam ID{t("col.login")}{t("col.password")}Login:Pass{t("col.status")}{t("col.ban")} toggleSort("prime")}>Prime{sortField === "prime" ? (sortDir === "asc" ? " ▲" : " ▼") : ""} toggleSort("trophy")}>Trophy{sortField === "trophy" ? (sortDir === "asc" ? " ▲" : " ▼") : ""} toggleSort("behavior")}>Behavior{sortField === "behavior" ? (sortDir === "asc" ? " ▲" : " ▼") : ""}{t("col.license")}{t("col.proxy")}{t("col.notes")}{t("col.actions")}
{t("empty.logpass")}
{t("paging.shown", { visible: visibleCount, total: filtered.length })}
+
+
+ + {editAccountId != null && (() => { + const acc = accounts.find((a) => a.id === editAccountId); + return acc ? setEditAccountId(null)} /> : null; + })()} + {showImport && setShowImport(false)} />} + {showAdd && setShowAdd(false)} />} + {confirmModal.open && ( + { confirmModal.onConfirm(); setConfirmModal((p) => ({ ...p, open: false })); }} + onCancel={() => setConfirmModal((p) => ({ ...p, open: false }))} + /> + )} +
+ ); +} + +/* ─── Row component ─── */ + +function LogpassRow({ account: a, cols, isProcessing, accountResult, accountStep, proxyLabel, onDelete, onOpenBrowser, onValidate, onEdit }: { + account: import("@/api/types").LogpassAccount; + cols: LogpassColumnSettings; + isProcessing: boolean; + accountResult?: { status: string; error?: string }; + accountStep?: { step: number; total: number }; + proxyLabel: string | null; + onDelete: (id: number) => void; + onOpenBrowser: (id: number) => void; + onValidate: (id: number) => void; + onEdit: (id: number) => void; +}) { + const selectedIds = useLogpassStore((s) => s.selectedIds); + const toggleSelect = useLogpassStore((s) => s.toggleSelect); + const addToast = useUiStore((s) => s.addToast); + const hidePasswords = useUiStore((s) => s.hidePasswords); + const loadAccounts = useLogpassStore((s) => s.loadAccounts); + const [revealed, setRevealed] = useState(false); + const [editingNotes, setEditingNotes] = useState(false); + const [notesValue, setNotesValue] = useState(a.notes || ""); + const [licExpanded, setLicExpanded] = useState(false); + + const t = useT(); + const masked = hidePasswords && !revealed; + const dots = "••••••••"; + + const handleCopy = async (text: string) => { + const ok = await copyText(text); + addToast(ok ? "success" : "error", ok ? t("toast.copied") : t("toast.copyFailed")); + }; + + return ( + + + toggleSelect(a.id)} /> + + {/* Progress/status icon */} + + {isProcessing ? ( +
+
+ {accountStep && [{accountStep.step}/{accountStep.total}]} +
+ ) : accountResult?.status === "ok" ? ( + + ) : accountResult?.status === "error" ? ( + + ) : null} + + {/* Browser login button */} + {cols.browser && ( + + {a.has_cookies ? ( + + ) : ( + + )} + + )} + {/* Profile — avatar + nickname + level */} + {cols.profile && ( + +
+ {a.avatar_url && } + {a.nickname || "—"} + {a.steam_level != null && } +
+ + )} + {/* Last online */} + {cols.last_online && ( + + {a.last_online || "—"} + + )} + {/* Steam ID */} + {cols.steam_id && ( + + {a.steam_id ? ( + + {a.steam_id} + + ) : "—"} + + )} + {/* Login */} + {cols.login && {a.login}} + {/* Password */} + {cols.password && ( + + handleCopy(a.password)} title={t("tip.copyPassword")}> + {masked ? dots : a.password} + + + + )} + {/* Login:Pass */} + {cols.login_pass && ( + + handleCopy(`${a.login}:${a.password}`)} title={t("tip.copyLoginPass")}> + {masked ? `${a.login}:${dots}` : `${a.login}:${a.password}`} + + + + )} + {/* Status */} + {cols.status && } + {/* Ban */} + {cols.ban && } + {/* Prime, Trophy, Behavior */} + {cols.prime && {a.prime ?? "—"}} + {cols.trophy && {a.trophy ?? "—"}} + {cols.behavior && {a.behavior ?? "—"}} + {/* License — expandable on click */} + {cols.license && ( + setLicExpanded((v) => !v)} + > + {a.license ?? "—"} + + )} + {/* Proxy */} + {cols.proxy && ( + + {a.proxy ? ( + handleCopy(a.proxy!)} title={a.proxy}> + {proxyLabel || a.proxy} + + ) : "—"} + + )} + {/* Notes — inline editable */} + {cols.notes && ( + + {editingNotes ? ( + setNotesValue(e.target.value)} + onBlur={async () => { + setEditingNotes(false); + if (notesValue !== (a.notes || "")) { + try { + await api.updateLogpassAccount(a.id, { notes: notesValue }); + addToast("success", t("toast.noteSaved")); + loadAccounts(); + } catch { addToast("error", t("toast.noteSaveError")); } + } + }} + onKeyDown={(e) => { if (e.key === "Enter") (e.target as HTMLInputElement).blur(); if (e.key === "Escape") { setNotesValue(a.notes || ""); setEditingNotes(false); } }} + /> + ) : ( + { setNotesValue(a.notes || ""); setEditingNotes(true); }} + title={a.notes || t("tip.addNote")} + > + {a.notes || "—"} + + )} + + )} + {/* Actions */} + {cols.actions && ( + +
+ + + +
+ + )} + + ); +} \ No newline at end of file diff --git a/frontend/src/components/accounts/SteamLevelBadge.tsx b/frontend/src/components/accounts/SteamLevelBadge.tsx new file mode 100644 index 0000000..62dce09 --- /dev/null +++ b/frontend/src/components/accounts/SteamLevelBadge.tsx @@ -0,0 +1,21 @@ +interface Props { + level: number; +} + +export function SteamLevelBadge({ level }: Props) { + if (level >= 100) { + const hundred = Math.floor(level / 100) * 100; + return ( +
+ {level} +
+ ); + } + + const ten = Math.floor(level / 10) * 10; + return ( +
+ {level} +
+ ); +} diff --git a/frontend/src/components/accounts/TokenAddModal.tsx b/frontend/src/components/accounts/TokenAddModal.tsx new file mode 100644 index 0000000..a0b4c32 --- /dev/null +++ b/frontend/src/components/accounts/TokenAddModal.tsx @@ -0,0 +1,53 @@ +import { useState } from "react"; +import { api } from "@/api/client"; +import { useTokenStore } from "@/stores/tokenStore"; +import type { TokenAccountCreate } from "@/api/types"; +import { useT } from "@/lib/i18n"; + +interface Props { + onClose: () => void; +} + +export function TokenAddModal({ onClose }: Props) { + const t = useT(); + const [token, setToken] = useState(""); + const [login, setLogin] = useState(""); + const [proxy, setProxy] = useState(""); + const [notes, setNotes] = useState(""); + const loadAccounts = useTokenStore((s) => s.loadAccounts); + + const handleSave = async () => { + if (!token.trim()) return; + const data: TokenAccountCreate = { + token: token.trim(), + login: login.trim() || undefined, + proxy: proxy.trim() || undefined, + notes: notes.trim() || undefined, + }; + await api.createTokenAccount(data); + await loadAccounts(); + onClose(); + }; + + return ( +
+
e.stopPropagation()}> +

{t("modal.addToken")}

+
+