Skip to content

docs: add device identity reference - #3

Merged
frahlg merged 1 commit into
masterfrom
docs/device-identity
Apr 15, 2026
Merged

docs: add device identity reference#3
frahlg merged 1 commit into
masterfrom
docs/device-identity

Conversation

@frahlg

@frahlg frahlg commented Apr 15, 2026

Copy link
Copy Markdown
Member

Summary

  • New docs/device-identity.md covering the hardware-stable device_id
    chain (make:serial > mac: > ep:), the ARP L2 fallback, the
    devices table schema, bootstrap flow, and the battery-model
    migration.
  • Cites file + line for every mechanism so readers can jump from prose
    to code.
  • Flags current behavior (one-shot registerAllDevices at boot) rather
    than implying a periodic re-tick.

Test plan

  • cd go && go build ./...
  • cd go && go test -timeout 120s -count=1 ./...
  • All cited paths exist (devices.go, store.go, arp.go, registry.go, main.go, api.go, sungrow.lua).
  • grep /api/devices internal/api/ confirms the endpoint is registered.

Co-Authored-By: Claude Opus 4.6 (1M context) noreply@anthropic.com

Generated with Claude Code

Documents the hardware-stable device_id resolution chain
(make:serial > mac: > ep:), the ARP-based L2 lookup, the devices
table schema, the 3-second bootstrap flow from registry.Add through
registerAllDevices, and the one-shot battery_models key migration.
Cites source lines so readers can jump from prose to code.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@frahlg
frahlg merged commit be2bc39 into master Apr 15, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 2.0.1 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

erikarenhill added a commit that referenced this pull request Apr 17, 2026
Three items in the 11-comment review are still outstanding after the
earlier rebase — the rest were already addressed in b6a5c78 / 4f3ad01:

- #3 EV card keyboard accessibility (web/index.html + web/app.js):
  add role="button", tabindex="0", aria-label, and Enter/Space key
  handler so the summary card can be activated without a mouse and is
  announced correctly by assistive tech.

- #9 Hardcoded "easee" provider (web/settings.js): Connect button now
  derives the provider name from the driver's lua path (strip dirs,
  trailing "_cloud", ".lua"), falling back to "easee" when the driver
  config is missing or oddly named. Lets a second cloud driver slot in
  without touching the button.

- #5 Missing tests for path normalization (config_test.go): cover
  stripLeadingDotDot, ResolveDriverPaths (relative / absolute-in-tree /
  absolute-out-of-tree / leading-"../"), and the full Resolve →
  Unresolve → Resolve round-trip. The round-trip test is the one that
  pins down Copilot #11 — absolute paths outside baseDir must stay
  absolute so the next Resolve doesn't strip a "../" from filepath.Rel
  and silently re-anchor the driver under baseDir.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
erikarenhill added a commit that referenced this pull request Apr 20, 2026
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>
erikarenhill added a commit that referenced this pull request Apr 30, 2026
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)
frahlg added a commit that referenced this pull request Jun 5, 2026
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>
frahlg added a commit that referenced this pull request Jun 5, 2026
* 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>
@frahlg
frahlg deleted the docs/device-identity branch August 7, 2026 08:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant