Skip to content

explore: --serve, a container, a fetch that survives a narrow token, and a tidier page - #9

Merged
katbyte merged 4 commits into
mainfrom
kt/serve-and-container
Oct 4, 2026
Merged

katbyte merged 4 commits into
mainfrom
kt/serve-and-container

Conversation

@katbyte

@katbyte katbyte commented Oct 4, 2026 •

Copy link
Copy Markdown
Owner

prawn explore --serve, a container that keeps the page fresh, a fetch that survives a narrow token, and a tidier page.

Why: the explore page needs to live somewhere that stays on, so the team can open it without anyone running a laptop command, and a fetch should not die because the token can't read one team name.

The page can be served. prawn explore --serve 8765 writes the page and then keeps handing it out until ctrl-c, printing the addresses other machines can open it on. Every request is logged with the viewer's login (when a login-aware proxy in front sets X-Forwarded-User or similar), the client address, path, status, size and time:

2026-10-03 17:28:57 katbyte 203.0.113.9 GET / 200 21.4MB 27ms
2026-10-03 17:28:57 - ::1 GET /favicon.ico 404 19B 0s

The page is swapped in whole. It used to be truncated and rewritten in place, so a viewer mid-refresh could get half a page. It is now rendered to a uniquely named draft beside it and renamed over:

-out, err := os.Create(path)
+out, err := os.CreateTemp(filepath.Dir(path), filepath.Base(path)+".*.tmp")
 ...
+if err := os.Rename(tmp, path); err != nil {

A container. ghp-sync's image adapted: prawn explore --serve is the main process and a cron job inside (EXPLORE_CRON) re-runs prawn explore to sync the database and swap in a new page. Compose reads the same .prawn file the cli does, so the token never sits in a committed file. The release workflow publishes ghcr.io/katbyte/prawn, signed and attested. Tested against a copy of the database: the page served, a cron refresh rewrote it under the running server with no leftover draft, and docker stop exits cleanly.

One difference from ghp-sync, found while testing: dcron reads /etc/cron.d on its own, so a crontab written there and then installed with crontab ran every job twice at once (fixed in ghp-sync by katbyte/ghp-sync#29). Here the file lives in /app:

-echo "..." > /etc/cron.d/crontab
-crontab /etc/cron.d/crontab
+echo "..." > /app/crontab
+crontab /app/crontab

A fetch no longer fails on a team it can't read. A review request to a team needs org access; without it GitHub returns the rest of the timeline page with that node null and an error beside it. The first page already tolerated this, the follow-up pages now do too. The team's name is only ever shown on the timeline, never used to decide anything:

-if err := c.Do(prTimelineQuery, vars, &resp); err != nil {
+if err := c.DoTolerant(prTimelineQuery, vars, &resp); err != nil {

The page's controls were reshuffled. The tab bar is now a dropdown in the header where the page's name was, and two tabs are renamed to say what they hold. Old links and saved views still open:

-const TABS = [['data', 'data'], ['trends', 'trends'], ['suggested', 'suggested'], ['areas', 'areas'], ...
+const TABS = [['prs', 'prs'], ['trends', 'trends'], ['suggested', 'suggested'], ['services', 'services'], ...

The row of view chips became a preset dropdown with shorter names, since only one is ever active; that puts the saved-view controls on the same line, and they now show on every tab.

Three new ways to cut the list. A ci failing preset beside conflicted, with the head commit's ci shown in a PR's detail. A draft toggle beside open/merged/closed, on for drafts only. And group by on the prs tab, which puts the table in sections (suggested category such as docs only or tests only, kind of change, service, court, status, ci, author group, effort, author) so alike PRs can be tackled together:

   ['conflicted', 'state:open mergeable:conflicting'],
+  ['ci failing', 'state:open ci:failing'],

Fine as it is: the data the page is built from is unchanged, the server adds no routes beyond / and /explore.html (anything else is a 404), and --serve is off unless asked for, so prawn explore without it behaves exactly as before. Build, tests, golangci-lint, shellcheck, yamllint, actionlint, zizmor and typos all pass.

The page is one self-contained file, so this is a file server for
exactly that file, kept up until ctrl-c, printing the addresses the
page can be opened on. The page is now rendered beside its path and
swapped in whole, so a page being served is never read half-written
and two writers never share a draft. Every request is logged: the
viewer's login when a proxy in front sets it, the client address,
path, status, size and time.
ghp-sync's image, adapted: prawn explore --serve runs as the main
process, and a cron job inside re-runs prawn explore on a schedule to
sync the database and swap in a new page. Compose reads the same
.prawn file the cli does, so the token never lands in a committed
file. The release workflow publishes the image to ghcr.io, signed and
attested, as ghp-sync's does.

Unlike ghp-sync, the crontab is written outside /etc/cron.d, which
dcron reads on its own: a crontab there and then installed with
crontab ran every job twice at once.
… run

A review request to a team needs org access to read the team; without
it GitHub returns the rest of the page with that one node null and an
error beside it. The first page already tolerated this; the follow-up
pages now do too, as the team's name is only ever shown on the
timeline and never drives a decision.
The tab bar is a dropdown in the header where the page's name was, and
two tabs are renamed: data is prs, areas is services. Old links and
saved views still open.

The view chips are a preset dropdown with shorter names, since only one
is ever active, which puts the saved-view controls on the same line;
those controls now show on every tab. A ci failing preset joins
conflicted, and the PR detail's review line shows the head commit's ci.

A draft toggle sits beside the states, on for drafts only. The prs
tab's group by puts the table in sections (suggested category, kind of
change, service, court, status, ci, author group, effort, author) to
tackle alike PRs together, each keeping the sort inside it.
@katbyte katbyte changed the title explore: --serve, a container that keeps the page fresh, and a fetch that survives a narrow token explore: --serve, a container, a fetch that survives a narrow token, and a tidier page Oct 4, 2026
@katbyte
katbyte merged commit 8142b00 into main Oct 4, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant