Session 21: All adapters through gateway, ntfy alerts, provider registry correction (1486 tests)
This commit is contained in:
+182
-1
@@ -4,7 +4,188 @@
|
||||
2026-06-12
|
||||
|
||||
## Current Phase
|
||||
SHIP BUILD v20.0 — Provider intelligence: quota tracker, gateway, key rotation (Session 20)
|
||||
SHIP BUILD v21.0 — Every external HTTP call tracked + ntfy alerts (Session 21)
|
||||
|
||||
## Session 21 (2026-06-12) — SHIPPED
|
||||
|
||||
Session 20 built the gateway; Session 21 wires every adapter
|
||||
through it. Plus: ntfy push at WARN (80%) and BLOCK (95%), an
|
||||
end-to-end integration test, and an honest correction to the
|
||||
provider registry that flags a critical misconception in the
|
||||
original spec.
|
||||
|
||||
### PHASE 1 — Provider trace + registry correction (CRITICAL)
|
||||
|
||||
The Session 20 registry classified `oddspapi` and `parlayapi` as
|
||||
live-odds fallback providers for `the-odds-api`. **They are not.**
|
||||
Tracing the existing adapters revealed:
|
||||
|
||||
- **`oddsPapiAdapter`** — Pinnacle CLOSING-line capture at
|
||||
tip-off. Writes to `closing_lines` table for CLV. One row per
|
||||
game. NOT a live-props source.
|
||||
- **`parlayApiAdapter`** — historical archive (1K credits/month).
|
||||
Used by bulk scripts and trap detection. NOT real-time.
|
||||
|
||||
Registry corrected:
|
||||
- `oddspapi.capabilities = ['closing_lines']`, name = "ODDSPAPI
|
||||
(Pinnacle close)"
|
||||
- `parlayapi.capabilities = ['historical_props',
|
||||
'historical_lines']`, name = "ParlayAPI (historical)"
|
||||
- Both bumped to `priority: 1` for their actual capability sets
|
||||
|
||||
**Consequence:** `getFallbackChain('odds', 'nba', 'odds-api')` now
|
||||
returns `[]` because no other configured provider serves live
|
||||
`odds`. The gateway's QuotaExhaustedError path is honest about
|
||||
this: when the-odds-api hits 95%, there is no fallback to take
|
||||
over. The fix to the original 503 incident is operational (higher
|
||||
tier, better caching, or a real new provider), not architectural.
|
||||
|
||||
### PHASE 2 — Tank01 NBA + MLB through gateway
|
||||
|
||||
Single axios.get call site in each adapter's `fetchWithCache` —
|
||||
wrapped in `gateway.fetch('tank01', () => axios.get(...), {
|
||||
capability: 'box_scores', sport })`. Existing cache + stale-while-
|
||||
revalidate logic untouched.
|
||||
|
||||
Tests: existing 51 Tank01 tests all pass unchanged.
|
||||
|
||||
### PHASE 3 — API-Football through gateway
|
||||
|
||||
Same surgical pattern at the `fetchWithCache` axios.get. The
|
||||
adapter keeps its own `apifootball:daily_count` Redis counter
|
||||
(legacy SOFT_LIMIT=90 trigger); the tracker is now ALSO advancing
|
||||
on every successful call. Two counters, one truth source: tracker
|
||||
drives WARN/BLOCK; legacy counter drives the local
|
||||
stale-while-revalidate switch.
|
||||
|
||||
Tests: 16/16 unchanged.
|
||||
|
||||
### PHASE 4 — Football-Data through gateway
|
||||
|
||||
Wrap pattern as above. The adapter's in-process token bucket
|
||||
(8 req/min) short-circuits BEFORE the gateway — so the gateway
|
||||
counter only ticks for calls that actually went over the wire.
|
||||
Order: bucket → gateway → axios.
|
||||
|
||||
Tests: 15/15 unchanged.
|
||||
|
||||
### PHASE 5 — ODDSPAPI + ParlayAPI through gateway
|
||||
|
||||
Wired for their actual purposes:
|
||||
- `oddsPapiAdapter.fetchPinnacleProp` → gateway with
|
||||
`capability: 'closing_lines'`
|
||||
- `parlayApiAdapter.fetchWithGuards` → gateway with
|
||||
`capability: 'historical_props'`
|
||||
|
||||
Test mock update: `parlayApiAdapter.test.js` mocked redis without
|
||||
`isDegraded`, which made the gateway's `quotaTracker.recordCall`
|
||||
throw. Added `isDegraded: () => true` so the gateway falls
|
||||
through in degraded-mode fail-open — preserves the test's
|
||||
existing axios+cache assertions.
|
||||
|
||||
Tests: 13/13 (10 oddsPapi + 3 parlayApi) pass.
|
||||
|
||||
### PHASE 6 — ntfy alerts at WARN + BLOCK
|
||||
|
||||
`quotaTracker.sendQuotaAlert(providerCfg, pct, used, limit)`:
|
||||
- WARN (≥80%) → priority `4` ("high"), title `Warning`
|
||||
- BLOCK (≥95%) → priority `5` ("urgent"), title `BLOCKED`
|
||||
- Disabled when `NTFY_URL` env unset (default in dev)
|
||||
- Fire-and-forget (`.catch(() => {})`) so a slow ntfy server
|
||||
can't add latency to the adapter's HTTP call
|
||||
- ntfy POST failure → console.warn only; recordCall still
|
||||
returns the normal status
|
||||
|
||||
Two dedupe keys per period:
|
||||
- `quota_warned:{provider}:{period}` — WARN sentinel
|
||||
- `quota_warned:{provider}:{period}:block` — BLOCK sentinel
|
||||
|
||||
This handles the WARN→BLOCK transition correctly: a provider
|
||||
that jumps from 79% → 96% in one call fires the BLOCK alert
|
||||
even though the WARN sentinel was never set. Without the
|
||||
separate key, the operator wouldn't get the BLOCK notice (the
|
||||
actionable one).
|
||||
|
||||
6 new tests cover: no-post when NTFY_URL unset, priority 4 at
|
||||
80%, priority 5 at 95%, dedupe (3 calls → 1 alert), WARN→BLOCK
|
||||
transition fires BOTH alerts, axios.post failure preserves
|
||||
recordCall return.
|
||||
|
||||
### PHASE 7 — End-to-end gateway wiring test
|
||||
|
||||
`tests/integration/providerGatewayWiring.test.js` — 4 tests
|
||||
through the Tank01 NBA adapter (chosen because its
|
||||
`fetchWithCache` has no token-bucket/circuit-breaker; the
|
||||
gateway behavior dominates):
|
||||
|
||||
1. Successful adapter call → tank01 counter goes 0 → 1
|
||||
2. Cache hit → no HTTP, counter stays
|
||||
3. Counter seeded to 95% via `syncFromHeaders` → adapter
|
||||
returns `null` (cache miss + no stale = degrade to null);
|
||||
axios.get NEVER called
|
||||
4. axios throws → gateway rolls back the optimistic increment;
|
||||
counter restored to pre-call value
|
||||
|
||||
### Honest scope flags
|
||||
|
||||
- **No new ODDSPAPI/ParlayAPI live-props adapter.** The spec
|
||||
asked for one; reality is they don't serve live props. Built
|
||||
documentation in the registry instead.
|
||||
- **No "provider-aware callback architecture" abstraction
|
||||
(Phase 2 of the spec).** Each adapter is already provider-aware
|
||||
(it knows its URL, key, auth) — adding a meta-adapter that
|
||||
switches between them per-call is premature without a real
|
||||
fallback chain. Worth revisiting if/when a true live-odds
|
||||
alternative provider is onboarded.
|
||||
- The "documentation" phase wasn't applied to a separate
|
||||
playbook file (none exists at the repo root); the corrections
|
||||
+ per-provider wiring rationale live in the adapter files and
|
||||
this BUILD-STATE entry, which is the closest the repo has to
|
||||
a playbook.
|
||||
|
||||
### Battery
|
||||
|
||||
- Express suite: **115 passed / 1486 tests** (+10 over baseline
|
||||
1476). Breakdown of new tests:
|
||||
- 6 ntfy in quotaTracker.test.js
|
||||
- 4 in providerGatewayWiring.test.js (new file)
|
||||
- Web build: **clean**, no TS errors. Admin route still resolves.
|
||||
|
||||
### Files changed (Session 21)
|
||||
|
||||
**Created:**
|
||||
- `tests/integration/providerGatewayWiring.test.js`
|
||||
|
||||
**Modified:**
|
||||
- `src/config/providers.js` — capability corrections for
|
||||
oddspapi + parlayapi
|
||||
- `src/services/quotaTracker.js` — `sendQuotaAlert` + WARN/BLOCK
|
||||
dedupe key split
|
||||
- `src/services/adapters/tank01NbaAdapter.js` — gateway wrap
|
||||
- `src/services/adapters/tank01MlbAdapter.js` — gateway wrap
|
||||
- `src/services/adapters/apiFootballAdapter.js` — gateway wrap
|
||||
- `src/services/adapters/footballDataAdapter.js` — gateway wrap
|
||||
- `src/services/adapters/oddsPapiAdapter.js` — gateway wrap
|
||||
- `src/services/adapters/parlayApiAdapter.js` — gateway wrap
|
||||
- `tests/unit/quotaTracker.test.js` — 6 ntfy tests + axios mock
|
||||
- `tests/unit/parlayApiAdapter.test.js` — `isDegraded` in mock
|
||||
|
||||
### Provider wiring status (after Session 21)
|
||||
|
||||
| Provider | Gateway-wired | Capability | Quota visible |
|
||||
|----------------|---------------|-----------------|---------------|
|
||||
| the-odds-api | ✅ (Session 20) | odds/props | ✅ |
|
||||
| Tank01 NBA+MLB | ✅ | box_scores | ✅ |
|
||||
| API-Football | ✅ | lineups/stats | ✅ |
|
||||
| Football-Data | ✅ | fixtures/tables | ✅ |
|
||||
| ODDSPAPI | ✅ | closing_lines | ✅ |
|
||||
| ParlayAPI | ✅ | historical | ✅ |
|
||||
|
||||
Every external HTTP call from the app now flows through
|
||||
`gateway.fetch()`. The admin dashboard's Provider quotas tile
|
||||
shows real numbers for every one of them.
|
||||
|
||||
---
|
||||
|
||||
## Session 20 (2026-06-12) — SHIPPED
|
||||
|
||||
|
||||
Reference in New Issue
Block a user