Owner profile page: markdown bio, display name, location, timezone, and repo list at /:owner #234

Closed
opened 2026-09-09 15:13:49 +00:00 by crueber · 3 comments
Owner

What's requested

Every owner (user or org) should have a profile page — the GitHub-profile model, scoped down: no achievements/badges, no followers, no contribution graph.

Currently /:owner (web/src/pages/Repos.jsx, route in web/src/index.jsx:62) renders only a bare owner heading, an optional "New repository" button, and an uncapped repo list. There is no profile surface anywhere; the closest thing is /explore (web/src/pages/Owners.jsx), which folds owner overflow into /:owner.

Desired behavior

/:owner becomes a profile page with:

  1. Display name — distinct from the login/owner slug.
  2. Location — free text.
  3. Timezone — selected from a picker (e.g. the IANA tz list), not free text.
  4. Markdown bio — owner-authored, rendered with the existing markdown renderer (web/src/lib/render-md.js). Editable by the owner.
  5. Owned repos — sorted by recent activity (the ActivityStamp/activity:{o}/{r} data already computed in lib/activity.js can order rows; today Repos.jsx lists in whatever order repos.owners.repos() returns and lib/owners.js has newestFirst for the explore page).

Architecture notes (for whoever implements)

  • Owners are not objects today. They exist only as store path prefixes: internal/store/keys.go RepoPrefix() = repos/<owner>/<repo>/, and the owner list is derived by scanning manifests (cmd/walhub/serve.go:512 repoRegistry.Owners → listOwners, manifest-gated). There is no owner record to hang profile fields on, so this feature needs a new store object for owner profile data — consistent with the "object store is the only database" rule. Suggest a owners/<owner>/profile.pb (or .json) key in internal/store/keys.go, written through the WAL like other policy/settings-style docs.
  • API surface: new endpoints following the existing NonRepo route pattern in internal/api/routes.go:46-51 (GET/PUT /api/v1/owners/{owner}/profile, exposed in all three twins: template, api-browser, services/api), plus a Profile accessor on or alongside RepoRegistry (internal/api/env.go:124). GET is AuthRead (public bio), PUT is AuthWrite and must be restricted to the owner itself — note me() (internal/api/discovery.go:101) currently only exposes principal/write/anonymous, so write authorization for "only this owner may edit their profile" may need the principal name compared against the owner slug.
  • Frontend: extend web/src/pages/Repos.jsx (route /:owner) with a profile header + bio render, and an edit form (markdown textarea + display name / location / tz picker) shown only when the viewer is the owner. Reuse useData cache keys, <RepoRow>, <ActivityStamp>. lib/owners.js already has newestFirst — repo list ordering can reuse it, but note it currently orders by name-derived data; ordering by recent activity needs the activity:{o}/{r} cache populated per repo (as explore does) or a new owner-level activity endpoint.
  • SDK: new methods in web/sdk/src/index.js for get/update profile.

Acceptance criteria

  • GET /api/v1/owners/{owner}/profile returns {display_name, location, timezone, bio_markdown} (empty values when unset) for any known owner; sensible empty/404 behavior for unknown owners consistent with §8 (ownerRepos returns 200 [] — match that convention or document the deviation).
  • PUT updates the profile; only the owning principal may write (anonymous and other users get 401/403). Data persists in the object store and survives a server restart (store is the only database — nothing in local memory only).
  • /:owner renders display name, location, timezone, and rendered markdown bio; falls back to the owner slug when no display name is set.
  • /:owner repo list is ordered by most recent activity, newest first.
  • An "Edit profile" affordance appears only for the authenticated owner; edit form writes through the SDK and the page reflects the update without a full reload.
  • Timezone is chosen from a picker of valid IANA zone names, not free-text.
  • Tests: store round-trip for the profile object, API auth matrix (read public / write owner-only), and at least one UI-level assertion on render + edit flow.
## What's requested Every owner (user or org) should have a profile page — the GitHub-profile model, scoped down: no achievements/badges, no followers, no contribution graph. Currently `/:owner` (web/src/pages/Repos.jsx, route in web/src/index.jsx:62) renders only a bare owner heading, an optional "New repository" button, and an uncapped repo list. There is no profile surface anywhere; the closest thing is `/explore` (web/src/pages/Owners.jsx), which folds owner overflow into `/:owner`. ## Desired behavior `/:owner` becomes a profile page with: 1. **Display name** — distinct from the login/owner slug. 2. **Location** — free text. 3. **Timezone** — selected from a picker (e.g. the IANA tz list), not free text. 4. **Markdown bio** — owner-authored, rendered with the existing markdown renderer (web/src/lib/render-md.js). Editable by the owner. 5. **Owned repos** — sorted by recent activity (the ActivityStamp/`activity:{o}/{r}` data already computed in lib/activity.js can order rows; today `Repos.jsx` lists in whatever order `repos.owners.repos()` returns and `lib/owners.js` has `newestFirst` for the explore page). ## Architecture notes (for whoever implements) - **Owners are not objects today.** They exist only as store path prefixes: `internal/store/keys.go` `RepoPrefix()` = `repos/<owner>/<repo>/`, and the owner list is derived by scanning manifests (`cmd/walhub/serve.go:512` `repoRegistry.Owners` → `listOwners`, manifest-gated). There is no owner record to hang profile fields on, so this feature needs a new store object for owner profile data — consistent with the "object store is the only database" rule. Suggest a `owners/<owner>/profile.pb` (or .json) key in `internal/store/keys.go`, written through the WAL like other policy/settings-style docs. - **API surface:** new endpoints following the existing NonRepo route pattern in `internal/api/routes.go:46-51` (GET/PUT `/api/v1/owners/{owner}/profile`, exposed in all three twins: template, api-browser, services/api), plus a `Profile` accessor on or alongside `RepoRegistry` (`internal/api/env.go:124`). GET is AuthRead (public bio), PUT is AuthWrite and must be restricted to the owner itself — note `me()` (`internal/api/discovery.go:101`) currently only exposes `principal/write/anonymous`, so write authorization for "only this owner may edit their profile" may need the principal name compared against the owner slug. - **Frontend:** extend `web/src/pages/Repos.jsx` (route `/:owner`) with a profile header + bio render, and an edit form (markdown textarea + display name / location / tz picker) shown only when the viewer is the owner. Reuse `useData` cache keys, `<RepoRow>`, `<ActivityStamp>`. `lib/owners.js` already has `newestFirst` — repo list ordering can reuse it, but note it currently orders by name-derived data; ordering by recent activity needs the `activity:{o}/{r}` cache populated per repo (as explore does) or a new owner-level activity endpoint. - **SDK:** new methods in `web/sdk/src/index.js` for get/update profile. ## Acceptance criteria - [ ] GET `/api/v1/owners/{owner}/profile` returns `{display_name, location, timezone, bio_markdown}` (empty values when unset) for any known owner; sensible empty/404 behavior for unknown owners consistent with §8 (`ownerRepos` returns 200 [] — match that convention or document the deviation). - [ ] PUT updates the profile; only the owning principal may write (anonymous and other users get 401/403). Data persists in the object store and survives a server restart (store is the only database — nothing in local memory only). - [ ] `/:owner` renders display name, location, timezone, and rendered markdown bio; falls back to the owner slug when no display name is set. - [ ] `/:owner` repo list is ordered by most recent activity, newest first. - [ ] An "Edit profile" affordance appears only for the authenticated owner; edit form writes through the SDK and the page reflects the update without a full reload. - [ ] Timezone is chosen from a picker of valid IANA zone names, not free-text. - [ ] Tests: store round-trip for the profile object, API auth matrix (read public / write owner-only), and at least one UI-level assertion on render + edit flow.
Author
Owner

Implemented in PR #269 (#269), branch fix/issue-234 — ready for review, not merged.

What landed: /:owner profile page (display name/slug fallback, location, IANA tz picker via Intl.supportedValuesOf, markdown bio via renderBody, activity-ordered repos via the #247 detailed shape — confirmed merged as addf028, reused as-is); new CAS'd sidecar owners//profile.json (frozen-list amended); GET/PUT triple twins + discovery + SDK owners.profile/updateProfile; PUT gated AuthWrite + admin/name-match/org-owner-role via the api.OwnerEditor seam (decision documented in docs/features/01); GET carries can_edit.

Verification: go test -race clean; cover api 95.2% / identity 97.2% / store 95.0%; node --test 542 pass; vite+esbuild green; live-server curl smoke (twins, discovery, validation 400s, restart survival) green. Open: real-Chromium both-themes console check (shared daemon blocks loopback — noted in PR).

Implemented in PR #269 (https://git.packden.us/crueber/walhub/pulls/269), branch fix/issue-234 — ready for review, not merged. What landed: /:owner profile page (display name/slug fallback, location, IANA tz picker via Intl.supportedValuesOf, markdown bio via renderBody, activity-ordered repos via the #247 detailed shape — confirmed merged as addf028, reused as-is); new CAS'd sidecar owners/<owner>/profile.json (frozen-list amended); GET/PUT triple twins + discovery + SDK owners.profile/updateProfile; PUT gated AuthWrite + admin/name-match/org-owner-role via the api.OwnerEditor seam (decision documented in docs/features/01); GET carries can_edit. Verification: go test -race clean; cover api 95.2% / identity 97.2% / store 95.0%; node --test 542 pass; vite+esbuild green; live-server curl smoke (twins, discovery, validation 400s, restart survival) green. Open: real-Chromium both-themes console check (shared daemon blocks loopback — noted in PR).
Author
Owner

Review of PR #269 (fix/issue-234, owner profile page) — verified in scratch worktree, all green.

PASS

  • Seam direction (law 8): OwnerEditor defined in internal/api/profile.go:60, implemented by (*identity.Service).CanEditOwnerProfile (internal/identity/profile_authz.go:32, no api import — only a comment reference), wired in cmd/walhub/collab.go:75-79. Matches OrgGate/AccessBoot shape.
  • Frozen-list amendment: 14_extensibility.md frozen table + §Decisions entry, store.OwnerProfileKey (internal/store/keys.go:118-125), same-revision adoption per §14.11 rule 2. Correct classification (CAS'd sidecar, CAS loop IS the commit point — owner scope has no WAL/manifest).
  • Edit matrix (all tested): anon→401 (auth-required) / 403 (public) — TestOwnerProfileAuthMatrix; wrong-namespace writer→403; host admin→200; case-insensitive name-match→200; org owner→true / member→false / unclaimed→false via seam (profile_authz_test.go); seam probe failure→PUT 503 / GET degrades can_edit=false (TestOwnerProfileEditorSeam). No name-match spoofing: PUT requires AuthWrite first, so anon never reaches defaultCanEdit.
  • Validation server-side: display/location 200 runes, tz 64B shape regex (not LoadLocation — documented tz-db rationale), bio 64KiB + UTF-8, DisallowUnknownFields, 128KiB MaxBytesReader. All 400-plain-text cases in TestOwnerProfileValidation.
  • No-client-version PUT: bounded CAS loop (5 attempts→409, TestOwnerProfileStoreOutage pins it). Last-writer-wins converges via re-read — acceptable at human rate, documented in profile.go header. No lost-update 409-to-client unlike access.json — deliberate, documented.
  • can_edit server-authoritative: UI (Repos.jsx) only reads profile.can_edit, never decides.
  • XSS: bio via shared renderBody (marked+DOMPurify pipeline); display name as Solid text (escaped). profile.test.js pins the marked pass-through/DOMPurify-gate split like blob-md.test.js.
  • Twins + discovery + SDK: triple twins GET+PUT (routes.go), discovery-listed (handlers_test.go pins), SDK owners.profile/updateProfile + OwnerProfile typedef, twins tested (TestOwnerProfileTwins).
  • #247 reuse intact: detailed listing + orderByActivity call untouched.
  • Coverage: internal/api 95.2%, internal/identity 97.2% (≥95 gate holds).
  • Deps: go.mod/package.json untouched — no new modules or npm deps (tz picker is Intl.supportedValuesOf).
  • Docs: 01_identity_permissions.md edit-gate decision, 07_api.md endpoint specs, 14 frozen amendment — all present and accurate.
  • Restart safety: TestOwnerProfileSurvivesRestart (bucket is the repo); corrupt doc→503, healing PUT converges.
  • gofmt clean, go vet clean, go test -race passes (api/identity/store), node --test 542/542 pass (incl. 14 new profile+timezone tests).

FIX APPLIED (pushed 6780479 to origin/fix/issue-234, re-tested):

  • internal/api/profile.go: ownerProfileGet could name-match the anonymous principal (Name=anonymous) on a namespace literally called 'anonymous', advertising can_edit whose PUT always 403s. Now anon short-circuits to can_edit=false (seam not consulted either). Regression test added to TestOwnerProfileGetEmpty. Cosmetic-only; PUT was never affected (AuthWrite gate first).

No browser pass: per task instructions, tests + reasoning only (no browser-facing serving change — pure API + SPA route already covered by the shared pipeline's Chromium history).

MERGE RECOMMENDATION: ready to merge.

Review of PR #269 (fix/issue-234, owner profile page) — verified in scratch worktree, all green. PASS - Seam direction (law 8): OwnerEditor defined in internal/api/profile.go:60, implemented by (*identity.Service).CanEditOwnerProfile (internal/identity/profile_authz.go:32, no api import — only a comment reference), wired in cmd/walhub/collab.go:75-79. Matches OrgGate/AccessBoot shape. - Frozen-list amendment: 14_extensibility.md frozen table + §Decisions entry, store.OwnerProfileKey (internal/store/keys.go:118-125), same-revision adoption per §14.11 rule 2. Correct classification (CAS'd sidecar, CAS loop IS the commit point — owner scope has no WAL/manifest). - Edit matrix (all tested): anon→401 (auth-required) / 403 (public) — TestOwnerProfileAuthMatrix; wrong-namespace writer→403; host admin→200; case-insensitive name-match→200; org owner→true / member→false / unclaimed→false via seam (profile_authz_test.go); seam probe failure→PUT 503 / GET degrades can_edit=false (TestOwnerProfileEditorSeam). No name-match spoofing: PUT requires AuthWrite first, so anon never reaches defaultCanEdit. - Validation server-side: display/location 200 runes, tz 64B shape regex (not LoadLocation — documented tz-db rationale), bio 64KiB + UTF-8, DisallowUnknownFields, 128KiB MaxBytesReader. All 400-plain-text cases in TestOwnerProfileValidation. - No-client-version PUT: bounded CAS loop (5 attempts→409, TestOwnerProfileStoreOutage pins it). Last-writer-wins converges via re-read — acceptable at human rate, documented in profile.go header. No lost-update 409-to-client unlike access.json — deliberate, documented. - can_edit server-authoritative: UI (Repos.jsx) only reads profile.can_edit, never decides. - XSS: bio via shared renderBody (marked+DOMPurify pipeline); display name as Solid text (escaped). profile.test.js pins the marked pass-through/DOMPurify-gate split like blob-md.test.js. - Twins + discovery + SDK: triple twins GET+PUT (routes.go), discovery-listed (handlers_test.go pins), SDK owners.profile/updateProfile + OwnerProfile typedef, twins tested (TestOwnerProfileTwins). - #247 reuse intact: detailed listing + orderByActivity call untouched. - Coverage: internal/api 95.2%, internal/identity 97.2% (≥95 gate holds). - Deps: go.mod/package.json untouched — no new modules or npm deps (tz picker is Intl.supportedValuesOf). - Docs: 01_identity_permissions.md edit-gate decision, 07_api.md endpoint specs, 14 frozen amendment — all present and accurate. - Restart safety: TestOwnerProfileSurvivesRestart (bucket is the repo); corrupt doc→503, healing PUT converges. - gofmt clean, go vet clean, go test -race passes (api/identity/store), node --test 542/542 pass (incl. 14 new profile+timezone tests). FIX APPLIED (pushed 6780479 to origin/fix/issue-234, re-tested): - internal/api/profile.go: ownerProfileGet could name-match the anonymous principal (Name=anonymous) on a namespace literally called 'anonymous', advertising can_edit whose PUT always 403s. Now anon short-circuits to can_edit=false (seam not consulted either). Regression test added to TestOwnerProfileGetEmpty. Cosmetic-only; PUT was never affected (AuthWrite gate first). No browser pass: per task instructions, tests + reasoning only (no browser-facing serving change — pure API + SPA route already covered by the shared pipeline's Chromium history). MERGE RECOMMENDATION: ready to merge.
Author
Owner

Fixed by PR #269 incl. review anon can_edit fix (seam direction, edit matrix, frozen amendment verified; 95.2/97.2% + 542/542), merged. Closing.

Fixed by PR #269 incl. review anon can_edit fix (seam direction, edit matrix, frozen amendment verified; 95.2/97.2% + 542/542), merged. Closing.
crueber added this to the v1 milestone 2026-09-10 22:27:11 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
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
crueber/walhub#234
No description provided.