Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0:2222/vole.git

A free and open-source forge that lives in your Git repo.

Reorganise the web package around one implementation per rule

c717d4eb92b4257b05fa065918b0071f6302f53e · Keenan Nemetz · 13d ago · parent 4506a4d3

Reorganise the web package around one implementation per rule

server/web had grown by feature, so several rules and widgets existed twice: the
comment stack for issues and change requests, the pager, the file tree, the write
preamble, and the ACL predicates. Each pair agreed today and would drift
tomorrow. No route, stored format, or permission rule changes; the few behaviour
differences found along the way are listed at the end.

Files now say what they answer:

  web.go          the Web type, render, open, the error pages
  page.go         Page, pageSection, who, nav
  templatefuncs.go the template function map, with the escaping rules noted
  ui.go           relative time, avatars, breadcrumbs, the pager
  markdown.go     the goldmark and bluemonday boundary
  codebrowser.go  the code browser, out of web.go
  tokens.go       the label/assignee token input
  rolepicker.go   the role control, out of admin.go
  comments.go     the comment view, thread builder, action menus, snippet
  comment_writes.go  add, edit and delete a comment, for both surfaces
  write_context.go   beginWrite/beginPost/beginAdminPost, fail, refuse
  capabilities.go    the ACL rules a page and a handler share
  audit.go        the audit log
  request_view.go the change conversation page, out of requests.go
  admin_access.go the instance-admin access rules, out of settings.go

A comment is one thing, so it has one view and one thread builder: metadata.Comment
is the same type on both surfaces and only a change request can anchor one to a
diff line. Adding, editing and deleting are one implementation each, with a
commentTopic naming what differs per surface. The pager is one control for issues,
changes and the instance feed; the file tree is one widget for the code browser
and a change's Files tab.

Every write starts at beginWrite (session, body bound, CSRF, repository, rules,
form) and asks capabilities.go for the one rule it needs, so the control a page
draws and the check a handler makes cannot disagree. A rejected write answers
through refuse; a write that fails for another reason answers through fail, which
re-renders the section for HTMX and returns text otherwise.

Templates follow the same split: chrome.html, components.html, diff.html and
role-picker.html hold what more than one surface renders, and each page owns its
fragments. The route table moved to routes.go, one function per surface.

Behaviour changes found while unifying:

  - A reply cycle (two comments answering each other, possible through a pushed
    metadata branch) used to drop both comments from the page; anything the walk
    does not reach is now emitted as a root.
  - A rejected anchored comment keeps its "Commenting on path:line" note and
    hidden anchor fields, so the reviewer does not have to pick the line again.
  - A repository whose access rules cannot be read answers 500 rather than 403;
    the account was being blamed for a broken instance.
  - Pager wording is now "Showing a-b of n / page x of y" with Previous/Next
    everywhere, and the instance feed's page links keep their kind filter.
  - The activity feed, issue list and change list share one page control.

Docs: docs/ui-plan.md gains a "Where the code lives" map, the write gate, the
shared comment model, the route table, and rules for rejected writes, shared
template fragments, and the stylesheet's section order. The passkey script is now
an ordinary asset (static/auth.js) under the same CSP as the rest.

docs/ui-plan.md +93

@@ -215,6 +215,86 @@ that is shown only to instance admins. Every admin write uses the existing store
215 215 CSRF, and the audit log; the web is a second surface over the same operations,
216 216 not a second implementation.
217 217
218 ## Where the code lives
219
220 `server/web` is the whole HTML surface. It is one package, split by what a file
221 answers rather than by layer:
222
223 | File | Holds |
224 |---|---|
225 | `web.go` | the `Web` type, `New`, `render`, `open`, the error pages, `htmxRequest` |
226 | `page.go` | `Page`, `pageSection`, `who`, `nav` — the data every page and fragment starts from |
227 | `templatefuncs.go` | the template function map, with a note on escaping (`esc` vs `pathurl`) |
228 | `ui.go` | relative time, avatar initials and tone, breadcrumbs, `safePath`, the `pager` |
229 | `markdown.go` | the goldmark + bluemonday boundary and `#<ulid>` cross-references |
230 | `auth.go`, `auth_handlers.go` | passkeys, sessions, CSRF, the middleware, the sign-in pages |
231 | `templates.go`, `icons.go`, `file_icons.go` | embedding, the icon set, language classes |
232 | `templates/` | one file per destination; `chrome.html`, `components.html`, `diff.html` and `role-picker.html` hold what two surfaces share |
233 | `home.go`, `activity.go` | the landing page and the two activity feeds |
234 | `overview.go`, `codebrowser.go`, `code_tree.go`, `code_search.go`, `commit.go`, `compare.go`, `diff.go`, `file_history.go`, `highlight.go` | one code-side read surface each |
235 | `issues.go`, `requests.go`, `request_view.go` | the issue pages, the change-request list and new form, the change conversation page |
236 | `request_files.go` | a change's Files tab and its inline review comments |
237 | `releases.go`, `runs.go`, `workflows.go`, `locks.go` | release, run, workflow and lock pages |
238 | `comments.go` | the comment view, the thread builder, the action menus, the snippet |
239 | `comment_writes.go` | add, edit and delete a comment, for both surfaces |
240 | `tokens.go`, `rolepicker.go` | the label/assignee token input and the instance/repository role control |
241 | `write_context.go`, `capabilities.go`, `audit.go` | the write gate, the ACL rules a page and a handler share, the audit log |
242 | `writes.go`, `release_actions.go`, `run_actions.go`, `ci.go`, `account.go` | the browser's writes, by what they change |
243 | `settings.go`, `admin_access.go` | repository settings, and the instance-admin access rules on the same page |
244 | `admin.go`, `admin_repos.go`, `admin_users.go`, `status.go`, `runners.go` | the instance-admin gate, accounts, repository creation, status, runners |
245
246 Read handler files answer a page or an HTMX fragment; `*_actions` and `writes.go`
247 answer a form POST and redirect. A handler resolves the repository through
248 `w.open`, which is also what makes a private repository indistinguishable from a
249 missing one, and builds its data with `w.page` so the chrome always agrees with
250 the ACL.
251
252 Every write starts at `beginWrite`, which establishes the session, the body bound,
253 the CSRF token, the repository and its rules, and parses the form in that order —
254 `beginPost` and `beginAdminPost` are the account-scoped and instance-admin
255 equivalents. The handler then asks `capabilities.go` for the one rule it needs
256 (`canDiscuss`, `canOpenChangeRequest`, `canTriage`, `canApprove`, `canMaintain`,
257 `canModerateComment`), which is the same function the page used to decide whether
258 to draw the control. A rejected write answers through `w.refuse`, a 403 that never
259 re-renders; a write that fails for another reason answers through `w.fail`, which
260 re-renders the section with the message for HTMX and returns the message as text
261 otherwise.
262
263 A comment is the same object on an issue and on a change request:
264 `metadata.Comment` is one type for both, so `comments.go` has one view, one thread
265 builder and one set of action rules, and `comment_writes.go` has one
266 implementation each of add, edit and delete. A `commentTopic` names what the two
267 surfaces do differently — which writer runs, which section re-renders, where a
268 plain form post lands — and a change request adds the one thing an issue cannot
269 have: a comment anchored to a line of the diff, with a snippet of the lines it was
270 written about.
271
272 Templates live under `server/web/templates/`, one file per destination, with a
273 page's fragments in the page's own file so a fragment and the element it swaps
274 are read together. Four files hold what more than one surface uses: `chrome.html`
275 (shell, global bar, repository header, error page), `components.html` (token
276 input, commit list, file tree, pager), `diff.html`, and `role-picker.html`. All
277 templates share one namespace, so a fragment name is prefixed with its surface
278 (`issue_labels`, `code_main`, `review_main`); a fragment both surfaces render
279 keeps the shared name (`comment_thread`, `comment_card`).
280
281 The file tree is one widget for both browsers. The code browser and a change's
282 Files tab pass the same tree nodes through `treeContext`, and
283 `treeHref` decides whether a row opens a blob at a ref or the file's diff in a
284 change. The pager is likewise one control for issues, change requests, and the
285 instance feed: the handler supplies the URLs, the label, and (for HTMX) the
286 fragment to swap.
287
288 Static assets are served from `/static/` and embedded in the binary at build
289 time; nothing is loaded from a CDN and no template inlines CSS or JS. The passkey
290 script is an ordinary asset (`static/auth.js`) and runs under the same CSP as the
291 rest: `script-src 'self'`.
292
293 The route table is `server/routes.go`, one function per surface (assets, global,
294 auth, admin, repository, git). Routes are grouped by prefix rather than by
295 registration order because `ServeMux` ranks by pattern specificity, and the
296 catch-all 404 is registered last only so a reader finds it there.
297
218 298 ## Icons
219 299
220 300 Icons are Lucide 0.468.0 SVGs (ISC, license at `server/web/static/lucide.LICENSE`),
@@ -226,7 +306,20 @@ named in a template exists.
226 306 ## Rules
227 307
228 308 - Every state-changing form carries the CSRF hidden input.
309 - A write handler opens with `beginWrite` (or `beginPost`/`beginAdminPost`); it
310 never reads the session, the CSRF token, the repo ACL, or the form by hand.
311 - A control and the handler that accepts it ask the same function in
312 `capabilities.go`; neither restates the rule.
229 313 - A private repository answers 404, never 403.
314 - A rejected write answers through `refuse` (403) or `fail` (the section with the
315 message for HTMX, plain text otherwise). A failed write is not a place for the
316 styled error page: that page is for a 404 or a 500, where there is no form to
317 return to.
230 318 - No inline CSS or JS in templates; assets live in `server/web/static/`.
319 - One file per destination under `templates/`, with the page's fragments beside
320 it; anything two surfaces share goes in `chrome.html`, `components.html`,
321 `diff.html` or its own component file.
322 - Stylesheet sections are listed in the table of contents at the top of
323 `vole.css`; add a line there when a section is added or moved.
231 324 - Tables are for tabular data; navigation and page chrome are not tables.
232 325 - Test the page at 1440, 1024, and 390 px wide before calling it done.