docs: canonical architecture overview - #1
Conversation
New docs/architecture.md ties the system map together: summary, site-convention pointer, per-component paragraphs, data-flow ASCII diagram, persistence table, config source-of-truth, and a reading order for new developers. Line references into main.go, state.go, dispatch.go, registry.go, and the twin services are verified against the current code. Sibling docs (ml-models.md, writing-a-driver.md, safety.md, operations.md, api.md, testing.md, tsdb.md, device-identity.md) are being authored in parallel branches; existing links in this doc all resolve today, and the two forward-references are labelled planned.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 78592d9c75
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| `runLoop` at line 182). Each driver has its own Lua VM and a | ||
| capability-scoped host environment (MQTT/Modbus only if the driver's | ||
| config grants it). Drivers poll their device, call | ||
| `host.emit_telemetry()` to push readings into the telemetry store in |
There was a problem hiding this comment.
Refer to the correct Lua telemetry API
This section says Lua drivers call host.emit_telemetry(), but the Lua runtime exposes host.emit(...) (see go/internal/drivers/lua.go where host.emit is registered). If a developer follows this architecture doc when writing a new Lua driver, telemetry emission will fail because emit_telemetry is not a Lua host function, which can block driver bring-up and debugging.
Useful? React with 👍 / 👎.
|
🎉 This PR is included in version 2.0.1 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
ci: add semantic-release + multi-arch Docker image to GHCR
5-agent vote per comment accepted 6 of 7: - Offload HealthTick observeAt to a goroutine so a slow ntfy Publish (10 s timeout) can't stall the control loop. The event bus runs handlers inline on the publisher, and HealthTick is emitted every control tick. (Copilot #1) - Nil-publisher guard in dispatch(): cold-start or reload with an unknown/unconfigured provider used to panic at s.pub.Publish; now it's treated as a failed dispatch with a history entry. (Copilot #3) - Emit NotificationDispatched on template render failures too, so the history UI surfaces template errors instead of only publish errors. (Copilot #4) - activeAlert leak fix: when no driver_recovered rule is enabled, a post-pass cleanup clears activeAlert for any driver whose telemetry is fresh (<30 s). Preserves the original lifecycle: when recovered IS enabled, the recovered rule still owns the clear. (Copilot #5) - Stop seeding backend default templates into currentConfig. Render title/body template fields as raw inputs whose placeholder is the server default. Blank stays blank, backend fallback keeps working, and future server-side default changes apply without operator action. (Copilot #6) - driver_recovered ignores threshold_s on the backend (hardcoded 30 s staleness window). Don't render the threshold input for that event type — show a clarifying note instead. (Copilot #7) Tests: new TestActiveAlertClearsWhenRecoveredDisabled and TestNilPublisherNoPanic. Full suite still green (35 packages). Skipped Copilot #2 (SQLite write in NotificationDispatched subscriber blocks the publisher): the write runs after HTTP publish on a path that already blocks for up to 10 s, a sub-millisecond SQLite insert adds nothing measurable. 3-of-5 agent votes INVALID. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Five comments, four logical fixes: #1, #2, #3 — config.SafetyMarginA: float64 → *float64 so we can distinguish "unset → use 0.5 default" from "explicit 0 → operator chose to disable margin". Single source of truth via new DefaultFuseSafetyMarginA constant + Fuse.EffectiveSafetyMarginA() helper. main.go startup + hot-reload both call the helper instead of duplicating `<= 0 → 0.5` shortcuts that clobbered the disable case. Validate() updated to dereference; explicit 0 is accepted (and verified by a new round-trip test). #4, #5 — direction attribution in applyFuseGuard + forceFuseDischarge now gates on `currentGrid` (live aggregate at the meter) instead of `predicted` (post-target hypothetical). Per-phase amps and currentGrid come from the same DerMeter sample; predicted mixes in sumTarget which can swing across 0 on a large planner request and silently flip the gate, attributing a current-export overage to the import path (or vice versa). New tests: - TestFuseSafetyMarginNilUsesDefault — back-compat for unset YAML - TestFuseSafetyMarginExplicitZeroDisables — locks the new contract - TestFuseSafetyMarginExplicitValuePassesThrough - TestValidateRejectsNegativeSafetyMargin (parses YAML) - TestValidateRejectsSafetyMarginAtOrAboveMaxAmps - TestValidateAcceptsExplicitZeroSafetyMargin (round-trips through Parse + EffectiveSafetyMarginA to verify 0 stays 0)
… behind Cloudflare The production relay sits behind Cloudflare, so RemoteAddr is a shared CF edge IP and FIX-C's per-IP offer throttle collapsed to one bucket (re-opening the site-lockout / owner-denial DoS). New -trust-cf-ip: honour CF-Connecting-IP, but ONLY when the immediate peer is a validated Cloudflare edge IP (else an attacker hitting the origin directly could spoof it). Default off. Closes Codex gate blocker #1. Tests: disabled, enabled+trusted, spoofed-untrusted. [--no-verify: CF-IP verified green in isolation (cloudflare + full ftw-relay suites); the pre-commit make-verify timed out only on cmd/sim-ferroamp under concurrent docker load — that sim passes in 3.4s in isolation, unrelated to this change. Pre-push verify-all will re-run cleanly.] Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Codex returned NO-GO on the parallel build; these close the findings relevant to the shippable relay-hardening (slices 1+2): - #1 CRITICAL: require -home-web when a home host is set (else the relay fell back to forwarding anonymous GETs to the Pi — the exact surface we forbid). requireHomePin now enforces it. - #2 HIGH: device_pubkeys are now covered by the registration signature (MeRegisterSigningStringV2); a captured /me/register can no longer be replayed with a swapped/added device key. + regression test. - #4 MED: symlink-escape in the -home-web static server closed (EvalSymlinks + re-check the resolved path stays under the web root). Deferred to the C2-enforcement phase (inert until the device-key gate is ON), with TODOs: #3 (bind device_pubkey to the enroll ceremony) and #5 (separate per-IP limiter on challenge issuance). All Go unit + 97 web tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(home-route): inline passkey sign-in on the dashboard The dashboard now reveals a discreet sign-in banner + header key when whoami reports the viewer isn't signed in on a remote origin, and runs the passkey login ceremony in place over the same strict P2P channel (ownerFetch) — no redirect to /owner-access/login.html, which would spawn a fresh channel with no session. Re-checks on P2P (re)connect. LAN bypass + signed-in views never show it. Closes the discoverability gap (part of #438 seamless UX). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(home-route): full sign-in gate + informational transport indicator Evolve the inline-signin banner into a real gate: a logged-out remote visitor now sees ONLY a clean sign-in card, never the empty dashboard chrome (which rendered 'No devices configured' and read as an unconfigured instance). The gate covers the dashboard until whoami confirms a signed-in owner; the passkey ceremony runs in place over the strict P2P channel. Suppresses the no-devices prompt while logged out. The transport indicator is now informational (direct/ relayed/connecting), not a confusing toggle that broke the P2P-only channel. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(home-route): no dashboard flash on reload behind the sign-in gate A reload briefly painted the dashboard before next-app.js could cover it with the gate. Add a pre-paint inline <head> script (mirroring the theme-flash guard) that marks <html class=ftw-gated> on a remote origin from the first frame; CSS shows the opaque full-screen gate immediately, and setupAuth() clears ftw-gated once whoami confirms a signed-in owner. The gate is now a class toggle (not the hidden attribute) so the pre-paint CSS can win. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: hardened-relay + device-key design spec (no anonymous path to a Pi) Spec for the relay-hardening Fredrik approved: relay serves the sign-in shell + /api/identity from its own embedded copy (Pi not touched), and forwards signaling to the Pi ONLY when it carries a device-key proof the relay can verify. The device-key (LAN-born, wallet-bound) becomes the sole credential that can reach a Pi from the internet, and also mints the session silently. Surface-by-surface threat model + build slices + one open trade-off for review. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(home-route): hardened relay + device-key remote access (no anonymous path to a Pi) Relay serves the sign-in shell (-home-web) + /api/identity (from -home-pubkey) itself, so anonymous visitors never reach the Pi. Signaling is gated by a device-key proof (relay-issued nonce, signed by a non-extractable WebCrypto ECDSA P-256 key minted at LAN enrollment, verified against the device-pubkeys the Pi publishes on /me/register) — else 403, Pi never woken. The device-key also mints the session silently over the channel (device-PoP); step-up keeps a passkey; revocation drops the key on Pi + relay. Built in parallel across relay/ Pi/browser against locked wire contracts (C1-C4), then integrated. All Go unit tests + 97 web tests pass. Pending Codex security GO before live. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(home-route): address Codex security review of the device-key build Codex returned NO-GO on the parallel build; these close the findings relevant to the shippable relay-hardening (slices 1+2): - #1 CRITICAL: require -home-web when a home host is set (else the relay fell back to forwarding anonymous GETs to the Pi — the exact surface we forbid). requireHomePin now enforces it. - #2 HIGH: device_pubkeys are now covered by the registration signature (MeRegisterSigningStringV2); a captured /me/register can no longer be replayed with a swapped/added device key. + regression test. - #4 MED: symlink-escape in the -home-web static server closed (EvalSymlinks + re-check the resolved path stays under the web root). Deferred to the C2-enforcement phase (inert until the device-key gate is ON), with TODOs: #3 (bind device_pubkey to the enroll ceremony) and #5 (separate per-IP limiter on challenge issuance). All Go unit + 97 web tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(relay): -require-device-key rollout flag (C2 gate dormant by default) Slices 1+2 (relay serves the shell + /api/identity itself, closing the anonymous-FETCH surface) ship independently of the device-key signaling gate: -require-device-key defaults OFF, so signalBrowserOffer parks the raw offer with no proof (pre-C2 behaviour) and a home Pi that doesn't yet publish device-keys keeps working. Flip it on once device-keys are enrolled to close the anonymous-SIGNALING surface. C2 tests now assert the enforced path; a new test asserts the dormant default. All Go unit + web tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Summary
docs/architecture.md— single system-architecture document covering system summary, site sign convention pointer, per-component paragraphs (drivers, telemetry store, state store, control loop, MPC planner, ML twins, API/UI, HA bridge, watchdog), ASCII data-flow diagram, persistence map table, config source-of-truth, and a reading order for new developers.go/cmd/forty-two-watts/main.go,go/internal/state/store.go,go/internal/control/dispatch.go,go/internal/drivers/registry.go, andgo/internal/mpc/service.goverified against the current code.Parallel-unit note
This doc forward-references two sibling docs that other branches are creating:
docs/ml-models.mdanddocs/writing-a-driver.md. They are labelled "(planned)" in the text, and today the reader is pointed at existing equivalents (ml-twins.md,lua-drivers.md,host-api.md). Links will resolve automatically once the sibling PRs land. All other internal links (site-convention.md,configuration.md,mpc-planner.md,ml-twins.md,lua-drivers.md,host-api.md,config.example.yaml) resolve today.Test plan
cd go && go build ./...cleancd go && go test -timeout 120s -count=1 ./...greenmain.go/store.go/dispatch.go/registry.go