feat/powerradar
needs attentionviewing older commitbeced58 · incrementalpre-PRreviewed 2026-07-21 01:13 UTC0H · 3M · 3L · 2I- Purpose
- Replace the manual-CSV PowerRadar (Centrica/Panoramic Power) collection path with an automatic pull worker against the portal's internal /eigw energy-insights-gateway API.
- Goal
- Add powerradar-api — a second access path to the powerradar brand (mirroring shelly-cloud→shelly-fleet) with headless OAuth auth and native 5/15/60-min collection.
- Sub-goals
- SG-1 ✓ Auth + transport engine + live probe — OAuth password grant, browser headers, WAF defeat, ≥12mo retention verified
- SG-2 □ Manifest + catalog + provider registration
- SG-3 □ Pure translation + unit tests (chart-data → MetricsPayload)
- SG-4 □ Handlers (metrics + exploration lambdas)
- SG-5 □ CDK stack + IAM + wiring
- SG-6 □ Seed routing + live end-to-end validation
- SG-7 □ Framework improvements — fold learnings back
- What
- This commit is SG-1: new package scaffold (services/metrics/integrations/powerradar-api/) with vendor types (domain/types.ts), the full transport engine (engine/eigw-client.ts — OAuth2 password grant, token memo, WAF-defeating browser headers, chart-data fetch with 401-retry), an intentionally-empty index.ts (handler-entry-point package), and intent log (3 exploration iterations).
- Why
- SG-1 discovered the auth model is simpler than spec'd: OAuth password grant against a Spring broker, not Cognito REFRESH_TOKEN_AUTH. The broker is fully headless (no MFA for this grant type). WAF requires browser headers. Retention ≥12mo verified. Transport layer is now live-proven.
- Areas
- .branch/+254−314services/metrics/integrations/powerradar-api/+644−0
- Blast
- Entirely new package — no existing code modified. 8 files, +898/-314 churn (mostly .branch/ doc updates). No risk to other integrations or domains.
Findings · 8
correctness1
JWT validation accepts 2-part tokens — should require exactly 3
services/metrics/integrations/powerradar-api/src/engine/eigw-client.ts:162
jwtExpiryMs() checks parts.length < 2 — accepts a 2-part token (header.payload with no signature) and attempts to decode it. Valid JWTs are always exactly 3 parts. Change to parts.length !== 3 to reject malformed inputs earlier. Practical impact is nil (the broker only returns well-formed Cognito JWTs) but the guard is loose.
security1
Public OAuth client credentials hardcoded in source
services/metrics/integrations/powerradar-api/src/engine/eigw-client.ts:55
OAUTH_CLIENT_ID='webapi' and OAUTH_CLIENT_SECRET='webapisecret' are documented as public SPA constants (extracted from the app bundle, shared across all users) — not an org secret. Low security risk. The maintenance concern: if PowerRadar ever rotates these constants, updating requires a code change + redeploy. A comment pointing to the specific bundle source (index-BpQKjFYa.js) would help a future maintainer verify and update them.
conventions4
scope.md success criteria stale — auth model changed
.branch/scope.md:16
Success criteria still reads 'one-time MFA login → refresh token stored in Secrets Manager' and 'Cognito REFRESH_TOKEN_AUTH'. Actual implementation is an OAuth2 password grant against a Spring broker (username+password credential, not a refresh token). The intent.md correctly documents the discovery but the scope.md checklist (line 16) and SG-2 credentialFields note (line 133) still reference the old model. Update scope.md so SG-2 implementers don't misread the credential shape.
Error types use ok/reason pattern instead of ADR-016 _tag+statusCode
services/metrics/integrations/powerradar-api/src/domain/types.ts:337
PowerradarFetchFailure uses {ok: false, reason, httpStatus, details} rather than the canonical discriminated union {_tag, statusCode} required by ADR-016. The transport layer intent (map to InvocationError._tag in the handler) is clear, but the handler (SG-4) will need an explicit mapping step. If other integrations follow _tag+statusCode, this creates an inconsistency. Consider whether this transport-internal pattern should be documented as a sanctioned exception or aligned to {_tag, statusCode} now before SG-4 consumes it.
clearTokenMemo() is exported as a test hook without a test file
services/metrics/integrations/powerradar-api/src/engine/eigw-client.ts:190
Exported only for testing (clearTokenMemo comment line 190). No __tests__/ directory or test files exist yet. The export is appropriate but could be marked with a JSDoc @internal tag to signal it's not part of the public API. Wire it into engine tests when they're written.
@batu/result not in dependencies — intentional deviation for transport layer
services/metrics/integrations/powerradar-api/package.json:18
Other integrations use Result<T, E> from @batu/result for fallible operations (ADR-016). This package uses its own ok/reason discriminated union. If intentional (transport layers can use simpler error shapes before mapping to domain errors), document it in a package-level comment or package.json description. If it should align with @batu/result, add the dependency and refactor before SG-4 solidifies the handler contract.
tests1
Engine unit tests absent — high-risk paths untested
services/metrics/integrations/powerradar-api/src/engine/eigw-client.ts:141
clearTokenMemo() test hook is exported (line 190) but no test files exist. isCloudFrontBlock(), jwtExpiryMs(), getToken() memo/refresh logic, and parseTokenResponse() error-envelope parsing are the riskiest paths — WAF detection depends on heuristics, JWT decoding must handle malformed tokens, token refresh must keep the old refresh token when the grant doesn't return a new one. These paths should have unit tests before SG-3 merges; adding them alongside chart-to-batu.ts translation tests is the natural time.
improvement1
Browser User-Agent string (Chrome/148) will stale silently
services/metrics/integrations/powerradar-api/src/engine/eigw-client.ts:70
The BROWSER_HEADERS object pins Chrome/148 + matching sec-ch-ua. CloudFront WAF bot-detection may eventually reject a browser version that's years old. A stale UA would produce blocked failures with no obvious root cause. Consider adding a comment with the capture date (2026-07-20) so future oncall knows when it was last verified, and add this UA to the SG-7 framework-learnings update checklist.
History · 15 commits
- f34e4ceneeds attentionincremental0H · 1M · 3L2026-07-23 00:32
- 1bc8aedneeds attentionincremental0H · 2M · 3L2026-07-22 21:34
- 31e63bbneeds attentionincremental0H · 2M · 5L2026-07-22 20:53
- 482cb88safeincremental0H · 0M · 1L2026-07-22 17:56
- 9a73b79safeincremental0H · 0M · 0L2026-07-22 17:34
- 87f8298needs attentionincremental0H · 1M · 3L2026-07-22 16:59
- 1e80096safeincremental0H · 0M · 1L2026-07-22 16:42
- 6ed65b4safeincremental0H · 1M · 3L2026-07-22 00:00
- 74a1131needs attentionfull0H · 7M · 12L2026-07-21 19:06
- 563252bneeds attentionincremental4H · 9M · 8L2026-07-21 18:32
- beced58needs attentionincremental0H · 3M · 3L2026-07-21 01:13current
- 7fa8684needs attentionincremental4H · 9M · 7L2026-07-20 22:56
- 230784fneeds attentionfull2H · 11M · 14L2026-07-10 00:15
- b876b54needs attentionincremental2H · 2M · 5L2026-07-08 04:44
- ec4847fneeds attentionfull2H · 11M · 8L2026-07-08 03:32