Claude: changes for issue #160 #161

Merged
Grandiras merged 1 commit from claude/issue-160 into main 2026-08-26 20:16:31 +00:00
Collaborator

Closes #160.

Closes #160.
Add a profile that only Ponente shows
All checks were successful
CI / build (pull_request) Successful in 33s
6107f021d9
A profile change on /profile went to the member's app.bsky.actor.profile
record, which is the point of the Atmosphere — and was also the page's only
offer. A name and a picture chosen for the people on one private board are not
always the ones somebody wants under a post on Bluesky, so the page now asks
which of the two a save is about, per save, with the network one still the
default.

The Ponente-only target writes the Members row and stops: no token, no scope,
no record, no account touched. It therefore also works on a deployment
configured AtProto:ProfileScope=none and for a member who signed in before this
app started asking — which is why the "sign in again" sentence now sits under a
disabled option rather than in place of the whole form.

It is the same two columns rather than a second pair, so every screen that draws
a face keeps reading what it always read; Member.UsesLocalProfile says whose
answer they are, and is what MemberService.RefreshProfileAsync, the startup
backfill and /dev/login now check before overwriting them. The picture has no
repository to live in, so it goes into a new MemberAvatar table (encrypted like
everything else, no navigation property so it can never ride along with a member
read) and is served by /avatar/{did} behind the allowlist, at a URL carrying a
version that changes with the picture.

The way back is the refresh button, which for a member holding a local profile
reads "use my Atmosphere profile" instead — for them the two are the same act.

Also generalises the board-access option cards to .choice-list/.choice-option,
since the profile page is now the second thing that needs that shape.

profile.spec.js reaches a save end to end for the first time: the local target
needs no token, so the dev sign-in can set a name and a picture, watch the nav
header take them, fetch the picture back off /avatar/{did}, and check both
survive the next sign-in.
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/388); 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-161/run-388.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 constructed feature (local/Ponente-only profiles alongside the existing Atmosphere-profile editing) and it holds together well under close reading.

What I verified:

  • ProfileService.LoadAsync correctly short-circuits to the local profile before creating an AT Proto client at all when UsesLocalProfile is set, matching the docs' claim that the repository is never read in that case.
  • The RefreshProfileAsync guard (if (member.UsesLocalProfile) return;) and the parallel guard inline in RefreshCachedCopyAsync/SaveAsync correctly prevent any lookup-driven path from clobbering a local profile, and the flag is set/cleared only in SetProfileAsync, which is the one path that's supposed to change it.
  • SaveLocalAsync's "unchanged" check only fires when already local (member.UsesLocalProfile && ...), so switching from network to local with a matching name/avatar correctly still writes and flips the flag rather than reporting Unchanged — consistent with the code comment.
  • DatabaseInitializer's backfill filters !UsesLocalProfile in SQL before materializing, which is legitimate since that column is deliberately left unencrypted for exactly this reason.
  • /avatar/{did} is behind the same allowlist policy as the rest of the board, consistent with the stated design (any allowlisted member may view another member's avatar, same as everywhere else in the app).
  • The EF ValueConverter/ValueComparer pair for MemberAvatar.Bytes is correctly wired to avoid the reference-equality re-encrypt-on-every-save trap the comment describes.
  • Migration, snapshot, and designer files are consistent with the model changes; the CSS rename (access-* → choice-*) is applied consistently across BoardAccess.razor, app.css, and access.spec.js.
  • Docs (auth.md, data.md, orientation.md) and the localization tables were updated in both locales for every new string, matching CLAUDE.md's conventions.

I found nothing to block on. One very minor, non-blocking observation: the ValueComparer<byte[]> hash function in KanbanDbContext.cs combines every byte individually via Aggregate, which is O(n) over up to a 1 MB avatar on every save-change-tracking pass — cheap relative to the AES-GCM work already happening on the same bytes, so not worth blocking on, just worth knowing if avatar size limits ever grow.


⏱ 1m 59s · 9 turns · tokens: 82.1k in / 10.6k out (+810.7k cache-read)
📜 Full session transcript

✅ **Verdict: approve** — no blocking issues found. This is a large, carefully constructed feature (local/Ponente-only profiles alongside the existing Atmosphere-profile editing) and it holds together well under close reading. What I verified: - `ProfileService.LoadAsync` correctly short-circuits to the local profile before creating an AT Proto client at all when `UsesLocalProfile` is set, matching the docs' claim that the repository is never read in that case. - The `RefreshProfileAsync` guard (`if (member.UsesLocalProfile) return;`) and the parallel guard inline in `RefreshCachedCopyAsync`/`SaveAsync` correctly prevent any lookup-driven path from clobbering a local profile, and the flag is set/cleared only in `SetProfileAsync`, which is the one path that's supposed to change it. - `SaveLocalAsync`'s "unchanged" check only fires when already local (`member.UsesLocalProfile && ...`), so switching from network to local with a matching name/avatar correctly still writes and flips the flag rather than reporting Unchanged — consistent with the code comment. - `DatabaseInitializer`'s backfill filters `!UsesLocalProfile` in SQL before materializing, which is legitimate since that column is deliberately left unencrypted for exactly this reason. - `/avatar/{did}` is behind the same allowlist policy as the rest of the board, consistent with the stated design (any allowlisted member may view another member's avatar, same as everywhere else in the app). - The EF `ValueConverter`/`ValueComparer` pair for `MemberAvatar.Bytes` is correctly wired to avoid the reference-equality re-encrypt-on-every-save trap the comment describes. - Migration, snapshot, and designer files are consistent with the model changes; the CSS rename (`access-*` → `choice-*`) is applied consistently across `BoardAccess.razor`, `app.css`, and `access.spec.js`. - Docs (`auth.md`, `data.md`, `orientation.md`) and the localization tables were updated in both locales for every new string, matching CLAUDE.md's conventions. I found nothing to block on. One very minor, non-blocking observation: the `ValueComparer<byte[]>` hash function in `KanbanDbContext.cs` combines every byte individually via `Aggregate`, which is O(n) over up to a 1 MB avatar on every save-change-tracking pass — cheap relative to the AES-GCM work already happening on the same bytes, so not worth blocking on, just worth knowing if avatar size limits ever grow. --- ⏱ 1m 59s · 9 turns · tokens: 82.1k in / 10.6k out (+810.7k cache-read) 📜 [Full session transcript](https://git.grandiras.net/Grandiras/claude-bot/src/branch/claude-logs/transcripts/Grandiras/Ponente/pr-161/run-388.md) <!-- claude-verdict:approve --> <!-- claude-reviewed-head:6107f021d95ca66902d36b8b71323ee9d3efc3d9 -->
Grandiras deleted branch claude/issue-160 2026-08-26 20:16:31 +00:00
Sign in to join this conversation.
No reviewers
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!161
No description provided.