steid

@jamesgill /

docs: record milestone 2 phase 1

Should have been part of the previous commit rather than a follow-up -- the
rule in CLAUDE.md exists precisely because this lags.

Ticks off phase 1 and records what it taught. The notable one for future
sessions: path_param is an attribute macro on a tuple struct in Topcoat 0.5,
not the function-like form the docs on main describe. The framework is two
weeks old and the released crate has already diverged from its own
repository, so the vendored source is the authority.

Also records that static routes beat parameterised ones, which was the open
question about putting handles at the root, and that this was verified rather
than assumed.

Styling moves into phase 2 as an explicit step rather than a carried-over
nicety, since flash messages are the first thing that needs a visual
treatment.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
JamesPatrickGill authored 25 days agoparenta69e380Browse files3954b457d810438d207bfdab22f4afbe5b4305be

2 files changed+52 −8

plans/current.md+22 −7View file
@@ -21,19 +21,22 @@ Shippable on its own: a public profile that renders signed out.
2121 - [x] Settle URL shape — handles at the root, routes grouped under prefixes
2222 ([0004](decisions/0004-root-handles-grouped-routes.md))
2323 - [x] Reserved-handle denylist in `OrgName::new`; auth routes moved under `/auth/`
24- [ ] Decide `module_router!` vs explicit `#[page]` paths
25- [ ] `PublicProfile` read model + `view_profile` use case — **must not carry email**
26- [ ] `/{handle}` page: label, handle, bio, and the section frame; 404 on unknown,
24+- [x] Explicit `#[page]` paths for now; `module_router!` still unexamined, and four
25+ routes is too few to judge it against
26+- [x] `PublicProfile` read model + `view_profile` use case — no email field, by design
27+- [x] `/{handle}` page: label, handle, bio, and the section frame; 404 on unknown,
2728 case-insensitive
28- [ ] Assert `/auth/login` and `/api/me` still route once `/{handle}` exists at the root
29- [ ] `/` redirects to the owner's profile once claimed, retiring the placeholder
30- [ ] `/api/users/{handle}` — same read model, public JSON
29+- [x] Asserted `/auth/login` and `/api/me` still route with `/{handle}` at the root —
30+ static beats parameterised
31+- [x] `/` redirects to the owner's profile once claimed, retiring the placeholder
32+- [x] `/api/users/{handle}` — same read model, public JSON
33+- [x] Migration `orgs.bio`, pulled forward so the page had a field to render
3134
3235 ### Phase 2 — make it yours
3336
3437 A profile you can't change is a stub. This is what makes it a portfolio page.
3538
36- [ ] Migration: `orgs.bio`
39+- [ ] Decide the styling approach — Tailwind and Topcoat UI, or hand-rolled CSS
3740 - [ ] Flash messages — the first edit form needs success and failure feedback, and
3841 every form after it inherits whatever we build here
3942 - [ ] `/{handle}/settings` — edit display name and bio, owner only, enforced in the use
@@ -46,6 +49,18 @@ Signed out, `/{handle}` renders the owner's display name and handle and nothing
4649 private. An unknown handle 404s. The owner can set a display name and bio and see them
4750 on the page. `/api/users/{handle}` returns the same public view.
4851
52+### Findings — phase 1
53+
54+- **`path_param` is an attribute in Topcoat 0.5**, applied to a tuple struct
55+ (`#[path_param] struct Handle(str);`), not the function-like `path_param!(handle)`
56+ that the docs on `main` describe. **Read the vendored crate, not GitHub `main`** —
57+ the framework is two weeks old and the two have already diverged.
58+- A `str` inner type yields the raw percent-decoded segment with no parsing, which
59+ suits validating through `OrgName` and 404ing what fails.
60+- **Static routes beat parameterised ones**, so `/auth/login` and `/api/me` still work
61+ with `/{handle}` registered at the root. Verified, not assumed.
62+- Topcoat serves bundled assets from `/_topcoat/assets/…` with content-hashed URLs.
63+
4964 ### Watch for
5065
5166 - **Do not leak email.** `describe_identity` carries email, and `/api/me` returns it —
plans/progress.md+30 −1View file
@@ -2,7 +2,7 @@
22
33 ## This attempt (#3, Topcoat)
44
591 tests. Active milestone in [current.md](current.md).
5+111 tests. Active milestone in [current.md](current.md).
66
77 ### Milestone 0 — Skeleton · done
88
@@ -59,6 +59,35 @@ in; re-claiming a claimed instance is refused.
5959 [runbook.md](runbook.md#steid_insecure_cookies--development-only). This one actually
6060 bit, and it looked exactly like broken auth logic.
6161
62+### Milestone 2 — Profile page · phase 1 done
63+
64+`/{handle}` is the real profile page: label, handle, bio, and the section frame for
65+repositories, writing, and projects. Public, renders signed out, 404s on an unknown
66+handle, and resolves regardless of casing. `/` forwards a signed-in owner to their own
67+profile. `/api/users/{handle}` serves the same read model as JSON.
68+
69+URLs settled as root handles with grouped application routes
70+([0004](decisions/0004-root-handles-grouped-routes.md)), superseding
71+[0003](decisions/0003-scoped-urls.md) the same day. A twenty-word reserved list in
72+`OrgName::new` keeps handles from shadowing routes.
73+
74+#### Decisions worth remembering
75+
76+- **`PublicProfile` has no email field, deliberately.** `Identity` does, and `/api/me`
77+ returns it, because that endpoint describes the caller to themselves. Giving the type
78+ that reaches the page nowhere to put an email makes the leak impossible rather than
79+ merely avoided.
80+- **`viewer_is_owner` is decided in the use case**, so the web form and `/api` cannot
81+ disagree about who may edit. Tested for a signed-in stranger and a non-owner member —
82+ "signed in" quietly becoming "allowed" is the usual failure.
83+- **`Organization::update_profile` clears on blank input** rather than storing
84+ whitespace, so cleared and never-set are one state and the page renders one case. A
85+ rejected edit applies nothing.
86+- **Bio length counts characters, not bytes.** A byte check would reject a bio of
87+ accented text well under the limit.
88+- **`path_param` is an attribute macro in 0.5**, not function-like. The vendored crate
89+ is the authority for the pinned version, not the docs on `main`.
90+
6291 ---
6392
6493 ## Reference: what attempt #2 proved