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:
266
admin-ui/js/core/error-recovery.js
Normal file
266
admin-ui/js/core/error-recovery.js
Normal file
@@ -0,0 +1,266 @@
|
||||
/**
|
||||
* 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;
|
||||
Reference in New Issue
Block a user