README.md

# mailglass_admin

Mountable LiveView surfaces for mailglass. Today that means:

- a dev-preview dashboard for mailable iteration
- a production operator dashboard for delivery inspection and targeted webhook replay inside an adopter-owned auth boundary

The operator surface is delivery-centric. It helps an authenticated operator
inspect provider lifecycle facts, replay facts, and reconcile facts for one
selected delivery. The canonical support runbook lives in
`guides/operator-incident-support.md`.

## API Stability

The canonical operator trust contract lives in
[`docs/operator-trust.md`](docs/operator-trust.md).

The canonical `v1.x` admin surface inventory lives in
[`docs/api_stability.md`](docs/api_stability.md).

The canonical matched-sibling compatibility and deprecation policy lives in the
core repo guide
[`../guides/compatibility-and-deprecations.md`](../guides/compatibility-and-deprecations.md)
and is re-exposed here through
[`docs/compatibility-and-deprecations.md`](docs/compatibility-and-deprecations.md).

The trust contract is intentionally narrow:

- stable: router macros, their documented options, the `MailglassAdmin.Auth`
  behaviour, and the operator auth/session/replay semantics
- internal: LiveView modules, component modules, DOM/CSS shape, preview assigns
  plumbing, layouts, and internal mount wiring

Do not treat ExDoc visibility, public function reachability, or framework
callback exports as the contract by themselves.

Use `docs/operator-trust.md` for the stable router/auth/session/replay
semantics. Use the compatibility guide for release-line matching,
support-matrix truth, retained compatibility bridges, and upgrade posture. Use
the admin stability page only for the package surface inventory.

## Installation

Add `mailglass_admin` to your adopter app's `mix.exs`:

    def deps do
      [
        {:mailglass, "~> 0.3"},
        {:mailglass_admin, "~> 0.3", only: :dev}
      ]
    end

Then `mix deps.get`.

## Mount the dev preview

Add four lines to `lib/my_app_web/router.ex`:

    import MailglassAdmin.Router

    if Application.compile_env(:my_app, :dev_routes) do
      scope "/dev" do
        pipe_through :browser
        mailglass_admin_routes "/mail"
      end
    end

Restart `mix phx.server`, visit `/dev/mail`. Done.

The `if Application.compile_env(:my_app, :dev_routes) do ... end` wrapper is
the Phoenix 1.8 convention (same gate that protects `live_dashboard` and
`Plug.Swoosh.MailboxPreview`). `mailglass_admin` does not check `Mix.env()`
itself — dev-only is the adopter's responsibility.

## Mount the production operator surface

Import the same router helpers, but mount the operator surface inside your
normal authenticated browser scope:

    import MailglassAdmin.Router

    scope "/ops" do
      pipe_through [:browser, :require_authenticated_user]

      mailglass_operator_routes "/mail",
        auth: MyApp.MailglassAdminAuth,
        session: [
          subject_id: "current_user_id",
          tenant_id: "current_tenant_id",
          auth_method: "auth_method",
          recent_auth_at: "recent_auth_at"
        ],
        on_mount: [{MyAppWeb.UserAuth, :require_authenticated_user}],
        unauthorized_path: "/users/log-in"
    end

`auth:` stays adopter-owned. `mailglass_admin` does not ship a login system,
session schema, or recent-auth prompt. It expects your app to decide who may
enter the operator surface and how "recent authentication" is satisfied.

## Operator replay contract

The operator surface now supports targeted webhook replay from the selected
delivery detail pane.

- Replay starts from one selected delivery, but the server resolves that UI
  selection to one exact stored `mailglass_webhook_events` row before any
  side effect runs.
- When exactly one raw webhook target is safe, the confirmation modal
  preselects it. When multiple targets are safe, the operator must choose
  one explicitly. When no exact target is safe, the modal explains why replay
  is unavailable instead of guessing.
- Confirming replay calls your adopter-owned `auth:` module with
  `:destructive_action` at action time. Mount-time authorization is not
  enough for replay.
- Replay reuses mailglass's existing webhook normalization and idempotency
  semantics. A replay can honestly result in either new work or a duplicate
  / no-op convergence outcome.
- Every replay attempt is ledger-audited with requested, succeeded, or failed
  facts that stay visible in the delivery timeline.

## Operator support boundary

- Provider lifecycle facts come from the delivery timeline, matched webhook
  events, and the shipped telemetry families documented in `guides/telemetry.md`.
- Replay facts are operator-triggered audit facts for one exact stored webhook
  target.
- Reconcile facts come from the background-first orphan sweep and
  `mix mailglass.reconcile`.
- `mailglass_admin` does not ship a separate observability dashboard,
  cross-tenant incident console, or unauthenticated support route.

## LiveReload setup (optional)

When your adopter app runs under `:phoenix_live_reload`, mailglass_admin can
refresh the preview automatically on file save. Add a `live_reload.notify`
entry to your endpoint:

    config :my_app, MyAppWeb.Endpoint,
      live_reload: [
        notify: [
          "mailglass:admin:reload": [~r"lib/.*mailer.*\.ex$"]
        ]
      ]

The topic is prefixed `mailglass:admin:reload` (not bare `mailglass_admin_reload`)
to match the package's `mailglass:`-prefixed PubSub naming convention. When
LiveReload is not configured the preview still works — the adopter just
refreshes the browser manually.

## `preview_props/0` contract

Each `Mailglass.Mailable` module can declare preview scenarios by defining
`preview_props/0`:

    defmodule MyApp.UserMailer do
      use Mailglass.Mailable, stream: :transactional

      def preview_props do
        [
          welcome_default: %{user: %User{name: "Ada"}, team: %Team{name: "Analytical Engines"}},
          welcome_enterprise: %{user: %User{name: "Ada"}, team: %Team{name: "Analytical Engines"}, plan: :enterprise}
        ]
      end

      def welcome(assigns), do: ...
    end

Each tuple is a discrete scenario; the sidebar nests scenarios under the
mailable module name (`MyApp.UserMailer -> welcome_default`). Scenarios
appear in insertion order. Mailables without `preview_props/0` still show
up in the sidebar as `No previews defined` — they remain discoverable even
before you write any scenarios.

## What this ships

- Auto-discovered mailable sidebar (collapsible scenario groups)
- Four tabs per scenario: HTML, Text, Raw (RFC 5322 envelope), Headers
- Type-inferred assigns form — edit any top-level assign inline and
  re-render
- Device toggle (375 / 768 / 1024) + chrome dark toggle
- Graceful failure badges for mailables whose `preview_props/0` raises
- A production operator mount with a separate `live_session`, explicit
  session whitelist, and adopter-owned auth seam
- Delivery, timeline, and suppression visibility in the operator UI
- Targeted webhook replay from the selected delivery detail view with
  action-time auth checks, exact-target confirmation, and durable timeline
  audit visibility

## What this does NOT ship

- Hosted authentication, user storage, or a recent-auth UX. Keep auth and
  step-up ownership in the adopter app.
- Bulk replay, "replay latest", or delivery-wide replay guessing. The operator
  surface replays one exact stored webhook target at a time.
- Suppression removal flows. The recent-auth seam exists, but suppression
  reversal remains a later phase.
- Search, filter, or pagination over mailables. v0.5.
- Inbound-mail (`mailglass_inbound`) Conductor LiveView — separate sibling
  package, v0.5+.
- Stable DOM/component/LiveView implementation APIs. Those remain internal even
  when they are visible in generated docs or reachable in source.

## License

MIT. Released alongside `mailglass` via coordinated linked-version Release
Please tags.