Files
Bjorn/web/js/app.js
Fabien POLLY eb20b168a6 Add RLUtils class for managing RL/AI dashboard endpoints
- Implemented methods for fetching AI stats, training history, and recent experiences.
- Added functionality to set operation mode (MANUAL, AUTO, AI) with appropriate handling.
- Included helper methods for querying the database and sending JSON responses.
- Integrated model metadata extraction for visualization purposes.
2026-02-18 22:36:10 +01:00

721 lines
25 KiB
JavaScript

/**
* app.js — SPA bootstrap.
* Initializes core modules, registers routes, starts the router.
* Wires shell UI: console, quickpanel, actions, settings, launcher, pollers.
*/
import * as router from './core/router.js';
import * as i18n from './core/i18n.js';
import * as theme from './core/theme.js';
import { api, Poller } from './core/api.js';
import { $, el, setText, toast } from './core/dom.js';
import * as consoleSSE from './core/console-sse.js';
import * as quickpanel from './core/quickpanel.js';
import * as actions from './core/actions.js';
import * as settingsConfig from './core/settings-config.js';
/* =========================================
* 1) Initialize core modules
* ========================================= */
// Theme: apply saved CSS vars immediately (no flash)
theme.init();
// i18n: load translations, then boot UI
i18n.init().then(() => {
bootUI();
}).catch(err => {
console.error('[App] i18n init failed:', err);
bootUI(); // Boot anyway with fallback keys
});
function bootUI() {
// Runtime i18n wrappers for legacy hardcoded dialogs.
if (!window.__bjornDialogsPatched) {
const nativeConfirm = window.confirm.bind(window);
const nativePrompt = window.prompt.bind(window);
window.confirm = (msg) => nativeConfirm(i18n.trLoose(String(msg ?? '')));
window.prompt = (msg, def = '') => nativePrompt(i18n.trLoose(String(msg ?? '')), def);
window.__bjornDialogsPatched = true;
}
/* =========================================
* 2) Register all routes (lazy-loaded)
* ========================================= */
router.route('/dashboard', () => import('./pages/dashboard.js'));
router.route('/netkb', () => import('./pages/netkb.js'));
router.route('/network', () => import('./pages/network.js'));
router.route('/credentials', () => import('./pages/credentials.js'));
router.route('/vulnerabilities', () => import('./pages/vulnerabilities.js'));
router.route('/attacks', () => import('./pages/attacks.js'));
router.route('/scheduler', () => import('./pages/scheduler.js'));
router.route('/database', () => import('./pages/database.js'));
router.route('/files', () => import('./pages/files.js'));
router.route('/loot', () => import('./pages/loot.js'));
router.route('/actions', () => import('./pages/actions.js'));
router.route('/actions-studio', () => import('./pages/actions-studio.js'));
router.route('/backup', () => import('./pages/backup.js'));
router.route('/web-enum', () => import('./pages/web-enum.js'));
router.route('/zombieland', () => import('./pages/zombieland.js'));
router.route('/ai-dashboard', () => import('./pages/rl-dashboard.js?t=' + Date.now()));
router.route('/bjorn-debug', () => import('./pages/bjorn-debug.js'));
router.route('/bjorn', () => import('./pages/bjorn.js'));
// 404 fallback
router.setNotFound((container, path) => {
container.appendChild(
el('div', { class: 'not-found' }, [
el('h2', {}, [i18n.t('common.notFound')]),
el('p', {}, [`${i18n.t('common.notFound')}: ${path}`]),
el('a', { href: '#/dashboard' }, [i18n.t('nav.dashboard')])
])
);
});
/* =========================================
* 3) Mount language selector in topbar
* ========================================= */
const langContainer = $('#langSelect');
if (langContainer) {
i18n.mountLangSelector(langContainer);
}
/* =========================================
* 4) Initialize router (reads hash, loads first page)
* ========================================= */
const appContainer = $('#app');
router.init(appContainer);
window.addEventListener('i18n:changed', () => {
i18n.updateDOM(document);
router.reloadCurrent?.();
});
/* =========================================
* 5) Wire up topbar buttons
* ========================================= */
wireTopbar();
/* =========================================
* 6) Start global pollers (status, character, say)
* ========================================= */
ensureBjornProgress();
startGlobalPollers();
/* =========================================
* 7) Wire page launcher overlay
* ========================================= */
wireLauncher();
/* =========================================
* 8) Initialize shell modules
* ========================================= */
consoleSSE.init();
quickpanel.init();
actions.init();
/* =========================================
* 9) Wire bottombar extras (liveview, footer fit)
* ========================================= */
wireLiveview();
setupFooterFit();
/* =========================================
* 10) Wire settings modal
* ========================================= */
wireSettingsModal();
/* =========================================
* 11) Wire chip editor
* ========================================= */
wireChipEditor();
/* =========================================
* 12) Global toast bridge
* ========================================= */
window.toast = (msg, ms = 2600) => toast(msg, ms);
console.info('[App] Bjorn SPA initialized');
}
/* =========================================
* Global pollers — status bar updates
* OPTIMIZED: Staggered timings to reduce CPU load
* ========================================= */
function ensureBjornProgress() {
const host = document.querySelector('.status-left .status-text');
if (!host) return;
if (document.getElementById('bjornProgress')) return; // déjà là
const progress = el('div', {
id: 'bjornProgress',
class: 'bjorn-progress',
style: 'display:none;'
}, [
el('div', { class: 'bjorn-progress-bar' }),
el('span', { class: 'bjorn-progress-text' })
]);
host.appendChild(progress);
}
function startGlobalPollers() {
// Status (Toutes les 6s)
const statusPoller = new Poller(async () => {
try {
const data = await api.get('/bjorn_status', { timeout: 5000, retries: 0 });
const statusEl = $('#bjornStatus');
const status2El = $('#bjornStatus2');
const progressEl = $('#bjornProgress');
const progressBar = progressEl?.querySelector('.bjorn-progress-bar');
const progressText = progressEl?.querySelector('.bjorn-progress-text');
const imgEl = $('#bjornStatusImage');
if (statusEl && data.status) setText(statusEl, data.status);
if (status2El) {
if (data.status2) {
setText(status2El, data.status2);
status2El.style.display = '';
} else {
status2El.style.display = 'none';
}
}
// 🟢 PROGRESS — show only when actively running (1-100)
if (progressEl) {
const pct = Number(data.progress) || 0;
if (pct > 0) {
progressEl.style.display = '';
progressBar.style.setProperty('--progress', `${pct}%`);
progressText.textContent = `${pct}%`;
} else {
progressEl.style.display = 'none';
}
}
if (imgEl && data.image_path) {
imgEl.src = data.image_path + '?t=' + Date.now();
}
} catch (e) { }
}, 6000);
// Character (Toutes les 10s - C'est suffisant pour une icône)
const charPoller = new Poller(async () => {
try {
const imgEl = $('#bjorncharacter');
if (!imgEl) return;
const res = await fetch('/bjorn_character');
if (!res.ok) return;
const blob = await res.blob();
if (imgEl.src && imgEl.src.startsWith('blob:')) URL.revokeObjectURL(imgEl.src);
imgEl.src = URL.createObjectURL(blob);
} catch (e) { }
}, 10000);
// Say (Toutes les 8s)
const sayPoller = new Poller(async () => {
try {
const data = await api.get('/bjorn_say', { timeout: 5000, retries: 0 });
const sayEl = $('#bjornSay');
if (sayEl && data?.text) setText(sayEl, data.text);
} catch (e) { }
}, 8000);
statusPoller.start();
charPoller.start();
sayPoller.start();
}
/* =========================================
* Topbar wiring
* ========================================= */
function wireTopbar() {
// Logo -> dashboard
const logo = $('#logoBtn');
if (logo) {
logo.addEventListener('click', () => router.navigate('/dashboard'));
logo.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); router.navigate('/dashboard'); }
});
}
// Settings button
const settingsBtn = $('#openSettings');
if (settingsBtn) {
settingsBtn.addEventListener('click', () => toggleSettings());
}
// Launcher button
const launcherBtn = $('#openLauncher');
if (launcherBtn) {
launcherBtn.addEventListener('click', () => toggleLauncher());
}
}
/* =========================================
* Liveview dropdown (BÉTON EDITION)
* Uses recursive setTimeout to prevent thread stacking
* ========================================= */
function wireLiveview() {
const character = $('#bjorncharacter');
const center = $('.status-center');
if (!character || !center) return;
const dropdown = el('div', { class: 'bjorn-dropdown' }, [
el('img', { id: 'screenImage_Home', src: '/web/screen.png', alt: 'Bjorn', style: 'cursor:pointer;max-width:200px;border-radius:6px' })
]);
center.appendChild(dropdown);
const liveImg = $('#screenImage_Home', dropdown);
let timer = null;
const LIVE_DELAY = 4000; // On passe à 4s pour matcher display.py
function updateLive() {
if (dropdown.style.display !== 'block') return; // Stop si caché
const n = new Image();
n.onload = () => {
liveImg.src = n.src;
// On ne planifie la suivante QUE quand celle-ci est affichée
timer = setTimeout(updateLive, LIVE_DELAY);
};
n.onerror = () => {
// En cas d'erreur, on attend un peu avant de réessayer
timer = setTimeout(updateLive, LIVE_DELAY * 2);
};
n.src = '/web/screen.png?t=' + Date.now();
}
const show = () => {
dropdown.style.display = 'block';
if (!timer) updateLive();
};
const hide = () => {
dropdown.style.display = 'none';
clearTimeout(timer);
timer = null;
};
// Events
character.addEventListener('mouseenter', show);
character.addEventListener('mouseleave', () => setTimeout(() => {
if (!dropdown.matches(':hover') && !character.matches(':hover')) hide();
}, 300));
character.addEventListener('click', (e) => {
e.stopPropagation();
dropdown.style.display === 'block' ? hide() : show();
});
document.addEventListener('click', (ev) => {
if (!dropdown.contains(ev.target) && !character.contains(ev.target)) hide();
});
if (liveImg) {
liveImg.addEventListener('click', () => router.navigate('/bjorn'));
}
}
/* =========================================
* Footer text fitting (adaptive font size)
* ========================================= */
function setupFooterFit() {
function fitTextById(id, opts = {}) {
const el = document.getElementById(id);
if (!el) return;
const box = el.parentElement || el;
const max = opts.max || 12;
const min = opts.min || 7;
let size = max;
el.style.fontSize = size + 'px';
const maxH = parseFloat(getComputedStyle(el).maxHeight) || Infinity;
while ((el.scrollWidth > box.clientWidth || el.scrollHeight > maxH) && size > min) {
size--;
el.style.fontSize = size + 'px';
}
}
function runFooterFit() {
fitTextById('bjornStatus', { max: 12, min: 7 });
fitTextById('bjornSay', { max: 12, min: 7 });
fitTextById('bjornStatus2', { max: 12, min: 7 });
fitTextById('bjornProgress', { max: 11, min: 7 }); // 🟢
}
// Run on load & resize
window.addEventListener('load', runFooterFit);
window.addEventListener('resize', runFooterFit);
// Observe size/content changes
const left = document.querySelector('.status-left');
const right = document.querySelector('.status-right');
const ro = new ResizeObserver(runFooterFit);
if (left) ro.observe(left);
if (right) ro.observe(right);
['bjornStatus', 'bjornSay', 'bjornStatus2', 'bjornProgress'].forEach(id => {
const elem = document.getElementById(id);
if (!elem) return;
ro.observe(elem);
new MutationObserver(runFooterFit).observe(elem, {
childList: true,
characterData: true,
subtree: true
});
});
const imgs = [document.getElementById('bjornStatusImage'), document.getElementById('bjorncharacter')];
imgs.forEach(img => {
if (!img) return;
if (img.complete) runFooterFit();
else img.addEventListener('load', runFooterFit, { once: true });
});
// Initial run
runFooterFit();
}
/* =========================================
* Page launcher
* ========================================= */
const NAV_MODE_KEY = 'bjorn.navMode'; // 'rail' or 'grid'
function getNavMode() { return localStorage.getItem(NAV_MODE_KEY) || 'rail'; }
function setNavMode(mode) { localStorage.setItem(NAV_MODE_KEY, mode); }
const PAGES = [
{ path: '/dashboard', icon: 'home.png', label: 'nav.dashboard' },
{ path: '/bjorn', icon: 'bjorn_icon.png', label: 'nav.bjorn' },
{ path: '/netkb', icon: 'netkb.png', label: 'nav.netkb' },
{ path: '/network', icon: 'network.png', label: 'nav.network' },
{ path: '/credentials', icon: 'credentials.png', label: 'nav.credentials' },
{ path: '/vulnerabilities', icon: 'vulnerabilities.png', label: 'nav.vulnerabilities' },
{ path: '/attacks', icon: 'attacks.png', label: 'nav.attacks' },
{ path: '/scheduler', icon: 'scheduler.png', label: 'nav.scheduler' },
{ path: '/database', icon: 'database.png', label: 'nav.database' },
{ path: '/files', icon: 'files_explorer.png', label: 'nav.files' },
{ path: '/loot', icon: 'loot.png', label: 'nav.loot' },
{ path: '/actions', icon: 'actions_launcher.png', label: 'nav.actions' },
{ path: '/actions-studio', icon: 'actions_studio.png', label: 'nav.actionsStudio' },
{ path: '/backup', icon: 'backup_update.png', label: 'nav.backup' },
{ path: '/web-enum', icon: 'web_enum.png', label: 'nav.webEnum' },
{ path: '/zombieland', icon: 'zombieland.png', label: 'nav.zombieland' },
{ path: '/ai-dashboard', icon: 'ai_dashboard.png', label: 'nav.ai_dashboard' },
{ path: '/bjorn-debug', icon: 'database.png', label: 'Bjorn Debug' },
];
function wireLauncher() {
const railOverlay = $('#launcher');
const gridOverlay = $('#navOverlay');
const navGrid = $('#navGrid');
if (!railOverlay) return;
// Build rail launcher
railOverlay.innerHTML = '';
const scroll = el('div', { class: 'launcher-scroll' });
for (const page of PAGES) {
const card = el('button', {
class: 'lbtn',
role: 'button',
tabindex: '0',
title: i18n.t(page.label),
onclick: () => {
router.navigate(page.path);
closeLauncher();
},
}, [
el('img', { src: `/web/images/${page.icon}`, alt: '', width: '48', height: '48' }),
el('span', { class: 'lbtn-label', 'data-i18n': page.label }, [i18n.t(page.label)]),
]);
scroll.appendChild(card);
}
railOverlay.appendChild(scroll);
// Build grid launcher
if (navGrid) {
navGrid.innerHTML = '';
for (const page of PAGES) {
const card = el('button', {
class: 'lbtn',
role: 'button',
tabindex: '0',
title: i18n.t(page.label),
onclick: () => {
router.navigate(page.path);
closeNavOverlay();
},
}, [
el('img', { src: `/web/images/${page.icon}`, alt: '', width: '48', height: '48' }),
el('span', { class: 'lbtn-label', 'data-i18n': page.label }, [i18n.t(page.label)]),
]);
navGrid.appendChild(card);
}
}
// Close rail on outside click
document.addEventListener('pointerdown', (e) => {
const btn = $('#openLauncher');
if (!railOverlay.classList.contains('show')) return;
if (railOverlay.contains(e.target)) return;
if (btn && btn.contains(e.target)) return;
closeLauncher();
});
// Close grid overlay on backdrop click
if (gridOverlay) {
gridOverlay.addEventListener('click', (e) => {
if (e.target === gridOverlay) closeNavOverlay();
});
}
}
function toggleLauncher() {
if (getNavMode() === 'grid') {
toggleNavOverlay();
} else {
const overlay = $('#launcher');
if (!overlay) return;
const isOpen = overlay.getAttribute('aria-hidden') !== 'false';
overlay.setAttribute('aria-hidden', String(!isOpen));
overlay.classList.toggle('show', isOpen);
}
}
function closeLauncher() {
const overlay = $('#launcher');
if (!overlay) return;
overlay.setAttribute('aria-hidden', 'true');
overlay.classList.remove('show');
}
function toggleNavOverlay() {
const overlay = $('#navOverlay');
if (!overlay) return;
const isOpen = overlay.classList.contains('show');
if (isOpen) {
overlay.classList.remove('show');
overlay.setAttribute('aria-hidden', 'true');
} else {
overlay.classList.add('show');
overlay.setAttribute('aria-hidden', 'false');
}
}
function closeNavOverlay() {
const overlay = $('#navOverlay');
if (!overlay) return;
overlay.classList.remove('show');
overlay.setAttribute('aria-hidden', 'true');
}
/* =========================================
* Settings modal (tabbed: General, Theme, Config)
* Uses the old-style modal-backdrop + modal with tabs
* ========================================= */
function wireSettingsModal() {
// Build modal content inside #settingsBackdrop
const backdrop = $('#settingsBackdrop');
if (!backdrop) return;
function buildSettings() {
backdrop.innerHTML = '';
const modal = el('div', { class: 'modal', role: 'dialog', 'aria-modal': 'true', 'aria-label': 'Settings' });
// Tabs navigation
const tabs = el('nav', { class: 'tabs', id: 'settingsTabs' });
const btnGeneral = el('button', { class: 'tabbtn active', 'data-tab': 'general' }, ['General']);
const btnTheme = el('button', { class: 'tabbtn', 'data-tab': 'theme' }, ['Theme']);
const btnConfig = el('button', { class: 'tabbtn', 'data-tab': 'config' }, ['Config']);
tabs.append(btnGeneral, btnTheme, btnConfig);
// General tab
const tabGeneral = el('section', { class: 'tabpanel', id: 'tab-general' });
tabGeneral.append(
el('h3', {}, [i18n.t('settings.general')]),
el('div', { class: 'row' }, [
el('label', {}, ['Notifications']),
el('div', { class: 'switch', id: 'switchNotifs' })
]),
el('div', { class: 'row' }, [
el('label', {}, ['Navigation Mode']),
el('select', { id: 'selectNavMode', class: 'select' }, [
el('option', { value: 'rail' }, ['Floating Bar']),
el('option', { value: 'grid' }, ['Grid Overlay']),
])
]),
el('div', { class: 'row' }, [
el('label', {}, [i18n.t('settings.language')]),
])
);
// Set current nav mode selection
const navModeSelect = tabGeneral.querySelector('#selectNavMode');
if (navModeSelect) {
navModeSelect.value = getNavMode();
navModeSelect.addEventListener('change', () => setNavMode(navModeSelect.value));
}
// Mount language selector inside general tab
const langRow = tabGeneral.querySelector('.row:last-child');
if (langRow) i18n.mountLangSelector(langRow);
// Theme tab
const tabTheme = el('section', { class: 'tabpanel', id: 'tab-theme', hidden: '' });
tabTheme.append(el('h3', {}, [i18n.t('settings.theme')]));
theme.mountEditor(tabTheme);
// Config tab
const tabConfig = el('section', { class: 'tabpanel', id: 'tab-config', hidden: '' }, [
el('div', { class: 'cfg-toolbar' }, [
el('button', { class: 'btn', id: 'cfgReload' }, [i18n.t('common.refresh')]),
el('button', { class: 'btn', id: 'cfgRestore' }, [i18n.t('common.reset')]),
el('button', { class: 'btn btn-primary', id: 'cfgSave' }, [i18n.t('common.save')]),
]),
el('div', { id: 'configFormHost', class: 'cfg-host' }),
]);
modal.append(tabs, tabGeneral, tabTheme, tabConfig);
backdrop.appendChild(modal);
const cfgHost = modal.querySelector('#configFormHost');
settingsConfig.mountConfig(cfgHost);
// Tab switching
tabs.addEventListener('click', (e) => {
const btn = e.target.closest('.tabbtn');
if (!btn) return;
const tabId = btn.dataset.tab;
tabs.querySelectorAll('.tabbtn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
modal.querySelectorAll('.tabpanel').forEach(p => p.hidden = true);
const panel = modal.querySelector(`#tab-${tabId}`);
if (panel) panel.hidden = false;
if (tabId === 'config') settingsConfig.loadConfig(cfgHost);
});
// Notifications switch
const notifSwitch = modal.querySelector('#switchNotifs');
if (notifSwitch) {
const notifOn = localStorage.getItem('bjorn.notifs') !== 'off';
if (notifOn) notifSwitch.classList.add('on');
notifSwitch.addEventListener('click', () => {
notifSwitch.classList.toggle('on');
localStorage.setItem('bjorn.notifs', notifSwitch.classList.contains('on') ? 'on' : 'off');
});
}
// Config actions
modal.querySelector('#cfgReload')?.addEventListener('click', () => settingsConfig.loadConfig(cfgHost));
modal.querySelector('#cfgSave')?.addEventListener('click', () => settingsConfig.saveConfig());
modal.querySelector('#cfgRestore')?.addEventListener('click', () => settingsConfig.restoreDefaults(cfgHost));
}
// Store build function for reuse
backdrop._buildSettings = buildSettings;
}
function toggleSettings() {
const backdrop = $('#settingsBackdrop');
if (!backdrop) return;
const isOpen = backdrop.style.display === 'flex';
if (isOpen) {
backdrop.style.display = 'none';
backdrop.setAttribute('aria-hidden', 'true');
} else {
if (backdrop._buildSettings) backdrop._buildSettings();
backdrop.style.display = 'flex';
backdrop.setAttribute('aria-hidden', 'false');
setTimeout(() => {
const m = backdrop.querySelector('.modal');
if (m) m.classList.add('show');
}, 0);
}
}
/* Close settings on backdrop click (persistent listener) */
document.addEventListener('click', (e) => {
const backdrop = $('#settingsBackdrop');
if (!backdrop || backdrop.style.display !== 'flex') return;
if (e.target === backdrop) toggleSettings();
});
/* Close settings on Escape (persistent listener) */
document.addEventListener('keydown', (e) => {
if (e.key !== 'Escape') return;
const backdrop = $('#settingsBackdrop');
if (!backdrop || backdrop.style.display !== 'flex') return;
toggleSettings();
});
/* =========================================
* Chip Editor (global singleton)
* Wires the existing #chipEditBackdrop from HTML
* ========================================= */
function wireChipEditor() {
const backdrop = $('#chipEditBackdrop');
if (!backdrop || window.ChipsEditor) return;
const title = $('#chipEditTitle');
const label = $('#chipEditLabel');
const input = $('#chipEditInput');
const ta = $('#chipEditTextarea');
const btnSave = $('#chipEditSave');
const btnCancel = $('#chipEditCancel');
const btnClose = $('#chipEditClose');
if (!input || !ta || !btnSave) return;
let resolver = null;
function show() { backdrop.classList.add('show'); requestAnimationFrame(() => (input.offsetParent ? input : ta).focus()); }
function hide() { backdrop.classList.remove('show'); resolver = null; }
function currentValue() { return (input.offsetParent ? input.value : ta.value).trim(); }
function resolve(val) { if (resolver) { resolver(val); hide(); } }
function save() { resolve(currentValue()); }
function cancel() { resolve(null); }
btnSave.addEventListener('click', save);
btnCancel.addEventListener('click', cancel);
btnClose.addEventListener('click', cancel);
backdrop.addEventListener('click', (e) => { if (e.target === backdrop) cancel(); });
document.addEventListener('keydown', (e) => {
if (!backdrop.classList.contains('show')) return;
if (e.key === 'Escape') { e.preventDefault(); cancel(); }
if (e.key === 'Enter' && e.target.closest('#chipEditBackdrop') && e.target.id !== 'chipEditTextarea') {
e.preventDefault(); save();
}
});
window.ChipsEditor = {
open(opts = {}) {
const { value = '', title: ttl = 'Edit value', label: lab = 'Value', placeholder = '', multiline = false, maxLength, confirmLabel = 'Save' } = opts;
if (title) title.textContent = ttl;
if (label) label.textContent = lab;
if (btnSave) btnSave.textContent = confirmLabel;
if (multiline) {
ta.style.display = '';
input.style.display = 'none';
ta.value = value;
ta.placeholder = placeholder;
ta.removeAttribute('maxlength');
if (maxLength) ta.setAttribute('maxlength', String(maxLength));
} else {
input.style.display = '';
ta.style.display = 'none';
input.value = value;
input.placeholder = placeholder;
input.removeAttribute('maxlength');
if (maxLength) input.setAttribute('maxlength', String(maxLength));
}
show();
return new Promise(res => { resolver = res; });
}
};
}