Claude: changes for issue #68 #70

Merged
Grandiras merged 2 commits from claude/issue-68 into main 2026-08-08 01:21:52 +00:00
Collaborator

Closes #68.

Closes #68.
Lay the foundations for multiple boards
All checks were successful
CI / build (pull_request) Successful in 33s
23bf8d8d6d
A deployment now holds any number of boards, with no per-board access
rights: every allowlisted member sees every board, which is the shape
issue #68 asked for.

A board owns its columns and its tags. Cards deliberately carry no board
of their own — the column they sit in already answers the question — so
reads take a board id and writes find it from the column or the card.

The rework:

- `Board` entity plus `BoardId` on `BoardColumn` and `Tag`, with the
  AddBoards migration handing an existing board's columns and tags to a
  board it creates for them. The name goes in as plaintext because SQL
  inside a migration has no encryptor; EncryptionMigrator, which runs
  straight after MigrateAsync, seals it.
- The old `BoardService` (cards and columns) is now `CardService`, and
  `BoardService` is the boards themselves — list, default, CRUD. A page
  component named `Board` would shadow the entity in every razor file,
  so `Board.razor` is `BoardPage.razor`; its route is unchanged.
- `BoardNotifier.BoardChanged` carries a board id, nullable for the
  mutations that really do touch every board (a member's profile), plus
  a `BoardsChanged` event for the set of boards itself.
- Which board is a route parameter, never circuit state: `/board/{id}`,
  `/calendar/board/{id}`, `/admin/columns/{id}`, `/admin/tags/{id}`. The
  bare paths mean the default board, so every existing link still works.
- Deleting a board never deletes a card: the FKs are Restrict, and the
  service refuses a board holding cards, and the last board outright.
- A new board is seeded with To Do / Doing / Done so it is usable at
  once. Tag name uniqueness is scoped to a board.
- The nav header's Board link becomes a switcher once there is a second
  board, and keeps you on the page you were on. `/admin/boards` manages
  them; the column sort interop is parameterised to serve both lists.
- The calendar, its export and its subscribable feed all take an
  optional `board`, 404 on one that is gone, and name the board in
  X-WR-CALNAME.

Verified: dotnet build, dotnet test (157 passed), and the e2e suite
driven against a live app with Postgres — tests/e2e/boards.spec.js (6
new tests) plus board, calendar, filter, mobile, i18n and prefs specs.
The AddBoards backfill was applied to a database seeded at the previous
migration and checked row by row.
Author
Collaborator

🤖 Reviewing this PR — follow along in the action run; the reply will link the full session transcript.

📜 Live transcript — updating every ~5 minutes while the task runs.

🤖 Reviewing this PR — follow along in [the action run](https://git.grandiras.net/Grandiras/claude-bot/actions/runs/173); the reply will link the full session transcript. 📜 [Live transcript](https://git.grandiras.net/Grandiras/claude-bot/src/branch/claude-logs/transcripts/Grandiras/Ponente/pr-70/run-173.md) — updating every ~5 minutes while the task runs.
claude-reviewer left a comment

Verdict: approve — no blocking issues found.

This is a large, carefully executed multi-board migration and every piece I checked (not just the visible diff, but the full files on origin/claude/issue-68 for the parts truncated in the diff — CardService.cs, ColumnService.cs, TagService.cs, MemberService.cs, Program.cs, translations) is internally consistent with the stated design:

  • BoardNotifier.BoardChanged correctly threads int? through every subscriber (BoardPage.razor, Calendar.razor, Admin/Members.razor), and every mutating service picks the right notification: NotifyBoardChanged(boardId) for board-scoped writes, NotifyAllBoardsChanged() for member-profile changes that ripple across every board, NotifyBoardsChanged() for board list changes (create/rename/move/delete).
  • CardService.MoveCardAsync explicitly guards against a cross-board move and throws — matches the claim that no UI surface can trigger one.
  • TagService.SetCardTagsAsync re-resolves the card's board and filters tagIds against that board's own tags before writing, so a stale/cross-board id can't leak through.
  • The AddBoards migration correctly backfills existing BoardColumns/Tags into a single new Board row before the FKs are added, and EncryptionMigrator picks up the plaintext name written by the migration's raw SQL.
  • BoardService.DeleteAsync refuses to remove the last board and refuses a non-empty one, and only removes columns/tags (never cards) via Restrict FKs.
  • Route-parameter-driven board switching (OnParametersSetAsync with a _loaded/_loadedBoardId guard) is applied consistently across BoardPage.razor, Calendar.razor, Columns.razor, Tags.razor.
  • en/de translation key parity holds for every new key introduced.

Two very minor, non-blocking observations below — neither is worth holding up the merge for.


⏱ 3m 1s · 27 turns · tokens: 132.1k in / 13.3k out (+3486.3k cache-read)
📜 Full session transcript

✅ **Verdict: approve** — no blocking issues found. This is a large, carefully executed multi-board migration and every piece I checked (not just the visible diff, but the full files on `origin/claude/issue-68` for the parts truncated in the diff — `CardService.cs`, `ColumnService.cs`, `TagService.cs`, `MemberService.cs`, `Program.cs`, translations) is internally consistent with the stated design: - `BoardNotifier.BoardChanged` correctly threads `int?` through every subscriber (`BoardPage.razor`, `Calendar.razor`, `Admin/Members.razor`), and every mutating service picks the right notification: `NotifyBoardChanged(boardId)` for board-scoped writes, `NotifyAllBoardsChanged()` for member-profile changes that ripple across every board, `NotifyBoardsChanged()` for board list changes (create/rename/move/delete). - `CardService.MoveCardAsync` explicitly guards against a cross-board move and throws — matches the claim that no UI surface can trigger one. - `TagService.SetCardTagsAsync` re-resolves the card's board and filters `tagIds` against that board's own tags before writing, so a stale/cross-board id can't leak through. - The `AddBoards` migration correctly backfills existing `BoardColumns`/`Tags` into a single new `Board` row before the FKs are added, and `EncryptionMigrator` picks up the plaintext name written by the migration's raw SQL. - `BoardService.DeleteAsync` refuses to remove the last board and refuses a non-empty one, and only removes columns/tags (never cards) via `Restrict` FKs. - Route-parameter-driven board switching (`OnParametersSetAsync` with a `_loaded`/`_loadedBoardId` guard) is applied consistently across `BoardPage.razor`, `Calendar.razor`, `Columns.razor`, `Tags.razor`. - en/de translation key parity holds for every new key introduced. Two very minor, non-blocking observations below — neither is worth holding up the merge for. --- ⏱ 3m 1s · 27 turns · tokens: 132.1k in / 13.3k out (+3486.3k cache-read) 📜 [Full session transcript](https://git.grandiras.net/Grandiras/claude-bot/src/branch/claude-logs/transcripts/Grandiras/Ponente/pr-70/run-173.md) <!-- claude-verdict:approve --> <!-- claude-reviewed-head:23bf8d8d6d4fb067982e0ce50f827f4ea2eb9910 -->
@ -124,0 +138,4 @@
var board = new Board { Name = "Board", Position = 1.0, CreatedAt = DateTime.UtcNow };
db.Boards.Add(board);
await db.SaveChangesAsync(ct);
Collaborator

Minor: the db.BoardColumns.AnyAsync(ct) early-return is effectively unreachable now — this method only runs when Boards is empty, and the AddBoards migration guarantees any pre-existing BoardColumns already got a board (and thus a Boards row) before this code path can run. Not a bug, just vestigial defensive code from before boards existed.

Minor: the `db.BoardColumns.AnyAsync(ct)` early-return is effectively unreachable now — this method only runs when `Boards` is empty, and the `AddBoards` migration guarantees any pre-existing `BoardColumns` already got a board (and thus a `Boards` row) before this code path can run. Not a bug, just vestigial defensive code from before boards existed.
@ -84,3 +103,3 @@
private static void ValidateCardContent(string title, string descriptionMarkdown)
public async Task<Board> CreateAsync(string name, CancellationToken ct = default)
{
Collaborator

CreateAsync persists the new board and its three seed columns via two separate SaveChangesAsync calls with no surrounding transaction. An interruption between them would leave a board with no columns (and DatabaseInitializer's seeding guard only fires when the whole Boards table is empty, so nothing would repair it). Low-likelihood edge case; wrapping both writes in one transaction (or building the columns before the first save) would close the window.

`CreateAsync` persists the new board and its three seed columns via two separate `SaveChangesAsync` calls with no surrounding transaction. An interruption between them would leave a board with no columns (and `DatabaseInitializer`'s seeding guard only fires when the whole `Boards` table is empty, so nothing would repair it). Low-likelihood edge case; wrapping both writes in one transaction (or building the columns before the first save) would close the window.
Owner

@claude-bot Please work on the review feedback

@claude-bot Please work on the review feedback
Author
Collaborator

🤖 On it — follow along in the action run; the reply will link the full session transcript.

📜 Live transcript — updating every ~5 minutes while the task runs.

🤖 On it — follow along in [the action run](https://git.grandiras.net/Grandiras/claude-bot/actions/runs/174); the reply will link the full session transcript. 📜 [Live transcript](https://git.grandiras.net/Grandiras/claude-bot/src/branch/claude-logs/transcripts/Grandiras/Ponente/issue-70/run-174.md) — updating every ~5 minutes while the task runs.
Make a new board and its columns one write
All checks were successful
CI / build (pull_request) Successful in 37s
1a2473d31d
BoardService.CreateAsync saved the board, then its three seed columns, in
two SaveChangesAsync calls with nothing around them. An interruption in
between leaves a board with no columns and nothing that repairs it —
DatabaseInitializer only seeds when the Boards table is empty, and by
then it is not. Hanging the columns off the board's own navigation makes
one save write both inside one transaction.

DatabaseInitializer's seeding had the same split, plus a second guard on
BoardColumns that can no longer fire: past the guard on Boards there is
no board, and since AddBoards a column cannot exist without one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Author
Collaborator

Both review notes were valid; both are fixed and pushed to claude/issue-68 (commit 1a2473d).

BoardService.CreateAsync — the two-save window (BoardService.cs:116)

The board and its three seed columns went in as two SaveChangesAsync calls with nothing around them, and the reviewer is right that nothing repairs the gap: DatabaseInitializer's seeding only fires when the Boards table is empty, which after the first save it isn't. Rather than open an explicit transaction I took the other option and built the columns first — they hang off board.Columns, so a single SaveChangesAsync writes the board and its columns together in EF's own implicit transaction, and there is no second call to fail after the first. EF fills in BoardId from the relationship, so nothing else changes.

DatabaseInitializer.SeedDefaultBoardAsync — the unreachable guard (DatabaseInitializer.cs:141)

Confirmed unreachable, for the reason given: the AddBoards migration inserts a Boards row whenever any BoardColumns or Tags exist and does it before the FKs go on, so a database with columns always arrives here with a board, and the guard above has already returned. Removed, with the reasoning written into the remarks so the next reader doesn't restore it as a safety net. That method had the same two-save split as CreateAsync, so it got the same treatment — the columns are now part of the board's initializer and go in with it.

Verified: dotnet build Ponente.sln clean (one pre-existing CS8604 warning in Calendar.razor, untouched by this change), dotnet test Ponente.sln — 157 passed, 0 failed. I could not run the Playwright suite: this container has neither podman nor docker, so there is no stack to point it at. Both changes are on startup/admin-create paths that the e2e suite exercises only indirectly (every run boots a seeded stack), and neither alters what any page renders.


⏱ 1m 53s · 16 turns · tokens: 52.5k in / 5.7k out (+885.6k cache-read)
📜 Full session transcript

Both review notes were valid; both are fixed and pushed to `claude/issue-68` (commit `1a2473d`). **`BoardService.CreateAsync` — the two-save window** (`BoardService.cs:116`) The board and its three seed columns went in as two `SaveChangesAsync` calls with nothing around them, and the reviewer is right that nothing repairs the gap: `DatabaseInitializer`'s seeding only fires when the `Boards` table is empty, which after the first save it isn't. Rather than open an explicit transaction I took the other option and built the columns first — they hang off `board.Columns`, so a single `SaveChangesAsync` writes the board and its columns together in EF's own implicit transaction, and there is no second call to fail after the first. EF fills in `BoardId` from the relationship, so nothing else changes. **`DatabaseInitializer.SeedDefaultBoardAsync` — the unreachable guard** (`DatabaseInitializer.cs:141`) Confirmed unreachable, for the reason given: the `AddBoards` migration inserts a `Boards` row whenever any `BoardColumns` or `Tags` exist and does it *before* the FKs go on, so a database with columns always arrives here with a board, and the guard above has already returned. Removed, with the reasoning written into the remarks so the next reader doesn't restore it as a safety net. That method had the same two-save split as `CreateAsync`, so it got the same treatment — the columns are now part of the board's initializer and go in with it. **Verified:** `dotnet build Ponente.sln` clean (one pre-existing `CS8604` warning in `Calendar.razor`, untouched by this change), `dotnet test Ponente.sln` — 157 passed, 0 failed. I could not run the Playwright suite: this container has neither podman nor docker, so there is no stack to point it at. Both changes are on startup/admin-create paths that the e2e suite exercises only indirectly (every run boots a seeded stack), and neither alters what any page renders. --- ⏱ 1m 53s · 16 turns · tokens: 52.5k in / 5.7k out (+885.6k cache-read) 📜 [Full session transcript](https://git.grandiras.net/Grandiras/claude-bot/src/branch/claude-logs/transcripts/Grandiras/Ponente/issue-70/run-174.md)
Grandiras deleted branch claude/issue-68 2026-08-08 01:21:53 +00:00
Grandiras referenced this pull request from a commit 2026-08-08 01:21:54 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
3 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Grandiras/Ponente!70
No description provided.