const { useState: useStateS, useMemo: useMemoS, useRef: useRefS, useEffect: useEffectS } = React; function Sidebar({ active, onChange, instancesCount, sidebarStyle, codeStats, codeFolders, onSelectFolder, selectedCode, liveStatus, user, onLogout }) { const showLabels = sidebarStyle !== "icons"; const items = [ { id: "scheduler", label: "Scheduler", icon: I.Clock }, { id: "instances", label: "Instances", icon: I.Server, count: instancesCount }, { id: "refresher", label: "Refresher", icon: I.Refresh }]; return ( ); } function SidebarItem({ active, onClick, label, icon: Cmp, count, showLabels }) { return ( ); } function SidebarFolder({ folder, selected, onSelect }) { const colorForCode = useColorForCode(); const c = colorForCode(folder.code); return (
{if (!selected) e.currentTarget.style.background = 'var(--card)';}} onMouseLeave={(e) => {if (!selected) e.currentTarget.style.background = 'transparent';}} onClick={onSelect}> {selected && } {folder.code} {folder.size}
); } /* ========================================================================= TOPBAR ========================================================================= */ function Topbar({ title, subtitle, search, setSearch, right, breadcrumbs }) { return (
{breadcrumbs ?
{breadcrumbs.map((b, i) => {i > 0 && /} {b.onClick ? : {b.label} } )}
:

{title}

} {subtitle && {subtitle}}
{search != null && } {right}
); } function InstancesScreen({ instances, codes, codeFolders, density, tableZebra, selected, setSelected, filterCode, setFilterCode, filterStatus, setFilterStatus, onLaunch, onStop, onReset, askConfirm }) { const [search, setSearch] = useStateS(""); const [activeId, setActiveId] = useStateS(null); const stats = useMemoS(() => { const s = { running: 0, booting: 0, idle: 0, stopped: 0, error: 0 }; instances.forEach((i) => { s[i.status] = (s[i.status] || 0) + 1; }); return s; }, [instances]); const filtered = useMemoS(() => { let out = instances; if (filterCode) out = out.filter((p) => p.code === filterCode); if (filterStatus) out = out.filter((p) => p.status === filterStatus); if (search.trim()) { const s = search.toLowerCase(); out = out.filter((p) => p.name.toLowerCase().includes(s) || p.id.toLowerCase().includes(s) || (p.code || "").toLowerCase().includes(s) || p.account.toLowerCase().includes(s) || (p.task || "").toLowerCase().includes(s) ); } return out; }, [instances, search, filterCode, filterStatus]); const groups = useMemoS(() => { const m = new Map(); filtered.forEach((i) => { const key = i.code || "__none__"; if (!m.has(key)) m.set(key, []); m.get(key).push(i); }); return [...m.entries()].map(([code, list]) => ({ code, list })); }, [filtered]); const [collapsed, setCollapsed] = useStateS(() => new Set()); const toggleGroup = (code) => setCollapsed((prev) => { const n = new Set(prev); n.has(code) ? n.delete(code) : n.add(code); return n; }); const visibleOrder = useMemoS(() => { const out = []; groups.forEach((g) => {if (!collapsed.has(g.code)) out.push(...g.list.map((i) => i.id));}); return out; }, [groups, collapsed]); const onRowClick = (_e, id) => setActiveId(id); useEffectS(() => { const onKey = (e) => { const tag = (e.target.tagName || "").toLowerCase(); if (tag === "input" || tag === "textarea") return; if (e.key === "Escape") setActiveId(null); }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, []); const active = activeId ? instances.find((p) => p.id === activeId) : null; const folderFilter = filterCode ? codeFolders.find((f) => f.code === filterCode) : null; const breadcrumbs = folderFilter ? [{ label: "Instances", onClick: () => setFilterCode(null) }, { label: folderFilter.code }] : [{ label: "Instances" }]; return (
} /> {filterStatus &&
filter {STATUSES[filterStatus].label}
} {/* Статистика по статусам — наглядная сводка вместо multi-select toolbar */}
{[ { key: 'running', label: 'in game', color: 'var(--ok)', dot: '#5EE083' }, { key: 'booting', label: 'starting', color: 'var(--warn)', dot: '#D7B074' }, { key: 'idle', label: 'idle', color: 'var(--ink-3)', dot: '#777' }, { key: 'stopped', label: 'stopped', color: 'var(--ink-4)', dot: '#555' }, { key: 'error', label: 'error', color: 'var(--bad)', dot: '#FF6B6B' }, ].map(({ key, label, dot }) => { const v = stats[key] || 0; const dim = v === 0; return (
{v} {label}
); })}
{filtered.length === 0 ? : {groups.map((g) => toggleGroup(g.code)} activeId={activeId} density={density} zebra={tableZebra} onRowClick={onRowClick} /> )}
Instance Task Status Map (live) Account
}
{active && setActiveId(null)} onLaunch={onLaunch} onStop={onStop} onReset={onReset} /> }
); } function GroupBlock({ group, isCollapsed, onToggle, activeId, density, zebra, onRowClick }) { const list = group.list; const colorForCode = useColorForCode(); const c = colorForCode(group.code); const counts = list.reduce((acc, i) => {acc[i.status] = (acc[i.status] || 0) + 1;return acc;}, {}); return ( <>
{list.length} instance{list.length !== 1 ? 's' : ''} {counts.running > 0 && ● {counts.running}} {counts.booting > 0 && ◐ {counts.booting}} {counts.stopped > 0 && ○ {counts.stopped}} {counts.error > 0 && ✕ {counts.error}} {counts.idle > 0 && ○ {counts.idle}}
{!isCollapsed && list.map((p, i) => onRowClick(e, p.id)} /> )} ); } function Th({ children, width, style }) { return ( {children}); } function TaskCell({ task, status }) { const inactive = status === "stopped" || status === "error" || !task || task === "—"; const color = inactive ? 'var(--ink-5)' : 'var(--ink-2)'; return ( {!inactive && (status === "running" || status === "booting") && } {task || "—"} ); } function InstanceRow({ instance, active, zebra, density, onClick }) { const padY = density === "compact" ? 8 : 12; const className = active ? "row-active anim-row" : "anim-row"; const rowBg = active ? 'var(--card-hi)' : zebra ? 'var(--panel-2)' : 'transparent'; return ( {if (!active) e.currentTarget.style.background = 'var(--panel-2)';}} onMouseLeave={(e) => {if (!active) e.currentTarget.style.background = zebra ? 'var(--panel-2)' : 'transparent';}}>
{instance.name}
{instance.id}
{instance.code ? : } {instance.account} ); } function Td({ children, style, stop }) { return e.stopPropagation() : undefined}>{children}; } function FilterMenu({ filterStatus, setFilterStatus }) { const [open, setOpen] = useStateS(false); const ref = useRefS(null); return ( <> setOpen(false)} anchorRef={ref} width={230}>
{Object.entries(STATUSES).map(([id, s]) =>
{setFilterStatus(filterStatus === id ? null : id);setOpen(false);}} style={{ padding: '7px 10px', borderRadius: 5, cursor: 'pointer', display: 'flex', alignItems: 'center', gap: 10, background: filterStatus === id ? 'var(--panel-2)' : 'transparent', color: filterStatus === id ? 'var(--ink)' : 'var(--ink-2)', fontSize: 13.5 }} onMouseEnter={(e) => e.currentTarget.style.background = 'var(--panel-2)'} onMouseLeave={(e) => {if (filterStatus !== id) e.currentTarget.style.background = 'transparent';}}> {s.label} {filterStatus === id && }
)}
); } function EmptyState() { return (
No instances match the filter
); } /* ========================================================================= MAP CODES — clean list view ========================================================================= */ function MapCodesScreen({ instances, codes, onCreateCode }) { const [search, setSearch] = useStateS(""); const [createOpen, setCreateOpen] = useStateS(false); const colorForCode = useColorForCode(); const counts = useMemoS(() => { const m = {}; codes.forEach((c) => m[c] = 0); instances.forEach((i) => {if (m[i.code] != null) m[i.code]++;}); return m; }, [instances, codes]); const filtered = codes.filter((c) => !search || c.toLowerCase().includes(search.toLowerCase())); const total = instances.length; return (
setCreateOpen(true)}> New code} />
{filtered.map((c, i) => { const color = colorForCode(c); const count = counts[c]; const pct = total ? count / total * 100 : 0; return (
e.currentTarget.style.background = 'var(--card-hi)'} onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'}>
{c} {color.name}
0 ? 'var(--ink)' : 'var(--ink-5)', minWidth: 36, textAlign: 'right' }}>{count}
); })} {filtered.length === 0 &&
No codes match the filter
}
{createOpen && setCreateOpen(false)} onCreate={onCreateCode} />}
); } // Fortnite map code: ровно 12 цифр через дефисы 0000-0000-0000. const MAP_CODE_RE_S = /^\d{4}-\d{4}-\d{4}$/; function formatMapCodeS(raw) { const d = (raw || "").replace(/\D/g, "").slice(0, 12); const parts = []; if (d.length > 0) parts.push(d.slice(0, 4)); if (d.length > 4) parts.push(d.slice(4, 8)); if (d.length > 8) parts.push(d.slice(8, 12)); return parts.join("-"); } function CreateCodeModal({ codes, onClose, onCreate }) { const [name, setName] = useStateS(""); const trimmed = name.trim(); const exists = codes.includes(trimmed); const valid = MAP_CODE_RE_S.test(trimmed) && !exists; const previewColor = CODE_PALETTE[codes.length % CODE_PALETTE.length]; const submit = () => {if (valid) {onCreate(trimmed);onClose();}}; return ( <>

New map code

Fortnite map code in format 0000-0000-0000 (12 digits). Example: 9695-5615-4966

setName(formatMapCodeS(e.target.value))} onKeyDown={(e) => {if (e.key === 'Enter') submit();}} placeholder="0000-0000-0000" inputMode="numeric" autoFocus className="mono" />
{trimmed && !valid &&
{exists ? `Code "${trimmed}" already exists` : 'Format: 0000-0000-0000'}
} {valid &&
preview {trimmed} · {previewColor.name}
}
); } function StatsScreen({ instances, codes, codeFolders, tzSync, onTzSyncChange }) { const stats = useMemoS(() => { const out = { total: instances.length, running: 0, idle: 0, booting: 0, stopped: 0, error: 0 }; instances.forEach((i) => out[i.status] = (out[i.status] || 0) + 1); return out; }, [instances]); const colorForCode = useColorForCode(); const segs = [ { k: 'running', l: 'Running', col: 'var(--ok)' }, { k: 'booting', l: 'Booting', col: 'var(--warn)' }, { k: 'idle', l: 'Idle', col: 'var(--ink-3)' }, { k: 'stopped', l: 'Stopped', col: 'var(--ink-5)' }, { k: 'error', l: 'Error', col: 'var(--bad)' }]; return (
{}
Total instances
{stats.total}
{segs.map((s) => { const v = stats[s.k] || 0; const pct = stats.total ? v / stats.total * 100 : 0; return
; })}
{segs.map((s) =>
{s.l} {stats[s.k] || 0}
)}
{/* By folder — clean list */}

By folder

{codeFolders.length} active
{codeFolders.length === 0 &&
No folders in use
} {codeFolders.map((f, i) => { const folderInstances = instances.filter((x) => x.code === f.code); const fStats = folderInstances.reduce((acc, x) => {acc[x.status] = (acc[x.status] || 0) + 1;return acc;}, {}); const col = colorForCode(f.code); const pct = stats.total ? folderInstances.length / stats.total * 100 : 0; return (
{f.code}
{fStats.running > 0 && {fStats.running} live} {fStats.error > 0 && {fStats.error} err} {!fStats.running && !fStats.error && inactive}
{folderInstances.length}
); })}
{/* Global controls */}

Global

Timezone sync
); } function Switch({ checked, onChange }) { return ( ); } /* ========================================================================= REFRESHER SCREEN — управление аккаунтами Epic: куки, рефреш, статус ========================================================================= */ function RefresherScreen() { const PAGE_SIZE = 100; const [accounts, setAccounts] = useStateS([]); const [total, setTotal] = useStateS(0); const [search, setSearch] = useStateS(""); const [loading, setLoading] = useStateS(true); const [hasMore, setHasMore] = useStateS(true); const [refreshing, setRefreshing] = useStateS(new Set()); const [detail, setDetail] = useStateS(null); // account_id для детальной панели const [workerOnline, setWorkerOnline] = useStateS(null); // null=checking, true=ok, false=err const scrollRef = useRefS(null); const loadingRef = useRefS(false); const load = (append = false) => { if (loadingRef.current) return; loadingRef.current = true; setLoading(true); const offset = append ? accounts.length : 0; fetch(`/api/accounts?limit=${PAGE_SIZE}&offset=${offset}`) .then((r) => { setWorkerOnline(r.ok); return r.json(); }) .then((d) => { const next = d.accounts || []; const nextTotal = d.total || 0; setAccounts((prev) => { if (!append) return next; const merged = new Map(prev.map((a) => [a.account_id, a])); next.forEach((a) => merged.set(a.account_id, a)); return Array.from(merged.values()); }); setTotal(nextTotal); const loaded = append ? accounts.length + next.length : next.length; setHasMore(next.length > 0 && loaded < nextTotal); }) .catch(() => { setWorkerOnline(false); }) .finally(() => { loadingRef.current = false; setLoading(false); }); }; useEffectS(() => { load(); }, []); useEffectS(() => { const el = scrollRef.current; if (!el || loading || !hasMore) return; if (el.scrollHeight <= el.clientHeight + 40) load(true); }, [accounts.length, loading, hasMore]); const handleAccountsScroll = (e) => { const el = e.currentTarget; if (loading || !hasMore) return; if (el.scrollHeight - el.scrollTop - el.clientHeight < 180) load(true); }; const filtered = useMemoS(() => { if (!search.trim()) return accounts; const s = search.toLowerCase(); return accounts.filter((a) => a.email.toLowerCase().includes(s) || (a.display_name || "").toLowerCase().includes(s) || a.account_id.toLowerCase().includes(s) ); }, [accounts, search]); const handleRefresh = async (account_id) => { setRefreshing((prev) => new Set([...prev, account_id])); try { const r = await fetch("/api/refresh", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ account_id }), }); const d = await r.json(); if (!d.ok) console.warn("refresh failed:", d.message); } catch (e) { console.error(e); } finally { setRefreshing((prev) => { const n = new Set(prev); n.delete(account_id); return n; }); load(); } }; const handleRefreshAll = async () => { await fetch("/api/refresh/all", { method: "POST" }); setTimeout(load, 3000); }; const [codeModal, setCodeModal] = useStateS(null); // {email, code, error} const handleMailCode = async (account_id, email) => { try { const r = await fetch(`/api/accounts/${account_id}/code`, { method: "POST" }); const d = await r.json(); if (d.code) { await navigator.clipboard.writeText(d.code).catch(() => {}); setCodeModal({ email, code: d.code, error: null }); } else { setCodeModal({ email, code: null, error: d.info || d.detail || 'unknown' }); } } catch (e) { setCodeModal({ email, code: null, error: e.message }); } }; const stats = useMemoS(() => { const ok = accounts.filter((a) => a.refresh_status === "ok").length; const err = accounts.filter((a) => a.refresh_status === "error").length; const pending = accounts.filter((a) => a.refresh_status === "pending" || !a.refresh_status).length; return { ok, err, pending }; }, [accounts]); return (
} /> {/* Статистика */}
{workerOnline === null ? '...' : workerOnline ? 'online' : 'offline'}
{loading && accounts.length === 0 ? (
Loading…
) : filtered.length === 0 ? (
{accounts.length === 0 ? "No accounts. Register accounts first." : "No accounts match the filter."}
) : ( {filtered.map((a) => ( handleRefresh(a.account_id)} onMailCode={() => handleMailCode(a.account_id, a.email)} onDetail={() => setDetail(detail === a.account_id ? null : a.account_id)} isDetailed={detail === a.account_id} /> ))}
Account Session expires Last refresh Cookies Status Device
)} {loading && accounts.length > 0 && (
Loading more...
)}
{detail && ( a.account_id === detail)} onClose={() => setDetail(null)} /> )} {codeModal && ( setCodeModal(null)} /> )}
); } function StatBadge({ label, value, color }) { return (
{value} {label}
); } function StatPill({ label, value, color, bg, border }) { return (
{value} {label}
); } function fmtTs(ts) { if (!ts) return "—"; const d = new Date(ts * 1000); const now = Date.now(); const diff = (d.getTime() - now) / 1000; // для expires — показываем "через N дней" если в будущем if (diff > 0) { const days = Math.floor(diff / 86400); if (days > 0) return `${days}d left`; const h = Math.floor(diff / 3600); return `${h}h left`; } // в прошлом const ago = -diff; if (ago < 60) return "just now"; if (ago < 3600) return `${Math.floor(ago / 60)}m ago`; if (ago < 86400) return `${Math.floor(ago / 3600)}h ago`; const days = Math.floor(ago / 86400); return `${days}d ago`; } function fmtDate(ts) { if (!ts) return "—"; const d = new Date(ts * 1000); return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`; } function AccountRefresherRow({ account: a, isRefreshing, onRefresh, onMailCode, onDetail, isDetailed }) { const refreshColor = { ok: 'var(--ok)', error: 'var(--bad)', pending: 'var(--ink-4)', }[a.refresh_status] || 'var(--ink-4)'; const deviceColor = { ok: 'var(--ok)', dead: 'var(--bad)', unknown: 'var(--ink-4)', }[a.device_auth_status] || 'var(--ink-4)'; const sessionExpires = a.session_ap_expires; const sessionColor = !sessionExpires ? 'var(--ink-5)' : sessionExpires * 1000 < Date.now() ? 'var(--bad)' : sessionExpires * 1000 < Date.now() + 30 * 86400 * 1000 ? 'var(--warn)' : 'var(--ok)'; return ( { if (!isDetailed) e.currentTarget.style.background = 'var(--panel-2)'; }} onMouseLeave={(e) => { if (!isDetailed) e.currentTarget.style.background = 'transparent'; }} onClick={onDetail} >
{a.email}
{a.display_name || a.account_id.slice(0, 16) + '…'}
{sessionExpires ? fmtTs(sessionExpires) : '—'} {sessionExpires && (
{fmtDate(sessionExpires)}
)} {a.last_refresh ? fmtTs(a.last_refresh) : 'never'} {a.cookies_count ?? 0} {a.refresh_status || 'pending'} {a.device_auth_status || 'unknown'}
); } function CodeModal({ data, onClose }) { const { email, code, error } = data; return (
e.stopPropagation()} style={{ width: 360, background: 'var(--card)', border: '1px solid var(--line)', borderRadius: 12, padding: 24, boxShadow: '0 12px 40px rgba(0,0,0,.12)', }} >
{email}
{code ? ( <>
{code}
Copied to clipboard
) : ( <>
{error || 'no code'}
)}
); } function AccountDetailPanel({ accountId, account: initialAccount, onClose }) { const [account, setAccount] = useStateS(initialAccount || null); const [logs, setLogs] = useStateS([]); const [exchangeCode, setExchangeCode] = useStateS(null); const [loadingExchange, setLoadingExchange] = useStateS(false); const [copied, setCopied] = useStateS(null); const [tab, setTab] = useStateS('info'); useEffectS(() => { setAccount(initialAccount || null); fetch(`/api/accounts/${accountId}/refresh-log?limit=10`) .then((r) => r.json()) .then((d) => setLogs(d.logs || [])); }, [accountId, initialAccount]); const copyToClipboard = (key, text) => { if (!text || text === "—") return; navigator.clipboard.writeText(text).then(() => { setCopied(key); setTimeout(() => setCopied(null), 1500); }); }; const getExchange = async () => { setLoadingExchange(true); setExchangeCode(null); try { const r = await fetch("/api/exchange", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ account_id: accountId }), }); const d = await r.json(); if (d.exchange_code) setExchangeCode(d.exchange_code); else setExchangeCode("ERROR: " + (d.detail || "unknown")); } catch (e) { setExchangeCode("ERROR: " + e.message); } finally { setLoadingExchange(false); } }; return (
{/* Header */}
Account detail
{account &&
{account.account_id.slice(0, 16)}…
}
{/* Tabs */} {account && (
{['info', 'cookies', 'log'].map((t) => { const on = tab === t; return ( ); })}
)}
{!account ? (
Loading…
) : tab === 'info' ? ( <> {/* Credentials */}
epic account
{/* Email account */}
email account
{/* Session */}
session
{/* Exchange */}
{exchangeCode && (
exchange code · 5 min {!exchangeCode.startsWith("ERROR") && ( )}
{exchangeCode}
)} ) : tab === 'cookies' ? ( <>
{account.cookies_count} cookies
{(account.cookies || []).map((c, i) => (
0 ? '1px solid var(--line-soft)' : 'none', cursor: 'pointer', }} onClick={() => copyToClipboard('ck_'+i, c.value)} style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '6px 10px', borderTop: i > 0 ? '1px solid var(--line-soft)' : 'none', cursor: 'pointer', background: copied === 'ck_'+i ? 'color-mix(in oklab, var(--ok) 8%, transparent)' : 'transparent' }}> {c.name} {c.value} {copied === 'ck_'+i ? : }
))}
) : ( /* Refresh log tab */ logs.length === 0 ? (
No refresh history
) : (
{logs.map((l, i) => (
0 ? '1px solid var(--line-soft)' : 'none' }}>
{l.status} {fmtTs(l.ts)}
{l.message &&
{l.message.slice(0, 100)}
} {l.proxy &&
via {l.proxy.split(':')[0]}
}
))}
) )}
); } function CopyField({ label, value, mono, small, secret, copyKey, copied, onCopy, badge, color, noCopy }) { const [show, setShow] = useStateS(!secret); const isCopied = copied === copyKey; const canCopy = !noCopy && value && value !== "—"; return (
{label}
{badge && } {value}
{secret && ( )} {canCopy && ( )}
); } /* ========================================================================= LOGIN SCREEN ========================================================================= */ function LoginScreen({ onLogin }) { const [username, setUsername] = useStateS(""); const [password, setPassword] = useStateS(""); const [error, setError] = useStateS(null); const [loading, setLoading] = useStateS(false); const [captchaReady, setCaptchaReady] = useStateS(false); const hcaptchaRef = useRefS(null); const [showHcaptcha, setShowHcaptcha] = useStateS(false); const [hcaptchaLoaded, setHcaptchaLoaded] = useStateS(false); const [turnstileToken, setTurnstileToken] = useStateS(null); // Когда showHcaptcha становится true — грузим скрипт если нужно useEffectS(() => { if (!showHcaptcha) return; if (window.hcaptcha) { setHcaptchaLoaded(true); return; } if (document.getElementById('hc-script')) return; const s = document.createElement('script'); s.id = 'hc-script'; s.src = 'https://js.hcaptcha.com/1/api.js?render=explicit'; s.async = true; s.onload = () => setHcaptchaLoaded(true); document.head.appendChild(s); }, [showHcaptcha]); // Когда скрипт загружен И div уже в DOM — рендерим виджет useEffectS(() => { if (!showHcaptcha || !hcaptchaLoaded || !hcaptchaRef.current) return; if (hcaptchaRef.current._hcMounted) return; window.hcaptcha.render(hcaptchaRef.current, { sitekey: '441af294-49f0-452c-9ea2-dc30178a20b6', theme: 'dark', }); hcaptchaRef.current._hcMounted = true; }, [showHcaptcha, hcaptchaLoaded]); // Turnstile invisible useEffectS(() => { if (document.getElementById('cf-ts-script')) return; const script = document.createElement('script'); script.id = 'cf-ts-script'; script.src = 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit'; script.async = true; script.onload = () => { window._cfWidget = window.turnstile.render('#cf-ts-box', { sitekey: '0x4AAAAAAC1HuZAb9oE1NMY6', size: 'invisible', callback: (t) => setTurnstileToken(t), 'error-callback': () => { setTurnstileToken(null); setShowHcaptcha(true); }, 'expired-callback': () => setTurnstileToken(null), }); }; document.head.appendChild(script); }, []); const submit = async (e) => { e.preventDefault(); setError(null); setLoading(true); let hcaptcha_token = null; let turnstile_token = turnstileToken; if (!turnstile_token && window.turnstile && window._cfWidget !== undefined) { try { window.turnstile.execute(window._cfWidget); } catch (_) {} await new Promise((resolve) => { let n = 0; const id = setInterval(() => { try { turnstile_token = window.turnstile.getResponse(window._cfWidget); } catch (_) {} if (turnstile_token || ++n > 25) { clearInterval(id); resolve(); } }, 200); }); } try { if (window.hcaptcha) hcaptcha_token = window.hcaptcha.getResponse(); } catch (_) {} try { const r = await fetch('/api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password, hcaptcha_token, turnstile_token }), }); const d = await r.json(); if (r.ok && d.ok) { onLogin(d.user); } else { const detail = d.detail || ''; if (detail === 'captcha_required' || detail === 'captcha_invalid') { // Показываем hCaptcha — Turnstile не сработал setShowHcaptcha(true); if (detail === 'captcha_invalid') setError('Captcha check failed, please solve it below'); } else { setError(detail || 'Invalid credentials'); } try { if (window.hcaptcha) window.hcaptcha.reset(); } catch (_) {} try { if (window._cfWidget !== undefined) { window.turnstile.reset(window._cfWidget); setTurnstileToken(null); } } catch (_) {} } } catch (err) { setError('Network error: ' + err.message); } finally { setLoading(false); } }; return (
{/* Logo + title */}
Epic Dashboard
Sign in to continue
{/* Form */}
setUsername(e.target.value)} placeholder="Username" autoFocus autoComplete="username" />
setPassword(e.target.value)} placeholder="Password" autoComplete="current-password" onKeyDown={(e) => e.key === 'Enter' && submit(e)} />
{/* Turnstile invisible */}
{/* hCaptcha — только если Turnstile не прошёл */} {showHcaptcha && (
)} {error && (
{error}
)}
); } Object.assign(window, { Sidebar, Topbar, InstancesScreen, MapCodesScreen, StatsScreen, RefresherScreen, LoginScreen });