Compare commits
16 Commits
7d2acca4e3
...
codex/issu
| Author | SHA1 | Date | |
|---|---|---|---|
| 28c5e7955a | |||
| 5c4bf80eb0 | |||
| e0e408d1eb | |||
| c159c83a07 | |||
|
|
a1d415e449 | ||
|
|
0f5f9c5f91 | ||
| 096544f6e6 | |||
|
|
5a3dbc6252 | ||
| 9f2083a324 | |||
| dd08ecaf27 | |||
|
|
bc354e7bc5 | ||
| 1c2b48f588 | |||
|
|
a590bf62c2 | ||
|
|
e4834cd3cd | ||
|
|
c102017b16 | ||
|
|
2025ae09db |
@@ -18,7 +18,7 @@ Production-ready Crucix fork for Docker, Dockge, Pangolin, local OSINT sweeps, s
|
||||
- `npm run test:unit`
|
||||
- `npm test`
|
||||
- `docker compose config`
|
||||
- `docker build -t git.wilkensxl.de/mrsphay/intelligence-terminal:latest .`
|
||||
- `docker build -t git.wilkensxl.de/code-inc/intelligence-terminal:latest .`
|
||||
|
||||
Heavy install/build/audit/release work should run on Gitea Ubuntu runners where possible. Local work should stay limited to targeted verification and Docker checks required for this deployment.
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ TERMINAL_ACTION_RATE_LIMIT_MAX=10
|
||||
BRIEF_VERBOSITY=standard
|
||||
|
||||
# LLM layer
|
||||
# Providers: openrouter | openai-compatible | lmstudio | ollama | openai | anthropic | gemini | mistral | minimax | grok | codex
|
||||
# Providers: litellm | openrouter | openai-compatible | lmstudio | ollama | openai | anthropic | gemini | mistral | minimax | grok | codex
|
||||
LLM_PROVIDER=openrouter
|
||||
LLM_BASE_URL=https://openrouter.ai/api/v1
|
||||
LLM_API_KEY=
|
||||
@@ -24,10 +24,11 @@ LLM_MODEL=openrouter/free
|
||||
LLM_TEMPERATURE=0.2
|
||||
LLM_MAX_TOKENS=2000
|
||||
LLM_TIMEOUT_MS=90000
|
||||
OPENROUTER_SITE_URL=https://git.wilkensxl.de/MrSphay/intelligence-terminal
|
||||
OPENROUTER_SITE_URL=https://git.wilkensxl.de/Code-Inc/intelligence-terminal
|
||||
OPENROUTER_APP_NAME=Intelligence Terminal
|
||||
|
||||
# Local OpenAI-compatible examples
|
||||
# LiteLLM: LLM_PROVIDER=litellm, LLM_BASE_URL=https://llm.example.com/v1, LLM_API_KEY=your-proxy-key, LLM_MODEL=your-model-alias
|
||||
# LM Studio: LLM_PROVIDER=lmstudio, LLM_BASE_URL=http://host.docker.internal:1234/v1, LLM_MODEL=local-model
|
||||
# Ollama: LLM_PROVIDER=ollama, LLM_BASE_URL=http://host.docker.internal:11434, LLM_MODEL=llama3.1:8b
|
||||
# Generic: LLM_PROVIDER=openai-compatible, LLM_BASE_URL=http://host.docker.internal:8000/v1, LLM_MODEL=your-model
|
||||
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
env:
|
||||
REGISTRY_HOST: git.wilkensxl.de
|
||||
REGISTRY_USERNAME: MrSphay
|
||||
REGISTRY_NAMESPACE: mrsphay
|
||||
REGISTRY_NAMESPACE: code-inc
|
||||
IMAGE_NAME: intelligence-terminal
|
||||
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||
steps:
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
docker build -t "${image}:${build_tag}" .
|
||||
|
||||
- name: Publish Docker image
|
||||
if: ${{ env.REGISTRY_TOKEN != '' }}
|
||||
if: ${{ env.REGISTRY_TOKEN != '' && github.event_name == 'push' && github.ref == 'refs/heads/codex/production-intelligence-terminal' }}
|
||||
shell: bash
|
||||
run: |
|
||||
image="${REGISTRY_HOST}/${REGISTRY_NAMESPACE}/${IMAGE_NAME}"
|
||||
|
||||
@@ -18,8 +18,8 @@ Intelligence Terminal is a Docker-first Crucix fork for home-server OSINT, marke
|
||||
- Unit tests: `npm run test:unit`
|
||||
- Full tests: `npm test`
|
||||
- Compose validation: `docker compose config`
|
||||
- Docker image: `docker build -t git.wilkensxl.de/mrsphay/intelligence-terminal:latest .`
|
||||
- Docker image: `docker build -t git.wilkensxl.de/code-inc/intelligence-terminal:latest .`
|
||||
|
||||
## Release Target
|
||||
|
||||
Push source to `https://git.wilkensxl.de/MrSphay/intelligence-terminal.git` and publish the Docker image to `git.wilkensxl.de/mrsphay/intelligence-terminal`.
|
||||
Push source to `https://git.wilkensxl.de/Code-Inc/intelligence-terminal.git` and publish the Docker image to `git.wilkensxl.de/code-inc/intelligence-terminal`.
|
||||
|
||||
81
README.md
81
README.md
@@ -2,10 +2,10 @@
|
||||
|
||||
# Intelligence Terminal
|
||||
|
||||
**Modrinth-app-inspired operator dashboard. 27 open sources. Docker-first. No telemetry.**
|
||||
**Self-hosted intelligence dashboard. 27 open sources. Docker-first. No telemetry.**
|
||||
|
||||
[](https://git.wilkensxl.de/MrSphay/intelligence-terminal)
|
||||
[](https://git.wilkensxl.de/MrSphay/-/packages/container/intelligence-terminal/latest)
|
||||
[](https://git.wilkensxl.de/Code-Inc/intelligence-terminal)
|
||||
[](https://git.wilkensxl.de/Code-Inc/-/packages/container/intelligence-terminal/latest)
|
||||
|
||||
[](#quick-start)
|
||||
[](LICENSE)
|
||||
@@ -32,12 +32,10 @@
|
||||
|
||||
> **Supported deployment:** private home-server or lab deployment through Docker, Dockge, Pangolin, or local Node.js.
|
||||
> Runtime data stays in your configured `runs/` volume and API keys are operator-owned.
|
||||
> **Source:** [git.wilkensxl.de/MrSphay/intelligence-terminal](https://git.wilkensxl.de/MrSphay/intelligence-terminal)
|
||||
> **Source:** [git.wilkensxl.de/Code-Inc/intelligence-terminal](https://git.wilkensxl.de/Code-Inc/intelligence-terminal)
|
||||
> Pull the image or clone the repository to run Intelligence Terminal on your own infrastructure.
|
||||
>
|
||||
> **Design transparency:** the dashboard is inspired by app-style marketplace UX patterns, especially dark desktop app shells with a strong left navigation. It does not use Modrinth branding, logos, or assets and is not affiliated with Modrinth.
|
||||
|
||||
Intelligence Terminal pulls satellite fire detection, flight tracking, radiation monitoring, satellite constellation tracking, economic indicators, live market prices, conflict data, sanctions lists, and social sentiment from 27 open-source intelligence feeds in parallel, every 15 minutes, and renders everything in a dark, app-style operator workspace.
|
||||
Intelligence Terminal pulls satellite fire detection, flight tracking, radiation monitoring, satellite constellation tracking, economic indicators, live market prices, conflict data, sanctions lists, and social sentiment from 27 open-source intelligence feeds in parallel, every 15 minutes, and renders everything on a single self-contained dashboard.
|
||||
|
||||
Hook it up to an LLM and it becomes a **two-way intelligence assistant**: pushing multi-tier alerts to Telegram and Discord when something meaningful changes, responding to commands like `/brief` and `/sweep` from your phone, and generating trade ideas grounded in real cross-domain data.
|
||||
|
||||
@@ -68,7 +66,7 @@ It was built for anyone who wants to understand what's actually happening in the
|
||||
|
||||
```bash
|
||||
# 1. Clone the repo
|
||||
git clone https://git.wilkensxl.de/MrSphay/intelligence-terminal.git
|
||||
git clone https://git.wilkensxl.de/Code-Inc/intelligence-terminal.git
|
||||
cd intelligence-terminal
|
||||
|
||||
# 2. Install dependencies (just Express)
|
||||
@@ -94,7 +92,7 @@ The dashboard opens automatically at `http://localhost:3117` and immediately beg
|
||||
### Docker
|
||||
|
||||
```bash
|
||||
docker pull git.wilkensxl.de/mrsphay/intelligence-terminal:latest
|
||||
docker pull git.wilkensxl.de/code-inc/intelligence-terminal:latest
|
||||
```
|
||||
|
||||
Dashboard at `http://localhost:3117`. Sweep data persists in `./runs/` via volume mount. The container disables browser auto-open by default, exposes `/api/health` and `/api/metrics`, and is suitable for Dockge/Pangolin.
|
||||
@@ -104,7 +102,7 @@ Dashboard at `http://localhost:3117`. Sweep data persists in `./runs/` via volum
|
||||
```yaml
|
||||
services:
|
||||
intelligence-terminal:
|
||||
image: git.wilkensxl.de/mrsphay/intelligence-terminal:latest
|
||||
image: git.wilkensxl.de/code-inc/intelligence-terminal:latest
|
||||
container_name: intelligence-terminal
|
||||
env_file:
|
||||
- path: .env
|
||||
@@ -148,7 +146,7 @@ LLM_MODEL=openrouter/free
|
||||
LLM_TEMPERATURE=0.2
|
||||
LLM_MAX_TOKENS=2000
|
||||
LLM_TIMEOUT_MS=90000
|
||||
OPENROUTER_SITE_URL=https://git.wilkensxl.de/MrSphay/intelligence-terminal
|
||||
OPENROUTER_SITE_URL=https://git.wilkensxl.de/Code-Inc/intelligence-terminal
|
||||
OPENROUTER_APP_NAME=Intelligence Terminal
|
||||
|
||||
FRED_API_KEY=
|
||||
@@ -171,6 +169,12 @@ DISCORD_WEBHOOK_URL=
|
||||
Local LLM examples:
|
||||
|
||||
```env
|
||||
# LiteLLM proxy (the URL must include the OpenAI-compatible /v1 path)
|
||||
LLM_PROVIDER=litellm
|
||||
LLM_BASE_URL=https://llm.example.com/v1
|
||||
LLM_API_KEY=your-litellm-api-key
|
||||
LLM_MODEL=your-model-alias
|
||||
|
||||
# LM Studio
|
||||
LLM_PROVIDER=lmstudio
|
||||
LLM_BASE_URL=http://host.docker.internal:1234/v1
|
||||
@@ -251,6 +255,8 @@ Recommended proxy settings:
|
||||
|
||||
If you raise the heartbeat interval, keep it shorter than the lowest idle timeout in the proxy chain.
|
||||
|
||||
`/api/metrics` includes network health grouped by host and source/provider. Source modules should use `safeFetch(url, { source: 'SourceName' })`; when omitted, the shared helper infers a stable provider bucket from the URL host instead of grouping normal source traffic under `unknown`. Raw fetch exceptions are documented in [Source Fetch Instrumentation](docs/source-fetch-instrumentation.md).
|
||||
|
||||
#### Scenario Watchlist
|
||||
|
||||
Intelligence Terminal can track operator hypotheses across sweeps with a runtime scenario file at `runs/scenarios.json`. On first run, the server creates three disabled starter examples:
|
||||
@@ -288,10 +294,10 @@ Scenario states are `dormant`, `watching`, `building`, and `confirmed`. The dash
|
||||
|
||||
```bash
|
||||
docker login git.wilkensxl.de -u MrSphay
|
||||
docker build -t git.wilkensxl.de/mrsphay/intelligence-terminal:latest .
|
||||
docker tag git.wilkensxl.de/mrsphay/intelligence-terminal:latest git.wilkensxl.de/mrsphay/intelligence-terminal:20260516
|
||||
docker push git.wilkensxl.de/mrsphay/intelligence-terminal:latest
|
||||
docker push git.wilkensxl.de/mrsphay/intelligence-terminal:20260516
|
||||
docker build -t git.wilkensxl.de/code-inc/intelligence-terminal:latest .
|
||||
docker tag git.wilkensxl.de/code-inc/intelligence-terminal:latest git.wilkensxl.de/code-inc/intelligence-terminal:YYYYMMDD
|
||||
docker push git.wilkensxl.de/code-inc/intelligence-terminal:latest
|
||||
docker push git.wilkensxl.de/code-inc/intelligence-terminal:YYYYMMDD
|
||||
```
|
||||
|
||||
Gitea Actions publishes the same image automatically when the repository secret `REGISTRY_TOKEN` is set with package read/write permissions. The workflow tags images as `latest`, the commit SHA, and a UTC `YYYYMMDD` release tag.
|
||||
@@ -301,21 +307,26 @@ Gitea Actions publishes the same image automatically when the repository secret
|
||||
## What You Get
|
||||
|
||||
### Live Dashboard
|
||||
A self-contained app-style operator dashboard with dark mode by default, a large left navigation rail, rounded content surfaces, and view-specific panels:
|
||||
- **Home** - sweep status, alert posture, latest feed, source count, macro summary, and high-level signal state
|
||||
- **Worldview** - Globe.gl 3D globe or flat map, regional focus controls, 9 marker types, flight corridors, and layer focus/hide controls
|
||||
- **Sources** - sensor grid, source health, API-key degradation signals, and transparent partial-data states
|
||||
- **Signals** - cross-source signals, sweep delta, scenario watchlist, OSINT feed, and escalation/de-escalation context
|
||||
- **Markets** - live indexes, crypto, energy, commodities, VIX, high-yield spread, supply-chain pressure, and LLM-assisted ideas
|
||||
- **Ops** - browser-triggered terminal actions, performance mode, Telegram/Discord operator workflows, and system status
|
||||
|
||||
The UI keeps the existing operational features: `/api/data`, SSE live refresh, globe/flat map mode, layer focus/hide, terminal actions, low-performance mode, LLM output, Telegram and Discord alerting, and scenario watchlist data.
|
||||
A self-contained Jarvis-style HUD with:
|
||||
- **3D WebGL globe** (Globe.gl) with atmosphere glow, star field, and smooth rotation — plus a classic flat map toggle
|
||||
- **9 marker types** across both views: fire detections, air traffic, radiation sites, maritime chokepoints, SDR receivers, OSINT events, health alerts, geolocated news, conflict events
|
||||
- **Animated 3D flight corridor arcs** between air traffic hotspots and global hubs
|
||||
- **Region filters** (World, Americas, Europe, Middle East, Asia Pacific, Africa) — rotates the globe or zooms the flat map
|
||||
- **Live market data** — indexes, crypto, energy, commodities via Yahoo Finance (no API key needed)
|
||||
- **Risk gauges** — VIX, high-yield spread, supply chain pressure index
|
||||
- **OSINT feed** — English-language posts from 17 Telegram intelligence channels (expandable)
|
||||
- **News ticker** — merged RSS + GDELT headlines + Telegram posts, auto-scrolling
|
||||
- **Sweep delta** — live panel showing what changed since last sweep (new signals, escalations, de-escalations with severity)
|
||||
- **Cross-source signals** — correlated intelligence across satellite, economic, conflict, and social domains
|
||||
- **Nuclear watch** — real-time radiation readings from Safecast + EPA RadNet
|
||||
- **Space watch** — CelesTrak satellite tracking: recent launches, ISS, military constellations, Starlink/OneWeb counts
|
||||
- **Leverageable ideas** — AI-generated trade ideas (with LLM) or signal-correlated ideas (without)
|
||||
|
||||
### Performance Modes
|
||||
The `VISUALS FULL` / `VISUALS LITE` button in the top bar only changes rendering behavior - it does **not** remove data sources or reduce sweep coverage.
|
||||
|
||||
When you switch to **VISUALS LITE**, the dashboard:
|
||||
- Disables decorative background effects such as radial and grid overlays
|
||||
- Disables decorative background effects such as the radial/grid overlays and scanlines
|
||||
- Removes expensive blur/backdrop-filter effects on panels and overlays
|
||||
- Stops non-essential animations like the logo ring blink, conflict rings, and corridor flow effects
|
||||
- Disables globe auto-rotation and turns off animated flight-arc dashes
|
||||
@@ -375,7 +386,7 @@ Alerts are delivered as rich embeds with color-coded sidebars: red for FLASH, ye
|
||||
Connect cloud or local OpenAI-compatible LLM providers for enhanced analysis:
|
||||
- **AI trade ideas** — quantitative analyst producing 5-8 actionable ideas citing specific data
|
||||
- **Smarter alert evaluation** — LLM classifies signals into FLASH/PRIORITY/ROUTINE tiers with cross-domain correlation and confidence scoring
|
||||
- Providers: OpenRouter, OpenAI-compatible APIs, LM Studio, Ollama, OpenAI, Anthropic Claude, Google Gemini, OpenAI Codex, MiniMax, Mistral, Grok
|
||||
- Providers: LiteLLM, OpenRouter, OpenAI-compatible APIs, LM Studio, Ollama, OpenAI, Anthropic Claude, Google Gemini, OpenAI Codex, MiniMax, Mistral, Grok
|
||||
- Graceful fallback — when LLM is unavailable, a rule-based engine takes over alert evaluation. LLM failures never crash the sweep cycle.
|
||||
|
||||
Primary env keys:
|
||||
@@ -503,7 +514,7 @@ intelligence-terminal/
|
||||
├── dashboard/
|
||||
│ ├── inject.mjs # Data synthesis + standalone HTML injection
|
||||
│ └── public/
|
||||
│ └── jarvis.html # Self-contained app-style operator dashboard
|
||||
│ └── jarvis.html # Self-contained Jarvis HUD
|
||||
│
|
||||
├── lib/
|
||||
│ ├── llm/ # LLM abstraction (8 providers, raw fetch, no SDKs)
|
||||
@@ -620,7 +631,7 @@ All settings are in `.env` with sensible defaults:
|
||||
| `STALE_DATA_MAX_AGE_MINUTES` | `60` | Data age threshold for stale health state |
|
||||
| `STALE_ALERT_COOLDOWN_MINUTES` | `60` | Minimum time between repeated operator stale-data alerts |
|
||||
| `DASHBOARD_URL` | local URL | Dashboard URL included in operator alerts |
|
||||
| `LLM_PROVIDER` | disabled | `anthropic`, `openai`, `gemini`, `codex`, `openrouter`, `minimax`, `mistral`, or `grok` |
|
||||
| `LLM_PROVIDER` | disabled | `litellm`, `openrouter`, `openai-compatible`, `lmstudio`, `ollama`, `anthropic`, `openai`, `gemini`, `codex`, `minimax`, `mistral`, or `grok` |
|
||||
| `LLM_API_KEY` | — | API key (not needed for codex) |
|
||||
| `LLM_MODEL` | per-provider default | Override model selection |
|
||||
| `TELEGRAM_BOT_TOKEN` | disabled | For Telegram alerts + bot commands |
|
||||
@@ -642,7 +653,7 @@ When running `npm run dev`:
|
||||
|
||||
| Endpoint | Description |
|
||||
|----------|-------------|
|
||||
| `GET /` | App-style operator dashboard |
|
||||
| `GET /` | Jarvis HUD dashboard |
|
||||
| `GET /api/data` | Current synthesized intelligence data (JSON) |
|
||||
| `GET /api/health` | Server status, uptime, source count, LLM status |
|
||||
| `GET /events` | SSE stream for live push updates |
|
||||
@@ -721,16 +732,16 @@ Check these in order:
|
||||
|
||||
## Screenshots
|
||||
|
||||
The `docs/` folder contains dashboard screenshots referenced by this README. The hero screenshot has been refreshed for the app-style shell; regenerate the supporting map/globe images from a running instance when those views materially change.
|
||||
The `docs/` folder contains dashboard screenshots referenced by this README:
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `docs/dashboard.png` | Full operator dashboard - hero image at the top of this README |
|
||||
| `docs/boot.png` | Boot sequence animation |
|
||||
| `docs/map.png` | Worldview map with marker types and flight arcs |
|
||||
| `docs/dashboard.png` | Full dashboard — hero image at the top of this README |
|
||||
| `docs/boot.png` | Cinematic boot sequence animation |
|
||||
| `docs/map.png` | D3 world map with marker types and flight arcs |
|
||||
| `docs/globe.png` | 3D WebGL globe view with atmosphere glow and markers |
|
||||
|
||||
For a fresh capture, run the dashboard, wait for a sweep to complete, then use your browser's DevTools (`F12` -> `Ctrl+Shift+P` -> "Capture full size screenshot") or a tool like [LICEcap](https://www.cockos.com/licecap/) for GIFs.
|
||||
To update them: run the dashboard, wait for a sweep to complete, then use your browser's DevTools (`F12` → `Ctrl+Shift+P` → "Capture full size screenshot") or a tool like [LICEcap](https://www.cockos.com/licecap/) for GIFs.
|
||||
|
||||
---
|
||||
|
||||
@@ -746,7 +757,7 @@ For contribution guidelines, review expectations, and source-add rules, see `CON
|
||||
|
||||
For bugs, feature requests, and integration ideas, use the Gitea issue tracker so discussion stays visible and actionable:
|
||||
|
||||
https://git.wilkensxl.de/MrSphay/intelligence-terminal/issues
|
||||
https://git.wilkensxl.de/Code-Inc/intelligence-terminal/issues
|
||||
|
||||
## Upstream And License
|
||||
|
||||
|
||||
@@ -10,6 +10,44 @@ const fetchMetrics = {
|
||||
recent: [],
|
||||
};
|
||||
|
||||
const SOURCE_BY_HOST = [
|
||||
[/api\.bls\.gov$/i, 'BLS'],
|
||||
[/api\.fred\.stlouisfed\.org$/i, 'FRED'],
|
||||
[/api\.eia\.gov$/i, 'EIA'],
|
||||
[/api\.gdeltproject\.org$/i, 'GDELT'],
|
||||
[/api\.weather\.gov$/i, 'NOAA'],
|
||||
[/api\.open-notify\.org$/i, 'OpenNotify'],
|
||||
[/opensky-network\.org$/i, 'OpenSky'],
|
||||
[/firms\.modaps\.eosdis\.nasa\.gov$/i, 'FIRMS'],
|
||||
[/api\.acleddata\.com$/i, 'ACLED'],
|
||||
[/api\.reliefweb\.int$/i, 'ReliefWeb'],
|
||||
[/receiverbook\.de$/i, 'KiwiSDR'],
|
||||
[/safecast\.org$/i, 'Safecast'],
|
||||
[/api\.patentsview\.org$/i, 'PatentsView'],
|
||||
[/api\.trade\.gov$/i, 'Comtrade'],
|
||||
[/api\.usaspending\.gov$/i, 'USASpending'],
|
||||
[/api\.telegram\.org$/i, 'Telegram'],
|
||||
[/oauth\.reddit\.com$/i, 'Reddit'],
|
||||
[/reddit\.com$/i, 'Reddit'],
|
||||
[/api\.bsky\.app$/i, 'Bluesky'],
|
||||
[/api\.yahoo\.com$/i, 'YahooFinance'],
|
||||
[/query\d?\.finance\.yahoo\.com$/i, 'YahooFinance'],
|
||||
[/api\.cloudflare\.com$/i, 'CloudflareRadar'],
|
||||
[/api\.opensanctions\.org$/i, 'OpenSanctions'],
|
||||
[/home\.treasury\.gov$/i, 'Treasury'],
|
||||
[/fiscaldata\.treasury\.gov$/i, 'Treasury'],
|
||||
[/who\.int$/i, 'WHO'],
|
||||
];
|
||||
|
||||
export function inferFetchSource(url) {
|
||||
let host = 'unknown';
|
||||
try { host = new URL(url).host.toLowerCase(); } catch { return 'unknown'; }
|
||||
for (const [pattern, source] of SOURCE_BY_HOST) {
|
||||
if (pattern.test(host)) return source;
|
||||
}
|
||||
return host;
|
||||
}
|
||||
|
||||
function metricBucket(map, key) {
|
||||
if (!map[key]) map[key] = { requests: 0, ok: 0, failed: 0, bytes: 0, lastStatus: null, lastError: null, lastMs: 0 };
|
||||
return map[key];
|
||||
@@ -38,7 +76,7 @@ export function getFetchMetrics() {
|
||||
}
|
||||
|
||||
export async function safeFetch(url, opts = {}) {
|
||||
const { timeout = 15000, retries = 1, headers = {}, source = undefined } = opts;
|
||||
const { timeout = 15000, retries = 1, headers = {}, source = inferFetchSource(url) } = opts;
|
||||
let lastError;
|
||||
for (let i = 0; i <= retries; i++) {
|
||||
const started = Date.now();
|
||||
@@ -79,11 +117,11 @@ export async function safeFetch(url, opts = {}) {
|
||||
if (i < retries) await new Promise(r => setTimeout(r, 2000 * (i + 1)));
|
||||
}
|
||||
}
|
||||
return { error: lastError?.message || 'Unknown error', source: url };
|
||||
return { error: lastError?.message || 'Unknown error', source };
|
||||
}
|
||||
|
||||
export async function safeFetchText(url, opts = {}) {
|
||||
const { timeout = 15000, retries = 1, headers = {}, source = undefined } = opts;
|
||||
const { timeout = 15000, retries = 1, headers = {}, source = inferFetchSource(url) } = opts;
|
||||
let lastError;
|
||||
for (let i = 0; i <= retries; i++) {
|
||||
const started = Date.now();
|
||||
|
||||
@@ -32,14 +32,14 @@ export default {
|
||||
sseHeartbeatIntervalMs: intEnv('SSE_HEARTBEAT_INTERVAL_MS', 25000),
|
||||
|
||||
llm: {
|
||||
provider: process.env.LLM_PROVIDER || null, // anthropic | openai | gemini | codex | openrouter | minimax | mistral | ollama | grok
|
||||
provider: process.env.LLM_PROVIDER || null, // litellm | openrouter | openai-compatible | lmstudio | ollama | other supported providers
|
||||
apiKey: process.env.LLM_API_KEY || null,
|
||||
model: process.env.LLM_MODEL || null,
|
||||
baseUrl: process.env.LLM_BASE_URL || process.env.OPENAI_BASE_URL || process.env.OLLAMA_BASE_URL || null,
|
||||
temperature: floatEnv('LLM_TEMPERATURE', 0.2),
|
||||
maxTokens: intEnv('LLM_MAX_TOKENS', 2000),
|
||||
timeoutMs: intEnv('LLM_TIMEOUT_MS', 90000),
|
||||
openRouterSiteUrl: process.env.OPENROUTER_SITE_URL || 'https://git.wilkensxl.de/MrSphay/intelligence-terminal',
|
||||
openRouterSiteUrl: process.env.OPENROUTER_SITE_URL || 'https://git.wilkensxl.de/Code-Inc/intelligence-terminal',
|
||||
openRouterAppName: process.env.OPENROUTER_APP_NAME || 'Intelligence Terminal',
|
||||
},
|
||||
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Intelligence Terminal</title>
|
||||
<title>CRUCIX — Intelligence Terminal</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@300;400;500;600;700&family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@300;400;500;600;700&family=Space+Grotesk:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script>
|
||||
<script src="https://d3js.org/d3.v7.min.js"></script>
|
||||
<script src="https://d3js.org/topojson.v3.min.js"></script>
|
||||
@@ -17,7 +17,7 @@
|
||||
--bg:#020408;--panel:rgba(6,14,22,0.82);--glass:rgba(10,20,32,0.55);
|
||||
--border:rgba(100,240,200,0.12);--border-bright:rgba(100,240,200,0.3);
|
||||
--text:#e8f4f0;--dim:#6a8a82;--accent:#64f0c8;--accent2:#44ccff;
|
||||
--warn:#ffb84c;--danger:#ff5f63;--mono:'IBM Plex Mono',monospace;--sans:'Inter',system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
|
||||
--warn:#ffb84c;--danger:#ff5f63;--mono:'IBM Plex Mono',monospace;--sans:'Space Grotesk',sans-serif;
|
||||
}
|
||||
html,body{height:100%;background:var(--bg);color:var(--text);font-family:var(--sans);overflow-x:hidden}
|
||||
.bg-grid{position:fixed;inset:0;pointer-events:none;opacity:0;
|
||||
@@ -25,6 +25,10 @@ html,body{height:100%;background:var(--bg);color:var(--text);font-family:var(--s
|
||||
background-size:60px 60px;mask-image:radial-gradient(ellipse at 50% 30%,black 20%,transparent 70%)}
|
||||
.bg-radial{position:fixed;inset:0;pointer-events:none;opacity:0;
|
||||
background:radial-gradient(ellipse at 50% 0%,rgba(40,120,100,0.15),transparent 50%),radial-gradient(ellipse at 80% 20%,rgba(40,100,180,0.08),transparent 40%)}
|
||||
.scanline{position:fixed;inset:0;pointer-events:none;overflow:hidden;opacity:0}
|
||||
.scanline::after{content:'';position:absolute;left:0;width:100%;height:2px;background:linear-gradient(90deg,transparent,rgba(100,240,200,0.12),transparent);animation:scanMove 4s linear infinite}
|
||||
@keyframes scanMove{0%{top:-2px}100%{top:100%}}
|
||||
|
||||
/* BOOT */
|
||||
#boot{position:fixed;inset:0;z-index:1000;display:flex;flex-direction:column;align-items:center;justify-content:center;background:var(--bg)}
|
||||
.logo-ring{width:120px;height:120px;border:2px solid var(--border);border-radius:50%;display:flex;align-items:center;justify-content:center;position:relative;opacity:0}
|
||||
@@ -270,7 +274,7 @@ html,body{height:100%;background:var(--bg);color:var(--text);font-family:var(--s
|
||||
.ideas-src.static{color:var(--dim);border-color:rgba(255,255,255,0.1);background:rgba(255,255,255,0.03)}
|
||||
|
||||
/* LOW PERFORMANCE MODE */
|
||||
body.low-perf .bg-grid,body.low-perf .bg-radial{display:none!important}
|
||||
body.low-perf .bg-grid,body.low-perf .bg-radial,body.low-perf .scanline{display:none!important}
|
||||
body.low-perf .topbar,body.low-perf .g-panel,body.low-perf .map-popup,body.low-perf .map-loading{backdrop-filter:none!important}
|
||||
body.low-perf .logo-ring::before,body.low-perf .logo-ring::after,body.low-perf .regime-chip .blink,body.low-perf .conflict-ring,body.low-perf .corridor-flow{animation:none!important}
|
||||
body.low-perf .ticker-wrap{overflow-y:auto;scrollbar-width:thin;scrollbar-color:rgba(100,240,200,0.2) transparent}
|
||||
@@ -331,202 +335,20 @@ body.low-perf .ticker-wrap::-webkit-scrollbar-thumb{background:rgba(100,240,200,
|
||||
|
||||
/* IDEA HORIZON BADGE */
|
||||
.idea-horizon{font-family:var(--mono);font-size:8px;letter-spacing:0.08em;text-transform:uppercase;padding:1px 5px;border:1px solid rgba(100,240,200,0.15);color:var(--dim);margin-left:6px}
|
||||
|
||||
/* APP-STYLE REDESIGN */
|
||||
:root{
|
||||
--app-bg:#17191f;
|
||||
--app-surface:#22252d;
|
||||
--app-surface-2:#2b2f38;
|
||||
--app-card:#2a2d35;
|
||||
--app-card-soft:#242830;
|
||||
--app-border:#3a3f49;
|
||||
--app-border-soft:#343942;
|
||||
--app-text:#f3f5f7;
|
||||
--app-muted:#aeb6c2;
|
||||
--app-dim:#77818f;
|
||||
--app-green:#1bd96a;
|
||||
--app-green-soft:rgba(27,217,106,0.17);
|
||||
--app-blue:#5fb4ff;
|
||||
--app-red:#ff5f6f;
|
||||
--app-yellow:#ffcc66;
|
||||
}
|
||||
body[data-theme="dark"]{
|
||||
--bg:var(--app-bg);
|
||||
--panel:var(--app-surface);
|
||||
--glass:var(--app-card);
|
||||
--border:var(--app-border);
|
||||
--border-bright:rgba(27,217,106,0.55);
|
||||
--text:var(--app-text);
|
||||
--dim:var(--app-muted);
|
||||
--accent:var(--app-green);
|
||||
--accent2:var(--app-blue);
|
||||
background:var(--app-bg);
|
||||
}
|
||||
body[data-theme="light"]{
|
||||
--bg:#eef1f4;
|
||||
--panel:#ffffff;
|
||||
--glass:#ffffff;
|
||||
--border:#d6dde5;
|
||||
--border-bright:rgba(18,177,88,0.55);
|
||||
--text:#161a21;
|
||||
--dim:#5e6875;
|
||||
--accent:#11b858;
|
||||
--accent2:#2677c9;
|
||||
--app-bg:#eef1f4;
|
||||
--app-surface:#ffffff;
|
||||
--app-surface-2:#f5f7fa;
|
||||
--app-card:#ffffff;
|
||||
--app-card-soft:#f3f6f9;
|
||||
--app-border:#d6dde5;
|
||||
--app-border-soft:#e1e7ee;
|
||||
--app-text:#161a21;
|
||||
--app-muted:#5e6875;
|
||||
--app-dim:#788391;
|
||||
color:#161a21;
|
||||
}
|
||||
body{font-family:var(--sans);letter-spacing:0;background:var(--app-bg)}
|
||||
.bg-grid,.bg-radial{display:none}
|
||||
#main.app-root{display:grid;grid-template-columns:112px minmax(0,1fr);gap:18px;min-height:100vh;padding:0;background:var(--app-bg)}
|
||||
.app-sidebar{position:sticky;top:0;height:100vh;padding:26px 18px 24px;border-right:1px solid rgba(255,255,255,0.05);background:#20232b;display:flex;flex-direction:column;align-items:center;gap:28px}
|
||||
body[data-theme="light"] .app-sidebar{background:#f8fafc;border-right-color:#dde3ea}
|
||||
.app-brand-mark{width:54px;height:54px;border:3px solid var(--app-green);border-radius:18px;display:grid;place-items:center;color:var(--app-green);font-weight:800;font-size:19px;letter-spacing:0;background:rgba(27,217,106,0.06)}
|
||||
.app-brand-mark span{line-height:1}
|
||||
.app-nav{display:flex;flex-direction:column;gap:12px;width:100%;align-items:center}
|
||||
.nav-item{width:74px;height:74px;border:0;border-radius:999px;background:transparent;color:var(--app-muted);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:5px;cursor:pointer;transition:background .18s ease,color .18s ease,transform .18s ease}
|
||||
.nav-item span{width:28px;height:28px;display:grid;place-items:center;font-weight:800;font-size:17px;border:2px solid currentColor;border-radius:10px;line-height:1}
|
||||
.nav-item small{font-size:9px;font-weight:700;letter-spacing:0;text-transform:none}
|
||||
.nav-item:hover{background:rgba(255,255,255,0.06);color:var(--app-text);transform:translateY(-1px)}
|
||||
.nav-item.active{background:rgba(27,217,106,0.22);color:var(--app-green)}
|
||||
.sidebar-status{margin-top:auto;width:74px;min-height:42px;border-top:1px solid var(--app-border);padding-top:14px;display:flex;align-items:center;justify-content:center;gap:7px;color:var(--app-muted);font-size:11px;font-weight:700}
|
||||
.sidebar-status-dot{width:8px;height:8px;border-radius:50%;background:var(--app-green);box-shadow:0 0 16px rgba(27,217,106,.5)}
|
||||
.app-shell{min-width:0;margin:24px 24px 24px 0;border:1px solid var(--app-border);border-radius:30px 0 0 30px;background:#181b21;overflow:hidden;box-shadow:0 24px 70px rgba(0,0,0,.26)}
|
||||
body[data-theme="light"] .app-shell{background:#eef1f4;box-shadow:0 20px 50px rgba(33,43,54,.12)}
|
||||
.topbar{border:0;border-bottom:1px solid var(--app-border);border-radius:0;background:var(--app-surface);padding:26px 36px;backdrop-filter:none;display:grid;grid-template-columns:minmax(220px,340px) minmax(240px,1fr);gap:18px;align-items:center}
|
||||
.top-left,.top-center,.top-right{width:auto}
|
||||
.top-left{align-items:flex-start;flex-direction:column;gap:6px}
|
||||
.brand{font-family:var(--sans);font-size:34px;font-weight:800;letter-spacing:0;text-transform:none;color:var(--app-text);line-height:1}
|
||||
.view-subtitle{font-size:14px;line-height:1.4;color:var(--app-muted);font-weight:600}
|
||||
.regime-chip{border:0;border-radius:999px;background:var(--app-green-soft);color:var(--app-green);font-family:var(--sans);font-size:12px;letter-spacing:0;text-transform:none;font-weight:800;padding:7px 12px}
|
||||
.regime-chip .blink{background:var(--app-green);box-shadow:0 0 10px rgba(27,217,106,.6)}
|
||||
.app-search{height:46px;border:1px solid var(--app-border);border-radius:999px;background:var(--app-card-soft);color:var(--app-dim);display:flex;align-items:center;padding:0 18px;font-size:14px;font-weight:600;min-width:0}
|
||||
.app-search span{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.top-right{grid-column:1/-1;justify-content:flex-start;flex-wrap:wrap}
|
||||
@media(min-width:1240px){.topbar{grid-template-columns:minmax(260px,1fr) minmax(260px,420px) auto}.top-right{grid-column:auto;justify-content:flex-end}}
|
||||
.meta-pill,.guide-btn,.alert-badge,.perf-pill{border:1px solid var(--app-border);border-radius:999px;background:var(--app-card-soft);color:var(--app-muted);font-family:var(--sans);font-size:12px;letter-spacing:0;text-transform:none;font-weight:700;padding:8px 12px}
|
||||
.meta-pill .v{color:var(--app-text)}
|
||||
.guide-btn{color:var(--app-blue)}
|
||||
.alert-badge{border-color:rgba(255,95,111,.35);background:rgba(255,95,111,.1);color:#ff9da8}
|
||||
.theme-switch{display:flex;align-items:center;gap:4px;padding:4px;border:1px solid var(--app-border);border-radius:999px;background:var(--app-card-soft)}
|
||||
.theme-btn{border:0;border-radius:999px;background:transparent;color:var(--app-muted);font:700 11px var(--sans);padding:7px 10px;cursor:pointer}
|
||||
.theme-btn.active{background:var(--app-green);color:#06120b}
|
||||
.grid{display:grid;margin:0;padding:24px;grid-template-columns:300px minmax(0,1fr);gap:16px;min-height:calc(100vh - 122px);background:#181b21}
|
||||
body[data-theme="light"] .grid{background:#eef1f4}
|
||||
.col{gap:16px;min-width:0}
|
||||
#leftRail,#centerCol,#rightRail{order:0}
|
||||
#rightRail{grid-column:1/-1;display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:16px}
|
||||
@media(min-width:1320px){.grid{grid-template-columns:280px minmax(0,1fr) 360px}#rightRail{grid-column:auto;display:flex;flex-direction:column}}
|
||||
.g-panel,.map-region-bar,.map-container,.glossary-panel{border:1px solid var(--app-border);border-radius:18px;background:var(--app-card);box-shadow:none;backdrop-filter:none}
|
||||
.g-panel{padding:18px;overflow:hidden}
|
||||
.g-panel::before{display:none}
|
||||
.sec-head{margin-bottom:14px;gap:10px}
|
||||
.sec-head h3{font-family:var(--sans);font-size:17px;font-weight:800;letter-spacing:0;text-transform:none;color:var(--app-text)}
|
||||
.badge{border:1px solid var(--app-border);border-radius:999px;background:var(--app-card-soft);font-family:var(--sans);font-size:11px;font-weight:800;color:var(--app-green);padding:4px 9px}
|
||||
.layer-item,.site-row,.econ-row,.src-item,.mc,.signal-row,.sm,.idea-card,.ic,.tk-card{border-color:var(--app-border-soft);border-radius:12px;background:var(--app-card-soft)}
|
||||
.layer-item{margin-bottom:8px;padding:11px}
|
||||
.layer-item.focused{border-color:var(--app-green);background:var(--app-green-soft)}
|
||||
.layer-name,.idea-title{font-weight:800;color:var(--app-text)}
|
||||
.layer-sub,.layer-mode,.idea-text,.ic .ic-t,.tk-head{color:var(--app-muted)}
|
||||
.layer-count,.site-val,.eval,.sm .smv{color:var(--app-green)}
|
||||
.mini-btn,.action-btn,.region-btn,.map-ctrl-btn,.proj-toggle,.glossary-close{border:1px solid var(--app-border);border-radius:10px;background:var(--app-card-soft);font-family:var(--sans);letter-spacing:0;color:var(--app-muted)}
|
||||
.action-grid{grid-template-columns:repeat(3,minmax(0,1fr));gap:8px}
|
||||
.action-btn{font-size:12px;font-weight:800;padding:10px 8px}
|
||||
.action-btn:hover,.mini-btn:hover,.region-btn:hover{border-color:var(--app-green);color:var(--app-green);background:var(--app-green-soft)}
|
||||
.terminal-output{border:1px solid var(--app-border);border-radius:14px;background:#151820;color:var(--app-muted)}
|
||||
body[data-theme="light"] .terminal-output{background:#f6f8fb}
|
||||
.map-region-bar{padding:12px;gap:8px;margin-bottom:0}
|
||||
.region-btn{font-weight:800;text-transform:none;font-size:12px;padding:8px 12px}
|
||||
.region-btn.active{background:var(--app-green);color:#06120b;border-color:var(--app-green)}
|
||||
.map-container{min-height:590px;background:#101319}
|
||||
body[data-theme="light"] .map-container{background:#e7ecf3}
|
||||
.map-legend{border:1px solid var(--app-border);border-radius:14px;background:rgba(24,27,33,.86);padding:9px 11px;backdrop-filter:blur(8px)}
|
||||
.map-hint,.map-hint-id{top:14px;right:18px;color:var(--app-muted);letter-spacing:0;font-family:var(--sans);font-weight:700}
|
||||
.lower{gap:16px;margin-top:16px}
|
||||
.lower .lp-ticker{flex:1.2 1 300px;max-width:460px}
|
||||
.lower .lp-macro{flex:2.4 1 520px}
|
||||
.lower .lp-ideas{flex:1.4 1 340px}
|
||||
.lower .source-health{flex:1.5 1 420px}
|
||||
.metrics-row{gap:8px}
|
||||
.src-grid{grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:8px}
|
||||
.feed{max-height:none}
|
||||
|
||||
body[data-view="worldview"] .grid{grid-template-columns:300px minmax(0,1fr)}
|
||||
body[data-view="worldview"] #rightRail,body[data-view="worldview"] .lower{display:none}
|
||||
body[data-view="worldview"] .map-container{min-height:calc(100vh - 250px)}
|
||||
body[data-view="sources"] .grid{grid-template-columns:360px minmax(0,1fr)}
|
||||
body[data-view="sources"] #rightRail,body[data-view="sources"] #mapContainer,body[data-view="sources"] #mapRegionBar{display:none!important}
|
||||
body[data-view="sources"] .lower .g-panel:not(.source-health){display:none}
|
||||
body[data-view="sources"] .lower{margin-top:0}
|
||||
body[data-view="signals"] .grid,body[data-view="ops"] .grid{display:block}
|
||||
body[data-view="signals"] #leftRail,body[data-view="signals"] #centerCol{display:none}
|
||||
body[data-view="signals"] #rightRail{display:grid;grid-template-columns:repeat(auto-fit,minmax(320px,1fr));gap:16px}
|
||||
body[data-view="signals"] #rightRail .right-actions{display:none}
|
||||
body[data-view="ops"] #leftRail,body[data-view="ops"] #centerCol{display:none}
|
||||
body[data-view="ops"] #rightRail{display:grid;grid-template-columns:repeat(auto-fit,minmax(320px,1fr));gap:16px}
|
||||
body[data-view="ops"] #rightRail .right-signals,body[data-view="ops"] #rightRail .right-scenarios,body[data-view="ops"] #rightRail .right-delta,body[data-view="ops"] #rightRail .right-osint{display:none}
|
||||
body[data-view="markets"] .grid{display:block}
|
||||
body[data-view="markets"] #leftRail,body[data-view="markets"] #rightRail,body[data-view="markets"] #mapContainer,body[data-view="markets"] #mapRegionBar{display:none!important}
|
||||
body[data-view="markets"] .lower{margin-top:0}
|
||||
body[data-view="markets"] .lower .g-panel:not(.lp-macro):not(.lp-ideas){display:none}
|
||||
body[data-view="markets"] .lp-macro,body[data-view="markets"] .lp-ideas{max-width:none}
|
||||
|
||||
@media(max-width:760px){
|
||||
#main.app-root{display:block;padding:0 0 86px}
|
||||
.app-sidebar{position:fixed;z-index:30;left:0;right:0;bottom:0;top:auto;height:76px;flex-direction:row;padding:8px 12px;border-right:0;border-top:1px solid var(--app-border);gap:12px}
|
||||
.app-brand-mark,.sidebar-status{display:none}
|
||||
.app-nav{flex-direction:row;justify-content:space-between;gap:6px;width:100%}
|
||||
.nav-item{width:54px;height:54px;border-radius:18px}
|
||||
.nav-item span{width:23px;height:23px;font-size:13px;border-radius:8px}
|
||||
.nav-item small{display:none}
|
||||
.app-shell{margin:10px;border-radius:24px;min-height:calc(100vh - 96px)}
|
||||
.topbar{grid-template-columns:1fr;padding:22px;gap:12px}
|
||||
.brand{font-size:30px}
|
||||
.top-center{display:flex;width:100%;overflow:auto}
|
||||
.top-right{justify-content:flex-start}
|
||||
.grid{padding:14px;display:flex;flex-direction:column}
|
||||
body[data-view="worldview"] .grid,body[data-view="sources"] .grid{display:flex}
|
||||
body[data-view="signals"] #rightRail,body[data-view="ops"] #rightRail{display:flex;flex-direction:column}
|
||||
.map-container{min-height:420px}
|
||||
.map-legend{left:8px;right:8px;bottom:8px}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body data-theme="dark" data-view="home">
|
||||
<body>
|
||||
<div id="boot">
|
||||
<div class="logo-ring"><span class="logo-text">IT</span></div>
|
||||
<div class="logo-ring"><span class="logo-text">CRUCIX</span></div>
|
||||
<div id="bootLines"></div>
|
||||
<div id="bootFinal">APP READY</div>
|
||||
<div id="bootFinal">TERMINAL ACTIVE</div>
|
||||
</div>
|
||||
<div class="bg-radial" id="bgRadial"></div>
|
||||
<div class="bg-grid" id="bgGrid"></div>
|
||||
<div id="main" class="app-root">
|
||||
<aside class="app-sidebar" aria-label="Primary views">
|
||||
<div class="app-brand-mark"><span>IT</span></div>
|
||||
<nav class="app-nav" id="appNav">
|
||||
<button class="nav-item active" data-view-target="home" onclick="setAppView('home')" title="Home"><span>H</span><small>Home</small></button>
|
||||
<button class="nav-item" data-view-target="worldview" onclick="setAppView('worldview')" title="Worldview"><span>W</span><small>World</small></button>
|
||||
<button class="nav-item" data-view-target="sources" onclick="setAppView('sources')" title="Sources"><span>S</span><small>Sources</small></button>
|
||||
<button class="nav-item" data-view-target="signals" onclick="setAppView('signals')" title="Signals"><span>I</span><small>Signals</small></button>
|
||||
<button class="nav-item" data-view-target="markets" onclick="setAppView('markets')" title="Markets"><span>M</span><small>Markets</small></button>
|
||||
<button class="nav-item" data-view-target="ops" onclick="setAppView('ops')" title="Ops"><span>O</span><small>Ops</small></button>
|
||||
</nav>
|
||||
<div class="sidebar-status">
|
||||
<span class="sidebar-status-dot"></span>
|
||||
<span id="sidebarStatus">Live</span>
|
||||
</div>
|
||||
</aside>
|
||||
<main class="app-shell">
|
||||
<div class="topbar" id="topbar"></div>
|
||||
<div class="grid">
|
||||
<div class="scanline" id="scanline"></div>
|
||||
<div id="main">
|
||||
<div class="topbar" id="topbar"></div>
|
||||
<div class="grid">
|
||||
<div class="col" id="leftRail"></div>
|
||||
<div class="col" id="centerCol">
|
||||
<div class="map-region-bar" id="mapRegionBar"></div>
|
||||
@@ -547,8 +369,7 @@ body[data-view="markets"] .lp-macro,body[data-view="markets"] .lp-ideas{max-widt
|
||||
<div class="lower" id="lowerGrid"></div>
|
||||
</div>
|
||||
<div class="col" id="rightRail"></div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
<div class="glossary-overlay" id="glossaryOverlay" onclick="if(event.target===this) closeGlossary()">
|
||||
<div class="glossary-panel">
|
||||
@@ -613,56 +434,6 @@ let currentRegion = 'world';
|
||||
let flatSvg, flatProjection, flatPath, flatG, flatZoom, flatW, flatH;
|
||||
const terminalActionTokenKey = 'crucix_sweep_token';
|
||||
|
||||
const appViews = {
|
||||
home: { title: 'Home', subtitle: 'Sweep summary, alert posture, source status, and the latest operator feed.' },
|
||||
worldview: { title: 'Worldview', subtitle: 'Globe or flat map with regional focus and layer controls.' },
|
||||
sources: { title: 'Sources', subtitle: 'Sensor grid, source health, API-key degradation, and data coverage.' },
|
||||
signals: { title: 'Signals', subtitle: 'Cross-source signals, sweep delta, scenario watchlist, and OSINT context.' },
|
||||
markets: { title: 'Markets', subtitle: 'Macro indicators, live markets, volatility gauges, and AI-assisted ideas.' },
|
||||
ops: { title: 'Ops', subtitle: 'Terminal actions, integration state, and low-performance controls.' }
|
||||
};
|
||||
let currentView = localStorage.getItem('intelligence_terminal_view') || 'home';
|
||||
if(!appViews[currentView]) currentView = 'home';
|
||||
let themePreference = localStorage.getItem('intelligence_terminal_theme') || 'dark';
|
||||
|
||||
function resolveTheme(pref){
|
||||
if(pref === 'auto'){
|
||||
return window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
|
||||
}
|
||||
return pref === 'light' ? 'light' : 'dark';
|
||||
}
|
||||
|
||||
function applyTheme(pref = themePreference){
|
||||
themePreference = pref;
|
||||
document.body.dataset.theme = resolveTheme(pref);
|
||||
document.querySelectorAll('.theme-btn').forEach(btn => btn.classList.toggle('active', btn.dataset.themeChoice === pref));
|
||||
}
|
||||
|
||||
function setTheme(pref){
|
||||
themePreference = pref;
|
||||
localStorage.setItem('intelligence_terminal_theme', pref);
|
||||
applyTheme(pref);
|
||||
renderTopbar();
|
||||
}
|
||||
|
||||
function renderAppNav(){
|
||||
document.body.dataset.view = currentView;
|
||||
document.querySelectorAll('.nav-item[data-view-target]').forEach(btn => {
|
||||
btn.classList.toggle('active', btn.dataset.viewTarget === currentView);
|
||||
});
|
||||
const status = document.getElementById('sidebarStatus');
|
||||
if(status) status.textContent = currentView === 'home' ? 'Live' : appViews[currentView].title;
|
||||
}
|
||||
|
||||
function setAppView(view){
|
||||
if(!appViews[view]) return;
|
||||
currentView = view;
|
||||
localStorage.setItem('intelligence_terminal_view', view);
|
||||
renderAppNav();
|
||||
renderTopbar();
|
||||
refreshMapViewport(true);
|
||||
}
|
||||
|
||||
const layerTypeMap = {
|
||||
air: ['air'],
|
||||
thermal: ['thermal'],
|
||||
@@ -863,35 +634,23 @@ function renderTopbar(){
|
||||
const d = ts.toLocaleDateString('en-US',{month:'short',day:'numeric',year:'numeric'}).toUpperCase();
|
||||
const timeStr = ts.toLocaleTimeString('en-US',{hour:'2-digit',minute:'2-digit',hour12:true});
|
||||
const hasActionToken = !!getTerminalActionToken();
|
||||
const view = appViews[currentView] || appViews.home;
|
||||
const direction = D.delta?.summary?.direction;
|
||||
const deltaLabel = direction === 'risk-off' ? '▲ '+t('dashboard.riskOff','RISK-OFF') : direction === 'risk-on' ? '▼ '+t('dashboard.riskOn','RISK-ON') : '◆ '+t('dashboard.mixed','MIXED');
|
||||
document.getElementById('topbar').innerHTML=`
|
||||
<div class="top-left">
|
||||
<span class="brand">${view.title}</span>
|
||||
<span class="view-subtitle">${view.subtitle}</span>
|
||||
<span class="regime-chip"><span class="blink"></span>Operator dashboard</span>
|
||||
<span class="brand">CRUCIX MONITOR</span>
|
||||
<span class="regime-chip"><span class="blink"></span>WARTIME STAGFLATION RISK</span>
|
||||
</div>
|
||||
<div class="app-search"><span>Search sources, signals, regions, markets...</span></div>
|
||||
${mobile ? `<div class="top-center">${getRegionControlsMarkup()}</div>` : ''}
|
||||
<div class="top-right">
|
||||
<div class="theme-switch" aria-label="Theme selector">
|
||||
<button class="theme-btn" data-theme-choice="dark" onclick="setTheme('dark')">Dark</button>
|
||||
<button class="theme-btn" data-theme-choice="auto" onclick="setTheme('auto')">Auto</button>
|
||||
<button class="theme-btn" data-theme-choice="light" onclick="setTheme('light')">Light</button>
|
||||
</div>
|
||||
<button class="meta-pill perf-pill" onclick="togglePerfMode()" title="Reduce visual effects and start mobile in flat mode">${t('dashboard.visuals','Visuals')} <span class="v" id="perfStatus">${lowPerfMode?t('dashboard.visualsLite','Lite'):t('dashboard.visualsFull','Full')}</span></button>
|
||||
<button class="meta-pill perf-pill" onclick="togglePerfMode()" title="Reduce visual effects and start mobile in flat mode">${t('dashboard.visuals','VISUALS')} <span class="v" id="perfStatus">${lowPerfMode?t('dashboard.visualsLite','LITE'):t('dashboard.visualsFull','FULL')}</span></button>
|
||||
<span class="meta-pill">${t('dashboard.sweep','SWEEP')} <span class="v">${(D.meta.totalDurationMs/1000).toFixed(1)}s</span></span>
|
||||
<span class="meta-pill">${d} <span class="v">${timeStr}</span></span>
|
||||
<span class="meta-pill">${t('dashboard.sources','SOURCES')} <span class="v">${D.meta.sourcesOk}/${D.meta.sourcesQueried}</span></span>
|
||||
${D.delta?.summary ? `<span class="meta-pill">${t('dashboard.delta','DELTA')} <span class="v">${deltaLabel}</span></span>` : ''}
|
||||
<button class="guide-btn" onclick="configureTerminalActionToken()" title="Configure SWEEP_TOKEN for protected terminal actions">${hasActionToken?'Token set':'Set token'}</button>
|
||||
${D.delta?.summary ? `<span class="meta-pill">${t('dashboard.delta','DELTA')} <span class="v">${D.delta.summary.direction==='risk-off'?'▲ '+t('dashboard.riskOff','RISK-OFF'):D.delta.summary.direction==='risk-on'?'▼ '+t('dashboard.riskOn','RISK-ON'):'◆ '+t('dashboard.mixed','MIXED')}</span></span>` : ''}
|
||||
<button class="guide-btn" onclick="configureTerminalActionToken()" title="Configure SWEEP_TOKEN for protected terminal actions">${hasActionToken?'TOKEN SET':'SET TOKEN'}</button>
|
||||
<button class="guide-btn" onclick="openGlossary()">${t('dashboard.guideBtn','What Signals Mean')}</button>
|
||||
<span class="alert-badge">${t('dashboard.highAlert','HIGH ALERT')}</span>
|
||||
</div>`;
|
||||
renderRegionControls();
|
||||
renderAppNav();
|
||||
applyTheme(themePreference);
|
||||
}
|
||||
|
||||
function getTerminalActionToken(){
|
||||
@@ -975,16 +734,16 @@ function renderLeftRail(){
|
||||
const claims=D.fred.find(f=>f.id==='ICSA');
|
||||
|
||||
document.getElementById('leftRail').innerHTML=`
|
||||
<div class="g-panel source-layers">
|
||||
<div class="g-panel">
|
||||
<div class="sec-head"><h3>${t('panels.sensorGrid','Sensor Grid')}</h3><div class="sensor-actions"><button class="mini-btn" onclick="resetLayerModes();event.stopPropagation()">RESET</button><span class="badge">${t('badges.live','LIVE')}</span></div></div>
|
||||
${layers.map(l=>`<div class="layer-item ${layerMode(l.key)==='focus'?'focused':''} ${layerMode(l.key)==='hidden'?'hidden-layer':''}" onclick="cycleLayerMode('${l.key}',event)" title="Click to focus. Shift/Ctrl-click to hide."><div class="layer-left"><div class="ldot ${l.dot}"></div><div><div class="layer-name">${l.name}</div><div class="layer-sub">${l.sub}</div><div class="layer-mode">${layerModeLabel(l.key)}</div></div></div><div class="layer-count">${l.count}</div></div>`).join('')}
|
||||
</div>
|
||||
<div class="g-panel nuclear-panel">
|
||||
<div class="g-panel">
|
||||
<div class="sec-head"><h3>${t('panels.nuclearWatch','Nuclear Watch')}</h3><span class="badge">${t('badges.radiation','RADIATION')}</span></div>
|
||||
<div class="nuke-ok">${allNormal?'● '+t('nuclear.allSitesNormal','ALL SITES NORMAL'):'⚠ '+t('nuclear.anomalyDetected','ANOMALY DETECTED')}</div>
|
||||
${nukeHtml}
|
||||
</div>
|
||||
<div class="g-panel risk-panel">
|
||||
<div class="g-panel">
|
||||
<div class="sec-head"><h3>${t('panels.riskGauges','Risk Gauges')}</h3><span class="badge">${t('badges.stress','STRESS')}</span></div>
|
||||
<div class="econ-row"><span class="elabel">${t('metrics.vix','VIX')} (Fear)</span><span class="eval" style="color:${vix?.value>20?'var(--warn)':'var(--accent)'}">${vix?.value||'--'}</span></div>
|
||||
<div class="econ-row"><span class="elabel">${t('metrics.hySpread','HY Spread')}</span><span class="eval">${hy?.value||'--'}</span></div>
|
||||
@@ -994,7 +753,7 @@ function renderLeftRail(){
|
||||
<div class="econ-row"><span class="elabel">${t('metrics.m2Supply','M2 Supply')}</span><span class="eval">$${(m2?.value/1000)?.toFixed(1)||'--'}T</span></div>
|
||||
<div class="econ-row"><span class="elabel">${t('metrics.natDebt','Nat. Debt')}</span><span class="eval">$${(parseFloat(D.treasury.totalDebt)/1e12).toFixed(2)}T</span></div>
|
||||
</div>
|
||||
<div class="g-panel space-panel">
|
||||
<div class="g-panel">
|
||||
<div class="sec-head"><h3>${t('panels.spaceWatch','Space Watch')}</h3><button class="mini-btn" onclick="toggleSpaceDisplay()">${spaceDisplayMode.toUpperCase()}</button></div>
|
||||
${D.space ? `
|
||||
<div class="econ-row"><span class="elabel">New Objects (30d)</span><span class="eval" style="color:var(--accent2)">${D.space.totalNewObjects||0}</span></div>
|
||||
@@ -1844,12 +1603,7 @@ function renderLower(){
|
||||
${ideasHtml}
|
||||
<div class="disclosure">FOR INFORMATIONAL PURPOSES ONLY. This is not financial advice, a recommendation to buy or sell any security, or a solicitation of any kind. All signal-based observations are derived from publicly available OSINT data and should not be relied upon for investment decisions. Consult a licensed financial advisor before making any investment. Past performance does not guarantee future results.</div>
|
||||
</div>`;
|
||||
const sourcePanel = `<div class="g-panel source-health">
|
||||
<div class="sec-head"><h3>Source Health</h3><span class="badge">${D.meta.sourcesOk}/${D.meta.sourcesQueried} online</span></div>
|
||||
<div class="src-grid">${srcHtml || '<div class="src-item"><div class="sd err"></div><span>No source snapshot</span></div>'}</div>
|
||||
<div class="disclosure">Sources that require API keys degrade visibly here while the rest of the sweep continues. The dashboard stays useful with partial data instead of hiding failures.</div>
|
||||
</div>`;
|
||||
document.getElementById('lowerGrid').innerHTML=`${tickerPanel}${osintPanel}${macroPanel}${ideasPanel}${sourcePanel}`;
|
||||
document.getElementById('lowerGrid').innerHTML=`${tickerPanel}${osintPanel}${macroPanel}${ideasPanel}`;
|
||||
}
|
||||
|
||||
async function runTerminalAction(action){
|
||||
@@ -2002,7 +1756,7 @@ function safeExternalUrl(raw){try{const u=new URL(raw,location.href);return u.pr
|
||||
function runBoot(){
|
||||
const acledStatus = D.acled?.totalEvents > 0 ? `<span class="ok">${D.acled.totalEvents} EVENTS</span>` : '<span style="color:var(--warn)">DEGRADED</span>';
|
||||
const lines=[
|
||||
{text:t('boot.initializing','INITIALIZING INTELLIGENCE TERMINAL'),delay:0},
|
||||
{text:t('boot.initializing','INITIALIZING CRUCIX ENGINE v2.1.0'),delay:0},
|
||||
{text:t('boot.connecting','CONNECTING {count} OSINT SOURCES...').replace('{count}',D.meta.sourcesQueried),delay:400},
|
||||
{text:'├─ '+t('boot.sourceGroup1','OPENSKY · FIRMS · KIWISDR · MARITIME'),delay:700},
|
||||
{text:'├─ '+t('boot.sourceGroup2','FRED · BLS · EIA · TREASURY · GSCPI'),delay:900},
|
||||
@@ -2014,7 +1768,7 @@ function runBoot(){
|
||||
{text:t('boot.intelligenceSynthesis','INTELLIGENCE SYNTHESIS')+': <span class="ok">'+t('boot.active','ACTIVE')+'</span>',delay:2400},
|
||||
];
|
||||
const container=document.getElementById('bootLines');
|
||||
document.getElementById('bootFinal').textContent=t('dashboard.terminalActive','APP READY');
|
||||
document.getElementById('bootFinal').textContent=t('dashboard.terminalActive','TERMINAL ACTIVE');
|
||||
const tl=gsap.timeline();
|
||||
tl.to('.logo-ring',{opacity:1,duration:0.6,ease:'power2.out'},0);
|
||||
tl.to(container,{opacity:1,duration:0.3},0.3);
|
||||
@@ -2029,6 +1783,7 @@ function runBoot(){
|
||||
tl.set('#boot',{display:'none'},4.2);
|
||||
tl.to('#bgRadial',{opacity:1,duration:1},3.8);
|
||||
tl.to('#bgGrid',{opacity:1,duration:1.2},4.0);
|
||||
tl.to('#scanline',{opacity:1,duration:0.8},4.3);
|
||||
tl.to('#main',{opacity:1,duration:0.6},3.9);
|
||||
tl.call(()=>{
|
||||
gsap.from('.g-panel,.topbar,.map-container',{opacity:0,y:20,scale:0.97,duration:0.5,stagger:0.06,ease:'power2.out'});
|
||||
@@ -2043,7 +1798,7 @@ function runBoot(){
|
||||
},[],4.0);
|
||||
}
|
||||
|
||||
function isMobileLayout(){ return window.innerWidth <= 760; }
|
||||
function isMobileLayout(){ return window.innerWidth <= 1100; }
|
||||
|
||||
function buildOsintPanel(panelClass='', maxHeight=260){
|
||||
const allPosts=[...D.tg.urgent,...D.tg.topPosts].sort((a,b)=>new Date(b.date||0)-new Date(a.date||0));
|
||||
@@ -2180,8 +1935,6 @@ function connectSSE(){
|
||||
// === INIT ===
|
||||
let booted = false;
|
||||
function init(){
|
||||
applyTheme(themePreference);
|
||||
renderAppNav();
|
||||
renderTopbar();renderLeftRail();renderLower();renderRight();
|
||||
renderGlossary();
|
||||
initMap();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
services:
|
||||
intelligence-terminal:
|
||||
image: git.wilkensxl.de/mrsphay/intelligence-terminal:latest
|
||||
image: git.wilkensxl.de/code-inc/intelligence-terminal:latest
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
|
||||
@@ -1,6 +1,18 @@
|
||||
# Agent Handoff
|
||||
|
||||
Last updated: 2026-05-17
|
||||
Last updated: 2026-07-03
|
||||
|
||||
## Latest Completed Work
|
||||
|
||||
- Canonical repository: `https://git.wilkensxl.de/Code-Inc/intelligence-terminal`
|
||||
- LiteLLM implementation merge: `5c4bf80eb0c19bd59080f5432a2a344798d7a3ce`
|
||||
- Merged PR: `#48 feat: add LiteLLM provider and publish Code-Inc image`
|
||||
- Completed issue: `#47 Add first-class LiteLLM provider and publish updated image`
|
||||
- LiteLLM is implemented through the OpenAI-compatible `/chat/completions` API and requires `LLM_BASE_URL`, `LLM_API_KEY`, and `LLM_MODEL`.
|
||||
- The build workflow now targets `git.wilkensxl.de/code-inc/intelligence-terminal` and publishes only from the production branch, not from pull requests.
|
||||
- Gitea Actions runs 231-235 passed for the PR and production merge, including unit tests, Compose validation, Docker build, release dry-run, and template compliance.
|
||||
- The first `code-inc` registry publication was verified through the Gitea Package API on 2026-07-03.
|
||||
- Related maintenance: issue #21 tracks the failing security scan, #45 tracks the dependency workflow, and #46 tracks remaining namespace/handoff cleanup.
|
||||
|
||||
## Repository State
|
||||
|
||||
@@ -15,7 +27,7 @@ C:\Users\MrSphay\Documents\Codex\Crucix\intelligence-terminal
|
||||
Remotes:
|
||||
|
||||
```text
|
||||
origin https://git.wilkensxl.de/MrSphay/intelligence-terminal.git
|
||||
origin https://git.wilkensxl.de/Code-Inc/intelligence-terminal.git
|
||||
upstream https://github.com/calesthio/Crucix.git
|
||||
```
|
||||
|
||||
@@ -25,23 +37,22 @@ Current branch tip:
|
||||
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:
|
||||
Production baseline before the current LiteLLM work:
|
||||
|
||||
```text
|
||||
53470cc701ec322080a89d220aef449b25850590
|
||||
c159c83a0768486c8c6f445b458b760dba4ba385
|
||||
```
|
||||
|
||||
Both pushed branches currently point to this commit:
|
||||
The default production branch points to this commit before the current PR:
|
||||
|
||||
```text
|
||||
origin/codex/production-intelligence-terminal
|
||||
origin/main
|
||||
```
|
||||
|
||||
Gitea repository:
|
||||
|
||||
```text
|
||||
https://git.wilkensxl.de/MrSphay/intelligence-terminal
|
||||
https://git.wilkensxl.de/Code-Inc/intelligence-terminal
|
||||
```
|
||||
|
||||
Default branch observed through the Gitea API:
|
||||
@@ -79,7 +90,7 @@ Rules applied from the kit:
|
||||
- `docker-compose.yml` uses the Gitea Registry image by default:
|
||||
|
||||
```text
|
||||
git.wilkensxl.de/mrsphay/intelligence-terminal:latest
|
||||
git.wilkensxl.de/code-inc/intelligence-terminal:latest
|
||||
```
|
||||
|
||||
### API And Health
|
||||
@@ -226,31 +237,40 @@ README includes:
|
||||
|
||||
## Registry And Images
|
||||
|
||||
Registry image:
|
||||
Production registry image:
|
||||
|
||||
```text
|
||||
git.wilkensxl.de/mrsphay/intelligence-terminal
|
||||
git.wilkensxl.de/code-inc/intelligence-terminal
|
||||
```
|
||||
|
||||
Verified package tags through Gitea API:
|
||||
The legacy `mrsphay` package remains available. Verified `code-inc` tags from the LiteLLM production merge:
|
||||
|
||||
```text
|
||||
latest
|
||||
20260517
|
||||
e933586b220656a2858d2215b934b22d1f08a908
|
||||
53470cc701ec322080a89d220aef449b25850590
|
||||
20260703
|
||||
5c4bf80eb0c19bd59080f5432a2a344798d7a3ce
|
||||
```
|
||||
|
||||
Successful pull test:
|
||||
Operator pull command:
|
||||
|
||||
```bash
|
||||
docker pull git.wilkensxl.de/mrsphay/intelligence-terminal:latest
|
||||
docker pull git.wilkensxl.de/code-inc/intelligence-terminal:latest
|
||||
```
|
||||
|
||||
Observed digest:
|
||||
Published manifest digest:
|
||||
|
||||
```text
|
||||
sha256:780a41413921bd9a676461eca1cd1372591f523be4b7c9513d9bc085cbe7922d
|
||||
sha256:5e29483ebfd9baae368673adc790789f02aed2d5d5d3a550fe55a4b71b5b62dd
|
||||
```
|
||||
|
||||
Relevant successful runner executions:
|
||||
|
||||
```text
|
||||
PR build: https://git.wilkensxl.de/Code-Inc/intelligence-terminal/actions/runs/231
|
||||
PR template check: https://git.wilkensxl.de/Code-Inc/intelligence-terminal/actions/runs/232
|
||||
Production publish: https://git.wilkensxl.de/Code-Inc/intelligence-terminal/actions/runs/233
|
||||
Release dry-run: https://git.wilkensxl.de/Code-Inc/intelligence-terminal/actions/runs/234
|
||||
Template compliance: https://git.wilkensxl.de/Code-Inc/intelligence-terminal/actions/runs/235
|
||||
```
|
||||
|
||||
## Gitea Actions
|
||||
@@ -280,12 +300,12 @@ 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
|
||||
https://git.wilkensxl.de/Code-Inc/intelligence-terminal/actions/runs/23
|
||||
https://git.wilkensxl.de/Code-Inc/intelligence-terminal/actions/runs/24
|
||||
https://git.wilkensxl.de/Code-Inc/intelligence-terminal/actions/runs/25
|
||||
https://git.wilkensxl.de/Code-Inc/intelligence-terminal/actions/runs/26
|
||||
https://git.wilkensxl.de/Code-Inc/intelligence-terminal/actions/runs/27
|
||||
https://git.wilkensxl.de/Code-Inc/intelligence-terminal/actions/runs/28
|
||||
```
|
||||
|
||||
Repository secret expected by the registry publish workflow:
|
||||
@@ -315,28 +335,28 @@ 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
|
||||
https://git.wilkensxl.de/Code-Inc/intelligence-terminal/issues/1
|
||||
|
||||
#2 Send operator alerts when dashboard data remains stale
|
||||
https://git.wilkensxl.de/MrSphay/intelligence-terminal/issues/2
|
||||
https://git.wilkensxl.de/Code-Inc/intelligence-terminal/issues/2
|
||||
|
||||
#3 ACLED credentialed integration needs regression test and diagnostics
|
||||
https://git.wilkensxl.de/MrSphay/intelligence-terminal/issues/3
|
||||
https://git.wilkensxl.de/Code-Inc/intelligence-terminal/issues/3
|
||||
|
||||
#4 Complete memory and prediction loop beyond Phase-1 SQLite
|
||||
https://git.wilkensxl.de/MrSphay/intelligence-terminal/issues/4
|
||||
https://git.wilkensxl.de/Code-Inc/intelligence-terminal/issues/4
|
||||
|
||||
#5 Remove old inline dashboard snapshot from production builds
|
||||
https://git.wilkensxl.de/MrSphay/intelligence-terminal/issues/5
|
||||
https://git.wilkensxl.de/Code-Inc/intelligence-terminal/issues/5
|
||||
|
||||
#6 Harden Terminal Actions for public reverse-proxy deployments
|
||||
https://git.wilkensxl.de/MrSphay/intelligence-terminal/issues/6
|
||||
https://git.wilkensxl.de/Code-Inc/intelligence-terminal/issues/6
|
||||
|
||||
#7 Replace ADS-B stub with real disabled/degraded source handling
|
||||
https://git.wilkensxl.de/MrSphay/intelligence-terminal/issues/7
|
||||
https://git.wilkensxl.de/Code-Inc/intelligence-terminal/issues/7
|
||||
|
||||
#8 Clean inherited public-demo and upstream marketing references
|
||||
https://git.wilkensxl.de/MrSphay/intelligence-terminal/issues/8
|
||||
https://git.wilkensxl.de/Code-Inc/intelligence-terminal/issues/8
|
||||
```
|
||||
|
||||
## Verification Already Performed
|
||||
@@ -366,7 +386,7 @@ Audit result:
|
||||
0 high vulnerabilities
|
||||
```
|
||||
|
||||
Docker build and smoke test were performed locally earlier:
|
||||
Docker build and smoke test were performed locally earlier against the now-legacy `mrsphay` image namespace. Current deployments must use the `code-inc` image documented above:
|
||||
|
||||
```bash
|
||||
docker build -t git.wilkensxl.de/mrsphay/intelligence-terminal:latest .
|
||||
@@ -420,7 +440,7 @@ origin/main
|
||||
1. Clone the Gitea repository:
|
||||
|
||||
```bash
|
||||
git clone https://git.wilkensxl.de/MrSphay/intelligence-terminal.git
|
||||
git clone https://git.wilkensxl.de/Code-Inc/intelligence-terminal.git
|
||||
cd intelligence-terminal
|
||||
git checkout codex/production-intelligence-terminal
|
||||
```
|
||||
@@ -460,7 +480,7 @@ if ($env:GITEA_TOKEN) { "GITEA_TOKEN=set" } else { "GITEA_TOKEN=missing" }
|
||||
```bash
|
||||
npm run test:unit
|
||||
docker compose --env-file .env.example config
|
||||
docker pull git.wilkensxl.de/mrsphay/intelligence-terminal:latest
|
||||
docker pull git.wilkensxl.de/code-inc/intelligence-terminal:latest
|
||||
```
|
||||
|
||||
6. Start with Dockge/Pangolin using the README compose example and a `.env` based on `.env.example`.
|
||||
@@ -479,11 +499,11 @@ docker pull git.wilkensxl.de/mrsphay/intelligence-terminal:latest
|
||||
For deployment:
|
||||
|
||||
```bash
|
||||
docker pull git.wilkensxl.de/mrsphay/intelligence-terminal:latest
|
||||
docker pull git.wilkensxl.de/code-inc/intelligence-terminal:latest
|
||||
```
|
||||
|
||||
For a pinned deployment:
|
||||
|
||||
```bash
|
||||
docker pull git.wilkensxl.de/mrsphay/intelligence-terminal:20260517
|
||||
docker pull git.wilkensxl.de/code-inc/intelligence-terminal:20260703
|
||||
```
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 881 KiB |
@@ -3,7 +3,7 @@
|
||||
1. Confirm `.env.example`, README compose sample, and registry image name match.
|
||||
2. Run `npm run test:unit`.
|
||||
3. Run `docker compose config`.
|
||||
4. Build `git.wilkensxl.de/mrsphay/intelligence-terminal:latest`.
|
||||
4. Build `git.wilkensxl.de/code-inc/intelligence-terminal:latest`.
|
||||
5. Start the image and verify `/api/health`.
|
||||
6. Push branch to Gitea.
|
||||
7. Push `latest` and a dated image tag to the Gitea Registry.
|
||||
|
||||
21
docs/source-fetch-instrumentation.md
Normal file
21
docs/source-fetch-instrumentation.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# Source Fetch Instrumentation
|
||||
|
||||
`safeFetch()` and `safeFetchText()` attribute requests to `/api/metrics.fetch.bySource`.
|
||||
|
||||
Rules:
|
||||
|
||||
- Prefer passing an explicit `source` option from source modules when the call has a clear Crucix source name.
|
||||
- If `source` is omitted, the shared helper infers a stable provider name from the request host.
|
||||
- Unknown hosts fall back to the lowercase host instead of the old `unknown` bucket.
|
||||
- Raw `fetch()` calls should be limited to cases where the shared helper cannot represent the protocol cleanly.
|
||||
|
||||
Current raw-fetch exceptions:
|
||||
|
||||
| Area | Reason |
|
||||
| --- | --- |
|
||||
| OAuth/session handshakes | Token exchange calls often need custom form bodies, credential headers, or status-specific diagnostics. |
|
||||
| Bot and alert delivery | Telegram/Discord alert calls are outbound operator notifications, not intelligence source health. |
|
||||
| LLM providers | Provider clients already track model/provider status separately from source fetch health. |
|
||||
| Dashboard browser calls | Browser-side `/api/*` and asset fetches are UI behavior, not source provider health. |
|
||||
|
||||
When adding a new intelligence source, use `safeFetch(url, { source: 'SourceName' })` unless there is a documented exception.
|
||||
@@ -10,6 +10,7 @@ import { MistralProvider } from "./mistral.mjs";
|
||||
import { OllamaProvider } from "./ollama.mjs";
|
||||
import { GrokProvider } from "./grok.mjs";
|
||||
import { OpenAICompatibleProvider } from "./openai-compatible.mjs";
|
||||
import { LiteLLMProvider } from "./litellm.mjs";
|
||||
|
||||
export { LLMProvider } from "./provider.mjs";
|
||||
export { AnthropicProvider } from "./anthropic.mjs";
|
||||
@@ -22,6 +23,7 @@ export { MistralProvider } from "./mistral.mjs";
|
||||
export { OllamaProvider } from "./ollama.mjs";
|
||||
export { GrokProvider } from "./grok.mjs";
|
||||
export { OpenAICompatibleProvider } from "./openai-compatible.mjs";
|
||||
export { LiteLLMProvider } from "./litellm.mjs";
|
||||
|
||||
/**
|
||||
* Create an LLM provider based on config.
|
||||
@@ -66,6 +68,9 @@ export function createLLMProvider(llmConfig) {
|
||||
model: model || 'local-model',
|
||||
requiresApiKey: false,
|
||||
});
|
||||
case "litellm":
|
||||
case "lite-llm":
|
||||
return new LiteLLMProvider(common);
|
||||
case "openrouter":
|
||||
return new OpenRouterProvider(common);
|
||||
case "gemini":
|
||||
|
||||
32
lib/llm/litellm.mjs
Normal file
32
lib/llm/litellm.mjs
Normal file
@@ -0,0 +1,32 @@
|
||||
// LiteLLM proxy provider using the OpenAI-compatible chat completions API.
|
||||
|
||||
import { OpenAICompatibleProvider } from './openai-compatible.mjs';
|
||||
|
||||
export class LiteLLMProvider extends OpenAICompatibleProvider {
|
||||
constructor(config = {}) {
|
||||
const baseUrl = config.baseUrl?.replace(/\/+$/, '') || null;
|
||||
const model = config.model || null;
|
||||
|
||||
super({
|
||||
...config,
|
||||
name: 'litellm',
|
||||
baseUrl: baseUrl || 'http://localhost:4000/v1',
|
||||
model: model || 'unconfigured',
|
||||
requiresApiKey: true,
|
||||
});
|
||||
|
||||
this.baseUrl = baseUrl;
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
get isConfigured() {
|
||||
return Boolean(this.baseUrl && this.apiKey && this.model);
|
||||
}
|
||||
|
||||
get status() {
|
||||
if (!this.baseUrl) return { state: 'misconfigured', reason: 'LLM_BASE_URL is required for LiteLLM' };
|
||||
if (!this.apiKey) return { state: 'misconfigured', reason: 'LLM_API_KEY is required for LiteLLM' };
|
||||
if (!this.model) return { state: 'misconfigured', reason: 'LLM_MODEL is required for LiteLLM' };
|
||||
return { state: 'configured', provider: this.name, model: this.model, baseUrl: this.baseUrl };
|
||||
}
|
||||
}
|
||||
@@ -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/reddit-source.test.mjs test/acled-source.test.mjs test/mojibake-text.test.mjs test/adsb.test.mjs test/dashboard-geotagging.test.mjs",
|
||||
"test:unit": "node --test test/llm-openrouter.test.mjs test/llm-ollama.test.mjs test/llm-openai-compatible.test.mjs test/llm-litellm.test.mjs test/fetch-utils.test.mjs test/reddit-source.test.mjs test/acled-source.test.mjs test/mojibake-text.test.mjs test/adsb.test.mjs test/dashboard-geotagging.test.mjs",
|
||||
"compose:config": "docker compose config",
|
||||
"clean": "node scripts/clean.mjs",
|
||||
"fresh-start": "npm run clean && npm start"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { safeFetch, safeFetchText, getFetchMetrics } from '../apis/utils/fetch.mjs';
|
||||
import { safeFetch, safeFetchText, getFetchMetrics, inferFetchSource } from '../apis/utils/fetch.mjs';
|
||||
import { formatStaleAlert, shouldSendStaleAlert } from '../lib/stale-alerts.mjs';
|
||||
|
||||
test('safeFetch reports HTML as degraded JSON response', async () => {
|
||||
@@ -101,6 +101,31 @@ test('safeFetchText returns text and byte count', async () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('safeFetch attributes unlabelled requests to a stable provider source', async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = async () => ({
|
||||
ok: true,
|
||||
status: 200,
|
||||
headers: { get: () => 'application/json' },
|
||||
text: async () => '{"observations":[]}',
|
||||
});
|
||||
try {
|
||||
const data = await safeFetch('https://api.fred.stlouisfed.org/fred/series/observations?series_id=VIXCLS', { retries: 0 });
|
||||
assert.deepEqual(data, { observations: [] });
|
||||
const bucket = getFetchMetrics().bySource.FRED;
|
||||
assert.ok(bucket.requests >= 1);
|
||||
assert.equal(bucket.lastStatus, 200);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test('inferFetchSource returns provider names and host fallback', () => {
|
||||
assert.equal(inferFetchSource('https://api.bls.gov/publicAPI/v2/timeseries/data/CPI'), 'BLS');
|
||||
assert.equal(inferFetchSource('https://query1.finance.yahoo.com/v8/finance/chart/%5EGSPC'), 'YahooFinance');
|
||||
assert.equal(inferFetchSource('https://unknown.example.test/path'), 'unknown.example.test');
|
||||
});
|
||||
|
||||
test('SSE endpoint sends reconnect guidance and clears heartbeat timer', () => {
|
||||
const server = readFileSync(new URL('../server.mjs', import.meta.url), 'utf8');
|
||||
const config = readFileSync(new URL('../crucix.config.mjs', import.meta.url), 'utf8');
|
||||
|
||||
76
test/llm-litellm.test.mjs
Normal file
76
test/llm-litellm.test.mjs
Normal file
@@ -0,0 +1,76 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { LiteLLMProvider } from '../lib/llm/litellm.mjs';
|
||||
import { createLLMProvider } from '../lib/llm/index.mjs';
|
||||
|
||||
test('factory creates a configured LiteLLM provider', () => {
|
||||
const provider = createLLMProvider({
|
||||
provider: 'litellm',
|
||||
baseUrl: 'https://llm.example.test/v1/',
|
||||
apiKey: 'proxy-key',
|
||||
model: 'private-model',
|
||||
});
|
||||
|
||||
assert.ok(provider instanceof LiteLLMProvider);
|
||||
assert.equal(provider.baseUrl, 'https://llm.example.test/v1');
|
||||
assert.equal(provider.isConfigured, true);
|
||||
assert.deepEqual(provider.status, {
|
||||
state: 'configured',
|
||||
provider: 'litellm',
|
||||
model: 'private-model',
|
||||
baseUrl: 'https://llm.example.test/v1',
|
||||
});
|
||||
});
|
||||
|
||||
test('LiteLLM requires base URL, API key, and model', () => {
|
||||
const missingBaseUrl = new LiteLLMProvider({ apiKey: 'key', model: 'model' });
|
||||
assert.equal(missingBaseUrl.isConfigured, false);
|
||||
assert.equal(missingBaseUrl.status.reason, 'LLM_BASE_URL is required for LiteLLM');
|
||||
|
||||
const missingKey = new LiteLLMProvider({ baseUrl: 'https://llm.example.test/v1', model: 'model' });
|
||||
assert.equal(missingKey.status.reason, 'LLM_API_KEY is required for LiteLLM');
|
||||
|
||||
const missingModel = new LiteLLMProvider({ baseUrl: 'https://llm.example.test/v1', apiKey: 'key' });
|
||||
assert.equal(missingModel.status.reason, 'LLM_MODEL is required for LiteLLM');
|
||||
});
|
||||
|
||||
test('LiteLLM sends bearer-authenticated OpenAI-compatible requests', async () => {
|
||||
const provider = new LiteLLMProvider({
|
||||
baseUrl: 'https://llm.example.test/v1',
|
||||
apiKey: 'proxy-key',
|
||||
model: 'private-model',
|
||||
temperature: 0.15,
|
||||
maxTokens: 512,
|
||||
});
|
||||
const originalFetch = globalThis.fetch;
|
||||
|
||||
globalThis.fetch = async (url, options) => {
|
||||
assert.equal(url, 'https://llm.example.test/v1/chat/completions');
|
||||
assert.equal(options.headers.Authorization, 'Bearer proxy-key');
|
||||
assert.deepEqual(JSON.parse(options.body), {
|
||||
model: 'private-model',
|
||||
temperature: 0.15,
|
||||
messages: [
|
||||
{ role: 'system', content: 'system' },
|
||||
{ role: 'user', content: 'user' },
|
||||
],
|
||||
max_tokens: 512,
|
||||
});
|
||||
return {
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
choices: [{ message: { content: 'response' } }],
|
||||
usage: { prompt_tokens: 7, completion_tokens: 11 },
|
||||
model: 'private-model',
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
try {
|
||||
const result = await provider.complete('system', 'user');
|
||||
assert.equal(result.text, 'response');
|
||||
assert.deepEqual(result.usage, { inputTokens: 7, outputTokens: 11 });
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user