Finish board settings, members and the profile in the UI language v2 #255

Merged
Grandiras merged 3 commits from ui-v2-settings into main 2026-10-04 16:38:21 +00:00
Owner

Board settings, Members and the profile in the UI language v2: the draft's grouped lists, saving as you go, Undo instead of Save buttons, and the phone's drill-in.

One grouped-list system in app.css ("Pages of grouped lists": .page/.page-head/.page-crumb/.page-title, .group/.group-label/.group-box/.group-row/.group-note, input.switch with role=switch, input.swatch, .chip, small and flat buttons, ⋯ row menus), plus Components/MemberFace.razor (picture, or initials on a DID-hashed colour) and Components/Settings/TrashGlyph.razor. The old admin cards, tables and choice cards are gone; the super-admin's board record uses the same lists.

Board settings has a crumb, the large "Board settings" title and segmented tabs, and no title bar, as in the draft.

  • General: the name saves when you leave the field, a "Pin to the top" switch (BoardService.IsPinnedAsync), and the delete row (soft delete with Undo).
  • Columns: an add row, rename on leave, card counts, a Deck chip on the last of two or more columns, one-click delete with Undo (refused while a column holds cards), drag to reorder (now SortableJS's clone, and rows are redrawn after a drop, which fixes a DOM-order mismatch on a second drag).
  • Tags: swatch, name, chip preview, count and trash, all saving on change; delete with Undo.
  • Access: radios, a member filter, and rows with checkbox, face, name and handle ("· you") and an Admin switch. Every change applies at once as a step whose Undo is the inverse step, queued rather than disabling the page. A last-admin refusal shows a toast and redraws the row, and the page reloads on BoardsChanged.
  • New helpers: ColumnService/TagService.CardCountsAsync, and UndoService keys for columns, tags and requests.

Members shows "N members · M waiting", an Add a member lookup row, and access requests with face, name, handle or email, chips, date and message. Their Deny / Approve / Approve and invite are deferred through UndoService with Undo, and an unresolvable or banned requester is refused first. Invites show code, chip, copy link, Disable, "+ Create invite" and "for {name}". Current members have a ⋯ menu with Ban…, Remove and Lift ban.

Profile is grouped lists: the name saves on leave, the picture on pick, and the "where it applies" radio writes nothing to the network by itself.

Phone (<600px): a drill-in. General is a list under the board's phone bar (name and pin, then Columns, Tags and Access rows, then a red Delete row). The sub-pages have a bar of their own (‹ Settings, the title, Share), with red-minus delete, a three-line handle, an add row closing the list, trailing ticks, and an Admin tag on Access.

Judgement calls

  • The phone's swipe-to-make-admin is a tappable Admin tag instead.
  • The profile's network group is one row: refreshing and "use my Atmosphere profile" are the same act.
  • App copy is kept where the draft shows demo text ("…folds into a deck", not "Done deck").
  • Undoing a revoke gives a fresh GrantedAt.
  • A late Undo says "Too late to undo".
  • Approve and deny land after the Undo window, about 7 s.

Docs: ui.md (a new Settings, members and your profile: grouped lists section, Undo, the title bar), boards-and-permissions.md (access applies at once, pin, delete), auth.md (profile), access-requests.md (deferred answers), and frontend.md (controls, drag, the v2 status line; the old Admin — Members · redesign canvas frame is marked superseded). design/trace/capture.mjs no longer clicks the one-click delete. The changelog line is folded into the save-as-you-go bullet.

Verified: tools/verify.sh (600 unit tests). The full Playwright suite passed 161/161 on an isolated stack, on this exact tree stacked on the board and Home PRs. The access, permissions, requests, moderation and profile specs were reworked for the new flows. Checked by hand against a seeded dev app, light and dark, desktop and phone: rename, delete and Undo for columns and tags; access grant, revoke, admin, visibility and Undo, including the last-admin refusal; ban and lift through the member menu; deny and Undo on a request; drag order.

🤖 Generated with Claude Code

Board settings, Members and the profile in the UI language v2: the draft's grouped lists, saving as you go, Undo instead of Save buttons, and the phone's drill-in. **One grouped-list system** in app.css ("Pages of grouped lists": `.page/.page-head/.page-crumb/.page-title`, `.group/.group-label/.group-box/.group-row/.group-note`, `input.switch` with role=switch, `input.swatch`, `.chip`, small and flat buttons, ⋯ row menus), plus `Components/MemberFace.razor` (picture, or initials on a DID-hashed colour) and `Components/Settings/TrashGlyph.razor`. The old admin cards, tables and choice cards are gone; the super-admin's board record uses the same lists. **Board settings** has a crumb, the large "Board settings" title and segmented tabs, and no title bar, as in the draft. - **General:** the name saves when you leave the field, a "Pin to the top" switch (`BoardService.IsPinnedAsync`), and the delete row (soft delete with Undo). - **Columns:** an add row, rename on leave, card counts, a Deck chip on the last of two or more columns, one-click delete with Undo (refused while a column holds cards), drag to reorder (now SortableJS's clone, and rows are redrawn after a drop, which fixes a DOM-order mismatch on a second drag). - **Tags:** swatch, name, chip preview, count and trash, all saving on change; delete with Undo. - **Access:** radios, a member filter, and rows with checkbox, face, name and handle ("· you") and an Admin switch. Every change applies at once as a step whose Undo is the inverse step, queued rather than disabling the page. A last-admin refusal shows a toast and redraws the row, and the page reloads on `BoardsChanged`. - New helpers: `ColumnService`/`TagService.CardCountsAsync`, and `UndoService` keys for columns, tags and requests. **Members** shows "N members · M waiting", an Add a member lookup row, and access requests with face, name, handle or email, chips, date and message. Their Deny / Approve / Approve and invite are deferred through `UndoService` with Undo, and an unresolvable or banned requester is refused first. Invites show code, chip, copy link, Disable, "+ Create invite" and "for {name}". Current members have a ⋯ menu with Ban…, Remove and Lift ban. **Profile** is grouped lists: the name saves on leave, the picture on pick, and the "where it applies" radio writes nothing to the network by itself. **Phone (<600px):** a drill-in. General is a list under the board's phone bar (name and pin, then Columns, Tags and Access rows, then a red Delete row). The sub-pages have a bar of their own (‹ Settings, the title, Share), with red-minus delete, a three-line handle, an add row closing the list, trailing ticks, and an Admin tag on Access. **Judgement calls** - The phone's swipe-to-make-admin is a tappable Admin tag instead. - The profile's network group is one row: refreshing and "use my Atmosphere profile" are the same act. - App copy is kept where the draft shows demo text ("…folds into a deck", not "Done deck"). - Undoing a revoke gives a fresh GrantedAt. - A late Undo says "Too late to undo". - Approve and deny land after the Undo window, about 7 s. Docs: `ui.md` (a new *Settings, members and your profile: grouped lists* section, Undo, the title bar), `boards-and-permissions.md` (access applies at once, pin, delete), `auth.md` (profile), `access-requests.md` (deferred answers), and `frontend.md` (controls, drag, the v2 status line; the old *Admin — Members · redesign* canvas frame is marked superseded). `design/trace/capture.mjs` no longer clicks the one-click delete. The changelog line is folded into the save-as-you-go bullet. **Verified:** `tools/verify.sh` (600 unit tests). The full Playwright suite passed 161/161 on an isolated stack, on this exact tree stacked on the board and Home PRs. The access, permissions, requests, moderation and profile specs were reworked for the new flows. Checked by hand against a seeded dev app, light and dark, desktop and phone: rename, delete and Undo for columns and tags; access grant, revoke, admin, visibility and Undo, including the last-admin refusal; ban and lift through the member menu; deny and Undo on a request; drag order. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
One grouped-list system (a large title, then groups: a label, a rounded box
of rows split by hairlines, a footnote) for a board's four settings pages,
/admin/members, /profile and the board record, light and dark, as the v2
Figma frames draw them.

- Nothing has a Save button: names save as the field is left, switches,
  radios and colours on the change, a picture on the pick. A new "Pin to
  the top" switch on General.
- Columns and tags delete in one click and wait out an Undo toast
  (UndoService); a column with cards cannot go. Rows show their cards, the
  last column a Deck chip. The drag lifts a clone and redraws the rows
  after a drop, which fixes a second drag landing one place off.
- Access applies each change at once, as a step with the inverse step for
  Undo; changes queue, a refused one redraws its row, and the page reloads
  on BoardsChanged.
- Members follows its frame: no DIDs on screen, faces, a menu per member,
  invites with copy and status, and approving or denying a request waits
  out an Undo toast because an approval mails and lets the person in.
- The profile's target radios write nothing by themselves.
- On a phone the settings drill in: General is a list under the board's
  own top, each other tab a page with its own bar.
- Settings no longer show the board's title bar, as on the canvas.

Docs updated where behaviour changed (ui, boards-and-permissions, auth,
access-requests, frontend) and the e2e specs reworked for it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Say in the changelog what settings, members and the profile became
All checks were successful
CI / build (pull_request) Successful in 43s
867fb2faa0
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Collaborator

🤖 Reviewing this PR — the review will link the full session transcript.

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

🤖 Reviewing this PR — the review will link the full session transcript. <!-- opencode-run:review --> 📜 [Live transcript](https://git.grandiras.net/Grandiras/opencode-bot/src/branch/opencode-logs/transcripts/Grandiras/Ponente/pr-255/run-2026-10-04T15-58-09-228Z.md) — updating every ~5 minutes while the review runs.
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/703); the reply will link the full session transcript. <!-- claude-run:pr_review --> 📜 [Live transcript](https://git.grandiras.net/Grandiras/claude-bot/src/branch/claude-logs/transcripts/Grandiras/Ponente/pr-255/run-703.md) — updating every ~5 minutes while the task runs.
claude-reviewer approved these changes 2026-10-04 15:58:39 +00:00
Dismissed
claude-reviewer left a comment

✅ Verdict: approve — no blocking issues found.

I reviewed this from a truncated diff, so the tail was not visible. That covers the remaining BoardSettings markup, the columns, tags and UndoService code, the CSS, the tests and the locale entries. The visible half hangs together.

  • Access page: steps with inverse-step Undo, a queue instead of disabling the ticks, and a _generation key to redraw refused rows are sound. The LastBoardAdminException path is handled.
  • Requests: deferring approve and deny behind UndoService, with the resolve and ban checks done before the toast, fits the stated reasoning. The docs were updated alongside, as CLAUDE.md requires.
  • Profile: a target switch that writes nothing is a sensible safeguard.
  • Race in RequestsPanel: the MemberBannedException catch was dropped from the deferred approval. A ban landing inside the roughly 7 s window would surface as the generic "already answered" toast rather than the banned message. This is minor and unlikely.
  • Whole-state writes: ApplyAsync reads the access state and writes the whole thing back, so two admins editing at the same moment can overwrite each other. The SetAccessAsync rules still hold, and the old Save button had the same exposure, so this is acceptable.

🧠 claude-sonnet-5-5 · ⏱ 0m 16s · 2 turns · tokens: 65.9k in / 1.9k out (+12.2k cache-read)
📜 Full session transcript

✅ **Verdict: approve** — no blocking issues found. I reviewed this from a truncated diff, so the tail was not visible. That covers the remaining BoardSettings markup, the columns, tags and `UndoService` code, the CSS, the tests and the locale entries. The visible half hangs together. - **Access page:** steps with inverse-step Undo, a queue instead of disabling the ticks, and a `_generation` key to redraw refused rows are sound. The `LastBoardAdminException` path is handled. - **Requests:** deferring approve and deny behind `UndoService`, with the resolve and ban checks done before the toast, fits the stated reasoning. The docs were updated alongside, as CLAUDE.md requires. - **Profile:** a target switch that writes nothing is a sensible safeguard. - **Race in `RequestsPanel`:** the `MemberBannedException` catch was dropped from the deferred approval. A ban landing inside the roughly 7 s window would surface as the generic "already answered" toast rather than the banned message. This is minor and unlikely. - **Whole-state writes:** `ApplyAsync` reads the access state and writes the whole thing back, so two admins editing at the same moment can overwrite each other. The `SetAccessAsync` rules still hold, and the old Save button had the same exposure, so this is acceptable. --- 🧠 claude-sonnet-5-5 · ⏱ 0m 16s · 2 turns · tokens: 65.9k in / 1.9k out (+12.2k cache-read) 📜 [Full session transcript](https://git.grandiras.net/Grandiras/claude-bot/src/branch/claude-logs/transcripts/Grandiras/Ponente/pr-255/run-703.md) <!-- claude-verdict:approve --> <!-- claude-reviewed-head:867fb2faa0092b3a04b161c86231172f76a1df63 -->
opencode-reviewer left a comment

💬 Verdict: comment — the reviewer did not submit a structured review; the raw output follows.

I've reviewed the diff and the touched components/services. Let me check one last detail — how toast actions run relative to the access write queue:


⏱ 30m 3s · 38 steps · $0.1453 · tokens: 408.4k in / 1.6k out (+1878.8k cache-read) · Go: 5h 5% · wk 14% · mo 90%
📜 Full session transcript

💬 **Verdict: comment** — the reviewer did not submit a structured review; the raw output follows. I've reviewed the diff and the touched components/services. Let me check one last detail — how toast actions run relative to the access write queue: --- ⏱ 30m 3s · 38 steps · $0.1453 · tokens: 408.4k in / 1.6k out (+1878.8k cache-read) · Go: 5h 5% · wk 14% · mo 90% 📜 [Full session transcript](https://git.grandiras.net/Grandiras/opencode-bot/src/branch/opencode-logs/transcripts/Grandiras/Ponente/pr-255/run-2026-10-04T15-58-09-228Z.md)
Author
Owner

47232d9: an access Undo now goes through the same write queue as the changes (UndoAsync). Each step reads and writes the whole access state, so an Undo pressed beside a change still in flight could have written back the state from before that change. opencode's review ran out of time just as it got to this.

Not changed:

  • A ban landing inside a request's Undo window shows the generic "already answered" toast instead of the banned one. Before the toast, the approval checks for a ban, and a ban landing in the next ~7 s still fails the approval safely and brings the request back. Only the wording is generic. Fixing it would mean letting UndoService.Defer pick a message per exception, for a very narrow race.
  • Concurrent admins: whole-state writes can still overwrite each other between two admins, as claude-reviewer says, the same as the old Save button.

@opencode /review

47232d9: an access Undo now goes through the same write queue as the changes (`UndoAsync`). Each step reads and writes the whole access state, so an Undo pressed beside a change still in flight could have written back the state from before that change. opencode's review ran out of time just as it got to this. Not changed: - **A ban landing inside a request's Undo window** shows the generic "already answered" toast instead of the banned one. Before the toast, the approval checks for a ban, and a ban landing in the next ~7 s still fails the approval safely and brings the request back. Only the wording is generic. Fixing it would mean letting `UndoService.Defer` pick a message per exception, for a very narrow race. - **Concurrent admins:** whole-state writes can still overwrite each other between two admins, as claude-reviewer says, the same as the old Save button. @opencode /review
Queue an access Undo behind the writes in flight
All checks were successful
CI / build (pull_request) Successful in 42s
47232d91d3
Each change reads and writes the whole access state, so an Undo running beside
one could write back the state before it. From review.

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

🤖 Reviewing this PR — the review will link the full session transcript.

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

🤖 Reviewing this PR — the review will link the full session transcript. <!-- opencode-run:review --> 📜 [Live transcript](https://git.grandiras.net/Grandiras/opencode-bot/src/branch/opencode-logs/transcripts/Grandiras/Ponente/pr-255/run-2026-10-04T16-30-53-688Z.md) — updating every ~5 minutes while the review runs.
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/707); the reply will link the full session transcript. <!-- claude-run:pr_review --> 📜 [Live transcript](https://git.grandiras.net/Grandiras/claude-bot/src/branch/claude-logs/transcripts/Grandiras/Ponente/pr-255/run-707.md) — updating every ~5 minutes while the task runs.
claude-reviewer left a comment

✅ Verdict: approve — no blocking issues found.

The latest commit, "Queue an access Undo behind the writes in flight", fixes a race I would otherwise have raised. UndoAsync now takes the same _writing semaphore as ChangeAsync. An Undo that overlaps a change in flight can no longer write back the state from before that change.

My earlier notes were non-blocking, and I haven't re-verified them:

  • MemberBannedException in the deferred approval: the catch is still absent in RequestsPanel.razor. A ban landing inside the roughly 7 s window would show the generic "already answered" toast instead of the banned message. This is unlikely and minor.
  • Whole-state writes on the access page: two admins editing at the same moment can still overwrite each other. The old Save button had the same exposure.

I have not re-read the truncated tail of the diff since the first pass. Nothing here blocks merging.


🧠 claude-sonnet-5-5 · ⏱ 0m 8s · 3 turns · tokens: 66.9k in / 574 out (+90.9k cache-read)
📜 Full session transcript

✅ **Verdict: approve** — no blocking issues found. The latest commit, "Queue an access Undo behind the writes in flight", fixes a race I would otherwise have raised. `UndoAsync` now takes the same `_writing` semaphore as `ChangeAsync`. An Undo that overlaps a change in flight can no longer write back the state from before that change. My earlier notes were non-blocking, and I haven't re-verified them: - **`MemberBannedException` in the deferred approval:** the catch is still absent in `RequestsPanel.razor`. A ban landing inside the roughly 7 s window would show the generic "already answered" toast instead of the banned message. This is unlikely and minor. - **Whole-state writes on the access page:** two admins editing at the same moment can still overwrite each other. The old Save button had the same exposure. I have not re-read the truncated tail of the diff since the first pass. Nothing here blocks merging. --- 🧠 claude-sonnet-5-5 · ⏱ 0m 8s · 3 turns · tokens: 66.9k in / 574 out (+90.9k cache-read) 📜 [Full session transcript](https://git.grandiras.net/Grandiras/claude-bot/src/branch/claude-logs/transcripts/Grandiras/Ponente/pr-255/run-707.md) <!-- claude-verdict:approve --> <!-- claude-reviewed-head:47232d91d3c56fef4693e65a91824c655fbcc581 -->
opencode-reviewer left a comment

✅ Verdict: approve — no blocking issues found.

Re-review. My previous pass left one open question — how a toast's Undo runs relative to the access page's write queue — and it is resolved: toast actions run through the circuit dispatcher (ToastContainer.RunAsync), and the access page's UndoAsync joins the same _writing semaphore as the changes, so an Undo beside an in-flight change queues instead of writing back the state before it. Nothing blocking remains; the non-blockers from earlier passes stand as noted (a ban landing inside a request's Undo window shows the generic "already handled" failure toast rather than the banned message; whole-state writes can lose a concurrent admin's edit, same exposure as the old Save button).

The rest of the change checks out against the code: the access steps and _generation redraw, the deferred request answers with the resolve and banned checks done before the toast, UndoService's claim race ("Too late to undo") and its failure path that settles the key so a hidden item comes back, the columns page's _moves-keyed redraw after a drop, the pin switch backed by real IsPinnedAsync/SetPinnedAsync, the board delete's Undo toast surviving the navigate-home because ToastContainer is global, and every new locale key present in both languages. Docs and the trace script were updated alongside, as CLAUDE.md requires.

Minor, non-blocking, in the findings: the restore-on-refusal of the text fields (board, column and tag renames) can leave the field showing the emptied or failed text, since Blazor skips writing a value equal to the one it last rendered — the very behaviour the access page works around with _generation and the Columns page documents. Relatedly, PinAsync flips the switch optimistically and doesn't redraw it on a failed write. Both are rare failure paths and don't lose data.


⏱ 6m 49s · 13 steps · $0.0554 · tokens: 236.3k in / 2.1k out (+479.1k cache-read) · Go: 5h 6% · wk 14% · mo 90%
📜 Full session transcript

✅ **Verdict: approve** — no blocking issues found. Re-review. My previous pass left one open question — how a toast's Undo runs relative to the access page's write queue — and it is resolved: toast actions run through the circuit dispatcher (`ToastContainer.RunAsync`), and the access page's `UndoAsync` joins the same `_writing` semaphore as the changes, so an Undo beside an in-flight change queues instead of writing back the state before it. Nothing blocking remains; the non-blockers from earlier passes stand as noted (a ban landing inside a request's Undo window shows the generic "already handled" failure toast rather than the banned message; whole-state writes can lose a concurrent admin's edit, same exposure as the old Save button). The rest of the change checks out against the code: the access steps and `_generation` redraw, the deferred request answers with the resolve and banned checks done before the toast, `UndoService`'s claim race ("Too late to undo") and its failure path that settles the key so a hidden item comes back, the columns page's `_moves`-keyed redraw after a drop, the pin switch backed by real `IsPinnedAsync`/`SetPinnedAsync`, the board delete's Undo toast surviving the navigate-home because `ToastContainer` is global, and every new locale key present in both languages. Docs and the trace script were updated alongside, as CLAUDE.md requires. Minor, non-blocking, in the findings: the restore-on-refusal of the text fields (board, column and tag renames) can leave the field showing the emptied or failed text, since Blazor skips writing a value equal to the one it last rendered — the very behaviour the access page works around with `_generation` and the Columns page documents. Relatedly, `PinAsync` flips the switch optimistically and doesn't redraw it on a failed write. Both are rare failure paths and don't lose data. --- ⏱ 6m 49s · 13 steps · $0.0554 · tokens: 236.3k in / 2.1k out (+479.1k cache-read) · Go: 5h 6% · wk 14% · mo 90% 📜 [Full session transcript](https://git.grandiras.net/Grandiras/opencode-bot/src/branch/opencode-logs/transcripts/Grandiras/Ponente/pr-255/run-2026-10-04T16-30-53-688Z.md) <!-- opencode-reviewed-head:47232d91d3c56fef4693e65a91824c655fbcc581 --> <!-- opencode-verdict:approve -->
@ -50,0 +142,4 @@
var name = _name.Trim();
if (name.Length == 0 || name == _board.Name)
{
_name = _board.Name;
Collaborator

This restores _name after a refused or emptied rename, but the input may keep showing what the user typed (or an emptied field): Blazor skips writing an attribute equal to the value it last rendered — the exact skip the access page works around with _generation and the comment in Columns.ReloadAsync documents. Same for the restore in Columns.RenameAsync (_edits[columnId] = current) and Tags.SaveAsync (draft with { Name = tag.Name }). The model is right, so data is safe; only the field can look wrong until something else re-renders. A generation counter in those rows' @key, as on the access page, would close it.

This restores `_name` after a refused or emptied rename, but the input may keep showing what the user typed (or an emptied field): Blazor skips writing an attribute equal to the value it last rendered — the exact skip the access page works around with `_generation` and the comment in `Columns.ReloadAsync` documents. Same for the restore in `Columns.RenameAsync` (`_edits[columnId] = current`) and `Tags.SaveAsync` (`draft with { Name = tag.Name }`). The model is right, so data is safe; only the field can look wrong until something else re-renders. A generation counter in those rows' `@key`, as on the access page, would close it.
Collaborator

🔧 Let opencode-bot fix this — tick the box and opencode-bot picks up the review above, on this PR branch. Nothing runs until you do.

  • @opencode-bot Address the findings of the latest review on this pull request.
🔧 **Let opencode-bot fix this** — tick the box and opencode-bot picks up the review above, on this PR branch. Nothing runs until you do. - [ ] @opencode-bot Address the findings of the latest review on this pull request. <!-- opencode-fix-button:255 -->
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
5 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!255
No description provided.