# `MishkaCmsCore.Settings`
[🔗](https://github.com/mishka-group/mishka_cms_core/blob/v0.0.1/lib/mishka_cms_core/settings.ex#L5)

CMS settings — one module for the whole subject: reading and writing values, plus the registry of
declared settings the admin page renders.

## Reading & writing

Reads come from the ETS cache (`MishkaCmsCore.Runtime.States.SettingsState`) — lock-free, safe on
every request; writes go to the durable `MishkaCmsCore.Runtime.Setting` resource and refresh the
cache. Scalars are stored wrapped as `%{"value" => term}`. Settings are multitenant: a `site_id` of
`nil` is the GLOBAL value (shared by every site), a `site_id` is that site's override, and a site
read resolves **site value → global → default**.

    Settings.get("cms.offline", false)                 # global read
    Settings.get("site.title", "MishkaCMS", site_id)   # site value, falling back to global
    Settings.put("cms.offline", true)                  # global write
    Settings.put("site.title", "Acme", site_id: id)    # per-site write

`put/3` is the trusted infra path — boot seeding and plugin registration, where there is no user
to speak for — and it skips authorization, multitenancy and the master check together. Anything
an admin typed goes through `put_as/4` instead, which routes to the resource's own `:set` /
`:master_set` action with the actor attached, so `MishkaCmsCore.Runtime.Setting`'s policies decide
which site's row they may touch.

`put/3` holds the resource's PubSub notifications back (`return_notifications?: true`), refreshes
the cache, and only then sends them. The order matters: subscribers of `"setting:updated"` react by
reading the cache, so announcing the write first hands every one of them the value it replaced.

## Declared settings (the registry)

A *definition* is a plain map (see `define/3`) describing a key, its input type, default, and where
it shows on the settings page. `load_definitions/0` runs once at boot: it aggregates definitions
from three sources, caches them in `:persistent_term`, and seeds any missing default into the store.

  1. **Core** — `core_definitions/0` below.
  2. **Extensions** — modules listed in `config :mishka_cms, :settings_providers`, each exporting a
     `definitions/0` returning a list of `define/3` maps:

         # config/config.exs
         config :mishka_cms, :settings_providers, [MishkaBlog.Settings]

         defmodule MishkaBlog.Settings do
           def definitions do
             [MishkaCmsCore.Settings.define("blog.posts_per_page", :integer,
                default: 10, category: "blog", source: :extension, label: "Posts per page")]
           end
         end

  3. **Runtime plugins** — anything answering the `settings:register` mishka_installer hook
     (`Hook.call("settings:register", %{definitions: [...]})`).

## Secrets

A `:secret` setting (a provider's API key) is stored sealed with the endpoint's
`secret_key_base` (`Phoenix.Token.encrypt/4`), so the row, the cache, the `"setting:updated"`
broadcast and the `cms_core:setting:after_*` plugin hooks carry only the sealed text. `get/3` hands
back that sealed text; `secret/2` opens it, and `secret_hint/2` gives the last four characters for
the settings page, which never draws a stored secret into a field. Rotating `secret_key_base`
makes stored secrets unreadable, and they are entered again.

See `MishkaCmsCore.Runtime.Setting` and `MishkaCmsCore.Runtime.States.SettingsState`.

# `definition`

```elixir
@type definition() :: %{
  key: String.t(),
  type: input_type(),
  default: term(),
  label: String.t() | nil,
  description: String.t() | nil,
  category: String.t(),
  group: String.t() | nil,
  source: :core | :extension | :plugin,
  scope: scope(),
  editable?: boolean(),
  options: [String.t()],
  min: integer() | nil,
  max: integer() | nil,
  attention_on_default: boolean(),
  permission:
    MishkaCmsCore.Accounts.Checks.AccessPermissions.requirement()
    | [MishkaCmsCore.Accounts.Checks.AccessPermissions.requirement()]
    | nil,
  validate: (term() -&gt; boolean()) | nil,
  resolve: :override | :any | :own,
  global_label: String.t() | nil,
  visible: boolean() | (map() -&gt; boolean()),
  collapsed: boolean(),
  layout: :table | nil,
  inverted: boolean(),
  under: String.t() | nil,
  trim: boolean()
}
```

# `input_type`

```elixir
@type input_type() ::
  :string | :text | :boolean | :integer | :select | :list | :secret | :image
```

# `scope`

```elixir
@type scope() :: :global | :site | :both | :master | :user
```

# `all_global`

```elixir
@spec all_global() :: %{optional(String.t()) =&gt; term()}
```

All GLOBAL settings as `%{key => scalar_value}` (from the RAM cache) — used for default seeding.

# `cast`

```elixir
@spec cast(definition() | map(), term()) :: {:ok, term()} | :error
```

Coerces a submitted value against the definition that declared it — the one gate both editors go
through, so a number typed on the settings page and the same number picked in a dashboard card's
gear are bounded identically.

`:integer` parses and clamps to the definition's `:min`/`:max`; `:boolean` reads the form's
`"on"`/`"true"` (and a missing checkbox as `false`); `:select` must name one of the declared
`:options`, given either as bare values or `{label, value}` pairs; `:string`/`:text` take any
binary; `:list` takes a list of strings or the comma-separated text the settings page sends, each
entry trimmed and blanks and repeats dropped. Anything else — an unparsable number, a value off a
select's list, a list holding something other than strings, an unknown type — is `:error`, and so
is a value of the right type that the definition's `:validate` answers `false` for. Nothing is
coerced to a silent zero or nil: a value that cannot be trusted is refused and the caller decides
what to tell the admin.

# `categories`

```elixir
@spec categories() :: [String.t()]
```

The distinct categories, in first-seen order (the settings-page tabs).

# `define`

```elixir
@spec define(String.t(), input_type(), keyword()) :: definition()
```

Builds a setting definition map. `opts`: `:default`, `:label`, `:description`, `:category`
(default `"general"`, the settings-page TAB — usually the owning area/extension), `:group`
(an optional sub-section HEADING within the tab, e.g. `"Notifications"`; nil = no heading),
`:source` (`:core` | `:extension` | `:plugin`, default `:core`), `:scope`
(`:global` one value for all sites, read-only to tenants | `:site` each tenant owns its own |
`:both` a global default a site may override | `:master` global AND master-only, hidden from
tenants; default `:global`), `:editable?` (default `true`), `:options` (choices for a `:select`),
`:attention_on_default` (default `false`) — set it when the default is only a placeholder an admin
is expected to replace, and the settings page marks the tab with a red dot until they do —
`:permission` (default `nil` = `admin:dashboard:setting`; one name or a list of them, where
holding ANY is enough) — what writing the setting takes, and who sees it on the settings page, e.g.
`"admin:dashboard:setting:deploy"`. A `:user` setting is seen by whoever holds its `:permission`
(everyone when `nil`) and written by its user alone. A tab whose every setting is gated away
disappears with them — and
`:validate` (default `nil`), a one-argument function answering whether a value `cast/2` has
already typed may be stored, for a rule the type alone does not hold: `site.languages` is a list of
strings, and only language codes — `:resolve` (default `:override`, a site's value replaces the
global one), set to `:any` for a boolean whose global and site values are OR-ed, as `cms.offline`'s
are: a site's view then shows the site's own value and says when the global one holds it on, or
`:own` for a value each scope sets for itself and never takes from the global one, as a site's
email provider and its key are (read it with `resolved/3`) — and
`:global_label` (default `nil`, the `:label` everywhere), the label in the Global view when it reads
differently there — and `:visible` (default `true`), or `fn values -> boolean end` over the tab's
values as the admin has them on screen (key → value, typed or saved), for a field that only means
something beside another one's choice: a provider's keys while that provider is chosen. The
settings page draws and saves it only while it holds; see `visible?/2` and `shown_when/3`.

`:trim` (default `false`) strips the whitespace around a `:string` or `:text` before it is checked
and stored.

`:collapsed` (default `false`) makes the setting's `:group` an accordion on the settings page: a
group with any shown setting that declares it is drawn as a closed `<details>` titled by the group,
whatever its settings hold. It opens by itself only when the last save refused a value of one of
its settings, so the message under the field is in view, and otherwise stays as the admin left it.
It needs a `:group`, whose heading it becomes, and works for any tab, so an extension's optional
settings can stay out of the way until they are used. The accordion stands where the group's first
setting is declared:

    Settings.define("site.theme_color", :string,
      group: gettext_noop("App & home screen"), collapsed: true, default: "", scope: :site)

`:layout` (default `nil`) set to `:table` draws the setting's `:group` as one table, a row per
setting — for a group of switches read together, such as the ways of signing in. Like
`:collapsed`, one setting declaring it is enough.

`:under` (default `nil`), in a `:table` group, names the switch whose row the setting is drawn
in, below that switch's description, rather than as a row of its own — a provider's keys under the
provider. Pair it with `shown_when/3` on that switch to draw it only while the switch is on.

`:inverted` (default `false`) draws a `:boolean` as its opposite: the switch is on while the stored
value is `false`. A `*.closed` key reads as the way it closes, on while open, beside switches that
are on while on.

# `definition`

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

The registered definition for a key, or `nil`.

# `definitions`

```elixir
@spec definitions() :: [definition()]
```

All registered definitions, in the order they were declared — core first, then config providers,
then runtime plugins, each key keeping the position it was first declared at while a later
redeclaration of the same key still wins on content.

The order is the settings page's tab order, and `handle_params` falls back to the FIRST category
when no tab is named, so it has to be stable. It was not: the registry was a plain map and this
returned `Map.values/1`, so both the tab order and which tab opened by default were map iteration
order — stable only by luck, and reshuffling as soon as enough definitions were registered.

# `definitions`

```elixir
@spec definitions(String.t()) :: [definition()]
```

Registered definitions in a category (a settings-page tab).

# `get`

```elixir
@spec get(String.t(), term(), term()) :: term()
```

Read a setting's scalar value from the RAM cache, or `default` if unset. With a `site_id` the read
resolves the site's override first, then the global value, then `default`.

# `get_own`

```elixir
@spec get_own(String.t(), term(), String.t() | nil) :: term()
```

A site's own value for a setting, or `default` when the site holds none. Unlike `get/3` it never
falls back to the global value — it is what the site itself set. `nil` for the site is the
installation's own value: the global one.

# `get_user`

```elixir
@spec get_user(String.t(), term(), term()) :: term()
```

Read a PER-USER preference (`:user` scope) — a value only that user sees, stored under a
user-namespaced key so it never touches the site/global value. `default` when unset.

# `load_definitions`

```elixir
@spec load_definitions() :: [definition()]
```

Aggregates every definition (core + config providers + the `settings:register` hook), caches them in
`:persistent_term`, and seeds any missing default into the store. Run once at boot; returns the
definitions.

# `offline?`

```elixir
@spec offline?(map() | String.t() | nil) :: boolean()
```

Whether a site is serving a holding page instead of itself.

Takes a site struct, a site id, or `nil` for "no site resolved". Two ETS reads, no query.

    Settings.offline?(conn.assigns[:current_site])

`cms.offline` is the one setting that does not resolve the usual way. Everywhere else a site's
value replaces the global one; here the two are OR-ed, so a master switching every site off cannot
be undone by a site switching itself on. A site's own value only ever adds to it.

| global | site  | result  |
|--------|-------|---------|
| off    | off   | online  |
| off    | on    | offline |
| on     | off   | offline |
| on     | unset | offline |

# `offline_by`

```elixir
@spec offline_by(map() | String.t() | nil) :: [:global | :site]
```

Which switches hold a site offline: `:global` for the one every site answers to, `:site` for the
site's own. `[]` when it is online; `nil` asks about the global switch alone.

    Settings.offline_by(site)   # => [:global, :site]

Two ETS reads, no query. `offline?/1` is `offline_by/1 != []`.

# `permitted?`

```elixir
@spec permitted?(map(), map() | nil) :: boolean()
```

Whether `user` may see something that declares a `:permission` — a setting definition, or anything
else carrying the field, such as a dashboard widget's descriptor.

One entry or a list of them, where holding ANY is enough — a name, or `{:under, prefix}` for
anyone holding anything under `prefix` (`MishkaCmsCore.Accounts.Checks.AccessPermissions`). `nil`
and `[]` gate nothing and mean everyone who got this far. Nobody (a `nil` user) is never
permitted, whatever is declared — an absent user is not an anonymous one with open access.

# `put`

```elixir
@spec put(String.t(), term(), keyword()) :: {:ok, term()} | {:error, term()}
```

Creates or updates a setting (durable), refreshes the RAM cache from the committed row, and notifies
plugins via the `cms_core:setting:after_master_set` hook. Options: `:site_id` (default `nil` ⇒
global; a uuid ⇒ that site's override), `:category` (default `"general"`), `:source`
(`:core` | `:extension` | `:plugin`, default `:core`), `:description`, `:editable?` (default
`true`). Returns `{:ok, value}` or `{:error, changeset}`.

# `put_as`

```elixir
@spec put_as(map(), String.t(), term(), keyword()) :: {:ok, term()} | {:error, term()}
```

Writes a setting **as a user**, through the resource's own tenant/master action with the actor
attached and authorization ON — the path every admin-facing editor takes. `put/3` stays the
trusted infra path (boot seeding, plugin registration) and deliberately skips all three of
authorization, multitenancy and the master check; this one skips none of them.

A master (`site_id` nil) writes the global row or any site's, via `:master_set`. A tenant writes
its own site's row via `:set`, and naming a different site is refused by the resource's policies
rather than clamped here — the caller finds out it asked for something it may not have, instead
of quietly getting something else. `opts` are `put/3`'s, where `:site_id` names the target row
(a tenant omitting it means its own site).

Returns `{:ok, value}` or `{:error, reason}`; anything without a `:site_id` key — `nil` included —
is `{:error, :no_actor}`, never a silent write.

# `put_user`

```elixir
@spec put_user(String.t(), term(), term()) :: {:ok, term()} | {:error, term()}
```

Write a per-user preference (see `get_user/3`). Returns `{:ok, value}` | `{:error, changeset}`.

# `resolved`

```elixir
@spec resolved(String.t(), term(), String.t() | nil) :: term()
```

A setting's value for `site_id` the way its definition resolves it: the scope's own value for
`resolve: :own` (see `get_own/3`), else `get/3`'s site → global → default.

# `secret`

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

A `:secret` setting opened for `site_id`, resolved like `resolved/3`; `nil` when none is stored or
it cannot be opened. See "Secrets" above.

# `secret_hint`

```elixir
@spec secret_hint(String.t(), String.t() | nil) :: String.t() | :unreadable | nil
```

What the settings page may say about a stored `:secret` for `site_id`: its last four characters;
`:unreadable` when one is stored but was sealed under another `SECRET_KEY_BASE` — the server's key
changed since — so it must be typed again; `nil` when none is stored.

# `shown_when`

```elixir
@spec shown_when(definition(), String.t(), term()) :: definition()
```

`definition`, shown only while the setting `key` holds `value` — and while its own `:visible`
holds. How a provider's fields follow the provider select: each provider's settings pass through
it, so no provider has to say so itself.

    Settings.shown_when(definition, "mailer.provider", "resend")

# `text`

```elixir
@spec text(String.t() | {String.t(), keyword() | map()} | nil) :: String.t() | nil
```

A definition's `:label`, `:description`, `:group`, `:global_label` or option label, in the reader's
language.

Definitions are cached at boot, before anybody's language is known, so each holds its English
words marked with `gettext_noop/1`, and the settings page translates them as it draws them. A text
built from another is `{msgid, bindings}`, each binding translated in turn:

    Settings.text(gettext_noop("Theme"))                                   #=> "پوسته"
    Settings.text({gettext_noop("%{label} — position"), label: "Blog"})    #=> "وبلاگ — جایگاه"

# `visible?`

```elixir
@spec visible?(definition() | map(), map()) :: boolean()
```

Whether `definition` shows beside the tab's `values` (key → value): its `:visible`, `true` by
default.

# `writable?`

```elixir
@spec writable?(definition(), map() | nil) :: boolean()
```

Whether `user` may write the setting `definition` declares (`write_permission/1`). A definition of
`scope: :user` is the user's own preference, and is theirs to write whenever they may see it
(`permitted?/2`).

# `write_permission`

```elixir
@spec write_permission(definition() | map() | String.t() | nil) :: [
  MishkaCmsCore.Accounts.Checks.AccessPermissions.requirement()
]
```

What writing a setting takes, as a list where holding ANY entry is enough: the `:permission` its
definition declares, or `admin:dashboard:setting` when it declares none. Takes a definition, or a
key. A key no definition names takes `admin:dashboard:setting` too, except the builder's own —
its Studio switches (`studio.*`) and component presets (`builder:preset:*`) — which take any name
that opens a builder: a runtime area of a document the builder edits, or `admin:dashboard:code`.

    Settings.write_permission("site.title")          #=> ["admin:dashboard:setting"]
    Settings.write_permission("studio.lock_tokens")  #=> ["admin:dashboard:runtime:page", …]

---

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