From 53470cc701ec322080a89d220aef449b25850590 Mon Sep 17 00:00:00 2001 From: MrSphay Date: Sun, 17 May 2026 13:13:38 +0200 Subject: [PATCH] fix: load live dashboard data and add terminal actions --- .env.example | 1 + README.md | 3 ++ crucix.config.mjs | 1 + dashboard/public/jarvis.html | 62 ++++++++++++++++++++++++++++++++++-- server.mjs | 45 +++++++++++++++++++++----- 5 files changed, 102 insertions(+), 10 deletions(-) diff --git a/.env.example b/.env.example index 3ea7f45..a862e47 100644 --- a/.env.example +++ b/.env.example @@ -6,6 +6,7 @@ PORT=3117 REFRESH_INTERVAL_MINUTES=15 AUTO_OPEN_BROWSER=false STALE_DATA_MAX_AGE_MINUTES=60 +TERMINAL_ACTIONS_ENABLED=true SWEEP_TOKEN= BRIEF_VERBOSITY=standard diff --git a/README.md b/README.md index e476fd8..74e69f3 100644 --- a/README.md +++ b/README.md @@ -135,6 +135,7 @@ PORT=3117 REFRESH_INTERVAL_MINUTES=15 AUTO_OPEN_BROWSER=false STALE_DATA_MAX_AGE_MINUTES=60 +TERMINAL_ACTIONS_ENABLED=true SWEEP_TOKEN= BRIEF_VERBOSITY=standard @@ -187,6 +188,8 @@ LLM_MODEL=your-model For Pangolin or another reverse proxy, forward HTTP traffic to `intelligence-terminal:3117` (or the `PORT` you set). Missing API keys do not crash sweeps; affected sources are reported as degraded in `/api/health`. +The dashboard Terminal Actions panel can trigger `status`, `sweep`, and `brief` through `/api/action`. Leave `TERMINAL_ACTIONS_ENABLED=true` for a private home-server deployment. For an internet-exposed deployment, set `SWEEP_TOKEN` and pass it through trusted automation, or set `TERMINAL_ACTIONS_ENABLED=false` to disable browser-triggered actions. If you protect actions with `SWEEP_TOKEN`, the browser can send it from `localStorage.crucix_sweep_token`. + #### Build And Publish Your Gitea Image ```bash diff --git a/crucix.config.mjs b/crucix.config.mjs index 8792df3..19bbce2 100644 --- a/crucix.config.mjs +++ b/crucix.config.mjs @@ -24,6 +24,7 @@ export default { autoOpenBrowser: boolEnv('AUTO_OPEN_BROWSER', false), staleDataMaxAgeMinutes: intEnv('STALE_DATA_MAX_AGE_MINUTES', 60), sweepToken: process.env.SWEEP_TOKEN || null, + terminalActionsEnabled: boolEnv('TERMINAL_ACTIONS_ENABLED', true), llm: { provider: process.env.LLM_PROVIDER || null, // anthropic | openai | gemini | codex | openrouter | minimax | mistral | ollama | grok diff --git a/dashboard/public/jarvis.html b/dashboard/public/jarvis.html index 921b3fd..5607403 100644 --- a/dashboard/public/jarvis.html +++ b/dashboard/public/jarvis.html @@ -83,6 +83,13 @@ html,body{height:100%;background:var(--bg);color:var(--text);font-family:var(--s .sensor-actions{display:flex;gap:6px;align-items:center} .mini-btn{border:1px solid rgba(100,240,200,0.18);background:rgba(100,240,200,0.04);color:var(--dim);font-family:var(--mono);font-size:9px;padding:3px 6px;cursor:pointer} .mini-btn:hover{color:var(--accent);border-color:rgba(100,240,200,0.4)} +.action-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:6px;margin-bottom:10px} +.action-btn{border:1px solid rgba(68,204,255,0.24);background:rgba(68,204,255,0.06);color:var(--text);font-family:var(--mono);font-size:9px;padding:7px 6px;cursor:pointer;text-transform:uppercase;letter-spacing:.08em} +.action-btn:hover{border-color:rgba(68,204,255,0.55);color:var(--accent2);background:rgba(68,204,255,0.12)} +.action-btn[disabled]{opacity:.45;cursor:wait} +.terminal-output{min-height:58px;max-height:180px;overflow:auto;border:1px solid rgba(255,255,255,0.05);background:rgba(0,0,0,0.22);padding:8px;font-family:var(--mono);font-size:10px;line-height:1.45;color:var(--dim);white-space:pre-wrap} +.terminal-output strong{color:var(--accent)} +.terminal-output .err{color:var(--danger)} .layer-left{display:flex;align-items:center;gap:8px} .ldot{width:10px;height:10px;border-radius:50%;flex-shrink:0} .ldot.air{background:var(--accent);box-shadow:0 0 6px rgba(100,240,200,0.4)} @@ -404,6 +411,8 @@ let lowPerfMode = localStorage.getItem('crucix_low_perf') === 'true'; let isFlat = shouldStartFlat(); let layerModes = JSON.parse(localStorage.getItem('crucix_layer_modes') || '{}'); let spaceDisplayMode = localStorage.getItem('crucix_space_display') || 'icons'; +let terminalOutput = 'Ready. Live data is loaded from /api/data in server mode.'; +let terminalBusy = false; let currentRegion = 'world'; let flatSvg, flatProjection, flatPath, flatG, flatZoom, flatW, flatH; @@ -1564,6 +1573,46 @@ function renderLower(){ document.getElementById('lowerGrid').innerHTML=`${tickerPanel}${osintPanel}${macroPanel}${ideasPanel}`; } +async function runTerminalAction(action){ + if(terminalBusy) return; + terminalBusy = true; + terminalOutput = `> ${action}\nRunning...`; + renderRight(); + try{ + const res = await fetch('/api/action', { + method:'POST', + headers:{ + 'Content-Type':'application/json', + ...(localStorage.getItem('crucix_sweep_token') ? {'x-crucix-token': localStorage.getItem('crucix_sweep_token')} : {}) + }, + body:JSON.stringify({action}) + }); + const payload = await res.json().catch(()=>({error:'Invalid server response'})); + if(!res.ok) throw new Error(payload.error || `HTTP ${res.status}`); + if(action === 'status'){ + const h = payload.health || {}; + terminalOutput = [ + '> status', + `State: ${h.status || '--'}`, + `Last sweep: ${h.lastSuccessfulSweep || h.lastSweep || '--'}`, + `Data age: ${h.dataAgeSeconds != null ? h.dataAgeSeconds + 's' : '--'}`, + `Sources: ${h.sourcesOk || 0} ok / ${h.sourcesDegraded || 0} degraded / ${h.sourcesFailed || 0} failed`, + `LLM: ${h.llm?.state || '--'}`, + `Sweep active: ${h.sweepInProgress ? 'yes' : 'no'}` + ].join('\n'); + }else if(action === 'brief'){ + terminalOutput = `> brief\n${payload.text || 'No briefing text returned.'}`; + }else if(action === 'sweep'){ + terminalOutput = `> sweep\n${payload.status === 'already_running' ? 'Sweep already running.' : 'Sweep accepted. The dashboard will update when the sweep finishes.'}`; + } + }catch(err){ + terminalOutput = `> ${action}\nERROR: ${err.message}`; + }finally{ + terminalBusy = false; + renderRight(); + } +} + // === RIGHT RAIL === function renderRight(){ const mobile = isMobileLayout(); @@ -1605,6 +1654,15 @@ function renderRight(){ const deltaHtml = hasDelta ? deltaRows.join('') : `
${t('delta.noChanges','No changes since last sweep')}
`; document.getElementById('rightRail').innerHTML=` +
+

Terminal Actions

${terminalBusy?'RUNNING':'READY'}
+
+ + + +
+
${terminalOutput.replace(/[&<>]/g,c=>({'&':'&','<':'<','>':'>'}[c])).replace(/\n/g,'
')}
+

${t('panels.crossSourceSignals','Cross-Source Signals')}

${t('badges.worldview','WORLDVIEW')}
${signals} @@ -1839,10 +1897,10 @@ document.addEventListener('DOMContentLoaded', () => { const hasInlineData = !!(D && D.meta); const canProbeApi = location.protocol !== 'file:'; - if (canProbeApi && !hasInlineData) { + if (canProbeApi) { // Server mode: always fetch live data from API (ignore any stale inline D) fetch('/api/data') - .then(r => r.json()) + .then(r => { if(!r.ok) throw new Error(`HTTP ${r.status}`); return r.json(); }) .then(data => { D = data; init(); connectSSE(); }) .catch(() => { // Should not reach here — server routes to loading.html when no data diff --git a/server.mjs b/server.mjs index 8f46df8..95949f0 100644 --- a/server.mjs +++ b/server.mjs @@ -289,14 +289,28 @@ app.get('/api/metrics', (req, res) => { }); app.post('/api/sweep', express.json(), (req, res) => { - const remote = req.ip || ''; - const local = remote.includes('127.0.0.1') || remote === '::1' || remote === '::ffff:127.0.0.1'; - const token = req.get('x-crucix-token') || req.query.token || req.body?.token; - if (config.sweepToken && token !== config.sweepToken) return res.status(401).json({ error: 'Invalid sweep token' }); - if (!config.sweepToken && !local) return res.status(403).json({ error: 'Manual sweep is local-only unless SWEEP_TOKEN is set' }); - if (sweepInProgress) return res.status(409).json({ status: 'already_running', sweepStartedAt }); - runSweepCycle().catch(err => console.error('[Crucix] API-triggered sweep failed:', err.message)); - res.status(202).json({ status: 'accepted' }); + if (!canRunTerminalAction(req)) return res.status(403).json({ error: 'Terminal actions disabled or unauthorized' }); + triggerSweep(res); +}); + +app.post('/api/action', express.json(), async (req, res) => { + if (!canRunTerminalAction(req)) return res.status(403).json({ error: 'Terminal actions disabled or unauthorized' }); + const action = String(req.body?.action || req.query.action || '').toLowerCase(); + + if (action === 'status') { + return res.json({ ok: true, action, health: buildHealth() }); + } + + if (action === 'brief') { + if (!currentData) return res.status(503).json({ ok: false, action, error: 'No data yet — first sweep in progress' }); + return res.json({ ok: true, action, text: buildBrief(currentData) }); + } + + if (action === 'sweep') { + return triggerSweep(res); + } + + res.status(400).json({ ok: false, error: 'Unknown action', actions: ['status', 'brief', 'sweep'] }); }); // API: available locales @@ -333,6 +347,20 @@ function dataAgeMs() { return Number.isFinite(ms) ? ms : null; } +function canRunTerminalAction(req) { + const remote = req.ip || ''; + const local = remote.includes('127.0.0.1') || remote === '::1' || remote === '::ffff:127.0.0.1'; + const token = req.get('x-crucix-token') || req.query.token || req.body?.token; + if (config.sweepToken) return token === config.sweepToken; + return Boolean(config.terminalActionsEnabled || local); +} + +function triggerSweep(res) { + if (sweepInProgress) return res.status(409).json({ ok: true, status: 'already_running', sweepStartedAt }); + runSweepCycle().catch(err => console.error('[Crucix] API-triggered sweep failed:', err.message)); + return res.status(202).json({ ok: true, status: 'accepted' }); +} + function getLLMStatus() { if (!config.llm.provider) return { state: 'disabled' }; if (!llmProvider) return { state: 'misconfigured', provider: config.llm.provider }; @@ -376,6 +404,7 @@ function buildHealth() { llm: getLLMStatus(), telegramEnabled: !!(config.telegram.botToken && config.telegram.chatId), discordEnabled: !!(config.discord?.botToken || config.discord?.webhookUrl), + terminalActionsEnabled: Boolean(config.terminalActionsEnabled || config.sweepToken), refreshIntervalMinutes: config.refreshIntervalMinutes, language: currentLanguage, memory: intelligenceStore.status(),