phantom-aggregator pulls public feeds, normalizes them, and renders low-bandwidth NomadNet pages for field use.
It runs as a FastAPI service with scheduled sync jobs. Source definitions are loaded from JSON, so you can add or disable feeds without changing application code.
- Fetches RSS, Atom, JSON API, and plain-text sources
- Supports parser-specific handlers for NOAA SWPC and CISA KEV
- Includes disaster-focused custom fetchers for FEMA, NHC, wildfires, flooding, and earthquakes
- Optionally summarizes snapshots with a local LLM endpoint
- Optionally extracts full article pages from feed links
- Renders NomadNet pages plus an index page
- Renders a dedicated emergency situational-awareness page (
emergencies.mu) - Captures system telemetry and renders a system status page
- Evaluates snapshots for critical events and can send LXMF alerts
- Cleans old raw snapshots on a daily retention schedule
app/service codecli/source management and custom fetcher import toolsconfig/sources.jsondynamic source definitionsdata/raw/raw source snapshots and extracted article JSONdata/nomadnet/rendered NomadNet pages and extracted article pagesdocs/operator guides
- Python 3.11+
- Network access to configured source URLs
- Optional local LLM server if LLM summarization is enabled
- Optional LXMF toolchain if alert dispatch is enabled
git clone https://github.com/comms-engineer/phantom-aggregator.git
cd phantom-aggregator
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env
python -m app.mainService listens on http://0.0.0.0:8080.
cp .env.example .env
docker compose up --build -dThe container mounts:
./data->/app/data./config->/app/config
On startup, the service:
- Creates runtime directories under
DATA_DIRandCONFIG_DIR - Schedules two jobs:
sync_sourceseverySOURCE_SYNC_INTERVAL_SECONDS- storage cleanup once per day
- Forces an initial sync
- Exposes HTTP endpoints for health, sources, rendered pages, and raw snapshots
During each sync cycle, the service:
- Reloads
/app/config/sources.jsonif it changed - Refreshes sources that are due by
poll_interval_mins - Optionally enriches feed items with extracted full articles
- Summarizes payloads (LLM or fallback text)
- Saves raw snapshots
- Evaluates critical alerts
- Captures system telemetry
- Renders NomadNet pages and index
GET /health-> service health statusGET /sources-> current parsed source documentGET /pages/{name}-> rendered NomadNet page contentGET /raw/{name}-> raw snapshot JSON for a source ID
Source definitions live in /app/config/sources.json (repo path: config/sources.json).
Supported values:
type:rss,atom,json_api,text_feedcategory:weather,space,news,cyber,maritimenomadnet_page: must end with.pageor.mu
Each source entry includes:
id(lowercase letters, numbers, underscore)nametypecategoryurlenabledpoll_interval_minsllm_summarizenomadnet_pagefetch_full_articles(optional override)max_articles_per_feed(optional override)options(optional parser/custom settings)
options.parser = "space_weather_swpc"- requires
options.solar_flux_url - requires
options.forecast_url
- requires
options.parser = "cisa_kev"
Set:
options.custom_fetcher = "app.fetchers.custom.<module>:<ClassName>"
The runtime imports the class dynamically and uses it as a BaseFetcher implementation.
Built-in emergency custom fetchers use:
app.fetchers.fema_alerts:FemaAlertsFetcherapp.fetchers.nhc_hurricanes:NhcHurricanesFetcherapp.fetchers.wildfires:WildfiresFetcherapp.fetchers.flooding_water:FloodingWaterFetcherapp.fetchers.earthquakes:EarthquakesFetcher
The default config/sources.json includes active emergency sources on emergencies.mu:
- FEMA IPAWS + declaration summaries (
fema_alerts, poll every 180m) - NHC tropical updates (
nhc_hurricanes, poll every 60m) - InciWeb wildfire incidents (
wildfires_active, poll every 120m) - USGS gauge + flood warnings (
flooding_water, poll every 30m) - USGS earthquakes M4.5+ (
usgs_earthquakes, poll every 15m)
Emergency sources are preconfigured with fetch_full_articles: true where incident links are available so /app/data/nomadnet/articles/ can carry deeper incident pages.
python cli/manage_sources.py listpython cli/manage_sources.py validate --url "https://example.org/feed.xml" --type rsspython cli/manage_sources.py add \
--type rss \
--category news \
--name "Example News" \
--url "https://example.org/feed.xml"python cli/manage_sources.py toggle --id example_newspython cli/import_custom_fetcher.py --file /tmp/custom_source.py --name custom_sourceImport flow validates the Python file, copies it to app/fetchers/custom/, and appends a source entry to config/sources.json.
When enabled for a source, feed items with link/url are enriched with:
article_uidarticle_pagepath underarticles/<article_uid>.mu
Stored outputs:
- raw article JSON:
data/raw/articles/*.json - rendered article pages:
data/nomadnet/articles/*.mu
Extraction uses trafilatura and request throttling via ARTICLE_REQUEST_DELAY_SECONDS.
The alert service checks snapshots for:
- space weather K-index threshold breaches
- keyword hits in KEV and feed item content
If LXMF_ALERTING_ENABLED=true, it dispatches messages using LXMF_ALERT_COMMAND to each destination in LXMF_ALERT_DESTINATIONS.
Each sync cycle also captures:
- CPU usage and temperature
- memory usage
- storage usage for the data directory
- internet reachability check
- mesh interface status
Outputs:
- raw JSON snapshot:
data/raw/system_health.json - NomadNet page:
data/nomadnet/system.mu
Environment variables are loaded from .env.
| Variable | Default | Purpose |
|---|---|---|
LOG_LEVEL |
INFO |
Application log level |
DATA_DIR |
/app/data |
Runtime data root |
CONFIG_DIR |
/app/config |
Runtime config root |
REQUEST_TIMEOUT_SECONDS |
20 |
HTTP timeout for source fetches |
LLM_TIMEOUT_SECONDS |
10 |
HTTP timeout for LLM requests |
SOURCE_SYNC_INTERVAL_SECONDS |
60 |
Scheduler interval for source refresh checks |
RAW_DATA_MAX_AGE_DAYS |
14 |
Retention period for raw snapshots |
FETCH_FULL_ARTICLES |
true |
Global default for article extraction |
MAX_ARTICLES_PER_FEED |
5 |
Global cap on extracted articles per source refresh |
ARTICLE_REQUEST_DELAY_SECONDS |
2 |
Delay between article extraction requests |
SYSTEM_HEALTH_PAGE_NAME |
system.mu |
Output filename for telemetry page |
SYSTEM_HEALTH_SNAPSHOT_NAME |
system_health |
Raw snapshot basename for telemetry JSON |
CRITICAL_ALERT_K_INDEX_THRESHOLD |
5 |
Threshold for geomagnetic storm alerts |
CRITICAL_ALERT_KEYWORDS |
built-in keyword tuple | Keywords for critical alert matching |
LXMF_ALERTING_ENABLED |
false |
Enable or disable LXMF dispatch |
LXMF_ALERT_DESTINATIONS |
() |
Destination hashes for LXMF alerts |
LXMF_ALERT_COMMAND |
() |
Command template for LXMF dispatch |
MESH_INTERFACE_NAMES |
built-in tuple | Interface name tokens treated as mesh links |
INTERNET_CONNECTIVITY_HOST |
1.1.1.1 |
Host used for reachability check |
INTERNET_CONNECTIVITY_PORT |
53 |
Port used for reachability check |
ENABLE_LLM_SUMMARIZATION |
false |
Global switch for LLM summarization |
LLM_ENDPOINT |
http://host.docker.internal:11434/api/generate |
LLM HTTP endpoint |
LLM_MODEL |
qwen2.5:7b |
LLM model name |
NOMADNET_LINE_LIMIT |
80 |
Wrapping width limit |
NOMADNET_MAX_LINES |
80 |
Max rendered lines per page |
Tuple-style settings can be provided as JSON arrays in .env.
- Source config changes are picked up automatically on the next sync cycle.
- A source can be disabled without removing it from config.
- The cleaner preserves the latest snapshot in each source group and deletes older expired files.
- Corrupt source snapshots are skipped during rendering instead of crashing the service.
docs/SOURCE_DISCOVERY_GUIDE.mddocs/ADDING_NON_TRADITIONAL_SOURCES.mdtemplates/SOURCE_ADAPTER_PROMPT.md
- localized services such as pulsepoint, watchduty, etc
- AI-based assessment of news trends to identify crises and spin up event tracking pages
- methodology for users to scope LLM work based on specific location, situations, and PIRs
- find publicly available intelligence analysis guides to help guide LLMs.
- ingestion of telegram channels
- export of key data elements to CoT tracks distributed over TAK
- ???