# 2026-07-13 -- D-113(a2) write path PROVEN on the live edge

The READ half was proven earlier today. This closes the other half: **configuration written
through the REST API reaches the running daemon.** (a2) is now demonstrated end to end, not
argued.

## What was executed (live, operator-approved)

    POST kea/dhcpv4/set_subnet/93473635-f2aa-5c0a-986c-438e59088c6c   -> {"result":"saved"}
    POST kea/service/reconfigure                                       -> {"status":"ok"}
    GET  kea/service/status                                            -> {"status":"running"}

Deliberately IDEMPOTENT: the payload was the exact values read back from the API moments before
(`subnet 10.10.0.0/24`, `pools 10.10.0.100-10.10.0.199`, `routers`/`domain_name_servers`
`10.10.0.1`, `description "LAN DHCP"`). Nothing about the edge's behaviour should change -- the
point was to exercise the WRITE path, not to alter the edge.

## GROUND TRUTH after the write (not the API's self-report)

The API saying `{"result":"saved"}` is the service's own verdict. What was actually measured on
the box:

    kea-dhcp4 pid 30027 (RESTARTED), bound udp4 10.10.0.1:67

Kea's OWN generated config (`/usr/local/etc/kea/kea-dhcp4.conf` -- the daemon's JSON, not
OPNsense's XML):

    interfaces           : ['vtnet0']
    subnet               : 10.10.0.0/24
    pools                : ['10.10.0.100-10.10.0.199']
    routers              : 10.10.0.1
    domain-name-servers  : 10.10.0.1
    domain-name          : office1.vr1.cloud.neumatrix.local

Router unaffected (the endpoint touches only Kea, but verify, never assume):
WAN 172.30.1.2, LAN 10.10.0.1, default route 172.30.1.1, 8 LAN pass rules loaded in pf,
egress to 1.1.1.1 at **0.0% packet loss**.

**The full chain works: API write -> OPNsense renders -> Kea loads -> daemon serves.**

## Payload shape (measured -- do not infer this)

OPNsense GET returns select fields as `{value: {selected: 0|1}}`; SET expects the comma-joined
selected values. A GET response is therefore NOT directly re-POSTable -- it must be flattened:

    {"routers": {"10.10.0.1": {"value": "10.10.0.1", "selected": 1}}}   # what GET returns
    {"routers": "10.10.0.1"}                                            # what SET expects

`kea/service/reconfigure` is the apply verb (ServiceController extends
ApiMutableServiceControllerBase; `$internalServiceName = 'kea'`). Without it the config is saved
but the daemon is not reloaded.

## Consequence: the config.xml push path is now RETIRED IN PRACTICE

DHCP on this edge is now API-managed. **Pushing a full rendered `config.xml` would clobber it.**
Do not run the old scp/install/reboot path against Office1. The template must be reduced to a
minimal bootstrap (sshd + root key + console) before it is used again -- that is the next step
under D-113(a2), and it is NOT yet done.

## Revert

Re-POST the previous values (they are unchanged, so nothing to undo), or from the GUI:
Services > Kea DHCP > DHCPv4 > Subnets. The pre-API `config.xml` backups remain in
`/conf/backup/` on the edge.
