feat: firmware registry view

This commit is contained in:
2025-10-21 17:51:22 +02:00
parent aa7467e1ca
commit 7def7bce81
10 changed files with 2287 additions and 1028 deletions

View File

@@ -0,0 +1,169 @@
# Firmware Registry Integration
This document describes the integration of the SPORE Registry into the SPORE UI, replacing the previous firmware upload functionality with a comprehensive CRUD interface for managing firmware in the registry.
## Overview
The firmware view has been completely redesigned to provide:
- **Registry Management**: Full CRUD operations for firmware in the SPORE Registry
- **Search & Filter**: Search firmware by name, version, or labels
- **Drawer Forms**: Add/edit forms displayed in the existing drawer component
- **Real-time Status**: Registry connection status indicator
- **Download Support**: Direct download of firmware binaries
## Architecture
### Components
1. **FirmwareComponent** (`FirmwareComponent.js`)
- Main component for the firmware registry interface
- Handles CRUD operations and UI interactions
- Manages registry connection status
2. **FirmwareFormComponent** (`FirmwareFormComponent.js`)
- Form component for add/edit operations
- Used within the drawer component
- Handles metadata and file uploads
3. **API Client Extensions** (`api-client.js`)
- New registry API methods added to existing ApiClient
- Auto-detection of registry server URL
- Support for multipart form data uploads
### API Integration
The integration uses the SPORE Registry API endpoints:
- `GET /health` - Health check
- `GET /firmware` - List firmware with optional filtering
- `POST /firmware` - Upload firmware with metadata
- `GET /firmware/{name}/{version}` - Download firmware binary
### Registry Server Configuration
The registry server is expected to run on:
- **Localhost**: `http://localhost:8080`
- **Remote**: `http://{hostname}:8080`
The UI automatically detects the appropriate URL based on the current hostname.
## Features
### Firmware Management
- **Add Firmware**: Upload new firmware with metadata and labels
- **Edit Firmware**: Modify existing firmware (requires new file upload)
- **Download Firmware**: Direct download of firmware binaries
- **Delete Firmware**: Remove firmware from registry (not yet implemented in API)
### Search & Filtering
- **Text Search**: Search by firmware name, version, or label values
- **Real-time Filtering**: Results update as you type
- **Label Display**: Visual display of firmware labels with color coding
### User Interface
- **Card Layout**: Clean card-based layout for firmware entries
- **Action Buttons**: Edit, download, and delete actions for each firmware
- **Status Indicators**: Registry connection status with visual feedback
- **Loading States**: Proper loading indicators during operations
- **Error Handling**: User-friendly error messages and notifications
### Form Interface
- **Drawer Integration**: Forms open in the existing drawer component
- **Metadata Fields**: Name, version, and custom labels
- **File Upload**: Drag-and-drop or click-to-upload file selection
- **Label Management**: Add/remove key-value label pairs
- **Validation**: Client-side validation with helpful error messages
## Usage
### Adding Firmware
1. Click the "Add Firmware" button in the header
2. Fill in the firmware name and version
3. Select a firmware file (.bin or .hex)
4. Add optional labels (key-value pairs)
5. Click "Upload Firmware"
### Editing Firmware
1. Click the edit button on any firmware card
2. Modify the metadata (name and version are read-only)
3. Upload a new firmware file
4. Update labels as needed
5. Click "Update Firmware"
### Downloading Firmware
1. Click the download button on any firmware card
2. The firmware binary will be downloaded automatically
### Searching Firmware
1. Use the search box to filter firmware
2. Search by name, version, or label values
3. Results update in real-time
## Testing
A test suite is provided to verify the registry integration:
```bash
cd spore-ui/test
node registry-integration-test.js
```
The test suite verifies:
- Registry health check
- List firmware functionality
- Upload firmware functionality
- Download firmware functionality
## Configuration
### Registry Server
Ensure the SPORE Registry server is running on port 8080:
```bash
cd spore-registry
go run main.go
```
### UI Configuration
The UI automatically detects the registry server URL. No additional configuration is required.
## Error Handling
The integration includes comprehensive error handling:
- **Connection Errors**: Clear indication when registry is unavailable
- **Upload Errors**: Detailed error messages for upload failures
- **Validation Errors**: Client-side validation with helpful messages
- **Network Errors**: Graceful handling of network timeouts and failures
## Future Enhancements
Planned improvements include:
- **Delete Functionality**: Complete delete operation when API supports it
- **Bulk Operations**: Select multiple firmware for bulk operations
- **Version History**: View and manage firmware version history
- **Deployment Integration**: Deploy firmware directly to nodes from registry
- **Advanced Filtering**: Filter by date, size, or other metadata
## Migration Notes
The previous firmware upload functionality has been completely replaced. The new interface provides:
- Better organization with the registry
- Improved user experience with search and filtering
- Consistent UI patterns with the rest of the application
- Better error handling and user feedback
All existing firmware functionality is now handled through the registry interface.

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 728 B

View File

@@ -147,77 +147,51 @@
<div id="firmware-view" class="view-content">
<div class="firmware-section">
<div id="firmware-container">
<div class="firmware-overview">
<div class="firmware-actions">
<div class="action-group">
<h3>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16" style="margin-right:6px; vertical-align: -2px;">
<path d="M4 7l8-4 8 4v10l-8 4-8-4z"/>
<path d="M12 8v8"/>
</svg>
Firmware Update
</h3>
<div class="firmware-upload-compact">
<div class="compact-upload-row">
<div class="file-upload-area">
<div class="target-options">
<label class="target-option">
<input type="radio" name="target-type" value="all" checked>
<span class="radio-custom"></span>
<span class="target-label">All Nodes</span>
</label>
<label class="target-option specific-node-option">
<input type="radio" name="target-type" value="specific">
<span class="radio-custom"></span>
<span class="target-label">Specific Node</span>
<select id="specific-node-select" class="node-select">
<option value="">Select a node...</option>
</select>
</label>
<label class="target-option by-label-option">
<input type="radio" name="target-type" value="labels">
<span class="radio-custom"></span>
<span class="target-label">By Label</span>
<select id="label-select" class="label-select"
style="min-width: 220px; display: inline-block; vertical-align: middle;">
<option value="">Select a label...</option>
</select>
<div id="selected-labels-container" class="selected-labels"></div>
</label>
</div>
<div class="file-input-wrapper">
<input type="file" id="global-firmware-file" accept=".bin,.hex"
style="display: none;">
<button class="upload-btn-compact"
onclick="document.getElementById('global-firmware-file').click()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14" style="margin-right:6px; vertical-align: -2px;">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
<path d="M14 2v6h6"/>
</svg>
Choose File
</button>
<span class="file-info" id="file-info">No file selected</span>
</div>
</div>
<button class="deploy-btn" id="deploy-btn" disabled>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16" style="margin-right:6px; vertical-align: -2px;">
<path d="M12 16V4"/>
<path d="M8 8l4-4 4 4"/>
<path d="M20 16v2a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-2"/>
</svg>
Deploy
</button>
</div>
</div>
</div>
<div class="firmware-header">
<div class="firmware-search">
<div class="search-input-wrapper">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16" class="search-icon">
<circle cx="11" cy="11" r="8"/>
<path d="M21 21l-4.35-4.35"/>
</svg>
<input type="text" id="firmware-search" placeholder="Search firmware by name, version, or labels (e.g., '1.0.0 base')...">
</div>
</div>
<div class="header-actions">
<div id="registry-status" class="registry-status">
<span class="status-indicator disconnected">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="12" height="12">
<circle cx="12" cy="12" r="10"/>
<line x1="15" y1="9" x2="9" y2="15"/>
<line x1="9" y1="9" x2="15" y2="15"/>
</svg>
Registry Disconnected
</span>
</div>
<button class="refresh-btn" id="refresh-firmware-btn" title="Refresh firmware list">
<svg class="refresh-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8" />
<path d="M21 3v5h-5" />
<path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16" />
<path d="M3 21v-5h5" />
</svg>
</button>
<button class="add-btn" id="add-firmware-btn" title="Add new firmware">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16">
<line x1="12" y1="5" x2="12" y2="19"/>
<line x1="5" y1="12" x2="19" y2="12"/>
</svg>
Add Firmware
</button>
</div>
</div>
<div class="firmware-nodes-list" id="firmware-nodes-list">
<!-- Nodes will be populated here -->
<div class="firmware-content">
<div id="firmware-list-container" class="firmware-list-container">
<div class="loading-state">
<div class="loading-spinner"></div>
<div class="loading-text">Loading firmware...</div>
</div>
</div>
</div>
</div>
@@ -272,6 +246,7 @@
<script src="./scripts/components/ClusterMembersComponent.js"></script>
<script src="./scripts/components/OverlayDialogComponent.js"></script>
<script src="./scripts/components/FirmwareComponent.js"></script>
<script src="./scripts/components/FirmwareFormComponent.js"></script>
<script src="./scripts/components/FirmwareUploadComponent.js"></script>
<script src="./scripts/components/WiFiConfigComponent.js"></script>
<!-- Container/view components after their deps -->

View File

@@ -136,6 +136,98 @@ class ApiClient {
}
});
}
// Registry API methods
async getRegistryBaseUrl() {
// Auto-detect registry server URL based on current location
const currentHost = window.location.hostname;
// If accessing from localhost, use localhost:8080
// If accessing from another device, use the same hostname but port 8080
if (currentHost === 'localhost' || currentHost === '127.0.0.1') {
return 'http://localhost:8080';
} else {
return `http://${currentHost}:8080`;
}
}
async uploadFirmwareToRegistry(metadata, firmwareFile) {
const registryBaseUrl = await this.getRegistryBaseUrl();
const formData = new FormData();
formData.append('metadata', JSON.stringify(metadata));
formData.append('firmware', firmwareFile);
const response = await fetch(`${registryBaseUrl}/firmware`, {
method: 'POST',
body: formData
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Registry upload failed: ${errorText}`);
}
return await response.json();
}
async updateFirmwareMetadata(name, version, metadata) {
const registryBaseUrl = await this.getRegistryBaseUrl();
const response = await fetch(`${registryBaseUrl}/firmware/${encodeURIComponent(name)}/${encodeURIComponent(version)}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(metadata)
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Registry metadata update failed: ${errorText}`);
}
return await response.json();
}
async listFirmwareFromRegistry(name = null, version = null) {
const registryBaseUrl = await this.getRegistryBaseUrl();
const query = {};
if (name) query.name = name;
if (version) query.version = version;
const response = await fetch(`${registryBaseUrl}/firmware${Object.keys(query).length ? '?' + new URLSearchParams(query).toString() : ''}`);
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Registry list failed: ${errorText}`);
}
return await response.json();
}
async downloadFirmwareFromRegistry(name, version) {
const registryBaseUrl = await this.getRegistryBaseUrl();
const response = await fetch(`${registryBaseUrl}/firmware/${encodeURIComponent(name)}/${encodeURIComponent(version)}`);
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Registry download failed: ${errorText}`);
}
return response.blob();
}
async getRegistryHealth() {
const registryBaseUrl = await this.getRegistryBaseUrl();
const response = await fetch(`${registryBaseUrl}/health`);
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Registry health check failed: ${errorText}`);
}
return await response.json();
}
}
// Global API client instance

View File

@@ -1,7 +1,7 @@
(function(){
// Simple readiness flag once all component constructors are present
function allReady(){
return !!(window.PrimaryNodeComponent && window.ClusterMembersComponent && window.NodeDetailsComponent && window.FirmwareComponent && window.ClusterViewComponent && window.FirmwareViewComponent && window.TopologyGraphComponent && window.MemberCardOverlayComponent && window.ClusterStatusComponent && window.DrawerComponent && window.WiFiConfigComponent);
return !!(window.PrimaryNodeComponent && window.ClusterMembersComponent && window.NodeDetailsComponent && window.FirmwareComponent && window.FirmwareFormComponent && window.ClusterViewComponent && window.FirmwareViewComponent && window.TopologyGraphComponent && window.MemberCardOverlayComponent && window.ClusterStatusComponent && window.DrawerComponent && window.WiFiConfigComponent);
}
window.waitForComponentsReady = function(timeoutMs = 5000){
return new Promise((resolve, reject) => {

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,350 @@
// Firmware Form Component for add/edit operations in drawer
class FirmwareFormComponent extends Component {
constructor(container, viewModel, eventBus) {
super(container, viewModel, eventBus);
this.firmwareData = null;
this.onSaveCallback = null;
this.onCancelCallback = null;
this.isEditMode = false;
}
setFirmwareData(firmwareData) {
this.firmwareData = firmwareData;
this.isEditMode = !!firmwareData;
}
setOnSaveCallback(callback) {
this.onSaveCallback = callback;
}
setOnCancelCallback(callback) {
this.onCancelCallback = callback;
}
setupEventListeners() {
// Submit button
const submitBtn = this.findElement('button[type="submit"]');
if (submitBtn) {
this.addEventListener(submitBtn, 'click', this.handleSubmit.bind(this));
}
// Cancel button
const cancelBtn = this.findElement('#cancel-btn');
if (cancelBtn) {
this.addEventListener(cancelBtn, 'click', this.handleCancel.bind(this));
}
// File input
const fileInput = this.findElement('#firmware-file');
if (fileInput) {
this.addEventListener(fileInput, 'change', this.handleFileSelect.bind(this));
}
// Labels management
this.setupLabelsManagement();
}
setupLabelsManagement() {
// Add label button
const addLabelBtn = this.findElement('#add-label-btn');
if (addLabelBtn) {
this.addEventListener(addLabelBtn, 'click', this.addLabel.bind(this));
}
// Remove label buttons (delegated event handling)
const labelsContainer = this.findElement('#labels-container');
if (labelsContainer) {
this.addEventListener(labelsContainer, 'click', (e) => {
const removeBtn = e.target.closest('.remove-label-btn');
if (removeBtn) {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
const key = removeBtn.getAttribute('data-label-key');
if (key) {
this.removeLabel(key);
}
}
});
}
}
mount() {
super.mount();
this.render();
this.setupEventListeners();
}
render() {
const container = this.container;
if (!container) return;
const labels = this.firmwareData?.labels || {};
const labelsHTML = Object.entries(labels).map(([key, value]) =>
`<div class="label-item">
<span class="label-key">${this.escapeHtml(key)}</span>
<span class="label-value">${this.escapeHtml(value)}</span>
<button type="button" class="remove-label-btn" data-label-key="${this.escapeHtml(key)}" title="Remove label">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14">
<line x1="18" y1="6" x2="6" y2="18"/>
<line x1="6" y1="6" x2="18" y2="18"/>
</svg>
</button>
</div>`
).join('');
container.innerHTML = `
<div class="firmware-form">
<div class="form-group">
<label for="firmware-name">Name *</label>
<input
type="text"
id="firmware-name"
name="name"
value="${this.firmwareData?.name || ''}"
placeholder="e.g., base, neopattern, relay"
${this.isEditMode ? 'readonly' : ''}
>
<small class="form-help">${this.isEditMode ? 'Name cannot be changed after creation' : 'Unique identifier for the firmware'}</small>
</div>
<div class="form-group">
<label for="firmware-version">Version *</label>
<input
type="text"
id="firmware-version"
name="version"
value="${this.firmwareData?.version || ''}"
placeholder="e.g., 1.0.0, 2.1.3"
${this.isEditMode ? 'readonly' : ''}
>
<small class="form-help">${this.isEditMode ? 'Version cannot be changed after creation' : 'Semantic version (e.g., 1.0.0)'}</small>
</div>
<div class="form-group">
<label for="firmware-file">Firmware File *</label>
<div class="file-input-wrapper">
<input
type="file"
id="firmware-file"
name="firmware"
accept=".bin,.hex"
>
<label for="firmware-file" class="file-input-label">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
<path d="M14 2v6h6"/>
</svg>
<span id="file-name">${this.isEditMode ? 'Current file: ' + (this.firmwareData?.name || 'unknown') + '.bin' : 'Choose firmware file...'}</span>
</label>
</div>
<small class="form-help">${this.isEditMode ? 'Select a new firmware file to update, or leave empty to update metadata only' : 'Binary firmware file (.bin or .hex)'}</small>
</div>
<div class="form-group">
<label>Labels</label>
<div class="labels-section">
<div class="add-label-controls">
<input type="text" id="label-key" placeholder="Key" class="label-key-input">
<span class="label-separator">:</span>
<input type="text" id="label-value" placeholder="Value" class="label-value-input">
<button type="button" id="add-label-btn" class="add-label-btn">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14">
<line x1="12" y1="5" x2="12" y2="19"/>
<line x1="5" y1="12" x2="19" y2="12"/>
</svg>
Add Label
</button>
</div>
<div id="labels-container" class="labels-container">
${labelsHTML}
</div>
</div>
<small class="form-help">Key-value pairs for categorizing firmware (e.g., platform: esp32, app: base)</small>
</div>
<div class="firmware-actions">
<button type="button" id="cancel-btn" class="config-btn">Cancel</button>
<button type="submit" class="config-btn">
${this.isEditMode ? 'Update Firmware' : 'Upload Firmware'}
</button>
</div>
</div>
`;
}
handleFileSelect(event) {
const file = event.target.files[0];
const fileNameSpan = this.findElement('#file-name');
if (file) {
fileNameSpan.textContent = file.name;
} else {
fileNameSpan.textContent = this.isEditMode ?
'Current file: ' + (this.firmwareData?.name || 'unknown') + '.bin' :
'Choose firmware file...';
}
}
addLabel() {
const keyInput = this.findElement('#label-key');
const valueInput = this.findElement('#label-value');
const labelsContainer = this.findElement('#labels-container');
const key = keyInput.value.trim();
const value = valueInput.value.trim();
if (!key || !value) {
this.showError('Please enter both key and value for the label');
return;
}
// Check if key already exists
const existingLabel = labelsContainer.querySelector(`[data-label-key="${this.escapeHtml(key)}"]`);
if (existingLabel) {
this.showError('A label with this key already exists');
return;
}
// Add the label
const labelHTML = `
<div class="label-item">
<span class="label-key">${this.escapeHtml(key)}</span>
<span class="label-value">${this.escapeHtml(value)}</span>
<button type="button" class="remove-label-btn" data-label-key="${this.escapeHtml(key)}" title="Remove label">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14">
<line x1="18" y1="6" x2="6" y2="18"/>
<line x1="6" y1="6" x2="18" y2="18"/>
</svg>
</button>
</div>
`;
labelsContainer.insertAdjacentHTML('beforeend', labelHTML);
// Clear inputs
keyInput.value = '';
valueInput.value = '';
}
removeLabel(key) {
const removeBtn = this.findElement(`.remove-label-btn[data-label-key="${this.escapeHtml(key)}"]`);
if (removeBtn) {
const labelItem = removeBtn.closest('.label-item');
if (labelItem) {
labelItem.remove();
}
}
}
async handleSubmit(event) {
event.preventDefault();
try {
const nameInput = this.findElement('#firmware-name');
const versionInput = this.findElement('#firmware-version');
const firmwareFile = this.findElement('#firmware-file').files[0];
const name = nameInput.value.trim();
const version = versionInput.value.trim();
if (!name || !version) {
this.showError('Name and version are required');
return;
}
// Only require file for new uploads, not for edit mode when keeping existing file
if (!this.isEditMode && (!firmwareFile || firmwareFile.size === 0)) {
this.showError('Please select a firmware file');
return;
}
// Collect labels
const labels = {};
const labelItems = this.findAllElements('.label-item');
labelItems.forEach(item => {
const key = item.querySelector('.label-key').textContent;
const value = item.querySelector('.label-value').textContent;
labels[key] = value;
});
// Prepare metadata
const metadata = {
name,
version,
labels
};
// Handle upload vs metadata-only update
if (this.isEditMode && (!firmwareFile || firmwareFile.size === 0)) {
// Metadata-only update
await window.apiClient.updateFirmwareMetadata(name, version, metadata);
} else {
// Full upload (new firmware or edit with new file)
await window.apiClient.uploadFirmwareToRegistry(metadata, firmwareFile);
}
this.showSuccess(this.isEditMode ? 'Firmware updated successfully' : 'Firmware uploaded successfully');
if (this.onSaveCallback) {
this.onSaveCallback();
}
} catch (error) {
logger.error('Firmware upload failed:', error);
this.showError('Upload failed: ' + error.message);
}
}
handleCancel() {
if (this.onCancelCallback) {
this.onCancelCallback();
}
}
showError(message) {
this.showNotification(message, 'error');
}
showSuccess(message) {
this.showNotification(message, 'success');
}
showNotification(message, type = 'info') {
// Remove any existing notifications
const existing = this.findElement('.form-notification');
if (existing) {
existing.remove();
}
const notification = document.createElement('div');
notification.className = `form-notification notification-${type}`;
notification.textContent = message;
this.container.insertBefore(notification, this.container.firstChild);
setTimeout(() => {
notification.classList.add('show');
}, 100);
setTimeout(() => {
notification.classList.remove('show');
setTimeout(() => {
if (notification.parentNode) {
notification.remove();
}
}, 300);
}, 3000);
}
escapeHtml(text) {
if (typeof text !== 'string') return text;
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
}
window.FirmwareFormComponent = FirmwareFormComponent;

View File

@@ -6,11 +6,11 @@ class FirmwareViewComponent extends Component {
logger.debug('FirmwareViewComponent: Constructor called');
logger.debug('FirmwareViewComponent: Container:', container);
const firmwareContainer = this.findElement('#firmware-container');
logger.debug('FirmwareViewComponent: Firmware container found:', !!firmwareContainer);
// Pass the entire firmware view container to the FirmwareComponent
logger.debug('FirmwareViewComponent: Using entire container for FirmwareComponent');
this.firmwareComponent = new FirmwareComponent(
firmwareContainer,
container,
viewModel,
eventBus
);
@@ -26,9 +26,6 @@ class FirmwareViewComponent extends Component {
// Mount sub-component
this.firmwareComponent.mount();
// Update available nodes
this.updateAvailableNodes();
logger.debug('FirmwareViewComponent: Mounted successfully');
}
@@ -67,16 +64,4 @@ class FirmwareViewComponent extends Component {
return false;
}
async updateAvailableNodes() {
try {
logger.debug('FirmwareViewComponent: updateAvailableNodes called');
const response = await window.apiClient.getClusterMembers();
const nodes = response.members || [];
logger.debug('FirmwareViewComponent: Got nodes:', nodes);
this.viewModel.updateAvailableNodes(nodes);
logger.debug('FirmwareViewComponent: Available nodes updated in view model');
} catch (error) {
logger.error('Failed to update available nodes:', error);
}
}
}

File diff suppressed because it is too large Load Diff

212
test/registry-integration-test.js Executable file
View File

@@ -0,0 +1,212 @@
#!/usr/bin/env node
/**
* Registry Integration Test
*
* Tests the registry API integration to ensure the firmware registry functionality works correctly
*/
const http = require('http');
const fs = require('fs');
const path = require('path');
const REGISTRY_URL = 'http://localhost:8080';
const TIMEOUT = 10000; // 10 seconds
function makeRequest(path, method = 'GET', body = null, isFormData = false) {
return new Promise((resolve, reject) => {
const options = {
hostname: 'localhost',
port: 8080,
path: path,
method: method,
headers: {}
};
if (body && !isFormData) {
options.headers['Content-Type'] = 'application/json';
}
const req = http.request(options, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
try {
const jsonData = JSON.parse(data);
resolve({ status: res.statusCode, data: jsonData });
} catch (error) {
resolve({ status: res.statusCode, data: data });
}
});
});
req.on('error', (error) => {
reject(error);
});
req.setTimeout(TIMEOUT, () => {
req.destroy();
reject(new Error('Request timeout'));
});
if (body) {
req.write(body);
}
req.end();
});
}
async function testRegistryHealth() {
console.log('Testing registry health endpoint...');
try {
const response = await makeRequest('/health');
if (response.status === 200 && response.data.status === 'healthy') {
console.log('✅ Registry health check passed');
return true;
} else {
console.log('❌ Registry health check failed:', response);
return false;
}
} catch (error) {
console.log('❌ Registry health check failed:', error.message);
return false;
}
}
async function testListFirmware() {
console.log('Testing list firmware endpoint...');
try {
const response = await makeRequest('/firmware');
if (response.status === 200 && Array.isArray(response.data)) {
console.log('✅ List firmware endpoint works, found', response.data.length, 'firmware entries');
return true;
} else {
console.log('❌ List firmware endpoint failed:', response);
return false;
}
} catch (error) {
console.log('❌ List firmware endpoint failed:', error.message);
return false;
}
}
async function testUploadFirmware() {
console.log('Testing upload firmware endpoint...');
// Create a small test firmware file
const testFirmwareContent = Buffer.from('test firmware content');
const metadata = {
name: 'test-firmware',
version: '1.0.0',
labels: {
platform: 'esp32',
app: 'test'
}
};
try {
// Create multipart form data
const boundary = '----formdata-test-boundary';
const formData = [
`--${boundary}`,
'Content-Disposition: form-data; name="metadata"',
'Content-Type: application/json',
'',
JSON.stringify(metadata),
`--${boundary}`,
'Content-Disposition: form-data; name="firmware"; filename="test.bin"',
'Content-Type: application/octet-stream',
'',
testFirmwareContent.toString(),
`--${boundary}--`
].join('\r\n');
const response = await makeRequest('/firmware', 'POST', formData, true);
if (response.status === 201 && response.data.success) {
console.log('✅ Upload firmware endpoint works');
return true;
} else {
console.log('❌ Upload firmware endpoint failed:', response);
return false;
}
} catch (error) {
console.log('❌ Upload firmware endpoint failed:', error.message);
return false;
}
}
async function testDownloadFirmware() {
console.log('Testing download firmware endpoint...');
try {
const response = await makeRequest('/firmware/test-firmware/1.0.0');
if (response.status === 200) {
console.log('✅ Download firmware endpoint works');
return true;
} else {
console.log('❌ Download firmware endpoint failed:', response);
return false;
}
} catch (error) {
console.log('❌ Download firmware endpoint failed:', error.message);
return false;
}
}
async function runTests() {
console.log('Starting Registry Integration Tests...\n');
const tests = [
{ name: 'Health Check', fn: testRegistryHealth },
{ name: 'List Firmware', fn: testListFirmware },
{ name: 'Upload Firmware', fn: testUploadFirmware },
{ name: 'Download Firmware', fn: testDownloadFirmware }
];
let passed = 0;
let total = tests.length;
for (const test of tests) {
console.log(`\n--- ${test.name} ---`);
try {
const result = await test.fn();
if (result) {
passed++;
}
} catch (error) {
console.log(`${test.name} failed with error:`, error.message);
}
}
console.log(`\n--- Test Results ---`);
console.log(`Passed: ${passed}/${total}`);
if (passed === total) {
console.log('🎉 All tests passed! Registry integration is working correctly.');
process.exit(0);
} else {
console.log('⚠️ Some tests failed. Please check the registry server.');
process.exit(1);
}
}
// Run tests if this script is executed directly
if (require.main === module) {
runTests().catch(error => {
console.error('Test runner failed:', error);
process.exit(1);
});
}
module.exports = {
testRegistryHealth,
testListFirmware,
testUploadFirmware,
testDownloadFirmware,
runTests
};