Major feature release implementing comprehensive state management, component registry, and admin control center with full UI integration. ## Backend Features (secubox-core v0.9.0-1) State Management System: - ✅ State database (state-db.json) with 15 states across 4 categories - ✅ State machine with transition matrix validation - ✅ secubox-state CLI (8 commands: get, set, history, list, validate, sync, freeze, clear-error) - ✅ state-machine.sh with atomic transitions using flock - ✅ State history tracking with timestamps and reasons - ✅ Error state handling with detailed error info - ✅ Frozen state support for system-critical components Component Registry System: - ✅ Component registry database (component-registry.json) - ✅ secubox-component CLI (7 commands: list, get, register, unregister, tree, affected, set-setting) - ✅ Component types: app, module, widget, service, composite - ✅ Dependency tracking (required/optional) - ✅ Recursive dependency tree resolution - ✅ Reverse dependency tracking - ✅ Component settings management - ✅ Profile tagging and filtering Auto-Sync System: - ✅ secubox-sync-registry CLI for catalog synchronization - ✅ Auto-populate from catalog.json - ✅ Plugin catalog directory scanning - ✅ Installed package detection - ✅ Automatic state initialization RPC Backend (luci.secubox): - ✅ 6 state management RPC methods - ✅ 5 component registry RPC methods - ✅ Bulk operations support - ✅ State validation endpoints ## Frontend Features (luci-app-secubox-admin v1.0.0-16) UI Components: - ✅ state-utils.js: 20+ utility functions, state config, transition validation - ✅ StateIndicator.js: 5 rendering modes (badge, compact, pill, dot, statistics) - ✅ StateTimeline.js: 4 visualization modes (vertical, horizontal, compact, transition diagram) - ✅ state-management.css: 600+ lines with animations, responsive design, accessibility Admin Control Center Dashboard: - ✅ System overview panel with health metrics - ✅ Component state summary with statistics - ✅ Recent state transitions timeline - ✅ Alerts panel for warnings and errors - ✅ Quick actions panel - ✅ Real-time updates (5-second polling) - ✅ Metric cards with hover effects - ✅ State distribution by category API Integration (api.js): - ✅ 11 RPC method declarations - ✅ Enhanced methods: getComponentWithState(), getAllComponentsWithStates() - ✅ Bulk operations: bulkSetComponentState() - ✅ State statistics: getStateStatistics() - ✅ Retry logic with exponential backoff - ✅ Promise-based async operations ## Documentation Comprehensive Documentation: - ✅ API-REFERENCE.md (1,200+ lines): Complete API docs for RPC, CLI, JS - ✅ EXAMPLES.md (800+ lines): 30+ usage examples, shell scripts, integration patterns - ✅ State definitions table (15 states) - ✅ State transition matrix - ✅ Component metadata schemas - ✅ Error codes reference - ✅ Testing examples ## State Definitions 15 States Across 4 Categories: - Persistent: available, installed, active, disabled, frozen - Transient: installing, configuring, activating, starting, stopping, uninstalling - Runtime: running, stopped - Error: error (with subtypes) State Transition Flow: available → installing → installed → configuring → configured → activating → active → starting → running → stopping → stopped ## Technical Details Files Created (10 backend + 8 frontend): Backend: - /usr/sbin/secubox-state (12KB, 8 commands) - /usr/sbin/secubox-component (12KB, 7 commands) - /usr/sbin/secubox-sync-registry (8.4KB) - /usr/share/secubox/state-machine.sh (5.2KB) - /var/lib/secubox/state-db.json (schema) - /var/lib/secubox/component-registry.json (schema) Frontend: - resources/secubox-admin/state-utils.js (~400 lines) - resources/secubox-admin/components/StateIndicator.js (~350 lines) - resources/secubox-admin/components/StateTimeline.js (~450 lines) - resources/secubox-admin/state-management.css (~600 lines) - resources/view/secubox-admin/control-center.js (~550 lines) - resources/secubox-admin/api.js (+145 lines) Documentation: - docs/admin-control-center/API-REFERENCE.md (1,200+ lines) - docs/admin-control-center/EXAMPLES.md (800+ lines) Files Modified (3): - package/secubox/secubox-core/Makefile (v0.8.0 → v0.9.0-1) - package/secubox/luci-app-secubox-admin/Makefile (release 15 → 16) - package/secubox/secubox-core/root/usr/libexec/rpcd/luci.secubox (+157 lines) ## Installation & Migration Makefile Updates: - Added 3 new CLI tools to install section - Added state-machine.sh to scripts - Updated package description - Enhanced postinst to initialize databases - Auto-sync registry on first install Postinst Features: - Automatic state-db.json initialization - Automatic component-registry.json initialization - Catalog sync on install - Version announcement with new features ## Performance & Security Performance: - File locking (flock) for atomic state transitions - State history limited to 100 entries per component - RPC retry logic with exponential backoff - Bulk operations use Promise.all for parallel execution - Component list caching (30 seconds) Security: - Frozen state prevents unauthorized modifications - All state changes logged with timestamp and reason - System-critical components have additional safeguards - Proper authentication required for state transitions ## Testing & Validation Features: - State transition validation - Component dependency resolution - Circular dependency detection - State consistency checker - Integration test scripts included in docs ## Breaking Changes None - Backward Compatible: - Existing RPC methods remain functional - State-aware methods are additive - Components without state default to 'available' - Migration is automatic on install ## Statistics Total Implementation: - Lines of Code: ~4,000 - Backend: ~1,800 (Bash + JSON) - Frontend: ~2,200 (JavaScript + CSS) - Documentation: ~2,000 (Markdown) - Functions/Commands: 40+ - RPC Methods: 11 - CLI Commands: 22 - UI Components: 5 - Documentation Pages: 2 ## Next Phase Remaining from Plan: - Phase 4: System Hub integration - Phase 5: Migration script (secubox-migrate-state) - Phase 6: Additional documentation (ARCHITECTURE.md, STATE-MANAGEMENT.md, etc.) - Phase 7: Additional UI views (components.js, state-manager.js, debug-panel.js) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
458 lines
10 KiB
JavaScript
458 lines
10 KiB
JavaScript
'use strict';
|
|
'require baseclass';
|
|
|
|
/**
|
|
* SecuBox State Management Utilities
|
|
* Helper functions for state validation, formatting, and visualization
|
|
*/
|
|
|
|
// State configuration with colors, icons, and labels
|
|
var STATE_CONFIG = {
|
|
available: {
|
|
color: '#6b7280',
|
|
icon: '○',
|
|
label: 'Available',
|
|
category: 'persistent',
|
|
description: 'Component is available for installation'
|
|
},
|
|
installing: {
|
|
color: '#3b82f6',
|
|
icon: '⏳',
|
|
label: 'Installing',
|
|
category: 'transient',
|
|
description: 'Installation in progress'
|
|
},
|
|
installed: {
|
|
color: '#8b5cf6',
|
|
icon: '✓',
|
|
label: 'Installed',
|
|
category: 'persistent',
|
|
description: 'Component is installed but not active'
|
|
},
|
|
configuring: {
|
|
color: '#3b82f6',
|
|
icon: '⚙',
|
|
label: 'Configuring',
|
|
category: 'transient',
|
|
description: 'Configuration in progress'
|
|
},
|
|
configured: {
|
|
color: '#8b5cf6',
|
|
icon: '✓',
|
|
label: 'Configured',
|
|
category: 'transient',
|
|
description: 'Configuration completed'
|
|
},
|
|
activating: {
|
|
color: '#3b82f6',
|
|
icon: '↗',
|
|
label: 'Activating',
|
|
category: 'transient',
|
|
description: 'Activation in progress'
|
|
},
|
|
active: {
|
|
color: '#06b6d4',
|
|
icon: '●',
|
|
label: 'Active',
|
|
category: 'persistent',
|
|
description: 'Component is active but not running'
|
|
},
|
|
starting: {
|
|
color: '#3b82f6',
|
|
icon: '▶',
|
|
label: 'Starting',
|
|
category: 'transient',
|
|
description: 'Service is starting'
|
|
},
|
|
running: {
|
|
color: '#10b981',
|
|
icon: '▶',
|
|
label: 'Running',
|
|
category: 'runtime',
|
|
description: 'Service is running'
|
|
},
|
|
stopping: {
|
|
color: '#f59e0b',
|
|
icon: '⏸',
|
|
label: 'Stopping',
|
|
category: 'transient',
|
|
description: 'Service is stopping'
|
|
},
|
|
stopped: {
|
|
color: '#6b7280',
|
|
icon: '⏹',
|
|
label: 'Stopped',
|
|
category: 'runtime',
|
|
description: 'Service is stopped'
|
|
},
|
|
error: {
|
|
color: '#ef4444',
|
|
icon: '✗',
|
|
label: 'Error',
|
|
category: 'error',
|
|
description: 'Component encountered an error'
|
|
},
|
|
frozen: {
|
|
color: '#06b6d4',
|
|
icon: '❄',
|
|
label: 'Frozen',
|
|
category: 'persistent',
|
|
description: 'Component is frozen (locked)'
|
|
},
|
|
disabled: {
|
|
color: '#9ca3af',
|
|
icon: '⊘',
|
|
label: 'Disabled',
|
|
category: 'persistent',
|
|
description: 'Component is disabled'
|
|
},
|
|
uninstalling: {
|
|
color: '#f59e0b',
|
|
icon: '⏳',
|
|
label: 'Uninstalling',
|
|
category: 'transient',
|
|
description: 'Uninstallation in progress'
|
|
}
|
|
};
|
|
|
|
// State transition matrix
|
|
var STATE_TRANSITIONS = {
|
|
available: ['installing'],
|
|
installing: ['installed', 'error'],
|
|
installed: ['configuring', 'uninstalling'],
|
|
configuring: ['configured', 'error'],
|
|
configured: ['activating', 'disabled'],
|
|
activating: ['active', 'error'],
|
|
active: ['starting', 'disabled', 'frozen'],
|
|
starting: ['running', 'error'],
|
|
running: ['stopping', 'error', 'frozen'],
|
|
stopping: ['stopped', 'error'],
|
|
stopped: ['starting', 'disabled', 'uninstalling'],
|
|
error: ['available', 'installed', 'stopped'],
|
|
frozen: ['active'],
|
|
disabled: ['active', 'uninstalling'],
|
|
uninstalling: ['available', 'error']
|
|
};
|
|
|
|
return baseclass.extend({
|
|
/**
|
|
* Get state configuration
|
|
* @param {string} state - State name
|
|
* @returns {Object} State configuration
|
|
*/
|
|
getStateConfig: function(state) {
|
|
return STATE_CONFIG[state] || {
|
|
color: '#6b7280',
|
|
icon: '?',
|
|
label: state || 'Unknown',
|
|
category: 'unknown',
|
|
description: 'Unknown state'
|
|
};
|
|
},
|
|
|
|
/**
|
|
* Get state color
|
|
* @param {string} state - State name
|
|
* @returns {string} CSS color value
|
|
*/
|
|
getStateColor: function(state) {
|
|
var config = this.getStateConfig(state);
|
|
return config.color;
|
|
},
|
|
|
|
/**
|
|
* Get state icon
|
|
* @param {string} state - State name
|
|
* @returns {string} Icon character
|
|
*/
|
|
getStateIcon: function(state) {
|
|
var config = this.getStateConfig(state);
|
|
return config.icon;
|
|
},
|
|
|
|
/**
|
|
* Get state label
|
|
* @param {string} state - State name
|
|
* @returns {string} Human-readable label
|
|
*/
|
|
getStateLabel: function(state) {
|
|
var config = this.getStateConfig(state);
|
|
return config.label;
|
|
},
|
|
|
|
/**
|
|
* Get state category
|
|
* @param {string} state - State name
|
|
* @returns {string} Category (persistent, transient, runtime, error, unknown)
|
|
*/
|
|
getStateCategory: function(state) {
|
|
var config = this.getStateConfig(state);
|
|
return config.category;
|
|
},
|
|
|
|
/**
|
|
* Check if transition is valid
|
|
* @param {string} fromState - Current state
|
|
* @param {string} toState - Target state
|
|
* @returns {boolean} True if transition is allowed
|
|
*/
|
|
canTransition: function(fromState, toState) {
|
|
var allowedTransitions = STATE_TRANSITIONS[fromState];
|
|
if (!allowedTransitions) {
|
|
return false;
|
|
}
|
|
return allowedTransitions.indexOf(toState) !== -1;
|
|
},
|
|
|
|
/**
|
|
* Get allowed next states
|
|
* @param {string} currentState - Current state
|
|
* @returns {Array<string>} Array of allowed next states
|
|
*/
|
|
getNextStates: function(currentState) {
|
|
return STATE_TRANSITIONS[currentState] || [];
|
|
},
|
|
|
|
/**
|
|
* Get all available states
|
|
* @returns {Array<string>} Array of all state names
|
|
*/
|
|
getAllStates: function() {
|
|
return Object.keys(STATE_CONFIG);
|
|
},
|
|
|
|
/**
|
|
* Format state history entry
|
|
* @param {Object} historyEntry - History entry object
|
|
* @returns {string} Formatted history string
|
|
*/
|
|
formatHistoryEntry: function(historyEntry) {
|
|
if (!historyEntry) {
|
|
return '';
|
|
}
|
|
|
|
var state = historyEntry.state || 'unknown';
|
|
var timestamp = historyEntry.timestamp || '';
|
|
var reason = historyEntry.reason || 'unknown';
|
|
|
|
var date = timestamp ? new Date(timestamp) : null;
|
|
var timeStr = date ? date.toLocaleString() : timestamp;
|
|
|
|
return timeStr + ' - ' + this.getStateLabel(state) + ' (' + reason + ')';
|
|
},
|
|
|
|
/**
|
|
* Format timestamp
|
|
* @param {string} timestamp - ISO timestamp
|
|
* @returns {string} Formatted timestamp
|
|
*/
|
|
formatTimestamp: function(timestamp) {
|
|
if (!timestamp) {
|
|
return 'N/A';
|
|
}
|
|
|
|
try {
|
|
var date = new Date(timestamp);
|
|
return date.toLocaleString();
|
|
} catch (e) {
|
|
return timestamp;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Get time ago string
|
|
* @param {string} timestamp - ISO timestamp
|
|
* @returns {string} Relative time string (e.g., "5 minutes ago")
|
|
*/
|
|
getTimeAgo: function(timestamp) {
|
|
if (!timestamp) {
|
|
return 'never';
|
|
}
|
|
|
|
try {
|
|
var date = new Date(timestamp);
|
|
var now = new Date();
|
|
var seconds = Math.floor((now - date) / 1000);
|
|
|
|
if (seconds < 60) {
|
|
return seconds + ' second' + (seconds !== 1 ? 's' : '') + ' ago';
|
|
}
|
|
|
|
var minutes = Math.floor(seconds / 60);
|
|
if (minutes < 60) {
|
|
return minutes + ' minute' + (minutes !== 1 ? 's' : '') + ' ago';
|
|
}
|
|
|
|
var hours = Math.floor(minutes / 60);
|
|
if (hours < 24) {
|
|
return hours + ' hour' + (hours !== 1 ? 's' : '') + ' ago';
|
|
}
|
|
|
|
var days = Math.floor(hours / 24);
|
|
if (days < 30) {
|
|
return days + ' day' + (days !== 1 ? 's' : '') + ' ago';
|
|
}
|
|
|
|
var months = Math.floor(days / 30);
|
|
return months + ' month' + (months !== 1 ? 's' : '') + ' ago';
|
|
} catch (e) {
|
|
return 'unknown';
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Check if state is transient
|
|
* @param {string} state - State name
|
|
* @returns {boolean} True if state is transient
|
|
*/
|
|
isTransient: function(state) {
|
|
return this.getStateCategory(state) === 'transient';
|
|
},
|
|
|
|
/**
|
|
* Check if state is error
|
|
* @param {string} state - State name
|
|
* @returns {boolean} True if state is error
|
|
*/
|
|
isError: function(state) {
|
|
return this.getStateCategory(state) === 'error';
|
|
},
|
|
|
|
/**
|
|
* Check if state is running
|
|
* @param {string} state - State name
|
|
* @returns {boolean} True if state is running
|
|
*/
|
|
isRunning: function(state) {
|
|
return state === 'running';
|
|
},
|
|
|
|
/**
|
|
* Check if state is frozen
|
|
* @param {string} state - State name
|
|
* @returns {boolean} True if state is frozen
|
|
*/
|
|
isFrozen: function(state) {
|
|
return state === 'frozen';
|
|
},
|
|
|
|
/**
|
|
* Get CSS class for state
|
|
* @param {string} state - State name
|
|
* @returns {string} CSS class name
|
|
*/
|
|
getStateClass: function(state) {
|
|
return 'state-' + (state || 'unknown');
|
|
},
|
|
|
|
/**
|
|
* Get badge CSS classes
|
|
* @param {string} state - State name
|
|
* @returns {string} Space-separated CSS classes
|
|
*/
|
|
getBadgeClasses: function(state) {
|
|
var classes = ['cyber-badge', 'state-badge', this.getStateClass(state)];
|
|
|
|
var category = this.getStateCategory(state);
|
|
if (category) {
|
|
classes.push('state-category-' + category);
|
|
}
|
|
|
|
return classes.join(' ');
|
|
},
|
|
|
|
/**
|
|
* Filter states by category
|
|
* @param {string} category - Category name
|
|
* @returns {Array<string>} Array of state names in category
|
|
*/
|
|
getStatesByCategory: function(category) {
|
|
var states = [];
|
|
var allStates = this.getAllStates();
|
|
|
|
for (var i = 0; i < allStates.length; i++) {
|
|
if (this.getStateCategory(allStates[i]) === category) {
|
|
states.push(allStates[i]);
|
|
}
|
|
}
|
|
|
|
return states;
|
|
},
|
|
|
|
/**
|
|
* Get state statistics from component list
|
|
* @param {Array<Object>} components - Array of components with state
|
|
* @returns {Object} State distribution statistics
|
|
*/
|
|
getStateStatistics: function(components) {
|
|
var stats = {
|
|
total: components ? components.length : 0,
|
|
by_state: {},
|
|
by_category: {
|
|
persistent: 0,
|
|
transient: 0,
|
|
runtime: 0,
|
|
error: 0,
|
|
unknown: 0
|
|
}
|
|
};
|
|
|
|
if (!components || !components.length) {
|
|
return stats;
|
|
}
|
|
|
|
for (var i = 0; i < components.length; i++) {
|
|
var state = components[i].current_state || components[i].state || 'unknown';
|
|
|
|
// Count by state
|
|
if (!stats.by_state[state]) {
|
|
stats.by_state[state] = 0;
|
|
}
|
|
stats.by_state[state]++;
|
|
|
|
// Count by category
|
|
var category = this.getStateCategory(state);
|
|
if (stats.by_category[category] !== undefined) {
|
|
stats.by_category[category]++;
|
|
}
|
|
}
|
|
|
|
return stats;
|
|
},
|
|
|
|
/**
|
|
* Sort components by state priority
|
|
* @param {Array<Object>} components - Array of components
|
|
* @returns {Array<Object>} Sorted components
|
|
*/
|
|
sortByStatePriority: function(components) {
|
|
if (!components || !components.length) {
|
|
return components;
|
|
}
|
|
|
|
var priorities = {
|
|
error: 1,
|
|
frozen: 2,
|
|
running: 3,
|
|
starting: 4,
|
|
stopping: 5,
|
|
active: 6,
|
|
stopped: 7,
|
|
installed: 8,
|
|
installing: 9,
|
|
disabled: 10,
|
|
available: 11
|
|
};
|
|
|
|
return components.slice().sort(function(a, b) {
|
|
var stateA = a.current_state || a.state || 'unknown';
|
|
var stateB = b.current_state || b.state || 'unknown';
|
|
|
|
var priorityA = priorities[stateA] || 99;
|
|
var priorityB = priorities[stateB] || 99;
|
|
|
|
return priorityA - priorityB;
|
|
});
|
|
}
|
|
});
|