Error correction

This commit is contained in:
2025-11-11 15:01:02 +01:00
parent 108c9bf215
commit 3f65ac83cd
4 changed files with 46 additions and 223 deletions

View File

@@ -24,25 +24,33 @@ if (savedTheme === 'light') {
document.querySelector('.theme-toggle i').className = 'fas fa-sun' document.querySelector('.theme-toggle i').className = 'fas fa-sun'
} }
// HTML escaping utility to prevent XSS attacks
function escapeHtml(text) {
if (text === null || text === undefined) return ''
const div = document.createElement('div')
div.textContent = String(text)
return div.innerHTML
}
// Toast notification // Toast notification
function showToast(message, type = 'success') { function showToast(message, type = 'success') {
const container = document.getElementById('toastContainer') const container = document.getElementById('toastContainer')
const toast = document.createElement('div') const toast = document.createElement('div')
toast.className = `toast toast-${type}` toast.className = `toast toast-${type}`
const iconMap = { const iconMap = {
success: 'fa-check-circle', success: 'fa-check-circle',
error: 'fa-exclamation-circle', error: 'fa-exclamation-circle',
info: 'fa-info-circle' info: 'fa-info-circle'
} }
toast.innerHTML = ` toast.innerHTML = `
<i class="fas ${iconMap[type]}"></i> <i class="fas ${iconMap[type]}"></i>
<span>${message}</span> <span>${escapeHtml(message)}</span>
` `
container.appendChild(toast) container.appendChild(toast)
setTimeout(() => { setTimeout(() => {
toast.remove() toast.remove()
}, 5000) }, 5000)
@@ -54,7 +62,7 @@ function updateStatus(data) {
const badge = document.getElementById('statusBadge') const badge = document.getElementById('statusBadge')
const btnStart = document.getElementById('btnStart') const btnStart = document.getElementById('btnStart')
const btnStop = document.getElementById('btnStop') const btnStop = document.getElementById('btnStop')
if (data.running) { if (data.running) {
badge.className = 'status-badge status-running' badge.className = 'status-badge status-running'
badge.textContent = 'RUNNING' badge.textContent = 'RUNNING'
@@ -78,29 +86,30 @@ function updateMetrics(data) {
function updateAccounts(data) { function updateAccounts(data) {
accounts = data accounts = data
const container = document.getElementById('accountsList') const container = document.getElementById('accountsList')
if (data.length === 0) { if (data.length === 0) {
container.innerHTML = '<div class="empty-state"><i class="fas fa-inbox"></i><p>No accounts configured</p></div>' container.innerHTML = '<div class="empty-state"><i class="fas fa-inbox"></i><p>No accounts configured</p></div>'
return return
} }
// SECURITY FIX: Escape all user-provided data to prevent XSS
container.innerHTML = data.map(acc => ` container.innerHTML = data.map(acc => `
<div class="account-item"> <div class="account-item">
<div class="account-info"> <div class="account-info">
<div class="account-avatar">${acc.maskedEmail.charAt(0).toUpperCase()}</div> <div class="account-avatar">${escapeHtml(acc.maskedEmail.charAt(0).toUpperCase())}</div>
<div class="account-details"> <div class="account-details">
<div class="account-email">${acc.maskedEmail}</div> <div class="account-email">${escapeHtml(acc.maskedEmail)}</div>
<div class="account-status-text"> <div class="account-status-text">
${acc.lastSync ? `Last sync: ${new Date(acc.lastSync).toLocaleString()}` : 'Never synced'} ${acc.lastSync ? `Last sync: ${escapeHtml(new Date(acc.lastSync).toLocaleString())}` : 'Never synced'}
</div> </div>
</div> </div>
</div> </div>
<div class="account-stats"> <div class="account-stats">
<div class="account-points"> <div class="account-points">
<div class="account-points-value">${acc.points !== undefined ? acc.points.toLocaleString() : 'N/A'}</div> <div class="account-points-value">${acc.points !== undefined ? escapeHtml(acc.points.toLocaleString()) : 'N/A'}</div>
<div class="account-points-label">Points</div> <div class="account-points-label">Points</div>
</div> </div>
<span class="account-badge badge-${acc.status}">${acc.status.toUpperCase()}</span> <span class="account-badge badge-${escapeHtml(acc.status)}">${escapeHtml(acc.status.toUpperCase())}</span>
</div> </div>
</div> </div>
`).join('') `).join('')
@@ -116,21 +125,22 @@ function addLog(log) {
function renderLogs() { function renderLogs() {
const container = document.getElementById('logsContainer') const container = document.getElementById('logsContainer')
if (logs.length === 0) { if (logs.length === 0) {
container.innerHTML = '<div class="empty-state"><i class="fas fa-stream"></i><p>No logs yet...</p></div>' container.innerHTML = '<div class="empty-state"><i class="fas fa-stream"></i><p>No logs yet...</p></div>'
return return
} }
// SECURITY FIX: Escape all log data to prevent XSS
container.innerHTML = logs.map(log => ` container.innerHTML = logs.map(log => `
<div class="log-entry log-level-${log.level}"> <div class="log-entry log-level-${escapeHtml(log.level)}">
<span class="log-timestamp">[${new Date(log.timestamp).toLocaleTimeString()}]</span> <span class="log-timestamp">[${escapeHtml(new Date(log.timestamp).toLocaleTimeString())}]</span>
<span class="log-platform platform-${log.platform}">${log.platform}</span> <span class="log-platform platform-${escapeHtml(log.platform)}">${escapeHtml(log.platform)}</span>
<span class="log-title">[${log.title}]</span> <span class="log-title">[${escapeHtml(log.title)}]</span>
<span>${log.message}</span> <span>${escapeHtml(log.message)}</span>
</div> </div>
`).join('') `).join('')
// Auto-scroll to bottom // Auto-scroll to bottom
container.scrollTop = container.scrollHeight container.scrollTop = container.scrollHeight
} }
@@ -144,7 +154,7 @@ async function fetchData() {
fetch('/api/metrics'), fetch('/api/metrics'),
fetch('/api/logs?limit=100') fetch('/api/logs?limit=100')
]) ])
updateStatus(await statusRes.json()) updateStatus(await statusRes.json())
updateAccounts(await accountsRes.json()) updateAccounts(await accountsRes.json())
updateMetrics(await metricsRes.json()) updateMetrics(await metricsRes.json())
@@ -223,15 +233,15 @@ function refreshData() {
function connectWebSocket() { function connectWebSocket() {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:' const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
ws = new WebSocket(`${protocol}//${window.location.host}`) ws = new WebSocket(`${protocol}//${window.location.host}`)
ws.onopen = () => { ws.onopen = () => {
console.log('WebSocket connected') console.log('WebSocket connected')
} }
ws.onmessage = (event) => { ws.onmessage = (event) => {
try { try {
const data = JSON.parse(event.data) const data = JSON.parse(event.data)
if (data.type === 'init') { if (data.type === 'init') {
logs = data.data.logs || [] logs = data.data.logs || []
renderLogs() renderLogs()
@@ -256,12 +266,12 @@ function connectWebSocket() {
console.error('WebSocket message error:', error) console.error('WebSocket message error:', error)
} }
} }
ws.onclose = () => { ws.onclose = () => {
console.log('WebSocket disconnected, reconnecting...') console.log('WebSocket disconnected, reconnecting...')
setTimeout(connectWebSocket, 3000) setTimeout(connectWebSocket, 3000)
} }
ws.onerror = (error) => { ws.onerror = (error) => {
console.error('WebSocket error:', error) console.error('WebSocket error:', error)
} }

View File

@@ -11,7 +11,7 @@ import Humanizer from './util/browser/Humanizer'
import { formatDetailedError, normalizeRecoveryEmail, shortErrorMessage, Util } from './util/core/Utils' import { formatDetailedError, normalizeRecoveryEmail, shortErrorMessage, Util } from './util/core/Utils'
import Axios from './util/network/Axios' import Axios from './util/network/Axios'
import { QueryDiversityEngine } from './util/network/QueryDiversityEngine' import { QueryDiversityEngine } from './util/network/QueryDiversityEngine'
import { log } from './util/notifications/Logger' import { log, stopWebhookCleanup } from './util/notifications/Logger'
import JobState from './util/state/JobState' import JobState from './util/state/JobState'
import { loadAccounts, loadConfig } from './util/state/Load' import { loadAccounts, loadConfig } from './util/state/Load'
import { MobileRetryTracker } from './util/state/MobileRetryTracker' import { MobileRetryTracker } from './util/state/MobileRetryTracker'
@@ -908,18 +908,22 @@ async function main(): Promise<void> {
const errorMsg = reason instanceof Error ? reason.message : String(reason) const errorMsg = reason instanceof Error ? reason.message : String(reason)
const stack = reason instanceof Error ? reason.stack : undefined const stack = reason instanceof Error ? reason.stack : undefined
log('main', 'FATAL', `UnhandledRejection: ${errorMsg}${stack ? `\nStack: ${stack.split('\n').slice(0, 3).join(' | ')}` : ''}`, 'error') log('main', 'FATAL', `UnhandledRejection: ${errorMsg}${stack ? `\nStack: ${stack.split('\n').slice(0, 3).join(' | ')}` : ''}`, 'error')
stopWebhookCleanup() // CLEANUP FIX: Stop webhook cleanup interval
gracefulExit(1) gracefulExit(1)
}) })
process.on('uncaughtException', (err: Error) => { process.on('uncaughtException', (err: Error) => {
log('main', 'FATAL', `UncaughtException: ${err.message}${err.stack ? `\nStack: ${err.stack.split('\n').slice(0, 3).join(' | ')}` : ''}`, 'error') log('main', 'FATAL', `UncaughtException: ${err.message}${err.stack ? `\nStack: ${err.stack.split('\n').slice(0, 3).join(' | ')}` : ''}`, 'error')
stopWebhookCleanup() // CLEANUP FIX: Stop webhook cleanup interval
gracefulExit(1) gracefulExit(1)
}) })
process.on('SIGTERM', () => { process.on('SIGTERM', () => {
log('main', 'SHUTDOWN', 'Received SIGTERM, shutting down gracefully...', 'log') log('main', 'SHUTDOWN', 'Received SIGTERM, shutting down gracefully...', 'log')
stopWebhookCleanup() // CLEANUP FIX: Stop webhook cleanup interval
gracefulExit(0) gracefulExit(0)
}) })
process.on('SIGINT', () => { process.on('SIGINT', () => {
log('main', 'SHUTDOWN', 'Received SIGINT (Ctrl+C), shutting down gracefully...', 'log') log('main', 'SHUTDOWN', 'Received SIGINT (Ctrl+C), shutting down gracefully...', 'log')
stopWebhookCleanup() // CLEANUP FIX: Stop webhook cleanup interval
gracefulExit(0) gracefulExit(0)
}) })
} }

View File

@@ -151,181 +151,5 @@ export async function waitForElementSmart(
} }
} }
/** // DEAD CODE REMOVED: waitForNavigationSmart, clickElementSmart, and typeIntoFieldSmart
* Wait for navigation to complete intelligently // These functions were never used in the codebase and have been removed to reduce complexity
* Uses URL change + DOM ready instead of fixed timeouts
*/
export async function waitForNavigationSmart(
page: Page,
options: {
expectedUrl?: string | RegExp
maxWaitMs?: number
logFn?: (msg: string) => void
} = {}
): Promise<{ completed: boolean; timeMs: number; url: string }> {
const startTime = Date.now()
const maxWaitMs = options.maxWaitMs ?? 15000
const logFn = options.logFn ?? (() => { })
try {
// Wait for URL to change (if we expect it to)
if (options.expectedUrl) {
const urlPattern = typeof options.expectedUrl === 'string'
? new RegExp(options.expectedUrl.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
: options.expectedUrl
let urlChanged = false
const checkInterval = 100
const maxChecks = maxWaitMs / checkInterval
for (let i = 0; i < maxChecks; i++) {
const currentUrl = page.url()
if (urlPattern.test(currentUrl)) {
urlChanged = true
logFn(`✓ URL changed to expected pattern (${Date.now() - startTime}ms)`)
break
}
await page.waitForTimeout(checkInterval)
}
if (!urlChanged) {
const elapsed = Date.now() - startTime
logFn(`⚠ URL did not match expected pattern after ${elapsed}ms`)
return { completed: false, timeMs: elapsed, url: page.url() }
}
}
// Wait for page to be ready after navigation
const readyResult = await waitForPageReady(page, {
logFn
})
const elapsed = Date.now() - startTime
return { completed: readyResult.ready, timeMs: elapsed, url: page.url() }
} catch (error) {
const elapsed = Date.now() - startTime
const errorMsg = error instanceof Error ? error.message : String(error)
logFn(`✗ Navigation wait failed after ${elapsed}ms: ${errorMsg}`)
return { completed: false, timeMs: elapsed, url: page.url() }
}
}
/**
* Click element with smart waiting (wait for element + click + verify action)
*/
export async function clickElementSmart(
page: Page,
selector: string,
options: {
waitBeforeClick?: number
waitAfterClick?: number
verifyDisappeared?: boolean
maxWaitMs?: number
logFn?: (msg: string) => void
} = {}
): Promise<{ success: boolean; timeMs: number }> {
const startTime = Date.now()
const waitBeforeClick = options.waitBeforeClick ?? 100
const waitAfterClick = options.waitAfterClick ?? 500
const logFn = options.logFn ?? (() => { })
try {
// Wait for element to be clickable
const elementResult = await waitForElementSmart(page, selector, {
state: 'visible',
initialTimeoutMs: options.maxWaitMs ? Math.floor(options.maxWaitMs * 0.4) : 2000,
extendedTimeoutMs: options.maxWaitMs ? Math.floor(options.maxWaitMs * 0.6) : 5000,
logFn
})
if (!elementResult.found || !elementResult.element) {
return { success: false, timeMs: Date.now() - startTime }
}
// Small delay for stability
if (waitBeforeClick > 0) {
await page.waitForTimeout(waitBeforeClick)
}
// Click the element
await elementResult.element.click()
logFn('✓ Clicked element')
// Wait for action to process
if (waitAfterClick > 0) {
await page.waitForTimeout(waitAfterClick)
}
// Verify element disappeared (optional)
if (options.verifyDisappeared) {
const disappeared = await page.locator(selector).isVisible()
.then(() => false)
.catch(() => true)
if (disappeared) {
logFn('✓ Element disappeared after click (expected)')
}
}
const elapsed = Date.now() - startTime
return { success: true, timeMs: elapsed }
} catch (error) {
const elapsed = Date.now() - startTime
const errorMsg = error instanceof Error ? error.message : String(error)
logFn(`✗ Click failed after ${elapsed}ms: ${errorMsg}`)
return { success: false, timeMs: elapsed }
}
}
/**
* Type text into input field with smart waiting
*/
export async function typeIntoFieldSmart(
page: Page,
selector: string,
text: string,
options: {
clearFirst?: boolean
delay?: number
maxWaitMs?: number
logFn?: (msg: string) => void
} = {}
): Promise<{ success: boolean; timeMs: number }> {
const startTime = Date.now()
const delay = options.delay ?? 20
const logFn = options.logFn ?? (() => { })
try {
// Wait for input field
const elementResult = await waitForElementSmart(page, selector, {
state: 'visible',
initialTimeoutMs: options.maxWaitMs ? Math.floor(options.maxWaitMs * 0.4) : 2000,
extendedTimeoutMs: options.maxWaitMs ? Math.floor(options.maxWaitMs * 0.6) : 5000,
logFn
})
if (!elementResult.found || !elementResult.element) {
return { success: false, timeMs: Date.now() - startTime }
}
// Clear field if requested
if (options.clearFirst) {
await elementResult.element.clear()
}
// Type text with delay
await elementResult.element.type(text, { delay })
logFn('✓ Typed into field')
const elapsed = Date.now() - startTime
return { success: true, timeMs: elapsed }
} catch (error) {
const elapsed = Date.now() - startTime
const errorMsg = error instanceof Error ? error.message : String(error)
logFn(`✗ Type failed after ${elapsed}ms: ${errorMsg}`)
return { success: false, timeMs: elapsed }
}
}

View File

@@ -9,23 +9,8 @@ export function getErrorMessage(error: unknown): string {
return error instanceof Error ? error.message : String(error) return error instanceof Error ? error.message : String(error)
} }
/** // DEAD CODE REMOVED: formatErrorMessage() was never used (only JSDoc examples existed)
* Format a standardized error message for logging // Use formatDetailedError() instead for error formatting with optional stack traces
* Ensures consistent error message formatting across all modules
*
* @param context - Context string (e.g., 'SEARCH-BING', 'LOGIN')
* @param error - Error object or unknown value
* @param prefix - Optional custom prefix (defaults to 'Error')
* @returns Formatted error message
*
* @example
* formatErrorMessage('SEARCH', err) // 'Error in SEARCH: Network timeout'
* formatErrorMessage('LOGIN', err, 'Failed') // 'Failed in LOGIN: Invalid credentials'
*/
export function formatErrorMessage(context: string, error: unknown, prefix: string = 'Error'): string {
const errorMsg = getErrorMessage(error)
return `${prefix} in ${context}: ${errorMsg}`
}
/** /**
* Utility class for common operations * Utility class for common operations