secubox-openwrt/package/secubox/luci-app-secubox/htdocs/luci-static/resources/view/secubox/wizard.js
CyberMind-FR 4b2241c86e feat(p2p): Add MirrorBox auto-init, self-recovery, and ACL fixes
MirrorBox Auto-Init:
- Add blockchain-like gigogne P2P structure with peer zero (P0) genesis
- Auto-create self-mesh on page load with configurable depth
- Preserve MirrorBox peers during refresh cycles

Self-Recovery System:
- Add secubox-restore script for bootstrapping new OpenWrt boxes
- Generate customized bootstrap.sh in Gitea backups
- Support interactive and command-line restore modes

ACL Fixes:
- Add missing deploy/pull methods to luci-app-secubox-p2p ACL
- Add luci.gitea and luci.secubox-p2p access to luci-app-secubox ACL
- Fix null display issue in hub.js (changed to empty string)

Backup Enhancements:
- Fix syntax error in RPCD heredoc (openwrt_version line)
- Add branch reference to Gitea API calls (main branch)
- Include bootstrap.sh and secubox-restore in backup push

Documentation:
- Add comprehensive README.md for SecuBox P2P module

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 19:46:28 +01:00

498 lines
18 KiB
JavaScript

'use strict';
'require view';
'require ui';
'require rpc';
'require secubox/api as API';
'require secubox-theme/theme as Theme';
'require secubox/nav as SecuNav';
'require secubox-portal/header as SbHeader';
// P2P Mesh RPC declarations
var callGetGiteaConfig = rpc.declare({
object: 'luci.secubox-p2p',
method: 'get_gitea_config',
expect: {}
});
var callSetGiteaConfig = rpc.declare({
object: 'luci.secubox-p2p',
method: 'set_gitea_config',
params: ['config'],
expect: { success: false }
});
var callCreateGiteaRepo = rpc.declare({
object: 'luci.secubox-p2p',
method: 'create_gitea_repo',
params: ['name', 'description', 'private', 'init_readme'],
expect: { success: false }
});
var callPushGiteaBackup = rpc.declare({
object: 'luci.secubox-p2p',
method: 'push_gitea_backup',
params: ['message', 'components'],
expect: { success: false }
});
var callGiteaGenerateToken = rpc.declare({
object: 'luci.gitea',
method: 'generate_token',
params: ['username', 'token_name', 'scopes'],
expect: {}
});
var callGiteaGetStatus = rpc.declare({
object: 'luci.gitea',
method: 'get_status',
expect: {}
});
// Load theme resources
document.head.appendChild(E('link', {
'rel': 'stylesheet',
'type': 'text/css',
'href': L.resource('secubox-theme/secubox-theme.css')
}));
var secuLang = (typeof L !== 'undefined' && L.env && L.env.lang) ||
(document.documentElement && document.documentElement.getAttribute('lang')) ||
(navigator.language ? navigator.language.split('-')[0] : 'en');
Theme.init({ language: secuLang });
var TIMEZONES = [
{ id: 'UTC', label: 'UTC' },
{ id: 'Europe/Paris', label: 'Europe/Paris' },
{ id: 'Europe/Berlin', label: 'Europe/Berlin' },
{ id: 'America/New_York', label: 'America/New_York' },
{ id: 'America/Los_Angeles', label: 'America/Los_Angeles' },
{ id: 'Asia/Singapore', label: 'Asia/Singapore' }
];
var NETWORK_MODES = [
{ id: 'router', label: _('Router (default)') },
{ id: 'dmz', label: _('Router + DMZ') }
];
return view.extend({
load: function() {
return Promise.all([
API.getFirstRunStatus(),
API.listApps(),
API.listProfiles(),
callGetGiteaConfig().catch(function() { return {}; }),
callGiteaGetStatus().catch(function() { return {}; })
]);
},
render: function(payload) {
console.log('[SecuBox Wizard] Received payload:', payload);
console.log('[SecuBox Wizard] Apps data:', payload[1]);
console.log('[SecuBox Wizard] Profiles data:', payload[2]);
this.firstRun = payload[0] || {};
// Handle both array and object formats
var allApps = Array.isArray(payload[1]) ? payload[1] : (payload[1] && payload[1].apps) || [];
// Filter to only show apps with wizards
this.appList = allApps.filter(function(app) { return app.has_wizard === true; });
this.profileList = Array.isArray(payload[2]) ? payload[2] : (payload[2] && payload[2].profiles) || [];
this.p2pConfig = payload[3] || {};
this.giteaStatus = payload[4] || {};
console.log('[SecuBox Wizard] Filtered appList (has_wizard only):', this.appList);
console.log('[SecuBox Wizard] Parsed profileList:', this.profileList);
console.log('[SecuBox Wizard] P2P Config:', this.p2pConfig);
console.log('[SecuBox Wizard] Gitea Status:', this.giteaStatus);
var container = E('div', { 'class': 'secubox-wizard-page' }, [
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/core/variables.css') }),
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox/common.css') }),
SecuNav.renderTabs('wizard'),
this.renderHeader(),
this.renderFirstRunCard(),
this.renderProfilesCard(),
this.renderAppsCard()
]);
var wrapper = E('div', { 'class': 'secubox-page-wrapper' });
wrapper.appendChild(SbHeader.render());
wrapper.appendChild(container);
return wrapper;
},
renderHeader: function() {
return E('div', { 'class': 'sh-page-header sh-page-header-lite' }, [
E('div', {}, [
E('h2', { 'class': 'sh-page-title' }, [
E('span', { 'class': 'sh-page-title-icon' }, '🧭'),
_('Setup Wizard')
]),
E('p', { 'class': 'sh-page-subtitle' }, _('Guide the first-run experience and configure apps with manifest-driven wizards.'))
])
]);
},
renderFirstRunCard: function() {
var self = this;
var data = this.firstRun || {};
var p2pData = this.p2pConfig || {};
var giteaData = this.giteaStatus || {};
var steps = [
{ icon: '🔐', label: _('Secure Admin Account'), description: _('Set a LuCI/root password to protect the router.'), complete: !!data.password_set, content: this.renderPasswordStep(data) },
{ icon: '🌍', label: _('Timezone & Locale'), description: _('Align system time with your region.'), complete: false, content: this.renderTimezoneStep(data) },
{ icon: '💾', label: _('Storage Path'), description: _('Choose where SecuBox apps store data (USB/NAS recommended).'), complete: !!data.storage_ready, content: this.renderStorageStep(data) },
{ icon: '🛡️', label: _('Network Mode'), description: _('Pick a default SecuBox network mode (router or DMZ).'), complete: false, content: this.renderModeStep(data) },
{ icon: '🌐', label: _('P2P Mesh Backup'), description: _('Auto-configure Gitea repository for mesh config versioning.'), complete: !!(p2pData.enabled && p2pData.repo_name), content: this.renderP2PMeshStep(p2pData, giteaData) }
];
return E('div', { 'class': 'sb-wizard-card' }, [
E('div', { 'class': 'sb-wizard-title' }, ['🧩 ', _('First-run Checklist')]),
E('div', { 'class': 'sb-wizard-steps' }, steps.map(function(step) {
return E('div', { 'class': 'sb-wizard-step' + (step.complete ? ' complete' : '') }, [
E('div', { 'class': 'sb-wizard-step-header' }, [
E('span', { 'class': 'sb-wizard-step-icon' }, step.icon),
E('div', {}, [
E('div', { 'class': 'sb-wizard-step-label' }, step.label),
E('div', { 'class': 'sb-wizard-step-desc' }, step.description)
])
]),
E('div', { 'class': 'sb-wizard-step-body' }, [step.content])
]);
}, this))
]);
},
renderPasswordStep: function(data) {
return E('div', { 'class': 'sb-wizard-inline' }, [
E('div', { 'class': 'sb-wizard-status ' + (data.password_set ? 'ok' : 'warn') }, data.password_set ? _('Password set') : _('Password missing')),
E('a', {
'class': 'cbi-button cbi-button-action',
'href': L.url('admin', 'system', 'admin')
}, _('Open password page'))
]);
},
renderTimezoneStep: function(data) {
var selected = data.timezone || 'UTC';
var select = E('select', { 'class': 'sb-wizard-select', 'id': 'wizard-timezone' }, TIMEZONES.map(function(zone) {
return E('option', { 'value': zone.id, 'selected': zone.id === selected }, zone.label);
}));
return E('div', { 'class': 'sb-wizard-inline' }, [
select,
E('button', {
'class': 'cbi-button cbi-button-action',
'click': this.applyTimezone.bind(this)
}, _('Apply'))
]);
},
renderStorageStep: function(data) {
var input = E('input', {
'class': 'sb-wizard-input',
'id': 'wizard-storage',
'value': data.storage_path || '/srv/secubox'
});
return E('div', { 'class': 'sb-wizard-inline' }, [
input,
E('button', {
'class': 'cbi-button cbi-button-action',
'click': this.prepareStorage.bind(this)
}, _('Prepare'))
]);
},
renderModeStep: function(data) {
var select = E('select', { 'class': 'sb-wizard-select', 'id': 'wizard-network-mode' }, NETWORK_MODES.map(function(mode) {
return E('option', { 'value': mode.id, 'selected': mode.id === data.network_mode }, mode.label);
}));
return E('div', { 'class': 'sb-wizard-inline' }, [
select,
E('button', {
'class': 'cbi-button cbi-button-action',
'click': this.applyNetworkMode.bind(this)
}, _('Switch'))
]);
},
renderP2PMeshStep: function(p2pData, giteaData) {
var self = this;
var giteaRunning = giteaData && giteaData.result && giteaData.result.running;
var repoConfigured = p2pData && p2pData.enabled && p2pData.repo_name;
if (repoConfigured) {
return E('div', { 'class': 'sb-wizard-inline' }, [
E('div', { 'class': 'sb-wizard-status ok' }, [
_('Repository: '),
E('strong', {}, (p2pData.repo_owner || 'user') + '/' + p2pData.repo_name)
]),
E('a', {
'class': 'cbi-button cbi-button-action',
'href': L.url('admin', 'secubox', 'p2p-hub')
}, _('Open P2P Hub'))
]);
}
if (!giteaRunning) {
return E('div', { 'class': 'sb-wizard-inline' }, [
E('div', { 'class': 'sb-wizard-status warn' }, _('Gitea not running')),
E('a', {
'class': 'cbi-button cbi-button-action',
'href': L.url('admin', 'services', 'gitea')
}, _('Start Gitea'))
]);
}
// Gitea running but repo not configured - show auto-setup
return E('div', { 'class': 'sb-wizard-inline' }, [
E('div', { 'class': 'sb-wizard-status warn' }, _('Not configured')),
E('button', {
'class': 'cbi-button cbi-button-action',
'id': 'btn-p2p-auto-setup',
'click': function() { self.autoSetupP2PMesh(); }
}, _('🚀 Auto Setup'))
]);
},
autoSetupP2PMesh: function() {
var self = this;
var btn = document.getElementById('btn-p2p-auto-setup');
if (btn) btn.disabled = true;
ui.showModal(_('P2P Mesh Auto Setup'), [
E('div', { 'class': 'spinning', 'style': 'margin: 20px auto;' }),
E('p', { 'id': 'p2p-setup-status', 'style': 'text-align: center;' }, _('Initializing...'))
]);
var updateStatus = function(msg) {
var el = document.getElementById('p2p-setup-status');
if (el) el.textContent = msg;
};
var hostname = (this.firstRun && this.firstRun.hostname) || 'secubox';
var repoName = hostname.toLowerCase().replace(/[^a-z0-9-]/g, '-') + '-p2p';
// Step 1: Generate token with proper scopes
updateStatus(_('Generating access token...'));
callGiteaGenerateToken('gandalf', repoName + '-token', 'write:repository,write:user,read:user')
.then(function(result) {
if (!result || !result.result || !result.result.token) {
throw new Error(_('Failed to generate token'));
}
var token = result.result.token;
// Step 2: Save token to P2P config
updateStatus(_('Saving configuration...'));
return callSetGiteaConfig({
server_url: 'http://localhost:3000',
repo_name: repoName,
access_token: token,
enabled: 1,
auto_backup: 1
}).then(function() { return token; });
})
.then(function(token) {
// Step 3: Create repository
updateStatus(_('Creating repository: ') + repoName);
return callCreateGiteaRepo(repoName, 'SecuBox P2P Mesh configuration', true, true);
})
.then(function(result) {
if (!result || !result.success) {
throw new Error(result && result.error ? result.error : _('Failed to create repository'));
}
// Step 4: Push initial config
updateStatus(_('Pushing initial configuration...'));
return callPushGiteaBackup('Initial mesh configuration from wizard', {});
})
.then(function(result) {
ui.hideModal();
if (result && result.success) {
ui.addNotification(null, E('p', {}, _('🎉 P2P Mesh setup complete! Repository created and initial config pushed.')), 'info');
setTimeout(function() { window.location.reload(); }, 1500);
} else {
throw new Error(_('Failed to push configuration'));
}
})
.catch(function(err) {
ui.hideModal();
ui.addNotification(null, E('p', {}, '❌ ' + (err.message || err)), 'error');
if (btn) btn.disabled = false;
});
},
renderAppsCard: function() {
var apps = this.appList || [];
return E('div', { 'class': 'sb-wizard-card' }, [
E('div', { 'class': 'sb-wizard-title' }, ['📦 ', _('App Wizards')]),
apps.length ? E('div', { 'class': 'sb-app-grid' }, apps.map(this.renderAppCard, this)) :
E('div', { 'class': 'secubox-empty-state' }, [
E('div', { 'class': 'secubox-empty-icon' }, '📭'),
E('div', { 'class': 'secubox-empty-title' }, _('No manifests detected')),
E('div', { 'class': 'secubox-empty-text' }, _('Install manifests under /usr/share/secubox/plugins/.'))
])
]);
},
renderProfilesCard: function() {
var profiles = this.profileList || [];
return E('div', { 'class': 'sb-wizard-card' }, [
E('div', { 'class': 'sb-wizard-title' }, ['🧱 ', _('Profiles')]),
profiles.length ? E('div', { 'class': 'sb-app-grid' }, profiles.map(this.renderProfileCard, this)) :
E('div', { 'class': 'secubox-empty-state' }, [
E('div', { 'class': 'secubox-empty-icon' }, '📭'),
E('div', { 'class': 'secubox-empty-title' }, _('No profiles available')),
E('div', { 'class': 'secubox-empty-text' }, _('Profiles are stored in /usr/share/secubox/profiles/.'))
]),
profiles.length ? E('div', { 'class': 'right', 'style': 'margin-top:12px;' }, [
E('button', {
'class': 'cbi-button cbi-button-action',
'click': this.rollbackProfile.bind(this)
}, _('Rollback last profile'))
]) : ''
]);
},
renderProfileCard: function(profile) {
var apps = profile.apps || [];
return E('div', { 'class': 'sb-app-card' }, [
E('div', { 'class': 'sb-app-card-info' }, [
E('div', { 'class': 'sb-app-name' }, [profile.name || profile.id]),
E('div', { 'class': 'sb-app-desc' }, profile.description || ''),
E('div', { 'class': 'sb-app-desc' }, _('Network mode: %s').format(profile.network_mode || '—')),
apps.length ? E('div', { 'class': 'sb-app-desc' }, _('Apps: %s').format(apps.join(', '))) : ''
]),
E('div', { 'class': 'sb-app-actions' }, [
E('span', { 'class': 'sb-app-state' + (profile.state === 'installed' ? ' ok' : '') }, profile.state || 'n/a'),
E('button', {
'class': 'cbi-button cbi-button-action',
'click': this.applyProfile.bind(this, profile.id)
}, _('Apply'))
])
]);
},
renderAppCard: function(app) {
return E('div', { 'class': 'sb-app-card' }, [
E('div', { 'class': 'sb-app-card-info' }, [
E('div', { 'class': 'sb-app-name' }, [app.name || app.id, app.version ? E('span', { 'class': 'sb-app-version' }, 'v' + app.version) : '']),
E('div', { 'class': 'sb-app-desc' }, app.description || '')
]),
E('div', { 'class': 'sb-app-actions' }, [
E('span', { 'class': 'sb-app-state' + (app.state === 'installed' ? ' ok' : '') }, app.state || 'n/a'),
(app.has_wizard ? E('button', { 'class': 'cbi-button cbi-button-action', 'click': this.openAppWizard.bind(this, app) }, _('Configure')) : '')
])
]);
},
applyTimezone: function(ev) {
var tz = document.getElementById('wizard-timezone').value;
API.applyFirstRun({ timezone: tz }).then(this.reloadPage).catch(this.showError);
},
prepareStorage: function(ev) {
var path = document.getElementById('wizard-storage').value.trim();
if (!path) {
ui.addNotification(null, E('p', {}, _('Storage path is required')), 'error');
return;
}
API.applyFirstRun({ storage_path: path }).then(this.reloadPage).catch(this.showError);
},
applyNetworkMode: function(ev) {
var mode = document.getElementById('wizard-network-mode').value;
API.applyFirstRun({ network_mode: mode }).then(this.reloadPage).catch(this.showError);
},
reloadPage: function() {
ui.hideModal();
window.location.reload();
},
showError: function(err) {
ui.hideModal();
ui.addNotification(null, E('p', {}, err && err.message ? err.message : err), 'error');
},
openAppWizard: function(app, ev) {
var self = this;
ui.showModal(_('Loading %s wizard…').format(app.name || app.id), [E('div', { 'class': 'spinning' })]);
API.getAppManifest(app.id).then(function(manifest) {
ui.hideModal();
manifest = manifest || {};
var wizard = manifest.wizard || {};
var fields = wizard.fields || [];
if (!fields.length) {
ui.addNotification(null, E('p', {}, _('No wizard metadata for this app.')), 'warn');
return;
}
var form = E('div', { 'class': 'sb-app-wizard-form' }, fields.map(function(field) {
return E('div', { 'class': 'sb-form-group' }, [
E('label', {}, field.label || field.id),
E('input', {
'class': 'sb-wizard-input',
'name': field.id,
'type': field.type || 'text',
'placeholder': field.placeholder || ''
})
]);
}));
ui.showModal(_('Configure %s').format(app.name || app.id), [
form,
E('div', { 'class': 'right', 'style': 'margin-top:16px;' }, [
E('button', {
'class': 'cbi-button cbi-button-cancel',
'click': ui.hideModal
}, _('Cancel')),
E('button', {
'class': 'cbi-button cbi-button-action',
'click': function() {
self.submitAppWizard(app.id, form, fields);
}
}, _('Apply'))
])
]);
}).catch(this.showError);
},
submitAppWizard: function(appId, form, fields) {
var values = {};
fields.forEach(function(field) {
var input = form.querySelector('[name="' + field.id + '"]');
if (input && input.value !== '')
values[field.id] = input.value;
});
ui.showModal(_('Saving…'), [E('div', { 'class': 'spinning' })]);
API.applyAppWizard(appId, values).then(function(result) {
ui.hideModal();
if (result && result.success) {
ui.addNotification(null, E('p', {}, _('Wizard applied.')), 'info');
} else {
ui.addNotification(null, E('p', {}, _('Failed to apply wizard.')), 'error');
}
}).catch(this.showError);
},
applyProfile: function(profileId) {
if (!profileId)
return;
ui.showModal(_('Applying profile…'), [E('div', { 'class': 'spinning' })]);
API.applyProfile(profileId).then(function(result) {
ui.hideModal();
if (result && result.success) {
ui.addNotification(null, E('p', {}, _('Profile applied. A reboot may be required.')), 'info');
} else {
ui.addNotification(null, E('p', {}, (result && result.error) || _('Failed to apply profile')), 'error');
}
}).catch(this.showError);
},
rollbackProfile: function() {
ui.showModal(_('Rolling back…'), [E('div', { 'class': 'spinning' })]);
API.rollbackProfile().then(function(result) {
ui.hideModal();
if (result && result.success) {
ui.addNotification(null, E('p', {}, result.message || _('Rollback completed.')), 'info');
} else {
ui.addNotification(null, E('p', {}, (result && result.error) || _('Rollback failed.')), 'error');
}
}).catch(this.showError);
}
});