simocracy-v2 · profile-page
A new /profile/<did> page reads + writes app.bsky.actor.profile on the user's PDS so it stays interoperable with Bluesky and any other AT-Protocol-aware client. End-to-end run hit two real PDS profiles, the 404 path, and a mobile breakpoint; the layout works and one visual bug was spotted and fixed mid-run.
Pass. The new profile page hydrates cleanly from public.api.bsky.app/xrpc/app.bsky.actor.getProfile, lists the user's org.simocracy.sim records and any org.simocracy.gathering they own or sit on the council of, and serves correct 404s for non-DID paths. The owner-only Edit dialog routes its PUT through the existing /api/records path with the new app.bsky.actor.profile allow-list entry, so writes are wired the same way every other simocracy record is. One visual bug — a long bio pushing the H1 up into a dark banner photo — was caught against @daviddao.org and fixed in the same run; the second commit is the screenshot “FIXED” set you see below.
Both DIDs are real Bluesky accounts that have records on a Simocracy PDS, so this hits the indexer + PDS fallback paths the page is meant to use — not a mocked fixture.
@daviddao.org: 4 sims, 2 events (1 admin, 1 council seat).@simocracy.climateai.org (the facilitator): 5 sims, 3 events (2 admin, 1 council seat)./profile/foobar: must 404, not crash, not show a half-rendered shell./sims/<did>/<rkey>; clicking an event card navigates to its gathering page.com.atproto.repo.getRecord on the facilitator's PDS is the one the Edit dialog writes back via /api/records.
items-end, “David Dao” ended up inside the dark banner photo and was barely legible.
The fix is one commit: fbecbbc — components/profile/profile-client.tsx, swap flex flex-row items-end for an avatar-row + text-block stack. Verified against both the banner-having and banner-less profiles after the change.
cdn.bsky.app, avatar floats over it, name/handle/bio sit below. Followers/following/posts come straight from the public AppView.
cdn.bsky.app avatar URL.
org.simocracy.sim + their shortDescription from the agents record. Events show pixel-sprite council previews, gathering type/status pills, and an ADMIN/COUNCIL chip describing the user’s relationship to the event.
/profile/foobar hits notFound() in the server component (only did:plc: and did:web: are allowed). Renders the standard simocracy 404, not a half-broken page.The contract that makes this interoperable, not just “a profile-shaped thing”: the page edits the exact app.bsky.actor.profile/self record that bsky.app, deer.social, tangled, etc. all read. We confirmed by curl-ing the facilitator’s own PDS:
$ curl -s 'https://simocracy.climateai.org/xrpc/com.atproto.repo.getRecord?\
repo=did:plc:awtx57rxuisy4sf4kzzr3uhf&\
collection=app.bsky.actor.profile&\
rkey=self' | jq '.value | {displayName, description, avatar: .avatar.ref."$link"}'
{
"displayName": "Simocracy",
"description": null,
"avatar": "bafkreibxzvcztohjohprogt4pmgutocxafcyzwct3qt7ddpo6rk3eynpbq"
}
The avatar CID matches what public.api.bsky.app/xrpc/app.bsky.actor.getProfile returns, which is what the page renders. Save flow:
POST /api/upload-blob on their own PDS, gets back a blob ref.app.bsky.actor.profile/self record so unknown fields (pronouns, website, labels, joinedViaStarterPack, pinnedPost) survive the round-trip.PUT /api/records with collection: "app.bsky.actor.profile", rkey: "self", and the merged record. The new allow-list entry on app/api/records/route.ts is what permits the write.Sims come from fetchSimsForDid (Simocracy indexer first, falls back to com.atproto.repo.listRecords). Events come from fetchGatherings, then are filtered client-side to those owned by the DID OR with one of the user's sim URIs in councilSims[]. Both signals showed up correctly on @daviddao.org (admin + council) and on the facilitator (admin x2 + council x1).
The Edit dialog targets app.bsky.actor.profile/self. Same NSID + rkey Bluesky uses. We don't store any simocracy-specific shape for the user’s identity — displayName, description, avatar, and banner all round-trip through the standard Bluesky lexicon, so changes here show up on bsky.app immediately, and changes there show up here on next page render.
components/navbar.tsx shows the new item when userDid is set, linking to /profile/<userDid>. Confirmed by reading the rendered tree (the dropdown only mounts when signed in, so visual capture requires a real OAuth session — covered in the navbar source diff in commit 11ae869).
The server component validates did.startsWith("did:plc:") || did.startsWith("did:web:") before any fetch, so /profile/foobar short-circuits to notFound(). No server error, no half-rendered shell, no wasted indexer hit.
generateMetadata() runs before the page component’s notFound(), so /profile/foobar ends up with browser-tab title “foobar · Profile · Simocracy” even though the body renders the 404. Cosmetic only — the page itself 404s correctly. Worth tightening if it ever shows up in shared social-card previews.
Original layout used flex-row items-end on a row that contained the avatar and a long-text column; when the bio overflowed the avatar height the column expanded upward and the H1 ended up inside the banner image. Re-shot against both profiles after the fix — see the figpair above. The fixed shape is the same Twitter/Bluesky layout users already expect.
/profile/did:plc:qc42fmqqlsmdq7jiypiiigww · /profile/did:plc:awtx57rxuisy4sf4kzzr3uhf · /profile/foobar
Test profiles@daviddao.org (banner, long bio, 4 sims) · @simocracy.climateai.org (no banner, 5 sims, 3 events)
Lexicon readcurl https://simocracy.climateai.org/xrpc/com.atproto.repo.getRecord?repo=<did>&collection=app.bsky.actor.profile&rkey=self
AppView readcurl https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=<did>
Mobile viewportagent-browser set viewport 390 844
Checksnpm run lint (no new errors), npm run build (✓ compiled, route ƒ /profile/[did] emitted)
Commits11ae869 add /profile/<did> · fbecbbc fix banner overlap