# `PhoenixKitManufacturing.EntitiesRegistry`
[🔗](https://github.com/BeamLabEU/phoenix_kit_manufacturing/blob/0.4.1/lib/phoenix_kit_manufacturing/entities_registry.ex#L1)

ETS-backed cache of the `machine_type` / `operation` / `defect_reason`
`phoenix_kit_entities` records, keyed by kind + uuid, with per-locale
title resolution. Subscribes to `PhoenixKitEntities.Events` and reloads
on any entity/entity-data change. Concurrent readers always see a
consistent snapshot.

Modeled 1:1 on `Andi.Orders.StatusRegistry`. The three "blueprint"
entities (`machine_type`/`operation`/`defect_reason`) are provisioned
idempotently by `provision_blueprints/0`, retried until all three are
confirmed present. Two retry paths run in parallel:

  * **Event-driven**: every reload (triggered by a PubSub event or an
    explicit `reload/0` call) retries provisioning at the top of
    `do_reload/1`.
  * **Timer-driven**: while `blueprints_provisioned` is `false`,
    `init/1` and each failed attempt schedule a
    `Process.send_after(self(), :retry_provision, @retry_provision_interval)`
    (default 30 s). `handle_info(:retry_provision, ...)` retries and
    reschedules if still not provisioned, and is a no-op once provisioned.
    This guarantees the subtabs become available even on a host that boots
    with no users and receives no entities PubSub events for a long time.

See `dev_docs/ENTITIES_MIGRATION_SPEC.md` for the original design rationale.

Not wired into `children/0` by this module alone — see
`PhoenixKitManufacturing.children/0` for supervision-tree wiring.

## Record shape

Every cached record is a plain map:

    %{
      uuid: "01…",
      entity_name: "machine_type",
      status: "published",
      position: 0,
      metadata: %{"field_template" => [...], "legacy_uuid" => "…"},
      primary_title: "CNC Mill",
      titles: %{"en-US" => "CNC Mill", "et-EE" => "CNC-frees"},
      name: "CNC Mill",
      unit: nil,
      base_time_norm_seconds: nil
    }

`titles` only carries the locale keys actually present on that
record's `data` — it is not restricted to the host's currently
"enabled" languages, so a translation someone filled in survives even
while that language is temporarily disabled site-wide.

`name` is a convenience field: the record's primary-language title by
default (as returned by `get/2`), or the title resolved for the
locale requested via `list/3` / `label/3`. `unit` /
`base_time_norm_seconds` only carry a value for `:operation` records —
they are read once from the primary-language data block and never
locale-overridden, even though the generic entities form technically
allows editing them on secondary language tabs (see
`dev_docs/ENTITIES_MIGRATION_SPEC.md` §5 — a known, accepted
limitation of the generic-UI approach). `metadata` is passed through
raw so callers (e.g. `Machines.merged_field_template/2`, which reads
`metadata["field_template"]`) can access `machine_type`-specific keys
this registry itself doesn't interpret.

## Locale handling

Callers pass this module's own bare Gettext locale codes (`"en"`,
`"et"`, `"ru"`); `phoenix_kit_entities` stores translations under
BCP-47 dialect codes (`"en-US"`, …). `normalize_locale/1` bridges the
two by mapping a bare or dialect code to an *enabled* PhoenixKit
Language sharing its prefix, falling back to the primary language for
`nil` or an unmatched code. On a fresh host with the Languages module
disabled, only the primary language's own prefix resolves distinctly —
every other requested locale falls back to the primary title, which is
the intended graceful-degradation behavior.

# `kind`

```elixir
@type kind() :: :machine_type | :operation | :defect_reason
```

# `record`

```elixir
@type record() :: %{
  uuid: String.t(),
  entity_name: String.t(),
  status: String.t(),
  position: integer(),
  metadata: map(),
  primary_title: String.t() | nil,
  titles: %{optional(String.t()) =&gt; String.t() | nil},
  name: String.t(),
  unit: String.t() | nil,
  base_time_norm_seconds: number() | nil
}
```

# `child_spec`

Returns a specification to start this module under a supervisor.

See `Supervisor`.

# `get`

```elixir
@spec get(String.t() | nil, kind()) :: record() | nil
```

Fetches a single cached record by uuid, or `nil` if unknown. `nil` is
accepted as `uuid` (returns `nil`) so callers can pass an optional
linked-record uuid straight through without a separate nil-check.

Unlike `list/3`, this does not take a locale — `:name` on the
returned record is the primary-language title (see `label/3` for
locale-specific resolution of a single record).

# `label`

```elixir
@spec label(String.t() | nil, kind(), String.t() | nil) :: String.t()
```

Resolves the title for `uuid` in `locale`, or `"Unknown"` if the uuid
isn't cached (including `nil`).

# `list`

```elixir
@spec list(kind(), String.t() | nil, keyword()) :: [record()]
```

Lists cached records for `kind`, with `:name` resolved for `locale`
(a bare Gettext code or BCP-47 dialect; `nil` resolves to the primary
language — see `normalize_locale/1`).

## Options

  * `:status` — when given, only records with this exact status are
    returned (e.g. `"published"`). Defaults to all cached (i.e. all
    non-trashed — trashed rows are never cached in the first place)
    statuses; callers that previously filtered `status: "active"`
    should now pass `status: "published"` explicitly.

# `normalize_locale`

```elixir
@spec normalize_locale(String.t() | nil) :: String.t()
```

Normalizes a bare Gettext locale (`"en"`) or BCP-47 dialect
(`"en-US"`) to the dialect code of an *enabled* PhoenixKit Language
sharing its prefix. `nil` and codes with no enabled match fall back
to the primary language.

# `ready?`

```elixir
@spec ready?() :: boolean()
```

True once the registry has completed its initial ETS load.

# `reload`

```elixir
@spec reload() :: :ok
```

Forces an immediate synchronous reload from the database.

# `start_link`

---

*Consult [api-reference.md](api-reference.md) for complete listing*
