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>
446 lines
12 KiB
JavaScript
446 lines
12 KiB
JavaScript
'use strict';
|
|
'require baseclass';
|
|
'require rpc';
|
|
|
|
// App Management
|
|
var callGetApps = rpc.declare({
|
|
object: 'luci.secubox',
|
|
method: 'get_appstore_apps',
|
|
expect: { apps: [] }
|
|
});
|
|
|
|
var callInstallApp = rpc.declare({
|
|
object: 'luci.secubox',
|
|
method: 'install_appstore_app',
|
|
params: ['app_id'],
|
|
expect: { success: false }
|
|
});
|
|
|
|
var callRemoveApp = rpc.declare({
|
|
object: 'luci.secubox',
|
|
method: 'remove_appstore_app',
|
|
params: ['app_id'],
|
|
expect: { success: false }
|
|
});
|
|
|
|
// Module Management
|
|
var callGetModules = rpc.declare({
|
|
object: 'luci.secubox',
|
|
method: 'getModules',
|
|
expect: { modules: [] }
|
|
});
|
|
|
|
var callEnableModule = rpc.declare({
|
|
object: 'luci.secubox',
|
|
method: 'enable_module',
|
|
params: ['module'],
|
|
expect: { success: false }
|
|
});
|
|
|
|
var callDisableModule = rpc.declare({
|
|
object: 'luci.secubox',
|
|
method: 'disable_module',
|
|
params: ['module'],
|
|
expect: { success: false }
|
|
});
|
|
|
|
// System Health
|
|
var callGetHealth = rpc.declare({
|
|
object: 'luci.secubox',
|
|
method: 'get_system_health',
|
|
expect: { }
|
|
});
|
|
|
|
var callGetAlerts = rpc.declare({
|
|
object: 'luci.secubox',
|
|
method: 'get_alerts',
|
|
expect: { alerts: [] }
|
|
});
|
|
|
|
// Logs
|
|
var callGetLogs = rpc.declare({
|
|
object: 'luci.secubox',
|
|
method: 'getLogs',
|
|
params: ['service', 'lines'],
|
|
expect: { logs: '' }
|
|
});
|
|
|
|
// Catalog Sources (with optimized timeout)
|
|
var callGetCatalogSources = rpc.declare({
|
|
object: 'luci.secubox',
|
|
method: 'get_catalog_sources',
|
|
expect: { sources: [] },
|
|
timeout: 15000 // 15 seconds (optimized backend with caching)
|
|
});
|
|
|
|
var callSetCatalogSource = rpc.declare({
|
|
object: 'luci.secubox',
|
|
method: 'set_catalog_source',
|
|
params: ['source'],
|
|
expect: { success: false },
|
|
timeout: 20000 // 20 seconds
|
|
});
|
|
|
|
var callSyncCatalog = rpc.declare({
|
|
object: 'luci.secubox',
|
|
method: 'sync_catalog',
|
|
params: ['source'],
|
|
expect: { success: false },
|
|
timeout: 90000 // Sync can take longer (90s for slow connections)
|
|
});
|
|
|
|
// Version Management (with optimized timeout)
|
|
var callCheckUpdates = rpc.declare({
|
|
object: 'luci.secubox',
|
|
method: 'check_updates',
|
|
expect: { },
|
|
timeout: 20000 // 20 seconds (optimized with persistent cache)
|
|
});
|
|
|
|
var callGetAppVersions = rpc.declare({
|
|
object: 'luci.secubox',
|
|
method: 'get_app_versions',
|
|
params: ['app_id'],
|
|
expect: { }
|
|
});
|
|
|
|
var callGetChangelog = rpc.declare({
|
|
object: 'luci.secubox',
|
|
method: 'get_changelog',
|
|
params: ['app_id', 'from_version', 'to_version'],
|
|
expect: { }
|
|
});
|
|
|
|
// Widget Data
|
|
var callGetWidgetData = rpc.declare({
|
|
object: 'luci.secubox',
|
|
method: 'get_widget_data',
|
|
params: ['app_id'],
|
|
expect: { }
|
|
});
|
|
|
|
// ===== State Management API =====
|
|
|
|
var callGetComponentState = rpc.declare({
|
|
object: 'luci.secubox',
|
|
method: 'get_component_state',
|
|
params: ['component_id'],
|
|
expect: { }
|
|
});
|
|
|
|
var callSetComponentState = rpc.declare({
|
|
object: 'luci.secubox',
|
|
method: 'set_component_state',
|
|
params: ['component_id', 'new_state', 'reason'],
|
|
expect: { success: false }
|
|
});
|
|
|
|
var callGetStateHistory = rpc.declare({
|
|
object: 'luci.secubox',
|
|
method: 'get_state_history',
|
|
params: ['component_id', 'limit'],
|
|
expect: { history: [] }
|
|
});
|
|
|
|
var callListComponents = rpc.declare({
|
|
object: 'luci.secubox',
|
|
method: 'list_components',
|
|
params: ['state_filter', 'type_filter'],
|
|
expect: { components: [] }
|
|
});
|
|
|
|
var callFreezeComponent = rpc.declare({
|
|
object: 'luci.secubox',
|
|
method: 'freeze_component',
|
|
params: ['component_id', 'reason'],
|
|
expect: { success: false }
|
|
});
|
|
|
|
var callClearErrorState = rpc.declare({
|
|
object: 'luci.secubox',
|
|
method: 'clear_error_state',
|
|
params: ['component_id'],
|
|
expect: { success: false }
|
|
});
|
|
|
|
// ===== Component Registry API =====
|
|
|
|
var callGetComponent = rpc.declare({
|
|
object: 'luci.secubox',
|
|
method: 'get_component',
|
|
params: ['component_id'],
|
|
expect: { }
|
|
});
|
|
|
|
var callGetComponentTree = rpc.declare({
|
|
object: 'luci.secubox',
|
|
method: 'get_component_tree',
|
|
params: ['component_id'],
|
|
expect: { }
|
|
});
|
|
|
|
var callUpdateComponentSettings = rpc.declare({
|
|
object: 'luci.secubox',
|
|
method: 'update_component_settings',
|
|
params: ['component_id', 'settings'],
|
|
expect: { success: false }
|
|
});
|
|
|
|
var callListComponentsByType = rpc.declare({
|
|
object: 'luci.secubox',
|
|
method: 'list_components',
|
|
params: ['state_filter', 'type_filter', 'profile_filter'],
|
|
expect: { components: [] }
|
|
});
|
|
|
|
var callValidateComponentState = rpc.declare({
|
|
object: 'luci.secubox',
|
|
method: 'validate_component_state',
|
|
params: ['component_id'],
|
|
expect: { valid: true }
|
|
});
|
|
|
|
// Utility functions
|
|
function formatBytes(bytes) {
|
|
if (bytes === 0) return '0 B';
|
|
var k = 1024;
|
|
var sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
var i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
|
|
}
|
|
|
|
function formatUptime(seconds) {
|
|
var days = Math.floor(seconds / 86400);
|
|
var hours = Math.floor((seconds % 86400) / 3600);
|
|
var mins = Math.floor((seconds % 3600) / 60);
|
|
return days + 'd ' + hours + 'h ' + mins + 'm';
|
|
}
|
|
|
|
function getAppStatus(app, modules) {
|
|
// Determine if app is installed by checking modules
|
|
var isInstalled = false;
|
|
var isRunning = false;
|
|
|
|
if (app.packages && app.packages.required) {
|
|
for (var i = 0; i < app.packages.required.length; i++) {
|
|
var pkg = app.packages.required[i];
|
|
if (modules[pkg]) {
|
|
isInstalled = true;
|
|
isRunning = modules[pkg].running || false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
installed: isInstalled,
|
|
running: isRunning,
|
|
status: isRunning ? 'running' : (isInstalled ? 'stopped' : 'available')
|
|
};
|
|
}
|
|
|
|
// Debug wrapper for RPC calls with retry logic
|
|
function debugRPC(name, call, options) {
|
|
options = options || {};
|
|
var maxRetries = options.retries || 2;
|
|
var retryDelay = options.retryDelay || 1000;
|
|
|
|
return function() {
|
|
var args = Array.prototype.slice.call(arguments);
|
|
var attemptCount = 0;
|
|
var self = this;
|
|
|
|
function attemptCall() {
|
|
attemptCount++;
|
|
console.log('[API-DEBUG] Calling:', name, 'with args:', args, '(attempt ' + attemptCount + ')');
|
|
|
|
return call.apply(self, args).then(function(result) {
|
|
console.log('[API-DEBUG] Success:', name, 'result:', result, '(attempt ' + attemptCount + ')');
|
|
return result;
|
|
}).catch(function(error) {
|
|
console.error('[API-DEBUG] Error:', name, 'error:', error, '(attempt ' + attemptCount + '/' + (maxRetries + 1) + ')');
|
|
console.error('[API-DEBUG] Error message:', error.message);
|
|
console.error('[API-DEBUG] Error stack:', error.stack || 'no stack');
|
|
|
|
// Retry on timeout errors
|
|
if (attemptCount <= maxRetries && error.message && error.message.indexOf('timed out') !== -1) {
|
|
console.warn('[API-DEBUG] Retrying', name, 'in', retryDelay, 'ms...');
|
|
return new Promise(function(resolve) {
|
|
setTimeout(function() {
|
|
resolve(attemptCall());
|
|
}, retryDelay);
|
|
});
|
|
}
|
|
|
|
throw error;
|
|
});
|
|
}
|
|
|
|
return attemptCall();
|
|
};
|
|
}
|
|
|
|
// Export API with debug wrappers and retry logic
|
|
return baseclass.extend({
|
|
// Apps
|
|
getApps: debugRPC('getApps', callGetApps, { retries: 2, retryDelay: 1500 }),
|
|
|
|
// Featured Apps
|
|
getFeaturedApps: function() {
|
|
var self = this;
|
|
return this.getApps().then(function(apps) {
|
|
if (!apps || !Array.isArray(apps)) {
|
|
return [];
|
|
}
|
|
// Filter apps with featured=true and sort by priority
|
|
return apps
|
|
.filter(function(app) {
|
|
return app.featured === true;
|
|
})
|
|
.sort(function(a, b) {
|
|
var priorityA = a.featured_priority || 999;
|
|
var priorityB = b.featured_priority || 999;
|
|
return priorityA - priorityB;
|
|
});
|
|
}).catch(function(err) {
|
|
console.error('[API] getFeaturedApps error:', err);
|
|
return [];
|
|
});
|
|
},
|
|
|
|
installApp: debugRPC('installApp', callInstallApp, { retries: 1 }),
|
|
removeApp: debugRPC('removeApp', callRemoveApp, { retries: 1 }),
|
|
|
|
// Modules
|
|
getModules: debugRPC('getModules', callGetModules, { retries: 2, retryDelay: 1500 }),
|
|
enableModule: debugRPC('enableModule', callEnableModule),
|
|
disableModule: debugRPC('disableModule', callDisableModule),
|
|
|
|
// System
|
|
getHealth: debugRPC('getHealth', callGetHealth, { retries: 1 }),
|
|
getAlerts: debugRPC('getAlerts', callGetAlerts, { retries: 1 }),
|
|
getLogs: debugRPC('getLogs', callGetLogs),
|
|
|
|
// Catalog Sources (critical - more retries)
|
|
getCatalogSources: debugRPC('getCatalogSources', callGetCatalogSources, { retries: 3, retryDelay: 2000 }),
|
|
setCatalogSource: debugRPC('setCatalogSource', callSetCatalogSource, { retries: 1 }),
|
|
syncCatalog: debugRPC('syncCatalog', callSyncCatalog, { retries: 1 }),
|
|
|
|
// Version Management (critical - more retries)
|
|
checkUpdates: debugRPC('checkUpdates', callCheckUpdates, { retries: 3, retryDelay: 2000 }),
|
|
getAppVersions: debugRPC('getAppVersions', callGetAppVersions, { retries: 1 }),
|
|
getChangelog: debugRPC('getChangelog', callGetChangelog, { retries: 1 }),
|
|
|
|
// Widget Data
|
|
getWidgetData: debugRPC('getWidgetData', callGetWidgetData, { retries: 1 }),
|
|
|
|
// ===== State Management =====
|
|
getComponentState: debugRPC('getComponentState', callGetComponentState, { retries: 2 }),
|
|
setComponentState: debugRPC('setComponentState', callSetComponentState, { retries: 1 }),
|
|
getStateHistory: debugRPC('getStateHistory', callGetStateHistory, { retries: 1 }),
|
|
listComponents: debugRPC('listComponents', callListComponents, { retries: 2, retryDelay: 1500 }),
|
|
freezeComponent: debugRPC('freezeComponent', callFreezeComponent, { retries: 1 }),
|
|
clearErrorState: debugRPC('clearErrorState', callClearErrorState, { retries: 1 }),
|
|
|
|
// ===== Component Registry =====
|
|
getComponent: debugRPC('getComponent', callGetComponent, { retries: 2 }),
|
|
getComponentTree: debugRPC('getComponentTree', callGetComponentTree, { retries: 1 }),
|
|
updateComponentSettings: debugRPC('updateComponentSettings', callUpdateComponentSettings, { retries: 1 }),
|
|
listComponentsByType: debugRPC('listComponentsByType', callListComponentsByType, { retries: 2 }),
|
|
validateComponentState: debugRPC('validateComponentState', callValidateComponentState, { retries: 1 }),
|
|
|
|
// Enhanced component methods
|
|
getComponentWithState: function(component_id) {
|
|
var self = this;
|
|
return Promise.all([
|
|
this.getComponent(component_id),
|
|
this.getComponentState(component_id)
|
|
]).then(function(results) {
|
|
var component = results[0];
|
|
var state = results[1];
|
|
return Object.assign({}, component, { state_info: state });
|
|
}).catch(function(err) {
|
|
console.error('[API] getComponentWithState error:', err);
|
|
throw err;
|
|
});
|
|
},
|
|
|
|
getAllComponentsWithStates: function(filters) {
|
|
var self = this;
|
|
filters = filters || {};
|
|
|
|
return this.listComponents(filters.state, filters.type).then(function(components) {
|
|
if (!components || !Array.isArray(components)) {
|
|
return [];
|
|
}
|
|
|
|
// Fetch states for all components in parallel
|
|
var statePromises = components.map(function(comp) {
|
|
return self.getComponentState(comp.id || comp.component_id).catch(function(err) {
|
|
console.warn('[API] Failed to get state for', comp.id, err);
|
|
return null;
|
|
});
|
|
});
|
|
|
|
return Promise.all(statePromises).then(function(states) {
|
|
return components.map(function(comp, index) {
|
|
return Object.assign({}, comp, { state_info: states[index] });
|
|
});
|
|
});
|
|
}).catch(function(err) {
|
|
console.error('[API] getAllComponentsWithStates error:', err);
|
|
return [];
|
|
});
|
|
},
|
|
|
|
// Bulk operations
|
|
bulkSetComponentState: function(component_ids, new_state, reason) {
|
|
var self = this;
|
|
var promises = component_ids.map(function(id) {
|
|
return self.setComponentState(id, new_state, reason).catch(function(err) {
|
|
console.warn('[API] Failed to set state for', id, err);
|
|
return { success: false, component_id: id, error: err };
|
|
});
|
|
});
|
|
|
|
return Promise.all(promises);
|
|
},
|
|
|
|
// State statistics
|
|
getStateStatistics: function() {
|
|
var self = this;
|
|
return this.listComponents(null, null).then(function(components) {
|
|
if (!components || !Array.isArray(components)) {
|
|
return { total: 0, by_state: {}, by_type: {}, by_category: {} };
|
|
}
|
|
|
|
var stats = {
|
|
total: components.length,
|
|
by_state: {},
|
|
by_type: {},
|
|
by_category: {}
|
|
};
|
|
|
|
components.forEach(function(comp) {
|
|
// Count by state
|
|
var state = comp.current_state || comp.state || 'unknown';
|
|
stats.by_state[state] = (stats.by_state[state] || 0) + 1;
|
|
|
|
// Count by type
|
|
var type = comp.type || 'unknown';
|
|
stats.by_type[type] = (stats.by_type[type] || 0) + 1;
|
|
});
|
|
|
|
return stats;
|
|
}).catch(function(err) {
|
|
console.error('[API] getStateStatistics error:', err);
|
|
return { total: 0, by_state: {}, by_type: {}, by_category: {} };
|
|
});
|
|
},
|
|
|
|
// Utilities
|
|
formatBytes: formatBytes,
|
|
formatUptime: formatUptime,
|
|
getAppStatus: getAppStatus
|
|
});
|