| 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 | |
076dbc9docs: close milestone 1, open milestone 21mo | 7 | ## Active: Milestone 2 — Profile page |
| 8 | |
f0444b7docs: plan milestone 2 in two phases1mo | 9 | **Goal:** `/{handle}` is the real profile page — public, working signed out, and the |
| 10 | frame that repos, writing, and projects hang off later. It replaces the Milestone 0 |
| 11 | placeholder. |
bd48b4bdocs: serve git over smart HTTP, reorder roadmap portfolio-first1mo | 12 | |
f0444b7docs: plan milestone 2 in two phases1mo | 13 | **Out of scope:** repos, posts, and projects don't exist yet, so there is nothing to |
| 14 | list. Avatars, following, and anything social. Organisation profiles beyond what falls |
| 15 | out for free. |
| 16 | |
f0444b7docs: plan milestone 2 in two phases1mo | 17 | ### Phase 1 — the page |
| 18 | |
f0444b7docs: plan milestone 2 in two phases1mo | 19 | Shippable 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)) |
aaefaabfeat: root handles, grouped routes, reserved-handle denylist1mo | 23 | - [x] Reserved-handle denylist in `OrgName::new`; auth routes moved under `/auth/` |
3954b45docs: record milestone 2 phase 125d | 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, |
87edb56docs: settle milestone 2 open questions1mo | 28 | case-insensitive |
3954b45docs: record milestone 2 phase 125d | 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 |
f0444b7docs: plan milestone 2 in two phases1mo | 34 | |
| 35 | ### Phase 2 — make it yours |
| 36 | |
| 37 | A profile you can't change is a stub. This is what makes it a portfolio page. |
| 38 | |
af126c7feat: tailwind theme and the first components25d | 39 | - [x] Styling: Tailwind via Topcoat, theme retuned, primitives copied in, `flash` |
| 40 | written by hand ([0005](decisions/0005-tailwind-and-copied-components.md)) |
f0444b7docs: plan milestone 2 in two phases1mo | 41 | - [ ] Flash messages — the first edit form needs success and failure feedback, and |
| 42 | every form after it inherits whatever we build here |
| 43 | - [ ] `/{handle}/settings` — edit display name and bio, owner only, enforced in the use |
87edb56docs: settle milestone 2 open questions1mo | 44 | case (settings belong to the org, and this scales to organisations) |
f0444b7docs: plan milestone 2 in two phases1mo | 45 | - [ ] Owner-only affordances on the profile (edit link) |
| 46 | |
| 47 | ### Done when |
| 48 | |
f0444b7docs: plan milestone 2 in two phases1mo | 49 | Signed out, `/{handle}` renders the owner's display name and handle and nothing |
| 50 | private. An unknown handle 404s. The owner can set a display name and bio and see them |
| 51 | on the page. `/api/users/{handle}` returns the same public view. |
88583f2docs: bring tracking docs up to date with milestone 11mo | 52 | |
3954b45docs: record milestone 2 phase 125d | 53 | ### Findings — phase 1 |
| 54 | |
| 55 | - **`path_param` is an attribute in Topcoat 0.5**, applied to a tuple struct |
| 56 | (`#[path_param] struct Handle(str);`), not the function-like `path_param!(handle)` |
| 57 | that the docs on `main` describe. **Read the vendored crate, not GitHub `main`** — |
| 58 | the framework is two weeks old and the two have already diverged. |
| 59 | - A `str` inner type yields the raw percent-decoded segment with no parsing, which |
| 60 | suits validating through `OrgName` and 404ing what fails. |
| 61 | - **Static routes beat parameterised ones**, so `/auth/login` and `/api/me` still work |
| 62 | with `/{handle}` registered at the root. Verified, not assumed. |
| 63 | - Topcoat serves bundled assets from `/_topcoat/assets/…` with content-hashed URLs. |
| 64 | |
bd48b4bdocs: serve git over smart HTTP, reorder roadmap portfolio-first1mo | 65 | ### Watch for |
d7b99d9docs: record milestone 0 progress and routing findings1mo | 66 | |
f0444b7docs: plan milestone 2 in two phases1mo | 67 | - **Do not leak email.** `describe_identity` carries email, and `/api/me` returns it — |
| 68 | correctly, because that endpoint describes the caller to themselves. The public |
| 69 | profile needs its **own** read model; reusing `Identity` would publish the owner's |
| 70 | email address to anonymous visitors. This is the single most likely mistake in this |
| 71 | milestone. |
87edb56docs: settle milestone 2 open questions1mo | 72 | - **Route precedence** between static routes and `/{handle}`. The reserved list stops a |
| 73 | user *owning* `auth`, but it does not stop the router matching `/api/me` against |
| 74 | `/{handle}/{x}` and shadowing the real route. Static-over-parameterised is near |
| 75 | universal, so this is an assertion when the route lands, not a blocking spike. |
f0444b7docs: plan milestone 2 in two phases1mo | 76 | - **Case-insensitive handles.** Storage is `collate nocase` and `OrgName::new` |
| 77 | lowercases, so `/JamesGill` must resolve rather than 404. |
| 78 | - **Authorization on settings** belongs in the use case, taking an `Actor` — not in the |
| 79 | page. Otherwise `/api` gets a different answer from the web form. |
| 80 | - **A profile is public.** This is the first page rendering for anonymous visitors by |
076dbc9docs: close milestone 1, open milestone 21mo | 81 | design, so anything private must be gated explicitly rather than by assuming a |
| 82 | session exists. |
f0444b7docs: plan milestone 2 in two phases1mo | 83 | - **Reserve handles early.** Adding to the denylist later is a breaking change for |
| 84 | whoever holds that handle ([0004](decisions/0004-root-handles-grouped-routes.md)). |
| 85 | |
87edb56docs: settle milestone 2 open questions1mo | 86 | ### Settled |
f0444b7docs: plan milestone 2 in two phases1mo | 87 | |
87edb56docs: settle milestone 2 open questions1mo | 88 | - **Settings live at `/{handle}/settings`.** They belong to the organisation, which |
| 89 | scales to real organisations in Milestone 7 without moving. |
| 90 | - **Render the full frame from the start**, including sections with nothing in them. |
| 91 | A profile page that renders almost nothing is a poor start for a portfolio-first |
| 92 | product; the shape of the page is part of what is being built, not scaffolding for |
| 93 | it. |
076dbc9docs: close milestone 1, open milestone 21mo | 94 | |
f0444b7docs: plan milestone 2 in two phases1mo | 95 | ### Carried over — small, unblocked |
076dbc9docs: close milestone 1, open milestone 21mo | 96 | |
f0444b7docs: plan milestone 2 in two phases1mo | 97 | - **No rate limiting** on `/auth/login` or `/auth/setup`. |
076dbc9docs: close milestone 1, open milestone 21mo | 98 | - **`sweep_expired` is never called**, so expired session rows accumulate. Expiry is |
| 99 | enforced on read, so this is tidiness, not a hole. |
| 100 | - **CSRF.** `SameSite=Lax` covers the common case; whether forms also want tokens is |
f0444b7docs: plan milestone 2 in two phases1mo | 101 | still undecided. Phase 2 adds a form, so this is the natural time to settle it. |
af126c7feat: tailwind theme and the first components25d | 102 | - **Light mode is untested.** The palette defines it, but every page has been looked at |
| 103 | dark-only. There is no toggle yet either. |
| 104 | - **Fonts are not loaded.** The theme names Geist and IBM Plex Mono; neither is |
| 105 | installed, so both fall back. `topcoat`'s `font-fontsource` feature handles this. |
| 106 | |
| 107 | ## Backlog |
| 108 | |
| 109 | Ordered. Pull from the top. |
| 110 | |
aaefaabfeat: root handles, grouped routes, reserved-handle denylist1mo | 111 | 1. **Milestone 3 — Writing.** Posts, markdown rendering, `/{handle}/posts/{slug}`. |
bd48b4bdocs: serve git over smart HTTP, reorder roadmap portfolio-first1mo | 112 | *Open question: is writing actually the first portfolio feature, or is it |
| 113 | projects/showcases?* |
076dbc9docs: close milestone 1, open milestone 21mo | 114 | 2. **Milestone 4 — Repo model.** `Repository` entity, `Visibility`, `create_repo`, |
bd48b4bdocs: serve git over smart HTTP, reorder roadmap portfolio-first1mo | 115 | bare repo on disk at `{data_dir}/{org}/{repo}.git`. Watch the DB-plus-filesystem |
| 116 | atomicity problem — see [architecture.md](architecture.md#db-plus-filesystem-writes). |
076dbc9docs: close milestone 1, open milestone 21mo | 117 | 3. **Milestone 5 — Git over HTTP.** `git http-backend` subprocess, PATs over HTTP |
bd48b4bdocs: serve git over smart HTTP, reorder roadmap portfolio-first1mo | 118 | Basic. See [0001](decisions/0001-git-over-http-not-ssh.md). |
| 119 | |
| 120 | ## Open questions |
| 121 | |
bd48b4bdocs: serve git over smart HTTP, reorder roadmap portfolio-first1mo | 122 | - **Topcoat is early** (v0.5.0, first released 2026-07-22, breaking changes expected |
| 123 | by its own authors). Expect churn that isn't feature work. |
| 124 | - Body size limits will reject large pushes at Milestone 5 — `topcoat-router` has a |
| 125 | `body_limit` layer that needs raising on the git routes. Recorded here because it |
| 126 | will surface as a confusing failure rather than a clear one. |
| 127 | - Topcoat ships Tailwind without Node, which reopens the design system attempt #1 |
| 128 | dropped purely to avoid an npm build step — see [ui.md](ui.md). |
| 129 | |
| 130 | ## Routing findings (Milestone 0) |
| 131 | |
| 132 | - **Topcoat 0.5 requires rustc ≥ 1.95.** On an older toolchain `cargo add topcoat` |
| 133 | silently resolves to an empty `topcoat v0.0.0` placeholder instead of failing. Local |
| 134 | stable is now 1.97.1. |
| 135 | - `Router::builder().discover()` collects `#[page]`-annotated items **at link time**, |
| 136 | so pages can live in any module. Layering is our choice, not the framework's. |
| 137 | - `module_router!` derives each URL from the module tree rather than a path string. |
aaefaabfeat: root handles, grouped routes, reserved-handle denylist1mo | 138 | Still deferred. Application routes now group cleanly (`auth/login`, `api/me`), but |
| 139 | handles sit at the root ([0004](decisions/0004-root-handles-grouped-routes.md)), so a |
| 140 | parameterised root segment still has to coexist with static ones. Worth checking how |
| 141 | `module_router!` handles that before committing to it. |
bd48b4bdocs: serve git over smart HTTP, reorder roadmap portfolio-first1mo | 142 | - Path and query params are read from `Cx` via `path_param!` / `#[query_params]`, not |
| 143 | injected as handler arguments. Parses are memoized per request. |
| 144 | - Layouts wrap by path prefix and nest outermost-first, and a layout can catch a page's |
| 145 | `NotFoundError` to render a branded 404. |
| 146 | - `HOST` / `PORT` configure the bind address, so `STEID_LISTEN_ADDR` is gone. |
| 147 | - `Body` is a boxed `http_body::Body` used for both requests and responses, with |
| 148 | `into_data_stream()` to read and `Body::new()` to wrap a stream — pack data can |
| 149 | stream both directions without buffering. This is what makes Milestone 5 viable. |