// Global Console Logger - Vercel-inspired design // Include in any page: const STYLES = ` #logger-fab { position: fixed; bottom: 20px; right: 20px; width: 48px; height: 48px; border-radius: 50%; background: #000; border: 1px solid #333; color: #fff; font-size: 20px; cursor: pointer; z-index: 99998; display: flex; align-items: center; justify-content: center; box-shadow: 0 4px 14px rgba(0,0,0,0.25); transition: transform 0.15s ease, background 0.15s ease; } #logger-fab:hover { transform: scale(1.05); background: #111; } #logger-fab:active { transform: scale(0.95); } #logger-fab .badge { position: absolute; top: -4px; right: -4px; min-width: 18px; height: 18px; padding: 0 5px; border-radius: 9px; background: #f00; color: #fff; font-size: 11px; font-weight: 600; display: flex; align-items: center; justify-content: center; } #logger-modal { position: fixed; inset: 0; background: rgba(0,0,0,0.6); backdrop-filter: blur(4px); z-index: 99999; display: none; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; } #logger-modal.open { display: flex; align-items: center; justify-content: center; } #logger-container { width: 90%; max-width: 900px; height: 80%; max-height: 700px; background: #0a0a0a; border: 1px solid #333; border-radius: 12px; display: flex; flex-direction: column; overflow: hidden; box-shadow: 0 25px 50px -12px rgba(0,0,0,0.5); } #logger-header { display: flex; align-items: center; justify-content: space-between; padding: 16px 20px; border-bottom: 1px solid #333; background: #0a0a0a; } #logger-header h3 { margin: 0; font-size: 14px; font-weight: 500; color: #fafafa; display: flex; align-items: center; gap: 8px; } #logger-header h3::before { content: ''; width: 8px; height: 8px; border-radius: 50%; background: #22c55e; animation: pulse 2s infinite; } @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } } #logger-actions { display: flex; gap: 8px; } #logger-actions button { padding: 8px 12px; border-radius: 6px; border: 1px solid #333; background: transparent; color: #888; font-size: 13px; font-weight: 500; cursor: pointer; transition: all 0.15s ease; display: flex; align-items: center; gap: 6px; } #logger-actions button:hover { background: #1a1a1a; color: #fafafa; border-color: #444; } #logger-actions button.primary { background: #fafafa; color: #0a0a0a; border-color: #fafafa; } #logger-actions button.primary:hover { background: #e5e5e5; } #logger-actions button.danger { color: #f87171; border-color: #7f1d1d; } #logger-actions button.danger:hover { background: #7f1d1d; color: #fafafa; } #logger-filters { display: flex; gap: 4px; padding: 12px 20px; border-bottom: 1px solid #222; background: #0a0a0a; } .filter-btn { padding: 6px 12px; border-radius: 20px; border: none; background: transparent; color: #666; font-size: 12px; font-weight: 500; cursor: pointer; transition: all 0.15s ease; } .filter-btn:hover { color: #999; } .filter-btn.active { background: #1a1a1a; color: #fafafa; } .filter-btn .count { margin-left: 4px; padding: 2px 6px; border-radius: 10px; background: #222; font-size: 11px; } #logger-content { flex: 1; overflow-y: auto; padding: 0; background: #0a0a0a; } #logger-content::-webkit-scrollbar { width: 8px; } #logger-content::-webkit-scrollbar-track { background: #0a0a0a; } #logger-content::-webkit-scrollbar-thumb { background: #333; border-radius: 4px; } .log-entry { display: flex; padding: 10px 20px; border-bottom: 1px solid #1a1a1a; font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace; font-size: 12px; line-height: 1.5; transition: background 0.1s ease; } .log-entry:hover { background: #111; } .log-entry.error { background: rgba(239, 68, 68, 0.1); border-left: 3px solid #ef4444; } .log-entry.warn { background: rgba(245, 158, 11, 0.1); border-left: 3px solid #f59e0b; } .log-entry .time { color: #666; margin-right: 12px; white-space: nowrap; min-width: 85px; } .log-entry .level { margin-right: 12px; padding: 2px 6px; border-radius: 4px; font-size: 10px; font-weight: 600; text-transform: uppercase; min-width: 50px; text-align: center; } .log-entry .level.log { background: #1e3a5f; color: #60a5fa; } .log-entry .level.info { background: #1e3a5f; color: #60a5fa; } .log-entry .level.warn { background: #422006; color: #fbbf24; } .log-entry .level.error { background: #450a0a; color: #f87171; } .log-entry .level.debug { background: #1a1a2e; color: #a78bfa; } .log-entry .msg { color: #e5e5e5; word-break: break-all; white-space: pre-wrap; flex: 1; } #logger-footer { display: flex; align-items: center; justify-content: space-between; padding: 12px 20px; border-top: 1px solid #333; background: #0a0a0a; font-size: 12px; color: #666; } #logger-empty { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; color: #666; font-size: 14px; } #logger-empty svg { margin-bottom: 16px; opacity: 0.5; } `; export const Logger = { logs: [], maxLogs: 2000, errorCount: 0, filter: 'all', isInitialized: false, init() { if (this.isInitialized) return; this.isInitialized = true; // Inject styles const style = document.createElement('style'); style.textContent = STYLES; document.head.appendChild(style); // Create FAB and Modal this.createUI(); // Store original console methods this.originalConsole = { log: console.log.bind(console), warn: console.warn.bind(console), error: console.error.bind(console), info: console.info.bind(console), debug: console.debug.bind(console) }; // Intercept console methods const self = this; console.log = (...args) => { self.capture('log', args); self.originalConsole.log(...args); }; console.warn = (...args) => { self.capture('warn', args); self.originalConsole.warn(...args); }; console.error = (...args) => { self.capture('error', args); self.originalConsole.error(...args); }; console.info = (...args) => { self.capture('info', args); self.originalConsole.info(...args); }; console.debug = (...args) => { self.capture('debug', args); self.originalConsole.debug(...args); }; // Capture uncaught errors window.addEventListener('error', (e) => { self.capture('error', [`Uncaught: ${e.message} at ${e.filename}:${e.lineno}`]); }); window.addEventListener('unhandledrejection', (e) => { self.capture('error', [`Promise: ${e.reason?.message || e.reason}`]); }); this.capture('info', ['🚀 Logger initialized']); }, createUI() { // FAB Button const fab = document.createElement('button'); fab.id = 'logger-fab'; fab.innerHTML = `›_`; fab.onclick = () => this.toggle(); document.body.appendChild(fab); // Modal const modal = document.createElement('div'); modal.id = 'logger-modal'; modal.innerHTML = `

Console

`; modal.onclick = (e) => { if (e.target === modal) this.toggle(); }; document.body.appendChild(modal); }, capture(level, args) { const timestamp = new Date(); const message = args.map(arg => { if (arg === null) return 'null'; if (arg === undefined) return 'undefined'; if (typeof arg === 'object') { try { return JSON.stringify(arg, null, 2); } catch { return String(arg); } } return String(arg); }).join(' '); this.logs.push({ timestamp, level, message }); if (level === 'error') { this.errorCount++; this.updateBadge(); } if (this.logs.length > this.maxLogs) { this.logs = this.logs.slice(-this.maxLogs); } // Write to Android if available if (window.AndroidNative?.writeLog) { try { window.AndroidNative.writeLog(level.toUpperCase(), message); } catch {} } }, updateBadge() { const badge = document.querySelector('#logger-fab .badge'); if (badge) { badge.style.display = this.errorCount > 0 ? 'flex' : 'none'; badge.textContent = this.errorCount > 99 ? '99+' : this.errorCount; } }, toggle() { const modal = document.getElementById('logger-modal'); if (modal.classList.contains('open')) { modal.classList.remove('open'); } else { modal.classList.add('open'); this.render(); } }, setFilter(filter) { this.filter = filter; document.querySelectorAll('.filter-btn').forEach(btn => { btn.classList.toggle('active', btn.dataset.filter === filter); }); this.render(); }, render() { const content = document.getElementById('logger-content'); if (!content) return; const filtered = this.filter === 'all' ? this.logs : this.logs.filter(l => l.level === this.filter); // Update counts const counts = { all: this.logs.length, error: 0, warn: 0, log: 0, info: 0, debug: 0 }; this.logs.forEach(l => counts[l.level] = (counts[l.level] || 0) + 1); counts.log += counts.info + counts.debug; ['all', 'error', 'warn', 'log'].forEach(k => { const el = document.getElementById(`count-${k}`); if (el) el.textContent = counts[k]; }); if (filtered.length === 0) { content.innerHTML = `
No logs yet
`; return; } content.innerHTML = filtered.map(log => { const time = log.timestamp.toLocaleTimeString('en-US', { hour12: false }); const levelClass = log.level; const entryClass = ['error', 'warn'].includes(log.level) ? log.level : ''; return `
${time} ${log.level} ${this.escapeHtml(log.message)}
`; }).join(''); content.scrollTop = content.scrollHeight; // Update footer const stats = document.getElementById('logger-stats'); if (stats) stats.textContent = `${filtered.length} entries`; const path = document.getElementById('logger-path'); if (path && window.AndroidNative?.getLogFilePath) { try { path.textContent = window.AndroidNative.getLogFilePath(); } catch {} } }, escapeHtml(str) { return str.replace(/&/g, '&').replace(//g, '>'); }, getLogsText() { return this.logs.map(l => { const ts = l.timestamp.toISOString(); return `[${ts}] [${l.level.toUpperCase()}] ${l.message}`; }).join('\n'); }, export() { const content = this.getLogsText(); const filename = `logs_${new Date().toISOString().replace(/[:.]/g, '-')}.txt`; if (window.AndroidNative?.saveBlob) { const blob = new Blob([content], { type: 'text/plain' }); const reader = new FileReader(); reader.onloadend = () => { window.AndroidNative.saveBlob(reader.result.split(',')[1], filename, 'text/plain'); }; reader.readAsDataURL(blob); return; } const blob = new Blob([content], { type: 'text/plain' }); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = filename; a.click(); }, async copyToClipboard() { try { await navigator.clipboard.writeText(this.getLogsText()); this.capture('info', ['📋 Logs copied to clipboard']); this.render(); } catch { prompt('Copy logs:', this.getLogsText().slice(0, 5000)); } }, clear() { this.logs = []; this.errorCount = 0; this.updateBadge(); this.capture('info', ['🗑️ Logs cleared']); this.render(); } }; // Auto-initialize when DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => Logger.init()); } else { Logger.init(); } // Global access window.Logger = Logger;