Central - data hub spine. Adapters -> NATS/JetStream -> archive.
  • Python 90.5%
  • HTML 9.1%
  • PLpgSQL 0.4%
Find a file
Matt Johnson 7de460bc06 fix(4-1): resolve api_key alias from per-adapter settings, not class attr
The /adapters list view's "⚠️ API Key Missing" chip, the /adapters/{name}
edit form's disabled enable-checkbox, the POST error re-render path, AND
the supervisor's adapter-start precondition all compared the hardcoded
SourceAdapter class attribute `requires_api_key` against `config.api_keys`,
ignoring the per-row `settings[api_key_field]` alias the operator
actually selected via the form.

FIRMS' class attr is `requires_api_key = "firms"`; the api_keys_new.html
placeholder text steers operators toward aliases like `firms_production`
instead, and the FIRMSSettings.api_key_alias field is exactly the
overridable slot that the form writes. The four predicates ignored that
slot, so a working key under any non-default alias was treated as
missing — chip on, checkbox disabled, supervisor refusing to start with
`last_error = "missing api key: firms"`.

Audit: FIRMS is the only adapter today with `requires_api_key != None`.
Every other adapter is unaffected by either the route or supervisor
predicate.

Helper module:
- src/central/api_key_resolver.py exposes:
    resolve_api_key_alias(adapter_cls, settings) -> str | None
        Pure sync function. Returns the alias to consult, or None when no
        key is required. Supervisor uses this directly + its own
        get_api_key.
    adapter_has_resolved_api_key(conn, adapter_cls, settings) -> (bool, alias)
        Async wrapper that runs the SELECT 1 against config.api_keys.
        The three GUI routes use this.
  Resolution: settings[api_key_field] when set to a non-empty str,
  otherwise the class-attr default.

Four call sites swapped:
- routes.py:adapters_list           (/adapters list — warning chip)
- routes.py:adapters_edit_form      (/adapters/{name} edit GET — disabled checkbox)
- routes.py:adapters_edit_submit    (POST error re-render)
- supervisor.py:_start_adapter      (adapter-start precondition)

Side-effect tests/test_adapters.py fix:
- TestAdaptersJsonbRegression::test_adapters_edit_fetches_api_keys_into_context
  used `AsyncMock()` (no return_value) for mock_conn.__aexit__. AsyncMock
  without a return_value yields a MagicMock — which is truthy, and the
  async context manager protocol reads truthy from __aexit__ as
  "exception suppressed." That silently swallowed any error inside
  `async with` blocks. The route refactor moved an assignment inside the
  one async with at site 2, so a swallowed mock error left the variable
  unbound. Fixed: `AsyncMock(return_value=None)` + a comment so the next
  person doesn't re-introduce the bug. fetchval mock added because the
  resolver now issues it (the swallowed exception previously hid the
  missing mock).

Verification:
- pytest: 479 passed (was 469; +10 new resolver tests).
- grep -rn "adapter_cls.requires_api_key" /opt/central/src returns only
  the new helper (2 lines, same file).
- Resolver against live FIRMS settings: resolved_alias='firms_production',
  has_key=True, api_key_missing=False -> NO warning chip, checkbox
  CLICKABLE.
- Supervisor on live CT104: FIRMS flipped enabled=true via DB UPDATE;
  supervisor started the adapter with `api_key_present: true,
  api_key_alias: 'firms_production'`; last_error cleared from "missing
  api key: firms" -> NULL; two satellite polls completed (VIIRS_SNPP_NRT
  477 features, VIIRS_NOAA20_NRT 400 features); 869 new events published
  to JetStream.

NOTE: This commit's verification flipped FIRMS to enabled=true in the
running config — the adapter is now actively polling. Pause via the UI
if that's not intended for now; the bug fix itself does not require
FIRMS to be enabled.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 23:08:11 +00:00
docs docs(2-I): producer integration spec — docs/PRODUCER-INTEGRATION.md 2026-05-19 21:17:48 +00:00
etc-templates scaffold: initial repository structure 2026-05-15 19:16:24 +00:00
scripts scaffold: initial repository structure 2026-05-15 19:16:24 +00:00
sql feat(2-G): USGS NWIS adapter (OGC API) + CENTRAL_HYDRO stream 2026-05-19 16:50:21 +00:00
src/central fix(4-1): resolve api_key alias from per-adapter settings, not class attr 2026-05-19 23:08:11 +00:00
systemd feat(gui): add auth core, setup gate, and first-run operator creation 2026-05-17 05:30:49 +00:00
tests fix(4-1): resolve api_key alias from per-adapter settings, not class attr 2026-05-19 23:08:11 +00:00
.gitattributes chore: normalize line endings to LF 2026-05-16 22:26:12 +00:00
.gitignore feat(gui): add auth core, setup gate, and first-run operator creation 2026-05-17 05:30:49 +00:00
.python-version foundation: models, adapter ABC, config, CE wire, schema 2026-05-15 21:08:56 +00:00
CHANGELOG.md docs: add v0.3.0 changelog entry and network bindings reference (#29) 2026-05-18 14:26:09 -06:00
LICENSE scaffold: initial repository structure 2026-05-15 19:16:24 +00:00
pyproject.toml release: bump version to 0.3.0 (#30) 2026-05-18 14:29:28 -06:00
README.md docs: add test database setup, restore geom to test fixture 2026-05-17 18:26:48 +00:00
uv.lock feat(gui): add auth core, setup gate, and first-run operator creation 2026-05-17 05:30:49 +00:00

Central

Central is the data hub spine for the infrastructure. Adapters normalize upstream sources into a canonical event shape, publish CloudEvents to NATS/JetStream, and archive to TimescaleDB for historical query. Single-LXC deployment.

Status

Phase 0 — scaffold. Not yet operational.

Architecture

  • Python 3.12 (uv-managed)
  • NATS + JetStream for live event bus
  • TimescaleDB + PostGIS for archive and geospatial query
  • One supervisor process managing adapter lifecycle
  • One archive consumer process persisting events to TimescaleDB
  • Both processes systemd-managed

Testing

See docs/test-database.md for test database setup.

License

MIT. See LICENSE.