Skip to content

Rewrite lessons 1-4 against real code, and revise the plan - #1

Open
ephbaum wants to merge 6 commits into
mainfrom
claude/modernization-implementation-puv702
Open

ephbaum wants to merge 6 commits into
mainfrom
claude/modernization-implementation-puv702

Conversation

@ephbaum

@ephbaum ephbaum commented Sep 2, 2026

Copy link
Copy Markdown
Owner

Lessons 1-4 are now written against code that exists and passes its tests, and the sixteen-lesson outline has been audited against that code.

The implementation is in ephbaum/elxrBB#1. Every command and snippet in these lessons was run, not assumed.

The lessons

Lesson 1 — Setting Up the Environment. Phoenix 1.8.13 on Elixir 1.20 / OTP 28. Uses the official elixir-lang install script, drops the Node.js step (1.8 builds assets with standalone esbuild and Tailwind binaries), and explains why the project is generated as elxrbb --module ElxrBB — an OTP application name cannot be mixed case, which is what the 2023 attempt tripped over.

Lesson 2 — User Accounts (renamed from "User Authentication with Pow"). Built on mix phx.gen.auth: scopes, magic links, sudo mode. Then elxrBB's own material — the username generator built from this repository's word lists, case-insensitive uniqueness via a functional index, and the profile form. The Pow draft is preserved in docs/archive/original-lessons/ for reference.

Lesson 3 — Forums, Topics and Replies. Replaces the planned flat /topics + /replies CRUD with the nested routes a forum actually needs. Covers the aggregate subquery that produces reply counts without an N+1 or a counter column, why route declaration order matters, why ownership is checked at mount and on save, and how a stream-backed list has to track its own count.

Lesson 4 — Threaded Replies, Sub-Topics and Voting (new; merges old lessons 4 and 5). The self-referencing foreign key and why depth is stored rather than derived, the check constraints that keep that denormalization honest, building a tree in two queries instead of one per level, and why sibling ordering needs an id tiebreak or the page reorders itself between renders. For voting: one table with two nullable targets and an exclusive-or check constraint, partial unique indexes and why they must be partial, and summing scores rather than keeping a counter column.

Each lesson calls out a bug hit while building it — the nil-intolerant String.trim/1 in update_change/3, the aggregate that loses its Ecto type crossing a subquery, a username generator that produced names its own schema rejected, and a duplicate DOM id the test suite caught. These are the parts a reader learns most from.

The plan audit

The original outline was drafted before any code existed. The spine holds — contexts, then the forum domain, then threading, moderation, deployment — but several specifics do not survive contact with the application. docs/00a-outline.md now carries the revised plan and a Revisions to this plan section explaining each change:

  1. Lessons 4 and 5 were the same lesson. "Threaded replies" and "sub-topics" both describe a reply with a parent_id. Merged.
  2. Real-time moved from lesson 11 to lesson 5, and changed technology. The plan reached for Phoenix Channels; a LiveView broadcasts over Phoenix.PubSub and receives in handle_info/2. Channels are for clients that are not LiveViews. Leaving it at 11 also means six lessons where a posted reply is invisible to everyone but its author.
  3. Pagination and search were missing entirely. Every list query in the application is unbounded today — fine with five topics, an outage with fifty thousand, and the largest gap between finishing the tutorial and having a working forum. New lesson 6.
  4. Testing moved earlier and narrowed. The application has had tests since lesson 2, so a lesson introducing testing at the end would describe a project that does not exist.
  5. The animal-names lesson is dropped. Lesson 2 loads those word lists at compile time; putting them in a table and exposing them to JavaScript adds nothing. The JS-hook material moves to lesson 8, where a Markdown preview gives it a purpose.
  6. Payments, SMS and browser push became appendices. A step needing a vendor account, a card and a public webhook endpoint is not one most readers can take. The roles and permissions work they were bundled with is real and stays, as lesson 9.

Also: old lesson 6 splits (profiles shipped in lesson 2; avatars and private messaging become lesson 7, with file storage behind a behaviour rather than one vendor named in the title), and rich text narrows to Markdown alone with sanitization as the point rather than a footnote.

Sixteen lessons became fifteen plus four optional appendices.

STATUS.md

Now reports lesson status and implementation status separately, and lists the application's known gaps — no pagination, no real-time, no search, plain-text bodies, unrestricted forum management, no rate limiting — so they read as scheduled rather than overlooked.

README

Gains a table of contents and a statement of what the stack actually is. The original account of the ChatGPT collaboration is kept intact, under "Where this came from".

🤖 Generated with Claude Code

https://claude.ai/code/session_01QKEb38aDmzRFVxFnM6Fe3X


Generated by Claude Code

The lessons now describe code that exists in ephbaum/elxrBB rather than
code that was planned. Every command and snippet was run.

Lesson 1: Phoenix 1.8.13 on Elixir 1.20 / OTP 28. Uses the official
elixir-lang install script, drops the Node.js step (1.8 builds assets with
standalone esbuild and Tailwind binaries), and explains why the project is
generated as `elxrbb --module ElxrBB` — an OTP application name cannot be
mixed case, which is what the 2023 attempt tripped over. `--live` is noted
as redundant rather than removed, since the flag is still accepted.

Lesson 2: rewritten around `mix phx.gen.auth` instead of Pow, and renamed
to "User Accounts". Pow has had no release since January 2025 and predates
the 1.8 layout and component conventions. The Pow draft moves to
docs/archive/original-lessons/02-user-authentication-pow.md. The new lesson
covers scopes, magic links and sudo mode, then adds elxrBB's own material:
the username generator built from this repo's word lists, case-insensitive
uniqueness via a functional index, and the profile form.

Lesson 3: forums, topics and replies, replacing the planned flat
`/topics` + `/replies` CRUD with the nested routes a forum actually needs.
Covers the aggregate subquery that produces reply counts without an N+1 or
a counter column, why route declaration order matters, why ownership is
checked at mount and again on save, and how a stream-backed reply list has
to track its own count.

Each lesson calls out a bug that was hit while building: the nil-intolerant
`String.trim/1` in `update_change/3`, the aggregate that loses its Ecto type
crossing a subquery, and a username generator that could produce names its
own schema rejected.

STATUS.md now reports lessons and implementation separately, records the
three deviations from the original plan, and lists what is next. The README
gains a table of contents; the original account of the ChatGPT collaboration
is kept as "Where this came from".

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QKEb38aDmzRFVxFnM6Fe3X
Lesson 2 opened by arguing against a library rather than explaining the one
it uses. It now states the choice — `mix phx.gen.auth` — and moves on. The
outline entry, the status note and the archive pointer follow suit.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QKEb38aDmzRFVxFnM6Fe3X
Audits the original sixteen-lesson outline now that lessons 1-3 exist. The
spine holds; several specifics do not.

- Lessons 4 and 5 describe one feature — a reply with a parent_id — and are
  merged.
- Real-time moves from lesson 11 to lesson 5 and switches from Phoenix
  Channels to PubSub. A LiveView receives broadcasts in handle_info/2;
  Channels are for clients that are not LiveViews. Leaving it at lesson 11
  also means six lessons where a posted reply is invisible to everyone but
  its author.
- Pagination and search were absent from the plan entirely. Every list query
  in the application is unbounded today, which is the largest gap between
  finishing the tutorial and having a working forum. New lesson 6.
- Testing moves earlier and narrows: the application has had tests since
  lesson 2, so lesson 13 covers what we have not used rather than
  introducing the idea.
- The animal-names lesson is dropped. Lesson 2 loads those word lists at
  compile time; putting them in a table and exposing them to JavaScript adds
  nothing. The JS-hook material moves to lesson 8 as a Markdown preview.
- Payments, SMS and browser push become appendices C and D. A step needing a
  vendor account, a card and a public webhook endpoint is not one most
  readers can take, and the roles work it was bundled with is real.
- Old lesson 6 splits: profiles already shipped in lesson 2, avatars and
  private messaging become lesson 7, and file storage sits behind a
  behaviour rather than naming one vendor in the title.
- Rich text narrows to Markdown alone, with sanitization as the point of the
  lesson rather than a footnote.

STATUS.md now lists the application's known gaps — no pagination, no
real-time, no search, plain-text bodies, unrestricted forum management, no
rate limiting — so they read as scheduled rather than overlooked.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QKEb38aDmzRFVxFnM6Fe3X
Written against the implementation in ephbaum/elxrBB. Merges what the
outline had as lessons 4 and 5, since "threaded replies" and "sub-topics"
are the same feature.

Covers the self-referencing foreign key and why depth is stored rather than
derived, the two check constraints that keep the denormalization honest,
building a tree in two queries instead of one per level, and why sibling
ordering needs an id tiebreak or the page reorders itself between renders.

For voting: one table with two nullable targets and an exclusive-or check
constraint, partial unique indexes and why they must be partial, summing
scores rather than keeping a counter column, and the pinned-value clause
that makes casting the same vote twice take it back.

The web section covers recursive function components, the duplicate DOM id
the test suite caught, the deliberate trade of a LiveView stream for a plain
assign now that replies are a tree, and three habits for event payloads:
parse rather than trust, check the id addresses something in scope, and
prefer the non-raising getter.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QKEb38aDmzRFVxFnM6Fe3X
ephbaum and others added 2 commits September 5, 2026 00:11
The lessons quote the application's source and nothing verified the quotes.
An audit of lessons 1-4 against the code found the quotes accurate but the
comparison itself unreliable: checking a lesson against the application's
HEAD reports drift wherever a later lesson changed the code on purpose.
Lesson 3 shows replies as a flat LiveView stream and lesson 4 replaces that
with a tree, so lesson 3 looks broken against HEAD and is not.

So pin each lesson to the commit that concludes it, in lessons.exs, and
check its blocks against that tree. Blocks name their source file in the
fence. Comparison ignores whitespace and whole-line comments, so a lesson
may re-indent an excerpt, reflow it to the page width, or leave out a
comment it explains in prose; it may not change a name, a literal or the
structure. A bare `...`, or a comment mentioning `...`, elides code the
lesson is not showing.

27 of 39 source blocks are annotated and pass. The remaining 12 condense
their source without marking the elision, which needs a judgement call per
block; --strict fails on unannotated blocks so coverage can be ratcheted.

Also correct lesson 1: it said Phoenix 1.8 needs Elixir 1.15 or newer, but
this application's mix.exs requires 1.17.

Add CI to run the check on push, on pull request, and weekly, since the
application can drift without this repository changing.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QKEb38aDmzRFVxFnM6Fe3X
Pushing to a PR branch fired both the push and pull_request triggers, so
every commit ran the same check twice. Branches are covered by
pull_request; keep push for merges to main.

Bump actions/checkout to v5 while here -- v4 targets Node 20, which the
runners now force onto Node 24 with a deprecation warning.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QKEb38aDmzRFVxFnM6Fe3X
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants