// 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..."
},
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: "Поиск..."
}
};
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;
if (tg.initDataUnsafe?.user?.language_code === '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 = `
`;
} 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) statusEl.textContent = data.status;
const limitEl = document.getElementById('dash-limit');
if (limitEl) limitEl.textContent = `${data.data_limit_gb} GB`;
const expireEl = document.getElementById('dash-expire');
if (expireEl) 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.data_limit > 0 ? plan.data_limit + ' GB' : 'Unlimited'}
${plan.days} Days
`;
container.appendChild(card);
});
try { if (window.lucide) lucide.createIcons(); } catch (e) { }
} catch (e) {
container.textContent = "Failed to load plans.";
}
}
async function initPayment(planId) {
if (!tg) return alert("Open in Telegram");
const btn = event.target;
const oldText = btn.innerText;
btn.innerText = '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('Successful Payment!', 'success');
router('dashboard');
}
});
} else {
showToast('Error: ' + (data.error || 'Unknown'), 'error');
}
} catch (e) {
showToast('Network error', '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 = "No active subscription";
}
// 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: 'Sleek & Fast', link: 'https://apps.apple.com/us/app/v2box-v2ray-client/id6446814690' },
{ name: 'Streisand', desc: 'Premium Choice', link: 'https://apps.apple.com/us/app/streisand/id6450534064' }
];
} else if (device === 'android') {
apps = [
{ name: 'v2rayNG', desc: 'Standard Client', link: 'https://play.google.com/store/apps/details?id=com.v2ray.ang' },
{ name: 'NekoBox', desc: 'Powerful & Modern', link: 'https://github.com/MatsuriDayo/NekoBoxForAndroid/releases' },
{ name: 'Hiddify', desc: 'Universal All-in-One', link: 'https://play.google.com/store/apps/details?id=app.hiddify.com' }
];
} else if (device === 'windows') {
apps = [
{ name: 'NekoRay', desc: 'Powerful & Open Source', link: 'https://github.com/MatsuriDayo/nekoray/releases' },
{ name: 'Hiddify Next', desc: 'Simple & Modern', link: 'https://github.com/hiddify/hiddify-next/releases' },
{ name: 'v2rayN', desc: 'Classic Client', link: 'https://github.com/2many986/v2rayN/releases' }
];
} else if (device === 'macos') {
apps = [
{ name: 'V2Box', desc: 'Native M1/M2 Support', link: 'https://apps.apple.com/us/app/v2box-v2ray-client/id6446814690' },
{ name: 'Hiddify Next', desc: 'Elegant Desktop Client', 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 = `
Choose a client for your device:
`;
apps.forEach(app => {
html += `
`;
});
html += `
`;
openModal("Download App", html);
lucide.createIcons();
}
function copyConfig() {
if (currentState.subUrl) {
navigator.clipboard.writeText(currentState.subUrl);
showToast("Link copied!", "success");
} else {
showToast("No subscription found.", "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(`Promo Applied! ${data.discount}% OFF.`, "success");
closeModal(); // Optional: close modal on success
} else {
showToast("Invalid Promo Code", "error");
}
} catch (e) {
showToast("Error checking promo", "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 = `
`;
openModal("Support", html);
}
function openAbout() {
openModal("About", `
Stellarisei VPN v1.2
Premium V2Ray service.
Fast, Secure, Reliable.
© 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("Message sent! Admin will contact you.", "success");
} else {
showToast("Failed to send.", "error");
}
} catch (e) {
showToast("Error sending message.", "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 = `
`;
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 || '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: 'Toggle Status', msg: 'Change user active/disabled state?', btnClass: 'btn-primary' },
'reset_traffic': { title: 'Reset Traffic', msg: 'Reset used traffic to zero?', btnClass: 'btn-primary' },
'delete_sub': { title: 'Delete Subscription', msg: 'Are you sure? User will lose VPN access immediately.', btnClass: 'btn-error' }
};
const cfg = config[action];
openModal(cfg.title, `
${cfg.msg}
`);
return;
}
let title = "";
let html = "";
if (action === 'add_days') {
title = "Add Days";
html = `
`;
} else if (action === 'set_limit') {
title = "Set Traffic Limit";
html = `
`;
} else if (action === 'set_expiry') {
title = "Set Expiration";
html = `
`;
} else if (action === 'set_plan') {
title = "Select Plan";
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", `
Are you sure you want to delete promo code ${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("New Promo", `
`);
}
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("Send this message to ALL users?")) 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(`Broadcast sent to ${data.sent} users!`);
}
function openPromoModal() {
openModal("Promo Code", `
`);
}
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);