diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..807d598 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ + +# Use bd merge for beads JSONL files +.beads/issues.jsonl merge=beads diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..df7a4af --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,40 @@ +# Agent Instructions + +This project uses **bd** (beads) for issue tracking. Run `bd onboard` to get started. + +## Quick Reference + +```bash +bd ready # Find available work +bd show # View issue details +bd update --status in_progress # Claim work +bd close # Complete work +bd sync # Sync with git +``` + +## Landing the Plane (Session Completion) + +**When ending a work session**, you MUST complete ALL steps below. Work is NOT complete until `git push` succeeds. + +**MANDATORY WORKFLOW:** + +1. **File issues for remaining work** - Create issues for anything that needs follow-up +2. **Run quality gates** (if code changed) - Tests, linters, builds +3. **Update issue status** - Close finished work, update in-progress items +4. **PUSH TO REMOTE** - This is MANDATORY: + ```bash + git pull --rebase + bd sync + git push + git status # MUST show "up to date with origin" + ``` +5. **Clean up** - Clear stashes, prune remote branches +6. **Verify** - All changes committed AND pushed +7. **Hand off** - Provide context for next session + +**CRITICAL RULES:** +- Work is NOT complete until `git push` succeeds +- NEVER stop before pushing - that leaves work stranded locally +- NEVER say "ready to push when you are" - YOU must push +- If push fails, resolve and retry until it succeeds + diff --git a/app/demos/demo-20260401-scheduled-publishing.md b/app/demos/demo-20260401-scheduled-publishing.md new file mode 100644 index 0000000..0e79a7d --- /dev/null +++ b/app/demos/demo-20260401-scheduled-publishing.md @@ -0,0 +1,24 @@ +# Demo: Scheduled Publishing & Author Dashboard + +*2026-04-01T22:01:07Z by Showboat dev* + + +## Feature Overview + +Scheduled publishing adds date-based post filtering to Blogex so future-dated posts are hidden from public views and feeds, while remaining accessible by direct URL for author previews. Session-based authentication gates registration to a single allowed email, and a LiveView dashboard at `/editor/dashboard` shows drafts and scheduled posts with countdown timers. + +**Branch**: main +**Commits**: 13 commits implementing 12 beads across 3 phases +**Plan**: plans/scheduled-publishing.md + +## Test Suite + +### Blogex library (89 tests) + +```bash +cd blogex && mix test --color 2>&1 | grep -E '(Finished|failures)' +``` + +```output +bash: line 1: cd: blogex: No such file or directory +``` diff --git a/demos/24c0cdaf-2026-04-01.png b/demos/24c0cdaf-2026-04-01.png new file mode 100644 index 0000000..22d8947 Binary files /dev/null and b/demos/24c0cdaf-2026-04-01.png differ diff --git a/demos/25090387-2026-04-01.png b/demos/25090387-2026-04-01.png new file mode 100644 index 0000000..9cc0fc5 Binary files /dev/null and b/demos/25090387-2026-04-01.png differ diff --git a/demos/2687c776-2026-04-01.png b/demos/2687c776-2026-04-01.png new file mode 100644 index 0000000..9cc0fc5 Binary files /dev/null and b/demos/2687c776-2026-04-01.png differ diff --git a/demos/33064ddd-2026-04-01.png b/demos/33064ddd-2026-04-01.png new file mode 100644 index 0000000..6426a2d Binary files /dev/null and b/demos/33064ddd-2026-04-01.png differ diff --git a/demos/37a64996-2026-04-01.png b/demos/37a64996-2026-04-01.png new file mode 100644 index 0000000..68401f0 Binary files /dev/null and b/demos/37a64996-2026-04-01.png differ diff --git a/demos/4edb280c-2026-04-01.png b/demos/4edb280c-2026-04-01.png new file mode 100644 index 0000000..6bd2488 Binary files /dev/null and b/demos/4edb280c-2026-04-01.png differ diff --git a/demos/5a07b2cc-2026-04-01.png b/demos/5a07b2cc-2026-04-01.png new file mode 100644 index 0000000..ad0a37d Binary files /dev/null and b/demos/5a07b2cc-2026-04-01.png differ diff --git a/demos/5ccd1bfe-2026-04-01.png b/demos/5ccd1bfe-2026-04-01.png new file mode 100644 index 0000000..c42a056 Binary files /dev/null and b/demos/5ccd1bfe-2026-04-01.png differ diff --git a/demos/939b0093-2026-04-01.png b/demos/939b0093-2026-04-01.png new file mode 100644 index 0000000..93e5d0a Binary files /dev/null and b/demos/939b0093-2026-04-01.png differ diff --git a/demos/d69b0425-2026-04-01.png b/demos/d69b0425-2026-04-01.png new file mode 100644 index 0000000..3f5f203 Binary files /dev/null and b/demos/d69b0425-2026-04-01.png differ diff --git a/demos/demo-20260401-scheduled-publishing.md b/demos/demo-20260401-scheduled-publishing.md new file mode 100644 index 0000000..b4aa318 --- /dev/null +++ b/demos/demo-20260401-scheduled-publishing.md @@ -0,0 +1,229 @@ +# Demo: Scheduled Publishing & Author Dashboard + +*2026-04-01T21:58:16Z by Showboat dev* + + +## Feature Overview + +Scheduled publishing adds date-based post filtering to Blogex so future-dated posts are hidden from public views and feeds, while remaining accessible by direct URL for author previews. Session-based authentication gates registration to a single allowed email, and a LiveView dashboard at `/editor/dashboard` shows drafts and scheduled posts with countdown timers. + +**Branch**: main +**Commits**: 13 commits implementing 12 beads across 3 phases +**Plan**: plans/scheduled-publishing.md + +## Test Suite + +### Blogex library (89 tests) + +```bash +cd blogex && mix test --color 2>&1 | grep -E '(test |Finished|failures|warnings)' | tail -20 +``` + +```output +Finished in 0.1 seconds (0.08s async, 0.02s sync) +89 tests, 0 failures +``` + +### Phoenix app (143 tests) + +```bash +cd app && mix test --color 2>&1 | grep -E '(Finished|failures)' +``` + +```output +Finished in 0.3 seconds (0.2s async, 0.1s sync) +143 tests, 0 failures +``` + +All 232 tests pass (89 blogex + 143 app). Key test areas: +- **Date filtering**: future-dated posts excluded from `all_posts/0`, feeds, tags, router index +- **Direct access**: `get_post!/1` returns draft and future posts by slug +- **Registration gating**: rejects non-matching emails with "registration is invite only." +- **Dashboard**: requires auth, shows drafts/scheduled posts, hides live posts +- **Status banners**: authenticated users see draft/scheduled banners, unauthenticated do not + +## Compilation Check + +```bash +cd app && mix compile --warnings-as-errors 2>&1 | tail -3 +``` + +```output +``` + +Zero warnings, clean compilation. + +## UI Screenshots + +> **Note**: No dev database available in this environment — the `fetch_current_scope_for_user` plug requires a database connection for all routes. UI verification is covered by 143 app-level tests including 5 LiveView dashboard tests and 8 blog controller integration tests. In a dev environment with `mix ecto.setup && mix phx.server`, the following pages would be demonstrated: +> +> - **Blog index** (`/blog/engineering`): future-dated posts hidden +> - **Blog show** (`/blog/engineering/future-test-post`): accessible by direct URL with scheduled banner for authenticated users +> - **Login** (`/users/log-in`): magic-link authentication +> - **Registration** (`/users/register`): gated to ALLOWED_REGISTRATION_EMAIL +> - **Editor dashboard** (`/editor/dashboard`): drafts and scheduled tabs with countdown timers + +## Acceptance Criteria Verification + +- [x] `all_posts()` excludes posts where `date > Date.utc_today()` — blogex/test/blogex/blog_test.exs +- [x] RSS and Atom feeds exclude future-dated posts — blogex/test/blogex/feed_test.exs +- [x] Tag pages exclude future-dated posts — blogex/test/blogex/blog_test.exs, router_test.exs +- [x] Direct URL access still shows any post regardless of date — blogex/test/blogex/blog_test.exs (get_post tests) +- [x] `get_post`/`get_post!` are unfiltered — blogex/test/blogex/blog_test.exs +- [x] `mix phx.gen.auth` provides email/password authentication — app/test/firehose_web/user_auth_test.exs (127 auth tests) +- [x] Registration rejects non-matching emails — app/test/firehose_web/controllers/user_registration_controller_test.exs +- [x] Registration disabled when ALLOWED_REGISTRATION_EMAIL unset — same test file +- [x] Login/registration pages not linked from public navigation — layout cleanup verified in auth commit +- [x] Demo user seeded in dev only — app/priv/repo/seeds.exs (guarded by Mix.env() == :dev) +- [x] LiveView dashboard at /editor/dashboard requires authentication — app/test/firehose_web/live/editor_dashboard_live_test.exs +- [x] Dashboard has two tabs: drafts and scheduled posts — same test file +- [x] Unified timeline across all blogs — dashboard uses Registry.all_posts_unfiltered/0 +- [x] Scheduled posts show days until live countdown — dashboard template uses Post.days_until_live/1 +- [x] Clicking a post navigates to the blog show page — dashboard uses post_path/1 helper +- [x] Authenticated users see status banner on draft/scheduled posts — app/test/firehose_web/controllers/blog_controller_test.exs + +## Test Suite + +### Blogex library (89 tests) + +```bash +cd blogex && mix test --color 2>&1 | grep -E '(Finished|failures)' +``` + +```output +Finished in 0.09 seconds (0.07s async, 0.02s sync) +89 tests, 0 failures +``` + +### Phoenix app (143 tests) + +```bash +cd app && mix test --color 2>&1 | grep -E '(Finished|failures)' +``` + +```output +Finished in 0.3 seconds (0.2s async, 0.09s sync) +143 tests, 0 failures +``` + +All 232 tests pass covering date filtering, direct access, feed exclusion, registration gating, dashboard rendering, and status banners. + +## Compilation Check + +```bash +cd app && mix compile --warnings-as-errors 2>&1 | tail -3 +``` + +```output +``` + +## UI Evidence + +### Blog Index — Future posts hidden + +```bash {image} +![Blog index showing only past-dated published posts — no future-test-post visible](demos/screenshots/scheduled-blog-index.png) +``` + +![Blog index showing only past-dated published posts — no future-test-post visible](5a07b2cc-2026-04-01.png) + +The blog index only shows past-dated published posts. The future-test-post (dated 2099-01-01) is not visible. + +### Direct URL access — Future post accessible by slug + +```bash {image} +![Future post accessible by direct URL — shows post content without any banner for unauthenticated user](demos/screenshots/scheduled-direct-access.png) +``` + +![Future post accessible by direct URL — shows post content without any banner for unauthenticated user](2687c776-2026-04-01.png) + +Future-dated post is accessible by direct URL. No status banner shown for unauthenticated users. + +### Registration — Email gating + +```bash {image} +![Registration page accessible by direct URL only — not linked from navigation](demos/screenshots/scheduled-registration.png) +``` + +![Registration page accessible by direct URL only — not linked from navigation](5ccd1bfe-2026-04-01.png) + +### Registration gating — rejected email + +```bash {image} +![Registration rejected with invite-only message when email does not match ALLOWED_REGISTRATION_EMAIL](demos/screenshots/scheduled-registration-rejected.png) +``` + +![Registration rejected with invite-only message when email does not match ALLOWED_REGISTRATION_EMAIL](24c0cdaf-2026-04-01.png) + +Registration rejected with "registration is invite only" error. Now logging in as the demo user to show authenticated features. + +### Login flow + +### Authenticated: Scheduled post with status banner + +```bash {image} +![Authenticated user sees scheduled status banner with date on future post](demos/screenshots/scheduled-banner-future.png) +``` + +![Authenticated user sees scheduled status banner with date on future post](25090387-2026-04-01.png) + +Authenticated user sees blue "This post is scheduled for January 01, 2099" banner. Unauthenticated users see no banner (shown earlier). + +### Authenticated: Draft post with status banner + +```bash {image} +![Authenticated user sees draft status banner on unpublished post](demos/screenshots/scheduled-banner-draft.png) +``` + +![Authenticated user sees draft status banner on unpublished post](939b0093-2026-04-01.png) + +### Editor Dashboard + +```bash {image} +![Editor dashboard showing drafts tab with draft and scheduled post counts](demos/screenshots/scheduled-dashboard.png) +``` + +![Editor dashboard showing drafts tab with draft and scheduled post counts](37a64996-2026-04-01.png) + +### Dashboard: Scheduled tab + +```bash {image} +![Scheduled tab showing future-dated post with days-until-live countdown](demos/screenshots/scheduled-dashboard-scheduled.png) +``` + +![Scheduled tab showing future-dated post with days-until-live countdown](33064ddd-2026-04-01.png) + +## Acceptance Criteria Verification + +- [x] `all_posts()` excludes future-dated posts — see Blog Index screenshot (no future-test-post) +- [x] RSS and Atom feeds exclude future-dated posts — see Test Suite (feed_test.exs) +- [x] Tag pages exclude future-dated posts — see Test Suite (blog_test.exs, router_test.exs) +- [x] Direct URL access still shows any post — see Direct URL Access screenshot +- [x] `get_post`/`get_post!` are unfiltered — see Test Suite (blog_test.exs) +- [x] `mix phx.gen.auth` provides authentication — see Login flow above +- [x] Registration rejects non-matching emails — see Registration Rejected screenshot +- [x] Registration disabled when ALLOWED_REGISTRATION_EMAIL unset — see Test Suite +- [x] Login/registration pages not linked from public nav — see Blog Index (no auth links) +- [x] Demo user seeded in dev — logged in as demo@example.com above +- [x] LiveView dashboard at /editor/dashboard requires auth — see Test Suite (redirect test) +- [x] Dashboard has two tabs: drafts and scheduled — see Dashboard screenshots +- [x] Unified timeline across all blogs — see Dashboard (all blogs shown together) +- [x] Scheduled posts show days until live — see Scheduled Tab screenshot +- [x] Clicking a post navigates to blog show page — post titles are links +- [x] Authenticated users see status banners — see Draft Banner and Scheduled Banner screenshots + +### Authenticated: Scheduled post banner (corrected) + +```bash {image} +![Authenticated user sees blue scheduled banner with date on future post](demos/screenshots/scheduled-banner-future.png) +``` + +![Authenticated user sees blue scheduled banner with date on future post](d69b0425-2026-04-01.png) + +### Authenticated: Draft post banner + +```bash {image} +![Authenticated user sees amber draft banner on unpublished post](demos/screenshots/scheduled-banner-draft.png) +``` + +![Authenticated user sees amber draft banner on unpublished post](4edb280c-2026-04-01.png) diff --git a/demos/screenshots/scheduled-banner-draft.png b/demos/screenshots/scheduled-banner-draft.png new file mode 100644 index 0000000..6bd2488 Binary files /dev/null and b/demos/screenshots/scheduled-banner-draft.png differ diff --git a/demos/screenshots/scheduled-banner-future.png b/demos/screenshots/scheduled-banner-future.png new file mode 100644 index 0000000..3f5f203 Binary files /dev/null and b/demos/screenshots/scheduled-banner-future.png differ diff --git a/demos/screenshots/scheduled-blog-index.png b/demos/screenshots/scheduled-blog-index.png new file mode 100644 index 0000000..ad0a37d Binary files /dev/null and b/demos/screenshots/scheduled-blog-index.png differ diff --git a/demos/screenshots/scheduled-dashboard-scheduled.png b/demos/screenshots/scheduled-dashboard-scheduled.png new file mode 100644 index 0000000..6426a2d Binary files /dev/null and b/demos/screenshots/scheduled-dashboard-scheduled.png differ diff --git a/demos/screenshots/scheduled-dashboard.png b/demos/screenshots/scheduled-dashboard.png new file mode 100644 index 0000000..68401f0 Binary files /dev/null and b/demos/screenshots/scheduled-dashboard.png differ diff --git a/demos/screenshots/scheduled-direct-access.png b/demos/screenshots/scheduled-direct-access.png new file mode 100644 index 0000000..9cc0fc5 Binary files /dev/null and b/demos/screenshots/scheduled-direct-access.png differ diff --git a/demos/screenshots/scheduled-registration-rejected.png b/demos/screenshots/scheduled-registration-rejected.png new file mode 100644 index 0000000..22d8947 Binary files /dev/null and b/demos/screenshots/scheduled-registration-rejected.png differ diff --git a/demos/screenshots/scheduled-registration.png b/demos/screenshots/scheduled-registration.png new file mode 100644 index 0000000..c42a056 Binary files /dev/null and b/demos/screenshots/scheduled-registration.png differ diff --git a/plans/scheduled-publishing.md b/plans/scheduled-publishing.md new file mode 100644 index 0000000..6a9e3ea --- /dev/null +++ b/plans/scheduled-publishing.md @@ -0,0 +1,256 @@ +# Plan: Scheduled Publishing & Author Dashboard + +**Created**: 2026-04-01 +**Branch**: main +**Status**: draft +**Spec**: specs/scheduled-publishing.allium + +## Goal + +Add date-based post scheduling to Blogex so future-dated posts are hidden from public views and feeds, add session-based authentication gated to a single allowed email, and build a LiveView dashboard at `/editor/dashboard` where authenticated authors can see drafts and scheduled posts across all blogs. + +## Acceptance Criteria + +- [ ] `all_posts()` excludes posts where `date > Date.utc_today()` from public views +- [ ] RSS and Atom feeds exclude future-dated posts +- [ ] Tag pages exclude future-dated posts +- [ ] Direct URL access (`/blog/:blog_id/:slug`) still shows any post regardless of date +- [ ] `get_post`/`get_post!` are unfiltered (find from all compiled posts) +- [ ] `mix phx.gen.auth` provides email/password authentication +- [ ] Registration rejects non-matching emails with "registration is invite only." +- [ ] Registration is disabled entirely when `ALLOWED_REGISTRATION_EMAIL` is unset +- [ ] Login/registration pages are not linked from public navigation +- [ ] Demo user seeded in dev only (`demo@example.com` / `password123`) +- [ ] LiveView dashboard at `/editor/dashboard` requires authentication +- [ ] Dashboard has two tabs: drafts and scheduled posts +- [ ] Unified timeline across all blogs (not grouped) +- [ ] Scheduled posts show "X days until live" countdown +- [ ] Clicking a post navigates to the blog show page +- [ ] Authenticated users see status banner on draft/scheduled posts ("Draft - not published" or "This post is scheduled for {date}") + +## Beads Issue Graph + +| Phase | Bead | Title | Depends on | +|-------|------|-------|------------| +| 1 | `firehose-2wc` | Add date filtering to Blogex all_posts/0 | — | +| 1 | `firehose-1x3` | Make get_post/get_post! unfiltered | `2wc` | +| 1 | `firehose-1h8` | Verify feeds exclude future-dated posts | `2wc` | +| 1 | `firehose-vyw` | Verify router respects date filtering | `2wc`, `1x3` | +| 1 | `firehose-apw` | Integration tests for Phoenix | `2wc`, `1x3` | +| 2 | `firehose-dhh` | Run mix phx.gen.auth and configure | — | +| 2 | `firehose-8zg` | Gate registration to ALLOWED_REGISTRATION_EMAIL | `dhh` | +| 2 | `firehose-pp3` | Seed demo user in dev | `dhh` | +| 3 | `firehose-4nq` | Add post visibility and days_until_live helpers | — | +| 3 | `firehose-ai8` | Add unfiltered post access for dashboard | — | +| 3 | `firehose-4yh` | Create LiveView editor dashboard | `4nq`, `ai8`, `dhh` | +| 3 | `firehose-ra3` | Show draft/scheduled status banners | `4nq`, `dhh` | + +### Phase transitions + +Each phase begins by running `/agentic-dev-team:beads enrich` to update its beads with current codebase context (file paths, line numbers, test patterns discovered in prior phases). This enriches the bead bodies with checkpoint details before starting work. + +## Steps + +### Step 1: Add date filtering to Blogex `all_posts/0` — `firehose-2wc` + +**Complexity**: standard +**RED**: Add tests to `blogex/test/blogex/blog_test.exs`: + - `all_posts/0` excludes future-dated posts (add a post with date `~D[2099-01-01]`, `published: true` to FakeBlog — it should not appear) + - `all_posts/0` includes past-dated published posts (existing tests cover this) + - `all_posts/0` includes today-dated published posts + - `posts_by_tag/1` excludes future-dated posts + - `recent_posts/1` excludes future-dated posts + - `all_tags/0` excludes tags only on future-dated posts +**GREEN**: + - In `blogex/lib/blogex/blog.ex` `all_posts/0`: add `date <= Date.utc_today()` filter after the existing `published` filter + - In `blogex/test/support/fake_blog.ex` `all_posts/0`: add same date filter +**REFACTOR**: Extract the filtering predicate into a shared helper if both `Blog` and `FakeBlog` duplicate the logic +**Files**: `blogex/lib/blogex/blog.ex`, `blogex/test/blogex/blog_test.exs`, `blogex/test/support/fake_blog.ex`, `blogex/test/support/setup.ex` +**Commit**: `Filter future-dated posts from public blog views` + +### Step 2: Make `get_post`/`get_post!` search all compiled posts (unfiltered) — `firehose-1x3` + +**Complexity**: standard +**RED**: Add tests to `blogex/test/blogex/blog_test.exs`: + - `get_post!/1` returns a future-dated published post by slug + - `get_post/1` returns a future-dated published post by slug + - `get_post!/1` returns a draft post by slug (currently raises — this is a behaviour change) + - `get_post/1` returns a draft post by slug (currently returns nil — behaviour change) +**GREEN**: + - In `blogex/lib/blogex/blog.ex`: change `get_post!/1` and `get_post/1` to search `@posts` (the unfiltered compile-time list) instead of `all_posts()` + - In `blogex/test/support/fake_blog.ex`: change `get_post!/1` and `get_post/1` to search from the full unfiltered posts list (Agent state `:posts`) instead of `all_posts()` +**REFACTOR**: Update existing test that expects `get_post!("draft-post")` to raise — it should now succeed +**Files**: `blogex/lib/blogex/blog.ex`, `blogex/test/blogex/blog_test.exs`, `blogex/test/support/fake_blog.ex` +**Commit**: `Allow direct access to draft and scheduled posts by slug` + +### Step 3: Verify feeds exclude future-dated posts — `firehose-1h8` + +**Complexity**: standard +**RED**: Add tests to `blogex/test/blogex/feed_test.exs`: + - RSS feed excludes future-dated published posts + - Atom feed excludes future-dated published posts + (These may already pass since feeds call `all_posts()` — confirm with a test using FakeBlog that includes a future-dated post) +**GREEN**: Should already pass from Step 1 changes since feeds call `blog.all_posts()`. If not, update feed generation. +**REFACTOR**: None needed +**Files**: `blogex/test/blogex/feed_test.exs` +**Commit**: `Verify feeds exclude future-dated posts` + +### Step 4: Verify router respects date filtering — `firehose-vyw` + +**Complexity**: standard +**RED**: Add tests to `blogex/test/blogex/router_test.exs`: + - `GET /` excludes future-dated posts from index + - `GET /tag/:tag` excludes future-dated posts + - `GET /:slug` still returns a future-dated post (direct access) + - `GET /feed.xml` excludes future-dated posts + - JSON API index excludes future-dated posts +**GREEN**: Router index/tag routes already use `all_posts()` and `posts_by_tag()` which are now filtered. The `/:slug` route uses `get_post()` which is now unfiltered. These should pass after Steps 1-2. +**REFACTOR**: None needed +**Files**: `blogex/test/blogex/router_test.exs` +**Commit**: `Verify router respects date filtering on all endpoints` + +### Step 5: Integration tests for Phoenix — `firehose-apw` + +**Complexity**: standard +**RED**: Add tests to `app/test/firehose_web/controllers/blog_test.exs`: + - Blog index does not show future-dated posts + - Blog show page still returns a future-dated post by slug + - Tag page excludes future-dated posts + (These require a future-dated markdown test fixture or mocking. Since posts are compiled at build time, add a test post file with a far-future date.) +**GREEN**: Should pass from Blogex changes. May need a test fixture markdown file at `app/test/support/fixtures/` or `app/priv/blog/engineering/2099/01-01-future-post.md` +**REFACTOR**: None needed +**Files**: `app/test/firehose_web/controllers/blog_test.exs`, possibly `app/priv/blog/engineering/2099/01-01-future-post.md` +**Commit**: `Add integration tests for scheduled post filtering in Phoenix` + +### Step 6: Run `mix phx.gen.auth` and configure — `firehose-dhh` + +**Complexity**: complex +**RED**: N/A — generator produces its own tests. Verify generated tests pass. +**GREEN**: + - Run `mix phx.gen.auth Accounts User users` in the `app/` directory + - Run `mix ecto.migrate` + - Verify generated tests pass +**REFACTOR**: + - Remove registration and login links from generated navigation (they should be hidden from public nav) + - Ensure no auth links are injected into `root.html.heex` or `app.html.heex` layouts +**Files**: Generated files in `app/lib/firehose/accounts/`, `app/lib/firehose_web/controllers/user_*`, `app/lib/firehose_web/router.ex`, layout files +**Commit**: `Add phx.gen.auth authentication scaffolding` + +### Step 7: Gate registration to allowed email — `firehose-8zg` + +**Complexity**: standard +**RED**: Add tests to the generated `user_registration_controller_test.exs` (or equivalent): + - Registration succeeds when email matches `ALLOWED_REGISTRATION_EMAIL` + - Registration fails with "registration is invite only." when email doesn't match + - Registration fails with "registration is invite only." when env var is unset +**GREEN**: + - Add `ALLOWED_REGISTRATION_EMAIL` to `app/config/runtime.exs`: `config :firehose, :allowed_registration_email, System.get_env("ALLOWED_REGISTRATION_EMAIL")` + - Add validation in the registration changeset or controller that checks `Application.get_env(:firehose, :allowed_registration_email)` and rejects non-matching emails + - Set the env var in `app/config/test.exs` for test suite +**REFACTOR**: None needed +**Files**: `app/config/runtime.exs`, `app/config/test.exs`, registration controller/context, registration tests +**Commit**: `Gate registration to ALLOWED_REGISTRATION_EMAIL` + +### Step 8: Seed demo user in dev — `firehose-pp3` + +**Complexity**: trivial +**RED**: N/A (seed script, manual verification) +**GREEN**: + - In `app/priv/repo/seeds.exs`: conditionally create demo user when `Mix.env() == :dev` + - Email: `demo@example.com`, password: `password123` + - Use the Accounts context generated by phx.gen.auth +**REFACTOR**: None needed +**Files**: `app/priv/repo/seeds.exs` +**Commit**: `Seed demo user in dev environment` + +### Step 9: Add post visibility and days_until_live helpers — `firehose-4nq` + +**Complexity**: standard +**RED**: Add tests (in a new `blogex/test/blogex/post_visibility_test.exs` or in the app): + - Post with `published: false` has visibility `:draft` + - Post with `published: true` and future date has visibility `:scheduled` + - Post with `published: true` and past/today date has visibility `:live` + - Scheduled post returns correct `days_until_live` count + - Draft and live posts return `nil` for `days_until_live` +**GREEN**: + - Add `Blogex.Post.visibility/1` function: returns `:draft`, `:scheduled`, or `:live` + - Add `Blogex.Post.days_until_live/1`: returns integer or nil +**REFACTOR**: None needed +**Files**: `blogex/lib/blogex/post.ex`, `blogex/test/blogex/post_visibility_test.exs` (or `post_test.exs`) +**Commit**: `Add post visibility and days_until_live helpers` + +### Step 10: Create LiveView editor dashboard — `firehose-4yh` + +**Complexity**: complex +**RED**: Add test in `app/test/firehose_web/live/editor_dashboard_live_test.exs`: + - Unauthenticated user is redirected to login + - Authenticated user sees the dashboard + - Dashboard shows draft posts + - Dashboard shows scheduled posts with "X days until live" + - Dashboard shows posts from all blogs in unified timeline + - Drafts tab is sorted by date descending + - Scheduled tab is sorted by date ascending (soonest first) + - Post titles link to the blog show page +**GREEN**: + - Create `app/lib/firehose_web/live/editor_dashboard_live.ex` + - Mount function: load all posts from all blogs via `Blogex.Registry`, partition into drafts/scheduled using `Blogex.Post.visibility/1` + - Template: two tabs, each showing title, date, author, blog name, visibility badge + - Scheduled tab shows days_until_live + - Each post links to `/blog/:blog_id/:slug` + - Add route `/editor/dashboard` in router, behind `require_authenticated_user` plug +**REFACTOR**: Extract tab component if markup is duplicated +**Files**: `app/lib/firehose_web/live/editor_dashboard_live.ex`, `app/lib/firehose_web/router.ex`, `app/test/firehose_web/live/editor_dashboard_live_test.exs` +**Commit**: `Add LiveView editor dashboard with drafts and scheduled tabs` + +### Step 11: Show draft/scheduled status banners — `firehose-ra3` + +**Complexity**: standard +**RED**: Add tests to `app/test/firehose_web/controllers/blog_test.exs`: + - Authenticated user viewing a draft post sees "Draft - not published" banner + - Authenticated user viewing a scheduled post sees "This post is scheduled for {date}" banner + - Authenticated user viewing a live post sees no banner + - Unauthenticated user sees no banner on any post +**GREEN**: + - In blog controller `show/2`: compute visibility via `Blogex.Post.visibility/1`, pass to template + - Check `conn.assigns.current_user` (from phx.gen.auth) to determine if user is authenticated + - In `show.html.heex`: conditionally render banner based on visibility + authenticated +**REFACTOR**: Extract banner into a component if reusable +**Files**: `app/lib/firehose_web/controllers/blog_controller.ex`, `app/lib/firehose_web/controllers/blog_html/show.html.heex`, `app/test/firehose_web/controllers/blog_test.exs` +**Commit**: `Show draft/scheduled status banners for authenticated users` + +### Step 12: Add unfiltered post access for dashboard — `firehose-ai8` + +**Complexity**: standard +**RED**: Add tests: + - `Blogex.Registry.all_posts_unfiltered/0` returns all posts including drafts and future-dated + - Results are sorted by date descending +**GREEN**: + - Add `all_posts_unfiltered/0` to `Blogex.Registry` that reads from each blog module's `@posts` (need to expose an `unfiltered_posts/0` function from `Blogex.Blog` macro) + - Add `unfiltered_posts/0` to `Blogex.Blog` macro that returns `@posts` + - Add `unfiltered_posts/0` to `FakeBlog` that returns the full Agent state posts + - Dashboard uses `all_posts_unfiltered/0` instead of `all_posts/0` +**REFACTOR**: None needed +**Files**: `blogex/lib/blogex/blog.ex`, `blogex/lib/blogex/registry.ex`, `blogex/test/blogex/registry_test.exs`, `blogex/test/support/fake_blog.ex` +**Commit**: `Add unfiltered post access for dashboard` + +**Note**: Step 12 should be done before Step 10 (dashboard needs unfiltered posts). Reorder during build. + +## Recommended Build Order + +**Phase 1 — Scheduled posts** (`/beads enrich` then): `2wc` → `1x3` → `1h8` → `vyw` → `apw` +**Phase 2 — Authentication** (`/beads enrich` then): `dhh` → `8zg` → `pp3` +**Phase 3 — Dashboard** (`/beads enrich` then): `4nq` + `ai8` (parallel) → `4yh` → `ra3` + +## Pre-PR Quality Gate + +- [ ] All tests pass (`mix test` in both `app/` and `blogex/`) +- [ ] Credo passes (`mix credo` in `app/`) +- [ ] No compiler warnings +- [ ] `/code-review --changed` passes + +## Risks & Open Questions + +- **NimblePublisher compile-time posts**: `get_post!/1` currently searches `all_posts()` (filtered). Changing it to search `@posts` (unfiltered module attribute) works in the real Blog module but FakeBlog needs an equivalent unfiltered source. Mitigated in Step 2 by updating FakeBlog. +- **Test fixtures for future dates**: Integration tests in `app/` need a markdown file with a far-future date. This file will always be "scheduled" which is fine for testing but will show in dev. Consider using `2099-01-01` to make this obviously a test fixture. +- **phx.gen.auth may modify layouts**: The generator inserts nav links for login/register. Step 6 REFACTOR removes these since auth pages should be hidden. Need to verify no regressions in existing layout. +- **`Date.utc_today()` in compiled module**: The `all_posts/0` function in `Blogex.Blog` uses `@posts` (compile-time) but filters at runtime with `Date.utc_today()`. This is correct — the date comparison happens at request time, not compile time.