~bigbes/sr-ht-dolt

ref: ecfc6bb21417a3997754a5399a2cd2a73a542037 sr-ht-dolt/web/templates d---------
cd0b0c0f — Eugene Blikh 3 days ago
web: rename a database from its settings page

A database's name lives in two places — the metadata row and the on-disk
store directory its path names — so a rename has to move both. The order
is creation's: the row first, where a name already taken is refused by the
unique index before anything on disk changes, then the store, then the
handle the remotesapi memoized under the old path. A store that will not
move rolls the row back, so the two halves never disagree about where a
database lives; only a failed rollback is escalated to a human, because
it is the one outcome no later request can repair.

storage.MoveStore is one os.Rename behind the containment guard DeleteStore
already used, extracted here as containedPath. It refuses an occupied
destination outright: os.Rename over an empty directory would succeed and
swallow it.

No redirect is left behind — the old address simply stops resolving, as on
git.sr.ht — so an existing clone needs its remote replaced, and a companion
of a git repository is re-provisioned under its old name by the next push
to that repository. The README's quickstart now says both.
93e69910 — Eugene Blikh 3 days ago
storage: create databases empty so the first push needs no --force

Every automatic creation path wrote an "Initialize data repository"
commit through WriteEmptyRepo, and that commit is history. dolt decides
fast-forward on the client (actions.CanFastForward over the remotesapi),
so the server cannot forgive the collision: pushing a database that has a
root commit of its own — a beads tracker, anything grown locally — was
rejected as a non-fast-forward and could only land with --force. That is
the whole reason the companion-database recipe starts with a forced push.

push-to-create already provisioned an empty store for this exact reason.
Give the other two paths the same default: /internal/repos, whose caller
is git.sr.ht's post-update hook and therefore fires before its user has
ever pushed, now always provisions empty, and the web form does unless
its new "initialize with an empty commit" checkbox is ticked. The
checkbox buys what an empty store cannot offer — a database that can be
cloned before anything is pushed to it, since dolt refuses a store with
no commits as "contains no Dolt data".

Which is also why the overview of a database with no branches now teaches
push rather than clone: the clone box there quoted a command that could
not work. A store that fails to open is deliberately not treated as
empty — an unreadable database must not be advertised as a fresh one.
60fdb6dd — Eugene Blikh 5 days ago
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.
bbaaa1f7 — Eugene Blikh 5 days ago
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.
96b5e53f — Eugene Blikh 5 days ago
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.
82d997d2 — Eugene Blikh 5 days ago
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.
11c622f3 — Eugene Blikh 5 days ago
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.
dc918296 — Eugene Blikh 5 days ago
web: link issue ids to the tracker that owns them
b6a01558 — Eugene Blikh 5 days ago
web: drop the author from a memory's revision line
043b0fd7 — Eugene Blikh 5 days ago
web: answer what is ready across every tracker
231b7764 — Eugene Blikh 5 days ago
web: give bd memories their own view
3b06523c — Eugene Blikh 5 days ago
web: offer the bd command for the issue on screen
64592988 — Eugene Blikh 5 days ago
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.
fe616c89 — Eugene Blikh 5 days ago
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.
ed5a65bc — Eugene Blikh 9 days ago
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".
17fa0f16 — Eugene Blikh 9 days ago
web: render through ecore's pages and its error page

The page list, the per-page parse loop and the view-template loop are
gone: pages discovers every file in templates/, so a page is registered
by existing, and one that defines no content block is refused at startup
rather than served as chrome around a hole. 404.html and 403.html are
gone with them — the shared error page carries the same body, and its
prose is deliberately the same for a database that is not there and one
the viewer may not see.

The renderer that replaced them closes a leak: the old one wrote
"template render error: "+err.Error() into the response body, handing
the viewer template names and field paths. pages answers a fixed
sentence and returns the error for the log.

reltime and abstime come from chrome.Funcs now; ours called every future
instant "just now", where the shared one says "in 3 weeks".
bf7897cd — Eugene Blikh 9 days ago
web: draw the chrome from sr-ht-ecore

The brand, the service switcher, the login block, the environment banner
and the database listing were a local port of core.sr.ht's nav — one of
five such ports on this instance, and they had already drifted. They are
now sourcecraft.dev/bigbes/sr-ht-ecore/chrome, the one copy every custom
service draws from.

Deleted: web/chrome.go entire (navEntry, networkOrder, networkExcluded,
buildNetwork, basePage, loginURL, logoutURL), templates/nav.html,
templates/icons/circle.svg (ecore inlines the identical SVG), the
repoList partial, and the local dict/shortHash duplicates.

Added: one chrome.Service built in newApp from our config section with
the hashed stylesheet href set on it, a chrome.Page per request through
app.page, chrome.Attach on every template set, and chrome.Funcs as the
base of the funcmap. Handlers embed chrome.Page in their view structs
instead of copying its fields; the row browser sets ContainerClass to
container-fluid, since its column count is the table's and not ours.

Three behaviour changes come with ecore's policy, all deliberate: the
profile link now prefers hub's ~username page when hub.sr.ht is
configured (it was always meta's /profile), the brand carries a fixed
15rem min-width so the switcher starts at the same x on every service,
and a binary built without a stylesheet renders bare rather than linking
an empty href. The nav test went with the code it tested — ordering,
exclusions and login URLs are ecore's to cover — and what replaced it
asserts only what is ours: that pages are drawn through the chrome at
all, and that the row browser is full-bleed.

The auth path is untouched: a foreign bearer token is still accepted as
a meta.sr.ht PAT.
25541bcd — Eugene Blikh 9 days ago
gitignore the in-repo git worktree directories

Agent worktrees are created under .worktrees/<branch> (and, in the older
repos of this family, .claude/worktrees/<branch>) so that they never
scatter as sibling directories next to the checkout. Neither path was
ignored here, so a worktree showed up as untracked in every git status
taken from the main checkout.
bb8ce43b — Eugene Blikh 11 days ago
web: restyle dashboard and database lists after git.sr.ht

Replace the bare 'Your databases' heading and compact grid with the
git.sr.ht dashboard layout: a sidebar column with a welcome blurb plus
block buttons (create database, configure dolt credentials), and the
shared event-list cards for the database list. The repoList partial now
renders each database as an event card — name, visibility as small
muted text (non-public only, like git.sr.ht), description below — and
the profile page picks the same partial up automatically.

Adds a 'lower' template func for the visibility label and drops the now
unused .repo-list grid from main.scss (.event-list ships with the core
theme already).
8527f0fd — Eugene Blikh 13 days ago
feat(web/beads): hierarchy in the milestone view

Arrange each milestone as a shallow hierarchy instead of a flat list.
- Put the milestone-typed issue(s) on top as the milestone's heads.
- Nest tasks under their epics via parent-child dependency edges,
  with a done/total rollup on the epic row; membership stays label-based.
- Keep remaining members as a loose tail; blocks edges are ignored.
- Read the dependencies table optionally, degrading to a flat list.
- Extend fixtures and real-data logging to cover the hierarchy.
Next