mirror of
https://github.com/infinition/Bjorn.git
synced 2026-03-18 18:00:23 +00:00
Add Loki and Sentinel utility classes for web API endpoints
- Implemented LokiUtils class with GET and POST endpoints for managing scripts, jobs, and payloads. - Added SentinelUtils class with GET and POST endpoints for managing events, rules, devices, and notifications. - Both classes include error handling and JSON response formatting.
This commit is contained in:
@@ -41,6 +41,8 @@ const LEVEL_CLASSES = {
|
||||
let evtSource = null;
|
||||
let reconnectCount = 0;
|
||||
let reconnectTimer = null;
|
||||
let healthyMessageCount = 0;
|
||||
const HEALTHY_THRESHOLD = 5; // messages needed before resetting reconnect counter
|
||||
|
||||
let isUserScrolling = false;
|
||||
let autoScroll = true;
|
||||
@@ -364,7 +366,11 @@ function connectSSE() {
|
||||
evtSource = new EventSource('/stream_logs');
|
||||
|
||||
evtSource.onmessage = (evt) => {
|
||||
reconnectCount = 0; // healthy connection resets counter
|
||||
// Only reset reconnect counter after sustained healthy connection
|
||||
healthyMessageCount++;
|
||||
if (healthyMessageCount >= HEALTHY_THRESHOLD) {
|
||||
reconnectCount = 0;
|
||||
}
|
||||
|
||||
const raw = evt.data;
|
||||
if (!raw) return;
|
||||
@@ -405,6 +411,7 @@ function connectSSE() {
|
||||
};
|
||||
|
||||
evtSource.onerror = () => {
|
||||
healthyMessageCount = 0;
|
||||
disconnectSSE();
|
||||
scheduleReconnect();
|
||||
};
|
||||
|
||||
1157
web/js/core/epd-editor.js
Normal file
1157
web/js/core/epd-editor.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
import { $, el, toast, empty } from './dom.js';
|
||||
import { $, el, toast, empty } from './dom.js';
|
||||
import { api } from './api.js';
|
||||
import { t } from './i18n.js';
|
||||
|
||||
@@ -33,10 +33,51 @@ const RANGES = {
|
||||
semaphore_slots: { min: 1, max: 128, step: 1 },
|
||||
line_spacing: { min: 0, max: 10, step: 0.1 },
|
||||
vuln_update_interval: { min: 1, max: 86400, step: 1 },
|
||||
ai_feature_selection_min_variance: { min: 0, max: 1, step: 0.001 },
|
||||
ai_model_history_max: { min: 1, max: 10, step: 1 },
|
||||
ai_auto_rollback_window: { min: 10, max: 500, step: 10 },
|
||||
ai_cold_start_bootstrap_weight: { min: 0, max: 1, step: 0.05 },
|
||||
circuit_breaker_threshold: { min: 1, max: 20, step: 1 },
|
||||
manual_mode_scan_interval: { min: 30, max: 3600, step: 10 },
|
||||
};
|
||||
|
||||
/* ── Sub-tab grouping: maps __title_* section keys → sub-tab id ── */
|
||||
const SECTION_TO_TAB = {
|
||||
'__title_Bjorn__': 'core',
|
||||
'__title_modes__': 'core',
|
||||
'__title_web__': 'core',
|
||||
'__title_interfaces__': 'network',
|
||||
'__title_network__': 'network',
|
||||
'__title_actions_studio__': 'actions',
|
||||
'__title_timewaits__': 'actions',
|
||||
'__title_orchestrator__': 'actions',
|
||||
'__title_bruteforce__': 'actions',
|
||||
'__title_display__': 'display',
|
||||
'__title_epd__': 'display',
|
||||
'__title_timing__': 'display',
|
||||
'__title_ai__': 'ai',
|
||||
'__title_vuln__': 'security',
|
||||
'__title_lists__': 'security',
|
||||
'__title_runtime__': 'system',
|
||||
'__title_power__': 'system',
|
||||
'__title_sentinel__': 'security',
|
||||
'__title_bifrost__': 'network',
|
||||
'__title_loki__': 'security',
|
||||
};
|
||||
|
||||
const SUB_TABS = [
|
||||
{ id: 'core', icon: '\u2699', label: 'Core' },
|
||||
{ id: 'network', icon: '\uD83C\uDF10', label: 'Network' },
|
||||
{ id: 'actions', icon: '\u26A1', label: 'Actions' },
|
||||
{ id: 'display', icon: '\uD83D\uDDA5', label: 'Display' },
|
||||
{ id: 'ai', icon: '\uD83E\uDDE0', label: 'AI / RL' },
|
||||
{ id: 'security', icon: '\uD83D\uDD12', label: 'Security' },
|
||||
{ id: 'system', icon: '\uD83D\uDD27', label: 'System' },
|
||||
];
|
||||
|
||||
let _host = null;
|
||||
let _lastConfig = null;
|
||||
let _activeSubTab = 'core';
|
||||
|
||||
function resolveTooltips(config) {
|
||||
const tips = config?.__tooltips_i18n__;
|
||||
@@ -260,41 +301,114 @@ function createSectionCard(title) {
|
||||
]);
|
||||
}
|
||||
|
||||
/* ── Sub-tab navigation bar ── */
|
||||
function createSubTabBar(onSwitch) {
|
||||
const nav = el('nav', { class: 'cfg-subtabs' });
|
||||
for (const tab of SUB_TABS) {
|
||||
const btn = el('button', {
|
||||
class: `cfg-subtab${tab.id === _activeSubTab ? ' active' : ''}`,
|
||||
'data-subtab': tab.id,
|
||||
type: 'button',
|
||||
}, [`${tab.icon}\u00A0${tab.label}`]);
|
||||
nav.appendChild(btn);
|
||||
}
|
||||
nav.addEventListener('click', (e) => {
|
||||
const btn = e.target.closest('.cfg-subtab');
|
||||
if (!btn) return;
|
||||
const id = btn.dataset.subtab;
|
||||
if (id === _activeSubTab) return;
|
||||
_activeSubTab = id;
|
||||
nav.querySelectorAll('.cfg-subtab').forEach(b => b.classList.toggle('active', b.dataset.subtab === id));
|
||||
onSwitch(id);
|
||||
});
|
||||
return nav;
|
||||
}
|
||||
|
||||
function render(config) {
|
||||
if (!_host) return;
|
||||
empty(_host);
|
||||
ensureChipHelpers();
|
||||
const tooltips = resolveTooltips(config);
|
||||
|
||||
const togglesCard = createSectionCard(t('settings.toggles'));
|
||||
const togglesBody = togglesCard.querySelector('.cfg-card-body');
|
||||
const cardsGrid = el('div', { class: 'cfg-cards-grid' });
|
||||
/* Buckets: one per sub-tab, each with a toggles card + section cards */
|
||||
const buckets = {};
|
||||
for (const tab of SUB_TABS) {
|
||||
buckets[tab.id] = {
|
||||
togglesBody: null,
|
||||
togglesCard: null,
|
||||
cardsGrid: el('div', { class: 'cfg-cards-grid' }),
|
||||
currentCard: null,
|
||||
pane: el('div', { class: 'cfg-subtab-pane', 'data-pane': tab.id }),
|
||||
};
|
||||
}
|
||||
|
||||
/* Helper: lazily create the toggles card for a bucket */
|
||||
const ensureToggles = (b) => {
|
||||
if (!b.togglesCard) {
|
||||
b.togglesCard = createSectionCard(t('settings.toggles'));
|
||||
b.togglesBody = b.togglesCard.querySelector('.cfg-card-body');
|
||||
}
|
||||
};
|
||||
|
||||
let currentTabId = 'core'; // default bucket for fields before first __title_*
|
||||
|
||||
let currentCard = null;
|
||||
for (const [key, value] of Object.entries(config || {})) {
|
||||
if (key.startsWith('__')) {
|
||||
if (key.startsWith('__title_')) {
|
||||
if (currentCard) cardsGrid.appendChild(currentCard);
|
||||
currentCard = createSectionCard(String(value).replace('__title_', '').replace(/__/g, ''));
|
||||
/* Close previous card if any */
|
||||
const prevBucket = buckets[currentTabId];
|
||||
if (prevBucket.currentCard) {
|
||||
prevBucket.cardsGrid.appendChild(prevBucket.currentCard);
|
||||
prevBucket.currentCard = null;
|
||||
}
|
||||
/* Switch to the right bucket */
|
||||
currentTabId = SECTION_TO_TAB[key] || 'core';
|
||||
const bucket = buckets[currentTabId];
|
||||
const sectionName = String(value).replace('__title_', '').replace(/__/g, '');
|
||||
bucket.currentCard = createSectionCard(sectionName);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const bucket = buckets[currentTabId];
|
||||
const tooltipI18nKey = String(tooltips[key] || '');
|
||||
|
||||
if (typeof value === 'boolean') {
|
||||
togglesBody.appendChild(createBooleanField(key, value, tooltipI18nKey));
|
||||
ensureToggles(bucket);
|
||||
bucket.togglesBody.appendChild(createBooleanField(key, value, tooltipI18nKey));
|
||||
continue;
|
||||
}
|
||||
if (!currentCard) currentCard = createSectionCard(t('settings.general'));
|
||||
const body = currentCard.querySelector('.cfg-card-body');
|
||||
|
||||
if (!bucket.currentCard) bucket.currentCard = createSectionCard(t('settings.general'));
|
||||
const body = bucket.currentCard.querySelector('.cfg-card-body');
|
||||
if (Array.isArray(value)) body.appendChild(createListField(key, value, tooltipI18nKey));
|
||||
else if (typeof value === 'number') body.appendChild(createNumberField(key, value, tooltipI18nKey));
|
||||
else body.appendChild(createStringField(key, value, tooltipI18nKey));
|
||||
}
|
||||
|
||||
if (currentCard) cardsGrid.appendChild(currentCard);
|
||||
_host.appendChild(togglesCard);
|
||||
_host.appendChild(cardsGrid);
|
||||
/* Finalize all buckets */
|
||||
for (const tab of SUB_TABS) {
|
||||
const b = buckets[tab.id];
|
||||
if (b.currentCard) b.cardsGrid.appendChild(b.currentCard);
|
||||
if (b.togglesCard) b.pane.appendChild(b.togglesCard);
|
||||
if (b.cardsGrid.children.length) b.pane.appendChild(b.cardsGrid);
|
||||
}
|
||||
|
||||
/* Build sub-tab bar */
|
||||
const showPane = (id) => {
|
||||
_host.querySelectorAll('.cfg-subtab-pane').forEach(p => {
|
||||
p.hidden = p.dataset.pane !== id;
|
||||
});
|
||||
};
|
||||
const subTabBar = createSubTabBar(showPane);
|
||||
_host.appendChild(subTabBar);
|
||||
|
||||
/* Append all panes */
|
||||
for (const tab of SUB_TABS) {
|
||||
const b = buckets[tab.id];
|
||||
b.pane.hidden = tab.id !== _activeSubTab;
|
||||
_host.appendChild(b.pane);
|
||||
}
|
||||
}
|
||||
|
||||
function collect() {
|
||||
@@ -371,6 +485,3 @@ export function mountConfig(host) {
|
||||
export function hasLoadedConfig() {
|
||||
return !!_lastConfig;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
* - User custom overrides persisted to localStorage
|
||||
* - Theme editor with color pickers + raw CSS textarea
|
||||
* - Icon pack switching via icon registry
|
||||
* - Import / Export themes as JSON
|
||||
* - Live preview: overlay disabled while Theme tab is active
|
||||
*/
|
||||
|
||||
import { t } from './i18n.js';
|
||||
@@ -29,9 +31,17 @@ const DEFAULT_THEME = {
|
||||
'--accent-2': '#18d6ff',
|
||||
'--c-border': '#00ffff22',
|
||||
'--c-border-strong': '#00ffff33',
|
||||
'--c-border-hi': '#00ffff44',
|
||||
'--panel': '#0e1717',
|
||||
'--panel-2': '#101c1c',
|
||||
'--c-panel': '#0b1218',
|
||||
'--c-panel-2': '#0a1118',
|
||||
'--c-btn': '#0d151c',
|
||||
'--switch-track': '#111111',
|
||||
'--switch-on-bg': '#022a1a',
|
||||
'--sb-track': '#07121a',
|
||||
'--sb-thumb': '#09372b',
|
||||
'--glass-8': '#00000088',
|
||||
'--radius': '14px'
|
||||
};
|
||||
|
||||
@@ -41,9 +51,13 @@ const TOKEN_GROUPS = [
|
||||
label: 'theme.group.colors',
|
||||
tokens: [
|
||||
{ key: '--bg', label: 'theme.token.bg', type: 'color' },
|
||||
{ key: '--bg-2', label: 'theme.token.bg2', type: 'color' },
|
||||
{ key: '--ink', label: 'theme.token.ink', type: 'color' },
|
||||
{ key: '--muted', label: 'theme.token.muted', type: 'color' },
|
||||
{ key: '--acid', label: 'theme.token.accent1', type: 'color' },
|
||||
{ key: '--acid-2', label: 'theme.token.accent2', type: 'color' },
|
||||
{ key: '--accent', label: 'theme.token.accent', type: 'color' },
|
||||
{ key: '--accent-2', label: 'theme.token.accentAlt', type: 'color' },
|
||||
{ key: '--danger', label: 'theme.token.danger', type: 'color' },
|
||||
{ key: '--warning', label: 'theme.token.warning', type: 'color' },
|
||||
{ key: '--ok', label: 'theme.token.ok', type: 'color' },
|
||||
@@ -55,7 +69,26 @@ const TOKEN_GROUPS = [
|
||||
{ key: '--panel', label: 'theme.token.panel', type: 'color' },
|
||||
{ key: '--panel-2', label: 'theme.token.panel2', type: 'color' },
|
||||
{ key: '--c-panel', label: 'theme.token.ctrlPanel', type: 'color' },
|
||||
{ key: '--c-panel-2', label: 'theme.token.ctrlPanel2', type: 'color' },
|
||||
{ key: '--c-btn', label: 'theme.token.btnBg', type: 'color' },
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'theme.group.borders',
|
||||
tokens: [
|
||||
{ key: '--c-border', label: 'theme.token.border', type: 'color' },
|
||||
{ key: '--c-border-strong', label: 'theme.token.borderStrong', type: 'color' },
|
||||
{ key: '--c-border-hi', label: 'theme.token.borderHi', type: 'color' },
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'theme.group.controls',
|
||||
tokens: [
|
||||
{ key: '--switch-track', label: 'theme.token.switchTrack', type: 'color' },
|
||||
{ key: '--switch-on-bg', label: 'theme.token.switchOnBg', type: 'color' },
|
||||
{ key: '--sb-track', label: 'theme.token.scrollTrack', type: 'color' },
|
||||
{ key: '--sb-thumb', label: 'theme.token.scrollThumb', type: 'color' },
|
||||
{ key: '--glass-8', label: 'theme.token.glass', type: 'color' },
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -144,6 +177,60 @@ export function getCurrentOverrides() {
|
||||
return { ...DEFAULT_THEME, ..._userOverrides };
|
||||
}
|
||||
|
||||
/* -- Import / Export -- */
|
||||
|
||||
/** Export current theme as JSON string */
|
||||
export function exportTheme() {
|
||||
return JSON.stringify(_userOverrides, null, 2);
|
||||
}
|
||||
|
||||
/** Import theme from JSON string */
|
||||
export function importTheme(json) {
|
||||
try {
|
||||
const parsed = JSON.parse(json);
|
||||
if (typeof parsed !== 'object' || Array.isArray(parsed)) throw new Error('Invalid');
|
||||
_userOverrides = parsed;
|
||||
persist();
|
||||
applyToDOM();
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/* -- Overlay management for live preview -- */
|
||||
|
||||
let _overlayWasVisible = false;
|
||||
|
||||
/** Disable the settings backdrop overlay so theme changes are visible live */
|
||||
export function disableOverlay() {
|
||||
const backdrop = document.getElementById('settingsBackdrop');
|
||||
if (!backdrop) return;
|
||||
_overlayWasVisible = true;
|
||||
backdrop.style.background = 'transparent';
|
||||
const modal = backdrop.querySelector('.modal');
|
||||
if (modal) {
|
||||
modal.style.boxShadow = '0 0 0 2px var(--acid), 0 20px 60px rgba(0,0,0,.6)';
|
||||
modal.style.maxHeight = '70vh';
|
||||
modal.style.overflow = 'auto';
|
||||
}
|
||||
}
|
||||
|
||||
/** Restore the overlay when leaving theme tab */
|
||||
export function restoreOverlay() {
|
||||
if (!_overlayWasVisible) return;
|
||||
const backdrop = document.getElementById('settingsBackdrop');
|
||||
if (!backdrop) return;
|
||||
backdrop.style.background = '';
|
||||
const modal = backdrop.querySelector('.modal');
|
||||
if (modal) {
|
||||
modal.style.boxShadow = '';
|
||||
modal.style.maxHeight = '';
|
||||
modal.style.overflow = '';
|
||||
}
|
||||
_overlayWasVisible = false;
|
||||
}
|
||||
|
||||
/* -- Icon registry -- */
|
||||
|
||||
/**
|
||||
@@ -180,6 +267,9 @@ export function setIconPack(name) {
|
||||
export function mountEditor(container) {
|
||||
container.innerHTML = '';
|
||||
|
||||
/* Disable overlay for live preview */
|
||||
disableOverlay();
|
||||
|
||||
const current = getCurrentOverrides();
|
||||
|
||||
// Color pickers grouped
|
||||
@@ -243,17 +333,61 @@ export function mountEditor(container) {
|
||||
});
|
||||
advSection.appendChild(applyBtn);
|
||||
|
||||
// Reset button
|
||||
container.appendChild(advSection);
|
||||
|
||||
// Import / Export / Reset buttons
|
||||
const actionsRow = document.createElement('div');
|
||||
actionsRow.className = 'theme-actions';
|
||||
|
||||
const exportBtn = document.createElement('button');
|
||||
exportBtn.className = 'btn btn-sm';
|
||||
exportBtn.textContent = t('theme.export');
|
||||
exportBtn.addEventListener('click', () => {
|
||||
const blob = new Blob([exportTheme()], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'bjorn-theme.json';
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
});
|
||||
actionsRow.appendChild(exportBtn);
|
||||
|
||||
const importBtn = document.createElement('button');
|
||||
importBtn.className = 'btn btn-sm';
|
||||
importBtn.textContent = t('theme.import');
|
||||
importBtn.addEventListener('click', () => {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = '.json';
|
||||
input.addEventListener('change', () => {
|
||||
const file = input.files?.[0];
|
||||
if (!file) return;
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
const ok = importTheme(reader.result);
|
||||
if (ok) {
|
||||
mountEditor(container);
|
||||
} else {
|
||||
alert(t('theme.importError'));
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
});
|
||||
input.click();
|
||||
});
|
||||
actionsRow.appendChild(importBtn);
|
||||
|
||||
const resetBtn = document.createElement('button');
|
||||
resetBtn.className = 'btn btn-sm btn-danger';
|
||||
resetBtn.textContent = t('theme.reset');
|
||||
resetBtn.addEventListener('click', () => {
|
||||
resetToDefault();
|
||||
mountEditor(container); // Re-render editor
|
||||
mountEditor(container);
|
||||
});
|
||||
advSection.appendChild(resetBtn);
|
||||
actionsRow.appendChild(resetBtn);
|
||||
|
||||
container.appendChild(advSection);
|
||||
container.appendChild(actionsRow);
|
||||
}
|
||||
|
||||
/** Parse raw CSS var declarations from textarea */
|
||||
|
||||
Reference in New Issue
Block a user