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
|
- `dovecot.conf` template: Changed mail_uid/gid, first_valid_uid/last_valid_uid
|
||||||
- `configure_postfix`: Changed virtual_uid_maps/virtual_gid_maps
|
- `configure_postfix`: Changed virtual_uid_maps/virtual_gid_maps
|
||||||
- `cmd_add_user`: Changed passwd file uid:gid entries
|
- `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)
|
# 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
|
> **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
|
- Gossip-based exposure config sync via secubox-p2p
|
||||||
- Created `luci-app-vortex-dns` dashboard
|
- 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
|
- Zero-touch provisioning for new mesh devices without pre-shared tokens
|
||||||
- Hardware inventory collection (MAC, serial, model, CPU, RAM, storage)
|
- Hardware inventory collection (MAC, serial, model, CPU, RAM, storage)
|
||||||
- Profile-based configuration (7 profiles: default, enterprise, home-*, media-server, smart-home)
|
- Profile-based configuration (7 profiles: default, enterprise, home-*, media-server, smart-home)
|
||||||
|
|||||||
@ -190,6 +190,56 @@ var callScanNetwork = rpc.declare({
|
|||||||
expect: { devices: [] }
|
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
|
// Helpers
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@ -248,6 +298,12 @@ return view.extend({
|
|||||||
buildLogOffset: 0,
|
buildLogOffset: 0,
|
||||||
remotes: [],
|
remotes: [],
|
||||||
scannedDevices: [],
|
scannedDevices: [],
|
||||||
|
// Factory state
|
||||||
|
pendingDevices: [],
|
||||||
|
hwInventory: [],
|
||||||
|
profiles: [],
|
||||||
|
discoveryStatus: {},
|
||||||
|
generatedTokens: [],
|
||||||
|
|
||||||
load: function() {
|
load: function() {
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
@ -260,7 +316,12 @@ return view.extend({
|
|||||||
callStorageInfo().catch(function() { return {}; }),
|
callStorageInfo().catch(function() { return {}; }),
|
||||||
callHistoryList().catch(function() { return []; }),
|
callHistoryList().catch(function() { return []; }),
|
||||||
callSerialPorts().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.history = data[7] || [];
|
||||||
this.serialPorts = data[8] || [];
|
this.serialPorts = data[8] || [];
|
||||||
this.remotes = data[9] || [];
|
this.remotes = data[9] || [];
|
||||||
|
// Factory data
|
||||||
|
this.pendingDevices = data[10] || [];
|
||||||
|
this.hwInventory = data[11] || [];
|
||||||
|
this.profiles = data[12] || [];
|
||||||
|
this.discoveryStatus = data[13] || {};
|
||||||
|
|
||||||
var tabs = [
|
var tabs = [
|
||||||
{ id: 'overview', label: 'Overview', icon: '🎛️' },
|
{ id: 'overview', label: 'Overview', icon: '🎛️' },
|
||||||
|
{ id: 'factory', label: 'Factory', icon: '🏭' },
|
||||||
{ id: 'remotes', label: 'Remotes', icon: '🌐' },
|
{ id: 'remotes', label: 'Remotes', icon: '🌐' },
|
||||||
{ id: 'build', label: 'Build', icon: '🔨' },
|
{ id: 'build', label: 'Build', icon: '🔨' },
|
||||||
{ id: 'console', label: 'Console', icon: '📟' },
|
{ id: 'console', label: 'Console', icon: '📟' },
|
||||||
@ -335,6 +402,7 @@ return view.extend({
|
|||||||
|
|
||||||
renderTabContent: function() {
|
renderTabContent: function() {
|
||||||
switch (this.currentTab) {
|
switch (this.currentTab) {
|
||||||
|
case 'factory': return this.renderFactoryTab();
|
||||||
case 'remotes': return this.renderRemotesTab();
|
case 'remotes': return this.renderRemotesTab();
|
||||||
case 'build': return this.renderBuildTab();
|
case 'build': return this.renderBuildTab();
|
||||||
case 'console': return this.renderConsoleTab();
|
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
|
// Remotes Tab
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
@ -1393,14 +1768,24 @@ return view.extend({
|
|||||||
|
|
||||||
refresh: function() {
|
refresh: function() {
|
||||||
var self = this;
|
var self = this;
|
||||||
return Promise.all([
|
var promises = [
|
||||||
callGetStatus(),
|
callGetStatus(),
|
||||||
callListImages(),
|
callListImages(),
|
||||||
callListTokens(),
|
callListTokens(),
|
||||||
callListClones(),
|
callListClones(),
|
||||||
callGetBuildProgress().catch(function() { return {}; }),
|
callGetBuildProgress().catch(function() { return {}; }),
|
||||||
callStorageInfo().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.status = data[0] || {};
|
||||||
self.images = data[1] || [];
|
self.images = data[1] || [];
|
||||||
self.tokens = data[2] || [];
|
self.tokens = data[2] || [];
|
||||||
@ -1408,6 +1793,12 @@ return view.extend({
|
|||||||
self.buildProgress = data[4] || {};
|
self.buildProgress = data[4] || {};
|
||||||
self.storage = data[5] || {};
|
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
|
// Only update current tab content
|
||||||
var tabContent = document.getElementById('tab-content');
|
var tabContent = document.getElementById('tab-content');
|
||||||
if (tabContent) {
|
if (tabContent) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user