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
267 lines
7.0 KiB
JavaScript
267 lines
7.0 KiB
JavaScript
/**
|
|
* Error Recovery - Phase 8 Enterprise Pattern
|
|
*
|
|
* Handles crashes, recovers lost state, and provides
|
|
* resilience against errors and edge cases.
|
|
*/
|
|
|
|
import store from '../stores/app-store.js';
|
|
import auditLogger from './audit-logger.js';
|
|
import persistence from './workflow-persistence.js';
|
|
|
|
class ErrorRecovery {
|
|
constructor() {
|
|
this.errorHandlers = new Map();
|
|
this.recoveryPoints = [];
|
|
this.maxRecoveryPoints = 5;
|
|
this.setupGlobalErrorHandlers();
|
|
}
|
|
|
|
/**
|
|
* Setup global error handlers
|
|
*/
|
|
setupGlobalErrorHandlers() {
|
|
// Unhandled promise rejections
|
|
window.addEventListener('unhandledrejection', (event) => {
|
|
this.handleError(event.reason, 'unhandled_promise');
|
|
// Prevent default error handling
|
|
event.preventDefault();
|
|
});
|
|
|
|
// Global errors
|
|
window.addEventListener('error', (event) => {
|
|
this.handleError(event.error, 'global_error');
|
|
});
|
|
|
|
// Log before unload (potential crash)
|
|
window.addEventListener('beforeunload', () => {
|
|
persistence.saveSnapshot();
|
|
auditLogger.logAction('session_end', {
|
|
cleanShutdown: true
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Register error handler for specific error type
|
|
*/
|
|
registerHandler(errorType, handler) {
|
|
this.handlers.set(errorType, handler);
|
|
}
|
|
|
|
/**
|
|
* Create recovery point
|
|
*/
|
|
createRecoveryPoint(label = '') {
|
|
const point = {
|
|
id: `recovery-${Date.now()}`,
|
|
label,
|
|
timestamp: new Date().toISOString(),
|
|
snapshot: persistence.snapshot(),
|
|
logs: auditLogger.getLogs({ limit: 50 }),
|
|
state: store.get()
|
|
};
|
|
|
|
this.recoveryPoints.unshift(point);
|
|
if (this.recoveryPoints.length > this.maxRecoveryPoints) {
|
|
this.recoveryPoints.pop();
|
|
}
|
|
|
|
auditLogger.logAction('recovery_point_created', { label });
|
|
return point.id;
|
|
}
|
|
|
|
/**
|
|
* Get recovery points
|
|
*/
|
|
getRecoveryPoints() {
|
|
return this.recoveryPoints;
|
|
}
|
|
|
|
/**
|
|
* Recover from recovery point
|
|
*/
|
|
recover(pointId) {
|
|
const point = this.recoveryPoints.find(p => p.id === pointId);
|
|
if (!point) {
|
|
auditLogger.logWarning('Recovery point not found', { pointId });
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
// Restore workflow state
|
|
persistence.restoreSnapshot(point.snapshot.id);
|
|
auditLogger.logAction('recovered_from_point', {
|
|
pointId,
|
|
label: point.label
|
|
});
|
|
return true;
|
|
} catch (e) {
|
|
this.handleError(e, 'recovery_failed');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if app is in crashed state
|
|
*/
|
|
detectCrash() {
|
|
const lastSnapshot = persistence.getLatestSnapshot();
|
|
if (!lastSnapshot) return false;
|
|
|
|
const lastActivityTime = new Date(lastSnapshot.timestamp);
|
|
const now = new Date();
|
|
const timeSinceLastActivity = now - lastActivityTime;
|
|
|
|
// If no activity in last 5 minutes and session started, likely crashed
|
|
return timeSinceLastActivity > 5 * 60 * 1000;
|
|
}
|
|
|
|
/**
|
|
* Main error handler
|
|
*/
|
|
handleError(error, context = 'unknown') {
|
|
const errorId = auditLogger.logError(error, context);
|
|
|
|
// Create recovery point before handling
|
|
const recoveryId = this.createRecoveryPoint(`Error recovery: ${context}`);
|
|
|
|
// Categorize error
|
|
const category = this.categorizeError(error);
|
|
|
|
// Apply recovery strategy
|
|
const recovery = this.getRecoveryStrategy(category);
|
|
if (recovery) {
|
|
recovery.execute(error, store, auditLogger);
|
|
}
|
|
|
|
// Notify user
|
|
this.notifyUser(error, category, errorId);
|
|
|
|
return { errorId, recoveryId, category };
|
|
}
|
|
|
|
/**
|
|
* Categorize error
|
|
*/
|
|
categorizeError(error) {
|
|
if (error.message.includes('Network')) return 'network';
|
|
if (error.message.includes('timeout')) return 'timeout';
|
|
if (error.message.includes('Permission')) return 'permission';
|
|
if (error.message.includes('Authentication')) return 'auth';
|
|
if (error.message.includes('not found')) return 'notfound';
|
|
return 'unknown';
|
|
}
|
|
|
|
/**
|
|
* Get recovery strategy for error category
|
|
*/
|
|
getRecoveryStrategy(category) {
|
|
const strategies = {
|
|
network: {
|
|
execute: (error, store, logger) => {
|
|
store.notify('Network error - retrying...', 'warning');
|
|
logger.logWarning('Network error detected', { retrying: true });
|
|
},
|
|
retryable: true
|
|
},
|
|
timeout: {
|
|
execute: (error, store, logger) => {
|
|
store.notify('Request timeout - please try again', 'warning');
|
|
logger.logWarning('Request timeout', { timeout: true });
|
|
},
|
|
retryable: true
|
|
},
|
|
auth: {
|
|
execute: (error, store, logger) => {
|
|
store.notify('Authentication required - redirecting to login', 'error');
|
|
window.location.hash = '#/login';
|
|
},
|
|
retryable: false
|
|
},
|
|
permission: {
|
|
execute: (error, store, logger) => {
|
|
store.notify('Access denied', 'error');
|
|
logger.logWarning('Permission denied', { error: error.message });
|
|
},
|
|
retryable: false
|
|
},
|
|
notfound: {
|
|
execute: (error, store, logger) => {
|
|
store.notify('Resource not found', 'warning');
|
|
},
|
|
retryable: false
|
|
}
|
|
};
|
|
|
|
return strategies[category] || strategies.unknown;
|
|
}
|
|
|
|
/**
|
|
* Notify user of error
|
|
*/
|
|
notifyUser(error, category, errorId) {
|
|
const messages = {
|
|
network: 'Network connection error. Please check your internet.',
|
|
timeout: 'Request took too long. Please try again.',
|
|
permission: 'You do not have permission to perform this action.',
|
|
auth: 'Your session has expired. Please log in again.',
|
|
notfound: 'The resource you requested could not be found.',
|
|
unknown: 'An unexpected error occurred. Please try again.'
|
|
};
|
|
|
|
const message = messages[category] || messages.unknown;
|
|
store.notify(`${message} (Error: ${errorId})`, 'error', 5000);
|
|
}
|
|
|
|
/**
|
|
* Retry operation with exponential backoff
|
|
*/
|
|
async retry(operation, maxRetries = 3, initialDelay = 1000) {
|
|
for (let i = 0; i < maxRetries; i++) {
|
|
try {
|
|
return await operation();
|
|
} catch (e) {
|
|
if (i === maxRetries - 1) {
|
|
throw e;
|
|
}
|
|
const delay = initialDelay * Math.pow(2, i);
|
|
await new Promise(resolve => setTimeout(resolve, delay));
|
|
auditLogger.logWarning('Retrying operation', { attempt: i + 1, maxRetries });
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get crash report
|
|
*/
|
|
getCrashReport() {
|
|
const logs = auditLogger.getLogs();
|
|
const errorLogs = logs.filter(l => l.level === 'error');
|
|
|
|
return {
|
|
timestamp: new Date().toISOString(),
|
|
sessionId: auditLogger.sessionId,
|
|
totalErrors: errorLogs.length,
|
|
errors: errorLogs.slice(0, 10),
|
|
recoveryPoints: this.recoveryPoints,
|
|
lastSnapshot: persistence.getLatestSnapshot(),
|
|
statistics: auditLogger.getStats()
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Export crash report
|
|
*/
|
|
exportCrashReport() {
|
|
const report = this.getCrashReport();
|
|
return JSON.stringify(report, null, 2);
|
|
}
|
|
}
|
|
|
|
// Create and export singleton
|
|
const errorRecovery = new ErrorRecovery();
|
|
|
|
export { ErrorRecovery };
|
|
export default errorRecovery;
|