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
JavaScript
/**
* 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.');