Add option to change app-specific profile #160

Closed
opened 2026-08-26 19:46:41 +00:00 by Grandiras · 2 comments
Owner

Right now, a profile change on Ponente also affects other apps like Bluesky. Add the possibility to only change the profile on Ponente.

Right now, a profile change on Ponente also affects other apps like Bluesky. Add the possibility to only change the profile on Ponente.
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/387); 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-160/run-387.md) — updating every ~5 minutes while the task runs.
Collaborator

Pushed claude/issue-160. Unit suite green (395/395), and the profile e2e spec passes against a live app.

What changed

/profile now asks where a change applies before it applies it — per save, with the Atmosphere profile still the default:

The profile page with the new “Where it applies” choice, on “Ponente only”, showing a locally-stored name and picture

Picking Ponente only writes Ponente's own Members row and stops there: no token, no scope, no app.bsky.actor.profile record, nothing sent to the member's account. Two consequences worth knowing:

  • It works with no permission at all — on a deployment configured AtProto:ProfileScope=none, and for a member who signed in before this app started asking. That is why "sign in once more" now sits under a disabled option instead of replacing the whole form; a name for Ponente alone was never gated on a grant it does not use.
  • It sticks. Member.UsesLocalProfile marks whose answer the name and picture are, and MemberService.RefreshProfileAsync, the startup backfill and /dev/login/{did} all check it before overwriting. Without that guard the first profile lookup after the save would quietly put the network's name back.

It reuses the same two columns rather than adding a second pair, so every screen that draws a face (card tile, comment, assignee picker, nav header, admin list) keeps reading what it always read — the change is confined to the writers, which is the short list above.

The picture is the part that needed new storage: a network avatar is a blob in a repository that already exists, this one has nowhere to live. New MemberAvatar table — encrypted through a byte[] converter (the first caller of IDataEncryptor.EncryptBytes), deliberately with no navigation property, so a megabyte of image can never ride along on a member read via an Include — served by GET /avatar/{did} behind the allowlist, at a URL carrying a random version that changes with the picture.

The way back is the button that used to say "refresh from the network": for a member holding a local profile it reads "Use my Atmosphere profile", because for them fetching what the account says and adopting it are the same act. Saving to the network is the other way back, and both delete the stored picture.

Also generalised the board-access option cards (.access-choice/.access-option) into .choice-list/.choice-option — the profile page is the second thing that needs that shape, and duplicating 30 lines of CSS was the alternative.

Verified

  • dotnet build / dotnet test Ponente.sln — 395 passed.
  • Ran the app on the host (Postgres 15, Development) and drove Playwright against it: profile.spec.js 4/4, plus board.spec.js, i18n.spec.js, prefs.spec.js, changelog.spec.js (32/32) and access.spec.js for the class rename — its two failures there are the PDS-signup tests, and no container runtime exists in this job to run the PDS.
  • profile.spec.js now reaches a save end to end, which it never could before: it sets a Ponente-only name and picture, asserts the preview points at /avatar/{did}?v=…, fetches that URL back (200, image/png), and checks both survive the next sign-in. That also pins the route literal in Program.cs against AvatarStore.Route.
  • Checked by hand that the nav header picks the new face up live, with no reload — NotifyProfileChanged fires on this path too.

Changelog entry added in both locales; docs/auth.md (new "the profile that stays here" section), docs/data.md and docs/orientation.md updated in the same commit. Writing the network profile still needs a real OAuth grant and remains a manual check, as it was before.


⏱ 20m 44s · 135 turns · tokens: 199.3k in / 74.6k out (+18703.2k cache-read)
📜 Full session transcript
→ Opened PR: #161

Pushed `claude/issue-160`. Unit suite green (395/395), and the profile e2e spec passes against a live app. ## What changed `/profile` now asks **where a change applies** before it applies it — per save, with the Atmosphere profile still the default: ![The profile page with the new “Where it applies” choice, on “Ponente only”, showing a locally-stored name and picture](https://git.grandiras.net/attachments/f9c8fa85-68ee-4950-b05e-8b5183ad8511) Picking **Ponente only** writes Ponente's own `Members` row and stops there: no token, no scope, no `app.bsky.actor.profile` record, nothing sent to the member's account. Two consequences worth knowing: - It **works with no permission at all** — on a deployment configured `AtProto:ProfileScope=none`, and for a member who signed in before this app started asking. That is why "sign in once more" now sits under a *disabled option* instead of replacing the whole form; a name for Ponente alone was never gated on a grant it does not use. - It **sticks**. `Member.UsesLocalProfile` marks whose answer the name and picture are, and `MemberService.RefreshProfileAsync`, the startup backfill and `/dev/login/{did}` all check it before overwriting. Without that guard the first profile lookup after the save would quietly put the network's name back. It reuses the same two columns rather than adding a second pair, so every screen that draws a face (card tile, comment, assignee picker, nav header, admin list) keeps reading what it always read — the change is confined to the *writers*, which is the short list above. The picture is the part that needed new storage: a network avatar is a blob in a repository that already exists, this one has nowhere to live. New `MemberAvatar` table — encrypted through a `byte[]` converter (the first caller of `IDataEncryptor.EncryptBytes`), deliberately with **no navigation property**, so a megabyte of image can never ride along on a member read via an `Include` — served by `GET /avatar/{did}` behind the allowlist, at a URL carrying a random version that changes with the picture. The way back is the button that used to say "refresh from the network": for a member holding a local profile it reads **"Use my Atmosphere profile"**, because for them fetching what the account says and adopting it are the same act. Saving to the network is the other way back, and both delete the stored picture. Also generalised the board-access option cards (`.access-choice`/`.access-option`) into `.choice-list`/`.choice-option` — the profile page is the second thing that needs that shape, and duplicating 30 lines of CSS was the alternative. ## Verified - `dotnet build` / `dotnet test Ponente.sln` — 395 passed. - Ran the app on the host (Postgres 15, Development) and drove Playwright against it: `profile.spec.js` 4/4, plus `board.spec.js`, `i18n.spec.js`, `prefs.spec.js`, `changelog.spec.js` (32/32) and `access.spec.js` for the class rename — its two failures there are the PDS-signup tests, and no container runtime exists in this job to run the PDS. - `profile.spec.js` now reaches a **save** end to end, which it never could before: it sets a Ponente-only name and picture, asserts the preview points at `/avatar/{did}?v=…`, fetches that URL back (200, `image/png`), and checks both survive the next sign-in. That also pins the route literal in `Program.cs` against `AvatarStore.Route`. - Checked by hand that the nav header picks the new face up live, with no reload — `NotifyProfileChanged` fires on this path too. Changelog entry added in both locales; `docs/auth.md` (new "the profile that stays here" section), `docs/data.md` and `docs/orientation.md` updated in the same commit. Writing the *network* profile still needs a real OAuth grant and remains a manual check, as it was before. --- ⏱ 20m 44s · 135 turns · tokens: 199.3k in / 74.6k out (+18703.2k cache-read) 📜 [Full session transcript](https://git.grandiras.net/Grandiras/claude-bot/src/branch/claude-logs/transcripts/Grandiras/Ponente/issue-160/run-387.md) → Opened PR: https://git.grandiras.net/Grandiras/Ponente/pulls/161
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#160
No description provided.