Files
CyberSim-OS/src/js/main.js
T

464 lines
17 KiB
JavaScript

/**
* CyberSim OS - Main Bootstrapper & Simulation Lifecycle Orchestrator (Phase 2.5)
*
* Coordinates deployment branding, runtime localization, immersive login,
* learner personalization, and scenario lifecycle.
*/
import { globalEventBus } from './engine/event_bus.js';
import { ConsequenceEngine } from './engine/consequence.js';
import { ScenarioState } from './engine/scenario_state.js';
import { ConditionEvaluator } from './engine/condition_evaluator.js';
import { EventScheduler } from './engine/event_scheduler.js';
import { ActionDispatcher } from './engine/action_dispatcher.js';
import { calculateScenarioFingerprint } from './scenario/fingerprint.js';
import { ScenarioLoader, getScenarioUrl } from './scenario/loader.js';
import { validateSchema } from './scenario/schema.js';
import { validateScenario } from './scenario/validator.js';
import { ScenarioDiagnostics, isDevelopmentMode } from './scenario/diagnostics.js';
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';
import { FilesApp } from './apps/files.js';
import { DocViewerApp } from './apps/docviewer.js';
import { SecurityCenterApp } from './apps/security_center.js';
import { BehavioralScorer } from './scoring/scorer.js';
import { AfterActionReport } from './scoring/aar.js';
import { CertificateGenerator } from './cert/cert_generator.js';
export class CyberSimEngine {
constructor() {
this.scenario = null;
this.scenarioFingerprint = null;
this.eventBus = globalEventBus;
this.scenarioState = null;
this.eventScheduler = null;
this.diagnostics = null;
this.currentLearnerFirstName = null;
}
showLoadingScreen(message = 'Loading scenario...') {
const desktop = document.getElementById('desktop');
if (!desktop) return;
desktop.innerHTML = `
<div style="display:flex; flex-direction:column; align-items:center; justify-content:center; height:100%; color:#94a3b8; font-family:system-ui, sans-serif;">
<div style="font-size:24px; font-weight:700; margin-bottom:8px; color:#e2e8f0;">CyberSim OS</div>
<div style="font-size:13px;">${message}</div>
<div style="margin-top:16px; width:200px; height:3px; background:#1e293b; border-radius:2px; overflow:hidden;">
<div style="width:40%; height:100%; background:#3b82f6; border-radius:2px; animation:loading 1.5s ease-in-out infinite;"></div>
</div>
</div>
<style>@keyframes loading { 0%{transform:translateX(-100%)} 100%{transform:translateX(350%)} }</style>
`;
}
showErrorScreen(errors, warnings) {
const desktop = document.getElementById('desktop');
if (!desktop) return;
const errorHtml = errors.map(e =>
`<div style="background:#450a0a; border:1px solid #7f1d1d; padding:8px 12px; border-radius:4px; font-size:12px; margin-bottom:6px;">
<strong style="color:#fca5a5;">[${e.field}]</strong> ${e.message}
${e.objectId ? `<span style="color:#94a3b8;"> (${e.objectId})</span>` : ''}
${e.expected ? `<div style="color:#6b7280; margin-top:2px;">Expected: ${e.expected}</div>` : ''}
</div>`
).join('');
const warningHtml = warnings.length > 0 ? warnings.slice(0, 10).map(w =>
`<div style="background:#422006; border:1px solid #78350f; padding:6px 10px; border-radius:4px; font-size:11px; margin-bottom:4px;">
<strong style="color:#fcd34d;">[${w.field}]</strong> ${w.message}
</div>`
).join('') : '';
desktop.innerHTML = `
<div style="display:flex; flex-direction:column; align-items:center; justify-content:center; height:100%; color:#e2e8f0; font-family:system-ui, sans-serif; padding:40px;">
<div style="max-width:640px; width:100%;">
<div style="font-size:20px; font-weight:700; margin-bottom:4px; color:#ef4444;">⚠ Scenario Validation Failed</div>
<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 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 simulation platform...');
// 1. Load deployment branding
await branding.load();
branding.applyBrandingStyles();
// 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
// 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);
this.showErrorScreen(loadResult.errors, loadResult.warnings);
return;
}
// Clone scenario to allow learner personalization
this.scenario = JSON.parse(JSON.stringify(loadResult.scenario));
if (loadResult.scenario._canonicalScenario) {
this.scenario._canonicalScenario = loadResult.scenario._canonicalScenario;
}
// 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);
const validationResult = validateScenario(this.scenario);
const allErrors = [...loadResult.errors, ...schemaResult.errors, ...validationResult.errors];
const allWarnings = [...loadResult.warnings, ...schemaResult.warnings, ...validationResult.warnings];
if (allErrors.length > 0) {
console.error(`[CyberSim OS] Scenario validation failed with ${allErrors.length} error(s)`);
this.showErrorScreen(allErrors, allWarnings);
return;
}
// Calculate canonical SHA-256 fingerprint
this.scenarioFingerprint = await calculateScenarioFingerprint(this.scenario);
console.log(`[CyberSim OS] Scenario Fingerprint (SHA-256): ${this.scenarioFingerprint}`);
// Diagnostics in dev mode
if (isDevelopmentMode()) {
this.diagnostics = new ScenarioDiagnostics({
scenario: this.scenario,
schemaResult,
validationResult
});
this.diagnostics.logToConsole();
}
// Initialize runtime scenario state
this.scenarioState = new ScenarioState(this.scenario, {
seed: this.scenario.seed
});
// Initialize UI
await this.initializeUI();
// 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,
scenario: this.scenario,
eventBus: this.eventBus,
notifications: this.notifications,
apps: {}
});
actionDispatcher.registerApps({
inlook: this.inlook,
navigator: this.navigator,
files: this.files,
docViewer: this.docViewer,
securityCenter: this.securityCenter
});
actionDispatcher.registerEndScenarioCallback(() => this.finishScenario());
this.eventScheduler = new EventScheduler(
this.scenario,
this.scenarioState,
conditionEvaluator,
actionDispatcher,
this.eventBus
);
this.consequences = new ConsequenceEngine(this.eventBus);
this.eventBus.on('*', (entry) => {
this.scenarioState.recordAction(entry);
});
this.eventBus.emit('SCENARIO_STARTED', {
target: this.scenario.id,
details: {
title: this.scenario.title,
fingerprint: this.scenarioFingerprint
}
});
this.eventScheduler.start(500);
if (this.diagnostics) {
this.diagnostics.setFingerprint(this.scenarioFingerprint);
this.diagnostics.setSeed(this.scenarioState.seed);
this.diagnostics.showOverlay();
}
window.CyberSimOS = {
engine: this,
navigator: this.navigator,
inlook: this.inlook,
files: this.files,
securityCenter: this.securityCenter,
docViewer: this.docViewer,
diagnostics: this.diagnostics,
state: this.scenarioState,
finishSimulation: () => this.finishScenario()
};
}
async initializeUI() {
const desktopEl = document.getElementById('desktop');
const taskbarAppsEl = document.getElementById('taskbar-apps');
const startMenuEl = document.getElementById('start-menu');
const startBtnEl = document.getElementById('start-btn');
const clockEl = document.getElementById('taskbar-clock');
desktopEl.innerHTML = '<div id="desktop-icons"></div>';
this.notifications = new NotificationService();
this.wm = new WindowManager(desktopEl, taskbarAppsEl);
this.docViewer = new DocViewerApp({
windowManager: this.wm,
eventBus: this.eventBus,
scenario: this.scenario
});
this.files = new FilesApp({
windowManager: this.wm,
eventBus: this.eventBus,
notifications: this.notifications,
scenario: this.scenario,
onOpenFile: (file) => this.docViewer.openDocument(file)
});
this.navigator = new NavigatorApp({
windowManager: this.wm,
eventBus: this.eventBus,
notifications: this.notifications,
scenario: this.scenario
});
this.securityCenter = new SecurityCenterApp({
windowManager: this.wm,
eventBus: this.eventBus,
scenario: this.scenario
});
this.inlook = new InlookApp({
windowManager: this.wm,
eventBus: this.eventBus,
notifications: this.notifications,
scenario: this.scenario,
onNavigateUrl: (url) => this.navigator.launch(url),
onOpenDoc: (file) => this.docViewer.openDocument(file),
onFileDownloaded: (file) => this.files.addFile(file)
});
this.desktop = new DesktopShell({
desktopElement: desktopEl,
startMenuElement: startMenuEl,
startBtnElement: startBtnEl,
clockElement: clockEl,
eventBus: this.eventBus,
onFinishScenario: () => this.finishScenario()
});
// Populate Start Menu user info
this._populateStartMenu();
const registeredApps = [
{
id: 'inlook',
name: i18n.t('apps.inlook'),
iconSvg: this.inlook.getIconSvg(),
launch: () => this.inlook.launch()
},
{
id: 'navigator',
name: i18n.t('apps.navigator'),
iconSvg: this.navigator.getIconSvg(),
launch: () => this.navigator.launch()
},
{
id: 'files',
name: i18n.t('apps.files'),
iconSvg: this.files.getIconSvg(),
launch: () => this.files.launch()
},
{
id: 'security_center',
name: i18n.t('apps.securityCenter'),
iconSvg: this.securityCenter.getIconSvg(),
launch: () => this.securityCenter.launch()
},
{
id: 'docviewer',
name: i18n.t('apps.docViewer'),
iconSvg: this.docViewer.getIconSvg(),
launch: () => {
const firstDoc = this.scenario.files && this.scenario.files[0];
if (firstDoc) this.docViewer.openDocument(firstDoc);
}
},
{
id: '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');
}
}
];
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() {
if (this.eventScheduler) {
this.eventScheduler.stop();
}
this.eventBus.emit('SCENARIO_FINISHED', {
target: this.scenario.id
});
const logs = this.eventBus.getLogs();
const scorer = new BehavioralScorer(this.scenario, logs, this.scenarioState);
const scoreResult = scorer.evaluate();
const aar = new AfterActionReport({
scenario: this.scenario,
scoreResult,
onClaimCertificate: () => {
const certGen = new CertificateGenerator(this.scenario, scoreResult, this.scenarioFingerprint);
const learnerName = this.scenario.learner ? this.scenario.learner.name : 'Learner';
certGen.showCertificateModal(learnerName);
},
onRestart: () => {
this.showLogin();
}
});
aar.show();
}
}
// Boot on DOM Ready
document.addEventListener('DOMContentLoaded', () => {
const sim = new CyberSimEngine();
sim.start();
});