generated from Labyricorn/labyricorn-project-template
Implement CyberSim Phase 2.5: Localization, Branding, and Immersive Login
This commit is contained in:
+145
-54
@@ -1,8 +1,8 @@
|
||||
/**
|
||||
* CyberSim OS - Main Bootstrapper & Simulation Lifecycle Orchestrator (Phase 2)
|
||||
* CyberSim OS - Main Bootstrapper & Simulation Lifecycle Orchestrator (Phase 2.5)
|
||||
*
|
||||
* Loads a scenario package from a URL, validates it, initializes the engine,
|
||||
* and orchestrates the simulation lifecycle.
|
||||
* Coordinates deployment branding, runtime localization, immersive login,
|
||||
* learner personalization, and scenario lifecycle.
|
||||
*/
|
||||
|
||||
import { globalEventBus } from './engine/event_bus.js';
|
||||
@@ -19,6 +19,9 @@ import { ScenarioDiagnostics, isDevelopmentMode } from './scenario/diagnostics.j
|
||||
import { WindowManager } from './core/window_manager.js';
|
||||
import { DesktopShell } from './core/desktop.js';
|
||||
import { NotificationService } from './core/notifications.js';
|
||||
import { i18n } from './core/i18n.js';
|
||||
import { branding } from './core/branding.js';
|
||||
import { LoginScreen } from './core/login.js';
|
||||
|
||||
import { InlookApp } from './apps/inlook.js';
|
||||
import { NavigatorApp } from './apps/navigator.js';
|
||||
@@ -30,7 +33,7 @@ import { BehavioralScorer } from './scoring/scorer.js';
|
||||
import { AfterActionReport } from './scoring/aar.js';
|
||||
import { CertificateGenerator } from './cert/cert_generator.js';
|
||||
|
||||
class CyberSimEngine {
|
||||
export class CyberSimEngine {
|
||||
constructor() {
|
||||
this.scenario = null;
|
||||
this.scenarioFingerprint = null;
|
||||
@@ -38,11 +41,9 @@ class CyberSimEngine {
|
||||
this.scenarioState = null;
|
||||
this.eventScheduler = null;
|
||||
this.diagnostics = null;
|
||||
this.currentLearnerFirstName = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a loading screen while the scenario loads.
|
||||
*/
|
||||
showLoadingScreen(message = 'Loading scenario...') {
|
||||
const desktop = document.getElementById('desktop');
|
||||
if (!desktop) return;
|
||||
@@ -58,9 +59,6 @@ class CyberSimEngine {
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a validation error screen.
|
||||
*/
|
||||
showErrorScreen(errors, warnings) {
|
||||
const desktop = document.getElementById('desktop');
|
||||
if (!desktop) return;
|
||||
@@ -86,22 +84,77 @@ class CyberSimEngine {
|
||||
<div style="font-size:13px; color:#94a3b8; margin-bottom:16px;">${errors.length} error(s), ${warnings.length} warning(s)</div>
|
||||
<div style="max-height:400px; overflow-y:auto; margin-bottom:16px;">${errorHtml}</div>
|
||||
${warningHtml ? `<div style="margin-bottom:16px;"><div style="font-size:11px; color:#94a3b8; margin-bottom:6px;">Warnings:</div>${warningHtml}</div>` : ''}
|
||||
<button onclick="window.location.reload()" style="background:#3b82f6; color:white; border:none; padding:8px 20px; border-radius:4px; cursor:pointer; font-size:13px;">Reload</button>
|
||||
<button id="btn-reload-error" style="background:#3b82f6; color:white; border:none; padding:8px 20px; border-radius:4px; cursor:pointer; font-size:13px;">${i18n.t('common.reload')}</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const reloadBtn = desktop.querySelector('#btn-reload-error');
|
||||
if (reloadBtn) {
|
||||
reloadBtn.addEventListener('click', () => this.showLogin());
|
||||
}
|
||||
}
|
||||
|
||||
async start() {
|
||||
console.log('[CyberSim OS] Booting enterprise desktop simulation...');
|
||||
console.log('[CyberSim OS] Booting enterprise simulation platform...');
|
||||
|
||||
// Phase 1: Load scenario
|
||||
this.showLoadingScreen('Loading scenario package...');
|
||||
// 1. Load deployment branding
|
||||
await branding.load();
|
||||
branding.applyBrandingStyles();
|
||||
|
||||
const scenarioUrl = getScenarioUrl();
|
||||
console.log(`[CyberSim OS] Loading scenario from: ${scenarioUrl}`);
|
||||
// 2. Initialize i18n
|
||||
const locConfig = branding.getLocalizationConfig();
|
||||
i18n.defaultLocale = locConfig.defaultLocale || 'en';
|
||||
i18n.enabledLocales = locConfig.enabledLocales || ['en', 'es'];
|
||||
await i18n.loadLocale(i18n.defaultLocale);
|
||||
await i18n.loadLocale('es'); // Preload demonstration locale
|
||||
|
||||
const loadResult = await ScenarioLoader.load(scenarioUrl);
|
||||
// Check for query parameters bypass (e.g. ?autostart=true)
|
||||
const params = (typeof window !== 'undefined' && window.location) ? new URLSearchParams(window.location.search) : null;
|
||||
const directScenario = params ? params.get('scenario') : null;
|
||||
const autoStart = params ? params.get('autostart') === 'true' : false;
|
||||
|
||||
if (directScenario && autoStart) {
|
||||
await this.launchScenario({
|
||||
firstName: params.get('name') || 'Chris',
|
||||
scenarioPath: directScenario,
|
||||
locale: params.get('locale') || i18n.defaultLocale
|
||||
});
|
||||
} else {
|
||||
this.showLogin();
|
||||
}
|
||||
}
|
||||
|
||||
showLogin() {
|
||||
const desktopEl = document.getElementById('desktop');
|
||||
const taskbarEl = document.getElementById('taskbar');
|
||||
const startMenuEl = document.getElementById('start-menu');
|
||||
|
||||
// Hide desktop shell during login
|
||||
if (taskbarEl) taskbarEl.style.display = 'none';
|
||||
if (startMenuEl) startMenuEl.style.display = 'none';
|
||||
|
||||
const login = new LoginScreen({
|
||||
containerElement: desktopEl,
|
||||
onConnect: (connectData) => this.launchScenario(connectData)
|
||||
});
|
||||
|
||||
login.render();
|
||||
}
|
||||
|
||||
async launchScenario({ firstName, scenarioId, scenarioPath, locale }) {
|
||||
this.currentLearnerFirstName = firstName;
|
||||
|
||||
// Set active locale
|
||||
await i18n.setLocale(locale || i18n.defaultLocale);
|
||||
|
||||
// Show loading screen
|
||||
this.showLoadingScreen(i18n.t('desktop.loading'));
|
||||
|
||||
const scenarioUrl = scenarioPath || getScenarioUrl();
|
||||
console.log(`[CyberSim OS] Loading scenario from: ${scenarioUrl} (Locale: ${locale})`);
|
||||
|
||||
const loadResult = await ScenarioLoader.load(scenarioUrl, { locale });
|
||||
|
||||
if (!loadResult.scenario) {
|
||||
console.error('[CyberSim OS] Failed to load scenario:', loadResult.errors);
|
||||
@@ -109,16 +162,28 @@ class CyberSimEngine {
|
||||
return;
|
||||
}
|
||||
|
||||
this.scenario = loadResult.scenario;
|
||||
// Clone scenario to allow learner personalization
|
||||
this.scenario = JSON.parse(JSON.stringify(loadResult.scenario));
|
||||
if (loadResult.scenario._canonicalScenario) {
|
||||
this.scenario._canonicalScenario = loadResult.scenario._canonicalScenario;
|
||||
}
|
||||
|
||||
// Phase 2: Validate schema
|
||||
this.showLoadingScreen('Validating scenario schema...');
|
||||
// Personalize learner state with first name
|
||||
if (!this.scenario.learner) {
|
||||
this.scenario.learner = { name: firstName, role: 'Employee', email: `${firstName.toLowerCase()}@example.internal` };
|
||||
}
|
||||
this.scenario.learner.firstName = firstName;
|
||||
// Personalize full name with entered first name
|
||||
if (this.scenario.learner.name) {
|
||||
const parts = this.scenario.learner.name.split(' ');
|
||||
const lastName = parts.length > 1 ? parts.slice(1).join(' ') : '';
|
||||
this.scenario.learner.name = lastName ? `${firstName} ${lastName}` : firstName;
|
||||
}
|
||||
|
||||
// Validate schema
|
||||
const schemaResult = validateSchema(this.scenario);
|
||||
|
||||
// Phase 3: Validate references and consistency
|
||||
const validationResult = validateScenario(this.scenario);
|
||||
|
||||
// Merge all diagnostics
|
||||
const allErrors = [...loadResult.errors, ...schemaResult.errors, ...validationResult.errors];
|
||||
const allWarnings = [...loadResult.warnings, ...schemaResult.warnings, ...validationResult.warnings];
|
||||
|
||||
@@ -128,15 +193,11 @@ class CyberSimEngine {
|
||||
return;
|
||||
}
|
||||
|
||||
if (allWarnings.length > 0) {
|
||||
console.warn(`[CyberSim OS] Scenario loaded with ${allWarnings.length} warning(s)`);
|
||||
}
|
||||
|
||||
// Phase 4: Calculate fingerprint
|
||||
// Calculate canonical SHA-256 fingerprint
|
||||
this.scenarioFingerprint = await calculateScenarioFingerprint(this.scenario);
|
||||
console.log(`[CyberSim OS] Scenario Fingerprint (SHA-256): ${this.scenarioFingerprint}`);
|
||||
|
||||
// Phase 5: Initialize diagnostics
|
||||
// Diagnostics in dev mode
|
||||
if (isDevelopmentMode()) {
|
||||
this.diagnostics = new ScenarioDiagnostics({
|
||||
scenario: this.scenario,
|
||||
@@ -146,16 +207,21 @@ class CyberSimEngine {
|
||||
this.diagnostics.logToConsole();
|
||||
}
|
||||
|
||||
// Phase 6: Initialize scenario runtime state
|
||||
// Initialize runtime scenario state
|
||||
this.scenarioState = new ScenarioState(this.scenario, {
|
||||
seed: this.scenario.seed
|
||||
});
|
||||
|
||||
// Phase 7: Initialize UI
|
||||
this.showLoadingScreen('Initializing desktop...');
|
||||
// Initialize UI
|
||||
await this.initializeUI();
|
||||
|
||||
// Phase 8: Start event scheduler
|
||||
// Show taskbar & start menu
|
||||
const taskbarEl = document.getElementById('taskbar');
|
||||
const startMenuEl = document.getElementById('start-menu');
|
||||
if (taskbarEl) taskbarEl.style.display = 'flex';
|
||||
if (startMenuEl) startMenuEl.style.display = 'flex';
|
||||
|
||||
// Start event scheduler
|
||||
const conditionEvaluator = new ConditionEvaluator(this.scenarioState);
|
||||
const actionDispatcher = new ActionDispatcher({
|
||||
scenarioState: this.scenarioState,
|
||||
@@ -165,7 +231,6 @@ class CyberSimEngine {
|
||||
apps: {}
|
||||
});
|
||||
|
||||
// Register apps with the action dispatcher
|
||||
actionDispatcher.registerApps({
|
||||
inlook: this.inlook,
|
||||
navigator: this.navigator,
|
||||
@@ -184,15 +249,12 @@ class CyberSimEngine {
|
||||
this.eventBus
|
||||
);
|
||||
|
||||
// Initialize consequence tracking
|
||||
this.consequences = new ConsequenceEngine(this.eventBus);
|
||||
|
||||
// Bridge learner actions from the event bus to scenario state
|
||||
this.eventBus.on('*', (entry) => {
|
||||
this.scenarioState.recordAction(entry);
|
||||
});
|
||||
|
||||
// Emit scenario start
|
||||
this.eventBus.emit('SCENARIO_STARTED', {
|
||||
target: this.scenario.id,
|
||||
details: {
|
||||
@@ -201,17 +263,14 @@ class CyberSimEngine {
|
||||
}
|
||||
});
|
||||
|
||||
// Start the event scheduler tick loop
|
||||
this.eventScheduler.start(500);
|
||||
|
||||
// Show diagnostics overlay in dev mode
|
||||
if (this.diagnostics) {
|
||||
this.diagnostics.setFingerprint(this.scenarioFingerprint);
|
||||
this.diagnostics.setSeed(this.scenarioState.seed);
|
||||
this.diagnostics.showOverlay();
|
||||
}
|
||||
|
||||
// Expose runtime global for form event triggers and diagnostics
|
||||
window.CyberSimOS = {
|
||||
engine: this,
|
||||
navigator: this.navigator,
|
||||
@@ -232,17 +291,15 @@ class CyberSimEngine {
|
||||
const startBtnEl = document.getElementById('start-btn');
|
||||
const clockEl = document.getElementById('taskbar-clock');
|
||||
|
||||
// Reset desktop content from loading screen
|
||||
desktopEl.innerHTML = '<div id="desktop-icons"></div>';
|
||||
|
||||
// Initialize Core Services
|
||||
this.notifications = new NotificationService();
|
||||
this.wm = new WindowManager(desktopEl, taskbarAppsEl);
|
||||
|
||||
// Initialize Apps — pass scenario data generically
|
||||
this.docViewer = new DocViewerApp({
|
||||
windowManager: this.wm,
|
||||
eventBus: this.eventBus
|
||||
eventBus: this.eventBus,
|
||||
scenario: this.scenario
|
||||
});
|
||||
|
||||
this.files = new FilesApp({
|
||||
@@ -276,7 +333,6 @@ class CyberSimEngine {
|
||||
onFileDownloaded: (file) => this.files.addFile(file)
|
||||
});
|
||||
|
||||
// Initialize Desktop Shell
|
||||
this.desktop = new DesktopShell({
|
||||
desktopElement: desktopEl,
|
||||
startMenuElement: startMenuEl,
|
||||
@@ -286,35 +342,37 @@ class CyberSimEngine {
|
||||
onFinishScenario: () => this.finishScenario()
|
||||
});
|
||||
|
||||
// Register Desktop Apps — configuration driven
|
||||
// Populate Start Menu user info
|
||||
this._populateStartMenu();
|
||||
|
||||
const registeredApps = [
|
||||
{
|
||||
id: 'inlook',
|
||||
name: 'Inlook Mail',
|
||||
name: i18n.t('apps.inlook'),
|
||||
iconSvg: this.inlook.getIconSvg(),
|
||||
launch: () => this.inlook.launch()
|
||||
},
|
||||
{
|
||||
id: 'navigator',
|
||||
name: 'Navigator',
|
||||
name: i18n.t('apps.navigator'),
|
||||
iconSvg: this.navigator.getIconSvg(),
|
||||
launch: () => this.navigator.launch()
|
||||
},
|
||||
{
|
||||
id: 'files',
|
||||
name: 'Files',
|
||||
name: i18n.t('apps.files'),
|
||||
iconSvg: this.files.getIconSvg(),
|
||||
launch: () => this.files.launch()
|
||||
},
|
||||
{
|
||||
id: 'security_center',
|
||||
name: 'Security Center',
|
||||
name: i18n.t('apps.securityCenter'),
|
||||
iconSvg: this.securityCenter.getIconSvg(),
|
||||
launch: () => this.securityCenter.launch()
|
||||
},
|
||||
{
|
||||
id: 'docviewer',
|
||||
name: 'Doc Viewer',
|
||||
name: i18n.t('apps.docViewer'),
|
||||
iconSvg: this.docViewer.getIconSvg(),
|
||||
launch: () => {
|
||||
const firstDoc = this.scenario.files && this.scenario.files[0];
|
||||
@@ -323,7 +381,7 @@ class CyberSimEngine {
|
||||
},
|
||||
{
|
||||
id: 'verify_cert',
|
||||
name: 'Verify Cert',
|
||||
name: i18n.t('apps.verifyCert'),
|
||||
iconSvg: `<svg viewBox="0 0 24 24" fill="none" stroke="#8b5cf6" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/></svg>`,
|
||||
launch: () => {
|
||||
window.open('verify.html', '_blank');
|
||||
@@ -334,8 +392,41 @@ class CyberSimEngine {
|
||||
this.desktop.renderDesktopIcons(registeredApps);
|
||||
}
|
||||
|
||||
_populateStartMenu() {
|
||||
const s = this.scenario;
|
||||
if (!s) return;
|
||||
|
||||
const avatarEl = document.getElementById('start-user-avatar');
|
||||
const nameEl = document.getElementById('start-user-name');
|
||||
const roleEl = document.getElementById('start-user-role');
|
||||
const objectivesEl = document.getElementById('start-objectives');
|
||||
const orgNameEl = document.getElementById('taskbar-org-name');
|
||||
const networkEl = document.getElementById('taskbar-network-icon');
|
||||
|
||||
const org = branding.getOrganization();
|
||||
const orgName = (s.organizations && s.organizations[0]) ? s.organizations[0].name : org.name;
|
||||
|
||||
if (s.learner && avatarEl) {
|
||||
const initials = s.learner.name.split(' ').map(n => n[0]).join('');
|
||||
avatarEl.textContent = initials;
|
||||
}
|
||||
if (s.learner && nameEl) nameEl.textContent = s.learner.name;
|
||||
|
||||
const roleLine = s.learner ? `${s.learner.role} \u2022 ${orgName}` : orgName;
|
||||
if (roleEl) roleEl.textContent = roleLine;
|
||||
if (orgNameEl) orgNameEl.textContent = org.shortName || orgName;
|
||||
if (networkEl) networkEl.title = `${i18n.t('desktop.networkProtected')} (${orgName})`;
|
||||
|
||||
if (s.objectives && objectivesEl) {
|
||||
const items = s.objectives.map(obj => {
|
||||
const text = typeof obj === 'string' ? obj : obj.text;
|
||||
return `\u2022 ${text}`;
|
||||
});
|
||||
objectivesEl.innerHTML = items.join('<br>');
|
||||
}
|
||||
}
|
||||
|
||||
finishScenario() {
|
||||
// Stop the event scheduler
|
||||
if (this.eventScheduler) {
|
||||
this.eventScheduler.stop();
|
||||
}
|
||||
@@ -357,7 +448,7 @@ class CyberSimEngine {
|
||||
certGen.showCertificateModal(learnerName);
|
||||
},
|
||||
onRestart: () => {
|
||||
window.location.reload();
|
||||
this.showLogin();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user