feat(cloner): Add Factory Dashboard for zero-touch provisioning
Add Factory tab to Cloning Station with: - Discovery Mode toggle (enable/disable zero-touch provisioning) - Pending Devices list with approve/reject and profile assignment - Bulk Token Generator (1-50 tokens with profile selection) - Hardware Inventory table (MAC, Model, CPU, RAM, Storage) Implementation: - 8 RPC declarations for factory methods - 5 state properties for factory data - 5 render functions, 6 event handlers - Factory data polling in 5-second refresh cycle when on tab - KISS theme UI components throughout Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
d43855b3d1
commit
ea9a86d485
@ -3498,3 +3498,27 @@ git checkout HEAD -- index.html
|
||||
- `dovecot.conf` template: Changed mail_uid/gid, first_valid_uid/last_valid_uid
|
||||
- `configure_postfix`: Changed virtual_uid_maps/virtual_gid_maps
|
||||
- `cmd_add_user`: Changed passwd file uid:gid entries
|
||||
|
||||
28. **Factory Dashboard LuCI Implementation (2026-02-25)**
|
||||
- Added Factory tab to Cloning Station (`luci-app-cloner/overview.js`)
|
||||
- **Features:**
|
||||
- Discovery Mode Toggle: Enable/disable zero-touch provisioning with visual status
|
||||
- Pending Devices: List and approve/reject devices awaiting provisioning with profile assignment
|
||||
- Bulk Token Generator: Generate multiple tokens at once with profile selection
|
||||
- Hardware Inventory: Table view of discovered device specs (MAC, Model, CPU, RAM, Storage)
|
||||
- **RPC Declarations Added:**
|
||||
- `callPendingDevices`, `callApproveDevice`, `callRejectDevice`
|
||||
- `callBulkTokens`, `callInventory`, `callListProfiles`
|
||||
- `callDiscoveryStatus`, `callToggleDiscovery`
|
||||
- **State Properties Added:**
|
||||
- `pendingDevices`, `hwInventory`, `profiles`, `discoveryStatus`, `generatedTokens`
|
||||
- **Render Functions Added:**
|
||||
- `renderFactoryTab()`: Main tab with stats grid and two-column layout
|
||||
- `renderPendingDevices()`: Device cards with approve/reject buttons
|
||||
- `renderGeneratedTokens()`: Token list with copy functionality
|
||||
- `renderInventory()`: Kiss-table with hardware specs
|
||||
- **Event Handlers Added:**
|
||||
- `handleToggleDiscovery()`, `handleApproveDevice()`, `handleRejectDevice()`
|
||||
- `handleGenerateBulkTokens()`, `handleCopyAllTokens()`, `refreshFactory()`
|
||||
- **Polling:** Factory data included in 5-second refresh when on Factory tab
|
||||
- **UI Pattern:** KISS theme components (stat boxes, cards, tables, buttons)
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# Work In Progress (Claude)
|
||||
|
||||
_Last updated: 2026-02-24 (Factory Auto-Provisioning)_
|
||||
_Last updated: 2026-02-25 (Factory Dashboard LuCI)_
|
||||
|
||||
> **Architecture Reference**: SecuBox Fanzine v3 — Les 4 Couches
|
||||
|
||||
@ -62,9 +62,19 @@ _Last updated: 2026-02-24 (Factory Auto-Provisioning)_
|
||||
- Gossip-based exposure config sync via secubox-p2p
|
||||
- Created `luci-app-vortex-dns` dashboard
|
||||
|
||||
### Just Completed (2026-02-24)
|
||||
### Just Completed (2026-02-25)
|
||||
|
||||
- **Factory Auto-Provisioning** — DONE (2026-02-24)
|
||||
- **Factory Dashboard LuCI** — DONE (2026-02-25)
|
||||
- Added Factory tab to Cloning Station (`luci-app-cloner/overview.js`)
|
||||
- Discovery Mode Toggle with visual status (🟢 ON / 🔴 OFF)
|
||||
- Pending Devices list with approve/reject and profile assignment
|
||||
- Bulk Token Generator with profile selection
|
||||
- Hardware Inventory table (MAC, Model, CPU, RAM, Storage)
|
||||
- 8 RPC declarations, 5 state properties, 5 render functions, 6 event handlers
|
||||
- Polling: Factory data included in 5-second refresh when on tab
|
||||
- UI Pattern: KISS theme components (stat boxes, cards, tables, buttons)
|
||||
|
||||
- **Factory Auto-Provisioning Backend** — DONE (2026-02-24)
|
||||
- Zero-touch provisioning for new mesh devices without pre-shared tokens
|
||||
- Hardware inventory collection (MAC, serial, model, CPU, RAM, storage)
|
||||
- Profile-based configuration (7 profiles: default, enterprise, home-*, media-server, smart-home)
|
||||
|
||||
@ -190,6 +190,56 @@ var callScanNetwork = rpc.declare({
|
||||
expect: { devices: [] }
|
||||
});
|
||||
|
||||
// Factory Auto-Provisioning RPC
|
||||
var callPendingDevices = rpc.declare({
|
||||
object: 'luci.cloner',
|
||||
method: 'pending_devices',
|
||||
expect: { devices: [] }
|
||||
});
|
||||
|
||||
var callApproveDevice = rpc.declare({
|
||||
object: 'luci.cloner',
|
||||
method: 'approve_device',
|
||||
params: ['device_id', 'profile']
|
||||
});
|
||||
|
||||
var callRejectDevice = rpc.declare({
|
||||
object: 'luci.cloner',
|
||||
method: 'reject_device',
|
||||
params: ['device_id', 'reason']
|
||||
});
|
||||
|
||||
var callBulkTokens = rpc.declare({
|
||||
object: 'luci.cloner',
|
||||
method: 'bulk_tokens',
|
||||
params: ['count', 'profile', 'ttl'],
|
||||
expect: { tokens: [] }
|
||||
});
|
||||
|
||||
var callInventory = rpc.declare({
|
||||
object: 'luci.cloner',
|
||||
method: 'inventory',
|
||||
expect: { inventory: [] }
|
||||
});
|
||||
|
||||
var callListProfiles = rpc.declare({
|
||||
object: 'luci.cloner',
|
||||
method: 'list_profiles',
|
||||
expect: { profiles: [] }
|
||||
});
|
||||
|
||||
var callDiscoveryStatus = rpc.declare({
|
||||
object: 'luci.cloner',
|
||||
method: 'discovery_status',
|
||||
expect: { }
|
||||
});
|
||||
|
||||
var callToggleDiscovery = rpc.declare({
|
||||
object: 'luci.cloner',
|
||||
method: 'toggle_discovery',
|
||||
params: ['enabled']
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Helpers
|
||||
// ============================================================================
|
||||
@ -248,6 +298,12 @@ return view.extend({
|
||||
buildLogOffset: 0,
|
||||
remotes: [],
|
||||
scannedDevices: [],
|
||||
// Factory state
|
||||
pendingDevices: [],
|
||||
hwInventory: [],
|
||||
profiles: [],
|
||||
discoveryStatus: {},
|
||||
generatedTokens: [],
|
||||
|
||||
load: function() {
|
||||
return Promise.all([
|
||||
@ -260,7 +316,12 @@ return view.extend({
|
||||
callStorageInfo().catch(function() { return {}; }),
|
||||
callHistoryList().catch(function() { return []; }),
|
||||
callSerialPorts().catch(function() { return []; }),
|
||||
callListRemotes().catch(function() { return []; })
|
||||
callListRemotes().catch(function() { return []; }),
|
||||
// Factory data
|
||||
callPendingDevices().catch(function() { return []; }),
|
||||
callInventory().catch(function() { return []; }),
|
||||
callListProfiles().catch(function() { return []; }),
|
||||
callDiscoveryStatus().catch(function() { return {}; })
|
||||
]);
|
||||
},
|
||||
|
||||
@ -276,9 +337,15 @@ return view.extend({
|
||||
this.history = data[7] || [];
|
||||
this.serialPorts = data[8] || [];
|
||||
this.remotes = data[9] || [];
|
||||
// Factory data
|
||||
this.pendingDevices = data[10] || [];
|
||||
this.hwInventory = data[11] || [];
|
||||
this.profiles = data[12] || [];
|
||||
this.discoveryStatus = data[13] || {};
|
||||
|
||||
var tabs = [
|
||||
{ id: 'overview', label: 'Overview', icon: '🎛️' },
|
||||
{ id: 'factory', label: 'Factory', icon: '🏭' },
|
||||
{ id: 'remotes', label: 'Remotes', icon: '🌐' },
|
||||
{ id: 'build', label: 'Build', icon: '🔨' },
|
||||
{ id: 'console', label: 'Console', icon: '📟' },
|
||||
@ -335,6 +402,7 @@ return view.extend({
|
||||
|
||||
renderTabContent: function() {
|
||||
switch (this.currentTab) {
|
||||
case 'factory': return this.renderFactoryTab();
|
||||
case 'remotes': return this.renderRemotesTab();
|
||||
case 'build': return this.renderBuildTab();
|
||||
case 'console': return this.renderConsoleTab();
|
||||
@ -515,6 +583,313 @@ return view.extend({
|
||||
]);
|
||||
},
|
||||
|
||||
// ========================================================================
|
||||
// Factory Tab
|
||||
// ========================================================================
|
||||
|
||||
renderFactoryTab: function() {
|
||||
var self = this;
|
||||
var disco = this.discoveryStatus || {};
|
||||
var pendingCount = this.pendingDevices.length;
|
||||
var inventoryCount = this.hwInventory.length;
|
||||
var profileCount = this.profiles.length;
|
||||
|
||||
return E('div', {}, [
|
||||
// Stats Grid
|
||||
E('div', { 'class': 'kiss-grid kiss-grid-4', 'style': 'margin-bottom:24px;' }, [
|
||||
KissTheme.stat(disco.enabled ? '🟢 ON' : '🔴 OFF', 'Discovery', disco.enabled ? 'var(--kiss-green)' : 'var(--kiss-red)'),
|
||||
KissTheme.stat(pendingCount, 'Pending', pendingCount > 0 ? 'var(--kiss-yellow)' : 'var(--kiss-muted)'),
|
||||
KissTheme.stat(inventoryCount, 'Inventory', 'var(--kiss-blue)'),
|
||||
KissTheme.stat(profileCount, 'Profiles', 'var(--kiss-purple)')
|
||||
]),
|
||||
|
||||
// Two column layout
|
||||
E('div', { 'class': 'kiss-grid kiss-grid-2', 'style': 'margin-bottom:16px;' }, [
|
||||
// Discovery Mode Toggle
|
||||
KissTheme.card([
|
||||
E('span', {}, '🔍 Discovery Mode')
|
||||
], E('div', { 'style': 'display:flex;flex-direction:column;gap:12px;' }, [
|
||||
E('div', { 'style': 'display:flex;align-items:center;gap:12px;' }, [
|
||||
E('span', { 'style': 'font-size:40px;' }, disco.enabled ? '🟢' : '🔴'),
|
||||
E('div', {}, [
|
||||
E('div', { 'style': 'font-weight:600;font-size:16px;' }, disco.enabled ? 'Zero-Touch Active' : 'Discovery Disabled'),
|
||||
E('div', { 'style': 'font-size:12px;color:var(--kiss-muted);' }, disco.enabled ? 'Listening for new devices' : 'No auto-provisioning')
|
||||
])
|
||||
]),
|
||||
E('div', { 'style': 'display:flex;gap:8px;' }, [
|
||||
E('button', {
|
||||
'class': 'kiss-btn kiss-btn-green',
|
||||
'disabled': disco.enabled,
|
||||
'style': disco.enabled ? 'opacity:0.5;' : '',
|
||||
'click': function() { self.handleToggleDiscovery(true); }
|
||||
}, '▶️ Enable'),
|
||||
E('button', {
|
||||
'class': 'kiss-btn kiss-btn-red',
|
||||
'disabled': !disco.enabled,
|
||||
'style': !disco.enabled ? 'opacity:0.5;' : '',
|
||||
'click': function() { self.handleToggleDiscovery(false); }
|
||||
}, '⏹️ Disable')
|
||||
]),
|
||||
disco.last_scan ? E('div', { 'style': 'font-size:11px;color:var(--kiss-muted);' }, 'Last scan: ' + fmtRelative(disco.last_scan)) : null
|
||||
].filter(Boolean))),
|
||||
|
||||
// Bulk Token Generator
|
||||
KissTheme.card([
|
||||
E('span', {}, '🎟️ Bulk Token Generator')
|
||||
], E('div', { 'style': 'display:flex;flex-direction:column;gap:12px;' }, [
|
||||
E('div', { 'style': 'display:flex;gap:12px;align-items:center;flex-wrap:wrap;' }, [
|
||||
E('input', {
|
||||
'id': 'bulk-token-count',
|
||||
'type': 'number',
|
||||
'min': '1',
|
||||
'max': '50',
|
||||
'value': '10',
|
||||
'placeholder': 'Count',
|
||||
'style': 'padding:10px;background:var(--kiss-bg2);border:1px solid var(--kiss-line);border-radius:6px;color:var(--kiss-text);width:80px;'
|
||||
}),
|
||||
E('select', {
|
||||
'id': 'bulk-token-profile',
|
||||
'style': 'padding:10px;background:var(--kiss-bg2);border:1px solid var(--kiss-line);border-radius:6px;color:var(--kiss-text);min-width:150px;'
|
||||
}, this.profiles.length ?
|
||||
this.profiles.map(function(p) { return E('option', { 'value': p.id }, p.name); }) :
|
||||
[E('option', { 'value': 'default' }, 'Default Profile')]
|
||||
),
|
||||
E('button', {
|
||||
'class': 'kiss-btn kiss-btn-blue',
|
||||
'click': function() { self.handleGenerateBulkTokens(); }
|
||||
}, '🎟️ Generate')
|
||||
]),
|
||||
E('div', { 'id': 'generated-tokens-container' }, this.renderGeneratedTokens())
|
||||
]))
|
||||
]),
|
||||
|
||||
// Pending Devices
|
||||
KissTheme.card([
|
||||
E('span', {}, '⏳ Pending Devices'),
|
||||
E('span', { 'style': 'margin-left:auto;font-size:12px;color:var(--kiss-muted);' }, pendingCount + ' awaiting approval')
|
||||
], E('div', { 'id': 'pending-devices-container' }, this.renderPendingDevices())),
|
||||
|
||||
// Hardware Inventory
|
||||
KissTheme.card([
|
||||
E('span', {}, '📦 Hardware Inventory'),
|
||||
E('span', { 'style': 'margin-left:auto;font-size:12px;color:var(--kiss-muted);' }, inventoryCount + ' devices')
|
||||
], E('div', { 'id': 'inventory-container' }, this.renderInventory()))
|
||||
]);
|
||||
},
|
||||
|
||||
renderPendingDevices: function() {
|
||||
var self = this;
|
||||
|
||||
if (!this.pendingDevices.length) {
|
||||
return E('div', { 'style': 'text-align:center;padding:30px;color:var(--kiss-muted);' }, [
|
||||
E('div', { 'style': 'font-size:32px;margin-bottom:8px;' }, '✅'),
|
||||
E('div', {}, 'No pending devices'),
|
||||
E('div', { 'style': 'font-size:12px;margin-top:4px;' }, 'New devices will appear here for approval')
|
||||
]);
|
||||
}
|
||||
|
||||
return E('div', { 'style': 'display:flex;flex-direction:column;gap:8px;' },
|
||||
this.pendingDevices.map(function(dev) {
|
||||
return E('div', {
|
||||
'style': 'display:flex;align-items:center;gap:12px;padding:12px;background:var(--kiss-bg2);border-radius:8px;border:1px solid var(--kiss-line);'
|
||||
}, [
|
||||
E('span', { 'style': 'font-size:24px;' }, '📱'),
|
||||
E('div', { 'style': 'flex:1;' }, [
|
||||
E('div', { 'style': 'font-weight:600;' }, dev.hostname || 'Unknown Device'),
|
||||
E('div', { 'style': 'font-size:12px;color:var(--kiss-muted);display:flex;gap:12px;' }, [
|
||||
E('span', {}, '🔗 ' + (dev.mac || '-')),
|
||||
E('span', {}, '📍 ' + (dev.ip || '-')),
|
||||
E('span', {}, '📱 ' + (dev.model || 'Unknown'))
|
||||
])
|
||||
]),
|
||||
E('select', {
|
||||
'class': 'device-profile-select',
|
||||
'data-device-id': dev.id,
|
||||
'style': 'padding:6px;background:var(--kiss-bg2);border:1px solid var(--kiss-line);border-radius:4px;color:var(--kiss-text);font-size:12px;'
|
||||
}, self.profiles.length ?
|
||||
self.profiles.map(function(p) { return E('option', { 'value': p.id }, p.name); }) :
|
||||
[E('option', { 'value': 'default' }, 'Default')]
|
||||
),
|
||||
E('button', {
|
||||
'class': 'kiss-btn kiss-btn-green',
|
||||
'style': 'padding:6px 12px;font-size:12px;',
|
||||
'data-device-id': dev.id,
|
||||
'click': function(ev) { self.handleApproveDevice(ev); }
|
||||
}, '✅'),
|
||||
E('button', {
|
||||
'class': 'kiss-btn kiss-btn-red',
|
||||
'style': 'padding:6px 12px;font-size:12px;',
|
||||
'data-device-id': dev.id,
|
||||
'click': function(ev) { self.handleRejectDevice(ev); }
|
||||
}, '❌')
|
||||
]);
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
renderGeneratedTokens: function() {
|
||||
if (!this.generatedTokens.length) {
|
||||
return E('div', { 'style': 'text-align:center;padding:16px;color:var(--kiss-muted);font-size:12px;' },
|
||||
'Generate tokens to see them here');
|
||||
}
|
||||
|
||||
var self = this;
|
||||
return E('div', { 'style': 'display:flex;flex-direction:column;gap:6px;' }, [
|
||||
E('div', { 'style': 'display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;' }, [
|
||||
E('span', { 'style': 'font-size:12px;color:var(--kiss-muted);' }, this.generatedTokens.length + ' tokens generated'),
|
||||
E('button', {
|
||||
'class': 'kiss-btn',
|
||||
'style': 'padding:4px 8px;font-size:11px;',
|
||||
'click': function() { self.handleCopyAllTokens(); }
|
||||
}, '📋 Copy All')
|
||||
]),
|
||||
E('div', { 'style': 'max-height:120px;overflow-y:auto;display:flex;flex-direction:column;gap:4px;' },
|
||||
this.generatedTokens.map(function(tok) {
|
||||
return E('div', {
|
||||
'style': 'font-family:monospace;font-size:11px;padding:6px 8px;background:var(--kiss-bg);border-radius:4px;color:var(--kiss-cyan);word-break:break-all;'
|
||||
}, tok);
|
||||
})
|
||||
)
|
||||
]);
|
||||
},
|
||||
|
||||
renderInventory: function() {
|
||||
if (!this.hwInventory.length) {
|
||||
return E('div', { 'style': 'text-align:center;padding:30px;color:var(--kiss-muted);' }, [
|
||||
E('div', { 'style': 'font-size:32px;margin-bottom:8px;' }, '📦'),
|
||||
E('div', {}, 'No devices in inventory'),
|
||||
E('div', { 'style': 'font-size:12px;margin-top:4px;' }, 'Discovered hardware will appear here')
|
||||
]);
|
||||
}
|
||||
|
||||
return E('table', { 'class': 'kiss-table' }, [
|
||||
E('thead', {}, E('tr', {}, [
|
||||
E('th', {}, 'ID'),
|
||||
E('th', {}, 'MAC'),
|
||||
E('th', {}, 'Model'),
|
||||
E('th', {}, 'CPU'),
|
||||
E('th', {}, 'RAM'),
|
||||
E('th', {}, 'Storage'),
|
||||
E('th', {}, 'Collected')
|
||||
])),
|
||||
E('tbody', {}, this.hwInventory.map(function(dev) {
|
||||
return E('tr', {}, [
|
||||
E('td', { 'style': 'font-family:monospace;font-size:11px;' }, dev.id || '-'),
|
||||
E('td', { 'style': 'font-family:monospace;font-size:11px;' }, dev.mac || '-'),
|
||||
E('td', {}, dev.model || '-'),
|
||||
E('td', {}, dev.cpu || '-'),
|
||||
E('td', {}, dev.ram ? fmtSize(dev.ram) : '-'),
|
||||
E('td', {}, dev.storage ? fmtSize(dev.storage) : '-'),
|
||||
E('td', { 'style': 'font-size:11px;' }, dev.collected ? fmtRelative(dev.collected) : '-')
|
||||
]);
|
||||
}))
|
||||
]);
|
||||
},
|
||||
|
||||
handleToggleDiscovery: function(enabled) {
|
||||
var self = this;
|
||||
callToggleDiscovery(enabled).then(function(res) {
|
||||
if (res.success) {
|
||||
ui.addNotification(null, E('p', enabled ? 'Discovery mode enabled' : 'Discovery mode disabled'), 'info');
|
||||
self.refreshFactory();
|
||||
} else {
|
||||
ui.addNotification(null, E('p', res.error || 'Failed to toggle discovery'), 'error');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
handleApproveDevice: function(ev) {
|
||||
var self = this;
|
||||
var deviceId = ev.currentTarget.dataset.deviceId;
|
||||
var row = ev.currentTarget.closest('div[style*="background:var(--kiss-bg2)"]');
|
||||
var profileSelect = row ? row.querySelector('.device-profile-select') : null;
|
||||
var profile = profileSelect ? profileSelect.value : 'default';
|
||||
|
||||
callApproveDevice(deviceId, profile).then(function(res) {
|
||||
if (res.success) {
|
||||
ui.addNotification(null, E('p', 'Device approved and provisioned'), 'info');
|
||||
self.refreshFactory();
|
||||
} else {
|
||||
ui.addNotification(null, E('p', res.error || 'Approval failed'), 'error');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
handleRejectDevice: function(ev) {
|
||||
var self = this;
|
||||
var deviceId = ev.currentTarget.dataset.deviceId;
|
||||
|
||||
if (confirm('Reject this device? It will need to reconnect to request provisioning again.')) {
|
||||
callRejectDevice(deviceId, 'Manual rejection').then(function(res) {
|
||||
if (res.success) {
|
||||
ui.addNotification(null, E('p', 'Device rejected'), 'info');
|
||||
self.refreshFactory();
|
||||
} else {
|
||||
ui.addNotification(null, E('p', res.error || 'Rejection failed'), 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
handleGenerateBulkTokens: function() {
|
||||
var self = this;
|
||||
var countEl = document.getElementById('bulk-token-count');
|
||||
var profileEl = document.getElementById('bulk-token-profile');
|
||||
var count = parseInt(countEl ? countEl.value : '10', 10);
|
||||
var profile = profileEl ? profileEl.value : 'default';
|
||||
|
||||
if (count < 1 || count > 50) {
|
||||
ui.addNotification(null, E('p', 'Count must be between 1 and 50'), 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
ui.addNotification(null, E('p', 'Generating ' + count + ' tokens...'), 'info');
|
||||
|
||||
callBulkTokens(count, profile, 86400).then(function(tokens) {
|
||||
self.generatedTokens = tokens || [];
|
||||
var container = document.getElementById('generated-tokens-container');
|
||||
if (container) {
|
||||
dom.content(container, self.renderGeneratedTokens());
|
||||
}
|
||||
if (self.generatedTokens.length) {
|
||||
ui.addNotification(null, E('p', 'Generated ' + self.generatedTokens.length + ' tokens'), 'info');
|
||||
} else {
|
||||
ui.addNotification(null, E('p', 'No tokens generated'), 'warning');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
handleCopyAllTokens: function() {
|
||||
if (!this.generatedTokens.length) return;
|
||||
var text = this.generatedTokens.join('\n');
|
||||
navigator.clipboard.writeText(text).then(function() {
|
||||
ui.addNotification(null, E('p', 'All tokens copied to clipboard'), 'info');
|
||||
});
|
||||
},
|
||||
|
||||
refreshFactory: function() {
|
||||
var self = this;
|
||||
return Promise.all([
|
||||
callPendingDevices().catch(function() { return []; }),
|
||||
callInventory().catch(function() { return []; }),
|
||||
callListProfiles().catch(function() { return []; }),
|
||||
callDiscoveryStatus().catch(function() { return {}; })
|
||||
]).then(function(data) {
|
||||
self.pendingDevices = data[0] || [];
|
||||
self.hwInventory = data[1] || [];
|
||||
self.profiles = data[2] || [];
|
||||
self.discoveryStatus = data[3] || {};
|
||||
|
||||
// Re-render Factory tab if active
|
||||
if (self.currentTab === 'factory') {
|
||||
var tabContent = document.getElementById('tab-content');
|
||||
if (tabContent) {
|
||||
dom.content(tabContent, self.renderFactoryTab());
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// ========================================================================
|
||||
// Remotes Tab
|
||||
// ========================================================================
|
||||
@ -1393,14 +1768,24 @@ return view.extend({
|
||||
|
||||
refresh: function() {
|
||||
var self = this;
|
||||
return Promise.all([
|
||||
var promises = [
|
||||
callGetStatus(),
|
||||
callListImages(),
|
||||
callListTokens(),
|
||||
callListClones(),
|
||||
callGetBuildProgress().catch(function() { return {}; }),
|
||||
callStorageInfo().catch(function() { return {}; })
|
||||
]).then(function(data) {
|
||||
];
|
||||
|
||||
// Include factory data when on factory tab
|
||||
if (self.currentTab === 'factory') {
|
||||
promises.push(
|
||||
callPendingDevices().catch(function() { return []; }),
|
||||
callDiscoveryStatus().catch(function() { return {}; })
|
||||
);
|
||||
}
|
||||
|
||||
return Promise.all(promises).then(function(data) {
|
||||
self.status = data[0] || {};
|
||||
self.images = data[1] || [];
|
||||
self.tokens = data[2] || [];
|
||||
@ -1408,6 +1793,12 @@ return view.extend({
|
||||
self.buildProgress = data[4] || {};
|
||||
self.storage = data[5] || {};
|
||||
|
||||
// Factory data if available
|
||||
if (self.currentTab === 'factory' && data.length > 6) {
|
||||
self.pendingDevices = data[6] || [];
|
||||
self.discoveryStatus = data[7] || {};
|
||||
}
|
||||
|
||||
// Only update current tab content
|
||||
var tabContent = document.getElementById('tab-content');
|
||||
if (tabContent) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user