UNPKG

shell-mirror

Version:

Access your Mac shell from any device securely. Perfect for mobile coding with Claude Code CLI, Gemini CLI, and any shell tool.

284 lines (243 loc) 9.42 kB
/** * Client-Side Logger for Shell Mirror * Captures browser console logs and provides debugging capabilities */ class ClientLogger { constructor() { this.logs = []; this.maxLogs = 500; this.isEnabled = true; this.logToServer = true; // Initialize logging this.setupConsoleInterception(); this.setupErrorHandling(); // Store original console methods this.originalConsole = { log: console.log.bind(console), error: console.error.bind(console), warn: console.warn.bind(console), info: console.info.bind(console), debug: console.debug.bind(console) }; this.log('INFO', 'Client logger initialized'); } setupConsoleInterception() { const self = this; ['log', 'error', 'warn', 'info', 'debug'].forEach(level => { const originalMethod = console[level]; console[level] = function(...args) { // Call original method originalMethod.apply(console, args); // Log to our system if (self.isEnabled) { self.log(level.toUpperCase(), args.join(' '), { source: 'console', stackTrace: self.getStackTrace() }); } }; }); } setupErrorHandling() { const self = this; window.addEventListener('error', function(event) { self.log('ERROR', `Uncaught error: ${event.message}`, { source: 'window.error', filename: event.filename, lineno: event.lineno, colno: event.colno, error: event.error ? event.error.toString() : 'Unknown' }); }); window.addEventListener('unhandledrejection', function(event) { self.log('ERROR', `Unhandled promise rejection: ${event.reason}`, { source: 'unhandledrejection', reason: event.reason }); }); } log(level, message, data = null) { if (!this.isEnabled) return; const timestamp = new Date().toISOString(); const logEntry = { timestamp, level, message, data, url: window.location.href, userAgent: navigator.userAgent, sessionId: this.getSessionId() }; // Add to local logs this.logs.push(logEntry); // Maintain max logs limit if (this.logs.length > this.maxLogs) { this.logs = this.logs.slice(-this.maxLogs); } // Store in localStorage for persistence this.persistLogs(); // Send to server if enabled if (this.logToServer && ['ERROR', 'CRITICAL'].includes(level)) { this.sendLogToServer(logEntry); } } getStackTrace() { try { throw new Error(); } catch (e) { return e.stack; } } getSessionId() { if (!window.sessionId) { window.sessionId = 'client-' + Math.random().toString(36).substr(2, 9); } return window.sessionId; } persistLogs() { try { const recentLogs = this.logs.slice(-100); // Keep last 100 logs localStorage.setItem('shell-mirror-client-logs', JSON.stringify(recentLogs)); } catch (e) { // localStorage might be full or unavailable } } loadPersistedLogs() { try { const stored = localStorage.getItem('shell-mirror-client-logs'); if (stored) { const logs = JSON.parse(stored); this.logs = [...logs, ...this.logs]; } } catch (e) { // Invalid JSON or other error } } async sendLogToServer(logEntry) { try { await fetch('/php-backend/api/client-log.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify(logEntry) }); } catch (e) { // Silently fail - don't create infinite loops } } getLogs(filter = {}) { let filteredLogs = [...this.logs]; if (filter.level) { filteredLogs = filteredLogs.filter(log => log.level.toLowerCase() === filter.level.toLowerCase() ); } if (filter.since) { const since = new Date(filter.since); filteredLogs = filteredLogs.filter(log => new Date(log.timestamp) >= since ); } if (filter.limit) { filteredLogs = filteredLogs.slice(-filter.limit); } return filteredLogs; } getLogsAsText() { return this.logs.map(log => { const data = log.data ? ` | ${JSON.stringify(log.data)}` : ''; return `[${log.timestamp}] [${log.level}] ${log.message}${data}`; }).join('\n'); } downloadLogs() { const logsText = this.getLogsAsText(); const blob = new Blob([logsText], { type: 'text/plain' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `shell-mirror-client-logs-${new Date().toISOString()}.txt`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } clearLogs() { this.logs = []; localStorage.removeItem('shell-mirror-client-logs'); this.log('INFO', 'Client logs cleared'); } enable() { this.isEnabled = true; this.log('INFO', 'Client logging enabled'); } disable() { this.isEnabled = false; } showDebugPanel() { const panel = document.createElement('div'); panel.id = 'client-debug-panel'; panel.style.cssText = ` position: fixed; top: 10px; right: 10px; width: 400px; height: 300px; background: #1e1e1e; color: #d4d4d4; border: 1px solid #333; font-family: monospace; font-size: 12px; z-index: 10000; padding: 10px; overflow-y: auto; border-radius: 4px; `; const recentLogs = this.getLogs({ limit: 20 }); panel.innerHTML = ` <div style="margin-bottom: 10px;"> <strong>Client Debug Logs</strong> <button onclick="clientLogger.clearLogs()" style="float: right; background: #007acc; color: white; border: none; padding: 2px 8px; border-radius: 2px; cursor: pointer;">Clear</button> <button onclick="clientLogger.downloadLogs()" style="float: right; background: #4ec9b0; color: white; border: none; padding: 2px 8px; border-radius: 2px; cursor: pointer; margin-right: 5px;">Download</button> <button onclick="document.getElementById('client-debug-panel').remove()" style="float: right; background: #f14c4c; color: white; border: none; padding: 2px 8px; border-radius: 2px; cursor: pointer; margin-right: 5px;">Close</button> </div> <div style="border-top: 1px solid #333; padding-top: 10px;"> ${recentLogs.reverse().map(log => { const levelColor = { 'DEBUG': '#007acc', 'INFO': '#4ec9b0', 'WARN': '#ffcc02', 'ERROR': '#f14c4c', 'CRITICAL': '#ff6b6b' }; return `<div style="margin-bottom: 5px; border-left: 3px solid ${levelColor[log.level] || '#333'}; padding-left: 5px;"> <span style="color: #808080;">${log.timestamp.substr(11, 8)}</span> <span style="color: ${levelColor[log.level] || '#d4d4d4'};">[${log.level}]</span> ${log.message} </div>`; }).join('')} </div> `; // Remove existing panel const existing = document.getElementById('client-debug-panel'); if (existing) existing.remove(); document.body.appendChild(panel); } } // Global instance window.clientLogger = new ClientLogger(); // Load persisted logs window.clientLogger.loadPersistedLogs(); // Expose debug functions globally window.showClientLogs = () => window.clientLogger.showDebugPanel(); window.downloadClientLogs = () => window.clientLogger.downloadLogs(); window.clearClientLogs = () => window.clientLogger.clearLogs(); // Add keyboard shortcut for debug panel (Ctrl+Shift+L) document.addEventListener('keydown', function(e) { if (e.ctrlKey && e.shiftKey && e.key === 'L') { e.preventDefault(); window.clientLogger.showDebugPanel(); } }); console.log('🔍 Client Logger loaded. Press Ctrl+Shift+L to show debug panel.');