Files
binance-alpha-farm-agent/agent.user.js

912 lines
38 KiB
JavaScript

// ==UserScript==
// @name Binance Alpha Farm Agent
// @namespace http://baf.thuanle.me
// @version 2025.08.01
// @author TL
// @description Automated trading agent for Binance Alpha Farm
// @match https://www.binance.com/*
// @match https://accounts.binance.com/*
// @run-at document-idle
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @grant GM_notification
// @grant GM_xmlhttpRequest
// @grant GM_log
// @connect baf.thuanle.me
// @connect localhost
// @downloadURL https://git.thuanle.me/public/binance-alpha-farm-agent/raw/branch/main/agent.user.js
// @updateURL https://git.thuanle.me/public/binance-alpha-farm-agent/raw/branch/main/agent.user.js
// ==/UserScript==
if (window.top !== window) {
GM_log(`[TL] ❌ Skipping in iframe ${window.location.href}`);
return;
}
GM_log('[TL] 🏁 Welcome to Binance Alpha Farm Agent.');
(async () => {
'use strict';
// ====== CONFIGURATION ======
const CONFIG = {
HEARTBEAT_INTERVAL: 10000,
DASHBOARD_UPDATE_INTERVAL: 500, // Update dashboard overlay every 0.5 seconds
SERVERS: {
local: { label: '🏠 Local', url: 'http://localhost:3000' },
prod: { label: '🌐 Prod', url: 'https://baf.thuanle.me' },
}
};
// ====== APP ENUMS ======
const AppEnums = {
BOT_STATUS: {
IDLE: 'idle',
RUNNING: 'running',
}
};
// ====== STATE MANAGEMENT ======
/**
* AppStateClass - Quản lý state của ứng dụng
* Lưu trữ runtime state (không persistent), persistent data được lưu trong AppSettings
*/
class AppStateClass {
constructor() {
this.data = {
// Server configuration - Thông tin server hiện tại
server_last_seen: null, // Thời gian cuối cùng ping server thành công
// Page state - Trạng thái trang hiện tại
current_page: null, // Trang hiện tại (detected từ URL)
is_logged_in: false, // Trạng thái đăng nhập Binance
// UI state - Trạng thái UI
is_loading: false, // Đang loading
error_message: null, // Thông báo lỗi
// Session flags - Flags cho session hiện tại (reset khi reload)
menuCreated: false, // Menu đã được tạo
popupInitialized: false, // Popup đã được khởi tạo
appInitialized: false // App đã được khởi tạo hoàn toàn
};
this.observers = new Map(); // Observer pattern cho state changes
this.initialized = false; // Flag khởi tạo
}
/**
* Khởi tạo state từ AppSettings và detect trang hiện tại
* Chỉ chạy một lần khi app khởi động
*/
async initialize() {
if (this.initialized) return;
this.initialized = true;
// Detect trang hiện tại (không block)
const currentPage = BINANCE.page.detectPage();
// Update state (không bao gồm login status)
await this.update({
current_page: currentPage,
});
TL.log('APP-STATE', 'AppState initialized');
}
/**
* Lấy copy của data hiện tại
* @returns {Object} Copy của state data
*/
getData() {
return { ...this.data };
}
// ====== GETTER METHODS ======
getServer() { return CONFIG.SERVERS[AppSettings.getServerType()]; }
getServerLastSeen() { return this.data.server_last_seen; }
/**
* Kiểm tra xem server có kết nối không
* @returns {boolean} true nếu server kết nối trong 1 phút, false nếu không
*/
getServerConnected() {
return this.data.server_last_seen && Date.now() - this.data.server_last_seen < 60000;
}
getCurrentPage() { return this.data.current_page; }
getIsLoggedIn() { return this.data.is_logged_in; }
getIsLoading() { return this.data.is_loading; }
getMenuCreated() { return this.data.menuCreated; }
getPopupInitialized() { return this.data.popupInitialized; }
getAppInitialized() { return this.data.appInitialized; }
/**
* Cập nhật state với partial update
* @param {Object} updates - Object chứa cặp key-value cần cập nhật vào state
* Ví dụ: { current_task: task, is_loading: true }
*/
async update(updates) {
const oldData = { ...this.data };
Object.assign(this.data, updates);
await this.notifyObservers(oldData, this.data);
// TL.debug('STATE', 'Updated', updates);
}
/**
* Đăng ký observer cho state change
* @param {string} key - Key của state cần observe
* @param {Function} callback - Callback function khi state thay đổi
*/
subscribe(key, callback) {
if (!this.observers.has(key)) {
this.observers.set(key, []);
}
this.observers.get(key).push(callback);
}
/**
* Hủy đăng ký observer
* @param {string} key - Key của state
* @param {Function} callback - Callback function cần hủy
*/
unsubscribe(key, callback) {
const callbacks = this.observers.get(key);
if (callbacks) {
const index = callbacks.indexOf(callback);
if (index > -1) {
callbacks.splice(index, 1);
}
}
}
/**
* Thông báo tất cả observers khi state thay đổi
* @param {Object} oldData - State cũ
* @param {Object} newData - State mới
*/
async notifyObservers(oldData, newData) {
for (const [key, callbacks] of this.observers) {
const oldValue = oldData[key];
const newValue = newData[key];
if (oldValue !== newValue) {
for (const callback of callbacks) {
try {
await callback(oldValue, newValue, newData);
} catch (error) {
TL.error('STATE', `Observer error for ${key}:`, error);
}
}
}
}
}
}
const AppState = new AppStateClass();
// ====== APP SETTINGS ======
/**
* AppSettings - Quản lý persistent settings của ứng dụng
* Lưu trữ cài đặt người dùng trong GM storage (vĩnh viễn)
*/
const AppSettings = {
key_token: 'baf-agent-token',
key_server_type: 'baf-server-type',
key_bot_status: 'baf-bot-status',
key_popup_position: 'baf-popup-position',
key_popup_visible: 'baf-popup-visible',
key_debug: 'baf-debug',
key_safety_guard: 'baf-safety-guard',
// Token management
getToken: () => GM_getValue(AppSettings.key_token, ''),
setToken: token => GM_setValue(AppSettings.key_token, String(token || '').trim().replace(/^Bearer\s+/i, '')),
// Server configuration
getServerType: () => GM_getValue(AppSettings.key_server_type, 'prod'),
setServerType: (type) => {
GM_setValue(AppSettings.key_server_type, type);
AppState.update({ server_last_seen: null });
},
getBotStatus: () => GM_getValue(AppSettings.key_bot_status, AppEnums.BOT_STATUS.IDLE),
setBotStatus: (status) => GM_setValue(AppSettings.key_bot_status, status),
// Popup configuration
getPopupPosition: () => GM_getValue(AppSettings.key_popup_position, 4),
setPopupPosition: (position) => GM_setValue(AppSettings.key_popup_position, position),
getPopupVisible: () => GM_getValue(AppSettings.key_popup_visible, true),
setPopupVisible: (visible) => GM_setValue(AppSettings.key_popup_visible, visible),
// Debug mode
getDebug: () => GM_getValue(AppSettings.key_debug, true),
setDebug: (debug) => GM_setValue(AppSettings.key_debug, debug),
// Safety guard
getSafetyGuard: () => GM_getValue(AppSettings.key_safety_guard, true),
setSafetyGuard: (safetyGuard) => {
GM_setValue(AppSettings.key_safety_guard, safetyGuard);
GM_log(`[TL] Safety guard set to: ${safetyGuard}`);
},
};
// ====== COMMON UTILITIES ======
/**
* TL - Common utilities cho logging, notifications, và DOM helpers
*/
const TL = {
debug: (tag, msg, ...args) => AppSettings.getDebug() && GM_log(`[TL] [${tag}]\n${msg}`, ...args),
log: (tag, msg, ...args) => GM_log(`[TL] [${tag}]\n${msg}`, ...args),
warn: (tag, msg, ...args) => GM_log(`[TL] [WARN] [${tag}] ⚠️\n${msg}`, ...args),
error: (tag, msg, ...args) => GM_log(`[TL] [ERROR] [${tag}] ❌\n${msg}`, ...args),
noti: (title, text, timeout = 2500) => {
if (typeof GM_notification === 'function') {
GM_notification({ title, text, timeout });
return true;
}
alert(`${title}\n${text}`);
return false;
},
delay: (ms) => new Promise(resolve => setTimeout(resolve, ms)),
// DOM helpers
dom: {
isVisible: (ele) => {
if (!ele) return false;
const cs = getComputedStyle(ele);
if (cs.display === 'none' || cs.visibility === 'hidden' || cs.opacity === '0') return false;
return (ele.offsetWidth + ele.offsetHeight) > 0;
},
isDisabled: (ele) => {
return ele?.disabled === true || ele?.getAttribute('aria-disabled') === 'true';
},
isInViewport: (ele) => {
if (!ele) return false;
const rect = ele.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
);
},
click: (ele) => {
if (ele && typeof ele.click === 'function') ele.click();
},
scrollToView: (ele, behavior = 'smooth') => {
ele?.scrollIntoView?.({ behavior, block: 'center' });
},
setHover: (ele, eventType, styles) => {
if (!ele) return;
const applyStyles = () => {
if (styles.scale) ele.style.transform = `scale(${styles.scale})`;
if (styles.transform) ele.style.transform = styles.transform;
if (styles.background) ele.style.background = styles.background;
if (styles.color) ele.style.color = styles.color;
if (styles.border) ele.style.border = styles.border;
};
ele.addEventListener(eventType, applyStyles);
},
},
// Browser helpers
browser: {
/**
* Navigate to URL với option forceReload
* @param {string} url - URL cần navigate
*/
navigate: (url) => {
window.location.href = url;
}
},
// Network helpers
net: {
gmRequest(url, init = {}) {
const headersToObject = (h) => {
const o = {};
(h || new Headers()).forEach((v, k) => { o[k] = v; });
return o;
};
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
url,
method: (init.method || 'GET').toUpperCase(),
headers: headersToObject(init.headers),
data: init.body,
onload: (resp) => {
const text = resp.responseText || '';
let data = text;
const isJSON = /content-type:\s*application\/json/i.test(resp.responseHeaders || '');
if (isJSON) { try { data = JSON.parse(text); } catch { } }
TL.debug(`net`, `${init.method} ${resp.status} ${url}\n${init.body}`, data);
resolve({
status: resp.status,
ok: resp.status >= 200 && resp.status < 300,
data,
rawText: text,
headers: null
});
},
onerror: reject,
ontimeout: () => reject(new Error('GM_xmlhttpRequest timeout')),
});
});
},
}
};
// ====== BAF API MODULE ======
const BAF = {
getHost: () => AppState.getServer()?.url,
request: async (method, path, { params, body, headers } = {}) => {
const base = BAF.getHost();
const url = new URL(path, base);
if (params) for (const [k, v] of Object.entries(params)) url.searchParams.append(k, v);
const h = new Headers(headers || {});
const token = await AppSettings.getToken();
if (token && !h.has('Authorization')) h.set('Authorization', `Bearer ${token}`);
const init = { method, headers: h };
if (body !== undefined && body !== null) {
if (!(body instanceof FormData) && !h.has('Content-Type')) h.set('Content-Type', 'application/json');
init.body = (h.get('Content-Type') || '').includes('application/json') && typeof body !== 'string'
? JSON.stringify(body)
: body;
}
return TL.net.gmRequest(url.toString(), init);
},
get: (path, params, init = {}) => BAF.request('GET', path, { params, headers: init.headers }),
post: (path, body, init = {}) => BAF.request('POST', path, { body, headers: init.headers }),
ping: (status) => BAF.post('/agent/ping', status),
};
// ====== BINANCE MODULE ======
// Binance Pages constants
const BINANCE_PAGES = {
LOGIN: 'login',
ALPHA_SWAP: 'alpha-swap',
ALPHA_ORDER_HISTORY: 'alpha-order-history',
WALLET_EARN: 'wallet-earn',
WALLET_FUNDING: 'wallet-funding',
WALLET_SPOT: 'wallet-spot',
IGNORE: 'ignore',
UNKNOWN: 'unknown'
};
// Page patterns for detection
const BINANCE_PAGE_PATTERNS = [
{
host: 'accounts.binance.com',
patterns: [
{ includes: '/login', page: BINANCE_PAGES.LOGIN }
]
},
{
host: 'www.binance.com',
patterns: [
{ includes: '/alpha/bsc/', page: BINANCE_PAGES.ALPHA_SWAP },
{ includes: '/my/orders/alpha/orderhistory', page: BINANCE_PAGES.ALPHA_ORDER_HISTORY },
{ includes: '/my/wallet/account/earn', page: BINANCE_PAGES.WALLET_EARN },
{ includes: '/my/wallet/funding', page: BINANCE_PAGES.WALLET_FUNDING },
{ includes: '/my/wallet/account/main', page: BINANCE_PAGES.WALLET_SPOT }
]
}
];
const BINANCE = {
page: {
detectPage: () => {
const { hostname, pathname } = window.location;
if (window.top !== window) {
TL.debug('BINANCE-PAGE-DETECT', 'Call from iframe ❌');
return BINANCE_PAGES.IGNORE;
}
const hostConfig = BINANCE_PAGE_PATTERNS.find(cfg => cfg.host === hostname);
if (!hostConfig) {
return BINANCE_PAGES.UNKNOWN;
}
// Check patterns in order (most specific first)
for (const pattern of hostConfig.patterns) {
if (pathname.includes(pattern.includes)) {
TL.debug('BINANCE-PAGE-DETECT', `Matched pattern: ${pattern.includes} -> ${pattern.page}`);
return pattern.page;
}
}
TL.debug('BINANCE-PAGE-DETECT', `No pattern matched: ${hostname}${pathname}`);
return BINANCE_PAGES.UNKNOWN;
},
isOnLoginPage: () => AppState.getCurrentPage() === BINANCE_PAGES.LOGIN,
isOnAlphaSwapPage: (contractAddress) => {
const page = AppState.getCurrentPage();
return page === BINANCE_PAGES.ALPHA_SWAP && (!contractAddress || window.location.pathname.includes(contractAddress));
},
isOnAlphaOrderHistoryPage: () => AppState.getCurrentPage() === BINANCE_PAGES.ALPHA_ORDER_HISTORY,
isOnWalletEarnPage: () => AppState.getCurrentPage() === BINANCE_PAGES.WALLET_EARN,
isOnWalletFundingPage: () => AppState.getCurrentPage() === BINANCE_PAGES.WALLET_FUNDING,
isOnWalletSpotPage: () => AppState.getCurrentPage() === BINANCE_PAGES.WALLET_SPOT,
// Navigation URL generators - chỉ trả về address, không thực hiện navigate
getLoginUrl: () => 'https://www.binance.com/en/login',
getAlphaSwapUrl: (contractAddress) => {
return contractAddress
? `https://www.binance.com/en/alpha/bsc/${contractAddress}`
: `https://www.binance.com/en/alpha`;
},
getAlphaOrderHistoryUrl: () => 'https://www.binance.com/en/my/orders/alpha/orderhistory',
getWalletEarnUrl: () => 'https://www.binance.com/en/my/wallet/account/earn',
getWalletFundingUrl: () => 'https://www.binance.com/en/my/wallet/funding',
getWalletSpotUrl: () => 'https://www.binance.com/en/my/wallet/account/main'
},
auth: {
detectLoginState: () => {
if (!AppSettings.getSafetyGuard()) return null;
if (window.top !== window) {
TL.debug('BINANCE-LOGIN', 'In iframe - cannot determine login state');
return null;
}
if (BINANCE.page.isOnLoginPage()) {
return false;
}
// Method 1: Check for login/register buttons (indicates NOT logged in)
const loginBtn = document.querySelector('#toLoginPage, [data-testid="login-button"], .login-btn');
const regBtn = document.querySelector('#toRegisterPage, [data-testid="register-button"], .register-btn');
let msg = `loginBtn: ${TL.dom.isVisible?.(loginBtn)} regBtn: ${TL.dom.isVisible?.(regBtn)}\n`;
// If login/register buttons are visible, definitely not logged in
if (TL.dom.isVisible?.(loginBtn) || TL.dom.isVisible?.(regBtn)) {
TL.debug('BINANCE-LOGIN', msg + 'Login/Register buttons visible -> NOT logged in');
return false;
}
// Method 2: Check for user account elements (indicates logged in)
const dashboardLink = document.querySelector('a[href*="/my/dashboard"]');
const accountIcon = document.querySelector('.header-account-icon');
const walletBtn = document.querySelector('#ba-wallet');
const isDashboardVisible = dashboardLink && TL.dom.isVisible?.(dashboardLink);
const isAccountIconVisible = !!accountIcon;
const isWalletVisible = walletBtn && TL.dom.isVisible?.(walletBtn);
msg += `Visible: dashboard: ${isDashboardVisible ? '✓' : '✗'}, accountIcon: ${isAccountIconVisible ? '✓' : '✗'}, walletBtn: ${isWalletVisible ? '✓' : '✗'}\n`;
// If we see user account elements, likely logged in
if (isDashboardVisible && isAccountIconVisible && isWalletVisible) {
TL.debug('BINANCE-LOGIN', msg + 'User account elements found -> LIKELY logged in');
return true;
}
TL.debug('BINANCE-LOGIN', msg + 'Cannot determine login state - returning null');
return null;
},
isLoggedIn: async (timeoutMs = 30000, pollMs = 500) => {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const state = BINANCE.auth.detectLoginState();
if (state !== null) {
TL.debug(`BINANCE-LOGIN`, `isLoggedIn: ${state ? 'true' : 'false'}`);
return state;
}
TL.debug(`BINANCE-LOGIN`, `isLoggedIn: not found. Sleeping...`);
await TL.delay(pollMs);
}
const fallback = BINANCE.auth.detectLoginState() ?? false;
TL.debug(`BINANCE-LOGIN`, `isLoggedIn (timeout, fallback) => ${fallback ? 'true' : 'false'}`);
return fallback;
}
}
};
// ====== APP UI ======
const AppUi = {
// ====== DASHBOARD OVERLAY ======
dashboardOverlay: {
element: null,
// Get position CSS based on position number
getPositionCSS: (position) => {
switch (position) {
case 1: return { top: '10px', left: '10px', bottom: 'auto', right: 'auto' }; // top-left
case 2: return { top: '10px', right: '10px', bottom: 'auto', left: 'auto' }; // top-right
case 3: return { bottom: '10px', left: '10px', top: 'auto', right: 'auto' }; // bottom-left
case 4: return { bottom: '10px', right: '10px', top: 'auto', left: 'auto' }; // bottom-right
default: return { bottom: '10px', right: '10px', top: 'auto', left: 'auto' };
}
},
// Create dashboard overlay element
create: () => {
if (AppUi.dashboardOverlay.element) {
document.body.removeChild(AppUi.dashboardOverlay.element);
}
const overlay = document.createElement('div');
overlay.id = 'baf-status-overlay';
overlay.style.cssText = `
position: fixed;
z-index: 999999;
background: rgba(0, 0, 0, 0.9);
color: white;
padding: 12px;
border-radius: 8px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 12px;
line-height: 1.4;
min-width: 200px;
max-width: 300px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
transition: all 0.3s ease;
user-select: none;
`;
// Add position toggle button
const positionBtn = document.createElement('div');
positionBtn.style.cssText = `
position: absolute;
top: -8px;
right: -8px;
width: 20px;
height: 20px;
background: #007bff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 10px;
color: white;
border: 2px solid rgba(255, 255, 255, 0.2);
transition: all 0.2s ease;
`;
positionBtn.innerHTML = '⇄';
positionBtn.title = 'Change position';
positionBtn.addEventListener('click', () => {
const newPosition = (AppSettings.getPopupPosition() % 4) + 1;
AppSettings.setPopupPosition(newPosition);
AppUi.statusOverlay.updatePosition();
});
TL.dom.setHover(positionBtn, 'mouseenter', { scale: 1.5, background: '#0056b3' });
TL.dom.setHover(positionBtn, 'mouseleave', { scale: 1, background: '#007bff' });
overlay.appendChild(positionBtn);
AppUi.dashboardOverlay.element = overlay;
document.body.appendChild(overlay);
return overlay;
},
// Update overlay position
updatePosition: () => {
if (!AppUi.dashboardOverlay.element) return;
const positionCSS = AppUi.dashboardOverlay.getPositionCSS(AppSettings.getPopupPosition());
Object.assign(AppUi.dashboardOverlay.element.style, positionCSS);
},
// Update overlay content
updateContent: () => {
// Get status display
let statusDisplay = '';
switch (AppSettings.getBotStatus()) {
case AppEnums.BOT_STATUS.IDLE:
statusDisplay = '💤 Idle';
break;
case AppEnums.BOT_STATUS.RUNNING:
statusDisplay = '▶️ Running';
break;
default:
statusDisplay = '❓ Unknown';
}
// Get server info
const serverLabel = AppState.getServer()?.label || 'Unknown';
const serverConnected = AppState.getServerConnected() ? '🟢' : '🔴';
// Get page info
const pageDisplay = AppState.getCurrentPage() || 'unknown';
// Get login status
const loginStatus = AppState.getIsLoggedIn() ? '✅' : '❌';
// Get SafetyGuard status
const safetyStatus = {
enabled: AppSettings.getSafetyGuard(),
message: AppSettings.getSafetyGuard() ? '✅ Operations Enabled' : '🚨 Operations Blocked'
};
// Build content
const content = `
<div style="margin-bottom: 8px;"><strong>BAF Agent Dashboard</strong></div>
<div style="margin-bottom: 4px;">
<span id="safety-toggle-btn" style="color: ${safetyStatus.enabled ? '#28a745' : '#dc3545'}; cursor: pointer; padding: 2px 4px; border-radius: 3px; border: 1px solid ${safetyStatus.enabled ? '#28a745' : '#dc3545'}; background: ${safetyStatus.enabled ? 'rgba(40, 167, 69, 0.1)' : 'rgba(220, 53, 69, 0.1)'}; transition: all 0.2s ease;" title="Click to toggle safety status">🛟 Safety: ${safetyStatus.enabled ? '🛡️ Safe' : '🚨 Blocked'}</span>
</div>
<div style="margin-bottom: 4px;">
<span id="debug-toggle-btn" style="color: ${AppSettings.getDebug() ? '#17a2b8' : '#6c757d'}; cursor: pointer; padding: 2px 4px; border-radius: 3px; border: 1px solid ${AppSettings.getDebug() ? '#17a2b8' : '#6c757d'}; background: ${AppSettings.getDebug() ? 'rgba(23, 162, 184, 0.1)' : 'rgba(108, 117, 125, 0.1)'}; transition: all 0.2s ease;" title="Click to toggle debug mode">🐛 Debug: ${AppSettings.getDebug() ? '✔️ ON' : '❌ OFF'}</span>
</div>
<div style="margin-bottom: 4px;">
<span style="color: #007bff;">Bot Status: ${statusDisplay}</span>
</div>
<div style="margin-bottom: 4px;">
<span id="server-toggle-btn" style="color: #e83e8c; cursor: pointer; padding: 2px 4px; border-radius: 3px; border: 1px solid #e83e8c; background: rgba(232, 62, 140, 0.1); transition: all 0.2s ease;" title="Click to switch between local and prod servers">🌐 Server: ${serverLabel} ${serverConnected}</span>
</div>
<div style="margin-bottom: 4px;">
<span style="color: #fd7e14;">👤 Login:${loginStatus}</span>
</div>
<div style="margin-bottom: 4px;">
<span style="color: #6f42c1;">Page: ${pageDisplay}</span>
</div>
`;
// Update content (excluding position button)
const contentDiv = AppUi.dashboardOverlay.element.querySelector('.baf-content') || document.createElement('div');
contentDiv.className = 'baf-content';
contentDiv.innerHTML = content;
if (!AppUi.dashboardOverlay.element.querySelector('.baf-content')) {
AppUi.dashboardOverlay.element.appendChild(contentDiv);
}
// Add event listener for safety toggle button
const safetyToggleBtn = contentDiv.querySelector('#safety-toggle-btn');
if (safetyToggleBtn) {
safetyToggleBtn.onclick = () => {
const currentStatus = {
enabled: AppSettings.getSafetyGuard(),
message: AppSettings.getSafetyGuard() ? '✅ Operations Enabled' : '🚨 Operations Blocked'
};
if (currentStatus.enabled) {
AppSettings.setSafetyGuard(false);
} else {
AppSettings.setSafetyGuard(true);
}
// Update the content to reflect the change
AppUi.dashboardOverlay.updateContent();
};
}
// Add event listener for debug toggle button
const debugToggleBtn = contentDiv.querySelector('#debug-toggle-btn');
if (debugToggleBtn) {
debugToggleBtn.onclick = () => {
const newDebugState = !AppSettings.getDebug();
AppSettings.setDebug(newDebugState);
// Update the content to reflect the change
AppUi.dashboardOverlay.updateContent();
};
}
// Add event listener for server toggle button
const serverToggleBtn = contentDiv.querySelector('#server-toggle-btn');
if (serverToggleBtn) {
serverToggleBtn.onclick = async () => {
const currentServerType = AppSettings.getServerType();
const newServerType = currentServerType === 'local' ? 'prod' : 'local';
// Update server type in settings
AppSettings.setServerType(newServerType);
await AppState.update({ server_last_seen: null });
// Update the content to reflect the change
AppUi.dashboardOverlay.updateContent();
};
}
},
// Show dashboard overlay
show: () => {
if (!AppUi.dashboardOverlay.element) {
AppUi.dashboardOverlay.create();
}
AppUi.dashboardOverlay.updatePosition();
AppUi.dashboardOverlay.updateContent();
AppUi.dashboardOverlay.element.style.display = 'block';
},
// Hide dashboard overlay
hide: () => {
if (AppUi.dashboardOverlay.element) {
AppUi.dashboardOverlay.element.style.display = 'none';
}
},
// Toggle dashboard overlay
toggle: () => {
if (AppSettings.getPopupVisible()) {
AppUi.dashboardOverlay.hide();
AppSettings.setPopupVisible(false);
} else {
AppUi.dashboardOverlay.show();
AppSettings.setPopupVisible(true);
}
},
// Initialize dashboard overlay
init: () => {
if (AppSettings.getPopupVisible()) {
AppUi.dashboardOverlay.show();
}
}
},
// ====== MENU SYSTEM ======
menu: {
// Create GM menu
create: async () => {
try {
if (AppState.getMenuCreated()) {
return;
}
AppState.update({ menuCreated: true });
GM_registerMenuCommand('🛟 Safety Guard: -> ✅ Enable Operations', () => {
AppSettings.setSafetyGuard(true);
});
GM_registerMenuCommand('🛟 Safety Guard: -> 🚨 Block Operations', () => {
AppSettings.setSafetyGuard(false);
});
GM_registerMenuCommand('🔑 Token', async () => {
try {
const curToken = await AppSettings.getToken();
const input = prompt('Bearer token:', curToken || '');
if (input !== null && input.trim() && input.trim() !== curToken) {
await AppSettings.setToken(input);
}
const s = AppState.getServer();
const res = await BAF.ping();
const resStr =
`Server: ${s.label} (${s.url})\n` +
`Status: ${res.ok ? 'Connected ✅' : 'Failed ❌'} (${res.status})`;
TL.debug(`BAF`, resStr);
TL.noti('BAF Server', resStr);
} catch (e) {
const resStr = `ping error: ${e.message}`;
TL.error('BAF', resStr);
TL.noti('BAF Server', resStr);
}
});
// Popup toggle menu
GM_registerMenuCommand("👁️ Toggle Popup", () => {
AppUi.dashboardOverlay.toggle();
});
} catch (error) {
TL.error('MENU', 'Failed to create menu:', error);
throw error;
}
}
},
};
// ====== APP SERVICE ======
const AppServices = {
initialize: async () => {
await AppState.initialize();
await AppUi.menu.create();
AppUi.dashboardOverlay.init();
// Setup observers for dashboard overlay updates
AppServices.initObservers();
// Setup interval services
AppServices.initInterval();
TL.log('APP-SERVICES', 'AppServices initialized');
},
initObservers: () => {
// AppState.subscribe('server_last_seen', () => {
// if (AppSettings.getPopupVisible()) {
// AppUi.dashboardOverlay.updateContent();
// }
// });
// AppState.subscribe('is_logged_in', () => {
// if (AppSettings.getPopupVisible()) {
// AppUi.dashboardOverlay.updateContent();
// }
// });
// AppState.subscribe('current_page', () => {
// if (AppSettings.getPopupVisible()) {
// AppUi.dashboardOverlay.updateContent();
// }
// });
},
// Setup interval services
initInterval: () => {
// Setup heartbeat interval
setInterval(() => AppServices.heartbeat(), CONFIG.HEARTBEAT_INTERVAL);
// Setup dashboard overlay update interval
setInterval(() => AppUi.dashboardOverlay.updateContent(), CONFIG.DASHBOARD_UPDATE_INTERVAL);
TL.debug('APP-SERVICES', 'Interval services started');
},
isConnected: () => {
const serverLastSeen = AppState.getServerLastSeen();
return serverLastSeen && new Date() - serverLastSeen < 60000;
},
// ====== HEARTBEAT SYSTEM ======
heartbeat: async () => {
if (!AppSettings.getSafetyGuard()) return null;
try {
const status = {
logged_in: AppState.getIsLoggedIn(),
current_page: AppState.getCurrentPage(),
bot_status: AppSettings.getBotStatus(),
current_task: null, // TODO: Add to AppState if needed
task_data: null, // TODO: Add to AppState if needed
current_step: null, // TODO: Add to AppState if needed
};
// TL.debug(`HEARTBEAT`, `${JSON.stringify(status, null, 2)}`);
const res = await BAF.ping(status);
if (res.ok) {
AppState.update({ server_last_seen: new Date() });
}
return status;
} catch (e) {
TL.error('HEARTBEAT', e.message);
return null;
}
}
};
// Start the application
try {
await AppServices.initialize();
} catch (error) {
TL.error('MAIN', 'Failed to initialize app:', error);
}
TL.log('MAIN', 'App started');
})();