Lay the foundations for multi-board support #68

Closed
opened 2026-08-06 17:23:36 +00:00 by Grandiras · 2 comments
Owner

Rework the backend to support multiple boards (for now without separate access rights)

Rework the backend to support multiple boards (for now without separate access rights)
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/170); 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-68/run-170.md) — updating every ~5 minutes while the task runs.
Collaborator

The work is done and pushed. Here's my reply for the issue:


Reworked the backend for multiple boards — branch claude/issue-68.

A deployment now holds any number of boards, with no per-board access rights: every allowlisted member sees every board, and every admin can edit every one, which is what the issue asked for. Where rights would go when they land is written down in CLAUDE.md (a join table beside Board, and BoardService.ResolveAsync as the one place that decides what somebody without access sees).

The model. A board owns its columns and its tags. Cards deliberately carry no board of their own — the column they sit in already answers that, and a second copy of the answer is a second thing to keep in step — so reads take a board id and writes find it from the column or the card.

What changed

  • New Board entity, BoardId on BoardColumn and Tag, and an AddBoards migration that backfills: an existing installation's columns and tags are handed to a board created for them before the foreign keys go on. Its name goes in as plaintext because SQL inside a migration has no encryptor — EncryptionMigrator (now with a Boards entry) seals it at the next startup.
  • The old BoardService (cards, columns, the board query) is now CardService; BoardService is the boards themselves — list, default, create/rename/reorder/delete. A page component named Board shadows the entity in every razor file, so Board.razor is now BoardPage.razor — the route (/) is unchanged.
  • BoardNotifier.BoardChanged now carries a board id so a circuit only reloads for its own board; it is nullable for the mutations that genuinely touch every board (a member's avatar or display name), and there is a separate BoardsChanged for the set of boards.
  • 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; a stale id renders "this board no longer exists" rather than silently substituting another board's cards.
  • Deleting a board never deletes a card: the FKs are Restrict all the way down, and the service refuses a board that still holds cards (the admin page shows the count) and refuses the last board outright.
  • A new board is seeded with To Do / Doing / Done so it is usable the moment it exists. Tag-name uniqueness is scoped per board.
  • UI to go with it, kept minimal: the nav's "Board" link becomes a switcher once there is a second board (and keeps you on the calendar if that's where you were), /admin/boards manages them with drag-reorder — the first is the default — and the columns/tags admin pages get a board picker. The column sort interop is parameterised rather than copied. New strings in both locales.
  • The calendar, its .ics download and the subscribable feed all take an optional board, 404 on one that has gone, and name the board in X-WR-CALNAME.

What I verified. dotnet build clean; dotnet test 157 passed (3 new IcsCalendarTests, and the en/de key-parity test covers the new strings). Then, because the interesting parts are only visible at runtime, I stood up Postgres and ran the real app: applied the schema at the previous migration, seeded legacy columns/tags/cards, ran AddBoards, and checked the backfilled BoardIds row by row. Against that live app I ran Playwright: the new tests/e2e/boards.spec.js (6 tests: a new board is usable and its own page, cards don't leak between boards, delete refuses while cards remain, the nav switcher, per-board tags and admin pages, a dead board link, and the export/feed naming their board) plus board, calendar, filter, mobile, i18n and prefs — 97 passed on a clean board. Three did not: two board-spec flakes that pass in isolation and on a re-run, and one calendar test that needs a dated card in the next month, which my throwaway database had none of. I also checked the six-week month and stored locale failures against unmodified main on the same machine — they fail there too (accumulated tags making the legend two rows tall; a cookie hard-coded to localhost:8090), so they aren't from this change.

Two existing calendar assertions needed updating, since the export href legitimately carries board= now, and global-teardown.js now reclaims boards a run created (and their columns, which carry no timestamp of their own). CLAUDE.md has a new Multiple boards section plus the updates the rename and the notifier change imply.


⏱ 63m 11s · 210 turns · tokens: 349.7k in / 144.5k out (+49438.6k cache-read)
📜 Full session transcript
→ Opened PR: #70

The work is done and pushed. Here's my reply for the issue: --- Reworked the backend for multiple boards — branch **`claude/issue-68`**. A deployment now holds any number of boards, with **no per-board access rights**: every allowlisted member sees every board, and every admin can edit every one, which is what the issue asked for. Where rights would go when they land is written down in `CLAUDE.md` (a join table beside `Board`, and `BoardService.ResolveAsync` as the one place that decides what somebody without access sees). **The model.** A board owns its columns and its tags. Cards deliberately carry no board of their own — the column they sit in already answers that, and a second copy of the answer is a second thing to keep in step — so reads take a board id and writes find it from the column or the card. **What changed** - New `Board` entity, `BoardId` on `BoardColumn` and `Tag`, and an `AddBoards` migration that backfills: an existing installation's columns and tags are handed to a board created for them *before* the foreign keys go on. Its name goes in as plaintext because SQL inside a migration has no encryptor — `EncryptionMigrator` (now with a `Boards` entry) seals it at the next startup. - The old `BoardService` (cards, columns, the board query) is now **`CardService`**; `BoardService` is the boards themselves — list, default, create/rename/reorder/delete. A page component named `Board` shadows the entity in every razor file, so `Board.razor` is now **`BoardPage.razor`** — the route (`/`) is unchanged. - `BoardNotifier.BoardChanged` now carries a board id so a circuit only reloads for its own board; it is *nullable* for the mutations that genuinely touch every board (a member's avatar or display name), and there is a separate `BoardsChanged` for the set of boards. - 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; a stale id renders "this board no longer exists" rather than silently substituting another board's cards. - **Deleting a board never deletes a card**: the FKs are `Restrict` all the way down, and the service refuses a board that still holds cards (the admin page shows the count) and refuses the last board outright. - A new board is seeded with To Do / Doing / Done so it is usable the moment it exists. Tag-name uniqueness is scoped per board. - UI to go with it, kept minimal: the nav's "Board" link becomes a switcher once there is a second board (and keeps you on the calendar if that's where you were), `/admin/boards` manages them with drag-reorder — the first is the default — and the columns/tags admin pages get a board picker. The column sort interop is parameterised rather than copied. New strings in **both** locales. - The calendar, its `.ics` download and the subscribable feed all take an optional `board`, 404 on one that has gone, and name the board in `X-WR-CALNAME`. **What I verified.** `dotnet build` clean; `dotnet test` 157 passed (3 new `IcsCalendarTests`, and the en/de key-parity test covers the new strings). Then, because the interesting parts are only visible at runtime, I stood up Postgres and ran the real app: applied the schema at the *previous* migration, seeded legacy columns/tags/cards, ran `AddBoards`, and checked the backfilled `BoardId`s row by row. Against that live app I ran Playwright: the new `tests/e2e/boards.spec.js` (6 tests: a new board is usable and its own page, cards don't leak between boards, delete refuses while cards remain, the nav switcher, per-board tags and admin pages, a dead board link, and the export/feed naming their board) plus `board`, `calendar`, `filter`, `mobile`, `i18n` and `prefs` — 97 passed on a clean board. Three did not: two board-spec flakes that pass in isolation and on a re-run, and one calendar test that needs a dated card in the *next* month, which my throwaway database had none of. I also checked the `six-week month` and `stored locale` failures against **unmodified `main`** on the same machine — they fail there too (accumulated tags making the legend two rows tall; a cookie hard-coded to `localhost:8090`), so they aren't from this change. Two existing calendar assertions needed updating, since the export href legitimately carries `board=` now, and `global-teardown.js` now reclaims boards a run created (and their columns, which carry no timestamp of their own). `CLAUDE.md` has a new **Multiple boards** section plus the updates the rename and the notifier change imply. --- ⏱ 63m 11s · 210 turns · tokens: 349.7k in / 144.5k out (+49438.6k cache-read) 📜 [Full session transcript](https://git.grandiras.net/Grandiras/claude-bot/src/branch/claude-logs/transcripts/Grandiras/Ponente/issue-68/run-170.md) → Opened PR: https://git.grandiras.net/Grandiras/Ponente/pulls/70
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
2 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#68
No description provided.