web_frontend: render the homepage task panel from open bd epics; link each epic to its design doc via a description pointer nf-ofmi ← Beads

open priority 2 task unassigned

Not sure where the discrepancy is coming from.

Also, I think we could make home simply render a table for each open "epic" rather than open designdocs, this woud mean we don't have to hit Github for the list of designdocs, and be more general / scalable for creating more "projects" that aren't necessarily keyed around a checked-in rigorous design doc artefact.

I would still like to see links to the designdocs easily from the open epics, this could be achieved by adding the designdoc link to a markdown field somewhere in the epic dataobject, and extracting it at render time. It would also be useful to have that link easily accessible on the board view:

https://nfold.dev/board/nf-758

But overall, reducing our rendering dependence on Github I hope also makes everything load faster.

Dependencies

None.

Comments

jcantsp-bot · 2026-08-21T07:01:44Z
Spec: render the homepage task panel from open bd epics; link each epic to its design doc via a description pointer

Prescriptive card for this issue (spec round, 2026-08-21). Aedan's ask is the issue description above, verbatim — this comment turns it into an implementable card. Supersedes and absorbs nf-s8op (same core swap, from Aedan's 2026-08-20 chat: "can you just read the designdocs from the beads epic for them?"); nf-s8op is closed as duplicate pointing here.

The discrepancy, diagnosed

The two pages apply different filters to different sources:

- / (fetch_design_task_panels, tools/web_frontend/server.py:317) walks design_docs/*.md over the GitHub Contents API and keeps a doc only if it has (a) a parseable task table, (b) a **Beads:** pointer line (server.py:365-367), and (c) an epic that resolves and is not closed (server.py:368-370). Today that leaves 012 only: 005b's doc has no **Beads:** line (its epic nf-lbh was minted outside operationalize_design.py --apply — verified, `grep 'Beads:' design_docs/*.md` matches only 011 and 012), and 011's epic nf-tvc is closed. - /beads (fetch_bd_epics_epics_by_id, server.py:406-435) renders every epic in the workspace, closed included (Bd.list_issues deliberately passes --all, server.py:205-212). Today: 005b + 011 + 012.

The prescription dissolves the discrepancy rather than patching either filter (A4): both pages read the same epic objects from bd; the homepage shows the non-closed ones.

Prescription

All in tools/web_frontend/server.py unless noted.

1. Swap the homepage panel's source. In _serve_index (server.py:1521), replace fetch_design_task_panels(_recording_gh(gh.repo, gh), bd) with the epic fetch: fetch_bd_epics(bd) filtered to epics whose status is not closed (the epic dict from _epics_by_id needs status added — one field). Render with the existing render_bd_epics/_bd_epic_panel (server.py:805-816), not a new renderer (B10). Retitle the section "Open epics" (was "Design-doc task status"). This removes the section's entire GitHub cost — contents_dir + the batched contents_files GraphQL call — which nf-ijyi measured as the dashboard's dominant cost.

2. Design-doc pointer on the epic. Convention: a line **Design doc:** <url> in the epic's description (the markdown field Aedan named; bd list --json already returns description — verified live — so this costs zero extra bd calls). Extract at render time with a _design_doc_pointer(text) helper mirroring _beads_pointer (server.py:299-304) — the same pointer idea, inverted: doc→epic became epic→doc. Render it as a link in two places: - _bd_epic_panel's <h3> — a Doc ↗ link styled like the existing project-link "Board ↗" (server.py:794-795). The panel is shared by / and /beads, so both pages get it. - render_board_page's nav <h1> (server.py:1425-1443) — Aedan asked for it on the board view explicitly. No pointer → no link, silently (an epic without a design doc is a valid, expected object — that generality is the point of the ask).

3. Stamp the pointer automatically for future epics (B7, C1 — a convention nobody has to remember). _resolve_epic in design_docs/tools/operationalize_design.py:327 creates the epic on --apply; pass a body containing the **Design doc:** line (the GitHub blob URL of the doc being operationalized, https://github.com/cfsh/ninefold/blob/main/design_docs/<name>) via nflib.bd.Bd.create(body_file=...) (tools/nflib/bd.py:163 already takes it). Reuse path (already-stamped doc): leave the epic untouched — the migration below covers the live ones.

4. One-off data migration (by hand or a tiny script in the PR, then discarded): both live epic descriptions are empty (verified), so wholesale -d is safe: - bd update nf-lbh -d '**Design doc:** https://github.com/cfsh/ninefold/blob/main/design_docs/005b-affected-probe-selection.md' - bd update nf-758 -d '**Design doc:** https://github.com/cfsh/ninefold/blob/main/design_docs/012-jj-and-the-merge-pipeline.md' (nf-tvc is closed; stamping it is optional, harmless, and nice for the /beads archive view.)

5. Delete the dead code, as its own mechanical PR (E3, B12). After the swap, the homepage's Contents-API path is unreachable: fetch_design_task_panels, _recording_gh, render_design_task_panels, _design_task_panel, _beads_pointer, _row_bd_id, and the github_live imports used only by it (parse_task_table, contents_dir/contents_files usage). Check what server_test.py::StandaloneTests' allowlist and redeploy.py's copy list still need — github_live may drop out of server.py entirely (the /issue/<id> PR-reference path uses the local Gh, not github_live). Do NOT fold in nf-sge6's wider github_live cleanup — that issue owns it.

Not touched / adjacent

- nf-amkl (async homepage sections, in_progress) also edits _serve_index — whoever lands second rebases; this change makes the epic section cheap enough that it may not need async at all, which is worth a comment on nf-amkl when this lands. - design_docs/tools/status.py (dev.py designs, the terminal view) keeps its doc-driven algorithm — it runs in a checkout and is out of scope here.

Test plan

- tools/web_frontend/server_test.py (unittest, the presubmit python lane): _design_doc_pointer extraction (present / absent / mid-description); homepage fetch returns only non-closed epics (FakeBd with an open + a closed epic); _bd_epic_panel renders the doc link when the description has the pointer and no <a> when it doesn't; render_board_page likewise. - web_golden_test.py: extend test_data/bd_fixture.jsonl with a **Design doc:**-stamped epic, regenerate via ./tools/web_frontend/generate_web_golden.py, review the diff before committing (the golden is expected to change — that's its job). - operationalize_design_test.py: --apply on a fresh doc creates the epic with the pointer in its description. - ./presubmit.py — all lanes. - Live (E5): after redeploy, / shows 005b + 012 panels each with a working Doc link and no 011; /board/nf-758 shows the doc link in the header. Screenshot or curl output in the PR body.

PR shape

A stack of two (E3): PR1 behaviour — homepage swap + pointer extract/render + operationalize stamp + migration + tests. PR2 mechanical — dead-code deletion. Splitting PR1 further (render vs stamp) is the dev's call.

Open questions, with defaults

- Hide closed epics on /beads too? Default: no/beads stays the full-workspace view (its --all is deliberate, server.py:206-211, so the closed column and the archive stay visible); the homepage is the "what's open" view. The remaining / vs /beads difference is then exactly "closed epics", by design, not by accident. - **Stamp the pointer into an existing epic on the --apply reuse path when missing? Default: no** — keep the tool's write surface minimal (B4); the migration covers the two live epics. - Homepage section link to the design_docs folder (nf-s8op's proposed static link)? Default: no — superseded; per-epic links are what Aedan asked for here.

Add a comment