diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index b372ebf..39f5146 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -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-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-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-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"} diff --git a/app/lib/firehose_web/controllers/blog_controller.ex b/app/lib/firehose_web/controllers/blog_controller.ex index 8e243e1..aa07a62 100644 --- a/app/lib/firehose_web/controllers/blog_controller.ex +++ b/app/lib/firehose_web/controllers/blog_controller.ex @@ -22,11 +22,14 @@ defmodule FirehoseWeb.BlogController do def show(conn, %{"slug" => slug}) do blog = conn.assigns.blog post = blog.get_post!(slug) + visibility = Blogex.Post.visibility(post) render(conn, :show, page_title: post.title, post: post, - base_path: blog.base_path() + base_path: blog.base_path(), + visibility: visibility, + authenticated: conn.assigns[:current_user] != nil ) end diff --git a/app/lib/firehose_web/controllers/blog_html/show.html.heex b/app/lib/firehose_web/controllers/blog_html/show.html.heex index 28cb722..8f4313b 100644 --- a/app/lib/firehose_web/controllers/blog_html/show.html.heex +++ b/app/lib/firehose_web/controllers/blog_html/show.html.heex @@ -1,4 +1,23 @@
← Back to posts + + <%= if @authenticated and @visibility == :draft do %> +
+ Draft — not published +
+ <% end %> + + <%= if @authenticated and @visibility == :scheduled do %> +
+ This post is scheduled for {Calendar.strftime(@post.date, "%B %d, %Y")} +
+ <% end %> + <.post_show post={@post} base_path={@base_path} />
diff --git a/app/priv/blog/engineering/2099/01-01-future-test-post.md b/app/priv/blog/engineering/2099/01-01-future-test-post.md new file mode 100644 index 0000000..792be17 --- /dev/null +++ b/app/priv/blog/engineering/2099/01-01-future-test-post.md @@ -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. diff --git a/app/test/firehose_web/controllers/blog_controller_test.exs b/app/test/firehose_web/controllers/blog_controller_test.exs new file mode 100644 index 0000000..658cb70 --- /dev/null +++ b/app/test/firehose_web/controllers/blog_controller_test.exs @@ -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