Add notifier configuration management for Sentinel and LLM

This commit is contained in:
infinition
2026-03-16 21:54:31 +01:00
parent b759ab6d4b
commit df83cd2e92
7 changed files with 81 additions and 10 deletions

View File

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

View File

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

View File

@@ -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',
}),
])