steid

@jamesgill /

steid/plans/current.md
25.6 KBCode·Blame·Raw
ab7fea9chore: plans setup1mo
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
3dd7293feat: ship Steid as an installable binary8d
7## Active: Milestone 5b — Deployable by anyone
a814db5feat: push and clone private repositories with a token8d
8
3dd7293feat: ship Steid as an installable binary8d
9**Goal:** a stranger can install Steid on a fresh Linux box in one command and reach it
10over HTTPS, and the author can publish an instance and push Steid's own source to it.
11Distributed as **a binary plus its assets**, not a container.
a814db5feat: push and clone private repositories with a token8d
12
3dd7293feat: ship Steid as an installable binary8d
13**Out of scope:** push-to-release and any CI/CD (8+ — see
14[ROADMAP.md]ROADMAP.md#milestone-ladder), multi-user registration (7), and hosting
15release artefacts on Steid itself, which it has no feature for.
16
17### Steps
18
19- [x] Release build producing `steid-<version>-<target>.tar.gz` — binary, `assets/`,
20 README — and verified to boot from a clean extraction
21- [x] `install.sh` — download, systemd unit, Caddy with automatic HTTPS, idempotent
22- [x] `README.md` — the repo has none, and it is the front door of a portfolio project
23- [x] Operability: `/healthz`, graceful shutdown on SIGTERM
24- [x] `STEID_SETUP_TOKEN` as an optional override, so claiming is not a race with
25 `journalctl`
26- [x] Rate limiting on `/auth/login` and `/auth/setup`
27- [x] Backup and restore, documented — two paths, rsync is enough
28
29### Done when
30
31`curl … | sh -s -- --domain git.example.com` on a fresh VPS yields a working HTTPS
32instance, and Steid's own source is pushed to it and browsable there.
33
34### Settled
35
36- **A binary plus `assets/`, not a container.** Topcoat can embed the asset *manifest*
37 (`Manifest::parse` + `include_str!`, for WASM) but not the asset *bytes* — that path
38 pairs with `AssetConfig::hosted_at`, which expects a CDN. A single self-contained file
39 would mean fighting the framework, so the artifact is a directory. The Dockerfile stays
40 as a secondary path.
41- **A TLS proxy is mandatory, not conventional.** Topcoat 0.5 has no TLS: no rustls, no
42 ACME, no HTTPS listener — checked, not assumed. Since git authenticates over HTTP
43 Basic, without TLS a personal access token crosses the network in cleartext on every
44 push. Caddy is chosen because automatic certificates are its headline feature and the
45 config is two lines.
46- **glibc, built on Debian bullseye — not static musl.** Two reasons, the second
47 decisive. musl failed on `ring` (Debian's `musl-gcc` wrapper rejects `-m64`) and would
48 need a real cross toolchain. But **musl's whole point is a binary with no runtime
49 dependencies, and Steid hard-requires `git` on `PATH`** — anyone installing it already
50 has a package manager, so the portability musl buys cannot be used. Building on
51 bullseye pins the glibc floor at 2.31, covering Debian 11+ and Ubuntu 20.04+; building
52 on bookworm would need 2.36 and silently exclude Ubuntu 22.04, which is still
53 everywhere. Verified: a release build in `rust:1.97-slim-bullseye` succeeds, 11.5 MB.
54- **The runtime binary links a TLS stack it never uses.** `ring``rustls``ureq`
55 Topcoat's `icon-iconify`/`tailwind` features, whose `ureq` exists to download the
56 Tailwind CLI *at build time*. Those features are enabled on the normal dependency as
57 well as the build one, so the crypto comes along for the ride. Moving them to
58 build-dependencies only would shrink the binary and drop an unused dependency from the
59 attack surface — worth trying, not yet attempted, and it is what made the musl attempt
60 fail where it did.
61- **Rsync is the deployment mechanism for now**, and that is enough while the artifact is
62 two paths. Push-to-release is the eventual answer and is parked at 8+.
8608574feat: personal access tokens, as a domain type8d
63
be4fac5docs: correct the Topcoat guidance in CLAUDE.md24d
64### Open
65
3dd7293feat: ship Steid as an installable binary8d
66- **Whether to add `STEID_TRUSTED_PROXY`.** The rate limiter keys on `X-Forwarded-For`
67 because **Topcoat 0.5 discards the peer address at accept time and never exposes it**
68 a handler sees headers and nothing else. Behind a proxy that is fine. Exposed directly,
69 a caller can vary the header for a fresh budget, leaving only the global cap. An
70 explicit "trust forwarded headers" flag defaulting to off would close it, at the cost
71 of one more thing an operator must get right. Not picked.
4ef3945feat: repository settings, README rendering, branch switcher, raw files7d
72- **`MAX_RAW_BYTES` is 10 MiB**, chosen rather than derived. It bounds what one raw
73 request can hold in memory, because `GitQuery` reads bytes rather than streaming them.
74 Raising it means a handful of concurrent requests can hold that much each.
75- **Renaming a repository** is still impossible, and now needs its own use case plus an
76 answer for moving the directory under every existing clone.
ef23868feat: rebuild the profile page on flat navigation7d
77- **The profile has no links.** `Organization` carries a display name and a bio and
78 nothing else, so the design's links row is not backed by data and was left out rather
79 than faked. A `links` field plus a settings control is the small feature that fixes it.
80- **`create_repo` and `serve_git` read the clock internally** rather than taking a `now`,
81 which is what `issue_token` does. Two call-site edits, or a `Clock` port if a third
82 case appears.
83- **A push touches `updated_at` twice** — once for the advertisement, once for the RPC.
84 Harmless, same second, but two writes per push.
3dd7293feat: ship Steid as an installable binary8d
85- **A licence.** A public portfolio repository probably wants one, and the README
86 deliberately says nothing about licensing rather than guessing.
0f19654feat: security headers on every response1d
87- ~~Blocked on a domain transfer~~**transferred 2026-08-29**, so Phases 1–6 of the
88 deployment runbook are unblocked.
7da29e0fix: the domain is jpgill.dev, not jpgilldev.com1d
89- **(historic) Blocked on a domain transfer** (noted 2026-08-29). `jpgill.dev` is the
4ef3945feat: repository settings, README rendering, branch switcher, raw files7d
90 intended host for both the instance and the release downloads; the transfer is in
91 flight. Phases 1–6 of the deployment runbook cannot start until DNS resolves, because
92 Caddy requests a certificate on startup. Nothing else is blocked by it — the artifact
93 is built and verified, and `install.sh` still needs its container dry-run.
3dd7293feat: ship Steid as an installable binary8d
94- **A domain.** Caddy needs a real hostname to obtain a certificate. This is the one
95 blocker that is DNS rather than code.
be4fac5docs: correct the Topcoat guidance in CLAUDE.md24d
96
4414bbadocs: record the first real deployment22h
97### Open
98
99- **Whether an absent *public* repository can 404 while private ones still 401.** The
100 uniform 401 from [0007]decisions/0007-tokens-over-http-basic.md is what makes a
101 private repository indistinguishable from one that does not exist — but it also turns
102 every mistyped URL into "Authentication failed", which cost real time on the first
103 deployment. The leak being prevented concerns *private* names only, so answering 404 for
104 a repository that would be public if it existed may cost nothing. Needs thought about
105 whether that is actually true.
607da17docs: licence Steid under AGPL-3.022h
106- **Zero-downtime deploys — deferred, deliberately.** `deploy.sh` currently stops the
107 service, swaps the binary and starts it: about two seconds. The plan, when it is worth
108 building: a systemd **template unit** `steid@.service` where the instance name *is* the
109 port, with `/opt/steid/3000` and `/opt/steid/3001` as separate install directories.
110 Deploy alternates — install to the idle port, start it, health-check it **directly on
111 loopback**, rewrite Caddy's upstream and `caddy reload` (graceful; existing connections
112 finish), then stop and disable the old one and enable the new so a reboot brings back
113 the right one.
114
115 Two things make this more than a shell script, and they are the reason it is deferred
116 rather than half-built:
117
118 - **Both processes share one SQLite database during the overlap.** WAL allows that, and
119 Steid's writes are short, so the overlap itself is fine. **Migrations are not.** The
120 new binary migrates on boot while the old one is still serving, so any schema change
121 that the old binary cannot tolerate — a new `NOT NULL` column, a dropped column —
122 breaks the still-live version. That is a constraint on how migrations are written
123 (**expand, deploy, contract**), not something the deploy script can solve.
124 - **An in-flight `git push` is cut when the old process stops.** `topcoat::start`
125 already drains on SIGTERM, so the fix is to stop the old one *after* Caddy has
126 repointed and let it finish what it has.
127
128 Worth building when instances belong to other people. At two seconds on a personal site,
129 it buys nothing today.
4414bbadocs: record the first real deployment22h
130- **No scheduled backups.** `/var/lib/steid` is the whole of the state and copies of it
131 exist only because they were taken by hand. This is the largest gap now that the
132 instance is live.
133- **Releases are not served from the instance yet.** The Caddy blocks are written and
134 commented in `deploy/Caddyfile`, and `install.sh`'s `RELEASE_BASE_URL` points at
135 `/jamesgill/repos/steid/releases`, but nothing has been rsynced there — so the
136 one-command install a stranger would run does not work yet.
137
6f0a608docs: fold the highlighting handover into plans17h
138### Opened by the section 1 wave
139
140- **Highlighting caches nothing.** The same file is re-highlighted on every view at
8daff1cfeat: blame reads as code, the same way the blob does15h
141 ~87 ms per thousand lines in release. Cache per blob object id when it shows. Blame
142 now pays this too, on top of being the most expensive read in `GitQuery` — the two
143 costs land on the same page, which is where it will show first.
6f0a608docs: fold the highlighting handover into plans17h
144- **Diffs, READMEs and markdown code fences are unhighlighted.** The README goes through
145 `web/markdown.rs`, which writes its own `<pre><code>`; wiring `highlight.rs` into it is
8daff1cfeat: blame reads as code, the same way the blob does15h
146 a small follow-up. Blame was the fourth of these and is now done, so the adapter is
147 reached from two pages and the pattern for a third is set: build the file's text in
148 line order, call `source_lines` **once**, render `Classed` through `Unescaped`.
610ab38feat: a .jsx file reads as JavaScript rather than as plain text16h
149- ~~`.jsx` is plain~~**aliased onto JavaScript.** `highlight.rs` gained an `ALIASES`
150 table, consulted only after the syntax set's own answer, so an alias can never
151 overrule a real grammar. It has one entry and should stay small.
6f0a608docs: fold the highlighting handover into plans17h
152- **`\r\n` files keep the `\r` inside highlighted markup.** Invisible under
153 `white-space: pre`; noted rather than fixed.
154- **The blob header does not name the detected language.** It is only visible as colour.
155
9b1feb6docs: fold the branches and tags handover into plans17h
156- **No ahead/behind counts on the branches page**, by decision: a `rev-list` per branch
157 is twenty forks for twenty branches. Waits for a kept-alive git.
158- **Neither refs page is paginated.** A thousand branches render a thousand rows.
159- **A repository with tags but no branches** says "No branches"; real but exotic, not
160 fixture-tested.
161- **`web::browse::{encode, timestamp}` are now `pub(super)`**, and `repo_stats` takes
00d9c06refactor: one place decides that git ran out of time16h
162 `handle` and `name` so the sidebar counts can link. `timed_out` has since moved to
163 `web::context`, as this said it should.
9b1feb6docs: fold the branches and tags handover into plans17h
164
4e4ba31docs: fold the archive and search handover into plans16h
165- **`git grep` output is collected whole before it is capped.** A one-letter query on a
166 big repository allocates all of git's output to keep 200 hits. Bounded by the 20 s
167 timeout, not by anything deliberate.
168- **The archive endpoint has no rate limiting** and is outside the read timeout, on
169 purpose (a large repository legitimately takes longer to pack). It is the most
170 expensive anonymous request in Steid: a full pack per hit. First thing to look at under
171 load. A late `git archive` failure is a truncated download, logged like the protocol
172 server's.
173- **Search has no revision switcher on the results page**, no paging, `trim()`s the
174 query, cuts matched lines at 500 characters (which can remove the match on a minified
175 line), and ignores `GrepHit::column` when marking.
176
64f6f0ddocs: fold the commit page and compare handover into plans16h
177- **The commit page's timeout state has never been rendered**, like search's and the
00d9c06refactor: one place decides that git ran out of time16h
178 refs pages'. The predicate itself is now `web::context::timed_out`, shared by `refs`,
179 `commit` and `blame`; each page still writes its own panel, and `search_repo` still
180 answers in the application layer.
64f6f0ddocs: fold the commit page and compare handover into plans16h
181- **`Diff::files` is emptied wholesale when the patch is truncated**, falling back to the
182 numstat list, so a 10 MiB commit shows no lines rather than the first few files.
183 numstat itself could in principle be truncated at ~300,000 changed files.
184- **A merge commit is diffed against its first parent only**, and merge rendering was
185 compiled, not seen: no fixture contains one.
186- **`MAX_RAW_BYTES` now does a second job** as the diff cap; retune both together.
187- **`bg-surface` is nearly `bg-background` in light mode**, so a file header reads only
188 by its border there. Pre-existing, but the commit page has one per file.
189- **No paging anywhere in history**: `/log` shows 50, a comparison 100, both say so.
190- **The compare form's `<datalist>` costs a `list_refs`** rendered inline on every visit.
191- ~~Nothing links to a commit by its sha yet~~ — done.
192
8804ad9feat: blame can change revision, like every other page under Code16h
193- ~~The blame page has no revision switcher~~**added**, and `Switch` in
194 `web/browse.rs` now has a `Blame` arm, so switching branch keeps you on blame at the
195 same path. It costs the page one `list_refs`, which is what the wave deferred.
691bf98docs: fold the blame handover into plans16h
196- **`BlameCommit::boundary` and `author_name` reach only the tooltip.** The row is sha,
197 summary, date and must stay one line. On a multi-user instance the author is the first
198 thing to revisit.
199- **Blame is the most expensive read in `GitQuery`** and the likeliest to meet the 20 s
200 timeout. Nothing but this page calls it.
fb21638docs: the section 1 wave, checked as one thing rather than five16h
201- ~~Cross-branch links were verified only after the merge~~**walked, 2026-09-05**,
202 see the integration pass below.
203
204#### The integration pass — 2026-09-05
205
206The five branches merged and run as one app, on a real instance holding Steid's own
207history: 9 branches, 2 annotated tags, one public repository and one private. Every
208link that crosses a feature boundary was followed in a browser, and **nothing was
209broken at a seam.** Landing sha → commit page; log sha → commit; commit's "Browse
210files" → tree at that sha and its parent sha → the parent's page; the counts and the
211sidebar → branches and tags; a branch row → tree, log and a compare with a real diff
212(a merged branch correctly says there is nothing to compare); a search hit's line
213number → `blob#L<n>` with the anchor present and landing; `Code · Blame · Raw` in both
214directions; blame's sha → the commit page and its line number → the blob anchor;
215`zip` and `tar.gz` for `main` and for a tag, both extracting under `steid-<rev>/`.
216**A private repository 404s on all eleven routes anonymously** — landing, log,
217branches, tags, commit, search, tree, blame, raw, archive, compare. Highlighting still
218renders after the blame toggle and the anchor edits landed on the same table: Rust and
219TOML coloured, a plain file not. Screenshots of the whole flow in dark and light are
220in `target/shots/`.
221
222Two things the pass found, neither a break:
223
224- **The sidebar's Download links can only ever offer the default branch.**
225 `clone_block` takes a revision and its doc says the links are "for the revision being
226 viewed", but the sidebar renders only on the landing page, which has no `{rev}` — and
227 the tree and blob pages, which do, are single-column by design. So a tag's tarball is
228 reachable only by typing `/archive/v0.2.0.zip`. Both formats work at both revisions;
229 it is the entry point that is missing, and putting it on the tree page means deciding
230 whether that page gets a sidebar, which [ui.md]ui.md#the-repository-page settled the
231 other way.
8daff1cfeat: blame reads as code, the same way the blob does15h
232- ~~Blame renders its lines unhighlighted while the blob highlights them~~**closed.**
233 Blame's code cell now goes through `source_lines`, the blob's own adapter, so the two
234 views share classes, caps and plain fallback. Both render the same file to an
235 identical set of `hl-` spans, and rows stay 19.5px — the only variance is at a run's
236 hairline, which is `border-collapse` splitting that 1px, not the markup.
691bf98docs: fold the blame handover into plans16h
237
e0856ebfeat: clone a public repository over HTTP8d
238### Carried over — small, unblocked
d7b99d9docs: record milestone 0 progress and routing findings1mo
239
47db238feat: serve the git protocol through http-backend, behind a port8d
240- **A client that disappears mid-request leaves the body-copy task waiting.** The copy
e0856ebfeat: clone a public repository over HTTP8d
241 into git's stdin runs in its own task and nothing cancels it if the connection drops.
47db238feat: serve the git protocol through http-backend, behind a port8d
242 Bounded by the backend exiting and closing the pipe, but not by anything deliberate.
a814db5feat: push and clone private repositories with a token8d
243- **`REMOTE_USER` is not set on the backend**, so a push is recorded in the repository's
244 reflog without naming who made it. Steid knows the actor by then; it simply is not
245 passed through. Small, and worth doing before anything reads reflogs.
0f19654feat: security headers on every response1d
246- **No automated test asserts the security headers.** There is no HTTP-level test
247 harness, so losing the CSP would be silent. The likeliest regression is someone using
248 `insert()` instead of `or_insert()` and flattening the raw endpoint's stricter policy.
249- **`cargo audit` has never been run**, and the dependency tree has not been reviewed.
a814db5feat: push and clone private repositories with a token8d
250- **No rate limiting on token authentication.** A token is 256 bits so guessing is not
251 the worry; unbounded hashing on an open endpoint is.
252- **Tokens have no expiry and no last-used timestamp.** Both deliberate omissions for
253 now — see [0007]decisions/0007-tokens-over-http-basic.md — but a token list with no
254 "last used" makes it hard to know which are safe to revoke.
e0856ebfeat: clone a public repository over HTTP8d
255- **A subprocess per git request.** Unlike Milestone 3's once-per-creation, this is on a
256 hot path and has not been measured. Milestone 5 is where that bill comes due.
257- **Streaming is by construction, not by measurement.** The response body is never
258 collected, but no clone large enough to prove it has been run.
6a2a925docs: close Milestone 3, plan Milestone 4a8d
259- **An orphaned repo directory is possible** if the process dies between the record
260 write and the filesystem write, and it then blocks re-creating that name. The durable
261 fix is a reconciliation sweep on boot
262 ([architecture.md]architecture.md#db-plus-filesystem-writes); clearing one is a
263 manual `rm` today, since repo deletion does not exist.
264- **The duplicate-name check races.** The loser is caught by `init_bare` or the unique
265 constraint, but surfaces as an opaque storage error rather than "name taken".
266- **Bare repos created on macOS carry `ignorecase = true`.** A migration gotcha if the
267 data directory ever moves to Linux.
0aca94efeat: a repository is one place, and its landing page says what it is17h
268- ~~**Light mode is still untested**~~**looked at 2026-09-05** on the repository
269 landing page, a tree and a blob, by temporarily flipping the layout's `class="dark"`
270 and screenshotting. Nothing was wrong: every colour on these pages already comes from
271 a token. The profile, the settings pages and the log have still not been checked, and
272 **there is still no way for a visitor to choose** — the layout hardcodes `dark`.
dce0bf3feat: browse a repository's files and history8d
273- **Submodule rendering was never seen**, only compiled: no fixture contained one.
4ef3945feat: repository settings, README rendering, branch switcher, raw files7d
274- **The `/log` page's switcher opens with nothing marked current** when no revision is
275 in the URL, because `repo_log` still does not report the revision it resolved. Now more
276 visible than before, since there is a switcher to look wrong.
277- **Task-list items keep their bullet** and footnotes render in place rather than
278 collected at the end. Both cosmetic.
0aca94efeat: a repository is one place, and its landing page says what it is17h
279- **The landing page now makes 15 `git` processes** for a repository with a README and
280 a licence (10 without, 1 for an empty one), up from 7. Five are the About sidebar's
281 and run concurrently, so the *latency* is roughly one call — but it is fifteen forks
282 per view, and this is now the most expensive page in Steid. The fix is 0006's
283 kept-alive `cat-file --batch`, not trimming the sidebar.
284- **`count_commits` costs two processes, not one.** It resolves the revision before
285 running `rev-list --count`, because `rev-list` is fatal on an empty repository and on
286 an unknown branch, and this module's rule is that a non-zero exit from git is always a
287 real fault. `--ignore-missing` was tried: it covers a bad object id, not a bad
288 revision *name*.
289- **`bg-muted` is not a token and never was.** Three `<pre>` blocks used it, so they
290 have been rendering with no background at all — silently, exactly as `ui.md` warns
c3ca6dafix: the new token sits on a background that exists16h
291 about classes Tailwind never ships. Fixed on the clone block, the empty-repository
292 push snippet and, last, `token.rs`'s new-token block. All three now use `bg-surface`,
293 and `bg-muted` appears nowhere in `src/` — only `bg-muted-foreground`, which is real.
0aca94efeat: a repository is one place, and its landing page says what it is17h
294- **The licence sniffer reads 1 KiB, not the 200 bytes first sketched.** BSD-2 and
295 BSD-3 differ only by a third clause about 900 bytes in. `LICENSE-MIT` and other
296 suffixed spellings are not detected: the filename list is deliberately short, because
297 every extra candidate is another speculative blob read.
dce0bf3feat: browse a repository's files and history8d
298- **A per-file last-commit column is still absent**, deliberately — see
299 [0006]decisions/0006-git-binary-behind-narrow-ports.md#amendment--20260829-the-milestone-5-read-path.
300 Wanting it is the trigger to move to a kept-alive `cat-file --batch`, not to reopen
301 `gix`.
2eb8681docs: put git next, plan the repo model24d
302- **Fonts are not loaded.** The theme names Geist and IBM Plex Mono; both fall back
303 today. Topcoat's `font-fontsource` feature handles it.
5d3dfa5feat: a global top bar, and pages choose their own width22h
304- **The auth pages are unstyled.** `/auth/setup` and `/auth/login` are still bare
305 milestone-1 HTML, and the styled top bar now sits right above them making it obvious.
306- **The top bar's wordmark says "steid"**, not the owner's identity — see the open
307 question at the end of [ui.md]ui.md#the-shell.
2eb8681docs: put git next, plan the repo model24d
308- **Light mode is untested.** The palette defines it; nobody has looked at it.
f0444b7docs: plan milestone 2 in two phases1mo
309- **No rate limiting** on `/auth/login` or `/auth/setup`.
076dbc9docs: close milestone 1, open milestone 21mo
310- **`sweep_expired` is never called**, so expired session rows accumulate. Expiry is
311 enforced on read, so this is tidiness, not a hole.
2eb8681docs: put git next, plan the repo model24d
312- **CSRF.** `SameSite=Lax` covers the common case. Forms now exist, so this is decidable
313 rather than hypothetical.
ab7fea9chore: plans setup1mo
314
315## Backlog
316
317Ordered. Pull from the top.
318
3dd7293feat: ship Steid as an installable binary8d
3191. **Milestone 6 — Writing.** Posts, markdown, `/{handle}/posts/{slug}`. Open when it
320 starts: which markdown crate, and whether raw HTML in markdown is trusted — safe for a
321 single author, a stored-XSS hole the moment Milestone 7 adds a second user. A
322 repository's README rendering on its page falls out of the same pipeline.
ab7fea9chore: plans setup1mo
323
31a6a7edocs: record the forge feature survey and what can run in parallel18h
324### Forge feature candidates (surveyed 2026-09-05)
325
326Not yet ordered against Milestone 6. Grouped by what they touch, because the plan is to
327run several agents at once and the grouping is what decides what can run together.
328**Section 1 is being tackled first.** Shared hotspots: `GitQuery` in `port.rs` (every
329browsing feature appends a method), `git_query.rs`, the repo sub-nav, and `plans/`
330itself. New tables touch `sqlite.rs` + `in_memory.rs` and should merge one at a time.
331
fb21638docs: the section 1 wave, checked as one thing rather than five16h
332**Section 1 is done**, apart from the per-file last-commit column — which is not a
333gap but a deliberate wait for the kept-alive `cat-file --batch`
334([0006]decisions/0006-git-binary-behind-narrow-ports.md). Shipped: the two-column
335repository landing page, syntax highlighting, branches and tags pages, archive
336download, code search, the commit page, compare, and blame. All eight were merged and
337then walked together as one app; see the integration pass above. See
338[ui.md]ui.md#the-repository-page for the layout and the entry-point map, and
339[progress.md]progress.md for what each cost.
340
3411. ~~**Read-only browsing**~~**done**, except the **per-file last-commit column**,
342 which stays blocked on the
343 [0006]decisions/0006-git-binary-behind-narrow-ports.md amendment: wanting it is the
344 trigger for a kept-alive `cat-file --batch`, not for reopening `gix`.
31a6a7edocs: record the forge feature survey and what can run in parallel18h
3452. **Repo model** (a column or use case each; merge serially): rename · default branch
346 setting · archived flag · topics and pinned repos on the profile · orphan-directory
347 reconciliation sweep on boot.
3483. **Collaboration** (8+): issues · pull requests (needs the commit and compare pages,
349 plus a merge on the bare repo) · labels and milestones.
3504. **Transport and git ops**: import from URL (mirror clone) · post-receive events and
351 webhooks (first brick of CI) · `REMOTE_USER` passthrough · LFS (not yet) · SSH
352 (would reverse [0001]decisions/0001-git-over-http-not-ssh.md).
3535. **Identity and auth**: token expiry and last-used · rate limiting on token auth ·
354 CSRF (touches every form — run alone) · Milestone 7 multi-user (run alone).
3556. **Portfolio**: posts (Milestone 6) · profile links · Atom feeds for posts and commits.
3567. **Platform and quality**: scheduled backups and served releases (closes 5b) ·
357 HTTP-level test harness plus security-header tests and `cargo audit` · `/api`
358 coverage for existing use cases · styled auth pages, light mode, fonts.
359
fb21638docs: the section 1 wave, checked as one thing rather than five16h
360Process decisions, now answered by the wave rather than open: agents **do** get
361worktrees and short-lived branches, which is a deliberate bend in the commit-to-main
362rule and worth keeping for a wave; and agents do **not** edit `plans/` — each writes a
363handover the merging session folds in, because five agents appending to `current.md`
364conflict every time. The integration pass afterwards is not optional: it is the only
365place a cross-feature link is ever exercised.
31a6a7edocs: record the forge feature survey and what can run in parallel18h
366
ab7fea9chore: plans setup1mo
367## Open questions
368
bd48b4bdocs: serve git over smart HTTP, reorder roadmap portfolio-first1mo
369- **Topcoat is early** (v0.5.0, first released 2026-07-22, breaking changes expected
370 by its own authors). Expect churn that isn't feature work.
371- Topcoat ships Tailwind without Node, which reopens the design system attempt #1
372 dropped purely to avoid an npm build step — see [ui.md]ui.md.
373
374## Routing findings (Milestone 0)
375
376- **Topcoat 0.5 requires rustc ≥ 1.95.** On an older toolchain `cargo add topcoat`
377 silently resolves to an empty `topcoat v0.0.0` placeholder instead of failing. Local
378 stable is now 1.97.1.
379- `Router::builder().discover()` collects `#[page]`-annotated items **at link time**,
380 so pages can live in any module. Layering is our choice, not the framework's.
381- `module_router!` derives each URL from the module tree rather than a path string.
aaefaabfeat: root handles, grouped routes, reserved-handle denylist1mo
382 Still deferred. Application routes now group cleanly (`auth/login`, `api/me`), but
383 handles sit at the root ([0004]decisions/0004-root-handles-grouped-routes.md), so a
384 parameterised root segment still has to coexist with static ones. Worth checking how
385 `module_router!` handles that before committing to it.
bd48b4bdocs: serve git over smart HTTP, reorder roadmap portfolio-first1mo
386- Path and query params are read from `Cx` via `path_param!` / `#[query_params]`, not
387 injected as handler arguments. Parses are memoized per request.
388- Layouts wrap by path prefix and nest outermost-first, and a layout can catch a page's
389 `NotFoundError` to render a branded 404.
390- `HOST` / `PORT` configure the bind address, so `STEID_LISTEN_ADDR` is gone.
391- `Body` is a boxed `http_body::Body` used for both requests and responses, with
392 `into_data_stream()` to read and `Body::new()` to wrap a stream — pack data can
ca76e1bdocs: fix milestone cross-references after the reorder24d
393 stream both directions without buffering. This is what makes Milestone 4 viable.