# Current

> Keep this file short. One active step, one ordered backlog. Completed work moves to
> [progress.md](progress.md). If this file starts reading like a changelog, it has
> drifted — that's exactly what went wrong last time.

## Active: Milestone 2 — Profile page

**Goal:** `/user/{handle}` becomes the real profile page, replacing the Milestone 0
placeholder. This is the frame the rest of the product hangs in — repos, writing, and
projects all appear on it later.

**Explicitly out of scope:** repos and posts don't exist yet, so the page shows
identity plus empty sections. Editing display name / bio is Milestone 2 only if it
stays small.

### Steps

- [x] Settle URL shape — scoped under `/user/{handle}`, see
      [0003](decisions/0003-scoped-urls.md)
- [ ] Settle routing mechanics: `module_router!` vs explicit `#[page]` paths, and
      `path_param!` for `{handle}` — deferred from Milestone 0
- [ ] Migration: `orgs.kind` (`personal` | `shared`), backfilled `personal`
- [ ] `/user/{handle}` — public page, resolves the org by handle, 404 when unknown
- [ ] `/` redirects to the owner's profile once claimed
- [ ] Owner-only affordances visible when the viewer is the owner
- [ ] `/api/users/{handle}` — the API surface for the same read model

### Done when

`/user/{handle}` renders for a signed-out visitor, shows extra affordances to the
owner, and an unknown handle 404s rather than erroring.

### Watch for

- **Handle lookups are case-insensitive** in storage (`collate nocase`) and lowercased
  by `OrgName::new`. A URL with different casing must resolve, not 404.
- ~~**Reserved handles.**~~ Resolved by [0003](decisions/0003-scoped-urls.md): handles
  are scoped under `/user/`, so they cannot collide with application routes and no
  denylist is needed.
- **A profile is public.** It is the first page rendering for anonymous visitors by
  design, so anything private must be gated explicitly rather than by assuming a
  session exists.

### Carried over — small, unblocked, worth doing alongside

- **Flash messages.** A wrong password or setup token bounces with no explanation.
  Deliberate on the security side, but indistinguishable from a broken form. Every
  form added from here inherits the problem.
- **No rate limiting** on `/login` or `/setup`.
- **`sweep_expired` is never called**, so expired session rows accumulate. Expiry is
  enforced on read, so this is tidiness, not a hole.
- **CSRF.** `SameSite=Lax` covers the common case; whether forms also want tokens is
  still undecided.
- **Styling.** Everything is unstyled HTML. Topcoat ships Tailwind without Node, and
  `steid-backup/AGENTS/UI.md` has a full OKLCH system to mine. Cheaper at two pages
  than at ten — see [ui.md](ui.md).

## Backlog

Ordered. Pull from the top.

1. **Milestone 3 — Writing.** Posts, markdown rendering, `/user/{handle}/posts/{slug}`.
   *Open question: is writing actually the first portfolio feature, or is it
   projects/showcases?*
2. **Milestone 4 — Repo model.** `Repository` entity, `Visibility`, `create_repo`,
   bare repo on disk at `{data_dir}/{org}/{repo}.git`. Watch the DB-plus-filesystem
   atomicity problem — see [architecture.md](architecture.md#db-plus-filesystem-writes).
3. **Milestone 5 — Git over HTTP.** `git http-backend` subprocess, PATs over HTTP
   Basic. See [0001](decisions/0001-git-over-http-not-ssh.md).

## Open questions

- **Topcoat is early** (v0.5.0, first released 2026-07-22, breaking changes expected
  by its own authors). Expect churn that isn't feature work.
- Body size limits will reject large pushes at Milestone 5 — `topcoat-router` has a
  `body_limit` layer that needs raising on the git routes. Recorded here because it
  will surface as a confusing failure rather than a clear one.
- Topcoat ships Tailwind without Node, which reopens the design system attempt #1
  dropped purely to avoid an npm build step — see [ui.md](ui.md).

## Routing findings (Milestone 0)

- **Topcoat 0.5 requires rustc ≥ 1.95.** On an older toolchain `cargo add topcoat`
  silently resolves to an empty `topcoat v0.0.0` placeholder instead of failing. Local
  stable is now 1.97.1.
- `Router::builder().discover()` collects `#[page]`-annotated items **at link time**,
  so pages can live in any module. Layering is our choice, not the framework's.
- `module_router!` derives each URL from the module tree rather than a path string.
  Still deferred. Now that URLs are scoped ([0003](decisions/0003-scoped-urls.md)) the
  module tree and the URL tree line up — `user/handle/repos/name` — which makes
  `module_router!` a much better fit than it was under root-level handles.
- Path and query params are read from `Cx` via `path_param!` / `#[query_params]`, not
  injected as handler arguments. Parses are memoized per request.
- Layouts wrap by path prefix and nest outermost-first, and a layout can catch a page's
  `NotFoundError` to render a branded 404.
- `HOST` / `PORT` configure the bind address, so `STEID_LISTEN_ADDR` is gone.
- `Body` is a boxed `http_body::Body` used for both requests and responses, with
  `into_data_stream()` to read and `Body::new()` to wrap a stream — pack data can
  stream both directions without buffering. This is what makes Milestone 5 viable.
