beads: show the stored rows behind an issue on its detail pane
Every field on the detail pane is a reading of a row: the Issue struct names
the columns bd surfaces and drops the rest, humanizeEvent turns two JSON blobs
into a sentence, an Edge keeps an id and a type out of a dependency row. A
reading that went wrong looks exactly like one that did not, so the detail modes
now carry the rows they were built from — the issue's own row first, then the
rows belonging to it in labels, dependencies, comments and events, in the order
the tables were read and the columns came back in. custom_statuses is read for
the lane and is deliberately absent: its rows describe the tracker's statuses,
not this issue. Related tables are capped at RawMax rows and counted whole, so a
busy issue's audit log cannot turn the pane into a second page.
A raw view may not report an absent cell and an empty string as the same thing,
which is the one place cell()'s flattening of a real NULL to "" is wrong.
rowCells.isNull is now the single site that decides, shared by cell and the raw
row, so the two can never disagree; RawCell carries Null beside Value and drops
browse's "NULL" text, which would have put the collision straight back.
The section renders as a collapsed <details> in the stream layout's Past Stand
idiom — it is a check on the rendering, not the reason a reader came. Values are
escaped and deliberately not run through beadLinks.Text: a linkifier rewrites
what it is given, and a value that has been rewritten is no longer the value
that is stored. A cell holding no value renders as a NULL chip, a cell storing
those four characters as the text it stores, an empty one as (empty); a long
value scrolls inside its own cell.
web: the milestones page says when its rollup is partial
MilestoneView has carried the clip since ebbd30f and list_milestones has
reported it since a419499; milestones.html rendered nothing, so the page
was the last surface presenting a partial rollup as whole-tracker
arithmetic. Every number on it is a rollup — "1/4 done", "1995 of 2000
issues carry no milestone label" — and arithmetic over the rows that were
read renders exactly like arithmetic over the tracker.
The notice is the detail pane's, word for word and class for class: it is
the same fact about the same read, and a second phrasing for it would be
a second style. It sits above the rollup and outside the
milestones/no-milestones branch, because "No milestones." over a clipped
read is the reading most easily mistaken for a fact about the tracker.
A milestone can lose members here rather than merely undercount them —
membership comes from labels — so the fixture makes that concrete: m1 has
five members, one of them past the cap, and the page counts four while
saying why.
beads: the board names every clipped table it draws from
The board raised its truncation flag for a clipped issues or dependencies
table and for nothing else, so a clipped labels table took the pills off
every card and the options out of the label filter in silence, and a
clipped custom_statuses table could put a card in the wrong lane with the
counts still reading as facts.
The flag could not grow to hold that. It is paired with a count line —
"the first N of M issues" — so it can only mean the reads that decide N
and M; widening it would have made a bool that means four different
things and a number that no longer follows from it. Data.Truncated and
ShownOf are therefore byte-for-byte what they were (mcpsrv's list_issues
reads them as they are), and the wider fact is a new field: Clipped, one
entry per clipped table the board reads, in read order, each naming the
table, the rows read against the rows that exist, and what the board lost
by the rest.
The banner follows. The count line now prints only when the issues read
was actually short — "the first 4 of 4 issues" was a wrong sentence, not
a warning — and every other clipped table gets its own line beneath it.
The detail pane keeps its single sentence from 82d997d; a pane that never
drew a lane has no use for a board's list.
TestBoardTruncationIsUnchanged is now TestBoardTruncationFlagIsUnchanged:
the flag is what it was, which is the claim that survives. Its "comments
does not flip the board" subtest survives as "a clip in a table the board
does not read" and now also asserts comments reaches neither the flag nor
the list, plus a new subtest pinning that labels and custom_statuses are
reported without touching the flag.
web: the detail pane says when its read was clipped
The board has always carried a truncation banner; the detail pane it
shares a template with carried nothing, so an issue assembled from
tables clipped at 2000 rows looked complete, and an id in the tail of a
big tracker was answered "Issue not found." — which the page cannot
know.
Two lines, both in the banner's idiom. A pane built from a clipped read
says so and names the tracker's true size when the issues table itself
was cut; a miss over such a read says the id was not among the rows
read, which is not the same as saying it does not exist. A complete read
keeps the plain "Issue not found.", and the board's own banner is
untouched.
web: stop printing store paths to the owner
web: stop printing browse errors to the reader
The overview rendered the browse layer's own error text into the page, under
"Could not read history: " — dolt internals and the store's path on our disk,
which nothing else on this surface discloses and which a reader can do nothing
with.
The page now carries a fixed sentence and the reason goes to the log with the
database id (slog + scribe.Err). The view field is a bool rather than a message,
so no error string can reach the template by being assigned to it later. The
empty-database state the page already reported ("No commits.") is untouched:
that is a fact about the database, not a failure.
web: tell a database outage from a missing database
loadRepoForBrowse turned any error from GetRepoByOwnerAndName into a 404, so a
metadata store that could not answer told every reader on the instance that
their database does not exist — for as long as Postgres was down, in the voice
reserved for "there is nothing here". loadRepoForAdmin did the same on the
settings page.
Both classify through repoLookupFailed now: db.ErrNotFound keeps the 404, and
it stays the same 404 the visibility rule renders for a PRIVATE database the
caller may not see, so a masked database and a missing one remain
indistinguishable. Anything else is a 500 through the shared error page, with
the cause logged (slog + scribe.Err) and never rendered.
web: link issue ids to the tracker that owns them
web: drop the author from a memory's revision line
web: answer what is ready across every tracker
web: register the views in one explicit list
web: give bd memories their own view
web: offer the bd command for the issue on screen
web: show how fresh a beads view is
handleView now reads the head commit of the rendered ref onto the envelope
(Head *browse.CommitInfo), and the beads and milestones headers carry a
shared beadsHead partial: <branch> · last commit <relative> · <short hash>,
the hash linking to the commit page and the exact stamp in the title.
The read is decoration on top of an answer: a database with no commits, or
a Log that fails, renders the page without the line rather than 500ing.
The relative time is a new ago func rather than chrome's reltime — it is
past-facing (clock skew reads as "just now", never "in 3 minutes") and
reads a package clock a test can pin.
beads: add a one-column stream layout beside the board
?layout=stream renders the same filtered set, the same buckets and the same
cards as one column of sections instead of four lanes side by side. It is a
layout of the Beads view and not a fifth tab: filters, the ready toggle, the
search box and ?issue= behave exactly as they do on the board.
A column read top to bottom can afford one sort per section, because each
section answers a different question: Rolling by started_at desc (what was
picked up last is what is being worked on), Lined Up ready-first then priority
then oldest, Stalled by blocker count (one blocker away is nearer than five),
Past Stand by closed_at desc. started_at and closed_at join the card model as
sort keys and are not displayed; a row missing one sorts last, because an unset
timestamp is not a very old one.
Past Stand opens collapsed in a <details> — the largest and least actionable
section, closed without a line of JavaScript. The sections are derived from the
finished lanes rather than bucketed again, so the section counts cannot drift
from the marquee, and the lanes keep the board order the board renders.
The Board/Stream toggle rebuilds the current query with layout replaced, via a
new withQuery template func, so every active filter survives the switch.
beads: drop the Bead/Beads prefix from the moved types
beads: extract the projection out of web/
web/beads.go held the one reading of the beads schema — the table
fingerprint, the lane bucketing, the ready rule, the status categories,
the transitive dependency walk, the event humanizer, the filter model and
the milestone rollup — where a second consumer could not reach it. The
MCP surface and the cross-database ready page both need it.
Move it to a new root package beads/ that depends on browse/ and the
standard library only: rows in, view model out, no net/http, no
html/template, no core. The BrowseSession seam is declared consumer-side
there and names the one method the projections call, so web's larger
BrowseSession satisfies it structurally and a session passes straight
through.
web/beads.go and web/milestones.go keep only their View adapters — slug,
label, template, Applies, and the hand-off of ref and query. The
templates are unchanged: the moved types keep their names and their
display methods, so every dot still resolves.
Pure move plus the beadsMax -> beads.Max export rename. The projection
tests move with the code and become testify; the render tests stay in
web/ unchanged. Same 63 tests pass before and after.
chrome: link the favicon through the shared head partial
The icon href was a literal in the layout, so a build with no static tree — a
test, a binary run out of a working copy — asked for /static/logo.svg once per
page and got a 404 each time. It is the chrome's FaviconHref now: our own logo
when this build ships one, checked the way the stylesheet already was, and
NewService's built-in data: URI when it does not. Both <link>s come from
srht-head-links, so the guard against <link href=""> is written once.
Also pins what the repo-list partial's optional fields do for a service that has
no timestamp in its schema: a card with no muted footer, not "0001-01-01".
pages: read forms through FormValues
Every mutation on this surface read its fields with r.PostFormValue after a bare
r.ParseForm. PostFormValue was already the right half — r.Form would merge the
query string into the body, which would let a mutation be driven from a URL
somebody was linked to, and that request is exactly the one the same-origin
guard sees nothing wrong with — but the body was bounded only by net/http's
10 MiB default, on pages anyone can reach. FormValues is both properties in one
call, and the values are passed down explicitly instead of each sub-handler
reaching back into the request.
chimw: take the chi helpers from ecore
Three things this service did not have. Read routes are registered for HEAD as
well as GET, so `curl -I` and every uptime probe stop being answered with a 405
and a kilobyte of error page; the twin shares the handler, so it cannot say 200
where the GET says 404. chi's two routing failures now render our own page
instead of net/http's plain text — an unrouted URL here was the one refusal on
the instance that did not look like the service it came from. And the request
line is a slog record rather than chi's colourised line on stdout, which was the
only line this daemon emitted that was neither structured nor on stderr.
chi's own middleware package loses the chimw alias to the package written
against; it is chimiddleware now, as ecore's package doc asks.