secubox-openwrt/package/secubox/luci-app-secubox-admin/htdocs/luci-static/resources/secubox-admin/components/StateTimeline.js
CyberMind-FR e258d86eea feat: Admin Control Center with State Management (v0.9.0)
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>
2026-01-05 10:05:32 +01:00

455 lines
13 KiB
JavaScript

'use strict';
'require baseclass';
'require secubox-admin.state-utils as stateUtils';
'require secubox-admin.components.StateIndicator as StateIndicator';
/**
* StateTimeline Component
* Visualizes state history as a timeline with transitions and events
*/
return baseclass.extend({
/**
* Render a state history timeline
* @param {Array<Object>} history - Array of history entries
* @param {Object} options - Display options
* @returns {Element} DOM element
*/
render: function(history, options) {
options = options || {};
if (!history || history.length === 0) {
return E('div', {
'class': 'state-timeline-empty',
'style': 'padding: 2rem; text-align: center; color: #6b7280;'
}, 'No state history available');
}
var container = E('div', {
'class': 'state-timeline',
'style': 'position: relative; padding-left: 2rem;'
});
// Vertical timeline line
var timelineLine = E('div', {
'class': 'timeline-line',
'style': 'position: absolute; left: 0.5rem; top: 0; bottom: 0; width: 2px; background-color: #e5e7eb;'
});
container.appendChild(timelineLine);
// Render history entries
var sortedHistory = this._sortHistory(history, options.sortOrder || 'desc');
var limit = options.limit || sortedHistory.length;
for (var i = 0; i < Math.min(limit, sortedHistory.length); i++) {
var entry = sortedHistory[i];
var timelineEntry = this._renderTimelineEntry(entry, i, options);
container.appendChild(timelineEntry);
}
// "Show more" button if limited
if (limit < sortedHistory.length && options.onShowMore) {
var showMoreBtn = E('button', {
'class': 'btn btn-sm timeline-show-more',
'style': 'margin-left: 2rem; margin-top: 1rem;'
}, 'Show ' + (sortedHistory.length - limit) + ' more...');
showMoreBtn.addEventListener('click', function() {
options.onShowMore();
});
container.appendChild(showMoreBtn);
}
return container;
},
/**
* Render a compact timeline (inline)
* @param {Array<Object>} history - Array of history entries
* @param {Object} options - Display options
* @returns {Element} DOM element
*/
renderCompact: function(history, options) {
options = options || {};
if (!history || history.length === 0) {
return E('span', { 'style': 'color: #6b7280; font-size: 0.875rem;' }, 'No history');
}
var container = E('div', {
'class': 'state-timeline-compact',
'style': 'display: flex; align-items: center; gap: 0.25rem;'
});
var sortedHistory = this._sortHistory(history, 'desc');
var limit = options.limit || 5;
for (var i = 0; i < Math.min(limit, sortedHistory.length); i++) {
var entry = sortedHistory[i];
var stateIndicator = StateIndicator.renderDot(entry.state, {
size: '0.625rem',
customTooltip: stateUtils.formatHistoryEntry(entry)
});
container.appendChild(stateIndicator);
}
// More indicator
if (sortedHistory.length > limit) {
var moreIndicator = E('span', {
'style': 'font-size: 0.75rem; color: #6b7280; margin-left: 0.25rem;'
}, '+' + (sortedHistory.length - limit));
container.appendChild(moreIndicator);
}
return container;
},
/**
* Render a horizontal timeline (for state transitions)
* @param {Array<Object>} history - Array of history entries
* @param {Object} options - Display options
* @returns {Element} DOM element
*/
renderHorizontal: function(history, options) {
options = options || {};
if (!history || history.length === 0) {
return E('div', {
'class': 'state-timeline-empty',
'style': 'padding: 1rem; text-align: center; color: #6b7280;'
}, 'No transitions');
}
var container = E('div', {
'class': 'state-timeline-horizontal',
'style': 'display: flex; align-items: center; gap: 0.5rem; overflow-x: auto; padding: 1rem 0;'
});
var sortedHistory = this._sortHistory(history, 'asc');
var limit = options.limit || sortedHistory.length;
for (var i = 0; i < Math.min(limit, sortedHistory.length); i++) {
var entry = sortedHistory[i];
var config = stateUtils.getStateConfig(entry.state);
// State node
var node = E('div', {
'class': 'timeline-node',
'style': 'display: flex; flex-direction: column; align-items: center; min-width: 80px;'
});
// Icon
var icon = StateIndicator.renderCompact(entry.state);
node.appendChild(icon);
// Label
var label = E('div', {
'style': 'font-size: 0.75rem; margin-top: 0.25rem; color: ' + config.color + '; font-weight: 600; text-align: center;'
}, config.label);
node.appendChild(label);
// Time
if (entry.timestamp) {
var time = E('div', {
'style': 'font-size: 0.625rem; color: #6b7280; margin-top: 0.125rem; text-align: center;'
}, stateUtils.getTimeAgo(entry.timestamp));
node.appendChild(time);
}
container.appendChild(node);
// Arrow connector (except for last)
if (i < Math.min(limit, sortedHistory.length) - 1) {
var arrow = E('div', {
'class': 'timeline-arrow',
'style': 'font-size: 1.25rem; color: #9ca3af; margin: 0 0.5rem;'
}, '→');
container.appendChild(arrow);
}
}
return container;
},
/**
* Render timeline entry (for vertical timeline)
* @private
*/
_renderTimelineEntry: function(entry, index, options) {
var config = stateUtils.getStateConfig(entry.state);
var isError = stateUtils.isError(entry.state);
var isTransient = stateUtils.isTransient(entry.state);
var entryContainer = E('div', {
'class': 'timeline-entry' + (isError ? ' timeline-entry-error' : '') + (isTransient ? ' timeline-entry-transient' : ''),
'style': 'position: relative; margin-bottom: 1.5rem;'
});
// Timeline dot
var dot = E('div', {
'class': 'timeline-dot',
'style': 'position: absolute; left: -1.75rem; top: 0.25rem; width: 1rem; height: 1rem; border-radius: 50%; background-color: ' + config.color + '; border: 3px solid #ffffff; z-index: 10;'
});
if (isTransient) {
dot.style.animation = 'pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite';
}
entryContainer.appendChild(dot);
// Entry content
var content = E('div', {
'class': 'timeline-content',
'style': 'padding: 0.75rem; border-radius: 0.5rem; background-color: ' + config.color + '10; border-left: 3px solid ' + config.color + ';'
});
// Header (state + timestamp)
var header = E('div', {
'class': 'timeline-header',
'style': 'display: flex; align-items: center; justify-content: space-between; margin-bottom: 0.5rem;'
});
var stateInfo = E('div', { 'style': 'display: flex; align-items: center; gap: 0.5rem;' });
var stateBadge = StateIndicator.render(entry.state, { showIcon: true, showLabel: true, animate: false });
stateInfo.appendChild(stateBadge);
// Category badge
if (options.showCategory !== false) {
var categoryBadge = E('span', {
'class': 'badge badge-secondary',
'style': 'font-size: 0.625rem; padding: 0.125rem 0.375rem; background-color: #f3f4f6; color: #6b7280;'
}, config.category);
stateInfo.appendChild(categoryBadge);
}
header.appendChild(stateInfo);
// Timestamp
if (entry.timestamp) {
var timestamp = E('div', {
'class': 'timeline-timestamp',
'style': 'font-size: 0.75rem; color: #6b7280;'
});
if (options.showRelativeTime !== false) {
timestamp.textContent = stateUtils.getTimeAgo(entry.timestamp);
timestamp.setAttribute('title', stateUtils.formatTimestamp(entry.timestamp));
} else {
timestamp.textContent = stateUtils.formatTimestamp(entry.timestamp);
}
header.appendChild(timestamp);
}
content.appendChild(header);
// Reason
if (entry.reason) {
var reason = E('div', {
'class': 'timeline-reason',
'style': 'font-size: 0.875rem; color: #4b5563; margin-bottom: 0.25rem;'
}, 'Reason: ' + this._formatReason(entry.reason));
content.appendChild(reason);
}
// Error details
if (isError && entry.error_details) {
var errorDetails = E('div', {
'class': 'timeline-error-details',
'style': 'margin-top: 0.5rem; padding: 0.5rem; background-color: #fee2e2; border-radius: 0.375rem; border-left: 3px solid #ef4444;'
});
if (entry.error_details.type) {
var errorType = E('div', {
'style': 'font-size: 0.75rem; font-weight: 600; color: #dc2626; margin-bottom: 0.25rem;'
}, 'Error Type: ' + entry.error_details.type);
errorDetails.appendChild(errorType);
}
if (entry.error_details.message) {
var errorMsg = E('div', {
'style': 'font-size: 0.75rem; color: #991b1b;'
}, entry.error_details.message);
errorDetails.appendChild(errorMsg);
}
if (entry.error_details.code) {
var errorCode = E('div', {
'style': 'font-size: 0.625rem; color: #7f1d1d; margin-top: 0.25rem; font-family: monospace;'
}, 'Code: ' + entry.error_details.code);
errorDetails.appendChild(errorCode);
}
content.appendChild(errorDetails);
}
// Metadata
if (options.showMetadata !== false && entry.metadata) {
var metadata = E('div', {
'class': 'timeline-metadata',
'style': 'margin-top: 0.5rem; padding-top: 0.5rem; border-top: 1px solid ' + config.color + '30; font-size: 0.75rem; color: #6b7280;'
});
var metadataEntries = Object.entries(entry.metadata);
for (var i = 0; i < metadataEntries.length; i++) {
var key = metadataEntries[i][0];
var value = metadataEntries[i][1];
var metaItem = E('div', {}, key + ': ' + value);
metadata.appendChild(metaItem);
}
content.appendChild(metadata);
}
// Actions
if (options.onEntryClick) {
content.style.cursor = 'pointer';
content.addEventListener('click', function() {
options.onEntryClick(entry, index);
});
}
entryContainer.appendChild(content);
return entryContainer;
},
/**
* Sort history entries
* @private
*/
_sortHistory: function(history, order) {
var sorted = history.slice();
sorted.sort(function(a, b) {
var timeA = new Date(a.timestamp || 0).getTime();
var timeB = new Date(b.timestamp || 0).getTime();
return order === 'asc' ? timeA - timeB : timeB - timeA;
});
return sorted;
},
/**
* Format reason string
* @private
*/
_formatReason: function(reason) {
if (!reason) {
return 'Unknown';
}
// Convert snake_case to Title Case
return reason
.split('_')
.map(function(word) {
return word.charAt(0).toUpperCase() + word.slice(1);
})
.join(' ');
},
/**
* Render a state transition diagram
* @param {string} currentState - Current state
* @param {Object} options - Display options
* @returns {Element} DOM element
*/
renderTransitionDiagram: function(currentState, options) {
options = options || {};
var container = E('div', {
'class': 'state-transition-diagram',
'style': 'padding: 1rem;'
});
// Current state (center)
var currentStateEl = E('div', {
'style': 'text-align: center; margin-bottom: 1.5rem;'
});
var currentLabel = E('div', {
'style': 'font-size: 0.875rem; color: #6b7280; margin-bottom: 0.5rem;'
}, 'Current State:');
currentStateEl.appendChild(currentLabel);
var currentBadge = StateIndicator.renderPill(currentState, {}, { showDescription: true });
currentStateEl.appendChild(currentBadge);
container.appendChild(currentStateEl);
// Possible transitions
var nextStates = stateUtils.getNextStates(currentState);
if (nextStates.length > 0) {
var transitionsLabel = E('div', {
'style': 'font-size: 0.875rem; color: #6b7280; margin-bottom: 0.75rem; text-align: center;'
}, 'Possible Transitions:');
container.appendChild(transitionsLabel);
var transitionsGrid = E('div', {
'style': 'display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 0.75rem;'
});
for (var i = 0; i < nextStates.length; i++) {
var nextState = nextStates[i];
var nextConfig = stateUtils.getStateConfig(nextState);
var transitionCard = E('div', {
'class': 'transition-card',
'style': 'padding: 0.75rem; border-radius: 0.5rem; border: 2px solid ' + nextConfig.color + '40; background-color: ' + nextConfig.color + '10; cursor: pointer; transition: all 0.2s;'
});
transitionCard.addEventListener('mouseenter', function() {
this.style.borderColor = nextConfig.color;
this.style.transform = 'translateY(-2px)';
});
transitionCard.addEventListener('mouseleave', function() {
this.style.borderColor = nextConfig.color + '40';
this.style.transform = 'translateY(0)';
});
// Arrow
var arrow = E('div', {
'style': 'text-align: center; font-size: 1.5rem; color: ' + nextConfig.color + '; margin-bottom: 0.5rem;'
}, '↓');
transitionCard.appendChild(arrow);
// State badge
var badge = StateIndicator.render(nextState, { showIcon: true, showLabel: true });
badge.style.display = 'flex';
badge.style.justifyContent = 'center';
transitionCard.appendChild(badge);
// Description
var desc = E('div', {
'style': 'font-size: 0.75rem; color: #6b7280; margin-top: 0.5rem; text-align: center;'
}, nextConfig.description);
transitionCard.appendChild(desc);
// Click handler
if (options.onTransitionClick) {
(function(state) {
transitionCard.addEventListener('click', function() {
options.onTransitionClick(currentState, state);
});
})(nextState);
}
transitionsGrid.appendChild(transitionCard);
}
container.appendChild(transitionsGrid);
} else {
var noTransitions = E('div', {
'style': 'text-align: center; padding: 1rem; color: #6b7280; font-size: 0.875rem;'
}, 'No transitions available from this state');
container.appendChild(noTransitions);
}
return container;
}
});