// Global State const API_BASE = '/api'; let currentState = { user: null, subUrl: "", promoCode: null, device: 'unknown', statsInterval: null, lang: 'en' }; /* --- LOCALIZATION --- */ const translations = { en: { nav_home: "Home", nav_shop: "Shop", nav_config: "Config", nav_promo: "Promo", nav_profile: "Profile", nav_support: "Support", nav_admin: "Admin", dash_welcome: "Welcome back", dash_used: "Used Traffic", dash_expiry: "Until Expiry", dash_status: "Subscription Status", btn_extend: "Extend", btn_connect: "Connect", shop_title: "Select Plan", shop_subtitle: "Upgrade your experience", btn_purchase: "Purchase", sub_title: "Connect", sub_subtitle: "Setup your device", sub_link_label: "Subscription Link", btn_copy: "Copy Link", sub_instructions: "Detailed Instructions", sub_instr_1: "1. Download app for your device.", sub_instr_2: "2. Copy the link above.", sub_instr_3: "3. Import from Clipboard.", sub_instr_4: "4. Connect.", btn_download: "Download", prof_joined: "Joined", prof_spent: "Total Spent", prof_purchases: "Purchases", prof_app_info: "App Info", btn_support: "Support", btn_about: "About", adm_title: "Admin", adm_stats: "Stats", adm_users: "Users", adm_promos: "Promos", adm_broadcast: "Broadcast", adm_bot_stats: "Bot Stats", adm_total_users: "Total Users", adm_active_subs: "Active Subs", adm_revenue: "Revenue", adm_server: "Server", adm_marzban: "Marzban Users", btn_search: "Search", btn_back: "Back", adm_status: "Status", adm_expiry: "Expiry", adm_traffic: "Traffic", adm_btn_days: "+Days", adm_btn_exp: "Set Exp", adm_btn_limit: "Set GB", adm_btn_plan: "Plan", adm_btn_toggle: "Toggle", adm_btn_reset: "Reset", adm_btn_delete: "Delete Sub", adm_create_promo: "+ Create Promo", adm_send: "Send", adm_broadcast_msg: "Send message to all users", ph_message: "Message...", ph_search: "Search...", status_active: "Active", status_inactive: "Inactive", status_expired: "Expired", status_no_sub: "No active subscription", unit_gb: "GB", // New Keys err_plans_load: "Failed to load plans.", err_open_tg: "Open in Telegram", btn_creating: "Creating..", plan_unlimited: "Unlimited", unit_days: "Days", msg_paid: "Successful Payment!", err_network: "Network error", msg_link_copied: "Link copied!", err_no_sub: "No subscription found.", modal_dl_title: "Download App", modal_dl_subtitle: "Choose a client for your device:", btn_dl_for: "Download for", msg_promo_applied: "Promo Applied! {0}% OFF.", err_promo_invalid: "Invalid Promo Code", err_promo_check: "Error checking promo", about_slogan: "Premium V2Ray service.
Fast, Secure, Reliable.", msg_support_sent: "Message sent! Admin will contact you.", err_send_fail: "Failed to send.", adm_no_users: "No users found", confirm_toggle: "Change user active/disabled state?", confirm_reset: "Reset used traffic to zero?", confirm_delete_sub: "Are you sure? User will lose VPN access immediately.", title_add_days: "Add Days", subtitle_add_days: "Enter days to ADD (negative to subtract):", btn_apply: "Apply", title_set_limit: "Set Traffic Limit", subtitle_set_limit: "Enter new limit in GB (0 for ∞):", title_set_exp: "Set Expiration", subtitle_set_exp: "Days from NOW (0 = expire, 36500 = ∞):", confirm_broadcast: "Send this message to ALL users?", msg_broadcast_sent: "Broadcast sent to {0} users!", title_new_promo: "New Promo", ph_code: "CODE (e.g. SUMMER2024)", ph_discount: "Discount %", ph_uses: "Uses Limit", ph_days: "Validity Days", ph_bonus: "Bonus Days", lbl_unlim_usage: "Unlimited Usage", lbl_sticky: "Permanent (Sticky)", note_sticky: "* Sticky promo locks the discount for the user forever.", btn_create_promo: "Create Promo", msg_promo_created: "Promo Created!", err_promo_create: "Error creating promo", msg_promo_deleted: "Promo deleted!", confirm_delete_promo: "Are you sure you want to delete promo code {0}?", app_desc_sleek: "Sleek & Fast", app_desc_premium: "Premium Choice", app_desc_standard: "Standard Client", app_desc_modern: "Powerful & Modern", app_desc_aio: "Universal All-in-One", app_desc_opensource: "Powerful & Open Source", app_desc_simple: "Simple & Modern", app_desc_classic: "Classic Client", app_desc_native: "Native M1/M2 Support", app_desc_elegant: "Elegant Desktop Client" }, ru: { nav_home: "Главная", nav_shop: "Магазин", nav_config: "Подкл.", nav_promo: "Промо", nav_profile: "Профиль", nav_support: "Поддержка", nav_admin: "Админка", dash_welcome: "С возвращением", dash_used: "Использовано", dash_expiry: "До истечения", dash_status: "Статус подписки", btn_extend: "Продлить", btn_connect: "Подключить", shop_title: "Выберите план", shop_subtitle: "Улучшите свой опыт", btn_purchase: "Купить", sub_title: "Подключение", sub_subtitle: "Настройка устройства", sub_link_label: "Ссылка подписки", btn_copy: "Скопировать", sub_instructions: "Инструкция", sub_instr_1: "1. Скачайте приложение.", sub_instr_2: "2. Скопируйте ссылку выше.", sub_instr_3: "3. Импортируйте из буфера.", sub_instr_4: "4. Подключитесь.", btn_download: "Скачать", prof_joined: "Регистрация", prof_spent: "Потрачено", prof_purchases: "Покупок", prof_app_info: "О приложении", btn_support: "Поддержка", btn_about: "О нас", adm_title: "Админка", adm_stats: "Статистика", adm_users: "Юзеры", adm_promos: "Промо", adm_broadcast: "Рассылка", adm_bot_stats: "Статистика бота", adm_total_users: "Всего юзеров", adm_active_subs: "Активных", adm_revenue: "Выручка", adm_server: "Сервер", adm_marzban: "В панели", btn_search: "Поиск", btn_back: "Назад", adm_status: "Статус", adm_expiry: "Истекает", adm_traffic: "Трафик", adm_btn_days: "+Дни", adm_btn_exp: "Срок", adm_btn_limit: "Лимит", adm_btn_plan: "План", adm_btn_toggle: "Вкл/Выкл", adm_btn_reset: "Сброс", adm_btn_delete: "Удалить", adm_create_promo: "+ Промокод", adm_send: "Отправить", adm_broadcast_msg: "Сообщение всем юзерам", ph_message: "Сообщение...", ph_search: "Поиск...", status_active: "Активна", status_inactive: "Неактивна", status_expired: "Истекла", status_no_sub: "Нет активной подписки", unit_gb: "ГБ", // New Keys RU err_plans_load: "Не удалось загрузить планы.", err_open_tg: "Откройте в Telegram", btn_creating: "Создание..", plan_unlimited: "Безлимит", unit_days: "Дней", msg_paid: "Успешная оплата!", err_network: "Ошибка сети", msg_link_copied: "Ссылка скопирована!", err_no_sub: "Подписка не найдена.", modal_dl_title: "Скачать приложение", modal_dl_subtitle: "Выберите клиент для устройства:", btn_dl_for: "Скачать для", msg_promo_applied: "Промо применено! Скидка {0}%.", err_promo_invalid: "Неверный промокод", err_promo_check: "Ошибка проверки промо", about_slogan: "Премиум V2Ray сервис.
Быстро, Надежно, Безопасно.", msg_support_sent: "Отправлено! Админ свяжется с вами.", err_send_fail: "Ошибка отправки.", adm_no_users: "Пользователи не найдены", confirm_toggle: "Изменить статус (вкл/выкл)?", confirm_reset: "Сбросить трафик в ноль?", confirm_delete_sub: "Удалить подписку? Пользователь потеряет доступ.", title_add_days: "Добавить дни", subtitle_add_days: "Дней для добавления (отрицат. = убавить):", btn_apply: "Применить", title_set_limit: "Установить лимит", subtitle_set_limit: "Лимит в ГБ (0 = безлимит):", title_set_exp: "Срок действия", subtitle_set_exp: "Дней от СЕЙЧАС (0 = истекло, 36500 = ∞):", confirm_broadcast: "Отправить сообщение ВСЕМ пользователям?", msg_broadcast_sent: "Рассылка отправлена {0} пользователям!", title_new_promo: "Новый Промокод", ph_code: "КОД (напр. SUMMER2024)", ph_discount: "Скидка %", ph_uses: "Лимит испол.", ph_days: "Дней действия", ph_bonus: "Бонусные дни", lbl_unlim_usage: "Безлимит использований", lbl_sticky: "Перманентный (Sticky)", note_sticky: "* Sticky закрепляет скидку за пользователем навсегда.", btn_create_promo: "Создать", msg_promo_created: "Промокод создан!", err_promo_create: "Ошибка создания", msg_promo_deleted: "Промокод удален!", confirm_delete_promo: "Удалить промокод {0}?", app_desc_sleek: "Быстрый и удобный", app_desc_premium: "Премиум выбор", app_desc_standard: "Стандартный клиент", app_desc_modern: "Мощный и современный", app_desc_aio: "Универсальный комбайн", app_desc_opensource: "Мощный Open Source", app_desc_simple: "Простой и современный", app_desc_classic: "Классика", app_desc_native: "Нативная поддержка M1/M2", app_desc_elegant: "Элегантный клиент" } }; function t(key) { const lang = currentState.lang || 'en'; return translations[lang][key] || translations['en'][key] || key; } function applyTranslations(container = document) { container.querySelectorAll('[data-t]').forEach(el => { el.textContent = t(el.dataset.t); }); container.querySelectorAll('[data-tp]').forEach(el => { el.placeholder = t(el.dataset.tp); }); } // Telegram Init const tg = window.Telegram?.WebApp; function showAccessDenied() { document.body.innerHTML = ''; document.body.style.display = 'block'; const div = document.createElement('div'); div.style.cssText = 'display:flex; flex-direction:column; align-items:center; justify-content:center; height:100vh; padding:24px; text-align:center; background:var(--md-sys-color-background); color:var(--md-sys-color-on-background); font-family:var(--font-brand);'; div.innerHTML = `

Access Restricted

This application is designed to be used exclusively via Telegram.

Open Telegram `; document.body.appendChild(div); } function showInviteRequired() { document.body.innerHTML = ''; document.body.style.display = 'block'; const div = document.createElement('div'); div.style.cssText = 'display:flex; flex-direction:column; align-items:center; justify-content:center; height:100vh; padding:24px; text-align:center; background:var(--md-sys-color-background); color:var(--md-sys-color-on-background); font-family:var(--font-brand);'; div.innerHTML = `

Registration Required

You need to activate the bot with an invite code to use this app.

`; document.body.appendChild(div); } function initApp() { // Enhanced OS Detection const ua = navigator.userAgent.toLowerCase(); if (/iphone|ipad|ipod/.test(ua)) currentState.device = 'ios'; else if (/android/.test(ua)) currentState.device = 'android'; else if (/macintosh|mac os x/.test(ua)) currentState.device = 'macos'; else if (/windows/.test(ua)) currentState.device = 'windows'; else if (/linux/.test(ua)) currentState.device = 'linux'; else currentState.device = 'desktop'; if (tg) { tg.ready(); tg.expand(); try { tg.setHeaderColor('#0f172a'); } catch (e) { } currentState.user = tg.initDataUnsafe?.user; const userLang = (tg.initDataUnsafe?.user?.language_code || 'en').toLowerCase(); if (userLang === 'ru' || userLang.startsWith('ru')) { currentState.lang = 'ru'; } // Refine from TG platform const p = tg.platform; if (['ios'].includes(p)) currentState.device = 'ios'; else if (['macos'].includes(p)) currentState.device = 'macos'; else if (['android', 'android_x'].includes(p)) currentState.device = 'android'; // Theming applyTheme(); tg.onEvent('themeChanged', applyTheme); } // Check Auth if (!currentState.user) { showAccessDenied(); return; } // Avatar Setup updateAvatar(currentState.user); // Initial Route router('dashboard'); applyTranslations(); // Global Key Listeners document.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeModal(); }); } function applyTheme() { const body = document.body; const root = document.documentElement; // Determine color scheme from Telegram or system preference let colorScheme = 'dark'; // default if (tg) { colorScheme = tg.colorScheme || 'dark'; } else if (window.matchMedia) { colorScheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } // Apply theme class to body body.classList.remove('dark', 'light'); body.classList.add(colorScheme); // Additional TG theme customization if (tg?.themeParams) { const theme = tg.themeParams; // Override primary color if provided by Telegram if (theme.button_color) { root.style.setProperty('--md-sys-color-primary', theme.button_color); if (theme.button_text_color) { root.style.setProperty('--md-sys-color-on-primary', theme.button_text_color); } } // Set header color for native feel try { tg.setHeaderColor(colorScheme === 'dark' ? '#111318' : '#f9f9ff'); } catch (e) { } } console.log(`Theme applied: ${colorScheme}`); } function updateAvatar(user) { const headerAvatar = document.getElementById('header-avatar'); if (!headerAvatar) return; if (user.photo_url) { headerAvatar.innerHTML = `Avatar`; } else { headerAvatar.textContent = (user.first_name || 'U')[0].toUpperCase(); } } // Navigation async function router(pageName) { const viewContainer = document.getElementById('app-view'); const template = document.getElementById(`view-${pageName}`); // Don't route if already on page (optional optimization, but good for UX) // const current = document.querySelector('.nav-item.active'); // if (current && current.dataset.page === pageName) return; // Clear existing intervals if (currentState.statsInterval) { clearInterval(currentState.statsInterval); currentState.statsInterval = null; } // Update Nav State document.querySelectorAll('.nav-item').forEach(item => { if (item.dataset.page === pageName) item.classList.add('active'); else item.classList.remove('active'); }); // Animate Exit if content exists if (viewContainer.innerHTML.trim() !== '') { viewContainer.classList.add('page-exit'); await new Promise(r => setTimeout(r, 200)); viewContainer.classList.remove('page-exit'); } // Update Header Title const headerTitle = document.getElementById('header-title'); if (headerTitle) { const titles = { 'dashboard': 'Dashboard', 'shop': 'Shop', 'subscription': 'Config', 'profile': 'Profile', 'support': 'Support', 'admin': 'Admin Control' }; headerTitle.textContent = titles[pageName] || 'Comet'; } // Swap View if (template) { viewContainer.innerHTML = ''; viewContainer.appendChild(template.content.cloneNode(true)); applyTranslations(viewContainer); // Retrigger enter animation viewContainer.style.animation = 'none'; viewContainer.offsetHeight; /* trigger reflow */ viewContainer.style.animation = null; } // Init Page Logic if (pageName === 'dashboard') loadDashboard(); if (pageName === 'shop') loadShop(); if (pageName === 'subscription') loadSubscription(); if (pageName === 'profile') loadProfile(); if (pageName === 'admin') loadAdmin(); // Lucide Icons try { if (window.lucide) lucide.createIcons(); } catch (e) { console.error("Lucide Error:", e); } // Smooth Scroll Top window.scrollTo({ top: 0, behavior: 'smooth' }); } // ------ PAGE LOGIC ------ async function loadDashboard() { document.getElementById('user-name').textContent = currentState.user.first_name; try { const username = currentState.user.username || ''; const lang = currentState.lang || 'en'; const res = await fetch(`${API_BASE}/user/${currentState.user.id}?username=${encodeURIComponent(username)}&lang=${lang}`); if (res.status === 404) { showInviteRequired(); return; } const data = await res.json(); if (data.error) throw new Error(data.error); // Update Text const statusEl = document.getElementById('dash-status'); if (statusEl) { // Translate status from server or map common values const statusMap = { 'Active': t('status_active'), 'Inactive': t('status_inactive'), 'Expired': t('status_expired') }; statusEl.textContent = statusMap[data.status] || data.status; } const limitEl = document.getElementById('dash-limit'); if (limitEl) limitEl.textContent = `${data.data_limit_gb} ${t('unit_gb')}`; const expireEl = document.getElementById('dash-expire'); if (expireEl) { // Handle "No active subscription" specifically or date if (data.expire_date === "No active subscription") { expireEl.textContent = t('status_no_sub'); } else { expireEl.textContent = data.expire_date; } } const leftEl = document.getElementById('dash-data-left'); if (leftEl) leftEl.textContent = data.used_traffic_gb; const daysLeftEl = document.getElementById('dash-days-left'); if (daysLeftEl) { daysLeftEl.textContent = data.days_left > 10000 ? "∞" : data.days_left; } // Progress Rings // 1. Traffic Ring const circle = document.getElementById('data-ring'); if (circle) { const limit = data.data_limit_gb || 0; const used = data.used_traffic_gb || 0; // Handle infinity (large limit) let percent = 0; if (limit > 900000) { percent = 0; // Or some "full" state? 0 is safe for used/unlim } else if (limit > 0) { percent = Math.min((used / limit) * 100, 100); } circle.setAttribute('stroke-dasharray', `${percent}, 100`); circle.style.stroke = percent > 90 ? '#f87171' : 'var(--md-sys-color-primary)'; } // 2. Expiry Ring const expCircle = document.getElementById('exp-ring'); if (expCircle) { const daysLeft = data.days_left || 0; let expPercent = 0; if (daysLeft > 10000) { expPercent = 0; // Infinity looks like a full/empty ring depending on logic. Let's say 0 used. } else { // Assuming max display represents 30 days for visual context or 100% is safe // Let's use 30 as a standard window for the ring animation if we don't know the plan const standardWindow = 30; expPercent = Math.max(0, 100 - (Math.min(daysLeft, standardWindow) / standardWindow * 100)); } expCircle.setAttribute('stroke-dasharray', `${expPercent}, 100`); expCircle.style.stroke = expPercent > 80 ? '#f87171' : 'var(--md-sys-color-primary)'; } currentState.subUrl = data.subscription_url; currentState.user_full = data; // Admin visibility const adminNav = document.getElementById('nav-admin'); const mobileProfileBtn = document.getElementById('mobile-profile-btn'); const mobileAdminBtn = document.getElementById('mobile-admin-btn'); if (data.is_admin) { // Desktop rail if (adminNav) adminNav.classList.remove('hidden'); // Mobile: hide Profile, show Admin if (mobileProfileBtn) mobileProfileBtn.classList.add('hidden'); if (mobileAdminBtn) mobileAdminBtn.classList.remove('hidden'); } else { // Desktop rail if (adminNav) adminNav.classList.add('hidden'); // Mobile: show Profile, hide Admin if (mobileProfileBtn) mobileProfileBtn.classList.remove('hidden'); if (mobileAdminBtn) mobileAdminBtn.classList.add('hidden'); } // Update user photo if not present or server provides it if (data.photo_url) { currentState.user.photo_url = data.photo_url; updateAvatar(currentState.user); } } catch (e) { console.error(e); const statusEl = document.getElementById('dash-status'); if (statusEl) statusEl.textContent = 'Error'; } } async function loadShop() { const container = document.getElementById('plans-container'); container.innerHTML = '
'; try { const res = await fetch(`${API_BASE}/plans`); const plans = await res.json(); container.innerHTML = ''; plans.forEach(plan => { const card = document.createElement('div'); card.className = 'glass plan-card plan-item'; card.innerHTML = `
${plan.name} ${plan.price} ⭐️
${plan.data_limit > 0 ? plan.data_limit + ' ' + t('unit_gb') : t('plan_unlimited')} ${plan.days} ${t('unit_days')}
`; container.appendChild(card); }); try { if (window.lucide) lucide.createIcons(); } catch (e) { } } catch (e) { container.textContent = t('err_plans_load'); } } async function initPayment(planId) { if (!tg) return alert(t('err_open_tg')); const btn = event.target; const oldText = btn.innerText; btn.innerText = t('btn_creating'); btn.disabled = true; try { const body = { user_id: currentState.user.id, plan_id: planId }; if (currentState.promoCode) { body.promo_code = currentState.promoCode; } const res = await fetch(`${API_BASE}/create-invoice`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); const data = await res.json(); if (data.invoice_link) { tg.openInvoice(data.invoice_link, (status) => { if (status === 'paid') { showToast(t('msg_paid'), 'success'); router('dashboard'); } }); } else { showToast('Error: ' + (data.error || 'Unknown'), 'error'); } } catch (e) { showToast(t('err_network'), 'error'); } finally { btn.innerText = oldText; btn.disabled = false; } } async function loadSubscription() { // Check missing sub url if (!currentState.subUrl) { try { const res = await fetch(`${API_BASE}/user/${currentState.user.id}`); const data = await res.json(); currentState.subUrl = data.subscription_url; } catch (e) { } } const url = currentState.subUrl; const linkEl = document.getElementById('config-link'); if (url) { if (linkEl) linkEl.textContent = url; const qrContainer = document.getElementById('qrcode-container'); if (qrContainer) { qrContainer.innerHTML = ''; new QRCode(qrContainer, { text: url, width: 160, height: 160, colorDark: "#000000", colorLight: "#ffffff", correctLevel: QRCode.CorrectLevel.M }); } } else { if (linkEl) linkEl.textContent = t('status_no_sub'); } // Dynamic Apps based on Device const device = currentState.device; const appsContainer = document.getElementById('device-apps-container'); if (appsContainer) { const osNames = { 'ios': 'iOS', 'android': 'Android', 'macos': 'macOS', 'windows': 'Windows', 'linux': 'Linux' }; const myOS = osNames[device] || 'Desktop'; appsContainer.innerHTML = `
`; lucide.createIcons(); } } function openDownloadModal() { const device = currentState.device; let apps = []; if (device === 'ios') { apps = [ { name: 'V2Box', desc: t('app_desc_sleek'), link: 'https://apps.apple.com/us/app/v2box-v2ray-client/id6446814690' }, { name: 'Streisand', desc: t('app_desc_premium'), link: 'https://apps.apple.com/us/app/streisand/id6450534064' } ]; } else if (device === 'android') { apps = [ { name: 'v2rayNG', desc: t('app_desc_standard'), link: 'https://play.google.com/store/apps/details?id=com.v2ray.ang' }, { name: 'NekoBox', desc: t('app_desc_modern'), link: 'https://github.com/MatsuriDayo/NekoBoxForAndroid/releases' }, { name: 'Hiddify', desc: t('app_desc_aio'), link: 'https://play.google.com/store/apps/details?id=app.hiddify.com' } ]; } else if (device === 'windows') { apps = [ { name: 'NekoRay', desc: t('app_desc_opensource'), link: 'https://github.com/MatsuriDayo/nekoray/releases' }, { name: 'Hiddify Next', desc: t('app_desc_simple'), link: 'https://github.com/hiddify/hiddify-next/releases' }, { name: 'v2rayN', desc: t('app_desc_classic'), link: 'https://github.com/2many986/v2rayN/releases' } ]; } else if (device === 'macos') { apps = [ { name: 'V2Box', desc: t('app_desc_native'), link: 'https://apps.apple.com/us/app/v2box-v2ray-client/id6446814690' }, { name: 'Hiddify Next', desc: t('app_desc_elegant'), link: 'https://github.com/hiddify/hiddify-next/releases' } ]; } else { // Linux or fallback apps = [ { name: 'NekoRay', desc: 'Best for Linux', link: 'https://github.com/MatsuriDayo/nekoray/releases' }, { name: 'Hiddify Next', desc: 'AppImage Available', link: 'https://github.com/hiddify/hiddify-next/releases' } ]; } let html = `

${t('modal_dl_subtitle')}

`; apps.forEach(app => { html += `
${app.name}
${app.desc}
`; }); html += `
`; openModal(t('modal_dl_title'), html); lucide.createIcons(); } function copyConfig() { if (currentState.subUrl) { navigator.clipboard.writeText(currentState.subUrl); showToast(t('msg_link_copied'), "success"); } else { showToast(t('err_no_sub'), "error"); } } function toggleAcc(header) { const accItem = header.parentElement; accItem.classList.toggle('open'); } async function loadProfile() { document.getElementById('profile-name').textContent = currentState.user.first_name; document.getElementById('profile-id').textContent = `ID: ${currentState.user.id}`; // Profile Avatar const avatar = document.getElementById('profile-avatar'); if (currentState.user.photo_url) { avatar.innerHTML = ``; } else { avatar.textContent = (currentState.user.first_name || 'U')[0].toUpperCase(); } // Load Stats if available if (currentState.user_full) { const data = currentState.user_full; const regEl = document.getElementById('stat-reg-date'); if (regEl) regEl.textContent = data.reg_date || '...'; const spentEl = document.getElementById('stat-spent'); if (spentEl) spentEl.textContent = data.total_spent || '0'; const payEl = document.getElementById('stat-payments'); if (payEl) payEl.textContent = data.total_payments || '0'; } } async function checkPromo() { const input = document.getElementById('promo-input'); const code = input.value.trim(); if (!code) return; try { const res = await fetch(`${API_BASE}/check-promo`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ code }) }); if (res.ok) { const data = await res.json(); currentState.promoCode = data.code; showToast(t('msg_promo_applied').replace('{0}', data.discount), "success"); closeModal(); // Optional: close modal on success } else { showToast(t('err_promo_invalid'), "error"); } } catch (e) { showToast(t('err_promo_check'), "error"); } } // ------ MODALS & SUPPORT ------ function openModal(title, contentHtml) { const overlay = document.getElementById('modal-overlay'); const dialog = overlay.querySelector('.modal-dialog'); const mTitle = document.getElementById('modal-title'); const mBody = document.getElementById('modal-body'); mTitle.textContent = title; mBody.innerHTML = contentHtml; overlay.classList.remove('hidden', 'closing'); dialog.classList.remove('closing'); // Trigger reflow for animation restart overlay.offsetHeight; // Lucide in modal try { if (window.lucide) lucide.createIcons(); } catch (e) { } } function closeModal(e) { if (e && e.target !== e.currentTarget && !e.target.classList.contains('close-btn')) return; const overlay = document.getElementById('modal-overlay'); const dialog = overlay.querySelector('.modal-dialog'); // Add closing class for exit animation overlay.classList.add('closing'); dialog.classList.add('closing'); // Wait for animation to complete before hiding setTimeout(() => { overlay.classList.add('hidden'); overlay.classList.remove('closing'); dialog.classList.remove('closing'); }, 250); } function openSupport() { const html = `

${t('msg_support_sent')}

`; openModal(t('nav_support'), html); } function openAbout() { openModal(t('btn_about'), `

Stellarisei VPN v1.2

${t('about_slogan')}

© 2026 Stellarisei

`); } async function sendSupport() { const msg = document.getElementById('support-msg').value; if (!msg.trim()) return; // Show loading const btn = event.target; const oldText = btn.innerText; btn.innerText = "Sending..."; btn.disabled = true; try { const res = await fetch(`${API_BASE}/support`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ user_id: currentState.user.id, username: currentState.user.username || "Unknown", message: msg }) }); if (res.ok) { closeModal(); showToast(t('msg_support_sent'), "success"); } else { showToast(t('err_send_fail'), "error"); } } catch (e) { showToast(t('err_network'), "error"); } finally { if (btn) { btn.innerText = oldText; btn.disabled = false; } } } // --- ADMIN LOGIC --- async function adminInit() { // Hidden tab check already in loadDashboard } async function loadAdmin() { // Micro-delay to ensure DOM is ready after router swap setTimeout(() => { const adminContent = document.getElementById('admin-content'); if (!adminContent) return; adminTab('stats'); }, 0); } async function adminTab(tabName) { // UI update document.querySelectorAll('.tab-item').forEach(btn => { btn.classList.toggle('active', btn.textContent.trim().toLowerCase() === tabName.toLowerCase()); }); const container = document.getElementById('admin-content'); const template = document.getElementById(`admin-${tabName}`); if (!template) return; // Exit animation container.classList.add('tab-exit'); // Wait for exit animation await new Promise(resolve => setTimeout(resolve, 150)); container.innerHTML = template.innerHTML; // Entry animation container.classList.remove('tab-exit'); container.classList.add('tab-enter'); // Remove entry class after animation setTimeout(() => container.classList.remove('tab-enter'), 350); lucide.createIcons(); // Clear any previous interval when switching tabs if (currentState.statsInterval) { clearInterval(currentState.statsInterval); currentState.statsInterval = null; } if (tabName === 'stats') { refreshAdminStats(); currentState.statsInterval = setInterval(refreshAdminStats, 1000); } else if (tabName === 'users') { adminSearchUsers(); } else if (tabName === 'promos') { loadAdminPromos(); } } async function refreshAdminStats() { try { const res = await fetch(`${API_BASE}/admin/stats?user_id=${currentState.user.id}`); const data = await res.json(); if (data.error) return; const elTotal = document.getElementById('adm-total-users'); const elActive = document.getElementById('adm-active-subs'); const elRevenue = document.getElementById('adm-revenue'); const elCpu = document.getElementById('adm-cpu'); const elRam = document.getElementById('adm-ram'); const elMarz = document.getElementById('adm-active-marz'); if (elTotal) elTotal.textContent = data.bot.total; if (elActive) elActive.textContent = data.bot.active; if (elRevenue) elRevenue.textContent = `${data.bot.revenue} ⭐️`; if (elCpu) elCpu.textContent = `${data.server.cpu}%`; if (elRam) elRam.textContent = `${data.server.ram_used} / ${data.server.ram_total} GB`; if (elMarz) elMarz.textContent = data.server.active_users; } catch (e) { console.error("Stats fetch error:", e); } } async function adminSearchUsers() { const searchInput = document.getElementById('admin-user-search'); if (searchInput && !searchInput.dataset.listener) { searchInput.addEventListener('keyup', (e) => { if (e.key === 'Enter') adminSearchUsers(); }); searchInput.dataset.listener = 'true'; } const query = searchInput ? searchInput.value : ''; const res = await fetch(`${API_BASE}/admin/users?user_id=${currentState.user.id}&query=${query}`); const users = await res.json(); const list = document.getElementById('admin-users-list'); list.innerHTML = ''; if (users.length === 0) { list.innerHTML = `

${t('adm_no_users')}

`; lucide.createIcons(); return; } users.forEach(u => { const div = document.createElement('div'); div.className = 'list-item glass'; div.style.marginBottom = '8px'; div.style.padding = '8px 16px'; div.innerHTML = `
${u.username || 'User'}
${u.user_id}
`; div.onclick = () => showAdminUserDetail(u.user_id); list.appendChild(div); }); lucide.createIcons(); } async function showAdminUserDetail(targetId) { const res = await fetch(`${API_BASE}/admin/user/${targetId}?user_id=${currentState.user.id}`); const data = await res.json(); currentState.admin_target = targetId; const container = document.getElementById('admin-content'); container.innerHTML = document.getElementById('admin-user-detail').innerHTML; document.getElementById('adm-user-name').textContent = data.user.username || 'User'; document.getElementById('adm-user-id').textContent = `ID: ${data.user.user_id}`; // Detail Avatar const detailAvatar = document.getElementById('adm-user-avatar'); if (detailAvatar) { const photoUrl = `/api/user-photo/${data.user.user_id}`; detailAvatar.innerHTML = ``; } const m = data.marzban || {}; document.getElementById('adm-user-status').textContent = m.status || t('status_inactive'); const subUntil = data.user.subscription_until ? new Date(data.user.subscription_until) : null; let expStr = 'None'; if (subUntil) { expStr = subUntil.getFullYear() >= 2099 ? '∞' : subUntil.toLocaleDateString(); } document.getElementById('adm-user-expire').textContent = expStr; const used = (m.used_traffic / (1024 ** 3)).toFixed(2); const limitGB = m.data_limit / (1024 ** 3); const limitStr = limitGB > 900000 ? '∞' : limitGB.toFixed(2); document.getElementById('adm-user-traffic').textContent = `${used} / ${limitStr} GB`; lucide.createIcons(); } async function adminUserAction(action) { if (action === 'toggle_status' || action === 'reset_traffic' || action === 'delete_sub') { const config = { 'toggle_status': { title: t('adm_btn_toggle'), msg: t('confirm_toggle'), btnClass: 'btn-primary' }, 'reset_traffic': { title: t('adm_btn_reset'), msg: t('confirm_reset'), btnClass: 'btn-primary' }, 'delete_sub': { title: t('adm_btn_delete'), msg: t('confirm_delete_sub'), btnClass: 'btn-error' } }; const cfg = config[action]; openModal(cfg.title, `

${cfg.msg}

`); return; } let title = ""; let html = ""; if (action === 'add_days') { title = t('title_add_days'); html = ` `; } else if (action === 'set_limit') { title = t('title_set_limit'); html = ` `; } else if (action === 'set_expiry') { title = t('title_set_exp'); html = ` `; } else if (action === 'set_plan') { title = t('shop_title'); const res = await fetch(`${API_BASE}/admin/plans_full?user_id=${currentState.user.id}`); const allPlans = await res.json(); html = `
`; allPlans.forEach(p => { html += `
${p.name}
${p.days}d / ${p.data_limit || '∞'}GB
`; }); html += `
`; } openModal(title, html); lucide.createIcons(); } async function submitAdminAction(action) { const input = document.getElementById('adm-action-val'); if (!input) return; const value = input.value; let payload = { user_id: currentState.user.id, action: action }; if (action === 'add_days' || action === 'set_expiry') payload.days = parseInt(value); if (action === 'set_limit') payload.limit_gb = parseFloat(value); const res = await fetch(`${API_BASE}/admin/user/${currentState.admin_target}/action`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); if (res.ok) { closeModal(); showToast("Success!"); showAdminUserDetail(currentState.admin_target); } else { showToast("Failed", "error"); } } async function submitAdminPlan(planId) { const res = await fetch(`${API_BASE}/admin/user/${currentState.admin_target}/action`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ user_id: currentState.user.id, action: 'set_plan', plan_id: planId }) }); if (res.ok) { closeModal(); showToast("Plan Applied!"); showAdminUserDetail(currentState.admin_target); } else { showToast("Failed", "error"); } } async function submitConfirmAction(action) { const res = await fetch(`${API_BASE}/admin/user/${currentState.admin_target}/action`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ user_id: currentState.user.id, action: action }) }); if (res.ok) { closeModal(); showToast("Success!"); showAdminUserDetail(currentState.admin_target); } else { showToast("Action failed", "error"); } } async function loadAdminPromos() { const res = await fetch(`${API_BASE}/admin/promos?user_id=${currentState.user.id}`); const promos = await res.json(); const list = document.getElementById('admin-promos-list'); list.innerHTML = ''; promos.forEach(p => { const div = document.createElement('div'); div.className = 'list-item glass'; div.style.marginBottom = '8px'; const expDate = p.expires_at ? new Date(p.expires_at).toLocaleDateString() : 'Never'; div.innerHTML = `
CODE: ${p.code}
${p.discount}% | Uses: ${p.uses_left} | Exp: ${expDate}
`; list.appendChild(div); }); lucide.createIcons(); } async function deleteAdminPromo(code) { openModal("Delete Promo", `

${t('confirm_delete_promo').replace('{0}', code)}

`); } async function submitDeletePromo(code) { const res = await fetch(`${API_BASE}/admin/promo/${code}?user_id=${currentState.user.id}`, { method: 'DELETE' }); if (res.ok) { closeModal(); showToast("Promo deleted!"); loadAdminPromos(); } else { showToast("Failed to delete", "error"); } } function openCreatePromoModal() { openModal(t('title_new_promo'), `

${t('note_sticky')}

`); } async function createPromo() { const payload = { user_id: currentState.user.id, code: document.getElementById('new-promo-code').value.toUpperCase(), discount: parseInt(document.getElementById('new-promo-discount').value || 0), uses: parseInt(document.getElementById('new-promo-uses').value || 1), days: parseInt(document.getElementById('new-promo-days').value || 0), bonus_days: parseInt(document.getElementById('new-promo-bonus').value || 0), is_unlimited: document.getElementById('new-promo-unlim').checked, is_sticky: document.getElementById('new-promo-sticky').checked }; const res = await fetch(`${API_BASE}/admin/promos/create`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); if (res.ok) { closeModal(); showToast("Promo Created!"); adminTab('promos'); } else { showToast("Error creating promo", "error"); } } async function sendBroadcast() { const msg = document.getElementById('broadcast-msg').value; if (!msg) return; if (!confirm(t('confirm_broadcast'))) return; const res = await fetch(`${API_BASE}/admin/broadcast`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ user_id: currentState.user.id, message: msg }) }); const data = await res.json(); showToast(t('msg_broadcast_sent').replace('{0}', data.sent)); } function openPromoModal() { openModal("Promo Code", `

Enter your promo code to get discounts or bonus days.

`); } function showToast(message, type = 'info') { const container = document.getElementById('toast-container'); if (!container) return; const toast = document.createElement('div'); toast.className = `toast ${type}`; const icons = { 'info': 'info', 'success': 'check-circle', 'error': 'alert-circle' }; toast.innerHTML = ` ${message} `; container.appendChild(toast); if (window.lucide) lucide.createIcons(); // Remove after delay setTimeout(() => { toast.classList.add('toast-exit'); // Wait for animation setTimeout(() => { toast.remove(); }, 400); }, 3000); } // App Start document.addEventListener('DOMContentLoaded', initApp);