The orchestration-substrate week. AETHER is the Elixir/Phoenix row of the six-layer substrate stack described in the [portfolio design-system spec][1] §4 — the load-bearing piece that sits above the NATS bus and below the user-facing surfaces. Before this week the row was a blank line in the diagram. By the end of the day it was running.
[1]: /codex/methods/stax-dev-portfolio-design-system
2026-05-15 — bootstrap, end to end
What landed
<internal-lab>/aether/—mix phx.new aether --no-ecto --no-mailer, Phoenix 1.8 +
LiveView 1.1 + Bandit. Initial commit 4182deb.
lib/aether/nats.ex(90 LOC) — thinGnatwrapper. One named
connection, registered as Aether.Nats.Conn, owned by a supervised GenServer. pub/2 JSON-encodes maps and forwards to gnat; sub/2 hands the calling pid through to gnat so messages arrive as {:msg, %{topic, body}}.
lib/aether/zone.ex(193 LOC) — per-zone GenServer. On init it
subscribes to lab.sonos.zone.<name>.state and registers in the unique Aether.ZoneRegistry. On every state push it merges the payload, broadcasts to a Phoenix.PubSub "zones" topic, and serves current_state/1 to the LiveView. Commands cast back the other way via lab.sonos.zone.<name>.cmd.
lib/aether/zone_supervisor.ex(61 LOC) — DynamicSupervisor that
reads config :aether, :zones and starts one Zone per row. Idempotent on {:already_started, pid}.
lib/aether_web/live/dashboard_live.ex(155 LOC) — the LiveView. Five
columns: zone, state pill, volume slider, current track, controls. Volume slider is phx-change="volume" debounced at 180 ms; Play/Pause/Stop buttons cast via the Zone GenServer.
Total AETHER-specific code in this commit: 537 LOC across five modules. Phoenix scaffolding accounts for the rest of the repo.
Composition with the rest of the stack
AETHER is a new consumer of the existing the lab substrate. It does not modify the running NATS server, the Sonos bridge daemon, or the unified hctl CLI surface. The contract is:
hctl status (CLI) ──┐
├──► same lab.sonos.zone.*.state stream
AETHER dashboard (LV) ──┘
LiveView phx-click ──► lab.sonos.zone.<name>.cmd
↓
sonos_bridge.py (UPnP)
↓
lab.event.audit (durable)
The bridge daemon owns the UPnP contract. AETHER never touches port 1400 directly. This is the substrate-discipline cut.
The two friction points
(1) Gnat subscription semantics. Gnat's sub/3 takes a subscriber pid and delivers {:msg, %{topic, body, ...}} directly to it. The natural pattern is to subscribe from inside each Zone's init/1 so the Zone's own mailbox receives the messages. Worked first try; the API is cleaner than the older gnat_consumer_supervisor pattern.
(2) The Aether.Zone GenServer is named via a {:via, Registry, ...} tuple. The LiveView looks up the pid via the Registry before casting. This avoids the global atom-name explosion (one named process per zone is fine at 10 zones; at 1000 it would not be).
Hot-reload demo — the design-system §4 contract
Per design-system §4, "hot-reload means edit-saves-takes-effect-mid-scene." The proof:
elixir --sname aether --cookie aether-cookie -S mix phx.server- Browse to
localhost:4000. All 10 zones render with current state. - Edit
lib/aether/zone.ex'stransport_pill/1— change#2e7d32
(green) to #00bcd4 (cyan).
- From a sibling node:
elixir --sname aether-rpc --cookie aether-cookie -e '
true = Node.connect(:"aether@staxx")
:rpc.call(:"aether@staxx", IEx.Helpers, :r, [Aether.Zone])
'
Returns {:reloaded, [Aether.Zone]}.
- The 10 Zone GenServers keep running. Their state is untouched. The
next state push from any zone renders that row's pill with the new color. No browser refresh.
This is not "reload the page and it looks different." This is "edit the code while the state keeps flowing." The Erlang module hot-swap is the substrate; AETHER inherits it for free.
See <internal-lab>/aether/HOT_RELOAD_DEMO.md for the full sequence.
Verification record
hctl statusshows: dining_room STOPPED vol=42, master_bath STOPPED
vol=43, bonus_room STOPPED vol=20, office PAUSED vol=32, family_room STOPPED vol=61, master_bedroom STOPPED vol=51, patio PAUSED vol=64 The Next Episode, gym PAUSED vol=80 This Is It, bonus_rm_balcony STOPPED vol=30, pool STOPPED vol=64.
curl localhost:4000returns the same 10 zones with bit-identical
state — same transports, same volumes, same track strings. Confirmed via a grep against the rendered HTML.
- RPC'd
Aether.Zone.play("bonus_rm_balcony")into the running node;
HOMELAB_AUDIT stream sequence advanced from 3 to 4. The bridge logged the cmd; the Sonos returned an HTTP 500 (no queue loaded — expected for a play with no preceding play_uri); the audit record was durably stored regardless. The pipeline is complete.
Doctrine cross-references
- [feedback_cli_first_mcp_dead.md][2] — "Single-binary Zig CLIs are the
substrate; Elixir orchestrates; Stax is meta." AETHER is the Elixir orchestrates row, materialized.
- [portfolio-design-system][1] §4 names AETHER explicitly. The
composition diagram is now matched by code.
[2]: https://github.com/SMC17/stax-blog (codex memory; not yet public)
What's deliberately not in v0.1
- The Membrane Framework dep is pinned (
membrane_core ~> 1.0) but no
pipeline elements exist yet. That is the v0.2 Lineage-Mode audio companion's job.
- Tailnet-aware rendering per design-system §8 is Phase 3; AETHER will
host that LiveView, but the surface lives in the blog-builder until the integration phase.
- The dashboard ships with plain inline CSS — no Tailwind/daisyUI on the
AETHER body content, even though the Phoenix generator pulled those in. Substrate-discipline: no framework where a <style> block is enough.
Spec amendments needed
One. The design-system §4 entry says:
Live state — Phoenix LiveView server at <internal-lab>/aether/ (new) — only the
/workshop surface uses it.
That phrasing implies AETHER is only the live-state companion of the blog. In practice AETHER is also the standalone control plane for the lab — the Sonos dashboard at localhost:4000 has nothing to do with /workshop. Recommended amendment: phrase §4 as "AETHER is the orchestration substrate; one of its surfaces is the live-state injection into /workshop, but it also stands alone as the control plane."
The atlas is updated accordingly: repo/aether is its own node, with edges to method/portfolio-design-system (governed-by) and to edition/i-phonograph (companion-of — the Phonograph capsule's audio component will eventually be a Membrane pipeline running inside AETHER).
Next milestones:
- v0.2 — Membrane pipeline element that renders one journal essay to
audio. Lineage Mode companion.
- v0.3 — Apple TV + HomePod fan-out via
pyatvbridge; same Zone
abstraction extended for AirPlay 2 group controls.
- v0.4 — Tailnet-aware LiveView mounted at
/workshop/liveon the
blog-builder; integration cut.