Compare commits
6 Commits
b2f604b120
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 8605d0baab | |||
| 53470cc701 | |||
| 4262c7e939 | |||
| e933586b22 | |||
| 8e096b2697 | |||
| b309bd690e |
@@ -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
|
||||
|
||||
|
||||
@@ -38,7 +38,12 @@ jobs:
|
||||
run: docker compose config
|
||||
|
||||
- name: Build Docker image
|
||||
run: docker build -t "${REGISTRY_HOST}/${REGISTRY_NAMESPACE}/${IMAGE_NAME}:${GITHUB_SHA}" .
|
||||
shell: bash
|
||||
run: |
|
||||
image="${REGISTRY_HOST}/${REGISTRY_NAMESPACE}/${IMAGE_NAME}"
|
||||
build_tag="build-${GITHUB_RUN_ID:-local}-${GITHUB_RUN_NUMBER:-0}"
|
||||
echo "BUILD_IMAGE=${image}:${build_tag}" >> "$GITHUB_ENV"
|
||||
docker build -t "${image}:${build_tag}" .
|
||||
|
||||
- name: Publish Docker image
|
||||
if: ${{ env.REGISTRY_TOKEN != '' }}
|
||||
@@ -47,8 +52,9 @@ jobs:
|
||||
image="${REGISTRY_HOST}/${REGISTRY_NAMESPACE}/${IMAGE_NAME}"
|
||||
date_tag="$(date -u +%Y%m%d)"
|
||||
echo "${REGISTRY_TOKEN}" | docker login "${REGISTRY_HOST}" -u "${REGISTRY_USERNAME}" --password-stdin
|
||||
docker tag "${image}:${GITHUB_SHA}" "${image}:latest"
|
||||
docker tag "${image}:${GITHUB_SHA}" "${image}:${date_tag}"
|
||||
docker tag "${BUILD_IMAGE}" "${image}:${GITHUB_SHA}"
|
||||
docker tag "${BUILD_IMAGE}" "${image}:latest"
|
||||
docker tag "${BUILD_IMAGE}" "${image}:${date_tag}"
|
||||
docker push "${image}:${GITHUB_SHA}"
|
||||
docker push "${image}:latest"
|
||||
docker push "${image}:${date_tag}"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -59,8 +59,7 @@ export async function runSource(name, fn, ...args) {
|
||||
});
|
||||
const data = await Promise.race([dataPromise, timeoutPromise]);
|
||||
const hasError = Boolean(data?.error);
|
||||
const degradedStatuses = ['no_credentials', 'no_key', 'disabled', 'degraded', 'failed', 'error'];
|
||||
const isDegraded = hasError || degradedStatuses.includes(data?.status);
|
||||
const isDegraded = hasError || ['no_credentials', 'degraded', 'failed'].includes(data?.status);
|
||||
return {
|
||||
name,
|
||||
status: isDegraded ? 'degraded' : 'ok',
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
// ADS-B Exchange — Unfiltered Flight Tracking (including Military)
|
||||
// Unlike FlightRadar24/FlightAware, ADS-B Exchange does NOT filter military aircraft.
|
||||
// Public feed access varies; RapidAPI tier available for programmatic use.
|
||||
// This module reports explicit disabled/degraded state instead of making
|
||||
// unavailable aircraft data look live.
|
||||
// This module attempts the public endpoints and falls back to a documented stub.
|
||||
|
||||
import { safeFetch } from '../utils/fetch.mjs';
|
||||
|
||||
@@ -141,7 +140,6 @@ async function fetchViaRapidApi(apiKey) {
|
||||
// Get all military aircraft
|
||||
const data = await safeFetch(`${ENDPOINTS.rapidApi}/mil`, {
|
||||
timeout: 20000,
|
||||
source: 'adsb-rapidapi',
|
||||
headers: {
|
||||
'X-RapidAPI-Key': apiKey,
|
||||
'X-RapidAPI-Host': 'adsbexchange-com1.p.rapidapi.com',
|
||||
@@ -153,26 +151,21 @@ async function fetchViaRapidApi(apiKey) {
|
||||
|
||||
// Attempt to fetch from public feed
|
||||
async function fetchPublicFeed() {
|
||||
const data = await safeFetch(ENDPOINTS.publicFeed, { timeout: 15000, source: 'adsb-public' });
|
||||
const data = await safeFetch(ENDPOINTS.publicFeed, { timeout: 15000 });
|
||||
return data;
|
||||
}
|
||||
|
||||
async function getMilitaryAircraftResult(apiKey) {
|
||||
const failures = [];
|
||||
|
||||
// Get military aircraft from available sources
|
||||
export async function getMilitaryAircraft(apiKey) {
|
||||
// Try RapidAPI first if key available
|
||||
if (apiKey) {
|
||||
const data = await fetchViaRapidApi(apiKey);
|
||||
if (data && !data.error) {
|
||||
const aircraft = data.ac || data.aircraft || [];
|
||||
if (Array.isArray(aircraft)) {
|
||||
return {
|
||||
provider: 'rapidapi',
|
||||
aircraft: aircraft.map(classifyAircraft).filter(a => a.isMilitary),
|
||||
};
|
||||
return aircraft.map(classifyAircraft).filter(a => a.isMilitary);
|
||||
}
|
||||
}
|
||||
failures.push({ provider: 'rapidapi', error: data?.error || 'RapidAPI returned an unsupported payload' });
|
||||
}
|
||||
|
||||
// Try public feed
|
||||
@@ -180,21 +173,11 @@ async function getMilitaryAircraftResult(apiKey) {
|
||||
if (pubData && !pubData.error) {
|
||||
const aircraft = pubData.ac || pubData.aircraft || pubData.states || [];
|
||||
if (Array.isArray(aircraft)) {
|
||||
return {
|
||||
provider: 'public-feed',
|
||||
aircraft: aircraft.map(classifyAircraft).filter(a => a.isMilitary),
|
||||
};
|
||||
return aircraft.map(classifyAircraft).filter(a => a.isMilitary);
|
||||
}
|
||||
}
|
||||
failures.push({ provider: 'public-feed', error: pubData?.error || 'Public feed returned an unsupported payload' });
|
||||
|
||||
return { provider: null, aircraft: null, failures };
|
||||
}
|
||||
|
||||
// Get military aircraft from available sources
|
||||
export async function getMilitaryAircraft(apiKey) {
|
||||
const result = await getMilitaryAircraftResult(apiKey);
|
||||
return result.aircraft;
|
||||
return null; // all sources failed
|
||||
}
|
||||
|
||||
// Get all aircraft in a geographic bounding box via RapidAPI
|
||||
@@ -225,8 +208,7 @@ export async function getAircraftInArea(lat, lon, radiusNm = 250, apiKey) {
|
||||
// Briefing — attempt to get military flight data, document what's available
|
||||
export async function briefing() {
|
||||
const apiKey = process.env.ADSB_API_KEY || process.env.RAPIDAPI_KEY || null;
|
||||
const result = await getMilitaryAircraftResult(apiKey);
|
||||
const militaryAircraft = result.aircraft;
|
||||
const militaryAircraft = await getMilitaryAircraft(apiKey);
|
||||
|
||||
// If we got data, analyze it
|
||||
if (militaryAircraft && militaryAircraft.length > 0) {
|
||||
@@ -273,7 +255,6 @@ export async function briefing() {
|
||||
source: 'ADS-B Exchange',
|
||||
timestamp: new Date().toISOString(),
|
||||
status: 'live',
|
||||
provider: result.provider,
|
||||
totalMilitary: militaryAircraft.length,
|
||||
byCountry,
|
||||
categories: {
|
||||
@@ -288,18 +269,10 @@ export async function briefing() {
|
||||
}
|
||||
|
||||
// No data available — return stub with integration documentation
|
||||
const status = apiKey ? 'degraded' : 'disabled';
|
||||
const error = apiKey
|
||||
? 'ADS-B providers returned no usable aircraft data'
|
||||
: 'ADSB_API_KEY or RAPIDAPI_KEY is not configured';
|
||||
|
||||
return {
|
||||
source: 'ADS-B Exchange',
|
||||
timestamp: new Date().toISOString(),
|
||||
status,
|
||||
provider: result.provider,
|
||||
error,
|
||||
failures: result.failures,
|
||||
status: apiKey ? 'error' : 'no_key',
|
||||
militaryAircraft: [],
|
||||
message: apiKey
|
||||
? 'ADS-B Exchange API returned no data. The endpoint may be temporarily unavailable.'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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('') : `<div style="padding:12px;text-align:center;color:var(--dim);font-family:var(--mono);font-size:10px">${t('delta.noChanges','No changes since last sweep')}</div>`;
|
||||
|
||||
document.getElementById('rightRail').innerHTML=`
|
||||
<div class="g-panel right-actions">
|
||||
<div class="sec-head"><h3>Terminal Actions</h3><span class="badge">${terminalBusy?'RUNNING':'READY'}</span></div>
|
||||
<div class="action-grid">
|
||||
<button class="action-btn" ${terminalBusy?'disabled':''} onclick="runTerminalAction('status')">Status</button>
|
||||
<button class="action-btn" ${terminalBusy?'disabled':''} onclick="runTerminalAction('sweep')">Sweep</button>
|
||||
<button class="action-btn" ${terminalBusy?'disabled':''} onclick="runTerminalAction('brief')">Brief</button>
|
||||
</div>
|
||||
<div class="terminal-output">${terminalOutput.replace(/[&<>]/g,c=>({'&':'&','<':'<','>':'>'}[c])).replace(/\n/g,'<br>')}</div>
|
||||
</div>
|
||||
<div class="g-panel right-signals">
|
||||
<div class="sec-head"><h3>${t('panels.crossSourceSignals','Cross-Source Signals')}</h3><span class="badge">${t('badges.worldview','WORLDVIEW')}</span></div>
|
||||
${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
|
||||
|
||||
@@ -1,18 +1,489 @@
|
||||
# Agent Handoff
|
||||
|
||||
## Current Release Goal
|
||||
Last updated: 2026-05-17
|
||||
|
||||
Source branch: `codex/production-intelligence-terminal`
|
||||
## Repository State
|
||||
|
||||
Registry image:
|
||||
Project: Crucix fork / Intelligence Terminal
|
||||
|
||||
Local workspace:
|
||||
|
||||
```text
|
||||
C:\Users\MrSphay\Documents\Codex\Crucix\intelligence-terminal
|
||||
```
|
||||
|
||||
Remotes:
|
||||
|
||||
```text
|
||||
origin https://git.wilkensxl.de/MrSphay/intelligence-terminal.git
|
||||
upstream https://github.com/calesthio/Crucix.git
|
||||
```
|
||||
|
||||
Current branch tip:
|
||||
|
||||
```text
|
||||
Run `git rev-parse HEAD` after clone/pull. This handoff was updated by the `docs: sync issue tracker and handoff` commit after the implementation commit below.
|
||||
```
|
||||
|
||||
Latest implementation commit before issue-sync documentation:
|
||||
|
||||
```text
|
||||
53470cc701ec322080a89d220aef449b25850590
|
||||
```
|
||||
|
||||
Both pushed branches currently point to this commit:
|
||||
|
||||
```text
|
||||
origin/codex/production-intelligence-terminal
|
||||
origin/main
|
||||
```
|
||||
|
||||
Gitea repository:
|
||||
|
||||
```text
|
||||
https://git.wilkensxl.de/MrSphay/intelligence-terminal
|
||||
```
|
||||
|
||||
Default branch observed through the Gitea API:
|
||||
|
||||
```text
|
||||
codex/production-intelligence-terminal
|
||||
```
|
||||
|
||||
## Agent Kit Requirements Applied
|
||||
|
||||
The mandatory kit was cloned and reviewed first:
|
||||
|
||||
```text
|
||||
C:\Users\MrSphay\Documents\Codex\Crucix\agent-kit
|
||||
```
|
||||
|
||||
Rules applied from the kit:
|
||||
|
||||
- Keep agent context in source control: `AGENTS.md`, `.codex/project.md`, and this handoff file.
|
||||
- Use Gitea Ubuntu runners for heavy verification and package publishing.
|
||||
- Keep Docker/Dockge operation first-class.
|
||||
- Do not commit secrets, `.env`, private logs, tokens, or generated `runs/` data.
|
||||
- Add report-only maintenance workflows for security, dependency checks, repo cleanup, release dry runs, and template compliance.
|
||||
- Poll pushed Gitea Actions until terminal state when a token is available.
|
||||
|
||||
## What Was Implemented
|
||||
|
||||
### Docker And Runtime
|
||||
|
||||
- Docker image is Docker-first and Dockge/Pangolin suitable.
|
||||
- Browser auto-open is disabled by default through `AUTO_OPEN_BROWSER=false`.
|
||||
- Runtime health checks now work in the container without `wget` or host browser tools.
|
||||
- `runs` is persisted through a volume.
|
||||
- A later fix added `docker-entrypoint.sh` to prepare `/app/runs` before dropping privileges, so mounted volumes work with the non-root Node runtime.
|
||||
- `docker-compose.yml` uses the Gitea Registry image by default:
|
||||
|
||||
```text
|
||||
git.wilkensxl.de/mrsphay/intelligence-terminal:latest
|
||||
```
|
||||
|
||||
## Notes
|
||||
### API And Health
|
||||
|
||||
- The repository is Docker-first and should stay suitable for Dockge/Pangolin.
|
||||
- Use `.env.example` as the operator-facing source of truth for configuration.
|
||||
- Source health and network metrics are available through `/api/health` and `/api/metrics`.
|
||||
- If Gitea Registry authentication is unavailable locally, build and push with the commands documented in `README.md`.
|
||||
Added or hardened:
|
||||
|
||||
- `GET /api/health`
|
||||
- `GET /api/data`
|
||||
- `GET /api/metrics`
|
||||
- `POST /api/sweep`
|
||||
- `POST /api/action`
|
||||
|
||||
Health now reports:
|
||||
|
||||
- `starting`
|
||||
- `healthy`
|
||||
- `degraded`
|
||||
- `stale`
|
||||
- `error`
|
||||
|
||||
It also reports:
|
||||
|
||||
- last sweep timestamps
|
||||
- stale/bootstrap state
|
||||
- data age
|
||||
- source health
|
||||
- source errors
|
||||
- LLM configuration state
|
||||
- Telegram/Discord enabled state
|
||||
- memory store state
|
||||
|
||||
### Live Data And Source Degradation
|
||||
|
||||
- Existing `runs/latest.json` is only treated as bootstrap/stale data until a real sweep completes.
|
||||
- Sweeps update `sourceHealth`, SSE/API data, and memory state.
|
||||
- RSS/news feed failures no longer silently look like fresh valid data.
|
||||
- `safeFetch` now tracks request counts, failures, bytes, source labels, hosts, and recent fetch events.
|
||||
- `safeFetch` has better timeout/retry/backoff/error behavior and reports HTML-as-API-error cases.
|
||||
- Yahoo Finance fetches are more explicit about source errors and HTML/API failures.
|
||||
- ACLED missing credentials now degrade transparently.
|
||||
- Telegram polling has quieter network-error backoff logs.
|
||||
|
||||
### LLM Integration
|
||||
|
||||
Added unified OpenAI-compatible provider layer:
|
||||
|
||||
```text
|
||||
lib/llm/openai-compatible.mjs
|
||||
```
|
||||
|
||||
Supported provider paths include:
|
||||
|
||||
- `openrouter`
|
||||
- `openai`
|
||||
- `openai-compatible`
|
||||
- `local-openai`
|
||||
- `lmstudio`
|
||||
- `lm-studio`
|
||||
- `ollama`
|
||||
|
||||
Relevant environment keys:
|
||||
|
||||
```text
|
||||
LLM_PROVIDER
|
||||
LLM_BASE_URL
|
||||
LLM_API_KEY
|
||||
LLM_MODEL
|
||||
LLM_TEMPERATURE
|
||||
LLM_MAX_TOKENS
|
||||
LLM_TIMEOUT_MS
|
||||
OPENROUTER_SITE_URL
|
||||
OPENROUTER_APP_NAME
|
||||
```
|
||||
|
||||
OpenRouter Free and local OpenAI-compatible endpoints are documented in `README.md` and `.env.example`.
|
||||
|
||||
### Memory
|
||||
|
||||
Added Phase-1 SQLite memory:
|
||||
|
||||
```text
|
||||
lib/intelligence-store.mjs
|
||||
runs/intelligence.db
|
||||
```
|
||||
|
||||
It uses `node:sqlite` when available and gracefully falls back when unavailable.
|
||||
|
||||
### Dashboard
|
||||
|
||||
Implemented:
|
||||
|
||||
- interactive Sensor Grid layer modes
|
||||
- focus/hide/normal states persisted in `localStorage`
|
||||
- Space Watch icon/orbit toggle
|
||||
- map/globe filtering consistency
|
||||
- flat map label redraw handling
|
||||
- live server-mode data loading from `/api/data` even when `jarvis.html` still contains an offline inline snapshot
|
||||
- Terminal Actions panel with `Status`, `Sweep`, and `Brief` buttons
|
||||
|
||||
Important UI markers in the final code:
|
||||
|
||||
```text
|
||||
layerModes
|
||||
spaceDisplayMode
|
||||
toggleSpaceDisplay()
|
||||
shouldShowType()
|
||||
runTerminalAction()
|
||||
```
|
||||
|
||||
### Briefings
|
||||
|
||||
Brief output now includes:
|
||||
|
||||
- Source Integrity
|
||||
- evidence links
|
||||
- event IDs
|
||||
- configurable verbosity through `BRIEF_VERBOSITY`
|
||||
|
||||
### Documentation
|
||||
|
||||
Updated:
|
||||
|
||||
- `README.md`
|
||||
- `.env.example`
|
||||
- `docs/sources/README.md`
|
||||
- `docs/sources/opensky.md`
|
||||
- `docs/sources/acled.md`
|
||||
- `docs/sources/telegram.md`
|
||||
- `docs/sources/firms.md`
|
||||
- `docs/sources/maritime.md`
|
||||
- `docs/security-review.md`
|
||||
- `docs/release-checklist.md`
|
||||
|
||||
README includes:
|
||||
|
||||
- Gitea Registry pull example
|
||||
- Dockge-compatible compose example
|
||||
- full `.env` examples
|
||||
- OpenRouter Free setup
|
||||
- LM Studio setup
|
||||
- Ollama setup
|
||||
- local OpenAI-compatible setup
|
||||
- Pangolin/reverse proxy notes
|
||||
|
||||
## Registry And Images
|
||||
|
||||
Registry image:
|
||||
|
||||
```text
|
||||
git.wilkensxl.de/mrsphay/intelligence-terminal
|
||||
```
|
||||
|
||||
Verified package tags through Gitea API:
|
||||
|
||||
```text
|
||||
latest
|
||||
20260517
|
||||
e933586b220656a2858d2215b934b22d1f08a908
|
||||
53470cc701ec322080a89d220aef449b25850590
|
||||
```
|
||||
|
||||
Successful pull test:
|
||||
|
||||
```bash
|
||||
docker pull git.wilkensxl.de/mrsphay/intelligence-terminal:latest
|
||||
```
|
||||
|
||||
Observed digest:
|
||||
|
||||
```text
|
||||
sha256:780a41413921bd9a676461eca1cd1372591f523be4b7c9513d9bc085cbe7922d
|
||||
```
|
||||
|
||||
## Gitea Actions
|
||||
|
||||
Workflows present:
|
||||
|
||||
```text
|
||||
.gitea/workflows/build.yml
|
||||
.gitea/workflows/security-scan.yml
|
||||
.gitea/workflows/repo-cleanup.yml
|
||||
.gitea/workflows/dependency-check.yml
|
||||
.gitea/workflows/release-dry-run.yml
|
||||
.gitea/workflows/template-compliance.yml
|
||||
```
|
||||
|
||||
Final runs for commit `53470cc701ec322080a89d220aef449b25850590` were polled through the Gitea API and succeeded:
|
||||
|
||||
```text
|
||||
build.yml on main: success
|
||||
build.yml on codex/production-intelligence-terminal: success
|
||||
release-dry-run.yml on main: success
|
||||
release-dry-run.yml on codex/production-intelligence-terminal: success
|
||||
template-compliance.yml on main: success
|
||||
template-compliance.yml on codex/production-intelligence-terminal: success
|
||||
```
|
||||
|
||||
Relevant run URLs:
|
||||
|
||||
```text
|
||||
https://git.wilkensxl.de/MrSphay/intelligence-terminal/actions/runs/23
|
||||
https://git.wilkensxl.de/MrSphay/intelligence-terminal/actions/runs/24
|
||||
https://git.wilkensxl.de/MrSphay/intelligence-terminal/actions/runs/25
|
||||
https://git.wilkensxl.de/MrSphay/intelligence-terminal/actions/runs/26
|
||||
https://git.wilkensxl.de/MrSphay/intelligence-terminal/actions/runs/27
|
||||
https://git.wilkensxl.de/MrSphay/intelligence-terminal/actions/runs/28
|
||||
```
|
||||
|
||||
Repository secret expected by the registry publish workflow:
|
||||
|
||||
```text
|
||||
REGISTRY_TOKEN
|
||||
```
|
||||
|
||||
Local token note:
|
||||
|
||||
- `GITEA_TOKEN` was visible in the final Codex process.
|
||||
- It was used only for Gitea API checks and not printed.
|
||||
|
||||
## Issue Sync
|
||||
|
||||
Open upstream GitHub issues were reviewed on 2026-05-17 from:
|
||||
|
||||
```text
|
||||
https://github.com/calesthio/Crucix/issues
|
||||
```
|
||||
|
||||
The upstream list contained 24 open issues. Issues already handled by this fork were not copied as open work, including the Docker stale-dashboard incident (#105), map label redraw (#70), Sensor Grid controls (#72), space display toggle (#51), source docs (#52), Dockge/CasaOS docs (#78), LLM timeout (#87), inject/static helper confusion (#100), network metrics (#101), Telegram polling backoff (#104), and briefing/evidence context (#75).
|
||||
|
||||
Issues not relevant to this fork were also not copied, including the Wallpaper Engine redesign (#41), the fork-inflation discussion (#107), empty/unclear placeholders (#79/#80), and the general use-case discussion (#93).
|
||||
|
||||
The following Gitea issues were created for real remaining work:
|
||||
|
||||
```text
|
||||
#1 Reddit source must stop unauthenticated .json scraping
|
||||
https://git.wilkensxl.de/MrSphay/intelligence-terminal/issues/1
|
||||
|
||||
#2 Send operator alerts when dashboard data remains stale
|
||||
https://git.wilkensxl.de/MrSphay/intelligence-terminal/issues/2
|
||||
|
||||
#3 ACLED credentialed integration needs regression test and diagnostics
|
||||
https://git.wilkensxl.de/MrSphay/intelligence-terminal/issues/3
|
||||
|
||||
#4 Complete memory and prediction loop beyond Phase-1 SQLite
|
||||
https://git.wilkensxl.de/MrSphay/intelligence-terminal/issues/4
|
||||
|
||||
#5 Remove old inline dashboard snapshot from production builds
|
||||
https://git.wilkensxl.de/MrSphay/intelligence-terminal/issues/5
|
||||
|
||||
#6 Harden Terminal Actions for public reverse-proxy deployments
|
||||
https://git.wilkensxl.de/MrSphay/intelligence-terminal/issues/6
|
||||
|
||||
#7 Replace ADS-B stub with real disabled/degraded source handling
|
||||
https://git.wilkensxl.de/MrSphay/intelligence-terminal/issues/7
|
||||
|
||||
#8 Clean inherited public-demo and upstream marketing references
|
||||
https://git.wilkensxl.de/MrSphay/intelligence-terminal/issues/8
|
||||
```
|
||||
|
||||
## Verification Already Performed
|
||||
|
||||
Local lightweight checks:
|
||||
|
||||
```bash
|
||||
npm run test:unit
|
||||
npm audit --omit=dev --audit-level=high
|
||||
docker compose --env-file .env.example config
|
||||
node --check server.mjs
|
||||
node --check dashboard/inject.mjs
|
||||
node --check lib/llm/openai-compatible.mjs
|
||||
git diff --check
|
||||
```
|
||||
|
||||
Unit test result:
|
||||
|
||||
```text
|
||||
21 tests passing
|
||||
0 failing
|
||||
```
|
||||
|
||||
Audit result:
|
||||
|
||||
```text
|
||||
0 high vulnerabilities
|
||||
```
|
||||
|
||||
Docker build and smoke test were performed locally earlier:
|
||||
|
||||
```bash
|
||||
docker build -t git.wilkensxl.de/mrsphay/intelligence-terminal:latest .
|
||||
docker run --rm -d --name intelligence-terminal-smoke -p 127.0.0.1::3117 -e AUTO_OPEN_BROWSER=false git.wilkensxl.de/mrsphay/intelligence-terminal:latest
|
||||
```
|
||||
|
||||
Smoke test observations:
|
||||
|
||||
- Server booted.
|
||||
- No `xdg-open` error.
|
||||
- Initial sweep completed.
|
||||
- `/api/health` moved from `starting` to `degraded` with transparent source errors.
|
||||
- Degraded state was expected without all optional API keys.
|
||||
|
||||
Additional checks after fixing the dashboard live-data bug and Terminal Actions:
|
||||
|
||||
```bash
|
||||
node --check server.mjs
|
||||
npm run test:unit
|
||||
docker compose --env-file .env.example config
|
||||
git diff --check
|
||||
```
|
||||
|
||||
The dashboard script was also syntax-checked after extracting script blocks from `dashboard/public/jarvis.html`.
|
||||
|
||||
## Important Commits
|
||||
|
||||
```text
|
||||
7e85a54 chore: apply agent kit project structure
|
||||
85f97bb feat: harden intelligence runtime and llm providers
|
||||
42b7fc2 docs: add registry dockge and dashboard operations
|
||||
d072390 ci: align gitea workflows with agent kit
|
||||
0559481 ci: fix gitea registry publish login
|
||||
f3c9331 ci: fix agent kit compliance checks
|
||||
c2d572e fix: prepare runs volume before dropping privileges
|
||||
8e096b2 ci: harden gitea workflow reruns
|
||||
e933586 merge: reconcile main with production branch
|
||||
4262c7e docs: expand agent handoff
|
||||
53470cc fix: load live dashboard data and add terminal actions
|
||||
```
|
||||
|
||||
The large implementation commit `85f97bb` and the dashboard/action fix `53470cc` are contained in both:
|
||||
|
||||
```text
|
||||
origin/codex/production-intelligence-terminal
|
||||
origin/main
|
||||
```
|
||||
|
||||
## How To Continue In A Fresh Codex Environment
|
||||
|
||||
1. Clone the Gitea repository:
|
||||
|
||||
```bash
|
||||
git clone https://git.wilkensxl.de/MrSphay/intelligence-terminal.git
|
||||
cd intelligence-terminal
|
||||
git checkout codex/production-intelligence-terminal
|
||||
```
|
||||
|
||||
2. Confirm the expected commit:
|
||||
|
||||
```bash
|
||||
git rev-parse HEAD
|
||||
```
|
||||
|
||||
Expected:
|
||||
|
||||
```text
|
||||
The branch tip should include commit 53470cc701ec322080a89d220aef449b25850590 and the later `docs: sync issue tracker and handoff` commit.
|
||||
```
|
||||
|
||||
3. Read these files first:
|
||||
|
||||
```text
|
||||
AGENTS.md
|
||||
.codex/project.md
|
||||
docs/agent-handoff.md
|
||||
README.md
|
||||
.env.example
|
||||
```
|
||||
|
||||
4. If checking Actions, use `GITEA_TOKEN` from the environment. Do not print it.
|
||||
|
||||
PowerShell check:
|
||||
|
||||
```powershell
|
||||
if ($env:GITEA_TOKEN) { "GITEA_TOKEN=set" } else { "GITEA_TOKEN=missing" }
|
||||
```
|
||||
|
||||
5. Useful commands:
|
||||
|
||||
```bash
|
||||
npm run test:unit
|
||||
docker compose --env-file .env.example config
|
||||
docker pull git.wilkensxl.de/mrsphay/intelligence-terminal:latest
|
||||
```
|
||||
|
||||
6. Start with Dockge/Pangolin using the README compose example and a `.env` based on `.env.example`.
|
||||
|
||||
## Remaining Risks And Follow-Ups
|
||||
|
||||
- Some sources will report `degraded` until optional keys are set, especially ACLED, FRED, EIA, and Cloudflare Radar.
|
||||
- OpenSky can rate-limit with HTTP 429; this is now visible in health instead of hidden.
|
||||
- GDELT/OFAC can time out under runner/network conditions; health reports this explicitly.
|
||||
- Browser-level visual verification of the full dashboard should be repeated after any future UI change.
|
||||
- The project still inherits the original Crucix broad source surface. Future work should prefer focused source-by-source tests over broad refactors.
|
||||
- If a new Codex environment sees non-fast-forward branch pushes, fetch first and preserve remote commits. Do not force-push without explicit approval.
|
||||
|
||||
## Operator Pull Command
|
||||
|
||||
For deployment:
|
||||
|
||||
```bash
|
||||
docker pull git.wilkensxl.de/mrsphay/intelligence-terminal:latest
|
||||
```
|
||||
|
||||
For a pinned deployment:
|
||||
|
||||
```bash
|
||||
docker pull git.wilkensxl.de/mrsphay/intelligence-terminal:20260517
|
||||
```
|
||||
|
||||
@@ -5,12 +5,21 @@
|
||||
- Shell execution: browser auto-open is gated by `AUTO_OPEN_BROWSER` and defaults to false.
|
||||
- Secrets: `.env` remains ignored; `.env.example` contains no real keys.
|
||||
- External network calls: source fetches use timeout/retry diagnostics and expose degraded state.
|
||||
- Manual actions: `/api/sweep` is local-only unless `SWEEP_TOKEN` is configured.
|
||||
- Manual actions: `/api/sweep` and `/api/action` are gated by `TERMINAL_ACTIONS_ENABLED` and local-only or `SWEEP_TOKEN` authorization.
|
||||
- File writes: runtime writes are limited to `runs/`.
|
||||
- HTML injection: dashboard data is JSON-injected only by the CLI path; server mode serves data through API/SSE.
|
||||
|
||||
## Terminal Actions
|
||||
|
||||
- `TERMINAL_ACTIONS_ENABLED=true` enables dashboard-triggered `status`, `sweep`, and `brief` actions through `POST /api/action`.
|
||||
- If `SWEEP_TOKEN` is set, callers must send the token through `x-sweep-token`, `Authorization: Bearer ...`, or the `token` request body field.
|
||||
- If `SWEEP_TOKEN` is empty, actions are accepted only from local loopback addresses.
|
||||
- For private Dockge/LAN deployments, this is intended to make the terminal operable from the browser.
|
||||
- For Pangolin or other internet-exposed deployments, set `SWEEP_TOKEN` or `TERMINAL_ACTIONS_ENABLED=false` until the public reverse-proxy hardening issue is completed.
|
||||
|
||||
## Residual Risk
|
||||
|
||||
- External feeds can return malformed, stale, or adversarial content. UI rendering should continue to sanitize titles and URLs.
|
||||
- LLM outputs are advisory only and must not be treated as financial advice.
|
||||
- `node:sqlite` availability depends on the Node 22 build; when unavailable the memory database degrades to a no-op placeholder.
|
||||
- Browser-stored sweep tokens are acceptable for a trusted home-server UI, but should not be treated as a strong auth boundary on a public endpoint.
|
||||
|
||||
@@ -16,4 +16,3 @@ Source docs:
|
||||
- [Telegram](telegram.md)
|
||||
- [FIRMS](firms.md)
|
||||
- [Maritime](maritime.md)
|
||||
- [ADS-B](adsb.md)
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
# ADS-B Source
|
||||
|
||||
ADS-B Exchange support is optional and intended for unfiltered aircraft and military-flight awareness.
|
||||
|
||||
- Source module: `apis/sources/adsb.mjs`
|
||||
- Preferred provider: ADS-B Exchange via RapidAPI
|
||||
- Credentials: `ADSB_API_KEY` or `RAPIDAPI_KEY`
|
||||
- Runtime status without credentials: `disabled`
|
||||
- Runtime status when providers fail: `degraded`
|
||||
- Runtime status with usable aircraft payloads: `live`
|
||||
|
||||
The source does not treat a missing key or unavailable public feed as normal live data. `/api/health` and `/api/metrics` surface the degraded source state through the sweep source summary.
|
||||
|
||||
Known failure modes:
|
||||
|
||||
- Missing `ADSB_API_KEY` / `RAPIDAPI_KEY`: source is disabled with operator guidance.
|
||||
- RapidAPI rejects or rate-limits the request: source is degraded and records provider failure detail.
|
||||
- Public feed is blocked, rate-limited, or changes shape: source remains degraded instead of returning stale-looking data.
|
||||
|
||||
Register for the provider documented in the README, then set:
|
||||
|
||||
```env
|
||||
ADSB_API_KEY=<rapidapi-key>
|
||||
```
|
||||
@@ -12,7 +12,7 @@
|
||||
"brief:save": "node apis/save-briefing.mjs",
|
||||
"diag": "node diag.mjs",
|
||||
"test": "npm run test:unit",
|
||||
"test:unit": "node --test test/llm-openrouter.test.mjs test/llm-ollama.test.mjs test/llm-openai-compatible.test.mjs test/fetch-utils.test.mjs test/adsb.test.mjs",
|
||||
"test:unit": "node --test test/llm-openrouter.test.mjs test/llm-ollama.test.mjs test/llm-openai-compatible.test.mjs test/fetch-utils.test.mjs",
|
||||
"compose:config": "docker compose config",
|
||||
"clean": "node scripts/clean.mjs",
|
||||
"fresh-start": "npm run clean && npm start"
|
||||
|
||||
45
server.mjs
45
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(),
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
async function withFetch(mockFetch, fn) {
|
||||
const originalFetch = globalThis.fetch;
|
||||
const originalAdsbKey = process.env.ADSB_API_KEY;
|
||||
const originalRapidKey = process.env.RAPIDAPI_KEY;
|
||||
globalThis.fetch = mockFetch;
|
||||
delete process.env.ADSB_API_KEY;
|
||||
delete process.env.RAPIDAPI_KEY;
|
||||
try {
|
||||
return await fn();
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
if (originalAdsbKey === undefined) delete process.env.ADSB_API_KEY;
|
||||
else process.env.ADSB_API_KEY = originalAdsbKey;
|
||||
if (originalRapidKey === undefined) delete process.env.RAPIDAPI_KEY;
|
||||
else process.env.RAPIDAPI_KEY = originalRapidKey;
|
||||
}
|
||||
}
|
||||
|
||||
function jsonResponse(payload, ok = true, status = 200) {
|
||||
return {
|
||||
ok,
|
||||
status,
|
||||
headers: { get: () => 'application/json' },
|
||||
text: async () => JSON.stringify(payload),
|
||||
};
|
||||
}
|
||||
|
||||
test('ADS-B reports disabled when no key is configured and public feed fails', async () => {
|
||||
await withFetch(async () => jsonResponse({ error: 'blocked' }, false, 403), async () => {
|
||||
const { briefing } = await import('../apis/sources/adsb.mjs');
|
||||
const data = await briefing();
|
||||
|
||||
assert.equal(data.status, 'disabled');
|
||||
assert.match(data.error, /ADSB_API_KEY|RAPIDAPI_KEY/);
|
||||
assert.equal(data.militaryAircraft.length, 0);
|
||||
});
|
||||
});
|
||||
|
||||
test('ADS-B reports degraded when RapidAPI and public feed fail', async () => {
|
||||
await withFetch(async () => jsonResponse({ error: 'unavailable' }, false, 503), async () => {
|
||||
process.env.ADSB_API_KEY = 'test-key';
|
||||
const { briefing } = await import('../apis/sources/adsb.mjs');
|
||||
const data = await briefing();
|
||||
|
||||
assert.equal(data.status, 'degraded');
|
||||
assert.match(data.error, /providers returned no usable/);
|
||||
assert.equal(data.failures.length, 2);
|
||||
});
|
||||
});
|
||||
|
||||
test('ADS-B returns live RapidAPI military aircraft payloads', async () => {
|
||||
await withFetch(async () => jsonResponse({
|
||||
ac: [{
|
||||
hex: 'AE1234',
|
||||
flight: 'RCH123',
|
||||
t: 'KC135',
|
||||
lat: 50,
|
||||
lon: 8,
|
||||
mil: true,
|
||||
}],
|
||||
}), async () => {
|
||||
process.env.ADSB_API_KEY = 'test-key';
|
||||
const { briefing } = await import('../apis/sources/adsb.mjs');
|
||||
const data = await briefing();
|
||||
|
||||
assert.equal(data.status, 'live');
|
||||
assert.equal(data.provider, 'rapidapi');
|
||||
assert.equal(data.totalMilitary, 1);
|
||||
assert.equal(data.militaryAircraft[0].callsign, 'RCH123');
|
||||
});
|
||||
});
|
||||
|
||||
test('runSource treats disabled source status as degraded health', async () => {
|
||||
const { runSource } = await import('../apis/briefing.mjs');
|
||||
const result = await runSource('ADS-B', async () => ({ status: 'disabled', message: 'missing key' }));
|
||||
|
||||
assert.equal(result.status, 'degraded');
|
||||
assert.equal(result.error, null);
|
||||
});
|
||||
Reference in New Issue
Block a user