howdy/admin

A development admin area for a Howdy app. Mount it from your dev/ entry point, and it appears at /_howdy:

import howdy/admin
import howdy/dev

pub fn main() {
  let db = my_app.open_database()
  let identity = my_app.identity(db)
  let assert Ok(_) =
    dev.start(fn() {
      my_app.app(db, identity)
      |> admin.mount(admin.new() |> admin.database(db) |> admin.auth(identity))
    })
  process.sleep_forever()
}

Gleam has no way to find out which packages an app uses at runtime, and no registry of the values it built, so the app hands the admin what it has. What is registered decides what the admin shows:

Some things need no registering, because mount can see them in the app:

Development only

There is no login: anyone who can reach the pages can read and change every row and sign in as anyone. Keep howdy_admin a dev dependency, mount it only from dev/, and serve it from howdy/dev, which listens on loopback. The pages also refuse a request whose Host is not localhost, 127.0.0.1 or [::1] unless allow_hosts says otherwise, so a browser on another machine, or a DNS-rebinding page, gets 403. Writes must also come from the admin’s own origin: a POST whose Sec-Fetch-Site or Origin header says another site sent it gets 403 too, so a page you visit while the admin runs cannot post to it.

Types

An admin area under construction.

pub opaque type Admin

Values

pub fn allow_hosts(admin: Admin, hosts: List(String)) -> Admin

Exact request hostnames (without port) the pages answer, replacing the loopback names. Anyone who can reach an allowed host owns your data.

pub fn at(admin: Admin, prefix: String) -> Admin

Mount the pages somewhere other than /_howdy.

pub fn auth(admin: Admin, identity: auth.Auth) -> Admin

Manage the users and groups of this auth configuration, and sign in as them. Its Repo is browsed too, unless database was given another.

pub fn authorization(
  admin: Admin,
  access: authorization.Authorization,
) -> Admin

Manage roles, their permissions and who holds them. Needs auth too, since roles are assigned to users.

pub fn controllers(admin: Admin) -> List(controller.Builder(Nil))

The admin’s controllers, for mounting them yourself. The API pages need the app, so only mount adds them.

pub fn database(admin: Admin, repo: repo.Repo) -> Admin

Browse and edit the tables of this Repo.

pub fn flags(admin: Admin, features: flags.Flags) -> Admin

Manage these feature flags: turn them off at once, allow or block them for users, organizations and groups, and undo any change. Rollouts and ramps are shown but set from the app. Changes are recorded as made by howdy_admin.

pub fn has_auth(admin: Admin) -> Bool

Whether auth was registered.

pub fn has_authorization(admin: Admin) -> Bool

Whether authorization was registered.

pub fn has_database(admin: Admin) -> Bool

Whether a database was registered.

pub fn has_flags(admin: Admin) -> Bool

Whether feature flags were registered.

pub fn has_jobs(admin: Admin) -> Bool

Whether a jobs runner was registered.

pub fn has_mail(admin: Admin) -> Bool

Whether an outbox was registered.

pub fn has_telemetry(admin: Admin) -> Bool

Whether a telemetry recorder was registered.

pub fn jobs(admin: Admin, runner: jobs.Runner) -> Admin

Show and manage the jobs of this runner, in the database it uses: each queue’s jobs by state, pausing queues, the runner’s schedules, and each job with its errors, to retry, cancel or delete. The runner need not be running in this process.

pub fn mail(admin: Admin, box: outbox.Outbox) -> Admin

Show the messages this outbox keeps, as they arrive: the rendered HTML, the text, the headers and the raw source, with links that open.

pub fn mail_previews(
  admin: Admin,
  previews: List(preview.Preview),
  send_with mailer: mail.Mailer,
) -> Admin

Render these email previews, each built from its sample data every time it is shown, and send one on demand through mailer. Previews from several calls are shown together; the last mailer given is used.

The mailer’s default sender applies, and its adapter really sends: point it at the outbox, or at a local SMTP server such as Mailpit, not at production.

pub fn mount(app: howdy.App, admin: Admin) -> howdy.App

Add the admin’s routes to an app. Mount it last: the admin looks at the app as it is here. If the app serves OpenAPI documents with howdy/openapi, the admin finds them and adds pages to read them and call the endpoints.

pub fn named(admin: Admin, name: String) -> Admin

The name shown in the sidebar. Defaults to “Howdy admin”.

pub fn new() -> Admin

An admin at /_howdy that knows about nothing yet.

pub fn notify_via(admin: Admin, config: pog.Config) -> Admin

How the grid’s listening connection reaches PostgreSQL, so it hears about changes through NOTIFY rather than polling. Give the same settings the app’s pool uses, such as pog.url_config(name, url); the pool name in them is not used. Only needed for a pool the app did not open with howdy/database/postgres, whose settings postgres.listen cannot find: without them the grid polls. Does nothing on SQLite.

pub fn prefix(admin: Admin) -> String

Where the pages are, for linking to them.

pub fn preview_count(admin: Admin) -> Int

How many email previews were registered.

pub fn remove_notify_triggers(
  repo: repo.Repo,
) -> Result(Nil, service.Error)

Drop the howdy_admin_notify function and triggers the grid installs on PostgreSQL to hear about changes. They are harmless to leave, and howdy/migration ignores them, but this puts a database back exactly as it was. Does nothing on SQLite.

pub fn telemetry(
  admin: Admin,
  recorder: recorder.Recorder,
) -> Admin

Show the traces and log lines recorder holds. Start telemetry with telemetry.record(recorder) so it has something to hold:

let recorder = recorder.new(keep: 200)
let assert Ok(Nil) =
  telemetry.new("my-app") |> telemetry.record(recorder) |> telemetry.start
admin.new() |> admin.telemetry(recorder)
✨ Search Document