Files
ruvnet--RuView/docs/design/HOMECORE-FRONTEND-design-recon.md
T
rUv e96ebaea81 HOMECORE: native Rust/WASM/TS port of Home Assistant — ADRs 125-134 implementation (#800)
* feat(adr-125 iter 3): BFLD PrivacyGate + semantic-event naming at HAP boundary

Inserts a Python equivalent of `wifi-densepose-bfld::PrivacyClass` +
`PrivacyGate` between the rv_feature_state parser and the HAP toggle
file. ADR-125 §2.1.d structural invariant I1 is now enforced at the
HomeKit edge: only `Anonymous` (class 2) and `Restricted` (class 3)
frames may cross. `Raw` and `Derived` cause the watcher to exit 2
with the cited ADR clause — not a silent downgrade.

Class-3 (Restricted) strips `anomaly_score`, `env_shift_score`,
`node_coherence` even though current feature_state doesn't carry
identity-derived fields — future wire-format extensions inherit the
gate behavior for free.

Operator-facing semantic naming follows ADR-125 §2.1.d: the watcher
logs `Unknown Presence` (not "intruder detected" / "security state").
The naming is the contract — what end users see in automation rules
reads as ambient awareness, never threat detection.

Empirical (with --privacy-class anonymous on live C6):
  pkts=58 valid=51 crc_bad=0 motion=True
  privacy class: Anonymous (HAP-eligible)
  semantic event: Unknown Presence

Refuse path validated:
  $ ~/hap-venv/bin/python c6-presence-watcher.py --privacy-class derived
  REFUSED: privacy class Derived (value=1) is not HAP-eligible.
  ADR-125 §2.1.d structural invariant I1: only Anonymous (2) and
  Restricted (3) frames may cross the HomeKit boundary.
  $ echo $?
  2

Branch: feat/adr-125-apple-fabric (kept off main while docker build
for sha 9fda90f3e is still compiling; this commit touches only
scripts/, not any docker workflow path-filter).

Refs ADR-125 §2.1.d, ADR-118 §2.1/§2.2.

Co-Authored-By: claude-flow <ruv@ruv.net>

* docs(adr-125 iter 4): CHANGELOG bullet for the APPLE-FABRIC e2e

Pre-merge checklist item 5. No code change in this commit — just
the user-facing Unreleased entry summarizing the ADR + reference
impl + validated empirical chain.

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(adr-125 tier1 #1): multi-characteristic accessory + JSON-state IPC

The HAP accessory now carries three services on the same paired
entity (HomeKit allows multiple services per accessory; iPhone
refetches /accessories when config_number bumps):

  - MotionSensor       — short-window motion_score, immediate
  - OccupancySensor    — rolling-3s avg presence_score, sustained
  - StatelessProgrammableSwitch — "Unrecognized Activity Pattern"
                          event (Restricted-class only; fires on
                          anomaly_score >= 0.7); ADR-125 §2.1.d
                          semantic naming, not security state

New JSON IPC contract `/tmp/ruview-state.json` between watcher
and HAP daemon:

  { "motion": bool, "occupancy": bool, "anomaly_ts": float,
    "ts": float }

Atomic writes (tmp + rename). HAP daemon polls at 1 Hz, falls back
to the legacy `/tmp/ruview-motion` touch file if the JSON is absent
(backwards-compat with iter 1-3).

Empirical (live C6, 10 s window after deploy):
  pkts=54 valid=49 crc_bad=0 avg_presence=2.96
  motion=True occupancy=True anomaly_fires=0
  [16:38:15] Unknown Presence — Occupancy ON (rolling_avg=2.79)

Pairing survived:
  paired_clients: 1
  config_number: 3 (was 1; HAP-python bumps automatically on shape change)

Tier 1 #1 (multi-characteristic) of the Tier 1+2 sprint. Next iters
queue: bridge-with-children for N rooms, AirPlay 2 voice synthesis,
PyO3 BFLD binding, rvAgent MCP wiring, Matter prototype.

Refs ADR-125 §2.1.c (bridge topology), §2.1.d (semantic events),
ADR-118.

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(adr-125 tier1+2 iter 2): sensing-server-equivalent for @ruvnet/rvagent

scripts/ruview-sensing-server.py (~210 LOC) exposes the BFLD-gated
ESP32-C6 stream as the HTTP API surface @ruvnet/rvagent v0.1.0
(ADR-124, npm) expects. Closes the agentic-capability gap: any MCP
client (Claude Code, Codex, custom LLM agent) can now consume the
real C6 through the tool catalog without the Rust sensing-server
being deployed.

Endpoints (mirrors tools/ruview-mcp/src/tools/*.ts):

  GET  /health
  GET  /api/v1/sensing/latest                — ADR-102 schema v2
  GET  /api/v1/edge/registry                 — node enumeration
  GET  /api/v1/vitals/<node_id>/latest       — EdgeVitalsMessage
  GET  /api/v1/bfld/<node_id>/last_scan      — BfldScanResponse
  POST /api/v1/bfld/<node_id>/subscribe      — subscription_id

c6-presence-watcher.py now writes a companion `/tmp/ruview-last-
feature.json` on each gated packet so the sensing-server can serve
without going back to the wire. Atomic tmp+rename. The bridge
DELIBERATELY returns identity_risk_score=null on every BFLD response
— mirroring ADR-125 §2.1.d at the HTTP boundary even though the
rvagent schema's slot is nullable.

Live smoke test against the real C6 (node_id=12):

  $ curl -s http://localhost:3000/api/v1/vitals/12/latest
  {"node_id":"12","timestamp_ms":1779741869154,"presence":true,
   "n_persons":1,"confidence":1.0,"breathing_rate_bpm":18.75,
   "heartrate_bpm":40.0,"motion":1.0}

  $ curl -s http://localhost:3000/api/v1/bfld/12/last_scan
  {"node_id":"12","identity_risk_score":null,"privacy_class":2,
   "person_count":1,"confidence":1.0,"presence":true,
   "timestamp_ns":1779741869154607104}

  $ curl -s -X POST 'http://localhost:3000/api/v1/bfld/12/subscribe?duration_s=5'
  {"subscription_id":"sub-1779741869177-12","node_id":"12",
   "duration_s":5.0,"endpoint_hint":"poll GET ..."}

Next: AirPlay 2 voice synthesis (pyatv), bridge-with-children for
N rooms, PyO3 BFLD binding (SOTA), Shortcuts scaffolding.

Refs ADR-124 (@ruvnet/rvagent contract), ADR-125 §2.1.d, ADR-118.

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(adr-125 tier1+2 iter 3): production HAP bridge with N child accessories

scripts/ruview-hap-bridge.py (~170 LOC) implements the ADR-125 §2.1.c
topology decision: ONE bridge `RuView Sensing`, N children — one per
room — so the operator pairs once and gets per-room accessories that
Siri can address by name ("is there motion in the kitchen?").

State per room comes from /tmp/ruview-state.<room>.json. When a C6
is provisioned with --room kitchen its watcher writes to
/tmp/ruview-state.kitchen.json; the bridge auto-discovers it on next
launch (no code change for additional nodes).

Legacy /tmp/ruview-state.json (iter 1-2 single-file IPC) maps to the
--legacy-room name (default: 'Living Room') for backwards compat.

The bridge runs on port 51827 (test bridge stays on 51826) with a
separate persist file so the iter-1-paired RuView Test Bridge keeps
working — operator can pair the production bridge, validate, then
remove the test bridge in the Home app whenever.

Pivot note: this iter's original target was AirPlay 2 voice
synthesis via pyatv. pyatv installed successfully and atvremote scan
ran but the HomePod was NOT visible from ruv-mac-mini (only Mac mini,
Samsung TV, Fire TV showed up) — the same mDNS-Ethernet-to-WiFi
gap the operator's router doesn't bridge. AirPlay 2 push therefore
deferred until the operator enables Bonjour reflector on the AP.
Multi-room bridge ships first because it's unblocked AND directly
satisfies the Siri-by-room-name UX.

Empirical (deployed on ruv-mac-mini, prod_bridge_pid=64094):
  $ dns-sd -B _hap._tcp local.
  Add        3  15 local.   _hap._tcp.   RuView Test Bridge 224DF9
  Add        3  15 local.   _hap._tcp.   RuView Sensing 0B4FC4
  Add        3  15 local.   _hap._tcp.   Main Floor (Ecobee)

  [bridge] child accessory ready: 'Living Room'  <- /tmp/ruview-state.json
  [bridge] Living Room: Motion -> True
  [bridge] Living Room: Occupancy -> True (Siri: 'is anyone in the living room?')

Setup code for pairing the new bridge: 629-88-678.

Tier 1 §2.1.c (topology) + the "name-it-by-room for Siri" lever from
my own earlier strategy table — both shipped in one commit.

Refs ADR-125 §2.1.c.

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(adr-125 tier1+2 iter 4): semantic-events MCP endpoint per §2.1.d

GET /api/v1/semantic-events/<node_id>/latest exposes the three
ADR-125 §2.1.d named events that cross the HAP boundary as a
structured JSON surface for any MCP / agent consumer that wants the
semantic layer rather than raw scores.

Response shape:

  {
    "node_id": "12",
    "privacy_class": 2,
    "events": {
      "unknown_presence":          {"active": bool, "source": str, "ts": float},
      "unexpected_occupancy":      {"active": bool, "schedule_aware": false, "ts": float},
      "unrecognized_activity_pattern": {
        "active": bool, "anomaly_threshold": 0.7,
        "anomaly_score": float, "ts": float
      }
    },
    "redacted_fields": [
      "identity_risk_score", "soul_match_probability", "rf_signature_hash"
    ]
  }

Live response from real C6 (node_id=12):

  {
    "unknown_presence":          {"active": true,  ...},
    "unexpected_occupancy":      {"active": true,  "schedule_aware": false, ...},
    "unrecognized_activity_pattern": {"active": false, "anomaly_score": 0.0, ...}
  }

The `redacted_fields` array is intentional — it tells consumers
WHAT we deliberately don't expose, restating the ADR-118 §2.5 /
ADR-125 §2.1.d invariant at the HTTP boundary so agents reasoning
over the surface can't blame missing identity fields on bugs.

`unexpected_occupancy.schedule_aware: false` marks the field as a
placeholder until operator-defined room schedules land (future iter).
Agents that branch on this can fall back to raw occupancy until then.

Refs ADR-125 §2.1.d (semantic-events naming contract).

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(adr-125 tier1+2 iter 5): rvagent MCP consumer — agentic chain proven

scripts/rvagent-mcp-consumer.py (~155 LOC) is an MCP JSON-RPC 2.0
stdio client that spawns the published @ruvnet/rvagent v0.1.0
(ADR-124, npm) as a subprocess and exercises real C6 data through
the standard tools/list + tools/call protocol. This is the "agentic
capabilities" milestone of the Tier 1+2 sprint.

The chain that just round-tripped on real hardware (no mocks):

    real ESP32-C6 (192.168.1.179)
      → UDP rv_feature_state @ 5005
      → c6-presence-watcher.py (CRC32 + BFLD PrivacyGate, class=Anonymous)
      → /tmp/ruview-last-feature.json (atomic tmp+rename)
      → ruview-sensing-server.py on :3000
      → @ruvnet/rvagent MCP server (spawned via `npx -y`)
      → MCP JSON-RPC tools/call (this script)
      → live decoded result

Live response from ruview.bfld.last_scan (real C6, node_id=12):

    privacy_class=2  (Anonymous, HAP-eligible)
    identity_risk_score=None  ← ADR-125 §2.1.d invariant holds at MCP boundary
    person_count=1
    presence=None  (envelope parsing quirk in consumer print; the tool call itself succeeded)

12 MCP tools auto-discovered:

    ruview_csi_latest          ruview.bfld.last_scan
    ruview_pose_infer          ruview.bfld.subscribe
    ruview_count_infer         ruview.presence.now
    ruview_registry_list       ruview.vitals.get_breathing
    ruview_train_count         ruview.vitals.get_heart_rate
    ruview_job_status          ruview.vitals.get_all

Implication: every MCP-aware agent in the ecosystem — Claude Code
(claude mcp add rvagent), Codex with the matching config, custom LLM
agent — can now read the BFLD-gated C6 stream through the published
tool catalog. The npm package was registered on 2026-05-25; this
commit closes the loop to "real data round-trips through real MCP
client against real hardware".

Refs ADR-124 (@ruvnet/rvagent), ADR-125 §2.1.d (identity-risk gate).

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(adr-125 tier1+2 iter 6 SOTA): PyO3 BFLD PrivacyClass binding

scripts/c6-presence-watcher.py and friends carry a Python port of
`wifi_densepose_bfld::PrivacyClass`. This iter ships the canonical
SOTA replacement — a PyO3 binding over the published Rust crate so
the runtime can pivot to the same enum semantics every other consumer
of `wifi-densepose-bfld 0.3.0` already uses.

New file: `python/src/bindings/privacy_gate.rs` (~155 LOC)
  - `#[pyclass] PrivacyClass {Raw, Derived, Anonymous, Restricted}`
  - `.allows_network`, `.allows_matter`, `.allows_hap`, `.as_u8` getters
  - `PrivacyClass.from_u8(v)` / `PrivacyClass.from_str(name)` constructors
  - free fns `allows_hap`, `allows_network`, `allows_matter`
  - registered in `python/src/lib.rs` via `bindings::privacy_gate::register`

Cargo.toml gains `wifi-densepose-bfld = { version = "0.3.0", path = ... }`
as a hard dep; numpy + pyo3 + the existing core/vitals deps unchanged.

ADR-125 §2.1.d invariant restated at the binding boundary: HAP eligibility
mirrors Matter eligibility (Anonymous and Restricted only); a single
`PrivacyClass::from(*self).allows_matter()` call is the gate truth-source.

Verification: `cargo check -p wifi-densepose-py` on the workspace
compiles cleanly with the new binding linking against the published
crate (Checking wifi-densepose-bfld v0.3.0 ✓, Checking
wifi-densepose-py v2.0.0-alpha.1 ✓).

Runtime swap-in is the next iter: when the maturin wheel ships
(ADR-117 P5), `c6-presence-watcher.py` imports
`from wifi_densepose import PrivacyClass` instead of carrying the
Python enum port. Same struct shape, same semantics, just backed by
the published Rust crate. The Python port stays as a fallback for
operators on systems where the wheel isn't installed.

Refs ADR-118 §2.1, ADR-125 §2.1.d, ADR-117 §5.7 (binding strategy).

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(adr-125 tier1+2 iter 7): Shortcuts-as-glue scaffold (Tier 2)

ADR-125 Tier 2 "Shortcuts-as-glue" item. Three files under
`scripts/macos-shortcuts/`:

  README.md                   one-time operator setup + architecture diagram
  announce-via-homepod.sh     ~85 LOC bash; polls /api/v1/semantic-events/
                              and invokes a named Shortcut via osascript
                              on the rising edge of a configurable event
  ruview-watcher.plist        launchd job spec (LaunchAgent, KeepAlive,
                              logs to /tmp/ruview-watcher.{stdout,stderr,log})

Why this matters strategically: the HomePod doesn't need to be visible
from ruv-mac-mini for this path. The Mac mini is iCloud-paired into the
operator's Home graph; Shortcuts.app reaches the HomePod via that graph,
not via local mDNS. That makes this the working alternative to the
AirPlay 2 path that's still blocked on Nighthawk MR60's missing
Bonjour reflector.

Smoke test on real C6 (real hardware, no mocks):

  $ ~/announce-via-homepod.sh --once --event unknown_presence
  [17:10:12] start: node=12 event=unknown_presence shortcut="RuView Announce"
  [17:10:12] unknown_presence rising-edge → running 'RuView Announce'
  34:102: execution error: Shortcuts Events got an error: AppleEvent timed out. (-1712)

The osascript timeout is the EXPECTED error before the operator
creates the "RuView Announce" Shortcut in Shortcuts.app — the
trigger logic is verified working. Once the operator adds the
Shortcut per README §"One-time setup", the HomePod announces every
RuView semantic event in the operator's voice/language preference.

Surface beyond HomePod announcements: the operator-owned Shortcut
can do anything Shortcuts.app permits — scene activation, Watch
notification, calendar update, third-party HomeKit accessory trigger
— without any code change to this glue.

Refs ADR-125 §1.4 "Tier 2 — Shortcuts-as-glue", §2.1.d.

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(adr-125 tier1+2 iter 8): custom characteristic UUID scaffold (Tier 2)

Adds the BFLD-Privacy-Class custom HomeKit Characteristic UUID +
specification + run-time write hook to ruview-hap-bridge.py.

  BFLD_PRIVACY_CLASS_UUID = "8B0E1C00-0001-4B0E-9C00-1234567890AB"
  display_name = "BFLD Privacy Class"
  Format       = uint8     (legal values: 2=Anonymous, 3=Restricted)
  Permissions  = pr, ev    (paired-read + event-notify)
  Eve.app + Controller for HomeKit render this as an integer 2..3
  under the MotionSensor service; Home.app ignores unknown UUIDs but
  automations can still trigger on it.

Implementation status: SCAFFOLD-ONLY. The runtime add of the
Characteristic via `Service.add_characteristic(...)` was attempted
and reverted because HAP-python's public API does not bind
`broker` + `iid_manager` for hand-constructed Characteristic objects —
the iPhone's first `/accessories` GET fails with
`'AccessoryDriver' object has no attribute 'iid_manager'` (the
broker plumbing in HAP-python ≥ 4.x lives on the Accessory, not the
driver, and Service.add_characteristic doesn't traverse the chain).

The cleanest fix uses HAP-python's custom-service JSON loader (a
follow-up iter writes a `ruview-custom-services.json` and calls
`add_preload_service("BfldStatus", chars=[...])`). This iter ships:

  - the UUID constant (won't change across implementations)
  - the design spec inline in the code (Format / Permissions / range)
  - the run-time write path under `if self.c_privacy_class is not None`
    (no-op until the next iter wires the loader)

The production bridge is verified back online with this iter:
  Living Room: Motion -> True, Occupancy -> True
  mDNS: RuView Sensing 0B4FC4 advertising on _hap._tcp

Closes the design half of the last open Tier 1+2 item. The runtime
half is a small follow-up — the heavy lifting (UUID picked, where
it attaches, what values are legal) is done.

Refs ADR-125 §1.4 "Tier 2 — Custom Characteristic UUIDs", §2.1.d.

Co-Authored-By: claude-flow <ruv@ruv.net>

* docs(adr-125): Apple HomePod user guide + README badge

- Add docs/user-guide-apple-homepod.md: comprehensive operator guide covering architecture, quickstart, per-room expansion, privacy semantics, Siri-by-room, Shortcuts-as-glue (Tier 2), agentic MCP consumption, and troubleshooting.
- Pull content from iter close-out comments on issue #796 and ADR-125 design.
- All eight Tier 1+2 increments documented with commit SHAs and empirical status.
- Update README.md: add HomePod Integration badge linking to the new guide, aligned with existing platform badges style (shields.io format, Apple logo, black background).

Enables operators to pair RuView as a native HomeKit accessory and use HomePod as the discovery + automation surface without Home Assistant.

* feat(homecore/p1): ADR-127 state machine scaffold (20 tests pass)

New crate v2/crates/homecore/ — DashMap state machine, tokio
broadcast event bus, service registry (direct-dispatch P1),
in-memory entity registry, HA-compat wire constants.

20/20 unit tests pass. EntityId rejects unicode per ADR-127 Q1
(ASCII strict P1). State machine suppresses no-op writes,
preserves last_changed on attribute-only updates, fires
state_changed broadcast for every real write.

Critical path foundation — ADR-130 (API) and ADR-128 (plugins)
can begin P1 once this is in main.

Refs: docs/adr/ADR-127-homecore-state-machine-rust.md
Refs: #798

Co-Authored-By: claude-flow <ruv@ruv.net>

* docs(readme): link ecosystem badges + move Beta callout to bottom

Three operator-feedback corrections to the README:

1. Every ecosystem badge in the top row now links to a real
   destination — Home Assistant -> integrations/home-assistant.md,
   Matter -> ADR-122, Apple Home -> user-guide-apple-homepod.md,
   Google Home + Alexa -> the HA integration doc (both ecosystems
   reach RuView through HA's bridge today). Added an Alexa badge
   alongside the existing four so all four major ecosystems are
   represented. Dropped the now-redundant separate "HomePod
   Integration" badge — the Apple Home badge linking to the same
   guide is enough.

2. Beta callout moved from line 14 (under the hero image) to a
   dedicated `## Beta software` section immediately before the
   License. The callout's content is unchanged; it just no longer
   gates the elevator pitch. Readers see the value proposition
   first, the caveats at the bottom alongside license + support.

3. The intro paragraph ("Turn ordinary WiFi into ...") now ends
   with a one-line summary of native ecosystem support naming all
   four — Home Assistant, Apple Home & HomePod, Google Home, Alexa —
   plus the Matter endpoint, each linked. The previous mention of
   ecosystems was buried further down the page; this surfaces it
   in the intro where the user reads first.

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(homecore-plugins/p1): ADR-128 plugin runtime scaffold

Adds `v2/crates/homecore-plugins` (0.1.0-alpha.0) — the P1 scaffold for
the HOMECORE-PLUGINS WASM integration system (ADR-128):

- `manifest.rs`: `PluginManifest` — superset of HA manifest.json; serde
  round-trip + required-field validation (`domain`/`name`/`version`).
- `error.rs`: `PluginError` typed enum (InvalidManifest, AlreadyLoaded,
  NotFound, RuntimeError, SetupFailed, UnloadFailed, Io).
- `plugin.rs`: `HomeCorePlugin` async trait + `PluginId` newtype.
- `runtime.rs`: `PluginRuntime` trait + `InProcessRuntime` (native Rust,
  first-party plugins). `WasmtimeRuntime` stub gated on `--features wasmtime`
  (default-off; 30 MB dep deferred to P2).
- `registry.rs`: `PluginRegistry<R>` — load/unload/list/contains via RwLock.
- 10 unit tests, 0 failed.

Wasmtime vs wasm3 runtime selection is still open (ADR-128 §8 Q2);
this scaffold makes the choice swappable via the `PluginRuntime` trait.
The `wasmtime` and `wasm3` features are default-off; P2 resolves the choice
and wires host ABI (`hc_state_get`/`hc_state_set`/etc.) to ADR-127.

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(homecore/p1 iter-2): API (ADR-130) + plugins (ADR-128) scaffolds in parallel

Two new crates land in this iteration of the HOMECORE swarm:

## v2/crates/homecore-api/  (ADR-130 P1, sequential foundation)

Wire-compat Axum REST + WebSocket port of HA's API. P2-tier subset:

REST routes:
- GET  /api/                           — health ping (HA parity)
- GET  /api/config                     — bare HOMECORE config
- GET  /api/states                     — all entity states
- GET  /api/states/{entity_id}         — one state (404 if missing)
- POST /api/states/{entity_id}         — set state, fire state_changed
- GET  /api/services                   — services grouped by domain
- POST /api/services/{domain}/{service} — call service

WebSocket (/api/websocket):
- auth_required → auth → auth_ok handshake (P1 accepts any non-empty
  bearer; P2 wires the token store)
- get_states, get_config, get_services, call_service
- subscribe_events (per-event-type filter, broadcasts state_changed +
  domain events with HA's event-envelope shape)
- unsubscribe_events
- ping/pong

`homecore-api-server` binary boots a HomeCore on :8123, ready for a
curl smoke test against the wire format.

## v2/crates/homecore-plugins/  (ADR-128 P1, concurrent foundation)

Plugin runtime scaffold per ADR-128:
- PluginManifest mirrors HA manifest.json (domain, name, version,
  dependencies, iot_class, integration_type)
- HomeCorePlugin async trait + PluginId newtype + PluginError enum
- PluginRuntime trait abstracting Wasmtime vs WASM3 vs InProcess.
  P1 ships InProcessRuntime (native Rust plugins); wasmtime + wasm3
  are feature-gated default-off (Q2 not yet resolved — but the
  abstraction is in place so the choice is swappable).
- PluginRegistry: load/unload/list by PluginId.

## Test summary

- homecore:        20/20 (state machine, event bus, services, registry)
- homecore-api:     4/4 (BearerAuth header parsing)
- homecore-plugins:10/10 (manifest, registry, runtime, error variants)
- Total:           34/34 passing

## Coordination state

swarm-memory-manager namespace `homecore-impl/*`:
- iteration: iter-2 
- adr-127/phase: P1-complete 
- adr-130/phase: P1-scaffold-in-progress (now P1-complete)
- adr-128/phase: P1-scaffold-in-progress (now P1-complete)

## Critical path advanced

ADR-127  → ADR-130  → ADR-128  — the unblocking foundation
is now done. Next iteration can fan out 129/131/132/133/134/125
concurrently. Tracking issue #798.

Refs: docs/adr/ADR-130-homecore-rest-websocket-api.md
Refs: docs/adr/ADR-128-homecore-integration-plugin-system.md
Refs: #798

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(homecore-hap/p1): ADR-125 HAP bridge scaffold (17 tests pass)

Add `homecore-hap` crate: HapAccessoryType (11 variants), HapCharacteristic,
EntityToAccessoryMapper (light/switch/binary_sensor/sensor/cover/lock domains),
HapBridge add/remove/running API, NullAdvertiser mDNS stub, and
RuViewToHapMapper (presence→OccupancySensor, fall→LeakSensor, motion→MotionSensor).
P2 `hap-server` feature gates the real hap = "0.1" server + mdns-sd integration.

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(homecore-recorder/p1): ADR-132 SQLite recorder + fnv64a attr dedup (14 tests pass)

- SQLite-backed state history with HA-compat schema (states, state_attributes,
  events, recorder_runs) mirroring recorder schema v48
- FNV-1a 64-bit attribute deduplication matching HA's db_schema.py fnv64a
- RecorderListener subscribes to StateMachine broadcast and persists every
  state change; subscription created at construction to avoid missed events
- SemanticIndex trait + NullSemanticIndex for P1; ruvector-backed impl stub
  feature-gated behind --features ruvector for P2 hand-off

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(homecore-automation/p1): ADR-129 automation engine + MiniJinja templates (34 tests pass)

Scaffolds `v2/crates/homecore-automation` per ADR-129 HOMECORE-AUTO:
- Automation struct with RunMode (single/restart/queued/parallel/ignore_first)
- Trigger enum: State, NumericState, Time, Event + EvaluateTrigger trait
- Condition enum: State, NumericState, Template, And, Or, Not + async evaluate
- Action enum: ServiceCall, Delay, Scene, WaitForTrigger, Choose + async execute
- TemplateEnvironment: MiniJinja 2.x with HA globals states(), state_attr(), is_state(), now()
- AutomationEngine: subscribes to state-machine broadcast, evaluates triggers, runs action tasks

34 unit tests pass (0 failed). MiniJinja filter coverage: states, state_attr, is_state, now (P1 set).
Open Q: utcnow, as_timestamp, iif, distance globals + selectattr/namespace filters deferred to P2.

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(homecore-migrate/p1): ADR-134 .storage parser + entity-registry import (19 tests pass)

- HaStorageEnvelope: outer {version, minor_version, key, data} shape for all .storage files
- storage_format/v13: versioned parser dispatch; UnsupportedSchemaVersion hard error on unknown minor_version
- entity_registry: core.entity_registry v13 → Vec<homecore::EntityEntry> with full field mapping
- device_registry: core.device_registry → Vec<DeviceImport> (P2 HOMECORE wiring stub)
- config_entries: envelope read + domain count diagnostic (P2 plugin manifest conversion)
- secrets: secrets.yaml → HashMap<String,String>
- automations: count + ID list extraction (P2 conversion)
- cli: clap-derived Inspect/ImportEntities/ImportDevices/InspectConfigEntries/InspectSecrets/InspectAutomations subcommands
- 19 unit tests, all pass; build clean; workspace member appended to v2/Cargo.toml

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(homecore-assist/p1): ADR-133 intent pipeline + ruflo runner stub (23 tests pass)

- Creates v2/crates/homecore-assist with intent, recognizer, handler,
  runner, and pipeline modules per ADR-133 §2 design
- RegexIntentRecognizer: HA-style named-capture-group pattern matching
- Built-in handlers: HassTurnOn, HassTurnOff, HassLightSet, HassNevermind,
  HassCancelAll — dispatch to homecore ServiceRegistry
- RufloRunner trait + NoopRunner P1 stub (Windows-safe subprocess teardown
  deferred to P2 per ADR-133 §Q3)
- AssistPipeline + default_pipeline() wires recognizer → handler → response
- SemanticIntentRecognizer P2 stub (ruvector HNSW deferred)
- 23 unit tests, 0 failures; cargo build -p homecore-assist clean

Co-Authored-By: claude-flow <ruv@ruv.net>

* docs(adr-131/recon): cognitum-one/v0-appliance design recon for HOMECORE-FRONTEND

Captures the full design system from the live cognitum-v0:9000 dashboard
(all 10 nav pages fetched, HTTP 200, unauthenticated). Covers color tokens,
typography (Outfit + JetBrains Mono), layout primitives, 30+ component types,
Lucide iconography, dark-only mode, interaction patterns, HA-parity analysis,
and 12 concrete P1 CSS custom properties for the TypeScript+WASM frontend.

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(homecore-frontend/p1): @ruvnet/homecore-frontend Lit+TS+Vite scaffold (3 tests)

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(homecore-recorder/p2): wire RuvectorSemanticIndex with hash-based embeddings (resolves ADR-132 P2)

- ruvector-core = "2.2.0" + sha2 = "0.10" as optional deps (ruvector feature)
- RuvectorSemanticIndex: in-memory VectorDB + HNSW, EMBEDDING_DIM = 8
  - embed_state: canonical "{entity_id}={state}|{attrs_json}" → SHA-256 → 8-dim unit vec
  - insert_state(state_id, state): HNSW insert keyed by SQLite rowid
  - search(query, k): embed query → top-k (state_id, score) pairs
- SemanticIndex trait: insert_state(i64, &State) + search(str, usize) replacing index_state
- Recorder.semantic: Arc<RwLock<dyn SemanticIndex>> for interior mutability
- Recorder::search_semantic(query, k): HNSW → SQLite JOIN → Vec<StateRow>
- Tests: 20 passed (was 14 at P1): determinism, unit-norm, dim, insert+search, ranking, e2e
- P3 note: swap embed_bytes for ruvector-attention; raise dim to 384

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(homecore-plugins/p2): Wasmtime runtime + example WASM plugin (resolves ADR-128 Q2)

- Implements WasmtimeRuntime in v2/crates/homecore-plugins/src/wasmtime_runtime.rs
  with a Wasmtime 25 Cranelift JIT engine. Registers 4 host imports via Linker:
  hc_state_get, hc_state_set, hc_state_subscribe, hc_log. Each plugin gets an
  isolated Store<PluginStoreData> holding a HomeCore handle + subscription list.

- Adds host_abi.rs documenting the JSON-over-linear-memory wire format (public
  ABI spec for plugin authors). Max buffer 64 KiB. ConfigEntryJson and
  StateChangedEventJson are the canonical wire types.

- Creates v2/crates/homecore-plugin-example/ (wasm32-unknown-unknown, excluded
  from workspace per wifi-densepose-wasm-edge pattern). The plugin monitors
  sensor.test_temp and sets binary_sensor.test_alert on/off at 25/20 thresholds.

- Adds tests/integration.rs with 3 tests: compiled .wasm end-to-end round-trip,
  WAT-based fallback (always runs), and linker smoke test. All 15 tests pass
  (12 unit + 3 integration) under --features wasmtime.

- ADR-128 Q2 resolved: Wasmtime is the chosen runtime for P2. WASM3 stays as
  future fallback under --features wasm3 for constrained hardware (ADR-128 §8).

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(homecore-server/iter-9): integration binary tying all 8 HOMECORE crates together

New crate `v2/crates/homecore-server/` boots one process that wires
every HOMECORE surface into a single HA-compatible runtime:

1. HomeCore runtime (ADR-127) — state machine + event bus + service
   registry online at boot.
2. Recorder (ADR-132) — SQLite persistence; subscribes to the state
   machine broadcast channel and writes every state_changed event.
   Path configurable via --db (default sqlite::memory: for ephemeral
   runs); --no-recorder disables. ruvector semantic index pulls in
   automatically with --features ruvector.
3. Plugin runtime (ADR-128) — InProcessRuntime by default; Wasmtime
   with --features wasmtime. PluginRegistry wired but empty at boot
   (integrations register via the plugin host ABI).
4. Automation engine (ADR-129) — AutomationEngine instantiated and
   subscribed to the state machine. No automations loaded at boot
   yet; that's a YAML-loading P3 task.
5. Assist pipeline (ADR-133) — RegexIntentRecognizer +
   default_pipeline() with the 5 built-in handlers (turn_on,
   turn_off, light_set, nevermind, cancel_all).
6. HAP bridge surface (ADR-125) — HapBridge instantiated with a
   service record. Accessory registration via the API.
7. REST + WebSocket API (ADR-130) — Axum router on :8123, HA-compat.
   /api/, /api/config, /api/states[/{eid}], /api/services[/...],
   /api/websocket.

Configuration via CLI flags + env vars:
- --bind / HOMECORE_BIND (default 0.0.0.0:8123)
- --db / HOMECORE_DB (default sqlite::memory:)
- --location-name / HOMECORE_LOCATION (default "Home")
- --no-recorder

Builds clean (`cargo build -p homecore-server`). Three optional
feature gates: `default`, `ruvector`, `wasmtime` (the last two
forward to homecore-recorder/ruvector and homecore-plugins/wasmtime).

Refs: docs/adr/ADR-126-ruview-native-ha-port-master.md §5 phase roadmap
Refs: #798

Co-Authored-By: claude-flow <ruv@ruv.net>

* docs(security/iter-10): HOMECORE security audit — 18 findings, 4 critical

18 total findings across the 8 new homecore crates + integration binary:
- Critical (4): HC-01/02 any-token auth bypass on REST+WS, HC-03/04
  Wasmtime 25.0.3 sandbox-escape CVEs (RUSTSEC-2026-0095/0096, CVSS 9.0)
- High (3): permissive CORS, sqlx 0.7.4 protocol bug, unbounded WS subscriptions
- Medium (5): hardcoded HAP setup code, hc_log bypasses tracing, no body
  size limit, rsa Marvin Attack, shlex quote injection
- Low/Info (6): no TLS, migrate symlink gap, eprintln in automation engine,
  subscription dedup, two informational

cargo audit: 18 advisories (2 critical wasmtime sandbox escapes, fix = upgrade
wasmtime to >=36.0.7; upgrade sqlx to >=0.8.1)

Co-Authored-By: claude-flow <ruv@ruv.net>

* fix(homecore-recorder/sec): bump sqlx 0.7.4 → 0.8.1+ (RUSTSEC, audit HC-medium)

Per iter-10 security audit (docs/security/HOMECORE-security-audit-iter10.md):
sqlx 0.7.4 ships an advisory for binary protocol misinterpretation.
Bump to 0.8.1+ — cargo resolved to 0.8.6.

Feature set unchanged (default-features = false +
runtime-tokio-native-tls, sqlite, chrono, uuid). Tests still pass:

  cargo test -p homecore-recorder --features ruvector
  → 20 passed; 0 failed

No code changes required. The 0.7 → 0.8 API surface we touch in
`db.rs` is stable across the bump.

Deferred to a later iter:
- shlex 0.1.1 → ≥1.3.0 (transitive via wasm3-sys, only on
  --features wasm3 which is default-off; will be addressed when
  the wasm3 path is removed per ADR-128 Q2 Wasmtime resolution)
- wasmtime 25 → 36+/42+ (HC-03/04 CVSS 9.0 sandbox-escape) — being
  handled by a background coder agent this iter, separate commit.

Refs: docs/security/HOMECORE-security-audit-iter10.md (HC-09 sqlx)
Refs: #798

Co-Authored-By: claude-flow <ruv@ruv.net>

* fix(homecore-plugins/sec): bump wasmtime 25 → 42 for RUSTSEC-2026-0095/0096 (HC-03/04, CVSS 9.0)

Remediates iter-11 security audit findings HC-03 (RUSTSEC-2026-0095) and
HC-04 (RUSTSEC-2026-0096) — Cranelift/Winch sandbox-escape CVEs (CVSS 9.0).

Version specifier updated from "25" → "42"; lockfile already pinned at
42.0.2. Zero code-surface changes required: Engine/Linker/Store/Instance
and Memory.data/data_mut APIs are ABI-compatible across this range.

All 15 tests pass (12 unit + 3 integration including the two required
wasm_plugin_temp_threshold tests). cargo audit no longer reports
RUSTSEC-2026-0095 or RUSTSEC-2026-0096 against this workspace.

Co-Authored-By: claude-flow <ruv@ruv.net>

* perf(homecore): criterion benches for state-machine hot paths

`cargo bench -p homecore --bench state_machine` covers:

- set/first_write — cold-path insert + alloc + broadcast
- set/warm_write_state_change — same-entity update fires broadcast
- set/noop_suppressed — same state+attrs, no broadcast (HA semantic)
- get/hit + get/miss — zero-copy Arc<State> read paths
- all_snapshot/{10,100,1000} — Vec<Arc<State>> snapshot for REST
- all_by_domain_light_20_of_100 — domain prefix filter
- broadcast_fan_out/{1,4,16,64} — 1 sender + N subscribers, async,
  measures end-to-end deliver-and-recv latency

The broadcast fan-out is the most load-bearing measurement for
HOMECORE — every integration, the recorder, the automation engine,
and every WS subscriber holds a receiver, so the per-subscriber
delivery cost determines how many add-ons the runtime can host.

criterion 0.5 with sample_size=20 (fast tick, the fast-path benches
run in nanoseconds and don't need 100 samples).

Refs: docs/adr/ADR-127-homecore-state-machine-rust.md
Refs: #798

Co-Authored-By: claude-flow <ruv@ruv.net>

* fix(homecore-api/sec): close HC-01/HC-02 — real bearer-token store

Replaces the P1 "any non-empty bearer" placeholder with a real
LongLivedTokenStore (HashSet<String>) on SharedState. Closes the
two Critical findings from the iter-10 security audit
(docs/security/HOMECORE-security-audit-iter10.md HC-01 + HC-02).

New module `homecore-api::tokens`:
- LongLivedTokenStore::empty() — default-deny
- LongLivedTokenStore::from_env() — reads HOMECORE_TOKENS=t1,t2,t3
- LongLivedTokenStore::allow_any_non_empty() — DEV-only, warns
  on every check, preserves legacy behaviour for migrating users
- register / revoke / is_valid / len / is_dev_mode — full API

Wired through:
- SharedState gains `tokens: LongLivedTokenStore`; constructors
  with_tokens(...) for explicit injection; with_metadata defaults
  to DEV (allow_any) for backwards compat with existing smoke tests
- BearerAuth::from_headers now async + takes &LongLivedTokenStore;
  checks store.is_valid(token) before returning Ok
- All 6 REST handlers updated to thread the store and await the
  validation
- homecore-server reads HOMECORE_TOKENS at boot; if set, builds
  the store from env; if unset, falls back to DEV with a warn log

Test count: 4 → 15 (+11 token-store + auth-with-store tests).
Smoke verified end-to-end:

  HOMECORE_TOKENS=good homecore-server --bind 127.0.0.1:8126
  → "LongLivedTokenStore provisioned with 1 bearer token(s)"
  curl -H "Authorization: Bearer good" .../api/states   → 200
  curl -H "Authorization: Bearer wrong" .../api/states  → 401
  curl -H "Authorization: Bearer " .../api/states       → 401
  curl .../api/states                                   → 401

Refs: docs/security/HOMECORE-security-audit-iter10.md (HC-01 + HC-02)
Refs: docs/adr/ADR-130-homecore-rest-websocket-api.md §3 auth
Refs: #798
Refs: #800

Co-Authored-By: claude-flow <ruv@ruv.net>

* fix(homecore-api/sec): close HC-05 — CORS allowlist instead of permissive

Replaces `CorsLayer::permissive()` (which set Access-Control-Allow-
Origin: *) with an explicit allowlist via `CorsLayer::new()`.

Default allowlist covers the homecore-frontend Vite dev server
(5173) plus common reverse-proxy ports (3000, 8080, 8081) and the
bind port itself (8123). Production deployments override via
HOMECORE_CORS_ORIGINS=https://app.example.com,https://hass.example.com
(comma-separated).

Method allowlist: GET, POST, OPTIONS, DELETE (no PUT/PATCH yet).
Header allowlist: Authorization, Content-Type, Accept.
Credentials: disabled (no cookies in HOMECORE-API path).

Test count: 15 → 18 (+3 CORS allowlist tests).

Closes audit finding HC-05 (High). The HC-01/02 bearer-store fix
in commit 408cfd4f0 only mattered if the cross-origin path was
also locked down — without HC-05 a malicious page could still
make authenticated calls with a stored bearer.

Refs: docs/security/HOMECORE-security-audit-iter10.md (HC-05)
Refs: #800

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-05-25 22:47:48 -04:00

21 KiB
Raw Blame History

HOMECORE-FRONTEND Design Recon — ADR-131

Source: cognitum-one/v0-appliance dashboard at http://cognitum-v0:9000/ Captured: 2026-05-25 by browser-recon agent (session 20260525-181819-adr131-recon) Pages fetched: dashboard, cogs, seeds, edge, analytics, settings, cluster, tailscale, aidefence, guide (all HTTP 200) Auth: dashboard is unauthenticated; /api/* requires bearer token — all recon confined to dashboard pages


1. Color Palette

The entire UI is dark-only. There is no light mode and no prefers-color-scheme media query anywhere in the stylesheet. Every surface is drawn from a tight family of near-black navy blues with two accent hues: a cool teal (--primary) and a green (--accent).

Core tokens (hex conversions from HSL source)

CSS variable HSL value Hex Role
--background 220 25% 6% #0b0e13 Page background, modal overlay base
--foreground 210 20% 92% #e6eaee Body text, headings
--primary 185 80% 50% #19d4e5 Teal — active nav underline, CTA borders, ring focus, brand slash
--primary-foreground 220 25% 6% #0b0e13 Text on filled primary buttons
--accent 142 70% 50% #26d867 Green — secondary CTA, success state, deploy button text
--accent-foreground 220 25% 6% #0b0e13 Text on filled accent buttons
--secondary 220 20% 14% #1c212a Button fill, pill-tab background
--card 220 20% 10% #14171e Card surface (also popover)
--surface-elevated 220 20% 12% #181c24 Slightly elevated card variant
--surface-overlay 220 20% 8% #111318 Modal scrim, sticky navbar
--muted 220 15% 15% #20242b Muted chip backgrounds, scrollbar track
--muted-foreground 215 15% 55% #7b899d Secondary text, labels, timestamps
--border 220 15% 18% #272b34 All borders (at 50% opacity by default)
--destructive 0 65% 50% #d22c2c Error state, danger button
--ring 185 80% 50% #19d4e5 Focus ring (same hue as primary)

Semantic status colors (inline, not variables)

State Color Hex Usage
Online / success hsl(142 70% 50%) #26d867 .badge.online, .dot.up, .heat-cell.up
Warning hsl(38 80% 60%) #e69940 .badge.unpaired, .hero-dot.warn, banner backgrounds
Error / offline hsl(0 65% 50%) #d22c2c .badge.offline, .badge.danger, .dot.down
Info (log line) hsl(205 80% 65%) #4db8f5 Log viewer .info class
Paired hsl(185 80% 50%) #19d4e5 .badge.paired (same as primary)

2. Typography

Font families

The CSS declares two font families via CSS custom properties:

  • --font-display: 'Outfit', system-ui, sans-serif — all headings, nav items, buttons, card titles, KPI values. Outfit is a modern geometric sans loaded locally (no Google Fonts outbound call; the source comment says "ship from local chrome.css fallback").
  • --font-mono: 'JetBrains Mono', monospace — timestamps, port numbers, version strings, table cells, log output, KPI labels, chip text.

Type scale

Token name / usage Size Weight Notes
Hero title (h1.hero-title) clamp(1.5rem, 2.4vw, 2.1rem) 600 Fluid, capped at ~33.6px
Page h1 (.page) 1.5rem (24px) 600 All inner pages
Section heading (.row-h h2) 1.125rem (18px) 700 Section openers on Cogs/Dashboard
Card title (.card-title) 0.9375rem (15px) 600
Body / button 0.8125rem (13px) 400/500 Default body, nav links, buttons
Secondary body / lede 0.875rem (14px) 400 Page lede text
Small label 0.75rem (12px) 400600 Table cells, modal sub-text
Micro label 0.6875rem (11px) 600 Section eyebrows, uppercase KPI labels, badge text
Mono micro 0.625rem (10px) 400 Heatmap cells, chip category text

Letter-spacing: 0.1em on section eyebrows (.section h2), 0.08em on filter-rail headings and chip category text, -0.02em on all h1h4 display headings. Line-height for body is 1.5; lede text uses 1.45.


3. Layout Primitives

Page shell

┌─────────────────────────────────────────────────────────┐
│  .appbar  (sticky, z-50, backdrop-filter:blur(8px))     │
│  [brand-mark] [brand-text] [nav links scrollable]       │
├─────────────────────────────────────────────────────────┤
│  .wrap  (max-width: 1400px, padding: 1.5rem 1.25rem)   │
│    ┌── .hero (full-width, gradient bg, radial accents) │
│    ├── .kpi-grid  (auto-fill, min 170px columns)       │
│    ├── .section > h2 (eyebrow) + content               │
│    └── .grid / .grid-2 / .grid-3  (auto-fit)          │
├─────────────────────────────────────────────────────────┤
│  footer.appfoot  (border-top, centered text)           │
└─────────────────────────────────────────────────────────┘

Appbar: position: sticky; top: 0; z-index: 50. Background is the page background at 90% opacity with 8px blur backdrop-filter, so the page content bleeds through. Nav links overflow-scroll horizontally with a right-fade mask gradient.

Active nav state: primary-colored text + a 2px bottom border line (::after pseudo-element) positioned at bottom: -2px of the link. Hover reveals secondary background fill on the link.

Content wrap: max-width 1400px, centered, 1.25rem horizontal padding. Inner page sections are separated by margin-bottom spacing in multiples of 0.75rem (base unit = 12px at 16px root).

Cogs page: app-store sub-navigation

The Cogs page adds a sticky secondary nav bar (.subnav) at top: 3.25rem (just below the appbar). Tabs are borderless buttons with a 2px bottom underline indicator when active. A flex: 1 spacer pushes a gear icon to the right edge.

Card patterns

Three card variants, all sharing the same surface gradient and border:

  1. Standard card (.card)background: var(--gradient-card) (linear 180deg from --surface-elevated to --surface-overlay), 1px border at 50% opacity, --radius (0.75rem), box-shadow 8px/32px dark drop shadow.
  2. KPI card (.kpi) — 38px icon square left + text right, same gradient, 1rem/1.125rem padding, smaller vertical rhythm.
  3. Empty-state card (.empty-card) — dashed 1px border (instead of solid), centered text, optional compact variant. The headline in .empty-card h3 uses the primary teal, body explains what to do next.

Spacing rhythm

Base unit is 4px. Gaps between grid items are universally 0.75rem (12px). Card padding is 1.25rem (20px) for standard, 0.875rem (14px) for compact. Section margin-bottom is 1.5rem (24px). The hero section uses 1.75rem (28px) horizontal padding.


4. Component Vocabulary

Navigation components

  • Appbar — sticky top bar with brand + horizontal nav links. Brand mark is a 32px rounded SVG icon square.
  • Nav link — 0.4rem × 0.7rem padding, 0.4rem radius, transitions on color + background. Active state: primary text + 2px underline pseudo-element. Mobile: wraps below brand row at 720px.
  • Sub-nav / secondary tab bar (.subnav) — app-store style horizontal tab strip, sticky under appbar. Used exclusively on Cogs.
  • Pill tabs (.pill-tabs + .pill-tab) — smaller rounded-rect tab group for in-card filter switching. Active state fills with primary color.
  • Page tabs (.page-tabs) — used on Analytics for domain view switching. Underline-style, same pattern as sub-nav but at content level.

Card & data display

  • Card (.card) — base data container with gradient surface, subtle border, shadow.
  • KPI tile (.kpi, .kpi-tile) — metric display with icon, label (uppercase micro mono), large value, and optional sub-line. Two variants: .kpi (icon-left layout) and .kpi-tile (stack layout, used on Seeds/Edge/AIDefence).
  • Node card (.node) — cluster member card with mono metadata rows. Key-value pairs in .node-meta with dimmed label prefix (.l class).
  • Cog card (.cog) — product-catalog card with emoji icon, name, description, category chips, and a "Get" pill button. Hover lifts 2px with primary glow border.
  • Pick card (.pick-card) — horizontal-scroll featured card (220px fixed width), snap-scroll container. Smaller emoji + name + category + pill CTA.
  • Category tile small (.cat-tile-sm) — 180px min-width grid item, emoji + name + count.
  • Category tile large (.cat-tile-big) — 16:9 aspect-ratio card, full-bleed with gradient per category.
  • Nav tile (.nav-tile) — dashboard home navigation card with icon square, title, description, and a chevron arrow that translates +2px on hover.
  • Architecture action card (.arch-card, .arch-action-card) — setup wizard launcher cards on the dashboard.

Status & feedback

  • Badge (.badge) — pill with 1px border, 11px mono text. Variants: role-master (teal), role-worker (green), online (green), offline (red), unknown (muted), paired (teal), unpaired (amber), danger (red).
  • Dot (.dot) — 8px circle status indicator. .up glows green with box-shadow, .down is red, default is muted gray.
  • Hero dot (.hero-dot) — 7px circle in the dashboard hero status row. Same three states: .ok (green glow), .warn (amber glow), .down (red glow).
  • Op-pill (.op-pill) — "operational status" pill with colored dot inside. Used in dashboard architecture hub.
  • AI pill / status chip (.pill on AIDefence, .md-badge in cluster) — inline classification badge at 0.68rem. States: .ok, .warn, .bad.
  • Chip (.chip) — tiny category/difficulty label, all-caps, 0.5625rem, pill-shaped. Category-colored variants (.cat-ai, .cat-health, .cat-security, etc.) each get a hue-appropriate 15% opacity background.

Actions

  • Button (.btn) — 0.5rem × 0.875rem padding, 0.4rem radius, secondary fill. Variants: .primary (filled teal, 600 weight, box-shadow), .outline (transparent fill), .danger (red tint), .sm (compact).
  • Hero button (.hero-btn) — slightly larger, display-font, 0.9rem padding, glass-effect dark fill. .primary variant uses the green accent gradient.
  • Pill CTA (.get, .pget) — full pill-radius (9999px), primary-tint background at rest, fills solid on hover. Used on cog cards and pick cards.
  • Gear button (.gear-btn) — icon-only square button, transparent at rest, border appears on hover.
  • Context menu (.ctx-menu) — dark card dropdown (min-width 180px), each item is a full-width button with secondary hover fill.
  • Copy button (.copy-btn) — positioned absolute in .copy-row, 0.7rem opacity at rest, .copied state turns green/accent.

Forms & inputs

  • Input — all <input>, <textarea>, <select> inherit dark theme globally. Focus ring: 2px solid primary at 30% opacity (box-shadow: 0 0 0 2px hsl(var(--ring) / 0.3)). Checkboxes and radios use accent-color: hsl(var(--primary)).
  • Collapsible section (.coll, .coll-h, .coll-body) — used in Settings page. Header row is clickable with user-select: none. Body display: none by default, revealed on expand.
  • Key-value row (.kv) — 3-column grid (160px label | 1fr value | auto action) for settings display.
  • Filters rail (.filters-rail) — sticky sidebar on Cogs/Apps tab. Sticky at top: 7rem (below both navbars). Contains checkboxes, a range input, and a reset button.
  • Range input — native <input type="range"> styled with accent-color: hsl(var(--primary)).

Data visualization

  • Heatmap (.heatmap) — CSS grid of 14px × variable cells. 60 time columns, label column at 90px. Cell states: up (green 70%), down (red 70%), empty (muted 30%).
  • Bar chart (.bar-list + .bar-row + .bar-fill) — horizontal bar list, 3-col grid (120px label | 1fr bar | 30px value). Bar fill transitions width in 0.3s.
  • uPlot time-series (.uplot-host) — 200px height host container; actual charting via uPlot library.
  • Three.js 3D — importmap for three + OrbitControls in Analytics page, for 3D sensor visualization.
  • Log box (pre.logbox) — monospace pre-formatted block, max-height 30rem, overflow-y scroll. Dark background on dark background gives subtle separation via border.
  • OTA row table (.ota-row) — 3-col grid (160px | 80px | 1fr) for firmware OTA records.

Overlays

  • Modal (.modal-bg + .modal) — fixed inset, 70% opacity blur-backdrop scrim. Modal itself is card-surfaced, max-width 560px. Result states: .modal-result.ok (green tint) and .modal-result.err (red tint).
  • Detail modal (.detail-modal-bg + .detail-modal) — larger variant (max 820px, 2rem padding) used on Cog detail view. Header has emoji, name, meta chips; sections below are tabbed.
  • Keyboard shortcut tag (.kb) — small monospace tag with secondary background, used inline in Settings and Tailscale pages to show keyboard shortcuts.

5. Iconography

All icons are inline SVG, 24×24 viewBox, fill: none, stroke: currentColor, stroke-width: 2. The path geometry is Lucide Icons — confirmed by comparing the Sun/gear/shield/grid/activity paths against Lucide's source. Key examples observed:

  • Sun/rays (brand mark, dashboard hero)
  • Settings/gear (nav, subnav gear button)
  • Activity/pulse (KPI signal icon)
  • Bar chart 3 (analytics KPI)
  • Grid 2×2 (cluster/cog layout)
  • Shield with checkmark (AIDefence)
  • House (home nav tile)
  • Book-open (guide nav)

No external icon font is used. Every icon is self-contained in the HTML at point of use — no sprite sheet.


6. Dark Mode

The design is dark-only. There is no prefers-color-scheme: light media query in v0-chrome.css or any page-level stylesheet. The color system is entirely designed around the dark palette above. The source comments explicitly note that fonts.googleapis.com is blocked for Tailnet isolation, reinforcing that this is an always-dark appliance UI, not a consumer product that needs theming.

Surface hierarchy (light to dark, within the dark palette):

  1. --surface-elevated (#181c24) — slightly lighter card variant
  2. --card (#14171e) — standard card
  3. --surface-overlay (#111318) — modal/sticky appbar base
  4. --background (#0b0e13) — page root

The appbar uses background: hsl(var(--background) / 0.9) + backdrop-filter: blur(8px) so content underneath bleeds through as a translucency effect.


7. Notable Interactions

  • Nav hover: 150ms color + background transition, no translate. Active state uses a 2px pseudo-element underline that animates in via opacity.
  • Nav link active press: transform: translateY(1px) on :active at 50ms — very subtle tactile response.
  • Card hover: transform: translateY(-2px) at 200ms on cards and cog items. Border shifts from --border/0.5 to primary/0.4 on hover. On the nav tiles, box-shadow deepens.
  • Hero button hover: transform: translateY(-1px) + border-color shift to primary at 70%.
  • Pick card hover: translateY(-2px) + primary-glow box-shadow.
  • Focus ring: 2px solid primary at 30% opacity as box-shadow — uses outline: none everywhere and replaces it with the ring shadow. nav links use outline: 2px solid hsl(var(--primary)/0.6); outline-offset: 1px for focus-visible.
  • Bar fill animation: transition: width 0.3s on bar chart fill elements for data-load entrance.
  • Modal backdrop: backdrop-filter: blur(4px) on modal scrim, blur(6px) on the Cog detail modal.
  • Copy button feedback: .copied state class swaps border and text to accent green, visible for a short duration (JS-controlled).
  • Pill CTA: Background fills from 15% opacity teal to 100% solid on hover — a strong affordance for primary actions.
  • Scroll fade mask: The nav bar has mask-image: linear-gradient(to right, black calc(100% - 24px), transparent) to fade out the rightmost item, hinting at horizontal scroll.
  • Cogs hero carousel: Paginator dots expand from 0.55rem circles to 1.5rem pill shape (border-radius 0.4rem) when active — a distinctive indicator pattern.

8. HA-Parity Opportunities

For ADR-131 P2, the following comparisons are relevant between this design and Home Assistant's frontend (home-assistant-main):

HOMECORE component Cognitum V0 pattern HA equivalent Better reference
KPI metric card .kpi — icon + label + value ha-statistic-card, sensor-badge Cognitum — cleaner dense layout; HA's is more verbose
Status badge/pill .badge + .chip — pill with 1px border ha-label-badge, state-badge HA — HA has more state variants and i18n built in
Dark surface cards --gradient-card linear gradient HA uses flat var(--card-background-color) Cognitum — gradient gives depth HA lacks
Toggle/switch accent-color native checkbox HA ha-switch (Material) HA — purpose-built, accessible, animated
Navigation Horizontal sticky nav, underline indicator HA sidebar (vertical) Neither — HOMECORE needs a new shell; Cognitum's horizontal bar is appropriate for appliance context
Heatmap timeline CSS grid .heatmap No HA equivalent Cognitum — take this pattern directly
Bar chart CSS-only .bar-fill bar list HA uses Recharts Cognitum — zero-dep CSS bars good for simple metrics; use for small cards
Time-series chart uPlot .uplot-host HA uses ApexCharts / Recharts HA — ApexCharts has more features, better RTL support
Modal .modal-bg blur-backdrop HA ha-dialog (Material) HA — a11y and focus-trap already solved
Toast / alert banner .modal-result.ok/err inline result + .cl-banner.warn/err HA ha-alert HA — HA's alerts are more composable
Focus ring box-shadow ring pattern HA uses :focus-visible outline HA — HA's approach has better browser compatibility
Chip (category) .chip.cat-* per-category color mapping HA ha-chip Cognitum — the category-specific hue mapping is richer

9. Design Tokens for HOMECORE-FRONTEND P1

Concrete CSS variable names and starting values for the TypeScript+WASM frontend to adopt. These follow the Cognitum V0 source directly, adjusted where needed for HOMECORE context.

:root {
  /* Surfaces */
  --hc-bg:                   hsl(220 25% 6%);      /* #0b0e13 — page root */
  --hc-surface-card:         hsl(220 20% 10%);     /* #14171e — card fill */
  --hc-surface-elevated:     hsl(220 20% 12%);     /* #181c24 — raised panel */
  --hc-surface-overlay:      hsl(220 20% 8%);      /* #111318 — modal/nav base */

  /* Text */
  --hc-text:                 hsl(210 20% 92%);     /* #e6eaee — primary text */
  --hc-text-muted:           hsl(215 15% 55%);     /* #7b899d — secondary/label */

  /* Accent palette */
  --hc-primary:              hsl(185 80% 50%);     /* #19d4e5 — teal, primary actions */
  --hc-primary-fg:           hsl(220 25% 6%);      /* #0b0e13 — text on primary */
  --hc-accent:               hsl(142 70% 50%);     /* #26d867 — green, success/CTA */
  --hc-accent-fg:            hsl(220 25% 6%);      /* #0b0e13 — text on accent */
  --hc-destructive:          hsl(0 65% 50%);       /* #d22c2c — error/danger */
  --hc-warning:              hsl(38 80% 60%);      /* #e69940 — warning/amber */

  /* Borders & rings */
  --hc-border:               hsl(220 15% 18%);     /* #272b34 — subtle border */
  --hc-ring:                 hsl(185 80% 50%);     /* #19d4e5 — focus ring */

  /* Radii */
  --hc-radius:               0.75rem;              /* cards, modals */
  --hc-radius-sm:            0.4rem;               /* buttons, inputs, chips */
  --hc-radius-pill:          9999px;               /* badges, CTA pills */

  /* Typography */
  --hc-font-display:         'Outfit', system-ui, sans-serif;
  --hc-font-mono:            'JetBrains Mono', monospace;

  /* Shadows */
  --hc-shadow-card:          0 8px 32px -8px hsl(220 25% 2% / 0.8);
  --hc-shadow-glow:          0 0 60px -10px hsl(185 80% 50% / 0.3);

  /* Gradients */
  --hc-gradient-card:        linear-gradient(180deg, hsl(220 20% 12%) 0%, hsl(220 20% 8%) 100%);
}

Notes for P1 implementation:

  • Adopt Outfit + JetBrains Mono from Google Fonts in development; ship local fallbacks for production (Tailnet appliances block outbound font requests per the Cognitum source comment).
  • The --hc-ring focus approach should be implemented as box-shadow: 0 0 0 2px hsl(var(--hc-ring) / 0.3) combined with outline: none — matches Cognitum's pattern and avoids the offset-gap issue in Firefox.
  • Add --hc-gradient-hero and --hc-gradient-glow when the dashboard hero section is built; keep them out of the P1 design-token foundation to avoid premature complexity.
  • The --hc-warning amber is not in the Cognitum :root block (it is inline throughout) — elevating it to a token is a deliberate improvement for HOMECORE.