diff --git a/shared.py b/shared.py index ccffa25..3dedf1b 100644 --- a/shared.py +++ b/shared.py @@ -450,6 +450,12 @@ class SharedData: "sentinel_discord_webhook": "", "sentinel_webhook_url": "", "sentinel_email_enabled": False, + "sentinel_email_smtp_host": "", + "sentinel_email_smtp_port": "", + "sentinel_email_username": "", + "sentinel_email_password": "", + "sentinel_email_from": "", + "sentinel_email_to": "", # Bifrost (Pwnagotchi Mode) "__title_bifrost__": "Bifrost (Pwnagotchi Mode)", diff --git a/web/css/shell.css b/web/css/shell.css index 0e6733c..1b27316 100644 --- a/web/css/shell.css +++ b/web/css/shell.css @@ -284,7 +284,7 @@ body.console-docked .app-container { box-shadow: 0 -30px 80px var(--glow-strong, #00ff9a33), inset 0 0 0 1px var(--glow-mid, #00ff9a22); z-index: 60; display: grid; - grid-template-rows: 8px auto auto 1fr; + grid-template-rows: 8px auto auto 1fr auto; transform: translateY(100%); transition: transform .25s ease; } @@ -2684,6 +2684,8 @@ input[type="color"].theme-input { font-family: var(--font-mono, 'Courier New', monospace); font-size: var(--console-font, 11px); line-height: 1.4; + max-height: 60px; + height: 26px; } .console-input:focus { border-color: var(--acid, #22c55e); outline: none; } .console-send-btn { diff --git a/web/js/pages/llm-config.js b/web/js/pages/llm-config.js index ed9ae30..6ac4b82 100644 --- a/web/js/pages/llm-config.js +++ b/web/js/pages/llm-config.js @@ -56,6 +56,10 @@ function buildShell() { toggleRow('llm_enabled', t('llm_cfg.enable_bridge')), toggleRow('llm_comments_enabled', t('llm_cfg.epd_comments')), + toggleRow('llm_comments_log', 'Log comments to console'), + toggleRow('llm_chat_enabled', 'Enable LLM chat'), + toggleRow('llm_chat_tools_enabled', 'Enable tools in chat (function calling)'), + toggleRow('epd_buttons_enabled', 'EPD physical buttons'), fieldEl(t('llm_cfg.backend'), el('select', { id: 'llm_backend', class: 'llmcfg-select' }, [ el('option', { value: 'auto' }, ['Auto (LaRuche → Ollama → API)']), @@ -125,6 +129,12 @@ function buildShell() { min: '20', max: '200', value: '80' })), ]), + el('div', { class: 'llmcfg-row' }, [ + fieldEl('Chat history size', + el('input', { type: 'number', id: 'llm_chat_history_size', class: 'llmcfg-input', + min: '2', max: '100', value: '20' })), + ]), + el('div', { class: 'llmcfg-status-row', id: 'llm-status-row' }), el('div', { class: 'llmcfg-actions' }, [ @@ -159,6 +169,7 @@ function buildShell() { toggleRow('llm_orchestrator_log_reasoning', 'Log reasoning to chat history'), toggleRow('llm_orchestrator_skip_if_no_change', 'Skip cycle when nothing changed'), + toggleRow('llm_orchestrator_skip_scheduler', 'Skip scheduler (LLM-only mode)'), el('div', { class: 'llmcfg-status-row', id: 'orch-status-row' }), @@ -315,10 +326,15 @@ async function loadAll() { } function applyLLMConfig(cfg) { - const boolKeys = ['llm_enabled', 'llm_comments_enabled', 'llm_laruche_discovery']; + const boolKeys = [ + 'llm_enabled', 'llm_comments_enabled', 'llm_comments_log', + 'llm_chat_enabled', 'llm_chat_tools_enabled', + 'llm_laruche_discovery', 'epd_buttons_enabled', + ]; const textKeys = ['llm_backend', 'llm_laruche_url', 'llm_ollama_url', 'llm_api_provider', 'llm_api_model', 'llm_api_base_url', 'llm_timeout_s', 'llm_max_tokens', 'llm_comment_max_tokens', + 'llm_chat_history_size', 'llm_user_name', 'llm_user_bio', 'llm_system_prompt_chat', 'llm_system_prompt_comment']; @@ -423,7 +439,8 @@ function applyLLMConfig(cfg) { const orchMax = $('#llm_orchestrator_max_actions', root); if (orchMax && cfg.llm_orchestrator_max_actions !== undefined) orchMax.value = cfg.llm_orchestrator_max_actions; - for (const k of ['llm_orchestrator_log_reasoning', 'llm_orchestrator_skip_if_no_change']) { + for (const k of ['llm_orchestrator_log_reasoning', 'llm_orchestrator_skip_if_no_change', + 'llm_orchestrator_skip_scheduler']) { const cb = $(('#' + k), root); if (cb) cb.checked = !!cfg[k]; } @@ -556,7 +573,11 @@ function populateModelSelect(selectEl, models, currentValue) { async function saveLLM() { const payload = {}; - for (const k of ['llm_enabled', 'llm_comments_enabled', 'llm_laruche_discovery']) { + for (const k of [ + 'llm_enabled', 'llm_comments_enabled', 'llm_comments_log', + 'llm_chat_enabled', 'llm_chat_tools_enabled', + 'llm_laruche_discovery', 'epd_buttons_enabled', + ]) { const el = $(('#' + k), root); payload[k] = el ? el.checked : false; } @@ -566,7 +587,8 @@ async function saveLLM() { const el = $(('#' + k), root); if (el) payload[k] = el.value; } - for (const k of ['llm_timeout_s', 'llm_max_tokens', 'llm_comment_max_tokens']) { + for (const k of ['llm_timeout_s', 'llm_max_tokens', 'llm_comment_max_tokens', + 'llm_chat_history_size']) { const el = $(('#' + k), root); if (el) payload[k] = parseInt(el.value) || undefined; } @@ -642,7 +664,8 @@ async function saveOrch() { const inp = $(('#' + k), root); if (inp) payload[k] = parseInt(inp.value) || undefined; } - for (const k of ['llm_orchestrator_log_reasoning', 'llm_orchestrator_skip_if_no_change']) { + for (const k of ['llm_orchestrator_log_reasoning', 'llm_orchestrator_skip_if_no_change', + 'llm_orchestrator_skip_scheduler']) { const cb = $(('#' + k), root); if (cb) payload[k] = cb.checked; } diff --git a/web/js/pages/sentinel.js b/web/js/pages/sentinel.js index 135d26b..08a162d 100644 --- a/web/js/pages/sentinel.js +++ b/web/js/pages/sentinel.js @@ -20,6 +20,7 @@ let events = []; let rules = []; let devices = []; let unreadCount = 0; +let notifierCfg = {}; // { discord_webhook: '...', webhook_url: '...', ... } let sideTab = 'rules'; // 'rules' | 'devices' | 'notifiers' /* ── Lifecycle ─────────────────────────────────────────── */ @@ -41,6 +42,7 @@ export function unmount() { events = []; rules = []; devices = []; + notifierCfg = {}; } /* ── Shell ─────────────────────────────────────────────── */ @@ -248,17 +250,19 @@ function bindEvents() { async function refresh() { try { - const [statusData, eventsData, rulesData, devicesData] = await Promise.all([ + const [statusData, eventsData, rulesData, devicesData, notifData] = await Promise.all([ api.get('/api/sentinel/status'), api.get('/api/sentinel/events?limit=100'), api.get('/api/sentinel/rules'), api.get('/api/sentinel/devices'), + api.get('/api/sentinel/notifiers').catch(() => null), ]); sentinelEnabled = statusData.enabled; events = eventsData.events || []; unreadCount = eventsData.unread_count || 0; rules = rulesData.rules || []; devices = devicesData.devices || []; + if (notifData?.notifiers) notifierCfg = notifData.notifiers; paint(); } catch (err) { console.warn('[sentinel] refresh error:', err.message); @@ -743,6 +747,7 @@ function paintNotifiers(container) { type: f.type || 'text', 'data-notifier': f.key, placeholder: f.placeholder, + value: notifierCfg[f.key] || '', class: 'sentinel-notifier-input', }), ]) diff --git a/web_utils/llm_utils.py b/web_utils/llm_utils.py index c40daf2..ab54499 100644 --- a/web_utils/llm_utils.py +++ b/web_utils/llm_utils.py @@ -154,9 +154,12 @@ class LLMUtils: "llm_orchestrator_mode", "llm_orchestrator_interval_s", "llm_orchestrator_max_actions", "llm_orchestrator_allowed_actions", "llm_orchestrator_skip_if_no_change", "llm_orchestrator_log_reasoning", + "llm_orchestrator_skip_scheduler", # Personality & prompt keys "llm_system_prompt_chat", "llm_system_prompt_comment", "llm_user_name", "llm_user_bio", + # EPD + "epd_buttons_enabled", } _int_keys = { "llm_timeout_s", "llm_max_tokens", "llm_comment_max_tokens", @@ -167,6 +170,7 @@ class LLMUtils: "llm_enabled", "llm_comments_enabled", "llm_comments_log", "llm_chat_enabled", "llm_laruche_discovery", "llm_chat_tools_enabled", "llm_orchestrator_skip_if_no_change", "llm_orchestrator_log_reasoning", + "llm_orchestrator_skip_scheduler", "epd_buttons_enabled", } try: cfg = self.shared_data.config @@ -299,7 +303,9 @@ class LLMUtils: # Orchestrator "llm_orchestrator_mode", "llm_orchestrator_interval_s", "llm_orchestrator_max_actions", "llm_orchestrator_skip_if_no_change", - "llm_orchestrator_log_reasoning", + "llm_orchestrator_log_reasoning", "llm_orchestrator_skip_scheduler", + # EPD + "epd_buttons_enabled", # Personality & prompts "llm_system_prompt_chat", "llm_system_prompt_comment", "llm_user_name", "llm_user_bio", diff --git a/web_utils/sentinel_utils.py b/web_utils/sentinel_utils.py index 0146922..8d59062 100644 --- a/web_utils/sentinel_utils.py +++ b/web_utils/sentinel_utils.py @@ -219,12 +219,40 @@ class SentinelUtils: except Exception as e: return {"status": "error", "message": str(e)} + # Mapping from frontend notifier keys to config keys + _NOTIFIER_KEY_MAP = { + "discord_webhook": "sentinel_discord_webhook", + "webhook_url": "sentinel_webhook_url", + "email_smtp_host": "sentinel_email_smtp_host", + "email_smtp_port": "sentinel_email_smtp_port", + "email_username": "sentinel_email_username", + "email_password": "sentinel_email_password", + "email_from": "sentinel_email_from", + "email_to": "sentinel_email_to", + } + + def get_notifier_config(self, handler) -> None: + """GET /api/sentinel/notifiers — return current notifier config.""" + cfg = self.shared_data.config + notifiers = {} + for frontend_key, cfg_key in self._NOTIFIER_KEY_MAP.items(): + val = cfg.get(cfg_key, "") + if val: + notifiers[frontend_key] = val + self._send_json(handler, {"status": "ok", "notifiers": notifiers}) + def save_notifier_config(self, data: Dict) -> Dict: """POST /api/sentinel/notifiers — save notification channel config.""" try: - # Store notifier configs in shared_data for persistence notifiers = data.get("notifiers", {}) - self.shared_data.sentinel_notifiers = notifiers + cfg = self.shared_data.config + + # Map frontend keys to config keys and persist + for frontend_key, cfg_key in self._NOTIFIER_KEY_MAP.items(): + cfg[cfg_key] = notifiers.get(frontend_key, "") + + self.shared_data.config = cfg + self.shared_data.save_config() # Re-register notifiers on the engine engine = self._engine diff --git a/webapp.py b/webapp.py index 30e8edd..ea8ee65 100644 --- a/webapp.py +++ b/webapp.py @@ -151,6 +151,7 @@ class CustomHandler(http.server.SimpleHTTPRequestHandler): '/api/sentinel/rules': wu.sentinel.get_rules, '/api/sentinel/devices': wu.sentinel.get_devices, '/api/sentinel/arp': wu.sentinel.get_arp_table, + '/api/sentinel/notifiers': wu.sentinel.get_notifier_config, # BIFROST '/api/bifrost/status': wu.bifrost.get_status,