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('') : `