Claude: changes for issue #98 #100

Merged
Grandiras merged 2 commits from claude/issue-98 into main 2026-08-18 02:45:55 +00:00
Collaborator

Closes #98.

Closes #98.
Guard card edits, and give comments editing and moderation
All checks were successful
CI / build (pull_request) Successful in 35s
41e79ae66f
A card could be rewritten or thrown away by anybody who could read the
board it was on. It now belongs to whoever made it — or to an admin, for
the reason admins see every board — and everybody else opens it to read:
the fields are text, there is no save button, and one note says why.

Services/CardPermissions.cs is the one place the rules live, taking the
same BoardViewer BoardAccess does, because "a DID, and whether it
administers this deployment" is the whole of what either question needs.
BoardAccess decides which boards you may see; this decides what you may
do to the things on one.

- Editing and deleting a card: its author, or an admin. Tags and
  assignees follow the card's rule, since they are saved by the same
  button — a guard that could be walked around by re-tagging is not one.
- Moving a card stays open to everybody. A drag changes which column a
  card is in and not a word of what it says.
- Commenting stays open to everybody, on any card.
- Editing a comment: its author, and nobody else, admins included.
  Moderation takes something down; it does not change what somebody is
  recorded as having said under their own name and avatar.
- Deleting a comment: its author, or an admin.

Enforced in the services rather than only in the markup — a
PermissionDeniedException the pages turn into translated copy — so a
stale circuit or a demotion mid-dialog cannot walk round a button that
is no longer drawn. Both pages and CommentList rebuild their viewer
where they reload their data, so a demotion lands on an open page.

Comment.EditedAt (migration AddCommentEditedAt, null for everything that
predates it) is stamped on a real change and shown beside the time: a
reply underneath may be answering words that are no longer there.

CardPermissionsTests pins the rule; tests/e2e/permissions.spec.js pins
what two members actually see.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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/267); 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-100/run-267.md) — updating every ~5 minutes while the task runs.
claude-reviewer approved these changes 2026-08-18 02:35:18 +00:00
Dismissed
claude-reviewer left a comment

Verdict: approve — no blocking issues found.

This implements per-card and per-comment permissions (issue #98) exactly in line with the codebase's established patterns: the rule lives once in CardPermissions (mirroring BoardAccess's single-expression philosophy), is enforced in the services (CardService, CommentService, TagService, MemberService) rather than only in markup, and the dialog (CardModal) purely renders the answer. Verified several load-bearing details:

  • BoardViewer.None.Did is string.Empty, and CardPermissions.IsSomebody correctly guards against an unidentified caller matching a card/comment whose author column is empty — pinned by CardPermissionsTests.Nobody_edits_anything.
  • The asymmetry (admin deletes but never edits a comment) is implemented and tested correctly, matching the stated moderation rationale.
  • _viewer/_modalCanEdit are recomputed on every reload in both BoardPage.razor and Calendar.razor, so a promotion/demotion under an open circuit is reflected without a reconnect — consistent with the rest of the app's revalidation model.
  • Service call-site signature changes (UpdateCardAsync, DeleteCardAsync, SetCardTagsAsync, SetCardAssigneesAsync, CommentService.DeleteAsync) are updated at every call site; no orphaned old-signature calls remain.
  • Comment.EditedAt is correctly left unencrypted (a clock reading, not user-supplied content), matching the encryption convention documented in CLAUDE.md.
  • Translation keys are all present and mirrored in en/de (modal.close used by the new Cancel/Close button already existed pre-PR, so nothing is missing there).
  • Both a unit-test file (CardPermissionsTests, covering cases no screen can reach) and a single consolidated e2e spec (permissions.spec.js, following the repo's "group by shared fixture" testing philosophy) back the change.

I did not find correctness or security bugs. The permission checks are done server-side as the ultimate gate, with the UI as a convenience layer only, so even a stale/tampered client can't bypass the rule.


⏱ 1m 55s · 8 turns · tokens: 111.4k in / 9.5k out (+939.0k cache-read)
📜 Full session transcript

✅ **Verdict: approve** — no blocking issues found. This implements per-card and per-comment permissions (issue #98) exactly in line with the codebase's established patterns: the rule lives once in `CardPermissions` (mirroring `BoardAccess`'s single-expression philosophy), is enforced in the services (`CardService`, `CommentService`, `TagService`, `MemberService`) rather than only in markup, and the dialog (`CardModal`) purely renders the answer. Verified several load-bearing details: - `BoardViewer.None.Did` is `string.Empty`, and `CardPermissions.IsSomebody` correctly guards against an unidentified caller matching a card/comment whose author column is empty — pinned by `CardPermissionsTests.Nobody_edits_anything`. - The asymmetry (admin deletes but never edits a comment) is implemented and tested correctly, matching the stated moderation rationale. - `_viewer`/`_modalCanEdit` are recomputed on every reload in both `BoardPage.razor` and `Calendar.razor`, so a promotion/demotion under an open circuit is reflected without a reconnect — consistent with the rest of the app's revalidation model. - Service call-site signature changes (`UpdateCardAsync`, `DeleteCardAsync`, `SetCardTagsAsync`, `SetCardAssigneesAsync`, `CommentService.DeleteAsync`) are updated at every call site; no orphaned old-signature calls remain. - `Comment.EditedAt` is correctly left unencrypted (a clock reading, not user-supplied content), matching the encryption convention documented in CLAUDE.md. - Translation keys are all present and mirrored in en/de (`modal.close` used by the new Cancel/Close button already existed pre-PR, so nothing is missing there). - Both a unit-test file (`CardPermissionsTests`, covering cases no screen can reach) and a single consolidated e2e spec (`permissions.spec.js`, following the repo's "group by shared fixture" testing philosophy) back the change. I did not find correctness or security bugs. The permission checks are done server-side as the ultimate gate, with the UI as a convenience layer only, so even a stale/tampered client can't bypass the rule. --- ⏱ 1m 55s · 8 turns · tokens: 111.4k in / 9.5k out (+939.0k cache-read) 📜 [Full session transcript](https://git.grandiras.net/Grandiras/claude-bot/src/branch/claude-logs/transcripts/Grandiras/Ponente/pr-100/run-267.md) <!-- claude-verdict:approve --> <!-- claude-reviewed-head:41e79ae66f7aaa7633af0d5ec12a2a71cb978549 -->
Owner

@claude-bot Remove the info banner at the top of cards, this is unnecessary.

@claude-bot Remove the info banner at the top of cards, this is unnecessary.
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/268); the reply will link the full session transcript. <!-- claude-run:comment --> 📜 [Live transcript](https://git.grandiras.net/Grandiras/claude-bot/src/branch/claude-logs/transcripts/Grandiras/Ponente/issue-100/run-268.md) — updating every ~5 minutes while the task runs.
Drop the read-only note from the card dialog
All checks were successful
CI / build (pull_request) Successful in 37s
15ebd5a733
A card somebody else made already reads as read-only without being told
so: the heading says Card rather than Edit card, the title and the
description are text, the pickers are labels and the footer has nothing
but Close. The banner over the top of all that was a paragraph saying
what the dialog underneath it was already showing, re-read on every open
of every card a member did not make.

Removed with its style, its two translations and the two assertions in
permissions.spec.js that were about it — everything else that spec pins
is the dialog itself, which is what actually carries the answer.

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

Removed the read-only note from the card dialog and pushed to claude/issue-98 (commit 15ebd5a), which updates PR #100.

What went:

  • CardModal.razor — the @if (!IsNew && !CanEdit) block rendering <p class="modal-note" data-readonly="card">, and its comment.
  • app.css — the .modal-note rule.
  • LocaleService.cs — the modal.readonly key in both en and de (key parity is enforced by LocaleServiceTests, so both had to go together).
  • permissions.spec.js — the two assertions that were about the banner (present on somebody else's card, absent on your own). Everything else that spec pins is the dialog itself.
  • CLAUDE.md — the sentence describing the note, replaced with why there isn't one: the dialog says it in the heading (modal.view — "Card" rather than "Edit card") and nowhere else.

The changelog entry needed no change; it never mentioned the note.

Verified: dotnet build Ponente.sln clean (one pre-existing Calendar.razor nullable warning, untouched by this), dotnet test Ponente.sln — 282/282 pass. I also ran the app for real (Postgres + dotnet run in Development, a second non-admin member seeded and signed in via /dev/login) and screenshotted the same card opened by someone who did not create it, before and after:

before after
Card dialog with the read-only banner above the title The same dialog with the banner removed

The heading, the static title, the read-only pickers and the lone Close button all still say read-only on their own — and the comment box below is still there, since commenting was never what the permission was about.


⏱ 7m 8s · 44 turns · tokens: 84.1k in / 12.0k out (+3873.1k cache-read)
📜 Full session transcript

Removed the read-only note from the card dialog and pushed to **`claude/issue-98`** (commit `15ebd5a`), which updates PR #100. **What went:** - `CardModal.razor` — the `@if (!IsNew && !CanEdit)` block rendering `<p class="modal-note" data-readonly="card">`, and its comment. - `app.css` — the `.modal-note` rule. - `LocaleService.cs` — the `modal.readonly` key in both `en` and `de` (key parity is enforced by `LocaleServiceTests`, so both had to go together). - `permissions.spec.js` — the two assertions that were about the banner (present on somebody else's card, absent on your own). Everything else that spec pins is the dialog itself. - `CLAUDE.md` — the sentence describing the note, replaced with why there isn't one: the dialog says it in the heading (`modal.view` — "Card" rather than "Edit card") and nowhere else. The changelog entry needed no change; it never mentioned the note. **Verified:** `dotnet build Ponente.sln` clean (one pre-existing `Calendar.razor` nullable warning, untouched by this), `dotnet test Ponente.sln` — 282/282 pass. I also ran the app for real (Postgres + `dotnet run` in Development, a second non-admin member seeded and signed in via `/dev/login`) and screenshotted the same card opened by someone who did not create it, before and after: | before | after | | --- | --- | | ![Card dialog with the read-only banner above the title](https://git.grandiras.net/attachments/973cfbef-163c-4fa3-9359-1b1981be2543) | ![The same dialog with the banner removed](https://git.grandiras.net/attachments/08dff0f0-5b34-48cd-a183-9b6edf2ffe90) | The heading, the static title, the read-only pickers and the lone Close button all still say read-only on their own — and the comment box below is still there, since commenting was never what the permission was about. --- ⏱ 7m 8s · 44 turns · tokens: 84.1k in / 12.0k out (+3873.1k cache-read) 📜 [Full session transcript](https://git.grandiras.net/Grandiras/claude-bot/src/branch/claude-logs/transcripts/Grandiras/Ponente/issue-100/run-268.md)
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/269); 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-100/run-269.md) — updating every ~5 minutes while the task runs.
claude-reviewer left a comment

Verdict: approve — no blocking issues found.

This is a re-review; no new commits changed the substance since my prior approval — the diff is identical in content. I re-verified the full PR branch directly (not just the diff) to check for anything the diff context might hide:

  • All call sites of CardService.UpdateCardAsync/DeleteCardAsync, TagService.SetCardTagsAsync, MemberService.SetCardAssigneesAsync, and CommentService.UpdateAsync/DeleteAsync across the whole branch (both BoardPage.razor and Calendar.razor) correctly pass the new BoardViewer actor/_viewer argument — no orphaned old-signature calls remain anywhere in the tree.
  • BoardViewer is the existing readonly record struct from BoardAccess.cs, reused as intended rather than duplicated.
  • AssigneePicker.Initials(member) (called from the new read-only assignee chips in CardModal.razor) exists and is public static, so that reference resolves.
  • CommentList.razor's _viewer is recomputed in ReloadAsync (which also runs on every BoardNotifier.CommentsChanged event and page reload), and canEdit/canDelete are recalculated per comment on every render, so permission state stays live under promotion/demotion exactly as documented.
  • Card creation (CreateCardAsync, which doesn't take an actor since creation is intentionally open) is followed by SetCardTagsAsync/SetCardAssigneesAsync using _viewer, and since a freshly created card's CreatedByDid is the creating member's own DID, CanEditCard correctly evaluates true for the same session.

No correctness or security issues found. Confirming approval from the previous pass.


⏱ 1m 17s · 21 turns · tokens: 104.7k in / 5.7k out (+2622.8k cache-read)
📜 Full session transcript

✅ **Verdict: approve** — no blocking issues found. This is a re-review; no new commits changed the substance since my prior approval — the diff is identical in content. I re-verified the full PR branch directly (not just the diff) to check for anything the diff context might hide: - All call sites of `CardService.UpdateCardAsync`/`DeleteCardAsync`, `TagService.SetCardTagsAsync`, `MemberService.SetCardAssigneesAsync`, and `CommentService.UpdateAsync`/`DeleteAsync` across the whole branch (both `BoardPage.razor` and `Calendar.razor`) correctly pass the new `BoardViewer actor`/`_viewer` argument — no orphaned old-signature calls remain anywhere in the tree. - `BoardViewer` is the existing `readonly record struct` from `BoardAccess.cs`, reused as intended rather than duplicated. - `AssigneePicker.Initials(member)` (called from the new read-only assignee chips in `CardModal.razor`) exists and is `public static`, so that reference resolves. - `CommentList.razor`'s `_viewer` is recomputed in `ReloadAsync` (which also runs on every `BoardNotifier.CommentsChanged` event and page reload), and `canEdit`/`canDelete` are recalculated per comment on every render, so permission state stays live under promotion/demotion exactly as documented. - Card creation (`CreateCardAsync`, which doesn't take an actor since creation is intentionally open) is followed by `SetCardTagsAsync`/`SetCardAssigneesAsync` using `_viewer`, and since a freshly created card's `CreatedByDid` is the creating member's own DID, `CanEditCard` correctly evaluates true for the same session. No correctness or security issues found. Confirming approval from the previous pass. --- ⏱ 1m 17s · 21 turns · tokens: 104.7k in / 5.7k out (+2622.8k cache-read) 📜 [Full session transcript](https://git.grandiras.net/Grandiras/claude-bot/src/branch/claude-logs/transcripts/Grandiras/Ponente/pr-100/run-269.md) <!-- claude-verdict:approve --> <!-- claude-reviewed-head:15ebd5a7337de5529bafeaabdd29da47e412beaa -->
Grandiras deleted branch claude/issue-98 2026-08-18 02:45:56 +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!100
No description provided.