%{
  title: "Scheduled Publishing & Author Dashboard",
  author: "Willem van den Ende",
  tags: ~w(release features),
  description: "Future-dated posts stay hidden until their publish date, authors get a dashboard to track drafts and scheduled content, and registration is locked down to invited emails only."
}
---

This post was generated by a coding agent, after using the demo skill to make a demo using Rodney and Showboat (thanks Simon Willison). I find it fun and useful to see this, gives some indication of what went in. 

There is a note at the bottom about tools used and the number of phases.

# As the agent wrote:

Posts in Firehose are markdown files with a date in the filename. Until now, every published post was immediately visible. That changes today: posts with a future date are now hidden from public views until their date arrives.

This was built in a single session using an agentic dev team -- 12 issues tracked in beads, executed in three parallel phases, producing 232 tests across the blogex library and Phoenix app.

## What changed

### Future-dated posts are hidden from public views

The blog index, tag pages, RSS feeds, and Atom feeds now filter out posts where the date is after today. If you schedule a post for next Tuesday, readers won't see it until then.

![Blog index showing only past-dated posts](/images/scheduled-publishing/scheduled-blog-index.png)

But here's the key design choice: **direct URL access still works**. If you know the slug, you can view the post. This lets authors share preview links with reviewers before the publish date.

![Future post accessible by direct URL](/images/scheduled-publishing/scheduled-direct-access.png)

### Status banners for authors

When you're logged in, draft and scheduled posts show a status banner so you always know what state a post is in. Unauthenticated visitors see nothing -- no clue the post isn't "live" yet.

**Scheduled posts** show a blue banner with the target date:

![Scheduled banner showing target publish date](/images/scheduled-publishing/scheduled-banner-future.png)

**Draft posts** (unpublished) show an amber banner:

![Draft banner on unpublished post](/images/scheduled-publishing/scheduled-banner-draft.png)

### Editor dashboard

A new LiveView at `/editor/dashboard` gives authors a unified view of all non-live content across every blog. Two tabs: drafts and scheduled posts. Scheduled posts show a "days until live" countdown.

![Dashboard drafts tab](/images/scheduled-publishing/scheduled-dashboard.png)

![Dashboard scheduled tab with countdown](/images/scheduled-publishing/scheduled-dashboard-scheduled.png)

The dashboard requires authentication -- unauthenticated users are redirected to the login page.

### Authentication and registration gating

We added `mix phx.gen.auth` for session-based authentication with magic links and password login. Login and registration pages are accessible by direct URL only -- they're intentionally not linked from the public navigation.

Registration is gated to a single email via the `ALLOWED_REGISTRATION_EMAIL` environment variable. Anyone else gets a polite rejection:

![Registration rejected for non-matching email](/images/scheduled-publishing/scheduled-registration-rejected.png)

When the environment variable isn't set, registration is disabled entirely. A demo user (`demo@example.com`) is seeded in dev for local testing.

## How it was built

The feature was planned as an [Allium specification](https://github.com/your-org/allium) with surfaces, rules, and domain entities, then broken into 12 beads (issues) across three phases:

1. **Scheduled posts** (5 beads): date filtering in blogex, unfiltered direct access, feed/router verification
2. **Authentication** (3 beads): phx.gen.auth scaffolding, registration gating, dev seed
3. **Dashboard** (4 beads): post visibility helpers, unfiltered registry access, LiveView dashboard, status banners

All 12 beads were executed with parallel agentic workers in isolated git worktrees, then merged and integrated on main. The demo caught one bug (auth check using `current_user` instead of `current_scope`) which was fixed before this post.

## By the numbers

- **232 tests** passing (89 blogex + 143 Phoenix app)
- **12 beads** planned, executed, and closed
- **3 phases** run with parallel workers
- **0 compiler warnings**
