Initial commit: Clean DSS implementation
Migrated from design-system-swarm with fresh git history.
Old project history preserved in /home/overbits/apps/design-system-swarm
Core components:
- MCP Server (Python FastAPI with mcp 1.23.1)
- Claude Plugin (agents, commands, skills, strategies, hooks, core)
- DSS Backend (dss-mvp1 - token translation, Figma sync)
- Admin UI (Node.js/React)
- Server (Node.js/Express)
- Storybook integration (dss-mvp1/.storybook)
Self-contained configuration:
- All paths relative or use DSS_BASE_PATH=/home/overbits/dss
- PYTHONPATH configured for dss-mvp1 and dss-claude-plugin
- .env file with all configuration
- Claude plugin uses ${CLAUDE_PLUGIN_ROOT} for portability
Migration completed: $(date)
🤖 Clean migration with full functionality preserved
This commit is contained in:
163
admin-ui/js/utils/console-forwarder.js
Normal file
163
admin-ui/js/utils/console-forwarder.js
Normal file
@@ -0,0 +1,163 @@
|
||||
/**
|
||||
* @file console-forwarder.js
|
||||
* @description Intercepts browser console logs and forwards them to the server.
|
||||
* Handles circular references, batches requests, and prevents infinite loops.
|
||||
*/
|
||||
|
||||
(function () {
|
||||
// CONFIGURATION
|
||||
const LOG_ENDPOINT = '/api/logs/browser';
|
||||
const BATCH_SIZE = 50;
|
||||
const FLUSH_INTERVAL_MS = 2000;
|
||||
const MAX_RETRIES = 3;
|
||||
|
||||
// STATE
|
||||
let logQueue = [];
|
||||
let flushTimer = null;
|
||||
let isSending = false;
|
||||
let retryCount = 0;
|
||||
|
||||
// CIRCULAR REFERENCE SAFE SERIALIZER
|
||||
const safeStringify = (obj) => {
|
||||
const seen = new WeakSet();
|
||||
return JSON.stringify(obj, (key, value) => {
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
if (seen.has(value)) {
|
||||
return '[Circular]';
|
||||
}
|
||||
seen.add(value);
|
||||
}
|
||||
// Handle Error objects explicitly as they don't stringify well by default
|
||||
if (value instanceof Error) {
|
||||
return {
|
||||
message: value.message,
|
||||
stack: value.stack,
|
||||
name: value.name
|
||||
};
|
||||
}
|
||||
return value;
|
||||
});
|
||||
};
|
||||
|
||||
// LOG QUEUE MANAGER
|
||||
const flushQueue = async () => {
|
||||
if (logQueue.length === 0 || isSending) return;
|
||||
|
||||
const batch = [...logQueue];
|
||||
logQueue = []; // Clear queue immediately to prevent duplicates
|
||||
isSending = true;
|
||||
|
||||
try {
|
||||
const response = await fetch(LOG_ENDPOINT, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ logs: batch }),
|
||||
// critical: prevent this fetch from triggering global error handlers
|
||||
// if it fails, to avoid infinite loops
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Server responded with ${response.status}`);
|
||||
}
|
||||
retryCount = 0;
|
||||
} catch (err) {
|
||||
// DO NOT log errors here to prevent infinite loops
|
||||
// If network fails, put specific logs back or drop them to prevent memory leaks
|
||||
if (retryCount < MAX_RETRIES) {
|
||||
retryCount++;
|
||||
// Re-queue logs at the front
|
||||
logQueue = [...batch, ...logQueue];
|
||||
}
|
||||
} finally {
|
||||
isSending = false;
|
||||
}
|
||||
};
|
||||
|
||||
const scheduleFlush = () => {
|
||||
if (flushTimer) return;
|
||||
flushTimer = setTimeout(() => {
|
||||
flushTimer = null;
|
||||
flushQueue();
|
||||
}, FLUSH_INTERVAL_MS);
|
||||
};
|
||||
|
||||
const pushLog = (level, args) => {
|
||||
// timestamp
|
||||
const timestamp = new Date().toISOString();
|
||||
|
||||
// safe serialization of arguments
|
||||
const serializedArgs = args.map(arg => {
|
||||
try {
|
||||
if (typeof arg === 'string') return arg;
|
||||
return JSON.parse(safeStringify(arg));
|
||||
} catch (e) {
|
||||
return '[Unserializable]';
|
||||
}
|
||||
});
|
||||
|
||||
logQueue.push({
|
||||
level,
|
||||
timestamp,
|
||||
message: serializedArgs.join(' '), // Flatten for simple viewing
|
||||
data: serializedArgs // Keep structured for deep inspection if backend supports it
|
||||
});
|
||||
|
||||
if (logQueue.length >= BATCH_SIZE) {
|
||||
if (flushTimer) clearTimeout(flushTimer);
|
||||
flushTimer = null;
|
||||
flushQueue();
|
||||
} else {
|
||||
scheduleFlush();
|
||||
}
|
||||
};
|
||||
|
||||
// INTERCEPTORS
|
||||
const originalConsole = {
|
||||
log: console.log,
|
||||
info: console.info,
|
||||
warn: console.warn,
|
||||
error: console.error,
|
||||
debug: console.debug,
|
||||
};
|
||||
|
||||
const methods = ['log', 'info', 'warn', 'error', 'debug'];
|
||||
|
||||
methods.forEach((method) => {
|
||||
console[method] = (...args) => {
|
||||
// 1. Call original method so developer tools still work
|
||||
originalConsole[method].apply(console, args);
|
||||
|
||||
// 2. Push to queue
|
||||
try {
|
||||
pushLog(method, args);
|
||||
} catch (e) {
|
||||
// Silent fail - do not use console.error here!
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// GLOBAL ERROR HANDLERS
|
||||
window.addEventListener('error', (event) => {
|
||||
pushLog('error', [`Uncaught Exception: ${event.message}`, event.filename, event.lineno, event.colno, event.error]);
|
||||
});
|
||||
|
||||
window.addEventListener('unhandledrejection', (event) => {
|
||||
pushLog('error', ['Unhandled Promise Rejection:', event.reason]);
|
||||
});
|
||||
|
||||
// PAGE UNLOAD FLUSH
|
||||
// Use sendBeacon for reliable "last gasp" logging
|
||||
window.addEventListener('visibilitychange', () => {
|
||||
if (document.visibilityState === 'hidden' && logQueue.length > 0) {
|
||||
const batch = safeStringify({ logs: logQueue });
|
||||
// sendBeacon is more reliable on unload than fetch
|
||||
if (navigator.sendBeacon) {
|
||||
const blob = new Blob([batch], { type: 'application/json' });
|
||||
navigator.sendBeacon(LOG_ENDPOINT, blob);
|
||||
logQueue = [];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.info('[Console Forwarder] Initialized. Monitoring active.');
|
||||
})();
|
||||
Reference in New Issue
Block a user