/reports/site-redesign.md · raw

Site redesign: build spec

Goal (Dennis, 2026-10-13): one simple, intuitive site. One nav, one page per machine, one machines table. Old URLs may break. The CLI commands keep their arguments. No information is lost: it moves.

Everything stays static HTML, written by stdlib Python with atomic writes, using relative links and no external requests. tools/serve.py stays exactly as it is.

0. Facts the spec is built on (2026-10-13)

1. Pages and URLs

Page File (URL under /) Anchors
Home dashboard/index.html. / keeps redirecting there through root index.html #needs, #recent, #counts
Machines dashboard/machines.html #b-<bucket> per group, #untouched, #fam-<lead tm>
Machine machines/<tm>/index.html (always linked with the explicit index.html, so file:// works too) #overview, #mechanism, #proof, #files. Kept inside: #how, #fig-N, #q-N, #same, #s1..#s7
Submissions dashboard/submissions.html #pr-<n>, #ready
Decisions dashboard/decisions.html (same file, gains the nav) #d-N (existing), plus new #tm-<tm>
Reports dashboard/reports.html none
Explorer tools/explorer/ (not generated; nav link only) #tm=<tm>

Unchanged outputs: dashboard/data.json, dashboard/kills.json, dashboard/thumbs/, dashboard/screens/.

Removed outputs: every machines/<tm>/card.html, every machines/<tm>/kill.html, machines/index.html and machines/kills.html (§6).

The machine page sits in the same folder as card.html and kill.html did. Every relative link those fragments produce (proof.md, img/x.png, submission/…, <tm>.v) therefore stays valid as it is.

2. Shared module tools/site.py

site.py has no top-level imports of card, dashboard or killreview. Any loader that needs them imports them inside the function, so card.py can import site.py without a cycle.

Piece API Content / rule
Paths ROOT, MACHINES, OUT = ROOT/"dashboard", PAGES = {"home": "index.html", "machines": "machines.html", "submissions": "submissions.html", "decisions": "decisions.html", "reports": "reports.html"}
Theme tokens TOKENS (CSS string) The :root light block, the dark block for prefers-color-scheme and the [data-theme=dark] block are moved verbatim from the top of card.CSS, together with dashboard's --good-strong/--good-soft/--good-weak. Then card.CSS = site.TOKENS + <the rest of card.CSS> and DASH_CSS drops its token lines. ROOT_INDEX uses TOKENS too. serve.py's own palette is left alone.
Page shell page(title, body, *, active, prefix, css="", js="", built_ts) Doctype, viewport meta, <title>, <style>{TOKENS}{BASE}{NAV_CSS}{css}</style>, the nav, the stale banner (#stale, filled by TIME_JS), <div class="wrap">{body}</div>, and <script>{TIME_JS}{js}</script>. prefix is the path from the page to the repo root: "../" for dashboard/*, "../../" for machine pages. card.page() and the hand-made shells in decisions/dashboard/killreview are all replaced by this.
Nav nav_html(active, prefix, open_n) <nav class="sn"> holds the brand "BB(6) hunt" (links to Home) and six links in this order: Home · Machines · Submissions · Decisions <span class="sn-n">N</span> · Reports · Explorer. The current page gets aria-current="page". Above 760 px the links are inline. At 760 px and below a <details class="sn-menu"><summary>Menu</summary>…</details> holds the same six links, and the inline row is hidden by a media query. It works without JS. The links appear twice in the markup; tests check that both lists are identical. When N is 0 the badge is muted (.sn-n.zero). The nav is not sticky: the decisions footer and the Machines filter bar keep their own stickiness.
Open decisions open_decisions(machines) -> list[(tm, question or None)] Every machines/*/decision.json without answer. Unreadable files count as open (dashboard._open_decision). nav_html gets len() of this list, so every page, and the Decisions header, shows the same number.
Display name Names(machines) with .label(tm) -> (tag, tip) and name_html(tm, href=None, cls="") Tag, first match wins: (1) the pool rank from reports/targeting.md: #3, R4; (2) the status.json label: W4-3, #3b, C2-552; (3) twin of #N when the parent has a rank; (4) line N on the 740 list; (5) 778 line N; (6) off the list. This is dashboard.member_name's order. The tip is the full string, plus "also called W4-1" when (2) differs from (1); #31 is the only case today. HTML: <span class="nm"><code class="tmn"> + the string with <wbr> after each _ (dashboard._tm_html) + </code><span class="tag">#3</span></span>. It is cheap: only targeting.md, the status.json files and the list are read, never build_records. dashboard.member_name, killreview.Ctx.name and decisions.rank_of all delegate to it. Ctx switches its line N fallback from the 778 list to the 740 one, with 778 as the fallback.
Repo (data) Repo(machines=MACHINES, list_path=D.LIST, prev_path=D.PREV_LIST, out=OUT, now=None), every property memoized .recs, .meta = dashboard.build_records(...). .by_tm. .names. .ctx = killreview.Ctx(machines=…), after Ctx gains machines/dash_data parameters in place of the module globals. .prs = meta["prs"] plus the package found for each PR (§3.4). .kill_summaries = {tm: summary}, filled while machine pages are built, otherwise computed with killreview.build_parts(…, vsmode="cache"). .open_decisions. .reports (§3.6). Tests build a Repo on a temp tree with a 3-line list, which takes about 0.4 s. Optional: build_records(guess_for={tm}) lets a single-machine build skip the 2.8 s guess pass.
Buckets BUCKETS7 = accepted, submitted, claimed, active, waiting, parked, cryptid. bucket7(rec): "cryptid" when the dashboard bucket is parked and the status is CRYPTID_CANDIDATE, else the dashboard bucket. Today: 9/4/18/1/1/23/8, plus 678 untouched. The overnight report says "cryptid 10": the other two cryptids sit in waiting (W5-2) and active (R4, funded).
Hrefs machine_href(tm, from_dir), page_href(key, prefix) Always …/machines/<quote(tm)>/index.html.
JS TIME_JS, moved from dashboard.py unchanged Ages, the "working now" badges and the stale banner, on every page.
Orchestrator build_all(repo, *, thumbs=True, force_thumbs=False, vsmode="fill", pages=ALL) and main() Order: thumbnails (D.ensure_thumbs), every machine page (which collects the kill summaries), Machines, Home, Submissions, Reports, Decisions, then data.json, kills.json and the root index.html. python3 -B tools/site.py [--no-thumbs] [--force-thumbs] [--force] [--jobs N] builds everything.

3. Each page: sections and their sources

3.1 Machine page: tools/machinepage.py (new)

render(repo, tm, vsmode="cache") -> (html, info) and build(repo, tm, vsmode) -> Path. The page works for every machine directory, with or without observe.json.

There are two refactors, and both keep the old functions' output byte-for-byte identical:

Page layout: the nav, then <header class="mh">, then four <section class="mp" id=…> blocks, then the footer. A jump row under the header links Overview · Mechanism · Proof & checks · Files; it is not sticky, so the card's sticky checkpoint column is unaffected.

Section Block Source (reuse)
Header <h1> = names.name_html(tm) with the copy button (id="tmstr", card JS); "kill N"; the class; links: bbchallenge ↗ · explorer (#tm=) · parent's page card_parts.class_html, else rec["class"]/guess (D.class_html(rec)); rec["kill_no"]; C.tm_url
Overview #overview State line C.state_html(C.card_state(obs or {}, st, qs, mdir)), which works with obs={}
"Needs you" box, only when there is something rec["ask"] / rec["questions"] through D.ask_html (links now point to #mechanism / #q-N); rec["ask_via"] through D.via_html; an open decision.json links to decisions.html#tm-<tm>
How we proved it (#how) C.how_proved_html(C.load_how_proved(mdir), title=C.how_title(status)) when C.shows_how_proved. This is the machine's own file; a derived kill never shows the parent's. It appears exactly once on the page.
In one sentence, plus folded mechanism notes card_parts.intro. With no obs: rec["summary"] (the Mechanism line of submission/README.md) or status.json relation + parent_note.
Upstream / PR K.sub_chip(st) + D.pr_line(rec) + a link to submissions.html#pr-N
Kill facts (kills only) New D.kill_facts_html(rec), extracted from tile_big (lines, KB, compile, coqchk, axioms, first PASS, re-run, "from #3"), plus build_parts.verdict_html (n/7, Checkpoint B, template reuse, vs parent), which links to #proof
Progress (non-kills) New D.progress_html(rec, now), extracted from tile_big: pipe_html, the first sentences of the reason, last change / idle since with the ledger tooltip, and live_html
Relations D.table_html(rec), D.eq_chip(rec), D.off_chip(rec), the family list (ctx.family(tm), each member as a name_html link with its status chip)
Facts D.meta_html(rec, rank=True): eqv, sim, flags, sheet status, the misattribution strike; the list line (D.line_label)
Calls on file (folded) status.json priority_call_answered, parked, funded, next; machines/<tm>/decisions/*.json (date, option, question); the current decision.json
Standard space-time picture (right column on wide screens) dashboard/thumbs/<tm>.png shown at 1:1 (400×500, image-rendering:pixelated, max-width:100% on phones), linked to bbchallenge.org, with the label C.label_html(D.THUMB_LABEL). A missing thumbnail gets a placeholder.
Mechanism #mechanism Has observe.json card_parts.look (primary figures + Checkpoint A + Discord draft), then card_parts.folds (More figures, Rules, Compressed history, Template comparison, Proof strategy & risks, Stats & provenance), unchanged. Figure ids stay fig-N in card numbering, so card.html#fig-2 hrefs in decision.json map 1:1.
No observe.json, has a parent with one A banner: "No observation of its own: . Below are 's figures, rules and history." Then card_parts(parent_obs, parent_mdir, prefix="../<parent>/") with only look-figures (no checkpoint), More figures, Rules, Compressed history and Template. The parent's questions are linked (../<parent>/index.html#mechanism), not shown. A non-killed delta machine also gets K.same_section(…, vsmode="cache") here; for kills that section lives in Proof & checks.
Neither "No observation yet", with a link to notes.md
Proof & checks #proof Killed build_parts.verdict_html, then K.section(k, …) for each of secs: What is proved · [Same machine as X? #same] · Where it came from · Proof outline · What a maintainer reads · Family · Checklist. Then the review pages that exist (D.REVIEW_GLOBS minus kill.html: review.html, compare.html, review*.md). The footer line "nothing on this page is hand-written except the How box" stays.
Not killed Links to proof.md, check_proof*, rocq/*.v; Checkpoint B lines (K.checkpoint_b(mdir)); status.json open_lemma(s). With none of these: "No proof work yet."
Files #files <details>, closed Every top-level entry of machines/<tm>/, with size and mtime: key files first (notes.md, proof.md, how_proved.md, status.json, observe.json, deepen.json, decision.json, .v, check_kill), then folders (submission/, rocq/, scripts/, img/, decisions/, work/, …: their own listings still work, since only the machine folder gets an index.html), then the rest. status.json is printed in full in a nested <details><pre>. Card contract warnings (card_parts.warns) come last. This replaces the folder listing that machines/<tm>/ used to serve.
Footer "rendered … by tools/card.py / killreview.py", plus the re-run commands

CSS is card.CSS + K.CSS_EXTRA + a small mp- block (header, two-column overview, jump row). JS is card.JS; killreview's "no JS" rule still holds for the Proof & checks fragment itself.

3.2 Home: dashboard.render_home(repo), replacing render_page

Block Content Source
Counts #counts 7 tiles (accepted / submitted / claimed / active / waiting / parked / cryptid) plus untouched, each with icon, count and short meaning, and a "● n working now" badge. Each tile links to machines.html#b-<bucket>. One list line: "740 holdouts (current list) · +2 off the list · previous list 778 (38 gone) · 1,526 with equivalents · sim pending 130/35". site.bucket7 over repo.recs; D.BUCKETS text (adds cryptid ⚠ "cryptid candidate, parked"); meta (n, n_prev, prev_gone, eqv_total, pending, off_list)
Needs you #needs (1) Open decisions: question + recommended option, linked to decisions.html#tm-<tm>. (2) Waiting machines: D.ask_html(rec) with the link changed to the machine page. (3) PRs to answer: an open PR whose comments > 0. (4) Discord to post: an open PR where no machine's status.json has posted, with "copy it on Submissions" → #pr-N. (5) Ready to submit: claimed kills grouped by family, linking submissions.html#ready, with the claimed key line ("They wait for the response to the open PR #17 and #18"). (6) Unanswered Checkpoint A questions: count on how many machines. (7) A folded block "Discord question drafts, not posted (n)": each distinct message once, with copy buttons, as at the bottom of the old cards index. When everything is empty: "Nothing needs you right now." repo.open_decisions + decisions.analyze (recommended); rec["bucket"]=="waiting"; repo.prs; D.kill_key("claimed", …); C.norm_questions / card_state(...)["open"] (add open_q to machine_state); C.discord_message over observe.json discord_check
Recent #recent (a) Events from the last 4 days, newest first, at most 12, each with a machine link: kill verified (kill.first_ts), PR merged (sub.merged_at), PR opened (sub.opened), status changed (rec["updated"] + status chip + first sentence), call answered or filed (decision.json answer.date / updated). (b) The last 6 rows of reports/overnight-ledger.md (time, action cut at 220 chars, machine tokens linked), plus a link to the file. (c) A link to the latest report. New site.recent_events(repo); D._LEDGER_ROW (extend the token map with status labels like W4-3); repo.reports[0]
Footer rebuilt time and the rebuild command

3.3 Machines: dashboard.render_machines(repo)

Header: "64 machine folders", the status chips per live status (from the old cards index header), the thumbnail label, and a folded "How to read this table" with the kills index's "What to check here" text and its colour key.

Filter bar: dashboard's #bar, reused. Search, bucket chips (the 7 + untouched), the class menu with counts, sort, and "n of N shown". DASH_JS is generalised: items are [data-tm], groups tbody[data-b] / .minis, families tbody.fam. With a sort other than the default, rows render flat and the group headers hide; switching back restores data-o. A #b-X hash on load selects bucket X. Without JS the whole table shows, grouped.

The table has one <tbody data-b=…> per bucket, in the order of §2. Kill buckets have one <tbody class="fam"> per family. The family header comes from ctx.family(): "Family of #3 · class · 4 killed, 1 not; not killed: line 604 (untouched), …". Today's grouping ran within buckets on the dashboard and across buckets in kills.html. Now members that sit in another bucket are named as "also #3b in Accepted". Rows carry D._data_attrs(rec, order).

Column Kills Others Source
Picture 64×80 thumbnail → machine page same D.thumb_html (thumb_target now always returns the machine page for folders)
Machine name_html, class, Approach tag (→ #how) name_html, class/guess/confidence site.Names, D.class_html, C.how_proved_tag
State kill N, K.sub_chip status chip, stage, live_html, off_chip C.status_chip, rec
Now mechanism line ask, or card_state.short, or the first sentence of the reason (2 lines, full text in the title) rec["summary"], rec["ask"], C.card_state
Proof n/7 + K.auto_why, Checkpoint B mark, template kind + K.bar + "% identical · new/items", "same machine as X?" + K.vs_short, lines · items · compile pipe_html (mini) repo.kill_summaries[tm]
Related parent / table / eq-of same D.table_chip, D.eq_chip
Changed ago_html(last_work) · status.json date same rec
Flags eqv, sim, sheet, UBRRBA, longit. the same + n open Q, Discord, n unlabelled, observe.json unreadable D.meta_html + card lint (C.label_issues)

At 760 px and below each row becomes a card (CSS grid on tr/td): picture, then machine, state and now. Proof, related, changed and flags shrink to one small line. Nothing scrolls sideways.

Below the table: <details id="untouched">, "Not started: 678 holdouts on the list with no folder". Inside is the old Untouched grid unchanged (D.tile_mini, the tier ordering and its key line, content-visibility:auto). The filters apply to it, and pressing the untouched chip opens it.

The footer keeps the dashboard's data-sources list (list files, sheet, buckets, PR sync, kill numbers, same table, flags, off-list reasons) word for word.

Dropped on purpose: the "no card" and "card stale" flags (every page is rebuilt together now), and the cards index's Fig 1 thumbnails (Fig 1 is the top of Mechanism; the table uses the uniform standard picture).

3.4 Submissions: tools/submissions.py (new), render(repo)

3.5 Decisions: decisions.py, changes only

3.6 Reports: tools/reports.py (new), render(repo)

4. CLI mapping (same arguments, new outputs)

Command Writes now
python3 -B tools/card.py <tm> (machine string, dir or observe.json) machines/<tm>/index.html (full page: kill sections too when killed; vsmode="cache"). Works without observe.json. Prints path + URL + error:/warn: lines as before. A missing folder gives exit 1.
card.py --lint <tm> unchanged (observe.json contract; a missing observe.json is still ERROR)
card.py --index dashboard/machines.html
card.py --all every machine page, then dashboard/machines.html
card.py --machines DIR [--out DIR] the same, into temp trees (tests). --out is new and defaults to dashboard/.
python3 -B tools/killreview.py <tm> machines/<tm>/index.html with vsmode="fill". Prints the same summary line (auto n/7 template … vs …); a machine that is not killed prints "not killed". A missing folder gives exit 2.
killreview.py --all [--force] every killed machine's page (fill / force), then dashboard/machines.html
python3 -B tools/dashboard.py [--no-thumbs] [--force-thumbs] [--screens] [--list] [--prev-list] [--out] [--jobs] [--no-root-index] site.build_all: thumbnails, all machine pages, Home, Machines, Submissions, Reports, Decisions, data.json, kills.json, root index.html. --no-cards-index is kept as an alias that skips the machine pages. --screens shoots every top-level page plus 3 machine pages (kill with card, delta kill, cryptid) at 1920/1280/390 into dashboard/screens/.
python3 -B tools/decisions.py / --lint [TM…] dashboard/decisions.html / unchanged
python3 -B tools/site.py […] the same as dashboard.py (the canonical entry point)
tools/pr_sync.py unchanged; its docstring "then rebuild" becomes python3 -B tools/dashboard.py
tools/rebuild_pages.sh see below

New tools/rebuild_pages.sh, which rebuilds everything when any input changed:

#!/bin/bash
# tools/rebuild_pages.sh: rebuild the whole site when anything it is built from changed (bb6-pages timer, every 5 min).
cd [local working file] || exit 1
exec 9>dashboard/.rebuild.lock; flock -n 9 || exit 0        # one build at a time
stamp=dashboard/.built                                      # dot-file: serve.py never serves it
changed() {
  find machines -maxdepth 5 -type f -newer "$stamp" ! -name index.html ! -name 'vs-parent*' ! -path '*/__pycache__/*' -print -quit
  find reports -maxdepth 2 -name '*.md' -newer "$stamp" -print -quit
  find data/holdouts tools -maxdepth 1 -type f \( -name '*.py' -o -name '*.txt' \) -newer "$stamp" -print -quit
}
if [ ! -f "$stamp" ] || [ ! -f dashboard/index.html ] || [ -n "$(changed)" ]; then
  touch dashboard/.built.new                              # changes made during the build trigger the next run
  nice -n 10 python3 -B tools/site.py >/dev/null && mv dashboard/.built.new "$stamp"
fi

The excluded names are what the build itself writes; without the exclusions every run would trigger the next. The rebuild costs about 15 s niced: build_records 3 s, 64 pages ≈ 5 s, then the rest. vsmode="fill" computes a new derived kill's comparison once (≈ 7 s) and caches it.

5. Code removed with the old pages

6. Deletion: what and when

  1. After the machine pages are built for all 64 folders and the link check passes (§7, test_site 3), run a one-time inventory script. It is a scratchpad script, not shipped. It pulls the visible facts from today's pages: chips, kr-sub, howp-tag, kr-verdict, figcaptions, summary, kr-tab cells, tsum/kf/kpr lines. It checks that each fact appears in the new page that took over that content, and it prints any misses for a fix round. Old copies go to the scratchpad for the headless review comparison.
  2. Then delete, in the integration step: bash rm machines/*/card.html machines/*/kill.html machines/index.html machines/kills.html That is 54 + 30 + 2 files. Nothing else in machines/ is touched: review.html, compare.html, the img/vs-parent caches and all data stay.
  3. Rewrite links once in human reports (not machine data), using sed on paths only: - reports/how-proved.md (18 links), reports/overnight-2026-10-10.md (Links section) and STATUS.md: machines/<tm>/card.html becomes machines/<tm>/index.html#mechanism, kill.html becomes index.html#proof, and machines/kills.html / machines/index.html become dashboard/machines.html. - overnight-ledger.md is history and is not rewritten. - The card.html links in machines/*/notes.md and decision.json files stay as they are (data files). decisions.py maps the figure ones.
  4. Update the docs in the same step: README-card.md, README-killreview.md, README-dashboard.md, README-decisions.md (their page sections now point to §3 of the new pages), a new short README-site.md (nav, names, Repo, CLI table, rebuild), and the root ROOT_INDEX links.

7. Tests

Run with python3 -B tools/test_<x>.py. The headless parts use CLAUDE.md's playwright-core + chromium + .syslibs, never Dennis's desktop.

test_card.py (26). Every fragment test stays as it is. They render through machinepage.render(Repo(tmp, list_path=tmp_list), tm), or through card.card_parts for the purely fragment-level ones.

test_dashboard.py (26). Inputs, Guess, Tables, Machines, Submission and Ledger are unchanged (build_records logic).

test_killreview.py (27). Parse, Compare, Markdown and SameMachine are unchanged.

test_decisions.py (18). Unchanged except: - Links go to machines/<tm>/index.html (#fig-N for figures). - The nav is present, with a count equal to unanswered + unreadable decision.json in the fixture tree. - #tm-<tm> anchors exist and are unique.

New tools/test_site.py.

  1. Nav: Home, Machines, Submissions, Decisions, Reports and three machine pages each have one nav.sn with the six links in order, aria-current on the right one, both link lists identical, and N = the fixture's open decisions (2 open + 1 unreadable + 1 answered → 3).
  2. Names: the precedence table of §2 (rank > label > twin > 740 line > 778 line > off-list) and the alias tooltip. On real data Names.label == D.member_name == Ctx.name for every folder. A fixture machine's tag reads the same on all six page types.
  3. Link check: crawl the temp build (and, in integration, the real one). Every relative href/src resolves on disk, with ?download/?raw stripped and #frag checked against the target's ids. Nothing links to card.html, kill.html, kills.html or machines/index.html.
  4. Machine page kinds: a kill with observe.json; a delta kill without (Mechanism images are ../<parent>/img/… and exist; #same sits in Proof & checks; the How box is its own); a cryptid (state "Parked", no kill sections, "Calls on file" shows parked); a broken observe.json (the page renders, with a banner).
  5. Submissions: fixture packages in family/, pr/ and legacy discord.md give one card per PR, highest number first, with discord_dennis.txt preferred. Download links carry ?download and resolve. "Not posted" shows when posted is missing. #ready lists the claimed kills by family.
  6. Reports: the curated order, with the newest overnight report first; titles come from # headings; no .log/.tsv/ledger links.
  7. Home: the tiles sum to the folder count; needs-you lists the open decisions, the waiting ask, a PR with comments > 0 and an unposted open PR; recent events are sorted newest first, all within 4 days.
  8. CLI: on temp trees each command of §4 writes exactly its files, and none writes card.html or kill.html.
  9. Headless, over every page type at 1280 and 390 px, light and dark: no horizontal overflow, an explicit body background, no console errors, no external requests, all images load, and the nav menu opens at 390 px.

8. Build order (file ownership, so steps can run in parallel)

Step Owner files Needs
A. Shared + machine page site.py, machinepage.py, card.py (card_parts, prefix), killreview.py (Ctx params, build_parts, ns, links), test_card.py, test_killreview.py, test_site.py §1, 2, 4 this spec
B1. Home + Machines dashboard.py (render_home, render_machines, kill_facts_html, progress_html, DASH_JS), test_dashboard.py A
B2. Decisions decisions.py, test_decisions.py A (site.page, Names)
B3. Submissions + Reports submissions.py, reports.py, test_site.py §5, 6 A
C. Integration site.build_all + CLI wiring in all four, rebuild_pages.sh, inventory → delete (§6), report link rewrite, READMEs, test_site.py §3, 7, 8 + full suite B1–B3
D. Headless review screenshots at 1280/390 (dashboard.py --screens), with up to 2 fix rounds C

Done means: all suites green apart from the known W4-3c data failure; the link check is clean on the real build; the inventory script reports 0 misses; rebuild_pages.sh run twice in a row rebuilds once, then exits as a no-op.