steid

@jamesgill /

steid/plans/current.md
6.9 KBCode·Blame·Raw
1# Current
2
3> Keep this file short. One active step, one ordered backlog. Completed work moves to
4> [progress.md]progress.md. If this file starts reading like a changelog, it has
5> drifted — that's exactly what went wrong last time.
6
7## Active: Milestone 2 — Profile page
8
9**Goal:** `/{handle}` is the real profile page — public, working signed out, and the
10frame that repos, writing, and projects hang off later. It replaces the Milestone 0
11placeholder.
12
13**Out of scope:** repos, posts, and projects don't exist yet, so there is nothing to
14list. Avatars, following, and anything social. Organisation profiles beyond what falls
15out for free.
16
17### Phase 1 — the page
18
19Shippable on its own: a public profile that renders signed out.
20
21- [x] Settle URL shape — handles at the root, routes grouped under prefixes
22 ([0004]decisions/0004-root-handles-grouped-routes.md)
23- [x] Reserved-handle denylist in `OrgName::new`; auth routes moved under `/auth/`
24- [ ] **Spike: route precedence.** Does `/auth/login` still win once `/{handle}` exists
25 at the root? Most routers prefer a static segment over a parameterised one, but
26 this is load-bearing and unverified — check before designing around it
27- [ ] Decide `module_router!` vs explicit `#[page]` paths, informed by the spike
28- [ ] `PublicProfile` read model + `view_profile` use case — **must not carry email**
29- [ ] `/{handle}` page: renders label and handle, 404 on unknown, case-insensitive
30- [ ] `/` redirects to the owner's profile once claimed, retiring the placeholder
31- [ ] `/api/users/{handle}` — same read model, public JSON
32
33### Phase 2 — make it yours
34
35A profile you can't change is a stub. This is what makes it a portfolio page.
36
37- [ ] Migration: `orgs.bio`
38- [ ] Flash messages — the first edit form needs success and failure feedback, and
39 every form after it inherits whatever we build here
40- [ ] `/{handle}/settings` — edit display name and bio, owner only, enforced in the use
41 case
42- [ ] Owner-only affordances on the profile (edit link)
43
44### Done when
45
46Signed out, `/{handle}` renders the owner's display name and handle and nothing
47private. An unknown handle 404s. The owner can set a display name and bio and see them
48on the page. `/api/users/{handle}` returns the same public view.
49
50### Watch for
51
52- **Do not leak email.** `describe_identity` carries email, and `/api/me` returns it —
53 correctly, because that endpoint describes the caller to themselves. The public
54 profile needs its **own** read model; reusing `Identity` would publish the owner's
55 email address to anonymous visitors. This is the single most likely mistake in this
56 milestone.
57- **Route precedence** between `/auth/login` and `/{handle}`. Verify, don't assume.
58- **Case-insensitive handles.** Storage is `collate nocase` and `OrgName::new`
59 lowercases, so `/JamesGill` must resolve rather than 404.
60- **Authorization on settings** belongs in the use case, taking an `Actor` — not in the
61 page. Otherwise `/api` gets a different answer from the web form.
62- **A profile is public.** This is the first page rendering for anonymous visitors by
63 design, so anything private must be gated explicitly rather than by assuming a
64 session exists.
65- **Reserve handles early.** Adding to the denylist later is a breaking change for
66 whoever holds that handle ([0004]decisions/0004-root-handles-grouped-routes.md).
67
68### Open questions
69
70- **Where do settings live?** `/{handle}/settings` is consistent with grouping and
71 scales to organisations; `/settings` is simpler for a single user but would need
72 another reserved word (already reserved). Leaning `/{handle}/settings`.
73- **Empty sections or none?** Rendering "Repositories (none yet)" makes the frame
74 visible but is speculative scaffolding for things that don't exist. Leaning towards
75 omitting them until the content type lands.
76
77### Carried over — small, unblocked
78
79- **No rate limiting** on `/auth/login` or `/auth/setup`.
80- **`sweep_expired` is never called**, so expired session rows accumulate. Expiry is
81 enforced on read, so this is tidiness, not a hole.
82- **CSRF.** `SameSite=Lax` covers the common case; whether forms also want tokens is
83 still undecided. Phase 2 adds a form, so this is the natural time to settle it.
84- **Styling.** Everything is unstyled HTML. Topcoat ships Tailwind without Node, and
85 `steid-backup/AGENTS/UI.md` has a full OKLCH system to mine. Cheaper now than at ten
86 pages — see [ui.md]ui.md.
87
88## Backlog
89
90Ordered. Pull from the top.
91
921. **Milestone 3 — Writing.** Posts, markdown rendering, `/{handle}/posts/{slug}`.
93 *Open question: is writing actually the first portfolio feature, or is it
94 projects/showcases?*
952. **Milestone 4 — Repo model.** `Repository` entity, `Visibility`, `create_repo`,
96 bare repo on disk at `{data_dir}/{org}/{repo}.git`. Watch the DB-plus-filesystem
97 atomicity problem — see [architecture.md]architecture.md#db-plus-filesystem-writes.
983. **Milestone 5 — Git over HTTP.** `git http-backend` subprocess, PATs over HTTP
99 Basic. See [0001]decisions/0001-git-over-http-not-ssh.md.
100
101## Open questions
102
103- **Topcoat is early** (v0.5.0, first released 2026-07-22, breaking changes expected
104 by its own authors). Expect churn that isn't feature work.
105- Body size limits will reject large pushes at Milestone 5 — `topcoat-router` has a
106 `body_limit` layer that needs raising on the git routes. Recorded here because it
107 will surface as a confusing failure rather than a clear one.
108- Topcoat ships Tailwind without Node, which reopens the design system attempt #1
109 dropped purely to avoid an npm build step — see [ui.md]ui.md.
110
111## Routing findings (Milestone 0)
112
113- **Topcoat 0.5 requires rustc ≥ 1.95.** On an older toolchain `cargo add topcoat`
114 silently resolves to an empty `topcoat v0.0.0` placeholder instead of failing. Local
115 stable is now 1.97.1.
116- `Router::builder().discover()` collects `#[page]`-annotated items **at link time**,
117 so pages can live in any module. Layering is our choice, not the framework's.
118- `module_router!` derives each URL from the module tree rather than a path string.
119 Still deferred. Application routes now group cleanly (`auth/login`, `api/me`), but
120 handles sit at the root ([0004]decisions/0004-root-handles-grouped-routes.md), so a
121 parameterised root segment still has to coexist with static ones. Worth checking how
122 `module_router!` handles that before committing to it.
123- Path and query params are read from `Cx` via `path_param!` / `#[query_params]`, not
124 injected as handler arguments. Parses are memoized per request.
125- Layouts wrap by path prefix and nest outermost-first, and a layout can catch a page's
126 `NotFoundError` to render a branded 404.
127- `HOST` / `PORT` configure the bind address, so `STEID_LISTEN_ADDR` is gone.
128- `Body` is a boxed `http_body::Body` used for both requests and responses, with
129 `into_data_stream()` to read and `Body::new()` to wrap a stream — pack data can
130 stream both directions without buffering. This is what makes Milestone 5 viable.