| | @@ -4,130 +4,88 @@ |
| 4 | 4 | > [progress.md](progress.md). If this file starts reading like a changelog, it has |
| 5 | 5 | > drifted — that's exactly what went wrong last time. |
| 6 | 6 | |
| 7 | | −## Active: Milestone 2 — Profile page |
| 8 | | − |
| 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. |
| 12 | | − |
| 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 | | − |
| 17 | | −### Phase 1 — the page |
| 18 | | − |
| 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)) |
| 23 | | −- [x] Reserved-handle denylist in `OrgName::new`; auth routes moved under `/auth/` |
| 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, |
| 28 | | − case-insensitive |
| 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 |
| 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 | | − |
| 39 | | −- [x] Styling: Tailwind via Topcoat, theme retuned, primitives copied in, `flash` |
| 40 | | − written by hand ([0005](decisions/0005-tailwind-and-copied-components.md)) |
| 41 | | −- [x] Flash messages — `flash` component, hand-written; the registry has no alert |
| 42 | | −- [x] `/{handle}/settings` — edit display name and bio, owner only, enforced in the use |
| 43 | | − case (settings belong to the org, and this scales to organisations) |
| 44 | | −- [x] Owner-only affordances on the profile (edit link) |
| 7 | +## Active: Milestone 3 — Repo model |
| 8 | + |
| 9 | +**Goal:** repositories exist as records and as bare git repos on disk, and they appear |
| 10 | +on the profile. No git protocol yet — that is milestone 4. This milestone fills the |
| 11 | +Repositories section and gets the storage layout right before anything serves it. |
| 12 | + |
| 13 | +**Out of scope:** clone, push, browsing a tree, README rendering, forks, stars. |
| 14 | +Deleting a repo — worth having, but it makes the filesystem/database consistency |
| 15 | +problem twice as interesting, so not in the first pass. |
| 16 | + |
| 17 | +### Steps |
| 18 | + |
| 19 | +- [ ] Domain: `RepoId`, `RepoName`, `Visibility` (Public/Private), `Repository` |
| 20 | +- [ ] Domain: `RepoRepository` port — `find_by_id`, `find_by_org_and_name`, |
| 21 | + `list_by_org`, `save` |
| 22 | +- [ ] Infrastructure: in-memory + SQLite implementations, migration |
| 23 | +- [ ] Application: `GitStorage` port — `init_bare`, `repo_path` |
| 24 | +- [ ] Infrastructure: `DiskGitStorage`, shelling out to `git init --bare` |
| 25 | +- [ ] Application: `create_repo` use case — owner only, validates, creates record and |
| 26 | + bare repo |
| 27 | +- [ ] Application: `list_repos` / `view_repo` read models — visibility-aware |
| 28 | +- [ ] Web: `/{handle}/repos/new` form, `/{handle}/repos/{name}` page |
| 29 | +- [ ] Web: the profile's Repositories section lists what the viewer may see |
| 30 | +- [ ] `/api/users/{handle}/repos` |
| 45 | 31 | |
| 46 | 32 | ### Done when |
| 47 | 33 | |
| 48 | | −Signed out, `/{handle}` renders the owner's display name and handle and nothing |
| 49 | | −private. An unknown handle 404s. The owner can set a display name and bio and see them |
| 50 | | −on the page. `/api/users/{handle}` returns the same public view. |
| 51 | | − |
| 52 | | −### Findings — phase 2 |
| 53 | | − |
| 54 | | −- **Components are invoked bare inside `view!`** — `label(attrs: …, "Text")`, not |
| 55 | | − `(label(…)?)`. `if`, `match`, `for` and `let` are native to the macro too, so the |
| 56 | | − wrapper-block pattern is unnecessary. |
| 57 | | −- **`#[query_params]` needs `error = …`** to be usable with `?`. Without it the `Err` |
| 58 | | − side borrows from `cx` and the borrow escapes the handler. |
| 59 | | −- **Post-redirect-get on success, re-render on failure.** A redirect after an error |
| 60 | | − would discard what was typed and lose the reason — the exact problem `flash` exists |
| 61 | | − to solve. |
| 62 | | −- `PublicProfile` carries `display_name` separately from `label`, so an edit form can |
| 63 | | − leave the field empty rather than prefilling the handle. |
| 64 | | − |
| 65 | | −### Findings — phase 1 |
| 66 | | − |
| 67 | | −- **`path_param` is an attribute in Topcoat 0.5**, applied to a tuple struct |
| 68 | | − (`#[path_param] struct Handle(str);`), not the function-like `path_param!(handle)` |
| 69 | | − that the docs on `main` describe. **Read the vendored crate, not GitHub `main`** — |
| 70 | | − the framework is two weeks old and the two have already diverged. |
| 71 | | −- A `str` inner type yields the raw percent-decoded segment with no parsing, which |
| 72 | | − suits validating through `OrgName` and 404ing what fails. |
| 73 | | −- **Static routes beat parameterised ones**, so `/auth/login` and `/api/me` still work |
| 74 | | − with `/{handle}` registered at the root. Verified, not assumed. |
| 75 | | −- Topcoat serves bundled assets from `/_topcoat/assets/…` with content-hashed URLs. |
| 34 | +The owner creates a repo through the UI, a bare repo appears at |
| 35 | +`{data_dir}/{handle}/{name}.git`, and it is listed on the profile. A private repo is |
| 36 | +invisible to a signed-out visitor. `git clone` does **not** work yet — that is |
| 37 | +milestone 4. |
| 38 | + |
| 39 | +### Decide during |
| 40 | + |
| 41 | +- **Repo name rules.** `OrgName` allows `[a-z0-9-]` and lowercases. Repo names |
| 42 | + conventionally allow dots and underscores (`.github`, `my_repo`, `foo.js`) and are |
| 43 | + often case-preserving. Following `OrgName` exactly is simplest and rejects names |
| 44 | + people will reasonably want; allowing more means deciding about case-insensitive |
| 45 | + uniqueness and about names that are awkward on disk. |
| 46 | +- **`new` collides.** `/{handle}/repos/new` is a static route and static beats |
| 47 | + parameterised, so a repo actually named `new` would be unreachable at its own URL. |
| 48 | + Same problem as handles, same fix: a small reserved list on `RepoName`. Reserve |
| 49 | + before the first repo exists. |
| 50 | +- **Visibility default.** Public matches a portfolio-first product; private matches |
| 51 | + every forge people are used to. |
| 76 | 52 | |
| 77 | 53 | ### Watch for |
| 78 | 54 | |
| 79 | | −- **Do not leak email.** `describe_identity` carries email, and `/api/me` returns it — |
| 80 | | − correctly, because that endpoint describes the caller to themselves. The public |
| 81 | | − profile needs its **own** read model; reusing `Identity` would publish the owner's |
| 82 | | − email address to anonymous visitors. This is the single most likely mistake in this |
| 83 | | − milestone. |
| 84 | | −- **Route precedence** between static routes and `/{handle}`. The reserved list stops a |
| 85 | | − user *owning* `auth`, but it does not stop the router matching `/api/me` against |
| 86 | | − `/{handle}/{x}` and shadowing the real route. Static-over-parameterised is near |
| 87 | | − universal, so this is an assertion when the route lands, not a blocking spike. |
| 88 | | −- **Case-insensitive handles.** Storage is `collate nocase` and `OrgName::new` |
| 89 | | − lowercases, so `/JamesGill` must resolve rather than 404. |
| 90 | | −- **Authorization on settings** belongs in the use case, taking an `Actor` — not in the |
| 91 | | − page. Otherwise `/api` gets a different answer from the web form. |
| 92 | | −- **A profile is public.** This is the first page rendering for anonymous visitors by |
| 93 | | − design, so anything private must be gated explicitly rather than by assuming a |
| 94 | | − session exists. |
| 95 | | −- **Reserve handles early.** Adding to the denylist later is a breaking change for |
| 96 | | − whoever holds that handle ([0004](decisions/0004-root-handles-grouped-routes.md)). |
| 97 | | − |
| 98 | | −### Settled |
| 99 | | − |
| 100 | | −- **Settings live at `/{handle}/settings`.** They belong to the organisation, which |
| 101 | | − scales to real organisations in Milestone 7 without moving. |
| 102 | | −- **Render the full frame from the start**, including sections with nothing in them. |
| 103 | | − A profile page that renders almost nothing is a poor start for a portfolio-first |
| 104 | | − product; the shape of the page is part of what is being built, not scaffolding for |
| 105 | | − it. |
| 55 | +- **The database and the filesystem cannot share a transaction.** Creating a repo |
| 56 | + writes a row and a directory. Neither previous attempt solved this properly — see |
| 57 | + [architecture.md](architecture.md#db-plus-filesystem-writes). A compensating delete is |
| 58 | + good enough to ship, but write down that an orphaned directory is possible if the |
| 59 | + process dies between the two, rather than rediscovering it. |
| 60 | +- **Path traversal.** `{data_dir}/{handle}/{name}.git` is built from user input. A name |
| 61 | + containing `..` or `/` must be impossible before it reaches the filesystem, and |
| 62 | + `RepoName` is the place to make it impossible rather than sanitising at the call site. |
| 63 | +- **Visibility is an authorization decision**, so it belongs in the use case. A private |
| 64 | + repo must be absent from listings, not merely unlinked — and `/api` must agree with |
| 65 | + the page. |
| 66 | +- **`git` becomes a runtime dependency** from this milestone. The runbook should say so. |
| 106 | 67 | |
| 107 | 68 | ### Carried over — small, unblocked |
| 108 | 69 | |
| 70 | +- **Fonts are not loaded.** The theme names Geist and IBM Plex Mono; both fall back |
| 71 | + today. Topcoat's `font-fontsource` feature handles it. |
| 72 | +- **Light mode is untested.** The palette defines it; nobody has looked at it. |
| 109 | 73 | - **No rate limiting** on `/auth/login` or `/auth/setup`. |
| 110 | 74 | - **`sweep_expired` is never called**, so expired session rows accumulate. Expiry is |
| 111 | 75 | enforced on read, so this is tidiness, not a hole. |
| 112 | | −- **CSRF.** `SameSite=Lax` covers the common case; whether forms also want tokens is |
| 113 | | − still undecided. Phase 2 adds a form, so this is the natural time to settle it. |
| 114 | | −- **Light mode is untested.** The palette defines it, but every page has been looked at |
| 115 | | − dark-only. There is no toggle yet either. |
| 116 | | −- **Fonts are not loaded.** The theme names Geist and IBM Plex Mono; neither is |
| 117 | | − installed, so both fall back. `topcoat`'s `font-fontsource` feature handles this. |
| 76 | +- **CSRF.** `SameSite=Lax` covers the common case. Forms now exist, so this is decidable |
| 77 | + rather than hypothetical. |
| 118 | 78 | |
| 119 | 79 | ## Backlog |
| 120 | 80 | |
| 121 | 81 | Ordered. Pull from the top. |
| 122 | 82 | |
| 123 | | −1. **Milestone 3 — Writing.** Posts, markdown rendering, `/{handle}/posts/{slug}`. |
| 124 | | − *Open question: is writing actually the first portfolio feature, or is it |
| 125 | | − projects/showcases?* |
| 126 | | −2. **Milestone 4 — Repo model.** `Repository` entity, `Visibility`, `create_repo`, |
| 127 | | − bare repo on disk at `{data_dir}/{org}/{repo}.git`. Watch the DB-plus-filesystem |
| 128 | | − atomicity problem — see [architecture.md](architecture.md#db-plus-filesystem-writes). |
| 129 | | −3. **Milestone 5 — Git over HTTP.** `git http-backend` subprocess, PATs over HTTP |
| 130 | | − Basic. See [0001](decisions/0001-git-over-http-not-ssh.md). |
| 83 | +1. **Milestone 4 — Git over HTTP.** `git http-backend` subprocess, PATs over HTTP |
| 84 | + Basic. See [0001](decisions/0001-git-over-http-not-ssh.md). The `body_limit` cap will |
| 85 | + reject large pushes until raised. |
| 86 | +2. **Milestone 5 — Repo browsing.** Tree, blob, commit log. |
| 87 | +3. **Milestone 6 — Writing.** Posts, markdown, `/{handle}/posts/{slug}`. Still open |
| 88 | + whether writing or projects/showcases is the better first portfolio feature. |
| 131 | 89 | |
| 132 | 90 | ## Open questions |
| 133 | 91 | |