Files
Bjorn/web/js/core/actions.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

438 lines
14 KiB
JavaScript

/**
* Actions Dropdown — ES module replacement for the monolithic global.js
* actions/dropdown logic. Builds the dropdown menu, wires hover/touch/keyboard
* behaviour, and dispatches action API calls.
*/
import { $, el, toast } from './dom.js';
import { api } from './api.js';
import { t } from './i18n.js';
/* ------------------------------------------------------------------ */
/* Dropdown item definitions */
/* ------------------------------------------------------------------ */
const dropdownItems = [
{ action: 'restart_bjorn_service', textKey: 'actions.menu.restartService', tipKey: 'actions.tip.restartService' },
{ action: 'remove_All_Actions', textKey: 'actions.menu.deleteActionStatus', tipKey: 'actions.tip.deleteActionStatus' },
{ action: 'clear_output_folder', textKey: 'actions.menu.clearOutput', tipKey: 'actions.tip.clearOutput' },
{ action: 'clear_logs', textKey: 'actions.menu.clearLogs', tipKey: 'actions.tip.clearLogs' },
{ action: 'reload_images', textKey: 'actions.menu.reloadImages', tipKey: 'actions.tip.reloadImages' },
{ action: 'reload_fonts', textKey: 'actions.menu.reloadFonts', tipKey: 'actions.tip.reloadFonts' },
{ action: 'reload_generate_actions_json', textKey: 'actions.menu.reloadActionsJson', tipKey: 'actions.tip.reloadActionsJson' },
{ action: 'initialize_csv', textKey: 'actions.menu.initializeCsv', tipKey: 'actions.tip.initializeCsv' },
{ action: 'clear_livestatus', textKey: 'actions.menu.clearLivestatus', tipKey: 'actions.tip.clearLivestatus' },
{ action: 'clear_actions_file', textKey: 'actions.menu.refreshActionsFile', tipKey: 'actions.tip.refreshActionsFile' },
{ action: 'clear_netkb', textKey: 'actions.menu.clearNetkb', tipKey: 'actions.tip.clearNetkb' },
{ action: 'clear_shared_config_json', textKey: 'actions.menu.clearSharedConfig', tipKey: 'actions.tip.clearSharedConfig' },
{ action: 'erase_bjorn_memories', textKey: 'actions.menu.eraseMemories', tipKey: 'actions.tip.eraseMemories' },
{ action: 'reboot_system', textKey: 'actions.menu.reboot', tipKey: 'actions.tip.reboot' },
{ action: 'shutdown_system', textKey: 'actions.menu.shutdown', tipKey: 'actions.tip.shutdown' },
];
/* ------------------------------------------------------------------ */
/* Action handlers — each returns a Promise */
/* ------------------------------------------------------------------ */
/**
* Helper: after a successful action that recommends a service restart,
* prompt the user and fire the restart if they agree.
*/
async function offerRestart() {
if (confirm(t('actions.confirm.restartRecommended'))) {
try {
await api.post('/restart_bjorn_service');
toast(t('actions.msg.restartingService'), 3000, 'success');
} catch (err) {
toast(`${t('actions.msg.restartFailed')}: ${err.message}`, 4000, 'error');
}
}
}
/** Map of action name -> handler function */
const actionHandlers = {
async restart_bjorn_service() {
if (!confirm(t('actions.confirm.restartService'))) return;
await api.post('/restart_bjorn_service');
toast(t('actions.msg.restartingService'), 3000, 'success');
},
async remove_All_Actions() {
if (!confirm(t('actions.confirm.deleteActionStatus'))) return;
await api.post('/delete_all_actions', { ip: '' });
toast(t('actions.msg.actionStatusDeleted'), 3000, 'success');
},
async clear_output_folder() {
if (!confirm(t('actions.confirm.clearOutput'))) return;
await api.post('/clear_output_folder');
toast(t('actions.msg.outputCleared'), 3000, 'success');
},
async clear_logs() {
if (!confirm(t('actions.confirm.clearLogs'))) return;
await api.post('/clear_logs');
toast(t('actions.msg.logsCleared'), 3000, 'success');
},
async clear_netkb() {
if (!confirm(t('actions.confirm.clearNetkb'))) return;
await api.post('/clear_netkb');
toast(t('actions.msg.netkbCleared'), 3000, 'success');
await offerRestart();
},
async clear_livestatus() {
if (!confirm(t('actions.confirm.clearLivestatus'))) return;
await api.post('/clear_livestatus');
toast(t('actions.msg.livestatusDeleted'), 3000, 'success');
await offerRestart();
},
async clear_actions_file() {
if (!confirm(t('actions.confirm.refreshActionsFile'))) return;
await api.post('/clear_actions_file');
toast(t('actions.msg.actionsFileRefreshed'), 3000, 'success');
await offerRestart();
},
async clear_shared_config_json() {
if (!confirm(t('actions.confirm.clearSharedConfig'))) return;
await api.post('/clear_shared_config_json');
toast(t('actions.msg.sharedConfigDeleted'), 3000, 'success');
await offerRestart();
},
async erase_bjorn_memories() {
if (!confirm(t('actions.confirm.eraseMemories'))) return;
await api.post('/erase_bjorn_memories');
toast(t('actions.msg.memoriesErased'), 3000, 'success');
await offerRestart();
},
async reboot_system() {
if (!confirm(t('actions.confirm.reboot'))) return;
await api.post('/reboot_system');
toast(t('actions.msg.rebooting'), 3000, 'success');
},
async shutdown_system() {
if (!confirm(t('actions.confirm.shutdown'))) return;
await api.post('/shutdown_system');
toast(t('actions.msg.shuttingDown'), 3000, 'success');
},
async initialize_csv() {
await api.post('/initialize_csv');
toast(t('actions.msg.csvInitialized'), 3000, 'success');
},
async reload_generate_actions_json() {
await api.post('/reload_generate_actions_json');
toast(t('actions.msg.actionsJsonReloaded'), 3000, 'success');
},
async reload_images() {
await api.post('/reload_images');
toast(t('actions.msg.imagesReloaded'), 3000, 'success');
},
async reload_fonts() {
await api.post('/reload_fonts');
toast(t('actions.msg.fontsReloaded'), 3000, 'success');
},
};
/* ------------------------------------------------------------------ */
/* Dropdown open / close helpers */
/* ------------------------------------------------------------------ */
let actionsBtn = null;
let actionsMenu = null;
let actionsWrap = null;
/** Whether the menu was explicitly toggled open via pointer/keyboard */
let sticky = false;
let hoverTimer = null;
const hoverMQ = window.matchMedia('(hover: hover) and (pointer: fine)');
function openMenu() {
if (!actionsMenu || !actionsBtn) return;
actionsMenu.style.display = 'block';
actionsMenu.hidden = false;
actionsMenu.classList.add('open');
actionsMenu.setAttribute('aria-hidden', 'false');
actionsBtn.setAttribute('aria-expanded', 'true');
placeActionsMenu();
}
function closeMenu() {
if (!actionsMenu || !actionsBtn) return;
actionsMenu.classList.remove('open');
actionsMenu.setAttribute('aria-hidden', 'true');
actionsBtn.setAttribute('aria-expanded', 'false');
actionsMenu.hidden = true;
actionsMenu.style.display = '';
sticky = false;
}
function isOpen() {
return actionsMenu && actionsMenu.classList.contains('open');
}
/**
* Position the dropdown menu beneath the topbar, horizontally centered.
*/
function placeActionsMenu() {
if (!actionsMenu || !actionsBtn) return;
const btnRect = actionsBtn.getBoundingClientRect();
const top = Math.round(btnRect.bottom + 6);
const margin = 8;
actionsMenu.style.position = 'fixed';
actionsMenu.style.top = `${top}px`;
actionsMenu.style.left = '0px';
actionsMenu.style.transform = 'none';
const menuWidth = actionsMenu.offsetWidth || 320;
const viewportWidth = window.innerWidth || document.documentElement.clientWidth || 1024;
const maxLeft = Math.max(margin, viewportWidth - menuWidth - margin);
let left = Math.round(btnRect.left + (btnRect.width - menuWidth) / 2);
left = Math.max(margin, Math.min(maxLeft, left));
actionsMenu.style.left = `${left}px`;
}
/* ------------------------------------------------------------------ */
/* Build the menu items into the DOM */
/* ------------------------------------------------------------------ */
function buildMenu() {
if (!actionsMenu) return;
// Clear any existing children (idempotent rebuild)
while (actionsMenu.firstChild) actionsMenu.removeChild(actionsMenu.firstChild);
for (const item of dropdownItems) {
const btn = el('button', {
class: 'dropdown-item',
role: 'menuitem',
tabindex: '-1',
title: t(item.tipKey),
'data-action': item.action,
}, [t(item.textKey)]);
actionsMenu.appendChild(btn);
}
}
/* ------------------------------------------------------------------ */
/* Execute an action by name */
/* ------------------------------------------------------------------ */
async function executeAction(actionName) {
const handler = actionHandlers[actionName];
if (!handler) {
toast(`${t('actions.msg.unknownAction')}: ${actionName}`, 3000, 'error');
return;
}
try {
await handler();
} catch (err) {
toast(`${t('actions.msg.actionFailed')}: ${err.message}`, 4000, 'error');
}
}
/* ------------------------------------------------------------------ */
/* Keyboard navigation helpers */
/* ------------------------------------------------------------------ */
function getMenuItems() {
if (!actionsMenu) return [];
return Array.from(actionsMenu.querySelectorAll('[role="menuitem"]'));
}
function focusItem(items, index) {
if (index < 0 || index >= items.length) return;
items[index].focus();
}
/* ------------------------------------------------------------------ */
/* Event wiring */
/* ------------------------------------------------------------------ */
function wireEvents() {
if (!actionsBtn || !actionsMenu || !actionsWrap) return;
/* -- Hover behavior (desktop only) -- */
actionsWrap.addEventListener('mouseenter', () => {
if (!hoverMQ.matches) return;
if (hoverTimer) {
clearTimeout(hoverTimer);
hoverTimer = null;
}
if (!sticky) openMenu();
});
actionsWrap.addEventListener('mouseleave', () => {
if (!hoverMQ.matches) return;
if (sticky) return;
hoverTimer = setTimeout(() => {
hoverTimer = null;
if (!sticky) closeMenu();
}, 150);
});
/* -- Button toggle (desktop + mobile) -- */
let lastToggleTime = 0;
function toggleFromButton(e) {
e.preventDefault();
e.stopPropagation();
// Guard against double-firing (pointerup + click both fire on mobile tap)
const now = Date.now();
if (now - lastToggleTime < 300) return;
lastToggleTime = now;
if (isOpen()) {
closeMenu();
} else {
sticky = true;
openMenu();
}
}
actionsBtn.addEventListener('click', toggleFromButton);
actionsBtn.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') toggleFromButton(e);
});
/* -- Close on pointerdown outside -- */
document.addEventListener('pointerdown', (e) => {
if (!isOpen()) return;
if (!actionsWrap.contains(e.target)) {
closeMenu();
}
});
/* -- Menu item clicks -- */
actionsMenu.addEventListener('click', (e) => {
const item = e.target.closest('[data-action]');
if (!item) return;
const actionName = item.getAttribute('data-action');
closeMenu();
executeAction(actionName);
});
/* -- Keyboard navigation -- */
actionsWrap.addEventListener('keydown', (e) => {
const items = getMenuItems();
if (!items.length) return;
const currentIndex = items.indexOf(document.activeElement);
switch (e.key) {
case 'Escape':
e.preventDefault();
closeMenu();
actionsBtn.focus();
break;
case 'ArrowDown':
e.preventDefault();
if (!isOpen()) {
openMenu();
focusItem(items, 0);
} else {
focusItem(items, currentIndex < items.length - 1 ? currentIndex + 1 : 0);
}
break;
case 'ArrowUp':
e.preventDefault();
if (!isOpen()) {
openMenu();
focusItem(items, items.length - 1);
} else {
focusItem(items, currentIndex > 0 ? currentIndex - 1 : items.length - 1);
}
break;
case 'Home':
if (isOpen()) {
e.preventDefault();
focusItem(items, 0);
}
break;
case 'End':
if (isOpen()) {
e.preventDefault();
focusItem(items, items.length - 1);
}
break;
case 'Enter':
case ' ':
if (document.activeElement && document.activeElement.hasAttribute('data-action')) {
e.preventDefault();
const actionName = document.activeElement.getAttribute('data-action');
closeMenu();
executeAction(actionName);
}
break;
default:
break;
}
});
/* -- Reposition on resize / scroll -- */
window.addEventListener('resize', () => {
if (isOpen()) placeActionsMenu();
});
window.addEventListener('scroll', () => {
if (isOpen()) placeActionsMenu();
}, { passive: true });
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && isOpen()) closeMenu();
});
window.addEventListener('hashchange', closeMenu);
}
function onLanguageChanged() {
buildMenu();
if (isOpen()) placeActionsMenu();
}
/* ------------------------------------------------------------------ */
/* Public init — idempotent */
/* ------------------------------------------------------------------ */
let _initialised = false;
/**
* Initialise the Actions dropdown.
* Safe to call once; subsequent calls are no-ops.
*/
export function init() {
if (_initialised) return;
actionsBtn = $('#actionsBtn');
actionsMenu = $('#actionsMenu');
actionsWrap = $('#actionsWrap');
if (!actionsBtn || !actionsMenu || !actionsWrap) {
console.warn('[actions] Required DOM elements not found; skipping init.');
return;
}
buildMenu();
wireEvents();
window.addEventListener('i18n:changed', onLanguageChanged);
_initialised = true;
console.debug('[actions] initialised');
}