diff --git a/app/lib/firehose_web/components/layouts/app.html.heex b/app/lib/firehose_web/components/layouts/app.html.heex index 5412d25..5d99688 100644 --- a/app/lib/firehose_web/components/layouts/app.html.heex +++ b/app/lib/firehose_web/components/layouts/app.html.heex @@ -23,6 +23,9 @@
  • Contact
  • +
  • + Dashboard +
  • <.theme_toggle />
  • diff --git a/app/test/firehose_web/controllers/page_controller_test.exs b/app/test/firehose_web/controllers/page_controller_test.exs index ea981ed..06e1374 100644 --- a/app/test/firehose_web/controllers/page_controller_test.exs +++ b/app/test/firehose_web/controllers/page_controller_test.exs @@ -1,9 +1,32 @@ defmodule FirehoseWeb.PageControllerTest do use FirehoseWeb.ConnCase - test "GET /", %{conn: conn} do - body = conn |> get(~p"/") |> html_response(200) - assert body =~ "Drinking from the firehose" - assert body =~ "Willem van den Ende" + describe "GET /" do + test "renders the homepage", %{conn: conn} do + body = conn |> get(~p"/") |> html_response(200) + assert body =~ "Drinking from the firehose" + assert body =~ "Willem van den Ende" + end + + test "shows navigation links", %{conn: conn} do + body = conn |> get(~p"/") |> html_response(200) + assert body =~ ~s|/blog/engineering| + assert body =~ ~s|/blog/releases| + assert body =~ ~s|/contact| + end + + test "does not show dashboard link when unauthenticated", %{conn: conn} do + body = conn |> get(~p"/") |> html_response(200) + refute body =~ ~s|/editor/dashboard| + end + end + + describe "GET / when authenticated" do + setup :register_and_log_in_user + + test "shows dashboard link in navigation", %{conn: conn} do + body = conn |> get(~p"/") |> html_response(200) + assert body =~ ~s|/editor/dashboard| + end end end diff --git a/investigation/context.md b/investigation/context.md new file mode 100644 index 0000000..3923920 --- /dev/null +++ b/investigation/context.md @@ -0,0 +1,77 @@ +# Codebase Context for Investigation + +## Project Structure +- **app/** - Phoenix LiveView application with blogs, user auth, editor dashboard +- **blogex/** - Multi-blog engine library (Blogex) with NimblePublisher-based markdown posts + +## Key Modules + +### Blog Config (app/lib/firehose/blogs/) +- `Firehose.EngineeringBlog` - blog_id: :engineering, base_path: "/blog/engineering" +- `Firehose.ReleaseNotes` - blog_id: :release_notes, base_path: "/blog/releases" + +### Blog Post System (blogex/lib/blogex/) +- `Blogex.Post` - struct with fields: id, title, author, body, description, date, blog, image, tags, published +- `Post.visibility(post)` returns :draft (published=false), :scheduled (future date), :live +- `Post.days_until_live(post)` returns days until scheduled post goes live +- Posts are compiled at build-time from markdown files in priv/blog/ + +### Blog Module (blogex/lib/blogex/blog.ex) +- `Blogex.Blog` - macro that uses NimblePublisher +- `all_posts()` - returns published, non-future-dated posts (drafts excluded in prod) +- `unfiltered_posts()` - returns ALL posts including drafts and future-dated +- `recent_posts(n)` - returns n most recent published posts +- `paginate(page, per_page)` - pagination support + +### Registry (blogex/lib/blogex/registry.ex) +- `Blogex.Registry.all_posts()` - all published posts across blogs +- `Blogex.Registry.all_posts_unfiltered()` - all posts including drafts/scheduled +- `Blogex.Registry.get_blog!(blog_id)` - get blog module by atom + +### Router (app/lib/firehose_web/router.ex) +- GET / - PageController.home (homepage with recent posts) +- GET /blog/:blog_id - BlogController.index (blog listing) +- GET /blog/:blog_id/:slug - BlogController.show (single post) +- GET /blog/:blog_id/tag/:tag - BlogController.tag (posts by tag) +- GET /api/blog/engineering - Blogex.Router (serves HTML+JSON+RSS) +- GET /api/blog/releases - Blogex.Router (serves HTML+JSON+RSS) +- LiveView: GET /editor/dashboard - EditorDashboardLive (authenticated) +- LiveView: GET /microprints - MicroprintsLive + +### Blogex Router (blogex/lib/blogex/router.ex) +- Serves at /api/blog/{engineering,releases} +- GET /feed.xml - RSS 2.0 feed +- GET /atom.xml - Atom feed +- These are mounted under /api/blog/* so feeds are at /api/blog/engineering/feed.xml + +### Blog Controller (app/lib/firehose_web/controllers/blog_controller.ex) +- Renders blog index, show, and tag pages using Phoenix templates with Blogex.Components +- Uses the Phoenix layout (app.html.heex) with full UI + +### Editor Dashboard (app/lib/firehose_web/live/editor_dashboard_live.ex) +- Authenticated LiveView at /editor/dashboard +- Shows Drafts and Scheduled posts tabs +- Uses Blogex.Registry.all_posts_unfiltered() +- Has a tab switcher between drafts and scheduled +- Each post shows title, author, date, status + +### Layout/UI (app/lib/firehose_web/components/layouts/app.html.heex) +- Navbar with links to: Home, Blog, Releases, QWAN (external), Contact, theme toggle +- No RSS icon/link visible in nav +- No link to Editor Dashboard in nav (only accessible via direct URL) + +### Homepage (app/lib/firehose_web/controllers/page_html/home.html.heex) +- Shows intro text and recent posts grid (6 most recent across both blogs) + +### Email/Mailer +- `Firehose.Mailer` uses Swoosh with gen_smtp adapter +- `Firehose.Accounts` has user registration, login, password change +- `Firehose.Accounts.UserNotifier` for email notifications +- No subscription/newsletter functionality exists + +### Mix Dependencies +- phoenix_live_view ~> 1.1.0 +- swoosh ~> 1.16 (email library) +- gen_smtp ~> 1.0 (SMTP adapter) +- ecto_sql ~> 3.13, postgrex +- blogex (path dependency, local) \ No newline at end of file diff --git a/investigation/question-1-drafts-scheduled-list.md b/investigation/question-1-drafts-scheduled-list.md new file mode 100644 index 0000000..68c9242 --- /dev/null +++ b/investigation/question-1-drafts-scheduled-list.md @@ -0,0 +1,153 @@ +# Question 1: "Is there a way to see all drafts and scheduled posts in a list?" + +## Answer Summary + +Yes — an editor dashboard exists at `/editor/dashboard` showing all drafts and scheduled posts, but it is **not linked from the navigation**. Only users who know the URL can access it. + +Public routes (index, homepage, tag pages) never show drafts/scheduled in production. However, authenticated users can view individual draft/scheduled posts **by direct URL** if they know the slug — the show template detects visibility and shows a status banner. + +--- + +## 1. Existing Views/Routes for Drafts & Scheduled Posts + +### Editor Dashboard (LiveView) — `/editor/dashboard` + +- **File:** `app/lib/firehose_web/live/editor_dashboard_live.ex` +- **Route:** `live "/editor/dashboard", EditorDashboardLive` (router.ex line 87) +- **Auth:** Requires authenticated user (`:require_authenticated_user` pipe, line 84) +- **Data source:** `Blogex.Registry.all_posts_unfiltered()` — returns ALL posts regardless of published/date +- **Features:** + - Two tabs: Drafts (published=false) and Scheduled (future date) + - Count badges per tab + - Shows title, author, date, status badge, and days-until-live for scheduled posts + - Links each post to its public URL + +### BlogController.show — individual draft/scheduled post viewing + +- **File:** `app/lib/firehose_web/controllers/blog_controller.ex` +- **Route:** `GET /blog/:blog_id/:slug` (router.ex line 38) +- **Auth:** No auth middleware — but show template only shows status banners when authenticated +- **Data source:** `blog.get_post!()` (line 22) uses `unfiltered_posts()` — returns ANY post by slug, including drafts/scheduled +- **Show template** (`app/lib/firehose_web/controllers/blog_html/show.html.heex`, lines 6-22): + - Shows a draft banner (amber) only when `@authenticated` and visibility is `:draft` + - Shows a scheduled banner (blue) only when `@authenticated` and visibility is `:scheduled` + +### BlogController.index / tag — do NOT show drafts/scheduled + +- Uses `blog.paginate()` / `blog.posts_by_tag()` which call `all_posts()` — filtered in production +- See section 3 for details + +--- + +## 2. Is the Editor Dashboard linked from the main navigation? + +**No.** The navbar in `app/lib/firehose_web/components/layouts/app.html.heex` (lines 1-23) contains: + +| Link | Target | +|------|--------| +| Blog | `/blog/engineering` | +| Releases | `/blog/releases` | +| QWAN | `https://qwan.eu` (external) | +| Contact | `/contact` | +| Theme toggle | inline JS | + +There are **no user/authentication-related links at all** — no login, logout, settings, or dashboard link. The layout does not reference `@current_scope` or any authenticated-user assigns. + +--- + +## 3. Do any public routes show draft or scheduled posts? + +| Route | Uses | Shows drafts/scheduled? | +|-------|------|------------------------| +| `GET /` (homepage) | `recent_posts(3)` → `all_posts()` | ❌ No (filtered) | +| `GET /blog/:blog_id` (index) | `paginate()` → `all_posts()` | ❌ No (filtered) | +| `GET /blog/:blog_id/tag/:tag` | `posts_by_tag()` → `all_posts()` | ❌ No (filtered) | +| `GET /blog/:blog_id/:slug` (show) | `get_post!()` → `unfiltered_posts()` | ✅ Yes — any authenticated user can view via direct URL, with status banner | + +### Filtering logic (in `Blogex.Blog`, `app/lib/firehose/blog/blog.ex` lines 85-90): + +```elixir +def all_posts do + today = Date.utc_today() + if Blogex.show_drafts?() do + Enum.filter(@posts, &(not Date.after?(&1.date, today))) + else + Enum.filter(@posts, &(&1.published and not Date.after?(&1.date, today))) + end +end +``` + +- **Dev mode** (`config/config.exs` line 78): `show_drafts: true` — includes drafts (published=false) but excludes future-dated (scheduled) +- **Prod mode** (`config/prod.exs` line 17): `show_drafts: false` — only live posts visible + +**Important gap:** `all_posts()` in dev includes drafts but NOT scheduled. The editor dashboard (`all_posts_unfiltered()`) handles both. + +### Show template control flow (show.html.heex lines 4-22): + +```heex +<%= if @authenticated and @visibility == :draft do %> +
    Draft — not published
    +<% end %> + +<%= if @authenticated and @visibility == :scheduled do %> +
    This post is scheduled for ...
    +<% end %> +``` + +The `@authenticated` flag comes from `conn.assigns[:current_scope]` (BlogController line 33): +```elixir +authenticated: !!(conn.assigns[:current_scope] && conn.assigns.current_scope.user) +``` + +--- + +## 4. What would need to change to add a link to the editor dashboard in the main nav? + +### Single-file change: `app/lib/firehose_web/components/layouts/app.html.heex` + +The app layout receives `@current_scope` from the `fetch_current_scope_for_user` plug in the browser pipeline (router.ex line 16). Currently it doesn't use it. + +**Required addition:** A conditional link inside the `