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:
infinition
2026-03-14 22:33:10 +01:00
parent eb20b168a6
commit aac77a3e76
525 changed files with 29400 additions and 13136 deletions

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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;
}

View File

@@ -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 */