Device Registry Unification
:::tip Status
Done. Source: docs/device-registry-unification-implementation-plan.md, header states "Status: Done" against every numbered AZD-* ticket below except one no-op, confirmed by docs/device-registry-unification-jira-breakdown.md ("Progress: all done.").
:::
Why this existed
Two full transmitter-provisioning systems ran side by side: the legacy
mosque_devices.py path (its own provision_state state machine) and the
unified device_registry.py + device_claim.py path that stream players
already used exclusively. A migration had folded mosque_devices into
devices once, id-preserving — but nothing kept them in sync afterward, so
every transmitter claimed through the legacy endpoint after that migration
existed only in mosque_devices: invisible to the device registry UI, to
invoicing, and to the unified audit trail. This was live drift, not a
one-time cleanup — it grew by one row per legacy claim.
Full architecture this unification targets: Device Management module, Device Lifecycle.
What shipped
| Ticket | Item | Status |
|---|---|---|
| AZD-132 | Drop dead transmitters/receivers/v_mosque_devices tables (Supabase-era leftovers, read/written by nothing) | Done |
| AZD-131 | Drift measurement — how many transmitters exist only in mosque_devices | Done — measured 0 rows of drift on the dev database at time of writing |
| AZD-133 | Backfill the drift window | No-op, confirmed by the AZD-131 measurement |
| AZD-142 | Registration creates a device_inventory manifest row (closes a gap where Flows A/B/D never touched a serial) | Registration half done |
| AZD-143 | Decision: Flow A/B/C claims land PENDING, same authorization gate as every other device kind | Decided and adopted |
| AZD-135 | Claim write-through — legacy claim functions delegate to core/device_claim.py:claim() instead of writing provision_state directly | Done — verified end to end (register → claim → approve → revoke) |
| AZD-144 | Flow D credential hand-off redesign — /announce now runs the real manifest check instead of an unconditional insert | Done |
| AZD-136 | Approve/reject/force-claim delegation | Force-claim done; approve/reject legacy endpoints now moot for new traffic |
| AZD-134 | mosque_devices.provision_state becomes a mirror of devices.lifecycle_status, not an independent write target | Done |
| AZD-137 | Inventory import unified onto one endpoint (POST /device-registry/inventory/import) | Done |
| AZD-146 | Telemetry mirror — mosque_devices' connectivity/status columns mirrored onto devices by DB trigger, since a transmitter's live status write target and its devices row have no id relationship | Done |
| AZD-140 | FleetPage.jsx transmitter tab repointed to the unified registry endpoint | Done |
| AZD-138 | Legacy GET /mosque-devices/fleet endpoint deleted | Done |
| AZD-139 | Full HTTP-level E2E test, legacy wire endpoint → unified registry | Done |
Three findings that changed the plan mid-implementation
The source document is explicit that these were found only by reading the actual code, not by re-deriving behavior from the architecture doc — worth preserving as a caution for similar future unification work:
core/device_claim.py:claim()had a dormant bug — it unconditionally inserted adevice_ownershiprow requiringowner_user_id, contradicting the rule that a transmitter never has an owning member. Fixed: the parameter became optional, with a hard check against the wrong kind supplying (or omitting) it.- Flows A/B/D never touched a serial number at all — only Flow C did,
and only against the legacy inventory path. Calling the unified claim
function (which requires a pre-existing
device_inventoryrow matched by serial) couldn't work until every transmitter had one — hence AZD-142 being sequenced before the claim delegation itself. - No separate Super-Admin approval step existed for Flows A/B/C —
the legacy claim functions set
provision_state='claimed'and handed back live broker credentials in the same response. Delegating onto the unified model changes observable behavior: a transmitter now sitsPENDINGuntil approved, same as every other device kind, rather than working immediately on claim (AZD-143's decision).
What does not change
- The MQTT topic scheme and
core/device_status.pyitself — transmitter and stream-player MQTT bindings stay two bindings in one file, not merged. - The firmware-facing
/claimand/announcerequest/response JSON — the documented contract in MQTT Protocol stays byte-identical; this unification changes what happens behind those endpoints, not the wire format.
Sequencing relative to Device Sales Invoices
AZD-116 (the sales-invoices auto-draft hook, see
Device Sales Invoices) assumes every
claim — transmitter or stream player — goes through
core/device_claim.py:claim(). This unification is what makes that
assumption true for transmitters; the two were sequenced together
deliberately, since shipping the invoice hook first would have silently
drafted nothing for transmitters until this unification landed.