# `MishkaCmsCoreWeb.Live.Admin.Dashboard.Runtime.SettingsLive`
[🔗](https://github.com/mishka-group/mishka_cms_core/blob/v0.0.1/lib/mishka_cms_core_web/live/admin/dashboard/runtime/settings_live.ex#L5)

The CMS settings admin page: a left rail of category tabs and a content panel whose fields are
rendered dynamically from `MishkaCmsCore.Settings` definitions, one input per setting by its
`type`. The markup lives in the co-located `settings_live.html.heex`.

Settings are multitenant (see `MishkaCmsCore.Runtime.Setting`). A **master** admin picks a scope —
"Global" (the value for every site) or a specific site — from the header; a **tenant** admin is
locked to their own site. Each setting declares a `scope`: `:global` settings are editable only in
the Global view (shown read-only to a site), `:master` settings are Global-only and hidden from a
site entirely, `:site` settings only in a site view, and `:both` (a global default a site may
override) in either, and one declared `editable?: false` is read-only to everyone. A setting is
shown to whoever may write it (`MishkaCmsCore.Settings.writable?/2`): its declared `permission`, or
`admin:dashboard:setting` when it declares none. A tab left with nothing to show is not drawn, so a
dashboard admin without the setting area sees their own Preferences alone.

Saving goes through `Settings.put_as/4`, which carries the admin as the Ash actor so
`MishkaCmsCore.Runtime.Setting`'s own policies decide which site's row they may write. Scope is
encoded as `nil` for the Global view and a site uuid for a specific site's view, and `write_scope/1`
clamps a tenant to their own site — but that clamp is a convenience, not the boundary: a submitted
scope that is not theirs is refused by the resource whether or not the page catches it first.

A save only writes what actually changed, and only fields the browser sent (an unticked checkbox
being the one thing whose absence is meaningful), so editing one input on a crowded tab costs one
write and one broadcast instead of one per field on the tab.

The page is live for everyone watching it: `MishkaCmsCore.Runtime.Setting` publishes every write to
`"setting:updated"`, which the page subscribes to. A master acts on all of it; a tenant discards
the writes belonging to sites other than its own (`reaches?/2`), so a neighbour's save never moves
their page. A broadcast is also ignored while the viewer is mid-edit (`dirty?`), so someone else's
save never clobbers unsaved input.

Settings are drawn in the order they are declared. A named `:group` is one section, where its first
setting stands; settings with no group form runs between the named groups, which have no heading.
A group whose settings declare `collapsed: true` (`MishkaCmsCore.Settings.define/3`) is drawn as a
closed accordion titled by the group, whatever its settings hold. It opens by itself only when a
setting in it was refused by the last save (`open_group?/2`), so the message under the field is
in view, and once drawn it stays as the admin left it, through every later change of the page. A
refused value is said under its field, whatever the tab.

A `:secret` setting (a provider's API key) is never drawn back: its field is empty, says the
last four characters of what is stored, and a save that leaves it empty keeps the stored one. The
Email tab adds a test email to any address, through the scope's provider, and the email history
(`MishkaCmsCore.Mail.Delivery`).

The SEO tab adds the Redraw share cards buttons (`MishkaCmsCoreWeb.Live.Admin.Components.ShareCardRedraw`)
for whoever may change the site's logo: every share card of the scope — every active site in
Global — and each producer's own, one row for each producer that answers
`MishkaCmsCore.Runtime.ShareCard.targets_event/0`.

See `MishkaCmsCore.Settings` and `MishkaCmsCore.Runtime.Setting`.

# `handle_info`

A setting changed somewhere. The global `setting:updated` topic carries EVERY site's writes, so a
tenant drops the ones outside its reach (`reaches?/2`) rather than re-reading for a site it cannot
see. A refresh is also skipped mid-edit so a broadcast never clobbers unsaved input.

# `render`

---

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