Show draft/scheduled status banners for authenticated users

This commit is contained in:
Willem van den Ende 2026-04-01 21:40:17 +00:00
parent 86f7ffbe94
commit 5395b2de80
5 changed files with 90 additions and 2 deletions

View File

@ -3,7 +3,7 @@
{"id":"firehose-2wc","title":"Add date filtering to Blogex all_posts/0","description":"## Context\nall_posts() in blogex/lib/blogex/blog.ex (line 77-83) currently filters by `published` boolean only.\nAdd `date \u003c= Date.utc_today()` filter so future-dated posts are hidden from public views.\n\n## Scope\n- blogex/lib/blogex/blog.ex: all_posts/0\n- blogex/test/support/fake_blog.ex: all_posts/0\n- blogex/test/blogex/blog_test.exs: new tests\n- blogex/test/support/setup.ex: add future-dated post to default_posts\n\n## TDD\nRED: Test that future-dated published post is excluded from all_posts, posts_by_tag, recent_posts, all_tags\nGREEN: Add date filter after published filter\nREFACTOR: Extract filtering predicate if duplicated","status":"closed","priority":2,"issue_type":"task","owner":"willem+gitea@livingsoftware.co.uk","created_at":"2026-04-01T20:06:54.303723951Z","created_by":"Willem van den Ende","updated_at":"2026-04-01T20:31:20.372076738Z","closed_at":"2026-04-01T20:31:20.372076738Z","close_reason":"Closed"} {"id":"firehose-2wc","title":"Add date filtering to Blogex all_posts/0","description":"## Context\nall_posts() in blogex/lib/blogex/blog.ex (line 77-83) currently filters by `published` boolean only.\nAdd `date \u003c= Date.utc_today()` filter so future-dated posts are hidden from public views.\n\n## Scope\n- blogex/lib/blogex/blog.ex: all_posts/0\n- blogex/test/support/fake_blog.ex: all_posts/0\n- blogex/test/blogex/blog_test.exs: new tests\n- blogex/test/support/setup.ex: add future-dated post to default_posts\n\n## TDD\nRED: Test that future-dated published post is excluded from all_posts, posts_by_tag, recent_posts, all_tags\nGREEN: Add date filter after published filter\nREFACTOR: Extract filtering predicate if duplicated","status":"closed","priority":2,"issue_type":"task","owner":"willem+gitea@livingsoftware.co.uk","created_at":"2026-04-01T20:06:54.303723951Z","created_by":"Willem van den Ende","updated_at":"2026-04-01T20:31:20.372076738Z","closed_at":"2026-04-01T20:31:20.372076738Z","close_reason":"Closed"}
{"id":"firehose-4nq","title":"Add post visibility and days_until_live helpers","description":"## Context\nDashboard and status banners need to compute post visibility (draft/scheduled/live)\nand days until a scheduled post goes live.\n\n## Scope\n- blogex/lib/blogex/post.ex: add visibility/1 and days_until_live/1\n- blogex/test/blogex/post_test.exs: new tests\n\n## TDD\nRED: Test visibility returns :draft/:scheduled/:live correctly, days_until_live returns integer or nil\nGREEN: Implement functions\nREFACTOR: None","status":"closed","priority":2,"issue_type":"task","owner":"willem+gitea@livingsoftware.co.uk","created_at":"2026-04-01T20:07:44.5973142Z","created_by":"Willem van den Ende","updated_at":"2026-04-01T20:24:39.851993851Z","closed_at":"2026-04-01T20:24:39.851993851Z","close_reason":"Closed"} {"id":"firehose-4nq","title":"Add post visibility and days_until_live helpers","description":"## Context\nDashboard and status banners need to compute post visibility (draft/scheduled/live)\nand days until a scheduled post goes live.\n\n## Scope\n- blogex/lib/blogex/post.ex: add visibility/1 and days_until_live/1\n- blogex/test/blogex/post_test.exs: new tests\n\n## TDD\nRED: Test visibility returns :draft/:scheduled/:live correctly, days_until_live returns integer or nil\nGREEN: Implement functions\nREFACTOR: None","status":"closed","priority":2,"issue_type":"task","owner":"willem+gitea@livingsoftware.co.uk","created_at":"2026-04-01T20:07:44.5973142Z","created_by":"Willem van den Ende","updated_at":"2026-04-01T20:24:39.851993851Z","closed_at":"2026-04-01T20:24:39.851993851Z","close_reason":"Closed"}
{"id":"firehose-4yh","title":"Create LiveView editor dashboard","description":"## Context\nLiveView at /editor/dashboard behind auth. Two tabs: drafts and scheduled.\nUnified timeline across all blogs. Scheduled posts show \"X days until live\".\nLinks to post show page.\n\n## Scope\n- app/lib/firehose_web/live/editor_dashboard_live.ex\n- app/lib/firehose_web/router.ex: add /editor scope\n- app/test/firehose_web/live/editor_dashboard_live_test.exs\n\n## TDD\nRED: Unauth redirected, auth sees dashboard, drafts tab, scheduled tab with countdown, links work\nGREEN: Implement LiveView, add route\nREFACTOR: Extract tab component if markup duplicated","status":"in_progress","priority":2,"issue_type":"task","owner":"willem+gitea@livingsoftware.co.uk","created_at":"2026-04-01T20:07:44.673871753Z","created_by":"Willem van den Ende","updated_at":"2026-04-01T20:32:40.635424214Z","dependencies":[{"issue_id":"firehose-4yh","depends_on_id":"firehose-4nq","type":"blocks","created_at":"2026-04-01T20:08:01.570736282Z","created_by":"Willem van den Ende"},{"issue_id":"firehose-4yh","depends_on_id":"firehose-ai8","type":"blocks","created_at":"2026-04-01T20:08:01.597663464Z","created_by":"Willem van den Ende"},{"issue_id":"firehose-4yh","depends_on_id":"firehose-dhh","type":"blocks","created_at":"2026-04-01T20:08:01.625180883Z","created_by":"Willem van den Ende"}]} {"id":"firehose-4yh","title":"Create LiveView editor dashboard","description":"## Context\nLiveView at /editor/dashboard behind auth. Two tabs: drafts and scheduled.\nUnified timeline across all blogs. Scheduled posts show \"X days until live\".\nLinks to post show page.\n\n## Scope\n- app/lib/firehose_web/live/editor_dashboard_live.ex\n- app/lib/firehose_web/router.ex: add /editor scope\n- app/test/firehose_web/live/editor_dashboard_live_test.exs\n\n## TDD\nRED: Unauth redirected, auth sees dashboard, drafts tab, scheduled tab with countdown, links work\nGREEN: Implement LiveView, add route\nREFACTOR: Extract tab component if markup duplicated","status":"in_progress","priority":2,"issue_type":"task","owner":"willem+gitea@livingsoftware.co.uk","created_at":"2026-04-01T20:07:44.673871753Z","created_by":"Willem van den Ende","updated_at":"2026-04-01T20:32:40.635424214Z","dependencies":[{"issue_id":"firehose-4yh","depends_on_id":"firehose-4nq","type":"blocks","created_at":"2026-04-01T20:08:01.570736282Z","created_by":"Willem van den Ende"},{"issue_id":"firehose-4yh","depends_on_id":"firehose-ai8","type":"blocks","created_at":"2026-04-01T20:08:01.597663464Z","created_by":"Willem van den Ende"},{"issue_id":"firehose-4yh","depends_on_id":"firehose-dhh","type":"blocks","created_at":"2026-04-01T20:08:01.625180883Z","created_by":"Willem van den Ende"}]}
{"id":"firehose-8zg","title":"Gate registration to ALLOWED_REGISTRATION_EMAIL","description":"## Context\nRegistration must be restricted to a single email from env var.\nUnset = disabled. Wrong email = \"registration is invite only.\"\n\n## Scope\n- app/config/runtime.exs: read ALLOWED_REGISTRATION_EMAIL\n- app/config/test.exs: set test value\n- Registration controller or Accounts context: add validation\n- Registration tests: add gating tests\n\n## TDD\nRED: Registration succeeds for matching email, fails for non-matching, fails when unset\nGREEN: Add config reading + validation check\nREFACTOR: None","status":"in_progress","priority":2,"issue_type":"task","owner":"willem+gitea@livingsoftware.co.uk","created_at":"2026-04-01T20:07:28.051938506Z","created_by":"Willem van den Ende","updated_at":"2026-04-01T20:32:40.555637642Z","dependencies":[{"issue_id":"firehose-8zg","depends_on_id":"firehose-dhh","type":"blocks","created_at":"2026-04-01T20:08:01.502562336Z","created_by":"Willem van den Ende"}]} {"id":"firehose-8zg","title":"Gate registration to ALLOWED_REGISTRATION_EMAIL","description":"## Context\nRegistration must be restricted to a single email from env var.\nUnset = disabled. Wrong email = \"registration is invite only.\"\n\n## Scope\n- app/config/runtime.exs: read ALLOWED_REGISTRATION_EMAIL\n- app/config/test.exs: set test value\n- Registration controller or Accounts context: add validation\n- Registration tests: add gating tests\n\n## TDD\nRED: Registration succeeds for matching email, fails for non-matching, fails when unset\nGREEN: Add config reading + validation check\nREFACTOR: None","status":"closed","priority":2,"issue_type":"task","owner":"willem+gitea@livingsoftware.co.uk","created_at":"2026-04-01T20:07:28.051938506Z","created_by":"Willem van den Ende","updated_at":"2026-04-01T21:39:21.420987916Z","closed_at":"2026-04-01T21:39:21.420987916Z","close_reason":"Closed","dependencies":[{"issue_id":"firehose-8zg","depends_on_id":"firehose-dhh","type":"blocks","created_at":"2026-04-01T20:08:01.502562336Z","created_by":"Willem van den Ende"}]}
{"id":"firehose-ai8","title":"Add unfiltered post access for dashboard","description":"## Context\nDashboard needs access to all posts including drafts and future-dated.\nAdd unfiltered_posts/0 to Blog macro and all_posts_unfiltered/0 to Registry.\n\n## Scope\n- blogex/lib/blogex/blog.ex: add unfiltered_posts/0\n- blogex/lib/blogex/registry.ex: add all_posts_unfiltered/0\n- blogex/test/support/fake_blog.ex: add unfiltered_posts/0\n- blogex/test/blogex/registry_test.exs: new tests\n\n## TDD\nRED: Test unfiltered returns all posts including drafts and future-dated\nGREEN: Implement functions\nREFACTOR: None","status":"closed","priority":2,"issue_type":"task","owner":"willem+gitea@livingsoftware.co.uk","created_at":"2026-04-01T20:07:44.63593107Z","created_by":"Willem van den Ende","updated_at":"2026-04-01T20:31:20.37549839Z","closed_at":"2026-04-01T20:31:20.37549839Z","close_reason":"Closed"} {"id":"firehose-ai8","title":"Add unfiltered post access for dashboard","description":"## Context\nDashboard needs access to all posts including drafts and future-dated.\nAdd unfiltered_posts/0 to Blog macro and all_posts_unfiltered/0 to Registry.\n\n## Scope\n- blogex/lib/blogex/blog.ex: add unfiltered_posts/0\n- blogex/lib/blogex/registry.ex: add all_posts_unfiltered/0\n- blogex/test/support/fake_blog.ex: add unfiltered_posts/0\n- blogex/test/blogex/registry_test.exs: new tests\n\n## TDD\nRED: Test unfiltered returns all posts including drafts and future-dated\nGREEN: Implement functions\nREFACTOR: None","status":"closed","priority":2,"issue_type":"task","owner":"willem+gitea@livingsoftware.co.uk","created_at":"2026-04-01T20:07:44.63593107Z","created_by":"Willem van den Ende","updated_at":"2026-04-01T20:31:20.37549839Z","closed_at":"2026-04-01T20:31:20.37549839Z","close_reason":"Closed"}
{"id":"firehose-apw","title":"Add integration tests for scheduled post filtering in Phoenix","description":"## Context\nPhoenix blog controller tests need to verify date filtering works end-to-end.\nMay need a far-future markdown test fixture (2099/01-01-future-post.md).\n\n## Scope\n- app/test/firehose_web/controllers/blog_test.exs\n- app/priv/blog/engineering/2099/01-01-future-post.md (test fixture)\n\n## TDD\nRED: Blog index hides future post, show page returns it, tag page excludes it\nGREEN: Should pass from Blogex changes\nREFACTOR: None","status":"in_progress","priority":2,"issue_type":"task","owner":"willem+gitea@livingsoftware.co.uk","created_at":"2026-04-01T20:07:16.294363414Z","created_by":"Willem van den Ende","updated_at":"2026-04-01T21:35:39.95804435Z","dependencies":[{"issue_id":"firehose-apw","depends_on_id":"firehose-2wc","type":"blocks","created_at":"2026-04-01T20:07:52.797645635Z","created_by":"Willem van den Ende"},{"issue_id":"firehose-apw","depends_on_id":"firehose-1x3","type":"blocks","created_at":"2026-04-01T20:07:52.829112074Z","created_by":"Willem van den Ende"}]} {"id":"firehose-apw","title":"Add integration tests for scheduled post filtering in Phoenix","description":"## Context\nPhoenix blog controller tests need to verify date filtering works end-to-end.\nMay need a far-future markdown test fixture (2099/01-01-future-post.md).\n\n## Scope\n- app/test/firehose_web/controllers/blog_test.exs\n- app/priv/blog/engineering/2099/01-01-future-post.md (test fixture)\n\n## TDD\nRED: Blog index hides future post, show page returns it, tag page excludes it\nGREEN: Should pass from Blogex changes\nREFACTOR: None","status":"in_progress","priority":2,"issue_type":"task","owner":"willem+gitea@livingsoftware.co.uk","created_at":"2026-04-01T20:07:16.294363414Z","created_by":"Willem van den Ende","updated_at":"2026-04-01T21:35:39.95804435Z","dependencies":[{"issue_id":"firehose-apw","depends_on_id":"firehose-2wc","type":"blocks","created_at":"2026-04-01T20:07:52.797645635Z","created_by":"Willem van den Ende"},{"issue_id":"firehose-apw","depends_on_id":"firehose-1x3","type":"blocks","created_at":"2026-04-01T20:07:52.829112074Z","created_by":"Willem van den Ende"}]}
{"id":"firehose-dhh","title":"Run mix phx.gen.auth and configure","description":"## Context\nNo auth exists. Run mix phx.gen.auth Accounts User users.\nRemove auth links from public nav (login/registration are hidden URLs).\n\n## Scope\n- Generated files in app/lib/firehose/accounts/, app/lib/firehose_web/\n- app/lib/firehose_web/router.ex\n- Layout files (root.html.heex, app.html.heex) — remove injected auth links\n\n## TDD\nRED: Generated tests should pass\nGREEN: Run generator, migrate, verify\nREFACTOR: Remove auth links from public navigation","status":"closed","priority":2,"issue_type":"task","owner":"willem+gitea@livingsoftware.co.uk","created_at":"2026-04-01T20:07:28.010843844Z","created_by":"Willem van den Ende","updated_at":"2026-04-01T20:31:20.37861782Z","closed_at":"2026-04-01T20:31:20.37861782Z","close_reason":"Closed"} {"id":"firehose-dhh","title":"Run mix phx.gen.auth and configure","description":"## Context\nNo auth exists. Run mix phx.gen.auth Accounts User users.\nRemove auth links from public nav (login/registration are hidden URLs).\n\n## Scope\n- Generated files in app/lib/firehose/accounts/, app/lib/firehose_web/\n- app/lib/firehose_web/router.ex\n- Layout files (root.html.heex, app.html.heex) — remove injected auth links\n\n## TDD\nRED: Generated tests should pass\nGREEN: Run generator, migrate, verify\nREFACTOR: Remove auth links from public navigation","status":"closed","priority":2,"issue_type":"task","owner":"willem+gitea@livingsoftware.co.uk","created_at":"2026-04-01T20:07:28.010843844Z","created_by":"Willem van den Ende","updated_at":"2026-04-01T20:31:20.37861782Z","closed_at":"2026-04-01T20:31:20.37861782Z","close_reason":"Closed"}

View File

@ -22,11 +22,14 @@ defmodule FirehoseWeb.BlogController do
def show(conn, %{"slug" => slug}) do def show(conn, %{"slug" => slug}) do
blog = conn.assigns.blog blog = conn.assigns.blog
post = blog.get_post!(slug) post = blog.get_post!(slug)
visibility = Blogex.Post.visibility(post)
render(conn, :show, render(conn, :show,
page_title: post.title, page_title: post.title,
post: post, post: post,
base_path: blog.base_path() base_path: blog.base_path(),
visibility: visibility,
authenticated: conn.assigns[:current_user] != nil
) )
end end

View File

@ -1,4 +1,23 @@
<div class="space-y-8"> <div class="space-y-8">
<a href={@base_path} class="text-sm text-primary hover:underline">&larr; Back to posts</a> <a href={@base_path} class="text-sm text-primary hover:underline">&larr; Back to posts</a>
<%= if @authenticated and @visibility == :draft do %>
<div
class="rounded-lg bg-amber-50 border border-amber-200 px-4 py-3 text-amber-800 text-sm font-medium"
id="post-status-banner"
>
Draft — not published
</div>
<% end %>
<%= if @authenticated and @visibility == :scheduled do %>
<div
class="rounded-lg bg-blue-50 border border-blue-200 px-4 py-3 text-blue-800 text-sm font-medium"
id="post-status-banner"
>
This post is scheduled for {Calendar.strftime(@post.date, "%B %d, %Y")}
</div>
<% end %>
<.post_show post={@post} base_path={@base_path} /> <.post_show post={@post} base_path={@base_path} />
</div> </div>

View File

@ -0,0 +1,8 @@
%{
title: "Future Test Post",
author: "Test Author",
tags: ~w(test),
description: "A post scheduled for the future"
}
---
This is a future test post.

View File

@ -0,0 +1,58 @@
defmodule FirehoseWeb.BlogControllerTest do
use FirehoseWeb.ConnCase, async: false
describe "GET /blog/:blog_id/:slug - status banners" do
test "authenticated user sees draft banner on draft post", %{conn: conn} do
conn =
conn
|> init_test_session(%{})
|> assign(:current_user, %{id: 1})
|> get(~p"/blog/engineering/hello-world")
assert html_response(conn, 200) =~ "Draft"
assert conn.resp_body =~ "not published"
end
test "authenticated user sees scheduled banner on future post", %{conn: conn} do
conn =
conn
|> init_test_session(%{})
|> assign(:current_user, %{id: 1})
|> get(~p"/blog/engineering/future-test-post")
response = html_response(conn, 200)
assert response =~ "scheduled for"
assert response =~ "January 01, 2099"
end
test "authenticated user sees no banner on live post", %{conn: conn} do
conn =
conn
|> init_test_session(%{})
|> assign(:current_user, %{id: 1})
|> get(~p"/blog/engineering/why-firehose")
response = html_response(conn, 200)
refute response =~ "Draft"
refute response =~ "scheduled for"
end
test "unauthenticated user sees no banner on draft post", %{conn: conn} do
response =
conn
|> get(~p"/blog/engineering/hello-world")
|> html_response(200)
refute response =~ "post-status-banner"
end
test "unauthenticated user sees no banner on future post", %{conn: conn} do
response =
conn
|> get(~p"/blog/engineering/future-test-post")
|> html_response(200)
refute response =~ "post-status-banner"
end
end
end